diff --git a/sample/BenchmarkServer/Controllers/ProductsController.cs b/sample/BenchmarkServer/Controllers/ProductsController.cs index 83a43a53c..3c5d88d48 100644 --- a/sample/BenchmarkServer/Controllers/ProductsController.cs +++ b/sample/BenchmarkServer/Controllers/ProductsController.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -11,92 +11,91 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using ODataPerformanceProfile.Models; -namespace ODataPerformanceProfile.Controllers +namespace ODataPerformanceProfile.Controllers; + +public class ProductsController : ODataController { - public class ProductsController : ODataController - { - private ProductsContext _context; - private IList products; + private ProductsContext _context; + private IList products; - public ProductsController(ProductsContext context) - { - _context = context; - products = new List(); + public ProductsController(ProductsContext context) + { + _context = context; + products = new List(); - for (int i = 1; i < 3000; i++) + for (int i = 1; i < 3000; i++) + { + var prod = new Product() { - var prod = new Product() + Id = i, + Category = "Goods" + i, + Color = Color.Red, + Others = new List {"Others1", "Others2", "Others3"}, + CreatedDate = new DateTimeOffset(2001, 4, 15, 16, 24, 8, TimeSpan.FromHours(-8)), + UpdatedDate = new DateTimeOffset(2011, 2, 15, 16, 24, 8, TimeSpan.FromHours(-8)), + Detail = new ProductDetail { Id = "Id" + i, Info = "Info" + i }, + ProductOrders = new List { + new Order + { + Id = i, + OrderNo = "Order"+i + } + }, + ProductSuppliers = new List { - Id = i, - Category = "Goods" + i, - Color = Color.Red, - Others = new List {"Others1", "Others2", "Others3"}, - CreatedDate = new DateTimeOffset(2001, 4, 15, 16, 24, 8, TimeSpan.FromHours(-8)), - UpdatedDate = new DateTimeOffset(2011, 2, 15, 16, 24, 8, TimeSpan.FromHours(-8)), - Detail = new ProductDetail { Id = "Id" + i, Info = "Info" + i }, - ProductOrders = new List { - new Order - { - Id = i, - OrderNo = "Order"+i - } - }, - ProductSuppliers = new List + new Supplier { - new Supplier + Id = i, + Name = "Supplier"+i, + Description = "SupplierDesc"+i, + SupplierAddress = new Location { - Id = i, - Name = "Supplier"+i, - Description = "SupplierDesc"+i, - SupplierAddress = new Location - { - City = "SupCity"+i, - Address = "SupAddre"+i - } + City = "SupCity"+i, + Address = "SupAddre"+i } - }, - Properties = new Dictionary - { - { "Prop1", new DateTimeOffset(2014, 7, 3, 0, 0, 0, 0, new TimeSpan(0))}, - { "Prop2", new [] { "Leonard G. Lobel", "Eric D. Boyd" }}, - { "Prop3", "Others"} } - }; + }, + Properties = new Dictionary + { + { "Prop1", new DateTimeOffset(2014, 7, 3, 0, 0, 0, 0, new TimeSpan(0))}, + { "Prop2", new [] { "Leonard G. Lobel", "Eric D. Boyd" }}, + { "Prop3", "Others"} + } + }; - products.Add(prod); - } + products.Add(prod); } + } - [HttpGet] - [EnableQuery(PageSize = 3000)] - public IActionResult Get() - { - return Ok(products); - } + [HttpGet] + [EnableQuery(PageSize = 3000)] + public IActionResult Get() + { + return Ok(products); + } - [HttpGet("odata/Products/mostRecent()")] - public IActionResult MostRecent() - { - var maxProductId = products.Max(x => x.Id); - return Ok(maxProductId); - } + [HttpGet("odata/Products/mostRecent()")] + public IActionResult MostRecent() + { + var maxProductId = products.Max(x => x.Id); + return Ok(maxProductId); + } - [HttpPost("odata/Products({key})/Rate")] - public IActionResult Rate([FromODataUri] string key, ODataActionParameters parameters) + [HttpPost("odata/Products({key})/Rate")] + public IActionResult Rate([FromODataUri] string key, ODataActionParameters parameters) + { + if (!ModelState.IsValid) { - if (!ModelState.IsValid) - { - return BadRequest(); - } - - int rating = (int)parameters["rating"]; + return BadRequest(); + } - if (rating < 0) - { - return BadRequest(); - } + int rating = (int)parameters["rating"]; - return Ok(new ProductRating() { Id = key, Rating = rating }); + if (rating < 0) + { + return BadRequest(); } + + return Ok(new ProductRating() { Id = key, Rating = rating }); } } diff --git a/sample/BenchmarkServer/EdmModelBuilder.cs b/sample/BenchmarkServer/EdmModelBuilder.cs index c4c8380fd..d5dc0d266 100644 --- a/sample/BenchmarkServer/EdmModelBuilder.cs +++ b/sample/BenchmarkServer/EdmModelBuilder.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -9,29 +9,28 @@ using Microsoft.OData.ModelBuilder; using ODataPerformanceProfile.Models; -namespace ODataPerformanceProfile +namespace ODataPerformanceProfile; + +public static class EdmModelBuilder { - public static class EdmModelBuilder + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Products"); - builder.EntitySet("Suppliers"); - builder.EntitySet("Orders"); + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Products"); + builder.EntitySet("Suppliers"); + builder.EntitySet("Orders"); - builder.EntityType().Collection - .Function("mostRecent") - .Returns(); + builder.EntityType().Collection + .Function("mostRecent") + .Returns(); - builder.EntityType() - .Action("rate") - .Parameter("rating"); + builder.EntityType() + .Action("rate") + .Parameter("rating"); - var model = builder.GetEdmModel(); - model.MarkAsImmutable(); + var model = builder.GetEdmModel(); + model.MarkAsImmutable(); - return model; - } + return model; } } diff --git a/sample/BenchmarkServer/Models/Product.cs b/sample/BenchmarkServer/Models/Product.cs index 36ebb2dd6..2ef933508 100644 --- a/sample/BenchmarkServer/Models/Product.cs +++ b/sample/BenchmarkServer/Models/Product.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -7,60 +7,59 @@ using System.ComponentModel.DataAnnotations; -namespace ODataPerformanceProfile.Models +namespace ODataPerformanceProfile.Models; + +public class Product { - public class Product - { - public int Id { get; set; } - public string Category { get; set; } - public Color Color { get; set; } - public IList Others { get; set; } - public DateTimeOffset CreatedDate { get; set; } + public int Id { get; set; } + public string Category { get; set; } + public Color Color { get; set; } + public IList Others { get; set; } + public DateTimeOffset CreatedDate { get; set; } - [ConcurrencyCheck] - public DateTimeOffset? UpdatedDate { get; set; } - public virtual ProductDetail Detail { get; set; } - public virtual ICollection ProductSuppliers { get; set; } - public virtual ICollection ProductOrders { get; set; } - public IDictionary Properties { get; set; } - } + [ConcurrencyCheck] + public DateTimeOffset? UpdatedDate { get; set; } + public virtual ProductDetail Detail { get; set; } + public virtual ICollection ProductSuppliers { get; set; } + public virtual ICollection ProductOrders { get; set; } + public IDictionary Properties { get; set; } +} - public class ProductDetail - { - public string Id { get; set; } - public string Info { get; set; } - } +public class ProductDetail +{ + public string Id { get; set; } + public string Info { get; set; } +} - public class Supplier - { - public int Id { get; set; } - public string Name { get; set; } - public string Description { get; set; } - public Location SupplierAddress { get; set; } - } +public class Supplier +{ + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public Location SupplierAddress { get; set; } +} - public enum Color - { - Red, - Green, - Blue - } +public enum Color +{ + Red, + Green, + Blue +} - public class Location - { - public string City { get; set; } - public string Address { get; set; } - } +public class Location +{ + public string City { get; set; } + public string Address { get; set; } +} - public class Order - { - public int Id { get; set; } - public string OrderNo { get; set; } - } +public class Order +{ + public int Id { get; set; } + public string OrderNo { get; set; } +} - public class ProductRating - { - public string Id { get; set; } - public int Rating { get; set; } - } +public class ProductRating +{ + public string Id { get; set; } + public int Rating { get; set; } } diff --git a/sample/BenchmarkServer/Models/ProductsContext.cs b/sample/BenchmarkServer/Models/ProductsContext.cs index 627ae40ae..67474fd67 100644 --- a/sample/BenchmarkServer/Models/ProductsContext.cs +++ b/sample/BenchmarkServer/Models/ProductsContext.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -7,23 +7,22 @@ using Microsoft.EntityFrameworkCore; -namespace ODataPerformanceProfile.Models +namespace ODataPerformanceProfile.Models; + +public class ProductsContext : DbContext { - public class ProductsContext : DbContext + public ProductsContext(DbContextOptions options) + : base(options) { - public ProductsContext(DbContextOptions options) - : base(options) - { - } + } - public DbSet Products { get; set; } - public DbSet Suppliers { get; set; } - public DbSet Orders { get; set; } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().HasMany(a => a.ProductSuppliers); - modelBuilder.Entity().HasMany(a => a.ProductOrders); - modelBuilder.Entity().OwnsOne(c => c.SupplierAddress).WithOwner(); - } + public DbSet Products { get; set; } + public DbSet Suppliers { get; set; } + public DbSet Orders { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasMany(a => a.ProductSuppliers); + modelBuilder.Entity().HasMany(a => a.ProductOrders); + modelBuilder.Entity().OwnsOne(c => c.SupplierAddress).WithOwner(); } } diff --git a/sample/ODataAlternateKeySample/Controllers/CustomersController.cs b/sample/ODataAlternateKeySample/Controllers/CustomersController.cs index 8cd6c2874..171a22a4d 100644 --- a/sample/ODataAlternateKeySample/Controllers/CustomersController.cs +++ b/sample/ODataAlternateKeySample/Controllers/CustomersController.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -11,71 +11,70 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using ODataAlternateKeySample.Models; -namespace ODataAlternateKeySample.Controllers +namespace ODataAlternateKeySample.Controllers; + +public class CustomersController : ODataController { - public class CustomersController : ODataController + private readonly IAlternateKeyRepository _repository; + + public CustomersController(IAlternateKeyRepository repository) { - private readonly IAlternateKeyRepository _repository; + _repository = repository; + } - public CustomersController(IAlternateKeyRepository repository) - { - _repository = repository; - } + [HttpGet] + [EnableQuery] + public IActionResult Get() + { + return Ok(_repository.GetCustomers()); + } - [HttpGet] - [EnableQuery] - public IActionResult Get() + [HttpGet] + [EnableQuery] + public IActionResult Get(int key) + { + var c = _repository.GetCustomers().FirstOrDefault(c => c.Id == key); + if (c == null) { - return Ok(_repository.GetCustomers()); + return NotFound(); } - [HttpGet] - [EnableQuery] - public IActionResult Get(int key) - { - var c = _repository.GetCustomers().FirstOrDefault(c => c.Id == key); - if (c == null) - { - return NotFound(); - } + return Ok(c); + } - return Ok(c); - } + [HttpPost] + [EnableQuery] + public IActionResult Post([FromBody]Customer c) + { + return Ok(c); + } - [HttpPost] - [EnableQuery] - public IActionResult Post([FromBody]Customer c) + // Alternate key: SSN + [HttpGet("odata/Customers(SSN={ssn})")] // use community alternate key + [HttpGet("odata/Customers(CoreSN={ssn})")] // use core alternate key + public IActionResult GetCustomerBySSN(string ssn) + { + ssn = ssn.Replace("%", "%25"); + var c = _repository.GetCustomers().FirstOrDefault(c => c.SSN == ssn); + if (c == null) { - return Ok(c); + return NotFound(); } - // Alternate key: SSN - [HttpGet("odata/Customers(SSN={ssn})")] // use community alternate key - [HttpGet("odata/Customers(CoreSN={ssn})")] // use core alternate key - public IActionResult GetCustomerBySSN(string ssn) - { - ssn = ssn.Replace("%", "%25"); - var c = _repository.GetCustomers().FirstOrDefault(c => c.SSN == ssn); - if (c == null) - { - return NotFound(); - } - - return Ok(c); - } + return Ok(c); + } - [HttpPatch("odata/Customers(SSN={ssnKey})")] // use community alternate key - [HttpPatch("odata/Customers(CoreSN={ssnKey})")] // use core alternate key - public IActionResult PatchCustomerBySSN(string ssnKey, Delta delta) + [HttpPatch("odata/Customers(SSN={ssnKey})")] // use community alternate key + [HttpPatch("odata/Customers(CoreSN={ssnKey})")] // use core alternate key + public IActionResult PatchCustomerBySSN(string ssnKey, Delta delta) + { + var originalCustomer = _repository.GetCustomers().FirstOrDefault(c => c.SSN == ssnKey); + if (originalCustomer == null) { - var originalCustomer = _repository.GetCustomers().FirstOrDefault(c => c.SSN == ssnKey); - if (originalCustomer == null) - { - return NotFound(); - } - - delta.Patch(originalCustomer); - return Updated(originalCustomer); + return NotFound(); } + + delta.Patch(originalCustomer); + return Updated(originalCustomer); } } diff --git a/sample/ODataAlternateKeySample/Controllers/OrdersController.cs b/sample/ODataAlternateKeySample/Controllers/OrdersController.cs index 5c921ce00..15255dbcc 100644 --- a/sample/ODataAlternateKeySample/Controllers/OrdersController.cs +++ b/sample/ODataAlternateKeySample/Controllers/OrdersController.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -10,63 +10,62 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using ODataAlternateKeySample.Models; -namespace ODataAlternateKeySample.Controllers +namespace ODataAlternateKeySample.Controllers; + +public class OrdersController : ODataController { - public class OrdersController : ODataController + private readonly IAlternateKeyRepository _repository; + + public OrdersController(IAlternateKeyRepository repository) { - private readonly IAlternateKeyRepository _repository; + _repository = repository; + } - public OrdersController(IAlternateKeyRepository repository) - { - _repository = repository; - } + [HttpGet] + [EnableQuery] + public IActionResult Get() + { + return Ok(_repository.GetOrders()); + } - [HttpGet] - [EnableQuery] - public IActionResult Get() + [HttpGet] + [EnableQuery] + public IActionResult Get(int key) + { + var c = _repository.GetOrders().FirstOrDefault(c => c.Id == key); + if (c == null) { - return Ok(_repository.GetOrders()); + return NotFound(); } - [HttpGet] - [EnableQuery] - public IActionResult Get(int key) - { - var c = _repository.GetOrders().FirstOrDefault(c => c.Id == key); - if (c == null) - { - return NotFound(); - } - - return Ok(c); - } + return Ok(c); + } - // alternate key: Name - [HttpGet("odata/Orders(Name={orderName})")] // use community alternate key - [HttpGet("odata/Orders(CoreName={orderName})")] // use core alternate key - public IActionResult GetOrderByName(string orderName) + // alternate key: Name + [HttpGet("odata/Orders(Name={orderName})")] // use community alternate key + [HttpGet("odata/Orders(CoreName={orderName})")] // use core alternate key + public IActionResult GetOrderByName(string orderName) + { + var c = _repository.GetOrders().FirstOrDefault(c => c.Name == orderName); + if (c == null) { - var c = _repository.GetOrders().FirstOrDefault(c => c.Name == orderName); - if (c == null) - { - return NotFound(); - } - - return Ok(c); + return NotFound(); } - // alternate key: Token - [HttpGet("odata/Orders(Token={token})")] // use community alternate key - [HttpGet("odata/Orders(CoreToken={token})")] // use core alternate key - public IActionResult GetOrderByToken(Guid token) - { - var c = _repository.GetOrders().FirstOrDefault(c => c.Token == token); - if (c == null) - { - return NotFound(); - } + return Ok(c); + } - return Ok(c); + // alternate key: Token + [HttpGet("odata/Orders(Token={token})")] // use community alternate key + [HttpGet("odata/Orders(CoreToken={token})")] // use core alternate key + public IActionResult GetOrderByToken(Guid token) + { + var c = _repository.GetOrders().FirstOrDefault(c => c.Token == token); + if (c == null) + { + return NotFound(); } + + return Ok(c); } } diff --git a/sample/ODataAlternateKeySample/Controllers/PeopleController.cs b/sample/ODataAlternateKeySample/Controllers/PeopleController.cs index 43454a58b..91988ba48 100644 --- a/sample/ODataAlternateKeySample/Controllers/PeopleController.cs +++ b/sample/ODataAlternateKeySample/Controllers/PeopleController.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -10,48 +10,47 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using ODataAlternateKeySample.Models; -namespace ODataAlternateKeySample.Controllers +namespace ODataAlternateKeySample.Controllers; + +public class PeopleController : ODataController { - public class PeopleController : ODataController + private readonly IAlternateKeyRepository _repository; + + public PeopleController(IAlternateKeyRepository repository) { - private readonly IAlternateKeyRepository _repository; + _repository = repository; + } - public PeopleController(IAlternateKeyRepository repository) - { - _repository = repository; - } + [HttpGet] + [EnableQuery] + public IActionResult Get() + { + return Ok(_repository.GetPeople()); + } - [HttpGet] - [EnableQuery] - public IActionResult Get() + [HttpGet] + [EnableQuery] + public IActionResult Get(int key) + { + var c = _repository.GetPeople().FirstOrDefault(c => c.Id == key); + if (c == null) { - return Ok(_repository.GetPeople()); + return NotFound(); } - [HttpGet] - [EnableQuery] - public IActionResult Get(int key) - { - var c = _repository.GetPeople().FirstOrDefault(c => c.Id == key); - if (c == null) - { - return NotFound(); - } - - return Ok(c); - } + return Ok(c); + } - [HttpGet("odata/People(c_or_r={cr},passport={passport})")] // use community alternate key - [HttpGet("odata/People(core_c_r={cr},core_passport={passport})")] // use core alternate key - public IActionResult FindPeopleByCountryAndPassport(string cr, string passport) + [HttpGet("odata/People(c_or_r={cr},passport={passport})")] // use community alternate key + [HttpGet("odata/People(core_c_r={cr},core_passport={passport})")] // use core alternate key + public IActionResult FindPeopleByCountryAndPassport(string cr, string passport) + { + var c = _repository.GetPeople().FirstOrDefault(c => c.CountryOrRegion == cr && c.Passport == passport); + if (c == null) { - var c = _repository.GetPeople().FirstOrDefault(c => c.CountryOrRegion == cr && c.Passport == passport); - if (c == null) - { - return NotFound(); - } - - return Ok(c); + return NotFound(); } + + return Ok(c); } } diff --git a/sample/ODataAlternateKeySample/Controllers/WeatherForecastController.cs b/sample/ODataAlternateKeySample/Controllers/WeatherForecastController.cs index f56955089..579012198 100644 --- a/sample/ODataAlternateKeySample/Controllers/WeatherForecastController.cs +++ b/sample/ODataAlternateKeySample/Controllers/WeatherForecastController.cs @@ -1,33 +1,32 @@ using Microsoft.AspNetCore.Mvc; -namespace ODataAlternateKeySample.Controllers +namespace ODataAlternateKeySample.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase { - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase + private static readonly string[] Summaries = new[] { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; - private readonly ILogger _logger; + private readonly ILogger _logger; - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } - [HttpGet] - public IEnumerable Get() + [HttpGet] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } + Date = DateTime.Now.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); } -} \ No newline at end of file +} diff --git a/sample/ODataAlternateKeySample/Models/AlternateKeyRepositoryInMemory.cs b/sample/ODataAlternateKeySample/Models/AlternateKeyRepositoryInMemory.cs index d2d6a686c..97aabcaba 100644 --- a/sample/ODataAlternateKeySample/Models/AlternateKeyRepositoryInMemory.cs +++ b/sample/ODataAlternateKeySample/Models/AlternateKeyRepositoryInMemory.cs @@ -1,66 +1,65 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. // //------------------------------------------------------------------------------ -namespace ODataAlternateKeySample.Models +namespace ODataAlternateKeySample.Models; + +public class AlternateKeyRepositoryInMemory : IAlternateKeyRepository { - public class AlternateKeyRepositoryInMemory : IAlternateKeyRepository - { - private static IList _customers; - private static IList _orders; - private static IList _people; + private static IList _customers; + private static IList _orders; + private static IList _people; - static AlternateKeyRepositoryInMemory() + static AlternateKeyRepositoryInMemory() + { + // Customers + var names = new[] { "Tom", "Jerry", "Mike", "Ben", "Sam", "Peter" }; + _customers = Enumerable.Range(1, 5).Select((e, i) => new Customer { - // Customers - var names = new[] { "Tom", "Jerry", "Mike", "Ben", "Sam", "Peter" }; - _customers = Enumerable.Range(1, 5).Select((e, i) => new Customer - { - Id = e, - Name = names[e - 1], - SSN = i % 2 == 0 ? "SSN-" + e + "-" + (100 + e) : "SSN-%25-" + e + "-" + (100 + e), - Titles = new string[] { "abc", null, "efg" } - }).ToList(); + Id = e, + Name = names[e - 1], + SSN = i % 2 == 0 ? "SSN-" + e + "-" + (100 + e) : "SSN-%25-" + e + "-" + (100 + e), + Titles = new string[] { "abc", null, "efg" } + }).ToList(); - // Orders - Guid[] tokes = - { - new Guid("196B3584-EF3D-41FD-90B4-76D59F9B929C"), - new Guid("6CED5600-28BA-40EE-A2DF-E80AFADBE6C7"), - new Guid("75036B94-C836-4946-8CC8-054CF54060EC"), - new Guid("B3FF5460-6E77-4678-B959-DCC1C4937FA7"), - new Guid("ED773C85-4E3C-4FC4-A3E9-9F1DA0A626DA"), - new Guid("E9CC3D9F-BC80-4D43-8C3E-ED38E8C9A8B6") - }; + // Orders + Guid[] tokes = + { + new Guid("196B3584-EF3D-41FD-90B4-76D59F9B929C"), + new Guid("6CED5600-28BA-40EE-A2DF-E80AFADBE6C7"), + new Guid("75036B94-C836-4946-8CC8-054CF54060EC"), + new Guid("B3FF5460-6E77-4678-B959-DCC1C4937FA7"), + new Guid("ED773C85-4E3C-4FC4-A3E9-9F1DA0A626DA"), + new Guid("E9CC3D9F-BC80-4D43-8C3E-ED38E8C9A8B6") + }; - _orders = Enumerable.Range(1, 6).Select(e => new Order - { - Id = e, - Name = string.Format("Order-{0}", e), - Token = tokes[e - 1], - Amount = 10 * (e + 1) - e, - Price = 8 * e - }).ToList(); + _orders = Enumerable.Range(1, 6).Select(e => new Order + { + Id = e, + Name = string.Format("Order-{0}", e), + Token = tokes[e - 1], + Amount = 10 * (e + 1) - e, + Price = 8 * e + }).ToList(); - // People - var cs = new[] { "EN", "CN", "USA", "RU", "JP", "KO" }; - var ps = new[] { "1001", "2010", "9999", "3199992", "00001", "8110" }; - _people = Enumerable.Range(1, 6).Select(e => new Person - { - Id = e, - Name = names[e - 1], - CountryOrRegion = cs[e - 1], - Passport = ps[e - 1] - }).ToList(); - } + // People + var cs = new[] { "EN", "CN", "USA", "RU", "JP", "KO" }; + var ps = new[] { "1001", "2010", "9999", "3199992", "00001", "8110" }; + _people = Enumerable.Range(1, 6).Select(e => new Person + { + Id = e, + Name = names[e - 1], + CountryOrRegion = cs[e - 1], + Passport = ps[e - 1] + }).ToList(); + } - public IEnumerable GetCustomers() => _customers; + public IEnumerable GetCustomers() => _customers; - public IEnumerable GetOrders() => _orders; + public IEnumerable GetOrders() => _orders; - public IEnumerable GetPeople() => _people; - } + public IEnumerable GetPeople() => _people; } diff --git a/sample/ODataAlternateKeySample/Models/Customer.cs b/sample/ODataAlternateKeySample/Models/Customer.cs index d19164ed3..a9e301e5c 100644 --- a/sample/ODataAlternateKeySample/Models/Customer.cs +++ b/sample/ODataAlternateKeySample/Models/Customer.cs @@ -1,23 +1,22 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. // //------------------------------------------------------------------------------ -namespace ODataAlternateKeySample.Models +namespace ODataAlternateKeySample.Models; + +/// +/// Entity type with one alternate key +/// +public class Customer { - /// - /// Entity type with one alternate key - /// - public class Customer - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public string SSN { get; set; } + public string SSN { get; set; } - public string[] Titles { get; set; } - } + public string[] Titles { get; set; } } diff --git a/sample/ODataAlternateKeySample/Models/EdmModelBuilder.cs b/sample/ODataAlternateKeySample/Models/EdmModelBuilder.cs index 5fd0fd6a1..6b81bcd40 100644 --- a/sample/ODataAlternateKeySample/Models/EdmModelBuilder.cs +++ b/sample/ODataAlternateKeySample/Models/EdmModelBuilder.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -10,108 +10,107 @@ using Microsoft.OData.Edm.Vocabularies; using Microsoft.OData.ModelBuilder; -namespace ODataAlternateKeySample.Models +namespace ODataAlternateKeySample.Models; + +public class EdmModelBuilder { - public class EdmModelBuilder + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); + builder.EntitySet("People"); + + EdmModel model = builder.GetEdmModel() as EdmModel; + + SetCustomerAlternateKey(model); + SetOrderAlternateKey(model); + SetPersonAlternateKey(model); + return model; + } + + private static void SetCustomerAlternateKey(EdmModel model) + { + // Add one alternate key using the extension method. + // It's using 'OData.Community.Keys.V1.AlternateKeys' + var customer = model.SchemaElements.OfType().First(c => c.Name == "Customer"); + var ssn = customer.FindProperty("SSN"); + + model.AddAlternateKeyAnnotation(customer, new Dictionary { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Orders"); - builder.EntitySet("People"); + {"SSN", ssn} + }); - EdmModel model = builder.GetEdmModel() as EdmModel; + // Use Core Vocabulary version. + model.AddAlternateKeyAnnotation(customer, new Dictionary() + { + {"CoreSN", ssn} + }, + true /*true means to use core.alternatekeys term*/); + } - SetCustomerAlternateKey(model); - SetOrderAlternateKey(model); - SetPersonAlternateKey(model); - return model; - } + private static void SetOrderAlternateKey(EdmModel model) + { + // Add multiple alternate keys using the Term/annotation methods. + // It's using 'Org.OData.Core.V1.AlternateKeys' + var order = model.SchemaElements.OfType().First(c => c.Name == "Order"); - private static void SetCustomerAlternateKey(EdmModel model) + var name = order.FindProperty("Name"); + model.AddAlternateKeyAnnotation(order, new Dictionary { - // Add one alternate key using the extension method. - // It's using 'OData.Community.Keys.V1.AlternateKeys' - var customer = model.SchemaElements.OfType().First(c => c.Name == "Customer"); - var ssn = customer.FindProperty("SSN"); - - model.AddAlternateKeyAnnotation(customer, new Dictionary - { - {"SSN", ssn} - }); - - // Use Core Vocabulary version. - model.AddAlternateKeyAnnotation(customer, new Dictionary() - { - {"CoreSN", ssn} - }, - true /*true means to use core.alternatekeys term*/); - } + {"Name", name}, + }); - private static void SetOrderAlternateKey(EdmModel model) + var token = order.FindProperty("Token"); + model.AddAlternateKeyAnnotation(order, new Dictionary { - // Add multiple alternate keys using the Term/annotation methods. - // It's using 'Org.OData.Core.V1.AlternateKeys' - var order = model.SchemaElements.OfType().First(c => c.Name == "Order"); - - var name = order.FindProperty("Name"); - model.AddAlternateKeyAnnotation(order, new Dictionary - { - {"Name", name}, - }); - - var token = order.FindProperty("Token"); - model.AddAlternateKeyAnnotation(order, new Dictionary - { - {"Token", token}, - }); - - // Use the APIs to build the core alternate keys - var alternateKeysCollection = new List(); - foreach (string item in new [] { "Name", "Token"}) - { - List propertyRefs = new List(); - - IEdmRecordExpression propertyRef = new EdmRecordExpression( - new EdmPropertyConstructor("Alias", new EdmStringConstant($"Core{item}")), - new EdmPropertyConstructor("Name", new EdmPropertyPathExpression(item))); - propertyRefs.Add(propertyRef); - - EdmRecordExpression alternateKeyRecord = new EdmRecordExpression( - new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); - - alternateKeysCollection.Add(alternateKeyRecord); - } - - var term = model.FindTerm("Org.OData.Core.V1.AlternateKeys"); - var annotation = new EdmVocabularyAnnotation(order, term, new EdmCollectionExpression(alternateKeysCollection)); - - annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); - model.SetVocabularyAnnotation(annotation); - } + {"Token", token}, + }); - private static void SetPersonAlternateKey(EdmModel model) + // Use the APIs to build the core alternate keys + var alternateKeysCollection = new List(); + foreach (string item in new [] { "Name", "Token"}) { - // Add composed alternate keys using the Term/annotation methods. - // It's using 'Org.OData.Core.V1.AlternateKeys' - var person = model.SchemaElements.OfType().First(c => c.Name == "Person"); - var cr = person.FindProperty("CountryOrRegion"); - var passport = person.FindProperty("Passport"); - - model.AddAlternateKeyAnnotation(person, new Dictionary - { - {"c_or_r", cr}, - {"passport", passport}, - }); - - // Use Core Vocabulary version. - model.AddAlternateKeyAnnotation(person, new Dictionary - { - {"core_c_r", cr}, - {"core_passport", passport}, - }, - true); + List propertyRefs = new List(); + + IEdmRecordExpression propertyRef = new EdmRecordExpression( + new EdmPropertyConstructor("Alias", new EdmStringConstant($"Core{item}")), + new EdmPropertyConstructor("Name", new EdmPropertyPathExpression(item))); + propertyRefs.Add(propertyRef); + + EdmRecordExpression alternateKeyRecord = new EdmRecordExpression( + new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); + + alternateKeysCollection.Add(alternateKeyRecord); } + + var term = model.FindTerm("Org.OData.Core.V1.AlternateKeys"); + var annotation = new EdmVocabularyAnnotation(order, term, new EdmCollectionExpression(alternateKeysCollection)); + + annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); + model.SetVocabularyAnnotation(annotation); + } + + private static void SetPersonAlternateKey(EdmModel model) + { + // Add composed alternate keys using the Term/annotation methods. + // It's using 'Org.OData.Core.V1.AlternateKeys' + var person = model.SchemaElements.OfType().First(c => c.Name == "Person"); + var cr = person.FindProperty("CountryOrRegion"); + var passport = person.FindProperty("Passport"); + + model.AddAlternateKeyAnnotation(person, new Dictionary + { + {"c_or_r", cr}, + {"passport", passport}, + }); + + // Use Core Vocabulary version. + model.AddAlternateKeyAnnotation(person, new Dictionary + { + {"core_c_r", cr}, + {"core_passport", passport}, + }, + true); } } diff --git a/sample/ODataAlternateKeySample/Models/IAlternateKeyRepository.cs b/sample/ODataAlternateKeySample/Models/IAlternateKeyRepository.cs index b728f7ea6..276d401e2 100644 --- a/sample/ODataAlternateKeySample/Models/IAlternateKeyRepository.cs +++ b/sample/ODataAlternateKeySample/Models/IAlternateKeyRepository.cs @@ -1,18 +1,17 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. // //------------------------------------------------------------------------------ -namespace ODataAlternateKeySample.Models +namespace ODataAlternateKeySample.Models; + +public interface IAlternateKeyRepository { - public interface IAlternateKeyRepository - { - IEnumerable GetCustomers(); + IEnumerable GetCustomers(); - IEnumerable GetOrders(); + IEnumerable GetOrders(); - IEnumerable GetPeople(); - } + IEnumerable GetPeople(); } diff --git a/sample/ODataAlternateKeySample/Models/Order.cs b/sample/ODataAlternateKeySample/Models/Order.cs index 9103b7f3c..dae11bb1f 100644 --- a/sample/ODataAlternateKeySample/Models/Order.cs +++ b/sample/ODataAlternateKeySample/Models/Order.cs @@ -1,25 +1,24 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. // //------------------------------------------------------------------------------ -namespace ODataAlternateKeySample.Models +namespace ODataAlternateKeySample.Models; + +/// +/// Entity type with multiple alternate keys +/// +public class Order { - /// - /// Entity type with multiple alternate keys - /// - public class Order - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public Guid Token { get; set; } + public Guid Token { get; set; } - public decimal Price { get; set; } + public decimal Price { get; set; } - public int Amount { get; set; } - } + public int Amount { get; set; } } diff --git a/sample/ODataAlternateKeySample/Models/Person.cs b/sample/ODataAlternateKeySample/Models/Person.cs index 80c12fa68..1ad8436c1 100644 --- a/sample/ODataAlternateKeySample/Models/Person.cs +++ b/sample/ODataAlternateKeySample/Models/Person.cs @@ -1,23 +1,22 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. // //------------------------------------------------------------------------------ -namespace ODataAlternateKeySample.Models +namespace ODataAlternateKeySample.Models; + +/// +/// Entity type with composed alternate keys +/// +public class Person { - /// - /// Entity type with composed alternate keys - /// - public class Person - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public string CountryOrRegion { get; set; } + public string CountryOrRegion { get; set; } - public string Passport { get; set; } - } + public string Passport { get; set; } } diff --git a/sample/ODataAlternateKeySample/WeatherForecast.cs b/sample/ODataAlternateKeySample/WeatherForecast.cs index c749b4fcd..a09e909a6 100644 --- a/sample/ODataAlternateKeySample/WeatherForecast.cs +++ b/sample/ODataAlternateKeySample/WeatherForecast.cs @@ -1,13 +1,12 @@ -namespace ODataAlternateKeySample +namespace ODataAlternateKeySample; + +public class WeatherForecast { - public class WeatherForecast - { - public DateTime Date { get; set; } + public DateTime Date { get; set; } - public int TemperatureC { get; set; } + public int TemperatureC { get; set; } - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - public string Summary { get; set; } - } -} \ No newline at end of file + public string Summary { get; set; } +} diff --git a/sample/ODataCustomizedSample/Controllers/AnyController.cs b/sample/ODataCustomizedSample/Controllers/AnyController.cs index cb1f6be95..10ddf7a58 100644 --- a/sample/ODataCustomizedSample/Controllers/AnyController.cs +++ b/sample/ODataCustomizedSample/Controllers/AnyController.cs @@ -8,31 +8,30 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Routing.Attributes; -namespace ODataCustomizedSample.Controllers +namespace ODataCustomizedSample.Controllers; + +[Route("")] +[ODataAttributeRouting] +public class AnyController : ControllerBase { - [Route("")] - [ODataAttributeRouting] - public class AnyController : ControllerBase + [HttpGet("Players")] // ~Players + public IActionResult DoAnything() { - [HttpGet("Players")] // ~Players - public IActionResult DoAnything() - { - return Ok("DoAnything"); - } + return Ok("DoAnything"); + } - // Use Absolute route template - [HttpGet("/v{version}/Players")] // v{version}/Players - public IActionResult DoAnything(string version) - { - return Ok($"DoAnything at version={version}"); - } + // Use Absolute route template + [HttpGet("/v{version}/Players")] // v{version}/Players + public IActionResult DoAnything(string version) + { + return Ok($"DoAnything at version={version}"); + } - // Use Absolute route template - [HttpGet("/v{version}/Players/{playerKey}/Default.PlayPiano(kind={kind},name={name})")] - [HttpGet("/v{version}/Players/{playerKey}/PlayPiano(kind={kind},name={name})")] - public string DoPlayPiano(int playerKey, int kind, string name) - { - return $"Players{playerKey} do Play Piano (kind={kind},name={name}"; - } + // Use Absolute route template + [HttpGet("/v{version}/Players/{playerKey}/Default.PlayPiano(kind={kind},name={name})")] + [HttpGet("/v{version}/Players/{playerKey}/PlayPiano(kind={kind},name={name})")] + public string DoPlayPiano(int playerKey, int kind, string name) + { + return $"Players{playerKey} do Play Piano (kind={kind},name={name}"; } } diff --git a/sample/ODataCustomizedSample/Controllers/AnyODataController.cs b/sample/ODataCustomizedSample/Controllers/AnyODataController.cs index 823a0ab98..7f7ebb249 100644 --- a/sample/ODataCustomizedSample/Controllers/AnyODataController.cs +++ b/sample/ODataCustomizedSample/Controllers/AnyODataController.cs @@ -8,24 +8,23 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace ODataCustomizedSample.Controllers +namespace ODataCustomizedSample.Controllers; + +public class AnyODataController : ODataController { - public class AnyODataController : ODataController + [HttpGet("Players/$count")] // ~/players/$count + [Route("Players")] // It will combine as two routes: [Post,Patch] ~/Players + [HttpPost] + [HttpPatch] + public IActionResult Dosomething() { - [HttpGet("Players/$count")] // ~/players/$count - [Route("Players")] // It will combine as two routes: [Post,Patch] ~/Players - [HttpPost] - [HttpPatch] - public IActionResult Dosomething() - { - return Ok("Dosomething on Players -> AnyODataController"); - } + return Ok("Dosomething on Players -> AnyODataController"); + } - [HttpGet("v{version}/Players/{key}/PlayPiano(kind={k},name={n})")] - public IActionResult LetsPlayPiano(string version, int key, int k, string n) - { - string output = $"Players {key} are playing piano with kind={k} and name={n}, version={version} -> AnyODataController"; - return Ok(output); - } + [HttpGet("v{version}/Players/{key}/PlayPiano(kind={k},name={n})")] + public IActionResult LetsPlayPiano(string version, int key, int k, string n) + { + string output = $"Players {key} are playing piano with kind={k} and name={n}, version={version} -> AnyODataController"; + return Ok(output); } } diff --git a/sample/ODataCustomizedSample/Controllers/EnumsController.cs b/sample/ODataCustomizedSample/Controllers/EnumsController.cs index 01eee097c..627d97531 100644 --- a/sample/ODataCustomizedSample/Controllers/EnumsController.cs +++ b/sample/ODataCustomizedSample/Controllers/EnumsController.cs @@ -15,268 +15,267 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using ODataCustomizedSample.Models; -namespace ODataCustomizedSample.Controller +namespace ODataCustomizedSample.Controller; + +public class EmployeesController : ODataController { - public class EmployeesController : ODataController + public EmployeesController() { - public EmployeesController() + if (null == Employees) { - if (null == Employees) - { - InitEmployees(); - } + InitEmployees(); } + } - /// - /// static so that the data is shared among requests. - /// - private static IList Employees = null; + /// + /// static so that the data is shared among requests. + /// + private static IList Employees = null; - private void InitEmployees() + private void InitEmployees() + { + Employees = new List { - Employees = new List + new Employee() { - new Employee() + ID=1, + Name="Name1", + SkillSet=new List { Skill.CSharp, Skill.Sql }, + Gender=Gender.Female, + AccessLevel=AccessLevel.Execute, + FavoriteSports=new FavoriteSports() { - ID=1, - Name="Name1", - SkillSet=new List { Skill.CSharp, Skill.Sql }, - Gender=Gender.Female, - AccessLevel=AccessLevel.Execute, - FavoriteSports=new FavoriteSports() - { - LikeMost=Sport.Pingpong, - Like=new List{Sport.Pingpong,Sport.Basketball} - } - }, - new Employee() + LikeMost=Sport.Pingpong, + Like=new List{Sport.Pingpong,Sport.Basketball} + } + }, + new Employee() + { + ID=2,Name="Name2", + SkillSet=new List(), + Gender=Gender.Female, + AccessLevel=AccessLevel.Read, + FavoriteSports=new FavoriteSports() { - ID=2,Name="Name2", - SkillSet=new List(), - Gender=Gender.Female, - AccessLevel=AccessLevel.Read, - FavoriteSports=new FavoriteSports() - { - LikeMost=Sport.Pingpong, - Like=new List { Sport.Pingpong, Sport.Basketball } - } - }, - new Employee() + LikeMost=Sport.Pingpong, + Like=new List { Sport.Pingpong, Sport.Basketball } + } + }, + new Employee() + { + ID=3,Name="Name3", + SkillSet=new List { Skill.Web, Skill.Sql }, + Gender=Gender.Female, + AccessLevel=AccessLevel.Read|AccessLevel.Write, + FavoriteSports=new FavoriteSports() { - ID=3,Name="Name3", - SkillSet=new List { Skill.Web, Skill.Sql }, - Gender=Gender.Female, - AccessLevel=AccessLevel.Read|AccessLevel.Write, - FavoriteSports=new FavoriteSports() - { - LikeMost=Sport.Pingpong|Sport.Basketball, - Like=new List { Sport.Pingpong, Sport.Basketball } - } - }, - }; - } + LikeMost=Sport.Pingpong|Sport.Basketball, + Like=new List { Sport.Pingpong, Sport.Basketball } + } + }, + }; + } - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - public IActionResult Get() - { - return Ok(Employees.AsQueryable()); - } + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public IActionResult Get() + { + return Ok(Employees.AsQueryable()); + } - public IActionResult Get(int key) + public IActionResult Get(int key) + { + return Ok(Employees.SingleOrDefault(e => e.ID == key)); + } + + [HttpGet] + // public IActionResult FindAccessLevelFromEmployee(int key) + public IActionResult FindAccessLevel(int key) + { + if (key == 9) { - return Ok(Employees.SingleOrDefault(e => e.ID == key)); + // Special key to verify the function call + return Ok(AccessLevel.Execute); } - [HttpGet] - // public IActionResult FindAccessLevelFromEmployee(int key) - public IActionResult FindAccessLevel(int key) - { - if (key == 9) - { - // Special key to verify the function call - return Ok(AccessLevel.Execute); - } + return Ok(Employees.SingleOrDefault(e => e.ID == key).AccessLevel); + } - return Ok(Employees.SingleOrDefault(e => e.ID == key).AccessLevel); - } + public IActionResult GetNameFromEmployee(int key) + { + return Ok(Employees.SingleOrDefault(e => e.ID == key).Name); + } - public IActionResult GetNameFromEmployee(int key) - { - return Ok(Employees.SingleOrDefault(e => e.ID == key).Name); - } + [EnableQuery] + public IActionResult GetSkillSetFromEmployee(int key) + { + return Ok(Employees.SingleOrDefault(e => e.ID == key).SkillSet); + } - [EnableQuery] - public IActionResult GetSkillSetFromEmployee(int key) - { - return Ok(Employees.SingleOrDefault(e => e.ID == key).SkillSet); - } + [EnableQuery] + public IActionResult GetFavoriteSportsFromEmployee(int key) + { + var employee = Employees.SingleOrDefault(e => e.ID == key); + return Ok(employee.FavoriteSports); + } - [EnableQuery] - public IActionResult GetFavoriteSportsFromEmployee(int key) - { - var employee = Employees.SingleOrDefault(e => e.ID == key); - return Ok(employee.FavoriteSports); - } + [HttpGet("Employees({key})/FavoriteSports/LikeMost")] // non-OData routing + public IActionResult GetFavoriteSportLikeMost(int key) + { + var firstOrDefault = Employees.FirstOrDefault(e => e.ID == key); + return Ok(firstOrDefault.FavoriteSports.LikeMost); + } - [HttpGet("Employees({key})/FavoriteSports/LikeMost")] // non-OData routing - public IActionResult GetFavoriteSportLikeMost(int key) + public IActionResult Post([FromBody] Employee employee) + { + employee.ID = Employees.Count + 1; + Employees.Add(employee); + + return Created(employee); + } + + [HttpPost("Employees({key})/FavoriteSports/LikeMost")] // non-OData routing + public IActionResult PostToSkillSet(int key, [FromBody] Skill newSkill) + { + Employee employee = Employees.FirstOrDefault(e => e.ID == key); + if (employee == null) { - var firstOrDefault = Employees.FirstOrDefault(e => e.ID == key); - return Ok(firstOrDefault.FavoriteSports.LikeMost); + return NotFound(); } + employee.SkillSet.Add(newSkill); + return Updated(employee.SkillSet); + } - public IActionResult Post([FromBody] Employee employee) + public IActionResult Put(int key, [FromBody] Employee employee) + { + employee.ID = key; + Employee originalEmployee = Employees.SingleOrDefault(c => c.ID == key); + + if (originalEmployee == null) { - employee.ID = Employees.Count + 1; Employees.Add(employee); return Created(employee); } - [HttpPost("Employees({key})/FavoriteSports/LikeMost")] // non-OData routing - public IActionResult PostToSkillSet(int key, [FromBody] Skill newSkill) - { - Employee employee = Employees.FirstOrDefault(e => e.ID == key); - if (employee == null) - { - return NotFound(); - } - employee.SkillSet.Add(newSkill); - return Updated(employee.SkillSet); - } + Employees.Remove(originalEmployee); + Employees.Add(employee); + return Ok(employee); + } - public IActionResult Put(int key, [FromBody] Employee employee) - { - employee.ID = key; - Employee originalEmployee = Employees.SingleOrDefault(c => c.ID == key); + public IActionResult Patch(int key, [FromBody] Delta delta) + { + Employee originalEmployee = Employees.SingleOrDefault(c => c.ID == key); - if (originalEmployee == null) - { - Employees.Add(employee); + if (originalEmployee == null) + { + Employee temp = new Employee(); + delta.Patch(temp); + Employees.Add(temp); + return Created(temp); + } - return Created(employee); - } + delta.Patch(originalEmployee); + return Ok(delta); + } - Employees.Remove(originalEmployee); - Employees.Add(employee); - return Ok(employee); - } + public IActionResult Delete(int key) + { + IEnumerable appliedEmployees = Employees.Where(c => c.ID == key); - public IActionResult Patch(int key, [FromBody] Delta delta) + if (appliedEmployees.Count() == 0) { - Employee originalEmployee = Employees.SingleOrDefault(c => c.ID == key); - - if (originalEmployee == null) - { - Employee temp = new Employee(); - delta.Patch(temp); - Employees.Add(temp); - return Created(temp); - } - - delta.Patch(originalEmployee); - return Ok(delta); + return BadRequest(string.Format("The entry with ID {0} doesn't exist", key)); } - public IActionResult Delete(int key) + Employee employee = appliedEmployees.Single(); + Employees.Remove(employee); + return this.StatusCode(StatusCodes.Status204NoContent); + } + + [HttpPost] + public IActionResult AddSkill([FromODataUri] int key, [FromBody] ODataActionParameters parameters) + { + if (!ModelState.IsValid) { - IEnumerable appliedEmployees = Employees.Where(c => c.ID == key); + return BadRequest(); + } - if (appliedEmployees.Count() == 0) - { - return BadRequest(string.Format("The entry with ID {0} doesn't exist", key)); - } + Skill skill = (Skill)parameters["skill"]; - Employee employee = appliedEmployees.Single(); - Employees.Remove(employee); - return this.StatusCode(StatusCodes.Status204NoContent); + if (key == 6) + { + return Ok(); } - [HttpPost] - public IActionResult AddSkill([FromODataUri] int key, [FromBody] ODataActionParameters parameters) + Employee employee = Employees.FirstOrDefault(e => e.ID == key); + if (!employee.SkillSet.Contains(skill)) { - if (!ModelState.IsValid) - { - return BadRequest(); - } - - Skill skill = (Skill)parameters["skill"]; + employee.SkillSet.Add(skill); + } - if (key == 6) - { - return Ok(); - } + return Ok(employee.SkillSet); + } - Employee employee = Employees.FirstOrDefault(e => e.ID == key); - if (!employee.SkillSet.Contains(skill)) - { - employee.SkillSet.Add(skill); - } + [HttpPost("ResetDataSource")] // Non-OData Routing + public IActionResult ResetDataSource() + { + this.InitEmployees(); + return this.StatusCode(StatusCodes.Status204NoContent); + } - return Ok(employee.SkillSet); + [HttpPost("SetAccessLevel")] // Non-OData Routing + public IActionResult SetAccessLevel([FromBody] ODataActionParameters parameters) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); } - [HttpPost("ResetDataSource")] // Non-OData Routing - public IActionResult ResetDataSource() + int ID = (int)parameters["ID"]; + AccessLevel accessLevel = (AccessLevel)parameters["accessLevel"]; + if (accessLevel.HasFlag(AccessLevel.Read) && + accessLevel.HasFlag(AccessLevel.Execute) && + ID == 7) // special { - this.InitEmployees(); - return this.StatusCode(StatusCodes.Status204NoContent); + return Ok(AccessLevel.Read | AccessLevel.Write); } - [HttpPost("SetAccessLevel")] // Non-OData Routing - public IActionResult SetAccessLevel([FromBody] ODataActionParameters parameters) - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - int ID = (int)parameters["ID"]; - AccessLevel accessLevel = (AccessLevel)parameters["accessLevel"]; - if (accessLevel.HasFlag(AccessLevel.Read) && - accessLevel.HasFlag(AccessLevel.Execute) && - ID == 7) // special - { - return Ok(AccessLevel.Read | AccessLevel.Write); - } + Employee employee = Employees.FirstOrDefault(e => e.ID == ID); + employee.AccessLevel = accessLevel; + return Ok(employee.AccessLevel); + } - Employee employee = Employees.FirstOrDefault(e => e.ID == ID); - employee.AccessLevel = accessLevel; - return Ok(employee.AccessLevel); + [HttpGet] + public IActionResult GetAccessLevel([FromODataUri] int key) + { + if (!ModelState.IsValid) + { + return BadRequest(); } - [HttpGet] - public IActionResult GetAccessLevel([FromODataUri] int key) - { - if (!ModelState.IsValid) - { - return BadRequest(); - } + Employee employee = Employees.FirstOrDefault(e => e.ID == key); - Employee employee = Employees.FirstOrDefault(e => e.ID == key); + return Ok(employee.AccessLevel); + } - return Ok(employee.AccessLevel); + [HttpGet("HasAccessLevel(ID={id},AccessLevel={accessLevel})")] // non-odata routing + public IActionResult HasAccessLevel([FromODataUri] int id, [FromODataUri] AccessLevel accessLevel) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); } - [HttpGet("HasAccessLevel(ID={id},AccessLevel={accessLevel})")] // non-odata routing - public IActionResult HasAccessLevel([FromODataUri] int id, [FromODataUri] AccessLevel accessLevel) + if (id == 1 && accessLevel == AccessLevel.Read) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - if (id == 1 && accessLevel == AccessLevel.Read) - { - return Ok(false); - } - - if (id == 2 && accessLevel == AccessLevel.Read) - { - return Ok(true); - } + return Ok(false); + } - return BadRequest("Bad request!"); + if (id == 2 && accessLevel == AccessLevel.Read) + { + return Ok(true); } + + return BadRequest("Bad request!"); } } diff --git a/sample/ODataCustomizedSample/Controllers/GenericODataController.cs b/sample/ODataCustomizedSample/Controllers/GenericODataController.cs index a9cfd5ffe..a2b6b4c40 100644 --- a/sample/ODataCustomizedSample/Controllers/GenericODataController.cs +++ b/sample/ODataCustomizedSample/Controllers/GenericODataController.cs @@ -9,16 +9,15 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Query; -namespace ODataCustomizedSample.Controllers +namespace ODataCustomizedSample.Controllers; + +public class GenericODataController : ControllerBase { - public class GenericODataController : ControllerBase + [EnableQuery] + public List GetTest(string classname) { - [EnableQuery] - public List GetTest(string classname) - { - var y = new List { $"classname={classname}", "Customer", "Car", "School" }; + var y = new List { $"classname={classname}", "Customer", "Car", "School" }; - return y; - } + return y; } } diff --git a/sample/ODataCustomizedSample/Controllers/HandAbolusteController.cs b/sample/ODataCustomizedSample/Controllers/HandAbolusteController.cs index bfac84bde..9aca55e8a 100644 --- a/sample/ODataCustomizedSample/Controllers/HandAbolusteController.cs +++ b/sample/ODataCustomizedSample/Controllers/HandAbolusteController.cs @@ -8,19 +8,18 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Routing.Attributes; -namespace ODataCustomizedSample.Controllers +namespace ODataCustomizedSample.Controllers; + +[Route("convention")] +[Route("explicit")] +[Route("odata")] +public class HandAbolusteController : ControllerBase { - [Route("convention")] - [Route("explicit")] - [Route("odata")] - public class HandAbolusteController : ControllerBase + [ODataAttributeRouting] + [HttpGet("/explicit/Employees({key})/Goto(lat={lat},lon={lon})")] + [HttpGet("~/convention/Employees({key})/ODataCustomizedSample.Models.Goto(lat={lat},lon={lon})")] + public string Goto(int key, double lat, double lon) { - [ODataAttributeRouting] - [HttpGet("/explicit/Employees({key})/Goto(lat={lat},lon={lon})")] - [HttpGet("~/convention/Employees({key})/ODataCustomizedSample.Models.Goto(lat={lat},lon={lon})")] - public string Goto(int key, double lat, double lon) - { - return $"Move Employees({key}) to location at ({lat},{lon})"; - } + return $"Move Employees({key}) to location at ({lat},{lon})"; } } diff --git a/sample/ODataCustomizedSample/Controllers/WeatherForecastController.cs b/sample/ODataCustomizedSample/Controllers/WeatherForecastController.cs index 392139baa..0ac7cb089 100644 --- a/sample/ODataCustomizedSample/Controllers/WeatherForecastController.cs +++ b/sample/ODataCustomizedSample/Controllers/WeatherForecastController.cs @@ -11,35 +11,34 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace ODataCustomizedSample.Controllers +namespace ODataCustomizedSample.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase { - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase + private static readonly string[] Summaries = new[] { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; - private readonly ILogger _logger; + private readonly ILogger _logger; - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } - [HttpGet] - public IEnumerable Get() + [HttpGet] + public IEnumerable Get() + { + var rng = new Random(); + return Enumerable.Range(1, 5).Select(index => new WeatherForecast { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }) - .ToArray(); - } + Date = DateTime.Now.AddDays(index), + TemperatureC = rng.Next(-20, 55), + Summary = Summaries[rng.Next(Summaries.Length)] + }) + .ToArray(); } } diff --git a/sample/ODataCustomizedSample/Extensions/EntitySetTemplateSegment.cs b/sample/ODataCustomizedSample/Extensions/EntitySetTemplateSegment.cs index c4684c795..3bf387332 100644 --- a/sample/ODataCustomizedSample/Extensions/EntitySetTemplateSegment.cs +++ b/sample/ODataCustomizedSample/Extensions/EntitySetTemplateSegment.cs @@ -13,36 +13,35 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace ODataCustomizedSample.Extensions +namespace ODataCustomizedSample.Extensions; + +public class EntitySetTemplateSegment : ODataSegmentTemplate { - public class EntitySetTemplateSegment : ODataSegmentTemplate + public override IEnumerable GetTemplates(ODataRouteOptions options) { - public override IEnumerable GetTemplates(ODataRouteOptions options) - { - yield return "/{classname}"; - } + yield return "/{classname}"; + } - public override bool TryTranslate(ODataTemplateTranslateContext context) + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (!context.RouteValues.TryGetValue("classname", out object classname)) { - if (!context.RouteValues.TryGetValue("classname", out object classname)) - { - return false; - } - - string entitySetName = classname as string; + return false; + } - // if you want to support case-insenstivie - var edmEntitySet = context.Model.EntityContainer.EntitySets() - .FirstOrDefault(e => string.Equals(entitySetName, e.Name, StringComparison.OrdinalIgnoreCase)); + string entitySetName = classname as string; - //var edmEntitySet = context.Model.EntityContainer.FindEntitySet(entitySetName); - if (edmEntitySet != null) - { - context.Segments.Add(new EntitySetSegment(edmEntitySet)); - return true; - } + // if you want to support case-insenstivie + var edmEntitySet = context.Model.EntityContainer.EntitySets() + .FirstOrDefault(e => string.Equals(entitySetName, e.Name, StringComparison.OrdinalIgnoreCase)); - return false; + //var edmEntitySet = context.Model.EntityContainer.FindEntitySet(entitySetName); + if (edmEntitySet != null) + { + context.Segments.Add(new EntitySetSegment(edmEntitySet)); + return true; } + + return false; } } diff --git a/sample/ODataCustomizedSample/Extensions/MyEntitySetRoutingConvention.cs b/sample/ODataCustomizedSample/Extensions/MyEntitySetRoutingConvention.cs index f080a5e74..3bba9716b 100644 --- a/sample/ODataCustomizedSample/Extensions/MyEntitySetRoutingConvention.cs +++ b/sample/ODataCustomizedSample/Extensions/MyEntitySetRoutingConvention.cs @@ -9,36 +9,35 @@ using Microsoft.AspNetCore.OData.Routing.Conventions; using Microsoft.AspNetCore.OData.Routing.Template; -namespace ODataCustomizedSample.Extensions +namespace ODataCustomizedSample.Extensions; + +public class MyEntitySetRoutingConvention : IODataControllerActionConvention { - public class MyEntitySetRoutingConvention : IODataControllerActionConvention + public virtual int Order => 0; + + public virtual bool AppliesToController(ODataControllerActionContext context) { - public virtual int Order => 0; + return context.Controller.ControllerName == "GenericOData"; + } - public virtual bool AppliesToController(ODataControllerActionContext context) + public virtual bool AppliesToAction(ODataControllerActionContext context) + { + if (context.Prefix == "v{version}") { - return context.Controller.ControllerName == "GenericOData"; + // let's only allow this prefix + return false; } - public virtual bool AppliesToAction(ODataControllerActionContext context) + if (context.Action.ActionName != "GetTest") { - if (context.Prefix == "v{version}") - { - // let's only allow this prefix - return false; - } - - if (context.Action.ActionName != "GetTest") - { - return false; - } - - ODataPathTemplate path = new ODataPathTemplate( - new EntitySetTemplateSegment() - ); - - context.Action.AddSelector("Get", context.Prefix, context.Model, path); - return true; + return false; } + + ODataPathTemplate path = new ODataPathTemplate( + new EntitySetTemplateSegment() + ); + + context.Action.AddSelector("Get", context.Prefix, context.Model, path); + return true; } } diff --git a/sample/ODataCustomizedSample/Models/EdmModelBuilder.cs b/sample/ODataCustomizedSample/Models/EdmModelBuilder.cs index a57b8f553..f6a98f05a 100644 --- a/sample/ODataCustomizedSample/Models/EdmModelBuilder.cs +++ b/sample/ODataCustomizedSample/Models/EdmModelBuilder.cs @@ -10,47 +10,46 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace ODataCustomizedSample.Models +namespace ODataCustomizedSample.Models; + +public class EdmModelBuilder { - public class EdmModelBuilder + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Players"); + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Players"); - var function = builder.EntityType().Function("PlayPiano").Returns(); - function.Parameter("kind"); - function.Parameter("name"); + var function = builder.EntityType().Function("PlayPiano").Returns(); + function.Parameter("kind"); + function.Parameter("name"); - return builder.GetEdmModel(); - } + return builder.GetEdmModel(); + } - public static IEdmModel BuildEdmModel() + public static IEdmModel BuildEdmModel() + { + var models = new Dictionary() { - var models = new Dictionary() - { - {"Customer", "CustomerId" }, - {"Car", "CarId" }, - {"School", "SchoolId" } - }; - - var model = new EdmModel(); + {"Customer", "CustomerId" }, + {"Car", "CarId" }, + {"School", "SchoolId" } + }; - EdmEntityContainer container = new EdmEntityContainer("Default", "Container"); + var model = new EdmModel(); - for (int i = 0; i < models.Count; i++) - { - var name = models.ElementAt(i); - EdmEntityType element = new EdmEntityType("Default", name.Key, null, false, true); - element.AddKeys(element.AddStructuralProperty(name.Value, EdmPrimitiveTypeKind.Int32)); + EdmEntityContainer container = new EdmEntityContainer("Default", "Container"); - model.AddElement(element); - container.AddEntitySet(name.Key, element); - } + for (int i = 0; i < models.Count; i++) + { + var name = models.ElementAt(i); + EdmEntityType element = new EdmEntityType("Default", name.Key, null, false, true); + element.AddKeys(element.AddStructuralProperty(name.Value, EdmPrimitiveTypeKind.Int32)); - model.AddElement(container); - return model; + model.AddElement(element); + container.AddEntitySet(name.Key, element); } + + model.AddElement(container); + return model; } } diff --git a/sample/ODataCustomizedSample/Models/Employee.cs b/sample/ODataCustomizedSample/Models/Employee.cs index 738920a5e..7e21b231b 100644 --- a/sample/ODataCustomizedSample/Models/Employee.cs +++ b/sample/ODataCustomizedSample/Models/Employee.cs @@ -9,69 +9,68 @@ using System.Collections.Generic; using System.Runtime.Serialization; -namespace ODataCustomizedSample.Models +namespace ODataCustomizedSample.Models; + +public class Employee { - public class Employee - { - public int ID { get; set; } + public int ID { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public List SkillSet { get; set; } + public List SkillSet { get; set; } - public Gender Gender { get; set; } + public Gender Gender { get; set; } - public AccessLevel AccessLevel { get; set; } + public AccessLevel AccessLevel { get; set; } - public FavoriteSports FavoriteSports { get; set; } - } + public FavoriteSports FavoriteSports { get; set; } +} - [Flags] - public enum AccessLevel - { - Read = 1, +[Flags] +public enum AccessLevel +{ + Read = 1, - Write = 2, + Write = 2, - Execute = 4 - } + Execute = 4 +} - public enum Gender - { - Male = 1, +public enum Gender +{ + Male = 1, - Female = 2 - } + Female = 2 +} - public enum Skill - { - CSharp, +public enum Skill +{ + CSharp, - Sql, + Sql, - Web, - } + Web, +} - public enum Sport - { - Pingpong, +public enum Sport +{ + Pingpong, - Basketball - } + Basketball +} - public class FavoriteSports - { - public Sport LikeMost { get; set; } - public List Like { get; set; } - } +public class FavoriteSports +{ + public Sport LikeMost { get; set; } + public List Like { get; set; } +} - [DataContract] - public enum Status - { - [EnumMember(Value = "Sold out")] - SoldOut, +[DataContract] +public enum Status +{ + [EnumMember(Value = "Sold out")] + SoldOut, - [EnumMember(Value = "In store")] - InStore - } + [EnumMember(Value = "In store")] + InStore } diff --git a/sample/ODataCustomizedSample/Models/EnumsEdmModel.cs b/sample/ODataCustomizedSample/Models/EnumsEdmModel.cs index 89d203e15..9d54b5639 100644 --- a/sample/ODataCustomizedSample/Models/EnumsEdmModel.cs +++ b/sample/ODataCustomizedSample/Models/EnumsEdmModel.cs @@ -8,95 +8,94 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace ODataCustomizedSample.Models +namespace ODataCustomizedSample.Models; + +internal class EnumsEdmModel { - internal class EnumsEdmModel + public static IEdmModel GetExplicitModel() { - public static IEdmModel GetExplicitModel() - { - ODataModelBuilder builder = new ODataModelBuilder(); - var employee = builder.EntityType(); - employee.HasKey(c => c.ID); - employee.Property(c => c.Name); - employee.CollectionProperty(c => c.SkillSet); - employee.EnumProperty(c => c.Gender); - employee.EnumProperty(c => c.AccessLevel); - employee.ComplexProperty(c => c.FavoriteSports); - - var skill = builder.EnumType(); - skill.Member(Skill.CSharp); - skill.Member(Skill.Sql); - skill.Member(Skill.Web); - - var gender = builder.EnumType(); - gender.Member(Gender.Female); - gender.Member(Gender.Male); - - var accessLevel = builder.EnumType(); - accessLevel.Member(AccessLevel.Execute); - accessLevel.Member(AccessLevel.Read); - accessLevel.Member(AccessLevel.Write); - - var favoriteSports = builder.ComplexType(); - favoriteSports.EnumProperty(f => f.LikeMost); - favoriteSports.CollectionProperty(f => f.Like); - - var sport = builder.EnumType(); - sport.Member(Sport.Basketball); - sport.Member(Sport.Pingpong); - - AddBoundActionsAndFunctions(employee); - AddUnboundActionsAndFunctions(builder); - - EntitySetConfiguration employees = builder.EntitySet("Employees"); - builder.Namespace = typeof(Employee).Namespace; - return builder.GetEdmModel(); - } - - public static IEdmModel GetConventionModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - EntitySetConfiguration employees = builder.EntitySet("Employees"); - EntityTypeConfiguration employee = employees.EntityType; - - // maybe following lines are not required once bug #1587 is fixed. - // 1587: It's better to support automatically adding actions and functions in ODataConventionModelBuilder. - AddBoundActionsAndFunctions(employee); - AddUnboundActionsAndFunctions(builder); - - builder.Namespace = typeof(Employee).Namespace; - - var edmModel = builder.GetEdmModel(); - return edmModel; - } - - private static void AddBoundActionsAndFunctions(EntityTypeConfiguration employee) - { - var actionConfiguration = employee.Action("AddSkill"); - actionConfiguration.Parameter("skill"); - actionConfiguration.ReturnsCollection(); - - var functionConfiguration = employee.Function("FindAccessLevel"); - functionConfiguration.Returns(); - - var function = employee.Function("Goto").Returns(); - function.Parameter("lat"); - function.Parameter("lon"); - } - - private static void AddUnboundActionsAndFunctions(ODataModelBuilder odataModelBuilder) - { - var actionConfiguration = odataModelBuilder.Action("SetAccessLevel"); - actionConfiguration.Parameter("ID"); - actionConfiguration.Parameter("accessLevel"); - actionConfiguration.Returns(); - - var functionConfiguration = odataModelBuilder.Function("HasAccessLevel"); - functionConfiguration.Parameter("ID"); - functionConfiguration.Parameter("AccessLevel"); - functionConfiguration.Returns(); - - var actionConfiguration2 = odataModelBuilder.Action("ResetDataSource"); - } + ODataModelBuilder builder = new ODataModelBuilder(); + var employee = builder.EntityType(); + employee.HasKey(c => c.ID); + employee.Property(c => c.Name); + employee.CollectionProperty(c => c.SkillSet); + employee.EnumProperty(c => c.Gender); + employee.EnumProperty(c => c.AccessLevel); + employee.ComplexProperty(c => c.FavoriteSports); + + var skill = builder.EnumType(); + skill.Member(Skill.CSharp); + skill.Member(Skill.Sql); + skill.Member(Skill.Web); + + var gender = builder.EnumType(); + gender.Member(Gender.Female); + gender.Member(Gender.Male); + + var accessLevel = builder.EnumType(); + accessLevel.Member(AccessLevel.Execute); + accessLevel.Member(AccessLevel.Read); + accessLevel.Member(AccessLevel.Write); + + var favoriteSports = builder.ComplexType(); + favoriteSports.EnumProperty(f => f.LikeMost); + favoriteSports.CollectionProperty(f => f.Like); + + var sport = builder.EnumType(); + sport.Member(Sport.Basketball); + sport.Member(Sport.Pingpong); + + AddBoundActionsAndFunctions(employee); + AddUnboundActionsAndFunctions(builder); + + EntitySetConfiguration employees = builder.EntitySet("Employees"); + builder.Namespace = typeof(Employee).Namespace; + return builder.GetEdmModel(); + } + + public static IEdmModel GetConventionModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + EntitySetConfiguration employees = builder.EntitySet("Employees"); + EntityTypeConfiguration employee = employees.EntityType; + + // maybe following lines are not required once bug #1587 is fixed. + // 1587: It's better to support automatically adding actions and functions in ODataConventionModelBuilder. + AddBoundActionsAndFunctions(employee); + AddUnboundActionsAndFunctions(builder); + + builder.Namespace = typeof(Employee).Namespace; + + var edmModel = builder.GetEdmModel(); + return edmModel; + } + + private static void AddBoundActionsAndFunctions(EntityTypeConfiguration employee) + { + var actionConfiguration = employee.Action("AddSkill"); + actionConfiguration.Parameter("skill"); + actionConfiguration.ReturnsCollection(); + + var functionConfiguration = employee.Function("FindAccessLevel"); + functionConfiguration.Returns(); + + var function = employee.Function("Goto").Returns(); + function.Parameter("lat"); + function.Parameter("lon"); + } + + private static void AddUnboundActionsAndFunctions(ODataModelBuilder odataModelBuilder) + { + var actionConfiguration = odataModelBuilder.Action("SetAccessLevel"); + actionConfiguration.Parameter("ID"); + actionConfiguration.Parameter("accessLevel"); + actionConfiguration.Returns(); + + var functionConfiguration = odataModelBuilder.Function("HasAccessLevel"); + functionConfiguration.Parameter("ID"); + functionConfiguration.Parameter("AccessLevel"); + functionConfiguration.Returns(); + + var actionConfiguration2 = odataModelBuilder.Action("ResetDataSource"); } } diff --git a/sample/ODataCustomizedSample/Models/Player.cs b/sample/ODataCustomizedSample/Models/Player.cs index 1c0d2ba12..45a5fb1f0 100644 --- a/sample/ODataCustomizedSample/Models/Player.cs +++ b/sample/ODataCustomizedSample/Models/Player.cs @@ -5,14 +5,13 @@ // //------------------------------------------------------------------------------ -namespace ODataCustomizedSample.Models +namespace ODataCustomizedSample.Models; + +public class Player { - public class Player - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public int Grade { get; set; } - } + public int Grade { get; set; } } diff --git a/sample/ODataCustomizedSample/Program.cs b/sample/ODataCustomizedSample/Program.cs index 4375d32f2..ef465b0c5 100644 --- a/sample/ODataCustomizedSample/Program.cs +++ b/sample/ODataCustomizedSample/Program.cs @@ -14,20 +14,19 @@ using System.Linq; using System.Threading.Tasks; -namespace ODataCustomizedSample +namespace ODataCustomizedSample; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + CreateHostBuilder(args).Build().Run(); } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); } diff --git a/sample/ODataCustomizedSample/Startup.cs b/sample/ODataCustomizedSample/Startup.cs index 1b9b820ea..92843c687 100644 --- a/sample/ODataCustomizedSample/Startup.cs +++ b/sample/ODataCustomizedSample/Startup.cs @@ -15,63 +15,62 @@ using ODataCustomizedSample.Extensions; using ODataCustomizedSample.Models; -namespace ODataCustomizedSample +namespace ODataCustomizedSample; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + Configuration = configuration; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - IEdmModel model1 = EdmModelBuilder.GetEdmModel(); - IEdmModel model2 = EdmModelBuilder.BuildEdmModel(); - IEdmModel model3 = EnumsEdmModel.GetConventionModel(); - IEdmModel model4 = EnumsEdmModel.GetExplicitModel(); + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + IEdmModel model1 = EdmModelBuilder.GetEdmModel(); + IEdmModel model2 = EdmModelBuilder.BuildEdmModel(); + IEdmModel model3 = EnumsEdmModel.GetConventionModel(); + IEdmModel model4 = EnumsEdmModel.GetExplicitModel(); - services.AddControllers().AddOData(opt => - opt - .AddRouteComponents(model1) - .AddRouteComponents("odata", model2) - .AddRouteComponents("v{version}", model1) - .AddRouteComponents("convention", model3) - .AddRouteComponents("explicit", model4) - .Conventions.Add(new MyEntitySetRoutingConvention())); + services.AddControllers().AddOData(opt => + opt + .AddRouteComponents(model1) + .AddRouteComponents("odata", model2) + .AddRouteComponents("v{version}", model1) + .AddRouteComponents("convention", model3) + .AddRouteComponents("explicit", model4) + .Conventions.Add(new MyEntitySetRoutingConvention())); - services.AddSwaggerGen(); - } + services.AddSwaggerGen(); + } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } + app.UseDeveloperExceptionPage(); + } - app.UseSwagger(); - app.UseSwaggerUI(c => - { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "OData 8.x OpenAPI"); - }); + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "OData 8.x OpenAPI"); + }); - app.UseRouting(); + app.UseRouting(); - // for route debug page. be noted: you can put the middleware after UseRouting. - // and you can use different route pattern name. - app.UseODataRouteDebug("$odata2"); + // for route debug page. be noted: you can put the middleware after UseRouting. + // and you can use different route pattern name. + app.UseODataRouteDebug("$odata2"); - app.UseAuthorization(); + app.UseAuthorization(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); } } diff --git a/sample/ODataCustomizedSample/WeatherForecast.cs b/sample/ODataCustomizedSample/WeatherForecast.cs index 5dad068d0..0cddba88d 100644 --- a/sample/ODataCustomizedSample/WeatherForecast.cs +++ b/sample/ODataCustomizedSample/WeatherForecast.cs @@ -7,16 +7,15 @@ using System; -namespace ODataCustomizedSample +namespace ODataCustomizedSample; + +public class WeatherForecast { - public class WeatherForecast - { - public DateTime Date { get; set; } + public DateTime Date { get; set; } - public int TemperatureC { get; set; } + public int TemperatureC { get; set; } - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - public string Summary { get; set; } - } + public string Summary { get; set; } } diff --git a/sample/ODataDynamicModel/Controllers/HandleAllController.cs b/sample/ODataDynamicModel/Controllers/HandleAllController.cs index c46ac6aa3..7798929fb 100644 --- a/sample/ODataDynamicModel/Controllers/HandleAllController.cs +++ b/sample/ODataDynamicModel/Controllers/HandleAllController.cs @@ -15,148 +15,147 @@ using Microsoft.OData.UriParser; using ODataDynamicModel.Extensions; -namespace ODataDynamicModel.Controllers +namespace ODataDynamicModel.Controllers; + +public class HandleAllController : ODataController { - public class HandleAllController : ODataController + private IDataSourceProvider _provider; + public HandleAllController(IDataSourceProvider provider) { - private IDataSourceProvider _provider; - public HandleAllController(IDataSourceProvider provider) - { - _provider = provider; - } - - // Get entityset - // odata/{datasource}/{entityset} - public EdmEntityObjectCollection Get(string datasource) - { - // Get entity set's EDM type: A collection type. - ODataPath path = Request.ODataFeature().Path; - IEdmCollectionType collectionType = (IEdmCollectionType)path.Last().EdmType; - IEdmEntityTypeReference edmEntityTypeReference = collectionType.ElementType.AsEntity(); - var edmEntityType = edmEntityTypeReference.EntityDefinition(); + _provider = provider; + } - //Set the SelectExpandClause on OdataFeature to include navigation property set in the $expand - SetSelectExpandClauseOnODataFeature(path, edmEntityType); + // Get entityset + // odata/{datasource}/{entityset} + public EdmEntityObjectCollection Get(string datasource) + { + // Get entity set's EDM type: A collection type. + ODataPath path = Request.ODataFeature().Path; + IEdmCollectionType collectionType = (IEdmCollectionType)path.Last().EdmType; + IEdmEntityTypeReference edmEntityTypeReference = collectionType.ElementType.AsEntity(); + var edmEntityType = edmEntityTypeReference.EntityDefinition(); - // Create an untyped collection with the EDM collection type. - EdmEntityObjectCollection collection = - new EdmEntityObjectCollection(new EdmCollectionTypeReference(collectionType)); + //Set the SelectExpandClause on OdataFeature to include navigation property set in the $expand + SetSelectExpandClauseOnODataFeature(path, edmEntityType); - // Add untyped objects to collection. - IDataSource ds = _provider.DataSources[datasource]; - ds.Get(edmEntityTypeReference, collection); + // Create an untyped collection with the EDM collection type. + EdmEntityObjectCollection collection = + new EdmEntityObjectCollection(new EdmCollectionTypeReference(collectionType)); - return collection; - } + // Add untyped objects to collection. + IDataSource ds = _provider.DataSources[datasource]; + ds.Get(edmEntityTypeReference, collection); - // Get entityset(key) odata/{datasource}/{entityset}({key}) - public IEdmEntityObject Get(string datasource, string key) - { - // Get entity type from path. - ODataPath path = Request.ODataFeature().Path; - IEdmEntityType entityType = (IEdmEntityType)path.Last().EdmType; + return collection; + } - //Set the SelectExpandClause on OdataFeature to include navigation property set in the $expand - SetSelectExpandClauseOnODataFeature(path, entityType); + // Get entityset(key) odata/{datasource}/{entityset}({key}) + public IEdmEntityObject Get(string datasource, string key) + { + // Get entity type from path. + ODataPath path = Request.ODataFeature().Path; + IEdmEntityType entityType = (IEdmEntityType)path.Last().EdmType; - // Create an untyped entity object with the entity type. - EdmEntityObject entity = new EdmEntityObject(entityType); + //Set the SelectExpandClause on OdataFeature to include navigation property set in the $expand + SetSelectExpandClauseOnODataFeature(path, entityType); - IDataSource ds = _provider.DataSources[datasource]; - ds.Get(key, entity); + // Create an untyped entity object with the entity type. + EdmEntityObject entity = new EdmEntityObject(entityType); - return entity; - } + IDataSource ds = _provider.DataSources[datasource]; + ds.Get(key, entity); - // odata/{datasource}/{entityset}({key})/Name - public IActionResult GetName(string datasource, string key) - { - // Get entity type from path. - ODataPath path = Request.ODataFeature().Path; + return entity; + } - PropertySegment property = path.Last() as PropertySegment; - IEdmEntityType entityType = property.Property.DeclaringType as IEdmEntityType; + // odata/{datasource}/{entityset}({key})/Name + public IActionResult GetName(string datasource, string key) + { + // Get entity type from path. + ODataPath path = Request.ODataFeature().Path; - //Set the SelectExpandClause on OdataFeature to include navigation property set in the $expand - SetSelectExpandClauseOnODataFeature(path, entityType); + PropertySegment property = path.Last() as PropertySegment; + IEdmEntityType entityType = property.Property.DeclaringType as IEdmEntityType; - // Create an untyped entity object with the entity type. - EdmEntityObject entity = new EdmEntityObject(entityType); + //Set the SelectExpandClause on OdataFeature to include navigation property set in the $expand + SetSelectExpandClauseOnODataFeature(path, entityType); - IDataSource ds = _provider.DataSources[datasource]; - ds.Get(key, entity); + // Create an untyped entity object with the entity type. + EdmEntityObject entity = new EdmEntityObject(entityType); - object value = ds.GetProperty("Name", entity); + IDataSource ds = _provider.DataSources[datasource]; + ds.Get(key, entity); - if (value == null) - { - return NotFound(); - } + object value = ds.GetProperty("Name", entity); - string strValue = value as string; - return Ok(strValue); + if (value == null) + { + return NotFound(); } - // odata/{datasource}/{entityset}({key})/{navigation} - public IActionResult GetNavigation(string datasource, string key, string navigation) - { - ODataPath path = Request.ODataFeature().Path; + string strValue = value as string; + return Ok(strValue); + } - NavigationPropertySegment property = path.Last() as NavigationPropertySegment; - if (property == null) - { - return BadRequest("Not the correct navigation property access request!"); - } + // odata/{datasource}/{entityset}({key})/{navigation} + public IActionResult GetNavigation(string datasource, string key, string navigation) + { + ODataPath path = Request.ODataFeature().Path; - IEdmEntityType entityType = property.NavigationProperty.DeclaringType as IEdmEntityType; - //Set the SelectExpandClause on OdataFeature to include navigation property set in the $expand - SetSelectExpandClauseOnODataFeature(path, entityType); + NavigationPropertySegment property = path.Last() as NavigationPropertySegment; + if (property == null) + { + return BadRequest("Not the correct navigation property access request!"); + } - EdmEntityObject entity = new EdmEntityObject(entityType); - IDataSource ds = _provider.DataSources[datasource]; + IEdmEntityType entityType = property.NavigationProperty.DeclaringType as IEdmEntityType; + //Set the SelectExpandClause on OdataFeature to include navigation property set in the $expand + SetSelectExpandClauseOnODataFeature(path, entityType); - ds.Get(key, entity); + EdmEntityObject entity = new EdmEntityObject(entityType); + IDataSource ds = _provider.DataSources[datasource]; - object value = ds.GetProperty(navigation, entity); + ds.Get(key, entity); - if (value == null) - { - return NotFound(); - } + object value = ds.GetProperty(navigation, entity); - IEdmEntityObject nav = value as IEdmEntityObject; - if (nav == null) - { - return NotFound(); - } + if (value == null) + { + return NotFound(); + } - return Ok(nav); + IEdmEntityObject nav = value as IEdmEntityObject; + if (nav == null) + { + return NotFound(); } - /// - /// Set the on ODataFeature. - /// Without this, the response does not contains navigation property included in $expand - /// - /// OData Path from the Request - /// Entity type on which the query is being performed - /// - private void SetSelectExpandClauseOnODataFeature(ODataPath odataPath, IEdmType edmEntityType) + return Ok(nav); + } + + /// + /// Set the on ODataFeature. + /// Without this, the response does not contains navigation property included in $expand + /// + /// OData Path from the Request + /// Entity type on which the query is being performed + /// + private void SetSelectExpandClauseOnODataFeature(ODataPath odataPath, IEdmType edmEntityType) + { + IDictionary options = new Dictionary(); + foreach (var k in Request.Query.Keys) { - IDictionary options = new Dictionary(); - foreach (var k in Request.Query.Keys) - { - options.Add(k, Request.Query[k]); - } - - //At this point, we should have valid entity segment and entity type. - //If there is invalid entity in the query, then OData routing should return 404 error before executing this api - var segment = odataPath.FirstSegment as EntitySetSegment; - IEdmNavigationSource source = segment?.EntitySet; - ODataQueryOptionParser parser = new(Request.GetModel(), edmEntityType, source, options); - parser.Resolver.EnableCaseInsensitive = true; - - //Set the SelectExpand Clause on the ODataFeature otherwise Odata formatter won't show the expand and select properties in the response. - Request.ODataFeature().SelectExpandClause = parser.ParseSelectAndExpand(); + options.Add(k, Request.Query[k]); } + + //At this point, we should have valid entity segment and entity type. + //If there is invalid entity in the query, then OData routing should return 404 error before executing this api + var segment = odataPath.FirstSegment as EntitySetSegment; + IEdmNavigationSource source = segment?.EntitySet; + ODataQueryOptionParser parser = new(Request.GetModel(), edmEntityType, source, options); + parser.Resolver.EnableCaseInsensitive = true; + + //Set the SelectExpand Clause on the ODataFeature otherwise Odata formatter won't show the expand and select properties in the response. + Request.ODataFeature().SelectExpandClause = parser.ParseSelectAndExpand(); } } diff --git a/sample/ODataDynamicModel/Controllers/WeatherForecastController.cs b/sample/ODataDynamicModel/Controllers/WeatherForecastController.cs index 81d340db4..0ef5bdb4a 100644 --- a/sample/ODataDynamicModel/Controllers/WeatherForecastController.cs +++ b/sample/ODataDynamicModel/Controllers/WeatherForecastController.cs @@ -12,35 +12,34 @@ using System.Linq; using System.Threading.Tasks; -namespace ODataDynamicModel.Controllers +namespace ODataDynamicModel.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase { - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase + private static readonly string[] Summaries = new[] { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; - private readonly ILogger _logger; + private readonly ILogger _logger; - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } - [HttpGet] - public IEnumerable Get() + [HttpGet] + public IEnumerable Get() + { + var rng = new Random(); + return Enumerable.Range(1, 5).Select(index => new WeatherForecast { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }) - .ToArray(); - } + Date = DateTime.Now.AddDays(index), + TemperatureC = rng.Next(-20, 55), + Summary = Summaries[rng.Next(Summaries.Length)] + }) + .ToArray(); } } diff --git a/sample/ODataDynamicModel/Extensions/AnotherDataSource.cs b/sample/ODataDynamicModel/Extensions/AnotherDataSource.cs index 5581d898c..98dbb257b 100644 --- a/sample/ODataDynamicModel/Extensions/AnotherDataSource.cs +++ b/sample/ODataDynamicModel/Extensions/AnotherDataSource.cs @@ -10,94 +10,93 @@ using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.OData.Edm; -namespace ODataDynamicModel.Extensions +namespace ODataDynamicModel.Extensions; + +internal class AnotherDataSource : IDataSource { - internal class AnotherDataSource : IDataSource - { - private IEdmEntityType _school; + private IEdmEntityType _school; - private static IEdmModel _edmModel; + private static IEdmModel _edmModel; - public virtual IEdmModel GetEdmModel() + public virtual IEdmModel GetEdmModel() + { + if (_edmModel != null) { - if (_edmModel != null) - { - return _edmModel; - } - - EdmModel model = new EdmModel(); - EdmEntityContainer container = new EdmEntityContainer("ns", "container"); - model.AddElement(container); - - EdmEntityType student = new EdmEntityType("ns", "Student"); - student.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - EdmStructuralProperty key = student.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32); - student.AddKeys(key); - model.AddElement(student); - EdmEntitySet students = container.AddEntitySet("Students", student); - - EdmEntityType school = new EdmEntityType("ns", "School"); - school.AddKeys(school.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - school.AddStructuralProperty("CreatedDay", EdmPrimitiveTypeKind.DateTimeOffset); - model.AddElement(school); - EdmEntitySet schools = container.AddEntitySet("Schools", student); - - EdmNavigationProperty schoolNavProp = student.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "School", - TargetMultiplicity = EdmMultiplicity.One, - Target = school - }); - students.AddNavigationTarget(schoolNavProp, schools); - - _school = school; - _edmModel = model; return _edmModel; } - public void Get(IEdmEntityTypeReference entityType, EdmEntityObjectCollection collection) - { - EdmEntityObject entity = new EdmEntityObject(entityType); - entity.TrySetPropertyValue("Name", "Foo"); - entity.TrySetPropertyValue("ID", 100); - entity.TrySetPropertyValue("School", Createchool(99, new DateTimeOffset(2016, 1, 19, 1, 2, 3, TimeSpan.Zero), entity.ActualEdmType)); - collection.Add(entity); - - entity = new EdmEntityObject(entityType); - entity.TrySetPropertyValue("Name", "Bar"); - entity.TrySetPropertyValue("ID", 101); - entity.TrySetPropertyValue("School", Createchool(99, new DateTimeOffset(1978, 11, 15, 1, 2, 3, TimeSpan.Zero), entity.ActualEdmType)); - - collection.Add(entity); - } + EdmModel model = new EdmModel(); + EdmEntityContainer container = new EdmEntityContainer("ns", "container"); + model.AddElement(container); + + EdmEntityType student = new EdmEntityType("ns", "Student"); + student.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + EdmStructuralProperty key = student.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32); + student.AddKeys(key); + model.AddElement(student); + EdmEntitySet students = container.AddEntitySet("Students", student); + + EdmEntityType school = new EdmEntityType("ns", "School"); + school.AddKeys(school.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + school.AddStructuralProperty("CreatedDay", EdmPrimitiveTypeKind.DateTimeOffset); + model.AddElement(school); + EdmEntitySet schools = container.AddEntitySet("Schools", student); + + EdmNavigationProperty schoolNavProp = student.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "School", + TargetMultiplicity = EdmMultiplicity.One, + Target = school + }); + students.AddNavigationTarget(schoolNavProp, schools); + + _school = school; + _edmModel = model; + return _edmModel; + } - public void Get(string key, EdmEntityObject entity) - { - entity.TrySetPropertyValue("Name", "Foo"); - entity.TrySetPropertyValue("ID", int.Parse(key)); - entity.TrySetPropertyValue("School", Createchool(99, new DateTimeOffset(2016, 1, 19, 1, 2, 3, TimeSpan.Zero), entity.ActualEdmType)); - } + public void Get(IEdmEntityTypeReference entityType, EdmEntityObjectCollection collection) + { + EdmEntityObject entity = new EdmEntityObject(entityType); + entity.TrySetPropertyValue("Name", "Foo"); + entity.TrySetPropertyValue("ID", 100); + entity.TrySetPropertyValue("School", Createchool(99, new DateTimeOffset(2016, 1, 19, 1, 2, 3, TimeSpan.Zero), entity.ActualEdmType)); + collection.Add(entity); + + entity = new EdmEntityObject(entityType); + entity.TrySetPropertyValue("Name", "Bar"); + entity.TrySetPropertyValue("ID", 101); + entity.TrySetPropertyValue("School", Createchool(99, new DateTimeOffset(1978, 11, 15, 1, 2, 3, TimeSpan.Zero), entity.ActualEdmType)); + + collection.Add(entity); + } - public object GetProperty(string property, EdmEntityObject entity) - { - object value; - entity.TryGetPropertyValue(property, out value); - return value; - } + public void Get(string key, EdmEntityObject entity) + { + entity.TrySetPropertyValue("Name", "Foo"); + entity.TrySetPropertyValue("ID", int.Parse(key)); + entity.TrySetPropertyValue("School", Createchool(99, new DateTimeOffset(2016, 1, 19, 1, 2, 3, TimeSpan.Zero), entity.ActualEdmType)); + } - private IEdmEntityObject Createchool(int id, DateTimeOffset dto, IEdmStructuredType edmType) - { - IEdmNavigationProperty navigationProperty = edmType.DeclaredProperties.OfType().FirstOrDefault(e => e.Name == "School"); - if (navigationProperty == null) - { - return null; - } + public object GetProperty(string property, EdmEntityObject entity) + { + object value; + entity.TryGetPropertyValue(property, out value); + return value; + } - EdmEntityObject entity = new EdmEntityObject(navigationProperty.ToEntityType()); - entity.TrySetPropertyValue("ID", id); - entity.TrySetPropertyValue("CreatedDay", dto); - return entity; + private IEdmEntityObject Createchool(int id, DateTimeOffset dto, IEdmStructuredType edmType) + { + IEdmNavigationProperty navigationProperty = edmType.DeclaredProperties.OfType().FirstOrDefault(e => e.Name == "School"); + if (navigationProperty == null) + { + return null; } + + EdmEntityObject entity = new EdmEntityObject(navigationProperty.ToEntityType()); + entity.TrySetPropertyValue("ID", id); + entity.TrySetPropertyValue("CreatedDay", dto); + return entity; } } diff --git a/sample/ODataDynamicModel/Extensions/DataSourceProvider.cs b/sample/ODataDynamicModel/Extensions/DataSourceProvider.cs index f33e7e28c..ae3b01652 100644 --- a/sample/ODataDynamicModel/Extensions/DataSourceProvider.cs +++ b/sample/ODataDynamicModel/Extensions/DataSourceProvider.cs @@ -7,19 +7,18 @@ using System.Collections.Generic; -namespace ODataDynamicModel.Extensions +namespace ODataDynamicModel.Extensions; + +public class DataSourceProvider : IDataSourceProvider { - public class DataSourceProvider : IDataSourceProvider + public DataSourceProvider() { - public DataSourceProvider() + DataSources = new Dictionary { - DataSources = new Dictionary - { - { "mydatasource", new MyDataSource() }, - { "anotherdatasource", new AnotherDataSource() } - }; - } - - public IDictionary DataSources { get; } + { "mydatasource", new MyDataSource() }, + { "anotherdatasource", new AnotherDataSource() } + }; } + + public IDictionary DataSources { get; } } diff --git a/sample/ODataDynamicModel/Extensions/EntitySetTemplateSegment.cs b/sample/ODataDynamicModel/Extensions/EntitySetTemplateSegment.cs index 2f0b602a9..73f207ed7 100644 --- a/sample/ODataDynamicModel/Extensions/EntitySetTemplateSegment.cs +++ b/sample/ODataDynamicModel/Extensions/EntitySetTemplateSegment.cs @@ -13,37 +13,36 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace ODataDynamicModel.Extensions +namespace ODataDynamicModel.Extensions; + +public class EntitySetTemplateSegment : ODataSegmentTemplate { - public class EntitySetTemplateSegment : ODataSegmentTemplate + public override IEnumerable GetTemplates(ODataRouteOptions options) { - public override IEnumerable GetTemplates(ODataRouteOptions options) - { - yield return "/{entityset}"; - } + yield return "/{entityset}"; + } - public override bool TryTranslate(ODataTemplateTranslateContext context) + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (!context.RouteValues.TryGetValue("entityset", out object classname)) { - if (!context.RouteValues.TryGetValue("entityset", out object classname)) - { - return false; - } - - string entitySetName = classname as string; + return false; + } - // if you want to support case-insensitive - var edmEntitySet = context.Model.EntityContainer.EntitySets() - .FirstOrDefault(e => string.Equals(entitySetName, e.Name, StringComparison.OrdinalIgnoreCase)); + string entitySetName = classname as string; - //var edmEntitySet = context.Model.EntityContainer.FindEntitySet(entitySetName); - if (edmEntitySet != null) - { - EntitySetSegment segment = new EntitySetSegment(edmEntitySet); - context.Segments.Add(segment); - return true; - } + // if you want to support case-insensitive + var edmEntitySet = context.Model.EntityContainer.EntitySets() + .FirstOrDefault(e => string.Equals(entitySetName, e.Name, StringComparison.OrdinalIgnoreCase)); - return false; + //var edmEntitySet = context.Model.EntityContainer.FindEntitySet(entitySetName); + if (edmEntitySet != null) + { + EntitySetSegment segment = new EntitySetSegment(edmEntitySet); + context.Segments.Add(segment); + return true; } + + return false; } } diff --git a/sample/ODataDynamicModel/Extensions/EntitySetWithKeyTemplateSegment.cs b/sample/ODataDynamicModel/Extensions/EntitySetWithKeyTemplateSegment.cs index 280f20d94..0b3d94ded 100644 --- a/sample/ODataDynamicModel/Extensions/EntitySetWithKeyTemplateSegment.cs +++ b/sample/ODataDynamicModel/Extensions/EntitySetWithKeyTemplateSegment.cs @@ -15,62 +15,61 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace ODataDynamicModel.Extensions +namespace ODataDynamicModel.Extensions; + +public class EntitySetWithKeyTemplateSegment : ODataSegmentTemplate { - public class EntitySetWithKeyTemplateSegment : ODataSegmentTemplate + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return "/{key}"; + } + + public override bool TryTranslate(ODataTemplateTranslateContext context) { - public override IEnumerable GetTemplates(ODataRouteOptions options) + if (!context.RouteValues.TryGetValue("entityset", out object entitysetNameObj)) { - yield return "/{key}"; + return false; } - public override bool TryTranslate(ODataTemplateTranslateContext context) + if (!context.RouteValues.TryGetValue("key", out object keyObj)) { - if (!context.RouteValues.TryGetValue("entityset", out object entitysetNameObj)) - { - return false; - } - - if (!context.RouteValues.TryGetValue("key", out object keyObj)) - { - return false; - } - - string entitySetName = entitysetNameObj as string; - string keyValue = keyObj as string; + return false; + } - // if you want to support case-insensitive - var edmEntitySet = context.Model.EntityContainer.EntitySets() - .FirstOrDefault(e => string.Equals(entitySetName, e.Name, StringComparison.OrdinalIgnoreCase)); + string entitySetName = entitysetNameObj as string; + string keyValue = keyObj as string; - //var edmEntitySet = context.Model.EntityContainer.FindEntitySet(entitySetName); - if (edmEntitySet != null) - { - EntitySetSegment entitySet = new EntitySetSegment(edmEntitySet); - IEdmEntityType entityType = entitySet.EntitySet.EntityType; + // if you want to support case-insensitive + var edmEntitySet = context.Model.EntityContainer.EntitySets() + .FirstOrDefault(e => string.Equals(entitySetName, e.Name, StringComparison.OrdinalIgnoreCase)); - IEdmProperty keyProperty = entityType.Key().First(); + //var edmEntitySet = context.Model.EntityContainer.FindEntitySet(entitySetName); + if (edmEntitySet != null) + { + EntitySetSegment entitySet = new EntitySetSegment(edmEntitySet); + IEdmEntityType entityType = entitySet.EntitySet.EntityType; - object newValue = ODataUriUtils.ConvertFromUriLiteral(keyValue, ODataVersion.V4, context.Model, keyProperty.Type); + IEdmProperty keyProperty = entityType.Key().First(); - // for non FromODataUri, so update it, for example, remove the single quote for string value. - context.UpdatedValues["key"] = newValue; + object newValue = ODataUriUtils.ConvertFromUriLiteral(keyValue, ODataVersion.V4, context.Model, keyProperty.Type); - // For FromODataUri, let's refactor it later. - string prefixName = ODataParameterValue.ParameterValuePrefix + "key"; - context.UpdatedValues[prefixName] = new ODataParameterValue(newValue, keyProperty.Type); + // for non FromODataUri, so update it, for example, remove the single quote for string value. + context.UpdatedValues["key"] = newValue; - IDictionary keysValues = new Dictionary(); - keysValues[keyProperty.Name] = newValue; + // For FromODataUri, let's refactor it later. + string prefixName = ODataParameterValue.ParameterValuePrefix + "key"; + context.UpdatedValues[prefixName] = new ODataParameterValue(newValue, keyProperty.Type); - KeySegment keySegment = new KeySegment(keysValues, entityType, entitySet.EntitySet); + IDictionary keysValues = new Dictionary(); + keysValues[keyProperty.Name] = newValue; - context.Segments.Add(keySegment); + KeySegment keySegment = new KeySegment(keysValues, entityType, entitySet.EntitySet); - return true; - } + context.Segments.Add(keySegment); - return false; + return true; } + + return false; } } diff --git a/sample/ODataDynamicModel/Extensions/IDataSource.cs b/sample/ODataDynamicModel/Extensions/IDataSource.cs index 9563704e3..82a4fc6bd 100644 --- a/sample/ODataDynamicModel/Extensions/IDataSource.cs +++ b/sample/ODataDynamicModel/Extensions/IDataSource.cs @@ -8,16 +8,15 @@ using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.OData.Edm; -namespace ODataDynamicModel.Extensions +namespace ODataDynamicModel.Extensions; + +public interface IDataSource { - public interface IDataSource - { - IEdmModel GetEdmModel(); + IEdmModel GetEdmModel(); - void Get(IEdmEntityTypeReference entityType, EdmEntityObjectCollection collection); + void Get(IEdmEntityTypeReference entityType, EdmEntityObjectCollection collection); - void Get(string key, EdmEntityObject entity); + void Get(string key, EdmEntityObject entity); - object GetProperty(string property, EdmEntityObject entity); - } + object GetProperty(string property, EdmEntityObject entity); } diff --git a/sample/ODataDynamicModel/Extensions/IDataSourceProvider.cs b/sample/ODataDynamicModel/Extensions/IDataSourceProvider.cs index e92acf914..a6468818e 100644 --- a/sample/ODataDynamicModel/Extensions/IDataSourceProvider.cs +++ b/sample/ODataDynamicModel/Extensions/IDataSourceProvider.cs @@ -7,10 +7,9 @@ using System.Collections.Generic; -namespace ODataDynamicModel.Extensions +namespace ODataDynamicModel.Extensions; + +public interface IDataSourceProvider { - public interface IDataSourceProvider - { - IDictionary DataSources { get; } - } + IDictionary DataSources { get; } } diff --git a/sample/ODataDynamicModel/Extensions/MyDataSource.cs b/sample/ODataDynamicModel/Extensions/MyDataSource.cs index f232f20d8..a8d814487 100644 --- a/sample/ODataDynamicModel/Extensions/MyDataSource.cs +++ b/sample/ODataDynamicModel/Extensions/MyDataSource.cs @@ -9,116 +9,115 @@ using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.OData.Edm; -namespace ODataDynamicModel.Extensions +namespace ODataDynamicModel.Extensions; + +internal class MyDataSource : IDataSource { - internal class MyDataSource : IDataSource - { - private static IEdmModel _edmModel; + private static IEdmModel _edmModel; - public virtual IEdmModel GetEdmModel() + public virtual IEdmModel GetEdmModel() + { + if (_edmModel != null) { - if (_edmModel != null) - { - return _edmModel; - } - - EdmModel model = new EdmModel(); - EdmEntityContainer container = new EdmEntityContainer("ns", "container"); - model.AddElement(container); - - EdmEntityType product = new EdmEntityType("ns", "Product"); - product.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - EdmStructuralProperty key = product.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32); - product.AddKeys(key); - model.AddElement(product); - EdmEntitySet products = container.AddEntitySet("Products", product); - - EdmEntityType detailInfo = new EdmEntityType("ns", "DetailInfo"); - detailInfo.AddKeys(detailInfo.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - detailInfo.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); - model.AddElement(detailInfo); - EdmEntitySet detailInfos = container.AddEntitySet("DetailInfos", detailInfo); - EdmNavigationProperty detailInfoNavProp = product.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "DetailInfo", - TargetMultiplicity = EdmMultiplicity.One, - Target = detailInfo - }); - products.AddNavigationTarget(detailInfoNavProp, detailInfos); - - // Add a contained navigation property - product.AddUnidirectionalNavigation( + return _edmModel; + } + + EdmModel model = new EdmModel(); + EdmEntityContainer container = new EdmEntityContainer("ns", "container"); + model.AddElement(container); + + EdmEntityType product = new EdmEntityType("ns", "Product"); + product.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + EdmStructuralProperty key = product.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32); + product.AddKeys(key); + model.AddElement(product); + EdmEntitySet products = container.AddEntitySet("Products", product); + + EdmEntityType detailInfo = new EdmEntityType("ns", "DetailInfo"); + detailInfo.AddKeys(detailInfo.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + detailInfo.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); + model.AddElement(detailInfo); + EdmEntitySet detailInfos = container.AddEntitySet("DetailInfos", detailInfo); + EdmNavigationProperty detailInfoNavProp = product.AddUnidirectionalNavigation( new EdmNavigationPropertyInfo { - Name = "ContainedDetailInfo", + Name = "DetailInfo", TargetMultiplicity = EdmMultiplicity.One, - Target = detailInfo, - ContainsTarget = true + Target = detailInfo }); - _edmModel = model; - return _edmModel; - } + products.AddNavigationTarget(detailInfoNavProp, detailInfos); - public void Get(IEdmEntityTypeReference entityType, EdmEntityObjectCollection collection) + // Add a contained navigation property + product.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo { - EdmEntityObject entity = new EdmEntityObject(entityType); - entity.TrySetPropertyValue("Name", "abc"); - entity.TrySetPropertyValue("ID", 1); - entity.TrySetPropertyValue("DetailInfo", CreateDetailInfo(88, "abc_detailinfo", entity.ActualEdmType)); - entity.TrySetPropertyValue("ContainedDetailInfo", CreateContainedDetailInfo(88, "abc_containeddetailinfo", entity.ActualEdmType)); - - collection.Add(entity); - entity = new EdmEntityObject(entityType); - entity.TrySetPropertyValue("Name", "def"); - entity.TrySetPropertyValue("ID", 2); - entity.TrySetPropertyValue("DetailInfo", CreateDetailInfo(99, "def_detailinfo", entity.ActualEdmType)); - entity.TrySetPropertyValue("ContainedDetailInfo", CreateContainedDetailInfo(99, "def_containeddetailinfo", entity.ActualEdmType)); - - collection.Add(entity); - } + Name = "ContainedDetailInfo", + TargetMultiplicity = EdmMultiplicity.One, + Target = detailInfo, + ContainsTarget = true + }); + _edmModel = model; + return _edmModel; + } - public void Get(string key, EdmEntityObject entity) - { - entity.TrySetPropertyValue("Name", "abc"); - entity.TrySetPropertyValue("ID", int.Parse(key)); - entity.TrySetPropertyValue("DetailInfo", CreateDetailInfo(88, "abc_detailinfo", entity.ActualEdmType)); - entity.TrySetPropertyValue("ContainedDetailInfo", CreateContainedDetailInfo(88, "abc_containeddetailinfo", entity.ActualEdmType)); - } + public void Get(IEdmEntityTypeReference entityType, EdmEntityObjectCollection collection) + { + EdmEntityObject entity = new EdmEntityObject(entityType); + entity.TrySetPropertyValue("Name", "abc"); + entity.TrySetPropertyValue("ID", 1); + entity.TrySetPropertyValue("DetailInfo", CreateDetailInfo(88, "abc_detailinfo", entity.ActualEdmType)); + entity.TrySetPropertyValue("ContainedDetailInfo", CreateContainedDetailInfo(88, "abc_containeddetailinfo", entity.ActualEdmType)); + + collection.Add(entity); + entity = new EdmEntityObject(entityType); + entity.TrySetPropertyValue("Name", "def"); + entity.TrySetPropertyValue("ID", 2); + entity.TrySetPropertyValue("DetailInfo", CreateDetailInfo(99, "def_detailinfo", entity.ActualEdmType)); + entity.TrySetPropertyValue("ContainedDetailInfo", CreateContainedDetailInfo(99, "def_containeddetailinfo", entity.ActualEdmType)); + + collection.Add(entity); + } - public object GetProperty(string property, EdmEntityObject entity) - { - object value; - entity.TryGetPropertyValue(property, out value); - return value; - } + public void Get(string key, EdmEntityObject entity) + { + entity.TrySetPropertyValue("Name", "abc"); + entity.TrySetPropertyValue("ID", int.Parse(key)); + entity.TrySetPropertyValue("DetailInfo", CreateDetailInfo(88, "abc_detailinfo", entity.ActualEdmType)); + entity.TrySetPropertyValue("ContainedDetailInfo", CreateContainedDetailInfo(88, "abc_containeddetailinfo", entity.ActualEdmType)); + } - private IEdmEntityObject CreateDetailInfo(int id, string title, IEdmStructuredType edmType) - { - IEdmNavigationProperty navigationProperty = edmType.DeclaredProperties.OfType().FirstOrDefault(e => e.Name == "DetailInfo"); - if (navigationProperty == null) - { - return null; - } + public object GetProperty(string property, EdmEntityObject entity) + { + object value; + entity.TryGetPropertyValue(property, out value); + return value; + } - EdmEntityObject entity = new EdmEntityObject(navigationProperty.ToEntityType()); - entity.TrySetPropertyValue("ID", id); - entity.TrySetPropertyValue("Title", title); - return entity; + private IEdmEntityObject CreateDetailInfo(int id, string title, IEdmStructuredType edmType) + { + IEdmNavigationProperty navigationProperty = edmType.DeclaredProperties.OfType().FirstOrDefault(e => e.Name == "DetailInfo"); + if (navigationProperty == null) + { + return null; } - private IEdmEntityObject CreateContainedDetailInfo(int id, string title, IEdmStructuredType edmType) - { - IEdmNavigationProperty navigationProperty = edmType.DeclaredProperties.OfType().FirstOrDefault(e => e.Name == "ContainedDetailInfo"); - if (navigationProperty == null) - { - return null; - } + EdmEntityObject entity = new EdmEntityObject(navigationProperty.ToEntityType()); + entity.TrySetPropertyValue("ID", id); + entity.TrySetPropertyValue("Title", title); + return entity; + } - EdmEntityObject entity = new EdmEntityObject(navigationProperty.ToEntityType()); - entity.TrySetPropertyValue("ID", id); - entity.TrySetPropertyValue("Title", title); - return entity; + private IEdmEntityObject CreateContainedDetailInfo(int id, string title, IEdmStructuredType edmType) + { + IEdmNavigationProperty navigationProperty = edmType.DeclaredProperties.OfType().FirstOrDefault(e => e.Name == "ContainedDetailInfo"); + if (navigationProperty == null) + { + return null; } + + EdmEntityObject entity = new EdmEntityObject(navigationProperty.ToEntityType()); + entity.TrySetPropertyValue("ID", id); + entity.TrySetPropertyValue("Title", title); + return entity; } } diff --git a/sample/ODataDynamicModel/Extensions/MyODataRoutingApplicationModelProvider.cs b/sample/ODataDynamicModel/Extensions/MyODataRoutingApplicationModelProvider.cs index 4cbd767f4..60c352fb1 100644 --- a/sample/ODataDynamicModel/Extensions/MyODataRoutingApplicationModelProvider.cs +++ b/sample/ODataDynamicModel/Extensions/MyODataRoutingApplicationModelProvider.cs @@ -12,97 +12,96 @@ using Microsoft.Extensions.Options; using Microsoft.OData.Edm; -namespace ODataDynamicModel.Extensions +namespace ODataDynamicModel.Extensions; + +public class MyODataRoutingApplicationModelProvider : IApplicationModelProvider { - public class MyODataRoutingApplicationModelProvider : IApplicationModelProvider + public MyODataRoutingApplicationModelProvider( + IOptions options) { - public MyODataRoutingApplicationModelProvider( - IOptions options) - { - options.Value.AddRouteComponents("odata/{datasource}", EdmCoreModel.Instance); - } + options.Value.AddRouteComponents("odata/{datasource}", EdmCoreModel.Instance); + } - /// - /// Gets the order value for determining the order of execution of providers. - /// - public int Order => 90; + /// + /// Gets the order value for determining the order of execution of providers. + /// + public int Order => 90; - public void OnProvidersExecuted(ApplicationModelProviderContext context) + public void OnProvidersExecuted(ApplicationModelProviderContext context) + { + EdmModel model = new EdmModel(); + const string prefix = "odata/{datasource}"; + foreach (var controllerModel in context.Result.Controllers) { - EdmModel model = new EdmModel(); - const string prefix = "odata/{datasource}"; - foreach (var controllerModel in context.Result.Controllers) + if (controllerModel.ControllerName == "HandleAll") { - if (controllerModel.ControllerName == "HandleAll") - { - ProcessHandleAll(prefix, model, controllerModel); - continue; - } + ProcessHandleAll(prefix, model, controllerModel); + continue; + } - if (controllerModel.ControllerName == "Metadata") - { - ProcessMetadata(prefix, model, controllerModel); - continue; - } + if (controllerModel.ControllerName == "Metadata") + { + ProcessMetadata(prefix, model, controllerModel); + continue; } } + } - public void OnProvidersExecuting(ApplicationModelProviderContext context) - { - } + public void OnProvidersExecuting(ApplicationModelProviderContext context) + { + } - private void ProcessHandleAll(string prefix, IEdmModel model, ControllerModel controllerModel) + private void ProcessHandleAll(string prefix, IEdmModel model, ControllerModel controllerModel) + { + foreach (var actionModel in controllerModel.Actions) { - foreach (var actionModel in controllerModel.Actions) + if (actionModel.ActionName == "GetNavigation") { - if (actionModel.ActionName == "GetNavigation") - { - ODataPathTemplate path = new ODataPathTemplate( - new EntitySetTemplateSegment(), - new EntitySetWithKeyTemplateSegment(), - new NavigationTemplateSegment()); + ODataPathTemplate path = new ODataPathTemplate( + new EntitySetTemplateSegment(), + new EntitySetWithKeyTemplateSegment(), + new NavigationTemplateSegment()); - actionModel.AddSelector("get", prefix, model, path); - } - else if (actionModel.ActionName == "GetName") - { - ODataPathTemplate path = new ODataPathTemplate( - new EntitySetTemplateSegment(), - new EntitySetWithKeyTemplateSegment(), - new StaticNameSegment()); + actionModel.AddSelector("get", prefix, model, path); + } + else if (actionModel.ActionName == "GetName") + { + ODataPathTemplate path = new ODataPathTemplate( + new EntitySetTemplateSegment(), + new EntitySetWithKeyTemplateSegment(), + new StaticNameSegment()); + actionModel.AddSelector("get", prefix, model, path); + } + else if (actionModel.ActionName == "Get") + { + if (actionModel.Parameters.Count == 1) + { + ODataPathTemplate path = new ODataPathTemplate(new EntitySetTemplateSegment()); actionModel.AddSelector("get", prefix, model, path); } - else if (actionModel.ActionName == "Get") + else { - if (actionModel.Parameters.Count == 1) - { - ODataPathTemplate path = new ODataPathTemplate(new EntitySetTemplateSegment()); - actionModel.AddSelector("get", prefix, model, path); - } - else - { - ODataPathTemplate path = new ODataPathTemplate(new EntitySetTemplateSegment(), new EntitySetWithKeyTemplateSegment()); - actionModel.AddSelector("get", prefix, model, path); - } + ODataPathTemplate path = new ODataPathTemplate(new EntitySetTemplateSegment(), new EntitySetWithKeyTemplateSegment()); + actionModel.AddSelector("get", prefix, model, path); } } } + } - private void ProcessMetadata(string prefix, IEdmModel model, ControllerModel controllerModel) + private void ProcessMetadata(string prefix, IEdmModel model, ControllerModel controllerModel) + { + foreach (var actionModel in controllerModel.Actions) { - foreach (var actionModel in controllerModel.Actions) + if (actionModel.ActionName == "GetMetadata") { - if (actionModel.ActionName == "GetMetadata") - { - ODataPathTemplate path = new ODataPathTemplate(MetadataSegmentTemplate.Instance); - actionModel.AddSelector("get", prefix, model, path); - } - else if (actionModel.ActionName == "GetServiceDocument") - { - ODataPathTemplate path = new ODataPathTemplate(); - actionModel.AddSelector("get", prefix, model, path); - } + ODataPathTemplate path = new ODataPathTemplate(MetadataSegmentTemplate.Instance); + actionModel.AddSelector("get", prefix, model, path); + } + else if (actionModel.ActionName == "GetServiceDocument") + { + ODataPathTemplate path = new ODataPathTemplate(); + actionModel.AddSelector("get", prefix, model, path); } } } diff --git a/sample/ODataDynamicModel/Extensions/MyODataRoutingMatcherPolicy.cs b/sample/ODataDynamicModel/Extensions/MyODataRoutingMatcherPolicy.cs index 312870ad8..b2401904e 100644 --- a/sample/ODataDynamicModel/Extensions/MyODataRoutingMatcherPolicy.cs +++ b/sample/ODataDynamicModel/Extensions/MyODataRoutingMatcherPolicy.cs @@ -20,140 +20,139 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace ODataDynamicModel.Extensions +namespace ODataDynamicModel.Extensions; + +/// +/// Defines a policy that applies behaviors to the OData Uri matcher. +/// +internal class MyODataRoutingMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy { + private IODataTemplateTranslator _translator; + private IDataSourceProvider _provider; + + public MyODataRoutingMatcherPolicy(IODataTemplateTranslator translator, + IDataSourceProvider provider ) + { + _translator = translator; + _provider = provider; + } + + /// + /// Gets a value that determines the order of this policy. + /// + public override int Order => 900 - 1; + /// - /// Defines a policy that applies behaviors to the OData Uri matcher. + /// Returns a value that indicates whether the matcher applies to any endpoint in endpoints. /// - internal class MyODataRoutingMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy + /// The set of candidate values. + /// true if the policy applies to any endpoint in endpoints, otherwise false. + public bool AppliesToEndpoints(IReadOnlyList endpoints) { - private IODataTemplateTranslator _translator; - private IDataSourceProvider _provider; + return endpoints.Any(e => e.Metadata.OfType().FirstOrDefault() != null); + } - public MyODataRoutingMatcherPolicy(IODataTemplateTranslator translator, - IDataSourceProvider provider ) + /// + /// Applies the policy to the CandidateSet. + /// + /// The context associated with the current request. + /// The CandidateSet. + /// The task. + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "")] + public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + { + if (httpContext == null) { - _translator = translator; - _provider = provider; + throw new ArgumentNullException(nameof(httpContext)); } - /// - /// Gets a value that determines the order of this policy. - /// - public override int Order => 900 - 1; - - /// - /// Returns a value that indicates whether the matcher applies to any endpoint in endpoints. - /// - /// The set of candidate values. - /// true if the policy applies to any endpoint in endpoints, otherwise false. - public bool AppliesToEndpoints(IReadOnlyList endpoints) + IODataFeature odataFeature = httpContext.ODataFeature(); + if (odataFeature.Path != null) { - return endpoints.Any(e => e.Metadata.OfType().FirstOrDefault() != null); + // If we have the OData path setting, it means there's some Policy working. + // Let's skip this default OData matcher policy. + return Task.CompletedTask; } - /// - /// Applies the policy to the CandidateSet. - /// - /// The context associated with the current request. - /// The CandidateSet. - /// The task. - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "")] - public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + // The goal of this method is to perform the final matching: + // Map between route values matched by the template and the ones we want to expose to the action for binding. + // (tweaking the route values is fine here) + // Invalidating the candidate if the key/function values are not valid/missing. + // Perform overload resolution for functions by looking at the candidates and their metadata. + for (var i = 0; i < candidates.Count; i++) { - if (httpContext == null) + ref CandidateState candidate = ref candidates[i]; + if (!candidates.IsValidCandidate(i)) { - throw new ArgumentNullException(nameof(httpContext)); + continue; } - IODataFeature odataFeature = httpContext.ODataFeature(); - if (odataFeature.Path != null) + IODataRoutingMetadata metadata = candidate.Endpoint.Metadata.OfType().FirstOrDefault(); + if (metadata == null) { - // If we have the OData path setting, it means there's some Policy working. - // Let's skip this default OData matcher policy. - return Task.CompletedTask; + continue; } - // The goal of this method is to perform the final matching: - // Map between route values matched by the template and the ones we want to expose to the action for binding. - // (tweaking the route values is fine here) - // Invalidating the candidate if the key/function values are not valid/missing. - // Perform overload resolution for functions by looking at the candidates and their metadata. - for (var i = 0; i < candidates.Count; i++) + IEdmModel model = GetEdmModel(candidate.Values); + if (model == null) { - ref CandidateState candidate = ref candidates[i]; - if (!candidates.IsValidCandidate(i)) - { - continue; - } + continue; + } - IODataRoutingMetadata metadata = candidate.Endpoint.Metadata.OfType().FirstOrDefault(); - if (metadata == null) - { - continue; - } + ODataTemplateTranslateContext translatorContext + = new ODataTemplateTranslateContext(httpContext, candidate.Endpoint, candidate.Values, model); - IEdmModel model = GetEdmModel(candidate.Values); - if (model == null) + try + { + ODataPath odataPath = _translator.Translate(metadata.Template, translatorContext); + if (odataPath != null) { - continue; - } - - ODataTemplateTranslateContext translatorContext - = new ODataTemplateTranslateContext(httpContext, candidate.Endpoint, candidate.Values, model); + odataFeature.RoutePrefix = metadata.Prefix; + odataFeature.Model = model; + odataFeature.Path = odataPath; - try - { - ODataPath odataPath = _translator.Translate(metadata.Template, translatorContext); - if (odataPath != null) - { - odataFeature.RoutePrefix = metadata.Prefix; - odataFeature.Model = model; - odataFeature.Path = odataPath; - - MergeRouteValues(translatorContext.UpdatedValues, candidate.Values); - } - else - { - candidates.SetValidity(i, false); - } + MergeRouteValues(translatorContext.UpdatedValues, candidate.Values); } - catch + else { + candidates.SetValidity(i, false); } } - - return Task.CompletedTask; - } - - private static void MergeRouteValues(RouteValueDictionary updates, RouteValueDictionary source) - { - foreach (var data in updates) + catch { - source[data.Key] = data.Value; } } - private IEdmModel GetEdmModel(RouteValueDictionary routeValues) + return Task.CompletedTask; + } + + private static void MergeRouteValues(RouteValueDictionary updates, RouteValueDictionary source) + { + foreach (var data in updates) { - if (routeValues == null) - { - return null; - } + source[data.Key] = data.Value; + } + } - if (!routeValues.TryGetValue("datasource", out object datasourceObj)) - { - return null; - } + private IEdmModel GetEdmModel(RouteValueDictionary routeValues) + { + if (routeValues == null) + { + return null; + } - string dataSource = datasourceObj as string; - if (dataSource == null) - { - return null; - } + if (!routeValues.TryGetValue("datasource", out object datasourceObj)) + { + return null; + } - _provider.DataSources.TryGetValue(dataSource, out IDataSource edmSource); - return edmSource?.GetEdmModel(); + string dataSource = datasourceObj as string; + if (dataSource == null) + { + return null; } + + _provider.DataSources.TryGetValue(dataSource, out IDataSource edmSource); + return edmSource?.GetEdmModel(); } } diff --git a/sample/ODataDynamicModel/Extensions/NavigationTemplateSegment.cs b/sample/ODataDynamicModel/Extensions/NavigationTemplateSegment.cs index 099e137a1..3d52a6b0b 100644 --- a/sample/ODataDynamicModel/Extensions/NavigationTemplateSegment.cs +++ b/sample/ODataDynamicModel/Extensions/NavigationTemplateSegment.cs @@ -12,38 +12,37 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace ODataDynamicModel.Extensions +namespace ODataDynamicModel.Extensions; + +public class NavigationTemplateSegment : ODataSegmentTemplate { - public class NavigationTemplateSegment : ODataSegmentTemplate + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return "/{navigation}"; + } + + public override bool TryTranslate(ODataTemplateTranslateContext context) { - public override IEnumerable GetTemplates(ODataRouteOptions options) + if (!context.RouteValues.TryGetValue("navigation", out object navigationNameObj)) { - yield return "/{navigation}"; + return false; } - public override bool TryTranslate(ODataTemplateTranslateContext context) + string navigationName = navigationNameObj as string; + KeySegment keySegment = context.Segments.Last() as KeySegment; + IEdmEntityType entityType = keySegment.EdmType as IEdmEntityType; + + IEdmNavigationProperty navigationProperty = entityType.NavigationProperties().FirstOrDefault(n => n.Name == navigationName); + if (navigationProperty != null) { - if (!context.RouteValues.TryGetValue("navigation", out object navigationNameObj)) - { - return false; - } - - string navigationName = navigationNameObj as string; - KeySegment keySegment = context.Segments.Last() as KeySegment; - IEdmEntityType entityType = keySegment.EdmType as IEdmEntityType; - - IEdmNavigationProperty navigationProperty = entityType.NavigationProperties().FirstOrDefault(n => n.Name == navigationName); - if (navigationProperty != null) - { - var navigationSource = keySegment.NavigationSource; - IEdmNavigationSource targetNavigationSource = navigationSource.FindNavigationTarget(navigationProperty); - - NavigationPropertySegment seg = new NavigationPropertySegment(navigationProperty, targetNavigationSource); - context.Segments.Add(seg); - return true; - } + var navigationSource = keySegment.NavigationSource; + IEdmNavigationSource targetNavigationSource = navigationSource.FindNavigationTarget(navigationProperty); - return false; + NavigationPropertySegment seg = new NavigationPropertySegment(navigationProperty, targetNavigationSource); + context.Segments.Add(seg); + return true; } + + return false; } } diff --git a/sample/ODataDynamicModel/Extensions/StaticNameSegment.cs b/sample/ODataDynamicModel/Extensions/StaticNameSegment.cs index dee643e66..db2475212 100644 --- a/sample/ODataDynamicModel/Extensions/StaticNameSegment.cs +++ b/sample/ODataDynamicModel/Extensions/StaticNameSegment.cs @@ -12,28 +12,27 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace ODataDynamicModel.Extensions +namespace ODataDynamicModel.Extensions; + +public class StaticNameSegment : ODataSegmentTemplate { - public class StaticNameSegment : ODataSegmentTemplate + public override IEnumerable GetTemplates(ODataRouteOptions options) { - public override IEnumerable GetTemplates(ODataRouteOptions options) - { - yield return "/Name"; - } + yield return "/Name"; + } - public override bool TryTranslate(ODataTemplateTranslateContext context) + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + KeySegment keySegment = context.Segments.Last() as KeySegment; + IEdmEntityType entityType = keySegment.EdmType as IEdmEntityType; + IEdmProperty edmProperty = entityType.Properties().FirstOrDefault(p => p.Name == "Name"); + if (edmProperty != null) { - KeySegment keySegment = context.Segments.Last() as KeySegment; - IEdmEntityType entityType = keySegment.EdmType as IEdmEntityType; - IEdmProperty edmProperty = entityType.Properties().FirstOrDefault(p => p.Name == "Name"); - if (edmProperty != null) - { - PropertySegment seg = new PropertySegment(edmProperty as IEdmStructuralProperty); - context.Segments.Add(seg); - return true; - } - - return false; + PropertySegment seg = new PropertySegment(edmProperty as IEdmStructuralProperty); + context.Segments.Add(seg); + return true; } + + return false; } } diff --git a/sample/ODataDynamicModel/Program.cs b/sample/ODataDynamicModel/Program.cs index 63a5a3796..88b1bca3e 100644 --- a/sample/ODataDynamicModel/Program.cs +++ b/sample/ODataDynamicModel/Program.cs @@ -8,20 +8,19 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace ODataDynamicModel +namespace ODataDynamicModel; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + CreateHostBuilder(args).Build().Run(); } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); } diff --git a/sample/ODataDynamicModel/Startup.cs b/sample/ODataDynamicModel/Startup.cs index c2083ca9c..f6a91fa34 100644 --- a/sample/ODataDynamicModel/Startup.cs +++ b/sample/ODataDynamicModel/Startup.cs @@ -16,49 +16,48 @@ using Microsoft.Extensions.Hosting; using ODataDynamicModel.Extensions; -namespace ODataDynamicModel +namespace ODataDynamicModel; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + Configuration = configuration; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers().AddOData(); + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers().AddOData(); - services.TryAddSingleton(); + services.TryAddSingleton(); - services.TryAddEnumerable( - ServiceDescriptor.Transient()); + services.TryAddEnumerable( + ServiceDescriptor.Transient()); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - } + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } + app.UseDeveloperExceptionPage(); + } - // Use odata route debug, /$odata - app.UseODataRouteDebug(); + // Use odata route debug, /$odata + app.UseODataRouteDebug(); - app.UseRouting(); + app.UseRouting(); - app.UseAuthorization(); + app.UseAuthorization(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); } } diff --git a/sample/ODataDynamicModel/WeatherForecast.cs b/sample/ODataDynamicModel/WeatherForecast.cs index 506492a19..b21b37ed2 100644 --- a/sample/ODataDynamicModel/WeatherForecast.cs +++ b/sample/ODataDynamicModel/WeatherForecast.cs @@ -7,16 +7,15 @@ using System; -namespace ODataDynamicModel +namespace ODataDynamicModel; + +public class WeatherForecast { - public class WeatherForecast - { - public DateTime Date { get; set; } + public DateTime Date { get; set; } - public int TemperatureC { get; set; } + public int TemperatureC { get; set; } - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - public string Summary { get; set; } - } + public string Summary { get; set; } } diff --git a/sample/ODataNewtonsoftJsonSample/Controllers/WeatherForecastController.cs b/sample/ODataNewtonsoftJsonSample/Controllers/WeatherForecastController.cs index ee445b553..ecf2c6c2d 100644 --- a/sample/ODataNewtonsoftJsonSample/Controllers/WeatherForecastController.cs +++ b/sample/ODataNewtonsoftJsonSample/Controllers/WeatherForecastController.cs @@ -13,47 +13,46 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace ODataNewtonsoftJsonSample.Controllers +namespace ODataNewtonsoftJsonSample.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase { - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase + private static readonly string[] Summaries = new[] { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; - private readonly ILogger _logger; - private readonly IOptions _options; + private readonly ILogger _logger; + private readonly IOptions _options; - public WeatherForecastController(ILogger logger, IOptions options) - { - _logger = logger; - _options = options; - } + public WeatherForecastController(ILogger logger, IOptions options) + { + _logger = logger; + _options = options; + } - // For example: http://localhost:12197/weatherforecast?$select=TemperatureC (IIS) - // For example: http://localhost:5000/weatherforecast?$select=TemperatureC (Non-IIS) - // Be noted: since we register "WeatherForecast" as one entity set name. - // OData conventional routing will build a new route for this method. - // If you send "$odata" request, you will see this method has three routes: - // 1) ~/odata/WeatherForecast - // 2) ~/odata/WeatherForecast/$count - // 3) ~/WeatherForecast - // Try it and you will get different payload. - [HttpGet] - [EnableQuery] - public IEnumerable Get() + // For example: http://localhost:12197/weatherforecast?$select=TemperatureC (IIS) + // For example: http://localhost:5000/weatherforecast?$select=TemperatureC (Non-IIS) + // Be noted: since we register "WeatherForecast" as one entity set name. + // OData conventional routing will build a new route for this method. + // If you send "$odata" request, you will see this method has three routes: + // 1) ~/odata/WeatherForecast + // 2) ~/odata/WeatherForecast/$count + // 3) ~/WeatherForecast + // Try it and you will get different payload. + [HttpGet] + [EnableQuery] + public IEnumerable Get() + { + var rng = new Random(); + return Enumerable.Range(1, 5).Select(index => new WeatherForecast { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }) - .ToArray(); - } + Date = DateTime.Now.AddDays(index), + TemperatureC = rng.Next(-20, 55), + Summary = Summaries[rng.Next(Summaries.Length)] + }) + .ToArray(); } } diff --git a/sample/ODataNewtonsoftJsonSample/Program.cs b/sample/ODataNewtonsoftJsonSample/Program.cs index b9ba37f84..83c48a0b8 100644 --- a/sample/ODataNewtonsoftJsonSample/Program.cs +++ b/sample/ODataNewtonsoftJsonSample/Program.cs @@ -8,20 +8,19 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace ODataNewtonsoftJsonSample +namespace ODataNewtonsoftJsonSample; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + CreateHostBuilder(args).Build().Run(); } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); } diff --git a/sample/ODataNewtonsoftJsonSample/Startup.cs b/sample/ODataNewtonsoftJsonSample/Startup.cs index 2931a3bc8..473f77687 100644 --- a/sample/ODataNewtonsoftJsonSample/Startup.cs +++ b/sample/ODataNewtonsoftJsonSample/Startup.cs @@ -15,59 +15,58 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace ODataNewtonsoftJsonSample +namespace ODataNewtonsoftJsonSample; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + Configuration = configuration; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers() - .AddOData(opt => opt.Select().Filter().Count().SetMaxTop(10).AddRouteComponents("odata", GetEdmModel())) - .AddODataNewtonsoftJson() - //.AddNewtonsoftJson( - //options => - //{ - // options.SerializerSettings.DefaultValueHandling = Newtonsoft.Json.DefaultValueHandling.Ignore; - // options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; - // //options.SerializerSettings.ContractResolver = WebApiJsonResolver.Instance; - //}) - ; - } + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers() + .AddOData(opt => opt.Select().Filter().Count().SetMaxTop(10).AddRouteComponents("odata", GetEdmModel())) + .AddODataNewtonsoftJson() + //.AddNewtonsoftJson( + //options => + //{ + // options.SerializerSettings.DefaultValueHandling = Newtonsoft.Json.DefaultValueHandling.Ignore; + // options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; + // //options.SerializerSettings.ContractResolver = WebApiJsonResolver.Instance; + //}) + ; + } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseODataRouteDebug(); + app.UseDeveloperExceptionPage(); + } - app.UseRouting(); + app.UseODataRouteDebug(); - app.UseAuthorization(); + app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + app.UseAuthorization(); - IEdmModel GetEdmModel() + app.UseEndpoints(endpoints => { - var odataBuilder = new ODataConventionModelBuilder(); + endpoints.MapControllers(); + }); + } - odataBuilder.EntitySet(nameof(WeatherForecast)); - return odataBuilder.GetEdmModel(); - } + IEdmModel GetEdmModel() + { + var odataBuilder = new ODataConventionModelBuilder(); + + odataBuilder.EntitySet(nameof(WeatherForecast)); + return odataBuilder.GetEdmModel(); } } diff --git a/sample/ODataNewtonsoftJsonSample/WeatherForecast.cs b/sample/ODataNewtonsoftJsonSample/WeatherForecast.cs index edb668f2c..1bd5ec32f 100644 --- a/sample/ODataNewtonsoftJsonSample/WeatherForecast.cs +++ b/sample/ODataNewtonsoftJsonSample/WeatherForecast.cs @@ -8,19 +8,18 @@ using System; using System.ComponentModel.DataAnnotations; -namespace ODataNewtonsoftJsonSample +namespace ODataNewtonsoftJsonSample; + +public class WeatherForecast { - public class WeatherForecast - { - [Key] - public int Key { get; set; } + [Key] + public int Key { get; set; } - public DateTime Date { get; set; } + public DateTime Date { get; set; } - public int TemperatureC { get; set; } + public int TemperatureC { get; set; } - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - public string Summary { get; set; } - } + public string Summary { get; set; } } diff --git a/sample/ODataRoutingSample/Controllers/AccountsController.cs b/sample/ODataRoutingSample/Controllers/AccountsController.cs index bcc2cbefe..e95e3164a 100644 --- a/sample/ODataRoutingSample/Controllers/AccountsController.cs +++ b/sample/ODataRoutingSample/Controllers/AccountsController.cs @@ -13,93 +13,92 @@ using Microsoft.Extensions.Logging; using ODataRoutingSample.Models; -namespace ODataRoutingSample.Controllers +namespace ODataRoutingSample.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AccountsController : ControllerBase { - [ApiController] - [Route("api/[controller]")] - public class AccountsController : ControllerBase - { - private static IList accounts; + private static IList accounts; - static AccountsController() + static AccountsController() + { + string[] names = new[] { - string[] names = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - Random rd = new(); + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + Random rd = new(); - string[] startingGuid = new string[] - { - "51496043-D2BB-4940-B579-3A94E8B7C773", - "240A32E6-5301-425E-A700-729CDD46CD42", - "5726E1F8-70BD-43E0-BE96-185D7163EF3D", - "305FCB7B-923F-4281-9163-A7B007EB7841", - "4B5445B4-E794-4ABE-BCF3-D663D3B8B45F", - "094863F1-7F5A-486E-A194-E87CED20D27E", - "A7BD97F8-C69A-4AA3-88A3-30B615C4C182", - "4D49FB7F-E631-4FD4-A62E-6D654705CE63", - "19C865CE-989E-41D5-A7E7-775B4E967CCB", - "1398B2AC-E9A9-4F86-A991-4078AC565F91" - }; + string[] startingGuid = new string[] + { + "51496043-D2BB-4940-B579-3A94E8B7C773", + "240A32E6-5301-425E-A700-729CDD46CD42", + "5726E1F8-70BD-43E0-BE96-185D7163EF3D", + "305FCB7B-923F-4281-9163-A7B007EB7841", + "4B5445B4-E794-4ABE-BCF3-D663D3B8B45F", + "094863F1-7F5A-486E-A194-E87CED20D27E", + "A7BD97F8-C69A-4AA3-88A3-30B615C4C182", + "4D49FB7F-E631-4FD4-A62E-6D654705CE63", + "19C865CE-989E-41D5-A7E7-775B4E967CCB", + "1398B2AC-E9A9-4F86-A991-4078AC565F91" + }; - accounts = new List(); - for (int i = 1; i <= names.Length; i++) + accounts = new List(); + for (int i = 1; i <= names.Length; i++) + { + int num = rd.Next(20, 200); + Account a = new Account { - int num = rd.Next(20, 200); - Account a = new Account + AccountId = new Guid(startingGuid[i-1]), + Name = names[i - 1], + HomeAddress = new Address { - AccountId = new Guid(startingGuid[i-1]), - Name = names[i - 1], - HomeAddress = new Address - { - Street = $"Road No{num}", - City = $"City No{num}" - }, - AccountInfo = new AccountInfo - { - Id = i, - Balance = (rd.NextDouble() + 1.0) * 100 - } - }; + Street = $"Road No{num}", + City = $"City No{num}" + }, + AccountInfo = new AccountInfo + { + Id = i, + Balance = (rd.NextDouble() + 1.0) * 100 + } + }; - accounts.Add(a); - } + accounts.Add(a); } + } - private readonly ILogger _logger; + private readonly ILogger _logger; - public AccountsController(ILogger logger) - { - _logger = logger; - } + public AccountsController(ILogger logger) + { + _logger = logger; + } - [HttpGet] - public IActionResult Get(ODataQueryOptions queryOptions) - { - var querable = accounts.AsQueryable(); - var finalQuery = queryOptions.ApplyTo(querable); - return Ok(finalQuery); - } + [HttpGet] + public IActionResult Get(ODataQueryOptions queryOptions) + { + var querable = accounts.AsQueryable(); + var finalQuery = queryOptions.ApplyTo(querable); + return Ok(finalQuery); + } - [HttpGet("{id}")] - public IActionResult Get(Guid id, ODataQueryOptions queryOptions) + [HttpGet("{id}")] + public IActionResult Get(Guid id, ODataQueryOptions queryOptions) + { + var accountQuery = accounts.Where(c => c.AccountId == id); + if (!accountQuery.Any()) { - var accountQuery = accounts.Where(c => c.AccountId == id); - if (!accountQuery.Any()) - { - return NotFound(); - } - - var finalQuery = queryOptions.ApplyTo(accountQuery.AsQueryable()) as IQueryable; - var result = finalQuery.FirstOrDefault(); + return NotFound(); + } - if (result == null) - { - return NotFound(); - } + var finalQuery = queryOptions.ApplyTo(accountQuery.AsQueryable()) as IQueryable; + var result = finalQuery.FirstOrDefault(); - return Ok(result); + if (result == null) + { + return NotFound(); } + + return Ok(result); } } diff --git a/sample/ODataRoutingSample/Controllers/ODataOpenApiController.cs b/sample/ODataRoutingSample/Controllers/ODataOpenApiController.cs index 5ef01e61e..cd807733e 100644 --- a/sample/ODataRoutingSample/Controllers/ODataOpenApiController.cs +++ b/sample/ODataRoutingSample/Controllers/ODataOpenApiController.cs @@ -24,204 +24,203 @@ using Microsoft.OpenApi.OData.Edm; using ODataRoutingSample.OpenApi; -namespace ODataRoutingSample.Controllers +namespace ODataRoutingSample.Controllers; + +public class ODataOpenApiController : ControllerBase { - public class ODataOpenApiController : ControllerBase - { - private EndpointDataSource _dataSource; + private EndpointDataSource _dataSource; - public ODataOpenApiController(EndpointDataSource dataSource) - { - _dataSource = dataSource; - } + public ODataOpenApiController(EndpointDataSource dataSource) + { + _dataSource = dataSource; + } - [HttpGet("$openapi")] - public ContentResult GetOpenApi() - { - OpenApiDocument document = CreateDocument(string.Empty); - return CreateContent(document); - } + [HttpGet("$openapi")] + public ContentResult GetOpenApi() + { + OpenApiDocument document = CreateDocument(string.Empty); + return CreateContent(document); + } - [HttpGet("v1/$openapi")] - public ContentResult GetV1OpenApi() - { - OpenApiDocument document = CreateDocument("v1"); - return CreateContent(document); - } + [HttpGet("v1/$openapi")] + public ContentResult GetV1OpenApi() + { + OpenApiDocument document = CreateDocument("v1"); + return CreateContent(document); + } - [HttpGet("v2{data}/$openapi")] - public ContentResult GetV2OpenApi(string data) - { - OpenApiDocument document = CreateDocument("v2{data}"); - return CreateContent(document); - } + [HttpGet("v2{data}/$openapi")] + public ContentResult GetV2OpenApi(string data) + { + OpenApiDocument document = CreateDocument("v2{data}"); + return CreateContent(document); + } - private ContentResult CreateContent(OpenApiDocument document) - { - HttpContext httpContext = Request.HttpContext; - (string contentType, OpenApiSpecVersion openApiSpecVersion) = GetContentTypeAndVersion(httpContext); - httpContext.Response.Headers["Content-Type"] = contentType; + private ContentResult CreateContent(OpenApiDocument document) + { + HttpContext httpContext = Request.HttpContext; + (string contentType, OpenApiSpecVersion openApiSpecVersion) = GetContentTypeAndVersion(httpContext); + httpContext.Response.Headers["Content-Type"] = contentType; - string output; + string output; - if (openApiSpecVersion == OpenApiSpecVersion.OpenApi3_0) + if (openApiSpecVersion == OpenApiSpecVersion.OpenApi3_0) + { + if (contentType == "application/json") { - if (contentType == "application/json") - { - output = document.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0); - } - else - { - output = document.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); - } + output = document.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0); } else { - if (contentType == "application/json") - { - output = document.SerializeAsJson(OpenApiSpecVersion.OpenApi2_0); - } - else - { - output = document.SerializeAsYaml(OpenApiSpecVersion.OpenApi2_0); - } + output = document.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); } - - return base.Content(output, contentType); } - - private OpenApiDocument CreateDocument(string prefixName) + else { - IDictionary tempateToPathDict = new Dictionary(); - ODataOpenApiPathProvider provider = new ODataOpenApiPathProvider(); - IEdmModel model = null; - foreach (var endpoint in _dataSource.Endpoints) + if (contentType == "application/json") { - IODataRoutingMetadata metadata = endpoint.Metadata.GetMetadata(); - if (metadata == null) - { - continue; - } - - if (metadata.Prefix != prefixName) - { - continue; - } - model = metadata.Model; + output = document.SerializeAsJson(OpenApiSpecVersion.OpenApi2_0); + } + else + { + output = document.SerializeAsYaml(OpenApiSpecVersion.OpenApi2_0); + } + } - RouteEndpoint routeEndpoint = endpoint as RouteEndpoint; - if (routeEndpoint == null) - { - continue; - } + return base.Content(output, contentType); + } - // get rid of the prefix - int length = prefixName.Length; - string routePathTemplate = routeEndpoint.RoutePattern.RawText.Substring(length); - routePathTemplate = routePathTemplate.StartsWith("/") ? routePathTemplate : "/" + routePathTemplate; + private OpenApiDocument CreateDocument(string prefixName) + { + IDictionary tempateToPathDict = new Dictionary(); + ODataOpenApiPathProvider provider = new ODataOpenApiPathProvider(); + IEdmModel model = null; + foreach (var endpoint in _dataSource.Endpoints) + { + IODataRoutingMetadata metadata = endpoint.Metadata.GetMetadata(); + if (metadata == null) + { + continue; + } - if (tempateToPathDict.TryGetValue(routePathTemplate, out ODataPath pathValue)) - { - var methods = GetHttpMethods(endpoint); - foreach (var method in methods) - { - pathValue.HttpMethods.Add(method); - } - continue; - } + if (metadata.Prefix != prefixName) + { + continue; + } + model = metadata.Model; - var path = metadata.Template.Translate(); - if (path == null) - { - continue; - } + RouteEndpoint routeEndpoint = endpoint as RouteEndpoint; + if (routeEndpoint == null) + { + continue; + } - path.PathTemplate = routePathTemplate; - provider.Add(path); + // get rid of the prefix + int length = prefixName.Length; + string routePathTemplate = routeEndpoint.RoutePattern.RawText.Substring(length); + routePathTemplate = routePathTemplate.StartsWith("/") ? routePathTemplate : "/" + routePathTemplate; - var methods1 = GetHttpMethods(endpoint); - foreach (var method in methods1) + if (tempateToPathDict.TryGetValue(routePathTemplate, out ODataPath pathValue)) + { + var methods = GetHttpMethods(endpoint); + foreach (var method in methods) { - path.HttpMethods.Add(method); + pathValue.HttpMethods.Add(method); } + continue; + } - tempateToPathDict[routePathTemplate] = path; + var path = metadata.Template.Translate(); + if (path == null) + { + continue; } - OpenApiConvertSettings settings = new OpenApiConvertSettings + path.PathTemplate = routePathTemplate; + provider.Add(path); + + var methods1 = GetHttpMethods(endpoint); + foreach (var method in methods1) { - PathProvider = provider, - ServiceRoot = BuildAbsolute() - }; + path.HttpMethods.Add(method); + } - return model.ConvertToOpenApi(settings); + tempateToPathDict[routePathTemplate] = path; } - internal static (string, OpenApiSpecVersion) GetContentTypeAndVersion(HttpContext context) + OpenApiConvertSettings settings = new OpenApiConvertSettings { - Contract.Assert(context != null); + PathProvider = provider, + ServiceRoot = BuildAbsolute() + }; - OpenApiSpecVersion specVersion = OpenApiSpecVersion.OpenApi3_0; // by default - // $format=application/json;version=2.0 - // $format=application/yaml;version=2.0 - // accept=application/json;version3.0 - HttpRequest request = context.Request; + return model.ConvertToOpenApi(settings); + } - string dollarFormatValue = null; - IQueryCollection queryCollection = request.Query; - if (queryCollection.ContainsKey("$format")) - { - StringValues dollarFormat = queryCollection["$format"]; - dollarFormatValue = dollarFormat.FirstOrDefault(); - } + internal static (string, OpenApiSpecVersion) GetContentTypeAndVersion(HttpContext context) + { + Contract.Assert(context != null); + + OpenApiSpecVersion specVersion = OpenApiSpecVersion.OpenApi3_0; // by default + // $format=application/json;version=2.0 + // $format=application/yaml;version=2.0 + // accept=application/json;version3.0 + HttpRequest request = context.Request; - if (dollarFormatValue != null) + string dollarFormatValue = null; + IQueryCollection queryCollection = request.Query; + if (queryCollection.ContainsKey("$format")) + { + StringValues dollarFormat = queryCollection["$format"]; + dollarFormatValue = dollarFormat.FirstOrDefault(); + } + + if (dollarFormatValue != null) + { + MediaTypeHeaderValue parsedValue; + bool success = MediaTypeHeaderValue.TryParse(dollarFormatValue, out parsedValue); + if (success) { - MediaTypeHeaderValue parsedValue; - bool success = MediaTypeHeaderValue.TryParse(dollarFormatValue, out parsedValue); - if (success) + NameValueHeaderValue nameValueHeaderValue = parsedValue.Parameters.FirstOrDefault(p => p.Name == "version"); + if (nameValueHeaderValue != null) { - NameValueHeaderValue nameValueHeaderValue = parsedValue.Parameters.FirstOrDefault(p => p.Name == "version"); - if (nameValueHeaderValue != null) + string version = nameValueHeaderValue.Value.Value; + if (version == "2.0") { - string version = nameValueHeaderValue.Value.Value; - if (version == "2.0") - { - specVersion = OpenApiSpecVersion.OpenApi2_0; - } + specVersion = OpenApiSpecVersion.OpenApi2_0; } - - if (parsedValue.MediaType == "application/yaml") - { - return ("application/yaml", specVersion); - } - - return ("application/json", specVersion); } - } - // default - return ("application/json", OpenApiSpecVersion.OpenApi3_0); - } + if (parsedValue.MediaType == "application/yaml") + { + return ("application/yaml", specVersion); + } - private static IEnumerable GetHttpMethods(Endpoint endpoint) - { - HttpMethodMetadata methodMetadata = endpoint.Metadata.GetMetadata(); - if (methodMetadata != null) - { - return methodMetadata.HttpMethods; + return ("application/json", specVersion); } - - throw new Exception(); } - private Uri BuildAbsolute() + // default + return ("application/json", OpenApiSpecVersion.OpenApi3_0); + } + + private static IEnumerable GetHttpMethods(Endpoint endpoint) + { + HttpMethodMetadata methodMetadata = endpoint.Metadata.GetMetadata(); + if (methodMetadata != null) { - HttpRequest request = Request; - string wholeRequest = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path); - int index = wholeRequest.IndexOf("/$openapi"); - string path = wholeRequest.Substring(0, index); - return new Uri(path); + return methodMetadata.HttpMethods; } + + throw new Exception(); + } + + private Uri BuildAbsolute() + { + HttpRequest request = Request; + string wholeRequest = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path); + int index = wholeRequest.IndexOf("/$openapi"); + string path = wholeRequest.Substring(0, index); + return new Uri(path); } } diff --git a/sample/ODataRoutingSample/Controllers/ODataOperationImportController.cs b/sample/ODataRoutingSample/Controllers/ODataOperationImportController.cs index 6de8015ef..c99e87535 100644 --- a/sample/ODataRoutingSample/Controllers/ODataOperationImportController.cs +++ b/sample/ODataRoutingSample/Controllers/ODataOperationImportController.cs @@ -11,20 +11,19 @@ using Microsoft.AspNetCore.Mvc; using ODataRoutingSample.Models; -namespace ODataRoutingSample.Controllers +namespace ODataRoutingSample.Controllers; + +public class ODataOperationImportController : ControllerBase { - public class ODataOperationImportController : ControllerBase + [HttpPost] + public IEnumerable ResetData() { - [HttpPost] - public IEnumerable ResetData() + var rng = new Random(); + return Enumerable.Range(1, 5).Select(index => new Product { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new Product - { - Id = index, - Category = "Category + " + index - }) - .ToArray(); - } + Id = index, + Category = "Category + " + index + }) + .ToArray(); } } diff --git a/sample/ODataRoutingSample/Controllers/PeopleController.cs b/sample/ODataRoutingSample/Controllers/PeopleController.cs index ab9d77c68..02587f2a8 100644 --- a/sample/ODataRoutingSample/Controllers/PeopleController.cs +++ b/sample/ODataRoutingSample/Controllers/PeopleController.cs @@ -15,274 +15,273 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using ODataRoutingSample.Models; -namespace ODataRoutingSample.Controllers +namespace ODataRoutingSample.Controllers; + +public class PeopleController : ODataController { - public class PeopleController : ODataController + private static IList _persons = new List { - private static IList _persons = new List + new Person { - new Person - { - FirstName = "Goods", - LastName = "Zha/ngg", - }, - new Person - { - FirstName = "Magazine", - LastName = "Jingchan", - }, - new Person - { - FirstName = "Fiction", - LastName = "Hollewye" - }, - }; + FirstName = "Goods", + LastName = "Zha/ngg", + }, + new Person + { + FirstName = "Magazine", + LastName = "Jingchan", + }, + new Person + { + FirstName = "Fiction", + LastName = "Hollewye" + }, + }; - [HttpGet] - [EnableQuery] - public IActionResult Get(CancellationToken token) + [HttpGet] + [EnableQuery] + public IActionResult Get(CancellationToken token) + { + return Ok(_persons); + } + + // People(FirstName='Goods',LastName='Zha%2Fngg') + [HttpGet] + [EnableQuery] + public IActionResult Get(string keyFirstName, string keyLastName) + { + var person = _persons.FirstOrDefault(p => p.FirstName == keyFirstName && p.LastName == keyLastName); + if (person == null) { - return Ok(_persons); + return NotFound($"Not found person with FirstName = {keyFirstName} and LastName = {keyLastName}"); } - // People(FirstName='Goods',LastName='Zha%2Fngg') - [HttpGet] - [EnableQuery] - public IActionResult Get(string keyFirstName, string keyLastName) - { - var person = _persons.FirstOrDefault(p => p.FirstName == keyFirstName && p.LastName == keyLastName); - if (person == null) - { - return NotFound($"Not found person with FirstName = {keyFirstName} and LastName = {keyLastName}"); - } + return Ok(person); + } - return Ok(person); - } + [HttpPost] + [EnableQuery] + public IActionResult Post([FromBody] Person person) + { + /* As example: If you send a POST request to: http://localhost:5000/People, you can get all things with 'person' parameter. - [HttpPost] - [EnableQuery] - public IActionResult Post([FromBody] Person person) +"firstname": "sam", +"lastname": "xu", +"data":[[42],{"k1": "abc", "k2": 42, "k3": { "a1": 2, "b2": null}, "k4": [null, 42]}], +"infos": [ 42, "str"], +"customProperties": { + "NullProp": null, + "CollectionDynamic": [ { - /* As example: If you send a POST request to: http://localhost:5000/People, you can get all things with 'person' parameter. -{ - "firstname": "sam", - "lastname": "xu", - "data":[[42],{"k1": "abc", "k2": 42, "k3": { "a1": 2, "b2": null}, "k4": [null, 42]}], - "infos": [ 42, "str"], - "customProperties": { - "NullProp": null, - "CollectionDynamic": [ - { - "P1": "v1", - "P2": "v2" - }, - { - "Y1": 1, - "Y2": true + "P1": "v1", + "P2": "v2" + }, + { + "Y1": 1, + "Y2": true + } + ], + "StringEqualsIgnoreCase": { + "IntProp": 42, + "awsTag":[ + null, + { + "X1": "Red", + "Data": { + "D1": 42 } - ], - "StringEqualsIgnoreCase": { - "IntProp": 42, - "awsTag":[ - null, - { - "X1": "Red", - "Data": { - "D1": 42 - } - }, - "finance", - "hr", - "legal", - 43 - ] - }, - "key1": "value1", - "key2": [ - "value2", - "value3" - ], - "key3": "value4" - } + }, + "finance", + "hr", + "legal", + 43 + ] + }, + "key1": "value1", + "key2": [ + "value2", + "value3" + ], + "key3": "value4" } - */ +} + */ - if (person == null || !ModelState.IsValid) - { - var message = string.Join(" | ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage)); - return BadRequest(message); - } + if (person == null || !ModelState.IsValid) + { + var message = string.Join(" | ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage)); + return BadRequest(message); + } - _persons.Add(person); - person.CustomProperties.Properties.TryGetValue("StringEqualsIgnoreCase", out var property); + _persons.Add(person); + person.CustomProperties.Properties.TryGetValue("StringEqualsIgnoreCase", out var property); - EdmUntypedObject odataObject = property as EdmUntypedObject; - if (odataObject != null) + EdmUntypedObject odataObject = property as EdmUntypedObject; + if (odataObject != null) + { + IEnumerable enumerable = odataObject["awsTag"] as IEnumerable; + foreach (var a in enumerable) { - IEnumerable enumerable = odataObject["awsTag"] as IEnumerable; - foreach (var a in enumerable) - { - // do thing, just for testing. - } + // do thing, just for testing. } + } - // Keep the following codes, it tests - // 1) you can use any C# class to create an object and assign it to 'Edm.Untyped' property - // 2) you can use dictionary now also. - //person.Other = new Address - //{ - // City = "Redmond", - // Street = "148TH AVE NE" - //}; - person.Other = new Dictionary - { - { "City", "Redmond" }, - { "Street/A", "1345TH" } - }; + // Keep the following codes, it tests + // 1) you can use any C# class to create an object and assign it to 'Edm.Untyped' property + // 2) you can use dictionary now also. + //person.Other = new Address + //{ + // City = "Redmond", + // Street = "148TH AVE NE" + //}; + person.Other = new Dictionary + { + { "City", "Redmond" }, + { "Street/A", "1345TH" } + }; - person.Sources = new List + person.Sources = new List + { + null, + 42, + "A string Value", + Color.Red, + AnyEnum.E1, + true, + new Address { City = "Issaquah", Street = "Klahanie Way" }, + new EdmUntypedObject + { + { "a1", "abc" } + }, + new EdmUntypedCollection { null, - 42, "A string Value", - Color.Red, - AnyEnum.E1, - true, new Address { City = "Issaquah", Street = "Klahanie Way" }, - new EdmUntypedObject + new Person { - { "a1", "abc" } - }, - new EdmUntypedCollection - { - null, - "A string Value", - new Address { City = "Issaquah", Street = "Klahanie Way" }, - new Person - { - FirstName = "Kerry", LastName = "Xu", - Infos = new List { 1, 2, 3 }, // Collection should have a value. - Sources = new List { } // Collection should have a value. - } + FirstName = "Kerry", LastName = "Xu", + Infos = new List { 1, 2, 3 }, // Collection should have a value. + Sources = new List { } // Collection should have a value. } - }; + } + }; - /* If you keep the above value, you could get the following response (It could be not up-to-date) -{ - "@odata.context": "http://localhost:5000/$metadata#People/$entity", - "@odata.type": "#ODataRoutingSample.Models.Person", - "@odata.id": "http://localhost:5000/People(FirstName='sam',LastName='xu')", - "@odata.editLink": "People(FirstName='sam',LastName='xu')", - "FirstName": "sam", - "LastName": "xu", - "Data": [ - [ - 42 - ], - { - "k1": "abc", - "k2@odata.type": "#Decimal", - "k2": 42, - "k3": { - "a1@odata.type": "#Decimal", - "a1": 2, - "b2": null - }, - "k4": [ - null, - 42 - ] - } + /* If you keep the above value, you could get the following response (It could be not up-to-date) + +"@odata.context": "http://localhost:5000/$metadata#People/$entity", +"@odata.type": "#ODataRoutingSample.Models.Person", +"@odata.id": "http://localhost:5000/People(FirstName='sam',LastName='xu')", +"@odata.editLink": "People(FirstName='sam',LastName='xu')", +"FirstName": "sam", +"LastName": "xu", +"Data": [ + [ + 42 ], - "Other": { - "City": "Redmond", - "Street/A": "1345TH" + { + "k1": "abc", + "k2@odata.type": "#Decimal", + "k2": 42, + "k3": { + "a1@odata.type": "#Decimal", + "a1": 2, + "b2": null + }, + "k4": [ + null, + 42 + ] + } +], +"Other": { + "City": "Redmond", + "Street/A": "1345TH" +}, +"Infos": [ + 42, + "str" +], +"Sources": [ + null, + 42, + "A string Value", + "Red", + "E1", + true, + { + "City": "Issaquah", + "Street": "Klahanie Way" }, - "Infos": [ - 42, - "str" - ], - "Sources": [ + { + "a1": "abc" + }, + [ null, - 42, "A string Value", - "Red", - "E1", - true, { "City": "Issaquah", "Street": "Klahanie Way" }, { - "a1": "abc" + "@odata.type": "#ODataRoutingSample.Models.Person", + "@odata.id": "http://localhost:5000/People(FirstName='Kerry',LastName='Xu')", + "@odata.editLink": "People(FirstName='Kerry',LastName='Xu')", + "FirstName": "Kerry", + "LastName": "Xu", + "Data": null, + "Other": null, + "Infos": [ + 1, + 2, + 3 + ], + "Sources": [], + "CustomProperties": null + } + ] +], +"CustomProperties": { + "@odata.type": "#ODataRoutingSample.Models.PersonExtraInfo", + "NullProp": null, + "key1": "value1", + "key3": "value4", + "CollectionDynamic": [ + { + "P1": "v1", + "P2": "v2" }, - [ - null, - "A string Value", - { - "City": "Issaquah", - "Street": "Klahanie Way" - }, - { - "@odata.type": "#ODataRoutingSample.Models.Person", - "@odata.id": "http://localhost:5000/People(FirstName='Kerry',LastName='Xu')", - "@odata.editLink": "People(FirstName='Kerry',LastName='Xu')", - "FirstName": "Kerry", - "LastName": "Xu", - "Data": null, - "Other": null, - "Infos": [ - 1, - 2, - 3 - ], - "Sources": [], - "CustomProperties": null - } - ] + { + "Y1@odata.type": "#Decimal", + "Y1": 1, + "Y2": true + } ], - "CustomProperties": { - "@odata.type": "#ODataRoutingSample.Models.PersonExtraInfo", - "NullProp": null, - "key1": "value1", - "key3": "value4", - "CollectionDynamic": [ + "StringEqualsIgnoreCase": { + "IntProp@odata.type": "#Decimal", + "IntProp": 42, + "awsTag": [ + null, { - "P1": "v1", - "P2": "v2" + "X1": "Red", + "Data": { + "D1@odata.type": "#Decimal", + "D1": 42 + } }, - { - "Y1@odata.type": "#Decimal", - "Y1": 1, - "Y2": true - } - ], - "StringEqualsIgnoreCase": { - "IntProp@odata.type": "#Decimal", - "IntProp": 42, - "awsTag": [ - null, - { - "X1": "Red", - "Data": { - "D1@odata.type": "#Decimal", - "D1": 42 - } - }, - "finance", - "hr", - "legal", - 43 - ] - }, - "key2": [ - "value2", - "value3" + "finance", + "hr", + "legal", + 43 ] - } + }, + "key2": [ + "value2", + "value3" + ] } - */ - return Created(person); - } +} + */ + return Created(person); } } diff --git a/sample/ODataRoutingSample/Controllers/ProductsController.cs b/sample/ODataRoutingSample/Controllers/ProductsController.cs index ae708509a..ac81a2261 100644 --- a/sample/ODataRoutingSample/Controllers/ProductsController.cs +++ b/sample/ODataRoutingSample/Controllers/ProductsController.cs @@ -15,204 +15,203 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using ODataRoutingSample.Models; -namespace ODataRoutingSample.Controllers +namespace ODataRoutingSample.Controllers; + +public class ProductsController : ODataController/*ControllerBase*/ { - public class ProductsController : ODataController/*ControllerBase*/ + private MyDataContext _context; + + public ProductsController(MyDataContext context) { - private MyDataContext _context; + _context = context; - public ProductsController(MyDataContext context) + if (_context.Products.Count() == 0) { - _context = context; - - if (_context.Products.Count() == 0) + IList products = new List { - IList products = new List + new Product { - new Product - { - Category = "Goods", - Color = Color.Red, - CreatedDate = new DateTimeOffset(2001, 4, 15, 16, 24, 8, TimeSpan.FromHours(-8)), - UpdatedDate = new DateTimeOffset(2011, 2, 15, 16, 24, 8, TimeSpan.FromHours(-8)), - Detail = new ProductDetail { Id = "3", Info = "Zhang" }, - }, - new Product - { - Category = "Magazine", - Color = Color.Blue, - CreatedDate = new DateTimeOffset(2021, 12, 27, 9, 12, 8, TimeSpan.FromHours(-8)), - UpdatedDate = null, - Detail = new ProductDetail { Id = "4", Info = "Jinchan" }, - }, - new Product - { - Category = "Fiction", - Color = Color.Green, - CreatedDate = new DateTimeOffset(1978, 11, 15, 9, 24, 8, TimeSpan.FromHours(-8)), - UpdatedDate = new DateTimeOffset(1987, 2, 25, 5, 1, 8, TimeSpan.FromHours(-8)), - Detail = new ProductDetail { Id = "5", Info = "Hollewye" }, - }, - }; - - foreach (var product in products) + Category = "Goods", + Color = Color.Red, + CreatedDate = new DateTimeOffset(2001, 4, 15, 16, 24, 8, TimeSpan.FromHours(-8)), + UpdatedDate = new DateTimeOffset(2011, 2, 15, 16, 24, 8, TimeSpan.FromHours(-8)), + Detail = new ProductDetail { Id = "3", Info = "Zhang" }, + }, + new Product { - _context.Products.Add(product); - } - - _context.SaveChanges(); + Category = "Magazine", + Color = Color.Blue, + CreatedDate = new DateTimeOffset(2021, 12, 27, 9, 12, 8, TimeSpan.FromHours(-8)), + UpdatedDate = null, + Detail = new ProductDetail { Id = "4", Info = "Jinchan" }, + }, + new Product + { + Category = "Fiction", + Color = Color.Green, + CreatedDate = new DateTimeOffset(1978, 11, 15, 9, 24, 8, TimeSpan.FromHours(-8)), + UpdatedDate = new DateTimeOffset(1987, 2, 25, 5, 1, 8, TimeSpan.FromHours(-8)), + Detail = new ProductDetail { Id = "5", Info = "Hollewye" }, + }, + }; + + foreach (var product in products) + { + _context.Products.Add(product); } - } - [HttpGet] - [EnableQuery] - public IActionResult Get(CancellationToken token) - { - return Ok(_context.Products); + _context.SaveChanges(); } + } - [EnableQuery] - [HttpGet] - public IActionResult Get(int key) - { - var product = _context.Products.FirstOrDefault(p => p.Id == key); - if (product == null) - { - return NotFound($"Not found product with id = {key}"); - } + [HttpGet] + [EnableQuery] + public IActionResult Get(CancellationToken token) + { + return Ok(_context.Products); + } - return Ok(product); + [EnableQuery] + [HttpGet] + public IActionResult Get(int key) + { + var product = _context.Products.FirstOrDefault(p => p.Id == key); + if (product == null) + { + return NotFound($"Not found product with id = {key}"); } - [HttpPost] - // [EnableQuery] - public IActionResult Post([FromBody]Product product, CancellationToken token) - { - _context.Products.Add(product); + return Ok(product); + } - _context.SaveChanges(); + [HttpPost] + // [EnableQuery] + public IActionResult Post([FromBody]Product product, CancellationToken token) + { + _context.Products.Add(product); - return Created(product); - } + _context.SaveChanges(); - [HttpPut] - public IActionResult Put(int key, [FromBody]Delta product) - { - var original = _context.Products.FirstOrDefault(p => p.Id == key); - if (original == null) - { - return NotFound($"Not found product with id = {key}"); - } + return Created(product); + } - product.Put(original); - _context.SaveChanges(); - return Updated(original); + [HttpPut] + public IActionResult Put(int key, [FromBody]Delta product) + { + var original = _context.Products.FirstOrDefault(p => p.Id == key); + if (original == null) + { + return NotFound($"Not found product with id = {key}"); } - [HttpPatch] - public IActionResult Patch(int key, Delta product) + product.Put(original); + _context.SaveChanges(); + return Updated(original); + } + + [HttpPatch] + public IActionResult Patch(int key, Delta product) + { + var original = _context.Products.FirstOrDefault(p => p.Id == key); + if (original == null) { - var original = _context.Products.FirstOrDefault(p => p.Id == key); - if (original == null) - { - return NotFound($"Not found product with id = {key}"); - } + return NotFound($"Not found product with id = {key}"); + } - product.Patch(original); + product.Patch(original); - _context.SaveChanges(); + _context.SaveChanges(); - return Updated(original); - } + return Updated(original); + } - [HttpDelete] - public IActionResult Delete(int key) + [HttpDelete] + public IActionResult Delete(int key) + { + var original = _context.Products.FirstOrDefault(p => p.Id == key); + if (original == null) { - var original = _context.Products.FirstOrDefault(p => p.Id == key); - if (original == null) - { - return NotFound($"Not found product with id = {key}"); - } - - _context.Products.Remove(original); - _context.SaveChanges(); - return Ok(); + return NotFound($"Not found product with id = {key}"); } - [HttpGet] - // ~/....(minSalary=4, maxSalary=5, aveSalary=9) - public string GetWholeSalary(int minSalary, int maxSalary, string aveSalary/*, CancellationToken token, ODataQueryOptions queryOptions, ODataPath path*/) - { - return $"Products/GetWholeSalary: {minSalary}, {maxSalary}, {aveSalary}"; - } + _context.Products.Remove(original); + _context.SaveChanges(); + return Ok(); + } - [HttpGet] - // ~/....(minSalary=4, maxSalary=5) - public string GetWholeSalary(int minSalary, int maxSalary) - { - // return $"Products/GetWholeSalary: {minSalary}, {maxSalary}"; - return GetWholeSalary(minSalary, maxSalary, aveSalary: "9"); - } + [HttpGet] + // ~/....(minSalary=4, maxSalary=5, aveSalary=9) + public string GetWholeSalary(int minSalary, int maxSalary, string aveSalary/*, CancellationToken token, ODataQueryOptions queryOptions, ODataPath path*/) + { + return $"Products/GetWholeSalary: {minSalary}, {maxSalary}, {aveSalary}"; + } - [HttpGet] - [ActionName("GetWholeSalary")] // - // ~/....(minSalary=4, maxSalary=5) - public string GetWholeSalary(int minSalary, string aveSalary) - { - int maxSalary = 10; - // return $"Products/GetWholeSalary: {minSalary}, {maxSalary}"; - return GetWholeSalary(minSalary, maxSalary, aveSalary); - } + [HttpGet] + // ~/....(minSalary=4, maxSalary=5) + public string GetWholeSalary(int minSalary, int maxSalary) + { + // return $"Products/GetWholeSalary: {minSalary}, {maxSalary}"; + return GetWholeSalary(minSalary, maxSalary, aveSalary: "9"); + } - [HttpGet] - // ~/....(minSalary=4) - // ~/....(minSalary=4, name='abc') - public string GetWholeSalary(int minSalary, /*[FromBody]*/double name) - { - return $"Products/GetWholeSalary: {minSalary}, {name}"; - } + [HttpGet] + [ActionName("GetWholeSalary")] // + // ~/....(minSalary=4, maxSalary=5) + public string GetWholeSalary(int minSalary, string aveSalary) + { + int maxSalary = 10; + // return $"Products/GetWholeSalary: {minSalary}, {maxSalary}"; + return GetWholeSalary(minSalary, maxSalary, aveSalary); + } - [HttpGet] - // GetWholeSalary(order={order}, name={name}) - // http://localhost:5000/Products/Default.GetWholeSalary(order='2',name='abc') - public string GetWholeSalary(string order, string name) - { - return $"Products/GetWholeSalary: {order}, {name}"; - - // Be noted: - // The function parameter from route value is unescaped, so we don't need to do it again. - // for example: we can send request like: - // http://localhost:5000/Products/Default.GetWholeSalary(order='2',name='key%253A') - // The response should look like: - // - // { - // "@odata.context": "http://localhost:5000/$metadata#Edm.String", - // "value": "Products/GetWholeSalary: 2, key%3A" - // } - } + [HttpGet] + // ~/....(minSalary=4) + // ~/....(minSalary=4, name='abc') + public string GetWholeSalary(int minSalary, /*[FromBody]*/double name) + { + return $"Products/GetWholeSalary: {minSalary}, {name}"; + } - [HttpGet] - public string GetOptional() - { - return "GetOptional without parameter"; - } + [HttpGet] + // GetWholeSalary(order={order}, name={name}) + // http://localhost:5000/Products/Default.GetWholeSalary(order='2',name='abc') + public string GetWholeSalary(string order, string name) + { + return $"Products/GetWholeSalary: {order}, {name}"; + + // Be noted: + // The function parameter from route value is unescaped, so we don't need to do it again. + // for example: we can send request like: + // http://localhost:5000/Products/Default.GetWholeSalary(order='2',name='key%253A') + // The response should look like: + // + // { + // "@odata.context": "http://localhost:5000/$metadata#Edm.String", + // "value": "Products/GetWholeSalary: 2, key%3A" + // } + } - [HttpGet] - public string GetOptional(string param) - { - return $"GetOptional without parameter value: param = {param}"; - } + [HttpGet] + public string GetOptional() + { + return "GetOptional without parameter"; + } - [HttpGet("CalculateSalary(minSalary={min},maxSalary={max})")] - public string CalculateSalary(int min, int max) - { - return $"Unbound function call on CalculateSalary: min={min}, max={max}"; - } + [HttpGet] + public string GetOptional(string param) + { + return $"GetOptional without parameter value: param = {param}"; + } - [HttpGet("CalculateSalary(minSalary={min},maxSalary={max},wholeName={name})")] - public string CalculateSalary(int min, int max, string name) - { - return $"Unbound function call on CalculateSalary: min={min}, max={max}, name={name}"; - } + [HttpGet("CalculateSalary(minSalary={min},maxSalary={max})")] + public string CalculateSalary(int min, int max) + { + return $"Unbound function call on CalculateSalary: min={min}, max={max}"; + } + + [HttpGet("CalculateSalary(minSalary={min},maxSalary={max},wholeName={name})")] + public string CalculateSalary(int min, int max, string name) + { + return $"Unbound function call on CalculateSalary: min={min}, max={max}, name={name}"; } } diff --git a/sample/ODataRoutingSample/Controllers/WeatherForecastController.cs b/sample/ODataRoutingSample/Controllers/WeatherForecastController.cs index 62f4171eb..10464eefa 100644 --- a/sample/ODataRoutingSample/Controllers/WeatherForecastController.cs +++ b/sample/ODataRoutingSample/Controllers/WeatherForecastController.cs @@ -12,51 +12,50 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.Extensions.Logging; -namespace ODataRoutingSample.Controllers +namespace ODataRoutingSample.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase { - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase + private static readonly string[] Summaries = new[] { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; - private readonly ILogger _logger; + private readonly ILogger _logger; - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } - [HttpGet] - [EnableQuery] // it supports odata query: for example, http://localhost:5000/WeatherForecast?$select=TemperatureC - // You can get: [{"TemperatureC":3},{"TemperatureC":1},{"TemperatureC":18},{"TemperatureC":48},{"TemperatureC":8}] - public IEnumerable Get() + [HttpGet] + [EnableQuery] // it supports odata query: for example, http://localhost:5000/WeatherForecast?$select=TemperatureC + // You can get: [{"TemperatureC":3},{"TemperatureC":1},{"TemperatureC":18},{"TemperatureC":48},{"TemperatureC":8}] + public IEnumerable Get() + { + var rng = new Random(); + return Enumerable.Range(1, 5).Select(index => new WeatherForecast { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }) - .ToArray(); - } + Date = DateTime.Now.AddDays(index), + TemperatureC = rng.Next(-20, 55), + Summary = Summaries[rng.Next(Summaries.Length)] + }) + .ToArray(); + } - [HttpGet] - [Route("bac{data}/NS.MyFunction(p1={p1},p2={p2})")] - public IEnumerable YourGet(int p1, int p2) + [HttpGet] + [Route("bac{data}/NS.MyFunction(p1={p1},p2={p2})")] + public IEnumerable YourGet(int p1, int p2) + { + var rng = new Random(); + return Enumerable.Range(1, 5).Select(index => new WeatherForecast { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }) - .ToArray(); - } + Date = DateTime.Now.AddDays(index), + TemperatureC = rng.Next(-20, 55), + Summary = Summaries[rng.Next(Summaries.Length)] + }) + .ToArray(); } } diff --git a/sample/ODataRoutingSample/Controllers/v1/CompaniesController.cs b/sample/ODataRoutingSample/Controllers/v1/CompaniesController.cs index 14b42a7dc..c70032d24 100644 --- a/sample/ODataRoutingSample/Controllers/v1/CompaniesController.cs +++ b/sample/ODataRoutingSample/Controllers/v1/CompaniesController.cs @@ -13,31 +13,30 @@ using Microsoft.AspNetCore.OData.Routing.Attributes; using ODataRoutingSample.Models; -namespace ODataRoutingSample.Controllers.v1 +namespace ODataRoutingSample.Controllers.v1; + +[ODataRouteComponent("v1")] +public class CompaniesController : ControllerBase { - [ODataRouteComponent("v1")] - public class CompaniesController : ControllerBase + [HttpGet] + [EnableQuery] + public IEnumerable Get() { - [HttpGet] - [EnableQuery] - public IEnumerable Get() + var rng = new Random(); + return Enumerable.Range(1, 5).Select(index => new Company { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new Company - { - Id = (short)index - }) - .ToArray(); - } + Id = (short)index + }) + .ToArray(); + } - [HttpGet] - [EnableQuery] - public Company Get(short key) + [HttpGet] + [EnableQuery] + public Company Get(short key) + { + return new Company { - return new Company - { - Id = key - }; - } + Id = key + }; } } diff --git a/sample/ODataRoutingSample/Controllers/v1/CustomersController.cs b/sample/ODataRoutingSample/Controllers/v1/CustomersController.cs index 55edc3e13..12976d088 100644 --- a/sample/ODataRoutingSample/Controllers/v1/CustomersController.cs +++ b/sample/ODataRoutingSample/Controllers/v1/CustomersController.cs @@ -16,128 +16,127 @@ using Microsoft.EntityFrameworkCore; using ODataRoutingSample.Models; -namespace ODataRoutingSample.Controllers.v1 +namespace ODataRoutingSample.Controllers.v1; + +[ODataRouteComponent("v1")] +public class CustomersController : ControllerBase { - [ODataRouteComponent("v1")] - public class CustomersController : ControllerBase - { - private MyDataContext _context; + private MyDataContext _context; - public CustomersController(MyDataContext context) + public CustomersController(MyDataContext context) + { + _context = context; + if (_context.Customers.Count() == 0) { - _context = context; - if (_context.Customers.Count() == 0) - { - IList customers = GetCustomers(); + IList customers = GetCustomers(); - foreach (var customer in customers) - { - _context.Customers.Add(customer); - } - - _context.SaveChanges(); + foreach (var customer in customers) + { + _context.Customers.Add(customer); } - } - // For example: http://localhost:5000/v1/customers?$apply=groupby((Name), aggregate($count as count))&$orderby=name desc - [HttpGet] - [EnableQuery] - public IActionResult Get() - { - return Ok(GetCustomers()); + _context.SaveChanges(); } + } - [HttpGet] - [EnableQuery] - public Customer Get(int key) - { - // Be noted: without the NoTracking setting, the query for $select=HomeAddress with throw exception: - // A tracking query projects owned entity without corresponding owner in result. Owned entities cannot be tracked without their owner... - _context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + // For example: http://localhost:5000/v1/customers?$apply=groupby((Name), aggregate($count as count))&$orderby=name desc + [HttpGet] + [EnableQuery] + public IActionResult Get() + { + return Ok(GetCustomers()); + } - return new Customer - { - Id = key, - Name = "Name + " + key - }; - } + [HttpGet] + [EnableQuery] + public Customer Get(int key) + { + // Be noted: without the NoTracking setting, the query for $select=HomeAddress with throw exception: + // A tracking query projects owned entity without corresponding owner in result. Owned entities cannot be tracked without their owner... + _context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - [HttpPost] - public IActionResult Post([FromBody] Customer newCustomer) + return new Customer { - return Ok(); - } + Id = key, + Name = "Name + " + key + }; + } - [HttpPost] - public string RateByName(int key, [FromODataBody] string name, [FromODataBody] int age) - { - return key + name + ": " + age; - } + [HttpPost] + public IActionResult Post([FromBody] Customer newCustomer) + { + return Ok(); + } - [HttpPost] - [EnableQuery] - public IActionResult BoundAction(int key, ODataActionParameters parameters) - { - return Ok($"BoundAction of Customers with key {key} : {System.Text.Json.JsonSerializer.Serialize(parameters)}"); - } + [HttpPost] + public string RateByName(int key, [FromODataBody] string name, [FromODataBody] int age) + { + return key + name + ": " + age; + } - private static IList GetCustomers() + [HttpPost] + [EnableQuery] + public IActionResult BoundAction(int key, ODataActionParameters parameters) + { + return Ok($"BoundAction of Customers with key {key} : {System.Text.Json.JsonSerializer.Serialize(parameters)}"); + } + + private static IList GetCustomers() + { + return new List { - return new List + new Customer { - new Customer + Id = 1, + Name = "Jonier", + FavoriteColor = Color.Red, + HomeAddress = new Address { City = "Redmond", Street = "156 AVE NE" }, + Amount = 3, + FavoriteAddresses = new List
{ - Id = 1, - Name = "Jonier", - FavoriteColor = Color.Red, - HomeAddress = new Address { City = "Redmond", Street = "156 AVE NE" }, - Amount = 3, - FavoriteAddresses = new List
- { - new Address { City = "Redmond", Street = "256 AVE NE" }, - new Address { City = "Redd", Street = "56 AVE NE" }, - }, + new Address { City = "Redmond", Street = "256 AVE NE" }, + new Address { City = "Redd", Street = "56 AVE NE" }, }, - new Customer + }, + new Customer + { + Id = 2, + Name = "Sam", + FavoriteColor = Color.Blue, + HomeAddress = new CnAddress { City = "Bellevue", Street = "Main St NE", Postcode = "201100" }, + Amount = 6, + FavoriteAddresses = new List
{ - Id = 2, - Name = "Sam", - FavoriteColor = Color.Blue, - HomeAddress = new CnAddress { City = "Bellevue", Street = "Main St NE", Postcode = "201100" }, - Amount = 6, - FavoriteAddresses = new List
- { - new Address { City = "Red4ond", Street = "456 AVE NE" }, - new Address { City = "Re4d", Street = "51 NE" }, - }, + new Address { City = "Red4ond", Street = "456 AVE NE" }, + new Address { City = "Re4d", Street = "51 NE" }, }, - new Customer + }, + new Customer + { + Id = 3, + Name = "Peter", + FavoriteColor = Color.Green, + HomeAddress = new UsAddress { City = "Hollewye", Street = "Main St NE", Zipcode = "98029" }, + Amount = 4, + FavoriteAddresses = new List
{ - Id = 3, - Name = "Peter", - FavoriteColor = Color.Green, - HomeAddress = new UsAddress { City = "Hollewye", Street = "Main St NE", Zipcode = "98029" }, - Amount = 4, - FavoriteAddresses = new List
- { - new Address { City = "R4mond", Street = "546 NE" }, - new Address { City = "R4d", Street = "546 AVE" }, - }, + new Address { City = "R4mond", Street = "546 NE" }, + new Address { City = "R4d", Street = "546 AVE" }, }, - new Customer + }, + new Customer + { + Id = 4, + Name = "Sam", + FavoriteColor = Color.Red, + HomeAddress = new UsAddress { City = "Banff", Street = "183 St NE", Zipcode = "111" }, + Amount = 5, + FavoriteAddresses = new List
{ - Id = 4, - Name = "Sam", - FavoriteColor = Color.Red, - HomeAddress = new UsAddress { City = "Banff", Street = "183 St NE", Zipcode = "111" }, - Amount = 5, - FavoriteAddresses = new List
- { - new Address { City = "R4m11ond", Street = "116 NE" }, - new Address { City = "Jesper", Street = "5416 AVE" }, - } + new Address { City = "R4m11ond", Street = "116 NE" }, + new Address { City = "Jesper", Street = "5416 AVE" }, } - }; - } + } + }; } } diff --git a/sample/ODataRoutingSample/Controllers/v1/MeController.cs b/sample/ODataRoutingSample/Controllers/v1/MeController.cs index c9ae2767d..310c4e705 100644 --- a/sample/ODataRoutingSample/Controllers/v1/MeController.cs +++ b/sample/ODataRoutingSample/Controllers/v1/MeController.cs @@ -12,46 +12,45 @@ using Microsoft.AspNetCore.OData.Routing.Attributes; using ODataRoutingSample.Models; -namespace ODataRoutingSample.Controllers.v1 +namespace ODataRoutingSample.Controllers.v1; + +[ODataRouteComponent("v1")] +public class MeController : ControllerBase { - [ODataRouteComponent("v1")] - public class MeController : ControllerBase + [HttpGet] + public Customer Get() { - [HttpGet] - public Customer Get() - { - return new Customer { Id = 9, Name = "Sam" }; - } + return new Customer { Id = 9, Name = "Sam" }; + } - [HttpGet] - public VipCustomer GetMeFromVipCustomer() - { - return new VipCustomer { Id = 10, Name = "Peter", Emails = new List { "abc@ef.com" } }; - } + [HttpGet] + public VipCustomer GetMeFromVipCustomer() + { + return new VipCustomer { Id = 10, Name = "Peter", Emails = new List { "abc@ef.com" } }; + } - [HttpPost] - [EnableQuery] - public IActionResult BoundAction(ODataActionParameters parameters) - { - /* -{ - "p1": 1, - "p2": { "Street": "1 Microsoft Way", "City": "Redmond" }, - "p3": [ "one", "two" ], - "p4": [ { "Street": "1 Microsoft Way", "City": "Redmond" } ], - "color": "Red", - "colors": ["Red", null, "Green"] + [HttpPost] + [EnableQuery] + public IActionResult BoundAction(ODataActionParameters parameters) + { + /* + +"p1": 1, +"p2": { "Street": "1 Microsoft Way", "City": "Redmond" }, +"p3": [ "one", "two" ], +"p4": [ { "Street": "1 Microsoft Way", "City": "Redmond" } ], +"color": "Red", +"colors": ["Red", null, "Green"] } - */ + */ - return Ok($"BoundAction of Me: {System.Text.Json.JsonSerializer.Serialize(parameters)}"); + return Ok($"BoundAction of Me: {System.Text.Json.JsonSerializer.Serialize(parameters)}"); - /* - { - "@odata.context": "http://localhost:5000/v1/Me/BoundAction/$metadata#Edm.String", - "value": "BoundAction of Me: {\"p1\":1,\"p2\":{\"City\":\"Redmond\",\"Street\":\"1 Microsoft Way\"},\"p3\":[\"one\",\"two\"],\"p4\":[{\"City\":\"Redmond\",\"Street\":\"1 Microsoft Way\"}],\"color\":0,\"colors\":[0,null,1]}" + /* + { + "@odata.context": "http://localhost:5000/v1/Me/BoundAction/$metadata#Edm.String", +"value": "BoundAction of Me: {\"p1\":1,\"p2\":{\"City\":\"Redmond\",\"Street\":\"1 Microsoft Way\"},\"p3\":[\"one\",\"two\"],\"p4\":[{\"City\":\"Redmond\",\"Street\":\"1 Microsoft Way\"}],\"color\":0,\"colors\":[0,null,1]}" } - */ - } + */ } } diff --git a/sample/ODataRoutingSample/Controllers/v1/ODataOperationImportController.cs b/sample/ODataRoutingSample/Controllers/v1/ODataOperationImportController.cs index 0e3604180..145505a4b 100644 --- a/sample/ODataRoutingSample/Controllers/v1/ODataOperationImportController.cs +++ b/sample/ODataRoutingSample/Controllers/v1/ODataOperationImportController.cs @@ -8,15 +8,14 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Routing.Attributes; -namespace ODataRoutingSample.Controllers.v1 +namespace ODataRoutingSample.Controllers.v1; + +[ODataRouteComponent("v1")] +public class ODataOperationImportController : ControllerBase { - [ODataRouteComponent("v1")] - public class ODataOperationImportController : ControllerBase + [HttpGet] + public int RateByOrder(int order) { - [HttpGet] - public int RateByOrder(int order) - { - return order; - } + return order; } } diff --git a/sample/ODataRoutingSample/Controllers/v1/OrganizationsController.cs b/sample/ODataRoutingSample/Controllers/v1/OrganizationsController.cs index 0bfa6be79..40967ba79 100644 --- a/sample/ODataRoutingSample/Controllers/v1/OrganizationsController.cs +++ b/sample/ODataRoutingSample/Controllers/v1/OrganizationsController.cs @@ -13,246 +13,245 @@ using Microsoft.AspNetCore.OData.Routing.Attributes; using ODataRoutingSample.Models; -namespace ODataRoutingSample.Controllers.v1 +namespace ODataRoutingSample.Controllers.v1; + +[ODataRouteComponent("v1")] +[ODataAttributeRouting] +public class OrganizationsController : Controller { - [ODataRouteComponent("v1")] - [ODataAttributeRouting] - public class OrganizationsController : Controller + [EnableQuery] + public IActionResult Post([FromBody] Organization org) { - [EnableQuery] - public IActionResult Post([FromBody] Organization org) + /* + * You can send a Post request with the request body as follows (v4.0): { - /* - * You can send a Post request with the request body as follows (v4.0): - { - "@odata.context":"http://localhost:5000/v1/$metadata#Organizations/$entity", - "Name": "Peter", - "Departs@odata.bind":[ "Departments(4)", "Departments(5)"] - } + "@odata.context":"http://localhost:5000/v1/$metadata#Organizations/$entity", + "Name": "Peter", + "Departs@odata.bind":[ "Departments(4)", "Departments(5)"] + } - or 4.01 request body as follows: + or 4.01 request body as follows: - { - "@odata.context":"http://localhost:5000/v1/$metadata#Organizations/$entity", - "Name": "Peter", - "Departs": [{ - "@odata.id": "Departments(4)" - }, - { - "@odata.id": "Departments(5)" - "Alias": "No2" - } - ] - } + { + "@odata.context":"http://localhost:5000/v1/$metadata#Organizations/$entity", + "Name": "Peter", + "Departs": [{ + "@odata.id": "Departments(4)" + }, + { + "@odata.id": "Departments(5)" + "Alias": "No2" + } + ] + } - */ + */ - org.OrganizationId = 99; // 99 is just for testing - return Ok(org); - } + org.OrganizationId = 99; // 99 is just for testing + return Ok(org); + } + + [HttpPatch] + [EnableQuery] + // public IActionResult Patch(EdmChangedObjectCollection changes) + public IActionResult Patch(DeltaSet changes) + { + /* - [HttpPatch] - [EnableQuery] - // public IActionResult Patch(EdmChangedObjectCollection changes) - public IActionResult Patch(DeltaSet changes) - { - /* -{ "@odata.context":"http://localhost/$metadata#Organizations/$delta", "value":[ { - "@odata.id":"Organizations(42)", - "Name":"Microsoft" + "@odata.id":"Organizations(42)", + "Name":"Microsoft" }, { - "@odata.context":"http://localhost/$metadata#Organizations/$deletedLink", - "source":"Organizations(32)", - "relationship":"Departs", - "target":"Departs(12)" + "@odata.context":"http://localhost/$metadata#Organizations/$deletedLink", + "source":"Organizations(32)", + "relationship":"Departs", + "target":"Departs(12)" }, { - "@odata.context":"http://localhost/$metadata#Organizations/$link", - "source":"Organizations(22)", - "relationship":"Departs", - "target":"Departs(2)" + "@odata.context":"http://localhost/$metadata#Organizations/$link", + "source":"Organizations(22)", + "relationship":"Departs", + "target":"Departs(2)" }, { - "@odata.context":"http://localhost/$metadata#Organizations/$deletedEntity", - "id":"Organizations(12)", - "reason":"deleted" + "@odata.context":"http://localhost/$metadata#Organizations/$deletedEntity", + "id":"Organizations(12)", + "reason":"deleted" } ] } - */ + */ - //changes.ApplyDeleteLink = (l) => { }; + //changes.ApplyDeleteLink = (l) => { }; - //IList originalSet = new List(); + //IList originalSet = new List(); - // changes.Patch(originalSet); + // changes.Patch(originalSet); - return Ok(); - } + return Ok(); + } - [HttpPatch] - [EnableQuery] - public IActionResult Patch(int key, Delta delta) - { - /* Send a PATCH request to: http://localhost:5000/v1/Organizations/1 - * using the following payload (v4.01 format, should enable the EnableReadingODataAnnotationWithoutPrefix on ODataSimplifiedOptions) -{ - "Departs@delta": [ - { - "@removed":{"reason":"deleted" }, - "@id":"Departments(13)" - }, + [HttpPatch] + [EnableQuery] + public IActionResult Patch(int key, Delta delta) { - "@id":"Departments(42)", - "Name":"Microsoft" + /* Send a PATCH request to: http://localhost:5000/v1/Organizations/1 + * using the following payload (v4.01 format, should enable the EnableReadingODataAnnotationWithoutPrefix on ODataSimplifiedOptions) + + "Departs@delta": [ +{ + "@removed":{"reason":"deleted" }, + "@id":"Departments(13)" +}, +{ + "@id":"Departments(42)", + "Name":"Microsoft" } ] } - */ + */ + + // Or using the following payload (v4.0 format) + /* - // Or using the following payload (v4.0 format) - /* -{ "Departs@delta": [ - { - "@odata.context":"http://localhost:5000/v1/$metadata#Departments/$deletedEntity", - "id":"Departments(13)", - "reason":"deleted" - }, - { - "@odata.id":"Departments(42)", - "Name":"Microsoft" +{ + "@odata.context":"http://localhost:5000/v1/$metadata#Departments/$deletedEntity", + "id":"Departments(13)", + "reason":"deleted" +}, +{ + "@odata.id":"Departments(42)", + "Name":"Microsoft" } ] } - Be noted: the "id" should go before "reason", otherwise we can't read the "id" value. - It's a bug in ODL side. - */ + Be noted: the "id" should go before "reason", otherwise we can't read the "id" value. + It's a bug in ODL side. + */ - if (delta != null && delta.TryGetPropertyValue("Departs", out object value)) + if (delta != null && delta.TryGetPropertyValue("Departs", out object value)) + { + if (value is DeltaSet departs) { - if (value is DeltaSet departs) + IList sb = new List(); + foreach (var setItem in departs) { - IList sb = new List(); - foreach (var setItem in departs) + if (setItem is IDeltaDeletedResource deletedResource) { - if (setItem is IDeltaDeletedResource deletedResource) - { - sb.Add($" |-> A DeletedResource Id = {deletedResource.Id}"); - } - else if (setItem is IDelta deltaResource) - { - sb.Add($" |-> A Delta Resource With ChangedProperties = {string.Join(",", deltaResource.GetChangedPropertyNames())}"); - } - else - { - sb.Add($" |-> Not fully supported: {setItem.Kind}"); - } + sb.Add($" |-> A DeletedResource Id = {deletedResource.Id}"); + } + else if (setItem is IDelta deltaResource) + { + sb.Add($" |-> A Delta Resource With ChangedProperties = {string.Join(",", deltaResource.GetChangedPropertyNames())}"); + } + else + { + sb.Add($" |-> Not fully supported: {setItem.Kind}"); } - - return Ok(sb); } - } - return Ok(); + return Ok(sb); + } } - public IActionResult GetName(int key) + return Ok(); + } + + public IActionResult GetName(int key) + { + Organization org = new Organization { - Organization org = new Organization - { - OrganizationId = 9, - Name = "MyName" - }; + OrganizationId = 9, + Name = "MyName" + }; - return Ok(org.Name); - } + return Ok(org.Name); + } - [HttpGet] - public IActionResult GetPrice([FromODataUri] string organizationId, [FromODataUri] string partId) - { - return Ok($"Calculated the price using {organizationId} and {partId}"); - } + [HttpGet] + public IActionResult GetPrice([FromODataUri] string organizationId, [FromODataUri] string partId) + { + return Ok($"Calculated the price using {organizationId} and {partId}"); + } - [HttpGet("v1/Organizations/GetPrice2(organizationId={orgId},partId={parId})")] - public IActionResult GetMorePrice(string orgId, string parId) - { - return Ok($"Calculated the price using {orgId} and {parId}"); - } + [HttpGet("v1/Organizations/GetPrice2(organizationId={orgId},partId={parId})")] + public IActionResult GetMorePrice(string orgId, string parId) + { + return Ok($"Calculated the price using {orgId} and {parId}"); + } - [HttpGet("v1/Organizations/GetPrice2(organizationId={orgId},partId={parId})/GetPrice2(organizationId={orgId2},partId={parId2})")] - public IActionResult GetMorePrice2(string orgId, string parId, string orgId2, string parId2) - { - return Ok($"Calculated the price using {orgId} and {parId} | using {orgId2} and {parId2}"); - } + [HttpGet("v1/Organizations/GetPrice2(organizationId={orgId},partId={parId})/GetPrice2(organizationId={orgId2},partId={parId2})")] + public IActionResult GetMorePrice2(string orgId, string parId, string orgId2, string parId2) + { + return Ok($"Calculated the price using {orgId} and {parId} | using {orgId2} and {parId2}"); + } - [HttpPost("v1/Organizations/GetByAccount(accountId={aId})/MarkAsFavourite")] - public IActionResult MarkAsFavouriteAfterGetByAccount(string aId) - { - /* - * It works for the following request; - * POST http://localhost:5000/v1/Organizations/GetByAccount(accountId=99)/MarkAsFavourite - * */ - return Ok($"MarkAsFavouriteAfterGetByAccount after {aId}"); - } + [HttpPost("v1/Organizations/GetByAccount(accountId={aId})/MarkAsFavourite")] + public IActionResult MarkAsFavouriteAfterGetByAccount(string aId) + { + /* + * It works for the following request; + * POST http://localhost:5000/v1/Organizations/GetByAccount(accountId=99)/MarkAsFavourite + * */ + return Ok($"MarkAsFavouriteAfterGetByAccount after {aId}"); + } - [HttpPost("v1/Organizations/GetByAccount2(accountId={aId})/{key}/MarkAsFavourite")] - // [HttpPost("v1/Organizations/GetByAccount2(accountId={aId})({key})/MarkAsFavourite")] this syntax has ODL problem. - public IActionResult MarkAsFavouriteAfterGetByAccount2(int key, string aId) - { - /* - * It works for the following request; - * POST http://localhost:5000/v1/Organizations/GetByAccount2(accountId=99)/4/MarkAsFavourite - * */ - return Ok($"MarkAsFavourite2AfterGetByAccount2 after {aId} with key={key}"); - } + [HttpPost("v1/Organizations/GetByAccount2(accountId={aId})/{key}/MarkAsFavourite")] + // [HttpPost("v1/Organizations/GetByAccount2(accountId={aId})({key})/MarkAsFavourite")] this syntax has ODL problem. + public IActionResult MarkAsFavouriteAfterGetByAccount2(int key, string aId) + { + /* + * It works for the following request; + * POST http://localhost:5000/v1/Organizations/GetByAccount2(accountId=99)/4/MarkAsFavourite + * */ + return Ok($"MarkAsFavourite2AfterGetByAccount2 after {aId} with key={key}"); + } - /* Conventional routing builds the following two routing templates: + /* Conventional routing builds the following two routing templates: GET ~/v1/Organizations({key})/{navigationProperty}/$ref GET ~/v1/Organizations/{key}/{navigationProperty}/$ref - */ - [HttpGet] - public IActionResult GetRef(int key, string navigationProperty) - { - return Ok($"GetRef - {key}: {navigationProperty}"); - } + */ + [HttpGet] + public IActionResult GetRef(int key, string navigationProperty) + { + return Ok($"GetRef - {key}: {navigationProperty}"); + } - /* Conventional routing builds the following two routing templates: + /* Conventional routing builds the following two routing templates: POST,PUT ~/v1/Organizations({ key})/{navigationProperty }/$ref POST,PUT ~/v1/Organizations/{key }/{navigationProperty}/$ref - */ - [HttpPost] - [HttpPut] - public IActionResult CreateRef(int key, string navigationProperty) - { - return Ok($"CreateRef - {key}: {navigationProperty}"); - } + */ + [HttpPost] + [HttpPut] + public IActionResult CreateRef(int key, string navigationProperty) + { + return Ok($"CreateRef - {key}: {navigationProperty}"); + } - /* Conventional routing builds the following two routing templates: + /* Conventional routing builds the following two routing templates: DELETE ~/v1/Organizations({key})/{navigationProperty}/$ref DELETE ~/v1/Organizations/{key}/{navigationProperty}/$ref - */ - [HttpDelete] - public IActionResult DeleteRef(int key, string navigationProperty) - { - return Ok($"DeleteRef - {key}: {navigationProperty}"); - } + */ + [HttpDelete] + public IActionResult DeleteRef(int key, string navigationProperty) + { + return Ok($"DeleteRef - {key}: {navigationProperty}"); + } - /* Conventional routing builds the following two routing templates: + /* Conventional routing builds the following two routing templates: DELETE ~/v1/Organizations({key})/{navigationProperty}({relatedKey})/$ref DELETE ~/v1/Organizations({key})/{navigationProperty}/{relatedKey}/$ref DELETE ~/v1/Organizations/{key}/{navigationProperty}({relatedKey})/$ref DELETE ~/v1/Organizations/{key}/{navigationProperty}/{relatedKey}/$ref - */ - [HttpDelete] - public IActionResult DeleteRef(int key, int relatedKey, string navigationProperty) - { - return Ok($"DeleteRef - {key} - {relatedKey}: {navigationProperty}"); - } + */ + [HttpDelete] + public IActionResult DeleteRef(int key, int relatedKey, string navigationProperty) + { + return Ok($"DeleteRef - {key} - {relatedKey}: {navigationProperty}"); } } diff --git a/sample/ODataRoutingSample/Controllers/v2/ODataOperationImportController.cs b/sample/ODataRoutingSample/Controllers/v2/ODataOperationImportController.cs index 85ff5666d..d04ec7db3 100644 --- a/sample/ODataRoutingSample/Controllers/v2/ODataOperationImportController.cs +++ b/sample/ODataRoutingSample/Controllers/v2/ODataOperationImportController.cs @@ -8,15 +8,14 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Routing.Attributes; -namespace ODataRoutingSample.Controllers.v2 +namespace ODataRoutingSample.Controllers.v2; + +[ODataRouteComponent("v2{data}")] +public class ODataOperationImportController : ControllerBase { - [ODataRouteComponent("v2{data}")] - public class ODataOperationImportController : ControllerBase + [HttpGet] + public int RateByOrder(int order) { - [HttpGet] - public int RateByOrder(int order) - { - return order; - } + return order; } } diff --git a/sample/ODataRoutingSample/Controllers/v2/OrdersController.cs b/sample/ODataRoutingSample/Controllers/v2/OrdersController.cs index f7aba8aeb..eb36d3f04 100644 --- a/sample/ODataRoutingSample/Controllers/v2/OrdersController.cs +++ b/sample/ODataRoutingSample/Controllers/v2/OrdersController.cs @@ -16,186 +16,186 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using ODataRoutingSample.Models; -namespace ODataRoutingSample.Controllers.v2 +namespace ODataRoutingSample.Controllers.v2; + +[ODataRouteComponent("v2{data}")] +public class OrdersController : ODataController { - [ODataRouteComponent("v2{data}")] - public class OrdersController : ODataController + private MyDataContext _context; + + public OrdersController(MyDataContext context) { - private MyDataContext _context; + _context = context; - public OrdersController(MyDataContext context) + if (_context.Orders.Count() == 0) { - _context = context; - - if (_context.Orders.Count() == 0) + IList orders = new List { - IList orders = new List + new Order { - new Order - { - Title = "Goods", - Category = new Category { }, - }, - new Order - { - Title = "Magazine", - Category = new Category { }, - }, - new Order - { - Title = "Fiction", - Category = new Category { }, - }, - }; - - foreach (var order in orders) + Title = "Goods", + Category = new Category { }, + }, + new Order { - _context.Orders.Add(order); - } - - _context.SaveChanges(); - } - } - - [HttpGet] - [EnableQuery] - public IEnumerable Get() - { - return _context.Orders; - } + Title = "Magazine", + Category = new Category { }, + }, + new Order + { + Title = "Fiction", + Category = new Category { }, + }, + }; - [HttpGet] // ~/Orders({key}) - [EnableQuery] - public Order Get(int key) - { - return new Order + foreach (var order in orders) { - Id = key, - Title = "Title + " + key - }; - } + _context.Orders.Add(order); + } - [HttpPost] - [EnableQuery] - public IActionResult Post([FromBody] Order order, CancellationToken token) - { - _context.Orders.Add(order); _context.SaveChanges(); - return Created(order); - } - - [HttpDelete] // ~/Orders({key}) - public string Delete(int key) - { - return $"Delete Order at {key}"; } + } - // [Http] // ~/Orders({key}) - [HttpPatch] - public string Patch(int key) - { - return $"Patch Order at {key}"; - } + [HttpGet] + [EnableQuery] + public IEnumerable Get() + { + return _context.Orders; + } - // http://localhost:5000/v21/Orders(2)/CanMoveToAddress(address={"City":"abc","Street":"fdsfg"}) - // http://localhost:5000/v21/Orders(2)/CanMoveToAddress(address=@p)?@p={"City":"abc","Street":"fdsfg"} - [HttpGet] - public IActionResult CanMoveToAddress(int key, [FromODataUri] Address address) + [HttpGet] // ~/Orders({key}) + [EnableQuery] + public Order Get(int key) + { + return new Order { - if (address == null) - { - return NotFound("address is null"); - } + Id = key, + Title = "Title + " + key + }; + } - return Ok(System.Text.Json.JsonSerializer.Serialize(address)); - } + [HttpPost] + [EnableQuery] + public IActionResult Post([FromBody] Order order, CancellationToken token) + { + _context.Orders.Add(order); + _context.SaveChanges(); + return Created(order); + } - // http://localhost:5000/v21/Orders/CanMoveToManyAddress(addresses=[{"City":"abc","Street":"sfg"},{"City":"ab2c","Street":"fdsfg"}]) - // http://localhost:5000/v21/Orders/CanMoveToManyAddress(addresses=@p)?@p=[{"City":"abc","Street":"sfg"},{"City":"ab2c","Street":"fdsfg"}] - [HttpGet] - public IActionResult CanMoveToManyAddress([FromODataUri] IEnumerable
addresses) - { - if (addresses == null) - { - return NotFound("addresses is null"); - } + [HttpDelete] // ~/Orders({key}) + public string Delete(int key) + { + return $"Delete Order at {key}"; + } - return Ok(System.Text.Json.JsonSerializer.Serialize(addresses)); - } + // [Http] // ~/Orders({key}) + [HttpPatch] + public string Patch(int key) + { + return $"Patch Order at {key}"; + } - [HttpGet] - public string GetWholeSalary(int key, int minSalary, int maxSalary, int aveSalary) + // http://localhost:5000/v21/Orders(2)/CanMoveToAddress(address={"City":"abc","Street":"fdsfg"}) + // http://localhost:5000/v21/Orders(2)/CanMoveToAddress(address=@p)?@p={"City":"abc","Street":"fdsfg"} + [HttpGet] + public IActionResult CanMoveToAddress(int key, [FromODataUri] Address address) + { + if (address == null) { - return $"Orders/{key}/GetWholeSalary: {minSalary}, {maxSalary}, {aveSalary}" ; + return NotFound("address is null"); } + return Ok(System.Text.Json.JsonSerializer.Serialize(address)); + } - [HttpGet] - public string GetProperty(int key, string property) + // http://localhost:5000/v21/Orders/CanMoveToManyAddress(addresses=[{"City":"abc","Street":"sfg"},{"City":"ab2c","Street":"fdsfg"}]) + // http://localhost:5000/v21/Orders/CanMoveToManyAddress(addresses=@p)?@p=[{"City":"abc","Street":"sfg"},{"City":"ab2c","Street":"fdsfg"}] + [HttpGet] + public IActionResult CanMoveToManyAddress([FromODataUri] IEnumerable
addresses) + { + if (addresses == null) { - return $"{property} in order"; + return NotFound("addresses is null"); } - [HttpGet] - public string GetTitle(int key) - { - return "Orders Title"; - } + return Ok(System.Text.Json.JsonSerializer.Serialize(addresses)); + } - [HttpPost] - public string PostToCategory(int key) - { - return "PostToCategory + " + key; - } + [HttpGet] + public string GetWholeSalary(int key, int minSalary, int maxSalary, int aveSalary) + { + return $"Orders/{key}/GetWholeSalary: {minSalary}, {maxSalary}, {aveSalary}" ; + } - [HttpPost] - public string PostToCategoryFromVipOrder(int key) - { - return "PostToCategoryFromVipOrder + " + key; - } - [HttpPost] - public string PostToCategoryFromUnknowOrder(int key) - { - return "PostToCategoryFromUnknowOrder + " + key; - } + [HttpGet] + public string GetProperty(int key, string property) + { + return $"{property} in order"; + } - [HttpPost] - public string CreateRefToCategory(int key) - { - return "CreateRefToCategory"; - } + [HttpGet] + public string GetTitle(int key) + { + return "Orders Title"; + } + + [HttpPost] + public string PostToCategory(int key) + { + return "PostToCategory + " + key; + } + + [HttpPost] + public string PostToCategoryFromVipOrder(int key) + { + return "PostToCategoryFromVipOrder + " + key; + } + + [HttpPost] + public string PostToCategoryFromUnknowOrder(int key) + { + return "PostToCategoryFromUnknowOrder + " + key; + } + + [HttpPost] + public string CreateRefToCategory(int key) + { + return "CreateRefToCategory"; } } + // Request using the $batch /* -{ + "requests":[ - { - "id": "2", - "atomicityGroup": "transaction", - "method": "post", - "url": "/v2bla/Orders", - "headers": { "content-type": "application/json", "Accept": "application/json", "odata-version": "4.0" }, - "body": {"Title":"MyName11"} - }, - { - "id": "3", - "atomicityGroup": "transaction", - "method": "post", - "url": "/v2bla/Orders", - "headers": { "content-type": "application/json", "Accept": "application/json", "odata-version": "4.0" }, - "body": {"Title":"MyName12"} - }, - { - "id": "4", - "atomicityGroup": "transaction", - "method": "post", - "url": "/v2bla/Orders", - "headers": { "content-type": "application/json", "Accept": "application/json", "odata-version": "4.0" }, - "body": {"Title":"MyName13"} - } + { + "id": "2", + "atomicityGroup": "transaction", + "method": "post", + "url": "/v2bla/Orders", + "headers": { "content-type": "application/json", "Accept": "application/json", "odata-version": "4.0" }, + "body": {"Title":"MyName11"} + }, + { + "id": "3", + "atomicityGroup": "transaction", + "method": "post", + "url": "/v2bla/Orders", + "headers": { "content-type": "application/json", "Accept": "application/json", "odata-version": "4.0" }, + "body": {"Title":"MyName12"} + }, + { + "id": "4", + "atomicityGroup": "transaction", + "method": "post", + "url": "/v2bla/Orders", + "headers": { "content-type": "application/json", "Accept": "application/json", "odata-version": "4.0" }, + "body": {"Title":"MyName13"} + } ] } */ diff --git a/sample/ODataRoutingSample/Controllers/v2/VipOrderController.cs b/sample/ODataRoutingSample/Controllers/v2/VipOrderController.cs index 188eeb3ab..434d8c1b3 100644 --- a/sample/ODataRoutingSample/Controllers/v2/VipOrderController.cs +++ b/sample/ODataRoutingSample/Controllers/v2/VipOrderController.cs @@ -9,21 +9,20 @@ using Microsoft.AspNetCore.OData.Routing.Attributes; using ODataRoutingSample.Models; -namespace ODataRoutingSample.Controllers +namespace ODataRoutingSample.Controllers; + +[ODataRouteComponent("v2{data}")] +public class VipOrderController : ControllerBase { - [ODataRouteComponent("v2{data}")] - public class VipOrderController : ControllerBase + [HttpGet] + public Order Get() { - [HttpGet] - public Order Get() - { - return new Order { Id = 9, Title = "Singleton Title" }; - } + return new Order { Id = 9, Title = "Singleton Title" }; + } - [HttpGet] - public string GetTitleFromOrder() - { - return "Singleton Title"; - } + [HttpGet] + public string GetTitleFromOrder() + { + return "Singleton Title"; } } diff --git a/sample/ODataRoutingSample/Controllers/v3/TenantsController.cs b/sample/ODataRoutingSample/Controllers/v3/TenantsController.cs index 1d8b8e83f..0a83b1f74 100644 --- a/sample/ODataRoutingSample/Controllers/v3/TenantsController.cs +++ b/sample/ODataRoutingSample/Controllers/v3/TenantsController.cs @@ -11,65 +11,64 @@ using Microsoft.AspNetCore.OData.Routing.Attributes; using ODataRoutingSample.Models; -namespace ODataRoutingSample.Controllers.v3 +namespace ODataRoutingSample.Controllers.v3; + +public class TestEntitiesController : Controller { - public class TestEntitiesController : Controller + /* HttpPatch http://localhost:5000/v3/TestEntities/1/Query + * + * Request Body is: + +"results": [ { - /* HttpPatch http://localhost:5000/v3/TestEntities/1/Query - * - * Request Body is: -{ - "results": [ - { - "EmailClusterId@odata.type": "#Int64", - "EmailClusterId": 2629759514 - }, - { - "EmailClusterId@odata.type": "#Int64", - "EmailClusterId": 2629759515 - } - ] + "EmailClusterId@odata.type": "#Int64", + "EmailClusterId": 2629759514 + }, + { + "EmailClusterId@odata.type": "#Int64", + "EmailClusterId": 2629759515 + } +] } - */ - [HttpPatch] - public IActionResult PatchToQuery(int key, Delta delta) - { - var changedPropertyNames = delta.GetChangedPropertyNames(); + */ + [HttpPatch] + public IActionResult PatchToQuery(int key, Delta delta) + { + var changedPropertyNames = delta.GetChangedPropertyNames(); - HuntingQueryResults original = new HuntingQueryResults(); - delta.Patch(original); + HuntingQueryResults original = new HuntingQueryResults(); + delta.Patch(original); - return Ok(key); - } + return Ok(key); } +} - [ODataAttributeRouting] - [Route("v3")] - public class tenantsController : Controller +[ODataAttributeRouting] +[Route("v3")] +public class tenantsController : Controller +{ + [HttpPut("tenants/{tenantId}/devices/{deviceId}")] + [HttpPut("tenants({tenantId})/devices({deviceId})")] + public IActionResult PutToDevices(string tenantId, string deviceId) { - [HttpPut("tenants/{tenantId}/devices/{deviceId}")] - [HttpPut("tenants({tenantId})/devices({deviceId})")] - public IActionResult PutToDevices(string tenantId, string deviceId) - { - return Ok($"PutTo Devices - tenantId={tenantId}: deviceId={deviceId}"); - } + return Ok($"PutTo Devices - tenantId={tenantId}: deviceId={deviceId}"); + } - [HttpGet("tenants/{tenantId}/folders/{folderId}")] - [HttpGet("tenants({tenantId})/folders({folderId})")] - public IActionResult GetFolders(string tenantId, Guid folderId) - { - return Ok($"GetFolders - tenantId={tenantId}: folderId={folderId}"); - } + [HttpGet("tenants/{tenantId}/folders/{folderId}")] + [HttpGet("tenants({tenantId})/folders({folderId})")] + public IActionResult GetFolders(string tenantId, Guid folderId) + { + return Ok($"GetFolders - tenantId={tenantId}: folderId={folderId}"); + } - [HttpGet("tenants/{tenantId}/pages/{pageId}")] - [HttpGet("tenants({tenantId})/pages({pageId})")] - public IActionResult GetDriverPages(string tenantId, int pageId) - { - // Example: - // 1) ~/v3/tenants/23281137-7a37-4c2f-ad57-a511f38dea09/pages/2 ==> works - // 2) ~/v3/tenants/'23281137-7a37-4c2f-ad57-a511f38dea09'/pages/2 ==> works - // 3) ~/v3/tenants/'23281137-7a37-4c2f-ad57-a511f38dea09'/pages/ab ==> throw exception on 'ab' - return Ok($"GetDriverPages - tenantId={tenantId}: pageId={pageId}"); - } + [HttpGet("tenants/{tenantId}/pages/{pageId}")] + [HttpGet("tenants({tenantId})/pages({pageId})")] + public IActionResult GetDriverPages(string tenantId, int pageId) + { + // Example: + // 1) ~/v3/tenants/23281137-7a37-4c2f-ad57-a511f38dea09/pages/2 ==> works + // 2) ~/v3/tenants/'23281137-7a37-4c2f-ad57-a511f38dea09'/pages/2 ==> works + // 3) ~/v3/tenants/'23281137-7a37-4c2f-ad57-a511f38dea09'/pages/ab ==> throw exception on 'ab' + return Ok($"GetDriverPages - tenantId={tenantId}: pageId={pageId}"); } } diff --git a/sample/ODataRoutingSample/Models/Account.cs b/sample/ODataRoutingSample/Models/Account.cs index c91455883..9b5c00659 100644 --- a/sample/ODataRoutingSample/Models/Account.cs +++ b/sample/ODataRoutingSample/Models/Account.cs @@ -7,23 +7,22 @@ using System; -namespace ODataRoutingSample.Models +namespace ODataRoutingSample.Models; + +public class Account { - public class Account - { - public Guid AccountId { get; set; } + public Guid AccountId { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public Address HomeAddress { get; set; } + public Address HomeAddress { get; set; } - public AccountInfo AccountInfo { get; set; } - } + public AccountInfo AccountInfo { get; set; } +} - public class AccountInfo - { - public int Id { get; set; } +public class AccountInfo +{ + public int Id { get; set; } - public double Balance { get; set; } - } + public double Balance { get; set; } } diff --git a/sample/ODataRoutingSample/Models/Address.cs b/sample/ODataRoutingSample/Models/Address.cs index fb9adaa69..eb2da983b 100644 --- a/sample/ODataRoutingSample/Models/Address.cs +++ b/sample/ODataRoutingSample/Models/Address.cs @@ -5,22 +5,21 @@ // //------------------------------------------------------------------------------ -namespace ODataRoutingSample.Models +namespace ODataRoutingSample.Models; + +public class Address { - public class Address - { - public string City { get; set; } + public string City { get; set; } - public string Street { get; set; } - } + public string Street { get; set; } +} - public class CnAddress : Address - { - public string Postcode { get; set; } - } +public class CnAddress : Address +{ + public string Postcode { get; set; } +} - public class UsAddress : Address - { - public string Zipcode { get; set; } - } +public class UsAddress : Address +{ + public string Zipcode { get; set; } } diff --git a/sample/ODataRoutingSample/Models/Category.cs b/sample/ODataRoutingSample/Models/Category.cs index 14a5a4c50..121df3be5 100644 --- a/sample/ODataRoutingSample/Models/Category.cs +++ b/sample/ODataRoutingSample/Models/Category.cs @@ -10,10 +10,9 @@ using System.Linq; using System.Threading.Tasks; -namespace ODataRoutingSample.Models +namespace ODataRoutingSample.Models; + +public class Category { - public class Category - { - public int Id { get; set; } - } + public int Id { get; set; } } diff --git a/sample/ODataRoutingSample/Models/Company.cs b/sample/ODataRoutingSample/Models/Company.cs index 4b4c3993d..1efdd6a3c 100644 --- a/sample/ODataRoutingSample/Models/Company.cs +++ b/sample/ODataRoutingSample/Models/Company.cs @@ -5,10 +5,9 @@ // //------------------------------------------------------------------------------ -namespace ODataRoutingSample.Models +namespace ODataRoutingSample.Models; + +public class Company { - public class Company - { - public short Id { get; set; } - } + public short Id { get; set; } } diff --git a/sample/ODataRoutingSample/Models/Customer.cs b/sample/ODataRoutingSample/Models/Customer.cs index 7d37a4778..5d4e1fb6e 100644 --- a/sample/ODataRoutingSample/Models/Customer.cs +++ b/sample/ODataRoutingSample/Models/Customer.cs @@ -7,25 +7,24 @@ using System.Collections.Generic; -namespace ODataRoutingSample.Models +namespace ODataRoutingSample.Models; + +public class Customer { - public class Customer - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public Color FavoriteColor { get; set; } + public Color FavoriteColor { get; set; } - public int Amount { get; set; } + public int Amount { get; set; } - public virtual Address HomeAddress { get; set; } + public virtual Address HomeAddress { get; set; } - public virtual IList
FavoriteAddresses { get; set; } - } + public virtual IList
FavoriteAddresses { get; set; } +} - public class VipCustomer : Customer - { - public IList Emails { get; set; } - } +public class VipCustomer : Customer +{ + public IList Emails { get; set; } } diff --git a/sample/ODataRoutingSample/Models/EdmModelBuilder.cs b/sample/ODataRoutingSample/Models/EdmModelBuilder.cs index ac25cbd8c..e8f8078df 100644 --- a/sample/ODataRoutingSample/Models/EdmModelBuilder.cs +++ b/sample/ODataRoutingSample/Models/EdmModelBuilder.cs @@ -8,157 +8,156 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace ODataRoutingSample.Models +namespace ODataRoutingSample.Models; + +public static class EdmModelBuilder { - public static class EdmModelBuilder + public static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Products"); + builder.EntitySet("People").EntityType.HasKey(c => new { c.FirstName, c.LastName }); + + // use the following codes to set the order and change the route template. + // builder.EntityType().Property(c => c.FirstName).Order = 2; + // builder.EntityType().Property(c => c.LastName).Order = 1; + + // function with optional parameters + var functionWithOptional = builder.EntityType().Collection.Function("GetWholeSalary").ReturnsCollectionFromEntitySet("Orders"); + functionWithOptional.Parameter("minSalary"); + functionWithOptional.Parameter("maxSalary").Optional(); + functionWithOptional.Parameter("aveSalary").HasDefaultValue("129"); + + // overload + functionWithOptional = builder.EntityType().Collection.Function("GetWholeSalary").ReturnsCollectionFromEntitySet("Orders"); + functionWithOptional.Parameter("minSalary"); + functionWithOptional.Parameter("name"); + + // overload + functionWithOptional = builder.EntityType().Collection.Function("GetWholeSalary").ReturnsCollectionFromEntitySet("Orders"); + functionWithOptional.Parameter("order"); + functionWithOptional.Parameter("name"); + + // function with only one parameter (optional) + functionWithOptional = builder.EntityType().Collection.Function("GetOptional").ReturnsCollectionFromEntitySet("Orders"); + functionWithOptional.Parameter("param").Optional(); + + // unbound + builder.Action("ResetData"); + + // using attribute routing + var unboundFunction = builder.Function("CalculateSalary").Returns(); + unboundFunction.Parameter("minSalary"); + unboundFunction.Parameter("maxSalary").Optional(); + unboundFunction.Parameter("wholeName").HasDefaultValue("abc"); + return builder.GetEdmModel(); + } + + public static IEdmModel GetEdmModelV1() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Organizations"); + builder.EntitySet("Departments"); + builder.EntitySet("Companies"); + builder.EntitySet("Customers"); + builder.Singleton("Me"); + + var function = builder.Function("RateByOrder"); + function.Parameter("order"); + function.Returns(); + + var action = builder.EntityType().Action("RateByName"); + action.Parameter("name"); + action.Parameter("age"); + action.Returns(); + + // bound action + ActionConfiguration boundAction = builder.EntityType().Action("BoundAction"); + boundAction.Parameter("p1"); + boundAction.Parameter
("p2"); + boundAction.Parameter("color"); + boundAction.CollectionParameter("p3"); + boundAction.CollectionParameter
("p4"); + boundAction.CollectionParameter("colors"); + + // bound function for organization + var productPrice = builder.EntityType().Collection. + Function("GetPrice").Returns(); + productPrice.Parameter("organizationId").Required(); + productPrice.Parameter("partId").Required(); + + productPrice = builder.EntityType().Collection. + Function("GetPrice2").ReturnsCollectionFromEntitySet("Organizations"); + productPrice.Parameter("organizationId").Required(); + productPrice.Parameter("partId").Required(); + productPrice.IsComposable = true; + + // Add a composable function + var getOrgByAccount = + builder.EntityType() + .Collection + .Function("GetByAccount") + .ReturnsFromEntitySet("Organizations"); + getOrgByAccount.Parameter("accountId").Required(); + getOrgByAccount.IsComposable = true; + + builder.EntityType().Action("MarkAsFavourite"); + + // Add another composable function + var getOrgByAccount2 = + builder.EntityType() + .Collection + .Function("GetByAccount2") + .ReturnsCollectionFromEntitySet("Organizations"); // be noted, it returns collection. + getOrgByAccount2.Parameter("accountId").Required(); + getOrgByAccount2.IsComposable = true; + + return builder.GetEdmModel(); + } + + public static IEdmModel GetEdmModelV2() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Orders"); + + builder.Singleton("VipOrder"); + builder.Singleton("Categories"); + + var functionWithComplexTypeParameter = builder.EntityType().Function("CanMoveToAddress").Returns(); + functionWithComplexTypeParameter.Parameter
("address"); + + var functionWithCollectionComplexTypeParameter = builder.EntityType().Collection.Function("CanMoveToManyAddress").Returns(); + functionWithCollectionComplexTypeParameter.CollectionParameter
("addresses"); + + // function with optional parameters + var functionWithOptional = builder.EntityType().Function("GetWholeSalary").ReturnsCollectionFromEntitySet("Orders"); + functionWithOptional.Parameter("minSalary"); + functionWithOptional.Parameter("maxSalary").Optional(); + functionWithOptional.Parameter("aveSalary").HasDefaultValue("129"); + + // Function 1 + var function = builder.Function("RateByOrder"); + function.Parameter("order"); + function.Returns(); + + // Function 2 + function = builder.Function("CalcByOrder"); + function.Parameter("name"); + function.Parameter("order"); + function.Returns(); + + return builder.GetEdmModel(); + } + + public static IEdmModel GetEdmModelV3() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Products"); - builder.EntitySet("People").EntityType.HasKey(c => new { c.FirstName, c.LastName }); - - // use the following codes to set the order and change the route template. - // builder.EntityType().Property(c => c.FirstName).Order = 2; - // builder.EntityType().Property(c => c.LastName).Order = 1; - - // function with optional parameters - var functionWithOptional = builder.EntityType().Collection.Function("GetWholeSalary").ReturnsCollectionFromEntitySet("Orders"); - functionWithOptional.Parameter("minSalary"); - functionWithOptional.Parameter("maxSalary").Optional(); - functionWithOptional.Parameter("aveSalary").HasDefaultValue("129"); - - // overload - functionWithOptional = builder.EntityType().Collection.Function("GetWholeSalary").ReturnsCollectionFromEntitySet("Orders"); - functionWithOptional.Parameter("minSalary"); - functionWithOptional.Parameter("name"); - - // overload - functionWithOptional = builder.EntityType().Collection.Function("GetWholeSalary").ReturnsCollectionFromEntitySet("Orders"); - functionWithOptional.Parameter("order"); - functionWithOptional.Parameter("name"); - - // function with only one parameter (optional) - functionWithOptional = builder.EntityType().Collection.Function("GetOptional").ReturnsCollectionFromEntitySet("Orders"); - functionWithOptional.Parameter("param").Optional(); - - // unbound - builder.Action("ResetData"); - - // using attribute routing - var unboundFunction = builder.Function("CalculateSalary").Returns(); - unboundFunction.Parameter("minSalary"); - unboundFunction.Parameter("maxSalary").Optional(); - unboundFunction.Parameter("wholeName").HasDefaultValue("abc"); - return builder.GetEdmModel(); - } - - public static IEdmModel GetEdmModelV1() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Organizations"); - builder.EntitySet("Departments"); - builder.EntitySet("Companies"); - builder.EntitySet("Customers"); - builder.Singleton("Me"); - - var function = builder.Function("RateByOrder"); - function.Parameter("order"); - function.Returns(); - - var action = builder.EntityType().Action("RateByName"); - action.Parameter("name"); - action.Parameter("age"); - action.Returns(); - - // bound action - ActionConfiguration boundAction = builder.EntityType().Action("BoundAction"); - boundAction.Parameter("p1"); - boundAction.Parameter
("p2"); - boundAction.Parameter("color"); - boundAction.CollectionParameter("p3"); - boundAction.CollectionParameter
("p4"); - boundAction.CollectionParameter("colors"); - - // bound function for organization - var productPrice = builder.EntityType().Collection. - Function("GetPrice").Returns(); - productPrice.Parameter("organizationId").Required(); - productPrice.Parameter("partId").Required(); - - productPrice = builder.EntityType().Collection. - Function("GetPrice2").ReturnsCollectionFromEntitySet("Organizations"); - productPrice.Parameter("organizationId").Required(); - productPrice.Parameter("partId").Required(); - productPrice.IsComposable = true; - - // Add a composable function - var getOrgByAccount = - builder.EntityType() - .Collection - .Function("GetByAccount") - .ReturnsFromEntitySet("Organizations"); - getOrgByAccount.Parameter("accountId").Required(); - getOrgByAccount.IsComposable = true; - - builder.EntityType().Action("MarkAsFavourite"); - - // Add another composable function - var getOrgByAccount2 = - builder.EntityType() - .Collection - .Function("GetByAccount2") - .ReturnsCollectionFromEntitySet("Organizations"); // be noted, it returns collection. - getOrgByAccount2.Parameter("accountId").Required(); - getOrgByAccount2.IsComposable = true; - - return builder.GetEdmModel(); - } - - public static IEdmModel GetEdmModelV2() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Orders"); - - builder.Singleton("VipOrder"); - builder.Singleton("Categories"); - - var functionWithComplexTypeParameter = builder.EntityType().Function("CanMoveToAddress").Returns(); - functionWithComplexTypeParameter.Parameter
("address"); - - var functionWithCollectionComplexTypeParameter = builder.EntityType().Collection.Function("CanMoveToManyAddress").Returns(); - functionWithCollectionComplexTypeParameter.CollectionParameter
("addresses"); - - // function with optional parameters - var functionWithOptional = builder.EntityType().Function("GetWholeSalary").ReturnsCollectionFromEntitySet("Orders"); - functionWithOptional.Parameter("minSalary"); - functionWithOptional.Parameter("maxSalary").Optional(); - functionWithOptional.Parameter("aveSalary").HasDefaultValue("129"); - - // Function 1 - var function = builder.Function("RateByOrder"); - function.Parameter("order"); - function.Returns(); - - // Function 2 - function = builder.Function("CalcByOrder"); - function.Parameter("name"); - function.Parameter("order"); - function.Returns(); - - return builder.GetEdmModel(); - } - - public static IEdmModel GetEdmModelV3() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("tenants"); - builder.EntitySet("devices"); - builder.EntitySet("folders"); - builder.EntitySet("pages"); - - builder.EntitySet("TestEntities"); - return builder.GetEdmModel(); - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("tenants"); + builder.EntitySet("devices"); + builder.EntitySet("folders"); + builder.EntitySet("pages"); + + builder.EntitySet("TestEntities"); + return builder.GetEdmModel(); } } diff --git a/sample/ODataRoutingSample/Models/MyDataContext.cs b/sample/ODataRoutingSample/Models/MyDataContext.cs index b527ffdf0..672336259 100644 --- a/sample/ODataRoutingSample/Models/MyDataContext.cs +++ b/sample/ODataRoutingSample/Models/MyDataContext.cs @@ -7,25 +7,24 @@ using Microsoft.EntityFrameworkCore; -namespace ODataRoutingSample.Models +namespace ODataRoutingSample.Models; + +public class MyDataContext : DbContext { - public class MyDataContext : DbContext + public MyDataContext(DbContextOptions options) + : base(options) { - public MyDataContext(DbContextOptions options) - : base(options) - { - } + } - public DbSet Customers { get; set; } + public DbSet Customers { get; set; } - public DbSet Orders { get; set; } + public DbSet Orders { get; set; } - public DbSet Products { get; set; } + public DbSet Products { get; set; } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().OwnsOne(c => c.HomeAddress).WithOwner(); - modelBuilder.Entity().OwnsMany(c => c.FavoriteAddresses).WithOwner(); - } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().OwnsOne(c => c.HomeAddress).WithOwner(); + modelBuilder.Entity().OwnsMany(c => c.FavoriteAddresses).WithOwner(); } } diff --git a/sample/ODataRoutingSample/Models/Order.cs b/sample/ODataRoutingSample/Models/Order.cs index 695afa463..d2cbc1e5c 100644 --- a/sample/ODataRoutingSample/Models/Order.cs +++ b/sample/ODataRoutingSample/Models/Order.cs @@ -5,19 +5,18 @@ // //------------------------------------------------------------------------------ -namespace ODataRoutingSample.Models +namespace ODataRoutingSample.Models; + +public class Order { - public class Order - { - public int Id { get; set; } + public int Id { get; set; } - public string Title { get; set; } + public string Title { get; set; } - public virtual Category Category { get; set; } - } + public virtual Category Category { get; set; } +} - public class VipOrder : Order - { - public virtual Category VipCategory { get; set; } - } +public class VipOrder : Order +{ + public virtual Category VipCategory { get; set; } } diff --git a/sample/ODataRoutingSample/Models/Organization.cs b/sample/ODataRoutingSample/Models/Organization.cs index d9eb3ac96..ab1e70bdd 100644 --- a/sample/ODataRoutingSample/Models/Organization.cs +++ b/sample/ODataRoutingSample/Models/Organization.cs @@ -7,23 +7,22 @@ using System.Collections.Generic; -namespace ODataRoutingSample.Models +namespace ODataRoutingSample.Models; + +public class Organization { - public class Organization - { - public int OrganizationId { get; set; } + public int OrganizationId { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public IList Departs { get; set; } - } + public IList Departs { get; set; } +} - public class Department - { - public int DepartmentId { get; set; } +public class Department +{ + public int DepartmentId { get; set; } - public string Alias { get; set; } + public string Alias { get; set; } - public string Name { get; set; } - } + public string Name { get; set; } } diff --git a/sample/ODataRoutingSample/Models/Person.cs b/sample/ODataRoutingSample/Models/Person.cs index f871f3c0f..a10a7aab9 100644 --- a/sample/ODataRoutingSample/Models/Person.cs +++ b/sample/ODataRoutingSample/Models/Person.cs @@ -7,38 +7,37 @@ using System.Collections.Generic; -namespace ODataRoutingSample.Models +namespace ODataRoutingSample.Models; + +public class Person { - public class Person - { - // [Column(Order = 1)] // This attribute can be used with [Key] convention model building - // It is ignored if the property is added explicitly. - public string FirstName { get; set; } + // [Column(Order = 1)] // This attribute can be used with [Key] convention model building + // It is ignored if the property is added explicitly. + public string FirstName { get; set; } - // [Column(Order = 2)] - public string LastName { get; set; } + // [Column(Order = 2)] + public string LastName { get; set; } - public object Data { get; set; } // Edm.Untyped + public object Data { get; set; } // Edm.Untyped - public object Other { get; set; } + public object Other { get; set; } - public IList Infos { get; set; } // Collection(Edm.Untyped) + public IList Infos { get; set; } // Collection(Edm.Untyped) - public IList Sources { get; set; } // Collection(Edm.Untyped) + public IList Sources { get; set; } // Collection(Edm.Untyped) - public PersonExtraInfo CustomProperties { get; set; } - } + public PersonExtraInfo CustomProperties { get; set; } +} - public class PersonExtraInfo - { - /// - /// Gets or sets the properties. - /// - public Dictionary Properties { get; set; } = new(); - } +public class PersonExtraInfo +{ + /// + /// Gets or sets the properties. + /// + public Dictionary Properties { get; set; } = new(); +} - public enum AnyEnum - { - E1 - } +public enum AnyEnum +{ + E1 } diff --git a/sample/ODataRoutingSample/Models/Product.cs b/sample/ODataRoutingSample/Models/Product.cs index bbefa40b4..e9890ecb1 100644 --- a/sample/ODataRoutingSample/Models/Product.cs +++ b/sample/ODataRoutingSample/Models/Product.cs @@ -7,36 +7,35 @@ using System; -namespace ODataRoutingSample.Models +namespace ODataRoutingSample.Models; + +public class Product { - public class Product - { - public int Id { get; set; } + public int Id { get; set; } - public string Category { get; set; } + public string Category { get; set; } - public Color Color { get; set; } + public Color Color { get; set; } - public DateTimeOffset CreatedDate { get; set; } + public DateTimeOffset CreatedDate { get; set; } - public DateTimeOffset? UpdatedDate { get; set; } + public DateTimeOffset? UpdatedDate { get; set; } - public virtual ProductDetail Detail { get; set; } - } + public virtual ProductDetail Detail { get; set; } +} - public class ProductDetail - { - public string Id { get; set; } +public class ProductDetail +{ + public string Id { get; set; } - public string Info { get; set; } - } + public string Info { get; set; } +} - public enum Color - { - Red, +public enum Color +{ + Red, - Green, + Green, - Blue - } + Blue } diff --git a/sample/ODataRoutingSample/Models/Tenant.cs b/sample/ODataRoutingSample/Models/Tenant.cs index aefee638b..d82cb6ad4 100644 --- a/sample/ODataRoutingSample/Models/Tenant.cs +++ b/sample/ODataRoutingSample/Models/Tenant.cs @@ -9,53 +9,52 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace ODataRoutingSample.Models -{ - public class DriverTenant - { - [Key] - public string tenantId { get; set; } +namespace ODataRoutingSample.Models; - public IList devices { get; set; } +public class DriverTenant +{ + [Key] + public string tenantId { get; set; } - public IList folders { get; set; } + public IList devices { get; set; } - public IList pages { get; set; } - } + public IList folders { get; set; } - public class DriverDevice - { - [Key] - public string deviceId { get; set; } - } + public IList pages { get; set; } +} - public class DriverFolder - { - [Key] - public Guid folderId { get; set; } - } +public class DriverDevice +{ + [Key] + public string deviceId { get; set; } +} - public class DriverPage - { - public int Id { get; set; } - } +public class DriverFolder +{ + [Key] + public Guid folderId { get; set; } +} +public class DriverPage +{ + public int Id { get; set; } +} - public class TestEntity - { - public int Id { get; set; } - public HuntingQueryResults Query { get; set; } - } +public class TestEntity +{ + public int Id { get; set; } - public class HuntingQueryResults - { - public IList Results { get; set; } - } + public HuntingQueryResults Query { get; set; } +} - public class HuntingRowResult - { - public IDictionary DynamicProperties { get; set; } - } +public class HuntingQueryResults +{ + public IList Results { get; set; } +} +public class HuntingRowResult +{ + public IDictionary DynamicProperties { get; set; } } + diff --git a/sample/ODataRoutingSample/OpenApi/ODataOpenApiBuilderExtensions.cs b/sample/ODataRoutingSample/OpenApi/ODataOpenApiBuilderExtensions.cs index 68679b725..7a114d52f 100644 --- a/sample/ODataRoutingSample/OpenApi/ODataOpenApiBuilderExtensions.cs +++ b/sample/ODataRoutingSample/OpenApi/ODataOpenApiBuilderExtensions.cs @@ -8,42 +8,41 @@ using System; using Microsoft.AspNetCore.Builder; -namespace ODataRoutingSample.OpenApi +namespace ODataRoutingSample.OpenApi; + +/// +/// Provides extension methods for to add OData routes. +/// +public static class ODataOpenApiBuilderExtensions { /// - /// Provides extension methods for to add OData routes. + /// Use OData OpenApi middleware. /// - public static class ODataOpenApiBuilderExtensions + /// The to use. + /// The . + public static IApplicationBuilder UseODataOpenApi(this IApplicationBuilder app) { - /// - /// Use OData OpenApi middleware. - /// - /// The to use. - /// The . - public static IApplicationBuilder UseODataOpenApi(this IApplicationBuilder app) + return app.UseODataOpenApi(route: "$openapi"); + } + + /// + /// Use OData OpenApi middleware. + /// + /// The to use. + /// The route name used to query the openapi. + /// The . + public static IApplicationBuilder UseODataOpenApi(this IApplicationBuilder app, string route) + { + if (app == null) { - return app.UseODataOpenApi(route: "$openapi"); + throw new ArgumentNullException(nameof(app)); } - /// - /// Use OData OpenApi middleware. - /// - /// The to use. - /// The route name used to query the openapi. - /// The . - public static IApplicationBuilder UseODataOpenApi(this IApplicationBuilder app, string route) + if (string.IsNullOrWhiteSpace(route)) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (string.IsNullOrWhiteSpace(route)) - { - throw new ArgumentNullException(nameof(route)); - } - - return app.UseMiddleware(route); + throw new ArgumentNullException(nameof(route)); } + + return app.UseMiddleware(route); } } diff --git a/sample/ODataRoutingSample/OpenApi/ODataOpenApiMiddleware.cs b/sample/ODataRoutingSample/OpenApi/ODataOpenApiMiddleware.cs index ddae14b82..f9d8a3c33 100644 --- a/sample/ODataRoutingSample/OpenApi/ODataOpenApiMiddleware.cs +++ b/sample/ODataRoutingSample/OpenApi/ODataOpenApiMiddleware.cs @@ -21,195 +21,194 @@ using Microsoft.Net.Http.Headers; using Microsoft.Extensions.Primitives; -namespace ODataRoutingSample.OpenApi +namespace ODataRoutingSample.OpenApi; + +public class ODataOpenApiMiddleware { - public class ODataOpenApiMiddleware + private readonly RequestDelegate _next; + private Dictionary _templateMappings = new Dictionary(); + private string _requestName = "$openapi"; + + /// + /// Instantiates a new instance of . + /// + /// The service provider, we don't inject the ODataOptions. + /// The next middleware. + /// The request name. + public ODataOpenApiMiddleware(IServiceProvider serviceProvider, RequestDelegate next, string requestName) { - private readonly RequestDelegate _next; - private Dictionary _templateMappings = new Dictionary(); - private string _requestName = "$openapi"; - - /// - /// Instantiates a new instance of . - /// - /// The service provider, we don't inject the ODataOptions. - /// The next middleware. - /// The request name. - public ODataOpenApiMiddleware(IServiceProvider serviceProvider, RequestDelegate next, string requestName) + _next = next; + _requestName = requestName; + + // We inject the service provider to let the middle ware pass without ODataOptions injected. + IOptions odataOptionsOptions = serviceProvider?.GetService>(); + if (odataOptionsOptions != null && odataOptionsOptions.Value != null) { - _next = next; - _requestName = requestName; + Initialize(odataOptionsOptions.Value); + } + } - // We inject the service provider to let the middle ware pass without ODataOptions injected. - IOptions odataOptionsOptions = serviceProvider?.GetService>(); - if (odataOptionsOptions != null && odataOptionsOptions.Value != null) - { - Initialize(odataOptionsOptions.Value); - } + /// + /// Invoke the OData $openapi middleware. + /// + /// The http context. + /// A task that can be awaited. + public async Task Invoke(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); } - /// - /// Invoke the OData $openapi middleware. - /// - /// The http context. - /// A task that can be awaited. - public async Task Invoke(HttpContext context) + string prefixName; + if (TryGetPrefixName(context, out prefixName)) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } + await ProcessOpenApiAsync(context, prefixName).ConfigureAwait(false); + } + else + { + await _next(context).ConfigureAwait(false); + } + } + + /// + /// process a $openapi request. + /// + /// The http context. + /// The related prefix. + /// + public virtual async Task ProcessOpenApiAsync(HttpContext context, string prefixName) + { + var doc = OpenApiDocumentExtensions.CreateDocument(context, prefixName); + + (string contentType, OpenApiSpecVersion openApiSpecVersion) = GetContentTypeAndVersion(context); + context.Response.Headers["Content-Type"] = contentType; - string prefixName; - if (TryGetPrefixName(context, out prefixName)) + string output; + + if (openApiSpecVersion == OpenApiSpecVersion.OpenApi3_0) + { + if (contentType == "application/json") { - await ProcessOpenApiAsync(context, prefixName).ConfigureAwait(false); + output = doc.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0); } else { - await _next(context).ConfigureAwait(false); + output = doc.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); } } - - /// - /// process a $openapi request. - /// - /// The http context. - /// The related prefix. - /// - public virtual async Task ProcessOpenApiAsync(HttpContext context, string prefixName) + else { - var doc = OpenApiDocumentExtensions.CreateDocument(context, prefixName); - - (string contentType, OpenApiSpecVersion openApiSpecVersion) = GetContentTypeAndVersion(context); - context.Response.Headers["Content-Type"] = contentType; - - string output; - - if (openApiSpecVersion == OpenApiSpecVersion.OpenApi3_0) + if (contentType == "application/json") { - if (contentType == "application/json") - { - output = doc.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0); - } - else - { - output = doc.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); - } + output = doc.SerializeAsJson(OpenApiSpecVersion.OpenApi2_0); } else { - if (contentType == "application/json") - { - output = doc.SerializeAsJson(OpenApiSpecVersion.OpenApi2_0); - } - else - { - output = doc.SerializeAsYaml(OpenApiSpecVersion.OpenApi2_0); - } + output = doc.SerializeAsYaml(OpenApiSpecVersion.OpenApi2_0); } - - await context.Response.WriteAsync(output); } - internal static (string, OpenApiSpecVersion) GetContentTypeAndVersion(HttpContext context) - { - Contract.Assert(context != null); + await context.Response.WriteAsync(output); + } - OpenApiSpecVersion specVersion = OpenApiSpecVersion.OpenApi3_0; // by default - // $format=application/json;version=2.0 - // $format=application/yaml;version=2.0 - // accept=application/json;version3.0 - HttpRequest request = context.Request; + internal static (string, OpenApiSpecVersion) GetContentTypeAndVersion(HttpContext context) + { + Contract.Assert(context != null); - string dollarFormatValue = null; - IQueryCollection queryCollection = request.Query; - if (queryCollection.ContainsKey("$format")) - { - StringValues dollarFormat = queryCollection["$format"]; - dollarFormatValue = dollarFormat.FirstOrDefault(); - } + OpenApiSpecVersion specVersion = OpenApiSpecVersion.OpenApi3_0; // by default + // $format=application/json;version=2.0 + // $format=application/yaml;version=2.0 + // accept=application/json;version3.0 + HttpRequest request = context.Request; - if (dollarFormatValue != null) + string dollarFormatValue = null; + IQueryCollection queryCollection = request.Query; + if (queryCollection.ContainsKey("$format")) + { + StringValues dollarFormat = queryCollection["$format"]; + dollarFormatValue = dollarFormat.FirstOrDefault(); + } + + if (dollarFormatValue != null) + { + MediaTypeHeaderValue parsedValue; + bool success = MediaTypeHeaderValue.TryParse(dollarFormatValue, out parsedValue); + if (success) { - MediaTypeHeaderValue parsedValue; - bool success = MediaTypeHeaderValue.TryParse(dollarFormatValue, out parsedValue); - if (success) + NameValueHeaderValue nameValueHeaderValue = parsedValue.Parameters.FirstOrDefault(p => p.Name == "version"); + if (nameValueHeaderValue != null) { - NameValueHeaderValue nameValueHeaderValue = parsedValue.Parameters.FirstOrDefault(p => p.Name == "version"); - if (nameValueHeaderValue != null) + string version = nameValueHeaderValue.Value.Value; + if (version == "2.0") { - string version = nameValueHeaderValue.Value.Value; - if (version == "2.0") - { - specVersion = OpenApiSpecVersion.OpenApi2_0; - } - } - - if (parsedValue.MediaType == "application/yaml") - { - return ("application/yaml", specVersion); + specVersion = OpenApiSpecVersion.OpenApi2_0; } + } - return ("application/json", specVersion); + if (parsedValue.MediaType == "application/yaml") + { + return ("application/yaml", specVersion); } - } - // default - return ("application/json", OpenApiSpecVersion.OpenApi3_0); + return ("application/json", specVersion); + } } - /// - /// Initialize the middleware - /// - /// The OData options. - private void Initialize(ODataOptions options) - { - Contract.Assert(options != null); + // default + return ("application/json", OpenApiSpecVersion.OpenApi3_0); + } - foreach (var model in options.RouteComponents) - { - string openapiPath = string.IsNullOrEmpty(model.Key) ? $"/{_requestName}" : $"/{model.Key}/{_requestName}"; - AddRoute(model.Key, openapiPath); - } - } + /// + /// Initialize the middleware + /// + /// The OData options. + private void Initialize(ODataOptions options) + { + Contract.Assert(options != null); - /// - /// Add a route name and template for openapi. - /// - /// The route prefix name. - /// The route template. - private void AddRoute(string prefixName, string routeTemplate) + foreach (var model in options.RouteComponents) { - string newRouteTemplate = routeTemplate.StartsWith("/", StringComparison.Ordinal) ? routeTemplate.Substring(1) : routeTemplate; - RouteTemplate parsedTemplate = TemplateParser.Parse(newRouteTemplate); - TemplateMatcher matcher = new TemplateMatcher(parsedTemplate, new RouteValueDictionary()); - _templateMappings[matcher] = prefixName; + string openapiPath = string.IsNullOrEmpty(model.Key) ? $"/{_requestName}" : $"/{model.Key}/{_requestName}"; + AddRoute(model.Key, openapiPath); } + } - /// - /// Try and get the openapi handler for a given path. - /// - /// The http context. - /// The route/prefix name if found or null. - /// true if a route name is found, otherwise false. - private bool TryGetPrefixName(HttpContext context, out string prefixName) - { - Contract.Assert(context != null); + /// + /// Add a route name and template for openapi. + /// + /// The route prefix name. + /// The route template. + private void AddRoute(string prefixName, string routeTemplate) + { + string newRouteTemplate = routeTemplate.StartsWith("/", StringComparison.Ordinal) ? routeTemplate.Substring(1) : routeTemplate; + RouteTemplate parsedTemplate = TemplateParser.Parse(newRouteTemplate); + TemplateMatcher matcher = new TemplateMatcher(parsedTemplate, new RouteValueDictionary()); + _templateMappings[matcher] = prefixName; + } + + /// + /// Try and get the openapi handler for a given path. + /// + /// The http context. + /// The route/prefix name if found or null. + /// true if a route name is found, otherwise false. + private bool TryGetPrefixName(HttpContext context, out string prefixName) + { + Contract.Assert(context != null); - prefixName = null; - string path = context.Request.Path; - foreach (var item in _templateMappings) + prefixName = null; + string path = context.Request.Path; + foreach (var item in _templateMappings) + { + RouteValueDictionary routeData = new RouteValueDictionary(); + if (item.Key.TryMatch(path, routeData)) { - RouteValueDictionary routeData = new RouteValueDictionary(); - if (item.Key.TryMatch(path, routeData)) - { - prefixName = item.Value; - return true; - } + prefixName = item.Value; + return true; } - - return false; } + + return false; } } diff --git a/sample/ODataRoutingSample/OpenApi/ODataOpenApiPathProvider.cs b/sample/ODataRoutingSample/OpenApi/ODataOpenApiPathProvider.cs index 93418e192..43360895a 100644 --- a/sample/ODataRoutingSample/OpenApi/ODataOpenApiPathProvider.cs +++ b/sample/ODataRoutingSample/OpenApi/ODataOpenApiPathProvider.cs @@ -10,28 +10,27 @@ using Microsoft.OpenApi.OData; using Microsoft.OpenApi.OData.Edm; -namespace ODataRoutingSample.OpenApi +namespace ODataRoutingSample.OpenApi; + +/// +/// OData openapi path provider +/// +internal class ODataOpenApiPathProvider : IODataPathProvider { - /// - /// OData openapi path provider - /// - internal class ODataOpenApiPathProvider : IODataPathProvider - { - private IList _paths = new List(); + private IList _paths = new List(); - public bool CanFilter(IEdmElement element) - { - return true; - } + public bool CanFilter(IEdmElement element) + { + return true; + } - public IEnumerable GetPaths(IEdmModel model, OpenApiConvertSettings settings) - { - return _paths; - } + public IEnumerable GetPaths(IEdmModel model, OpenApiConvertSettings settings) + { + return _paths; + } - public void Add(ODataPath path) - { - _paths.Add(path); - } + public void Add(ODataPath path) + { + _paths.Add(path); } } diff --git a/sample/ODataRoutingSample/OpenApi/ODataPathTranslater.cs b/sample/ODataRoutingSample/OpenApi/ODataPathTranslater.cs index fb14e430a..92f788dc7 100644 --- a/sample/ODataRoutingSample/OpenApi/ODataPathTranslater.cs +++ b/sample/ODataRoutingSample/OpenApi/ODataPathTranslater.cs @@ -11,161 +11,160 @@ using Microsoft.OData.Edm; using Microsoft.OpenApi.OData.Edm; -namespace ODataRoutingSample.OpenApi +namespace ODataRoutingSample.OpenApi; + +public static class ODataPathTranslater { - public static class ODataPathTranslater + public static ODataPath Translate(this ODataPathTemplate pathTemplate) { - public static ODataPath Translate(this ODataPathTemplate pathTemplate) + if (pathTemplate.Count == 0) { - if (pathTemplate.Count == 0) - { - // It's service root, so far, let's skip it. - return null; - } + // It's service root, so far, let's skip it. + return null; + } - IList newSegments = new List(); - foreach (var segment in pathTemplate) + IList newSegments = new List(); + foreach (var segment in pathTemplate) + { + switch (segment) { - switch (segment) - { - case MetadataSegmentTemplate: - newSegments.Add(new ODataMetadataSegment()); - break; - - case EntitySetSegmentTemplate entitySet: - newSegments.Add(entitySet.ConvertTo()); - break; - - case SingletonSegmentTemplate singleton: - newSegments.Add(singleton.ConvertTo()); - break; - - case KeySegmentTemplate key: - newSegments.Add(key.ConvertTo()); - break; - - case CastSegmentTemplate cast: - newSegments.Add(cast.ConvertTo()); - break; - - case PropertySegmentTemplate property: - // TODO: - return null; - //PropertySegmentTemplate property = (PropertySegmentTemplate)segment; - //newSegments.Add(property.ConvertTo()); - //break; - - case NavigationSegmentTemplate navigation: - newSegments.Add(navigation.ConvertTo()); - break; - - case FunctionSegmentTemplate function: - newSegments.Add(function.ConvertTo()); - break; - - case ActionSegmentTemplate action: - newSegments.Add(action.ConvertTo()); - break; - - case FunctionImportSegmentTemplate functionImport: - newSegments.Add(functionImport.ConvertTo()); - break; - - case ActionImportSegmentTemplate actionImport: - newSegments.Add(actionImport.ConvertTo()); - break; - - case ValueSegmentTemplate value: - return null; - //ValueSegmentTemplate value = (ValueSegmentTemplate)segment; - //newSegments.Add(value.ConvertTo()); - //break; - - case NavigationLinkSegmentTemplate navigationLink: - return null; - //NavigationLinkSegmentTemplate navigationLink = (NavigationLinkSegmentTemplate)segment; - //newSegments.Add(navigationLink.ConvertTo()); - //break; - - case CountSegmentTemplate count: - newSegments.Add(count.ConvertTo()); - break; - - case PathTemplateSegmentTemplate: - return null; - //KeySegmentTemplate key = (KeySegmentTemplate)segment; - //newSegments.Add(key.ConvertTo()); - //break; - - case DynamicSegmentTemplate: - return null; - //KeySegmentTemplate key = (KeySegmentTemplate)segment; - //newSegments.Add(key.ConvertTo()); - //break; - - default: - throw new NotSupportedException(); - } + case MetadataSegmentTemplate: + newSegments.Add(new ODataMetadataSegment()); + break; + + case EntitySetSegmentTemplate entitySet: + newSegments.Add(entitySet.ConvertTo()); + break; + + case SingletonSegmentTemplate singleton: + newSegments.Add(singleton.ConvertTo()); + break; + + case KeySegmentTemplate key: + newSegments.Add(key.ConvertTo()); + break; + + case CastSegmentTemplate cast: + newSegments.Add(cast.ConvertTo()); + break; + + case PropertySegmentTemplate property: + // TODO: + return null; + //PropertySegmentTemplate property = (PropertySegmentTemplate)segment; + //newSegments.Add(property.ConvertTo()); + //break; + + case NavigationSegmentTemplate navigation: + newSegments.Add(navigation.ConvertTo()); + break; + + case FunctionSegmentTemplate function: + newSegments.Add(function.ConvertTo()); + break; + + case ActionSegmentTemplate action: + newSegments.Add(action.ConvertTo()); + break; + + case FunctionImportSegmentTemplate functionImport: + newSegments.Add(functionImport.ConvertTo()); + break; + + case ActionImportSegmentTemplate actionImport: + newSegments.Add(actionImport.ConvertTo()); + break; + + case ValueSegmentTemplate value: + return null; + //ValueSegmentTemplate value = (ValueSegmentTemplate)segment; + //newSegments.Add(value.ConvertTo()); + //break; + + case NavigationLinkSegmentTemplate navigationLink: + return null; + //NavigationLinkSegmentTemplate navigationLink = (NavigationLinkSegmentTemplate)segment; + //newSegments.Add(navigationLink.ConvertTo()); + //break; + + case CountSegmentTemplate count: + newSegments.Add(count.ConvertTo()); + break; + + case PathTemplateSegmentTemplate: + return null; + //KeySegmentTemplate key = (KeySegmentTemplate)segment; + //newSegments.Add(key.ConvertTo()); + //break; + + case DynamicSegmentTemplate: + return null; + //KeySegmentTemplate key = (KeySegmentTemplate)segment; + //newSegments.Add(key.ConvertTo()); + //break; + + default: + throw new NotSupportedException(); } - - return new ODataPath(newSegments); } - public static ODataNavigationSourceSegment ConvertTo(this EntitySetSegmentTemplate entitySet) - { - return new ODataNavigationSourceSegment(entitySet.Segment.EntitySet); - } + return new ODataPath(newSegments); + } - public static ODataNavigationSourceSegment ConvertTo(this SingletonSegmentTemplate singleton) - { - return new ODataNavigationSourceSegment(singleton.Singleton); - } + public static ODataNavigationSourceSegment ConvertTo(this EntitySetSegmentTemplate entitySet) + { + return new ODataNavigationSourceSegment(entitySet.Segment.EntitySet); + } - public static ODataKeySegment ConvertTo(this KeySegmentTemplate key) - { - return new ODataKeySegment(key.EntityType, key.KeyMappings); - } + public static ODataNavigationSourceSegment ConvertTo(this SingletonSegmentTemplate singleton) + { + return new ODataNavigationSourceSegment(singleton.Singleton); + } - public static ODataTypeCastSegment ConvertTo(this CastSegmentTemplate cast) - { - // So far, only support the entity type cast - return new ODataTypeCastSegment(cast.ExpectedType as IEdmEntityType); - } + public static ODataKeySegment ConvertTo(this KeySegmentTemplate key) + { + return new ODataKeySegment(key.EntityType, key.KeyMappings); + } - //public static ODataTypeCastSegment ConvertTo(this PropertySegmentTemplate property) - //{ - // // So far, only support the entity type cast - // return new ODataTypeCastSegment(property); - //} + public static ODataTypeCastSegment ConvertTo(this CastSegmentTemplate cast) + { + // So far, only support the entity type cast + return new ODataTypeCastSegment(cast.ExpectedType as IEdmEntityType); + } - public static ODataNavigationPropertySegment ConvertTo(this NavigationSegmentTemplate navigation) - { - return new ODataNavigationPropertySegment(navigation.Segment.NavigationProperty); - } + //public static ODataTypeCastSegment ConvertTo(this PropertySegmentTemplate property) + //{ + // // So far, only support the entity type cast + // return new ODataTypeCastSegment(property); + //} - public static ODataOperationSegment ConvertTo(this FunctionSegmentTemplate function) - { - return new ODataOperationSegment(function.Function); - } + public static ODataNavigationPropertySegment ConvertTo(this NavigationSegmentTemplate navigation) + { + return new ODataNavigationPropertySegment(navigation.Segment.NavigationProperty); + } - public static ODataOperationSegment ConvertTo(this ActionSegmentTemplate action) - { - return new ODataOperationSegment(action.Action); - } + public static ODataOperationSegment ConvertTo(this FunctionSegmentTemplate function) + { + return new ODataOperationSegment(function.Function); + } - public static ODataOperationImportSegment ConvertTo(this FunctionImportSegmentTemplate functionImport) - { - return new ODataOperationImportSegment(functionImport.FunctionImport, functionImport.ParameterMappings); - } + public static ODataOperationSegment ConvertTo(this ActionSegmentTemplate action) + { + return new ODataOperationSegment(action.Action); + } - public static ODataOperationImportSegment ConvertTo(this ActionImportSegmentTemplate actionImport) - { - return new ODataOperationImportSegment(actionImport.ActionImport); - } + public static ODataOperationImportSegment ConvertTo(this FunctionImportSegmentTemplate functionImport) + { + return new ODataOperationImportSegment(functionImport.FunctionImport, functionImport.ParameterMappings); + } - public static ODataDollarCountSegment ConvertTo(this CountSegmentTemplate count) - { - return new ODataDollarCountSegment(); - } + public static ODataOperationImportSegment ConvertTo(this ActionImportSegmentTemplate actionImport) + { + return new ODataOperationImportSegment(actionImport.ActionImport); + } + + public static ODataDollarCountSegment ConvertTo(this CountSegmentTemplate count) + { + return new ODataDollarCountSegment(); } } diff --git a/sample/ODataRoutingSample/OpenApi/OpenApiDocumentExtensions.cs b/sample/ODataRoutingSample/OpenApi/OpenApiDocumentExtensions.cs index d23f47a94..d90dce5f9 100644 --- a/sample/ODataRoutingSample/OpenApi/OpenApiDocumentExtensions.cs +++ b/sample/ODataRoutingSample/OpenApi/OpenApiDocumentExtensions.cs @@ -18,97 +18,96 @@ using Microsoft.OpenApi.OData; using Microsoft.OpenApi.OData.Edm; -namespace ODataRoutingSample.OpenApi +namespace ODataRoutingSample.OpenApi; + +/// +/// Provides methods for . +/// +internal static class OpenApiDocumentExtensions { - /// - /// Provides methods for . - /// - internal static class OpenApiDocumentExtensions + public static OpenApiDocument CreateDocument(HttpContext context, string prefixName) { - public static OpenApiDocument CreateDocument(HttpContext context, string prefixName) + IDictionary tempateToPathDict = new Dictionary(); + ODataOpenApiPathProvider provider = new ODataOpenApiPathProvider(); + IEdmModel model = null; + EndpointDataSource dataSource = context.RequestServices.GetRequiredService(); + foreach (var endpoint in dataSource.Endpoints) { - IDictionary tempateToPathDict = new Dictionary(); - ODataOpenApiPathProvider provider = new ODataOpenApiPathProvider(); - IEdmModel model = null; - EndpointDataSource dataSource = context.RequestServices.GetRequiredService(); - foreach (var endpoint in dataSource.Endpoints) + IODataRoutingMetadata metadata = endpoint.Metadata.GetMetadata(); + if (metadata == null) { - IODataRoutingMetadata metadata = endpoint.Metadata.GetMetadata(); - if (metadata == null) - { - continue; - } - - if (metadata.Prefix != prefixName) - { - continue; - } - model = metadata.Model; - - RouteEndpoint routeEndpoint = endpoint as RouteEndpoint; - if (routeEndpoint == null) - { - continue; - } + continue; + } - // get rid of the prefix - int length = prefixName.Length; - string routePathTemplate = routeEndpoint.RoutePattern.RawText.Substring(length); - routePathTemplate = routePathTemplate.StartsWith("/") ? routePathTemplate : "/" + routePathTemplate; + if (metadata.Prefix != prefixName) + { + continue; + } + model = metadata.Model; - if (tempateToPathDict.TryGetValue(routePathTemplate, out ODataPath pathValue)) - { - var methods = GetHttpMethods(endpoint); - foreach (var method in methods) - { - pathValue.HttpMethods.Add(method); - } + RouteEndpoint routeEndpoint = endpoint as RouteEndpoint; + if (routeEndpoint == null) + { + continue; + } - continue; - } + // get rid of the prefix + int length = prefixName.Length; + string routePathTemplate = routeEndpoint.RoutePattern.RawText.Substring(length); + routePathTemplate = routePathTemplate.StartsWith("/") ? routePathTemplate : "/" + routePathTemplate; - var path = metadata.Template.Translate(); - if (path == null) + if (tempateToPathDict.TryGetValue(routePathTemplate, out ODataPath pathValue)) + { + var methods = GetHttpMethods(endpoint); + foreach (var method in methods) { - continue; + pathValue.HttpMethods.Add(method); } - path.PathTemplate = routePathTemplate; - provider.Add(path); - - var method1 = GetHttpMethods(endpoint); - foreach (var method in method1) - { - path.HttpMethods.Add(method); - } - tempateToPathDict[routePathTemplate] = path; + continue; } - OpenApiConvertSettings settings = new OpenApiConvertSettings + var path = metadata.Template.Translate(); + if (path == null) { - PathProvider = provider, - ServiceRoot = BuildAbsolute(context, prefixName) - }; + continue; + } - return model.ConvertToOpenApi(settings); - } + path.PathTemplate = routePathTemplate; + provider.Add(path); - private static IEnumerable GetHttpMethods(Endpoint endpoint) - { - HttpMethodMetadata methodMetadata = endpoint.Metadata.GetMetadata(); - if (methodMetadata != null) + var method1 = GetHttpMethods(endpoint); + foreach (var method in method1) { - return methodMetadata.HttpMethods; + path.HttpMethods.Add(method); } - - throw new Exception(); + tempateToPathDict[routePathTemplate] = path; } - internal static Uri BuildAbsolute(HttpContext context, string prefix) + OpenApiConvertSettings settings = new OpenApiConvertSettings { - HttpRequest request = context.Request; + PathProvider = provider, + ServiceRoot = BuildAbsolute(context, prefixName) + }; + + return model.ConvertToOpenApi(settings); + } - return new Uri(UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase) + prefix); + private static IEnumerable GetHttpMethods(Endpoint endpoint) + { + HttpMethodMetadata methodMetadata = endpoint.Metadata.GetMetadata(); + if (methodMetadata != null) + { + return methodMetadata.HttpMethods; } + + throw new Exception(); + } + + internal static Uri BuildAbsolute(HttpContext context, string prefix) + { + HttpRequest request = context.Request; + + return new Uri(UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase) + prefix); } } diff --git a/sample/ODataRoutingSample/Program.cs b/sample/ODataRoutingSample/Program.cs index 665ceb02f..29585a54f 100644 --- a/sample/ODataRoutingSample/Program.cs +++ b/sample/ODataRoutingSample/Program.cs @@ -14,20 +14,19 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace ODataRoutingSample +namespace ODataRoutingSample; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + CreateHostBuilder(args).Build().Run(); } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); } diff --git a/sample/ODataRoutingSample/Startup.cs b/sample/ODataRoutingSample/Startup.cs index f6d59df65..4914bf8bb 100644 --- a/sample/ODataRoutingSample/Startup.cs +++ b/sample/ODataRoutingSample/Startup.cs @@ -21,160 +21,159 @@ using ODataRoutingSample.Models; using ODataRoutingSample.OpenApi; -namespace ODataRoutingSample +namespace ODataRoutingSample; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + Configuration = configuration; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - #region Codes for backup and use to compare with preview design/implementation - //services.AddControllers(options => { - //{ - // options.Conventions.Add(new MetadataApplicationModelConventionAttribute()); - // options.Conventions.Add(new MetadataActionModelConvention()); - //}); - - /*services.AddConvention(); + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + #region Codes for backup and use to compare with preview design/implementation + //services.AddControllers(options => { + //{ + // options.Conventions.Add(new MetadataApplicationModelConventionAttribute()); + // options.Conventions.Add(new MetadataActionModelConvention()); + //}); + + /*services.AddConvention(); - services.AddOData() - .AddODataRouting(options => options - .AddModel(EdmModelBuilder.GetEdmModel()) - .AddModel("v1", EdmModelBuilder.GetEdmModelV1()) - .AddModel("v2{data}", EdmModelBuilder.GetEdmModelV2())); - - services.AddODataFormatter(); - services.AddODataQuery(options => options.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5)); + services.AddOData() + .AddODataRouting(options => options + .AddModel(EdmModelBuilder.GetEdmModel()) + .AddModel("v1", EdmModelBuilder.GetEdmModelV1()) + .AddModel("v2{data}", EdmModelBuilder.GetEdmModelV2())); + + services.AddODataFormatter(); + services.AddODataQuery(options => options.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5)); + */ + #endregion + + services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseInMemoryDatabase("MyDataContextList")); + + IEdmModel model0 = EdmModelBuilder.GetEdmModel(); + IEdmModel model1 = EdmModelBuilder.GetEdmModelV1(); + IEdmModel model2 = EdmModelBuilder.GetEdmModelV2(); + IEdmModel model3 = EdmModelBuilder.GetEdmModelV3(); + + services.AddControllers() + /* If you want to remove $metadata endpoint, you can use ControllerFeatureProvider as follows + .ConfigureApplicationPartManager(manager => + { + manager.FeatureProviders.Remove(manager.FeatureProviders.OfType().FirstOrDefault()); + manager.FeatureProviders.Add(new RemoveMetadataControllerFeatureProvider()); + }) + + or, remove MetadataRoutingConvention in AddOData as + opt.Conventions.Remove(opt.Conventions.First(convention => convention is MetadataRoutingConvention)); */ - #endregion - - services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseInMemoryDatabase("MyDataContextList")); - - IEdmModel model0 = EdmModelBuilder.GetEdmModel(); - IEdmModel model1 = EdmModelBuilder.GetEdmModelV1(); - IEdmModel model2 = EdmModelBuilder.GetEdmModelV2(); - IEdmModel model3 = EdmModelBuilder.GetEdmModelV3(); - - services.AddControllers() - /* If you want to remove $metadata endpoint, you can use ControllerFeatureProvider as follows - .ConfigureApplicationPartManager(manager => - { - manager.FeatureProviders.Remove(manager.FeatureProviders.OfType().FirstOrDefault()); - manager.FeatureProviders.Add(new RemoveMetadataControllerFeatureProvider()); - }) - - or, remove MetadataRoutingConvention in AddOData as - opt.Conventions.Remove(opt.Conventions.First(convention => convention is MetadataRoutingConvention)); - */ - .AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5) - .AddRouteComponents(model0) - .AddRouteComponents("v1", model1) - .AddRouteComponents("v2{data}", model2, services => services.AddSingleton()) - .AddRouteComponents("v3", model3) - .Conventions.Add(new MyConvention()) - ); - - services.AddSwaggerGen(); - } + .AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5) + .AddRouteComponents(model0) + .AddRouteComponents("v1", model1) + .AddRouteComponents("v2{data}", model2, services => services.AddSingleton()) + .AddRouteComponents("v3", model3) + .Conventions.Add(new MyConvention()) + ); + + services.AddSwaggerGen(); + } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } + app.UseDeveloperExceptionPage(); + } - // Use odata route debug, /$odata - app.UseODataRouteDebug(); + // Use odata route debug, /$odata + app.UseODataRouteDebug(); - // If you want to use /$openapi, enable the middleware. - app.UseODataOpenApi(); + // If you want to use /$openapi, enable the middleware. + app.UseODataOpenApi(); - // Add OData /$query middleware - app.UseODataQueryRequest(); + // Add OData /$query middleware + app.UseODataQueryRequest(); - // Add the OData Batch middleware to support OData $Batch - app.UseODataBatching(); + // Add the OData Batch middleware to support OData $Batch + app.UseODataBatching(); - app.UseSwagger(); - app.UseSwaggerUI(c => - { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "OData 8.x OpenAPI"); - c.SwaggerEndpoint("/$openapi", "OData raw OpenAPI"); - }); + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "OData 8.x OpenAPI"); + c.SwaggerEndpoint("/$openapi", "OData raw OpenAPI"); + }); - app.UseRouting(); + app.UseRouting(); - // Test middleware - app.Use(next => context => + // Test middleware + app.Use(next => context => + { + var endpoint = context.GetEndpoint(); + if (endpoint == null) { - var endpoint = context.GetEndpoint(); - if (endpoint == null) - { - return next(context); - } - return next(context); - }); + } - //app.UseAuthorization(); + return next(context); + }); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + //app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); } +} - public class RemoveMetadataControllerFeatureProvider : ControllerFeatureProvider +public class RemoveMetadataControllerFeatureProvider : ControllerFeatureProvider +{ + protected override bool IsController(TypeInfo typeInfo) { - protected override bool IsController(TypeInfo typeInfo) + if (typeInfo.FullName == "Microsoft.AspNetCore.OData.Routing.Controllers.MetadataController") { - if (typeInfo.FullName == "Microsoft.AspNetCore.OData.Routing.Controllers.MetadataController") - { - return false; - } - - return base.IsController(typeInfo); + return false; } + + return base.IsController(typeInfo); } +} +/// +/// My simple convention +/// +public class MyConvention : IODataControllerActionConvention +{ /// - /// My simple convention + /// Order value. /// - public class MyConvention : IODataControllerActionConvention + public int Order => -100; + + /// + /// Apply to action,. + /// + /// Http context. + /// true/false + public bool AppliesToAction(ODataControllerActionContext context) { - /// - /// Order value. - /// - public int Order => -100; - - /// - /// Apply to action,. - /// - /// Http context. - /// true/false - public bool AppliesToAction(ODataControllerActionContext context) - { - return true; // apply to all controller - } + return true; // apply to all controller + } - /// - /// Apply to controller - /// - /// Http context. - /// true/false - public bool AppliesToController(ODataControllerActionContext context) - { - return false; // continue for all others - } + /// + /// Apply to controller + /// + /// Http context. + /// true/false + public bool AppliesToController(ODataControllerActionContext context) + { + return false; // continue for all others } } diff --git a/sample/ODataRoutingSample/WeatherForecast.cs b/sample/ODataRoutingSample/WeatherForecast.cs index 6a589399b..3e62ecd63 100644 --- a/sample/ODataRoutingSample/WeatherForecast.cs +++ b/sample/ODataRoutingSample/WeatherForecast.cs @@ -7,16 +7,15 @@ using System; -namespace ODataRoutingSample +namespace ODataRoutingSample; + +public class WeatherForecast { - public class WeatherForecast - { - public DateTime Date { get; set; } + public DateTime Date { get; set; } - public int TemperatureC { get; set; } + public int TemperatureC { get; set; } - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - public string Summary { get; set; } - } + public string Summary { get; set; } } diff --git a/sample/ODataSampleCommon/ODataEndpointController.cs b/sample/ODataSampleCommon/ODataEndpointController.cs index 316c23756..b97dedb3d 100644 --- a/sample/ODataSampleCommon/ODataEndpointController.cs +++ b/sample/ODataSampleCommon/ODataEndpointController.cs @@ -15,142 +15,141 @@ using Microsoft.AspNetCore.OData.Routing; using Microsoft.AspNetCore.Routing; -namespace ODataSampleCommon +namespace ODataSampleCommon; + +/// +/// A controller for debugging that shows the OData endpoints. +/// +public class ODataEndpointController : ControllerBase { + private EndpointDataSource _dataSource; + /// - /// A controller for debugging that shows the OData endpoints. + /// Initializes a new instance of the class. /// - public class ODataEndpointController : ControllerBase + /// The data source. + public ODataEndpointController(EndpointDataSource dataSource) { - private EndpointDataSource _dataSource; - - /// - /// Initializes a new instance of the class. - /// - /// The data source. - public ODataEndpointController(EndpointDataSource dataSource) - { - _dataSource = dataSource; - } + _dataSource = dataSource; + } - /// - /// Get all routes. - /// - /// The content result. - [HttpGet("$odata")] - public IActionResult GetAllRoutes() - { - CreateRouteTables(_dataSource.Endpoints, out var stdRouteTable, out var odataRouteTable); + /// + /// Get all routes. + /// + /// The content result. + [HttpGet("$odata")] + public IActionResult GetAllRoutes() + { + CreateRouteTables(_dataSource.Endpoints, out var stdRouteTable, out var odataRouteTable); - string output = ODataRouteMappingHtmlTemplate; - output = output.Replace("ODATA_ROUTE_CONTENT", odataRouteTable, StringComparison.OrdinalIgnoreCase); - output = output.Replace("STD_ROUTE_CONTENT", stdRouteTable, StringComparison.OrdinalIgnoreCase); + string output = ODataRouteMappingHtmlTemplate; + output = output.Replace("ODATA_ROUTE_CONTENT", odataRouteTable, StringComparison.OrdinalIgnoreCase); + output = output.Replace("STD_ROUTE_CONTENT", stdRouteTable, StringComparison.OrdinalIgnoreCase); - return base.Content(output, "text/html"); - } + return base.Content(output, "text/html"); + } - private void CreateRouteTables(IReadOnlyList endpoints, out string stdRouteTable, out string odataRouteTable) + private void CreateRouteTables(IReadOnlyList endpoints, out string stdRouteTable, out string odataRouteTable) + { + var stdRoutes = new StringBuilder(); + var odataRoutes = new StringBuilder(); + foreach (var endpoint in _dataSource.Endpoints.OfType()) { - var stdRoutes = new StringBuilder(); - var odataRoutes = new StringBuilder(); - foreach (var endpoint in _dataSource.Endpoints.OfType()) + var controllerActionDescriptor = endpoint.Metadata.GetMetadata(); + if (controllerActionDescriptor == null) { - var controllerActionDescriptor = endpoint.Metadata.GetMetadata(); - if (controllerActionDescriptor == null) - { - continue; - } - - var metadata = endpoint.Metadata.GetMetadata(); - if (metadata == null) - { - AppendRoute(stdRoutes, endpoint); - } - else - { - AppendRoute(odataRoutes, endpoint); - } + continue; } - stdRouteTable = stdRoutes.ToString(); - odataRouteTable = odataRoutes.ToString(); - } - private static string GetHttpMethods(Endpoint endpoint) - { - var methodMetadata = endpoint.Metadata.GetMetadata(); - if (methodMetadata == null) + var metadata = endpoint.Metadata.GetMetadata(); + if (metadata == null) + { + AppendRoute(stdRoutes, endpoint); + } + else { - return ""; + AppendRoute(odataRoutes, endpoint); } - return string.Join(", ", methodMetadata.HttpMethods); } + stdRouteTable = stdRoutes.ToString(); + odataRouteTable = odataRoutes.ToString(); + } - /// - /// Process the endpoint - /// - /// The string builder to append HTML to. - /// The endpoint to render. - private static void AppendRoute(StringBuilder sb, RouteEndpoint endpoint) + private static string GetHttpMethods(Endpoint endpoint) + { + var methodMetadata = endpoint.Metadata.GetMetadata(); + if (methodMetadata == null) { - sb.Append(""); - sb.Append($"{endpoint.DisplayName}"); + return ""; + } + return string.Join(", ", methodMetadata.HttpMethods); + } - sb.Append($"{string.Join(",", GetHttpMethods(endpoint))}"); + /// + /// Process the endpoint + /// + /// The string builder to append HTML to. + /// The endpoint to render. + private static void AppendRoute(StringBuilder sb, RouteEndpoint endpoint) + { + sb.Append(""); + sb.Append($"{endpoint.DisplayName}"); - sb.Append(""); - var link = "" + endpoint.RoutePattern.RawText.TrimStart('/'); - sb.Append($"~/{link}"); - sb.Append(""); + sb.Append($"{string.Join(",", GetHttpMethods(endpoint))}"); - sb.Append(""); - } + sb.Append(""); + var link = "" + endpoint.RoutePattern.RawText.TrimStart('/'); + sb.Append($"~/{link}"); + sb.Append(""); - private static string ODataRouteMappingHtmlTemplate = @" + sb.Append(""); + } + + private static string ODataRouteMappingHtmlTemplate = @" - OData Endpoint Routing Debugger - +OData Endpoint Routing Debugger + -

OData Endpoint Mappings

-

- Got to non-OData endpoint mappings -

- - - - - - - ODATA_ROUTE_CONTENT -
Controller & Action HttpMethods Template
-

Non-OData Endpoint Mappings

-

- Go to OData endpoint mappings -

- - - - - - - STD_ROUTE_CONTENT -
Controller HttpMethods Template
+

OData Endpoint Mappings

+

+ Got to non-OData endpoint mappings +

+ + + + + + + ODATA_ROUTE_CONTENT +
Controller & Action HttpMethods Template
+

Non-OData Endpoint Mappings

+

+ Go to OData endpoint mappings +

+ + + + + + + STD_ROUTE_CONTENT +
Controller HttpMethods Template
"; - } } diff --git a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JDynamicTypeWrapperConverter.cs b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JDynamicTypeWrapperConverter.cs index 65f674fc2..99bf4298b 100644 --- a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JDynamicTypeWrapperConverter.cs +++ b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JDynamicTypeWrapperConverter.cs @@ -9,59 +9,58 @@ using Microsoft.AspNetCore.OData.Query.Wrapper; using Newtonsoft.Json; -namespace Microsoft.AspNetCore.OData.NewtonsoftJson +namespace Microsoft.AspNetCore.OData.NewtonsoftJson; + +/// +/// Represents a custom to serialize instances to JSON. +/// +internal class JDynamicTypeWrapperConverter : JsonConverter { /// - /// Represents a custom to serialize instances to JSON. + /// Determines whether this instance can convert the specified type. /// - internal class JDynamicTypeWrapperConverter : JsonConverter + /// Type of the object. + /// true if this instance can convert the specified object type; otherwise, false. + public override bool CanConvert(Type objectType) { - /// - /// Determines whether this instance can convert the specified type. - /// - /// Type of the object. - /// true if this instance can convert the specified object type; otherwise, false. - public override bool CanConvert(Type objectType) + if (objectType is null) { - if (objectType is null) - { - throw new ArgumentNullException(nameof(objectType)); - } - - return typeof(DynamicTypeWrapper).IsAssignableFrom(objectType); + throw new ArgumentNullException(nameof(objectType)); } - /// - /// Reads the JSON representation of the object. - /// - /// The to read from. - /// Type of the object. - /// The existing value of object being read. - /// The calling serializer. - /// The object value. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + return typeof(DynamicTypeWrapper).IsAssignableFrom(objectType); + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// The object value. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(SRResources.ReadDynamicTypeWrapperNotImplemented); + } + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (serializer is null) { - throw new NotImplementedException(SRResources.ReadDynamicTypeWrapperNotImplemented); + throw new ArgumentNullException(nameof(serializer)); } - /// - /// Writes the JSON representation of the object. - /// - /// The to write to. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + DynamicTypeWrapper dynamicTypeWrapper = value as DynamicTypeWrapper; + if (dynamicTypeWrapper != null) { - if (serializer is null) - { - throw new ArgumentNullException(nameof(serializer)); - } - - DynamicTypeWrapper dynamicTypeWrapper = value as DynamicTypeWrapper; - if (dynamicTypeWrapper != null) - { - serializer.Serialize(writer, dynamicTypeWrapper.Values); - } + serializer.Serialize(writer, dynamicTypeWrapper.Values); } } } diff --git a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JPageResultValueConverter.cs b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JPageResultValueConverter.cs index 6de89b644..fffc626de 100644 --- a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JPageResultValueConverter.cs +++ b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JPageResultValueConverter.cs @@ -9,59 +9,58 @@ using Microsoft.AspNetCore.OData.Results; using Newtonsoft.Json; -namespace Microsoft.AspNetCore.OData.NewtonsoftJson +namespace Microsoft.AspNetCore.OData.NewtonsoftJson; + +/// +/// Represents a custom to serialize instances to JSON. +/// +internal class JPageResultValueConverter : JsonConverter { /// - /// Represents a custom to serialize instances to JSON. + /// Determines whether this instance can convert the specified type. /// - internal class JPageResultValueConverter : JsonConverter + /// Type of the object. + /// true if this instance can convert the specified object type; otherwise, false. + public override bool CanConvert(Type objectType) { - /// - /// Determines whether this instance can convert the specified type. - /// - /// Type of the object. - /// true if this instance can convert the specified object type; otherwise, false. - public override bool CanConvert(Type objectType) + if (objectType is null) { - if (objectType is null) - { - throw new ArgumentNullException(nameof(objectType)); - } - - return typeof(PageResult).IsAssignableFrom(objectType); + throw new ArgumentNullException(nameof(objectType)); } - /// - /// Reads the JSON representation of the object. - /// - /// The to read from. - /// Type of the object. - /// The existing value of object being read. - /// The calling serializer. - /// The object value. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + return typeof(PageResult).IsAssignableFrom(objectType); + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// The object value. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(SRResources.ReadPageResultNotImplemented); + } + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (serializer is null) { - throw new NotImplementedException(SRResources.ReadPageResultNotImplemented); + throw new ArgumentNullException(nameof(serializer)); } - /// - /// Writes the JSON representation of the object. - /// - /// The to write to. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + PageResult pageResult = value as PageResult; + if (pageResult != null) { - if (serializer is null) - { - throw new ArgumentNullException(nameof(serializer)); - } - - PageResult pageResult = value as PageResult; - if (pageResult != null) - { - serializer.Serialize(writer, pageResult.ToDictionary()); - } + serializer.Serialize(writer, pageResult.ToDictionary()); } } } diff --git a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JSelectExpandWrapperConverter.cs b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JSelectExpandWrapperConverter.cs index 5fd198625..cecc87c2b 100644 --- a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JSelectExpandWrapperConverter.cs +++ b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JSelectExpandWrapperConverter.cs @@ -11,78 +11,77 @@ using Microsoft.OData.Edm; using Newtonsoft.Json; -namespace Microsoft.AspNetCore.OData.NewtonsoftJson +namespace Microsoft.AspNetCore.OData.NewtonsoftJson; + +/// +/// Represents a custom to serialize instances to JSON. +/// +internal class JSelectExpandWrapperConverter : JsonConverter { + private Func _mapperProvider; + /// - /// Represents a custom to serialize instances to JSON. + /// Initializes a new instance of the class. /// - internal class JSelectExpandWrapperConverter : JsonConverter + public JSelectExpandWrapperConverter() + : this ((IEdmModel model, IEdmStructuredType type) => new JsonPropertyNameMapper(model, type)) { - private Func _mapperProvider; + } - /// - /// Initializes a new instance of the class. - /// - public JSelectExpandWrapperConverter() - : this ((IEdmModel model, IEdmStructuredType type) => new JsonPropertyNameMapper(model, type)) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The mapper provider. + public JSelectExpandWrapperConverter(Func mapperProvider) + { + _mapperProvider = mapperProvider ?? throw new ArgumentNullException(nameof(mapperProvider)); + } - /// - /// Initializes a new instance of the class. - /// - /// The mapper provider. - public JSelectExpandWrapperConverter(Func mapperProvider) + /// + /// Determines whether this instance can convert the specified type. + /// + /// Type of the object. + /// true if this instance can convert the specified object type; otherwise, false. + public override bool CanConvert(Type objectType) + { + if (objectType is null) { - _mapperProvider = mapperProvider ?? throw new ArgumentNullException(nameof(mapperProvider)); + throw new ArgumentNullException(nameof(objectType)); } - /// - /// Determines whether this instance can convert the specified type. - /// - /// Type of the object. - /// true if this instance can convert the specified object type; otherwise, false. - public override bool CanConvert(Type objectType) - { - if (objectType is null) - { - throw new ArgumentNullException(nameof(objectType)); - } + return typeof(ISelectExpandWrapper).IsAssignableFrom(objectType); + } - return typeof(ISelectExpandWrapper).IsAssignableFrom(objectType); - } + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// The object value. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(SRResources.ReadSelectExpandWrapperNotImplemented); + } - /// - /// Reads the JSON representation of the object. - /// - /// The to read from. - /// Type of the object. - /// The existing value of object being read. - /// The calling serializer. - /// The object value. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (serializer is null) { - throw new NotImplementedException(SRResources.ReadSelectExpandWrapperNotImplemented); + throw new ArgumentNullException(nameof(serializer)); } - /// - /// Writes the JSON representation of the object. - /// - /// The to write to. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + ISelectExpandWrapper selectExpandWrapper = value as ISelectExpandWrapper; + if (selectExpandWrapper != null) { - if (serializer is null) - { - throw new ArgumentNullException(nameof(serializer)); - } - - ISelectExpandWrapper selectExpandWrapper = value as ISelectExpandWrapper; - if (selectExpandWrapper != null) - { - serializer.Serialize(writer, selectExpandWrapper.ToDictionary(_mapperProvider)); - } + serializer.Serialize(writer, selectExpandWrapper.ToDictionary(_mapperProvider)); } } } diff --git a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JSingleResultValueConverter.cs b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JSingleResultValueConverter.cs index 26032821c..a863a4bfc 100644 --- a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JSingleResultValueConverter.cs +++ b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JSingleResultValueConverter.cs @@ -10,63 +10,62 @@ using Microsoft.AspNetCore.OData.Results; using Newtonsoft.Json; -namespace Microsoft.AspNetCore.OData.NewtonsoftJson +namespace Microsoft.AspNetCore.OData.NewtonsoftJson; + +/// +/// Represents a custom to serialize instances to JSON. +/// +internal class JSingleResultValueConverter : JsonConverter { /// - /// Represents a custom to serialize instances to JSON. + /// Determines whether this instance can convert the specified type. /// - internal class JSingleResultValueConverter : JsonConverter + /// Type of the object. + /// true if this instance can convert the specified object type; otherwise, false. + public override bool CanConvert(Type objectType) { - /// - /// Determines whether this instance can convert the specified type. - /// - /// Type of the object. - /// true if this instance can convert the specified object type; otherwise, false. - public override bool CanConvert(Type objectType) + if (objectType is null) { - if (objectType is null) - { - throw new ArgumentNullException(nameof(objectType)); - } - - return typeof(SingleResult).IsAssignableFrom(objectType); + throw new ArgumentNullException(nameof(objectType)); } - /// - /// Reads the JSON representation of the object. - /// - /// The to read from. - /// Type of the object. - /// The existing value of object being read. - /// The calling serializer. - /// The object value. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + return typeof(SingleResult).IsAssignableFrom(objectType); + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// The object value. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(SRResources.ReadSingleResultNotImplemented); + } + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (serializer is null) { - throw new NotImplementedException(SRResources.ReadSingleResultNotImplemented); + throw new ArgumentNullException(nameof(serializer)); } - /// - /// Writes the JSON representation of the object. - /// - /// The to write to. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + SingleResult singleResult = value as SingleResult; + if (singleResult != null) { - if (serializer is null) - { - throw new ArgumentNullException(nameof(serializer)); - } - - SingleResult singleResult = value as SingleResult; - if (singleResult != null) + // TODO: make sure the implementation (to get the first object) is correct? + var singleObject = singleResult.Queryable.Cast().FirstOrDefault(); + if (singleObject is not null) { - // TODO: make sure the implementation (to get the first object) is correct? - var singleObject = singleResult.Queryable.Cast().FirstOrDefault(); - if (singleObject is not null) - { - serializer.Serialize(writer, singleObject); - } + serializer.Serialize(writer, singleObject); } } } diff --git a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JsonPropertyNameMapper.cs b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JsonPropertyNameMapper.cs index cc7e0ded2..bd5f7bf75 100644 --- a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JsonPropertyNameMapper.cs +++ b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/JsonPropertyNameMapper.cs @@ -14,86 +14,85 @@ using Microsoft.OData.ModelBuilder; using Newtonsoft.Json; -namespace Microsoft.AspNetCore.OData.NewtonsoftJson +namespace Microsoft.AspNetCore.OData.NewtonsoftJson; + +/// +/// Edm Property name mapper. +/// +internal class JsonPropertyNameMapper : IPropertyMapper { + private IEdmModel _model; + private IEdmStructuredType _type; + /// - /// Edm Property name mapper. + /// Initializes a new instance of the class. /// - internal class JsonPropertyNameMapper : IPropertyMapper + /// The Edm model. + /// The Edm structured type. + public JsonPropertyNameMapper(IEdmModel model, IEdmStructuredType type) { - private IEdmModel _model; - private IEdmStructuredType _type; + _model = model ?? throw new ArgumentNullException(nameof(model)); + _type = type ?? throw new ArgumentNullException(nameof(type)); + } - /// - /// Initializes a new instance of the class. - /// - /// The Edm model. - /// The Edm structured type. - public JsonPropertyNameMapper(IEdmModel model, IEdmStructuredType type) + /// + /// Map the given property name. + /// + /// The given property name. + /// The mapped property name. + public string MapProperty(string propertyName) + { + if (string.IsNullOrEmpty(propertyName)) { - _model = model ?? throw new ArgumentNullException(nameof(model)); - _type = type ?? throw new ArgumentNullException(nameof(type)); + throw new ArgumentNullException(nameof(propertyName)); } - /// - /// Map the given property name. - /// - /// The given property name. - /// The mapped property name. - public string MapProperty(string propertyName) - { - if (string.IsNullOrEmpty(propertyName)) - { - throw new ArgumentNullException(nameof(propertyName)); - } + IEdmProperty property = _type.Properties().Single(s => s.Name == propertyName); + PropertyInfo info = GetPropertyInfo(property); - IEdmProperty property = _type.Properties().Single(s => s.Name == propertyName); - PropertyInfo info = GetPropertyInfo(property); - - JsonIgnoreAttribute jsonIgnore = GetJsonIgnore(info); - if (jsonIgnore != null) - { - return null; - } - - JsonPropertyAttribute jsonProperty = GetJsonProperty(info); - if (jsonProperty != null && !string.IsNullOrWhiteSpace(jsonProperty.PropertyName)) - { - return jsonProperty.PropertyName; - } - else - { - return property.Name; - } + JsonIgnoreAttribute jsonIgnore = GetJsonIgnore(info); + if (jsonIgnore != null) + { + return null; } - private PropertyInfo GetPropertyInfo(IEdmProperty property) + JsonPropertyAttribute jsonProperty = GetJsonProperty(info); + if (jsonProperty != null && !string.IsNullOrWhiteSpace(jsonProperty.PropertyName)) { - ClrPropertyInfoAnnotation clrPropertyAnnotation = _model.GetAnnotationValue(property); - if (clrPropertyAnnotation != null) - { - return clrPropertyAnnotation.ClrPropertyInfo; - } - - ClrTypeAnnotation clrTypeAnnotation = _model.GetAnnotationValue(property.DeclaringType); - Contract.Assert(clrTypeAnnotation != null); - - PropertyInfo info = clrTypeAnnotation.ClrType.GetProperty(property.Name); - Contract.Assert(info != null); - - return info; + return jsonProperty.PropertyName; } - - private static JsonPropertyAttribute GetJsonProperty(PropertyInfo property) + else { - return property.GetCustomAttributes(typeof(JsonPropertyAttribute), inherit: false) - .OfType().SingleOrDefault(); + return property.Name; } + } - private static JsonIgnoreAttribute GetJsonIgnore(PropertyInfo property) + private PropertyInfo GetPropertyInfo(IEdmProperty property) + { + ClrPropertyInfoAnnotation clrPropertyAnnotation = _model.GetAnnotationValue(property); + if (clrPropertyAnnotation != null) { - return property.GetCustomAttributes(typeof(JsonIgnoreAttribute), inherit: false) - .OfType().SingleOrDefault(); + return clrPropertyAnnotation.ClrPropertyInfo; } + + ClrTypeAnnotation clrTypeAnnotation = _model.GetAnnotationValue(property.DeclaringType); + Contract.Assert(clrTypeAnnotation != null); + + PropertyInfo info = clrTypeAnnotation.ClrType.GetProperty(property.Name); + Contract.Assert(info != null); + + return info; + } + + private static JsonPropertyAttribute GetJsonProperty(PropertyInfo property) + { + return property.GetCustomAttributes(typeof(JsonPropertyAttribute), inherit: false) + .OfType().SingleOrDefault(); + } + + private static JsonIgnoreAttribute GetJsonIgnore(PropertyInfo property) + { + return property.GetCustomAttributes(typeof(JsonIgnoreAttribute), inherit: false) + .OfType().SingleOrDefault(); } } diff --git a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/ODataNewtonsoftJsonMvcBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/ODataNewtonsoftJsonMvcBuilderExtensions.cs index fee0eb930..3b3292ee8 100644 --- a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/ODataNewtonsoftJsonMvcBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/ODataNewtonsoftJsonMvcBuilderExtensions.cs @@ -11,90 +11,89 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.NewtonsoftJson +namespace Microsoft.AspNetCore.OData.NewtonsoftJson; + +/// +/// Extension methods for adding OData Json converter to Newtonsoft.Json to and . +/// +public static class ODataNewtonsoftJsonMvcBuilderExtensions { + #region IMvcBuilder /// - /// Extension methods for adding OData Json converter to Newtonsoft.Json to and . + /// Configures Newtonsoft.Json using OData Json converter. /// - public static class ODataNewtonsoftJsonMvcBuilderExtensions + /// The Mvc builder. + /// The . + public static IMvcBuilder AddODataNewtonsoftJson(this IMvcBuilder builder) { - #region IMvcBuilder - /// - /// Configures Newtonsoft.Json using OData Json converter. - /// - /// The Mvc builder. - /// The . - public static IMvcBuilder AddODataNewtonsoftJson(this IMvcBuilder builder) + return builder.AddODataNewtonsoftJson(null); + } + + /// + /// Configures Newtonsoft.Json using OData Json converter. + /// + /// The Mvc builder. + /// The mapper provider. + /// The . + public static IMvcBuilder AddODataNewtonsoftJson(this IMvcBuilder builder, + Func mapperProvider) + { + if (builder is null) { - return builder.AddODataNewtonsoftJson(null); + throw new ArgumentNullException(nameof(builder)); } - /// - /// Configures Newtonsoft.Json using OData Json converter. - /// - /// The Mvc builder. - /// The mapper provider. - /// The . - public static IMvcBuilder AddODataNewtonsoftJson(this IMvcBuilder builder, - Func mapperProvider) - { - if (builder is null) - { - throw new ArgumentNullException(nameof(builder)); - } + return builder.AddNewtonsoftJson(BuildSetupAction(mapperProvider)); + } + #endregion - return builder.AddNewtonsoftJson(BuildSetupAction(mapperProvider)); - } - #endregion + #region IMvcCoreBuilder + /// + /// Configures Newtonsoft.Json using OData Json converter. + /// + /// The Mvc core builder. + /// The . + public static IMvcCoreBuilder AddODataNewtonsoftJson(this IMvcCoreBuilder builder) + { + return builder.AddODataNewtonsoftJson(null); + } - #region IMvcCoreBuilder - /// - /// Configures Newtonsoft.Json using OData Json converter. - /// - /// The Mvc core builder. - /// The . - public static IMvcCoreBuilder AddODataNewtonsoftJson(this IMvcCoreBuilder builder) + /// + /// Configures Newtonsoft.Json using OData Json converter. + /// + /// The Mvc core builder. + /// The mapper provider. + /// The . + public static IMvcCoreBuilder AddODataNewtonsoftJson(this IMvcCoreBuilder builder, + Func mapperProvider) + { + if (builder is null) { - return builder.AddODataNewtonsoftJson(null); + throw new ArgumentNullException(nameof(builder)); } - /// - /// Configures Newtonsoft.Json using OData Json converter. - /// - /// The Mvc core builder. - /// The mapper provider. - /// The . - public static IMvcCoreBuilder AddODataNewtonsoftJson(this IMvcCoreBuilder builder, - Func mapperProvider) + return builder.AddNewtonsoftJson(BuildSetupAction(mapperProvider)); + } + #endregion + + private static Action BuildSetupAction(Func mapperProvider) + { + Action odataSetupAction = opt => { - if (builder is null) + if (mapperProvider is null) { - throw new ArgumentNullException(nameof(builder)); + opt.SerializerSettings.Converters.Add(new JSelectExpandWrapperConverter()); } - - return builder.AddNewtonsoftJson(BuildSetupAction(mapperProvider)); - } - #endregion - - private static Action BuildSetupAction(Func mapperProvider) - { - Action odataSetupAction = opt => + else { - if (mapperProvider is null) - { - opt.SerializerSettings.Converters.Add(new JSelectExpandWrapperConverter()); - } - else - { - opt.SerializerSettings.Converters.Add(new JSelectExpandWrapperConverter(mapperProvider)); - } + opt.SerializerSettings.Converters.Add(new JSelectExpandWrapperConverter(mapperProvider)); + } - opt.SerializerSettings.Converters.Add(new JDynamicTypeWrapperConverter()); - opt.SerializerSettings.Converters.Add(new JPageResultValueConverter()); - opt.SerializerSettings.Converters.Add(new JSingleResultValueConverter()); - }; + opt.SerializerSettings.Converters.Add(new JDynamicTypeWrapperConverter()); + opt.SerializerSettings.Converters.Add(new JPageResultValueConverter()); + opt.SerializerSettings.Converters.Add(new JSingleResultValueConverter()); + }; - return odataSetupAction; - } + return odataSetupAction; } } diff --git a/src/Microsoft.AspNetCore.OData/Abstracts/AssemblyResolverHelper.cs b/src/Microsoft.AspNetCore.OData/Abstracts/AssemblyResolverHelper.cs index 4a8a8629a..f1d76b0cf 100644 --- a/src/Microsoft.AspNetCore.OData/Abstracts/AssemblyResolverHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Abstracts/AssemblyResolverHelper.cs @@ -7,10 +7,9 @@ using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Abstracts +namespace Microsoft.AspNetCore.OData.Abstracts; + +internal static class AssemblyResolverHelper { - internal static class AssemblyResolverHelper - { - public static IAssemblyResolver Default = new DefaultAssemblyResolver(); - } + public static IAssemblyResolver Default = new DefaultAssemblyResolver(); } diff --git a/src/Microsoft.AspNetCore.OData/Abstracts/ETagActionFilterAttribute.cs b/src/Microsoft.AspNetCore.OData/Abstracts/ETagActionFilterAttribute.cs index c4e252903..1f7299221 100644 --- a/src/Microsoft.AspNetCore.OData/Abstracts/ETagActionFilterAttribute.cs +++ b/src/Microsoft.AspNetCore.OData/Abstracts/ETagActionFilterAttribute.cs @@ -22,199 +22,198 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Abstracts +namespace Microsoft.AspNetCore.OData.Abstracts; + +/// +/// Defines a to add an ETag header value to an OData response when the response +/// is a single resource that has an ETag defined. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] +public class ETagActionFilterAttribute : ActionFilterAttribute { - /// - /// Defines a to add an ETag header value to an OData response when the response - /// is a single resource that has an ETag defined. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public class ETagActionFilterAttribute : ActionFilterAttribute + /// + public override void OnActionExecuted(ActionExecutedContext actionExecutedContext) { - /// - public override void OnActionExecuted(ActionExecutedContext actionExecutedContext) + if (actionExecutedContext == null) { - if (actionExecutedContext == null) - { - throw Error.ArgumentNull(nameof(actionExecutedContext)); - } + throw Error.ArgumentNull(nameof(actionExecutedContext)); + } - if (actionExecutedContext.HttpContext == null) - { - throw Error.ArgumentNull("httpContext"); - } + if (actionExecutedContext.HttpContext == null) + { + throw Error.ArgumentNull("httpContext"); + } - HttpRequest request = actionExecutedContext.HttpContext.Request; - ODataPath path = request.ODataFeature().Path; - if (path == null) - { - throw Error.ArgumentNull("path"); - } + HttpRequest request = actionExecutedContext.HttpContext.Request; + ODataPath path = request.ODataFeature().Path; + if (path == null) + { + throw Error.ArgumentNull("path"); + } - IEdmModel model = request.GetModel(); - if (model == null) - { - throw Error.ArgumentNull("model"); - } + IEdmModel model = request.GetModel(); + if (model == null) + { + throw Error.ArgumentNull("model"); + } - IETagHandler etagHandler = request.GetETagHandler(); - if (etagHandler == null) - { - throw Error.ArgumentNull("etagHandler"); - } + IETagHandler etagHandler = request.GetETagHandler(); + if (etagHandler == null) + { + throw Error.ArgumentNull("etagHandler"); + } - // Need a value to operate on. - ObjectResult result = actionExecutedContext.Result as ObjectResult; - if (result == null) - { - return; - } + // Need a value to operate on. + ObjectResult result = actionExecutedContext.Result as ObjectResult; + if (result == null) + { + return; + } - HttpResponse response = actionExecutedContext.HttpContext.Response; - EntityTagHeaderValue etag = GetETag(response?.StatusCode, path, model, result.Value, etagHandler); + HttpResponse response = actionExecutedContext.HttpContext.Response; + EntityTagHeaderValue etag = GetETag(response?.StatusCode, path, model, result.Value, etagHandler); - if (etag != null) - { - response.Headers["ETag"] = etag.ToString(); - } + if (etag != null) + { + response.Headers["ETag"] = etag.ToString(); } + } - private static EntityTagHeaderValue GetETag(int? statusCode, ODataPath path, IEdmModel model, object value, IETagHandler etagHandler) + private static EntityTagHeaderValue GetETag(int? statusCode, ODataPath path, IEdmModel model, object value, IETagHandler etagHandler) + { + Contract.Assert(path != null); + Contract.Assert(model != null); + Contract.Assert(etagHandler != null); + + // Do not interfere with null responses, we want to bubble it up to the top. + // Do not handle 204 responses as the spec says a 204 response must not include an ETag header + // unless the request's representation data was saved without any transformation applied to the body + // (i.e., the resource's new representation data is identical to the representation data received in the + // PUT request) and the ETag value reflects the new representation. + // Even in that case returning an ETag is optional and it requires access to the original object which is + // not possible with the current architecture, so if the user is interested he can set the ETag in that + // case by himself on the response. + if (statusCode == null || + !(statusCode.Value >= StatusCodes.Status200OK && statusCode.Value < StatusCodes.Status300MultipleChoices) || + statusCode.Value == StatusCodes.Status204NoContent) { - Contract.Assert(path != null); - Contract.Assert(model != null); - Contract.Assert(etagHandler != null); + return null; + } - // Do not interfere with null responses, we want to bubble it up to the top. - // Do not handle 204 responses as the spec says a 204 response must not include an ETag header - // unless the request's representation data was saved without any transformation applied to the body - // (i.e., the resource's new representation data is identical to the representation data received in the - // PUT request) and the ETag value reflects the new representation. - // Even in that case returning an ETag is optional and it requires access to the original object which is - // not possible with the current architecture, so if the user is interested he can set the ETag in that - // case by himself on the response. - if (statusCode == null || - !(statusCode.Value >= StatusCodes.Status200OK && statusCode.Value < StatusCodes.Status300MultipleChoices) || - statusCode.Value == StatusCodes.Status204NoContent) - { - return null; - } + IEdmEntityType edmType = GetSingleEntityEntityType(path); - IEdmEntityType edmType = GetSingleEntityEntityType(path); + IEdmEntityTypeReference typeReference = GetTypeReference(model, edmType, value); + if (typeReference != null) + { + ResourceContext context = CreateInstanceContext(model, typeReference, value); + context.EdmModel = model; + context.NavigationSource = path.GetNavigationSource(); + return CreateETag(context, etagHandler); + } - IEdmEntityTypeReference typeReference = GetTypeReference(model, edmType, value); - if (typeReference != null) - { - ResourceContext context = CreateInstanceContext(model, typeReference, value); - context.EdmModel = model; - context.NavigationSource = path.GetNavigationSource(); - return CreateETag(context, etagHandler); - } + return null; + } + private static IEdmEntityTypeReference GetTypeReference(IEdmModel model, IEdmEntityType edmType, object value) + { + if (model == null || edmType == null || value == null) + { return null; } - private static IEdmEntityTypeReference GetTypeReference(IEdmModel model, IEdmEntityType edmType, object value) + IEdmObject edmObject = value as IEdmEntityObject; + if (edmObject != null) { - if (model == null || edmType == null || value == null) - { - return null; - } + IEdmTypeReference edmTypeReference = edmObject.GetEdmType(); + return edmTypeReference.AsEntity(); + } - IEdmObject edmObject = value as IEdmEntityObject; - if (edmObject != null) - { - IEdmTypeReference edmTypeReference = edmObject.GetEdmType(); - return edmTypeReference.AsEntity(); - } + IEdmTypeReference reference = model.GetEdmTypeReference(value.GetType()); + if (reference != null && reference.Definition.IsOrInheritsFrom(edmType)) + { + return (IEdmEntityTypeReference)reference; + } - IEdmTypeReference reference = model.GetEdmTypeReference(value.GetType()); - if (reference != null && reference.Definition.IsOrInheritsFrom(edmType)) - { - return (IEdmEntityTypeReference)reference; - } + return null; + } - return null; - } + private static EntityTagHeaderValue CreateETag(ResourceContext resourceContext, IETagHandler handler) + { + IEdmModel model = resourceContext.EdmModel; - private static EntityTagHeaderValue CreateETag(ResourceContext resourceContext, IETagHandler handler) + IEnumerable concurrencyProperties; + if (model != null && resourceContext.NavigationSource != null) { - IEdmModel model = resourceContext.EdmModel; - - IEnumerable concurrencyProperties; - if (model != null && resourceContext.NavigationSource != null) - { - concurrencyProperties = model.GetConcurrencyProperties(resourceContext.NavigationSource).OrderBy(c => c.Name); - } - else - { - concurrencyProperties = Enumerable.Empty(); - } + concurrencyProperties = model.GetConcurrencyProperties(resourceContext.NavigationSource).OrderBy(c => c.Name); + } + else + { + concurrencyProperties = Enumerable.Empty(); + } - IDictionary properties = new Dictionary(); - foreach (IEdmStructuralProperty etagProperty in concurrencyProperties) - { - properties.Add(etagProperty.Name, resourceContext.GetPropertyValue(etagProperty.Name)); - } - return handler.CreateETag(properties, resourceContext.TimeZone); + IDictionary properties = new Dictionary(); + foreach (IEdmStructuralProperty etagProperty in concurrencyProperties) + { + properties.Add(etagProperty.Name, resourceContext.GetPropertyValue(etagProperty.Name)); } + return handler.CreateETag(properties, resourceContext.TimeZone); + } - private static ResourceContext CreateInstanceContext(IEdmModel model, IEdmEntityTypeReference reference, object value) + private static ResourceContext CreateInstanceContext(IEdmModel model, IEdmEntityTypeReference reference, object value) + { + Contract.Assert(reference != null); + Contract.Assert(value != null); + + ODataSerializerContext serializerCtx = new ODataSerializerContext { - Contract.Assert(reference != null); - Contract.Assert(value != null); + Model = model + }; - ODataSerializerContext serializerCtx = new ODataSerializerContext - { - Model = model - }; + return new ResourceContext(serializerCtx, reference, value); + } - return new ResourceContext(serializerCtx, reference, value); + // Retrieves the IEdmEntityType from the path only in the case that we are addressing a single entity. + // We iterate the path backwards and we return as soon as we realize we are referencing a single entity. + // That is, as soon as we find a singleton segment, a key segment or a navigation segment with target + // multiplicity 0..1 or 1. + internal static IEdmEntityType GetSingleEntityEntityType(ODataPath path) + { + if (path == null || path.Count == 0) + { + return null; } - // Retrieves the IEdmEntityType from the path only in the case that we are addressing a single entity. - // We iterate the path backwards and we return as soon as we realize we are referencing a single entity. - // That is, as soon as we find a singleton segment, a key segment or a navigation segment with target - // multiplicity 0..1 or 1. - internal static IEdmEntityType GetSingleEntityEntityType(ODataPath path) + int currentSegmentIndex = path.Count - 1; + + // Skip a possible sequence of casts at the end of the path. + while (currentSegmentIndex >= 0 && + path.ElementAt(currentSegmentIndex) is TypeSegment) { - if (path == null || path.Count == 0) - { - return null; - } + currentSegmentIndex--; + } - int currentSegmentIndex = path.Count - 1; + if (currentSegmentIndex < 0) + { + return null; + } - // Skip a possible sequence of casts at the end of the path. - while (currentSegmentIndex >= 0 && - path.ElementAt(currentSegmentIndex) is TypeSegment) - { - currentSegmentIndex--; - } + ODataPathSegment currentSegment = path.ElementAt(currentSegmentIndex); - if (currentSegmentIndex < 0) - { - return null; - } - - ODataPathSegment currentSegment = path.ElementAt(currentSegmentIndex); + if (currentSegment is SingletonSegment || currentSegment is KeySegment) + { + return (IEdmEntityType)path.GetEdmType(); + } - if (currentSegment is SingletonSegment || currentSegment is KeySegment) + NavigationPropertySegment navigationPropertySegment = currentSegment as NavigationPropertySegment; + if (navigationPropertySegment != null) + { + if (navigationPropertySegment.NavigationProperty.TargetMultiplicity() == EdmMultiplicity.ZeroOrOne || + navigationPropertySegment.NavigationProperty.TargetMultiplicity() == EdmMultiplicity.One) { return (IEdmEntityType)path.GetEdmType(); } - - NavigationPropertySegment navigationPropertySegment = currentSegment as NavigationPropertySegment; - if (navigationPropertySegment != null) - { - if (navigationPropertySegment.NavigationProperty.TargetMultiplicity() == EdmMultiplicity.ZeroOrOne || - navigationPropertySegment.NavigationProperty.TargetMultiplicity() == EdmMultiplicity.One) - { - return (IEdmEntityType)path.GetEdmType(); - } - } - - return null; } + + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Abstracts/HttpRequestScope.cs b/src/Microsoft.AspNetCore.OData/Abstracts/HttpRequestScope.cs index 0b3ae6a1c..703657c00 100644 --- a/src/Microsoft.AspNetCore.OData/Abstracts/HttpRequestScope.cs +++ b/src/Microsoft.AspNetCore.OData/Abstracts/HttpRequestScope.cs @@ -7,18 +7,17 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.OData.Abstracts +namespace Microsoft.AspNetCore.OData.Abstracts; + +/// +/// Provides access to the +/// to which the OData service container instance is scoped. +/// +public class HttpRequestScope { /// /// Provides access to the /// to which the OData service container instance is scoped. /// - public class HttpRequestScope - { - /// - /// Provides access to the - /// to which the OData service container instance is scoped. - /// - public HttpRequest HttpRequest { get; set; } - } + public HttpRequest HttpRequest { get; set; } } diff --git a/src/Microsoft.AspNetCore.OData/Abstracts/IETagHandler.cs b/src/Microsoft.AspNetCore.OData/Abstracts/IETagHandler.cs index d0b723114..6016ad792 100644 --- a/src/Microsoft.AspNetCore.OData/Abstracts/IETagHandler.cs +++ b/src/Microsoft.AspNetCore.OData/Abstracts/IETagHandler.cs @@ -9,27 +9,26 @@ using System.Collections.Generic; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.OData.Abstracts +namespace Microsoft.AspNetCore.OData.Abstracts; + +/// +/// Exposes the ability to convert a collection of concurrency property names and values into an +/// and parse an into a list of concurrency property values. +/// +public interface IETagHandler { /// - /// Exposes the ability to convert a collection of concurrency property names and values into an - /// and parse an into a list of concurrency property values. + /// Creates an ETag from concurrency property names and values. /// - public interface IETagHandler - { - /// - /// Creates an ETag from concurrency property names and values. - /// - /// The input property names and values. - /// The timezone info. - /// The generated ETag string. - EntityTagHeaderValue CreateETag(IDictionary properties, TimeZoneInfo timeZoneInfo = null); + /// The input property names and values. + /// The timezone info. + /// The generated ETag string. + EntityTagHeaderValue CreateETag(IDictionary properties, TimeZoneInfo timeZoneInfo = null); - /// - /// Parses an ETag header value into concurrency property names and values. - /// - /// The ETag header value. - /// Concurrency property names and values. - IDictionary ParseETag(EntityTagHeaderValue etagHeaderValue); - } + /// + /// Parses an ETag header value into concurrency property names and values. + /// + /// The ETag header value. + /// Concurrency property names and values. + IDictionary ParseETag(EntityTagHeaderValue etagHeaderValue); } diff --git a/src/Microsoft.AspNetCore.OData/Abstracts/IODataBatchFeature.cs b/src/Microsoft.AspNetCore.OData/Abstracts/IODataBatchFeature.cs index e117ac85f..8ec25844d 100644 --- a/src/Microsoft.AspNetCore.OData/Abstracts/IODataBatchFeature.cs +++ b/src/Microsoft.AspNetCore.OData/Abstracts/IODataBatchFeature.cs @@ -8,31 +8,30 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.Abstracts +namespace Microsoft.AspNetCore.OData.Abstracts; + +/// +/// Provide the interface for the details of a given OData batch request. +/// +public interface IODataBatchFeature { /// - /// Provide the interface for the details of a given OData batch request. + /// Gets or sets the batch id. /// - public interface IODataBatchFeature - { - /// - /// Gets or sets the batch id. - /// - Guid? BatchId { get; set; } + Guid? BatchId { get; set; } - /// - /// Gets or sets the change set id. - /// - Guid? ChangeSetId { get; set; } + /// + /// Gets or sets the change set id. + /// + Guid? ChangeSetId { get; set; } - /// - /// Gets or sets the content id. - /// - string ContentId { get; set; } + /// + /// Gets or sets the content id. + /// + string ContentId { get; set; } - /// - /// Gets or sets the content id mapping. - /// - IDictionary ContentIdMapping { get; } - } + /// + /// Gets or sets the content id mapping. + /// + IDictionary ContentIdMapping { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Abstracts/IODataFeature.cs b/src/Microsoft.AspNetCore.OData/Abstracts/IODataFeature.cs index ed9fd8f3d..8d970099d 100644 --- a/src/Microsoft.AspNetCore.OData/Abstracts/IODataFeature.cs +++ b/src/Microsoft.AspNetCore.OData/Abstracts/IODataFeature.cs @@ -14,89 +14,88 @@ using Microsoft.OData.UriParser; using Microsoft.OData.UriParser.Aggregation; -namespace Microsoft.AspNetCore.OData.Abstracts +namespace Microsoft.AspNetCore.OData.Abstracts; + +/// +/// Provide the interface for the details of a given OData request. +/// +public interface IODataFeature { + + /// + /// Gets or sets the OData model. + /// + IEdmModel Model { get; set; } + + /// + /// Gets or sets the OData path. + /// + ODataPath Path { get; set; } + + /// + /// Gets/sets the route prefix name + /// + string RoutePrefix { get; set; } + + /// + /// Gets/sets the endpoint selected + /// + EndPoint Endpoint { get; set; } + + /// + /// Gets or sets the OData base address. + /// + string BaseAddress { get; set; } + + /// + /// Gets or sets the request scope. + /// + IServiceScope RequestScope { get; set; } + + /// + /// Gets or sets the request container. + /// + IServiceProvider Services { get; set; } + + /// + /// Gets or sets the batch route data. + /// + RouteValueDictionary BatchRouteData { get; } + + /// + /// Gets or sets the total count for the OData response. + /// + /// null if no count should be sent back to the client. + long? TotalCount { get; set; } + + /// + /// Gets or sets the total count function for the OData response. + /// + Func TotalCountFunc { get; set; } + + /// + /// Gets or sets the parsed OData of the request. + /// + ApplyClause ApplyClause { get; set; } + + /// + /// Gets or sets the parsed OData of the request. + /// + SelectExpandClause SelectExpandClause { get; set; } + + /// + /// Gets or sets the next link for the OData response. + /// + Uri NextLink { get; set; } + + /// + /// Gets or sets the delta link for the OData response. + /// + Uri DeltaLink { get; set; } + /// - /// Provide the interface for the details of a given OData request. + /// Gets the data store used by routing conventions to store any custom route data. /// - public interface IODataFeature - { - - /// - /// Gets or sets the OData model. - /// - IEdmModel Model { get; set; } - - /// - /// Gets or sets the OData path. - /// - ODataPath Path { get; set; } - - /// - /// Gets/sets the route prefix name - /// - string RoutePrefix { get; set; } - - /// - /// Gets/sets the endpoint selected - /// - EndPoint Endpoint { get; set; } - - /// - /// Gets or sets the OData base address. - /// - string BaseAddress { get; set; } - - /// - /// Gets or sets the request scope. - /// - IServiceScope RequestScope { get; set; } - - /// - /// Gets or sets the request container. - /// - IServiceProvider Services { get; set; } - - /// - /// Gets or sets the batch route data. - /// - RouteValueDictionary BatchRouteData { get; } - - /// - /// Gets or sets the total count for the OData response. - /// - /// null if no count should be sent back to the client. - long? TotalCount { get; set; } - - /// - /// Gets or sets the total count function for the OData response. - /// - Func TotalCountFunc { get; set; } - - /// - /// Gets or sets the parsed OData of the request. - /// - ApplyClause ApplyClause { get; set; } - - /// - /// Gets or sets the parsed OData of the request. - /// - SelectExpandClause SelectExpandClause { get; set; } - - /// - /// Gets or sets the next link for the OData response. - /// - Uri NextLink { get; set; } - - /// - /// Gets or sets the delta link for the OData response. - /// - Uri DeltaLink { get; set; } - - /// - /// Gets the data store used by routing conventions to store any custom route data. - /// - /// Initially an empty IDictionary<string, object>. - IDictionary RoutingConventionsStore { get; } - } + /// Initially an empty IDictionary<string, object>. + IDictionary RoutingConventionsStore { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Abstracts/ODataBatchFeature.cs b/src/Microsoft.AspNetCore.OData/Abstracts/ODataBatchFeature.cs index 09d2de183..e52a22058 100644 --- a/src/Microsoft.AspNetCore.OData/Abstracts/ODataBatchFeature.cs +++ b/src/Microsoft.AspNetCore.OData/Abstracts/ODataBatchFeature.cs @@ -8,31 +8,30 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.Abstracts +namespace Microsoft.AspNetCore.OData.Abstracts; + +/// +/// Provide the interface for the details of a given OData batch request. +/// +public class ODataBatchFeature : IODataBatchFeature { /// - /// Provide the interface for the details of a given OData batch request. + /// Gets or sets the batch id. /// - public class ODataBatchFeature : IODataBatchFeature - { - /// - /// Gets or sets the batch id. - /// - public Guid? BatchId { get; set; } + public Guid? BatchId { get; set; } - /// - /// Gets or sets the change set id. - /// - public Guid? ChangeSetId { get; set; } + /// + /// Gets or sets the change set id. + /// + public Guid? ChangeSetId { get; set; } - /// - /// Gets or sets the content id. - /// - public string ContentId { get; set; } + /// + /// Gets or sets the content id. + /// + public string ContentId { get; set; } - /// - /// Gets or sets the content id mapping. - /// - public IDictionary ContentIdMapping { get; } = new Dictionary(); - } + /// + /// Gets or sets the content id mapping. + /// + public IDictionary ContentIdMapping { get; } = new Dictionary(); } diff --git a/src/Microsoft.AspNetCore.OData/Abstracts/ODataFeature.cs b/src/Microsoft.AspNetCore.OData/Abstracts/ODataFeature.cs index 7c0c7fb92..6c2d0f84a 100644 --- a/src/Microsoft.AspNetCore.OData/Abstracts/ODataFeature.cs +++ b/src/Microsoft.AspNetCore.OData/Abstracts/ODataFeature.cs @@ -16,142 +16,141 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.OData.Abstracts +namespace Microsoft.AspNetCore.OData.Abstracts; + +/// +/// Contains the details of a given OData request. These properties should all be mutable. +/// None of these properties should ever be set to null. +/// +public class ODataFeature : IODataFeature { + internal const string ODataServiceVersionHeader = "OData-Version"; + internal const ODataVersion DefaultODataVersion = ODataVersion.V4; + + private long? _totalCount; + private bool _totalCountSet; + /// - /// Contains the details of a given OData request. These properties should all be mutable. - /// None of these properties should ever be set to null. + /// Instantiates a new instance of the class. /// - public class ODataFeature : IODataFeature + public ODataFeature() { - internal const string ODataServiceVersionHeader = "OData-Version"; - internal const ODataVersion DefaultODataVersion = ODataVersion.V4; + _totalCountSet = false; + } - private long? _totalCount; - private bool _totalCountSet; + /// + /// Gets or sets the OData path. + /// + public IEdmModel Model { get; set; } - /// - /// Instantiates a new instance of the class. - /// - public ODataFeature() - { - _totalCountSet = false; - } + /// + /// Gets or sets the OData path. + /// + public ODataPath Path { get; set; } + + /// + /// Add a boolean value indicate whether it's endpoint routing or not. + /// Maybe it's unnecessary later. + /// + public EndPoint Endpoint { get; set; } + + /// + /// Gets or sets the route prefix name. + /// + public string RoutePrefix { get; set; } - /// - /// Gets or sets the OData path. - /// - public IEdmModel Model { get; set; } - - /// - /// Gets or sets the OData path. - /// - public ODataPath Path { get; set; } - - /// - /// Add a boolean value indicate whether it's endpoint routing or not. - /// Maybe it's unnecessary later. - /// - public EndPoint Endpoint { get; set; } - - /// - /// Gets or sets the route prefix name. - /// - public string RoutePrefix { get; set; } - - /// - /// Gets or sets the OData base address. - /// - public string BaseAddress { get; set; } - - /// - /// Gets or sets the request scope. - /// - public IServiceScope RequestScope { get; set; } - - /// - /// Gets or sets the request container. - /// - public IServiceProvider Services { get; set; } - - /// - /// Gets or sets the batch route data. - /// - public RouteValueDictionary BatchRouteData { get; } = new RouteValueDictionary(); - - /// - /// Gets or sets the total count for the OData response. - /// - /// null if no count should be sent back to the client. - public long? TotalCount + /// + /// Gets or sets the OData base address. + /// + public string BaseAddress { get; set; } + + /// + /// Gets or sets the request scope. + /// + public IServiceScope RequestScope { get; set; } + + /// + /// Gets or sets the request container. + /// + public IServiceProvider Services { get; set; } + + /// + /// Gets or sets the batch route data. + /// + public RouteValueDictionary BatchRouteData { get; } = new RouteValueDictionary(); + + /// + /// Gets or sets the total count for the OData response. + /// + /// null if no count should be sent back to the client. + public long? TotalCount + { + get { - get + if (_totalCountSet) { - if (_totalCountSet) - { - return _totalCount; - } - - if (this.TotalCountFunc != null) - { - _totalCount = this.TotalCountFunc(); - _totalCountSet = true; - return _totalCount; - } - - return null; + return _totalCount; } - set + + if (this.TotalCountFunc != null) { - _totalCount = value; - _totalCountSet = value.HasValue; + _totalCount = this.TotalCountFunc(); + _totalCountSet = true; + return _totalCount; } - } - /// - /// Gets or sets the total count function for the OData response. - /// - public Func TotalCountFunc { get; set; } - - /// - /// Gets or sets the parsed OData of the request. - /// - public ApplyClause ApplyClause { get; set; } - - /// - /// Gets or sets the parsed OData of the request. - /// - public SelectExpandClause SelectExpandClause { get; set; } - - /// - /// Gets or sets the next link for the OData response. - /// - public Uri NextLink { get; set; } - - /// - /// Gets or sets the delta link for the OData response. - /// - public Uri DeltaLink { get; set; } - - /// - /// Gets or sets the parsed of the request. - /// - internal ODataQueryOptions QueryOptions { get; set; } - - /// - /// Page size to be used by skiptoken implementation for the top-level resource for the request. - /// - internal int PageSize { get; set; } - - /// - /// Gets the body values from OData request. - /// - internal IDictionary BodyValues { get; set; } - - /// - /// Gets the data store used routing conventions to store any custom route data. - /// - /// Initially an empty IDictionary<string, object>. - public IDictionary RoutingConventionsStore { get; } = new Dictionary(); + return null; + } + set + { + _totalCount = value; + _totalCountSet = value.HasValue; + } } + + /// + /// Gets or sets the total count function for the OData response. + /// + public Func TotalCountFunc { get; set; } + + /// + /// Gets or sets the parsed OData of the request. + /// + public ApplyClause ApplyClause { get; set; } + + /// + /// Gets or sets the parsed OData of the request. + /// + public SelectExpandClause SelectExpandClause { get; set; } + + /// + /// Gets or sets the next link for the OData response. + /// + public Uri NextLink { get; set; } + + /// + /// Gets or sets the delta link for the OData response. + /// + public Uri DeltaLink { get; set; } + + /// + /// Gets or sets the parsed of the request. + /// + internal ODataQueryOptions QueryOptions { get; set; } + + /// + /// Page size to be used by skiptoken implementation for the top-level resource for the request. + /// + internal int PageSize { get; set; } + + /// + /// Gets the body values from OData request. + /// + internal IDictionary BodyValues { get; set; } + + /// + /// Gets the data store used routing conventions to store any custom route data. + /// + /// Initially an empty IDictionary<string, object>. + public IDictionary RoutingConventionsStore { get; } = new Dictionary(); } diff --git a/src/Microsoft.AspNetCore.OData/Abstracts/ODataServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.OData/Abstracts/ODataServiceCollectionExtensions.cs index 8a4e81099..35b6b495a 100644 --- a/src/Microsoft.AspNetCore.OData/Abstracts/ODataServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Abstracts/ODataServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -15,103 +15,102 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Abstracts +namespace Microsoft.AspNetCore.OData.Abstracts; + +internal static class ODataServiceCollectionExtensions { - internal static class ODataServiceCollectionExtensions + /// + /// Injects the default Web API OData services. + /// + /// The service collection. + /// The calling itself. + public static IServiceCollection AddDefaultWebApiServices(this IServiceCollection services) { - /// - /// Injects the default Web API OData services. - /// - /// The service collection. - /// The calling itself. - public static IServiceCollection AddDefaultWebApiServices(this IServiceCollection services) + if (services == null) { - if (services == null) - { - throw Error.ArgumentNull(nameof(services)); - } + throw Error.ArgumentNull(nameof(services)); + } - services.AddSingleton(); + services.AddSingleton(); - //builder.AddService(ServiceLifetime.Singleton); + //builder.AddService(ServiceLifetime.Singleton); - // ReaderSettings and WriterSettings are registered as prototype services. - // There will be a copy (if it is accessed) of each prototype for each request. + // ReaderSettings and WriterSettings are registered as prototype services. + // There will be a copy (if it is accessed) of each prototype for each request. #pragma warning disable CS0618 // ReadUntypedAsString is obsolete in ODL 8. - services.AddSingleton(new ODataMessageReaderSettings - { - EnableMessageStreamDisposal = false, - MessageQuotas = new ODataMessageQuotas { MaxReceivedMessageSize = Int64.MaxValue }, - - // WebAPI should read untyped values as structural values by setting ReadUntypedAsString=false. - // In ODL 8.x, ReadUntypedAsString option will be deleted. - ReadUntypedAsString = false, - - // Enable read property name case-insensitive from payload. - EnablePropertyNameCaseInsensitive = true, - EnableReadingODataAnnotationWithoutPrefix = true - }); + services.AddSingleton(new ODataMessageReaderSettings + { + EnableMessageStreamDisposal = false, + MessageQuotas = new ODataMessageQuotas { MaxReceivedMessageSize = Int64.MaxValue }, + + // WebAPI should read untyped values as structural values by setting ReadUntypedAsString=false. + // In ODL 8.x, ReadUntypedAsString option will be deleted. + ReadUntypedAsString = false, + + // Enable read property name case-insensitive from payload. + EnablePropertyNameCaseInsensitive = true, + EnableReadingODataAnnotationWithoutPrefix = true + }); #pragma warning restore CS0618 // Type or member is obsolete - services.AddSingleton(new ODataMessageWriterSettings - { - EnableMessageStreamDisposal = false, - MessageQuotas = new ODataMessageQuotas { MaxReceivedMessageSize = Int64.MaxValue }, - }); - - // QueryValidators. - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - - // SerializerProvider and DeserializerProvider. - services.AddSingleton(); - services.AddSingleton(); - - // Deserializers. - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - // Serializers. - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - // Binders. - services.AddScoped(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - // HttpRequestScope. - services.AddScoped(); - services.AddScoped(sp => sp.GetRequiredService().HttpRequest); - return services; - } + services.AddSingleton(new ODataMessageWriterSettings + { + EnableMessageStreamDisposal = false, + MessageQuotas = new ODataMessageQuotas { MaxReceivedMessageSize = Int64.MaxValue }, + }); + + // QueryValidators. + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + + // SerializerProvider and DeserializerProvider. + services.AddSingleton(); + services.AddSingleton(); + + // Deserializers. + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Serializers. + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Binders. + services.AddScoped(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // HttpRequestScope. + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService().HttpRequest); + return services; } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/ChangeSetRequestItem.cs b/src/Microsoft.AspNetCore.OData/Batch/ChangeSetRequestItem.cs index cf49b1841..8590837e9 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/ChangeSetRequestItem.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/ChangeSetRequestItem.cs @@ -11,61 +11,60 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OData.Extensions; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// Represents a ChangeSet request. +/// +public class ChangeSetRequestItem : ODataBatchRequestItem { /// - /// Represents a ChangeSet request. + /// Initializes a new instance of the class. + /// + /// The request contexts in the ChangeSet. + public ChangeSetRequestItem(IEnumerable contexts) + { + Contexts = contexts ?? throw new ArgumentNullException(nameof(contexts)); + } + + /// + /// Gets the request contexts in the ChangeSet. + /// + public IEnumerable Contexts { get; } + + /// + /// Sends the ChangeSet request to the request delegate. /// - public class ChangeSetRequestItem : ODataBatchRequestItem + /// The handler for processing a message. + /// A . + public override async Task SendRequestAsync(RequestDelegate handler) { - /// - /// Initializes a new instance of the class. - /// - /// The request contexts in the ChangeSet. - public ChangeSetRequestItem(IEnumerable contexts) + if (handler == null) { - Contexts = contexts ?? throw new ArgumentNullException(nameof(contexts)); + throw new ArgumentNullException(nameof(handler)); } - /// - /// Gets the request contexts in the ChangeSet. - /// - public IEnumerable Contexts { get; } + List responseContexts = new List(); - /// - /// Sends the ChangeSet request to the request delegate. - /// - /// The handler for processing a message. - /// A . - public override async Task SendRequestAsync(RequestDelegate handler) + IDictionary contentIdToLocationMapping = this.ContentIdToLocationMapping ?? new Dictionary(); + + foreach (HttpContext context in Contexts) { - if (handler == null) + await SendRequestAsync(handler, context, contentIdToLocationMapping).ConfigureAwait(false); + + HttpResponse response = context.Response; + if (response.IsSuccessStatusCode()) { - throw new ArgumentNullException(nameof(handler)); + responseContexts.Add(context); } - - List responseContexts = new List(); - - IDictionary contentIdToLocationMapping = this.ContentIdToLocationMapping ?? new Dictionary(); - - foreach (HttpContext context in Contexts) + else { - await SendRequestAsync(handler, context, contentIdToLocationMapping).ConfigureAwait(false); - - HttpResponse response = context.Response; - if (response.IsSuccessStatusCode()) - { - responseContexts.Add(context); - } - else - { - responseContexts.Clear(); - responseContexts.Add(context); - return new ChangeSetResponseItem(responseContexts); - } + responseContexts.Clear(); + responseContexts.Add(context); + return new ChangeSetResponseItem(responseContexts); } - - return new ChangeSetResponseItem(responseContexts); } + + return new ChangeSetResponseItem(responseContexts); } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/ChangeSetResponseItem.cs b/src/Microsoft.AspNetCore.OData/Batch/ChangeSetResponseItem.cs index 16757bfa9..58a1725e2 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/ChangeSetResponseItem.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/ChangeSetResponseItem.cs @@ -13,54 +13,53 @@ using Microsoft.AspNetCore.OData.Extensions; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// Represents a ChangeSet response. +/// +public class ChangeSetResponseItem : ODataBatchResponseItem { /// - /// Represents a ChangeSet response. + /// Initializes a new instance of the class. /// - public class ChangeSetResponseItem : ODataBatchResponseItem + /// The response contexts for the ChangeSet requests. + public ChangeSetResponseItem(IEnumerable contexts) { - /// - /// Initializes a new instance of the class. - /// - /// The response contexts for the ChangeSet requests. - public ChangeSetResponseItem(IEnumerable contexts) - { - Contexts = contexts ?? throw new ArgumentNullException(nameof(contexts)); - } + Contexts = contexts ?? throw new ArgumentNullException(nameof(contexts)); + } - /// - /// Gets the response contexts for the ChangeSet. - /// - public IEnumerable Contexts { get; } + /// + /// Gets the response contexts for the ChangeSet. + /// + public IEnumerable Contexts { get; } - /// - /// Writes the responses as a ChangeSet. - /// - /// The . - public override async Task WriteResponseAsync(ODataBatchWriter writer) + /// + /// Writes the responses as a ChangeSet. + /// + /// The . + public override async Task WriteResponseAsync(ODataBatchWriter writer) + { + if (writer == null) { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - await writer.WriteStartChangesetAsync().ConfigureAwait(false); - - foreach (HttpContext context in Contexts) - { - await WriteMessageAsync(writer, context).ConfigureAwait(false); - } - - await writer.WriteEndChangesetAsync().ConfigureAwait(false); + throw new ArgumentNullException(nameof(writer)); } - /// - /// Gets a value that indicates if the responses in this item are successful. - /// - internal override bool IsResponseSuccessful() + await writer.WriteStartChangesetAsync().ConfigureAwait(false); + + foreach (HttpContext context in Contexts) { - return Contexts.All(c => c.Response.IsSuccessStatusCode()); + await WriteMessageAsync(writer, context).ConfigureAwait(false); } + + await writer.WriteEndChangesetAsync().ConfigureAwait(false); + } + + /// + /// Gets a value that indicates if the responses in this item are successful. + /// + internal override bool IsResponseSuccessful() + { + return Contexts.All(c => c.Response.IsSuccessStatusCode()); } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/DefaultODataBatchHandler.cs b/src/Microsoft.AspNetCore.OData/Batch/DefaultODataBatchHandler.cs index 077c30647..9efc441d8 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/DefaultODataBatchHandler.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/DefaultODataBatchHandler.cs @@ -15,128 +15,127 @@ using Microsoft.Extensions.Options; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// Default implementation of for handling OData batch request. +/// By default, it buffers the request content stream. +/// +public class DefaultODataBatchHandler : ODataBatchHandler { - /// - /// Default implementation of for handling OData batch request. - /// By default, it buffers the request content stream. - /// - public class DefaultODataBatchHandler : ODataBatchHandler + /// + public override async Task ProcessBatchAsync(HttpContext context, RequestDelegate nextHandler) { - /// - public override async Task ProcessBatchAsync(HttpContext context, RequestDelegate nextHandler) + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - if (nextHandler == null) - { - throw Error.ArgumentNull(nameof(nextHandler)); - } + if (nextHandler == null) + { + throw Error.ArgumentNull(nameof(nextHandler)); + } - if (!await ValidateRequest(context.Request).ConfigureAwait(false)) - { - return; - } + if (!await ValidateRequest(context.Request).ConfigureAwait(false)) + { + return; + } + + IList subRequests = await ParseBatchRequestsAsync(context).ConfigureAwait(false); - IList subRequests = await ParseBatchRequestsAsync(context).ConfigureAwait(false); + ODataOptions options = context.RequestServices.GetRequiredService>().Value; + bool enableContinueOnErrorHeader = (options != null) ? options.EnableContinueOnErrorHeader : false; - ODataOptions options = context.RequestServices.GetRequiredService>().Value; - bool enableContinueOnErrorHeader = (options != null) ? options.EnableContinueOnErrorHeader : false; + SetContinueOnError(context.Request.Headers, enableContinueOnErrorHeader); - SetContinueOnError(context.Request.Headers, enableContinueOnErrorHeader); + IList responses = await ExecuteRequestMessagesAsync(subRequests, nextHandler).ConfigureAwait(false); - IList responses = await ExecuteRequestMessagesAsync(subRequests, nextHandler).ConfigureAwait(false); + await CreateResponseMessageAsync(responses, context.Request).ConfigureAwait(false); + } - await CreateResponseMessageAsync(responses, context.Request).ConfigureAwait(false); + /// + /// Executes the OData batch requests. + /// + /// The collection of OData batch requests. + /// The handler for processing a message. + /// A collection of for the batch requests. + public virtual async Task> ExecuteRequestMessagesAsync(IEnumerable requests, RequestDelegate handler) + { + if (requests == null) + { + throw Error.ArgumentNull(nameof(requests)); } - /// - /// Executes the OData batch requests. - /// - /// The collection of OData batch requests. - /// The handler for processing a message. - /// A collection of for the batch requests. - public virtual async Task> ExecuteRequestMessagesAsync(IEnumerable requests, RequestDelegate handler) + if (handler == null) { - if (requests == null) - { - throw Error.ArgumentNull(nameof(requests)); - } + throw Error.ArgumentNull(nameof(handler)); + } - if (handler == null) - { - throw Error.ArgumentNull(nameof(handler)); - } + IList responses = new List(); - IList responses = new List(); + foreach (ODataBatchRequestItem request in requests) + { + ODataBatchResponseItem responseItem = await request.SendRequestAsync(handler).ConfigureAwait(false); + responses.Add(responseItem); - foreach (ODataBatchRequestItem request in requests) + if (responseItem != null && responseItem.IsResponseSuccessful() == false && ContinueOnError == false) { - ODataBatchResponseItem responseItem = await request.SendRequestAsync(handler).ConfigureAwait(false); - responses.Add(responseItem); - - if (responseItem != null && responseItem.IsResponseSuccessful() == false && ContinueOnError == false) - { - break; - } + break; } - - return responses; } - /// - /// Converts the incoming OData batch request into a collection of request messages. - /// - /// The context containing the batch request messages. - /// A collection of . - public virtual async Task> ParseBatchRequestsAsync(HttpContext context) + return responses; + } + + /// + /// Converts the incoming OData batch request into a collection of request messages. + /// + /// The context containing the batch request messages. + /// A collection of . + public virtual async Task> ParseBatchRequestsAsync(HttpContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - HttpRequest request = context.Request; - IServiceProvider requestContainer = request.CreateRouteServices(PrefixName); - requestContainer.GetRequiredService().BaseUri = GetBaseUri(request); + HttpRequest request = context.Request; + IServiceProvider requestContainer = request.CreateRouteServices(PrefixName); + requestContainer.GetRequiredService().BaseUri = GetBaseUri(request); - using (ODataMessageReader reader = request.GetODataMessageReader(requestContainer)) - { - CancellationToken cancellationToken = context.RequestAborted; - List requests = new List(); - ODataBatchReader batchReader = await reader.CreateODataBatchReaderAsync().ConfigureAwait(false); - Guid batchId = Guid.NewGuid(); - Dictionary contentToLocationMapping = new Dictionary(); + using (ODataMessageReader reader = request.GetODataMessageReader(requestContainer)) + { + CancellationToken cancellationToken = context.RequestAborted; + List requests = new List(); + ODataBatchReader batchReader = await reader.CreateODataBatchReaderAsync().ConfigureAwait(false); + Guid batchId = Guid.NewGuid(); + Dictionary contentToLocationMapping = new Dictionary(); - while (await batchReader.ReadAsync().ConfigureAwait(false)) + while (await batchReader.ReadAsync().ConfigureAwait(false)) + { + if (batchReader.State == ODataBatchReaderState.ChangesetStart) { - if (batchReader.State == ODataBatchReaderState.ChangesetStart) - { - IList changeSetContexts = await batchReader.ReadChangeSetRequestAsync(context, batchId, cancellationToken).ConfigureAwait(false); - foreach (HttpContext changeSetContext in changeSetContexts) - { - changeSetContext.Request.ClearRouteServices(); - } - - ChangeSetRequestItem requestItem = new ChangeSetRequestItem(changeSetContexts); - requestItem.ContentIdToLocationMapping = contentToLocationMapping; - requests.Add(requestItem); - } - else if (batchReader.State == ODataBatchReaderState.Operation) + IList changeSetContexts = await batchReader.ReadChangeSetRequestAsync(context, batchId, cancellationToken).ConfigureAwait(false); + foreach (HttpContext changeSetContext in changeSetContexts) { - HttpContext operationContext = await batchReader.ReadOperationRequestAsync(context, batchId, cancellationToken).ConfigureAwait(false); - operationContext.Request.ClearRouteServices(); - OperationRequestItem requestItem = new OperationRequestItem(operationContext); - requestItem.ContentIdToLocationMapping = contentToLocationMapping; - requests.Add(requestItem); + changeSetContext.Request.ClearRouteServices(); } - } - return requests; + ChangeSetRequestItem requestItem = new ChangeSetRequestItem(changeSetContexts); + requestItem.ContentIdToLocationMapping = contentToLocationMapping; + requests.Add(requestItem); + } + else if (batchReader.State == ODataBatchReaderState.Operation) + { + HttpContext operationContext = await batchReader.ReadOperationRequestAsync(context, batchId, cancellationToken).ConfigureAwait(false); + operationContext.Request.ClearRouteServices(); + OperationRequestItem requestItem = new OperationRequestItem(operationContext); + requestItem.ContentIdToLocationMapping = contentToLocationMapping; + requests.Add(requestItem); + } } + + return requests; } } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/HttpRequestExtensions.cs b/src/Microsoft.AspNetCore.OData/Batch/HttpRequestExtensions.cs index 969340321..1ee7d40ae 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/HttpRequestExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/HttpRequestExtensions.cs @@ -13,63 +13,62 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// Provides extension methods for the class. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class HttpRequestExtensions { /// - /// Provides extension methods for the class. + /// Gets the for the stream. /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static class HttpRequestExtensions + /// The request. + /// The dependency injection container for the request. + /// A task object that produces an when completed. + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Creates a message reader that the user manages.")] + public static ODataMessageReader GetODataMessageReader(this HttpRequest request, IServiceProvider requestContainer) { - /// - /// Gets the for the stream. - /// - /// The request. - /// The dependency injection container for the request. - /// A task object that produces an when completed. - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Creates a message reader that the user manages.")] - public static ODataMessageReader GetODataMessageReader(this HttpRequest request, IServiceProvider requestContainer) + if (request == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - // how to dispose it? - IODataRequestMessage oDataRequestMessage = ODataMessageWrapperHelper.Create(request.Body, request.Headers, requestContainer); - ODataMessageReaderSettings settings = requestContainer.GetRequiredService(); - ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, settings); - return oDataMessageReader; + throw new ArgumentNullException(nameof(request)); } - /// - /// Copy an absolute Uri to a stream. - /// - /// The request. - /// The absolute uri to copy. - public static void CopyAbsoluteUrl(this HttpRequest request, Uri uri) + // how to dispose it? + IODataRequestMessage oDataRequestMessage = ODataMessageWrapperHelper.Create(request.Body, request.Headers, requestContainer); + ODataMessageReaderSettings settings = requestContainer.GetRequiredService(); + ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, settings); + return oDataMessageReader; + } + + /// + /// Copy an absolute Uri to a stream. + /// + /// The request. + /// The absolute uri to copy. + public static void CopyAbsoluteUrl(this HttpRequest request, Uri uri) + { + if (request == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + throw new ArgumentNullException(nameof(request)); + } - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } - request.Scheme = uri.Scheme; - request.Host = uri.IsDefaultPort ? - new HostString(uri.Host) : - new HostString(uri.Host, uri.Port); - request.QueryString = new QueryString(uri.Query); - var path = new PathString(uri.AbsolutePath); - if (path.StartsWithSegments(request.PathBase, out PathString remainingPath)) - { - path = remainingPath; - } - request.Path = path; + request.Scheme = uri.Scheme; + request.Host = uri.IsDefaultPort ? + new HostString(uri.Host) : + new HostString(uri.Host, uri.Port); + request.QueryString = new QueryString(uri.Query); + var path = new PathString(uri.AbsolutePath); + if (path.StartsWithSegments(request.PathBase, out PathString remainingPath)) + { + path = remainingPath; } + request.Path = path; } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchContent.cs b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchContent.cs index c00ea9d86..50cd59f2e 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchContent.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchContent.cs @@ -17,97 +17,96 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// Encapsulates a collection of OData batch responses. +/// +public class ODataBatchContent { + private IServiceProvider _requestContainer; + private ODataMessageWriterSettings _writerSettings; + /// - /// Encapsulates a collection of OData batch responses. + /// Initializes a new instance of the class. /// - public class ODataBatchContent + /// The batch responses. + /// The dependency injection container for the request. + public ODataBatchContent(IEnumerable responses, IServiceProvider requestContainer) + : this(responses, requestContainer, null/*contentType*/) { - private IServiceProvider _requestContainer; - private ODataMessageWriterSettings _writerSettings; - - /// - /// Initializes a new instance of the class. - /// - /// The batch responses. - /// The dependency injection container for the request. - public ODataBatchContent(IEnumerable responses, IServiceProvider requestContainer) - : this(responses, requestContainer, null/*contentType*/) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The batch responses. - /// The dependency injection container for the request. - /// The response content type. - public ODataBatchContent(IEnumerable responses, IServiceProvider requestContainer, string contentType) - { - Responses = responses ?? throw Error.ArgumentNull(nameof(responses)); - - _requestContainer = requestContainer; - _writerSettings = requestContainer.GetService() ?? new ODataMessageWriterSettings(); - - // Set the Content-Type header for existing batch formats - if (contentType == null) - { - contentType = string.Format( - CultureInfo.InvariantCulture, "multipart/mixed;boundary=batchresponse_{0}", Guid.NewGuid()); - } - - Headers = new HeaderDictionary(); - Headers["Content-Type"] = contentType; - ODataVersion version = _writerSettings.Version ?? ODataVersionConstraint.DefaultODataVersion; - Headers.Append(ODataVersionConstraint.ODataServiceVersionHeader, ODataUtils.ODataVersionToString(version)); - } + /// + /// Initializes a new instance of the class. + /// + /// The batch responses. + /// The dependency injection container for the request. + /// The response content type. + public ODataBatchContent(IEnumerable responses, IServiceProvider requestContainer, string contentType) + { + Responses = responses ?? throw Error.ArgumentNull(nameof(responses)); - /// - /// Gets the batch responses. - /// - public IEnumerable Responses { get; } - - /// - /// Gets the Headers for the batch content. - /// - public IHeaderDictionary Headers { get; } - - /// - /// Serialize the batch content to a stream. - /// - /// The stream to serialize to. - /// A that can be awaited. - /// This function uses types that are AspNetCore-specific. - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "")] - public Task SerializeToStreamAsync(Stream stream) + _requestContainer = requestContainer; + _writerSettings = requestContainer.GetService() ?? new ODataMessageWriterSettings(); + + // Set the Content-Type header for existing batch formats + if (contentType == null) { - // how to dispose it? - IODataResponseMessage responseMessage = ODataMessageWrapperHelper.Create(stream, Headers, _requestContainer); - return WriteToResponseMessageAsync(responseMessage); + contentType = string.Format( + CultureInfo.InvariantCulture, "multipart/mixed;boundary=batchresponse_{0}", Guid.NewGuid()); } - /// - /// Serialize the batch responses to an . - /// - /// The response message. - /// - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "")] - private async Task WriteToResponseMessageAsync(IODataResponseMessage responseMessage) - { - // how to dispose it? - ODataMessageWriter messageWriter = new ODataMessageWriter(responseMessage, _writerSettings); + Headers = new HeaderDictionary(); + Headers["Content-Type"] = contentType; + ODataVersion version = _writerSettings.Version ?? ODataVersionConstraint.DefaultODataVersion; + Headers.Append(ODataVersionConstraint.ODataServiceVersionHeader, ODataUtils.ODataVersionToString(version)); + } - ODataBatchWriter writer = await messageWriter.CreateODataBatchWriterAsync().ConfigureAwait(false); + /// + /// Gets the batch responses. + /// + public IEnumerable Responses { get; } - await writer.WriteStartBatchAsync().ConfigureAwait(false); + /// + /// Gets the Headers for the batch content. + /// + public IHeaderDictionary Headers { get; } + + /// + /// Serialize the batch content to a stream. + /// + /// The stream to serialize to. + /// A that can be awaited. + /// This function uses types that are AspNetCore-specific. + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "")] + public Task SerializeToStreamAsync(Stream stream) + { + // how to dispose it? + IODataResponseMessage responseMessage = ODataMessageWrapperHelper.Create(stream, Headers, _requestContainer); + return WriteToResponseMessageAsync(responseMessage); + } - foreach (ODataBatchResponseItem response in Responses) - { - await response.WriteResponseAsync(writer).ConfigureAwait(false); - } + /// + /// Serialize the batch responses to an . + /// + /// The response message. + /// + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "")] + private async Task WriteToResponseMessageAsync(IODataResponseMessage responseMessage) + { + // how to dispose it? + ODataMessageWriter messageWriter = new ODataMessageWriter(responseMessage, _writerSettings); + + ODataBatchWriter writer = await messageWriter.CreateODataBatchWriterAsync().ConfigureAwait(false); - await writer.WriteEndBatchAsync().ConfigureAwait(false); + await writer.WriteStartBatchAsync().ConfigureAwait(false); + + foreach (ODataBatchResponseItem response in Responses) + { + await response.WriteResponseAsync(writer).ConfigureAwait(false); } + + await writer.WriteEndBatchAsync().ConfigureAwait(false); } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchHandler.cs b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchHandler.cs index 2a053a262..007b24bc5 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchHandler.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchHandler.cs @@ -12,111 +12,110 @@ using Microsoft.AspNetCore.OData.Extensions; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// Defines the abstraction for handling OData batch requests. +/// +public abstract class ODataBatchHandler { + // Maxing out the received message size as we depend on the hosting layer to enforce this limit. + private readonly ODataMessageQuotas _messageQuotas = new ODataMessageQuotas { MaxReceivedMessageSize = Int64.MaxValue }; + + // Preference odata.continue-on-error. + internal const string PreferenceContinueOnError = "continue-on-error"; + internal const string PreferenceContinueOnErrorFalse = "continue-on-error=false"; + /// - /// Defines the abstraction for handling OData batch requests. + /// Gets the used for reading/writing the batch request/response. /// - public abstract class ODataBatchHandler + public ODataMessageQuotas MessageQuotas { - // Maxing out the received message size as we depend on the hosting layer to enforce this limit. - private readonly ODataMessageQuotas _messageQuotas = new ODataMessageQuotas { MaxReceivedMessageSize = Int64.MaxValue }; + get { return _messageQuotas; } + } - // Preference odata.continue-on-error. - internal const string PreferenceContinueOnError = "continue-on-error"; - internal const string PreferenceContinueOnErrorFalse = "continue-on-error=false"; + /// + /// Gets or sets the OData route associated with this batch handler. + /// + public string PrefixName { get; set; } + + /// + /// Abstract method for processing a batch request. + /// + /// The http content. + /// >The next handler in the middleware chain. + /// + public abstract Task ProcessBatchAsync(HttpContext context, RequestDelegate nextHandler); - /// - /// Gets the used for reading/writing the batch request/response. - /// - public ODataMessageQuotas MessageQuotas + /// + /// Creates the batch response message. + /// + /// The responses for the batch requests. + /// The original request containing all the batch requests. + /// The batch response message. + public virtual Task CreateResponseMessageAsync(IEnumerable responses, HttpRequest request) + { + if (request == null) { - get { return _messageQuotas; } + throw new ArgumentNullException(nameof(request)); } - /// - /// Gets or sets the OData route associated with this batch handler. - /// - public string PrefixName { get; set; } - - /// - /// Abstract method for processing a batch request. - /// - /// The http content. - /// >The next handler in the middleware chain. - /// - public abstract Task ProcessBatchAsync(HttpContext context, RequestDelegate nextHandler); + return request.CreateODataBatchResponseAsync(responses, MessageQuotas); + } - /// - /// Creates the batch response message. - /// - /// The responses for the batch requests. - /// The original request containing all the batch requests. - /// The batch response message. - public virtual Task CreateResponseMessageAsync(IEnumerable responses, HttpRequest request) + /// + /// Validates the incoming request that contains the batch request messages. + /// + /// The request containing the batch request messages. + /// true if the request is valid, otherwise false. + public virtual Task ValidateRequest(HttpRequest request) + { + if (request == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - return request.CreateODataBatchResponseAsync(responses, MessageQuotas); + throw new ArgumentNullException(nameof(request)); } - /// - /// Validates the incoming request that contains the batch request messages. - /// - /// The request containing the batch request messages. - /// true if the request is valid, otherwise false. - public virtual Task ValidateRequest(HttpRequest request) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - return request.ValidateODataBatchRequest(); - } + return request.ValidateODataBatchRequest(); + } - /// - /// Gets the base URI for the batched requests. - /// - /// The original request containing all the batch requests. - /// The base URI. - public virtual Uri GetBaseUri(HttpRequest request) + /// + /// Gets the base URI for the batched requests. + /// + /// The original request containing all the batch requests. + /// The base URI. + public virtual Uri GetBaseUri(HttpRequest request) + { + if (request == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - return request.GetODataBatchBaseUri(PrefixName); + throw new ArgumentNullException(nameof(request)); } - /// - /// Gets or sets if the continue-on-error header is enable or not. - /// - internal bool ContinueOnError { get; private set; } + return request.GetODataBatchBaseUri(PrefixName); + } - /// - /// Set ContinueOnError based on the request and headers. - /// - /// The request header. - /// Flag indicating if continue on error header is enabled. - internal void SetContinueOnError(IHeaderDictionary header, bool enableContinueOnErrorHeader) + /// + /// Gets or sets if the continue-on-error header is enable or not. + /// + internal bool ContinueOnError { get; private set; } + + /// + /// Set ContinueOnError based on the request and headers. + /// + /// The request header. + /// Flag indicating if continue on error header is enabled. + internal void SetContinueOnError(IHeaderDictionary header, bool enableContinueOnErrorHeader) + { + string preferHeader = RequestPreferenceHelpers.GetRequestPreferHeader(header); + if ((preferHeader != null && + preferHeader.Contains(PreferenceContinueOnError, StringComparison.OrdinalIgnoreCase) && + !preferHeader.Contains(PreferenceContinueOnErrorFalse, StringComparison.OrdinalIgnoreCase)) + || (!enableContinueOnErrorHeader)) + { + ContinueOnError = true; + } + else { - string preferHeader = RequestPreferenceHelpers.GetRequestPreferHeader(header); - if ((preferHeader != null && - preferHeader.Contains(PreferenceContinueOnError, StringComparison.OrdinalIgnoreCase) && - !preferHeader.Contains(PreferenceContinueOnErrorFalse, StringComparison.OrdinalIgnoreCase)) - || (!enableContinueOnErrorHeader)) - { - ContinueOnError = true; - } - else - { - ContinueOnError = false; - } + ContinueOnError = false; } } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchHttpRequestExtensions.cs b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchHttpRequestExtensions.cs index cddb3cd73..07f0bb056 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchHttpRequestExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchHttpRequestExtensions.cs @@ -24,304 +24,303 @@ using Microsoft.Net.Http.Headers; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// Provides extension methods for the class. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class ODataBatchHttpRequestExtensions { + private const string BatchMediaTypeMime = "multipart/mixed"; + private const string BatchMediaTypeJson = "application/json"; + private const string Boundary = "boundary"; + /// - /// Provides extension methods for the class. + /// Determine if the request is a batch request. /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static class ODataBatchHttpRequestExtensions + /// + /// + public static bool IsODataBatchRequest(this HttpRequest request) { - private const string BatchMediaTypeMime = "multipart/mixed"; - private const string BatchMediaTypeJson = "application/json"; - private const string Boundary = "boundary"; - - /// - /// Determine if the request is a batch request. - /// - /// - /// - public static bool IsODataBatchRequest(this HttpRequest request) + if (request == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - return request.ContentType != null && - (request.ContentType.StartsWith(BatchMediaTypeMime, StringComparison.OrdinalIgnoreCase) || - request.ContentType.StartsWith(BatchMediaTypeJson, StringComparison.OrdinalIgnoreCase)); + throw new ArgumentNullException(nameof(request)); } - /// - /// Retrieves the Batch ID associated with the request. - /// - /// The Http request. - /// The Batch ID associated with this request, or null if there isn't one. - public static Guid? GetODataBatchId(this HttpRequest request) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + return request.ContentType != null && + (request.ContentType.StartsWith(BatchMediaTypeMime, StringComparison.OrdinalIgnoreCase) || + request.ContentType.StartsWith(BatchMediaTypeJson, StringComparison.OrdinalIgnoreCase)); + } - return request.ODataBatchFeature().BatchId; + /// + /// Retrieves the Batch ID associated with the request. + /// + /// The Http request. + /// The Batch ID associated with this request, or null if there isn't one. + public static Guid? GetODataBatchId(this HttpRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); } - /// - /// Associates a given Batch ID with the request. - /// - /// The request. - /// The Batch ID. - public static void SetODataBatchId(this HttpRequest request, Guid batchId) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + return request.ODataBatchFeature().BatchId; + } - request.ODataBatchFeature().BatchId = batchId; + /// + /// Associates a given Batch ID with the request. + /// + /// The request. + /// The Batch ID. + public static void SetODataBatchId(this HttpRequest request, Guid batchId) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); } - /// - /// Retrieves the ChangeSet ID associated with the request. - /// - /// The request. - /// The ChangeSet ID associated with this request, or null if there isn't one. - public static Guid? GetODataChangeSetId(this HttpRequest request) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + request.ODataBatchFeature().BatchId = batchId; + } - return request.ODataBatchFeature().ChangeSetId; + /// + /// Retrieves the ChangeSet ID associated with the request. + /// + /// The request. + /// The ChangeSet ID associated with this request, or null if there isn't one. + public static Guid? GetODataChangeSetId(this HttpRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); } - /// - /// Associates a given ChangeSet ID with the request. - /// - /// The request. - /// The ChangeSet ID. - public static void SetODataChangeSetId(this HttpRequest request, Guid changeSetId) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + return request.ODataBatchFeature().ChangeSetId; + } - request.ODataBatchFeature().ChangeSetId = changeSetId; + /// + /// Associates a given ChangeSet ID with the request. + /// + /// The request. + /// The ChangeSet ID. + public static void SetODataChangeSetId(this HttpRequest request, Guid changeSetId) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); } - /// - /// Retrieves the Content-ID associated with the sub-request of a batch. - /// - /// The request. - /// The Content-ID associated with this request, or null if there isn't one. - public static string GetODataContentId(this HttpRequest request) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + request.ODataBatchFeature().ChangeSetId = changeSetId; + } - return request.ODataBatchFeature().ContentId; + /// + /// Retrieves the Content-ID associated with the sub-request of a batch. + /// + /// The request. + /// The Content-ID associated with this request, or null if there isn't one. + public static string GetODataContentId(this HttpRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); } - /// - /// Associates a given Content-ID with the sub-request of a batch. - /// - /// The request. - /// The Content-ID. - public static void SetODataContentId(this HttpRequest request, string contentId) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + return request.ODataBatchFeature().ContentId; + } - request.ODataBatchFeature().ContentId = contentId; + /// + /// Associates a given Content-ID with the sub-request of a batch. + /// + /// The request. + /// The Content-ID. + public static void SetODataContentId(this HttpRequest request, string contentId) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); } - /// - /// Retrieves the Content-ID to Location mapping associated with the request. - /// - /// The request. - /// The Content-ID to Location mapping associated with this request, or null if there isn't one. - public static IDictionary GetODataContentIdMapping(this HttpRequest request) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + request.ODataBatchFeature().ContentId = contentId; + } - return request.ODataBatchFeature().ContentIdMapping; + /// + /// Retrieves the Content-ID to Location mapping associated with the request. + /// + /// The request. + /// The Content-ID to Location mapping associated with this request, or null if there isn't one. + public static IDictionary GetODataContentIdMapping(this HttpRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); } - /// - /// Associates a given Content-ID to Location mapping with the request. - /// - /// The request. - /// The Content-ID to Location mapping. - public static void SetODataContentIdMapping(this HttpRequest request, IDictionary contentIdMapping) + return request.ODataBatchFeature().ContentIdMapping; + } + + /// + /// Associates a given Content-ID to Location mapping with the request. + /// + /// The request. + /// The Content-ID to Location mapping. + public static void SetODataContentIdMapping(this HttpRequest request, IDictionary contentIdMapping) + { + if (request == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + throw new ArgumentNullException(nameof(request)); + } - IDictionary old = request.ODataBatchFeature().ContentIdMapping; - old.Clear(); + IDictionary old = request.ODataBatchFeature().ContentIdMapping; + old.Clear(); - if (contentIdMapping != null) + if (contentIdMapping != null) + { + foreach (var idMap in contentIdMapping) { - foreach (var idMap in contentIdMapping) - { - old.Add(idMap); - } + old.Add(idMap); } } + } - [SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "")] - internal static Task CreateODataBatchResponseAsync(this HttpRequest request, IEnumerable responses, ODataMessageQuotas messageQuotas) - { - Contract.Assert(request != null); + [SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "")] + internal static Task CreateODataBatchResponseAsync(this HttpRequest request, IEnumerable responses, ODataMessageQuotas messageQuotas) + { + Contract.Assert(request != null); - ODataVersion odataVersion = GetODataResponseVersion(request); + ODataVersion odataVersion = GetODataResponseVersion(request); - IServiceProvider requestContainer = request.GetRouteServices(); - ODataMessageWriterSettings writerSettings = requestContainer.GetRequiredService(); - writerSettings.Version = odataVersion; - writerSettings.MessageQuotas = messageQuotas; + IServiceProvider requestContainer = request.GetRouteServices(); + ODataMessageWriterSettings writerSettings = requestContainer.GetRequiredService(); + writerSettings.Version = odataVersion; + writerSettings.MessageQuotas = messageQuotas; - HttpResponse response = request.HttpContext.Response; + HttpResponse response = request.HttpContext.Response; - IEnumerable acceptHeaders = MediaTypeHeaderValue.ParseList(request.Headers.GetCommaSeparatedValues("Accept")); - string responseContentType = null; - foreach (MediaTypeHeaderValue acceptHeader in acceptHeaders.OrderByDescending(h => h.Quality == null ? 1 : h.Quality)) + IEnumerable acceptHeaders = MediaTypeHeaderValue.ParseList(request.Headers.GetCommaSeparatedValues("Accept")); + string responseContentType = null; + foreach (MediaTypeHeaderValue acceptHeader in acceptHeaders.OrderByDescending(h => h.Quality == null ? 1 : h.Quality)) + { + if (acceptHeader.MediaType.Equals(BatchMediaTypeMime, StringComparison.OrdinalIgnoreCase)) { - if (acceptHeader.MediaType.Equals(BatchMediaTypeMime, StringComparison.OrdinalIgnoreCase)) - { - responseContentType = string.Format(CultureInfo.InvariantCulture, "multipart/mixed;boundary=batchresponse_{0}", Guid.NewGuid()); - break; - } - else if (acceptHeader.MediaType.Equals(BatchMediaTypeJson, StringComparison.OrdinalIgnoreCase)) - { - responseContentType = BatchMediaTypeJson; - break; - } + responseContentType = string.Format(CultureInfo.InvariantCulture, "multipart/mixed;boundary=batchresponse_{0}", Guid.NewGuid()); + break; } - if (responseContentType == null) + else if (acceptHeader.MediaType.Equals(BatchMediaTypeJson, StringComparison.OrdinalIgnoreCase)) { - // In absence of accept, if request was JSON then default response to be JSON. - // Note that, if responseContentType is not set, then it will default to multipart/mixed - // when constructing the BatchContent, so we don't need to handle that case here - if (!string.IsNullOrEmpty(request.ContentType) - && request.ContentType.IndexOf(BatchMediaTypeJson, StringComparison.OrdinalIgnoreCase) > -1) - { - responseContentType = BatchMediaTypeJson; - } + responseContentType = BatchMediaTypeJson; + break; } - - response.StatusCode = StatusCodes.Status200OK; - ODataBatchContent batchContent = new ODataBatchContent(responses, requestContainer, responseContentType); - foreach (var header in batchContent.Headers) + } + if (responseContentType == null) + { + // In absence of accept, if request was JSON then default response to be JSON. + // Note that, if responseContentType is not set, then it will default to multipart/mixed + // when constructing the BatchContent, so we don't need to handle that case here + if (!string.IsNullOrEmpty(request.ContentType) + && request.ContentType.IndexOf(BatchMediaTypeJson, StringComparison.OrdinalIgnoreCase) > -1) { - // Copy headers from batch content, overwriting any existing headers. - response.Headers[header.Key] = header.Value; + responseContentType = BatchMediaTypeJson; } - - return batchContent.SerializeToStreamAsync(response.Body); } - internal static async Task ValidateODataBatchRequest(this HttpRequest request) + response.StatusCode = StatusCodes.Status200OK; + ODataBatchContent batchContent = new ODataBatchContent(responses, requestContainer, responseContentType); + foreach (var header in batchContent.Headers) { - Contract.Assert(request != null); + // Copy headers from batch content, overwriting any existing headers. + response.Headers[header.Key] = header.Value; + } - HttpResponse response = request.HttpContext.Response; + return batchContent.SerializeToStreamAsync(response.Body); + } - if (request.Body == null) - { - response.StatusCode = (int)HttpStatusCode.BadRequest; - await response.WriteAsync(SRResources.BatchRequestMissingBody).ConfigureAwait(false); - return false; - } + internal static async Task ValidateODataBatchRequest(this HttpRequest request) + { + Contract.Assert(request != null); - RequestHeaders headers = request.GetTypedHeaders(); - MediaTypeHeaderValue contentType = headers.ContentType; - if (contentType == null) - { - response.StatusCode = (int)HttpStatusCode.BadRequest; - await response.WriteAsync(SRResources.BatchRequestMissingContentType).ConfigureAwait(false); - return false; - } + HttpResponse response = request.HttpContext.Response; + + if (request.Body == null) + { + response.StatusCode = (int)HttpStatusCode.BadRequest; + await response.WriteAsync(SRResources.BatchRequestMissingBody).ConfigureAwait(false); + return false; + } + + RequestHeaders headers = request.GetTypedHeaders(); + MediaTypeHeaderValue contentType = headers.ContentType; + if (contentType == null) + { + response.StatusCode = (int)HttpStatusCode.BadRequest; + await response.WriteAsync(SRResources.BatchRequestMissingContentType).ConfigureAwait(false); + return false; + } - string mediaType = contentType.MediaType.ToString(); - bool isMimeBatch = string.Equals(mediaType, BatchMediaTypeMime, StringComparison.OrdinalIgnoreCase); - bool isJsonBatch = string.Equals(mediaType, BatchMediaTypeJson, StringComparison.OrdinalIgnoreCase); + string mediaType = contentType.MediaType.ToString(); + bool isMimeBatch = string.Equals(mediaType, BatchMediaTypeMime, StringComparison.OrdinalIgnoreCase); + bool isJsonBatch = string.Equals(mediaType, BatchMediaTypeJson, StringComparison.OrdinalIgnoreCase); - if (!isMimeBatch && !isJsonBatch) + if (!isMimeBatch && !isJsonBatch) + { + response.StatusCode = (int)HttpStatusCode.BadRequest; + await response.WriteAsync(Error.Format(SRResources.BatchRequestInvalidMediaType, + BatchMediaTypeMime, BatchMediaTypeJson)).ConfigureAwait(false); + return false; + } + + if (isMimeBatch) + { + NameValueHeaderValue boundary = contentType.Parameters.FirstOrDefault(p => String.Equals(p.Name.ToString(), Boundary, StringComparison.OrdinalIgnoreCase)); + if (boundary == null || string.IsNullOrEmpty(boundary.Value.ToString())) { response.StatusCode = (int)HttpStatusCode.BadRequest; - await response.WriteAsync(Error.Format(SRResources.BatchRequestInvalidMediaType, - BatchMediaTypeMime, BatchMediaTypeJson)).ConfigureAwait(false); + await response.WriteAsync(SRResources.BatchRequestMissingBoundary).ConfigureAwait(false); return false; } + } - if (isMimeBatch) - { - NameValueHeaderValue boundary = contentType.Parameters.FirstOrDefault(p => String.Equals(p.Name.ToString(), Boundary, StringComparison.OrdinalIgnoreCase)); - if (boundary == null || string.IsNullOrEmpty(boundary.Value.ToString())) - { - response.StatusCode = (int)HttpStatusCode.BadRequest; - await response.WriteAsync(SRResources.BatchRequestMissingBoundary).ConfigureAwait(false); - return false; - } - } + return true; + } - return true; - } + internal static Uri GetODataBatchBaseUri(this HttpRequest request, string oDataPrefixName) + { + Contract.Assert(request != null); - internal static Uri GetODataBatchBaseUri(this HttpRequest request, string oDataPrefixName) + if (oDataPrefixName == null) { - Contract.Assert(request != null); - - if (oDataPrefixName == null) - { - // Return request's base address. - return new Uri(request.GetDisplayUrl()); - } + // Return request's base address. + return new Uri(request.GetDisplayUrl()); + } - // Maybe we can just do: - // string requestUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path); - // use requestUri to remove the "$batch"? + // Maybe we can just do: + // string requestUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path); + // use requestUri to remove the "$batch"? - request.ODataFeature().RoutePrefix = oDataPrefixName; + request.ODataFeature().RoutePrefix = oDataPrefixName; - RouteValueDictionary batchRouteData = request.ODataFeature().BatchRouteData; - if (batchRouteData != null && batchRouteData.Any()) + RouteValueDictionary batchRouteData = request.ODataFeature().BatchRouteData; + if (batchRouteData != null && batchRouteData.Any()) + { + foreach (var data in batchRouteData) { - foreach (var data in batchRouteData) - { - request.RouteValues.Add(data.Key, data.Value); - } + request.RouteValues.Add(data.Key, data.Value); } - - return new Uri(request.CreateODataLink()); } - internal static ODataVersion GetODataResponseVersion(HttpRequest request) - { - // OData protocol requires that you send the minimum version that the client needs to know to - // understand the response. There is no easy way we can figure out the minimum version that the client - // needs to understand our response. We send response headers much ahead generating the response. So if - // the requestMessage has a OData-MaxVersion, tell the client that our response is of the same - // version; else use the DataServiceVersionHeader. Our response might require a higher version of the - // client and it might fail. If the client doesn't send these headers respond with the default version - // (V4). - return request.ODataMaxServiceVersion() ?? - request.ODataServiceVersion() ?? - ODataVersionConstraint.DefaultODataVersion; - } + return new Uri(request.CreateODataLink()); + } + + internal static ODataVersion GetODataResponseVersion(HttpRequest request) + { + // OData protocol requires that you send the minimum version that the client needs to know to + // understand the response. There is no easy way we can figure out the minimum version that the client + // needs to understand our response. We send response headers much ahead generating the response. So if + // the requestMessage has a OData-MaxVersion, tell the client that our response is of the same + // version; else use the DataServiceVersionHeader. Our response might require a higher version of the + // client and it might fail. If the client doesn't send these headers respond with the default version + // (V4). + return request.ODataMaxServiceVersion() ?? + request.ODataServiceVersion() ?? + ODataVersionConstraint.DefaultODataVersion; } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchMiddleware.cs b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchMiddleware.cs index bf24e53be..5e70fe9f1 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchMiddleware.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchMiddleware.cs @@ -12,98 +12,97 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// Defines the middleware for handling OData $batch requests. +/// This middleware essentially acts like branching middleware and redirects OData $batch +/// requests to the appropriate ODataBatchHandler. +/// +public class ODataBatchMiddleware { + private readonly RequestDelegate _next; + private ODataBatchPathMapping _batchMapping; + /// - /// Defines the middleware for handling OData $batch requests. - /// This middleware essentially acts like branching middleware and redirects OData $batch - /// requests to the appropriate ODataBatchHandler. + /// Instantiates a new instance of . /// - public class ODataBatchMiddleware + /// The service provider, we don't inject the ODataOptions. + /// The next middleware. + public ODataBatchMiddleware(IServiceProvider serviceProvider, RequestDelegate next) { - private readonly RequestDelegate _next; - private ODataBatchPathMapping _batchMapping; + _next = next; - /// - /// Instantiates a new instance of . - /// - /// The service provider, we don't inject the ODataOptions. - /// The next middleware. - public ODataBatchMiddleware(IServiceProvider serviceProvider, RequestDelegate next) + // We inject the service provider to let the middle ware pass without ODataOptions injected. + IOptions odataOptionsOptions = serviceProvider?.GetService>(); + if (odataOptionsOptions != null && odataOptionsOptions.Value != null) { - _next = next; - - // We inject the service provider to let the middle ware pass without ODataOptions injected. - IOptions odataOptionsOptions = serviceProvider?.GetService>(); - if (odataOptionsOptions != null && odataOptionsOptions.Value != null) - { - Initialize(odataOptionsOptions.Value); - } + Initialize(odataOptionsOptions.Value); } + } - /// - /// Gets the batch path mapping, for unit test only - /// - internal ODataBatchPathMapping BatchMapping => _batchMapping; + /// + /// Gets the batch path mapping, for unit test only + /// + internal ODataBatchPathMapping BatchMapping => _batchMapping; - /// - /// Invoke the OData $Batch middleware. - /// - /// The http context. - /// A task that can be awaited. - public async Task Invoke(HttpContext context) + /// + /// Invoke the OData $Batch middleware. + /// + /// The http context. + /// A task that can be awaited. + public async Task Invoke(HttpContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - string prefixName; - ODataBatchHandler batchHandler; + throw Error.ArgumentNull(nameof(context)); + } + string prefixName; + ODataBatchHandler batchHandler; - // The batch middleware should not handle the options requests for cors to properly function. - bool isPostRequest = HttpMethods.IsPost(context.Request.Method); + // The batch middleware should not handle the options requests for cors to properly function. + bool isPostRequest = HttpMethods.IsPost(context.Request.Method); - if (isPostRequest - && _batchMapping != null - && _batchMapping.TryGetPrefixName(context, out prefixName, out batchHandler)) - { - Contract.Assert(batchHandler != null); - await batchHandler.ProcessBatchAsync(context, _next).ConfigureAwait(false); - } - else - { - await _next(context).ConfigureAwait(false); - } + if (isPostRequest + && _batchMapping != null + && _batchMapping.TryGetPrefixName(context, out prefixName, out batchHandler)) + { + Contract.Assert(batchHandler != null); + await batchHandler.ProcessBatchAsync(context, _next).ConfigureAwait(false); + } + else + { + await _next(context).ConfigureAwait(false); } + } + + private void Initialize(ODataOptions options) + { + Contract.Assert(options != null); - private void Initialize(ODataOptions options) + foreach (var model in options.RouteComponents) { - Contract.Assert(options != null); + IServiceProvider subServiceProvider = model.Value.ServiceProvider; + if (subServiceProvider == null) + { + continue; + } - foreach (var model in options.RouteComponents) + // If a batch handler is present, register the route with the batch path mapper. This will be used + // by the batching middleware to handle the batch request. Batching still requires the injection + // of the batching middleware via UseODataBatching(). + ODataBatchHandler batchHandler = subServiceProvider.GetService(); + if (batchHandler != null) { - IServiceProvider subServiceProvider = model.Value.ServiceProvider; - if (subServiceProvider == null) - { - continue; - } + batchHandler.PrefixName = model.Key; + string batchPath = string.IsNullOrEmpty(model.Key) ? "/$batch" : $"/{model.Key}/$batch"; - // If a batch handler is present, register the route with the batch path mapper. This will be used - // by the batching middleware to handle the batch request. Batching still requires the injection - // of the batching middleware via UseODataBatching(). - ODataBatchHandler batchHandler = subServiceProvider.GetService(); - if (batchHandler != null) + if (_batchMapping == null) { - batchHandler.PrefixName = model.Key; - string batchPath = string.IsNullOrEmpty(model.Key) ? "/$batch" : $"/{model.Key}/$batch"; - - if (_batchMapping == null) - { - _batchMapping = new ODataBatchPathMapping(); - } - - _batchMapping.AddRoute(model.Key, batchPath, batchHandler); + _batchMapping = new ODataBatchPathMapping(); } + + _batchMapping.AddRoute(model.Key, batchPath, batchHandler); } } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchPathMapping.cs b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchPathMapping.cs index cb42e6694..3d1a8e1c5 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchPathMapping.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchPathMapping.cs @@ -12,77 +12,76 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Template; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// A class for storing batch route names and prefixes used to determine if a route is a +/// batch route. +/// +internal class ODataBatchPathMapping { + private Dictionary templateMappings = new Dictionary(); + /// - /// A class for storing batch route names and prefixes used to determine if a route is a - /// batch route. + /// Add a route name and template for batching. /// - internal class ODataBatchPathMapping + /// The route prefix name. + /// The route template. + /// The batch handler. + public void AddRoute(string prefixName, string routeTemplate, ODataBatchHandler handler) { - private Dictionary templateMappings = new Dictionary(); - - /// - /// Add a route name and template for batching. - /// - /// The route prefix name. - /// The route template. - /// The batch handler. - public void AddRoute(string prefixName, string routeTemplate, ODataBatchHandler handler) + if (routeTemplate == null) { - if (routeTemplate == null) - { - throw Error.ArgumentNull(nameof(routeTemplate)); - } - - string newRouteTemplate = routeTemplate.StartsWith("/", StringComparison.Ordinal) ? routeTemplate.Substring(1) : routeTemplate; - RouteTemplate parsedTemplate = TemplateParser.Parse(newRouteTemplate); - TemplateMatcher matcher = new TemplateMatcher(parsedTemplate, new RouteValueDictionary()); - templateMappings[matcher] = (prefixName, handler); + throw Error.ArgumentNull(nameof(routeTemplate)); } - /// - /// Try and get the batch handler for a given path. - /// - /// The http context. - /// The route/prefix name if found or null. - /// The batch handler. - /// true if a route name is found, otherwise false. - public bool TryGetPrefixName(HttpContext context, out string prefixName, out ODataBatchHandler handler) + string newRouteTemplate = routeTemplate.StartsWith("/", StringComparison.Ordinal) ? routeTemplate.Substring(1) : routeTemplate; + RouteTemplate parsedTemplate = TemplateParser.Parse(newRouteTemplate); + TemplateMatcher matcher = new TemplateMatcher(parsedTemplate, new RouteValueDictionary()); + templateMappings[matcher] = (prefixName, handler); + } + + /// + /// Try and get the batch handler for a given path. + /// + /// The http context. + /// The route/prefix name if found or null. + /// The batch handler. + /// true if a route name is found, otherwise false. + public bool TryGetPrefixName(HttpContext context, out string prefixName, out ODataBatchHandler handler) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - prefixName = null; - handler = null; - string path = context.Request.Path; - foreach (var item in templateMappings) + prefixName = null; + handler = null; + string path = context.Request.Path; + foreach (var item in templateMappings) + { + RouteValueDictionary routeData = new RouteValueDictionary(); + if (item.Key.TryMatch(path, routeData)) { - RouteValueDictionary routeData = new RouteValueDictionary(); - if (item.Key.TryMatch(path, routeData)) + prefixName = item.Value.Item1; + handler = item.Value.Item2; + if (routeData.Count > 0) { - prefixName = item.Value.Item1; - handler = item.Value.Item2; - if (routeData.Count > 0) - { - Merge(context.ODataFeature().BatchRouteData, routeData); - } - - return true; + Merge(context.ODataFeature().BatchRouteData, routeData); } - } - return false; + return true; + } } - private static void Merge(RouteValueDictionary batchRouteData, RouteValueDictionary routeData) + return false; + } + + private static void Merge(RouteValueDictionary batchRouteData, RouteValueDictionary routeData) + { + foreach (var item in routeData) { - foreach (var item in routeData) - { - batchRouteData.Add(item.Key, item.Value); - } + batchRouteData.Add(item.Key, item.Value); } } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchReaderExtensions.cs b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchReaderExtensions.cs index 4f39af3ea..71bbf3e9c 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchReaderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchReaderExtensions.cs @@ -20,390 +20,389 @@ using Microsoft.Extensions.Primitives; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// Provides extension methods for the class. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class ODataBatchReaderExtensions { + private static readonly string[] nonInheritableHeaders = new string[] { "content-length", "content-type" }; + + // do not inherit respond-async and continue-on-error (odata.continue-on-error in OData 4.0) from Prefer header + private static readonly string[] nonInheritablePreferences = new string[] { "respond-async", "continue-on-error", "odata.continue-on-error" }; + /// - /// Provides extension methods for the class. + /// Reads a ChangeSet request. /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static class ODataBatchReaderExtensions + /// The . + /// The context containing the batch request messages. + /// The Batch Id. + /// The token to monitor for cancellation requests. + /// A collection of in the ChangeSet. + public static async Task> ReadChangeSetRequestAsync( + this ODataBatchReader reader, HttpContext context, Guid batchId, CancellationToken cancellationToken) { - private static readonly string[] nonInheritableHeaders = new string[] { "content-length", "content-type" }; - - // do not inherit respond-async and continue-on-error (odata.continue-on-error in OData 4.0) from Prefer header - private static readonly string[] nonInheritablePreferences = new string[] { "respond-async", "continue-on-error", "odata.continue-on-error" }; - - /// - /// Reads a ChangeSet request. - /// - /// The . - /// The context containing the batch request messages. - /// The Batch Id. - /// The token to monitor for cancellation requests. - /// A collection of in the ChangeSet. - public static async Task> ReadChangeSetRequestAsync( - this ODataBatchReader reader, HttpContext context, Guid batchId, CancellationToken cancellationToken) + if (reader == null) { - if (reader == null) - { - throw new ArgumentNullException(nameof(reader)); - } + throw new ArgumentNullException(nameof(reader)); + } - if (reader.State != ODataBatchReaderState.ChangesetStart) - { - throw Error.InvalidOperation( - SRResources.InvalidBatchReaderState, - reader.State.ToString(), - ODataBatchReaderState.ChangesetStart.ToString()); - } + if (reader.State != ODataBatchReaderState.ChangesetStart) + { + throw Error.InvalidOperation( + SRResources.InvalidBatchReaderState, + reader.State.ToString(), + ODataBatchReaderState.ChangesetStart.ToString()); + } - Guid changeSetId = Guid.NewGuid(); - List contexts = new List(); - while (await reader.ReadAsync().ConfigureAwait(false) && reader.State != ODataBatchReaderState.ChangesetEnd) + Guid changeSetId = Guid.NewGuid(); + List contexts = new List(); + while (await reader.ReadAsync().ConfigureAwait(false) && reader.State != ODataBatchReaderState.ChangesetEnd) + { + if (reader.State == ODataBatchReaderState.Operation) { - if (reader.State == ODataBatchReaderState.Operation) - { - contexts.Add(await ReadOperationInternalAsync(reader, context, batchId, changeSetId, cancellationToken).ConfigureAwait(false)); - } + contexts.Add(await ReadOperationInternalAsync(reader, context, batchId, changeSetId, cancellationToken).ConfigureAwait(false)); } + } + + return contexts; + } - return contexts; + /// + /// Reads an Operation request. + /// + /// The . + /// The context containing the batch request messages. + /// The Batch ID. + /// The token to monitor for cancellation requests. + /// A representing the operation. + public static Task ReadOperationRequestAsync( + this ODataBatchReader reader, HttpContext context, Guid batchId, CancellationToken cancellationToken) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); } - /// - /// Reads an Operation request. - /// - /// The . - /// The context containing the batch request messages. - /// The Batch ID. - /// The token to monitor for cancellation requests. - /// A representing the operation. - public static Task ReadOperationRequestAsync( - this ODataBatchReader reader, HttpContext context, Guid batchId, CancellationToken cancellationToken) + if (reader.State != ODataBatchReaderState.Operation) { - if (reader == null) - { - throw new ArgumentNullException(nameof(reader)); - } + throw Error.InvalidOperation( + SRResources.InvalidBatchReaderState, + reader.State.ToString(), + ODataBatchReaderState.Operation.ToString()); + } - if (reader.State != ODataBatchReaderState.Operation) - { - throw Error.InvalidOperation( - SRResources.InvalidBatchReaderState, - reader.State.ToString(), - ODataBatchReaderState.Operation.ToString()); - } + return ReadOperationInternalAsync(reader, context, batchId, null, cancellationToken); + } - return ReadOperationInternalAsync(reader, context, batchId, null, cancellationToken); + /// + /// Reads an Operation request in a ChangeSet. + /// + /// The . + /// The context containing the batch request messages. + /// The Batch ID. + /// The ChangeSet ID. + /// The token to monitor for cancellation requests. + /// A representing a ChangeSet operation + public static Task ReadChangeSetOperationRequestAsync( + this ODataBatchReader reader, HttpContext context, Guid batchId, Guid changeSetId, CancellationToken cancellationToken) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); } - /// - /// Reads an Operation request in a ChangeSet. - /// - /// The . - /// The context containing the batch request messages. - /// The Batch ID. - /// The ChangeSet ID. - /// The token to monitor for cancellation requests. - /// A representing a ChangeSet operation - public static Task ReadChangeSetOperationRequestAsync( - this ODataBatchReader reader, HttpContext context, Guid batchId, Guid changeSetId, CancellationToken cancellationToken) + if (reader.State != ODataBatchReaderState.Operation) { - if (reader == null) - { - throw new ArgumentNullException(nameof(reader)); - } + throw Error.InvalidOperation( + SRResources.InvalidBatchReaderState, + reader.State.ToString(), + ODataBatchReaderState.Operation.ToString()); + } - if (reader.State != ODataBatchReaderState.Operation) - { - throw Error.InvalidOperation( - SRResources.InvalidBatchReaderState, - reader.State.ToString(), - ODataBatchReaderState.Operation.ToString()); - } + return ReadOperationInternalAsync(reader, context, batchId, changeSetId, cancellationToken); + } - return ReadOperationInternalAsync(reader, context, batchId, changeSetId, cancellationToken); - } + private static async Task ReadOperationInternalAsync( + ODataBatchReader reader, HttpContext originalContext, Guid batchId, Guid? changeSetId, CancellationToken cancellationToken) + { + ODataBatchOperationRequestMessage batchRequest = await reader.CreateOperationRequestMessageAsync().ConfigureAwait(false); - private static async Task ReadOperationInternalAsync( - ODataBatchReader reader, HttpContext originalContext, Guid batchId, Guid? changeSetId, CancellationToken cancellationToken) + HttpContext context = CreateHttpContext(originalContext); + HttpRequest request = context.Request; + Uri requestUri = batchRequest.Url; + + if (!requestUri.IsAbsoluteUri) { - ODataBatchOperationRequestMessage batchRequest = await reader.CreateOperationRequestMessageAsync().ConfigureAwait(false); + Uri baseUri = batchRequest.ServiceProvider.GetRequiredService().BaseUri; + requestUri = new Uri(baseUri, requestUri); + } - HttpContext context = CreateHttpContext(originalContext); - HttpRequest request = context.Request; - Uri requestUri = batchRequest.Url; + request.Method = batchRequest.Method; + request.CopyAbsoluteUrl(requestUri); - if (!requestUri.IsAbsoluteUri) + // Not using bufferContentStream. Unlike AspNet, AspNetCore cannot guarantee the disposal + // of the stream in the context of execution so there is no choice but to copy the stream + // from the batch reader. + using (Stream stream = await batchRequest.GetStreamAsync().ConfigureAwait(false)) + { + MemoryStream bufferedStream = new MemoryStream(); + // Passing in the default buffer size of 81920 so that we can also pass in a cancellation token + await stream.CopyToAsync(bufferedStream, bufferSize: 81920, cancellationToken: cancellationToken).ConfigureAwait(false); + bufferedStream.Position = 0; + request.Body = bufferedStream; + if (bufferedStream.Length > 0) { - Uri baseUri = batchRequest.ServiceProvider.GetRequiredService().BaseUri; - requestUri = new Uri(baseUri, requestUri); + request.Headers.ContentLength = bufferedStream.Length; } + } - request.Method = batchRequest.Method; - request.CopyAbsoluteUrl(requestUri); + foreach (var header in batchRequest.Headers) + { + string headerName = header.Key; + string headerValue = header.Value; - // Not using bufferContentStream. Unlike AspNet, AspNetCore cannot guarantee the disposal - // of the stream in the context of execution so there is no choice but to copy the stream - // from the batch reader. - using (Stream stream = await batchRequest.GetStreamAsync().ConfigureAwait(false)) + if (headerName.Trim().ToUpperInvariant() == "PREFER") { - MemoryStream bufferedStream = new MemoryStream(); - // Passing in the default buffer size of 81920 so that we can also pass in a cancellation token - await stream.CopyToAsync(bufferedStream, bufferSize: 81920, cancellationToken: cancellationToken).ConfigureAwait(false); - bufferedStream.Position = 0; - request.Body = bufferedStream; - if (bufferedStream.Length > 0) - { - request.Headers.ContentLength = bufferedStream.Length; - } + // in the case of Prefer header, we don't want to overwrite, + // instead we merge preferences defined in the individual request with those inherited from the batch + request.Headers.TryGetValue(headerName, out StringValues batchReferences); + request.Headers[headerName] = MergeIndividualAndBatchPreferences(headerValue, batchReferences); } - - foreach (var header in batchRequest.Headers) + else { - string headerName = header.Key; - string headerValue = header.Value; - - if (headerName.Trim().ToUpperInvariant() == "PREFER") - { - // in the case of Prefer header, we don't want to overwrite, - // instead we merge preferences defined in the individual request with those inherited from the batch - request.Headers.TryGetValue(headerName, out StringValues batchReferences); - request.Headers[headerName] = MergeIndividualAndBatchPreferences(headerValue, batchReferences); - } - else - { - // Copy headers from batch, overwriting any existing headers. - request.Headers[headerName] = headerValue; - } + // Copy headers from batch, overwriting any existing headers. + request.Headers[headerName] = headerValue; } + } - request.SetODataBatchId(batchId); - request.SetODataContentId(batchRequest.ContentId); - - if (changeSetId != null && changeSetId.HasValue) - { - request.SetODataChangeSetId(changeSetId.Value); - } + request.SetODataBatchId(batchId); + request.SetODataContentId(batchRequest.ContentId); - return context; + if (changeSetId != null && changeSetId.HasValue) + { + request.SetODataChangeSetId(changeSetId.Value); } - [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "")] - private static HttpContext CreateHttpContext(HttpContext originalContext) + return context; + } + + [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "")] + private static HttpContext CreateHttpContext(HttpContext originalContext) + { + // Clone the features so that a new set is used for each context. + // The features themselves will be reused but not the collection. We + // store the request container as a feature of the request and we don't want + // the features added to one context/request to be visible on another. + // + // Note that just about everything in the HttpContext and HttpRequest is + // backed by one of these features. So reusing the features means the HttContext + // and HttpRequests are the same without needing to copy properties. To make them + // different, we need to avoid copying certain features to that the objects don't + // share the same storage/ + IFeatureCollection features = new FeatureCollection(); + string pathBase = ""; + foreach (KeyValuePair kvp in originalContext.Features) { - // Clone the features so that a new set is used for each context. - // The features themselves will be reused but not the collection. We - // store the request container as a feature of the request and we don't want - // the features added to one context/request to be visible on another. + // Don't include the OData features. They may already + // be present. This will get re-created later. + // + // Also, clear out the items feature, which is used + // to store a few object, the one that is an issue here is the Url + // helper, which has an affinity to the context. If we leave it, + // the context of the helper no longer matches the new context and + // the resulting url helper doesn't have access to the OData feature + // because it's looking in the wrong context. // - // Note that just about everything in the HttpContext and HttpRequest is - // backed by one of these features. So reusing the features means the HttContext - // and HttpRequests are the same without needing to copy properties. To make them - // different, we need to avoid copying certain features to that the objects don't - // share the same storage/ - IFeatureCollection features = new FeatureCollection(); - string pathBase = ""; - foreach (KeyValuePair kvp in originalContext.Features) + // Because we need a different request and response, leave those features + // out as well. + if (kvp.Key == typeof(IHttpRequestFeature)) { - // Don't include the OData features. They may already - // be present. This will get re-created later. - // - // Also, clear out the items feature, which is used - // to store a few object, the one that is an issue here is the Url - // helper, which has an affinity to the context. If we leave it, - // the context of the helper no longer matches the new context and - // the resulting url helper doesn't have access to the OData feature - // because it's looking in the wrong context. - // - // Because we need a different request and response, leave those features - // out as well. - if (kvp.Key == typeof(IHttpRequestFeature)) - { - pathBase = ((IHttpRequestFeature)kvp.Value).PathBase; - } - - if (kvp.Key == typeof(IODataBatchFeature) || - kvp.Key == typeof(IODataFeature) || - kvp.Key == typeof(IItemsFeature) || - kvp.Key == typeof(IHttpRequestFeature) || - kvp.Key == typeof(IHttpResponseFeature) || - kvp.Key == typeof(IQueryFeature)) // Noted: we should not pass the QueryFeature from Main request to the sub request - { - continue; - } - - if (kvp.Key == typeof(IEndpointFeature)) - { - continue; - } - - features[kvp.Key] = kvp.Value; + pathBase = ((IHttpRequestFeature)kvp.Value).PathBase; } - // Add in an items, request and response feature. - features[typeof(IItemsFeature)] = new ItemsFeature(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature + if (kvp.Key == typeof(IODataBatchFeature) || + kvp.Key == typeof(IODataFeature) || + kvp.Key == typeof(IItemsFeature) || + kvp.Key == typeof(IHttpRequestFeature) || + kvp.Key == typeof(IHttpResponseFeature) || + kvp.Key == typeof(IQueryFeature)) // Noted: we should not pass the QueryFeature from Main request to the sub request { - PathBase = pathBase - }; - - features[typeof(IHttpResponseFeature)] = new HttpResponseFeature(); - - // Create a context from the factory or use the default context. - HttpContext context = null; - IHttpContextFactory httpContextFactory = originalContext.RequestServices.GetService(); - if (httpContextFactory != null) - { - context = httpContextFactory.Create(features); + continue; } - else + + if (kvp.Key == typeof(IEndpointFeature)) { - context = new DefaultHttpContext(features); + continue; } - // Clone parts of the request. All other parts of the request will be - // populated during batch processing. - context.Request.Cookies = originalContext.Request.Cookies; - foreach (KeyValuePair header in originalContext.Request.Headers) + features[kvp.Key] = kvp.Value; + } + + // Add in an items, request and response feature. + features[typeof(IItemsFeature)] = new ItemsFeature(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature + { + PathBase = pathBase + }; + + features[typeof(IHttpResponseFeature)] = new HttpResponseFeature(); + + // Create a context from the factory or use the default context. + HttpContext context = null; + IHttpContextFactory httpContextFactory = originalContext.RequestServices.GetService(); + if (httpContextFactory != null) + { + context = httpContextFactory.Create(features); + } + else + { + context = new DefaultHttpContext(features); + } + + // Clone parts of the request. All other parts of the request will be + // populated during batch processing. + context.Request.Cookies = originalContext.Request.Cookies; + foreach (KeyValuePair header in originalContext.Request.Headers) + { + string headerKey = header.Key.ToLowerInvariant(); + // do not copy over headers that should not be inherited from batch to individual requests + if (!nonInheritableHeaders.Contains(headerKey)) { - string headerKey = header.Key.ToLowerInvariant(); - // do not copy over headers that should not be inherited from batch to individual requests - if (!nonInheritableHeaders.Contains(headerKey)) + // some preferences may be inherited, others discarded + if (headerKey == "prefer") { - // some preferences may be inherited, others discarded - if (headerKey == "prefer") + string preferencesToInherit = GetPreferencesToInheritFromBatch(header.Value); + if (!string.IsNullOrEmpty(preferencesToInherit)) { - string preferencesToInherit = GetPreferencesToInheritFromBatch(header.Value); - if (!string.IsNullOrEmpty(preferencesToInherit)) - { - context.Request.Headers.Append(header.Key, preferencesToInherit); - } - } - // do not copy already existing headers, such as Cookie - else if (!context.Request.Headers.ContainsKey(header.Key)) - { - context.Request.Headers.Add(header); + context.Request.Headers.Append(header.Key, preferencesToInherit); } } + // do not copy already existing headers, such as Cookie + else if (!context.Request.Headers.ContainsKey(header.Key)) + { + context.Request.Headers.Add(header); + } } + } - // Create a response body as the default response feature does not - // have a valid stream. - // Use a special batch stream that remains open after the writer is disposed. - context.Response.Body = new ODataBatchStream(); + // Create a response body as the default response feature does not + // have a valid stream. + // Use a special batch stream that remains open after the writer is disposed. + context.Response.Body = new ODataBatchStream(); + + return context; + } - return context; - } + /// + /// Extract preferences that can be inherited from the overall batch request to + /// an individual request. + /// + /// The value of the Prefer header from the batch request + /// comma-separated preferences that can be passed down to an individual request + [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "")] + private static string GetPreferencesToInheritFromBatch(string batchPreferences) + { + IEnumerable preferencesToInherit = SplitPreferences(batchPreferences) + .Where(value => + !nonInheritablePreferences.Any( + prefToIgnore => + value.ToLowerInvariant().StartsWith(prefToIgnore, StringComparison.OrdinalIgnoreCase) + ) + ); + return string.Join(",", preferencesToInherit); + } - /// - /// Extract preferences that can be inherited from the overall batch request to - /// an individual request. - /// - /// The value of the Prefer header from the batch request - /// comma-separated preferences that can be passed down to an individual request - [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "")] - private static string GetPreferencesToInheritFromBatch(string batchPreferences) + /// + /// Merges the preferences from the batch request and an individual request inside the batch into one value. + /// If a given preference is defined in both the batch and individual request, the one from the individual + /// request is retained and the one from the batch is discarded. + /// + /// The value of the Prefer header from the individual request inside the batch + /// The value of the Prefer header from the overall batch request + /// Value containing the combined preferences + private static string MergeIndividualAndBatchPreferences(string individualPreferences, string batchPreferences) + { + if (string.IsNullOrEmpty(individualPreferences)) { - IEnumerable preferencesToInherit = SplitPreferences(batchPreferences) - .Where(value => - !nonInheritablePreferences.Any( - prefToIgnore => - value.ToLowerInvariant().StartsWith(prefToIgnore, StringComparison.OrdinalIgnoreCase) - ) - ); - return string.Join(",", preferencesToInherit); + return batchPreferences; } - /// - /// Merges the preferences from the batch request and an individual request inside the batch into one value. - /// If a given preference is defined in both the batch and individual request, the one from the individual - /// request is retained and the one from the batch is discarded. - /// - /// The value of the Prefer header from the individual request inside the batch - /// The value of the Prefer header from the overall batch request - /// Value containing the combined preferences - private static string MergeIndividualAndBatchPreferences(string individualPreferences, string batchPreferences) + if (string.IsNullOrEmpty(batchPreferences)) { - if (string.IsNullOrEmpty(individualPreferences)) - { - return batchPreferences; - } + return individualPreferences; + } + // get the name of each preference to avoid adding duplicates from batch + IEnumerable individualList = SplitPreferences(individualPreferences); + HashSet individualPreferenceNames = new HashSet(individualList.Select(pref => pref.Split('=').FirstOrDefault())); - if (string.IsNullOrEmpty(batchPreferences)) - { - return individualPreferences; - } - // get the name of each preference to avoid adding duplicates from batch - IEnumerable individualList = SplitPreferences(individualPreferences); - HashSet individualPreferenceNames = new HashSet(individualList.Select(pref => pref.Split('=').FirstOrDefault())); + IEnumerable filteredBatchList = SplitPreferences(batchPreferences) + // do not add duplicate preferences from batch + .Where(pref => !individualPreferenceNames.Contains(pref.Split('=').FirstOrDefault())); + string filteredBatchPreferences = string.Join(",", filteredBatchList); - IEnumerable filteredBatchList = SplitPreferences(batchPreferences) - // do not add duplicate preferences from batch - .Where(pref => !individualPreferenceNames.Contains(pref.Split('=').FirstOrDefault())); - string filteredBatchPreferences = string.Join(",", filteredBatchList); + if (string.IsNullOrEmpty(filteredBatchPreferences)) + { + return individualPreferences; + } - if (string.IsNullOrEmpty(filteredBatchPreferences)) - { - return individualPreferences; - } + return string.Join(",", individualPreferences, filteredBatchPreferences); + } - return string.Join(",", individualPreferences, filteredBatchPreferences); - } + /// + /// Splits the value of a Prefer header into separate preferences + /// e.g. a value like 'a, b=c, foo="bar,baz"' will return an IEnumerable with + /// - a + /// - b=c + /// - foo="bar,baz" + /// + /// + /// + private static IEnumerable SplitPreferences(string preferences) + { + int preferenceStartIndex = 0; - /// - /// Splits the value of a Prefer header into separate preferences - /// e.g. a value like 'a, b=c, foo="bar,baz"' will return an IEnumerable with - /// - a - /// - b=c - /// - foo="bar,baz" - /// - /// - /// - private static IEnumerable SplitPreferences(string preferences) + HashSet addedPreferences = new HashSet(); + bool insideQuotedValue = false; + for (int currentIndex = 0; currentIndex < preferences.Length; currentIndex++) { - int preferenceStartIndex = 0; - - HashSet addedPreferences = new HashSet(); - bool insideQuotedValue = false; - for (int currentIndex = 0; currentIndex < preferences.Length; currentIndex++) + char c = preferences[currentIndex]; + if (c == '"') { - char c = preferences[currentIndex]; - if (c == '"') + if (!insideQuotedValue) { - if (!insideQuotedValue) - { - // we are starting a double-quoted value - insideQuotedValue = true; - } - else - { - // this could be the end of a quoted value, or it could be an escaped quote - // we're sure that currentIndex > 0 here since insideQuotedValue is true, so need to check for bounds - insideQuotedValue = preferences[currentIndex - 1] == '\\'; - } + // we are starting a double-quoted value + insideQuotedValue = true; } - else if (c == ',' && !insideQuotedValue) + else { - string result = preferences.Substring(preferenceStartIndex, currentIndex - preferenceStartIndex).Trim(); - string prefName = result.Split('=')[0].Trim(); - // do not add duplicate preference - if (!addedPreferences.Contains(prefName)) - { - addedPreferences.Add(prefName); - yield return result; - } - - preferenceStartIndex = currentIndex + 1; + // this could be the end of a quoted value, or it could be an escaped quote + // we're sure that currentIndex > 0 here since insideQuotedValue is true, so need to check for bounds + insideQuotedValue = preferences[currentIndex - 1] == '\\'; } } - - if (preferences.Length > preferenceStartIndex + 1) + else if (c == ',' && !insideQuotedValue) { - yield return preferences.Substring(preferenceStartIndex).Trim(); + string result = preferences.Substring(preferenceStartIndex, currentIndex - preferenceStartIndex).Trim(); + string prefName = result.Split('=')[0].Trim(); + // do not add duplicate preference + if (!addedPreferences.Contains(prefName)) + { + addedPreferences.Add(prefName); + yield return result; + } + + preferenceStartIndex = currentIndex + 1; } } + + if (preferences.Length > preferenceStartIndex + 1) + { + yield return preferences.Substring(preferenceStartIndex).Trim(); + } } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchRequestItem.cs b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchRequestItem.cs index c7bf3b4d7..c4c40462a 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchRequestItem.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchRequestItem.cs @@ -14,93 +14,92 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.OData.Formatter; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// Represents an OData batch request. +/// +public abstract class ODataBatchRequestItem { /// - /// Represents an OData batch request. + /// Routes a single OData batch request. /// - public abstract class ODataBatchRequestItem + /// The handler for processing a message. + /// The http context. + /// The Content-ID to Location mapping. + /// + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "")] + public static async Task SendRequestAsync(RequestDelegate handler, HttpContext context, IDictionary contentIdToLocationMapping) { - /// - /// Routes a single OData batch request. - /// - /// The handler for processing a message. - /// The http context. - /// The Content-ID to Location mapping. - /// - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "")] - public static async Task SendRequestAsync(RequestDelegate handler, HttpContext context, IDictionary contentIdToLocationMapping) + if (handler == null) { - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } + throw new ArgumentNullException(nameof(handler)); + } - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } - if (contentIdToLocationMapping != null) + if (contentIdToLocationMapping != null) + { + string encodedUrl = context.Request.GetEncodedUrl(); + string resolvedRequestUrl = ContentIdHelpers.ResolveContentId(encodedUrl, contentIdToLocationMapping); + Uri resolvedUri; + if (!string.IsNullOrEmpty(resolvedRequestUrl) + && Uri.TryCreate(resolvedRequestUrl, UriKind.Absolute, out resolvedUri)) { - string encodedUrl = context.Request.GetEncodedUrl(); - string resolvedRequestUrl = ContentIdHelpers.ResolveContentId(encodedUrl, contentIdToLocationMapping); - Uri resolvedUri; - if (!string.IsNullOrEmpty(resolvedRequestUrl) - && Uri.TryCreate(resolvedRequestUrl, UriKind.Absolute, out resolvedUri)) - { - context.Request.CopyAbsoluteUrl(resolvedUri); - } - - context.Request.SetODataContentIdMapping(contentIdToLocationMapping); + context.Request.CopyAbsoluteUrl(resolvedUri); } - try - { - await handler(context).ConfigureAwait(false); + context.Request.SetODataContentIdMapping(contentIdToLocationMapping); + } - string contentId = context.Request.GetODataContentId(); + try + { + await handler(context).ConfigureAwait(false); - if (contentIdToLocationMapping != null && contentId != null) - { - AddLocationHeaderToMapping(context.Response, contentIdToLocationMapping, contentId); - } - } - catch (Exception) + string contentId = context.Request.GetODataContentId(); + + if (contentIdToLocationMapping != null && contentId != null) { - // Unlike AspNet, the exception handling is (by default) upstream of this middleware - // so we need to trap exceptions on our own. This code is similar to the - // ExceptionHandlerMiddleware class in AspNetCore. - context.Response.Clear(); - context.Response.StatusCode = StatusCodes.Status500InternalServerError; + AddLocationHeaderToMapping(context.Response, contentIdToLocationMapping, contentId); } } + catch (Exception) + { + // Unlike AspNet, the exception handling is (by default) upstream of this middleware + // so we need to trap exceptions on our own. This code is similar to the + // ExceptionHandlerMiddleware class in AspNetCore. + context.Response.Clear(); + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + } + } - /// - /// Routes the request. - /// - /// The handler for processing a message. - /// A . - public abstract Task SendRequestAsync(RequestDelegate handler); + /// + /// Routes the request. + /// + /// The handler for processing a message. + /// A . + public abstract Task SendRequestAsync(RequestDelegate handler); - /// - /// Default dictionary that ODataBatchRequestItems should use when mapping ID references to URLs - /// - [SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Collection needs to be provided externally.")] - public IDictionary ContentIdToLocationMapping { get; set; } + /// + /// Default dictionary that ODataBatchRequestItems should use when mapping ID references to URLs + /// + [SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "Collection needs to be provided externally.")] + public IDictionary ContentIdToLocationMapping { get; set; } - private static void AddLocationHeaderToMapping(HttpResponse response, IDictionary contentIdToLocationMapping, string contentId) - { - Contract.Assert(response != null); - Contract.Assert(response.Headers != null); - Contract.Assert(contentIdToLocationMapping != null); - Contract.Assert(contentId != null); + private static void AddLocationHeaderToMapping(HttpResponse response, IDictionary contentIdToLocationMapping, string contentId) + { + Contract.Assert(response != null); + Contract.Assert(response.Headers != null); + Contract.Assert(contentIdToLocationMapping != null); + Contract.Assert(contentId != null); - var headers = response.GetTypedHeaders(); - if (headers.Location != null) - { - contentIdToLocationMapping.Add(contentId, headers.Location.AbsoluteUri); - } + var headers = response.GetTypedHeaders(); + if (headers.Location != null) + { + contentIdToLocationMapping.Add(contentId, headers.Location.AbsoluteUri); } } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchResponseItem.cs b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchResponseItem.cs index df24ac2f6..43616808b 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchResponseItem.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchResponseItem.cs @@ -12,68 +12,67 @@ using Microsoft.Extensions.Primitives; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// Represents an OData batch response. +/// +public abstract class ODataBatchResponseItem { /// - /// Represents an OData batch response. + /// Writes a single OData batch response. /// - public abstract class ODataBatchResponseItem + /// The . + /// The message context. + public static async Task WriteMessageAsync(ODataBatchWriter writer, HttpContext context) { - /// - /// Writes a single OData batch response. - /// - /// The . - /// The message context. - public static async Task WriteMessageAsync(ODataBatchWriter writer, HttpContext context) + if (writer == null) { - if (writer == null) - { - throw Error.ArgumentNull(nameof(writer)); - } + throw Error.ArgumentNull(nameof(writer)); + } - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - string contentId = (context.Request != null) ? context.Request.GetODataContentId() : string.Empty; + string contentId = (context.Request != null) ? context.Request.GetODataContentId() : string.Empty; - ODataBatchOperationResponseMessage batchResponse = await writer.CreateOperationResponseMessageAsync(contentId).ConfigureAwait(false); + ODataBatchOperationResponseMessage batchResponse = await writer.CreateOperationResponseMessageAsync(contentId).ConfigureAwait(false); - batchResponse.StatusCode = context.Response.StatusCode; + batchResponse.StatusCode = context.Response.StatusCode; - foreach (KeyValuePair header in context.Response.Headers) - { - batchResponse.SetHeader(header.Key, string.Join(",", header.Value.ToArray())); - } + foreach (KeyValuePair header in context.Response.Headers) + { + batchResponse.SetHeader(header.Key, string.Join(",", header.Value.ToArray())); + } - if (context.Response.Body != null && context.Response.Body.Length != 0) + if (context.Response.Body != null && context.Response.Body.Length != 0) + { + using (Stream stream = await batchResponse.GetStreamAsync().ConfigureAwait(false)) { - using (Stream stream = await batchResponse.GetStreamAsync().ConfigureAwait(false)) - { - context.RequestAborted.ThrowIfCancellationRequested(); - context.Response.Body.Seek(0L, SeekOrigin.Begin); - await context.Response.Body.CopyToAsync(stream).ConfigureAwait(false); + context.RequestAborted.ThrowIfCancellationRequested(); + context.Response.Body.Seek(0L, SeekOrigin.Begin); + await context.Response.Body.CopyToAsync(stream).ConfigureAwait(false); - // Close and release the stream for the individual response - ODataBatchStream batchStream = context.Response.Body as ODataBatchStream; - if (batchStream != null) - { - await batchStream.InternalDisposeAsync().ConfigureAwait(false); - } + // Close and release the stream for the individual response + ODataBatchStream batchStream = context.Response.Body as ODataBatchStream; + if (batchStream != null) + { + await batchStream.InternalDisposeAsync().ConfigureAwait(false); } } } + } - /// - /// Writes the response. - /// - /// The . - public abstract Task WriteResponseAsync(ODataBatchWriter writer); + /// + /// Writes the response. + /// + /// The . + public abstract Task WriteResponseAsync(ODataBatchWriter writer); - /// - /// Gets a value that indicates if the responses in this item are successful. - /// - internal abstract bool IsResponseSuccessful(); - } + /// + /// Gets a value that indicates if the responses in this item are successful. + /// + internal abstract bool IsResponseSuccessful(); } diff --git a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchStream.cs b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchStream.cs index b9686b4aa..582f6db3a 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/ODataBatchStream.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/ODataBatchStream.cs @@ -8,50 +8,49 @@ using System.IO; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +// Normally, the stream is closed when the ODataMessageWriter is disposed. +// For responses within a batch, we need to read from the stream after the response is +// written in order to write it to the batch response stream. So we need to ignore the Close() +// and provide an alternate method to release the stream after writing its content to the batch response. +internal class ODataBatchStream : MemoryStream { - // Normally, the stream is closed when the ODataMessageWriter is disposed. - // For responses within a batch, we need to read from the stream after the response is - // written in order to write it to the batch response stream. So we need to ignore the Close() - // and provide an alternate method to release the stream after writing its content to the batch response. - internal class ODataBatchStream : MemoryStream - { - private bool isDisposed = false; + private bool isDisposed = false; - /// - /// Dispose the batch stream and underlying resources - /// - internal void InternalDispose() + /// + /// Dispose the batch stream and underlying resources + /// + internal void InternalDispose() + { + if (!isDisposed) { - if (!isDisposed) - { - base.Flush(); - base.Close(); - base.Dispose(); - isDisposed = true; - } + base.Flush(); + base.Close(); + base.Dispose(); + isDisposed = true; } + } - /// - /// Dispose the batch stream and underlying resources - /// - internal async Task InternalDisposeAsync() + /// + /// Dispose the batch stream and underlying resources + /// + internal async Task InternalDisposeAsync() + { + if (!isDisposed) { - if (!isDisposed) - { - await base.FlushAsync().ConfigureAwait(false); - base.Close(); - base.Dispose(); - isDisposed = true; - } + await base.FlushAsync().ConfigureAwait(false); + base.Close(); + base.Dispose(); + isDisposed = true; } + } - /// - /// Override Close() in order to hold the stream open until we are able to - /// copy it to the batch response stream. - /// - public override void Close() - { - } + /// + /// Override Close() in order to hold the stream open until we are able to + /// copy it to the batch response stream. + /// + public override void Close() + { } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/OperationRequestItem.cs b/src/Microsoft.AspNetCore.OData/Batch/OperationRequestItem.cs index ea980008d..2f7b0551c 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/OperationRequestItem.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/OperationRequestItem.cs @@ -9,42 +9,41 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// Represents an Operation request. +/// +public class OperationRequestItem : ODataBatchRequestItem { /// - /// Represents an Operation request. + /// Initializes a new instance of the class. /// - public class OperationRequestItem : ODataBatchRequestItem + /// The Operation http context. + public OperationRequestItem(HttpContext context) { - /// - /// Initializes a new instance of the class. - /// - /// The Operation http context. - public OperationRequestItem(HttpContext context) - { - Context = context ?? throw new ArgumentNullException(nameof(context)); - } + Context = context ?? throw new ArgumentNullException(nameof(context)); + } - /// - /// Gets the Operation request context. - /// - public HttpContext Context { get; } + /// + /// Gets the Operation request context. + /// + public HttpContext Context { get; } - /// - /// Sends the Operation request. - /// - /// The handler for processing a Http request. - /// A . - public override async Task SendRequestAsync(RequestDelegate handler) + /// + /// Sends the Operation request. + /// + /// The handler for processing a Http request. + /// A . + public override async Task SendRequestAsync(RequestDelegate handler) + { + if (handler == null) { - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } + throw new ArgumentNullException(nameof(handler)); + } - await SendRequestAsync(handler, Context, this.ContentIdToLocationMapping).ConfigureAwait(false); + await SendRequestAsync(handler, Context, this.ContentIdToLocationMapping).ConfigureAwait(false); - return new OperationResponseItem(Context); - } + return new OperationResponseItem(Context); } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/OperationResponseItem.cs b/src/Microsoft.AspNetCore.OData/Batch/OperationResponseItem.cs index f9c57c41e..98d208e16 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/OperationResponseItem.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/OperationResponseItem.cs @@ -11,47 +11,46 @@ using Microsoft.AspNetCore.OData.Extensions; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// Represents an Operation response. +/// +public class OperationResponseItem : ODataBatchResponseItem { /// - /// Represents an Operation response. + /// Initializes a new instance of the class. /// - public class OperationResponseItem : ODataBatchResponseItem + /// The response context for the Operation request. + public OperationResponseItem(HttpContext context) { - /// - /// Initializes a new instance of the class. - /// - /// The response context for the Operation request. - public OperationResponseItem(HttpContext context) - { - Context = context ?? throw new ArgumentNullException(nameof(context)); - } + Context = context ?? throw new ArgumentNullException(nameof(context)); + } - /// - /// Gets the response messages for the Operation. - /// - public HttpContext Context { get; private set; } + /// + /// Gets the response messages for the Operation. + /// + public HttpContext Context { get; private set; } - /// - /// Writes the response as an Operation. - /// - /// The . - public override Task WriteResponseAsync(ODataBatchWriter writer) + /// + /// Writes the response as an Operation. + /// + /// The . + public override Task WriteResponseAsync(ODataBatchWriter writer) + { + if (writer == null) { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - return WriteMessageAsync(writer, Context); + throw new ArgumentNullException(nameof(writer)); } - /// - /// Gets a value that indicates if the responses in this item are successful. - /// - internal override bool IsResponseSuccessful() - { - return Context.Response.IsSuccessStatusCode(); - } + return WriteMessageAsync(writer, Context); + } + + /// + /// Gets a value that indicates if the responses in this item are successful. + /// + internal override bool IsResponseSuccessful() + { + return Context.Response.IsSuccessStatusCode(); } } diff --git a/src/Microsoft.AspNetCore.OData/Batch/UnbufferedODataBatchHandler.cs b/src/Microsoft.AspNetCore.OData/Batch/UnbufferedODataBatchHandler.cs index bbe96a95b..d8df27db5 100644 --- a/src/Microsoft.AspNetCore.OData/Batch/UnbufferedODataBatchHandler.cs +++ b/src/Microsoft.AspNetCore.OData/Batch/UnbufferedODataBatchHandler.cs @@ -16,163 +16,162 @@ using Microsoft.Extensions.Options; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Batch +namespace Microsoft.AspNetCore.OData.Batch; + +/// +/// An implementation of that doesn't buffer the request content stream. +/// +public class UnbufferedODataBatchHandler : ODataBatchHandler { - /// - /// An implementation of that doesn't buffer the request content stream. - /// - public class UnbufferedODataBatchHandler : ODataBatchHandler + /// + public override async Task ProcessBatchAsync(HttpContext context, RequestDelegate nextHandler) { - /// - public override async Task ProcessBatchAsync(HttpContext context, RequestDelegate nextHandler) + if (context == null) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } + throw new ArgumentNullException(nameof(context)); + } - if (nextHandler == null) - { - throw new ArgumentNullException(nameof(nextHandler)); - } + if (nextHandler == null) + { + throw new ArgumentNullException(nameof(nextHandler)); + } - if (!await ValidateRequest(context.Request).ConfigureAwait(false)) - { - return; - } + if (!await ValidateRequest(context.Request).ConfigureAwait(false)) + { + return; + } - // This container is for the overall batch request. - HttpRequest request = context.Request; - IServiceProvider requestContainer = request.CreateRouteServices(PrefixName); - requestContainer.GetRequiredService().BaseUri = GetBaseUri(request); - List responses = new List(); + // This container is for the overall batch request. + HttpRequest request = context.Request; + IServiceProvider requestContainer = request.CreateRouteServices(PrefixName); + requestContainer.GetRequiredService().BaseUri = GetBaseUri(request); + List responses = new List(); - using (ODataMessageReader reader = request.GetODataMessageReader(requestContainer)) - { - ODataBatchReader batchReader = await reader.CreateODataBatchReaderAsync().ConfigureAwait(false); - Guid batchId = Guid.NewGuid(); + using (ODataMessageReader reader = request.GetODataMessageReader(requestContainer)) + { + ODataBatchReader batchReader = await reader.CreateODataBatchReaderAsync().ConfigureAwait(false); + Guid batchId = Guid.NewGuid(); - ODataOptions options = context.RequestServices.GetRequiredService>().Value; - bool enableContinueOnErrorHeader = (options != null) - ? options.EnableContinueOnErrorHeader - : false; + ODataOptions options = context.RequestServices.GetRequiredService>().Value; + bool enableContinueOnErrorHeader = (options != null) + ? options.EnableContinueOnErrorHeader + : false; - SetContinueOnError(request.Headers, enableContinueOnErrorHeader); + SetContinueOnError(request.Headers, enableContinueOnErrorHeader); - while (await batchReader.ReadAsync().ConfigureAwait(false)) + while (await batchReader.ReadAsync().ConfigureAwait(false)) + { + ODataBatchResponseItem responseItem = null; + if (batchReader.State == ODataBatchReaderState.ChangesetStart) { - ODataBatchResponseItem responseItem = null; - if (batchReader.State == ODataBatchReaderState.ChangesetStart) - { - responseItem = await ExecuteChangeSetAsync(batchReader, batchId, request, nextHandler).ConfigureAwait(false); - } - else if (batchReader.State == ODataBatchReaderState.Operation) - { - responseItem = await ExecuteOperationAsync(batchReader, batchId, request, nextHandler).ConfigureAwait(false); - } - if (responseItem != null) + responseItem = await ExecuteChangeSetAsync(batchReader, batchId, request, nextHandler).ConfigureAwait(false); + } + else if (batchReader.State == ODataBatchReaderState.Operation) + { + responseItem = await ExecuteOperationAsync(batchReader, batchId, request, nextHandler).ConfigureAwait(false); + } + if (responseItem != null) + { + responses.Add(responseItem); + if (responseItem.IsResponseSuccessful() == false && ContinueOnError == false) { - responses.Add(responseItem); - if (responseItem.IsResponseSuccessful() == false && ContinueOnError == false) - { - break; - } + break; } } } + } - await CreateResponseMessageAsync(responses, request).ConfigureAwait(false); + await CreateResponseMessageAsync(responses, request).ConfigureAwait(false); + } + + /// + /// Executes the operation. + /// + /// The batch reader. + /// The batch id. + /// The original request containing all the batch requests. + /// The handler for processing a message. + /// The response for the operation. + public virtual async Task ExecuteOperationAsync(ODataBatchReader batchReader, Guid batchId, HttpRequest originalRequest, RequestDelegate handler) + { + if (batchReader == null) + { + throw new ArgumentNullException(nameof(batchReader)); } - /// - /// Executes the operation. - /// - /// The batch reader. - /// The batch id. - /// The original request containing all the batch requests. - /// The handler for processing a message. - /// The response for the operation. - public virtual async Task ExecuteOperationAsync(ODataBatchReader batchReader, Guid batchId, HttpRequest originalRequest, RequestDelegate handler) + if (originalRequest == null) { - if (batchReader == null) - { - throw new ArgumentNullException(nameof(batchReader)); - } + throw new ArgumentNullException(nameof(originalRequest)); + } - if (originalRequest == null) - { - throw new ArgumentNullException(nameof(originalRequest)); - } + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } + CancellationToken cancellationToken = originalRequest.HttpContext.RequestAborted; + cancellationToken.ThrowIfCancellationRequested(); + HttpContext operationContext = await batchReader.ReadOperationRequestAsync(originalRequest.HttpContext, batchId, cancellationToken).ConfigureAwait(false); - CancellationToken cancellationToken = originalRequest.HttpContext.RequestAborted; - cancellationToken.ThrowIfCancellationRequested(); - HttpContext operationContext = await batchReader.ReadOperationRequestAsync(originalRequest.HttpContext, batchId, cancellationToken).ConfigureAwait(false); + operationContext.Request.ClearRouteServices(); + OperationRequestItem operation = new OperationRequestItem(operationContext); + operation.ContentIdToLocationMapping = originalRequest.HttpContext.ODataBatchFeature().ContentIdMapping; - operationContext.Request.ClearRouteServices(); - OperationRequestItem operation = new OperationRequestItem(operationContext); - operation.ContentIdToLocationMapping = originalRequest.HttpContext.ODataBatchFeature().ContentIdMapping; + ODataBatchResponseItem responseItem = await operation.SendRequestAsync(handler).ConfigureAwait(false); - ODataBatchResponseItem responseItem = await operation.SendRequestAsync(handler).ConfigureAwait(false); + return responseItem; + } - return responseItem; + /// + /// Executes the ChangeSet. + /// + /// The batch reader. + /// The batch id. + /// The original request containing all the batch requests. + /// The handler for processing a message. + /// The response for the ChangeSet. + public virtual async Task ExecuteChangeSetAsync(ODataBatchReader batchReader, Guid batchId, HttpRequest originalRequest, RequestDelegate handler) + { + if (batchReader == null) + { + throw new ArgumentNullException(nameof(batchReader)); } - /// - /// Executes the ChangeSet. - /// - /// The batch reader. - /// The batch id. - /// The original request containing all the batch requests. - /// The handler for processing a message. - /// The response for the ChangeSet. - public virtual async Task ExecuteChangeSetAsync(ODataBatchReader batchReader, Guid batchId, HttpRequest originalRequest, RequestDelegate handler) + if (originalRequest == null) { - if (batchReader == null) - { - throw new ArgumentNullException(nameof(batchReader)); - } - - if (originalRequest == null) - { - throw new ArgumentNullException(nameof(originalRequest)); - } + throw new ArgumentNullException(nameof(originalRequest)); + } - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } - Guid changeSetId = Guid.NewGuid(); - List changeSetResponse = new List(); + Guid changeSetId = Guid.NewGuid(); + List changeSetResponse = new List(); - IDictionary contentIdToLocationMapping = originalRequest.HttpContext.ODataBatchFeature().ContentIdMapping; - while (await batchReader.ReadAsync().ConfigureAwait(false) && batchReader.State != ODataBatchReaderState.ChangesetEnd) + IDictionary contentIdToLocationMapping = originalRequest.HttpContext.ODataBatchFeature().ContentIdMapping; + while (await batchReader.ReadAsync().ConfigureAwait(false) && batchReader.State != ODataBatchReaderState.ChangesetEnd) + { + if (batchReader.State == ODataBatchReaderState.Operation) { - if (batchReader.State == ODataBatchReaderState.Operation) - { - CancellationToken cancellationToken = originalRequest.HttpContext.RequestAborted; - HttpContext changeSetOperationContext = await batchReader.ReadChangeSetOperationRequestAsync(originalRequest.HttpContext, batchId, changeSetId, cancellationToken).ConfigureAwait(false); + CancellationToken cancellationToken = originalRequest.HttpContext.RequestAborted; + HttpContext changeSetOperationContext = await batchReader.ReadChangeSetOperationRequestAsync(originalRequest.HttpContext, batchId, changeSetId, cancellationToken).ConfigureAwait(false); - await ODataBatchRequestItem.SendRequestAsync(handler, changeSetOperationContext, contentIdToLocationMapping).ConfigureAwait(false); - if (changeSetOperationContext.Response.IsSuccessStatusCode()) - { - changeSetResponse.Add(changeSetOperationContext); - } - else - { - changeSetResponse.Clear(); - changeSetResponse.Add(changeSetOperationContext); - return new ChangeSetResponseItem(changeSetResponse); - } + await ODataBatchRequestItem.SendRequestAsync(handler, changeSetOperationContext, contentIdToLocationMapping).ConfigureAwait(false); + if (changeSetOperationContext.Response.IsSuccessStatusCode()) + { + changeSetResponse.Add(changeSetOperationContext); + } + else + { + changeSetResponse.Clear(); + changeSetResponse.Add(changeSetOperationContext); + return new ChangeSetResponseItem(changeSetResponse); } } - - return new ChangeSetResponseItem(changeSetResponse); } + + return new ChangeSetResponseItem(changeSetResponse); } } diff --git a/src/Microsoft.AspNetCore.OData/Common/CollectionExtensions.cs b/src/Microsoft.AspNetCore.OData/Common/CollectionExtensions.cs index 2d0dd23b7..02c174148 100644 --- a/src/Microsoft.AspNetCore.OData/Common/CollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Common/CollectionExtensions.cs @@ -7,19 +7,18 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.Common +namespace Microsoft.AspNetCore.OData.Common; + +/// +/// Helper extension methods for fast use of collections. +/// +internal static class CollectionExtensions { - /// - /// Helper extension methods for fast use of collections. - /// - internal static class CollectionExtensions + public static void MergeWithReplace(this Dictionary target, Dictionary source) { - public static void MergeWithReplace(this Dictionary target, Dictionary source) + foreach (var kvp in source) { - foreach (var kvp in source) - { - target[kvp.Key] = kvp.Value; - } + target[kvp.Key] = kvp.Value; } } } diff --git a/src/Microsoft.AspNetCore.OData/Common/Error.cs b/src/Microsoft.AspNetCore.OData/Common/Error.cs index 1caff0eaa..0e3113ec2 100644 --- a/src/Microsoft.AspNetCore.OData/Common/Error.cs +++ b/src/Microsoft.AspNetCore.OData/Common/Error.cs @@ -11,223 +11,222 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; -namespace Microsoft.AspNetCore.OData +namespace Microsoft.AspNetCore.OData; + +/// +/// Utility class for creating and unwrapping instances. +/// +[ExcludeFromCodeCoverage] +internal static class Error { /// - /// Utility class for creating and unwrapping instances. - /// - [ExcludeFromCodeCoverage] - internal static class Error - { - /// - /// Formats the specified resource string using . - /// - /// A composite format string. - /// An object array that contains zero or more objects to format. - /// The formatted string. - internal static string Format(string format, params object[] args) - { - return String.Format(CultureInfo.CurrentCulture, format, args); - } - - /// - /// Creates an with the provided properties. - /// - /// A composite format string explaining the reason for the exception. - /// An object array that contains zero or more objects to format. - /// The logged . - internal static ArgumentException Argument(string messageFormat, params object[] messageArgs) - { - return new ArgumentException(Error.Format(messageFormat, messageArgs)); - } - - /// - /// Creates an with the provided properties. - /// - /// The name of the parameter that caused the current exception. - /// A composite format string explaining the reason for the exception. - /// An object array that contains zero or more objects to format. - /// The logged . - internal static ArgumentException Argument(string parameterName, string messageFormat, params object[] messageArgs) - { - return new ArgumentException(Error.Format(messageFormat, messageArgs), parameterName); - } - - /// - /// Creates an with the provided properties. - /// - /// The logged . - [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The purpose of this API is to return an error for properties")] - internal static ArgumentNullException PropertyNull() - { - return new ArgumentNullException("value"); - } - - /// - /// Creates an with the provided properties. - /// - /// The name of the parameter that caused the current exception. - /// The logged . - internal static ArgumentNullException ArgumentNull(string parameterName) - { - return new ArgumentNullException(parameterName); - } - - /// - /// Creates an with the provided properties. - /// - /// The name of the parameter that caused the current exception. - /// A composite format string explaining the reason for the exception. - /// An object array that contains zero or more objects to format. - /// The logged . - internal static ArgumentNullException ArgumentNull(string parameterName, string messageFormat, params object[] messageArgs) - { - return new ArgumentNullException(parameterName, Error.Format(messageFormat, messageArgs)); - } - - /// - /// Creates an with a default message. - /// - /// The name of the parameter that caused the current exception. - /// The logged . - internal static ArgumentException ArgumentNullOrEmpty(string parameterName) - { - return Error.Argument(parameterName, SRResources.ArgumentNullOrEmpty, parameterName); - } - - /// - /// Creates an with the provided properties. - /// - /// The name of the parameter that caused the current exception. - /// The value of the argument that causes this exception. - /// A composite format string explaining the reason for the exception. - /// An object array that contains zero or more objects to format. - /// The logged . - internal static ArgumentOutOfRangeException ArgumentOutOfRange(string parameterName, object actualValue, string messageFormat, params object[] messageArgs) - { - return new ArgumentOutOfRangeException(parameterName, actualValue, Error.Format(messageFormat, messageArgs)); - } - - /// - /// Creates an with a message saying that the argument must be greater than or equal to . - /// - /// The name of the parameter that caused the current exception. - /// The value of the argument that causes this exception. - /// The minimum size of the argument. - /// The logged . - internal static ArgumentOutOfRangeException ArgumentMustBeGreaterThanOrEqualTo(string parameterName, object actualValue, object minValue) - { - return new ArgumentOutOfRangeException(parameterName, actualValue, Error.Format(SRResources.ArgumentMustBeGreaterThanOrEqualTo, minValue)); - } - - /// - /// Creates an with a message saying that the argument must be less than or equal to . - /// - /// The name of the parameter that caused the current exception. - /// The value of the argument that causes this exception. - /// The maximum size of the argument. - /// The logged . - internal static ArgumentOutOfRangeException ArgumentMustBeLessThanOrEqualTo(string parameterName, object actualValue, object maxValue) - { - return new ArgumentOutOfRangeException(parameterName, actualValue, Error.Format(SRResources.ArgumentMustBeLessThanOrEqualTo, maxValue)); - } - - /// - /// Creates an with a message saying that the key was not found. - /// - /// The logged . - internal static KeyNotFoundException KeyNotFound() - { - return new KeyNotFoundException(); - } - - /// - /// Creates an with a message saying that the key was not found. - /// - /// A composite format string explaining the reason for the exception. - /// An object array that contains zero or more objects to format. - /// The logged . - internal static KeyNotFoundException KeyNotFound(string messageFormat, params object[] messageArgs) - { - return new KeyNotFoundException(Error.Format(messageFormat, messageArgs)); - } - - /// - /// Creates an initialized according to guidelines. - /// - /// A composite format string explaining the reason for the exception. - /// An object array that contains zero or more objects to format. - /// The logged . - internal static ObjectDisposedException ObjectDisposed(string messageFormat, params object[] messageArgs) - { - // Pass in null, not disposedObject.GetType().FullName as per the above guideline - return new ObjectDisposedException(null, Error.Format(messageFormat, messageArgs)); - } - - /// - /// Creates an initialized with the provided parameters. - /// - /// The logged . - internal static OperationCanceledException OperationCanceled() - { - return new OperationCanceledException(); - } - - /// - /// Creates an initialized with the provided parameters. - /// - /// A composite format string explaining the reason for the exception. - /// An object array that contains zero or more objects to format. - /// The logged . - internal static OperationCanceledException OperationCanceled(string messageFormat, params object[] messageArgs) - { - return new OperationCanceledException(Error.Format(messageFormat, messageArgs)); - } - - /// - /// Creates an for an invalid enum argument. - /// - /// The name of the parameter that caused the current exception. - /// The value of the argument that failed. - /// A that represents the enumeration class with the valid values. - /// The logged . - internal static ArgumentException InvalidEnumArgument(string parameterName, int invalidValue, Type enumClass) - { - return new InvalidEnumArgumentException(parameterName, invalidValue, enumClass); - } - - /// - /// Creates an . - /// - /// A composite format string explaining the reason for the exception. - /// An object array that contains zero or more objects to format. - /// The logged . - internal static InvalidOperationException InvalidOperation(string messageFormat, params object[] messageArgs) - { - return new InvalidOperationException(Error.Format(messageFormat, messageArgs)); - } - - /// - /// Creates an . - /// - /// Inner exception - /// A composite format string explaining the reason for the exception. - /// An object array that contains zero or more objects to format. - /// The logged . - internal static InvalidOperationException InvalidOperation(Exception innerException, string messageFormat, params object[] messageArgs) - { - return new InvalidOperationException(Error.Format(messageFormat, messageArgs), innerException); - } - - /// - /// Creates an . - /// - /// A composite format string explaining the reason for the exception. - /// An object array that contains zero or more objects to format. - /// The logged . - internal static NotSupportedException NotSupported(string messageFormat, params object[] messageArgs) - { - return new NotSupportedException(Error.Format(messageFormat, messageArgs)); - } + /// Formats the specified resource string using . + /// + /// A composite format string. + /// An object array that contains zero or more objects to format. + /// The formatted string. + internal static string Format(string format, params object[] args) + { + return String.Format(CultureInfo.CurrentCulture, format, args); + } + + /// + /// Creates an with the provided properties. + /// + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static ArgumentException Argument(string messageFormat, params object[] messageArgs) + { + return new ArgumentException(Error.Format(messageFormat, messageArgs)); + } + + /// + /// Creates an with the provided properties. + /// + /// The name of the parameter that caused the current exception. + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static ArgumentException Argument(string parameterName, string messageFormat, params object[] messageArgs) + { + return new ArgumentException(Error.Format(messageFormat, messageArgs), parameterName); + } + + /// + /// Creates an with the provided properties. + /// + /// The logged . + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The purpose of this API is to return an error for properties")] + internal static ArgumentNullException PropertyNull() + { + return new ArgumentNullException("value"); + } + + /// + /// Creates an with the provided properties. + /// + /// The name of the parameter that caused the current exception. + /// The logged . + internal static ArgumentNullException ArgumentNull(string parameterName) + { + return new ArgumentNullException(parameterName); + } + + /// + /// Creates an with the provided properties. + /// + /// The name of the parameter that caused the current exception. + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static ArgumentNullException ArgumentNull(string parameterName, string messageFormat, params object[] messageArgs) + { + return new ArgumentNullException(parameterName, Error.Format(messageFormat, messageArgs)); + } + + /// + /// Creates an with a default message. + /// + /// The name of the parameter that caused the current exception. + /// The logged . + internal static ArgumentException ArgumentNullOrEmpty(string parameterName) + { + return Error.Argument(parameterName, SRResources.ArgumentNullOrEmpty, parameterName); + } + + /// + /// Creates an with the provided properties. + /// + /// The name of the parameter that caused the current exception. + /// The value of the argument that causes this exception. + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static ArgumentOutOfRangeException ArgumentOutOfRange(string parameterName, object actualValue, string messageFormat, params object[] messageArgs) + { + return new ArgumentOutOfRangeException(parameterName, actualValue, Error.Format(messageFormat, messageArgs)); + } + + /// + /// Creates an with a message saying that the argument must be greater than or equal to . + /// + /// The name of the parameter that caused the current exception. + /// The value of the argument that causes this exception. + /// The minimum size of the argument. + /// The logged . + internal static ArgumentOutOfRangeException ArgumentMustBeGreaterThanOrEqualTo(string parameterName, object actualValue, object minValue) + { + return new ArgumentOutOfRangeException(parameterName, actualValue, Error.Format(SRResources.ArgumentMustBeGreaterThanOrEqualTo, minValue)); + } + + /// + /// Creates an with a message saying that the argument must be less than or equal to . + /// + /// The name of the parameter that caused the current exception. + /// The value of the argument that causes this exception. + /// The maximum size of the argument. + /// The logged . + internal static ArgumentOutOfRangeException ArgumentMustBeLessThanOrEqualTo(string parameterName, object actualValue, object maxValue) + { + return new ArgumentOutOfRangeException(parameterName, actualValue, Error.Format(SRResources.ArgumentMustBeLessThanOrEqualTo, maxValue)); + } + + /// + /// Creates an with a message saying that the key was not found. + /// + /// The logged . + internal static KeyNotFoundException KeyNotFound() + { + return new KeyNotFoundException(); + } + + /// + /// Creates an with a message saying that the key was not found. + /// + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static KeyNotFoundException KeyNotFound(string messageFormat, params object[] messageArgs) + { + return new KeyNotFoundException(Error.Format(messageFormat, messageArgs)); + } + + /// + /// Creates an initialized according to guidelines. + /// + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static ObjectDisposedException ObjectDisposed(string messageFormat, params object[] messageArgs) + { + // Pass in null, not disposedObject.GetType().FullName as per the above guideline + return new ObjectDisposedException(null, Error.Format(messageFormat, messageArgs)); + } + + /// + /// Creates an initialized with the provided parameters. + /// + /// The logged . + internal static OperationCanceledException OperationCanceled() + { + return new OperationCanceledException(); + } + + /// + /// Creates an initialized with the provided parameters. + /// + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static OperationCanceledException OperationCanceled(string messageFormat, params object[] messageArgs) + { + return new OperationCanceledException(Error.Format(messageFormat, messageArgs)); + } + + /// + /// Creates an for an invalid enum argument. + /// + /// The name of the parameter that caused the current exception. + /// The value of the argument that failed. + /// A that represents the enumeration class with the valid values. + /// The logged . + internal static ArgumentException InvalidEnumArgument(string parameterName, int invalidValue, Type enumClass) + { + return new InvalidEnumArgumentException(parameterName, invalidValue, enumClass); + } + + /// + /// Creates an . + /// + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static InvalidOperationException InvalidOperation(string messageFormat, params object[] messageArgs) + { + return new InvalidOperationException(Error.Format(messageFormat, messageArgs)); + } + + /// + /// Creates an . + /// + /// Inner exception + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static InvalidOperationException InvalidOperation(Exception innerException, string messageFormat, params object[] messageArgs) + { + return new InvalidOperationException(Error.Format(messageFormat, messageArgs), innerException); + } + + /// + /// Creates an . + /// + /// A composite format string explaining the reason for the exception. + /// An object array that contains zero or more objects to format. + /// The logged . + internal static NotSupportedException NotSupported(string messageFormat, params object[] messageArgs) + { + return new NotSupportedException(Error.Format(messageFormat, messageArgs)); } } diff --git a/src/Microsoft.AspNetCore.OData/Common/ExpressionLexer.cs b/src/Microsoft.AspNetCore.OData/Common/ExpressionLexer.cs index e0316ce80..24a8348c0 100644 --- a/src/Microsoft.AspNetCore.OData/Common/ExpressionLexer.cs +++ b/src/Microsoft.AspNetCore.OData/Common/ExpressionLexer.cs @@ -8,289 +8,288 @@ using System; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Common +namespace Microsoft.AspNetCore.OData.Common; + +/// +/// Enumeration values for token kinds. +/// +internal enum ExpressionTokenKind { - /// - /// Enumeration values for token kinds. - /// - internal enum ExpressionTokenKind - { - /// Unknown. - Unknown = 0, + /// Unknown. + Unknown = 0, - /// End of text. - TextEnd, + /// End of text. + TextEnd, - /// Literal, could be an identifier, a number, null, guid, etc. - Literal, + /// Literal, could be an identifier, a number, null, guid, etc. + Literal, - /// Comma ',' - Comma, + /// Comma ',' + Comma, - /// Equal '=' - Equal - } + /// Equal '=' + Equal +} - /// - /// Use this struct to represent a lexical expression token. - /// - internal struct ExpressionToken - { - /// InternalKind of token. - public ExpressionTokenKind Kind; +/// +/// Use this struct to represent a lexical expression token. +/// +internal struct ExpressionToken +{ + /// InternalKind of token. + public ExpressionTokenKind Kind; - /// Token text. - public string Text; + /// Token text. + public string Text; - /// Position of token. - public int Position; - } + /// Position of token. + public int Position; +} - /// - /// Components to parse an expression into token. - /// - internal class ExpressionLexer - { - /// The raw input text being parsed. - protected string _rawText { get; } +/// +/// Components to parse an expression into token. +/// +internal class ExpressionLexer +{ + /// The raw input text being parsed. + protected string _rawText { get; } - /// Length of raw input text being parsed. - protected int _length { get; } + /// Length of raw input text being parsed. + protected int _length { get; } - /// Character being processed. - private char? _char; + /// Character being processed. + private char? _char; - /// Token being processed. - private ExpressionToken _token; + /// Token being processed. + private ExpressionToken _token; - /// Position on text being parsed. - private int _textPos; + /// Position on text being parsed. + private int _textPos; - /// - /// Initializes a new instance of the class. - /// - /// The expression to lexer. - public ExpressionLexer(string expression) + /// + /// Initializes a new instance of the class. + /// + /// The expression to lexer. + public ExpressionLexer(string expression) + { + if (expression == null) { - if (expression == null) - { - throw Error.ArgumentNull(nameof(expression)); - } - - _rawText = expression; - _length = expression.Length; - - SetTextPos(0); - NextToken(); + throw Error.ArgumentNull(nameof(expression)); } - /// Token being processed. - public ExpressionToken CurrentToken { get => _token; set => _token = value; } + _rawText = expression; + _length = expression.Length; - /// - /// Reads the next token, skipping whitespace as necessary, advancing the Lexer. - /// - /// The parsed token. - public ExpressionToken NextToken() - { - Exception error; - ExpressionToken nextToken = this.ReadNextToken(out error); + SetTextPos(0); + NextToken(); + } - if (error != null) - { - throw error; - } + /// Token being processed. + public ExpressionToken CurrentToken { get => _token; set => _token = value; } - return nextToken; - } + /// + /// Reads the next token, skipping whitespace as necessary, advancing the Lexer. + /// + /// The parsed token. + public ExpressionToken NextToken() + { + Exception error; + ExpressionToken nextToken = this.ReadNextToken(out error); - /// Validates the current token is of the specified kind. - /// Expected token kind. - public void ValidateToken(ExpressionTokenKind kind) + if (error != null) { - if (_token.Kind != kind) - { - throw new ODataException(Error.Format(SRResources.ExpressionLexerSyntaxError, _textPos, _rawText)); - } + throw error; } - /// - /// Gets if the current char is whitespace. - /// - protected virtual bool IsValidWhiteSpace => _char != null && char.IsWhiteSpace(_char.Value); - - /// - /// Reads the next token, skipping whitespace as necessary. - /// - /// The potential error. - /// The next token, which may be 'bad' if an error occurs. - protected virtual ExpressionToken ReadNextToken(out Exception error) - { - error = null; - this.ParseWhitespace(); - - ExpressionTokenKind t; - int tokenPos = _textPos; - switch (_char) - { - case ',': - NextChar(); - t = ExpressionTokenKind.Comma; - break; - - case '=': - NextChar(); - t = ExpressionTokenKind.Equal; - break; - - case '\'': - char quote = _char.Value; - do - { - this.AdvanceToNextOccurenceOf(quote); + return nextToken; + } - if (_textPos == _length) - { - error = new ODataException(Error.Format(SRResources.ExpressionLexerUnterminatedStringLiteral, _textPos, _rawText)); - } + /// Validates the current token is of the specified kind. + /// Expected token kind. + public void ValidateToken(ExpressionTokenKind kind) + { + if (_token.Kind != kind) + { + throw new ODataException(Error.Format(SRResources.ExpressionLexerSyntaxError, _textPos, _rawText)); + } + } - // double single quote will include in the string. - this.NextChar(); - } - while (_char.HasValue && (_char.Value == quote)); + /// + /// Gets if the current char is whitespace. + /// + protected virtual bool IsValidWhiteSpace => _char != null && char.IsWhiteSpace(_char.Value); - t = ExpressionTokenKind.Literal; - break; + /// + /// Reads the next token, skipping whitespace as necessary. + /// + /// The potential error. + /// The next token, which may be 'bad' if an error occurs. + protected virtual ExpressionToken ReadNextToken(out Exception error) + { + error = null; + this.ParseWhitespace(); - case '{': - NextChar(); - AdvanceThroughBalancedExpression('{', '}'); - t = ExpressionTokenKind.Literal; - break; + ExpressionTokenKind t; + int tokenPos = _textPos; + switch (_char) + { + case ',': + NextChar(); + t = ExpressionTokenKind.Comma; + break; - case '[': - NextChar(); - AdvanceThroughBalancedExpression('[', ']'); - t = ExpressionTokenKind.Literal; - break; + case '=': + NextChar(); + t = ExpressionTokenKind.Equal; + break; - default: - if (this.IsValidWhiteSpace) - { - this.ParseWhitespace(); - t = ExpressionTokenKind.Unknown; - break; - } + case '\'': + char quote = _char.Value; + do + { + this.AdvanceToNextOccurenceOf(quote); if (_textPos == _length) { - t = ExpressionTokenKind.TextEnd; - break; + error = new ODataException(Error.Format(SRResources.ExpressionLexerUnterminatedStringLiteral, _textPos, _rawText)); } - ParseLiteral(); - t = ExpressionTokenKind.Literal; - break; - } + // double single quote will include in the string. + this.NextChar(); + } + while (_char.HasValue && (_char.Value == quote)); - _token.Kind = t; - _token.Text = _rawText.Substring(tokenPos, _textPos - tokenPos); - _token.Position = tokenPos; - return _token; - } + t = ExpressionTokenKind.Literal; + break; - /// - /// Advance the pointer to the next occurrence of the given value, swallowing all characters in between. - /// - /// the ending delimiter. - protected virtual void AdvanceToNextOccurenceOf(char endingValue) - { - NextChar(); - while (_char.HasValue && (_char != endingValue)) - { + case '{': NextChar(); - } - } + AdvanceThroughBalancedExpression('{', '}'); + t = ExpressionTokenKind.Literal; + break; - /// - /// Parses an expression of text that we do not know how to handle in this class, which is between a - /// and an . - /// - /// the starting delimiter - /// the ending delimiter. - private void AdvanceThroughBalancedExpression(char startingCharacter, char endingCharacter) - { - int currentBracketDepth = 1; + case '[': + NextChar(); + AdvanceThroughBalancedExpression('[', ']'); + t = ExpressionTokenKind.Literal; + break; - while (currentBracketDepth > 0) - { - if (_char == startingCharacter) - { - currentBracketDepth++; - } - else if (_char == endingCharacter) + default: + if (this.IsValidWhiteSpace) { - currentBracketDepth--; + this.ParseWhitespace(); + t = ExpressionTokenKind.Unknown; + break; } - if (_char == null) + if (_textPos == _length) { - throw new ODataException(Error.Format(SRResources.ExpressionLexer_UnbalancedBracketExpression, startingCharacter, endingCharacter)); + t = ExpressionTokenKind.TextEnd; + break; } - this.NextChar(); - } + ParseLiteral(); + t = ExpressionTokenKind.Literal; + break; } - /// - /// Parses a literal be checking for delimiting characters '\0', ',',')' and ' ' - /// - private void ParseLiteral() + _token.Kind = t; + _token.Text = _rawText.Substring(tokenPos, _textPos - tokenPos); + _token.Position = tokenPos; + return _token; + } + + /// + /// Advance the pointer to the next occurrence of the given value, swallowing all characters in between. + /// + /// the ending delimiter. + protected virtual void AdvanceToNextOccurenceOf(char endingValue) + { + NextChar(); + while (_char.HasValue && (_char != endingValue)) { - do - { - NextChar(); - } - while (_char.HasValue && _char != ',' && _char != ' ' && _char != '='); + NextChar(); } + } - /// - /// Advanced to the next character. - /// - protected void NextChar() + /// + /// Parses an expression of text that we do not know how to handle in this class, which is between a + /// and an . + /// + /// the starting delimiter + /// the ending delimiter. + private void AdvanceThroughBalancedExpression(char startingCharacter, char endingCharacter) + { + int currentBracketDepth = 1; + + while (currentBracketDepth > 0) { - if (_textPos < _length) + if (_char == startingCharacter) { - _textPos++; - if (_textPos < _length) - { - _char = this._rawText[_textPos]; - return; - } + currentBracketDepth++; + } + else if (_char == endingCharacter) + { + currentBracketDepth--; + } + + if (_char == null) + { + throw new ODataException(Error.Format(SRResources.ExpressionLexer_UnbalancedBracketExpression, startingCharacter, endingCharacter)); } - _char = null; + this.NextChar(); } + } - /// - /// Sets the text position. - /// - /// The new position. - private void SetTextPos(int pos) + /// + /// Parses a literal be checking for delimiting characters '\0', ',',')' and ' ' + /// + private void ParseLiteral() + { + do { - _textPos = pos; - _char = _textPos < _length ? _rawText[_textPos] : (char?)null; + NextChar(); } + while (_char.HasValue && _char != ',' && _char != ' ' && _char != '='); + } - /// - /// Parses (skip) white spaces - /// - protected void ParseWhitespace() + /// + /// Advanced to the next character. + /// + protected void NextChar() + { + if (_textPos < _length) { - while (IsValidWhiteSpace) + _textPos++; + if (_textPos < _length) { - NextChar(); + _char = this._rawText[_textPos]; + return; } } + + _char = null; + } + + /// + /// Sets the text position. + /// + /// The new position. + private void SetTextPos(int pos) + { + _textPos = pos; + _char = _textPos < _length ? _rawText[_textPos] : (char?)null; + } + + /// + /// Parses (skip) white spaces + /// + protected void ParseWhitespace() + { + while (IsValidWhiteSpace) + { + NextChar(); + } } } diff --git a/src/Microsoft.AspNetCore.OData/Common/FastPropertyAccessor.cs b/src/Microsoft.AspNetCore.OData/Common/FastPropertyAccessor.cs index eea827603..0a0250944 100644 --- a/src/Microsoft.AspNetCore.OData/Common/FastPropertyAccessor.cs +++ b/src/Microsoft.AspNetCore.OData/Common/FastPropertyAccessor.cs @@ -9,60 +9,59 @@ using System.Reflection; using Microsoft.AspNetCore.OData.Formatter.Deserialization; -namespace Microsoft.AspNetCore.OData.Common +namespace Microsoft.AspNetCore.OData.Common; + +/// +/// FastPropertyAccessor is a that speeds up (compares to reflection) +/// a Getter and Setter for the PropertyInfo of TEntityType provided via the constructor. +/// +/// The type on which the PropertyInfo exists +internal class FastPropertyAccessor : PropertyAccessor where TStructuralType : class { - /// - /// FastPropertyAccessor is a that speeds up (compares to reflection) - /// a Getter and Setter for the PropertyInfo of TEntityType provided via the constructor. - /// - /// The type on which the PropertyInfo exists - internal class FastPropertyAccessor : PropertyAccessor where TStructuralType : class + private bool _isCollection; + private PropertyInfo _property; + private Action _setter; + private Func _getter; + + public FastPropertyAccessor(PropertyInfo property) + : base(property) { - private bool _isCollection; - private PropertyInfo _property; - private Action _setter; - private Func _getter; + _property = property; + _isCollection = TypeHelper.IsCollection(property.PropertyType); - public FastPropertyAccessor(PropertyInfo property) - : base(property) + if (!_isCollection) { - _property = property; - _isCollection = TypeHelper.IsCollection(property.PropertyType); + _setter = PropertyHelper.MakeFastPropertySetter(property); + } - if (!_isCollection) - { - _setter = PropertyHelper.MakeFastPropertySetter(property); - } + _getter = PropertyHelper.MakeFastPropertyGetter(property); + } - _getter = PropertyHelper.MakeFastPropertyGetter(property); + public override object GetValue(TStructuralType instance) + { + if (instance == null) + { + throw Error.ArgumentNull(nameof(instance)); } - public override object GetValue(TStructuralType instance) - { - if (instance == null) - { - throw Error.ArgumentNull(nameof(instance)); - } + return _getter(instance); + } - return _getter(instance); + public override void SetValue(TStructuralType instance, object value) + { + if (instance == null) + { + throw Error.ArgumentNull(nameof(instance)); } - public override void SetValue(TStructuralType instance, object value) + if (_isCollection) { - if (instance == null) - { - throw Error.ArgumentNull(nameof(instance)); - } - - if (_isCollection) - { - DeserializationHelpers.SetCollectionProperty(instance, _property.Name, edmPropertyType: null, - value: value, clearCollection: true); - } - else - { - _setter(instance, value); - } + DeserializationHelpers.SetCollectionProperty(instance, _property.Name, edmPropertyType: null, + value: value, clearCollection: true); + } + else + { + _setter(instance, value); } } } diff --git a/src/Microsoft.AspNetCore.OData/Common/KeyValuePairParser.cs b/src/Microsoft.AspNetCore.OData/Common/KeyValuePairParser.cs index fae030741..5277196d5 100644 --- a/src/Microsoft.AspNetCore.OData/Common/KeyValuePairParser.cs +++ b/src/Microsoft.AspNetCore.OData/Common/KeyValuePairParser.cs @@ -8,105 +8,104 @@ using System.Collections.Generic; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Common +namespace Microsoft.AspNetCore.OData.Common; + +/// +/// Parsing function parameters and entity key in paths. +/// +internal class KeyValuePairParser { /// - /// Parsing function parameters and entity key in paths. + /// Parse the expression string into key/value pairs /// - internal class KeyValuePairParser + /// The contents of the key/value pairs. + /// true/false + public static IDictionary Parse(string expression) { - /// - /// Parse the expression string into key/value pairs - /// - /// The contents of the key/value pairs. - /// true/false - public static IDictionary Parse(string expression) + if (string.IsNullOrWhiteSpace(expression)) { - if (string.IsNullOrWhiteSpace(expression)) - { - return null; - } + return null; + } - ExpressionLexer lexer = new ExpressionLexer(expression); + ExpressionLexer lexer = new ExpressionLexer(expression); - IDictionary pairs = new Dictionary(); + IDictionary pairs = new Dictionary(); - while (lexer.CurrentToken.Kind != ExpressionTokenKind.TextEnd) - { - // It should start with a literal, and the content of literal could be anything. - lexer.ValidateToken(ExpressionTokenKind.Literal); + while (lexer.CurrentToken.Kind != ExpressionTokenKind.TextEnd) + { + // It should start with a literal, and the content of literal could be anything. + lexer.ValidateToken(ExpressionTokenKind.Literal); - string key = lexer.CurrentToken.Text; - lexer.NextToken(); + string key = lexer.CurrentToken.Text; + lexer.NextToken(); - // it could be the end or a comma, in this case, it's a single key value - if (lexer.CurrentToken.Kind == ExpressionTokenKind.TextEnd || - lexer.CurrentToken.Kind == ExpressionTokenKind.Comma) + // it could be the end or a comma, in this case, it's a single key value + if (lexer.CurrentToken.Kind == ExpressionTokenKind.TextEnd || + lexer.CurrentToken.Kind == ExpressionTokenKind.Comma) + { + // it should be a single key value + if (pairs.ContainsKey(string.Empty)) { - // it should be a single key value - if (pairs.ContainsKey(string.Empty)) - { - throw new ODataException(Error.Format(SRResources.MultipleSingleLiteralNotAllowed, expression)); - } - - // we use the "string.empty" as the key for single key pattern. - pairs[string.Empty] = key; - - if (lexer.CurrentToken.Kind == ExpressionTokenKind.Comma) - { - lexer.NextToken(); - continue; - } - else - { - // Hit the text end. - break; - } + throw new ODataException(Error.Format(SRResources.MultipleSingleLiteralNotAllowed, expression)); } - // Verify the current token should be equal '=' and let's skip it. - lexer.ValidateToken(ExpressionTokenKind.Equal); - lexer.NextToken(); + // we use the "string.empty" as the key for single key pattern. + pairs[string.Empty] = key; - // Verify the current token should be literal - lexer.ValidateToken(ExpressionTokenKind.Literal); - string value = lexer.CurrentToken.Text; - - pairs[key] = value; // if have same key, the last will win - - lexer.NextToken(); if (lexer.CurrentToken.Kind == ExpressionTokenKind.Comma) { lexer.NextToken(); + continue; + } + else + { + // Hit the text end. + break; } } - return pairs; - } + // Verify the current token should be equal '=' and let's skip it. + lexer.ValidateToken(ExpressionTokenKind.Equal); + lexer.NextToken(); - /// - /// Tries to parse the expression string into key/value pairs - /// - /// The contents of the key/value pairs. - /// the output key/value pair - /// true/false - public static bool TryParse(string expression, out IDictionary pairs) - { - pairs = null; - try - { - pairs = Parse(expression); - if (pairs == null) - { - return false; - } + // Verify the current token should be literal + lexer.ValidateToken(ExpressionTokenKind.Literal); + string value = lexer.CurrentToken.Text; - return true; + pairs[key] = value; // if have same key, the last will win + + lexer.NextToken(); + if (lexer.CurrentToken.Kind == ExpressionTokenKind.Comma) + { + lexer.NextToken(); } - catch(ODataException) + } + + return pairs; + } + + /// + /// Tries to parse the expression string into key/value pairs + /// + /// The contents of the key/value pairs. + /// the output key/value pair + /// true/false + public static bool TryParse(string expression, out IDictionary pairs) + { + pairs = null; + try + { + pairs = Parse(expression); + if (pairs == null) { return false; } + + return true; + } + catch(ODataException) + { + return false; } } } diff --git a/src/Microsoft.AspNetCore.OData/Common/ListWrapperCollectionOfT.cs b/src/Microsoft.AspNetCore.OData/Common/ListWrapperCollectionOfT.cs index 3161ca51a..4559e83f4 100644 --- a/src/Microsoft.AspNetCore.OData/Common/ListWrapperCollectionOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Common/ListWrapperCollectionOfT.cs @@ -8,29 +8,28 @@ using System.Collections.Generic; using System.Collections.ObjectModel; -namespace Microsoft.AspNetCore.OData.Common +namespace Microsoft.AspNetCore.OData.Common; + +/// +/// A class that inherits from Collection of T but also exposes its underlying data as List of T for performance. +/// +internal sealed class ListWrapperCollection : Collection { - /// - /// A class that inherits from Collection of T but also exposes its underlying data as List of T for performance. - /// - internal sealed class ListWrapperCollection : Collection - { - private readonly List _items; + private readonly List _items; - internal ListWrapperCollection() - : this(new List()) - { - } + internal ListWrapperCollection() + : this(new List()) + { + } - internal ListWrapperCollection(List list) - : base(list) - { - _items = list; - } + internal ListWrapperCollection(List list) + : base(list) + { + _items = list; + } - internal List ItemsList - { - get { return _items; } - } + internal List ItemsList + { + get { return _items; } } } diff --git a/src/Microsoft.AspNetCore.OData/Common/ODataVersionConstraint.cs b/src/Microsoft.AspNetCore.OData/Common/ODataVersionConstraint.cs index b44973d77..7582e1f41 100644 --- a/src/Microsoft.AspNetCore.OData/Common/ODataVersionConstraint.cs +++ b/src/Microsoft.AspNetCore.OData/Common/ODataVersionConstraint.cs @@ -7,20 +7,19 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Common +namespace Microsoft.AspNetCore.OData.Common; + +/// +/// OData Version constants. +/// +internal static class ODataVersionConstraint { - /// - /// OData Version constants. - /// - internal static class ODataVersionConstraint - { - // The header names used for versioning in the versions 4.0+ of the OData protocol. - internal const string ODataServiceVersionHeader = "OData-Version"; + // The header names used for versioning in the versions 4.0+ of the OData protocol. + internal const string ODataServiceVersionHeader = "OData-Version"; - internal const string ODataMaxServiceVersionHeader = "OData-MaxVersion"; + internal const string ODataMaxServiceVersionHeader = "OData-MaxVersion"; - internal const string ODataMinServiceVersionHeader = "OData-MinVersion"; + internal const string ODataMinServiceVersionHeader = "OData-MinVersion"; - internal const ODataVersion DefaultODataVersion = ODataVersion.V4; - } + internal const ODataVersion DefaultODataVersion = ODataVersion.V4; } diff --git a/src/Microsoft.AspNetCore.OData/Common/PropertyAccessor.cs b/src/Microsoft.AspNetCore.OData/Common/PropertyAccessor.cs index 5e6803f59..faea0aac8 100644 --- a/src/Microsoft.AspNetCore.OData/Common/PropertyAccessor.cs +++ b/src/Microsoft.AspNetCore.OData/Common/PropertyAccessor.cs @@ -8,52 +8,51 @@ using System; using System.Reflection; -namespace Microsoft.AspNetCore.OData.Common +namespace Microsoft.AspNetCore.OData.Common; + +/// +/// Represents a strategy for Getting and Setting a PropertyInfo on +/// +/// The type that contains the PropertyInfo +internal abstract class PropertyAccessor where TStructuralType : class { - /// - /// Represents a strategy for Getting and Setting a PropertyInfo on - /// - /// The type that contains the PropertyInfo - internal abstract class PropertyAccessor where TStructuralType : class + protected PropertyAccessor(PropertyInfo property) { - protected PropertyAccessor(PropertyInfo property) + if (property == null) { - if (property == null) - { - throw new ArgumentNullException(nameof(property)); - } - - Property = property; - if (Property.GetGetMethod() == null || - (!TypeHelper.IsCollection(property.PropertyType) && Property.GetSetMethod() == null)) - { - throw Error.Argument("property", SRResources.PropertyMustHavePublicGetterAndSetter); - } + throw new ArgumentNullException(nameof(property)); } - public PropertyInfo Property + Property = property; + if (Property.GetGetMethod() == null || + (!TypeHelper.IsCollection(property.PropertyType) && Property.GetSetMethod() == null)) { - get; - private set; + throw Error.Argument("property", SRResources.PropertyMustHavePublicGetterAndSetter); } + } - public void Copy(TStructuralType from, TStructuralType to) - { - if (from == null) - { - throw Error.ArgumentNull(nameof(from)); - } - - if (to == null) - { - throw Error.ArgumentNull(nameof(to)); - } + public PropertyInfo Property + { + get; + private set; + } - SetValue(to, GetValue(from)); + public void Copy(TStructuralType from, TStructuralType to) + { + if (from == null) + { + throw Error.ArgumentNull(nameof(from)); } - public abstract object GetValue(TStructuralType instance); + if (to == null) + { + throw Error.ArgumentNull(nameof(to)); + } - public abstract void SetValue(TStructuralType instance, object value); + SetValue(to, GetValue(from)); } + + public abstract object GetValue(TStructuralType instance); + + public abstract void SetValue(TStructuralType instance, object value); } diff --git a/src/Microsoft.AspNetCore.OData/Common/PropertyHelper.cs b/src/Microsoft.AspNetCore.OData/Common/PropertyHelper.cs index e26c5e657..6be8df23c 100644 --- a/src/Microsoft.AspNetCore.OData/Common/PropertyHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Common/PropertyHelper.cs @@ -12,179 +12,178 @@ using System.Linq; using System.Reflection; -namespace Microsoft.AspNetCore.OData.Common -{ - internal class PropertyHelper - { - private static ConcurrentDictionary _reflectionCache = new ConcurrentDictionary(); +namespace Microsoft.AspNetCore.OData.Common; - private Func _valueGetter; +internal class PropertyHelper +{ + private static ConcurrentDictionary _reflectionCache = new ConcurrentDictionary(); - /// - /// Initializes a fast property helper. This constructor does not cache the helper. - /// - public PropertyHelper(PropertyInfo property) - { - Contract.Assert(property != null); + private Func _valueGetter; - Name = property.Name; - _valueGetter = MakeFastPropertyGetter(property); - } + /// + /// Initializes a fast property helper. This constructor does not cache the helper. + /// + public PropertyHelper(PropertyInfo property) + { + Contract.Assert(property != null); - /// - /// Creates a single fast property setter. The result is not cached. - /// - /// propertyInfo to extract the getter for. - /// a fast setter. - /// This method is more memory efficient than a dynamically compiled lambda, and about the same speed. - public static Action MakeFastPropertySetter(PropertyInfo propertyInfo) - where TDeclaringType : class - { - Contract.Assert(propertyInfo != null); + Name = property.Name; + _valueGetter = MakeFastPropertyGetter(property); + } - MethodInfo setMethod = propertyInfo.GetSetMethod(); + /// + /// Creates a single fast property setter. The result is not cached. + /// + /// propertyInfo to extract the getter for. + /// a fast setter. + /// This method is more memory efficient than a dynamically compiled lambda, and about the same speed. + public static Action MakeFastPropertySetter(PropertyInfo propertyInfo) + where TDeclaringType : class + { + Contract.Assert(propertyInfo != null); - Contract.Assert(setMethod != null); - Contract.Assert(!setMethod.IsStatic); - Contract.Assert(setMethod.GetParameters().Length == 1); - Contract.Assert(!TypeHelper.GetReflectedType(propertyInfo).IsValueType); + MethodInfo setMethod = propertyInfo.GetSetMethod(); - // Instance methods in the CLR can be turned into static methods where the first parameter - // is open over "this". This parameter is always passed by reference, so we have a code - // path for value types and a code path for reference types. - Type typeInput = TypeHelper.GetReflectedType(propertyInfo); - Type typeValue = setMethod.GetParameters()[0].ParameterType; + Contract.Assert(setMethod != null); + Contract.Assert(!setMethod.IsStatic); + Contract.Assert(setMethod.GetParameters().Length == 1); + Contract.Assert(!TypeHelper.GetReflectedType(propertyInfo).IsValueType); - Delegate callPropertySetterDelegate; + // Instance methods in the CLR can be turned into static methods where the first parameter + // is open over "this". This parameter is always passed by reference, so we have a code + // path for value types and a code path for reference types. + Type typeInput = TypeHelper.GetReflectedType(propertyInfo); + Type typeValue = setMethod.GetParameters()[0].ParameterType; - // Create a delegate TValue -> "TDeclaringType.Property" - var propertySetterAsAction = setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(typeInput, typeValue)); - var callPropertySetterClosedGenericMethod = _callPropertySetterOpenGenericMethod.MakeGenericMethod(typeInput, typeValue); - callPropertySetterDelegate = callPropertySetterClosedGenericMethod.CreateDelegate(typeof(Action), propertySetterAsAction); + Delegate callPropertySetterDelegate; - return (Action)callPropertySetterDelegate; - } + // Create a delegate TValue -> "TDeclaringType.Property" + var propertySetterAsAction = setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(typeInput, typeValue)); + var callPropertySetterClosedGenericMethod = _callPropertySetterOpenGenericMethod.MakeGenericMethod(typeInput, typeValue); + callPropertySetterDelegate = callPropertySetterClosedGenericMethod.CreateDelegate(typeof(Action), propertySetterAsAction); - public virtual string Name { get; protected set; } + return (Action)callPropertySetterDelegate; + } - public object GetValue(object instance) - { - Contract.Assert(_valueGetter != null, "Must call Initialize before using this object"); + public virtual string Name { get; protected set; } - return _valueGetter(instance); - } + public object GetValue(object instance) + { + Contract.Assert(_valueGetter != null, "Must call Initialize before using this object"); - /// - /// Creates and caches fast property helpers that expose getters for every public get property on the underlying type. - /// - /// the instance to extract property accessors for. - /// a cached array of all public property getters from the underlying type of this instance. - public static PropertyHelper[] GetProperties(object instance) - { - return GetProperties(instance, CreateInstance, _reflectionCache); - } + return _valueGetter(instance); + } - /// - /// Creates a single fast property getter. The result is not cached. - /// - /// propertyInfo to extract the getter for. - /// a fast getter. - /// This method is more memory efficient than a dynamically compiled lambda, and about the same speed. - public static Func MakeFastPropertyGetter(PropertyInfo propertyInfo) - { - Contract.Assert(propertyInfo != null); + /// + /// Creates and caches fast property helpers that expose getters for every public get property on the underlying type. + /// + /// the instance to extract property accessors for. + /// a cached array of all public property getters from the underlying type of this instance. + public static PropertyHelper[] GetProperties(object instance) + { + return GetProperties(instance, CreateInstance, _reflectionCache); + } - MethodInfo getMethod = propertyInfo.GetGetMethod(); - Contract.Assert(getMethod != null); - Contract.Assert(!getMethod.IsStatic); - Contract.Assert(getMethod.GetParameters().Length == 0); + /// + /// Creates a single fast property getter. The result is not cached. + /// + /// propertyInfo to extract the getter for. + /// a fast getter. + /// This method is more memory efficient than a dynamically compiled lambda, and about the same speed. + public static Func MakeFastPropertyGetter(PropertyInfo propertyInfo) + { + Contract.Assert(propertyInfo != null); - // Instance methods in the CLR can be turned into static methods where the first parameter - // is open over "this". This parameter is always passed by reference, so we have a code - // path for value types and a code path for reference types. - Type typeInput = TypeHelper.GetReflectedType(getMethod); - Type typeOutput = getMethod.ReturnType; + MethodInfo getMethod = propertyInfo.GetGetMethod(); + Contract.Assert(getMethod != null); + Contract.Assert(!getMethod.IsStatic); + Contract.Assert(getMethod.GetParameters().Length == 0); - Delegate callPropertyGetterDelegate; - if (typeInput.IsValueType) - { - // Create a delegate (ref TDeclaringType) -> TValue - Delegate propertyGetterAsFunc = getMethod.CreateDelegate(typeof(ByRefFunc<,>).MakeGenericType(typeInput, typeOutput)); - MethodInfo callPropertyGetterClosedGenericMethod = _callPropertyGetterByReferenceOpenGenericMethod.MakeGenericMethod(typeInput, typeOutput); - callPropertyGetterDelegate = callPropertyGetterClosedGenericMethod.CreateDelegate(typeof(Func), propertyGetterAsFunc); - } - else - { - // Create a delegate TDeclaringType -> TValue - Delegate propertyGetterAsFunc = getMethod.CreateDelegate(typeof(Func<,>).MakeGenericType(typeInput, typeOutput)); - MethodInfo callPropertyGetterClosedGenericMethod = _callPropertyGetterOpenGenericMethod.MakeGenericMethod(typeInput, typeOutput); - callPropertyGetterDelegate = callPropertyGetterClosedGenericMethod.CreateDelegate(typeof(Func), propertyGetterAsFunc); - } + // Instance methods in the CLR can be turned into static methods where the first parameter + // is open over "this". This parameter is always passed by reference, so we have a code + // path for value types and a code path for reference types. + Type typeInput = TypeHelper.GetReflectedType(getMethod); + Type typeOutput = getMethod.ReturnType; - return (Func)callPropertyGetterDelegate; + Delegate callPropertyGetterDelegate; + if (typeInput.IsValueType) + { + // Create a delegate (ref TDeclaringType) -> TValue + Delegate propertyGetterAsFunc = getMethod.CreateDelegate(typeof(ByRefFunc<,>).MakeGenericType(typeInput, typeOutput)); + MethodInfo callPropertyGetterClosedGenericMethod = _callPropertyGetterByReferenceOpenGenericMethod.MakeGenericMethod(typeInput, typeOutput); + callPropertyGetterDelegate = callPropertyGetterClosedGenericMethod.CreateDelegate(typeof(Func), propertyGetterAsFunc); } - - private static PropertyHelper CreateInstance(PropertyInfo property) + else { - return new PropertyHelper(property); + // Create a delegate TDeclaringType -> TValue + Delegate propertyGetterAsFunc = getMethod.CreateDelegate(typeof(Func<,>).MakeGenericType(typeInput, typeOutput)); + MethodInfo callPropertyGetterClosedGenericMethod = _callPropertyGetterOpenGenericMethod.MakeGenericMethod(typeInput, typeOutput); + callPropertyGetterDelegate = callPropertyGetterClosedGenericMethod.CreateDelegate(typeof(Func), propertyGetterAsFunc); } - // Implementation of the fast getter. - private delegate TValue ByRefFunc(ref TDeclaringType arg); + return (Func)callPropertyGetterDelegate; + } - private static readonly MethodInfo _callPropertyGetterOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertyGetter", BindingFlags.NonPublic | BindingFlags.Static); - private static readonly MethodInfo _callPropertyGetterByReferenceOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertyGetterByReference", BindingFlags.NonPublic | BindingFlags.Static); + private static PropertyHelper CreateInstance(PropertyInfo property) + { + return new PropertyHelper(property); + } - private static object CallPropertyGetter(Func getter, object @this) - { - return getter((TDeclaringType)@this); - } + // Implementation of the fast getter. + private delegate TValue ByRefFunc(ref TDeclaringType arg); - private static object CallPropertyGetterByReference(ByRefFunc getter, object @this) - { - TDeclaringType unboxed = (TDeclaringType)@this; - return getter(ref unboxed); - } + private static readonly MethodInfo _callPropertyGetterOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertyGetter", BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo _callPropertyGetterByReferenceOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertyGetterByReference", BindingFlags.NonPublic | BindingFlags.Static); - // Implementation of the fast setter. - private static readonly MethodInfo _callPropertySetterOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertySetter", BindingFlags.NonPublic | BindingFlags.Static); + private static object CallPropertyGetter(Func getter, object @this) + { + return getter((TDeclaringType)@this); + } - private static void CallPropertySetter(Action setter, object @this, object value) - { - setter((TDeclaringType)@this, (TValue)value); - } + private static object CallPropertyGetterByReference(ByRefFunc getter, object @this) + { + TDeclaringType unboxed = (TDeclaringType)@this; + return getter(ref unboxed); + } - protected static PropertyHelper[] GetProperties(object instance, - Func createPropertyHelper, - ConcurrentDictionary cache) - { - // Using an array rather than IEnumerable, as this will be called on the hot path numerous times. - PropertyHelper[] helpers; + // Implementation of the fast setter. + private static readonly MethodInfo _callPropertySetterOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertySetter", BindingFlags.NonPublic | BindingFlags.Static); + + private static void CallPropertySetter(Action setter, object @this, object value) + { + setter((TDeclaringType)@this, (TValue)value); + } - Type type = instance.GetType(); + protected static PropertyHelper[] GetProperties(object instance, + Func createPropertyHelper, + ConcurrentDictionary cache) + { + // Using an array rather than IEnumerable, as this will be called on the hot path numerous times. + PropertyHelper[] helpers; - if (!cache.TryGetValue(type, out helpers)) - { - // We avoid loading indexed properties using the where statement. - // Indexed properties are not useful (or valid) for grabbing properties off an anonymous object. - IEnumerable properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(prop => prop.GetIndexParameters().Length == 0 && - prop.GetMethod != null); + Type type = instance.GetType(); - var newHelpers = new List(); + if (!cache.TryGetValue(type, out helpers)) + { + // We avoid loading indexed properties using the where statement. + // Indexed properties are not useful (or valid) for grabbing properties off an anonymous object. + IEnumerable properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(prop => prop.GetIndexParameters().Length == 0 && + prop.GetMethod != null); - foreach (PropertyInfo property in properties) - { - PropertyHelper propertyHelper = createPropertyHelper(property); + var newHelpers = new List(); - newHelpers.Add(propertyHelper); - } + foreach (PropertyInfo property in properties) + { + PropertyHelper propertyHelper = createPropertyHelper(property); - helpers = newHelpers.ToArray(); - cache.TryAdd(type, helpers); + newHelpers.Add(propertyHelper); } - return helpers; + helpers = newHelpers.ToArray(); + cache.TryAdd(type, helpers); } + + return helpers; } } diff --git a/src/Microsoft.AspNetCore.OData/Common/StringExtensions.cs b/src/Microsoft.AspNetCore.OData/Common/StringExtensions.cs index 82ad1e467..f6d67959d 100644 --- a/src/Microsoft.AspNetCore.OData/Common/StringExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Common/StringExtensions.cs @@ -7,181 +7,159 @@ using System; -namespace Microsoft.AspNetCore.OData.Common +namespace Microsoft.AspNetCore.OData.Common; + +internal static class StringExtensions { - internal static class StringExtensions + /// + /// Unescape Uri string for %2F + /// See details at: https://github.com/dotnet/aspnetcore/issues/14170#issuecomment-533342396 + /// + /// The Uri string. + /// Unescaped back slash Uri string. + public static string UnescapeBackSlashUriString(this string uriString) { - /// - /// Unescape Uri string for %2F - /// See details at: https://github.com/dotnet/aspnetcore/issues/14170#issuecomment-533342396 - /// - /// The Uri string. - /// Unescaped back slash Uri string. - public static string UnescapeBackSlashUriString(this string uriString) + if (uriString == null) { - if (uriString == null) - { - return null; - } - - return uriString.Replace("%2f", "%2F").Replace("%2F", "/"); + return null; } - /// - /// Escape Uri string for '/' - /// - /// The Uri string. - /// Escaped back slash Uri string. - public static string EscapeBackSlashUriString(this string uriString) - { - if (uriString == null) - { - return null; - } + return uriString.Replace("%2f", "%2F").Replace("%2F", "/"); + } - return uriString.Replace("/", "%2F"); + /// + /// Escape Uri string for '/' + /// + /// The Uri string. + /// Escaped back slash Uri string. + public static string EscapeBackSlashUriString(this string uriString) + { + if (uriString == null) + { + return null; } - /// - /// Normalize the http method. - /// - /// The http method. - /// Normalized http method. - internal static string NormalizeHttpMethod(this string method) + return uriString.Replace("/", "%2F"); + } + + /// + /// Normalize the http method. + /// + /// The http method. + /// Normalized http method. + internal static string NormalizeHttpMethod(this string method) + { + switch (method.ToUpperInvariant()) { - switch (method.ToUpperInvariant()) - { - case "POSTTO": - return "Post"; + case "POSTTO": + return "Post"; - case "PUTTO": - return "Put"; + case "PUTTO": + return "Put"; - case "PATCHTO": - return "Patch"; + case "PATCHTO": + return "Patch"; - case "DELETETO": - return "Delete"; + case "DELETETO": + return "Delete"; - default: - return method; - } + default: + return method; } + } - /// - /// Check whether given literal matches the uri template pattern {literals}. - /// - /// The text to be evaluated - /// True if is valid for Uri template - internal static bool IsValidTemplateLiteral(this string literalText) - { - return !string.IsNullOrEmpty(literalText) - && literalText.StartsWith("{", StringComparison.Ordinal) - && literalText.EndsWith("}", StringComparison.Ordinal); - } + /// + /// Check whether given literal matches the uri template pattern {literals}. + /// + /// The text to be evaluated + /// True if is valid for Uri template + internal static bool IsValidTemplateLiteral(this string literalText) + { + return !string.IsNullOrEmpty(literalText) + && literalText.StartsWith("{", StringComparison.Ordinal) + && literalText.EndsWith("}", StringComparison.Ordinal); + } #if false // comment out the following codes for backup. - /// - /// Extract key/value pairs, the value could have "=" or ', or "", etc. - /// - /// The input string - /// - /// true or false - public static bool TryExtractKeyValuePairs(this string input, out IDictionary pairs) + /// + /// Extract key/value pairs, the value could have "=" or ', or "", etc. + /// + /// The input string + /// + /// true or false + public static bool TryExtractKeyValuePairs(this string input, out IDictionary pairs) + { + if (input == null) { - if (input == null) - { - throw Error.ArgumentNull(nameof(input)); - } + throw Error.ArgumentNull(nameof(input)); + } - // " minSalary='af''d,2,=897abc' , maxSalary=3" - pairs = new Dictionary(); - input = input.Trim(); + // " minSalary='af''d,2,=897abc' , maxSalary=3" + pairs = new Dictionary(); + input = input.Trim(); - int length = input.Length; - int start = 0; - string key, value; - int end; - while (true) - { - (key, end) = ReadKey(input, start); + int length = input.Length; + int start = 0; + string key, value; + int end; + while (true) + { + (key, end) = ReadKey(input, start); - if (end == length || input[end] != '=') - { - throw new ODataException(); - } + if (end == length || input[end] != '=') + { + throw new ODataException(); + } - start = end + 1; // skip = - (value, end) = ReadValue(input, start); + start = end + 1; // skip = + (value, end) = ReadValue(input, start); - pairs[key.Trim()] = value.Trim(); - start = end + 1; + pairs[key.Trim()] = value.Trim(); + start = end + 1; - if (end >= length) - { - break; - } + if (end >= length) + { + break; } - - return true; } - private static (string, int) ReadKey(string input, int start) - { - int length = input.Length; - int end = start; - for (; end < length; end++) - { - if (input[end] == '=') - { - break; - } - } + return true; + } - if (end == length) + private static (string, int) ReadKey(string input, int start) + { + int length = input.Length; + int end = start; + for (; end < length; end++) + { + if (input[end] == '=') { - throw new ODataException(""); + break; } - - return (input.Substring(start, end - start), end); } - private static (string, int) ReadValue(string input, int start) + if (end == length) { - int length = input.Length; - - char ch = input[start]; - if (ch == '\'') // TODO: it could be {. [ - { - int end = start; - do - { - int j = end + 1; - for (; j < length; j++) - { - if (input[j] == '\'') - { - break; - } - } + throw new ODataException(""); + } - if (j == length) - { - throw new ODataException(""); - } + return (input.Substring(start, end - start), end); + } - end = j + 1; - } - while (end != length && input[end] == '\''); + private static (string, int) ReadValue(string input, int start) + { + int length = input.Length; - return (input.Substring(start, end - start), end); - } - else + char ch = input[start]; + if (ch == '\'') // TODO: it could be {. [ + { + int end = start; + do { - int j = start + 1; + int j = end + 1; for (; j < length; j++) { - if (input[j] == ',') + if (input[j] == '\'') { break; } @@ -189,204 +167,225 @@ private static (string, int) ReadValue(string input, int start) if (j == length) { - return (input.Substring(start), j); + throw new ODataException(""); } - return (input.Substring(start, j - start), j); + end = j + 1; } - } + while (end != length && input[end] == '\''); - /// - /// Each key/value pair is separated using ",". - /// Each value could be a string using single quote, so it could have "," and escaped double single quote. - /// "minSalary=2,maxSalary=3" - /// "minSalary='af''d,2,897abc' , maxSalary=3" - /// - /// - /// - public static IDictionary ExtractKeyValuePairs(this string input) + return (input.Substring(start, end - start), end); + } + else { - if (input == null) + int j = start + 1; + for (; j < length; j++) + { + if (input[j] == ',') + { + break; + } + } + + if (j == length) { - throw new ArgumentNullException(nameof(input)); + return (input.Substring(start), j); } - // "minSalary=2,maxSalary=3" - // "minSalary='afd,2',maxSalary=3" - input = input.Trim(); + return (input.Substring(start, j - start), j); + } + } - IDictionary result = new Dictionary(); - while (!string.IsNullOrEmpty(input)) + /// + /// Each key/value pair is separated using ",". + /// Each value could be a string using single quote, so it could have "," and escaped double single quote. + /// "minSalary=2,maxSalary=3" + /// "minSalary='af''d,2,897abc' , maxSalary=3" + /// + /// + /// + public static IDictionary ExtractKeyValuePairs(this string input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + // "minSalary=2,maxSalary=3" + // "minSalary='afd,2',maxSalary=3" + input = input.Trim(); + + IDictionary result = new Dictionary(); + while (!string.IsNullOrEmpty(input)) + { + int index = input.IndexOf("=", StringComparison.Ordinal); + if (index <= 0) { - int index = input.IndexOf("=", StringComparison.Ordinal); - if (index <= 0) - { - throw new ODataException($"Invalid string '{input}' without '='."); - } + throw new ODataException($"Invalid string '{input}' without '='."); + } - string key = input.Substring(0, index); - input = input.Substring(index + 1); // ignore = - if (string.IsNullOrEmpty(input)) - { - throw new ODataException($"Empty string '{input}'"); - } + string key = input.Substring(0, index); + input = input.Substring(index + 1); // ignore = + if (string.IsNullOrEmpty(input)) + { + throw new ODataException($"Empty string '{input}'"); + } - string value; - if (input[0] == '\'') + string value; + if (input[0] == '\'') + { + bool found = false; + int i = 1; + // If it's a string + for (; i < input.Length; i++) { - bool found = false; - int i = 1; - // If it's a string - for (; i < input.Length; i++) + if (input[i] == '\'') { - if (input[i] == '\'') + if (i == input.Length - 1) + { + found = true; + break; + } + else { - if (i == input.Length - 1) + if (input[i + 1] == '\'') // '' escaped single quote { - found = true; - break; + i++; + continue; } else { - if (input[i + 1] == '\'') // '' escaped single quote - { - i++; - continue; - } - else - { - found = true; - break; - } + found = true; + break; } } } + } - if (!found) - { - throw new ODataException($"non match ''' for '{input}'"); - } + if (!found) + { + throw new ODataException($"non match ''' for '{input}'"); + } - value = input.Substring(i); - input = input.Substring(i + 1); + value = input.Substring(i); + input = input.Substring(i + 1); + } + else + { + int valueIndex = input.IndexOf(",", StringComparison.Ordinal); + if (valueIndex < 0) + { + value = input; + input = null; } else { - int valueIndex = input.IndexOf(",", StringComparison.Ordinal); - if (valueIndex < 0) - { - value = input; - input = null; - } - else - { - value = input.Substring(0, valueIndex); - input = input.Substring(valueIndex + 1); // ignore , - } + value = input.Substring(0, valueIndex); + input = input.Substring(valueIndex + 1); // ignore , } - - result[key] = value; - input = input.Trim(); } - return result; + result[key] = value; + input = input.Trim(); } - public static string[] ExtractItems(this string input, params string[] seperators) + return result; + } + + public static string[] ExtractItems(this string input, params string[] seperators) + { + string text = input; + List items = new List(); + for (int i = seperators.Length - 1; i >= 0; i--) { - string text = input; - List items = new List(); - for (int i = seperators.Length - 1; i >= 0; i--) + int index = text.IndexOf(seperators[i], StringComparison.Ordinal); + if (index > 0) { - int index = text.IndexOf(seperators[i], StringComparison.Ordinal); - if (index > 0) - { - items.Add(text.Substring(index + seperators[i].Length)); - text = text.Substring(0, index); - } + items.Add(text.Substring(index + seperators[i].Length)); + text = text.Substring(0, index); } - - items.Reverse(); - return items.ToArray(); } - public static string ExtractParenthesis(this string identifier, out string parenthesisExpressions) - { - // also supports: name(abc)(efg) - // => name - // => (abc)(efg) - parenthesisExpressions = null; + items.Reverse(); + return items.ToArray(); + } - int parenthesisStart = identifier.IndexOf('(', StringComparison.Ordinal); - if (parenthesisStart >= 0) - { - if (identifier[identifier.Length - 1] != ')') - { - throw new Exception($"Invalid identifier {identifier}, cannot find the end ')' character."); - } + public static string ExtractParenthesis(this string identifier, out string parenthesisExpressions) + { + // also supports: name(abc)(efg) + // => name + // => (abc)(efg) + parenthesisExpressions = null; - // split the string to grab the identifier and remove the parentheses - string returnStr = identifier.Substring(0, parenthesisStart); - parenthesisExpressions = identifier.Substring(parenthesisStart, identifier.Length - returnStr.Length); - identifier = returnStr; + int parenthesisStart = identifier.IndexOf('(', StringComparison.Ordinal); + if (parenthesisStart >= 0) + { + if (identifier[identifier.Length - 1] != ')') + { + throw new Exception($"Invalid identifier {identifier}, cannot find the end ')' character."); } - return identifier; + // split the string to grab the identifier and remove the parentheses + string returnStr = identifier.Substring(0, parenthesisStart); + parenthesisExpressions = identifier.Substring(parenthesisStart, identifier.Length - returnStr.Length); + identifier = returnStr; } - public static void ExtractKeyValuePairs(this string input, out IDictionary pairs, out string remaining) + return identifier; + } + + public static void ExtractKeyValuePairs(this string input, out IDictionary pairs, out string remaining) + { + pairs = new Dictionary(); + remaining = null; + + if (String.IsNullOrEmpty(input)) { - pairs = new Dictionary(); - remaining = null; + return; + } - if (String.IsNullOrEmpty(input)) - { - return; - } + // maybe with keys after function parameters + // .... (...)(...) + int startIndex = input.IndexOf('(', StringComparison.Ordinal); + int endIndex = input.IndexOf(')', startIndex + 1); + string parenthsis = input.Substring(startIndex + 1, endIndex - startIndex - 1); - // maybe with keys after function parameters - // .... (...)(...) - int startIndex = input.IndexOf('(', StringComparison.Ordinal); - int endIndex = input.IndexOf(')', startIndex + 1); - string parenthsis = input.Substring(startIndex + 1, endIndex - startIndex - 1); + if (endIndex != input.Length - 1) + { + remaining = input.Substring(endIndex + 1); + } - if (endIndex != input.Length - 1) - { - remaining = input.Substring(endIndex + 1); - } + var items = parenthsis.Split(','); + foreach (var item in items) + { + var subItems = item.Split('='); - var items = parenthsis.Split(','); - foreach (var item in items) + if (subItems.Length == 1) { - var subItems = item.Split('='); - - if (subItems.Length == 1) + if (pairs.TryGetValue(String.Empty, out string value)) { - if (pairs.TryGetValue(String.Empty, out string value)) - { - throw new Exception($"Invalid string '{input}', has multiple items without '='."); - } - - // VerifyIsKeyOrParameterTemplate(subItems[0]); - - pairs[String.Empty] = subItems[0].Trim(); + throw new Exception($"Invalid string '{input}', has multiple items without '='."); } - else if (subItems.Length == 2) - { - if (String.IsNullOrEmpty(subItems[0])) - { - throw new Exception($"Invalid string '{input}', has empty key in '{item}'."); - } - // VerifyIsKeyOrParameterTemplate(subItems[1]); - pairs[subItems[0].Trim()] = subItems[1].Trim(); - } - else + // VerifyIsKeyOrParameterTemplate(subItems[0]); + + pairs[String.Empty] = subItems[0].Trim(); + } + else if (subItems.Length == 2) + { + if (String.IsNullOrEmpty(subItems[0])) { - throw new Exception($"Invalid parameter at {item}"); + throw new Exception($"Invalid string '{input}', has empty key in '{item}'."); } + + // VerifyIsKeyOrParameterTemplate(subItems[1]); + pairs[subItems[0].Trim()] = subItems[1].Trim(); + } + else + { + throw new Exception($"Invalid parameter at {item}"); } } -#endif } +#endif } diff --git a/src/Microsoft.AspNetCore.OData/Common/TimeZoneInfoHelper.cs b/src/Microsoft.AspNetCore.OData/Common/TimeZoneInfoHelper.cs index 6b90ce432..0cd5489a6 100644 --- a/src/Microsoft.AspNetCore.OData/Common/TimeZoneInfoHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Common/TimeZoneInfoHelper.cs @@ -7,66 +7,65 @@ using System; -namespace Microsoft.AspNetCore.OData.Common +namespace Microsoft.AspNetCore.OData.Common; + +internal class TimeZoneInfoHelper { - internal class TimeZoneInfoHelper + public static DateTimeOffset ConvertToDateTimeOffset(DateTime dateTime) + { + return ConvertToDateTimeOffset(dateTime, TimeZoneInfo.Local); + } + + public static DateTimeOffset ConvertToDateTimeOffset(DateTime dateTime, TimeZoneInfo timeZone) { - public static DateTimeOffset ConvertToDateTimeOffset(DateTime dateTime) + if (timeZone == null) { - return ConvertToDateTimeOffset(dateTime, TimeZoneInfo.Local); + timeZone = TimeZoneInfo.Local; } - public static DateTimeOffset ConvertToDateTimeOffset(DateTime dateTime, TimeZoneInfo timeZone) + TimeSpan utcOffset = timeZone.GetUtcOffset(dateTime); + if (utcOffset >= TimeSpan.Zero) { - if (timeZone == null) + if (dateTime <= DateTime.MinValue + utcOffset) { - timeZone = TimeZoneInfo.Local; + return DateTimeOffset.MinValue; } - - TimeSpan utcOffset = timeZone.GetUtcOffset(dateTime); - if (utcOffset >= TimeSpan.Zero) + } + else + { + if (dateTime >= DateTime.MaxValue + utcOffset) { - if (dateTime <= DateTime.MinValue + utcOffset) - { - return DateTimeOffset.MinValue; - } + return DateTimeOffset.MaxValue; } - else + } + + if (dateTime.Kind == DateTimeKind.Local) + { + TimeZoneInfo localTimeZoneInfo = TimeZoneInfo.Local; + TimeSpan localTimeSpan = localTimeZoneInfo.GetUtcOffset(dateTime); + if (localTimeSpan < TimeSpan.Zero) { - if (dateTime >= DateTime.MaxValue + utcOffset) + if (dateTime >= DateTime.MaxValue + localTimeSpan) { return DateTimeOffset.MaxValue; } } - - if (dateTime.Kind == DateTimeKind.Local) + else { - TimeZoneInfo localTimeZoneInfo = TimeZoneInfo.Local; - TimeSpan localTimeSpan = localTimeZoneInfo.GetUtcOffset(dateTime); - if (localTimeSpan < TimeSpan.Zero) + if (dateTime <= DateTime.MinValue + localTimeSpan) { - if (dateTime >= DateTime.MaxValue + localTimeSpan) - { - return DateTimeOffset.MaxValue; - } - } - else - { - if (dateTime <= DateTime.MinValue + localTimeSpan) - { - return DateTimeOffset.MinValue; - } + return DateTimeOffset.MinValue; } - - return TimeZoneInfo.ConvertTime(new DateTimeOffset(dateTime), timeZone); } - if (dateTime.Kind == DateTimeKind.Utc) - { - return TimeZoneInfo.ConvertTime(new DateTimeOffset(dateTime), timeZone); - } + return TimeZoneInfo.ConvertTime(new DateTimeOffset(dateTime), timeZone); + } - return new DateTimeOffset(dateTime, timeZone.GetUtcOffset(dateTime)); + if (dateTime.Kind == DateTimeKind.Utc) + { + return TimeZoneInfo.ConvertTime(new DateTimeOffset(dateTime), timeZone); } + + return new DateTimeOffset(dateTime, timeZone.GetUtcOffset(dateTime)); } } diff --git a/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs b/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs index 315a63ac7..4be2d89aa 100644 --- a/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs @@ -19,417 +19,416 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Common +namespace Microsoft.AspNetCore.OData.Common; + +/// +/// The type related helper methods. +/// +internal static class TypeHelper { /// - /// The type related helper methods. + /// Test whether the input type is /// - internal static class TypeHelper + /// The test type. + /// true/false + public static bool IsDynamicTypeWrapper(this Type type) + { + return (type != null && typeof(DynamicTypeWrapper).IsAssignableFrom(type)); + } + + public static bool IsDeltaSetWrapper(this Type type, out Type entityType) => IsTypeWrapper(typeof(DeltaSet<>), type, out entityType); + + public static bool IsSelectExpandWrapper(this Type type, out Type entityType) => IsTypeWrapper(typeof(SelectExpandWrapper<>), type, out entityType); + + public static bool IsComputeWrapper(this Type type, out Type entityType) => IsTypeWrapper(typeof(ComputeWrapper<>), type, out entityType); + + private static bool IsTypeWrapper(Type wrappedType, Type type, out Type entityType) { - /// - /// Test whether the input type is - /// - /// The test type. - /// true/false - public static bool IsDynamicTypeWrapper(this Type type) + if (type == null) { - return (type != null && typeof(DynamicTypeWrapper).IsAssignableFrom(type)); + entityType = null; + return false; } - public static bool IsDeltaSetWrapper(this Type type, out Type entityType) => IsTypeWrapper(typeof(DeltaSet<>), type, out entityType); + if (type.IsGenericType && type.GetGenericTypeDefinition() == wrappedType) + { + entityType = type.GetGenericArguments()[0]; + return true; + } - public static bool IsSelectExpandWrapper(this Type type, out Type entityType) => IsTypeWrapper(typeof(SelectExpandWrapper<>), type, out entityType); + return IsTypeWrapper(wrappedType, type.BaseType, out entityType); + } - public static bool IsComputeWrapper(this Type type, out Type entityType) => IsTypeWrapper(typeof(ComputeWrapper<>), type, out entityType); + /// + /// Return the collection element type. + /// + /// The type to convert. + /// The collection element type from a type. + public static Type GetInnerElementType(Type clrType) + { + IsCollection(clrType, out Type elementType); + Contract.Assert(elementType != null); - private static bool IsTypeWrapper(Type wrappedType, Type type, out Type entityType) - { - if (type == null) - { - entityType = null; - return false; - } + return elementType; + } - if (type.IsGenericType && type.GetGenericTypeDefinition() == wrappedType) - { - entityType = type.GetGenericArguments()[0]; - return true; - } + /// + /// Return the underlying type or itself. + /// + /// The input type. + /// The underlying type. + public static Type GetUnderlyingTypeOrSelf(Type type) + { + return Nullable.GetUnderlyingType(type) ?? type; + } - return IsTypeWrapper(wrappedType, type.BaseType, out entityType); - } + /// + /// Determine if a type is a DateTime. + /// + /// The type to test. + /// True if the type is a DateTime; false otherwise. + public static bool IsDateTime(Type clrType) + { + Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); + return Type.GetTypeCode(underlyingTypeOrSelf) == TypeCode.DateTime; + } - /// - /// Return the collection element type. - /// - /// The type to convert. - /// The collection element type from a type. - public static Type GetInnerElementType(Type clrType) - { - IsCollection(clrType, out Type elementType); - Contract.Assert(elementType != null); + /// + /// Determine if a type is a . + /// + /// The type to test. + /// True if the type is a DateOnly; false otherwise. + public static bool IsDateOnly(this Type clrType) + { + Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); + return underlyingTypeOrSelf == typeof(DateOnly); + } - return elementType; - } + /// + /// Determine if a type is a . + /// + /// The type to test. + /// True if the type is a TimeOnly; false otherwise. + public static bool IsTimeOnly(this Type clrType) + { + Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); + return underlyingTypeOrSelf == typeof(TimeOnly); + } + + /// + /// Determine if a type is a TimeSpan. + /// + /// The type to test. + /// True if the type is a TimeSpan; false otherwise. + public static bool IsTimeSpan(Type clrType) + { + Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); + return underlyingTypeOrSelf == typeof(TimeSpan); + } + + /// + /// Determine if a type is assignable from another type. + /// + /// The type to test. + /// The type to assign from. + /// True if the type is assignable; false otherwise. + public static bool IsTypeAssignableFrom(Type clrType, Type fromType) + { + return clrType.IsAssignableFrom(fromType); + } - /// - /// Return the underlying type or itself. - /// - /// The input type. - /// The underlying type. - public static Type GetUnderlyingTypeOrSelf(Type type) + /// + /// Return the reflected type from a member info. + /// + /// The member info to convert. + /// The reflected type from a member info. + public static Type GetReflectedType(MemberInfo memberInfo) + { + return memberInfo.ReflectedType; + } + + /// + /// Determine if a type is an enumeration. + /// + /// The type to test. + /// True if the type is an enumeration; false otherwise. + public static bool IsEnum(Type clrType) + { + Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); + return underlyingTypeOrSelf.IsEnum; + } + + /// + /// Determine if a type is null-able. + /// + /// The type to test. + /// True if the type is null-able; false otherwise. + public static bool IsNullable(this Type clrType) + { + if (clrType == null) { - return Nullable.GetUnderlyingType(type) ?? type; + return false; } - /// - /// Determine if a type is a DateTime. - /// - /// The type to test. - /// True if the type is a DateTime; false otherwise. - public static bool IsDateTime(Type clrType) + if (clrType.IsValueType) { - Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); - return Type.GetTypeCode(underlyingTypeOrSelf) == TypeCode.DateTime; + // value types are only nullable if they are Nullable + return clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(Nullable<>); } - - /// - /// Determine if a type is a . - /// - /// The type to test. - /// True if the type is a DateOnly; false otherwise. - public static bool IsDateOnly(this Type clrType) + else { - Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); - return underlyingTypeOrSelf == typeof(DateOnly); + // reference types are always nullable + return true; } + } - /// - /// Determine if a type is a . - /// - /// The type to test. - /// True if the type is a TimeOnly; false otherwise. - public static bool IsTimeOnly(this Type clrType) + /// + /// Return the type from a nullable type. + /// + /// The type to convert. + /// The type from a nullable type. + public static Type ToNullable(Type clrType) + { + if (IsNullable(clrType)) { - Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); - return underlyingTypeOrSelf == typeof(TimeOnly); + return clrType; } - - /// - /// Determine if a type is a TimeSpan. - /// - /// The type to test. - /// True if the type is a TimeSpan; false otherwise. - public static bool IsTimeSpan(Type clrType) + else { - Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); - return underlyingTypeOrSelf == typeof(TimeSpan); + return typeof(Nullable<>).MakeGenericType(clrType); } + } + + /// + /// Determine if a type is a collection. + /// + /// The type to test. + /// True if the type is an enumeration; false otherwise. + public static bool IsCollection(Type clrType) => IsCollection(clrType, out _); - /// - /// Determine if a type is assignable from another type. - /// - /// The type to test. - /// The type to assign from. - /// True if the type is assignable; false otherwise. - public static bool IsTypeAssignableFrom(Type clrType, Type fromType) + /// + /// Determine if a type is a collection. + /// + /// The type to test. + /// out: the element type of the collection. + /// True if the type is an enumeration; false otherwise. + public static bool IsCollection(Type clrType, out Type elementType) + { + if (clrType == null) { - return clrType.IsAssignableFrom(fromType); + throw Error.ArgumentNull(nameof(clrType)); } - /// - /// Return the reflected type from a member info. - /// - /// The member info to convert. - /// The reflected type from a member info. - public static Type GetReflectedType(MemberInfo memberInfo) + elementType = clrType; + + // see if this type should be ignored. + if (clrType == typeof(string)) { - return memberInfo.ReflectedType; + return false; } - /// - /// Determine if a type is an enumeration. - /// - /// The type to test. - /// True if the type is an enumeration; false otherwise. - public static bool IsEnum(Type clrType) + // Since IDictionary is a collection of KeyValuePair + if (clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) { - Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); - return underlyingTypeOrSelf.IsEnum; + return false; } - /// - /// Determine if a type is null-able. - /// - /// The type to test. - /// True if the type is null-able; false otherwise. - public static bool IsNullable(this Type clrType) - { - if (clrType == null) - { - return false; - } + Type collectionInterface + = clrType.GetInterfaces() + .Union(new[] { clrType }) + .FirstOrDefault( + t => t.IsGenericType + && (t.GetGenericTypeDefinition() == typeof(IEnumerable<>) + || t.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>))); - if (clrType.IsValueType) - { - // value types are only nullable if they are Nullable - return clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(Nullable<>); - } - else - { - // reference types are always nullable - return true; - } + if (collectionInterface != null) + { + elementType = collectionInterface.GetGenericArguments().Single(); + return true; } - /// - /// Return the type from a nullable type. - /// - /// The type to convert. - /// The type from a nullable type. - public static Type ToNullable(Type clrType) + return false; + } + + internal static bool IsDictionary(Type clrType) + { + if (clrType == null) { - if (IsNullable(clrType)) - { - return clrType; - } - else - { - return typeof(Nullable<>).MakeGenericType(clrType); - } + return false; } - /// - /// Determine if a type is a collection. - /// - /// The type to test. - /// True if the type is an enumeration; false otherwise. - public static bool IsCollection(Type clrType) => IsCollection(clrType, out _); - - /// - /// Determine if a type is a collection. - /// - /// The type to test. - /// out: the element type of the collection. - /// True if the type is an enumeration; false otherwise. - public static bool IsCollection(Type clrType, out Type elementType) + if (typeof(IDictionary).IsAssignableFrom(clrType)) { - if (clrType == null) - { - throw Error.ArgumentNull(nameof(clrType)); - } - - elementType = clrType; - - // see if this type should be ignored. - if (clrType == typeof(string)) - { - return false; - } - - // Since IDictionary is a collection of KeyValuePair - if (clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) - { - return false; - } + return true; + } - Type collectionInterface - = clrType.GetInterfaces() - .Union(new[] { clrType }) - .FirstOrDefault( - t => t.IsGenericType - && (t.GetGenericTypeDefinition() == typeof(IEnumerable<>) - || t.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>))); + if (clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) + { + return true; + } - if (collectionInterface != null) + foreach (var interfaceType in clrType.GetInterfaces()) + { + if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) { - elementType = collectionInterface.GetGenericArguments().Single(); return true; } - - return false; } - internal static bool IsDictionary(Type clrType) + return false; + } + + internal static IEdmTypeReference GetUntypedEdmType(Type clrType) + { + if (clrType == null) { - if (clrType == null) - { - return false; - } + throw Error.ArgumentNull(nameof(clrType)); + } - if (typeof(IDictionary).IsAssignableFrom(clrType)) - { - return true; - } + return IsDictionary(clrType) ? + (IEdmTypeReference)EdmUntypedStructuredTypeReference.NullableTypeReference : + (TypeHelper.IsCollection(clrType) ? + (IEdmTypeReference)EdmUntypedHelpers.NullableUntypedCollectionReference : + (IEdmTypeReference)EdmUntypedStructuredTypeReference.NullableTypeReference); + } - if (clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) - { - return true; - } + /// + /// Returns type of T if the type implements IEnumerable of T, otherwise, return null. + /// + /// + /// + internal static Type GetImplementedIEnumerableType(Type type) + { + // get inner type from Task + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>)) + { + type = type.GetGenericArguments().First(); + } - foreach (var interfaceType in clrType.GetInterfaces()) + if (type.IsGenericType && type.IsInterface && + (type.GetGenericTypeDefinition() == typeof(IEnumerable<>) || + type.GetGenericTypeDefinition() == typeof(IQueryable<>))) + { + // special case the IEnumerable + return GetInnerGenericType(type); + } + else + { + // for the rest of interfaces and strongly Type collections + Type[] interfaces = type.GetInterfaces(); + foreach (Type interfaceType in interfaces) { - if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) + if (interfaceType.IsGenericType && + (interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>) || + interfaceType.GetGenericTypeDefinition() == typeof(IQueryable<>))) { - return true; + // special case the IEnumerable + return GetInnerGenericType(interfaceType); } } - - return false; } - internal static IEdmTypeReference GetUntypedEdmType(Type clrType) - { - if (clrType == null) - { - throw Error.ArgumentNull(nameof(clrType)); - } + return null; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Catching all exceptions in this case is the right to do.")] + // This code is copied from DefaultHttpControllerTypeResolver.GetControllerTypes. + internal static IEnumerable GetLoadedTypes(IAssemblyResolver assembliesResolver) + { + List result = new List(); - return IsDictionary(clrType) ? - (IEdmTypeReference)EdmUntypedStructuredTypeReference.NullableTypeReference : - (TypeHelper.IsCollection(clrType) ? - (IEdmTypeReference)EdmUntypedHelpers.NullableUntypedCollectionReference : - (IEdmTypeReference)EdmUntypedStructuredTypeReference.NullableTypeReference); + if (assembliesResolver == null) + { + return result; } - /// - /// Returns type of T if the type implements IEnumerable of T, otherwise, return null. - /// - /// - /// - internal static Type GetImplementedIEnumerableType(Type type) + // Go through all assemblies referenced by the application and search for types matching a predicate + IEnumerable assemblies = assembliesResolver.Assemblies; + foreach (Assembly assembly in assemblies) { - // get inner type from Task - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>)) + Type[] exportedTypes = null; + if (assembly == null || assembly.IsDynamic) { - type = type.GetGenericArguments().First(); + // can't call GetTypes on a null (or dynamic?) assembly + continue; } - if (type.IsGenericType && type.IsInterface && - (type.GetGenericTypeDefinition() == typeof(IEnumerable<>) || - type.GetGenericTypeDefinition() == typeof(IQueryable<>))) + try { - // special case the IEnumerable - return GetInnerGenericType(type); + exportedTypes = assembly.GetTypes(); } - else + catch (ReflectionTypeLoadException ex) { - // for the rest of interfaces and strongly Type collections - Type[] interfaces = type.GetInterfaces(); - foreach (Type interfaceType in interfaces) - { - if (interfaceType.IsGenericType && - (interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>) || - interfaceType.GetGenericTypeDefinition() == typeof(IQueryable<>))) - { - // special case the IEnumerable - return GetInnerGenericType(interfaceType); - } - } + exportedTypes = ex.Types; } - - return null; - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Catching all exceptions in this case is the right to do.")] - // This code is copied from DefaultHttpControllerTypeResolver.GetControllerTypes. - internal static IEnumerable GetLoadedTypes(IAssemblyResolver assembliesResolver) - { - List result = new List(); - - if (assembliesResolver == null) + catch { - return result; + continue; } - // Go through all assemblies referenced by the application and search for types matching a predicate - IEnumerable assemblies = assembliesResolver.Assemblies; - foreach (Assembly assembly in assemblies) + if (exportedTypes != null) { - Type[] exportedTypes = null; - if (assembly == null || assembly.IsDynamic) - { - // can't call GetTypes on a null (or dynamic?) assembly - continue; - } - - try - { - exportedTypes = assembly.GetTypes(); - } - catch (ReflectionTypeLoadException ex) - { - exportedTypes = ex.Types; - } - catch - { - continue; - } - - if (exportedTypes != null) - { - result.AddRange(exportedTypes.Where(t => t != null && t.IsVisible)); - } + result.AddRange(exportedTypes.Where(t => t != null && t.IsVisible)); } - - return result; } - internal static Type GetTaskInnerTypeOrSelf(Type type) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>)) - { - return type.GetGenericArguments().First(); - } + return result; + } - return type; + internal static Type GetTaskInnerTypeOrSelf(Type type) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>)) + { + return type.GetGenericArguments().First(); } - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Catching all exceptions in this case is the right to do.")] - internal static bool TryGetInstance(Type type, object value, out object instance) - { - instance = null; + return type; + } - // Trial to create an instance, using parsing - var methodInfo = type.GetMethod("TryParse", BindingFlags.Public | BindingFlags.Static, null, new[] { value.GetType(), type.MakeByRefType() }, null); - if (methodInfo != null) - { - object[] parameters = new object[] { value, null }; - var result = (bool)methodInfo.Invoke(null, parameters); - if (result) - { - instance = parameters[1]; - return true; - } - } + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Catching all exceptions in this case is the right to do.")] + internal static bool TryGetInstance(Type type, object value, out object instance) + { + instance = null; - try - { - // Trial to create an instance, using constructor - instance = Activator.CreateInstance(type, args: value); - if (instance != null) - { - return true; - } - } - catch (Exception) + // Trial to create an instance, using parsing + var methodInfo = type.GetMethod("TryParse", BindingFlags.Public | BindingFlags.Static, null, new[] { value.GetType(), type.MakeByRefType() }, null); + if (methodInfo != null) + { + object[] parameters = new object[] { value, null }; + var result = (bool)methodInfo.Invoke(null, parameters); + if (result) { - // Proceed further + instance = parameters[1]; + return true; } - return false; } - private static Type GetInnerGenericType(Type interfaceType) + try { - // Getting the type T definition if the returning type implements IEnumerable - Type[] parameterTypes = interfaceType.GetGenericArguments(); - - if (parameterTypes.Length == 1) + // Trial to create an instance, using constructor + instance = Activator.CreateInstance(type, args: value); + if (instance != null) { - return parameterTypes[0]; + return true; } + } + catch (Exception) + { + // Proceed further + } + return false; + } + + private static Type GetInnerGenericType(Type interfaceType) + { + // Getting the type T definition if the returning type implements IEnumerable + Type[] parameterTypes = interfaceType.GetGenericArguments(); - return null; + if (parameterTypes.Length == 1) + { + return parameterTypes[0]; } + + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/Delta.cs b/src/Microsoft.AspNetCore.OData/Deltas/Delta.cs index 9eda97a6a..37490efac 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/Delta.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/Delta.cs @@ -9,102 +9,101 @@ using System.Collections.Generic; using System.Dynamic; -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// Base class for the Delta resource +/// +public abstract class Delta : DynamicObject, IDelta { /// - /// Base class for the Delta resource + /// DeltaKind for the objects part of the DeltaResourceSet Payload. /// - public abstract class Delta : DynamicObject, IDelta - { - /// - /// DeltaKind for the objects part of the DeltaResourceSet Payload. - /// - public abstract DeltaItemKind Kind { get; } + public abstract DeltaItemKind Kind { get; } - /// - /// Clears the Delta and resets the underlying Entity. - /// - public abstract void Clear(); + /// + /// Clears the Delta and resets the underlying Entity. + /// + public abstract void Clear(); - /// - /// Attempts to set the Property called to the specified. - /// - /// Only properties that exist on Entity can be set. - /// If there is a type mismatch the request will fail. - /// - /// - /// The name of the Property - /// The new value of the Property - /// True if successful - public abstract bool TrySetPropertyValue(string name, object value); + /// + /// Attempts to set the Property called to the specified. + /// + /// Only properties that exist on Entity can be set. + /// If there is a type mismatch the request will fail. + /// + /// + /// The name of the Property + /// The new value of the Property + /// True if successful + public abstract bool TrySetPropertyValue(string name, object value); - /// - /// Attempts to get the value of the Property called from the underlying Entity. - /// - /// Only properties that exist on Entity can be retrieved. - /// Both modified and unmodified properties can be retrieved. - /// - /// - /// The name of the Property - /// The value of the Property - /// True if the Property was found - public abstract bool TryGetPropertyValue(string name, out object value); + /// + /// Attempts to get the value of the Property called from the underlying Entity. + /// + /// Only properties that exist on Entity can be retrieved. + /// Both modified and unmodified properties can be retrieved. + /// + /// + /// The name of the Property + /// The value of the Property + /// True if the Property was found + public abstract bool TryGetPropertyValue(string name, out object value); - /// - /// Attempts to get the of the Property called from the underlying Entity. - /// - /// Only properties that exist on Entity can be retrieved. - /// Both modified and unmodified properties can be retrieved. - /// - /// - /// The name of the Property - /// The type of the Property - /// Returns true if the Property was found and false if not. - public abstract bool TryGetPropertyType(string name, out Type type); + /// + /// Attempts to get the of the Property called from the underlying Entity. + /// + /// Only properties that exist on Entity can be retrieved. + /// Both modified and unmodified properties can be retrieved. + /// + /// + /// The name of the Property + /// The type of the Property + /// Returns true if the Property was found and false if not. + public abstract bool TryGetPropertyType(string name, out Type type); - /// - /// Overrides the DynamicObject TrySetMember method, so that only the properties - /// of Entity can be set. - /// - public override bool TrySetMember(SetMemberBinder binder, object value) + /// + /// Overrides the DynamicObject TrySetMember method, so that only the properties + /// of Entity can be set. + /// + public override bool TrySetMember(SetMemberBinder binder, object value) + { + if (binder == null) { - if (binder == null) - { - throw Error.ArgumentNull("binder"); - } - - return TrySetPropertyValue(binder.Name, value); + throw Error.ArgumentNull("binder"); } - /// - /// Overrides the DynamicObject TryGetMember method, so that only the properties - /// of Entity can be got. - /// - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - if (binder == null) - { - throw Error.ArgumentNull("binder"); - } + return TrySetPropertyValue(binder.Name, value); + } - return TryGetPropertyValue(binder.Name, out result); + /// + /// Overrides the DynamicObject TryGetMember method, so that only the properties + /// of Entity can be got. + /// + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + if (binder == null) + { + throw Error.ArgumentNull("binder"); } - /// - /// Returns the Properties that have been modified through this Delta as an - /// enumeration of Property Names - /// - public abstract IEnumerable GetChangedPropertyNames(); + return TryGetPropertyValue(binder.Name, out result); + } + + /// + /// Returns the Properties that have been modified through this Delta as an + /// enumeration of Property Names + /// + public abstract IEnumerable GetChangedPropertyNames(); - /// - /// Returns the Properties that have not been modified through this Delta as an - /// enumeration of Property Names - /// - public abstract IEnumerable GetUnchangedPropertyNames(); + /// + /// Returns the Properties that have not been modified through this Delta as an + /// enumeration of Property Names + /// + public abstract IEnumerable GetUnchangedPropertyNames(); - /// - /// Gets the nested resources changed at this level. - /// - public abstract IDictionary GetDeltaNestedNavigationProperties(); - } + /// + /// Gets the nested resources changed at this level. + /// + public abstract IDictionary GetDeltaNestedNavigationProperties(); } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/DeltaDeletedLinkOfT.cs b/src/Microsoft.AspNetCore.OData/Deltas/DeltaDeletedLinkOfT.cs index d2ce1ec0b..45d4c14db 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/DeltaDeletedLinkOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/DeltaDeletedLinkOfT.cs @@ -7,31 +7,30 @@ using System; -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// allows and tracks changes to delta deleted link. +/// +internal sealed class DeltaDeletedLink : DeltaLinkBase, IDeltaDeletedLink where T: class { /// - /// allows and tracks changes to delta deleted link. + /// Initializes a new instance of . /// - internal sealed class DeltaDeletedLink : DeltaLinkBase, IDeltaDeletedLink where T: class + public DeltaDeletedLink() + : base() { - /// - /// Initializes a new instance of . - /// - public DeltaDeletedLink() - : base() - { - } - - /// - /// Initializes a new instance of . - /// - /// The actual structural type. - public DeltaDeletedLink(Type structuralType) - : base(structuralType) - { - } + } - /// - public override DeltaItemKind Kind => DeltaItemKind.DeltaDeletedLink; + /// + /// Initializes a new instance of . + /// + /// The actual structural type. + public DeltaDeletedLink(Type structuralType) + : base(structuralType) + { } + + /// + public override DeltaItemKind Kind => DeltaItemKind.DeltaDeletedLink; } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/DeltaDeletedResourceOfT.cs b/src/Microsoft.AspNetCore.OData/Deltas/DeltaDeletedResourceOfT.cs index 5dc3b01ca..ed907afa2 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/DeltaDeletedResourceOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/DeltaDeletedResourceOfT.cs @@ -10,80 +10,79 @@ using System.Reflection; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// allows and tracks changes to a delta deleted resource. +/// +public class DeltaDeletedResource : Delta, IDeltaDeletedResource where T: class { /// - /// allows and tracks changes to a delta deleted resource. + /// Initializes a new instance of . /// - public class DeltaDeletedResource : Delta, IDeltaDeletedResource where T: class + public DeltaDeletedResource() + : base() { - /// - /// Initializes a new instance of . - /// - public DeltaDeletedResource() - : base() - { - } + } - /// - /// Initializes a new instance of . - /// - /// The derived entity type which the changes would be tracked. - /// should be assignable to instances of . - /// - public DeltaDeletedResource(Type structuralType) - : base(structuralType) - { - } + /// + /// Initializes a new instance of . + /// + /// The derived entity type which the changes would be tracked. + /// should be assignable to instances of . + /// + public DeltaDeletedResource(Type structuralType) + : base(structuralType) + { + } - /// - /// Initializes a new instance of . - /// - /// The derived entity type for which the changes would be tracked. - /// should be assignable to instances of . - /// The set of properties that can be updated or reset. Unknown property - /// names, including those of dynamic properties, are ignored. - public DeltaDeletedResource(Type structuralType, IEnumerable updatableProperties) - : this(structuralType, updatableProperties: updatableProperties, dynamicDictionaryPropertyInfo: null) - { - } + /// + /// Initializes a new instance of . + /// + /// The derived entity type for which the changes would be tracked. + /// should be assignable to instances of . + /// The set of properties that can be updated or reset. Unknown property + /// names, including those of dynamic properties, are ignored. + public DeltaDeletedResource(Type structuralType, IEnumerable updatableProperties) + : this(structuralType, updatableProperties: updatableProperties, dynamicDictionaryPropertyInfo: null) + { + } - /// - /// Initializes a new instance of . - /// - /// The derived entity type which the changes would be tracked. - /// should be assignable to instances of . - /// The set of properties that can be updated or reset. Unknown property - /// names, including those of dynamic properties, are ignored. - /// The property info that is used as dictionary of dynamic - /// properties. null means this entity type is not open. - public DeltaDeletedResource(Type structuralType, IEnumerable updatableProperties, PropertyInfo dynamicDictionaryPropertyInfo) - : base(structuralType, updatableProperties, dynamicDictionaryPropertyInfo) - { - } + /// + /// Initializes a new instance of . + /// + /// The derived entity type which the changes would be tracked. + /// should be assignable to instances of . + /// The set of properties that can be updated or reset. Unknown property + /// names, including those of dynamic properties, are ignored. + /// The property info that is used as dictionary of dynamic + /// properties. null means this entity type is not open. + public DeltaDeletedResource(Type structuralType, IEnumerable updatableProperties, PropertyInfo dynamicDictionaryPropertyInfo) + : base(structuralType, updatableProperties, dynamicDictionaryPropertyInfo) + { + } - /// - /// Initializes a new instance of . - /// - /// The derived entity type which the changes would be tracked. - /// should be assignable to instances of . - /// The set of properties that can be updated or reset. Unknown property - /// names, including those of dynamic properties, are ignored. - /// The property info that is used as dictionary of dynamic - /// If structuralType is a complex type. - /// properties. null means this entity type is not open. - public DeltaDeletedResource(Type structuralType, IEnumerable updatableProperties, PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType) - : base(structuralType, updatableProperties, dynamicDictionaryPropertyInfo, isComplexType) - { - } + /// + /// Initializes a new instance of . + /// + /// The derived entity type which the changes would be tracked. + /// should be assignable to instances of . + /// The set of properties that can be updated or reset. Unknown property + /// names, including those of dynamic properties, are ignored. + /// The property info that is used as dictionary of dynamic + /// If structuralType is a complex type. + /// properties. null means this entity type is not open. + public DeltaDeletedResource(Type structuralType, IEnumerable updatableProperties, PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType) + : base(structuralType, updatableProperties, dynamicDictionaryPropertyInfo, isComplexType) + { + } - /// - public Uri Id { get; set; } + /// + public Uri Id { get; set; } - /// - public DeltaDeletedEntryReason? Reason { get; set; } + /// + public DeltaDeletedEntryReason? Reason { get; set; } - /// - public override DeltaItemKind Kind => DeltaItemKind.DeletedResource; - } + /// + public override DeltaItemKind Kind => DeltaItemKind.DeletedResource; } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/DeltaHelper.cs b/src/Microsoft.AspNetCore.OData/Deltas/DeltaHelper.cs index 25f20d6dd..f4a0f5ae8 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/DeltaHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/DeltaHelper.cs @@ -8,46 +8,45 @@ using Microsoft.AspNetCore.OData.Formatter.Value; using System; -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// The delta helpers. +/// +internal static class DeltaHelper { /// - /// The delta helpers. + /// Helper method to check whether the given type is Delta generic type. /// - internal static class DeltaHelper + /// The type to check. + /// True if it is a Delta generic type; false otherwise. + public static bool IsDeltaOfT(Type type) { - /// - /// Helper method to check whether the given type is Delta generic type. - /// - /// The type to check. - /// True if it is a Delta generic type; false otherwise. - public static bool IsDeltaOfT(Type type) + return type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Delta<>); + } + + /// + /// Helper method to check whether the given object is Delta resource set. + /// + /// The given object. + /// True/False. + public static bool IsDeltaResourceSet(object result) + { + if (result == null) { - return type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Delta<>); + return false; } - /// - /// Helper method to check whether the given object is Delta resource set. - /// - /// The given object. - /// True/False. - public static bool IsDeltaResourceSet(object result) + Type resultType = result.GetType(); + if (typeof(IDeltaSet).IsAssignableFrom(resultType)) { - if (result == null) - { - return false; - } - - Type resultType = result.GetType(); - if (typeof(IDeltaSet).IsAssignableFrom(resultType)) - { - return true; - } - else if (typeof(EdmChangedObjectCollection).IsAssignableFrom(resultType)) - { - return true; - } - - return false; + return true; } + else if (typeof(EdmChangedObjectCollection).IsAssignableFrom(resultType)) + { + return true; + } + + return false; } } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/DeltaItemKind.cs b/src/Microsoft.AspNetCore.OData/Deltas/DeltaItemKind.cs index 844feb86f..d215e1c44 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/DeltaItemKind.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/DeltaItemKind.cs @@ -5,37 +5,36 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// The Kind of the object within the DeltaPayload used to distinguish between +/// Resource/DeletedResource/DeltaDeletedLink/AddedLink. +/// +public enum DeltaItemKind { /// - /// The Kind of the object within the DeltaPayload used to distinguish between - /// Resource/DeletedResource/DeltaDeletedLink/AddedLink. + /// Corresponds to EdmEntityObject (Equivalent of ODataResource in ODL). /// - public enum DeltaItemKind - { - /// - /// Corresponds to EdmEntityObject (Equivalent of ODataResource in ODL). - /// - Resource = 0, + Resource = 0, - /// - /// Corresponds to EdmDeltaDeletedResourceObject (Equivalent of ODataDeletedResource in ODL). - /// - DeletedResource = 1, + /// + /// Corresponds to EdmDeltaDeletedResourceObject (Equivalent of ODataDeletedResource in ODL). + /// + DeletedResource = 1, - /// - /// Corresponds to EdmDeltaDeletedLink (Equivalent of ODataDeltaDeletedLink in ODL). - /// - DeltaDeletedLink = 2, + /// + /// Corresponds to EdmDeltaDeletedLink (Equivalent of ODataDeltaDeletedLink in ODL). + /// + DeltaDeletedLink = 2, - /// - /// Corresponds to EdmDeltaLink (Equivalent of ODataDeltaLink in ODL). - /// - DeltaLink = 3, + /// + /// Corresponds to EdmDeltaLink (Equivalent of ODataDeltaLink in ODL). + /// + DeltaLink = 3, - /// - /// Corresponds to any Unknown item added. - /// - Unknown = 4 - } + /// + /// Corresponds to any Unknown item added. + /// + Unknown = 4 } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/DeltaLinkBaseOfT.cs b/src/Microsoft.AspNetCore.OData/Deltas/DeltaLinkBaseOfT.cs index 6abf6eb04..981193e39 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/DeltaLinkBaseOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/DeltaLinkBaseOfT.cs @@ -7,66 +7,65 @@ using System; -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// Base class for delta link. +/// +internal abstract class DeltaLinkBase : ITypedDelta, IDeltaLinkBase where T : class { /// - /// Base class for delta link. + /// Initializes a new instance of . + /// + protected DeltaLinkBase() + : this(typeof(T)) + { + } + + /// + /// Initializes a new instance of . /// - internal abstract class DeltaLinkBase : ITypedDelta, IDeltaLinkBase where T : class + /// The derived structural type for which the changes would be tracked. + protected DeltaLinkBase(Type structuralType) { - /// - /// Initializes a new instance of . - /// - protected DeltaLinkBase() - : this(typeof(T)) + if (structuralType == null) { + throw Error.ArgumentNull(nameof(structuralType)); } - /// - /// Initializes a new instance of . - /// - /// The derived structural type for which the changes would be tracked. - protected DeltaLinkBase(Type structuralType) + if (!typeof(T).IsAssignableFrom(structuralType)) { - if (structuralType == null) - { - throw Error.ArgumentNull(nameof(structuralType)); - } - - if (!typeof(T).IsAssignableFrom(structuralType)) - { - throw Error.InvalidOperation(SRResources.DeltaEntityTypeNotAssignable, structuralType, typeof(T)); - } - - StructuredType = structuralType; + throw Error.InvalidOperation(SRResources.DeltaEntityTypeNotAssignable, structuralType, typeof(T)); } - /// - public abstract DeltaItemKind Kind { get; } + StructuredType = structuralType; + } - /// - /// Gets the actual type of the structural object for which the changes are tracked. - /// - public virtual Type StructuredType { get; } + /// + public abstract DeltaItemKind Kind { get; } - /// - /// Gets the expected type of the entity for which the changes are tracked. - /// - public virtual Type ExpectedClrType => typeof(T); + /// + /// Gets the actual type of the structural object for which the changes are tracked. + /// + public virtual Type StructuredType { get; } - /// - /// The Uri of the entity from which the relationship is defined, which may be absolute or relative. - /// - public Uri Source { get; set; } + /// + /// Gets the expected type of the entity for which the changes are tracked. + /// + public virtual Type ExpectedClrType => typeof(T); - /// - /// The Uri of the related entity, which may be absolute or relative. - /// - public Uri Target { get; set; } + /// + /// The Uri of the entity from which the relationship is defined, which may be absolute or relative. + /// + public Uri Source { get; set; } - /// - /// The name of the relationship property on the parent object. - /// - public string Relationship { get; set; } - } + /// + /// The Uri of the related entity, which may be absolute or relative. + /// + public Uri Target { get; set; } + + /// + /// The name of the relationship property on the parent object. + /// + public string Relationship { get; set; } } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/DeltaLinkOfT.cs b/src/Microsoft.AspNetCore.OData/Deltas/DeltaLinkOfT.cs index f9c35d605..a4f4a8ab8 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/DeltaLinkOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/DeltaLinkOfT.cs @@ -7,31 +7,30 @@ using System; -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// allows and tracks changes to delta added link. +/// +internal sealed class DeltaLink : DeltaLinkBase, IDeltaLink where T: class { /// - /// allows and tracks changes to delta added link. + /// Initializes a new instance of . /// - internal sealed class DeltaLink : DeltaLinkBase, IDeltaLink where T: class + public DeltaLink() + : base() { - /// - /// Initializes a new instance of . - /// - public DeltaLink() - : base() - { - } - - /// - /// Initializes a new instance of . - /// - /// The derived structural type for which the changes would be tracked. - public DeltaLink(Type structuralType) - : base(structuralType) - { - } + } - /// - public override DeltaItemKind Kind => DeltaItemKind.DeltaLink; + /// + /// Initializes a new instance of . + /// + /// The derived structural type for which the changes would be tracked. + public DeltaLink(Type structuralType) + : base(structuralType) + { } + + /// + public override DeltaItemKind Kind => DeltaItemKind.DeltaLink; } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs b/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs index bbde3507d..84a0180c1 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs @@ -18,829 +18,828 @@ using Microsoft.AspNetCore.OData.Abstracts; using Microsoft.AspNetCore.OData.Common; -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// A class the tracks changes (i.e. the Delta) for a particular . +/// +/// T is the type of the instance this delta tracks changes for. +[NonValidatingParameterBinding] +public class Delta : Delta, IDelta, ITypedDelta where T : class { + // cache property accessors for this type and all its derived types. + private static readonly ConcurrentDictionary>> _propertyCache + = new ConcurrentDictionary>>(); + + private Dictionary> _allProperties; + private List _updatableProperties; + + private HashSet _changedProperties; + + // Nested resources or structures changed at this level. + private IDictionary _deltaNestedResources; + + private T _instance; + private Type _structuredType; + private bool _isComplexType; + + private readonly PropertyInfo _dynamicDictionaryPropertyinfo; + private HashSet _changedDynamicProperties; + private IDictionary _dynamicDictionaryCache; + /// - /// A class the tracks changes (i.e. the Delta) for a particular . + /// Initializes a new instance of . /// - /// T is the type of the instance this delta tracks changes for. - [NonValidatingParameterBinding] - public class Delta : Delta, IDelta, ITypedDelta where T : class + public Delta() + : this(typeof(T)) { - // cache property accessors for this type and all its derived types. - private static readonly ConcurrentDictionary>> _propertyCache - = new ConcurrentDictionary>>(); - - private Dictionary> _allProperties; - private List _updatableProperties; - - private HashSet _changedProperties; - - // Nested resources or structures changed at this level. - private IDictionary _deltaNestedResources; - - private T _instance; - private Type _structuredType; - private bool _isComplexType; - - private readonly PropertyInfo _dynamicDictionaryPropertyinfo; - private HashSet _changedDynamicProperties; - private IDictionary _dynamicDictionaryCache; + } - /// - /// Initializes a new instance of . - /// - public Delta() - : this(typeof(T)) - { - } + /// + /// Initializes a new instance of . + /// + /// The derived entity type or complex type for which the changes would be tracked. + /// should be assignable to instances of . + /// + public Delta(Type structuralType) + : this(structuralType, updatableProperties: null) + { + } - /// - /// Initializes a new instance of . - /// - /// The derived entity type or complex type for which the changes would be tracked. - /// should be assignable to instances of . - /// - public Delta(Type structuralType) - : this(structuralType, updatableProperties: null) - { - } + /// + /// Initializes a new instance of . + /// + /// The derived entity type or complex type for which the changes would be tracked. + /// should be assignable to instances of . + /// + /// The set of properties that can be updated or reset. Unknown property + /// names, including those of dynamic properties, are ignored. + public Delta(Type structuralType, IEnumerable updatableProperties) + : this(structuralType, updatableProperties: updatableProperties, dynamicDictionaryPropertyInfo: null) + { + } - /// - /// Initializes a new instance of . - /// - /// The derived entity type or complex type for which the changes would be tracked. - /// should be assignable to instances of . - /// - /// The set of properties that can be updated or reset. Unknown property - /// names, including those of dynamic properties, are ignored. - public Delta(Type structuralType, IEnumerable updatableProperties) - : this(structuralType, updatableProperties: updatableProperties, dynamicDictionaryPropertyInfo: null) - { - } + /// + /// Initializes a new instance of . + /// + /// The derived entity type or complex type for which the changes would be tracked. + /// should be assignable to instances of . + /// + /// The set of properties that can be updated or reset. Unknown property + /// names, including those of dynamic properties, are ignored. + /// The property info that is used as dictionary of dynamic + /// properties. null means this entity type is not open. + public Delta(Type structuralType, IEnumerable updatableProperties, + PropertyInfo dynamicDictionaryPropertyInfo) + :this(structuralType, updatableProperties, dynamicDictionaryPropertyInfo, isComplexType: false) + { + } - /// - /// Initializes a new instance of . - /// - /// The derived entity type or complex type for which the changes would be tracked. - /// should be assignable to instances of . - /// - /// The set of properties that can be updated or reset. Unknown property - /// names, including those of dynamic properties, are ignored. - /// The property info that is used as dictionary of dynamic - /// properties. null means this entity type is not open. - public Delta(Type structuralType, IEnumerable updatableProperties, - PropertyInfo dynamicDictionaryPropertyInfo) - :this(structuralType, updatableProperties, dynamicDictionaryPropertyInfo, isComplexType: false) - { - } + /// + /// Initializes a new instance of . + /// + /// The derived entity type or complex type for which the changes would be tracked. + /// should be assignable to instances of . + /// + /// The set of properties that can be updated or reset. Unknown property + /// names, including those of dynamic properties, are ignored. + /// The property info that is used as dictionary of dynamic + /// properties. null means this entity type is not open. + /// If structuralType is a complex type. + public Delta(Type structuralType, IEnumerable updatableProperties, + PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType) + { + _dynamicDictionaryPropertyinfo = dynamicDictionaryPropertyInfo; + _isComplexType = isComplexType; + Reset(structuralType); + InitializeProperties(updatableProperties); + } - /// - /// Initializes a new instance of . - /// - /// The derived entity type or complex type for which the changes would be tracked. - /// should be assignable to instances of . - /// - /// The set of properties that can be updated or reset. Unknown property - /// names, including those of dynamic properties, are ignored. - /// The property info that is used as dictionary of dynamic - /// properties. null means this entity type is not open. - /// If structuralType is a complex type. - public Delta(Type structuralType, IEnumerable updatableProperties, - PropertyInfo dynamicDictionaryPropertyInfo, bool isComplexType) - { - _dynamicDictionaryPropertyinfo = dynamicDictionaryPropertyInfo; - _isComplexType = isComplexType; - Reset(structuralType); - InitializeProperties(updatableProperties); - } + /// + public override DeltaItemKind Kind => DeltaItemKind.Resource; - /// - public override DeltaItemKind Kind => DeltaItemKind.Resource; + /// + public virtual Type StructuredType => _structuredType; - /// - public virtual Type StructuredType => _structuredType; + /// + public virtual Type ExpectedClrType => typeof(T); - /// - public virtual Type ExpectedClrType => typeof(T); + /// + /// The list of property names that can be updated. + /// + /// When the list is modified, any modified properties that were removed from the list are no longer + /// considered to be changed. + public IList UpdatableProperties => _updatableProperties; - /// - /// The list of property names that can be updated. - /// - /// When the list is modified, any modified properties that were removed from the list are no longer - /// considered to be changed. - public IList UpdatableProperties => _updatableProperties; + /// + /// If the StructuralType is a Complex type. + /// + public bool IsComplexType => _isComplexType; - /// - /// If the StructuralType is a Complex type. - /// - public bool IsComplexType => _isComplexType; + /// + public override void Clear() + { + Reset(_structuredType); + } - /// - public override void Clear() + /// + public override bool TrySetPropertyValue(string name, object value) + { + if (string.IsNullOrWhiteSpace(name)) { - Reset(_structuredType); + throw Error.ArgumentNull(nameof(name)); } - /// - public override bool TrySetPropertyValue(string name, object value) + if (_dynamicDictionaryPropertyinfo != null) { - if (string.IsNullOrWhiteSpace(name)) - { - throw Error.ArgumentNull(nameof(name)); - } - - if (_dynamicDictionaryPropertyinfo != null) + // Dynamic property can have the same name as the dynamic property dictionary. + if (name == _dynamicDictionaryPropertyinfo.Name || + !_allProperties.ContainsKey(name)) { - // Dynamic property can have the same name as the dynamic property dictionary. - if (name == _dynamicDictionaryPropertyinfo.Name || - !_allProperties.ContainsKey(name)) + if (_dynamicDictionaryCache == null) { - if (_dynamicDictionaryCache == null) - { - _dynamicDictionaryCache = - GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, _instance, create: true); - } - - _dynamicDictionaryCache[name] = value; - _changedDynamicProperties.Add(name); - return true; + _dynamicDictionaryCache = + GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, _instance, create: true); } - } - if (value is IDelta || value is IDeltaSet) - { - return TrySetNestedResourceInternal(name, value); - } - else - { - return TrySetPropertyValueInternal(name, value); + _dynamicDictionaryCache[name] = value; + _changedDynamicProperties.Add(name); + return true; } } - /// - public override bool TryGetPropertyValue(string name, out object value) + if (value is IDelta || value is IDeltaSet) { - if (name == null) - { - throw Error.ArgumentNull(nameof(name)); - } + return TrySetNestedResourceInternal(name, value); + } + else + { + return TrySetPropertyValueInternal(name, value); + } + } - if (_dynamicDictionaryPropertyinfo != null) - { - if (_dynamicDictionaryCache == null) - { - _dynamicDictionaryCache = - GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, _instance, create: false); - } + /// + public override bool TryGetPropertyValue(string name, out object value) + { + if (name == null) + { + throw Error.ArgumentNull(nameof(name)); + } - if (_dynamicDictionaryCache != null && _dynamicDictionaryCache.TryGetValue(name, out value)) - { - return true; - } + if (_dynamicDictionaryPropertyinfo != null) + { + if (_dynamicDictionaryCache == null) + { + _dynamicDictionaryCache = + GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, _instance, create: false); } - if (TryGetNestedPropertyValue(name, out value)) + if (_dynamicDictionaryCache != null && _dynamicDictionaryCache.TryGetValue(name, out value)) { return true; } - else - { - // try to retrieve the value of property. - PropertyAccessor cacheHit; - if (_allProperties.TryGetValue(name, out cacheHit)) - { - value = cacheHit.GetValue(_instance); - return true; - } - } - - value = null; - return false; } - /// - /// Attempts to get the value of the nested Property called from the underlying resource. - /// - /// Only properties that exist on Entity can be retrieved. - /// Only modified nested properties can be retrieved. - /// The nested Property type will be of its defined type. - /// - /// - /// The name of the nested Property - /// The value of the nested Property - /// True if the Property was found and is a nested Property - internal bool TryGetNestedPropertyValue(string name, out object value) - { - if (name == null) + if (TryGetNestedPropertyValue(name, out value)) + { + return true; + } + else + { + // try to retrieve the value of property. + PropertyAccessor cacheHit; + if (_allProperties.TryGetValue(name, out cacheHit)) { - throw Error.ArgumentNull(nameof(name)); + value = cacheHit.GetValue(_instance); + return true; } + } - if (!_deltaNestedResources.ContainsKey(name)) - { - value = null; - return false; - } + value = null; + return false; + } - // This is a nested resource, the value returned must be an IDelta - // from the dictionary of nested resources to allow the traversal of - // hierarchies of Delta. - object deltaNestedResource = _deltaNestedResources[name]; + /// + /// Attempts to get the value of the nested Property called from the underlying resource. + /// + /// Only properties that exist on Entity can be retrieved. + /// Only modified nested properties can be retrieved. + /// The nested Property type will be of its defined type. + /// + /// + /// The name of the nested Property + /// The value of the nested Property + /// True if the Property was found and is a nested Property + internal bool TryGetNestedPropertyValue(string name, out object value) + { + if (name == null) + { + throw Error.ArgumentNull(nameof(name)); + } - Contract.Assert(deltaNestedResource != null, "deltaNestedResource != null"); + if (!_deltaNestedResources.ContainsKey(name)) + { + value = null; + return false; + } - //If DeltaSet collection, we are handling delta collections so the value will be that itself and no need to get instance value - if (deltaNestedResource is IDeltaSet) - { - value = deltaNestedResource; - return true; - } + // This is a nested resource, the value returned must be an IDelta + // from the dictionary of nested resources to allow the traversal of + // hierarchies of Delta. + object deltaNestedResource = _deltaNestedResources[name]; - Contract.Assert(DeltaHelper.IsDeltaOfT(deltaNestedResource.GetType())); + Contract.Assert(deltaNestedResource != null, "deltaNestedResource != null"); + //If DeltaSet collection, we are handling delta collections so the value will be that itself and no need to get instance value + if (deltaNestedResource is IDeltaSet) + { value = deltaNestedResource; return true; } - /// - public override bool TryGetPropertyType(string name, out Type type) + Contract.Assert(DeltaHelper.IsDeltaOfT(deltaNestedResource.GetType())); + + value = deltaNestedResource; + return true; + } + + /// + public override bool TryGetPropertyType(string name, out Type type) + { + if (name == null) + { + throw Error.ArgumentNull(nameof(name)); + } + + if (_dynamicDictionaryPropertyinfo != null) { - if (name == null) + if (_dynamicDictionaryCache == null) { - throw Error.ArgumentNull(nameof(name)); + _dynamicDictionaryCache = + GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, _instance, create: false); } - if (_dynamicDictionaryPropertyinfo != null) + object dynamicValue; + if (_dynamicDictionaryCache != null && + _dynamicDictionaryCache.TryGetValue(name, out dynamicValue)) { - if (_dynamicDictionaryCache == null) + if (dynamicValue == null) { - _dynamicDictionaryCache = - GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, _instance, create: false); - } - - object dynamicValue; - if (_dynamicDictionaryCache != null && - _dynamicDictionaryCache.TryGetValue(name, out dynamicValue)) - { - if (dynamicValue == null) - { - type = null; - return false; - } - - type = dynamicValue.GetType(); - return true; + type = null; + return false; } - } - PropertyAccessor value; - if (_allProperties.TryGetValue(name, out value)) - { - type = value.Property.PropertyType; + type = dynamicValue.GetType(); return true; } - - type = null; - return false; } - /// - /// Returns the instance that holds all the changes (and original values) being tracked by this Delta. - /// - public T GetInstance() + PropertyAccessor value; + if (_allProperties.TryGetValue(name, out value)) { - return _instance; + type = value.Property.PropertyType; + return true; } - /// - /// Returns the known properties that have been modified through this as an - /// of property Names. - /// Includes the structural properties at current level. - /// Does not include the names of the changed dynamic properties. - /// - public override IEnumerable GetChangedPropertyNames() + type = null; + return false; + } + + /// + /// Returns the instance that holds all the changes (and original values) being tracked by this Delta. + /// + public T GetInstance() + { + return _instance; + } + + /// + /// Returns the known properties that have been modified through this as an + /// of property Names. + /// Includes the structural properties at current level. + /// Does not include the names of the changed dynamic properties. + /// + public override IEnumerable GetChangedPropertyNames() + { + return _changedProperties.Intersect(_updatableProperties).Concat(_deltaNestedResources.Keys); + } + + /// + /// Returns the known properties that have not been modified through this as an + /// of property Names. Does not include the names of the changed dynamic + /// properties. + /// + public override IEnumerable GetUnchangedPropertyNames() + { + // UpdatableProperties could include arbitrary strings, filter by _allProperties + return _updatableProperties.Intersect(_allProperties.Keys).Except(GetChangedPropertyNames()); + } + + /// + /// Gets the nested resources changed at this level. + /// + public override IDictionary GetDeltaNestedNavigationProperties() + { + return _deltaNestedResources; + } + + /// + /// Copies the changed property values from the underlying entity (accessible via ) + /// to the entity recursively. + /// + /// The entity to be updated. + public void CopyChangedValues(T original) + { + if (original == null) { - return _changedProperties.Intersect(_updatableProperties).Concat(_deltaNestedResources.Keys); + throw Error.ArgumentNull(nameof(original)); } - /// - /// Returns the known properties that have not been modified through this as an - /// of property Names. Does not include the names of the changed dynamic - /// properties. - /// - public override IEnumerable GetUnchangedPropertyNames() + // Delta parameter type cannot be derived type of original + // to prevent unrecognizable information from being applied to original resource. + if (!_structuredType.IsAssignableFrom(original.GetType())) { - // UpdatableProperties could include arbitrary strings, filter by _allProperties - return _updatableProperties.Intersect(_allProperties.Keys).Except(GetChangedPropertyNames()); + throw Error.Argument(nameof(original), SRResources.DeltaTypeMismatch, _structuredType, original.GetType()); } - /// - /// Gets the nested resources changed at this level. - /// - public override IDictionary GetDeltaNestedNavigationProperties() + RuntimeHelpers.EnsureSufficientExecutionStack(); + + // For regular non-structural properties at current level. + PropertyAccessor[] propertiesToCopy = + _changedProperties.Intersect(_updatableProperties).Select(s => _allProperties[s]).ToArray(); + foreach (PropertyAccessor propertyToCopy in propertiesToCopy) { - return _deltaNestedResources; + propertyToCopy.Copy(_instance, original); } - /// - /// Copies the changed property values from the underlying entity (accessible via ) - /// to the entity recursively. - /// - /// The entity to be updated. - public void CopyChangedValues(T original) + CopyChangedDynamicValues(original); + + // For nested resources. + foreach (string nestedResourceName in _deltaNestedResources.Keys) { - if (original == null) + // Patch for each nested resource changed under this T. + dynamic deltaNestedResource = _deltaNestedResources[nestedResourceName]; + + if (deltaNestedResource is IDeltaSet) { - throw Error.ArgumentNull(nameof(original)); + // TODO: That's the bulk insert OData Path handler feature, + // See the comments in https://github.com/OData/AspNetCoreOData/issues/748 + // So far, Let's skip DeltaSet and figure it out later. + continue; } - // Delta parameter type cannot be derived type of original - // to prevent unrecognizable information from being applied to original resource. - if (!_structuredType.IsAssignableFrom(original.GetType())) + dynamic originalNestedResource = null; + if (!TryGetPropertyRef(original, nestedResourceName, out originalNestedResource)) { - throw Error.Argument(nameof(original), SRResources.DeltaTypeMismatch, _structuredType, original.GetType()); + throw Error.Argument(nestedResourceName, SRResources.DeltaNestedResourceNameNotFound, + nestedResourceName, original.GetType()); } - RuntimeHelpers.EnsureSufficientExecutionStack(); - - // For regular non-structural properties at current level. - PropertyAccessor[] propertiesToCopy = - _changedProperties.Intersect(_updatableProperties).Select(s => _allProperties[s]).ToArray(); - foreach (PropertyAccessor propertyToCopy in propertiesToCopy) + if (originalNestedResource == null) { - propertyToCopy.Copy(_instance, original); - } + // When patching original target of null value, directly set nested resource. + dynamic deltaObject = _deltaNestedResources[nestedResourceName]; + dynamic instance = deltaObject.GetInstance(); - CopyChangedDynamicValues(original); + // Recursively patch up the instance with the nested resources. + deltaObject.CopyChangedValues(instance); - // For nested resources. - foreach (string nestedResourceName in _deltaNestedResources.Keys) + _allProperties[nestedResourceName].SetValue(original, instance); + } + else { - // Patch for each nested resource changed under this T. - dynamic deltaNestedResource = _deltaNestedResources[nestedResourceName]; + // Recursively patch the subtree. + bool isDeltaType = DeltaHelper.IsDeltaOfT(deltaNestedResource.GetType()); + Contract.Assert(isDeltaType, nestedResourceName + "'s corresponding value should be Delta type but is not."); - if (deltaNestedResource is IDeltaSet) - { - // TODO: That's the bulk insert OData Path handler feature, - // See the comments in https://github.com/OData/AspNetCoreOData/issues/748 - // So far, Let's skip DeltaSet and figure it out later. - continue; - } + Type newType = deltaNestedResource.StructuredType; + Type originalType = originalNestedResource.GetType(); - dynamic originalNestedResource = null; - if (!TryGetPropertyRef(original, nestedResourceName, out originalNestedResource)) + if (deltaNestedResource.IsComplexType && newType != originalType) { - throw Error.Argument(nestedResourceName, SRResources.DeltaNestedResourceNameNotFound, - nestedResourceName, original.GetType()); + originalNestedResource = ReAssignComplexDerivedType(originalNestedResource, newType, originalType, deltaNestedResource.ExpectedClrType); + _structuredType.GetProperty(nestedResourceName).SetValue(original, (object)originalNestedResource); } - if (originalNestedResource == null) - { - // When patching original target of null value, directly set nested resource. - dynamic deltaObject = _deltaNestedResources[nestedResourceName]; - dynamic instance = deltaObject.GetInstance(); + deltaNestedResource.CopyChangedValues(originalNestedResource); + } + } + } - // Recursively patch up the instance with the nested resources. - deltaObject.CopyChangedValues(instance); + /// + /// Copies the unchanged property values from the underlying entity (accessible via ) + /// to the entity. + /// + /// The entity to be updated. + public void CopyUnchangedValues(T original) + { + if (original == null) + { + throw Error.ArgumentNull(nameof(original)); + } - _allProperties[nestedResourceName].SetValue(original, instance); - } - else - { - // Recursively patch the subtree. - bool isDeltaType = DeltaHelper.IsDeltaOfT(deltaNestedResource.GetType()); - Contract.Assert(isDeltaType, nestedResourceName + "'s corresponding value should be Delta type but is not."); + if (!_structuredType.IsInstanceOfType(original)) + { + throw Error.Argument(nameof(original), SRResources.DeltaTypeMismatch, _structuredType, original.GetType()); + } - Type newType = deltaNestedResource.StructuredType; - Type originalType = originalNestedResource.GetType(); + IEnumerable> propertiesToCopy = GetUnchangedPropertyNames().Select(s => _allProperties[s]); + foreach (PropertyAccessor propertyToCopy in propertiesToCopy) + { + propertyToCopy.Copy(_instance, original); + } - if (deltaNestedResource.IsComplexType && newType != originalType) - { - originalNestedResource = ReAssignComplexDerivedType(originalNestedResource, newType, originalType, deltaNestedResource.ExpectedClrType); - _structuredType.GetProperty(nestedResourceName).SetValue(original, (object)originalNestedResource); - } + CopyUnchangedDynamicValues(original); + } - deltaNestedResource.CopyChangedValues(originalNestedResource); - } - } + /// + /// Overwrites the entity with the changes tracked by this Delta. + /// The semantics of this operation are equivalent to a HTTP PATCH operation, hence the name. + /// + /// The entity to be updated. + /// The updated entity. + public T Patch(T original) + { + if (IsComplexType) + { + original = ReAssignComplexDerivedType(original, _structuredType, original.GetType(), ExpectedClrType) as T; } - /// - /// Copies the unchanged property values from the underlying entity (accessible via ) - /// to the entity. - /// - /// The entity to be updated. - public void CopyUnchangedValues(T original) - { - if (original == null) - { - throw Error.ArgumentNull(nameof(original)); - } + CopyChangedValues(original); - if (!_structuredType.IsInstanceOfType(original)) - { - throw Error.Argument(nameof(original), SRResources.DeltaTypeMismatch, _structuredType, original.GetType()); - } + return original; + } - IEnumerable> propertiesToCopy = GetUnchangedPropertyNames().Select(s => _allProperties[s]); - foreach (PropertyAccessor propertyToCopy in propertiesToCopy) - { - propertyToCopy.Copy(_instance, original); - } + /// + /// Overwrites the entity with the values stored in this Delta. + /// The semantics of this operation are equivalent to a HTTP PUT operation, hence the name. + /// + /// The entity to be updated. + public void Put(T original) + { + CopyChangedValues(original); + CopyUnchangedValues(original); + } - CopyUnchangedDynamicValues(original); - } + private dynamic ReAssignComplexDerivedType(dynamic originalValue, Type newType, Type originalType, Type declaredType) + { + // As per OASIS discussion, changing a complex type from 1 derived type to another is allowed if both derived type have a common ancestor and the property + // is declared in terms of a common ancestor. The logic below checks for a common ancestor. Create a new object of the derived type in delta request. + // And copy the common properties. - /// - /// Overwrites the entity with the changes tracked by this Delta. - /// The semantics of this operation are equivalent to a HTTP PATCH operation, hence the name. - /// - /// The entity to be updated. - /// The updated entity. - public T Patch(T original) + if (newType == originalType) { - if (IsComplexType) - { - original = ReAssignComplexDerivedType(original, _structuredType, original.GetType(), ExpectedClrType) as T; - } - - CopyChangedValues(original); - - return original; + return originalValue; } - /// - /// Overwrites the entity with the values stored in this Delta. - /// The semantics of this operation are equivalent to a HTTP PUT operation, hence the name. - /// - /// The entity to be updated. - public void Put(T original) + Type newBaseType = newType; + HashSet newBaseTypes = new HashSet(); + + //Iterate till you find the declaring base type and add all that to hashset + while (newBaseType != null && newBaseType != declaredType) { - CopyChangedValues(original); - CopyUnchangedValues(original); + newBaseTypes.Add(newBaseType); + newBaseType = newBaseType.BaseType; } - private dynamic ReAssignComplexDerivedType(dynamic originalValue, Type newType, Type originalType, Type declaredType) - { - // As per OASIS discussion, changing a complex type from 1 derived type to another is allowed if both derived type have a common ancestor and the property - // is declared in terms of a common ancestor. The logic below checks for a common ancestor. Create a new object of the derived type in delta request. - // And copy the common properties. + newBaseTypes.Add(declaredType); - if (newType == originalType) - { - return originalValue; - } + //Here original type is the type for original (T) resource. + //We will keep going to base types and finally will get the Common Basetype for the derived complex types in to the originalType variable. - Type newBaseType = newType; - HashSet newBaseTypes = new HashSet(); + //The new Original type, means the new complex type (T) which will replace the current complex type. + dynamic newOriginalNestedResource = originalValue; - //Iterate till you find the declaring base type and add all that to hashset - while (newBaseType != null && newBaseType != declaredType) + while (originalType != null) + { + if (newBaseTypes.Contains(originalType)) { - newBaseTypes.Add(newBaseType); - newBaseType = newBaseType.BaseType; - } - - newBaseTypes.Add(declaredType); + //Now originalType = common base type of the derived complex types. + //OriginalNested Resource = T(of current Complex type). We are creating newOriginalNestedResource (T - new complex type). + newOriginalNestedResource = Activator.CreateInstance(newType); - //Here original type is the type for original (T) resource. - //We will keep going to base types and finally will get the Common Basetype for the derived complex types in to the originalType variable. + //Here we get all the properties of common base type and get value from original complex type(T) and + //copy it to the new complex type newOriginalNestedResource(came as a part of Delta) - //The new Original type, means the new complex type (T) which will replace the current complex type. - dynamic newOriginalNestedResource = originalValue; - - while (originalType != null) - { - if (newBaseTypes.Contains(originalType)) + foreach (PropertyInfo property in originalType.GetProperties()) { - //Now originalType = common base type of the derived complex types. - //OriginalNested Resource = T(of current Complex type). We are creating newOriginalNestedResource (T - new complex type). - newOriginalNestedResource = Activator.CreateInstance(newType); - - //Here we get all the properties of common base type and get value from original complex type(T) and - //copy it to the new complex type newOriginalNestedResource(came as a part of Delta) - - foreach (PropertyInfo property in originalType.GetProperties()) - { - object value = property.GetValue(originalValue); - property.SetValue(newOriginalNestedResource, value); - } - - break; + object value = property.GetValue(originalValue); + property.SetValue(newOriginalNestedResource, value); } - originalType = originalType.BaseType; + break; } - return newOriginalNestedResource; + originalType = originalType.BaseType; } - private static void CopyDynamicPropertyDictionary(IDictionary source, - IDictionary dest, PropertyInfo dynamicPropertyInfo, T targetEntity) - { - Contract.Assert(source != null); - Contract.Assert(dynamicPropertyInfo != null); - Contract.Assert(targetEntity != null); + return newOriginalNestedResource; + } + + private static void CopyDynamicPropertyDictionary(IDictionary source, + IDictionary dest, PropertyInfo dynamicPropertyInfo, T targetEntity) + { + Contract.Assert(source != null); + Contract.Assert(dynamicPropertyInfo != null); + Contract.Assert(targetEntity != null); - if (source.Count == 0) + if (source.Count == 0) + { + if (dest != null) { - if (dest != null) - { - dest.Clear(); - } + dest.Clear(); + } + } + else + { + if (dest == null) + { + dest = GetDynamicPropertyDictionary(dynamicPropertyInfo, targetEntity, create: true); } else { - if (dest == null) - { - dest = GetDynamicPropertyDictionary(dynamicPropertyInfo, targetEntity, create: true); - } - else - { - dest.Clear(); - } + dest.Clear(); + } - foreach (KeyValuePair item in source) - { - dest.Add(item); - } + foreach (KeyValuePair item in source) + { + dest.Add(item); } } + } + + private static IDictionary GetDynamicPropertyDictionary(PropertyInfo propertyInfo, + T entity, bool create) + { + Contract.Assert(propertyInfo != null); + Contract.Assert(entity != null); - private static IDictionary GetDynamicPropertyDictionary(PropertyInfo propertyInfo, - T entity, bool create) + object propertyValue = propertyInfo.GetValue(entity); + if (propertyValue != null) { - Contract.Assert(propertyInfo != null); - Contract.Assert(entity != null); + return (IDictionary)propertyValue; + } - object propertyValue = propertyInfo.GetValue(entity); - if (propertyValue != null) + if (create) + { + if (!propertyInfo.CanWrite) { - return (IDictionary)propertyValue; + throw Error.InvalidOperation(SRResources.CannotSetDynamicPropertyDictionary, propertyInfo.Name, + entity.GetType().FullName); } + IDictionary newPropertyValue = new Dictionary(); - if (create) - { - if (!propertyInfo.CanWrite) - { - throw Error.InvalidOperation(SRResources.CannotSetDynamicPropertyDictionary, propertyInfo.Name, - entity.GetType().FullName); - } - IDictionary newPropertyValue = new Dictionary(); + propertyInfo.SetValue(entity, newPropertyValue); + return newPropertyValue; + } - propertyInfo.SetValue(entity, newPropertyValue); - return newPropertyValue; - } + return null; + } - return null; + /// + /// Attempts to get the property by the specified name. + /// + /// The structural object. + /// Name of the property. + /// Output for property value. + /// true if the property is found; false otherwise. + private static bool TryGetPropertyRef(T structural, string propertyName, + out dynamic propertyRef) + { + propertyRef = null; + PropertyInfo propertyInfo = structural.GetType().GetProperty(propertyName); + if (propertyInfo != null) + { + propertyRef = propertyInfo.GetValue(structural, null); + return true; } - /// - /// Attempts to get the property by the specified name. - /// - /// The structural object. - /// Name of the property. - /// Output for property value. - /// true if the property is found; false otherwise. - private static bool TryGetPropertyRef(T structural, string propertyName, - out dynamic propertyRef) - { - propertyRef = null; - PropertyInfo propertyInfo = structural.GetType().GetProperty(propertyName); - if (propertyInfo != null) - { - propertyRef = propertyInfo.GetValue(structural, null); - return true; - } + return false; + } - return false; + private void Reset(Type structuralType) + { + if (structuralType == null) + { + throw Error.ArgumentNull(nameof(structuralType)); } - private void Reset(Type structuralType) + if (!typeof(T).IsAssignableFrom(structuralType)) { - if (structuralType == null) - { - throw Error.ArgumentNull(nameof(structuralType)); - } + throw Error.InvalidOperation(SRResources.DeltaEntityTypeNotAssignable, structuralType, typeof(T)); + } - if (!typeof(T).IsAssignableFrom(structuralType)) - { - throw Error.InvalidOperation(SRResources.DeltaEntityTypeNotAssignable, structuralType, typeof(T)); - } + _instance = Activator.CreateInstance(structuralType) as T; + _changedProperties = new HashSet(); + _deltaNestedResources = new Dictionary(); + _structuredType = structuralType; - _instance = Activator.CreateInstance(structuralType) as T; - _changedProperties = new HashSet(); - _deltaNestedResources = new Dictionary(); - _structuredType = structuralType; + _changedDynamicProperties = new HashSet(); + _dynamicDictionaryCache = null; + } - _changedDynamicProperties = new HashSet(); - _dynamicDictionaryCache = null; + private void InitializeProperties(IEnumerable updatableProperties) + { + _allProperties = _propertyCache.GetOrAdd( + _structuredType, + (backingType) => backingType + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => !IsIgnoredProperty(backingType.GetCustomAttributes(typeof(DataContractAttribute), inherit: true).Any(), p) && (p.GetSetMethod() != null || TypeHelper.IsCollection(p.PropertyType)) && p.GetGetMethod() != null) + .Select>(p => new FastPropertyAccessor(p)) + .ToDictionary(p => p.Property.Name)); + + if (updatableProperties != null) + { + _updatableProperties = updatableProperties.Intersect(_allProperties.Keys).ToList(); + } + else + { + _updatableProperties = new List(_allProperties.Keys); } - private void InitializeProperties(IEnumerable updatableProperties) + if (_dynamicDictionaryPropertyinfo != null) { - _allProperties = _propertyCache.GetOrAdd( - _structuredType, - (backingType) => backingType - .GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(p => !IsIgnoredProperty(backingType.GetCustomAttributes(typeof(DataContractAttribute), inherit: true).Any(), p) && (p.GetSetMethod() != null || TypeHelper.IsCollection(p.PropertyType)) && p.GetGetMethod() != null) - .Select>(p => new FastPropertyAccessor(p)) - .ToDictionary(p => p.Property.Name)); + _updatableProperties.Remove(_dynamicDictionaryPropertyinfo.Name); + } + } - if (updatableProperties != null) - { - _updatableProperties = updatableProperties.Intersect(_allProperties.Keys).ToList(); - } - else - { - _updatableProperties = new List(_allProperties.Keys); - } + private static bool IsIgnoredProperty(bool isTypeDataContract, PropertyInfo propertyInfo) + { + //This is for Ignoring the property that matches below criteria + //1. Its marked as NotMapped + //2. Its a datacontract type but property is not marked as datamember + //3. Its marked with IgnoreDataMember (but not where types datacontract and property marked with datamember) - if (_dynamicDictionaryPropertyinfo != null) - { - _updatableProperties.Remove(_dynamicDictionaryPropertyinfo.Name); - } - } + bool hasNotMappedAttr = propertyInfo.GetCustomAttributes(typeof(NotMappedAttribute), inherit: true).Any(); - private static bool IsIgnoredProperty(bool isTypeDataContract, PropertyInfo propertyInfo) + if (hasNotMappedAttr) { - //This is for Ignoring the property that matches below criteria - //1. Its marked as NotMapped - //2. Its a datacontract type but property is not marked as datamember - //3. Its marked with IgnoreDataMember (but not where types datacontract and property marked with datamember) - - bool hasNotMappedAttr = propertyInfo.GetCustomAttributes(typeof(NotMappedAttribute), inherit: true).Any(); + return true; + } - if (hasNotMappedAttr) - { - return true; - } + if (isTypeDataContract) + { + return !propertyInfo.GetCustomAttributes(typeof(DataMemberAttribute), inherit: true).Any(); + } - if (isTypeDataContract) - { - return !propertyInfo.GetCustomAttributes(typeof(DataMemberAttribute), inherit: true).Any(); - } + return propertyInfo.GetCustomAttributes(typeof(IgnoreDataMemberAttribute), inherit: true).Any(); + } - return propertyInfo.GetCustomAttributes(typeof(IgnoreDataMemberAttribute), inherit: true).Any(); + // Copy changed dynamic properties and leave the unchanged dynamic properties + private void CopyChangedDynamicValues(T targetEntity) + { + if (_dynamicDictionaryPropertyinfo == null) + { + return; } - // Copy changed dynamic properties and leave the unchanged dynamic properties - private void CopyChangedDynamicValues(T targetEntity) + if (_dynamicDictionaryCache == null) { - if (_dynamicDictionaryPropertyinfo == null) - { - return; - } + _dynamicDictionaryCache = + GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, _instance, create: false); + } - if (_dynamicDictionaryCache == null) - { - _dynamicDictionaryCache = - GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, _instance, create: false); - } + IDictionary fromDictionary = _dynamicDictionaryCache; + if (fromDictionary == null) + { + return; + } - IDictionary fromDictionary = _dynamicDictionaryCache; - if (fromDictionary == null) - { - return; - } + IDictionary toDictionary = + GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, targetEntity, create: false); - IDictionary toDictionary = - GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, targetEntity, create: false); + IDictionary tempDictionary = toDictionary != null + ? new Dictionary(toDictionary) + : new Dictionary(); - IDictionary tempDictionary = toDictionary != null - ? new Dictionary(toDictionary) - : new Dictionary(); + foreach (string dynamicPropertyName in _changedDynamicProperties) + { + object dynamicPropertyValue = fromDictionary[dynamicPropertyName]; - foreach (string dynamicPropertyName in _changedDynamicProperties) + // a dynamic property value equal to null, it means to remove this dynamic property + if (dynamicPropertyValue == null) { - object dynamicPropertyValue = fromDictionary[dynamicPropertyName]; - - // a dynamic property value equal to null, it means to remove this dynamic property - if (dynamicPropertyValue == null) + tempDictionary.Remove(dynamicPropertyName); + } + else + { + if (dynamicPropertyValue is IDelta) { - tempDictionary.Remove(dynamicPropertyName); + dynamic deltaObject = dynamicPropertyValue; + dynamic instance = deltaObject.GetInstance(); + + deltaObject.CopyChangedValues(instance); + tempDictionary[dynamicPropertyName] = instance; } else { - if (dynamicPropertyValue is IDelta) - { - dynamic deltaObject = dynamicPropertyValue; - dynamic instance = deltaObject.GetInstance(); - - deltaObject.CopyChangedValues(instance); - tempDictionary[dynamicPropertyName] = instance; - } - else - { - tempDictionary[dynamicPropertyName] = dynamicPropertyValue; - } + tempDictionary[dynamicPropertyName] = dynamicPropertyValue; } } + } - CopyDynamicPropertyDictionary(tempDictionary, toDictionary, _dynamicDictionaryPropertyinfo, - targetEntity); + CopyDynamicPropertyDictionary(tempDictionary, toDictionary, _dynamicDictionaryPropertyinfo, + targetEntity); + } + + // Missing dynamic structural properties MUST be removed or set to null in *Put* + private void CopyUnchangedDynamicValues(T targetEntity) + { + if (_dynamicDictionaryPropertyinfo == null) + { + return; } - // Missing dynamic structural properties MUST be removed or set to null in *Put* - private void CopyUnchangedDynamicValues(T targetEntity) + if (_dynamicDictionaryCache == null) { - if (_dynamicDictionaryPropertyinfo == null) - { - return; - } + _dynamicDictionaryCache = + GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, _instance, create: false); + } - if (_dynamicDictionaryCache == null) + IDictionary toDictionary = + GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, targetEntity, create: false); + + if (_dynamicDictionaryCache == null) + { + if (toDictionary != null) { - _dynamicDictionaryCache = - GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, _instance, create: false); + toDictionary.Clear(); } + } + else + { + IDictionary tempDictionary = toDictionary != null + ? new Dictionary(toDictionary) + : new Dictionary(); - IDictionary toDictionary = - GetDynamicPropertyDictionary(_dynamicDictionaryPropertyinfo, targetEntity, create: false); + List removedSet = tempDictionary.Keys.Except(_changedDynamicProperties).ToList(); - if (_dynamicDictionaryCache == null) + foreach (string name in removedSet) { - if (toDictionary != null) - { - toDictionary.Clear(); - } + tempDictionary.Remove(name); } - else - { - IDictionary tempDictionary = toDictionary != null - ? new Dictionary(toDictionary) - : new Dictionary(); - List removedSet = tempDictionary.Keys.Except(_changedDynamicProperties).ToList(); + CopyDynamicPropertyDictionary(tempDictionary, toDictionary, _dynamicDictionaryPropertyinfo, + targetEntity); + } + } - foreach (string name in removedSet) - { - tempDictionary.Remove(name); - } + private bool TrySetPropertyValueInternal(string name, object value) + { + Debug.Assert(name != null, "Argument name is null"); - CopyDynamicPropertyDictionary(tempDictionary, toDictionary, _dynamicDictionaryPropertyinfo, - targetEntity); - } + if (!(_allProperties.ContainsKey(name) && _updatableProperties.Contains(name))) + { + return false; } - private bool TrySetPropertyValueInternal(string name, object value) - { - Debug.Assert(name != null, "Argument name is null"); + PropertyAccessor cacheHit = _allProperties[name]; - if (!(_allProperties.ContainsKey(name) && _updatableProperties.Contains(name))) - { - return false; - } + if (value == null && !cacheHit.Property.PropertyType.IsNullable()) + { + return false; + } - PropertyAccessor cacheHit = _allProperties[name]; + Type propertyType = cacheHit.Property.PropertyType; + if (value != null && !TypeHelper.IsCollection(propertyType) && !propertyType.IsAssignableFrom(value.GetType())) + { + return false; + } - if (value == null && !cacheHit.Property.PropertyType.IsNullable()) - { - return false; - } + cacheHit.SetValue(_instance, value); + _changedProperties.Add(name); + return true; + } - Type propertyType = cacheHit.Property.PropertyType; - if (value != null && !TypeHelper.IsCollection(propertyType) && !propertyType.IsAssignableFrom(value.GetType())) - { - return false; - } + private bool TrySetNestedResourceInternal(string name, object deltaNestedResource) + { + Debug.Assert(name != null, "Argument name is null"); - cacheHit.SetValue(_instance, value); - _changedProperties.Add(name); - return true; + if (!(_allProperties.ContainsKey(name) && _updatableProperties.Contains(name))) + { + return false; } - private bool TrySetNestedResourceInternal(string name, object deltaNestedResource) + if (_deltaNestedResources.ContainsKey(name)) { - Debug.Assert(name != null, "Argument name is null"); - - if (!(_allProperties.ContainsKey(name) && _updatableProperties.Contains(name))) - { - return false; - } - - if (_deltaNestedResources.ContainsKey(name)) - { - // Ignore duplicated nested resource. - return false; - } + // Ignore duplicated nested resource. + return false; + } - if (!(deltaNestedResource is IDeltaSet)) - { - PropertyAccessor cacheHit = _allProperties[name]; - // Get the Delta<{NestedResourceType}>._instance using Reflection. - FieldInfo field = deltaNestedResource.GetType().GetField("_instance", BindingFlags.NonPublic | BindingFlags.Instance); - Contract.Assert(field != null, "field != null"); - cacheHit.SetValue(_instance, field.GetValue(deltaNestedResource)); - } + if (!(deltaNestedResource is IDeltaSet)) + { + PropertyAccessor cacheHit = _allProperties[name]; + // Get the Delta<{NestedResourceType}>._instance using Reflection. + FieldInfo field = deltaNestedResource.GetType().GetField("_instance", BindingFlags.NonPublic | BindingFlags.Instance); + Contract.Assert(field != null, "field != null"); + cacheHit.SetValue(_instance, field.GetValue(deltaNestedResource)); + } - // Add the nested resource in the hierarchy. - // Note: We shouldn't add the structural properties to the _changedProperties, which - // is used for keeping track of changed non-structural properties at current level. - _deltaNestedResources[name] = deltaNestedResource; + // Add the nested resource in the hierarchy. + // Note: We shouldn't add the structural properties to the _changedProperties, which + // is used for keeping track of changed non-structural properties at current level. + _deltaNestedResources[name] = deltaNestedResource; - return true; - } + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/DeltaSetOfT.cs b/src/Microsoft.AspNetCore.OData/Deltas/DeltaSetOfT.cs index 8e039bb77..1334a72b7 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/DeltaSetOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/DeltaSetOfT.cs @@ -10,96 +10,95 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.OData.Abstracts; -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// allows and tracks changes to a delta resource set. +/// +[SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "")] +[NonValidatingParameterBinding] +public class DeltaSet : Collection, IDeltaSet, ITypedDelta where T : class { /// - /// allows and tracks changes to a delta resource set. + /// Gets the actual type of the structural object for which the changes are tracked. /// - [SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "")] - [NonValidatingParameterBinding] - public class DeltaSet : Collection, IDeltaSet, ITypedDelta where T : class - { - /// - /// Gets the actual type of the structural object for which the changes are tracked. - /// - public Type StructuredType => typeof(T); + public Type StructuredType => typeof(T); - /// - /// Gets the expected type of the entity for which the changes are tracked. - /// - public Type ExpectedClrType => typeof(T); + /// + /// Gets the expected type of the entity for which the changes are tracked. + /// + public Type ExpectedClrType => typeof(T); - #region Exclude unfinished APIs + #region Exclude unfinished APIs #if false - /// - /// Overwrites the entity with the changes tracked by this Delta resource set. - /// - /// - /// TODO: this functionality hasn't finished yet. We'd like to get more feedback about how to - /// patch the deltaset to the original data source. - /// - /// The original set. - internal void Patch(IEnumerable originalSet) + /// + /// Overwrites the entity with the changes tracked by this Delta resource set. + /// + /// + /// TODO: this functionality hasn't finished yet. We'd like to get more feedback about how to + /// patch the deltaset to the original data source. + /// + /// The original set. + internal void Patch(IEnumerable originalSet) + { + if (originalSet == null) { - if (originalSet == null) - { - throw Error.ArgumentNull(nameof(originalSet)); - } + throw Error.ArgumentNull(nameof(originalSet)); + } - // TODO: work out the patch process - foreach (IDeltaSetItem delta in this) - { - T originalObj = GetOriginal(delta, originalSet); + // TODO: work out the patch process + foreach (IDeltaSetItem delta in this) + { + T originalObj = GetOriginal(delta, originalSet); - switch (delta) - { - case IDelta deltaResource: - IDeltaDeletedResource deltaDeleteResource = delta as IDeltaDeletedResource; - if (deltaDeleteResource != null) - { - // TODO: it's a delta deleted resource - } - else - { - // TODO: it's a normal (added, updated) resource - } - break; + switch (delta) + { + case IDelta deltaResource: + IDeltaDeletedResource deltaDeleteResource = delta as IDeltaDeletedResource; + if (deltaDeleteResource != null) + { + // TODO: it's a delta deleted resource + } + else + { + // TODO: it's a normal (added, updated) resource + } + break; - case IDeltaDeletedLink deltaDeletedLink: - // TODO: a delta deleted link - break; + case IDeltaDeletedLink deltaDeletedLink: + // TODO: a delta deleted link + break; - case IDeltaLink deltaLink: - // TODO: a delta added link - break; + case IDeltaLink deltaLink: + // TODO: a delta added link + break; - default: - throw Error.InvalidOperation($"Unknown delta type {delta.GetType()}"); - } + default: + throw Error.InvalidOperation($"Unknown delta type {delta.GetType()}"); } } + } - /// - /// Find the related instance. - /// - /// The delta item. - /// The original set. - /// - protected virtual T GetOriginal(IDeltaSetItem deltaItem, IEnumerable originalSet) + /// + /// Find the related instance. + /// + /// The delta item. + /// The original set. + /// + protected virtual T GetOriginal(IDeltaSetItem deltaItem, IEnumerable originalSet) + { + if (deltaItem == null) { - if (deltaItem == null) - { - throw Error.ArgumentNull(nameof(deltaItem)); - } - - if (originalSet == null) - { - throw Error.ArgumentNull(nameof(originalSet)); - } + throw Error.ArgumentNull(nameof(deltaItem)); + } - return null; + if (originalSet == null) + { + throw Error.ArgumentNull(nameof(originalSet)); } + + return null; + } #endif #endregion - } } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/IDelta.cs b/src/Microsoft.AspNetCore.OData/Deltas/IDelta.cs index 44e1bbc3f..16ff2e5e0 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/IDelta.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/IDelta.cs @@ -8,57 +8,56 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// allows and tracks changes to an object. +/// +public interface IDelta : IDeltaSetItem { /// - /// allows and tracks changes to an object. + /// Returns the Properties that have been modified through this IDelta as an + /// enumerable of Property Names /// - public interface IDelta : IDeltaSetItem - { - /// - /// Returns the Properties that have been modified through this IDelta as an - /// enumerable of Property Names - /// - IEnumerable GetChangedPropertyNames(); + IEnumerable GetChangedPropertyNames(); - /// - /// Returns the Properties that have not been modified through this IDelta as an - /// enumerable of Property Names - /// - IEnumerable GetUnchangedPropertyNames(); + /// + /// Returns the Properties that have not been modified through this IDelta as an + /// enumerable of Property Names + /// + IEnumerable GetUnchangedPropertyNames(); - /// - /// Gets the nested resources changed at this level. - /// - IDictionary GetDeltaNestedNavigationProperties(); + /// + /// Gets the nested resources changed at this level. + /// + IDictionary GetDeltaNestedNavigationProperties(); - /// - /// Attempts to set the Property called to the specified. - /// - /// The name of the Property - /// The new value of the Property - /// Returns true if successful and false if not. - bool TrySetPropertyValue(string name, object value); + /// + /// Attempts to set the Property called to the specified. + /// + /// The name of the Property + /// The new value of the Property + /// Returns true if successful and false if not. + bool TrySetPropertyValue(string name, object value); - /// - /// Attempts to get the value of the Property called from the underlying Entity. - /// - /// The name of the Property - /// The value of the Property - /// Returns true if the Property was found and false if not. - bool TryGetPropertyValue(string name, out object value); + /// + /// Attempts to get the value of the Property called from the underlying Entity. + /// + /// The name of the Property + /// The value of the Property + /// Returns true if the Property was found and false if not. + bool TryGetPropertyValue(string name, out object value); - /// - /// Attempts to get the of the Property called from the underlying Entity. - /// - /// The name of the Property - /// The type of the Property - /// Returns true if the Property was found and false if not. - bool TryGetPropertyType(string name, out Type type); + /// + /// Attempts to get the of the Property called from the underlying Entity. + /// + /// The name of the Property + /// The type of the Property + /// Returns true if the Property was found and false if not. + bool TryGetPropertyType(string name, out Type type); - /// - /// Clears the . - /// - void Clear(); - } + /// + /// Clears the . + /// + void Clear(); } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/IDeltaDeletedLink.cs b/src/Microsoft.AspNetCore.OData/Deltas/IDeltaDeletedLink.cs index c2d2b3bce..e54715ff1 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/IDeltaDeletedLink.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/IDeltaDeletedLink.cs @@ -5,12 +5,11 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// allows and tracks changes to an delta deleted link. +/// +internal interface IDeltaDeletedLink : IDeltaLinkBase { - /// - /// allows and tracks changes to an delta deleted link. - /// - internal interface IDeltaDeletedLink : IDeltaLinkBase - { - } } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/IDeltaDeletedResource.cs b/src/Microsoft.AspNetCore.OData/Deltas/IDeltaDeletedResource.cs index 7f4349de4..5631b492a 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/IDeltaDeletedResource.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/IDeltaDeletedResource.cs @@ -8,17 +8,16 @@ using Microsoft.OData; using System; -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// allows and tracks changes to a deleted resource. +/// +public interface IDeltaDeletedResource : IDelta { - /// - /// allows and tracks changes to a deleted resource. - /// - public interface IDeltaDeletedResource : IDelta - { - /// - Uri Id { get; set; } + /// + Uri Id { get; set; } - /// - DeltaDeletedEntryReason? Reason { get; set; } - } + /// + DeltaDeletedEntryReason? Reason { get; set; } } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/IDeltaLink.cs b/src/Microsoft.AspNetCore.OData/Deltas/IDeltaLink.cs index bac940878..5f6f0ded5 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/IDeltaLink.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/IDeltaLink.cs @@ -5,12 +5,11 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// allows and tracks changes to an added link. +/// +internal interface IDeltaLink : IDeltaLinkBase { - /// - /// allows and tracks changes to an added link. - /// - internal interface IDeltaLink : IDeltaLinkBase - { - } } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/IDeltaLinkBase.cs b/src/Microsoft.AspNetCore.OData/Deltas/IDeltaLinkBase.cs index 36a007b87..8123f5e91 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/IDeltaLinkBase.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/IDeltaLinkBase.cs @@ -7,26 +7,25 @@ using System; -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// allows and tracks changes to delta link. +/// +internal interface IDeltaLinkBase : IDeltaSetItem { /// - /// allows and tracks changes to delta link. + /// The Uri of the entity from which the relationship is defined, which may be absolute or relative. /// - internal interface IDeltaLinkBase : IDeltaSetItem - { - /// - /// The Uri of the entity from which the relationship is defined, which may be absolute or relative. - /// - Uri Source { get; set; } + Uri Source { get; set; } - /// - /// The Uri of the related entity, which may be absolute or relative. - /// - Uri Target { get; set; } + /// + /// The Uri of the related entity, which may be absolute or relative. + /// + Uri Target { get; set; } - /// - /// The name of the relationship property on the parent object. - /// - string Relationship { get; set; } - } + /// + /// The name of the relationship property on the parent object. + /// + string Relationship { get; set; } } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/IDeltaSet.cs b/src/Microsoft.AspNetCore.OData/Deltas/IDeltaSet.cs index 9d36facba..cdddcddb7 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/IDeltaSet.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/IDeltaSet.cs @@ -8,13 +8,12 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// The interface for a delta resource set. +/// +[SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix.", Justification = "The set suffix is correct.")] +public interface IDeltaSet : ICollection { - /// - /// The interface for a delta resource set. - /// - [SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix.", Justification = "The set suffix is correct.")] - public interface IDeltaSet : ICollection - { - } } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/IDeltaSetItem.cs b/src/Microsoft.AspNetCore.OData/Deltas/IDeltaSetItem.cs index 7b8d0efd6..4679fec68 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/IDeltaSetItem.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/IDeltaSetItem.cs @@ -5,16 +5,15 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// The delta set item base. +/// +public interface IDeltaSetItem { /// - /// The delta set item base. + /// Gets the delta item kind. /// - public interface IDeltaSetItem - { - /// - /// Gets the delta item kind. - /// - DeltaItemKind Kind { get; } - } + DeltaItemKind Kind { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Deltas/ITypedDelta.cs b/src/Microsoft.AspNetCore.OData/Deltas/ITypedDelta.cs index b14827f27..e39815668 100644 --- a/src/Microsoft.AspNetCore.OData/Deltas/ITypedDelta.cs +++ b/src/Microsoft.AspNetCore.OData/Deltas/ITypedDelta.cs @@ -7,21 +7,20 @@ using System; -namespace Microsoft.AspNetCore.OData.Deltas +namespace Microsoft.AspNetCore.OData.Deltas; + +/// +/// The typed delta. +/// +public interface ITypedDelta { /// - /// The typed delta. + /// Gets the actual type of the structural object for which the changes are tracked. /// - public interface ITypedDelta - { - /// - /// Gets the actual type of the structural object for which the changes are tracked. - /// - Type StructuredType { get; } + Type StructuredType { get; } - /// - /// Gets the expected type of the entity for which the changes are tracked. - /// - Type ExpectedClrType { get; } - } + /// + /// Gets the expected type of the entity for which the changes are tracked. + /// + Type ExpectedClrType { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/AutoSelectExpandHelper.cs b/src/Microsoft.AspNetCore.OData/Edm/AutoSelectExpandHelper.cs index 30199f790..b4ae48a39 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/AutoSelectExpandHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/AutoSelectExpandHelper.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -14,338 +14,337 @@ using Microsoft.OData.ModelBuilder.Annotations; using Microsoft.OData.ModelBuilder.Config; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +internal static class AutoSelectExpandHelper { - internal static class AutoSelectExpandHelper + #region Auto Select and Expand Test + /// + /// Tests whether there are auto select properties. + /// So far, we only test one depth for auto select, shall we go through the deeper depth? + /// + /// The Edm model. + /// The type from value or from path. + /// The property from path, it can be null. + /// true if the structured type has auto select properties; otherwise false. + public static bool HasAutoSelectProperty(this IEdmModel edmModel, IEdmStructuredType structuredType, IEdmProperty property) { - #region Auto Select and Expand Test - /// - /// Tests whether there are auto select properties. - /// So far, we only test one depth for auto select, shall we go through the deeper depth? - /// - /// The Edm model. - /// The type from value or from path. - /// The property from path, it can be null. - /// true if the structured type has auto select properties; otherwise false. - public static bool HasAutoSelectProperty(this IEdmModel edmModel, IEdmStructuredType structuredType, IEdmProperty property) + if (edmModel == null) { - if (edmModel == null) - { - throw Error.ArgumentNull(nameof(edmModel)); - } + throw Error.ArgumentNull(nameof(edmModel)); + } - if (structuredType == null) - { - throw Error.ArgumentNull(nameof(structuredType)); - } + if (structuredType == null) + { + throw Error.ArgumentNull(nameof(structuredType)); + } - List structuredTypes = new List(); - structuredTypes.Add(structuredType); - structuredTypes.AddRange(edmModel.FindAllDerivedTypes(structuredType)); + List structuredTypes = new List(); + structuredTypes.Add(structuredType); + structuredTypes.AddRange(edmModel.FindAllDerivedTypes(structuredType)); - foreach (IEdmStructuredType edmStructuredType in structuredTypes) - { - // for top type, let's retrieve its properties and the properties from base type of top type if has. - // for derived type, let's retrieve the declared properties. - IEnumerable properties = edmStructuredType == structuredType - ? edmStructuredType.StructuralProperties() - : edmStructuredType.DeclaredStructuralProperties(); + foreach (IEdmStructuredType edmStructuredType in structuredTypes) + { + // for top type, let's retrieve its properties and the properties from base type of top type if has. + // for derived type, let's retrieve the declared properties. + IEnumerable properties = edmStructuredType == structuredType + ? edmStructuredType.StructuralProperties() + : edmStructuredType.DeclaredStructuralProperties(); - foreach (IEdmStructuralProperty subProperty in properties) + foreach (IEdmStructuralProperty subProperty in properties) + { + if (IsAutoSelect(subProperty, property, edmStructuredType, edmModel)) { - if (IsAutoSelect(subProperty, property, edmStructuredType, edmModel)) - { - return true; - } + return true; } } + } - return false; + return false; + } + + /// + /// Tests whether there are auto expand properties. + /// + /// The Edm model. + /// The Edm structured type. + /// The property from path, it can be null. + /// true if the structured type has auto expand properties; otherwise false. + public static bool HasAutoExpandProperty(this IEdmModel edmModel, IEdmStructuredType structuredType, IEdmProperty property) + { + if (edmModel == null) + { + throw Error.ArgumentNull(nameof(edmModel)); } - /// - /// Tests whether there are auto expand properties. - /// - /// The Edm model. - /// The Edm structured type. - /// The property from path, it can be null. - /// true if the structured type has auto expand properties; otherwise false. - public static bool HasAutoExpandProperty(this IEdmModel edmModel, IEdmStructuredType structuredType, IEdmProperty property) + if (structuredType == null) { - if (edmModel == null) - { - throw Error.ArgumentNull(nameof(edmModel)); - } + throw Error.ArgumentNull(nameof(structuredType)); + } - if (structuredType == null) - { - throw Error.ArgumentNull(nameof(structuredType)); - } + return edmModel.HasAutoExpandProperty(structuredType, property, new HashSet()); + } - return edmModel.HasAutoExpandProperty(structuredType, property, new HashSet()); + private static bool HasAutoExpandProperty(this IEdmModel edmModel, IEdmStructuredType structuredType, IEdmProperty pathProperty, ISet visited) + { + if (visited.Contains(structuredType)) + { + return false; } + visited.Add(structuredType); - private static bool HasAutoExpandProperty(this IEdmModel edmModel, IEdmStructuredType structuredType, IEdmProperty pathProperty, ISet visited) - { - if (visited.Contains(structuredType)) - { - return false; - } - visited.Add(structuredType); + List structuredTypes = new List(); + structuredTypes.Add(structuredType); + structuredTypes.AddRange(edmModel.FindAllDerivedTypes(structuredType)); - List structuredTypes = new List(); - structuredTypes.Add(structuredType); - structuredTypes.AddRange(edmModel.FindAllDerivedTypes(structuredType)); + foreach (IEdmStructuredType edmStructuredType in structuredTypes) + { + // for top type, let's retrieve its properties and the properties from base type of top type if has. + // for derived type, let's retrieve the declared properties. + IEnumerable properties = edmStructuredType == structuredType + ? edmStructuredType.Properties() + : edmStructuredType.DeclaredProperties; - foreach (IEdmStructuredType edmStructuredType in structuredTypes) + foreach (IEdmProperty property in properties) { - // for top type, let's retrieve its properties and the properties from base type of top type if has. - // for derived type, let's retrieve the declared properties. - IEnumerable properties = edmStructuredType == structuredType - ? edmStructuredType.Properties() - : edmStructuredType.DeclaredProperties; - - foreach (IEdmProperty property in properties) + switch (property.PropertyKind) { - switch (property.PropertyKind) - { - case EdmPropertyKind.Structural: - IEdmStructuralProperty structuralProperty = (IEdmStructuralProperty)property; - IEdmTypeReference typeRef = property.Type.GetElementTypeOrSelf(); - if (typeRef.IsComplex() && edmModel.CanExpand(typeRef.AsComplex().ComplexDefinition(), structuralProperty)) + case EdmPropertyKind.Structural: + IEdmStructuralProperty structuralProperty = (IEdmStructuralProperty)property; + IEdmTypeReference typeRef = property.Type.GetElementTypeOrSelf(); + if (typeRef.IsComplex() && edmModel.CanExpand(typeRef.AsComplex().ComplexDefinition(), structuralProperty)) + { + IEdmStructuredType subStructuredType = typeRef.AsStructured().StructuredDefinition(); + if (edmModel.HasAutoExpandProperty(subStructuredType, structuralProperty, visited)) { - IEdmStructuredType subStructuredType = typeRef.AsStructured().StructuredDefinition(); - if (edmModel.HasAutoExpandProperty(subStructuredType, structuralProperty, visited)) - { - return true; - } + return true; } - break; + } + break; - case EdmPropertyKind.Navigation: - IEdmNavigationProperty navigationProperty = (IEdmNavigationProperty)property; - if (IsAutoExpand(navigationProperty, pathProperty, edmStructuredType, edmModel)) - { - return true; // find an auto-expand navigation property path - } - break; - } + case EdmPropertyKind.Navigation: + IEdmNavigationProperty navigationProperty = (IEdmNavigationProperty)property; + if (IsAutoExpand(navigationProperty, pathProperty, edmStructuredType, edmModel)) + { + return true; // find an auto-expand navigation property path + } + break; } } + } - return false; + return false; + } + #endregion + + /// + /// Gets the auto select paths. + /// + /// The Edm model. + /// The Edm structured type. + /// The property from path, it can be null. + /// The query settings. + /// The auto select paths. + public static IList GetAutoSelectPaths(this IEdmModel edmModel, IEdmStructuredType structuredType, + IEdmProperty pathProperty, ModelBoundQuerySettings querySettings = null) + { + if (edmModel == null) + { + throw Error.ArgumentNull(nameof(edmModel)); } - #endregion - - /// - /// Gets the auto select paths. - /// - /// The Edm model. - /// The Edm structured type. - /// The property from path, it can be null. - /// The query settings. - /// The auto select paths. - public static IList GetAutoSelectPaths(this IEdmModel edmModel, IEdmStructuredType structuredType, - IEdmProperty pathProperty, ModelBoundQuerySettings querySettings = null) + + if (structuredType == null) { - if (edmModel == null) - { - throw Error.ArgumentNull(nameof(edmModel)); - } + throw Error.ArgumentNull(nameof(structuredType)); + } - if (structuredType == null) - { - throw Error.ArgumentNull(nameof(structuredType)); - } + List autoSelectProperties = new List(); - List autoSelectProperties = new List(); + List structuredTypes = new List(); + structuredTypes.Add(structuredType); + structuredTypes.AddRange(edmModel.FindAllDerivedTypes(structuredType)); - List structuredTypes = new List(); - structuredTypes.Add(structuredType); - structuredTypes.AddRange(edmModel.FindAllDerivedTypes(structuredType)); + foreach (IEdmStructuredType edmStructuredType in structuredTypes) + { + // for top type, let's retrieve its properties and the properties from base type of top type if has. + // for derived type, let's retrieve the declared properties. + IEnumerable properties = (edmStructuredType == structuredType) ? + edmStructuredType.StructuralProperties() : + properties = edmStructuredType.DeclaredStructuralProperties(); - foreach (IEdmStructuredType edmStructuredType in structuredTypes) + foreach (IEdmStructuralProperty property in properties) { - // for top type, let's retrieve its properties and the properties from base type of top type if has. - // for derived type, let's retrieve the declared properties. - IEnumerable properties = (edmStructuredType == structuredType) ? - edmStructuredType.StructuralProperties() : - properties = edmStructuredType.DeclaredStructuralProperties(); - - foreach (IEdmStructuralProperty property in properties) + if (IsAutoSelect(property, pathProperty, edmStructuredType, edmModel, querySettings)) { - if (IsAutoSelect(property, pathProperty, edmStructuredType, edmModel, querySettings)) + if (edmStructuredType == structuredType) { - if (edmStructuredType == structuredType) - { - autoSelectProperties.Add(new SelectModelPath(new[] { property })); - } - else - { - autoSelectProperties.Add(new SelectModelPath(new IEdmElement[] { edmStructuredType, property })); - } + autoSelectProperties.Add(new SelectModelPath(new[] { property })); + } + else + { + autoSelectProperties.Add(new SelectModelPath(new IEdmElement[] { edmStructuredType, property })); } } } + } - return autoSelectProperties; + return autoSelectProperties; + } + + /// + /// Gets the auto expand paths. + /// + /// The Edm model. + /// The Edm structured type. + /// The property starting from, it can be null. + /// Is $select presented. + /// The query settings. + /// The auto expand paths. + public static IList GetAutoExpandPaths(this IEdmModel edmModel, IEdmStructuredType structuredType, + IEdmProperty property, bool isSelectPresent = false, ModelBoundQuerySettings querySettings = null) + { + if (edmModel == null) + { + throw Error.ArgumentNull(nameof(edmModel)); } - /// - /// Gets the auto expand paths. - /// - /// The Edm model. - /// The Edm structured type. - /// The property starting from, it can be null. - /// Is $select presented. - /// The query settings. - /// The auto expand paths. - public static IList GetAutoExpandPaths(this IEdmModel edmModel, IEdmStructuredType structuredType, - IEdmProperty property, bool isSelectPresent = false, ModelBoundQuerySettings querySettings = null) + if (structuredType == null) { - if (edmModel == null) - { - throw Error.ArgumentNull(nameof(edmModel)); - } + throw Error.ArgumentNull(nameof(structuredType)); + } - if (structuredType == null) - { - throw Error.ArgumentNull(nameof(structuredType)); - } + Stack nodes = new Stack(); + ISet visited = new HashSet(); + IList results = new List(); - Stack nodes = new Stack(); - ISet visited = new HashSet(); - IList results = new List(); + // type and property from path is higher priority + edmModel.GetAutoExpandPaths(structuredType, property, nodes, visited, results, isSelectPresent, querySettings); - // type and property from path is higher priority - edmModel.GetAutoExpandPaths(structuredType, property, nodes, visited, results, isSelectPresent, querySettings); + Contract.Assert(nodes.Count == 0); + return results; + } - Contract.Assert(nodes.Count == 0); - return results; + public static bool IsAutoExpand(IEdmProperty navigationProperty, + IEdmProperty pathProperty, IEdmStructuredType pathStructuredType, IEdmModel edmModel, + bool isSelectPresent = false, ModelBoundQuerySettings querySettings = null) + { + QueryableRestrictionsAnnotation annotation = EdmHelpers.GetPropertyRestrictions(navigationProperty, edmModel); + if (annotation != null && annotation.Restrictions.AutoExpand) + { + return !annotation.Restrictions.DisableAutoExpandWhenSelectIsPresent || !isSelectPresent; } - public static bool IsAutoExpand(IEdmProperty navigationProperty, - IEdmProperty pathProperty, IEdmStructuredType pathStructuredType, IEdmModel edmModel, - bool isSelectPresent = false, ModelBoundQuerySettings querySettings = null) + if (querySettings == null) { - QueryableRestrictionsAnnotation annotation = EdmHelpers.GetPropertyRestrictions(navigationProperty, edmModel); - if (annotation != null && annotation.Restrictions.AutoExpand) - { - return !annotation.Restrictions.DisableAutoExpandWhenSelectIsPresent || !isSelectPresent; - } + querySettings = edmModel.GetModelBoundQuerySettings(pathProperty, pathStructuredType); + } - if (querySettings == null) - { - querySettings = edmModel.GetModelBoundQuerySettings(pathProperty, pathStructuredType); - } + if (querySettings != null && querySettings.IsAutomaticExpand(navigationProperty.Name)) + { + return true; + } - if (querySettings != null && querySettings.IsAutomaticExpand(navigationProperty.Name)) - { - return true; - } + return false; + } - return false; + public static bool IsAutoSelect(IEdmProperty property, IEdmProperty pathProperty, + IEdmStructuredType pathStructuredType, IEdmModel edmModel, ModelBoundQuerySettings querySettings = null) + { + if (querySettings == null) + { + querySettings = edmModel.GetModelBoundQuerySettings(pathProperty, pathStructuredType); } - public static bool IsAutoSelect(IEdmProperty property, IEdmProperty pathProperty, - IEdmStructuredType pathStructuredType, IEdmModel edmModel, ModelBoundQuerySettings querySettings = null) + if (querySettings != null && querySettings.IsAutomaticSelect(property.Name)) { - if (querySettings == null) - { - querySettings = edmModel.GetModelBoundQuerySettings(pathProperty, pathStructuredType); - } + return true; + } - if (querySettings != null && querySettings.IsAutomaticSelect(property.Name)) - { - return true; - } + return false; + } + private static bool CanExpand(this IEdmModel edmModel, IEdmStructuredType structuredType, IEdmProperty property) + { + // first for back-compatible, check the queryable restriction + QueryableRestrictionsAnnotation annotation = EdmHelpers.GetPropertyRestrictions(property, edmModel); + if (annotation != null && annotation.Restrictions.NotExpandable) + { return false; } - private static bool CanExpand(this IEdmModel edmModel, IEdmStructuredType structuredType, IEdmProperty property) + ModelBoundQuerySettings settings = edmModel.GetModelBoundQuerySettingsOrNull(structuredType, property); + if (settings != null && !settings.Expandable(property.Name)) { - // first for back-compatible, check the queryable restriction - QueryableRestrictionsAnnotation annotation = EdmHelpers.GetPropertyRestrictions(property, edmModel); - if (annotation != null && annotation.Restrictions.NotExpandable) - { - return false; - } + return false; + } - ModelBoundQuerySettings settings = edmModel.GetModelBoundQuerySettingsOrNull(structuredType, property); - if (settings != null && !settings.Expandable(property.Name)) - { - return false; - } + return true; + } - return true; + private static void GetAutoExpandPaths(this IEdmModel edmModel, IEdmStructuredType structuredType, IEdmProperty pathProperty, + Stack nodes, ISet visited, IList results, + bool isSelectPresent = false, ModelBoundQuerySettings querySettings = null) + { + if (visited.Contains(structuredType)) + { + return; } + visited.Add(structuredType); - private static void GetAutoExpandPaths(this IEdmModel edmModel, IEdmStructuredType structuredType, IEdmProperty pathProperty, - Stack nodes, ISet visited, IList results, - bool isSelectPresent = false, ModelBoundQuerySettings querySettings = null) + List structuredTypes = new List(); + structuredTypes.Add(structuredType); + structuredTypes.AddRange(edmModel.FindAllDerivedTypes(structuredType)); + + foreach (IEdmStructuredType edmStructuredType in structuredTypes) { - if (visited.Contains(structuredType)) + IEnumerable properties; + + if (edmStructuredType == structuredType) { - return; + // for base type, let's retrieve its properties and the properties from base type of base type if have. + properties = edmStructuredType.Properties(); } - visited.Add(structuredType); - - List structuredTypes = new List(); - structuredTypes.Add(structuredType); - structuredTypes.AddRange(edmModel.FindAllDerivedTypes(structuredType)); - - foreach (IEdmStructuredType edmStructuredType in structuredTypes) + else { - IEnumerable properties; - - if (edmStructuredType == structuredType) - { - // for base type, let's retrieve its properties and the properties from base type of base type if have. - properties = edmStructuredType.Properties(); - } - else - { - // for derived type, let's retrieve the declared properties. - properties = edmStructuredType.DeclaredProperties; - nodes.Push(edmStructuredType); // add a type cast for derived type - } + // for derived type, let's retrieve the declared properties. + properties = edmStructuredType.DeclaredProperties; + nodes.Push(edmStructuredType); // add a type cast for derived type + } - foreach (IEdmProperty property in properties) + foreach (IEdmProperty property in properties) + { + switch (property.PropertyKind) { - switch (property.PropertyKind) - { - case EdmPropertyKind.Structural: - IEdmStructuralProperty structuralProperty = (IEdmStructuralProperty)property; - IEdmTypeReference typeRef = property.Type.GetElementTypeOrSelf(); - if (typeRef.IsComplex() && edmModel.CanExpand(typeRef.AsComplex().ComplexDefinition(), structuralProperty)) - { - IEdmStructuredType subStructuredType = typeRef.AsStructured().StructuredDefinition(); + case EdmPropertyKind.Structural: + IEdmStructuralProperty structuralProperty = (IEdmStructuralProperty)property; + IEdmTypeReference typeRef = property.Type.GetElementTypeOrSelf(); + if (typeRef.IsComplex() && edmModel.CanExpand(typeRef.AsComplex().ComplexDefinition(), structuralProperty)) + { + IEdmStructuredType subStructuredType = typeRef.AsStructured().StructuredDefinition(); - nodes.Push(structuralProperty); + nodes.Push(structuralProperty); - edmModel.GetAutoExpandPaths(subStructuredType, structuralProperty, nodes, visited, results, isSelectPresent, querySettings); + edmModel.GetAutoExpandPaths(subStructuredType, structuralProperty, nodes, visited, results, isSelectPresent, querySettings); - nodes.Pop(); - } - break; + nodes.Pop(); + } + break; - case EdmPropertyKind.Navigation: - IEdmNavigationProperty navigationProperty = (IEdmNavigationProperty)property; - if (IsAutoExpand(navigationProperty, pathProperty, edmStructuredType, edmModel, isSelectPresent, querySettings)) - { - nodes.Push(navigationProperty); - results.Add(new ExpandModelPath(nodes.Reverse())); // found an auto-expand navigation property path - nodes.Pop(); - } - break; - } + case EdmPropertyKind.Navigation: + IEdmNavigationProperty navigationProperty = (IEdmNavigationProperty)property; + if (IsAutoExpand(navigationProperty, pathProperty, edmStructuredType, edmModel, isSelectPresent, querySettings)) + { + nodes.Push(navigationProperty); + results.Add(new ExpandModelPath(nodes.Reverse())); // found an auto-expand navigation property path + nodes.Pop(); + } + break; } + } - if (edmStructuredType != structuredType) - { - nodes.Pop(); // pop the type cast for derived type - } + if (edmStructuredType != structuredType) + { + nodes.Pop(); // pop the type cast for derived type } } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/BindableOperationFinder.cs b/src/Microsoft.AspNetCore.OData/Edm/BindableOperationFinder.cs index 6c1ea0c1c..9eb4e7ae4 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/BindableOperationFinder.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/BindableOperationFinder.cs @@ -9,114 +9,113 @@ using System.Linq; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// This class builds a cache that allows for efficient look up of bindable operation by EntityType. +/// +internal class BindableOperationFinder { + private Dictionary> _map = new Dictionary>(); + + private Dictionary> _collectionMap = new Dictionary>(); + /// - /// This class builds a cache that allows for efficient look up of bindable operation by EntityType. + /// Constructs a concurrent cache for looking up bindable operations for any EntityType in the provided model. /// - internal class BindableOperationFinder + public BindableOperationFinder(IEdmModel model) { - private Dictionary> _map = new Dictionary>(); + var operationGroups = + from op in model.SchemaElements.OfType() + where op.IsBound && (op.Parameters.First().Type.TypeKind() == EdmTypeKind.Entity || op.Parameters.First().Type.TypeKind() == EdmTypeKind.Collection) + group op by op.Parameters.First().Type.Definition; - private Dictionary> _collectionMap = new Dictionary>(); - - /// - /// Constructs a concurrent cache for looking up bindable operations for any EntityType in the provided model. - /// - public BindableOperationFinder(IEdmModel model) + foreach (var operationGroup in operationGroups) { - var operationGroups = - from op in model.SchemaElements.OfType() - where op.IsBound && (op.Parameters.First().Type.TypeKind() == EdmTypeKind.Entity || op.Parameters.First().Type.TypeKind() == EdmTypeKind.Collection) - group op by op.Parameters.First().Type.Definition; - - foreach (var operationGroup in operationGroups) + var entityType = operationGroup.Key as IEdmEntityType; + if (entityType != null) { - var entityType = operationGroup.Key as IEdmEntityType; - if (entityType != null) - { - _map[entityType] = operationGroup.ToList(); - } + _map[entityType] = operationGroup.ToList(); + } - var collectionType = operationGroup.Key as IEdmCollectionType; - if (collectionType != null) + var collectionType = operationGroup.Key as IEdmCollectionType; + if (collectionType != null) + { + var elementType = collectionType.ElementType.Definition as IEdmEntityType; + if (elementType != null) { - var elementType = collectionType.ElementType.Definition as IEdmEntityType; - if (elementType != null) + // because collection type is temp instance. + List value; + if (_collectionMap.TryGetValue(elementType, out value)) + { + value.AddRange(operationGroup); + } + else { - // because collection type is temp instance. - List value; - if (_collectionMap.TryGetValue(elementType, out value)) - { - value.AddRange(operationGroup); - } - else - { - _collectionMap[elementType] = operationGroup.ToList(); - } + _collectionMap[elementType] = operationGroup.ToList(); } } } } + } + + /// + /// Finds operations that can be invoked on the given entity type. This would include all the operations that are bound + /// to the given type and its base types. + /// + /// The EDM entity type. + /// A collection of operations bound to the entity type. + public virtual IEnumerable FindOperations(IEdmEntityType entityType) + { + return GetTypeHierarchy(entityType).SelectMany(FindDeclaredOperations); + } + + /// + /// Finds operations that can be invoked on the feed. This would include all the operations that are bound to the given + /// type and its base types. + /// + /// The EDM entity type. + /// A collection of operations bound to the feed. + public virtual IEnumerable FindOperationsBoundToCollection(IEdmEntityType entityType) + { + return GetTypeHierarchy(entityType).SelectMany(FindDeclaredOperationsBoundToCollection); + } - /// - /// Finds operations that can be invoked on the given entity type. This would include all the operations that are bound - /// to the given type and its base types. - /// - /// The EDM entity type. - /// A collection of operations bound to the entity type. - public virtual IEnumerable FindOperations(IEdmEntityType entityType) + private static IEnumerable GetTypeHierarchy(IEdmEntityType entityType) + { + IEdmEntityType current = entityType; + while (current != null) { - return GetTypeHierarchy(entityType).SelectMany(FindDeclaredOperations); + yield return current; + current = current.BaseEntityType(); } + } + + private IEnumerable FindDeclaredOperations(IEdmEntityType entityType) + { + List results; - /// - /// Finds operations that can be invoked on the feed. This would include all the operations that are bound to the given - /// type and its base types. - /// - /// The EDM entity type. - /// A collection of operations bound to the feed. - public virtual IEnumerable FindOperationsBoundToCollection(IEdmEntityType entityType) + if (_map.TryGetValue(entityType, out results)) { - return GetTypeHierarchy(entityType).SelectMany(FindDeclaredOperationsBoundToCollection); + return results; } - - private static IEnumerable GetTypeHierarchy(IEdmEntityType entityType) + else { - IEdmEntityType current = entityType; - while (current != null) - { - yield return current; - current = current.BaseEntityType(); - } + return Enumerable.Empty(); } + } - private IEnumerable FindDeclaredOperations(IEdmEntityType entityType) - { - List results; + private IEnumerable FindDeclaredOperationsBoundToCollection(IEdmEntityType entityType) + { + List results; - if (_map.TryGetValue(entityType, out results)) - { - return results; - } - else - { - return Enumerable.Empty(); - } + if (_collectionMap.TryGetValue(entityType, out results)) + { + return results; } - - private IEnumerable FindDeclaredOperationsBoundToCollection(IEdmEntityType entityType) + else { - List results; - - if (_collectionMap.TryGetValue(entityType, out results)) - { - return results; - } - else - { - return Enumerable.Empty(); - } + return Enumerable.Empty(); } } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/BindingPathHelper.cs b/src/Microsoft.AspNetCore.OData/Edm/BindingPathHelper.cs index f934844b8..731b265d2 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/BindingPathHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/BindingPathHelper.cs @@ -10,69 +10,68 @@ using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +internal class BindingPathHelper { - internal class BindingPathHelper + public static bool MatchBindingPath(IEdmPathExpression bindingPath, IList parsedSegments) { - public static bool MatchBindingPath(IEdmPathExpression bindingPath, IList parsedSegments) + List paths = bindingPath.PathSegments.ToList(); + + // If binding path only includes navigation property name, it matches. + if (paths.Count == 1) { - List paths = bindingPath.PathSegments.ToList(); + return true; + } - // If binding path only includes navigation property name, it matches. - if (paths.Count == 1) - { - return true; - } + int pathIndex = paths.Count - 2; // Skip the last segment which is navigation property name. - int pathIndex = paths.Count - 2; // Skip the last segment which is navigation property name. + // Match from tail to head. + for (int segmentIndex = parsedSegments.Count - 1; segmentIndex >= 0; segmentIndex--) + { + ODataSegmentTemplate segment = parsedSegments[segmentIndex]; - // Match from tail to head. - for (int segmentIndex = parsedSegments.Count - 1; segmentIndex >= 0; segmentIndex--) + PropertySegmentTemplate propertySegment = segment as PropertySegmentTemplate; + NavigationSegmentTemplate navigationSegment = segment as NavigationSegmentTemplate; + // Containment navigation property or complex property in binding path. + if (propertySegment != null || + (navigationSegment != null && navigationSegment.Segment.NavigationSource is IEdmContainedEntitySet)) { - ODataSegmentTemplate segment = parsedSegments[segmentIndex]; + string pathName = propertySegment != null ? + propertySegment.Property.Name : + navigationSegment.NavigationProperty.Name; - PropertySegmentTemplate propertySegment = segment as PropertySegmentTemplate; - NavigationSegmentTemplate navigationSegment = segment as NavigationSegmentTemplate; - // Containment navigation property or complex property in binding path. - if (propertySegment != null || - (navigationSegment != null && navigationSegment.Segment.NavigationSource is IEdmContainedEntitySet)) + if (pathIndex < 0 || string.CompareOrdinal(paths[pathIndex], pathName) != 0) { - string pathName = propertySegment != null ? - propertySegment.Property.Name : - navigationSegment.NavigationProperty.Name; + return false; + } - if (pathIndex < 0 || string.CompareOrdinal(paths[pathIndex], pathName) != 0) + pathIndex--; + } + else if (segment is CastSegmentTemplate) + { + CastSegmentTemplate cast = (CastSegmentTemplate)segment; + // May need match type if the binding path contains type cast. + if (pathIndex >= 0 && paths[pathIndex].Contains(".", System.StringComparison.Ordinal)) + { + if (string.CompareOrdinal(paths[pathIndex], cast.CastType.AsElementType().FullTypeName()) != 0) { return false; } pathIndex--; } - else if (segment is CastSegmentTemplate) - { - CastSegmentTemplate cast = (CastSegmentTemplate)segment; - // May need match type if the binding path contains type cast. - if (pathIndex >= 0 && paths[pathIndex].Contains(".", System.StringComparison.Ordinal)) - { - if (string.CompareOrdinal(paths[pathIndex], cast.CastType.AsElementType().FullTypeName()) != 0) - { - return false; - } - - pathIndex--; - } - } - else if (segment is EntitySetSegmentTemplate - || segment is SingletonSegmentTemplate - || navigationSegment != null) - { - // Containment navigation property in first if statement for NavigationPropertySegment. - break; - } } - - // Return true if all the segments in binding path have been matched. - return pathIndex == -1 ? true : false; + else if (segment is EntitySetSegmentTemplate + || segment is SingletonSegmentTemplate + || navigationSegment != null) + { + // Containment navigation property in first if statement for NavigationPropertySegment. + break; + } } + + // Return true if all the segments in binding path have been matched. + return pathIndex == -1 ? true : false; } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/ContainmentPathBuilder.cs b/src/Microsoft.AspNetCore.OData/Edm/ContainmentPathBuilder.cs index 4aca3d0e0..5ecc6566e 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/ContainmentPathBuilder.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/ContainmentPathBuilder.cs @@ -11,170 +11,169 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +internal class ContainmentPathBuilder { - internal class ContainmentPathBuilder + private List _segments; + + public ODataPath TryComputeCanonicalContainingPath(ODataPath path) { - private List _segments; + Contract.Assert(path != null); + Contract.Assert(path.Count >= 2); - public ODataPath TryComputeCanonicalContainingPath(ODataPath path) - { - Contract.Assert(path != null); - Contract.Assert(path.Count >= 2); + _segments = path.ToList(); + + RemoveAllTypeCasts(); - _segments = path.ToList(); + // New ODataPath will be extended later to include any final required key or cast. + RemovePathSegmentsAfterTheLastNavigationProperty(); - RemoveAllTypeCasts(); + RemoveRedundantContainingPathSegments(); - // New ODataPath will be extended later to include any final required key or cast. - RemovePathSegmentsAfterTheLastNavigationProperty(); + AddTypeCastsIfNecessary(); - RemoveRedundantContainingPathSegments(); + // Also remove the last navigation property segment, since it is not part of the containing path segments. + if (_segments.Count > 0) + { + _segments.RemoveAt(_segments.Count - 1); + } - AddTypeCastsIfNecessary(); + return new ODataPath(_segments); + } - // Also remove the last navigation property segment, since it is not part of the containing path segments. - if (_segments.Count > 0) + private void RemovePathSegmentsAfterTheLastNavigationProperty() + { + // Find the last navigation property segment. + ODataPathSegment lastNavigationProperty = _segments.OfType().LastOrDefault(); + List newSegments = new List(); + foreach (ODataPathSegment segment in _segments) + { + newSegments.Add(segment); + if (segment == lastNavigationProperty) { - _segments.RemoveAt(_segments.Count - 1); + break; } - - return new ODataPath(_segments); } - private void RemovePathSegmentsAfterTheLastNavigationProperty() + _segments = newSegments; + } + + private void RemoveRedundantContainingPathSegments() + { + // Find the last non-contained navigation property segment: + // Collection valued: entity set + // -or- + // Single valued: singleton + // Copy over other path segments such as: not a navigation path segment, contained navigation property, + // single valued navigation property with navigation source targeting an entity set (we won't have key + // information for that navigation property.) + _segments.Reverse(); + NavigationPropertySegment navigationPropertySegment = null; + List newSegments = new List(); + foreach (ODataPathSegment segment in _segments) { - // Find the last navigation property segment. - ODataPathSegment lastNavigationProperty = _segments.OfType().LastOrDefault(); - List newSegments = new List(); - foreach (ODataPathSegment segment in _segments) + navigationPropertySegment = segment as NavigationPropertySegment; + if (navigationPropertySegment != null) { - newSegments.Add(segment); - if (segment == lastNavigationProperty) + EdmNavigationSourceKind navigationSourceKind = + navigationPropertySegment.NavigationSource.NavigationSourceKind(); + if ((navigationPropertySegment.NavigationProperty.TargetMultiplicity() == EdmMultiplicity.Many && + navigationSourceKind == EdmNavigationSourceKind.EntitySet) || + (navigationSourceKind == EdmNavigationSourceKind.Singleton)) { break; } } - _segments = newSegments; + newSegments.Insert(0, segment); } - private void RemoveRedundantContainingPathSegments() + // Start the path with the navigation source of the navigation property found above. + if (navigationPropertySegment != null) { - // Find the last non-contained navigation property segment: - // Collection valued: entity set - // -or- - // Single valued: singleton - // Copy over other path segments such as: not a navigation path segment, contained navigation property, - // single valued navigation property with navigation source targeting an entity set (we won't have key - // information for that navigation property.) - _segments.Reverse(); - NavigationPropertySegment navigationPropertySegment = null; - List newSegments = new List(); - foreach (ODataPathSegment segment in _segments) + IEdmNavigationSource navigationSource = navigationPropertySegment.NavigationSource; + Contract.Assert(navigationSource != null); + if (navigationSource.NavigationSourceKind() == EdmNavigationSourceKind.Singleton) { - navigationPropertySegment = segment as NavigationPropertySegment; - if (navigationPropertySegment != null) - { - EdmNavigationSourceKind navigationSourceKind = - navigationPropertySegment.NavigationSource.NavigationSourceKind(); - if ((navigationPropertySegment.NavigationProperty.TargetMultiplicity() == EdmMultiplicity.Many && - navigationSourceKind == EdmNavigationSourceKind.EntitySet) || - (navigationSourceKind == EdmNavigationSourceKind.Singleton)) - { - break; - } - } - - newSegments.Insert(0, segment); + SingletonSegment singletonSegment = new SingletonSegment((IEdmSingleton)navigationSource); + newSegments.Insert(0, singletonSegment); } - - // Start the path with the navigation source of the navigation property found above. - if (navigationPropertySegment != null) + else { - IEdmNavigationSource navigationSource = navigationPropertySegment.NavigationSource; - Contract.Assert(navigationSource != null); - if (navigationSource.NavigationSourceKind() == EdmNavigationSourceKind.Singleton) - { - SingletonSegment singletonSegment = new SingletonSegment((IEdmSingleton)navigationSource); - newSegments.Insert(0, singletonSegment); - } - else - { - Contract.Assert(navigationSource.NavigationSourceKind() == EdmNavigationSourceKind.EntitySet); - EntitySetSegment entitySetSegment = new EntitySetSegment((IEdmEntitySet)navigationSource); - newSegments.Insert(0, entitySetSegment); - } + Contract.Assert(navigationSource.NavigationSourceKind() == EdmNavigationSourceKind.EntitySet); + EntitySetSegment entitySetSegment = new EntitySetSegment((IEdmEntitySet)navigationSource); + newSegments.Insert(0, entitySetSegment); } - - _segments = newSegments; } - private void RemoveAllTypeCasts() - { - List newSegments = new List(); - foreach (ODataPathSegment segment in _segments) - { - if (!(segment is TypeSegment)) - { - newSegments.Add(segment); - } - } - - _segments = newSegments; - } + _segments = newSegments; + } - private void AddTypeCastsIfNecessary() + private void RemoveAllTypeCasts() + { + List newSegments = new List(); + foreach (ODataPathSegment segment in _segments) { - IEdmEntityType owningType = null; - List newSegments = new List(); - foreach (ODataPathSegment segment in _segments) + if (!(segment is TypeSegment)) { - NavigationPropertySegment navProp = segment as NavigationPropertySegment; - if (navProp != null && owningType != null && - owningType.FindProperty(navProp.NavigationProperty.Name) == null) - { - // need a type cast - TypeSegment typeCast = new TypeSegment( - navProp.NavigationProperty.DeclaringType, - navigationSource: null); - newSegments.Add(typeCast); - } - newSegments.Add(segment); - IEdmEntityType targetEntityType = GetTargetEntityType(segment); - if (targetEntityType != null) - { - owningType = targetEntityType; - } } - - _segments = newSegments; } - private static IEdmEntityType GetTargetEntityType(ODataPathSegment segment) - { - Contract.Assert(segment != null); + _segments = newSegments; + } - EntitySetSegment entitySetSegment = segment as EntitySetSegment; - if (entitySetSegment != null) + private void AddTypeCastsIfNecessary() + { + IEdmEntityType owningType = null; + List newSegments = new List(); + foreach (ODataPathSegment segment in _segments) + { + NavigationPropertySegment navProp = segment as NavigationPropertySegment; + if (navProp != null && owningType != null && + owningType.FindProperty(navProp.NavigationProperty.Name) == null) { - return entitySetSegment.EntitySet.EntityType; + // need a type cast + TypeSegment typeCast = new TypeSegment( + navProp.NavigationProperty.DeclaringType, + navigationSource: null); + newSegments.Add(typeCast); } - SingletonSegment singletonSegment = segment as SingletonSegment; - if (singletonSegment != null) + newSegments.Add(segment); + IEdmEntityType targetEntityType = GetTargetEntityType(segment); + if (targetEntityType != null) { - return singletonSegment.Singleton.EntityType; + owningType = targetEntityType; } + } - NavigationPropertySegment navigationPropertySegment = segment as NavigationPropertySegment; - if (navigationPropertySegment != null) - { - return navigationPropertySegment.NavigationSource.EntityType; - } + _segments = newSegments; + } + + private static IEdmEntityType GetTargetEntityType(ODataPathSegment segment) + { + Contract.Assert(segment != null); + + EntitySetSegment entitySetSegment = segment as EntitySetSegment; + if (entitySetSegment != null) + { + return entitySetSegment.EntitySet.EntityType; + } - return null; + SingletonSegment singletonSegment = segment as SingletonSegment; + if (singletonSegment != null) + { + return singletonSegment.Singleton.EntityType; } + + NavigationPropertySegment navigationPropertySegment = segment as NavigationPropertySegment; + if (navigationPropertySegment != null) + { + return navigationPropertySegment.NavigationSource.EntityType; + } + + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/CustomAggregateMethodAnnotation.cs b/src/Microsoft.AspNetCore.OData/Edm/CustomAggregateMethodAnnotation.cs index a368cb989..fca6f54fe 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/CustomAggregateMethodAnnotation.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/CustomAggregateMethodAnnotation.cs @@ -9,50 +9,49 @@ using System.Collections.Generic; using System.Reflection; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// Allows client to tell OData which are the custom aggregation methods defined. +/// In order to do it, it must receive a methodToken - that is the full identifier +/// of the method in the OData URL - and an IDictionary that maps the input type +/// of the aggregation method to its MethodInfo. +/// +public class CustomAggregateMethodAnnotation { + private readonly Dictionary> _tokenToMethodMap + = new Dictionary>(); + /// - /// Allows client to tell OData which are the custom aggregation methods defined. - /// In order to do it, it must receive a methodToken - that is the full identifier - /// of the method in the OData URL - and an IDictionary that maps the input type - /// of the aggregation method to its MethodInfo. + /// Adds all implementations of a method that share the same methodToken. /// - public class CustomAggregateMethodAnnotation + /// The given method token. + /// The given method dictionary. + /// + public CustomAggregateMethodAnnotation AddMethod(string methodToken, IDictionary methods) { - private readonly Dictionary> _tokenToMethodMap - = new Dictionary>(); + _tokenToMethodMap.Add(methodToken, methods); + return this; + } - /// - /// Adds all implementations of a method that share the same methodToken. - /// - /// The given method token. - /// The given method dictionary. - /// - public CustomAggregateMethodAnnotation AddMethod(string methodToken, IDictionary methods) - { - _tokenToMethodMap.Add(methodToken, methods); - return this; - } + /// + /// Get an implementation of a method with the specifies returnType and methodToken. + /// If there's no method that matches the requirements, returns null. + /// + /// The given method token. + /// The given return type. + /// The output of method info. + /// True if the method info was found, false otherwise. + public bool GetMethodInfo(string methodToken, Type returnType, out MethodInfo methodInfo) + { + IDictionary methodWrapper; + methodInfo = null; - /// - /// Get an implementation of a method with the specifies returnType and methodToken. - /// If there's no method that matches the requirements, returns null. - /// - /// The given method token. - /// The given return type. - /// The output of method info. - /// True if the method info was found, false otherwise. - public bool GetMethodInfo(string methodToken, Type returnType, out MethodInfo methodInfo) + if (_tokenToMethodMap.TryGetValue(methodToken, out methodWrapper)) { - IDictionary methodWrapper; - methodInfo = null; - - if (_tokenToMethodMap.TryGetValue(methodToken, out methodWrapper)) - { - return methodWrapper.TryGetValue(returnType, out methodInfo); - } - - return false; + return methodWrapper.TryGetValue(returnType, out methodInfo); } + + return false; } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs b/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs index cb651e448..c4cac6daf 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs @@ -18,429 +18,428 @@ using Microsoft.OData.ModelBuilder; using Microsoft.Spatial; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// The default implementation for . +/// +public class DefaultODataTypeMapper : IODataTypeMapper { /// - /// The default implementation for . + /// Creates a static instance for the Default type mapper. + /// + internal static DefaultODataTypeMapper Default = new DefaultODataTypeMapper(); + + #region Default_PrimitiveTypeMapping + /// + /// The default mapping between Edm primitive type and Clr primitive type. + /// Primitive types are cross Edm models. + /// + private static IDictionary ClrPrimitiveTypes + = new Dictionary(); + + /// + /// Item1 --> non-nullable + /// Item2 --> nullable + /// + private static IDictionary EdmPrimitiveTypes + = new Dictionary(); + + static DefaultODataTypeMapper() + { + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.String); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Boolean); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Byte); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Decimal); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Double); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Guid); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Int16); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Int32); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Int64); + BuildValueTypeMapping(EdmPrimitiveTypeKind.SByte); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Single); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.Binary); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.Stream); + BuildValueTypeMapping(EdmPrimitiveTypeKind.DateTimeOffset); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Duration); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Date); + BuildValueTypeMapping(EdmPrimitiveTypeKind.TimeOfDay); + + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.Geography); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyPoint); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyLineString); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyPolygon); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyCollection); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyMultiLineString); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyMultiPoint); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyMultiPolygon); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.Geometry); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryPoint); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryLineString); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryPolygon); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryCollection); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryMultiLineString); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryMultiPoint); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryMultiPolygon); + + // non-standard mappings + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.String, isStandard: false); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Int32, isStandard: false); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Int64, isStandard: false); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Int64, isStandard: false); + BuildReferenceTypeMapping(EdmPrimitiveTypeKind.String, isStandard: false); + BuildValueTypeMapping(EdmPrimitiveTypeKind.String, isStandard: false); + BuildValueTypeMapping(EdmPrimitiveTypeKind.DateTimeOffset, isStandard: false); + BuildValueTypeMapping(EdmPrimitiveTypeKind.Date, isStandard: false); + BuildValueTypeMapping(EdmPrimitiveTypeKind.TimeOfDay, isStandard: false); + } + #endregion + + #region IODataTypeMapper.GetPrimitiveType + /// + /// Gets the corresponding Edm primitive type for a given type. /// - public class DefaultODataTypeMapper : IODataTypeMapper + /// The given CLR type. + /// Null or the Edm primitive type. + public virtual IEdmPrimitiveTypeReference GetEdmPrimitiveType(Type clrType) { - /// - /// Creates a static instance for the Default type mapper. - /// - internal static DefaultODataTypeMapper Default = new DefaultODataTypeMapper(); - - #region Default_PrimitiveTypeMapping - /// - /// The default mapping between Edm primitive type and Clr primitive type. - /// Primitive types are cross Edm models. - /// - private static IDictionary ClrPrimitiveTypes - = new Dictionary(); - - /// - /// Item1 --> non-nullable - /// Item2 --> nullable - /// - private static IDictionary EdmPrimitiveTypes - = new Dictionary(); - - static DefaultODataTypeMapper() + if (clrType == null) { - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.String); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Boolean); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Byte); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Decimal); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Double); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Guid); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Int16); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Int32); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Int64); - BuildValueTypeMapping(EdmPrimitiveTypeKind.SByte); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Single); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.Binary); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.Stream); - BuildValueTypeMapping(EdmPrimitiveTypeKind.DateTimeOffset); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Duration); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Date); - BuildValueTypeMapping(EdmPrimitiveTypeKind.TimeOfDay); - - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.Geography); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyPoint); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyLineString); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyPolygon); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyCollection); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyMultiLineString); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyMultiPoint); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeographyMultiPolygon); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.Geometry); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryPoint); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryLineString); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryPolygon); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryCollection); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryMultiLineString); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryMultiPoint); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.GeometryMultiPolygon); - - // non-standard mappings - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.String, isStandard: false); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Int32, isStandard: false); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Int64, isStandard: false); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Int64, isStandard: false); - BuildReferenceTypeMapping(EdmPrimitiveTypeKind.String, isStandard: false); - BuildValueTypeMapping(EdmPrimitiveTypeKind.String, isStandard: false); - BuildValueTypeMapping(EdmPrimitiveTypeKind.DateTimeOffset, isStandard: false); - BuildValueTypeMapping(EdmPrimitiveTypeKind.Date, isStandard: false); - BuildValueTypeMapping(EdmPrimitiveTypeKind.TimeOfDay, isStandard: false); + return null; } - #endregion - - #region IODataTypeMapper.GetPrimitiveType - /// - /// Gets the corresponding Edm primitive type for a given type. - /// - /// The given CLR type. - /// Null or the Edm primitive type. - public virtual IEdmPrimitiveTypeReference GetEdmPrimitiveType(Type clrType) - { - if (clrType == null) - { - return null; - } - return ClrPrimitiveTypes.TryGetValue(clrType, out IEdmPrimitiveTypeReference primitive) ? primitive : null; - } + return ClrPrimitiveTypes.TryGetValue(clrType, out IEdmPrimitiveTypeReference primitive) ? primitive : null; + } - /// - /// Gets the corresponding type for a given Edm primitive type . - /// - /// The given Edm primitive type. - /// The nullable or not. - /// Null or the CLR type. - public virtual Type GetClrPrimitiveType(IEdmPrimitiveType primitiveType, bool nullable) + /// + /// Gets the corresponding type for a given Edm primitive type . + /// + /// The given Edm primitive type. + /// The nullable or not. + /// Null or the CLR type. + public virtual Type GetClrPrimitiveType(IEdmPrimitiveType primitiveType, bool nullable) + { + if (primitiveType == null) { - if (primitiveType == null) - { - return null; - } - - if (EdmPrimitiveTypes.TryGetValue(primitiveType, out (Type, Type) types)) - { - if (nullable) - { - return types.Item2; - } - else - { - return types.Item1; - } - } - return null; } - #endregion - - /// - /// The cache used to hold the type mapping between and . - /// - private ConcurrentDictionary _cache = new ConcurrentDictionary(); - - #region ClrType -> EdmType - /// - /// Gets the corresponding Edm type for the given CLR type . - /// - /// The given Edm model. - /// The given CLR type. - /// Null or the corresponding Edm type reference. - public virtual IEdmTypeReference GetEdmTypeReference(IEdmModel edmModel, Type clrType) + + if (EdmPrimitiveTypes.TryGetValue(primitiveType, out (Type, Type) types)) { - if (clrType == null) + if (nullable) { - throw Error.ArgumentNull(nameof(clrType)); + return types.Item2; } - - IEdmPrimitiveTypeReference primitiveType = GetEdmPrimitiveType(clrType); - if (primitiveType != null) + else { - return primitiveType; + return types.Item1; } + } - if (edmModel == null) - { - throw Error.ArgumentNull(nameof(edmModel)); - } + return null; + } + #endregion - TypeCacheItem map = _cache.GetOrAdd(edmModel, d => new TypeCacheItem()); + /// + /// The cache used to hold the type mapping between and . + /// + private ConcurrentDictionary _cache = new ConcurrentDictionary(); - // Search from cache - if (map.TryFindEdmType(clrType, out IEdmTypeReference edmTypeRef)) - { - return edmTypeRef; - } + #region ClrType -> EdmType + /// + /// Gets the corresponding Edm type for the given CLR type . + /// + /// The given Edm model. + /// The given CLR type. + /// Null or the corresponding Edm type reference. + public virtual IEdmTypeReference GetEdmTypeReference(IEdmModel edmModel, Type clrType) + { + if (clrType == null) + { + throw Error.ArgumentNull(nameof(clrType)); + } - // Not in the cache, let's build the Edm type reference. - IEdmType edmType = GetEdmType(edmModel, clrType, testCollections: true); - if (edmType != null) - { - bool isNullable = clrType.IsNullable(); - edmTypeRef = edmType.ToEdmTypeReference(isNullable); - } - else - { - edmTypeRef = null; - } + IEdmPrimitiveTypeReference primitiveType = GetEdmPrimitiveType(clrType); + if (primitiveType != null) + { + return primitiveType; + } + + if (edmModel == null) + { + throw Error.ArgumentNull(nameof(edmModel)); + } - map.AddClrToEdmMap(clrType, edmTypeRef); + TypeCacheItem map = _cache.GetOrAdd(edmModel, d => new TypeCacheItem()); + + // Search from cache + if (map.TryFindEdmType(clrType, out IEdmTypeReference edmTypeRef)) + { return edmTypeRef; } - private IEdmType GetEdmType(IEdmModel edmModel, Type clrType, bool testCollections) + // Not in the cache, let's build the Edm type reference. + IEdmType edmType = GetEdmType(edmModel, clrType, testCollections: true); + if (edmType != null) { - Contract.Assert(edmModel != null); - Contract.Assert(clrType != null); + bool isNullable = clrType.IsNullable(); + edmTypeRef = edmType.ToEdmTypeReference(isNullable); + } + else + { + edmTypeRef = null; + } - IEdmPrimitiveTypeReference primitiveType = GetEdmPrimitiveType(clrType); - if (primitiveType != null) - { - return primitiveType.Definition; - } - else + map.AddClrToEdmMap(clrType, edmTypeRef); + return edmTypeRef; + } + + private IEdmType GetEdmType(IEdmModel edmModel, Type clrType, bool testCollections) + { + Contract.Assert(edmModel != null); + Contract.Assert(clrType != null); + + IEdmPrimitiveTypeReference primitiveType = GetEdmPrimitiveType(clrType); + if (primitiveType != null) + { + return primitiveType.Definition; + } + else + { + if (testCollections) { - if (testCollections) + Type entityType; + if (clrType.IsDeltaSetWrapper(out entityType)) { - Type entityType; - if (clrType.IsDeltaSetWrapper(out entityType)) + IEdmType elementType = GetEdmType(edmModel, entityType, testCollections: false); + if (elementType != null) { - IEdmType elementType = GetEdmType(edmModel, entityType, testCollections: false); - if (elementType != null) - { - return new EdmCollectionType(elementType.ToEdmTypeReference(entityType.IsNullable())); - } + return new EdmCollectionType(elementType.ToEdmTypeReference(entityType.IsNullable())); } + } - Type enumerableOfT = ExtractGenericInterface(clrType, typeof(IEnumerable<>)); - Type asyncEnumerableOfT = ExtractGenericInterface(clrType, typeof(IAsyncEnumerable<>)); + Type enumerableOfT = ExtractGenericInterface(clrType, typeof(IEnumerable<>)); + Type asyncEnumerableOfT = ExtractGenericInterface(clrType, typeof(IAsyncEnumerable<>)); - if (enumerableOfT != null || asyncEnumerableOfT != null) - { - Type elementClrType = null; + if (enumerableOfT != null || asyncEnumerableOfT != null) + { + Type elementClrType = null; - if (enumerableOfT != null) - { - elementClrType = enumerableOfT.GetGenericArguments()[0]; - } - else - { - elementClrType = asyncEnumerableOfT.GetGenericArguments()[0]; - } - - // IEnumerable> is a collection of T. - if (elementClrType.IsSelectExpandWrapper(out entityType)) - { - elementClrType = entityType; - } - - if (elementClrType.IsComputeWrapper(out entityType)) - { - elementClrType = entityType; - } - - IEdmType elementType = GetEdmType(edmModel, elementClrType, testCollections: false); - if (elementType != null) - { - return new EdmCollectionType(elementType.ToEdmTypeReference(elementClrType.IsNullable())); - } + if (enumerableOfT != null) + { + elementClrType = enumerableOfT.GetGenericArguments()[0]; + } + else + { + elementClrType = asyncEnumerableOfT.GetGenericArguments()[0]; } - } - - Type underlyingType = TypeHelper.GetUnderlyingTypeOrSelf(clrType); - if (TypeHelper.IsEnum(underlyingType)) - { - clrType = underlyingType; - } - // search for the ClrTypeAnnotation and return it if present - IEdmType returnType = - edmModel - .SchemaElements - .OfType() - .Select(edmType => new { EdmType = edmType, Annotation = edmModel.GetAnnotationValue(edmType) }) - .Where(tuple => tuple.Annotation != null && tuple.Annotation.ClrType == clrType) - .Select(tuple => tuple.EdmType) - .SingleOrDefault(); + // IEnumerable> is a collection of T. + if (elementClrType.IsSelectExpandWrapper(out entityType)) + { + elementClrType = entityType; + } - // default to the EdmType with the same name as the ClrType name - returnType = returnType ?? edmModel.FindType(clrType.EdmFullName()); + if (elementClrType.IsComputeWrapper(out entityType)) + { + elementClrType = entityType; + } - if (clrType.BaseType != null) - { - // go up the inheritance tree to see if we have a mapping defined for the base type. - returnType = returnType ?? GetEdmType(edmModel, clrType.BaseType, testCollections); + IEdmType elementType = GetEdmType(edmModel, elementClrType, testCollections: false); + if (elementType != null) + { + return new EdmCollectionType(elementType.ToEdmTypeReference(elementClrType.IsNullable())); + } } - - return returnType; - } - } - #endregion - - #region EdmType -> ClrType - /// - /// Gets the corresponding for a given Edm type . - /// - /// The Edm model. - /// The Edm type. - /// The nullable or not. - /// The assembly resolver. if it's null, will use the default resolver. - /// Null or the CLR type. - public virtual Type GetClrType(IEdmModel edmModel, IEdmType edmType, bool nullable, IAssemblyResolver assembliesResolver) - { - if (edmType == null) - { - throw Error.ArgumentNull(nameof(edmType)); } - if (edmType.TypeKind == EdmTypeKind.Primitive) + Type underlyingType = TypeHelper.GetUnderlyingTypeOrSelf(clrType); + if (TypeHelper.IsEnum(underlyingType)) { - return GetClrPrimitiveType((IEdmPrimitiveType)edmType, nullable); + clrType = underlyingType; } - if (edmModel == null) - { - throw Error.ArgumentNull(nameof(edmModel)); - } + // search for the ClrTypeAnnotation and return it if present + IEdmType returnType = + edmModel + .SchemaElements + .OfType() + .Select(edmType => new { EdmType = edmType, Annotation = edmModel.GetAnnotationValue(edmType) }) + .Where(tuple => tuple.Annotation != null && tuple.Annotation.ClrType == clrType) + .Select(tuple => tuple.EdmType) + .SingleOrDefault(); - assembliesResolver = assembliesResolver ?? AssemblyResolverHelper.Default; + // default to the EdmType with the same name as the ClrType name + returnType = returnType ?? edmModel.FindType(clrType.EdmFullName()); - // Let's search from cache - TypeCacheItem map = _cache.GetOrAdd(edmModel, d => new TypeCacheItem()); - if (map.TryFindClrType(edmType, nullable, out Type clrType)) + if (clrType.BaseType != null) { - return clrType; + // go up the inheritance tree to see if we have a mapping defined for the base type. + returnType = returnType ?? GetEdmType(edmModel, clrType.BaseType, testCollections); } - // If not cached, find the CLR type from the model. - clrType = FindClrType(edmModel, edmType, assembliesResolver); + return returnType; + } + } + #endregion - if (clrType != null && nullable && clrType.IsEnum) - { - clrType = TypeHelper.ToNullable(clrType); - } + #region EdmType -> ClrType + /// + /// Gets the corresponding for a given Edm type . + /// + /// The Edm model. + /// The Edm type. + /// The nullable or not. + /// The assembly resolver. if it's null, will use the default resolver. + /// Null or the CLR type. + public virtual Type GetClrType(IEdmModel edmModel, IEdmType edmType, bool nullable, IAssemblyResolver assembliesResolver) + { + if (edmType == null) + { + throw Error.ArgumentNull(nameof(edmType)); + } - map.AddEdmToClrMap(edmType, nullable, clrType); + if (edmType.TypeKind == EdmTypeKind.Primitive) + { + return GetClrPrimitiveType((IEdmPrimitiveType)edmType, nullable); + } - return clrType; + if (edmModel == null) + { + throw Error.ArgumentNull(nameof(edmModel)); } - /// - /// Finds the corresponding CLR type for a given Edm type reference. - /// - /// The Edm model. - /// The Edm type. - /// The assembly resolver. - /// Null or the CLR type. - internal static Type FindClrType(IEdmModel edmModel, IEdmType edmType, IAssemblyResolver assembliesResolver) + assembliesResolver = assembliesResolver ?? AssemblyResolverHelper.Default; + + // Let's search from cache + TypeCacheItem map = _cache.GetOrAdd(edmModel, d => new TypeCacheItem()); + if (map.TryFindClrType(edmType, nullable, out Type clrType)) { - if (edmModel == null) - { - throw Error.ArgumentNull(nameof(edmModel)); - } + return clrType; + } - if (edmType == null) - { - throw Error.ArgumentNull(nameof(edmType)); - } + // If not cached, find the CLR type from the model. + clrType = FindClrType(edmModel, edmType, assembliesResolver); - if (assembliesResolver == null) - { - throw Error.ArgumentNull(nameof(assembliesResolver)); - } + if (clrType != null && nullable && clrType.IsEnum) + { + clrType = TypeHelper.ToNullable(clrType); + } - IEdmSchemaType edmSchemaType = edmType as IEdmSchemaType; - if (edmSchemaType == null) - { - return null; - } + map.AddEdmToClrMap(edmType, nullable, clrType); - // by default, retrieve it from Clr type annotation. - ClrTypeAnnotation annotation = edmModel.GetAnnotationValue(edmSchemaType); - if (annotation != null) - { - return annotation.ClrType; - } + return clrType; + } - string typeName = edmSchemaType.FullName(); - IEnumerable matchingTypes = GetMatchingTypes(typeName, assembliesResolver); + /// + /// Finds the corresponding CLR type for a given Edm type reference. + /// + /// The Edm model. + /// The Edm type. + /// The assembly resolver. + /// Null or the CLR type. + internal static Type FindClrType(IEdmModel edmModel, IEdmType edmType, IAssemblyResolver assembliesResolver) + { + if (edmModel == null) + { + throw Error.ArgumentNull(nameof(edmModel)); + } - if (matchingTypes.Count() > 1) - { - throw Error.InvalidOperation(SRResources.MultipleMatchingClrTypesForEdmType, - typeName, string.Join(",", matchingTypes.Select(type => type.AssemblyQualifiedName))); - } + if (edmType == null) + { + throw Error.ArgumentNull(nameof(edmType)); + } - Type clrType = matchingTypes.SingleOrDefault(); + if (assembliesResolver == null) + { + throw Error.ArgumentNull(nameof(assembliesResolver)); + } - // TODO: shall we save it back to model because we will cache it? - // I think we should not save it back to model, since we will cache it - // edmModel.SetAnnotationValue(edmSchemaType, new ClrTypeAnnotation(clrType)); - return clrType; + IEdmSchemaType edmSchemaType = edmType as IEdmSchemaType; + if (edmSchemaType == null) + { + return null; } - #endregion - private static Type ExtractGenericInterface(Type queryType, Type interfaceType) + // by default, retrieve it from Clr type annotation. + ClrTypeAnnotation annotation = edmModel.GetAnnotationValue(edmSchemaType); + if (annotation != null) { - Func matchesInterface = t => t.IsGenericType && t.GetGenericTypeDefinition() == interfaceType; - return matchesInterface(queryType) ? queryType : queryType.GetInterfaces().FirstOrDefault(matchesInterface); + return annotation.ClrType; } - private static IEnumerable GetMatchingTypes(string edmFullName, IAssemblyResolver assembliesResolver) - => TypeHelper.GetLoadedTypes(assembliesResolver).Where(t => t.IsPublic && t.EdmFullName() == edmFullName); + string typeName = edmSchemaType.FullName(); + IEnumerable matchingTypes = GetMatchingTypes(typeName, assembliesResolver); - private static void BuildTypeMapping(EdmPrimitiveTypeKind primitiveKind, bool isStandard) + if (matchingTypes.Count() > 1) { - Type type = typeof(T); - bool isNullable = type.IsNullable(); - IEdmPrimitiveTypeReference edmPrimitiveTypeReference = EdmCoreModel.Instance.GetPrimitive(primitiveKind, isNullable); - ClrPrimitiveTypes[type] = edmPrimitiveTypeReference; + throw Error.InvalidOperation(SRResources.MultipleMatchingClrTypesForEdmType, + typeName, string.Join(",", matchingTypes.Select(type => type.AssemblyQualifiedName))); + } + + Type clrType = matchingTypes.SingleOrDefault(); + + // TODO: shall we save it back to model because we will cache it? + // I think we should not save it back to model, since we will cache it + // edmModel.SetAnnotationValue(edmSchemaType, new ClrTypeAnnotation(clrType)); + return clrType; + } + #endregion + + private static Type ExtractGenericInterface(Type queryType, Type interfaceType) + { + Func matchesInterface = t => t.IsGenericType && t.GetGenericTypeDefinition() == interfaceType; + return matchesInterface(queryType) ? queryType : queryType.GetInterfaces().FirstOrDefault(matchesInterface); + } + + private static IEnumerable GetMatchingTypes(string edmFullName, IAssemblyResolver assembliesResolver) + => TypeHelper.GetLoadedTypes(assembliesResolver).Where(t => t.IsPublic && t.EdmFullName() == edmFullName); + + private static void BuildTypeMapping(EdmPrimitiveTypeKind primitiveKind, bool isStandard) + { + Type type = typeof(T); + bool isNullable = type.IsNullable(); + IEdmPrimitiveTypeReference edmPrimitiveTypeReference = EdmCoreModel.Instance.GetPrimitive(primitiveKind, isNullable); + ClrPrimitiveTypes[type] = edmPrimitiveTypeReference; - if (isStandard) + if (isStandard) + { + IEdmPrimitiveType primitiveType = edmPrimitiveTypeReference.PrimitiveDefinition(); + if (isNullable) { - IEdmPrimitiveType primitiveType = edmPrimitiveTypeReference.PrimitiveDefinition(); - if (isNullable) + // for nullable, for example System.String, we don't have non-nullable string. + // so, let's save it for both. + // And since we make the order un-changable, it means 'nullable' coming first. + EdmPrimitiveTypes[primitiveType] = (type, type); + } + else + { + if (EdmPrimitiveTypes.ContainsKey(primitiveType)) { - // for nullable, for example System.String, we don't have non-nullable string. - // so, let's save it for both. - // And since we make the order un-changable, it means 'nullable' coming first. - EdmPrimitiveTypes[primitiveType] = (type, type); + (Type _, Type edmType2) = EdmPrimitiveTypes[primitiveType]; + EdmPrimitiveTypes[primitiveType] = (type, edmType2); } else { - if (EdmPrimitiveTypes.ContainsKey(primitiveType)) - { - (Type _, Type edmType2) = EdmPrimitiveTypes[primitiveType]; - EdmPrimitiveTypes[primitiveType] = (type, edmType2); - } - else - { - EdmPrimitiveTypes[primitiveType] = (type, null); - } + EdmPrimitiveTypes[primitiveType] = (type, null); } } } + } - private static void BuildValueTypeMapping(EdmPrimitiveTypeKind primitiveKind, bool isStandard = true) - where T : struct - { - // Do not change the order for the nullable or non-nullable. Put nullable ahead of non-nullable. - // By design: non-nullable will overwrite the item1. + private static void BuildValueTypeMapping(EdmPrimitiveTypeKind primitiveKind, bool isStandard = true) + where T : struct + { + // Do not change the order for the nullable or non-nullable. Put nullable ahead of non-nullable. + // By design: non-nullable will overwrite the item1. - BuildTypeMapping(primitiveKind, isStandard); - BuildTypeMapping(primitiveKind, isStandard); - } + BuildTypeMapping(primitiveKind, isStandard); + BuildTypeMapping(primitiveKind, isStandard); + } - private static void BuildReferenceTypeMapping(EdmPrimitiveTypeKind primitiveKind, bool isStandard = true) - where T : class - { - BuildTypeMapping(primitiveKind, isStandard); - } + private static void BuildReferenceTypeMapping(EdmPrimitiveTypeKind primitiveKind, bool isStandard = true) + where T : class + { + BuildTypeMapping(primitiveKind, isStandard); } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/EdmClrTypeMapExtensions.cs b/src/Microsoft.AspNetCore.OData/Edm/EdmClrTypeMapExtensions.cs index 86e210ca1..85a9f6704 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EdmClrTypeMapExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EdmClrTypeMapExtensions.cs @@ -13,181 +13,180 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// The extensions used to map between C# types and Edm types. +/// +internal static class EdmClrTypeMapExtensions { /// - /// The extensions used to map between C# types and Edm types. + /// Gets the corresponding Edm primitive type for a given type. /// - internal static class EdmClrTypeMapExtensions + /// The given CLR type. + /// Null or the Edm primitive type. + public static IEdmPrimitiveTypeReference GetEdmPrimitiveTypeReference(this Type clrType) { - /// - /// Gets the corresponding Edm primitive type for a given type. - /// - /// The given CLR type. - /// Null or the Edm primitive type. - public static IEdmPrimitiveTypeReference GetEdmPrimitiveTypeReference(this Type clrType) + return DefaultODataTypeMapper.Default.GetEdmPrimitiveType(clrType); + } + + /// + /// Gets the corresponding Edm primitive type for a given type. + /// + /// The Edm model. + /// The given CLR type. + /// Null or the Edm primitive type. + public static IEdmPrimitiveTypeReference GetEdmPrimitiveTypeReference(this IEdmModel edmModel, Type clrType) + { + if (edmModel == null || edmModel is EdmCoreModel) { return DefaultODataTypeMapper.Default.GetEdmPrimitiveType(clrType); } - /// - /// Gets the corresponding Edm primitive type for a given type. - /// - /// The Edm model. - /// The given CLR type. - /// Null or the Edm primitive type. - public static IEdmPrimitiveTypeReference GetEdmPrimitiveTypeReference(this IEdmModel edmModel, Type clrType) - { - if (edmModel == null || edmModel is EdmCoreModel) - { - return DefaultODataTypeMapper.Default.GetEdmPrimitiveType(clrType); - } + return edmModel.GetTypeMapper().GetEdmPrimitiveType(clrType); + } - return edmModel.GetTypeMapper().GetEdmPrimitiveType(clrType); + /// + /// Gets the corresponding CLR type for a given Edm primitive type. + /// + /// The Edm model. + /// The given Edm primitive type. + /// Null or the CLR type. + public static Type GetClrPrimitiveType(this IEdmModel edmModel, IEdmPrimitiveTypeReference edmPrimitiveType) + { + if (edmPrimitiveType == null) + { + return null; } - /// - /// Gets the corresponding CLR type for a given Edm primitive type. - /// - /// The Edm model. - /// The given Edm primitive type. - /// Null or the CLR type. - public static Type GetClrPrimitiveType(this IEdmModel edmModel, IEdmPrimitiveTypeReference edmPrimitiveType) + if (edmModel == null || edmModel is EdmCoreModel) { - if (edmPrimitiveType == null) - { - return null; - } + return DefaultODataTypeMapper.Default.GetClrPrimitiveType(edmPrimitiveType.PrimitiveDefinition(), edmPrimitiveType.IsNullable); + } - if (edmModel == null || edmModel is EdmCoreModel) - { - return DefaultODataTypeMapper.Default.GetClrPrimitiveType(edmPrimitiveType.PrimitiveDefinition(), edmPrimitiveType.IsNullable); - } + return edmModel.GetTypeMapper().GetPrimitiveType(edmPrimitiveType); + } - return edmModel.GetTypeMapper().GetPrimitiveType(edmPrimitiveType); + /// + /// Figures out if the given clr type is nonstandard edm primitive like uint, ushort, char[] etc. + /// and returns the corresponding clr type to which we map like uint => long. + /// + /// The Edm model. + /// The potential non-standard CLR type. + /// A boolean value out to indicate whether the input CLR type is standard OData primitive type. + /// The standard CLR type or the input CLR type itself. + public static Type IsNonstandardEdmPrimitive(this IEdmModel edmModel, Type clrType, out bool isNonstandardEdmPrimitive) + { + IEdmPrimitiveTypeReference edmType = edmModel.GetEdmPrimitiveTypeReference(clrType); + if (edmType == null) + { + isNonstandardEdmPrimitive = false; + return clrType; } - /// - /// Figures out if the given clr type is nonstandard edm primitive like uint, ushort, char[] etc. - /// and returns the corresponding clr type to which we map like uint => long. - /// - /// The Edm model. - /// The potential non-standard CLR type. - /// A boolean value out to indicate whether the input CLR type is standard OData primitive type. - /// The standard CLR type or the input CLR type itself. - public static Type IsNonstandardEdmPrimitive(this IEdmModel edmModel, Type clrType, out bool isNonstandardEdmPrimitive) - { - IEdmPrimitiveTypeReference edmType = edmModel.GetEdmPrimitiveTypeReference(clrType); - if (edmType == null) - { - isNonstandardEdmPrimitive = false; - return clrType; - } + Type reverseLookupClrType = edmModel.GetClrPrimitiveType(edmType); + isNonstandardEdmPrimitive = (clrType != reverseLookupClrType); - Type reverseLookupClrType = edmModel.GetClrPrimitiveType(edmType); - isNonstandardEdmPrimitive = (clrType != reverseLookupClrType); + return reverseLookupClrType; + } - return reverseLookupClrType; - } + /// + /// Gets the Edm type reference from the CLR type. + /// + /// The Edm model. + /// The given CLR type. + /// null or the Edm type reference. + public static IEdmTypeReference GetEdmTypeReference(this IEdmModel edmModel, Type clrType) + { + return edmModel.GetTypeMapper().GetEdmTypeReference(edmModel, clrType); + } - /// - /// Gets the Edm type reference from the CLR type. - /// - /// The Edm model. - /// The given CLR type. - /// null or the Edm type reference. - public static IEdmTypeReference GetEdmTypeReference(this IEdmModel edmModel, Type clrType) - { - return edmModel.GetTypeMapper().GetEdmTypeReference(edmModel, clrType); - } + /// + /// Gets the Edm type from the CLR type. + /// + /// The Edm model. + /// The given CLR type. + /// null or the Edm type. + public static IEdmType GetEdmType(this IEdmModel edmModel, Type clrType) + { + return edmModel.GetEdmTypeReference(clrType)?.Definition; + } - /// - /// Gets the Edm type from the CLR type. - /// - /// The Edm model. - /// The given CLR type. - /// null or the Edm type. - public static IEdmType GetEdmType(this IEdmModel edmModel, Type clrType) - { - return edmModel.GetEdmTypeReference(clrType)?.Definition; - } + /// + /// Gets the corresponding CLR type for a given Edm type reference. + /// + /// The Edm model. + /// The Edm type reference. + /// Null or the CLR type. + public static Type GetClrType(this IEdmModel edmModel, IEdmTypeReference edmTypeReference) + { + return edmModel.GetTypeMapper().GetClrType(edmModel, edmTypeReference, AssemblyResolverHelper.Default); + } - /// - /// Gets the corresponding CLR type for a given Edm type reference. - /// - /// The Edm model. - /// The Edm type reference. - /// Null or the CLR type. - public static Type GetClrType(this IEdmModel edmModel, IEdmTypeReference edmTypeReference) - { - return edmModel.GetTypeMapper().GetClrType(edmModel, edmTypeReference, AssemblyResolverHelper.Default); - } + /// + /// Gets the corresponding CLR type for a given Edm type reference. + /// + /// The Edm model. + /// The Edm type reference. + /// The assembly resolver. + /// Null or the CLR type. + public static Type GetClrType(this IEdmModel edmModel, IEdmTypeReference edmTypeReference, IAssemblyResolver assembliesResolver) + { + return edmModel.GetTypeMapper().GetClrType(edmModel, edmTypeReference, assembliesResolver); + } - /// - /// Gets the corresponding CLR type for a given Edm type reference. - /// - /// The Edm model. - /// The Edm type reference. - /// The assembly resolver. - /// Null or the CLR type. - public static Type GetClrType(this IEdmModel edmModel, IEdmTypeReference edmTypeReference, IAssemblyResolver assembliesResolver) - { - return edmModel.GetTypeMapper().GetClrType(edmModel, edmTypeReference, assembliesResolver); - } + /// + /// Gets the corresponding CLR type for a given Edm type. + /// + /// The Edm model. + /// The Edm type. + /// Null or the CLR type. + public static Type GetClrType(this IEdmModel edmModel, IEdmType edmType) + { + return edmModel.GetClrType(edmType, AssemblyResolverHelper.Default); + } - /// - /// Gets the corresponding CLR type for a given Edm type. - /// - /// The Edm model. - /// The Edm type. - /// Null or the CLR type. - public static Type GetClrType(this IEdmModel edmModel, IEdmType edmType) - { - return edmModel.GetClrType(edmType, AssemblyResolverHelper.Default); - } + /// + /// Gets the corresponding CLR type for a given Edm type. + /// + /// The Edm model. + /// The Edm type. + /// The assembly resolver. + /// Null or the CLR type. + public static Type GetClrType(this IEdmModel edmModel, IEdmType edmType, IAssemblyResolver assembliesResolver) + { + return edmModel.GetTypeMapper().GetClrType(edmModel, edmType, true, assembliesResolver); + } - /// - /// Gets the corresponding CLR type for a given Edm type. - /// - /// The Edm model. - /// The Edm type. - /// The assembly resolver. - /// Null or the CLR type. - public static Type GetClrType(this IEdmModel edmModel, IEdmType edmType, IAssemblyResolver assembliesResolver) - { - return edmModel.GetTypeMapper().GetClrType(edmModel, edmType, true, assembliesResolver); - } + internal static string EdmFullName(this Type clrType) + { + return String.Format(CultureInfo.InvariantCulture, "{0}.{1}", clrType.Namespace, clrType.EdmName()); + } - internal static string EdmFullName(this Type clrType) - { - return String.Format(CultureInfo.InvariantCulture, "{0}.{1}", clrType.Namespace, clrType.EdmName()); - } + // Mangle the invalid EDM literal Type.FullName (System.Collections.Generic.IEnumerable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]) + // to a valid EDM literal (the C# type name IEnumerable). + internal static string EdmName(this Type clrType) + { + // We cannot use just Type.Name here as it doesn't work for generic types. + return MangleClrTypeName(clrType); + } + + // TODO (work item 336): Support nested types and anonymous types. + private static string MangleClrTypeName(Type type) + { + Contract.Assert(type != null); - // Mangle the invalid EDM literal Type.FullName (System.Collections.Generic.IEnumerable`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]) - // to a valid EDM literal (the C# type name IEnumerable). - internal static string EdmName(this Type clrType) + if (!type.IsGenericType) { - // We cannot use just Type.Name here as it doesn't work for generic types. - return MangleClrTypeName(clrType); + return type.Name; } - - // TODO (work item 336): Support nested types and anonymous types. - private static string MangleClrTypeName(Type type) + else { - Contract.Assert(type != null); - - if (!type.IsGenericType) - { - return type.Name; - } - else - { - return String.Format( - CultureInfo.InvariantCulture, - "{0}Of{1}", - type.Name.Replace('`', '_'), - String.Join("_", type.GetGenericArguments().Select(t => MangleClrTypeName(t)))); - } + return String.Format( + CultureInfo.InvariantCulture, + "{0}Of{1}", + type.Name.Replace('`', '_'), + String.Join("_", type.GetGenericArguments().Select(t => MangleClrTypeName(t)))); } } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/EdmHelpers.cs b/src/Microsoft.AspNetCore.OData/Edm/EdmHelpers.cs index ad545ca52..5c8256138 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EdmHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EdmHelpers.cs @@ -12,305 +12,279 @@ using Microsoft.OData.ModelBuilder.Annotations; using Microsoft.OData.ModelBuilder.Config; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// Provides the functionalities related to the Edm type. +/// +internal static class EdmHelpers { - /// - /// Provides the functionalities related to the Edm type. - /// - internal static class EdmHelpers + public static IEdmStructuredTypeReference ToStructuredTypeReference(this IEdmTypeReference edmTypeReference) { - public static IEdmStructuredTypeReference ToStructuredTypeReference(this IEdmTypeReference edmTypeReference) + if (edmTypeReference == null) { - if (edmTypeReference == null) - { - return null; - } - - if (edmTypeReference is IEdmStructuredTypeReference structuredTypeReference) - { - return structuredTypeReference; - } - - if (edmTypeReference.IsUntyped()) - { - return EdmUntypedStructuredTypeReference.NullableTypeReference; - } - - return edmTypeReference.AsStructured(); + return null; } - public static bool IsStructuredOrUntypedStructuredCollection(this IEdmTypeReference edmTypeReference) + if (edmTypeReference is IEdmStructuredTypeReference structuredTypeReference) { - if (edmTypeReference == null) - { - return false; - } + return structuredTypeReference; + } - if (!edmTypeReference.IsCollection()) - { - return false; - } + if (edmTypeReference.IsUntyped()) + { + return EdmUntypedStructuredTypeReference.NullableTypeReference; + } - IEdmTypeReference elementType = edmTypeReference.AsCollection().ElementType(); + return edmTypeReference.AsStructured(); + } - return elementType.IsStructuredOrUntypedStructured(); + public static bool IsStructuredOrUntypedStructuredCollection(this IEdmTypeReference edmTypeReference) + { + if (edmTypeReference == null) + { + return false; } - public static bool IsStructuredOrUntypedStructured(this IEdmTypeReference edmTypeReference) + if (!edmTypeReference.IsCollection()) { - if (edmTypeReference == null) - { - return false; - } + return false; + } - if (edmTypeReference.IsStructured()) - { - return true; - } + IEdmTypeReference elementType = edmTypeReference.AsCollection().ElementType(); - if (edmTypeReference.IsUntyped() && - typeof(IEdmStructuredTypeReference).IsAssignableFrom(edmTypeReference.GetType())) - { - return true; - } + return elementType.IsStructuredOrUntypedStructured(); + } + public static bool IsStructuredOrUntypedStructured(this IEdmTypeReference edmTypeReference) + { + if (edmTypeReference == null) + { return false; } - /// - /// Tests whether a is 'Edm.Untyped' or 'Collection(Edm.Untyped)'. - /// - /// The test type reference. - /// True/false. - public static bool IsStructuredOrUntyped(this IEdmTypeReference edmTypeReference) + if (edmTypeReference.IsStructured()) { - IEdmTypeReference elementType = edmTypeReference.IsCollection() ? - edmTypeReference.AsCollection().ElementType() : - edmTypeReference; - - return elementType.IsUntyped() || elementType.IsStructured(); + return true; } - /// - /// Tests whether a is 'Collection(Edm.Untyped)'. - /// - /// The test type reference. - /// True/false. - public static bool IsCollectionUntyped(this IEdmTypeReference typeReference) + if (edmTypeReference.IsUntyped() && + typeof(IEdmStructuredTypeReference).IsAssignableFrom(edmTypeReference.GetType())) { - return typeReference != null && - typeReference.IsCollection() && typeReference.AsCollection().ElementType().IsUntyped(); + return true; } - /// - /// Tests whether a is 'Edm.Untyped' or 'Collection(Edm.Untyped)'. - /// - /// The test type reference. - /// True/false - public static bool IsUntypedOrCollectionUntyped(this IEdmTypeReference typeReference) - { - return typeReference != null && - (typeReference.IsUntyped() || - (typeReference.IsCollection() && typeReference.AsCollection().ElementType().IsUntyped())); - } + return false; + } - /// - /// Get element type reference if it's collection or return itself - /// - /// The test type reference. - /// Element type or itself. - public static IEdmTypeReference GetElementTypeOrSelf(this IEdmTypeReference typeReference) - { - if (typeReference == null) - { - return typeReference; - } + /// + /// Tests whether a is 'Edm.Untyped' or 'Collection(Edm.Untyped)'. + /// + /// The test type reference. + /// True/false. + public static bool IsStructuredOrUntyped(this IEdmTypeReference edmTypeReference) + { + IEdmTypeReference elementType = edmTypeReference.IsCollection() ? + edmTypeReference.AsCollection().ElementType() : + edmTypeReference; - if (typeReference.TypeKind() == EdmTypeKind.Collection) - { - IEdmCollectionTypeReference collectType = typeReference.AsCollection(); - return collectType.ElementType(); - } + return elementType.IsUntyped() || elementType.IsStructured(); + } + + /// + /// Tests whether a is 'Collection(Edm.Untyped)'. + /// + /// The test type reference. + /// True/false. + public static bool IsCollectionUntyped(this IEdmTypeReference typeReference) + { + return typeReference != null && + typeReference.IsCollection() && typeReference.AsCollection().ElementType().IsUntyped(); + } + /// + /// Tests whether a is 'Edm.Untyped' or 'Collection(Edm.Untyped)'. + /// + /// The test type reference. + /// True/false + public static bool IsUntypedOrCollectionUntyped(this IEdmTypeReference typeReference) + { + return typeReference != null && + (typeReference.IsUntyped() || + (typeReference.IsCollection() && typeReference.AsCollection().ElementType().IsUntyped())); + } + + /// + /// Get element type reference if it's collection or return itself + /// + /// The test type reference. + /// Element type or itself. + public static IEdmTypeReference GetElementTypeOrSelf(this IEdmTypeReference typeReference) + { + if (typeReference == null) + { return typeReference; } - /// - /// Get the elementType if it's collection or return itself - /// - /// The test type reference. - /// Element type or itself. - public static IEdmType GetElementType(this IEdmTypeReference edmTypeReference) + if (typeReference.TypeKind() == EdmTypeKind.Collection) { - return edmTypeReference.GetElementTypeOrSelf()?.Definition; + IEdmCollectionTypeReference collectType = typeReference.AsCollection(); + return collectType.ElementType(); } - /// - /// Converts the to . - /// - /// The given Edm type. - /// Nullable or not. - /// The collection type. - public static IEdmCollectionType ToCollection(this IEdmType edmType, bool isNullable) + return typeReference; + } + + /// + /// Get the elementType if it's collection or return itself + /// + /// The test type reference. + /// Element type or itself. + public static IEdmType GetElementType(this IEdmTypeReference edmTypeReference) + { + return edmTypeReference.GetElementTypeOrSelf()?.Definition; + } + + /// + /// Converts the to . + /// + /// The given Edm type. + /// Nullable or not. + /// The collection type. + public static IEdmCollectionType ToCollection(this IEdmType edmType, bool isNullable) + { + if (edmType == null) { - if (edmType == null) - { - throw Error.ArgumentNull(nameof(edmType)); - } + throw Error.ArgumentNull(nameof(edmType)); + } + + return new EdmCollectionType(edmType.ToEdmTypeReference(isNullable)); + } - return new EdmCollectionType(edmType.ToEdmTypeReference(isNullable)); + /// + /// Converts an Edm Type to Edm type reference. + /// + /// The Edm type. + /// Nullable value. + /// The Edm type reference. + public static IEdmTypeReference ToEdmTypeReference(this IEdmType edmType, bool isNullable) + { + if (edmType == null) + { + throw Error.ArgumentNull(nameof(edmType)); } - /// - /// Converts an Edm Type to Edm type reference. - /// - /// The Edm type. - /// Nullable value. - /// The Edm type reference. - public static IEdmTypeReference ToEdmTypeReference(this IEdmType edmType, bool isNullable) + switch (edmType.TypeKind) { - if (edmType == null) - { - throw Error.ArgumentNull(nameof(edmType)); - } + case EdmTypeKind.Collection: + return new EdmCollectionTypeReference((IEdmCollectionType)edmType); - switch (edmType.TypeKind) - { - case EdmTypeKind.Collection: - return new EdmCollectionTypeReference((IEdmCollectionType)edmType); + case EdmTypeKind.Complex: + return new EdmComplexTypeReference((IEdmComplexType)edmType, isNullable); - case EdmTypeKind.Complex: - return new EdmComplexTypeReference((IEdmComplexType)edmType, isNullable); + case EdmTypeKind.Entity: + return new EdmEntityTypeReference((IEdmEntityType)edmType, isNullable); - case EdmTypeKind.Entity: - return new EdmEntityTypeReference((IEdmEntityType)edmType, isNullable); + case EdmTypeKind.EntityReference: + return new EdmEntityReferenceTypeReference((IEdmEntityReferenceType)edmType, isNullable); - case EdmTypeKind.EntityReference: - return new EdmEntityReferenceTypeReference((IEdmEntityReferenceType)edmType, isNullable); + case EdmTypeKind.Enum: + return new EdmEnumTypeReference((IEdmEnumType)edmType, isNullable); - case EdmTypeKind.Enum: - return new EdmEnumTypeReference((IEdmEnumType)edmType, isNullable); + case EdmTypeKind.Primitive: + return EdmCoreModel.Instance.GetPrimitive(((IEdmPrimitiveType)edmType).PrimitiveKind, isNullable); - case EdmTypeKind.Primitive: - return EdmCoreModel.Instance.GetPrimitive(((IEdmPrimitiveType)edmType).PrimitiveKind, isNullable); + case EdmTypeKind.Path: + return new EdmPathTypeReference((IEdmPathType)edmType, isNullable); - case EdmTypeKind.Path: - return new EdmPathTypeReference((IEdmPathType)edmType, isNullable); + case EdmTypeKind.TypeDefinition: + return new EdmTypeDefinitionReference((IEdmTypeDefinition)edmType, isNullable); - case EdmTypeKind.TypeDefinition: - return new EdmTypeDefinitionReference((IEdmTypeDefinition)edmType, isNullable); + default: + throw Error.NotSupported(SRResources.EdmTypeNotSupported, edmType.ToTraceString()); + } + } - default: - throw Error.NotSupported(SRResources.EdmTypeNotSupported, edmType.ToTraceString()); - } + public static bool IsTopLimitExceeded(IEdmProperty property, IEdmStructuredType structuredType, + IEdmModel edmModel, int top, DefaultQueryConfigurations defaultQueryConfigs, out int maxTop) + { + maxTop = 0; + ModelBoundQuerySettings querySettings = edmModel.GetModelBoundQuerySettings(property, structuredType, defaultQueryConfigs); + if (querySettings != null && top > querySettings.MaxTop) + { + maxTop = querySettings.MaxTop.Value; + return true; } - public static bool IsTopLimitExceeded(IEdmProperty property, IEdmStructuredType structuredType, - IEdmModel edmModel, int top, DefaultQueryConfigurations defaultQueryConfigs, out int maxTop) + return false; + } + + public static bool IsNotCountable(IEdmProperty property, IEdmStructuredType structuredType, IEdmModel edmModel, + bool enableCount) + { + if (property != null) { - maxTop = 0; - ModelBoundQuerySettings querySettings = edmModel.GetModelBoundQuerySettings(property, structuredType, defaultQueryConfigs); - if (querySettings != null && top > querySettings.MaxTop) + QueryableRestrictionsAnnotation annotation = GetPropertyRestrictions(property, edmModel); + if (annotation != null && annotation.Restrictions.NotCountable) { - maxTop = querySettings.MaxTop.Value; return true; } - - return false; } - public static bool IsNotCountable(IEdmProperty property, IEdmStructuredType structuredType, IEdmModel edmModel, - bool enableCount) + ModelBoundQuerySettings querySettings = edmModel.GetModelBoundQuerySettings(property, structuredType); + if (querySettings != null && + ((!querySettings.Countable.HasValue && !enableCount) || + querySettings.Countable == false)) { - if (property != null) - { - QueryableRestrictionsAnnotation annotation = GetPropertyRestrictions(property, edmModel); - if (annotation != null && annotation.Restrictions.NotCountable) - { - return true; - } - } + return true; + } - ModelBoundQuerySettings querySettings = edmModel.GetModelBoundQuerySettings(property, structuredType); - if (querySettings != null && - ((!querySettings.Countable.HasValue && !enableCount) || - querySettings.Countable == false)) - { - return true; - } + return false; + } - return false; + public static bool IsNotFilterable(IEdmProperty edmProperty, IEdmProperty pathEdmProperty, + IEdmStructuredType pathEdmStructuredType, + IEdmModel edmModel, bool enableFilter) + { + QueryableRestrictionsAnnotation annotation = GetPropertyRestrictions(edmProperty, edmModel); + if (annotation != null && annotation.Restrictions.NotFilterable) + { + return true; } - - public static bool IsNotFilterable(IEdmProperty edmProperty, IEdmProperty pathEdmProperty, - IEdmStructuredType pathEdmStructuredType, - IEdmModel edmModel, bool enableFilter) + else { - QueryableRestrictionsAnnotation annotation = GetPropertyRestrictions(edmProperty, edmModel); - if (annotation != null && annotation.Restrictions.NotFilterable) + if (pathEdmStructuredType == null) { - return true; + pathEdmStructuredType = edmProperty.DeclaringType; } - else - { - if (pathEdmStructuredType == null) - { - pathEdmStructuredType = edmProperty.DeclaringType; - } - ModelBoundQuerySettings querySettings = edmModel.GetModelBoundQuerySettings(pathEdmProperty, pathEdmStructuredType); - if (!enableFilter) - { - return !querySettings.Filterable(edmProperty.Name); - } - - bool enable; - if (querySettings.FilterConfigurations.TryGetValue(edmProperty.Name, out enable)) - { - return !enable; - } - else - { - return querySettings.DefaultEnableFilter == false; - } + ModelBoundQuerySettings querySettings = edmModel.GetModelBoundQuerySettings(pathEdmProperty, pathEdmStructuredType); + if (!enableFilter) + { + return !querySettings.Filterable(edmProperty.Name); } - } - public static bool IsNotSortable(IEdmProperty edmProperty, IEdmProperty pathEdmProperty, - IEdmStructuredType pathEdmStructuredType, IEdmModel edmModel, bool enableOrderBy) - { - QueryableRestrictionsAnnotation annotation = GetPropertyRestrictions(edmProperty, edmModel); - if (annotation != null && annotation.Restrictions.NotSortable) + bool enable; + if (querySettings.FilterConfigurations.TryGetValue(edmProperty.Name, out enable)) { - return true; + return !enable; } else { - if (pathEdmStructuredType == null) - { - pathEdmStructuredType = edmProperty.DeclaringType; - } - - ModelBoundQuerySettings querySettings = edmModel.GetModelBoundQuerySettings(pathEdmProperty, pathEdmStructuredType); - if (!enableOrderBy) - { - return !querySettings.Sortable(edmProperty.Name); - } - - bool enable; - if (querySettings.OrderByConfigurations.TryGetValue(edmProperty.Name, out enable)) - { - return !enable; - } - else - { - return querySettings.DefaultEnableOrderBy == false; - } + return querySettings.DefaultEnableFilter == false; } } + } - public static bool IsNotSelectable(IEdmProperty edmProperty, IEdmProperty pathEdmProperty, - IEdmStructuredType pathEdmStructuredType, IEdmModel edmModel, bool enableSelect) + public static bool IsNotSortable(IEdmProperty edmProperty, IEdmProperty pathEdmProperty, + IEdmStructuredType pathEdmStructuredType, IEdmModel edmModel, bool enableOrderBy) + { + QueryableRestrictionsAnnotation annotation = GetPropertyRestrictions(edmProperty, edmModel); + if (annotation != null && annotation.Restrictions.NotSortable) + { + return true; + } + else { if (pathEdmStructuredType == null) { @@ -318,207 +292,232 @@ public static bool IsNotSelectable(IEdmProperty edmProperty, IEdmProperty pathEd } ModelBoundQuerySettings querySettings = edmModel.GetModelBoundQuerySettings(pathEdmProperty, pathEdmStructuredType); - if (!enableSelect) + if (!enableOrderBy) { - return !querySettings.Selectable(edmProperty.Name); + return !querySettings.Sortable(edmProperty.Name); } - SelectExpandType enable; - if (querySettings.SelectConfigurations.TryGetValue(edmProperty.Name, out enable)) + bool enable; + if (querySettings.OrderByConfigurations.TryGetValue(edmProperty.Name, out enable)) { - return enable == SelectExpandType.Disabled; + return !enable; } else { - return querySettings.DefaultSelectType == SelectExpandType.Disabled; + return querySettings.DefaultEnableOrderBy == false; } } + } - public static bool IsNotNavigable(IEdmProperty edmProperty, IEdmModel edmModel) + public static bool IsNotSelectable(IEdmProperty edmProperty, IEdmProperty pathEdmProperty, + IEdmStructuredType pathEdmStructuredType, IEdmModel edmModel, bool enableSelect) + { + if (pathEdmStructuredType == null) { - QueryableRestrictionsAnnotation annotation = GetPropertyRestrictions(edmProperty, edmModel); - return annotation == null ? false : annotation.Restrictions.NotNavigable; + pathEdmStructuredType = edmProperty.DeclaringType; } - public static bool IsNotExpandable(IEdmProperty edmProperty, IEdmModel edmModel) + ModelBoundQuerySettings querySettings = edmModel.GetModelBoundQuerySettings(pathEdmProperty, pathEdmStructuredType); + if (!enableSelect) { - QueryableRestrictionsAnnotation annotation = GetPropertyRestrictions(edmProperty, edmModel); - return annotation == null ? false : annotation.Restrictions.NotExpandable; + return !querySettings.Selectable(edmProperty.Name); } - public static bool IsExpandable(string propertyName, IEdmProperty property, IEdmStructuredType structuredType, - IEdmModel edmModel, - out ExpandConfiguration expandConfiguration) + SelectExpandType enable; + if (querySettings.SelectConfigurations.TryGetValue(edmProperty.Name, out enable)) { - expandConfiguration = null; - ModelBoundQuerySettings querySettings = edmModel.GetModelBoundQuerySettings(property, structuredType); - if (querySettings != null) - { - bool result = querySettings.Expandable(propertyName); - if (!querySettings.ExpandConfigurations.TryGetValue(propertyName, out expandConfiguration) && result) - { - expandConfiguration = new ExpandConfiguration - { - ExpandType = querySettings.DefaultExpandType.Value, - MaxDepth = querySettings.DefaultMaxDepth - }; - } + return enable == SelectExpandType.Disabled; + } + else + { + return querySettings.DefaultSelectType == SelectExpandType.Disabled; + } + } - return result; - } + public static bool IsNotNavigable(IEdmProperty edmProperty, IEdmModel edmModel) + { + QueryableRestrictionsAnnotation annotation = GetPropertyRestrictions(edmProperty, edmModel); + return annotation == null ? false : annotation.Restrictions.NotNavigable; + } - return false; - } + public static bool IsNotExpandable(IEdmProperty edmProperty, IEdmModel edmModel) + { + QueryableRestrictionsAnnotation annotation = GetPropertyRestrictions(edmProperty, edmModel); + return annotation == null ? false : annotation.Restrictions.NotExpandable; + } - public static ModelBoundQuerySettings GetModelBoundQuerySettingsOrNull(this IEdmModel edmModel, IEdmStructuredType structuredType, IEdmProperty property) + public static bool IsExpandable(string propertyName, IEdmProperty property, IEdmStructuredType structuredType, + IEdmModel edmModel, + out ExpandConfiguration expandConfiguration) + { + expandConfiguration = null; + ModelBoundQuerySettings querySettings = edmModel.GetModelBoundQuerySettings(property, structuredType); + if (querySettings != null) { - if (edmModel == null) + bool result = querySettings.Expandable(propertyName); + if (!querySettings.ExpandConfigurations.TryGetValue(propertyName, out expandConfiguration) && result) { - throw Error.ArgumentNull(nameof(edmModel)); + expandConfiguration = new ExpandConfiguration + { + ExpandType = querySettings.DefaultExpandType.Value, + MaxDepth = querySettings.DefaultMaxDepth + }; } - ModelBoundQuerySettings querySettingsOnType = null; - if (structuredType != null) - { - querySettingsOnType = edmModel.GetAnnotationValue(structuredType); - } + return result; + } - if (property == null) - { - return querySettingsOnType; - } + return false; + } - ModelBoundQuerySettings querySettingsOnProperty = edmModel.GetAnnotationValue(property); - if (querySettingsOnProperty == null) - { - return querySettingsOnType; - } - else - { - if (querySettingsOnType == null) - { - return querySettingsOnProperty; - } - else - { - // Settings on property is higher priority than the ones on type. - return GetMergedPropertyQuerySettings(querySettingsOnProperty, querySettingsOnType); - } - } + public static ModelBoundQuerySettings GetModelBoundQuerySettingsOrNull(this IEdmModel edmModel, IEdmStructuredType structuredType, IEdmProperty property) + { + if (edmModel == null) + { + throw Error.ArgumentNull(nameof(edmModel)); } - public static ModelBoundQuerySettings GetModelBoundQuerySettings(this IEdmModel edmModel, IEdmProperty property, - IEdmStructuredType structuredType, DefaultQueryConfigurations defaultQueryConfigs = null) + ModelBoundQuerySettings querySettingsOnType = null; + if (structuredType != null) { - if (edmModel == null) - { - throw Error.ArgumentNull(nameof(edmModel)); - } + querySettingsOnType = edmModel.GetAnnotationValue(structuredType); + } + + if (property == null) + { + return querySettingsOnType; + } - ModelBoundQuerySettings querySettings = edmModel.GetModelBoundQuerySettings(structuredType, defaultQueryConfigs); - if (property == null) + ModelBoundQuerySettings querySettingsOnProperty = edmModel.GetAnnotationValue(property); + if (querySettingsOnProperty == null) + { + return querySettingsOnType; + } + else + { + if (querySettingsOnType == null) { - return querySettings; + return querySettingsOnProperty; } else { // Settings on property is higher priority than the ones on type. - ModelBoundQuerySettings propertyQuerySettings = edmModel.GetModelBoundQuerySettings(property, defaultQueryConfigs); - return GetMergedPropertyQuerySettings(propertyQuerySettings, querySettings); + return GetMergedPropertyQuerySettings(querySettingsOnProperty, querySettingsOnType); } } + } - private static ModelBoundQuerySettings GetModelBoundQuerySettings(this IEdmModel edmModel, T key, DefaultQueryConfigurations defaultQueryConfigs = null) - where T : IEdmElement + public static ModelBoundQuerySettings GetModelBoundQuerySettings(this IEdmModel edmModel, IEdmProperty property, + IEdmStructuredType structuredType, DefaultQueryConfigurations defaultQueryConfigs = null) + { + if (edmModel == null) { - if (key == null) - { - return null; - } - else - { - ModelBoundQuerySettings querySettings = edmModel.GetAnnotationValue(key); - if (querySettings == null) - { - querySettings = new ModelBoundQuerySettings(); - if (defaultQueryConfigs != null && - (!defaultQueryConfigs.MaxTop.HasValue || defaultQueryConfigs.MaxTop > 0)) - { - querySettings.MaxTop = defaultQueryConfigs.MaxTop; - } - } + throw Error.ArgumentNull(nameof(edmModel)); + } - return querySettings; - } + ModelBoundQuerySettings querySettings = edmModel.GetModelBoundQuerySettings(structuredType, defaultQueryConfigs); + if (property == null) + { + return querySettings; + } + else + { + // Settings on property is higher priority than the ones on type. + ModelBoundQuerySettings propertyQuerySettings = edmModel.GetModelBoundQuerySettings(property, defaultQueryConfigs); + return GetMergedPropertyQuerySettings(propertyQuerySettings, querySettings); } + } - private static ModelBoundQuerySettings GetMergedPropertyQuerySettings(ModelBoundQuerySettings propertyQuerySettings, ModelBoundQuerySettings propertyTypeQuerySettings) + private static ModelBoundQuerySettings GetModelBoundQuerySettings(this IEdmModel edmModel, T key, DefaultQueryConfigurations defaultQueryConfigs = null) + where T : IEdmElement + { + if (key == null) { - ModelBoundQuerySettings mergedQuerySettings = new ModelBoundQuerySettings(propertyQuerySettings); - if (propertyTypeQuerySettings != null) + return null; + } + else + { + ModelBoundQuerySettings querySettings = edmModel.GetAnnotationValue(key); + if (querySettings == null) { - if (!mergedQuerySettings.PageSize.HasValue) + querySettings = new ModelBoundQuerySettings(); + if (defaultQueryConfigs != null && + (!defaultQueryConfigs.MaxTop.HasValue || defaultQueryConfigs.MaxTop > 0)) { - mergedQuerySettings.PageSize = propertyTypeQuerySettings.PageSize; + querySettings.MaxTop = defaultQueryConfigs.MaxTop; } + } - if (mergedQuerySettings.MaxTop == 0 && propertyTypeQuerySettings.MaxTop != 0) - { - mergedQuerySettings.MaxTop = propertyTypeQuerySettings.MaxTop; - } + return querySettings; + } + } - if (!mergedQuerySettings.Countable.HasValue) - { - mergedQuerySettings.Countable = propertyTypeQuerySettings.Countable; - } + private static ModelBoundQuerySettings GetMergedPropertyQuerySettings(ModelBoundQuerySettings propertyQuerySettings, ModelBoundQuerySettings propertyTypeQuerySettings) + { + ModelBoundQuerySettings mergedQuerySettings = new ModelBoundQuerySettings(propertyQuerySettings); + if (propertyTypeQuerySettings != null) + { + if (!mergedQuerySettings.PageSize.HasValue) + { + mergedQuerySettings.PageSize = propertyTypeQuerySettings.PageSize; + } - if (mergedQuerySettings.OrderByConfigurations.Count == 0 && !mergedQuerySettings.DefaultEnableOrderBy.HasValue) - { - mergedQuerySettings.CopyOrderByConfigurations(propertyTypeQuerySettings.OrderByConfigurations); - mergedQuerySettings.DefaultEnableOrderBy = propertyTypeQuerySettings.DefaultEnableOrderBy; - } + if (mergedQuerySettings.MaxTop == 0 && propertyTypeQuerySettings.MaxTop != 0) + { + mergedQuerySettings.MaxTop = propertyTypeQuerySettings.MaxTop; + } - if (mergedQuerySettings.FilterConfigurations.Count == 0 && !mergedQuerySettings.DefaultEnableFilter.HasValue) - { - mergedQuerySettings.CopyFilterConfigurations(propertyTypeQuerySettings.FilterConfigurations); - mergedQuerySettings.DefaultEnableFilter = propertyTypeQuerySettings.DefaultEnableFilter; - } + if (!mergedQuerySettings.Countable.HasValue) + { + mergedQuerySettings.Countable = propertyTypeQuerySettings.Countable; + } - if (mergedQuerySettings.SelectConfigurations.Count == 0 && !mergedQuerySettings.DefaultSelectType.HasValue) - { - mergedQuerySettings.CopySelectConfigurations(propertyTypeQuerySettings.SelectConfigurations); - mergedQuerySettings.DefaultSelectType = propertyTypeQuerySettings.DefaultSelectType; - } + if (mergedQuerySettings.OrderByConfigurations.Count == 0 && !mergedQuerySettings.DefaultEnableOrderBy.HasValue) + { + mergedQuerySettings.CopyOrderByConfigurations(propertyTypeQuerySettings.OrderByConfigurations); + mergedQuerySettings.DefaultEnableOrderBy = propertyTypeQuerySettings.DefaultEnableOrderBy; + } - if (mergedQuerySettings.ExpandConfigurations.Count == 0 && !mergedQuerySettings.DefaultExpandType.HasValue) - { - mergedQuerySettings.CopyExpandConfigurations(propertyTypeQuerySettings.ExpandConfigurations); - mergedQuerySettings.DefaultExpandType = propertyTypeQuerySettings.DefaultExpandType; - mergedQuerySettings.DefaultMaxDepth = propertyTypeQuerySettings.DefaultMaxDepth; - } + if (mergedQuerySettings.FilterConfigurations.Count == 0 && !mergedQuerySettings.DefaultEnableFilter.HasValue) + { + mergedQuerySettings.CopyFilterConfigurations(propertyTypeQuerySettings.FilterConfigurations); + mergedQuerySettings.DefaultEnableFilter = propertyTypeQuerySettings.DefaultEnableFilter; + } + + if (mergedQuerySettings.SelectConfigurations.Count == 0 && !mergedQuerySettings.DefaultSelectType.HasValue) + { + mergedQuerySettings.CopySelectConfigurations(propertyTypeQuerySettings.SelectConfigurations); + mergedQuerySettings.DefaultSelectType = propertyTypeQuerySettings.DefaultSelectType; } - return mergedQuerySettings; + if (mergedQuerySettings.ExpandConfigurations.Count == 0 && !mergedQuerySettings.DefaultExpandType.HasValue) + { + mergedQuerySettings.CopyExpandConfigurations(propertyTypeQuerySettings.ExpandConfigurations); + mergedQuerySettings.DefaultExpandType = propertyTypeQuerySettings.DefaultExpandType; + mergedQuerySettings.DefaultMaxDepth = propertyTypeQuerySettings.DefaultMaxDepth; + } } - internal static QueryableRestrictionsAnnotation GetPropertyRestrictions(IEdmProperty edmProperty, IEdmModel edmModel) - { - Contract.Assert(edmProperty != null); - Contract.Assert(edmModel != null); + return mergedQuerySettings; + } - return edmModel.GetAnnotationValue(edmProperty); - } + internal static QueryableRestrictionsAnnotation GetPropertyRestrictions(IEdmProperty edmProperty, IEdmModel edmModel) + { + Contract.Assert(edmProperty != null); + Contract.Assert(edmModel != null); - internal static void SetOperationTitleAnnotation(this IEdmModel model, IEdmOperation action, OperationTitleAnnotation title) - { - Contract.Assert(model != null); - model.SetAnnotationValue(action, title); - } + return edmModel.GetAnnotationValue(edmProperty); + } - internal static OperationTitleAnnotation GetOperationTitleAnnotation(this IEdmModel model, IEdmOperation operation) - { - Contract.Assert(model != null); - return model.GetAnnotationValue(operation); - } + internal static void SetOperationTitleAnnotation(this IEdmModel model, IEdmOperation action, OperationTitleAnnotation title) + { + Contract.Assert(model != null); + model.SetAnnotationValue(action, title); + } + + internal static OperationTitleAnnotation GetOperationTitleAnnotation(this IEdmModel model, IEdmOperation operation) + { + Contract.Assert(model != null); + return model.GetAnnotationValue(operation); } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/EdmModelAnnotationExtensions.cs b/src/Microsoft.AspNetCore.OData/Edm/EdmModelAnnotationExtensions.cs index c04cb57da..ff9e752d8 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EdmModelAnnotationExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EdmModelAnnotationExtensions.cs @@ -16,399 +16,398 @@ using Microsoft.OData.Edm.Vocabularies.V1; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// The extensions for the for the annotations. +/// +public static class EdmModelAnnotationExtensions { /// - /// The extensions for the for the annotations. + /// Gets the Org.OData.Core.V1.AcceptableMediaTypes /// - public static class EdmModelAnnotationExtensions + /// The Edm model. + /// The vocabulary annotate target. + /// null or the collection of media type. + internal static IList GetAcceptableMediaTypes(this IEdmModel model, IEdmVocabularyAnnotatable target) { - /// - /// Gets the Org.OData.Core.V1.AcceptableMediaTypes - /// - /// The Edm model. - /// The vocabulary annotate target. - /// null or the collection of media type. - internal static IList GetAcceptableMediaTypes(this IEdmModel model, IEdmVocabularyAnnotatable target) - { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); + } - if (target == null) - { - throw Error.ArgumentNull(nameof(target)); - } + if (target == null) + { + throw Error.ArgumentNull(nameof(target)); + } - IList mediaTypes = null; - var annotations = model.FindVocabularyAnnotations(target, CoreVocabularyModel.AcceptableMediaTypesTerm); - IEdmVocabularyAnnotation annotation = annotations.FirstOrDefault(); - if (annotation != null) + IList mediaTypes = null; + var annotations = model.FindVocabularyAnnotations(target, CoreVocabularyModel.AcceptableMediaTypesTerm); + IEdmVocabularyAnnotation annotation = annotations.FirstOrDefault(); + if (annotation != null) + { + IEdmCollectionExpression properties = annotation.Value as IEdmCollectionExpression; + if (properties != null) { - IEdmCollectionExpression properties = annotation.Value as IEdmCollectionExpression; - if (properties != null) + mediaTypes = new List(); + foreach (var element in properties.Elements) { - mediaTypes = new List(); - foreach (var element in properties.Elements) + IEdmStringConstantExpression elementValue = element as IEdmStringConstantExpression; + if (elementValue != null) { - IEdmStringConstantExpression elementValue = element as IEdmStringConstantExpression; - if (elementValue != null) - { - mediaTypes.Add(elementValue.Value); - } + mediaTypes.Add(elementValue.Value); } } } - - if (mediaTypes == null || mediaTypes.Count == 0) - { - return null; - } - - return mediaTypes; } - /// - /// Get concurrency properties. - /// - /// The Edm model. - /// The navigation source. - /// The concurrency properties. - public static IEnumerable GetConcurrencyProperties(this IEdmModel model, IEdmNavigationSource navigationSource) + if (mediaTypes == null || mediaTypes.Count == 0) { - Contract.Assert(model != null); - Contract.Assert(navigationSource != null); + return null; + } - // Ensure that concurrency properties cache is attached to model as an annotation to avoid expensive calculations each time - ConcurrencyPropertiesAnnotation concurrencyProperties = model.GetAnnotationValue(model); - if (concurrencyProperties == null) - { - concurrencyProperties = new ConcurrencyPropertiesAnnotation(); - model.SetAnnotationValue(model, concurrencyProperties); - } + return mediaTypes; + } - IEnumerable cachedProperties; - if (concurrencyProperties.TryGetValue(navigationSource, out cachedProperties)) - { - return cachedProperties; - } + /// + /// Get concurrency properties. + /// + /// The Edm model. + /// The navigation source. + /// The concurrency properties. + public static IEnumerable GetConcurrencyProperties(this IEdmModel model, IEdmNavigationSource navigationSource) + { + Contract.Assert(model != null); + Contract.Assert(navigationSource != null); + + // Ensure that concurrency properties cache is attached to model as an annotation to avoid expensive calculations each time + ConcurrencyPropertiesAnnotation concurrencyProperties = model.GetAnnotationValue(model); + if (concurrencyProperties == null) + { + concurrencyProperties = new ConcurrencyPropertiesAnnotation(); + model.SetAnnotationValue(model, concurrencyProperties); + } - IList results = new List(); - IEdmEntityType entityType = navigationSource.EntityType; - IEdmVocabularyAnnotatable annotatable = navigationSource as IEdmVocabularyAnnotatable; - if (annotatable != null) + IEnumerable cachedProperties; + if (concurrencyProperties.TryGetValue(navigationSource, out cachedProperties)) + { + return cachedProperties; + } + + IList results = new List(); + IEdmEntityType entityType = navigationSource.EntityType; + IEdmVocabularyAnnotatable annotatable = navigationSource as IEdmVocabularyAnnotatable; + if (annotatable != null) + { + var annotations = model.FindVocabularyAnnotations(annotatable, CoreVocabularyModel.ConcurrencyTerm); + IEdmVocabularyAnnotation annotation = annotations.FirstOrDefault(); + if (annotation != null) { - var annotations = model.FindVocabularyAnnotations(annotatable, CoreVocabularyModel.ConcurrencyTerm); - IEdmVocabularyAnnotation annotation = annotations.FirstOrDefault(); - if (annotation != null) + IEdmCollectionExpression properties = annotation.Value as IEdmCollectionExpression; + if (properties != null) { - IEdmCollectionExpression properties = annotation.Value as IEdmCollectionExpression; - if (properties != null) + foreach (var property in properties.Elements) { - foreach (var property in properties.Elements) + IEdmPathExpression pathExpression = property as IEdmPathExpression; + if (pathExpression != null) { - IEdmPathExpression pathExpression = property as IEdmPathExpression; - if (pathExpression != null) + // So far, we only consider the single path, because only the direct properties from declaring type are used. + // However we have an issue tracking on: https://github.com/OData/WebApi/issues/472 + string propertyName = pathExpression.PathSegments.First(); + IEdmProperty edmProperty = entityType.FindProperty(propertyName); + IEdmStructuralProperty structuralProperty = edmProperty as IEdmStructuralProperty; + if (structuralProperty != null) { - // So far, we only consider the single path, because only the direct properties from declaring type are used. - // However we have an issue tracking on: https://github.com/OData/WebApi/issues/472 - string propertyName = pathExpression.PathSegments.First(); - IEdmProperty edmProperty = entityType.FindProperty(propertyName); - IEdmStructuralProperty structuralProperty = edmProperty as IEdmStructuralProperty; - if (structuralProperty != null) - { - results.Add(structuralProperty); - } + results.Add(structuralProperty); } } } } } + } + + concurrencyProperties[navigationSource] = results; + return results; + } - concurrencyProperties[navigationSource] = results; - return results; + /// + /// Gets the Enum member annotations. + /// + /// The Edm model. + /// The Enum Type. + /// The Enum member annotation. + public static ClrEnumMemberAnnotation GetClrEnumMemberAnnotation(this IEdmModel edmModel, IEdmEnumType enumType) + { + if (edmModel == null) + { + throw Error.ArgumentNull(nameof(edmModel)); } - /// - /// Gets the Enum member annotations. - /// - /// The Edm model. - /// The Enum Type. - /// The Enum member annotation. - public static ClrEnumMemberAnnotation GetClrEnumMemberAnnotation(this IEdmModel edmModel, IEdmEnumType enumType) + if (enumType == null) { - if (edmModel == null) - { - throw Error.ArgumentNull(nameof(edmModel)); - } + throw Error.ArgumentNull(nameof(enumType)); + } - if (enumType == null) - { - throw Error.ArgumentNull(nameof(enumType)); - } + ClrEnumMemberAnnotation annotation = edmModel.GetAnnotationValue(enumType); + if (annotation != null) + { + return annotation; + } - ClrEnumMemberAnnotation annotation = edmModel.GetAnnotationValue(enumType); - if (annotation != null) - { - return annotation; - } + return null; + } - return null; + /// + /// Get the CLR property name. + /// + /// The Edm model. + /// The Edm property. + /// The property name. + public static string GetClrPropertyName(this IEdmModel edmModel, IEdmProperty edmProperty) + { + if (edmModel == null) + { + throw Error.ArgumentNull(nameof(edmModel)); } - /// - /// Get the CLR property name. - /// - /// The Edm model. - /// The Edm property. - /// The property name. - public static string GetClrPropertyName(this IEdmModel edmModel, IEdmProperty edmProperty) + if (edmProperty == null) { - if (edmModel == null) - { - throw Error.ArgumentNull(nameof(edmModel)); - } + throw Error.ArgumentNull(nameof(edmProperty)); + } - if (edmProperty == null) + string propertyName = edmProperty.Name; + ClrPropertyInfoAnnotation annotation = edmModel.GetAnnotationValue(edmProperty); + if (annotation != null) + { + PropertyInfo propertyInfo = annotation.ClrPropertyInfo; + if (propertyInfo != null) { - throw Error.ArgumentNull(nameof(edmProperty)); + propertyName = propertyInfo.Name; } + } - string propertyName = edmProperty.Name; - ClrPropertyInfoAnnotation annotation = edmModel.GetAnnotationValue(edmProperty); - if (annotation != null) - { - PropertyInfo propertyInfo = annotation.ClrPropertyInfo; - if (propertyInfo != null) - { - propertyName = propertyInfo.Name; - } - } + return propertyName; + } - return propertyName; + /// + /// Gets the dynamic property container name. + /// + /// The Edm model. + /// The Edm type. + /// The dynamic property container property info. + public static PropertyInfo GetDynamicPropertyDictionary(this IEdmModel edmModel, IEdmStructuredType edmType) + { + if (edmModel == null) + { + throw Error.ArgumentNull(nameof(edmModel)); } - /// - /// Gets the dynamic property container name. - /// - /// The Edm model. - /// The Edm type. - /// The dynamic property container property info. - public static PropertyInfo GetDynamicPropertyDictionary(this IEdmModel edmModel, IEdmStructuredType edmType) + if (edmType == null) { - if (edmModel == null) - { - throw Error.ArgumentNull(nameof(edmModel)); - } + throw Error.ArgumentNull(nameof(edmType)); + } - if (edmType == null) - { - throw Error.ArgumentNull(nameof(edmType)); - } + DynamicPropertyDictionaryAnnotation annotation = + edmModel.GetAnnotationValue(edmType); + if (annotation != null) + { + return annotation.PropertyInfo; + } - DynamicPropertyDictionaryAnnotation annotation = - edmModel.GetAnnotationValue(edmType); - if (annotation != null) - { - return annotation.PropertyInfo; - } + return null; + } - return null; + /// + /// Gets the model name. + /// + /// The Edm model. + /// The Edm model name. + public static string GetModelName(this IEdmModel model) + { + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); } - /// - /// Gets the model name. - /// - /// The Edm model. - /// The Edm model name. - public static string GetModelName(this IEdmModel model) + ModelNameAnnotation annotation = + model.GetAnnotationValue(model); + if (annotation != null) { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + return annotation.ModelName; + } - ModelNameAnnotation annotation = - model.GetAnnotationValue(model); - if (annotation != null) - { - return annotation.ModelName; - } + string name = Guid.NewGuid().ToString(); + SetModelName(model, name); + return name; + } - string name = Guid.NewGuid().ToString(); - SetModelName(model, name); - return name; + /// + /// Sets the Edm model name. + /// + /// The Edm model. + /// The Edm model name. + public static void SetModelName(this IEdmModel model, string name) + { + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); } - /// - /// Sets the Edm model name. - /// - /// The Edm model. - /// The Edm model name. - public static void SetModelName(this IEdmModel model, string name) + if (name == null) { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + throw Error.ArgumentNull(nameof(name)); + } - if (name == null) - { - throw Error.ArgumentNull(nameof(name)); - } + model.SetAnnotationValue(model, new ModelNameAnnotation(name)); + } - model.SetAnnotationValue(model, new ModelNameAnnotation(name)); + /// + /// Gets the OData type mapping provider from the model. + /// + /// The Edm model. + /// The . + public static IODataTypeMapper GetTypeMapper(this IEdmModel model) + { + // use the default one if no model or no mapper registered. + if (model == null) + { + return DefaultODataTypeMapper.Default; } - /// - /// Gets the OData type mapping provider from the model. - /// - /// The Edm model. - /// The . - public static IODataTypeMapper GetTypeMapper(this IEdmModel model) + IODataTypeMapper provider = model.GetAnnotationValue(model); + if (provider == null) { - // use the default one if no model or no mapper registered. - if (model == null) - { - return DefaultODataTypeMapper.Default; - } + return DefaultODataTypeMapper.Default; + } - IODataTypeMapper provider = model.GetAnnotationValue(model); - if (provider == null) - { - return DefaultODataTypeMapper.Default; - } + return provider; + } - return provider; + /// + /// Sets the OData type mapping provider to the model. + /// + /// The Edm model. + /// The given mapper. + public static void SetTypeMapper(this IEdmModel model, IODataTypeMapper mapper) + { + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); } - /// - /// Sets the OData type mapping provider to the model. - /// - /// The Edm model. - /// The given mapper. - public static void SetTypeMapper(this IEdmModel model, IODataTypeMapper mapper) + if (mapper == null) { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + throw Error.ArgumentNull(nameof(mapper)); + } - if (mapper == null) - { - throw Error.ArgumentNull(nameof(mapper)); - } + model.SetAnnotationValue(model, mapper); + } - model.SetAnnotationValue(model, mapper); + /// + /// Gets the declared alternate keys of the most defined entity with a declared key present. + /// Each entity type could define a set of alternate keys. + /// + /// The Edm model. + /// The Edm entity type. + /// Alternate Keys of this type. + public static IEnumerable> GetAlternateKeys(this IEdmModel model, IEdmEntityType entityType) + { + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); } - /// - /// Gets the declared alternate keys of the most defined entity with a declared key present. - /// Each entity type could define a set of alternate keys. - /// - /// The Edm model. - /// The Edm entity type. - /// Alternate Keys of this type. - public static IEnumerable> GetAlternateKeys(this IEdmModel model, IEdmEntityType entityType) + if (entityType == null) { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } - - if (entityType == null) - { - throw Error.ArgumentNull(nameof(entityType)); - } + throw Error.ArgumentNull(nameof(entityType)); + } - IEnumerable> alternateCoreKeys = null; - // Let's search the core.alternate key term first - IEdmTerm coreAlternateTerm = CoreVocabularyModel.Instance.FindDeclaredTerm("Org.OData.Core.V1.AlternateKeys"); - if (coreAlternateTerm != null) - { - model.TryGetAlternateKeys(entityType, coreAlternateTerm, out alternateCoreKeys); - } + IEnumerable> alternateCoreKeys = null; + // Let's search the core.alternate key term first + IEdmTerm coreAlternateTerm = CoreVocabularyModel.Instance.FindDeclaredTerm("Org.OData.Core.V1.AlternateKeys"); + if (coreAlternateTerm != null) + { + model.TryGetAlternateKeys(entityType, coreAlternateTerm, out alternateCoreKeys); + } - // for back compatibility, let's support the community.alternatekey - IEnumerable> alternateKeys = null; - IEdmTerm communityAlternateTerm = AlternateKeysVocabularyModel.Instance.FindDeclaredTerm("OData.Community.Keys.V1.AlternateKeys"); - if (communityAlternateTerm != null) - { - model.TryGetAlternateKeys(entityType, communityAlternateTerm, out alternateKeys); - } + // for back compatibility, let's support the community.alternatekey + IEnumerable> alternateKeys = null; + IEdmTerm communityAlternateTerm = AlternateKeysVocabularyModel.Instance.FindDeclaredTerm("OData.Community.Keys.V1.AlternateKeys"); + if (communityAlternateTerm != null) + { + model.TryGetAlternateKeys(entityType, communityAlternateTerm, out alternateKeys); + } - if (alternateCoreKeys == null) - { - return alternateKeys; - } - else if (alternateKeys == null) - { - return alternateCoreKeys; - } - else - { - return alternateCoreKeys.Concat(alternateKeys); - } + if (alternateCoreKeys == null) + { + return alternateKeys; } + else if (alternateKeys == null) + { + return alternateCoreKeys; + } + else + { + return alternateCoreKeys.Concat(alternateKeys); + } + } - private static bool TryGetAlternateKeys(this IEdmModel model, IEdmEntityType entityType, IEdmTerm term, - out IEnumerable> alternateKeys) + private static bool TryGetAlternateKeys(this IEdmModel model, IEdmEntityType entityType, IEdmTerm term, + out IEnumerable> alternateKeys) + { + IEdmEntityType checkingType = entityType; + while (checkingType != null) { - IEdmEntityType checkingType = entityType; - while (checkingType != null) + IEnumerable> declaredAlternateKeys = GetDeclaredAlternateKeysForType(model, checkingType, term); + if (declaredAlternateKeys != null) { - IEnumerable> declaredAlternateKeys = GetDeclaredAlternateKeysForType(model, checkingType, term); - if (declaredAlternateKeys != null) - { - alternateKeys = declaredAlternateKeys; - return true; - } - - checkingType = checkingType.BaseEntityType(); + alternateKeys = declaredAlternateKeys; + return true; } - alternateKeys = null; - return false; + checkingType = checkingType.BaseEntityType(); } - private static IEnumerable> GetDeclaredAlternateKeysForType(IEdmModel model, IEdmEntityType type, IEdmTerm term) + alternateKeys = null; + return false; + } + + private static IEnumerable> GetDeclaredAlternateKeysForType(IEdmModel model, IEdmEntityType type, IEdmTerm term) + { + IEdmVocabularyAnnotation annotationValue = model.FindVocabularyAnnotations(type, term).FirstOrDefault(); + if (annotationValue != null) { - IEdmVocabularyAnnotation annotationValue = model.FindVocabularyAnnotations(type, term).FirstOrDefault(); - if (annotationValue != null) - { - List> declaredAlternateKeys = new List>(); + List> declaredAlternateKeys = new List>(); - IEdmCollectionExpression keys = annotationValue.Value as IEdmCollectionExpression; + IEdmCollectionExpression keys = annotationValue.Value as IEdmCollectionExpression; - foreach (IEdmRecordExpression key in keys.Elements.OfType()) + foreach (IEdmRecordExpression key in keys.Elements.OfType()) + { + var edmPropertyConstructor = key.Properties.FirstOrDefault(e => e.Name == "Key"); + if (edmPropertyConstructor != null) { - var edmPropertyConstructor = key.Properties.FirstOrDefault(e => e.Name == "Key"); - if (edmPropertyConstructor != null) - { - IEdmCollectionExpression collectionExpression = edmPropertyConstructor.Value as IEdmCollectionExpression; + IEdmCollectionExpression collectionExpression = edmPropertyConstructor.Value as IEdmCollectionExpression; - IDictionary alternateKey = new Dictionary(); - foreach (IEdmRecordExpression propertyRef in collectionExpression.Elements.OfType()) - { - var aliasProp = propertyRef.Properties.FirstOrDefault(e => e.Name == "Alias"); - string alias = ((IEdmStringConstantExpression)aliasProp.Value).Value; + IDictionary alternateKey = new Dictionary(); + foreach (IEdmRecordExpression propertyRef in collectionExpression.Elements.OfType()) + { + var aliasProp = propertyRef.Properties.FirstOrDefault(e => e.Name == "Alias"); + string alias = ((IEdmStringConstantExpression)aliasProp.Value).Value; - var nameProp = propertyRef.Properties.FirstOrDefault(e => e.Name == "Name"); - alternateKey[alias] = (IEdmPathExpression)nameProp.Value; - } + var nameProp = propertyRef.Properties.FirstOrDefault(e => e.Name == "Name"); + alternateKey[alias] = (IEdmPathExpression)nameProp.Value; + } - if (alternateKey.Any()) - { - declaredAlternateKeys.Add(alternateKey); - } + if (alternateKey.Any()) + { + declaredAlternateKeys.Add(alternateKey); } } - - return declaredAlternateKeys; } - return null; + return declaredAlternateKeys; } + + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/EdmModelExtensions.cs b/src/Microsoft.AspNetCore.OData/Edm/EdmModelExtensions.cs index 833e2e6b8..176007a40 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EdmModelExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EdmModelExtensions.cs @@ -15,596 +15,595 @@ using Microsoft.OData.Edm.Validation; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +internal static class EdmModelExtensions { - internal static class EdmModelExtensions + /// + /// Resolve the type reference from the type name of + /// + /// The Edm model. + /// The given resource set. + /// The resolved type. + public static IEdmCollectionTypeReference ResolveResourceSetType(this IEdmModel model, ODataResourceSetBase resourceSet) { - /// - /// Resolve the type reference from the type name of - /// - /// The Edm model. - /// The given resource set. - /// The resolved type. - public static IEdmCollectionTypeReference ResolveResourceSetType(this IEdmModel model, ODataResourceSetBase resourceSet) - { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); + } - if (resourceSet == null) - { - throw Error.ArgumentNull(nameof(resourceSet)); - } + if (resourceSet == null) + { + throw Error.ArgumentNull(nameof(resourceSet)); + } - EdmCollectionTypeReference collectionType; - if (string.IsNullOrEmpty(resourceSet.TypeName) || - string.Equals(resourceSet.TypeName, "Collection(Edm.Untyped)", StringComparison.OrdinalIgnoreCase)) - { - collectionType = EdmUntypedHelpers.NullableUntypedCollectionReference; - } - else - { - string elementTypeName = - DeserializationHelpers.GetCollectionElementTypeName(resourceSet.TypeName, - isNested: false); - IEdmSchemaType elementType = model.FindType(elementTypeName); + EdmCollectionTypeReference collectionType; + if (string.IsNullOrEmpty(resourceSet.TypeName) || + string.Equals(resourceSet.TypeName, "Collection(Edm.Untyped)", StringComparison.OrdinalIgnoreCase)) + { + collectionType = EdmUntypedHelpers.NullableUntypedCollectionReference; + } + else + { + string elementTypeName = + DeserializationHelpers.GetCollectionElementTypeName(resourceSet.TypeName, + isNested: false); + IEdmSchemaType elementType = model.FindType(elementTypeName); - IEdmTypeReference edmTypeReference = elementType.ToEdmTypeReference(true); - collectionType = new EdmCollectionTypeReference(new EdmCollectionType(edmTypeReference)); - } + IEdmTypeReference edmTypeReference = elementType.ToEdmTypeReference(true); + collectionType = new EdmCollectionTypeReference(new EdmCollectionType(edmTypeReference)); + } + + return collectionType; + } - return collectionType; + /// + /// Resolve the type reference from the type name of + /// + /// The Edm model. + /// The given resource. + /// The resolved type. + public static IEdmStructuredTypeReference ResolveResourceType(this IEdmModel model, ODataResourceBase resource) + { + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); } - /// - /// Resolve the type reference from the type name of - /// - /// The Edm model. - /// The given resource. - /// The resolved type. - public static IEdmStructuredTypeReference ResolveResourceType(this IEdmModel model, ODataResourceBase resource) + if (resource == null) { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + throw Error.ArgumentNull(nameof(resource)); + } - if (resource == null) + IEdmStructuredTypeReference resourceType; + if (string.IsNullOrEmpty(resource.TypeName) || + string.Equals(resource.TypeName, "Edm.Untyped", StringComparison.OrdinalIgnoreCase)) + { + resourceType = EdmUntypedStructuredTypeReference.NullableTypeReference; + } + else + { + IEdmStructuredType actualType = model.FindType(resource.TypeName) as IEdmStructuredType; + if (actualType == null) { - throw Error.ArgumentNull(nameof(resource)); + throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, resource.TypeName)); } - IEdmStructuredTypeReference resourceType; - if (string.IsNullOrEmpty(resource.TypeName) || - string.Equals(resource.TypeName, "Edm.Untyped", StringComparison.OrdinalIgnoreCase)) + if (actualType is IEdmEntityType actualEntityType) { - resourceType = EdmUntypedStructuredTypeReference.NullableTypeReference; + resourceType = new EdmEntityTypeReference(actualEntityType, isNullable: false); } else { - IEdmStructuredType actualType = model.FindType(resource.TypeName) as IEdmStructuredType; - if (actualType == null) - { - throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, resource.TypeName)); - } - - if (actualType is IEdmEntityType actualEntityType) - { - resourceType = new EdmEntityTypeReference(actualEntityType, isNullable: false); - } - else - { - resourceType = new EdmComplexTypeReference(actualType as IEdmComplexType, isNullable: false); - } + resourceType = new EdmComplexTypeReference(actualType as IEdmComplexType, isNullable: false); } + } + + return resourceType; + } - return resourceType; + /// + /// Get all property names for the given structured type. + /// + /// The Edm model. + /// The given structured type. + /// All property names. + public static ICollection GetAllProperties(this IEdmModel model, IEdmStructuredType structuredType) + { + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); } - /// - /// Get all property names for the given structured type. - /// - /// The Edm model. - /// The given structured type. - /// All property names. - public static ICollection GetAllProperties(this IEdmModel model, IEdmStructuredType structuredType) + if (structuredType == null) { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + throw Error.ArgumentNull(nameof(structuredType)); + } - if (structuredType == null) - { - throw Error.ArgumentNull(nameof(structuredType)); - } + IList allProperties = new List(); + foreach (var property in structuredType.StructuralProperties()) + { + allProperties.Add(model.GetClrPropertyName(property)); + } - IList allProperties = new List(); - foreach (var property in structuredType.StructuralProperties()) - { - allProperties.Add(model.GetClrPropertyName(property)); - } + foreach (var property in structuredType.NavigationProperties()) + { + allProperties.Add(model.GetClrPropertyName(property)); + } - foreach (var property in structuredType.NavigationProperties()) - { - allProperties.Add(model.GetClrPropertyName(property)); - } + return allProperties; + } - return allProperties; + /// + /// Resolve the alternate key properties. + /// + /// The Edm model. + /// The key segment. + /// The resolved Edm properties. + public static IDictionary ResolveAlternateKeyProperties(this IEdmModel model, KeySegment keySegment) + { + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); } - /// - /// Resolve the alternate key properties. - /// - /// The Edm model. - /// The key segment. - /// The resolved Edm properties. - public static IDictionary ResolveAlternateKeyProperties(this IEdmModel model, KeySegment keySegment) + if (keySegment == null) { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + throw Error.ArgumentNull(nameof(keySegment)); + } - if (keySegment == null) - { - throw Error.ArgumentNull(nameof(keySegment)); - } + IEdmEntityType entityType = (IEdmEntityType)keySegment.EdmType; + var alternateKeys = model.GetAlternateKeys(entityType); + if (alternateKeys == null) + { + return null; + } - IEdmEntityType entityType = (IEdmEntityType)keySegment.EdmType; - var alternateKeys = model.GetAlternateKeys(entityType); - if (alternateKeys == null) - { - return null; - } + // It should be case-sensitive, then we can support "Id" & "ID", they are different, but it's valid. + HashSet keyNames = keySegment.Keys.Select(k => k.Key).ToHashSet(/*StringComparer.OrdinalIgnoreCase*/); - // It should be case-sensitive, then we can support "Id" & "ID", they are different, but it's valid. - HashSet keyNames = keySegment.Keys.Select(k => k.Key).ToHashSet(/*StringComparer.OrdinalIgnoreCase*/); + // Let's find the alternate key in alternate keys + // The count should match + // The keys should match the alias + int count = keySegment.Keys.Count(); + IDictionary foundAlternateKey = alternateKeys.FirstOrDefault(a => a.Count == count && keyNames.SetEquals(a.Keys)); - // Let's find the alternate key in alternate keys - // The count should match - // The keys should match the alias - int count = keySegment.Keys.Count(); - IDictionary foundAlternateKey = alternateKeys.FirstOrDefault(a => a.Count == count && keyNames.SetEquals(a.Keys)); + if (foundAlternateKey == null) + { + return null; + } - if (foundAlternateKey == null) + IDictionary properties = null; + foreach (var alternateKey in foundAlternateKey) + { + IEdmProperty edmProperty = model.FindProperty(entityType, alternateKey.Value); + if (edmProperty == null) { - return null; + throw new ODataException(Error.Format(SRResources.PropertyNotFoundOnPathExpression, alternateKey.Value.Path, entityType.FullName())); } - IDictionary properties = null; - foreach (var alternateKey in foundAlternateKey) + if (properties == null) { - IEdmProperty edmProperty = model.FindProperty(entityType, alternateKey.Value); - if (edmProperty == null) - { - throw new ODataException(Error.Format(SRResources.PropertyNotFoundOnPathExpression, alternateKey.Value.Path, entityType.FullName())); - } - - if (properties == null) - { - properties = new Dictionary(); - } - - properties[alternateKey.Key] = edmProperty; + properties = new Dictionary(); } - return properties; + properties[alternateKey.Key] = edmProperty; } - /// - /// Resolve the using the property name. This method supports the property name case insensitive. - /// However, ODL only support case-sensitive. - /// - /// The given structural type - /// The given property name. - /// The resolved . - public static IEdmProperty ResolveProperty(this IEdmStructuredType structuredType, string propertyName) + return properties; + } + + /// + /// Resolve the using the property name. This method supports the property name case insensitive. + /// However, ODL only support case-sensitive. + /// + /// The given structural type + /// The given property name. + /// The resolved . + public static IEdmProperty ResolveProperty(this IEdmStructuredType structuredType, string propertyName) + { + if (structuredType == null) { - if (structuredType == null) - { - throw Error.ArgumentNull(nameof(structuredType)); - } + throw Error.ArgumentNull(nameof(structuredType)); + } - bool ambiguous = false; - IEdmProperty edmProperty = null; - foreach (var property in structuredType.Properties()) + bool ambiguous = false; + IEdmProperty edmProperty = null; + foreach (var property in structuredType.Properties()) + { + string name = property.Name; + if (name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) { - string name = property.Name; - if (name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)) + if (name.Equals(propertyName, StringComparison.Ordinal)) { - if (name.Equals(propertyName, StringComparison.Ordinal)) - { - return property; - } - else if (edmProperty != null) - { - ambiguous = true; - } - else - { - edmProperty = property; - } + return property; + } + else if (edmProperty != null) + { + ambiguous = true; + } + else + { + edmProperty = property; } } + } - if (ambiguous) - { - throw new ODataException(Error.Format(SRResources.AmbiguousPropertyNameFound, propertyName)); - } - - return edmProperty; + if (ambiguous) + { + throw new ODataException(Error.Format(SRResources.AmbiguousPropertyNameFound, propertyName)); } - /// - /// Resolve the using the type name. This method supports the type name case insensitive. - /// - /// The Edm model. - /// The type name. - /// The Edm schema type. - public static IEdmSchemaType ResolveType(this IEdmModel model, string typeName) + return edmProperty; + } + + /// + /// Resolve the using the type name. This method supports the type name case insensitive. + /// + /// The Edm model. + /// The type name. + /// The Edm schema type. + public static IEdmSchemaType ResolveType(this IEdmModel model, string typeName) + { + IEdmSchemaType type = model.FindType(typeName); + if (type != null) { - IEdmSchemaType type = model.FindType(typeName); - if (type != null) - { - return type; - } + return type; + } - var types = model.SchemaElements.OfType() + var types = model.SchemaElements.OfType() + .Where(e => string.Equals(typeName, e.FullName(), StringComparison.OrdinalIgnoreCase)); + + foreach (var refModels in model.ReferencedModels) + { + var refedTypes = refModels.SchemaElements.OfType() .Where(e => string.Equals(typeName, e.FullName(), StringComparison.OrdinalIgnoreCase)); - foreach (var refModels in model.ReferencedModels) - { - var refedTypes = refModels.SchemaElements.OfType() - .Where(e => string.Equals(typeName, e.FullName(), StringComparison.OrdinalIgnoreCase)); + types = types.Concat(refedTypes); + } - types = types.Concat(refedTypes); - } + if (types.Count() > 1) + { + throw new ODataException(Error.Format(SRResources.AmbiguousTypeNameFound, typeName)); + } - if (types.Count() > 1) - { - throw new ODataException(Error.Format(SRResources.AmbiguousTypeNameFound, typeName)); - } + return types.SingleOrDefault(); + } - return types.SingleOrDefault(); + /// + /// Find the property using the given starting from the given . + /// + /// The Edm model. + /// The structured type. + /// The property path. + /// Null or the found edm property. + public static IEdmProperty FindProperty(this IEdmModel model, IEdmStructuredType structuredType, IEdmPathExpression path) + { + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); } - /// - /// Find the property using the given starting from the given . - /// - /// The Edm model. - /// The structured type. - /// The property path. - /// Null or the found edm property. - public static IEdmProperty FindProperty(this IEdmModel model, IEdmStructuredType structuredType, IEdmPathExpression path) + if (structuredType == null) { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + throw Error.ArgumentNull(nameof(structuredType)); + } - if (structuredType == null) - { - throw Error.ArgumentNull(nameof(structuredType)); - } + if (path == null) + { + throw Error.ArgumentNull(nameof(path)); + } - if (path == null) + IEdmProperty property = null; + IEdmStructuredType startingType = structuredType; + foreach (var segment in path.PathSegments) + { + if (string.IsNullOrEmpty(segment)) { - throw Error.ArgumentNull(nameof(path)); + // Let's simply ignore the empty segment + continue; } - IEdmProperty property = null; - IEdmStructuredType startingType = structuredType; - foreach (var segment in path.PathSegments) + // So far, we only support "property and type cast in the path" + if (segment.Contains('.', StringComparison.Ordinal)) { - if (string.IsNullOrEmpty(segment)) + startingType = model.ResolveType(segment) as IEdmStructuredType; + if (startingType == null) { - // Let's simply ignore the empty segment - continue; + throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, segment)); } - - // So far, we only support "property and type cast in the path" - if (segment.Contains('.', StringComparison.Ordinal)) + } + else + { + if (startingType == null) { - startingType = model.ResolveType(segment) as IEdmStructuredType; - if (startingType == null) - { - throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, segment)); - } + return null; } - else + + property = startingType.ResolveProperty(segment); + if (property == null) { - if (startingType == null) - { - return null; - } - - property = startingType.ResolveProperty(segment); - if (property == null) - { - throw new ODataException(Error.Format(SRResources.PropertyNotFoundOnPathExpression, path.Path, structuredType.FullTypeName())); - } - - startingType = property.Type.GetElementTypeOrSelf().Definition as IEdmStructuredType; + throw new ODataException(Error.Format(SRResources.PropertyNotFoundOnPathExpression, path.Path, structuredType.FullTypeName())); } + + startingType = property.Type.GetElementTypeOrSelf().Definition as IEdmStructuredType; } + } + + return property; + } - return property; + /// + /// Resolve the navigation source using the input identifier + /// + /// The Edm model. + /// The identifier + /// Enable case insensitive + /// Null or the found navigation source. + public static IEdmNavigationSource ResolveNavigationSource(this IEdmModel model, string identifier, bool enableCaseInsensitive = false) + { + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); } - /// - /// Resolve the navigation source using the input identifier - /// - /// The Edm model. - /// The identifier - /// Enable case insensitive - /// Null or the found navigation source. - public static IEdmNavigationSource ResolveNavigationSource(this IEdmModel model, string identifier, bool enableCaseInsensitive = false) + IEdmNavigationSource navSource = model.FindDeclaredNavigationSource(identifier); + if (navSource != null || !enableCaseInsensitive) { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + return navSource; + } - IEdmNavigationSource navSource = model.FindDeclaredNavigationSource(identifier); - if (navSource != null || !enableCaseInsensitive) - { - return navSource; - } + IEdmEntityContainer container = model.EntityContainer; + if (container == null) + { + return null; + } - IEdmEntityContainer container = model.EntityContainer; - if (container == null) - { - return null; - } + var result = container.Elements.OfType() + .Where(source => string.Equals(identifier, source.Name, StringComparison.OrdinalIgnoreCase)).ToList(); - var result = container.Elements.OfType() - .Where(source => string.Equals(identifier, source.Name, StringComparison.OrdinalIgnoreCase)).ToList(); + if (result.Count > 1) + { + throw new ODataException(Error.Format(SRResources.AmbiguousNavigationSourceNameFound, identifier)); + } - if (result.Count > 1) - { - throw new ODataException(Error.Format(SRResources.AmbiguousNavigationSourceNameFound, identifier)); - } + return result.SingleOrDefault(); + } - return result.SingleOrDefault(); + public static IEnumerable ResolveOperationImports(this IEdmModel model, + string identifier, + bool enableCaseInsensitive = false) + { + IEnumerable results = model.FindDeclaredOperationImports(identifier); + if (results.Any() || !enableCaseInsensitive) + { + return results; } - public static IEnumerable ResolveOperationImports(this IEdmModel model, - string identifier, - bool enableCaseInsensitive = false) + IEdmEntityContainer container = model.EntityContainer; + if (container == null) { - IEnumerable results = model.FindDeclaredOperationImports(identifier); - if (results.Any() || !enableCaseInsensitive) - { - return results; - } + return null; + } - IEdmEntityContainer container = model.EntityContainer; - if (container == null) - { - return null; - } + return container.Elements.OfType() + .Where(source => string.Equals(identifier, source.Name, StringComparison.OrdinalIgnoreCase)); + } - return container.Elements.OfType() - .Where(source => string.Equals(identifier, source.Name, StringComparison.OrdinalIgnoreCase)); + internal static IEdmEntitySetBase GetTargetEntitySet(this IEdmOperation operation, IEdmNavigationSource source, IEdmModel model) + { + if (source == null) + { + return null; } - internal static IEdmEntitySetBase GetTargetEntitySet(this IEdmOperation operation, IEdmNavigationSource source, IEdmModel model) + if (operation.IsBound && operation.Parameters.Any()) { - if (source == null) - { - return null; - } + IEdmOperationParameter parameter; + Dictionary path; + IEdmEntityType lastEntityType; - if (operation.IsBound && operation.Parameters.Any()) + if (operation.TryGetRelativeEntitySetPath(model, out parameter, out path, out lastEntityType, out IEnumerable _)) { - IEdmOperationParameter parameter; - Dictionary path; - IEdmEntityType lastEntityType; + IEdmNavigationSource target = source; - if (operation.TryGetRelativeEntitySetPath(model, out parameter, out path, out lastEntityType, out IEnumerable _)) + foreach (var navigation in path) { - IEdmNavigationSource target = source; - - foreach (var navigation in path) - { - target = target.FindNavigationTarget(navigation.Key, navigation.Value); - } - - return target as IEdmEntitySetBase; + target = target.FindNavigationTarget(navigation.Key, navigation.Value); } - } - return null; + return target as IEdmEntitySetBase; + } } - public static IEdmNavigationSource FindNavigationTarget(this IEdmNavigationSource navigationSource, - IEdmNavigationProperty navigationProperty, - IList parsedSegments, - out IEdmPathExpression bindingPath) - { - bindingPath = null; + return null; + } - if (navigationProperty.ContainsTarget) - { - return navigationSource.FindNavigationTarget(navigationProperty); - } + public static IEdmNavigationSource FindNavigationTarget(this IEdmNavigationSource navigationSource, + IEdmNavigationProperty navigationProperty, + IList parsedSegments, + out IEdmPathExpression bindingPath) + { + bindingPath = null; + + if (navigationProperty.ContainsTarget) + { + return navigationSource.FindNavigationTarget(navigationProperty); + } - IEnumerable bindings = - navigationSource.FindNavigationPropertyBindings(navigationProperty); + IEnumerable bindings = + navigationSource.FindNavigationPropertyBindings(navigationProperty); - if (bindings != null) + if (bindings != null) + { + foreach (var binding in bindings) { - foreach (var binding in bindings) + if (BindingPathHelper.MatchBindingPath(binding.Path, parsedSegments)) { - if (BindingPathHelper.MatchBindingPath(binding.Path, parsedSegments)) - { - bindingPath = binding.Path; - return binding.Target; - } + bindingPath = binding.Path; + return binding.Target; } } + } - return null; + return null; + } + + public static bool IsEntityOrEntityCollectionType(this IEdmType edmType, out IEdmEntityType entityType) + { + if (edmType.TypeKind == EdmTypeKind.Entity) + { + entityType = (IEdmEntityType)edmType; + return true; } - public static bool IsEntityOrEntityCollectionType(this IEdmType edmType, out IEdmEntityType entityType) + if (edmType.TypeKind != EdmTypeKind.Collection) { - if (edmType.TypeKind == EdmTypeKind.Entity) - { - entityType = (IEdmEntityType)edmType; - return true; - } + entityType = null; + return false; + } - if (edmType.TypeKind != EdmTypeKind.Collection) - { - entityType = null; - return false; - } + entityType = ((IEdmCollectionType)edmType).ElementType.Definition as IEdmEntityType; + return entityType != null; + } - entityType = ((IEdmCollectionType)edmType).ElementType.Definition as IEdmEntityType; - return entityType != null; + internal static bool IsResourceOrCollectionResource(this IEdmTypeReference edmType) + { + if (edmType.IsEntity() || edmType.IsComplex()) + { + return true; } - internal static bool IsResourceOrCollectionResource(this IEdmTypeReference edmType) + if (edmType.IsCollection()) { - if (edmType.IsEntity() || edmType.IsComplex()) - { - return true; - } + return IsResourceOrCollectionResource(edmType.AsCollection().ElementType()); + } - if (edmType.IsCollection()) - { - return IsResourceOrCollectionResource(edmType.AsCollection().ElementType()); - } + return false; + } - return false; + /// + /// Tests type reference is enum or collection enum + /// + /// + /// + public static bool IsEnumOrCollectionEnum(this IEdmTypeReference edmType) + { + if (edmType.IsEnum()) + { + return true; } - /// - /// Tests type reference is enum or collection enum - /// - /// - /// - public static bool IsEnumOrCollectionEnum(this IEdmTypeReference edmType) + if (edmType.IsCollection()) { - if (edmType.IsEnum()) - { - return true; - } + return IsEnumOrCollectionEnum(edmType.AsCollection().ElementType()); + } + + return false; + } - if (edmType.IsCollection()) + /// + /// Find the given type in a structured type inheritance, include itself. + /// + /// The starting structural type. + /// The Edm model. + /// The searching type name. + /// If true, performs case insensitive search + /// The found type. + public static IEdmStructuredType FindTypeInInheritance(this IEdmStructuredType structuralType, IEdmModel model, string typeName, bool caseInsensitive = false) + { + StringComparison typeStringComparison = caseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + IEdmStructuredType baseType = structuralType; + while (baseType != null) + { + if (GetName(baseType).Equals(typeName, typeStringComparison)) { - return IsEnumOrCollectionEnum(edmType.AsCollection().ElementType()); + return baseType; } - return false; + baseType = baseType.BaseType; } - /// - /// Find the given type in a structured type inheritance, include itself. - /// - /// The starting structural type. - /// The Edm model. - /// The searching type name. - /// If true, performs case insensitive search - /// The found type. - public static IEdmStructuredType FindTypeInInheritance(this IEdmStructuredType structuralType, IEdmModel model, string typeName, bool caseInsensitive = false) + return model.FindAllDerivedTypes(structuralType).FirstOrDefault(c => GetName(c).Equals(typeName, typeStringComparison)); + } + + private static string GetName(IEdmStructuredType type) + { + IEdmEntityType entityType = type as IEdmEntityType; + if (entityType != null) { - StringComparison typeStringComparison = caseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - IEdmStructuredType baseType = structuralType; - while (baseType != null) - { - if (GetName(baseType).Equals(typeName, typeStringComparison)) - { - return baseType; - } + return entityType.Name; + } - baseType = baseType.BaseType; - } + return ((IEdmComplexType)type).Name; + } - return model.FindAllDerivedTypes(structuralType).FirstOrDefault(c => GetName(c).Equals(typeName, typeStringComparison)); - } + /// + /// + /// + /// + /// + /// + public static IEnumerable GetAvailableActions(this IEdmModel model, IEdmEntityType entityType) + { + return model.GetAvailableOperations(entityType, false).OfType(); + } - private static string GetName(IEdmStructuredType type) - { - IEdmEntityType entityType = type as IEdmEntityType; - if (entityType != null) - { - return entityType.Name; - } + /// + /// + /// + /// + /// + /// + public static IEnumerable GetAvailableFunctions(this IEdmModel model, IEdmEntityType entityType) + { + return model.GetAvailableOperations(entityType, false).OfType(); + } - return ((IEdmComplexType)type).Name; - } + /// + /// + /// + /// + /// + /// + public static IEnumerable GetAvailableOperationsBoundToCollection(this IEdmModel model, IEdmEntityType entityType) + { + return model.GetAvailableOperations(entityType, true); + } - /// - /// - /// - /// - /// - /// - public static IEnumerable GetAvailableActions(this IEdmModel model, IEdmEntityType entityType) + /// + /// + /// + /// + /// + /// + /// + public static IEnumerable GetAvailableOperations(this IEdmModel model, IEdmEntityType entityType, bool boundToCollection = false) + { + if (model == null) { - return model.GetAvailableOperations(entityType, false).OfType(); + throw new ArgumentNullException(nameof(model)); } - /// - /// - /// - /// - /// - /// - public static IEnumerable GetAvailableFunctions(this IEdmModel model, IEdmEntityType entityType) + if (entityType == null) { - return model.GetAvailableOperations(entityType, false).OfType(); + throw new ArgumentNullException(nameof(entityType)); } - /// - /// - /// - /// - /// - /// - public static IEnumerable GetAvailableOperationsBoundToCollection(this IEdmModel model, IEdmEntityType entityType) + BindableOperationFinder annotation = model.GetAnnotationValue(model); + if (annotation == null) { - return model.GetAvailableOperations(entityType, true); + annotation = new BindableOperationFinder(model); + model.SetAnnotationValue(model, annotation); } - /// - /// - /// - /// - /// - /// - /// - public static IEnumerable GetAvailableOperations(this IEdmModel model, IEdmEntityType entityType, bool boundToCollection = false) + if (boundToCollection) { - if (model == null) - { - throw new ArgumentNullException(nameof(model)); - } - - if (entityType == null) - { - throw new ArgumentNullException(nameof(entityType)); - } - - BindableOperationFinder annotation = model.GetAnnotationValue(model); - if (annotation == null) - { - annotation = new BindableOperationFinder(model); - model.SetAnnotationValue(model, annotation); - } - - if (boundToCollection) - { - return annotation.FindOperationsBoundToCollection(entityType); - } - else - { - return annotation.FindOperations(entityType); - } + return annotation.FindOperationsBoundToCollection(entityType); + } + else + { + return annotation.FindOperations(entityType); } } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/EdmModelLinkBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData/Edm/EdmModelLinkBuilderExtensions.cs index 37895a215..cd127f65f 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EdmModelLinkBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EdmModelLinkBuilderExtensions.cs @@ -11,198 +11,197 @@ using Microsoft.AspNetCore.OData.Formatter; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// Extension methods to set the link builder. +/// +public static class EdmModelLinkBuilderExtensions { /// - /// Extension methods to set the link builder. + /// Sets the ID link builder for the given . /// - public static class EdmModelLinkBuilderExtensions + /// The Edm model. + /// The navigation source. + /// The Id link builder. + public static void HasIdLink(this IEdmModel model, IEdmNavigationSource navigationSource, SelfLinkBuilder idLinkBuilder) { - /// - /// Sets the ID link builder for the given . - /// - /// The Edm model. - /// The navigation source. - /// The Id link builder. - public static void HasIdLink(this IEdmModel model, IEdmNavigationSource navigationSource, SelfLinkBuilder idLinkBuilder) - { - NavigationSourceLinkBuilderAnnotation annotation = model.GetNavigationSourceLinkBuilder(navigationSource); - Contract.Assert(annotation != null); - annotation.IdLinkBuilder = idLinkBuilder; - } + NavigationSourceLinkBuilderAnnotation annotation = model.GetNavigationSourceLinkBuilder(navigationSource); + Contract.Assert(annotation != null); + annotation.IdLinkBuilder = idLinkBuilder; + } - /// - /// Sets the Edit link builder for the given . - /// - /// The Edm model. - /// The navigation source. - /// The Edit link builder. - public static void HasEditLink(this IEdmModel model, IEdmNavigationSource navigationSource, SelfLinkBuilder editLinkBuilder) - { - NavigationSourceLinkBuilderAnnotation annotation = model.GetNavigationSourceLinkBuilder(navigationSource); - Contract.Assert(annotation != null); - annotation.EditLinkBuilder = editLinkBuilder; - } + /// + /// Sets the Edit link builder for the given . + /// + /// The Edm model. + /// The navigation source. + /// The Edit link builder. + public static void HasEditLink(this IEdmModel model, IEdmNavigationSource navigationSource, SelfLinkBuilder editLinkBuilder) + { + NavigationSourceLinkBuilderAnnotation annotation = model.GetNavigationSourceLinkBuilder(navigationSource); + Contract.Assert(annotation != null); + annotation.EditLinkBuilder = editLinkBuilder; + } + + /// + /// Sets the Read link builder for the given . + /// + /// The Edm model. + /// The navigation source. + /// The Read link builder. + public static void HasReadLink(this IEdmModel model, IEdmNavigationSource navigationSource, SelfLinkBuilder readLinkBuilder) + { + NavigationSourceLinkBuilderAnnotation annotation = model.GetNavigationSourceLinkBuilder(navigationSource); + Contract.Assert(annotation != null); + annotation.ReadLinkBuilder = readLinkBuilder; + } + + /// + /// Sets the navigation property link builder for the given and . + /// + /// The Edm model. + /// The navigation source. + /// The navigation property. + /// The navigation property link builder. + public static void HasNavigationPropertyLink(this IEdmModel model, IEdmNavigationSource navigationSource, + IEdmNavigationProperty navigationProperty, NavigationLinkBuilder linkBuilder) + { + NavigationSourceLinkBuilderAnnotation annotation = model.GetNavigationSourceLinkBuilder(navigationSource); + Contract.Assert(annotation != null); + annotation.AddNavigationPropertyLinkBuilder(navigationProperty, linkBuilder); + } - /// - /// Sets the Read link builder for the given . - /// - /// The Edm model. - /// The navigation source. - /// The Read link builder. - public static void HasReadLink(this IEdmModel model, IEdmNavigationSource navigationSource, SelfLinkBuilder readLinkBuilder) + /// + /// Gets the to be used while generating self and navigation + /// links for the given navigation source. + /// + /// The containing the navigation source. + /// The navigation source. + /// The if set for the given the singleton; otherwise, + /// a new that generates URLs that follow OData URL conventions. + /// + public static NavigationSourceLinkBuilderAnnotation GetNavigationSourceLinkBuilder(this IEdmModel model, + IEdmNavigationSource navigationSource) + { + if (model == null) { - NavigationSourceLinkBuilderAnnotation annotation = model.GetNavigationSourceLinkBuilder(navigationSource); - Contract.Assert(annotation != null); - annotation.ReadLinkBuilder = readLinkBuilder; + throw Error.ArgumentNull("model"); } - /// - /// Sets the navigation property link builder for the given and . - /// - /// The Edm model. - /// The navigation source. - /// The navigation property. - /// The navigation property link builder. - public static void HasNavigationPropertyLink(this IEdmModel model, IEdmNavigationSource navigationSource, - IEdmNavigationProperty navigationProperty, NavigationLinkBuilder linkBuilder) + NavigationSourceLinkBuilderAnnotation annotation = model + .GetAnnotationValue(navigationSource); + if (annotation == null) { - NavigationSourceLinkBuilderAnnotation annotation = model.GetNavigationSourceLinkBuilder(navigationSource); - Contract.Assert(annotation != null); - annotation.AddNavigationPropertyLinkBuilder(navigationProperty, linkBuilder); + // construct and set a navigation source link builder that follows OData URL conventions. + annotation = new NavigationSourceLinkBuilderAnnotation(navigationSource, model); + model.SetNavigationSourceLinkBuilder(navigationSource, annotation); } - /// - /// Gets the to be used while generating self and navigation - /// links for the given navigation source. - /// - /// The containing the navigation source. - /// The navigation source. - /// The if set for the given the singleton; otherwise, - /// a new that generates URLs that follow OData URL conventions. - /// - public static NavigationSourceLinkBuilderAnnotation GetNavigationSourceLinkBuilder(this IEdmModel model, - IEdmNavigationSource navigationSource) + return annotation; + } + + /// + /// Sets the to be used while generating self and navigation + /// links for the given navigation source. + /// + /// The containing the navigation source. + /// The navigation source. + /// The to set. + public static void SetNavigationSourceLinkBuilder(this IEdmModel model, IEdmNavigationSource navigationSource, + NavigationSourceLinkBuilderAnnotation navigationSourceLinkBuilder) + { + if (model == null) { - if (model == null) - { - throw Error.ArgumentNull("model"); - } + throw Error.ArgumentNull("model"); + } - NavigationSourceLinkBuilderAnnotation annotation = model - .GetAnnotationValue(navigationSource); - if (annotation == null) - { - // construct and set a navigation source link builder that follows OData URL conventions. - annotation = new NavigationSourceLinkBuilderAnnotation(navigationSource, model); - model.SetNavigationSourceLinkBuilder(navigationSource, annotation); - } + model.SetAnnotationValue(navigationSource, navigationSourceLinkBuilder); + } - return annotation; + /// + /// Gets the to be used while generating operation links for the given action. + /// + /// The containing the operation. + /// The operation for which the link builder is needed. + /// The for the given operation if one is set; otherwise, a new + /// that generates operation links following OData URL conventions. + public static OperationLinkBuilder GetOperationLinkBuilder(this IEdmModel model, IEdmOperation operation) + { + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); } - - /// - /// Sets the to be used while generating self and navigation - /// links for the given navigation source. - /// - /// The containing the navigation source. - /// The navigation source. - /// The to set. - public static void SetNavigationSourceLinkBuilder(this IEdmModel model, IEdmNavigationSource navigationSource, - NavigationSourceLinkBuilderAnnotation navigationSourceLinkBuilder) + if (operation == null) { - if (model == null) - { - throw Error.ArgumentNull("model"); - } - - model.SetAnnotationValue(navigationSource, navigationSourceLinkBuilder); + throw Error.ArgumentNull(nameof(operation)); } - /// - /// Gets the to be used while generating operation links for the given action. - /// - /// The containing the operation. - /// The operation for which the link builder is needed. - /// The for the given operation if one is set; otherwise, a new - /// that generates operation links following OData URL conventions. - public static OperationLinkBuilder GetOperationLinkBuilder(this IEdmModel model, IEdmOperation operation) + OperationLinkBuilder linkBuilder = model.GetAnnotationValue(operation); + if (linkBuilder == null) { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } - if (operation == null) - { - throw Error.ArgumentNull(nameof(operation)); - } + linkBuilder = GetDefaultOperationLinkBuilder(operation); + model.SetOperationLinkBuilder(operation, linkBuilder); + } - OperationLinkBuilder linkBuilder = model.GetAnnotationValue(operation); - if (linkBuilder == null) - { - linkBuilder = GetDefaultOperationLinkBuilder(operation); - model.SetOperationLinkBuilder(operation, linkBuilder); - } + return linkBuilder; + } - return linkBuilder; + /// + /// Sets the to be used for generating the OData operation link for the given operation. + /// + /// The containing the entity set. + /// The operation for which the operation link is to be generated. + /// The to set. + public static void SetOperationLinkBuilder(this IEdmModel model, IEdmOperation operation, OperationLinkBuilder operationLinkBuilder) + { + if (model == null) + { + throw Error.ArgumentNull("model"); } - /// - /// Sets the to be used for generating the OData operation link for the given operation. - /// - /// The containing the entity set. - /// The operation for which the operation link is to be generated. - /// The to set. - public static void SetOperationLinkBuilder(this IEdmModel model, IEdmOperation operation, OperationLinkBuilder operationLinkBuilder) + model.SetAnnotationValue(operation, operationLinkBuilder); + } + + private static OperationLinkBuilder GetDefaultOperationLinkBuilder(IEdmOperation operation) + { + OperationLinkBuilder linkBuilder = null; + if (operation.Parameters != null) { - if (model == null) + if (operation.Parameters.First().Type.IsEntity()) { - throw Error.ArgumentNull("model"); + if (operation is IEdmAction) + { + linkBuilder = new OperationLinkBuilder( + (ResourceContext resourceContext) => + resourceContext.GenerateActionLink(operation), followsConventions: true); + } + else + { + linkBuilder = new OperationLinkBuilder( + (ResourceContext resourceContext) => + resourceContext.GenerateFunctionLink(operation), followsConventions: true); + } } - - model.SetAnnotationValue(operation, operationLinkBuilder); - } - - private static OperationLinkBuilder GetDefaultOperationLinkBuilder(IEdmOperation operation) - { - OperationLinkBuilder linkBuilder = null; - if (operation.Parameters != null) + else if (operation.Parameters.First().Type.IsCollection()) { - if (operation.Parameters.First().Type.IsEntity()) + if (operation is IEdmAction) { - if (operation is IEdmAction) - { - linkBuilder = new OperationLinkBuilder( - (ResourceContext resourceContext) => - resourceContext.GenerateActionLink(operation), followsConventions: true); - } - else - { - linkBuilder = new OperationLinkBuilder( - (ResourceContext resourceContext) => - resourceContext.GenerateFunctionLink(operation), followsConventions: true); - } + linkBuilder = + new OperationLinkBuilder( + (ResourceSetContext resourceSetContext) => + resourceSetContext.GenerateActionLink(operation), followsConventions: true); } - else if (operation.Parameters.First().Type.IsCollection()) + else { - if (operation is IEdmAction) - { - linkBuilder = - new OperationLinkBuilder( - (ResourceSetContext resourceSetContext) => - resourceSetContext.GenerateActionLink(operation), followsConventions: true); - } - else - { - linkBuilder = - new OperationLinkBuilder( - (ResourceSetContext resourceSetContext) => - resourceSetContext.GenerateFunctionLink(operation), followsConventions: true); - } + linkBuilder = + new OperationLinkBuilder( + (ResourceSetContext resourceSetContext) => + resourceSetContext.GenerateFunctionLink(operation), followsConventions: true); } } - - return linkBuilder; } + + return linkBuilder; } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/EdmPrimitiveHelper.cs b/src/Microsoft.AspNetCore.OData/Edm/EdmPrimitiveHelper.cs index 1cb66d8e2..33e4cf031 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EdmPrimitiveHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EdmPrimitiveHelper.cs @@ -13,173 +13,172 @@ using Microsoft.AspNetCore.OData.Common; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +internal static class EdmPrimitiveHelper { - internal static class EdmPrimitiveHelper + public static object ConvertPrimitiveValue(object value, Type type) + { + return ConvertPrimitiveValue(value, type, timeZoneInfo: null); + } + + public static object ConvertPrimitiveValue(object value, Type type, TimeZoneInfo timeZoneInfo) { - public static object ConvertPrimitiveValue(object value, Type type) + Contract.Assert(value != null); + Contract.Assert(type != null); + + // if value is of the same type nothing to do here. + if (value.GetType() == type || value.GetType() == Nullable.GetUnderlyingType(type)) { - return ConvertPrimitiveValue(value, type, timeZoneInfo: null); + return value; } - public static object ConvertPrimitiveValue(object value, Type type, TimeZoneInfo timeZoneInfo) + if (type.IsInstanceOfType(value)) { - Contract.Assert(value != null); - Contract.Assert(type != null); + return value; + } + + string str = value as string; - // if value is of the same type nothing to do here. - if (value.GetType() == type || value.GetType() == Nullable.GetUnderlyingType(type)) + if (type == typeof(char)) + { + if (str == null || str.Length != 1) { - return value; + throw new ValidationException(Error.Format(SRResources.PropertyMustBeStringLengthOne)); } - if (type.IsInstanceOfType(value)) + return str[0]; + } + else if (type == typeof(char?)) + { + if (str == null || str.Length > 1) { - return value; + throw new ValidationException(Error.Format(SRResources.PropertyMustBeStringMaxLengthOne)); } - string str = value as string; - - if (type == typeof(char)) + return str.Length > 0 ? str[0] : (char?)null; + } + else if (type == typeof(char[])) + { + if (str == null) { - if (str == null || str.Length != 1) - { - throw new ValidationException(Error.Format(SRResources.PropertyMustBeStringLengthOne)); - } - - return str[0]; + throw new ValidationException(Error.Format(SRResources.PropertyMustBeString)); } - else if (type == typeof(char?)) - { - if (str == null || str.Length > 1) - { - throw new ValidationException(Error.Format(SRResources.PropertyMustBeStringMaxLengthOne)); - } - return str.Length > 0 ? str[0] : (char?)null; + return str.ToCharArray(); + } + else if (type == typeof(XElement)) + { + if (str == null) + { + throw new ValidationException(Error.Format(SRResources.PropertyMustBeString)); } - else if (type == typeof(char[])) + + return XElement.Parse(str); + } + else + { + type = Nullable.GetUnderlyingType(type) ?? type; + + // Convert.ChangeType invalid cast from 'System.String' to 'System.Guid' + if (type == typeof(Guid)) { if (str == null) { throw new ValidationException(Error.Format(SRResources.PropertyMustBeString)); } - return str.ToCharArray(); + return Guid.Parse(str); } - else if (type == typeof(XElement)) + else if (TypeHelper.IsEnum(type)) { if (str == null) { throw new ValidationException(Error.Format(SRResources.PropertyMustBeString)); } - return XElement.Parse(str); + return Enum.Parse(type, str); } - else + else if (type == typeof(DateTime)) { - type = Nullable.GetUnderlyingType(type) ?? type; - - // Convert.ChangeType invalid cast from 'System.String' to 'System.Guid' - if (type == typeof(Guid)) + if (value is DateTimeOffset) { - if (str == null) - { - throw new ValidationException(Error.Format(SRResources.PropertyMustBeString)); - } - - return Guid.Parse(str); + DateTimeOffset dateTimeOffsetValue = (DateTimeOffset)value; + TimeZoneInfo timeZone = timeZoneInfo ?? TimeZoneInfo.Local; + dateTimeOffsetValue = TimeZoneInfo.ConvertTime(dateTimeOffsetValue, timeZone); + return dateTimeOffsetValue.DateTime; } - else if (TypeHelper.IsEnum(type)) + + if (value is Date) { - if (str == null) - { - throw new ValidationException(Error.Format(SRResources.PropertyMustBeString)); - } + Date dt = (Date)value; + return (DateTime)dt; + } - return Enum.Parse(type, str); + throw new ValidationException(Error.Format(SRResources.PropertyMustBeDateTimeOffsetOrDate)); + } + else if (type == typeof(TimeSpan)) + { + if (value is TimeOfDay) + { + TimeOfDay tod = (TimeOfDay)value; + return (TimeSpan)tod; } - else if (type == typeof(DateTime)) + + throw new ValidationException(Error.Format(SRResources.PropertyMustBeTimeOfDay)); + } + else if (type == typeof(bool)) + { + bool result; + if (str != null && Boolean.TryParse(str, out result)) { - if (value is DateTimeOffset) - { - DateTimeOffset dateTimeOffsetValue = (DateTimeOffset)value; - TimeZoneInfo timeZone = timeZoneInfo ?? TimeZoneInfo.Local; - dateTimeOffsetValue = TimeZoneInfo.ConvertTime(dateTimeOffsetValue, timeZone); - return dateTimeOffsetValue.DateTime; - } - - if (value is Date) - { - Date dt = (Date)value; - return (DateTime)dt; - } - - throw new ValidationException(Error.Format(SRResources.PropertyMustBeDateTimeOffsetOrDate)); + return result; } - else if (type == typeof(TimeSpan)) + + throw new ValidationException(Error.Format(SRResources.PropertyMustBeBoolean)); + } + else if (type == typeof(DateOnly)) + { + if (value is Date dt) { - if (value is TimeOfDay) - { - TimeOfDay tod = (TimeOfDay)value; - return (TimeSpan)tod; - } + return new DateOnly(dt.Year, dt.Month, dt.Day); + } - throw new ValidationException(Error.Format(SRResources.PropertyMustBeTimeOfDay)); + throw new ValidationException(Error.Format(SRResources.PropertyMustBeDateTimeOffsetOrDate)); + } + else if (type == typeof(TimeOnly)) + { + if (value is TimeOfDay tod) + { + return new TimeOnly(tod.Hours, tod.Minutes, tod.Seconds, (int)tod.Milliseconds); } - else if (type == typeof(bool)) + + throw new ValidationException(Error.Format(SRResources.PropertyMustBeTimeOfDay)); + } + else + { + if (TypeHelper.TryGetInstance(type, value, out var result)) { - bool result; - if (str != null && Boolean.TryParse(str, out result)) - { - return result; - } + return result; + } - throw new ValidationException(Error.Format(SRResources.PropertyMustBeBoolean)); + try + { + // Note that we are not casting the return value to nullable as even if we do it + // CLR would un-box it back to T. + return Convert.ChangeType(value, type, CultureInfo.InvariantCulture); } - else if (type == typeof(DateOnly)) + catch (InvalidCastException) { - if (value is Date dt) - { - return new DateOnly(dt.Year, dt.Month, dt.Day); - } - - throw new ValidationException(Error.Format(SRResources.PropertyMustBeDateTimeOffsetOrDate)); + throw new ValidationException(Error.Format(SRResources.PropertyCannotBeConverted, type)); } - else if (type == typeof(TimeOnly)) + catch (FormatException) { - if (value is TimeOfDay tod) - { - return new TimeOnly(tod.Hours, tod.Minutes, tod.Seconds, (int)tod.Milliseconds); - } - - throw new ValidationException(Error.Format(SRResources.PropertyMustBeTimeOfDay)); + throw new ValidationException(Error.Format(SRResources.PropertyUnrecognizedFormat, type)); } - else + catch (OverflowException) { - if (TypeHelper.TryGetInstance(type, value, out var result)) - { - return result; - } - - try - { - // Note that we are not casting the return value to nullable as even if we do it - // CLR would un-box it back to T. - return Convert.ChangeType(value, type, CultureInfo.InvariantCulture); - } - catch (InvalidCastException) - { - throw new ValidationException(Error.Format(SRResources.PropertyCannotBeConverted, type)); - } - catch (FormatException) - { - throw new ValidationException(Error.Format(SRResources.PropertyUnrecognizedFormat, type)); - } - catch (OverflowException) - { - throw new ValidationException(Error.Format(SRResources.PropertyTypeOverflow, type)); - } + throw new ValidationException(Error.Format(SRResources.PropertyTypeOverflow, type)); } } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/EdmUntypedHelpers.cs b/src/Microsoft.AspNetCore.OData/Edm/EdmUntypedHelpers.cs index 03039f2d7..7299e88d6 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EdmUntypedHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EdmUntypedHelpers.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -7,12 +7,11 @@ using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +internal class EdmUntypedHelpers { - internal class EdmUntypedHelpers - { - public readonly static EdmCollectionTypeReference NullableUntypedCollectionReference - = new EdmCollectionTypeReference( - new EdmCollectionType(EdmUntypedStructuredTypeReference.NullableTypeReference)); - } + public readonly static EdmCollectionTypeReference NullableUntypedCollectionReference + = new EdmCollectionTypeReference( + new EdmCollectionType(EdmUntypedStructuredTypeReference.NullableTypeReference)); } diff --git a/src/Microsoft.AspNetCore.OData/Edm/EntitySelfLinks.cs b/src/Microsoft.AspNetCore.OData/Edm/EntitySelfLinks.cs index de270180f..0b670b9eb 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EntitySelfLinks.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EntitySelfLinks.cs @@ -7,26 +7,25 @@ using System; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// EntitySelfLinks contains the Id, Edit and Read links for an entity. +/// +public class EntitySelfLinks { /// - /// EntitySelfLinks contains the Id, Edit and Read links for an entity. + /// A string that uniquely identifies the resource. /// - public class EntitySelfLinks - { - /// - /// A string that uniquely identifies the resource. - /// - public Uri IdLink { get; set; } + public Uri IdLink { get; set; } - /// - /// A URL that can be used to edit a copy of the resource. - /// - public Uri EditLink { get; set; } + /// + /// A URL that can be used to edit a copy of the resource. + /// + public Uri EditLink { get; set; } - /// - /// A URL that can be used to retrieve a copy of the resource. - /// - public Uri ReadLink { get; set; } - } + /// + /// A URL that can be used to retrieve a copy of the resource. + /// + public Uri ReadLink { get; set; } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/ExpandModelPath.cs b/src/Microsoft.AspNetCore.OData/Edm/ExpandModelPath.cs index f4c3ff669..2dbc3fbc5 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/ExpandModelPath.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/ExpandModelPath.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -9,100 +9,99 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// The navigation property path is a model path with the following restriction: +/// A non-null path MUST resolve to a model element whose type is an entity type, or a collection of entity types, e.g. a navigation property. +/// +/// If a path segment is a qualified name, it represents a type cast, and the segment MUST be the name of a type in scope. +/// If a path segment is a simple identifier, it MUST be the name of a child model element of the model element identified by the preceding path part, or a structural or navigation property of the instance identified by the preceding path part. +/// A model path MAY contain any number of segments representing collection-valued structural or navigation properties. +/// +internal class ExpandModelPath : List { + private string _navigationPath; + /// - /// The navigation property path is a model path with the following restriction: - /// A non-null path MUST resolve to a model element whose type is an entity type, or a collection of entity types, e.g. a navigation property. - /// - /// If a path segment is a qualified name, it represents a type cast, and the segment MUST be the name of a type in scope. - /// If a path segment is a simple identifier, it MUST be the name of a child model element of the model element identified by the preceding path part, or a structural or navigation property of the instance identified by the preceding path part. - /// A model path MAY contain any number of segments representing collection-valued structural or navigation properties. + /// Initializes a new instance of the class. /// - internal class ExpandModelPath : List + /// The segment nodes. + public ExpandModelPath(IEnumerable nodes) + : base(nodes) { - private string _navigationPath; - - /// - /// Initializes a new instance of the class. - /// - /// The segment nodes. - public ExpandModelPath(IEnumerable nodes) - : base(nodes) - { - ValidateAndCalculateElementPath(); - } + ValidateAndCalculateElementPath(); + } - /// - /// Gets the navigation property resolved from this path. - /// - public IEdmNavigationProperty Navigation { get; private set; } + /// + /// Gets the navigation property resolved from this path. + /// + public IEdmNavigationProperty Navigation { get; private set; } - /// - /// Gets the navigation property path, it doesn't include the navigation property. - /// - public string NavigationPropertyPath => _navigationPath; + /// + /// Gets the navigation property path, it doesn't include the navigation property. + /// + public string NavigationPropertyPath => _navigationPath; - /// - /// Gets the whole expand path. - /// - public string ExpandPath => string.IsNullOrEmpty(_navigationPath) ? Navigation.Name : $"{_navigationPath}/{Navigation.Name}"; + /// + /// Gets the whole expand path. + /// + public string ExpandPath => string.IsNullOrEmpty(_navigationPath) ? Navigation.Name : $"{_navigationPath}/{Navigation.Name}"; - private void ValidateAndCalculateElementPath() + private void ValidateAndCalculateElementPath() + { + int index = 0; + int count = Count; + bool foundNavProp = false; + IList identifiers = new List(); + foreach (IEdmElement element in this) { - int index = 0; - int count = Count; - bool foundNavProp = false; - IList identifiers = new List(); - foreach (IEdmElement element in this) + if (element is IEdmStructuredType structuredType) { - if (element is IEdmStructuredType structuredType) + if (index == count - 1) { - if (index == count - 1) - { - throw new ODataException(Error.Format(SRResources.InvalidLastSegmentInSelectExpandPath, element.GetType().Name)); - } - - identifiers.Add(structuredType.FullTypeName()); + throw new ODataException(Error.Format(SRResources.InvalidLastSegmentInSelectExpandPath, element.GetType().Name)); } - else if (element is IEdmStructuralProperty structuralProperty) - { - if (index == count - 1) - { - throw new ODataException(Error.Format(SRResources.InvalidLastSegmentInSelectExpandPath, element.GetType().Name)); - } - identifiers.Add(structuralProperty.Name); - } - else if (element is IEdmNavigationProperty navigationProperty) + identifiers.Add(structuredType.FullTypeName()); + } + else if (element is IEdmStructuralProperty structuralProperty) + { + if (index == count - 1) { - if (index < count - 1 || foundNavProp) - { - throw new ODataException(Error.Format(SRResources.InvalidSegmentInSelectExpandPath, element.GetType().Name)); - } - - foundNavProp = true; - Navigation = navigationProperty; - - // don't add the navigation property into identifiers - // Because the navigation property path (without the last navigation property name) is used to retrieve the navigation - // property binding. See "ODL api, FindNavigationTarget". - // identifiers.Add(navigationProperty.Name); + throw new ODataException(Error.Format(SRResources.InvalidLastSegmentInSelectExpandPath, element.GetType().Name)); } - else + + identifiers.Add(structuralProperty.Name); + } + else if (element is IEdmNavigationProperty navigationProperty) + { + if (index < count - 1 || foundNavProp) { throw new ODataException(Error.Format(SRResources.InvalidSegmentInSelectExpandPath, element.GetType().Name)); } - index++; - } + foundNavProp = true; + Navigation = navigationProperty; - if (!foundNavProp) + // don't add the navigation property into identifiers + // Because the navigation property path (without the last navigation property name) is used to retrieve the navigation + // property binding. See "ODL api, FindNavigationTarget". + // identifiers.Add(navigationProperty.Name); + } + else { - throw new ODataException(Error.Format(SRResources.ShouldHaveNavigationPropertyInNavigationExpandPath)); + throw new ODataException(Error.Format(SRResources.InvalidSegmentInSelectExpandPath, element.GetType().Name)); } - _navigationPath = string.Join("/", identifiers); + index++; } + + if (!foundNavProp) + { + throw new ODataException(Error.Format(SRResources.ShouldHaveNavigationPropertyInNavigationExpandPath)); + } + + _navigationPath = string.Join("/", identifiers); } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/IODataTypeMapper.cs b/src/Microsoft.AspNetCore.OData/Edm/IODataTypeMapper.cs index 3311c5632..5cde21891 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/IODataTypeMapper.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/IODataTypeMapper.cs @@ -9,44 +9,43 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// Provides the mapping between CLR type and Edm type. +/// +public interface IODataTypeMapper { /// - /// Provides the mapping between CLR type and Edm type. + /// Gets the corresponding Edm primitive type for a given . /// - public interface IODataTypeMapper - { - /// - /// Gets the corresponding Edm primitive type for a given . - /// - /// The given CLR type. - /// Null or the Edm primitive type. - IEdmPrimitiveTypeReference GetEdmPrimitiveType(Type clrType); + /// The given CLR type. + /// Null or the Edm primitive type. + IEdmPrimitiveTypeReference GetEdmPrimitiveType(Type clrType); - /// - /// Gets the corresponding for a given Edm primitive type . - /// - /// The given Edm primitive type. - /// The nullable or not. - /// Null or the CLR type. - Type GetClrPrimitiveType(IEdmPrimitiveType primitiveType, bool nullable); + /// + /// Gets the corresponding for a given Edm primitive type . + /// + /// The given Edm primitive type. + /// The nullable or not. + /// Null or the CLR type. + Type GetClrPrimitiveType(IEdmPrimitiveType primitiveType, bool nullable); - /// - /// Gets the corresponding Edm type for the given CLR type . - /// - /// The given Edm model. - /// The given CLR type. - /// Null or the corresponding Edm type reference. - IEdmTypeReference GetEdmTypeReference(IEdmModel edmModel, Type clrType); + /// + /// Gets the corresponding Edm type for the given CLR type . + /// + /// The given Edm model. + /// The given CLR type. + /// Null or the corresponding Edm type reference. + IEdmTypeReference GetEdmTypeReference(IEdmModel edmModel, Type clrType); - /// - /// Gets the corresponding for a given Edm type . - /// - /// The given Edm model. - /// The given Edm type. - /// The nullable or not. - /// The assembly resolver. - /// Null or the CLR type. - Type GetClrType(IEdmModel edmModel, IEdmType edmType, bool nullable, IAssemblyResolver assembliesResolver); - } + /// + /// Gets the corresponding for a given Edm type . + /// + /// The given Edm model. + /// The given Edm type. + /// The nullable or not. + /// The assembly resolver. + /// Null or the CLR type. + Type GetClrType(IEdmModel edmModel, IEdmType edmType, bool nullable, IAssemblyResolver assembliesResolver); } diff --git a/src/Microsoft.AspNetCore.OData/Edm/IODataTypeMapperExtensions.cs b/src/Microsoft.AspNetCore.OData/Edm/IODataTypeMapperExtensions.cs index e91ae3413..acdcc5b43 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/IODataTypeMapperExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/IODataTypeMapperExtensions.cs @@ -10,84 +10,83 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// Extension methods for . +/// +public static class IODataTypeMapperExtensions { /// - /// Extension methods for . + /// Gets the corresponding for a given Edm primitive type . /// - public static class IODataTypeMapperExtensions + /// The type mapper. + /// The Edm primitive type reference. + /// Null or the CLR type. + public static Type GetPrimitiveType(this IODataTypeMapper mapper, IEdmPrimitiveTypeReference primitiveType) { - /// - /// Gets the corresponding for a given Edm primitive type . - /// - /// The type mapper. - /// The Edm primitive type reference. - /// Null or the CLR type. - public static Type GetPrimitiveType(this IODataTypeMapper mapper, IEdmPrimitiveTypeReference primitiveType) + if (mapper == null) { - if (mapper == null) - { - throw Error.ArgumentNull(nameof(mapper)); - } - - if (primitiveType == null) - { - throw Error.ArgumentNull(nameof(primitiveType)); - } - - return mapper.GetClrPrimitiveType(primitiveType.PrimitiveDefinition(), primitiveType.IsNullable); + throw Error.ArgumentNull(nameof(mapper)); } - /// - /// Gets the corresponding Edm type for the given CLR type . - /// - /// The type mapper. - /// The given Edm model. - /// The given CLR type. - /// Null or the corresponding Edm type. - public static IEdmType GetEdmType(this IODataTypeMapper mapper, IEdmModel edmModel, Type clrType) + if (primitiveType == null) { - if (mapper == null) - { - throw Error.ArgumentNull(nameof(mapper)); - } - - return mapper.GetEdmTypeReference(edmModel, clrType)?.Definition; + throw Error.ArgumentNull(nameof(primitiveType)); } - /// - /// Gets the corresponding for a given Edm type . - /// - /// The type mapper. - /// The Edm model. - /// The Edm type reference. - /// Null or the CLR type. - public static Type GetClrType(this IODataTypeMapper mapper, IEdmModel edmModel, IEdmTypeReference edmType) + return mapper.GetClrPrimitiveType(primitiveType.PrimitiveDefinition(), primitiveType.IsNullable); + } + + /// + /// Gets the corresponding Edm type for the given CLR type . + /// + /// The type mapper. + /// The given Edm model. + /// The given CLR type. + /// Null or the corresponding Edm type. + public static IEdmType GetEdmType(this IODataTypeMapper mapper, IEdmModel edmModel, Type clrType) + { + if (mapper == null) { - return mapper.GetClrType(edmModel, edmType, AssemblyResolverHelper.Default); + throw Error.ArgumentNull(nameof(mapper)); } - /// - /// Gets the corresponding for a given Edm type . - /// - /// The type mapper. - /// The Edm model. - /// The Edm type. - /// The assembly resolver. - /// Null or the CLR type. - public static Type GetClrType(this IODataTypeMapper mapper, IEdmModel edmModel, IEdmTypeReference edmType, IAssemblyResolver assembliesResolver) - { - if (mapper == null) - { - throw Error.ArgumentNull(nameof(mapper)); - } + return mapper.GetEdmTypeReference(edmModel, clrType)?.Definition; + } - if (edmType == null) - { - throw Error.ArgumentNull(nameof(edmType)); - } + /// + /// Gets the corresponding for a given Edm type . + /// + /// The type mapper. + /// The Edm model. + /// The Edm type reference. + /// Null or the CLR type. + public static Type GetClrType(this IODataTypeMapper mapper, IEdmModel edmModel, IEdmTypeReference edmType) + { + return mapper.GetClrType(edmModel, edmType, AssemblyResolverHelper.Default); + } - return mapper.GetClrType(edmModel, edmType.Definition, edmType.IsNullable, assembliesResolver); + /// + /// Gets the corresponding for a given Edm type . + /// + /// The type mapper. + /// The Edm model. + /// The Edm type. + /// The assembly resolver. + /// Null or the CLR type. + public static Type GetClrType(this IODataTypeMapper mapper, IEdmModel edmModel, IEdmTypeReference edmType, IAssemblyResolver assembliesResolver) + { + if (mapper == null) + { + throw Error.ArgumentNull(nameof(mapper)); } + + if (edmType == null) + { + throw Error.ArgumentNull(nameof(edmType)); + } + + return mapper.GetClrType(edmModel, edmType.Definition, edmType.IsNullable, assembliesResolver); } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/ModelNameAnnotation.cs b/src/Microsoft.AspNetCore.OData/Edm/ModelNameAnnotation.cs index a0a0dfaa6..5a5c2fd5b 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/ModelNameAnnotation.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/ModelNameAnnotation.cs @@ -7,25 +7,24 @@ using System; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// The Edm model name annotation. +/// +public class ModelNameAnnotation { /// - /// The Edm model name annotation. + /// Initializes a new instance of the class. /// - public class ModelNameAnnotation + /// The model name. + public ModelNameAnnotation(string name) { - /// - /// Initializes a new instance of the class. - /// - /// The model name. - public ModelNameAnnotation(string name) - { - ModelName = name ?? throw Error.ArgumentNull(nameof(name)); - } - - /// - /// Gets the backing CLR type for the EDM type. - /// - public string ModelName { get; } + ModelName = name ?? throw Error.ArgumentNull(nameof(name)); } + + /// + /// Gets the backing CLR type for the EDM type. + /// + public string ModelName { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/NavigationLinkBuilder.cs b/src/Microsoft.AspNetCore.OData/Edm/NavigationLinkBuilder.cs index 816ace5f5..5491f58b2 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/NavigationLinkBuilder.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/NavigationLinkBuilder.cs @@ -9,32 +9,31 @@ using Microsoft.AspNetCore.OData.Formatter; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// Encapsulates a navigation link factory and whether the link factory follows conventions or not. +/// +public class NavigationLinkBuilder { /// - /// Encapsulates a navigation link factory and whether the link factory follows conventions or not. + /// Initializes a new instance of the class. /// - public class NavigationLinkBuilder + /// The navigation link factory for creating navigation links. + /// Represents whether this factory follows OData conventions or not. + public NavigationLinkBuilder(Func navigationLinkFactory, bool followsConventions) { - /// - /// Initializes a new instance of the class. - /// - /// The navigation link factory for creating navigation links. - /// Represents whether this factory follows OData conventions or not. - public NavigationLinkBuilder(Func navigationLinkFactory, bool followsConventions) - { - Factory = navigationLinkFactory ?? throw Error.ArgumentNull(nameof(navigationLinkFactory)); - FollowsConventions = followsConventions; - } + Factory = navigationLinkFactory ?? throw Error.ArgumentNull(nameof(navigationLinkFactory)); + FollowsConventions = followsConventions; + } - /// - /// Gets the navigation link factory for creating navigation links. - /// - public Func Factory { get; } + /// + /// Gets the navigation link factory for creating navigation links. + /// + public Func Factory { get; } - /// - /// Gets a value representing whether this factory follows OData conventions or not. - /// - public bool FollowsConventions { get; } - } + /// + /// Gets a value representing whether this factory follows OData conventions or not. + /// + public bool FollowsConventions { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/NavigationSourceLinkBuilderAnnotation.cs b/src/Microsoft.AspNetCore.OData/Edm/NavigationSourceLinkBuilderAnnotation.cs index c33424847..6a16194c4 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/NavigationSourceLinkBuilderAnnotation.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/NavigationSourceLinkBuilderAnnotation.cs @@ -11,238 +11,237 @@ using Microsoft.AspNetCore.OData.Formatter; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// is a class used to annotate +/// an inside an +/// with information about how to build links related to that navigation source. +/// +public class NavigationSourceLinkBuilderAnnotation { + private readonly Dictionary _navigationPropertyLinkBuilderLookup = new Dictionary(); + /// - /// is a class used to annotate - /// an inside an - /// with information about how to build links related to that navigation source. + /// Initializes a new instance of the class. /// - public class NavigationSourceLinkBuilderAnnotation + /// The default constructor is intended for use by unit testing only. + public NavigationSourceLinkBuilderAnnotation() { - private readonly Dictionary _navigationPropertyLinkBuilderLookup = new Dictionary(); + } - /// - /// Initializes a new instance of the class. - /// - /// The default constructor is intended for use by unit testing only. - public NavigationSourceLinkBuilderAnnotation() + /// + /// Initializes a new instance of the class. + /// + /// The navigation source for which the link builder is being constructed. + /// The EDM model that this navigation source belongs to. + /// This constructor creates a link builder that generates URL's that follow OData conventions for the given navigation source. + public NavigationSourceLinkBuilderAnnotation(IEdmNavigationSource navigationSource, IEdmModel model) + { + if (navigationSource == null) { + throw Error.ArgumentNull(nameof(navigationSource)); } - /// - /// Initializes a new instance of the class. - /// - /// The navigation source for which the link builder is being constructed. - /// The EDM model that this navigation source belongs to. - /// This constructor creates a link builder that generates URL's that follow OData conventions for the given navigation source. - public NavigationSourceLinkBuilderAnnotation(IEdmNavigationSource navigationSource, IEdmModel model) + if (model == null) { - if (navigationSource == null) - { - throw Error.ArgumentNull(nameof(navigationSource)); - } + throw Error.ArgumentNull(nameof(model)); + } - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + IEdmEntityType elementType = navigationSource.EntityType; + IEnumerable derivedTypes = model.FindAllDerivedTypes(elementType).Cast(); - IEdmEntityType elementType = navigationSource.EntityType; - IEnumerable derivedTypes = model.FindAllDerivedTypes(elementType).Cast(); + // Add navigation link builders for all navigation properties of entity. + foreach (IEdmNavigationProperty navigationProperty in elementType.NavigationProperties()) + { + Func navigationLinkFactory = + (resourceContext, navProperty) => resourceContext.GenerateNavigationPropertyLink(navProperty, includeCast: false); + AddNavigationPropertyLinkBuilder(navigationProperty, new NavigationLinkBuilder(navigationLinkFactory, followsConventions: true)); + } - // Add navigation link builders for all navigation properties of entity. - foreach (IEdmNavigationProperty navigationProperty in elementType.NavigationProperties()) + // Add navigation link builders for all navigation properties in derived types. + foreach (IEdmEntityType derivedEntityType in derivedTypes) + { + foreach (IEdmNavigationProperty navigationProperty in derivedEntityType.DeclaredNavigationProperties()) { Func navigationLinkFactory = - (resourceContext, navProperty) => resourceContext.GenerateNavigationPropertyLink(navProperty, includeCast: false); + (resourceContext, navProperty) => resourceContext.GenerateNavigationPropertyLink(navProperty, includeCast: true); AddNavigationPropertyLinkBuilder(navigationProperty, new NavigationLinkBuilder(navigationLinkFactory, followsConventions: true)); } + } - // Add navigation link builders for all navigation properties in derived types. - foreach (IEdmEntityType derivedEntityType in derivedTypes) - { - foreach (IEdmNavigationProperty navigationProperty in derivedEntityType.DeclaredNavigationProperties()) - { - Func navigationLinkFactory = - (resourceContext, navProperty) => resourceContext.GenerateNavigationPropertyLink(navProperty, includeCast: true); - AddNavigationPropertyLinkBuilder(navigationProperty, new NavigationLinkBuilder(navigationLinkFactory, followsConventions: true)); - } - } + Func selfLinkFactory = + (resourceContext) => resourceContext.GenerateSelfLink(includeCast: false); + IdLinkBuilder = new SelfLinkBuilder(selfLinkFactory, followsConventions: true); + } - Func selfLinkFactory = - (resourceContext) => resourceContext.GenerateSelfLink(includeCast: false); - IdLinkBuilder = new SelfLinkBuilder(selfLinkFactory, followsConventions: true); - } + /// + /// Gets/sets the ID link builder. + /// + public SelfLinkBuilder IdLinkBuilder { get; set; } - /// - /// Gets/sets the ID link builder. - /// - public SelfLinkBuilder IdLinkBuilder { get; set; } + /// + /// Gets/sets the read link builder. + /// + public SelfLinkBuilder ReadLinkBuilder { get; set; } - /// - /// Gets/sets the read link builder. - /// - public SelfLinkBuilder ReadLinkBuilder { get; set; } + /// + /// Gets/sets the edit link builder. + /// + public SelfLinkBuilder EditLinkBuilder { get; set; } - /// - /// Gets/sets the edit link builder. - /// - public SelfLinkBuilder EditLinkBuilder { get; set; } + /// + /// Register a link builder for a that navigates from Entities in this navigation source. + /// + public void AddNavigationPropertyLinkBuilder(IEdmNavigationProperty navigationProperty, NavigationLinkBuilder linkBuilder) + { + _navigationPropertyLinkBuilderLookup[navigationProperty] = linkBuilder; + } + + /// + /// Constructs the for a particular and . + /// + public virtual EntitySelfLinks BuildEntitySelfLinks(ResourceContext instanceContext, ODataMetadataLevel metadataLevel) + { + EntitySelfLinks selfLinks = new EntitySelfLinks(); + selfLinks.IdLink = BuildIdLink(instanceContext, metadataLevel); + selfLinks.EditLink = BuildEditLink(instanceContext, metadataLevel, selfLinks.IdLink); + selfLinks.ReadLink = BuildReadLink(instanceContext, metadataLevel, selfLinks.EditLink); + return selfLinks; + } - /// - /// Register a link builder for a that navigates from Entities in this navigation source. - /// - public void AddNavigationPropertyLinkBuilder(IEdmNavigationProperty navigationProperty, NavigationLinkBuilder linkBuilder) + /// + /// Constructs the IdLink for a particular and . + /// + public virtual Uri BuildIdLink(ResourceContext instanceContext, ODataMetadataLevel metadataLevel) + { + if (instanceContext == null) { - _navigationPropertyLinkBuilderLookup[navigationProperty] = linkBuilder; + throw Error.ArgumentNull(nameof(instanceContext)); } - /// - /// Constructs the for a particular and . - /// - public virtual EntitySelfLinks BuildEntitySelfLinks(ResourceContext instanceContext, ODataMetadataLevel metadataLevel) + if (IdLinkBuilder != null && + (metadataLevel == ODataMetadataLevel.Full || + (metadataLevel == ODataMetadataLevel.Minimal && !IdLinkBuilder.FollowsConventions))) { - EntitySelfLinks selfLinks = new EntitySelfLinks(); - selfLinks.IdLink = BuildIdLink(instanceContext, metadataLevel); - selfLinks.EditLink = BuildEditLink(instanceContext, metadataLevel, selfLinks.IdLink); - selfLinks.ReadLink = BuildReadLink(instanceContext, metadataLevel, selfLinks.EditLink); - return selfLinks; + return IdLinkBuilder.Factory(instanceContext); } - /// - /// Constructs the IdLink for a particular and . - /// - public virtual Uri BuildIdLink(ResourceContext instanceContext, ODataMetadataLevel metadataLevel) - { - if (instanceContext == null) - { - throw Error.ArgumentNull(nameof(instanceContext)); - } + // Return null to let ODL decide when and how to build the id link. + return null; + } - if (IdLinkBuilder != null && - (metadataLevel == ODataMetadataLevel.Full || - (metadataLevel == ODataMetadataLevel.Minimal && !IdLinkBuilder.FollowsConventions))) - { - return IdLinkBuilder.Factory(instanceContext); - } + // Build an id link unconditionally, it doesn't depend on metadata level but does require a non-null link builder. + internal Uri BuildIdLink(ResourceContext instanceContext) + { + return BuildIdLink(instanceContext, ODataMetadataLevel.Full); + } - // Return null to let ODL decide when and how to build the id link. - return null; + /// + /// Constructs the EditLink URL for a particular and . + /// + public virtual Uri BuildEditLink(ResourceContext instanceContext, ODataMetadataLevel metadataLevel, Uri idLink) + { + if (instanceContext == null) + { + throw Error.ArgumentNull(nameof(instanceContext)); } - // Build an id link unconditionally, it doesn't depend on metadata level but does require a non-null link builder. - internal Uri BuildIdLink(ResourceContext instanceContext) + if (EditLinkBuilder != null && + (metadataLevel == ODataMetadataLevel.Full || + (metadataLevel == ODataMetadataLevel.Minimal && !EditLinkBuilder.FollowsConventions))) { - return BuildIdLink(instanceContext, ODataMetadataLevel.Full); + // edit link is the not the same as id link. Generate if the client asked for it (full metadata modes) or + // if the client cannot infer it (not follow conventions). + return EditLinkBuilder.Factory(instanceContext); } - /// - /// Constructs the EditLink URL for a particular and . - /// - public virtual Uri BuildEditLink(ResourceContext instanceContext, ODataMetadataLevel metadataLevel, Uri idLink) - { - if (instanceContext == null) - { - throw Error.ArgumentNull(nameof(instanceContext)); - } + // Return null to let ODL decide when and how to build the edit link. + return null; + } - if (EditLinkBuilder != null && - (metadataLevel == ODataMetadataLevel.Full || - (metadataLevel == ODataMetadataLevel.Minimal && !EditLinkBuilder.FollowsConventions))) - { - // edit link is the not the same as id link. Generate if the client asked for it (full metadata modes) or - // if the client cannot infer it (not follow conventions). - return EditLinkBuilder.Factory(instanceContext); - } + // Build an edit link unconditionally, it doesn't depend on metadata level but does require a non-null link builder. + internal Uri BuildEditLink(ResourceContext instanceContext) + { + return BuildEditLink(instanceContext, ODataMetadataLevel.Full, null); + } - // Return null to let ODL decide when and how to build the edit link. - return null; + /// + /// Constructs a ReadLink URL for a particular and . + /// + public virtual Uri BuildReadLink(ResourceContext instanceContext, ODataMetadataLevel metadataLevel, Uri editLink) + { + if (instanceContext == null) + { + throw Error.ArgumentNull(nameof(instanceContext)); } - // Build an edit link unconditionally, it doesn't depend on metadata level but does require a non-null link builder. - internal Uri BuildEditLink(ResourceContext instanceContext) + if (ReadLinkBuilder != null && + (metadataLevel == ODataMetadataLevel.Full || + (metadataLevel == ODataMetadataLevel.Minimal && !ReadLinkBuilder.FollowsConventions))) { - return BuildEditLink(instanceContext, ODataMetadataLevel.Full, null); + // read link is not the same as edit link. Generate if the client asked for it (full metadata modes) or + // if the client cannot infer it (not follow conventions). + return ReadLinkBuilder.Factory(instanceContext); } - /// - /// Constructs a ReadLink URL for a particular and . - /// - public virtual Uri BuildReadLink(ResourceContext instanceContext, ODataMetadataLevel metadataLevel, Uri editLink) - { - if (instanceContext == null) - { - throw Error.ArgumentNull(nameof(instanceContext)); - } + // Return null to let ODL decide when and how to build the read link. + return null; + } - if (ReadLinkBuilder != null && - (metadataLevel == ODataMetadataLevel.Full || - (metadataLevel == ODataMetadataLevel.Minimal && !ReadLinkBuilder.FollowsConventions))) - { - // read link is not the same as edit link. Generate if the client asked for it (full metadata modes) or - // if the client cannot infer it (not follow conventions). - return ReadLinkBuilder.Factory(instanceContext); - } + // Build a read link unconditionally, it doesn't depend on metadata level but does require a non-null link builder. + internal Uri BuildReadLink(ResourceContext instanceContext) + { + return BuildReadLink(instanceContext, ODataMetadataLevel.Full, null); + } - // Return null to let ODL decide when and how to build the read link. - return null; + /// + /// Constructs a NavigationLink for a particular , and . + /// + public virtual Uri BuildNavigationLink(ResourceContext instanceContext, IEdmNavigationProperty navigationProperty, ODataMetadataLevel metadataLevel) + { + if (instanceContext == null) + { + throw Error.ArgumentNull(nameof(instanceContext)); } - // Build a read link unconditionally, it doesn't depend on metadata level but does require a non-null link builder. - internal Uri BuildReadLink(ResourceContext instanceContext) + if (navigationProperty == null) { - return BuildReadLink(instanceContext, ODataMetadataLevel.Full, null); + throw Error.ArgumentNull(nameof(navigationProperty)); } - /// - /// Constructs a NavigationLink for a particular , and . - /// - public virtual Uri BuildNavigationLink(ResourceContext instanceContext, IEdmNavigationProperty navigationProperty, ODataMetadataLevel metadataLevel) + NavigationLinkBuilder navigationLinkBuilder; + if (_navigationPropertyLinkBuilderLookup.TryGetValue(navigationProperty, out navigationLinkBuilder) + && !navigationLinkBuilder.FollowsConventions + && (metadataLevel == ODataMetadataLevel.Minimal || metadataLevel == ODataMetadataLevel.Full)) { - if (instanceContext == null) - { - throw Error.ArgumentNull(nameof(instanceContext)); - } - - if (navigationProperty == null) - { - throw Error.ArgumentNull(nameof(navigationProperty)); - } + return navigationLinkBuilder.Factory(instanceContext, navigationProperty); + } - NavigationLinkBuilder navigationLinkBuilder; - if (_navigationPropertyLinkBuilderLookup.TryGetValue(navigationProperty, out navigationLinkBuilder) - && !navigationLinkBuilder.FollowsConventions - && (metadataLevel == ODataMetadataLevel.Minimal || metadataLevel == ODataMetadataLevel.Full)) - { - return navigationLinkBuilder.Factory(instanceContext, navigationProperty); - } + // Return null to let ODL decide when and how to build the navigation link. + return null; + } - // Return null to let ODL decide when and how to build the navigation link. - return null; + // Build a navigation link unconditionally, it doesn't depend on metadata level but does require a non-null link builder. + internal Uri BuildNavigationLink(ResourceContext instanceContext, IEdmNavigationProperty navigationProperty) + { + if (instanceContext == null) + { + throw Error.ArgumentNull(nameof(instanceContext)); } - // Build a navigation link unconditionally, it doesn't depend on metadata level but does require a non-null link builder. - internal Uri BuildNavigationLink(ResourceContext instanceContext, IEdmNavigationProperty navigationProperty) + if (navigationProperty == null) { - if (instanceContext == null) - { - throw Error.ArgumentNull(nameof(instanceContext)); - } - - if (navigationProperty == null) - { - throw Error.ArgumentNull(nameof(navigationProperty)); - } - - NavigationLinkBuilder navigationLinkBuilder; - if (_navigationPropertyLinkBuilderLookup.TryGetValue(navigationProperty, out navigationLinkBuilder)) - { - return navigationLinkBuilder.Factory(instanceContext, navigationProperty); - } + throw Error.ArgumentNull(nameof(navigationProperty)); + } - // Return null to let ODL decide when and how to build the navigation link. - return null; + NavigationLinkBuilder navigationLinkBuilder; + if (_navigationPropertyLinkBuilderLookup.TryGetValue(navigationProperty, out navigationLinkBuilder)) + { + return navigationLinkBuilder.Factory(instanceContext, navigationProperty); } + + // Return null to let ODL decide when and how to build the navigation link. + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/ODataNullValueExtensions.cs b/src/Microsoft.AspNetCore.OData/Edm/ODataNullValueExtensions.cs index e27ae74e0..d9d100026 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/ODataNullValueExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/ODataNullValueExtensions.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -7,10 +7,9 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +internal class ODataNullValueExtensions { - internal class ODataNullValueExtensions - { - public static readonly ODataNullValue NullValue = new ODataNullValue(); - } + public static readonly ODataNullValue NullValue = new ODataNullValue(); } diff --git a/src/Microsoft.AspNetCore.OData/Edm/OperationHelper.cs b/src/Microsoft.AspNetCore.OData/Edm/OperationHelper.cs index d12d629c3..8e55e62ff 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/OperationHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/OperationHelper.cs @@ -12,188 +12,187 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// Helpers for Edm operation. +/// +internal static class OperationHelper { /// - /// Helpers for Edm operation. + /// Verify and build the function parameters /// - internal static class OperationHelper + /// The Edm function. + /// The input parameter template mapping. + /// The build function parameter mapping. + public static IDictionary VerifyAndBuildParameterMappings(this IEdmFunction function, IDictionary parameters) { - /// - /// Verify and build the function parameters - /// - /// The Edm function. - /// The input parameter template mapping. - /// The build function parameter mapping. - public static IDictionary VerifyAndBuildParameterMappings(this IEdmFunction function, IDictionary parameters) + if (function == null) + { + throw Error.ArgumentNull(nameof(function)); + } + + if (parameters == null) { - if (function == null) + throw Error.ArgumentNull(nameof(parameters)); + } + + Dictionary parameterMappings = new Dictionary(); + + int skip = function.IsBound ? 1 : 0; + ISet funcParameters = new HashSet(); + foreach (var parameter in function.Parameters.Skip(skip)) + { + funcParameters.Add(parameter.Name); + + IEdmOptionalParameter optionalParameter = parameter as IEdmOptionalParameter; + if (optionalParameter != null) { - throw Error.ArgumentNull(nameof(function)); + // skip verification for optional parameter + continue; } - if (parameters == null) + // for required parameter, it should be in the parameter template mapping. + if (!parameters.ContainsKey(parameter.Name)) { - throw Error.ArgumentNull(nameof(parameters)); + throw new ODataException(Error.Format(SRResources.MissingRequiredParameterInOperation, parameter.Name, function.FullName())); } + } - Dictionary parameterMappings = new Dictionary(); - - int skip = function.IsBound ? 1 : 0; - ISet funcParameters = new HashSet(); - foreach (var parameter in function.Parameters.Skip(skip)) + foreach (var parameter in parameters) + { + if (!funcParameters.Contains(parameter.Key)) { - funcParameters.Add(parameter.Name); - - IEdmOptionalParameter optionalParameter = parameter as IEdmOptionalParameter; - if (optionalParameter != null) - { - // skip verification for optional parameter - continue; - } - - // for required parameter, it should be in the parameter template mapping. - if (!parameters.ContainsKey(parameter.Name)) - { - throw new ODataException(Error.Format(SRResources.MissingRequiredParameterInOperation, parameter.Name, function.FullName())); - } + throw new ODataException(Error.Format(SRResources.CannotFindParameterInOperation, parameter.Key, function.FullName())); } - foreach (var parameter in parameters) + string templateName = parameter.Value; + if (templateName == null || !templateName.IsValidTemplateLiteral()) { - if (!funcParameters.Contains(parameter.Key)) - { - throw new ODataException(Error.Format(SRResources.CannotFindParameterInOperation, parameter.Key, function.FullName())); - } - - string templateName = parameter.Value; - if (templateName == null || !templateName.IsValidTemplateLiteral()) - { - throw new ODataException(Error.Format(SRResources.ParameterTemplateMustBeInCurlyBraces, parameter.Value, function.FullName())); - } - - templateName = templateName.Substring(1, templateName.Length - 2).Trim(); - if (string.IsNullOrEmpty(templateName)) - { - throw new ODataException(Error.Format(SRResources.EmptyParameterAlias, parameter.Key, function.FullName())); - } + throw new ODataException(Error.Format(SRResources.ParameterTemplateMustBeInCurlyBraces, parameter.Value, function.FullName())); + } - parameterMappings[parameter.Key] = templateName; + templateName = templateName.Substring(1, templateName.Length - 2).Trim(); + if (string.IsNullOrEmpty(templateName)) + { + throw new ODataException(Error.Format(SRResources.EmptyParameterAlias, parameter.Key, function.FullName())); } - return parameterMappings; + parameterMappings[parameter.Key] = templateName; } - /// - /// Build the function parameter mapping. - /// - /// The given function parameter - /// The segment string. - /// The build function parameter mapping. - public static IDictionary BuildParameterMappings(this IEnumerable parameters, string segment) - { - if (parameters == null) - { - throw Error.ArgumentNull(nameof(parameters)); - } + return parameterMappings; + } - Dictionary parameterMappings = new Dictionary(); + /// + /// Build the function parameter mapping. + /// + /// The given function parameter + /// The segment string. + /// The build function parameter mapping. + public static IDictionary BuildParameterMappings(this IEnumerable parameters, string segment) + { + if (parameters == null) + { + throw Error.ArgumentNull(nameof(parameters)); + } - foreach (OperationSegmentParameter parameter in parameters) - { - string parameterName = parameter.Name; - string nameInRouteData = null; + Dictionary parameterMappings = new Dictionary(); - ConstantNode node = parameter.Value as ConstantNode; - if (node != null) - { - UriTemplateExpression uriTemplateExpression = node.Value as UriTemplateExpression; - if (uriTemplateExpression != null) - { - nameInRouteData = uriTemplateExpression.LiteralText.Trim(); - } - } - else - { - // Just for easy constructor the function parameters - nameInRouteData = parameter.Value as string; - } + foreach (OperationSegmentParameter parameter in parameters) + { + string parameterName = parameter.Name; + string nameInRouteData = null; - if (nameInRouteData == null || !nameInRouteData.IsValidTemplateLiteral()) + ConstantNode node = parameter.Value as ConstantNode; + if (node != null) + { + UriTemplateExpression uriTemplateExpression = node.Value as UriTemplateExpression; + if (uriTemplateExpression != null) { - throw new ODataException(Error.Format(SRResources.ParameterTemplateMustBeInCurlyBraces, parameter.Value, segment)); + nameInRouteData = uriTemplateExpression.LiteralText.Trim(); } + } + else + { + // Just for easy constructor the function parameters + nameInRouteData = parameter.Value as string; + } - nameInRouteData = nameInRouteData.Substring(1, nameInRouteData.Length - 2).Trim(); - if (string.IsNullOrEmpty(nameInRouteData)) - { - throw new ODataException(Error.Format(SRResources.EmptyParameterAlias, parameter.Name, segment)); - } + if (nameInRouteData == null || !nameInRouteData.IsValidTemplateLiteral()) + { + throw new ODataException(Error.Format(SRResources.ParameterTemplateMustBeInCurlyBraces, parameter.Value, segment)); + } - parameterMappings[parameterName] = nameInRouteData; + nameInRouteData = nameInRouteData.Substring(1, nameInRouteData.Length - 2).Trim(); + if (string.IsNullOrEmpty(nameInRouteData)) + { + throw new ODataException(Error.Format(SRResources.EmptyParameterAlias, parameter.Name, segment)); } - return parameterMappings; + parameterMappings[parameterName] = nameInRouteData; } - /// - /// Gets the function parameter sets. - /// - /// The input function. - /// The set of parameter name. - public static IDictionary GetFunctionParamterMappings(this IEdmFunction function) - { - if (function == null) - { - throw Error.ArgumentNull(nameof(function)); - } + return parameterMappings; + } - int skip = function.IsBound ? 1 : 0; - return function.Parameters.Skip(skip).ToDictionary(p => p.Name, p => $"{{{p.Name}}}"); + /// + /// Gets the function parameter sets. + /// + /// The input function. + /// The set of parameter name. + public static IDictionary GetFunctionParamterMappings(this IEdmFunction function) + { + if (function == null) + { + throw Error.ArgumentNull(nameof(function)); } - /// - /// Gets the function import parameter sets. - /// - /// The input function import. - /// The set of parameter name. - public static IDictionary GetFunctionParamterMappings(this IEdmFunctionImport functionImport) + int skip = function.IsBound ? 1 : 0; + return function.Parameters.Skip(skip).ToDictionary(p => p.Name, p => $"{{{p.Name}}}"); + } + + /// + /// Gets the function import parameter sets. + /// + /// The input function import. + /// The set of parameter name. + public static IDictionary GetFunctionParamterMappings(this IEdmFunctionImport functionImport) + { + if (functionImport == null) { - if (functionImport == null) - { - throw Error.ArgumentNull(nameof(functionImport)); - } + throw Error.ArgumentNull(nameof(functionImport)); + } - return functionImport.Function.Parameters.ToDictionary(p => p.Name, p => $"{{{p.Name}}}"); + return functionImport.Function.Parameters.ToDictionary(p => p.Name, p => $"{{{p.Name}}}"); + } + + /// + /// Split the operation into function and action. + /// + /// The operation imports + /// + public static (IList, IList) SplitOperationImports(this IEnumerable operationImports) + { + if (operationImports == null) + { + throw Error.ArgumentNull(nameof(operationImports)); } - /// - /// Split the operation into function and action. - /// - /// The operation imports - /// - public static (IList, IList) SplitOperationImports(this IEnumerable operationImports) + IList actions = new List(); + IList functions = new List(); + foreach (var import in operationImports) { - if (operationImports == null) + if (import.IsActionImport()) { - throw Error.ArgumentNull(nameof(operationImports)); + actions.Add((IEdmActionImport)import); } - - IList actions = new List(); - IList functions = new List(); - foreach (var import in operationImports) + else { - if (import.IsActionImport()) - { - actions.Add((IEdmActionImport)import); - } - else - { - functions.Add((IEdmFunctionImport)import); - } + functions.Add((IEdmFunctionImport)import); } - - return (actions, functions); } + + return (actions, functions); } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/OperationLinkBuilder.cs b/src/Microsoft.AspNetCore.OData/Edm/OperationLinkBuilder.cs index e351fcd7b..efa708a21 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/OperationLinkBuilder.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/OperationLinkBuilder.cs @@ -8,93 +8,92 @@ using System; using Microsoft.AspNetCore.OData.Formatter; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// can be used to annotate an action or a function. +/// This is how formatters create links to invoke bound actions or functions. +/// +public class OperationLinkBuilder { /// - /// can be used to annotate an action or a function. - /// This is how formatters create links to invoke bound actions or functions. + /// Create a new based on an entity link factory. /// - public class OperationLinkBuilder + /// The link factory this should use when building links. + /// + /// A value indicating whether the link factory generates links that follow OData conventions. + /// + public OperationLinkBuilder(Func linkFactory, bool followsConventions) { - /// - /// Create a new based on an entity link factory. - /// - /// The link factory this should use when building links. - /// - /// A value indicating whether the link factory generates links that follow OData conventions. - /// - public OperationLinkBuilder(Func linkFactory, bool followsConventions) + if (linkFactory == null) { - if (linkFactory == null) - { - throw Error.ArgumentNull(nameof(linkFactory)); - } - - LinkFactory = linkFactory; - FollowsConventions = followsConventions; + throw Error.ArgumentNull(nameof(linkFactory)); } - /// - /// Create a new based on a feed link factory. - /// - /// The link factory this should use when building links. - /// - /// A value indicating whether the action link factory generates links that follow OData conventions. - /// - public OperationLinkBuilder(Func linkFactory, bool followsConventions) - { - if (linkFactory == null) - { - throw Error.ArgumentNull(nameof(linkFactory)); - } + LinkFactory = linkFactory; + FollowsConventions = followsConventions; + } - FeedLinkFactory = linkFactory; - FollowsConventions = followsConventions; + /// + /// Create a new based on a feed link factory. + /// + /// The link factory this should use when building links. + /// + /// A value indicating whether the action link factory generates links that follow OData conventions. + /// + public OperationLinkBuilder(Func linkFactory, bool followsConventions) + { + if (linkFactory == null) + { + throw Error.ArgumentNull(nameof(linkFactory)); } - /// - /// Gets the resource link factory. - /// - internal Func LinkFactory { get; } + FeedLinkFactory = linkFactory; + FollowsConventions = followsConventions; + } - /// - /// Gets the feed link factory. - /// - internal Func FeedLinkFactory { get; } + /// + /// Gets the resource link factory. + /// + internal Func LinkFactory { get; } - /// - /// Gets a Boolean indicating whether the link factory follows OData conventions or not. - /// - public bool FollowsConventions { get; } + /// + /// Gets the feed link factory. + /// + internal Func FeedLinkFactory { get; } - /// - /// Builds the operation link for the given resource. - /// - /// An instance context wrapping the resource instance. - /// The generated link. - public virtual Uri BuildLink(ResourceContext context) - { - if (LinkFactory == null) - { - return null; - } + /// + /// Gets a Boolean indicating whether the link factory follows OData conventions or not. + /// + public bool FollowsConventions { get; } - return LinkFactory(context); + /// + /// Builds the operation link for the given resource. + /// + /// An instance context wrapping the resource instance. + /// The generated link. + public virtual Uri BuildLink(ResourceContext context) + { + if (LinkFactory == null) + { + return null; } - /// - /// Builds the operation link for the given feed. - /// - /// An feed context wrapping the feed instance. - /// The generated link. - public virtual Uri BuildLink(ResourceSetContext context) - { - if (FeedLinkFactory == null) - { - return null; - } + return LinkFactory(context); + } - return FeedLinkFactory(context); + /// + /// Builds the operation link for the given feed. + /// + /// An feed context wrapping the feed instance. + /// The generated link. + public virtual Uri BuildLink(ResourceSetContext context) + { + if (FeedLinkFactory == null) + { + return null; } + + return FeedLinkFactory(context); } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/OrderByClauseHelpers.cs b/src/Microsoft.AspNetCore.OData/Edm/OrderByClauseHelpers.cs index 08c857f0d..23688a12d 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/OrderByClauseHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/OrderByClauseHelpers.cs @@ -10,95 +10,94 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// Helper methods for . +/// +internal static class OrderByClauseHelpers { + public static readonly string OrderByGlobalNameKey = "__orderby_EDC7A1AC-97F7-463F-BFF9-DE1FD7FCE27E"; + public static readonly string OrderByPropertyNamePrefix = "__orderby_"; + /// - /// Helper methods for . + /// Convert the OrderByClause to list. /// - internal static class OrderByClauseHelpers + /// The input orderby clause, the 'ThenBy' in each node does matter. + /// The output orderby clauses, the 'ThenBy' in each node does NOT matter. + public static List ToList(this OrderByClause clause) { - public static readonly string OrderByGlobalNameKey = "__orderby_EDC7A1AC-97F7-463F-BFF9-DE1FD7FCE27E"; - public static readonly string OrderByPropertyNamePrefix = "__orderby_"; - - /// - /// Convert the OrderByClause to list. - /// - /// The input orderby clause, the 'ThenBy' in each node does matter. - /// The output orderby clauses, the 'ThenBy' in each node does NOT matter. - public static List ToList(this OrderByClause clause) + List clauses = new List(); + while (clause != null) { - List clauses = new List(); - while (clause != null) - { - // Be noted, in order to save the memory, we don't need to create a new OrderByClause and set the 'ThenBy' to null. - // - // clauses.Add(new OrderByClause(null, clause.Expression, clause.Direction, clause.RangeVariable)); - // - clauses.Add(clause); - clause = clause.ThenBy; - } - - return clauses; + // Be noted, in order to save the memory, we don't need to create a new OrderByClause and set the 'ThenBy' to null. + // + // clauses.Add(new OrderByClause(null, clause.Expression, clause.Direction, clause.RangeVariable)); + // + clauses.Add(clause); + clause = clause.ThenBy; } - /// - /// Test the OrderByClause to see whether it's an orderby like: $orderby=name - /// For others, for example, $orderby=location/city is not top-level single property orderby. - /// When we support key alias (key on sub-property or complex property), we should consider to update this. - /// - /// The OrderbyClause. - /// The output property. - /// The output property name, it works for dynamic property. - /// true/false. - public static bool IsTopLevelSingleProperty(this OrderByClause clause, out IEdmProperty property, out string propertyName) - { - property = null; - propertyName = null; - if (clause == null) - { - return false; - } - - // we only care about scenarios like: $orderby=name - // we do nothing for others, for example: $orderby=location/city or $orderby=tolower(name) - if (clause.Expression is SingleValuePropertyAccessNode node && - (node.Source is ResourceRangeVariableReferenceNode || node.Source is NonResourceRangeVariableReferenceNode)) - { - property = node.Property; - propertyName = node.Property.Name; - return true; - } - - if (clause.Expression is SingleValueOpenPropertyAccessNode openNode && - (openNode.Source is ResourceRangeVariableReferenceNode || openNode.Source is NonResourceRangeVariableReferenceNode)) - { - propertyName = openNode.Name; - return true; - } + return clauses; + } + /// + /// Test the OrderByClause to see whether it's an orderby like: $orderby=name + /// For others, for example, $orderby=location/city is not top-level single property orderby. + /// When we support key alias (key on sub-property or complex property), we should consider to update this. + /// + /// The OrderbyClause. + /// The output property. + /// The output property name, it works for dynamic property. + /// true/false. + public static bool IsTopLevelSingleProperty(this OrderByClause clause, out IEdmProperty property, out string propertyName) + { + property = null; + propertyName = null; + if (clause == null) + { return false; } - /// - /// Remove the ' desc' from the orderby clause. - /// - /// The orderby clause. - /// The orderby direction. - /// changed orderby clause. - public static string RemoveDirection(this string orderby, string direction) + // we only care about scenarios like: $orderby=name + // we do nothing for others, for example: $orderby=location/city or $orderby=tolower(name) + if (clause.Expression is SingleValuePropertyAccessNode node && + (node.Source is ResourceRangeVariableReferenceNode || node.Source is NonResourceRangeVariableReferenceNode)) { - if (orderby == null) - { - return orderby; - } + property = node.Property; + propertyName = node.Property.Name; + return true; + } - int index = orderby.LastIndexOf(direction); - if (index != -1) - { - return orderby.Substring(0, index); - } + if (clause.Expression is SingleValueOpenPropertyAccessNode openNode && + (openNode.Source is ResourceRangeVariableReferenceNode || openNode.Source is NonResourceRangeVariableReferenceNode)) + { + propertyName = openNode.Name; + return true; + } + + return false; + } + /// + /// Remove the ' desc' from the orderby clause. + /// + /// The orderby clause. + /// The orderby direction. + /// changed orderby clause. + public static string RemoveDirection(this string orderby, string direction) + { + if (orderby == null) + { return orderby; } + + int index = orderby.LastIndexOf(direction); + if (index != -1) + { + return orderby.Substring(0, index); + } + + return orderby; } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/SelectModelPath.cs b/src/Microsoft.AspNetCore.OData/Edm/SelectModelPath.cs index 3e5ab4786..60caee9c5 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/SelectModelPath.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/SelectModelPath.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -9,51 +9,50 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// The select property path is a model path with the following restriction: +/// 1) only include type case and structural property +/// +internal class SelectModelPath : List { + private string _selectPath; + /// - /// The select property path is a model path with the following restriction: - /// 1) only include type case and structural property + /// Initializes a new instance of the class. /// - internal class SelectModelPath : List + /// The segment nodes. + public SelectModelPath(IEnumerable nodes) + : base(nodes) { - private string _selectPath; - - /// - /// Initializes a new instance of the class. - /// - /// The segment nodes. - public SelectModelPath(IEnumerable nodes) - : base(nodes) - { - ValidateAndCalculateElementPath(); - } + ValidateAndCalculateElementPath(); + } - /// - /// Gets the select path. - /// - public string SelectPath => _selectPath; + /// + /// Gets the select path. + /// + public string SelectPath => _selectPath; - private void ValidateAndCalculateElementPath() + private void ValidateAndCalculateElementPath() + { + IList identifiers = new List(); + foreach (IEdmElement element in this) { - IList identifiers = new List(); - foreach (IEdmElement element in this) + if (element is IEdmStructuredType structuredType) { - if (element is IEdmStructuredType structuredType) - { - identifiers.Add(structuredType.FullTypeName()); - } - else if (element is IEdmStructuralProperty structuralProperty) - { - identifiers.Add(structuralProperty.Name); - } - else - { - throw new ODataException(Error.Format(SRResources.InvalidSegmentInSelectExpandPath, element.GetType().Name)); - } + identifiers.Add(structuredType.FullTypeName()); + } + else if (element is IEdmStructuralProperty structuralProperty) + { + identifiers.Add(structuralProperty.Name); + } + else + { + throw new ODataException(Error.Format(SRResources.InvalidSegmentInSelectExpandPath, element.GetType().Name)); } - - _selectPath = string.Join("/", identifiers); } + + _selectPath = string.Join("/", identifiers); } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/SelfLinkBuilder.cs b/src/Microsoft.AspNetCore.OData/Edm/SelfLinkBuilder.cs index 4d943a5a3..7e14a664f 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/SelfLinkBuilder.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/SelfLinkBuilder.cs @@ -8,33 +8,32 @@ using System; using Microsoft.AspNetCore.OData.Formatter; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +/// +/// Encapsulates a self link factory and whether the link factory follows conventions or not. +/// +/// The type of the self link generated. This should be for ID links and for read and edit links. +public class SelfLinkBuilder { /// - /// Encapsulates a self link factory and whether the link factory follows conventions or not. + /// Constructs a new instance of . /// - /// The type of the self link generated. This should be for ID links and for read and edit links. - public class SelfLinkBuilder + /// The link factory. + /// Whether the factory follows odata conventions for link generation. + public SelfLinkBuilder(Func linkFactory, bool followsConventions) { - /// - /// Constructs a new instance of . - /// - /// The link factory. - /// Whether the factory follows odata conventions for link generation. - public SelfLinkBuilder(Func linkFactory, bool followsConventions) - { - Factory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory)); - FollowsConventions = followsConventions; - } + Factory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory)); + FollowsConventions = followsConventions; + } - /// - /// Gets the factory for generating links. - /// - public Func Factory { get; } + /// + /// Gets the factory for generating links. + /// + public Func Factory { get; } - /// - /// Gets a boolean indicating whether the link factory follows OData conventions or not. - /// - public bool FollowsConventions { get; } - } + /// + /// Gets a boolean indicating whether the link factory follows OData conventions or not. + /// + public bool FollowsConventions { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Edm/TypeCacheItem.cs b/src/Microsoft.AspNetCore.OData/Edm/TypeCacheItem.cs index ee9eb39bb..69e6ead50 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/TypeCacheItem.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/TypeCacheItem.cs @@ -9,76 +9,75 @@ using System.Collections.Concurrent; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Edm +namespace Microsoft.AspNetCore.OData.Edm; + +internal class TypeCacheItem { - internal class TypeCacheItem - { - #region ClrType => EdmType - /// - /// to . - /// - public ConcurrentDictionary ClrToEdmTypeCache { get; } = new ConcurrentDictionary(); + #region ClrType => EdmType + /// + /// to . + /// + public ConcurrentDictionary ClrToEdmTypeCache { get; } = new ConcurrentDictionary(); - public bool TryFindEdmType(Type clrType, out IEdmTypeReference edmType) + public bool TryFindEdmType(Type clrType, out IEdmTypeReference edmType) + { + edmType = null; + if (clrType == null) { - edmType = null; - if (clrType == null) - { - return false; - } - - return ClrToEdmTypeCache.TryGetValue(clrType, out edmType); + return false; } - public void AddClrToEdmMap(Type clrType, IEdmTypeReference edmType) - { - ClrToEdmTypeCache[clrType] = edmType; - } - #endregion + return ClrToEdmTypeCache.TryGetValue(clrType, out edmType); + } - #region EdmType => ClrType - /// - /// to . - /// item1: non-nullable - /// item2: nullable - /// - public ConcurrentDictionary EdmToClrTypeCache { get; } = new ConcurrentDictionary(); + public void AddClrToEdmMap(Type clrType, IEdmTypeReference edmType) + { + ClrToEdmTypeCache[clrType] = edmType; + } + #endregion - public bool TryFindClrType(IEdmType edmType, bool isNullable, out Type clrType) - { - if (edmType == null) - { - clrType = null; - return false; - } + #region EdmType => ClrType + /// + /// to . + /// item1: non-nullable + /// item2: nullable + /// + public ConcurrentDictionary EdmToClrTypeCache { get; } = new ConcurrentDictionary(); + public bool TryFindClrType(IEdmType edmType, bool isNullable, out Type clrType) + { + if (edmType == null) + { clrType = null; - if (EdmToClrTypeCache.TryGetValue(edmType, out (Type, Type) clrTypes)) - { - if (isNullable) - { - clrType = clrTypes.Item2; - } - else - { - clrType = clrTypes.Item1; - } - } - - return clrType != null; + return false; } - public void AddEdmToClrMap(IEdmType edmType, bool isNullable, Type clrType) + clrType = null; + if (EdmToClrTypeCache.TryGetValue(edmType, out (Type, Type) clrTypes)) { if (isNullable) { - EdmToClrTypeCache.AddOrUpdate(edmType, (null, clrType), (k, v) => (v.Item1, clrType)); + clrType = clrTypes.Item2; } else { - EdmToClrTypeCache.AddOrUpdate(edmType, (clrType, null), (k, v) => (clrType, v.Item2)); + clrType = clrTypes.Item1; } } - #endregion + + return clrType != null; + } + + public void AddEdmToClrMap(IEdmType edmType, bool isNullable, Type clrType) + { + if (isNullable) + { + EdmToClrTypeCache.AddOrUpdate(edmType, (null, clrType), (k, v) => (v.Item1, clrType)); + } + else + { + EdmToClrTypeCache.AddOrUpdate(edmType, (clrType, null), (k, v) => (clrType, v.Item2)); + } } + #endregion } diff --git a/src/Microsoft.AspNetCore.OData/Extensions/ActionDescriptorExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/ActionDescriptorExtensions.cs index d84e0f568..e51767aa6 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/ActionDescriptorExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/ActionDescriptorExtensions.cs @@ -13,72 +13,71 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Extensions +namespace Microsoft.AspNetCore.OData.Extensions; + +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class ActionDescriptorExtensions { - [EditorBrowsable(EditorBrowsableState.Never)] - internal static class ActionDescriptorExtensions - { - // Maintain the Microsoft.AspNetCore.OData. prefix in any new properties to avoid conflicts with user properties - // and those of the v3 assembly. Concern is reduced here due to addition of user type name but prefix - // also clearly ties the property to code in this assembly. - private const string ModelKeyPrefix = "Microsoft.AspNetCore.OData.Model+"; + // Maintain the Microsoft.AspNetCore.OData. prefix in any new properties to avoid conflicts with user properties + // and those of the v3 assembly. Concern is reduced here due to addition of user type name but prefix + // also clearly ties the property to code in this assembly. + private const string ModelKeyPrefix = "Microsoft.AspNetCore.OData.Model+"; - private static readonly object SyncLock = new object(); + private static readonly object SyncLock = new object(); - internal static IEdmModel GetEdmModel(this ActionDescriptor actionDescriptor, HttpRequest request, Type entityClrType) + internal static IEdmModel GetEdmModel(this ActionDescriptor actionDescriptor, HttpRequest request, Type entityClrType) + { + if (actionDescriptor == null) { - if (actionDescriptor == null) - { - throw Error.ArgumentNull(nameof(actionDescriptor)); - } + throw Error.ArgumentNull(nameof(actionDescriptor)); + } - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); + } - if (entityClrType == null) - { - throw Error.ArgumentNull(nameof(entityClrType)); - } + if (entityClrType == null) + { + throw Error.ArgumentNull(nameof(entityClrType)); + } - IEdmModel model; + IEdmModel model; - string key = ModelKeyPrefix + entityClrType.FullName; - object modelAsObject; - if (actionDescriptor.Properties.TryGetValue(key, out modelAsObject)) + string key = ModelKeyPrefix + entityClrType.FullName; + object modelAsObject; + if (actionDescriptor.Properties.TryGetValue(key, out modelAsObject)) + { + model = modelAsObject as IEdmModel; + } + else + { + IAssemblyResolver resolver = request.HttpContext.RequestServices.GetService(); + ODataConventionModelBuilder builder; + if (resolver != null) { - model = modelAsObject as IEdmModel; + builder = new ODataConventionModelBuilder(resolver, isQueryCompositionMode: true); } else { - IAssemblyResolver resolver = request.HttpContext.RequestServices.GetService(); - ODataConventionModelBuilder builder; - if (resolver != null) - { - builder = new ODataConventionModelBuilder(resolver, isQueryCompositionMode: true); - } - else - { - // need the model builder same as here???? - // TODO: - builder = new ODataConventionModelBuilder(/*DefaultAssemblyResolver.Default, isQueryCompositionMode: true*/); - } + // need the model builder same as here???? + // TODO: + builder = new ODataConventionModelBuilder(/*DefaultAssemblyResolver.Default, isQueryCompositionMode: true*/); + } - EntityTypeConfiguration entityTypeConfiguration = builder.AddEntityType(entityClrType); - builder.AddEntitySet(entityClrType.Name, entityTypeConfiguration); - model = builder.GetEdmModel(); + EntityTypeConfiguration entityTypeConfiguration = builder.AddEntityType(entityClrType); + builder.AddEntitySet(entityClrType.Name, entityTypeConfiguration); + model = builder.GetEdmModel(); - lock (SyncLock) + lock (SyncLock) + { + if (!actionDescriptor.Properties.ContainsKey(key)) { - if (!actionDescriptor.Properties.ContainsKey(key)) - { - actionDescriptor.Properties.Add(key, model); - } + actionDescriptor.Properties.Add(key, model); } } - - return model; } + + return model; } } diff --git a/src/Microsoft.AspNetCore.OData/Extensions/ActionModelExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/ActionModelExtensions.cs index ddf4b3d8c..1f428d397 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/ActionModelExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/ActionModelExtensions.cs @@ -19,317 +19,316 @@ using Microsoft.AspNetCore.Routing; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Extensions +namespace Microsoft.AspNetCore.OData.Extensions; + +/// +/// The extension methods for . +/// +public static class ActionModelExtensions { /// - /// The extension methods for . + /// Tests whether the action is not suitable for OData action. /// - public static class ActionModelExtensions + /// The given action model. + /// True/False. + public static bool IsODataIgnored(this ActionModel action) { - /// - /// Tests whether the action is not suitable for OData action. - /// - /// The given action model. - /// True/False. - public static bool IsODataIgnored(this ActionModel action) + if (action == null) { - if (action == null) - { - throw Error.ArgumentNull(nameof(action)); - } + throw Error.ArgumentNull(nameof(action)); + } - return action.Attributes.Any(a => a is ODataIgnoredAttribute); + return action.Attributes.Any(a => a is ODataIgnoredAttribute); + } + + /// + /// Tests whether the action has the given parameter with the given type. + /// + /// The parameter type. + /// The given action model. + /// The given parameter name. + /// True/False. + public static bool HasParameter(this ActionModel action, string parameterName) + { + if (action == null) + { + throw Error.ArgumentNull(nameof(action)); } - /// - /// Tests whether the action has the given parameter with the given type. - /// - /// The parameter type. - /// The given action model. - /// The given parameter name. - /// True/False. - public static bool HasParameter(this ActionModel action, string parameterName) + // parameter name is unique? + ParameterModel parameter = action.Parameters.FirstOrDefault(p => p.Name == parameterName); + if (parameter == null) { - if (action == null) - { - throw Error.ArgumentNull(nameof(action)); - } + return false; + } - // parameter name is unique? - ParameterModel parameter = action.Parameters.FirstOrDefault(p => p.Name == parameterName); - if (parameter == null) - { - return false; - } + return parameter.ParameterType == typeof(T); + } - return parameter.ParameterType == typeof(T); + /// + /// Gets the attribute on an action model. + /// + /// The required attribute type. + /// The given action model. + /// Null or the corresponding attribute. + public static T GetAttribute(this ActionModel action) + { + if (action == null) + { + throw Error.ArgumentNull(nameof(action)); } - /// - /// Gets the attribute on an action model. - /// - /// The required attribute type. - /// The given action model. - /// Null or the corresponding attribute. - public static T GetAttribute(this ActionModel action) - { - if (action == null) - { - throw Error.ArgumentNull(nameof(action)); - } + return action.Attributes.OfType().FirstOrDefault(); + } - return action.Attributes.OfType().FirstOrDefault(); + /// + /// Test whether the action has the key parameters defined. + /// + /// The action model. + /// The Edm entity type. + /// Enable property name case insensitive. + /// The key prefix for the action parameter. + /// True/false. + public static bool HasODataKeyParameter(this ActionModel action, IEdmEntityType entityType, bool enablePropertyNameCaseInsensitive = false, string keyPrefix = "key") + { + if (action == null) + { + throw Error.ArgumentNull(nameof(action)); } - /// - /// Test whether the action has the key parameters defined. - /// - /// The action model. - /// The Edm entity type. - /// Enable property name case insensitive. - /// The key prefix for the action parameter. - /// True/false. - public static bool HasODataKeyParameter(this ActionModel action, IEdmEntityType entityType, bool enablePropertyNameCaseInsensitive = false, string keyPrefix = "key") + if (entityType == null) { - if (action == null) - { - throw Error.ArgumentNull(nameof(action)); - } + throw Error.ArgumentNull(nameof(entityType)); + } - if (entityType == null) + // TODO: shall we make sure the type is matching? + var keys = entityType.Key().ToArray(); + if (keys.Length == 1) + { + // one key + return action.Parameters.Any(p => p.ParameterInfo.Name == keyPrefix); + } + else + { + // multiple keys + foreach (var key in keys) { - throw Error.ArgumentNull(nameof(entityType)); - } + string keyName = $"{keyPrefix}{key.Name}"; - // TODO: shall we make sure the type is matching? - var keys = entityType.Key().ToArray(); - if (keys.Length == 1) - { - // one key - return action.Parameters.Any(p => p.ParameterInfo.Name == keyPrefix); - } - else - { - // multiple keys - foreach (var key in keys) + if (enablePropertyNameCaseInsensitive) { - string keyName = $"{keyPrefix}{key.Name}"; - - if (enablePropertyNameCaseInsensitive) + keyName = keyName.ToUpperInvariant(); + if (!action.Parameters.Any(p => p.ParameterInfo.Name.ToUpperInvariant() == keyName)) { - keyName = keyName.ToUpperInvariant(); - if (!action.Parameters.Any(p => p.ParameterInfo.Name.ToUpperInvariant() == keyName)) - { - return false; - } + return false; } - else + } + else + { + if (!action.Parameters.Any(p => p.ParameterInfo.Name == keyName)) { - if (!action.Parameters.Any(p => p.ParameterInfo.Name == keyName)) - { - return false; - } + return false; } } - - return true; } + + return true; } + } - /// - /// Adds the OData selector model to the action. - /// - /// The given action model. - /// The supported http methods, if multiple, using ',' to separate. - /// The prefix. - /// The Edm model. - /// The OData path template. - /// The route build options. - public static void AddSelector(this ActionModel action, string httpMethods, string prefix, IEdmModel model, ODataPathTemplate path, ODataRouteOptions options = null) + /// + /// Adds the OData selector model to the action. + /// + /// The given action model. + /// The supported http methods, if multiple, using ',' to separate. + /// The prefix. + /// The Edm model. + /// The OData path template. + /// The route build options. + public static void AddSelector(this ActionModel action, string httpMethods, string prefix, IEdmModel model, ODataPathTemplate path, ODataRouteOptions options = null) + { + if (action == null) { - if (action == null) - { - throw Error.ArgumentNull(nameof(action)); - } + throw Error.ArgumentNull(nameof(action)); + } - if (string.IsNullOrEmpty(httpMethods)) - { - throw Error.ArgumentNullOrEmpty(nameof(httpMethods)); - } + if (string.IsNullOrEmpty(httpMethods)) + { + throw Error.ArgumentNullOrEmpty(nameof(httpMethods)); + } - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); + } - if (path == null) - { - throw Error.ArgumentNull(nameof(path)); - } + if (path == null) + { + throw Error.ArgumentNull(nameof(path)); + } - // if the controller has attribute route decorated, for example: - // [Route("api/[controller]")] - // public class CustomersController : Controller - // {} - // let's always create new selector model for action. - // Since the new created selector model is absolute attribute route, the controller attribute route doesn't apply to this selector model. - bool hasAttributeRouteOnController = action.Controller.Selectors.Any(s => s.AttributeRouteModel != null); + // if the controller has attribute route decorated, for example: + // [Route("api/[controller]")] + // public class CustomersController : Controller + // {} + // let's always create new selector model for action. + // Since the new created selector model is absolute attribute route, the controller attribute route doesn't apply to this selector model. + bool hasAttributeRouteOnController = action.Controller.Selectors.Any(s => s.AttributeRouteModel != null); - // Check if CORS attribute is specified on action. New selectors need to be registered with CORS support. - bool acceptPreflight = action.Controller.Attributes.OfType().Any() || - action.Controller.Attributes.OfType().Any() || - action.Attributes.OfType().Any() || - action.Attributes.OfType().Any(); + // Check if CORS attribute is specified on action. New selectors need to be registered with CORS support. + bool acceptPreflight = action.Controller.Attributes.OfType().Any() || + action.Controller.Attributes.OfType().Any() || + action.Attributes.OfType().Any() || + action.Attributes.OfType().Any(); - // If the methods have different case sensitive, for example, "get", "Get", in the ASP.NET Core 3.1, - // It will throw "An item with the same key has already been added. Key: GET", in - // HttpMethodMatcherPolicy.BuildJumpTable(Int32 exitDestination, IReadOnlyList`1 edges) - // Another root cause is that in attribute routing, we reuse the HttpMethodMetadata, the method name is always "upper" case. - // Therefore, we upper the http method name always. - string[] methods = httpMethods.ToUpperInvariant().Split(','); - foreach (string template in path.GetTemplates(options)) + // If the methods have different case sensitive, for example, "get", "Get", in the ASP.NET Core 3.1, + // It will throw "An item with the same key has already been added. Key: GET", in + // HttpMethodMatcherPolicy.BuildJumpTable(Int32 exitDestination, IReadOnlyList`1 edges) + // Another root cause is that in attribute routing, we reuse the HttpMethodMetadata, the method name is always "upper" case. + // Therefore, we upper the http method name always. + string[] methods = httpMethods.ToUpperInvariant().Split(','); + foreach (string template in path.GetTemplates(options)) + { + // Be noted: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs#L74-L75 + // No matter whether the action selector model is absolute route template, the controller's attribute will apply automatically + // So, let's only create/update the action selector model + SelectorModel selectorModel = action.Selectors.FirstOrDefault(s => s.AttributeRouteModel == null); + if (hasAttributeRouteOnController || selectorModel == null) { - // Be noted: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs#L74-L75 - // No matter whether the action selector model is absolute route template, the controller's attribute will apply automatically - // So, let's only create/update the action selector model - SelectorModel selectorModel = action.Selectors.FirstOrDefault(s => s.AttributeRouteModel == null); - if (hasAttributeRouteOnController || selectorModel == null) - { - // Create a new selector model. - selectorModel = CreateSelectorModel(action, methods, acceptPreflight); - action.Selectors.Add(selectorModel); - } - else - { - // Update the existing non attribute routing selector model. - selectorModel = UpdateSelectorModel(selectorModel, methods, acceptPreflight); - } + // Create a new selector model. + selectorModel = CreateSelectorModel(action, methods, acceptPreflight); + action.Selectors.Add(selectorModel); + } + else + { + // Update the existing non attribute routing selector model. + selectorModel = UpdateSelectorModel(selectorModel, methods, acceptPreflight); + } - ODataRoutingMetadata odataMetadata = new ODataRoutingMetadata(prefix, model, path); - selectorModel.EndpointMetadata.Add(odataMetadata); + ODataRoutingMetadata odataMetadata = new ODataRoutingMetadata(prefix, model, path); + selectorModel.EndpointMetadata.Add(odataMetadata); - string templateStr = string.IsNullOrEmpty(prefix) ? template : $"{prefix}/{template}"; + string templateStr = string.IsNullOrEmpty(prefix) ? template : $"{prefix}/{template}"; - selectorModel.AttributeRouteModel = new AttributeRouteModel - { - // OData convention route template doesn't get combined with the route template applied to the controller. - // Route templates applied to an action that begin with / or ~/ don't get combined with route templates applied to the controller. - Template = $"/{templateStr}", - Order = options?.Order, - Name = templateStr // do we need this? - }; - - // Check with .NET Team whether the "Endpoint name metadata" needed? - selectorModel.EndpointMetadata.Add(new EndpointNameMetadata(Guid.NewGuid().ToString())); // Do we need this? - } + selectorModel.AttributeRouteModel = new AttributeRouteModel + { + // OData convention route template doesn't get combined with the route template applied to the controller. + // Route templates applied to an action that begin with / or ~/ don't get combined with route templates applied to the controller. + Template = $"/{templateStr}", + Order = options?.Order, + Name = templateStr // do we need this? + }; + + // Check with .NET Team whether the "Endpoint name metadata" needed? + selectorModel.EndpointMetadata.Add(new EndpointNameMetadata(Guid.NewGuid().ToString())); // Do we need this? } + } - internal static SelectorModel UpdateSelectorModel(SelectorModel selectorModel, string[] httpMethods, bool acceptPreflight) - { - Contract.Assert(selectorModel != null); + internal static SelectorModel UpdateSelectorModel(SelectorModel selectorModel, string[] httpMethods, bool acceptPreflight) + { + Contract.Assert(selectorModel != null); - // remove the unused constraints (just for safe) - for (var i = selectorModel.ActionConstraints.Count - 1; i >= 0; i--) + // remove the unused constraints (just for safe) + for (var i = selectorModel.ActionConstraints.Count - 1; i >= 0; i--) + { + if (selectorModel.ActionConstraints[i] is IRouteTemplateProvider) { - if (selectorModel.ActionConstraints[i] is IRouteTemplateProvider) - { - selectorModel.ActionConstraints.RemoveAt(i); - } + selectorModel.ActionConstraints.RemoveAt(i); } + } - for (var i = selectorModel.ActionConstraints.Count - 1; i >= 0; i--) + for (var i = selectorModel.ActionConstraints.Count - 1; i >= 0; i--) + { + if (selectorModel.ActionConstraints[i] is HttpMethodActionConstraint) { - if (selectorModel.ActionConstraints[i] is HttpMethodActionConstraint) - { - selectorModel.ActionConstraints.RemoveAt(i); - } + selectorModel.ActionConstraints.RemoveAt(i); } + } - // remove the unused metadata - for (var i = selectorModel.EndpointMetadata.Count - 1; i >= 0; i--) + // remove the unused metadata + for (var i = selectorModel.EndpointMetadata.Count - 1; i >= 0; i--) + { + if (selectorModel.EndpointMetadata[i] is IRouteTemplateProvider) { - if (selectorModel.EndpointMetadata[i] is IRouteTemplateProvider) - { - selectorModel.EndpointMetadata.RemoveAt(i); - } + selectorModel.EndpointMetadata.RemoveAt(i); } + } - for (var i = selectorModel.EndpointMetadata.Count - 1; i >= 0; i--) + for (var i = selectorModel.EndpointMetadata.Count - 1; i >= 0; i--) + { + if (selectorModel.EndpointMetadata[i] is IHttpMethodMetadata) { - if (selectorModel.EndpointMetadata[i] is IHttpMethodMetadata) - { - selectorModel.EndpointMetadata.RemoveAt(i); - } + selectorModel.EndpointMetadata.RemoveAt(i); } + } - // append the http method metadata. - Contract.Assert(httpMethods.Length >= 1); - selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(httpMethods)); - selectorModel.EndpointMetadata.Add(new HttpMethodMetadata(httpMethods, acceptPreflight)); + // append the http method metadata. + Contract.Assert(httpMethods.Length >= 1); + selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(httpMethods)); + selectorModel.EndpointMetadata.Add(new HttpMethodMetadata(httpMethods, acceptPreflight)); - // append controller attributes to action selector model? -- NO - // Be noted: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs#L74-L75 + // append controller attributes to action selector model? -- NO + // Be noted: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs#L74-L75 - return selectorModel; - } + return selectorModel; + } - internal static SelectorModel CreateSelectorModel(ActionModel actionModel, string[] httpMethods, bool acceptPreflight) - { - Contract.Assert(actionModel != null); + internal static SelectorModel CreateSelectorModel(ActionModel actionModel, string[] httpMethods, bool acceptPreflight) + { + Contract.Assert(actionModel != null); - SelectorModel selectorModel = new SelectorModel(); - IReadOnlyList attributes = actionModel.Attributes; + SelectorModel selectorModel = new SelectorModel(); + IReadOnlyList attributes = actionModel.Attributes; - AddRange(selectorModel.ActionConstraints, attributes.OfType()); + AddRange(selectorModel.ActionConstraints, attributes.OfType()); - for (var i = selectorModel.ActionConstraints.Count - 1; i >= 0; i--) + for (var i = selectorModel.ActionConstraints.Count - 1; i >= 0; i--) + { + if (selectorModel.ActionConstraints[i] is IRouteTemplateProvider) { - if (selectorModel.ActionConstraints[i] is IRouteTemplateProvider) - { - selectorModel.ActionConstraints.RemoveAt(i); - } + selectorModel.ActionConstraints.RemoveAt(i); } + } - for (var i = selectorModel.ActionConstraints.Count - 1; i >= 0; i--) + for (var i = selectorModel.ActionConstraints.Count - 1; i >= 0; i--) + { + if (selectorModel.ActionConstraints[i] is HttpMethodActionConstraint) { - if (selectorModel.ActionConstraints[i] is HttpMethodActionConstraint) - { - selectorModel.ActionConstraints.RemoveAt(i); - } + selectorModel.ActionConstraints.RemoveAt(i); } + } - AddRange(selectorModel.EndpointMetadata, attributes); - for (var i = selectorModel.EndpointMetadata.Count - 1; i >= 0; i--) + AddRange(selectorModel.EndpointMetadata, attributes); + for (var i = selectorModel.EndpointMetadata.Count - 1; i >= 0; i--) + { + if (selectorModel.EndpointMetadata[i] is IRouteTemplateProvider) { - if (selectorModel.EndpointMetadata[i] is IRouteTemplateProvider) - { - selectorModel.EndpointMetadata.RemoveAt(i); - } + selectorModel.EndpointMetadata.RemoveAt(i); } + } - for (var i = selectorModel.EndpointMetadata.Count - 1; i >= 0; i--) + for (var i = selectorModel.EndpointMetadata.Count - 1; i >= 0; i--) + { + if (selectorModel.EndpointMetadata[i] is IHttpMethodMetadata) { - if (selectorModel.EndpointMetadata[i] is IHttpMethodMetadata) - { - selectorModel.EndpointMetadata.RemoveAt(i); - } + selectorModel.EndpointMetadata.RemoveAt(i); } + } - Contract.Assert(httpMethods.Length >= 1); - selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(httpMethods)); - selectorModel.EndpointMetadata.Add(new HttpMethodMetadata(httpMethods, acceptPreflight)); + Contract.Assert(httpMethods.Length >= 1); + selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(httpMethods)); + selectorModel.EndpointMetadata.Add(new HttpMethodMetadata(httpMethods, acceptPreflight)); - // append controller attributes to action selector model? -- NO - // Be noted: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs#L74-L75 - return selectorModel; - } + // append controller attributes to action selector model? -- NO + // Be noted: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs#L74-L75 + return selectorModel; + } - private static void AddRange(IList list, IEnumerable items) + private static void AddRange(IList list, IEnumerable items) + { + foreach (var item in items) { - foreach (var item in items) - { - list.Add(item); - } + list.Add(item); } } } diff --git a/src/Microsoft.AspNetCore.OData/Extensions/ControllerModelExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/ControllerModelExtensions.cs index 816c1cd28..0051751f7 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/ControllerModelExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/ControllerModelExtensions.cs @@ -10,60 +10,59 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.OData.Routing.Attributes; -namespace Microsoft.AspNetCore.OData.Extensions +namespace Microsoft.AspNetCore.OData.Extensions; + +/// +/// The extension methods for the . +/// +public static class ControllerModelExtensions { /// - /// The extension methods for the . + /// Test whether the controller is not suitable for OData controller. /// - public static class ControllerModelExtensions + /// The given controller model. + /// True/False. + public static bool IsODataIgnored(this ControllerModel controller) { - /// - /// Test whether the controller is not suitable for OData controller. - /// - /// The given controller model. - /// True/False. - public static bool IsODataIgnored(this ControllerModel controller) + if (controller == null) { - if (controller == null) - { - throw Error.ArgumentNull(nameof(controller)); - } - - return controller.Attributes.Any(a => a is ODataIgnoredAttribute); + throw Error.ArgumentNull(nameof(controller)); } - /// - /// Test whether the controller has the specified attribute defined - /// - /// The attribute type. - /// The given controller model. - /// True/False. - public static bool HasAttribute(this ControllerModel controller) - where T : Attribute - { - if (controller == null) - { - throw Error.ArgumentNull(nameof(controller)); - } + return controller.Attributes.Any(a => a is ODataIgnoredAttribute); + } - return controller.Attributes.Any(a => a is T); + /// + /// Test whether the controller has the specified attribute defined + /// + /// The attribute type. + /// The given controller model. + /// True/False. + public static bool HasAttribute(this ControllerModel controller) + where T : Attribute + { + if (controller == null) + { + throw Error.ArgumentNull(nameof(controller)); } - /// - /// Gets the attribute from the controller model. - /// - /// The attribute type. - /// The given controller model. - /// The attribute or null. - public static T GetAttribute(this ControllerModel controller) - where T : Attribute - { - if (controller == null) - { - throw Error.ArgumentNull(nameof(controller)); - } + return controller.Attributes.Any(a => a is T); + } - return controller.Attributes.OfType().FirstOrDefault(); + /// + /// Gets the attribute from the controller model. + /// + /// The attribute type. + /// The given controller model. + /// The attribute or null. + public static T GetAttribute(this ControllerModel controller) + where T : Attribute + { + if (controller == null) + { + throw Error.ArgumentNull(nameof(controller)); } + + return controller.Attributes.OfType().FirstOrDefault(); } } diff --git a/src/Microsoft.AspNetCore.OData/Extensions/GetNextPageHelper.cs b/src/Microsoft.AspNetCore.OData/Extensions/GetNextPageHelper.cs index d3aaf1a75..4d133953c 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/GetNextPageHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/GetNextPageHelper.cs @@ -15,114 +15,113 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.OData.Extensions +namespace Microsoft.AspNetCore.OData.Extensions; + +/// +/// Helper to generate next page links. +/// +internal static class GetNextPageHelper { - /// - /// Helper to generate next page links. - /// - internal static class GetNextPageHelper + [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "")] + internal static Uri GetNextPageLink(Uri requestUri, IEnumerable> queryParameters, int pageSize, object instance = null, Func objectToSkipTokenValue = null) { - [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "")] - internal static Uri GetNextPageLink(Uri requestUri, IEnumerable> queryParameters, int pageSize, object instance = null, Func objectToSkipTokenValue = null) - { - Contract.Assert(requestUri != null); - Contract.Assert(queryParameters != null); + Contract.Assert(requestUri != null); + Contract.Assert(queryParameters != null); - StringBuilder queryBuilder = new StringBuilder(); + StringBuilder queryBuilder = new StringBuilder(); - int nextPageSkip = pageSize; + int nextPageSkip = pageSize; - String skipTokenValue = objectToSkipTokenValue == null ? null : objectToSkipTokenValue(instance); - //If no value for skiptoken can be extracted; revert to using skip - bool useDefaultSkip = String.IsNullOrWhiteSpace(skipTokenValue); + String skipTokenValue = objectToSkipTokenValue == null ? null : objectToSkipTokenValue(instance); + //If no value for skiptoken can be extracted; revert to using skip + bool useDefaultSkip = String.IsNullOrWhiteSpace(skipTokenValue); - foreach (KeyValuePair kvp in queryParameters) + foreach (KeyValuePair kvp in queryParameters) + { + string key = kvp.Key.ToLowerInvariant(); + string value = kvp.Value; + + switch (key) { - string key = kvp.Key.ToLowerInvariant(); - string value = kvp.Value; - - switch (key) - { - case "$top": - int top; - if (Int32.TryParse(value, out top)) + case "$top": + int top; + if (Int32.TryParse(value, out top)) + { + // We decrease top by the pageSize because that's the number of results we're returning in the current page. + // If the $top query option's value is less than or equal to the page size, there is no next page. + if (top > pageSize) { - // We decrease top by the pageSize because that's the number of results we're returning in the current page. - // If the $top query option's value is less than or equal to the page size, there is no next page. - if (top > pageSize) - { - value = (top - pageSize).ToString(CultureInfo.InvariantCulture); - } - else - { - return null; - } + value = (top - pageSize).ToString(CultureInfo.InvariantCulture); } - break; - case "$skip": - if (useDefaultSkip) + else { - //Need to increment skip only if we are not using skiptoken - int skip; - if (Int32.TryParse(value, out skip)) - { - // We increase skip by the pageSize because that's the number of results we're returning in the current page - nextPageSkip += skip; - } + return null; } - continue; - case "$skiptoken": - continue; - default: - key = kvp.Key; // Leave parameters that are not OData query options in initial form - break; - } - - if (key.Length > 0 && key[0] == '$') - { - // $ is a legal first character in query keys - key = '$' + Uri.EscapeDataString(key.Substring(1)); - } - else - { - key = Uri.EscapeDataString(key); - } - - value = Uri.EscapeDataString(value); - - queryBuilder.Append(key); - queryBuilder.Append('='); - queryBuilder.Append(value); - queryBuilder.Append('&'); + } + break; + case "$skip": + if (useDefaultSkip) + { + //Need to increment skip only if we are not using skiptoken + int skip; + if (Int32.TryParse(value, out skip)) + { + // We increase skip by the pageSize because that's the number of results we're returning in the current page + nextPageSkip += skip; + } + } + continue; + case "$skiptoken": + continue; + default: + key = kvp.Key; // Leave parameters that are not OData query options in initial form + break; } - if (useDefaultSkip) + if (key.Length > 0 && key[0] == '$') { - queryBuilder.AppendFormat(CultureInfo.CurrentCulture, "$skip={0}", nextPageSkip); + // $ is a legal first character in query keys + key = '$' + Uri.EscapeDataString(key.Substring(1)); } else { - queryBuilder.AppendFormat(CultureInfo.CurrentCulture, "$skiptoken={0}", skipTokenValue); + key = Uri.EscapeDataString(key); } - UriBuilder uriBuilder = new UriBuilder(requestUri) - { - Query = queryBuilder.ToString() - }; + value = Uri.EscapeDataString(value); - return uriBuilder.Uri; + queryBuilder.Append(key); + queryBuilder.Append('='); + queryBuilder.Append(value); + queryBuilder.Append('&'); } - /// This signature uses types that are AspNetCore-specific. - internal static Uri GetNextPageLink(Uri requestUri, int pageSize, object instance = null, Func objectToSkipTokenValue = null) + if (useDefaultSkip) + { + queryBuilder.AppendFormat(CultureInfo.CurrentCulture, "$skip={0}", nextPageSkip); + } + else { - Contract.Assert(requestUri != null); + queryBuilder.AppendFormat(CultureInfo.CurrentCulture, "$skiptoken={0}", skipTokenValue); + } - Dictionary queryValues = QueryHelpers.ParseQuery(requestUri.Query); - IEnumerable> queryParameters = queryValues.SelectMany( - kvp => kvp.Value, (kvp, value) => new KeyValuePair(kvp.Key, value)); + UriBuilder uriBuilder = new UriBuilder(requestUri) + { + Query = queryBuilder.ToString() + }; - return GetNextPageLink(requestUri, queryParameters, pageSize, instance, objectToSkipTokenValue); - } + return uriBuilder.Uri; + } + + /// This signature uses types that are AspNetCore-specific. + internal static Uri GetNextPageLink(Uri requestUri, int pageSize, object instance = null, Func objectToSkipTokenValue = null) + { + Contract.Assert(requestUri != null); + + Dictionary queryValues = QueryHelpers.ParseQuery(requestUri.Query); + IEnumerable> queryParameters = queryValues.SelectMany( + kvp => kvp.Value, (kvp, value) => new KeyValuePair(kvp.Key, value)); + + return GetNextPageLink(requestUri, queryParameters, pageSize, instance, objectToSkipTokenValue); } } diff --git a/src/Microsoft.AspNetCore.OData/Extensions/HttpContextExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/HttpContextExtensions.cs index 5dcc74ef0..3f19f95eb 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/HttpContextExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/HttpContextExtensions.cs @@ -10,70 +10,69 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.OData.Extensions +namespace Microsoft.AspNetCore.OData.Extensions; + +/// +/// Provides extension methods for the . +/// +public static class HttpContextExtensions { /// - /// Provides extension methods for the . + /// Return the from the . /// - public static class HttpContextExtensions + /// The instance to extend. + /// The . + public static IODataFeature ODataFeature(this HttpContext httpContext) { - /// - /// Return the from the . - /// - /// The instance to extend. - /// The . - public static IODataFeature ODataFeature(this HttpContext httpContext) + if (httpContext == null) { - if (httpContext == null) - { - throw Error.ArgumentNull(nameof(httpContext)); - } - - IODataFeature odataFeature = httpContext.Features.Get(); - if (odataFeature == null) - { - odataFeature = new ODataFeature(); - httpContext.Features.Set(odataFeature); - } - - return odataFeature; + throw Error.ArgumentNull(nameof(httpContext)); } - /// - /// Return the from the . - /// - /// The instance to extend. - /// The . - public static IODataBatchFeature ODataBatchFeature(this HttpContext httpContext) + IODataFeature odataFeature = httpContext.Features.Get(); + if (odataFeature == null) { - if (httpContext == null) - { - throw Error.ArgumentNull(nameof(httpContext)); - } + odataFeature = new ODataFeature(); + httpContext.Features.Set(odataFeature); + } - IODataBatchFeature odataBatchFeature = httpContext.Features.Get(); - if (odataBatchFeature == null) - { - odataBatchFeature = new ODataBatchFeature(); - httpContext.Features.Set(odataBatchFeature); - } + return odataFeature; + } - return odataBatchFeature; + /// + /// Return the from the . + /// + /// The instance to extend. + /// The . + public static IODataBatchFeature ODataBatchFeature(this HttpContext httpContext) + { + if (httpContext == null) + { + throw Error.ArgumentNull(nameof(httpContext)); } - /// - /// Returns the instance from the DI container. - /// - /// The instance to extend. - /// The instance from the DI container. - public static ODataOptions ODataOptions(this HttpContext httpContext) + IODataBatchFeature odataBatchFeature = httpContext.Features.Get(); + if (odataBatchFeature == null) { - if (httpContext == null) - { - throw Error.ArgumentNull(nameof(httpContext)); - } + odataBatchFeature = new ODataBatchFeature(); + httpContext.Features.Set(odataBatchFeature); + } + + return odataBatchFeature; + } - return httpContext.RequestServices?.GetService>()?.Value; + /// + /// Returns the instance from the DI container. + /// + /// The instance to extend. + /// The instance from the DI container. + public static ODataOptions ODataOptions(this HttpContext httpContext) + { + if (httpContext == null) + { + throw Error.ArgumentNull(nameof(httpContext)); } + + return httpContext.RequestServices?.GetService>()?.Value; } } diff --git a/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs index 83beb1bc1..d786ef7ec 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs @@ -19,416 +19,415 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Extensions +namespace Microsoft.AspNetCore.OData.Extensions; + +/// +/// Provides extension methods for the . +/// +public static class HttpRequestExtensions { /// - /// Provides extension methods for the . + /// Returns the from the DI container. /// - public static class HttpRequestExtensions + /// The instance to extend. + /// The from the services container. + public static IODataFeature ODataFeature(this HttpRequest request) { - /// - /// Returns the from the DI container. - /// - /// The instance to extend. - /// The from the services container. - public static IODataFeature ODataFeature(this HttpRequest request) + if (request == null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } - - return request.HttpContext.ODataFeature(); + throw Error.ArgumentNull(nameof(request)); } - /// - /// Gets the from the DI container. - /// - /// The instance to extend. - /// The from the services container. - public static IODataBatchFeature ODataBatchFeature(this HttpRequest request) - { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + return request.HttpContext.ODataFeature(); + } - return request.HttpContext.ODataBatchFeature(); + /// + /// Gets the from the DI container. + /// + /// The instance to extend. + /// The from the services container. + public static IODataBatchFeature ODataBatchFeature(this HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - /// - /// Returns the instance from the DI container. - /// - /// The instance to extend. - /// The instance from the DI container. - public static ODataOptions ODataOptions(this HttpRequest request) - { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + return request.HttpContext.ODataBatchFeature(); + } - return request.HttpContext.ODataOptions(); + /// + /// Returns the instance from the DI container. + /// + /// The instance to extend. + /// The instance from the DI container. + public static ODataOptions ODataOptions(this HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - /// - /// Gets the from the request container. - /// - /// The instance to extend. - /// The from the request container. - public static IEdmModel GetModel(this HttpRequest request) - { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + return request.HttpContext.ODataOptions(); + } - return request.ODataFeature().Model; + /// + /// Gets the from the request container. + /// + /// The instance to extend. + /// The from the request container. + public static IEdmModel GetModel(this HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - /// - /// Gets the setting. - /// - /// The instance to extend. - /// null or the time zone info. - public static TimeZoneInfo GetTimeZoneInfo(this HttpRequest request) - { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + return request.ODataFeature().Model; + } - return request.ODataOptions()?.TimeZone; + /// + /// Gets the setting. + /// + /// The instance to extend. + /// null or the time zone info. + public static TimeZoneInfo GetTimeZoneInfo(this HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - /// - /// Gets the boolean value indicating whether the non-dollar prefix query option. - /// - /// The instance to extend. - /// True/false. - public static bool IsNoDollarQueryEnable(this HttpRequest request) - { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + return request.ODataOptions()?.TimeZone; + } - return request.ODataOptions()?.EnableNoDollarQueryOptions ?? false; + /// + /// Gets the boolean value indicating whether the non-dollar prefix query option. + /// + /// The instance to extend. + /// True/false. + public static bool IsNoDollarQueryEnable(this HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - /// - /// Gets a value indicating if this is a count request. - /// - /// - public static bool IsCountRequest(this HttpRequest request) - { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + return request.ODataOptions()?.EnableNoDollarQueryOptions ?? false; + } - ODataPath path = request.ODataFeature().Path; - return path != null && path.LastSegment is CountSegment; + /// + /// Gets a value indicating if this is a count request. + /// + /// + public static bool IsCountRequest(this HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - /// - /// Gets the from the request container. - /// - /// The instance to extend. - /// The from the request container. - public static ODataMessageReaderSettings GetReaderSettings(this HttpRequest request) - { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + ODataPath path = request.ODataFeature().Path; + return path != null && path.LastSegment is CountSegment; + } - return request.GetRouteServices().GetRequiredService(); + /// + /// Gets the from the request container. + /// + /// The instance to extend. + /// The from the request container. + public static ODataMessageReaderSettings GetReaderSettings(this HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - /// - /// Gets the from the request container. - /// - /// The instance to extend. - /// The from the request container. - public static ODataMessageWriterSettings GetWriterSettings(this HttpRequest request) - { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + return request.GetRouteServices().GetRequiredService(); + } - return request.GetRouteServices().GetRequiredService(); + /// + /// Gets the from the request container. + /// + /// The instance to extend. + /// The from the request container. + public static ODataMessageWriterSettings GetWriterSettings(this HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - /// - /// get the deserializer provider associated with the request. - /// - /// The instance to extend. - /// - public static IODataDeserializerProvider GetDeserializerProvider(this HttpRequest request) + return request.GetRouteServices().GetRequiredService(); + } + + /// + /// get the deserializer provider associated with the request. + /// + /// The instance to extend. + /// + public static IODataDeserializerProvider GetDeserializerProvider(this HttpRequest request) + { + if (request == null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + throw Error.ArgumentNull(nameof(request)); + } + + return request.GetRouteServices().GetRequiredService(); + } - return request.GetRouteServices().GetRequiredService(); + /// + /// Creates a link for the next page of results; To be used as the value of @odata.nextLink. + /// + /// The instance to extend. + /// The number of results allowed per page. + /// Object which can be used to generate the skiptoken value. + /// Function that takes in the last object and returns the skiptoken value string. + /// A next page link. + public static Uri GetNextPageLink(this HttpRequest request, int pageSize, object instance, Func objectToSkipTokenValue) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - /// - /// Creates a link for the next page of results; To be used as the value of @odata.nextLink. - /// - /// The instance to extend. - /// The number of results allowed per page. - /// Object which can be used to generate the skiptoken value. - /// Function that takes in the last object and returns the skiptoken value string. - /// A next page link. - public static Uri GetNextPageLink(this HttpRequest request, int pageSize, object instance, Func objectToSkipTokenValue) + UriBuilder uriBuilder = new UriBuilder(request.Scheme, request.Host.Host) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + Path = (request.PathBase + request.Path).ToUriComponent() + }; + if (request.Host.Port.HasValue) + { + uriBuilder.Port = request.Host.Port.Value; + } - UriBuilder uriBuilder = new UriBuilder(request.Scheme, request.Host.Host) - { - Path = (request.PathBase + request.Path).ToUriComponent() - }; - if (request.Host.Port.HasValue) - { - uriBuilder.Port = request.Host.Port.Value; - } + IEnumerable> queryParameters = request.Query.SelectMany(kvp => kvp.Value, (kvp, value) => new KeyValuePair(kvp.Key, value)); + return GetNextPageHelper.GetNextPageLink(uriBuilder.Uri, queryParameters, pageSize, instance, objectToSkipTokenValue); + } - IEnumerable> queryParameters = request.Query.SelectMany(kvp => kvp.Value, (kvp, value) => new KeyValuePair(kvp.Key, value)); - return GetNextPageHelper.GetNextPageLink(uriBuilder.Uri, queryParameters, pageSize, instance, objectToSkipTokenValue); - } + /// + /// Creates an ETag from concurrency property names and values. + /// + /// The instance to extend. + /// The input property names and values. + /// The Time zone info. + /// The generated ETag string. + public static string CreateETag(this HttpRequest request, IDictionary properties, TimeZoneInfo timeZone = null) + { + return request.GetETagHandler().CreateETag(properties, timeZone)?.ToString(); + } - /// - /// Creates an ETag from concurrency property names and values. - /// - /// The instance to extend. - /// The input property names and values. - /// The Time zone info. - /// The generated ETag string. - public static string CreateETag(this HttpRequest request, IDictionary properties, TimeZoneInfo timeZone = null) + /// + /// Gets the from the services container. + /// + /// The instance to extend. + /// The from the services container. + public static IETagHandler GetETagHandler(this HttpRequest request) + { + if (request == null) { - return request.GetETagHandler().CreateETag(properties, timeZone)?.ToString(); + throw Error.ArgumentNull(nameof(request)); } - /// - /// Gets the from the services container. - /// - /// The instance to extend. - /// The from the services container. - public static IETagHandler GetETagHandler(this HttpRequest request) - { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + return request.GetRouteServices().GetService(); + } - return request.GetRouteServices().GetService(); + /// + /// Checks whether the request is a POST targeted at a resource path ending in /$query. + /// + /// The instance to extend. + /// true if the request path has $query segment. + internal static bool IsODataQueryRequest(this HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - /// - /// Checks whether the request is a POST targeted at a resource path ending in /$query. - /// - /// The instance to extend. - /// true if the request path has $query segment. - internal static bool IsODataQueryRequest(this HttpRequest request) + // Requests to paths ending in /$query MUST use the POST verb. + if (!string.Equals(request.Method, HttpMethods.Post, StringComparison.OrdinalIgnoreCase)) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + return false; + } - // Requests to paths ending in /$query MUST use the POST verb. - if (!string.Equals(request.Method, HttpMethods.Post, StringComparison.OrdinalIgnoreCase)) - { - return false; - } + string path = request.Path.Value.TrimEnd('/'); + return path.EndsWith("/$query", StringComparison.OrdinalIgnoreCase); + } - string path = request.Path.Value.TrimEnd('/'); - return path.EndsWith("/$query", StringComparison.OrdinalIgnoreCase); + /// + /// Gets the dependency injection container for the OData request. + /// + /// The instance to extend. + /// The dependency injection container. + public static IServiceProvider GetRouteServices(this HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - /// - /// Gets the dependency injection container for the OData request. - /// - /// The instance to extend. - /// The dependency injection container. - public static IServiceProvider GetRouteServices(this HttpRequest request) + IServiceProvider requestContainer = request.ODataFeature().Services; + if (requestContainer != null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + return requestContainer; + } - IServiceProvider requestContainer = request.ODataFeature().Services; - if (requestContainer != null) - { - return requestContainer; - } + // if the prefixName == null, it's a non-model scenario + if (request.ODataFeature().RoutePrefix == null) + { + return null; + } - // if the prefixName == null, it's a non-model scenario - if (request.ODataFeature().RoutePrefix == null) - { - return null; - } + // HTTP routes will not have chance to call CreateRequestContainer. We have to call it. + return request.CreateRouteServices(request.ODataFeature().RoutePrefix); + } - // HTTP routes will not have chance to call CreateRequestContainer. We have to call it. - return request.CreateRouteServices(request.ODataFeature().RoutePrefix); + /// + /// Creates a request container that associates with the . + /// + /// The instance to extend. + /// The route prefix for this request. Should match an entry in . + /// The request container created. + public static IServiceProvider CreateRouteServices(this HttpRequest request, string routePrefix) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - /// - /// Creates a request container that associates with the . - /// - /// The instance to extend. - /// The route prefix for this request. Should match an entry in . - /// The request container created. - public static IServiceProvider CreateRouteServices(this HttpRequest request, string routePrefix) + if (request.ODataFeature().Services != null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } - - if (request.ODataFeature().Services != null) - { - throw Error.InvalidOperation(SRResources.RouteServicesAlreadyExist); - } + throw Error.InvalidOperation(SRResources.RouteServicesAlreadyExist); + } - IServiceScope requestScope = request.CreateRequestScope(routePrefix); - IServiceProvider requestContainer = requestScope.ServiceProvider; + IServiceScope requestScope = request.CreateRequestScope(routePrefix); + IServiceProvider requestContainer = requestScope.ServiceProvider; - request.ODataFeature().RequestScope = requestScope; - request.ODataFeature().Services = requestContainer; + request.ODataFeature().RequestScope = requestScope; + request.ODataFeature().Services = requestContainer; - return requestContainer; - } + return requestContainer; + } - /// - /// Removes the and from - /// the and optionally disposes of the . - /// - /// The instance to extend. - /// - /// Specifies whether or not to dispose of the . Defaults to false. - /// - public static void ClearRouteServices(this HttpRequest request, bool dispose = false) + /// + /// Removes the and from + /// the and optionally disposes of the . + /// + /// The instance to extend. + /// + /// Specifies whether or not to dispose of the . Defaults to false. + /// + public static void ClearRouteServices(this HttpRequest request, bool dispose = false) + { + if (request.ODataFeature().RequestScope != null) { - if (request.ODataFeature().RequestScope != null) - { - IServiceScope requestScope = request.ODataFeature().RequestScope; - request.ODataFeature().RequestScope = null; - request.ODataFeature().Services = null; + IServiceScope requestScope = request.ODataFeature().RequestScope; + request.ODataFeature().RequestScope = null; + request.ODataFeature().Services = null; - if (dispose) - { - requestScope.Dispose(); - } + if (dispose) + { + requestScope.Dispose(); } } + } - /// - /// Create a scoped request. - /// - /// The instance to extend. - /// The route prefix for this request. Should match an entry in . - /// - private static IServiceScope CreateRequestScope(this HttpRequest request, string routePrefix) - { - ODataOptions options = request.ODataOptions(); - - IServiceProvider rootContainer = options.GetRouteServices(routePrefix); - IServiceScope scope = rootContainer.GetRequiredService().CreateScope(); - - // Bind scoping request into the OData container. - if (!string.IsNullOrEmpty(routePrefix)) - { - scope.ServiceProvider.GetRequiredService().HttpRequest = request; - } + /// + /// Create a scoped request. + /// + /// The instance to extend. + /// The route prefix for this request. Should match an entry in . + /// + private static IServiceScope CreateRequestScope(this HttpRequest request, string routePrefix) + { + ODataOptions options = request.ODataOptions(); - return scope; - } + IServiceProvider rootContainer = options.GetRouteServices(routePrefix); + IServiceScope scope = rootContainer.GetRequiredService().CreateScope(); - /// - /// Gets the OData version from the request context. - /// - /// The instance to extend. - /// The OData version. - public static ODataVersion GetODataVersion(this HttpRequest request) + // Bind scoping request into the OData container. + if (!string.IsNullOrEmpty(routePrefix)) { - return request.ODataMaxServiceVersion() ?? - request.ODataMinServiceVersion() ?? - request.ODataServiceVersion() ?? - ODataVersionConstraint.DefaultODataVersion; + scope.ServiceProvider.GetRequiredService().HttpRequest = request; } - internal static ODataQueryOptions GetQueryOptions(this HttpRequest request) - { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + return scope; + } - ODataFeature feature = request.ODataFeature() as ODataFeature; + /// + /// Gets the OData version from the request context. + /// + /// The instance to extend. + /// The OData version. + public static ODataVersion GetODataVersion(this HttpRequest request) + { + return request.ODataMaxServiceVersion() ?? + request.ODataMinServiceVersion() ?? + request.ODataServiceVersion() ?? + ODataVersionConstraint.DefaultODataVersion; + } - return feature.QueryOptions; + internal static ODataQueryOptions GetQueryOptions(this HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - internal static ODataVersion? ODataServiceVersion(this HttpRequest request) - { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + ODataFeature feature = request.ODataFeature() as ODataFeature; - return GetODataVersionFromHeader(request.Headers, ODataVersionConstraint.ODataServiceVersionHeader); - } + return feature.QueryOptions; + } - internal static ODataVersion? ODataMaxServiceVersion(this HttpRequest request) + internal static ODataVersion? ODataServiceVersion(this HttpRequest request) + { + if (request == null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } - - return GetODataVersionFromHeader(request.Headers, ODataVersionConstraint.ODataMaxServiceVersionHeader); + throw Error.ArgumentNull(nameof(request)); } - internal static ODataVersion? ODataMinServiceVersion(this HttpRequest request) + return GetODataVersionFromHeader(request.Headers, ODataVersionConstraint.ODataServiceVersionHeader); + } + + internal static ODataVersion? ODataMaxServiceVersion(this HttpRequest request) + { + if (request == null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + throw Error.ArgumentNull(nameof(request)); + } - return GetODataVersionFromHeader(request.Headers, ODataVersionConstraint.ODataMinServiceVersionHeader); + return GetODataVersionFromHeader(request.Headers, ODataVersionConstraint.ODataMaxServiceVersionHeader); + } + + internal static ODataVersion? ODataMinServiceVersion(this HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - private static ODataVersion? GetODataVersionFromHeader(IHeaderDictionary headers, string headerName) + return GetODataVersionFromHeader(request.Headers, ODataVersionConstraint.ODataMinServiceVersionHeader); + } + + private static ODataVersion? GetODataVersionFromHeader(IHeaderDictionary headers, string headerName) + { + StringValues values; + if (headers.TryGetValue(headerName, out values)) { - StringValues values; - if (headers.TryGetValue(headerName, out values)) + string value = values.FirstOrDefault(); + if (value != null) { - string value = values.FirstOrDefault(); - if (value != null) + string trimmedValue = value.Trim(' ', ';'); + try + { + return ODataUtils.StringToODataVersion(trimmedValue); + } + catch (ODataException) { - string trimmedValue = value.Trim(' ', ';'); - try - { - return ODataUtils.StringToODataVersion(trimmedValue); - } - catch (ODataException) - { - // Parsing the odata version failed. - } + // Parsing the odata version failed. } } - - return null; } + + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Extensions/HttpResponseExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/HttpResponseExtensions.cs index 71c6415a1..3e4c85fbb 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/HttpResponseExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/HttpResponseExtensions.cs @@ -7,21 +7,20 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.OData.Extensions +namespace Microsoft.AspNetCore.OData.Extensions; + +/// +/// Provides extension methods for the . +/// +public static class HttpResponseExtensions { /// - /// Provides extension methods for the . + /// Determine if the response has a success status code. /// - public static class HttpResponseExtensions + /// The response. + /// True if the response has a success status code; false otherwise. + public static bool IsSuccessStatusCode(this HttpResponse response) { - /// - /// Determine if the response has a success status code. - /// - /// The response. - /// True if the response has a success status code; false otherwise. - public static bool IsSuccessStatusCode(this HttpResponse response) - { - return response?.StatusCode >= 200 && response.StatusCode < 300; - } + return response?.StatusCode >= 200 && response.StatusCode < 300; } } diff --git a/src/Microsoft.AspNetCore.OData/Extensions/LinkGeneratorHelpers.cs b/src/Microsoft.AspNetCore.OData/Extensions/LinkGeneratorHelpers.cs index d222ffb32..5ae720221 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/LinkGeneratorHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/LinkGeneratorHelpers.cs @@ -21,142 +21,141 @@ using Microsoft.OData; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Extensions +namespace Microsoft.AspNetCore.OData.Extensions; + +/// +/// Extensions to generator the Link. +/// +public static class LinkGeneratorHelpers { /// - /// Extensions to generator the Link. + /// Generates an OData link using the request's OData route name and path handler and given segments. /// - public static class LinkGeneratorHelpers + /// The Http request. + /// The OData path segments. + /// The generated OData link. + public static string CreateODataLink(this HttpRequest request, params ODataPathSegment[] segments) { - /// - /// Generates an OData link using the request's OData route name and path handler and given segments. - /// - /// The Http request. - /// The OData path segments. - /// The generated OData link. - public static string CreateODataLink(this HttpRequest request, params ODataPathSegment[] segments) - { - return request.CreateODataLink(segments as IList); - } + return request.CreateODataLink(segments as IList); + } - /// - /// Generates an OData link using the given OData route name, path handler, and segments. - /// - /// The Http request. - /// The OData path segments. - /// The generated OData link. - public static string CreateODataLink(this HttpRequest request, IList segments) + /// + /// Generates an OData link using the given OData route name, path handler, and segments. + /// + /// The Http request. + /// The OData path segments. + /// The generated OData link. + public static string CreateODataLink(this HttpRequest request, IList segments) + { + if (request == null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + throw Error.ArgumentNull(nameof(request)); + } - IODataFeature oDataFeature = request.ODataFeature(); - string odataPath = segments.GetPathString(); + IODataFeature oDataFeature = request.ODataFeature(); + string odataPath = segments.GetPathString(); - // retrieve the cached base address - string baseAddress = oDataFeature.BaseAddress; - if (baseAddress != null) - { - return CombinePath(baseAddress, odataPath); - } + // retrieve the cached base address + string baseAddress = oDataFeature.BaseAddress; + if (baseAddress != null) + { + return CombinePath(baseAddress, odataPath); + } - // if no, calculate the base address - string uriString = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase); - string prefix = oDataFeature.RoutePrefix; - if (string.IsNullOrEmpty(prefix)) + // if no, calculate the base address + string uriString = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase); + string prefix = oDataFeature.RoutePrefix; + if (string.IsNullOrEmpty(prefix)) + { + baseAddress = uriString; + } + else + { + // Construct the prefix template if it's a template + RoutePattern routePattern = RoutePatternFactory.Parse(prefix); + if (!routePattern.Parameters.Any()) { - baseAddress = uriString; + baseAddress = CombinePath(uriString, prefix); } else { - // Construct the prefix template if it's a template - RoutePattern routePattern = RoutePatternFactory.Parse(prefix); - if (!routePattern.Parameters.Any()) + if (TryProcessPrefixTemplate(request, routePattern, out var path)) { - baseAddress = CombinePath(uriString, prefix); + baseAddress = CombinePath(uriString, path); } else { - if (TryProcessPrefixTemplate(request, routePattern, out var path)) - { - baseAddress = CombinePath(uriString, path); - } - else - { - throw new ODataException(Error.Format(SRResources.CannotProcessPrefixTemplate, prefix)); - } + throw new ODataException(Error.Format(SRResources.CannotProcessPrefixTemplate, prefix)); } } - - // cache the base address - oDataFeature.BaseAddress = baseAddress; - return CombinePath(baseAddress, odataPath); } - private static bool TryProcessPrefixTemplate(HttpRequest request, RoutePattern routePattern, out string path) - { - // TODO: Do you have a better way to process the prefix template? - Contract.Assert(request != null); - Contract.Assert(routePattern != null); + // cache the base address + oDataFeature.BaseAddress = baseAddress; + return CombinePath(baseAddress, odataPath); + } - HttpContext httpContext = request.HttpContext; - TemplateBinderFactory factory = request.HttpContext.RequestServices.GetRequiredService(); - TemplateBinder templateBinder = factory.Create(routePattern); + private static bool TryProcessPrefixTemplate(HttpRequest request, RoutePattern routePattern, out string path) + { + // TODO: Do you have a better way to process the prefix template? + Contract.Assert(request != null); + Contract.Assert(routePattern != null); - RouteValueDictionary ambientValues = GetAmbientValues(httpContext); + HttpContext httpContext = request.HttpContext; + TemplateBinderFactory factory = request.HttpContext.RequestServices.GetRequiredService(); + TemplateBinder templateBinder = factory.Create(routePattern); - var templateValuesResult = templateBinder.GetValues(ambientValues, request.RouteValues); - if (templateValuesResult == null) - { - // We're missing one of the required values for this route. - path = default; - return false; - } + RouteValueDictionary ambientValues = GetAmbientValues(httpContext); - if (!templateBinder.TryProcessConstraints(httpContext, templateValuesResult.CombinedValues, out var _, out var _)) - { - path = default; - return false; - } + var templateValuesResult = templateBinder.GetValues(ambientValues, request.RouteValues); + if (templateValuesResult == null) + { + // We're missing one of the required values for this route. + path = default; + return false; + } - path = templateBinder.BindValues(templateValuesResult.AcceptedValues); - int index = path.IndexOf("?", StringComparison.Ordinal); // remove the query string + if (!templateBinder.TryProcessConstraints(httpContext, templateValuesResult.CombinedValues, out var _, out var _)) + { + path = default; + return false; + } - if (index >= 0) - { - path = path.Substring(0, index); - } + path = templateBinder.BindValues(templateValuesResult.AcceptedValues); + int index = path.IndexOf("?", StringComparison.Ordinal); // remove the query string - return true; + if (index >= 0) + { + path = path.Substring(0, index); } - private static RouteValueDictionary GetAmbientValues(HttpContext httpContext) + return true; + } + + private static RouteValueDictionary GetAmbientValues(HttpContext httpContext) + { + return httpContext?.Features.Get()?.RouteValues; + } + + private static string CombinePath(string baseAddress, string path) + { + if (string.IsNullOrEmpty(path)) { - return httpContext?.Features.Get()?.RouteValues; + return baseAddress; } - private static string CombinePath(string baseAddress, string path) + if (path.StartsWith("/", StringComparison.Ordinal)) { - if (string.IsNullOrEmpty(path)) - { - return baseAddress; - } - - if (path.StartsWith("/", StringComparison.Ordinal)) - { - path = path.Substring(1); // remove the first "/" - } + path = path.Substring(1); // remove the first "/" + } - if (baseAddress.EndsWith("/", StringComparison.Ordinal)) - { - return baseAddress + path; - } - else - { - return $"{baseAddress}/{path}"; - } + if (baseAddress.EndsWith("/", StringComparison.Ordinal)) + { + return baseAddress + path; + } + else + { + return $"{baseAddress}/{path}"; } } } diff --git a/src/Microsoft.AspNetCore.OData/Extensions/RequestPreferenceHelpers.cs b/src/Microsoft.AspNetCore.OData/Extensions/RequestPreferenceHelpers.cs index b1597a3ef..8d7a81a93 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/RequestPreferenceHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/RequestPreferenceHelpers.cs @@ -11,96 +11,95 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.OData.Extensions +namespace Microsoft.AspNetCore.OData.Extensions; + +internal static class RequestPreferenceHelpers { - internal static class RequestPreferenceHelpers - { - public const string PreferHeaderName = "Prefer"; - public const string ReturnContentHeaderValue = "return=representation"; - public const string ReturnNoContentHeaderValue = "return=minimal"; - public const string ODataMaxPageSize = "odata.maxpagesize"; - public const string MaxPageSize = "maxpagesize"; + public const string PreferHeaderName = "Prefer"; + public const string ReturnContentHeaderValue = "return=representation"; + public const string ReturnNoContentHeaderValue = "return=minimal"; + public const string ODataMaxPageSize = "odata.maxpagesize"; + public const string MaxPageSize = "maxpagesize"; - internal static bool RequestPrefersReturnContent(IHeaderDictionary headers) + internal static bool RequestPrefersReturnContent(IHeaderDictionary headers) + { + StringValues preferences; + if (headers.TryGetValue(PreferHeaderName, out preferences)) { - StringValues preferences; - if (headers.TryGetValue(PreferHeaderName, out preferences)) - { - return (preferences.FirstOrDefault(s => s.IndexOf(ReturnContentHeaderValue, StringComparison.OrdinalIgnoreCase) >= 0) != null); - } - return false; + return (preferences.FirstOrDefault(s => s.IndexOf(ReturnContentHeaderValue, StringComparison.OrdinalIgnoreCase) >= 0) != null); } + return false; + } - internal static bool RequestPrefersReturnNoContent(IHeaderDictionary headers) + internal static bool RequestPrefersReturnNoContent(IHeaderDictionary headers) + { + StringValues preferences; + if (headers.TryGetValue(PreferHeaderName, out preferences)) { - StringValues preferences; - if (headers.TryGetValue(PreferHeaderName, out preferences)) - { - return (preferences.FirstOrDefault(s => s.IndexOf(ReturnNoContentHeaderValue, StringComparison.OrdinalIgnoreCase) >= 0) != null); - } - return false; + return (preferences.FirstOrDefault(s => s.IndexOf(ReturnNoContentHeaderValue, StringComparison.OrdinalIgnoreCase) >= 0) != null); } + return false; + } - internal static bool RequestPrefersMaxPageSize(IHeaderDictionary headers, out int pageSize) + internal static bool RequestPrefersMaxPageSize(IHeaderDictionary headers, out int pageSize) + { + pageSize = -1; + StringValues preferences; + if (headers.TryGetValue(PreferHeaderName, out preferences)) { - pageSize = -1; - StringValues preferences; - if (headers.TryGetValue(PreferHeaderName, out preferences)) + pageSize = GetMaxPageSize(preferences, MaxPageSize); + if (pageSize >= 0) { - pageSize = GetMaxPageSize(preferences, MaxPageSize); - if (pageSize >= 0) - { - return true; - } - //maxpagesize supersedes odata.maxpagesize - pageSize = GetMaxPageSize(preferences, ODataMaxPageSize); - if (pageSize >= 0) - { - return true; - } + return true; + } + //maxpagesize supersedes odata.maxpagesize + pageSize = GetMaxPageSize(preferences, ODataMaxPageSize); + if (pageSize >= 0) + { + return true; } - - return false; } - private static int GetMaxPageSize(IEnumerable preferences, string preferenceHeaderName) + return false; + } + + private static int GetMaxPageSize(IEnumerable preferences, string preferenceHeaderName) + { + const int Failed = -1; + string maxPageSize = preferences.FirstOrDefault(s => s.IndexOf(preferenceHeaderName, StringComparison.OrdinalIgnoreCase) >= 0); + if (String.IsNullOrEmpty(maxPageSize)) { - const int Failed = -1; - string maxPageSize = preferences.FirstOrDefault(s => s.IndexOf(preferenceHeaderName, StringComparison.OrdinalIgnoreCase) >= 0); - if (String.IsNullOrEmpty(maxPageSize)) - { - return Failed; - } - else + return Failed; + } + else + { + int index = maxPageSize.IndexOf(preferenceHeaderName, StringComparison.OrdinalIgnoreCase) + preferenceHeaderName.Length; + String value = String.Empty; + if (maxPageSize[index++] == '=') { - int index = maxPageSize.IndexOf(preferenceHeaderName, StringComparison.OrdinalIgnoreCase) + preferenceHeaderName.Length; - String value = String.Empty; - if (maxPageSize[index++] == '=') - { - while (index < maxPageSize.Length && Char.IsDigit(maxPageSize[index])) - { - value += maxPageSize[index++]; - } - } - int pageSize = -1; - if (Int32.TryParse(value, out pageSize)) + while (index < maxPageSize.Length && Char.IsDigit(maxPageSize[index])) { - return pageSize; + value += maxPageSize[index++]; } } - return Failed; - } - - internal static string GetRequestPreferHeader(IHeaderDictionary headers) - { - StringValues values; - if (headers.TryGetValue(PreferHeaderName, out values)) + int pageSize = -1; + if (Int32.TryParse(value, out pageSize)) { - // If there are many "Prefer" headers, pick up the first one. - return values.FirstOrDefault(); + return pageSize; } + } + return Failed; + } - return null; + internal static string GetRequestPreferHeader(IHeaderDictionary headers) + { + StringValues values; + if (headers.TryGetValue(PreferHeaderName, out values)) + { + // If there are many "Prefer" headers, pick up the first one. + return values.FirstOrDefault(); } + + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Extensions/SerializableErrorExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/SerializableErrorExtensions.cs index 6fc00214b..44b86483f 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/SerializableErrorExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/SerializableErrorExtensions.cs @@ -16,240 +16,239 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Extensions +namespace Microsoft.AspNetCore.OData.Extensions; + +/// +/// Provides extension methods for the class. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class SerializableErrorExtensions { /// - /// Provides extension methods for the class. + /// Converts the to an . /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static class SerializableErrorExtensions + /// The instance to convert. + /// The converted + public static ODataError CreateODataError(this SerializableError serializableError) { - /// - /// Converts the to an . - /// - /// The instance to convert. - /// The converted - public static ODataError CreateODataError(this SerializableError serializableError) + if (serializableError == null) { - if (serializableError == null) - { - throw Error.ArgumentNull(nameof(serializableError)); - } - - //Clone for removal of handled entries - var errors = serializableError.ToDictionary(pair => pair.Key, pair => pair.Value); - - var innerError = errors.ToODataInnerError(); - - string errorCode = errors.GetPropertyValue(SerializableErrorKeys.ErrorCodeKey); - string message = errors.GetPropertyValue(SerializableErrorKeys.MessageKey); - - errors.Remove(SerializableErrorKeys.ErrorCodeKey); - errors.Remove(SerializableErrorKeys.MessageKey); - - return new ODataError - { - Code = string.IsNullOrWhiteSpace(errorCode) ? null : errorCode, - Message = string.IsNullOrWhiteSpace(message) ? errors.ConvertModelStateErrors() : message, - Details = errors.CreateErrorDetails(), - InnerError = innerError - }; + throw Error.ArgumentNull(nameof(serializableError)); } - private static ODataInnerError ToODataInnerError(this Dictionary errors) - { - string innerErrorMessage = errors.GetPropertyValue(SerializableErrorKeys.ExceptionMessageKey); + //Clone for removal of handled entries + var errors = serializableError.ToDictionary(pair => pair.Key, pair => pair.Value); - if (innerErrorMessage == null) - { - string messageDetail = errors.GetPropertyValue(SerializableErrorKeys.MessageDetailKey); + var innerError = errors.ToODataInnerError(); - if (messageDetail == null) - { - SerializableError modelStateError = errors.GetPropertyValue(SerializableErrorKeys.ModelStateKey); + string errorCode = errors.GetPropertyValue(SerializableErrorKeys.ErrorCodeKey); + string message = errors.GetPropertyValue(SerializableErrorKeys.MessageKey); - errors.Remove(SerializableErrorKeys.ModelStateKey); + errors.Remove(SerializableErrorKeys.ErrorCodeKey); + errors.Remove(SerializableErrorKeys.MessageKey); - return (modelStateError == null) ? null - : new ODataInnerError( - new Dictionary - { - { - SerializableErrorKeys.MessageKey, new ODataPrimitiveValue(ConvertModelStateErrors(modelStateError)) - } - }); - } - - errors.Remove(SerializableErrorKeys.MessageDetailKey); - - return new ODataInnerError(new Dictionary { { SerializableErrorKeys.MessageKey, new ODataPrimitiveValue(messageDetail) } }); - } + return new ODataError + { + Code = string.IsNullOrWhiteSpace(errorCode) ? null : errorCode, + Message = string.IsNullOrWhiteSpace(message) ? errors.ConvertModelStateErrors() : message, + Details = errors.CreateErrorDetails(), + InnerError = innerError + }; + } - errors.Remove(SerializableErrorKeys.ExceptionMessageKey); + private static ODataInnerError ToODataInnerError(this Dictionary errors) + { + string innerErrorMessage = errors.GetPropertyValue(SerializableErrorKeys.ExceptionMessageKey); - string typeName = errors.GetPropertyValue(SerializableErrorKeys.ExceptionTypeKey); - string stackTrace = errors.GetPropertyValue(SerializableErrorKeys.StackTraceKey); + if (innerErrorMessage == null) + { + string messageDetail = errors.GetPropertyValue(SerializableErrorKeys.MessageDetailKey); - Dictionary properties = new Dictionary + if (messageDetail == null) { - { SerializableErrorKeys.MessageKey, new ODataPrimitiveValue(innerErrorMessage) } - }; + SerializableError modelStateError = errors.GetPropertyValue(SerializableErrorKeys.ModelStateKey); - if (typeName != null) - { - properties.Add(SerializableErrorKeys.ExceptionTypeKey, new ODataPrimitiveValue(typeName)); - } + errors.Remove(SerializableErrorKeys.ModelStateKey); - if (stackTrace != null) - { - properties.Add(SerializableErrorKeys.StackTraceKey, new ODataPrimitiveValue(stackTrace)); + return (modelStateError == null) ? null + : new ODataInnerError( + new Dictionary + { + { + SerializableErrorKeys.MessageKey, new ODataPrimitiveValue(ConvertModelStateErrors(modelStateError)) + } + }); } - ODataInnerError innerError = new ODataInnerError(properties); + errors.Remove(SerializableErrorKeys.MessageDetailKey); - errors.Remove(SerializableErrorKeys.ExceptionTypeKey); - errors.Remove(SerializableErrorKeys.StackTraceKey); + return new ODataInnerError(new Dictionary { { SerializableErrorKeys.MessageKey, new ODataPrimitiveValue(messageDetail) } }); + } - SerializableError innerExceptionError = errors.GetPropertyValue(SerializableErrorKeys.InnerExceptionKey); + errors.Remove(SerializableErrorKeys.ExceptionMessageKey); - errors.Remove(SerializableErrorKeys.InnerExceptionKey); + string typeName = errors.GetPropertyValue(SerializableErrorKeys.ExceptionTypeKey); + string stackTrace = errors.GetPropertyValue(SerializableErrorKeys.StackTraceKey); - if (innerExceptionError != null) - { - innerError.InnerError = ToODataInnerError(innerExceptionError); - } + Dictionary properties = new Dictionary + { + { SerializableErrorKeys.MessageKey, new ODataPrimitiveValue(innerErrorMessage) } + }; - return innerError; + if (typeName != null) + { + properties.Add(SerializableErrorKeys.ExceptionTypeKey, new ODataPrimitiveValue(typeName)); } - // Convert the model state errors in to a string (for debugging only). - // This should be improved once ODataError allows more details. - [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "The default format provider is fine here.")] - private static string ConvertModelStateErrors(this IReadOnlyDictionary errors) + if (stackTrace != null) { - StringBuilder builder = new StringBuilder(); - - foreach (KeyValuePair modelStateError in errors.Where(kvp => kvp.Value != null)) - { - if (builder.Length > 0) - { - builder.AppendLine(); - } + properties.Add(SerializableErrorKeys.StackTraceKey, new ODataPrimitiveValue(stackTrace)); + } - if (!string.IsNullOrWhiteSpace(modelStateError.Key)) - { - builder.AppendLine($"{modelStateError.Key}:"); - } + ODataInnerError innerError = new ODataInnerError(properties); - IEnumerable errorMessages = modelStateError.Value as IEnumerable; - if (errorMessages != null) - { - foreach (string errorMessage in errorMessages) - { - builder.AppendLine(errorMessage); - } - } - else - { - builder.AppendLine(modelStateError.Value.ToString()); - } - } + errors.Remove(SerializableErrorKeys.ExceptionTypeKey); + errors.Remove(SerializableErrorKeys.StackTraceKey); - var result = builder.ToString(); + SerializableError innerExceptionError = errors.GetPropertyValue(SerializableErrorKeys.InnerExceptionKey); - return !result.EndsWith(Environment.NewLine, StringComparison.Ordinal) ? result : result.Substring(0, result.Length - Environment.NewLine.Length); - } + errors.Remove(SerializableErrorKeys.InnerExceptionKey); - private static ICollection CreateErrorDetails(this IReadOnlyDictionary errors) + if (innerExceptionError != null) { - return errors.SelectMany(CreateErrorDetails).ToList(); + innerError.InnerError = ToODataInnerError(innerExceptionError); } - private static IEnumerable CreateErrorDetails(KeyValuePair pair) + return innerError; + } + + // Convert the model state errors in to a string (for debugging only). + // This should be improved once ODataError allows more details. + [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "The default format provider is fine here.")] + private static string ConvertModelStateErrors(this IReadOnlyDictionary errors) + { + StringBuilder builder = new StringBuilder(); + + foreach (KeyValuePair modelStateError in errors.Where(kvp => kvp.Value != null)) { - var errors = pair.Value as IEnumerable; + if (builder.Length > 0) + { + builder.AppendLine(); + } - if (errors != null) + if (!string.IsNullOrWhiteSpace(modelStateError.Key)) { - return errors.Select(error => new ODataErrorDetail - { - Target = string.IsNullOrWhiteSpace(pair.Key) ? null : pair.Key, - Message = error - }); + builder.AppendLine($"{modelStateError.Key}:"); } - return new[] + IEnumerable errorMessages = modelStateError.Value as IEnumerable; + if (errorMessages != null) { - new ODataErrorDetail + foreach (string errorMessage in errorMessages) { - Target = pair.Key, - Message = pair.Value?.ToString() + builder.AppendLine(errorMessage); } - }; + } + else + { + builder.AppendLine(modelStateError.Value.ToString()); + } } - private static TValue GetPropertyValue(this IReadOnlyDictionary error, string errorKey) + var result = builder.ToString(); + + return !result.EndsWith(Environment.NewLine, StringComparison.Ordinal) ? result : result.Substring(0, result.Length - Environment.NewLine.Length); + } + + private static ICollection CreateErrorDetails(this IReadOnlyDictionary errors) + { + return errors.SelectMany(CreateErrorDetails).ToList(); + } + + private static IEnumerable CreateErrorDetails(KeyValuePair pair) + { + var errors = pair.Value as IEnumerable; + + if (errors != null) { - object value; + return errors.Select(error => new ODataErrorDetail + { + Target = string.IsNullOrWhiteSpace(pair.Key) ? null : pair.Key, + Message = error + }); + } - if (error.TryGetValue(errorKey, out value) && value is TValue) + return new[] + { + new ODataErrorDetail { - return (TValue)value; + Target = pair.Key, + Message = pair.Value?.ToString() } + }; + } + + private static TValue GetPropertyValue(this IReadOnlyDictionary error, string errorKey) + { + object value; - return default(TValue); + if (error.TryGetValue(errorKey, out value) && value is TValue) + { + return (TValue)value; } + + return default(TValue); } +} +/// +/// Different keys for adding entries to an instance so +/// that it can be parsed to a instance +/// +public static class SerializableErrorKeys +{ /// - /// Different keys for adding entries to an instance so - /// that it can be parsed to a instance + /// Provides a key for the Message. /// - public static class SerializableErrorKeys - { - /// - /// Provides a key for the Message. - /// - public static readonly string MessageKey = "message"; - - /// - /// Provides a key for the MessageDetail. - /// - public static readonly string MessageDetailKey = "MessageDetail"; - - /// - /// Provides a key for the ModelState. - /// - public static readonly string ModelStateKey = "ModelState"; - - /// - /// Provides a key for the ExceptionMessage. - /// - public static readonly string ExceptionMessageKey = "ExceptionMessage"; - - /// - /// Provides a key for the ExceptionType. - /// - public static readonly string ExceptionTypeKey = "type"; - - /// - /// Provides a key for the StackTrace. - /// - public static readonly string StackTraceKey = "stacktrace"; - - /// - /// Provides a key for the InnerException. - /// - public static readonly string InnerExceptionKey = "InnerException"; - - /// - /// Provides a key for the MessageLanguage. - /// - public static readonly string MessageLanguageKey = "MessageLanguage"; - - /// - /// Provides a key for the ErrorCode. - /// - public static readonly string ErrorCodeKey = "ErrorCode"; - } + public static readonly string MessageKey = "message"; + + /// + /// Provides a key for the MessageDetail. + /// + public static readonly string MessageDetailKey = "MessageDetail"; + + /// + /// Provides a key for the ModelState. + /// + public static readonly string ModelStateKey = "ModelState"; + + /// + /// Provides a key for the ExceptionMessage. + /// + public static readonly string ExceptionMessageKey = "ExceptionMessage"; + + /// + /// Provides a key for the ExceptionType. + /// + public static readonly string ExceptionTypeKey = "type"; + + /// + /// Provides a key for the StackTrace. + /// + public static readonly string StackTraceKey = "stacktrace"; + + /// + /// Provides a key for the InnerException. + /// + public static readonly string InnerExceptionKey = "InnerException"; + + /// + /// Provides a key for the MessageLanguage. + /// + public static readonly string MessageLanguageKey = "MessageLanguage"; + + /// + /// Provides a key for the ErrorCode. + /// + public static readonly string ErrorCodeKey = "ErrorCode"; } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Attributes/NonValidatingParameterBindingAttribute.cs b/src/Microsoft.AspNetCore.OData/Formatter/Attributes/NonValidatingParameterBindingAttribute.cs index 09fc91d26..986c42185 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Attributes/NonValidatingParameterBindingAttribute.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Attributes/NonValidatingParameterBindingAttribute.cs @@ -10,27 +10,26 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; -namespace Microsoft.AspNetCore.OData.Abstracts +namespace Microsoft.AspNetCore.OData.Abstracts; + +/// +/// An attribute to disable model validation for a particular type. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public class NonValidatingParameterBindingAttribute : ModelBinderAttribute, IPropertyValidationFilter { - /// - /// An attribute to disable model validation for a particular type. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class NonValidatingParameterBindingAttribute : ModelBinderAttribute, IPropertyValidationFilter + /// + public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry) { - /// - public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry) - { - return false; - } + return false; + } - /// - public override BindingSource BindingSource + /// + public override BindingSource BindingSource + { + get { - get - { - return BindingSource.Body; - } + return BindingSource.Body; } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ContentIdHelpers.cs b/src/Microsoft.AspNetCore.OData/Formatter/ContentIdHelpers.cs index e3cd3232b..6f5314ec6 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ContentIdHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ContentIdHelpers.cs @@ -9,68 +9,67 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +internal static class ContentIdHelpers { - internal static class ContentIdHelpers + public static string ResolveContentId(string url, IDictionary contentIdToLocationMapping) { - public static string ResolveContentId(string url, IDictionary contentIdToLocationMapping) - { - Contract.Assert(url != null); - Contract.Assert(contentIdToLocationMapping != null); + Contract.Assert(url != null); + Contract.Assert(contentIdToLocationMapping != null); - int startIndex = 0; + int startIndex = 0; - while (true) + while (true) + { + startIndex = url.IndexOf('$', startIndex); + + if (startIndex == -1) { - startIndex = url.IndexOf('$', startIndex); + break; + } - if (startIndex == -1) - { - break; - } + int keyLength = 0; - int keyLength = 0; + while (startIndex + keyLength < url.Length - 1 && IsContentIdCharacter(url[startIndex + keyLength + 1])) + { + keyLength++; + } - while (startIndex + keyLength < url.Length - 1 && IsContentIdCharacter(url[startIndex + keyLength + 1])) - { - keyLength++; - } + if (keyLength > 0) + { + // Might have matched a $ alias. + string locationKey = url.Substring(startIndex + 1, keyLength); + string locationValue; - if (keyLength > 0) + if (contentIdToLocationMapping.TryGetValue(locationKey, out locationValue)) { - // Might have matched a $ alias. - string locationKey = url.Substring(startIndex + 1, keyLength); - string locationValue; - - if (contentIdToLocationMapping.TryGetValue(locationKey, out locationValue)) - { - // As location headers MUST be absolute URL's, we can ignore everything - // before the $content-id while resolving it. - return locationValue + url.Substring(startIndex + 1 + keyLength); - } + // As location headers MUST be absolute URL's, we can ignore everything + // before the $content-id while resolving it. + return locationValue + url.Substring(startIndex + 1 + keyLength); } - - startIndex++; } - return url; + startIndex++; } - private static bool IsContentIdCharacter(char c) + return url; + } + + private static bool IsContentIdCharacter(char c) + { + // According to the OData ABNF grammar, Content-IDs follow the scheme. + // content-id = "Content-ID" ":" OWS 1*unreserved + // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + switch (c) { - // According to the OData ABNF grammar, Content-IDs follow the scheme. - // content-id = "Content-ID" ":" OWS 1*unreserved - // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - switch (c) - { - case '-': - case '.': - case '_': - case '~': - return true; - default: - return Char.IsLetterOrDigit(c); - } + case '-': + case '.': + case '_': + case '~': + return true; + default: + return Char.IsLetterOrDigit(c); } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ConventionsHelpers.cs b/src/Microsoft.AspNetCore.OData/Formatter/ConventionsHelpers.cs index 137a1c34b..bd1b2434a 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ConventionsHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ConventionsHelpers.cs @@ -17,140 +17,139 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +internal static class ConventionsHelpers { - internal static class ConventionsHelpers + public static IEnumerable> GetEntityKey(ResourceContext resourceContext) { - public static IEnumerable> GetEntityKey(ResourceContext resourceContext) + Contract.Assert(resourceContext != null); + Contract.Assert(resourceContext.StructuredType != null); + Contract.Assert(resourceContext.EdmObject != null); + + IEdmEntityType entityType = resourceContext.StructuredType as IEdmEntityType; + if (entityType == null) { - Contract.Assert(resourceContext != null); - Contract.Assert(resourceContext.StructuredType != null); - Contract.Assert(resourceContext.EdmObject != null); - - IEdmEntityType entityType = resourceContext.StructuredType as IEdmEntityType; - if (entityType == null) - { - return Enumerable.Empty>(); - } - - IEnumerable keys = entityType.Key(); - return keys.Select(k => new KeyValuePair(k.Name, GetKeyValue(k, resourceContext))); + return Enumerable.Empty>(); } - private static object GetKeyValue(IEdmProperty key, ResourceContext resourceContext) - { - Contract.Assert(key != null); - Contract.Assert(resourceContext != null); + IEnumerable keys = entityType.Key(); + return keys.Select(k => new KeyValuePair(k.Name, GetKeyValue(k, resourceContext))); + } - object value = resourceContext.GetPropertyValue(key.Name); - if (value == null) - { - IEdmTypeReference edmType = resourceContext.EdmObject.GetEdmType(); - throw Error.InvalidOperation(SRResources.KeyValueCannotBeNull, key.Name, edmType.Definition); - } + private static object GetKeyValue(IEdmProperty key, ResourceContext resourceContext) + { + Contract.Assert(key != null); + Contract.Assert(resourceContext != null); - return ConvertValue(value, resourceContext.TimeZone, resourceContext.EdmModel); + object value = resourceContext.GetPropertyValue(key.Name); + if (value == null) + { + IEdmTypeReference edmType = resourceContext.EdmObject.GetEdmType(); + throw Error.InvalidOperation(SRResources.KeyValueCannotBeNull, key.Name, edmType.Definition); } - public static object ConvertValue(object value, TimeZoneInfo timeZone, IEdmModel model) + return ConvertValue(value, resourceContext.TimeZone, resourceContext.EdmModel); + } + + public static object ConvertValue(object value, TimeZoneInfo timeZone, IEdmModel model) + { + Contract.Assert(value != null); + + Type type = value.GetType(); + if (TypeHelper.IsEnum(type)) { - Contract.Assert(value != null); - - Type type = value.GetType(); - if (TypeHelper.IsEnum(type)) - { - value = new ODataEnumValue(value.ToString(), type.EdmFullName()); - } - else - { - Contract.Assert(model.GetEdmPrimitiveTypeReference(type) != null); - value = ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(value, timeZone); - } - - return value; + value = new ODataEnumValue(value.ToString(), type.EdmFullName()); } - - public static string GetEntityKeyValue(ResourceContext resourceContext) + else { - Contract.Assert(resourceContext != null); - Contract.Assert(resourceContext.StructuredType != null); - Contract.Assert(resourceContext.EdmObject != null); - - IEdmEntityType entityType = resourceContext.StructuredType as IEdmEntityType; - if (entityType == null) - { - return string.Empty; - } - - IEnumerable keys = entityType.Key(); - if (keys.Count() == 1) - { - return GetUriRepresentationForKeyValue(keys.First(), resourceContext); - } - else - { - IEnumerable keyValues = - keys.Select(key => string.Format( - CultureInfo.InvariantCulture, "{0}={1}", key.Name, GetUriRepresentationForKeyValue(key, resourceContext))); - return string.Join(",", keyValues); - } + Contract.Assert(model.GetEdmPrimitiveTypeReference(type) != null); + value = ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(value, timeZone); } - // gets the primitive odata uri representation. - public static string GetUriRepresentationForValue(object value) + return value; + } + + public static string GetEntityKeyValue(ResourceContext resourceContext) + { + Contract.Assert(resourceContext != null); + Contract.Assert(resourceContext.StructuredType != null); + Contract.Assert(resourceContext.EdmObject != null); + + IEdmEntityType entityType = resourceContext.StructuredType as IEdmEntityType; + if (entityType == null) { - return GetUriRepresentationForValue(value, TimeZoneInfo.Local); + return string.Empty; } - public static string GetUriRepresentationForValue(object value, TimeZoneInfo timeZone) + IEnumerable keys = entityType.Key(); + if (keys.Count() == 1) { - Contract.Assert(value != null); - - Type type = value.GetType(); - if (TypeHelper.IsEnum(type)) - { - value = new ODataEnumValue(value.ToString(), type.EdmFullName()); - } - else - { - value = ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(value, timeZone); - } - - return ODataUriUtils.ConvertToUriLiteral(value, ODataVersion.V4); + return GetUriRepresentationForKeyValue(keys.First(), resourceContext); } - - private static string GetUriRepresentationForKeyValue(IEdmProperty key, ResourceContext resourceContext) + else { - Contract.Assert(key != null); - Contract.Assert(resourceContext != null); + IEnumerable keyValues = + keys.Select(key => string.Format( + CultureInfo.InvariantCulture, "{0}={1}", key.Name, GetUriRepresentationForKeyValue(key, resourceContext))); + return string.Join(",", keyValues); + } + } - object value = resourceContext.GetPropertyValue(key.Name); - if (value == null) - { - IEdmTypeReference edmType = resourceContext.EdmObject.GetEdmType(); - throw Error.InvalidOperation(SRResources.KeyValueCannotBeNull, key.Name, edmType.Definition); - } + // gets the primitive odata uri representation. + public static string GetUriRepresentationForValue(object value) + { + return GetUriRepresentationForValue(value, TimeZoneInfo.Local); + } + + public static string GetUriRepresentationForValue(object value, TimeZoneInfo timeZone) + { + Contract.Assert(value != null); - return GetUriRepresentationForValue(value, resourceContext.TimeZone); + Type type = value.GetType(); + if (TypeHelper.IsEnum(type)) + { + value = new ODataEnumValue(value.ToString(), type.EdmFullName()); } + else + { + value = ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(value, timeZone); + } + + return ODataUriUtils.ConvertToUriLiteral(value, ODataVersion.V4); + } + + private static string GetUriRepresentationForKeyValue(IEdmProperty key, ResourceContext resourceContext) + { + Contract.Assert(key != null); + Contract.Assert(resourceContext != null); - private class PropertyEqualityComparer : IEqualityComparer + object value = resourceContext.GetPropertyValue(key.Name); + if (value == null) { - public static PropertyEqualityComparer Instance = new PropertyEqualityComparer(); + IEdmTypeReference edmType = resourceContext.EdmObject.GetEdmType(); + throw Error.InvalidOperation(SRResources.KeyValueCannotBeNull, key.Name, edmType.Definition); + } - public bool Equals(PropertyInfo x, PropertyInfo y) - { - Contract.Assert(x != null); - Contract.Assert(y != null); + return GetUriRepresentationForValue(value, resourceContext.TimeZone); + } - return x.Name == y.Name; - } + private class PropertyEqualityComparer : IEqualityComparer + { + public static PropertyEqualityComparer Instance = new PropertyEqualityComparer(); - public int GetHashCode(PropertyInfo obj) - { - Contract.Assert(obj != null); - return obj.Name.GetHashCode(StringComparison.Ordinal); - } + public bool Equals(PropertyInfo x, PropertyInfo y) + { + Contract.Assert(x != null); + Contract.Assert(y != null); + + return x.Name == y.Name; + } + + public int GetHashCode(PropertyInfo obj) + { + Contract.Assert(obj != null); + return obj.Name.GetHashCode(StringComparison.Ordinal); } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/DefaultODataETagHandler.cs b/src/Microsoft.AspNetCore.OData/Formatter/DefaultODataETagHandler.cs index 6eb72db4f..fc338856a 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/DefaultODataETagHandler.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/DefaultODataETagHandler.cs @@ -13,85 +13,84 @@ using Microsoft.Net.Http.Headers; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +internal class DefaultODataETagHandler : IETagHandler { - internal class DefaultODataETagHandler : IETagHandler + /// null literal that needs to be return in ETag value when the value is null + private const string NullLiteralInETag = "null"; + + private const char Separator = ','; + + public EntityTagHeaderValue CreateETag(IDictionary properties, TimeZoneInfo timeZoneInfo = null) { - /// null literal that needs to be return in ETag value when the value is null - private const string NullLiteralInETag = "null"; + if (properties == null) + { + throw Error.ArgumentNull(nameof(properties)); + } - private const char Separator = ','; + if (properties.Count == 0) + { + return null; + } + + StringBuilder builder = new StringBuilder(); + builder.Append('\"'); + bool firstProperty = true; - public EntityTagHeaderValue CreateETag(IDictionary properties, TimeZoneInfo timeZoneInfo = null) + foreach (object propertyValue in properties.Values) { - if (properties == null) + if (firstProperty) { - throw Error.ArgumentNull(nameof(properties)); + firstProperty = false; } - - if (properties.Count == 0) + else { - return null; + builder.Append(Separator); } - StringBuilder builder = new StringBuilder(); - builder.Append('\"'); - bool firstProperty = true; - - foreach (object propertyValue in properties.Values) - { - if (firstProperty) - { - firstProperty = false; - } - else - { - builder.Append(Separator); - } - - string str = propertyValue == null - ? NullLiteralInETag - : ConventionsHelpers.GetUriRepresentationForValue(propertyValue, timeZoneInfo); - - // base64 encode - byte[] bytes = Encoding.UTF8.GetBytes(str); - string etagValueText = Convert.ToBase64String(bytes); - builder.Append(etagValueText); - } + string str = propertyValue == null + ? NullLiteralInETag + : ConventionsHelpers.GetUriRepresentationForValue(propertyValue, timeZoneInfo); - builder.Append('\"'); - string tag = builder.ToString(); - return new EntityTagHeaderValue(tag, isWeak: true); + // base64 encode + byte[] bytes = Encoding.UTF8.GetBytes(str); + string etagValueText = Convert.ToBase64String(bytes); + builder.Append(etagValueText); } - public IDictionary ParseETag(EntityTagHeaderValue etagHeaderValue) + builder.Append('\"'); + string tag = builder.ToString(); + return new EntityTagHeaderValue(tag, isWeak: true); + } + + public IDictionary ParseETag(EntityTagHeaderValue etagHeaderValue) + { + if (etagHeaderValue == null) { - if (etagHeaderValue == null) - { - throw Error.ArgumentNull(nameof(etagHeaderValue)); - } + throw Error.ArgumentNull(nameof(etagHeaderValue)); + } + + string tag = etagHeaderValue.Tag.ToString().Trim('\"'); - string tag = etagHeaderValue.Tag.ToString().Trim('\"'); + // split etag + string[] rawValues = tag.Split(Separator); + IDictionary properties = new Dictionary(); + for (int index = 0; index < rawValues.Length; index++) + { + string rawValue = rawValues[index]; - // split etag - string[] rawValues = tag.Split(Separator); - IDictionary properties = new Dictionary(); - for (int index = 0; index < rawValues.Length; index++) + // base64 decode + byte[] bytes = Convert.FromBase64String(rawValue); + string valueString = Encoding.UTF8.GetString(bytes); + object obj = ODataUriUtils.ConvertFromUriLiteral(valueString, ODataVersion.V4); + if (obj is ODataNullValue) { - string rawValue = rawValues[index]; - - // base64 decode - byte[] bytes = Convert.FromBase64String(rawValue); - string valueString = Encoding.UTF8.GetString(bytes); - object obj = ODataUriUtils.ConvertFromUriLiteral(valueString, ODataVersion.V4); - if (obj is ODataNullValue) - { - obj = null; - } - properties.Add(index.ToString(CultureInfo.InvariantCulture), obj); + obj = null; } - - return properties; + properties.Add(index.ToString(CultureInfo.InvariantCulture), obj); } + + return properties; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/CollectionDeserializationHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/CollectionDeserializationHelper.cs index a139df107..791a3c16f 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/CollectionDeserializationHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/CollectionDeserializationHelper.cs @@ -16,162 +16,161 @@ using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +internal static class CollectionDeserializationHelpers { - internal static class CollectionDeserializationHelpers - { - private static readonly Type[] _emptyTypeArray = Array.Empty(); - private static readonly object[] _emptyObjectArray = Array.Empty(); - private static readonly MethodInfo _toArrayMethodInfo = typeof(Enumerable).GetMethod("ToArray"); + private static readonly Type[] _emptyTypeArray = Array.Empty(); + private static readonly object[] _emptyObjectArray = Array.Empty(); + private static readonly MethodInfo _toArrayMethodInfo = typeof(Enumerable).GetMethod("ToArray"); - public static void AddToCollection(this IEnumerable items, IEnumerable collection, Type elementType, - Type resourceType, string propertyName, Type propertyType, ODataDeserializerContext context = null) - { - Contract.Assert(items != null); - Contract.Assert(collection != null); - Contract.Assert(elementType != null); - Contract.Assert(resourceType != null); - Contract.Assert(propertyName != null); - Contract.Assert(propertyType != null); + public static void AddToCollection(this IEnumerable items, IEnumerable collection, Type elementType, + Type resourceType, string propertyName, Type propertyType, ODataDeserializerContext context = null) + { + Contract.Assert(items != null); + Contract.Assert(collection != null); + Contract.Assert(elementType != null); + Contract.Assert(resourceType != null); + Contract.Assert(propertyName != null); + Contract.Assert(propertyType != null); - MethodInfo addMethod = null; - IList list = collection as IList; + MethodInfo addMethod = null; + IList list = collection as IList; - if (list == null) - { - addMethod = collection.GetType().GetMethod("Add", new Type[] { elementType }); - if (addMethod == null) - { - string message = Error.Format(SRResources.CollectionShouldHaveAddMethod, propertyType.FullName, propertyName, resourceType.FullName); - throw new SerializationException(message); - } - } - else if (list.GetType().IsArray) + if (list == null) + { + addMethod = collection.GetType().GetMethod("Add", new Type[] { elementType }); + if (addMethod == null) { - string message = Error.Format(SRResources.GetOnlyCollectionCannotBeArray, propertyName, resourceType.FullName); + string message = Error.Format(SRResources.CollectionShouldHaveAddMethod, propertyType.FullName, propertyName, resourceType.FullName); throw new SerializationException(message); } - - items.AddToCollectionCore(collection, elementType, list, addMethod, context); } - - public static void AddToCollection(this IEnumerable items, IEnumerable collection, Type elementType, string paramName, Type paramType, ODataDeserializerContext context = null) + else if (list.GetType().IsArray) { - Contract.Assert(items != null); - Contract.Assert(collection != null); - Contract.Assert(elementType != null); - Contract.Assert(paramType != null); + string message = Error.Format(SRResources.GetOnlyCollectionCannotBeArray, propertyName, resourceType.FullName); + throw new SerializationException(message); + } - MethodInfo addMethod = null; - IList list = collection as IList; + items.AddToCollectionCore(collection, elementType, list, addMethod, context); + } - if (list == null) - { - addMethod = collection.GetType().GetMethod("Add", new Type[] { elementType }); - if (addMethod == null) - { - string message = Error.Format(SRResources.CollectionParameterShouldHaveAddMethod, paramType, paramName); - throw new SerializationException(message); - } - } + public static void AddToCollection(this IEnumerable items, IEnumerable collection, Type elementType, string paramName, Type paramType, ODataDeserializerContext context = null) + { + Contract.Assert(items != null); + Contract.Assert(collection != null); + Contract.Assert(elementType != null); + Contract.Assert(paramType != null); - items.AddToCollectionCore(collection, elementType, list, addMethod, context); - } + MethodInfo addMethod = null; + IList list = collection as IList; - private static void AddToCollectionCore(this IEnumerable items, IEnumerable collection, Type elementType, IList list, MethodInfo addMethod, ODataDeserializerContext context = null) + if (list == null) { - IEdmModel model = context?.Model; - bool isNonstandardEdmPrimitiveCollection; - model.IsNonstandardEdmPrimitive(elementType, out isNonstandardEdmPrimitiveCollection); - - foreach (object item in items) + addMethod = collection.GetType().GetMethod("Add", new Type[] { elementType }); + if (addMethod == null) { - object element = item; - - if (isNonstandardEdmPrimitiveCollection && element != null) - { - // convert non-standard edm primitives if required. - element = EdmPrimitiveHelper.ConvertPrimitiveValue(element, elementType, context?.TimeZone); - } - - if (list != null) - { - list.Add(element); - } - else - { - Contract.Assert(addMethod != null); - addMethod.Invoke(collection, new object[] { element }); - } + string message = Error.Format(SRResources.CollectionParameterShouldHaveAddMethod, paramType, paramName); + throw new SerializationException(message); } } - public static void Clear(this IEnumerable collection, string propertyName, Type resourceType) - { - Contract.Assert(collection != null); - - MethodInfo clearMethod = collection.GetType().GetMethod("Clear", _emptyTypeArray); - if (clearMethod == null) - { - string message = Error.Format(SRResources.CollectionShouldHaveClearMethod, collection.GetType().FullName, - propertyName, resourceType.FullName); - throw new SerializationException(message); - } + items.AddToCollectionCore(collection, elementType, list, addMethod, context); + } - clearMethod.Invoke(collection, _emptyObjectArray); - } + private static void AddToCollectionCore(this IEnumerable items, IEnumerable collection, Type elementType, IList list, MethodInfo addMethod, ODataDeserializerContext context = null) + { + IEdmModel model = context?.Model; + bool isNonstandardEdmPrimitiveCollection; + model.IsNonstandardEdmPrimitive(elementType, out isNonstandardEdmPrimitiveCollection); - public static bool TryCreateInstance(Type collectionType, IEdmCollectionTypeReference edmCollectionType, Type elementType, out IEnumerable instance) + foreach (object item in items) { - Contract.Assert(collectionType != null); + object element = item; - if (collectionType == typeof(EdmComplexObjectCollection)) + if (isNonstandardEdmPrimitiveCollection && element != null) { - instance = new EdmComplexObjectCollection(edmCollectionType); - return true; + // convert non-standard edm primitives if required. + element = EdmPrimitiveHelper.ConvertPrimitiveValue(element, elementType, context?.TimeZone); } - else if (collectionType == typeof(EdmEntityObjectCollection)) + + if (list != null) { - instance = new EdmEntityObjectCollection(edmCollectionType); - return true; + list.Add(element); } - else if (collectionType == typeof(EdmEnumObjectCollection)) + else { - instance = new EdmEnumObjectCollection(edmCollectionType); - return true; - } - else if (collectionType.IsGenericType) - { - Type genericDefinition = collectionType.GetGenericTypeDefinition(); - if (genericDefinition == typeof(IEnumerable<>) || - genericDefinition == typeof(ICollection<>) || - genericDefinition == typeof(IList<>)) - { - instance = Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType)) as IEnumerable; - return true; - } + Contract.Assert(addMethod != null); + addMethod.Invoke(collection, new object[] { element }); } + } + } - if (collectionType.IsArray) - { - // We don't know the size of the collection in advance. So, create a list and later call ToArray. - instance = Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType)) as IEnumerable; - return true; - } + public static void Clear(this IEnumerable collection, string propertyName, Type resourceType) + { + Contract.Assert(collection != null); + + MethodInfo clearMethod = collection.GetType().GetMethod("Clear", _emptyTypeArray); + if (clearMethod == null) + { + string message = Error.Format(SRResources.CollectionShouldHaveClearMethod, collection.GetType().FullName, + propertyName, resourceType.FullName); + throw new SerializationException(message); + } + + clearMethod.Invoke(collection, _emptyObjectArray); + } + + public static bool TryCreateInstance(Type collectionType, IEdmCollectionTypeReference edmCollectionType, Type elementType, out IEnumerable instance) + { + Contract.Assert(collectionType != null); - if (collectionType.GetConstructor(Type.EmptyTypes) != null && !collectionType.IsAbstract) + if (collectionType == typeof(EdmComplexObjectCollection)) + { + instance = new EdmComplexObjectCollection(edmCollectionType); + return true; + } + else if (collectionType == typeof(EdmEntityObjectCollection)) + { + instance = new EdmEntityObjectCollection(edmCollectionType); + return true; + } + else if (collectionType == typeof(EdmEnumObjectCollection)) + { + instance = new EdmEnumObjectCollection(edmCollectionType); + return true; + } + else if (collectionType.IsGenericType) + { + Type genericDefinition = collectionType.GetGenericTypeDefinition(); + if (genericDefinition == typeof(IEnumerable<>) || + genericDefinition == typeof(ICollection<>) || + genericDefinition == typeof(IList<>)) { - instance = Activator.CreateInstance(collectionType) as IEnumerable; + instance = Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType)) as IEnumerable; return true; } + } - instance = null; - return false; + if (collectionType.IsArray) + { + // We don't know the size of the collection in advance. So, create a list and later call ToArray. + instance = Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType)) as IEnumerable; + return true; } - public static IEnumerable ToArray(IEnumerable value, Type elementType) + if (collectionType.GetConstructor(Type.EmptyTypes) != null && !collectionType.IsAbstract) { - return _toArrayMethodInfo.MakeGenericMethod(elementType).Invoke(null, new object[] { value }) as IEnumerable; + instance = Activator.CreateInstance(collectionType) as IEnumerable; + return true; } + + instance = null; + return false; + } + + public static IEnumerable ToArray(IEnumerable value, Type elementType) + { + return _toArrayMethodInfo.MakeGenericMethod(elementType).Invoke(null, new object[] { value }) as IEnumerable; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/DeserializationHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/DeserializationHelper.cs index bb5c29dbf..9210f7d26 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/DeserializationHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/DeserializationHelper.cs @@ -19,469 +19,468 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +internal static class DeserializationHelpers { - internal static class DeserializationHelpers + internal static void ApplyProperty(ODataProperty property, IEdmStructuredTypeReference resourceType, object resource, + IODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext) { - internal static void ApplyProperty(ODataProperty property, IEdmStructuredTypeReference resourceType, object resource, - IODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext) - { - IEdmStructuredType structuredType = resourceType.StructuredDefinition(); - IEdmProperty edmProperty = structuredType == null ? null : structuredType.ResolveProperty(property.Name); + IEdmStructuredType structuredType = resourceType.StructuredDefinition(); + IEdmProperty edmProperty = structuredType == null ? null : structuredType.ResolveProperty(property.Name); - bool isDynamicProperty = false; - string propertyName = property.Name; - if (edmProperty != null) - { - propertyName = readContext.Model.GetClrPropertyName(edmProperty); - } - else - { - isDynamicProperty = structuredType != null && structuredType.IsOpen; - } + bool isDynamicProperty = false; + string propertyName = property.Name; + if (edmProperty != null) + { + propertyName = readContext.Model.GetClrPropertyName(edmProperty); + } + else + { + isDynamicProperty = structuredType != null && structuredType.IsOpen; + } - if (!isDynamicProperty && edmProperty == null) - { - throw new ODataException( - Error.Format(SRResources.CannotDeserializeUnknownProperty, property.Name, resourceType.Definition)); - } + if (!isDynamicProperty && edmProperty == null) + { + throw new ODataException( + Error.Format(SRResources.CannotDeserializeUnknownProperty, property.Name, resourceType.Definition)); + } - // dynamic properties have null values - IEdmTypeReference propertyType = edmProperty != null ? edmProperty.Type : null; + // dynamic properties have null values + IEdmTypeReference propertyType = edmProperty != null ? edmProperty.Type : null; - EdmTypeKind propertyKind; - object value = ConvertValue(property.Value, ref propertyType, deserializerProvider, readContext, - out propertyKind); + EdmTypeKind propertyKind; + object value = ConvertValue(property.Value, ref propertyType, deserializerProvider, readContext, + out propertyKind); - if (isDynamicProperty) - { - SetDynamicProperty(resource, resourceType, propertyKind, propertyName, value, propertyType, - readContext.Model); - } - else - { - SetDeclaredProperty(resource, propertyKind, propertyName, value, edmProperty, readContext); - } + if (isDynamicProperty) + { + SetDynamicProperty(resource, resourceType, propertyKind, propertyName, value, propertyType, + readContext.Model); } - - internal static void SetDynamicProperty(object resource, IEdmStructuredTypeReference resourceType, - EdmTypeKind propertyKind, string propertyName, object propertyValue, IEdmTypeReference propertyType, - IEdmModel model) + else { - if (propertyKind == EdmTypeKind.Collection && propertyValue.GetType() != typeof(EdmComplexObjectCollection) - && propertyValue.GetType() != typeof(EdmEnumObjectCollection)) - { - SetDynamicCollectionProperty(resource, propertyName, propertyValue, propertyType.AsCollection(), - resourceType?.StructuredDefinition(), model); - } - else - { - SetDynamicProperty(resource, propertyName, propertyValue, resourceType.StructuredDefinition(), - model); - } + SetDeclaredProperty(resource, propertyKind, propertyName, value, edmProperty, readContext); } + } - internal static void SetDeclaredProperty(object resource, EdmTypeKind propertyKind, string propertyName, - object propertyValue, IEdmProperty edmProperty, ODataDeserializerContext readContext) + internal static void SetDynamicProperty(object resource, IEdmStructuredTypeReference resourceType, + EdmTypeKind propertyKind, string propertyName, object propertyValue, IEdmTypeReference propertyType, + IEdmModel model) + { + if (propertyKind == EdmTypeKind.Collection && propertyValue.GetType() != typeof(EdmComplexObjectCollection) + && propertyValue.GetType() != typeof(EdmEnumObjectCollection)) { - if (propertyKind == EdmTypeKind.Collection) - { - SetCollectionProperty(resource, edmProperty, propertyValue, propertyName); - } - else - { - if (!readContext.IsNoClrType) - { - if (propertyKind == EdmTypeKind.Primitive) - { - propertyValue = EdmPrimitiveHelper.ConvertPrimitiveValue(propertyValue, - GetPropertyType(resource, propertyName), readContext.TimeZone); - } - } - - SetProperty(resource, propertyName, propertyValue); - } + SetDynamicCollectionProperty(resource, propertyName, propertyValue, propertyType.AsCollection(), + resourceType?.StructuredDefinition(), model); } - - internal static void SetCollectionProperty(object resource, IEdmProperty edmProperty, object value, string propertyName, ODataDeserializerContext context = null) + else { - Contract.Assert(edmProperty != null); - - SetCollectionProperty(resource, propertyName, edmProperty.Type.AsCollection(), value, clearCollection: false, context: context); + SetDynamicProperty(resource, propertyName, propertyValue, resourceType.StructuredDefinition(), + model); } + } - internal static void SetCollectionProperty(object resource, string propertyName, - IEdmCollectionTypeReference edmPropertyType, object value, bool clearCollection, ODataDeserializerContext context = null) + internal static void SetDeclaredProperty(object resource, EdmTypeKind propertyKind, string propertyName, + object propertyValue, IEdmProperty edmProperty, ODataDeserializerContext readContext) + { + if (propertyKind == EdmTypeKind.Collection) { - if (value != null) + SetCollectionProperty(resource, edmProperty, propertyValue, propertyName); + } + else + { + if (!readContext.IsNoClrType) { - // If the setting value is a delta set, we don't need to create a new collection, just use it. - if (value is IDeltaSet set) + if (propertyKind == EdmTypeKind.Primitive) { - SetProperty(resource, propertyName, set); - return; + propertyValue = EdmPrimitiveHelper.ConvertPrimitiveValue(propertyValue, + GetPropertyType(resource, propertyName), readContext.TimeZone); } + } - IEnumerable collection = value as IEnumerable; - Contract.Assert(collection != null, - "SetCollectionProperty is always passed the result of ODataFeedDeserializer or ODataCollectionDeserializer"); - - Type resourceType = resource.GetType(); - Type propertyType = GetPropertyType(resource, propertyName); - - if (propertyType == typeof(object)) - { - SetProperty(resource, propertyName, collection); - return; - } + SetProperty(resource, propertyName, propertyValue); + } + } - Type elementType; - if (!TypeHelper.IsCollection(propertyType, out elementType)) - { - string message = Error.Format(SRResources.PropertyIsNotCollection, propertyType.FullName, propertyName, resourceType.FullName); - throw new SerializationException(message); - } + internal static void SetCollectionProperty(object resource, IEdmProperty edmProperty, object value, string propertyName, ODataDeserializerContext context = null) + { + Contract.Assert(edmProperty != null); - IEnumerable newCollection; - if (CanSetProperty(resource, propertyName) && - CollectionDeserializationHelpers.TryCreateInstance(propertyType, edmPropertyType, elementType, out newCollection)) - { - // settable collections - collection.AddToCollection(newCollection, elementType, resourceType, propertyName, propertyType, context); - if (propertyType.IsArray) - { - newCollection = CollectionDeserializationHelpers.ToArray(newCollection, elementType); - } - - SetProperty(resource, propertyName, newCollection); - } - else - { - // get-only collections. - newCollection = GetProperty(resource, propertyName) as IEnumerable; - if (newCollection == null) - { - string message = Error.Format(SRResources.CannotAddToNullCollection, propertyName, resourceType.FullName); - throw new SerializationException(message); - } - - if (clearCollection) - { - newCollection.Clear(propertyName, resourceType); - } - - collection.AddToCollection(newCollection, elementType, resourceType, propertyName, propertyType, context); - } - } - } + SetCollectionProperty(resource, propertyName, edmProperty.Type.AsCollection(), value, clearCollection: false, context: context); + } - internal static void SetDynamicCollectionProperty(object resource, string propertyName, object value, - IEdmCollectionTypeReference edmPropertyType, IEdmStructuredType structuredType, - IEdmModel model) + internal static void SetCollectionProperty(object resource, string propertyName, + IEdmCollectionTypeReference edmPropertyType, object value, bool clearCollection, ODataDeserializerContext context = null) + { + if (value != null) { - Contract.Assert(value != null); - Contract.Assert(model != null); + // If the setting value is a delta set, we don't need to create a new collection, just use it. + if (value is IDeltaSet set) + { + SetProperty(resource, propertyName, set); + return; + } IEnumerable collection = value as IEnumerable; - Contract.Assert(collection != null); + Contract.Assert(collection != null, + "SetCollectionProperty is always passed the result of ODataFeedDeserializer or ODataCollectionDeserializer"); Type resourceType = resource.GetType(); - Type elementType; - Type propertyType; - if (edmPropertyType.ElementType().IsUntyped()) - { - elementType = typeof(object); - propertyType = typeof(IList); + Type propertyType = GetPropertyType(resource, propertyName); - SetDynamicProperty(resource, propertyName, value, structuredType, model); + if (propertyType == typeof(object)) + { + SetProperty(resource, propertyName, collection); return; } - else + + Type elementType; + if (!TypeHelper.IsCollection(propertyType, out elementType)) { - elementType = model.GetClrType(edmPropertyType.ElementType()); - propertyType = typeof(ICollection<>).MakeGenericType(elementType); + string message = Error.Format(SRResources.PropertyIsNotCollection, propertyType.FullName, propertyName, resourceType.FullName); + throw new SerializationException(message); } IEnumerable newCollection; - if (CollectionDeserializationHelpers.TryCreateInstance(propertyType, edmPropertyType, elementType, - out newCollection)) + if (CanSetProperty(resource, propertyName) && + CollectionDeserializationHelpers.TryCreateInstance(propertyType, edmPropertyType, elementType, out newCollection)) { - collection.AddToCollection(newCollection, elementType, resourceType, propertyName, propertyType); - SetDynamicProperty(resource, propertyName, newCollection, structuredType, model); - } - } + // settable collections + collection.AddToCollection(newCollection, elementType, resourceType, propertyName, propertyType, context); + if (propertyType.IsArray) + { + newCollection = CollectionDeserializationHelpers.ToArray(newCollection, elementType); + } - internal static void SetProperty(object resource, string propertyName, object value) - { - IDelta delta = resource as IDelta; - if (delta == null) - { - resource.GetType().GetProperty(propertyName).SetValue(resource, value, index: null); + SetProperty(resource, propertyName, newCollection); } else { - delta.TrySetPropertyValue(propertyName, value); - } - } - - internal static void SetDynamicProperty(object resource, string propertyName, object value, - IEdmStructuredType structuredType, IEdmModel model) - { - IDelta delta = resource as IDelta; - if (delta != null) - { - delta.TrySetPropertyValue(propertyName, value); - } - // else if (resource is ODataObject oObject) - else if (resource is EdmUntypedObject oObject) - { - oObject[propertyName] = value; - } - else - { - PropertyInfo propertyInfo = model.GetDynamicPropertyDictionary(structuredType); - if (propertyInfo == null) - { - return; - } - - IDictionary dynamicPropertyDictionary; - object dynamicDictionaryObject = propertyInfo.GetValue(resource); - if (dynamicDictionaryObject == null) + // get-only collections. + newCollection = GetProperty(resource, propertyName) as IEnumerable; + if (newCollection == null) { - if (!propertyInfo.CanWrite) - { - throw Error.InvalidOperation(SRResources.CannotSetDynamicPropertyDictionary, propertyName, - resource.GetType().FullName); - } - - dynamicPropertyDictionary = new Dictionary(); - propertyInfo.SetValue(resource, dynamicPropertyDictionary); - } - else - { - dynamicPropertyDictionary = (IDictionary)dynamicDictionaryObject; + string message = Error.Format(SRResources.CannotAddToNullCollection, propertyName, resourceType.FullName); + throw new SerializationException(message); } - if (dynamicPropertyDictionary.ContainsKey(propertyName)) + if (clearCollection) { - throw Error.InvalidOperation(SRResources.DuplicateDynamicPropertyNameFound, - propertyName, structuredType.FullTypeName()); + newCollection.Clear(propertyName, resourceType); } - dynamicPropertyDictionary.Add(propertyName, value); + collection.AddToCollection(newCollection, elementType, resourceType, propertyName, propertyType, context); } } + } + + internal static void SetDynamicCollectionProperty(object resource, string propertyName, object value, + IEdmCollectionTypeReference edmPropertyType, IEdmStructuredType structuredType, + IEdmModel model) + { + Contract.Assert(value != null); + Contract.Assert(model != null); + + IEnumerable collection = value as IEnumerable; + Contract.Assert(collection != null); - internal static object ConvertValue(object oDataValue, ref IEdmTypeReference propertyType, IODataDeserializerProvider deserializerProvider, - ODataDeserializerContext readContext, out EdmTypeKind typeKind) + Type resourceType = resource.GetType(); + Type elementType; + Type propertyType; + if (edmPropertyType.ElementType().IsUntyped()) { - if (oDataValue == null) - { - typeKind = EdmTypeKind.None; - return null; - } + elementType = typeof(object); + propertyType = typeof(IList); - ODataEnumValue enumValue = oDataValue as ODataEnumValue; - if (enumValue != null) - { - typeKind = EdmTypeKind.Enum; - return ConvertEnumValue(enumValue, ref propertyType, deserializerProvider, readContext); - } + SetDynamicProperty(resource, propertyName, value, structuredType, model); + return; + } + else + { + elementType = model.GetClrType(edmPropertyType.ElementType()); + propertyType = typeof(ICollection<>).MakeGenericType(elementType); + } - ODataCollectionValue collection = oDataValue as ODataCollectionValue; - if (collection != null) + IEnumerable newCollection; + if (CollectionDeserializationHelpers.TryCreateInstance(propertyType, edmPropertyType, elementType, + out newCollection)) + { + collection.AddToCollection(newCollection, elementType, resourceType, propertyName, propertyType); + SetDynamicProperty(resource, propertyName, newCollection, structuredType, model); + } + } + + internal static void SetProperty(object resource, string propertyName, object value) + { + IDelta delta = resource as IDelta; + if (delta == null) + { + resource.GetType().GetProperty(propertyName).SetValue(resource, value, index: null); + } + else + { + delta.TrySetPropertyValue(propertyName, value); + } + } + + internal static void SetDynamicProperty(object resource, string propertyName, object value, + IEdmStructuredType structuredType, IEdmModel model) + { + IDelta delta = resource as IDelta; + if (delta != null) + { + delta.TrySetPropertyValue(propertyName, value); + } + // else if (resource is ODataObject oObject) + else if (resource is EdmUntypedObject oObject) + { + oObject[propertyName] = value; + } + else + { + PropertyInfo propertyInfo = model.GetDynamicPropertyDictionary(structuredType); + if (propertyInfo == null) { - typeKind = EdmTypeKind.Collection; - return ConvertCollectionValue(collection, ref propertyType, deserializerProvider, readContext); + return; } - ODataUntypedValue untypedValue = oDataValue as ODataUntypedValue; - if (untypedValue != null) + IDictionary dynamicPropertyDictionary; + object dynamicDictionaryObject = propertyInfo.GetValue(resource); + if (dynamicDictionaryObject == null) { - Contract.Assert(!String.IsNullOrEmpty(untypedValue.RawValue)); - - if (untypedValue.RawValue.StartsWith("[", StringComparison.Ordinal) || - untypedValue.RawValue.StartsWith("{", StringComparison.Ordinal)) + if (!propertyInfo.CanWrite) { - throw new ODataException(Error.Format(SRResources.InvalidODataUntypedValue, untypedValue.RawValue)); + throw Error.InvalidOperation(SRResources.CannotSetDynamicPropertyDictionary, propertyName, + resource.GetType().FullName); } - oDataValue = ConvertPrimitiveValue(untypedValue.RawValue); + dynamicPropertyDictionary = new Dictionary(); + propertyInfo.SetValue(resource, dynamicPropertyDictionary); } - - typeKind = EdmTypeKind.Primitive; - return oDataValue; - } - - internal static Type GetPropertyType(object resource, string propertyName) - { - Contract.Assert(resource != null); - Contract.Assert(propertyName != null); - - IDelta delta = resource as IDelta; - if (delta != null) + else { - Type type; - delta.TryGetPropertyType(propertyName, out type); - return type; + dynamicPropertyDictionary = (IDictionary)dynamicDictionaryObject; } - else + + if (dynamicPropertyDictionary.ContainsKey(propertyName)) { - PropertyInfo property = resource.GetType().GetProperty(propertyName); - return property == null ? null : property.PropertyType; + throw Error.InvalidOperation(SRResources.DuplicateDynamicPropertyNameFound, + propertyName, structuredType.FullTypeName()); } + + dynamicPropertyDictionary.Add(propertyName, value); } + } - private static bool CanSetProperty(object resource, string propertyName) + internal static object ConvertValue(object oDataValue, ref IEdmTypeReference propertyType, IODataDeserializerProvider deserializerProvider, + ODataDeserializerContext readContext, out EdmTypeKind typeKind) + { + if (oDataValue == null) { - IDelta delta = resource as IDelta; - if (delta != null) - { - return true; - } - else - { - PropertyInfo property = resource.GetType().GetProperty(propertyName); - return property != null && property.GetSetMethod() != null; - } + typeKind = EdmTypeKind.None; + return null; } - private static object GetProperty(object resource, string propertyName) + ODataEnumValue enumValue = oDataValue as ODataEnumValue; + if (enumValue != null) { - IDelta delta = resource as IDelta; - if (delta != null) - { - object value; - delta.TryGetPropertyValue(propertyName, out value); - return value; - } - else - { - PropertyInfo property = resource.GetType().GetProperty(propertyName); - Contract.Assert(property != null, "ODataLib should have already verified that the property exists on the type."); - return property.GetValue(resource, index: null); - } + typeKind = EdmTypeKind.Enum; + return ConvertEnumValue(enumValue, ref propertyType, deserializerProvider, readContext); } - private static object ConvertCollectionValue(ODataCollectionValue collection, - ref IEdmTypeReference propertyType, IODataDeserializerProvider deserializerProvider, - ODataDeserializerContext readContext) + ODataCollectionValue collection = oDataValue as ODataCollectionValue; + if (collection != null) { - // Be noted: If a declared property (propertyType != null) is untyped (or collection), - // It should be never come here. Because for collection untyped, it goes to nested resource set. - // ODL reads the value as ODataResourceSet in a ODataNestedResourceInfo. - // So, if it comes here, the untyped value is odata.type annotated. for example, create a ODataProperty using ODataCollectionValue - IEdmCollectionTypeReference collectionType; - if (propertyType == null || propertyType.IsUntyped()) - { - // dynamic collection property or untyped value @odata.type annotated - Contract.Assert(!String.IsNullOrEmpty(collection.TypeName), - "ODataLib should have verified that dynamic collection value has a type name " + - "since we provided metadata."); - - string elementTypeName = GetCollectionElementTypeName(collection.TypeName, isNested: false); - IEdmModel model = readContext.Model; - IEdmSchemaType elementType = model.FindType(elementTypeName); - Contract.Assert(elementType != null); - collectionType = - new EdmCollectionTypeReference( - new EdmCollectionType(elementType.ToEdmTypeReference(isNullable: false))); - propertyType = collectionType; - } - else + typeKind = EdmTypeKind.Collection; + return ConvertCollectionValue(collection, ref propertyType, deserializerProvider, readContext); + } + + ODataUntypedValue untypedValue = oDataValue as ODataUntypedValue; + if (untypedValue != null) + { + Contract.Assert(!String.IsNullOrEmpty(untypedValue.RawValue)); + + if (untypedValue.RawValue.StartsWith("[", StringComparison.Ordinal) || + untypedValue.RawValue.StartsWith("{", StringComparison.Ordinal)) { - collectionType = propertyType as IEdmCollectionTypeReference; - Contract.Assert(collectionType != null, "The type for collection must be a IEdmCollectionType."); + throw new ODataException(Error.Format(SRResources.InvalidODataUntypedValue, untypedValue.RawValue)); } - IODataEdmTypeDeserializer deserializer = deserializerProvider.GetEdmTypeDeserializer(collectionType); - return deserializer.ReadInline(collection, collectionType, readContext); + oDataValue = ConvertPrimitiveValue(untypedValue.RawValue); } - private static object ConvertPrimitiveValue(string value) + typeKind = EdmTypeKind.Primitive; + return oDataValue; + } + + internal static Type GetPropertyType(object resource, string propertyName) + { + Contract.Assert(resource != null); + Contract.Assert(propertyName != null); + + IDelta delta = resource as IDelta; + if (delta != null) { - if (String.CompareOrdinal(value, "null") == 0) - { - return null; - } + Type type; + delta.TryGetPropertyType(propertyName, out type); + return type; + } + else + { + PropertyInfo property = resource.GetType().GetProperty(propertyName); + return property == null ? null : property.PropertyType; + } + } - if (Int32.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out int intValue)) - { - return intValue; - } + private static bool CanSetProperty(object resource, string propertyName) + { + IDelta delta = resource as IDelta; + if (delta != null) + { + return true; + } + else + { + PropertyInfo property = resource.GetType().GetProperty(propertyName); + return property != null && property.GetSetMethod() != null; + } + } - // Todo: if it is Ieee754Compatible, parse decimal after double - if (Decimal.TryParse(value, NumberStyles.Number, NumberFormatInfo.InvariantInfo, out decimal decimalValue)) - { - return decimalValue; - } + private static object GetProperty(object resource, string propertyName) + { + IDelta delta = resource as IDelta; + if (delta != null) + { + object value; + delta.TryGetPropertyValue(propertyName, out value); + return value; + } + else + { + PropertyInfo property = resource.GetType().GetProperty(propertyName); + Contract.Assert(property != null, "ODataLib should have already verified that the property exists on the type."); + return property.GetValue(resource, index: null); + } + } - if (Double.TryParse(value, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out double doubleValue)) - { - return doubleValue; - } + private static object ConvertCollectionValue(ODataCollectionValue collection, + ref IEdmTypeReference propertyType, IODataDeserializerProvider deserializerProvider, + ODataDeserializerContext readContext) + { + // Be noted: If a declared property (propertyType != null) is untyped (or collection), + // It should be never come here. Because for collection untyped, it goes to nested resource set. + // ODL reads the value as ODataResourceSet in a ODataNestedResourceInfo. + // So, if it comes here, the untyped value is odata.type annotated. for example, create a ODataProperty using ODataCollectionValue + IEdmCollectionTypeReference collectionType; + if (propertyType == null || propertyType.IsUntyped()) + { + // dynamic collection property or untyped value @odata.type annotated + Contract.Assert(!String.IsNullOrEmpty(collection.TypeName), + "ODataLib should have verified that dynamic collection value has a type name " + + "since we provided metadata."); + + string elementTypeName = GetCollectionElementTypeName(collection.TypeName, isNested: false); + IEdmModel model = readContext.Model; + IEdmSchemaType elementType = model.FindType(elementTypeName); + Contract.Assert(elementType != null); + collectionType = + new EdmCollectionTypeReference( + new EdmCollectionType(elementType.ToEdmTypeReference(isNullable: false))); + propertyType = collectionType; + } + else + { + collectionType = propertyType as IEdmCollectionTypeReference; + Contract.Assert(collectionType != null, "The type for collection must be a IEdmCollectionType."); + } - if (!value.StartsWith("\"", StringComparison.Ordinal) || !value.EndsWith("\"", StringComparison.Ordinal)) - { - throw new ODataException(Error.Format(SRResources.InvalidODataUntypedValue, value)); - } + IODataEdmTypeDeserializer deserializer = deserializerProvider.GetEdmTypeDeserializer(collectionType); + return deserializer.ReadInline(collection, collectionType, readContext); + } - return value.Substring(1, value.Length - 2); + private static object ConvertPrimitiveValue(string value) + { + if (String.CompareOrdinal(value, "null") == 0) + { + return null; } - private static object ConvertEnumValue(ODataEnumValue enumValue, ref IEdmTypeReference propertyType, - IODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext) + if (Int32.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out int intValue)) { - IEdmEnumTypeReference edmEnumType; - if (propertyType == null || propertyType.IsUntyped()) - { - // dynamic enum property or untyped property - Contract.Assert(!String.IsNullOrEmpty(enumValue.TypeName), - "ODataLib should have verified that dynamic enum value has a type name since we provided metadata."); - IEdmModel model = readContext.Model; - IEdmType edmType = model.FindType(enumValue.TypeName); - Contract.Assert(edmType.TypeKind == EdmTypeKind.Enum, "ODataLib should have verified that enum value has a enum resource type."); - edmEnumType = new EdmEnumTypeReference(edmType as IEdmEnumType, isNullable: true); - propertyType = edmEnumType; - } - else - { - edmEnumType = propertyType.AsEnum(); - } + return intValue; + } - IODataEdmTypeDeserializer deserializer = deserializerProvider.GetEdmTypeDeserializer(edmEnumType); - return deserializer.ReadInline(enumValue, propertyType, readContext); + // Todo: if it is Ieee754Compatible, parse decimal after double + if (Decimal.TryParse(value, NumberStyles.Number, NumberFormatInfo.InvariantInfo, out decimal decimalValue)) + { + return decimalValue; } - // The same logic from ODL to get the element type name in a collection. - internal static string GetCollectionElementTypeName(string typeName, bool isNested) + if (Double.TryParse(value, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out double doubleValue)) { - const string CollectionTypeQualifier = "Collection"; - int collectionTypeQualifierLength = CollectionTypeQualifier.Length; + return doubleValue; + } - // A collection type name must not be null, it has to start with "Collection(" and end with ")" - // and must not be "Collection()" - if (typeName != null && - typeName.StartsWith(CollectionTypeQualifier + "(", StringComparison.Ordinal) && - typeName[typeName.Length - 1] == ')' && - typeName.Length != collectionTypeQualifierLength + 2) - { - if (isNested) - { - throw new ODataException(Error.Format(SRResources.NestedCollectionsNotSupported, typeName)); - } + if (!value.StartsWith("\"", StringComparison.Ordinal) || !value.EndsWith("\"", StringComparison.Ordinal)) + { + throw new ODataException(Error.Format(SRResources.InvalidODataUntypedValue, value)); + } - string innerTypeName = typeName.Substring(collectionTypeQualifierLength + 1, - typeName.Length - (collectionTypeQualifierLength + 2)); + return value.Substring(1, value.Length - 2); + } - // Check if it is not a nested collection and throw if it is - GetCollectionElementTypeName(innerTypeName, true); + private static object ConvertEnumValue(ODataEnumValue enumValue, ref IEdmTypeReference propertyType, + IODataDeserializerProvider deserializerProvider, ODataDeserializerContext readContext) + { + IEdmEnumTypeReference edmEnumType; + if (propertyType == null || propertyType.IsUntyped()) + { + // dynamic enum property or untyped property + Contract.Assert(!String.IsNullOrEmpty(enumValue.TypeName), + "ODataLib should have verified that dynamic enum value has a type name since we provided metadata."); + IEdmModel model = readContext.Model; + IEdmType edmType = model.FindType(enumValue.TypeName); + Contract.Assert(edmType.TypeKind == EdmTypeKind.Enum, "ODataLib should have verified that enum value has a enum resource type."); + edmEnumType = new EdmEnumTypeReference(edmType as IEdmEnumType, isNullable: true); + propertyType = edmEnumType; + } + else + { + edmEnumType = propertyType.AsEnum(); + } - return innerTypeName; + IODataEdmTypeDeserializer deserializer = deserializerProvider.GetEdmTypeDeserializer(edmEnumType); + return deserializer.ReadInline(enumValue, propertyType, readContext); + } + + // The same logic from ODL to get the element type name in a collection. + internal static string GetCollectionElementTypeName(string typeName, bool isNested) + { + const string CollectionTypeQualifier = "Collection"; + int collectionTypeQualifierLength = CollectionTypeQualifier.Length; + + // A collection type name must not be null, it has to start with "Collection(" and end with ")" + // and must not be "Collection()" + if (typeName != null && + typeName.StartsWith(CollectionTypeQualifier + "(", StringComparison.Ordinal) && + typeName[typeName.Length - 1] == ')' && + typeName.Length != collectionTypeQualifierLength + 2) + { + if (isNested) + { + throw new ODataException(Error.Format(SRResources.NestedCollectionsNotSupported, typeName)); } - return null; + string innerTypeName = typeName.Substring(collectionTypeQualifierLength + 1, + typeName.Length - (collectionTypeQualifierLength + 2)); + + // Check if it is not a nested collection and throw if it is + GetCollectionElementTypeName(innerTypeName, true); + + return innerTypeName; } + + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/EnumDeserializationHelpers.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/EnumDeserializationHelpers.cs index baa085d9f..ba95ace1e 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/EnumDeserializationHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/EnumDeserializationHelpers.cs @@ -10,43 +10,42 @@ using Microsoft.AspNetCore.OData.Common; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +internal static class EnumDeserializationHelpers { - internal static class EnumDeserializationHelpers + public static object ConvertEnumValue(object value, Type type) { - public static object ConvertEnumValue(object value, Type type) + if (value == null) { - if (value == null) - { - throw Error.ArgumentNull(nameof(value)); - } - - if (type == null) - { - throw Error.ArgumentNull(nameof(type)); - } + throw Error.ArgumentNull(nameof(value)); + } - Type enumType = TypeHelper.GetUnderlyingTypeOrSelf(type); + if (type == null) + { + throw Error.ArgumentNull(nameof(type)); + } - // if value is of the requested type nothing to do here. - if (value.GetType() == enumType) - { - return value; - } + Type enumType = TypeHelper.GetUnderlyingTypeOrSelf(type); - ODataEnumValue enumValue = value as ODataEnumValue; + // if value is of the requested type nothing to do here. + if (value.GetType() == enumType) + { + return value; + } - if (enumValue == null) - { - throw new ValidationException(Error.Format(SRResources.PropertyMustBeEnum, value.GetType().Name, "ODataEnumValue")); - } + ODataEnumValue enumValue = value as ODataEnumValue; - if (!TypeHelper.IsEnum(enumType)) - { - throw Error.InvalidOperation(Error.Format(SRResources.TypeMustBeEnumOrNullableEnum, type.Name)); - } + if (enumValue == null) + { + throw new ValidationException(Error.Format(SRResources.PropertyMustBeEnum, value.GetType().Name, "ODataEnumValue")); + } - return Enum.Parse(enumType, enumValue.Value); + if (!TypeHelper.IsEnum(enumType)) + { + throw Error.InvalidOperation(Error.Format(SRResources.TypeMustBeEnumOrNullableEnum, type.Name)); } + + return Enum.Parse(enumType, enumValue.Value); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/IODataDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/IODataDeserializer.cs index 0c2567948..f490f2555 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/IODataDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/IODataDeserializer.cs @@ -9,30 +9,29 @@ using System; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// An is used to read an ODataMessage into a CLR object. +/// +/// +/// Each supported CLR type has a corresponding . A CLR type is supported if it is one of +/// the special types or if it has a backing EDM type. Some of the special types are Uri which maps to ODataReferenceLink payload, +/// Uri[] which maps to ODataReferenceLinks payload, ODataWorkspace which maps to ODataServiceDocument payload. +/// +public interface IODataDeserializer { /// - /// An is used to read an ODataMessage into a CLR object. + /// The kind of ODataPayload this deserializer handles. /// - /// - /// Each supported CLR type has a corresponding . A CLR type is supported if it is one of - /// the special types or if it has a backing EDM type. Some of the special types are Uri which maps to ODataReferenceLink payload, - /// Uri[] which maps to ODataReferenceLinks payload, ODataWorkspace which maps to ODataServiceDocument payload. - /// - public interface IODataDeserializer - { - /// - /// The kind of ODataPayload this deserializer handles. - /// - ODataPayloadKind ODataPayloadKind { get; } + ODataPayloadKind ODataPayloadKind { get; } - /// - /// Reads an using messageReader. - /// - /// The messageReader to use. - /// The type of the object to read into. - /// The read context. - /// The deserialized object. - Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext); - } + /// + /// Reads an using messageReader. + /// + /// The messageReader to use. + /// The type of the object to read into. + /// The read context. + /// The deserialized object. + Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext); } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/IODataDeserializerProvider.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/IODataDeserializerProvider.cs index 5cbf68600..e985f162b 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/IODataDeserializerProvider.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/IODataDeserializerProvider.cs @@ -9,28 +9,27 @@ using Microsoft.AspNetCore.Http; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// Represents a factory that creates an . +/// +public interface IODataDeserializerProvider + { /// - /// Represents a factory that creates an . + /// Gets an for the given type. /// - public interface IODataDeserializerProvider - - { - /// - /// Gets an for the given type. - /// - /// The CLR type. - /// The request being deserialized. - /// An that can deserialize the given type. - public IODataDeserializer GetODataDeserializer(Type type, HttpRequest request); + /// The CLR type. + /// The request being deserialized. + /// An that can deserialize the given type. + public IODataDeserializer GetODataDeserializer(Type type, HttpRequest request); - /// - /// Gets the for the given EDM type. - /// - /// The EDM type. - /// Is delta - /// An that can deserialize the given EDM type. - public IODataEdmTypeDeserializer GetEdmTypeDeserializer(IEdmTypeReference edmType, bool isDelta = false); - } + /// + /// Gets the for the given EDM type. + /// + /// The EDM type. + /// Is delta + /// An that can deserialize the given EDM type. + public IODataEdmTypeDeserializer GetEdmTypeDeserializer(IEdmTypeReference edmType, bool isDelta = false); } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/IODataEdmTypeDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/IODataEdmTypeDeserializer.cs index 96b8bee0d..0bb3d1d34 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/IODataEdmTypeDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/IODataEdmTypeDeserializer.cs @@ -7,20 +7,19 @@ using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// Interface for all s that deserialize into an object backed by . +/// +public interface IODataEdmTypeDeserializer: IODataDeserializer { /// - /// Interface for all s that deserialize into an object backed by . + /// Deserializes the item into a new object of type corresponding to . /// - public interface IODataEdmTypeDeserializer: IODataDeserializer - { - /// - /// Deserializes the item into a new object of type corresponding to . - /// - /// The item to deserialize. - /// The EDM type of the object to read into. - /// The . - /// The deserialized object. - object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext); - } + /// The item to deserialize. + /// The EDM type of the object to read into. + /// The . + /// The deserialized object. + object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext); } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataActionPayloadDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataActionPayloadDeserializer.cs index 6fa527fd9..d0d0e45aa 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataActionPayloadDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataActionPayloadDeserializer.cs @@ -23,188 +23,187 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.OData.Routing; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// Represents an for reading OData action parameters. +/// +public class ODataActionPayloadDeserializer : ODataDeserializer { + private static readonly MethodInfo _castMethodInfo = typeof(Enumerable).GetMethod("Cast"); + + /// + /// Initializes a new instance of the class. + /// + /// The deserializer provider to use to read inner objects. + public ODataActionPayloadDeserializer(IODataDeserializerProvider deserializerProvider) + : base(ODataPayloadKind.Parameter) + { + DeserializerProvider = deserializerProvider ?? throw new ArgumentNullException(nameof(deserializerProvider)); + } + /// - /// Represents an for reading OData action parameters. + /// Gets the deserializer provider to use to read inner objects. /// - public class ODataActionPayloadDeserializer : ODataDeserializer + public IODataDeserializerProvider DeserializerProvider { get; private set; } + + /// + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", + Justification = "The majority of types referenced by this method are EdmLib types this method needs to know about to operate correctly")] + public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) { - private static readonly MethodInfo _castMethodInfo = typeof(Enumerable).GetMethod("Cast"); - - /// - /// Initializes a new instance of the class. - /// - /// The deserializer provider to use to read inner objects. - public ODataActionPayloadDeserializer(IODataDeserializerProvider deserializerProvider) - : base(ODataPayloadKind.Parameter) + if (messageReader == null) { - DeserializerProvider = deserializerProvider ?? throw new ArgumentNullException(nameof(deserializerProvider)); + throw Error.ArgumentNull("messageReader"); } - /// - /// Gets the deserializer provider to use to read inner objects. - /// - public IODataDeserializerProvider DeserializerProvider { get; private set; } + IEdmAction action = GetAction(readContext); + Contract.Assert(action != null); - /// - [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", - Justification = "The majority of types referenced by this method are EdmLib types this method needs to know about to operate correctly")] - public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + // Create the correct resource type; + Dictionary payload; + if (type == typeof(ODataActionParameters)) { - if (messageReader == null) - { - throw Error.ArgumentNull("messageReader"); - } + payload = new ODataActionParameters(); + } + else + { + payload = new ODataUntypedActionParameters(action); + } - IEdmAction action = GetAction(readContext); - Contract.Assert(action != null); + ODataParameterReader reader = await messageReader.CreateODataParameterReaderAsync(action).ConfigureAwait(false); - // Create the correct resource type; - Dictionary payload; - if (type == typeof(ODataActionParameters)) - { - payload = new ODataActionParameters(); - } - else - { - payload = new ODataUntypedActionParameters(action); - } - - ODataParameterReader reader = await messageReader.CreateODataParameterReaderAsync(action).ConfigureAwait(false); + while (await reader.ReadAsync().ConfigureAwait(false)) + { + string parameterName = null; + IEdmOperationParameter parameter = null; - while (await reader.ReadAsync().ConfigureAwait(false)) + switch (reader.State) { - string parameterName = null; - IEdmOperationParameter parameter = null; - - switch (reader.State) - { - case ODataParameterReaderState.Value: - parameterName = reader.Name; - parameter = action.Parameters.SingleOrDefault(p => p.Name == parameterName); - // ODataLib protects against this but asserting just in case. - Contract.Assert(parameter != null, String.Format(CultureInfo.InvariantCulture, "Parameter '{0}' not found.", parameterName)); - if (parameter.Type.IsPrimitive()) + case ODataParameterReaderState.Value: + parameterName = reader.Name; + parameter = action.Parameters.SingleOrDefault(p => p.Name == parameterName); + // ODataLib protects against this but asserting just in case. + Contract.Assert(parameter != null, String.Format(CultureInfo.InvariantCulture, "Parameter '{0}' not found.", parameterName)); + if (parameter.Type.IsPrimitive()) + { + payload[parameterName] = reader.Value; + } + else + { + IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(parameter.Type); + payload[parameterName] = deserializer.ReadInline(reader.Value, parameter.Type, readContext); + } + break; + + case ODataParameterReaderState.Collection: + parameterName = reader.Name; + parameter = action.Parameters.SingleOrDefault(p => p.Name == parameterName); + // ODataLib protects against this but asserting just in case. + Contract.Assert(parameter != null, String.Format(CultureInfo.InvariantCulture, "Parameter '{0}' not found.", parameterName)); + IEdmCollectionTypeReference collectionType = parameter.Type as IEdmCollectionTypeReference; + Contract.Assert(collectionType != null); + ODataCollectionValue value = await ODataCollectionDeserializer + .ReadCollectionAsync(reader.CreateCollectionReader()).ConfigureAwait(false); + ODataCollectionDeserializer collectionDeserializer = (ODataCollectionDeserializer)DeserializerProvider.GetEdmTypeDeserializer(collectionType); + payload[parameterName] = collectionDeserializer.ReadInline(value, collectionType, readContext); + break; + + case ODataParameterReaderState.Resource: + parameterName = reader.Name; + parameter = action.Parameters.SingleOrDefault(p => p.Name == parameterName); + Contract.Assert(parameter != null, String.Format(CultureInfo.InvariantCulture, "Parameter '{0}' not found.", parameterName)); + Contract.Assert(parameter.Type.IsStructured()); + + ODataReader resourceReader = reader.CreateResourceReader(); + object item = await resourceReader.ReadResourceOrResourceSetAsync().ConfigureAwait(false); + ODataResourceDeserializer resourceDeserializer = (ODataResourceDeserializer)DeserializerProvider.GetEdmTypeDeserializer(parameter.Type); + payload[parameterName] = resourceDeserializer.ReadInline(item, parameter.Type, readContext); + break; + + case ODataParameterReaderState.ResourceSet: + parameterName = reader.Name; + parameter = action.Parameters.SingleOrDefault(p => p.Name == parameterName); + Contract.Assert(parameter != null, String.Format(CultureInfo.InvariantCulture, "Parameter '{0}' not found.", parameterName)); + + IEdmCollectionTypeReference resourceSetType = parameter.Type as IEdmCollectionTypeReference; + Contract.Assert(resourceSetType != null); + + ODataReader resourceSetReader = reader.CreateResourceSetReader(); + object feed = await resourceSetReader.ReadResourceOrResourceSetAsync().ConfigureAwait(false); + ODataResourceSetDeserializer resourceSetDeserializer = (ODataResourceSetDeserializer)DeserializerProvider.GetEdmTypeDeserializer(resourceSetType); + + object result = resourceSetDeserializer.ReadInline(feed, resourceSetType, readContext); + + IEdmTypeReference elementTypeReference = resourceSetType.ElementType(); + Contract.Assert(elementTypeReference.IsStructured()); + + IEnumerable enumerable = result as IEnumerable; + if (enumerable != null) + { + if (readContext.IsNoClrType) { - payload[parameterName] = reader.Value; + payload[parameterName] = enumerable.ConvertToEdmObject(resourceSetType); } else { - IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(parameter.Type); - payload[parameterName] = deserializer.ReadInline(reader.Value, parameter.Type, readContext); - } - break; - - case ODataParameterReaderState.Collection: - parameterName = reader.Name; - parameter = action.Parameters.SingleOrDefault(p => p.Name == parameterName); - // ODataLib protects against this but asserting just in case. - Contract.Assert(parameter != null, String.Format(CultureInfo.InvariantCulture, "Parameter '{0}' not found.", parameterName)); - IEdmCollectionTypeReference collectionType = parameter.Type as IEdmCollectionTypeReference; - Contract.Assert(collectionType != null); - ODataCollectionValue value = await ODataCollectionDeserializer - .ReadCollectionAsync(reader.CreateCollectionReader()).ConfigureAwait(false); - ODataCollectionDeserializer collectionDeserializer = (ODataCollectionDeserializer)DeserializerProvider.GetEdmTypeDeserializer(collectionType); - payload[parameterName] = collectionDeserializer.ReadInline(value, collectionType, readContext); - break; - - case ODataParameterReaderState.Resource: - parameterName = reader.Name; - parameter = action.Parameters.SingleOrDefault(p => p.Name == parameterName); - Contract.Assert(parameter != null, String.Format(CultureInfo.InvariantCulture, "Parameter '{0}' not found.", parameterName)); - Contract.Assert(parameter.Type.IsStructured()); - - ODataReader resourceReader = reader.CreateResourceReader(); - object item = await resourceReader.ReadResourceOrResourceSetAsync().ConfigureAwait(false); - ODataResourceDeserializer resourceDeserializer = (ODataResourceDeserializer)DeserializerProvider.GetEdmTypeDeserializer(parameter.Type); - payload[parameterName] = resourceDeserializer.ReadInline(item, parameter.Type, readContext); - break; - - case ODataParameterReaderState.ResourceSet: - parameterName = reader.Name; - parameter = action.Parameters.SingleOrDefault(p => p.Name == parameterName); - Contract.Assert(parameter != null, String.Format(CultureInfo.InvariantCulture, "Parameter '{0}' not found.", parameterName)); - - IEdmCollectionTypeReference resourceSetType = parameter.Type as IEdmCollectionTypeReference; - Contract.Assert(resourceSetType != null); - - ODataReader resourceSetReader = reader.CreateResourceSetReader(); - object feed = await resourceSetReader.ReadResourceOrResourceSetAsync().ConfigureAwait(false); - ODataResourceSetDeserializer resourceSetDeserializer = (ODataResourceSetDeserializer)DeserializerProvider.GetEdmTypeDeserializer(resourceSetType); - - object result = resourceSetDeserializer.ReadInline(feed, resourceSetType, readContext); - - IEdmTypeReference elementTypeReference = resourceSetType.ElementType(); - Contract.Assert(elementTypeReference.IsStructured()); - - IEnumerable enumerable = result as IEnumerable; - if (enumerable != null) - { - if (readContext.IsNoClrType) - { - payload[parameterName] = enumerable.ConvertToEdmObject(resourceSetType); - } - else - { - Type elementClrType = readContext.Model.GetClrType(elementTypeReference); - IEnumerable castedResult = - _castMethodInfo.MakeGenericMethod(elementClrType) - .Invoke(null, new[] { result }) as IEnumerable; - payload[parameterName] = castedResult; - } + Type elementClrType = readContext.Model.GetClrType(elementTypeReference); + IEnumerable castedResult = + _castMethodInfo.MakeGenericMethod(elementClrType) + .Invoke(null, new[] { result }) as IEnumerable; + payload[parameterName] = castedResult; } - break; - } + } + break; } - - return payload; } - internal static IEdmAction GetAction(ODataDeserializerContext readContext) + return payload; + } + + internal static IEdmAction GetAction(ODataDeserializerContext readContext) + { + if (readContext == null) { - if (readContext == null) - { - throw Error.ArgumentNull("readContext"); - } + throw Error.ArgumentNull("readContext"); + } - ODataPath path = readContext.Path; - if (path == null || path.Count == 0) - { - throw new SerializationException(SRResources.ODataPathMissing); - } + ODataPath path = readContext.Path; + if (path == null || path.Count == 0) + { + throw new SerializationException(SRResources.ODataPathMissing); + } - IEdmAction action = null; - if (path.Count == 1) - { - // only one segment, it may be an unbound action - OperationImportSegment unboundActionSegment = path.FirstSegment as OperationImportSegment; - if (unboundActionSegment != null) - { - IEdmActionImport actionImport = unboundActionSegment.OperationImports.First() as IEdmActionImport; - if (actionImport != null) - { - action = actionImport.Action; - } - } - } - else + IEdmAction action = null; + if (path.Count == 1) + { + // only one segment, it may be an unbound action + OperationImportSegment unboundActionSegment = path.FirstSegment as OperationImportSegment; + if (unboundActionSegment != null) { - // otherwise, it may be a bound action - OperationSegment actionSegment = path.LastSegment as OperationSegment; - if (actionSegment != null) + IEdmActionImport actionImport = unboundActionSegment.OperationImports.First() as IEdmActionImport; + if (actionImport != null) { - action = actionSegment.Operations.First() as IEdmAction; + action = actionImport.Action; } } - - if (action == null) + } + else + { + // otherwise, it may be a bound action + OperationSegment actionSegment = path.LastSegment as OperationSegment; + if (actionSegment != null) { - string message = Error.Format(SRResources.RequestNotActionInvocation, path.GetPathString()); - throw new SerializationException(message); + action = actionSegment.Operations.First() as IEdmAction; } + } - return action; + if (action == null) + { + string message = Error.Format(SRResources.RequestNotActionInvocation, path.GetPathString()); + throw new SerializationException(message); } + + return action; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataCollectionDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataCollectionDeserializer.cs index 38eaacf20..654e424d2 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataCollectionDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataCollectionDeserializer.cs @@ -18,162 +18,161 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// Represents an that can read OData collection payloads. +/// +public class ODataCollectionDeserializer : ODataEdmTypeDeserializer { + private static readonly MethodInfo _castMethodInfo = typeof(Enumerable).GetMethod("Cast"); + /// - /// Represents an that can read OData collection payloads. + /// Initializes a new instance of the class. /// - public class ODataCollectionDeserializer : ODataEdmTypeDeserializer + /// The deserializer provider to use to read inner objects. + public ODataCollectionDeserializer(IODataDeserializerProvider deserializerProvider) + : base(ODataPayloadKind.Collection, deserializerProvider) + { + } + + /// + public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) { - private static readonly MethodInfo _castMethodInfo = typeof(Enumerable).GetMethod("Cast"); - - /// - /// Initializes a new instance of the class. - /// - /// The deserializer provider to use to read inner objects. - public ODataCollectionDeserializer(IODataDeserializerProvider deserializerProvider) - : base(ODataPayloadKind.Collection, deserializerProvider) + if (messageReader == null) { + throw Error.ArgumentNull("messageReader"); } - /// - public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + if (readContext == null) { - if (messageReader == null) - { - throw Error.ArgumentNull("messageReader"); - } + throw new ArgumentNullException(nameof(readContext)); + } - if (readContext == null) - { - throw new ArgumentNullException(nameof(readContext)); - } + IEdmTypeReference edmType = readContext.GetEdmType(type); + Contract.Assert(edmType != null); - IEdmTypeReference edmType = readContext.GetEdmType(type); - Contract.Assert(edmType != null); + if (!edmType.IsCollection()) + { + throw Error.Argument("type", SRResources.ArgumentMustBeOfType, EdmTypeKind.Collection); + } - if (!edmType.IsCollection()) - { - throw Error.Argument("type", SRResources.ArgumentMustBeOfType, EdmTypeKind.Collection); - } + IEdmCollectionTypeReference collectionType = edmType.AsCollection(); + IEdmTypeReference elementType = collectionType.ElementType(); + ODataCollectionReader reader = await messageReader.CreateODataCollectionReaderAsync(elementType).ConfigureAwait(false); + return ReadInline(await ReadCollectionAsync(reader).ConfigureAwait(false), edmType, readContext); + } - IEdmCollectionTypeReference collectionType = edmType.AsCollection(); - IEdmTypeReference elementType = collectionType.ElementType(); - ODataCollectionReader reader = await messageReader.CreateODataCollectionReaderAsync(elementType).ConfigureAwait(false); - return ReadInline(await ReadCollectionAsync(reader).ConfigureAwait(false), edmType, readContext); + /// + public sealed override object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) + { + if (item == null) + { + return null; } - /// - public sealed override object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) + if (edmType == null) { - if (item == null) - { - return null; - } + throw new ArgumentNullException(nameof(edmType)); + } - if (edmType == null) - { - throw new ArgumentNullException(nameof(edmType)); - } + if (readContext == null) + { + throw new ArgumentNullException(nameof(readContext)); + } - if (readContext == null) - { - throw new ArgumentNullException(nameof(readContext)); - } + if (!edmType.IsCollection()) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeDeserialized, edmType.ToTraceString())); + } - if (!edmType.IsCollection()) - { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeDeserialized, edmType.ToTraceString())); - } + IEdmCollectionTypeReference collectionType = edmType.AsCollection(); + IEdmTypeReference elementType = collectionType.ElementType(); - IEdmCollectionTypeReference collectionType = edmType.AsCollection(); - IEdmTypeReference elementType = collectionType.ElementType(); + ODataCollectionValue collection = item as ODataCollectionValue; - ODataCollectionValue collection = item as ODataCollectionValue; + if (collection == null) + { + throw Error.Argument("item", SRResources.ArgumentMustBeOfType, typeof(ODataCollectionValue).Name); + } + // Recursion guard to avoid stack overflows + RuntimeHelpers.EnsureSufficientExecutionStack(); - if (collection == null) + IEnumerable result = ReadCollectionValue(collection, elementType, readContext); + if (result != null) + { + if (readContext.IsNoClrType && elementType.IsEnum()) { - throw Error.Argument("item", SRResources.ArgumentMustBeOfType, typeof(ODataCollectionValue).Name); + return result.ConvertToEdmObject(collectionType); } - // Recursion guard to avoid stack overflows - RuntimeHelpers.EnsureSufficientExecutionStack(); - - IEnumerable result = ReadCollectionValue(collection, elementType, readContext); - if (result != null) + else { - if (readContext.IsNoClrType && elementType.IsEnum()) - { - return result.ConvertToEdmObject(collectionType); - } - else - { - Type elementClrType = readContext.Model.GetClrType(elementType); - IEnumerable castedResult = _castMethodInfo.MakeGenericMethod(elementClrType).Invoke(null, new object[] { result }) as IEnumerable; - return castedResult; - } + Type elementClrType = readContext.Model.GetClrType(elementType); + IEnumerable castedResult = _castMethodInfo.MakeGenericMethod(elementClrType).Invoke(null, new object[] { result }) as IEnumerable; + return castedResult; } + } - return null; + return null; + } + + /// + /// Deserializes the given under the given . + /// + /// The to deserialize. + /// The element type of the collection to read. + /// The deserializer context. + /// The deserialized collection. + public virtual IEnumerable ReadCollectionValue(ODataCollectionValue collectionValue, IEdmTypeReference elementType, + ODataDeserializerContext readContext) + { + if (collectionValue == null) + { + throw Error.ArgumentNull("collectionValue"); + } + if (elementType == null) + { + throw Error.ArgumentNull("elementType"); } - /// - /// Deserializes the given under the given . - /// - /// The to deserialize. - /// The element type of the collection to read. - /// The deserializer context. - /// The deserialized collection. - public virtual IEnumerable ReadCollectionValue(ODataCollectionValue collectionValue, IEdmTypeReference elementType, - ODataDeserializerContext readContext) + IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(elementType); + if (deserializer == null) { - if (collectionValue == null) - { - throw Error.ArgumentNull("collectionValue"); - } - if (elementType == null) - { - throw Error.ArgumentNull("elementType"); - } + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeDeserialized, elementType.FullName())); + } - IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(elementType); - if (deserializer == null) + foreach (object item in collectionValue.Items) + { + if (elementType.IsPrimitive()) { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeDeserialized, elementType.FullName())); + yield return item; } - - foreach (object item in collectionValue.Items) + else { - if (elementType.IsPrimitive()) - { - yield return item; - } - else - { - yield return deserializer.ReadInline(item, elementType, readContext); - } + yield return deserializer.ReadInline(item, elementType, readContext); } } + } - internal static async Task ReadCollectionAsync(ODataCollectionReader reader) - { - ArrayList items = new ArrayList(); - string typeName = null; + internal static async Task ReadCollectionAsync(ODataCollectionReader reader) + { + ArrayList items = new ArrayList(); + string typeName = null; - while (await reader.ReadAsync().ConfigureAwait(false)) + while (await reader.ReadAsync().ConfigureAwait(false)) + { + if (ODataCollectionReaderState.Value == reader.State) { - if (ODataCollectionReaderState.Value == reader.State) - { - items.Add(reader.Item); - } - else if (ODataCollectionReaderState.CollectionStart == reader.State) - { - typeName = reader.Item.ToString(); - } + items.Add(reader.Item); + } + else if (ODataCollectionReaderState.CollectionStart == reader.State) + { + typeName = reader.Item.ToString(); } - - return new ODataCollectionValue { Items = items.Cast(), TypeName = typeName }; } + + return new ODataCollectionValue { Items = items.Cast(), TypeName = typeName }; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeltaResourceSetDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeltaResourceSetDeserializer.cs index 08dd1cc42..445f9ad34 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeltaResourceSetDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeltaResourceSetDeserializer.cs @@ -19,326 +19,325 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// Represents an that can read OData delta resource sets. +/// +public class ODataDeltaResourceSetDeserializer : ODataEdmTypeDeserializer { /// - /// Represents an that can read OData delta resource sets. + /// Initializes a new instance of the class. /// - public class ODataDeltaResourceSetDeserializer : ODataEdmTypeDeserializer + /// The deserializer provider to use to read inner objects. + public ODataDeltaResourceSetDeserializer(IODataDeserializerProvider deserializerProvider) + : base(ODataPayloadKind.Delta, deserializerProvider) { - /// - /// Initializes a new instance of the class. - /// - /// The deserializer provider to use to read inner objects. - public ODataDeltaResourceSetDeserializer(IODataDeserializerProvider deserializerProvider) - : base(ODataPayloadKind.Delta, deserializerProvider) + } + + /// + public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + { + if (messageReader == null) { + throw Error.ArgumentNull(nameof(messageReader)); } - /// - public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + if (readContext == null) { - if (messageReader == null) - { - throw Error.ArgumentNull(nameof(messageReader)); - } + throw Error.ArgumentNull(nameof(readContext)); + } - if (readContext == null) - { - throw Error.ArgumentNull(nameof(readContext)); - } + // TODO: Need to modify how to get the Edm type for the delta resource set? + IEdmTypeReference edmType = readContext.GetEdmType(type); + Contract.Assert(edmType != null); - // TODO: Need to modify how to get the Edm type for the delta resource set? - IEdmTypeReference edmType = readContext.GetEdmType(type); - Contract.Assert(edmType != null); + IEdmEntityType entityType = edmType.Definition as IEdmEntityType; - IEdmEntityType entityType = edmType.Definition as IEdmEntityType; + EdmDeltaCollectionType edmCollectionType = new EdmDeltaCollectionType(edmType); + edmType = new EdmCollectionTypeReference(edmCollectionType); - EdmDeltaCollectionType edmCollectionType = new EdmDeltaCollectionType(edmType); - edmType = new EdmCollectionTypeReference(edmCollectionType); + // TODO: is it OK to read the top level collection of entity? + if (!(edmType.IsCollection() && edmType.AsCollection().ElementType().IsStructured())) + { + throw Error.Argument("edmType", SRResources.ArgumentMustBeOfType, EdmTypeKind.Complex + " or " + EdmTypeKind.Entity); + } - // TODO: is it OK to read the top level collection of entity? - if (!(edmType.IsCollection() && edmType.AsCollection().ElementType().IsStructured())) - { - throw Error.Argument("edmType", SRResources.ArgumentMustBeOfType, EdmTypeKind.Complex + " or " + EdmTypeKind.Entity); - } + IEdmNavigationSource navigationSource; + if (readContext.Path == null) + { + throw Error.Argument("readContext", SRResources.ODataPathMissing); + } - IEdmNavigationSource navigationSource; - if (readContext.Path == null) - { - throw Error.Argument("readContext", SRResources.ODataPathMissing); - } + navigationSource = readContext.Path.GetNavigationSource(); + if (navigationSource == null) + { + throw new SerializationException(SRResources.NavigationSourceMissingDuringDeserialization); + } - navigationSource = readContext.Path.GetNavigationSource(); - if (navigationSource == null) - { - throw new SerializationException(SRResources.NavigationSourceMissingDuringDeserialization); - } + ODataReader resourceSetReader = await messageReader.CreateODataDeltaResourceSetReaderAsync(navigationSource as IEdmEntitySet, entityType).ConfigureAwait(false); + object deltaResourceSet = await resourceSetReader.ReadResourceOrResourceSetAsync().ConfigureAwait(false); + return ReadInline(deltaResourceSet, edmType, readContext); + } - ODataReader resourceSetReader = await messageReader.CreateODataDeltaResourceSetReaderAsync(navigationSource as IEdmEntitySet, entityType).ConfigureAwait(false); - object deltaResourceSet = await resourceSetReader.ReadResourceOrResourceSetAsync().ConfigureAwait(false); - return ReadInline(deltaResourceSet, edmType, readContext); + /// + public override object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) + { + if (item == null) + { + return null; } - /// - public override object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) + if (edmType == null) { - if (item == null) - { - return null; - } - - if (edmType == null) - { - throw Error.ArgumentNull(nameof(edmType)); - } + throw Error.ArgumentNull(nameof(edmType)); + } - if (readContext == null) - { - throw Error.ArgumentNull(nameof(readContext)); - } + if (readContext == null) + { + throw Error.ArgumentNull(nameof(readContext)); + } - if (!edmType.IsCollection() || !edmType.AsCollection().ElementType().IsStructured()) - { - throw Error.Argument(nameof(edmType), SRResources.TypeMustBeResourceSet, edmType.FullName()); - } + if (!edmType.IsCollection() || !edmType.AsCollection().ElementType().IsStructured()) + { + throw Error.Argument(nameof(edmType), SRResources.TypeMustBeResourceSet, edmType.FullName()); + } - ODataDeltaResourceSetWrapper resourceSet = item as ODataDeltaResourceSetWrapper; - if (resourceSet == null) - { - throw Error.Argument(nameof(item), SRResources.ArgumentMustBeOfType, typeof(ODataDeltaResourceSetWrapper).Name); - } + ODataDeltaResourceSetWrapper resourceSet = item as ODataDeltaResourceSetWrapper; + if (resourceSet == null) + { + throw Error.Argument(nameof(item), SRResources.ArgumentMustBeOfType, typeof(ODataDeltaResourceSetWrapper).Name); + } - // Recursion guard to avoid stack overflows - RuntimeHelpers.EnsureSufficientExecutionStack(); + // Recursion guard to avoid stack overflows + RuntimeHelpers.EnsureSufficientExecutionStack(); - IEdmStructuredTypeReference elementType = edmType.AsCollection().ElementType().AsStructured(); + IEdmStructuredTypeReference elementType = edmType.AsCollection().ElementType().AsStructured(); - IEnumerable result = ReadDeltaResourceSet(resourceSet, elementType, readContext); - if (result != null) + IEnumerable result = ReadDeltaResourceSet(resourceSet, elementType, readContext); + if (result != null) + { + if (readContext.IsNoClrType) { - if (readContext.IsNoClrType) + EdmChangedObjectCollection changedObjCollection = new EdmChangedObjectCollection(elementType.Definition as IEdmEntityType); + foreach (IEdmChangedObject changedObject in result) { - EdmChangedObjectCollection changedObjCollection = new EdmChangedObjectCollection(elementType.Definition as IEdmEntityType); - foreach (IEdmChangedObject changedObject in result) - { - changedObjCollection.Add(changedObject); - } - - return changedObjCollection; + changedObjCollection.Add(changedObject); } - else - { - Type elementClrType = readContext.Model.GetClrType(elementType); - Type changedObjCollType = typeof(DeltaSet<>).MakeGenericType(elementClrType); - IDeltaSet deltaSet = Activator.CreateInstance(changedObjCollType) as IDeltaSet; - foreach (var delta in result) - { - IDeltaSetItem deltaItem = delta as IDeltaSetItem; - if (deltaItem != null) - { - deltaSet.Add(deltaItem); - } - } - - return deltaSet; - } + return changedObjCollection; } else { - return result; + Type elementClrType = readContext.Model.GetClrType(elementType); + Type changedObjCollType = typeof(DeltaSet<>).MakeGenericType(elementClrType); + + IDeltaSet deltaSet = Activator.CreateInstance(changedObjCollType) as IDeltaSet; + foreach (var delta in result) + { + IDeltaSetItem deltaItem = delta as IDeltaSetItem; + if (deltaItem != null) + { + deltaSet.Add(deltaItem); + } + } + + return deltaSet; } } + else + { + return result; + } + } + + /// + /// Deserializes the given under the given . + /// + /// The delta resource set to deserialize. + /// The element type. + /// The deserializer context. + /// The deserialized resource set object. + public virtual IEnumerable ReadDeltaResourceSet(ODataDeltaResourceSetWrapper deltaResourceSet, IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + { + if (deltaResourceSet == null) + { + throw Error.ArgumentNull(nameof(deltaResourceSet)); + } + + if (readContext == null) + { + throw Error.ArgumentNull(nameof(readContext)); + } - /// - /// Deserializes the given under the given . - /// - /// The delta resource set to deserialize. - /// The element type. - /// The deserializer context. - /// The deserialized resource set object. - public virtual IEnumerable ReadDeltaResourceSet(ODataDeltaResourceSetWrapper deltaResourceSet, IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + // Delta Items + foreach (ODataItemWrapper itemWrapper in deltaResourceSet.DeltaItems) { - if (deltaResourceSet == null) + // Deleted Link + ODataDeltaDeletedLinkWrapper deletedLinkWrapper = itemWrapper as ODataDeltaDeletedLinkWrapper; + if (deletedLinkWrapper != null) { - throw Error.ArgumentNull(nameof(deltaResourceSet)); + yield return ReadDeltaDeletedLink(deletedLinkWrapper, elementType, readContext); } - if (readContext == null) + // Added Link + ODataDeltaLinkWrapper deltaLinkWrapper = itemWrapper as ODataDeltaLinkWrapper; + if (deltaLinkWrapper != null) { - throw Error.ArgumentNull(nameof(readContext)); + yield return ReadDeltaLink(deltaLinkWrapper, elementType, readContext); } - // Delta Items - foreach (ODataItemWrapper itemWrapper in deltaResourceSet.DeltaItems) + // Added resource, updated resource and deleted resource + ODataResourceWrapper resourceWrapper = itemWrapper as ODataResourceWrapper; + if (resourceWrapper != null) { - // Deleted Link - ODataDeltaDeletedLinkWrapper deletedLinkWrapper = itemWrapper as ODataDeltaDeletedLinkWrapper; - if (deletedLinkWrapper != null) - { - yield return ReadDeltaDeletedLink(deletedLinkWrapper, elementType, readContext); - } + yield return ReadDeltaResource(resourceWrapper, elementType, readContext); + } + } + } - // Added Link - ODataDeltaLinkWrapper deltaLinkWrapper = itemWrapper as ODataDeltaLinkWrapper; - if (deltaLinkWrapper != null) - { - yield return ReadDeltaLink(deltaLinkWrapper, elementType, readContext); - } + /// + /// Deserializes the given under the given . + /// The given resource could be "Added Resource", "Updated Resource" or "Deleted Resource". + /// + /// The Added Updated or Deleted Resource. + /// The element type. + /// The deserializer context. + /// The created object. + public virtual object ReadDeltaResource(ODataResourceWrapper resource, IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + { + if (resource == null) + { + throw Error.ArgumentNull(nameof(resource)); + } - // Added resource, updated resource and deleted resource - ODataResourceWrapper resourceWrapper = itemWrapper as ODataResourceWrapper; - if (resourceWrapper != null) - { - yield return ReadDeltaResource(resourceWrapper, elementType, readContext); - } - } + if (readContext == null) + { + throw Error.ArgumentNull(nameof(readContext)); + } + + IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(elementType); + if (deserializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeDeserialized, elementType.FullName())); } - /// - /// Deserializes the given under the given . - /// The given resource could be "Added Resource", "Updated Resource" or "Deleted Resource". - /// - /// The Added Updated or Deleted Resource. - /// The element type. - /// The deserializer context. - /// The created object. - public virtual object ReadDeltaResource(ODataResourceWrapper resource, IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + ODataDeserializerContext resourceReadContext = readContext.CloneWithoutType(); + if (readContext.IsNoClrType) // check the clr type status from parent context. { - if (resource == null) + if (resource.IsDeletedResource) { - throw Error.ArgumentNull(nameof(resource)); + resourceReadContext.ResourceType = typeof(EdmDeltaDeletedResourceObject); } - - if (readContext == null) + else { - throw Error.ArgumentNull(nameof(readContext)); + resourceReadContext.ResourceType = typeof(EdmEntityObject); } - - IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(elementType); - if (deserializer == null) + } + else + { + Type clrType = readContext.Model.GetClrType(elementType); + if (clrType == null) { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeDeserialized, elementType.FullName())); + throw new ODataException( + Error.Format(SRResources.MappingDoesNotContainResourceType, elementType.FullName())); } - ODataDeserializerContext resourceReadContext = readContext.CloneWithoutType(); - if (readContext.IsNoClrType) // check the clr type status from parent context. + if (resource.IsDeletedResource) { - if (resource.IsDeletedResource) - { - resourceReadContext.ResourceType = typeof(EdmDeltaDeletedResourceObject); - } - else - { - resourceReadContext.ResourceType = typeof(EdmEntityObject); - } + resourceReadContext.ResourceType = typeof(DeltaDeletedResource<>).MakeGenericType(clrType); } else { - Type clrType = readContext.Model.GetClrType(elementType); - if (clrType == null) - { - throw new ODataException( - Error.Format(SRResources.MappingDoesNotContainResourceType, elementType.FullName())); - } - - if (resource.IsDeletedResource) - { - resourceReadContext.ResourceType = typeof(DeltaDeletedResource<>).MakeGenericType(clrType); - } - else - { - resourceReadContext.ResourceType = typeof(Delta<>).MakeGenericType(clrType); - } + resourceReadContext.ResourceType = typeof(Delta<>).MakeGenericType(clrType); } - - return deserializer.ReadInline(resource, elementType, resourceReadContext); } - /// - /// Deserializes the given under the given . - /// - /// The given deleted link. - /// The element type. - /// The deserializer context. - /// The created object. - internal virtual object ReadDeltaDeletedLink(ODataDeltaDeletedLinkWrapper deletedLink, IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + return deserializer.ReadInline(resource, elementType, resourceReadContext); + } + + /// + /// Deserializes the given under the given . + /// + /// The given deleted link. + /// The element type. + /// The deserializer context. + /// The created object. + internal virtual object ReadDeltaDeletedLink(ODataDeltaDeletedLinkWrapper deletedLink, IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + { + if (deletedLink == null) { - if (deletedLink == null) - { - throw Error.ArgumentNull(nameof(deletedLink)); - } + throw Error.ArgumentNull(nameof(deletedLink)); + } - if (readContext == null) - { - throw Error.ArgumentNull(nameof(readContext)); - } + if (readContext == null) + { + throw Error.ArgumentNull(nameof(readContext)); + } - if (readContext.IsNoClrType) - { - EdmDeltaDeletedLink edmDeltaLink = new EdmDeltaDeletedLink(elementType?.Definition as IEdmEntityType); - edmDeltaLink.Source = deletedLink.DeltaDeletedLink.Source; - edmDeltaLink.Target = deletedLink.DeltaDeletedLink.Target; - edmDeltaLink.Relationship = deletedLink.DeltaDeletedLink.Relationship; - return edmDeltaLink; - } - else - { - // refactor how to get the CLR type - Type elementClrType = readContext.Model.GetClrType(elementType); + if (readContext.IsNoClrType) + { + EdmDeltaDeletedLink edmDeltaLink = new EdmDeltaDeletedLink(elementType?.Definition as IEdmEntityType); + edmDeltaLink.Source = deletedLink.DeltaDeletedLink.Source; + edmDeltaLink.Target = deletedLink.DeltaDeletedLink.Target; + edmDeltaLink.Relationship = deletedLink.DeltaDeletedLink.Relationship; + return edmDeltaLink; + } + else + { + // refactor how to get the CLR type + Type elementClrType = readContext.Model.GetClrType(elementType); - Type deltaLinkType = typeof(DeltaDeletedLink<>).MakeGenericType(elementClrType); - IDeltaDeletedLink deltaLink = Activator.CreateInstance(deltaLinkType) as IDeltaDeletedLink; + Type deltaLinkType = typeof(DeltaDeletedLink<>).MakeGenericType(elementClrType); + IDeltaDeletedLink deltaLink = Activator.CreateInstance(deltaLinkType) as IDeltaDeletedLink; - deltaLink.Source = deletedLink.DeltaDeletedLink.Source; - deltaLink.Target = deletedLink.DeltaDeletedLink.Target; - deltaLink.Relationship = deletedLink.DeltaDeletedLink.Relationship; - return deltaLink; - } + deltaLink.Source = deletedLink.DeltaDeletedLink.Source; + deltaLink.Target = deletedLink.DeltaDeletedLink.Target; + deltaLink.Relationship = deletedLink.DeltaDeletedLink.Relationship; + return deltaLink; } + } - /// - /// Deserializes the given under the given . - /// - /// The given delta link. - /// The element type. - /// The deserializer context. - /// The created object. - internal virtual object ReadDeltaLink(ODataDeltaLinkWrapper link, IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + /// + /// Deserializes the given under the given . + /// + /// The given delta link. + /// The element type. + /// The deserializer context. + /// The created object. + internal virtual object ReadDeltaLink(ODataDeltaLinkWrapper link, IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + { + if (link == null) { - if (link == null) - { - throw Error.ArgumentNull(nameof(link)); - } + throw Error.ArgumentNull(nameof(link)); + } - if (readContext == null) - { - throw Error.ArgumentNull(nameof(readContext)); - } + if (readContext == null) + { + throw Error.ArgumentNull(nameof(readContext)); + } - if (readContext.IsNoClrType) - { - EdmDeltaLink edmDeltaLink = new EdmDeltaLink(elementType?.Definition as IEdmEntityType); - edmDeltaLink.Source = link.DeltaLink.Source; - edmDeltaLink.Target = link.DeltaLink.Target; - edmDeltaLink.Relationship = link.DeltaLink.Relationship; - return edmDeltaLink; - } - else - { - // refactor how to get the CLR type - Type elementClrType = readContext.Model.GetClrType(elementType); + if (readContext.IsNoClrType) + { + EdmDeltaLink edmDeltaLink = new EdmDeltaLink(elementType?.Definition as IEdmEntityType); + edmDeltaLink.Source = link.DeltaLink.Source; + edmDeltaLink.Target = link.DeltaLink.Target; + edmDeltaLink.Relationship = link.DeltaLink.Relationship; + return edmDeltaLink; + } + else + { + // refactor how to get the CLR type + Type elementClrType = readContext.Model.GetClrType(elementType); - Type deltaLinkType = typeof(DeltaLink<>).MakeGenericType(elementClrType); - IDeltaLink deltaLink = Activator.CreateInstance(deltaLinkType) as IDeltaLink; + Type deltaLinkType = typeof(DeltaLink<>).MakeGenericType(elementClrType); + IDeltaLink deltaLink = Activator.CreateInstance(deltaLinkType) as IDeltaLink; - deltaLink.Source = link.DeltaLink.Source; - deltaLink.Target = link.DeltaLink.Target; - deltaLink.Relationship = link.DeltaLink.Relationship; - return deltaLink; - } + deltaLink.Source = link.DeltaLink.Source; + deltaLink.Target = link.DeltaLink.Target; + deltaLink.Relationship = link.DeltaLink.Relationship; + return deltaLink; } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializer.cs index 5cb28e45b..322a9b4dc 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializer.cs @@ -9,34 +9,33 @@ using System.Threading.Tasks; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// An is used to read an ODataMessage into a CLR object. +/// +/// +/// Each supported CLR type has a corresponding . A CLR type is supported if it is one of +/// the special types or if it has a backing EDM type. Some of the special types are Uri which maps to ODataReferenceLink payload, +/// Uri[] which maps to ODataReferenceLinks payload, ODataWorkspace which maps to ODataServiceDocument payload. +/// +public abstract class ODataDeserializer : IODataDeserializer { /// - /// An is used to read an ODataMessage into a CLR object. + /// Initializes a new instance of the class. /// - /// - /// Each supported CLR type has a corresponding . A CLR type is supported if it is one of - /// the special types or if it has a backing EDM type. Some of the special types are Uri which maps to ODataReferenceLink payload, - /// Uri[] which maps to ODataReferenceLinks payload, ODataWorkspace which maps to ODataServiceDocument payload. - /// - public abstract class ODataDeserializer : IODataDeserializer + /// The kind of payload this deserializer handles. + protected ODataDeserializer(ODataPayloadKind payloadKind) { - /// - /// Initializes a new instance of the class. - /// - /// The kind of payload this deserializer handles. - protected ODataDeserializer(ODataPayloadKind payloadKind) - { - ODataPayloadKind = payloadKind; - } + ODataPayloadKind = payloadKind; + } - /// - public ODataPayloadKind ODataPayloadKind { get; private set; } + /// + public ODataPayloadKind ODataPayloadKind { get; private set; } - /// - public virtual Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) - { - throw Error.NotSupported(SRResources.DeserializerDoesNotSupportRead, GetType().Name); - } + /// + public virtual Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + { + throw Error.NotSupported(SRResources.DeserializerDoesNotSupportRead, GetType().Name); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerContext.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerContext.cs index 65fc77756..e3d4c5af2 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerContext.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerContext.cs @@ -13,120 +13,119 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// This class encapsulates the state and settings that get passed to . +/// +public class ODataDeserializerContext { + private bool? _isDeltaOfT; + private bool? _isDeltaDeleted; + private bool? _isNoClrType; + + /// + /// Gets or sets the type of the top-level object the request needs to be deserialized into. + /// + public Type ResourceType { get; set; } + + /// + /// Gets or sets the of the top-level object the request needs to be deserialized into. + /// + public IEdmTypeReference ResourceEdmType { get; set; } + + /// + /// Gets or sets the of the request. + /// + public ODataPath Path { get; set; } + /// - /// This class encapsulates the state and settings that get passed to . + /// Gets or sets the EDM model associated with the request. /// - public class ODataDeserializerContext + public IEdmModel Model { get; set; } + + /// + /// Gets or sets the HTTP Request whose response is being serialized. + /// + public HttpRequest Request { get; set; } + + /// + /// Gets or sets the . + /// + public TimeZoneInfo TimeZone { get; set; } + + internal bool IsDeltaOfT { - private bool? _isDeltaOfT; - private bool? _isDeltaDeleted; - private bool? _isNoClrType; - - /// - /// Gets or sets the type of the top-level object the request needs to be deserialized into. - /// - public Type ResourceType { get; set; } - - /// - /// Gets or sets the of the top-level object the request needs to be deserialized into. - /// - public IEdmTypeReference ResourceEdmType { get; set; } - - /// - /// Gets or sets the of the request. - /// - public ODataPath Path { get; set; } - - /// - /// Gets or sets the EDM model associated with the request. - /// - public IEdmModel Model { get; set; } - - /// - /// Gets or sets the HTTP Request whose response is being serialized. - /// - public HttpRequest Request { get; set; } - - /// - /// Gets or sets the . - /// - public TimeZoneInfo TimeZone { get; set; } - - internal bool IsDeltaOfT + get { - get + if (!_isDeltaOfT.HasValue) { - if (!_isDeltaOfT.HasValue) - { - _isDeltaOfT = ResourceType != null && ResourceType.IsGenericType && - ResourceType.GetGenericTypeDefinition() == typeof(Delta<>); - } - - return _isDeltaOfT.Value; + _isDeltaOfT = ResourceType != null && ResourceType.IsGenericType && + ResourceType.GetGenericTypeDefinition() == typeof(Delta<>); } + + return _isDeltaOfT.Value; } + } - internal bool IsDeltaDeleted + internal bool IsDeltaDeleted + { + get { - get + if (_isDeltaDeleted == null) { - if (_isDeltaDeleted == null) + if (typeof(IEdmDeltaDeletedResourceObject).IsAssignableFrom(ResourceType)) { - if (typeof(IEdmDeltaDeletedResourceObject).IsAssignableFrom(ResourceType)) - { - _isDeltaDeleted = true; - } - else - { - _isDeltaDeleted = ResourceType != null && - ResourceType.IsGenericType && - ResourceType.GetGenericTypeDefinition() == typeof(DeltaDeletedResource<>); - } + _isDeltaDeleted = true; } - - return _isDeltaDeleted.Value; - } - } - - // TODO: need refactor this part. - // We can't only use the resource type to identify there's no Clr Type or not. - // We should use the model type mapping to identify there's no Clr Type or not. - internal bool IsNoClrType - { - get - { - if (!_isNoClrType.HasValue) + else { - _isNoClrType = (TypeHelper.IsTypeAssignableFrom(typeof(IEdmObject), ResourceType) && - (typeof(EdmUntypedObject) != ResourceType && typeof(EdmUntypedCollection) != ResourceType)) - || typeof(ODataUntypedActionParameters) == ResourceType; + _isDeltaDeleted = ResourceType != null && + ResourceType.IsGenericType && + ResourceType.GetGenericTypeDefinition() == typeof(DeltaDeletedResource<>); } - - return _isNoClrType.Value; } + + return _isDeltaDeleted.Value; } + } - internal IEdmTypeReference GetEdmType(Type type) + // TODO: need refactor this part. + // We can't only use the resource type to identify there's no Clr Type or not. + // We should use the model type mapping to identify there's no Clr Type or not. + internal bool IsNoClrType + { + get { - if (ResourceEdmType != null) + if (!_isNoClrType.HasValue) { - return ResourceEdmType; + _isNoClrType = (TypeHelper.IsTypeAssignableFrom(typeof(IEdmObject), ResourceType) && + (typeof(EdmUntypedObject) != ResourceType && typeof(EdmUntypedCollection) != ResourceType)) + || typeof(ODataUntypedActionParameters) == ResourceType; } - return EdmLibHelper.GetExpectedPayloadType(type, Path, Model); + return _isNoClrType.Value; } + } - internal ODataDeserializerContext CloneWithoutType() + internal IEdmTypeReference GetEdmType(Type type) + { + if (ResourceEdmType != null) { - return new ODataDeserializerContext - { - Path = this.Path, - Model = this.Model, - Request = this.Request, - TimeZone = this.TimeZone - }; + return ResourceEdmType; } + + return EdmLibHelper.GetExpectedPayloadType(type, Path, Model); + } + + internal ODataDeserializerContext CloneWithoutType() + { + return new ODataDeserializerContext + { + Path = this.Path, + Model = this.Model, + Request = this.Request, + TimeZone = this.TimeZone + }; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerProvider.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerProvider.cs index 485abb688..9a64f47d0 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerProvider.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataDeserializerProvider.cs @@ -15,116 +15,115 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// The default . +/// +public class ODataDeserializerProvider: IODataDeserializerProvider { + private readonly IServiceProvider _serviceProvider; + /// - /// The default . + /// Initializes a new instance of the class. /// - public class ODataDeserializerProvider: IODataDeserializerProvider + /// The service provider. + public ODataDeserializerProvider(IServiceProvider serviceProvider) { - private readonly IServiceProvider _serviceProvider; + _serviceProvider = serviceProvider ?? throw Error.ArgumentNull(nameof(serviceProvider)); + } + + /// + public virtual IODataEdmTypeDeserializer GetEdmTypeDeserializer(IEdmTypeReference edmType, bool isDelta = false) + { + if (edmType == null) + { + throw Error.ArgumentNull(nameof(edmType)); + } - /// - /// Initializes a new instance of the class. - /// - /// The service provider. - public ODataDeserializerProvider(IServiceProvider serviceProvider) + switch (edmType.TypeKind()) { - _serviceProvider = serviceProvider ?? throw Error.ArgumentNull(nameof(serviceProvider)); + case EdmTypeKind.Entity: + case EdmTypeKind.Complex: + case EdmTypeKind.Untyped: + return _serviceProvider.GetRequiredService(); + + case EdmTypeKind.Enum: + return _serviceProvider.GetRequiredService(); + + case EdmTypeKind.Primitive: + return _serviceProvider.GetRequiredService(); + + case EdmTypeKind.Collection: + if (isDelta) + { + return _serviceProvider.GetRequiredService(); + } + + IEdmTypeReference elementType = edmType.AsCollection().ElementType(); + if (elementType.IsEntity() || elementType.IsComplex() || elementType.IsUntyped()) + { + return _serviceProvider.GetRequiredService(); + } + else + { + return _serviceProvider.GetRequiredService(); + } + + default: + return null; } + } - /// - public virtual IODataEdmTypeDeserializer GetEdmTypeDeserializer(IEdmTypeReference edmType, bool isDelta = false) + /// + public virtual IODataDeserializer GetODataDeserializer(Type type, HttpRequest request) + { + if (type == null) { - if (edmType == null) - { - throw Error.ArgumentNull(nameof(edmType)); - } - - switch (edmType.TypeKind()) - { - case EdmTypeKind.Entity: - case EdmTypeKind.Complex: - case EdmTypeKind.Untyped: - return _serviceProvider.GetRequiredService(); - - case EdmTypeKind.Enum: - return _serviceProvider.GetRequiredService(); - - case EdmTypeKind.Primitive: - return _serviceProvider.GetRequiredService(); - - case EdmTypeKind.Collection: - if (isDelta) - { - return _serviceProvider.GetRequiredService(); - } - - IEdmTypeReference elementType = edmType.AsCollection().ElementType(); - if (elementType.IsEntity() || elementType.IsComplex() || elementType.IsUntyped()) - { - return _serviceProvider.GetRequiredService(); - } - else - { - return _serviceProvider.GetRequiredService(); - } - - default: - return null; - } + throw Error.ArgumentNull(nameof(type)); } - /// - public virtual IODataDeserializer GetODataDeserializer(Type type, HttpRequest request) + if (request == null) { - if (type == null) - { - throw Error.ArgumentNull(nameof(type)); - } - - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } - - if (type == typeof(Uri)) - { - return _serviceProvider.GetRequiredService(); - } - - if (type == typeof(ODataActionParameters) || type == typeof(ODataUntypedActionParameters)) - { - return _serviceProvider.GetRequiredService(); - } - - if (IsDelta(type)) - { - return _serviceProvider.GetRequiredService(); - } - - IEdmModel model = request.GetModel(); - IEdmTypeReference edmType = model.GetEdmTypeReference(type); - - if (edmType == null) - { - return null; - } - else - { - return GetEdmTypeDeserializer(edmType); - } + throw Error.ArgumentNull(nameof(request)); + } + + if (type == typeof(Uri)) + { + return _serviceProvider.GetRequiredService(); } - private static bool IsDelta(Type type) + if (type == typeof(ODataActionParameters) || type == typeof(ODataUntypedActionParameters)) { - if (type == typeof(EdmChangedObjectCollection) || - (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(DeltaSet<>))) - { - return true; - } + return _serviceProvider.GetRequiredService(); + } + + if (IsDelta(type)) + { + return _serviceProvider.GetRequiredService(); + } + + IEdmModel model = request.GetModel(); + IEdmTypeReference edmType = model.GetEdmTypeReference(type); + + if (edmType == null) + { + return null; + } + else + { + return GetEdmTypeDeserializer(edmType); + } + } - return false; + private static bool IsDelta(Type type) + { + if (type == typeof(EdmChangedObjectCollection) || + (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(DeltaSet<>))) + { + return true; } + + return false; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEdmTypeDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEdmTypeDeserializer.cs index 8c0bbee61..578909799 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEdmTypeDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEdmTypeDeserializer.cs @@ -8,47 +8,46 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// Base class for all s that deserialize into an object backed by . +/// +public abstract class ODataEdmTypeDeserializer : ODataDeserializer, IODataEdmTypeDeserializer { /// - /// Base class for all s that deserialize into an object backed by . + /// Initializes a new instance of the class. /// - public abstract class ODataEdmTypeDeserializer : ODataDeserializer, IODataEdmTypeDeserializer + /// The kind of OData payload that this deserializer reads. + protected ODataEdmTypeDeserializer(ODataPayloadKind payloadKind) + : base(payloadKind) { - /// - /// Initializes a new instance of the class. - /// - /// The kind of OData payload that this deserializer reads. - protected ODataEdmTypeDeserializer(ODataPayloadKind payloadKind) - : base(payloadKind) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The kind of OData payload this deserializer handles. - /// The . - protected ODataEdmTypeDeserializer(ODataPayloadKind payloadKind, IODataDeserializerProvider deserializerProvider) - : this(payloadKind) + /// + /// Initializes a new instance of the class. + /// + /// The kind of OData payload this deserializer handles. + /// The . + protected ODataEdmTypeDeserializer(ODataPayloadKind payloadKind, IODataDeserializerProvider deserializerProvider) + : this(payloadKind) + { + if (deserializerProvider == null) { - if (deserializerProvider == null) - { - throw Error.ArgumentNull("deserializerProvider"); - } - - DeserializerProvider = deserializerProvider; + throw Error.ArgumentNull("deserializerProvider"); } - /// - /// The to use for deserializing inner items. - /// - public IODataDeserializerProvider DeserializerProvider { get; private set; } + DeserializerProvider = deserializerProvider; + } - /// - public virtual object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) - { - throw Error.NotSupported(SRResources.DoesNotSupportReadInLine, GetType().Name); - } + /// + /// The to use for deserializing inner items. + /// + public IODataDeserializerProvider DeserializerProvider { get; private set; } + + /// + public virtual object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) + { + throw Error.NotSupported(SRResources.DoesNotSupportReadInLine, GetType().Name); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEntityReferenceLinkDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEntityReferenceLinkDeserializer.cs index 6c4b7c11a..eccd50836 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEntityReferenceLinkDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEntityReferenceLinkDeserializer.cs @@ -12,64 +12,63 @@ using Microsoft.AspNetCore.OData.Extensions; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// Represents an that can read OData entity reference link payloads. +/// +public class ODataEntityReferenceLinkDeserializer : ODataDeserializer { /// - /// Represents an that can read OData entity reference link payloads. + /// Initializes a new instance of the class. /// - public class ODataEntityReferenceLinkDeserializer : ODataDeserializer + public ODataEntityReferenceLinkDeserializer() + : base(ODataPayloadKind.EntityReferenceLink) { - /// - /// Initializes a new instance of the class. - /// - public ODataEntityReferenceLinkDeserializer() - : base(ODataPayloadKind.EntityReferenceLink) + } + + /// + public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + { + if (messageReader == null) { + throw Error.ArgumentNull(nameof(messageReader)); } - /// - public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + if (readContext == null) { - if (messageReader == null) - { - throw Error.ArgumentNull(nameof(messageReader)); - } - - if (readContext == null) - { - throw Error.ArgumentNull(nameof(readContext)); - } - - ODataEntityReferenceLink entityReferenceLink = await messageReader.ReadEntityReferenceLinkAsync().ConfigureAwait(false); + throw Error.ArgumentNull(nameof(readContext)); + } - if (entityReferenceLink != null) - { - return ResolveContentId(entityReferenceLink.Url, readContext); - } + ODataEntityReferenceLink entityReferenceLink = await messageReader.ReadEntityReferenceLinkAsync().ConfigureAwait(false); - return null; + if (entityReferenceLink != null) + { + return ResolveContentId(entityReferenceLink.Url, readContext); } - private static Uri ResolveContentId(Uri uri, ODataDeserializerContext readContext) + return null; + } + + private static Uri ResolveContentId(Uri uri, ODataDeserializerContext readContext) + { + if (uri != null) { - if (uri != null) + IDictionary contentIDToLocationMapping = readContext.Request.GetODataContentIdMapping(); + if (contentIDToLocationMapping != null) { - IDictionary contentIDToLocationMapping = readContext.Request.GetODataContentIdMapping(); - if (contentIDToLocationMapping != null) + Uri baseAddress = new Uri(readContext.Request.CreateODataLink()); + string relativeUrl = uri.IsAbsoluteUri ? baseAddress.MakeRelativeUri(uri).OriginalString : uri.OriginalString; + string resolvedUrl = ContentIdHelpers.ResolveContentId(relativeUrl, contentIDToLocationMapping); + Uri resolvedUri = new Uri(resolvedUrl, UriKind.RelativeOrAbsolute); + if (!resolvedUri.IsAbsoluteUri) { - Uri baseAddress = new Uri(readContext.Request.CreateODataLink()); - string relativeUrl = uri.IsAbsoluteUri ? baseAddress.MakeRelativeUri(uri).OriginalString : uri.OriginalString; - string resolvedUrl = ContentIdHelpers.ResolveContentId(relativeUrl, contentIDToLocationMapping); - Uri resolvedUri = new Uri(resolvedUrl, UriKind.RelativeOrAbsolute); - if (!resolvedUri.IsAbsoluteUri) - { - resolvedUri = new Uri(baseAddress, uri); - } - return resolvedUri; + resolvedUri = new Uri(baseAddress, uri); } + return resolvedUri; } - - return uri; } + + return uri; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs index ffcdf5c9e..c25943ade 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataEnumDeserializer.cs @@ -14,95 +14,94 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// Represents an that can read OData enum types. +/// +public class ODataEnumDeserializer : ODataEdmTypeDeserializer { /// - /// Represents an that can read OData enum types. + /// Initializes a new instance of the class. /// - public class ODataEnumDeserializer : ODataEdmTypeDeserializer + public ODataEnumDeserializer() + : base(ODataPayloadKind.Property) { - /// - /// Initializes a new instance of the class. - /// - public ODataEnumDeserializer() - : base(ODataPayloadKind.Property) + } + + /// + public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + { + if (messageReader == null) { + throw Error.ArgumentNull(nameof(messageReader)); } - /// - public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + if (type == null) { - if (messageReader == null) - { - throw Error.ArgumentNull(nameof(messageReader)); - } + throw Error.ArgumentNull(nameof(type)); + } - if (type == null) - { - throw Error.ArgumentNull(nameof(type)); - } + if (readContext == null) + { + throw Error.ArgumentNull(nameof(readContext)); + } - if (readContext == null) - { - throw Error.ArgumentNull(nameof(readContext)); - } + IEdmTypeReference edmType = readContext.GetEdmType(type); + Contract.Assert(edmType != null); - IEdmTypeReference edmType = readContext.GetEdmType(type); - Contract.Assert(edmType != null); + ODataProperty property = await messageReader.ReadPropertyAsync(edmType).ConfigureAwait(false); + return ReadInline(property, edmType, readContext); + } - ODataProperty property = await messageReader.ReadPropertyAsync(edmType).ConfigureAwait(false); - return ReadInline(property, edmType, readContext); + /// + public override object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) + { + if (item == null) + { + return null; } - /// - public override object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) + if (readContext == null) { - if (item == null) - { - return null; - } - - if (readContext == null) - { - throw Error.ArgumentNull(nameof(readContext)); - } + throw Error.ArgumentNull(nameof(readContext)); + } - ODataProperty property = item as ODataProperty; - if (property != null) - { - item = property.Value; - } + ODataProperty property = item as ODataProperty; + if (property != null) + { + item = property.Value; + } - IEdmEnumTypeReference enumTypeReference = edmType.AsEnum(); - ODataEnumValue enumValue = item as ODataEnumValue; - if (readContext.IsNoClrType) - { - Contract.Assert(edmType.TypeKind() == EdmTypeKind.Enum); - return new EdmEnumObject(enumTypeReference, enumValue.Value); - } + IEdmEnumTypeReference enumTypeReference = edmType.AsEnum(); + ODataEnumValue enumValue = item as ODataEnumValue; + if (readContext.IsNoClrType) + { + Contract.Assert(edmType.TypeKind() == EdmTypeKind.Enum); + return new EdmEnumObject(enumTypeReference, enumValue.Value); + } - IEdmEnumType enumType = enumTypeReference.EnumDefinition(); + IEdmEnumType enumType = enumTypeReference.EnumDefinition(); - // Enum member supports model alias case. So, try to use the Edm member name to retrieve the Enum value. - var memberMapAnnotation = readContext.Model.GetClrEnumMemberAnnotation(enumType); - if (memberMapAnnotation != null) + // Enum member supports model alias case. So, try to use the Edm member name to retrieve the Enum value. + var memberMapAnnotation = readContext.Model.GetClrEnumMemberAnnotation(enumType); + if (memberMapAnnotation != null) + { + if (enumValue != null) { - if (enumValue != null) + IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name == enumValue.Value); + if (enumMember != null) { - IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name == enumValue.Value); - if (enumMember != null) + var clrMember = memberMapAnnotation.GetClrEnumMember(enumMember); + if (clrMember != null) { - var clrMember = memberMapAnnotation.GetClrEnumMember(enumMember); - if (clrMember != null) - { - return clrMember; - } + return clrMember; } } } - - Type clrType = readContext.Model.GetClrType(edmType); - return EnumDeserializationHelpers.ConvertEnumValue(item, clrType); } + + Type clrType = readContext.Model.GetClrType(edmType); + return EnumDeserializationHelpers.ConvertEnumValue(item, clrType); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataPrimitiveDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataPrimitiveDeserializer.cs index e23d00692..b352fecfd 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataPrimitiveDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataPrimitiveDeserializer.cs @@ -12,83 +12,82 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// Represents an that can read OData primitive types. +/// +public class ODataPrimitiveDeserializer : ODataEdmTypeDeserializer { /// - /// Represents an that can read OData primitive types. + /// Initializes a new instance of the class. /// - public class ODataPrimitiveDeserializer : ODataEdmTypeDeserializer + public ODataPrimitiveDeserializer() + : base(ODataPayloadKind.Property) { - /// - /// Initializes a new instance of the class. - /// - public ODataPrimitiveDeserializer() - : base(ODataPayloadKind.Property) + } + + /// + public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + { + if (messageReader == null) { + throw new ArgumentNullException(nameof(messageReader)); } - /// - public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + if (readContext == null) { - if (messageReader == null) - { - throw new ArgumentNullException(nameof(messageReader)); - } + throw new ArgumentNullException(nameof(readContext)); + } - if (readContext == null) - { - throw new ArgumentNullException(nameof(readContext)); - } + IEdmTypeReference edmType = readContext.GetEdmType(type); + Contract.Assert(edmType != null); - IEdmTypeReference edmType = readContext.GetEdmType(type); - Contract.Assert(edmType != null); + ODataProperty property = await messageReader.ReadPropertyAsync(edmType).ConfigureAwait(false); + return ReadInline(property, edmType, readContext); + } - ODataProperty property = await messageReader.ReadPropertyAsync(edmType).ConfigureAwait(false); - return ReadInline(property, edmType, readContext); + /// + public sealed override object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) + { + if (item == null) + { + return null; } - /// - public sealed override object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) + ODataProperty property = item as ODataProperty; + if (property == null) { - if (item == null) - { - return null; - } + throw Error.Argument("item", SRResources.ArgumentMustBeOfType, typeof(ODataProperty).Name); + } - ODataProperty property = item as ODataProperty; - if (property == null) - { - throw Error.Argument("item", SRResources.ArgumentMustBeOfType, typeof(ODataProperty).Name); - } + return ReadPrimitive(property, readContext); + } - return ReadPrimitive(property, readContext); + /// + /// Deserializes the primitive from the given under the given . + /// + /// The primitive property to deserialize. + /// The deserializer context. + /// The deserialized OData primitive value. + public virtual object ReadPrimitive(ODataProperty primitiveProperty, ODataDeserializerContext readContext) + { + if (primitiveProperty == null) + { + throw Error.ArgumentNull("primitiveProperty"); } - /// - /// Deserializes the primitive from the given under the given . - /// - /// The primitive property to deserialize. - /// The deserializer context. - /// The deserialized OData primitive value. - public virtual object ReadPrimitive(ODataProperty primitiveProperty, ODataDeserializerContext readContext) + if (readContext == null) { - if (primitiveProperty == null) - { - throw Error.ArgumentNull("primitiveProperty"); - } - - if (readContext == null) - { - throw Error.ArgumentNull("readContext"); - } - - //Try and change the value appropriately if type is specified - if (readContext.ResourceType != null && primitiveProperty.Value != null) - { - return EdmPrimitiveHelper.ConvertPrimitiveValue(primitiveProperty.Value, readContext.ResourceType, readContext.TimeZone); - } + throw Error.ArgumentNull("readContext"); + } - return primitiveProperty.Value; + //Try and change the value appropriately if type is specified + if (readContext.ResourceType != null && primitiveProperty.Value != null) + { + return EdmPrimitiveHelper.ConvertPrimitiveValue(primitiveProperty.Value, readContext.ResourceType, readContext.TimeZone); } + + return primitiveProperty.Value; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceDeserializer.cs index 4fdc92d2b..bbe7a6b5f 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceDeserializer.cs @@ -29,934 +29,933 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// Represents an for reading OData resource payloads. +/// +public class ODataResourceDeserializer : ODataEdmTypeDeserializer { + private static readonly Regex ContentIdReferencePattern = new Regex(@"\$\d", RegexOptions.Compiled); + /// - /// Represents an for reading OData resource payloads. + /// Initializes a new instance of the class. /// - public class ODataResourceDeserializer : ODataEdmTypeDeserializer + /// The deserializer provider to use to read inner objects. + public ODataResourceDeserializer(IODataDeserializerProvider deserializerProvider) + : base(ODataPayloadKind.Resource, deserializerProvider) { - private static readonly Regex ContentIdReferencePattern = new Regex(@"\$\d", RegexOptions.Compiled); + } - /// - /// Initializes a new instance of the class. - /// - /// The deserializer provider to use to read inner objects. - public ODataResourceDeserializer(IODataDeserializerProvider deserializerProvider) - : base(ODataPayloadKind.Resource, deserializerProvider) + /// + public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + { + if (messageReader == null) { + throw new ArgumentNullException(nameof(messageReader)); } - /// - public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + if (readContext == null) { - if (messageReader == null) + throw new ArgumentNullException(nameof(readContext)); + } + + // TODO: any scenarios to read a top-level Edm.Untyped? + IEdmTypeReference edmType = readContext.GetEdmType(type); + Contract.Assert(edmType != null); + + if (!edmType.IsStructured()) + { + throw Error.Argument("type", SRResources.ArgumentMustBeOfType, "Structured"); + } + + IEdmStructuredTypeReference structuredType = edmType.AsStructured(); + + IEdmNavigationSource navigationSource = null; + if (structuredType.IsEntity()) + { + if (readContext.Path == null) { - throw new ArgumentNullException(nameof(messageReader)); + throw Error.Argument("readContext", SRResources.ODataPathMissing); } - if (readContext == null) + navigationSource = readContext.Path.GetNavigationSource(); + if (navigationSource == null) { - throw new ArgumentNullException(nameof(readContext)); + throw new SerializationException(SRResources.NavigationSourceMissingDuringDeserialization); } + } - // TODO: any scenarios to read a top-level Edm.Untyped? - IEdmTypeReference edmType = readContext.GetEdmType(type); - Contract.Assert(edmType != null); + ODataReader odataReader = await messageReader + .CreateODataResourceReaderAsync(navigationSource, structuredType.StructuredDefinition()).ConfigureAwait(false); + ODataResourceWrapper topLevelResource = await odataReader.ReadResourceOrResourceSetAsync().ConfigureAwait(false) + as ODataResourceWrapper; + Contract.Assert(topLevelResource != null); - if (!edmType.IsStructured()) - { - throw Error.Argument("type", SRResources.ArgumentMustBeOfType, "Structured"); - } + return ReadInline(topLevelResource, structuredType, readContext); + } - IEdmStructuredTypeReference structuredType = edmType.AsStructured(); + /// + public sealed override object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) + { + if (edmType == null) + { + throw new ArgumentNullException(nameof(edmType)); + } - IEdmNavigationSource navigationSource = null; - if (structuredType.IsEntity()) - { - if (readContext.Path == null) - { - throw Error.Argument("readContext", SRResources.ODataPathMissing); - } + if ((edmType.IsComplex() || edmType.IsUntyped()) && item == null) + { + return null; + } - navigationSource = readContext.Path.GetNavigationSource(); - if (navigationSource == null) - { - throw new SerializationException(SRResources.NavigationSourceMissingDuringDeserialization); - } - } + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } - ODataReader odataReader = await messageReader - .CreateODataResourceReaderAsync(navigationSource, structuredType.StructuredDefinition()).ConfigureAwait(false); - ODataResourceWrapper topLevelResource = await odataReader.ReadResourceOrResourceSetAsync().ConfigureAwait(false) - as ODataResourceWrapper; - Contract.Assert(topLevelResource != null); + if (!edmType.IsStructured() && !edmType.IsUntyped()) + { + throw Error.Argument("edmType", SRResources.ArgumentMustBeOfType, "Entity, Complex or Untyped"); + } - return ReadInline(topLevelResource, structuredType, readContext); + ODataResourceWrapper resourceWrapper = item as ODataResourceWrapper; + if (resourceWrapper == null) + { + throw Error.Argument("item", SRResources.ArgumentMustBeOfType, typeof(ODataResource).Name); } - /// - public sealed override object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) + // Recursion guard to avoid stack overflows + RuntimeHelpers.EnsureSufficientExecutionStack(); + + resourceWrapper = UpdateResourceWrapper(resourceWrapper, readContext); + + return ReadResource(resourceWrapper, edmType.ToStructuredTypeReference(), readContext); + } + + /// + /// Deserializes the given under the given . + /// + /// The OData resource to deserialize. + /// The type of the resource to deserialize. + /// The deserializer context. + /// The deserialized resource. + public virtual object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, + ODataDeserializerContext readContext) + { + if (resourceWrapper == null) + { + throw new ArgumentNullException(nameof(resourceWrapper)); + } + + if (readContext == null) + { + throw new ArgumentNullException(nameof(readContext)); + } + + if (!String.IsNullOrEmpty(resourceWrapper.Resource.TypeName) && + structuredType.FullName() != resourceWrapper.Resource.TypeName && + resourceWrapper.Resource.TypeName != "Edm.Untyped") { - if (edmType == null) + // received a derived type in a base type deserializer. delegate it to the appropriate derived type deserializer. + IEdmModel model = readContext.Model; + + if (model == null) { - throw new ArgumentNullException(nameof(edmType)); + throw Error.Argument("readContext", SRResources.ModelMissingFromReadContext); } - if ((edmType.IsComplex() || edmType.IsUntyped()) && item == null) + IEdmStructuredType actualType = model.FindType(resourceWrapper.Resource.TypeName) as IEdmStructuredType; + if (actualType == null) { - return null; + throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, resourceWrapper.Resource.TypeName)); } - if (item == null) + if (actualType.IsAbstract) { - throw new ArgumentNullException(nameof(item)); + string message = Error.Format(SRResources.CannotInstantiateAbstractResourceType, resourceWrapper.Resource.TypeName); + throw new ODataException(message); } - if (!edmType.IsStructured() && !edmType.IsUntyped()) + IEdmTypeReference actualStructuredType; + IEdmEntityType actualEntityType = actualType as IEdmEntityType; + if (actualEntityType != null) { - throw Error.Argument("edmType", SRResources.ArgumentMustBeOfType, "Entity, Complex or Untyped"); + actualStructuredType = new EdmEntityTypeReference(actualEntityType, isNullable: false); } - - ODataResourceWrapper resourceWrapper = item as ODataResourceWrapper; - if (resourceWrapper == null) + else { - throw Error.Argument("item", SRResources.ArgumentMustBeOfType, typeof(ODataResource).Name); + actualStructuredType = new EdmComplexTypeReference(actualType as IEdmComplexType, isNullable: false); } - // Recursion guard to avoid stack overflows - RuntimeHelpers.EnsureSufficientExecutionStack(); - - resourceWrapper = UpdateResourceWrapper(resourceWrapper, readContext); - - return ReadResource(resourceWrapper, edmType.ToStructuredTypeReference(), readContext); - } - - /// - /// Deserializes the given under the given . - /// - /// The OData resource to deserialize. - /// The type of the resource to deserialize. - /// The deserializer context. - /// The deserialized resource. - public virtual object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, - ODataDeserializerContext readContext) - { - if (resourceWrapper == null) + IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(actualStructuredType); + if (deserializer == null) { - throw new ArgumentNullException(nameof(resourceWrapper)); + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeDeserialized, actualStructuredType.FullName())); } - if (readContext == null) + object resource = deserializer.ReadInline(resourceWrapper, actualStructuredType, readContext); + + EdmStructuredObject structuredObject = resource as EdmStructuredObject; + if (structuredObject != null) { - throw new ArgumentNullException(nameof(readContext)); + structuredObject.ExpectedEdmType = structuredType.StructuredDefinition(); } - if (!String.IsNullOrEmpty(resourceWrapper.Resource.TypeName) && - structuredType.FullName() != resourceWrapper.Resource.TypeName && - resourceWrapper.Resource.TypeName != "Edm.Untyped") - { - // received a derived type in a base type deserializer. delegate it to the appropriate derived type deserializer. - IEdmModel model = readContext.Model; + return resource; + } + else + { + object resource = CreateResourceInstance(structuredType, readContext); + ApplyResourceProperties(resource, resourceWrapper, structuredType, readContext); + ApplyDeletedResource(resource, resourceWrapper, readContext); + return resource; + } + } - if (model == null) - { - throw Error.Argument("readContext", SRResources.ModelMissingFromReadContext); - } + /// + /// Creates a new instance of the backing CLR object for the given resource type. + /// + /// The EDM type of the resource to create. + /// The deserializer context. + /// The created CLR object. + public virtual object CreateResourceInstance(IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + { + if (structuredType == null) + { + throw Error.ArgumentNull(nameof(structuredType)); + } - IEdmStructuredType actualType = model.FindType(resourceWrapper.Resource.TypeName) as IEdmStructuredType; - if (actualType == null) - { - throw new ODataException(Error.Format(SRResources.ResourceTypeNotInModel, resourceWrapper.Resource.TypeName)); - } + if (readContext == null) + { + throw Error.ArgumentNull(nameof(readContext)); + } - if (actualType.IsAbstract) - { - string message = Error.Format(SRResources.CannotInstantiateAbstractResourceType, resourceWrapper.Resource.TypeName); - throw new ODataException(message); - } + IEdmModel model = readContext.Model; + if (model == null) + { + throw Error.Argument("readContext", SRResources.ModelMissingFromReadContext); + } - IEdmTypeReference actualStructuredType; - IEdmEntityType actualEntityType = actualType as IEdmEntityType; - if (actualEntityType != null) - { - actualStructuredType = new EdmEntityTypeReference(actualEntityType, isNullable: false); - } - else - { - actualStructuredType = new EdmComplexTypeReference(actualType as IEdmComplexType, isNullable: false); - } + if (structuredType.IsUntyped()) + { + return new EdmUntypedObject(); + } - IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(actualStructuredType); - if (deserializer == null) + if (readContext.IsNoClrType) + { + if (structuredType.IsEntity()) + { + if (readContext.IsDeltaDeleted) { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeDeserialized, actualStructuredType.FullName())); + return new EdmDeltaDeletedResourceObject(structuredType.AsEntity()); } - - object resource = deserializer.ReadInline(resourceWrapper, actualStructuredType, readContext); - - EdmStructuredObject structuredObject = resource as EdmStructuredObject; - if (structuredObject != null) + else { - structuredObject.ExpectedEdmType = structuredType.StructuredDefinition(); + return new EdmEntityObject(structuredType.AsEntity()); } - - return resource; - } - else - { - object resource = CreateResourceInstance(structuredType, readContext); - ApplyResourceProperties(resource, resourceWrapper, structuredType, readContext); - ApplyDeletedResource(resource, resourceWrapper, readContext); - return resource; } - } - /// - /// Creates a new instance of the backing CLR object for the given resource type. - /// - /// The EDM type of the resource to create. - /// The deserializer context. - /// The created CLR object. - public virtual object CreateResourceInstance(IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + return new EdmComplexObject(structuredType.AsComplex()); + } + else { - if (structuredType == null) + Type clrType = model.GetClrType(structuredType); + if (clrType == null) { - throw Error.ArgumentNull(nameof(structuredType)); + throw new ODataException( + Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName())); } - if (readContext == null) + if (readContext.IsDeltaOfT || readContext.IsDeltaDeleted) { - throw Error.ArgumentNull(nameof(readContext)); - } + IEnumerable updatablePoperties = model.GetAllProperties(structuredType.StructuredDefinition()); - IEdmModel model = readContext.Model; - if (model == null) - { - throw Error.Argument("readContext", SRResources.ModelMissingFromReadContext); - } - - if (structuredType.IsUntyped()) - { - return new EdmUntypedObject(); - } + if (structuredType.IsOpen()) + { + PropertyInfo dynamicDictionaryPropertyInfo = model.GetDynamicPropertyDictionary( + structuredType.StructuredDefinition()); - if (readContext.IsNoClrType) - { - if (structuredType.IsEntity()) + return Activator.CreateInstance(readContext.ResourceType, clrType, updatablePoperties, + dynamicDictionaryPropertyInfo, structuredType.IsComplex()); + } + else { - if (readContext.IsDeltaDeleted) - { - return new EdmDeltaDeletedResourceObject(structuredType.AsEntity()); - } - else - { - return new EdmEntityObject(structuredType.AsEntity()); - } + return Activator.CreateInstance(readContext.ResourceType, clrType, updatablePoperties, null, structuredType.IsComplex()); } - - return new EdmComplexObject(structuredType.AsComplex()); } else { - Type clrType = model.GetClrType(structuredType); - if (clrType == null) - { - throw new ODataException( - Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName())); - } + return Activator.CreateInstance(clrType); + } + } + } - if (readContext.IsDeltaOfT || readContext.IsDeltaDeleted) - { - IEnumerable updatablePoperties = model.GetAllProperties(structuredType.StructuredDefinition()); + /// + /// Deserializes the delete information from into . + /// + /// The object into which the nested properties should be read. + /// The resource object containing the nested properties. + /// The deserializer context. + public virtual void ApplyDeletedResource(object resource, ODataResourceWrapper resourceWrapper, ODataDeserializerContext readContext) + { + if (resourceWrapper == null) + { + throw Error.ArgumentNull(nameof(resourceWrapper)); + } - if (structuredType.IsOpen()) - { - PropertyInfo dynamicDictionaryPropertyInfo = model.GetDynamicPropertyDictionary( - structuredType.StructuredDefinition()); + if (readContext == null) + { + throw Error.ArgumentNull(nameof(readContext)); + } - return Activator.CreateInstance(readContext.ResourceType, clrType, updatablePoperties, - dynamicDictionaryPropertyInfo, structuredType.IsComplex()); - } - else - { - return Activator.CreateInstance(readContext.ResourceType, clrType, updatablePoperties, null, structuredType.IsComplex()); - } - } - else - { - return Activator.CreateInstance(clrType); - } - } + if (!resourceWrapper.IsDeletedResource) + { + return; } - /// - /// Deserializes the delete information from into . - /// - /// The object into which the nested properties should be read. - /// The resource object containing the nested properties. - /// The deserializer context. - public virtual void ApplyDeletedResource(object resource, ODataResourceWrapper resourceWrapper, ODataDeserializerContext readContext) + if (resourceWrapper.Resource is ODataDeletedResource deletedResource) { - if (resourceWrapper == null) + if (resource is IDeltaDeletedResource deltaDeletedResource) { - throw Error.ArgumentNull(nameof(resourceWrapper)); + // typed scenario + deltaDeletedResource.Id = deletedResource.Id; + deltaDeletedResource.Reason = deletedResource.Reason; } - - if (readContext == null) + else if (resource is IEdmDeltaDeletedResourceObject deletedObject) { - throw Error.ArgumentNull(nameof(readContext)); + // non-typed scenario + deletedObject.Id = deletedResource.Id; + deletedObject.Reason = deletedResource.Reason ?? DeltaDeletedEntryReason.Deleted; } + } + } - if (!resourceWrapper.IsDeletedResource) - { - return; - } + /// + /// Deserializes the nested properties from into . + /// + /// The object into which the nested properties should be read. + /// The resource object containing the nested properties. + /// The type of the resource. + /// The deserializer context. + public virtual void ApplyNestedProperties(object resource, ODataResourceWrapper resourceWrapper, + IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + { + if (resourceWrapper == null) + { + throw Error.ArgumentNull(nameof(resourceWrapper)); + } - if (resourceWrapper.Resource is ODataDeletedResource deletedResource) - { - if (resource is IDeltaDeletedResource deltaDeletedResource) - { - // typed scenario - deltaDeletedResource.Id = deletedResource.Id; - deltaDeletedResource.Reason = deletedResource.Reason; - } - else if (resource is IEdmDeltaDeletedResourceObject deletedObject) - { - // non-typed scenario - deletedObject.Id = deletedResource.Id; - deletedObject.Reason = deletedResource.Reason ?? DeltaDeletedEntryReason.Deleted; - } - } + foreach (ODataNestedResourceInfoWrapper nestedResourceInfo in resourceWrapper.NestedResourceInfos) + { + ApplyNestedProperty(resource, nestedResourceInfo, structuredType, readContext); } + } - /// - /// Deserializes the nested properties from into . - /// - /// The object into which the nested properties should be read. - /// The resource object containing the nested properties. - /// The type of the resource. - /// The deserializer context. - public virtual void ApplyNestedProperties(object resource, ODataResourceWrapper resourceWrapper, - IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + /// + /// Deserializes the nested property from into . + /// + /// The object into which the nested property should be read. + /// The nested resource info. + /// The type of the resource. + /// The deserializer context. + public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfoWrapper resourceInfoWrapper, + IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + { + if (resource == null) { - if (resourceWrapper == null) - { - throw Error.ArgumentNull(nameof(resourceWrapper)); - } + throw Error.ArgumentNull(nameof(resource)); + } - foreach (ODataNestedResourceInfoWrapper nestedResourceInfo in resourceWrapper.NestedResourceInfos) - { - ApplyNestedProperty(resource, nestedResourceInfo, structuredType, readContext); - } + if (resourceInfoWrapper == null) + { + throw Error.ArgumentNull(nameof(resourceInfoWrapper)); } - /// - /// Deserializes the nested property from into . - /// - /// The object into which the nested property should be read. - /// The nested resource info. - /// The type of the resource. - /// The deserializer context. - public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfoWrapper resourceInfoWrapper, - IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + // ODL has "FindProperty" method to find property using property name case-sensitive. + // We use "ResolveProperty" method to support case-insensitive + IEdmProperty edmProperty = structuredType.StructuredDefinition().ResolveProperty(resourceInfoWrapper.NestedResourceInfo.Name); + if (edmProperty == null) { - if (resource == null) + if (!structuredType.IsOpen()) { - throw Error.ArgumentNull(nameof(resource)); + throw new ODataException( + Error.Format(SRResources.NestedPropertyNotfound, resourceInfoWrapper.NestedResourceInfo.Name, + structuredType.FullName())); } + } - if (resourceInfoWrapper == null) + IList nestedItems; + ODataEntityReferenceLinkWrapper[] referenceLinks = resourceInfoWrapper.NestedItems.OfType().ToArray(); + if (referenceLinks.Length > 0) + { + // Be noted: + // 1) OData v4.0, it's "Orders@odata.bind", and we get "ODataEntityReferenceLinkWrapper"(s) for that. + // 2) OData v4.01, it's {"odata.id" ...}, and we get "ODataResource"(s) for that. + // So, in OData v4, if it's a single, NestedItems contains one ODataEntityReferenceLinkWrapper, + // if it's a collection, NestedItems contains multiple ODataEntityReferenceLinkWrapper(s) + // We can use the following codes to adjust the `ODataEntityReferenceLinkWrapper` to `ODataResourceWrapper`. + // In OData v4.01, we will not be here. + // Only supports declared property + Contract.Assert(edmProperty != null); + + nestedItems = new List(); + if (edmProperty.Type.IsCollection()) + { + IEdmCollectionTypeReference edmCollectionTypeReference = edmProperty.Type.AsCollection(); + ODataResourceSetWrapper resourceSetWrapper = CreateResourceSetWrapper(edmCollectionTypeReference, referenceLinks, readContext); + nestedItems.Add(resourceSetWrapper); + } + else { - throw Error.ArgumentNull(nameof(resourceInfoWrapper)); + ODataResourceWrapper resourceWrapper = CreateResourceWrapper(edmProperty.Type, referenceLinks[0], readContext); + nestedItems.Add(resourceWrapper); } + } + else + { + nestedItems = resourceInfoWrapper.NestedItems; + } - // ODL has "FindProperty" method to find property using property name case-sensitive. - // We use "ResolveProperty" method to support case-insensitive - IEdmProperty edmProperty = structuredType.StructuredDefinition().ResolveProperty(resourceInfoWrapper.NestedResourceInfo.Name); - if (edmProperty == null) + foreach (ODataItemWrapper childItem in nestedItems) + { + // it maybe null. + if (childItem == null) { - if (!structuredType.IsOpen()) + if (edmProperty == null) + { + // for the dynamic, OData.net has a bug. see https://github.com/OData/odata.net/issues/977 + ApplyDynamicResourceInNestedProperty(resourceInfoWrapper.NestedResourceInfo.Name, resource, + structuredType, null, readContext); + } + else { - throw new ODataException( - Error.Format(SRResources.NestedPropertyNotfound, resourceInfoWrapper.NestedResourceInfo.Name, - structuredType.FullName())); + ApplyResourceInNestedProperty(edmProperty, resource, null, readContext); } } - IList nestedItems; - ODataEntityReferenceLinkWrapper[] referenceLinks = resourceInfoWrapper.NestedItems.OfType().ToArray(); - if (referenceLinks.Length > 0) + ODataResourceSetWrapper resourceSetWrapper = childItem as ODataResourceSetWrapper; + if (resourceSetWrapper != null) { - // Be noted: - // 1) OData v4.0, it's "Orders@odata.bind", and we get "ODataEntityReferenceLinkWrapper"(s) for that. - // 2) OData v4.01, it's {"odata.id" ...}, and we get "ODataResource"(s) for that. - // So, in OData v4, if it's a single, NestedItems contains one ODataEntityReferenceLinkWrapper, - // if it's a collection, NestedItems contains multiple ODataEntityReferenceLinkWrapper(s) - // We can use the following codes to adjust the `ODataEntityReferenceLinkWrapper` to `ODataResourceWrapper`. - // In OData v4.01, we will not be here. - // Only supports declared property - Contract.Assert(edmProperty != null); - - nestedItems = new List(); - if (edmProperty.Type.IsCollection()) + if (edmProperty == null) { - IEdmCollectionTypeReference edmCollectionTypeReference = edmProperty.Type.AsCollection(); - ODataResourceSetWrapper resourceSetWrapper = CreateResourceSetWrapper(edmCollectionTypeReference, referenceLinks, readContext); - nestedItems.Add(resourceSetWrapper); + ApplyDynamicResourceSetInNestedProperty(resourceInfoWrapper.NestedResourceInfo.Name, + resource, structuredType, resourceSetWrapper, readContext); } else { - ODataResourceWrapper resourceWrapper = CreateResourceWrapper(edmProperty.Type, referenceLinks[0], readContext); - nestedItems.Add(resourceWrapper); + ApplyResourceSetInNestedProperty(edmProperty, resource, resourceSetWrapper, readContext); } + + continue; } - else + + if (childItem is ODataDeltaResourceSetWrapper deltaResourceSetWrapper) { - nestedItems = resourceInfoWrapper.NestedItems; + Contract.Assert(edmProperty != null, "nested delta resource cannot be dynamic property!"); + ApplyNestedDeltaResourceSet(edmProperty, resource, deltaResourceSetWrapper, readContext); + continue; } - foreach (ODataItemWrapper childItem in nestedItems) + // It must be resource by now. + ODataResourceWrapper resourceWrapper = (ODataResourceWrapper)childItem; + if (resourceWrapper != null) { - // it maybe null. - if (childItem == null) + if (edmProperty == null) { - if (edmProperty == null) - { - // for the dynamic, OData.net has a bug. see https://github.com/OData/odata.net/issues/977 - ApplyDynamicResourceInNestedProperty(resourceInfoWrapper.NestedResourceInfo.Name, resource, - structuredType, null, readContext); - } - else - { - ApplyResourceInNestedProperty(edmProperty, resource, null, readContext); - } + ApplyDynamicResourceInNestedProperty(resourceInfoWrapper.NestedResourceInfo.Name, resource, + structuredType, resourceWrapper, readContext); } - - ODataResourceSetWrapper resourceSetWrapper = childItem as ODataResourceSetWrapper; - if (resourceSetWrapper != null) - { - if (edmProperty == null) - { - ApplyDynamicResourceSetInNestedProperty(resourceInfoWrapper.NestedResourceInfo.Name, - resource, structuredType, resourceSetWrapper, readContext); - } - else - { - ApplyResourceSetInNestedProperty(edmProperty, resource, resourceSetWrapper, readContext); - } - - continue; - } - - if (childItem is ODataDeltaResourceSetWrapper deltaResourceSetWrapper) - { - Contract.Assert(edmProperty != null, "nested delta resource cannot be dynamic property!"); - ApplyNestedDeltaResourceSet(edmProperty, resource, deltaResourceSetWrapper, readContext); - continue; - } - - // It must be resource by now. - ODataResourceWrapper resourceWrapper = (ODataResourceWrapper)childItem; - if (resourceWrapper != null) + else { - if (edmProperty == null) - { - ApplyDynamicResourceInNestedProperty(resourceInfoWrapper.NestedResourceInfo.Name, resource, - structuredType, resourceWrapper, readContext); - } - else - { - ApplyResourceInNestedProperty(edmProperty, resource, resourceWrapper, readContext); - } + ApplyResourceInNestedProperty(edmProperty, resource, resourceWrapper, readContext); } } } + } - /// - /// Deserializes the structural properties from into . - /// - /// The object into which the structural properties should be read. - /// The resource object containing the structural properties. - /// The type of the resource. - /// The deserializer context. - public virtual void ApplyStructuralProperties(object resource, ODataResourceWrapper resourceWrapper, - IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + /// + /// Deserializes the structural properties from into . + /// + /// The object into which the structural properties should be read. + /// The resource object containing the structural properties. + /// The type of the resource. + /// The deserializer context. + public virtual void ApplyStructuralProperties(object resource, ODataResourceWrapper resourceWrapper, + IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + { + if (resourceWrapper == null) { - if (resourceWrapper == null) - { - throw new ArgumentNullException(nameof(resourceWrapper)); - } - - foreach (ODataProperty property in resourceWrapper.Resource.Properties) - { - ApplyStructuralProperty(resource, property, structuredType, readContext); - } + throw new ArgumentNullException(nameof(resourceWrapper)); } - /// - /// Deserializes the given into . - /// - /// The object into which the structural property should be read. - /// The structural property. - /// The type of the resource. - /// The deserializer context. - public virtual void ApplyStructuralProperty(object resource, ODataProperty structuralProperty, - IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + foreach (ODataProperty property in resourceWrapper.Resource.Properties) { - if (resource == null) - { - throw new ArgumentNullException(nameof(resource)); - } - - if (structuralProperty == null) - { - throw new ArgumentNullException(nameof(structuralProperty)); - } - - if (structuredType == null) - { - throw new ArgumentNullException(nameof(structuredType)); - } + ApplyStructuralProperty(resource, property, structuredType, readContext); + } + } - if (readContext == null) - { - throw new ArgumentNullException(nameof(readContext)); - } + /// + /// Deserializes the given into . + /// + /// The object into which the structural property should be read. + /// The structural property. + /// The type of the resource. + /// The deserializer context. + public virtual void ApplyStructuralProperty(object resource, ODataProperty structuralProperty, + IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + { + if (resource == null) + { + throw new ArgumentNullException(nameof(resource)); + } - DeserializationHelpers.ApplyProperty(structuralProperty, structuredType, resource, DeserializerProvider, readContext); + if (structuralProperty == null) + { + throw new ArgumentNullException(nameof(structuralProperty)); } - private void ApplyResourceProperties(object resource, ODataResourceWrapper resourceWrapper, - IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + if (structuredType == null) { - ApplyStructuralProperties(resource, resourceWrapper, structuredType, readContext); - ApplyNestedProperties(resource, resourceWrapper, structuredType, readContext); + throw new ArgumentNullException(nameof(structuredType)); } - private void ApplyResourceInNestedProperty(IEdmProperty nestedProperty, object resource, - ODataResourceWrapper resourceWrapper, ODataDeserializerContext readContext) + if (readContext == null) { - Contract.Assert(nestedProperty != null); - Contract.Assert(resource != null); - Contract.Assert(readContext != null); + throw new ArgumentNullException(nameof(readContext)); + } - // If the property is declared as "Edm.Untyped" and its value is a resource, - // Let's switch to use 'EdmUntypedStructuredTypeReference'. - IEdmTypeReference edmType = nestedProperty.Type.IsUntyped() ? - EdmUntypedStructuredTypeReference.NullableTypeReference : - nestedProperty.Type; + DeserializationHelpers.ApplyProperty(structuralProperty, structuredType, resource, DeserializerProvider, readContext); + } - object value = ReadNestedResourceInline(resourceWrapper, edmType, readContext); + private void ApplyResourceProperties(object resource, ODataResourceWrapper resourceWrapper, + IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) + { + ApplyStructuralProperties(resource, resourceWrapper, structuredType, readContext); + ApplyNestedProperties(resource, resourceWrapper, structuredType, readContext); + } - // First resolve Data member alias or annotation, then set the regular - // or delta resource accordingly. - string propertyName = readContext.Model.GetClrPropertyName(nestedProperty); + private void ApplyResourceInNestedProperty(IEdmProperty nestedProperty, object resource, + ODataResourceWrapper resourceWrapper, ODataDeserializerContext readContext) + { + Contract.Assert(nestedProperty != null); + Contract.Assert(resource != null); + Contract.Assert(readContext != null); - DeserializationHelpers.SetProperty(resource, propertyName, value); - } + // If the property is declared as "Edm.Untyped" and its value is a resource, + // Let's switch to use 'EdmUntypedStructuredTypeReference'. + IEdmTypeReference edmType = nestedProperty.Type.IsUntyped() ? + EdmUntypedStructuredTypeReference.NullableTypeReference : + nestedProperty.Type; - private void ApplyDynamicResourceInNestedProperty(string propertyName, object resource, IEdmStructuredTypeReference resourceStructuredType, - ODataResourceWrapper resourceWrapper, ODataDeserializerContext readContext) - { - Contract.Assert(resource != null); - Contract.Assert(readContext != null); + object value = ReadNestedResourceInline(resourceWrapper, edmType, readContext); - object value = null; - if (resourceWrapper != null) - { - IEdmTypeReference edmTypeReference; - if (resourceWrapper.Resource.TypeName == null || - string.Equals(resourceWrapper.Resource.TypeName, "Edm.Untyped", StringComparison.OrdinalIgnoreCase)) - { - edmTypeReference = EdmUntypedStructuredTypeReference.NullableTypeReference; - } - else - { - IEdmSchemaType elementType = readContext.Model.FindDeclaredType(resourceWrapper.Resource.TypeName); - edmTypeReference = elementType.ToEdmTypeReference(true); - } + // First resolve Data member alias or annotation, then set the regular + // or delta resource accordingly. + string propertyName = readContext.Model.GetClrPropertyName(nestedProperty); - value = ReadNestedResourceInline(resourceWrapper, edmTypeReference, readContext); - } + DeserializationHelpers.SetProperty(resource, propertyName, value); + } - DeserializationHelpers.SetDynamicProperty(resource, propertyName, value, - resourceStructuredType.StructuredDefinition(), readContext.Model); - } + private void ApplyDynamicResourceInNestedProperty(string propertyName, object resource, IEdmStructuredTypeReference resourceStructuredType, + ODataResourceWrapper resourceWrapper, ODataDeserializerContext readContext) + { + Contract.Assert(resource != null); + Contract.Assert(readContext != null); - private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IEdmTypeReference edmType, ODataDeserializerContext readContext) + object value = null; + if (resourceWrapper != null) { - Contract.Assert(edmType != null); - Contract.Assert(readContext != null); - - if (resourceWrapper == null) + IEdmTypeReference edmTypeReference; + if (resourceWrapper.Resource.TypeName == null || + string.Equals(resourceWrapper.Resource.TypeName, "Edm.Untyped", StringComparison.OrdinalIgnoreCase)) { - return null; + edmTypeReference = EdmUntypedStructuredTypeReference.NullableTypeReference; } - - IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(edmType); - if (deserializer == null) + else { - throw new SerializationException(Error.Format(SRResources.TypeCannotBeDeserialized, edmType.FullName())); + IEdmSchemaType elementType = readContext.Model.FindDeclaredType(resourceWrapper.Resource.TypeName); + edmTypeReference = elementType.ToEdmTypeReference(true); } - ODataDeserializerContext nestedReadContext = new ODataDeserializerContext - { - Path = readContext.Path, - Model = readContext.Model, - Request = readContext.Request, - TimeZone = readContext.TimeZone - }; - - if (edmType.IsUntyped()) - { - // We should use the given type name to replace the EdmType. - // If it's real untyped, use untyped object to read. - edmType = readContext.Model.ResolveResourceType(resourceWrapper.Resource); - if (edmType.IsUntyped()) - { - nestedReadContext.ResourceType = typeof(EdmUntypedObject); - return deserializer.ReadInline(resourceWrapper, edmType, nestedReadContext); - } - } + value = ReadNestedResourceInline(resourceWrapper, edmTypeReference, readContext); + } - IEdmStructuredTypeReference structuredType = edmType.AsStructured(); + DeserializationHelpers.SetDynamicProperty(resource, propertyName, value, + resourceStructuredType.StructuredDefinition(), readContext.Model); + } - Type clrType; - if (readContext.IsNoClrType) - { - clrType = structuredType.IsEntity() - ? typeof(EdmEntityObject) - : typeof(EdmComplexObject); - } - else - { - clrType = readContext.Model.GetClrType(structuredType); + private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IEdmTypeReference edmType, ODataDeserializerContext readContext) + { + Contract.Assert(edmType != null); + Contract.Assert(readContext != null); - if (clrType == null) - { - throw new ODataException( - Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName())); - } - } + if (resourceWrapper == null) + { + return null; + } - nestedReadContext.ResourceType = readContext.IsDeltaOfT - ? typeof(Delta<>).MakeGenericType(clrType) - : clrType; - return deserializer.ReadInline(resourceWrapper, edmType, nestedReadContext); + IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(edmType); + if (deserializer == null) + { + throw new SerializationException(Error.Format(SRResources.TypeCannotBeDeserialized, edmType.FullName())); } - private void ApplyResourceSetInNestedProperty(IEdmProperty nestedProperty, object resource, - ODataResourceSetWrapper resourceSetWrapper, ODataDeserializerContext readContext) + ODataDeserializerContext nestedReadContext = new ODataDeserializerContext { - Contract.Assert(nestedProperty != null); - Contract.Assert(resource != null); - Contract.Assert(readContext != null); + Path = readContext.Path, + Model = readContext.Model, + Request = readContext.Request, + TimeZone = readContext.TimeZone + }; - // If the property is declared as "Edm.Untyped" and its value could be a collection (resource set). - // If the property is declared as "Collection(Edm.Untyped)" and its value must be a resource set. - // Let's switch to use Collection of 'EdmUntypedStructuredTypeReference'. - IEdmTypeReference edmType = nestedProperty.Type; - if (edmType.IsUntypedOrCollectionUntyped()) + if (edmType.IsUntyped()) + { + // We should use the given type name to replace the EdmType. + // If it's real untyped, use untyped object to read. + edmType = readContext.Model.ResolveResourceType(resourceWrapper.Resource); + if (edmType.IsUntyped()) { - edmType = EdmUntypedHelpers.NullableUntypedCollectionReference; + nestedReadContext.ResourceType = typeof(EdmUntypedObject); + return deserializer.ReadInline(resourceWrapper, edmType, nestedReadContext); } + } - object value = ReadNestedResourceSetInline(resourceSetWrapper, edmType, readContext); + IEdmStructuredTypeReference structuredType = edmType.AsStructured(); - string propertyName = readContext.Model.GetClrPropertyName(nestedProperty); + Type clrType; + if (readContext.IsNoClrType) + { + clrType = structuredType.IsEntity() + ? typeof(EdmEntityObject) + : typeof(EdmComplexObject); + } + else + { + clrType = readContext.Model.GetClrType(structuredType); - if (nestedProperty.Type.IsUntyped()) - { - DeserializationHelpers.SetDeclaredProperty(resource, EdmTypeKind.Untyped, propertyName, value, nestedProperty, readContext); - } - else + if (clrType == null) { - DeserializationHelpers.SetCollectionProperty(resource, nestedProperty, value, propertyName); + throw new ODataException( + Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName())); } } - private void ApplyDynamicResourceSetInNestedProperty(string propertyName, object resource, IEdmStructuredTypeReference structuredType, - ODataResourceSetWrapper resourceSetWrapper, ODataDeserializerContext readContext) + nestedReadContext.ResourceType = readContext.IsDeltaOfT + ? typeof(Delta<>).MakeGenericType(clrType) + : clrType; + return deserializer.ReadInline(resourceWrapper, edmType, nestedReadContext); + } + + private void ApplyResourceSetInNestedProperty(IEdmProperty nestedProperty, object resource, + ODataResourceSetWrapper resourceSetWrapper, ODataDeserializerContext readContext) + { + Contract.Assert(nestedProperty != null); + Contract.Assert(resource != null); + Contract.Assert(readContext != null); + + // If the property is declared as "Edm.Untyped" and its value could be a collection (resource set). + // If the property is declared as "Collection(Edm.Untyped)" and its value must be a resource set. + // Let's switch to use Collection of 'EdmUntypedStructuredTypeReference'. + IEdmTypeReference edmType = nestedProperty.Type; + if (edmType.IsUntypedOrCollectionUntyped()) { - Contract.Assert(resource != null); - Contract.Assert(readContext != null); + edmType = EdmUntypedHelpers.NullableUntypedCollectionReference; + } - IEdmCollectionTypeReference collectionType = readContext.Model.ResolveResourceSetType(resourceSetWrapper.ResourceSet); + object value = ReadNestedResourceSetInline(resourceSetWrapper, edmType, readContext); - IEnumerable value = ReadNestedResourceSetInline(resourceSetWrapper, collectionType, readContext) as IEnumerable; - object result = value; - if (value != null) - { - if (readContext.IsNoClrType) - { - result = value.ConvertToEdmObject(collectionType); - } - } + string propertyName = readContext.Model.GetClrPropertyName(nestedProperty); - DeserializationHelpers.SetDynamicProperty(resource, structuredType, EdmTypeKind.Collection, propertyName, - result, collectionType, readContext.Model); + if (nestedProperty.Type.IsUntyped()) + { + DeserializationHelpers.SetDeclaredProperty(resource, EdmTypeKind.Untyped, propertyName, value, nestedProperty, readContext); } - - private object ReadNestedResourceSetInline(ODataResourceSetWrapper resourceSetWrapper, IEdmTypeReference edmType, - ODataDeserializerContext readContext) + else { - Contract.Assert(resourceSetWrapper != null); - Contract.Assert(edmType != null); - Contract.Assert(readContext != null); + DeserializationHelpers.SetCollectionProperty(resource, nestedProperty, value, propertyName); + } + } - IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(edmType); - if (deserializer == null) + private void ApplyDynamicResourceSetInNestedProperty(string propertyName, object resource, IEdmStructuredTypeReference structuredType, + ODataResourceSetWrapper resourceSetWrapper, ODataDeserializerContext readContext) + { + Contract.Assert(resource != null); + Contract.Assert(readContext != null); + + IEdmCollectionTypeReference collectionType = readContext.Model.ResolveResourceSetType(resourceSetWrapper.ResourceSet); + + IEnumerable value = ReadNestedResourceSetInline(resourceSetWrapper, collectionType, readContext) as IEnumerable; + object result = value; + if (value != null) + { + if (readContext.IsNoClrType) { - throw new SerializationException(Error.Format(SRResources.TypeCannotBeDeserialized, edmType.FullName())); + result = value.ConvertToEdmObject(collectionType); } + } - var elementType = edmType.AsCollection().ElementType(); - IEdmStructuredTypeReference structuredType = elementType.ToStructuredTypeReference(); + DeserializationHelpers.SetDynamicProperty(resource, structuredType, EdmTypeKind.Collection, propertyName, + result, collectionType, readContext.Model); + } - ODataDeserializerContext nestedReadContext = readContext.CloneWithoutType(); + private object ReadNestedResourceSetInline(ODataResourceSetWrapper resourceSetWrapper, IEdmTypeReference edmType, + ODataDeserializerContext readContext) + { + Contract.Assert(resourceSetWrapper != null); + Contract.Assert(edmType != null); + Contract.Assert(readContext != null); - if (elementType.IsUntyped()) - { - nestedReadContext.ResourceType = typeof(EdmUntypedCollection); - } - else if (readContext.IsNoClrType) + IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(edmType); + if (deserializer == null) + { + throw new SerializationException(Error.Format(SRResources.TypeCannotBeDeserialized, edmType.FullName())); + } + + var elementType = edmType.AsCollection().ElementType(); + IEdmStructuredTypeReference structuredType = elementType.ToStructuredTypeReference(); + + ODataDeserializerContext nestedReadContext = readContext.CloneWithoutType(); + + if (elementType.IsUntyped()) + { + nestedReadContext.ResourceType = typeof(EdmUntypedCollection); + } + else if (readContext.IsNoClrType) + { + if (structuredType.IsEntity()) { - if (structuredType.IsEntity()) - { - nestedReadContext.ResourceType = typeof(EdmEntityObjectCollection); - } - else - { - nestedReadContext.ResourceType = typeof(EdmComplexObjectCollection); - } + nestedReadContext.ResourceType = typeof(EdmEntityObjectCollection); } else { - Type clrType = readContext.Model.GetClrType(structuredType); - - if (clrType == null) - { - throw new ODataException( - Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName())); - } + nestedReadContext.ResourceType = typeof(EdmComplexObjectCollection); + } + } + else + { + Type clrType = readContext.Model.GetClrType(structuredType); - nestedReadContext.ResourceType = typeof(List<>).MakeGenericType(clrType); + if (clrType == null) + { + throw new ODataException( + Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName())); } - return deserializer.ReadInline(resourceSetWrapper, edmType, nestedReadContext); + nestedReadContext.ResourceType = typeof(List<>).MakeGenericType(clrType); } - private void ApplyNestedDeltaResourceSet(IEdmProperty nestedProperty, object resource, - ODataDeltaResourceSetWrapper deltaResourceSetWrapper, ODataDeserializerContext readContext) + return deserializer.ReadInline(resourceSetWrapper, edmType, nestedReadContext); + } + + private void ApplyNestedDeltaResourceSet(IEdmProperty nestedProperty, object resource, + ODataDeltaResourceSetWrapper deltaResourceSetWrapper, ODataDeserializerContext readContext) + { + Contract.Assert(nestedProperty != null); + Contract.Assert(resource != null); + Contract.Assert(readContext != null); + + IEdmTypeReference edmType = nestedProperty.Type; + IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(edmType, true); + if (deserializer == null) { - Contract.Assert(nestedProperty != null); - Contract.Assert(resource != null); - Contract.Assert(readContext != null); + throw new SerializationException(Error.Format(SRResources.TypeCannotBeDeserialized, edmType.FullName())); + } - IEdmTypeReference edmType = nestedProperty.Type; - IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(edmType, true); - if (deserializer == null) - { - throw new SerializationException(Error.Format(SRResources.TypeCannotBeDeserialized, edmType.FullName())); - } + IEdmStructuredTypeReference structuredType = edmType.AsCollection().ElementType().AsStructured(); + ODataDeserializerContext nestedReadContext = readContext.CloneWithoutType(); - IEdmStructuredTypeReference structuredType = edmType.AsCollection().ElementType().AsStructured(); - ODataDeserializerContext nestedReadContext = readContext.CloneWithoutType(); + if (readContext.IsNoClrType) + { + nestedReadContext.ResourceType = typeof(EdmChangedObjectCollection); + } + else + { + Type clrType = readContext.Model.GetClrType(structuredType); - if (readContext.IsNoClrType) + if (clrType == null) { - nestedReadContext.ResourceType = typeof(EdmChangedObjectCollection); + throw new ODataException( + Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName())); } - else - { - Type clrType = readContext.Model.GetClrType(structuredType); - if (clrType == null) - { - throw new ODataException( - Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName())); - } + nestedReadContext.ResourceType = typeof(DeltaSet<>).MakeGenericType(clrType); + } - nestedReadContext.ResourceType = typeof(DeltaSet<>).MakeGenericType(clrType); - } + object value = deserializer.ReadInline(deltaResourceSetWrapper, edmType, nestedReadContext); - object value = deserializer.ReadInline(deltaResourceSetWrapper, edmType, nestedReadContext); + string propertyName = readContext.Model.GetClrPropertyName(nestedProperty); + DeserializationHelpers.SetCollectionProperty(resource, nestedProperty, value, propertyName); + } - string propertyName = readContext.Model.GetClrPropertyName(nestedProperty); - DeserializationHelpers.SetCollectionProperty(resource, nestedProperty, value, propertyName); - } + /// + /// Create from a set of + /// + /// The Edm property type. + /// The reference links. + /// The reader context. + /// The created . + private static ODataResourceSetWrapper CreateResourceSetWrapper(IEdmCollectionTypeReference edmPropertyType, + ODataEntityReferenceLinkWrapper[] refLinks, ODataDeserializerContext readContext) + { + Contract.Assert(edmPropertyType != null); + Contract.Assert(refLinks != null); + Contract.Assert(readContext != null); - /// - /// Create from a set of - /// - /// The Edm property type. - /// The reference links. - /// The reader context. - /// The created . - private static ODataResourceSetWrapper CreateResourceSetWrapper(IEdmCollectionTypeReference edmPropertyType, - ODataEntityReferenceLinkWrapper[] refLinks, ODataDeserializerContext readContext) + ODataResourceSet resourceSet = new ODataResourceSet { - Contract.Assert(edmPropertyType != null); - Contract.Assert(refLinks != null); - Contract.Assert(readContext != null); + TypeName = edmPropertyType.FullName(), + }; - ODataResourceSet resourceSet = new ODataResourceSet - { - TypeName = edmPropertyType.FullName(), - }; + IEdmTypeReference elementType = edmPropertyType.ElementType(); + ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(resourceSet); + foreach (ODataEntityReferenceLinkWrapper refLinkWrapper in refLinks) + { + ODataResourceWrapper resourceWrapper = CreateResourceWrapper(elementType, refLinkWrapper, readContext); + resourceSetWrapper.Resources.Add(resourceWrapper); + resourceSetWrapper.Items.Add(resourceWrapper); // in the next major release, we should only use 'Items'. + } - IEdmTypeReference elementType = edmPropertyType.ElementType(); - ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(resourceSet); - foreach (ODataEntityReferenceLinkWrapper refLinkWrapper in refLinks) - { - ODataResourceWrapper resourceWrapper = CreateResourceWrapper(elementType, refLinkWrapper, readContext); - resourceSetWrapper.Resources.Add(resourceWrapper); - resourceSetWrapper.Items.Add(resourceWrapper); // in the next major release, we should only use 'Items'. - } + return resourceSetWrapper; + } - return resourceSetWrapper; - } + /// + /// Create from a + /// + /// The Edm property type. + /// The reference link. + /// The reader context. + /// The created . + private static ODataResourceWrapper CreateResourceWrapper(IEdmTypeReference edmPropertyType, ODataEntityReferenceLinkWrapper refLink, ODataDeserializerContext readContext) + { + Contract.Assert(edmPropertyType != null); + Contract.Assert(refLink != null); + Contract.Assert(readContext != null); - /// - /// Create from a - /// - /// The Edm property type. - /// The reference link. - /// The reader context. - /// The created . - private static ODataResourceWrapper CreateResourceWrapper(IEdmTypeReference edmPropertyType, ODataEntityReferenceLinkWrapper refLink, ODataDeserializerContext readContext) + ODataResource resource = new ODataResource { - Contract.Assert(edmPropertyType != null); - Contract.Assert(refLink != null); - Contract.Assert(readContext != null); + TypeName = edmPropertyType.FullName(), + }; - ODataResource resource = new ODataResource - { - TypeName = edmPropertyType.FullName(), - }; + resource.Properties = CreateKeyProperties(refLink.EntityReferenceLink.Url, readContext) ?? Array.Empty(); + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(resource); - resource.Properties = CreateKeyProperties(refLink.EntityReferenceLink.Url, readContext) ?? Array.Empty(); - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(resource); + foreach (ODataInstanceAnnotation instanceAnnotation in refLink.EntityReferenceLink.InstanceAnnotations) + { + resource.InstanceAnnotations.Add(instanceAnnotation); + } - foreach (ODataInstanceAnnotation instanceAnnotation in refLink.EntityReferenceLink.InstanceAnnotations) - { - resource.InstanceAnnotations.Add(instanceAnnotation); - } + return resourceWrapper; + } + /// + /// Update the resource wrapper if it's have the "Id" value. + /// + /// The resource wrapper. + /// The read context. + /// The resource wrapper. + private ODataResourceWrapper UpdateResourceWrapper(ODataResourceWrapper resourceWrapper, ODataDeserializerContext readContext) + { + Contract.Assert(readContext != null); + + if (resourceWrapper == null || + resourceWrapper.Resource == null || + resourceWrapper.Resource.Id == null) + { return resourceWrapper; } - /// - /// Update the resource wrapper if it's have the "Id" value. - /// - /// The resource wrapper. - /// The read context. - /// The resource wrapper. - private ODataResourceWrapper UpdateResourceWrapper(ODataResourceWrapper resourceWrapper, ODataDeserializerContext readContext) + IEnumerable keys = CreateKeyProperties(resourceWrapper.Resource.Id, readContext); + if (keys == null) { - Contract.Assert(readContext != null); + return resourceWrapper; + } - if (resourceWrapper == null || - resourceWrapper.Resource == null || - resourceWrapper.Resource.Id == null) + if (resourceWrapper.Resource.Properties == null) + { + resourceWrapper.Resource.Properties = keys; + } + else + { + IDictionary newPropertiesDic = resourceWrapper.Resource.Properties.ToDictionary(p => p.Name, p => p); + foreach (ODataPropertyInfo key in keys) { - return resourceWrapper; + // Logic: if we have the key property, try to keep the key property and get rid of the key value from ID. + // Need to double confirm whether it is the right logic? + if (!newPropertiesDic.ContainsKey(key.Name)) + { + newPropertiesDic[key.Name] = key; + } } - IEnumerable keys = CreateKeyProperties(resourceWrapper.Resource.Id, readContext); - if (keys == null) - { - return resourceWrapper; - } + resourceWrapper.Resource.Properties = newPropertiesDic.Values; + } - if (resourceWrapper.Resource.Properties == null) - { - resourceWrapper.Resource.Properties = keys; - } - else - { - IDictionary newPropertiesDic = resourceWrapper.Resource.Properties.ToDictionary(p => p.Name, p => p); - foreach (ODataPropertyInfo key in keys) - { - // Logic: if we have the key property, try to keep the key property and get rid of the key value from ID. - // Need to double confirm whether it is the right logic? - if (!newPropertiesDic.ContainsKey(key.Name)) - { - newPropertiesDic[key.Name] = key; - } - } + return resourceWrapper; + } - resourceWrapper.Resource.Properties = newPropertiesDic.Values; - } + /// + /// Do uri parsing to get the key values. + /// + /// The key Id. + /// The reader context. + /// The key properties. + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "")] + private static IList CreateKeyProperties(Uri id, ODataDeserializerContext readContext) + { + Contract.Assert(id != null); + Contract.Assert(readContext != null); - return resourceWrapper; + if (readContext.Request == null) + { + return null; } - /// - /// Do uri parsing to get the key values. - /// - /// The key Id. - /// The reader context. - /// The key properties. - [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "")] - private static IList CreateKeyProperties(Uri id, ODataDeserializerContext readContext) + try { - Contract.Assert(id != null); - Contract.Assert(readContext != null); + IEdmModel model = readContext.Model; + HttpRequest request = readContext.Request; + IServiceProvider requestContainer = request.GetRouteServices(); + Uri resolvedId = id; - if (readContext.Request == null) + string idOriginalString = id.OriginalString; + if (ContentIdReferencePattern.IsMatch(idOriginalString)) { - return null; + // We can expect request.ODataBatchFeature() to not be null + string resolvedUri = ContentIdHelpers.ResolveContentId( + idOriginalString, + request.ODataBatchFeature().ContentIdMapping); + resolvedId = new Uri(resolvedUri, UriKind.RelativeOrAbsolute); } - try + Uri serviceRootUri = new Uri(request.CreateODataLink()); + IODataPathParser pathParser = requestContainer?.GetService(); + if (pathParser == null) // Seems like IODataPathParser is NOT injected into DI container by default { - IEdmModel model = readContext.Model; - HttpRequest request = readContext.Request; - IServiceProvider requestContainer = request.GetRouteServices(); - Uri resolvedId = id; - - string idOriginalString = id.OriginalString; - if (ContentIdReferencePattern.IsMatch(idOriginalString)) - { - // We can expect request.ODataBatchFeature() to not be null - string resolvedUri = ContentIdHelpers.ResolveContentId( - idOriginalString, - request.ODataBatchFeature().ContentIdMapping); - resolvedId = new Uri(resolvedUri, UriKind.RelativeOrAbsolute); - } - - Uri serviceRootUri = new Uri(request.CreateODataLink()); - IODataPathParser pathParser = requestContainer?.GetService(); - if (pathParser == null) // Seems like IODataPathParser is NOT injected into DI container by default - { - pathParser = new DefaultODataPathParser(); - } + pathParser = new DefaultODataPathParser(); + } - IList properties = null; - ODataPath path = pathParser.Parse(model, serviceRootUri, resolvedId, requestContainer); - KeySegment keySegment = path.OfType().LastOrDefault(); - if (keySegment != null) + IList properties = null; + ODataPath path = pathParser.Parse(model, serviceRootUri, resolvedId, requestContainer); + KeySegment keySegment = path.OfType().LastOrDefault(); + if (keySegment != null) + { + properties = new List(); + foreach (KeyValuePair key in keySegment.Keys) { - properties = new List(); - foreach (KeyValuePair key in keySegment.Keys) + properties.Add(new ODataProperty { - properties.Add(new ODataProperty - { - Name = key.Key, - Value = key.Value - }); - } + Name = key.Key, + Value = key.Value + }); } - - return properties; - } - catch (Exception) - { - return null; } + + return properties; + } + catch (Exception) + { + return null; } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceSetDeserializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceSetDeserializer.cs index f055cc981..52f0711fa 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceSetDeserializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Deserialization/ODataResourceSetDeserializer.cs @@ -20,323 +20,322 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Formatter.Deserialization; + +/// +/// Represents an that can read OData resource sets. +/// +public class ODataResourceSetDeserializer : ODataEdmTypeDeserializer { + private static readonly MethodInfo CastMethodInfo = typeof(Enumerable).GetMethod("Cast"); + /// - /// Represents an that can read OData resource sets. + /// Initializes a new instance of the class. /// - public class ODataResourceSetDeserializer : ODataEdmTypeDeserializer + /// The deserializer provider to use to read inner objects. + public ODataResourceSetDeserializer(IODataDeserializerProvider deserializerProvider) + : base(ODataPayloadKind.ResourceSet, deserializerProvider) + { + } + + /// + public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) { - private static readonly MethodInfo CastMethodInfo = typeof(Enumerable).GetMethod("Cast"); - - /// - /// Initializes a new instance of the class. - /// - /// The deserializer provider to use to read inner objects. - public ODataResourceSetDeserializer(IODataDeserializerProvider deserializerProvider) - : base(ODataPayloadKind.ResourceSet, deserializerProvider) + if (messageReader == null) { + throw Error.ArgumentNull(nameof(messageReader)); } - /// - public override async Task ReadAsync(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext) + if (readContext == null) { - if (messageReader == null) - { - throw Error.ArgumentNull(nameof(messageReader)); - } - - if (readContext == null) - { - throw Error.ArgumentNull(nameof(readContext)); - } + throw Error.ArgumentNull(nameof(readContext)); + } - IEdmTypeReference edmType = readContext.GetEdmType(type); - Contract.Assert(edmType != null); + IEdmTypeReference edmType = readContext.GetEdmType(type); + Contract.Assert(edmType != null); - // TODO: is it OK to read the top level collection of entity? - if (!edmType.IsStructuredOrUntypedStructuredCollection()) - { - throw Error.Argument("edmType", SRResources.ArgumentMustBeOfType, "Collection of complex, entity or untyped"); - } + // TODO: is it OK to read the top level collection of entity? + if (!edmType.IsStructuredOrUntypedStructuredCollection()) + { + throw Error.Argument("edmType", SRResources.ArgumentMustBeOfType, "Collection of complex, entity or untyped"); + } - IEdmStructuredType structuredType = edmType.AsCollection().ElementType().Definition as IEdmStructuredType; + IEdmStructuredType structuredType = edmType.AsCollection().ElementType().Definition as IEdmStructuredType; - ODataReader resourceSetReader = await messageReader.CreateODataResourceSetReaderAsync(structuredType).ConfigureAwait(false); - object resourceSet = await resourceSetReader.ReadResourceOrResourceSetAsync().ConfigureAwait(false); - return ReadInline(resourceSet, edmType, readContext); - } + ODataReader resourceSetReader = await messageReader.CreateODataResourceSetReaderAsync(structuredType).ConfigureAwait(false); + object resourceSet = await resourceSetReader.ReadResourceOrResourceSetAsync().ConfigureAwait(false); + return ReadInline(resourceSet, edmType, readContext); + } - /// - public sealed override object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) + /// + public sealed override object ReadInline(object item, IEdmTypeReference edmType, ODataDeserializerContext readContext) + { + if (item == null) { - if (item == null) - { - return null; - } + return null; + } - IEdmTypeReference edmElementType = VerifyAndGetElementType(edmType, readContext); + IEdmTypeReference edmElementType = VerifyAndGetElementType(edmType, readContext); - ODataResourceSetWrapper resourceSet = item as ODataResourceSetWrapper; - if (resourceSet == null) - { - throw Error.Argument(nameof(item), SRResources.ArgumentMustBeOfType, typeof(ODataResourceSetWrapper).Name); - } + ODataResourceSetWrapper resourceSet = item as ODataResourceSetWrapper; + if (resourceSet == null) + { + throw Error.Argument(nameof(item), SRResources.ArgumentMustBeOfType, typeof(ODataResourceSetWrapper).Name); + } - // Recursion guard to avoid stack overflows - RuntimeHelpers.EnsureSufficientExecutionStack(); + // Recursion guard to avoid stack overflows + RuntimeHelpers.EnsureSufficientExecutionStack(); - IEdmStructuredTypeReference elementType = edmElementType.IsUntyped() ? - EdmUntypedStructuredTypeReference.NullableTypeReference : - edmElementType.AsStructured(); + IEdmStructuredTypeReference elementType = edmElementType.IsUntyped() ? + EdmUntypedStructuredTypeReference.NullableTypeReference : + edmElementType.AsStructured(); - IEnumerable result = ReadResourceSet(resourceSet, elementType, readContext); - if (edmElementType.IsUntyped()) + IEnumerable result = ReadResourceSet(resourceSet, elementType, readContext); + if (edmElementType.IsUntyped()) + { + EdmUntypedCollection untypedList = new EdmUntypedCollection(); + foreach (var element in result) { - EdmUntypedCollection untypedList = new EdmUntypedCollection(); - foreach (var element in result) - { - untypedList.Add(element); - } - - return untypedList; + untypedList.Add(element); } - if (result != null && elementType.IsComplex()) + return untypedList; + } + + if (result != null && elementType.IsComplex()) + { + if (readContext.IsNoClrType) { - if (readContext.IsNoClrType) - { - EdmComplexObjectCollection complexCollection = new EdmComplexObjectCollection(edmType.AsCollection()); - foreach (EdmComplexObject complexObject in result) - { - complexCollection.Add(complexObject); - } - return complexCollection; - } - else + EdmComplexObjectCollection complexCollection = new EdmComplexObjectCollection(edmType.AsCollection()); + foreach (EdmComplexObject complexObject in result) { - Type elementClrType = readContext.Model.GetClrType(elementType); - IEnumerable castedResult = - CastMethodInfo.MakeGenericMethod(elementClrType).Invoke(null, new object[] { result }) as - IEnumerable; - return castedResult; + complexCollection.Add(complexObject); } + return complexCollection; } else { - return result; + Type elementClrType = readContext.Model.GetClrType(elementType); + IEnumerable castedResult = + CastMethodInfo.MakeGenericMethod(elementClrType).Invoke(null, new object[] { result }) as + IEnumerable; + return castedResult; } } - - /// - /// Deserializes the given under the given . - /// - /// The resource set to deserialize. - /// The deserializer context. - /// The element type of the resource set being read. - /// The deserialized resource set object. - public virtual IEnumerable ReadResourceSet(ODataResourceSetWrapper resourceSet, IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + else { - if (resourceSet == null) - { - throw Error.ArgumentNull(nameof(resourceSet)); - } - - IList items = GetItems(resourceSet); - foreach (ODataItemWrapper wrapper in items) - { - if (wrapper == null) - { - yield return null; - } - else if (wrapper is ODataPrimitiveWrapper primitiveResourceWrapper) - { - yield return ReadPrimitiveItem(primitiveResourceWrapper, elementType, readContext); - } - else if (wrapper is ODataResourceWrapper resourceWrapper) - { - yield return ReadResourceItem(resourceWrapper, elementType, readContext); - } - else if (wrapper is ODataResourceSetWrapper resourceSetWrapper) - { - yield return ReadResourceSetItem(resourceSetWrapper, elementType, readContext); - } - } + return result; } + } - /// - /// Deserializes the given under the given . - /// - /// The primitive item in a set to deserialize. - /// The element type of the parent resource set being read. - /// The deserializer context. - /// The deserialized primitive object. - public virtual object ReadPrimitiveItem(ODataPrimitiveWrapper primitiveWrapper, IEdmTypeReference elementType, ODataDeserializerContext readContext) + /// + /// Deserializes the given under the given . + /// + /// The resource set to deserialize. + /// The deserializer context. + /// The element type of the resource set being read. + /// The deserialized resource set object. + public virtual IEnumerable ReadResourceSet(ODataResourceSetWrapper resourceSet, IEdmStructuredTypeReference elementType, ODataDeserializerContext readContext) + { + if (resourceSet == null) { - if (primitiveWrapper == null) - { - throw Error.ArgumentNull(nameof(primitiveWrapper)); - } - - return primitiveWrapper.Value.Value; + throw Error.ArgumentNull(nameof(resourceSet)); } - /// - /// Deserializes the given under the given . - /// - /// The resource item in a set to deserialize. - /// The element type of the parent resource set being read. - /// The deserializer context. - /// The deserialized resource object. - public virtual object ReadResourceItem(ODataResourceWrapper resourceWrapper, IEdmTypeReference elementType, ODataDeserializerContext readContext) + IList items = GetItems(resourceSet); + foreach (ODataItemWrapper wrapper in items) { - if (resourceWrapper == null) + if (wrapper == null) { - throw Error.ArgumentNull(nameof(resourceWrapper)); + yield return null; } - - IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(elementType); - if (deserializer == null) + else if (wrapper is ODataPrimitiveWrapper primitiveResourceWrapper) { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeDeserialized, elementType.FullName())); + yield return ReadPrimitiveItem(primitiveResourceWrapper, elementType, readContext); } - - ODataDeserializerContext nestedReadContext = readContext.CloneWithoutType(); - if (elementType == null || elementType.IsUntyped()) + else if (wrapper is ODataResourceWrapper resourceWrapper) { - // We should use the given type name to read - elementType = readContext.Model.ResolveResourceType(resourceWrapper.Resource); - if (elementType.IsUntyped()) - { - nestedReadContext.ResourceType = typeof(EdmUntypedObject); - } + yield return ReadResourceItem(resourceWrapper, elementType, readContext); } - - if (nestedReadContext.ResourceType == null) + else if (wrapper is ODataResourceSetWrapper resourceSetWrapper) { - if (readContext.IsNoClrType) - { - if (elementType.IsEntity()) - { - nestedReadContext.ResourceType = typeof(EdmEntityObject); - } - else - { - nestedReadContext.ResourceType = typeof(EdmComplexObject); - } - } - else - { - Type clrType = readContext.Model.GetClrType(elementType); - if (clrType == null) - { - throw new ODataException( - Error.Format(SRResources.MappingDoesNotContainResourceType, elementType.FullName())); - } - - nestedReadContext.ResourceType = clrType; - } + yield return ReadResourceSetItem(resourceSetWrapper, elementType, readContext); } - - return deserializer.ReadInline(resourceWrapper, elementType, nestedReadContext); } + } - /// - /// Deserializes the given under the given . - /// - /// The resource set item in a set to deserialize. - /// The element type of the parent resource set being read. - /// The deserializer context. - /// The deserialized resource set object. - public virtual object ReadResourceSetItem(ODataResourceSetWrapper resourceSetWrapper, IEdmTypeReference elementType, ODataDeserializerContext readContext) + /// + /// Deserializes the given under the given . + /// + /// The primitive item in a set to deserialize. + /// The element type of the parent resource set being read. + /// The deserializer context. + /// The deserialized primitive object. + public virtual object ReadPrimitiveItem(ODataPrimitiveWrapper primitiveWrapper, IEdmTypeReference elementType, ODataDeserializerContext readContext) + { + if (primitiveWrapper == null) { - if (resourceSetWrapper == null) - { - throw Error.ArgumentNull(nameof(resourceSetWrapper)); - } - - IEdmCollectionTypeReference edmType = readContext.Model.ResolveResourceSetType(resourceSetWrapper.ResourceSet); + throw Error.ArgumentNull(nameof(primitiveWrapper)); + } - IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(edmType); - if (deserializer == null) - { - throw new SerializationException(Error.Format(SRResources.TypeCannotBeDeserialized, edmType.FullName())); - } + return primitiveWrapper.Value.Value; + } - IEdmTypeReference setItemElementType = edmType.AsCollection().ElementType(); - IEdmStructuredTypeReference structuredType = setItemElementType.ToStructuredTypeReference(); + /// + /// Deserializes the given under the given . + /// + /// The resource item in a set to deserialize. + /// The element type of the parent resource set being read. + /// The deserializer context. + /// The deserialized resource object. + public virtual object ReadResourceItem(ODataResourceWrapper resourceWrapper, IEdmTypeReference elementType, ODataDeserializerContext readContext) + { + if (resourceWrapper == null) + { + throw Error.ArgumentNull(nameof(resourceWrapper)); + } - ODataDeserializerContext nestedReadContext = readContext.CloneWithoutType(); + IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(elementType); + if (deserializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeDeserialized, elementType.FullName())); + } - if (setItemElementType.IsUntyped()) + ODataDeserializerContext nestedReadContext = readContext.CloneWithoutType(); + if (elementType == null || elementType.IsUntyped()) + { + // We should use the given type name to read + elementType = readContext.Model.ResolveResourceType(resourceWrapper.Resource); + if (elementType.IsUntyped()) { - nestedReadContext.ResourceType = typeof(EdmUntypedCollection); + nestedReadContext.ResourceType = typeof(EdmUntypedObject); } - else if (readContext.IsNoClrType) + } + + if (nestedReadContext.ResourceType == null) + { + if (readContext.IsNoClrType) { - if (structuredType.IsEntity()) + if (elementType.IsEntity()) { - nestedReadContext.ResourceType = typeof(EdmEntityObjectCollection); + nestedReadContext.ResourceType = typeof(EdmEntityObject); } else { - nestedReadContext.ResourceType = typeof(EdmComplexObjectCollection); + nestedReadContext.ResourceType = typeof(EdmComplexObject); } } else { - Type clrType = readContext.Model.GetClrType(structuredType); - + Type clrType = readContext.Model.GetClrType(elementType); if (clrType == null) { throw new ODataException( - Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName())); + Error.Format(SRResources.MappingDoesNotContainResourceType, elementType.FullName())); } - nestedReadContext.ResourceType = typeof(List<>).MakeGenericType(clrType); + nestedReadContext.ResourceType = clrType; } - - return deserializer.ReadInline(resourceSetWrapper, edmType, nestedReadContext); } - private static IList GetItems(ODataResourceSetWrapper setWrapper) + return deserializer.ReadInline(resourceWrapper, elementType, nestedReadContext); + } + + /// + /// Deserializes the given under the given . + /// + /// The resource set item in a set to deserialize. + /// The element type of the parent resource set being read. + /// The deserializer context. + /// The deserialized resource set object. + public virtual object ReadResourceSetItem(ODataResourceSetWrapper resourceSetWrapper, IEdmTypeReference elementType, ODataDeserializerContext readContext) + { + if (resourceSetWrapper == null) { - // Could have extra 'resources' added by customer, since it's very very low possibility. - // So, let's use this logic to avoid potential breaking. - var extras = setWrapper.Resources.Except(setWrapper.Items).ToList(); - foreach (ODataItemWrapper itemWrapper in extras) - { - setWrapper.Items.Add(itemWrapper); - } + throw Error.ArgumentNull(nameof(resourceSetWrapper)); + } - return setWrapper.Items; + IEdmCollectionTypeReference edmType = readContext.Model.ResolveResourceSetType(resourceSetWrapper.ResourceSet); + + IODataEdmTypeDeserializer deserializer = DeserializerProvider.GetEdmTypeDeserializer(edmType); + if (deserializer == null) + { + throw new SerializationException(Error.Format(SRResources.TypeCannotBeDeserialized, edmType.FullName())); } - private IEdmTypeReference VerifyAndGetElementType(IEdmTypeReference edmType, ODataDeserializerContext readContext) + IEdmTypeReference setItemElementType = edmType.AsCollection().ElementType(); + IEdmStructuredTypeReference structuredType = setItemElementType.ToStructuredTypeReference(); + + ODataDeserializerContext nestedReadContext = readContext.CloneWithoutType(); + + if (setItemElementType.IsUntyped()) { - if (edmType == null) + nestedReadContext.ResourceType = typeof(EdmUntypedCollection); + } + else if (readContext.IsNoClrType) + { + if (structuredType.IsEntity()) { - throw Error.ArgumentNull(nameof(edmType)); + nestedReadContext.ResourceType = typeof(EdmEntityObjectCollection); } - - if (readContext == null) + else { - throw Error.ArgumentNull(nameof(readContext)); + nestedReadContext.ResourceType = typeof(EdmComplexObjectCollection); } + } + else + { + Type clrType = readContext.Model.GetClrType(structuredType); - if (!edmType.IsCollection()) + if (clrType == null) { - throw Error.Argument(nameof(edmType), SRResources.TypeMustBeResourceSet, edmType.ToTraceString()); + throw new ODataException( + Error.Format(SRResources.MappingDoesNotContainResourceType, structuredType.FullName())); } - IEdmTypeReference edmElementType = edmType.AsCollection().ElementType(); - if (!edmElementType.IsStructured() && !edmElementType.IsUntyped()) - { - throw Error.Argument(nameof(edmType), SRResources.TypeMustBeResourceSet, edmType.ToTraceString()); - } + nestedReadContext.ResourceType = typeof(List<>).MakeGenericType(clrType); + } - return edmElementType; + return deserializer.ReadInline(resourceSetWrapper, edmType, nestedReadContext); + } + + private static IList GetItems(ODataResourceSetWrapper setWrapper) + { + // Could have extra 'resources' added by customer, since it's very very low possibility. + // So, let's use this logic to avoid potential breaking. + var extras = setWrapper.Resources.Except(setWrapper.Items).ToList(); + foreach (ODataItemWrapper itemWrapper in extras) + { + setWrapper.Items.Add(itemWrapper); } + + return setWrapper.Items; + } + + private IEdmTypeReference VerifyAndGetElementType(IEdmTypeReference edmType, ODataDeserializerContext readContext) + { + if (edmType == null) + { + throw Error.ArgumentNull(nameof(edmType)); + } + + if (readContext == null) + { + throw Error.ArgumentNull(nameof(readContext)); + } + + if (!edmType.IsCollection()) + { + throw Error.Argument(nameof(edmType), SRResources.TypeMustBeResourceSet, edmType.ToTraceString()); + } + + IEdmTypeReference edmElementType = edmType.AsCollection().ElementType(); + if (!edmElementType.IsStructured() && !edmElementType.IsUntyped()) + { + throw Error.Argument(nameof(edmType), SRResources.TypeMustBeResourceSet, edmType.ToTraceString()); + } + + return edmElementType; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/EdmLibHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/EdmLibHelper.cs index 0c584233c..e9ea1579d 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/EdmLibHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/EdmLibHelper.cs @@ -14,68 +14,67 @@ using Microsoft.AspNetCore.OData.Edm; using Microsoft.AspNetCore.OData.Deltas; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +internal static class EdmLibHelper { - internal static class EdmLibHelper + /// + /// Get the expected payload type of an OData path. + /// + /// The Type to use. + /// The path to use. + /// The EdmModel to use. + /// The expected payload type of an OData path. + internal static IEdmTypeReference GetExpectedPayloadType(Type type, ODataPath path, IEdmModel model) { - /// - /// Get the expected payload type of an OData path. - /// - /// The Type to use. - /// The path to use. - /// The EdmModel to use. - /// The expected payload type of an OData path. - internal static IEdmTypeReference GetExpectedPayloadType(Type type, ODataPath path, IEdmModel model) - { - IEdmTypeReference expectedPayloadType = null; + IEdmTypeReference expectedPayloadType = null; - if (typeof(IEdmObject).IsAssignableFrom(type)) + if (typeof(IEdmObject).IsAssignableFrom(type)) + { + // typeless mode. figure out the expected payload type from the OData Path. + IEdmType edmType = path.LastSegment.EdmType; + if (edmType != null) { - // typeless mode. figure out the expected payload type from the OData Path. - IEdmType edmType = path.LastSegment.EdmType; - if (edmType != null) + expectedPayloadType = edmType.ToEdmTypeReference(isNullable: false); + if (expectedPayloadType.TypeKind() == EdmTypeKind.Collection) { - expectedPayloadType = edmType.ToEdmTypeReference(isNullable: false); - if (expectedPayloadType.TypeKind() == EdmTypeKind.Collection) + IEdmTypeReference elementType = expectedPayloadType.AsCollection().ElementType(); + if (elementType.IsEntity()) { - IEdmTypeReference elementType = expectedPayloadType.AsCollection().ElementType(); - if (elementType.IsEntity()) - { - // collection of entities cannot be CREATE/UPDATEd. Instead, the request would contain a single entry. - expectedPayloadType = elementType; - } + // collection of entities cannot be CREATE/UPDATEd. Instead, the request would contain a single entry. + expectedPayloadType = elementType; } } } - else - { - TryGetInnerTypeForDelta(ref type); - expectedPayloadType = model.GetEdmTypeReference(type); - } - - return expectedPayloadType; } - - /// - /// Try to return the inner type of a generic Delta. - /// - /// in: The type to test; out: inner type of a generic Delta. - /// True if the type was generic Delta; false otherwise. - internal static bool TryGetInnerTypeForDelta(ref Type type) + else { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Delta<>)) - { - type = type.GetGenericArguments()[0]; - return true; - } + TryGetInnerTypeForDelta(ref type); + expectedPayloadType = model.GetEdmTypeReference(type); + } - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(DeltaSet<>)) - { - type = type.GetGenericArguments()[0]; - return true; - } + return expectedPayloadType; + } - return false; + /// + /// Try to return the inner type of a generic Delta. + /// + /// in: The type to test; out: inner type of a generic Delta. + /// True if the type was generic Delta; false otherwise. + internal static bool TryGetInnerTypeForDelta(ref Type type) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Delta<>)) + { + type = type.GetGenericArguments()[0]; + return true; } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(DeltaSet<>)) + { + type = type.GetGenericArguments()[0]; + return true; + } + + return false; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/EdmTypeReferenceEqualityComparer.cs b/src/Microsoft.AspNetCore.OData/Formatter/EdmTypeReferenceEqualityComparer.cs index d4c018352..7ab5b3de9 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/EdmTypeReferenceEqualityComparer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/EdmTypeReferenceEqualityComparer.cs @@ -10,34 +10,33 @@ using System.Diagnostics.Contracts; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// An equality comparer for . +/// +internal class EdmTypeReferenceEqualityComparer : IEqualityComparer { - /// - /// An equality comparer for . - /// - internal class EdmTypeReferenceEqualityComparer : IEqualityComparer + public bool Equals(IEdmTypeReference x, IEdmTypeReference y) + { + Contract.Assert(x != null); + return x.IsEquivalentTo(y); + } + + public int GetHashCode(IEdmTypeReference obj) { - public bool Equals(IEdmTypeReference x, IEdmTypeReference y) + Contract.Assert(obj != null); + + string fullName = obj.FullName(); + if (fullName == null) { - Contract.Assert(x != null); - return x.IsEquivalentTo(y); + // EdmTypeReferences without an IEdmSchemaElement Definition will all be hashed to 0 + // This is mostly so unit tests don't cause this method to null-ref + return 0; } - - public int GetHashCode(IEdmTypeReference obj) + else { - Contract.Assert(obj != null); - - string fullName = obj.FullName(); - if (fullName == null) - { - // EdmTypeReferences without an IEdmSchemaElement Definition will all be hashed to 0 - // This is mostly so unit tests don't cause this method to null-ref - return 0; - } - else - { - return fullName.GetHashCode(StringComparison.Ordinal); - } + return fullName.GetHashCode(StringComparison.Ordinal); } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/FromODataBodyAttribute.cs b/src/Microsoft.AspNetCore.OData/Formatter/FromODataBodyAttribute.cs index 8de6f0186..a67237637 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/FromODataBodyAttribute.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/FromODataBodyAttribute.cs @@ -8,24 +8,23 @@ using System; using Microsoft.AspNetCore.Mvc; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// An implementation of that can bind URI parameters using OData conventions. +/// +/// +/// I'd like to use this attribute for the action parameters whose value are from request body. +/// It's not finished yet. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)] +public sealed class FromODataBodyAttribute : ModelBinderAttribute { /// - /// An implementation of that can bind URI parameters using OData conventions. + /// Instantiates a new instance of the class. /// - /// - /// I'd like to use this attribute for the action parameters whose value are from request body. - /// It's not finished yet. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)] - public sealed class FromODataBodyAttribute : ModelBinderAttribute + public FromODataBodyAttribute() + : base(typeof(ODataBodyModelBinder)) { - /// - /// Instantiates a new instance of the class. - /// - public FromODataBodyAttribute() - : base(typeof(ODataBodyModelBinder)) - { - } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/FromODataUriAttribute.cs b/src/Microsoft.AspNetCore.OData/Formatter/FromODataUriAttribute.cs index 917b418f6..4333b7e2c 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/FromODataUriAttribute.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/FromODataUriAttribute.cs @@ -8,20 +8,19 @@ using System; using Microsoft.AspNetCore.Mvc; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// An implementation of that can bind URI parameters using OData conventions. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)] +public sealed class FromODataUriAttribute : ModelBinderAttribute { /// - /// An implementation of that can bind URI parameters using OData conventions. + /// Instantiates a new instance of the class. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)] - public sealed class FromODataUriAttribute : ModelBinderAttribute + public FromODataUriAttribute() + : base(typeof(ODataModelBinder)) { - /// - /// Instantiates a new instance of the class. - /// - public FromODataUriAttribute() - : base(typeof(ODataModelBinder)) - { - } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs b/src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs index 751fac674..79f2e5271 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/LinkGenerationHelpers.cs @@ -16,565 +16,564 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// Contains helper methods for generating OData links that follow OData URL conventions. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class LinkGenerationHelpers { /// - /// Contains helper methods for generating OData links that follow OData URL conventions. + /// Generates a self link following the OData URL conventions for the entity represented by . /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static class LinkGenerationHelpers + /// The representing the entity for which the self link needs to be generated. + /// Represents whether the generated link should have a cast segment representing a type cast. + /// The self link following the OData URL conventions. + public static Uri GenerateSelfLink(this ResourceContext resourceContext, bool includeCast) { - /// - /// Generates a self link following the OData URL conventions for the entity represented by . - /// - /// The representing the entity for which the self link needs to be generated. - /// Represents whether the generated link should have a cast segment representing a type cast. - /// The self link following the OData URL conventions. - public static Uri GenerateSelfLink(this ResourceContext resourceContext, bool includeCast) - { - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } - IList idLinkPathSegments = resourceContext.GenerateBaseODataPathSegments(); + IList idLinkPathSegments = resourceContext.GenerateBaseODataPathSegments(); - bool isSameType = resourceContext.StructuredType == resourceContext.NavigationSource.EntityType; - if (includeCast && !isSameType) - { - idLinkPathSegments.Add(new TypeSegment(resourceContext.StructuredType, navigationSource: null)); - } + bool isSameType = resourceContext.StructuredType == resourceContext.NavigationSource.EntityType; + if (includeCast && !isSameType) + { + idLinkPathSegments.Add(new TypeSegment(resourceContext.StructuredType, navigationSource: null)); + } - string idLink = resourceContext.Request.CreateODataLink(idLinkPathSegments); - if (idLink == null) - { - return null; - } + string idLink = resourceContext.Request.CreateODataLink(idLinkPathSegments); + if (idLink == null) + { + return null; + } + + return new Uri(idLink); + } - return new Uri(idLink); + /// + /// Generates a navigation link following the OData URL conventions for the entity represented by and the given + /// navigation property. + /// + /// The representing the entity for which the navigation link needs to be generated. + /// The EDM navigation property. + /// Represents whether the generated link should have a cast segment representing a type cast. + /// The navigation link following the OData URL conventions. + public static Uri GenerateNavigationPropertyLink(this ResourceContext resourceContext, + IEdmNavigationProperty navigationProperty, bool includeCast) + { + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); } - /// - /// Generates a navigation link following the OData URL conventions for the entity represented by and the given - /// navigation property. - /// - /// The representing the entity for which the navigation link needs to be generated. - /// The EDM navigation property. - /// Represents whether the generated link should have a cast segment representing a type cast. - /// The navigation link following the OData URL conventions. - public static Uri GenerateNavigationPropertyLink(this ResourceContext resourceContext, - IEdmNavigationProperty navigationProperty, bool includeCast) + IList navigationPathSegments; + if (resourceContext.NavigationSource is IEdmContainedEntitySet && + resourceContext.NavigationSource != resourceContext.SerializerContext.Path.NavigationSource()) { - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } + navigationPathSegments = resourceContext.GenerateContainmentODataPathSegments(); + } + else + { + navigationPathSegments = resourceContext.GenerateBaseODataPathSegments(); + } - IList navigationPathSegments; - if (resourceContext.NavigationSource is IEdmContainedEntitySet && - resourceContext.NavigationSource != resourceContext.SerializerContext.Path.NavigationSource()) - { - navigationPathSegments = resourceContext.GenerateContainmentODataPathSegments(); - } - else - { - navigationPathSegments = resourceContext.GenerateBaseODataPathSegments(); - } + if (includeCast) + { + navigationPathSegments.Add(new TypeSegment(resourceContext.StructuredType, navigationSource: null)); + } - if (includeCast) - { - navigationPathSegments.Add(new TypeSegment(resourceContext.StructuredType, navigationSource: null)); - } + navigationPathSegments.Add(new NavigationPropertySegment(navigationProperty, navigationSource: null)); - navigationPathSegments.Add(new NavigationPropertySegment(navigationProperty, navigationSource: null)); + string link = resourceContext.Request.CreateODataLink(navigationPathSegments); + if (link == null) + { + return null; + } - string link = resourceContext.Request.CreateODataLink(navigationPathSegments); - if (link == null) - { - return null; - } + return new Uri(link); + } - return new Uri(link); + /// + /// Generates an action link following the OData URL conventions for the action and bound to the + /// collection of entity represented by . + /// + /// The representing the feed for which the action link needs to be generated. + /// The action for which the action link needs to be generated. + /// The generated action link following OData URL conventions. + public static Uri GenerateActionLink(this ResourceSetContext resourceSetContext, IEdmOperation action) + { + if (resourceSetContext == null) + { + throw Error.ArgumentNull(nameof(resourceSetContext)); } - /// - /// Generates an action link following the OData URL conventions for the action and bound to the - /// collection of entity represented by . - /// - /// The representing the feed for which the action link needs to be generated. - /// The action for which the action link needs to be generated. - /// The generated action link following OData URL conventions. - public static Uri GenerateActionLink(this ResourceSetContext resourceSetContext, IEdmOperation action) + if (action == null) { - if (resourceSetContext == null) - { - throw Error.ArgumentNull(nameof(resourceSetContext)); - } + throw Error.ArgumentNull(nameof(action)); + } - if (action == null) - { - throw Error.ArgumentNull(nameof(action)); - } + IEdmOperationParameter bindingParameter = action.Parameters.FirstOrDefault(); + if (bindingParameter == null || + !bindingParameter.Type.IsCollection() || + !((IEdmCollectionType)bindingParameter.Type.Definition).ElementType.IsEntity()) + { + throw Error.Argument("action", SRResources.ActionNotBoundToCollectionOfEntity, action.Name); + } - IEdmOperationParameter bindingParameter = action.Parameters.FirstOrDefault(); - if (bindingParameter == null || - !bindingParameter.Type.IsCollection() || - !((IEdmCollectionType)bindingParameter.Type.Definition).ElementType.IsEntity()) - { - throw Error.Argument("action", SRResources.ActionNotBoundToCollectionOfEntity, action.Name); - } + return GenerateActionLink(resourceSetContext, bindingParameter.Type, action); + } - return GenerateActionLink(resourceSetContext, bindingParameter.Type, action); - } + internal static Uri GenerateActionLink(this ResourceSetContext resourceSetContext, IEdmTypeReference bindingParameterType, + IEdmOperation action) + { + Contract.Assert(resourceSetContext != null); - internal static Uri GenerateActionLink(this ResourceSetContext resourceSetContext, IEdmTypeReference bindingParameterType, - IEdmOperation action) + if (resourceSetContext.EntitySetBase is IEdmContainedEntitySet) { - Contract.Assert(resourceSetContext != null); + return null; + } - if (resourceSetContext.EntitySetBase is IEdmContainedEntitySet) - { - return null; - } + IList actionPathSegments = new List(); + resourceSetContext.GenerateBaseODataPathSegmentsForFeed(actionPathSegments); - IList actionPathSegments = new List(); - resourceSetContext.GenerateBaseODataPathSegmentsForFeed(actionPathSegments); + // generate link with cast if the navigation source doesn't match the type the action is bound to. + if (resourceSetContext.EntitySetBase.Type.FullTypeName() != bindingParameterType.FullName()) + { + actionPathSegments.Add(new TypeSegment(bindingParameterType.Definition, resourceSetContext.EntitySetBase)); + } - // generate link with cast if the navigation source doesn't match the type the action is bound to. - if (resourceSetContext.EntitySetBase.Type.FullTypeName() != bindingParameterType.FullName()) - { - actionPathSegments.Add(new TypeSegment(bindingParameterType.Definition, resourceSetContext.EntitySetBase)); - } + OperationSegment operationSegment = new OperationSegment(action, entitySet: null); + actionPathSegments.Add(operationSegment); - OperationSegment operationSegment = new OperationSegment(action, entitySet: null); - actionPathSegments.Add(operationSegment); + string actionLink = resourceSetContext.Request.CreateODataLink(actionPathSegments); + return actionLink == null ? null : new Uri(actionLink); + } - string actionLink = resourceSetContext.Request.CreateODataLink(actionPathSegments); - return actionLink == null ? null : new Uri(actionLink); + /// + /// Generates a function link following the OData URL conventions for the function and bound to the + /// collection of entity represented by . + /// + /// The representing the feed for which the function link needs to be generated. + /// The function for which the function link needs to be generated. + /// The generated function link following OData URL conventions. + public static Uri GenerateFunctionLink(this ResourceSetContext resourceSetContext, IEdmOperation function) + { + if (resourceSetContext == null) + { + throw Error.ArgumentNull(nameof(resourceSetContext)); } - /// - /// Generates a function link following the OData URL conventions for the function and bound to the - /// collection of entity represented by . - /// - /// The representing the feed for which the function link needs to be generated. - /// The function for which the function link needs to be generated. - /// The generated function link following OData URL conventions. - public static Uri GenerateFunctionLink(this ResourceSetContext resourceSetContext, IEdmOperation function) + if (function == null) { - if (resourceSetContext == null) - { - throw Error.ArgumentNull(nameof(resourceSetContext)); - } + throw Error.ArgumentNull(nameof(function)); + } - if (function == null) - { - throw Error.ArgumentNull(nameof(function)); - } + IEdmOperationParameter bindingParameter = function.Parameters.FirstOrDefault(); + if (bindingParameter == null || + !bindingParameter.Type.IsCollection() || + !((IEdmCollectionType)bindingParameter.Type.Definition).ElementType.IsEntity()) + { + throw Error.Argument("function", SRResources.FunctionNotBoundToCollectionOfEntity, function.Name); + } - IEdmOperationParameter bindingParameter = function.Parameters.FirstOrDefault(); - if (bindingParameter == null || - !bindingParameter.Type.IsCollection() || - !((IEdmCollectionType)bindingParameter.Type.Definition).ElementType.IsEntity()) - { - throw Error.Argument("function", SRResources.FunctionNotBoundToCollectionOfEntity, function.Name); - } + return GenerateFunctionLink(resourceSetContext, bindingParameter.Type, function, + function.Parameters.Select(p => p.Name)); + } - return GenerateFunctionLink(resourceSetContext, bindingParameter.Type, function, - function.Parameters.Select(p => p.Name)); - } + internal static Uri GenerateFunctionLink(this ResourceSetContext resourceSetContext, IEdmTypeReference bindingParameterType, + IEdmOperation functionImport, IEnumerable parameterNames) + { + Contract.Assert(resourceSetContext != null); - internal static Uri GenerateFunctionLink(this ResourceSetContext resourceSetContext, IEdmTypeReference bindingParameterType, - IEdmOperation functionImport, IEnumerable parameterNames) + if (resourceSetContext.EntitySetBase is IEdmContainedEntitySet) { - Contract.Assert(resourceSetContext != null); + return null; + } - if (resourceSetContext.EntitySetBase is IEdmContainedEntitySet) - { - return null; - } + IList functionPathSegments = new List(); + resourceSetContext.GenerateBaseODataPathSegmentsForFeed(functionPathSegments); - IList functionPathSegments = new List(); - resourceSetContext.GenerateBaseODataPathSegmentsForFeed(functionPathSegments); + // generate link with cast if the navigation source type doesn't match the entity type the function is bound to. + if (resourceSetContext.EntitySetBase.Type.FullTypeName() != bindingParameterType.Definition.FullTypeName()) + { + functionPathSegments.Add(new TypeSegment(bindingParameterType.Definition, null)); + } - // generate link with cast if the navigation source type doesn't match the entity type the function is bound to. - if (resourceSetContext.EntitySetBase.Type.FullTypeName() != bindingParameterType.Definition.FullTypeName()) - { - functionPathSegments.Add(new TypeSegment(bindingParameterType.Definition, null)); - } + IList parameters = new List(); + // skip the binding parameter + foreach (string param in parameterNames.Skip(1)) + { + string value = "@" + param; + parameters.Add(new OperationSegmentParameter(param, new ConstantNode(value, value))); + } - IList parameters = new List(); - // skip the binding parameter - foreach (string param in parameterNames.Skip(1)) - { - string value = "@" + param; - parameters.Add(new OperationSegmentParameter(param, new ConstantNode(value, value))); - } + OperationSegment segment = new OperationSegment(new[] { functionImport }, parameters, null); + functionPathSegments.Add(segment); - OperationSegment segment = new OperationSegment(new[] { functionImport }, parameters, null); - functionPathSegments.Add(segment); + string functionLink = resourceSetContext.Request.CreateODataLink(functionPathSegments); + return functionLink == null ? null : new Uri(functionLink); + } - string functionLink = resourceSetContext.Request.CreateODataLink(functionPathSegments); - return functionLink == null ? null : new Uri(functionLink); + /// + /// Generates an action link following the OData URL conventions for the action and bound to the entity + /// represented by . + /// + /// The representing the entity for which the action link needs to be generated. + /// The action for which the action link needs to be generated. + /// The generated action link following OData URL conventions. + public static Uri GenerateActionLink(this ResourceContext resourceContext, IEdmOperation action) + { + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + if (action == null) + { + throw Error.ArgumentNull(nameof(action)); } - /// - /// Generates an action link following the OData URL conventions for the action and bound to the entity - /// represented by . - /// - /// The representing the entity for which the action link needs to be generated. - /// The action for which the action link needs to be generated. - /// The generated action link following OData URL conventions. - public static Uri GenerateActionLink(this ResourceContext resourceContext, IEdmOperation action) + IEdmOperationParameter bindingParameter = action.Parameters.FirstOrDefault(); + if (bindingParameter == null || !bindingParameter.Type.IsEntity()) { - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } - if (action == null) - { - throw Error.ArgumentNull(nameof(action)); - } + throw Error.Argument("action", SRResources.ActionNotBoundToEntity, action.Name); + } - IEdmOperationParameter bindingParameter = action.Parameters.FirstOrDefault(); - if (bindingParameter == null || !bindingParameter.Type.IsEntity()) - { - throw Error.Argument("action", SRResources.ActionNotBoundToEntity, action.Name); - } + return GenerateActionLink(resourceContext, bindingParameter.Type, action); + } - return GenerateActionLink(resourceContext, bindingParameter.Type, action); + internal static Uri GenerateActionLink(this ResourceContext resourceContext, + IEdmTypeReference bindingParameterType, IEdmOperation action) + { + Contract.Assert(resourceContext != null); + if (resourceContext.NavigationSource is IEdmContainedEntitySet) + { + return null; } - internal static Uri GenerateActionLink(this ResourceContext resourceContext, - IEdmTypeReference bindingParameterType, IEdmOperation action) - { - Contract.Assert(resourceContext != null); - if (resourceContext.NavigationSource is IEdmContainedEntitySet) - { - return null; - } + IList actionPathSegments = resourceContext.GenerateBaseODataPathSegments(); - IList actionPathSegments = resourceContext.GenerateBaseODataPathSegments(); + // generate link with cast if the navigation source doesn't match the entity type the action is bound to. + if (resourceContext.NavigationSource.EntityType != bindingParameterType.Definition) + { + actionPathSegments.Add(new TypeSegment((IEdmEntityType)bindingParameterType.Definition, null)); + // entity set can be null + } - // generate link with cast if the navigation source doesn't match the entity type the action is bound to. - if (resourceContext.NavigationSource.EntityType != bindingParameterType.Definition) - { - actionPathSegments.Add(new TypeSegment((IEdmEntityType)bindingParameterType.Definition, null)); - // entity set can be null - } + OperationSegment operationSegment = new OperationSegment(new[] { action }, null); + actionPathSegments.Add(operationSegment); - OperationSegment operationSegment = new OperationSegment(new[] { action }, null); - actionPathSegments.Add(operationSegment); + string actionLink = resourceContext.Request.CreateODataLink(actionPathSegments); + return actionLink == null ? null : new Uri(actionLink); + } - string actionLink = resourceContext.Request.CreateODataLink(actionPathSegments); - return actionLink == null ? null : new Uri(actionLink); + /// + /// Generates an function link following the OData URL conventions for the function and bound to the entity + /// represented by . + /// + /// The representing the entity for which the function link needs to be generated. + /// The function for which the function link needs to be generated. + /// The generated function link following OData URL conventions. + public static Uri GenerateFunctionLink(this ResourceContext resourceContext, IEdmOperation function) + { + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + if (function == null) + { + throw Error.ArgumentNull(nameof(function)); } - /// - /// Generates an function link following the OData URL conventions for the function and bound to the entity - /// represented by . - /// - /// The representing the entity for which the function link needs to be generated. - /// The function for which the function link needs to be generated. - /// The generated function link following OData URL conventions. - public static Uri GenerateFunctionLink(this ResourceContext resourceContext, IEdmOperation function) + IEdmOperationParameter bindingParameter = function.Parameters.FirstOrDefault(); + if (bindingParameter == null || !bindingParameter.Type.IsEntity()) { - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } - if (function == null) - { - throw Error.ArgumentNull(nameof(function)); - } + throw Error.Argument("function", SRResources.FunctionNotBoundToEntity, function.Name); + } - IEdmOperationParameter bindingParameter = function.Parameters.FirstOrDefault(); - if (bindingParameter == null || !bindingParameter.Type.IsEntity()) - { - throw Error.Argument("function", SRResources.FunctionNotBoundToEntity, function.Name); - } + return GenerateFunctionLink(resourceContext, bindingParameter.Type.FullName(), function.FullName(), + function.Parameters.Select(p => p.Name)); + } + + internal static Uri GenerateFunctionLink(this ResourceContext resourceContext, + IEdmTypeReference bindingParameterType, IEdmOperation function, + IEnumerable parameterNames) + { + IList functionPathSegments = resourceContext.GenerateBaseODataPathSegments(); - return GenerateFunctionLink(resourceContext, bindingParameter.Type.FullName(), function.FullName(), - function.Parameters.Select(p => p.Name)); + // generate link with cast if the navigation source type doesn't match the entity type the function is bound to. + if (resourceContext.NavigationSource.EntityType != bindingParameterType.Definition) + { + functionPathSegments.Add(new TypeSegment(bindingParameterType.Definition, null)); } - internal static Uri GenerateFunctionLink(this ResourceContext resourceContext, - IEdmTypeReference bindingParameterType, IEdmOperation function, - IEnumerable parameterNames) + IList parameters = new List(); + // skip the binding parameter + foreach (string param in parameterNames.Skip(1)) { - IList functionPathSegments = resourceContext.GenerateBaseODataPathSegments(); + string value = "@" + param; + parameters.Add(new OperationSegmentParameter(param, new ConstantNode(value, value))); + } - // generate link with cast if the navigation source type doesn't match the entity type the function is bound to. - if (resourceContext.NavigationSource.EntityType != bindingParameterType.Definition) - { - functionPathSegments.Add(new TypeSegment(bindingParameterType.Definition, null)); - } + OperationSegment segment = new OperationSegment(new[] { function }, parameters, null); + functionPathSegments.Add(segment); - IList parameters = new List(); - // skip the binding parameter - foreach (string param in parameterNames.Skip(1)) - { - string value = "@" + param; - parameters.Add(new OperationSegmentParameter(param, new ConstantNode(value, value))); - } + string functionLink = resourceContext.Request.CreateODataLink(functionPathSegments); + return functionLink == null ? null : new Uri(functionLink); + } - OperationSegment segment = new OperationSegment(new[] { function }, parameters, null); - functionPathSegments.Add(segment); + internal static Uri GenerateFunctionLink(this ResourceContext resourceContext, string bindingParameterType, + string functionName, IEnumerable parameterNames) + { + Contract.Assert(resourceContext.EdmModel != null); - string functionLink = resourceContext.Request.CreateODataLink(functionPathSegments); - return functionLink == null ? null : new Uri(functionLink); + if (resourceContext.EdmModel == null) + { + return null; } - internal static Uri GenerateFunctionLink(this ResourceContext resourceContext, string bindingParameterType, - string functionName, IEnumerable parameterNames) - { - Contract.Assert(resourceContext.EdmModel != null); + IEdmModel model = resourceContext.EdmModel; + IEdmTypeReference typeReference = model.FindDeclaredType(bindingParameterType).ToEdmTypeReference(true); + IEdmOperation operation = model.FindDeclaredOperations(functionName).First(); + return resourceContext.GenerateFunctionLink(typeReference, operation, parameterNames); + } - if (resourceContext.EdmModel == null) - { - return null; - } + internal static IList GenerateBaseODataPathSegments(this ResourceContext resourceContext) + { + IList odataPath = new List(); - IEdmModel model = resourceContext.EdmModel; - IEdmTypeReference typeReference = model.FindDeclaredType(bindingParameterType).ToEdmTypeReference(true); - IEdmOperation operation = model.FindDeclaredOperations(functionName).First(); - return resourceContext.GenerateFunctionLink(typeReference, operation, parameterNames); + if (resourceContext.NavigationSource.NavigationSourceKind() == EdmNavigationSourceKind.Singleton) + { + // Per the OData V4 specification, a singleton is expected to be a child of the entity container, and + // as a result we can make the assumption that it is the only segment in the generated path. + odataPath.Add(new SingletonSegment((IEdmSingleton)resourceContext.NavigationSource)); } - - internal static IList GenerateBaseODataPathSegments(this ResourceContext resourceContext) + else { - IList odataPath = new List(); - - if (resourceContext.NavigationSource.NavigationSourceKind() == EdmNavigationSourceKind.Singleton) - { - // Per the OData V4 specification, a singleton is expected to be a child of the entity container, and - // as a result we can make the assumption that it is the only segment in the generated path. - odataPath.Add(new SingletonSegment((IEdmSingleton)resourceContext.NavigationSource)); - } - else - { - resourceContext.GenerateBaseODataPathSegmentsForEntity(odataPath); - } - - return odataPath; + resourceContext.GenerateBaseODataPathSegmentsForEntity(odataPath); } - private static void GenerateBaseODataPathSegments( - ODataPath path, - IEdmNavigationSource navigationSource, - IList odataPath) + return odataPath; + } + + private static void GenerateBaseODataPathSegments( + ODataPath path, + IEdmNavigationSource navigationSource, + IList odataPath) + { + // If the navigation is a contained property, we need to walk all of the path segments + // to generate a contextually accurate URI. + bool segmentFound = false; + bool containedFound = false; + if (path != null) { - // If the navigation is a contained property, we need to walk all of the path segments - // to generate a contextually accurate URI. - bool segmentFound = false; - bool containedFound = false; - if (path != null) + var segments = path.ToArray(); + int length = segments.Length; + int previousNavigationPathIndex = -1; + for (int i = 0; i < length; i++) { - var segments = path.ToArray(); - int length = segments.Length; - int previousNavigationPathIndex = -1; - for (int i = 0; i < length; i++) - { - ODataPathSegment pathSegment = segments[i]; - IEdmNavigationSource currentNavigationSource = null; + ODataPathSegment pathSegment = segments[i]; + IEdmNavigationSource currentNavigationSource = null; - var entitySetPathSegment = pathSegment as EntitySetSegment; - if (entitySetPathSegment != null) - { - currentNavigationSource = entitySetPathSegment.EntitySet; - } + var entitySetPathSegment = pathSegment as EntitySetSegment; + if (entitySetPathSegment != null) + { + currentNavigationSource = entitySetPathSegment.EntitySet; + } - var navigationPathSegment = pathSegment as NavigationPropertySegment; - if (navigationPathSegment != null) - { - currentNavigationSource = navigationPathSegment.NavigationSource; - } + var navigationPathSegment = pathSegment as NavigationPropertySegment; + if (navigationPathSegment != null) + { + currentNavigationSource = navigationPathSegment.NavigationSource; + } - var singletonPathSegment = pathSegment as SingletonSegment; - if (singletonPathSegment != null) - { - currentNavigationSource = singletonPathSegment.Singleton; - } + var singletonPathSegment = pathSegment as SingletonSegment; + if (singletonPathSegment != null) + { + currentNavigationSource = singletonPathSegment.Singleton; + } - if (containedFound) - { - odataPath.Add(pathSegment); - } - else + if (containedFound) + { + odataPath.Add(pathSegment); + } + else + { + if (navigationPathSegment != null && + navigationPathSegment.NavigationProperty.ContainsTarget) { - if (navigationPathSegment != null && - navigationPathSegment.NavigationProperty.ContainsTarget) + containedFound = true; + //The path should have the last non-contained navigation property + if (previousNavigationPathIndex != -1) { - containedFound = true; - //The path should have the last non-contained navigation property - if (previousNavigationPathIndex != -1) + for (int j = previousNavigationPathIndex; j <= i; j++) { - for (int j = previousNavigationPathIndex; j <= i; j++) - { - odataPath.Add(segments[j]); - } + odataPath.Add(segments[j]); } } } + } - // If we've found our target navigation in the path that means we've correctly populated the - // segments up to the navigation and we can ignore the remaining segments. - if (currentNavigationSource != null) + // If we've found our target navigation in the path that means we've correctly populated the + // segments up to the navigation and we can ignore the remaining segments. + if (currentNavigationSource != null) + { + previousNavigationPathIndex = i; + if (currentNavigationSource == navigationSource) { - previousNavigationPathIndex = i; - if (currentNavigationSource == navigationSource) - { - segmentFound = true; - break; - } + segmentFound = true; + break; } } } + } - if (!segmentFound || !containedFound) - { - // If the target navigation was not found in the current path that means we lack any context that - // would suggest a scenario other than directly accessing an entity set, so we must assume that's - // the case. - odataPath.Clear(); + if (!segmentFound || !containedFound) + { + // If the target navigation was not found in the current path that means we lack any context that + // would suggest a scenario other than directly accessing an entity set, so we must assume that's + // the case. + odataPath.Clear(); - if (navigationSource is IEdmContainedEntitySet) - { - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - IEdmEntitySet entitySet = new EdmEntitySet(container, navigationSource.Name, - navigationSource.EntityType); - odataPath.Add(new EntitySetSegment(entitySet)); - } - else - { - odataPath.Add(new EntitySetSegment((IEdmEntitySet)navigationSource)); - } + if (navigationSource is IEdmContainedEntitySet) + { + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + IEdmEntitySet entitySet = new EdmEntitySet(container, navigationSource.Name, + navigationSource.EntityType); + odataPath.Add(new EntitySetSegment(entitySet)); + } + else + { + odataPath.Add(new EntitySetSegment((IEdmEntitySet)navigationSource)); } } + } - private static void GenerateBaseODataPathSegmentsForEntity( - this ResourceContext resourceContext, - IList odataPath) - { - // If the navigation is a contained property, we need to walk all of the path segments - // to generate a contextually accurate URI. - GenerateBaseODataPathSegments( - resourceContext.SerializerContext.Path, resourceContext.NavigationSource, odataPath); + private static void GenerateBaseODataPathSegmentsForEntity( + this ResourceContext resourceContext, + IList odataPath) + { + // If the navigation is a contained property, we need to walk all of the path segments + // to generate a contextually accurate URI. + GenerateBaseODataPathSegments( + resourceContext.SerializerContext.Path, resourceContext.NavigationSource, odataPath); - odataPath.Add(new KeySegment(ConventionsHelpers.GetEntityKey(resourceContext), resourceContext.StructuredType as IEdmEntityType, - null)); - } + odataPath.Add(new KeySegment(ConventionsHelpers.GetEntityKey(resourceContext), resourceContext.StructuredType as IEdmEntityType, + null)); + } - private static void GenerateBaseODataPathSegmentsForFeed( - this ResourceSetContext feedContext, - IList odataPath) - { - GenerateBaseODataPathSegments(feedContext.Request.ODataFeature().Path, - feedContext.EntitySetBase, - odataPath); - } + private static void GenerateBaseODataPathSegmentsForFeed( + this ResourceSetContext feedContext, + IList odataPath) + { + GenerateBaseODataPathSegments(feedContext.Request.ODataFeature().Path, + feedContext.EntitySetBase, + odataPath); + } - private static IList GenerateContainmentODataPathSegments(this ResourceContext resourceContext) - { - List navigationPathSegments = new List(); - ResourceContext currentResourceContext = resourceContext; + private static IList GenerateContainmentODataPathSegments(this ResourceContext resourceContext) + { + List navigationPathSegments = new List(); + ResourceContext currentResourceContext = resourceContext; - // We loop till the base of the $expand expression then use GenerateBaseODataPathSegments to generate the base path segments - // For instance, given $expand=Tabs($expand=Items($expand=Notes($expand=Tips))), we loop until we get to Tabs at the base - while (currentResourceContext != null && currentResourceContext.NavigationSource != resourceContext.SerializerContext.Path.NavigationSource()) + // We loop till the base of the $expand expression then use GenerateBaseODataPathSegments to generate the base path segments + // For instance, given $expand=Tabs($expand=Items($expand=Notes($expand=Tips))), we loop until we get to Tabs at the base + while (currentResourceContext != null && currentResourceContext.NavigationSource != resourceContext.SerializerContext.Path.NavigationSource()) + { + if (currentResourceContext.NavigationSource is IEdmContainedEntitySet containedEntitySet) { - if (currentResourceContext.NavigationSource is IEdmContainedEntitySet containedEntitySet) + // Type-cast segment for the expanded resource that is passed into the method is added by the caller + if (currentResourceContext != resourceContext && currentResourceContext.StructuredType != containedEntitySet.EntityType) { - // Type-cast segment for the expanded resource that is passed into the method is added by the caller - if (currentResourceContext != resourceContext && currentResourceContext.StructuredType != containedEntitySet.EntityType) - { - navigationPathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource)); - } + navigationPathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource)); + } - KeySegment keySegment = new KeySegment( - ConventionsHelpers.GetEntityKey(currentResourceContext), - currentResourceContext.StructuredType as IEdmEntityType, - navigationSource: currentResourceContext.NavigationSource); - navigationPathSegments.Add(keySegment); + KeySegment keySegment = new KeySegment( + ConventionsHelpers.GetEntityKey(currentResourceContext), + currentResourceContext.StructuredType as IEdmEntityType, + navigationSource: currentResourceContext.NavigationSource); + navigationPathSegments.Add(keySegment); - NavigationPropertySegment navPropertySegment = new NavigationPropertySegment( - containedEntitySet.NavigationProperty, - containedEntitySet.ParentNavigationSource); - navigationPathSegments.Add(navPropertySegment); - } - else if (currentResourceContext.NavigationSource is IEdmEntitySet entitySet) + NavigationPropertySegment navPropertySegment = new NavigationPropertySegment( + containedEntitySet.NavigationProperty, + containedEntitySet.ParentNavigationSource); + navigationPathSegments.Add(navPropertySegment); + } + else if (currentResourceContext.NavigationSource is IEdmEntitySet entitySet) + { + // We will get here if there's a non-contained entity set on the $expand expression + if (currentResourceContext.StructuredType != entitySet.EntityType) { - // We will get here if there's a non-contained entity set on the $expand expression - if (currentResourceContext.StructuredType != entitySet.EntityType) - { - navigationPathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource)); - } + navigationPathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource)); + } - KeySegment keySegment = new KeySegment( - ConventionsHelpers.GetEntityKey(currentResourceContext), - currentResourceContext.StructuredType as IEdmEntityType, - currentResourceContext.NavigationSource); - navigationPathSegments.Add(keySegment); + KeySegment keySegment = new KeySegment( + ConventionsHelpers.GetEntityKey(currentResourceContext), + currentResourceContext.StructuredType as IEdmEntityType, + currentResourceContext.NavigationSource); + navigationPathSegments.Add(keySegment); - EntitySetSegment entitySetSegment = new EntitySetSegment(entitySet); - navigationPathSegments.Add(entitySetSegment); + EntitySetSegment entitySetSegment = new EntitySetSegment(entitySet); + navigationPathSegments.Add(entitySetSegment); - // Reverse the list such that the segments are in the right order - navigationPathSegments.Reverse(); - return navigationPathSegments; - } - else if (currentResourceContext.NavigationSource is IEdmSingleton singleton) + // Reverse the list such that the segments are in the right order + navigationPathSegments.Reverse(); + return navigationPathSegments; + } + else if (currentResourceContext.NavigationSource is IEdmSingleton singleton) + { + // We will get here if there's a singleton on the $expand expression + if (currentResourceContext.StructuredType != singleton.EntityType) { - // We will get here if there's a singleton on the $expand expression - if (currentResourceContext.StructuredType != singleton.EntityType) - { - navigationPathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource)); - } - - SingletonSegment singletonSegment = new SingletonSegment(singleton); - navigationPathSegments.Add(singletonSegment); - - // Reverse the list such that the segments are in the right order - navigationPathSegments.Reverse(); - return navigationPathSegments; + navigationPathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource)); } - currentResourceContext = currentResourceContext.SerializerContext.ExpandedResource; + SingletonSegment singletonSegment = new SingletonSegment(singleton); + navigationPathSegments.Add(singletonSegment); + + // Reverse the list such that the segments are in the right order + navigationPathSegments.Reverse(); + return navigationPathSegments; } - Debug.Assert(currentResourceContext != null, "currentResourceContext != null"); - // Once we are at the base of the $expand expression, we call GenerateBaseODataPathSegments to generate the base path segments - IList pathSegments = currentResourceContext.GenerateBaseODataPathSegments(); + currentResourceContext = currentResourceContext.SerializerContext.ExpandedResource; + } - Debug.Assert(pathSegments.Count > 0, "pathSegments.Count > 0"); + Debug.Assert(currentResourceContext != null, "currentResourceContext != null"); + // Once we are at the base of the $expand expression, we call GenerateBaseODataPathSegments to generate the base path segments + IList pathSegments = currentResourceContext.GenerateBaseODataPathSegments(); - ODataPathSegment lastNonKeySegment; + Debug.Assert(pathSegments.Count > 0, "pathSegments.Count > 0"); - if (pathSegments.Count == 1) - { - lastNonKeySegment = pathSegments[0]; - Debug.Assert(lastNonKeySegment is SingletonSegment, "lastNonKeySegment is SingletonSegment"); - } - else - { - Debug.Assert(pathSegments[pathSegments.Count - 1] is KeySegment, "pathSegments[pathSegments.Count - 1] is KeySegment"); - // 2nd last segment would be NavigationPathSegment or EntitySetSegment - lastNonKeySegment = pathSegments[pathSegments.Count - 2]; - } + ODataPathSegment lastNonKeySegment; - if (currentResourceContext.StructuredType != lastNonKeySegment.EdmType.AsElementType()) - { - pathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource)); - } + if (pathSegments.Count == 1) + { + lastNonKeySegment = pathSegments[0]; + Debug.Assert(lastNonKeySegment is SingletonSegment, "lastNonKeySegment is SingletonSegment"); + } + else + { + Debug.Assert(pathSegments[pathSegments.Count - 1] is KeySegment, "pathSegments[pathSegments.Count - 1] is KeySegment"); + // 2nd last segment would be NavigationPathSegment or EntitySetSegment + lastNonKeySegment = pathSegments[pathSegments.Count - 2]; + } - // Add the segments from the $expand expression in reverse order - for (int i = navigationPathSegments.Count - 1; i >= 0; i--) - { - pathSegments.Add(navigationPathSegments[i]); - } + if (currentResourceContext.StructuredType != lastNonKeySegment.EdmType.AsElementType()) + { + pathSegments.Add(new TypeSegment(currentResourceContext.StructuredType, currentResourceContext.NavigationSource)); + } - return pathSegments; + // Add the segments from the $expand expression in reverse order + for (int i = navigationPathSegments.Count - 1; i >= 0; i--) + { + pathSegments.Add(navigationPathSegments[i]); } + + return pathSegments; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/IMediaTypeMappingCollection.cs b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/IMediaTypeMappingCollection.cs index f78ccce16..2cbcc18bd 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/IMediaTypeMappingCollection.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/IMediaTypeMappingCollection.cs @@ -7,16 +7,15 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Formatter.MediaType; + +/// +/// An interface that defines a property to access a collection of objects. +/// +interface IMediaTypeMappingCollection { /// - /// An interface that defines a property to access a collection of objects. + /// Gets a collection of objects. /// - interface IMediaTypeMappingCollection - { - /// - /// Gets a collection of objects. - /// - ICollection MediaTypeMappings { get; } - } + ICollection MediaTypeMappings { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/MediaTypeMapping.cs b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/MediaTypeMapping.cs index 0ff9f8879..412871678 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/MediaTypeMapping.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/MediaTypeMapping.cs @@ -9,39 +9,38 @@ using System.Net.Http.Headers; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.OData.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Formatter.MediaType; + +/// +/// A class to support matching media types. +/// +public abstract class MediaTypeMapping { /// - /// A class to support matching media types. + /// Initializes a new instance of a with + /// the given mediaType value. /// - public abstract class MediaTypeMapping + /// The mediaType that is associated with the request. + protected MediaTypeMapping(string mediaType) { - /// - /// Initializes a new instance of a with - /// the given mediaType value. - /// - /// The mediaType that is associated with the request. - protected MediaTypeMapping(string mediaType) + if (!MediaTypeHeaderValue.TryParse(mediaType, out MediaTypeHeaderValue value)) { - if (!MediaTypeHeaderValue.TryParse(mediaType, out MediaTypeHeaderValue value)) - { - throw new ArgumentNullException(nameof(mediaType)); - } - - this.MediaType = value; + throw new ArgumentNullException(nameof(mediaType)); } - /// - /// Gets the media type that is associated with request. - /// - public MediaTypeHeaderValue MediaType { get; protected set; } - - /// - /// Returns a value indicating whether this instance can provide a - /// for the given . - /// - /// The to check. - /// If this 's route data contains it returns 1.0 otherwise 0.0. - public abstract double TryMatchMediaType(HttpRequest request); + this.MediaType = value; } + + /// + /// Gets the media type that is associated with request. + /// + public MediaTypeHeaderValue MediaType { get; protected set; } + + /// + /// Returns a value indicating whether this instance can provide a + /// for the given . + /// + /// The to check. + /// If this 's route data contains it returns 1.0 otherwise 0.0. + public abstract double TryMatchMediaType(HttpRequest request); } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataBinaryValueMediaTypeMapping.cs b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataBinaryValueMediaTypeMapping.cs index 198c5cf8f..39b1788f6 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataBinaryValueMediaTypeMapping.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataBinaryValueMediaTypeMapping.cs @@ -8,26 +8,25 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Formatter.MediaType; + +/// +/// Media type mapping that associates requests for the raw value of binary properties to +/// the application/octet-stream content type. +/// +public class ODataBinaryValueMediaTypeMapping : ODataRawValueMediaTypeMapping { /// - /// Media type mapping that associates requests for the raw value of binary properties to - /// the application/octet-stream content type. + /// Initializes a new instance of the class. /// - public class ODataBinaryValueMediaTypeMapping : ODataRawValueMediaTypeMapping + public ODataBinaryValueMediaTypeMapping() + : base("application/octet-stream") { - /// - /// Initializes a new instance of the class. - /// - public ODataBinaryValueMediaTypeMapping() - : base("application/octet-stream") - { - } + } - /// - protected override bool IsMatch(PropertySegment propertySegment) - { - return propertySegment != null && propertySegment.Property.Type.IsBinary(); - } + /// + protected override bool IsMatch(PropertySegment propertySegment) + { + return propertySegment != null && propertySegment.Property.Type.IsBinary(); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataCountMediaTypeMapping.cs b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataCountMediaTypeMapping.cs index d74c38f4f..e019526a2 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataCountMediaTypeMapping.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataCountMediaTypeMapping.cs @@ -9,35 +9,34 @@ using Microsoft.AspNetCore.OData.Extensions; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Formatter.MediaType; + +/// +/// Media type mapping that associates requests with $count. +/// +public class ODataCountMediaTypeMapping : MediaTypeMapping { /// - /// Media type mapping that associates requests with $count. + /// Initializes a new instance of the class. /// - public class ODataCountMediaTypeMapping : MediaTypeMapping + public ODataCountMediaTypeMapping() + : base("text/plain") { - /// - /// Initializes a new instance of the class. - /// - public ODataCountMediaTypeMapping() - : base("text/plain") - { - } + } - internal static bool IsCountRequest(ODataPath path) - { - return path != null && path.LastSegment is CountSegment; - } + internal static bool IsCountRequest(ODataPath path) + { + return path != null && path.LastSegment is CountSegment; + } - /// - public override double TryMatchMediaType(HttpRequest request) + /// + public override double TryMatchMediaType(HttpRequest request) + { + if (request == null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } - - return IsCountRequest(request.ODataFeature().Path) ? 1 : 0; + throw Error.ArgumentNull(nameof(request)); } + + return IsCountRequest(request.ODataFeature().Path) ? 1 : 0; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataEnumValueMediaTypeMapping.cs b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataEnumValueMediaTypeMapping.cs index fee95b9dc..8ebb411a3 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataEnumValueMediaTypeMapping.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataEnumValueMediaTypeMapping.cs @@ -9,25 +9,24 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Formatter.MediaType; + +/// +/// Media type mapping that associates requests with $count. +/// +public class ODataEnumValueMediaTypeMapping : ODataRawValueMediaTypeMapping { /// - /// Media type mapping that associates requests with $count. + /// Initializes a new instance of the class. /// - public class ODataEnumValueMediaTypeMapping : ODataRawValueMediaTypeMapping + public ODataEnumValueMediaTypeMapping() + : base("text/plain") { - /// - /// Initializes a new instance of the class. - /// - public ODataEnumValueMediaTypeMapping() - : base("text/plain") - { - } + } - /// - protected override bool IsMatch(PropertySegment propertySegment) - { - return propertySegment != null && propertySegment.Property.Type.IsEnum(); - } + /// + protected override bool IsMatch(PropertySegment propertySegment) + { + return propertySegment != null && propertySegment.Property.Type.IsEnum(); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataMediaTypes.cs b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataMediaTypes.cs index 9caaeff0a..aab009862 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataMediaTypes.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataMediaTypes.cs @@ -10,83 +10,82 @@ using System.Diagnostics.Contracts; using System.Linq; -namespace Microsoft.AspNetCore.OData.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Formatter.MediaType; + +/// +/// Contains media types used by the OData formatter. +/// +internal static class ODataMediaTypes { - /// - /// Contains media types used by the OData formatter. - /// - internal static class ODataMediaTypes + public const string ApplicationJson = "application/json"; + public const string ApplicationJsonODataFullMetadata = "application/json;odata.metadata=full"; + public const string ApplicationJsonODataFullMetadataStreamingFalse = "application/json;odata.metadata=full;odata.streaming=false"; + public const string ApplicationJsonODataFullMetadataStreamingTrue = "application/json;odata.metadata=full;odata.streaming=true"; + public const string ApplicationJsonODataMinimalMetadata = "application/json;odata.metadata=minimal"; + public const string ApplicationJsonODataMinimalMetadataStreamingFalse = "application/json;odata.metadata=minimal;odata.streaming=false"; + public const string ApplicationJsonODataMinimalMetadataStreamingTrue = "application/json;odata.metadata=minimal;odata.streaming=true"; + public const string ApplicationJsonODataNoMetadata = "application/json;odata.metadata=none"; + public const string ApplicationJsonODataNoMetadataStreamingFalse = "application/json;odata.metadata=none;odata.streaming=false"; + public const string ApplicationJsonODataNoMetadataStreamingTrue = "application/json;odata.metadata=none;odata.streaming=true"; + public const string ApplicationJsonStreamingFalse = "application/json;odata.streaming=false"; + public const string ApplicationJsonStreamingTrue = "application/json;odata.streaming=true"; + public const string ApplicationJsonIeee754CompatibleTrue = "application/json;IEEE754Compatible=true"; + public const string ApplicationJsonIeee754CompatibleFalse = "application/json;IEEE754Compatible=false"; + public const string ApplicationJsonODataFullMetadataIeee754CompatibleTrue = "application/json;odata.metadata=full;IEEE754Compatible=true"; + public const string ApplicationJsonODataFullMetadataIeee754CompatibleFalse = "application/json;odata.metadata=full;IEEE754Compatible=false"; + public const string ApplicationJsonODataFullMetadataStreamingFalseIeee754CompatibleTrue = "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=true"; + public const string ApplicationJsonODataFullMetadataStreamingFalseIeee754CompatibleFalse = "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=false"; + public const string ApplicationJsonODataFullMetadataStreamingTrueIeee754CompatibleTrue = "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=true"; + public const string ApplicationJsonODataFullMetadataStreamingTrueIeee754CompatibleFalse = "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=false"; + public const string ApplicationJsonODataMinimalMetadataIeee754CompatibleTrue = "application/json;odata.metadata=minimal;IEEE754Compatible=true"; + public const string ApplicationJsonODataMinimalMetadataIeee754CompatibleFalse = "application/json;odata.metadata=minimal;IEEE754Compatible=false"; + public const string ApplicationJsonODataMinimalMetadataStreamingFalseIeee754CompatibleTrue = "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=true"; + public const string ApplicationJsonODataMinimalMetadataStreamingFalseIeee754CompatibleFalse = "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=false"; + public const string ApplicationJsonODataMinimalMetadataStreamingTrueIeee754CompatibleTrue = "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=true"; + public const string ApplicationJsonODataMinimalMetadataStreamingTrueIeee754CompatibleFalse = "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false"; + public const string ApplicationJsonODataNoMetadataIeee754CompatibleTrue = "application/json;odata.metadata=none;IEEE754Compatible=true"; + public const string ApplicationJsonODataNoMetadataIeee754CompatibleFalse = "application/json;odata.metadata=none;IEEE754Compatible=false"; + public const string ApplicationJsonODataNoMetadataStreamingFalseIeee754CompatibleTrue = "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=true"; + public const string ApplicationJsonODataNoMetadataStreamingFalseIeee754CompatibleFalse = "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=false"; + public const string ApplicationJsonODataNoMetadataStreamingTrueIeee754CompatibleTrue = "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=true"; + public const string ApplicationJsonODataNoMetadataStreamingTrueIeee754CompatibleFalse = "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=false"; + public const string ApplicationJsonStreamingFalseIeee754CompatibleTrue = "application/json;odata.streaming=false;IEEE754Compatible=true"; + public const string ApplicationJsonStreamingFalseIeee754CompatibleFalse = "application/json;odata.streaming=false;IEEE754Compatible=false"; + public const string ApplicationJsonStreamingTrueIeee754CompatibleTrue = "application/json;odata.streaming=true;IEEE754Compatible=true"; + public const string ApplicationJsonStreamingTrueIeee754CompatibleFalse = "application/json;odata.streaming=true;IEEE754Compatible=false"; + public const string ApplicationXml = "application/xml"; + + public static ODataMetadataLevel GetMetadataLevel(string mediaType, IEnumerable> parameters) { - public const string ApplicationJson = "application/json"; - public const string ApplicationJsonODataFullMetadata = "application/json;odata.metadata=full"; - public const string ApplicationJsonODataFullMetadataStreamingFalse = "application/json;odata.metadata=full;odata.streaming=false"; - public const string ApplicationJsonODataFullMetadataStreamingTrue = "application/json;odata.metadata=full;odata.streaming=true"; - public const string ApplicationJsonODataMinimalMetadata = "application/json;odata.metadata=minimal"; - public const string ApplicationJsonODataMinimalMetadataStreamingFalse = "application/json;odata.metadata=minimal;odata.streaming=false"; - public const string ApplicationJsonODataMinimalMetadataStreamingTrue = "application/json;odata.metadata=minimal;odata.streaming=true"; - public const string ApplicationJsonODataNoMetadata = "application/json;odata.metadata=none"; - public const string ApplicationJsonODataNoMetadataStreamingFalse = "application/json;odata.metadata=none;odata.streaming=false"; - public const string ApplicationJsonODataNoMetadataStreamingTrue = "application/json;odata.metadata=none;odata.streaming=true"; - public const string ApplicationJsonStreamingFalse = "application/json;odata.streaming=false"; - public const string ApplicationJsonStreamingTrue = "application/json;odata.streaming=true"; - public const string ApplicationJsonIeee754CompatibleTrue = "application/json;IEEE754Compatible=true"; - public const string ApplicationJsonIeee754CompatibleFalse = "application/json;IEEE754Compatible=false"; - public const string ApplicationJsonODataFullMetadataIeee754CompatibleTrue = "application/json;odata.metadata=full;IEEE754Compatible=true"; - public const string ApplicationJsonODataFullMetadataIeee754CompatibleFalse = "application/json;odata.metadata=full;IEEE754Compatible=false"; - public const string ApplicationJsonODataFullMetadataStreamingFalseIeee754CompatibleTrue = "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=true"; - public const string ApplicationJsonODataFullMetadataStreamingFalseIeee754CompatibleFalse = "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=false"; - public const string ApplicationJsonODataFullMetadataStreamingTrueIeee754CompatibleTrue = "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=true"; - public const string ApplicationJsonODataFullMetadataStreamingTrueIeee754CompatibleFalse = "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=false"; - public const string ApplicationJsonODataMinimalMetadataIeee754CompatibleTrue = "application/json;odata.metadata=minimal;IEEE754Compatible=true"; - public const string ApplicationJsonODataMinimalMetadataIeee754CompatibleFalse = "application/json;odata.metadata=minimal;IEEE754Compatible=false"; - public const string ApplicationJsonODataMinimalMetadataStreamingFalseIeee754CompatibleTrue = "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=true"; - public const string ApplicationJsonODataMinimalMetadataStreamingFalseIeee754CompatibleFalse = "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=false"; - public const string ApplicationJsonODataMinimalMetadataStreamingTrueIeee754CompatibleTrue = "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=true"; - public const string ApplicationJsonODataMinimalMetadataStreamingTrueIeee754CompatibleFalse = "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false"; - public const string ApplicationJsonODataNoMetadataIeee754CompatibleTrue = "application/json;odata.metadata=none;IEEE754Compatible=true"; - public const string ApplicationJsonODataNoMetadataIeee754CompatibleFalse = "application/json;odata.metadata=none;IEEE754Compatible=false"; - public const string ApplicationJsonODataNoMetadataStreamingFalseIeee754CompatibleTrue = "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=true"; - public const string ApplicationJsonODataNoMetadataStreamingFalseIeee754CompatibleFalse = "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=false"; - public const string ApplicationJsonODataNoMetadataStreamingTrueIeee754CompatibleTrue = "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=true"; - public const string ApplicationJsonODataNoMetadataStreamingTrueIeee754CompatibleFalse = "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=false"; - public const string ApplicationJsonStreamingFalseIeee754CompatibleTrue = "application/json;odata.streaming=false;IEEE754Compatible=true"; - public const string ApplicationJsonStreamingFalseIeee754CompatibleFalse = "application/json;odata.streaming=false;IEEE754Compatible=false"; - public const string ApplicationJsonStreamingTrueIeee754CompatibleTrue = "application/json;odata.streaming=true;IEEE754Compatible=true"; - public const string ApplicationJsonStreamingTrueIeee754CompatibleFalse = "application/json;odata.streaming=true;IEEE754Compatible=false"; - public const string ApplicationXml = "application/xml"; + if (mediaType == null) + { + return ODataMetadataLevel.Minimal; + } - public static ODataMetadataLevel GetMetadataLevel(string mediaType, IEnumerable> parameters) + if (!String.Equals(ODataMediaTypes.ApplicationJson, mediaType, + StringComparison.Ordinal)) { - if (mediaType == null) - { - return ODataMetadataLevel.Minimal; - } + return ODataMetadataLevel.Minimal; + } + + Contract.Assert(parameters != null); + KeyValuePair odataParameter = + parameters.FirstOrDefault( + (p) => String.Equals("odata.metadata", p.Key, StringComparison.OrdinalIgnoreCase)); - if (!String.Equals(ODataMediaTypes.ApplicationJson, mediaType, - StringComparison.Ordinal)) + if (!odataParameter.Equals(default(KeyValuePair))) + { + if (String.Equals("full", odataParameter.Value, StringComparison.OrdinalIgnoreCase)) { - return ODataMetadataLevel.Minimal; + return ODataMetadataLevel.Full; } - - Contract.Assert(parameters != null); - KeyValuePair odataParameter = - parameters.FirstOrDefault( - (p) => String.Equals("odata.metadata", p.Key, StringComparison.OrdinalIgnoreCase)); - - if (!odataParameter.Equals(default(KeyValuePair))) + if (String.Equals("none", odataParameter.Value, StringComparison.OrdinalIgnoreCase)) { - if (String.Equals("full", odataParameter.Value, StringComparison.OrdinalIgnoreCase)) - { - return ODataMetadataLevel.Full; - } - if (String.Equals("none", odataParameter.Value, StringComparison.OrdinalIgnoreCase)) - { - return ODataMetadataLevel.None; - } + return ODataMetadataLevel.None; } - - // Minimal is the default metadata level - return ODataMetadataLevel.Minimal; } + + // Minimal is the default metadata level + return ODataMetadataLevel.Minimal; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataPrimitiveValueMediaTypeMapping.cs b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataPrimitiveValueMediaTypeMapping.cs index b2ed5cc31..57f730c7a 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataPrimitiveValueMediaTypeMapping.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataPrimitiveValueMediaTypeMapping.cs @@ -8,28 +8,27 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Formatter.MediaType; + +/// +/// Media type mapping that associates requests with $value on primitive property. +/// +public class ODataPrimitiveValueMediaTypeMapping : ODataRawValueMediaTypeMapping { /// - /// Media type mapping that associates requests with $value on primitive property. + /// Initializes a new instance of the class. /// - public class ODataPrimitiveValueMediaTypeMapping : ODataRawValueMediaTypeMapping + public ODataPrimitiveValueMediaTypeMapping() + : base("text/plain") { - /// - /// Initializes a new instance of the class. - /// - public ODataPrimitiveValueMediaTypeMapping() - : base("text/plain") - { - } + } - /// - protected override bool IsMatch(PropertySegment propertySegment) - { - return propertySegment != null && - propertySegment.Property.Type.IsPrimitive() && - !propertySegment.Property.Type.IsBinary() && - !propertySegment.Property.Type.IsStream(); - } + /// + protected override bool IsMatch(PropertySegment propertySegment) + { + return propertySegment != null && + propertySegment.Property.Type.IsPrimitive() && + !propertySegment.Property.Type.IsBinary() && + !propertySegment.Property.Type.IsStream(); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataRawValueMediaTypeMapping.cs b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataRawValueMediaTypeMapping.cs index cb273b715..ce4fbd257 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataRawValueMediaTypeMapping.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataRawValueMediaTypeMapping.cs @@ -10,53 +10,52 @@ using Microsoft.AspNetCore.OData.Extensions; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Formatter.MediaType; + +/// +/// Media type mapping that associates requests with $count. +/// +public abstract class ODataRawValueMediaTypeMapping : MediaTypeMapping { /// - /// Media type mapping that associates requests with $count. + /// Initializes a new instance of the class. /// - public abstract class ODataRawValueMediaTypeMapping : MediaTypeMapping + protected ODataRawValueMediaTypeMapping(string mediaType) + : base(mediaType) + { + } + + /// + public override double TryMatchMediaType(HttpRequest request) { - /// - /// Initializes a new instance of the class. - /// - protected ODataRawValueMediaTypeMapping(string mediaType) - : base(mediaType) + if (request == null) { + throw Error.ArgumentNull(nameof(request)); } - /// - public override double TryMatchMediaType(HttpRequest request) - { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + ODataPath odataPath = request.ODataFeature().Path; + return (IsRawValueRequest(odataPath) && IsMatch(GetProperty(odataPath))) ? 1 : 0; + } - ODataPath odataPath = request.ODataFeature().Path; - return (IsRawValueRequest(odataPath) && IsMatch(GetProperty(odataPath))) ? 1 : 0; - } + /// + /// This method determines if the is an OData Raw value request. + /// + /// The of the path. + /// True if the request is an OData raw value request. + protected abstract bool IsMatch(PropertySegment propertySegment); - /// - /// This method determines if the is an OData Raw value request. - /// - /// The of the path. - /// True if the request is an OData raw value request. - protected abstract bool IsMatch(PropertySegment propertySegment); + internal static bool IsRawValueRequest(ODataPath path) + { + return path != null && path.LastSegment is ValueSegment; + } - internal static bool IsRawValueRequest(ODataPath path) + private static PropertySegment GetProperty(ODataPath odataPath) + { + if (odataPath == null || odataPath.Count < 2) { - return path != null && path.LastSegment is ValueSegment; + return null; } - private static PropertySegment GetProperty(ODataPath odataPath) - { - if (odataPath == null || odataPath.Count < 2) - { - return null; - } - - return odataPath.ElementAt(odataPath.Count - 2) as PropertySegment; - } + return odataPath.ElementAt(odataPath.Count - 2) as PropertySegment; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataStreamMediaTypeMapping.cs b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataStreamMediaTypeMapping.cs index 696613547..c83edffb8 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataStreamMediaTypeMapping.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/ODataStreamMediaTypeMapping.cs @@ -9,30 +9,29 @@ using Microsoft.AspNetCore.OData.Extensions; using Microsoft.AspNetCore.OData.Routing; -namespace Microsoft.AspNetCore.OData.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Formatter.MediaType; + +/// +/// Media type mapping that associates requests with stream property. +/// +public class ODataStreamMediaTypeMapping : MediaTypeMapping { /// - /// Media type mapping that associates requests with stream property. + /// Initializes a new instance of the class. /// - public class ODataStreamMediaTypeMapping : MediaTypeMapping + public ODataStreamMediaTypeMapping() + : base("application/octet-stream") { - /// - /// Initializes a new instance of the class. - /// - public ODataStreamMediaTypeMapping() - : base("application/octet-stream") - { - } + } - /// - public override double TryMatchMediaType(HttpRequest request) + /// + public override double TryMatchMediaType(HttpRequest request) + { + if (request == null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } - - return request.ODataFeature().Path.IsStreamPropertyPath() ? 1 : 0; + throw Error.ArgumentNull(nameof(request)); } + + return request.ODataFeature().Path.IsStreamPropertyPath() ? 1 : 0; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/QueryStringMediaTypeMapping.cs b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/QueryStringMediaTypeMapping.cs index c67ba863b..ddc9260e4 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/MediaType/QueryStringMediaTypeMapping.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/MediaType/QueryStringMediaTypeMapping.cs @@ -12,109 +12,108 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.OData.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Formatter.MediaType; + +/// +/// Class that provides s from query strings. +/// +public class QueryStringMediaTypeMapping : MediaTypeMapping { /// - /// Class that provides s from query strings. + /// Initializes a new instance of the class. /// - public class QueryStringMediaTypeMapping : MediaTypeMapping + /// The name of the query string parameter to match, if present. + /// The media type to use if the query parameter specified by is present + /// and assigned the value specified by . + public QueryStringMediaTypeMapping(string queryStringParameterName, string mediaType) + : this(queryStringParameterName, null, mediaType) { - /// - /// Initializes a new instance of the class. - /// - /// The name of the query string parameter to match, if present. - /// The media type to use if the query parameter specified by is present - /// and assigned the value specified by . - public QueryStringMediaTypeMapping(string queryStringParameterName, string mediaType) - : this(queryStringParameterName, null, mediaType) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The name of the query string parameter to match, if present. - /// The value of the query string parameter to match, if present. - /// The media type to use if the query parameter specified by is present - /// and assigned the value specified by . - public QueryStringMediaTypeMapping(string queryStringParameterName, string queryStringParameterValue, string mediaType) - : base(mediaType) + /// + /// Initializes a new instance of the class. + /// + /// The name of the query string parameter to match, if present. + /// The value of the query string parameter to match, if present. + /// The media type to use if the query parameter specified by is present + /// and assigned the value specified by . + public QueryStringMediaTypeMapping(string queryStringParameterName, string queryStringParameterValue, string mediaType) + : base(mediaType) + { + if (queryStringParameterName == null) { - if (queryStringParameterName == null) - { - throw Error.ArgumentNull(nameof(queryStringParameterName)); - } - - QueryStringParameterName = queryStringParameterName; - QueryStringParameterValue = queryStringParameterValue; + throw Error.ArgumentNull(nameof(queryStringParameterName)); } - /// - /// Gets the query string parameter name. - /// - public string QueryStringParameterName { get; private set; } + QueryStringParameterName = queryStringParameterName; + QueryStringParameterValue = queryStringParameterValue; + } + + /// + /// Gets the query string parameter name. + /// + public string QueryStringParameterName { get; private set; } - /// - /// Gets the query string parameter value. - /// - public string QueryStringParameterValue { get; private set; } + /// + /// Gets the query string parameter value. + /// + public string QueryStringParameterValue { get; private set; } - /// - public override double TryMatchMediaType(HttpRequest request) + /// + public override double TryMatchMediaType(HttpRequest request) + { + if (request == null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + throw Error.ArgumentNull(nameof(request)); + } - double quality = 0; + double quality = 0; - QueryString queryString = request.QueryString; - if (queryString.HasValue) + QueryString queryString = request.QueryString; + if (queryString.HasValue) + { + Dictionary parsedQuery = QueryHelpers.ParseNullableQuery(queryString.Value); + if (parsedQuery != null) { - Dictionary parsedQuery = QueryHelpers.ParseNullableQuery(queryString.Value); - if (parsedQuery != null) + IDictionary queryValues = parsedQuery + .Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.FirstOrDefault())) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + quality = DoesQueryStringMatch(queryValues) ? 1 : 0; + if (quality < 1) { - IDictionary queryValues = parsedQuery - .Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.FirstOrDefault())) - .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - - quality = DoesQueryStringMatch(queryValues) ? 1 : 0; - if (quality < 1) - { - string queryValue = queryValues.Where(kvp => kvp.Key == QueryStringParameterName) - .FirstOrDefault() - .Value; - - quality = (!string.IsNullOrEmpty(queryValue) && (queryValue == QueryStringParameterValue)) ? 1 : 0; - } + string queryValue = queryValues.Where(kvp => kvp.Key == QueryStringParameterName) + .FirstOrDefault() + .Value; + + quality = (!string.IsNullOrEmpty(queryValue) && (queryValue == QueryStringParameterValue)) ? 1 : 0; } } - - return quality; } - private bool DoesQueryStringMatch(IEnumerable> queryString) + return quality; + } + + private bool DoesQueryStringMatch(IEnumerable> queryString) + { + if (queryString != null) { - if (queryString != null) - { - string queryValue = queryString.Where(kvp => kvp.Key == QueryStringParameterName) - .FirstOrDefault() - .Value; + string queryValue = queryString.Where(kvp => kvp.Key == QueryStringParameterName) + .FirstOrDefault() + .Value; - if (queryValue != null) + if (queryValue != null) + { + // construct a media type from the query value + MediaTypeHeaderValue parsedValue; + bool success = MediaTypeHeaderValue.TryParse(queryValue, out parsedValue); + if (success && MediaType.Equals(parsedValue)) { - // construct a media type from the query value - MediaTypeHeaderValue parsedValue; - bool success = MediaTypeHeaderValue.TryParse(queryValue, out parsedValue); - if (success && MediaType.Equals(parsedValue)) - { - return true; - } + return true; } } - - return false; } + + return false; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataActionParameters.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataActionParameters.cs index 6a64d2bed..d97c16413 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataActionParameters.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataActionParameters.cs @@ -9,15 +9,14 @@ using System.Collections.Generic; using Microsoft.AspNetCore.OData.Abstracts; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// ActionPayload holds the Parameter names and values provided by a client in a POST request +/// to invoke a particular Action. The Parameter values are stored in the dictionary keyed using the Parameter name. +/// +[NonValidatingParameterBinding] +[SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "ODataActionParameters is more appropriate here.")] +public class ODataActionParameters : Dictionary { - /// - /// ActionPayload holds the Parameter names and values provided by a client in a POST request - /// to invoke a particular Action. The Parameter values are stored in the dictionary keyed using the Parameter name. - /// - [NonValidatingParameterBinding] - [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "ODataActionParameters is more appropriate here.")] - public class ODataActionParameters : Dictionary - { - } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataBodyModelBinder.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataBodyModelBinder.cs index 6ea961550..6ade07930 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataBodyModelBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataBodyModelBinder.cs @@ -19,95 +19,94 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// A model binder for ODataParameterValue values. +/// +/// +/// This class is similar to ODataModelBinderProvider in AspNet. The flow is similar but the +/// type are dissimilar enough making a common version more complex than separate versions. +/// +internal class ODataBodyModelBinder : IModelBinder { - /// - /// A model binder for ODataParameterValue values. - /// - /// - /// This class is similar to ODataModelBinderProvider in AspNet. The flow is similar but the - /// type are dissimilar enough making a common version more complex than separate versions. - /// - internal class ODataBodyModelBinder : IModelBinder + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We don't want to fail in model binding.")] + public async Task BindModelAsync(ModelBindingContext bindingContext) { - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We don't want to fail in model binding.")] - public async Task BindModelAsync(ModelBindingContext bindingContext) + if (bindingContext == null) { - if (bindingContext == null) - { - throw new ArgumentNullException(nameof(bindingContext)); - } + throw new ArgumentNullException(nameof(bindingContext)); + } - if (bindingContext.ModelMetadata == null) - { - throw Error.Argument("bindingContext", SRResources.ModelBinderUtil_ModelMetadataCannotBeNull); - } + if (bindingContext.ModelMetadata == null) + { + throw Error.Argument("bindingContext", SRResources.ModelBinderUtil_ModelMetadataCannotBeNull); + } - HttpContext httpContext = bindingContext.HttpContext; + HttpContext httpContext = bindingContext.HttpContext; - ODataFeature odataFeature = httpContext.ODataFeature() as ODataFeature; - IDictionary values = odataFeature?.BodyValues; + ODataFeature odataFeature = httpContext.ODataFeature() as ODataFeature; + IDictionary values = odataFeature?.BodyValues; + if (values == null) + { + values = await ReadODataBodyAsync(bindingContext).ConfigureAwait(false); if (values == null) { - values = await ReadODataBodyAsync(bindingContext).ConfigureAwait(false); - if (values == null) - { - values = new Dictionary(); - } - - if (odataFeature != null) - { - odataFeature.BodyValues = values; - } + values = new Dictionary(); } - if (values.TryGetValue(bindingContext.ModelMetadata.Name, out object result)) + if (odataFeature != null) { - //ValueProviderResult valueProviderResult = new ValueProviderResult(result); - //bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); - - bindingContext.Result = ModelBindingResult.Success(result); + odataFeature.BodyValues = values; } } - internal static ODataDeserializerContext BuildDeserializerContext(ModelBindingContext bindingContext/*, IEdmTypeReference edmTypeReference*/) + if (values.TryGetValue(bindingContext.ModelMetadata.Name, out object result)) { - HttpRequest request = bindingContext.HttpContext.Request; - ODataPath path = request.ODataFeature().Path; - IEdmModel edmModel = request.GetModel(); - TimeZoneInfo tzi = request.GetTimeZoneInfo(); + //ValueProviderResult valueProviderResult = new ValueProviderResult(result); + //bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); - return new ODataDeserializerContext - { - Path = path, - Model = edmModel, - Request = request, - ResourceType = bindingContext.ModelType, - TimeZone = tzi, - // ResourceEdmType = edmTypeReference, - }; + bindingContext.Result = ModelBindingResult.Success(result); } + } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "")] - public static async Task> ReadODataBodyAsync(ModelBindingContext bindingContext) + internal static ODataDeserializerContext BuildDeserializerContext(ModelBindingContext bindingContext/*, IEdmTypeReference edmTypeReference*/) + { + HttpRequest request = bindingContext.HttpContext.Request; + ODataPath path = request.ODataFeature().Path; + IEdmModel edmModel = request.GetModel(); + TimeZoneInfo tzi = request.GetTimeZoneInfo(); + + return new ODataDeserializerContext { - ODataActionPayloadDeserializer deserializer = bindingContext.HttpContext.Request.GetRouteServices().GetService(); - if (deserializer == null) - { - return null; - } + Path = path, + Model = edmModel, + Request = request, + ResourceType = bindingContext.ModelType, + TimeZone = tzi, + // ResourceEdmType = edmTypeReference, + }; + } + + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "")] + public static async Task> ReadODataBodyAsync(ModelBindingContext bindingContext) + { + ODataActionPayloadDeserializer deserializer = bindingContext.HttpContext.Request.GetRouteServices().GetService(); + if (deserializer == null) + { + return null; + } - ODataDeserializerContext context = BuildDeserializerContext(bindingContext); - HttpRequest request = bindingContext.HttpContext.Request; + ODataDeserializerContext context = BuildDeserializerContext(bindingContext); + HttpRequest request = bindingContext.HttpContext.Request; - IODataRequestMessage oDataRequestMessage = - ODataMessageWrapperHelper.Create(request.Body, request.Headers); - IEdmModel model = request.GetModel(); - using (var messageReader = new ODataMessageReader(oDataRequestMessage, null, model)) - { - var result = await deserializer.ReadAsync(messageReader, typeof(ODataActionParameters), context).ConfigureAwait(false); - return result as ODataActionParameters; - } + IODataRequestMessage oDataRequestMessage = + ODataMessageWrapperHelper.Create(request.Body, request.Headers); + IEdmModel model = request.GetModel(); + using (var messageReader = new ODataMessageReader(oDataRequestMessage, null, model)) + { + var result = await deserializer.ReadAsync(messageReader, typeof(ODataActionParameters), context).ConfigureAwait(false); + return result as ODataActionParameters; } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataInputFormatter.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataInputFormatter.cs index bea107117..0be8f284a 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataInputFormatter.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataInputFormatter.cs @@ -25,283 +25,282 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// The implementation of class to handle OData reading. +/// +public class ODataInputFormatter : TextInputFormatter { /// - /// The implementation of class to handle OData reading. + /// The set of payload kinds this formatter will accept in CanRead. + /// + private readonly ISet _payloadKinds; + + /// + /// Initializes a new instance of the class. /// - public class ODataInputFormatter : TextInputFormatter + /// The kind of payloads this formatter supports. + public ODataInputFormatter(IEnumerable payloadKinds) { - /// - /// The set of payload kinds this formatter will accept in CanRead. - /// - private readonly ISet _payloadKinds; - - /// - /// Initializes a new instance of the class. - /// - /// The kind of payloads this formatter supports. - public ODataInputFormatter(IEnumerable payloadKinds) + if (payloadKinds == null) { - if (payloadKinds == null) - { - throw new ArgumentNullException(nameof(payloadKinds)); - } - - _payloadKinds = new HashSet(payloadKinds); + throw new ArgumentNullException(nameof(payloadKinds)); } - /// - /// Gets or sets a method that allows consumers to provide an alternate base address for OData Uri. - /// - public Func BaseAddressFactory { get; set; } + _payloadKinds = new HashSet(payloadKinds); + } + + /// + /// Gets or sets a method that allows consumers to provide an alternate base address for OData Uri. + /// + public Func BaseAddressFactory { get; set; } - /// - public override IReadOnlyList GetSupportedContentTypes(string contentType, Type objectType) + /// + public override IReadOnlyList GetSupportedContentTypes(string contentType, Type objectType) + { + if (SupportedMediaTypes.Count == 0) { - if (SupportedMediaTypes.Count == 0) - { - // note: this is parity with the base implementation when there are no matches - return default; - } + // note: this is parity with the base implementation when there are no matches + return default; + } + + return base.GetSupportedContentTypes(contentType, objectType); + } - return base.GetSupportedContentTypes(contentType, objectType); + /// + public override bool CanRead(InputFormatterContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); } - /// - public override bool CanRead(InputFormatterContext context) + HttpRequest request = context.HttpContext.Request; + if (request == null) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } + throw Error.InvalidOperation(SRResources.ReadFromStreamAsyncMustHaveRequest); + } - HttpRequest request = context.HttpContext.Request; - if (request == null) - { - throw Error.InvalidOperation(SRResources.ReadFromStreamAsyncMustHaveRequest); - } + // Ignore non-OData requests. + if (request.ODataFeature().Path == null) + { + return false; + } - // Ignore non-OData requests. - if (request.ODataFeature().Path == null) - { - return false; - } + Type type = context.ModelType; + if (type == null) + { + throw Error.ArgumentNull("type"); + } - Type type = context.ModelType; - if (type == null) - { - throw Error.ArgumentNull("type"); - } + IODataDeserializer deserializer = GetDeserializer(request, type, out _); + if (deserializer != null) + { + return _payloadKinds.Contains(deserializer.ODataPayloadKind); + } - IODataDeserializer deserializer = GetDeserializer(request, type, out _); - if (deserializer != null) - { - return _payloadKinds.Contains(deserializer.ODataPayloadKind); - } + return false; + } - return false; + /// + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")] + public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); } - /// - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is reflected into a faulted task.")] - public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) + Type type = context.ModelType; + if (type == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - Type type = context.ModelType; - if (type == null) - { - throw Error.ArgumentNull("type"); - } - - HttpRequest request = context.HttpContext.Request; - if (request == null) - { - throw Error.InvalidOperation(SRResources.ReadFromStreamAsyncMustHaveRequest); - } + throw Error.ArgumentNull("type"); + } - object defaultValue = GetDefaultValueForType(type); - try - { + HttpRequest request = context.HttpContext.Request; + if (request == null) + { + throw Error.InvalidOperation(SRResources.ReadFromStreamAsyncMustHaveRequest); + } - IList toDispose = new List(); + object defaultValue = GetDefaultValueForType(type); + try + { - Uri baseAddress = GetBaseAddressInternal(request); + IList toDispose = new List(); - object result = await ReadFromStreamAsync( - type, - defaultValue, - baseAddress, - request.GetODataVersion(), - request, - toDispose).ConfigureAwait(false); + Uri baseAddress = GetBaseAddressInternal(request); - foreach (IDisposable obj in toDispose) - { - obj.Dispose(); - } + object result = await ReadFromStreamAsync( + type, + defaultValue, + baseAddress, + request.GetODataVersion(), + request, + toDispose).ConfigureAwait(false); - return InputFormatterResult.Success(result); - } - catch (Exception ex) + foreach (IDisposable obj in toDispose) { - context.ModelState.AddModelError(context.ModelName, ex, context.Metadata); - return InputFormatterResult.Failure(); + obj.Dispose(); } + + return InputFormatterResult.Success(result); } + catch (Exception ex) + { + context.ModelState.AddModelError(context.ModelName, ex, context.Metadata); + return InputFormatterResult.Failure(); + } + } - /// - /// Returns a base address to be used in the service root when reading or writing OData uris. - /// - /// The HttpRequest object for the given request. - /// The base address to be used as part of the service root in the OData uri; must terminate with a trailing '/'. - public static Uri GetDefaultBaseAddress(HttpRequest request) + /// + /// Returns a base address to be used in the service root when reading or writing OData uris. + /// + /// The HttpRequest object for the given request. + /// The base address to be used as part of the service root in the OData uri; must terminate with a trailing '/'. + public static Uri GetDefaultBaseAddress(HttpRequest request) + { + if (request == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + throw new ArgumentNullException(nameof(request)); + } - string baseAddress = request.CreateODataLink(); + string baseAddress = request.CreateODataLink(); - if (baseAddress == null) - { - throw new SerializationException(SRResources.UnableToDetermineBaseUrl); - } + if (baseAddress == null) + { + throw new SerializationException(SRResources.UnableToDetermineBaseUrl); + } + + return baseAddress[baseAddress.Length - 1] != '/' ? new Uri(baseAddress + '/') : new Uri(baseAddress); - return baseAddress[baseAddress.Length - 1] != '/' ? new Uri(baseAddress + '/') : new Uri(baseAddress); + } + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is sent to the logger, which may throw it.")] + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "oDataMessageReader mis registered for disposal.")] + internal static async Task ReadFromStreamAsync( + Type type, + object defaultValue, + Uri baseAddress, + ODataVersion version, + HttpRequest request, + IList disposes) + { + object result; + IEdmModel model = request.GetModel(); + IEdmTypeReference expectedPayloadType; + IODataDeserializer deserializer = GetDeserializer(request, type, out expectedPayloadType); + if (deserializer == null) + { + throw Error.Argument("type", SRResources.FormatterReadIsNotSupportedForType, type.FullName, typeof(ODataInputFormatter).FullName); } - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The caught exception type is sent to the logger, which may throw it.")] - [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "oDataMessageReader mis registered for disposal.")] - internal static async Task ReadFromStreamAsync( - Type type, - object defaultValue, - Uri baseAddress, - ODataVersion version, - HttpRequest request, - IList disposes) + try { - object result; - IEdmModel model = request.GetModel(); - IEdmTypeReference expectedPayloadType; - IODataDeserializer deserializer = GetDeserializer(request, type, out expectedPayloadType); - if (deserializer == null) - { - throw Error.Argument("type", SRResources.FormatterReadIsNotSupportedForType, type.FullName, typeof(ODataInputFormatter).FullName); - } + ODataMessageReaderSettings oDataReaderSettings = request.GetReaderSettings(); + oDataReaderSettings.BaseUri = baseAddress; + oDataReaderSettings.Validations = oDataReaderSettings.Validations & ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType; + oDataReaderSettings.Version = version; - try - { - ODataMessageReaderSettings oDataReaderSettings = request.GetReaderSettings(); - oDataReaderSettings.BaseUri = baseAddress; - oDataReaderSettings.Validations = oDataReaderSettings.Validations & ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType; - oDataReaderSettings.Version = version; + IODataRequestMessage oDataRequestMessage = + ODataMessageWrapperHelper.Create(new StreamWrapper(request.Body), request.Headers, request.GetODataContentIdMapping(), request.GetRouteServices()); + ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, oDataReaderSettings, model); + disposes.Add(oDataMessageReader); - IODataRequestMessage oDataRequestMessage = - ODataMessageWrapperHelper.Create(new StreamWrapper(request.Body), request.Headers, request.GetODataContentIdMapping(), request.GetRouteServices()); - ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, oDataReaderSettings, model); - disposes.Add(oDataMessageReader); + ODataPath path = request.ODataFeature().Path; + ODataDeserializerContext readContext = BuildDeserializerContext(request); - ODataPath path = request.ODataFeature().Path; - ODataDeserializerContext readContext = BuildDeserializerContext(request); + readContext.Path = path; + readContext.Model = model; + readContext.ResourceType = type; + readContext.ResourceEdmType = expectedPayloadType; - readContext.Path = path; - readContext.Model = model; - readContext.ResourceType = type; - readContext.ResourceEdmType = expectedPayloadType; + result = await deserializer.ReadAsync(oDataMessageReader, type, readContext).ConfigureAwait(false); + } + catch (Exception ex) + { + LoggerError(request.HttpContext, ex); + result = defaultValue; + } - result = await deserializer.ReadAsync(oDataMessageReader, type, readContext).ConfigureAwait(false); - } - catch (Exception ex) - { - LoggerError(request.HttpContext, ex); - result = defaultValue; - } + return result; + } - return result; + private static ODataDeserializerContext BuildDeserializerContext(HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - private static ODataDeserializerContext BuildDeserializerContext(HttpRequest request) + return new ODataDeserializerContext() { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + Request = request, + TimeZone = request.GetTimeZoneInfo(), + }; + } - return new ODataDeserializerContext() - { - Request = request, - TimeZone = request.GetTimeZoneInfo(), - }; + private static void LoggerError(HttpContext context, Exception ex) + { + ILogger logger = context.RequestServices.GetService(); + if (logger == null) + { + ExceptionDispatchInfo.Capture(ex).Throw(); } - private static void LoggerError(HttpContext context, Exception ex) - { - ILogger logger = context.RequestServices.GetService(); - if (logger == null) - { - ExceptionDispatchInfo.Capture(ex).Throw(); - } + logger.LogError(ex, string.Empty); + } - logger.LogError(ex, string.Empty); + /// + /// Internal method used for selecting the base address to be used with OData uris. + /// If the consumer has provided a delegate for overriding our default implementation, + /// we call that, otherwise we default to existing behavior below. + /// + /// The HttpRequest object for the given request. + /// The base address to be used as part of the service root; must terminate with a trailing '/'. + private Uri GetBaseAddressInternal(HttpRequest request) + { + if (BaseAddressFactory != null) + { + return BaseAddressFactory(request); } - - /// - /// Internal method used for selecting the base address to be used with OData uris. - /// If the consumer has provided a delegate for overriding our default implementation, - /// we call that, otherwise we default to existing behavior below. - /// - /// The HttpRequest object for the given request. - /// The base address to be used as part of the service root; must terminate with a trailing '/'. - private Uri GetBaseAddressInternal(HttpRequest request) + else { - if (BaseAddressFactory != null) - { - return BaseAddressFactory(request); - } - else - { - return ODataInputFormatter.GetDefaultBaseAddress(request); - } + return ODataInputFormatter.GetDefaultBaseAddress(request); } + } - /// - /// Gets the deserializer and the expected payload type. - /// - /// The HttpRequest. - /// The input type. - /// Output the expected payload type. - /// null or the OData deserializer - private static IODataDeserializer GetDeserializer(HttpRequest request, Type type, out IEdmTypeReference expectedPayloadType) - { - Contract.Assert(request != null); + /// + /// Gets the deserializer and the expected payload type. + /// + /// The HttpRequest. + /// The input type. + /// Output the expected payload type. + /// null or the OData deserializer + private static IODataDeserializer GetDeserializer(HttpRequest request, Type type, out IEdmTypeReference expectedPayloadType) + { + Contract.Assert(request != null); - IODataFeature odataFeature = request.ODataFeature(); - ODataPath path = odataFeature.Path; - IEdmModel model = odataFeature.Model; - expectedPayloadType = null; + IODataFeature odataFeature = request.ODataFeature(); + ODataPath path = odataFeature.Path; + IEdmModel model = odataFeature.Model; + expectedPayloadType = null; - IODataDeserializerProvider deserializerProvider = request.GetRouteServices().GetRequiredService(); + IODataDeserializerProvider deserializerProvider = request.GetRouteServices().GetRequiredService(); - // Get the deserializer using the CLR type first from the deserializer provider. - IODataDeserializer deserializer = deserializerProvider.GetODataDeserializer(type, request); - if (deserializer == null) + // Get the deserializer using the CLR type first from the deserializer provider. + IODataDeserializer deserializer = deserializerProvider.GetODataDeserializer(type, request); + if (deserializer == null) + { + expectedPayloadType = EdmLibHelper.GetExpectedPayloadType(type, path, model); + if (expectedPayloadType != null) { - expectedPayloadType = EdmLibHelper.GetExpectedPayloadType(type, path, model); - if (expectedPayloadType != null) - { - // we are in typeless mode, get the deserializer using the edm type from the path. - deserializer = deserializerProvider.GetEdmTypeDeserializer(expectedPayloadType); - } + // we are in typeless mode, get the deserializer using the edm type from the path. + deserializer = deserializerProvider.GetEdmTypeDeserializer(expectedPayloadType); } - - return deserializer; } + + return deserializer; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataInputFormatterFactory.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataInputFormatterFactory.cs index a2b3bcbc0..6de8fdfd9 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataInputFormatterFactory.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataInputFormatterFactory.cs @@ -10,117 +10,116 @@ using Microsoft.AspNetCore.OData.Formatter.MediaType; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// Factory for classes to handle OData. +/// +public static class ODataInputFormatterFactory { /// - /// Factory for classes to handle OData. + /// Creates a list of media type formatters to handle OData deserialization. /// - public static class ODataInputFormatterFactory + /// A list of media type formatters to handle OData. + public static IList Create() { - /// - /// Creates a list of media type formatters to handle OData deserialization. - /// - /// A list of media type formatters to handle OData. - public static IList Create() + return new List() { - return new List() - { - // Place JSON formatter first so it gets used when the request doesn't ask for a specific content type - CreateApplicationJson(), - CreateApplicationXml(), - CreateRawValue() - }; - } + // Place JSON formatter first so it gets used when the request doesn't ask for a specific content type + CreateApplicationJson(), + CreateApplicationXml(), + CreateRawValue() + }; + } - private static ODataInputFormatter CreateApplicationJson() - { - ODataInputFormatter formatter = CreateFormatterWithoutMediaTypes( - ODataPayloadKind.ResourceSet, - ODataPayloadKind.Resource, - ODataPayloadKind.Property, - ODataPayloadKind.EntityReferenceLink, - ODataPayloadKind.EntityReferenceLinks, - ODataPayloadKind.Collection, - ODataPayloadKind.ServiceDocument, - ODataPayloadKind.Error, - ODataPayloadKind.Parameter, - ODataPayloadKind.Delta); + private static ODataInputFormatter CreateApplicationJson() + { + ODataInputFormatter formatter = CreateFormatterWithoutMediaTypes( + ODataPayloadKind.ResourceSet, + ODataPayloadKind.Resource, + ODataPayloadKind.Property, + ODataPayloadKind.EntityReferenceLink, + ODataPayloadKind.EntityReferenceLinks, + ODataPayloadKind.Collection, + ODataPayloadKind.ServiceDocument, + ODataPayloadKind.Error, + ODataPayloadKind.Parameter, + ODataPayloadKind.Delta); - // Add minimal metadata as the first media type so it gets used when the request doesn't - // ask for a specific content type - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadata); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadata); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadata); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJson); - // NOTE: The order in which the media types are added is relevant due to how ASP.NET Core handles content negotiation - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingTrueIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingTrueIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingFalseIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingFalseIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingTrueIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingTrueIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingFalseIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingFalseIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingTrueIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingTrueIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingFalseIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingFalseIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingTrueIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingTrueIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingFalseIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingFalseIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonIeee754CompatibleTrue); + // Add minimal metadata as the first media type so it gets used when the request doesn't + // ask for a specific content type + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadata); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadata); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadata); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJson); + // NOTE: The order in which the media types are added is relevant due to how ASP.NET Core handles content negotiation + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingTrueIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingTrueIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingFalseIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingFalseIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingTrueIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingTrueIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingFalseIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingFalseIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingTrueIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingTrueIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingFalseIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingFalseIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingTrueIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingTrueIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingFalseIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingFalseIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonIeee754CompatibleTrue); - return formatter; - } + return formatter; + } - private static ODataInputFormatter CreateApplicationXml() - { - ODataInputFormatter formatter = CreateFormatterWithoutMediaTypes( - ODataPayloadKind.MetadataDocument); + private static ODataInputFormatter CreateApplicationXml() + { + ODataInputFormatter formatter = CreateFormatterWithoutMediaTypes( + ODataPayloadKind.MetadataDocument); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationXml); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationXml); - return formatter; - } + return formatter; + } - private static ODataInputFormatter CreateRawValue() - { - ODataInputFormatter formatter = CreateFormatterWithoutMediaTypes(ODataPayloadKind.Value); + private static ODataInputFormatter CreateRawValue() + { + ODataInputFormatter formatter = CreateFormatterWithoutMediaTypes(ODataPayloadKind.Value); - formatter.SupportedMediaTypes.Add("text/plain"); - return formatter; - } + formatter.SupportedMediaTypes.Add("text/plain"); + return formatter; + } - private static ODataInputFormatter CreateFormatterWithoutMediaTypes(params ODataPayloadKind[] payloadKinds) - { - ODataInputFormatter formatter = new ODataInputFormatter(payloadKinds); - AddSupportedEncodings(formatter); - return formatter; - } + private static ODataInputFormatter CreateFormatterWithoutMediaTypes(params ODataPayloadKind[] payloadKinds) + { + ODataInputFormatter formatter = new ODataInputFormatter(payloadKinds); + AddSupportedEncodings(formatter); + return formatter; + } - private static void AddSupportedEncodings(ODataInputFormatter formatter) - { - formatter.SupportedEncodings.Add(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, - throwOnInvalidBytes: true)); + private static void AddSupportedEncodings(ODataInputFormatter formatter) + { + formatter.SupportedEncodings.Add(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, + throwOnInvalidBytes: true)); - formatter.SupportedEncodings.Add(new UnicodeEncoding(bigEndian: false, byteOrderMark: true, - throwOnInvalidBytes: true)); - } + formatter.SupportedEncodings.Add(new UnicodeEncoding(bigEndian: false, byteOrderMark: true, + throwOnInvalidBytes: true)); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataMessageWrapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataMessageWrapper.cs index c6d11f908..ec1aa9fc9 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataMessageWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataMessageWrapper.cs @@ -12,163 +12,162 @@ using System.Threading.Tasks; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// Wrapper for IODataRequestMessage and IODataResponseMessage. +/// +internal class ODataMessageWrapper : IODataRequestMessageAsync, IODataResponseMessageAsync, IODataPayloadUriConverter, IServiceCollectionProvider, IDisposable { - /// - /// Wrapper for IODataRequestMessage and IODataResponseMessage. - /// - internal class ODataMessageWrapper : IODataRequestMessageAsync, IODataResponseMessageAsync, IODataPayloadUriConverter, IServiceCollectionProvider, IDisposable + private Stream _stream; + private Dictionary _headers; + private IDictionary _contentIdMapping; + private static readonly Regex ContentIdReferencePattern = new Regex(@"\$\d", RegexOptions.Compiled); + + public ODataMessageWrapper() + : this(stream: null, headers: null) { - private Stream _stream; - private Dictionary _headers; - private IDictionary _contentIdMapping; - private static readonly Regex ContentIdReferencePattern = new Regex(@"\$\d", RegexOptions.Compiled); + } - public ODataMessageWrapper() - : this(stream: null, headers: null) - { - } + public ODataMessageWrapper(Stream stream) + : this(stream: stream, headers: null) + { + } - public ODataMessageWrapper(Stream stream) - : this(stream: stream, headers: null) - { - } + public ODataMessageWrapper(Stream stream, Dictionary headers) + : this(stream: stream, headers: headers, contentIdMapping: null) + { + } - public ODataMessageWrapper(Stream stream, Dictionary headers) - : this(stream: stream, headers: headers, contentIdMapping: null) + public ODataMessageWrapper(Stream stream, Dictionary headers, IDictionary contentIdMapping) + { + _stream = stream; + if (headers != null) { + _headers = headers; } - - public ODataMessageWrapper(Stream stream, Dictionary headers, IDictionary contentIdMapping) + else { - _stream = stream; - if (headers != null) - { - _headers = headers; - } - else - { - _headers = new Dictionary(); - } - _contentIdMapping = contentIdMapping ?? new Dictionary(); + _headers = new Dictionary(); } + _contentIdMapping = contentIdMapping ?? new Dictionary(); + } - public IEnumerable> Headers + public IEnumerable> Headers + { + get { - get - { - return _headers; - } + return _headers; } + } - public string Method + public string Method + { + get { - get - { - throw new NotImplementedException(); - } - set - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } - - public Uri Url + set { - get - { - throw new NotImplementedException(); - } - set - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } + } - public int StatusCode + public Uri Url + { + get { - get - { - throw new NotImplementedException(); - } - set - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } - - public IServiceProvider ServiceProvider { get; set; } - - public string GetHeader(string headerName) + set { - string value; - if (_headers.TryGetValue(headerName, out value)) - { - return value; - } - - // try case-insensitive - foreach (string key in _headers.Keys) - { - if (key.Equals(headerName, StringComparison.OrdinalIgnoreCase)) - { - return _headers[key]; - } - } - - return null; + throw new NotImplementedException(); } + } - public Stream GetStream() + public int StatusCode + { + get { - return _stream; + throw new NotImplementedException(); } - - public Task GetStreamAsync() + set { - TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); - taskCompletionSource.SetResult(_stream); - return taskCompletionSource.Task; + throw new NotImplementedException(); } + } + + public IServiceProvider ServiceProvider { get; set; } - public void SetHeader(string headerName, string headerValue) + public string GetHeader(string headerName) + { + string value; + if (_headers.TryGetValue(headerName, out value)) { - _headers[headerName] = headerValue; + return value; } - public Uri ConvertPayloadUri(Uri baseUri, Uri payloadUri) + // try case-insensitive + foreach (string key in _headers.Keys) { - if (payloadUri == null) + if (key.Equals(headerName, StringComparison.OrdinalIgnoreCase)) { - throw new ArgumentNullException(nameof(payloadUri)); + return _headers[key]; } + } - string originalPayloadUri = payloadUri.OriginalString; - if (ContentIdReferencePattern.IsMatch(originalPayloadUri)) - { - string resolvedUri = ContentIdHelpers.ResolveContentId(originalPayloadUri, _contentIdMapping); - return new Uri(resolvedUri, UriKind.RelativeOrAbsolute); - } + return null; + } + + public Stream GetStream() + { + return _stream; + } + + public Task GetStreamAsync() + { + TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); + taskCompletionSource.SetResult(_stream); + return taskCompletionSource.Task; + } + + public void SetHeader(string headerName, string headerValue) + { + _headers[headerName] = headerValue; + } - // Returning null for default resolution. - return null; + public Uri ConvertPayloadUri(Uri baseUri, Uri payloadUri) + { + if (payloadUri == null) + { + throw new ArgumentNullException(nameof(payloadUri)); } - /// - public void Dispose() + string originalPayloadUri = payloadUri.OriginalString; + if (ContentIdReferencePattern.IsMatch(originalPayloadUri)) { - Dispose(true); + string resolvedUri = ContentIdHelpers.ResolveContentId(originalPayloadUri, _contentIdMapping); + return new Uri(resolvedUri, UriKind.RelativeOrAbsolute); } - /// - protected void Dispose(bool disposing) + // Returning null for default resolution. + return null; + } + + /// + public void Dispose() + { + Dispose(true); + } + + /// + protected void Dispose(bool disposing) + { + if (disposing) { - if (disposing) + if (_stream != null) { - if (_stream != null) - { - _stream.Dispose(); - } + _stream.Dispose(); } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataMessageWrapperHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataMessageWrapperHelper.cs index c9d60d6b5..600052f89 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataMessageWrapperHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataMessageWrapperHelper.cs @@ -11,34 +11,33 @@ using System.Linq; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +internal static class ODataMessageWrapperHelper { - internal static class ODataMessageWrapperHelper + internal static ODataMessageWrapper Create(Stream stream, IHeaderDictionary headers) { - internal static ODataMessageWrapper Create(Stream stream, IHeaderDictionary headers) - { - return Create(stream, headers, contentIdMapping: null); - } + return Create(stream, headers, contentIdMapping: null); + } - internal static ODataMessageWrapper Create(Stream stream, IHeaderDictionary headers, IServiceProvider container) - { - return Create(stream, headers, null, container); - } + internal static ODataMessageWrapper Create(Stream stream, IHeaderDictionary headers, IServiceProvider container) + { + return Create(stream, headers, null, container); + } - internal static ODataMessageWrapper Create(Stream stream, IHeaderDictionary headers, IDictionary contentIdMapping, IServiceProvider serviceProvider) - { - ODataMessageWrapper responseMessageWrapper = Create(stream, headers, contentIdMapping); - responseMessageWrapper.ServiceProvider = serviceProvider; + internal static ODataMessageWrapper Create(Stream stream, IHeaderDictionary headers, IDictionary contentIdMapping, IServiceProvider serviceProvider) + { + ODataMessageWrapper responseMessageWrapper = Create(stream, headers, contentIdMapping); + responseMessageWrapper.ServiceProvider = serviceProvider; - return responseMessageWrapper; - } + return responseMessageWrapper; + } - internal static ODataMessageWrapper Create(Stream stream, IHeaderDictionary headers, IDictionary contentIdMapping) - { - return new ODataMessageWrapper( - stream, - headers.ToDictionary(kvp => kvp.Key, kvp => string.Join(";", kvp.Value)), - contentIdMapping); - } + internal static ODataMessageWrapper Create(Stream stream, IHeaderDictionary headers, IDictionary contentIdMapping) + { + return new ODataMessageWrapper( + stream, + headers.ToDictionary(kvp => kvp.Key, kvp => string.Join(";", kvp.Value)), + contentIdMapping); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataMetadataLevel.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataMetadataLevel.cs index bc6717365..5c1a70f48 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataMetadataLevel.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataMetadataLevel.cs @@ -5,26 +5,25 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// The amount of metadata information to serialize in an OData response. +/// +public enum ODataMetadataLevel { /// - /// The amount of metadata information to serialize in an OData response. + /// JSON minimal metadata. /// - public enum ODataMetadataLevel - { - /// - /// JSON minimal metadata. - /// - Minimal = 0, + Minimal = 0, - /// - /// JSON full metadata. - /// - Full = 1, + /// + /// JSON full metadata. + /// + Full = 1, - /// - /// JSON none metadata. - /// - None = 2 - } + /// + /// JSON none metadata. + /// + None = 2 } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataModelBinder.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataModelBinder.cs index d72b413e4..c5c226d71 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataModelBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataModelBinder.cs @@ -21,146 +21,145 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// A model binder for ODataParameterValue values. +/// +/// +/// This class is similar to ODataModelBinderProvider in AspNet. The flow is similar but the +/// type are dissimilar enough making a common version more complex than separate versions. +/// +internal class ODataModelBinder : IModelBinder { - /// - /// A model binder for ODataParameterValue values. - /// - /// - /// This class is similar to ODataModelBinderProvider in AspNet. The flow is similar but the - /// type are dissimilar enough making a common version more complex than separate versions. - /// - internal class ODataModelBinder : IModelBinder + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We don't want to fail in model binding.")] + public Task BindModelAsync(ModelBindingContext bindingContext) { - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We don't want to fail in model binding.")] - public Task BindModelAsync(ModelBindingContext bindingContext) + if (bindingContext == null) { - if (bindingContext == null) + throw Error.ArgumentNull(nameof(bindingContext)); + } + + if (bindingContext.ModelMetadata == null) + { + throw Error.Argument(nameof(bindingContext), SRResources.ModelBinderUtil_ModelMetadataCannotBeNull); + } + + ValueProviderResult valueProviderResult = ValueProviderResult.None; + string modelName = ODataParameterValue.ParameterValuePrefix + bindingContext.ModelName; + try + { + // Look in route data for a ODataParameterValue. + object valueAsObject = null; + if (!bindingContext.HttpContext.Request.ODataFeature().RoutingConventionsStore.TryGetValue(modelName, out valueAsObject)) { - throw Error.ArgumentNull(nameof(bindingContext)); + bindingContext.ActionContext.RouteData.Values.TryGetValue(modelName, out valueAsObject); } - if (bindingContext.ModelMetadata == null) + if (valueAsObject != null) { - throw Error.Argument(nameof(bindingContext), SRResources.ModelBinderUtil_ModelMetadataCannotBeNull); - } + StringValues stringValues = new StringValues(valueAsObject.ToString()); + valueProviderResult = new ValueProviderResult(stringValues); + bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); - ValueProviderResult valueProviderResult = ValueProviderResult.None; - string modelName = ODataParameterValue.ParameterValuePrefix + bindingContext.ModelName; - try + ODataParameterValue paramValue = valueAsObject as ODataParameterValue; + if (paramValue != null) + { + HttpRequest request = bindingContext.HttpContext.Request; + object model = ConvertTo(paramValue, bindingContext, request.GetRouteServices()); + bindingContext.Result = ModelBindingResult.Success(model); + return Task.CompletedTask; + } + } + else { - // Look in route data for a ODataParameterValue. - object valueAsObject = null; - if (!bindingContext.HttpContext.Request.ODataFeature().RoutingConventionsStore.TryGetValue(modelName, out valueAsObject)) + // If not in the route data, ask the value provider. + valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); + if (valueProviderResult == ValueProviderResult.None) { - bindingContext.ActionContext.RouteData.Values.TryGetValue(modelName, out valueAsObject); + valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); } - if (valueAsObject != null) + if (valueProviderResult != ValueProviderResult.None) { - StringValues stringValues = new StringValues(valueAsObject.ToString()); - valueProviderResult = new ValueProviderResult(stringValues); bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); - ODataParameterValue paramValue = valueAsObject as ODataParameterValue; - if (paramValue != null) + HttpRequest request = bindingContext.HttpContext.Request; + TimeZoneInfo timeZone = request.GetTimeZoneInfo(); + IEdmModel edmModel = request.GetModel(); + object model = ODataModelBinderConverter.ConvertTo(valueProviderResult.FirstValue, bindingContext.ModelType, timeZone, edmModel); + if (model != null) { - HttpRequest request = bindingContext.HttpContext.Request; - object model = ConvertTo(paramValue, bindingContext, request.GetRouteServices()); bindingContext.Result = ModelBindingResult.Success(model); return Task.CompletedTask; } } - else - { - // If not in the route data, ask the value provider. - valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); - if (valueProviderResult == ValueProviderResult.None) - { - valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); - } - - if (valueProviderResult != ValueProviderResult.None) - { - bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); - - HttpRequest request = bindingContext.HttpContext.Request; - TimeZoneInfo timeZone = request.GetTimeZoneInfo(); - IEdmModel edmModel = request.GetModel(); - object model = ODataModelBinderConverter.ConvertTo(valueProviderResult.FirstValue, bindingContext.ModelType, timeZone, edmModel); - if (model != null) - { - bindingContext.Result = ModelBindingResult.Success(model); - return Task.CompletedTask; - } - } - } - - // No matches, binding failed. - bindingContext.Result = ModelBindingResult.Failed(); - } - catch (ODataException ex) - { - bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message); - bindingContext.Result = ModelBindingResult.Failed(); - } - catch (ValidationException ex) - { - bindingContext.ModelState.AddModelError(bindingContext.ModelName, Error.Format(SRResources.ValueIsInvalid, valueProviderResult.FirstValue, ex.Message)); - bindingContext.Result = ModelBindingResult.Failed(); - } - catch (FormatException ex) - { - bindingContext.ModelState.AddModelError(bindingContext.ModelName, Error.Format(SRResources.ValueIsInvalid, valueProviderResult.FirstValue, ex.Message)); - bindingContext.Result = ModelBindingResult.Failed(); - } - catch (Exception e) - { - bindingContext.ModelState.AddModelError(bindingContext.ModelName, e.Message); - bindingContext.Result = ModelBindingResult.Failed(); } - return Task.CompletedTask; + // No matches, binding failed. + bindingContext.Result = ModelBindingResult.Failed(); } - - internal static object ConvertTo(ODataParameterValue parameterValue, ModelBindingContext bindingContext, IServiceProvider requestContainer) + catch (ODataException ex) + { + bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message); + bindingContext.Result = ModelBindingResult.Failed(); + } + catch (ValidationException ex) + { + bindingContext.ModelState.AddModelError(bindingContext.ModelName, Error.Format(SRResources.ValueIsInvalid, valueProviderResult.FirstValue, ex.Message)); + bindingContext.Result = ModelBindingResult.Failed(); + } + catch (FormatException ex) { - Contract.Assert(parameterValue != null && parameterValue.EdmType != null); + bindingContext.ModelState.AddModelError(bindingContext.ModelName, Error.Format(SRResources.ValueIsInvalid, valueProviderResult.FirstValue, ex.Message)); + bindingContext.Result = ModelBindingResult.Failed(); + } + catch (Exception e) + { + bindingContext.ModelState.AddModelError(bindingContext.ModelName, e.Message); + bindingContext.Result = ModelBindingResult.Failed(); + } - object oDataValue = parameterValue.Value; - if (oDataValue == null || oDataValue is ODataNullValue) - { - return null; - } + return Task.CompletedTask; + } - IEdmTypeReference edmTypeReference = parameterValue.EdmType; - ODataDeserializerContext readContext = BuildDeserializerContext(bindingContext, edmTypeReference); - return ODataModelBinderConverter.Convert(oDataValue, edmTypeReference, bindingContext.ModelType, - bindingContext.ModelName, readContext, requestContainer); - } + internal static object ConvertTo(ODataParameterValue parameterValue, ModelBindingContext bindingContext, IServiceProvider requestContainer) + { + Contract.Assert(parameterValue != null && parameterValue.EdmType != null); - internal static ODataDeserializerContext BuildDeserializerContext(ModelBindingContext bindingContext, IEdmTypeReference edmTypeReference) + object oDataValue = parameterValue.Value; + if (oDataValue == null || oDataValue is ODataNullValue) { - HttpRequest request = bindingContext.HttpContext.Request; - ODataPath path = request.ODataFeature().Path; - IEdmModel edmModel = request.GetModel(); + return null; + } - TimeZoneInfo timeZone = null; - IOptions odataOptions = request.HttpContext.RequestServices.GetService>(); - if (odataOptions != null && odataOptions.Value != null) - { - timeZone = odataOptions.Value.TimeZone; - } + IEdmTypeReference edmTypeReference = parameterValue.EdmType; + ODataDeserializerContext readContext = BuildDeserializerContext(bindingContext, edmTypeReference); + return ODataModelBinderConverter.Convert(oDataValue, edmTypeReference, bindingContext.ModelType, + bindingContext.ModelName, readContext, requestContainer); + } - return new ODataDeserializerContext - { - Path = path, - Model = edmModel, - Request = request, - ResourceType = bindingContext.ModelType, - ResourceEdmType = edmTypeReference, - TimeZone = timeZone - }; + internal static ODataDeserializerContext BuildDeserializerContext(ModelBindingContext bindingContext, IEdmTypeReference edmTypeReference) + { + HttpRequest request = bindingContext.HttpContext.Request; + ODataPath path = request.ODataFeature().Path; + IEdmModel edmModel = request.GetModel(); + + TimeZoneInfo timeZone = null; + IOptions odataOptions = request.HttpContext.RequestServices.GetService>(); + if (odataOptions != null && odataOptions.Value != null) + { + timeZone = odataOptions.Value.TimeZone; } + + return new ODataDeserializerContext + { + Path = path, + Model = edmModel, + Request = request, + ResourceType = bindingContext.ModelType, + ResourceEdmType = edmTypeReference, + TimeZone = timeZone + }; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataModelBinderConverter.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataModelBinderConverter.cs index 74ac86df1..3ffa218ee 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataModelBinderConverter.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataModelBinderConverter.cs @@ -28,312 +28,311 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// Expose functionality to convert an function parameter value into a CLR object. +/// +internal static class ODataModelBinderConverter { + // .NET 6 adds a new overload: TryParse(ReadOnlySpan, TEnum) + // Now, with `TryParse(String, TEnum)`, there will have two versions with two parameters + // So, the previous Single() will throw exception. + private static readonly MethodInfo EnumTryParseMethod = typeof(Enum).GetMethod("TryParse", + new[] + { + typeof(string), + Type.MakeGenericMethodParameter(0).MakeByRefType() + }); + + private static readonly MethodInfo CastMethodInfo = typeof(Enumerable).GetMethod("Cast"); + /// - /// Expose functionality to convert an function parameter value into a CLR object. + /// Convert an OData value into a CLR object. /// - internal static class ODataModelBinderConverter + /// The given object. + /// The EDM type of the given object. + /// The CLR type of the given object. + /// The parameter name of the given object. + /// The use to convert. + /// The dependency injection container for the request. + /// The converted object. + public static object Convert(object graph, IEdmTypeReference edmTypeReference, + Type clrType, string parameterName, ODataDeserializerContext readContext, + IServiceProvider requestContainer) { - // .NET 6 adds a new overload: TryParse(ReadOnlySpan, TEnum) - // Now, with `TryParse(String, TEnum)`, there will have two versions with two parameters - // So, the previous Single() will throw exception. - private static readonly MethodInfo EnumTryParseMethod = typeof(Enum).GetMethod("TryParse", - new[] - { - typeof(string), - Type.MakeGenericMethodParameter(0).MakeByRefType() - }); - - private static readonly MethodInfo CastMethodInfo = typeof(Enumerable).GetMethod("Cast"); - - /// - /// Convert an OData value into a CLR object. - /// - /// The given object. - /// The EDM type of the given object. - /// The CLR type of the given object. - /// The parameter name of the given object. - /// The use to convert. - /// The dependency injection container for the request. - /// The converted object. - public static object Convert(object graph, IEdmTypeReference edmTypeReference, - Type clrType, string parameterName, ODataDeserializerContext readContext, - IServiceProvider requestContainer) + if (graph == null || graph is ODataNullValue) { - if (graph == null || graph is ODataNullValue) - { - return null; - } - - // collection of primitive, enum - ODataCollectionValue collectionValue = graph as ODataCollectionValue; - if (collectionValue != null) - { - return ConvertCollection(collectionValue, edmTypeReference, clrType, parameterName, readContext, - requestContainer); - } - - // enum value - ODataEnumValue enumValue = graph as ODataEnumValue; - if (enumValue != null) - { - IEdmEnumTypeReference edmEnumType = edmTypeReference.AsEnum(); - Contract.Assert(edmEnumType != null); + return null; + } - IODataDeserializerProvider deserializerProvider = - requestContainer.GetRequiredService(); + // collection of primitive, enum + ODataCollectionValue collectionValue = graph as ODataCollectionValue; + if (collectionValue != null) + { + return ConvertCollection(collectionValue, edmTypeReference, clrType, parameterName, readContext, + requestContainer); + } - ODataEnumDeserializer deserializer = - (ODataEnumDeserializer)deserializerProvider.GetEdmTypeDeserializer(edmEnumType); + // enum value + ODataEnumValue enumValue = graph as ODataEnumValue; + if (enumValue != null) + { + IEdmEnumTypeReference edmEnumType = edmTypeReference.AsEnum(); + Contract.Assert(edmEnumType != null); - return deserializer.ReadInline(enumValue, edmEnumType, readContext); - } + IODataDeserializerProvider deserializerProvider = + requestContainer.GetRequiredService(); - // primitive value - if (edmTypeReference.IsPrimitive() || edmTypeReference.IsTypeDefinition()) - { - ConstantNode node = graph as ConstantNode; - return EdmPrimitiveHelper.ConvertPrimitiveValue(node != null ? node.Value : graph, clrType, readContext?.TimeZone); - } + ODataEnumDeserializer deserializer = + (ODataEnumDeserializer)deserializerProvider.GetEdmTypeDeserializer(edmEnumType); - // Resource, ResourceSet, Entity Reference or collection of entity reference - return ConvertResourceOrResourceSet(graph, edmTypeReference, readContext); + return deserializer.ReadInline(enumValue, edmEnumType, readContext); } - internal static object ConvertTo(string valueString, Type type, TimeZoneInfo timeZone, IEdmModel edmModel = null) + // primitive value + if (edmTypeReference.IsPrimitive() || edmTypeReference.IsTypeDefinition()) { - if (valueString == null) - { - return null; - } - - if (TypeHelper.IsNullable(type) && String.Equals(valueString, "null", StringComparison.Ordinal)) - { - return null; - } - - // TODO 1668: ODL beta1's ODataUriUtils.ConvertFromUriLiteral does not support converting uri literal - // to ODataEnumValue, but beta1's ODataUriUtils.ConvertToUriLiteral supports converting ODataEnumValue - // to uri literal. - if (TypeHelper.IsEnum(type)) - { - string[] values = valueString.Split(new[] { '\'' }, StringSplitOptions.None); - if (values.Length == 3 && String.IsNullOrEmpty(values[2])) - { - // Remove the type name if the enum value is a fully qualified literal. - valueString = values[1]; - } - - Type enumType = TypeHelper.GetUnderlyingTypeOrSelf(type); - object[] parameters = new[] { valueString, Enum.ToObject(enumType, 0) }; - bool isSuccessful = (bool)EnumTryParseMethod.MakeGenericMethod(enumType).Invoke(null, parameters); + ConstantNode node = graph as ConstantNode; + return EdmPrimitiveHelper.ConvertPrimitiveValue(node != null ? node.Value : graph, clrType, readContext?.TimeZone); + } - if (!isSuccessful) - { - throw Error.InvalidOperation(SRResources.ModelBinderUtil_ValueCannotBeEnum, valueString, type.Name); - } + // Resource, ResourceSet, Entity Reference or collection of entity reference + return ConvertResourceOrResourceSet(graph, edmTypeReference, readContext); + } - return parameters[1]; - } + internal static object ConvertTo(string valueString, Type type, TimeZoneInfo timeZone, IEdmModel edmModel = null) + { + if (valueString == null) + { + return null; + } - // The logic of "public static object ConvertFromUriLiteral(string value, ODataVersion version);" treats - // the date value string (for example: 2015-01-02) as DateTimeOffset literal, and return a DateTimeOffset - // object. However, the logic of - // "object ConvertFromUriLiteral(string value, ODataVersion version, IEdmModel model, IEdmTypeReference typeReference);" - // can return the correct Date object. - if (type == typeof(Date) || type == typeof(Date?)) - { - IEdmModel model = edmModel ?? EdmCoreModel.Instance; - IEdmPrimitiveTypeReference dateTypeReference = model.GetEdmPrimitiveTypeReference(type); - return ODataUriUtils.ConvertFromUriLiteral(valueString, ODataVersion.V4, model, dateTypeReference); - } + if (TypeHelper.IsNullable(type) && String.Equals(valueString, "null", StringComparison.Ordinal)) + { + return null; + } - object value; - try - { - value = ODataUriUtils.ConvertFromUriLiteral(valueString, ODataVersion.V4); - } - catch + // TODO 1668: ODL beta1's ODataUriUtils.ConvertFromUriLiteral does not support converting uri literal + // to ODataEnumValue, but beta1's ODataUriUtils.ConvertToUriLiteral supports converting ODataEnumValue + // to uri literal. + if (TypeHelper.IsEnum(type)) + { + string[] values = valueString.Split(new[] { '\'' }, StringSplitOptions.None); + if (values.Length == 3 && String.IsNullOrEmpty(values[2])) { - if (type == typeof(string)) - { - return valueString; - } - - throw; + // Remove the type name if the enum value is a fully qualified literal. + valueString = values[1]; } - bool isNonStandardEdmPrimitive; - edmModel.IsNonstandardEdmPrimitive(type, out isNonStandardEdmPrimitive); + Type enumType = TypeHelper.GetUnderlyingTypeOrSelf(type); + object[] parameters = new[] { valueString, Enum.ToObject(enumType, 0) }; + bool isSuccessful = (bool)EnumTryParseMethod.MakeGenericMethod(enumType).Invoke(null, parameters); - if (isNonStandardEdmPrimitive) + if (!isSuccessful) { - // shall we get the timezone? - return EdmPrimitiveHelper.ConvertPrimitiveValue(value, type, timeZone); - } - else - { - type = Nullable.GetUnderlyingType(type) ?? type; - return System.Convert.ChangeType(value, type, CultureInfo.InvariantCulture); + throw Error.InvalidOperation(SRResources.ModelBinderUtil_ValueCannotBeEnum, valueString, type.Name); } + + return parameters[1]; } - private static object ConvertCollection(ODataCollectionValue collectionValue, - IEdmTypeReference edmTypeReference, Type clrType, string parameterName, - ODataDeserializerContext readContext, IServiceProvider requestContainer) + // The logic of "public static object ConvertFromUriLiteral(string value, ODataVersion version);" treats + // the date value string (for example: 2015-01-02) as DateTimeOffset literal, and return a DateTimeOffset + // object. However, the logic of + // "object ConvertFromUriLiteral(string value, ODataVersion version, IEdmModel model, IEdmTypeReference typeReference);" + // can return the correct Date object. + if (type == typeof(Date) || type == typeof(Date?)) { - Contract.Assert(collectionValue != null); - - IEdmCollectionTypeReference collectionType = edmTypeReference as IEdmCollectionTypeReference; - Contract.Assert(collectionType != null); - - IODataDeserializerProvider deserializerProvider = - requestContainer.GetRequiredService(); - ODataCollectionDeserializer deserializer = - (ODataCollectionDeserializer)deserializerProvider.GetEdmTypeDeserializer(collectionType); + IEdmModel model = edmModel ?? EdmCoreModel.Instance; + IEdmPrimitiveTypeReference dateTypeReference = model.GetEdmPrimitiveTypeReference(type); + return ODataUriUtils.ConvertFromUriLiteral(valueString, ODataVersion.V4, model, dateTypeReference); + } - object value = deserializer.ReadInline(collectionValue, collectionType, readContext); - if (value == null) + object value; + try + { + value = ODataUriUtils.ConvertFromUriLiteral(valueString, ODataVersion.V4); + } + catch + { + if (type == typeof(string)) { - return null; + return valueString; } - IEnumerable collection = value as IEnumerable; - Contract.Assert(collection != null); + throw; + } - Type elementType; - if (!TypeHelper.IsCollection(clrType, out elementType)) - { - // EdmEntityCollectionObject and EdmComplexCollectionObject are collection types. - throw new ODataException(String.Format(CultureInfo.InvariantCulture, - SRResources.ParameterTypeIsNotCollection, parameterName, clrType)); - } + bool isNonStandardEdmPrimitive; + edmModel.IsNonstandardEdmPrimitive(type, out isNonStandardEdmPrimitive); - IEnumerable newCollection; - if (CollectionDeserializationHelpers.TryCreateInstance(clrType, collectionType, elementType, - out newCollection)) - { - collection.AddToCollection(newCollection, elementType, parameterName, clrType); - if (clrType.IsArray) - { - newCollection = CollectionDeserializationHelpers.ToArray(newCollection, elementType); - } + if (isNonStandardEdmPrimitive) + { + // shall we get the timezone? + return EdmPrimitiveHelper.ConvertPrimitiveValue(value, type, timeZone); + } + else + { + type = Nullable.GetUnderlyingType(type) ?? type; + return System.Convert.ChangeType(value, type, CultureInfo.InvariantCulture); + } + } - return newCollection; - } + private static object ConvertCollection(ODataCollectionValue collectionValue, + IEdmTypeReference edmTypeReference, Type clrType, string parameterName, + ODataDeserializerContext readContext, IServiceProvider requestContainer) + { + Contract.Assert(collectionValue != null); + + IEdmCollectionTypeReference collectionType = edmTypeReference as IEdmCollectionTypeReference; + Contract.Assert(collectionType != null); + + IODataDeserializerProvider deserializerProvider = + requestContainer.GetRequiredService(); + ODataCollectionDeserializer deserializer = + (ODataCollectionDeserializer)deserializerProvider.GetEdmTypeDeserializer(collectionType); + object value = deserializer.ReadInline(collectionValue, collectionType, readContext); + if (value == null) + { return null; } - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "")] - internal static object ConvertResourceOrResourceSet(object oDataValue, IEdmTypeReference edmTypeReference, - ODataDeserializerContext readContext) + IEnumerable collection = value as IEnumerable; + Contract.Assert(collection != null); + + Type elementType; + if (!TypeHelper.IsCollection(clrType, out elementType)) { - string valueString = oDataValue as string; - Contract.Assert(valueString != null); + // EdmEntityCollectionObject and EdmComplexCollectionObject are collection types. + throw new ODataException(String.Format(CultureInfo.InvariantCulture, + SRResources.ParameterTypeIsNotCollection, parameterName, clrType)); + } - if (edmTypeReference.IsNullable && String.Equals(valueString, "null", StringComparison.Ordinal)) + IEnumerable newCollection; + if (CollectionDeserializationHelpers.TryCreateInstance(clrType, collectionType, elementType, + out newCollection)) + { + collection.AddToCollection(newCollection, elementType, parameterName, clrType); + if (clrType.IsArray) { - return null; + newCollection = CollectionDeserializationHelpers.ToArray(newCollection, elementType); } - HttpRequest request = readContext.Request; - ODataMessageReaderSettings oDataReaderSettings = request.GetReaderSettings(); - - using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(valueString))) - { - stream.Seek(0, SeekOrigin.Begin); - - // Do we need to dispose it? - IODataRequestMessage oDataRequestMessage = new ODataMessageWrapper(stream, null, request.GetODataContentIdMapping()); - using ( - ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, - oDataReaderSettings, readContext.Model)) - { - if (edmTypeReference.IsCollection()) - { - return ConvertResourceSet(oDataMessageReader, edmTypeReference, readContext); - } - else - { - return ConvertResource(oDataMessageReader, edmTypeReference, readContext); - } - } - } + return newCollection; } - private static object ConvertResourceSet(ODataMessageReader oDataMessageReader, - IEdmTypeReference edmTypeReference, ODataDeserializerContext readContext) - { - IEdmCollectionTypeReference collectionType = edmTypeReference.AsCollection(); + return null; + } - EdmEntitySet tempEntitySet = null; - if (collectionType.ElementType().IsEntity()) - { - tempEntitySet = new EdmEntitySet(readContext.Model.EntityContainer, "temp", - collectionType.ElementType().AsEntity().EntityDefinition()); - } + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "")] + internal static object ConvertResourceOrResourceSet(object oDataValue, IEdmTypeReference edmTypeReference, + ODataDeserializerContext readContext) + { + string valueString = oDataValue as string; + Contract.Assert(valueString != null); - // TODO: Sam xu, can we use the parameter-less overload - ODataReader odataReader = oDataMessageReader.CreateODataUriParameterResourceSetReader(tempEntitySet, - collectionType.ElementType().AsStructured().StructuredDefinition()); - ODataResourceSetWrapper resourceSet = - odataReader.ReadResourceOrResourceSet() as ODataResourceSetWrapper; + if (edmTypeReference.IsNullable && String.Equals(valueString, "null", StringComparison.Ordinal)) + { + return null; + } - IODataDeserializerProvider deserializerProvider = readContext.Request.GetDeserializerProvider(); + HttpRequest request = readContext.Request; + ODataMessageReaderSettings oDataReaderSettings = request.GetReaderSettings(); - ODataResourceSetDeserializer resourceSetDeserializer = - (ODataResourceSetDeserializer)deserializerProvider.GetEdmTypeDeserializer(collectionType); + using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(valueString))) + { + stream.Seek(0, SeekOrigin.Begin); - object result = resourceSetDeserializer.ReadInline(resourceSet, collectionType, readContext); - IEnumerable enumerable = result as IEnumerable; - if (enumerable != null) + // Do we need to dispose it? + IODataRequestMessage oDataRequestMessage = new ODataMessageWrapper(stream, null, request.GetODataContentIdMapping()); + using ( + ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, + oDataReaderSettings, readContext.Model)) { - if (readContext.IsNoClrType) + if (edmTypeReference.IsCollection()) { - return enumerable.ConvertToEdmObject(collectionType); + return ConvertResourceSet(oDataMessageReader, edmTypeReference, readContext); } else { - IEdmTypeReference elementTypeReference = collectionType.ElementType(); - - Type elementClrType = readContext.Model.GetClrType(elementTypeReference); - IEnumerable castedResult = - CastMethodInfo.MakeGenericMethod(elementClrType) - .Invoke(null, new object[] { enumerable }) as IEnumerable; - return castedResult; + return ConvertResource(oDataMessageReader, edmTypeReference, readContext); } } + } + } - return null; + private static object ConvertResourceSet(ODataMessageReader oDataMessageReader, + IEdmTypeReference edmTypeReference, ODataDeserializerContext readContext) + { + IEdmCollectionTypeReference collectionType = edmTypeReference.AsCollection(); + + EdmEntitySet tempEntitySet = null; + if (collectionType.ElementType().IsEntity()) + { + tempEntitySet = new EdmEntitySet(readContext.Model.EntityContainer, "temp", + collectionType.ElementType().AsEntity().EntityDefinition()); } - private static object ConvertResource(ODataMessageReader oDataMessageReader, IEdmTypeReference edmTypeReference, - ODataDeserializerContext readContext) + // TODO: Sam xu, can we use the parameter-less overload + ODataReader odataReader = oDataMessageReader.CreateODataUriParameterResourceSetReader(tempEntitySet, + collectionType.ElementType().AsStructured().StructuredDefinition()); + ODataResourceSetWrapper resourceSet = + odataReader.ReadResourceOrResourceSet() as ODataResourceSetWrapper; + + IODataDeserializerProvider deserializerProvider = readContext.Request.GetDeserializerProvider(); + + ODataResourceSetDeserializer resourceSetDeserializer = + (ODataResourceSetDeserializer)deserializerProvider.GetEdmTypeDeserializer(collectionType); + + object result = resourceSetDeserializer.ReadInline(resourceSet, collectionType, readContext); + IEnumerable enumerable = result as IEnumerable; + if (enumerable != null) { - EdmEntitySet tempEntitySet = null; - if (edmTypeReference.IsEntity()) + if (readContext.IsNoClrType) { - IEdmEntityTypeReference entityType = edmTypeReference.AsEntity(); - tempEntitySet = new EdmEntitySet(readContext.Model.EntityContainer, "temp", - entityType.EntityDefinition()); + return enumerable.ConvertToEdmObject(collectionType); } + else + { + IEdmTypeReference elementTypeReference = collectionType.ElementType(); - // TODO: Sam xu, can we use the parameter-less overload - ODataReader resourceReader = oDataMessageReader.CreateODataUriParameterResourceReader(tempEntitySet, - edmTypeReference.ToStructuredType()); + Type elementClrType = readContext.Model.GetClrType(elementTypeReference); + IEnumerable castedResult = + CastMethodInfo.MakeGenericMethod(elementClrType) + .Invoke(null, new object[] { enumerable }) as IEnumerable; + return castedResult; + } + } - object item = resourceReader.ReadResourceOrResourceSet(); + return null; + } - ODataResourceWrapper topLevelResource = item as ODataResourceWrapper; - Contract.Assert(topLevelResource != null); + private static object ConvertResource(ODataMessageReader oDataMessageReader, IEdmTypeReference edmTypeReference, + ODataDeserializerContext readContext) + { + EdmEntitySet tempEntitySet = null; + if (edmTypeReference.IsEntity()) + { + IEdmEntityTypeReference entityType = edmTypeReference.AsEntity(); + tempEntitySet = new EdmEntitySet(readContext.Model.EntityContainer, "temp", + entityType.EntityDefinition()); + } - IODataDeserializerProvider deserializerProvider = readContext.Request.GetDeserializerProvider(); + // TODO: Sam xu, can we use the parameter-less overload + ODataReader resourceReader = oDataMessageReader.CreateODataUriParameterResourceReader(tempEntitySet, + edmTypeReference.ToStructuredType()); - ODataResourceDeserializer entityDeserializer = - (ODataResourceDeserializer)deserializerProvider.GetEdmTypeDeserializer(edmTypeReference); - return entityDeserializer.ReadInline(topLevelResource, edmTypeReference, readContext); - } + object item = resourceReader.ReadResourceOrResourceSet(); + + ODataResourceWrapper topLevelResource = item as ODataResourceWrapper; + Contract.Assert(topLevelResource != null); + + IODataDeserializerProvider deserializerProvider = readContext.Request.GetDeserializerProvider(); + + ODataResourceDeserializer entityDeserializer = + (ODataResourceDeserializer)deserializerProvider.GetEdmTypeDeserializer(edmTypeReference); + return entityDeserializer.ReadInline(topLevelResource, edmTypeReference, readContext); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatter.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatter.cs index d9255bfed..c51d2acd1 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatter.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatter.cs @@ -27,406 +27,405 @@ using Microsoft.Net.Http.Headers; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// class to handle OData. +/// +public class ODataOutputFormatter : TextOutputFormatter, IMediaTypeMappingCollection { /// - /// class to handle OData. + /// The set of payload kinds this formatter will accept in CanWriteType. /// - public class ODataOutputFormatter : TextOutputFormatter, IMediaTypeMappingCollection - { - /// - /// The set of payload kinds this formatter will accept in CanWriteType. - /// - private readonly ISet _payloadKinds; - - /// - /// Initializes a new instance of the class. - /// - /// The kind of payloads this formatter supports. - public ODataOutputFormatter(IEnumerable payloadKinds) - { - if (payloadKinds == null) - { - throw Error.ArgumentNull(nameof(payloadKinds)); - } + private readonly ISet _payloadKinds; - _payloadKinds = new HashSet(payloadKinds); + /// + /// Initializes a new instance of the class. + /// + /// The kind of payloads this formatter supports. + public ODataOutputFormatter(IEnumerable payloadKinds) + { + if (payloadKinds == null) + { + throw Error.ArgumentNull(nameof(payloadKinds)); } - /// - /// Gets or sets a method that allows consumers to provide an alternate base - /// address for OData Uri. - /// - public Func BaseAddressFactory { get; set; } + _payloadKinds = new HashSet(payloadKinds); + } - /// - /// Gets a collection of objects. - /// - public ICollection MediaTypeMappings { get; } = new List(); + /// + /// Gets or sets a method that allows consumers to provide an alternate base + /// address for OData Uri. + /// + public Func BaseAddressFactory { get; set; } - /// - public override IReadOnlyList GetSupportedContentTypes(string contentType, Type objectType) - { - if (SupportedMediaTypes.Count == 0) - { - // note: this is parity with the base implementation when there are no matches - return default; - } + /// + /// Gets a collection of objects. + /// + public ICollection MediaTypeMappings { get; } = new List(); - return base.GetSupportedContentTypes(contentType, objectType); + /// + public override IReadOnlyList GetSupportedContentTypes(string contentType, Type objectType) + { + if (SupportedMediaTypes.Count == 0) + { + // note: this is parity with the base implementation when there are no matches + return default; } - /// - public override bool CanWriteResult(OutputFormatterCanWriteContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + return base.GetSupportedContentTypes(contentType, objectType); + } - // Ensure we have a valid request. - HttpRequest request = context.HttpContext.Request; - if (request == null) - { - throw Error.InvalidOperation(SRResources.WriteToResponseAsyncMustHaveRequest); - } + /// + public override bool CanWriteResult(OutputFormatterCanWriteContext context) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - // Ignore non-OData requests. - if (request.ODataFeature().Path == null) - { - return false; - } + // Ensure we have a valid request. + HttpRequest request = context.HttpContext.Request; + if (request == null) + { + throw Error.InvalidOperation(SRResources.WriteToResponseAsyncMustHaveRequest); + } - // Be noted: Before coming here (.NET 5), the ContentType is reset as empty as: - // formatterContext.ContentType = new StringSegment(); + // Ignore non-OData requests. + if (request.ODataFeature().Path == null) + { + return false; + } - // Allow the base class to make its determination, which includes - // checks for SupportedMediaTypes. - bool suportedMediaTypeFound = false; - if (SupportedMediaTypes.Any()) - { - suportedMediaTypeFound = base.CanWriteResult(context); - } + // Be noted: Before coming here (.NET 5), the ContentType is reset as empty as: + // formatterContext.ContentType = new StringSegment(); - // See if the request satisfies any mappings. - IEnumerable matchedMappings = (MediaTypeMappings == null) ? null : MediaTypeMappings - .Where(m => m.TryMatchMediaType(request) > 0); + // Allow the base class to make its determination, which includes + // checks for SupportedMediaTypes. + bool suportedMediaTypeFound = false; + if (SupportedMediaTypes.Any()) + { + suportedMediaTypeFound = base.CanWriteResult(context); + } - // Now pick the best content type. If a media mapping was found, use that and override the - // value specified by the controller, if any. Otherwise, let the base class decide. - if (matchedMappings != null && matchedMappings.Any()) - { - context.ContentType = matchedMappings.First().MediaType.ToString(); - } - else if (!suportedMediaTypeFound) - { - return false; - } + // See if the request satisfies any mappings. + IEnumerable matchedMappings = (MediaTypeMappings == null) ? null : MediaTypeMappings + .Where(m => m.TryMatchMediaType(request) > 0); - // We need the type in order to write it. - Type type = context.ObjectType ?? context.Object?.GetType(); - if (type == null) - { - return false; - } - type = TypeHelper.GetTaskInnerTypeOrSelf(type); + // Now pick the best content type. If a media mapping was found, use that and override the + // value specified by the controller, if any. Otherwise, let the base class decide. + if (matchedMappings != null && matchedMappings.Any()) + { + context.ContentType = matchedMappings.First().MediaType.ToString(); + } + else if (!suportedMediaTypeFound) + { + return false; + } - IODataSerializerProvider serializerProvider = request.GetRouteServices().GetRequiredService(); + // We need the type in order to write it. + Type type = context.ObjectType ?? context.Object?.GetType(); + if (type == null) + { + return false; + } + type = TypeHelper.GetTaskInnerTypeOrSelf(type); - // See if this type is a SingleResult or is derived from SingleResult. - bool isSingleResult = false; - if (type.IsGenericType) - { - Type genericType = type.GetGenericTypeDefinition(); - Type baseType = type.BaseType; - isSingleResult = (genericType == typeof(SingleResult<>) || baseType == typeof(SingleResult)); - } + IODataSerializerProvider serializerProvider = request.GetRouteServices().GetRequiredService(); - ODataPayloadKind? payloadKind; + // See if this type is a SingleResult or is derived from SingleResult. + bool isSingleResult = false; + if (type.IsGenericType) + { + Type genericType = type.GetGenericTypeDefinition(); + Type baseType = type.BaseType; + isSingleResult = (genericType == typeof(SingleResult<>) || baseType == typeof(SingleResult)); + } - Type elementType; - if (typeof(IEdmObject).IsAssignableFrom(type) || - (TypeHelper.IsCollection(type, out elementType) && typeof(IEdmObject).IsAssignableFrom(elementType))) - { - payloadKind = GetEdmObjectPayloadKind(type, request); - } - else - { - payloadKind = GetClrObjectResponsePayloadKind(type, isSingleResult, serializerProvider, request); - } + ODataPayloadKind? payloadKind; - return payloadKind == null ? false : _payloadKinds.Contains(payloadKind.Value); + Type elementType; + if (typeof(IEdmObject).IsAssignableFrom(type) || + (TypeHelper.IsCollection(type, out elementType) && typeof(IEdmObject).IsAssignableFrom(elementType))) + { + payloadKind = GetEdmObjectPayloadKind(type, request); } - - /// - public override void WriteResponseHeaders(OutputFormatterWriteContext context) + else { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + payloadKind = GetClrObjectResponsePayloadKind(type, isSingleResult, serializerProvider, request); + } - Type type = context.ObjectType; - if (type == null) - { - throw Error.ArgumentNull(nameof(type)); - } - type = TypeHelper.GetTaskInnerTypeOrSelf(type); + return payloadKind == null ? false : _payloadKinds.Contains(payloadKind.Value); + } - HttpRequest request = context.HttpContext.Request; - if (request == null) - { - throw Error.InvalidOperation(SRResources.WriteToResponseAsyncMustHaveRequest); - } + /// + public override void WriteResponseHeaders(OutputFormatterWriteContext context) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - HttpResponse response = context.HttpContext.Response; - response.ContentType = context.ContentType.Value; + Type type = context.ObjectType; + if (type == null) + { + throw Error.ArgumentNull(nameof(type)); + } + type = TypeHelper.GetTaskInnerTypeOrSelf(type); - MediaTypeHeaderValue contentType = GetContentType(response.Headers[HeaderNames.ContentType].FirstOrDefault()); + HttpRequest request = context.HttpContext.Request; + if (request == null) + { + throw Error.InvalidOperation(SRResources.WriteToResponseAsyncMustHaveRequest); + } - // Determine the content type. - MediaTypeHeaderValue newMediaType = null; - if (TryGetContentHeader(type, contentType, out newMediaType)) - { - response.Headers[HeaderNames.ContentType] = new StringValues(newMediaType.ToString()); - } + HttpResponse response = context.HttpContext.Response; + response.ContentType = context.ContentType.Value; - // Set the character set. - MediaTypeHeaderValue currentContentType = GetContentType(response.Headers[HeaderNames.ContentType].FirstOrDefault()); - RequestHeaders requestHeader = request.GetTypedHeaders(); - // Starting from ASP .NET Core 3.0 AcceptCharset returns an empty collection instead of null. - if (requestHeader?.AcceptCharset.Count > 0) - { - IEnumerable acceptCharsetValues = requestHeader.AcceptCharset.Select(cs => cs.Value.Value); - - string newCharSet = string.Empty; - if (TryGetCharSet(currentContentType, acceptCharsetValues, out newCharSet)) - { - currentContentType.Charset = new StringSegment(newCharSet); - response.Headers[HeaderNames.ContentType] = new StringValues(currentContentType.ToString()); - } - } + MediaTypeHeaderValue contentType = GetContentType(response.Headers[HeaderNames.ContentType].FirstOrDefault()); - // Add version header. - response.Headers["OData-Version"] = ODataUtils.ODataVersionToString(request.GetODataVersion()); + // Determine the content type. + MediaTypeHeaderValue newMediaType = null; + if (TryGetContentHeader(type, contentType, out newMediaType)) + { + response.Headers[HeaderNames.ContentType] = new StringValues(newMediaType.ToString()); } - /// - public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) + // Set the character set. + MediaTypeHeaderValue currentContentType = GetContentType(response.Headers[HeaderNames.ContentType].FirstOrDefault()); + RequestHeaders requestHeader = request.GetTypedHeaders(); + // Starting from ASP .NET Core 3.0 AcceptCharset returns an empty collection instead of null. + if (requestHeader?.AcceptCharset.Count > 0) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - Type type = context.ObjectType; - if (type == null) - { - throw Error.ArgumentNull(nameof(type)); - } - type = TypeHelper.GetTaskInnerTypeOrSelf(type); + IEnumerable acceptCharsetValues = requestHeader.AcceptCharset.Select(cs => cs.Value.Value); - HttpRequest request = context.HttpContext.Request; - if (request == null) + string newCharSet = string.Empty; + if (TryGetCharSet(currentContentType, acceptCharsetValues, out newCharSet)) { - throw Error.InvalidOperation(SRResources.WriteToResponseAsyncMustHaveRequest); + currentContentType.Charset = new StringSegment(newCharSet); + response.Headers[HeaderNames.ContentType] = new StringValues(currentContentType.ToString()); } + } - HttpResponse response = context.HttpContext.Response; - if (typeof(Stream).IsAssignableFrom(type)) - { - // Ideally, it should go into the "ODataRawValueSerializer" - // However, OData lib doesn't provide the method to overwrite/copyto stream - // So, Here's the workaround - Stream objStream = context.Object as Stream; - return CopyStreamAsync(objStream, response); - } + // Add version header. + response.Headers["OData-Version"] = ODataUtils.ODataVersionToString(request.GetODataVersion()); + } - Uri baseAddress = GetBaseAddressInternal(request); - MediaTypeHeaderValue contentType = GetContentType(response.Headers[HeaderNames.ContentType].FirstOrDefault()); + /// + public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - IODataSerializerProvider serializerProvider = request.GetRouteServices().GetRequiredService(); + Type type = context.ObjectType; + if (type == null) + { + throw Error.ArgumentNull(nameof(type)); + } + type = TypeHelper.GetTaskInnerTypeOrSelf(type); - return ODataOutputFormatterHelper.WriteToStreamAsync( - type, - context.Object, - request.GetModel(), - request.GetODataVersion(), - baseAddress, - contentType, - request, - request.Headers, - serializerProvider); + HttpRequest request = context.HttpContext.Request; + if (request == null) + { + throw Error.InvalidOperation(SRResources.WriteToResponseAsyncMustHaveRequest); } - /// - /// Returns a base address to be used in the service root when reading or writing OData uris. - /// - /// The HttpRequest object for the given request. - /// The base address to be used as part of the service root in the OData uri; must terminate with a trailing '/'. - public static Uri GetDefaultBaseAddress(HttpRequest request) + HttpResponse response = context.HttpContext.Response; + if (typeof(Stream).IsAssignableFrom(type)) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + // Ideally, it should go into the "ODataRawValueSerializer" + // However, OData lib doesn't provide the method to overwrite/copyto stream + // So, Here's the workaround + Stream objStream = context.Object as Stream; + return CopyStreamAsync(objStream, response); + } - string baseAddress = request.CreateODataLink(); - if (baseAddress == null) - { - throw new SerializationException(SRResources.UnableToDetermineBaseUrl); - } + Uri baseAddress = GetBaseAddressInternal(request); + MediaTypeHeaderValue contentType = GetContentType(response.Headers[HeaderNames.ContentType].FirstOrDefault()); + + IODataSerializerProvider serializerProvider = request.GetRouteServices().GetRequiredService(); + + return ODataOutputFormatterHelper.WriteToStreamAsync( + type, + context.Object, + request.GetModel(), + request.GetODataVersion(), + baseAddress, + contentType, + request, + request.Headers, + serializerProvider); + } - return baseAddress[baseAddress.Length - 1] != '/' ? new Uri(baseAddress + '/') : new Uri(baseAddress); + /// + /// Returns a base address to be used in the service root when reading or writing OData uris. + /// + /// The HttpRequest object for the given request. + /// The base address to be used as part of the service root in the OData uri; must terminate with a trailing '/'. + public static Uri GetDefaultBaseAddress(HttpRequest request) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); } - internal static bool TryGetCharSet(MediaTypeHeaderValue mediaType, IEnumerable acceptCharsetValues, out string charSet) + string baseAddress = request.CreateODataLink(); + if (baseAddress == null) { - charSet = String.Empty; + throw new SerializationException(SRResources.UnableToDetermineBaseUrl); + } - // In general, in Web API we pick a default charset based on the supported character sets - // of the formatter. However, according to the OData spec, the service shouldn't be sending - // a character set unless explicitly specified, so if the client didn't send the charset we chose - // we just clean it. - if (mediaType != null && - !acceptCharsetValues - .Any(cs => cs.Equals(mediaType.Charset.ToString(), StringComparison.OrdinalIgnoreCase))) - { - return true; - } + return baseAddress[baseAddress.Length - 1] != '/' ? new Uri(baseAddress + '/') : new Uri(baseAddress); + } - return false; + internal static bool TryGetCharSet(MediaTypeHeaderValue mediaType, IEnumerable acceptCharsetValues, out string charSet) + { + charSet = String.Empty; + + // In general, in Web API we pick a default charset based on the supported character sets + // of the formatter. However, according to the OData spec, the service shouldn't be sending + // a character set unless explicitly specified, so if the client didn't send the charset we chose + // we just clean it. + if (mediaType != null && + !acceptCharsetValues + .Any(cs => cs.Equals(mediaType.Charset.ToString(), StringComparison.OrdinalIgnoreCase))) + { + return true; } - [SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "")] - internal static bool TryGetContentHeader(Type type, MediaTypeHeaderValue mediaType, out MediaTypeHeaderValue newMediaType) + return false; + } + + [SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "")] + internal static bool TryGetContentHeader(Type type, MediaTypeHeaderValue mediaType, out MediaTypeHeaderValue newMediaType) + { + if (type == null) { - if (type == null) - { - throw Error.ArgumentNull(nameof(type)); - } + throw Error.ArgumentNull(nameof(type)); + } - newMediaType = null; + newMediaType = null; - // When the user asks for application/json we really need to set the content type to - // application/json; odata.metadata=minimal. If the user provides the media type and is - // application/json we are going to add automatically odata.metadata=minimal. Otherwise we are - // going to fallback to the default implementation. + // When the user asks for application/json we really need to set the content type to + // application/json; odata.metadata=minimal. If the user provides the media type and is + // application/json we are going to add automatically odata.metadata=minimal. Otherwise we are + // going to fallback to the default implementation. - // When calling this formatter as part of content negotiation the content negotiator will always - // pick a non null media type. In case the user creates a new ObjectContent and doesn't pass in a - // media type, we delegate to the base class to rely on the default behavior. It's the user's - // responsibility to pass in the right media type. + // When calling this formatter as part of content negotiation the content negotiator will always + // pick a non null media type. In case the user creates a new ObjectContent and doesn't pass in a + // media type, we delegate to the base class to rely on the default behavior. It's the user's + // responsibility to pass in the right media type. - if (mediaType != null) - { - if (mediaType.MediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase) && - !mediaType.Parameters.Any(p => p.Name.Equals("odata.metadata", StringComparison.OrdinalIgnoreCase))) - { - mediaType.Parameters.Add(new NameValueHeaderValue("odata.metadata", "minimal")); - } - - //newMediaType = (MediaTypeHeaderValue)((ICloneable)mediaType).Clone(); - newMediaType = mediaType.Copy(); - return true; - } - else + if (mediaType != null) + { + if (mediaType.MediaType.Equals("application/json", StringComparison.OrdinalIgnoreCase) && + !mediaType.Parameters.Any(p => p.Name.Equals("odata.metadata", StringComparison.OrdinalIgnoreCase))) { - // This is the case when a user creates a new ObjectContent passing in a null mediaType - return false; + mediaType.Parameters.Add(new NameValueHeaderValue("odata.metadata", "minimal")); } - } - private static ODataPayloadKind? GetClrObjectResponsePayloadKind(Type type, bool isGenericSingleResult, - IODataSerializerProvider serializerProvider, HttpRequest request) + //newMediaType = (MediaTypeHeaderValue)((ICloneable)mediaType).Clone(); + newMediaType = mediaType.Copy(); + return true; + } + else { - // SingleResult should be serialized as T. - if (isGenericSingleResult) - { - type = type.GetGenericArguments()[0]; - } + // This is the case when a user creates a new ObjectContent passing in a null mediaType + return false; + } + } - IODataSerializer serializer = serializerProvider.GetODataPayloadSerializer(type, request); - return serializer == null ? null : (ODataPayloadKind?)serializer.ODataPayloadKind; + private static ODataPayloadKind? GetClrObjectResponsePayloadKind(Type type, bool isGenericSingleResult, + IODataSerializerProvider serializerProvider, HttpRequest request) + { + // SingleResult should be serialized as T. + if (isGenericSingleResult) + { + type = type.GetGenericArguments()[0]; } - /// - /// Internal method used for selecting the base address to be used with OData uris. - /// If the consumer has provided a delegate for overriding our default implementation, - /// we call that, otherwise we default to existing behavior below. - /// - /// The HttpRequest object for the given request. - /// The base address to be used as part of the service root; must terminate with a trailing '/'. - private Uri GetBaseAddressInternal(HttpRequest request) + IODataSerializer serializer = serializerProvider.GetODataPayloadSerializer(type, request); + return serializer == null ? null : (ODataPayloadKind?)serializer.ODataPayloadKind; + } + + /// + /// Internal method used for selecting the base address to be used with OData uris. + /// If the consumer has provided a delegate for overriding our default implementation, + /// we call that, otherwise we default to existing behavior below. + /// + /// The HttpRequest object for the given request. + /// The base address to be used as part of the service root; must terminate with a trailing '/'. + private Uri GetBaseAddressInternal(HttpRequest request) + { + if (BaseAddressFactory != null) { - if (BaseAddressFactory != null) - { - return BaseAddressFactory(request); - } - else - { - return ODataOutputFormatter.GetDefaultBaseAddress(request); - } + return BaseAddressFactory(request); + } + else + { + return ODataOutputFormatter.GetDefaultBaseAddress(request); } + } - [SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "")] - private MediaTypeHeaderValue GetContentType(string contentTypeValue) + [SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "")] + private MediaTypeHeaderValue GetContentType(string contentTypeValue) + { + MediaTypeHeaderValue contentType = null; + if (!string.IsNullOrEmpty(contentTypeValue)) { - MediaTypeHeaderValue contentType = null; - if (!string.IsNullOrEmpty(contentTypeValue)) - { - MediaTypeHeaderValue.TryParse(contentTypeValue, out contentType); - } + MediaTypeHeaderValue.TryParse(contentTypeValue, out contentType); + } - return contentType; + return contentType; + } + + private static ODataPayloadKind? GetEdmObjectPayloadKind(Type type, HttpRequest request) + { + if (request.IsCountRequest()) + { + return ODataPayloadKind.Value; } - private static ODataPayloadKind? GetEdmObjectPayloadKind(Type type, HttpRequest request) + Type elementType; + if (TypeHelper.IsCollection(type, out elementType)) { - if (request.IsCountRequest()) + if (typeof(IEdmComplexObject).IsAssignableFrom(elementType) || typeof(IEdmEnumObject).IsAssignableFrom(elementType)) { - return ODataPayloadKind.Value; + return ODataPayloadKind.Collection; } - - Type elementType; - if (TypeHelper.IsCollection(type, out elementType)) + else if (typeof(IEdmEntityObject).IsAssignableFrom(elementType)) { - if (typeof(IEdmComplexObject).IsAssignableFrom(elementType) || typeof(IEdmEnumObject).IsAssignableFrom(elementType)) - { - return ODataPayloadKind.Collection; - } - else if (typeof(IEdmEntityObject).IsAssignableFrom(elementType)) - { - return ODataPayloadKind.ResourceSet; - } - else if (typeof(IEdmChangedObject).IsAssignableFrom(elementType)) - { - return ODataPayloadKind.Delta; - } + return ODataPayloadKind.ResourceSet; } - else + else if (typeof(IEdmChangedObject).IsAssignableFrom(elementType)) { - if (typeof(IEdmComplexObject).IsAssignableFrom(elementType) || typeof(IEdmEnumObject).IsAssignableFrom(elementType)) - { - return ODataPayloadKind.Property; - } - else if (typeof(IEdmEntityObject).IsAssignableFrom(elementType)) - { - return ODataPayloadKind.Resource; - } + return ODataPayloadKind.Delta; } - - return null; } - - private static async Task CopyStreamAsync(Stream source, HttpResponse response) + else { - if (source != null) + if (typeof(IEdmComplexObject).IsAssignableFrom(elementType) || typeof(IEdmEnumObject).IsAssignableFrom(elementType)) { - await source.CopyToAsync(response.Body).ConfigureAwait(false); + return ODataPayloadKind.Property; } + else if (typeof(IEdmEntityObject).IsAssignableFrom(elementType)) + { + return ODataPayloadKind.Resource; + } + } - await response.Body.FlushAsync().ConfigureAwait(false); + return null; + } + + private static async Task CopyStreamAsync(Stream source, HttpResponse response) + { + if (source != null) + { + await source.CopyToAsync(response.Body).ConfigureAwait(false); } + + await response.Body.FlushAsync().ConfigureAwait(false); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterFactory.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterFactory.cs index 3ca21fe86..ac93fb4f8 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterFactory.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterFactory.cs @@ -11,154 +11,153 @@ using Microsoft.AspNetCore.OData.Formatter.MediaType; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// Factory for classes to handle OData. +/// +public static class ODataOutputFormatterFactory { - /// - /// Factory for classes to handle OData. - /// - public static class ODataOutputFormatterFactory - { - private const string DollarFormat = "$format"; + private const string DollarFormat = "$format"; - private const string JsonFormat = "json"; + private const string JsonFormat = "json"; - private const string XmlFormat = "xml"; + private const string XmlFormat = "xml"; - /// - /// Creates a list of output formatters to handle OData serialization. - /// - /// A list of media type formatters to handle OData. - public static IList Create() - { - return new List() - { - // Place JSON formatter first so it gets used when the request doesn't ask for a specific content type - CreateApplicationJson(), - CreateApplicationXml(), - CreateRawValue() - }; - } - - private static ODataOutputFormatter CreateApplicationJson() + /// + /// Creates a list of output formatters to handle OData serialization. + /// + /// A list of media type formatters to handle OData. + public static IList Create() + { + return new List() { - ODataOutputFormatter formatter = CreateFormatterWithoutMediaTypes( - ODataPayloadKind.ResourceSet, - ODataPayloadKind.Resource, - ODataPayloadKind.Property, - ODataPayloadKind.EntityReferenceLink, - ODataPayloadKind.EntityReferenceLinks, - ODataPayloadKind.Collection, - ODataPayloadKind.ServiceDocument, - ODataPayloadKind.Error, - ODataPayloadKind.Parameter, - ODataPayloadKind.Delta); - - // Add minimal metadata as the first media type so it gets used when the request doesn't - // ask for a specific content type - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadata); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadata); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadata); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJson); - // NOTE: The order in which the media types are added is relevant due to how ASP.NET Core handles content negotiation - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingTrueIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingTrueIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingFalseIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingFalseIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingTrueIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingTrueIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingFalseIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingFalseIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingTrueIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingTrueIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingFalseIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingFalseIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingTrueIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingTrueIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingFalseIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingFalseIeee754CompatibleTrue); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonIeee754CompatibleFalse); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonIeee754CompatibleTrue); - - formatter.AddDollarFormatQueryStringMappings(); - formatter.AddQueryStringMapping(DollarFormat, JsonFormat, ODataMediaTypes.ApplicationJson); - - return formatter; - } + // Place JSON formatter first so it gets used when the request doesn't ask for a specific content type + CreateApplicationJson(), + CreateApplicationXml(), + CreateRawValue() + }; + } - private static ODataOutputFormatter CreateApplicationXml() - { - ODataOutputFormatter formatter = CreateFormatterWithoutMediaTypes( - ODataPayloadKind.MetadataDocument); + private static ODataOutputFormatter CreateApplicationJson() + { + ODataOutputFormatter formatter = CreateFormatterWithoutMediaTypes( + ODataPayloadKind.ResourceSet, + ODataPayloadKind.Resource, + ODataPayloadKind.Property, + ODataPayloadKind.EntityReferenceLink, + ODataPayloadKind.EntityReferenceLinks, + ODataPayloadKind.Collection, + ODataPayloadKind.ServiceDocument, + ODataPayloadKind.Error, + ODataPayloadKind.Parameter, + ODataPayloadKind.Delta); + + // Add minimal metadata as the first media type so it gets used when the request doesn't + // ask for a specific content type + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadata); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadata); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadata); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJson); + // NOTE: The order in which the media types are added is relevant due to how ASP.NET Core handles content negotiation + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingTrueIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingTrueIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingFalseIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataStreamingFalseIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadataIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingTrueIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingTrueIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingFalseIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataStreamingFalseIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataFullMetadataIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingTrueIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingTrueIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingFalseIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataStreamingFalseIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataNoMetadataIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingTrueIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingTrueIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingFalseIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonStreamingFalseIeee754CompatibleTrue); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonIeee754CompatibleFalse); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonIeee754CompatibleTrue); + + formatter.AddDollarFormatQueryStringMappings(); + formatter.AddQueryStringMapping(DollarFormat, JsonFormat, ODataMediaTypes.ApplicationJson); + + return formatter; + } - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationXml); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJson); + private static ODataOutputFormatter CreateApplicationXml() + { + ODataOutputFormatter formatter = CreateFormatterWithoutMediaTypes( + ODataPayloadKind.MetadataDocument); - formatter.AddDollarFormatQueryStringMappings(); - formatter.AddQueryStringMapping(DollarFormat, XmlFormat, ODataMediaTypes.ApplicationXml); - formatter.AddQueryStringMapping(DollarFormat, JsonFormat, ODataMediaTypes.ApplicationJson); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationXml); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJson); - return formatter; - } + formatter.AddDollarFormatQueryStringMappings(); + formatter.AddQueryStringMapping(DollarFormat, XmlFormat, ODataMediaTypes.ApplicationXml); + formatter.AddQueryStringMapping(DollarFormat, JsonFormat, ODataMediaTypes.ApplicationJson); - private static ODataOutputFormatter CreateRawValue() - { - ODataOutputFormatter formatter = CreateFormatterWithoutMediaTypes(ODataPayloadKind.Value); + return formatter; + } - formatter.SupportedMediaTypes.Add("text/plain"); - formatter.SupportedMediaTypes.Add("application/octet-stream"); + private static ODataOutputFormatter CreateRawValue() + { + ODataOutputFormatter formatter = CreateFormatterWithoutMediaTypes(ODataPayloadKind.Value); - formatter.MediaTypeMappings.Add(new ODataPrimitiveValueMediaTypeMapping()); - formatter.MediaTypeMappings.Add(new ODataEnumValueMediaTypeMapping()); - formatter.MediaTypeMappings.Add(new ODataBinaryValueMediaTypeMapping()); - formatter.MediaTypeMappings.Add(new ODataCountMediaTypeMapping()); - formatter.MediaTypeMappings.Add(new ODataStreamMediaTypeMapping()); - return formatter; - } + formatter.SupportedMediaTypes.Add("text/plain"); + formatter.SupportedMediaTypes.Add("application/octet-stream"); - private static ODataOutputFormatter CreateFormatterWithoutMediaTypes(params ODataPayloadKind[] payloadKinds) - { - ODataOutputFormatter formatter = new ODataOutputFormatter(payloadKinds); - AddSupportedEncodings(formatter); - return formatter; - } + formatter.MediaTypeMappings.Add(new ODataPrimitiveValueMediaTypeMapping()); + formatter.MediaTypeMappings.Add(new ODataEnumValueMediaTypeMapping()); + formatter.MediaTypeMappings.Add(new ODataBinaryValueMediaTypeMapping()); + formatter.MediaTypeMappings.Add(new ODataCountMediaTypeMapping()); + formatter.MediaTypeMappings.Add(new ODataStreamMediaTypeMapping()); + return formatter; + } - private static void AddSupportedEncodings(ODataOutputFormatter formatter) - { - formatter.SupportedEncodings.Add(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, - throwOnInvalidBytes: true)); - formatter.SupportedEncodings.Add(new UnicodeEncoding(bigEndian: false, byteOrderMark: true, - throwOnInvalidBytes: true)); - } + private static ODataOutputFormatter CreateFormatterWithoutMediaTypes(params ODataPayloadKind[] payloadKinds) + { + ODataOutputFormatter formatter = new ODataOutputFormatter(payloadKinds); + AddSupportedEncodings(formatter); + return formatter; + } - private static void AddDollarFormatQueryStringMappings(this ODataOutputFormatter formatter) - { - MediaTypeCollection supportedMediaTypes = formatter.SupportedMediaTypes; - foreach (string supportedMediaType in supportedMediaTypes) - { - QueryStringMediaTypeMapping mapping = new QueryStringMediaTypeMapping(DollarFormat, supportedMediaType); - formatter.MediaTypeMappings.Add(mapping); - } - } + private static void AddSupportedEncodings(ODataOutputFormatter formatter) + { + formatter.SupportedEncodings.Add(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, + throwOnInvalidBytes: true)); + formatter.SupportedEncodings.Add(new UnicodeEncoding(bigEndian: false, byteOrderMark: true, + throwOnInvalidBytes: true)); + } - private static void AddQueryStringMapping(this ODataOutputFormatter formatter, string queryStringParameterName, - string queryStringParameterValue, string mediaType) + private static void AddDollarFormatQueryStringMappings(this ODataOutputFormatter formatter) + { + MediaTypeCollection supportedMediaTypes = formatter.SupportedMediaTypes; + foreach (string supportedMediaType in supportedMediaTypes) { - QueryStringMediaTypeMapping mapping = new QueryStringMediaTypeMapping(queryStringParameterName, queryStringParameterValue, mediaType); + QueryStringMediaTypeMapping mapping = new QueryStringMediaTypeMapping(DollarFormat, supportedMediaType); formatter.MediaTypeMappings.Add(mapping); } } + + private static void AddQueryStringMapping(this ODataOutputFormatter formatter, string queryStringParameterName, + string queryStringParameterValue, string mediaType) + { + QueryStringMediaTypeMapping mapping = new QueryStringMediaTypeMapping(queryStringParameterName, queryStringParameterValue, mediaType); + formatter.MediaTypeMappings.Add(mapping); + } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs index 5ab2cb38a..ba279c144 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs @@ -27,327 +27,326 @@ using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +internal static class ODataOutputFormatterHelper { - internal static class ODataOutputFormatterHelper + public static ODataSerializerContext BuildSerializerContext(HttpRequest request) { - public static ODataSerializerContext BuildSerializerContext(HttpRequest request) + if (request == null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + throw Error.ArgumentNull(nameof(request)); + } - return new ODataSerializerContext() - { - Request = request, - TimeZone = request.GetTimeZoneInfo(), - }; + return new ODataSerializerContext() + { + Request = request, + TimeZone = request.GetTimeZoneInfo(), + }; + } + + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Class coupling acceptable")] + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "")] + internal static async Task WriteToStreamAsync( + Type type, + object value, + IEdmModel model, + ODataVersion version, + Uri baseAddress, + MediaTypeHeaderValue contentType, + HttpRequest request, + IHeaderDictionary requestHeaders, + IODataSerializerProvider serializerProvider) + { + if (model == null) + { + throw Error.InvalidOperation(SRResources.RequestMustHaveModel); } - [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Class coupling acceptable")] - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "")] - internal static async Task WriteToStreamAsync( - Type type, - object value, - IEdmModel model, - ODataVersion version, - Uri baseAddress, - MediaTypeHeaderValue contentType, - HttpRequest request, - IHeaderDictionary requestHeaders, - IODataSerializerProvider serializerProvider) + IODataSerializer serializer = GetSerializer(type, value, request, serializerProvider); + + ODataPath path = request.ODataFeature().Path; + IEdmNavigationSource targetNavigationSource = path.GetNavigationSource(); + HttpResponse response = request.HttpContext.Response; + + // serialize a response + string preferHeader = RequestPreferenceHelpers.GetRequestPreferHeader(requestHeaders); + string annotationFilter = null; + if (!string.IsNullOrEmpty(preferHeader)) { - if (model == null) - { - throw Error.InvalidOperation(SRResources.RequestMustHaveModel); - } + ODataMessageWrapper messageWrapper = ODataMessageWrapperHelper.Create(response.Body, response.Headers); + messageWrapper.SetHeader(RequestPreferenceHelpers.PreferHeaderName, preferHeader); + annotationFilter = messageWrapper.PreferHeader().AnnotationFilter; + } - IODataSerializer serializer = GetSerializer(type, value, request, serializerProvider); + IODataResponseMessageAsync responseMessage = ODataMessageWrapperHelper.Create(new StreamWrapper(response.Body), response.Headers, request.GetRouteServices()); + if (annotationFilter != null) + { + responseMessage.PreferenceAppliedHeader().AnnotationFilter = annotationFilter; + } - ODataPath path = request.ODataFeature().Path; - IEdmNavigationSource targetNavigationSource = path.GetNavigationSource(); - HttpResponse response = request.HttpContext.Response; + ODataMessageWriterSettings writerSettings = request.GetWriterSettings(); + writerSettings.BaseUri = baseAddress; - // serialize a response - string preferHeader = RequestPreferenceHelpers.GetRequestPreferHeader(requestHeaders); - string annotationFilter = null; - if (!string.IsNullOrEmpty(preferHeader)) - { - ODataMessageWrapper messageWrapper = ODataMessageWrapperHelper.Create(response.Body, response.Headers); - messageWrapper.SetHeader(RequestPreferenceHelpers.PreferHeaderName, preferHeader); - annotationFilter = messageWrapper.PreferHeader().AnnotationFilter; - } + //use v401 to write delta payloads. + if (serializer.ODataPayloadKind == ODataPayloadKind.Delta) + { + writerSettings.Version = ODataVersion.V401; + } + else + { + writerSettings.Version = version; + } - IODataResponseMessageAsync responseMessage = ODataMessageWrapperHelper.Create(new StreamWrapper(response.Body), response.Headers, request.GetRouteServices()); - if (annotationFilter != null) - { - responseMessage.PreferenceAppliedHeader().AnnotationFilter = annotationFilter; - } + writerSettings.Validations = writerSettings.Validations & ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType; - ODataMessageWriterSettings writerSettings = request.GetWriterSettings(); - writerSettings.BaseUri = baseAddress; + string metadataLink = request.CreateODataLink(MetadataSegment.Instance); + if (metadataLink == null) + { + throw new SerializationException(SRResources.UnableToDetermineMetadataUrl); + } - //use v401 to write delta payloads. - if (serializer.ODataPayloadKind == ODataPayloadKind.Delta) - { - writerSettings.Version = ODataVersion.V401; - } - else + // Set this variable if the SelectExpandClause is different from the processed clause on the Query options + SelectExpandClause selectExpandDifferentFromQueryOptions = null; + ODataQueryOptions queryOptions = request.GetQueryOptions(); + SelectExpandClause processedSelectExpandClause = request.ODataFeature().SelectExpandClause; + if (queryOptions != null && queryOptions.SelectExpand != null) + { + if (queryOptions.SelectExpand.ProcessedSelectExpandClause != processedSelectExpandClause) { - writerSettings.Version = version; + selectExpandDifferentFromQueryOptions = processedSelectExpandClause; } + } + else if (processedSelectExpandClause != null) + { + selectExpandDifferentFromQueryOptions = processedSelectExpandClause; + } - writerSettings.Validations = writerSettings.Validations & ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType; + writerSettings.ODataUri = new ODataUri + { + ServiceRoot = baseAddress, - string metadataLink = request.CreateODataLink(MetadataSegment.Instance); - if (metadataLink == null) - { - throw new SerializationException(SRResources.UnableToDetermineMetadataUrl); - } + // TODO: 1604 Convert webapi.odata's ODataPath to ODL's ODataPath, or use ODL's ODataPath. + SelectAndExpand = processedSelectExpandClause, + Apply = request.ODataFeature().ApplyClause, + Path = path + }; - // Set this variable if the SelectExpandClause is different from the processed clause on the Query options - SelectExpandClause selectExpandDifferentFromQueryOptions = null; - ODataQueryOptions queryOptions = request.GetQueryOptions(); - SelectExpandClause processedSelectExpandClause = request.ODataFeature().SelectExpandClause; - if (queryOptions != null && queryOptions.SelectExpand != null) - { - if (queryOptions.SelectExpand.ProcessedSelectExpandClause != processedSelectExpandClause) - { - selectExpandDifferentFromQueryOptions = processedSelectExpandClause; - } - } - else if (processedSelectExpandClause != null) + ODataMetadataLevel metadataLevel = ODataMetadataLevel.Minimal; + if (contentType != null) + { + IEnumerable> parameters = + contentType.Parameters.Select(val => new KeyValuePair(val.Name.ToString(), + val.Value.ToString())); + metadataLevel = ODataMediaTypes.GetMetadataLevel(contentType.MediaType.ToString(), parameters); + } + + using (ODataMessageWriter messageWriter = new ODataMessageWriter(responseMessage, writerSettings, model)) + { + ODataSerializerContext writeContext = BuildSerializerContext(request); + writeContext.NavigationSource = targetNavigationSource; + writeContext.Model = model; + writeContext.RootElementName = GetRootElementName(path) ?? "root"; + writeContext.SkipExpensiveAvailabilityChecks = serializer.ODataPayloadKind == ODataPayloadKind.ResourceSet; + writeContext.Path = path; + writeContext.MetadataLevel = metadataLevel; + writeContext.QueryOptions = queryOptions; + writeContext.SetComputedProperties(queryOptions?.Compute?.ComputeClause); + writeContext.Type = type; + + //Set the SelectExpandClause on the context if it was explicitly specified. + if (selectExpandDifferentFromQueryOptions != null) { - selectExpandDifferentFromQueryOptions = processedSelectExpandClause; + writeContext.SelectExpandClause = selectExpandDifferentFromQueryOptions; } - writerSettings.ODataUri = new ODataUri - { - ServiceRoot = baseAddress, + await serializer.WriteObjectAsync(value, type, messageWriter, writeContext).ConfigureAwait(false); + } + } - // TODO: 1604 Convert webapi.odata's ODataPath to ODL's ODataPath, or use ODL's ODataPath. - SelectAndExpand = processedSelectExpandClause, - Apply = request.ODataFeature().ApplyClause, - Path = path - }; + internal static IODataSerializer GetSerializer(Type type, object value, HttpRequest request, + IODataSerializerProvider serializerProvider) + { + IODataSerializer serializer; - ODataMetadataLevel metadataLevel = ODataMetadataLevel.Minimal; - if (contentType != null) + IEdmObject edmObject = value as IEdmObject; + if (edmObject != null) + { + IEdmTypeReference edmType = edmObject.GetEdmType(); + if (edmType == null) { - IEnumerable> parameters = - contentType.Parameters.Select(val => new KeyValuePair(val.Name.ToString(), - val.Value.ToString())); - metadataLevel = ODataMediaTypes.GetMetadataLevel(contentType.MediaType.ToString(), parameters); + throw new SerializationException(Error.Format(SRResources.EdmTypeCannotBeNull, + edmObject.GetType().FullName, typeof(IEdmObject).Name)); } - using (ODataMessageWriter messageWriter = new ODataMessageWriter(responseMessage, writerSettings, model)) + serializer = serializerProvider.GetEdmTypeSerializer(edmType); + if (serializer == null) { - ODataSerializerContext writeContext = BuildSerializerContext(request); - writeContext.NavigationSource = targetNavigationSource; - writeContext.Model = model; - writeContext.RootElementName = GetRootElementName(path) ?? "root"; - writeContext.SkipExpensiveAvailabilityChecks = serializer.ODataPayloadKind == ODataPayloadKind.ResourceSet; - writeContext.Path = path; - writeContext.MetadataLevel = metadataLevel; - writeContext.QueryOptions = queryOptions; - writeContext.SetComputedProperties(queryOptions?.Compute?.ComputeClause); - writeContext.Type = type; - - //Set the SelectExpandClause on the context if it was explicitly specified. - if (selectExpandDifferentFromQueryOptions != null) - { - writeContext.SelectExpandClause = selectExpandDifferentFromQueryOptions; - } - - await serializer.WriteObjectAsync(value, type, messageWriter, writeContext).ConfigureAwait(false); + string message = Error.Format(SRResources.TypeCannotBeSerialized, edmType.ToTraceString()); + throw new SerializationException(message); } } - - internal static IODataSerializer GetSerializer(Type type, object value, HttpRequest request, - IODataSerializerProvider serializerProvider) + else { - IODataSerializer serializer; + var applyClause = request.ODataFeature().ApplyClause; - IEdmObject edmObject = value as IEdmObject; - if (edmObject != null) + // get the most appropriate serializer given that we support inheritance. + if (applyClause == null) { - IEdmTypeReference edmType = edmObject.GetEdmType(); - if (edmType == null) - { - throw new SerializationException(Error.Format(SRResources.EdmTypeCannotBeNull, - edmObject.GetType().FullName, typeof(IEdmObject).Name)); - } - - serializer = serializerProvider.GetEdmTypeSerializer(edmType); - if (serializer == null) - { - string message = Error.Format(SRResources.TypeCannotBeSerialized, edmType.ToTraceString()); - throw new SerializationException(message); - } + type = value == null ? type : value.GetType(); } - else - { - var applyClause = request.ODataFeature().ApplyClause; - // get the most appropriate serializer given that we support inheritance. - if (applyClause == null) - { - type = value == null ? type : value.GetType(); - } - - serializer = serializerProvider.GetODataPayloadSerializer(type, request); - if (serializer == null) - { - string message = Error.Format(SRResources.TypeCannotBeSerialized, type.Name); - throw new SerializationException(message); - } + serializer = serializerProvider.GetODataPayloadSerializer(type, request); + if (serializer == null) + { + string message = Error.Format(SRResources.TypeCannotBeSerialized, type.Name); + throw new SerializationException(message); } - - return serializer; } - private static string GetRootElementName(ODataPath path) + return serializer; + } + + private static string GetRootElementName(ODataPath path) + { + if (path != null) { - if (path != null) + ODataPathSegment lastSegment = path.LastSegment; + if (lastSegment != null) { - ODataPathSegment lastSegment = path.LastSegment; - if (lastSegment != null) + OperationSegment actionSegment = lastSegment as OperationSegment; + if (actionSegment != null) { - OperationSegment actionSegment = lastSegment as OperationSegment; - if (actionSegment != null) + IEdmAction action = actionSegment.Operations.Single() as IEdmAction; + if (action != null) { - IEdmAction action = actionSegment.Operations.Single() as IEdmAction; - if (action != null) - { - return action.Name; - } + return action.Name; } + } - PropertySegment propertyAccessSegment = lastSegment as PropertySegment; - if (propertyAccessSegment != null) - { - return propertyAccessSegment.Property.Name; - } + PropertySegment propertyAccessSegment = lastSegment as PropertySegment; + if (propertyAccessSegment != null) + { + return propertyAccessSegment.Property.Name; } } - - return null; } + + return null; } +} - // Since OData metadata write is not async. - // Any $metadata request will throw "Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true." - // So, we have to use "StreamWrapper" to override "Write(byte[] buffer, int offset, int count)" - // Once we enable async for metadata writer, we should remove this class. - [ExcludeFromCodeCoverage] - internal class StreamWrapper : Stream +// Since OData metadata write is not async. +// Any $metadata request will throw "Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true." +// So, we have to use "StreamWrapper" to override "Write(byte[] buffer, int offset, int count)" +// Once we enable async for metadata writer, we should remove this class. +[ExcludeFromCodeCoverage] +internal class StreamWrapper : Stream +{ + private Stream stream; + public StreamWrapper(Stream stream) { - private Stream stream; - public StreamWrapper(Stream stream) - { - this.stream = stream; - } + this.stream = stream; + } - public override bool CanRead => this.stream.CanRead; + public override bool CanRead => this.stream.CanRead; - public override bool CanSeek => this.stream.CanSeek; + public override bool CanSeek => this.stream.CanSeek; - public override bool CanWrite => this.stream.CanWrite; + public override bool CanWrite => this.stream.CanWrite; - public override long Length => this.stream.Length; + public override long Length => this.stream.Length; - public override int ReadTimeout { get => this.stream.ReadTimeout; set => this.stream.ReadTimeout = value; } + public override int ReadTimeout { get => this.stream.ReadTimeout; set => this.stream.ReadTimeout = value; } - public override int WriteTimeout { get => this.stream.WriteTimeout; set => this.stream.WriteTimeout = value; } + public override int WriteTimeout { get => this.stream.WriteTimeout; set => this.stream.WriteTimeout = value; } - public override bool CanTimeout => this.stream.CanTimeout; + public override bool CanTimeout => this.stream.CanTimeout; - public override void Close() - { - this.stream.Close(); - } + public override void Close() + { + this.stream.Close(); + } - public override long Position { get => this.stream.Position; set => this.stream.Position = value; } + public override long Position { get => this.stream.Position; set => this.stream.Position = value; } - public override void Flush() - { - this.stream.FlushAsync().Wait(); - } + public override void Flush() + { + this.stream.FlushAsync().Wait(); + } - public override Task FlushAsync(CancellationToken cancellationToken) - { - return stream.FlushAsync(cancellationToken); - } + public override Task FlushAsync(CancellationToken cancellationToken) + { + return stream.FlushAsync(cancellationToken); + } - public override int Read(byte[] buffer, int offset, int count) - { - return this.stream.ReadAsync(buffer, offset, count).Result; - } + public override int Read(byte[] buffer, int offset, int count) + { + return this.stream.ReadAsync(buffer, offset, count).Result; + } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return this.stream.ReadAsync(buffer, offset, count, cancellationToken); - } + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return this.stream.ReadAsync(buffer, offset, count, cancellationToken); + } - public override int ReadByte() - { - return this.stream.ReadByte(); - } + public override int ReadByte() + { + return this.stream.ReadByte(); + } - public override void WriteByte(byte value) - { - this.stream.WriteByte(value); - } + public override void WriteByte(byte value) + { + this.stream.WriteByte(value); + } - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - return this.stream.CopyToAsync(destination, bufferSize, cancellationToken); - } + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return this.stream.CopyToAsync(destination, bufferSize, cancellationToken); + } - public override long Seek(long offset, SeekOrigin origin) - { - return this.stream.Seek(offset, origin); - } + public override long Seek(long offset, SeekOrigin origin) + { + return this.stream.Seek(offset, origin); + } - public override void SetLength(long value) - { - this.stream.SetLength(value); - } + public override void SetLength(long value) + { + this.stream.SetLength(value); + } - public override void Write(byte[] buffer, int offset, int count) - { - this.stream.WriteAsync(buffer, offset, count).GetAwaiter().GetResult(); - } + public override void Write(byte[] buffer, int offset, int count) + { + this.stream.WriteAsync(buffer, offset, count).GetAwaiter().GetResult(); + } - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return this.stream.WriteAsync(buffer, offset, count, cancellationToken); - } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return this.stream.WriteAsync(buffer, offset, count, cancellationToken); + } - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - return this.stream.BeginRead(buffer, offset, count, callback, state); - } + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return this.stream.BeginRead(buffer, offset, count, callback, state); + } - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) - { - return this.stream.BeginWrite(buffer, offset, count, callback, state); - } + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return this.stream.BeginWrite(buffer, offset, count, callback, state); + } - public override int EndRead(IAsyncResult asyncResult) - { - return this.stream.EndRead(asyncResult); - } + public override int EndRead(IAsyncResult asyncResult) + { + return this.stream.EndRead(asyncResult); + } - public override void EndWrite(IAsyncResult asyncResult) - { - this.stream.EndWrite(asyncResult); - } + public override void EndWrite(IAsyncResult asyncResult) + { + this.stream.EndWrite(asyncResult); + } - public override string ToString() - { - return this.stream.ToString(); - } + public override string ToString() + { + return this.stream.ToString(); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataParameterValue.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataParameterValue.cs index 420012f23..df822c96d 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataParameterValue.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataParameterValue.cs @@ -8,42 +8,41 @@ using System; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// OData parameter value used for function parameter binding. +/// +public class ODataParameterValue { /// - /// OData parameter value used for function parameter binding. + /// This prefix is used to identify parameters in [FromODataUri] binding scenario. /// - public class ODataParameterValue - { - /// - /// This prefix is used to identify parameters in [FromODataUri] binding scenario. - /// - public const string ParameterValuePrefix = "DF908045-6922-46A0-82F2-2F6E7F43D1B1_"; + public const string ParameterValuePrefix = "DF908045-6922-46A0-82F2-2F6E7F43D1B1_"; - /// - /// Initializes a new instance of the class. - /// - /// The parameter value. - /// The parameter type. - public ODataParameterValue(object paramValue, IEdmTypeReference paramType) + /// + /// Initializes a new instance of the class. + /// + /// The parameter value. + /// The parameter type. + public ODataParameterValue(object paramValue, IEdmTypeReference paramType) + { + if (paramType == null) { - if (paramType == null) - { - throw new ArgumentNullException(nameof(paramType)); - } - - Value = paramValue; - EdmType = paramType; + throw new ArgumentNullException(nameof(paramType)); } - /// - /// Gets the parameter type. - /// - public IEdmTypeReference EdmType { get; } - - /// - /// Gets the parameter value. - /// - public object Value { get; } + Value = paramValue; + EdmType = paramType; } + + /// + /// Gets the parameter type. + /// + public IEdmTypeReference EdmType { get; } + + /// + /// Gets the parameter value. + /// + public object Value { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataUntypedActionParameters.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataUntypedActionParameters.cs index b478f6056..286e28a2c 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ODataUntypedActionParameters.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataUntypedActionParameters.cs @@ -11,28 +11,27 @@ using Microsoft.OData.Edm; using Microsoft.AspNetCore.OData.Abstracts; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// ActionPayload holds the Parameter names and values provided by a client in a POST request +/// to invoke a particular Action. The Parameter values are stored in the dictionary keyed using the Parameter name. +/// +[SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "ODataUntypedActionParameters is more appropriate here.")] +[NonValidatingParameterBinding] +public class ODataUntypedActionParameters : Dictionary { /// - /// ActionPayload holds the Parameter names and values provided by a client in a POST request - /// to invoke a particular Action. The Parameter values are stored in the dictionary keyed using the Parameter name. + /// Initializes a new instance of the class. /// - [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "ODataUntypedActionParameters is more appropriate here.")] - [NonValidatingParameterBinding] - public class ODataUntypedActionParameters : Dictionary + /// The OData action of this parameters. + public ODataUntypedActionParameters(IEdmAction action) { - /// - /// Initializes a new instance of the class. - /// - /// The OData action of this parameters. - public ODataUntypedActionParameters(IEdmAction action) - { - Action = action ?? throw new ArgumentNullException(nameof(action)); - } - - /// - /// Gets the OData action of this parameters. - /// - public IEdmAction Action { get; } + Action = action ?? throw new ArgumentNullException(nameof(action)); } + + /// + /// Gets the OData action of this parameters. + /// + public IEdmAction Action { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/RequestPreferenceHelpers.cs b/src/Microsoft.AspNetCore.OData/Formatter/RequestPreferenceHelpers.cs index 0009d7f2e..02d97fdd3 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/RequestPreferenceHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/RequestPreferenceHelpers.cs @@ -9,26 +9,25 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +internal static class RequestPreferenceHelpers { - internal static class RequestPreferenceHelpers - { - public const string PreferHeaderName = "Prefer"; - public const string ReturnContentHeaderValue = "return=representation"; - public const string ReturnNoContentHeaderValue = "return=minimal"; - public const string ODataMaxPageSize = "odata.maxpagesize"; - public const string MaxPageSize = "maxpagesize"; + public const string PreferHeaderName = "Prefer"; + public const string ReturnContentHeaderValue = "return=representation"; + public const string ReturnNoContentHeaderValue = "return=minimal"; + public const string ODataMaxPageSize = "odata.maxpagesize"; + public const string MaxPageSize = "maxpagesize"; - internal static string GetRequestPreferHeader(IHeaderDictionary headers) + internal static string GetRequestPreferHeader(IHeaderDictionary headers) + { + StringValues values; + if (headers.TryGetValue(PreferHeaderName, out values)) { - StringValues values; - if (headers.TryGetValue(PreferHeaderName, out values)) - { - // If there are many "Prefer" headers, pick up the first one. - return values.FirstOrDefault(); - } - - return null; + // If there are many "Prefer" headers, pick up the first one. + return values.FirstOrDefault(); } + + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ResourceContext.cs b/src/Microsoft.AspNetCore.OData/Formatter/ResourceContext.cs index 958bd4c69..8a467ec8a 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ResourceContext.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ResourceContext.cs @@ -19,265 +19,264 @@ using Microsoft.AspNetCore.OData.Query.Wrapper; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// Contains context information about the resource currently being serialized. +/// +public class ResourceContext { + private object _resourceInstance; + /// - /// Contains context information about the resource currently being serialized. + /// Initializes a new instance of the class. /// - public class ResourceContext + public ResourceContext() { - private object _resourceInstance; + SerializerContext = new ODataSerializerContext(); + } - /// - /// Initializes a new instance of the class. - /// - public ResourceContext() - { - SerializerContext = new ODataSerializerContext(); - } + /// + /// Initializes a new instance of the class. + /// + /// The backing . + /// The EDM structured type of this instance context. + /// The object representing the instance of this context. + public ResourceContext(ODataSerializerContext serializerContext, IEdmStructuredTypeReference structuredType, object resourceInstance) + : this(serializerContext, structuredType, AsEdmResourceObject(serializerContext, resourceInstance, structuredType)) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The backing . - /// The EDM structured type of this instance context. - /// The object representing the instance of this context. - public ResourceContext(ODataSerializerContext serializerContext, IEdmStructuredTypeReference structuredType, object resourceInstance) - : this(serializerContext, structuredType, AsEdmResourceObject(serializerContext, resourceInstance, structuredType)) + private ResourceContext(ODataSerializerContext serializerContext, IEdmStructuredTypeReference structuredType, IEdmStructuredObject edmObject) + { + if (serializerContext == null) { + throw Error.ArgumentNull(nameof(serializerContext)); } - private ResourceContext(ODataSerializerContext serializerContext, IEdmStructuredTypeReference structuredType, IEdmStructuredObject edmObject) + SerializerContext = serializerContext; + StructuredType = structuredType.StructuredDefinition(); + EdmObject = edmObject; + } + + /// + /// Gets or sets the . + /// + public ODataSerializerContext SerializerContext { get; set; } + + /// + /// Gets or sets the to which this instance belongs. + /// + public IEdmModel EdmModel + { + get => SerializerContext.Model; + set => SerializerContext.Model = value; + } + + /// + /// Gets or sets the to which this instance belongs. + /// + public IEdmNavigationSource NavigationSource + { + get => SerializerContext.NavigationSource; + set => SerializerContext.NavigationSource = value; + } + + /// + /// Gets or sets the of this resource instance. + /// + public IEdmStructuredType StructuredType { get; set; } + + /// + /// Gets or sets the backing this instance. + /// + public IEdmStructuredObject EdmObject { get; set; } + + /// + /// Gets or sets the value of this resource instance. + /// + public object ResourceInstance + { + get { - if (serializerContext == null) + if (_resourceInstance == null) { - throw Error.ArgumentNull(nameof(serializerContext)); + _resourceInstance = BuildResourceInstance(); } - SerializerContext = serializerContext; - StructuredType = structuredType.StructuredDefinition(); - EdmObject = edmObject; + return _resourceInstance; } + set => _resourceInstance = value; + } - /// - /// Gets or sets the . - /// - public ODataSerializerContext SerializerContext { get; set; } - - /// - /// Gets or sets the to which this instance belongs. - /// - public IEdmModel EdmModel - { - get => SerializerContext.Model; - set => SerializerContext.Model = value; - } + /// + /// Gets or sets the HTTP request that caused this instance to be generated. + /// + /// This signature uses types that are AspNetCore-specific. + public HttpRequest Request + { + get => SerializerContext.Request; + set => SerializerContext.Request = value; + } - /// - /// Gets or sets the to which this instance belongs. - /// - public IEdmNavigationSource NavigationSource - { - get => SerializerContext.NavigationSource; - set => SerializerContext.NavigationSource = value; - } + /// + /// Gets or sets the . + /// + internal TimeZoneInfo TimeZone => SerializerContext.TimeZone; - /// - /// Gets or sets the of this resource instance. - /// - public IEdmStructuredType StructuredType { get; set; } + /// + /// Gets or sets a value indicating whether ActionAvailabilityChecks should be performed or not. + /// + /// + /// This value is used to tell the formatter whether to check availability of an action before including a link + /// to it. When in a feed we skip this check. + /// + public bool SkipExpensiveAvailabilityChecks + { + get => SerializerContext.SkipExpensiveAvailabilityChecks; + set => SerializerContext.SkipExpensiveAvailabilityChecks = value; + } - /// - /// Gets or sets the backing this instance. - /// - public IEdmStructuredObject EdmObject { get; set; } + /// + /// Gets or sets the dynamic complex or collection of complex properties should be nested in this instance. + /// + /// + /// The key is the dynamic property name. + /// The value is the dynamic property value. + /// + [SuppressMessage("Microsoft.Usage", "CA2227:EnableSetterForProperty", Justification = "Enable setter for dictionary property")] + public IDictionary DynamicComplexProperties { get; set; } - /// - /// Gets or sets the value of this resource instance. - /// - public object ResourceInstance + /// + /// Gets the value of the property with the given name from the of this instance if present; throws if the property is + /// not present. + /// + /// The name of the property to get. + /// The value of the property if present. + public object GetPropertyValue(string propertyName) + { + if (EdmObject == null) { - get - { - if (_resourceInstance == null) - { - _resourceInstance = BuildResourceInstance(); - } - - return _resourceInstance; - } - set => _resourceInstance = value; + throw Error.InvalidOperation(SRResources.EdmObjectNull, typeof(ResourceContext).Name); } - /// - /// Gets or sets the HTTP request that caused this instance to be generated. - /// - /// This signature uses types that are AspNetCore-specific. - public HttpRequest Request + if (SerializerContext.IsDeltaOfT && ResourceInstance is IDelta delta && delta.TryGetPropertyValue(propertyName, out object value)) { - get => SerializerContext.Request; - set => SerializerContext.Request = value; + return value; } - /// - /// Gets or sets the . - /// - internal TimeZoneInfo TimeZone => SerializerContext.TimeZone; - - /// - /// Gets or sets a value indicating whether ActionAvailabilityChecks should be performed or not. - /// - /// - /// This value is used to tell the formatter whether to check availability of an action before including a link - /// to it. When in a feed we skip this check. - /// - public bool SkipExpensiveAvailabilityChecks + if (EdmObject.TryGetPropertyValue(propertyName, out value)) { - get => SerializerContext.SkipExpensiveAvailabilityChecks; - set => SerializerContext.SkipExpensiveAvailabilityChecks = value; + return value; } - - /// - /// Gets or sets the dynamic complex or collection of complex properties should be nested in this instance. - /// - /// - /// The key is the dynamic property name. - /// The value is the dynamic property value. - /// - [SuppressMessage("Microsoft.Usage", "CA2227:EnableSetterForProperty", Justification = "Enable setter for dictionary property")] - public IDictionary DynamicComplexProperties { get; set; } - - /// - /// Gets the value of the property with the given name from the of this instance if present; throws if the property is - /// not present. - /// - /// The name of the property to get. - /// The value of the property if present. - public object GetPropertyValue(string propertyName) + else { - if (EdmObject == null) + IEdmTypeReference edmType = EdmObject.GetEdmType(); + if (edmType == null) { - throw Error.InvalidOperation(SRResources.EdmObjectNull, typeof(ResourceContext).Name); + // Provide general guidance in the message. typeof(IEdmTypeReference).Name would be too specific. + throw Error.InvalidOperation(SRResources.EdmTypeCannotBeNull, EdmObject.GetType().FullName, + typeof(IEdmObject).Name); } - if (SerializerContext.IsDeltaOfT && ResourceInstance is IDelta delta && delta.TryGetPropertyValue(propertyName, out object value)) - { - return value; - } + throw Error.InvalidOperation(SRResources.PropertyNotFound, edmType.ToTraceString(), propertyName); + } + } - if (EdmObject.TryGetPropertyValue(propertyName, out value)) - { - return value; - } - else - { - IEdmTypeReference edmType = EdmObject.GetEdmType(); - if (edmType == null) - { - // Provide general guidance in the message. typeof(IEdmTypeReference).Name would be too specific. - throw Error.InvalidOperation(SRResources.EdmTypeCannotBeNull, EdmObject.GetType().FullName, - typeof(IEdmObject).Name); - } + private object BuildResourceInstance() + { + if (EdmObject == null) + { + return null; + } - throw Error.InvalidOperation(SRResources.PropertyNotFound, edmType.ToTraceString(), propertyName); - } + TypedEdmStructuredObject edmStructuredObject = EdmObject as TypedEdmStructuredObject; + if (edmStructuredObject != null) + { + return edmStructuredObject.Instance; } - private object BuildResourceInstance() + SelectExpandWrapper selectExpandWrapper = EdmObject as SelectExpandWrapper; + if (selectExpandWrapper != null && selectExpandWrapper.UntypedInstance != null) { - if (EdmObject == null) - { - return null; - } + return selectExpandWrapper.UntypedInstance; + } - TypedEdmStructuredObject edmStructuredObject = EdmObject as TypedEdmStructuredObject; - if (edmStructuredObject != null) - { - return edmStructuredObject.Instance; - } + Type clrType = EdmModel.GetClrType(StructuredType); + if (clrType == null) + { + throw new InvalidOperationException(Error.Format(SRResources.MappingDoesNotContainResourceType, StructuredType.FullTypeName())); + } - SelectExpandWrapper selectExpandWrapper = EdmObject as SelectExpandWrapper; - if (selectExpandWrapper != null && selectExpandWrapper.UntypedInstance != null) + object resource = Activator.CreateInstance(clrType); + foreach (IEdmStructuralProperty property in StructuredType.StructuralProperties()) + { + object value; + if (EdmObject.TryGetPropertyValue(property.Name, out value) && value != null) { - return selectExpandWrapper.UntypedInstance; - } + if (value is SelectExpandWrapper) + { + // Skip the select expand property + continue; + } - Type clrType = EdmModel.GetClrType(StructuredType); - if (clrType == null) - { - throw new InvalidOperationException(Error.Format(SRResources.MappingDoesNotContainResourceType, StructuredType.FullTypeName())); - } + string propertyName = EdmModel.GetClrPropertyName(property); - object resource = Activator.CreateInstance(clrType); - foreach (IEdmStructuralProperty property in StructuredType.StructuralProperties()) - { - object value; - if (EdmObject.TryGetPropertyValue(property.Name, out value) && value != null) + if (TypeHelper.IsCollection(value.GetType())) + { + DeserializationHelpers.SetCollectionProperty(resource, property, value, propertyName); + } + else { - if (value is SelectExpandWrapper) - { - // Skip the select expand property - continue; - } - - string propertyName = EdmModel.GetClrPropertyName(property); - - if (TypeHelper.IsCollection(value.GetType())) - { - DeserializationHelpers.SetCollectionProperty(resource, property, value, propertyName); - } - else - { - DeserializationHelpers.SetProperty(resource, propertyName, value); - } + DeserializationHelpers.SetProperty(resource, propertyName, value); } } - - return resource; } - private static IEdmStructuredObject AsEdmResourceObject(ODataSerializerContext serializerContext, object resourceInstance, IEdmStructuredTypeReference structuredType) - { - if (serializerContext == null) - { - throw Error.ArgumentNull(nameof(serializerContext)); - } - - if (structuredType == null) - { - throw Error.ArgumentNull(nameof(structuredType)); - } + return resource; + } - IEdmModel model = serializerContext.Model; + private static IEdmStructuredObject AsEdmResourceObject(ODataSerializerContext serializerContext, object resourceInstance, IEdmStructuredTypeReference structuredType) + { + if (serializerContext == null) + { + throw Error.ArgumentNull(nameof(serializerContext)); + } - IEdmStructuredObject edmStructuredObject = resourceInstance as IEdmStructuredObject; - if (edmStructuredObject != null) - { - return edmStructuredObject; - } + if (structuredType == null) + { + throw Error.ArgumentNull(nameof(structuredType)); + } - if (structuredType.IsEntity()) - { - return new TypedEdmEntityObject(resourceInstance, structuredType.AsEntity(), model); - } + IEdmModel model = serializerContext.Model; - if (structuredType.IsUntyped()) - { - return new TypedEdmUntypedObject(serializerContext, resourceInstance); - } + IEdmStructuredObject edmStructuredObject = resourceInstance as IEdmStructuredObject; + if (edmStructuredObject != null) + { + return edmStructuredObject; + } - Contract.Assert(structuredType.IsComplex()); - return new TypedEdmComplexObject(resourceInstance, structuredType.AsComplex(), model); + if (structuredType.IsEntity()) + { + return new TypedEdmEntityObject(resourceInstance, structuredType.AsEntity(), model); } - internal void AppendDynamicOrUntypedProperty(string propertyName, object value) + if (structuredType.IsUntyped()) { - if (DynamicComplexProperties == null) - { - DynamicComplexProperties = new Dictionary(); - } + return new TypedEdmUntypedObject(serializerContext, resourceInstance); + } + + Contract.Assert(structuredType.IsComplex()); + return new TypedEdmComplexObject(resourceInstance, structuredType.AsComplex(), model); + } - DynamicComplexProperties.Add(propertyName, value); + internal void AppendDynamicOrUntypedProperty(string propertyName, object value) + { + if (DynamicComplexProperties == null) + { + DynamicComplexProperties = new Dictionary(); } + + DynamicComplexProperties.Add(propertyName, value); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ResourceContextOfTStructuredType.cs b/src/Microsoft.AspNetCore.OData/Formatter/ResourceContextOfTStructuredType.cs index 7bbd7cb35..05c7b2f45 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ResourceContextOfTStructuredType.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ResourceContextOfTStructuredType.cs @@ -5,21 +5,20 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// An instance of gets passed to the self link +/// and navigation link builders and can be used by the link builders to generate links. +/// +/// The structural type +internal class ResourceContext : ResourceContext { /// - /// An instance of gets passed to the self link - /// and navigation link builders and can be used by the link builders to generate links. + /// Initializes a new instance of the class. /// - /// The structural type - internal class ResourceContext : ResourceContext + public ResourceContext() + : base() { - /// - /// Initializes a new instance of the class. - /// - public ResourceContext() - : base() - { - } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ResourceSetContext.cs b/src/Microsoft.AspNetCore.OData/Formatter/ResourceSetContext.cs index ecdfa244b..321b5d1c8 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ResourceSetContext.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ResourceSetContext.cs @@ -12,71 +12,70 @@ using Microsoft.AspNetCore.OData.Formatter.Serialization; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +/// +/// Contains context information about the resource set currently being serialized. +/// +public class ResourceSetContext { /// - /// Contains context information about the resource set currently being serialized. + /// Gets the this instance belongs to. /// - public class ResourceSetContext - { - /// - /// Gets the this instance belongs to. - /// - public IEdmEntitySetBase EntitySetBase { get; set; } + public IEdmEntitySetBase EntitySetBase { get; set; } - /// - /// Gets the value of this feed instance. - /// - public object ResourceSetInstance { get; set; } + /// + /// Gets the value of this feed instance. + /// + public object ResourceSetInstance { get; set; } - /// - /// Gets or sets the HTTP request that caused this instance to be generated. - /// - public HttpRequest Request { get; set; } + /// + /// Gets or sets the HTTP request that caused this instance to be generated. + /// + public HttpRequest Request { get; set; } - /// - /// Gets or sets the to which this instance belongs. - /// - /// This signature uses types that are AspNetCore-specific. - public IEdmModel EdmModel - { - get { return Request.GetModel(); } - } + /// + /// Gets or sets the to which this instance belongs. + /// + /// This signature uses types that are AspNetCore-specific. + public IEdmModel EdmModel + { + get { return Request.GetModel(); } + } - /// - /// Create a from an and . - /// - /// The instance representing the resourceSet being written. - /// The serializer context. - /// A new . - /// This signature uses types that are AspNetCore-specific. - internal static ResourceSetContext Create(ODataSerializerContext writeContext, IEnumerable resourceSetInstance) + /// + /// Create a from an and . + /// + /// The instance representing the resourceSet being written. + /// The serializer context. + /// A new . + /// This signature uses types that are AspNetCore-specific. + internal static ResourceSetContext Create(ODataSerializerContext writeContext, IEnumerable resourceSetInstance) + { + ResourceSetContext resourceSetContext = new ResourceSetContext { - ResourceSetContext resourceSetContext = new ResourceSetContext - { - Request = writeContext.Request, - EntitySetBase = writeContext.NavigationSource as IEdmEntitySetBase, - ResourceSetInstance = resourceSetInstance - }; + Request = writeContext.Request, + EntitySetBase = writeContext.NavigationSource as IEdmEntitySetBase, + ResourceSetInstance = resourceSetInstance + }; - return resourceSetContext; - } + return resourceSetContext; + } - /// - /// Create a from an and . - /// - /// The serializer context. - /// The instance representing the resourceSet being written. - /// A new . - /// This signature uses types that are AspNetCore-specific. - internal static ResourceSetContext Create(ODataSerializerContext writeContext, IAsyncEnumerable resourceSetInstance) + /// + /// Create a from an and . + /// + /// The serializer context. + /// The instance representing the resourceSet being written. + /// A new . + /// This signature uses types that are AspNetCore-specific. + internal static ResourceSetContext Create(ODataSerializerContext writeContext, IAsyncEnumerable resourceSetInstance) + { + return new ResourceSetContext { - return new ResourceSetContext - { - Request = writeContext.Request, - EntitySetBase = writeContext.NavigationSource as IEdmEntitySetBase, - ResourceSetInstance = resourceSetInstance - }; - } + Request = writeContext.Request, + EntitySetBase = writeContext.NavigationSource as IEdmEntitySetBase, + ResourceSetInstance = resourceSetInstance + }; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IODataEdmTypeSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IODataEdmTypeSerializer.cs index 91d5cb0b8..6436c6495 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IODataEdmTypeSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IODataEdmTypeSerializer.cs @@ -9,30 +9,29 @@ using Microsoft.OData.Edm; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// Represents an that serializes instances of objects backed by an . +/// +public interface IODataEdmTypeSerializer: IODataSerializer { /// - /// Represents an that serializes instances of objects backed by an . + /// Creates an for the object represented by . /// - public interface IODataEdmTypeSerializer: IODataSerializer - { - /// - /// Creates an for the object represented by . - /// - /// The value of the to be created. - /// The expected EDM type of the object represented by . - /// The . - /// The created. - ODataValue CreateODataValue(object graph, IEdmTypeReference expectedType, ODataSerializerContext writeContext); + /// The value of the to be created. + /// The expected EDM type of the object represented by . + /// The . + /// The created. + ODataValue CreateODataValue(object graph, IEdmTypeReference expectedType, ODataSerializerContext writeContext); - /// - /// Writes the given object specified by the parameter graph as a part of an existing OData message using the given - /// messageWriter and the writeContext. - /// - /// The object to be written. - /// The expected EDM type of the object represented by . - /// The to be used for writing. - /// The . - Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext); - } + /// + /// Writes the given object specified by the parameter graph as a part of an existing OData message using the given + /// messageWriter and the writeContext. + /// + /// The object to be written. + /// The expected EDM type of the object represented by . + /// The to be used for writing. + /// The . + Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext); } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IODataSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IODataSerializer.cs index 0865976dc..b9cdbe2e8 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IODataSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IODataSerializer.cs @@ -9,30 +9,29 @@ using System.Threading.Tasks; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// An is used to write a CLR object to an ODataMessage. +/// +/// +/// Each supported CLR type has a corresponding . A CLR type is supported if it is one of +/// the special types or if it has a backing EDM type. Some of the special types are Uri which maps to ODataReferenceLink payload, +/// Uri[] which maps to ODataReferenceLinks payload, etc. +/// +public interface IODataSerializer { /// - /// An is used to write a CLR object to an ODataMessage. + /// The kind of OData payload that this serializer generates. /// - /// - /// Each supported CLR type has a corresponding . A CLR type is supported if it is one of - /// the special types or if it has a backing EDM type. Some of the special types are Uri which maps to ODataReferenceLink payload, - /// Uri[] which maps to ODataReferenceLinks payload, etc. - /// - public interface IODataSerializer - { - /// - /// The kind of OData payload that this serializer generates. - /// - ODataPayloadKind ODataPayloadKind { get; } + ODataPayloadKind ODataPayloadKind { get; } - /// - /// Writes the given object specified by the parameter graph as a whole using the given messageWriter and writeContext. - /// - /// The object to be written - /// The type of the object to be written. - /// The to be used for writing. - /// The . - Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext); - } + /// + /// Writes the given object specified by the parameter graph as a whole using the given messageWriter and writeContext. + /// + /// The object to be written + /// The type of the object to be written. + /// The to be used for writing. + /// The . + Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext); } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IODataSerializerProvider.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IODataSerializerProvider.cs index 358e79b26..c64fe9ca5 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IODataSerializerProvider.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IODataSerializerProvider.cs @@ -9,27 +9,26 @@ using Microsoft.AspNetCore.Http; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// An is a factory for creating s. +/// +public interface IODataSerializerProvider { /// - /// An is a factory for creating s. + /// Gets an for the given edmType. /// - public interface IODataSerializerProvider - { - /// - /// Gets an for the given edmType. - /// - /// The . - /// The . - public IODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType); + /// The . + /// The . + public IODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType); - /// - /// Gets an for the given . - /// - /// The for which the serializer is being requested. - /// The request for which the response is being serialized. - /// The for the given type. - /// This signature uses types that are AspNetCore-specific. - public IODataSerializer GetODataPayloadSerializer(Type type, HttpRequest request); - } + /// + /// Gets an for the given . + /// + /// The for which the serializer is being requested. + /// The request for which the response is being serialized. + /// The for the given type. + /// This signature uses types that are AspNetCore-specific. + public IODataSerializer GetODataPayloadSerializer(Type type, HttpRequest request); } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IUntypedResourceMapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IUntypedResourceMapper.cs index 24f28133b..8c4b2b566 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IUntypedResourceMapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/IUntypedResourceMapper.cs @@ -12,91 +12,90 @@ using System.Reflection; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// The mapper interface to map a resource to a dictionary of properties. +/// +public interface IUntypedResourceMapper { /// - /// The mapper interface to map a resource to a dictionary of properties. + /// Map the given object to a dictionary. /// - public interface IUntypedResourceMapper - { - /// - /// Map the given object to a dictionary. - /// - /// The given resource. - /// The serializer context. - /// The mapped dictionary. - IDictionary Map(object resource, ODataSerializerContext context); - } + /// The given resource. + /// The serializer context. + /// The mapped dictionary. + IDictionary Map(object resource, ODataSerializerContext context); +} + +/// +/// Default implementation of . +/// +public class DefaultUntypedResourceMapper : IUntypedResourceMapper +{ + /// + /// Gets the instance + /// + public static IUntypedResourceMapper Instance = new DefaultUntypedResourceMapper(); /// - /// Default implementation of . + /// Map the given object to a dictionary. /// - public class DefaultUntypedResourceMapper : IUntypedResourceMapper + /// The given resource. + /// The serializer context. + /// The mapped dictionary. + public virtual IDictionary Map(object resource, ODataSerializerContext context) { - /// - /// Gets the instance - /// - public static IUntypedResourceMapper Instance = new DefaultUntypedResourceMapper(); + IDictionary mapped = new Dictionary(); + if (resource == null) + { + return mapped; + } - /// - /// Map the given object to a dictionary. - /// - /// The given resource. - /// The serializer context. - /// The mapped dictionary. - public virtual IDictionary Map(object resource, ODataSerializerContext context) + Type originalType = resource.GetType(); + // Let's consider a dictionary is a resource (key and value pairs) + if (resource is IDictionary dict) { - IDictionary mapped = new Dictionary(); - if (resource == null) + foreach (var item in dict.Keys) { - return mapped; + // Now matter what type of the key, let's covert it to string + // If duplicated, the last wins. + mapped[item.ToString()] = dict[item]; } - Type originalType = resource.GetType(); - // Let's consider a dictionary is a resource (key and value pairs) - if (resource is IDictionary dict) - { - foreach (var item in dict.Keys) - { - // Now matter what type of the key, let's covert it to string - // If duplicated, the last wins. - mapped[item.ToString()] = dict[item]; - } - - return mapped; - } + return mapped; + } - IEnumerable propeInfos = - originalType.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(prop => prop.GetIndexParameters().Length == 0 && prop.GetMethod != null); + IEnumerable propeInfos = + originalType.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(prop => prop.GetIndexParameters().Length == 0 && prop.GetMethod != null); - foreach (PropertyInfo propInfo in propeInfos) + foreach (PropertyInfo propInfo in propeInfos) + { + JsonIgnoreAttribute jsonIgnore = GetJsonIgnore(propInfo); + if (jsonIgnore != null) { - JsonIgnoreAttribute jsonIgnore = GetJsonIgnore(propInfo); - if (jsonIgnore != null) - { - continue; - } - - string propertyName = propInfo.Name; - JsonPropertyNameAttribute jsonProperty = GetJsonProperty(propInfo); - if (jsonProperty != null && !string.IsNullOrWhiteSpace(jsonProperty.Name)) - { - propertyName = jsonProperty.Name; - } + continue; + } - mapped[propertyName] = propInfo.GetValue(resource); + string propertyName = propInfo.Name; + JsonPropertyNameAttribute jsonProperty = GetJsonProperty(propInfo); + if (jsonProperty != null && !string.IsNullOrWhiteSpace(jsonProperty.Name)) + { + propertyName = jsonProperty.Name; } - return mapped; + mapped[propertyName] = propInfo.GetValue(resource); } - private static JsonPropertyNameAttribute GetJsonProperty(PropertyInfo property) => - property.GetCustomAttributes(typeof(JsonPropertyNameAttribute), inherit: false) - .OfType().SingleOrDefault(); - - private static JsonIgnoreAttribute GetJsonIgnore(PropertyInfo property) => - property.GetCustomAttributes(typeof(JsonIgnoreAttribute), inherit: false) - .OfType().SingleOrDefault(); + return mapped; } + + private static JsonPropertyNameAttribute GetJsonProperty(PropertyInfo property) => + property.GetCustomAttributes(typeof(JsonPropertyNameAttribute), inherit: false) + .OfType().SingleOrDefault(); + + private static JsonIgnoreAttribute GetJsonIgnore(PropertyInfo property) => + property.GetCustomAttributes(typeof(JsonIgnoreAttribute), inherit: false) + .OfType().SingleOrDefault(); } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataCollectionSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataCollectionSerializer.cs index b6fd611cc..5ec676771 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataCollectionSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataCollectionSerializer.cs @@ -18,267 +18,266 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// ODataSerializer for serializing collection of primitive or enum types. +/// +public class ODataCollectionSerializer : ODataEdmTypeSerializer { /// - /// ODataSerializer for serializing collection of primitive or enum types. + /// Initializes a new instance of the class. /// - public class ODataCollectionSerializer : ODataEdmTypeSerializer + /// The serializer provider to use to serialize nested objects. + public ODataCollectionSerializer(IODataSerializerProvider serializerProvider) + : base(ODataPayloadKind.Collection, serializerProvider) + { + } + + /// + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, + ODataSerializerContext writeContext) { - /// - /// Initializes a new instance of the class. - /// - /// The serializer provider to use to serialize nested objects. - public ODataCollectionSerializer(IODataSerializerProvider serializerProvider) - : base(ODataPayloadKind.Collection, serializerProvider) + if (messageWriter == null) { + throw Error.ArgumentNull(nameof(messageWriter)); } - /// - public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, - ODataSerializerContext writeContext) + if (writeContext == null) { - if (messageWriter == null) - { - throw Error.ArgumentNull(nameof(messageWriter)); - } + throw Error.ArgumentNull(nameof(writeContext)); + } - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + IEdmTypeReference collectionType = writeContext.GetEdmType(graph, type); + Contract.Assert(collectionType != null); - IEdmTypeReference collectionType = writeContext.GetEdmType(graph, type); - Contract.Assert(collectionType != null); + IEdmTypeReference elementType = GetElementType(collectionType); + ODataCollectionWriter writer = await messageWriter.CreateODataCollectionWriterAsync(elementType) + .ConfigureAwait(false); + await WriteCollectionAsync(writer, graph, collectionType.AsCollection(), writeContext).ConfigureAwait(false); + } - IEdmTypeReference elementType = GetElementType(collectionType); - ODataCollectionWriter writer = await messageWriter.CreateODataCollectionWriterAsync(elementType) - .ConfigureAwait(false); - await WriteCollectionAsync(writer, graph, collectionType.AsCollection(), writeContext).ConfigureAwait(false); + /// + public sealed override ODataValue CreateODataValue(object graph, IEdmTypeReference expectedType, + ODataSerializerContext writeContext) + { + IEnumerable enumerable = graph as IEnumerable; + if (enumerable == null && graph != null) + { + throw Error.Argument(nameof(graph), SRResources.ArgumentMustBeOfType, typeof(IEnumerable).Name); } - /// - public sealed override ODataValue CreateODataValue(object graph, IEdmTypeReference expectedType, - ODataSerializerContext writeContext) + if (expectedType == null) { - IEnumerable enumerable = graph as IEnumerable; - if (enumerable == null && graph != null) - { - throw Error.Argument(nameof(graph), SRResources.ArgumentMustBeOfType, typeof(IEnumerable).Name); - } + throw Error.ArgumentNull(nameof(expectedType)); + } - if (expectedType == null) - { - throw Error.ArgumentNull(nameof(expectedType)); - } + IEdmTypeReference elementType = GetElementType(expectedType); + return CreateODataCollectionValue(enumerable, elementType, writeContext); + } - IEdmTypeReference elementType = GetElementType(expectedType); - return CreateODataCollectionValue(enumerable, elementType, writeContext); + /// + /// Writes the given using the given . + /// + /// The to use. + /// The collection to write. + /// The EDM type of the collection. + /// The serializer context. + public virtual async Task WriteCollectionAsync(ODataCollectionWriter writer, object graph, IEdmTypeReference collectionType, + ODataSerializerContext writeContext) + { + if (writer == null) + { + throw Error.ArgumentNull(nameof(writer)); } - /// - /// Writes the given using the given . - /// - /// The to use. - /// The collection to write. - /// The EDM type of the collection. - /// The serializer context. - public virtual async Task WriteCollectionAsync(ODataCollectionWriter writer, object graph, IEdmTypeReference collectionType, - ODataSerializerContext writeContext) + if (writeContext == null) { - if (writer == null) + throw Error.ArgumentNull(nameof(writeContext)); + } + + ODataCollectionStart collectionStart = new ODataCollectionStart { Name = writeContext.RootElementName }; + + if (writeContext.Request != null) + { + ODataFeature odataFeature = writeContext.Request.ODataFeature() as ODataFeature; + if (odataFeature.NextLink != null) { - throw Error.ArgumentNull(nameof(writer)); + collectionStart.NextPageLink = odataFeature.NextLink; } - - if (writeContext == null) + else if (odataFeature.QueryOptions != null) { - throw Error.ArgumentNull(nameof(writeContext)); + // Collection serializer is called only for collection of primitive values - A null object will be supplied since it is a non-entity value + SkipTokenHandler skipTokenHandler = writeContext.QueryOptions.Context.GetSkipTokenHandler(); + collectionStart.NextPageLink = skipTokenHandler.GenerateNextPageLink(new Uri(writeContext.Request.GetEncodedUrl()), odataFeature.PageSize, null, writeContext); } - ODataCollectionStart collectionStart = new ODataCollectionStart { Name = writeContext.RootElementName }; - - if (writeContext.Request != null) + if (odataFeature.TotalCount != null) { - ODataFeature odataFeature = writeContext.Request.ODataFeature() as ODataFeature; - if (odataFeature.NextLink != null) - { - collectionStart.NextPageLink = odataFeature.NextLink; - } - else if (odataFeature.QueryOptions != null) - { - // Collection serializer is called only for collection of primitive values - A null object will be supplied since it is a non-entity value - SkipTokenHandler skipTokenHandler = writeContext.QueryOptions.Context.GetSkipTokenHandler(); - collectionStart.NextPageLink = skipTokenHandler.GenerateNextPageLink(new Uri(writeContext.Request.GetEncodedUrl()), odataFeature.PageSize, null, writeContext); - } - - if (odataFeature.TotalCount != null) - { - collectionStart.Count = odataFeature.TotalCount; - } + collectionStart.Count = odataFeature.TotalCount; } + } - await writer.WriteStartAsync(collectionStart).ConfigureAwait(false); + await writer.WriteStartAsync(collectionStart).ConfigureAwait(false); - if (graph != null) + if (graph != null) + { + ODataCollectionValue collectionValue = CreateODataValue(graph, collectionType, writeContext) as ODataCollectionValue; + if (collectionValue != null) { - ODataCollectionValue collectionValue = CreateODataValue(graph, collectionType, writeContext) as ODataCollectionValue; - if (collectionValue != null) + foreach (object item in collectionValue.Items) { - foreach (object item in collectionValue.Items) - { - await writer.WriteItemAsync(item).ConfigureAwait(false); - } + await writer.WriteItemAsync(item).ConfigureAwait(false); } } - - await writer.WriteEndAsync().ConfigureAwait(false); } - /// - /// Creates an for the enumerable represented by . - /// - /// The value of the collection to be created. - /// The element EDM type of the collection. - /// The serializer context to be used while creating the collection. - /// The created . - public virtual ODataCollectionValue CreateODataCollectionValue(IEnumerable enumerable, IEdmTypeReference elementType, - ODataSerializerContext writeContext) + await writer.WriteEndAsync().ConfigureAwait(false); + } + + /// + /// Creates an for the enumerable represented by . + /// + /// The value of the collection to be created. + /// The element EDM type of the collection. + /// The serializer context to be used while creating the collection. + /// The created . + public virtual ODataCollectionValue CreateODataCollectionValue(IEnumerable enumerable, IEdmTypeReference elementType, + ODataSerializerContext writeContext) + { + if (elementType == null) { - if (elementType == null) - { - throw Error.ArgumentNull(nameof(elementType)); - } + throw Error.ArgumentNull(nameof(elementType)); + } - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + if (writeContext == null) + { + throw Error.ArgumentNull(nameof(writeContext)); + } - ArrayList valueCollection = new ArrayList(); + ArrayList valueCollection = new ArrayList(); - if (enumerable != null) + if (enumerable != null) + { + IODataEdmTypeSerializer itemSerializer = null; + foreach (object item in enumerable) { - IODataEdmTypeSerializer itemSerializer = null; - foreach (object item in enumerable) + if (item == null) { - if (item == null) + if (elementType.IsNullable) { - if (elementType.IsNullable) - { - valueCollection.Add(value: null); - continue; - } - - throw new SerializationException(SRResources.NullElementInCollection); + valueCollection.Add(value: null); + continue; } - IEdmTypeReference actualType = writeContext.GetEdmType(item, item.GetType()); - Contract.Assert(actualType != null); + throw new SerializationException(SRResources.NullElementInCollection); + } - itemSerializer = itemSerializer ?? SerializerProvider.GetEdmTypeSerializer(actualType); - if (itemSerializer == null) - { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeSerialized, actualType.FullName())); - } + IEdmTypeReference actualType = writeContext.GetEdmType(item, item.GetType()); + Contract.Assert(actualType != null); - // ODataCollectionWriter expects the individual elements in the collection to be the underlying - // values and not ODataValues. - valueCollection.Add( - itemSerializer.CreateODataValue(item, actualType, writeContext).GetInnerValue()); + itemSerializer = itemSerializer ?? SerializerProvider.GetEdmTypeSerializer(actualType); + if (itemSerializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, actualType.FullName())); } + + // ODataCollectionWriter expects the individual elements in the collection to be the underlying + // values and not ODataValues. + valueCollection.Add( + itemSerializer.CreateODataValue(item, actualType, writeContext).GetInnerValue()); } + } - // Ideally, we'd like to do this: - // string typeName = _edmCollectionType.FullName(); - // But ODataLib currently doesn't support .FullName() for collections. As a workaround, we construct the - // collection type name the hard way. - string typeName = "Collection(" + elementType.FullName() + ")"; + // Ideally, we'd like to do this: + // string typeName = _edmCollectionType.FullName(); + // But ODataLib currently doesn't support .FullName() for collections. As a workaround, we construct the + // collection type name the hard way. + string typeName = "Collection(" + elementType.FullName() + ")"; - // ODataCollectionValue is only a V3 property, arrays inside Complex Types or Entity types are only supported in V3 - // if a V1 or V2 Client requests a type that has a collection within it ODataLib will throw. - ODataCollectionValue value = new ODataCollectionValue - { - Items = valueCollection.Cast(), - TypeName = typeName - }; + // ODataCollectionValue is only a V3 property, arrays inside Complex Types or Entity types are only supported in V3 + // if a V1 or V2 Client requests a type that has a collection within it ODataLib will throw. + ODataCollectionValue value = new ODataCollectionValue + { + Items = valueCollection.Cast(), + TypeName = typeName + }; - AddTypeNameAnnotationAsNeeded(value, writeContext.MetadataLevel); - return value; - } + AddTypeNameAnnotationAsNeeded(value, writeContext.MetadataLevel); + return value; + } - /// - /// Adds the type name annotations required for proper json light serialization. - /// - /// The collection value for which the annotations have to be added. - /// The OData metadata level of the response. - protected internal static void AddTypeNameAnnotationAsNeeded(ODataCollectionValue value, ODataMetadataLevel metadataLevel) + /// + /// Adds the type name annotations required for proper json light serialization. + /// + /// The collection value for which the annotations have to be added. + /// The OData metadata level of the response. + protected internal static void AddTypeNameAnnotationAsNeeded(ODataCollectionValue value, ODataMetadataLevel metadataLevel) + { + // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties + // null when values should not be serialized. The TypeName property is different and should always be + // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not + // to serialize the type name (a null value prevents serialization). + Contract.Assert(value != null); + + // Only add an annotation if we want to override ODataLib's default type name serialization behavior. + if (ShouldAddTypeNameAnnotation(metadataLevel)) { - // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties - // null when values should not be serialized. The TypeName property is different and should always be - // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not - // to serialize the type name (a null value prevents serialization). - Contract.Assert(value != null); - - // Only add an annotation if we want to override ODataLib's default type name serialization behavior. - if (ShouldAddTypeNameAnnotation(metadataLevel)) - { - string typeName; - - // Provide the type name to serialize (or null to force it not to serialize). - if (ShouldSuppressTypeNameSerialization(metadataLevel)) - { - typeName = null; - } - else - { - typeName = value.TypeName; - } + string typeName; - value.TypeAnnotation = new ODataTypeAnnotation(typeName); + // Provide the type name to serialize (or null to force it not to serialize). + if (ShouldSuppressTypeNameSerialization(metadataLevel)) + { + typeName = null; } - } - - internal static bool ShouldAddTypeNameAnnotation(ODataMetadataLevel metadataLevel) - { - switch (metadataLevel) + else { - // For collections, the default behavior matches the requirements for minimal metadata mode, so no - // annotation is necessary. - case ODataMetadataLevel.Minimal: - return false; - // In other cases, this class must control the type name serialization behavior. - case ODataMetadataLevel.Full: - case ODataMetadataLevel.None: - default: // All values already specified; just keeping the compiler happy. - return true; + typeName = value.TypeName; } + + value.TypeAnnotation = new ODataTypeAnnotation(typeName); } + } - internal static bool ShouldSuppressTypeNameSerialization(ODataMetadataLevel metadataLevel) + internal static bool ShouldAddTypeNameAnnotation(ODataMetadataLevel metadataLevel) + { + switch (metadataLevel) { - Contract.Assert(metadataLevel != ODataMetadataLevel.Minimal); - - switch (metadataLevel) - { - case ODataMetadataLevel.None: - return true; - case ODataMetadataLevel.Full: - default: // All values already specified; just keeping the compiler happy. - return false; - } + // For collections, the default behavior matches the requirements for minimal metadata mode, so no + // annotation is necessary. + case ODataMetadataLevel.Minimal: + return false; + // In other cases, this class must control the type name serialization behavior. + case ODataMetadataLevel.Full: + case ODataMetadataLevel.None: + default: // All values already specified; just keeping the compiler happy. + return true; } + } + + internal static bool ShouldSuppressTypeNameSerialization(ODataMetadataLevel metadataLevel) + { + Contract.Assert(metadataLevel != ODataMetadataLevel.Minimal); - internal static IEdmTypeReference GetElementType(IEdmTypeReference feedType) + switch (metadataLevel) { - if (feedType.IsCollection()) - { - return feedType.AsCollection().ElementType(); - } + case ODataMetadataLevel.None: + return true; + case ODataMetadataLevel.Full: + default: // All values already specified; just keeping the compiler happy. + return false; + } + } - string message = Error.Format(SRResources.CannotWriteType, typeof(ODataCollectionSerializer).Name, feedType.FullName()); - throw new SerializationException(message); + internal static IEdmTypeReference GetElementType(IEdmTypeReference feedType) + { + if (feedType.IsCollection()) + { + return feedType.AsCollection().ElementType(); } + + string message = Error.Format(SRResources.CannotWriteType, typeof(ODataCollectionSerializer).Name, feedType.FullName()); + throw new SerializationException(message); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs index 3a4c37f0e..25b700958 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataDeltaResourceSetSerializer.cs @@ -18,401 +18,400 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// OData serializer for serializing a collection of +/// The Collection is of which is the base interface implemented by all objects which are a part of the DeltaResourceSet payload. +/// +public class ODataDeltaResourceSetSerializer : ODataEdmTypeSerializer { + private const string DeltaResourceSet = "DeltaResourceSet"; + /// - /// OData serializer for serializing a collection of - /// The Collection is of which is the base interface implemented by all objects which are a part of the DeltaResourceSet payload. + /// Initializes a new instance of . /// - public class ODataDeltaResourceSetSerializer : ODataEdmTypeSerializer + /// The to use to write nested entries. + public ODataDeltaResourceSetSerializer(IODataSerializerProvider serializerProvider) + : base(ODataPayloadKind.Delta, serializerProvider) { - private const string DeltaResourceSet = "DeltaResourceSet"; + } - /// - /// Initializes a new instance of . - /// - /// The to use to write nested entries. - public ODataDeltaResourceSetSerializer(IODataSerializerProvider serializerProvider) - : base(ODataPayloadKind.Delta, serializerProvider) + /// + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + { + if (messageWriter == null) { + throw Error.ArgumentNull(nameof(messageWriter)); } - /// - public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + if (writeContext == null) { - if (messageWriter == null) - { - throw Error.ArgumentNull(nameof(messageWriter)); - } + throw Error.ArgumentNull(nameof(writeContext)); + } - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + if (graph == null) + { + throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, DeltaResourceSet)); + } - if (graph == null) - { - throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, DeltaResourceSet)); - } + IEdmEntitySetBase entitySet = writeContext.NavigationSource as IEdmEntitySetBase; + if (entitySet == null) + { + throw new SerializationException(SRResources.EntitySetMissingDuringSerialization); + } - IEdmEntitySetBase entitySet = writeContext.NavigationSource as IEdmEntitySetBase; - if (entitySet == null) - { - throw new SerializationException(SRResources.EntitySetMissingDuringSerialization); - } + IEdmTypeReference feedType = writeContext.GetEdmType(graph, type); + Contract.Assert(feedType != null); - IEdmTypeReference feedType = writeContext.GetEdmType(graph, type); - Contract.Assert(feedType != null); + IEdmEntityTypeReference entityType = GetResourceType(feedType).AsEntity(); + ODataWriter writer = await messageWriter.CreateODataDeltaResourceSetWriterAsync(entitySet, entityType.EntityDefinition()) + .ConfigureAwait(false); - IEdmEntityTypeReference entityType = GetResourceType(feedType).AsEntity(); - ODataWriter writer = await messageWriter.CreateODataDeltaResourceSetWriterAsync(entitySet, entityType.EntityDefinition()) - .ConfigureAwait(false); + await WriteObjectInlineAsync(graph, feedType, writer, writeContext).ConfigureAwait(false); + } - await WriteObjectInlineAsync(graph, feedType, writer, writeContext).ConfigureAwait(false); + /// + public override async Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, + ODataSerializerContext writeContext) + { + if (writer == null) + { + throw Error.ArgumentNull(nameof(writer)); } - /// - public override async Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, - ODataSerializerContext writeContext) + if (writeContext == null) { - if (writer == null) - { - throw Error.ArgumentNull(nameof(writer)); - } + throw Error.ArgumentNull(nameof(writeContext)); + } - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + if (expectedType == null) + { + throw Error.ArgumentNull(nameof(expectedType)); + } - if (expectedType == null) - { - throw Error.ArgumentNull(nameof(expectedType)); - } + if (graph == null) + { + throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, DeltaResourceSet)); + } + + IEnumerable enumerable = graph as IEnumerable; // Data to serialize + if (enumerable == null) + { + throw new SerializationException( + Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName)); + } + + await WriteDeltaResourceSetAsync(enumerable, expectedType, writer, writeContext).ConfigureAwait(false); + } + + private async Task WriteDeltaResourceSetAsync(IEnumerable enumerable, IEdmTypeReference feedType, ODataWriter writer, + ODataSerializerContext writeContext) + { + Contract.Assert(writer != null); + Contract.Assert(writeContext != null); + Contract.Assert(enumerable != null); + Contract.Assert(feedType != null); - if (graph == null) + IEdmStructuredTypeReference elementType = GetResourceType(feedType); + + if (elementType.IsComplex()) + { + ODataResourceSet resourceSet = new ODataResourceSet() { - throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, DeltaResourceSet)); - } + TypeName = feedType.FullName() + }; + + await writer.WriteStartAsync(resourceSet).ConfigureAwait(false); - IEnumerable enumerable = graph as IEnumerable; // Data to serialize - if (enumerable == null) + ODataResourceSerializer entrySerializer = SerializerProvider.GetEdmTypeSerializer(elementType) as ODataResourceSerializer; + if (entrySerializer == null) { throw new SerializationException( - Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName)); + Error.Format(SRResources.TypeCannotBeSerialized, elementType.FullName())); } - await WriteDeltaResourceSetAsync(enumerable, expectedType, writer, writeContext).ConfigureAwait(false); + foreach (object entry in enumerable) + { + await entrySerializer.WriteDeltaObjectInlineAsync(entry, elementType, writer, writeContext).ConfigureAwait(false); + } } - - private async Task WriteDeltaResourceSetAsync(IEnumerable enumerable, IEdmTypeReference feedType, ODataWriter writer, - ODataSerializerContext writeContext) + else { - Contract.Assert(writer != null); - Contract.Assert(writeContext != null); - Contract.Assert(enumerable != null); - Contract.Assert(feedType != null); - - IEdmStructuredTypeReference elementType = GetResourceType(feedType); - - if (elementType.IsComplex()) + ODataDeltaResourceSet deltaResourceSet = CreateODataDeltaResourceSet(enumerable, feedType.AsCollection(), writeContext); + if (deltaResourceSet == null) { - ODataResourceSet resourceSet = new ODataResourceSet() - { - TypeName = feedType.FullName() - }; + throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, DeltaResourceSet)); + } - await writer.WriteStartAsync(resourceSet).ConfigureAwait(false); + // save the next page link for later to support JSON odata.streaming. + Func nextLinkGenerator = GetNextLinkGenerator(deltaResourceSet, enumerable, writeContext); + deltaResourceSet.NextPageLink = null; - ODataResourceSerializer entrySerializer = SerializerProvider.GetEdmTypeSerializer(elementType) as ODataResourceSerializer; - if (entrySerializer == null) - { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeSerialized, elementType.FullName())); - } + //Start writing of the Delta Feed + await writer.WriteStartAsync(deltaResourceSet).ConfigureAwait(false); - foreach (object entry in enumerable) - { - await entrySerializer.WriteDeltaObjectInlineAsync(entry, elementType, writer, writeContext).ConfigureAwait(false); - } - } - else + object lastResource = null; + //Iterate over all the entries present and select the appropriate write method. + //Write method creates ODataDeltaDeletedEntry / ODataDeltaDeletedLink / ODataDeltaLink or ODataEntry. + foreach (object item in enumerable) { - ODataDeltaResourceSet deltaResourceSet = CreateODataDeltaResourceSet(enumerable, feedType.AsCollection(), writeContext); - if (deltaResourceSet == null) + if (item == null) { - throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, DeltaResourceSet)); + throw new SerializationException(SRResources.NullElementInCollection); } - // save the next page link for later to support JSON odata.streaming. - Func nextLinkGenerator = GetNextLinkGenerator(deltaResourceSet, enumerable, writeContext); - deltaResourceSet.NextPageLink = null; - - //Start writing of the Delta Feed - await writer.WriteStartAsync(deltaResourceSet).ConfigureAwait(false); - - object lastResource = null; - //Iterate over all the entries present and select the appropriate write method. - //Write method creates ODataDeltaDeletedEntry / ODataDeltaDeletedLink / ODataDeltaLink or ODataEntry. - foreach (object item in enumerable) + lastResource = item; + DeltaItemKind kind = GetDelteItemKind(item); + switch (kind) { - if (item == null) - { - throw new SerializationException(SRResources.NullElementInCollection); - } - - lastResource = item; - DeltaItemKind kind = GetDelteItemKind(item); - switch (kind) - { - case DeltaItemKind.DeletedResource: - await WriteDeltaDeletedResourceAsync(item, writer, writeContext).ConfigureAwait(false); - break; - case DeltaItemKind.DeltaDeletedLink: - await WriteDeltaDeletedLinkAsync(item, writer, writeContext).ConfigureAwait(false); - break; - case DeltaItemKind.DeltaLink: - await WriteDeltaLinkAsync(item, writer, writeContext).ConfigureAwait(false); - break; - case DeltaItemKind.Resource: + case DeltaItemKind.DeletedResource: + await WriteDeltaDeletedResourceAsync(item, writer, writeContext).ConfigureAwait(false); + break; + case DeltaItemKind.DeltaDeletedLink: + await WriteDeltaDeletedLinkAsync(item, writer, writeContext).ConfigureAwait(false); + break; + case DeltaItemKind.DeltaLink: + await WriteDeltaLinkAsync(item, writer, writeContext).ConfigureAwait(false); + break; + case DeltaItemKind.Resource: + { + ODataResourceSerializer entrySerializer = SerializerProvider.GetEdmTypeSerializer(elementType) as ODataResourceSerializer; + if (entrySerializer == null) { - ODataResourceSerializer entrySerializer = SerializerProvider.GetEdmTypeSerializer(elementType) as ODataResourceSerializer; - if (entrySerializer == null) - { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeSerialized, elementType.FullName())); - } - await entrySerializer.WriteDeltaObjectInlineAsync(item, elementType, writer, writeContext) - .ConfigureAwait(false); - break; + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, elementType.FullName())); } - default: + await entrySerializer.WriteDeltaObjectInlineAsync(item, elementType, writer, writeContext) + .ConfigureAwait(false); break; - } + } + default: + break; } - - // Subtle and surprising behavior: If the NextPageLink property is set before calling WriteStart(feed), - // the next page link will be written early in a manner not compatible with odata.streaming=true. Instead, if - // the next page link is not set when calling WriteStart(feed) but is instead set later on that feed - // object before calling WriteEnd(), the next page link will be written at the end, as required for - // odata.streaming=true support. - - deltaResourceSet.NextPageLink = nextLinkGenerator(lastResource); } - //End Writing of the Delta Feed - await writer.WriteEndAsync().ConfigureAwait(false); + // Subtle and surprising behavior: If the NextPageLink property is set before calling WriteStart(feed), + // the next page link will be written early in a manner not compatible with odata.streaming=true. Instead, if + // the next page link is not set when calling WriteStart(feed) but is instead set later on that feed + // object before calling WriteEnd(), the next page link will be written at the end, as required for + // odata.streaming=true support. + + deltaResourceSet.NextPageLink = nextLinkGenerator(lastResource); } - /// - /// Creates a function that takes in an object and generates nextlink uri. - /// - /// The resource set describing a collection of structured objects. - /// >The instance representing the resourceSet being written. - /// The serializer context. - /// The function that generates the NextLink from an object. - /// - internal static Func GetNextLinkGenerator(ODataDeltaResourceSet deltaResourceSet, IEnumerable enumerable, ODataSerializerContext writeContext) + //End Writing of the Delta Feed + await writer.WriteEndAsync().ConfigureAwait(false); + } + + /// + /// Creates a function that takes in an object and generates nextlink uri. + /// + /// The resource set describing a collection of structured objects. + /// >The instance representing the resourceSet being written. + /// The serializer context. + /// The function that generates the NextLink from an object. + /// + internal static Func GetNextLinkGenerator(ODataDeltaResourceSet deltaResourceSet, IEnumerable enumerable, ODataSerializerContext writeContext) + { + return ODataResourceSetSerializer.GetNextLinkGenerator(deltaResourceSet, enumerable, writeContext); + } + + /// + /// Create the to be written for the given feed instance. + /// + /// The instance representing the feed being written. + /// The EDM type of the feed being written. + /// The serializer context. + /// The created object. + public virtual ODataDeltaResourceSet CreateODataDeltaResourceSet(IEnumerable feedInstance, IEdmCollectionTypeReference feedType, + ODataSerializerContext writeContext) + { + if (writeContext == null) { - return ODataResourceSetSerializer.GetNextLinkGenerator(deltaResourceSet, enumerable, writeContext); + throw Error.ArgumentNull(nameof(writeContext)); } - /// - /// Create the to be written for the given feed instance. - /// - /// The instance representing the feed being written. - /// The EDM type of the feed being written. - /// The serializer context. - /// The created object. - public virtual ODataDeltaResourceSet CreateODataDeltaResourceSet(IEnumerable feedInstance, IEdmCollectionTypeReference feedType, - ODataSerializerContext writeContext) + ODataDeltaResourceSet feed = new ODataDeltaResourceSet(); + + if (writeContext.ExpandedResource == null) { - if (writeContext == null) + // If we have more OData format specific information apply it now, only if we are the root feed. + PageResult odataFeedAnnotations = feedInstance as PageResult; + if (odataFeedAnnotations != null) { - throw Error.ArgumentNull(nameof(writeContext)); + feed.Count = odataFeedAnnotations.Count; + feed.NextPageLink = odataFeedAnnotations.NextPageLink; } - - ODataDeltaResourceSet feed = new ODataDeltaResourceSet(); - - if (writeContext.ExpandedResource == null) + else if (writeContext.Request != null) { - // If we have more OData format specific information apply it now, only if we are the root feed. - PageResult odataFeedAnnotations = feedInstance as PageResult; - if (odataFeedAnnotations != null) - { - feed.Count = odataFeedAnnotations.Count; - feed.NextPageLink = odataFeedAnnotations.NextPageLink; - } - else if (writeContext.Request != null) + IODataFeature odataFeature = writeContext.Request.ODataFeature(); + feed.NextPageLink = odataFeature.NextLink; + feed.DeltaLink = odataFeature.DeltaLink; + + long? countValue = odataFeature.TotalCount; + if (countValue.HasValue) { - IODataFeature odataFeature = writeContext.Request.ODataFeature(); - feed.NextPageLink = odataFeature.NextLink; - feed.DeltaLink = odataFeature.DeltaLink; - - long? countValue = odataFeature.TotalCount; - if (countValue.HasValue) - { - feed.Count = countValue.Value; - } + feed.Count = countValue.Value; } } - return feed; } + return feed; + } - /// - /// Writes the given deltaDeletedEntry specified by the parameter graph as a part of an existing OData message using the given - /// messageWriter and the writeContext. - /// - /// The object to be written. - /// The to be used for writing. - /// The . - public virtual async Task WriteDeltaDeletedResourceAsync(object value, ODataWriter writer, ODataSerializerContext writeContext) + /// + /// Writes the given deltaDeletedEntry specified by the parameter graph as a part of an existing OData message using the given + /// messageWriter and the writeContext. + /// + /// The object to be written. + /// The to be used for writing. + /// The . + public virtual async Task WriteDeltaDeletedResourceAsync(object value, ODataWriter writer, ODataSerializerContext writeContext) + { + if (writer == null) { - if (writer == null) - { - throw Error.ArgumentNull(nameof(writer)); - } - - ODataDeletedResource odataDeletedResource; + throw Error.ArgumentNull(nameof(writer)); + } - if (value is EdmDeltaDeletedResourceObject edmDeltaDeletedEntity) - { - odataDeletedResource = new ODataDeletedResource(edmDeltaDeletedEntity.Id, edmDeltaDeletedEntity.Reason ?? DeltaDeletedEntryReason.Deleted); + ODataDeletedResource odataDeletedResource; - if (edmDeltaDeletedEntity.NavigationSource != null) - { - ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo - { - NavigationSourceName = edmDeltaDeletedEntity.NavigationSource.Name - }; - odataDeletedResource.SetSerializationInfo(serializationInfo); - } - } - else if (value is IDeltaDeletedResource deltaDeletedResource) - { - odataDeletedResource = new ODataDeletedResource(deltaDeletedResource.Id, deltaDeletedResource.Reason ?? DeltaDeletedEntryReason.Deleted); - } - else - { - throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, value?.GetType().FullName)); - } + if (value is EdmDeltaDeletedResourceObject edmDeltaDeletedEntity) + { + odataDeletedResource = new ODataDeletedResource(edmDeltaDeletedEntity.Id, edmDeltaDeletedEntity.Reason ?? DeltaDeletedEntryReason.Deleted); - if (odataDeletedResource != null) + if (edmDeltaDeletedEntity.NavigationSource != null) { - await writer.WriteStartAsync(odataDeletedResource).ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); + ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo + { + NavigationSourceName = edmDeltaDeletedEntity.NavigationSource.Name + }; + odataDeletedResource.SetSerializationInfo(serializationInfo); } } + else if (value is IDeltaDeletedResource deltaDeletedResource) + { + odataDeletedResource = new ODataDeletedResource(deltaDeletedResource.Id, deltaDeletedResource.Reason ?? DeltaDeletedEntryReason.Deleted); + } + else + { + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, value?.GetType().FullName)); + } - /// - /// Writes the given deltaDeletedLink specified by the parameter graph as a part of an existing OData message using the given - /// messageWriter and the writeContext. - /// - /// The object to be written. - /// The to be used for writing. - /// The . - public virtual async Task WriteDeltaDeletedLinkAsync(object value, ODataWriter writer, ODataSerializerContext writeContext) + if (odataDeletedResource != null) { - if (value == null) - { - throw Error.ArgumentNull(nameof(value)); - } + await writer.WriteStartAsync(odataDeletedResource).ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); + } + } - if (writer == null) - { - throw Error.ArgumentNull(nameof(writer)); - } + /// + /// Writes the given deltaDeletedLink specified by the parameter graph as a part of an existing OData message using the given + /// messageWriter and the writeContext. + /// + /// The object to be written. + /// The to be used for writing. + /// The . + public virtual async Task WriteDeltaDeletedLinkAsync(object value, ODataWriter writer, ODataSerializerContext writeContext) + { + if (value == null) + { + throw Error.ArgumentNull(nameof(value)); + } - ODataDeltaDeletedLink odataDeltaDeletedLink; - if (value is EdmDeltaDeletedLink edmDeltaDeletedLink) - { - odataDeltaDeletedLink = new ODataDeltaDeletedLink(edmDeltaDeletedLink.Source, edmDeltaDeletedLink.Target, edmDeltaDeletedLink.Relationship); - } - else if (value is IDeltaDeletedLink deltaDeletedLink) - { - odataDeltaDeletedLink = new ODataDeltaDeletedLink(deltaDeletedLink.Source, deltaDeletedLink.Target, deltaDeletedLink.Relationship); - } - else - { - throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, value.GetType().FullName)); - } + if (writer == null) + { + throw Error.ArgumentNull(nameof(writer)); + } - if (odataDeltaDeletedLink != null) - { - await writer.WriteDeltaDeletedLinkAsync(odataDeltaDeletedLink).ConfigureAwait(false); - } + ODataDeltaDeletedLink odataDeltaDeletedLink; + if (value is EdmDeltaDeletedLink edmDeltaDeletedLink) + { + odataDeltaDeletedLink = new ODataDeltaDeletedLink(edmDeltaDeletedLink.Source, edmDeltaDeletedLink.Target, edmDeltaDeletedLink.Relationship); + } + else if (value is IDeltaDeletedLink deltaDeletedLink) + { + odataDeltaDeletedLink = new ODataDeltaDeletedLink(deltaDeletedLink.Source, deltaDeletedLink.Target, deltaDeletedLink.Relationship); + } + else + { + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, value.GetType().FullName)); } - /// - /// Writes the given deltaLink specified by the parameter graph as a part of an existing OData message using the given - /// messageWriter and the writeContext. - /// - /// The object to be written. - /// The to be used for writing. - /// The . - public virtual async Task WriteDeltaLinkAsync(object value, ODataWriter writer, ODataSerializerContext writeContext) + if (odataDeltaDeletedLink != null) { - if (value == null) - { - throw Error.ArgumentNull(nameof(value)); - } + await writer.WriteDeltaDeletedLinkAsync(odataDeltaDeletedLink).ConfigureAwait(false); + } + } - if (writer == null) - { - throw Error.ArgumentNull(nameof(writer)); - } + /// + /// Writes the given deltaLink specified by the parameter graph as a part of an existing OData message using the given + /// messageWriter and the writeContext. + /// + /// The object to be written. + /// The to be used for writing. + /// The . + public virtual async Task WriteDeltaLinkAsync(object value, ODataWriter writer, ODataSerializerContext writeContext) + { + if (value == null) + { + throw Error.ArgumentNull(nameof(value)); + } - ODataDeltaLink odataDeltaLink; - if (value is EdmDeltaLink edmDeltaLink) // typeless - { - odataDeltaLink = new ODataDeltaLink(edmDeltaLink.Source, edmDeltaLink.Target, edmDeltaLink.Relationship); - } - else if (value is IDeltaLink deltaLink) // typed - { - odataDeltaLink = new ODataDeltaLink(deltaLink.Source, deltaLink.Target, deltaLink.Relationship); - } - else - { - throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, value.GetType().FullName)); - } + if (writer == null) + { + throw Error.ArgumentNull(nameof(writer)); + } - if (odataDeltaLink != null) - { - await writer.WriteDeltaLinkAsync(odataDeltaLink).ConfigureAwait(false); - } + ODataDeltaLink odataDeltaLink; + if (value is EdmDeltaLink edmDeltaLink) // typeless + { + odataDeltaLink = new ODataDeltaLink(edmDeltaLink.Source, edmDeltaLink.Target, edmDeltaLink.Relationship); + } + else if (value is IDeltaLink deltaLink) // typed + { + odataDeltaLink = new ODataDeltaLink(deltaLink.Source, deltaLink.Target, deltaLink.Relationship); + } + else + { + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, value.GetType().FullName)); } - internal DeltaItemKind GetDelteItemKind(object item) + if (odataDeltaLink != null) { - IEdmChangedObject edmChangedObject = item as IEdmChangedObject; - if (edmChangedObject != null) - { - return edmChangedObject.Kind; - } + await writer.WriteDeltaLinkAsync(odataDeltaLink).ConfigureAwait(false); + } + } - IDeltaSetItem deltaSetItem = item as IDeltaSetItem; - if (deltaSetItem != null) - { - return deltaSetItem.Kind; - } + internal DeltaItemKind GetDelteItemKind(object item) + { + IEdmChangedObject edmChangedObject = item as IEdmChangedObject; + if (edmChangedObject != null) + { + return edmChangedObject.Kind; + } - throw new SerializationException(Error.Format( - SRResources.CannotWriteType, GetType().Name, item.GetType().FullName)); + IDeltaSetItem deltaSetItem = item as IDeltaSetItem; + if (deltaSetItem != null) + { + return deltaSetItem.Kind; } - private static IEdmStructuredTypeReference GetResourceType(IEdmTypeReference feedType) + throw new SerializationException(Error.Format( + SRResources.CannotWriteType, GetType().Name, item.GetType().FullName)); + } + + private static IEdmStructuredTypeReference GetResourceType(IEdmTypeReference feedType) + { + if (feedType.IsCollection()) { - if (feedType.IsCollection()) + IEdmTypeReference elementType = feedType.AsCollection().ElementType(); + if (elementType.IsEntity() || elementType.IsComplex()) { - IEdmTypeReference elementType = feedType.AsCollection().ElementType(); - if (elementType.IsEntity() || elementType.IsComplex()) - { - return elementType.AsStructured(); - } + return elementType.AsStructured(); } - - string message = Error.Format(SRResources.CannotWriteType, typeof(ODataDeltaResourceSetSerializer).Name, feedType.FullName()); - throw new SerializationException(message); } + + string message = Error.Format(SRResources.CannotWriteType, typeof(ODataDeltaResourceSetSerializer).Name, feedType.FullName()); + throw new SerializationException(message); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEdmTypeSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEdmTypeSerializer.cs index ad24c5571..13e105780 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEdmTypeSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEdmTypeSerializer.cs @@ -10,49 +10,48 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// Represents an that serializes instances of objects backed by an . +/// +public abstract class ODataEdmTypeSerializer : ODataSerializer, IODataEdmTypeSerializer { /// - /// Represents an that serializes instances of objects backed by an . + /// Initializes a new instance of the class. /// - public abstract class ODataEdmTypeSerializer : ODataSerializer, IODataEdmTypeSerializer + /// The kind of OData payload that this serializer generates. + protected ODataEdmTypeSerializer(ODataPayloadKind payloadKind) + : base(payloadKind) { - /// - /// Initializes a new instance of the class. - /// - /// The kind of OData payload that this serializer generates. - protected ODataEdmTypeSerializer(ODataPayloadKind payloadKind) - : base(payloadKind) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The kind of OData payload that this serializer generates. - /// The to use to write inner objects. - protected ODataEdmTypeSerializer(ODataPayloadKind payloadKind, IODataSerializerProvider serializerProvider) - : this(payloadKind) - { - SerializerProvider = serializerProvider ?? throw Error.ArgumentNull(nameof(serializerProvider)); - } + /// + /// Initializes a new instance of the class. + /// + /// The kind of OData payload that this serializer generates. + /// The to use to write inner objects. + protected ODataEdmTypeSerializer(ODataPayloadKind payloadKind, IODataSerializerProvider serializerProvider) + : this(payloadKind) + { + SerializerProvider = serializerProvider ?? throw Error.ArgumentNull(nameof(serializerProvider)); + } - /// - /// Gets the that can be used to write inner objects. - /// - public IODataSerializerProvider SerializerProvider { get; } + /// + /// Gets the that can be used to write inner objects. + /// + public IODataSerializerProvider SerializerProvider { get; } - /// - public virtual Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, - ODataSerializerContext writeContext) - { - throw Error.NotSupported(SRResources.WriteObjectInlineNotSupported, GetType().Name); - } + /// + public virtual Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, + ODataSerializerContext writeContext) + { + throw Error.NotSupported(SRResources.WriteObjectInlineNotSupported, GetType().Name); + } - /// - public virtual ODataValue CreateODataValue(object graph, IEdmTypeReference expectedType, ODataSerializerContext writeContext) - { - throw Error.NotSupported(SRResources.CreateODataValueNotSupported, GetType().Name); - } + /// + public virtual ODataValue CreateODataValue(object graph, IEdmTypeReference expectedType, ODataSerializerContext writeContext) + { + throw Error.NotSupported(SRResources.CreateODataValueNotSupported, GetType().Name); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEntityReferenceLinkSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEntityReferenceLinkSerializer.cs index 3c7a4bab6..847e8886b 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEntityReferenceLinkSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEntityReferenceLinkSerializer.cs @@ -10,51 +10,50 @@ using System.Threading.Tasks; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// Represents an for serializing $ref response. +/// +// For example, the response to the url http://localhost/Products(10)/Category/$ref gets serialized using this. +public class ODataEntityReferenceLinkSerializer : ODataSerializer { /// - /// Represents an for serializing $ref response. + /// Initializes a new instance of . /// - // For example, the response to the url http://localhost/Products(10)/Category/$ref gets serialized using this. - public class ODataEntityReferenceLinkSerializer : ODataSerializer + public ODataEntityReferenceLinkSerializer() + : base(ODataPayloadKind.EntityReferenceLink) + { + } + + /// + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) { - /// - /// Initializes a new instance of . - /// - public ODataEntityReferenceLinkSerializer() - : base(ODataPayloadKind.EntityReferenceLink) + if (messageWriter == null) { + throw Error.ArgumentNull(nameof(messageWriter)); } - /// - public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + if (writeContext == null) { - if (messageWriter == null) - { - throw Error.ArgumentNull(nameof(messageWriter)); - } - - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + throw Error.ArgumentNull(nameof(writeContext)); + } - if (graph != null) + if (graph != null) + { + ODataEntityReferenceLink entityReferenceLink = graph as ODataEntityReferenceLink; + if (entityReferenceLink == null) { - ODataEntityReferenceLink entityReferenceLink = graph as ODataEntityReferenceLink; - if (entityReferenceLink == null) + Uri uri = graph as Uri; + if (uri == null) { - Uri uri = graph as Uri; - if (uri == null) - { - throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName)); - } - - entityReferenceLink = new ODataEntityReferenceLink { Url = uri }; + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName)); } - await messageWriter.WriteEntityReferenceLinkAsync(entityReferenceLink).ConfigureAwait(false); + entityReferenceLink = new ODataEntityReferenceLink { Url = uri }; } + + await messageWriter.WriteEntityReferenceLinkAsync(entityReferenceLink).ConfigureAwait(false); } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEntityReferenceLinksSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEntityReferenceLinksSerializer.cs index 72a01c1f9..fc41b3f25 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEntityReferenceLinksSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEntityReferenceLinksSerializer.cs @@ -13,58 +13,57 @@ using Microsoft.AspNetCore.OData.Extensions; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// Represents an for serializing $ref response for a collection navigation property. +/// +public class ODataEntityReferenceLinksSerializer : ODataSerializer { /// - /// Represents an for serializing $ref response for a collection navigation property. + /// Initializes a new instance of the class. /// - public class ODataEntityReferenceLinksSerializer : ODataSerializer + public ODataEntityReferenceLinksSerializer() + : base(ODataPayloadKind.EntityReferenceLinks) + { + } + + /// + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) { - /// - /// Initializes a new instance of the class. - /// - public ODataEntityReferenceLinksSerializer() - : base(ODataPayloadKind.EntityReferenceLinks) + if (messageWriter == null) { + throw Error.ArgumentNull(nameof(messageWriter)); } - /// - public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + if (writeContext == null) { - if (messageWriter == null) - { - throw Error.ArgumentNull(nameof(messageWriter)); - } - - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + throw Error.ArgumentNull(nameof(writeContext)); + } - if (graph != null) + if (graph != null) + { + ODataEntityReferenceLinks entityReferenceLinks = graph as ODataEntityReferenceLinks; + if (entityReferenceLinks == null) { - ODataEntityReferenceLinks entityReferenceLinks = graph as ODataEntityReferenceLinks; - if (entityReferenceLinks == null) + IEnumerable uris = graph as IEnumerable; + if (uris == null) { - IEnumerable uris = graph as IEnumerable; - if (uris == null) - { - throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName)); - } + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName)); + } - entityReferenceLinks = new ODataEntityReferenceLinks - { - Links = uris.Select(uri => new ODataEntityReferenceLink { Url = uri }) - }; + entityReferenceLinks = new ODataEntityReferenceLinks + { + Links = uris.Select(uri => new ODataEntityReferenceLink { Url = uri }) + }; - if (writeContext.Request != null) - { - entityReferenceLinks.Count = writeContext.Request.ODataFeature().TotalCount; - } + if (writeContext.Request != null) + { + entityReferenceLinks.Count = writeContext.Request.ODataFeature().TotalCount; } - - await messageWriter.WriteEntityReferenceLinksAsync(entityReferenceLinks).ConfigureAwait(false); } + + await messageWriter.WriteEntityReferenceLinksAsync(entityReferenceLinks).ConfigureAwait(false); } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs index 1f5104d64..77e949498 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs @@ -14,162 +14,161 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// Represents an for serializing 's. +/// +public class ODataEnumSerializer : ODataEdmTypeSerializer { /// - /// Represents an for serializing 's. + /// Initializes a new instance of . /// - public class ODataEnumSerializer : ODataEdmTypeSerializer + public ODataEnumSerializer(IODataSerializerProvider serializerProvider) + : base(ODataPayloadKind.Property, serializerProvider) + { + } + + /// + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) { - /// - /// Initializes a new instance of . - /// - public ODataEnumSerializer(IODataSerializerProvider serializerProvider) - : base(ODataPayloadKind.Property, serializerProvider) + if (messageWriter == null) { + throw Error.ArgumentNull(nameof(messageWriter)); } - /// - public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + if (writeContext == null) { - if (messageWriter == null) - { - throw Error.ArgumentNull(nameof(messageWriter)); - } + throw Error.ArgumentNull(nameof(writeContext)); + } - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + if (writeContext.RootElementName == null) + { + throw Error.Argument(nameof(writeContext), SRResources.RootElementNameMissing, typeof(ODataSerializerContext).Name); + } - if (writeContext.RootElementName == null) - { - throw Error.Argument(nameof(writeContext), SRResources.RootElementNameMissing, typeof(ODataSerializerContext).Name); - } + IEdmTypeReference edmType = writeContext.GetEdmType(graph, type); + Contract.Assert(edmType != null); - IEdmTypeReference edmType = writeContext.GetEdmType(graph, type); - Contract.Assert(edmType != null); + await messageWriter.WritePropertyAsync(this.CreateProperty(graph, edmType, writeContext.RootElementName, writeContext)).ConfigureAwait(false); + } - await messageWriter.WritePropertyAsync(this.CreateProperty(graph, edmType, writeContext.RootElementName, writeContext)).ConfigureAwait(false); + /// + public sealed override ODataValue CreateODataValue(object graph, IEdmTypeReference expectedType, ODataSerializerContext writeContext) + { + if (!expectedType.IsEnum()) + { + throw Error.InvalidOperation(SRResources.CannotWriteType, typeof(ODataEnumSerializer).Name, expectedType.FullName()); } - /// - public sealed override ODataValue CreateODataValue(object graph, IEdmTypeReference expectedType, ODataSerializerContext writeContext) + ODataEnumValue value = CreateODataEnumValue(graph, expectedType.AsEnum(), writeContext); + if (value == null) { - if (!expectedType.IsEnum()) - { - throw Error.InvalidOperation(SRResources.CannotWriteType, typeof(ODataEnumSerializer).Name, expectedType.FullName()); - } + return ODataNullValueExtensions.NullValue; + } - ODataEnumValue value = CreateODataEnumValue(graph, expectedType.AsEnum(), writeContext); - if (value == null) - { - return ODataNullValueExtensions.NullValue; - } + return value; + } - return value; + /// + /// Creates an for the object represented by . + /// + /// The enum value. + /// The EDM enum type of the value. + /// The serializer write context. + /// The created . + public virtual ODataEnumValue CreateODataEnumValue(object graph, IEdmEnumTypeReference enumType, + ODataSerializerContext writeContext) + { + if (graph == null) + { + return null; } - /// - /// Creates an for the object represented by . - /// - /// The enum value. - /// The EDM enum type of the value. - /// The serializer write context. - /// The created . - public virtual ODataEnumValue CreateODataEnumValue(object graph, IEdmEnumTypeReference enumType, - ODataSerializerContext writeContext) + string value = null; + if (TypeHelper.IsEnum(graph.GetType())) + { + value = graph.ToString(); + } + else { - if (graph == null) + if (graph.GetType() == typeof(EdmEnumObject)) { - return null; + value = ((EdmEnumObject)graph).Value; } + } - string value = null; - if (TypeHelper.IsEnum(graph.GetType())) - { - value = graph.ToString(); - } - else + // Enum member supports model alias case. So, try to use the Edm member name to create Enum value. + var memberMapAnnotation = writeContext?.Model.GetClrEnumMemberAnnotation(enumType.EnumDefinition()); + if (memberMapAnnotation != null) + { + var edmEnumMember = memberMapAnnotation.GetEdmEnumMember((Enum)graph); + if (edmEnumMember != null) { - if (graph.GetType() == typeof(EdmEnumObject)) - { - value = ((EdmEnumObject)graph).Value; - } + value = edmEnumMember.Name; } + } - // Enum member supports model alias case. So, try to use the Edm member name to create Enum value. - var memberMapAnnotation = writeContext?.Model.GetClrEnumMemberAnnotation(enumType.EnumDefinition()); - if (memberMapAnnotation != null) - { - var edmEnumMember = memberMapAnnotation.GetEdmEnumMember((Enum)graph); - if (edmEnumMember != null) - { - value = edmEnumMember.Name; - } - } + ODataEnumValue enumValue = new ODataEnumValue(value, enumType.FullName()); - ODataEnumValue enumValue = new ODataEnumValue(value, enumType.FullName()); + ODataMetadataLevel metadataLevel = writeContext != null ? writeContext.MetadataLevel : ODataMetadataLevel.Minimal; + AddTypeNameAnnotationAsNeeded(enumValue, enumType, metadataLevel); - ODataMetadataLevel metadataLevel = writeContext != null ? writeContext.MetadataLevel : ODataMetadataLevel.Minimal; - AddTypeNameAnnotationAsNeeded(enumValue, enumType, metadataLevel); + return enumValue; + } - return enumValue; - } + internal static void AddTypeNameAnnotationAsNeeded(ODataEnumValue enumValue, IEdmEnumTypeReference enumType, ODataMetadataLevel metadataLevel) + { + // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties + // null when values should not be serialized. The TypeName property is different and should always be + // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not + // to serialize the type name (a null value prevents serialization). - internal static void AddTypeNameAnnotationAsNeeded(ODataEnumValue enumValue, IEdmEnumTypeReference enumType, ODataMetadataLevel metadataLevel) - { - // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties - // null when values should not be serialized. The TypeName property is different and should always be - // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not - // to serialize the type name (a null value prevents serialization). + Contract.Assert(enumValue != null); - Contract.Assert(enumValue != null); + // Only add an annotation if we want to override ODataLib's default type name serialization behavior. + if (ShouldAddTypeNameAnnotation(metadataLevel)) + { + string typeName; - // Only add an annotation if we want to override ODataLib's default type name serialization behavior. - if (ShouldAddTypeNameAnnotation(metadataLevel)) + // Provide the type name to serialize (or null to force it not to serialize). + if (ShouldSuppressTypeNameSerialization(metadataLevel)) { - string typeName; - - // Provide the type name to serialize (or null to force it not to serialize). - if (ShouldSuppressTypeNameSerialization(metadataLevel)) - { - typeName = null; - } - else - { - typeName = enumType.FullName(); - } - - enumValue.TypeAnnotation = new ODataTypeAnnotation(typeName); + typeName = null; } - } - - private static bool ShouldAddTypeNameAnnotation(ODataMetadataLevel metadataLevel) - { - switch (metadataLevel) + else { - case ODataMetadataLevel.Minimal: - return false; - case ODataMetadataLevel.Full: - case ODataMetadataLevel.None: - default: - return true; + typeName = enumType.FullName(); } + + enumValue.TypeAnnotation = new ODataTypeAnnotation(typeName); } + } - private static bool ShouldSuppressTypeNameSerialization(ODataMetadataLevel metadataLevel) + private static bool ShouldAddTypeNameAnnotation(ODataMetadataLevel metadataLevel) + { + switch (metadataLevel) { - Contract.Assert(metadataLevel != ODataMetadataLevel.Minimal); + case ODataMetadataLevel.Minimal: + return false; + case ODataMetadataLevel.Full: + case ODataMetadataLevel.None: + default: + return true; + } + } - switch (metadataLevel) - { - case ODataMetadataLevel.None: - return true; - case ODataMetadataLevel.Full: - default: - return false; - } + private static bool ShouldSuppressTypeNameSerialization(ODataMetadataLevel metadataLevel) + { + Contract.Assert(metadataLevel != ODataMetadataLevel.Minimal); + + switch (metadataLevel) + { + case ODataMetadataLevel.None: + return true; + case ODataMetadataLevel.Full: + default: + return false; } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataErrorSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataErrorSerializer.cs index 5efb14e04..4c64b9db3 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataErrorSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataErrorSerializer.cs @@ -12,71 +12,70 @@ using Microsoft.AspNetCore.OData.Extensions; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// Represents an to serialize s. +/// +public class ODataErrorSerializer : ODataSerializer { /// - /// Represents an to serialize s. + /// Initializes a new instance of the class . /// - public class ODataErrorSerializer : ODataSerializer + public ODataErrorSerializer() + : base(ODataPayloadKind.Error) + { + } + + /// + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) { - /// - /// Initializes a new instance of the class . - /// - public ODataErrorSerializer() - : base(ODataPayloadKind.Error) + if (graph == null) { + throw Error.ArgumentNull(nameof(graph)); } - /// - public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + if (messageWriter == null) { - if (graph == null) - { - throw Error.ArgumentNull(nameof(graph)); - } + throw Error.ArgumentNull(nameof(messageWriter)); + } - if (messageWriter == null) + ODataError oDataError = graph as ODataError; + if (oDataError == null) + { + if (!IsHttpError(graph)) { - throw Error.ArgumentNull(nameof(messageWriter)); + throw new SerializationException( + Error.Format(SRResources.ErrorTypeMustBeODataErrorOrHttpError, graph.GetType().FullName)); } - - ODataError oDataError = graph as ODataError; - if (oDataError == null) + else { - if (!IsHttpError(graph)) - { - throw new SerializationException( - Error.Format(SRResources.ErrorTypeMustBeODataErrorOrHttpError, graph.GetType().FullName)); - } - else - { - oDataError = CreateODataError(graph); - } + oDataError = CreateODataError(graph); } - - bool includeDebugInformation = oDataError.InnerError != null; - await messageWriter.WriteErrorAsync(oDataError, includeDebugInformation).ConfigureAwait(false); } - /// - /// Return true if the object is an HttpError. - /// - /// The error to test. - /// true if the object is an HttpError - internal static bool IsHttpError(object error) - { - return error is SerializableError; - } + bool includeDebugInformation = oDataError.InnerError != null; + await messageWriter.WriteErrorAsync(oDataError, includeDebugInformation).ConfigureAwait(false); + } - /// - /// Create an ODataError from an HttpError. - /// - /// The error to use. - /// an ODataError. - internal static ODataError CreateODataError(object error) - { - SerializableError serializableError = error as SerializableError; - return serializableError.CreateODataError(); - } + /// + /// Return true if the object is an HttpError. + /// + /// The error to test. + /// true if the object is an HttpError + internal static bool IsHttpError(object error) + { + return error is SerializableError; + } + + /// + /// Create an ODataError from an HttpError. + /// + /// The error to use. + /// an ODataError. + internal static ODataError CreateODataError(object error) + { + SerializableError serializableError = error as SerializableError; + return serializableError.CreateODataError(); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataMetadataSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataMetadataSerializer.cs index 4faada39e..be59dee5d 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataMetadataSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataMetadataSerializer.cs @@ -9,34 +9,33 @@ using System.Threading.Tasks; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// Represents an for serializing $metadata. +/// +public class ODataMetadataSerializer : ODataSerializer { /// - /// Represents an for serializing $metadata. + /// Initializes a new instance of . /// - public class ODataMetadataSerializer : ODataSerializer + public ODataMetadataSerializer() + : base(ODataPayloadKind.MetadataDocument) { - /// - /// Initializes a new instance of . - /// - public ODataMetadataSerializer() - : base(ODataPayloadKind.MetadataDocument) - { - } + } - /// - /// The metadata written is from the model set on the . The - /// is not used. - public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + /// + /// The metadata written is from the model set on the . The + /// is not used. + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + { + if (messageWriter == null) { - if (messageWriter == null) - { - throw Error.ArgumentNull(nameof(messageWriter)); - } - - // NOTE: ODataMessageWriter doesn't have a way to set the IEdmModel. So, there is an underlying assumption here that - // the model received by this method and the model passed(from configuration) while building ODataMessageWriter is the same (clr object). - await messageWriter.WriteMetadataDocumentAsync().ConfigureAwait(false); + throw Error.ArgumentNull(nameof(messageWriter)); } + + // NOTE: ODataMessageWriter doesn't have a way to set the IEdmModel. So, there is an underlying assumption here that + // the model received by this method and the model passed(from configuration) while building ODataMessageWriter is the same (clr object). + await messageWriter.WriteMetadataDocumentAsync().ConfigureAwait(false); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPayloadKindHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPayloadKindHelper.cs index d19e966e5..c86e4e373 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPayloadKindHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPayloadKindHelper.cs @@ -7,37 +7,36 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +internal static class ODataPayloadKindHelper { - internal static class ODataPayloadKindHelper + public static bool IsDefined(ODataPayloadKind payloadKind) { - public static bool IsDefined(ODataPayloadKind payloadKind) - { - return payloadKind == ODataPayloadKind.Batch - || payloadKind == ODataPayloadKind.BinaryValue - || payloadKind == ODataPayloadKind.Collection - || payloadKind == ODataPayloadKind.EntityReferenceLink - || payloadKind == ODataPayloadKind.EntityReferenceLinks - || payloadKind == ODataPayloadKind.Resource - || payloadKind == ODataPayloadKind.Error - || payloadKind == ODataPayloadKind.ResourceSet - || payloadKind == ODataPayloadKind.MetadataDocument - || payloadKind == ODataPayloadKind.Parameter - || payloadKind == ODataPayloadKind.Property - || payloadKind == ODataPayloadKind.ServiceDocument - || payloadKind == ODataPayloadKind.Value - || payloadKind == ODataPayloadKind.IndividualProperty - || payloadKind == ODataPayloadKind.Delta - || payloadKind == ODataPayloadKind.Asynchronous - || payloadKind == ODataPayloadKind.Unsupported; - } + return payloadKind == ODataPayloadKind.Batch + || payloadKind == ODataPayloadKind.BinaryValue + || payloadKind == ODataPayloadKind.Collection + || payloadKind == ODataPayloadKind.EntityReferenceLink + || payloadKind == ODataPayloadKind.EntityReferenceLinks + || payloadKind == ODataPayloadKind.Resource + || payloadKind == ODataPayloadKind.Error + || payloadKind == ODataPayloadKind.ResourceSet + || payloadKind == ODataPayloadKind.MetadataDocument + || payloadKind == ODataPayloadKind.Parameter + || payloadKind == ODataPayloadKind.Property + || payloadKind == ODataPayloadKind.ServiceDocument + || payloadKind == ODataPayloadKind.Value + || payloadKind == ODataPayloadKind.IndividualProperty + || payloadKind == ODataPayloadKind.Delta + || payloadKind == ODataPayloadKind.Asynchronous + || payloadKind == ODataPayloadKind.Unsupported; + } - public static void Validate(ODataPayloadKind payloadKind, string parameterName) + public static void Validate(ODataPayloadKind payloadKind, string parameterName) + { + if (!IsDefined(payloadKind)) { - if (!IsDefined(payloadKind)) - { - throw Error.InvalidEnumArgument(parameterName, (int)payloadKind, typeof(ODataPayloadKind)); - } + throw Error.InvalidEnumArgument(parameterName, (int)payloadKind, typeof(ODataPayloadKind)); } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs index d8742f963..1f6000bbe 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs @@ -14,247 +14,246 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// Represents an for serializing 's. +/// +public class ODataPrimitiveSerializer : ODataEdmTypeSerializer { /// - /// Represents an for serializing 's. + /// Initializes a new instance of . /// - public class ODataPrimitiveSerializer : ODataEdmTypeSerializer + public ODataPrimitiveSerializer() + : base(ODataPayloadKind.Property) + { + } + + /// + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) { - /// - /// Initializes a new instance of . - /// - public ODataPrimitiveSerializer() - : base(ODataPayloadKind.Property) + if (messageWriter == null) { + throw Error.ArgumentNull(nameof(messageWriter)); } - /// - public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + if (writeContext == null) { - if (messageWriter == null) - { - throw Error.ArgumentNull(nameof(messageWriter)); - } - - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } - - if (writeContext.RootElementName == null) - { - throw Error.Argument("writeContext", SRResources.RootElementNameMissing, typeof(ODataSerializerContext).Name); - } - - IEdmTypeReference edmType = writeContext.GetEdmType(graph, type); - Contract.Assert(edmType != null); - - await messageWriter.WritePropertyAsync(this.CreateProperty(graph, edmType, writeContext.RootElementName, writeContext)).ConfigureAwait(false); + throw Error.ArgumentNull(nameof(writeContext)); } - /// - public sealed override ODataValue CreateODataValue(object graph, IEdmTypeReference expectedType, ODataSerializerContext writeContext) + if (writeContext.RootElementName == null) { - if (!expectedType.IsPrimitive()) - { - throw Error.InvalidOperation(SRResources.CannotWriteType, typeof(ODataPrimitiveSerializer), expectedType.FullName()); - } + throw Error.Argument("writeContext", SRResources.RootElementNameMissing, typeof(ODataSerializerContext).Name); + } - ODataPrimitiveValue value = CreateODataPrimitiveValue(graph, expectedType.AsPrimitive(), writeContext); - if (value == null) - { - return ODataNullValueExtensions.NullValue; - } + IEdmTypeReference edmType = writeContext.GetEdmType(graph, type); + Contract.Assert(edmType != null); - return value; - } + await messageWriter.WritePropertyAsync(this.CreateProperty(graph, edmType, writeContext.RootElementName, writeContext)).ConfigureAwait(false); + } - /// - /// Creates an for the object represented by . - /// - /// The primitive value. - /// The EDM primitive type of the value. - /// The serializer write context. - /// The created . - public virtual ODataPrimitiveValue CreateODataPrimitiveValue(object graph, IEdmPrimitiveTypeReference primitiveType, - ODataSerializerContext writeContext) + /// + public sealed override ODataValue CreateODataValue(object graph, IEdmTypeReference expectedType, ODataSerializerContext writeContext) + { + if (!expectedType.IsPrimitive()) { - return CreatePrimitive(graph, primitiveType, writeContext); + throw Error.InvalidOperation(SRResources.CannotWriteType, typeof(ODataPrimitiveSerializer), expectedType.FullName()); } - internal static void AddTypeNameAnnotationAsNeeded(ODataPrimitiveValue primitive, IEdmPrimitiveTypeReference primitiveType, - ODataMetadataLevel metadataLevel) + ODataPrimitiveValue value = CreateODataPrimitiveValue(graph, expectedType.AsPrimitive(), writeContext); + if (value == null) { - // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties - // null when values should not be serialized. The TypeName property is different and should always be - // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not - // to serialize the type name (a null value prevents serialization). - Contract.Assert(primitive != null); - - object value = primitive.Value; - string typeName = null; // Set null to force the type name not to serialize. - - // Provide the type name to serialize. - if (!ShouldSuppressTypeNameSerialization(value, metadataLevel)) - { - typeName = primitiveType.FullName(); - } - - if (typeName != null) - { - primitive.TypeAnnotation = new ODataTypeAnnotation(typeName); - } + return ODataNullValueExtensions.NullValue; } - internal static ODataPrimitiveValue CreatePrimitive(object value, IEdmPrimitiveTypeReference primitiveType, - ODataSerializerContext writeContext) - { - if (value == null) - { - return null; - } + return value; + } + + /// + /// Creates an for the object represented by . + /// + /// The primitive value. + /// The EDM primitive type of the value. + /// The serializer write context. + /// The created . + public virtual ODataPrimitiveValue CreateODataPrimitiveValue(object graph, IEdmPrimitiveTypeReference primitiveType, + ODataSerializerContext writeContext) + { + return CreatePrimitive(graph, primitiveType, writeContext); + } - object supportedValue = ConvertPrimitiveValue(value, primitiveType, writeContext?.TimeZone); - ODataPrimitiveValue primitive = new ODataPrimitiveValue(supportedValue); + internal static void AddTypeNameAnnotationAsNeeded(ODataPrimitiveValue primitive, IEdmPrimitiveTypeReference primitiveType, + ODataMetadataLevel metadataLevel) + { + // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties + // null when values should not be serialized. The TypeName property is different and should always be + // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not + // to serialize the type name (a null value prevents serialization). + Contract.Assert(primitive != null); - if (writeContext != null) - { - AddTypeNameAnnotationAsNeeded(primitive, primitiveType, writeContext.MetadataLevel); - } + object value = primitive.Value; + string typeName = null; // Set null to force the type name not to serialize. - return primitive; + // Provide the type name to serialize. + if (!ShouldSuppressTypeNameSerialization(value, metadataLevel)) + { + typeName = primitiveType.FullName(); } - internal static object ConvertPrimitiveValue(object value, IEdmPrimitiveTypeReference primitiveType, TimeZoneInfo timeZoneInfo) + if (typeName != null) { - if (value == null) - { - return null; - } + primitive.TypeAnnotation = new ODataTypeAnnotation(typeName); + } + } - Type type = value.GetType(); + internal static ODataPrimitiveValue CreatePrimitive(object value, IEdmPrimitiveTypeReference primitiveType, + ODataSerializerContext writeContext) + { + if (value == null) + { + return null; + } - // Return values for supported primitive values. - if (type == typeof(string) - || type == typeof(int) - || type == typeof(bool) - || type == typeof(double) - || type == typeof(Guid)) - { - return value; - } + object supportedValue = ConvertPrimitiveValue(value, primitiveType, writeContext?.TimeZone); + ODataPrimitiveValue primitive = new ODataPrimitiveValue(supportedValue); - if (primitiveType != null && primitiveType.IsDate() && TypeHelper.IsDateTime(type)) - { - Date dt = (DateTime)value; - return dt; - } + if (writeContext != null) + { + AddTypeNameAnnotationAsNeeded(primitive, primitiveType, writeContext.MetadataLevel); + } - if (primitiveType != null && primitiveType.IsTimeOfDay() && TypeHelper.IsTimeSpan(type)) - { - TimeOfDay tod = (TimeSpan)value; - return tod; - } + return primitive; + } - // Since ODL doesn't support "DateOnly", we have to use Date defined in ODL. - if (primitiveType != null && primitiveType.IsDate() && TypeHelper.IsDateOnly(type)) - { - DateOnly dateOnly = (DateOnly)value; - return new Date(dateOnly.Year, dateOnly.Month, dateOnly.Day); - } + internal static object ConvertPrimitiveValue(object value, IEdmPrimitiveTypeReference primitiveType, TimeZoneInfo timeZoneInfo) + { + if (value == null) + { + return null; + } - // Since ODL doesn't support "TimeOnly", we have to use TimeOfDay defined in ODL. - if (primitiveType != null && primitiveType.IsTimeOfDay() && TypeHelper.IsTimeOnly(type)) - { - TimeOnly timeOnly = (TimeOnly)value; - return new TimeOfDay(timeOnly.Hour, timeOnly.Minute, timeOnly.Second, timeOnly.Millisecond); - } + Type type = value.GetType(); - return ConvertUnsupportedPrimitives(value, timeZoneInfo); + // Return values for supported primitive values. + if (type == typeof(string) + || type == typeof(int) + || type == typeof(bool) + || type == typeof(double) + || type == typeof(Guid)) + { + return value; } - internal static object ConvertUnsupportedPrimitives(object value, TimeZoneInfo timeZoneInfo) + if (primitiveType != null && primitiveType.IsDate() && TypeHelper.IsDateTime(type)) { - if (value != null) - { - Type type = value.GetType(); + Date dt = (DateTime)value; + return dt; + } - // Note that type cannot be a nullable type as value is not null and it is boxed. - switch (Type.GetTypeCode(type)) - { - case TypeCode.Char: - return new string((char)value, 1); + if (primitiveType != null && primitiveType.IsTimeOfDay() && TypeHelper.IsTimeSpan(type)) + { + TimeOfDay tod = (TimeSpan)value; + return tod; + } - case TypeCode.UInt16: - return (int)(ushort)value; + // Since ODL doesn't support "DateOnly", we have to use Date defined in ODL. + if (primitiveType != null && primitiveType.IsDate() && TypeHelper.IsDateOnly(type)) + { + DateOnly dateOnly = (DateOnly)value; + return new Date(dateOnly.Year, dateOnly.Month, dateOnly.Day); + } - case TypeCode.UInt32: - return (long)(uint)value; + // Since ODL doesn't support "TimeOnly", we have to use TimeOfDay defined in ODL. + if (primitiveType != null && primitiveType.IsTimeOfDay() && TypeHelper.IsTimeOnly(type)) + { + TimeOnly timeOnly = (TimeOnly)value; + return new TimeOfDay(timeOnly.Hour, timeOnly.Minute, timeOnly.Second, timeOnly.Millisecond); + } - case TypeCode.UInt64: - return checked((long)(ulong)value); + return ConvertUnsupportedPrimitives(value, timeZoneInfo); + } - case TypeCode.DateTime: - DateTime dateTime = (DateTime)value; - return TimeZoneInfoHelper.ConvertToDateTimeOffset(dateTime, timeZoneInfo); + internal static object ConvertUnsupportedPrimitives(object value, TimeZoneInfo timeZoneInfo) + { + if (value != null) + { + Type type = value.GetType(); - default: - if (type == typeof(char[])) - { - return new string(value as char[]); - } - else if (type == typeof(XElement)) - { - return ((XElement)value).ToString(); - } + // Note that type cannot be a nullable type as value is not null and it is boxed. + switch (Type.GetTypeCode(type)) + { + case TypeCode.Char: + return new string((char)value, 1); - break; - } - } + case TypeCode.UInt16: + return (int)(ushort)value; - return value; - } + case TypeCode.UInt32: + return (long)(uint)value; - internal static bool CanTypeBeInferredInJson(object value) - { - Contract.Assert(value != null); + case TypeCode.UInt64: + return checked((long)(ulong)value); - TypeCode typeCode = Type.GetTypeCode(value.GetType()); + case TypeCode.DateTime: + DateTime dateTime = (DateTime)value; + return TimeZoneInfoHelper.ConvertToDateTimeOffset(dateTime, timeZoneInfo); - switch (typeCode) - { - // The type for a Boolean, Int32 or String can always be inferred in JSON. - case TypeCode.Boolean: - case TypeCode.Int32: - case TypeCode.String: - return true; - // The type for a Double can be inferred in JSON ... - case TypeCode.Double: - double doubleValue = (double)value; - // ... except for NaN or Infinity (positive or negative). - if (double.IsNaN(doubleValue) || double.IsInfinity(doubleValue)) + default: + if (type == typeof(char[])) { - return false; + return new string(value as char[]); } - else + else if (type == typeof(XElement)) { - return true; + return ((XElement)value).ToString(); } - default: - return false; + + break; } } - internal static bool ShouldSuppressTypeNameSerialization(object value, ODataMetadataLevel metadataLevel) + return value; + } + + internal static bool CanTypeBeInferredInJson(object value) + { + Contract.Assert(value != null); + + TypeCode typeCode = Type.GetTypeCode(value.GetType()); + + switch (typeCode) { - // For dynamic properties in minimal metadata level, the type name always appears as declared property. - if (metadataLevel != ODataMetadataLevel.Full) - { + // The type for a Boolean, Int32 or String can always be inferred in JSON. + case TypeCode.Boolean: + case TypeCode.Int32: + case TypeCode.String: return true; - } + // The type for a Double can be inferred in JSON ... + case TypeCode.Double: + double doubleValue = (double)value; + // ... except for NaN or Infinity (positive or negative). + if (double.IsNaN(doubleValue) || double.IsInfinity(doubleValue)) + { + return false; + } + else + { + return true; + } + default: + return false; + } + } - return CanTypeBeInferredInJson(value); + internal static bool ShouldSuppressTypeNameSerialization(object value, ODataMetadataLevel metadataLevel) + { + // For dynamic properties in minimal metadata level, the type name always appears as declared property. + if (metadataLevel != ODataMetadataLevel.Full) + { + return true; } + + return CanTypeBeInferredInJson(value); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataRawValueSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataRawValueSerializer.cs index 007d81784..86d028d07 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataRawValueSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataRawValueSerializer.cs @@ -11,44 +11,43 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// Represents an for serializing the raw value of an . +/// +public class ODataRawValueSerializer : ODataSerializer { /// - /// Represents an for serializing the raw value of an . + /// Initializes a new instance of . /// - public class ODataRawValueSerializer : ODataSerializer + public ODataRawValueSerializer() + : base(ODataPayloadKind.Value) { - /// - /// Initializes a new instance of . - /// - public ODataRawValueSerializer() - : base(ODataPayloadKind.Value) + } + + /// + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + { + if (graph == null) { + throw new ArgumentNullException(nameof(graph)); } - /// - public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + if (messageWriter == null) { - if (graph == null) - { - throw new ArgumentNullException(nameof(graph)); - } - - if (messageWriter == null) - { - throw new ArgumentNullException(nameof(messageWriter)); - } + throw new ArgumentNullException(nameof(messageWriter)); + } - // TODO: Call Async version? - // TODO: Make the enum alias working - if (TypeHelper.IsEnum(graph.GetType())) - { - await messageWriter.WriteValueAsync(graph.ToString()).ConfigureAwait(false); - } - else - { - await messageWriter.WriteValueAsync(ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(graph, writeContext?.TimeZone)).ConfigureAwait(false); - } + // TODO: Call Async version? + // TODO: Make the enum alias working + if (TypeHelper.IsEnum(graph.GetType())) + { + await messageWriter.WriteValueAsync(graph.ToString()).ConfigureAwait(false); + } + else + { + await messageWriter.WriteValueAsync(ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(graph, writeContext?.TimeZone)).ConfigureAwait(false); } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs index 72677f7d0..9397fe89c 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs @@ -26,1855 +26,1854 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.OData.Deltas; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// ODataSerializer for serializing instances of or +/// +[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many ODataLib classes.")] +public class ODataResourceSerializer : ODataEdmTypeSerializer { - /// - /// ODataSerializer for serializing instances of or - /// - [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many ODataLib classes.")] - public class ODataResourceSerializer : ODataEdmTypeSerializer + private const string Resource = "Resource"; + + /// + public ODataResourceSerializer(IODataSerializerProvider serializerProvider) + : base(ODataPayloadKind.Resource, serializerProvider) { - private const string Resource = "Resource"; + } - /// - public ODataResourceSerializer(IODataSerializerProvider serializerProvider) - : base(ODataPayloadKind.Resource, serializerProvider) + /// + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, + ODataSerializerContext writeContext) + { + if (messageWriter == null) { + throw Error.ArgumentNull(nameof(messageWriter)); } - /// - public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, - ODataSerializerContext writeContext) + if (writeContext == null) { - if (messageWriter == null) - { - throw Error.ArgumentNull(nameof(messageWriter)); - } + throw Error.ArgumentNull(nameof(writeContext)); + } - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + bool isUntypedPath = writeContext.Path.IsUntypedPropertyPath(); + IEdmTypeReference edmType = writeContext.GetEdmType(graph, type, isUntypedPath); + Contract.Assert(edmType != null); - bool isUntypedPath = writeContext.Path.IsUntypedPropertyPath(); - IEdmTypeReference edmType = writeContext.GetEdmType(graph, type, isUntypedPath); - Contract.Assert(edmType != null); + IEdmNavigationSource navigationSource = writeContext.NavigationSource; + ODataWriter writer = await messageWriter.CreateODataResourceWriterAsync(navigationSource, edmType.ToStructuredType()) + .ConfigureAwait(false); + await WriteObjectInlineAsync(graph, edmType, writer, writeContext).ConfigureAwait(false); + } - IEdmNavigationSource navigationSource = writeContext.NavigationSource; - ODataWriter writer = await messageWriter.CreateODataResourceWriterAsync(navigationSource, edmType.ToStructuredType()) - .ConfigureAwait(false); - await WriteObjectInlineAsync(graph, edmType, writer, writeContext).ConfigureAwait(false); + /// + public override async Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, + ODataSerializerContext writeContext) + { + if (writer == null) + { + throw Error.ArgumentNull(nameof(writer)); } - /// - public override async Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, - ODataSerializerContext writeContext) + if (writeContext == null) { - if (writer == null) - { - throw Error.ArgumentNull(nameof(writer)); - } + throw Error.ArgumentNull(nameof(writeContext)); + } - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + if (graph == null || graph is NullEdmComplexObject) + { + throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, Resource)); + } + else + { + await WriteResourceAsync(graph, writer, writeContext, expectedType).ConfigureAwait(false); + } + } - if (graph == null || graph is NullEdmComplexObject) - { - throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, Resource)); - } - else - { - await WriteResourceAsync(graph, writer, writeContext, expectedType).ConfigureAwait(false); - } + /// + /// Writes the given object specified by the parameter graph as a part of an existing OData message using the given + /// deltaWriter and the writeContext. + /// + /// The object to be written. + /// The expected EDM type of the object represented by . + /// The to be used for writing. + /// The . + public virtual async Task WriteDeltaObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, + ODataSerializerContext writeContext) + { + if (writer == null) + { + throw Error.ArgumentNull(nameof(writer)); } - /// - /// Writes the given object specified by the parameter graph as a part of an existing OData message using the given - /// deltaWriter and the writeContext. - /// - /// The object to be written. - /// The expected EDM type of the object represented by . - /// The to be used for writing. - /// The . - public virtual async Task WriteDeltaObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, - ODataSerializerContext writeContext) + if (writeContext == null) { - if (writer == null) - { - throw Error.ArgumentNull(nameof(writer)); - } + throw Error.ArgumentNull(nameof(writeContext)); + } - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + if (graph == null) + { + throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, Resource)); + } + else + { + await WriteDeltaResourceAsync(graph, writer, writeContext).ConfigureAwait(false); + } + } - if (graph == null) - { - throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, Resource)); - } - else - { - await WriteDeltaResourceAsync(graph, writer, writeContext).ConfigureAwait(false); - } + private async Task WriteDeltaResourceAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext) + { + Contract.Assert(writeContext != null); + + IEdmStructuredTypeReference structuredType = GetResourceType(graph, writeContext); + ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); + EdmDeltaResourceObject deltaResource = graph as EdmDeltaResourceObject; + if (deltaResource != null && deltaResource.NavigationSource != null) + { + resourceContext.NavigationSource = deltaResource.NavigationSource; } - private async Task WriteDeltaResourceAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext) + SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); + if (selectExpandNode != null) { - Contract.Assert(writeContext != null); + ODataResource resource = CreateResource(selectExpandNode, resourceContext); - IEdmStructuredTypeReference structuredType = GetResourceType(graph, writeContext); - ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); - EdmDeltaResourceObject deltaResource = graph as EdmDeltaResourceObject; - if (deltaResource != null && deltaResource.NavigationSource != null) + if (resource != null) { - resourceContext.NavigationSource = deltaResource.NavigationSource; + await writer.WriteStartAsync(resource).ConfigureAwait(false); + await WriteDeltaComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteDynamicComplexPropertiesAsync(resourceContext, writer).ConfigureAwait(false); + await WriteDeltaNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); } + } + } - SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); - if (selectExpandNode != null) - { - ODataResource resource = CreateResource(selectExpandNode, resourceContext); + private async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpandNode, + ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(resourceContext != null); + Contract.Assert(writer != null); - if (resource != null) - { - await writer.WriteStartAsync(resource).ConfigureAwait(false); - await WriteDeltaComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteDynamicComplexPropertiesAsync(resourceContext, writer).ConfigureAwait(false); - await WriteDeltaNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); - } - } + if (selectExpandNode.SelectedComplexProperties == null) + { + return; } + IEnumerable complexProperties = selectExpandNode.SelectedComplexProperties.Keys; - private async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpandNode, - ResourceContext resourceContext, ODataWriter writer) + if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) { - Contract.Assert(resourceContext != null); - Contract.Assert(writer != null); - - if (selectExpandNode.SelectedComplexProperties == null) - { - return; - } - IEnumerable complexProperties = selectExpandNode.SelectedComplexProperties.Keys; - - if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) + if (resourceContext.EdmObject is TypedEdmEntityObject obj && obj.Instance is IDelta deltaObject) { - if (resourceContext.EdmObject is TypedEdmEntityObject obj && obj.Instance is IDelta deltaObject) - { - IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); - complexProperties = complexProperties.Where(p => changedProperties.Contains(p.Name)); - } + IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); + complexProperties = complexProperties.Where(p => changedProperties.Contains(p.Name)); } + } - foreach (IEdmStructuralProperty complexProperty in complexProperties) + foreach (IEdmStructuralProperty complexProperty in complexProperties) + { + ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo { - ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo - { - IsCollection = complexProperty.Type.IsCollection(), - Name = complexProperty.Name - }; + IsCollection = complexProperty.Type.IsCollection(), + Name = complexProperty.Name + }; - await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); - await WriteDeltaComplexAndExpandedNavigationPropertyAsync(complexProperty, null, resourceContext, writer) - .ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); - } + await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); + await WriteDeltaComplexAndExpandedNavigationPropertyAsync(complexProperty, null, resourceContext, writer) + .ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); } + } - private async Task WriteDeltaComplexAndExpandedNavigationPropertyAsync( - IEdmProperty edmProperty, - SelectExpandClause selectExpandClause, - ResourceContext resourceContext, - ODataWriter writer, - Type navigationPropertyType = null) - { - Contract.Assert(edmProperty != null); - Contract.Assert(resourceContext != null); - Contract.Assert(writer != null); + private async Task WriteDeltaComplexAndExpandedNavigationPropertyAsync( + IEdmProperty edmProperty, + SelectExpandClause selectExpandClause, + ResourceContext resourceContext, + ODataWriter writer, + Type navigationPropertyType = null) + { + Contract.Assert(edmProperty != null); + Contract.Assert(resourceContext != null); + Contract.Assert(writer != null); - object propertyValue = resourceContext.GetPropertyValue(edmProperty.Name); + object propertyValue = resourceContext.GetPropertyValue(edmProperty.Name); - if (propertyValue == null || propertyValue is NullEdmComplexObject) + if (propertyValue == null || propertyValue is NullEdmComplexObject) + { + if (edmProperty.Type.IsCollection()) { - if (edmProperty.Type.IsCollection()) - { - // A complex or navigation property whose Type attribute specifies a collection, the collection always exists, - // it may just be empty. - // If a collection of complex or entities can be related, it is represented as a JSON array. An empty - // collection of resources (one that contains no resource) is represented as an empty JSON array. - await writer.WriteStartAsync(new ODataResourceSet - { - TypeName = edmProperty.Type.FullName() - }).ConfigureAwait(false); - } - else + // A complex or navigation property whose Type attribute specifies a collection, the collection always exists, + // it may just be empty. + // If a collection of complex or entities can be related, it is represented as a JSON array. An empty + // collection of resources (one that contains no resource) is represented as an empty JSON array. + await writer.WriteStartAsync(new ODataResourceSet { - // If at most one resource can be related, the value is null if no resource is currently related. - await writer.WriteStartAsync(resource: null).ConfigureAwait(false); - } + TypeName = edmProperty.Type.FullName() + }).ConfigureAwait(false); + } + else + { + // If at most one resource can be related, the value is null if no resource is currently related. + await writer.WriteStartAsync(resource: null).ConfigureAwait(false); + } - await writer.WriteEndAsync().ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); + } + else + { + // create the serializer context for the complex and expanded item. + ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, selectExpandClause, edmProperty); + nestedWriteContext.Type = navigationPropertyType; + // write object. + + // TODO: enable overriding serializer based on type. Currently requires serializer supports WriteDeltaObjectinline, because it takes an ODataDeltaWriter + // ODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmProperty.Type); + // if (serializer == null) + // { + // throw new SerializationException( + // Error.Format(SRResources.TypeCannotBeSerialized, edmProperty.Type.ToTraceString())); + // } + if (edmProperty.Type.IsCollection()) + { + ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(SerializerProvider); + await serializer.WriteObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); } else { - // create the serializer context for the complex and expanded item. - ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, selectExpandClause, edmProperty); - nestedWriteContext.Type = navigationPropertyType; - // write object. - - // TODO: enable overriding serializer based on type. Currently requires serializer supports WriteDeltaObjectinline, because it takes an ODataDeltaWriter - // ODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmProperty.Type); - // if (serializer == null) - // { - // throw new SerializationException( - // Error.Format(SRResources.TypeCannotBeSerialized, edmProperty.Type.ToTraceString())); - // } - if (edmProperty.Type.IsCollection()) - { - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(SerializerProvider); - await serializer.WriteObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); - } - else - { - ODataResourceSerializer serializer = new ODataResourceSerializer(SerializerProvider); - await serializer.WriteDeltaObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); - } + ODataResourceSerializer serializer = new ODataResourceSerializer(SerializerProvider); + await serializer.WriteDeltaObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext).ConfigureAwait(false); } } + } - /// - /// Writes delta navigation properties asynchronously. - /// - /// Contains the set of properties and actions to use to select and expand while writing an entity. - /// The resource context for the resource being written. - /// The ODataWriter. - /// A task that represents the asynchronous write operation - internal async Task WriteDeltaNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) - { - Contract.Assert(resourceContext != null, "The ResourceContext cannot be null"); - Contract.Assert(writer != null, "The ODataWriter cannot be null"); + /// + /// Writes delta navigation properties asynchronously. + /// + /// Contains the set of properties and actions to use to select and expand while writing an entity. + /// The resource context for the resource being written. + /// The ODataWriter. + /// A task that represents the asynchronous write operation + internal async Task WriteDeltaNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(resourceContext != null, "The ResourceContext cannot be null"); + Contract.Assert(writer != null, "The ODataWriter cannot be null"); - IEnumerable> navigationProperties = GetNavigationPropertiesToWrite(selectExpandNode, resourceContext); + IEnumerable> navigationProperties = GetNavigationPropertiesToWrite(selectExpandNode, resourceContext); - foreach (KeyValuePair navigationProperty in navigationProperties) + foreach (KeyValuePair navigationProperty in navigationProperties) + { + ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo { - ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo - { - IsCollection = navigationProperty.Key.Type.IsCollection(), - Name = navigationProperty.Key.Name - }; + IsCollection = navigationProperty.Key.Type.IsCollection(), + Name = navigationProperty.Key.Name + }; - await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); - await WriteDeltaComplexAndExpandedNavigationPropertyAsync(navigationProperty.Key, null, resourceContext, writer, navigationProperty.Value).ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); - } + await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); + await WriteDeltaComplexAndExpandedNavigationPropertyAsync(navigationProperty.Key, null, resourceContext, writer, navigationProperty.Value).ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); } + } + + private IEnumerable> GetNavigationPropertiesToWrite(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + { + ISet navigationProperties = selectExpandNode.SelectedNavigationProperties; - private IEnumerable> GetNavigationPropertiesToWrite(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + if (navigationProperties == null) { - ISet navigationProperties = selectExpandNode.SelectedNavigationProperties; + yield break; + } - if (navigationProperties == null) - { - yield break; - } + if (resourceContext.EdmObject is IDelta changedObject) + { + IEnumerable changedProperties = changedObject.GetChangedPropertyNames(); - if (resourceContext.EdmObject is IDelta changedObject) + foreach (IEdmNavigationProperty navigationProperty in navigationProperties) { - IEnumerable changedProperties = changedObject.GetChangedPropertyNames(); - - foreach (IEdmNavigationProperty navigationProperty in navigationProperties) + if (changedProperties != null && changedProperties.Contains(navigationProperty.Name)) { - if (changedProperties != null && changedProperties.Contains(navigationProperty.Name)) - { - yield return new KeyValuePair(navigationProperty, typeof(IEdmChangedObject)); - } + yield return new KeyValuePair(navigationProperty, typeof(IEdmChangedObject)); } } - else if (resourceContext.ResourceInstance is IDelta deltaObject) - { - IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); - IDictionary deltaNestedProperties = deltaObject.GetDeltaNestedNavigationProperties(); + } + else if (resourceContext.ResourceInstance is IDelta deltaObject) + { + IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); + IDictionary deltaNestedProperties = deltaObject.GetDeltaNestedNavigationProperties(); - foreach (IEdmNavigationProperty navigationProperty in navigationProperties) + foreach (IEdmNavigationProperty navigationProperty in navigationProperties) + { + if (changedProperties != null && changedProperties.Contains(navigationProperty.Name) && deltaNestedProperties.TryGetValue(navigationProperty.Name, out object obj)) { - if (changedProperties != null && changedProperties.Contains(navigationProperty.Name) && deltaNestedProperties.TryGetValue(navigationProperty.Name, out object obj)) + if (obj != null) { - if (obj != null) - { - yield return new KeyValuePair(navigationProperty, obj.GetType()); - } + yield return new KeyValuePair(navigationProperty, obj.GetType()); } } } } + } - private static IEnumerable CreateODataPropertiesFromDynamicType(EdmStructuredType structuredType, object graph, - Dictionary dynamicTypeProperties, ODataSerializerContext writeContext) - { - Contract.Assert(dynamicTypeProperties != null); + private static IEnumerable CreateODataPropertiesFromDynamicType(EdmStructuredType structuredType, object graph, + Dictionary dynamicTypeProperties, ODataSerializerContext writeContext) + { + Contract.Assert(dynamicTypeProperties != null); - var properties = new List(); - var dynamicObject = graph as DynamicTypeWrapper; - if (dynamicObject == null) + var properties = new List(); + var dynamicObject = graph as DynamicTypeWrapper; + if (dynamicObject == null) + { + var dynamicEnumerable = (graph as IEnumerable); + if (dynamicEnumerable != null) { - var dynamicEnumerable = (graph as IEnumerable); - if (dynamicEnumerable != null) - { - dynamicObject = dynamicEnumerable.SingleOrDefault(); - } + dynamicObject = dynamicEnumerable.SingleOrDefault(); } - if (dynamicObject != null) + } + if (dynamicObject != null) + { + foreach (var prop in dynamicObject.Values) { - foreach (var prop in dynamicObject.Values) - { - IEdmProperty edmProperty = structuredType?.Properties() - .FirstOrDefault(p => p.Name.Equals(prop.Key, StringComparison.Ordinal)); + IEdmProperty edmProperty = structuredType?.Properties() + .FirstOrDefault(p => p.Name.Equals(prop.Key, StringComparison.Ordinal)); - if (prop.Value != null - && (prop.Value is DynamicTypeWrapper || (prop.Value is IEnumerable))) + if (prop.Value != null + && (prop.Value is DynamicTypeWrapper || (prop.Value is IEnumerable))) + { + if (edmProperty != null) { - if (edmProperty != null) + dynamicTypeProperties.Add(edmProperty, prop.Value); + } + } + else + { + ODataProperty property; + if (prop.Value == null) + { + property = new ODataProperty { - dynamicTypeProperties.Add(edmProperty, prop.Value); - } + Name = prop.Key, + Value = ODataNullValueExtensions.NullValue + }; } else { - ODataProperty property; - if (prop.Value == null) + if (edmProperty != null) { property = new ODataProperty { Name = prop.Key, - Value = ODataNullValueExtensions.NullValue + Value = ODataPrimitiveSerializer.ConvertPrimitiveValue(prop.Value, edmProperty.Type.AsPrimitive(), writeContext?.TimeZone) }; } else { - if (edmProperty != null) - { - property = new ODataProperty - { - Name = prop.Key, - Value = ODataPrimitiveSerializer.ConvertPrimitiveValue(prop.Value, edmProperty.Type.AsPrimitive(), writeContext?.TimeZone) - }; - } - else + property = new ODataProperty { - property = new ODataProperty - { - Name = prop.Key, - Value = prop.Value - }; - } + Name = prop.Key, + Value = prop.Value + }; } - - properties.Add(property); } + + properties.Add(property); } } - - return properties; } - private async Task WriteDynamicTypeResourceAsync(object graph, ODataWriter writer, IEdmTypeReference expectedType, - ODataSerializerContext writeContext) + return properties; + } + + private async Task WriteDynamicTypeResourceAsync(object graph, ODataWriter writer, IEdmTypeReference expectedType, + ODataSerializerContext writeContext) + { + var dynamicTypeProperties = new Dictionary(); + var structuredType = expectedType.Definition as EdmStructuredType; + var resource = new ODataResource() { - var dynamicTypeProperties = new Dictionary(); - var structuredType = expectedType.Definition as EdmStructuredType; - var resource = new ODataResource() - { - TypeName = expectedType.FullName(), - Properties = CreateODataPropertiesFromDynamicType(structuredType, graph, dynamicTypeProperties, writeContext) - }; + TypeName = expectedType.FullName(), + Properties = CreateODataPropertiesFromDynamicType(structuredType, graph, dynamicTypeProperties, writeContext) + }; - resource.IsTransient = true; - await writer.WriteStartAsync(resource).ConfigureAwait(false); - foreach (var property in dynamicTypeProperties.Keys) + resource.IsTransient = true; + await writer.WriteStartAsync(resource).ConfigureAwait(false); + foreach (var property in dynamicTypeProperties.Keys) + { + var resourceContext = new ResourceContext(writeContext, expectedType.AsStructured(), graph); + if (structuredType.NavigationProperties().Any(p => p.Type.Equals(property.Type)) && !(property.Type is EdmCollectionTypeReference)) { - var resourceContext = new ResourceContext(writeContext, expectedType.AsStructured(), graph); - if (structuredType.NavigationProperties().Any(p => p.Type.Equals(property.Type)) && !(property.Type is EdmCollectionTypeReference)) - { - var navigationProperty = structuredType.NavigationProperties().FirstOrDefault(p => p.Type.Equals(property.Type)); - var navigationLink = CreateNavigationLink(navigationProperty, resourceContext); - if (navigationLink != null) - { - await writer.WriteStartAsync(navigationLink).ConfigureAwait(false); - await WriteDynamicTypeResourceAsync(dynamicTypeProperties[property], writer, property.Type, writeContext) - .ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); - } - } - else + var navigationProperty = structuredType.NavigationProperties().FirstOrDefault(p => p.Type.Equals(property.Type)); + var navigationLink = CreateNavigationLink(navigationProperty, resourceContext); + if (navigationLink != null) { - ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo - { - IsCollection = property.Type.IsCollection(), - Name = property.Name - }; - - await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); - await WriteDynamicComplexPropertyAsync(dynamicTypeProperties[property], property.Type, resourceContext, writer) + await writer.WriteStartAsync(navigationLink).ConfigureAwait(false); + await WriteDynamicTypeResourceAsync(dynamicTypeProperties[property], writer, property.Type, writeContext) .ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); } } + else + { + ODataNestedResourceInfo nestedResourceInfo = new ODataNestedResourceInfo + { + IsCollection = property.Type.IsCollection(), + Name = property.Name + }; - await writer.WriteEndAsync().ConfigureAwait(false); + await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); + await WriteDynamicComplexPropertyAsync(dynamicTypeProperties[property], property.Type, resourceContext, writer) + .ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); + } } - private async Task WriteResourceAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext, - IEdmTypeReference expectedType) - { - Contract.Assert(writeContext != null); + await writer.WriteEndAsync().ConfigureAwait(false); + } - if (graph.GetType().IsDynamicTypeWrapper()) - { - await WriteDynamicTypeResourceAsync(graph, writer, expectedType, writeContext).ConfigureAwait(false); - return; - } + private async Task WriteResourceAsync(object graph, ODataWriter writer, ODataSerializerContext writeContext, + IEdmTypeReference expectedType) + { + Contract.Assert(writeContext != null); + + if (graph.GetType().IsDynamicTypeWrapper()) + { + await WriteDynamicTypeResourceAsync(graph, writer, expectedType, writeContext).ConfigureAwait(false); + return; + } - IEdmStructuredTypeReference structuredType = GetResourceType(graph, writeContext); - ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); + IEdmStructuredTypeReference structuredType = GetResourceType(graph, writeContext); + ResourceContext resourceContext = new ResourceContext(writeContext, structuredType, graph); - SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); - if (selectExpandNode != null) + SelectExpandNode selectExpandNode = CreateSelectExpandNode(resourceContext); + if (selectExpandNode != null) + { + ODataResource resource = CreateResource(selectExpandNode, resourceContext); + if (resource != null) { - ODataResource resource = CreateResource(selectExpandNode, resourceContext); - if (resource != null) + if (resourceContext.SerializerContext.ExpandReference) { - if (resourceContext.SerializerContext.ExpandReference) - { - await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink - { - Url = resource.Id - }).ConfigureAwait(false); - } - else + await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink { - await writer.WriteStartAsync(resource).ConfigureAwait(false); - await WriteUntypedPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteStreamPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteDynamicComplexPropertiesAsync(resourceContext, writer).ConfigureAwait(false); - await WriteNavigationLinksAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteExpandedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await WriteReferencedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); - } + Url = resource.Id + }).ConfigureAwait(false); + } + else + { + await writer.WriteStartAsync(resource).ConfigureAwait(false); + await WriteUntypedPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteStreamPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteComplexPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteDynamicComplexPropertiesAsync(resourceContext, writer).ConfigureAwait(false); + await WriteNavigationLinksAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteExpandedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await WriteReferencedNavigationPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); } } } + } - /// - /// Creates the that describes the set of properties and actions to select and expand while writing this entity. - /// - /// Contains the entity instance being written and the context. - /// - /// The that describes the set of properties and actions to select and expand while writing this entity. - /// - public virtual SelectExpandNode CreateSelectExpandNode(ResourceContext resourceContext) + /// + /// Creates the that describes the set of properties and actions to select and expand while writing this entity. + /// + /// Contains the entity instance being written and the context. + /// + /// The that describes the set of properties and actions to select and expand while writing this entity. + /// + public virtual SelectExpandNode CreateSelectExpandNode(ResourceContext resourceContext) + { + if (resourceContext == null) { - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } - - ODataSerializerContext writeContext = resourceContext.SerializerContext; - IEdmStructuredType structuredType = resourceContext.StructuredType; + throw Error.ArgumentNull(nameof(resourceContext)); + } - object selectExpandNode; + ODataSerializerContext writeContext = resourceContext.SerializerContext; + IEdmStructuredType structuredType = resourceContext.StructuredType; - Tuple key = Tuple.Create(writeContext.SelectExpandClause, structuredType); - if (!writeContext.Items.TryGetValue(key, out selectExpandNode)) - { - // cache the selectExpandNode so that if we are writing a feed we don't have to construct it again. - selectExpandNode = new SelectExpandNode(structuredType, writeContext); - writeContext.Items[key] = selectExpandNode; - } + object selectExpandNode; - return selectExpandNode as SelectExpandNode; + Tuple key = Tuple.Create(writeContext.SelectExpandClause, structuredType); + if (!writeContext.Items.TryGetValue(key, out selectExpandNode)) + { + // cache the selectExpandNode so that if we are writing a feed we don't have to construct it again. + selectExpandNode = new SelectExpandNode(structuredType, writeContext); + writeContext.Items[key] = selectExpandNode; } - /// - /// Creates the to be written while writing this resource. - /// - /// The describing the response graph. - /// The context for the resource instance being written. - /// The created . - public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + return selectExpandNode as SelectExpandNode; + } + + /// + /// Creates the to be written while writing this resource. + /// + /// The describing the response graph. + /// The context for the resource instance being written. + /// The created . + public virtual ODataResource CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + { + if (selectExpandNode == null) { - if (selectExpandNode == null) - { - throw Error.ArgumentNull(nameof(selectExpandNode)); - } + throw Error.ArgumentNull(nameof(selectExpandNode)); + } - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } - if (resourceContext.SerializerContext.ExpandReference) + if (resourceContext.SerializerContext.ExpandReference) + { + return new ODataResource { - return new ODataResource - { - Id = resourceContext.GenerateSelfLink(false) - }; - } + Id = resourceContext.GenerateSelfLink(false) + }; + } - string typeName = resourceContext.StructuredType.FullTypeName(); + string typeName = resourceContext.StructuredType.FullTypeName(); - ODataResource resource = new ODataResource - { - TypeName = typeName ?? "Edm.Untyped", - Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), - }; + ODataResource resource = new ODataResource + { + TypeName = typeName ?? "Edm.Untyped", + Properties = CreateStructuralPropertyBag(selectExpandNode, resourceContext), + }; - if (resourceContext.EdmObject is EdmDeltaResourceObject && resourceContext.NavigationSource != null) + if (resourceContext.EdmObject is EdmDeltaResourceObject && resourceContext.NavigationSource != null) + { + ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo(); + serializationInfo.NavigationSourceName = resourceContext.NavigationSource.Name; + serializationInfo.NavigationSourceKind = resourceContext.NavigationSource.NavigationSourceKind(); + IEdmEntityType sourceType = resourceContext.NavigationSource.EntityType; + if (sourceType != null) { - ODataResourceSerializationInfo serializationInfo = new ODataResourceSerializationInfo(); - serializationInfo.NavigationSourceName = resourceContext.NavigationSource.Name; - serializationInfo.NavigationSourceKind = resourceContext.NavigationSource.NavigationSourceKind(); - IEdmEntityType sourceType = resourceContext.NavigationSource.EntityType; - if (sourceType != null) - { - serializationInfo.NavigationSourceEntityTypeName = sourceType.Name; - } - resource.SetSerializationInfo(serializationInfo); + serializationInfo.NavigationSourceEntityTypeName = sourceType.Name; } + resource.SetSerializationInfo(serializationInfo); + } - // Try to add the dynamic properties if the structural type is open. - AppendDynamicProperties(resource, selectExpandNode, resourceContext); + // Try to add the dynamic properties if the structural type is open. + AppendDynamicProperties(resource, selectExpandNode, resourceContext); - if (selectExpandNode.SelectedActions != null) + if (selectExpandNode.SelectedActions != null) + { + IEnumerable actions = CreateODataActions(selectExpandNode.SelectedActions, resourceContext); + foreach (ODataAction action in actions) { - IEnumerable actions = CreateODataActions(selectExpandNode.SelectedActions, resourceContext); - foreach (ODataAction action in actions) - { - resource.AddAction(action); - } + resource.AddAction(action); } + } - if (selectExpandNode.SelectedFunctions != null) - { - IEnumerable functions = CreateODataFunctions(selectExpandNode.SelectedFunctions, resourceContext); - foreach (ODataFunction function in functions) - { - resource.AddFunction(function); - } - } - - IEdmStructuredType pathType = GetODataPathType(resourceContext.SerializerContext); - if (resourceContext.StructuredType.TypeKind == EdmTypeKind.Complex) - { - AddTypeNameAnnotationAsNeededForComplex(resource, resourceContext.SerializerContext.MetadataLevel); - } - else + if (selectExpandNode.SelectedFunctions != null) + { + IEnumerable functions = CreateODataFunctions(selectExpandNode.SelectedFunctions, resourceContext); + foreach (ODataFunction function in functions) { - AddTypeNameAnnotationAsNeeded(resource, pathType, resourceContext.SerializerContext.MetadataLevel); + resource.AddFunction(function); } + } - if (resourceContext.StructuredType.TypeKind == EdmTypeKind.Entity && resourceContext.NavigationSource != null) - { - // Condition 1. If resourceContext.NavigationSource is a contained entity set - // and a contained resource is being written, the id/read/edit links can be derived - // from the entity set or parent resource, i.e., no need to use link builder to build the links. - // Condition 2. If resourceContext.NavigationSource is a contained entity set - // but an expanded non-contained resource is being written, - // deriving the id/read/edit links from the entity set or parent resource will - // most likely result into invalid links. - // A navigation property binding should exist and we should try - // to use the navigation link builder to build the links. - // NOTE: resourceContext.SerializerContext.NavigationProperty will not be null when writing an expanded resource - if (!(resourceContext.NavigationSource is IEdmContainedEntitySet) - || resourceContext.SerializerContext.NavigationProperty?.ContainsTarget == false) - { - IEdmModel model = resourceContext.SerializerContext.Model; - NavigationSourceLinkBuilderAnnotation linkBuilder = EdmModelLinkBuilderExtensions.GetNavigationSourceLinkBuilder(model, resourceContext.NavigationSource); - EntitySelfLinks selfLinks = linkBuilder.BuildEntitySelfLinks(resourceContext, resourceContext.SerializerContext.MetadataLevel); - - if (selfLinks.IdLink != null) - { - resource.Id = selfLinks.IdLink; - } + IEdmStructuredType pathType = GetODataPathType(resourceContext.SerializerContext); + if (resourceContext.StructuredType.TypeKind == EdmTypeKind.Complex) + { + AddTypeNameAnnotationAsNeededForComplex(resource, resourceContext.SerializerContext.MetadataLevel); + } + else + { + AddTypeNameAnnotationAsNeeded(resource, pathType, resourceContext.SerializerContext.MetadataLevel); + } - if (selfLinks.ReadLink != null) - { - resource.ReadLink = selfLinks.ReadLink; - } + if (resourceContext.StructuredType.TypeKind == EdmTypeKind.Entity && resourceContext.NavigationSource != null) + { + // Condition 1. If resourceContext.NavigationSource is a contained entity set + // and a contained resource is being written, the id/read/edit links can be derived + // from the entity set or parent resource, i.e., no need to use link builder to build the links. + // Condition 2. If resourceContext.NavigationSource is a contained entity set + // but an expanded non-contained resource is being written, + // deriving the id/read/edit links from the entity set or parent resource will + // most likely result into invalid links. + // A navigation property binding should exist and we should try + // to use the navigation link builder to build the links. + // NOTE: resourceContext.SerializerContext.NavigationProperty will not be null when writing an expanded resource + if (!(resourceContext.NavigationSource is IEdmContainedEntitySet) + || resourceContext.SerializerContext.NavigationProperty?.ContainsTarget == false) + { + IEdmModel model = resourceContext.SerializerContext.Model; + NavigationSourceLinkBuilderAnnotation linkBuilder = EdmModelLinkBuilderExtensions.GetNavigationSourceLinkBuilder(model, resourceContext.NavigationSource); + EntitySelfLinks selfLinks = linkBuilder.BuildEntitySelfLinks(resourceContext, resourceContext.SerializerContext.MetadataLevel); + + if (selfLinks.IdLink != null) + { + resource.Id = selfLinks.IdLink; + } - if (selfLinks.EditLink != null) - { - resource.EditLink = selfLinks.EditLink; - } + if (selfLinks.ReadLink != null) + { + resource.ReadLink = selfLinks.ReadLink; } - string etag = CreateETag(resourceContext); - if (etag != null) + if (selfLinks.EditLink != null) { - resource.ETag = etag; + resource.EditLink = selfLinks.EditLink; } } - return resource; + string etag = CreateETag(resourceContext); + if (etag != null) + { + resource.ETag = etag; + } } - /// - /// Appends the dynamic properties of primitive, enum or the collection of them into the given . - /// If the dynamic property is a property of the complex or collection of complex, it will be saved into - /// the dynamic complex properties dictionary of and be written later. - /// - /// The describing the resource. - /// The describing the response graph. - /// The context for the resource instance being written. - [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many classes.")] - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "These are simple conversion function and cannot be split up.")] - public virtual void AppendDynamicProperties(ODataResource resource, SelectExpandNode selectExpandNode, - ResourceContext resourceContext) + return resource; + } + + /// + /// Appends the dynamic properties of primitive, enum or the collection of them into the given . + /// If the dynamic property is a property of the complex or collection of complex, it will be saved into + /// the dynamic complex properties dictionary of and be written later. + /// + /// The describing the resource. + /// The describing the response graph. + /// The context for the resource instance being written. + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many classes.")] + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "These are simple conversion function and cannot be split up.")] + public virtual void AppendDynamicProperties(ODataResource resource, SelectExpandNode selectExpandNode, + ResourceContext resourceContext) + { + Contract.Assert(resource != null); + Contract.Assert(selectExpandNode != null); + Contract.Assert(resourceContext != null); + + if (!resourceContext.StructuredType.IsOpen || // non-open type + (!selectExpandNode.SelectAllDynamicProperties && selectExpandNode.SelectedDynamicProperties == null)) { - Contract.Assert(resource != null); - Contract.Assert(selectExpandNode != null); - Contract.Assert(resourceContext != null); + return; + } - if (!resourceContext.StructuredType.IsOpen || // non-open type - (!selectExpandNode.SelectAllDynamicProperties && selectExpandNode.SelectedDynamicProperties == null)) + IEdmStructuredType structuredType = resourceContext.StructuredType; + IEdmStructuredObject structuredObject = resourceContext.EdmObject; + ODataSerializerContext serializierContext = resourceContext.SerializerContext; + object value; + IDelta delta = structuredObject as IDelta; + if (structuredObject is EdmUntypedObject untypedObject) // NO CLR, NO EDM + { + value = untypedObject; + } + else if (structuredObject is TypedEdmUntypedObject typedUntypedObject) // has CLR, but no EDM + { + value = typedUntypedObject.GetProperties(); + } + else if (delta == null) + { + PropertyInfo dynamicPropertyInfo = resourceContext.EdmModel.GetDynamicPropertyDictionary(structuredType); + if (dynamicPropertyInfo == null || structuredObject == null || + !structuredObject.TryGetPropertyValue(dynamicPropertyInfo.Name, out value) || value == null) { return; } + } + else + { + value = ((EdmStructuredObject)structuredObject).TryGetDynamicProperties(); + } - IEdmStructuredType structuredType = resourceContext.StructuredType; - IEdmStructuredObject structuredObject = resourceContext.EdmObject; - ODataSerializerContext serializierContext = resourceContext.SerializerContext; - object value; - IDelta delta = structuredObject as IDelta; - if (structuredObject is EdmUntypedObject untypedObject) // NO CLR, NO EDM - { - value = untypedObject; - } - else if (structuredObject is TypedEdmUntypedObject typedUntypedObject) // has CLR, but no EDM - { - value = typedUntypedObject.GetProperties(); - } - else if (delta == null) + IDictionary dynamicPropertyDictionary = (IDictionary)value; + + // Build a HashSet to store the declared property names. + // It is used to make sure the dynamic property name is different from all declared property names. + HashSet declaredPropertyNameSet = new HashSet(resource.Properties.Select(p => p.Name)); + List dynamicProperties = new List(); + + // To test SelectedDynamicProperties == null is enough to filter the dynamic properties. + // Because if SelectAllDynamicProperties == true, SelectedDynamicProperties should be null always. + // So `selectExpandNode.SelectedDynamicProperties == null` covers `SelectAllDynamicProperties == true` scenario. + // If `selectExpandNode.SelectedDynamicProperties != null`, then we should test whether the property is selected or not using "Contains(...)". + IEnumerable> dynamicPropertiesToSelect = + dynamicPropertyDictionary.Where(x => selectExpandNode.SelectedDynamicProperties == null || selectExpandNode.SelectedDynamicProperties.Contains(x.Key)); + foreach (KeyValuePair dynamicProperty in dynamicPropertiesToSelect) + { + if (string.IsNullOrEmpty(dynamicProperty.Key)) { - PropertyInfo dynamicPropertyInfo = resourceContext.EdmModel.GetDynamicPropertyDictionary(structuredType); - if (dynamicPropertyInfo == null || structuredObject == null || - !structuredObject.TryGetPropertyValue(dynamicPropertyInfo.Name, out value) || value == null) - { - return; - } + continue; } - else + + if (declaredPropertyNameSet.Contains(dynamicProperty.Key)) { - value = ((EdmStructuredObject)structuredObject).TryGetDynamicProperties(); + throw Error.InvalidOperation(SRResources.DynamicPropertyNameAlreadyUsedAsDeclaredPropertyName, + dynamicProperty.Key, structuredType.FullTypeName()); } - IDictionary dynamicPropertyDictionary = (IDictionary)value; - - // Build a HashSet to store the declared property names. - // It is used to make sure the dynamic property name is different from all declared property names. - HashSet declaredPropertyNameSet = new HashSet(resource.Properties.Select(p => p.Name)); - List dynamicProperties = new List(); - - // To test SelectedDynamicProperties == null is enough to filter the dynamic properties. - // Because if SelectAllDynamicProperties == true, SelectedDynamicProperties should be null always. - // So `selectExpandNode.SelectedDynamicProperties == null` covers `SelectAllDynamicProperties == true` scenario. - // If `selectExpandNode.SelectedDynamicProperties != null`, then we should test whether the property is selected or not using "Contains(...)". - IEnumerable> dynamicPropertiesToSelect = - dynamicPropertyDictionary.Where(x => selectExpandNode.SelectedDynamicProperties == null || selectExpandNode.SelectedDynamicProperties.Contains(x.Key)); - foreach (KeyValuePair dynamicProperty in dynamicPropertiesToSelect) + object dynamicPropertyValue = dynamicProperty.Value; + if (dynamicPropertyValue == null) { - if (string.IsNullOrEmpty(dynamicProperty.Key)) + dynamicProperties.Add(new ODataProperty { - continue; - } + Name = dynamicProperty.Key, + Value = ODataNullValueExtensions.NullValue + }); - if (declaredPropertyNameSet.Contains(dynamicProperty.Key)) - { - throw Error.InvalidOperation(SRResources.DynamicPropertyNameAlreadyUsedAsDeclaredPropertyName, - dynamicProperty.Key, structuredType.FullTypeName()); - } + continue; + } - object dynamicPropertyValue = dynamicProperty.Value; - if (dynamicPropertyValue == null) + Type propertyType = dynamicPropertyValue.GetType(); + IEdmTypeReference edmTypeReference = serializierContext.GetEdmType(dynamicPropertyValue, propertyType, true); + if (edmTypeReference == null || edmTypeReference.IsStructuredOrUntyped()) + { + if (TypeHelper.IsEnum(propertyType)) { + // we don't have the Edm enum type in the model, let's write it as string. dynamicProperties.Add(new ODataProperty { Name = dynamicProperty.Key, - Value = ODataNullValueExtensions.NullValue + + // TBD: Shall we write the un-declared enum value as full-name string? + // So, "Data":"Apple" => should be ""Data":"Namespace.EnumTypeName.Apple" ? + Value = dynamicPropertyValue.ToString() }); continue; } - Type propertyType = dynamicPropertyValue.GetType(); - IEdmTypeReference edmTypeReference = serializierContext.GetEdmType(dynamicPropertyValue, propertyType, true); - if (edmTypeReference == null || edmTypeReference.IsStructuredOrUntyped()) - { - if (TypeHelper.IsEnum(propertyType)) - { - // we don't have the Edm enum type in the model, let's write it as string. - dynamicProperties.Add(new ODataProperty - { - Name = dynamicProperty.Key, - - // TBD: Shall we write the un-declared enum value as full-name string? - // So, "Data":"Apple" => should be ""Data":"Namespace.EnumTypeName.Apple" ? - Value = dynamicPropertyValue.ToString() - }); - - continue; - } - - resourceContext.AppendDynamicOrUntypedProperty(dynamicProperty.Key, dynamicPropertyValue); - } - else - { - IODataEdmTypeSerializer propertySerializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); - if (propertySerializer == null) - { - throw Error.NotSupported(SRResources.DynamicPropertyCannotBeSerialized, dynamicProperty.Key, - edmTypeReference.FullName()); - } - - dynamicProperties.Add(propertySerializer.CreateProperty( - dynamicPropertyValue, edmTypeReference, dynamicProperty.Key, serializierContext)); - } - } - - if (dynamicProperties.Any()) - { - resource.Properties = resource.Properties.Concat(dynamicProperties); - } - } - - /// - /// Creates the ETag for the given entity. - /// - /// The context for the resource instance being written. - /// The created ETag. - public virtual string CreateETag(ResourceContext resourceContext) - { - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); + resourceContext.AppendDynamicOrUntypedProperty(dynamicProperty.Key, dynamicPropertyValue); } - - if (resourceContext.Request != null) + else { - IEdmModel model = resourceContext.EdmModel; - IEdmNavigationSource navigationSource = resourceContext.NavigationSource; - - IEnumerable concurrencyProperties; - if (model != null && navigationSource != null) - { - concurrencyProperties = model.GetConcurrencyProperties(navigationSource); - } - else - { - concurrencyProperties = Enumerable.Empty(); - } - - IDictionary properties = null; - foreach (IEdmStructuralProperty etagProperty in concurrencyProperties) + IODataEdmTypeSerializer propertySerializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); + if (propertySerializer == null) { - properties ??= new SortedDictionary(); - - properties.Add(etagProperty.Name, resourceContext.GetPropertyValue(etagProperty.Name)); + throw Error.NotSupported(SRResources.DynamicPropertyCannotBeSerialized, dynamicProperty.Key, + edmTypeReference.FullName()); } - if (properties != null) - { - return resourceContext.Request.CreateETag(properties, resourceContext.TimeZone); - } + dynamicProperties.Add(propertySerializer.CreateProperty( + dynamicPropertyValue, edmTypeReference, dynamicProperty.Key, serializierContext)); } + } - return null; + if (dynamicProperties.Any()) + { + resource.Properties = resource.Properties.Concat(dynamicProperties); } + } + /// + /// Creates the ETag for the given entity. + /// + /// The context for the resource instance being written. + /// The created ETag. + public virtual string CreateETag(ResourceContext resourceContext) + { + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } - /// - /// Write the navigation link for the select navigation properties. - /// - private async Task WriteNavigationLinksAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + if (resourceContext.Request != null) { - Contract.Assert(selectExpandNode != null); - Contract.Assert(resourceContext != null); + IEdmModel model = resourceContext.EdmModel; + IEdmNavigationSource navigationSource = resourceContext.NavigationSource; - if (selectExpandNode.SelectedNavigationProperties == null) + IEnumerable concurrencyProperties; + if (model != null && navigationSource != null) { - return; + concurrencyProperties = model.GetConcurrencyProperties(navigationSource); } - - IEnumerable navigationLinks = CreateNavigationLinks(selectExpandNode.SelectedNavigationProperties, resourceContext); - foreach (ODataNestedResourceInfo navigationLink in navigationLinks) + else { - await writer.WriteStartAsync(navigationLink).ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); + concurrencyProperties = Enumerable.Empty(); } - } - private async Task WriteDynamicComplexPropertiesAsync(ResourceContext resourceContext, ODataWriter writer) - { - Contract.Assert(resourceContext != null); - Contract.Assert(resourceContext.EdmModel != null); - - if (resourceContext.DynamicComplexProperties == null) + IDictionary properties = null; + foreach (IEdmStructuralProperty etagProperty in concurrencyProperties) { - return; + properties ??= new SortedDictionary(); + + properties.Add(etagProperty.Name, resourceContext.GetPropertyValue(etagProperty.Name)); } - foreach (KeyValuePair dynamicComplexProperty in resourceContext.DynamicComplexProperties) + if (properties != null) { - // If the dynamic property is "null", it should be treated ahead by creating an ODataProperty with ODataNullValue. - // However, it's safety here to skip the null dynamic property. - if (String.IsNullOrEmpty(dynamicComplexProperty.Key) || dynamicComplexProperty.Value == null) - { - continue; - } - - IEdmTypeReference edmTypeReference = - resourceContext.SerializerContext.GetEdmType(dynamicComplexProperty.Value, - dynamicComplexProperty.Value.GetType(), true); - - if (edmTypeReference.IsStructuredOrUntyped()) - { - ODataNestedResourceInfo nestedResourceInfo - = CreateDynamicComplexNestedResourceInfo(dynamicComplexProperty.Key, dynamicComplexProperty.Value, edmTypeReference, resourceContext); - - if (nestedResourceInfo != null) - { - await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); - await WriteDynamicComplexPropertyAsync(dynamicComplexProperty.Value, edmTypeReference, resourceContext, writer) - .ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); - } - } + return resourceContext.Request.CreateETag(properties, resourceContext.TimeZone); } } - private async Task WriteDynamicComplexPropertyAsync(object propertyValue, IEdmTypeReference edmType, ResourceContext resourceContext, ODataWriter writer) - { - Contract.Assert(resourceContext != null); - Contract.Assert(writer != null); + return null; + } - // If the dynamic property is "null", it should be treated ahead by creating an ODataProperty with ODataNullValue. - Contract.Assert(propertyValue != null); - // Create the serializer context for the nested and expanded item. - ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, null, null); + /// + /// Write the navigation link for the select navigation properties. + /// + private async Task WriteNavigationLinksAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(selectExpandNode != null); + Contract.Assert(resourceContext != null); - // Write object. - IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmType); - if (serializer == null) - { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeSerialized, edmType.ToTraceString())); - } + if (selectExpandNode.SelectedNavigationProperties == null) + { + return; + } - await serializer.WriteObjectInlineAsync(propertyValue, edmType, writer, nestedWriteContext).ConfigureAwait(false); + IEnumerable navigationLinks = CreateNavigationLinks(selectExpandNode.SelectedNavigationProperties, resourceContext); + foreach (ODataNestedResourceInfo navigationLink in navigationLinks) + { + await writer.WriteStartAsync(navigationLink).ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); } + } + + private async Task WriteDynamicComplexPropertiesAsync(ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(resourceContext != null); + Contract.Assert(resourceContext.EdmModel != null); - /// - /// Writing the declared Edm.Untyped properties. - /// - private async Task WriteUntypedPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + if (resourceContext.DynamicComplexProperties == null) { - Contract.Assert(selectExpandNode != null); - Contract.Assert(resourceContext != null); - Contract.Assert(writer != null); + return; + } - if (selectExpandNode.SelectedStructuralProperties == null) + foreach (KeyValuePair dynamicComplexProperty in resourceContext.DynamicComplexProperties) + { + // If the dynamic property is "null", it should be treated ahead by creating an ODataProperty with ODataNullValue. + // However, it's safety here to skip the null dynamic property. + if (String.IsNullOrEmpty(dynamicComplexProperty.Key) || dynamicComplexProperty.Value == null) { - return; + continue; } - foreach (IEdmStructuralProperty structuralProperty in selectExpandNode.SelectedStructuralProperties) - { - if (structuralProperty.Type == null || - (!structuralProperty.Type.IsUntyped() && !structuralProperty.Type.IsCollectionUntyped())) - { - continue; - } + IEdmTypeReference edmTypeReference = + resourceContext.SerializerContext.GetEdmType(dynamicComplexProperty.Value, + dynamicComplexProperty.Value.GetType(), true); - object propertyValue = CreateUntypedPropertyValue(structuralProperty, resourceContext, out IEdmTypeReference actualType); - if (propertyValue == null) - { - // if we get a null value, it means to skip it. - continue; - } + if (edmTypeReference.IsStructuredOrUntyped()) + { + ODataNestedResourceInfo nestedResourceInfo + = CreateDynamicComplexNestedResourceInfo(dynamicComplexProperty.Key, dynamicComplexProperty.Value, edmTypeReference, resourceContext); - if (propertyValue is ODataProperty odataProperty) + if (nestedResourceInfo != null) { - await writer.WriteStartAsync(odataProperty).ConfigureAwait(false); + await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); + await WriteDynamicComplexPropertyAsync(dynamicComplexProperty.Value, edmTypeReference, resourceContext, writer) + .ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); - continue; - } - - // Ok, it's an resource or collection value. - if (actualType != null && actualType.IsStructuredOrUntyped()) - { - ODataNestedResourceInfo nestedResourceInfo - = CreateUntypedNestedResourceInfo(structuralProperty, propertyValue, actualType, null/*not used now*/, resourceContext); - - if (nestedResourceInfo != null) - { - await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); - await WriteDynamicComplexPropertyAsync(propertyValue, actualType, resourceContext, writer).ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); - } } } } + } - /// - /// Creates the to be written while writing this declared untyped property. - /// - /// The declared untyped property for which the nested resource info is being created. - /// The declared property real value. - /// The resolved edm type of the declared property real value. - /// The corresponding sub select item belongs to this untyped property. - /// The context for the untyped instance being written. - /// The nested resource info to be written. Returns 'null' will omit this untyped serialization. - /// It enables customer to get more control by overriding this method. - public virtual ODataNestedResourceInfo CreateUntypedNestedResourceInfo(IEdmStructuralProperty structuralProperty, - object propertyValue, IEdmTypeReference valueType, - PathSelectItem pathSelectItem, ResourceContext resourceContext) - { - if (structuralProperty == null) - { - throw Error.ArgumentNull(nameof(structuralProperty)); - } + private async Task WriteDynamicComplexPropertyAsync(object propertyValue, IEdmTypeReference edmType, ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(resourceContext != null); + Contract.Assert(writer != null); - if (valueType != null) - { - return new ODataNestedResourceInfo - { - IsCollection = valueType.IsCollection(), - Name = structuralProperty.Name - }; - } + // If the dynamic property is "null", it should be treated ahead by creating an ODataProperty with ODataNullValue. + Contract.Assert(propertyValue != null); - return null; - } + // Create the serializer context for the nested and expanded item. + ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, null, null); - private async Task WriteStreamPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + // Write object. + IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmType); + if (serializer == null) { - Contract.Assert(selectExpandNode != null); - Contract.Assert(resourceContext != null); - Contract.Assert(writer != null); + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, edmType.ToTraceString())); + } - if (selectExpandNode.SelectedStructuralProperties != null) - { - IEnumerable structuralProperties = selectExpandNode.SelectedStructuralProperties; + await serializer.WriteObjectInlineAsync(propertyValue, edmType, writer, nestedWriteContext).ConfigureAwait(false); + } - foreach (IEdmStructuralProperty structuralProperty in structuralProperties) - { - if (structuralProperty.Type != null && structuralProperty.Type.IsStream()) - { - ODataStreamPropertyInfo property = CreateStreamProperty(structuralProperty, resourceContext); + /// + /// Writing the declared Edm.Untyped properties. + /// + private async Task WriteUntypedPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(selectExpandNode != null); + Contract.Assert(resourceContext != null); + Contract.Assert(writer != null); - if (property != null) - { - await writer.WriteStartAsync(property).ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); - } - } - } - } + if (selectExpandNode.SelectedStructuralProperties == null) + { + return; } - private async Task WriteComplexPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + foreach (IEdmStructuralProperty structuralProperty in selectExpandNode.SelectedStructuralProperties) { - Contract.Assert(selectExpandNode != null); - Contract.Assert(resourceContext != null); - Contract.Assert(writer != null); + if (structuralProperty.Type == null || + (!structuralProperty.Type.IsUntyped() && !structuralProperty.Type.IsCollectionUntyped())) + { + continue; + } - IDictionary complexProperties = selectExpandNode.SelectedComplexProperties; - if (complexProperties == null) + object propertyValue = CreateUntypedPropertyValue(structuralProperty, resourceContext, out IEdmTypeReference actualType); + if (propertyValue == null) { - return; + // if we get a null value, it means to skip it. + continue; } - if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) + if (propertyValue is ODataProperty odataProperty) { - IDelta deltaObject = resourceContext.EdmObject as IDelta; - IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); - complexProperties = complexProperties.Where(p => changedProperties.Contains(p.Key.Name)).ToDictionary(a => a.Key, a => a.Value); + await writer.WriteStartAsync(odataProperty).ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); + continue; } - foreach (KeyValuePair selectedComplex in complexProperties) + // Ok, it's an resource or collection value. + if (actualType != null && actualType.IsStructuredOrUntyped()) { - IEdmStructuralProperty complexProperty = selectedComplex.Key; + ODataNestedResourceInfo nestedResourceInfo + = CreateUntypedNestedResourceInfo(structuralProperty, propertyValue, actualType, null/*not used now*/, resourceContext); - ODataNestedResourceInfo nestedResourceInfo = CreateComplexNestedResourceInfo(complexProperty, selectedComplex.Value, resourceContext); if (nestedResourceInfo != null) { await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); - await WriteComplexAndExpandedNavigationPropertyAsync(complexProperty, selectedComplex.Value, resourceContext, writer) - .ConfigureAwait(false); + await WriteDynamicComplexPropertyAsync(propertyValue, actualType, resourceContext, writer).ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); } } } + } - private async Task WriteExpandedNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + /// + /// Creates the to be written while writing this declared untyped property. + /// + /// The declared untyped property for which the nested resource info is being created. + /// The declared property real value. + /// The resolved edm type of the declared property real value. + /// The corresponding sub select item belongs to this untyped property. + /// The context for the untyped instance being written. + /// The nested resource info to be written. Returns 'null' will omit this untyped serialization. + /// It enables customer to get more control by overriding this method. + public virtual ODataNestedResourceInfo CreateUntypedNestedResourceInfo(IEdmStructuralProperty structuralProperty, + object propertyValue, IEdmTypeReference valueType, + PathSelectItem pathSelectItem, ResourceContext resourceContext) + { + if (structuralProperty == null) { - Contract.Assert(resourceContext != null); - Contract.Assert(writer != null); + throw Error.ArgumentNull(nameof(structuralProperty)); + } - IDictionary navigationPropertiesToExpand = selectExpandNode.ExpandedProperties; - if (navigationPropertiesToExpand == null) + if (valueType != null) + { + return new ODataNestedResourceInfo { - return; - } + IsCollection = valueType.IsCollection(), + Name = structuralProperty.Name + }; + } - foreach (KeyValuePair navPropertyToExpand in navigationPropertiesToExpand) - { - IEdmNavigationProperty navigationProperty = navPropertyToExpand.Key; + return null; + } - ODataNestedResourceInfo navigationLink = CreateNavigationLink(navigationProperty, resourceContext); - if (navigationLink != null) + private async Task WriteStreamPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(selectExpandNode != null); + Contract.Assert(resourceContext != null); + Contract.Assert(writer != null); + + if (selectExpandNode.SelectedStructuralProperties != null) + { + IEnumerable structuralProperties = selectExpandNode.SelectedStructuralProperties; + + foreach (IEdmStructuralProperty structuralProperty in structuralProperties) + { + if (structuralProperty.Type != null && structuralProperty.Type.IsStream()) { - await writer.WriteStartAsync(navigationLink).ConfigureAwait(false); - await WriteComplexAndExpandedNavigationPropertyAsync(navigationProperty, navPropertyToExpand.Value, resourceContext, writer).ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); + ODataStreamPropertyInfo property = CreateStreamProperty(structuralProperty, resourceContext); + + if (property != null) + { + await writer.WriteStartAsync(property).ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); + } } } } + } + + private async Task WriteComplexPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(selectExpandNode != null); + Contract.Assert(resourceContext != null); + Contract.Assert(writer != null); - private async Task WriteReferencedNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + IDictionary complexProperties = selectExpandNode.SelectedComplexProperties; + if (complexProperties == null) { - Contract.Assert(resourceContext != null); - Contract.Assert(writer != null); + return; + } - IDictionary referencedPropertiesToExpand = selectExpandNode.ReferencedProperties; - if (referencedPropertiesToExpand == null) - { - return; - } + if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) + { + IDelta deltaObject = resourceContext.EdmObject as IDelta; + IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); + complexProperties = complexProperties.Where(p => changedProperties.Contains(p.Key.Name)).ToDictionary(a => a.Key, a => a.Value); + } - foreach (KeyValuePair referenced in referencedPropertiesToExpand) - { - IEdmNavigationProperty navigationProperty = referenced.Key; + foreach (KeyValuePair selectedComplex in complexProperties) + { + IEdmStructuralProperty complexProperty = selectedComplex.Key; - ODataNestedResourceInfo nestedResourceInfo = CreateNavigationLink(navigationProperty, resourceContext); - if (nestedResourceInfo != null) - { - await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); - await WriteComplexAndExpandedNavigationPropertyAsync(navigationProperty, referenced.Value, resourceContext, writer) - .ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); - } + ODataNestedResourceInfo nestedResourceInfo = CreateComplexNestedResourceInfo(complexProperty, selectedComplex.Value, resourceContext); + if (nestedResourceInfo != null) + { + await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); + await WriteComplexAndExpandedNavigationPropertyAsync(complexProperty, selectedComplex.Value, resourceContext, writer) + .ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); } } + } - private async Task WriteComplexAndExpandedNavigationPropertyAsync(IEdmProperty edmProperty, SelectItem selectItem, ResourceContext resourceContext, ODataWriter writer) + private async Task WriteExpandedNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(resourceContext != null); + Contract.Assert(writer != null); + + IDictionary navigationPropertiesToExpand = selectExpandNode.ExpandedProperties; + if (navigationPropertiesToExpand == null) { - Contract.Assert(edmProperty != null); - Contract.Assert(resourceContext != null); - Contract.Assert(writer != null); + return; + } - object propertyValue = resourceContext.GetPropertyValue(edmProperty.Name); + foreach (KeyValuePair navPropertyToExpand in navigationPropertiesToExpand) + { + IEdmNavigationProperty navigationProperty = navPropertyToExpand.Key; - if (propertyValue == null || propertyValue is NullEdmComplexObject) + ODataNestedResourceInfo navigationLink = CreateNavigationLink(navigationProperty, resourceContext); + if (navigationLink != null) { - if (edmProperty.Type.IsCollection()) - { - // A complex or navigation property whose Type attribute specifies a collection, the collection always exists, - // it may just be empty. - // If a collection of complex or entities can be related, it is represented as a JSON array. An empty - // collection of resources (one that contains no resource) is represented as an empty JSON array. - await writer.WriteStartAsync(new ODataResourceSet - { - TypeName = edmProperty.Type.FullName() - }).ConfigureAwait(false); - } - else - { - // If at most one resource can be related, the value is null if no resource is currently related. - await writer.WriteStartAsync(resource: null).ConfigureAwait(false); - } - + await writer.WriteStartAsync(navigationLink).ConfigureAwait(false); + await WriteComplexAndExpandedNavigationPropertyAsync(navigationProperty, navPropertyToExpand.Value, resourceContext, writer).ConfigureAwait(false); await writer.WriteEndAsync().ConfigureAwait(false); } - else - { - // create the serializer context for the complex and expanded item. - ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, edmProperty, resourceContext.SerializerContext.QueryContext, selectItem); + } + } - // write object. - IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmProperty.Type); - if (serializer == null) - { - throw new SerializationException(Error.Format(SRResources.TypeCannotBeSerialized, edmProperty.Type.ToTraceString())); - } + private async Task WriteReferencedNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(resourceContext != null); + Contract.Assert(writer != null); - await serializer.WriteObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext) - .ConfigureAwait(false); - } + IDictionary referencedPropertiesToExpand = selectExpandNode.ReferencedProperties; + if (referencedPropertiesToExpand == null) + { + return; } - private IEnumerable CreateNavigationLinks( - IEnumerable navigationProperties, ResourceContext resourceContext) + foreach (KeyValuePair referenced in referencedPropertiesToExpand) { - Contract.Assert(navigationProperties != null); - Contract.Assert(resourceContext != null); + IEdmNavigationProperty navigationProperty = referenced.Key; - foreach (IEdmNavigationProperty navProperty in navigationProperties) + ODataNestedResourceInfo nestedResourceInfo = CreateNavigationLink(navigationProperty, resourceContext); + if (nestedResourceInfo != null) { - ODataNestedResourceInfo navigationLink = CreateNavigationLink(navProperty, resourceContext); - if (ShouldWriteNavigation(navigationLink, resourceContext)) - { - yield return navigationLink; - } + await writer.WriteStartAsync(nestedResourceInfo).ConfigureAwait(false); + await WriteComplexAndExpandedNavigationPropertyAsync(navigationProperty, referenced.Value, resourceContext, writer) + .ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); } } + } + + private async Task WriteComplexAndExpandedNavigationPropertyAsync(IEdmProperty edmProperty, SelectItem selectItem, ResourceContext resourceContext, ODataWriter writer) + { + Contract.Assert(edmProperty != null); + Contract.Assert(resourceContext != null); + Contract.Assert(writer != null); + + object propertyValue = resourceContext.GetPropertyValue(edmProperty.Name); - /// - /// Checks whether a navigation link should be written or not. - /// - /// The navigation link to be written. - /// The resource context for the resource whose navigation link is being written. - /// true if navigation link should be written; otherwise false. - protected virtual bool ShouldWriteNavigation(ODataNestedResourceInfo navigationLink, ResourceContext resourceContext) + if (propertyValue == null || propertyValue is NullEdmComplexObject) { - if (navigationLink?.Url != null || (navigationLink != null && resourceContext.SerializerContext.MetadataLevel == ODataMetadataLevel.Full)) + if (edmProperty.Type.IsCollection()) { - return true; + // A complex or navigation property whose Type attribute specifies a collection, the collection always exists, + // it may just be empty. + // If a collection of complex or entities can be related, it is represented as a JSON array. An empty + // collection of resources (one that contains no resource) is represented as an empty JSON array. + await writer.WriteStartAsync(new ODataResourceSet + { + TypeName = edmProperty.Type.FullName() + }).ConfigureAwait(false); + } + else + { + // If at most one resource can be related, the value is null if no resource is currently related. + await writer.WriteStartAsync(resource: null).ConfigureAwait(false); } - return false; + await writer.WriteEndAsync().ConfigureAwait(false); } - - /// - /// Creates the to be written while writing this dynamic complex property. - /// - /// The dynamic property name. - /// The dynamic property value. - /// The edm type reference. - /// The context for the complex instance being written. - /// The nested resource info to be written. Returns 'null' will omit this serialization. - /// It enables customer to get more control by overriding this method. - public virtual ODataNestedResourceInfo CreateDynamicComplexNestedResourceInfo(string propertyName, object propertyValue, IEdmTypeReference edmType, ResourceContext resourceContext) + else { - ODataNestedResourceInfo nestedInfo = null; - if (propertyName != null && edmType != null) + // create the serializer context for the complex and expanded item. + ODataSerializerContext nestedWriteContext = new ODataSerializerContext(resourceContext, edmProperty, resourceContext.SerializerContext.QueryContext, selectItem); + + // write object. + IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmProperty.Type); + if (serializer == null) { - nestedInfo = new ODataNestedResourceInfo - { - IsCollection = edmType.IsCollection(), - Name = propertyName, - }; + throw new SerializationException(Error.Format(SRResources.TypeCannotBeSerialized, edmProperty.Type.ToTraceString())); } - return nestedInfo; + await serializer.WriteObjectInlineAsync(propertyValue, edmProperty.Type, writer, nestedWriteContext) + .ConfigureAwait(false); } + } + + private IEnumerable CreateNavigationLinks( + IEnumerable navigationProperties, ResourceContext resourceContext) + { + Contract.Assert(navigationProperties != null); + Contract.Assert(resourceContext != null); - /// - /// Creates the to be written while writing this complex property. - /// - /// The complex property for which the nested resource info is being created. - /// The corresponding sub select item belongs to this complex property. - /// The context for the complex instance being written. - /// The nested resource info to be written. Returns 'null' will omit this complex serialization. - /// It enables customer to get more control by overriding this method. - public virtual ODataNestedResourceInfo CreateComplexNestedResourceInfo(IEdmStructuralProperty complexProperty, PathSelectItem pathSelectItem, ResourceContext resourceContext) + foreach (IEdmNavigationProperty navProperty in navigationProperties) { - if (complexProperty == null) + ODataNestedResourceInfo navigationLink = CreateNavigationLink(navProperty, resourceContext); + if (ShouldWriteNavigation(navigationLink, resourceContext)) { - throw Error.ArgumentNull(nameof(complexProperty)); + yield return navigationLink; } + } + } - ODataNestedResourceInfo nestedInfo = null; + /// + /// Checks whether a navigation link should be written or not. + /// + /// The navigation link to be written. + /// The resource context for the resource whose navigation link is being written. + /// true if navigation link should be written; otherwise false. + protected virtual bool ShouldWriteNavigation(ODataNestedResourceInfo navigationLink, ResourceContext resourceContext) + { + if (navigationLink?.Url != null || (navigationLink != null && resourceContext.SerializerContext.MetadataLevel == ODataMetadataLevel.Full)) + { + return true; + } + + return false; + } - if (complexProperty.Type != null) + /// + /// Creates the to be written while writing this dynamic complex property. + /// + /// The dynamic property name. + /// The dynamic property value. + /// The edm type reference. + /// The context for the complex instance being written. + /// The nested resource info to be written. Returns 'null' will omit this serialization. + /// It enables customer to get more control by overriding this method. + public virtual ODataNestedResourceInfo CreateDynamicComplexNestedResourceInfo(string propertyName, object propertyValue, IEdmTypeReference edmType, ResourceContext resourceContext) + { + ODataNestedResourceInfo nestedInfo = null; + if (propertyName != null && edmType != null) + { + nestedInfo = new ODataNestedResourceInfo { - nestedInfo = new ODataNestedResourceInfo - { - IsCollection = complexProperty.Type.IsCollection(), - Name = complexProperty.Name - }; - } + IsCollection = edmType.IsCollection(), + Name = propertyName, + }; + } + + return nestedInfo; + } - return nestedInfo; + /// + /// Creates the to be written while writing this complex property. + /// + /// The complex property for which the nested resource info is being created. + /// The corresponding sub select item belongs to this complex property. + /// The context for the complex instance being written. + /// The nested resource info to be written. Returns 'null' will omit this complex serialization. + /// It enables customer to get more control by overriding this method. + public virtual ODataNestedResourceInfo CreateComplexNestedResourceInfo(IEdmStructuralProperty complexProperty, PathSelectItem pathSelectItem, ResourceContext resourceContext) + { + if (complexProperty == null) + { + throw Error.ArgumentNull(nameof(complexProperty)); } - /// - /// Creates the to be written while writing this entity. - /// - /// The navigation property for which the navigation link is being created. - /// The context for the entity instance being written. - /// The navigation link to be written. - public virtual ODataNestedResourceInfo CreateNavigationLink(IEdmNavigationProperty navigationProperty, ResourceContext resourceContext) + ODataNestedResourceInfo nestedInfo = null; + + if (complexProperty.Type != null) { - if (navigationProperty == null) + nestedInfo = new ODataNestedResourceInfo { - throw Error.ArgumentNull(nameof(navigationProperty)); - } + IsCollection = complexProperty.Type.IsCollection(), + Name = complexProperty.Name + }; + } - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } + return nestedInfo; + } + + /// + /// Creates the to be written while writing this entity. + /// + /// The navigation property for which the navigation link is being created. + /// The context for the entity instance being written. + /// The navigation link to be written. + public virtual ODataNestedResourceInfo CreateNavigationLink(IEdmNavigationProperty navigationProperty, ResourceContext resourceContext) + { + if (navigationProperty == null) + { + throw Error.ArgumentNull(nameof(navigationProperty)); + } - ODataSerializerContext writeContext = resourceContext.SerializerContext; - IEdmNavigationSource navigationSource = writeContext.NavigationSource; - ODataNestedResourceInfo navigationLink = null; + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } - if (navigationProperty.Type != null) + ODataSerializerContext writeContext = resourceContext.SerializerContext; + IEdmNavigationSource navigationSource = writeContext.NavigationSource; + ODataNestedResourceInfo navigationLink = null; + + if (navigationProperty.Type != null) + { + IEdmTypeReference propertyType = navigationProperty.Type; + navigationLink = new ODataNestedResourceInfo { - IEdmTypeReference propertyType = navigationProperty.Type; - navigationLink = new ODataNestedResourceInfo - { - IsCollection = propertyType.IsCollection(), - Name = navigationProperty.Name, - }; + IsCollection = propertyType.IsCollection(), + Name = navigationProperty.Name, + }; - if (navigationSource != null) - { - IEdmModel model = writeContext.Model; - NavigationSourceLinkBuilderAnnotation linkBuilder = EdmModelLinkBuilderExtensions.GetNavigationSourceLinkBuilder(model, navigationSource); - Uri navigationUrl = linkBuilder.BuildNavigationLink(resourceContext, navigationProperty, writeContext.MetadataLevel); + if (navigationSource != null) + { + IEdmModel model = writeContext.Model; + NavigationSourceLinkBuilderAnnotation linkBuilder = EdmModelLinkBuilderExtensions.GetNavigationSourceLinkBuilder(model, navigationSource); + Uri navigationUrl = linkBuilder.BuildNavigationLink(resourceContext, navigationProperty, writeContext.MetadataLevel); - if (navigationUrl != null) - { - navigationLink.Url = navigationUrl; - } + if (navigationUrl != null) + { + navigationLink.Url = navigationUrl; } } - - return navigationLink; } - private IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) - { - Contract.Assert(selectExpandNode != null); - Contract.Assert(resourceContext != null); + return navigationLink; + } - int propertiesCount = (selectExpandNode.SelectedStructuralProperties?.Count ?? 0) + (selectExpandNode.SelectedComputedProperties?.Count ?? 0); - List properties = new List(propertiesCount); + private IEnumerable CreateStructuralPropertyBag(SelectExpandNode selectExpandNode, ResourceContext resourceContext) + { + Contract.Assert(selectExpandNode != null); + Contract.Assert(resourceContext != null); - if (selectExpandNode.SelectedStructuralProperties != null) - { - IEnumerable structuralProperties = selectExpandNode.SelectedStructuralProperties; + int propertiesCount = (selectExpandNode.SelectedStructuralProperties?.Count ?? 0) + (selectExpandNode.SelectedComputedProperties?.Count ?? 0); + List properties = new List(propertiesCount); - if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) + if (selectExpandNode.SelectedStructuralProperties != null) + { + IEnumerable structuralProperties = selectExpandNode.SelectedStructuralProperties; + + if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource()) + { + if (resourceContext.EdmObject is TypedEdmEntityObject obj && obj.Instance is IDelta deltaObject) { - if (resourceContext.EdmObject is TypedEdmEntityObject obj && obj.Instance is IDelta deltaObject) - { - IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); - structuralProperties = structuralProperties.Where(p => changedProperties.Contains(p.Name)); - } + IEnumerable changedProperties = deltaObject.GetChangedPropertyNames(); + structuralProperties = structuralProperties.Where(p => changedProperties.Contains(p.Name)); } + } - foreach (IEdmStructuralProperty structuralProperty in structuralProperties) + foreach (IEdmStructuralProperty structuralProperty in structuralProperties) + { + if (structuralProperty.Type != null && structuralProperty.Type.IsStream()) { - if (structuralProperty.Type != null && structuralProperty.Type.IsStream()) - { - // skip the stream property, the stream property is written in its own logic - continue; - } + // skip the stream property, the stream property is written in its own logic + continue; + } - if (structuralProperty.Type != null && - (structuralProperty.Type.IsUntyped() || structuralProperty.Type.IsCollectionUntyped())) - { - // skip it here, we use a different method to write all 'declared' untyped properties - continue; - } + if (structuralProperty.Type != null && + (structuralProperty.Type.IsUntyped() || structuralProperty.Type.IsCollectionUntyped())) + { + // skip it here, we use a different method to write all 'declared' untyped properties + continue; + } - ODataProperty property = CreateStructuralProperty(structuralProperty, resourceContext); - if (property != null) - { - properties.Add(property); - } + ODataProperty property = CreateStructuralProperty(structuralProperty, resourceContext); + if (property != null) + { + properties.Add(property); } } + } - // Try to add computed properties - if (selectExpandNode.SelectedComputedProperties != null) + // Try to add computed properties + if (selectExpandNode.SelectedComputedProperties != null) + { + foreach (string propertyName in selectExpandNode.SelectedComputedProperties) { - foreach (string propertyName in selectExpandNode.SelectedComputedProperties) + ODataProperty property = CreateComputedProperty(propertyName, resourceContext); + if (property != null) { - ODataProperty property = CreateComputedProperty(propertyName, resourceContext); - if (property != null) - { - properties.Add(property); - } + properties.Add(property); } } + } + + return properties; + } - return properties; + /// + /// Creates the to be written for the given resource. + /// + /// The computed property being written. + /// The context for the resource instance being written. + /// The to write. + public virtual ODataProperty CreateComputedProperty(string propertyName, ResourceContext resourceContext) + { + if (string.IsNullOrWhiteSpace(propertyName)) + { + throw Error.ArgumentNullOrEmpty(nameof(propertyName)); } - /// - /// Creates the to be written for the given resource. - /// - /// The computed property being written. - /// The context for the resource instance being written. - /// The to write. - public virtual ODataProperty CreateComputedProperty(string propertyName, ResourceContext resourceContext) + if (resourceContext == null) { - if (string.IsNullOrWhiteSpace(propertyName)) - { - throw Error.ArgumentNullOrEmpty(nameof(propertyName)); - } + throw Error.ArgumentNull(nameof(resourceContext)); + } - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } + // The computed value is from the Linq expression binding. + object propertyValue = resourceContext.GetPropertyValue(propertyName); + if (propertyValue == null) + { + return new ODataProperty { Name = propertyName, Value = null }; + } - // The computed value is from the Linq expression binding. - object propertyValue = resourceContext.GetPropertyValue(propertyName); - if (propertyValue == null) - { - return new ODataProperty { Name = propertyName, Value = null }; - } + ODataSerializerContext writeContext = resourceContext.SerializerContext; - ODataSerializerContext writeContext = resourceContext.SerializerContext; + IEdmTypeReference edmTypeReference = resourceContext.SerializerContext.GetEdmType(propertyValue, propertyValue.GetType()); + if (edmTypeReference == null) + { + throw Error.NotSupported(SRResources.TypeOfDynamicPropertyNotSupported, propertyValue.GetType().FullName, propertyName); + } - IEdmTypeReference edmTypeReference = resourceContext.SerializerContext.GetEdmType(propertyValue, propertyValue.GetType()); - if (edmTypeReference == null) - { - throw Error.NotSupported(SRResources.TypeOfDynamicPropertyNotSupported, propertyValue.GetType().FullName, propertyName); - } + IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); + if (serializer == null) + { + throw new SerializationException(Error.Format(SRResources.TypeCannotBeSerialized, edmTypeReference.FullName())); + } - IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(edmTypeReference); - if (serializer == null) - { - throw new SerializationException(Error.Format(SRResources.TypeCannotBeSerialized, edmTypeReference.FullName())); - } + return serializer.CreateProperty(propertyValue, edmTypeReference, propertyName, writeContext); + } - return serializer.CreateProperty(propertyValue, edmTypeReference, propertyName, writeContext); + /// + /// Creates the to be written for the given stream property. + /// + /// The EDM structural property being written. + /// The context for the entity instance being written. + /// The to write. + public virtual ODataStreamPropertyInfo CreateStreamProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext) + { + if (structuralProperty == null) + { + throw Error.ArgumentNull("structuralProperty"); } - /// - /// Creates the to be written for the given stream property. - /// - /// The EDM structural property being written. - /// The context for the entity instance being written. - /// The to write. - public virtual ODataStreamPropertyInfo CreateStreamProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext) + if (resourceContext == null) { - if (structuralProperty == null) - { - throw Error.ArgumentNull("structuralProperty"); - } + throw Error.ArgumentNull("resourceContext"); + } - if (resourceContext == null) - { - throw Error.ArgumentNull("resourceContext"); - } + if (structuralProperty.Type == null || !structuralProperty.Type.IsStream()) + { + return null; + } - if (structuralProperty.Type == null || !structuralProperty.Type.IsStream()) - { - return null; - } + if (resourceContext.SerializerContext.MetadataLevel != ODataMetadataLevel.Full) + { + return null; + } - if (resourceContext.SerializerContext.MetadataLevel != ODataMetadataLevel.Full) - { - return null; - } + // TODO: we need to return ODataStreamReferenceValue if + // 1) If we have the EditLink link builder + // 2) If we have the ReadLink link builder + // 3) If we have the Core.AcceptableMediaTypes annotation associated with the Stream property, - // TODO: we need to return ODataStreamReferenceValue if - // 1) If we have the EditLink link builder - // 2) If we have the ReadLink link builder - // 3) If we have the Core.AcceptableMediaTypes annotation associated with the Stream property, + // So far, let's return null and let OData.lib to calculate the ODataStreamReferenceValue by conventions. + return null; + } - // So far, let's return null and let OData.lib to calculate the ODataStreamReferenceValue by conventions. + /// + /// Creates the property value to be written for the given, declared, "Edm.Untyped" or collection property. + /// + /// The EDM structural property being written. + /// The context for the entity instance being written. + /// The inferred actual type. + /// 1) return 'null' to skip it. + /// 2) return 'ODataProperty' to write it as primitive property. + /// 3) otherwise, return the inferred value and its type. + public virtual object CreateUntypedPropertyValue(IEdmStructuralProperty structuralProperty, + ResourceContext resourceContext, out IEdmTypeReference actualType) + { + if (structuralProperty == null) + { + throw Error.ArgumentNull(nameof(structuralProperty)); + } + + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } + + actualType = null; + if (structuralProperty.Type == null || + (!structuralProperty.Type.IsUntyped() && !structuralProperty.Type.IsCollectionUntyped())) + { return null; } - /// - /// Creates the property value to be written for the given, declared, "Edm.Untyped" or collection property. - /// - /// The EDM structural property being written. - /// The context for the entity instance being written. - /// The inferred actual type. - /// 1) return 'null' to skip it. - /// 2) return 'ODataProperty' to write it as primitive property. - /// 3) otherwise, return the inferred value and its type. - public virtual object CreateUntypedPropertyValue(IEdmStructuralProperty structuralProperty, - ResourceContext resourceContext, out IEdmTypeReference actualType) + // Retrieve the original/raw value from Data source. + object propertyValue = resourceContext.GetPropertyValue(structuralProperty.Name); + if (propertyValue == null) { - if (structuralProperty == null) + return new ODataProperty { - throw Error.ArgumentNull(nameof(structuralProperty)); - } + Name = structuralProperty.Name, + Value = ODataNullValueExtensions.NullValue + }; + } - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } + Type propertyType = propertyValue.GetType(); + ODataSerializerContext writeContext = resourceContext.SerializerContext; - actualType = null; - if (structuralProperty.Type == null || - (!structuralProperty.Type.IsUntyped() && !structuralProperty.Type.IsCollectionUntyped())) - { - return null; - } + // Scenarios: + // 1) If we can get EdmType from model, Let's use it. + // 2) If no (aka, we don't have an Edm type associated). So, let's treat it a Untyped. + actualType = writeContext.GetEdmType(propertyValue, propertyType, true); - // Retrieve the original/raw value from Data source. - object propertyValue = resourceContext.GetPropertyValue(structuralProperty.Name); - if (propertyValue == null) + if (actualType.IsStructuredOrUntyped()) + { + if (TypeHelper.IsEnum(propertyType)) { + // we don't have the Edm enum type in the model, let's write it as string. return new ODataProperty { Name = structuralProperty.Name, - Value = ODataNullValueExtensions.NullValue + + // Shall we write the un-declared enum value as full-name string? + // So, "Data":"Apple" => should be ""Data":"Namespace.EnumTypeName.Apple" ? + // We keep it simple to write it as string (enum member name), not the full-name string. + Value = propertyValue.ToString() }; } - Type propertyType = propertyValue.GetType(); - ODataSerializerContext writeContext = resourceContext.SerializerContext; - - // Scenarios: - // 1) If we can get EdmType from model, Let's use it. - // 2) If no (aka, we don't have an Edm type associated). So, let's treat it a Untyped. - actualType = writeContext.GetEdmType(propertyValue, propertyType, true); - - if (actualType.IsStructuredOrUntyped()) - { - if (TypeHelper.IsEnum(propertyType)) - { - // we don't have the Edm enum type in the model, let's write it as string. - return new ODataProperty - { - Name = structuralProperty.Name, - - // Shall we write the un-declared enum value as full-name string? - // So, "Data":"Apple" => should be ""Data":"Namespace.EnumTypeName.Apple" ? - // We keep it simple to write it as string (enum member name), not the full-name string. - Value = propertyValue.ToString() - }; - } + return propertyValue; + } - return propertyValue; - } + // Ok, we have the Edm type associated and it's not strctured or untyped. + // we only handle the 'Primitive', the defined 'Enum' or collection of them. + IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(actualType); + if (serializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, structuralProperty.Type.FullName())); + } - // Ok, we have the Edm type associated and it's not strctured or untyped. - // we only handle the 'Primitive', the defined 'Enum' or collection of them. - IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(actualType); - if (serializer == null) - { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeSerialized, structuralProperty.Type.FullName())); - } + return serializer.CreateProperty(propertyValue, actualType, structuralProperty.Name, writeContext); + } - return serializer.CreateProperty(propertyValue, actualType, structuralProperty.Name, writeContext); + /// + /// Creates the to be written for the given entity and the structural property. + /// + /// The EDM structural property being written. + /// The context for the entity instance being written. + /// The to write. + public virtual ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext) + { + if (structuralProperty == null) + { + throw Error.ArgumentNull(nameof(structuralProperty)); } - - /// - /// Creates the to be written for the given entity and the structural property. - /// - /// The EDM structural property being written. - /// The context for the entity instance being written. - /// The to write. - public virtual ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext) + if (resourceContext == null) { - if (structuralProperty == null) - { - throw Error.ArgumentNull(nameof(structuralProperty)); - } - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } + throw Error.ArgumentNull(nameof(resourceContext)); + } - ODataSerializerContext writeContext = resourceContext.SerializerContext; + ODataSerializerContext writeContext = resourceContext.SerializerContext; - IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(structuralProperty.Type); - if (serializer == null) - { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeSerialized, structuralProperty.Type.FullName())); - } + IODataEdmTypeSerializer serializer = SerializerProvider.GetEdmTypeSerializer(structuralProperty.Type); + if (serializer == null) + { + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, structuralProperty.Type.FullName())); + } - object propertyValue = resourceContext.GetPropertyValue(structuralProperty.Name); + object propertyValue = resourceContext.GetPropertyValue(structuralProperty.Name); - IEdmTypeReference propertyType = structuralProperty.Type; - if (propertyValue != null) + IEdmTypeReference propertyType = structuralProperty.Type; + if (propertyValue != null) + { + if (!propertyType.IsPrimitive() && !propertyType.IsEnum()) { - if (!propertyType.IsPrimitive() && !propertyType.IsEnum()) + IEdmTypeReference actualType = writeContext.GetEdmType(propertyValue, propertyValue.GetType()); + if (propertyType != null && propertyType != actualType) { - IEdmTypeReference actualType = writeContext.GetEdmType(propertyValue, propertyValue.GetType()); - if (propertyType != null && propertyType != actualType) - { - propertyType = actualType; - } + propertyType = actualType; } } - - return serializer.CreateProperty(propertyValue, propertyType, structuralProperty.Name, writeContext); } - private IEnumerable CreateODataActions( - IEnumerable actions, ResourceContext resourceContext) - { - Contract.Assert(actions != null); - Contract.Assert(resourceContext != null); + return serializer.CreateProperty(propertyValue, propertyType, structuralProperty.Name, writeContext); + } - foreach (IEdmAction action in actions) + private IEnumerable CreateODataActions( + IEnumerable actions, ResourceContext resourceContext) + { + Contract.Assert(actions != null); + Contract.Assert(resourceContext != null); + + foreach (IEdmAction action in actions) + { + ODataAction oDataAction = CreateODataAction(action, resourceContext); + if (oDataAction != null) { - ODataAction oDataAction = CreateODataAction(action, resourceContext); - if (oDataAction != null) - { - yield return oDataAction; - } + yield return oDataAction; } } + } - private IEnumerable CreateODataFunctions( - IEnumerable functions, ResourceContext resourceContext) - { - Contract.Assert(functions != null); - Contract.Assert(resourceContext != null); + private IEnumerable CreateODataFunctions( + IEnumerable functions, ResourceContext resourceContext) + { + Contract.Assert(functions != null); + Contract.Assert(resourceContext != null); - foreach (IEdmFunction function in functions) + foreach (IEdmFunction function in functions) + { + ODataFunction oDataFunction = CreateODataFunction(function, resourceContext); + if (oDataFunction != null) { - ODataFunction oDataFunction = CreateODataFunction(function, resourceContext); - if (oDataFunction != null) - { - yield return oDataFunction; - } + yield return oDataFunction; } } + } - /// - /// Creates an to be written for the given action and the entity instance. - /// - /// The OData action. - /// The context for the entity instance being written. - /// The created action or null if the action should not be written. - [SuppressMessage("Microsoft.Usage", "CA2234: Pass System.Uri objects instead of strings", Justification = "This overload is equally good")] - public virtual ODataAction CreateODataAction(IEdmAction action, ResourceContext resourceContext) + /// + /// Creates an to be written for the given action and the entity instance. + /// + /// The OData action. + /// The context for the entity instance being written. + /// The created action or null if the action should not be written. + [SuppressMessage("Microsoft.Usage", "CA2234: Pass System.Uri objects instead of strings", Justification = "This overload is equally good")] + public virtual ODataAction CreateODataAction(IEdmAction action, ResourceContext resourceContext) + { + if (action == null) { - if (action == null) - { - throw Error.ArgumentNull(nameof(action)); - } + throw Error.ArgumentNull(nameof(action)); + } - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } + if (resourceContext == null) + { + throw Error.ArgumentNull(nameof(resourceContext)); + } - IEdmModel model = resourceContext.EdmModel; - OperationLinkBuilder builder = model.GetOperationLinkBuilder(action); + IEdmModel model = resourceContext.EdmModel; + OperationLinkBuilder builder = model.GetOperationLinkBuilder(action); - if (builder == null) - { - return null; - } + if (builder == null) + { + return null; + } - return CreateODataOperation(action, builder, resourceContext) as ODataAction; + return CreateODataOperation(action, builder, resourceContext) as ODataAction; + } + + /// + /// Creates an to be written for the given action and the entity instance. + /// + /// The OData function. + /// The context for the entity instance being written. + /// The created function or null if the action should not be written. + [SuppressMessage("Microsoft.Usage", "CA2234: Pass System.Uri objects instead of strings", + Justification = "This overload is equally good")] + [SuppressMessage("Microsoft.Naming", "CA1716: Use function as parameter name", Justification = "Function")] + public virtual ODataFunction CreateODataFunction(IEdmFunction function, ResourceContext resourceContext) + { + if (function == null) + { + throw Error.ArgumentNull(nameof(function)); } - /// - /// Creates an to be written for the given action and the entity instance. - /// - /// The OData function. - /// The context for the entity instance being written. - /// The created function or null if the action should not be written. - [SuppressMessage("Microsoft.Usage", "CA2234: Pass System.Uri objects instead of strings", - Justification = "This overload is equally good")] - [SuppressMessage("Microsoft.Naming", "CA1716: Use function as parameter name", Justification = "Function")] - public virtual ODataFunction CreateODataFunction(IEdmFunction function, ResourceContext resourceContext) + if (resourceContext == null) { - if (function == null) - { - throw Error.ArgumentNull(nameof(function)); - } + throw Error.ArgumentNull(nameof(resourceContext)); + } - if (resourceContext == null) - { - throw Error.ArgumentNull(nameof(resourceContext)); - } + IEdmModel model = resourceContext.EdmModel; + OperationLinkBuilder builder = model.GetOperationLinkBuilder(function); - IEdmModel model = resourceContext.EdmModel; - OperationLinkBuilder builder = model.GetOperationLinkBuilder(function); + if (builder == null) + { + return null; + } - if (builder == null) - { - return null; - } + return CreateODataOperation(function, builder, resourceContext) as ODataFunction; + } - return CreateODataOperation(function, builder, resourceContext) as ODataFunction; + private static ODataOperation CreateODataOperation(IEdmOperation operation, OperationLinkBuilder builder, ResourceContext resourceContext) + { + Contract.Assert(operation != null); + Contract.Assert(builder != null); + Contract.Assert(resourceContext != null); + + ODataMetadataLevel metadataLevel = resourceContext.SerializerContext.MetadataLevel; + IEdmModel model = resourceContext.EdmModel; + + if (ShouldOmitOperation(operation, builder, metadataLevel)) + { + return null; } - private static ODataOperation CreateODataOperation(IEdmOperation operation, OperationLinkBuilder builder, ResourceContext resourceContext) + Uri target = builder.BuildLink(resourceContext); + if (target == null) { - Contract.Assert(operation != null); - Contract.Assert(builder != null); - Contract.Assert(resourceContext != null); + return null; + } - ODataMetadataLevel metadataLevel = resourceContext.SerializerContext.MetadataLevel; - IEdmModel model = resourceContext.EdmModel; + Uri baseUri = new Uri(resourceContext.Request.CreateODataLink(MetadataSegment.Instance)); + Uri metadata = new Uri(baseUri, "#" + CreateMetadataFragment(operation)); - if (ShouldOmitOperation(operation, builder, metadataLevel)) - { - return null; - } + ODataOperation odataOperation; + if (operation is IEdmAction) + { + odataOperation = new ODataAction(); + } + else + { + odataOperation = new ODataFunction(); + } + odataOperation.Metadata = metadata; - Uri target = builder.BuildLink(resourceContext); - if (target == null) - { - return null; - } + // Always omit the title in minimal/no metadata modes. + if (metadataLevel == ODataMetadataLevel.Full) + { + EmitTitle(model, operation, odataOperation); + } - Uri baseUri = new Uri(resourceContext.Request.CreateODataLink(MetadataSegment.Instance)); - Uri metadata = new Uri(baseUri, "#" + CreateMetadataFragment(operation)); + // Omit the target in minimal/no metadata modes unless it doesn't follow conventions. + if (!builder.FollowsConventions || metadataLevel == ODataMetadataLevel.Full) + { + odataOperation.Target = target; + } - ODataOperation odataOperation; - if (operation is IEdmAction) - { - odataOperation = new ODataAction(); - } - else - { - odataOperation = new ODataFunction(); - } - odataOperation.Metadata = metadata; + return odataOperation; + } - // Always omit the title in minimal/no metadata modes. - if (metadataLevel == ODataMetadataLevel.Full) - { - EmitTitle(model, operation, odataOperation); - } + internal static void EmitTitle(IEdmModel model, IEdmOperation operation, ODataOperation odataOperation) + { + // The title should only be emitted in full metadata. + OperationTitleAnnotation titleAnnotation = model.GetOperationTitleAnnotation(operation); + if (titleAnnotation != null) + { + odataOperation.Title = titleAnnotation.Title; + } + else + { + odataOperation.Title = operation.Name; + } + } - // Omit the target in minimal/no metadata modes unless it doesn't follow conventions. - if (!builder.FollowsConventions || metadataLevel == ODataMetadataLevel.Full) - { - odataOperation.Target = target; - } + internal static string CreateMetadataFragment(IEdmOperation operation) + { + // There can only be one entity container in OData V4. + string actionName = operation.Name; + string fragment = operation.Namespace + "." + actionName; - return odataOperation; - } + return fragment; + } - internal static void EmitTitle(IEdmModel model, IEdmOperation operation, ODataOperation odataOperation) + private static IEdmStructuredType GetODataPathType(ODataSerializerContext serializerContext) + { + Contract.Assert(serializerContext != null); + if (serializerContext.EdmProperty != null) { - // The title should only be emitted in full metadata. - OperationTitleAnnotation titleAnnotation = model.GetOperationTitleAnnotation(operation); - if (titleAnnotation != null) + // we are in an nested complex or expanded navigation property. + if (serializerContext.EdmProperty.Type.IsCollection()) { - odataOperation.Title = titleAnnotation.Title; + return serializerContext.EdmProperty.Type.AsCollection().ElementType().ToStructuredType(); } else { - odataOperation.Title = operation.Name; + return serializerContext.EdmProperty.Type.AsStructured().StructuredDefinition(); } } - - internal static string CreateMetadataFragment(IEdmOperation operation) + else { - // There can only be one entity container in OData V4. - string actionName = operation.Name; - string fragment = operation.Namespace + "." + actionName; + if (serializerContext.ExpandedResource != null) + { + // we are in dynamic complex. + return null; + } - return fragment; - } + IEdmType edmType = null; - private static IEdmStructuredType GetODataPathType(ODataSerializerContext serializerContext) - { - Contract.Assert(serializerContext != null); - if (serializerContext.EdmProperty != null) + // figure out the type from the navigation source + if (serializerContext.NavigationSource != null) { - // we are in an nested complex or expanded navigation property. - if (serializerContext.EdmProperty.Type.IsCollection()) - { - return serializerContext.EdmProperty.Type.AsCollection().ElementType().ToStructuredType(); - } - else + edmType = serializerContext.NavigationSource.EntityType; + if (edmType.TypeKind == EdmTypeKind.Collection) { - return serializerContext.EdmProperty.Type.AsStructured().StructuredDefinition(); + edmType = (edmType as IEdmCollectionType).ElementType.Definition; } } - else - { - if (serializerContext.ExpandedResource != null) - { - // we are in dynamic complex. - return null; - } - - IEdmType edmType = null; - // figure out the type from the navigation source - if (serializerContext.NavigationSource != null) + // figure out the type from the path. + if (serializerContext.Path != null) + { + // Note: The navigation source may be different from the path if the instance has redefined the context + // (for example, in a flattened delta response) + if (serializerContext.NavigationSource == null || serializerContext.NavigationSource == serializerContext.Path.GetNavigationSource()) { - edmType = serializerContext.NavigationSource.EntityType; - if (edmType.TypeKind == EdmTypeKind.Collection) + edmType = serializerContext.Path.GetEdmType(); + if (edmType != null && edmType.TypeKind == EdmTypeKind.Collection) { edmType = (edmType as IEdmCollectionType).ElementType.Definition; } } - - // figure out the type from the path. - if (serializerContext.Path != null) - { - // Note: The navigation source may be different from the path if the instance has redefined the context - // (for example, in a flattened delta response) - if (serializerContext.NavigationSource == null || serializerContext.NavigationSource == serializerContext.Path.GetNavigationSource()) - { - edmType = serializerContext.Path.GetEdmType(); - if (edmType != null && edmType.TypeKind == EdmTypeKind.Collection) - { - edmType = (edmType as IEdmCollectionType).ElementType.Definition; - } - } - } - - return edmType as IEdmStructuredType; } + + return edmType as IEdmStructuredType; } + } - internal static void AddTypeNameAnnotationAsNeeded(ODataResource resource, IEdmStructuredType odataPathType, - ODataMetadataLevel metadataLevel) - { - // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties - // null when values should not be serialized. The TypeName property is different and should always be - // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not - // to serialize the type name (a null value prevents serialization). + internal static void AddTypeNameAnnotationAsNeeded(ODataResource resource, IEdmStructuredType odataPathType, + ODataMetadataLevel metadataLevel) + { + // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties + // null when values should not be serialized. The TypeName property is different and should always be + // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not + // to serialize the type name (a null value prevents serialization). - // Note: In the current version of ODataLib the default behavior likely now matches the requirements for - // minimal metadata mode. However, there have been behavior changes/bugs there in the past, so the safer - // option is for this class to take control of type name serialization in minimal metadata mode. + // Note: In the current version of ODataLib the default behavior likely now matches the requirements for + // minimal metadata mode. However, there have been behavior changes/bugs there in the past, so the safer + // option is for this class to take control of type name serialization in minimal metadata mode. - Contract.Assert(resource != null); + Contract.Assert(resource != null); - string typeName = null; // Set null to force the type name not to serialize. + string typeName = null; // Set null to force the type name not to serialize. - // Provide the type name to serialize. - if (!ShouldSuppressTypeNameSerialization(resource, odataPathType, metadataLevel)) - { - typeName = resource.TypeName; - } - - resource.TypeAnnotation = new ODataTypeAnnotation(typeName); + // Provide the type name to serialize. + if (!ShouldSuppressTypeNameSerialization(resource, odataPathType, metadataLevel)) + { + typeName = resource.TypeName; } - internal static void AddTypeNameAnnotationAsNeededForComplex(ODataResource resource, ODataMetadataLevel metadataLevel) + resource.TypeAnnotation = new ODataTypeAnnotation(typeName); + } + + internal static void AddTypeNameAnnotationAsNeededForComplex(ODataResource resource, ODataMetadataLevel metadataLevel) + { + // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties + // null when values should not be serialized. The TypeName property is different and should always be + // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not + // to serialize the type name (a null value prevents serialization). + Contract.Assert(resource != null); + + // Only add an annotation if we want to override ODataLib's default type name serialization behavior. + if (ShouldAddTypeNameAnnotationForComplex(metadataLevel)) { - // ODataLib normally has the caller decide whether or not to serialize properties by leaving properties - // null when values should not be serialized. The TypeName property is different and should always be - // provided to ODataLib to enable model validation. A separate annotation is used to decide whether or not - // to serialize the type name (a null value prevents serialization). - Contract.Assert(resource != null); + string typeName; - // Only add an annotation if we want to override ODataLib's default type name serialization behavior. - if (ShouldAddTypeNameAnnotationForComplex(metadataLevel)) + // Provide the type name to serialize (or null to force it not to serialize). + if (ShouldSuppressTypeNameSerializationForComplex(metadataLevel)) { - string typeName; - - // Provide the type name to serialize (or null to force it not to serialize). - if (ShouldSuppressTypeNameSerializationForComplex(metadataLevel)) - { - typeName = null; - } - else - { - typeName = resource.TypeName; - } - - resource.TypeAnnotation = new ODataTypeAnnotation(typeName); + typeName = null; } - } - - internal static bool ShouldAddTypeNameAnnotationForComplex(ODataMetadataLevel metadataLevel) - { - switch (metadataLevel) + else { - // For complex types, the default behavior matches the requirements for minimal metadata mode, so no - // annotation is necessary. - case ODataMetadataLevel.Minimal: - return false; - // In other cases, this class must control the type name serialization behavior. - case ODataMetadataLevel.Full: - case ODataMetadataLevel.None: - default: // All values already specified; just keeping the compiler happy. - return true; + typeName = resource.TypeName; } - } - internal static bool ShouldSuppressTypeNameSerializationForComplex(ODataMetadataLevel metadataLevel) - { - Contract.Assert(metadataLevel != ODataMetadataLevel.Minimal); - - switch (metadataLevel) - { - case ODataMetadataLevel.None: - return true; - case ODataMetadataLevel.Full: - default: // All values already specified; just keeping the compiler happy. - return false; - } + resource.TypeAnnotation = new ODataTypeAnnotation(typeName); } + } - internal static bool ShouldOmitOperation(IEdmOperation operation, OperationLinkBuilder builder, - ODataMetadataLevel metadataLevel) + internal static bool ShouldAddTypeNameAnnotationForComplex(ODataMetadataLevel metadataLevel) + { + switch (metadataLevel) { - Contract.Assert(builder != null); + // For complex types, the default behavior matches the requirements for minimal metadata mode, so no + // annotation is necessary. + case ODataMetadataLevel.Minimal: + return false; + // In other cases, this class must control the type name serialization behavior. + case ODataMetadataLevel.Full: + case ODataMetadataLevel.None: + default: // All values already specified; just keeping the compiler happy. + return true; + } + } - switch (metadataLevel) - { - case ODataMetadataLevel.Minimal: - case ODataMetadataLevel.None: - return operation.IsBound && builder.FollowsConventions; + internal static bool ShouldSuppressTypeNameSerializationForComplex(ODataMetadataLevel metadataLevel) + { + Contract.Assert(metadataLevel != ODataMetadataLevel.Minimal); - case ODataMetadataLevel.Full: - default: // All values already specified; just keeping the compiler happy. - return false; - } + switch (metadataLevel) + { + case ODataMetadataLevel.None: + return true; + case ODataMetadataLevel.Full: + default: // All values already specified; just keeping the compiler happy. + return false; } + } - internal static bool ShouldSuppressTypeNameSerialization(ODataResource resource, IEdmStructuredType edmType, - ODataMetadataLevel metadataLevel) + internal static bool ShouldOmitOperation(IEdmOperation operation, OperationLinkBuilder builder, + ODataMetadataLevel metadataLevel) + { + Contract.Assert(builder != null); + + switch (metadataLevel) { - Contract.Assert(resource != null); + case ODataMetadataLevel.Minimal: + case ODataMetadataLevel.None: + return operation.IsBound && builder.FollowsConventions; - switch (metadataLevel) - { - case ODataMetadataLevel.None: - return true; - case ODataMetadataLevel.Full: - return false; - case ODataMetadataLevel.Minimal: - default: // All values already specified; just keeping the compiler happy. - string pathTypeName = null; - if (edmType != null) - { - pathTypeName = edmType.FullTypeName(); - } - string resourceTypeName = resource.TypeName; - return string.Equals(resourceTypeName, pathTypeName, StringComparison.Ordinal); - } + case ODataMetadataLevel.Full: + default: // All values already specified; just keeping the compiler happy. + return false; } + } - private IEdmStructuredTypeReference GetResourceType(object graph, ODataSerializerContext writeContext) + internal static bool ShouldSuppressTypeNameSerialization(ODataResource resource, IEdmStructuredType edmType, + ODataMetadataLevel metadataLevel) + { + Contract.Assert(resource != null); + + switch (metadataLevel) { - Contract.Assert(graph != null); + case ODataMetadataLevel.None: + return true; + case ODataMetadataLevel.Full: + return false; + case ODataMetadataLevel.Minimal: + default: // All values already specified; just keeping the compiler happy. + string pathTypeName = null; + if (edmType != null) + { + pathTypeName = edmType.FullTypeName(); + } + string resourceTypeName = resource.TypeName; + return string.Equals(resourceTypeName, pathTypeName, StringComparison.Ordinal); + } + } - IEdmTypeReference edmType = writeContext.GetEdmType(graph, graph.GetType(), true); - Contract.Assert(edmType != null); + private IEdmStructuredTypeReference GetResourceType(object graph, ODataSerializerContext writeContext) + { + Contract.Assert(graph != null); - if (edmType.IsUntyped()) - { - return edmType.ToStructuredTypeReference(); - } + IEdmTypeReference edmType = writeContext.GetEdmType(graph, graph.GetType(), true); + Contract.Assert(edmType != null); - if (!edmType.IsStructured()) - { - throw new SerializationException( - Error.Format(SRResources.CannotWriteType, GetType().Name, edmType.FullName())); - } + if (edmType.IsUntyped()) + { + return edmType.ToStructuredTypeReference(); + } - return edmType.AsStructured(); + if (!edmType.IsStructured()) + { + throw new SerializationException( + Error.Format(SRResources.CannotWriteType, GetType().Name, edmType.FullName())); } + + return edmType.AsStructured(); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs index 09a02049d..1e7594ae7 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs @@ -27,756 +27,755 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// OData serializer for serializing a collection of or +/// +public class ODataResourceSetSerializer : ODataEdmTypeSerializer { + private const string ResourceSet = "ResourceSet"; + /// - /// OData serializer for serializing a collection of or + /// Initializes a new instance of . /// - public class ODataResourceSetSerializer : ODataEdmTypeSerializer + /// The to use to write nested entries. + public ODataResourceSetSerializer(IODataSerializerProvider serializerProvider) + : base(ODataPayloadKind.ResourceSet, serializerProvider) { - private const string ResourceSet = "ResourceSet"; + } - /// - /// Initializes a new instance of . - /// - /// The to use to write nested entries. - public ODataResourceSetSerializer(IODataSerializerProvider serializerProvider) - : base(ODataPayloadKind.ResourceSet, serializerProvider) + /// + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + { + if (messageWriter == null) { + throw Error.ArgumentNull(nameof(messageWriter)); } - /// - public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + if (writeContext == null) { - if (messageWriter == null) - { - throw Error.ArgumentNull(nameof(messageWriter)); - } - - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + throw Error.ArgumentNull(nameof(writeContext)); + } - IEdmEntitySetBase entitySet = writeContext.NavigationSource as IEdmEntitySetBase; + IEdmEntitySetBase entitySet = writeContext.NavigationSource as IEdmEntitySetBase; - bool isUntypedPath = writeContext.Path.IsUntypedPropertyPath(); - IEdmTypeReference resourceSetType = writeContext.GetEdmType(graph, type, isUntypedPath); - Contract.Assert(resourceSetType != null); + bool isUntypedPath = writeContext.Path.IsUntypedPropertyPath(); + IEdmTypeReference resourceSetType = writeContext.GetEdmType(graph, type, isUntypedPath); + Contract.Assert(resourceSetType != null); - IEdmStructuredTypeReference resourceType = GetResourceType(resourceSetType); + IEdmStructuredTypeReference resourceType = GetResourceType(resourceSetType); - ODataWriter writer = await messageWriter.CreateODataResourceSetWriterAsync(entitySet, resourceType.StructuredDefinition()) - .ConfigureAwait(false); - await WriteObjectInlineAsync(graph, resourceSetType, writer, writeContext) - .ConfigureAwait(false); - } + ODataWriter writer = await messageWriter.CreateODataResourceSetWriterAsync(entitySet, resourceType.StructuredDefinition()) + .ConfigureAwait(false); + await WriteObjectInlineAsync(graph, resourceSetType, writer, writeContext) + .ConfigureAwait(false); + } - /// - public override async Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, - ODataSerializerContext writeContext) + /// + public override async Task WriteObjectInlineAsync(object graph, IEdmTypeReference expectedType, ODataWriter writer, + ODataSerializerContext writeContext) + { + if (writer == null) { - if (writer == null) - { - throw Error.ArgumentNull(nameof(writer)); - } - - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } - - if (expectedType == null) - { - throw Error.ArgumentNull(nameof(expectedType)); - } - - if (graph == null) - { - throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, ResourceSet)); - } + throw Error.ArgumentNull(nameof(writer)); + } - if (writeContext.Type != null && - writeContext.Type.IsGenericType && - writeContext.Type.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>) && - graph is IAsyncEnumerable asyncEnumerable) - { - await WriteResourceSetAsync(asyncEnumerable, expectedType, writer, writeContext).ConfigureAwait(false); - } - else if (graph is IEnumerable enumerable) - { - await WriteResourceSetAsync(enumerable, expectedType, writer, writeContext).ConfigureAwait(false); - } - else - { - throw new SerializationException( - Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName)); - } + if (writeContext == null) + { + throw Error.ArgumentNull(nameof(writeContext)); } - private async Task WriteResourceSetAsync(IEnumerable enumerable, IEdmTypeReference resourceSetType, ODataWriter writer, - ODataSerializerContext writeContext) + if (expectedType == null) { - Contract.Assert(writer != null); - Contract.Assert(writeContext != null); - Contract.Assert(enumerable != null); - Contract.Assert(resourceSetType != null); + throw Error.ArgumentNull(nameof(expectedType)); + } - IEdmStructuredTypeReference elementType = GetResourceType(resourceSetType); - ODataResourceSet resourceSet = CreateResourceSet(enumerable, resourceSetType.AsCollection(), writeContext); + if (graph == null) + { + throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, ResourceSet)); + } - Func nextLinkGenerator = GetNextLinkGenerator(resourceSet, enumerable, writeContext); + if (writeContext.Type != null && + writeContext.Type.IsGenericType && + writeContext.Type.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>) && + graph is IAsyncEnumerable asyncEnumerable) + { + await WriteResourceSetAsync(asyncEnumerable, expectedType, writer, writeContext).ConfigureAwait(false); + } + else if (graph is IEnumerable enumerable) + { + await WriteResourceSetAsync(enumerable, expectedType, writer, writeContext).ConfigureAwait(false); + } + else + { + throw new SerializationException( + Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName)); + } + } - WriteResourceSetInternal(resourceSet, elementType, resourceSetType, writeContext, out bool isUntypedCollection, out IODataEdmTypeSerializer resourceSerializer); + private async Task WriteResourceSetAsync(IEnumerable enumerable, IEdmTypeReference resourceSetType, ODataWriter writer, + ODataSerializerContext writeContext) + { + Contract.Assert(writer != null); + Contract.Assert(writeContext != null); + Contract.Assert(enumerable != null); + Contract.Assert(resourceSetType != null); - await writer.WriteStartAsync(resourceSet).ConfigureAwait(false); - object lastResource = null; + IEdmStructuredTypeReference elementType = GetResourceType(resourceSetType); + ODataResourceSet resourceSet = CreateResourceSet(enumerable, resourceSetType.AsCollection(), writeContext); - foreach (object item in enumerable) - { - lastResource = item; + Func nextLinkGenerator = GetNextLinkGenerator(resourceSet, enumerable, writeContext); - await WriteResourceSetItemAsync(item, elementType, isUntypedCollection, resourceSetType, writer, resourceSerializer, writeContext).ConfigureAwait(false); - } + WriteResourceSetInternal(resourceSet, elementType, resourceSetType, writeContext, out bool isUntypedCollection, out IODataEdmTypeSerializer resourceSerializer); - // Subtle and surprising behavior: If the NextPageLink property is set before calling WriteStart(resourceSet), - // the next page link will be written early in a manner not compatible with odata.streaming=true. Instead, if - // the next page link is not set when calling WriteStart(resourceSet) but is instead set later on that resourceSet - // object before calling WriteEnd(), the next page link will be written at the end, as required for - // odata.streaming=true support. + await writer.WriteStartAsync(resourceSet).ConfigureAwait(false); + object lastResource = null; - resourceSet.NextPageLink = nextLinkGenerator(lastResource); + foreach (object item in enumerable) + { + lastResource = item; - await writer.WriteEndAsync().ConfigureAwait(false); + await WriteResourceSetItemAsync(item, elementType, isUntypedCollection, resourceSetType, writer, resourceSerializer, writeContext).ConfigureAwait(false); } - private async Task WriteResourceSetAsync(IAsyncEnumerable asyncEnumerable, IEdmTypeReference resourceSetType, ODataWriter writer, - ODataSerializerContext writeContext) - { - Contract.Assert(writer != null); - Contract.Assert(writeContext != null); - Contract.Assert(asyncEnumerable != null); - Contract.Assert(resourceSetType != null); + // Subtle and surprising behavior: If the NextPageLink property is set before calling WriteStart(resourceSet), + // the next page link will be written early in a manner not compatible with odata.streaming=true. Instead, if + // the next page link is not set when calling WriteStart(resourceSet) but is instead set later on that resourceSet + // object before calling WriteEnd(), the next page link will be written at the end, as required for + // odata.streaming=true support. - IEdmStructuredTypeReference elementType = GetResourceType(resourceSetType); - ODataResourceSet resourceSet = CreateResourceSet(asyncEnumerable, resourceSetType.AsCollection(), writeContext); + resourceSet.NextPageLink = nextLinkGenerator(lastResource); - Func nextLinkGenerator = GetNextLinkGenerator(resourceSet, asyncEnumerable, writeContext); + await writer.WriteEndAsync().ConfigureAwait(false); + } - WriteResourceSetInternal(resourceSet, elementType, resourceSetType, writeContext, out bool isUntypedCollection, out IODataEdmTypeSerializer resourceSerializer); - - await writer.WriteStartAsync(resourceSet).ConfigureAwait(false); - object lastResource = null; + private async Task WriteResourceSetAsync(IAsyncEnumerable asyncEnumerable, IEdmTypeReference resourceSetType, ODataWriter writer, + ODataSerializerContext writeContext) + { + Contract.Assert(writer != null); + Contract.Assert(writeContext != null); + Contract.Assert(asyncEnumerable != null); + Contract.Assert(resourceSetType != null); - await foreach (object item in asyncEnumerable) - { - lastResource = item; + IEdmStructuredTypeReference elementType = GetResourceType(resourceSetType); + ODataResourceSet resourceSet = CreateResourceSet(asyncEnumerable, resourceSetType.AsCollection(), writeContext); - await WriteResourceSetItemAsync(item, elementType, isUntypedCollection, resourceSetType, writer, resourceSerializer, writeContext).ConfigureAwait(false); - } + Func nextLinkGenerator = GetNextLinkGenerator(resourceSet, asyncEnumerable, writeContext); - // Subtle and surprising behavior: If the NextPageLink property is set before calling WriteStart(resourceSet), - // the next page link will be written early in a manner not compatible with odata.streaming=true. Instead, if - // the next page link is not set when calling WriteStart(resourceSet) but is instead set later on that resourceSet - // object before calling WriteEnd(), the next page link will be written at the end, as required for - // odata.streaming=true support. + WriteResourceSetInternal(resourceSet, elementType, resourceSetType, writeContext, out bool isUntypedCollection, out IODataEdmTypeSerializer resourceSerializer); + + await writer.WriteStartAsync(resourceSet).ConfigureAwait(false); + object lastResource = null; - resourceSet.NextPageLink = nextLinkGenerator(lastResource); + await foreach (object item in asyncEnumerable) + { + lastResource = item; - await writer.WriteEndAsync().ConfigureAwait(false); + await WriteResourceSetItemAsync(item, elementType, isUntypedCollection, resourceSetType, writer, resourceSerializer, writeContext).ConfigureAwait(false); } - private void WriteResourceSetInternal( - ODataResourceSet resourceSet, - IEdmStructuredTypeReference elementType, - IEdmTypeReference resourceSetType, - ODataSerializerContext writeContext, - out bool isUntypedCollection, - out IODataEdmTypeSerializer resourceSerializer) - { - if (resourceSet == null) - { - throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, ResourceSet)); - } + // Subtle and surprising behavior: If the NextPageLink property is set before calling WriteStart(resourceSet), + // the next page link will be written early in a manner not compatible with odata.streaming=true. Instead, if + // the next page link is not set when calling WriteStart(resourceSet) but is instead set later on that resourceSet + // object before calling WriteEnd(), the next page link will be written at the end, as required for + // odata.streaming=true support. - IEdmEntitySetBase entitySet = writeContext.NavigationSource as IEdmEntitySetBase; - if (entitySet == null) - { - resourceSet.SetSerializationInfo(new ODataResourceSerializationInfo - { - IsFromCollection = true, - NavigationSourceEntityTypeName = elementType.FullName(), - NavigationSourceKind = EdmNavigationSourceKind.UnknownEntitySet, - NavigationSourceName = null - }); - } - - resourceSerializer = SerializerProvider.GetEdmTypeSerializer(elementType); - if (resourceSerializer == null) - { - throw new SerializationException( - Error.Format(SRResources.TypeCannotBeSerialized, elementType.FullName())); - } + resourceSet.NextPageLink = nextLinkGenerator(lastResource); - isUntypedCollection = resourceSetType.IsCollectionUntyped(); + await writer.WriteEndAsync().ConfigureAwait(false); + } - // set the nextpagelink to null to support JSON odata.streaming. - resourceSet.NextPageLink = null; + private void WriteResourceSetInternal( + ODataResourceSet resourceSet, + IEdmStructuredTypeReference elementType, + IEdmTypeReference resourceSetType, + ODataSerializerContext writeContext, + out bool isUntypedCollection, + out IODataEdmTypeSerializer resourceSerializer) + { + if (resourceSet == null) + { + throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, ResourceSet)); } - private async Task WriteResourceSetItemAsync( - object item, - IEdmStructuredTypeReference elementType, - bool isUntypedCollection, - IEdmTypeReference resourceSetType, - ODataWriter writer, - IODataEdmTypeSerializer resourceSerializer, - ODataSerializerContext writeContext) + IEdmEntitySetBase entitySet = writeContext.NavigationSource as IEdmEntitySetBase; + if (entitySet == null) { - if (item == null || item is NullEdmComplexObject) + resourceSet.SetSerializationInfo(new ODataResourceSerializationInfo { - if (elementType.IsEntity()) - { - throw new SerializationException(SRResources.NullElementInCollection); - } - - // for null complex element, it can be serialized as "null" in the collection. - await writer.WriteStartAsync(resource: null).ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); - } - else if (isUntypedCollection) - { - await WriteUntypedResourceSetItemAsync(item, resourceSetType, writer, writeContext).ConfigureAwait(false); - } - else - { - await resourceSerializer.WriteObjectInlineAsync(item, elementType, writer, writeContext).ConfigureAwait(false); - } + IsFromCollection = true, + NavigationSourceEntityTypeName = elementType.FullName(), + NavigationSourceKind = EdmNavigationSourceKind.UnknownEntitySet, + NavigationSourceName = null + }); } - private async Task WriteUntypedResourceSetItemAsync(object item, IEdmTypeReference parentSetType, ODataWriter writer, ODataSerializerContext writeContext) + resourceSerializer = SerializerProvider.GetEdmTypeSerializer(elementType); + if (resourceSerializer == null) { - Contract.Assert(item != null); // "item == null" is handled. - - Type itemType = item.GetType(); - IEdmTypeReference itemEdmType = writeContext.GetEdmType(item, itemType, true); - Contract.Assert(itemType != null); - - // if the type of value is declared as enum in the edm model, let's use it. - if (itemEdmType.IsEnum()) - { - await WriteEnumItemAsync(item, itemEdmType, parentSetType, writer, writeContext).ConfigureAwait(false); - return; - } + throw new SerializationException( + Error.Format(SRResources.TypeCannotBeSerialized, elementType.FullName())); + } - // the value is an enum whose type is not defined in edm model. Let's write it as string. - if (TypeHelper.IsEnum(item.GetType())) - { - await WriteEnumItemAsync(item, null, parentSetType, writer, writeContext).ConfigureAwait(false); - return; - } + isUntypedCollection = resourceSetType.IsCollectionUntyped(); - // The value is a primitive value, write it as untyped primitive value item. - if (itemEdmType.IsPrimitive()) - { - await WritePrimitiveItemAsync(item, itemEdmType, parentSetType, writer, writeContext).ConfigureAwait(false); - return; - } + // set the nextpagelink to null to support JSON odata.streaming. + resourceSet.NextPageLink = null; + } - if (itemEdmType.IsCollection()) + private async Task WriteResourceSetItemAsync( + object item, + IEdmStructuredTypeReference elementType, + bool isUntypedCollection, + IEdmTypeReference resourceSetType, + ODataWriter writer, + IODataEdmTypeSerializer resourceSerializer, + ODataSerializerContext writeContext) + { + if (item == null || item is NullEdmComplexObject) + { + if (elementType.IsEntity()) { - // If the value is a IList, or other similars, the TryGetEdmType(...) return Collection(Edm.Int32). - // But, ODL doesn't support to write ODataCollectionValue. - // Let's directly use untyped collection serialization no matter what type this collection is. - itemEdmType = EdmUntypedHelpers.NullableUntypedCollectionReference; - await WriteResourceSetItemAsync(item, itemEdmType, parentSetType, writer, writeContext).ConfigureAwait(false); - return; + throw new SerializationException(SRResources.NullElementInCollection); } - if (itemEdmType.IsStructuredOrUntypedStructured()) - { - await WriteResourceItemAsync(item, itemEdmType, parentSetType, writer, writeContext).ConfigureAwait(false); - } + // for null complex element, it can be serialized as "null" in the collection. + await writer.WriteStartAsync(resource: null).ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); + } + else if (isUntypedCollection) + { + await WriteUntypedResourceSetItemAsync(item, resourceSetType, writer, writeContext).ConfigureAwait(false); + } + else + { + await resourceSerializer.WriteObjectInlineAsync(item, elementType, writer, writeContext).ConfigureAwait(false); } + } - /// - /// Write a primitive value into a collection (untyped collection) - /// - /// The primitive value. - /// The primitive edm type. - /// The parent collection edm type. - /// The writer. - /// The writer context. - /// Task. - protected virtual async Task WritePrimitiveItemAsync(object primitiveValue, IEdmTypeReference primitiveType, - IEdmTypeReference parentSetType, - ODataWriter writer, ODataSerializerContext writeContext) - { - if (primitiveValue == null) - { - throw Error.ArgumentNull(nameof(primitiveValue)); - } + private async Task WriteUntypedResourceSetItemAsync(object item, IEdmTypeReference parentSetType, ODataWriter writer, ODataSerializerContext writeContext) + { + Contract.Assert(item != null); // "item == null" is handled. - var odataPrimitiveValue = ODataPrimitiveSerializer.CreatePrimitive(primitiveValue, primitiveType.AsPrimitive(), writeContext); - await writer.WritePrimitiveAsync(odataPrimitiveValue).ConfigureAwait(false); - } + Type itemType = item.GetType(); + IEdmTypeReference itemEdmType = writeContext.GetEdmType(item, itemType, true); + Contract.Assert(itemType != null); - /// - /// Write an enum value into a collection (untyped collection) - /// - /// The enum value. - /// The enum edm type or null if no edm type defined. - /// The parent collection edm type. - /// The writer. - /// The writer context. - /// Task. - protected virtual async Task WriteEnumItemAsync(object enumValue, IEdmTypeReference enumType, IEdmTypeReference parentSetType, - ODataWriter writer, ODataSerializerContext writeContext) + // if the type of value is declared as enum in the edm model, let's use it. + if (itemEdmType.IsEnum()) { - IEdmTypeReference edmTypeRef = EdmCoreModel.Instance.GetString(true); - if (enumType == null) - { - // we don't have the Edm enum type in the model, let's write it as string. - string enumValueStr = enumValue.ToString(); - await WritePrimitiveItemAsync(enumValueStr, edmTypeRef, parentSetType, writer, writeContext).ConfigureAwait(false); - } - else - { - ODataEnumSerializer enumSerializer = writeContext?.Request?.GetRouteServices()?.GetRequiredService(); - if (enumSerializer != null) - { - ODataEnumValue oDataEnumValue = enumSerializer.CreateODataEnumValue(enumValue, enumType.AsEnum(), writeContext); - - // We can't write the 'ODataEnumValue' directly by calling write methods on writer. - // Let's switch to use 'string' and write it as primitive value. - // Once ODL supports, let's call the correct writing methods. see issue: https://github.com/OData/odata.net/issues/2659 - await WritePrimitiveItemAsync(oDataEnumValue.Value, edmTypeRef, parentSetType, writer, writeContext); - } - } + await WriteEnumItemAsync(item, itemEdmType, parentSetType, writer, writeContext).ConfigureAwait(false); + return; } - /// - /// Write a nested collection value into a collection (untyped collection) - /// - /// The collection value. - /// The nested collection Edm type. - /// The parent collection edm type. - /// The writer. - /// The writer context. - /// Task. - protected virtual async Task WriteResourceSetItemAsync(object itemSetValue, IEdmTypeReference itemSetType, IEdmTypeReference parentSetType, - ODataWriter writer, ODataSerializerContext writeContext) + // the value is an enum whose type is not defined in edm model. Let's write it as string. + if (TypeHelper.IsEnum(item.GetType())) { - if (itemSetType == null) - { - throw Error.ArgumentNull(nameof(itemSetType)); - } + await WriteEnumItemAsync(item, null, parentSetType, writer, writeContext).ConfigureAwait(false); + return; + } - IODataEdmTypeSerializer resourceSetSerializer = SerializerProvider.GetEdmTypeSerializer(itemSetType); - await resourceSetSerializer.WriteObjectInlineAsync(itemSetValue, itemSetType, writer, writeContext).ConfigureAwait(false); + // The value is a primitive value, write it as untyped primitive value item. + if (itemEdmType.IsPrimitive()) + { + await WritePrimitiveItemAsync(item, itemEdmType, parentSetType, writer, writeContext).ConfigureAwait(false); + return; } - /// - /// Write a nested resource/object into a collection (untyped collection) - /// - /// The resource value. - /// The nested resource Edm type. - /// The parent collection edm type. - /// The writer. - /// The writer context. - /// Task. - protected virtual async Task WriteResourceItemAsync(object resourceValue, IEdmTypeReference resourceType, IEdmTypeReference parentSetType, - ODataWriter writer, ODataSerializerContext writeContext) + if (itemEdmType.IsCollection()) { - if (resourceType == null) - { - throw Error.ArgumentNull(nameof(resourceType)); - } + // If the value is a IList, or other similars, the TryGetEdmType(...) return Collection(Edm.Int32). + // But, ODL doesn't support to write ODataCollectionValue. + // Let's directly use untyped collection serialization no matter what type this collection is. + itemEdmType = EdmUntypedHelpers.NullableUntypedCollectionReference; + await WriteResourceSetItemAsync(item, itemEdmType, parentSetType, writer, writeContext).ConfigureAwait(false); + return; + } - IODataEdmTypeSerializer resourceSerializer = SerializerProvider.GetEdmTypeSerializer(resourceType); - await resourceSerializer.WriteObjectInlineAsync(resourceValue, resourceType, writer, writeContext).ConfigureAwait(false); + if (itemEdmType.IsStructuredOrUntypedStructured()) + { + await WriteResourceItemAsync(item, itemEdmType, parentSetType, writer, writeContext).ConfigureAwait(false); } + } - /// - /// Create the to be written for the given resourceSet instance. - /// - /// The instance representing the resourceSet being written. - /// The EDM type of the resourceSet being written. - /// The serializer context. - /// The created object. - public virtual ODataResourceSet CreateResourceSet(IEnumerable resourceSetInstance, IEdmCollectionTypeReference resourceSetType, - ODataSerializerContext writeContext) + /// + /// Write a primitive value into a collection (untyped collection) + /// + /// The primitive value. + /// The primitive edm type. + /// The parent collection edm type. + /// The writer. + /// The writer context. + /// Task. + protected virtual async Task WritePrimitiveItemAsync(object primitiveValue, IEdmTypeReference primitiveType, + IEdmTypeReference parentSetType, + ODataWriter writer, ODataSerializerContext writeContext) + { + if (primitiveValue == null) { - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + throw Error.ArgumentNull(nameof(primitiveValue)); + } - ODataResourceSet resourceSet = new ODataResourceSet - { - TypeName = resourceSetType.FullName() - }; + var odataPrimitiveValue = ODataPrimitiveSerializer.CreatePrimitive(primitiveValue, primitiveType.AsPrimitive(), writeContext); + await writer.WritePrimitiveAsync(odataPrimitiveValue).ConfigureAwait(false); + } - IEdmStructuredTypeReference structuredType = GetResourceType(resourceSetType).AsStructured(); - if (writeContext.NavigationSource != null && structuredType.IsEntity()) + /// + /// Write an enum value into a collection (untyped collection) + /// + /// The enum value. + /// The enum edm type or null if no edm type defined. + /// The parent collection edm type. + /// The writer. + /// The writer context. + /// Task. + protected virtual async Task WriteEnumItemAsync(object enumValue, IEdmTypeReference enumType, IEdmTypeReference parentSetType, + ODataWriter writer, ODataSerializerContext writeContext) + { + IEdmTypeReference edmTypeRef = EdmCoreModel.Instance.GetString(true); + if (enumType == null) + { + // we don't have the Edm enum type in the model, let's write it as string. + string enumValueStr = enumValue.ToString(); + await WritePrimitiveItemAsync(enumValueStr, edmTypeRef, parentSetType, writer, writeContext).ConfigureAwait(false); + } + else + { + ODataEnumSerializer enumSerializer = writeContext?.Request?.GetRouteServices()?.GetRequiredService(); + if (enumSerializer != null) { - ResourceSetContext resourceSetContext = ResourceSetContext.Create(writeContext, resourceSetInstance); - WriteEntityTypeOperations(resourceSet, resourceSetContext, structuredType, writeContext); - } + ODataEnumValue oDataEnumValue = enumSerializer.CreateODataEnumValue(enumValue, enumType.AsEnum(), writeContext); - if (writeContext.ExpandedResource == null) - { - // If we have more OData format specific information apply it now, only if we are the root feed. - PageResult odataResourceSetAnnotations = resourceSetInstance as PageResult; - ApplyODataResourceSetAnnotations(resourceSet, odataResourceSetAnnotations, writeContext); - } - else - { - ICountOptionCollection countOptionCollection = resourceSetInstance as ICountOptionCollection; - if (countOptionCollection != null && countOptionCollection.TotalCount != null) - { - resourceSet.Count = countOptionCollection.TotalCount; - } + // We can't write the 'ODataEnumValue' directly by calling write methods on writer. + // Let's switch to use 'string' and write it as primitive value. + // Once ODL supports, let's call the correct writing methods. see issue: https://github.com/OData/odata.net/issues/2659 + await WritePrimitiveItemAsync(oDataEnumValue.Value, edmTypeRef, parentSetType, writer, writeContext); } + } + } - return resourceSet; + /// + /// Write a nested collection value into a collection (untyped collection) + /// + /// The collection value. + /// The nested collection Edm type. + /// The parent collection edm type. + /// The writer. + /// The writer context. + /// Task. + protected virtual async Task WriteResourceSetItemAsync(object itemSetValue, IEdmTypeReference itemSetType, IEdmTypeReference parentSetType, + ODataWriter writer, ODataSerializerContext writeContext) + { + if (itemSetType == null) + { + throw Error.ArgumentNull(nameof(itemSetType)); } - /// - /// Create the to be written for the given resourceSet instance. - /// - /// The instance representing the resourceSet being written. - /// The EDM type of the resourceSet being written. - /// The serializer context. - /// The created object. - public virtual ODataResourceSet CreateResourceSet(IAsyncEnumerable resourceSetInstance, IEdmCollectionTypeReference resourceSetType, - ODataSerializerContext writeContext) + IODataEdmTypeSerializer resourceSetSerializer = SerializerProvider.GetEdmTypeSerializer(itemSetType); + await resourceSetSerializer.WriteObjectInlineAsync(itemSetValue, itemSetType, writer, writeContext).ConfigureAwait(false); + } + + /// + /// Write a nested resource/object into a collection (untyped collection) + /// + /// The resource value. + /// The nested resource Edm type. + /// The parent collection edm type. + /// The writer. + /// The writer context. + /// Task. + protected virtual async Task WriteResourceItemAsync(object resourceValue, IEdmTypeReference resourceType, IEdmTypeReference parentSetType, + ODataWriter writer, ODataSerializerContext writeContext) + { + if (resourceType == null) { - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + throw Error.ArgumentNull(nameof(resourceType)); + } - ODataResourceSet resourceSet = new ODataResourceSet - { - TypeName = resourceSetType.FullName() - }; + IODataEdmTypeSerializer resourceSerializer = SerializerProvider.GetEdmTypeSerializer(resourceType); + await resourceSerializer.WriteObjectInlineAsync(resourceValue, resourceType, writer, writeContext).ConfigureAwait(false); + } - IEdmStructuredTypeReference structuredType = GetResourceType(resourceSetType).AsStructured(); - if (writeContext.NavigationSource != null && structuredType.IsEntity()) - { - ResourceSetContext resourceSetContext = ResourceSetContext.Create(writeContext, resourceSetInstance); - WriteEntityTypeOperations(resourceSet, resourceSetContext, structuredType, writeContext); - } + /// + /// Create the to be written for the given resourceSet instance. + /// + /// The instance representing the resourceSet being written. + /// The EDM type of the resourceSet being written. + /// The serializer context. + /// The created object. + public virtual ODataResourceSet CreateResourceSet(IEnumerable resourceSetInstance, IEdmCollectionTypeReference resourceSetType, + ODataSerializerContext writeContext) + { + if (writeContext == null) + { + throw Error.ArgumentNull(nameof(writeContext)); + } - if (writeContext.ExpandedResource == null) - { - // If we have more OData format specific information apply it now, only if we are the root feed. - PageResult odataResourceSetAnnotations = resourceSetInstance as PageResult; - ApplyODataResourceSetAnnotations(resourceSet, odataResourceSetAnnotations, writeContext); - } - else - { - ICountOptionCollection countOptionCollection = resourceSetInstance as ICountOptionCollection; - if (countOptionCollection != null && countOptionCollection.TotalCount != null) - { - resourceSet.Count = countOptionCollection.TotalCount; - } - } + ODataResourceSet resourceSet = new ODataResourceSet + { + TypeName = resourceSetType.FullName() + }; - return resourceSet; + IEdmStructuredTypeReference structuredType = GetResourceType(resourceSetType).AsStructured(); + if (writeContext.NavigationSource != null && structuredType.IsEntity()) + { + ResourceSetContext resourceSetContext = ResourceSetContext.Create(writeContext, resourceSetInstance); + WriteEntityTypeOperations(resourceSet, resourceSetContext, structuredType, writeContext); } - private void WriteEntityTypeOperations( - ODataResourceSet resourceSet, - ResourceSetContext resourceSetContext, - IEdmStructuredTypeReference structuredType, - ODataSerializerContext writeContext) + if (writeContext.ExpandedResource == null) { - IEdmEntityType entityType = structuredType.AsEntity().EntityDefinition(); - IEnumerable operations = writeContext.Model.GetAvailableOperationsBoundToCollection(entityType); - var odataOperations = CreateODataOperations(operations, resourceSetContext, writeContext); - foreach (var odataOperation in odataOperations) + // If we have more OData format specific information apply it now, only if we are the root feed. + PageResult odataResourceSetAnnotations = resourceSetInstance as PageResult; + ApplyODataResourceSetAnnotations(resourceSet, odataResourceSetAnnotations, writeContext); + } + else + { + ICountOptionCollection countOptionCollection = resourceSetInstance as ICountOptionCollection; + if (countOptionCollection != null && countOptionCollection.TotalCount != null) { - ODataAction action = odataOperation as ODataAction; - if (action != null) - { - resourceSet.AddAction(action); - } - else - { - resourceSet.AddFunction((ODataFunction)odataOperation); - } + resourceSet.Count = countOptionCollection.TotalCount; } } - private void ApplyODataResourceSetAnnotations( - ODataResourceSet resourceSet, - PageResult odataResourceSetAnnotations, - ODataSerializerContext writeContext) + return resourceSet; + } + + /// + /// Create the to be written for the given resourceSet instance. + /// + /// The instance representing the resourceSet being written. + /// The EDM type of the resourceSet being written. + /// The serializer context. + /// The created object. + public virtual ODataResourceSet CreateResourceSet(IAsyncEnumerable resourceSetInstance, IEdmCollectionTypeReference resourceSetType, + ODataSerializerContext writeContext) + { + if (writeContext == null) { - if (odataResourceSetAnnotations != null) - { - resourceSet.Count = odataResourceSetAnnotations.Count; - resourceSet.NextPageLink = odataResourceSetAnnotations.NextPageLink; - } - else if (writeContext.Request != null) - { - IODataFeature odataFeature = writeContext.Request.ODataFeature(); - resourceSet.NextPageLink = odataFeature.NextLink; - resourceSet.DeltaLink = odataFeature.DeltaLink; + throw Error.ArgumentNull(nameof(writeContext)); + } - long? countValue = odataFeature.TotalCount; - if (countValue.HasValue) - { - resourceSet.Count = countValue.Value; - } - } + ODataResourceSet resourceSet = new ODataResourceSet + { + TypeName = resourceSetType.FullName() + }; + + IEdmStructuredTypeReference structuredType = GetResourceType(resourceSetType).AsStructured(); + if (writeContext.NavigationSource != null && structuredType.IsEntity()) + { + ResourceSetContext resourceSetContext = ResourceSetContext.Create(writeContext, resourceSetInstance); + WriteEntityTypeOperations(resourceSet, resourceSetContext, structuredType, writeContext); } - /// - /// Creates a function that takes in an object and generates nextlink uri. - /// - /// The resource set describing a collection of structured objects. - /// The instance representing the resourceSet being written. - /// The serializer context. - /// The function that generates the NextLink from an object. - internal static Func GetNextLinkGenerator(ODataResourceSetBase resourceSet, IEnumerable resourceSetInstance, ODataSerializerContext writeContext) + if (writeContext.ExpandedResource == null) + { + // If we have more OData format specific information apply it now, only if we are the root feed. + PageResult odataResourceSetAnnotations = resourceSetInstance as PageResult; + ApplyODataResourceSetAnnotations(resourceSet, odataResourceSetAnnotations, writeContext); + } + else { - if (resourceSet != null && resourceSet.NextPageLink != null) + ICountOptionCollection countOptionCollection = resourceSetInstance as ICountOptionCollection; + if (countOptionCollection != null && countOptionCollection.TotalCount != null) { - Uri defaultUri = resourceSet.NextPageLink; - return (obj) => { return defaultUri; }; + resourceSet.Count = countOptionCollection.TotalCount; } + } - if (writeContext.ExpandedResource == null) + return resourceSet; + } + + private void WriteEntityTypeOperations( + ODataResourceSet resourceSet, + ResourceSetContext resourceSetContext, + IEdmStructuredTypeReference structuredType, + ODataSerializerContext writeContext) + { + IEdmEntityType entityType = structuredType.AsEntity().EntityDefinition(); + IEnumerable operations = writeContext.Model.GetAvailableOperationsBoundToCollection(entityType); + var odataOperations = CreateODataOperations(operations, resourceSetContext, writeContext); + foreach (var odataOperation in odataOperations) + { + ODataAction action = odataOperation as ODataAction; + if (action != null) { - if (writeContext.Request != null && writeContext.QueryContext != null) - { - SkipTokenHandler handler = writeContext.QueryContext.GetSkipTokenHandler(); - return (obj) => { return handler.GenerateNextPageLink(new System.Uri(writeContext.Request.GetEncodedUrl()), - (writeContext.Request.ODataFeature() as ODataFeature).PageSize, obj, writeContext); }; - } + resourceSet.AddAction(action); } else { - // nested resourceSet - ITruncatedCollection truncatedCollection = resourceSetInstance as ITruncatedCollection; - if (truncatedCollection != null && truncatedCollection.IsTruncated) - { - return (obj) => { return GetNestedNextPageLink(writeContext, truncatedCollection.PageSize, obj); }; - } + resourceSet.AddFunction((ODataFunction)odataOperation); } - - return (obj) => { return null; }; } + } - /// - /// Creates a function that takes in an object and generates a nextlink uri. - /// - /// The resource set describing a collection of structured objects. - /// The instance representing the resourceSet being written. - /// The serializer context. - /// The function that generates the NextLink from an object. - internal static Func GetNextLinkGenerator(ODataResourceSetBase resourceSet, IAsyncEnumerable resourceSetInstance, ODataSerializerContext writeContext) + private void ApplyODataResourceSetAnnotations( + ODataResourceSet resourceSet, + PageResult odataResourceSetAnnotations, + ODataSerializerContext writeContext) + { + if (odataResourceSetAnnotations != null) { - if (resourceSet != null && resourceSet.NextPageLink != null) - { - Uri defaultUri = resourceSet.NextPageLink; - return (obj) => { return defaultUri; }; - } + resourceSet.Count = odataResourceSetAnnotations.Count; + resourceSet.NextPageLink = odataResourceSetAnnotations.NextPageLink; + } + else if (writeContext.Request != null) + { + IODataFeature odataFeature = writeContext.Request.ODataFeature(); + resourceSet.NextPageLink = odataFeature.NextLink; + resourceSet.DeltaLink = odataFeature.DeltaLink; - if (writeContext.ExpandedResource == null) + long? countValue = odataFeature.TotalCount; + if (countValue.HasValue) { - if (writeContext.Request != null && writeContext.QueryContext != null) - { - SkipTokenHandler handler = writeContext.QueryContext.GetSkipTokenHandler(); - return (obj) => { - return handler.GenerateNextPageLink(new System.Uri(writeContext.Request.GetEncodedUrl()), - (writeContext.Request.ODataFeature() as ODataFeature).PageSize, obj, writeContext); - }; - } - } - else - { - // nested resourceSet - ITruncatedCollection truncatedCollection = resourceSetInstance as ITruncatedCollection; - if (truncatedCollection != null && truncatedCollection.IsTruncated) - { - return (obj) => { return GetNestedNextPageLink(writeContext, truncatedCollection.PageSize, obj); }; - } + resourceSet.Count = countValue.Value; } + } + } - return (obj) => { return null; }; + /// + /// Creates a function that takes in an object and generates nextlink uri. + /// + /// The resource set describing a collection of structured objects. + /// The instance representing the resourceSet being written. + /// The serializer context. + /// The function that generates the NextLink from an object. + internal static Func GetNextLinkGenerator(ODataResourceSetBase resourceSet, IEnumerable resourceSetInstance, ODataSerializerContext writeContext) + { + if (resourceSet != null && resourceSet.NextPageLink != null) + { + Uri defaultUri = resourceSet.NextPageLink; + return (obj) => { return defaultUri; }; } - /// - /// Creates an to be written for the given operation and the resourceSet instance. - /// - /// The OData operation. - /// The context for the resourceSet instance being written. - /// The serializer context. - /// The created operation or null if the operation should not be written. - public virtual ODataOperation CreateODataOperation(IEdmOperation operation, ResourceSetContext resourceSetContext, ODataSerializerContext writeContext) + if (writeContext.ExpandedResource == null) { - if (operation == null) + if (writeContext.Request != null && writeContext.QueryContext != null) { - throw Error.ArgumentNull(nameof(operation)); + SkipTokenHandler handler = writeContext.QueryContext.GetSkipTokenHandler(); + return (obj) => { return handler.GenerateNextPageLink(new System.Uri(writeContext.Request.GetEncodedUrl()), + (writeContext.Request.ODataFeature() as ODataFeature).PageSize, obj, writeContext); }; } - - if (resourceSetContext == null) + } + else + { + // nested resourceSet + ITruncatedCollection truncatedCollection = resourceSetInstance as ITruncatedCollection; + if (truncatedCollection != null && truncatedCollection.IsTruncated) { - throw Error.ArgumentNull(nameof(resourceSetContext)); + return (obj) => { return GetNestedNextPageLink(writeContext, truncatedCollection.PageSize, obj); }; } + } - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + return (obj) => { return null; }; + } - ODataMetadataLevel metadataLevel = writeContext.MetadataLevel; - IEdmModel model = writeContext.Model; + /// + /// Creates a function that takes in an object and generates a nextlink uri. + /// + /// The resource set describing a collection of structured objects. + /// The instance representing the resourceSet being written. + /// The serializer context. + /// The function that generates the NextLink from an object. + internal static Func GetNextLinkGenerator(ODataResourceSetBase resourceSet, IAsyncEnumerable resourceSetInstance, ODataSerializerContext writeContext) + { + if (resourceSet != null && resourceSet.NextPageLink != null) + { + Uri defaultUri = resourceSet.NextPageLink; + return (obj) => { return defaultUri; }; + } - if (metadataLevel != ODataMetadataLevel.Full) + if (writeContext.ExpandedResource == null) + { + if (writeContext.Request != null && writeContext.QueryContext != null) { - return null; + SkipTokenHandler handler = writeContext.QueryContext.GetSkipTokenHandler(); + return (obj) => { + return handler.GenerateNextPageLink(new System.Uri(writeContext.Request.GetEncodedUrl()), + (writeContext.Request.ODataFeature() as ODataFeature).PageSize, obj, writeContext); + }; } - - OperationLinkBuilder builder = model.GetOperationLinkBuilder(operation); - if (builder == null) + } + else + { + // nested resourceSet + ITruncatedCollection truncatedCollection = resourceSetInstance as ITruncatedCollection; + if (truncatedCollection != null && truncatedCollection.IsTruncated) { - return null; + return (obj) => { return GetNestedNextPageLink(writeContext, truncatedCollection.PageSize, obj); }; } + } - Uri target = builder.BuildLink(resourceSetContext); - if (target == null) - { - return null; - } + return (obj) => { return null; }; + } - Uri baseUri = new Uri(writeContext.Request.CreateODataLink(MetadataSegment.Instance)); - Uri metadata = new Uri(baseUri, "#" + operation.FullName()); + /// + /// Creates an to be written for the given operation and the resourceSet instance. + /// + /// The OData operation. + /// The context for the resourceSet instance being written. + /// The serializer context. + /// The created operation or null if the operation should not be written. + public virtual ODataOperation CreateODataOperation(IEdmOperation operation, ResourceSetContext resourceSetContext, ODataSerializerContext writeContext) + { + if (operation == null) + { + throw Error.ArgumentNull(nameof(operation)); + } - ODataOperation odataOperation; - IEdmAction action = operation as IEdmAction; - if (action != null) - { - odataOperation = new ODataAction(); - } - else - { - odataOperation = new ODataFunction(); - } - odataOperation.Metadata = metadata; + if (resourceSetContext == null) + { + throw Error.ArgumentNull(nameof(resourceSetContext)); + } - // Always omit the title in minimal/no metadata modes. - ODataResourceSerializer.EmitTitle(model, operation, odataOperation); + if (writeContext == null) + { + throw Error.ArgumentNull(nameof(writeContext)); + } - // Omit the target in minimal/no metadata modes unless it doesn't follow conventions. - if (metadataLevel == ODataMetadataLevel.Full || !builder.FollowsConventions) - { - odataOperation.Target = target; - } + ODataMetadataLevel metadataLevel = writeContext.MetadataLevel; + IEdmModel model = writeContext.Model; - return odataOperation; + if (metadataLevel != ODataMetadataLevel.Full) + { + return null; } - private IEnumerable CreateODataOperations(IEnumerable operations, ResourceSetContext resourceSetContext, ODataSerializerContext writeContext) + OperationLinkBuilder builder = model.GetOperationLinkBuilder(operation); + if (builder == null) { - Contract.Assert(operations != null); - Contract.Assert(resourceSetContext != null); - Contract.Assert(writeContext != null); + return null; + } - foreach (IEdmOperation operation in operations) - { - ODataOperation oDataOperation = CreateODataOperation(operation, resourceSetContext, writeContext); - if (oDataOperation != null) - { - yield return oDataOperation; - } - } + Uri target = builder.BuildLink(resourceSetContext); + if (target == null) + { + return null; } - private static Uri GetNestedNextPageLink(ODataSerializerContext writeContext, int pageSize, object obj) + Uri baseUri = new Uri(writeContext.Request.CreateODataLink(MetadataSegment.Instance)); + Uri metadata = new Uri(baseUri, "#" + operation.FullName()); + + ODataOperation odataOperation; + IEdmAction action = operation as IEdmAction; + if (action != null) { - Contract.Assert(writeContext.ExpandedResource != null); - IEdmNavigationSource sourceNavigationSource = writeContext.ExpandedResource.NavigationSource; - NavigationSourceLinkBuilderAnnotation linkBuilder = writeContext.Model.GetNavigationSourceLinkBuilder(sourceNavigationSource); + odataOperation = new ODataAction(); + } + else + { + odataOperation = new ODataFunction(); + } + odataOperation.Metadata = metadata; + + // Always omit the title in minimal/no metadata modes. + ODataResourceSerializer.EmitTitle(model, operation, odataOperation); + + // Omit the target in minimal/no metadata modes unless it doesn't follow conventions. + if (metadataLevel == ODataMetadataLevel.Full || !builder.FollowsConventions) + { + odataOperation.Target = target; + } - Uri navigationLink = linkBuilder.BuildNavigationLink( - writeContext.ExpandedResource, - writeContext.NavigationProperty); + return odataOperation; + } - Uri nestedNextLink = GenerateQueryFromExpandedItem(writeContext, navigationLink); + private IEnumerable CreateODataOperations(IEnumerable operations, ResourceSetContext resourceSetContext, ODataSerializerContext writeContext) + { + Contract.Assert(operations != null); + Contract.Assert(resourceSetContext != null); + Contract.Assert(writeContext != null); - SkipTokenHandler nextLinkGenerator = null; - if (writeContext.QueryContext != null) + foreach (IEdmOperation operation in operations) + { + ODataOperation oDataOperation = CreateODataOperation(operation, resourceSetContext, writeContext); + if (oDataOperation != null) { - nextLinkGenerator = writeContext.QueryContext.GetSkipTokenHandler(); + yield return oDataOperation; } + } + } - if (nestedNextLink != null) - { - if (nextLinkGenerator != null) - { - return nextLinkGenerator.GenerateNextPageLink(nestedNextLink, pageSize, obj, writeContext); - } + private static Uri GetNestedNextPageLink(ODataSerializerContext writeContext, int pageSize, object obj) + { + Contract.Assert(writeContext.ExpandedResource != null); + IEdmNavigationSource sourceNavigationSource = writeContext.ExpandedResource.NavigationSource; + NavigationSourceLinkBuilderAnnotation linkBuilder = writeContext.Model.GetNavigationSourceLinkBuilder(sourceNavigationSource); - return GetNextPageHelper.GetNextPageLink(nestedNextLink, pageSize); - } + Uri navigationLink = linkBuilder.BuildNavigationLink( + writeContext.ExpandedResource, + writeContext.NavigationProperty); - return null; + Uri nestedNextLink = GenerateQueryFromExpandedItem(writeContext, navigationLink); + + SkipTokenHandler nextLinkGenerator = null; + if (writeContext.QueryContext != null) + { + nextLinkGenerator = writeContext.QueryContext.GetSkipTokenHandler(); } - private static Uri GenerateQueryFromExpandedItem(ODataSerializerContext writeContext, Uri navigationLink) + if (nestedNextLink != null) { - string serviceRoot = writeContext.Request.CreateODataLink(new List()); - Uri serviceRootUri = new Uri(serviceRoot); - ODataUriParser parser = new ODataUriParser(writeContext.Model, serviceRootUri, navigationLink); - ODataUri newUri = parser.ParseUri(); - newUri.SelectAndExpand = writeContext.SelectExpandClause; - if (writeContext.CurrentExpandedSelectItem != null) + if (nextLinkGenerator != null) { - newUri.OrderBy = writeContext.CurrentExpandedSelectItem.OrderByOption; - newUri.Filter = writeContext.CurrentExpandedSelectItem.FilterOption; - newUri.Skip = writeContext.CurrentExpandedSelectItem.SkipOption; - newUri.Top = writeContext.CurrentExpandedSelectItem.TopOption; + return nextLinkGenerator.GenerateNextPageLink(nestedNextLink, pageSize, obj, writeContext); + } - if (writeContext.CurrentExpandedSelectItem.CountOption != null) - { - if (writeContext.CurrentExpandedSelectItem.CountOption.HasValue) - { - newUri.QueryCount = writeContext.CurrentExpandedSelectItem.CountOption.Value; - } - } + return GetNextPageHelper.GetNextPageLink(nestedNextLink, pageSize); + } + + return null; + } - ExpandedNavigationSelectItem expandedNavigationItem = writeContext.CurrentExpandedSelectItem as ExpandedNavigationSelectItem; - if (expandedNavigationItem != null) + private static Uri GenerateQueryFromExpandedItem(ODataSerializerContext writeContext, Uri navigationLink) + { + string serviceRoot = writeContext.Request.CreateODataLink(new List()); + Uri serviceRootUri = new Uri(serviceRoot); + ODataUriParser parser = new ODataUriParser(writeContext.Model, serviceRootUri, navigationLink); + ODataUri newUri = parser.ParseUri(); + newUri.SelectAndExpand = writeContext.SelectExpandClause; + if (writeContext.CurrentExpandedSelectItem != null) + { + newUri.OrderBy = writeContext.CurrentExpandedSelectItem.OrderByOption; + newUri.Filter = writeContext.CurrentExpandedSelectItem.FilterOption; + newUri.Skip = writeContext.CurrentExpandedSelectItem.SkipOption; + newUri.Top = writeContext.CurrentExpandedSelectItem.TopOption; + + if (writeContext.CurrentExpandedSelectItem.CountOption != null) + { + if (writeContext.CurrentExpandedSelectItem.CountOption.HasValue) { - newUri.SelectAndExpand = expandedNavigationItem.SelectAndExpand; + newUri.QueryCount = writeContext.CurrentExpandedSelectItem.CountOption.Value; } } - ODataUrlKeyDelimiter keyDelimiter = ODataUrlKeyDelimiter.Parentheses; - ODataOptions options = writeContext.Request.HttpContext.RequestServices.GetRequiredService>().Value; - if (options != null) + ExpandedNavigationSelectItem expandedNavigationItem = writeContext.CurrentExpandedSelectItem as ExpandedNavigationSelectItem; + if (expandedNavigationItem != null) { - keyDelimiter = options.UrlKeyDelimiter; + newUri.SelectAndExpand = expandedNavigationItem.SelectAndExpand; } - - return newUri.BuildUri(keyDelimiter); } - private static IEdmStructuredTypeReference GetResourceType(IEdmTypeReference resourceSetType) + ODataUrlKeyDelimiter keyDelimiter = ODataUrlKeyDelimiter.Parentheses; + ODataOptions options = writeContext.Request.HttpContext.RequestServices.GetRequiredService>().Value; + if (options != null) { - if (resourceSetType.IsStructuredOrUntypedStructuredCollection()) - { - IEdmTypeReference elementType = resourceSetType.AsCollection().ElementType(); - return elementType.ToStructuredTypeReference(); - } + keyDelimiter = options.UrlKeyDelimiter; + } - string message = Error.Format(SRResources.CannotWriteType, typeof(ODataResourceSetSerializer).Name, resourceSetType.FullName()); - throw new SerializationException(message); + return newUri.BuildUri(keyDelimiter); + } + + private static IEdmStructuredTypeReference GetResourceType(IEdmTypeReference resourceSetType) + { + if (resourceSetType.IsStructuredOrUntypedStructuredCollection()) + { + IEdmTypeReference elementType = resourceSetType.AsCollection().ElementType(); + return elementType.ToStructuredTypeReference(); } + + string message = Error.Format(SRResources.CannotWriteType, typeof(ODataResourceSetSerializer).Name, resourceSetType.FullName()); + throw new SerializationException(message); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializer.cs index 31e4017ee..3db5656b1 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializer.cs @@ -9,36 +9,35 @@ using System.Threading.Tasks; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// Base class for implementations. +/// +/// +/// Each supported CLR type has a corresponding . A CLR type is supported if it is one of +/// the special types or if it has a backing EDM type. Some of the special types are Uri which maps to ODataReferenceLink payload, +/// Uri[] which maps to ODataReferenceLinks payload, etc. +/// +public abstract class ODataSerializer : IODataSerializer { /// - /// Base class for implementations. + /// Initializes a new instance of the class. /// - /// - /// Each supported CLR type has a corresponding . A CLR type is supported if it is one of - /// the special types or if it has a backing EDM type. Some of the special types are Uri which maps to ODataReferenceLink payload, - /// Uri[] which maps to ODataReferenceLinks payload, etc. - /// - public abstract class ODataSerializer : IODataSerializer + /// The kind of OData payload that this serializer generates. + protected ODataSerializer(ODataPayloadKind payloadKind) { - /// - /// Initializes a new instance of the class. - /// - /// The kind of OData payload that this serializer generates. - protected ODataSerializer(ODataPayloadKind payloadKind) - { - ODataPayloadKindHelper.Validate(payloadKind, nameof(payloadKind)); + ODataPayloadKindHelper.Validate(payloadKind, nameof(payloadKind)); - ODataPayloadKind = payloadKind; - } + ODataPayloadKind = payloadKind; + } - /// - public ODataPayloadKind ODataPayloadKind { get; } + /// + public ODataPayloadKind ODataPayloadKind { get; } - /// - public virtual async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) - { - await Task.Run(() => throw Error.NotSupported(SRResources.WriteObjectNotSupported, GetType().Name)).ConfigureAwait(false); - } + /// + public virtual async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + { + await Task.Run(() => throw Error.NotSupported(SRResources.WriteObjectNotSupported, GetType().Name)).ConfigureAwait(false); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerContext.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerContext.cs index b6530dc8a..efb1979dc 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerContext.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerContext.cs @@ -19,407 +19,406 @@ using Microsoft.AspNetCore.OData.Deltas; using Microsoft.AspNetCore.OData.Common; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// Represents an for serializing the raw value of an . +/// +public class ODataSerializerContext { + private IDictionary _items; + private ODataQueryContext _queryContext; + private SelectExpandClause _selectExpandClause; + private bool _isSelectExpandClauseSet; + internal Type Type { get; set; } + private bool? _isDeltaOfT; + /// - /// Represents an for serializing the raw value of an . + /// Initializes a new instance of the class. /// - public class ODataSerializerContext + public ODataSerializerContext() { - private IDictionary _items; - private ODataQueryContext _queryContext; - private SelectExpandClause _selectExpandClause; - private bool _isSelectExpandClauseSet; - internal Type Type { get; set; } - private bool? _isDeltaOfT; - - /// - /// Initializes a new instance of the class. - /// - public ODataSerializerContext() - { - } + } + + /// + /// Initializes a new instance of the class. + /// + /// The resource whose property is being nested. + /// The for the property being nested. + /// The complex property being nested or the navigation property being expanded. + /// If the resource property is the dynamic complex, the resource property is null. + /// + /// This constructor is used to construct the serializer context for writing nested and expanded properties. + public ODataSerializerContext(ResourceContext resource, SelectExpandClause selectExpandClause, IEdmProperty edmProperty) + : this(resource, edmProperty, null, null) + { + SelectExpandClause = selectExpandClause; + } - /// - /// Initializes a new instance of the class. - /// - /// The resource whose property is being nested. - /// The for the property being nested. - /// The complex property being nested or the navigation property being expanded. - /// If the resource property is the dynamic complex, the resource property is null. - /// - /// This constructor is used to construct the serializer context for writing nested and expanded properties. - public ODataSerializerContext(ResourceContext resource, SelectExpandClause selectExpandClause, IEdmProperty edmProperty) - : this(resource, edmProperty, null, null) + /// + /// Initializes a new instance of the class for nested resources. + /// + /// The resource whose property is being nested. + /// The complex property being nested or the navigation property being expanded. + /// If the resource property is the dynamic complex, the resource property is null. + /// + /// The for the property being nested. + /// The for the property being nested.> + internal ODataSerializerContext(ResourceContext resource, IEdmProperty edmProperty, ODataQueryContext queryContext, SelectItem currentSelectItem) + { + if (resource == null) { - SelectExpandClause = selectExpandClause; + throw Error.ArgumentNull("resource"); } - /// - /// Initializes a new instance of the class for nested resources. - /// - /// The resource whose property is being nested. - /// The complex property being nested or the navigation property being expanded. - /// If the resource property is the dynamic complex, the resource property is null. - /// - /// The for the property being nested. - /// The for the property being nested.> - internal ODataSerializerContext(ResourceContext resource, IEdmProperty edmProperty, ODataQueryContext queryContext, SelectItem currentSelectItem) - { - if (resource == null) - { - throw Error.ArgumentNull("resource"); - } + // Clone the resource's context. Use a helper function so it can + // handle platform-specific differences in ODataSerializerContext. + ODataSerializerContext context = resource.SerializerContext; + this.Request = context.Request; - // Clone the resource's context. Use a helper function so it can - // handle platform-specific differences in ODataSerializerContext. - ODataSerializerContext context = resource.SerializerContext; - this.Request = context.Request; + Model = context.Model; + Path = context.Path; + RootElementName = context.RootElementName; + SkipExpensiveAvailabilityChecks = context.SkipExpensiveAvailabilityChecks; + MetadataLevel = context.MetadataLevel; + Items = context.Items; + ExpandReference = context.ExpandReference; + TimeZone = context.TimeZone; + Type = context.Type; - Model = context.Model; - Path = context.Path; - RootElementName = context.RootElementName; - SkipExpensiveAvailabilityChecks = context.SkipExpensiveAvailabilityChecks; - MetadataLevel = context.MetadataLevel; - Items = context.Items; - ExpandReference = context.ExpandReference; - TimeZone = context.TimeZone; - Type = context.Type; + QueryContext = queryContext; - QueryContext = queryContext; + ExpandedResource = resource; // parent resource - ExpandedResource = resource; // parent resource + CurrentSelectItem = currentSelectItem; - CurrentSelectItem = currentSelectItem; + var expandedNavigationSelectItem = currentSelectItem as ExpandedNavigationSelectItem; + if (expandedNavigationSelectItem != null) + { + SelectExpandClause = expandedNavigationSelectItem.SelectAndExpand; + NavigationSource = expandedNavigationSelectItem.NavigationSource; - var expandedNavigationSelectItem = currentSelectItem as ExpandedNavigationSelectItem; - if (expandedNavigationSelectItem != null) + SetComputedProperties(expandedNavigationSelectItem.ComputeOption); + } + else + { + var pathSelectItem = currentSelectItem as PathSelectItem; + if (pathSelectItem != null) { - SelectExpandClause = expandedNavigationSelectItem.SelectAndExpand; - NavigationSource = expandedNavigationSelectItem.NavigationSource; + SelectExpandClause = pathSelectItem.SelectAndExpand; + NavigationSource = resource.NavigationSource; // Use it's parent navigation source. - SetComputedProperties(expandedNavigationSelectItem.ComputeOption); + SetComputedProperties(pathSelectItem.ComputeOption); } - else - { - var pathSelectItem = currentSelectItem as PathSelectItem; - if (pathSelectItem != null) - { - SelectExpandClause = pathSelectItem.SelectAndExpand; - NavigationSource = resource.NavigationSource; // Use it's parent navigation source. - - SetComputedProperties(pathSelectItem.ComputeOption); - } - var referencedNavigation = currentSelectItem as ExpandedReferenceSelectItem; - if (referencedNavigation != null) - { - ExpandReference = true; - NavigationSource = referencedNavigation.NavigationSource; + var referencedNavigation = currentSelectItem as ExpandedReferenceSelectItem; + if (referencedNavigation != null) + { + ExpandReference = true; + NavigationSource = referencedNavigation.NavigationSource; - SetComputedProperties(referencedNavigation.ComputeOption); - } + SetComputedProperties(referencedNavigation.ComputeOption); } + } - EdmProperty = edmProperty; // should be nested property + EdmProperty = edmProperty; // should be nested property - if (currentSelectItem == null || (NavigationSource as IEdmUnknownEntitySet) != null) + if (currentSelectItem == null || (NavigationSource as IEdmUnknownEntitySet) != null) + { + IEdmNavigationProperty navigationProperty = edmProperty as IEdmNavigationProperty; + if (navigationProperty != null && context.NavigationSource != null) { - IEdmNavigationProperty navigationProperty = edmProperty as IEdmNavigationProperty; - if (navigationProperty != null && context.NavigationSource != null) - { - NavigationSource = context.NavigationSource.FindNavigationTarget(NavigationProperty); - } - else - { - NavigationSource = resource.NavigationSource; - } + NavigationSource = context.NavigationSource.FindNavigationTarget(NavigationProperty); + } + else + { + NavigationSource = resource.NavigationSource; } } + } + + /// + /// Gets or sets the navigation source. + /// + public IEdmNavigationSource NavigationSource { get; set; } + + /// + /// Gets or sets the EDM model associated with the request. + /// + public IEdmModel Model { get; set; } + + /// + /// Gets or sets the of the request. + /// + public ODataPath Path { get; set; } - /// - /// Gets or sets the navigation source. - /// - public IEdmNavigationSource NavigationSource { get; set; } - - /// - /// Gets or sets the EDM model associated with the request. - /// - public IEdmModel Model { get; set; } - - /// - /// Gets or sets the of the request. - /// - public ODataPath Path { get; set; } - - /// - /// Gets or sets the metadata level of the response. - /// - public ODataMetadataLevel MetadataLevel { get; set; } - - /// - /// Gets or sets the HTTP Request whose response is being serialized. - /// - public HttpRequest Request { get;set; } - - /// - /// Gets or sets the root element name which is used when writing primitive and enum types - /// - public string RootElementName { get; set; } - - /// - /// Gets or sets the boolean value indicating whether it's $ref expanded. - /// - public bool ExpandReference { get; set; } - - /// - /// Gets or sets the complex property being nested or navigation property being expanded. - /// - public IEdmProperty EdmProperty { get; set; } - - /// - /// Get or sets whether expensive links should be calculated. - /// - public bool SkipExpensiveAvailabilityChecks { get; set; } - - /// - /// Gets or sets the . - /// - public TimeZoneInfo TimeZone { get; set; } - - /// - /// Gets or sets the . - /// - public ODataQueryOptions QueryOptions { get; internal set; } - - /// - /// Gets the computed properties in serializer context. - /// It contains all computed properties at current serializer context. - /// - public ISet ComputedProperties { get; } = new HashSet(); - - private IUntypedResourceMapper _valueMapper; - internal IUntypedResourceMapper UntypedMapper + /// + /// Gets or sets the metadata level of the response. + /// + public ODataMetadataLevel MetadataLevel { get; set; } + + /// + /// Gets or sets the HTTP Request whose response is being serialized. + /// + public HttpRequest Request { get;set; } + + /// + /// Gets or sets the root element name which is used when writing primitive and enum types + /// + public string RootElementName { get; set; } + + /// + /// Gets or sets the boolean value indicating whether it's $ref expanded. + /// + public bool ExpandReference { get; set; } + + /// + /// Gets or sets the complex property being nested or navigation property being expanded. + /// + public IEdmProperty EdmProperty { get; set; } + + /// + /// Get or sets whether expensive links should be calculated. + /// + public bool SkipExpensiveAvailabilityChecks { get; set; } + + /// + /// Gets or sets the . + /// + public TimeZoneInfo TimeZone { get; set; } + + /// + /// Gets or sets the . + /// + public ODataQueryOptions QueryOptions { get; internal set; } + + /// + /// Gets the computed properties in serializer context. + /// It contains all computed properties at current serializer context. + /// + public ISet ComputedProperties { get; } = new HashSet(); + + private IUntypedResourceMapper _valueMapper; + internal IUntypedResourceMapper UntypedMapper + { + get { - get + if (_valueMapper == null) { - if (_valueMapper == null) - { - _valueMapper = Request?.GetRouteServices()?.GetService(); - _valueMapper = _valueMapper ?? DefaultUntypedResourceMapper.Instance; - } - - return _valueMapper; + _valueMapper = Request?.GetRouteServices()?.GetService(); + _valueMapper = _valueMapper ?? DefaultUntypedResourceMapper.Instance; } + + return _valueMapper; } + } - /// - /// ODataQueryContext object, retrieved from query options for top-level context and passed down to nested serializer context as is. - /// - internal ODataQueryContext QueryContext + /// + /// ODataQueryContext object, retrieved from query options for top-level context and passed down to nested serializer context as is. + /// + internal ODataQueryContext QueryContext + { + get { - get + if (QueryOptions != null) { - if (QueryOptions != null) - { - return QueryOptions.Context; - } - - return _queryContext; + return QueryOptions.Context; } - private set { _queryContext = value; } + + return _queryContext; } + private set { _queryContext = value; } + } - /// - /// Gets or sets the . - /// - internal SelectItem CurrentSelectItem { get; set; } + /// + /// Gets or sets the . + /// + internal SelectItem CurrentSelectItem { get; set; } - /// - /// Gets a property bag associated with this context to store any generic data. - /// - public IDictionary Items + /// + /// Gets a property bag associated with this context to store any generic data. + /// + public IDictionary Items + { + get { - get - { - _items = _items ?? new Dictionary(); - return _items; - } - private set - { - _items = value; - } + _items = _items ?? new Dictionary(); + return _items; + } + private set + { + _items = value; } + } - /// - /// Gets or sets the resource that is being expanded. - /// - public ResourceContext ExpandedResource { get; set; } + /// + /// Gets or sets the resource that is being expanded. + /// + public ResourceContext ExpandedResource { get; set; } - /// - /// Gets or sets the . - /// - public SelectExpandClause SelectExpandClause + /// + /// Gets or sets the . + /// + public SelectExpandClause SelectExpandClause + { + get { - get + // private backing field to be removed once public setter from ODataFeature is removed. + if (_isSelectExpandClauseSet) { - // private backing field to be removed once public setter from ODataFeature is removed. - if (_isSelectExpandClauseSet) - { - return _selectExpandClause; - } - - if (QueryOptions != null) - { - if (QueryOptions.SelectExpand != null) - { - return QueryOptions.SelectExpand.ProcessedSelectExpandClause; - } - - return null; - } + return _selectExpandClause; + } - ExpandedNavigationSelectItem expandedItem = CurrentSelectItem as ExpandedNavigationSelectItem; - if (expandedItem != null) + if (QueryOptions != null) + { + if (QueryOptions.SelectExpand != null) { - return expandedItem.SelectAndExpand; + return QueryOptions.SelectExpand.ProcessedSelectExpandClause; } return null; } - set + + ExpandedNavigationSelectItem expandedItem = CurrentSelectItem as ExpandedNavigationSelectItem; + if (expandedItem != null) { - _isSelectExpandClauseSet = true; - _selectExpandClause = value; + return expandedItem.SelectAndExpand; } + + return null; + } + set + { + _isSelectExpandClauseSet = true; + _selectExpandClause = value; } + } - internal bool IsDeltaOfT + internal bool IsDeltaOfT + { + get { - get + if (_isDeltaOfT.HasValue) { - if (_isDeltaOfT.HasValue) - { - return _isDeltaOfT.Value; - } + return _isDeltaOfT.Value; + } - if (!(Type is { IsGenericType: true })) - { - _isDeltaOfT = false; - return _isDeltaOfT.Value; - } - - var genericTypeDefinition = Type.GetGenericTypeDefinition(); - _isDeltaOfT = genericTypeDefinition == typeof(Delta<>) || - genericTypeDefinition == typeof(DeltaSet<>) || - genericTypeDefinition == typeof(DeltaDeletedResource<>); - + if (!(Type is { IsGenericType: true })) + { + _isDeltaOfT = false; return _isDeltaOfT.Value; } + + var genericTypeDefinition = Type.GetGenericTypeDefinition(); + _isDeltaOfT = genericTypeDefinition == typeof(Delta<>) || + genericTypeDefinition == typeof(DeltaSet<>) || + genericTypeDefinition == typeof(DeltaDeletedResource<>); + + return _isDeltaOfT.Value; } + } - /// - /// Gets or sets the . - /// - internal ExpandedReferenceSelectItem CurrentExpandedSelectItem + /// + /// Gets or sets the . + /// + internal ExpandedReferenceSelectItem CurrentExpandedSelectItem + { + get { - get - { - return CurrentSelectItem as ExpandedReferenceSelectItem; - } + return CurrentSelectItem as ExpandedReferenceSelectItem; } + } + + /// + /// Gets or sets the navigation property being expanded. + /// + public IEdmNavigationProperty NavigationProperty + { + get + { + return EdmProperty as IEdmNavigationProperty; + } + } + + internal IEdmTypeReference GetEdmType(object instance, Type type, bool isUntyped = false) + { + IEdmTypeReference edmType = null; - /// - /// Gets or sets the navigation property being expanded. - /// - public IEdmNavigationProperty NavigationProperty + IEdmObject edmObject = instance as IEdmObject; + if (edmObject != null) { - get + edmType = edmObject.GetEdmType(); + if (edmType == null && !isUntyped) { - return EdmProperty as IEdmNavigationProperty; + throw Error.InvalidOperation(SRResources.EdmTypeCannotBeNull, edmObject.GetType().FullName, + typeof(IEdmObject).Name); } } - - internal IEdmTypeReference GetEdmType(object instance, Type type, bool isUntyped = false) + else { - IEdmTypeReference edmType = null; - - IEdmObject edmObject = instance as IEdmObject; - if (edmObject != null) + if (typeof(IDeltaSet).IsAssignableFrom(type)) { - edmType = edmObject.GetEdmType(); - if (edmType == null && !isUntyped) - { - throw Error.InvalidOperation(SRResources.EdmTypeCannotBeNull, edmObject.GetType().FullName, - typeof(IEdmObject).Name); - } + return Model.GetEdmTypeReference(type); } - else + + if (Model == null && !isUntyped) { - if (typeof(IDeltaSet).IsAssignableFrom(type)) - { - return Model.GetEdmTypeReference(type); - } + throw Error.InvalidOperation(SRResources.RequestMustHaveModel); + } - if (Model == null && !isUntyped) - { - throw Error.InvalidOperation(SRResources.RequestMustHaveModel); - } + if (Model != null) + { + edmType = Model.GetEdmTypeReference(type); - if (Model != null) + if (edmType == null) { - edmType = Model.GetEdmTypeReference(type); - - if (edmType == null) + if (instance != null) { - if (instance != null) + if (instance is ITypedDelta delta) { - if (instance is ITypedDelta delta) - { - edmType = Model.GetEdmTypeReference(delta.ExpectedClrType); - } - else - { - edmType = Model.GetEdmTypeReference(instance.GetType()); - } + edmType = Model.GetEdmTypeReference(delta.ExpectedClrType); } - - if (edmType == null && !isUntyped) + else { - throw Error.InvalidOperation(SRResources.ClrTypeNotInModel, type); + edmType = Model.GetEdmTypeReference(instance.GetType()); } } - else if (instance != null) + + if (edmType == null && !isUntyped) { - IEdmTypeReference actualType = Model.GetEdmTypeReference(instance.GetType()); - if (actualType != null && actualType != edmType) - { - edmType = actualType; - } + throw Error.InvalidOperation(SRResources.ClrTypeNotInModel, type); + } + } + else if (instance != null) + { + IEdmTypeReference actualType = Model.GetEdmTypeReference(instance.GetType()); + if (actualType != null && actualType != edmType) + { + edmType = actualType; } } } + } - if (edmType == null && isUntyped) - { - // we can't find the Edm type and it's in untyped. Let's return it as Untyped resource type (or collection) - return TypeHelper.GetUntypedEdmType(type ?? instance.GetType()); - } - - return edmType; + if (edmType == null && isUntyped) + { + // we can't find the Edm type and it's in untyped. Let's return it as Untyped resource type (or collection) + return TypeHelper.GetUntypedEdmType(type ?? instance.GetType()); } - internal void SetComputedProperties(ComputeClause computeClause) + return edmType; + } + + internal void SetComputedProperties(ComputeClause computeClause) + { + if (computeClause == null || !computeClause.ComputedItems.Any()) { - if (computeClause == null || !computeClause.ComputedItems.Any()) - { - return; - } + return; + } - foreach (var item in computeClause.ComputedItems) - { - ComputedProperties.Add(item.Alias); - } + foreach (var item in computeClause.ComputedItems) + { + ComputedProperties.Add(item.Alias); } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerPropertyHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerPropertyHelper.cs index 8c1e1a2cd..f8b92cb81 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerPropertyHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerPropertyHelper.cs @@ -10,53 +10,52 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +internal static class ODataSerializerPropertyHelper { - internal static class ODataSerializerPropertyHelper + /// + /// Creates an with name and value + /// based on the object represented by . + /// + /// The writing the property value. + /// The object to base the value of the property on. + /// The expected EDM type of the object represented by . + /// The name of the property. + /// The . + /// The created. + public static ODataProperty CreateProperty(this IODataEdmTypeSerializer serializer, object graph, IEdmTypeReference expectedType, string elementName, + ODataSerializerContext writeContext) { - /// - /// Creates an with name and value - /// based on the object represented by . - /// - /// The writing the property value. - /// The object to base the value of the property on. - /// The expected EDM type of the object represented by . - /// The name of the property. - /// The . - /// The created. - public static ODataProperty CreateProperty(this IODataEdmTypeSerializer serializer, object graph, IEdmTypeReference expectedType, string elementName, - ODataSerializerContext writeContext) + if (serializer is ODataCollectionSerializer collectionSerializer) { - if (serializer is ODataCollectionSerializer collectionSerializer) - { - return CreateCollectionProperty(collectionSerializer, graph, expectedType, elementName, writeContext); - } + return CreateCollectionProperty(collectionSerializer, graph, expectedType, elementName, writeContext); + } + + Contract.Assert(elementName != null); + return new ODataProperty + { + Name = elementName, + Value = serializer.CreateODataValue(graph, expectedType, writeContext) + }; + } - Contract.Assert(elementName != null); + private static ODataProperty CreateCollectionProperty(ODataCollectionSerializer serializer, object graph, IEdmTypeReference expectedType, string elementName, + ODataSerializerContext writeContext) + { + Contract.Assert(elementName != null); + var property = serializer.CreateODataValue(graph, expectedType, writeContext); + if (property != null) + { return new ODataProperty { Name = elementName, - Value = serializer.CreateODataValue(graph, expectedType, writeContext) + Value = property }; } - - private static ODataProperty CreateCollectionProperty(ODataCollectionSerializer serializer, object graph, IEdmTypeReference expectedType, string elementName, - ODataSerializerContext writeContext) + else { - Contract.Assert(elementName != null); - var property = serializer.CreateODataValue(graph, expectedType, writeContext); - if (property != null) - { - return new ODataProperty - { - Name = elementName, - Value = property - }; - } - else - { - return null; - } + return null; } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerProvider.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerProvider.cs index 74c62fed2..32ec696b7 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerProvider.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataSerializerProvider.cs @@ -20,143 +20,142 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// The default implementation of . +/// +public class ODataSerializerProvider: IODataSerializerProvider { + private readonly IServiceProvider _serviceProvider; + /// - /// The default implementation of . + /// Initializes a new instance of the class. /// - public class ODataSerializerProvider: IODataSerializerProvider + /// The root container. + public ODataSerializerProvider(IServiceProvider serviceProvider) { - private readonly IServiceProvider _serviceProvider; + _serviceProvider = serviceProvider ?? throw Error.ArgumentNull(nameof(serviceProvider)); + } - /// - /// Initializes a new instance of the class. - /// - /// The root container. - public ODataSerializerProvider(IServiceProvider serviceProvider) + /// + public virtual IODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType) + { + if (edmType == null) { - _serviceProvider = serviceProvider ?? throw Error.ArgumentNull(nameof(serviceProvider)); + throw new ArgumentNullException(nameof(edmType)); } - /// - public virtual IODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType) + switch (edmType.TypeKind()) { - if (edmType == null) - { - throw new ArgumentNullException(nameof(edmType)); - } + case EdmTypeKind.Enum: + return _serviceProvider.GetRequiredService(); - switch (edmType.TypeKind()) - { - case EdmTypeKind.Enum: - return _serviceProvider.GetRequiredService(); - - case EdmTypeKind.Primitive: - return _serviceProvider.GetRequiredService(); - - case EdmTypeKind.Collection: - IEdmCollectionTypeReference collectionType = edmType.AsCollection(); - if (collectionType.Definition.IsDeltaResourceSet()) - { - return _serviceProvider.GetRequiredService(); - } - else if (collectionType.ElementType().IsEntity() || collectionType.ElementType().IsComplex() - || collectionType.ElementType().IsUntyped()) - { - return _serviceProvider.GetRequiredService(); - } - else - { - return _serviceProvider.GetRequiredService(); - } - - case EdmTypeKind.Complex: - case EdmTypeKind.Entity: - case EdmTypeKind.Untyped: - return _serviceProvider.GetRequiredService(); - - default: - return null; - } + case EdmTypeKind.Primitive: + return _serviceProvider.GetRequiredService(); + + case EdmTypeKind.Collection: + IEdmCollectionTypeReference collectionType = edmType.AsCollection(); + if (collectionType.Definition.IsDeltaResourceSet()) + { + return _serviceProvider.GetRequiredService(); + } + else if (collectionType.ElementType().IsEntity() || collectionType.ElementType().IsComplex() + || collectionType.ElementType().IsUntyped()) + { + return _serviceProvider.GetRequiredService(); + } + else + { + return _serviceProvider.GetRequiredService(); + } + + case EdmTypeKind.Complex: + case EdmTypeKind.Entity: + case EdmTypeKind.Untyped: + return _serviceProvider.GetRequiredService(); + + default: + return null; } + } - /// - public virtual IODataSerializer GetODataPayloadSerializer(Type type, HttpRequest request) + /// + public virtual IODataSerializer GetODataPayloadSerializer(Type type, HttpRequest request) + { + if (type == null) { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } + throw new ArgumentNullException(nameof(type)); + } - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } - ODataPath path = request.ODataFeature().Path; - Type errorType = typeof(SerializableError); + ODataPath path = request.ODataFeature().Path; + Type errorType = typeof(SerializableError); - // handle the special types. - if (type == typeof(ODataServiceDocument)) - { - return _serviceProvider.GetRequiredService(); - } - else if (type == typeof(Uri) || type == typeof(ODataEntityReferenceLink)) - { - return _serviceProvider.GetRequiredService(); - } - else if (TypeHelper.IsTypeAssignableFrom(typeof(IEnumerable), type) || type == typeof(ODataEntityReferenceLinks)) - { - return _serviceProvider.GetRequiredService(); - } - else if (type == typeof(ODataError) || type == errorType) - { - return _serviceProvider.GetRequiredService(); - } - else if (TypeHelper.IsTypeAssignableFrom(typeof(IEdmModel), type)) - { - return _serviceProvider.GetRequiredService(); - } - else if (typeof(IDeltaSet).IsAssignableFrom(type)) - { - return _serviceProvider.GetRequiredService(); - } + // handle the special types. + if (type == typeof(ODataServiceDocument)) + { + return _serviceProvider.GetRequiredService(); + } + else if (type == typeof(Uri) || type == typeof(ODataEntityReferenceLink)) + { + return _serviceProvider.GetRequiredService(); + } + else if (TypeHelper.IsTypeAssignableFrom(typeof(IEnumerable), type) || type == typeof(ODataEntityReferenceLinks)) + { + return _serviceProvider.GetRequiredService(); + } + else if (type == typeof(ODataError) || type == errorType) + { + return _serviceProvider.GetRequiredService(); + } + else if (TypeHelper.IsTypeAssignableFrom(typeof(IEdmModel), type)) + { + return _serviceProvider.GetRequiredService(); + } + else if (typeof(IDeltaSet).IsAssignableFrom(type)) + { + return _serviceProvider.GetRequiredService(); + } - IEdmModel model = request.GetModel(); + IEdmModel model = request.GetModel(); - // if it is not a special type, assume it has a corresponding EdmType. - IEdmTypeReference edmType = model.GetEdmTypeReference(type); + // if it is not a special type, assume it has a corresponding EdmType. + IEdmTypeReference edmType = model.GetEdmTypeReference(type); - if (edmType != null) - { - bool isCountRequest = path != null && path.LastSegment is CountSegment; - bool isRawValueRequest = path != null && path.LastSegment is ValueSegment; - bool isStreamRequest = path.IsStreamPropertyPath(); + if (edmType != null) + { + bool isCountRequest = path != null && path.LastSegment is CountSegment; + bool isRawValueRequest = path != null && path.LastSegment is ValueSegment; + bool isStreamRequest = path.IsStreamPropertyPath(); - if (((edmType.IsPrimitive() || edmType.IsEnum()) && isRawValueRequest) || isCountRequest || isStreamRequest) - { - // Should rethink about the stream property serializer - return _serviceProvider.GetRequiredService(); - } - else - { - return GetEdmTypeSerializer(edmType); - } + if (((edmType.IsPrimitive() || edmType.IsEnum()) && isRawValueRequest) || isCountRequest || isStreamRequest) + { + // Should rethink about the stream property serializer + return _serviceProvider.GetRequiredService(); } else { - // Ok, we are here because the value type is not defined in OData Edm model - // (Known primitive, known enum, known strucutred or collection are handled above.) - // One of such case is that it's an untyped property (or dynamic property) query request - // and the value type is an unknown to OData Edm model. - if (path.IsUntypedPropertyPath()) - { - edmType = TypeHelper.GetUntypedEdmType(type); - return GetEdmTypeSerializer(edmType); - } - - return null; + return GetEdmTypeSerializer(edmType); + } + } + else + { + // Ok, we are here because the value type is not defined in OData Edm model + // (Known primitive, known enum, known strucutred or collection are handled above.) + // One of such case is that it's an untyped property (or dynamic property) query request + // and the value type is an unknown to OData Edm model. + if (path.IsUntypedPropertyPath()) + { + edmType = TypeHelper.GetUntypedEdmType(type); + return GetEdmTypeSerializer(edmType); } + + return null; } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataServiceDocumentSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataServiceDocumentSerializer.cs index 9fb0be2f3..02c62c743 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataServiceDocumentSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataServiceDocumentSerializer.cs @@ -10,41 +10,40 @@ using System.Threading.Tasks; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// Represents an for serializing 's for generating service document. +/// +public class ODataServiceDocumentSerializer : ODataSerializer { /// - /// Represents an for serializing 's for generating service document. + /// Initializes a new instance of . /// - public class ODataServiceDocumentSerializer : ODataSerializer + public ODataServiceDocumentSerializer() + : base(ODataPayloadKind.ServiceDocument) { - /// - /// Initializes a new instance of . - /// - public ODataServiceDocumentSerializer() - : base(ODataPayloadKind.ServiceDocument) + } + + /// + public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + { + if (graph == null) { + throw Error.ArgumentNull(nameof(graph)); } - /// - public override async Task WriteObjectAsync(object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) + if (messageWriter == null) { - if (graph == null) - { - throw Error.ArgumentNull(nameof(graph)); - } - - if (messageWriter == null) - { - throw Error.ArgumentNull(nameof(messageWriter)); - } - - ODataServiceDocument serviceDocument = graph as ODataServiceDocument; - if (serviceDocument == null) - { - throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, type?.Name)); - } + throw Error.ArgumentNull(nameof(messageWriter)); + } - await messageWriter.WriteServiceDocumentAsync(serviceDocument).ConfigureAwait(false); + ODataServiceDocument serviceDocument = graph as ODataServiceDocument; + if (serviceDocument == null) + { + throw new SerializationException(Error.Format(SRResources.CannotWriteType, GetType().Name, type?.Name)); } + + await messageWriter.WriteServiceDocumentAsync(serviceDocument).ConfigureAwait(false); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataValueExtensions.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataValueExtensions.cs index 2d4d6280f..6fae797e9 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataValueExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataValueExtensions.cs @@ -7,24 +7,23 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +internal static class ODataValueExtensions { - internal static class ODataValueExtensions + public static object GetInnerValue(this ODataValue odataValue) { - public static object GetInnerValue(this ODataValue odataValue) + if (odataValue is ODataNullValue) { - if (odataValue is ODataNullValue) - { - return null; - } - - ODataPrimitiveValue oDataPrimitiveValue = odataValue as ODataPrimitiveValue; - if (oDataPrimitiveValue != null) - { - return oDataPrimitiveValue.Value; - } + return null; + } - return odataValue; + ODataPrimitiveValue oDataPrimitiveValue = odataValue as ODataPrimitiveValue; + if (oDataPrimitiveValue != null) + { + return oDataPrimitiveValue.Value; } + + return odataValue; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/SelectExpandNode.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/SelectExpandNode.cs index 299ddca34..75f089d7c 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/SelectExpandNode.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/SelectExpandNode.cs @@ -14,722 +14,721 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Formatter.Serialization; + +/// +/// Describes the set of structural properties and navigation properties and actions to select and navigation properties to expand while +/// writing an in the response. +/// +public class SelectExpandNode { /// - /// Describes the set of structural properties and navigation properties and actions to select and navigation properties to expand while - /// writing an in the response. + /// Creates a new instance of the class. /// - public class SelectExpandNode + /// The default constructor is for unit testing only. + public SelectExpandNode() { - /// - /// Creates a new instance of the class. - /// - /// The default constructor is for unit testing only. - public SelectExpandNode() + } + + /// + /// Creates a new instance of the class describing the set of structural properties, + /// nested properties, navigation properties, and actions to select and expand for the given . + /// + /// The structural type of the resource that would be written. + /// The serializer context to be used while creating the collection. + /// The default constructor is for unit testing only. + public SelectExpandNode(IEdmStructuredType structuredType, ODataSerializerContext writeContext) + : this() + { + if (writeContext == null) { + throw Error.ArgumentNull(nameof(writeContext)); } - /// - /// Creates a new instance of the class describing the set of structural properties, - /// nested properties, navigation properties, and actions to select and expand for the given . - /// - /// The structural type of the resource that would be written. - /// The serializer context to be used while creating the collection. - /// The default constructor is for unit testing only. - public SelectExpandNode(IEdmStructuredType structuredType, ODataSerializerContext writeContext) - : this() - { - if (writeContext == null) - { - throw Error.ArgumentNull(nameof(writeContext)); - } + Initialize(writeContext.SelectExpandClause, structuredType, writeContext.Model, writeContext.ExpandReference, writeContext.ComputedProperties); + } - Initialize(writeContext.SelectExpandClause, structuredType, writeContext.Model, writeContext.ExpandReference, writeContext.ComputedProperties); - } + /// + /// Creates a new instance of the class describing the set of structural properties, + /// nested properties, navigation properties, and actions to select and expand for the given . + /// + /// The parsed $select and $expand query options. + /// The structural type of the resource that would be written. + /// The that contains the given structural type. + public SelectExpandNode(SelectExpandClause selectExpandClause, IEdmStructuredType structuredType, IEdmModel model) + : this() + { + Initialize(selectExpandClause, structuredType, model, false, null); + } - /// - /// Creates a new instance of the class describing the set of structural properties, - /// nested properties, navigation properties, and actions to select and expand for the given . - /// - /// The parsed $select and $expand query options. - /// The structural type of the resource that would be written. - /// The that contains the given structural type. - public SelectExpandNode(SelectExpandClause selectExpandClause, IEdmStructuredType structuredType, IEdmModel model) - : this() - { - Initialize(selectExpandClause, structuredType, model, false, null); - } + /// + /// Gets the list of EDM structural properties (primitive, enum or collection of them) to be included in the response. + /// It could be null if there's no property selected. + /// + public ISet SelectedStructuralProperties { get; internal set; } - /// - /// Gets the list of EDM structural properties (primitive, enum or collection of them) to be included in the response. - /// It could be null if there's no property selected. - /// - public ISet SelectedStructuralProperties { get; internal set; } + /// + /// Gets the list of Edm structural properties (complex or complex collection) to be included in the response. + /// The key is the Edm structural property. + /// The value is the potential sub select item. + /// + public IDictionary SelectedComplexProperties { get; internal set; } - /// - /// Gets the list of Edm structural properties (complex or complex collection) to be included in the response. - /// The key is the Edm structural property. - /// The value is the potential sub select item. - /// - public IDictionary SelectedComplexProperties { get; internal set; } + /// + /// Gets the list of EDM navigation properties to be included as links in the response. It could be null. + /// + public ISet SelectedNavigationProperties { get; internal set; } - /// - /// Gets the list of EDM navigation properties to be included as links in the response. It could be null. - /// - public ISet SelectedNavigationProperties { get; internal set; } + /// + /// Gets the list of EDM navigation properties to be expanded in the response along with the nested query options embedded in the expand. + /// It could be null if no navigation property to expand. + /// + public IDictionary ExpandedProperties { get; internal set; } - /// - /// Gets the list of EDM navigation properties to be expanded in the response along with the nested query options embedded in the expand. - /// It could be null if no navigation property to expand. - /// - public IDictionary ExpandedProperties { get; internal set; } + /// + /// Gets the list of EDM navigation properties to be referenced in the response along with the nested query options embedded in the expand. + /// It could be null if no navigation property to reference. + /// + public IDictionary ReferencedProperties { get; internal set; } - /// - /// Gets the list of EDM navigation properties to be referenced in the response along with the nested query options embedded in the expand. - /// It could be null if no navigation property to reference. - /// - public IDictionary ReferencedProperties { get; internal set; } + /// s + /// Gets the list of dynamic properties to select. It could be null. + /// + public ISet SelectedDynamicProperties { get; internal set; } - /// s - /// Gets the list of dynamic properties to select. It could be null. - /// - public ISet SelectedDynamicProperties { get; internal set; } + /// + /// Gets the set of computed property in select. + /// + public ISet SelectedComputedProperties { get; } = new HashSet(); - /// - /// Gets the set of computed property in select. - /// - public ISet SelectedComputedProperties { get; } = new HashSet(); + /// + /// Gets the flag to indicate the dynamic property to be included in the response or not. + /// + public bool SelectAllDynamicProperties { get; internal set; } - /// - /// Gets the flag to indicate the dynamic property to be included in the response or not. - /// - public bool SelectAllDynamicProperties { get; internal set; } + /// + /// Gets the list of OData actions to be included in the response. It could be null. + /// + public ISet SelectedActions { get; internal set; } - /// - /// Gets the list of OData actions to be included in the response. It could be null. - /// - public ISet SelectedActions { get; internal set; } + /// + /// Gets the list of OData functions to be included in the response. It could be null. + /// + public ISet SelectedFunctions { get; internal set; } - /// - /// Gets the list of OData functions to be included in the response. It could be null. - /// - public ISet SelectedFunctions { get; internal set; } + /// + /// Initialize the Node from for the given . + /// + /// The input select and expand clause ($select and $expand). + /// The related structural type to select and expand. + /// The Edm model. + /// Is expanded reference. + /// The computed properties. + private void Initialize(SelectExpandClause selectExpandClause, IEdmStructuredType structuredType, IEdmModel model, bool expandedReference, ISet computedProperties) + { + if (structuredType == null) + { + throw Error.ArgumentNull("structuredType"); + } - /// - /// Initialize the Node from for the given . - /// - /// The input select and expand clause ($select and $expand). - /// The related structural type to select and expand. - /// The Edm model. - /// Is expanded reference. - /// The computed properties. - private void Initialize(SelectExpandClause selectExpandClause, IEdmStructuredType structuredType, IEdmModel model, bool expandedReference, ISet computedProperties) + if (model == null) { - if (structuredType == null) - { - throw Error.ArgumentNull("structuredType"); - } + throw Error.ArgumentNull("model"); + } - if (model == null) + IEdmEntityType entityType = structuredType as IEdmEntityType; + if (expandedReference) + { + SelectAllDynamicProperties = false; + if (entityType != null) { - throw Error.ArgumentNull("model"); + // only need to include the key properties. + SelectedStructuralProperties = new HashSet(entityType.Key()); } + } + else + { + EdmStructuralTypeInfo structuralTypeInfo = new EdmStructuralTypeInfo(model, structuredType); - IEdmEntityType entityType = structuredType as IEdmEntityType; - if (expandedReference) + if (selectExpandClause == null) { - SelectAllDynamicProperties = false; - if (entityType != null) + SelectAllDynamicProperties = true; + + // includes navigation properties + SelectedNavigationProperties = structuralTypeInfo.AllNavigationProperties; + + // includes all bound actions + SelectedActions = structuralTypeInfo.AllActions; + + // includes all bound functions + SelectedFunctions = structuralTypeInfo.AllFunctions; + + // includes all structural properties + if (structuralTypeInfo.AllStructuralProperties != null) { - // only need to include the key properties. - SelectedStructuralProperties = new HashSet(entityType.Key()); + foreach (var property in structuralTypeInfo.AllStructuralProperties) + { + AddStructuralProperty(property, null); + } } } else { - EdmStructuralTypeInfo structuralTypeInfo = new EdmStructuralTypeInfo(model, structuredType); + BuildSelectExpand(selectExpandClause, computedProperties, structuralTypeInfo); + } - if (selectExpandClause == null) - { - SelectAllDynamicProperties = true; + AdjustSelectNavigationProperties(); + } + } - // includes navigation properties - SelectedNavigationProperties = structuralTypeInfo.AllNavigationProperties; + /// + /// Build $select and $expand clause + /// + /// The select expand clause + /// The structural type properties. + /// The computed properties. + private void BuildSelectExpand(SelectExpandClause selectExpandClause, ISet computedProperties, EdmStructuralTypeInfo structuralTypeInfo) + { + Contract.Assert(selectExpandClause != null); + Contract.Assert(structuralTypeInfo != null); + + var currentLevelPropertiesInclude = new Dictionary(); - // includes all bound actions - SelectedActions = structuralTypeInfo.AllActions; + // Explicitly set SelectAllDynamicProperties as false, + // Below will re-set it as true if it meets the select all condition. + SelectAllDynamicProperties = false; + foreach (SelectItem selectItem in selectExpandClause.SelectedItems) + { + // $expand=... + ExpandedReferenceSelectItem expandReferenceItem = selectItem as ExpandedReferenceSelectItem; + if (expandReferenceItem != null) + { + BuildExpandItem(expandReferenceItem, currentLevelPropertiesInclude, structuralTypeInfo); + continue; + } - // includes all bound functions - SelectedFunctions = structuralTypeInfo.AllFunctions; + PathSelectItem pathSelectItem = selectItem as PathSelectItem; + if (pathSelectItem != null) + { + // $select=abc/.../xyz + BuildSelectItem(pathSelectItem, currentLevelPropertiesInclude, computedProperties, structuralTypeInfo); + continue; + } + + WildcardSelectItem wildCardSelectItem = selectItem as WildcardSelectItem; + if (wildCardSelectItem != null) + { + // $select=* + MergeAllStructuralProperties(structuralTypeInfo.AllStructuralProperties, currentLevelPropertiesInclude); + MergeSelectedNavigationProperties(structuralTypeInfo.AllNavigationProperties); + SelectAllDynamicProperties = true; - // includes all structural properties - if (structuralTypeInfo.AllStructuralProperties != null) + if (computedProperties != null) + { + foreach (var property in computedProperties) { - foreach (var property in structuralTypeInfo.AllStructuralProperties) - { - AddStructuralProperty(property, null); - } + SelectedComputedProperties.Add(property); } } - else - { - BuildSelectExpand(selectExpandClause, computedProperties, structuralTypeInfo); - } + continue; + } - AdjustSelectNavigationProperties(); + NamespaceQualifiedWildcardSelectItem wildCardActionSelection = selectItem as NamespaceQualifiedWildcardSelectItem; + if (wildCardActionSelection != null) + { + // $select=NS.* + AddNamespaceWildcardOperation(wildCardActionSelection, structuralTypeInfo.AllActions, structuralTypeInfo.AllFunctions); + continue; } + + throw new ODataException(Error.Format(SRResources.SelectionTypeNotSupported, selectItem.GetType().Name)); } - /// - /// Build $select and $expand clause - /// - /// The select expand clause - /// The structural type properties. - /// The computed properties. - private void BuildSelectExpand(SelectExpandClause selectExpandClause, ISet computedProperties, EdmStructuralTypeInfo structuralTypeInfo) + if (selectExpandClause.AllSelected) { - Contract.Assert(selectExpandClause != null); - Contract.Assert(structuralTypeInfo != null); - - var currentLevelPropertiesInclude = new Dictionary(); + MergeAllStructuralProperties(structuralTypeInfo.AllStructuralProperties, currentLevelPropertiesInclude); + MergeSelectedNavigationProperties(structuralTypeInfo.AllNavigationProperties); + MergeSelectedAction(structuralTypeInfo.AllActions); + MergeSelectedFunction(structuralTypeInfo.AllFunctions); + SelectAllDynamicProperties = true; + } - // Explicitly set SelectAllDynamicProperties as false, - // Below will re-set it as true if it meets the select all condition. - SelectAllDynamicProperties = false; - foreach (SelectItem selectItem in selectExpandClause.SelectedItems) + // to make sure the structural properties are in the same order defined in the type. + if (structuralTypeInfo.AllStructuralProperties != null) + { + foreach (var structuralProperty in structuralTypeInfo.AllStructuralProperties) { - // $expand=... - ExpandedReferenceSelectItem expandReferenceItem = selectItem as ExpandedReferenceSelectItem; - if (expandReferenceItem != null) + SelectExpandIncludedProperty includeProperty; + if (!currentLevelPropertiesInclude.TryGetValue(structuralProperty, out includeProperty)) { - BuildExpandItem(expandReferenceItem, currentLevelPropertiesInclude, structuralTypeInfo); continue; } - PathSelectItem pathSelectItem = selectItem as PathSelectItem; - if (pathSelectItem != null) - { - // $select=abc/.../xyz - BuildSelectItem(pathSelectItem, currentLevelPropertiesInclude, computedProperties, structuralTypeInfo); - continue; - } - - WildcardSelectItem wildCardSelectItem = selectItem as WildcardSelectItem; - if (wildCardSelectItem != null) - { - // $select=* - MergeAllStructuralProperties(structuralTypeInfo.AllStructuralProperties, currentLevelPropertiesInclude); - MergeSelectedNavigationProperties(structuralTypeInfo.AllNavigationProperties); - SelectAllDynamicProperties = true; - - if (computedProperties != null) - { - foreach (var property in computedProperties) - { - SelectedComputedProperties.Add(property); - } - } - continue; - } + PathSelectItem pathSelectItem = includeProperty == null ? null : includeProperty.ToPathSelectItem(); + AddStructuralProperty(structuralProperty, pathSelectItem); + } + } + } - NamespaceQualifiedWildcardSelectItem wildCardActionSelection = selectItem as NamespaceQualifiedWildcardSelectItem; - if (wildCardActionSelection != null) - { - // $select=NS.* - AddNamespaceWildcardOperation(wildCardActionSelection, structuralTypeInfo.AllActions, structuralTypeInfo.AllFunctions); - continue; - } + /// + /// Build the $expand item, it maybe $expand=nav, $expand=complex/nav, $expand=nav/$ref, etc. + /// + /// The expanded reference select item. + /// The current properties to include at current level. + /// The structural type properties. + private void BuildExpandItem(ExpandedReferenceSelectItem expandReferenceItem, + IDictionary currentLevelPropertiesInclude, + EdmStructuralTypeInfo structuralTypeInfo) + { + Contract.Assert(expandReferenceItem != null && expandReferenceItem.PathToNavigationProperty != null); + Contract.Assert(currentLevelPropertiesInclude != null); + Contract.Assert(structuralTypeInfo != null); - throw new ODataException(Error.Format(SRResources.SelectionTypeNotSupported, selectItem.GetType().Name)); - } + // Verify and process the $expand=abc/xyz/nav. + ODataExpandPath expandPath = expandReferenceItem.PathToNavigationProperty; + IList remainingSegments; + ODataPathSegment segment = expandPath.GetFirstNonTypeCastSegment(out remainingSegments); - if (selectExpandClause.AllSelected) - { - MergeAllStructuralProperties(structuralTypeInfo.AllStructuralProperties, currentLevelPropertiesInclude); - MergeSelectedNavigationProperties(structuralTypeInfo.AllNavigationProperties); - MergeSelectedAction(structuralTypeInfo.AllActions); - MergeSelectedFunction(structuralTypeInfo.AllFunctions); - SelectAllDynamicProperties = true; - } + PropertySegment firstPropertySegment = segment as PropertySegment; + if (firstPropertySegment != null) + { + // for example: $expand=abc/xyz/nav, the remaining segment can't be null + // because at least the last navigation property segment is there. + Contract.Assert(remainingSegments != null); - // to make sure the structural properties are in the same order defined in the type. - if (structuralTypeInfo.AllStructuralProperties != null) + if (structuralTypeInfo.IsStructuralPropertyDefined(firstPropertySegment.Property)) { - foreach (var structuralProperty in structuralTypeInfo.AllStructuralProperties) + SelectExpandIncludedProperty newPropertySelectItem; + if (!currentLevelPropertiesInclude.TryGetValue(firstPropertySegment.Property, out newPropertySelectItem)) { - SelectExpandIncludedProperty includeProperty; - if (!currentLevelPropertiesInclude.TryGetValue(structuralProperty, out includeProperty)) - { - continue; - } - - PathSelectItem pathSelectItem = includeProperty == null ? null : includeProperty.ToPathSelectItem(); - AddStructuralProperty(structuralProperty, pathSelectItem); + newPropertySelectItem = new SelectExpandIncludedProperty(firstPropertySegment); + currentLevelPropertiesInclude[firstPropertySegment.Property] = newPropertySelectItem; } + + newPropertySelectItem.AddSubExpandItem(remainingSegments, expandReferenceItem); } } + else + { + // for example: $expand=nav, or $expand=NS.SubType/nav, the navigation property segment should be the last segment. + // So, the remaining segments should be null. + Contract.Assert(remainingSegments == null); - /// - /// Build the $expand item, it maybe $expand=nav, $expand=complex/nav, $expand=nav/$ref, etc. - /// - /// The expanded reference select item. - /// The current properties to include at current level. - /// The structural type properties. - private void BuildExpandItem(ExpandedReferenceSelectItem expandReferenceItem, - IDictionary currentLevelPropertiesInclude, - EdmStructuralTypeInfo structuralTypeInfo) - { - Contract.Assert(expandReferenceItem != null && expandReferenceItem.PathToNavigationProperty != null); - Contract.Assert(currentLevelPropertiesInclude != null); - Contract.Assert(structuralTypeInfo != null); - - // Verify and process the $expand=abc/xyz/nav. - ODataExpandPath expandPath = expandReferenceItem.PathToNavigationProperty; - IList remainingSegments; - ODataPathSegment segment = expandPath.GetFirstNonTypeCastSegment(out remainingSegments); - - PropertySegment firstPropertySegment = segment as PropertySegment; - if (firstPropertySegment != null) - { - // for example: $expand=abc/xyz/nav, the remaining segment can't be null - // because at least the last navigation property segment is there. - Contract.Assert(remainingSegments != null); + NavigationPropertySegment firstNavigationSegment = segment as NavigationPropertySegment; + Contract.Assert(firstNavigationSegment != null); - if (structuralTypeInfo.IsStructuralPropertyDefined(firstPropertySegment.Property)) + if (structuralTypeInfo.IsNavigationPropertyDefined(firstNavigationSegment.NavigationProperty)) + { + // It's not allowed to have multiple navigation expanded or referenced. + // for example: "$expand=nav($top=2),nav($skip=3)" is not allowed and will be merged (or throw exception) at ODL side. + ExpandedNavigationSelectItem expanded = expandReferenceItem as ExpandedNavigationSelectItem; + if (expanded != null) { - SelectExpandIncludedProperty newPropertySelectItem; - if (!currentLevelPropertiesInclude.TryGetValue(firstPropertySegment.Property, out newPropertySelectItem)) + if (ExpandedProperties == null) { - newPropertySelectItem = new SelectExpandIncludedProperty(firstPropertySegment); - currentLevelPropertiesInclude[firstPropertySegment.Property] = newPropertySelectItem; + ExpandedProperties = new Dictionary(); } - newPropertySelectItem.AddSubExpandItem(remainingSegments, expandReferenceItem); + ExpandedProperties[firstNavigationSegment.NavigationProperty] = expanded; } - } - else - { - // for example: $expand=nav, or $expand=NS.SubType/nav, the navigation property segment should be the last segment. - // So, the remaining segments should be null. - Contract.Assert(remainingSegments == null); - - NavigationPropertySegment firstNavigationSegment = segment as NavigationPropertySegment; - Contract.Assert(firstNavigationSegment != null); - - if (structuralTypeInfo.IsNavigationPropertyDefined(firstNavigationSegment.NavigationProperty)) + else { - // It's not allowed to have multiple navigation expanded or referenced. - // for example: "$expand=nav($top=2),nav($skip=3)" is not allowed and will be merged (or throw exception) at ODL side. - ExpandedNavigationSelectItem expanded = expandReferenceItem as ExpandedNavigationSelectItem; - if (expanded != null) + // $expand=..../nav/$ref + if (ReferencedProperties == null) { - if (ExpandedProperties == null) - { - ExpandedProperties = new Dictionary(); - } - - ExpandedProperties[firstNavigationSegment.NavigationProperty] = expanded; + ReferencedProperties = new Dictionary(); } - else - { - // $expand=..../nav/$ref - if (ReferencedProperties == null) - { - ReferencedProperties = new Dictionary(); - } - ReferencedProperties[firstNavigationSegment.NavigationProperty] = expandReferenceItem; - } + ReferencedProperties[firstNavigationSegment.NavigationProperty] = expandReferenceItem; } } } + } - /// - /// Build the $select item, it maybe $select=complex/abc, $select=abc, $select=nav, etc. - /// - /// The expanded reference select item. - /// The current properties to include at current level. - /// The computed properties. - /// The structural type properties. - private void BuildSelectItem(PathSelectItem pathSelectItem, - IDictionary currentLevelPropertiesInclude, - ISet computedProperties, - EdmStructuralTypeInfo structuralTypeInfo) - { - Contract.Assert(pathSelectItem != null && pathSelectItem.SelectedPath != null); - Contract.Assert(currentLevelPropertiesInclude != null); - Contract.Assert(structuralTypeInfo != null); - - // Verify and process the $select=abc/xyz/.... - ODataSelectPath selectPath = pathSelectItem.SelectedPath; - IList remainingSegments; - ODataPathSegment segment = selectPath.GetFirstNonTypeCastSegment(out remainingSegments); - - PropertySegment firstPropertySegment = segment as PropertySegment; - if (firstPropertySegment != null) + /// + /// Build the $select item, it maybe $select=complex/abc, $select=abc, $select=nav, etc. + /// + /// The expanded reference select item. + /// The current properties to include at current level. + /// The computed properties. + /// The structural type properties. + private void BuildSelectItem(PathSelectItem pathSelectItem, + IDictionary currentLevelPropertiesInclude, + ISet computedProperties, + EdmStructuralTypeInfo structuralTypeInfo) + { + Contract.Assert(pathSelectItem != null && pathSelectItem.SelectedPath != null); + Contract.Assert(currentLevelPropertiesInclude != null); + Contract.Assert(structuralTypeInfo != null); + + // Verify and process the $select=abc/xyz/.... + ODataSelectPath selectPath = pathSelectItem.SelectedPath; + IList remainingSegments; + ODataPathSegment segment = selectPath.GetFirstNonTypeCastSegment(out remainingSegments); + + PropertySegment firstPropertySegment = segment as PropertySegment; + if (firstPropertySegment != null) + { + if (structuralTypeInfo.IsStructuralPropertyDefined(firstPropertySegment.Property)) { - if (structuralTypeInfo.IsStructuralPropertyDefined(firstPropertySegment.Property)) + // $select=abc/xyz/... + SelectExpandIncludedProperty newPropertySelectItem; + if (!currentLevelPropertiesInclude.TryGetValue(firstPropertySegment.Property, out newPropertySelectItem)) { - // $select=abc/xyz/... - SelectExpandIncludedProperty newPropertySelectItem; - if (!currentLevelPropertiesInclude.TryGetValue(firstPropertySegment.Property, out newPropertySelectItem)) - { - newPropertySelectItem = new SelectExpandIncludedProperty(firstPropertySegment); - currentLevelPropertiesInclude[firstPropertySegment.Property] = newPropertySelectItem; - } - - newPropertySelectItem.AddSubSelectItem(remainingSegments, pathSelectItem); + newPropertySelectItem = new SelectExpandIncludedProperty(firstPropertySegment); + currentLevelPropertiesInclude[firstPropertySegment.Property] = newPropertySelectItem; } - return; + newPropertySelectItem.AddSubSelectItem(remainingSegments, pathSelectItem); } - // If the first segment is not a property segment, - // that segment must be the last segment, so the remaining segments should be null. - Contract.Assert(remainingSegments == null); + return; + } + + // If the first segment is not a property segment, + // that segment must be the last segment, so the remaining segments should be null. + Contract.Assert(remainingSegments == null); - NavigationPropertySegment navigationSegment = segment as NavigationPropertySegment; - if (navigationSegment != null) + NavigationPropertySegment navigationSegment = segment as NavigationPropertySegment; + if (navigationSegment != null) + { + // for example: $select=NavigationProperty or $select=NS.VipCustomer/VipNav + if (structuralTypeInfo.IsNavigationPropertyDefined(navigationSegment.NavigationProperty)) { - // for example: $select=NavigationProperty or $select=NS.VipCustomer/VipNav - if (structuralTypeInfo.IsNavigationPropertyDefined(navigationSegment.NavigationProperty)) + if (SelectedNavigationProperties == null) { - if (SelectedNavigationProperties == null) - { - SelectedNavigationProperties = new HashSet(); - } - - SelectedNavigationProperties.Add(navigationSegment.NavigationProperty); + SelectedNavigationProperties = new HashSet(); } - return; + SelectedNavigationProperties.Add(navigationSegment.NavigationProperty); } - OperationSegment operationSegment = segment as OperationSegment; - if (operationSegment != null) + return; + } + + OperationSegment operationSegment = segment as OperationSegment; + if (operationSegment != null) + { + // for example: $select=NS.Operation, or, $select=NS.VipCustomer/NS.Operation + AddOperations(operationSegment, structuralTypeInfo.AllActions, structuralTypeInfo.AllFunctions); + return; + } + + DynamicPathSegment dynamicPathSegment = segment as DynamicPathSegment; + if (dynamicPathSegment != null) + { + if (computedProperties != null && computedProperties.Contains(dynamicPathSegment.Identifier)) { - // for example: $select=NS.Operation, or, $select=NS.VipCustomer/NS.Operation - AddOperations(operationSegment, structuralTypeInfo.AllActions, structuralTypeInfo.AllFunctions); - return; + // If it's from $compute + SelectedComputedProperties.Add(dynamicPathSegment.Identifier); } - - DynamicPathSegment dynamicPathSegment = segment as DynamicPathSegment; - if (dynamicPathSegment != null) + else { - if (computedProperties != null && computedProperties.Contains(dynamicPathSegment.Identifier)) + // or it's from dynamic property + if (SelectedDynamicProperties == null) { - // If it's from $compute - SelectedComputedProperties.Add(dynamicPathSegment.Identifier); + SelectedDynamicProperties = new HashSet(); } - else - { - // or it's from dynamic property - if (SelectedDynamicProperties == null) - { - SelectedDynamicProperties = new HashSet(); - } - SelectedDynamicProperties.Add(dynamicPathSegment.Identifier); - } - return; + SelectedDynamicProperties.Add(dynamicPathSegment.Identifier); } + return; + } + + // In fact, we should never be here, because it's verified above + throw new ODataException(Error.Format(SRResources.SelectionTypeNotSupported, segment.GetType().Name)); + } - // In fact, we should never be here, because it's verified above - throw new ODataException(Error.Format(SRResources.SelectionTypeNotSupported, segment.GetType().Name)); + private static void MergeAllStructuralProperties(ISet allStructuralProperties, + IDictionary currentLevelPropertiesInclude) + { + if (allStructuralProperties == null) + { + return; } - private static void MergeAllStructuralProperties(ISet allStructuralProperties, - IDictionary currentLevelPropertiesInclude) + Contract.Assert(currentLevelPropertiesInclude != null); + + foreach (var property in allStructuralProperties) { - if (allStructuralProperties == null) + if (!currentLevelPropertiesInclude.ContainsKey(property)) { - return; + // Set the value as null is safe, because this property should not further process. + // Besides, if there's "WildcardSelectItem", there's no other property selection items. + currentLevelPropertiesInclude[property] = null; } + } + } - Contract.Assert(currentLevelPropertiesInclude != null); + private void MergeSelectedNavigationProperties(ISet allNavigationProperties) + { + if (allNavigationProperties == null) + { + return; + } - foreach (var property in allStructuralProperties) - { - if (!currentLevelPropertiesInclude.ContainsKey(property)) - { - // Set the value as null is safe, because this property should not further process. - // Besides, if there's "WildcardSelectItem", there's no other property selection items. - currentLevelPropertiesInclude[property] = null; - } - } + if (SelectedNavigationProperties == null) + { + SelectedNavigationProperties = allNavigationProperties; } + else + { + SelectedNavigationProperties.UnionWith(allNavigationProperties); + } + } - private void MergeSelectedNavigationProperties(ISet allNavigationProperties) + private void MergeSelectedAction(ISet allActions) + { + if (allActions == null) { - if (allNavigationProperties == null) - { - return; - } + return; + } - if (SelectedNavigationProperties == null) - { - SelectedNavigationProperties = allNavigationProperties; - } - else - { - SelectedNavigationProperties.UnionWith(allNavigationProperties); - } + if (SelectedActions == null) + { + SelectedActions = allActions; + } + else + { + SelectedActions.UnionWith(allActions); } + } - private void MergeSelectedAction(ISet allActions) + private void MergeSelectedFunction(ISet allFunctions) + { + if (allFunctions == null) { - if (allActions == null) - { - return; - } + return; + } - if (SelectedActions == null) - { - SelectedActions = allActions; - } - else - { - SelectedActions.UnionWith(allActions); - } + if (SelectedFunctions == null) + { + SelectedFunctions = allFunctions; + } + else + { + SelectedFunctions.UnionWith(allFunctions); } + } + + private void AddStructuralProperty(IEdmStructuralProperty structuralProperty, PathSelectItem pathSelectItem) + { + bool isComplexOrCollectComplex = IsComplexOrCollectionComplex(structuralProperty); - private void MergeSelectedFunction(ISet allFunctions) + if (isComplexOrCollectComplex) { - if (allFunctions == null) + if (SelectedComplexProperties == null) { - return; + SelectedComplexProperties = new Dictionary(); } - if (SelectedFunctions == null) - { - SelectedFunctions = allFunctions; - } - else + SelectedComplexProperties[structuralProperty] = pathSelectItem; + } + else + { + // TODO: Do we support the query option on property whose type is 'Edm.Untyped' or 'Collection(Edm.Untyped)'? + // Created issue to track on it: https://github.com/OData/AspNetCoreOData/issues/916 + if (SelectedStructuralProperties == null) { - SelectedFunctions.UnionWith(allFunctions); + SelectedStructuralProperties = new HashSet(); } + + // for primitive, enum and collection them, needn't care about the nested query options now. + // So, skip the path select item. + SelectedStructuralProperties.Add(structuralProperty); + } + } + + private void AddNamespaceWildcardOperation(NamespaceQualifiedWildcardSelectItem namespaceSelectItem, ISet allActions, + ISet allFunctions) + { + if (allActions == null) + { + SelectedActions = null; + } + else + { + SelectedActions = new HashSet(allActions.Where(a => a.Namespace == namespaceSelectItem.Namespace)); } - private void AddStructuralProperty(IEdmStructuralProperty structuralProperty, PathSelectItem pathSelectItem) + if (allFunctions == null) { - bool isComplexOrCollectComplex = IsComplexOrCollectionComplex(structuralProperty); + SelectedFunctions = null; + } + else + { + SelectedFunctions = new HashSet(allFunctions.Where(a => a.Namespace == namespaceSelectItem.Namespace)); + } + } - if (isComplexOrCollectComplex) + private void AddOperations(OperationSegment operationSegment, ISet allActions, ISet allFunctions) + { + foreach (IEdmOperation operation in operationSegment.Operations) + { + IEdmAction action = operation as IEdmAction; + if (action != null && allActions.Contains(action)) { - if (SelectedComplexProperties == null) + if (SelectedActions == null) { - SelectedComplexProperties = new Dictionary(); + SelectedActions = new HashSet(); } - SelectedComplexProperties[structuralProperty] = pathSelectItem; + SelectedActions.Add(action); } - else + + IEdmFunction function = operation as IEdmFunction; + if (function != null && allFunctions.Contains(function)) { - // TODO: Do we support the query option on property whose type is 'Edm.Untyped' or 'Collection(Edm.Untyped)'? - // Created issue to track on it: https://github.com/OData/AspNetCoreOData/issues/916 - if (SelectedStructuralProperties == null) + if (SelectedFunctions == null) { - SelectedStructuralProperties = new HashSet(); + SelectedFunctions = new HashSet(); } - // for primitive, enum and collection them, needn't care about the nested query options now. - // So, skip the path select item. - SelectedStructuralProperties.Add(structuralProperty); + SelectedFunctions.Add(function); } } + } - private void AddNamespaceWildcardOperation(NamespaceQualifiedWildcardSelectItem namespaceSelectItem, ISet allActions, - ISet allFunctions) + private void AdjustSelectNavigationProperties() + { + if (SelectedNavigationProperties != null) { - if (allActions == null) + // remove expanded navigation properties from the selected navigation properties. + if (ExpandedProperties != null) { - SelectedActions = null; - } - else - { - SelectedActions = new HashSet(allActions.Where(a => a.Namespace == namespaceSelectItem.Namespace)); + SelectedNavigationProperties.ExceptWith(ExpandedProperties.Keys); } - if (allFunctions == null) + // remove referenced navigation properties from the selected navigation properties. + if (ReferencedProperties != null) { - SelectedFunctions = null; - } - else - { - SelectedFunctions = new HashSet(allFunctions.Where(a => a.Namespace == namespaceSelectItem.Namespace)); + SelectedNavigationProperties.ExceptWith(ReferencedProperties.Keys); } } - private void AddOperations(OperationSegment operationSegment, ISet allActions, ISet allFunctions) + if (SelectedNavigationProperties != null && !SelectedNavigationProperties.Any()) { - foreach (IEdmOperation operation in operationSegment.Operations) - { - IEdmAction action = operation as IEdmAction; - if (action != null && allActions.Contains(action)) - { - if (SelectedActions == null) - { - SelectedActions = new HashSet(); - } - - SelectedActions.Add(action); - } - - IEdmFunction function = operation as IEdmFunction; - if (function != null && allFunctions.Contains(function)) - { - if (SelectedFunctions == null) - { - SelectedFunctions = new HashSet(); - } - - SelectedFunctions.Add(function); - } - } + SelectedNavigationProperties = null; } + } - private void AdjustSelectNavigationProperties() + /// + /// Test whether the input structural property is complex property or collection of complex property. + /// + /// The test structural property. + /// True/false. + internal static bool IsComplexOrCollectionComplex(IEdmStructuralProperty structuralProperty) + { + if (structuralProperty == null) { - if (SelectedNavigationProperties != null) - { - // remove expanded navigation properties from the selected navigation properties. - if (ExpandedProperties != null) - { - SelectedNavigationProperties.ExceptWith(ExpandedProperties.Keys); - } + return false; + } - // remove referenced navigation properties from the selected navigation properties. - if (ReferencedProperties != null) - { - SelectedNavigationProperties.ExceptWith(ReferencedProperties.Keys); - } - } + if (structuralProperty.Type.IsComplex()) + { + return true; + } - if (SelectedNavigationProperties != null && !SelectedNavigationProperties.Any()) + if (structuralProperty.Type.IsCollection()) + { + if (structuralProperty.Type.AsCollection().ElementType().IsComplex()) { - SelectedNavigationProperties = null; + return true; } } + return false; + } + + /// + /// An internal cache class used to provide the property, operations + /// and do verification on the given . + /// + internal class EdmStructuralTypeInfo + { /// - /// Test whether the input structural property is complex property or collection of complex property. + /// Gets all structural properties defined on the structure type. /// - /// The test structural property. - /// True/false. - internal static bool IsComplexOrCollectionComplex(IEdmStructuralProperty structuralProperty) - { - if (structuralProperty == null) - { - return false; - } + public ISet AllStructuralProperties { get; } - if (structuralProperty.Type.IsComplex()) - { - return true; - } + /// + /// Gets all navigation properties defined on the structure type. + /// + public ISet AllNavigationProperties { get; } - if (structuralProperty.Type.IsCollection()) - { - if (structuralProperty.Type.AsCollection().ElementType().IsComplex()) - { - return true; - } - } + /// + /// Gets all actions bonding to the structure type. + /// + public ISet AllActions { get; } - return false; - } + /// + /// Gets all function bonding to the structure type. + /// + public ISet AllFunctions { get; } /// - /// An internal cache class used to provide the property, operations - /// and do verification on the given . + /// Creates a new instance of the class /// - internal class EdmStructuralTypeInfo - { - /// - /// Gets all structural properties defined on the structure type. - /// - public ISet AllStructuralProperties { get; } - - /// - /// Gets all navigation properties defined on the structure type. - /// - public ISet AllNavigationProperties { get; } - - /// - /// Gets all actions bonding to the structure type. - /// - public ISet AllActions { get; } - - /// - /// Gets all function bonding to the structure type. - /// - public ISet AllFunctions { get; } - - /// - /// Creates a new instance of the class - /// - /// The Edm model. - /// The Edm structured Type. - public EdmStructuralTypeInfo(IEdmModel model, IEdmStructuredType structuredType) - { - Contract.Assert(model != null); - Contract.Assert(structuredType != null); + /// The Edm model. + /// The Edm structured Type. + public EdmStructuralTypeInfo(IEdmModel model, IEdmStructuredType structuredType) + { + Contract.Assert(model != null); + Contract.Assert(structuredType != null); - foreach (var edmProperty in structuredType.Properties()) + foreach (var edmProperty in structuredType.Properties()) + { + switch (edmProperty.PropertyKind) { - switch (edmProperty.PropertyKind) - { - case EdmPropertyKind.Structural: - if (AllStructuralProperties == null) - { - AllStructuralProperties = new HashSet(); - } - - AllStructuralProperties.Add((IEdmStructuralProperty)edmProperty); - break; - - case EdmPropertyKind.Navigation: - if (AllNavigationProperties == null) - { - AllNavigationProperties = new HashSet(); - } - - AllNavigationProperties.Add((IEdmNavigationProperty)edmProperty); - break; - } - } + case EdmPropertyKind.Structural: + if (AllStructuralProperties == null) + { + AllStructuralProperties = new HashSet(); + } - IEdmEntityType entityType = structuredType as IEdmEntityType; - if (entityType != null) - { - var actions = model.GetAvailableActions(entityType); - AllActions = actions.Any() ? new HashSet(actions) : null; + AllStructuralProperties.Add((IEdmStructuralProperty)edmProperty); + break; - var functions = model.GetAvailableFunctions(entityType); - AllFunctions = functions.Any() ? new HashSet(functions) : null; + case EdmPropertyKind.Navigation: + if (AllNavigationProperties == null) + { + AllNavigationProperties = new HashSet(); + } + + AllNavigationProperties.Add((IEdmNavigationProperty)edmProperty); + break; } } - /// - /// Tests whether a is defined on this type. - /// - /// The test property. - /// True/false - public bool IsStructuralPropertyDefined(IEdmStructuralProperty property) + IEdmEntityType entityType = structuredType as IEdmEntityType; + if (entityType != null) { - return AllStructuralProperties != null && AllStructuralProperties.Contains(property); - } + var actions = model.GetAvailableActions(entityType); + AllActions = actions.Any() ? new HashSet(actions) : null; - /// - /// Tests whether a is defined on this type. - /// - /// The test property. - /// True/false - public bool IsNavigationPropertyDefined(IEdmNavigationProperty property) - { - return AllNavigationProperties != null && AllNavigationProperties.Contains(property); + var functions = model.GetAvailableFunctions(entityType); + AllFunctions = functions.Any() ? new HashSet(functions) : null; } } + + /// + /// Tests whether a is defined on this type. + /// + /// The test property. + /// True/false + public bool IsStructuralPropertyDefined(IEdmStructuralProperty property) + { + return AllStructuralProperties != null && AllStructuralProperties.Contains(property); + } + + /// + /// Tests whether a is defined on this type. + /// + /// The test property. + /// True/false + public bool IsNavigationPropertyDefined(IEdmNavigationProperty property) + { + return AllNavigationProperties != null && AllNavigationProperties.Contains(property); + } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmChangedObjectCollection.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmChangedObjectCollection.cs index 9a95c356b..9e93e0827 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmChangedObjectCollection.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmChangedObjectCollection.cs @@ -11,55 +11,54 @@ using Microsoft.AspNetCore.OData.Abstracts; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an that is a collection of s. +/// +[NonValidatingParameterBinding] +public class EdmChangedObjectCollection : Collection, IEdmObject { + private IEdmEntityType _entityType; + private EdmDeltaCollectionType _edmType; + private IEdmCollectionTypeReference _edmTypeReference; + /// - /// Represents an that is a collection of s. + /// Initializes a new instance of the class. /// - [NonValidatingParameterBinding] - public class EdmChangedObjectCollection : Collection, IEdmObject + /// The Edm entity type of the collection. + public EdmChangedObjectCollection(IEdmEntityType entityType) + : base(Enumerable.Empty().ToList()) { - private IEdmEntityType _entityType; - private EdmDeltaCollectionType _edmType; - private IEdmCollectionTypeReference _edmTypeReference; - - /// - /// Initializes a new instance of the class. - /// - /// The Edm entity type of the collection. - public EdmChangedObjectCollection(IEdmEntityType entityType) - : base(Enumerable.Empty().ToList()) - { - Initialize(entityType); - } + Initialize(entityType); + } - /// - /// Initializes a new instance of the class. - /// - /// The Edm type of the collection. - /// The list that is wrapped by the new collection. - public EdmChangedObjectCollection(IEdmEntityType entityType, IList changedObjectList) - : base(changedObjectList) - { - Initialize(entityType); - } + /// + /// Initializes a new instance of the class. + /// + /// The Edm type of the collection. + /// The list that is wrapped by the new collection. + public EdmChangedObjectCollection(IEdmEntityType entityType, IList changedObjectList) + : base(changedObjectList) + { + Initialize(entityType); + } - /// - public IEdmTypeReference GetEdmType() - { - return _edmTypeReference; - } + /// + public IEdmTypeReference GetEdmType() + { + return _edmTypeReference; + } - private void Initialize(IEdmEntityType entityType) + private void Initialize(IEdmEntityType entityType) + { + if (entityType == null) { - if (entityType == null) - { - throw Error.ArgumentNull("entityType"); - } - - _entityType = entityType; - _edmType = new EdmDeltaCollectionType(new EdmEntityTypeReference(_entityType, isNullable: true)); - _edmTypeReference = new EdmCollectionTypeReference(_edmType); + throw Error.ArgumentNull("entityType"); } + + _entityType = entityType; + _edmType = new EdmDeltaCollectionType(new EdmEntityTypeReference(_entityType, isNullable: true)); + _edmTypeReference = new EdmCollectionTypeReference(_edmType); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmComplexCollectionObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmComplexCollectionObject.cs index 34c141bcc..7f552dcba 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmComplexCollectionObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmComplexCollectionObject.cs @@ -10,56 +10,55 @@ using Microsoft.AspNetCore.OData.Abstracts; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an that is a collection of s. +/// +[NonValidatingParameterBinding] +public class EdmComplexObjectCollection : Collection, IEdmObject { + private IEdmCollectionTypeReference _edmType; + /// - /// Represents an that is a collection of s. + /// Initializes a new instance of the class. /// - [NonValidatingParameterBinding] - public class EdmComplexObjectCollection : Collection, IEdmObject + /// The edm type of the collection. + public EdmComplexObjectCollection(IEdmCollectionTypeReference edmType) { - private IEdmCollectionTypeReference _edmType; + Initialize(edmType); + } - /// - /// Initializes a new instance of the class. - /// - /// The edm type of the collection. - public EdmComplexObjectCollection(IEdmCollectionTypeReference edmType) - { - Initialize(edmType); - } + /// + /// Initializes a new instance of the class. + /// + /// The edm type of the collection. + /// The list that is wrapped by the new collection. + public EdmComplexObjectCollection(IEdmCollectionTypeReference edmType, IList list) + : base(list) + { + Initialize(edmType); + } - /// - /// Initializes a new instance of the class. - /// - /// The edm type of the collection. - /// The list that is wrapped by the new collection. - public EdmComplexObjectCollection(IEdmCollectionTypeReference edmType, IList list) - : base(list) - { - Initialize(edmType); - } + /// + public IEdmTypeReference GetEdmType() + { + return _edmType; + } - /// - public IEdmTypeReference GetEdmType() + private void Initialize(IEdmCollectionTypeReference edmType) + { + if (edmType == null) { - return _edmType; + throw Error.ArgumentNull("edmType"); } - private void Initialize(IEdmCollectionTypeReference edmType) + if (!edmType.ElementType().IsComplex()) { - if (edmType == null) - { - throw Error.ArgumentNull("edmType"); - } - - if (!edmType.ElementType().IsComplex()) - { - throw Error.Argument("edmType", - SRResources.UnexpectedElementType, edmType.ElementType().ToTraceString(), edmType.ToTraceString(), typeof(IEdmComplexType).Name); - } - - _edmType = edmType; + throw Error.Argument("edmType", + SRResources.UnexpectedElementType, edmType.ElementType().ToTraceString(), edmType.ToTraceString(), typeof(IEdmComplexType).Name); } + + _edmType = edmType; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmComplexObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmComplexObject.cs index 8fdc3a490..7604769c5 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmComplexObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmComplexObject.cs @@ -10,41 +10,40 @@ using Microsoft.OData.Edm; using Microsoft.AspNetCore.OData.Abstracts; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an with no backing CLR . +/// +[NonValidatingParameterBinding] +public class EdmComplexObject : EdmStructuredObject, IEdmComplexObject { /// - /// Represents an with no backing CLR . + /// Initializes a new instance of the class. /// - [NonValidatingParameterBinding] - public class EdmComplexObject : EdmStructuredObject, IEdmComplexObject + /// The of this object. + public EdmComplexObject(IEdmComplexType edmType) + : this(edmType, isNullable: false) { - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - public EdmComplexObject(IEdmComplexType edmType) - : this(edmType, isNullable: false) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "ComplexDefinition checks the nullable.")] - public EdmComplexObject(IEdmComplexTypeReference edmType) - : this(edmType.ComplexDefinition(), edmType.IsNullable) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The of this object. + [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "ComplexDefinition checks the nullable.")] + public EdmComplexObject(IEdmComplexTypeReference edmType) + : this(edmType.ComplexDefinition(), edmType.IsNullable) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - /// true if this object can be nullable; otherwise, false. - public EdmComplexObject(IEdmComplexType edmType, bool isNullable) - : base(edmType, isNullable) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The of this object. + /// true if this object can be nullable; otherwise, false. + public EdmComplexObject(IEdmComplexType edmType, bool isNullable) + : base(edmType, isNullable) + { } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaCollectionType.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaCollectionType.cs index 55e555ec4..db9724ad7 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaCollectionType.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaCollectionType.cs @@ -7,26 +7,25 @@ using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Implementing IEdmCollectionType to identify collection of DeltaResourceSet. +/// +internal class EdmDeltaCollectionType : IEdmCollectionType { /// - /// Implementing IEdmCollectionType to identify collection of DeltaResourceSet. + /// Initializes a new instance of the class. /// - internal class EdmDeltaCollectionType : IEdmCollectionType + /// The element type reference. + internal EdmDeltaCollectionType(IEdmTypeReference typeReference) { - /// - /// Initializes a new instance of the class. - /// - /// The element type reference. - internal EdmDeltaCollectionType(IEdmTypeReference typeReference) - { - ElementType = typeReference ?? throw Error.ArgumentNull(nameof(typeReference)); - } + ElementType = typeReference ?? throw Error.ArgumentNull(nameof(typeReference)); + } - /// - public EdmTypeKind TypeKind => EdmTypeKind.Collection; + /// + public EdmTypeKind TypeKind => EdmTypeKind.Collection; - /// - public IEdmTypeReference ElementType { get; } - } + /// + public IEdmTypeReference ElementType { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaComplexObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaComplexObject.cs index 72d21f93c..cf78aeacd 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaComplexObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaComplexObject.cs @@ -10,42 +10,41 @@ using Microsoft.OData.Edm; using Microsoft.AspNetCore.OData.Abstracts; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an with no backing CLR . +/// Used to hold the Entry object in the Delta Feed Payload. +/// +[NonValidatingParameterBinding] +public class EdmDeltaComplexObject : EdmComplexObject { /// - /// Represents an with no backing CLR . - /// Used to hold the Entry object in the Delta Feed Payload. + /// Initializes a new instance of the class. /// - [NonValidatingParameterBinding] - public class EdmDeltaComplexObject : EdmComplexObject + /// The of this object. + public EdmDeltaComplexObject(IEdmComplexType edmType) + : this(edmType, isNullable: false) { - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - public EdmDeltaComplexObject(IEdmComplexType edmType) - : this(edmType, isNullable: false) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "ComplexDefinition checks the nullable.")] - public EdmDeltaComplexObject(IEdmComplexTypeReference edmType) - : this(edmType.ComplexDefinition(), edmType.IsNullable) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The of this object. + [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "ComplexDefinition checks the nullable.")] + public EdmDeltaComplexObject(IEdmComplexTypeReference edmType) + : this(edmType.ComplexDefinition(), edmType.IsNullable) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - /// true if this object can be nullable; otherwise, false. - public EdmDeltaComplexObject(IEdmComplexType edmType, bool isNullable) - : base(edmType, isNullable) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The of this object. + /// true if this object can be nullable; otherwise, false. + public EdmDeltaComplexObject(IEdmComplexType edmType, bool isNullable) + : base(edmType, isNullable) + { } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaDeletedLink.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaDeletedLink.cs index 6bd985f3e..6cfc8e7e3 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaDeletedLink.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaDeletedLink.cs @@ -10,44 +10,43 @@ using Microsoft.AspNetCore.OData.Abstracts; using Microsoft.AspNetCore.OData.Deltas; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an with no backing CLR . +/// Used to hold the Deleted Link object in the Delta ResourceSet Payload. +/// +[NonValidatingParameterBinding] +public class EdmDeltaDeletedLink : EdmDeltaLinkBase, IEdmDeltaDeletedLink { /// - /// Represents an with no backing CLR . - /// Used to hold the Deleted Link object in the Delta ResourceSet Payload. + /// Initializes a new instance of the class. /// - [NonValidatingParameterBinding] - public class EdmDeltaDeletedLink : EdmDeltaLinkBase, IEdmDeltaDeletedLink + /// The of this DeltaDeletedLink. + public EdmDeltaDeletedLink(IEdmEntityType entityType) + : this(entityType, isNullable: false) { - /// - /// Initializes a new instance of the class. - /// - /// The of this DeltaDeletedLink. - public EdmDeltaDeletedLink(IEdmEntityType entityType) - : this(entityType, isNullable: false) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The of this DeltaDeletedLink. - public EdmDeltaDeletedLink(IEdmEntityTypeReference entityTypeReference) - : base(entityTypeReference) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The of this DeltaDeletedLink. - /// true if this object can be nullable; otherwise, false. - public EdmDeltaDeletedLink(IEdmEntityType entityType, bool isNullable) - : base(entityType, isNullable) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The of this DeltaDeletedLink. + public EdmDeltaDeletedLink(IEdmEntityTypeReference entityTypeReference) + : base(entityTypeReference) + { + } - /// - public override DeltaItemKind Kind => DeltaItemKind.DeltaDeletedLink; + /// + /// Initializes a new instance of the class. + /// + /// The of this DeltaDeletedLink. + /// true if this object can be nullable; otherwise, false. + public EdmDeltaDeletedLink(IEdmEntityType entityType, bool isNullable) + : base(entityType, isNullable) + { } + + /// + public override DeltaItemKind Kind => DeltaItemKind.DeltaDeletedLink; } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaDeletedResourceObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaDeletedResourceObject.cs index b05d36e32..7a149c1f9 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaDeletedResourceObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaDeletedResourceObject.cs @@ -13,59 +13,58 @@ using Microsoft.AspNetCore.OData.Abstracts; using Microsoft.AspNetCore.OData.Deltas; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an with no backing CLR . +/// Used to hold the Deleted Resource object in the Delta Feed Payload. +/// +[NonValidatingParameterBinding] +public class EdmDeltaDeletedResourceObject : EdmEntityObject, IEdmDeltaDeletedResourceObject { + private EdmDeltaType _edmType; + /// - /// Represents an with no backing CLR . - /// Used to hold the Deleted Resource object in the Delta Feed Payload. + /// Initializes a new instance of the class. /// - [NonValidatingParameterBinding] - public class EdmDeltaDeletedResourceObject : EdmEntityObject, IEdmDeltaDeletedResourceObject + /// The of this DeltaDeletedEntityObject. + public EdmDeltaDeletedResourceObject(IEdmEntityType entityType) + : this(entityType, isNullable: false) { - private EdmDeltaType _edmType; - - /// - /// Initializes a new instance of the class. - /// - /// The of this DeltaDeletedEntityObject. - public EdmDeltaDeletedResourceObject(IEdmEntityType entityType) - : this(entityType, isNullable: false) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The of this DeltaDeletedEntityObject. - [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "EntityDefinition checks the nullable.")] - public EdmDeltaDeletedResourceObject(IEdmEntityTypeReference entityTypeReference) - : this(entityTypeReference.EntityDefinition(), entityTypeReference.IsNullable) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The of this DeltaDeletedEntityObject. + [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "EntityDefinition checks the nullable.")] + public EdmDeltaDeletedResourceObject(IEdmEntityTypeReference entityTypeReference) + : this(entityTypeReference.EntityDefinition(), entityTypeReference.IsNullable) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The of this DeltaDeletedEntityObject. - /// true if this object can be nullable; otherwise, false. - public EdmDeltaDeletedResourceObject(IEdmEntityType entityType, bool isNullable) - : base(entityType, isNullable) - { - _edmType = new EdmDeltaType(entityType, DeltaItemKind.DeletedResource); - } + /// + /// Initializes a new instance of the class. + /// + /// The of this DeltaDeletedEntityObject. + /// true if this object can be nullable; otherwise, false. + public EdmDeltaDeletedResourceObject(IEdmEntityType entityType, bool isNullable) + : base(entityType, isNullable) + { + _edmType = new EdmDeltaType(entityType, DeltaItemKind.DeletedResource); + } - /// - public Uri Id { get; set; } + /// + public Uri Id { get; set; } - /// - public DeltaDeletedEntryReason? Reason { get; set; } + /// + public DeltaDeletedEntryReason? Reason { get; set; } - /// - public override DeltaItemKind Kind => DeltaItemKind.DeletedResource; + /// + public override DeltaItemKind Kind => DeltaItemKind.DeletedResource; - /// - /// The navigation source of the deleted entity. If null, then the deleted entity is from the current feed. - /// - public IEdmNavigationSource NavigationSource { get; set; } - } + /// + /// The navigation source of the deleted entity. If null, then the deleted entity is from the current feed. + /// + public IEdmNavigationSource NavigationSource { get; set; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaLink.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaLink.cs index fc3a8557f..9deba0142 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaLink.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaLink.cs @@ -10,44 +10,43 @@ using Microsoft.AspNetCore.OData.Abstracts; using Microsoft.AspNetCore.OData.Deltas; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an with no backing CLR . +/// Used to hold the Added/Modified Link object in the Delta ResourceSet Payload. +/// +[NonValidatingParameterBinding] +public class EdmDeltaLink : EdmDeltaLinkBase, IEdmDeltaLink { /// - /// Represents an with no backing CLR . - /// Used to hold the Added/Modified Link object in the Delta ResourceSet Payload. + /// Initializes a new instance of the class. /// - [NonValidatingParameterBinding] - public class EdmDeltaLink : EdmDeltaLinkBase, IEdmDeltaLink + /// The of this DeltaLink. + public EdmDeltaLink(IEdmEntityType entityType) + : this(entityType, isNullable: false) { - /// - /// Initializes a new instance of the class. - /// - /// The of this DeltaLink. - public EdmDeltaLink(IEdmEntityType entityType) - : this(entityType, isNullable: false) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The of this DeltaLink. - public EdmDeltaLink(IEdmEntityTypeReference entityTypeReference) - : base(entityTypeReference) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The of this DeltaLink. - /// true if this object can be nullable; otherwise, false. - public EdmDeltaLink(IEdmEntityType entityType, bool isNullable) - : base(entityType, isNullable) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The of this DeltaLink. + public EdmDeltaLink(IEdmEntityTypeReference entityTypeReference) + : base(entityTypeReference) + { + } - /// - public override DeltaItemKind Kind => DeltaItemKind.DeltaLink; + /// + /// Initializes a new instance of the class. + /// + /// The of this DeltaLink. + /// true if this object can be nullable; otherwise, false. + public EdmDeltaLink(IEdmEntityType entityType, bool isNullable) + : base(entityType, isNullable) + { } + + /// + public override DeltaItemKind Kind => DeltaItemKind.DeltaLink; } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaLinkBase.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaLinkBase.cs index 708389762..8010567ed 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaLinkBase.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaLinkBase.cs @@ -9,68 +9,67 @@ using Microsoft.OData.Edm; using Microsoft.AspNetCore.OData.Deltas; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// The base class for delta link. +/// +public abstract class EdmDeltaLinkBase : IEdmDeltaLinkBase { + private IEdmEntityTypeReference _edmTypeReference; + /// - /// The base class for delta link. + /// Initializes a new instance of the class. /// - public abstract class EdmDeltaLinkBase : IEdmDeltaLinkBase + /// The given entity type reference. + protected EdmDeltaLinkBase(IEdmEntityTypeReference typeReference) { - private IEdmEntityTypeReference _edmTypeReference; + _edmTypeReference = typeReference ?? throw Error.ArgumentNull(nameof(typeReference)); + } - /// - /// Initializes a new instance of the class. - /// - /// The given entity type reference. - protected EdmDeltaLinkBase(IEdmEntityTypeReference typeReference) + /// + /// Initializes a new instance of the class. + /// + /// The given entity type. + /// Nullable or not. + protected EdmDeltaLinkBase(IEdmEntityType entityType, bool isNullable) + { + if (entityType == null) { - _edmTypeReference = typeReference ?? throw Error.ArgumentNull(nameof(typeReference)); + throw Error.ArgumentNull(nameof(entityType)); } - /// - /// Initializes a new instance of the class. - /// - /// The given entity type. - /// Nullable or not. - protected EdmDeltaLinkBase(IEdmEntityType entityType, bool isNullable) - { - if (entityType == null) - { - throw Error.ArgumentNull(nameof(entityType)); - } - - _edmTypeReference = new EdmEntityTypeReference(entityType, isNullable); - } + _edmTypeReference = new EdmEntityTypeReference(entityType, isNullable); + } - /// - /// Gets the entity type. - /// - public IEdmEntityType EntityType => _edmTypeReference.EntityDefinition(); + /// + /// Gets the entity type. + /// + public IEdmEntityType EntityType => _edmTypeReference.EntityDefinition(); - /// - /// Gets the nullable value. - /// - public bool IsNullable => _edmTypeReference.IsNullable; + /// + /// Gets the nullable value. + /// + public bool IsNullable => _edmTypeReference.IsNullable; - /// - /// The Uri of the entity from which the relationship is defined, which may be absolute or relative. - /// - public Uri Source { get; set; } + /// + /// The Uri of the entity from which the relationship is defined, which may be absolute or relative. + /// + public Uri Source { get; set; } - /// - /// The Uri of the related entity, which may be absolute or relative. - /// - public Uri Target { get; set; } + /// + /// The Uri of the related entity, which may be absolute or relative. + /// + public Uri Target { get; set; } - /// - /// The name of the relationship property on the parent object. - /// - public string Relationship { get; set; } + /// + /// The name of the relationship property on the parent object. + /// + public string Relationship { get; set; } - /// - public abstract DeltaItemKind Kind { get; } + /// + public abstract DeltaItemKind Kind { get; } - /// - public IEdmTypeReference GetEdmType() => _edmTypeReference; - } + /// + public IEdmTypeReference GetEdmType() => _edmTypeReference; } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaResourceObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaResourceObject.cs index 28ce0c1e5..2cc601730 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaResourceObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaResourceObject.cs @@ -12,61 +12,60 @@ using Microsoft.AspNetCore.OData.Abstracts; using Microsoft.AspNetCore.OData.Deltas; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an with no backing CLR . +/// Used to hold the Entry object in the Delta Feed Payload. +/// +[NonValidatingParameterBinding] +public class EdmDeltaResourceObject : EdmEntityObject, IEdmChangedObject { + // TODO: this class should remove, use the EdmEntityObject. + private EdmDeltaType _edmType; + /// - /// Represents an with no backing CLR . - /// Used to hold the Entry object in the Delta Feed Payload. + /// Initializes a new instance of the class. /// - [NonValidatingParameterBinding] - public class EdmDeltaResourceObject : EdmEntityObject, IEdmChangedObject + /// The of this DeltaEntityObject. + public EdmDeltaResourceObject(IEdmEntityType entityType) + : this(entityType, isNullable: false) { - // TODO: this class should remove, use the EdmEntityObject. - private EdmDeltaType _edmType; - - /// - /// Initializes a new instance of the class. - /// - /// The of this DeltaEntityObject. - public EdmDeltaResourceObject(IEdmEntityType entityType) - : this(entityType, isNullable: false) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The of this DeltaEntityObject. - [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "EntityDefinition checks the nullable.")] - public EdmDeltaResourceObject(IEdmEntityTypeReference entityTypeReference) - : this(entityTypeReference.EntityDefinition(), entityTypeReference.IsNullable) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The of this DeltaEntityObject. + [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "EntityDefinition checks the nullable.")] + public EdmDeltaResourceObject(IEdmEntityTypeReference entityTypeReference) + : this(entityTypeReference.EntityDefinition(), entityTypeReference.IsNullable) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The of this DeltaEntityObject. - /// true if this object can be nullable; otherwise, false. - public EdmDeltaResourceObject(IEdmEntityType entityType, bool isNullable) - : base(entityType, isNullable) - { - _edmType = new EdmDeltaType(entityType, DeltaItemKind.Resource); - } + /// + /// Initializes a new instance of the class. + /// + /// The of this DeltaEntityObject. + /// true if this object can be nullable; otherwise, false. + public EdmDeltaResourceObject(IEdmEntityType entityType, bool isNullable) + : base(entityType, isNullable) + { + _edmType = new EdmDeltaType(entityType, DeltaItemKind.Resource); + } - /// - public DeltaItemKind DeltaKind + /// + public DeltaItemKind DeltaKind + { + get { - get - { - Contract.Assert(_edmType != null); - return _edmType.DeltaKind; - } + Contract.Assert(_edmType != null); + return _edmType.DeltaKind; } - - /// - /// The navigation source of the entity. If null, then the entity is from the current feed. - /// - public IEdmNavigationSource NavigationSource { get; set; } } + + /// + /// The navigation source of the entity. If null, then the entity is from the current feed. + /// + public IEdmNavigationSource NavigationSource { get; set; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaType.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaType.cs index 3f9d79ef2..151e347ec 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaType.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmDeltaType.cs @@ -8,27 +8,26 @@ using Microsoft.AspNetCore.OData.Deltas; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Implementing IEdmType to identify objects which are part of DeltaResourceSet Payload. +/// +internal class EdmDeltaType : IEdmType { - /// - /// Implementing IEdmType to identify objects which are part of DeltaResourceSet Payload. - /// - internal class EdmDeltaType : IEdmType + internal EdmDeltaType(IEdmEntityType entityType, DeltaItemKind deltaKind) { - internal EdmDeltaType(IEdmEntityType entityType, DeltaItemKind deltaKind) - { - EntityType = entityType ?? throw Error.ArgumentNull(nameof(entityType)); - DeltaKind = deltaKind; - } + EntityType = entityType ?? throw Error.ArgumentNull(nameof(entityType)); + DeltaKind = deltaKind; + } - /// - public EdmTypeKind TypeKind => EdmTypeKind.Entity; + /// + public EdmTypeKind TypeKind => EdmTypeKind.Entity; - public IEdmEntityType EntityType { get; } + public IEdmEntityType EntityType { get; } - /// - /// Returning DeltaKind of the object within DeltaResourceSet payload - /// - public DeltaItemKind DeltaKind { get; } - } + /// + /// Returning DeltaKind of the object within DeltaResourceSet payload + /// + public DeltaItemKind DeltaKind { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEntityCollectionObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEntityCollectionObject.cs index 9786d6051..c24223c28 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEntityCollectionObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEntityCollectionObject.cs @@ -10,55 +10,54 @@ using Microsoft.OData.Edm; using Microsoft.AspNetCore.OData.Abstracts; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an that is a collection of s. +/// +[NonValidatingParameterBinding] +public class EdmEntityObjectCollection : Collection, IEdmObject { + private IEdmCollectionTypeReference _edmType; + /// - /// Represents an that is a collection of s. + /// Initializes a new instance of the class. /// - [NonValidatingParameterBinding] - public class EdmEntityObjectCollection : Collection, IEdmObject + /// The edm type of the collection. + public EdmEntityObjectCollection(IEdmCollectionTypeReference edmType) { - private IEdmCollectionTypeReference _edmType; + Initialize(edmType); + } - /// - /// Initializes a new instance of the class. - /// - /// The edm type of the collection. - public EdmEntityObjectCollection(IEdmCollectionTypeReference edmType) - { - Initialize(edmType); - } + /// + /// Initializes a new instance of the class. + /// + /// The edm type of the collection. + /// The list that is wrapped by the new collection. + public EdmEntityObjectCollection(IEdmCollectionTypeReference edmType, IList list) + : base(list) + { + Initialize(edmType); + } - /// - /// Initializes a new instance of the class. - /// - /// The edm type of the collection. - /// The list that is wrapped by the new collection. - public EdmEntityObjectCollection(IEdmCollectionTypeReference edmType, IList list) - : base(list) - { - Initialize(edmType); - } + /// + public IEdmTypeReference GetEdmType() + { + return _edmType; + } - /// - public IEdmTypeReference GetEdmType() + private void Initialize(IEdmCollectionTypeReference edmType) + { + if (edmType == null) { - return _edmType; + throw Error.ArgumentNull("edmType"); } - - private void Initialize(IEdmCollectionTypeReference edmType) + if (!edmType.ElementType().IsEntity()) { - if (edmType == null) - { - throw Error.ArgumentNull("edmType"); - } - if (!edmType.ElementType().IsEntity()) - { - throw Error.Argument("edmType", - SRResources.UnexpectedElementType, edmType.ElementType().ToTraceString(), edmType.ToTraceString(), typeof(IEdmEntityType).Name); - } - - _edmType = edmType; + throw Error.Argument("edmType", + SRResources.UnexpectedElementType, edmType.ElementType().ToTraceString(), edmType.ToTraceString(), typeof(IEdmEntityType).Name); } + + _edmType = edmType; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEntityObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEntityObject.cs index 635abcfa4..95e5c24f3 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEntityObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEntityObject.cs @@ -10,41 +10,40 @@ using Microsoft.OData.Edm; using Microsoft.AspNetCore.OData.Abstracts; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an with no backing CLR . +/// +[NonValidatingParameterBinding] +public class EdmEntityObject : EdmStructuredObject, IEdmEntityObject { /// - /// Represents an with no backing CLR . + /// Initializes a new instance of the class. /// - [NonValidatingParameterBinding] - public class EdmEntityObject : EdmStructuredObject, IEdmEntityObject + /// The of this object. + public EdmEntityObject(IEdmEntityType edmType) + : this(edmType, isNullable: false) { - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - public EdmEntityObject(IEdmEntityType edmType) - : this(edmType, isNullable: false) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "EntityDefinition checks the nullable.")] - public EdmEntityObject(IEdmEntityTypeReference edmType) - : this(edmType.EntityDefinition(), edmType.IsNullable) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The of this object. + [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "EntityDefinition checks the nullable.")] + public EdmEntityObject(IEdmEntityTypeReference edmType) + : this(edmType.EntityDefinition(), edmType.IsNullable) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - /// true if this object can be nullable; otherwise, false. - public EdmEntityObject(IEdmEntityType edmType, bool isNullable) - : base(edmType, isNullable) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The of this object. + /// true if this object can be nullable; otherwise, false. + public EdmEntityObject(IEdmEntityType edmType, bool isNullable) + : base(edmType, isNullable) + { } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEnumObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEnumObject.cs index 29caf8cca..28714467d 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEnumObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEnumObject.cs @@ -10,68 +10,67 @@ using Microsoft.OData.Edm; using Microsoft.AspNetCore.OData.Abstracts; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an with no backing CLR . +/// +[NonValidatingParameterBinding] +public class EdmEnumObject : IEdmEnumObject { + private readonly IEdmType _edmType; + /// - /// Represents an with no backing CLR . + /// Gets the value of the enumeration type. /// - [NonValidatingParameterBinding] - public class EdmEnumObject : IEdmEnumObject - { - private readonly IEdmType _edmType; + public string Value { get; set; } - /// - /// Gets the value of the enumeration type. - /// - public string Value { get; set; } - - /// - /// Gets or sets whether the enum object is nullable or not. - /// - public bool IsNullable { get; set; } + /// + /// Gets or sets whether the enum object is nullable or not. + /// + public bool IsNullable { get; set; } - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - /// The value of the enumeration type. - public EdmEnumObject(IEdmEnumType edmType, string value) - : this(edmType, value, isNullable: false) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The of this object. + /// The value of the enumeration type. + public EdmEnumObject(IEdmEnumType edmType, string value) + : this(edmType, value, isNullable: false) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - /// The value of the enumeration type. - [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "EnumDefinition checks the nullable.")] - public EdmEnumObject(IEdmEnumTypeReference edmType, string value) - : this(edmType.EnumDefinition(), value, edmType.IsNullable) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The of this object. + /// The value of the enumeration type. + [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "EnumDefinition checks the nullable.")] + public EdmEnumObject(IEdmEnumTypeReference edmType, string value) + : this(edmType.EnumDefinition(), value, edmType.IsNullable) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - /// The value of the enumeration type. - /// true if this object can be nullable; otherwise, false. - public EdmEnumObject(IEdmEnumType edmType, string value, bool isNullable) + /// + /// Initializes a new instance of the class. + /// + /// The of this object. + /// The value of the enumeration type. + /// true if this object can be nullable; otherwise, false. + public EdmEnumObject(IEdmEnumType edmType, string value, bool isNullable) + { + if (edmType == null) { - if (edmType == null) - { - throw Error.ArgumentNull("edmType"); - } - _edmType = edmType; - Value = value; - IsNullable = isNullable; + throw Error.ArgumentNull("edmType"); } + _edmType = edmType; + Value = value; + IsNullable = isNullable; + } - /// - public IEdmTypeReference GetEdmType() - { - return new EdmEnumTypeReference(_edmType as IEdmEnumType, IsNullable); - } + /// + public IEdmTypeReference GetEdmType() + { + return new EdmEnumTypeReference(_edmType as IEdmEnumType, IsNullable); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEnumObjectCollection.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEnumObjectCollection.cs index 754587318..37228e705 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEnumObjectCollection.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmEnumObjectCollection.cs @@ -11,55 +11,54 @@ using Microsoft.OData.Edm; using Microsoft.AspNetCore.OData.Abstracts; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an that is a collection of s. +/// +[NonValidatingParameterBinding] +public class EdmEnumObjectCollection : Collection, IEdmObject { + private IEdmCollectionTypeReference _edmType; + /// - /// Represents an that is a collection of s. + /// Initializes a new instance of the class. /// - [NonValidatingParameterBinding] - public class EdmEnumObjectCollection : Collection, IEdmObject + /// The edm type of the collection. + public EdmEnumObjectCollection(IEdmCollectionTypeReference edmType) + : this(edmType, Enumerable.Empty().ToList()) { - private IEdmCollectionTypeReference _edmType; + } - /// - /// Initializes a new instance of the class. - /// - /// The edm type of the collection. - public EdmEnumObjectCollection(IEdmCollectionTypeReference edmType) - : this(edmType, Enumerable.Empty().ToList()) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The edm type of the collection. + /// The list that is wrapped by the new collection. + public EdmEnumObjectCollection(IEdmCollectionTypeReference edmType, IList list) + : base(list) + { + Initialize(edmType); + } - /// - /// Initializes a new instance of the class. - /// - /// The edm type of the collection. - /// The list that is wrapped by the new collection. - public EdmEnumObjectCollection(IEdmCollectionTypeReference edmType, IList list) - : base(list) - { - Initialize(edmType); - } + /// + public IEdmTypeReference GetEdmType() + { + return _edmType; + } - /// - public IEdmTypeReference GetEdmType() + private void Initialize(IEdmCollectionTypeReference edmType) + { + if (edmType == null) { - return _edmType; + throw Error.ArgumentNull("edmType"); } - - private void Initialize(IEdmCollectionTypeReference edmType) + if (!edmType.ElementType().IsEnum()) { - if (edmType == null) - { - throw Error.ArgumentNull("edmType"); - } - if (!edmType.ElementType().IsEnum()) - { - throw Error.Argument("edmType", - SRResources.UnexpectedElementType, edmType.ElementType().ToTraceString(), edmType.ToTraceString(), typeof(IEdmEnumType).Name); - } - - _edmType = edmType; + throw Error.Argument("edmType", + SRResources.UnexpectedElementType, edmType.ElementType().ToTraceString(), edmType.ToTraceString(), typeof(IEdmEnumType).Name); } + + _edmType = edmType; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectExtensions.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectExtensions.cs index 26bfb1d09..5082e15ec 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectExtensions.cs @@ -8,55 +8,54 @@ using Microsoft.AspNetCore.OData.Deltas; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Extension methods for the interface. +/// +public static class EdmTypeExtensions { /// - /// Extension methods for the interface. + /// Method to determine whether the current type is a Delta resource set. /// - public static class EdmTypeExtensions + /// IEdmType to be compared + /// True or False if type is same as + public static bool IsDeltaResourceSet(this IEdmType type) { - /// - /// Method to determine whether the current type is a Delta resource set. - /// - /// IEdmType to be compared - /// True or False if type is same as - public static bool IsDeltaResourceSet(this IEdmType type) + if (type == null) { - if (type == null) - { - throw Error.ArgumentNull(nameof(type)); - } - - return (type.GetType() == typeof(EdmDeltaCollectionType)); + throw Error.ArgumentNull(nameof(type)); } - /// - /// Method to determine whether the current Edm object is a Delta resource - /// - /// IEdmObject to be compared - /// True or False if type is same as or - public static bool IsDeltaResource(this IEdmObject resource) - { - if (resource == null) - { - throw Error.ArgumentNull(nameof(resource)); - } + return (type.GetType() == typeof(EdmDeltaCollectionType)); + } - TypedEdmEntityObject obj = resource as TypedEdmEntityObject; - if (obj != null) - { - if (obj.Instance is IDeltaSetItem) - { - return true; - } - } + /// + /// Method to determine whether the current Edm object is a Delta resource + /// + /// IEdmObject to be compared + /// True or False if type is same as or + public static bool IsDeltaResource(this IEdmObject resource) + { + if (resource == null) + { + throw Error.ArgumentNull(nameof(resource)); + } - if (resource is IDeltaSetItem) + TypedEdmEntityObject obj = resource as TypedEdmEntityObject; + if (obj != null) + { + if (obj.Instance is IDeltaSetItem) { return true; } + } - return (resource is EdmDeltaResourceObject || resource is EdmDeltaComplexObject); + if (resource is IDeltaSetItem) + { + return true; } + + return (resource is EdmDeltaResourceObject || resource is EdmDeltaComplexObject); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectHelper.cs index 0c3f1033a..051d22211 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmObjectHelper.cs @@ -9,60 +9,59 @@ using System.Diagnostics.Contracts; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +internal static class EdmObjectHelper { - internal static class EdmObjectHelper + public static IEdmObject ConvertToEdmObject(this IEnumerable enumerable, IEdmCollectionTypeReference collectionType) { - public static IEdmObject ConvertToEdmObject(this IEnumerable enumerable, IEdmCollectionTypeReference collectionType) + Contract.Assert(enumerable != null); + Contract.Assert(collectionType != null); + + if (enumerable is IEdmObject edmObject) { - Contract.Assert(enumerable != null); - Contract.Assert(collectionType != null); + return edmObject; + } - if (enumerable is IEdmObject edmObject) - { - return edmObject; - } + IEdmTypeReference elementType = collectionType.ElementType(); - IEdmTypeReference elementType = collectionType.ElementType(); + if (elementType.IsEntity()) + { + EdmEntityObjectCollection entityCollection = + new EdmEntityObjectCollection(collectionType); - if (elementType.IsEntity()) + foreach (EdmEntityObject entityObject in enumerable) { - EdmEntityObjectCollection entityCollection = - new EdmEntityObjectCollection(collectionType); - - foreach (EdmEntityObject entityObject in enumerable) - { - entityCollection.Add(entityObject); - } - - return entityCollection; + entityCollection.Add(entityObject); } - else if (elementType.IsComplex()) - { - EdmComplexObjectCollection complexCollection = - new EdmComplexObjectCollection(collectionType); - foreach (EdmComplexObject complexObject in enumerable) - { - complexCollection.Add(complexObject); - } + return entityCollection; + } + else if (elementType.IsComplex()) + { + EdmComplexObjectCollection complexCollection = + new EdmComplexObjectCollection(collectionType); - return complexCollection; - } - else if (elementType.IsEnum()) + foreach (EdmComplexObject complexObject in enumerable) { - EdmEnumObjectCollection enumCollection = - new EdmEnumObjectCollection(collectionType); + complexCollection.Add(complexObject); + } - foreach (EdmEnumObject enumObject in enumerable) - { - enumCollection.Add(enumObject); - } + return complexCollection; + } + else if (elementType.IsEnum()) + { + EdmEnumObjectCollection enumCollection = + new EdmEnumObjectCollection(collectionType); - return enumCollection; + foreach (EdmEnumObject enumObject in enumerable) + { + enumCollection.Add(enumObject); } - return null; + return enumCollection; } + + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmStructuredObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmStructuredObject.cs index 5cb471426..a573081c9 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmStructuredObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmStructuredObject.cs @@ -15,283 +15,282 @@ using Microsoft.AspNetCore.OData.Edm; using Microsoft.AspNetCore.OData.Deltas; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an with no backing CLR . +/// +[NonValidatingParameterBinding] +public abstract class EdmStructuredObject : Delta, IEdmStructuredObject, IEdmChangedObject { + private Dictionary _container = new Dictionary(); + private HashSet _setProperties = new HashSet(); + + private IEdmStructuredType _expectedEdmType; + private IEdmStructuredType _actualEdmType; + /// - /// Represents an with no backing CLR . + /// Initializes a new instance of the class. /// - [NonValidatingParameterBinding] - public abstract class EdmStructuredObject : Delta, IEdmStructuredObject, IEdmChangedObject + /// The of this object. + protected EdmStructuredObject(IEdmStructuredType edmType) + : this(edmType, isNullable: false) { - private Dictionary _container = new Dictionary(); - private HashSet _setProperties = new HashSet(); - - private IEdmStructuredType _expectedEdmType; - private IEdmStructuredType _actualEdmType; - - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - protected EdmStructuredObject(IEdmStructuredType edmType) - : this(edmType, isNullable: false) - { - } + } + + /// + /// Initializes a new instance of the class. + /// + /// The of this object. + [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "StructuredDefinition checks the nullable.")] + protected EdmStructuredObject(IEdmStructuredTypeReference edmType) + : this(edmType.StructuredDefinition(), edmType.IsNullable) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "StructuredDefinition checks the nullable.")] - protected EdmStructuredObject(IEdmStructuredTypeReference edmType) - : this(edmType.StructuredDefinition(), edmType.IsNullable) + /// + /// Initializes a new instance of the class. + /// + /// The of this object. + /// true if this object can be nullable; otherwise, false. + protected EdmStructuredObject(IEdmStructuredType edmType, bool isNullable) + { + if (edmType == null) { + throw Error.ArgumentNull("edmType"); } - /// - /// Initializes a new instance of the class. - /// - /// The of this object. - /// true if this object can be nullable; otherwise, false. - protected EdmStructuredObject(IEdmStructuredType edmType, bool isNullable) + _expectedEdmType = edmType; + _actualEdmType = edmType; + IsNullable = isNullable; + } + + /// + public override DeltaItemKind Kind => DeltaItemKind.Resource; + + /// + /// Gets or sets the expected of the entity or complex type of this object. + /// + public IEdmStructuredType ExpectedEdmType + { + get { return _expectedEdmType; } + set { - if (edmType == null) + if (value == null) { - throw Error.ArgumentNull("edmType"); + throw Error.PropertyNull(); + } + if (!_actualEdmType.IsOrInheritsFrom(value)) + { + throw Error.InvalidOperation(SRResources.DeltaEntityTypeNotAssignable, + _actualEdmType.ToTraceString(), value.ToTraceString()); } - _expectedEdmType = edmType; - _actualEdmType = edmType; - IsNullable = isNullable; + _expectedEdmType = value; } + } - /// - public override DeltaItemKind Kind => DeltaItemKind.Resource; - - /// - /// Gets or sets the expected of the entity or complex type of this object. - /// - public IEdmStructuredType ExpectedEdmType + /// + /// Gets or sets the actual of the entity or complex type of this object. + /// + public IEdmStructuredType ActualEdmType + { + get { return _actualEdmType; } + set { - get { return _expectedEdmType; } - set + if (value == null) { - if (value == null) - { - throw Error.PropertyNull(); - } - if (!_actualEdmType.IsOrInheritsFrom(value)) - { - throw Error.InvalidOperation(SRResources.DeltaEntityTypeNotAssignable, - _actualEdmType.ToTraceString(), value.ToTraceString()); - } - - _expectedEdmType = value; + throw Error.PropertyNull(); } - } - /// - /// Gets or sets the actual of the entity or complex type of this object. - /// - public IEdmStructuredType ActualEdmType - { - get { return _actualEdmType; } - set + if (!value.IsOrInheritsFrom(_expectedEdmType)) { - if (value == null) - { - throw Error.PropertyNull(); - } - - if (!value.IsOrInheritsFrom(_expectedEdmType)) - { - throw Error.InvalidOperation(SRResources.DeltaEntityTypeNotAssignable, - value.ToTraceString(), _expectedEdmType.ToTraceString()); - } - - _actualEdmType = value; + throw Error.InvalidOperation(SRResources.DeltaEntityTypeNotAssignable, + value.ToTraceString(), _expectedEdmType.ToTraceString()); } + + _actualEdmType = value; } + } + + /// + /// Gets or sets whether the EDM object is nullable or not. + /// + public bool IsNullable { get; set; } - /// - /// Gets or sets whether the EDM object is nullable or not. - /// - public bool IsNullable { get; set; } + /// + public override void Clear() + { + _container.Clear(); + _setProperties.Clear(); + } - /// - public override void Clear() + /// + public override bool TrySetPropertyValue(string name, object value) + { + IEdmProperty property = _actualEdmType.FindProperty(name); + if (property != null || _actualEdmType.IsOpen) { - _container.Clear(); - _setProperties.Clear(); + _setProperties.Add(name); + _container[name] = value; + return true; } - /// - public override bool TrySetPropertyValue(string name, object value) + return false; + } + + /// + public override bool TryGetPropertyValue(string name, out object value) + { + IEdmProperty property = _actualEdmType.FindProperty(name); + if (property != null || _actualEdmType.IsOpen) { - IEdmProperty property = _actualEdmType.FindProperty(name); - if (property != null || _actualEdmType.IsOpen) + if (_container.ContainsKey(name)) + { + value = _container[name]; + return true; + } + else { - _setProperties.Add(name); + value = GetDefaultValue(property.Type); + // store the default value (but don't update the list of 'set properties'). _container[name] = value; return true; } - + } + else + { + value = null; return false; } + } - /// - public override bool TryGetPropertyValue(string name, out object value) + /// + public override bool TryGetPropertyType(string name, out Type type) + { + IEdmProperty property = _actualEdmType.FindProperty(name); + if (property != null) { - IEdmProperty property = _actualEdmType.FindProperty(name); - if (property != null || _actualEdmType.IsOpen) - { - if (_container.ContainsKey(name)) - { - value = _container[name]; - return true; - } - else - { - value = GetDefaultValue(property.Type); - // store the default value (but don't update the list of 'set properties'). - _container[name] = value; - return true; - } - } - else - { - value = null; - return false; - } + type = GetClrTypeForUntypedDelta(property.Type); + return true; + } + else if (_actualEdmType.IsOpen && _container.ContainsKey(name)) + { + type = _container[name].GetType(); + return true; + } + else + { + type = null; + return false; } + } - /// - public override bool TryGetPropertyType(string name, out Type type) + /// + /// Get all dynamic properties + /// + public Dictionary TryGetDynamicProperties() + { + if (!_actualEdmType.IsOpen) { - IEdmProperty property = _actualEdmType.FindProperty(name); - if (property != null) - { - type = GetClrTypeForUntypedDelta(property.Type); - return true; - } - else if (_actualEdmType.IsOpen && _container.ContainsKey(name)) - { - type = _container[name].GetType(); - return true; - } - else - { - type = null; - return false; - } + return new Dictionary(); + } + else + { + return _container.Where(p => _actualEdmType.FindProperty(p.Key) == null).ToDictionary(property => property.Key, property => property.Value); } + } + + /// + public override IEnumerable GetChangedPropertyNames() + { + return _setProperties; + } + + /// + public override IDictionary GetDeltaNestedNavigationProperties() + { + return default; + } - /// - /// Get all dynamic properties - /// - public Dictionary TryGetDynamicProperties() + /// + public override IEnumerable GetUnchangedPropertyNames() + { + return _actualEdmType.Properties().Select(p => p.Name).Except(GetChangedPropertyNames()); + } + + /// + public IEdmTypeReference GetEdmType() + { + return _actualEdmType.ToEdmTypeReference(IsNullable); + } + + internal static object GetDefaultValue(IEdmTypeReference propertyType) + { + Contract.Assert(propertyType != null); + + bool isCollection = propertyType.IsCollection(); + if (!propertyType.IsNullable || isCollection) { - if (!_actualEdmType.IsOpen) + Type clrType = GetClrTypeForUntypedDelta(propertyType); + + if (propertyType.IsPrimitive() || + (isCollection && propertyType.AsCollection().ElementType().IsPrimitive())) { - return new Dictionary(); + // primitive or primitive collection + return Activator.CreateInstance(clrType); } else { - return _container.Where(p => _actualEdmType.FindProperty(p.Key) == null).ToDictionary(property => property.Key, property => property.Value); + // IEdmObject + return Activator.CreateInstance(clrType, propertyType); } } - /// - public override IEnumerable GetChangedPropertyNames() - { - return _setProperties; - } + return null; + } - /// - public override IDictionary GetDeltaNestedNavigationProperties() - { - return default; - } + internal static Type GetClrTypeForUntypedDelta(IEdmTypeReference edmType) + { + Contract.Assert(edmType != null); - /// - public override IEnumerable GetUnchangedPropertyNames() + switch (edmType.TypeKind()) { - return _actualEdmType.Properties().Select(p => p.Name).Except(GetChangedPropertyNames()); - } + case EdmTypeKind.Primitive: + return EdmCoreModel.Instance.GetClrType(edmType.AsPrimitive()); - /// - public IEdmTypeReference GetEdmType() - { - return _actualEdmType.ToEdmTypeReference(IsNullable); - } + case EdmTypeKind.Complex: + return typeof(EdmComplexObject); - internal static object GetDefaultValue(IEdmTypeReference propertyType) - { - Contract.Assert(propertyType != null); + case EdmTypeKind.Entity: + return typeof(EdmEntityObject); - bool isCollection = propertyType.IsCollection(); - if (!propertyType.IsNullable || isCollection) - { - Type clrType = GetClrTypeForUntypedDelta(propertyType); + case EdmTypeKind.Enum: + return typeof(EdmEnumObject); - if (propertyType.IsPrimitive() || - (isCollection && propertyType.AsCollection().ElementType().IsPrimitive())) + case EdmTypeKind.Collection: + IEdmTypeReference elementType = edmType.AsCollection().ElementType(); + if (elementType.IsPrimitive()) { - // primitive or primitive collection - return Activator.CreateInstance(clrType); + Type elementClrType = GetClrTypeForUntypedDelta(elementType); + return typeof(List<>).MakeGenericType(elementClrType); } - else + else if (elementType.IsComplex()) { - // IEdmObject - return Activator.CreateInstance(clrType, propertyType); + return typeof(EdmComplexObjectCollection); + } + else if (elementType.IsEntity()) + { + return typeof(EdmEntityObjectCollection); + } + else if (elementType.IsEnum()) + { + return typeof(EdmEnumObjectCollection); } - } - return null; + break; } - internal static Type GetClrTypeForUntypedDelta(IEdmTypeReference edmType) - { - Contract.Assert(edmType != null); - - switch (edmType.TypeKind()) - { - case EdmTypeKind.Primitive: - return EdmCoreModel.Instance.GetClrType(edmType.AsPrimitive()); - - case EdmTypeKind.Complex: - return typeof(EdmComplexObject); - - case EdmTypeKind.Entity: - return typeof(EdmEntityObject); - - case EdmTypeKind.Enum: - return typeof(EdmEnumObject); - - case EdmTypeKind.Collection: - IEdmTypeReference elementType = edmType.AsCollection().ElementType(); - if (elementType.IsPrimitive()) - { - Type elementClrType = GetClrTypeForUntypedDelta(elementType); - return typeof(List<>).MakeGenericType(elementClrType); - } - else if (elementType.IsComplex()) - { - return typeof(EdmComplexObjectCollection); - } - else if (elementType.IsEntity()) - { - return typeof(EdmEntityObjectCollection); - } - else if (elementType.IsEnum()) - { - return typeof(EdmEnumObjectCollection); - } - - break; - } - - throw Error.InvalidOperation(SRResources.UnsupportedEdmType, edmType.ToTraceString(), edmType.TypeKind()); - } + throw Error.InvalidOperation(SRResources.UnsupportedEdmType, edmType.ToTraceString(), edmType.TypeKind()); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmUntypedCollection.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmUntypedCollection.cs index 80981d658..2b80e0e5b 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmUntypedCollection.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmUntypedCollection.cs @@ -10,15 +10,14 @@ using Microsoft.AspNetCore.OData.Edm; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an that is a collection of untyped values. +/// +[NonValidatingParameterBinding] +public sealed class EdmUntypedCollection : List, IEdmObject { - /// - /// Represents an that is a collection of untyped values. - /// - [NonValidatingParameterBinding] - public sealed class EdmUntypedCollection : List, IEdmObject - { - /// - public IEdmTypeReference GetEdmType() => EdmUntypedHelpers.NullableUntypedCollectionReference; - } + /// + public IEdmTypeReference GetEdmType() => EdmUntypedHelpers.NullableUntypedCollectionReference; } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmUntypedObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmUntypedObject.cs index b807baf84..74a89436a 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmUntypedObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/EdmUntypedObject.cs @@ -9,20 +9,19 @@ using Microsoft.AspNetCore.OData.Abstracts; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an with no backing Edm type, or its Edm.Untyped. +/// +[NonValidatingParameterBinding] +public sealed class EdmUntypedObject : Dictionary, IEdmUntypedObject { - /// - /// Represents an with no backing Edm type, or its Edm.Untyped. - /// - [NonValidatingParameterBinding] - public sealed class EdmUntypedObject : Dictionary, IEdmUntypedObject - { - /// - public IEdmTypeReference GetEdmType() - => EdmUntypedStructuredTypeReference.NullableTypeReference; + /// + public IEdmTypeReference GetEdmType() + => EdmUntypedStructuredTypeReference.NullableTypeReference; - /// - public bool TryGetPropertyValue(string propertyName, out object value) - => TryGetValue(propertyName, out value); - } + /// + public bool TryGetPropertyValue(string propertyName, out object value) + => TryGetValue(propertyName, out value); } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmChangedObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmChangedObject.cs index 8e4ba869b..b8a5a2fd2 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmChangedObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmChangedObject.cs @@ -7,18 +7,17 @@ using Microsoft.AspNetCore.OData.Deltas; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an instance of an . +/// Base interface to be implemented by any Delta object required to be part of the DeltaResourceSet Payload. +/// +public interface IEdmChangedObject : IEdmObject { /// - /// Represents an instance of an . - /// Base interface to be implemented by any Delta object required to be part of the DeltaResourceSet Payload. + /// DeltaKind for the objects part of the DeltaResourceSet Payload. + /// Used to determine which Delta object to create during serialization. /// - public interface IEdmChangedObject : IEdmObject - { - /// - /// DeltaKind for the objects part of the DeltaResourceSet Payload. - /// Used to determine which Delta object to create during serialization. - /// - DeltaItemKind Kind { get; } - } + DeltaItemKind Kind { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmComplexObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmComplexObject.cs index 7b929aad7..6e347ac5f 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmComplexObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmComplexObject.cs @@ -7,12 +7,11 @@ using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an instance of an . +/// +public interface IEdmComplexObject : IEdmStructuredObject { - /// - /// Represents an instance of an . - /// - public interface IEdmComplexObject : IEdmStructuredObject - { - } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaDeletedLink.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaDeletedLink.cs index 1be9d1222..3eae515b1 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaDeletedLink.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaDeletedLink.cs @@ -5,13 +5,12 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an instance of an . +/// Holds the properties necessary to create the ODataDeltaDeletedLink. +/// +public interface IEdmDeltaDeletedLink : IEdmDeltaLinkBase, IEdmChangedObject { - /// - /// Represents an instance of an . - /// Holds the properties necessary to create the ODataDeltaDeletedLink. - /// - public interface IEdmDeltaDeletedLink : IEdmDeltaLinkBase, IEdmChangedObject - { - } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaDeletedResourceObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaDeletedResourceObject.cs index 6d4228fd4..b8c2bc6c0 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaDeletedResourceObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaDeletedResourceObject.cs @@ -8,22 +8,21 @@ using System; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an instance of an . +/// Holds the properties necessary to create the ODataDeletedResource. +/// +public interface IEdmDeltaDeletedResourceObject : IEdmChangedObject { /// - /// Represents an instance of an . - /// Holds the properties necessary to create the ODataDeletedResource. + /// The id of the deleted entity (same as the odata.id returned or computed when calling GET on resource), which may be absolute or relative. /// - public interface IEdmDeltaDeletedResourceObject : IEdmChangedObject - { - /// - /// The id of the deleted entity (same as the odata.id returned or computed when calling GET on resource), which may be absolute or relative. - /// - Uri Id { get; set; } + Uri Id { get; set; } - /// - /// Optional. Either deleted, if the entity was deleted (destroyed), or changed if the entity was removed from membership in the result (i.e., due to a data change). - /// - DeltaDeletedEntryReason? Reason { get; set; } - } + /// + /// Optional. Either deleted, if the entity was deleted (destroyed), or changed if the entity was removed from membership in the result (i.e., due to a data change). + /// + DeltaDeletedEntryReason? Reason { get; set; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaLink.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaLink.cs index 34ad7a94c..beff300e2 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaLink.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaLink.cs @@ -5,13 +5,12 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an instance of an . +/// Holds the properties necessary to create the ODataDeltaLink. +/// +public interface IEdmDeltaLink : IEdmDeltaLinkBase, IEdmChangedObject { - /// - /// Represents an instance of an . - /// Holds the properties necessary to create the ODataDeltaLink. - /// - public interface IEdmDeltaLink : IEdmDeltaLinkBase, IEdmChangedObject - { - } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaLinkBase.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaLinkBase.cs index c2e8fc602..e78e1418d 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaLinkBase.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmDeltaLinkBase.cs @@ -7,27 +7,26 @@ using System; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an instance of an . +/// Holds the properties necessary to create either ODataDeltaLink or ODataDeltaDeletedLink. +/// +public interface IEdmDeltaLinkBase : IEdmChangedObject { /// - /// Represents an instance of an . - /// Holds the properties necessary to create either ODataDeltaLink or ODataDeltaDeletedLink. + /// The Uri of the entity from which the relationship is defined, which may be absolute or relative. /// - public interface IEdmDeltaLinkBase : IEdmChangedObject - { - /// - /// The Uri of the entity from which the relationship is defined, which may be absolute or relative. - /// - Uri Source { get; set; } + Uri Source { get; set; } - /// - /// The Uri of the related entity, which may be absolute or relative. - /// - Uri Target { get; set; } + /// + /// The Uri of the related entity, which may be absolute or relative. + /// + Uri Target { get; set; } - /// - /// The name of the relationship property on the parent object. - /// - string Relationship { get; set; } - } + /// + /// The name of the relationship property on the parent object. + /// + string Relationship { get; set; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmEntityObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmEntityObject.cs index a089947d9..0880e31b0 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmEntityObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmEntityObject.cs @@ -7,12 +7,11 @@ using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an instance of an . +/// +public interface IEdmEntityObject : IEdmStructuredObject { - /// - /// Represents an instance of an . - /// - public interface IEdmEntityObject : IEdmStructuredObject - { - } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmEnumObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmEnumObject.cs index fea24ea9d..bea65d2fe 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmEnumObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmEnumObject.cs @@ -5,12 +5,11 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an instance of an enum value. +/// +public interface IEdmEnumObject : IEdmObject { - /// - /// Represents an instance of an enum value. - /// - public interface IEdmEnumObject : IEdmObject - { - } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmObject.cs index 20503395b..b86b6aec0 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmObject.cs @@ -7,17 +7,16 @@ using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an instance of an . +/// +public interface IEdmObject { /// - /// Represents an instance of an . + /// Gets the of this instance. /// - public interface IEdmObject - { - /// - /// Gets the of this instance. - /// - /// The of this instance. - IEdmTypeReference GetEdmType(); - } + /// The of this instance. + IEdmTypeReference GetEdmType(); } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmStructuredObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmStructuredObject.cs index 74aae4da7..e5d57d91c 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmStructuredObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmStructuredObject.cs @@ -7,20 +7,19 @@ using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an instance of an . +/// +public interface IEdmStructuredObject : IEdmObject { /// - /// Represents an instance of an . + /// Gets the value of the property with the given name. /// - public interface IEdmStructuredObject : IEdmObject - { - /// - /// Gets the value of the property with the given name. - /// - /// The name of the property to get. - /// When this method returns, contains the value of the property with the given name, if the property is found; - /// otherwise, null. The parameter is passed uninitialized. - /// true if the instance contains the property with the given name; otherwise, false. - bool TryGetPropertyValue(string propertyName, out object value); - } + /// The name of the property to get. + /// When this method returns, contains the value of the property with the given name, if the property is found; + /// otherwise, null. The parameter is passed uninitialized. + /// true if the instance contains the property with the given name; otherwise, false. + bool TryGetPropertyValue(string propertyName, out object value); } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmUntypedObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmUntypedObject.cs index a772bfcf9..67ebd4bed 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmUntypedObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/IEdmUntypedObject.cs @@ -5,13 +5,12 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an instance of an untyped structured object. +/// Untyped means this structured Edm type is "Edm.Untyped" or no Edm type. +/// +public interface IEdmUntypedObject : IEdmStructuredObject { - /// - /// Represents an instance of an untyped structured object. - /// Untyped means this structured Edm type is "Edm.Untyped" or no Edm type. - /// - public interface IEdmUntypedObject : IEdmStructuredObject - { - } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/NullEdmComplexObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/NullEdmComplexObject.cs index b931ede6e..1ce89429b 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/NullEdmComplexObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/NullEdmComplexObject.cs @@ -7,34 +7,33 @@ using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an that is null. +/// +public class NullEdmComplexObject : IEdmComplexObject { + private IEdmComplexTypeReference _edmType; + /// - /// Represents an that is null. + /// Initializes a new instance of the class. /// - public class NullEdmComplexObject : IEdmComplexObject + /// The EDM type of this object. + public NullEdmComplexObject(IEdmComplexTypeReference edmType) { - private IEdmComplexTypeReference _edmType; - - /// - /// Initializes a new instance of the class. - /// - /// The EDM type of this object. - public NullEdmComplexObject(IEdmComplexTypeReference edmType) - { - _edmType = edmType ?? throw Error.ArgumentNull(nameof(edmType)); - } + _edmType = edmType ?? throw Error.ArgumentNull(nameof(edmType)); + } - /// - public bool TryGetPropertyValue(string propertyName, out object value) - { - throw Error.InvalidOperation(SRResources.EdmComplexObjectNullRef, propertyName, _edmType.ToTraceString()); - } + /// + public bool TryGetPropertyValue(string propertyName, out object value) + { + throw Error.InvalidOperation(SRResources.EdmComplexObjectNullRef, propertyName, _edmType.ToTraceString()); + } - /// - public IEdmTypeReference GetEdmType() - { - return _edmType; - } + /// + public IEdmTypeReference GetEdmType() + { + return _edmType; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmComplexObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmComplexObject.cs index b615b9e67..2a1a1a970 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmComplexObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmComplexObject.cs @@ -7,22 +7,21 @@ using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an backed by a CLR object with a one-to-one mapping. +/// +internal class TypedEdmComplexObject : TypedEdmStructuredObject, IEdmComplexObject { /// - /// Represents an backed by a CLR object with a one-to-one mapping. + /// Initializes a new instance of the class. /// - internal class TypedEdmComplexObject : TypedEdmStructuredObject, IEdmComplexObject + /// The backing CLR instance. + /// The of this object. + /// The . + public TypedEdmComplexObject(object instance, IEdmComplexTypeReference edmType, IEdmModel edmModel) + : base(instance, edmType, edmModel) { - /// - /// Initializes a new instance of the class. - /// - /// The backing CLR instance. - /// The of this object. - /// The . - public TypedEdmComplexObject(object instance, IEdmComplexTypeReference edmType, IEdmModel edmModel) - : base(instance, edmType, edmModel) - { - } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmEntityObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmEntityObject.cs index 5a0644d17..140982b7a 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmEntityObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmEntityObject.cs @@ -7,22 +7,21 @@ using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an backed by a CLR object with a one-to-one mapping. +/// +internal class TypedEdmEntityObject : TypedEdmStructuredObject, IEdmEntityObject { /// - /// Represents an backed by a CLR object with a one-to-one mapping. + /// Initializes a new instance of the class. /// - internal class TypedEdmEntityObject : TypedEdmStructuredObject, IEdmEntityObject + /// The backing CLR instance. + /// The of this object. + /// The . + public TypedEdmEntityObject(object instance, IEdmEntityTypeReference edmType, IEdmModel edmModel) + : base(instance, edmType, edmModel) { - /// - /// Initializes a new instance of the class. - /// - /// The backing CLR instance. - /// The of this object. - /// The . - public TypedEdmEntityObject(object instance, IEdmEntityTypeReference edmType, IEdmModel edmModel) - : base(instance, edmType, edmModel) - { - } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmStructuredObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmStructuredObject.cs index c86c3f5d3..25971e8d8 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmStructuredObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmStructuredObject.cs @@ -14,144 +14,143 @@ using Microsoft.AspNetCore.OData.Edm; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an backed by a CLR object with a one-to-one mapping. +/// +internal abstract class TypedEdmStructuredObject : IEdmStructuredObject { + private static readonly ConcurrentDictionary<(string, Type), Func> _propertyGetterCache = + new ConcurrentDictionary<(string, Type), Func>(new PropertyGetterCacheEqualityComparer()); + + private IEdmStructuredTypeReference _edmType; + private Type _type; + /// - /// Represents an backed by a CLR object with a one-to-one mapping. + /// Initializes a new instance of the class. /// - internal abstract class TypedEdmStructuredObject : IEdmStructuredObject + /// The backing CLR instance. + /// The of this object. + /// The . + protected TypedEdmStructuredObject(object instance, IEdmStructuredTypeReference edmType, IEdmModel edmModel) { - private static readonly ConcurrentDictionary<(string, Type), Func> _propertyGetterCache = - new ConcurrentDictionary<(string, Type), Func>(new PropertyGetterCacheEqualityComparer()); - - private IEdmStructuredTypeReference _edmType; - private Type _type; - - /// - /// Initializes a new instance of the class. - /// - /// The backing CLR instance. - /// The of this object. - /// The . - protected TypedEdmStructuredObject(object instance, IEdmStructuredTypeReference edmType, IEdmModel edmModel) - { - Contract.Assert(edmType != null); + Contract.Assert(edmType != null); - Instance = instance; - _edmType = edmType; - _type = instance == null ? null : instance.GetType(); - Model = edmModel; - } + Instance = instance; + _edmType = edmType; + _type = instance == null ? null : instance.GetType(); + Model = edmModel; + } - /// - /// Gets the backing CLR object. - /// - public object Instance { get; private set; } + /// + /// Gets the backing CLR object. + /// + public object Instance { get; private set; } - /// - /// Gets the EDM model. - /// - public IEdmModel Model { get; private set; } + /// + /// Gets the EDM model. + /// + public IEdmModel Model { get; private set; } - /// - public IEdmTypeReference GetEdmType() - { - return _edmType; - } + /// + public IEdmTypeReference GetEdmType() + { + return _edmType; + } - /// - public bool TryGetPropertyValue(string propertyName, out object value) + /// + public bool TryGetPropertyValue(string propertyName, out object value) + { + if (Instance == null) { - if (Instance == null) - { - value = null; - return false; - } + value = null; + return false; + } - Contract.Assert(_type != null); + Contract.Assert(_type != null); - Func getter = GetOrCreatePropertyGetter(_type, propertyName, _edmType, Model); - if (getter == null) - { - value = null; - return false; - } - else - { - value = getter(Instance); - return true; - } + Func getter = GetOrCreatePropertyGetter(_type, propertyName, _edmType, Model); + if (getter == null) + { + value = null; + return false; } - - internal static Func GetOrCreatePropertyGetter( - Type type, - string propertyName, - IEdmStructuredTypeReference edmType, - IEdmModel model) + else { - (string, Type) key = (propertyName, type); - Func getter; + value = getter(Instance); + return true; + } + } - if (!_propertyGetterCache.TryGetValue(key, out getter)) + internal static Func GetOrCreatePropertyGetter( + Type type, + string propertyName, + IEdmStructuredTypeReference edmType, + IEdmModel model) + { + (string, Type) key = (propertyName, type); + Func getter; + + if (!_propertyGetterCache.TryGetValue(key, out getter)) + { + IEdmProperty property = edmType.FindProperty(propertyName); + if (property != null && model != null) { - IEdmProperty property = edmType.FindProperty(propertyName); - if (property != null && model != null) - { - propertyName = model.GetClrPropertyName(property) ?? propertyName; - } - - getter = CreatePropertyGetter(type, propertyName); - _propertyGetterCache[key] = getter; + propertyName = model.GetClrPropertyName(property) ?? propertyName; } - return getter; + getter = CreatePropertyGetter(type, propertyName); + _propertyGetterCache[key] = getter; } - private static Func CreatePropertyGetter(Type type, string propertyName) + return getter; + } + + private static Func CreatePropertyGetter(Type type, string propertyName) + { + PropertyInfo property = type.GetProperty(propertyName); + + if (property == null) { - PropertyInfo property = type.GetProperty(propertyName); + return null; + } - if (property == null) - { - return null; - } + var helper = new PropertyHelper(property); - var helper = new PropertyHelper(property); + return helper.GetValue; + } +} - return helper.GetValue; - } +/// +/// A custom equality comparer for the property getter cache. +/// +internal class PropertyGetterCacheEqualityComparer : IEqualityComparer<(string, Type)> +{ + public bool Equals((string, Type) x, (string, Type) y) + { + return x.Item1 == y.Item1 && x.Item2 == y.Item2; } /// - /// A custom equality comparer for the property getter cache. + /// This method overrides the default GetHashCode() implementation + /// for a tuple of (string, Type) to provide a more effective hash code. /// - internal class PropertyGetterCacheEqualityComparer : IEqualityComparer<(string, Type)> + /// The tuple object to calculate a hash code for + /// The calculated hash code. + public int GetHashCode((string, Type) obj) { - public bool Equals((string, Type) x, (string, Type) y) + unchecked { - return x.Item1 == y.Item1 && x.Item2 == y.Item2; - } - - /// - /// This method overrides the default GetHashCode() implementation - /// for a tuple of (string, Type) to provide a more effective hash code. - /// - /// The tuple object to calculate a hash code for - /// The calculated hash code. - public int GetHashCode((string, Type) obj) - { - unchecked - { - // The choice of 19 as the initial prime number is arbitrary but common in most hash code implementations. - // Multyplying by a prime number helps to reduce the chance of collisions. - // The hashcode of each tuple element is combined with the calculated hash to create - // a more unique hash code for the tuple. - int hash = 19; - hash = hash * 23 + obj.Item1.GetHashCode(); - hash = hash * 23 + obj.Item2.GetHashCode(); - - return hash; - } + // The choice of 19 as the initial prime number is arbitrary but common in most hash code implementations. + // Multyplying by a prime number helps to reduce the chance of collisions. + // The hashcode of each tuple element is combined with the calculated hash to create + // a more unique hash code for the tuple. + int hash = 19; + hash = hash * 23 + obj.Item1.GetHashCode(); + hash = hash * 23 + obj.Item2.GetHashCode(); + + return hash; } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmUntypedObject.cs b/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmUntypedObject.cs index 9b6f1bdf1..898359a18 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmUntypedObject.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Value/TypedEdmUntypedObject.cs @@ -9,24 +9,23 @@ using Microsoft.AspNetCore.OData.Formatter.Serialization; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Formatter.Value +namespace Microsoft.AspNetCore.OData.Formatter.Value; + +/// +/// Represents an backed by a CLR object without Edm type. +/// +internal class TypedEdmUntypedObject : TypedEdmStructuredObject, IEdmUntypedObject { - /// - /// Represents an backed by a CLR object without Edm type. - /// - internal class TypedEdmUntypedObject : TypedEdmStructuredObject, IEdmUntypedObject + private ODataSerializerContext _context; + public TypedEdmUntypedObject(ODataSerializerContext context, object instance) + : base(instance, EdmUntypedStructuredTypeReference.NullableTypeReference, context?.Model) { - private ODataSerializerContext _context; - public TypedEdmUntypedObject(ODataSerializerContext context, object instance) - : base(instance, EdmUntypedStructuredTypeReference.NullableTypeReference, context?.Model) - { - _context = context; - } + _context = context; + } - public IDictionary GetProperties() - { - IUntypedResourceMapper mapper = _context.UntypedMapper; - return mapper.Map(Instance, _context); - } + public IDictionary GetProperties() + { + IUntypedResourceMapper mapper = _context.UntypedMapper; + return mapper.Map(Instance, _context); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaDeletedLinkWrapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaDeletedLinkWrapper.cs index 184b90fdf..238fdb2f2 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaDeletedLinkWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaDeletedLinkWrapper.cs @@ -7,25 +7,24 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Formatter.Wrapper; + +/// +/// Encapsulates an deleted link. +/// +public sealed class ODataDeltaDeletedLinkWrapper : ODataDeltaLinkBaseWrapper { /// - /// Encapsulates an deleted link. + /// Initializes a new instance of . /// - public sealed class ODataDeltaDeletedLinkWrapper : ODataDeltaLinkBaseWrapper + /// The wrapped deleted link. + public ODataDeltaDeletedLinkWrapper(ODataDeltaDeletedLink deltaDeletedLink) { - /// - /// Initializes a new instance of . - /// - /// The wrapped deleted link. - public ODataDeltaDeletedLinkWrapper(ODataDeltaDeletedLink deltaDeletedLink) - { - DeltaDeletedLink = deltaDeletedLink; - } - - /// - /// Gets the wrapped . - /// - public ODataDeltaDeletedLink DeltaDeletedLink { get; } + DeltaDeletedLink = deltaDeletedLink; } + + /// + /// Gets the wrapped . + /// + public ODataDeltaDeletedLink DeltaDeletedLink { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaLinkBaseWrapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaLinkBaseWrapper.cs index ba01d0d2d..f896d9b35 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaLinkBaseWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaLinkBaseWrapper.cs @@ -7,12 +7,11 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Formatter.Wrapper; + +/// +/// Encapsulates an +/// +public abstract class ODataDeltaLinkBaseWrapper : ODataItemWrapper { - /// - /// Encapsulates an - /// - public abstract class ODataDeltaLinkBaseWrapper : ODataItemWrapper - { - } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaLinkWrapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaLinkWrapper.cs index a2dde68e0..4a1bb1a47 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaLinkWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaLinkWrapper.cs @@ -7,25 +7,24 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Formatter.Wrapper; + +/// +/// Encapsulates an added link. +/// +public sealed class ODataDeltaLinkWrapper : ODataDeltaLinkBaseWrapper { /// - /// Encapsulates an added link. + /// Initializes a new instance of . /// - public sealed class ODataDeltaLinkWrapper : ODataDeltaLinkBaseWrapper + /// The wrapped added link item. + public ODataDeltaLinkWrapper(ODataDeltaLink deltaLink) { - /// - /// Initializes a new instance of . - /// - /// The wrapped added link item. - public ODataDeltaLinkWrapper(ODataDeltaLink deltaLink) - { - DeltaLink = deltaLink; - } - - /// - /// Gets the wrapped . - /// - public ODataDeltaLink DeltaLink { get; } + DeltaLink = deltaLink; } + + /// + /// Gets the wrapped . + /// + public ODataDeltaLink DeltaLink { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaResourceSetWrapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaResourceSetWrapper.cs index c33cf9e3f..457e77e3e 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaResourceSetWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataDeltaResourceSetWrapper.cs @@ -8,36 +8,35 @@ using System.Collections.Generic; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Formatter.Wrapper; + +/// +/// Encapsulates an . +/// +/// +/// The Delta resource set could have normal resource or the deleted resource. +/// The Delta resource set could have delta link. +/// +public sealed class ODataDeltaResourceSetWrapper : ODataResourceSetBaseWrapper { /// - /// Encapsulates an . + /// Initializes a new instance of . /// - /// - /// The Delta resource set could have normal resource or the deleted resource. - /// The Delta resource set could have delta link. - /// - public sealed class ODataDeltaResourceSetWrapper : ODataResourceSetBaseWrapper + /// The wrapped delta resource set item. + public ODataDeltaResourceSetWrapper(ODataDeltaResourceSet deltaResourceSet) { - /// - /// Initializes a new instance of . - /// - /// The wrapped delta resource set item. - public ODataDeltaResourceSetWrapper(ODataDeltaResourceSet deltaResourceSet) - { - DeltaResourceSet = deltaResourceSet; - DeltaItems = new List(); - } + DeltaResourceSet = deltaResourceSet; + DeltaItems = new List(); + } - /// - /// Gets the wrapped . - /// - public ODataDeltaResourceSet DeltaResourceSet { get; } + /// + /// Gets the wrapped . + /// + public ODataDeltaResourceSet DeltaResourceSet { get; } - /// - /// Gets the nested delta items (resource, or deleted resource, or deleted link, or added link). - /// Be noted: the order of the delta items matters. - /// - public IList DeltaItems { get; } - } + /// + /// Gets the nested delta items (resource, or deleted resource, or deleted link, or added link). + /// Be noted: the order of the delta items matters. + /// + public IList DeltaItems { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataEntityReferenceLinkWrapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataEntityReferenceLinkWrapper.cs index f758c331e..8ae4da102 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataEntityReferenceLinkWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataEntityReferenceLinkWrapper.cs @@ -7,25 +7,24 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Formatter.Wrapper; + +/// +/// Encapsulates an . +/// +public class ODataEntityReferenceLinkWrapper : ODataItemWrapper { /// - /// Encapsulates an . + /// Initializes a new instance of . /// - public class ODataEntityReferenceLinkWrapper : ODataItemWrapper + /// The wrapped entity reference item. + public ODataEntityReferenceLinkWrapper(ODataEntityReferenceLink link) { - /// - /// Initializes a new instance of . - /// - /// The wrapped entity reference item. - public ODataEntityReferenceLinkWrapper(ODataEntityReferenceLink link) - { - EntityReferenceLink = link; - } - - /// - /// Gets the wrapped . - /// - public ODataEntityReferenceLink EntityReferenceLink { get; } + EntityReferenceLink = link; } + + /// + /// Gets the wrapped . + /// + public ODataEntityReferenceLink EntityReferenceLink { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataItemWrapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataItemWrapper.cs index 864faabb0..1f99fee07 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataItemWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataItemWrapper.cs @@ -7,12 +7,11 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Formatter.Wrapper; + +/// +/// Base class for all classes that wrap an . +/// +public abstract class ODataItemWrapper { - /// - /// Base class for all classes that wrap an . - /// - public abstract class ODataItemWrapper - { - } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataNestedResourceInfoWrapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataNestedResourceInfoWrapper.cs index e559a8a59..7295e60e0 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataNestedResourceInfoWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataNestedResourceInfoWrapper.cs @@ -8,31 +8,30 @@ using System.Collections.Generic; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Formatter.Wrapper; + +/// +/// Encapsulates an and the list of nested items. +/// +public sealed class ODataNestedResourceInfoWrapper : ODataItemWrapper { /// - /// Encapsulates an and the list of nested items. + /// Initializes a new instance of . /// - public sealed class ODataNestedResourceInfoWrapper : ODataItemWrapper + /// The wrapped nested resource info item. + public ODataNestedResourceInfoWrapper(ODataNestedResourceInfo nestedInfo) { - /// - /// Initializes a new instance of . - /// - /// The wrapped nested resource info item. - public ODataNestedResourceInfoWrapper(ODataNestedResourceInfo nestedInfo) - { - NestedResourceInfo = nestedInfo; - NestedItems = new List(); - } + NestedResourceInfo = nestedInfo; + NestedItems = new List(); + } - /// - /// Gets the wrapped . - /// - public ODataNestedResourceInfo NestedResourceInfo { get; } + /// + /// Gets the wrapped . + /// + public ODataNestedResourceInfo NestedResourceInfo { get; } - /// - /// Gets the nested items that are part of this nested resource info. - /// - public IList NestedItems { get; } - } + /// + /// Gets the nested items that are part of this nested resource info. + /// + public IList NestedItems { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataPrimitiveWrapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataPrimitiveWrapper.cs index fbd371d6d..169649fd8 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataPrimitiveWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataPrimitiveWrapper.cs @@ -7,25 +7,24 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Formatter.Wrapper; + +/// +/// Encapsulates in an Untyped (declared or undeclared) collection. +/// +public class ODataPrimitiveWrapper : ODataItemWrapper { /// - /// Encapsulates in an Untyped (declared or undeclared) collection. + /// Initializes a new instance of . /// - public class ODataPrimitiveWrapper : ODataItemWrapper + /// The wrapped primitive value. + public ODataPrimitiveWrapper(ODataPrimitiveValue value) { - /// - /// Initializes a new instance of . - /// - /// The wrapped primitive value. - public ODataPrimitiveWrapper(ODataPrimitiveValue value) - { - Value = value ?? throw Error.ArgumentNull(nameof(value)); - } - - /// - /// Gets the wrapped . - /// - public ODataPrimitiveValue Value { get; } + Value = value ?? throw Error.ArgumentNull(nameof(value)); } + + /// + /// Gets the wrapped . + /// + public ODataPrimitiveValue Value { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataReaderExtensions.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataReaderExtensions.cs index c0fc09238..cc9a6346e 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataReaderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataReaderExtensions.cs @@ -10,358 +10,357 @@ using System.Threading.Tasks; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Formatter.Wrapper; + +/// +/// Extension methods for . +/// +public static class ODataReaderExtensions { /// - /// Extension methods for . + /// Reads a or object. /// - public static class ODataReaderExtensions + /// The OData reader to read from. + /// The read resource or resource set. + public static ODataItemWrapper ReadResourceOrResourceSet(this ODataReader reader) { - /// - /// Reads a or object. - /// - /// The OData reader to read from. - /// The read resource or resource set. - public static ODataItemWrapper ReadResourceOrResourceSet(this ODataReader reader) + if (reader == null) { - if (reader == null) - { - throw Error.ArgumentNull(nameof(reader)); - } - - ODataItemWrapper topLevelItem = null; - Stack itemsStack = new Stack(); + throw Error.ArgumentNull(nameof(reader)); + } - while (reader.Read()) - { - ReadODataItem(reader, itemsStack, ref topLevelItem); - } + ODataItemWrapper topLevelItem = null; + Stack itemsStack = new Stack(); - Contract.Assert(reader.State == ODataReaderState.Completed, "We should have consumed all of the input by now."); - Contract.Assert(topLevelItem != null, "A top level resource or resource set should have been read by now."); - return topLevelItem; + while (reader.Read()) + { + ReadODataItem(reader, itemsStack, ref topLevelItem); } - /// - /// Reads a or object. - /// - /// The OData reader to read from. - /// The read resource or resource set. - public static async Task ReadResourceOrResourceSetAsync(this ODataReader reader) - { - if (reader == null) - { - throw Error.ArgumentNull(nameof(reader)); - } + Contract.Assert(reader.State == ODataReaderState.Completed, "We should have consumed all of the input by now."); + Contract.Assert(topLevelItem != null, "A top level resource or resource set should have been read by now."); + return topLevelItem; + } - ODataItemWrapper topLevelItem = null; - Stack itemsStack = new Stack(); + /// + /// Reads a or object. + /// + /// The OData reader to read from. + /// The read resource or resource set. + public static async Task ReadResourceOrResourceSetAsync(this ODataReader reader) + { + if (reader == null) + { + throw Error.ArgumentNull(nameof(reader)); + } - while (await reader.ReadAsync().ConfigureAwait(false)) - { - ReadODataItem(reader, itemsStack, ref topLevelItem); - } + ODataItemWrapper topLevelItem = null; + Stack itemsStack = new Stack(); - Contract.Assert(reader.State == ODataReaderState.Completed, "We should have consumed all of the input by now."); - Contract.Assert(topLevelItem != null, "A top level resource or resource set should have been read by now."); - return topLevelItem; + while (await reader.ReadAsync().ConfigureAwait(false)) + { + ReadODataItem(reader, itemsStack, ref topLevelItem); } - /// - /// Read OData item. - /// - /// The OData reader. - /// The item stack. - /// The top level item. - private static void ReadODataItem(ODataReader reader, Stack itemsStack, ref ODataItemWrapper topLevelItem) - { - Contract.Assert(reader != null); - Contract.Assert(itemsStack != null); + Contract.Assert(reader.State == ODataReaderState.Completed, "We should have consumed all of the input by now."); + Contract.Assert(topLevelItem != null, "A top level resource or resource set should have been read by now."); + return topLevelItem; + } - switch (reader.State) - { - case ODataReaderState.ResourceStart: - ReadResource(reader, itemsStack, ref topLevelItem); - break; - - case ODataReaderState.DeletedResourceStart: - ReadDeletedResource(reader, itemsStack); - break; - - case ODataReaderState.ResourceEnd: - Contract.Assert(itemsStack.Count > 0, "The resource which is ending should be on the top of the items stack."); - ODataResourceWrapper resourceWrapper = itemsStack.Peek() as ODataResourceWrapper; - if (resourceWrapper != null) - { - // Resource could be null - Contract.Assert(resourceWrapper.Resource == reader.Item, "The resource should be the same item in the reader."); - } - - itemsStack.Pop(); - break; - - case ODataReaderState.DeletedResourceEnd: - Contract.Assert(itemsStack.Count > 0, "The deleted resource which is ending should be on the top of the items stack."); - ODataResourceWrapper deletedResourceWrapper = itemsStack.Peek() as ODataResourceWrapper; - Contract.Assert(deletedResourceWrapper != null, "The top object in the stack should be delete resource wrapper."); - Contract.Assert(deletedResourceWrapper.Resource == reader.Item, "The deleted resource should be the same item in the reader."); - itemsStack.Pop(); - break; - - case ODataReaderState.NestedResourceInfoStart: - ODataNestedResourceInfo nestedResourceInfo = (ODataNestedResourceInfo)reader.Item; - Contract.Assert(nestedResourceInfo != null, "nested resource info should never be null."); - - ODataNestedResourceInfoWrapper nestedResourceInfoWrapper = new ODataNestedResourceInfoWrapper(nestedResourceInfo); - Contract.Assert(itemsStack.Count > 0, "nested resource info can't appear as top-level item."); - { - ODataResourceWrapper parentResource = (ODataResourceWrapper)itemsStack.Peek(); - parentResource.NestedResourceInfos.Add(nestedResourceInfoWrapper); - } - - itemsStack.Push(nestedResourceInfoWrapper); - break; - - case ODataReaderState.NestedResourceInfoEnd: - Contract.Assert(itemsStack.Count > 0, "The nested resource info which is ending should be on the top of the items stack."); - ODataNestedResourceInfoWrapper nestedInfoWrapper = itemsStack.Peek() as ODataNestedResourceInfoWrapper; - Contract.Assert(nestedInfoWrapper != null, "The top object in the stack should be nested resource info wrapper."); - Contract.Assert(nestedInfoWrapper.NestedResourceInfo == reader.Item, "The nested resource info should be the same item in the reader."); - itemsStack.Pop(); - break; - - case ODataReaderState.ResourceSetStart: // resource set - ReadResourceSet(reader, itemsStack, ref topLevelItem); - break; - - case ODataReaderState.DeltaResourceSetStart: // delta resource set - ReadDeltaResourceSet(reader, itemsStack, ref topLevelItem); - break; - - case ODataReaderState.ResourceSetEnd: - Contract.Assert(itemsStack.Count > 0, "The resource set which is ending should be on the top of the items stack."); - ODataResourceSetWrapper resourceSetWrapper = itemsStack.Peek() as ODataResourceSetWrapper; - Contract.Assert(resourceSetWrapper != null, "The top object in the stack should be resource set wrapper."); - Contract.Assert(resourceSetWrapper.ResourceSet == reader.Item, "The resource set should be the same item in the reader."); - itemsStack.Pop(); - break; - - case ODataReaderState.DeltaResourceSetEnd: - Contract.Assert(itemsStack.Count > 0, "The delta resource set which is ending should be on the top of the items stack."); - ODataDeltaResourceSetWrapper deltaResourceSetWrapper = itemsStack.Peek() as ODataDeltaResourceSetWrapper; - Contract.Assert(deltaResourceSetWrapper != null, "The top object in the stack should be delta resource set wrapper."); - Contract.Assert(deltaResourceSetWrapper.DeltaResourceSet == reader.Item, "The delta resource set should be the same item in the reader."); - itemsStack.Pop(); - break; - - case ODataReaderState.EntityReferenceLink: - ODataEntityReferenceLink entityReferenceLink = (ODataEntityReferenceLink)reader.Item; - Contract.Assert(entityReferenceLink != null, "Entity reference link should never be null."); - ODataEntityReferenceLinkWrapper entityReferenceLinkWrapper = new ODataEntityReferenceLinkWrapper(entityReferenceLink); - - Contract.Assert(itemsStack.Count > 0, "Entity reference link should never be reported as top-level item."); - { - ODataNestedResourceInfoWrapper parentNestedResource = (ODataNestedResourceInfoWrapper)itemsStack.Peek(); - parentNestedResource.NestedItems.Add(entityReferenceLinkWrapper); - } - - break; - - case ODataReaderState.DeltaLink: // added link - case ODataReaderState.DeltaDeletedLink: // deleted link - ODataDeltaLinkBaseWrapper linkBaseWrapper; - if (ODataReaderState.DeltaLink == reader.State) - { - ODataDeltaLink deltaLink = (ODataDeltaLink)reader.Item; - Contract.Assert(deltaLink != null, "Delta link should never be null."); - linkBaseWrapper = new ODataDeltaLinkWrapper(deltaLink); - } - else - { - ODataDeltaDeletedLink deltaDeletedLink = (ODataDeltaDeletedLink)reader.Item; - Contract.Assert(deltaDeletedLink != null, "Delta deleted link should never be null."); - linkBaseWrapper = new ODataDeltaDeletedLinkWrapper(deltaDeletedLink); - } - - Contract.Assert(itemsStack.Count > 0, "Delta link should never be reported as top-level item."); - // Should never add a delta link to a non-delta resource set. - ODataDeltaResourceSetWrapper linkResourceSetWrapper = (ODataDeltaResourceSetWrapper)itemsStack.Peek(); - Contract.Assert(linkResourceSetWrapper != null, "ODataDeltaResourceSetWrapper for delta link should not be null."); - linkResourceSetWrapper.DeltaItems.Add(linkBaseWrapper); - break; - - case ODataReaderState.Primitive: - Contract.Assert(itemsStack.Count > 0, "The primitive should be a non-null primitive value within an untyped collection."); - // Be noted: - // 1) if a 'null' value or a resource/object in the untyped collection goes to ODataResource flow - // 2) if a collection value in the untyped collection goes to ODataResourceSet flow - // 3) Since it's untyped, there's no logic for 'Enum' value, it means it's treated as primitive value. - ODataResourceSetWrapper resourceSetParentWrapper = (ODataResourceSetWrapper)itemsStack.Peek(); - resourceSetParentWrapper.Items.Add(new ODataPrimitiveWrapper((ODataPrimitiveValue)reader.Item)); - break; - - default: - Contract.Assert(false, "We should never get here, it means the ODataReader reported a wrong state."); - break; - } - } + /// + /// Read OData item. + /// + /// The OData reader. + /// The item stack. + /// The top level item. + private static void ReadODataItem(ODataReader reader, Stack itemsStack, ref ODataItemWrapper topLevelItem) + { + Contract.Assert(reader != null); + Contract.Assert(itemsStack != null); - /// - /// Read the normal resource. - /// - /// The OData reader. - /// The item stack. - /// the top level item. - private static void ReadResource(ODataReader reader, Stack itemsStack, ref ODataItemWrapper topLevelItem) + switch (reader.State) { - Contract.Assert(reader != null); - Contract.Assert(itemsStack != null); - Contract.Assert(ODataReaderState.ResourceStart == reader.State); + case ODataReaderState.ResourceStart: + ReadResource(reader, itemsStack, ref topLevelItem); + break; + + case ODataReaderState.DeletedResourceStart: + ReadDeletedResource(reader, itemsStack); + break; + + case ODataReaderState.ResourceEnd: + Contract.Assert(itemsStack.Count > 0, "The resource which is ending should be on the top of the items stack."); + ODataResourceWrapper resourceWrapper = itemsStack.Peek() as ODataResourceWrapper; + if (resourceWrapper != null) + { + // Resource could be null + Contract.Assert(resourceWrapper.Resource == reader.Item, "The resource should be the same item in the reader."); + } - ODataResource resource = (ODataResource)reader.Item; - ODataResourceWrapper resourceWrapper = null; - if (resource != null) - { - resourceWrapper = new ODataResourceWrapper(resource); - } + itemsStack.Pop(); + break; - if (itemsStack.Count == 0) - { - Contract.Assert(resource != null, "The top-level resource can never be null."); - topLevelItem = resourceWrapper; - } - else - { - ODataItemWrapper parentItem = itemsStack.Peek(); - ODataResourceSetWrapper parentResourceSet = parentItem as ODataResourceSetWrapper; - ODataDeltaResourceSetWrapper parentDeleteResourceSet = parentItem as ODataDeltaResourceSetWrapper; - if (parentResourceSet != null) + case ODataReaderState.DeletedResourceEnd: + Contract.Assert(itemsStack.Count > 0, "The deleted resource which is ending should be on the top of the items stack."); + ODataResourceWrapper deletedResourceWrapper = itemsStack.Peek() as ODataResourceWrapper; + Contract.Assert(deletedResourceWrapper != null, "The top object in the stack should be delete resource wrapper."); + Contract.Assert(deletedResourceWrapper.Resource == reader.Item, "The deleted resource should be the same item in the reader."); + itemsStack.Pop(); + break; + + case ODataReaderState.NestedResourceInfoStart: + ODataNestedResourceInfo nestedResourceInfo = (ODataNestedResourceInfo)reader.Item; + Contract.Assert(nestedResourceInfo != null, "nested resource info should never be null."); + + ODataNestedResourceInfoWrapper nestedResourceInfoWrapper = new ODataNestedResourceInfoWrapper(nestedResourceInfo); + Contract.Assert(itemsStack.Count > 0, "nested resource info can't appear as top-level item."); { - parentResourceSet.Resources.Add(resourceWrapper); - parentResourceSet.Items.Add(resourceWrapper);// in the next major release, we should only use 'Items'. + ODataResourceWrapper parentResource = (ODataResourceWrapper)itemsStack.Peek(); + parentResource.NestedResourceInfos.Add(nestedResourceInfoWrapper); } - else if (parentDeleteResourceSet != null) + + itemsStack.Push(nestedResourceInfoWrapper); + break; + + case ODataReaderState.NestedResourceInfoEnd: + Contract.Assert(itemsStack.Count > 0, "The nested resource info which is ending should be on the top of the items stack."); + ODataNestedResourceInfoWrapper nestedInfoWrapper = itemsStack.Peek() as ODataNestedResourceInfoWrapper; + Contract.Assert(nestedInfoWrapper != null, "The top object in the stack should be nested resource info wrapper."); + Contract.Assert(nestedInfoWrapper.NestedResourceInfo == reader.Item, "The nested resource info should be the same item in the reader."); + itemsStack.Pop(); + break; + + case ODataReaderState.ResourceSetStart: // resource set + ReadResourceSet(reader, itemsStack, ref topLevelItem); + break; + + case ODataReaderState.DeltaResourceSetStart: // delta resource set + ReadDeltaResourceSet(reader, itemsStack, ref topLevelItem); + break; + + case ODataReaderState.ResourceSetEnd: + Contract.Assert(itemsStack.Count > 0, "The resource set which is ending should be on the top of the items stack."); + ODataResourceSetWrapper resourceSetWrapper = itemsStack.Peek() as ODataResourceSetWrapper; + Contract.Assert(resourceSetWrapper != null, "The top object in the stack should be resource set wrapper."); + Contract.Assert(resourceSetWrapper.ResourceSet == reader.Item, "The resource set should be the same item in the reader."); + itemsStack.Pop(); + break; + + case ODataReaderState.DeltaResourceSetEnd: + Contract.Assert(itemsStack.Count > 0, "The delta resource set which is ending should be on the top of the items stack."); + ODataDeltaResourceSetWrapper deltaResourceSetWrapper = itemsStack.Peek() as ODataDeltaResourceSetWrapper; + Contract.Assert(deltaResourceSetWrapper != null, "The top object in the stack should be delta resource set wrapper."); + Contract.Assert(deltaResourceSetWrapper.DeltaResourceSet == reader.Item, "The delta resource set should be the same item in the reader."); + itemsStack.Pop(); + break; + + case ODataReaderState.EntityReferenceLink: + ODataEntityReferenceLink entityReferenceLink = (ODataEntityReferenceLink)reader.Item; + Contract.Assert(entityReferenceLink != null, "Entity reference link should never be null."); + ODataEntityReferenceLinkWrapper entityReferenceLinkWrapper = new ODataEntityReferenceLinkWrapper(entityReferenceLink); + + Contract.Assert(itemsStack.Count > 0, "Entity reference link should never be reported as top-level item."); + { + ODataNestedResourceInfoWrapper parentNestedResource = (ODataNestedResourceInfoWrapper)itemsStack.Peek(); + parentNestedResource.NestedItems.Add(entityReferenceLinkWrapper); + } + + break; + + case ODataReaderState.DeltaLink: // added link + case ODataReaderState.DeltaDeletedLink: // deleted link + ODataDeltaLinkBaseWrapper linkBaseWrapper; + if (ODataReaderState.DeltaLink == reader.State) { - // Delta resource set could have the normal resource - parentDeleteResourceSet.DeltaItems.Add(resourceWrapper); + ODataDeltaLink deltaLink = (ODataDeltaLink)reader.Item; + Contract.Assert(deltaLink != null, "Delta link should never be null."); + linkBaseWrapper = new ODataDeltaLinkWrapper(deltaLink); } else { - ODataNestedResourceInfoWrapper parentNestedResource = (ODataNestedResourceInfoWrapper)parentItem; - Contract.Assert(parentNestedResource.NestedResourceInfo.IsCollection == false, "Only singleton nested properties can contain resource as their child."); - Contract.Assert(parentNestedResource.NestedItems.Count == 0, "Each nested property can contain only one resource as its direct child."); - parentNestedResource.NestedItems.Add(resourceWrapper); + ODataDeltaDeletedLink deltaDeletedLink = (ODataDeltaDeletedLink)reader.Item; + Contract.Assert(deltaDeletedLink != null, "Delta deleted link should never be null."); + linkBaseWrapper = new ODataDeltaDeletedLinkWrapper(deltaDeletedLink); } - } - itemsStack.Push(resourceWrapper); + Contract.Assert(itemsStack.Count > 0, "Delta link should never be reported as top-level item."); + // Should never add a delta link to a non-delta resource set. + ODataDeltaResourceSetWrapper linkResourceSetWrapper = (ODataDeltaResourceSetWrapper)itemsStack.Peek(); + Contract.Assert(linkResourceSetWrapper != null, "ODataDeltaResourceSetWrapper for delta link should not be null."); + linkResourceSetWrapper.DeltaItems.Add(linkBaseWrapper); + break; + + case ODataReaderState.Primitive: + Contract.Assert(itemsStack.Count > 0, "The primitive should be a non-null primitive value within an untyped collection."); + // Be noted: + // 1) if a 'null' value or a resource/object in the untyped collection goes to ODataResource flow + // 2) if a collection value in the untyped collection goes to ODataResourceSet flow + // 3) Since it's untyped, there's no logic for 'Enum' value, it means it's treated as primitive value. + ODataResourceSetWrapper resourceSetParentWrapper = (ODataResourceSetWrapper)itemsStack.Peek(); + resourceSetParentWrapper.Items.Add(new ODataPrimitiveWrapper((ODataPrimitiveValue)reader.Item)); + break; + + default: + Contract.Assert(false, "We should never get here, it means the ODataReader reported a wrong state."); + break; } + } - /// - /// Read the deleted resource. - /// - /// The OData reader. - /// The item stack. - private static void ReadDeletedResource(ODataReader reader, Stack itemsStack) - { - Contract.Assert(reader != null); - Contract.Assert(itemsStack != null); - Contract.Assert(ODataReaderState.DeletedResourceStart == reader.State); - - ODataDeletedResource deletedResource = (ODataDeletedResource)reader.Item; - Contract.Assert(deletedResource != null, "Deleted resource should not be null"); - - ODataResourceWrapper deletedResourceWrapper = new ODataResourceWrapper(deletedResource); + /// + /// Read the normal resource. + /// + /// The OData reader. + /// The item stack. + /// the top level item. + private static void ReadResource(ODataReader reader, Stack itemsStack, ref ODataItemWrapper topLevelItem) + { + Contract.Assert(reader != null); + Contract.Assert(itemsStack != null); + Contract.Assert(ODataReaderState.ResourceStart == reader.State); - // top-level resource should never be deleted. - Contract.Assert(itemsStack.Count != 0, "Deleted Resource should not be top level item"); + ODataResource resource = (ODataResource)reader.Item; + ODataResourceWrapper resourceWrapper = null; + if (resource != null) + { + resourceWrapper = new ODataResourceWrapper(resource); + } + if (itemsStack.Count == 0) + { + Contract.Assert(resource != null, "The top-level resource can never be null."); + topLevelItem = resourceWrapper; + } + else + { ODataItemWrapper parentItem = itemsStack.Peek(); - ODataDeltaResourceSetWrapper parentDeletaResourceSet = parentItem as ODataDeltaResourceSetWrapper; - if (parentDeletaResourceSet != null) + ODataResourceSetWrapper parentResourceSet = parentItem as ODataResourceSetWrapper; + ODataDeltaResourceSetWrapper parentDeleteResourceSet = parentItem as ODataDeltaResourceSetWrapper; + if (parentResourceSet != null) { - parentDeletaResourceSet.DeltaItems.Add(deletedResourceWrapper); + parentResourceSet.Resources.Add(resourceWrapper); + parentResourceSet.Items.Add(resourceWrapper);// in the next major release, we should only use 'Items'. + } + else if (parentDeleteResourceSet != null) + { + // Delta resource set could have the normal resource + parentDeleteResourceSet.DeltaItems.Add(resourceWrapper); } else { - ODataNestedResourceInfoWrapper parentNestedResource = (ODataNestedResourceInfoWrapper)itemsStack.Peek(); + ODataNestedResourceInfoWrapper parentNestedResource = (ODataNestedResourceInfoWrapper)parentItem; Contract.Assert(parentNestedResource.NestedResourceInfo.IsCollection == false, "Only singleton nested properties can contain resource as their child."); - Contract.Assert(parentNestedResource.NestedItems.Count == 0, "Each nested property can contain only one deleted resource as its direct child."); - parentNestedResource.NestedItems.Add(deletedResourceWrapper); + Contract.Assert(parentNestedResource.NestedItems.Count == 0, "Each nested property can contain only one resource as its direct child."); + parentNestedResource.NestedItems.Add(resourceWrapper); } - - itemsStack.Push(deletedResourceWrapper); } - /// - /// Read the resource set. - /// - /// The OData reader. - /// The item stack. - /// The top level item. - private static void ReadResourceSet(ODataReader reader, Stack itemsStack, ref ODataItemWrapper topLevelItem) - { - Contract.Assert(reader != null); - Contract.Assert(itemsStack != null); - Contract.Assert(ODataReaderState.ResourceSetStart == reader.State); + itemsStack.Push(resourceWrapper); + } - ODataResourceSet resourceSet = (ODataResourceSet)reader.Item; - Contract.Assert(resourceSet != null, "ResourceSet should never be null."); + /// + /// Read the deleted resource. + /// + /// The OData reader. + /// The item stack. + private static void ReadDeletedResource(ODataReader reader, Stack itemsStack) + { + Contract.Assert(reader != null); + Contract.Assert(itemsStack != null); + Contract.Assert(ODataReaderState.DeletedResourceStart == reader.State); - ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(resourceSet); - if (itemsStack.Count > 0) - { - ODataItemWrapper peekedWrapper = itemsStack.Peek(); - if (peekedWrapper is ODataNestedResourceInfoWrapper parentNestedResourceInfo) - { - Contract.Assert(parentNestedResourceInfo.NestedResourceInfo.IsCollection == true, "Only collection nested properties can contain resource set as their child."); - Contract.Assert(parentNestedResourceInfo.NestedItems.Count == 0, "Each nested property can contain only one resource set as its direct child."); - parentNestedResourceInfo.NestedItems.Add(resourceSetWrapper); - } - else - { - ODataResourceSetWrapper parentResourceSet = (ODataResourceSetWrapper)peekedWrapper; - parentResourceSet.Items.Add(resourceSetWrapper); - } - } - else - { - topLevelItem = resourceSetWrapper; - } + ODataDeletedResource deletedResource = (ODataDeletedResource)reader.Item; + Contract.Assert(deletedResource != null, "Deleted resource should not be null"); - itemsStack.Push(resourceSetWrapper); - } + ODataResourceWrapper deletedResourceWrapper = new ODataResourceWrapper(deletedResource); - /// - /// Read the delta resource set. - /// - /// The OData reader. - /// The item stack. - /// The top level item. - private static void ReadDeltaResourceSet(ODataReader reader, Stack itemsStack, ref ODataItemWrapper topLevelItem) + // top-level resource should never be deleted. + Contract.Assert(itemsStack.Count != 0, "Deleted Resource should not be top level item"); + + ODataItemWrapper parentItem = itemsStack.Peek(); + ODataDeltaResourceSetWrapper parentDeletaResourceSet = parentItem as ODataDeltaResourceSetWrapper; + if (parentDeletaResourceSet != null) + { + parentDeletaResourceSet.DeltaItems.Add(deletedResourceWrapper); + } + else { - Contract.Assert(reader != null); - Contract.Assert(itemsStack != null); - Contract.Assert(ODataReaderState.DeltaResourceSetStart == reader.State); + ODataNestedResourceInfoWrapper parentNestedResource = (ODataNestedResourceInfoWrapper)itemsStack.Peek(); + Contract.Assert(parentNestedResource.NestedResourceInfo.IsCollection == false, "Only singleton nested properties can contain resource as their child."); + Contract.Assert(parentNestedResource.NestedItems.Count == 0, "Each nested property can contain only one deleted resource as its direct child."); + parentNestedResource.NestedItems.Add(deletedResourceWrapper); + } - ODataDeltaResourceSet deltaResourceSet = (ODataDeltaResourceSet)reader.Item; - Contract.Assert(deltaResourceSet != null, "Delta ResourceSet should never be null."); + itemsStack.Push(deletedResourceWrapper); + } - ODataDeltaResourceSetWrapper deltaResourceSetWrapper = new ODataDeltaResourceSetWrapper(deltaResourceSet); - if (itemsStack.Count > 0) + /// + /// Read the resource set. + /// + /// The OData reader. + /// The item stack. + /// The top level item. + private static void ReadResourceSet(ODataReader reader, Stack itemsStack, ref ODataItemWrapper topLevelItem) + { + Contract.Assert(reader != null); + Contract.Assert(itemsStack != null); + Contract.Assert(ODataReaderState.ResourceSetStart == reader.State); + + ODataResourceSet resourceSet = (ODataResourceSet)reader.Item; + Contract.Assert(resourceSet != null, "ResourceSet should never be null."); + + ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(resourceSet); + if (itemsStack.Count > 0) + { + ODataItemWrapper peekedWrapper = itemsStack.Peek(); + if (peekedWrapper is ODataNestedResourceInfoWrapper parentNestedResourceInfo) { - ODataNestedResourceInfoWrapper parentNestedResourceInfo = (ODataNestedResourceInfoWrapper)itemsStack.Peek(); - Contract.Assert(parentNestedResourceInfo != null, "this has to be an inner delta resource set. inner delta resource sets always have a nested resource info."); - Contract.Assert(parentNestedResourceInfo.NestedResourceInfo.IsCollection == true, "Only collection nested properties can contain delta resource set as their child."); - Contract.Assert(parentNestedResourceInfo.NestedItems.Count == 0, "Each nested property can contain only one delta resource set as its direct child."); - parentNestedResourceInfo.NestedItems.Add(deltaResourceSetWrapper); + Contract.Assert(parentNestedResourceInfo.NestedResourceInfo.IsCollection == true, "Only collection nested properties can contain resource set as their child."); + Contract.Assert(parentNestedResourceInfo.NestedItems.Count == 0, "Each nested property can contain only one resource set as its direct child."); + parentNestedResourceInfo.NestedItems.Add(resourceSetWrapper); } else { - topLevelItem = deltaResourceSetWrapper; + ODataResourceSetWrapper parentResourceSet = (ODataResourceSetWrapper)peekedWrapper; + parentResourceSet.Items.Add(resourceSetWrapper); } + } + else + { + topLevelItem = resourceSetWrapper; + } + + itemsStack.Push(resourceSetWrapper); + } + + /// + /// Read the delta resource set. + /// + /// The OData reader. + /// The item stack. + /// The top level item. + private static void ReadDeltaResourceSet(ODataReader reader, Stack itemsStack, ref ODataItemWrapper topLevelItem) + { + Contract.Assert(reader != null); + Contract.Assert(itemsStack != null); + Contract.Assert(ODataReaderState.DeltaResourceSetStart == reader.State); + + ODataDeltaResourceSet deltaResourceSet = (ODataDeltaResourceSet)reader.Item; + Contract.Assert(deltaResourceSet != null, "Delta ResourceSet should never be null."); - itemsStack.Push(deltaResourceSetWrapper); + ODataDeltaResourceSetWrapper deltaResourceSetWrapper = new ODataDeltaResourceSetWrapper(deltaResourceSet); + if (itemsStack.Count > 0) + { + ODataNestedResourceInfoWrapper parentNestedResourceInfo = (ODataNestedResourceInfoWrapper)itemsStack.Peek(); + Contract.Assert(parentNestedResourceInfo != null, "this has to be an inner delta resource set. inner delta resource sets always have a nested resource info."); + Contract.Assert(parentNestedResourceInfo.NestedResourceInfo.IsCollection == true, "Only collection nested properties can contain delta resource set as their child."); + Contract.Assert(parentNestedResourceInfo.NestedItems.Count == 0, "Each nested property can contain only one delta resource set as its direct child."); + parentNestedResourceInfo.NestedItems.Add(deltaResourceSetWrapper); } + else + { + topLevelItem = deltaResourceSetWrapper; + } + + itemsStack.Push(deltaResourceSetWrapper); } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceSetBaseWrapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceSetBaseWrapper.cs index 9ecb2d10f..190610190 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceSetBaseWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceSetBaseWrapper.cs @@ -7,12 +7,11 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Formatter.Wrapper; + +/// +/// Encapsulates an and the 's that are part of it. +/// +public abstract class ODataResourceSetBaseWrapper : ODataItemWrapper { - /// - /// Encapsulates an and the 's that are part of it. - /// - public abstract class ODataResourceSetBaseWrapper : ODataItemWrapper - { - } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceSetWrapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceSetWrapper.cs index 2fc4fd957..ffb14a1c1 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceSetWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceSetWrapper.cs @@ -8,42 +8,41 @@ using System.Collections.Generic; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Formatter.Wrapper; + +/// +/// Encapsulates an . +/// +public sealed class ODataResourceSetWrapper : ODataResourceSetBaseWrapper { /// - /// Encapsulates an . + /// Initializes a new instance of . /// - public sealed class ODataResourceSetWrapper : ODataResourceSetBaseWrapper + /// The wrapped resource set item. + public ODataResourceSetWrapper(ODataResourceSet resourceSet) { - /// - /// Initializes a new instance of . - /// - /// The wrapped resource set item. - public ODataResourceSetWrapper(ODataResourceSet resourceSet) - { - Resources = new List(); - Items = new List(); - ResourceSet = resourceSet; - } + Resources = new List(); + Items = new List(); + ResourceSet = resourceSet; + } - /// - /// Gets the wrapped . - /// - public ODataResourceSet ResourceSet { get; } + /// + /// Gets the wrapped . + /// + public ODataResourceSet ResourceSet { get; } - /// - /// Gets the nested resources of this ResourceSet. - /// Resource set only contains resources. - /// - public IList Resources { get; } + /// + /// Gets the nested resources of this ResourceSet. + /// Resource set only contains resources. + /// + public IList Resources { get; } - /// - /// Gets the nested items of this ResourceSet. - /// Since we have 'Resources' to contain ODataResource items in the collection (we have to keep it avoid breaking changes). - /// This list is used to hold other items also, for example a primitive or a collection, etc. - /// I assume the order of items does matter, so 'Items' contains all 'Resources' item to keep the same order. - /// In the next major release, we should combine 'Resources' and 'Items' together. - /// - public IList Items { get; } - } + /// + /// Gets the nested items of this ResourceSet. + /// Since we have 'Resources' to contain ODataResource items in the collection (we have to keep it avoid breaking changes). + /// This list is used to hold other items also, for example a primitive or a collection, etc. + /// I assume the order of items does matter, so 'Items' contains all 'Resources' item to keep the same order. + /// In the next major release, we should combine 'Resources' and 'Items' together. + /// + public IList Items { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceWrapper.cs b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceWrapper.cs index a0dc68b37..c60041123 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Wrapper/ODataResourceWrapper.cs @@ -8,39 +8,38 @@ using System.Collections.Generic; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Formatter.Wrapper; + +/// +/// Encapsulates an . +/// +public sealed class ODataResourceWrapper : ODataItemWrapper { /// - /// Encapsulates an . + /// Initializes a new instance of . /// - public sealed class ODataResourceWrapper : ODataItemWrapper + /// The wrapped resource item, it could be null. + public ODataResourceWrapper(ODataResourceBase resource) { - /// - /// Initializes a new instance of . - /// - /// The wrapped resource item, it could be null. - public ODataResourceWrapper(ODataResourceBase resource) - { - Resource = resource; - - IsDeletedResource = resource != null && resource is ODataDeletedResource; - - NestedResourceInfos = new List(); - } - - /// - /// Gets the wrapped . - /// - public ODataResourceBase Resource { get; } - - /// - /// Gets a boolean indicating whether the resource is deleted resource. - /// - public bool IsDeletedResource { get; } - - /// - /// Gets the inner nested resource infos. - /// - public IList NestedResourceInfos { get; } + Resource = resource; + + IsDeletedResource = resource != null && resource is ODataDeletedResource; + + NestedResourceInfos = new List(); } + + /// + /// Gets the wrapped . + /// + public ODataResourceBase Resource { get; } + + /// + /// Gets a boolean indicating whether the resource is deleted resource. + /// + public bool IsDeletedResource { get; } + + /// + /// Gets the inner nested resource infos. + /// + public IList NestedResourceInfos { get; } } diff --git a/src/Microsoft.AspNetCore.OData/ODataApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData/ODataApplicationBuilderExtensions.cs index 560c9f1be..dbef4aed9 100644 --- a/src/Microsoft.AspNetCore.OData/ODataApplicationBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/ODataApplicationBuilderExtensions.cs @@ -10,77 +10,76 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing; -namespace Microsoft.AspNetCore.OData +namespace Microsoft.AspNetCore.OData; + +/// +/// Provides extension methods for to add OData routes. +/// +public static class ODataApplicationBuilderExtensions { + private const string DefaultODataRouteDebugMiddlewareRoutePattern = "$odata"; + /// - /// Provides extension methods for to add OData routes. + /// Use OData batching middleware. /// - public static class ODataApplicationBuilderExtensions + /// The to use. + /// The . + public static IApplicationBuilder UseODataBatching(this IApplicationBuilder app) { - private const string DefaultODataRouteDebugMiddlewareRoutePattern = "$odata"; - - /// - /// Use OData batching middleware. - /// - /// The to use. - /// The . - public static IApplicationBuilder UseODataBatching(this IApplicationBuilder app) + if (app == null) { - if (app == null) - { - throw Error.ArgumentNull(nameof(app)); - } - - return app.UseMiddleware(); + throw Error.ArgumentNull(nameof(app)); } - /// - /// Use OData query request middleware. An OData query request is a Http Post request ending with /$query. - /// The Request body contains the query options. - /// - /// The to use. - /// The . - public static IApplicationBuilder UseODataQueryRequest(this IApplicationBuilder app) - { - if (app == null) - { - throw Error.ArgumentNull(nameof(app)); - } + return app.UseMiddleware(); + } - return app.UseMiddleware(); + /// + /// Use OData query request middleware. An OData query request is a Http Post request ending with /$query. + /// The Request body contains the query options. + /// + /// The to use. + /// The . + public static IApplicationBuilder UseODataQueryRequest(this IApplicationBuilder app) + { + if (app == null) + { + throw Error.ArgumentNull(nameof(app)); } - /// - /// Use OData route debug middleware. You can send request "~/$odata" after enabling this middleware. - /// - /// The to use. - /// The . - public static IApplicationBuilder UseODataRouteDebug(this IApplicationBuilder app) + return app.UseMiddleware(); + } + + /// + /// Use OData route debug middleware. You can send request "~/$odata" after enabling this middleware. + /// + /// The to use. + /// The . + public static IApplicationBuilder UseODataRouteDebug(this IApplicationBuilder app) + { + return app.UseODataRouteDebug(DefaultODataRouteDebugMiddlewareRoutePattern); + } + + /// + /// Use OData route debug middleware using the given route pattern. + /// For example, if the given route pattern is "myrouteinfo", then you can send request "~/myrouteinfo" after enabling this middleware. + /// Please use basic (literal) route pattern. + /// + /// The to use. + /// The given route pattern. + /// The . + public static IApplicationBuilder UseODataRouteDebug(this IApplicationBuilder app, string routePattern) + { + if (app == null) { - return app.UseODataRouteDebug(DefaultODataRouteDebugMiddlewareRoutePattern); + throw Error.ArgumentNull(nameof(app)); } - /// - /// Use OData route debug middleware using the given route pattern. - /// For example, if the given route pattern is "myrouteinfo", then you can send request "~/myrouteinfo" after enabling this middleware. - /// Please use basic (literal) route pattern. - /// - /// The to use. - /// The given route pattern. - /// The . - public static IApplicationBuilder UseODataRouteDebug(this IApplicationBuilder app, string routePattern) + if (routePattern == null) { - if (app == null) - { - throw Error.ArgumentNull(nameof(app)); - } - - if (routePattern == null) - { - throw Error.ArgumentNull(nameof(routePattern)); - } - - return app.UseMiddleware(routePattern); + throw Error.ArgumentNull(nameof(routePattern)); } + + return app.UseMiddleware(routePattern); } } diff --git a/src/Microsoft.AspNetCore.OData/ODataJsonOptionsSetup.cs b/src/Microsoft.AspNetCore.OData/ODataJsonOptionsSetup.cs index 99dedef30..301759c59 100644 --- a/src/Microsoft.AspNetCore.OData/ODataJsonOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.OData/ODataJsonOptionsSetup.cs @@ -10,28 +10,27 @@ using Microsoft.AspNetCore.OData.Results; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.OData +namespace Microsoft.AspNetCore.OData; + +/// +/// Sets up default OData options for . +/// +public class ODataJsonOptionsSetup : IConfigureOptions { /// - /// Sets up default OData options for . + /// Configure the default /// - public class ODataJsonOptionsSetup : IConfigureOptions + /// The Json Options. + public void Configure(JsonOptions options) { - /// - /// Configure the default - /// - /// The Json Options. - public void Configure(JsonOptions options) + if (options == null) { - if (options == null) - { - throw Error.ArgumentNull(nameof(options)); - } - - options.JsonSerializerOptions.Converters.Add(new SelectExpandWrapperConverter()); - options.JsonSerializerOptions.Converters.Add(new PageResultValueConverter()); - options.JsonSerializerOptions.Converters.Add(new DynamicTypeWrapperConverter()); - options.JsonSerializerOptions.Converters.Add(new SingleResultValueConverter()); + throw Error.ArgumentNull(nameof(options)); } + + options.JsonSerializerOptions.Converters.Add(new SelectExpandWrapperConverter()); + options.JsonSerializerOptions.Converters.Add(new PageResultValueConverter()); + options.JsonSerializerOptions.Converters.Add(new DynamicTypeWrapperConverter()); + options.JsonSerializerOptions.Converters.Add(new SingleResultValueConverter()); } } diff --git a/src/Microsoft.AspNetCore.OData/ODataMvcBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData/ODataMvcBuilderExtensions.cs index 9f3ad095a..a4960fba8 100644 --- a/src/Microsoft.AspNetCore.OData/ODataMvcBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/ODataMvcBuilderExtensions.cs @@ -8,73 +8,72 @@ using System; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.OData +namespace Microsoft.AspNetCore.OData; + +/// +/// Provides extension methods to add OData services based on . +/// +public static class ODataMvcBuilderExtensions { /// - /// Provides extension methods to add OData services based on . + /// Adds essential OData services to the specified . + /// + /// The to add services to. + /// A that can be used to further configure the OData services. + public static IMvcBuilder AddOData(this IMvcBuilder builder) + { + return builder.AddOData(opt => { }); + } + + /// + /// Adds essential OData services to the specified . /// - public static class ODataMvcBuilderExtensions + /// The to add services to. + /// The OData options to configure the services with, + /// including access to a service provider which you can resolve services from. + /// A that can be used to further configure the OData services. + public static IMvcBuilder AddOData(this IMvcBuilder builder, Action setupAction) { - /// - /// Adds essential OData services to the specified . - /// - /// The to add services to. - /// A that can be used to further configure the OData services. - public static IMvcBuilder AddOData(this IMvcBuilder builder) + if (builder == null) { - return builder.AddOData(opt => { }); + throw Error.ArgumentNull(nameof(builder)); } - /// - /// Adds essential OData services to the specified . - /// - /// The to add services to. - /// The OData options to configure the services with, - /// including access to a service provider which you can resolve services from. - /// A that can be used to further configure the OData services. - public static IMvcBuilder AddOData(this IMvcBuilder builder, Action setupAction) + if (setupAction == null) { - if (builder == null) - { - throw Error.ArgumentNull(nameof(builder)); - } + throw Error.ArgumentNull(nameof(setupAction)); + } - if (setupAction == null) - { - throw Error.ArgumentNull(nameof(setupAction)); - } + builder.Services.AddODataCore(); - builder.Services.AddODataCore(); + builder.Services.Configure(setupAction); - builder.Services.Configure(setupAction); + return builder; + } - return builder; + /// + /// Adds essential OData services to the specified . + /// + /// The to add services to. + /// The OData options to configure the services with, + /// including access to a service provider which you can resolve services from. + /// A that can be used to further configure the OData services. + public static IMvcBuilder AddOData(this IMvcBuilder builder, Action setupAction) + { + if (builder == null) + { + throw Error.ArgumentNull(nameof(builder)); } - /// - /// Adds essential OData services to the specified . - /// - /// The to add services to. - /// The OData options to configure the services with, - /// including access to a service provider which you can resolve services from. - /// A that can be used to further configure the OData services. - public static IMvcBuilder AddOData(this IMvcBuilder builder, Action setupAction) + if (setupAction == null) { - if (builder == null) - { - throw Error.ArgumentNull(nameof(builder)); - } - - if (setupAction == null) - { - throw Error.ArgumentNull(nameof(setupAction)); - } + throw Error.ArgumentNull(nameof(setupAction)); + } - builder.Services.AddODataCore(); + builder.Services.AddODataCore(); - builder.Services.AddOptions().Configure(setupAction); + builder.Services.AddOptions().Configure(setupAction); - return builder; - } + return builder; } } diff --git a/src/Microsoft.AspNetCore.OData/ODataMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData/ODataMvcCoreBuilderExtensions.cs index b0eacf9dc..733c0133f 100644 --- a/src/Microsoft.AspNetCore.OData/ODataMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/ODataMvcCoreBuilderExtensions.cs @@ -8,70 +8,69 @@ using System; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.OData +namespace Microsoft.AspNetCore.OData; + +/// +/// Provides extension methods to add OData services based on . +/// +public static class ODataMvcCoreBuilderExtensions { /// - /// Provides extension methods to add OData services based on . + /// Adds essential OData services to the specified . + /// + /// The to add services to. + /// A that can be used to further configure the OData services. + public static IMvcCoreBuilder AddOData(this IMvcCoreBuilder builder) + { + return builder.AddOData(opt => { }); + } + + /// + /// Adds essential OData services to the specified . /// - public static class ODataMvcCoreBuilderExtensions + /// The to add services to. + /// The OData options to configure the services with, + /// including access to a service provider which you can resolve services from. + /// A that can be used to further configure the OData services. + public static IMvcCoreBuilder AddOData(this IMvcCoreBuilder builder, Action setupAction) { - /// - /// Adds essential OData services to the specified . - /// - /// The to add services to. - /// A that can be used to further configure the OData services. - public static IMvcCoreBuilder AddOData(this IMvcCoreBuilder builder) + if (builder == null) { - return builder.AddOData(opt => { }); + throw Error.ArgumentNull(nameof(builder)); } - /// - /// Adds essential OData services to the specified . - /// - /// The to add services to. - /// The OData options to configure the services with, - /// including access to a service provider which you can resolve services from. - /// A that can be used to further configure the OData services. - public static IMvcCoreBuilder AddOData(this IMvcCoreBuilder builder, Action setupAction) + if (setupAction == null) { - if (builder == null) - { - throw Error.ArgumentNull(nameof(builder)); - } + throw Error.ArgumentNull(nameof(setupAction)); + } - if (setupAction == null) - { - throw Error.ArgumentNull(nameof(setupAction)); - } + builder.Services.AddODataCore(); + builder.Services.Configure(setupAction); + return builder; + } - builder.Services.AddODataCore(); - builder.Services.Configure(setupAction); - return builder; + /// + /// Adds essential OData services to the specified . + /// + /// The to add services to. + /// The OData options to configure the services with, + /// including access to a service provider which you can resolve services from. + /// A that can be used to further configure the OData services. + public static IMvcCoreBuilder AddOData(this IMvcCoreBuilder builder, Action setupAction) + { + if (builder == null) + { + throw Error.ArgumentNull(nameof(builder)); } - /// - /// Adds essential OData services to the specified . - /// - /// The to add services to. - /// The OData options to configure the services with, - /// including access to a service provider which you can resolve services from. - /// A that can be used to further configure the OData services. - public static IMvcCoreBuilder AddOData(this IMvcCoreBuilder builder, Action setupAction) + if (setupAction == null) { - if (builder == null) - { - throw Error.ArgumentNull(nameof(builder)); - } - - if (setupAction == null) - { - throw Error.ArgumentNull(nameof(setupAction)); - } + throw Error.ArgumentNull(nameof(setupAction)); + } - builder.Services.AddODataCore(); - builder.Services.AddOptions().Configure(setupAction); + builder.Services.AddODataCore(); + builder.Services.AddOptions().Configure(setupAction); - return builder; - } + return builder; } } diff --git a/src/Microsoft.AspNetCore.OData/ODataMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.OData/ODataMvcOptionsSetup.cs index 939d82a68..0a7f907b0 100644 --- a/src/Microsoft.AspNetCore.OData/ODataMvcOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.OData/ODataMvcOptionsSetup.cs @@ -10,35 +10,34 @@ using Microsoft.AspNetCore.OData.Formatter; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.OData +namespace Microsoft.AspNetCore.OData; + +/// +/// Sets up default OData options for . +/// +public class ODataMvcOptionsSetup : IConfigureOptions { /// - /// Sets up default OData options for . + /// Configure the default /// - public class ODataMvcOptionsSetup : IConfigureOptions + /// The to configure. + public void Configure(MvcOptions options) { - /// - /// Configure the default - /// - /// The to configure. - public void Configure(MvcOptions options) + if (options == null) { - if (options == null) - { - throw Error.ArgumentNull(nameof(options)); - } + throw Error.ArgumentNull(nameof(options)); + } - // Read formatters - foreach (ODataInputFormatter inputFormatter in ODataInputFormatterFactory.Create().Reverse()) - { - options.InputFormatters.Insert(0, inputFormatter); - } + // Read formatters + foreach (ODataInputFormatter inputFormatter in ODataInputFormatterFactory.Create().Reverse()) + { + options.InputFormatters.Insert(0, inputFormatter); + } - // Write formatters - foreach (ODataOutputFormatter outputFormatter in ODataOutputFormatterFactory.Create().Reverse()) - { - options.OutputFormatters.Insert(0, outputFormatter); - } + // Write formatters + foreach (ODataOutputFormatter outputFormatter in ODataOutputFormatterFactory.Create().Reverse()) + { + options.OutputFormatters.Insert(0, outputFormatter); } } } diff --git a/src/Microsoft.AspNetCore.OData/ODataOptions.cs b/src/Microsoft.AspNetCore.OData/ODataOptions.cs index 451940273..15c692e07 100644 --- a/src/Microsoft.AspNetCore.OData/ODataOptions.cs +++ b/src/Microsoft.AspNetCore.OData/ODataOptions.cs @@ -20,342 +20,341 @@ using Microsoft.OData.ModelBuilder.Config; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData +namespace Microsoft.AspNetCore.OData; + +/// +/// Contains the detail configurations of a given OData request. +/// +/// Caution: The properties in this class should not be . +public class ODataOptions { + #region Settings /// - /// Contains the detail configurations of a given OData request. + /// Gets or sets the to use while parsing, specifically + /// whether to recognize keys as segments or not. + /// By default, it supports key as segment only if the key is single key. /// - /// Caution: The properties in this class should not be . - public class ODataOptions - { - #region Settings - /// - /// Gets or sets the to use while parsing, specifically - /// whether to recognize keys as segments or not. - /// By default, it supports key as segment only if the key is single key. - /// - public ODataUrlKeyDelimiter UrlKeyDelimiter { get; set; } = ODataUrlKeyDelimiter.Slash; - - /// - /// Gets or sets a value indicating if batch requests should continue on error. - /// By default, it's false. - /// - public bool EnableContinueOnErrorHeader { get; set; } - - /// - /// Gets or sets a value indicating if attribute routing is enabled or not. - /// Defaults to true. - /// - public bool EnableAttributeRouting { get; set; } = true; - - /// - /// Gets or sets a TimeZoneInfo for the serialization and deserialization. - /// - public TimeZoneInfo TimeZone { get; set; } = TimeZoneInfo.Local; - - /// - /// Gets the routing conventions. - /// - public IList Conventions { get; } = new List(); - - /// - /// Gets the instance responsible for configuring the route template. - /// - public ODataRouteOptions RouteOptions { get; } = new ODataRouteOptions(); - - #endregion - - #region RouteComponents - - /// - /// Contains the OData instances and dependency injection containers for specific routes. - /// - /// DO NOT modify this dictionary yourself. Instead, use the 'AddRouteComponents()` methods for registering model instances. - public IDictionary RouteComponents { get; } = new Dictionary(); - - /// - /// Adds an to the default route. - /// - /// The to add. - /// The current instance to enable fluent configuration. - public ODataOptions AddRouteComponents(IEdmModel model) - { - return AddRouteComponents(string.Empty, model, configureServices: null); - } + public ODataUrlKeyDelimiter UrlKeyDelimiter { get; set; } = ODataUrlKeyDelimiter.Slash; - /// - /// Adds an , as well as the given , to the default route. - /// - /// The to add. - /// The batch handler to add. - /// The current instance to enable fluent configuration. - public ODataOptions AddRouteComponents(IEdmModel model, ODataBatchHandler batchHandler) - { - return AddRouteComponents(string.Empty, model, services => services.AddSingleton(sp => batchHandler)); - } + /// + /// Gets or sets a value indicating if batch requests should continue on error. + /// By default, it's false. + /// + public bool EnableContinueOnErrorHeader { get; set; } - /// - /// Adds an to the specified route. - /// - /// The model related prefix. It could be null which means there's no prefix when access this model. - /// The to add. - /// The current instance to enable fluent configuration. - public ODataOptions AddRouteComponents(string routePrefix, IEdmModel model) - { - return AddRouteComponents(routePrefix, model, configureServices: null); - } + /// + /// Gets or sets a value indicating if attribute routing is enabled or not. + /// Defaults to true. + /// + public bool EnableAttributeRouting { get; set; } = true; - /// - /// Adds an , as well as the given , to the specified route. - /// - /// The model related prefix. It could be null which means there's no prefix when access this model. - /// The to add. - /// The $batch handler . - /// The current instance to enable fluent configuration. - public ODataOptions AddRouteComponents(string routePrefix, IEdmModel model, ODataBatchHandler batchHandler) - { - return AddRouteComponents(routePrefix, model, services => services.AddSingleton(sp => batchHandler)); - } + /// + /// Gets or sets a TimeZoneInfo for the serialization and deserialization. + /// + public TimeZoneInfo TimeZone { get; set; } = TimeZoneInfo.Local; - /// - /// Adds an using the service configuration. - /// - /// The model related prefix. - /// The to add. - /// The sub service configuration action. - /// The current instance to enable fluent configuration. - public ODataOptions AddRouteComponents(string routePrefix, IEdmModel model, Action configureServices) - { - return AddRouteComponents(routePrefix, model, ODataVersion.V4, configureServices); - } + /// + /// Gets the routing conventions. + /// + public IList Conventions { get; } = new List(); - /// - /// Adds an using the service configuration. - /// - /// The model related prefix. - /// The to add. - /// The OData version to be used. - /// The sub service configuration action. - /// The current instance to enable fluent configuration. - public ODataOptions AddRouteComponents(string routePrefix, IEdmModel model, ODataVersion version, Action configureServices) - { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + /// + /// Gets the instance responsible for configuring the route template. + /// + public ODataRouteOptions RouteOptions { get; } = new ODataRouteOptions(); - if (routePrefix == null) - { - throw Error.ArgumentNull(nameof(routePrefix)); - } + #endregion - string sanitizedRoutePrefix = SanitizeRoutePrefix(routePrefix); + #region RouteComponents - if (RouteComponents.ContainsKey(sanitizedRoutePrefix)) - { - throw Error.InvalidOperation(SRResources.ModelPrefixAlreadyUsed, sanitizedRoutePrefix); - } + /// + /// Contains the OData instances and dependency injection containers for specific routes. + /// + /// DO NOT modify this dictionary yourself. Instead, use the 'AddRouteComponents()` methods for registering model instances. + public IDictionary RouteComponents { get; } = new Dictionary(); - // Consider to use Lazy ? - IServiceProvider serviceProvider = BuildRouteContainer(model, version, configureServices); - RouteComponents[sanitizedRoutePrefix] = (model, serviceProvider); - return this; - } + /// + /// Adds an to the default route. + /// + /// The to add. + /// The current instance to enable fluent configuration. + public ODataOptions AddRouteComponents(IEdmModel model) + { + return AddRouteComponents(string.Empty, model, configureServices: null); + } - /// - /// Get the root service provider for a given route (prefix) name. - /// - /// The route name (the route prefix name). - /// The root service provider for the route (prefix) name. - public IServiceProvider GetRouteServices(string routePrefix) - { - if (routePrefix == null) - { - return null; - } + /// + /// Adds an , as well as the given , to the default route. + /// + /// The to add. + /// The batch handler to add. + /// The current instance to enable fluent configuration. + public ODataOptions AddRouteComponents(IEdmModel model, ODataBatchHandler batchHandler) + { + return AddRouteComponents(string.Empty, model, services => services.AddSingleton(sp => batchHandler)); + } - string sanitizedRoutePrefix = SanitizeRoutePrefix(routePrefix); + /// + /// Adds an to the specified route. + /// + /// The model related prefix. It could be null which means there's no prefix when access this model. + /// The to add. + /// The current instance to enable fluent configuration. + public ODataOptions AddRouteComponents(string routePrefix, IEdmModel model) + { + return AddRouteComponents(routePrefix, model, configureServices: null); + } - if (RouteComponents.TryGetValue(sanitizedRoutePrefix, out var components)) - { - return components.ServiceProvider; - } + /// + /// Adds an , as well as the given , to the specified route. + /// + /// The model related prefix. It could be null which means there's no prefix when access this model. + /// The to add. + /// The $batch handler . + /// The current instance to enable fluent configuration. + public ODataOptions AddRouteComponents(string routePrefix, IEdmModel model, ODataBatchHandler batchHandler) + { + return AddRouteComponents(routePrefix, model, services => services.AddSingleton(sp => batchHandler)); + } - return null; - } - #endregion - - #region Global Query settings - - /// - /// Enables all OData query features in one command. - /// - /// - /// The maximum value of $top that a client can request. Defaults to , which does not set an upper limit. - /// - /// The current instance to enable fluent configuration. - public ODataOptions EnableQueryFeatures(int? maxTopValue = null) - { - QueryConfigurations.EnableExpand = true; - QueryConfigurations.EnableSelect = true; - QueryConfigurations.EnableFilter = true; - QueryConfigurations.EnableOrderBy = true; - QueryConfigurations.EnableCount = true; - QueryConfigurations.EnableSkipToken = true; - SetMaxTop(maxTopValue); - return this; - } + /// + /// Adds an using the service configuration. + /// + /// The model related prefix. + /// The to add. + /// The sub service configuration action. + /// The current instance to enable fluent configuration. + public ODataOptions AddRouteComponents(string routePrefix, IEdmModel model, Action configureServices) + { + return AddRouteComponents(routePrefix, model, ODataVersion.V4, configureServices); + } - /// - /// Enable $expand query options. - /// - /// The current instance to enable fluent configuration. - public ODataOptions Expand() + /// + /// Adds an using the service configuration. + /// + /// The model related prefix. + /// The to add. + /// The OData version to be used. + /// The sub service configuration action. + /// The current instance to enable fluent configuration. + public ODataOptions AddRouteComponents(string routePrefix, IEdmModel model, ODataVersion version, Action configureServices) + { + if (model == null) { - QueryConfigurations.EnableExpand = true; - return this; + throw Error.ArgumentNull(nameof(model)); } - /// - /// Enable $select query options. - /// - /// The current instance to enable fluent configuration. - public ODataOptions Select() + if (routePrefix == null) { - QueryConfigurations.EnableSelect = true; - return this; + throw Error.ArgumentNull(nameof(routePrefix)); } - /// - /// Enable $filter query options. - /// - /// The current instance to enable fluent configuration. - public ODataOptions Filter() - { - QueryConfigurations.EnableFilter = true; - return this; - } + string sanitizedRoutePrefix = SanitizeRoutePrefix(routePrefix); - /// - /// Enable $orderby query options. - /// - /// The current instance to enable fluent configuration. - public ODataOptions OrderBy() + if (RouteComponents.ContainsKey(sanitizedRoutePrefix)) { - QueryConfigurations.EnableOrderBy = true; - return this; + throw Error.InvalidOperation(SRResources.ModelPrefixAlreadyUsed, sanitizedRoutePrefix); } - /// - /// Enable $count query options. - /// - /// The current instance to enable fluent configuration. - public ODataOptions Count() + // Consider to use Lazy ? + IServiceProvider serviceProvider = BuildRouteContainer(model, version, configureServices); + RouteComponents[sanitizedRoutePrefix] = (model, serviceProvider); + return this; + } + + /// + /// Get the root service provider for a given route (prefix) name. + /// + /// The route name (the route prefix name). + /// The root service provider for the route (prefix) name. + public IServiceProvider GetRouteServices(string routePrefix) + { + if (routePrefix == null) { - QueryConfigurations.EnableCount = true; - return this; + return null; } - /// - /// Enable $skiptoken query option. - /// - /// The current instance to enable fluent configuration. - public ODataOptions SkipToken() + string sanitizedRoutePrefix = SanitizeRoutePrefix(routePrefix); + + if (RouteComponents.TryGetValue(sanitizedRoutePrefix, out var components)) { - QueryConfigurations.EnableSkipToken = true; - return this; + return components.ServiceProvider; } - /// - ///Sets the maximum value of $top that a client can request. - /// - /// The maximum value of $top that a client can request. - /// The current instance to enable fluent configuration. - public ODataOptions SetMaxTop(int? maxTopValue) - { - if (maxTopValue.HasValue && maxTopValue.Value < 0) - { - throw Error.ArgumentMustBeGreaterThanOrEqualTo(nameof(maxTopValue), maxTopValue, 0); - } + return null; + } + #endregion - QueryConfigurations.MaxTop = maxTopValue; - return this; - } + #region Global Query settings + + /// + /// Enables all OData query features in one command. + /// + /// + /// The maximum value of $top that a client can request. Defaults to , which does not set an upper limit. + /// + /// The current instance to enable fluent configuration. + public ODataOptions EnableQueryFeatures(int? maxTopValue = null) + { + QueryConfigurations.EnableExpand = true; + QueryConfigurations.EnableSelect = true; + QueryConfigurations.EnableFilter = true; + QueryConfigurations.EnableOrderBy = true; + QueryConfigurations.EnableCount = true; + QueryConfigurations.EnableSkipToken = true; + SetMaxTop(maxTopValue); + return this; + } + + /// + /// Enable $expand query options. + /// + /// The current instance to enable fluent configuration. + public ODataOptions Expand() + { + QueryConfigurations.EnableExpand = true; + return this; + } + + /// + /// Enable $select query options. + /// + /// The current instance to enable fluent configuration. + public ODataOptions Select() + { + QueryConfigurations.EnableSelect = true; + return this; + } - /// - /// Gets or sets whether or not the OData system query options should be prefixed with '$'. - /// - public bool EnableNoDollarQueryOptions { get; set; } = true; - - /// - /// Gets the query settings. - /// - [Obsolete("QuerySettings will be removed in the next major version. Use QueryConfigurations instead.")] - public DefaultQuerySettings QuerySettings => QueryConfigurations; - - /// - /// Gets the query configurations. - /// - public DefaultQueryConfigurations QueryConfigurations { get; } = new DefaultQueryConfigurations(); - - #endregion - - /// - /// Build the container. - /// - /// The Edm model. - /// The setup config. - /// The OData version config. - /// The built service provider. - private IServiceProvider BuildRouteContainer(IEdmModel model, ODataVersion version, Action setupAction) + /// + /// Enable $filter query options. + /// + /// The current instance to enable fluent configuration. + public ODataOptions Filter() + { + QueryConfigurations.EnableFilter = true; + return this; + } + + /// + /// Enable $orderby query options. + /// + /// The current instance to enable fluent configuration. + public ODataOptions OrderBy() + { + QueryConfigurations.EnableOrderBy = true; + return this; + } + + /// + /// Enable $count query options. + /// + /// The current instance to enable fluent configuration. + public ODataOptions Count() + { + QueryConfigurations.EnableCount = true; + return this; + } + + /// + /// Enable $skiptoken query option. + /// + /// The current instance to enable fluent configuration. + public ODataOptions SkipToken() + { + QueryConfigurations.EnableSkipToken = true; + return this; + } + + /// + ///Sets the maximum value of $top that a client can request. + /// + /// The maximum value of $top that a client can request. + /// The current instance to enable fluent configuration. + public ODataOptions SetMaxTop(int? maxTopValue) + { + if (maxTopValue.HasValue && maxTopValue.Value < 0) { - Contract.Assert(model != null); + throw Error.ArgumentMustBeGreaterThanOrEqualTo(nameof(maxTopValue), maxTopValue, 0); + } - IServiceCollection services = new ServiceCollection(); + QueryConfigurations.MaxTop = maxTopValue; + return this; + } - // Inject the core odata services. - services.AddDefaultODataServices(version); + /// + /// Gets or sets whether or not the OData system query options should be prefixed with '$'. + /// + public bool EnableNoDollarQueryOptions { get; set; } = true; - // Inject the default query configuration from this options. - services.AddSingleton(sp => this.QueryConfigurations); + /// + /// Gets the query settings. + /// + [Obsolete("QuerySettings will be removed in the next major version. Use QueryConfigurations instead.")] + public DefaultQuerySettings QuerySettings => QueryConfigurations; - // Inject the default Web API OData services. - services.AddDefaultWebApiServices(); + /// + /// Gets the query configurations. + /// + public DefaultQueryConfigurations QueryConfigurations { get; } = new DefaultQueryConfigurations(); - // Set Uri resolver to by default enabling unqualified functions/actions and case insensitive match. - services.AddSingleton(sp => - new UnqualifiedODataUriResolver - { - EnableCaseInsensitive = true, // by default to enable case insensitive - EnableNoDollarQueryOptions = EnableNoDollarQueryOptions // retrieve it from global setting - }); + #endregion - // Inject the Edm model. - // From Current ODL implement, such injection only be used in reader and writer if the input - // model is null. - services.AddSingleton(sp => model); + /// + /// Build the container. + /// + /// The Edm model. + /// The setup config. + /// The OData version config. + /// The built service provider. + private IServiceProvider BuildRouteContainer(IEdmModel model, ODataVersion version, Action setupAction) + { + Contract.Assert(model != null); - // Inject the customized services. - setupAction?.Invoke(services); + IServiceCollection services = new ServiceCollection(); - return services.BuildServiceProvider(); - } + // Inject the core odata services. + services.AddDefaultODataServices(version); - /// - /// Sanitizes the route prefix by stripping leading and trailing forward slashes. - /// - /// Route prefix to sanitize. - /// Sanitized route prefix. - private static string SanitizeRoutePrefix(string routePrefix) - { - Debug.Assert(routePrefix != null); + // Inject the default query configuration from this options. + services.AddSingleton(sp => this.QueryConfigurations); + + // Inject the default Web API OData services. + services.AddDefaultWebApiServices(); - if (routePrefix.Length > 0 && routePrefix[0] != '/' && routePrefix[^1] != '/') + // Set Uri resolver to by default enabling unqualified functions/actions and case insensitive match. + services.AddSingleton(sp => + new UnqualifiedODataUriResolver { - return routePrefix; - } + EnableCaseInsensitive = true, // by default to enable case insensitive + EnableNoDollarQueryOptions = EnableNoDollarQueryOptions // retrieve it from global setting + }); + + // Inject the Edm model. + // From Current ODL implement, such injection only be used in reader and writer if the input + // model is null. + services.AddSingleton(sp => model); + + // Inject the customized services. + setupAction?.Invoke(services); - return routePrefix.Trim('/'); + return services.BuildServiceProvider(); + } + + /// + /// Sanitizes the route prefix by stripping leading and trailing forward slashes. + /// + /// Route prefix to sanitize. + /// Sanitized route prefix. + private static string SanitizeRoutePrefix(string routePrefix) + { + Debug.Assert(routePrefix != null); + + if (routePrefix.Length > 0 && routePrefix[0] != '/' && routePrefix[^1] != '/') + { + return routePrefix; } + + return routePrefix.Trim('/'); } } diff --git a/src/Microsoft.AspNetCore.OData/ODataOptionsSetup.cs b/src/Microsoft.AspNetCore.OData/ODataOptionsSetup.cs index 6ea626f59..5e216a4e5 100644 --- a/src/Microsoft.AspNetCore.OData/ODataOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.OData/ODataOptionsSetup.cs @@ -10,50 +10,49 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.OData +namespace Microsoft.AspNetCore.OData; + +/// +/// Sets up default options for . +/// +public class ODataOptionsSetup : IConfigureOptions { + private readonly ILoggerFactory _loggerFactory; + private IODataPathTemplateParser _templateParser; + /// - /// Sets up default options for . + /// Initializes a new instance of the class. /// - public class ODataOptionsSetup : IConfigureOptions + /// The logger factory. + /// The OData path template parser. + public ODataOptionsSetup(ILoggerFactory loggerFactory, IODataPathTemplateParser parser) { - private readonly ILoggerFactory _loggerFactory; - private IODataPathTemplateParser _templateParser; + _loggerFactory = loggerFactory; + _templateParser = parser; + } - /// - /// Initializes a new instance of the class. - /// - /// The logger factory. - /// The OData path template parser. - public ODataOptionsSetup(ILoggerFactory loggerFactory, IODataPathTemplateParser parser) + /// + /// Configure the default + /// + /// The OData options. + public void Configure(ODataOptions options) + { + if (options == null) { - _loggerFactory = loggerFactory; - _templateParser = parser; + throw Error.ArgumentNull(nameof(options)); } - /// - /// Configure the default - /// - /// The OData options. - public void Configure(ODataOptions options) - { - if (options == null) - { - throw Error.ArgumentNull(nameof(options)); - } - - // Setup built-in routing conventions - options.Conventions.Add(new MetadataRoutingConvention()); - options.Conventions.Add(new EntitySetRoutingConvention()); - options.Conventions.Add(new EntityRoutingConvention()); - options.Conventions.Add(new SingletonRoutingConvention()); - options.Conventions.Add(new FunctionRoutingConvention()); - options.Conventions.Add(new ActionRoutingConvention()); - options.Conventions.Add(new OperationImportRoutingConvention()); - options.Conventions.Add(new PropertyRoutingConvention()); - options.Conventions.Add(new NavigationRoutingConvention(_loggerFactory.CreateLogger())); - options.Conventions.Add(new RefRoutingConvention()); - options.Conventions.Add(new AttributeRoutingConvention(_loggerFactory.CreateLogger(), _templateParser)); - } + // Setup built-in routing conventions + options.Conventions.Add(new MetadataRoutingConvention()); + options.Conventions.Add(new EntitySetRoutingConvention()); + options.Conventions.Add(new EntityRoutingConvention()); + options.Conventions.Add(new SingletonRoutingConvention()); + options.Conventions.Add(new FunctionRoutingConvention()); + options.Conventions.Add(new ActionRoutingConvention()); + options.Conventions.Add(new OperationImportRoutingConvention()); + options.Conventions.Add(new PropertyRoutingConvention()); + options.Conventions.Add(new NavigationRoutingConvention(_loggerFactory.CreateLogger())); + options.Conventions.Add(new RefRoutingConvention()); + options.Conventions.Add(new AttributeRoutingConvention(_loggerFactory.CreateLogger(), _templateParser)); } } diff --git a/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs index 4481291fa..b73f5798d 100644 --- a/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs @@ -19,91 +19,90 @@ using Microsoft.Extensions.Options; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData +namespace Microsoft.AspNetCore.OData; + +/// +/// Provides extension methods to add OData services. +/// +public static class ODataServiceCollectionExtensions { /// - /// Provides extension methods to add OData services. + /// Enables query support for actions with an or return + /// type. To avoid processing unexpected or malicious queries, use the validation settings on + /// to validate incoming queries. For more information, visit + /// http://go.microsoft.com/fwlink/?LinkId=279712. /// - public static class ODataServiceCollectionExtensions + /// The services collection. + /// The so that additional calls can be chained. + public static IServiceCollection AddODataQueryFilter(this IServiceCollection services) { - /// - /// Enables query support for actions with an or return - /// type. To avoid processing unexpected or malicious queries, use the validation settings on - /// to validate incoming queries. For more information, visit - /// http://go.microsoft.com/fwlink/?LinkId=279712. - /// - /// The services collection. - /// The so that additional calls can be chained. - public static IServiceCollection AddODataQueryFilter(this IServiceCollection services) - { - return AddODataQueryFilter(services, new EnableQueryAttribute()); - } + return AddODataQueryFilter(services, new EnableQueryAttribute()); + } - /// - /// Enables query support for actions with an or return - /// type. To avoid processing unexpected or malicious queries, use the validation settings on - /// to validate incoming queries. For more information, visit - /// http://go.microsoft.com/fwlink/?LinkId=279712. - /// - /// The services collection. - /// The action filter that executes the query. - /// The so that additional calls can be chained. - public static IServiceCollection AddODataQueryFilter(this IServiceCollection services, IActionFilter queryFilter) + /// + /// Enables query support for actions with an or return + /// type. To avoid processing unexpected or malicious queries, use the validation settings on + /// to validate incoming queries. For more information, visit + /// http://go.microsoft.com/fwlink/?LinkId=279712. + /// + /// The services collection. + /// The action filter that executes the query. + /// The so that additional calls can be chained. + public static IServiceCollection AddODataQueryFilter(this IServiceCollection services, IActionFilter queryFilter) + { + if (services == null) { - if (services == null) - { - throw Error.ArgumentNull(nameof(services)); - } - - services.TryAddEnumerable(ServiceDescriptor.Singleton(new QueryFilterProvider(queryFilter))); - return services; + throw Error.ArgumentNull(nameof(services)); } - /// - /// Adds the core OData services required for OData requests. - /// - /// The to add the services to. - /// The so that additional calls can be chained. - internal static IServiceCollection AddODataCore(this IServiceCollection services) + services.TryAddEnumerable(ServiceDescriptor.Singleton(new QueryFilterProvider(queryFilter))); + return services; + } + + /// + /// Adds the core OData services required for OData requests. + /// + /// The to add the services to. + /// The so that additional calls can be chained. + internal static IServiceCollection AddODataCore(this IServiceCollection services) + { + if (services == null) { - if (services == null) - { - throw Error.ArgumentNull(nameof(services)); - } + throw Error.ArgumentNull(nameof(services)); + } - // - // Options - // - services.TryAddEnumerable( - ServiceDescriptor.Transient, ODataOptionsSetup>()); + // + // Options + // + services.TryAddEnumerable( + ServiceDescriptor.Transient, ODataOptionsSetup>()); - services.TryAddEnumerable( - ServiceDescriptor.Transient, ODataMvcOptionsSetup>()); + services.TryAddEnumerable( + ServiceDescriptor.Transient, ODataMvcOptionsSetup>()); - services.TryAddEnumerable( - ServiceDescriptor.Transient, ODataJsonOptionsSetup>()); + services.TryAddEnumerable( + ServiceDescriptor.Transient, ODataJsonOptionsSetup>()); - // - // Parser & Resolver & Provider - // - services.TryAddEnumerable( - ServiceDescriptor.Singleton()); + // + // Parser & Resolver & Provider + // + services.TryAddEnumerable( + ServiceDescriptor.Singleton()); - services.TryAddSingleton(); + services.TryAddSingleton(); - // - // Routing - // - services.TryAddEnumerable( - ServiceDescriptor.Transient()); + // + // Routing + // + services.TryAddEnumerable( + ServiceDescriptor.Transient()); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); - services.TryAddSingleton(); + services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); - return services; - } + return services; } } diff --git a/src/Microsoft.AspNetCore.OData/ODataUriFunctions.cs b/src/Microsoft.AspNetCore.OData/ODataUriFunctions.cs index 68f923ee9..a17abf81f 100644 --- a/src/Microsoft.AspNetCore.OData/ODataUriFunctions.cs +++ b/src/Microsoft.AspNetCore.OData/ODataUriFunctions.cs @@ -10,58 +10,57 @@ using Microsoft.AspNetCore.OData.Query.Expressions; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData +namespace Microsoft.AspNetCore.OData; + +/// +/// OData UriFunctions helper. +/// +public static class ODataUriFunctions { /// - /// OData UriFunctions helper. + /// This is a shortcut of adding the custom FunctionSignature through 'CustomUriFunctions' class and + /// binding the function name to it's MethodInfo through 'UriFunctionsBinder' class. + /// See these classes documentations. + /// In case of an exception, both operations(adding the signature and binding the function) will be undone. /// - public static class ODataUriFunctions + /// The uri function name that appears in the OData request uri. + /// The new custom function signature. + /// The MethodInfo to bind the given function name. + /// Any exception thrown by 'CustomUriFunctions.AddCustomUriFunction' and 'UriFunctionBinder.BindUriFunctionName' methods. + public static void AddCustomUriFunction(string functionName, + FunctionSignatureWithReturnType functionSignature, MethodInfo methodInfo) { - /// - /// This is a shortcut of adding the custom FunctionSignature through 'CustomUriFunctions' class and - /// binding the function name to it's MethodInfo through 'UriFunctionsBinder' class. - /// See these classes documentations. - /// In case of an exception, both operations(adding the signature and binding the function) will be undone. - /// - /// The uri function name that appears in the OData request uri. - /// The new custom function signature. - /// The MethodInfo to bind the given function name. - /// Any exception thrown by 'CustomUriFunctions.AddCustomUriFunction' and 'UriFunctionBinder.BindUriFunctionName' methods. - public static void AddCustomUriFunction(string functionName, - FunctionSignatureWithReturnType functionSignature, MethodInfo methodInfo) + try { - try - { - // Add to OData.Libs function signature - CustomUriFunctions.AddCustomUriFunction(functionName, functionSignature); + // Add to OData.Libs function signature + CustomUriFunctions.AddCustomUriFunction(functionName, functionSignature); - // Bind the method to it's MethoInfo - UriFunctionsBinder.BindUriFunctionName(functionName, methodInfo); - } - catch - { - // Clear in case of exception - RemoveCustomUriFunction(functionName, functionSignature, methodInfo); - throw; - } + // Bind the method to it's MethoInfo + UriFunctionsBinder.BindUriFunctionName(functionName, methodInfo); } - - /// - /// This is a shortcut of removing the FunctionSignature through 'CustomUriFunctions' class and - /// unbinding the function name from it's MethodInfo through 'UriFunctionsBinder' class. - /// See these classes documentations. - /// - /// The uri function name that appears in the OData request uri. - /// The new custom function signature. - /// The MethodInfo to bind the given function name. - /// Any exception thrown by 'CustomUriFunctions.RemoveCustomUriFunction' and 'UriFunctionsBinder.UnbindUriFunctionName' methods. - /// 'True' if the function signature has successfully removed and unbounded. 'False' otherwise. - public static bool RemoveCustomUriFunction(string functionName, - FunctionSignatureWithReturnType functionSignature, MethodInfo methodInfo) + catch { - return - CustomUriFunctions.RemoveCustomUriFunction(functionName, functionSignature) && - UriFunctionsBinder.UnbindUriFunctionName(functionName, methodInfo); + // Clear in case of exception + RemoveCustomUriFunction(functionName, functionSignature, methodInfo); + throw; } } + + /// + /// This is a shortcut of removing the FunctionSignature through 'CustomUriFunctions' class and + /// unbinding the function name from it's MethodInfo through 'UriFunctionsBinder' class. + /// See these classes documentations. + /// + /// The uri function name that appears in the OData request uri. + /// The new custom function signature. + /// The MethodInfo to bind the given function name. + /// Any exception thrown by 'CustomUriFunctions.RemoveCustomUriFunction' and 'UriFunctionsBinder.UnbindUriFunctionName' methods. + /// 'True' if the function signature has successfully removed and unbounded. 'False' otherwise. + public static bool RemoveCustomUriFunction(string functionName, + FunctionSignatureWithReturnType functionSignature, MethodInfo methodInfo) + { + return + CustomUriFunctions.RemoveCustomUriFunction(functionName, functionSignature) && + UriFunctionsBinder.UnbindUriFunctionName(functionName, methodInfo); + } } diff --git a/src/Microsoft.AspNetCore.OData/Query/AllowedArithmeticOperators.cs b/src/Microsoft.AspNetCore.OData/Query/AllowedArithmeticOperators.cs index f93ec797c..5fcf01894 100644 --- a/src/Microsoft.AspNetCore.OData/Query/AllowedArithmeticOperators.cs +++ b/src/Microsoft.AspNetCore.OData/Query/AllowedArithmeticOperators.cs @@ -7,47 +7,46 @@ using System; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Arithmetic operators to allow for querying using $filter. +/// +[Flags] +public enum AllowedArithmeticOperators { /// - /// Arithmetic operators to allow for querying using $filter. + /// A value that corresponds to allowing no arithmetic operators in $filter. + /// + None = 0x0, + + /// + /// A value that corresponds to allowing 'Add' arithmetic operator in $filter. + /// + Add = 0x1, + + /// + /// A value that corresponds to allowing 'Subtract' arithmetic operator in $filter. + /// + Subtract = 0x2, + + /// + /// A value that corresponds to allowing 'Multiply' arithmetic operator in $filter. + /// + Multiply = 0x4, + + /// + /// A value that corresponds to allowing 'Divide' arithmetic operator in $filter. + /// + Divide = 0x8, + + /// + /// A value that corresponds to allowing 'Modulo' arithmetic operator in $filter. + /// + Modulo = 0x10, + + /// + /// A value that corresponds to allowing all arithmetic operators in $filter. /// - [Flags] - public enum AllowedArithmeticOperators - { - /// - /// A value that corresponds to allowing no arithmetic operators in $filter. - /// - None = 0x0, - - /// - /// A value that corresponds to allowing 'Add' arithmetic operator in $filter. - /// - Add = 0x1, - - /// - /// A value that corresponds to allowing 'Subtract' arithmetic operator in $filter. - /// - Subtract = 0x2, - - /// - /// A value that corresponds to allowing 'Multiply' arithmetic operator in $filter. - /// - Multiply = 0x4, - - /// - /// A value that corresponds to allowing 'Divide' arithmetic operator in $filter. - /// - Divide = 0x8, - - /// - /// A value that corresponds to allowing 'Modulo' arithmetic operator in $filter. - /// - Modulo = 0x10, - - /// - /// A value that corresponds to allowing all arithmetic operators in $filter. - /// - All = Add | Subtract | Multiply | Divide | Modulo - } + All = Add | Subtract | Multiply | Divide | Modulo } diff --git a/src/Microsoft.AspNetCore.OData/Query/AllowedFunctions.cs b/src/Microsoft.AspNetCore.OData/Query/AllowedFunctions.cs index bcdca1897..62533a7a8 100644 --- a/src/Microsoft.AspNetCore.OData/Query/AllowedFunctions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/AllowedFunctions.cs @@ -7,172 +7,171 @@ using System; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Functions to allow for querying using $filter. +/// +[Flags] +public enum AllowedFunctions { /// - /// Functions to allow for querying using $filter. - /// - [Flags] - public enum AllowedFunctions - { - /// - /// A value that corresponds to allowing no functions in $filter. - /// - None = 0x0, - - /// - /// A value that corresponds to allowing 'StartsWith' function in $filter. - /// - StartsWith = 0x1, - - /// - /// A value that corresponds to allowing 'EndsWith' function in $filter. - /// - EndsWith = 0x2, - - /// - /// A value that corresponds to allowing 'Contains' function in $filter. - /// - Contains = 0x4, - - /// - /// A value that corresponds to allowing 'Length' function in $filter. - /// - Length = 0x8, - - /// - /// A value that corresponds to allowing 'IndexOf' function in $filter. - /// - IndexOf = 0x10, - - /// - /// A value that corresponds to allowing 'Concat' function in $filter. - /// - Concat = 0x20, - - /// - /// A value that corresponds to allowing 'Substring' function in $filter. - /// - Substring = 0x40, - - /// - /// A value that corresponds to allowing 'ToLower' function in $filter. - /// - ToLower = 0x80, - - /// - /// A value that corresponds to allowing 'ToUpper' function in $filter. - /// - ToUpper = 0x100, - - /// - /// A value that corresponds to allowing 'Trim' function in $filter. - /// - Trim = 0x200, - - /// - /// A value that corresponds to allowing 'Cast' function in $filter. - /// - Cast = 0x400, - - /// - /// A value that corresponds to allowing 'Year' function in $filter. - /// - Year = 0x800, - - /// - /// A value that corresponds to allowing 'Date' function in $filter. - /// - Date = 0x1000, - - /// - /// A value that corresponds to allowing 'Month' function in $filter. - /// - Month = 0x2000, - - /// - /// A value that corresponds to allowing 'Time' function in $filter. - /// - Time = 0x4000, - - /// - /// A value that corresponds to allowing 'Day' function in $filter. - /// - Day = 0x8000, - - /// - /// A value that corresponds to allowing 'Hour' function in $filter. - /// - Hour = 0x20000, - - /// - /// A value that corresponds to allowing 'Minute' function in $filter. - /// - Minute = 0x80000, - - /// - /// A value that corresponds to allowing 'Second' function in $filter. - /// - Second = 0x200000, - - /// - /// A value that corresponds to allowing 'Fractionalseconds' function in $filter. - /// - FractionalSeconds = 0x400000, - - /// - /// A value that corresponds to allowing 'Round' function in $filter. - /// - Round = 0x800000, - - /// - /// A value that corresponds to allowing 'Floor' function in $filter. - /// - Floor = 0x1000000, - - /// - /// A value that corresponds to allowing 'Ceiling' function in $filter. - /// - Ceiling = 0x2000000, - - /// - /// A value that corresponds to allowing 'IsOf' function in $filter. - /// - IsOf = 0x4000000, - - /// - /// A value that corresponds to allowing 'Any' function in $filter. - /// - Any = 0x8000000, - - /// - /// A value that corresponds to allowing 'All' function in $filter. - /// - All = 0x10000000, - - /// - /// A value that corresponds to allowing 'MatchesPattern' function in $filter. - /// - MatchesPattern = 0x20000000, - - /// - /// A value that corresponds to allowing all string related functions in $filter. - /// - AllStringFunctions = StartsWith | EndsWith | Contains | Length | IndexOf | Concat | Substring | ToLower | ToUpper | Trim | MatchesPattern, - - /// - /// A value that corresponds to allowing all datetime related functions in $filter. - /// - AllDateTimeFunctions = Year | Month | Day | Hour | Minute | Second | FractionalSeconds | Date | Time, - - /// - /// A value that corresponds to allowing math related functions in $filter. - /// - AllMathFunctions = Round | Floor | Ceiling, - - /// - /// A value that corresponds to allowing all functions in $filter. - /// - AllFunctions = AllStringFunctions | AllDateTimeFunctions | AllMathFunctions | Cast | IsOf | Any | All - } + /// A value that corresponds to allowing no functions in $filter. + /// + None = 0x0, + + /// + /// A value that corresponds to allowing 'StartsWith' function in $filter. + /// + StartsWith = 0x1, + + /// + /// A value that corresponds to allowing 'EndsWith' function in $filter. + /// + EndsWith = 0x2, + + /// + /// A value that corresponds to allowing 'Contains' function in $filter. + /// + Contains = 0x4, + + /// + /// A value that corresponds to allowing 'Length' function in $filter. + /// + Length = 0x8, + + /// + /// A value that corresponds to allowing 'IndexOf' function in $filter. + /// + IndexOf = 0x10, + + /// + /// A value that corresponds to allowing 'Concat' function in $filter. + /// + Concat = 0x20, + + /// + /// A value that corresponds to allowing 'Substring' function in $filter. + /// + Substring = 0x40, + + /// + /// A value that corresponds to allowing 'ToLower' function in $filter. + /// + ToLower = 0x80, + + /// + /// A value that corresponds to allowing 'ToUpper' function in $filter. + /// + ToUpper = 0x100, + + /// + /// A value that corresponds to allowing 'Trim' function in $filter. + /// + Trim = 0x200, + + /// + /// A value that corresponds to allowing 'Cast' function in $filter. + /// + Cast = 0x400, + + /// + /// A value that corresponds to allowing 'Year' function in $filter. + /// + Year = 0x800, + + /// + /// A value that corresponds to allowing 'Date' function in $filter. + /// + Date = 0x1000, + + /// + /// A value that corresponds to allowing 'Month' function in $filter. + /// + Month = 0x2000, + + /// + /// A value that corresponds to allowing 'Time' function in $filter. + /// + Time = 0x4000, + + /// + /// A value that corresponds to allowing 'Day' function in $filter. + /// + Day = 0x8000, + + /// + /// A value that corresponds to allowing 'Hour' function in $filter. + /// + Hour = 0x20000, + + /// + /// A value that corresponds to allowing 'Minute' function in $filter. + /// + Minute = 0x80000, + + /// + /// A value that corresponds to allowing 'Second' function in $filter. + /// + Second = 0x200000, + + /// + /// A value that corresponds to allowing 'Fractionalseconds' function in $filter. + /// + FractionalSeconds = 0x400000, + + /// + /// A value that corresponds to allowing 'Round' function in $filter. + /// + Round = 0x800000, + + /// + /// A value that corresponds to allowing 'Floor' function in $filter. + /// + Floor = 0x1000000, + + /// + /// A value that corresponds to allowing 'Ceiling' function in $filter. + /// + Ceiling = 0x2000000, + + /// + /// A value that corresponds to allowing 'IsOf' function in $filter. + /// + IsOf = 0x4000000, + + /// + /// A value that corresponds to allowing 'Any' function in $filter. + /// + Any = 0x8000000, + + /// + /// A value that corresponds to allowing 'All' function in $filter. + /// + All = 0x10000000, + + /// + /// A value that corresponds to allowing 'MatchesPattern' function in $filter. + /// + MatchesPattern = 0x20000000, + + /// + /// A value that corresponds to allowing all string related functions in $filter. + /// + AllStringFunctions = StartsWith | EndsWith | Contains | Length | IndexOf | Concat | Substring | ToLower | ToUpper | Trim | MatchesPattern, + + /// + /// A value that corresponds to allowing all datetime related functions in $filter. + /// + AllDateTimeFunctions = Year | Month | Day | Hour | Minute | Second | FractionalSeconds | Date | Time, + + /// + /// A value that corresponds to allowing math related functions in $filter. + /// + AllMathFunctions = Round | Floor | Ceiling, + + /// + /// A value that corresponds to allowing all functions in $filter. + /// + AllFunctions = AllStringFunctions | AllDateTimeFunctions | AllMathFunctions | Cast | IsOf | Any | All } diff --git a/src/Microsoft.AspNetCore.OData/Query/AllowedLogicalOperators.cs b/src/Microsoft.AspNetCore.OData/Query/AllowedLogicalOperators.cs index 3a7de6dbb..c0dfc693a 100644 --- a/src/Microsoft.AspNetCore.OData/Query/AllowedLogicalOperators.cs +++ b/src/Microsoft.AspNetCore.OData/Query/AllowedLogicalOperators.cs @@ -7,72 +7,71 @@ using System; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Logical operators to allow for querying using $filter. +/// +[Flags] +public enum AllowedLogicalOperators { /// - /// Logical operators to allow for querying using $filter. + /// A value that corresponds to allowing no logical operators in $filter. /// - [Flags] - public enum AllowedLogicalOperators - { - /// - /// A value that corresponds to allowing no logical operators in $filter. - /// - None = 0x0, + None = 0x0, - /// - /// A value that corresponds to allowing 'Or' logical operator in $filter. - /// - Or = 0x1, + /// + /// A value that corresponds to allowing 'Or' logical operator in $filter. + /// + Or = 0x1, - /// - /// A value that corresponds to allowing 'And' logical operator in $filter. - /// - And = 0x2, + /// + /// A value that corresponds to allowing 'And' logical operator in $filter. + /// + And = 0x2, - /// - /// A value that corresponds to allowing 'Equal' logical operator in $filter. - /// - Equal = 0x4, + /// + /// A value that corresponds to allowing 'Equal' logical operator in $filter. + /// + Equal = 0x4, - /// - /// A value that corresponds to allowing 'NotEqual' logical operator in $filter. - /// - NotEqual = 0x8, + /// + /// A value that corresponds to allowing 'NotEqual' logical operator in $filter. + /// + NotEqual = 0x8, - /// - /// A value that corresponds to allowing 'GreaterThan' logical operator in $filter. - /// - GreaterThan = 0x10, + /// + /// A value that corresponds to allowing 'GreaterThan' logical operator in $filter. + /// + GreaterThan = 0x10, - /// - /// A value that corresponds to allowing 'GreaterThanOrEqual' logical operator in $filter. - /// - GreaterThanOrEqual = 0x20, + /// + /// A value that corresponds to allowing 'GreaterThanOrEqual' logical operator in $filter. + /// + GreaterThanOrEqual = 0x20, - /// - /// A value that corresponds to allowing 'LessThan' logical operator in $filter. - /// - LessThan = 0x40, + /// + /// A value that corresponds to allowing 'LessThan' logical operator in $filter. + /// + LessThan = 0x40, - /// - /// A value that corresponds to allowing 'LessThanOrEqual' logical operator in $filter. - /// - LessThanOrEqual = 0x80, + /// + /// A value that corresponds to allowing 'LessThanOrEqual' logical operator in $filter. + /// + LessThanOrEqual = 0x80, - /// - /// A value that corresponds to allowing 'Not' logical operator in $filter. - /// - Not = 0x100, + /// + /// A value that corresponds to allowing 'Not' logical operator in $filter. + /// + Not = 0x100, - /// - /// A value that corresponds to allowing 'Has' logical operator in $filter. - /// - Has = 0x200, + /// + /// A value that corresponds to allowing 'Has' logical operator in $filter. + /// + Has = 0x200, - /// - /// A value that corresponds to allowing all logical operators in $filter. - /// - All = Or | And | Equal | NotEqual | GreaterThan | GreaterThanOrEqual | LessThan | LessThanOrEqual | Not | Has - } + /// + /// A value that corresponds to allowing all logical operators in $filter. + /// + All = Or | And | Equal | NotEqual | GreaterThan | GreaterThanOrEqual | LessThan | LessThanOrEqual | Not | Has } diff --git a/src/Microsoft.AspNetCore.OData/Query/AllowedQueryOptions.cs b/src/Microsoft.AspNetCore.OData/Query/AllowedQueryOptions.cs index fe3b684be..46d4235d4 100644 --- a/src/Microsoft.AspNetCore.OData/Query/AllowedQueryOptions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/AllowedQueryOptions.cs @@ -7,92 +7,91 @@ using System; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// OData query options to allow for querying. +/// +[Flags] +public enum AllowedQueryOptions { /// - /// OData query options to allow for querying. - /// - [Flags] - public enum AllowedQueryOptions - { - /// - /// A value that corresponds to allowing no query options. - /// - None = 0x0, - - /// - /// A value that corresponds to allowing the $filter query option. - /// - Filter = 0x1, - - /// - /// A value that corresponds to allowing the $expand query option. - /// - Expand = 0x2, - - /// - /// A value that corresponds to allowing the $select query option. - /// - Select = 0x4, - - /// - /// A value that corresponds to allowing the $orderby query option. - /// - OrderBy = 0x8, - - /// - /// A value that corresponds to allowing the $top query option. - /// - Top = 0x10, - - /// - /// A value that corresponds to allowing the $skip query option. - /// - Skip = 0x20, - - /// - /// A value that corresponds to allowing the $count query option. - /// - Count = 0x40, - - /// - /// A value that corresponds to allowing the $format query option. - /// - Format = 0x80, - - /// - /// A value that corresponds to allowing the $skiptoken query option. - /// - SkipToken = 0x100, - - /// - /// A value that corresponds to allowing the $deltatoken query option. - /// - DeltaToken = 0x200, - - /// - /// A value that corresponds to allowing the $apply query option. - /// - Apply = 0x400, - - /// - /// A value that corresponds to allowing the $compute query option. - /// - Compute = 0x800, - - /// - /// A value that corresponds to allowing the $search query option. - /// - Search = 0x1000, - - /// - /// A value that corresponds to the default query options supported. - /// - Supported = Filter | OrderBy | Top | Skip | SkipToken | Count | Select | Expand | Format | Apply | Compute | Search, - - /// - /// A value that corresponds to allowing all query options. - /// - All = Filter | Expand | Select | OrderBy | Top | Skip | Count | Format | SkipToken | DeltaToken | Apply | Compute | Search - } + /// A value that corresponds to allowing no query options. + /// + None = 0x0, + + /// + /// A value that corresponds to allowing the $filter query option. + /// + Filter = 0x1, + + /// + /// A value that corresponds to allowing the $expand query option. + /// + Expand = 0x2, + + /// + /// A value that corresponds to allowing the $select query option. + /// + Select = 0x4, + + /// + /// A value that corresponds to allowing the $orderby query option. + /// + OrderBy = 0x8, + + /// + /// A value that corresponds to allowing the $top query option. + /// + Top = 0x10, + + /// + /// A value that corresponds to allowing the $skip query option. + /// + Skip = 0x20, + + /// + /// A value that corresponds to allowing the $count query option. + /// + Count = 0x40, + + /// + /// A value that corresponds to allowing the $format query option. + /// + Format = 0x80, + + /// + /// A value that corresponds to allowing the $skiptoken query option. + /// + SkipToken = 0x100, + + /// + /// A value that corresponds to allowing the $deltatoken query option. + /// + DeltaToken = 0x200, + + /// + /// A value that corresponds to allowing the $apply query option. + /// + Apply = 0x400, + + /// + /// A value that corresponds to allowing the $compute query option. + /// + Compute = 0x800, + + /// + /// A value that corresponds to allowing the $search query option. + /// + Search = 0x1000, + + /// + /// A value that corresponds to the default query options supported. + /// + Supported = Filter | OrderBy | Top | Skip | SkipToken | Count | Select | Expand | Format | Apply | Compute | Search, + + /// + /// A value that corresponds to allowing all query options. + /// + All = Filter | Expand | Select | OrderBy | Top | Skip | Count | Format | SkipToken | DeltaToken | Apply | Compute | Search } diff --git a/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs b/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs index 6bbfd9042..a9a3e37da 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs @@ -19,159 +19,158 @@ using System.Text.RegularExpressions; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +internal class ClrCanonicalFunctions { - internal class ClrCanonicalFunctions + private static string _defaultString = default(string); + private static Enum _defaultEnum = default(Enum); + + // function names + internal const string StartswithFunctionName = "startswith"; + internal const string EndswithFunctionName = "endswith"; + internal const string ContainsFunctionName = "contains"; + internal const string SubstringFunctionName = "substring"; + internal const string LengthFunctionName = "length"; + internal const string IndexofFunctionName = "indexof"; + internal const string TolowerFunctionName = "tolower"; + internal const string ToupperFunctionName = "toupper"; + internal const string TrimFunctionName = "trim"; + internal const string ConcatFunctionName = "concat"; + internal const string MatchesPatternFunctionName = "matchesPattern"; + internal const string YearFunctionName = "year"; + internal const string MonthFunctionName = "month"; + internal const string DayFunctionName = "day"; + internal const string HourFunctionName = "hour"; + internal const string MinuteFunctionName = "minute"; + internal const string SecondFunctionName = "second"; + internal const string MillisecondFunctionName = "millisecond"; + internal const string FractionalSecondsFunctionName = "fractionalseconds"; + internal const string RoundFunctionName = "round"; + internal const string FloorFunctionName = "floor"; + internal const string CeilingFunctionName = "ceiling"; + internal const string CastFunctionName = "cast"; + internal const string IsofFunctionName = "isof"; + internal const string DateFunctionName = "date"; + internal const string TimeFunctionName = "time"; + internal const string NowFunctionName = "now"; + + // string functions + public static readonly MethodInfo StartsWith = MethodOf(_ => _defaultString.StartsWith(default(string))); + public static readonly MethodInfo EndsWith = MethodOf(_ => _defaultString.EndsWith(default(string))); + public static readonly MethodInfo Contains = MethodOf(_ => _defaultString.Contains(default(string))); + public static readonly MethodInfo SubstringStart = MethodOf(_ => _defaultString.Substring(default(int))); + public static readonly MethodInfo SubstringStartAndLength = MethodOf(_ => _defaultString.Substring(default(int), default(int))); + public static readonly MethodInfo SubstringStartNoThrow = MethodOf(_ => ClrSafeFunctions.SubstringStart(default(string), default(int))); + public static readonly MethodInfo SubstringStartAndLengthNoThrow = MethodOf(_ => ClrSafeFunctions.SubstringStartAndLength(default(string), default(int), default(int))); + public static readonly MethodInfo IndexOf = MethodOf(_ => _defaultString.IndexOf(default(string))); + public static readonly MethodInfo ToLower = MethodOf(_ => _defaultString.ToLower()); + public static readonly MethodInfo ToUpper = MethodOf(_ => _defaultString.ToUpper()); + public static readonly MethodInfo Trim = MethodOf(_ => _defaultString.Trim()); + public static readonly MethodInfo Concat = MethodOf(_ => String.Concat(default(string), default(string))); + public static readonly MethodInfo MatchesMattern = MethodOf(_ => Regex.IsMatch(default(string), default(string), default(RegexOptions))); + + // math functions + public static readonly MethodInfo CeilingOfDouble = MethodOf(_ => Math.Ceiling(default(double))); + public static readonly MethodInfo RoundOfDouble = MethodOf(_ => Math.Round(default(double))); + public static readonly MethodInfo FloorOfDouble = MethodOf(_ => Math.Floor(default(double))); + + public static readonly MethodInfo CeilingOfDecimal = MethodOf(_ => Math.Ceiling(default(decimal))); + public static readonly MethodInfo RoundOfDecimal = MethodOf(_ => Math.Round(default(decimal))); + public static readonly MethodInfo FloorOfDecimal = MethodOf(_ => Math.Floor(default(decimal))); + + // enum functions + public static readonly MethodInfo HasFlag = MethodOf(_ => _defaultEnum.HasFlag(default(Enum))); + + // Date properties + public static readonly Dictionary DateProperties = new[] + { + new KeyValuePair(YearFunctionName, typeof(Date).GetProperty("Year")), + new KeyValuePair(MonthFunctionName, typeof(Date).GetProperty("Month")), + new KeyValuePair(DayFunctionName, typeof(Date).GetProperty("Day")), + }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + // DateTimeproperties + public static readonly Dictionary DateTimeProperties = new[] + { + new KeyValuePair(YearFunctionName, typeof(DateTime).GetProperty("Year")), + new KeyValuePair(MonthFunctionName, typeof(DateTime).GetProperty("Month")), + new KeyValuePair(DayFunctionName, typeof(DateTime).GetProperty("Day")), + new KeyValuePair(HourFunctionName, typeof(DateTime).GetProperty("Hour")), + new KeyValuePair(MinuteFunctionName, typeof(DateTime).GetProperty("Minute")), + new KeyValuePair(SecondFunctionName, typeof(DateTime).GetProperty("Second")), + new KeyValuePair(MillisecondFunctionName, typeof(DateTime).GetProperty("Millisecond")), + }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + // DateTimeOffset properties + public static readonly Dictionary DateTimeOffsetProperties = new[] + { + new KeyValuePair(YearFunctionName, typeof(DateTimeOffset).GetProperty("Year")), + new KeyValuePair(MonthFunctionName, typeof(DateTimeOffset).GetProperty("Month")), + new KeyValuePair(DayFunctionName, typeof(DateTimeOffset).GetProperty("Day")), + new KeyValuePair(HourFunctionName, typeof(DateTimeOffset).GetProperty("Hour")), + new KeyValuePair(MinuteFunctionName, typeof(DateTimeOffset).GetProperty("Minute")), + new KeyValuePair(SecondFunctionName, typeof(DateTimeOffset).GetProperty("Second")), + new KeyValuePair(MillisecondFunctionName, typeof(DateTimeOffset).GetProperty("Millisecond")), + }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + // TimeOfDay properties + // ODL uses the Hour(s), Minute(s), Second(s), It's the wrong property name. It should be Hour, Minute, Second. + public static readonly Dictionary TimeOfDayProperties = new[] + { + new KeyValuePair(HourFunctionName, typeof(TimeOfDay).GetProperty("Hours")), + new KeyValuePair(MinuteFunctionName, typeof(TimeOfDay).GetProperty("Minutes")), + new KeyValuePair(SecondFunctionName, typeof(TimeOfDay).GetProperty("Seconds")), + new KeyValuePair(MillisecondFunctionName, typeof(TimeOfDay).GetProperty("Milliseconds")), + }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + // DateOnly properties + public static readonly Dictionary DateOnlyProperties = new Dictionary + { + { YearFunctionName, typeof(DateOnly).GetProperty(nameof(DateOnly.Year)) }, + { MonthFunctionName, typeof(DateOnly).GetProperty(nameof(DateOnly.Month)) }, + { DayFunctionName, typeof(DateOnly).GetProperty(nameof(DateOnly.Day)) } + }; + + // TimeOnly properties + public static readonly Dictionary TimeOnlyProperties = new Dictionary + { + { HourFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Hour)) }, + { MinuteFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Minute)) }, + { SecondFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Second)) }, + { MillisecondFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Millisecond)) } + }; + + // TimeSpan properties + public static readonly Dictionary TimeSpanProperties = new[] + { + new KeyValuePair(HourFunctionName, typeof(TimeSpan).GetProperty("Hours")), + new KeyValuePair(MinuteFunctionName, typeof(TimeSpan).GetProperty("Minutes")), + new KeyValuePair(SecondFunctionName, typeof(TimeSpan).GetProperty("Seconds")), + new KeyValuePair(MillisecondFunctionName, typeof(TimeSpan).GetProperty("Milliseconds")), + }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + // String Properties + public static readonly PropertyInfo Length = typeof(string).GetProperty("Length"); + + // PropertyInfo and MethodInfo of DateTime & DateTimeOffset related. + public static readonly PropertyInfo DateTimeKindPropertyInfo = typeof(DateTime).GetProperty("Kind"); + public static readonly MethodInfo ToUniversalTimeDateTime = typeof(DateTime).GetMethod("ToUniversalTime", BindingFlags.Instance | BindingFlags.Public); + public static readonly MethodInfo ToUniversalTimeDateTimeOffset = typeof(DateTimeOffset).GetMethod("ToUniversalTime", BindingFlags.Instance | BindingFlags.Public); + public static readonly MethodInfo ToOffsetFunction = typeof(DateTimeOffset).GetMethod("ToOffset", BindingFlags.Instance | BindingFlags.Public); + public static readonly MethodInfo GetUtcOffset = typeof(TimeZoneInfo).GetMethod("GetUtcOffset", new[] { typeof(DateTime) }); + + private static MethodInfo MethodOf(Expression> expression) + { + return MethodOf(expression as Expression); + } + + private static MethodInfo MethodOf(Expression expression) { - private static string _defaultString = default(string); - private static Enum _defaultEnum = default(Enum); - - // function names - internal const string StartswithFunctionName = "startswith"; - internal const string EndswithFunctionName = "endswith"; - internal const string ContainsFunctionName = "contains"; - internal const string SubstringFunctionName = "substring"; - internal const string LengthFunctionName = "length"; - internal const string IndexofFunctionName = "indexof"; - internal const string TolowerFunctionName = "tolower"; - internal const string ToupperFunctionName = "toupper"; - internal const string TrimFunctionName = "trim"; - internal const string ConcatFunctionName = "concat"; - internal const string MatchesPatternFunctionName = "matchesPattern"; - internal const string YearFunctionName = "year"; - internal const string MonthFunctionName = "month"; - internal const string DayFunctionName = "day"; - internal const string HourFunctionName = "hour"; - internal const string MinuteFunctionName = "minute"; - internal const string SecondFunctionName = "second"; - internal const string MillisecondFunctionName = "millisecond"; - internal const string FractionalSecondsFunctionName = "fractionalseconds"; - internal const string RoundFunctionName = "round"; - internal const string FloorFunctionName = "floor"; - internal const string CeilingFunctionName = "ceiling"; - internal const string CastFunctionName = "cast"; - internal const string IsofFunctionName = "isof"; - internal const string DateFunctionName = "date"; - internal const string TimeFunctionName = "time"; - internal const string NowFunctionName = "now"; - - // string functions - public static readonly MethodInfo StartsWith = MethodOf(_ => _defaultString.StartsWith(default(string))); - public static readonly MethodInfo EndsWith = MethodOf(_ => _defaultString.EndsWith(default(string))); - public static readonly MethodInfo Contains = MethodOf(_ => _defaultString.Contains(default(string))); - public static readonly MethodInfo SubstringStart = MethodOf(_ => _defaultString.Substring(default(int))); - public static readonly MethodInfo SubstringStartAndLength = MethodOf(_ => _defaultString.Substring(default(int), default(int))); - public static readonly MethodInfo SubstringStartNoThrow = MethodOf(_ => ClrSafeFunctions.SubstringStart(default(string), default(int))); - public static readonly MethodInfo SubstringStartAndLengthNoThrow = MethodOf(_ => ClrSafeFunctions.SubstringStartAndLength(default(string), default(int), default(int))); - public static readonly MethodInfo IndexOf = MethodOf(_ => _defaultString.IndexOf(default(string))); - public static readonly MethodInfo ToLower = MethodOf(_ => _defaultString.ToLower()); - public static readonly MethodInfo ToUpper = MethodOf(_ => _defaultString.ToUpper()); - public static readonly MethodInfo Trim = MethodOf(_ => _defaultString.Trim()); - public static readonly MethodInfo Concat = MethodOf(_ => String.Concat(default(string), default(string))); - public static readonly MethodInfo MatchesMattern = MethodOf(_ => Regex.IsMatch(default(string), default(string), default(RegexOptions))); - - // math functions - public static readonly MethodInfo CeilingOfDouble = MethodOf(_ => Math.Ceiling(default(double))); - public static readonly MethodInfo RoundOfDouble = MethodOf(_ => Math.Round(default(double))); - public static readonly MethodInfo FloorOfDouble = MethodOf(_ => Math.Floor(default(double))); - - public static readonly MethodInfo CeilingOfDecimal = MethodOf(_ => Math.Ceiling(default(decimal))); - public static readonly MethodInfo RoundOfDecimal = MethodOf(_ => Math.Round(default(decimal))); - public static readonly MethodInfo FloorOfDecimal = MethodOf(_ => Math.Floor(default(decimal))); - - // enum functions - public static readonly MethodInfo HasFlag = MethodOf(_ => _defaultEnum.HasFlag(default(Enum))); - - // Date properties - public static readonly Dictionary DateProperties = new[] - { - new KeyValuePair(YearFunctionName, typeof(Date).GetProperty("Year")), - new KeyValuePair(MonthFunctionName, typeof(Date).GetProperty("Month")), - new KeyValuePair(DayFunctionName, typeof(Date).GetProperty("Day")), - }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - - // DateTimeproperties - public static readonly Dictionary DateTimeProperties = new[] - { - new KeyValuePair(YearFunctionName, typeof(DateTime).GetProperty("Year")), - new KeyValuePair(MonthFunctionName, typeof(DateTime).GetProperty("Month")), - new KeyValuePair(DayFunctionName, typeof(DateTime).GetProperty("Day")), - new KeyValuePair(HourFunctionName, typeof(DateTime).GetProperty("Hour")), - new KeyValuePair(MinuteFunctionName, typeof(DateTime).GetProperty("Minute")), - new KeyValuePair(SecondFunctionName, typeof(DateTime).GetProperty("Second")), - new KeyValuePair(MillisecondFunctionName, typeof(DateTime).GetProperty("Millisecond")), - }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - - // DateTimeOffset properties - public static readonly Dictionary DateTimeOffsetProperties = new[] - { - new KeyValuePair(YearFunctionName, typeof(DateTimeOffset).GetProperty("Year")), - new KeyValuePair(MonthFunctionName, typeof(DateTimeOffset).GetProperty("Month")), - new KeyValuePair(DayFunctionName, typeof(DateTimeOffset).GetProperty("Day")), - new KeyValuePair(HourFunctionName, typeof(DateTimeOffset).GetProperty("Hour")), - new KeyValuePair(MinuteFunctionName, typeof(DateTimeOffset).GetProperty("Minute")), - new KeyValuePair(SecondFunctionName, typeof(DateTimeOffset).GetProperty("Second")), - new KeyValuePair(MillisecondFunctionName, typeof(DateTimeOffset).GetProperty("Millisecond")), - }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - - // TimeOfDay properties - // ODL uses the Hour(s), Minute(s), Second(s), It's the wrong property name. It should be Hour, Minute, Second. - public static readonly Dictionary TimeOfDayProperties = new[] - { - new KeyValuePair(HourFunctionName, typeof(TimeOfDay).GetProperty("Hours")), - new KeyValuePair(MinuteFunctionName, typeof(TimeOfDay).GetProperty("Minutes")), - new KeyValuePair(SecondFunctionName, typeof(TimeOfDay).GetProperty("Seconds")), - new KeyValuePair(MillisecondFunctionName, typeof(TimeOfDay).GetProperty("Milliseconds")), - }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - - // DateOnly properties - public static readonly Dictionary DateOnlyProperties = new Dictionary - { - { YearFunctionName, typeof(DateOnly).GetProperty(nameof(DateOnly.Year)) }, - { MonthFunctionName, typeof(DateOnly).GetProperty(nameof(DateOnly.Month)) }, - { DayFunctionName, typeof(DateOnly).GetProperty(nameof(DateOnly.Day)) } - }; - - // TimeOnly properties - public static readonly Dictionary TimeOnlyProperties = new Dictionary - { - { HourFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Hour)) }, - { MinuteFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Minute)) }, - { SecondFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Second)) }, - { MillisecondFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Millisecond)) } - }; - - // TimeSpan properties - public static readonly Dictionary TimeSpanProperties = new[] - { - new KeyValuePair(HourFunctionName, typeof(TimeSpan).GetProperty("Hours")), - new KeyValuePair(MinuteFunctionName, typeof(TimeSpan).GetProperty("Minutes")), - new KeyValuePair(SecondFunctionName, typeof(TimeSpan).GetProperty("Seconds")), - new KeyValuePair(MillisecondFunctionName, typeof(TimeSpan).GetProperty("Milliseconds")), - }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - - // String Properties - public static readonly PropertyInfo Length = typeof(string).GetProperty("Length"); - - // PropertyInfo and MethodInfo of DateTime & DateTimeOffset related. - public static readonly PropertyInfo DateTimeKindPropertyInfo = typeof(DateTime).GetProperty("Kind"); - public static readonly MethodInfo ToUniversalTimeDateTime = typeof(DateTime).GetMethod("ToUniversalTime", BindingFlags.Instance | BindingFlags.Public); - public static readonly MethodInfo ToUniversalTimeDateTimeOffset = typeof(DateTimeOffset).GetMethod("ToUniversalTime", BindingFlags.Instance | BindingFlags.Public); - public static readonly MethodInfo ToOffsetFunction = typeof(DateTimeOffset).GetMethod("ToOffset", BindingFlags.Instance | BindingFlags.Public); - public static readonly MethodInfo GetUtcOffset = typeof(TimeZoneInfo).GetMethod("GetUtcOffset", new[] { typeof(DateTime) }); - - private static MethodInfo MethodOf(Expression> expression) - { - return MethodOf(expression as Expression); - } - - private static MethodInfo MethodOf(Expression expression) - { - LambdaExpression lambdaExpression = expression as LambdaExpression; - Contract.Assert(lambdaExpression != null); - Contract.Assert(expression.NodeType == ExpressionType.Lambda); - Contract.Assert(lambdaExpression.Body.NodeType == ExpressionType.Call); - return (lambdaExpression.Body as MethodCallExpression).Method; - } + LambdaExpression lambdaExpression = expression as LambdaExpression; + Contract.Assert(lambdaExpression != null); + Contract.Assert(expression.NodeType == ExpressionType.Lambda); + Contract.Assert(lambdaExpression.Body.NodeType == ExpressionType.Call); + return (lambdaExpression.Body as MethodCallExpression).Method; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/ClrSafeFunctions.cs b/src/Microsoft.AspNetCore.OData/Query/ClrSafeFunctions.cs index 65c38a4f2..b80d5d464 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ClrSafeFunctions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ClrSafeFunctions.cs @@ -8,50 +8,49 @@ using System; using System.Diagnostics.Contracts; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This class contains safe equivalents of CLR functions that +/// could throw exceptions at runtime. +/// +internal class ClrSafeFunctions { - /// - /// This class contains safe equivalents of CLR functions that - /// could throw exceptions at runtime. - /// - internal class ClrSafeFunctions + public static string SubstringStart(string str, int startIndex) { - public static string SubstringStart(string str, int startIndex) - { - Contract.Assert(str != null); - - if (startIndex < 0) - { - startIndex = 0; - } + Contract.Assert(str != null); - // String.Substring(int) accepts startIndex==length - return startIndex <= str.Length - ? str.Substring(startIndex) - : String.Empty; + if (startIndex < 0) + { + startIndex = 0; } - public static string SubstringStartAndLength(string str, int startIndex, int length) - { - Contract.Assert(str != null); + // String.Substring(int) accepts startIndex==length + return startIndex <= str.Length + ? str.Substring(startIndex) + : String.Empty; + } - if (startIndex < 0) - { - startIndex = 0; - } + public static string SubstringStartAndLength(string str, int startIndex, int length) + { + Contract.Assert(str != null); - int strLength = str.Length; + if (startIndex < 0) + { + startIndex = 0; + } - // String.Substring(int, int) accepts startIndex==length - if (startIndex > strLength) - { - return String.Empty; - } + int strLength = str.Length; - length = Math.Min(length, strLength - startIndex); - return length >= 0 - ? str.Substring(startIndex, length) - : String.Empty; + // String.Substring(int, int) accepts startIndex==length + if (startIndex > strLength) + { + return String.Empty; } + + length = Math.Min(length, strLength - startIndex); + return length >= 0 + ? str.Substring(startIndex, length) + : String.Empty; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/AggregationPropertyContainer.cs b/src/Microsoft.AspNetCore.OData/Query/Container/AggregationPropertyContainer.cs index 7e59b0c08..313506b0c 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/AggregationPropertyContainer.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/AggregationPropertyContainer.cs @@ -11,155 +11,154 @@ using System.Linq.Expressions; using Microsoft.AspNetCore.OData.Query.Wrapper; -namespace Microsoft.AspNetCore.OData.Query.Container +namespace Microsoft.AspNetCore.OData.Query.Container; + +/// +/// Represent properties used in groupby and aggregate clauses to make them accessible in further clauses/transformations +/// +/// +/// When we have $apply=groupby((Prop1,Prop2, Prop3))&$orderby=Prop1, Prop2 +/// We will have following expression in .GroupBy +/// $it => new AggregationPropertyContainer() { +/// Name = "Prop1", +/// Value = $it.Prop1, // string +/// Next = new AggregationPropertyContainer() { +/// Name = "Prop2", +/// Value = $it.Prop2, // int +/// Next = new LastInChain() { +/// Name = "Prop3", +/// Value = $it.Prop3 +/// } +/// } +/// } +/// when in $orderby (see AggregationBinder CollectProperties method) +/// Prop1 could be referenced us $it => (string)$it.Value +/// Prop2 could be referenced us $it => (int)$it.Next.Value +/// Prop3 could be referenced us $it => (int)$it.Next.Next.Value +/// Generic type for Value is used to avoid type casts for on primitive types that not supported in EF +/// +/// Also we have 4 use cases and base type have all required properties to support no cast usage. +/// 1. Primitive property with Next +/// 2. Primitive property without Next +/// 3. Nested property with Next +/// 4. Nested property without Next +/// However, EF doesn't allow to set different properties for the same type in two places in an lambda-expression => using new type with just new name to workaround that issue +/// +/// +internal class AggregationPropertyContainer : NamedProperty { - /// - /// Represent properties used in groupby and aggregate clauses to make them accessible in further clauses/transformations - /// - /// - /// When we have $apply=groupby((Prop1,Prop2, Prop3))&$orderby=Prop1, Prop2 - /// We will have following expression in .GroupBy - /// $it => new AggregationPropertyContainer() { - /// Name = "Prop1", - /// Value = $it.Prop1, // string - /// Next = new AggregationPropertyContainer() { - /// Name = "Prop2", - /// Value = $it.Prop2, // int - /// Next = new LastInChain() { - /// Name = "Prop3", - /// Value = $it.Prop3 - /// } - /// } - /// } - /// when in $orderby (see AggregationBinder CollectProperties method) - /// Prop1 could be referenced us $it => (string)$it.Value - /// Prop2 could be referenced us $it => (int)$it.Next.Value - /// Prop3 could be referenced us $it => (int)$it.Next.Next.Value - /// Generic type for Value is used to avoid type casts for on primitive types that not supported in EF - /// - /// Also we have 4 use cases and base type have all required properties to support no cast usage. - /// 1. Primitive property with Next - /// 2. Primitive property without Next - /// 3. Nested property with Next - /// 4. Nested property without Next - /// However, EF doesn't allow to set different properties for the same type in two places in an lambda-expression => using new type with just new name to workaround that issue - /// - /// - internal class AggregationPropertyContainer : NamedProperty + public GroupByWrapper NestedValue { - public GroupByWrapper NestedValue + get { - get - { - return (GroupByWrapper)this.Value; - } - set - { - Value = value; - } + return (GroupByWrapper)this.Value; } - - public AggregationPropertyContainer Next { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) + set { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - if (Next != null) - { - Next.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + Value = value; } + } - public override object GetValue() - { - // Value is object and when Value is populated form the DB by EF or other ORM, it will not auto converted to null as in case of real type - if (Value == DBNull.Value) - { - return null; - } - - return base.GetValue(); - } + public AggregationPropertyContainer Next { get; set; } - private class LastInChain : AggregationPropertyContainer + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + if (Next != null) { + Next.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } + } - private class NestedPropertyLastInChain : AggregationPropertyContainer + public override object GetValue() + { + // Value is object and when Value is populated form the DB by EF or other ORM, it will not auto converted to null as in case of real type + if (Value == DBNull.Value) { + return null; } - private class NestedProperty : AggregationPropertyContainer - { - } + return base.GetValue(); + } - public static Expression CreateNextNamedPropertyContainer(IList properties) - { - Expression container = null; + private class LastInChain : AggregationPropertyContainer + { + } - // build the linked list of properties. - foreach (NamedPropertyExpression property in properties) - { - container = CreateNextNamedPropertyCreationExpression(property, container); - } + private class NestedPropertyLastInChain : AggregationPropertyContainer + { + } - return container; - } + private class NestedProperty : AggregationPropertyContainer + { + } - private static Expression CreateNextNamedPropertyCreationExpression(NamedPropertyExpression property, Expression next) + public static Expression CreateNextNamedPropertyContainer(IList properties) + { + Expression container = null; + + // build the linked list of properties. + foreach (NamedPropertyExpression property in properties) { - Contract.Assert(property != null); - Contract.Assert(property.Value != null); + container = CreateNextNamedPropertyCreationExpression(property, container); + } + + return container; + } + + private static Expression CreateNextNamedPropertyCreationExpression(NamedPropertyExpression property, Expression next) + { + Contract.Assert(property != null); + Contract.Assert(property.Value != null); - Type namedPropertyType = null; - if (next != null) + Type namedPropertyType = null; + if (next != null) + { + if (property.Value.Type == typeof(GroupByWrapper)) { - if (property.Value.Type == typeof(GroupByWrapper)) - { - namedPropertyType = typeof(NestedProperty); - } - else - { - namedPropertyType = typeof(AggregationPropertyContainer); - } + namedPropertyType = typeof(NestedProperty); } else { - if (property.Value.Type == typeof(GroupByWrapper)) - { - namedPropertyType = typeof(NestedPropertyLastInChain); - } - else - { - namedPropertyType = typeof(LastInChain); - } + namedPropertyType = typeof(AggregationPropertyContainer); } - - List memberBindings = new List(); - - memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Name"), property.Name)); - + } + else + { if (property.Value.Type == typeof(GroupByWrapper)) { - memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("NestedValue"), property.Value)); + namedPropertyType = typeof(NestedPropertyLastInChain); } else { - memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Value"), property.Value)); + namedPropertyType = typeof(LastInChain); } + } - if (next != null) - { - memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Next"), next)); - } + List memberBindings = new List(); - if (property.NullCheck != null) - { - memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("IsNull"), property.NullCheck)); - } + memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Name"), property.Name)); + + if (property.Value.Type == typeof(GroupByWrapper)) + { + memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("NestedValue"), property.Value)); + } + else + { + memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Value"), property.Value)); + } - return Expression.MemberInit(Expression.New(namedPropertyType), memberBindings); + if (next != null) + { + memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Next"), next)); } + + if (property.NullCheck != null) + { + memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("IsNull"), property.NullCheck)); + } + + return Expression.MemberInit(Expression.New(namedPropertyType), memberBindings); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/AutoSelectedNamedPropertyOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Container/AutoSelectedNamedPropertyOfT.cs index 318d88df7..bcbf58ef4 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/AutoSelectedNamedPropertyOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/AutoSelectedNamedPropertyOfT.cs @@ -5,13 +5,12 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Container +namespace Microsoft.AspNetCore.OData.Query.Container; + +internal class AutoSelectedNamedProperty : NamedProperty { - internal class AutoSelectedNamedProperty : NamedProperty + public AutoSelectedNamedProperty() { - public AutoSelectedNamedProperty() - { - AutoSelected = true; - } + AutoSelected = true; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/CollectionExpandedPropertyOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Container/CollectionExpandedPropertyOfT.cs index 2b83f5f5e..428bd308c 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/CollectionExpandedPropertyOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/CollectionExpandedPropertyOfT.cs @@ -7,31 +7,30 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.Query.Container +namespace Microsoft.AspNetCore.OData.Query.Container; + +internal class CollectionExpandedProperty : NamedProperty { - internal class CollectionExpandedProperty : NamedProperty - { - public int PageSize { get; set; } + public int PageSize { get; set; } - public long? TotalCount { get; set; } + public long? TotalCount { get; set; } - public IEnumerable Collection { get; set; } + public IEnumerable Collection { get; set; } - public override object GetValue() + public override object GetValue() + { + if (Collection == null) { - if (Collection == null) - { - return null; - } + return null; + } - if (TotalCount == null) - { - return new TruncatedCollection(Collection, PageSize); - } - else - { - return new TruncatedCollection(Collection, PageSize, TotalCount); - } + if (TotalCount == null) + { + return new TruncatedCollection(Collection, PageSize); + } + else + { + return new TruncatedCollection(Collection, PageSize, TotalCount); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/IPropertyMapper.cs b/src/Microsoft.AspNetCore.OData/Query/Container/IPropertyMapper.cs index 2ad722ac7..4d9ede9c6 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/IPropertyMapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/IPropertyMapper.cs @@ -10,38 +10,37 @@ using Microsoft.AspNetCore.OData.Query.Wrapper; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Query.Container +namespace Microsoft.AspNetCore.OData.Query.Container; + +/// +/// The result of a $select and $expand projection is represented as an +/// instance. That instance can be projected into an instance by calling +/// . +/// That method will use the function to construct an that will map the property +/// names in that projection to the keys in the returned . +/// The main purpose of converting an instance into an +/// (using the method mentioned above) is to allow changing the names of the +/// properties in the that will be used during the serialization of the $select +/// and $expand projection by a given formatter. For example, to support custom serialization attributes of a +/// particular formatter. +/// +/// It also allows you to ignore a field (ensure that the returned +/// does not have a key for that field), by mapping the property name to null. +/// +public interface IPropertyMapper { /// - /// The result of a $select and $expand projection is represented as an - /// instance. That instance can be projected into an instance by calling - /// . - /// That method will use the function to construct an that will map the property - /// names in that projection to the keys in the returned . - /// The main purpose of converting an instance into an - /// (using the method mentioned above) is to allow changing the names of the - /// properties in the that will be used during the serialization of the $select - /// and $expand projection by a given formatter. For example, to support custom serialization attributes of a - /// particular formatter. - /// - /// It also allows you to ignore a field (ensure that the returned - /// does not have a key for that field), by mapping the property name to null. + /// Defines a mapping between the name of an of an + /// and the name that should be used in other contexts, for example, when projecting an instance of an + /// into an instance of an /// - public interface IPropertyMapper - { - /// - /// Defines a mapping between the name of an of an - /// and the name that should be used in other contexts, for example, when projecting an instance of an - /// into an instance of an - /// - /// - /// The name of the property in the represented - /// by this instance of . - /// - /// - /// The value that will be used as the key for this property in the - /// resulting from calling ToDictionary on an instance. - /// - string MapProperty(string propertyName); - } + /// + /// The name of the property in the represented + /// by this instance of . + /// + /// + /// The value that will be used as the key for this property in the + /// resulting from calling ToDictionary on an instance. + /// + string MapProperty(string propertyName); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/ITruncatedCollection.cs b/src/Microsoft.AspNetCore.OData/Query/Container/ITruncatedCollection.cs index d7dc49a7c..203b98294 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/ITruncatedCollection.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/ITruncatedCollection.cs @@ -8,22 +8,21 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.AspNetCore.OData.Query.Container +namespace Microsoft.AspNetCore.OData.Query.Container; + +/// +/// Represents a collection that is truncated to a given page size. +/// +[SuppressMessage("Design", "CA1010:Collections should implement generic interface", Justification = "")] +public interface ITruncatedCollection : IEnumerable { /// - /// Represents a collection that is truncated to a given page size. + /// Gets the page size the collection is truncated to. /// - [SuppressMessage("Design", "CA1010:Collections should implement generic interface", Justification = "")] - public interface ITruncatedCollection : IEnumerable - { - /// - /// Gets the page size the collection is truncated to. - /// - int PageSize { get; } + int PageSize { get; } - /// - /// Gets a value representing if the collection is truncated or not. - /// - bool IsTruncated { get; } - } + /// + /// Gets a value representing if the collection is truncated or not. + /// + bool IsTruncated { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/IdentityPropertyMapper.cs b/src/Microsoft.AspNetCore.OData/Query/Container/IdentityPropertyMapper.cs index 81a7528f3..ac669132c 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/IdentityPropertyMapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/IdentityPropertyMapper.cs @@ -5,13 +5,12 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Container +namespace Microsoft.AspNetCore.OData.Query.Container; + +internal class IdentityPropertyMapper : IPropertyMapper { - internal class IdentityPropertyMapper : IPropertyMapper + public string MapProperty(string propertyName) { - public string MapProperty(string propertyName) - { - return propertyName; - } + return propertyName; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/JsonPropertyNameMapper.cs b/src/Microsoft.AspNetCore.OData/Query/Container/JsonPropertyNameMapper.cs index bb23dff72..b5a622ed3 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/JsonPropertyNameMapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/JsonPropertyNameMapper.cs @@ -13,75 +13,74 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Query.Container +namespace Microsoft.AspNetCore.OData.Query.Container; + +internal class JsonPropertyNameMapper : IPropertyMapper { - internal class JsonPropertyNameMapper : IPropertyMapper + private IEdmModel _model; + private IEdmStructuredType _type; + + public JsonPropertyNameMapper(IEdmModel model, IEdmStructuredType type) { - private IEdmModel _model; - private IEdmStructuredType _type; + _model = model; + _type = type; + } - public JsonPropertyNameMapper(IEdmModel model, IEdmStructuredType type) + public string MapProperty(string propertyName) + { + IEdmProperty property = _type.Properties().FirstOrDefault(s => s.Name == propertyName); + if (property == null) { - _model = model; - _type = type; + // If we can't find a property on the Edm type, it could be a dynamic property. + // We should simply return the property name. + return propertyName; } - public string MapProperty(string propertyName) - { - IEdmProperty property = _type.Properties().FirstOrDefault(s => s.Name == propertyName); - if (property == null) - { - // If we can't find a property on the Edm type, it could be a dynamic property. - // We should simply return the property name. - return propertyName; - } - - PropertyInfo info = GetPropertyInfo(property); + PropertyInfo info = GetPropertyInfo(property); - JsonIgnoreAttribute jsonIgnore = GetJsonIgnore(info); - if (jsonIgnore != null) - { - return null; - } - - JsonPropertyNameAttribute jsonProperty = GetJsonProperty(info); - if (jsonProperty != null && !String.IsNullOrWhiteSpace(jsonProperty.Name)) - { - return jsonProperty.Name; - } - else - { - return property.Name; - } + JsonIgnoreAttribute jsonIgnore = GetJsonIgnore(info); + if (jsonIgnore != null) + { + return null; } - private PropertyInfo GetPropertyInfo(IEdmProperty property) + JsonPropertyNameAttribute jsonProperty = GetJsonProperty(info); + if (jsonProperty != null && !String.IsNullOrWhiteSpace(jsonProperty.Name)) { - ClrPropertyInfoAnnotation clrPropertyAnnotation = _model.GetAnnotationValue(property); - if (clrPropertyAnnotation != null) - { - return clrPropertyAnnotation.ClrPropertyInfo; - } - - ClrTypeAnnotation clrTypeAnnotation = _model.GetAnnotationValue(property.DeclaringType); - Contract.Assert(clrTypeAnnotation != null); - - PropertyInfo info = clrTypeAnnotation.ClrType.GetProperty(property.Name); - Contract.Assert(info != null); - - return info; + return jsonProperty.Name; } - - private static JsonPropertyNameAttribute GetJsonProperty(PropertyInfo property) + else { - return property.GetCustomAttributes(typeof(JsonPropertyNameAttribute), inherit: false) - .OfType().SingleOrDefault(); + return property.Name; } + } - private static JsonIgnoreAttribute GetJsonIgnore(PropertyInfo property) + private PropertyInfo GetPropertyInfo(IEdmProperty property) + { + ClrPropertyInfoAnnotation clrPropertyAnnotation = _model.GetAnnotationValue(property); + if (clrPropertyAnnotation != null) { - return property.GetCustomAttributes(typeof(JsonIgnoreAttribute), inherit: false) - .OfType().SingleOrDefault(); + return clrPropertyAnnotation.ClrPropertyInfo; } + + ClrTypeAnnotation clrTypeAnnotation = _model.GetAnnotationValue(property.DeclaringType); + Contract.Assert(clrTypeAnnotation != null); + + PropertyInfo info = clrTypeAnnotation.ClrType.GetProperty(property.Name); + Contract.Assert(info != null); + + return info; + } + + private static JsonPropertyNameAttribute GetJsonProperty(PropertyInfo property) + { + return property.GetCustomAttributes(typeof(JsonPropertyNameAttribute), inherit: false) + .OfType().SingleOrDefault(); + } + + private static JsonIgnoreAttribute GetJsonIgnore(PropertyInfo property) + { + return property.GetCustomAttributes(typeof(JsonIgnoreAttribute), inherit: false) + .OfType().SingleOrDefault(); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/LinqParameterContainer.cs b/src/Microsoft.AspNetCore.OData/Query/Container/LinqParameterContainer.cs index fb9258191..73281e039 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/LinqParameterContainer.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/LinqParameterContainer.cs @@ -10,62 +10,61 @@ using System.Linq.Expressions; using System.Reflection; -namespace Microsoft.AspNetCore.OData.Query.Container +namespace Microsoft.AspNetCore.OData.Query.Container; + +// wraps a constant value so that EntityFramework parametrizes the constant. +internal abstract class LinqParameterContainer { - // wraps a constant value so that EntityFramework parametrizes the constant. - internal abstract class LinqParameterContainer - { - private static ConcurrentDictionary> _ctors = new ConcurrentDictionary>(); + private static ConcurrentDictionary> _ctors = new ConcurrentDictionary>(); - // the value of the constant. - public abstract object Property { get; } + // the value of the constant. + public abstract object Property { get; } - public static Expression Parameterize(Type type, object value) - { - // () => new LinqParameterContainer(constant).Property - // instead of returning a constant expression node, wrap that constant in a class the way compiler - // does a closure, so that EF can parameterize the constant (resulting in better performance due to expression translation caching). - LinqParameterContainer containedValue = LinqParameterContainer.Create(type, value); - return Expression.Property(Expression.Constant(containedValue), "TypedProperty"); - } + public static Expression Parameterize(Type type, object value) + { + // () => new LinqParameterContainer(constant).Property + // instead of returning a constant expression node, wrap that constant in a class the way compiler + // does a closure, so that EF can parameterize the constant (resulting in better performance due to expression translation caching). + LinqParameterContainer containedValue = LinqParameterContainer.Create(type, value); + return Expression.Property(Expression.Constant(containedValue), "TypedProperty"); + } - private static LinqParameterContainer Create(Type type, object value) + private static LinqParameterContainer Create(Type type, object value) + { + return _ctors.GetOrAdd(type, t => { - return _ctors.GetOrAdd(type, t => - { - MethodInfo createMethod = typeof(LinqParameterContainer).GetMethod("CreateInternal").MakeGenericMethod(t); - ParameterExpression valueParameter = Expression.Parameter(typeof(object)); - return - Expression.Lambda>( - Expression.Call( - createMethod, - Expression.Convert(valueParameter, t)), - valueParameter) - .Compile(); - })(value); - } + MethodInfo createMethod = typeof(LinqParameterContainer).GetMethod("CreateInternal").MakeGenericMethod(t); + ParameterExpression valueParameter = Expression.Parameter(typeof(object)); + return + Expression.Lambda>( + Expression.Call( + createMethod, + Expression.Convert(valueParameter, t)), + valueParameter) + .Compile(); + })(value); + } - // invoked dynamically at runtime. - public static LinqParameterContainer CreateInternal(T value) - { - return new TypedLinqParameterContainer(value); - } + // invoked dynamically at runtime. + public static LinqParameterContainer CreateInternal(T value) + { + return new TypedLinqParameterContainer(value); + } - // having a strongly typed property avoids the a cast in the property access expression that would be - // generated for this constant. - internal class TypedLinqParameterContainer : LinqParameterContainer + // having a strongly typed property avoids the a cast in the property access expression that would be + // generated for this constant. + internal class TypedLinqParameterContainer : LinqParameterContainer + { + public TypedLinqParameterContainer(T value) { - public TypedLinqParameterContainer(T value) - { - TypedProperty = value; - } + TypedProperty = value; + } - public T TypedProperty { get; set; } + public T TypedProperty { get; set; } - public override object Property - { - get { return TypedProperty; } - } + public override object Property + { + get { return TypedProperty; } } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/NamedPropertyExpression.cs b/src/Microsoft.AspNetCore.OData/Query/Container/NamedPropertyExpression.cs index 0a955fcf8..22cb947f6 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/NamedPropertyExpression.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/NamedPropertyExpression.cs @@ -8,72 +8,71 @@ using System.Diagnostics.Contracts; using System.Linq.Expressions; -namespace Microsoft.AspNetCore.OData.Query.Container +namespace Microsoft.AspNetCore.OData.Query.Container; + +/// +/// Represents a container that captures a named property that is a part of the select expand query. +/// +public class NamedPropertyExpression { /// - /// Represents a container that captures a named property that is a part of the select expand query. + /// Initializes a new instance of the class. /// - public class NamedPropertyExpression + /// Property name expression. + /// Property value expression. + public NamedPropertyExpression(Expression name, Expression value) { - /// - /// Initializes a new instance of the class. - /// - /// Property name expression. - /// Property value expression. - public NamedPropertyExpression(Expression name, Expression value) - { - Contract.Assert(name != null); - Contract.Assert(value != null); + Contract.Assert(name != null); + Contract.Assert(value != null); - Name = name; - Value = value; - } + Name = name; + Value = value; + } - /// - /// Property name expression. - /// - public Expression Name { get; private set; } + /// + /// Property name expression. + /// + public Expression Name { get; private set; } - /// - /// Property value expression. - /// - public Expression Value { get; private set; } + /// + /// Property value expression. + /// + public Expression Value { get; private set; } - /// - /// The total count expression. - /// - public Expression TotalCount { get; set; } + /// + /// The total count expression. + /// + public Expression TotalCount { get; set; } - // Checks whether this property is null or not. This is required for expanded navigation properties that are null as entityframework cannot - // create null's of type SelectExpandWrapper i.e. an expression like - // => new NamedProperty { Value = order.Customer == null : null : new SelectExpandWrapper { .... } } - // cannot be translated by EF. So, we generate the following expression instead, - // => new ExpandProperty { Value = new SelectExpandWrapper { .... }, IsNull = nullCheck } - // and use Value only if IsNull is false. - /// - /// The null check expression. - /// - public Expression NullCheck { get; set; } + // Checks whether this property is null or not. This is required for expanded navigation properties that are null as entityframework cannot + // create null's of type SelectExpandWrapper i.e. an expression like + // => new NamedProperty { Value = order.Customer == null : null : new SelectExpandWrapper { .... } } + // cannot be translated by EF. So, we generate the following expression instead, + // => new ExpandProperty { Value = new SelectExpandWrapper { .... }, IsNull = nullCheck } + // and use Value only if IsNull is false. + /// + /// The null check expression. + /// + public Expression NullCheck { get; set; } - /// - /// The page size value. - /// - public int? PageSize { get; set; } + /// + /// The page size value. + /// + public int? PageSize { get; set; } - // Option that indicates if we are creating this expression - // Next = new AutoSelectedNamedProperty() - // { - // Name = "...", - // Value = ... - // } - /// - /// The bool option to indicate if we are creating an AutoSelectedNamedProperty expression. - /// - public bool AutoSelected { get; set; } + // Option that indicates if we are creating this expression + // Next = new AutoSelectedNamedProperty() + // { + // Name = "...", + // Value = ... + // } + /// + /// The bool option to indicate if we are creating an AutoSelectedNamedProperty expression. + /// + public bool AutoSelected { get; set; } - /// - /// Bool option that indicates if we have count value. - /// - public bool? CountOption { get; set; } - } + /// + /// Bool option that indicates if we have count value. + /// + public bool? CountOption { get; set; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/NamedPropertyOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Container/NamedPropertyOfT.cs index a8de825cd..b2472c54f 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/NamedPropertyOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/NamedPropertyOfT.cs @@ -9,39 +9,38 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; -namespace Microsoft.AspNetCore.OData.Query.Container +namespace Microsoft.AspNetCore.OData.Query.Container; + +internal class NamedProperty : PropertyContainer { - internal class NamedProperty : PropertyContainer - { - public string Name { get; set; } + public string Name { get; set; } - public T Value { get; set; } + public T Value { get; set; } - public bool AutoSelected { get; set; } + public bool AutoSelected { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - Contract.Assert(dictionary != null); + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + Contract.Assert(dictionary != null); - if (Name != null && (includeAutoSelected || !AutoSelected)) + if (Name != null && (includeAutoSelected || !AutoSelected)) + { + string mappedName = propertyMapper.MapProperty(Name); + if (mappedName != null) { - string mappedName = propertyMapper.MapProperty(Name); - if (mappedName != null) + if (String.IsNullOrEmpty(mappedName)) { - if (String.IsNullOrEmpty(mappedName)) - { - throw Error.InvalidOperation(SRResources.InvalidPropertyMapping, Name); - } - - dictionary.Add(mappedName, GetValue()); + throw Error.InvalidOperation(SRResources.InvalidPropertyMapping, Name); } + + dictionary.Add(mappedName, GetValue()); } } + } - public virtual object GetValue() - { - return Value; - } + public virtual object GetValue() + { + return Value; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/PropertyContainer.cs b/src/Microsoft.AspNetCore.OData/Query/Container/PropertyContainer.cs index 31df61571..ad2fc70b5 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/PropertyContainer.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/PropertyContainer.cs @@ -14,163 +14,162 @@ using System.Linq.Expressions; using Microsoft.AspNetCore.OData.Common; -namespace Microsoft.AspNetCore.OData.Query.Container +namespace Microsoft.AspNetCore.OData.Query.Container; + +/// +/// A container of property names and property values. +/// +/// +/// EntityFramework understands only member initializations in Select expressions. Also, it doesn't understand type casts for non-primitive types. So, +/// SelectExpandBinder has to generate strongly types expressions that involve only property access. This class represents the base class for a bunch of +/// generic derived types that are used in the expressions that SelectExpandBinder generates. +/// Also, Expression.Compile() could fail with stack overflow if expression is to deep and causes too many levels of recursion. To avoid that we are b-tree property container. +/// +[ExcludeFromCodeCoverage] +internal abstract partial class PropertyContainer { /// - /// A container of property names and property values. + /// Initializes a new instance of the class. /// - /// - /// EntityFramework understands only member initializations in Select expressions. Also, it doesn't understand type casts for non-primitive types. So, - /// SelectExpandBinder has to generate strongly types expressions that involve only property access. This class represents the base class for a bunch of - /// generic derived types that are used in the expressions that SelectExpandBinder generates. - /// Also, Expression.Compile() could fail with stack overflow if expression is to deep and causes too many levels of recursion. To avoid that we are b-tree property container. - /// - [ExcludeFromCodeCoverage] - internal abstract partial class PropertyContainer + protected PropertyContainer() { - /// - /// Initializes a new instance of the class. - /// - protected PropertyContainer() - { - } + } - /// - /// Builds the dictionary of properties in this container keyed by the property name. - /// - /// The dictionary of properties in this container keyed by the property name. - public Dictionary ToDictionary(IPropertyMapper propertyMapper, bool includeAutoSelected = true) - { - Contract.Assert(propertyMapper != null); - Dictionary result = new Dictionary(); - ToDictionaryCore(result, propertyMapper, includeAutoSelected); - return result; - } + /// + /// Builds the dictionary of properties in this container keyed by the property name. + /// + /// The dictionary of properties in this container keyed by the property name. + public Dictionary ToDictionary(IPropertyMapper propertyMapper, bool includeAutoSelected = true) + { + Contract.Assert(propertyMapper != null); + Dictionary result = new Dictionary(); + ToDictionaryCore(result, propertyMapper, includeAutoSelected); + return result; + } - /// - /// Adds the properties in this container to the given dictionary. - /// - /// The dictionary to add the properties to. - /// Specifies whether auto selected properties should be included. - /// An object responsible to map the properties in this property container to the - /// the value that will be used as the key in the dictionary we are adding properties to. - public abstract void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected); - - // Expression: - // new NamedProperty - // { - // Name = properties[0].Key, - // Value = properties[0].Value, - // - // Next0 = new NamedProperty<> { ..... } - // Next1 = new NamedProperty<> { ..... }, - // ... - // } - public static Expression CreatePropertyContainer(IList properties) - { - Expression container = null; + /// + /// Adds the properties in this container to the given dictionary. + /// + /// The dictionary to add the properties to. + /// Specifies whether auto selected properties should be included. + /// An object responsible to map the properties in this property container to the + /// the value that will be used as the key in the dictionary we are adding properties to. + public abstract void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected); + + // Expression: + // new NamedProperty + // { + // Name = properties[0].Key, + // Value = properties[0].Value, + // + // Next0 = new NamedProperty<> { ..... } + // Next1 = new NamedProperty<> { ..... }, + // ... + // } + public static Expression CreatePropertyContainer(IList properties) + { + Expression container = null; - // build the linked list of properties. - if (properties.Count >= 1) + // build the linked list of properties. + if (properties.Count >= 1) + { + NamedPropertyExpression property = properties.First(); + int count = properties.Count - 1; + List nextExpressions = new List(); + int parts = SingleExpandedPropertyTypes.Count - 1; + int offset = 0; + for (int step = parts; step > 0; step--) { - NamedPropertyExpression property = properties.First(); - int count = properties.Count - 1; - List nextExpressions = new List(); - int parts = SingleExpandedPropertyTypes.Count - 1; - int offset = 0; - for (int step = parts; step > 0; step--) - { - int leftSize = GetLeftSize(count - offset, step); - nextExpressions.Add(CreatePropertyContainer(properties.Skip(1 + offset).Take(leftSize).ToList())); - offset += leftSize; - } - - container = CreateNamedPropertyCreationExpression(property, nextExpressions.Where(e => e != null).ToList()); + int leftSize = GetLeftSize(count - offset, step); + nextExpressions.Add(CreatePropertyContainer(properties.Skip(1 + offset).Take(leftSize).ToList())); + offset += leftSize; } - return container; + container = CreateNamedPropertyCreationExpression(property, nextExpressions.Where(e => e != null).ToList()); } - private static int GetLeftSize(int count, int parts) + return container; + } + + private static int GetLeftSize(int count, int parts) + { + if (count % parts != 0) { - if (count % parts != 0) - { - return (count / parts) + 1; - } - return count / parts; + return (count / parts) + 1; } + return count / parts; + } - // Expression: - // new NamedProperty { Name = property.Name, Value = property.Value, Next0 = next0, Next1 = next1, .... }. - private static Expression CreateNamedPropertyCreationExpression(NamedPropertyExpression property, IList expressions) - { - Contract.Assert(property != null); - Contract.Assert(property.Value != null); + // Expression: + // new NamedProperty { Name = property.Name, Value = property.Value, Next0 = next0, Next1 = next1, .... }. + private static Expression CreateNamedPropertyCreationExpression(NamedPropertyExpression property, IList expressions) + { + Contract.Assert(property != null); + Contract.Assert(property.Value != null); - Type namedPropertyType = GetNamedPropertyType(property, expressions); - List memberBindings = new List(); + Type namedPropertyType = GetNamedPropertyType(property, expressions); + List memberBindings = new List(); - memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Name"), property.Name)); + memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Name"), property.Name)); - if (property.PageSize != null || property.CountOption != null) - { - memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Collection"), property.Value)); - - if (property.PageSize != null) - { - memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("PageSize"), - Expression.Constant(property.PageSize))); - } - - if (property.CountOption != null && property.CountOption.Value) - { - memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("TotalCount"), ExpressionHelpers.ToNullable(property.TotalCount))); - } - } - else - { - memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Value"), property.Value)); - } + if (property.PageSize != null || property.CountOption != null) + { + memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Collection"), property.Value)); - for (int i = 0; i < expressions.Count; i++) + if (property.PageSize != null) { - memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Next" + i.ToString(CultureInfo.CurrentCulture)), expressions[i])); + memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("PageSize"), + Expression.Constant(property.PageSize))); } - if (property.NullCheck != null) + if (property.CountOption != null && property.CountOption.Value) { - memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("IsNull"), property.NullCheck)); + memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("TotalCount"), ExpressionHelpers.ToNullable(property.TotalCount))); } + } + else + { + memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Value"), property.Value)); + } - return Expression.MemberInit(Expression.New(namedPropertyType), memberBindings); + for (int i = 0; i < expressions.Count; i++) + { + memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("Next" + i.ToString(CultureInfo.CurrentCulture)), expressions[i])); } - private static Type GetNamedPropertyType(NamedPropertyExpression property, IList expressions) + if (property.NullCheck != null) { - Type namedPropertyGenericType; + memberBindings.Add(Expression.Bind(namedPropertyType.GetProperty("IsNull"), property.NullCheck)); + } - if (property.NullCheck != null) - { - namedPropertyGenericType = SingleExpandedPropertyTypes[expressions.Count]; - } - else if (property.PageSize != null || property.CountOption != null) - { - namedPropertyGenericType = CollectionExpandedPropertyTypes[expressions.Count]; - } - else if (property.AutoSelected) - { - namedPropertyGenericType = AutoSelectedNamedPropertyTypes[expressions.Count]; - } - else - { - namedPropertyGenericType = NamedPropertyTypes[expressions.Count]; - } + return Expression.MemberInit(Expression.New(namedPropertyType), memberBindings); + } + + private static Type GetNamedPropertyType(NamedPropertyExpression property, IList expressions) + { + Type namedPropertyGenericType; - Type elementType = (property.PageSize == null && property.CountOption == null) - ? property.Value.Type - : TypeHelper.GetInnerElementType(property.Value.Type); - return namedPropertyGenericType.MakeGenericType(elementType); + if (property.NullCheck != null) + { + namedPropertyGenericType = SingleExpandedPropertyTypes[expressions.Count]; + } + else if (property.PageSize != null || property.CountOption != null) + { + namedPropertyGenericType = CollectionExpandedPropertyTypes[expressions.Count]; + } + else if (property.AutoSelected) + { + namedPropertyGenericType = AutoSelectedNamedPropertyTypes[expressions.Count]; } + else + { + namedPropertyGenericType = NamedPropertyTypes[expressions.Count]; + } + + Type elementType = (property.PageSize == null && property.CountOption == null) + ? property.Value.Type + : TypeHelper.GetInnerElementType(property.Value.Type); + return namedPropertyGenericType.MakeGenericType(elementType); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/PropertyContainer.generated.cs b/src/Microsoft.AspNetCore.OData/Query/Container/PropertyContainer.generated.cs index 6adf4442c..f63b3fe76 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/PropertyContainer.generated.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/PropertyContainer.generated.cs @@ -11,1569 +11,1568 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.Query.Container -{ +namespace Microsoft.AspNetCore.OData.Query.Container; + // - internal abstract partial class PropertyContainer +internal abstract partial class PropertyContainer +{ + // Entityframework requires that the two different type initializers for a given type in the same query have the same set of properties in the same order. + // A $select=Prop1,Prop2,Prop3 where Prop1 and Prop2 are of the same type without this extra NamedPropertyWithNext type results in an select expression that looks like, + // c => new NamedProperty { Name = "Prop1", Value = c.Prop1, Next0 = new NamedProperty { Name = "Prop2", Value = c.Prop2 }, Next2 = new NamedProperty { Name = "Prop3", Value = c.Prop3 } }; + // Entityframework cannot translate this expression as the first NamedProperty initialization has Next and the second one doesn't. Also, Entityframework cannot + // create null's of NamedProperty. So, you cannot generate an expression like new NamedProperty { Next = null }. The exception that EF throws looks like this, + // "The type 'NamedProperty`1[SystemInt32...]' appears in two structurally incompatible initializations within a single LINQ to Entities query. + // A type can be initialized in two places in the same query, but only if the same properties are set in both places and those properties are set in the same order." + + private class SingleExpandedPropertyWithNext0 : SingleExpandedProperty { - // Entityframework requires that the two different type initializers for a given type in the same query have the same set of properties in the same order. - // A $select=Prop1,Prop2,Prop3 where Prop1 and Prop2 are of the same type without this extra NamedPropertyWithNext type results in an select expression that looks like, - // c => new NamedProperty { Name = "Prop1", Value = c.Prop1, Next0 = new NamedProperty { Name = "Prop2", Value = c.Prop2 }, Next2 = new NamedProperty { Name = "Prop3", Value = c.Prop3 } }; - // Entityframework cannot translate this expression as the first NamedProperty initialization has Next and the second one doesn't. Also, Entityframework cannot - // create null's of NamedProperty. So, you cannot generate an expression like new NamedProperty { Next = null }. The exception that EF throws looks like this, - // "The type 'NamedProperty`1[SystemInt32...]' appears in two structurally incompatible initializations within a single LINQ to Entities query. - // A type can be initialized in two places in the same query, but only if the same properties are set in both places and those properties are set in the same order." + public PropertyContainer Next0 { get; set; } - private class SingleExpandedPropertyWithNext0 : SingleExpandedProperty + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next0 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next0.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next0.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext1 : SingleExpandedPropertyWithNext0 - { - public PropertyContainer Next1 { get; set; } + } + private class SingleExpandedPropertyWithNext1 : SingleExpandedPropertyWithNext0 + { + public PropertyContainer Next1 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next1.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext2 : SingleExpandedPropertyWithNext1 + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next2 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next2.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next1.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext3 : SingleExpandedPropertyWithNext2 - { - public PropertyContainer Next3 { get; set; } + } + private class SingleExpandedPropertyWithNext2 : SingleExpandedPropertyWithNext1 + { + public PropertyContainer Next2 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next3.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext4 : SingleExpandedPropertyWithNext3 + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next4 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next4.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next2.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext5 : SingleExpandedPropertyWithNext4 - { - public PropertyContainer Next5 { get; set; } + } + private class SingleExpandedPropertyWithNext3 : SingleExpandedPropertyWithNext2 + { + public PropertyContainer Next3 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next5.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext6 : SingleExpandedPropertyWithNext5 + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next6 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next6.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next3.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext7 : SingleExpandedPropertyWithNext6 - { - public PropertyContainer Next7 { get; set; } + } + private class SingleExpandedPropertyWithNext4 : SingleExpandedPropertyWithNext3 + { + public PropertyContainer Next4 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next7.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext8 : SingleExpandedPropertyWithNext7 + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next8 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next8.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next4.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext9 : SingleExpandedPropertyWithNext8 - { - public PropertyContainer Next9 { get; set; } + } + private class SingleExpandedPropertyWithNext5 : SingleExpandedPropertyWithNext4 + { + public PropertyContainer Next5 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next9.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext10 : SingleExpandedPropertyWithNext9 + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next10 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next10.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next5.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext11 : SingleExpandedPropertyWithNext10 - { - public PropertyContainer Next11 { get; set; } + } + private class SingleExpandedPropertyWithNext6 : SingleExpandedPropertyWithNext5 + { + public PropertyContainer Next6 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next11.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext12 : SingleExpandedPropertyWithNext11 + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next12 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next12.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next6.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext13 : SingleExpandedPropertyWithNext12 - { - public PropertyContainer Next13 { get; set; } + } + private class SingleExpandedPropertyWithNext7 : SingleExpandedPropertyWithNext6 + { + public PropertyContainer Next7 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next13.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext14 : SingleExpandedPropertyWithNext13 + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next14 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next14.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next7.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext15 : SingleExpandedPropertyWithNext14 - { - public PropertyContainer Next15 { get; set; } + } + private class SingleExpandedPropertyWithNext8 : SingleExpandedPropertyWithNext7 + { + public PropertyContainer Next8 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next15.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext16 : SingleExpandedPropertyWithNext15 + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next16 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next16.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next8.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext17 : SingleExpandedPropertyWithNext16 - { - public PropertyContainer Next17 { get; set; } + } + private class SingleExpandedPropertyWithNext9 : SingleExpandedPropertyWithNext8 + { + public PropertyContainer Next9 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next17.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext18 : SingleExpandedPropertyWithNext17 + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next18 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next9.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class SingleExpandedPropertyWithNext10 : SingleExpandedPropertyWithNext9 + { + public PropertyContainer Next10 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next18.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next10.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext19 : SingleExpandedPropertyWithNext18 + } + private class SingleExpandedPropertyWithNext11 : SingleExpandedPropertyWithNext10 + { + public PropertyContainer Next11 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next19 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next11.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class SingleExpandedPropertyWithNext12 : SingleExpandedPropertyWithNext11 + { + public PropertyContainer Next12 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next19.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next12.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext20 : SingleExpandedPropertyWithNext19 + } + private class SingleExpandedPropertyWithNext13 : SingleExpandedPropertyWithNext12 + { + public PropertyContainer Next13 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next20 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next13.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class SingleExpandedPropertyWithNext14 : SingleExpandedPropertyWithNext13 + { + public PropertyContainer Next14 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next20.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next14.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext21 : SingleExpandedPropertyWithNext20 + } + private class SingleExpandedPropertyWithNext15 : SingleExpandedPropertyWithNext14 + { + public PropertyContainer Next15 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next21 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next15.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class SingleExpandedPropertyWithNext16 : SingleExpandedPropertyWithNext15 + { + public PropertyContainer Next16 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next21.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next16.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext22 : SingleExpandedPropertyWithNext21 + } + private class SingleExpandedPropertyWithNext17 : SingleExpandedPropertyWithNext16 + { + public PropertyContainer Next17 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next22 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next17.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class SingleExpandedPropertyWithNext18 : SingleExpandedPropertyWithNext17 + { + public PropertyContainer Next18 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next22.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next18.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext23 : SingleExpandedPropertyWithNext22 + } + private class SingleExpandedPropertyWithNext19 : SingleExpandedPropertyWithNext18 + { + public PropertyContainer Next19 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next23 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next19.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class SingleExpandedPropertyWithNext20 : SingleExpandedPropertyWithNext19 + { + public PropertyContainer Next20 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next23.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next20.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class SingleExpandedPropertyWithNext24 : SingleExpandedPropertyWithNext23 + } + private class SingleExpandedPropertyWithNext21 : SingleExpandedPropertyWithNext20 + { + public PropertyContainer Next21 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next24 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next21.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class SingleExpandedPropertyWithNext22 : SingleExpandedPropertyWithNext21 + { + public PropertyContainer Next22 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next24.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext25 : SingleExpandedPropertyWithNext24 - { - public PropertyContainer Next25 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next25.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext26 : SingleExpandedPropertyWithNext25 - { - public PropertyContainer Next26 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next26.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext27 : SingleExpandedPropertyWithNext26 - { - public PropertyContainer Next27 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next27.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext28 : SingleExpandedPropertyWithNext27 - { - public PropertyContainer Next28 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next28.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext29 : SingleExpandedPropertyWithNext28 - { - public PropertyContainer Next29 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next29.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext30 : SingleExpandedPropertyWithNext29 - { - public PropertyContainer Next30 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next30.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class SingleExpandedPropertyWithNext31 : SingleExpandedPropertyWithNext30 - { - public PropertyContainer Next31 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next31.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private static List SingleExpandedPropertyTypes = new List { - typeof(SingleExpandedProperty<>), - typeof(SingleExpandedPropertyWithNext0<>), - typeof(SingleExpandedPropertyWithNext1<>), - typeof(SingleExpandedPropertyWithNext2<>), - typeof(SingleExpandedPropertyWithNext3<>), - typeof(SingleExpandedPropertyWithNext4<>), - typeof(SingleExpandedPropertyWithNext5<>), - typeof(SingleExpandedPropertyWithNext6<>), - typeof(SingleExpandedPropertyWithNext7<>), - typeof(SingleExpandedPropertyWithNext8<>), - typeof(SingleExpandedPropertyWithNext9<>), - typeof(SingleExpandedPropertyWithNext10<>), - typeof(SingleExpandedPropertyWithNext11<>), - typeof(SingleExpandedPropertyWithNext12<>), - typeof(SingleExpandedPropertyWithNext13<>), - typeof(SingleExpandedPropertyWithNext14<>), - typeof(SingleExpandedPropertyWithNext15<>), - typeof(SingleExpandedPropertyWithNext16<>), - typeof(SingleExpandedPropertyWithNext17<>), - typeof(SingleExpandedPropertyWithNext18<>), - typeof(SingleExpandedPropertyWithNext19<>), - typeof(SingleExpandedPropertyWithNext20<>), - typeof(SingleExpandedPropertyWithNext21<>), - typeof(SingleExpandedPropertyWithNext22<>), - typeof(SingleExpandedPropertyWithNext23<>), - typeof(SingleExpandedPropertyWithNext24<>), - typeof(SingleExpandedPropertyWithNext25<>), - typeof(SingleExpandedPropertyWithNext26<>), - typeof(SingleExpandedPropertyWithNext27<>), - typeof(SingleExpandedPropertyWithNext28<>), - typeof(SingleExpandedPropertyWithNext29<>), - typeof(SingleExpandedPropertyWithNext30<>), - typeof(SingleExpandedPropertyWithNext31<>), - }; - private class CollectionExpandedPropertyWithNext0 : CollectionExpandedProperty - { - public PropertyContainer Next0 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next0.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext1 : CollectionExpandedPropertyWithNext0 - { - public PropertyContainer Next1 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next1.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext2 : CollectionExpandedPropertyWithNext1 - { - public PropertyContainer Next2 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next2.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext3 : CollectionExpandedPropertyWithNext2 - { - public PropertyContainer Next3 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next3.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext4 : CollectionExpandedPropertyWithNext3 - { - public PropertyContainer Next4 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next4.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext5 : CollectionExpandedPropertyWithNext4 - { - public PropertyContainer Next5 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next5.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext6 : CollectionExpandedPropertyWithNext5 - { - public PropertyContainer Next6 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next6.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext7 : CollectionExpandedPropertyWithNext6 - { - public PropertyContainer Next7 { get; set; } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next22.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class SingleExpandedPropertyWithNext23 : SingleExpandedPropertyWithNext22 + { + public PropertyContainer Next23 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next7.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next23.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext8 : CollectionExpandedPropertyWithNext7 + } + private class SingleExpandedPropertyWithNext24 : SingleExpandedPropertyWithNext23 + { + public PropertyContainer Next24 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next8 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next24.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class SingleExpandedPropertyWithNext25 : SingleExpandedPropertyWithNext24 + { + public PropertyContainer Next25 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next8.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next25.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext9 : CollectionExpandedPropertyWithNext8 + } + private class SingleExpandedPropertyWithNext26 : SingleExpandedPropertyWithNext25 + { + public PropertyContainer Next26 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next9 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next26.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class SingleExpandedPropertyWithNext27 : SingleExpandedPropertyWithNext26 + { + public PropertyContainer Next27 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next9.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next27.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext10 : CollectionExpandedPropertyWithNext9 + } + private class SingleExpandedPropertyWithNext28 : SingleExpandedPropertyWithNext27 + { + public PropertyContainer Next28 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next10 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next28.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class SingleExpandedPropertyWithNext29 : SingleExpandedPropertyWithNext28 + { + public PropertyContainer Next29 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next10.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next29.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext11 : CollectionExpandedPropertyWithNext10 + } + private class SingleExpandedPropertyWithNext30 : SingleExpandedPropertyWithNext29 + { + public PropertyContainer Next30 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next11 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next30.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class SingleExpandedPropertyWithNext31 : SingleExpandedPropertyWithNext30 + { + public PropertyContainer Next31 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next11.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next31.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext12 : CollectionExpandedPropertyWithNext11 + } +private static List SingleExpandedPropertyTypes = new List { + typeof(SingleExpandedProperty<>), + typeof(SingleExpandedPropertyWithNext0<>), + typeof(SingleExpandedPropertyWithNext1<>), + typeof(SingleExpandedPropertyWithNext2<>), + typeof(SingleExpandedPropertyWithNext3<>), + typeof(SingleExpandedPropertyWithNext4<>), + typeof(SingleExpandedPropertyWithNext5<>), + typeof(SingleExpandedPropertyWithNext6<>), + typeof(SingleExpandedPropertyWithNext7<>), + typeof(SingleExpandedPropertyWithNext8<>), + typeof(SingleExpandedPropertyWithNext9<>), + typeof(SingleExpandedPropertyWithNext10<>), + typeof(SingleExpandedPropertyWithNext11<>), + typeof(SingleExpandedPropertyWithNext12<>), + typeof(SingleExpandedPropertyWithNext13<>), + typeof(SingleExpandedPropertyWithNext14<>), + typeof(SingleExpandedPropertyWithNext15<>), + typeof(SingleExpandedPropertyWithNext16<>), + typeof(SingleExpandedPropertyWithNext17<>), + typeof(SingleExpandedPropertyWithNext18<>), + typeof(SingleExpandedPropertyWithNext19<>), + typeof(SingleExpandedPropertyWithNext20<>), + typeof(SingleExpandedPropertyWithNext21<>), + typeof(SingleExpandedPropertyWithNext22<>), + typeof(SingleExpandedPropertyWithNext23<>), + typeof(SingleExpandedPropertyWithNext24<>), + typeof(SingleExpandedPropertyWithNext25<>), + typeof(SingleExpandedPropertyWithNext26<>), + typeof(SingleExpandedPropertyWithNext27<>), + typeof(SingleExpandedPropertyWithNext28<>), + typeof(SingleExpandedPropertyWithNext29<>), + typeof(SingleExpandedPropertyWithNext30<>), + typeof(SingleExpandedPropertyWithNext31<>), + }; + private class CollectionExpandedPropertyWithNext0 : CollectionExpandedProperty + { + public PropertyContainer Next0 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next12 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next0.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext1 : CollectionExpandedPropertyWithNext0 + { + public PropertyContainer Next1 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next12.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next1.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext13 : CollectionExpandedPropertyWithNext12 + } + private class CollectionExpandedPropertyWithNext2 : CollectionExpandedPropertyWithNext1 + { + public PropertyContainer Next2 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next13 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next2.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext3 : CollectionExpandedPropertyWithNext2 + { + public PropertyContainer Next3 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next13.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next3.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext14 : CollectionExpandedPropertyWithNext13 + } + private class CollectionExpandedPropertyWithNext4 : CollectionExpandedPropertyWithNext3 + { + public PropertyContainer Next4 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next14 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next4.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext5 : CollectionExpandedPropertyWithNext4 + { + public PropertyContainer Next5 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next14.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next5.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext15 : CollectionExpandedPropertyWithNext14 + } + private class CollectionExpandedPropertyWithNext6 : CollectionExpandedPropertyWithNext5 + { + public PropertyContainer Next6 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next15 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next6.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext7 : CollectionExpandedPropertyWithNext6 + { + public PropertyContainer Next7 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next15.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next7.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext16 : CollectionExpandedPropertyWithNext15 + } + private class CollectionExpandedPropertyWithNext8 : CollectionExpandedPropertyWithNext7 + { + public PropertyContainer Next8 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next16 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next8.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext9 : CollectionExpandedPropertyWithNext8 + { + public PropertyContainer Next9 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next16.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next9.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext17 : CollectionExpandedPropertyWithNext16 + } + private class CollectionExpandedPropertyWithNext10 : CollectionExpandedPropertyWithNext9 + { + public PropertyContainer Next10 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next17 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next10.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext11 : CollectionExpandedPropertyWithNext10 + { + public PropertyContainer Next11 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next17.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next11.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext18 : CollectionExpandedPropertyWithNext17 + } + private class CollectionExpandedPropertyWithNext12 : CollectionExpandedPropertyWithNext11 + { + public PropertyContainer Next12 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next18 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next12.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext13 : CollectionExpandedPropertyWithNext12 + { + public PropertyContainer Next13 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next18.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next13.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext19 : CollectionExpandedPropertyWithNext18 + } + private class CollectionExpandedPropertyWithNext14 : CollectionExpandedPropertyWithNext13 + { + public PropertyContainer Next14 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next19 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next14.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext15 : CollectionExpandedPropertyWithNext14 + { + public PropertyContainer Next15 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next19.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next15.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext20 : CollectionExpandedPropertyWithNext19 + } + private class CollectionExpandedPropertyWithNext16 : CollectionExpandedPropertyWithNext15 + { + public PropertyContainer Next16 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next20 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next16.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext17 : CollectionExpandedPropertyWithNext16 + { + public PropertyContainer Next17 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next20.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next17.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext21 : CollectionExpandedPropertyWithNext20 + } + private class CollectionExpandedPropertyWithNext18 : CollectionExpandedPropertyWithNext17 + { + public PropertyContainer Next18 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next21 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next18.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext19 : CollectionExpandedPropertyWithNext18 + { + public PropertyContainer Next19 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next21.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next19.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext22 : CollectionExpandedPropertyWithNext21 + } + private class CollectionExpandedPropertyWithNext20 : CollectionExpandedPropertyWithNext19 + { + public PropertyContainer Next20 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next22 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next20.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext21 : CollectionExpandedPropertyWithNext20 + { + public PropertyContainer Next21 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next22.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next21.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext23 : CollectionExpandedPropertyWithNext22 + } + private class CollectionExpandedPropertyWithNext22 : CollectionExpandedPropertyWithNext21 + { + public PropertyContainer Next22 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next23 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next22.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext23 : CollectionExpandedPropertyWithNext22 + { + public PropertyContainer Next23 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next23.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next23.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class CollectionExpandedPropertyWithNext24 : CollectionExpandedPropertyWithNext23 + } + private class CollectionExpandedPropertyWithNext24 : CollectionExpandedPropertyWithNext23 + { + public PropertyContainer Next24 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next24 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next24.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext25 : CollectionExpandedPropertyWithNext24 + { + public PropertyContainer Next25 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next24.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext25 : CollectionExpandedPropertyWithNext24 - { - public PropertyContainer Next25 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next25.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext26 : CollectionExpandedPropertyWithNext25 - { - public PropertyContainer Next26 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next26.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext27 : CollectionExpandedPropertyWithNext26 - { - public PropertyContainer Next27 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next27.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext28 : CollectionExpandedPropertyWithNext27 - { - public PropertyContainer Next28 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next28.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext29 : CollectionExpandedPropertyWithNext28 - { - public PropertyContainer Next29 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next29.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext30 : CollectionExpandedPropertyWithNext29 - { - public PropertyContainer Next30 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next30.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class CollectionExpandedPropertyWithNext31 : CollectionExpandedPropertyWithNext30 - { - public PropertyContainer Next31 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next31.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private static List CollectionExpandedPropertyTypes = new List { - typeof(CollectionExpandedProperty<>), - typeof(CollectionExpandedPropertyWithNext0<>), - typeof(CollectionExpandedPropertyWithNext1<>), - typeof(CollectionExpandedPropertyWithNext2<>), - typeof(CollectionExpandedPropertyWithNext3<>), - typeof(CollectionExpandedPropertyWithNext4<>), - typeof(CollectionExpandedPropertyWithNext5<>), - typeof(CollectionExpandedPropertyWithNext6<>), - typeof(CollectionExpandedPropertyWithNext7<>), - typeof(CollectionExpandedPropertyWithNext8<>), - typeof(CollectionExpandedPropertyWithNext9<>), - typeof(CollectionExpandedPropertyWithNext10<>), - typeof(CollectionExpandedPropertyWithNext11<>), - typeof(CollectionExpandedPropertyWithNext12<>), - typeof(CollectionExpandedPropertyWithNext13<>), - typeof(CollectionExpandedPropertyWithNext14<>), - typeof(CollectionExpandedPropertyWithNext15<>), - typeof(CollectionExpandedPropertyWithNext16<>), - typeof(CollectionExpandedPropertyWithNext17<>), - typeof(CollectionExpandedPropertyWithNext18<>), - typeof(CollectionExpandedPropertyWithNext19<>), - typeof(CollectionExpandedPropertyWithNext20<>), - typeof(CollectionExpandedPropertyWithNext21<>), - typeof(CollectionExpandedPropertyWithNext22<>), - typeof(CollectionExpandedPropertyWithNext23<>), - typeof(CollectionExpandedPropertyWithNext24<>), - typeof(CollectionExpandedPropertyWithNext25<>), - typeof(CollectionExpandedPropertyWithNext26<>), - typeof(CollectionExpandedPropertyWithNext27<>), - typeof(CollectionExpandedPropertyWithNext28<>), - typeof(CollectionExpandedPropertyWithNext29<>), - typeof(CollectionExpandedPropertyWithNext30<>), - typeof(CollectionExpandedPropertyWithNext31<>), - }; - private class AutoSelectedNamedPropertyWithNext0 : AutoSelectedNamedProperty - { - public PropertyContainer Next0 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next0.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext1 : AutoSelectedNamedPropertyWithNext0 - { - public PropertyContainer Next1 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next1.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext2 : AutoSelectedNamedPropertyWithNext1 - { - public PropertyContainer Next2 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next2.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext3 : AutoSelectedNamedPropertyWithNext2 - { - public PropertyContainer Next3 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next3.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext4 : AutoSelectedNamedPropertyWithNext3 - { - public PropertyContainer Next4 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next4.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext5 : AutoSelectedNamedPropertyWithNext4 - { - public PropertyContainer Next5 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next5.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext6 : AutoSelectedNamedPropertyWithNext5 - { - public PropertyContainer Next6 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next6.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext7 : AutoSelectedNamedPropertyWithNext6 - { - public PropertyContainer Next7 { get; set; } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next25.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext26 : CollectionExpandedPropertyWithNext25 + { + public PropertyContainer Next26 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next7.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next26.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext8 : AutoSelectedNamedPropertyWithNext7 + } + private class CollectionExpandedPropertyWithNext27 : CollectionExpandedPropertyWithNext26 + { + public PropertyContainer Next27 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next8 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next27.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext28 : CollectionExpandedPropertyWithNext27 + { + public PropertyContainer Next28 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next8.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next28.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext9 : AutoSelectedNamedPropertyWithNext8 + } + private class CollectionExpandedPropertyWithNext29 : CollectionExpandedPropertyWithNext28 + { + public PropertyContainer Next29 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next9 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next29.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class CollectionExpandedPropertyWithNext30 : CollectionExpandedPropertyWithNext29 + { + public PropertyContainer Next30 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next9.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next30.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext10 : AutoSelectedNamedPropertyWithNext9 + } + private class CollectionExpandedPropertyWithNext31 : CollectionExpandedPropertyWithNext30 + { + public PropertyContainer Next31 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next10 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next31.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } +private static List CollectionExpandedPropertyTypes = new List { + typeof(CollectionExpandedProperty<>), + typeof(CollectionExpandedPropertyWithNext0<>), + typeof(CollectionExpandedPropertyWithNext1<>), + typeof(CollectionExpandedPropertyWithNext2<>), + typeof(CollectionExpandedPropertyWithNext3<>), + typeof(CollectionExpandedPropertyWithNext4<>), + typeof(CollectionExpandedPropertyWithNext5<>), + typeof(CollectionExpandedPropertyWithNext6<>), + typeof(CollectionExpandedPropertyWithNext7<>), + typeof(CollectionExpandedPropertyWithNext8<>), + typeof(CollectionExpandedPropertyWithNext9<>), + typeof(CollectionExpandedPropertyWithNext10<>), + typeof(CollectionExpandedPropertyWithNext11<>), + typeof(CollectionExpandedPropertyWithNext12<>), + typeof(CollectionExpandedPropertyWithNext13<>), + typeof(CollectionExpandedPropertyWithNext14<>), + typeof(CollectionExpandedPropertyWithNext15<>), + typeof(CollectionExpandedPropertyWithNext16<>), + typeof(CollectionExpandedPropertyWithNext17<>), + typeof(CollectionExpandedPropertyWithNext18<>), + typeof(CollectionExpandedPropertyWithNext19<>), + typeof(CollectionExpandedPropertyWithNext20<>), + typeof(CollectionExpandedPropertyWithNext21<>), + typeof(CollectionExpandedPropertyWithNext22<>), + typeof(CollectionExpandedPropertyWithNext23<>), + typeof(CollectionExpandedPropertyWithNext24<>), + typeof(CollectionExpandedPropertyWithNext25<>), + typeof(CollectionExpandedPropertyWithNext26<>), + typeof(CollectionExpandedPropertyWithNext27<>), + typeof(CollectionExpandedPropertyWithNext28<>), + typeof(CollectionExpandedPropertyWithNext29<>), + typeof(CollectionExpandedPropertyWithNext30<>), + typeof(CollectionExpandedPropertyWithNext31<>), + }; + private class AutoSelectedNamedPropertyWithNext0 : AutoSelectedNamedProperty + { + public PropertyContainer Next0 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next10.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next0.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext11 : AutoSelectedNamedPropertyWithNext10 + } + private class AutoSelectedNamedPropertyWithNext1 : AutoSelectedNamedPropertyWithNext0 + { + public PropertyContainer Next1 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next11 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next1.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext2 : AutoSelectedNamedPropertyWithNext1 + { + public PropertyContainer Next2 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next11.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next2.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext12 : AutoSelectedNamedPropertyWithNext11 + } + private class AutoSelectedNamedPropertyWithNext3 : AutoSelectedNamedPropertyWithNext2 + { + public PropertyContainer Next3 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next12 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next3.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext4 : AutoSelectedNamedPropertyWithNext3 + { + public PropertyContainer Next4 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next12.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next4.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext13 : AutoSelectedNamedPropertyWithNext12 + } + private class AutoSelectedNamedPropertyWithNext5 : AutoSelectedNamedPropertyWithNext4 + { + public PropertyContainer Next5 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next13 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next5.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext6 : AutoSelectedNamedPropertyWithNext5 + { + public PropertyContainer Next6 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next13.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next6.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext14 : AutoSelectedNamedPropertyWithNext13 + } + private class AutoSelectedNamedPropertyWithNext7 : AutoSelectedNamedPropertyWithNext6 + { + public PropertyContainer Next7 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next14 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next7.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext8 : AutoSelectedNamedPropertyWithNext7 + { + public PropertyContainer Next8 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next14.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next8.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext15 : AutoSelectedNamedPropertyWithNext14 + } + private class AutoSelectedNamedPropertyWithNext9 : AutoSelectedNamedPropertyWithNext8 + { + public PropertyContainer Next9 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next15 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next9.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext10 : AutoSelectedNamedPropertyWithNext9 + { + public PropertyContainer Next10 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next15.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next10.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext16 : AutoSelectedNamedPropertyWithNext15 + } + private class AutoSelectedNamedPropertyWithNext11 : AutoSelectedNamedPropertyWithNext10 + { + public PropertyContainer Next11 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next16 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next11.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext12 : AutoSelectedNamedPropertyWithNext11 + { + public PropertyContainer Next12 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next16.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next12.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext17 : AutoSelectedNamedPropertyWithNext16 + } + private class AutoSelectedNamedPropertyWithNext13 : AutoSelectedNamedPropertyWithNext12 + { + public PropertyContainer Next13 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next17 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next13.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext14 : AutoSelectedNamedPropertyWithNext13 + { + public PropertyContainer Next14 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next17.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next14.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext18 : AutoSelectedNamedPropertyWithNext17 + } + private class AutoSelectedNamedPropertyWithNext15 : AutoSelectedNamedPropertyWithNext14 + { + public PropertyContainer Next15 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next18 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next15.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext16 : AutoSelectedNamedPropertyWithNext15 + { + public PropertyContainer Next16 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next18.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next16.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext19 : AutoSelectedNamedPropertyWithNext18 + } + private class AutoSelectedNamedPropertyWithNext17 : AutoSelectedNamedPropertyWithNext16 + { + public PropertyContainer Next17 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next19 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next17.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext18 : AutoSelectedNamedPropertyWithNext17 + { + public PropertyContainer Next18 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next19.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next18.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext20 : AutoSelectedNamedPropertyWithNext19 + } + private class AutoSelectedNamedPropertyWithNext19 : AutoSelectedNamedPropertyWithNext18 + { + public PropertyContainer Next19 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next20 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next19.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext20 : AutoSelectedNamedPropertyWithNext19 + { + public PropertyContainer Next20 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next20.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next20.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext21 : AutoSelectedNamedPropertyWithNext20 + } + private class AutoSelectedNamedPropertyWithNext21 : AutoSelectedNamedPropertyWithNext20 + { + public PropertyContainer Next21 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next21 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next21.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext22 : AutoSelectedNamedPropertyWithNext21 + { + public PropertyContainer Next22 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next21.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next22.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext22 : AutoSelectedNamedPropertyWithNext21 + } + private class AutoSelectedNamedPropertyWithNext23 : AutoSelectedNamedPropertyWithNext22 + { + public PropertyContainer Next23 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next22 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next23.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext24 : AutoSelectedNamedPropertyWithNext23 + { + public PropertyContainer Next24 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next22.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next24.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext23 : AutoSelectedNamedPropertyWithNext22 + } + private class AutoSelectedNamedPropertyWithNext25 : AutoSelectedNamedPropertyWithNext24 + { + public PropertyContainer Next25 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next23 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next25.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext26 : AutoSelectedNamedPropertyWithNext25 + { + public PropertyContainer Next26 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next23.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next26.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class AutoSelectedNamedPropertyWithNext24 : AutoSelectedNamedPropertyWithNext23 + } + private class AutoSelectedNamedPropertyWithNext27 : AutoSelectedNamedPropertyWithNext26 + { + public PropertyContainer Next27 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next24 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next27.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext28 : AutoSelectedNamedPropertyWithNext27 + { + public PropertyContainer Next28 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next24.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext25 : AutoSelectedNamedPropertyWithNext24 - { - public PropertyContainer Next25 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next25.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext26 : AutoSelectedNamedPropertyWithNext25 - { - public PropertyContainer Next26 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next26.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext27 : AutoSelectedNamedPropertyWithNext26 - { - public PropertyContainer Next27 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next27.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext28 : AutoSelectedNamedPropertyWithNext27 - { - public PropertyContainer Next28 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next28.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext29 : AutoSelectedNamedPropertyWithNext28 - { - public PropertyContainer Next29 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next29.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext30 : AutoSelectedNamedPropertyWithNext29 - { - public PropertyContainer Next30 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next30.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class AutoSelectedNamedPropertyWithNext31 : AutoSelectedNamedPropertyWithNext30 - { - public PropertyContainer Next31 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next31.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private static List AutoSelectedNamedPropertyTypes = new List { - typeof(AutoSelectedNamedProperty<>), - typeof(AutoSelectedNamedPropertyWithNext0<>), - typeof(AutoSelectedNamedPropertyWithNext1<>), - typeof(AutoSelectedNamedPropertyWithNext2<>), - typeof(AutoSelectedNamedPropertyWithNext3<>), - typeof(AutoSelectedNamedPropertyWithNext4<>), - typeof(AutoSelectedNamedPropertyWithNext5<>), - typeof(AutoSelectedNamedPropertyWithNext6<>), - typeof(AutoSelectedNamedPropertyWithNext7<>), - typeof(AutoSelectedNamedPropertyWithNext8<>), - typeof(AutoSelectedNamedPropertyWithNext9<>), - typeof(AutoSelectedNamedPropertyWithNext10<>), - typeof(AutoSelectedNamedPropertyWithNext11<>), - typeof(AutoSelectedNamedPropertyWithNext12<>), - typeof(AutoSelectedNamedPropertyWithNext13<>), - typeof(AutoSelectedNamedPropertyWithNext14<>), - typeof(AutoSelectedNamedPropertyWithNext15<>), - typeof(AutoSelectedNamedPropertyWithNext16<>), - typeof(AutoSelectedNamedPropertyWithNext17<>), - typeof(AutoSelectedNamedPropertyWithNext18<>), - typeof(AutoSelectedNamedPropertyWithNext19<>), - typeof(AutoSelectedNamedPropertyWithNext20<>), - typeof(AutoSelectedNamedPropertyWithNext21<>), - typeof(AutoSelectedNamedPropertyWithNext22<>), - typeof(AutoSelectedNamedPropertyWithNext23<>), - typeof(AutoSelectedNamedPropertyWithNext24<>), - typeof(AutoSelectedNamedPropertyWithNext25<>), - typeof(AutoSelectedNamedPropertyWithNext26<>), - typeof(AutoSelectedNamedPropertyWithNext27<>), - typeof(AutoSelectedNamedPropertyWithNext28<>), - typeof(AutoSelectedNamedPropertyWithNext29<>), - typeof(AutoSelectedNamedPropertyWithNext30<>), - typeof(AutoSelectedNamedPropertyWithNext31<>), - }; - private class NamedPropertyWithNext0 : NamedProperty - { - public PropertyContainer Next0 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next0.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext1 : NamedPropertyWithNext0 - { - public PropertyContainer Next1 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next1.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext2 : NamedPropertyWithNext1 - { - public PropertyContainer Next2 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next2.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext3 : NamedPropertyWithNext2 - { - public PropertyContainer Next3 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next3.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext4 : NamedPropertyWithNext3 - { - public PropertyContainer Next4 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next4.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext5 : NamedPropertyWithNext4 - { - public PropertyContainer Next5 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next5.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext6 : NamedPropertyWithNext5 - { - public PropertyContainer Next6 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next6.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext7 : NamedPropertyWithNext6 - { - public PropertyContainer Next7 { get; set; } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next28.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext29 : AutoSelectedNamedPropertyWithNext28 + { + public PropertyContainer Next29 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next7.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next29.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext8 : NamedPropertyWithNext7 + } + private class AutoSelectedNamedPropertyWithNext30 : AutoSelectedNamedPropertyWithNext29 + { + public PropertyContainer Next30 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next8 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next30.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class AutoSelectedNamedPropertyWithNext31 : AutoSelectedNamedPropertyWithNext30 + { + public PropertyContainer Next31 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next8.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next31.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext9 : NamedPropertyWithNext8 + } +private static List AutoSelectedNamedPropertyTypes = new List { + typeof(AutoSelectedNamedProperty<>), + typeof(AutoSelectedNamedPropertyWithNext0<>), + typeof(AutoSelectedNamedPropertyWithNext1<>), + typeof(AutoSelectedNamedPropertyWithNext2<>), + typeof(AutoSelectedNamedPropertyWithNext3<>), + typeof(AutoSelectedNamedPropertyWithNext4<>), + typeof(AutoSelectedNamedPropertyWithNext5<>), + typeof(AutoSelectedNamedPropertyWithNext6<>), + typeof(AutoSelectedNamedPropertyWithNext7<>), + typeof(AutoSelectedNamedPropertyWithNext8<>), + typeof(AutoSelectedNamedPropertyWithNext9<>), + typeof(AutoSelectedNamedPropertyWithNext10<>), + typeof(AutoSelectedNamedPropertyWithNext11<>), + typeof(AutoSelectedNamedPropertyWithNext12<>), + typeof(AutoSelectedNamedPropertyWithNext13<>), + typeof(AutoSelectedNamedPropertyWithNext14<>), + typeof(AutoSelectedNamedPropertyWithNext15<>), + typeof(AutoSelectedNamedPropertyWithNext16<>), + typeof(AutoSelectedNamedPropertyWithNext17<>), + typeof(AutoSelectedNamedPropertyWithNext18<>), + typeof(AutoSelectedNamedPropertyWithNext19<>), + typeof(AutoSelectedNamedPropertyWithNext20<>), + typeof(AutoSelectedNamedPropertyWithNext21<>), + typeof(AutoSelectedNamedPropertyWithNext22<>), + typeof(AutoSelectedNamedPropertyWithNext23<>), + typeof(AutoSelectedNamedPropertyWithNext24<>), + typeof(AutoSelectedNamedPropertyWithNext25<>), + typeof(AutoSelectedNamedPropertyWithNext26<>), + typeof(AutoSelectedNamedPropertyWithNext27<>), + typeof(AutoSelectedNamedPropertyWithNext28<>), + typeof(AutoSelectedNamedPropertyWithNext29<>), + typeof(AutoSelectedNamedPropertyWithNext30<>), + typeof(AutoSelectedNamedPropertyWithNext31<>), + }; + private class NamedPropertyWithNext0 : NamedProperty + { + public PropertyContainer Next0 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next9 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next0.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext1 : NamedPropertyWithNext0 + { + public PropertyContainer Next1 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next9.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next1.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext10 : NamedPropertyWithNext9 + } + private class NamedPropertyWithNext2 : NamedPropertyWithNext1 + { + public PropertyContainer Next2 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next10 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next2.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext3 : NamedPropertyWithNext2 + { + public PropertyContainer Next3 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next10.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next3.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext11 : NamedPropertyWithNext10 + } + private class NamedPropertyWithNext4 : NamedPropertyWithNext3 + { + public PropertyContainer Next4 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next11 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next4.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext5 : NamedPropertyWithNext4 + { + public PropertyContainer Next5 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next11.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next5.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext12 : NamedPropertyWithNext11 + } + private class NamedPropertyWithNext6 : NamedPropertyWithNext5 + { + public PropertyContainer Next6 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next12 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next6.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext7 : NamedPropertyWithNext6 + { + public PropertyContainer Next7 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next12.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next7.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext13 : NamedPropertyWithNext12 + } + private class NamedPropertyWithNext8 : NamedPropertyWithNext7 + { + public PropertyContainer Next8 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next13 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next8.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext9 : NamedPropertyWithNext8 + { + public PropertyContainer Next9 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next13.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next9.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext14 : NamedPropertyWithNext13 + } + private class NamedPropertyWithNext10 : NamedPropertyWithNext9 + { + public PropertyContainer Next10 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next14 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next10.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext11 : NamedPropertyWithNext10 + { + public PropertyContainer Next11 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next14.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next11.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext15 : NamedPropertyWithNext14 + } + private class NamedPropertyWithNext12 : NamedPropertyWithNext11 + { + public PropertyContainer Next12 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next15 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next12.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext13 : NamedPropertyWithNext12 + { + public PropertyContainer Next13 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next15.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next13.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext16 : NamedPropertyWithNext15 + } + private class NamedPropertyWithNext14 : NamedPropertyWithNext13 + { + public PropertyContainer Next14 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next16 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next14.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext15 : NamedPropertyWithNext14 + { + public PropertyContainer Next15 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next16.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next15.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext17 : NamedPropertyWithNext16 + } + private class NamedPropertyWithNext16 : NamedPropertyWithNext15 + { + public PropertyContainer Next16 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next17 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next16.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext17 : NamedPropertyWithNext16 + { + public PropertyContainer Next17 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next17.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next17.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext18 : NamedPropertyWithNext17 + } + private class NamedPropertyWithNext18 : NamedPropertyWithNext17 + { + public PropertyContainer Next18 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next18 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next18.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext19 : NamedPropertyWithNext18 + { + public PropertyContainer Next19 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next18.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next19.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext19 : NamedPropertyWithNext18 + } + private class NamedPropertyWithNext20 : NamedPropertyWithNext19 + { + public PropertyContainer Next20 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next19 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next20.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext21 : NamedPropertyWithNext20 + { + public PropertyContainer Next21 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next19.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next21.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext20 : NamedPropertyWithNext19 + } + private class NamedPropertyWithNext22 : NamedPropertyWithNext21 + { + public PropertyContainer Next22 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next20 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next22.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext23 : NamedPropertyWithNext22 + { + public PropertyContainer Next23 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next20.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next23.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext21 : NamedPropertyWithNext20 + } + private class NamedPropertyWithNext24 : NamedPropertyWithNext23 + { + public PropertyContainer Next24 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next21 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next24.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext25 : NamedPropertyWithNext24 + { + public PropertyContainer Next25 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next21.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next25.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext22 : NamedPropertyWithNext21 + } + private class NamedPropertyWithNext26 : NamedPropertyWithNext25 + { + public PropertyContainer Next26 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next22 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next26.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext27 : NamedPropertyWithNext26 + { + public PropertyContainer Next27 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next22.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next27.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext23 : NamedPropertyWithNext22 + } + private class NamedPropertyWithNext28 : NamedPropertyWithNext27 + { + public PropertyContainer Next28 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next23 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next28.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext29 : NamedPropertyWithNext28 + { + public PropertyContainer Next29 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next23.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next29.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); } - private class NamedPropertyWithNext24 : NamedPropertyWithNext23 + } + private class NamedPropertyWithNext30 : NamedPropertyWithNext29 + { + public PropertyContainer Next30 { get; set; } + + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) { - public PropertyContainer Next24 { get; set; } + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next30.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } + } + private class NamedPropertyWithNext31 : NamedPropertyWithNext30 + { + public PropertyContainer Next31 { get; set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next24.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext25 : NamedPropertyWithNext24 - { - public PropertyContainer Next25 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next25.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext26 : NamedPropertyWithNext25 - { - public PropertyContainer Next26 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next26.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext27 : NamedPropertyWithNext26 - { - public PropertyContainer Next27 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next27.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext28 : NamedPropertyWithNext27 - { - public PropertyContainer Next28 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next28.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext29 : NamedPropertyWithNext28 - { - public PropertyContainer Next29 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next29.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext30 : NamedPropertyWithNext29 - { - public PropertyContainer Next30 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next30.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private class NamedPropertyWithNext31 : NamedPropertyWithNext30 - { - public PropertyContainer Next31 { get; set; } - - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) - { - base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - Next31.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); - } - } - private static List NamedPropertyTypes = new List { - typeof(NamedProperty<>), - typeof(NamedPropertyWithNext0<>), - typeof(NamedPropertyWithNext1<>), - typeof(NamedPropertyWithNext2<>), - typeof(NamedPropertyWithNext3<>), - typeof(NamedPropertyWithNext4<>), - typeof(NamedPropertyWithNext5<>), - typeof(NamedPropertyWithNext6<>), - typeof(NamedPropertyWithNext7<>), - typeof(NamedPropertyWithNext8<>), - typeof(NamedPropertyWithNext9<>), - typeof(NamedPropertyWithNext10<>), - typeof(NamedPropertyWithNext11<>), - typeof(NamedPropertyWithNext12<>), - typeof(NamedPropertyWithNext13<>), - typeof(NamedPropertyWithNext14<>), - typeof(NamedPropertyWithNext15<>), - typeof(NamedPropertyWithNext16<>), - typeof(NamedPropertyWithNext17<>), - typeof(NamedPropertyWithNext18<>), - typeof(NamedPropertyWithNext19<>), - typeof(NamedPropertyWithNext20<>), - typeof(NamedPropertyWithNext21<>), - typeof(NamedPropertyWithNext22<>), - typeof(NamedPropertyWithNext23<>), - typeof(NamedPropertyWithNext24<>), - typeof(NamedPropertyWithNext25<>), - typeof(NamedPropertyWithNext26<>), - typeof(NamedPropertyWithNext27<>), - typeof(NamedPropertyWithNext28<>), - typeof(NamedPropertyWithNext29<>), - typeof(NamedPropertyWithNext30<>), - typeof(NamedPropertyWithNext31<>), - }; - + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + base.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + Next31.ToDictionaryCore(dictionary, propertyMapper, includeAutoSelected); + } } +private static List NamedPropertyTypes = new List { + typeof(NamedProperty<>), + typeof(NamedPropertyWithNext0<>), + typeof(NamedPropertyWithNext1<>), + typeof(NamedPropertyWithNext2<>), + typeof(NamedPropertyWithNext3<>), + typeof(NamedPropertyWithNext4<>), + typeof(NamedPropertyWithNext5<>), + typeof(NamedPropertyWithNext6<>), + typeof(NamedPropertyWithNext7<>), + typeof(NamedPropertyWithNext8<>), + typeof(NamedPropertyWithNext9<>), + typeof(NamedPropertyWithNext10<>), + typeof(NamedPropertyWithNext11<>), + typeof(NamedPropertyWithNext12<>), + typeof(NamedPropertyWithNext13<>), + typeof(NamedPropertyWithNext14<>), + typeof(NamedPropertyWithNext15<>), + typeof(NamedPropertyWithNext16<>), + typeof(NamedPropertyWithNext17<>), + typeof(NamedPropertyWithNext18<>), + typeof(NamedPropertyWithNext19<>), + typeof(NamedPropertyWithNext20<>), + typeof(NamedPropertyWithNext21<>), + typeof(NamedPropertyWithNext22<>), + typeof(NamedPropertyWithNext23<>), + typeof(NamedPropertyWithNext24<>), + typeof(NamedPropertyWithNext25<>), + typeof(NamedPropertyWithNext26<>), + typeof(NamedPropertyWithNext27<>), + typeof(NamedPropertyWithNext28<>), + typeof(NamedPropertyWithNext29<>), + typeof(NamedPropertyWithNext30<>), + typeof(NamedPropertyWithNext31<>), + }; + } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/SingleExpandedPropertyOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Container/SingleExpandedPropertyOfT.cs index eee32190d..de2811260 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/SingleExpandedPropertyOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/SingleExpandedPropertyOfT.cs @@ -5,15 +5,14 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Container +namespace Microsoft.AspNetCore.OData.Query.Container; + +internal class SingleExpandedProperty : NamedProperty { - internal class SingleExpandedProperty : NamedProperty - { - public bool IsNull { get; set; } + public bool IsNull { get; set; } - public override object GetValue() - { - return IsNull ? (object)null : Value; - } + public override object GetValue() + { + return IsNull ? (object)null : Value; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Container/TruncatedCollectionOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Container/TruncatedCollectionOfT.cs index 1660f3981..fc62d83ff 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Container/TruncatedCollectionOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Container/TruncatedCollectionOfT.cs @@ -9,168 +9,167 @@ using System.Collections.Generic; using System.Linq; -namespace Microsoft.AspNetCore.OData.Query.Container +namespace Microsoft.AspNetCore.OData.Query.Container; + +/// +/// Represents a class that truncates a collection to a given page size. +/// +/// The collection element type. +public class TruncatedCollection : List, ITruncatedCollection, IEnumerable, ICountOptionCollection { + // The default capacity of the list. + // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs#L23 + private const int DefaultCapacity = 4; + private const int MinPageSize = 1; + + private bool _isTruncated; + private int _pageSize; + private long? _totalCount; + /// - /// Represents a class that truncates a collection to a given page size. + /// Initializes a new instance of the class. /// - /// The collection element type. - public class TruncatedCollection : List, ITruncatedCollection, IEnumerable, ICountOptionCollection + /// The collection to be truncated. + /// The page size. + public TruncatedCollection(IEnumerable source, int pageSize) + : base(checked(pageSize + 1)) { - // The default capacity of the list. - // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs#L23 - private const int DefaultCapacity = 4; - private const int MinPageSize = 1; - - private bool _isTruncated; - private int _pageSize; - private long? _totalCount; - - /// - /// Initializes a new instance of the class. - /// - /// The collection to be truncated. - /// The page size. - public TruncatedCollection(IEnumerable source, int pageSize) - : base(checked(pageSize + 1)) - { - var items = source.Take(Capacity); - AddRange(items); - Initialize(pageSize); - } + var items = source.Take(Capacity); + AddRange(items); + Initialize(pageSize); + } - /// - /// Initializes a new instance of the class. - /// - /// The queryable collection to be truncated. - /// The page size. - // NOTE: The queryable version calls Queryable.Take which actually gets translated to the backend query where as - // the enumerable version just enumerates and is inefficient. - public TruncatedCollection(IQueryable source, int pageSize) : this(source, pageSize, false) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The queryable collection to be truncated. + /// The page size. + // NOTE: The queryable version calls Queryable.Take which actually gets translated to the backend query where as + // the enumerable version just enumerates and is inefficient. + public TruncatedCollection(IQueryable source, int pageSize) : this(source, pageSize, false) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The queryable collection to be truncated. - /// The page size. - /// Flag indicating whether constants should be parameterized - // NOTE: The queryable version calls Queryable.Take which actually gets translated to the backend query where as - // the enumerable version just enumerates and is inefficient. - public TruncatedCollection(IQueryable source, int pageSize, bool parameterize) - : base(checked(pageSize + 1)) + /// + /// Initializes a new instance of the class. + /// + /// The queryable collection to be truncated. + /// The page size. + /// Flag indicating whether constants should be parameterized + // NOTE: The queryable version calls Queryable.Take which actually gets translated to the backend query where as + // the enumerable version just enumerates and is inefficient. + public TruncatedCollection(IQueryable source, int pageSize, bool parameterize) + : base(checked(pageSize + 1)) + { + var items = Take(source, pageSize, parameterize); + AddRange(items); + Initialize(pageSize); + } + + /// + /// Initializes a new instance of the class. + /// + /// The queryable collection to be truncated. + /// The page size. + /// The total count. + public TruncatedCollection(IEnumerable source, int pageSize, long? totalCount) + : base(pageSize > 0 + ? checked(pageSize + 1) + : (totalCount > 0 ? (totalCount < int.MaxValue ? (int)totalCount : int.MaxValue) : DefaultCapacity)) + { + if (pageSize > 0) { - var items = Take(source, pageSize, parameterize); - AddRange(items); - Initialize(pageSize); + AddRange(source.Take(Capacity)); } - - /// - /// Initializes a new instance of the class. - /// - /// The queryable collection to be truncated. - /// The page size. - /// The total count. - public TruncatedCollection(IEnumerable source, int pageSize, long? totalCount) - : base(pageSize > 0 - ? checked(pageSize + 1) - : (totalCount > 0 ? (totalCount < int.MaxValue ? (int)totalCount : int.MaxValue) : DefaultCapacity)) + else { - if (pageSize > 0) - { - AddRange(source.Take(Capacity)); - } - else - { - AddRange(source); - } - - if (pageSize > 0) - { - Initialize(pageSize); - } - - _totalCount = totalCount; + AddRange(source); } - /// - /// Initializes a new instance of the class. - /// - /// The queryable collection to be truncated. - /// The page size. - /// The total count. - // NOTE: The queryable version calls Queryable.Take which actually gets translated to the backend query where as - // the enumerable version just enumerates and is inefficient. - [Obsolete("should not be used, will be marked internal in the next major version")] - public TruncatedCollection(IQueryable source, int pageSize, long? totalCount) : this(source, pageSize, - totalCount, false) + if (pageSize > 0) { + Initialize(pageSize); } - /// - /// Initializes a new instance of the class. - /// - /// The queryable collection to be truncated. - /// The page size. - /// The total count. - /// Flag indicating whether constants should be parameterized - // NOTE: The queryable version calls Queryable.Take which actually gets translated to the backend query where as - // the enumerable version just enumerates and is inefficient. - [Obsolete("should not be used, will be marked internal in the next major version")] - public TruncatedCollection(IQueryable source, int pageSize, long? totalCount, bool parameterize) - : base(pageSize > 0 ? Take(source, pageSize, parameterize) : source) - { - if (pageSize > 0) - { - Initialize(pageSize); - } + _totalCount = totalCount; + } - _totalCount = totalCount; - } + /// + /// Initializes a new instance of the class. + /// + /// The queryable collection to be truncated. + /// The page size. + /// The total count. + // NOTE: The queryable version calls Queryable.Take which actually gets translated to the backend query where as + // the enumerable version just enumerates and is inefficient. + [Obsolete("should not be used, will be marked internal in the next major version")] + public TruncatedCollection(IQueryable source, int pageSize, long? totalCount) : this(source, pageSize, + totalCount, false) + { + } - private void Initialize(int pageSize) + /// + /// Initializes a new instance of the class. + /// + /// The queryable collection to be truncated. + /// The page size. + /// The total count. + /// Flag indicating whether constants should be parameterized + // NOTE: The queryable version calls Queryable.Take which actually gets translated to the backend query where as + // the enumerable version just enumerates and is inefficient. + [Obsolete("should not be used, will be marked internal in the next major version")] + public TruncatedCollection(IQueryable source, int pageSize, long? totalCount, bool parameterize) + : base(pageSize > 0 ? Take(source, pageSize, parameterize) : source) + { + if (pageSize > 0) { - if (pageSize < MinPageSize) - { - throw Error.ArgumentMustBeGreaterThanOrEqualTo("pageSize", pageSize, MinPageSize); - } - - _pageSize = pageSize; - - if (Count > pageSize) - { - _isTruncated = true; - RemoveAt(Count - 1); - } + Initialize(pageSize); } - private static IQueryable Take(IQueryable source, int pageSize, bool parameterize) - { - if (source == null) - { - throw Error.ArgumentNull("source"); - } - - return ExpressionHelpers.Take(source, checked(pageSize + 1), typeof(T), parameterize) as IQueryable; - } + _totalCount = totalCount; + } - /// - public int PageSize + private void Initialize(int pageSize) + { + if (pageSize < MinPageSize) { - get { return _pageSize; } + throw Error.ArgumentMustBeGreaterThanOrEqualTo("pageSize", pageSize, MinPageSize); } - /// - public bool IsTruncated + _pageSize = pageSize; + + if (Count > pageSize) { - get { return _isTruncated; } + _isTruncated = true; + RemoveAt(Count - 1); } + } - /// - public long? TotalCount + private static IQueryable Take(IQueryable source, int pageSize, bool parameterize) + { + if (source == null) { - get { return _totalCount; } + throw Error.ArgumentNull("source"); } + + return ExpressionHelpers.Take(source, checked(pageSize + 1), typeof(T), parameterize) as IQueryable; + } + + /// + public int PageSize + { + get { return _pageSize; } + } + + /// + public bool IsTruncated + { + get { return _isTruncated; } + } + + /// + public long? TotalCount + { + get { return _totalCount; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.OData/Query/DefaultODataQueryRequestParser.cs b/src/Microsoft.AspNetCore.OData/Query/DefaultODataQueryRequestParser.cs index 46f2178ce..ff331e15e 100644 --- a/src/Microsoft.AspNetCore.OData/Query/DefaultODataQueryRequestParser.cs +++ b/src/Microsoft.AspNetCore.OData/Query/DefaultODataQueryRequestParser.cs @@ -13,57 +13,56 @@ using Microsoft.AspNetCore.Http; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Exposes the ability to read and parse the content of a +/// into a query options part of an OData URL. Query options may be passed +/// in the request body to a resource path ending in /$query. +/// +public class DefaultODataQueryRequestParser : IODataQueryRequestParser { - /// - /// Exposes the ability to read and parse the content of a - /// into a query options part of an OData URL. Query options may be passed - /// in the request body to a resource path ending in /$query. - /// - public class DefaultODataQueryRequestParser : IODataQueryRequestParser - { - private static MediaTypeHeaderValue SupportedMediaType = MediaTypeHeaderValue.Parse("text/plain"); + private static MediaTypeHeaderValue SupportedMediaType = MediaTypeHeaderValue.Parse("text/plain"); - /// - public bool CanParse(HttpRequest request) + /// + public bool CanParse(HttpRequest request) + { + if (request == null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } - - return request.ContentType?.StartsWith(SupportedMediaType.MediaType, StringComparison.Ordinal) == true ? true : false; + throw Error.ArgumentNull(nameof(request)); } - /// - public async Task ParseAsync(HttpRequest request) + return request.ContentType?.StartsWith(SupportedMediaType.MediaType, StringComparison.Ordinal) == true ? true : false; + } + + /// + public async Task ParseAsync(HttpRequest request) + { + if (request == null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + throw Error.ArgumentNull(nameof(request)); + } - try - { - Stream requestStream = request.Body; + try + { + Stream requestStream = request.Body; - using (var reader = new StreamReader( - requestStream, - encoding: Encoding.UTF8, - detectEncodingFromByteOrderMarks: false, - bufferSize: 1024, - leaveOpen: true)) - { - // Based on OData OASIS Standard, the request body is expected to contain the query portion of the URL - // and MUST use the same percent-encoding as in URLs (especially: no spaces, tabs, or line breaks allowed) - // and MUST follow the expected syntax rules - return await reader.ReadToEndAsync().ConfigureAwait(false); - } - } - catch + using (var reader = new StreamReader( + requestStream, + encoding: Encoding.UTF8, + detectEncodingFromByteOrderMarks: false, + bufferSize: 1024, + leaveOpen: true)) { - throw new ODataException(SRResources.CannotParseQueryRequestPayload); + // Based on OData OASIS Standard, the request body is expected to contain the query portion of the URL + // and MUST use the same percent-encoding as in URLs (especially: no spaces, tabs, or line breaks allowed) + // and MUST follow the expected syntax rules + return await reader.ReadToEndAsync().ConfigureAwait(false); } } + catch + { + throw new ODataException(SRResources.CannotParseQueryRequestPayload); + } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/DefaultQueryConfigurations.cs b/src/Microsoft.AspNetCore.OData/Query/DefaultQueryConfigurations.cs index 2901320e5..df1743743 100644 --- a/src/Microsoft.AspNetCore.OData/Query/DefaultQueryConfigurations.cs +++ b/src/Microsoft.AspNetCore.OData/Query/DefaultQueryConfigurations.cs @@ -7,14 +7,13 @@ using Microsoft.OData.ModelBuilder.Config; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This class describes the default configurations to use during query composition. +/// +public class DefaultQueryConfigurations : DefaultQuerySettings { - /// - /// This class describes the default configurations to use during query composition. - /// - public class DefaultQueryConfigurations : DefaultQuerySettings - { - // We will add other query settings, for example, $compute, $search here - // In the next major release, we should remove the inheritance from 'DefaultQuerySettings'. - } + // We will add other query settings, for example, $compute, $search here + // In the next major release, we should remove the inheritance from 'DefaultQuerySettings'. } diff --git a/src/Microsoft.AspNetCore.OData/Query/ETag.cs b/src/Microsoft.AspNetCore.OData/Query/ETag.cs index bd5e6c22b..3d9d665f5 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ETag.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ETag.cs @@ -12,146 +12,145 @@ using System.Linq.Expressions; using Microsoft.AspNetCore.OData.Query.Container; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// The ETag parsed from request. +/// +public class ETag : DynamicObject { + private IDictionary _concurrencyProperties = new Dictionary(); + /// - /// The ETag parsed from request. + /// Create an instance of . /// - public class ETag : DynamicObject + public ETag() { - private IDictionary _concurrencyProperties = new Dictionary(); - - /// - /// Create an instance of . - /// - public ETag() - { - IsWellFormed = true; - } + IsWellFormed = true; + } - /// - /// Gets or sets the value associated with the specified key. - /// - /// The key of the value to get or set. - public object this[string key] + /// + /// Gets or sets the value associated with the specified key. + /// + /// The key of the value to get or set. + public object this[string key] + { + get { - get - { - if (!IsWellFormed) - { - throw Error.InvalidOperation(SRResources.ETagNotWellFormed); - } - return ConcurrencyProperties[key]; - } - set + if (!IsWellFormed) { - ConcurrencyProperties[key] = value; + throw Error.InvalidOperation(SRResources.ETagNotWellFormed); } + return ConcurrencyProperties[key]; + } + set + { + ConcurrencyProperties[key] = value; } + } - /// - /// Gets or sets whether the ETag is well-formed. - /// - public bool IsWellFormed { get; set; } + /// + /// Gets or sets whether the ETag is well-formed. + /// + public bool IsWellFormed { get; set; } - /// - /// Gets or sets an entity type of the ETag. - /// - public Type EntityType { get; set; } + /// + /// Gets or sets an entity type of the ETag. + /// + public Type EntityType { get; set; } - /// - /// Gets or sets whether the ETag is corresponding to "*". - /// - public bool IsAny { get; set; } + /// + /// Gets or sets whether the ETag is corresponding to "*". + /// + public bool IsAny { get; set; } - /// - /// Gets or sets whether If-None-Match set in the request header. - /// - public bool IsIfNoneMatch { get; set; } + /// + /// Gets or sets whether If-None-Match set in the request header. + /// + public bool IsIfNoneMatch { get; set; } - internal IDictionary ConcurrencyProperties + internal IDictionary ConcurrencyProperties + { + get { - get - { - return _concurrencyProperties; - } - set - { - _concurrencyProperties = value; - } + return _concurrencyProperties; } - - /// - /// Gets a property value from the ETag. - /// - public override bool TryGetMember(GetMemberBinder binder, out object result) + set { - if (binder == null) - { - throw Error.ArgumentNull("binder"); - } - - if (!IsWellFormed) - { - throw Error.InvalidOperation(SRResources.ETagNotWellFormed); - } - - string name = binder.Name; - return ConcurrencyProperties.TryGetValue(name, out result); + _concurrencyProperties = value; } + } - /// - /// Sets a property value to ETag. - /// - public override bool TrySetMember(SetMemberBinder binder, object value) + /// + /// Gets a property value from the ETag. + /// + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + if (binder == null) { - if (binder == null) - { - throw Error.ArgumentNull("binder"); - } + throw Error.ArgumentNull("binder"); + } - ConcurrencyProperties[binder.Name] = value; - return true; + if (!IsWellFormed) + { + throw Error.InvalidOperation(SRResources.ETagNotWellFormed); } - /// - /// Apply the ETag to the given IQueryable. - /// - /// The original . - /// The new after the ETag has been applied to. - public virtual IQueryable ApplyTo(IQueryable query) + string name = binder.Name; + return ConcurrencyProperties.TryGetValue(name, out result); + } + + /// + /// Sets a property value to ETag. + /// + public override bool TrySetMember(SetMemberBinder binder, object value) + { + if (binder == null) { - if (IsAny) - { - return query; - } + throw Error.ArgumentNull("binder"); + } - Type type = EntityType; - ParameterExpression param = Expression.Parameter(type); - Expression where = null; - foreach (KeyValuePair item in ConcurrencyProperties) - { - MemberExpression name = Expression.Property(param, item.Key); - object itemValue = item.Value; - Expression value = itemValue != null - ? LinqParameterContainer.Parameterize(itemValue.GetType(), itemValue) - : Expression.Constant(value: null); - BinaryExpression equal = Expression.Equal(name, value); - where = where == null ? equal : Expression.AndAlso(where, equal); - } + ConcurrencyProperties[binder.Name] = value; + return true; + } - if (where == null) - { - return query; - } + /// + /// Apply the ETag to the given IQueryable. + /// + /// The original . + /// The new after the ETag has been applied to. + public virtual IQueryable ApplyTo(IQueryable query) + { + if (IsAny) + { + return query; + } - if (IsIfNoneMatch) - { - where = Expression.Not(where); - } + Type type = EntityType; + ParameterExpression param = Expression.Parameter(type); + Expression where = null; + foreach (KeyValuePair item in ConcurrencyProperties) + { + MemberExpression name = Expression.Property(param, item.Key); + object itemValue = item.Value; + Expression value = itemValue != null + ? LinqParameterContainer.Parameterize(itemValue.GetType(), itemValue) + : Expression.Constant(value: null); + BinaryExpression equal = Expression.Equal(name, value); + where = where == null ? equal : Expression.AndAlso(where, equal); + } - Expression whereLambda = Expression.Lambda(where, param); - return ExpressionHelpers.Where(query, whereLambda, type); + if (where == null) + { + return query; } + + if (IsIfNoneMatch) + { + where = Expression.Not(where); + } + + Expression whereLambda = Expression.Lambda(where, param); + return ExpressionHelpers.Where(query, whereLambda, type); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/ETagOfTEntity.cs b/src/Microsoft.AspNetCore.OData/Query/ETagOfTEntity.cs index 5935f1bd8..40b43e2c5 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ETagOfTEntity.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ETagOfTEntity.cs @@ -8,61 +8,60 @@ using System.Linq; using Microsoft.AspNetCore.OData.Common; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// OData ETag of an entity type . +/// +/// TEntity is the type of entity. +public class ETag : ETag { /// - /// OData ETag of an entity type . + /// Creates an instance of . /// - /// TEntity is the type of entity. - public class ETag : ETag + public ETag() { - /// - /// Creates an instance of . - /// - public ETag() - { - EntityType = typeof(TEntity); - } + EntityType = typeof(TEntity); + } - /// - public override IQueryable ApplyTo(IQueryable query) + /// + public override IQueryable ApplyTo(IQueryable query) + { + ValidateQuery(query); + return base.ApplyTo(query); + } + + /// + /// Apply the ETag to the given . + /// + /// The original . + /// The new after the ETag has been applied. + public IQueryable ApplyTo(IQueryable query) + { + if (query == null) { - ValidateQuery(query); - return base.ApplyTo(query); + throw Error.ArgumentNull("query"); } - /// - /// Apply the ETag to the given . - /// - /// The original . - /// The new after the ETag has been applied. - public IQueryable ApplyTo(IQueryable query) - { - if (query == null) - { - throw Error.ArgumentNull("query"); - } + return (IQueryable)base.ApplyTo(query); + } - return (IQueryable)base.ApplyTo(query); + private static void ValidateQuery(IQueryable query) + { + if (query == null) + { + throw Error.ArgumentNull("query"); } - private static void ValidateQuery(IQueryable query) + if (!TypeHelper.IsTypeAssignableFrom(typeof(TEntity), query.ElementType)) { - if (query == null) - { - throw Error.ArgumentNull("query"); - } - - if (!TypeHelper.IsTypeAssignableFrom(typeof(TEntity), query.ElementType)) - { - throw Error.Argument( - "query", - SRResources.CannotApplyETagOfT, - typeof(ETag).Name, - typeof(TEntity).FullName, - typeof(IQueryable).Name, - query.ElementType.FullName); - } + throw Error.Argument( + "query", + SRResources.CannotApplyETagOfT, + typeof(ETag).Name, + typeof(TEntity).FullName, + typeof(IQueryable).Name, + query.ElementType.FullName); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.Config.cs b/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.Config.cs index 8db9a7cb0..12533031a 100644 --- a/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.Config.cs +++ b/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.Config.cs @@ -8,263 +8,262 @@ using System; using Microsoft.AspNetCore.OData.Query.Validator; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This partial class defines the configuration on . +/// +public partial class EnableQueryAttribute { + private const char CommaSeparator = ','; + + // validation settings + private ODataValidationSettings _validationSettings; + private string _allowedOrderByProperties; + + // query settings + private ODataQuerySettings _querySettings; + /// - /// This partial class defines the configuration on . + /// Enables a controller action to support OData query parameters. /// - public partial class EnableQueryAttribute + public EnableQueryAttribute() { - private const char CommaSeparator = ','; - - // validation settings - private ODataValidationSettings _validationSettings; - private string _allowedOrderByProperties; - - // query settings - private ODataQuerySettings _querySettings; + _validationSettings = new ODataValidationSettings(); + _querySettings = new ODataQuerySettings(); + } - /// - /// Enables a controller action to support OData query parameters. - /// - public EnableQueryAttribute() - { - _validationSettings = new ODataValidationSettings(); - _querySettings = new ODataQuerySettings(); - } + /// + /// Gets or sets a value indicating whether query composition should + /// alter the original query when necessary to ensure a stable sort order. + /// + /// A true value indicates the original query should + /// be modified when necessary to guarantee a stable sort order. + /// A false value indicates the sort order can be considered + /// stable without modifying the query. Query providers that ensure + /// a stable sort order should set this value to false. + /// The default value is true. + public bool EnsureStableOrdering + { + get => _querySettings.EnsureStableOrdering; + set => _querySettings.EnsureStableOrdering = value; + } - /// - /// Gets or sets a value indicating whether query composition should - /// alter the original query when necessary to ensure a stable sort order. - /// - /// A true value indicates the original query should - /// be modified when necessary to guarantee a stable sort order. - /// A false value indicates the sort order can be considered - /// stable without modifying the query. Query providers that ensure - /// a stable sort order should set this value to false. - /// The default value is true. - public bool EnsureStableOrdering - { - get => _querySettings.EnsureStableOrdering; - set => _querySettings.EnsureStableOrdering = value; - } + /// + /// Gets or sets a value indicating how null propagation should + /// be handled during query composition. + /// + /// + /// The default is . + /// + public HandleNullPropagationOption HandleNullPropagation + { + get => _querySettings.HandleNullPropagation; + set => _querySettings.HandleNullPropagation = value; + } - /// - /// Gets or sets a value indicating how null propagation should - /// be handled during query composition. - /// - /// - /// The default is . - /// - public HandleNullPropagationOption HandleNullPropagation - { - get => _querySettings.HandleNullPropagation; - set => _querySettings.HandleNullPropagation = value; - } + /// + /// Gets or sets a value indicating whether constants should be parameterized. Parameterizing constants + /// would result in better performance with Entity framework. + /// + /// The default value is true. + public bool EnableConstantParameterization + { + get => _querySettings.EnableConstantParameterization; + set => _querySettings.EnableConstantParameterization = value; + } - /// - /// Gets or sets a value indicating whether constants should be parameterized. Parameterizing constants - /// would result in better performance with Entity framework. - /// - /// The default value is true. - public bool EnableConstantParameterization - { - get => _querySettings.EnableConstantParameterization; - set => _querySettings.EnableConstantParameterization = value; - } + /// + /// Gets or sets a value indicating whether queries with expanded navigations should be formulated + /// to encourage correlated sub-query results to be buffered. + /// Buffering correlated sub-query results can reduce the number of queries from N + 1 to 2 + /// by buffering results from the sub-query. + /// + /// The default value is false. + public bool EnableCorrelatedSubqueryBuffering + { + get => _querySettings.EnableCorrelatedSubqueryBuffering; + set => _querySettings.EnableCorrelatedSubqueryBuffering = value; + } - /// - /// Gets or sets a value indicating whether queries with expanded navigations should be formulated - /// to encourage correlated sub-query results to be buffered. - /// Buffering correlated sub-query results can reduce the number of queries from N + 1 to 2 - /// by buffering results from the sub-query. - /// - /// The default value is false. - public bool EnableCorrelatedSubqueryBuffering - { - get => _querySettings.EnableCorrelatedSubqueryBuffering; - set => _querySettings.EnableCorrelatedSubqueryBuffering = value; - } + /// + /// Gets or sets the maximum depth of the Any or All elements nested inside the query. This limit helps prevent + /// Denial of Service attacks. + /// + /// + /// The maximum depth of the Any or All elements nested inside the query. The default value is 1. + /// + public int MaxAnyAllExpressionDepth + { + get => _validationSettings.MaxAnyAllExpressionDepth; + set => _validationSettings.MaxAnyAllExpressionDepth = value; + } - /// - /// Gets or sets the maximum depth of the Any or All elements nested inside the query. This limit helps prevent - /// Denial of Service attacks. - /// - /// - /// The maximum depth of the Any or All elements nested inside the query. The default value is 1. - /// - public int MaxAnyAllExpressionDepth - { - get => _validationSettings.MaxAnyAllExpressionDepth; - set => _validationSettings.MaxAnyAllExpressionDepth = value; - } + /// + /// Gets or sets the maximum number of nodes inside the $filter syntax tree. + /// + /// The default value is 100. + public int MaxNodeCount + { + get => _validationSettings.MaxNodeCount; + set => _validationSettings.MaxNodeCount = value; + } - /// - /// Gets or sets the maximum number of nodes inside the $filter syntax tree. - /// - /// The default value is 100. - public int MaxNodeCount - { - get => _validationSettings.MaxNodeCount; - set => _validationSettings.MaxNodeCount = value; - } + /// + /// Gets or sets the maximum number of query results to send back to clients. + /// + /// + /// The maximum number of query results to send back to clients. + /// + public int PageSize + { + get => _querySettings.PageSize ?? default(int); + set => _querySettings.PageSize = value; + } - /// - /// Gets or sets the maximum number of query results to send back to clients. - /// - /// - /// The maximum number of query results to send back to clients. - /// - public int PageSize - { - get => _querySettings.PageSize ?? default(int); - set => _querySettings.PageSize = value; - } + /// + /// Honor $filter inside $expand of non-collection navigation property. + /// The expanded property is only populated when the filter evaluates to true. + /// This setting is false by default. + /// + public bool HandleReferenceNavigationPropertyExpandFilter + { + get => _querySettings.HandleReferenceNavigationPropertyExpandFilter; + set => _querySettings.HandleReferenceNavigationPropertyExpandFilter = value; + } - /// - /// Honor $filter inside $expand of non-collection navigation property. - /// The expanded property is only populated when the filter evaluates to true. - /// This setting is false by default. - /// - public bool HandleReferenceNavigationPropertyExpandFilter - { - get => _querySettings.HandleReferenceNavigationPropertyExpandFilter; - set => _querySettings.HandleReferenceNavigationPropertyExpandFilter = value; - } + /// + /// Gets or sets the query parameters that are allowed in queries. + /// + /// The default includes all query options: $filter, $skip, $top, $orderby, $expand, $select, $count, + /// $format, $skiptoken and $deltatoken. + public AllowedQueryOptions AllowedQueryOptions + { + get => _validationSettings.AllowedQueryOptions; + set => _validationSettings.AllowedQueryOptions = value; + } - /// - /// Gets or sets the query parameters that are allowed in queries. - /// - /// The default includes all query options: $filter, $skip, $top, $orderby, $expand, $select, $count, - /// $format, $skiptoken and $deltatoken. - public AllowedQueryOptions AllowedQueryOptions - { - get => _validationSettings.AllowedQueryOptions; - set => _validationSettings.AllowedQueryOptions = value; - } + /// + /// Gets or sets a value that represents a list of allowed functions used in the $filter query. Supported + /// functions include the following: + /// + /// + /// String related: + /// contains, endswith, startswith, length, indexof, substring, tolower, toupper, trim, + /// concat, matchesPattern e.g. ~/Customers?$filter=length(CompanyName) eq 19 + /// + /// + /// DateTime related: + /// year, month, day, hour, minute, second, fractionalseconds, date, time + /// e.g. ~/Employees?$filter=year(BirthDate) eq 1971 + /// + /// + /// Math related: + /// round, floor, ceiling + /// + /// + /// Type related: + /// isof, cast + /// + /// + /// Collection related: + /// any, all + /// + /// + /// + public AllowedFunctions AllowedFunctions + { + get => _validationSettings.AllowedFunctions; + set => _validationSettings.AllowedFunctions = value; + } - /// - /// Gets or sets a value that represents a list of allowed functions used in the $filter query. Supported - /// functions include the following: - /// - /// - /// String related: - /// contains, endswith, startswith, length, indexof, substring, tolower, toupper, trim, - /// concat, matchesPattern e.g. ~/Customers?$filter=length(CompanyName) eq 19 - /// - /// - /// DateTime related: - /// year, month, day, hour, minute, second, fractionalseconds, date, time - /// e.g. ~/Employees?$filter=year(BirthDate) eq 1971 - /// - /// - /// Math related: - /// round, floor, ceiling - /// - /// - /// Type related: - /// isof, cast - /// - /// - /// Collection related: - /// any, all - /// - /// - /// - public AllowedFunctions AllowedFunctions - { - get => _validationSettings.AllowedFunctions; - set => _validationSettings.AllowedFunctions = value; - } + /// + /// Gets or sets a value that represents a list of allowed arithmetic operators including 'add', 'sub', 'mul', + /// 'div', 'mod'. + /// + public AllowedArithmeticOperators AllowedArithmeticOperators + { + get => _validationSettings.AllowedArithmeticOperators; + set => _validationSettings.AllowedArithmeticOperators = value; + } - /// - /// Gets or sets a value that represents a list of allowed arithmetic operators including 'add', 'sub', 'mul', - /// 'div', 'mod'. - /// - public AllowedArithmeticOperators AllowedArithmeticOperators - { - get => _validationSettings.AllowedArithmeticOperators; - set => _validationSettings.AllowedArithmeticOperators = value; - } + /// + /// Gets or sets a value that represents a list of allowed logical Operators such as 'eq', 'ne', 'gt', 'ge', + /// 'lt', 'le', 'and', 'or', 'not'. + /// + public AllowedLogicalOperators AllowedLogicalOperators + { + get => _validationSettings.AllowedLogicalOperators; + set => _validationSettings.AllowedLogicalOperators = value; + } - /// - /// Gets or sets a value that represents a list of allowed logical Operators such as 'eq', 'ne', 'gt', 'ge', - /// 'lt', 'le', 'and', 'or', 'not'. - /// - public AllowedLogicalOperators AllowedLogicalOperators + /// + /// Gets or sets a string with comma separated list of property names. The queryable result can only be + /// ordered by those properties defined in this list. + /// + /// Note, by default this string is null, which means it can be ordered by any property. + /// + /// For example, setting this value to null or empty string means that we allow ordering the queryable + /// result by any properties. Setting this value to "Name" means we only allow queryable result to be ordered + /// by Name property. + /// + public string AllowedOrderByProperties + { + get => _allowedOrderByProperties; + set { - get => _validationSettings.AllowedLogicalOperators; - set => _validationSettings.AllowedLogicalOperators = value; - } + _allowedOrderByProperties = value; - /// - /// Gets or sets a string with comma separated list of property names. The queryable result can only be - /// ordered by those properties defined in this list. - /// - /// Note, by default this string is null, which means it can be ordered by any property. - /// - /// For example, setting this value to null or empty string means that we allow ordering the queryable - /// result by any properties. Setting this value to "Name" means we only allow queryable result to be ordered - /// by Name property. - /// - public string AllowedOrderByProperties - { - get => _allowedOrderByProperties; - set + if (String.IsNullOrEmpty(value)) { - _allowedOrderByProperties = value; - - if (String.IsNullOrEmpty(value)) - { - _validationSettings.AllowedOrderByProperties.Clear(); - } - else + _validationSettings.AllowedOrderByProperties.Clear(); + } + else + { + // now parse the value and set it to validationSettings + string[] properties = _allowedOrderByProperties.Split(CommaSeparator); + for (int i = 0; i < properties.Length; i++) { - // now parse the value and set it to validationSettings - string[] properties = _allowedOrderByProperties.Split(CommaSeparator); - for (int i = 0; i < properties.Length; i++) - { - _validationSettings.AllowedOrderByProperties.Add(properties[i].Trim()); - } + _validationSettings.AllowedOrderByProperties.Add(properties[i].Trim()); } } } + } - /// - /// Gets or sets the max value of $skip that a client can request. - /// - public int MaxSkip - { - get => _validationSettings.MaxSkip ?? default(int); - set => _validationSettings.MaxSkip = value; - } + /// + /// Gets or sets the max value of $skip that a client can request. + /// + public int MaxSkip + { + get => _validationSettings.MaxSkip ?? default(int); + set => _validationSettings.MaxSkip = value; + } - /// - /// Gets or sets the max value of $top that a client can request. - /// - public int MaxTop - { - get => _validationSettings.MaxTop ?? default(int); - set => _validationSettings.MaxTop = value; - } + /// + /// Gets or sets the max value of $top that a client can request. + /// + public int MaxTop + { + get => _validationSettings.MaxTop ?? default(int); + set => _validationSettings.MaxTop = value; + } - /// - /// Gets or sets the max expansion depth for the $expand query option. To disable the maximum expansion depth - /// check, set this property to 0. - /// - public int MaxExpansionDepth - { - get => _validationSettings.MaxExpansionDepth; - set => _validationSettings.MaxExpansionDepth = value; - } + /// + /// Gets or sets the max expansion depth for the $expand query option. To disable the maximum expansion depth + /// check, set this property to 0. + /// + public int MaxExpansionDepth + { + get => _validationSettings.MaxExpansionDepth; + set => _validationSettings.MaxExpansionDepth = value; + } - /// - /// Gets or sets the maximum number of expressions that can be present in the $orderby. - /// - public int MaxOrderByNodeCount - { - get => _validationSettings.MaxOrderByNodeCount; - set => _validationSettings.MaxOrderByNodeCount = value; - } + /// + /// Gets or sets the maximum number of expressions that can be present in the $orderby. + /// + public int MaxOrderByNodeCount + { + get => _validationSettings.MaxOrderByNodeCount; + set => _validationSettings.MaxOrderByNodeCount = value; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.cs b/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.cs index bc6eee654..1ddc21247 100644 --- a/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.cs +++ b/src/Microsoft.AspNetCore.OData/Query/EnableQueryAttribute.cs @@ -31,825 +31,824 @@ using Microsoft.OData.ModelBuilder.Config; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This class defines an attribute that can be applied to an action to enable querying using the OData query +/// syntax. To avoid processing unexpected or malicious queries, use the validation settings on +/// to validate incoming queries. +/// +[SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "We want to be able to subclass this type.")] +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)] +public partial class EnableQueryAttribute : ActionFilterAttribute { /// - /// This class defines an attribute that can be applied to an action to enable querying using the OData query - /// syntax. To avoid processing unexpected or malicious queries, use the validation settings on - /// to validate incoming queries. + /// Performs the query composition before action is executing. /// - [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "We want to be able to subclass this type.")] - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)] - public partial class EnableQueryAttribute : ActionFilterAttribute + /// The action executing context. + public override void OnActionExecuting(ActionExecutingContext actionExecutingContext) { - /// - /// Performs the query composition before action is executing. - /// - /// The action executing context. - public override void OnActionExecuting(ActionExecutingContext actionExecutingContext) + if (actionExecutingContext == null) + { + throw new ArgumentNullException(nameof(actionExecutingContext)); + } + + base.OnActionExecuting(actionExecutingContext); + + RequestQueryData requestQueryData = new RequestQueryData() { - if (actionExecutingContext == null) + QueryValidationRunBeforeActionExecution = false, + }; + + actionExecutingContext.HttpContext.Items.TryAdd(nameof(RequestQueryData), requestQueryData); + + try + { + ODataQueryOptions queryOptions = CreateQueryOptionsOnExecuting(actionExecutingContext); + + if (queryOptions == null) { - throw new ArgumentNullException(nameof(actionExecutingContext)); + return; // skip validation } - base.OnActionExecuting(actionExecutingContext); + // Create and validate the query options. + requestQueryData.QueryValidationRunBeforeActionExecution = true; + requestQueryData.ProcessedQueryOptions = queryOptions; + + HttpRequest request = actionExecutingContext.HttpContext.Request; + ValidateQuery(request, requestQueryData.ProcessedQueryOptions); + } + catch (ArgumentOutOfRangeException e) + { + actionExecutingContext.Result = CreateBadRequestResult( + Error.Format(SRResources.QueryParameterNotSupported, e.Message), + e); + } + catch (NotImplementedException e) + { + actionExecutingContext.Result = CreateBadRequestResult( + Error.Format(SRResources.UriQueryStringInvalid, e.Message), + e); + } + catch (NotSupportedException e) + { + actionExecutingContext.Result = CreateBadRequestResult( + Error.Format(SRResources.UriQueryStringInvalid, e.Message), + e); + } + catch (InvalidOperationException e) + { + // Will also catch ODataException here because ODataException derives from InvalidOperationException. + actionExecutingContext.Result = CreateBadRequestResult( + Error.Format(SRResources.UriQueryStringInvalid, e.Message), + e); + } + } + + /// + /// Creates the for action executing validation. + /// + /// The action executing context. + /// The created or null if we can't create it during action executing. + protected virtual ODataQueryOptions CreateQueryOptionsOnExecuting(ActionExecutingContext actionExecutingContext) + { + if (actionExecutingContext == null) + { + throw new ArgumentNullException(nameof(actionExecutingContext)); + } - RequestQueryData requestQueryData = new RequestQueryData() - { - QueryValidationRunBeforeActionExecution = false, - }; + HttpRequest request = actionExecutingContext.HttpContext.Request; + ODataPath path = request.ODataFeature().Path; - actionExecutingContext.HttpContext.Items.TryAdd(nameof(RequestQueryData), requestQueryData); + _querySettings.TimeZone = request.GetTimeZoneInfo(); - try - { - ODataQueryOptions queryOptions = CreateQueryOptionsOnExecuting(actionExecutingContext); + ODataQueryContext queryContext; - if (queryOptions == null) - { - return; // skip validation - } + // For OData based controllers. + if (path != null) + { + IEdmType edmType = path.GetEdmType(); - // Create and validate the query options. - requestQueryData.QueryValidationRunBeforeActionExecution = true; - requestQueryData.ProcessedQueryOptions = queryOptions; - - HttpRequest request = actionExecutingContext.HttpContext.Request; - ValidateQuery(request, requestQueryData.ProcessedQueryOptions); + // When $count is at the end, the return type is always int. Trying to instead fetch the return type of the actual type being counted on. + if (request.IsCountRequest()) + { + ODataPathSegment[] pathSegments = path.ToArray(); + edmType = pathSegments[pathSegments.Length - 2].EdmType; } - catch (ArgumentOutOfRangeException e) + + IEdmType elementType = edmType.AsElementType(); + IEdmModel edmModel = request.GetModel(); + + // For Swagger metadata request. elementType is null. + if (elementType == null || edmModel == null) { - actionExecutingContext.Result = CreateBadRequestResult( - Error.Format(SRResources.QueryParameterNotSupported, e.Message), - e); + return null; } - catch (NotImplementedException e) + + if (elementType.IsUntyped()) { - actionExecutingContext.Result = CreateBadRequestResult( - Error.Format(SRResources.UriQueryStringInvalid, e.Message), - e); + // TODO: so far, we don't know how to process query on Edm.Untyped. + // So, if the query data type is Edm.Untyped, or collection of Edm.Untyped, + // Let's simply skip it now. + return null; } - catch (NotSupportedException e) + + Type clrType = edmModel.GetClrType(elementType.ToEdmTypeReference(isNullable: false)); + + // CLRType can be missing if untyped registrations were made. + if (clrType != null) { - actionExecutingContext.Result = CreateBadRequestResult( - Error.Format(SRResources.UriQueryStringInvalid, e.Message), - e); + queryContext = new ODataQueryContext(edmModel, clrType, path); } - catch (InvalidOperationException e) + else { - // Will also catch ODataException here because ODataException derives from InvalidOperationException. - actionExecutingContext.Result = CreateBadRequestResult( - Error.Format(SRResources.UriQueryStringInvalid, e.Message), - e); + // In case where CLRType is missing, $count, $expand verifications cannot be done. + // More importantly $expand required ODataQueryContext with clrType which cannot be done + // If the model is untyped. Hence for such cases, letting the validation run post action. + return null; } } - - /// - /// Creates the for action executing validation. - /// - /// The action executing context. - /// The created or null if we can't create it during action executing. - protected virtual ODataQueryOptions CreateQueryOptionsOnExecuting(ActionExecutingContext actionExecutingContext) + else { - if (actionExecutingContext == null) + // For non-OData Json based controllers. + // For these cases few options are supported like IEnumerable, Task>, T, Task + // Other cases where we cannot determine the return type upfront, are not supported + // Like IActionResult, SingleResult. For such cases, the validation is run in OnActionExecuted + // When we have the result. + ControllerActionDescriptor controllerActionDescriptor = actionExecutingContext.ActionDescriptor as ControllerActionDescriptor; + if (controllerActionDescriptor == null) { - throw new ArgumentNullException(nameof(actionExecutingContext)); + return null; } - HttpRequest request = actionExecutingContext.HttpContext.Request; - ODataPath path = request.ODataFeature().Path; - - _querySettings.TimeZone = request.GetTimeZoneInfo(); + Type returnType = controllerActionDescriptor.MethodInfo.ReturnType; + Type elementType; - ODataQueryContext queryContext; - - // For OData based controllers. - if (path != null) + // For Task<> get the base object. + if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) { - IEdmType edmType = path.GetEdmType(); - - // When $count is at the end, the return type is always int. Trying to instead fetch the return type of the actual type being counted on. - if (request.IsCountRequest()) - { - ODataPathSegment[] pathSegments = path.ToArray(); - edmType = pathSegments[pathSegments.Length - 2].EdmType; - } - - IEdmType elementType = edmType.AsElementType(); - IEdmModel edmModel = request.GetModel(); - - // For Swagger metadata request. elementType is null. - if (elementType == null || edmModel == null) - { - return null; - } - - if (elementType.IsUntyped()) - { - // TODO: so far, we don't know how to process query on Edm.Untyped. - // So, if the query data type is Edm.Untyped, or collection of Edm.Untyped, - // Let's simply skip it now. - return null; - } + returnType = returnType.GetGenericArguments().First(); + } - Type clrType = edmModel.GetClrType(elementType.ToEdmTypeReference(isNullable: false)); + // For NetCore2.2+ new type ActionResult<> was created which encapsulates IActionResult and T result. + if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ActionResult<>)) + { + returnType = returnType.GetGenericArguments().First(); + } - // CLRType can be missing if untyped registrations were made. - if (clrType != null) - { - queryContext = new ODataQueryContext(edmModel, clrType, path); - } - else - { - // In case where CLRType is missing, $count, $expand verifications cannot be done. - // More importantly $expand required ODataQueryContext with clrType which cannot be done - // If the model is untyped. Hence for such cases, letting the validation run post action. - return null; - } + if (TypeHelper.IsCollection(returnType)) + { + elementType = TypeHelper.GetImplementedIEnumerableType(returnType); + } + else if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) + { + elementType = returnType.GetGenericArguments().First(); } else { - // For non-OData Json based controllers. - // For these cases few options are supported like IEnumerable, Task>, T, Task - // Other cases where we cannot determine the return type upfront, are not supported - // Like IActionResult, SingleResult. For such cases, the validation is run in OnActionExecuted - // When we have the result. - ControllerActionDescriptor controllerActionDescriptor = actionExecutingContext.ActionDescriptor as ControllerActionDescriptor; - if (controllerActionDescriptor == null) - { - return null; - } - - Type returnType = controllerActionDescriptor.MethodInfo.ReturnType; - Type elementType; - - // For Task<> get the base object. - if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) - { - returnType = returnType.GetGenericArguments().First(); - } + return null; + } - // For NetCore2.2+ new type ActionResult<> was created which encapsulates IActionResult and T result. - if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ActionResult<>)) - { - returnType = returnType.GetGenericArguments().First(); - } + IEdmModel edmModel = GetModel(elementType, request, controllerActionDescriptor); + queryContext = new ODataQueryContext(edmModel, elementType); + } - if (TypeHelper.IsCollection(returnType)) - { - elementType = TypeHelper.GetImplementedIEnumerableType(returnType); - } - else if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) - { - elementType = returnType.GetGenericArguments().First(); - } - else - { - return null; - } + // Create and validate the query options. + return new ODataQueryOptions(queryContext, request); + } - IEdmModel edmModel = GetModel(elementType, request, controllerActionDescriptor); - queryContext = new ODataQueryContext(edmModel, elementType); - } + /// + /// Performs the query composition after action is executed. It first tries to retrieve the IQueryable from the + /// returning response message. It then validates the query from uri based on the validation settings on + /// . It finally applies the query appropriately, and reset it back on + /// the response message. + /// + /// The context related to this action, including the response message, + /// request message and HttpConfiguration etc. + public override void OnActionExecuted(ActionExecutedContext actionExecutedContext) + { + if (actionExecutedContext == null) + { + throw new ArgumentNullException(nameof(actionExecutedContext)); + } - // Create and validate the query options. - return new ODataQueryOptions(queryContext, request); + HttpRequest request = actionExecutedContext.HttpContext.Request; + if (request == null) + { + throw Error.Argument("actionExecutedContext", SRResources.ActionExecutedContextMustHaveRequest); } - /// - /// Performs the query composition after action is executed. It first tries to retrieve the IQueryable from the - /// returning response message. It then validates the query from uri based on the validation settings on - /// . It finally applies the query appropriately, and reset it back on - /// the response message. - /// - /// The context related to this action, including the response message, - /// request message and HttpConfiguration etc. - public override void OnActionExecuted(ActionExecutedContext actionExecutedContext) + ActionDescriptor actionDescriptor = actionExecutedContext.ActionDescriptor; + if (actionDescriptor == null) { - if (actionExecutedContext == null) - { - throw new ArgumentNullException(nameof(actionExecutedContext)); - } + throw Error.Argument("actionExecutedContext", SRResources.ActionContextMustHaveDescriptor); + } - HttpRequest request = actionExecutedContext.HttpContext.Request; - if (request == null) - { - throw Error.Argument("actionExecutedContext", SRResources.ActionExecutedContextMustHaveRequest); - } + HttpResponse response = actionExecutedContext.HttpContext.Response; - ActionDescriptor actionDescriptor = actionExecutedContext.ActionDescriptor; - if (actionDescriptor == null) + // Check is the response is set and successful. + if (response != null && IsSuccessStatusCode(response.StatusCode) && actionExecutedContext.Result != null) + { + // actionExecutedContext.Result might also indicate a status code that has not yet + // been applied to the result; make sure it's also successful. + IStatusCodeActionResult statusCodeResult = actionExecutedContext.Result as IStatusCodeActionResult; + if (statusCodeResult?.StatusCode == null || IsSuccessStatusCode(statusCodeResult.StatusCode.Value)) { - throw Error.Argument("actionExecutedContext", SRResources.ActionContextMustHaveDescriptor); - } + ObjectResult responseContent = actionExecutedContext.Result as ObjectResult; - HttpResponse response = actionExecutedContext.HttpContext.Response; + ControllerActionDescriptor controllerActionDescriptor = actionDescriptor as ControllerActionDescriptor; + Type returnType = controllerActionDescriptor.MethodInfo.ReturnType; - // Check is the response is set and successful. - if (response != null && IsSuccessStatusCode(response.StatusCode) && actionExecutedContext.Result != null) - { - // actionExecutedContext.Result might also indicate a status code that has not yet - // been applied to the result; make sure it's also successful. - IStatusCodeActionResult statusCodeResult = actionExecutedContext.Result as IStatusCodeActionResult; - if (statusCodeResult?.StatusCode == null || IsSuccessStatusCode(statusCodeResult.StatusCode.Value)) + if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ActionResult<>)) { - ObjectResult responseContent = actionExecutedContext.Result as ObjectResult; - - ControllerActionDescriptor controllerActionDescriptor = actionDescriptor as ControllerActionDescriptor; - Type returnType = controllerActionDescriptor.MethodInfo.ReturnType; + returnType = returnType.GetGenericArguments().First(); - if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ActionResult<>)) + if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) { - returnType = returnType.GetGenericArguments().First(); - - if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) - { - responseContent.DeclaredType = returnType; - } + responseContent.DeclaredType = returnType; } + } - if (responseContent != null) + if (responseContent != null) + { + // Get collection from SingleResult. + IQueryable singleResultCollection = null; + SingleResult singleResult = responseContent.Value as SingleResult; + if (singleResult != null) { - // Get collection from SingleResult. - IQueryable singleResultCollection = null; - SingleResult singleResult = responseContent.Value as SingleResult; - if (singleResult != null) - { - // This could be a SingleResult, which has the property Queryable. - // But it could be a SingleResult() or SingleResult. Sort by number of parameters - // on the property and get the one with the most parameters. - PropertyInfo propInfo = responseContent.Value.GetType().GetProperties() - .OrderBy(p => p.GetIndexParameters().Length) - .Where(p => p.Name.Equals("Queryable", StringComparison.Ordinal)) - .LastOrDefault(); - - singleResultCollection = propInfo.GetValue(singleResult) as IQueryable; - } - - // Execution the action. - object queryResult = OnActionExecuted( - actionExecutedContext, - responseContent.Value, - singleResultCollection, - actionDescriptor as ControllerActionDescriptor, - request); - - if (queryResult != null) - { - responseContent.Value = queryResult; - } + // This could be a SingleResult, which has the property Queryable. + // But it could be a SingleResult() or SingleResult. Sort by number of parameters + // on the property and get the one with the most parameters. + PropertyInfo propInfo = responseContent.Value.GetType().GetProperties() + .OrderBy(p => p.GetIndexParameters().Length) + .Where(p => p.Name.Equals("Queryable", StringComparison.Ordinal)) + .LastOrDefault(); + + singleResultCollection = propInfo.GetValue(singleResult) as IQueryable; } - } - } - } - /// - /// Performs the query composition after action is executed. It first tries to retrieve the IQueryable from the - /// returning response message. It then validates the query from uri based on the validation settings on - /// . It finally applies the query appropriately, and reset it back on - /// the response message. - /// - /// . - /// The response content value. - /// The content as SingleResult.Queryable. - /// The action context, i.e. action and controller name. - /// The internal request. - private object OnActionExecuted( - ActionExecutedContext actionExecutedContext, - object responseValue, - IQueryable singleResultCollection, - ControllerActionDescriptor actionDescriptor, - HttpRequest request) - { - if (!_querySettings.PageSize.HasValue && responseValue != null) - { - GetModelBoundPageSize(actionExecutedContext, responseValue, singleResultCollection, actionDescriptor, request); - } + // Execution the action. + object queryResult = OnActionExecuted( + actionExecutedContext, + responseContent.Value, + singleResultCollection, + actionDescriptor as ControllerActionDescriptor, + request); - // Apply the query if there are any query options, if there is a page size set, in the case of - // SingleResult or in the case of $count request. - bool shouldApplyQuery = responseValue != null && - request.GetEncodedUrl() != null && - (!String.IsNullOrWhiteSpace(request.QueryString.Value) || - _querySettings.PageSize.HasValue || - _querySettings.ModelBoundPageSize.HasValue || - singleResultCollection != null || - request.IsCountRequest() || - ContainsAutoSelectExpandProperty(responseValue, singleResultCollection, actionDescriptor, request)); - - object returnValue = null; - if (shouldApplyQuery) - { - try - { - object queryResult = ExecuteQuery(responseValue, singleResultCollection, actionDescriptor, request); - if (queryResult == null && (request.ODataFeature().Path == null || singleResultCollection != null)) + if (queryResult != null) { - // This is the case in which a regular OData service uses the EnableQuery attribute. - // For OData services ODataNullValueMessageHandler should be plugged in for the service - // if this behavior is desired. - // For non OData services this behavior is equivalent as the one in the v3 version in order - // to reduce the friction when they decide to move to use the v4 EnableQueryAttribute. - actionExecutedContext.Result = new StatusCodeResult((int)HttpStatusCode.NotFound); + responseContent.Value = queryResult; } - - returnValue = queryResult; - } - catch (ArgumentOutOfRangeException e) - { - actionExecutedContext.Result = CreateBadRequestResult(Error.Format(SRResources.QueryParameterNotSupported, e.Message), e); - } - catch (NotImplementedException e) - { - actionExecutedContext.Result = CreateBadRequestResult(Error.Format(SRResources.UriQueryStringInvalid, e.Message), e); - } - catch (NotSupportedException e) - { - actionExecutedContext.Result = CreateBadRequestResult(Error.Format(SRResources.UriQueryStringInvalid, e.Message), e); - } - catch (InvalidOperationException e) - { - // Will also catch ODataException here because ODataException derives from InvalidOperationException. - actionExecutedContext.Result = CreateBadRequestResult(Error.Format(SRResources.UriQueryStringInvalid, e.Message), e); } } - - return returnValue; } + } - /// - /// Get the page size. - /// - /// The response value. - /// The response value. - /// The content as SingleResult.Queryable. - /// The action context, i.e. action and controller name. - /// The request. - private void GetModelBoundPageSize( - ActionExecutedContext actionExecutedContext, - object responseValue, - IQueryable singleResultCollection, - ControllerActionDescriptor actionDescriptor, - HttpRequest request) - { - ODataQueryContext queryContext; + /// + /// Performs the query composition after action is executed. It first tries to retrieve the IQueryable from the + /// returning response message. It then validates the query from uri based on the validation settings on + /// . It finally applies the query appropriately, and reset it back on + /// the response message. + /// + /// . + /// The response content value. + /// The content as SingleResult.Queryable. + /// The action context, i.e. action and controller name. + /// The internal request. + private object OnActionExecuted( + ActionExecutedContext actionExecutedContext, + object responseValue, + IQueryable singleResultCollection, + ControllerActionDescriptor actionDescriptor, + HttpRequest request) + { + if (!_querySettings.PageSize.HasValue && responseValue != null) + { + GetModelBoundPageSize(actionExecutedContext, responseValue, singleResultCollection, actionDescriptor, request); + } + // Apply the query if there are any query options, if there is a page size set, in the case of + // SingleResult or in the case of $count request. + bool shouldApplyQuery = responseValue != null && + request.GetEncodedUrl() != null && + (!String.IsNullOrWhiteSpace(request.QueryString.Value) || + _querySettings.PageSize.HasValue || + _querySettings.ModelBoundPageSize.HasValue || + singleResultCollection != null || + request.IsCountRequest() || + ContainsAutoSelectExpandProperty(responseValue, singleResultCollection, actionDescriptor, request)); + + object returnValue = null; + if (shouldApplyQuery) + { try { - queryContext = GetODataQueryContext(responseValue, singleResultCollection, actionDescriptor, request); + object queryResult = ExecuteQuery(responseValue, singleResultCollection, actionDescriptor, request); + if (queryResult == null && (request.ODataFeature().Path == null || singleResultCollection != null)) + { + // This is the case in which a regular OData service uses the EnableQuery attribute. + // For OData services ODataNullValueMessageHandler should be plugged in for the service + // if this behavior is desired. + // For non OData services this behavior is equivalent as the one in the v3 version in order + // to reduce the friction when they decide to move to use the v4 EnableQueryAttribute. + actionExecutedContext.Result = new StatusCodeResult((int)HttpStatusCode.NotFound); + } + + returnValue = queryResult; + } + catch (ArgumentOutOfRangeException e) + { + actionExecutedContext.Result = CreateBadRequestResult(Error.Format(SRResources.QueryParameterNotSupported, e.Message), e); } - catch (InvalidOperationException e) + catch (NotImplementedException e) { actionExecutedContext.Result = CreateBadRequestResult(Error.Format(SRResources.UriQueryStringInvalid, e.Message), e); - return; } - - ModelBoundQuerySettings querySettings = queryContext.Model.GetModelBoundQuerySettings(queryContext.TargetProperty, - queryContext.TargetStructuredType); - if (querySettings != null && querySettings.PageSize.HasValue) + catch (NotSupportedException e) + { + actionExecutedContext.Result = CreateBadRequestResult(Error.Format(SRResources.UriQueryStringInvalid, e.Message), e); + } + catch (InvalidOperationException e) { - _querySettings.ModelBoundPageSize = querySettings.PageSize; + // Will also catch ODataException here because ODataException derives from InvalidOperationException. + actionExecutedContext.Result = CreateBadRequestResult(Error.Format(SRResources.UriQueryStringInvalid, e.Message), e); } } - /// - /// Create a BadRequestObjectResult. - /// - /// The error message. - /// The exception. - /// A BadRequestObjectResult. - private static BadRequestObjectResult CreateBadRequestResult(string message, Exception exception) + return returnValue; + } + + /// + /// Get the page size. + /// + /// The response value. + /// The response value. + /// The content as SingleResult.Queryable. + /// The action context, i.e. action and controller name. + /// The request. + private void GetModelBoundPageSize( + ActionExecutedContext actionExecutedContext, + object responseValue, + IQueryable singleResultCollection, + ControllerActionDescriptor actionDescriptor, + HttpRequest request) + { + ODataQueryContext queryContext; + + try + { + queryContext = GetODataQueryContext(responseValue, singleResultCollection, actionDescriptor, request); + } + catch (InvalidOperationException e) { - SerializableError error = CreateErrorResponse(message, exception); - return new BadRequestObjectResult(error); + actionExecutedContext.Result = CreateBadRequestResult(Error.Format(SRResources.UriQueryStringInvalid, e.Message), e); + return; } - /// - /// Create an error response. - /// - /// The message of the error. - /// The error exception if any. - /// A SerializableError. - /// This function is recursive. - public static SerializableError CreateErrorResponse(string message, Exception exception = null) - { - // The key values mimic the behavior of HttpError in AspNet. It's a fine format - // and many of the test cases expect it. - SerializableError error = new SerializableError(); - if (!String.IsNullOrEmpty(message)) - { - error.Add(SerializableErrorKeys.MessageKey, message); - } + ModelBoundQuerySettings querySettings = queryContext.Model.GetModelBoundQuerySettings(queryContext.TargetProperty, + queryContext.TargetStructuredType); + if (querySettings != null && querySettings.PageSize.HasValue) + { + _querySettings.ModelBoundPageSize = querySettings.PageSize; + } + } - if (exception != null) - { - error.Add(SerializableErrorKeys.ExceptionMessageKey, exception.Message); - error.Add(SerializableErrorKeys.ExceptionTypeKey, exception.GetType().FullName); - error.Add(SerializableErrorKeys.StackTraceKey, exception.StackTrace); - if (exception.InnerException != null) - { - error.Add(SerializableErrorKeys.InnerExceptionKey, CreateErrorResponse(String.Empty, exception.InnerException)); - } - } + /// + /// Create a BadRequestObjectResult. + /// + /// The error message. + /// The exception. + /// A BadRequestObjectResult. + private static BadRequestObjectResult CreateBadRequestResult(string message, Exception exception) + { + SerializableError error = CreateErrorResponse(message, exception); + return new BadRequestObjectResult(error); + } - return error; + /// + /// Create an error response. + /// + /// The message of the error. + /// The error exception if any. + /// A SerializableError. + /// This function is recursive. + public static SerializableError CreateErrorResponse(string message, Exception exception = null) + { + // The key values mimic the behavior of HttpError in AspNet. It's a fine format + // and many of the test cases expect it. + SerializableError error = new SerializableError(); + if (!String.IsNullOrEmpty(message)) + { + error.Add(SerializableErrorKeys.MessageKey, message); } - /// - /// Determine if the status code indicates success. - /// - /// The status code. - /// True if the response has a success status code; false otherwise. - private static bool IsSuccessStatusCode(int statusCode) + if (exception != null) { - return statusCode >= 200 && statusCode < 300; + error.Add(SerializableErrorKeys.ExceptionMessageKey, exception.Message); + error.Add(SerializableErrorKeys.ExceptionTypeKey, exception.GetType().FullName); + error.Add(SerializableErrorKeys.StackTraceKey, exception.StackTrace); + if (exception.InnerException != null) + { + error.Add(SerializableErrorKeys.InnerExceptionKey, CreateErrorResponse(String.Empty, exception.InnerException)); + } } - /// - /// Execute the query. - /// - /// The response value. - /// The content as SingleResult.Queryable. - /// The action context, i.e. action and controller name. - /// The internal request. - /// - private object ExecuteQuery( - object responseValue, - IQueryable singleResultCollection, - ControllerActionDescriptor actionDescriptor, - HttpRequest request) - { - ODataQueryContext queryContext = GetODataQueryContext(responseValue, singleResultCollection, actionDescriptor, request); + return error; + } - // Create and validate the query options. - ODataQueryOptions queryOptions = CreateAndValidateQueryOptions(request, queryContext); + /// + /// Determine if the status code indicates success. + /// + /// The status code. + /// True if the response has a success status code; false otherwise. + private static bool IsSuccessStatusCode(int statusCode) + { + return statusCode >= 200 && statusCode < 300; + } - // apply the query - IEnumerable enumerable = responseValue as IEnumerable; - if (enumerable == null || responseValue is string || responseValue is byte[]) - { - // response is not a collection; we only support $select and $expand on single entities. - ValidateSelectExpandOnly(queryOptions); + /// + /// Execute the query. + /// + /// The response value. + /// The content as SingleResult.Queryable. + /// The action context, i.e. action and controller name. + /// The internal request. + /// + private object ExecuteQuery( + object responseValue, + IQueryable singleResultCollection, + ControllerActionDescriptor actionDescriptor, + HttpRequest request) + { + ODataQueryContext queryContext = GetODataQueryContext(responseValue, singleResultCollection, actionDescriptor, request); - if (singleResultCollection == null) - { - // response is a single entity. - return ApplyQuery(entity: responseValue, queryOptions: queryOptions); - } - else - { - IQueryable queryable = singleResultCollection as IQueryable; - queryable = ApplyQuery(queryable, queryOptions); - return SingleOrDefault(queryable, actionDescriptor); - } + // Create and validate the query options. + ODataQueryOptions queryOptions = CreateAndValidateQueryOptions(request, queryContext); + + // apply the query + IEnumerable enumerable = responseValue as IEnumerable; + if (enumerable == null || responseValue is string || responseValue is byte[]) + { + // response is not a collection; we only support $select and $expand on single entities. + ValidateSelectExpandOnly(queryOptions); + + if (singleResultCollection == null) + { + // response is a single entity. + return ApplyQuery(entity: responseValue, queryOptions: queryOptions); } else { - // response is a collection. - IQueryable queryable = (enumerable as IQueryable) ?? enumerable.AsQueryable(); + IQueryable queryable = singleResultCollection as IQueryable; queryable = ApplyQuery(queryable, queryOptions); + return SingleOrDefault(queryable, actionDescriptor); + } + } + else + { + // response is a collection. + IQueryable queryable = (enumerable as IQueryable) ?? enumerable.AsQueryable(); + queryable = ApplyQuery(queryable, queryOptions); - if (request.IsCountRequest()) - { - long? count = request.ODataFeature().TotalCount; + if (request.IsCountRequest()) + { + long? count = request.ODataFeature().TotalCount; - if (count.HasValue) - { - // Return the count value if it is a $count request. - return count.Value; - } + if (count.HasValue) + { + // Return the count value if it is a $count request. + return count.Value; } - - return queryable; } + + return queryable; } + } - /// - /// Applies the query to the given IQueryable based on incoming query from uri and query settings. By default, - /// the implementation supports $top, $skip, $orderby and $filter. Override this method to perform additional - /// query composition of the query. - /// - /// The original queryable instance from the response message. - /// - /// The instance constructed based on the incoming request. - /// - public virtual IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions) + /// + /// Applies the query to the given IQueryable based on incoming query from uri and query settings. By default, + /// the implementation supports $top, $skip, $orderby and $filter. Override this method to perform additional + /// query composition of the query. + /// + /// The original queryable instance from the response message. + /// + /// The instance constructed based on the incoming request. + /// + public virtual IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions) + { + if (queryable == null) { - if (queryable == null) - { - throw Error.ArgumentNull("queryable"); - } - if (queryOptions == null) - { - throw Error.ArgumentNull("queryOptions"); - } + throw Error.ArgumentNull("queryable"); + } + if (queryOptions == null) + { + throw Error.ArgumentNull("queryOptions"); + } + + return queryOptions.ApplyTo(queryable, _querySettings); + } - return queryOptions.ApplyTo(queryable, _querySettings); + /// + /// Applies the query to the given entity based on incoming query from uri and query settings. + /// + /// The original entity from the response message. + /// + /// The instance constructed based on the incoming request. + /// + /// The new entity after the $select and $expand query has been applied to. + public virtual object ApplyQuery(object entity, ODataQueryOptions queryOptions) + { + if (entity == null) + { + throw Error.ArgumentNull("entity"); + } + if (queryOptions == null) + { + throw Error.ArgumentNull("queryOptions"); } - /// - /// Applies the query to the given entity based on incoming query from uri and query settings. - /// - /// The original entity from the response message. - /// - /// The instance constructed based on the incoming request. - /// - /// The new entity after the $select and $expand query has been applied to. - public virtual object ApplyQuery(object entity, ODataQueryOptions queryOptions) - { - if (entity == null) - { - throw Error.ArgumentNull("entity"); - } - if (queryOptions == null) - { - throw Error.ArgumentNull("queryOptions"); - } + return queryOptions.ApplyTo(entity, _querySettings); + } - return queryOptions.ApplyTo(entity, _querySettings); + /// + /// Create and validate a new instance of from a query and context during action executed. + /// Developers can override this virtual method to provide its own . + /// + /// The incoming request. + /// The query context. + /// The created . + protected virtual ODataQueryOptions CreateAndValidateQueryOptions(HttpRequest request, ODataQueryContext queryContext) + { + if (request == null) + { + throw Error.ArgumentNull("request"); } - /// - /// Create and validate a new instance of from a query and context during action executed. - /// Developers can override this virtual method to provide its own . - /// - /// The incoming request. - /// The query context. - /// The created . - protected virtual ODataQueryOptions CreateAndValidateQueryOptions(HttpRequest request, ODataQueryContext queryContext) + if (queryContext == null) { - if (request == null) - { - throw Error.ArgumentNull("request"); - } + throw Error.ArgumentNull("queryContext"); + } - if (queryContext == null) - { - throw Error.ArgumentNull("queryContext"); - } + RequestQueryData requestQueryData = request.HttpContext.Items[nameof(RequestQueryData)] as RequestQueryData; - RequestQueryData requestQueryData = request.HttpContext.Items[nameof(RequestQueryData)] as RequestQueryData; + if (requestQueryData != null && requestQueryData.QueryValidationRunBeforeActionExecution) + { + // processed, just return the query option and skip validation. + return requestQueryData.ProcessedQueryOptions; + } - if (requestQueryData != null && requestQueryData.QueryValidationRunBeforeActionExecution) - { - // processed, just return the query option and skip validation. - return requestQueryData.ProcessedQueryOptions; - } + ODataQueryOptions queryOptions = new ODataQueryOptions(queryContext, request); - ODataQueryOptions queryOptions = new ODataQueryOptions(queryContext, request); + ValidateQuery(request, queryOptions); - ValidateQuery(request, queryOptions); + return queryOptions; + } - return queryOptions; - } + /// + /// Get a single or default value from a collection. + /// + /// The response value as . + /// The action context, i.e. action and controller name. + /// + internal static object SingleOrDefault( + IQueryable queryable, + ControllerActionDescriptor actionDescriptor) + { + var enumerator = queryable.GetEnumerator(); + try + { + var result = enumerator.MoveNext() ? enumerator.Current : null; - /// - /// Get a single or default value from a collection. - /// - /// The response value as . - /// The action context, i.e. action and controller name. - /// - internal static object SingleOrDefault( - IQueryable queryable, - ControllerActionDescriptor actionDescriptor) - { - var enumerator = queryable.GetEnumerator(); - try + if (enumerator.MoveNext()) { - var result = enumerator.MoveNext() ? enumerator.Current : null; - - if (enumerator.MoveNext()) - { - throw new InvalidOperationException(Error.Format( - SRResources.SingleResultHasMoreThanOneEntity, - actionDescriptor.ActionName, - actionDescriptor.ControllerName, - "SingleResult")); - } - - return result; + throw new InvalidOperationException(Error.Format( + SRResources.SingleResultHasMoreThanOneEntity, + actionDescriptor.ActionName, + actionDescriptor.ControllerName, + "SingleResult")); } - finally + + return result; + } + finally + { + // Ensure any active/open database objects that were created + // iterating over the IQueryable object are properly closed. + var disposable = enumerator as IDisposable; + if (disposable != null) { - // Ensure any active/open database objects that were created - // iterating over the IQueryable object are properly closed. - var disposable = enumerator as IDisposable; - if (disposable != null) - { - disposable.Dispose(); - } + disposable.Dispose(); } } + } - /// - /// Validate the select and expand options. - /// - /// The query options. - internal static void ValidateSelectExpandOnly(ODataQueryOptions queryOptions) + /// + /// Validate the select and expand options. + /// + /// The query options. + internal static void ValidateSelectExpandOnly(ODataQueryOptions queryOptions) + { + if (queryOptions.Filter != null || queryOptions.Count != null || queryOptions.OrderBy != null + || queryOptions.Skip != null || queryOptions.Top != null) { - if (queryOptions.Filter != null || queryOptions.Count != null || queryOptions.OrderBy != null - || queryOptions.Skip != null || queryOptions.Top != null) - { - throw new ODataException(Error.Format(SRResources.NonSelectExpandOnSingleEntity)); - } + throw new ODataException(Error.Format(SRResources.NonSelectExpandOnSingleEntity)); } + } - /// - /// Get the OData query context. - /// - /// The response value. - /// The content as SingleResult.Queryable. - /// The action context, i.e. action and controller name. - /// The OData path. - /// - private ODataQueryContext GetODataQueryContext( - object responseValue, - IQueryable singleResultCollection, - ControllerActionDescriptor actionDescriptor, - HttpRequest request) - { - Type elementClrType = GetElementType(responseValue, singleResultCollection, actionDescriptor); - - IEdmModel model = GetModel(elementClrType, request, actionDescriptor); - if (model == null) - { - throw Error.InvalidOperation(SRResources.QueryGetModelMustNotReturnNull); - } + /// + /// Get the OData query context. + /// + /// The response value. + /// The content as SingleResult.Queryable. + /// The action context, i.e. action and controller name. + /// The OData path. + /// + private ODataQueryContext GetODataQueryContext( + object responseValue, + IQueryable singleResultCollection, + ControllerActionDescriptor actionDescriptor, + HttpRequest request) + { + Type elementClrType = GetElementType(responseValue, singleResultCollection, actionDescriptor); - return new ODataQueryContext(model, elementClrType, request.ODataFeature().Path); + IEdmModel model = GetModel(elementClrType, request, actionDescriptor); + if (model == null) + { + throw Error.InvalidOperation(SRResources.QueryGetModelMustNotReturnNull); } - /// - /// Get the element type. - /// - /// The response value. - /// The content as SingleResult.Queryable. - /// The action context, i.e. action and controller name. - /// - internal static Type GetElementType( - object responseValue, - IQueryable singleResultCollection, - ControllerActionDescriptor actionDescriptor) - { - Contract.Assert(responseValue != null); - - IEnumerable enumerable = responseValue as IEnumerable; - if (enumerable == null) - { - if (singleResultCollection == null) - { - return responseValue.GetType(); - } + return new ODataQueryContext(model, elementClrType, request.ODataFeature().Path); + } - enumerable = singleResultCollection; - } + /// + /// Get the element type. + /// + /// The response value. + /// The content as SingleResult.Queryable. + /// The action context, i.e. action and controller name. + /// + internal static Type GetElementType( + object responseValue, + IQueryable singleResultCollection, + ControllerActionDescriptor actionDescriptor) + { + Contract.Assert(responseValue != null); - Type elementClrType = TypeHelper.GetImplementedIEnumerableType(enumerable.GetType()); - if (elementClrType == null) + IEnumerable enumerable = responseValue as IEnumerable; + if (enumerable == null) + { + if (singleResultCollection == null) { - // The element type cannot be determined because the type of the content - // is not IEnumerable or IQueryable. - throw Error.InvalidOperation( - SRResources.FailedToRetrieveTypeToBuildEdmModel, - typeof(EnableQueryAttribute).Name, - actionDescriptor.ActionName, - actionDescriptor.ControllerName, - responseValue.GetType().FullName); + return responseValue.GetType(); } - return elementClrType; + enumerable = singleResultCollection; } - /// - /// Validates the OData query in the incoming request. By default, the implementation throws an exception if - /// the query contains unsupported query parameters. Override this method to perform additional validation of - /// the query. - /// - /// The incoming request. - /// - /// The instance constructed based on the incoming request. - /// - public virtual void ValidateQuery(HttpRequest request, ODataQueryOptions queryOptions) + Type elementClrType = TypeHelper.GetImplementedIEnumerableType(enumerable.GetType()); + if (elementClrType == null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + // The element type cannot be determined because the type of the content + // is not IEnumerable or IQueryable. + throw Error.InvalidOperation( + SRResources.FailedToRetrieveTypeToBuildEdmModel, + typeof(EnableQueryAttribute).Name, + actionDescriptor.ActionName, + actionDescriptor.ControllerName, + responseValue.GetType().FullName); + } - if (queryOptions == null) - { - throw Error.ArgumentNull(nameof(queryOptions)); - } + return elementClrType; + } - IQueryCollection queryParameters = request.Query; - foreach (var kvp in queryParameters) - { - if (!queryOptions.IsSupportedQueryOption(kvp.Key) && - kvp.Key.StartsWith("$", StringComparison.Ordinal)) - { - // we don't support any custom query options that start with $ - // this should be caught be OnActionExecuted(). - throw new ODataException(Error.Format(SRResources.CustomQueryOptionNotSupportedWithDollarSign, kvp.Key)); - } - } + /// + /// Validates the OData query in the incoming request. By default, the implementation throws an exception if + /// the query contains unsupported query parameters. Override this method to perform additional validation of + /// the query. + /// + /// The incoming request. + /// + /// The instance constructed based on the incoming request. + /// + public virtual void ValidateQuery(HttpRequest request, ODataQueryOptions queryOptions) + { + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); + } - queryOptions.Validate(_validationSettings); + if (queryOptions == null) + { + throw Error.ArgumentNull(nameof(queryOptions)); } - /// - /// Determine if the query contains auto select and expand property. - /// - /// The response value. - /// The content as SingleResult.Queryable. - /// The action context, i.e. action and controller name. - /// The Http request. - /// true/false - private bool ContainsAutoSelectExpandProperty(object responseValue, IQueryable singleResultCollection, - ControllerActionDescriptor actionDescriptor, HttpRequest request) - { - Type elementClrType = GetElementType(responseValue, singleResultCollection, actionDescriptor); - - IEdmModel model = GetModel(elementClrType, request, actionDescriptor); - if (model == null) + IQueryCollection queryParameters = request.Query; + foreach (var kvp in queryParameters) + { + if (!queryOptions.IsSupportedQueryOption(kvp.Key) && + kvp.Key.StartsWith("$", StringComparison.Ordinal)) { - throw Error.InvalidOperation(SRResources.QueryGetModelMustNotReturnNull); + // we don't support any custom query options that start with $ + // this should be caught be OnActionExecuted(). + throw new ODataException(Error.Format(SRResources.CustomQueryOptionNotSupportedWithDollarSign, kvp.Key)); } - IEdmType edmType = model.GetEdmTypeReference(elementClrType)?.Definition; + } - IEdmStructuredType structuredType = edmType as IEdmStructuredType; - ODataPath path = request.ODataFeature().Path; + queryOptions.Validate(_validationSettings); + } - IEdmProperty pathProperty = null; - IEdmStructuredType pathStructuredType = null; - if (path != null) - { - (pathProperty, pathStructuredType, _) = path.GetPropertyAndStructuredTypeFromPath(); - } + /// + /// Determine if the query contains auto select and expand property. + /// + /// The response value. + /// The content as SingleResult.Queryable. + /// The action context, i.e. action and controller name. + /// The Http request. + /// true/false + private bool ContainsAutoSelectExpandProperty(object responseValue, IQueryable singleResultCollection, + ControllerActionDescriptor actionDescriptor, HttpRequest request) + { + Type elementClrType = GetElementType(responseValue, singleResultCollection, actionDescriptor); - // Take the type and property from path first, it's higher priority than the value type. - if (pathStructuredType != null && pathProperty != null) - { - return model.HasAutoExpandProperty(pathStructuredType, pathProperty) || model.HasAutoSelectProperty(pathStructuredType, pathProperty); - } - else if (structuredType != null) - { - return model.HasAutoExpandProperty(structuredType, null) || model.HasAutoSelectProperty(structuredType, null); - } + IEdmModel model = GetModel(elementClrType, request, actionDescriptor); + if (model == null) + { + throw Error.InvalidOperation(SRResources.QueryGetModelMustNotReturnNull); + } + IEdmType edmType = model.GetEdmTypeReference(elementClrType)?.Definition; + + IEdmStructuredType structuredType = edmType as IEdmStructuredType; + ODataPath path = request.ODataFeature().Path; + + IEdmProperty pathProperty = null; + IEdmStructuredType pathStructuredType = null; + if (path != null) + { + (pathProperty, pathStructuredType, _) = path.GetPropertyAndStructuredTypeFromPath(); + } - return false; + // Take the type and property from path first, it's higher priority than the value type. + if (pathStructuredType != null && pathProperty != null) + { + return model.HasAutoExpandProperty(pathStructuredType, pathProperty) || model.HasAutoSelectProperty(pathStructuredType, pathProperty); + } + else if (structuredType != null) + { + return model.HasAutoExpandProperty(structuredType, null) || model.HasAutoSelectProperty(structuredType, null); } - /// - /// Gets the EDM model for the given type and request.Override this method to customize the EDM model used for - /// querying. - /// - /// The CLR type to retrieve a model for. - /// The request message to retrieve a model for. - /// The action descriptor for the action being queried on. - /// The EDM model for the given type and request. - public virtual IEdmModel GetModel( - Type elementClrType, - HttpRequest request, - ActionDescriptor actionDescriptor) - { - // Get model for the request - IEdmModel model = request.GetModel(); - - if (model == null || - model == EdmCoreModel.Instance || model.GetEdmType(elementClrType) == null) - { - // user has not configured anything or has registered a model without the element type - // let's create one just for this type and cache it in the action descriptor - model = actionDescriptor.GetEdmModel(request, elementClrType); - } + return false; + } + + /// + /// Gets the EDM model for the given type and request.Override this method to customize the EDM model used for + /// querying. + /// + /// The CLR type to retrieve a model for. + /// The request message to retrieve a model for. + /// The action descriptor for the action being queried on. + /// The EDM model for the given type and request. + public virtual IEdmModel GetModel( + Type elementClrType, + HttpRequest request, + ActionDescriptor actionDescriptor) + { + // Get model for the request + IEdmModel model = request.GetModel(); - Contract.Assert(model != null); - return model; + if (model == null || + model == EdmCoreModel.Instance || model.GetEdmType(elementClrType) == null) + { + // user has not configured anything or has registered a model without the element type + // let's create one just for this type and cache it in the action descriptor + model = actionDescriptor.GetEdmModel(request, elementClrType); } + Contract.Assert(model != null); + return model; + } + + /// + /// Holds request level query information. + /// + private class RequestQueryData + { + /// + /// Gets or sets a value indicating whether query validation was run before action (controller method) is executed. + /// + /// + /// Marks if the query validation was run before the action execution. This is not always possible. + /// For cases where the run failed before action execution. We will run validation on result. + /// + public bool QueryValidationRunBeforeActionExecution { get; set; } + /// - /// Holds request level query information. + /// Gets or sets the processed query options. /// - private class RequestQueryData - { - /// - /// Gets or sets a value indicating whether query validation was run before action (controller method) is executed. - /// - /// - /// Marks if the query validation was run before the action execution. This is not always possible. - /// For cases where the run failed before action execution. We will run validation on result. - /// - public bool QueryValidationRunBeforeActionExecution { get; set; } - - /// - /// Gets or sets the processed query options. - /// - /// - /// Stores the processed query options to be used later if OnActionExecuting was able to verify the query. - /// This is because ValidateQuery internally modifies query options (expands are prime example of this). - /// - public ODataQueryOptions ProcessedQueryOptions { get; set; } - } + /// + /// Stores the processed query options to be used later if OnActionExecuting was able to verify the query. + /// This is because ValidateQuery internally modifies query options (expands are prime example of this). + /// + public ODataQueryOptions ProcessedQueryOptions { get; set; } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/ExpressionHelperMethods.cs b/src/Microsoft.AspNetCore.OData/Query/ExpressionHelperMethods.cs index 0026ca802..eeb30b81d 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ExpressionHelperMethods.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ExpressionHelperMethods.cs @@ -14,280 +14,279 @@ using System.Linq.Expressions; using System.Reflection; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +internal class ExpressionHelperMethods { - internal class ExpressionHelperMethods - { - private static MethodInfo _enumerableWhereMethod = GenericMethodOf(_ => Enumerable.Where(default(IEnumerable), default(Func))); - private static MethodInfo _queryableToListMethod = GenericMethodOf(_ => Enumerable.ToList(default(IEnumerable))); - private static MethodInfo _orderByMethod = GenericMethodOf(_ => Queryable.OrderBy(default(IQueryable), default(Expression>))); - private static MethodInfo _enumerableOrderByMethod = GenericMethodOf(_ => Enumerable.OrderBy(default(IEnumerable), default(Func))); - private static MethodInfo _orderByDescendingMethod = GenericMethodOf(_ => Queryable.OrderByDescending(default(IQueryable), default(Expression>))); - private static MethodInfo _enumerableOrderByDescendingMethod = GenericMethodOf(_ => Enumerable.OrderByDescending(default(IEnumerable), default(Func))); - private static MethodInfo _thenByMethod = GenericMethodOf(_ => Queryable.ThenBy(default(IOrderedQueryable), default(Expression>))); - private static MethodInfo _enumerableThenByMethod = GenericMethodOf(_ => Enumerable.ThenBy(default(IOrderedEnumerable), default(Func))); - private static MethodInfo _thenByDescendingMethod = GenericMethodOf(_ => Queryable.ThenByDescending(default(IOrderedQueryable), default(Expression>))); - private static MethodInfo _enumerableThenByDescendingMethod = GenericMethodOf(_ => Enumerable.ThenByDescending(default(IOrderedEnumerable), default(Func))); - private static MethodInfo _countMethod = GenericMethodOf(_ => Queryable.LongCount(default(IQueryable))); - private static MethodInfo _enumerableGroupByMethod = GenericMethodOf(_ => Enumerable.GroupBy(default(IQueryable), default(Func))); - private static MethodInfo _groupByMethod = GenericMethodOf(_ => Queryable.GroupBy(default(IQueryable), default(Expression>))); - private static MethodInfo _aggregateMethod = GenericMethodOf(_ => Queryable.Aggregate(default(IQueryable), default(int), default(Expression>))); - private static MethodInfo _skipMethod = GenericMethodOf(_ => Queryable.Skip(default(IQueryable), default(int))); - private static MethodInfo _enumerableSkipMethod = GenericMethodOf(_ => Enumerable.Skip(default(IEnumerable), default(int))); - private static MethodInfo _whereMethod = GenericMethodOf(_ => Queryable.Where(default(IQueryable), default(Expression>))); + private static MethodInfo _enumerableWhereMethod = GenericMethodOf(_ => Enumerable.Where(default(IEnumerable), default(Func))); + private static MethodInfo _queryableToListMethod = GenericMethodOf(_ => Enumerable.ToList(default(IEnumerable))); + private static MethodInfo _orderByMethod = GenericMethodOf(_ => Queryable.OrderBy(default(IQueryable), default(Expression>))); + private static MethodInfo _enumerableOrderByMethod = GenericMethodOf(_ => Enumerable.OrderBy(default(IEnumerable), default(Func))); + private static MethodInfo _orderByDescendingMethod = GenericMethodOf(_ => Queryable.OrderByDescending(default(IQueryable), default(Expression>))); + private static MethodInfo _enumerableOrderByDescendingMethod = GenericMethodOf(_ => Enumerable.OrderByDescending(default(IEnumerable), default(Func))); + private static MethodInfo _thenByMethod = GenericMethodOf(_ => Queryable.ThenBy(default(IOrderedQueryable), default(Expression>))); + private static MethodInfo _enumerableThenByMethod = GenericMethodOf(_ => Enumerable.ThenBy(default(IOrderedEnumerable), default(Func))); + private static MethodInfo _thenByDescendingMethod = GenericMethodOf(_ => Queryable.ThenByDescending(default(IOrderedQueryable), default(Expression>))); + private static MethodInfo _enumerableThenByDescendingMethod = GenericMethodOf(_ => Enumerable.ThenByDescending(default(IOrderedEnumerable), default(Func))); + private static MethodInfo _countMethod = GenericMethodOf(_ => Queryable.LongCount(default(IQueryable))); + private static MethodInfo _enumerableGroupByMethod = GenericMethodOf(_ => Enumerable.GroupBy(default(IQueryable), default(Func))); + private static MethodInfo _groupByMethod = GenericMethodOf(_ => Queryable.GroupBy(default(IQueryable), default(Expression>))); + private static MethodInfo _aggregateMethod = GenericMethodOf(_ => Queryable.Aggregate(default(IQueryable), default(int), default(Expression>))); + private static MethodInfo _skipMethod = GenericMethodOf(_ => Queryable.Skip(default(IQueryable), default(int))); + private static MethodInfo _enumerableSkipMethod = GenericMethodOf(_ => Enumerable.Skip(default(IEnumerable), default(int))); + private static MethodInfo _whereMethod = GenericMethodOf(_ => Queryable.Where(default(IQueryable), default(Expression>))); - private static MethodInfo _queryableCastMethod = GenericMethodOf(_ => Queryable.Cast(default(IQueryable))); - private static MethodInfo _enumerableCastMethod = GenericMethodOf(_ => Enumerable.Cast(default(IEnumerable))); + private static MethodInfo _queryableCastMethod = GenericMethodOf(_ => Queryable.Cast(default(IQueryable))); + private static MethodInfo _enumerableCastMethod = GenericMethodOf(_ => Enumerable.Cast(default(IEnumerable))); - private static MethodInfo _queryableContainsMethod = GenericMethodOf(_ => Queryable.Contains(default(IQueryable), default(int))); - private static MethodInfo _enumerableContainsMethod = GenericMethodOf(_ => Enumerable.Contains(default(IEnumerable), default(int))); + private static MethodInfo _queryableContainsMethod = GenericMethodOf(_ => Queryable.Contains(default(IQueryable), default(int))); + private static MethodInfo _enumerableContainsMethod = GenericMethodOf(_ => Enumerable.Contains(default(IEnumerable), default(int))); - private static MethodInfo _queryableEmptyAnyMethod = GenericMethodOf(_ => Queryable.Any(default(IQueryable))); - private static MethodInfo _queryableNonEmptyAnyMethod = GenericMethodOf(_ => Queryable.Any(default(IQueryable), default(Expression>))); - private static MethodInfo _queryableAllMethod = GenericMethodOf(_ => Queryable.All(default(IQueryable), default(Expression>))); + private static MethodInfo _queryableEmptyAnyMethod = GenericMethodOf(_ => Queryable.Any(default(IQueryable))); + private static MethodInfo _queryableNonEmptyAnyMethod = GenericMethodOf(_ => Queryable.Any(default(IQueryable), default(Expression>))); + private static MethodInfo _queryableAllMethod = GenericMethodOf(_ => Queryable.All(default(IQueryable), default(Expression>))); - private static MethodInfo _enumerableEmptyAnyMethod = GenericMethodOf(_ => Enumerable.Any(default(IEnumerable))); - private static MethodInfo _enumerableNonEmptyAnyMethod = GenericMethodOf(_ => Enumerable.Any(default(IEnumerable), default(Func))); - private static MethodInfo _enumerableAllMethod = GenericMethodOf(_ => Enumerable.All(default(IEnumerable), default(Func))); + private static MethodInfo _enumerableEmptyAnyMethod = GenericMethodOf(_ => Enumerable.Any(default(IEnumerable))); + private static MethodInfo _enumerableNonEmptyAnyMethod = GenericMethodOf(_ => Enumerable.Any(default(IEnumerable), default(Func))); + private static MethodInfo _enumerableAllMethod = GenericMethodOf(_ => Enumerable.All(default(IEnumerable), default(Func))); - private static MethodInfo _enumerableOfTypeMethod = GenericMethodOf(_ => Enumerable.OfType(default(IEnumerable))); - private static MethodInfo _queryableOfTypeMethod = GenericMethodOf(_ => Queryable.OfType(default(IQueryable))); + private static MethodInfo _enumerableOfTypeMethod = GenericMethodOf(_ => Enumerable.OfType(default(IEnumerable))); + private static MethodInfo _queryableOfTypeMethod = GenericMethodOf(_ => Queryable.OfType(default(IQueryable))); - private static MethodInfo _enumerableSelectManyMethod = GenericMethodOf(_ => Enumerable.SelectMany(default(IEnumerable), default(Func>))); - private static MethodInfo _queryableSelectManyMethod = GenericMethodOf(_ => Queryable.SelectMany(default(IQueryable), default(Expression>>))); + private static MethodInfo _enumerableSelectManyMethod = GenericMethodOf(_ => Enumerable.SelectMany(default(IEnumerable), default(Func>))); + private static MethodInfo _queryableSelectManyMethod = GenericMethodOf(_ => Queryable.SelectMany(default(IQueryable), default(Expression>>))); - private static MethodInfo _enumerableSelectMethod = GenericMethodOf(_ => Enumerable.Select(default(IEnumerable), i => i)); - private static MethodInfo _queryableSelectMethod = GenericMethodOf(_ => Queryable.Select(default(IQueryable), i => i)); + private static MethodInfo _enumerableSelectMethod = GenericMethodOf(_ => Enumerable.Select(default(IEnumerable), i => i)); + private static MethodInfo _queryableSelectMethod = GenericMethodOf(_ => Queryable.Select(default(IQueryable), i => i)); - private static MethodInfo _queryableTakeMethod = GenericMethodOf(_ => Queryable.Take(default(IQueryable), default(int))); - private static MethodInfo _enumerableTakeMethod = GenericMethodOf(_ => Enumerable.Take(default(IEnumerable), default(int))); + private static MethodInfo _queryableTakeMethod = GenericMethodOf(_ => Queryable.Take(default(IQueryable), default(int))); + private static MethodInfo _enumerableTakeMethod = GenericMethodOf(_ => Enumerable.Take(default(IEnumerable), default(int))); - private static MethodInfo _queryableAsQueryableMethod = GenericMethodOf(_ => Queryable.AsQueryable(default(IEnumerable))); + private static MethodInfo _queryableAsQueryableMethod = GenericMethodOf(_ => Queryable.AsQueryable(default(IEnumerable))); - private static MethodInfo _toQueryableMethod = GenericMethodOf(_ => ExpressionHelperMethods.ToQueryable(default(int))); + private static MethodInfo _toQueryableMethod = GenericMethodOf(_ => ExpressionHelperMethods.ToQueryable(default(int))); - private static Dictionary _queryableSumMethods = GetQueryableAggregationMethods("Sum"); - private static Dictionary _enumerableSumMethods = GetEnumerableAggregationMethods("Sum"); + private static Dictionary _queryableSumMethods = GetQueryableAggregationMethods("Sum"); + private static Dictionary _enumerableSumMethods = GetEnumerableAggregationMethods("Sum"); - private static MethodInfo _enumerableMinMethod = GenericMethodOf(_ => Enumerable.Min(default(IQueryable), default(Func))); - private static MethodInfo _enumerableMaxMethod = GenericMethodOf(_ => Enumerable.Max(default(IQueryable), default(Func))); + private static MethodInfo _enumerableMinMethod = GenericMethodOf(_ => Enumerable.Min(default(IQueryable), default(Func))); + private static MethodInfo _enumerableMaxMethod = GenericMethodOf(_ => Enumerable.Max(default(IQueryable), default(Func))); - private static MethodInfo _enumerableDistinctMethod = GenericMethodOf(_ => Enumerable.Distinct(default(IEnumerable))); + private static MethodInfo _enumerableDistinctMethod = GenericMethodOf(_ => Enumerable.Distinct(default(IEnumerable))); - private static MethodInfo _queryableMinMethod = GenericMethodOf(_ => Queryable.Min(default(IQueryable), default(Expression>))); - private static MethodInfo _queryableMaxMethod = GenericMethodOf(_ => Queryable.Max(default(IQueryable), default(Expression>))); + private static MethodInfo _queryableMinMethod = GenericMethodOf(_ => Queryable.Min(default(IQueryable), default(Expression>))); + private static MethodInfo _queryableMaxMethod = GenericMethodOf(_ => Queryable.Max(default(IQueryable), default(Expression>))); - private static MethodInfo _queryableDistinctMethod = GenericMethodOf(_ => Queryable.Distinct(default(IQueryable))); + private static MethodInfo _queryableDistinctMethod = GenericMethodOf(_ => Queryable.Distinct(default(IQueryable))); - private static MethodInfo _createQueryGenericMethod = GetCreateQueryGenericMethod(); + private static MethodInfo _createQueryGenericMethod = GetCreateQueryGenericMethod(); - //Unlike the Sum method, the return types are not unique and do not match the input type of the expression. - //Inspecting the 2nd parameters expression's function's 2nd argument is too specific for the GetQueryableAggregationMethods - private static Dictionary _enumerableAverageMethods = new Dictionary() - { - { typeof(int), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, - { typeof(int?), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, - { typeof(long), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, - { typeof(long?), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, - { typeof(float), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, - { typeof(float?), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, - { typeof(decimal), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, - { typeof(decimal?), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, - { typeof(double), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, - { typeof(double?), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, - }; - - private static Dictionary _queryableAverageMethods = new Dictionary() - { - { typeof(int), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, - { typeof(int?), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, - { typeof(long), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, - { typeof(long?), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, - { typeof(float), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, - { typeof(float?), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, - { typeof(decimal), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, - { typeof(decimal?), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, - { typeof(double), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, - { typeof(double?), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, - }; + //Unlike the Sum method, the return types are not unique and do not match the input type of the expression. + //Inspecting the 2nd parameters expression's function's 2nd argument is too specific for the GetQueryableAggregationMethods + private static Dictionary _enumerableAverageMethods = new Dictionary() + { + { typeof(int), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, + { typeof(int?), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, + { typeof(long), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, + { typeof(long?), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, + { typeof(float), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, + { typeof(float?), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, + { typeof(decimal), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, + { typeof(decimal?), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, + { typeof(double), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, + { typeof(double?), GenericMethodOf(_ => Enumerable.Average(default(IEnumerable), default(Func))) }, + }; + + private static Dictionary _queryableAverageMethods = new Dictionary() + { + { typeof(int), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, + { typeof(int?), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, + { typeof(long), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, + { typeof(long?), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, + { typeof(float), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, + { typeof(float?), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, + { typeof(decimal), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, + { typeof(decimal?), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, + { typeof(double), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, + { typeof(double?), GenericMethodOf(_ => Queryable.Average(default(IQueryable), default(Expression>))) }, + }; - private static MethodInfo _enumerableCountMethod = GenericMethodOf(_ => Enumerable.LongCount(default(IEnumerable))); + private static MethodInfo _enumerableCountMethod = GenericMethodOf(_ => Enumerable.LongCount(default(IEnumerable))); - private static MethodInfo _safeConvertToDecimalMethod = typeof(ExpressionHelperMethods).GetMethod("SafeConvertToDecimal"); + private static MethodInfo _safeConvertToDecimalMethod = typeof(ExpressionHelperMethods).GetMethod("SafeConvertToDecimal"); - public static MethodInfo EnumerableWhereGeneric => _enumerableWhereMethod; + public static MethodInfo EnumerableWhereGeneric => _enumerableWhereMethod; - public static MethodInfo QueryableToList => _queryableToListMethod; + public static MethodInfo QueryableToList => _queryableToListMethod; - public static MethodInfo QueryableOrderByGeneric => _orderByMethod; + public static MethodInfo QueryableOrderByGeneric => _orderByMethod; - public static MethodInfo EnumerableOrderByGeneric => _enumerableOrderByMethod; + public static MethodInfo EnumerableOrderByGeneric => _enumerableOrderByMethod; - public static MethodInfo QueryableOrderByDescendingGeneric => _orderByDescendingMethod; + public static MethodInfo QueryableOrderByDescendingGeneric => _orderByDescendingMethod; - public static MethodInfo EnumerableOrderByDescendingGeneric => _enumerableOrderByDescendingMethod; + public static MethodInfo EnumerableOrderByDescendingGeneric => _enumerableOrderByDescendingMethod; - public static MethodInfo QueryableThenByGeneric => _thenByMethod; + public static MethodInfo QueryableThenByGeneric => _thenByMethod; - public static MethodInfo EnumerableThenByGeneric => _enumerableThenByMethod; + public static MethodInfo EnumerableThenByGeneric => _enumerableThenByMethod; - public static MethodInfo QueryableThenByDescendingGeneric => _thenByDescendingMethod; + public static MethodInfo QueryableThenByDescendingGeneric => _thenByDescendingMethod; - public static MethodInfo EnumerableThenByDescendingGeneric => _enumerableThenByDescendingMethod; + public static MethodInfo EnumerableThenByDescendingGeneric => _enumerableThenByDescendingMethod; - public static MethodInfo QueryableCountGeneric => _countMethod; + public static MethodInfo QueryableCountGeneric => _countMethod; - public static Dictionary QueryableSumGenerics => _queryableSumMethods; + public static Dictionary QueryableSumGenerics => _queryableSumMethods; - public static Dictionary EnumerableSumGenerics => _enumerableSumMethods; + public static Dictionary EnumerableSumGenerics => _enumerableSumMethods; - public static MethodInfo QueryableMin => _queryableMinMethod; + public static MethodInfo QueryableMin => _queryableMinMethod; - public static MethodInfo EnumerableMin => _enumerableMinMethod; + public static MethodInfo EnumerableMin => _enumerableMinMethod; - public static MethodInfo QueryableMax => _queryableMaxMethod; + public static MethodInfo QueryableMax => _queryableMaxMethod; - public static MethodInfo EnumerableMax => _enumerableMaxMethod; + public static MethodInfo EnumerableMax => _enumerableMaxMethod; - public static Dictionary QueryableAverageGenerics => _queryableAverageMethods; + public static Dictionary QueryableAverageGenerics => _queryableAverageMethods; - public static Dictionary EnumerableAverageGenerics => _enumerableAverageMethods; + public static Dictionary EnumerableAverageGenerics => _enumerableAverageMethods; - public static MethodInfo QueryableDistinct => _queryableDistinctMethod; + public static MethodInfo QueryableDistinct => _queryableDistinctMethod; - public static MethodInfo EnumerableDistinct => _enumerableDistinctMethod; + public static MethodInfo EnumerableDistinct => _enumerableDistinctMethod; - public static MethodInfo QueryableGroupByGeneric => _groupByMethod; + public static MethodInfo QueryableGroupByGeneric => _groupByMethod; - public static MethodInfo EnumerableGroupByGeneric => _enumerableGroupByMethod; + public static MethodInfo EnumerableGroupByGeneric => _enumerableGroupByMethod; - public static MethodInfo QueryableAggregateGeneric => _aggregateMethod; + public static MethodInfo QueryableAggregateGeneric => _aggregateMethod; - public static MethodInfo QueryableTakeGeneric => _queryableTakeMethod; + public static MethodInfo QueryableTakeGeneric => _queryableTakeMethod; - public static MethodInfo EnumerableTakeGeneric => _enumerableTakeMethod; + public static MethodInfo EnumerableTakeGeneric => _enumerableTakeMethod; - public static MethodInfo QueryableSkipGeneric => _skipMethod; + public static MethodInfo QueryableSkipGeneric => _skipMethod; - public static MethodInfo EnumerableSkipGeneric => _enumerableSkipMethod; + public static MethodInfo EnumerableSkipGeneric => _enumerableSkipMethod; - public static MethodInfo QueryableWhereGeneric => _whereMethod; + public static MethodInfo QueryableWhereGeneric => _whereMethod; - public static MethodInfo QueryableCastGeneric => _queryableCastMethod; + public static MethodInfo QueryableCastGeneric => _queryableCastMethod; - public static MethodInfo EnumerableCastGeneric => _enumerableCastMethod; + public static MethodInfo EnumerableCastGeneric => _enumerableCastMethod; - public static MethodInfo QueryableContainsGeneric => _queryableContainsMethod; + public static MethodInfo QueryableContainsGeneric => _queryableContainsMethod; - public static MethodInfo EnumerableContainsGeneric => _enumerableContainsMethod; + public static MethodInfo EnumerableContainsGeneric => _enumerableContainsMethod; - public static MethodInfo QueryableSelectGeneric => _queryableSelectMethod; + public static MethodInfo QueryableSelectGeneric => _queryableSelectMethod; - public static MethodInfo EnumerableSelectGeneric => _enumerableSelectMethod; + public static MethodInfo EnumerableSelectGeneric => _enumerableSelectMethod; - public static MethodInfo QueryableSelectManyGeneric => _queryableSelectManyMethod; + public static MethodInfo QueryableSelectManyGeneric => _queryableSelectManyMethod; - public static MethodInfo EnumerableSelectManyGeneric => _enumerableSelectManyMethod; + public static MethodInfo EnumerableSelectManyGeneric => _enumerableSelectManyMethod; - public static MethodInfo QueryableEmptyAnyGeneric => _queryableEmptyAnyMethod; + public static MethodInfo QueryableEmptyAnyGeneric => _queryableEmptyAnyMethod; - public static MethodInfo QueryableNonEmptyAnyGeneric => _queryableNonEmptyAnyMethod; + public static MethodInfo QueryableNonEmptyAnyGeneric => _queryableNonEmptyAnyMethod; - public static MethodInfo QueryableAllGeneric => _queryableAllMethod; + public static MethodInfo QueryableAllGeneric => _queryableAllMethod; - public static MethodInfo EnumerableEmptyAnyGeneric => _enumerableEmptyAnyMethod; + public static MethodInfo EnumerableEmptyAnyGeneric => _enumerableEmptyAnyMethod; - public static MethodInfo EnumerableNonEmptyAnyGeneric => _enumerableNonEmptyAnyMethod; + public static MethodInfo EnumerableNonEmptyAnyGeneric => _enumerableNonEmptyAnyMethod; - public static MethodInfo EnumerableAllGeneric => _enumerableAllMethod; + public static MethodInfo EnumerableAllGeneric => _enumerableAllMethod; - public static MethodInfo EnumerableOfType => _enumerableOfTypeMethod; + public static MethodInfo EnumerableOfType => _enumerableOfTypeMethod; - public static MethodInfo QueryableOfType => _queryableOfTypeMethod; + public static MethodInfo QueryableOfType => _queryableOfTypeMethod; - public static MethodInfo QueryableAsQueryable => _queryableAsQueryableMethod; + public static MethodInfo QueryableAsQueryable => _queryableAsQueryableMethod; - public static MethodInfo EntityAsQueryable => _toQueryableMethod; + public static MethodInfo EntityAsQueryable => _toQueryableMethod; - public static MethodInfo EnumerableCountGeneric => _enumerableCountMethod; + public static MethodInfo EnumerableCountGeneric => _enumerableCountMethod; - public static MethodInfo ConvertToDecimal => _safeConvertToDecimalMethod; + public static MethodInfo ConvertToDecimal => _safeConvertToDecimalMethod; - public static MethodInfo CreateQueryGeneric => _createQueryGenericMethod; + public static MethodInfo CreateQueryGeneric => _createQueryGenericMethod; - public static IQueryable ToQueryable(T value) - { - return (new List { value }).AsQueryable(); - } + public static IQueryable ToQueryable(T value) + { + return (new List { value }).AsQueryable(); + } - public static decimal? SafeConvertToDecimal(object value) + public static decimal? SafeConvertToDecimal(object value) + { + if (value == null || value == DBNull.Value) { - if (value == null || value == DBNull.Value) - { - return null; - } - - Type type = value.GetType(); - type = Nullable.GetUnderlyingType(type) ?? type; - if (type == typeof(short) || - type == typeof(int) || - type == typeof(long) || - type == typeof(decimal) || - type == typeof(double) || - type == typeof(float)) - { - return (decimal?)Convert.ChangeType(value, typeof(decimal), CultureInfo.InvariantCulture); - } - return null; } - private static MethodInfo GenericMethodOf(Expression> expression) + Type type = value.GetType(); + type = Nullable.GetUnderlyingType(type) ?? type; + if (type == typeof(short) || + type == typeof(int) || + type == typeof(long) || + type == typeof(decimal) || + type == typeof(double) || + type == typeof(float)) { - return GenericMethodOf(expression as Expression); + return (decimal?)Convert.ChangeType(value, typeof(decimal), CultureInfo.InvariantCulture); } - private static MethodInfo GenericMethodOf(Expression expression) - { - LambdaExpression lambdaExpression = expression as LambdaExpression; + return null; + } + + private static MethodInfo GenericMethodOf(Expression> expression) + { + return GenericMethodOf(expression as Expression); + } - Contract.Assert(expression.NodeType == ExpressionType.Lambda); - Contract.Assert(lambdaExpression != null); - Contract.Assert(lambdaExpression.Body.NodeType == ExpressionType.Call); + private static MethodInfo GenericMethodOf(Expression expression) + { + LambdaExpression lambdaExpression = expression as LambdaExpression; - return (lambdaExpression.Body as MethodCallExpression).Method.GetGenericMethodDefinition(); - } + Contract.Assert(expression.NodeType == ExpressionType.Lambda); + Contract.Assert(lambdaExpression != null); + Contract.Assert(lambdaExpression.Body.NodeType == ExpressionType.Call); - private static Dictionary GetQueryableAggregationMethods(string methodName) - { - //Sum to not have generic by property method return type so have to generate a table - // Looking for methods like - // Queryable.Sum(default(IQueryable), default(Expression>))) - - return typeof(Queryable).GetMethods() - .Where(m => m.Name == methodName) - .Where(m => m.GetParameters().Length == 2) - .ToDictionary(m => m.ReturnType); - } + return (lambdaExpression.Body as MethodCallExpression).Method.GetGenericMethodDefinition(); + } - private static Dictionary GetEnumerableAggregationMethods(string methodName) - { - //Sum to not have generic by property method return type so have to generate a table - // Looking for methods like - // Queryable.Sum(default(IQueryable), default(Expression>))) - - return typeof(Enumerable).GetMethods() - .Where(m => m.Name == methodName) - .Where(m => m.GetParameters().Length == 2) - .ToDictionary(m => m.ReturnType); - } + private static Dictionary GetQueryableAggregationMethods(string methodName) + { + //Sum to not have generic by property method return type so have to generate a table + // Looking for methods like + // Queryable.Sum(default(IQueryable), default(Expression>))) + + return typeof(Queryable).GetMethods() + .Where(m => m.Name == methodName) + .Where(m => m.GetParameters().Length == 2) + .ToDictionary(m => m.ReturnType); + } - private static MethodInfo GetCreateQueryGenericMethod() - { - return typeof(IQueryProvider).GetTypeInfo() - .GetDeclaredMethods("CreateQuery") - .Where(m => m.IsGenericMethod) - .FirstOrDefault(); - } + private static Dictionary GetEnumerableAggregationMethods(string methodName) + { + //Sum to not have generic by property method return type so have to generate a table + // Looking for methods like + // Queryable.Sum(default(IQueryable), default(Expression>))) + + return typeof(Enumerable).GetMethods() + .Where(m => m.Name == methodName) + .Where(m => m.GetParameters().Length == 2) + .ToDictionary(m => m.ReturnType); + } + + private static MethodInfo GetCreateQueryGenericMethod() + { + return typeof(IQueryProvider).GetTypeInfo() + .GetDeclaredMethods("CreateQuery") + .Where(m => m.IsGenericMethod) + .FirstOrDefault(); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/ExpressionHelpers.cs b/src/Microsoft.AspNetCore.OData/Query/ExpressionHelpers.cs index 2eaaab00d..38bfa1add 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ExpressionHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ExpressionHelpers.cs @@ -15,274 +15,273 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +internal static class ExpressionHelpers { - internal static class ExpressionHelpers + public static Func Count(IQueryable query, Type type) { - public static Func Count(IQueryable query, Type type) - { - MethodInfo countMethod = ExpressionHelperMethods.QueryableCountGeneric.MakeGenericMethod(type); - Func func = () => (long)countMethod.Invoke(null, new object[] { query }); - return func; - } + MethodInfo countMethod = ExpressionHelperMethods.QueryableCountGeneric.MakeGenericMethod(type); + Func func = () => (long)countMethod.Invoke(null, new object[] { query }); + return func; + } - public static IQueryable Skip(IQueryable query, int count, Type type, bool parameterize) - { - MethodInfo skipMethod = ExpressionHelperMethods.QueryableSkipGeneric.MakeGenericMethod(type); - Expression skipValueExpression = parameterize ? LinqParameterContainer.Parameterize(typeof(int), count) : Expression.Constant(count); + public static IQueryable Skip(IQueryable query, int count, Type type, bool parameterize) + { + MethodInfo skipMethod = ExpressionHelperMethods.QueryableSkipGeneric.MakeGenericMethod(type); + Expression skipValueExpression = parameterize ? LinqParameterContainer.Parameterize(typeof(int), count) : Expression.Constant(count); - Expression skipQuery = Expression.Call(null, skipMethod, new[] { query.Expression, skipValueExpression }); + Expression skipQuery = Expression.Call(null, skipMethod, new[] { query.Expression, skipValueExpression }); - var createMethod = ExpressionHelperMethods.CreateQueryGeneric.MakeGenericMethod(type); + var createMethod = ExpressionHelperMethods.CreateQueryGeneric.MakeGenericMethod(type); - return createMethod.Invoke(query.Provider, new[] { skipQuery }) as IQueryable; - } + return createMethod.Invoke(query.Provider, new[] { skipQuery }) as IQueryable; + } - public static IQueryable Take(IQueryable query, int count, Type type, bool parameterize) - { - Expression takeQuery = Take(query.Expression, count, type, parameterize); - var createMethod = ExpressionHelperMethods.CreateQueryGeneric.MakeGenericMethod(type); + public static IQueryable Take(IQueryable query, int count, Type type, bool parameterize) + { + Expression takeQuery = Take(query.Expression, count, type, parameterize); + var createMethod = ExpressionHelperMethods.CreateQueryGeneric.MakeGenericMethod(type); - return createMethod.Invoke(query.Provider, new[] { takeQuery }) as IQueryable; - } + return createMethod.Invoke(query.Provider, new[] { takeQuery }) as IQueryable; + } - public static Expression Skip(Expression source, int count, Type type, bool parameterize) + public static Expression Skip(Expression source, int count, Type type, bool parameterize) + { + MethodInfo skipMethod; + if (typeof(IQueryable).IsAssignableFrom(source.Type)) { - MethodInfo skipMethod; - if (typeof(IQueryable).IsAssignableFrom(source.Type)) - { - skipMethod = ExpressionHelperMethods.QueryableSkipGeneric.MakeGenericMethod(type); - } - else - { - skipMethod = ExpressionHelperMethods.EnumerableSkipGeneric.MakeGenericMethod(type); - } - - Expression skipValueExpression = parameterize ? LinqParameterContainer.Parameterize(typeof(int), count) : Expression.Constant(count); - Expression skipQuery = Expression.Call(null, skipMethod, new[] { source, skipValueExpression }); - return skipQuery; + skipMethod = ExpressionHelperMethods.QueryableSkipGeneric.MakeGenericMethod(type); } - - public static Expression Take(Expression source, int count, Type elementType, bool parameterize) + else { - MethodInfo takeMethod; - if (typeof(IQueryable).IsAssignableFrom(source.Type)) - { - takeMethod = ExpressionHelperMethods.QueryableTakeGeneric.MakeGenericMethod(elementType); - } - else - { - takeMethod = ExpressionHelperMethods.EnumerableTakeGeneric.MakeGenericMethod(elementType); - } - - Expression takeValueExpression = parameterize ? LinqParameterContainer.Parameterize(typeof(int), count) : Expression.Constant(count); - Expression takeQuery = Expression.Call(null, takeMethod, new[] { source, takeValueExpression }); - return takeQuery; + skipMethod = ExpressionHelperMethods.EnumerableSkipGeneric.MakeGenericMethod(type); } - public static Expression OrderByPropertyExpression( - Expression source, - string propertyName, - Type elementType, - bool alreadyOrdered = false) + Expression skipValueExpression = parameterize ? LinqParameterContainer.Parameterize(typeof(int), count) : Expression.Constant(count); + Expression skipQuery = Expression.Call(null, skipMethod, new[] { source, skipValueExpression }); + return skipQuery; + } + + public static Expression Take(Expression source, int count, Type elementType, bool parameterize) + { + MethodInfo takeMethod; + if (typeof(IQueryable).IsAssignableFrom(source.Type)) + { + takeMethod = ExpressionHelperMethods.QueryableTakeGeneric.MakeGenericMethod(elementType); + } + else { - LambdaExpression orderByLambda = GetPropertyAccessLambda(elementType, propertyName); - return OrderBy(source, orderByLambda, elementType, OrderByDirection.Ascending, alreadyOrdered); + takeMethod = ExpressionHelperMethods.EnumerableTakeGeneric.MakeGenericMethod(elementType); } - public static Expression OrderBy( - Expression source, - LambdaExpression orderByLambda, - Type elementType, - OrderByDirection direction, - bool alreadyOrdered = false) + Expression takeValueExpression = parameterize ? LinqParameterContainer.Parameterize(typeof(int), count) : Expression.Constant(count); + Expression takeQuery = Expression.Call(null, takeMethod, new[] { source, takeValueExpression }); + return takeQuery; + } + + public static Expression OrderByPropertyExpression( + Expression source, + string propertyName, + Type elementType, + bool alreadyOrdered = false) + { + LambdaExpression orderByLambda = GetPropertyAccessLambda(elementType, propertyName); + return OrderBy(source, orderByLambda, elementType, OrderByDirection.Ascending, alreadyOrdered); + } + + public static Expression OrderBy( + Expression source, + LambdaExpression orderByLambda, + Type elementType, + OrderByDirection direction, + bool alreadyOrdered = false) + { + Type returnType = orderByLambda.Body.Type; + MethodInfo orderByMethod; + if (!alreadyOrdered) { - Type returnType = orderByLambda.Body.Type; - MethodInfo orderByMethod; - if (!alreadyOrdered) + if (typeof(IQueryable).IsAssignableFrom(source.Type)) { - if (typeof(IQueryable).IsAssignableFrom(source.Type)) + if (direction == OrderByDirection.Ascending) { - if (direction == OrderByDirection.Ascending) - { - orderByMethod = ExpressionHelperMethods.QueryableOrderByGeneric.MakeGenericMethod(elementType, - returnType); - } - else - { - orderByMethod = ExpressionHelperMethods.QueryableOrderByDescendingGeneric.MakeGenericMethod(elementType, - returnType); - } + orderByMethod = ExpressionHelperMethods.QueryableOrderByGeneric.MakeGenericMethod(elementType, + returnType); } else { - if (direction == OrderByDirection.Ascending) - { - orderByMethod = ExpressionHelperMethods.EnumerableOrderByGeneric.MakeGenericMethod(elementType, - returnType); - } - else - { - orderByMethod = ExpressionHelperMethods.EnumerableOrderByDescendingGeneric.MakeGenericMethod(elementType, - returnType); - } + orderByMethod = ExpressionHelperMethods.QueryableOrderByDescendingGeneric.MakeGenericMethod(elementType, + returnType); } } else { - if (typeof(IQueryable).IsAssignableFrom(source.Type)) + if (direction == OrderByDirection.Ascending) { - if (direction == OrderByDirection.Ascending) - { - orderByMethod = ExpressionHelperMethods.QueryableThenByGeneric.MakeGenericMethod(elementType, - returnType); - } - else - { - orderByMethod = ExpressionHelperMethods.QueryableThenByDescendingGeneric.MakeGenericMethod(elementType, - returnType); - } + orderByMethod = ExpressionHelperMethods.EnumerableOrderByGeneric.MakeGenericMethod(elementType, + returnType); } else { - if (direction == OrderByDirection.Ascending) - { - orderByMethod = ExpressionHelperMethods.EnumerableThenByGeneric.MakeGenericMethod(elementType, - returnType); - } - else - { - orderByMethod = ExpressionHelperMethods.EnumerableThenByDescendingGeneric.MakeGenericMethod(elementType, - returnType); - } + orderByMethod = ExpressionHelperMethods.EnumerableOrderByDescendingGeneric.MakeGenericMethod(elementType, + returnType); } } - return Expression.Call(null, orderByMethod, new[] { source, orderByLambda }); - } - - public static IQueryable OrderByIt(IQueryable query, OrderByDirection direction, Type type, bool alreadyOrdered = false) - { - ParameterExpression odataItParameter = Expression.Parameter(type, "$it"); - LambdaExpression orderByLambda = Expression.Lambda(odataItParameter, odataItParameter); - return OrderBy(query, orderByLambda, direction, type, alreadyOrdered); } - - public static IQueryable OrderByProperty(IQueryable query, IEdmModel model, IEdmProperty property, OrderByDirection direction, Type type, bool alreadyOrdered = false) + else { - // property aliasing - string propertyName = model.GetClrPropertyName(property); - LambdaExpression orderByLambda = GetPropertyAccessLambda(type, propertyName); - return OrderBy(query, orderByLambda, direction, type, alreadyOrdered); - } - - public static IQueryable OrderBy(IQueryable query, LambdaExpression orderByLambda, OrderByDirection direction, Type type, bool alreadyOrdered = false) - { - Type returnType = orderByLambda.Body.Type; - - MethodInfo orderByMethod = null; - IOrderedQueryable orderedQuery = null; - - // unfortunately unordered L2O.AsQueryable implements IOrderedQueryable - // so we can't try casting to IOrderedQueryable to provide a clue to whether - // we should be calling ThenBy or ThenByDescending - if (alreadyOrdered) + if (typeof(IQueryable).IsAssignableFrom(source.Type)) { if (direction == OrderByDirection.Ascending) { - orderByMethod = ExpressionHelperMethods.QueryableThenByGeneric.MakeGenericMethod(type, returnType); + orderByMethod = ExpressionHelperMethods.QueryableThenByGeneric.MakeGenericMethod(elementType, + returnType); } else { - orderByMethod = ExpressionHelperMethods.QueryableThenByDescendingGeneric.MakeGenericMethod(type, returnType); + orderByMethod = ExpressionHelperMethods.QueryableThenByDescendingGeneric.MakeGenericMethod(elementType, + returnType); } - - orderedQuery = query as IOrderedQueryable; - orderedQuery = orderByMethod.Invoke(null, new object[] { orderedQuery, orderByLambda }) as IOrderedQueryable; } else { if (direction == OrderByDirection.Ascending) { - orderByMethod = ExpressionHelperMethods.QueryableOrderByGeneric.MakeGenericMethod(type, returnType); + orderByMethod = ExpressionHelperMethods.EnumerableThenByGeneric.MakeGenericMethod(elementType, + returnType); } else { - orderByMethod = ExpressionHelperMethods.QueryableOrderByDescendingGeneric.MakeGenericMethod(type, returnType); + orderByMethod = ExpressionHelperMethods.EnumerableThenByDescendingGeneric.MakeGenericMethod(elementType, + returnType); } - - orderedQuery = orderByMethod.Invoke(null, new object[] { query, orderByLambda }) as IOrderedQueryable; } - - return orderedQuery; - } - - public static IQueryable GroupBy(IQueryable query, Expression expression, Type type, Type wrapperType) - { - MethodInfo groupByMethod = ExpressionHelperMethods.QueryableGroupByGeneric.MakeGenericMethod(type, wrapperType); - return groupByMethod.Invoke(null, new object[] { query, expression }) as IQueryable; - } - - public static IQueryable Select(IQueryable query, LambdaExpression expression, Type type) - { - MethodInfo selectMethod = ExpressionHelperMethods.QueryableSelectGeneric.MakeGenericMethod(type, expression.Body.Type); - return selectMethod.Invoke(null, new object[] { query, expression }) as IQueryable; - } - -#if false - public static IQueryable SelectMany(IQueryable query, LambdaExpression expression, Type type) - { - Type elementType = TypeHelper.GetInnerElementType(expression.Body.Type); - MethodInfo selectManyMethod = ExpressionHelperMethods.QueryableSelectManyGeneric.MakeGenericMethod(type, elementType); - return selectManyMethod.Invoke(null, new object[] { query, expression }) as IQueryable; } + return Expression.Call(null, orderByMethod, new[] { source, orderByLambda }); + } - public static IQueryable Aggregate(IQueryable query, object init, LambdaExpression sumLambda, Type type, Type wrapperType) - { - Type returnType = sumLambda.Body.Type; - MethodInfo sumMethod = ExpressionHelperMethods.QueryableAggregateGeneric.MakeGenericMethod(type, returnType); - var agg = sumMethod.Invoke(null, new object[] { query, init, sumLambda }); + public static IQueryable OrderByIt(IQueryable query, OrderByDirection direction, Type type, bool alreadyOrdered = false) + { + ParameterExpression odataItParameter = Expression.Parameter(type, "$it"); + LambdaExpression orderByLambda = Expression.Lambda(odataItParameter, odataItParameter); + return OrderBy(query, orderByLambda, direction, type, alreadyOrdered); + } - MethodInfo converterMethod = ExpressionHelperMethods.EntityAsQueryable.MakeGenericMethod(wrapperType); + public static IQueryable OrderByProperty(IQueryable query, IEdmModel model, IEdmProperty property, OrderByDirection direction, Type type, bool alreadyOrdered = false) + { + // property aliasing + string propertyName = model.GetClrPropertyName(property); + LambdaExpression orderByLambda = GetPropertyAccessLambda(type, propertyName); + return OrderBy(query, orderByLambda, direction, type, alreadyOrdered); + } - return converterMethod.Invoke(null, new object[] { agg }) as IQueryable; - } -#endif + public static IQueryable OrderBy(IQueryable query, LambdaExpression orderByLambda, OrderByDirection direction, Type type, bool alreadyOrdered = false) + { + Type returnType = orderByLambda.Body.Type; - public static IQueryable Where(IQueryable query, Expression where, Type type) - { - MethodInfo whereMethod = ExpressionHelperMethods.QueryableWhereGeneric.MakeGenericMethod(type); - return whereMethod.Invoke(null, new object[] { query, where }) as IQueryable; - } + MethodInfo orderByMethod = null; + IOrderedQueryable orderedQuery = null; - // If the expression is not a nullable type, cast it to one. - public static Expression ToNullable(Expression expression) + // unfortunately unordered L2O.AsQueryable implements IOrderedQueryable + // so we can't try casting to IOrderedQueryable to provide a clue to whether + // we should be calling ThenBy or ThenByDescending + if (alreadyOrdered) { - if (!TypeHelper.IsNullable(expression.Type)) + if (direction == OrderByDirection.Ascending) + { + orderByMethod = ExpressionHelperMethods.QueryableThenByGeneric.MakeGenericMethod(type, returnType); + } + else { - return Expression.Convert(expression, TypeHelper.ToNullable(expression.Type)); + orderByMethod = ExpressionHelperMethods.QueryableThenByDescendingGeneric.MakeGenericMethod(type, returnType); } - return expression; + orderedQuery = query as IOrderedQueryable; + orderedQuery = orderByMethod.Invoke(null, new object[] { orderedQuery, orderByLambda }) as IOrderedQueryable; } - - // Entity Framework does not understand default(T) expression. Hence, generate a constant expression with the default value. - public static Expression Default(Type type) + else { - if (type.IsValueType) + if (direction == OrderByDirection.Ascending) { - return Expression.Constant(Activator.CreateInstance(type), type); + orderByMethod = ExpressionHelperMethods.QueryableOrderByGeneric.MakeGenericMethod(type, returnType); } else { - return Expression.Constant(null, type); + orderByMethod = ExpressionHelperMethods.QueryableOrderByDescendingGeneric.MakeGenericMethod(type, returnType); } + + orderedQuery = orderByMethod.Invoke(null, new object[] { query, orderByLambda }) as IOrderedQueryable; } - public static LambdaExpression GetPropertyAccessLambda(Type type, string propertyName) + return orderedQuery; + } + + public static IQueryable GroupBy(IQueryable query, Expression expression, Type type, Type wrapperType) + { + MethodInfo groupByMethod = ExpressionHelperMethods.QueryableGroupByGeneric.MakeGenericMethod(type, wrapperType); + return groupByMethod.Invoke(null, new object[] { query, expression }) as IQueryable; + } + + public static IQueryable Select(IQueryable query, LambdaExpression expression, Type type) + { + MethodInfo selectMethod = ExpressionHelperMethods.QueryableSelectGeneric.MakeGenericMethod(type, expression.Body.Type); + return selectMethod.Invoke(null, new object[] { query, expression }) as IQueryable; + } + +#if false + public static IQueryable SelectMany(IQueryable query, LambdaExpression expression, Type type) + { + Type elementType = TypeHelper.GetInnerElementType(expression.Body.Type); + MethodInfo selectManyMethod = ExpressionHelperMethods.QueryableSelectManyGeneric.MakeGenericMethod(type, elementType); + return selectManyMethod.Invoke(null, new object[] { query, expression }) as IQueryable; + } + + public static IQueryable Aggregate(IQueryable query, object init, LambdaExpression sumLambda, Type type, Type wrapperType) + { + Type returnType = sumLambda.Body.Type; + MethodInfo sumMethod = ExpressionHelperMethods.QueryableAggregateGeneric.MakeGenericMethod(type, returnType); + var agg = sumMethod.Invoke(null, new object[] { query, init, sumLambda }); + + MethodInfo converterMethod = ExpressionHelperMethods.EntityAsQueryable.MakeGenericMethod(wrapperType); + + return converterMethod.Invoke(null, new object[] { agg }) as IQueryable; + } +#endif + + public static IQueryable Where(IQueryable query, Expression where, Type type) + { + MethodInfo whereMethod = ExpressionHelperMethods.QueryableWhereGeneric.MakeGenericMethod(type); + return whereMethod.Invoke(null, new object[] { query, where }) as IQueryable; + } + + // If the expression is not a nullable type, cast it to one. + public static Expression ToNullable(Expression expression) + { + if (!TypeHelper.IsNullable(expression.Type)) + { + return Expression.Convert(expression, TypeHelper.ToNullable(expression.Type)); + } + + return expression; + } + + // Entity Framework does not understand default(T) expression. Hence, generate a constant expression with the default value. + public static Expression Default(Type type) + { + if (type.IsValueType) + { + return Expression.Constant(Activator.CreateInstance(type), type); + } + else { - ParameterExpression odataItParameter = Expression.Parameter(type, "$it"); - MemberExpression propertyAccess = Expression.Property(odataItParameter, propertyName); - return Expression.Lambda(propertyAccess, odataItParameter); + return Expression.Constant(null, type); } } + + public static LambdaExpression GetPropertyAccessLambda(Type type, string propertyName) + { + ParameterExpression odataItParameter = Expression.Parameter(type, "$it"); + MemberExpression propertyAccess = Expression.Property(odataItParameter, propertyName); + return Expression.Lambda(propertyAccess, odataItParameter); + } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/AggregationBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/AggregationBinder.cs index 982060916..cfbc3a8af 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/AggregationBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/AggregationBinder.cs @@ -22,608 +22,607 @@ using Microsoft.OData.UriParser; using Microsoft.OData.UriParser.Aggregation; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many ODataLib classes.")] +internal class AggregationBinder : TransformationBinderBase { - [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many ODataLib classes.")] - internal class AggregationBinder : TransformationBinderBase - { - private const string GroupByContainerProperty = "GroupByContainer"; - private TransformationNode _transformation; + private const string GroupByContainerProperty = "GroupByContainer"; + private TransformationNode _transformation; - private IEnumerable _aggregateExpressions; - private IEnumerable _groupingProperties; + private IEnumerable _aggregateExpressions; + private IEnumerable _groupingProperties; - private Type _groupByClrType; + private Type _groupByClrType; - internal AggregationBinder(ODataQuerySettings settings, IAssemblyResolver assembliesResolver, Type elementType, - IEdmModel model, TransformationNode transformation) - : base(settings, assembliesResolver, elementType, model) - { - Contract.Assert(transformation != null); + internal AggregationBinder(ODataQuerySettings settings, IAssemblyResolver assembliesResolver, Type elementType, + IEdmModel model, TransformationNode transformation) + : base(settings, assembliesResolver, elementType, model) + { + Contract.Assert(transformation != null); - _transformation = transformation; + _transformation = transformation; - switch (transformation.Kind) - { - case TransformationNodeKind.Aggregate: - var aggregateClause = this._transformation as AggregateTransformationNode; - _aggregateExpressions = FixCustomMethodReturnTypes(aggregateClause.AggregateExpressions); - ResultClrType = typeof(NoGroupByAggregationWrapper); - break; - case TransformationNodeKind.GroupBy: - var groupByClause = this._transformation as GroupByTransformationNode; - _groupingProperties = groupByClause.GroupingProperties; - if (groupByClause.ChildTransformations != null) + switch (transformation.Kind) + { + case TransformationNodeKind.Aggregate: + var aggregateClause = this._transformation as AggregateTransformationNode; + _aggregateExpressions = FixCustomMethodReturnTypes(aggregateClause.AggregateExpressions); + ResultClrType = typeof(NoGroupByAggregationWrapper); + break; + case TransformationNodeKind.GroupBy: + var groupByClause = this._transformation as GroupByTransformationNode; + _groupingProperties = groupByClause.GroupingProperties; + if (groupByClause.ChildTransformations != null) + { + if (groupByClause.ChildTransformations.Kind == TransformationNodeKind.Aggregate) { - if (groupByClause.ChildTransformations.Kind == TransformationNodeKind.Aggregate) - { - var aggregationNode = (AggregateTransformationNode)groupByClause.ChildTransformations; - _aggregateExpressions = FixCustomMethodReturnTypes(aggregationNode.AggregateExpressions); - } - else - { - throw new NotImplementedException(); - } + var aggregationNode = (AggregateTransformationNode)groupByClause.ChildTransformations; + _aggregateExpressions = FixCustomMethodReturnTypes(aggregationNode.AggregateExpressions); } + else + { + throw new NotImplementedException(); + } + } - _groupByClrType = typeof(GroupByWrapper); - ResultClrType = typeof(AggregationWrapper); - break; - default: - throw new NotSupportedException(String.Format(CultureInfo.InvariantCulture, - SRResources.NotSupportedTransformationKind, transformation.Kind)); - } - - _groupByClrType = _groupByClrType ?? typeof(NoGroupByWrapper); + _groupByClrType = typeof(GroupByWrapper); + ResultClrType = typeof(AggregationWrapper); + break; + default: + throw new NotSupportedException(String.Format(CultureInfo.InvariantCulture, + SRResources.NotSupportedTransformationKind, transformation.Kind)); } - private static Expression WrapDynamicCastIfNeeded(Expression propertyAccessor) - { - if (propertyAccessor.Type == typeof(object)) - { - return Expression.Call(null, ExpressionHelperMethods.ConvertToDecimal, propertyAccessor); - } - - return propertyAccessor; - } + _groupByClrType = _groupByClrType ?? typeof(NoGroupByWrapper); + } - private IEnumerable FixCustomMethodReturnTypes(IEnumerable aggregateExpressions) + private static Expression WrapDynamicCastIfNeeded(Expression propertyAccessor) + { + if (propertyAccessor.Type == typeof(object)) { - return aggregateExpressions.Select(x => - { - var ae = x as AggregateExpression; - return ae != null ? FixCustomMethodReturnType(ae) : x; - }); + return Expression.Call(null, ExpressionHelperMethods.ConvertToDecimal, propertyAccessor); } - private AggregateExpression FixCustomMethodReturnType(AggregateExpression expression) + return propertyAccessor; + } + + private IEnumerable FixCustomMethodReturnTypes(IEnumerable aggregateExpressions) + { + return aggregateExpressions.Select(x => { - if (expression.Method != AggregationMethod.Custom) - { - return expression; - } + var ae = x as AggregateExpression; + return ae != null ? FixCustomMethodReturnType(ae) : x; + }); + } - var customMethod = GetCustomMethod(expression); + private AggregateExpression FixCustomMethodReturnType(AggregateExpression expression) + { + if (expression.Method != AggregationMethod.Custom) + { + return expression; + } - // var typeReference = customMethod.ReturnType.GetEdmPrimitiveTypeReference(); - var typeReference = Model.GetEdmPrimitiveTypeReference(customMethod.ReturnType); + var customMethod = GetCustomMethod(expression); - return new AggregateExpression(expression.Expression, expression.MethodDefinition, expression.Alias, typeReference); - } + // var typeReference = customMethod.ReturnType.GetEdmPrimitiveTypeReference(); + var typeReference = Model.GetEdmPrimitiveTypeReference(customMethod.ReturnType); - private MethodInfo GetCustomMethod(AggregateExpression expression) - { - var propertyLambda = Expression.Lambda(BindAccessor(expression.Expression), this.LambdaParameter); - Type inputType = propertyLambda.Body.Type; + return new AggregateExpression(expression.Expression, expression.MethodDefinition, expression.Alias, typeReference); + } - string methodToken = expression.MethodDefinition.MethodLabel; - var customFunctionAnnotations = Model.GetAnnotationValue(Model); + private MethodInfo GetCustomMethod(AggregateExpression expression) + { + var propertyLambda = Expression.Lambda(BindAccessor(expression.Expression), this.LambdaParameter); + Type inputType = propertyLambda.Body.Type; - MethodInfo customMethod; - if (!customFunctionAnnotations.GetMethodInfo(methodToken, inputType, out customMethod)) - { - throw new ODataException( - Error.Format( - SRResources.AggregationNotSupportedForType, - expression.Method, - expression.Expression, - inputType)); - } + string methodToken = expression.MethodDefinition.MethodLabel; + var customFunctionAnnotations = Model.GetAnnotationValue(Model); - return customMethod; + MethodInfo customMethod; + if (!customFunctionAnnotations.GetMethodInfo(methodToken, inputType, out customMethod)) + { + throw new ODataException( + Error.Format( + SRResources.AggregationNotSupportedForType, + expression.Method, + expression.Expression, + inputType)); } - public IQueryable Bind(IQueryable query) - { - PreprocessQuery(query); + return customMethod; + } + + public IQueryable Bind(IQueryable query) + { + PreprocessQuery(query); - query = FlattenReferencedProperties(query); + query = FlattenReferencedProperties(query); - // Answer is query.GroupBy($it => new DynamicType1() {...}).Select($it => new DynamicType2() {...}) - // We are doing Grouping even if only aggregate was specified to have a IQuaryable after aggregation - IQueryable grouping = BindGroupBy(query); + // Answer is query.GroupBy($it => new DynamicType1() {...}).Select($it => new DynamicType2() {...}) + // We are doing Grouping even if only aggregate was specified to have a IQuaryable after aggregation + IQueryable grouping = BindGroupBy(query); - IQueryable result = BindSelect(grouping); + IQueryable result = BindSelect(grouping); - return result; - } + return result; + } - /// - /// Pre flattens properties referenced in aggregate clause to avoid generation of nested queries by EF. - /// For query like groupby((A), aggregate(B/C with max as Alias1, B/D with max as Alias2)) we need to generate - /// .Select( - /// $it => new FlattenninWrapper () { - /// Source = $it, // Will used in groupby stage - /// Container = new { - /// Value = $it.B.C - /// Next = new { - /// Value = $it.B.D - /// } - /// } - /// } - /// ) - /// Also we need to populate expressions to access B/C and B/D in aggregate stage. It will look like: - /// B/C : $it.Container.Value - /// B/D : $it.Container.Next.Value - /// - /// - /// Query with Select that flattens properties - private IQueryable FlattenReferencedProperties(IQueryable query) + /// + /// Pre flattens properties referenced in aggregate clause to avoid generation of nested queries by EF. + /// For query like groupby((A), aggregate(B/C with max as Alias1, B/D with max as Alias2)) we need to generate + /// .Select( + /// $it => new FlattenninWrapper () { + /// Source = $it, // Will used in groupby stage + /// Container = new { + /// Value = $it.B.C + /// Next = new { + /// Value = $it.B.D + /// } + /// } + /// } + /// ) + /// Also we need to populate expressions to access B/C and B/D in aggregate stage. It will look like: + /// B/C : $it.Container.Value + /// B/D : $it.Container.Next.Value + /// + /// + /// Query with Select that flattens properties + private IQueryable FlattenReferencedProperties(IQueryable query) + { + if (_aggregateExpressions != null + && _aggregateExpressions.OfType().Any(e => e.Method != AggregationMethod.VirtualPropertyCount) + && _groupingProperties != null + && _groupingProperties.Any() + && (FlattenedPropertyContainer == null || !FlattenedPropertyContainer.Any())) { - if (_aggregateExpressions != null - && _aggregateExpressions.OfType().Any(e => e.Method != AggregationMethod.VirtualPropertyCount) - && _groupingProperties != null - && _groupingProperties.Any() - && (FlattenedPropertyContainer == null || !FlattenedPropertyContainer.Any())) + var wrapperType = typeof(FlatteningWrapper<>).MakeGenericType(this.ElementType); + var sourceProperty = wrapperType.GetProperty("Source"); + List wta = new List(); + wta.Add(Expression.Bind(sourceProperty, this.LambdaParameter)); + + var aggrregatedPropertiesToFlatten = _aggregateExpressions.OfType().Where(e => e.Method != AggregationMethod.VirtualPropertyCount).ToList(); + // Generated Select will be stack like, meaning that first property in the list will be deepest one + // For example if we add $it.B.C, $it.B.D, select will look like + // new { + // Value = $it.B.C + // Next = new { + // Value = $it.B.D + // } + // } + // We are generated references (in currentContainerExpression) from the beginning of the Select ($it.Value, then $it.Next.Value etc.) + // We have proper match we need insert properties in reverse order + // After this + // properties = { $it.B.D, $it.B.C} + // _preFlattendMAp = { {$it.B.C, $it.Value}, {$it.B.D, $it.Next.Value} } + var properties = new NamedPropertyExpression[aggrregatedPropertiesToFlatten.Count]; + var aliasIdx = aggrregatedPropertiesToFlatten.Count - 1; + var aggParam = Expression.Parameter(wrapperType, "$it"); + var currentContainerExpression = Expression.Property(aggParam, GroupByContainerProperty); + foreach (var aggExpression in aggrregatedPropertiesToFlatten) { - var wrapperType = typeof(FlatteningWrapper<>).MakeGenericType(this.ElementType); - var sourceProperty = wrapperType.GetProperty("Source"); - List wta = new List(); - wta.Add(Expression.Bind(sourceProperty, this.LambdaParameter)); - - var aggrregatedPropertiesToFlatten = _aggregateExpressions.OfType().Where(e => e.Method != AggregationMethod.VirtualPropertyCount).ToList(); - // Generated Select will be stack like, meaning that first property in the list will be deepest one - // For example if we add $it.B.C, $it.B.D, select will look like - // new { - // Value = $it.B.C - // Next = new { - // Value = $it.B.D - // } - // } - // We are generated references (in currentContainerExpression) from the beginning of the Select ($it.Value, then $it.Next.Value etc.) - // We have proper match we need insert properties in reverse order - // After this - // properties = { $it.B.D, $it.B.C} - // _preFlattendMAp = { {$it.B.C, $it.Value}, {$it.B.D, $it.Next.Value} } - var properties = new NamedPropertyExpression[aggrregatedPropertiesToFlatten.Count]; - var aliasIdx = aggrregatedPropertiesToFlatten.Count - 1; - var aggParam = Expression.Parameter(wrapperType, "$it"); - var currentContainerExpression = Expression.Property(aggParam, GroupByContainerProperty); - foreach (var aggExpression in aggrregatedPropertiesToFlatten) - { - var alias = "Property" + aliasIdx.ToString(CultureInfo.CurrentCulture); // We just need unique alias, we aren't going to use it - - // Add Value = $it.B.C - var propAccessExpression = BindAccessor(aggExpression.Expression); - var type = propAccessExpression.Type; - propAccessExpression = WrapConvert(propAccessExpression); - properties[aliasIdx] = new NamedPropertyExpression(Expression.Constant(alias), propAccessExpression); - - // Save $it.Container.Next.Value for future use - UnaryExpression flatAccessExpression = Expression.Convert( - Expression.Property(currentContainerExpression, "Value"), - type); - currentContainerExpression = Expression.Property(currentContainerExpression, "Next"); - _preFlattenedMap.Add(aggExpression.Expression, flatAccessExpression); - aliasIdx--; - } - - var wrapperProperty = ResultClrType.GetProperty(GroupByContainerProperty); + var alias = "Property" + aliasIdx.ToString(CultureInfo.CurrentCulture); // We just need unique alias, we aren't going to use it + + // Add Value = $it.B.C + var propAccessExpression = BindAccessor(aggExpression.Expression); + var type = propAccessExpression.Type; + propAccessExpression = WrapConvert(propAccessExpression); + properties[aliasIdx] = new NamedPropertyExpression(Expression.Constant(alias), propAccessExpression); + + // Save $it.Container.Next.Value for future use + UnaryExpression flatAccessExpression = Expression.Convert( + Expression.Property(currentContainerExpression, "Value"), + type); + currentContainerExpression = Expression.Property(currentContainerExpression, "Next"); + _preFlattenedMap.Add(aggExpression.Expression, flatAccessExpression); + aliasIdx--; + } - wta.Add(Expression.Bind(wrapperProperty, AggregationPropertyContainer.CreateNextNamedPropertyContainer(properties))); + var wrapperProperty = ResultClrType.GetProperty(GroupByContainerProperty); - var flatLambda = Expression.Lambda(Expression.MemberInit(Expression.New(wrapperType), wta), LambdaParameter); + wta.Add(Expression.Bind(wrapperProperty, AggregationPropertyContainer.CreateNextNamedPropertyContainer(properties))); - query = ExpressionHelpers.Select(query, flatLambda, this.ElementType); + var flatLambda = Expression.Lambda(Expression.MemberInit(Expression.New(wrapperType), wta), LambdaParameter); - // We applied flattening let .GroupBy know about it. - this.LambdaParameter = aggParam; - } + query = ExpressionHelpers.Select(query, flatLambda, this.ElementType); - return query; + // We applied flattening let .GroupBy know about it. + this.LambdaParameter = aggParam; } - private Dictionary _preFlattenedMap = new Dictionary(); + return query; + } + + private Dictionary _preFlattenedMap = new Dictionary(); - private IQueryable BindSelect(IQueryable grouping) + private IQueryable BindSelect(IQueryable grouping) + { + // Should return following expression + // .Select($it => New DynamicType2() + // { + // GroupByContainer = $it.Key.GroupByContainer // If groupby section present + // Container => new AggregationPropertyContainer() { + // Name = "Alias1", + // Value = $it.AsQuaryable().Sum(i => i.AggregatableProperty), + // Next = new LastInChain() { + // Name = "Alias2", + // Value = $it.AsQuaryable().Sum(i => i.AggregatableProperty) + // } + // } + // }) + var groupingType = typeof(IGrouping<,>).MakeGenericType(this._groupByClrType, this.ElementType); + ParameterExpression accum = Expression.Parameter(groupingType, "$it"); + + List wrapperTypeMemberAssignments = new List(); + + // Setting GroupByContainer property when previous step was grouping + if (this._groupingProperties != null && this._groupingProperties.Any()) { - // Should return following expression - // .Select($it => New DynamicType2() - // { - // GroupByContainer = $it.Key.GroupByContainer // If groupby section present - // Container => new AggregationPropertyContainer() { - // Name = "Alias1", - // Value = $it.AsQuaryable().Sum(i => i.AggregatableProperty), - // Next = new LastInChain() { - // Name = "Alias2", - // Value = $it.AsQuaryable().Sum(i => i.AggregatableProperty) - // } - // } - // }) - var groupingType = typeof(IGrouping<,>).MakeGenericType(this._groupByClrType, this.ElementType); - ParameterExpression accum = Expression.Parameter(groupingType, "$it"); - - List wrapperTypeMemberAssignments = new List(); - - // Setting GroupByContainer property when previous step was grouping - if (this._groupingProperties != null && this._groupingProperties.Any()) - { - var wrapperProperty = this.ResultClrType.GetProperty(GroupByContainerProperty); + var wrapperProperty = this.ResultClrType.GetProperty(GroupByContainerProperty); - wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, Expression.Property(Expression.Property(accum, "Key"), GroupByContainerProperty))); - } + wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, Expression.Property(Expression.Property(accum, "Key"), GroupByContainerProperty))); + } - // Setting Container property when we have aggregation clauses - if (_aggregateExpressions != null) + // Setting Container property when we have aggregation clauses + if (_aggregateExpressions != null) + { + var properties = new List(); + foreach (var aggExpression in _aggregateExpressions) { - var properties = new List(); - foreach (var aggExpression in _aggregateExpressions) - { - properties.Add(new NamedPropertyExpression(Expression.Constant(aggExpression.Alias), CreateAggregationExpression(accum, aggExpression, this.ElementType))); - } - - var wrapperProperty = ResultClrType.GetProperty("Container"); - wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, AggregationPropertyContainer.CreateNextNamedPropertyContainer(properties))); + properties.Add(new NamedPropertyExpression(Expression.Constant(aggExpression.Alias), CreateAggregationExpression(accum, aggExpression, this.ElementType))); } - var initilizedMember = - Expression.MemberInit(Expression.New(ResultClrType), wrapperTypeMemberAssignments); - var selectLambda = Expression.Lambda(initilizedMember, accum); - - var result = ExpressionHelpers.Select(grouping, selectLambda, groupingType); - return result; + var wrapperProperty = ResultClrType.GetProperty("Container"); + wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, AggregationPropertyContainer.CreateNextNamedPropertyContainer(properties))); } - private List CreateSelectMemberAssigments(Type type, MemberExpression propertyAccessor, - IEnumerable properties) + var initilizedMember = + Expression.MemberInit(Expression.New(ResultClrType), wrapperTypeMemberAssignments); + var selectLambda = Expression.Lambda(initilizedMember, accum); + + var result = ExpressionHelpers.Select(grouping, selectLambda, groupingType); + return result; + } + + private List CreateSelectMemberAssigments(Type type, MemberExpression propertyAccessor, + IEnumerable properties) + { + var wrapperTypeMemberAssignments = new List(); + if (_groupingProperties != null) { - var wrapperTypeMemberAssignments = new List(); - if (_groupingProperties != null) + foreach (var node in properties) { - foreach (var node in properties) + var nodePropertyAccessor = Expression.Property(propertyAccessor, node.Name); + var member = type.GetMember(node.Name).Single(); + if (node.Expression != null) { - var nodePropertyAccessor = Expression.Property(propertyAccessor, node.Name); - var member = type.GetMember(node.Name).Single(); - if (node.Expression != null) - { - wrapperTypeMemberAssignments.Add(Expression.Bind(member, nodePropertyAccessor)); - } - else - { - var memberType = (member as PropertyInfo).PropertyType; - var expr = Expression.MemberInit(Expression.New(memberType), - CreateSelectMemberAssigments(memberType, nodePropertyAccessor, node.ChildTransformations)); - wrapperTypeMemberAssignments.Add(Expression.Bind(member, expr)); - } + wrapperTypeMemberAssignments.Add(Expression.Bind(member, nodePropertyAccessor)); + } + else + { + var memberType = (member as PropertyInfo).PropertyType; + var expr = Expression.MemberInit(Expression.New(memberType), + CreateSelectMemberAssigments(memberType, nodePropertyAccessor, node.ChildTransformations)); + wrapperTypeMemberAssignments.Add(Expression.Bind(member, expr)); } } - - return wrapperTypeMemberAssignments; } - private Expression CreateAggregationExpression(ParameterExpression accum, AggregateExpressionBase expression, Type baseType) + return wrapperTypeMemberAssignments; + } + + private Expression CreateAggregationExpression(ParameterExpression accum, AggregateExpressionBase expression, Type baseType) + { + switch (expression.AggregateKind) { - switch (expression.AggregateKind) - { - case AggregateExpressionKind.PropertyAggregate: - return CreatePropertyAggregateExpression(accum, expression as AggregateExpression, baseType); - case AggregateExpressionKind.EntitySetAggregate: - return CreateEntitySetAggregateExpression(accum, expression as EntitySetAggregateExpression, baseType); - default: - throw new ODataException(Error.Format(SRResources.AggregateKindNotSupported, expression.AggregateKind)); - } + case AggregateExpressionKind.PropertyAggregate: + return CreatePropertyAggregateExpression(accum, expression as AggregateExpression, baseType); + case AggregateExpressionKind.EntitySetAggregate: + return CreateEntitySetAggregateExpression(accum, expression as EntitySetAggregateExpression, baseType); + default: + throw new ODataException(Error.Format(SRResources.AggregateKindNotSupported, expression.AggregateKind)); } + } - private Expression CreateEntitySetAggregateExpression( - ParameterExpression accum, EntitySetAggregateExpression expression, Type baseType) + private Expression CreateEntitySetAggregateExpression( + ParameterExpression accum, EntitySetAggregateExpression expression, Type baseType) + { + // Should return following expression + // $it => $it.AsQueryable() + // .SelectMany($it => $it.SomeEntitySet) + // .GroupBy($gr => new Object()) + // .Select($p => new DynamicTypeWrapper() + // { + // AliasOne = $p.AsQueryable().AggMethodOne($it => $it.SomePropertyOfSomeEntitySet), + // AliasTwo = $p.AsQueryable().AggMethodTwo($it => $it.AnotherPropertyOfSomeEntitySet), + // ... + // AliasN = ... , // A nested expression of this same format. + // ... + // }) + + List wrapperTypeMemberAssignments = new List(); + var asQueryableMethod = ExpressionHelperMethods.QueryableAsQueryable.MakeGenericMethod(baseType); + Expression asQueryableExpression = Expression.Call(null, asQueryableMethod, accum); + + // Create lambda to access the entity set from expression + var source = BindAccessor(expression.Expression.Source); + string propertyName = Model.GetClrPropertyName(expression.Expression.NavigationProperty); + + var property = Expression.Property(source, propertyName); + + var baseElementType = source.Type; + var selectedElementType = property.Type.GenericTypeArguments.Single(); + + // Create method to get property collections to aggregate + MethodInfo selectManyMethod + = ExpressionHelperMethods.EnumerableSelectManyGeneric.MakeGenericMethod(baseElementType, selectedElementType); + + // Create the lambda that access the property in the selectMany clause. + var selectManyParam = Expression.Parameter(baseElementType, "$it"); + var propertyExpression = Expression.Property(selectManyParam, expression.Expression.NavigationProperty.Name); + + // Collection selector body is IQueryable, we need to adjust the type to IEnumerable, to match the SelectMany signature + // therefore the delegate type is specified explicitly + var collectionSelectorLambdaType = typeof(Func<,>).MakeGenericType( + source.Type, + typeof(IEnumerable<>).MakeGenericType(selectedElementType)); + var selectManyLambda = Expression.Lambda(collectionSelectorLambdaType, propertyExpression, selectManyParam); + + // Get expression to get collection of entities + var entitySet = Expression.Call(null, selectManyMethod, asQueryableExpression, selectManyLambda); + + // Getting method and lambda expression of groupBy + var groupKeyType = typeof(object); + MethodInfo groupByMethod = + ExpressionHelperMethods.EnumerableGroupByGeneric.MakeGenericMethod(selectedElementType, groupKeyType); + var groupByLambda = Expression.Lambda( + Expression.New(groupKeyType), + Expression.Parameter(selectedElementType, "$gr")); + + // Group entities in a single group to apply select + var groupedEntitySet = Expression.Call(null, groupByMethod, entitySet, groupByLambda); + + var groupingType = typeof(IGrouping<,>).MakeGenericType(groupKeyType, selectedElementType); + ParameterExpression innerAccum = Expression.Parameter(groupingType, "$p"); + + // Nested properties + // Create dynamicTypeWrapper to encapsulate the aggregate result + var properties = new List(); + foreach (var aggExpression in expression.Children) { - // Should return following expression - // $it => $it.AsQueryable() - // .SelectMany($it => $it.SomeEntitySet) - // .GroupBy($gr => new Object()) - // .Select($p => new DynamicTypeWrapper() - // { - // AliasOne = $p.AsQueryable().AggMethodOne($it => $it.SomePropertyOfSomeEntitySet), - // AliasTwo = $p.AsQueryable().AggMethodTwo($it => $it.AnotherPropertyOfSomeEntitySet), - // ... - // AliasN = ... , // A nested expression of this same format. - // ... - // }) - - List wrapperTypeMemberAssignments = new List(); - var asQueryableMethod = ExpressionHelperMethods.QueryableAsQueryable.MakeGenericMethod(baseType); - Expression asQueryableExpression = Expression.Call(null, asQueryableMethod, accum); - - // Create lambda to access the entity set from expression - var source = BindAccessor(expression.Expression.Source); - string propertyName = Model.GetClrPropertyName(expression.Expression.NavigationProperty); - - var property = Expression.Property(source, propertyName); - - var baseElementType = source.Type; - var selectedElementType = property.Type.GenericTypeArguments.Single(); - - // Create method to get property collections to aggregate - MethodInfo selectManyMethod - = ExpressionHelperMethods.EnumerableSelectManyGeneric.MakeGenericMethod(baseElementType, selectedElementType); - - // Create the lambda that access the property in the selectMany clause. - var selectManyParam = Expression.Parameter(baseElementType, "$it"); - var propertyExpression = Expression.Property(selectManyParam, expression.Expression.NavigationProperty.Name); - - // Collection selector body is IQueryable, we need to adjust the type to IEnumerable, to match the SelectMany signature - // therefore the delegate type is specified explicitly - var collectionSelectorLambdaType = typeof(Func<,>).MakeGenericType( - source.Type, - typeof(IEnumerable<>).MakeGenericType(selectedElementType)); - var selectManyLambda = Expression.Lambda(collectionSelectorLambdaType, propertyExpression, selectManyParam); - - // Get expression to get collection of entities - var entitySet = Expression.Call(null, selectManyMethod, asQueryableExpression, selectManyLambda); - - // Getting method and lambda expression of groupBy - var groupKeyType = typeof(object); - MethodInfo groupByMethod = - ExpressionHelperMethods.EnumerableGroupByGeneric.MakeGenericMethod(selectedElementType, groupKeyType); - var groupByLambda = Expression.Lambda( - Expression.New(groupKeyType), - Expression.Parameter(selectedElementType, "$gr")); - - // Group entities in a single group to apply select - var groupedEntitySet = Expression.Call(null, groupByMethod, entitySet, groupByLambda); - - var groupingType = typeof(IGrouping<,>).MakeGenericType(groupKeyType, selectedElementType); - ParameterExpression innerAccum = Expression.Parameter(groupingType, "$p"); - - // Nested properties - // Create dynamicTypeWrapper to encapsulate the aggregate result - var properties = new List(); - foreach (var aggExpression in expression.Children) - { - properties.Add(new NamedPropertyExpression(Expression.Constant(aggExpression.Alias), CreateAggregationExpression(innerAccum, aggExpression, selectedElementType))); - } + properties.Add(new NamedPropertyExpression(Expression.Constant(aggExpression.Alias), CreateAggregationExpression(innerAccum, aggExpression, selectedElementType))); + } - var nestedResultType = typeof(EntitySetAggregationWrapper); - var wrapperProperty = nestedResultType.GetProperty("Container"); - wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, AggregationPropertyContainer.CreateNextNamedPropertyContainer(properties))); + var nestedResultType = typeof(EntitySetAggregationWrapper); + var wrapperProperty = nestedResultType.GetProperty("Container"); + wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, AggregationPropertyContainer.CreateNextNamedPropertyContainer(properties))); - var initializedMember = - Expression.MemberInit(Expression.New(nestedResultType), wrapperTypeMemberAssignments); - var selectLambda = Expression.Lambda(initializedMember, innerAccum); + var initializedMember = + Expression.MemberInit(Expression.New(nestedResultType), wrapperTypeMemberAssignments); + var selectLambda = Expression.Lambda(initializedMember, innerAccum); - // Get select method - MethodInfo selectMethod = - ExpressionHelperMethods.EnumerableSelectGeneric.MakeGenericMethod( - groupingType, - selectLambda.Body.Type); + // Get select method + MethodInfo selectMethod = + ExpressionHelperMethods.EnumerableSelectGeneric.MakeGenericMethod( + groupingType, + selectLambda.Body.Type); - return Expression.Call(null, selectMethod, groupedEntitySet, selectLambda); - } + return Expression.Call(null, selectMethod, groupedEntitySet, selectLambda); + } - private Expression CreatePropertyAggregateExpression(ParameterExpression accum, AggregateExpression expression, Type baseType) + private Expression CreatePropertyAggregateExpression(ParameterExpression accum, AggregateExpression expression, Type baseType) + { + // accumulate type is IGrouping<,baseType> that implements IEnumerable + // we need cast it to IEnumerable during expression building (IEnumerable)$it + // however for EF6 we need to use $it.AsQueryable() due to limitations in types of casts that will properly translated + Expression asQuerableExpression = null; + if (ClassicEF) { - // accumulate type is IGrouping<,baseType> that implements IEnumerable - // we need cast it to IEnumerable during expression building (IEnumerable)$it - // however for EF6 we need to use $it.AsQueryable() due to limitations in types of casts that will properly translated - Expression asQuerableExpression = null; - if (ClassicEF) - { - var asQuerableMethod = ExpressionHelperMethods.QueryableAsQueryable.MakeGenericMethod(baseType); - asQuerableExpression = Expression.Call(null, asQuerableMethod, accum); - } - else - { - var queryableType = typeof(IEnumerable<>).MakeGenericType(baseType); - asQuerableExpression = Expression.Convert(accum, queryableType); - } + var asQuerableMethod = ExpressionHelperMethods.QueryableAsQueryable.MakeGenericMethod(baseType); + asQuerableExpression = Expression.Call(null, asQuerableMethod, accum); + } + else + { + var queryableType = typeof(IEnumerable<>).MakeGenericType(baseType); + asQuerableExpression = Expression.Convert(accum, queryableType); + } - // $count is a virtual property, so there's not a propertyLambda to create. - if (expression.Method == AggregationMethod.VirtualPropertyCount) - { - var countMethod = (ClassicEF - ? ExpressionHelperMethods.QueryableCountGeneric - : ExpressionHelperMethods.EnumerableCountGeneric).MakeGenericMethod(baseType); - return WrapConvert(Expression.Call(null, countMethod, asQuerableExpression)); - } + // $count is a virtual property, so there's not a propertyLambda to create. + if (expression.Method == AggregationMethod.VirtualPropertyCount) + { + var countMethod = (ClassicEF + ? ExpressionHelperMethods.QueryableCountGeneric + : ExpressionHelperMethods.EnumerableCountGeneric).MakeGenericMethod(baseType); + return WrapConvert(Expression.Call(null, countMethod, asQuerableExpression)); + } - Expression body; + Expression body; - var lambdaParameter = baseType == this.ElementType ? this.LambdaParameter : Expression.Parameter(baseType, "$it"); - if (!this._preFlattenedMap.TryGetValue(expression.Expression, out body)) - { - body = BindAccessor(expression.Expression, lambdaParameter); - } - LambdaExpression propertyLambda = Expression.Lambda(body, lambdaParameter); + var lambdaParameter = baseType == this.ElementType ? this.LambdaParameter : Expression.Parameter(baseType, "$it"); + if (!this._preFlattenedMap.TryGetValue(expression.Expression, out body)) + { + body = BindAccessor(expression.Expression, lambdaParameter); + } + LambdaExpression propertyLambda = Expression.Lambda(body, lambdaParameter); - Expression aggregationExpression; + Expression aggregationExpression; - switch (expression.Method) - { - case AggregationMethod.Min: - { - var minMethod = (ClassicEF - ? ExpressionHelperMethods.QueryableMin - : ExpressionHelperMethods.EnumerableMin).MakeGenericMethod(baseType, - propertyLambda.Body.Type); - aggregationExpression = Expression.Call(null, minMethod, asQuerableExpression, propertyLambda); - } - break; - case AggregationMethod.Max: - { - var maxMethod = (ClassicEF - ? ExpressionHelperMethods.QueryableMax - : ExpressionHelperMethods.EnumerableMax).MakeGenericMethod(baseType, - propertyLambda.Body.Type); - aggregationExpression = Expression.Call(null, maxMethod, asQuerableExpression, propertyLambda); - } - break; - case AggregationMethod.Sum: + switch (expression.Method) + { + case AggregationMethod.Min: + { + var minMethod = (ClassicEF + ? ExpressionHelperMethods.QueryableMin + : ExpressionHelperMethods.EnumerableMin).MakeGenericMethod(baseType, + propertyLambda.Body.Type); + aggregationExpression = Expression.Call(null, minMethod, asQuerableExpression, propertyLambda); + } + break; + case AggregationMethod.Max: + { + var maxMethod = (ClassicEF + ? ExpressionHelperMethods.QueryableMax + : ExpressionHelperMethods.EnumerableMax).MakeGenericMethod(baseType, + propertyLambda.Body.Type); + aggregationExpression = Expression.Call(null, maxMethod, asQuerableExpression, propertyLambda); + } + break; + case AggregationMethod.Sum: + { + MethodInfo sumGenericMethod; + // For Dynamic properties cast to decimal + Expression propertyExpression = WrapDynamicCastIfNeeded(body); + propertyLambda = Expression.Lambda(propertyExpression, lambdaParameter); + + if ( + !(ClassicEF + ? ExpressionHelperMethods.QueryableSumGenerics + : ExpressionHelperMethods.EnumerableSumGenerics).TryGetValue(propertyExpression.Type, + out sumGenericMethod)) { - MethodInfo sumGenericMethod; - // For Dynamic properties cast to decimal - Expression propertyExpression = WrapDynamicCastIfNeeded(body); - propertyLambda = Expression.Lambda(propertyExpression, lambdaParameter); - - if ( - !(ClassicEF - ? ExpressionHelperMethods.QueryableSumGenerics - : ExpressionHelperMethods.EnumerableSumGenerics).TryGetValue(propertyExpression.Type, - out sumGenericMethod)) - { - throw new ODataException(Error.Format(SRResources.AggregationNotSupportedForType, - expression.Method, expression.Expression, propertyExpression.Type)); - } - - var sumMethod = sumGenericMethod.MakeGenericMethod(baseType); - aggregationExpression = Expression.Call(null, sumMethod, asQuerableExpression, propertyLambda); - - // For Dynamic properties cast back to object - if (propertyLambda.Type == typeof(object)) - { - aggregationExpression = Expression.Convert(aggregationExpression, typeof(object)); - } + throw new ODataException(Error.Format(SRResources.AggregationNotSupportedForType, + expression.Method, expression.Expression, propertyExpression.Type)); } - break; - case AggregationMethod.Average: + + var sumMethod = sumGenericMethod.MakeGenericMethod(baseType); + aggregationExpression = Expression.Call(null, sumMethod, asQuerableExpression, propertyLambda); + + // For Dynamic properties cast back to object + if (propertyLambda.Type == typeof(object)) { - MethodInfo averageGenericMethod; - // For Dynamic properties cast to decimal - Expression propertyExpression = WrapDynamicCastIfNeeded(body); - propertyLambda = Expression.Lambda(propertyExpression, lambdaParameter); - - if ( - !(ClassicEF - ? ExpressionHelperMethods.QueryableAverageGenerics - : ExpressionHelperMethods.EnumerableAverageGenerics).TryGetValue(propertyExpression.Type, - out averageGenericMethod)) - { - throw new ODataException(Error.Format(SRResources.AggregationNotSupportedForType, - expression.Method, expression.Expression, propertyExpression.Type)); - } - - var averageMethod = averageGenericMethod.MakeGenericMethod(baseType); - aggregationExpression = Expression.Call(null, averageMethod, asQuerableExpression, propertyLambda); - - // For Dynamic properties cast back to object - if (propertyLambda.Type == typeof(object)) - { - aggregationExpression = Expression.Convert(aggregationExpression, typeof(object)); - } + aggregationExpression = Expression.Convert(aggregationExpression, typeof(object)); } - break; - case AggregationMethod.CountDistinct: + } + break; + case AggregationMethod.Average: + { + MethodInfo averageGenericMethod; + // For Dynamic properties cast to decimal + Expression propertyExpression = WrapDynamicCastIfNeeded(body); + propertyLambda = Expression.Lambda(propertyExpression, lambdaParameter); + + if ( + !(ClassicEF + ? ExpressionHelperMethods.QueryableAverageGenerics + : ExpressionHelperMethods.EnumerableAverageGenerics).TryGetValue(propertyExpression.Type, + out averageGenericMethod)) { - // I select the specific field - var selectMethod = - (ClassicEF - ? ExpressionHelperMethods.QueryableSelectGeneric - : ExpressionHelperMethods.EnumerableSelectGeneric).MakeGenericMethod(this.ElementType, - propertyLambda.Body.Type); - Expression queryableSelectExpression = Expression.Call(null, selectMethod, asQuerableExpression, - propertyLambda); - - // I run distinct over the set of items - var distinctMethod = - (ClassicEF - ? ExpressionHelperMethods.QueryableDistinct - : ExpressionHelperMethods.EnumerableDistinct).MakeGenericMethod(propertyLambda.Body.Type); - Expression distinctExpression = Expression.Call(null, distinctMethod, queryableSelectExpression); - - // I count the distinct items as the aggregation expression - var countMethod = - (ClassicEF - ? ExpressionHelperMethods.QueryableCountGeneric - : ExpressionHelperMethods.EnumerableCountGeneric).MakeGenericMethod(propertyLambda.Body.Type); - aggregationExpression = Expression.Call(null, countMethod, distinctExpression); + throw new ODataException(Error.Format(SRResources.AggregationNotSupportedForType, + expression.Method, expression.Expression, propertyExpression.Type)); } - break; - case AggregationMethod.Custom: + + var averageMethod = averageGenericMethod.MakeGenericMethod(baseType); + aggregationExpression = Expression.Call(null, averageMethod, asQuerableExpression, propertyLambda); + + // For Dynamic properties cast back to object + if (propertyLambda.Type == typeof(object)) { - MethodInfo customMethod = GetCustomMethod(expression); - var selectMethod = - (ClassicEF - ? ExpressionHelperMethods.QueryableSelectGeneric - : ExpressionHelperMethods.EnumerableSelectGeneric).MakeGenericMethod(this.ElementType, propertyLambda.Body.Type); - var selectExpression = Expression.Call(null, selectMethod, asQuerableExpression, propertyLambda); - aggregationExpression = Expression.Call(null, customMethod, selectExpression); + aggregationExpression = Expression.Convert(aggregationExpression, typeof(object)); } - break; - default: - throw new ODataException(Error.Format(SRResources.AggregationMethodNotSupported, expression.Method)); - } + } + break; + case AggregationMethod.CountDistinct: + { + // I select the specific field + var selectMethod = + (ClassicEF + ? ExpressionHelperMethods.QueryableSelectGeneric + : ExpressionHelperMethods.EnumerableSelectGeneric).MakeGenericMethod(this.ElementType, + propertyLambda.Body.Type); + Expression queryableSelectExpression = Expression.Call(null, selectMethod, asQuerableExpression, + propertyLambda); + + // I run distinct over the set of items + var distinctMethod = + (ClassicEF + ? ExpressionHelperMethods.QueryableDistinct + : ExpressionHelperMethods.EnumerableDistinct).MakeGenericMethod(propertyLambda.Body.Type); + Expression distinctExpression = Expression.Call(null, distinctMethod, queryableSelectExpression); + + // I count the distinct items as the aggregation expression + var countMethod = + (ClassicEF + ? ExpressionHelperMethods.QueryableCountGeneric + : ExpressionHelperMethods.EnumerableCountGeneric).MakeGenericMethod(propertyLambda.Body.Type); + aggregationExpression = Expression.Call(null, countMethod, distinctExpression); + } + break; + case AggregationMethod.Custom: + { + MethodInfo customMethod = GetCustomMethod(expression); + var selectMethod = + (ClassicEF + ? ExpressionHelperMethods.QueryableSelectGeneric + : ExpressionHelperMethods.EnumerableSelectGeneric).MakeGenericMethod(this.ElementType, propertyLambda.Body.Type); + var selectExpression = Expression.Call(null, selectMethod, asQuerableExpression, propertyLambda); + aggregationExpression = Expression.Call(null, customMethod, selectExpression); + } + break; + default: + throw new ODataException(Error.Format(SRResources.AggregationMethodNotSupported, expression.Method)); + } + + return WrapConvert(aggregationExpression); + } - return WrapConvert(aggregationExpression); + private IQueryable BindGroupBy(IQueryable query) + { + LambdaExpression groupLambda = null; + Type elementType = query.ElementType; + if (_groupingProperties != null && _groupingProperties.Any()) + { + // Generates expression + // .GroupBy($it => new DynamicTypeWrapper() + // { + // GroupByContainer => new AggregationPropertyContainer() { + // Name = "Prop1", + // Value = $it.Prop1, + // Next = new AggregationPropertyContainer() { + // Name = "Prop2", + // Value = $it.Prop2, // int + // Next = new LastInChain() { + // Name = "Prop3", + // Value = $it.Prop3 + // } + // } + // } + // }) + List properties = CreateGroupByMemberAssignments(_groupingProperties); + + var wrapperProperty = typeof(GroupByWrapper).GetProperty(GroupByContainerProperty); + List wta = new List(); + wta.Add(Expression.Bind(wrapperProperty, AggregationPropertyContainer.CreateNextNamedPropertyContainer(properties))); + groupLambda = Expression.Lambda(Expression.MemberInit(Expression.New(typeof(GroupByWrapper)), wta), LambdaParameter); + } + else + { + // We do not have properties to aggregate + // .GroupBy($it => new NoGroupByWrapper()) + groupLambda = Expression.Lambda(Expression.New(this._groupByClrType), this.LambdaParameter); } - private IQueryable BindGroupBy(IQueryable query) + return ExpressionHelpers.GroupBy(query, groupLambda, elementType, this._groupByClrType); + } + + private List CreateGroupByMemberAssignments(IEnumerable nodes) + { + var properties = new List(); + foreach (var grpProp in nodes) { - LambdaExpression groupLambda = null; - Type elementType = query.ElementType; - if (_groupingProperties != null && _groupingProperties.Any()) + var propertyName = grpProp.Name; + if (grpProp.Expression != null) { - // Generates expression - // .GroupBy($it => new DynamicTypeWrapper() - // { - // GroupByContainer => new AggregationPropertyContainer() { - // Name = "Prop1", - // Value = $it.Prop1, - // Next = new AggregationPropertyContainer() { - // Name = "Prop2", - // Value = $it.Prop2, // int - // Next = new LastInChain() { - // Name = "Prop3", - // Value = $it.Prop3 - // } - // } - // } - // }) - List properties = CreateGroupByMemberAssignments(_groupingProperties); - - var wrapperProperty = typeof(GroupByWrapper).GetProperty(GroupByContainerProperty); - List wta = new List(); - wta.Add(Expression.Bind(wrapperProperty, AggregationPropertyContainer.CreateNextNamedPropertyContainer(properties))); - groupLambda = Expression.Lambda(Expression.MemberInit(Expression.New(typeof(GroupByWrapper)), wta), LambdaParameter); + properties.Add(new NamedPropertyExpression(Expression.Constant(propertyName), WrapConvert(BindAccessor(grpProp.Expression)))); } else { - // We do not have properties to aggregate - // .GroupBy($it => new NoGroupByWrapper()) - groupLambda = Expression.Lambda(Expression.New(this._groupByClrType), this.LambdaParameter); + var wrapperProperty = typeof(GroupByWrapper).GetProperty(GroupByContainerProperty); + List wta = new List(); + wta.Add(Expression.Bind(wrapperProperty, AggregationPropertyContainer.CreateNextNamedPropertyContainer(CreateGroupByMemberAssignments(grpProp.ChildTransformations)))); + properties.Add(new NamedPropertyExpression(Expression.Constant(propertyName), Expression.MemberInit(Expression.New(typeof(GroupByWrapper)), wta))); } - - return ExpressionHelpers.GroupBy(query, groupLambda, elementType, this._groupByClrType); } - private List CreateGroupByMemberAssignments(IEnumerable nodes) - { - var properties = new List(); - foreach (var grpProp in nodes) - { - var propertyName = grpProp.Name; - if (grpProp.Expression != null) - { - properties.Add(new NamedPropertyExpression(Expression.Constant(propertyName), WrapConvert(BindAccessor(grpProp.Expression)))); - } - else - { - var wrapperProperty = typeof(GroupByWrapper).GetProperty(GroupByContainerProperty); - List wta = new List(); - wta.Add(Expression.Bind(wrapperProperty, AggregationPropertyContainer.CreateNextNamedPropertyContainer(CreateGroupByMemberAssignments(grpProp.ChildTransformations)))); - properties.Add(new NamedPropertyExpression(Expression.Constant(propertyName), Expression.MemberInit(Expression.New(typeof(GroupByWrapper)), wta))); - } - } - - return properties; - } + return properties; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/BinderExtensions.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/BinderExtensions.cs index d2bcca4c8..963f255c8 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/BinderExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/BinderExtensions.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -13,347 +13,346 @@ using System.Reflection; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// Extension methods for Query binders +/// +public static class BinderExtensions { /// - /// Extension methods for Query binders + /// Translates an OData $filter represented by to and apply to . + /// + /// The given filter binder. + /// The given IEnumerable. + /// The filter clause. + /// The query binder context. + /// The applied result. + public static IEnumerable ApplyBind(this IFilterBinder binder, IEnumerable query, FilterClause filterClause, QueryBinderContext context) + { + if (binder == null) + { + throw Error.ArgumentNull(nameof(binder)); + } + + if (query == null) + { + throw Error.ArgumentNull(nameof(query)); + } + + if (filterClause == null) + { + throw Error.ArgumentNull(nameof(filterClause)); + } + + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + Expression filterExp = binder.BindFilter(filterClause, context); + + MethodInfo whereMethod = ExpressionHelperMethods.EnumerableWhereGeneric.MakeGenericMethod(context.ElementClrType); + return whereMethod.Invoke(null, new object[] { query, filterExp }) as IEnumerable; + } + + /// + /// Translates an OData $filter represented by to and apply to . + /// + /// The given filter binder. + /// The given queryable. + /// The filter clause. + /// The query binder context. + /// The applied result. + public static IQueryable ApplyBind(this IFilterBinder binder, IQueryable query, FilterClause filterClause, QueryBinderContext context) + { + if (binder == null) + { + throw Error.ArgumentNull(nameof(binder)); + } + + if (query == null) + { + throw Error.ArgumentNull(nameof(query)); + } + + if (filterClause == null) + { + throw Error.ArgumentNull(nameof(filterClause)); + } + + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + context.EnsureFlattenedProperties(context.CurrentParameter, query); + + Expression filterExp = binder.BindFilter(filterClause, context); + return ExpressionHelpers.Where(query, filterExp, context.ElementClrType); + } + + /// + /// Translates an OData $filter represented by to and apply to . + /// + /// The given filter binder. + /// The given source. + /// The filter clause. + /// The query binder context. + /// The applied result. + public static Expression ApplyBind(this IFilterBinder binder, Expression source, FilterClause filterClause, QueryBinderContext context) + { + if (binder == null) + { + throw Error.ArgumentNull(nameof(binder)); + } + + if (source == null) + { + throw Error.ArgumentNull(nameof(source)); + } + + if (filterClause == null) + { + throw Error.ArgumentNull(nameof(filterClause)); + } + + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + Expression filterExp = binder.BindFilter(filterClause, context); + + Type elementType = context.ElementClrType; + + MethodInfo filterMethod; + if (typeof(IQueryable).IsAssignableFrom(source.Type)) + { + filterMethod = ExpressionHelperMethods.QueryableWhereGeneric.MakeGenericMethod(elementType); + } + else + { + filterMethod = ExpressionHelperMethods.EnumerableWhereGeneric.MakeGenericMethod(elementType); + } + + return Expression.Call(filterMethod, source, filterExp); + } + + /// + /// Translates an OData $orderby represented by to and apply to . /// - public static class BinderExtensions + /// The given filter binder. + /// The given source. + /// The filter clause. + /// The query binder context. + /// The boolean value indicating whether it's ordered or not. + /// The applied result. + public static IQueryable ApplyBind(this IOrderByBinder binder, IQueryable query, OrderByClause orderByClause, QueryBinderContext context, bool alreadyOrdered) { - /// - /// Translates an OData $filter represented by to and apply to . - /// - /// The given filter binder. - /// The given IEnumerable. - /// The filter clause. - /// The query binder context. - /// The applied result. - public static IEnumerable ApplyBind(this IFilterBinder binder, IEnumerable query, FilterClause filterClause, QueryBinderContext context) - { - if (binder == null) - { - throw Error.ArgumentNull(nameof(binder)); - } - - if (query == null) - { - throw Error.ArgumentNull(nameof(query)); - } - - if (filterClause == null) - { - throw Error.ArgumentNull(nameof(filterClause)); - } - - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - Expression filterExp = binder.BindFilter(filterClause, context); - - MethodInfo whereMethod = ExpressionHelperMethods.EnumerableWhereGeneric.MakeGenericMethod(context.ElementClrType); - return whereMethod.Invoke(null, new object[] { query, filterExp }) as IEnumerable; - } - - /// - /// Translates an OData $filter represented by to and apply to . - /// - /// The given filter binder. - /// The given queryable. - /// The filter clause. - /// The query binder context. - /// The applied result. - public static IQueryable ApplyBind(this IFilterBinder binder, IQueryable query, FilterClause filterClause, QueryBinderContext context) - { - if (binder == null) - { - throw Error.ArgumentNull(nameof(binder)); - } - - if (query == null) - { - throw Error.ArgumentNull(nameof(query)); - } - - if (filterClause == null) - { - throw Error.ArgumentNull(nameof(filterClause)); - } - - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - context.EnsureFlattenedProperties(context.CurrentParameter, query); - - Expression filterExp = binder.BindFilter(filterClause, context); - return ExpressionHelpers.Where(query, filterExp, context.ElementClrType); - } - - /// - /// Translates an OData $filter represented by to and apply to . - /// - /// The given filter binder. - /// The given source. - /// The filter clause. - /// The query binder context. - /// The applied result. - public static Expression ApplyBind(this IFilterBinder binder, Expression source, FilterClause filterClause, QueryBinderContext context) - { - if (binder == null) - { - throw Error.ArgumentNull(nameof(binder)); - } - - if (source == null) - { - throw Error.ArgumentNull(nameof(source)); - } - - if (filterClause == null) - { - throw Error.ArgumentNull(nameof(filterClause)); - } - - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - Expression filterExp = binder.BindFilter(filterClause, context); - - Type elementType = context.ElementClrType; - - MethodInfo filterMethod; - if (typeof(IQueryable).IsAssignableFrom(source.Type)) - { - filterMethod = ExpressionHelperMethods.QueryableWhereGeneric.MakeGenericMethod(elementType); - } - else - { - filterMethod = ExpressionHelperMethods.EnumerableWhereGeneric.MakeGenericMethod(elementType); - } - - return Expression.Call(filterMethod, source, filterExp); - } - - /// - /// Translates an OData $orderby represented by to and apply to . - /// - /// The given filter binder. - /// The given source. - /// The filter clause. - /// The query binder context. - /// The boolean value indicating whether it's ordered or not. - /// The applied result. - public static IQueryable ApplyBind(this IOrderByBinder binder, IQueryable query, OrderByClause orderByClause, QueryBinderContext context, bool alreadyOrdered) - { - if (binder == null) - { - throw Error.ArgumentNull(nameof(binder)); - } - - if (query == null) - { - throw Error.ArgumentNull(nameof(query)); - } - - if (orderByClause == null) - { - throw Error.ArgumentNull(nameof(orderByClause)); - } - - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - OrderByBinderResult orderByResult = binder.BindOrderBy(orderByClause, context); - IQueryable querySoFar = query; - - Type elementType = context.ElementClrType; - OrderByBinderResult result = orderByResult; - do - { - LambdaExpression orderByExpression = result.OrderByExpression as LambdaExpression; - Contract.Assert(orderByExpression != null); - - OrderByDirection direction = result.Direction; - - querySoFar = ExpressionHelpers.OrderBy(querySoFar, orderByExpression, direction, elementType, alreadyOrdered); - - alreadyOrdered = true; - - result = result.ThenBy; - } - while (result != null); - - return querySoFar; - } - - /// - /// Translates an OData $orderby represented by to and apply to . - /// - /// The given filter binder. - /// The given source. - /// The filter clause. - /// The query binder context. - /// The boolean value indicating whether it's ordered or not. - /// The applied result. - public static Expression ApplyBind(this IOrderByBinder binder, Expression source, OrderByClause orderByClause, QueryBinderContext context, bool alreadyOrdered) - { - if (binder == null) - { - throw Error.ArgumentNull(nameof(binder)); - } - - if (source == null) - { - throw Error.ArgumentNull(nameof(source)); - } - - if (orderByClause == null) - { - throw Error.ArgumentNull(nameof(orderByClause)); - } - - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - OrderByBinderResult orderByResult = binder.BindOrderBy(orderByClause, context); - - Type elementType = context.ElementClrType; - OrderByBinderResult result = orderByResult; - do - { - LambdaExpression orderByExpression = result.OrderByExpression as LambdaExpression; - Contract.Assert(orderByExpression != null); - - OrderByDirection direction = result.Direction; - - source = ExpressionHelpers.OrderBy(source, orderByExpression, elementType, direction, alreadyOrdered); - - alreadyOrdered = true; - - result = result.ThenBy; - } - while (result != null); - - return source; - } - - /// - /// Translate an OData $select or $expand parse tree represented by to - /// an and applies it to an . ALso - /// - /// The built in - /// The original . - /// The OData $select or $expand parse tree. - /// An instance of the . - /// - public static IQueryable ApplyBind(this ISelectExpandBinder binder, IQueryable source, SelectExpandClause selectExpandClause, QueryBinderContext context) - { - if (binder == null) - { - throw Error.ArgumentNull(nameof(binder)); - } - - if (source == null) - { - throw Error.ArgumentNull(nameof(source)); - } - - if (selectExpandClause == null) - { - throw Error.ArgumentNull(nameof(selectExpandClause)); - } - - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - Type elementType = context.ElementClrType; - - LambdaExpression projectionLambda = binder.BindSelectExpand(selectExpandClause, context) as LambdaExpression; - - MethodInfo selectMethod = ExpressionHelperMethods.QueryableSelectGeneric.MakeGenericMethod(elementType, projectionLambda.Body.Type); - return selectMethod.Invoke(null, new object[] { source, projectionLambda }) as IQueryable; - } - - /// - /// Translate an OData $select or $expand parse tree represented by to - /// an and applies it to an . - /// - /// The built in - /// The original . - /// The OData $select or $expand parse tree. - /// An instance of the . - /// - public static object ApplyBind(this ISelectExpandBinder binder, object source, SelectExpandClause selectExpandClause, QueryBinderContext context) - { - if (binder == null) - { - throw Error.ArgumentNull(nameof(binder)); - } - - if (source == null) - { - throw Error.ArgumentNull(nameof(source)); - } - - if (selectExpandClause == null) - { - throw Error.ArgumentNull(nameof(selectExpandClause)); - } - - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - LambdaExpression projectionLambda = binder.BindSelectExpand(selectExpandClause, context) as LambdaExpression; - - return projectionLambda.Compile().DynamicInvoke(source); - } - - /// - /// Translate an OData $search parse tree represented by to - /// an and applies it to an . - /// - /// The built in - /// The original . - /// The OData $search parse tree. - /// An instance of the . - /// The applied result. - public static IQueryable ApplyBind(this ISearchBinder binder, IQueryable source, SearchClause searchClause, QueryBinderContext context) - { - if (binder == null) - { - throw Error.ArgumentNull(nameof(binder)); - } - - if (source == null) - { - throw Error.ArgumentNull(nameof(source)); - } - - if (searchClause == null) - { - throw Error.ArgumentNull(nameof(searchClause)); - } - - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - Expression searchExp = binder.BindSearch(searchClause, context); - return ExpressionHelpers.Where(source, searchExp, context.ElementClrType); + if (binder == null) + { + throw Error.ArgumentNull(nameof(binder)); + } + + if (query == null) + { + throw Error.ArgumentNull(nameof(query)); + } + + if (orderByClause == null) + { + throw Error.ArgumentNull(nameof(orderByClause)); + } + + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + OrderByBinderResult orderByResult = binder.BindOrderBy(orderByClause, context); + IQueryable querySoFar = query; + + Type elementType = context.ElementClrType; + OrderByBinderResult result = orderByResult; + do + { + LambdaExpression orderByExpression = result.OrderByExpression as LambdaExpression; + Contract.Assert(orderByExpression != null); + + OrderByDirection direction = result.Direction; + + querySoFar = ExpressionHelpers.OrderBy(querySoFar, orderByExpression, direction, elementType, alreadyOrdered); + + alreadyOrdered = true; + + result = result.ThenBy; } + while (result != null); + + return querySoFar; + } + + /// + /// Translates an OData $orderby represented by to and apply to . + /// + /// The given filter binder. + /// The given source. + /// The filter clause. + /// The query binder context. + /// The boolean value indicating whether it's ordered or not. + /// The applied result. + public static Expression ApplyBind(this IOrderByBinder binder, Expression source, OrderByClause orderByClause, QueryBinderContext context, bool alreadyOrdered) + { + if (binder == null) + { + throw Error.ArgumentNull(nameof(binder)); + } + + if (source == null) + { + throw Error.ArgumentNull(nameof(source)); + } + + if (orderByClause == null) + { + throw Error.ArgumentNull(nameof(orderByClause)); + } + + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + OrderByBinderResult orderByResult = binder.BindOrderBy(orderByClause, context); + + Type elementType = context.ElementClrType; + OrderByBinderResult result = orderByResult; + do + { + LambdaExpression orderByExpression = result.OrderByExpression as LambdaExpression; + Contract.Assert(orderByExpression != null); + + OrderByDirection direction = result.Direction; + + source = ExpressionHelpers.OrderBy(source, orderByExpression, elementType, direction, alreadyOrdered); + + alreadyOrdered = true; + + result = result.ThenBy; + } + while (result != null); + + return source; + } + + /// + /// Translate an OData $select or $expand parse tree represented by to + /// an and applies it to an . ALso + /// + /// The built in + /// The original . + /// The OData $select or $expand parse tree. + /// An instance of the . + /// + public static IQueryable ApplyBind(this ISelectExpandBinder binder, IQueryable source, SelectExpandClause selectExpandClause, QueryBinderContext context) + { + if (binder == null) + { + throw Error.ArgumentNull(nameof(binder)); + } + + if (source == null) + { + throw Error.ArgumentNull(nameof(source)); + } + + if (selectExpandClause == null) + { + throw Error.ArgumentNull(nameof(selectExpandClause)); + } + + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + Type elementType = context.ElementClrType; + + LambdaExpression projectionLambda = binder.BindSelectExpand(selectExpandClause, context) as LambdaExpression; + + MethodInfo selectMethod = ExpressionHelperMethods.QueryableSelectGeneric.MakeGenericMethod(elementType, projectionLambda.Body.Type); + return selectMethod.Invoke(null, new object[] { source, projectionLambda }) as IQueryable; + } + + /// + /// Translate an OData $select or $expand parse tree represented by to + /// an and applies it to an . + /// + /// The built in + /// The original . + /// The OData $select or $expand parse tree. + /// An instance of the . + /// + public static object ApplyBind(this ISelectExpandBinder binder, object source, SelectExpandClause selectExpandClause, QueryBinderContext context) + { + if (binder == null) + { + throw Error.ArgumentNull(nameof(binder)); + } + + if (source == null) + { + throw Error.ArgumentNull(nameof(source)); + } + + if (selectExpandClause == null) + { + throw Error.ArgumentNull(nameof(selectExpandClause)); + } + + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + LambdaExpression projectionLambda = binder.BindSelectExpand(selectExpandClause, context) as LambdaExpression; + + return projectionLambda.Compile().DynamicInvoke(source); + } + + /// + /// Translate an OData $search parse tree represented by to + /// an and applies it to an . + /// + /// The built in + /// The original . + /// The OData $search parse tree. + /// An instance of the . + /// The applied result. + public static IQueryable ApplyBind(this ISearchBinder binder, IQueryable source, SearchClause searchClause, QueryBinderContext context) + { + if (binder == null) + { + throw Error.ArgumentNull(nameof(binder)); + } + + if (source == null) + { + throw Error.ArgumentNull(nameof(source)); + } + + if (searchClause == null) + { + throw Error.ArgumentNull(nameof(searchClause)); + } + + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + Expression searchExp = binder.BindSearch(searchClause, context); + return ExpressionHelpers.Where(source, searchExp, context.ElementClrType); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/ComputeBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/ComputeBinder.cs index cab2acd09..7e726d8b4 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/ComputeBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/ComputeBinder.cs @@ -17,76 +17,75 @@ using Microsoft.OData.UriParser; using Microsoft.OData.UriParser.Aggregation; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +internal class ComputeBinder : TransformationBinderBase { - internal class ComputeBinder : TransformationBinderBase - { - private ComputeTransformationNode _transformation; - private IEdmModel _model; + private ComputeTransformationNode _transformation; + private IEdmModel _model; - internal ComputeBinder(ODataQuerySettings settings, IAssemblyResolver assembliesResolver, Type elementType, - IEdmModel model, ComputeTransformationNode transformation) - : base(settings, assembliesResolver, elementType, model) - { - Contract.Assert(transformation != null); + internal ComputeBinder(ODataQuerySettings settings, IAssemblyResolver assembliesResolver, Type elementType, + IEdmModel model, ComputeTransformationNode transformation) + : base(settings, assembliesResolver, elementType, model) + { + Contract.Assert(transformation != null); - _transformation = transformation; - _model = model; + _transformation = transformation; + _model = model; - this.ResultClrType = typeof(ComputeWrapper<>).MakeGenericType(this.ElementType); - } + this.ResultClrType = typeof(ComputeWrapper<>).MakeGenericType(this.ElementType); + } - public IQueryable Bind(IQueryable query) - { - PreprocessQuery(query); - // compute(X add Y as Z, A mul B as C) adds new properties to the output - // Should return following expression - // .Select($it => new ComputeWrapper { - // Instance = $it, - // Model = parametrized(IEdmModel), - // Container => new AggregationPropertyContainer() { - // Name = "Z", - // Value = $it.X + $it.Y, - // Next = new LastInChain() { - // Name = "C", - // Value = $it.A * $it.B - // } - // }) + public IQueryable Bind(IQueryable query) + { + PreprocessQuery(query); + // compute(X add Y as Z, A mul B as C) adds new properties to the output + // Should return following expression + // .Select($it => new ComputeWrapper { + // Instance = $it, + // Model = parametrized(IEdmModel), + // Container => new AggregationPropertyContainer() { + // Name = "Z", + // Value = $it.X + $it.Y, + // Next = new LastInChain() { + // Name = "C", + // Value = $it.A * $it.B + // } + // }) - List wrapperTypeMemberAssignments = new List(); + List wrapperTypeMemberAssignments = new List(); - // Set Instance property - var wrapperProperty = this.ResultClrType.GetProperty("Instance"); - wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, this.LambdaParameter)); - var properties = new List(); - foreach (var computeExpression in this._transformation.Expressions) - { - properties.Add(new NamedPropertyExpression(Expression.Constant(computeExpression.Alias), CreateComputeExpression(computeExpression))); - } + // Set Instance property + var wrapperProperty = this.ResultClrType.GetProperty("Instance"); + wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, this.LambdaParameter)); + var properties = new List(); + foreach (var computeExpression in this._transformation.Expressions) + { + properties.Add(new NamedPropertyExpression(Expression.Constant(computeExpression.Alias), CreateComputeExpression(computeExpression))); + } - // Initialize property 'Model' on the wrapper class. - // source = new Wrapper { Model = parameterized(a-edm-model) } - // Always parameterize as EntityFramework does not let you inject non primitive constant values (like IEdmModel). - wrapperProperty = this.ResultClrType.GetProperty("Model"); - var wrapperPropertyValueExpression = LinqParameterContainer.Parameterize(typeof(IEdmModel), _model); - wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, wrapperPropertyValueExpression)); + // Initialize property 'Model' on the wrapper class. + // source = new Wrapper { Model = parameterized(a-edm-model) } + // Always parameterize as EntityFramework does not let you inject non primitive constant values (like IEdmModel). + wrapperProperty = this.ResultClrType.GetProperty("Model"); + var wrapperPropertyValueExpression = LinqParameterContainer.Parameterize(typeof(IEdmModel), _model); + wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, wrapperPropertyValueExpression)); - // Set new compute properties - wrapperProperty = ResultClrType.GetProperty("Container"); - wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, AggregationPropertyContainer.CreateNextNamedPropertyContainer(properties))); + // Set new compute properties + wrapperProperty = ResultClrType.GetProperty("Container"); + wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, AggregationPropertyContainer.CreateNextNamedPropertyContainer(properties))); - var initilizedMember = - Expression.MemberInit(Expression.New(ResultClrType), wrapperTypeMemberAssignments); - var selectLambda = Expression.Lambda(initilizedMember, this.LambdaParameter); + var initilizedMember = + Expression.MemberInit(Expression.New(ResultClrType), wrapperTypeMemberAssignments); + var selectLambda = Expression.Lambda(initilizedMember, this.LambdaParameter); - var result = ExpressionHelpers.Select(query, selectLambda, this.ElementType); - return result; - } + var result = ExpressionHelpers.Select(query, selectLambda, this.ElementType); + return result; + } - private Expression CreateComputeExpression(ComputeExpression expression) - { - Expression body = BindAccessor(expression.Expression); - return WrapConvert(body); - } + private Expression CreateComputeExpression(ComputeExpression expression) + { + Expression body = BindAccessor(expression.Expression); + return WrapConvert(body); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderBase.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderBase.cs index bcfca94b5..b222668c0 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderBase.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderBase.cs @@ -25,1262 +25,1257 @@ using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// The base class for all expression binders. +/// +public abstract class ExpressionBinderBase { - /// - /// The base class for all expression binders. - /// - public abstract class ExpressionBinderBase - { - #region Properties - internal static readonly string DictionaryStringObjectIndexerName = typeof(Dictionary).GetDefaultMembers()[0].Name; - - internal static readonly Expression NullConstant = Expression.Constant(null); - internal static readonly Expression FalseConstant = Expression.Constant(false); - internal static readonly Expression TrueConstant = Expression.Constant(true); - - // .NET 6 adds a new overload: TryParse(ReadOnlySpan, TEnum) - // Now, with `TryParse(String, TEnum)`, there will have two versions with two parameters - // So, the previous Single() will throw exception. - internal static readonly MethodInfo EnumTryParseMethod = typeof(Enum).GetMethod("TryParse", - new[] - { - typeof(string), - Type.MakeGenericMethodParameter(0).MakeByRefType() - }); + #region Properties + internal static readonly string DictionaryStringObjectIndexerName = typeof(Dictionary).GetDefaultMembers()[0].Name; + + internal static readonly Expression NullConstant = Expression.Constant(null); + internal static readonly Expression FalseConstant = Expression.Constant(false); + internal static readonly Expression TrueConstant = Expression.Constant(true); + + // .NET 6 adds a new overload: TryParse(ReadOnlySpan, TEnum) + // Now, with `TryParse(String, TEnum)`, there will have two versions with two parameters + // So, the previous Single() will throw exception. + internal static readonly MethodInfo EnumTryParseMethod = typeof(Enum).GetMethod("TryParse", + new[] + { + typeof(string), + Type.MakeGenericMethodParameter(0).MakeByRefType() + }); - internal IEdmModel Model { get; set; } + internal IEdmModel Model { get; set; } - internal ODataQuerySettings QuerySettings { get; set; } + internal ODataQuerySettings QuerySettings { get; set; } - internal IAssemblyResolver InternalAssembliesResolver { get; set; } + internal IAssemblyResolver InternalAssembliesResolver { get; set; } - internal bool HasInstancePropertyContainer; + internal bool HasInstancePropertyContainer; - /// - /// Base query used for the binder. - /// - internal IQueryable BaseQuery; + /// + /// Base query used for the binder. + /// + internal IQueryable BaseQuery; - /// - /// Flattened list of properties from base query, for case when binder is applied for aggregated query. - /// - internal IDictionary FlattenedPropertyContainer; - #endregion + /// + /// Flattened list of properties from base query, for case when binder is applied for aggregated query. + /// + internal IDictionary FlattenedPropertyContainer; + #endregion - #region Constructors + #region Constructors - internal ExpressionBinderBase(IEdmModel model, IAssemblyResolver assembliesResolver, ODataQuerySettings querySettings) - : this(model, querySettings) - { - InternalAssembliesResolver = assembliesResolver; - } + internal ExpressionBinderBase(IEdmModel model, IAssemblyResolver assembliesResolver, ODataQuerySettings querySettings) + : this(model, querySettings) + { + InternalAssembliesResolver = assembliesResolver; + } - internal ExpressionBinderBase(IEdmModel model, ODataQuerySettings querySettings) - { - Contract.Assert(model != null); - Contract.Assert(querySettings != null); + internal ExpressionBinderBase(IEdmModel model, ODataQuerySettings querySettings) + { + Contract.Assert(model != null); + Contract.Assert(querySettings != null); - QuerySettings = querySettings; - Model = model; - } - #endregion + QuerySettings = querySettings; + Model = model; + } + #endregion + + #region Abstract properties and methods + /// + /// Binds a to create a LINQ that represents the semantics + /// of the . + /// + /// The node to bind. + /// The LINQ created. + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", + Justification = "These are simple conversion function and cannot be split up.")] + public abstract Expression Bind(QueryNode node); - #region Abstract properties and methods - /// - /// Binds a to create a LINQ that represents the semantics - /// of the . - /// - /// The node to bind. - /// The LINQ created. - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", - Justification = "These are simple conversion function and cannot be split up.")] - public abstract Expression Bind(QueryNode node); + /// + /// Gets $it parameter + /// + /// + protected abstract ParameterExpression Parameter { get; } + #endregion - /// - /// Gets $it parameter - /// - /// - protected abstract ParameterExpression Parameter { get; } - #endregion + #region Bind Node methods + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The node to bind. + /// The LINQ created. + public virtual Expression BindConstantNode(ConstantNode constantNode) + { + Contract.Assert(constantNode != null); - #region Bind Node methods - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The node to bind. - /// The LINQ created. - public virtual Expression BindConstantNode(ConstantNode constantNode) + // no need to parameterize null's as there cannot be multiple values for null. + if (constantNode.Value == null) { - Contract.Assert(constantNode != null); + return NullConstant; + } - // no need to parameterize null's as there cannot be multiple values for null. - if (constantNode.Value == null) - { - return NullConstant; - } + object value = constantNode.Value; + Type constantType = RetrieveClrTypeForConstant(constantNode.TypeReference, ref value); - object value = constantNode.Value; - Type constantType = RetrieveClrTypeForConstant(constantNode.TypeReference, ref value); + if (QuerySettings.EnableConstantParameterization) + { + return LinqParameterContainer.Parameterize(constantType, value); + } + else + { + return Expression.Constant(value, constantType); + } + } - if (QuerySettings.EnableConstantParameterization) - { - return LinqParameterContainer.Parameterize(constantType, value); - } - else - { - return Expression.Constant(value, constantType); - } + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The node to bind. + /// The LINQ created. + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", + Justification = "These are simple binding functions and cannot be split up.")] + public virtual Expression BindSingleValueFunctionCallNode(SingleValueFunctionCallNode node) + { + if (node == null) + { + throw Error.ArgumentNull(nameof(node)); } - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The node to bind. - /// The LINQ created. - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", - Justification = "These are simple binding functions and cannot be split up.")] - public virtual Expression BindSingleValueFunctionCallNode(SingleValueFunctionCallNode node) + switch (node.Name) { - if (node == null) - { - throw Error.ArgumentNull(nameof(node)); - } + case ClrCanonicalFunctions.StartswithFunctionName: + return BindStartsWith(node); - switch (node.Name) - { - case ClrCanonicalFunctions.StartswithFunctionName: - return BindStartsWith(node); + case ClrCanonicalFunctions.EndswithFunctionName: + return BindEndsWith(node); - case ClrCanonicalFunctions.EndswithFunctionName: - return BindEndsWith(node); + case ClrCanonicalFunctions.ContainsFunctionName: + return BindContains(node); - case ClrCanonicalFunctions.ContainsFunctionName: - return BindContains(node); + case ClrCanonicalFunctions.SubstringFunctionName: + return BindSubstring(node); - case ClrCanonicalFunctions.SubstringFunctionName: - return BindSubstring(node); + case ClrCanonicalFunctions.LengthFunctionName: + return BindLength(node); - case ClrCanonicalFunctions.LengthFunctionName: - return BindLength(node); + case ClrCanonicalFunctions.IndexofFunctionName: + return BindIndexOf(node); - case ClrCanonicalFunctions.IndexofFunctionName: - return BindIndexOf(node); + case ClrCanonicalFunctions.TolowerFunctionName: + return BindToLower(node); - case ClrCanonicalFunctions.TolowerFunctionName: - return BindToLower(node); + case ClrCanonicalFunctions.ToupperFunctionName: + return BindToUpper(node); - case ClrCanonicalFunctions.ToupperFunctionName: - return BindToUpper(node); + case ClrCanonicalFunctions.TrimFunctionName: + return BindTrim(node); - case ClrCanonicalFunctions.TrimFunctionName: - return BindTrim(node); + case ClrCanonicalFunctions.ConcatFunctionName: + return BindConcat(node); - case ClrCanonicalFunctions.ConcatFunctionName: - return BindConcat(node); + case ClrCanonicalFunctions.MatchesPatternFunctionName: + return BindMatchesPattern(node); - case ClrCanonicalFunctions.MatchesPatternFunctionName: - return BindMatchesPattern(node); + case ClrCanonicalFunctions.YearFunctionName: + case ClrCanonicalFunctions.MonthFunctionName: + case ClrCanonicalFunctions.DayFunctionName: + return BindDateRelatedProperty(node); // Date & DateTime & DateTimeOffset - case ClrCanonicalFunctions.YearFunctionName: - case ClrCanonicalFunctions.MonthFunctionName: - case ClrCanonicalFunctions.DayFunctionName: - return BindDateRelatedProperty(node); // Date & DateTime & DateTimeOffset + case ClrCanonicalFunctions.HourFunctionName: + case ClrCanonicalFunctions.MinuteFunctionName: + case ClrCanonicalFunctions.SecondFunctionName: + return BindTimeRelatedProperty(node); // TimeOfDay & DateTime & DateTimeOffset - case ClrCanonicalFunctions.HourFunctionName: - case ClrCanonicalFunctions.MinuteFunctionName: - case ClrCanonicalFunctions.SecondFunctionName: - return BindTimeRelatedProperty(node); // TimeOfDay & DateTime & DateTimeOffset + case ClrCanonicalFunctions.FractionalSecondsFunctionName: + return BindFractionalSeconds(node); - case ClrCanonicalFunctions.FractionalSecondsFunctionName: - return BindFractionalSeconds(node); + case ClrCanonicalFunctions.RoundFunctionName: + return BindRound(node); - case ClrCanonicalFunctions.RoundFunctionName: - return BindRound(node); + case ClrCanonicalFunctions.FloorFunctionName: + return BindFloor(node); - case ClrCanonicalFunctions.FloorFunctionName: - return BindFloor(node); + case ClrCanonicalFunctions.CeilingFunctionName: + return BindCeiling(node); - case ClrCanonicalFunctions.CeilingFunctionName: - return BindCeiling(node); + case ClrCanonicalFunctions.CastFunctionName: + return BindCastSingleValue(node); - case ClrCanonicalFunctions.CastFunctionName: - return BindCastSingleValue(node); + case ClrCanonicalFunctions.IsofFunctionName: + return BindIsOf(node); - case ClrCanonicalFunctions.IsofFunctionName: - return BindIsOf(node); + case ClrCanonicalFunctions.DateFunctionName: + return BindDate(node); - case ClrCanonicalFunctions.DateFunctionName: - return BindDate(node); + case ClrCanonicalFunctions.TimeFunctionName: + return BindTime(node); - case ClrCanonicalFunctions.TimeFunctionName: - return BindTime(node); + case ClrCanonicalFunctions.NowFunctionName: + return BindNow(node); - case ClrCanonicalFunctions.NowFunctionName: - return BindNow(node); + default: + // Get Expression of custom binded method. + Expression expression = BindCustomMethodExpressionOrNull(node); + if (expression != null) + { + return expression; + } - default: - // Get Expression of custom binded method. - Expression expression = BindCustomMethodExpressionOrNull(node); - if (expression != null) - { - return expression; - } + throw new NotImplementedException(Error.Format(SRResources.ODataFunctionNotSupported, node.Name)); + } + } - throw new NotImplementedException(Error.Format(SRResources.ODataFunctionNotSupported, node.Name)); - } + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The node to bind. + /// The LINQ created. + public virtual Expression BindCollectionConstantNode(CollectionConstantNode node) + { + if (node == null) + { + throw Error.ArgumentNull(nameof(node)); } - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The node to bind. - /// The LINQ created. - public virtual Expression BindCollectionConstantNode(CollectionConstantNode node) + // It's fine if the collection is empty; the returned value will be an empty list. + ConstantNode firstNode = node.Collection.FirstOrDefault(); + object value = null; + if (firstNode != null) { - if (node == null) - { - throw Error.ArgumentNull(nameof(node)); - } + value = firstNode.Value; + } + + Type constantType = RetrieveClrTypeForConstant(node.ItemType, ref value); + Type nullableConstantType = node.ItemType.IsNullable && constantType.IsValueType && Nullable.GetUnderlyingType(constantType) == null + ? typeof(Nullable<>).MakeGenericType(constantType) + : constantType; + Type listType = typeof(List<>).MakeGenericType(nullableConstantType); + IList castedList = Activator.CreateInstance(listType) as IList; - // It's fine if the collection is empty; the returned value will be an empty list. - ConstantNode firstNode = node.Collection.FirstOrDefault(); - object value = null; - if (firstNode != null) + // Getting a LINQ expression to dynamically cast each item in the Collection during runtime is tricky, + // so using a foreach loop and doing an implicit cast from object to the CLR type of ItemType. + foreach (ConstantNode item in node.Collection) + { + object member; + if (item.Value == null) { - value = firstNode.Value; + member = null; } - - Type constantType = RetrieveClrTypeForConstant(node.ItemType, ref value); - Type nullableConstantType = node.ItemType.IsNullable && constantType.IsValueType && Nullable.GetUnderlyingType(constantType) == null - ? typeof(Nullable<>).MakeGenericType(constantType) - : constantType; - Type listType = typeof(List<>).MakeGenericType(nullableConstantType); - IList castedList = Activator.CreateInstance(listType) as IList; - - // Getting a LINQ expression to dynamically cast each item in the Collection during runtime is tricky, - // so using a foreach loop and doing an implicit cast from object to the CLR type of ItemType. - foreach (ConstantNode item in node.Collection) + else if (constantType.IsEnum) { - object member; - if (item.Value == null) - { - member = null; - } - else if (constantType.IsEnum) - { - member = EnumDeserializationHelpers.ConvertEnumValue(item.Value, constantType); - } - else - { - member = item.Value; - } - - castedList.Add(member); + member = EnumDeserializationHelpers.ConvertEnumValue(item.Value, constantType); } - - if (QuerySettings.EnableConstantParameterization) + else { - return LinqParameterContainer.Parameterize(listType, castedList); + member = item.Value; } - return Expression.Constant(castedList, listType); + castedList.Add(member); } - private Expression BindIsOf(SingleValueFunctionCallNode node) + if (QuerySettings.EnableConstantParameterization) { - Contract.Assert(ClrCanonicalFunctions.IsofFunctionName == node.Name); + return LinqParameterContainer.Parameterize(listType, castedList); + } - Expression[] arguments = BindArguments(node.Parameters); + return Expression.Constant(castedList, listType); + } - // Edm.Boolean isof(type) or - // Edm.Boolean isof(expression,type) - Contract.Assert(arguments.Length == 1 || arguments.Length == 2); + private Expression BindIsOf(SingleValueFunctionCallNode node) + { + Contract.Assert(ClrCanonicalFunctions.IsofFunctionName == node.Name); - Expression source = arguments.Length == 1 ? this.Parameter : arguments[0]; - if (source == NullConstant) - { - return FalseConstant; - } + Expression[] arguments = BindArguments(node.Parameters); - string typeName = (string)((ConstantNode)node.Parameters.Last()).Value; + // Edm.Boolean isof(type) or + // Edm.Boolean isof(expression,type) + Contract.Assert(arguments.Length == 1 || arguments.Length == 2); - IEdmType edmType = Model.FindType(typeName); - Type clrType = null; - if (edmType != null) - { - // bool nullable = source.Type.IsNullable(); - IEdmTypeReference edmTypeReference = edmType.ToEdmTypeReference(false); - clrType = Model.GetClrType(edmTypeReference); - } + Expression source = arguments.Length == 1 ? this.Parameter : arguments[0]; + if (source == NullConstant) + { + return FalseConstant; + } - if (clrType == null) - { - return FalseConstant; - } + string typeName = (string)((ConstantNode)node.Parameters.Last()).Value; + + IEdmType edmType = Model.FindType(typeName); + Type clrType = null; + if (edmType != null) + { + // bool nullable = source.Type.IsNullable(); + IEdmTypeReference edmTypeReference = edmType.ToEdmTypeReference(false); + clrType = Model.GetClrType(edmTypeReference); + } + + if (clrType == null) + { + return FalseConstant; + } - bool isSourcePrimitiveOrEnum = Model.GetEdmPrimitiveTypeReference(source.Type) != null || - TypeHelper.IsEnum(source.Type); + bool isSourcePrimitiveOrEnum = Model.GetEdmPrimitiveTypeReference(source.Type) != null || + TypeHelper.IsEnum(source.Type); - bool isTargetPrimitiveOrEnum = Model.GetEdmPrimitiveTypeReference(clrType) != null || - TypeHelper.IsEnum(clrType); + bool isTargetPrimitiveOrEnum = Model.GetEdmPrimitiveTypeReference(clrType) != null || + TypeHelper.IsEnum(clrType); - if (isSourcePrimitiveOrEnum && isTargetPrimitiveOrEnum) + if (isSourcePrimitiveOrEnum && isTargetPrimitiveOrEnum) + { + if (TypeHelper.IsNullable(source.Type)) { - if (TypeHelper.IsNullable(source.Type)) - { - clrType = TypeHelper.ToNullable(clrType); - } + clrType = TypeHelper.ToNullable(clrType); } - - // Be caution: Type method of LINQ to Entities only supports entity type. - return Expression.Condition(Expression.TypeIs(source, clrType), TrueConstant, FalseConstant); } - private Expression BindCeiling(SingleValueFunctionCallNode node) - { - Contract.Assert("ceiling" == node.Name); + // Be caution: Type method of LINQ to Entities only supports entity type. + return Expression.Condition(Expression.TypeIs(source, clrType), TrueConstant, FalseConstant); + } - Expression[] arguments = BindArguments(node.Parameters); + private Expression BindCeiling(SingleValueFunctionCallNode node) + { + Contract.Assert("ceiling" == node.Name); - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDoubleOrDecimal(arguments[0].Type)); + Expression[] arguments = BindArguments(node.Parameters); - MethodInfo ceiling = ExpressionBinderHelper.IsType(arguments[0].Type) - ? ClrCanonicalFunctions.CeilingOfDouble - : ClrCanonicalFunctions.CeilingOfDecimal; - return ExpressionBinderHelper.MakeFunctionCall(ceiling, QuerySettings, arguments); - } + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDoubleOrDecimal(arguments[0].Type)); - private Expression BindFloor(SingleValueFunctionCallNode node) - { - Contract.Assert("floor" == node.Name); + MethodInfo ceiling = ExpressionBinderHelper.IsType(arguments[0].Type) + ? ClrCanonicalFunctions.CeilingOfDouble + : ClrCanonicalFunctions.CeilingOfDecimal; + return ExpressionBinderHelper.MakeFunctionCall(ceiling, QuerySettings, arguments); + } - Expression[] arguments = BindArguments(node.Parameters); + private Expression BindFloor(SingleValueFunctionCallNode node) + { + Contract.Assert("floor" == node.Name); - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDoubleOrDecimal(arguments[0].Type)); + Expression[] arguments = BindArguments(node.Parameters); - MethodInfo floor = ExpressionBinderHelper.IsType(arguments[0].Type) - ? ClrCanonicalFunctions.FloorOfDouble - : ClrCanonicalFunctions.FloorOfDecimal; - return ExpressionBinderHelper.MakeFunctionCall(floor, QuerySettings, arguments); - } + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDoubleOrDecimal(arguments[0].Type)); - private Expression BindRound(SingleValueFunctionCallNode node) - { - Contract.Assert("round" == node.Name); + MethodInfo floor = ExpressionBinderHelper.IsType(arguments[0].Type) + ? ClrCanonicalFunctions.FloorOfDouble + : ClrCanonicalFunctions.FloorOfDecimal; + return ExpressionBinderHelper.MakeFunctionCall(floor, QuerySettings, arguments); + } - Expression[] arguments = BindArguments(node.Parameters); + private Expression BindRound(SingleValueFunctionCallNode node) + { + Contract.Assert("round" == node.Name); - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDoubleOrDecimal(arguments[0].Type)); + Expression[] arguments = BindArguments(node.Parameters); - MethodInfo round = ExpressionBinderHelper.IsType(arguments[0].Type) - ? ClrCanonicalFunctions.RoundOfDouble - : ClrCanonicalFunctions.RoundOfDecimal; - return ExpressionBinderHelper.MakeFunctionCall(round, QuerySettings, arguments); - } + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDoubleOrDecimal(arguments[0].Type)); - private Expression BindDate(SingleValueFunctionCallNode node) - { - Contract.Assert("date" == node.Name); + MethodInfo round = ExpressionBinderHelper.IsType(arguments[0].Type) + ? ClrCanonicalFunctions.RoundOfDouble + : ClrCanonicalFunctions.RoundOfDecimal; + return ExpressionBinderHelper.MakeFunctionCall(round, QuerySettings, arguments); + } - Expression[] arguments = BindArguments(node.Parameters); + private Expression BindDate(SingleValueFunctionCallNode node) + { + Contract.Assert("date" == node.Name); - // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateOrOffset(arguments[0].Type)); + Expression[] arguments = BindArguments(node.Parameters); - // EF doesn't support new Date(int, int, int), also doesn't support other property access, for example DateTime.Date. - // Therefore, we just return the source (DateTime or DateTimeOffset). - return arguments[0]; - } + // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateOrOffset(arguments[0].Type)); - private Expression BindNow(SingleValueFunctionCallNode node) - { - Contract.Assert("now" == node.Name); + // EF doesn't support new Date(int, int, int), also doesn't support other property access, for example DateTime.Date. + // Therefore, we just return the source (DateTime or DateTimeOffset). + return arguments[0]; + } - // Function Now() does not take any arguments. - Expression[] arguments = BindArguments(node.Parameters); - Contract.Assert(arguments.Length == 0); + private Expression BindNow(SingleValueFunctionCallNode node) + { + Contract.Assert("now" == node.Name); - return Expression.Property(null, typeof(DateTimeOffset), "UtcNow"); - } + // Function Now() does not take any arguments. + Expression[] arguments = BindArguments(node.Parameters); + Contract.Assert(arguments.Length == 0); - private Expression BindTime(SingleValueFunctionCallNode node) - { - Contract.Assert("time" == node.Name); + return Expression.Property(null, typeof(DateTimeOffset), "UtcNow"); + } - Expression[] arguments = BindArguments(node.Parameters); + private Expression BindTime(SingleValueFunctionCallNode node) + { + Contract.Assert("time" == node.Name); - // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateOrOffset(arguments[0].Type)); + Expression[] arguments = BindArguments(node.Parameters); - // EF doesn't support new TimeOfDay(int, int, int, int), also doesn't support other property access, for example DateTimeOffset.DateTime. - // Therefore, we just return the source (DateTime or DateTimeOffset). - return arguments[0]; - } + // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateOrOffset(arguments[0].Type)); - private Expression BindFractionalSeconds(SingleValueFunctionCallNode node) + // EF doesn't support new TimeOfDay(int, int, int, int), also doesn't support other property access, for example DateTimeOffset.DateTime. + // Therefore, we just return the source (DateTime or DateTimeOffset). + return arguments[0]; + } + + private Expression BindFractionalSeconds(SingleValueFunctionCallNode node) + { + Contract.Assert("fractionalseconds" == node.Name); + + Expression[] arguments = BindArguments(node.Parameters); + Contract.Assert(arguments.Length == 1 && (ExpressionBinderHelper.IsTimeRelated(arguments[0].Type))); + + // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. + Expression parameter = arguments[0]; + + PropertyInfo property; + if (ExpressionBinderHelper.IsTimeOfDay(parameter.Type)) + { + property = ClrCanonicalFunctions.TimeOfDayProperties[ClrCanonicalFunctions.MillisecondFunctionName]; + } + else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) { - Contract.Assert("fractionalseconds" == node.Name); + property = ClrCanonicalFunctions.DateTimeProperties[ClrCanonicalFunctions.MillisecondFunctionName]; + } + else if (ExpressionBinderHelper.IsTimeSpan(parameter.Type)) + { + property = ClrCanonicalFunctions.TimeSpanProperties[ClrCanonicalFunctions.MillisecondFunctionName]; + } + else + { + property = ClrCanonicalFunctions.DateTimeOffsetProperties[ClrCanonicalFunctions.MillisecondFunctionName]; + } - Expression[] arguments = BindArguments(node.Parameters); - Contract.Assert(arguments.Length == 1 && (ExpressionBinderHelper.IsTimeRelated(arguments[0].Type))); + // Millisecond + Expression milliSecond = ExpressionBinderHelper.MakePropertyAccess(property, parameter, QuerySettings); + Expression decimalMilliSecond = Expression.Convert(milliSecond, typeof(decimal)); + Expression fractionalSeconds = Expression.Divide(decimalMilliSecond, Expression.Constant(1000m, typeof(decimal))); - // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. - Expression parameter = arguments[0]; + return ExpressionBinderHelper.CreateFunctionCallWithNullPropagation(fractionalSeconds, arguments, QuerySettings); + } - PropertyInfo property; - if (ExpressionBinderHelper.IsTimeOfDay(parameter.Type)) - { - property = ClrCanonicalFunctions.TimeOfDayProperties[ClrCanonicalFunctions.MillisecondFunctionName]; - } - else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) - { - property = ClrCanonicalFunctions.DateTimeProperties[ClrCanonicalFunctions.MillisecondFunctionName]; - } - else if (ExpressionBinderHelper.IsTimeSpan(parameter.Type)) - { - property = ClrCanonicalFunctions.TimeSpanProperties[ClrCanonicalFunctions.MillisecondFunctionName]; - } - else - { - property = ClrCanonicalFunctions.DateTimeOffsetProperties[ClrCanonicalFunctions.MillisecondFunctionName]; - } + private Expression BindDateRelatedProperty(SingleValueFunctionCallNode node) + { + Expression[] arguments = BindArguments(node.Parameters); + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateRelated(arguments[0].Type)); - // Millisecond - Expression milliSecond = ExpressionBinderHelper.MakePropertyAccess(property, parameter, QuerySettings); - Expression decimalMilliSecond = Expression.Convert(milliSecond, typeof(decimal)); - Expression fractionalSeconds = Expression.Divide(decimalMilliSecond, Expression.Constant(1000m, typeof(decimal))); + // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. + Expression parameter = arguments[0]; - return ExpressionBinderHelper.CreateFunctionCallWithNullPropagation(fractionalSeconds, arguments, QuerySettings); + PropertyInfo property; + if (ExpressionBinderHelper.IsDate(parameter.Type)) + { + Contract.Assert(ClrCanonicalFunctions.DateProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.DateProperties[node.Name]; } - - private Expression BindDateRelatedProperty(SingleValueFunctionCallNode node) + else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) + { + Contract.Assert(ClrCanonicalFunctions.DateTimeProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.DateTimeProperties[node.Name]; + } + else { - Expression[] arguments = BindArguments(node.Parameters); - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateRelated(arguments[0].Type)); + Contract.Assert(ClrCanonicalFunctions.DateTimeOffsetProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.DateTimeOffsetProperties[node.Name]; + } - // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. - Expression parameter = arguments[0]; + return ExpressionBinderHelper.MakeFunctionCall(property, QuerySettings, parameter); + } - PropertyInfo property; - if (ExpressionBinderHelper.IsDate(parameter.Type)) - { - Contract.Assert(ClrCanonicalFunctions.DateProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.DateProperties[node.Name]; - } - else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) - { - Contract.Assert(ClrCanonicalFunctions.DateTimeProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.DateTimeProperties[node.Name]; - } - else - { - Contract.Assert(ClrCanonicalFunctions.DateTimeOffsetProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.DateTimeOffsetProperties[node.Name]; - } + private Expression BindTimeRelatedProperty(SingleValueFunctionCallNode node) + { + Expression[] arguments = BindArguments(node.Parameters); + Contract.Assert(arguments.Length == 1 && (ExpressionBinderHelper.IsTimeRelated(arguments[0].Type))); - return ExpressionBinderHelper.MakeFunctionCall(property, QuerySettings, parameter); - } + // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. + Expression parameter = arguments[0]; - private Expression BindTimeRelatedProperty(SingleValueFunctionCallNode node) + PropertyInfo property; + if (ExpressionBinderHelper.IsTimeOfDay(parameter.Type)) + { + Contract.Assert(ClrCanonicalFunctions.TimeOfDayProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.TimeOfDayProperties[node.Name]; + } + else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) + { + Contract.Assert(ClrCanonicalFunctions.DateTimeProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.DateTimeProperties[node.Name]; + } + else if (ExpressionBinderHelper.IsTimeSpan(parameter.Type)) + { + Contract.Assert(ClrCanonicalFunctions.TimeSpanProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.TimeSpanProperties[node.Name]; + } + else { - Expression[] arguments = BindArguments(node.Parameters); - Contract.Assert(arguments.Length == 1 && (ExpressionBinderHelper.IsTimeRelated(arguments[0].Type))); + Contract.Assert(ClrCanonicalFunctions.DateTimeOffsetProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.DateTimeOffsetProperties[node.Name]; + } - // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. - Expression parameter = arguments[0]; + return ExpressionBinderHelper.MakeFunctionCall(property, QuerySettings, parameter); + } - PropertyInfo property; - if (ExpressionBinderHelper.IsTimeOfDay(parameter.Type)) - { - Contract.Assert(ClrCanonicalFunctions.TimeOfDayProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.TimeOfDayProperties[node.Name]; - } - else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) - { - Contract.Assert(ClrCanonicalFunctions.DateTimeProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.DateTimeProperties[node.Name]; - } - else if (ExpressionBinderHelper.IsTimeSpan(parameter.Type)) - { - Contract.Assert(ClrCanonicalFunctions.TimeSpanProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.TimeSpanProperties[node.Name]; - } - else - { - Contract.Assert(ClrCanonicalFunctions.DateTimeOffsetProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.DateTimeOffsetProperties[node.Name]; - } + private Expression BindConcat(SingleValueFunctionCallNode node) + { + Contract.Assert("concat" == node.Name); - return ExpressionBinderHelper.MakeFunctionCall(property, QuerySettings, parameter); - } + Expression[] arguments = BindArguments(node.Parameters); + ValidateAllStringArguments(node.Name, arguments); - private Expression BindConcat(SingleValueFunctionCallNode node) - { - Contract.Assert("concat" == node.Name); + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); - Expression[] arguments = BindArguments(node.Parameters); - ValidateAllStringArguments(node.Name, arguments); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Concat, QuerySettings, arguments); + } - Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + private Expression BindMatchesPattern(SingleValueFunctionCallNode node) + { + Contract.Assert("matchesPattern" == node.Name); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Concat, QuerySettings, arguments); - } + Expression[] arguments = BindArguments(node.Parameters); + ValidateAllStringArguments(node.Name, arguments); - private Expression BindMatchesPattern(SingleValueFunctionCallNode node) - { - Contract.Assert("matchesPattern" == node.Name); + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); - Expression[] arguments = BindArguments(node.Parameters); - ValidateAllStringArguments(node.Name, arguments); + //add argument that must be ECMAScript compatible regex + arguments = new[] { arguments[0], arguments[1], Expression.Constant(RegexOptions.ECMAScript) }; - Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.MatchesMattern, QuerySettings, arguments); + } - //add argument that must be ECMAScript compatible regex - arguments = new[] { arguments[0], arguments[1], Expression.Constant(RegexOptions.ECMAScript) }; + private Expression BindTrim(SingleValueFunctionCallNode node) + { + Contract.Assert("trim" == node.Name); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.MatchesMattern, QuerySettings, arguments); - } + Expression[] arguments = BindArguments(node.Parameters); + ValidateAllStringArguments(node.Name, arguments); - private Expression BindTrim(SingleValueFunctionCallNode node) - { - Contract.Assert("trim" == node.Name); + Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); - Expression[] arguments = BindArguments(node.Parameters); - ValidateAllStringArguments(node.Name, arguments); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Trim, QuerySettings, arguments); + } - Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); + private Expression BindToUpper(SingleValueFunctionCallNode node) + { + Contract.Assert("toupper" == node.Name); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Trim, QuerySettings, arguments); - } + Expression[] arguments = BindArguments(node.Parameters); + ValidateAllStringArguments(node.Name, arguments); - private Expression BindToUpper(SingleValueFunctionCallNode node) - { - Contract.Assert("toupper" == node.Name); + Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); - Expression[] arguments = BindArguments(node.Parameters); - ValidateAllStringArguments(node.Name, arguments); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.ToUpper, QuerySettings, arguments); + } - Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); + private Expression BindToLower(SingleValueFunctionCallNode node) + { + Contract.Assert("tolower" == node.Name); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.ToUpper, QuerySettings, arguments); - } + Expression[] arguments = BindArguments(node.Parameters); + ValidateAllStringArguments(node.Name, arguments); - private Expression BindToLower(SingleValueFunctionCallNode node) - { - Contract.Assert("tolower" == node.Name); + Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); - Expression[] arguments = BindArguments(node.Parameters); - ValidateAllStringArguments(node.Name, arguments); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.ToLower, QuerySettings, arguments); + } - Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); + private Expression BindIndexOf(SingleValueFunctionCallNode node) + { + Contract.Assert("indexof" == node.Name); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.ToLower, QuerySettings, arguments); - } + Expression[] arguments = BindArguments(node.Parameters); + ValidateAllStringArguments(node.Name, arguments); - private Expression BindIndexOf(SingleValueFunctionCallNode node) - { - Contract.Assert("indexof" == node.Name); + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); - Expression[] arguments = BindArguments(node.Parameters); - ValidateAllStringArguments(node.Name, arguments); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.IndexOf, QuerySettings, arguments); + } - Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + private Expression BindSubstring(SingleValueFunctionCallNode node) + { + Contract.Assert("substring" == node.Name); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.IndexOf, QuerySettings, arguments); + Expression[] arguments = BindArguments(node.Parameters); + if (arguments[0].Type != typeof(string)) + { + throw new ODataException(Error.Format(SRResources.FunctionNotSupportedOnEnum, node.Name)); } - private Expression BindSubstring(SingleValueFunctionCallNode node) + Expression functionCall; + if (arguments.Length == 2) { - Contract.Assert("substring" == node.Name); + Contract.Assert(ExpressionBinderHelper.IsInteger(arguments[1].Type)); - Expression[] arguments = BindArguments(node.Parameters); - if (arguments[0].Type != typeof(string)) + // When null propagation is allowed, we use a safe version of String.Substring(int). + // But for providers that would not recognize custom expressions like this, we map + // directly to String.Substring(int) + if (QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) { - throw new ODataException(Error.Format(SRResources.FunctionNotSupportedOnEnum, node.Name)); + // Safe function is static and takes string "this" as first argument + functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStartNoThrow, QuerySettings, arguments); } - - Expression functionCall; - if (arguments.Length == 2) + else { - Contract.Assert(ExpressionBinderHelper.IsInteger(arguments[1].Type)); + functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStart, QuerySettings, arguments); + } + } + else + { + // arguments.Length == 3 implies String.Substring(int, int) + Contract.Assert(arguments.Length == 3 && ExpressionBinderHelper.IsInteger(arguments[1].Type) && ExpressionBinderHelper.IsInteger(arguments[2].Type)); - // When null propagation is allowed, we use a safe version of String.Substring(int). - // But for providers that would not recognize custom expressions like this, we map - // directly to String.Substring(int) - if (QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) - { - // Safe function is static and takes string "this" as first argument - functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStartNoThrow, QuerySettings, arguments); - } - else - { - functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStart, QuerySettings, arguments); - } + // When null propagation is allowed, we use a safe version of String.Substring(int, int). + // But for providers that would not recognize custom expressions like this, we map + // directly to String.Substring(int, int) + if (QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) + { + // Safe function is static and takes string "this" as first argument + functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStartAndLengthNoThrow, QuerySettings, arguments); } else { - // arguments.Length == 3 implies String.Substring(int, int) - Contract.Assert(arguments.Length == 3 && ExpressionBinderHelper.IsInteger(arguments[1].Type) && ExpressionBinderHelper.IsInteger(arguments[2].Type)); - - // When null propagation is allowed, we use a safe version of String.Substring(int, int). - // But for providers that would not recognize custom expressions like this, we map - // directly to String.Substring(int, int) - if (QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) - { - // Safe function is static and takes string "this" as first argument - functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStartAndLengthNoThrow, QuerySettings, arguments); - } - else - { - functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStartAndLength, QuerySettings, arguments); - } + functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStartAndLength, QuerySettings, arguments); } - - return functionCall; } - private Expression BindLength(SingleValueFunctionCallNode node) - { - Contract.Assert("length" == node.Name); - - Expression[] arguments = BindArguments(node.Parameters); - ValidateAllStringArguments(node.Name, arguments); + return functionCall; + } - Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); + private Expression BindLength(SingleValueFunctionCallNode node) + { + Contract.Assert("length" == node.Name); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Length, QuerySettings, arguments); - } + Expression[] arguments = BindArguments(node.Parameters); + ValidateAllStringArguments(node.Name, arguments); - private Expression BindContains(SingleValueFunctionCallNode node) - { - Contract.Assert("contains" == node.Name); + Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); - Expression[] arguments = BindArguments(node.Parameters); - ValidateAllStringArguments(node.Name, arguments); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Length, QuerySettings, arguments); + } - Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + private Expression BindContains(SingleValueFunctionCallNode node) + { + Contract.Assert("contains" == node.Name); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Contains, QuerySettings, arguments[0], arguments[1]); - } + Expression[] arguments = BindArguments(node.Parameters); + ValidateAllStringArguments(node.Name, arguments); - private Expression BindStartsWith(SingleValueFunctionCallNode node) - { - Contract.Assert("startswith" == node.Name); + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); - Expression[] arguments = BindArguments(node.Parameters); - ValidateAllStringArguments(node.Name, arguments); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Contains, QuerySettings, arguments[0], arguments[1]); + } - Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + private Expression BindStartsWith(SingleValueFunctionCallNode node) + { + Contract.Assert("startswith" == node.Name); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.StartsWith, QuerySettings, arguments); - } + Expression[] arguments = BindArguments(node.Parameters); + ValidateAllStringArguments(node.Name, arguments); - private Expression BindEndsWith(SingleValueFunctionCallNode node) - { - Contract.Assert("endswith" == node.Name); + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); - Expression[] arguments = BindArguments(node.Parameters); - ValidateAllStringArguments(node.Name, arguments); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.StartsWith, QuerySettings, arguments); + } - Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + private Expression BindEndsWith(SingleValueFunctionCallNode node) + { + Contract.Assert("endswith" == node.Name); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.EndsWith, QuerySettings, arguments); - } + Expression[] arguments = BindArguments(node.Parameters); + ValidateAllStringArguments(node.Name, arguments); - private Expression BindCustomMethodExpressionOrNull(SingleValueFunctionCallNode node) - { - Expression[] arguments = BindArguments(node.Parameters); - IEnumerable methodArgumentsType = arguments.Select(argument => argument.Type); + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); - // Search for custom method info that are binded to the node name - MethodInfo methodInfo; - if (UriFunctionsBinder.TryGetMethodInfo(node.Name, methodArgumentsType, out methodInfo)) - { - return ExpressionBinderHelper.MakeFunctionCall(methodInfo, QuerySettings, arguments); - } + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.EndsWith, QuerySettings, arguments); + } - return null; - } + private Expression BindCustomMethodExpressionOrNull(SingleValueFunctionCallNode node) + { + Expression[] arguments = BindArguments(node.Parameters); + IEnumerable methodArgumentsType = arguments.Select(argument => argument.Type); - private Expression BindCastSingleValue(SingleValueFunctionCallNode node) + // Search for custom method info that are binded to the node name + MethodInfo methodInfo; + if (UriFunctionsBinder.TryGetMethodInfo(node.Name, methodArgumentsType, out methodInfo)) { - Contract.Assert(ClrCanonicalFunctions.CastFunctionName == node.Name); + return ExpressionBinderHelper.MakeFunctionCall(methodInfo, QuerySettings, arguments); + } - Expression[] arguments = BindArguments(node.Parameters); - Contract.Assert(arguments.Length == 1 || arguments.Length == 2); + return null; + } - Expression source = arguments.Length == 1 ? this.Parameter : arguments[0]; - string targetTypeName = (string)((ConstantNode)node.Parameters.Last()).Value; - IEdmType targetEdmType = Model.FindType(targetTypeName); - Type targetClrType = null; + private Expression BindCastSingleValue(SingleValueFunctionCallNode node) + { + Contract.Assert(ClrCanonicalFunctions.CastFunctionName == node.Name); - if (targetEdmType != null) - { - IEdmTypeReference targetEdmTypeReference = targetEdmType.ToEdmTypeReference(false); - targetClrType = Model.GetClrType(targetEdmTypeReference); + Expression[] arguments = BindArguments(node.Parameters); + Contract.Assert(arguments.Length == 1 || arguments.Length == 2); - if (source != NullConstant) - { - if (source.Type == targetClrType) - { - return source; - } + Expression source = arguments.Length == 1 ? this.Parameter : arguments[0]; + string targetTypeName = (string)((ConstantNode)node.Parameters.Last()).Value; + IEdmType targetEdmType = Model.FindType(targetTypeName); + Type targetClrType = null; - if ((!targetEdmTypeReference.IsPrimitive() && !targetEdmTypeReference.IsEnum()) || - (Model.GetEdmPrimitiveTypeReference(source.Type) == null && !TypeHelper.IsEnum(source.Type))) - { - // Cast fails and return null. - return NullConstant; - } - } - } + if (targetEdmType != null) + { + IEdmTypeReference targetEdmTypeReference = targetEdmType.ToEdmTypeReference(false); + targetClrType = Model.GetClrType(targetEdmTypeReference); - if (targetClrType == null || source == NullConstant) + if (source != NullConstant) { - return NullConstant; - } - - if (targetClrType == typeof(string)) - { - return ExpressionBinderHelper.BindCastToStringType(source); - } - else if (TypeHelper.IsEnum(targetClrType)) - { - return BindCastToEnumType(source.Type, targetClrType, node.Parameters.First(), arguments.Length); - } - else - { - if (TypeHelper.IsNullable(source.Type) && !TypeHelper.IsNullable(targetClrType)) + if (source.Type == targetClrType) { - // Make the target Clr type nullable to avoid failure while casting - // nullable source, whose value may be null, to a non-nullable type. - // For example: cast(NullableInt32Property,Edm.Int64) - // The target Clr type should be Nullable rather than Int64. - targetClrType = typeof(Nullable<>).MakeGenericType(targetClrType); + return source; } - try - { - return Expression.Convert(source, targetClrType); - } - catch (InvalidOperationException) + if ((!targetEdmTypeReference.IsPrimitive() && !targetEdmTypeReference.IsEnum()) || + (Model.GetEdmPrimitiveTypeReference(source.Type) == null && !TypeHelper.IsEnum(source.Type))) { // Cast fails and return null. return NullConstant; } } } - #endregion - #region Private helper methods - private static void ValidateAllStringArguments(string functionName, Expression[] arguments) + if (targetClrType == null || source == NullConstant) { - if (arguments.Any(arg => arg.Type != typeof(string))) - { - throw new ODataException(Error.Format(SRResources.FunctionNotSupportedOnEnum, functionName)); - } + return NullConstant; } - /// - /// Recognize $it.Source where $it is FlatteningWrapper - /// Using that do avoid wrapping it redundant into Null propagation - /// - /// - /// - private bool IsFlatteningSource(Expression source) + if (targetClrType == typeof(string)) { - var member = source as MemberExpression; - return member != null - && this.Parameter.Type.IsGenericType - && this.Parameter.Type.GetGenericTypeDefinition() == typeof(FlatteningWrapper<>) - && member.Expression == this.Parameter; + return ExpressionBinderHelper.BindCastToStringType(source); } - - private static MethodCallExpression SkipFilters(MethodCallExpression expression) + else if (TypeHelper.IsEnum(targetClrType)) { - while (expression.Method.Name == "Where") + return BindCastToEnumType(source.Type, targetClrType, node.Parameters.First(), arguments.Length); + } + else + { + if (TypeHelper.IsNullable(source.Type) && !TypeHelper.IsNullable(targetClrType)) { - expression = expression.Arguments.FirstOrDefault() as MethodCallExpression; + // Make the target Clr type nullable to avoid failure while casting + // nullable source, whose value may be null, to a non-nullable type. + // For example: cast(NullableInt32Property,Edm.Int64) + // The target Clr type should be Nullable rather than Int64. + targetClrType = typeof(Nullable<>).MakeGenericType(targetClrType); } - return expression; + try + { + return Expression.Convert(source, targetClrType); + } + catch (InvalidOperationException) + { + // Cast fails and return null. + return NullConstant; + } } + } + #endregion - private static void CollectContainerAssignments(Expression source, MethodCallExpression expression, Dictionary result) + #region Private helper methods + private static void ValidateAllStringArguments(string functionName, Expression[] arguments) + { + if (arguments.Any(arg => arg.Type != typeof(string))) { - CollectAssigments(result, Expression.Property(source, "GroupByContainer"), ExtractContainerExpression(expression.Arguments.FirstOrDefault() as MethodCallExpression, "GroupByContainer")); - CollectAssigments(result, Expression.Property(source, "Container"), ExtractContainerExpression(expression, "Container")); + throw new ODataException(Error.Format(SRResources.FunctionNotSupportedOnEnum, functionName)); } + } + + /// + /// Recognize $it.Source where $it is FlatteningWrapper + /// Using that do avoid wrapping it redundant into Null propagation + /// + /// + /// + private bool IsFlatteningSource(Expression source) + { + var member = source as MemberExpression; + return member != null + && this.Parameter.Type.IsGenericType + && this.Parameter.Type.GetGenericTypeDefinition() == typeof(FlatteningWrapper<>) + && member.Expression == this.Parameter; + } - private static void CollectAssigments(IDictionary flattenPropertyContainer, Expression source, MemberInitExpression expression, string prefix = null) + private static MethodCallExpression SkipFilters(MethodCallExpression expression) + { + while (expression.Method.Name == "Where") { - if (expression == null) - { - return; - } + expression = expression.Arguments.FirstOrDefault() as MethodCallExpression; + } - string nameToAdd = null; - Type resultType = null; - MemberInitExpression nextExpression = null; - Expression nestedExpression = null; - foreach (var expr in expression.Bindings.OfType()) - { - var initExpr = expr.Expression as MemberInitExpression; - if (initExpr != null && expr.Member.Name == "Next") - { - nextExpression = initExpr; - } - else if (expr.Member.Name == "Name") - { - nameToAdd = (expr.Expression as ConstantExpression).Value as string; - } - else if (expr.Member.Name == "Value" || expr.Member.Name == "NestedValue") - { - resultType = expr.Expression.Type; - if (resultType == typeof(object) && expr.Expression.NodeType == ExpressionType.Convert) - { - resultType = ((UnaryExpression)expr.Expression).Operand.Type; - } + return expression; + } - if (typeof(GroupByWrapper).IsAssignableFrom(resultType)) - { - nestedExpression = expr.Expression; - } - } - } + private static void CollectContainerAssignments(Expression source, MethodCallExpression expression, Dictionary result) + { + CollectAssigments(result, Expression.Property(source, "GroupByContainer"), ExtractContainerExpression(expression.Arguments.FirstOrDefault() as MethodCallExpression, "GroupByContainer")); + CollectAssigments(result, Expression.Property(source, "Container"), ExtractContainerExpression(expression, "Container")); + } - if (prefix != null) - { - nameToAdd = prefix + "\\" + nameToAdd; - } + private static void CollectAssigments(IDictionary flattenPropertyContainer, Expression source, MemberInitExpression expression, string prefix = null) + { + if (expression == null) + { + return; + } - if (typeof(GroupByWrapper).IsAssignableFrom(resultType)) + string nameToAdd = null; + Type resultType = null; + MemberInitExpression nextExpression = null; + Expression nestedExpression = null; + foreach (var expr in expression.Bindings.OfType()) + { + var initExpr = expr.Expression as MemberInitExpression; + if (initExpr != null && expr.Member.Name == "Next") { - flattenPropertyContainer.Add(nameToAdd, Expression.Property(source, "NestedValue")); + nextExpression = initExpr; } - else + else if (expr.Member.Name == "Name") { - flattenPropertyContainer.Add(nameToAdd, Expression.Convert(Expression.Property(source, "Value"), resultType)); + nameToAdd = (expr.Expression as ConstantExpression).Value as string; } - - if (nextExpression != null) + else if (expr.Member.Name == "Value" || expr.Member.Name == "NestedValue") { - CollectAssigments(flattenPropertyContainer, Expression.Property(source, "Next"), nextExpression, prefix); - } + resultType = expr.Expression.Type; + if (resultType == typeof(object) && expr.Expression.NodeType == ExpressionType.Convert) + { + resultType = ((UnaryExpression)expr.Expression).Operand.Type; + } - if (nestedExpression != null) - { - var nestedAccessor = ((nestedExpression as MemberInitExpression).Bindings.First() as MemberAssignment).Expression as MemberInitExpression; - var newSource = Expression.Property(Expression.Property(source, "NestedValue"), "GroupByContainer"); - CollectAssigments(flattenPropertyContainer, newSource, nestedAccessor, nameToAdd); + if (typeof(GroupByWrapper).IsAssignableFrom(resultType)) + { + nestedExpression = expr.Expression; + } } } - private static MemberInitExpression ExtractContainerExpression(MethodCallExpression expression, string containerName) + if (prefix != null) { - if (expression == null || expression.Arguments.Count < 2) - { - return null; - } + nameToAdd = prefix + "\\" + nameToAdd; + } - var memberInitExpression = ((expression.Arguments[1] as UnaryExpression).Operand as LambdaExpression).Body as MemberInitExpression; - if (memberInitExpression != null) - { - var containerAssigment = memberInitExpression.Bindings.FirstOrDefault(m => m.Member.Name == containerName) as MemberAssignment; - if (containerAssigment != null) - { - return containerAssigment.Expression as MemberInitExpression; - } - } - return null; + if (typeof(GroupByWrapper).IsAssignableFrom(resultType)) + { + flattenPropertyContainer.Add(nameToAdd, Expression.Property(source, "NestedValue")); + } + else + { + flattenPropertyContainer.Add(nameToAdd, Expression.Convert(Expression.Property(source, "Value"), resultType)); } - #endregion - #region Protected methods - /// - /// Bind function arguments - /// - /// - /// - protected Expression[] BindArguments(IEnumerable nodes) + if (nextExpression != null) { - return nodes.OfType().Select(n => Bind(n)).ToArray(); + CollectAssigments(flattenPropertyContainer, Expression.Property(source, "Next"), nextExpression, prefix); } - /// - /// Gets property for dynamic properties dictionary. - /// - /// - /// Returns CLR property for dynamic properties container. - protected PropertyInfo GetDynamicPropertyContainer(SingleValueOpenPropertyAccessNode openNode) + if (nestedExpression != null) { - if (openNode == null) - { - throw Error.ArgumentNull(nameof(openNode)); - } + var nestedAccessor = ((nestedExpression as MemberInitExpression).Bindings.First() as MemberAssignment).Expression as MemberInitExpression; + var newSource = Expression.Property(Expression.Property(source, "NestedValue"), "GroupByContainer"); + CollectAssigments(flattenPropertyContainer, newSource, nestedAccessor, nameToAdd); + } + } - IEdmStructuredType edmStructuredType; - IEdmTypeReference edmTypeReference = openNode.Source.TypeReference; - if (edmTypeReference.IsEntity()) - { - edmStructuredType = edmTypeReference.AsEntity().EntityDefinition(); - } - else if (edmTypeReference.IsComplex()) - { - edmStructuredType = edmTypeReference.AsComplex().ComplexDefinition(); - } - else + private static MemberInitExpression ExtractContainerExpression(MethodCallExpression expression, string containerName) + { + if (expression == null || expression.Arguments.Count < 2) + { + return null; + } + + var memberInitExpression = ((expression.Arguments[1] as UnaryExpression).Operand as LambdaExpression).Body as MemberInitExpression; + if (memberInitExpression != null) + { + var containerAssigment = memberInitExpression.Bindings.FirstOrDefault(m => m.Member.Name == containerName) as MemberAssignment; + if (containerAssigment != null) { - throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, openNode.Kind, typeof(ExpressionBinderBase).Name); + return containerAssigment.Expression as MemberInitExpression; } - - return Model.GetDynamicPropertyDictionary(edmStructuredType); } + return null; + } + #endregion - /// - /// Analyze previous query and extract grouped properties. - /// - /// - protected void EnsureFlattenedPropertyContainer(ParameterExpression source) + #region Protected methods + /// + /// Bind function arguments + /// + /// + /// + protected Expression[] BindArguments(IEnumerable nodes) + { + return nodes.OfType().Select(n => Bind(n)).ToArray(); + } + + /// + /// Gets property for dynamic properties dictionary. + /// + /// + /// Returns CLR property for dynamic properties container. + protected PropertyInfo GetDynamicPropertyContainer(SingleValueOpenPropertyAccessNode openNode) + { + if (openNode == null) { - if (this.BaseQuery != null) - { - this.HasInstancePropertyContainer = this.BaseQuery.ElementType.IsGenericType - && this.BaseQuery.ElementType.GetGenericTypeDefinition() == typeof(ComputeWrapper<>); + throw Error.ArgumentNull(nameof(openNode)); + } - this.FlattenedPropertyContainer = this.FlattenedPropertyContainer ?? GetFlattenedProperties(source); - } + IEdmStructuredType edmStructuredType; + IEdmTypeReference edmTypeReference = openNode.Source.TypeReference; + if (edmTypeReference.IsEntity()) + { + edmStructuredType = edmTypeReference.AsEntity().EntityDefinition(); + } + else if (edmTypeReference.IsComplex()) + { + edmStructuredType = edmTypeReference.AsComplex().ComplexDefinition(); } + else + { + throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, openNode.Kind, typeof(ExpressionBinderBase).Name); + } + + return Model.GetDynamicPropertyDictionary(edmStructuredType); + } - /// - /// Gets expression for property from previously aggregated query - /// - /// - /// Returns null if no aggregations were used so far - protected Expression GetFlattenedPropertyExpression(string propertyPath) + /// + /// Analyze previous query and extract grouped properties. + /// + /// + protected void EnsureFlattenedPropertyContainer(ParameterExpression source) + { + if (this.BaseQuery != null) { - if (FlattenedPropertyContainer == null) - { - return null; - } + this.HasInstancePropertyContainer = this.BaseQuery.ElementType.IsGenericType + && this.BaseQuery.ElementType.GetGenericTypeDefinition() == typeof(ComputeWrapper<>); - Expression expression; - if (FlattenedPropertyContainer.TryGetValue(propertyPath, out expression)) - { - return expression; - } + this.FlattenedPropertyContainer = this.FlattenedPropertyContainer ?? GetFlattenedProperties(source); + } + } - if (this.HasInstancePropertyContainer) - { - return null; - } + /// + /// Gets expression for property from previously aggregated query + /// + /// + /// Returns null if no aggregations were used so far + protected Expression GetFlattenedPropertyExpression(string propertyPath) + { + if (FlattenedPropertyContainer == null) + { + return null; + } - throw new ODataException(Error.Format(SRResources.PropertyOrPathWasRemovedFromContext, propertyPath)); + Expression expression; + if (FlattenedPropertyContainer.TryGetValue(propertyPath, out expression)) + { + return expression; } - #endregion - #region Internal methods - internal string GetFullPropertyPath(SingleValueNode node) + if (this.HasInstancePropertyContainer) { - string path = null; - SingleValueNode parent = null; - switch (node.Kind) - { - case QueryNodeKind.SingleComplexNode: - var complexNode = (SingleComplexNode)node; - path = complexNode.Property.Name; - parent = complexNode.Source; - break; - case QueryNodeKind.SingleValuePropertyAccess: - var propertyNode = ((SingleValuePropertyAccessNode)node); - path = propertyNode.Property.Name; - parent = propertyNode.Source; - break; - case QueryNodeKind.SingleNavigationNode: - var navNode = ((SingleNavigationNode)node); - path = navNode.NavigationProperty.Name; - parent = navNode.Source; - break; - } + return null; + } - if (parent != null) - { - var parentPath = GetFullPropertyPath(parent); - if (parentPath != null) - { - path = parentPath + "\\" + path; - } - } + throw new ODataException(Error.Format(SRResources.PropertyOrPathWasRemovedFromContext, propertyPath)); + } + #endregion - return path; + #region Internal methods + internal string GetFullPropertyPath(SingleValueNode node) + { + string path = null; + SingleValueNode parent = null; + switch (node.Kind) + { + case QueryNodeKind.SingleComplexNode: + var complexNode = (SingleComplexNode)node; + path = complexNode.Property.Name; + parent = complexNode.Source; + break; + case QueryNodeKind.SingleValuePropertyAccess: + var propertyNode = ((SingleValuePropertyAccessNode)node); + path = propertyNode.Property.Name; + parent = propertyNode.Source; + break; + case QueryNodeKind.SingleNavigationNode: + var navNode = ((SingleNavigationNode)node); + path = navNode.NavigationProperty.Name; + parent = navNode.Source; + break; } - internal Expression CreatePropertyAccessExpression(Expression source, IEdmProperty property, string propertyPath = null) + if (parent != null) { - string propertyName = Model.GetClrPropertyName(property); - propertyPath = propertyPath ?? propertyName; - if (QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && ExpressionBinderHelper.IsNullable(source.Type) && - source != this.Parameter && - !IsFlatteningSource(source)) + var parentPath = GetFullPropertyPath(parent); + if (parentPath != null) { - Expression cleanSource = ExpressionBinderHelper.RemoveInnerNullPropagation(source, QuerySettings); - Expression propertyAccessExpression = null; - propertyAccessExpression = GetFlattenedPropertyExpression(propertyPath) ?? Expression.Property(cleanSource, propertyName); - - // source.property => source == null ? null : [CastToNullable]RemoveInnerNullPropagation(source).property - // Notice that we are checking if source is null already. so we can safely remove any null checks when doing source.Property - - Expression ifFalse = ExpressionBinderHelper.ToNullable(ConvertNonStandardPrimitives(propertyAccessExpression)); - return - Expression.Condition( - test: Expression.Equal(source, NullConstant), - ifTrue: Expression.Constant(null, ifFalse.Type), - ifFalse: ifFalse); - } - else - { - return GetFlattenedPropertyExpression(propertyPath) - ?? ConvertNonStandardPrimitives(GetPropertyExpression(source, (this.HasInstancePropertyContainer && !propertyPath.Contains("\\", StringComparison.Ordinal) ? "Instance\\" : String.Empty) + propertyName)); + path = parentPath + "\\" + path; } } - internal static Expression GetPropertyExpression(Expression source, string propertyPath) + return path; + } + + internal Expression CreatePropertyAccessExpression(Expression source, IEdmProperty property, string propertyPath = null) + { + string propertyName = Model.GetClrPropertyName(property); + propertyPath = propertyPath ?? propertyName; + if (QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && ExpressionBinderHelper.IsNullable(source.Type) && + source != this.Parameter && + !IsFlatteningSource(source)) { - string[] propertyNameParts = propertyPath.Split('\\'); - Expression propertyValue = source; - foreach (var propertyName in propertyNameParts) - { - propertyValue = Expression.Property(propertyValue, propertyName); - } - return propertyValue; + Expression cleanSource = ExpressionBinderHelper.RemoveInnerNullPropagation(source, QuerySettings); + Expression propertyAccessExpression = null; + propertyAccessExpression = GetFlattenedPropertyExpression(propertyPath) ?? Expression.Property(cleanSource, propertyName); + + // source.property => source == null ? null : [CastToNullable]RemoveInnerNullPropagation(source).property + // Notice that we are checking if source is null already. so we can safely remove any null checks when doing source.Property + + Expression ifFalse = ExpressionBinderHelper.ToNullable(ConvertNonStandardPrimitives(propertyAccessExpression)); + return + Expression.Condition( + test: Expression.Equal(source, NullConstant), + ifTrue: Expression.Constant(null, ifFalse.Type), + ifFalse: ifFalse); } + else + { + return GetFlattenedPropertyExpression(propertyPath) + ?? ConvertNonStandardPrimitives(GetPropertyExpression(source, (this.HasInstancePropertyContainer && !propertyPath.Contains("\\", StringComparison.Ordinal) ? "Instance\\" : String.Empty) + propertyName)); + } + } - // If the expression is of non-standard edm primitive type (like uint), convert the expression to its standard edm type. - // Also, note that only expressions generated for ushort, uint and ulong can be understood by linq2sql and EF. - // The rest (char, char[], Binary) would cause issues with linq2sql and EF. - internal Expression ConvertNonStandardPrimitives(Expression source) + internal static Expression GetPropertyExpression(Expression source, string propertyPath) + { + string[] propertyNameParts = propertyPath.Split('\\'); + Expression propertyValue = source; + foreach (var propertyName in propertyNameParts) { - bool isNonstandardEdmPrimitive; - Type conversionType = Model.IsNonstandardEdmPrimitive(source.Type, out isNonstandardEdmPrimitive); + propertyValue = Expression.Property(propertyValue, propertyName); + } + return propertyValue; + } - if (isNonstandardEdmPrimitive) - { - Type sourceType = TypeHelper.GetUnderlyingTypeOrSelf(source.Type); + // If the expression is of non-standard edm primitive type (like uint), convert the expression to its standard edm type. + // Also, note that only expressions generated for ushort, uint and ulong can be understood by linq2sql and EF. + // The rest (char, char[], Binary) would cause issues with linq2sql and EF. + internal Expression ConvertNonStandardPrimitives(Expression source) + { + bool isNonstandardEdmPrimitive; + Type conversionType = Model.IsNonstandardEdmPrimitive(source.Type, out isNonstandardEdmPrimitive); - Contract.Assert(sourceType != conversionType); + if (isNonstandardEdmPrimitive) + { + Type sourceType = TypeHelper.GetUnderlyingTypeOrSelf(source.Type); - Expression convertedExpression = null; + Contract.Assert(sourceType != conversionType); - if (TypeHelper.IsEnum(sourceType)) - { - // we handle enum conversions ourselves - convertedExpression = source; - } - else + Expression convertedExpression = null; + + if (TypeHelper.IsEnum(sourceType)) + { + // we handle enum conversions ourselves + convertedExpression = source; + } + else + { + switch (Type.GetTypeCode(sourceType)) { - switch (Type.GetTypeCode(sourceType)) - { - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - convertedExpression = Expression.Convert(ExpressionBinderHelper.ExtractValueFromNullableExpression(source), conversionType); - break; - - case TypeCode.Char: - convertedExpression = Expression.Call(ExpressionBinderHelper.ExtractValueFromNullableExpression(source), "ToString", typeArguments: null, arguments: null); - break; - - case TypeCode.DateTime: - convertedExpression = source; - break; - - case TypeCode.Object: - if (sourceType == typeof(char[])) - { - convertedExpression = Expression.New(typeof(string).GetConstructor(new[] { typeof(char[]) }), source); - } - else if (sourceType == typeof(XElement)) - { - convertedExpression = Expression.Call(source, "ToString", typeArguments: null, arguments: null); - } + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + convertedExpression = Expression.Convert(ExpressionBinderHelper.ExtractValueFromNullableExpression(source), conversionType); + break; + + case TypeCode.Char: + convertedExpression = Expression.Call(ExpressionBinderHelper.ExtractValueFromNullableExpression(source), "ToString", typeArguments: null, arguments: null); + break; + + case TypeCode.DateTime: + convertedExpression = source; + break; + + case TypeCode.Object: + if (sourceType == typeof(char[])) + { + convertedExpression = Expression.New(typeof(string).GetConstructor(new[] { typeof(char[]) }), source); + } + else if (sourceType == typeof(XElement)) + { + convertedExpression = Expression.Call(source, "ToString", typeArguments: null, arguments: null); + } #if NETFX // System.Data.Linq.Binary is only supported in the AspNet version. - else if (sourceType == typeof(Binary)) - { - convertedExpression = Expression.Call(source, "ToArray", typeArguments: null, arguments: null); - } + else if (sourceType == typeof(Binary)) + { + convertedExpression = Expression.Call(source, "ToArray", typeArguments: null, arguments: null); + } #endif - break; - - default: - Contract.Assert(false, Error.Format("missing non-standard type support for {0}", sourceType.Name)); - break; - } - } + break; - if (QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && ExpressionBinderHelper.IsNullable(source.Type)) - { - // source == null ? null : source - return Expression.Condition( - ExpressionBinderHelper.CheckForNull(source), - ifTrue: Expression.Constant(null, ExpressionBinderHelper.ToNullable(convertedExpression.Type)), - ifFalse: ExpressionBinderHelper.ToNullable(convertedExpression)); - } - else - { - return convertedExpression; + default: + Contract.Assert(false, Error.Format("missing non-standard type support for {0}", sourceType.Name)); + break; } } - return source; - } - - internal Expression CreateConvertExpression(ConvertNode convertNode, Expression source) - { - Type conversionType = Model.GetClrType(convertNode.TypeReference, InternalAssembliesResolver); - - if (conversionType == typeof(bool?) && source.Type == typeof(bool)) - { - // we handle null propagation ourselves. So, if converting from bool to Nullable ignore. - return source; - } - else if (conversionType == typeof(Date?) && - (source.Type == typeof(DateTimeOffset?) || source.Type == typeof(DateTime?))) - { - return source; - } - if ((conversionType == typeof(TimeOfDay?) && source.Type == typeof(TimeOfDay)) || - ((conversionType == typeof(Date?) && source.Type == typeof(Date)))) - { - return source; - } - else if (conversionType == typeof(TimeOfDay?) && - (source.Type == typeof(DateTimeOffset?) || source.Type == typeof(DateTime?) || source.Type == typeof(TimeSpan?))) + if (QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && ExpressionBinderHelper.IsNullable(source.Type)) { - return source; + // source == null ? null : source + return Expression.Condition( + ExpressionBinderHelper.CheckForNull(source), + ifTrue: Expression.Constant(null, ExpressionBinderHelper.ToNullable(convertedExpression.Type)), + ifFalse: ExpressionBinderHelper.ToNullable(convertedExpression)); } - else if (ExpressionBinderHelper.IsDateAndTimeRelated(conversionType) && ExpressionBinderHelper.IsDateAndTimeRelated(source.Type)) + else { - return source; + return convertedExpression; } - else if (source == NullConstant) + } + + return source; + } + + internal Expression CreateConvertExpression(ConvertNode convertNode, Expression source) + { + Type conversionType = Model.GetClrType(convertNode.TypeReference, InternalAssembliesResolver); + + if (conversionType == typeof(bool?) && source.Type == typeof(bool)) + { + // we handle null propagation ourselves. So, if converting from bool to Nullable ignore. + return source; + } + else if (conversionType == typeof(Date?) && + (source.Type == typeof(DateTimeOffset?) || source.Type == typeof(DateTime?))) + { + return source; + } + if ((conversionType == typeof(TimeOfDay?) && source.Type == typeof(TimeOfDay)) || + ((conversionType == typeof(Date?) && source.Type == typeof(Date)))) + { + return source; + } + else if (conversionType == typeof(TimeOfDay?) && + (source.Type == typeof(DateTimeOffset?) || source.Type == typeof(DateTime?) || source.Type == typeof(TimeSpan?))) + { + return source; + } + else if (ExpressionBinderHelper.IsDateAndTimeRelated(conversionType) && ExpressionBinderHelper.IsDateAndTimeRelated(source.Type)) + { + return source; + } + else if (source == NullConstant) + { + return source; + } + else + { + if (TypeHelper.IsEnum(source.Type)) { + // we handle enum conversions ourselves return source; } else { - if (TypeHelper.IsEnum(source.Type)) + // if a cast is from Nullable to Non-Nullable we need to check if source is null + if (QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True + && ExpressionBinderHelper.IsNullable(source.Type) && !ExpressionBinderHelper.IsNullable(conversionType)) { - // we handle enum conversions ourselves - return source; + // source == null ? null : source.Value + return + Expression.Condition( + test: ExpressionBinderHelper.CheckForNull(source), + ifTrue: Expression.Constant(null, ExpressionBinderHelper.ToNullable(conversionType)), + ifFalse: Expression.Convert(ExpressionBinderHelper.ExtractValueFromNullableExpression(source), ExpressionBinderHelper.ToNullable(conversionType))); } else { - // if a cast is from Nullable to Non-Nullable we need to check if source is null - if (QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True - && ExpressionBinderHelper.IsNullable(source.Type) && !ExpressionBinderHelper.IsNullable(conversionType)) - { - // source == null ? null : source.Value - return - Expression.Condition( - test: ExpressionBinderHelper.CheckForNull(source), - ifTrue: Expression.Constant(null, ExpressionBinderHelper.ToNullable(conversionType)), - ifFalse: Expression.Convert(ExpressionBinderHelper.ExtractValueFromNullableExpression(source), ExpressionBinderHelper.ToNullable(conversionType))); - } - else - { - return Expression.Convert(source, conversionType); - } + return Expression.Convert(source, conversionType); } } } + } - internal IDictionary GetFlattenedProperties(ParameterExpression source) + internal IDictionary GetFlattenedProperties(ParameterExpression source) + { + if (this.BaseQuery == null) { - if (this.BaseQuery == null) - { - return null; - } + return null; + } - if (!typeof(GroupByWrapper).IsAssignableFrom(BaseQuery.ElementType)) - { - return null; - } + if (!typeof(GroupByWrapper).IsAssignableFrom(BaseQuery.ElementType)) + { + return null; + } - var expression = BaseQuery.Expression as MethodCallExpression; - if (expression == null) - { - return null; - } + var expression = BaseQuery.Expression as MethodCallExpression; + if (expression == null) + { + return null; + } - // After $apply we could have other clauses, like $filter, $orderby etc. - // Skip of filter expressions - expression = SkipFilters(expression); + // After $apply we could have other clauses, like $filter, $orderby etc. + // Skip of filter expressions + expression = SkipFilters(expression); - if (expression == null) - { - return null; - } + if (expression == null) + { + return null; + } - var result = new Dictionary(); - CollectContainerAssignments(source, expression, result); - if (this.HasInstancePropertyContainer) + var result = new Dictionary(); + CollectContainerAssignments(source, expression, result); + if (this.HasInstancePropertyContainer) + { + var instanceProperty = Expression.Property(source, "Instance"); + if (typeof(DynamicTypeWrapper).IsAssignableFrom(instanceProperty.Type)) { - var instanceProperty = Expression.Property(source, "Instance"); - if (typeof(DynamicTypeWrapper).IsAssignableFrom(instanceProperty.Type)) + var computeExpression = expression.Arguments.FirstOrDefault() as MethodCallExpression; + computeExpression = SkipFilters(computeExpression); + if (computeExpression != null) { - var computeExpression = expression.Arguments.FirstOrDefault() as MethodCallExpression; - computeExpression = SkipFilters(computeExpression); - if (computeExpression != null) - { - CollectContainerAssignments(instanceProperty, computeExpression, result); - } + CollectContainerAssignments(instanceProperty, computeExpression, result); } } - - return result; } - internal Type RetrieveClrTypeForConstant(IEdmTypeReference edmTypeReference, ref object value) - { - Type constantType = Model.GetClrType(edmTypeReference, InternalAssembliesResolver); + return result; + } - if (value != null && edmTypeReference != null && edmTypeReference.IsEnum()) - { - ODataEnumValue odataEnumValue = (ODataEnumValue)value; - string strValue = odataEnumValue.Value; - Contract.Assert(strValue != null); + internal Type RetrieveClrTypeForConstant(IEdmTypeReference edmTypeReference, ref object value) + { + Type constantType = Model.GetClrType(edmTypeReference, InternalAssembliesResolver); - constantType = Nullable.GetUnderlyingType(constantType) ?? constantType; + if (value != null && edmTypeReference != null && edmTypeReference.IsEnum()) + { + ODataEnumValue odataEnumValue = (ODataEnumValue)value; + string strValue = odataEnumValue.Value; + Contract.Assert(strValue != null); + + constantType = Nullable.GetUnderlyingType(constantType) ?? constantType; - IEdmEnumType enumType = edmTypeReference.AsEnum().EnumDefinition(); - ClrEnumMemberAnnotation memberMapAnnotation = Model.GetClrEnumMemberAnnotation(enumType); - if (memberMapAnnotation != null) + IEdmEnumType enumType = edmTypeReference.AsEnum().EnumDefinition(); + ClrEnumMemberAnnotation memberMapAnnotation = Model.GetClrEnumMemberAnnotation(enumType); + if (memberMapAnnotation != null) + { + IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name == strValue); + if (enumMember == null) { - IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name == strValue); - if (enumMember == null) - { - enumMember = enumType.Members.FirstOrDefault(m => m.Value.ToString() == strValue); - } + enumMember = enumType.Members.FirstOrDefault(m => m.Value.ToString() == strValue); + } - if (enumMember != null) + if (enumMember != null) + { + Enum clrMember = memberMapAnnotation.GetClrEnumMember(enumMember); + if (clrMember != null) { - Enum clrMember = memberMapAnnotation.GetClrEnumMember(enumMember); - if (clrMember != null) - { - value = clrMember; - } - else - { - throw new ODataException(Error.Format(SRResources.CannotGetEnumClrMember, enumMember.Name)); - } + value = clrMember; } else { - value = Enum.Parse(constantType, strValue); + throw new ODataException(Error.Format(SRResources.CannotGetEnumClrMember, enumMember.Name)); } } else @@ -1288,50 +1283,54 @@ internal Type RetrieveClrTypeForConstant(IEdmTypeReference edmTypeReference, ref value = Enum.Parse(constantType, strValue); } } - - if (edmTypeReference != null && - edmTypeReference.IsNullable && - (edmTypeReference.IsDate() || edmTypeReference.IsTimeOfDay())) + else { - constantType = Nullable.GetUnderlyingType(constantType) ?? constantType; + value = Enum.Parse(constantType, strValue); } + } - return constantType; + if (edmTypeReference != null && + edmTypeReference.IsNullable && + (edmTypeReference.IsDate() || edmTypeReference.IsTimeOfDay())) + { + constantType = Nullable.GetUnderlyingType(constantType) ?? constantType; } - internal Expression BindCastToEnumType(Type sourceType, Type targetClrType, QueryNode firstParameter, int parameterLength) + return constantType; + } + + internal Expression BindCastToEnumType(Type sourceType, Type targetClrType, QueryNode firstParameter, int parameterLength) + { + Type enumType = TypeHelper.GetUnderlyingTypeOrSelf(targetClrType); + ConstantNode sourceNode = firstParameter as ConstantNode; + + if (parameterLength == 1 || sourceNode == null || sourceType != typeof(string)) + { + // We only support to cast Enumeration type from constant string now, + // because LINQ to Entities does not recognize the method Enum.TryParse. + return NullConstant; + } + else { - Type enumType = TypeHelper.GetUnderlyingTypeOrSelf(targetClrType); - ConstantNode sourceNode = firstParameter as ConstantNode; + object[] parameters = new[] { sourceNode.Value, Enum.ToObject(enumType, 0) }; + bool isSuccessful = (bool)EnumTryParseMethod.MakeGenericMethod(enumType).Invoke(null, parameters); - if (parameterLength == 1 || sourceNode == null || sourceType != typeof(string)) + if (isSuccessful) { - // We only support to cast Enumeration type from constant string now, - // because LINQ to Entities does not recognize the method Enum.TryParse. - return NullConstant; - } - else - { - object[] parameters = new[] { sourceNode.Value, Enum.ToObject(enumType, 0) }; - bool isSuccessful = (bool)EnumTryParseMethod.MakeGenericMethod(enumType).Invoke(null, parameters); - - if (isSuccessful) + if (QuerySettings.EnableConstantParameterization) { - if (QuerySettings.EnableConstantParameterization) - { - return LinqParameterContainer.Parameterize(targetClrType, parameters[1]); - } - else - { - return Expression.Constant(parameters[1], targetClrType); - } + return LinqParameterContainer.Parameterize(targetClrType, parameters[1]); } else { - return NullConstant; + return Expression.Constant(parameters[1], targetClrType); } } + else + { + return NullConstant; + } } - #endregion } + #endregion } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderHelper.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderHelper.cs index c88132fbd..060e178f1 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderHelper.cs @@ -20,724 +20,723 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions -{ - /// - /// The helper class for all expression binders. - /// - internal static class ExpressionBinderHelper - { - private static readonly MethodInfo StringCompareMethodInfo = typeof(string).GetMethod("Compare", new[] { typeof(string), typeof(string) }); - private static readonly MethodInfo GuidCompareMethodInfo = typeof(Guid).GetMethod("CompareTo", new[] { typeof(Guid) }); - - private static readonly Expression NullConstant = Expression.Constant(null); - private static readonly Expression FalseConstant = Expression.Constant(false); - private static readonly Expression TrueConstant = Expression.Constant(true); - private static readonly Expression ZeroConstant = Expression.Constant(0); - - private static readonly Dictionary BinaryOperatorMapping = new Dictionary - { - { BinaryOperatorKind.Add, ExpressionType.Add }, - { BinaryOperatorKind.And, ExpressionType.AndAlso }, - { BinaryOperatorKind.Divide, ExpressionType.Divide }, - { BinaryOperatorKind.Equal, ExpressionType.Equal }, - { BinaryOperatorKind.GreaterThan, ExpressionType.GreaterThan }, - { BinaryOperatorKind.GreaterThanOrEqual, ExpressionType.GreaterThanOrEqual }, - { BinaryOperatorKind.LessThan, ExpressionType.LessThan }, - { BinaryOperatorKind.LessThanOrEqual, ExpressionType.LessThanOrEqual }, - { BinaryOperatorKind.Modulo, ExpressionType.Modulo }, - { BinaryOperatorKind.Multiply, ExpressionType.Multiply }, - { BinaryOperatorKind.NotEqual, ExpressionType.NotEqual }, - { BinaryOperatorKind.Or, ExpressionType.OrElse }, - { BinaryOperatorKind.Subtract, ExpressionType.Subtract }, - }; - - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "These are simple conversion function and cannot be split up.")] - public static Expression CreateBinaryExpression(BinaryOperatorKind binaryOperator, Expression left, Expression right, bool liftToNull, ODataQuerySettings querySettings) - { - ExpressionType binaryExpressionType; - - // When comparing an enum to a string, parse the string, convert both to the enum underlying type, and compare the values - // When comparing an enum to an enum with the same type, convert both to the underlying type, and compare the values - Type leftUnderlyingType = Nullable.GetUnderlyingType(left.Type) ?? left.Type; - Type rightUnderlyingType = Nullable.GetUnderlyingType(right.Type) ?? right.Type; - - // Convert to integers unless Enum type is required - if ((TypeHelper.IsEnum(leftUnderlyingType) || TypeHelper.IsEnum(rightUnderlyingType)) && binaryOperator != BinaryOperatorKind.Has) - { - Type enumType = TypeHelper.IsEnum(leftUnderlyingType) ? leftUnderlyingType : rightUnderlyingType; - Type enumUnderlyingType = Enum.GetUnderlyingType(enumType); - left = ConvertToEnumUnderlyingType(left, enumType, enumUnderlyingType); - right = ConvertToEnumUnderlyingType(right, enumType, enumUnderlyingType); - } +namespace Microsoft.AspNetCore.OData.Query.Expressions; - if (leftUnderlyingType == typeof(DateTime) && rightUnderlyingType == typeof(DateTimeOffset)) - { - right = DateTimeOffsetToDateTime(right, querySettings.TimeZone, querySettings); - } - else if (rightUnderlyingType == typeof(DateTime) && leftUnderlyingType == typeof(DateTimeOffset)) - { - left = DateTimeOffsetToDateTime(left, querySettings.TimeZone, querySettings); - } +/// +/// The helper class for all expression binders. +/// +internal static class ExpressionBinderHelper +{ + private static readonly MethodInfo StringCompareMethodInfo = typeof(string).GetMethod("Compare", new[] { typeof(string), typeof(string) }); + private static readonly MethodInfo GuidCompareMethodInfo = typeof(Guid).GetMethod("CompareTo", new[] { typeof(Guid) }); - if ((IsDateOrOffset(leftUnderlyingType) && IsDate(rightUnderlyingType)) || - (IsDate(leftUnderlyingType) && IsDateOrOffset(rightUnderlyingType))) - { - left = CreateDateBinaryExpression(left, querySettings); - right = CreateDateBinaryExpression(right, querySettings); - } + private static readonly Expression NullConstant = Expression.Constant(null); + private static readonly Expression FalseConstant = Expression.Constant(false); + private static readonly Expression TrueConstant = Expression.Constant(true); + private static readonly Expression ZeroConstant = Expression.Constant(0); - if ((IsDateOrOffset(leftUnderlyingType) && IsTimeOfDay(rightUnderlyingType)) || - (IsTimeOfDay(leftUnderlyingType) && IsDateOrOffset(rightUnderlyingType)) || - (IsTimeSpan(leftUnderlyingType) && IsTimeOfDay(rightUnderlyingType)) || - (IsTimeOfDay(leftUnderlyingType) && IsTimeSpan(rightUnderlyingType))) - { - left = CreateTimeBinaryExpression(left, querySettings); - right = CreateTimeBinaryExpression(right, querySettings); - } + private static readonly Dictionary BinaryOperatorMapping = new Dictionary + { + { BinaryOperatorKind.Add, ExpressionType.Add }, + { BinaryOperatorKind.And, ExpressionType.AndAlso }, + { BinaryOperatorKind.Divide, ExpressionType.Divide }, + { BinaryOperatorKind.Equal, ExpressionType.Equal }, + { BinaryOperatorKind.GreaterThan, ExpressionType.GreaterThan }, + { BinaryOperatorKind.GreaterThanOrEqual, ExpressionType.GreaterThanOrEqual }, + { BinaryOperatorKind.LessThan, ExpressionType.LessThan }, + { BinaryOperatorKind.LessThanOrEqual, ExpressionType.LessThanOrEqual }, + { BinaryOperatorKind.Modulo, ExpressionType.Modulo }, + { BinaryOperatorKind.Multiply, ExpressionType.Multiply }, + { BinaryOperatorKind.NotEqual, ExpressionType.NotEqual }, + { BinaryOperatorKind.Or, ExpressionType.OrElse }, + { BinaryOperatorKind.Subtract, ExpressionType.Subtract }, + }; + + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "These are simple conversion function and cannot be split up.")] + public static Expression CreateBinaryExpression(BinaryOperatorKind binaryOperator, Expression left, Expression right, bool liftToNull, ODataQuerySettings querySettings) + { + ExpressionType binaryExpressionType; - if ((IsType(leftUnderlyingType) && IsDate(rightUnderlyingType)) || - (IsDate(leftUnderlyingType) && IsType(rightUnderlyingType))) - { - left = CreateDateBinaryExpression(left, querySettings); - right = CreateDateBinaryExpression(right, querySettings); - } - else if((IsType(leftUnderlyingType) && IsTimeOfDay(rightUnderlyingType)) || - (IsTimeOfDay(leftUnderlyingType) && IsType(rightUnderlyingType))) - { - left = CreateTimeBinaryExpression(left, querySettings); - right = CreateTimeBinaryExpression(right, querySettings); - } + // When comparing an enum to a string, parse the string, convert both to the enum underlying type, and compare the values + // When comparing an enum to an enum with the same type, convert both to the underlying type, and compare the values + Type leftUnderlyingType = Nullable.GetUnderlyingType(left.Type) ?? left.Type; + Type rightUnderlyingType = Nullable.GetUnderlyingType(right.Type) ?? right.Type; - if (left.Type != right.Type) - { - // one of them must be nullable and the other is not. - left = ToNullable(left); - right = ToNullable(right); - } + // Convert to integers unless Enum type is required + if ((TypeHelper.IsEnum(leftUnderlyingType) || TypeHelper.IsEnum(rightUnderlyingType)) && binaryOperator != BinaryOperatorKind.Has) + { + Type enumType = TypeHelper.IsEnum(leftUnderlyingType) ? leftUnderlyingType : rightUnderlyingType; + Type enumUnderlyingType = Enum.GetUnderlyingType(enumType); + left = ConvertToEnumUnderlyingType(left, enumType, enumUnderlyingType); + right = ConvertToEnumUnderlyingType(right, enumType, enumUnderlyingType); + } - if (left.Type == typeof(Guid) || right.Type == typeof(Guid)) - { - switch (binaryOperator) - { - case BinaryOperatorKind.GreaterThan: - case BinaryOperatorKind.GreaterThanOrEqual: - case BinaryOperatorKind.LessThan: - case BinaryOperatorKind.LessThanOrEqual: - left = Expression.Call(left, GuidCompareMethodInfo, right); - right = ZeroConstant; - break; - default: - break; - } - } + if (leftUnderlyingType == typeof(DateTime) && rightUnderlyingType == typeof(DateTimeOffset)) + { + right = DateTimeOffsetToDateTime(right, querySettings.TimeZone, querySettings); + } + else if (rightUnderlyingType == typeof(DateTime) && leftUnderlyingType == typeof(DateTimeOffset)) + { + left = DateTimeOffsetToDateTime(left, querySettings.TimeZone, querySettings); + } - if (left.Type == typeof(string) || right.Type == typeof(string)) - { - // convert nulls of type object to nulls of type string to make the String.Compare call work - left = ConvertNull(left, typeof(string)); - right = ConvertNull(right, typeof(string)); + if ((IsDateOrOffset(leftUnderlyingType) && IsDate(rightUnderlyingType)) || + (IsDate(leftUnderlyingType) && IsDateOrOffset(rightUnderlyingType))) + { + left = CreateDateBinaryExpression(left, querySettings); + right = CreateDateBinaryExpression(right, querySettings); + } - // Use string.Compare instead of comparison for gt, ge, lt, le between two strings since direct comparisons are not supported - switch (binaryOperator) - { - case BinaryOperatorKind.GreaterThan: - case BinaryOperatorKind.GreaterThanOrEqual: - case BinaryOperatorKind.LessThan: - case BinaryOperatorKind.LessThanOrEqual: - left = Expression.Call(StringCompareMethodInfo, left, right); - right = ZeroConstant; - break; - default: - break; - } - } + if ((IsDateOrOffset(leftUnderlyingType) && IsTimeOfDay(rightUnderlyingType)) || + (IsTimeOfDay(leftUnderlyingType) && IsDateOrOffset(rightUnderlyingType)) || + (IsTimeSpan(leftUnderlyingType) && IsTimeOfDay(rightUnderlyingType)) || + (IsTimeOfDay(leftUnderlyingType) && IsTimeSpan(rightUnderlyingType))) + { + left = CreateTimeBinaryExpression(left, querySettings); + right = CreateTimeBinaryExpression(right, querySettings); + } - if (BinaryOperatorMapping.TryGetValue(binaryOperator, out binaryExpressionType)) - { - if (left.Type == typeof(byte[]) || right.Type == typeof(byte[])) - { - left = ConvertNull(left, typeof(byte[])); - right = ConvertNull(right, typeof(byte[])); + if ((IsType(leftUnderlyingType) && IsDate(rightUnderlyingType)) || + (IsDate(leftUnderlyingType) && IsType(rightUnderlyingType))) + { + left = CreateDateBinaryExpression(left, querySettings); + right = CreateDateBinaryExpression(right, querySettings); + } + else if((IsType(leftUnderlyingType) && IsTimeOfDay(rightUnderlyingType)) || + (IsTimeOfDay(leftUnderlyingType) && IsType(rightUnderlyingType))) + { + left = CreateTimeBinaryExpression(left, querySettings); + right = CreateTimeBinaryExpression(right, querySettings); + } - switch (binaryExpressionType) - { - case ExpressionType.Equal: - return Expression.MakeBinary(binaryExpressionType, left, right, liftToNull, method: Linq2ObjectsComparisonMethods.AreByteArraysEqualMethodInfo); - case ExpressionType.NotEqual: - return Expression.MakeBinary(binaryExpressionType, left, right, liftToNull, method: Linq2ObjectsComparisonMethods.AreByteArraysNotEqualMethodInfo); - default: - throw new ODataException(Error.Format(SRResources.BinaryOperatorNotSupported, "Edm.Binary", "Edm.Binary", binaryOperator)); - } - } - else - { - return Expression.MakeBinary(binaryExpressionType, left, right, liftToNull, method: null); - } - } - else - { - // Enum has a "has" operator - // {(c1, c2) => c1.HasFlag(Convert(c2))} - if (TypeHelper.IsEnum(left.Type) && TypeHelper.IsEnum(right.Type) && binaryOperator == BinaryOperatorKind.Has) - { - UnaryExpression flag = Expression.Convert(right, typeof(Enum)); - return BindHas(left, flag, querySettings); - } - else - { - throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, binaryOperator, typeof(ExpressionBinderBase).Name); - } - } + if (left.Type != right.Type) + { + // one of them must be nullable and the other is not. + left = ToNullable(left); + right = ToNullable(right); } - public static Expression MakePropertyAccess(PropertyInfo propertyInfo, Expression argument, ODataQuerySettings querySettings) + if (left.Type == typeof(Guid) || right.Type == typeof(Guid)) { - Expression propertyArgument = argument; - if (querySettings.HandleNullPropagation == HandleNullPropagationOption.True) + switch (binaryOperator) { - // we don't have to check if the argument is null inside the function call as we do it already - // before calling the function. So remove the redundant null checks. - propertyArgument = RemoveInnerNullPropagation(argument, querySettings); + case BinaryOperatorKind.GreaterThan: + case BinaryOperatorKind.GreaterThanOrEqual: + case BinaryOperatorKind.LessThan: + case BinaryOperatorKind.LessThanOrEqual: + left = Expression.Call(left, GuidCompareMethodInfo, right); + right = ZeroConstant; + break; + default: + break; } - - // if the argument is of type Nullable, then translate the argument to Nullable.Value as none - // of the canonical functions have overloads for Nullable<> arguments. - propertyArgument = ExtractValueFromNullableExpression(propertyArgument); - - return Expression.Property(propertyArgument, propertyInfo); } - // creates an expression for the corresponding OData function. - public static Expression MakeFunctionCall(MemberInfo member, ODataQuerySettings querySettings, params Expression[] arguments) + if (left.Type == typeof(string) || right.Type == typeof(string)) { - Contract.Assert(member.MemberType == MemberTypes.Property || member.MemberType == MemberTypes.Method); + // convert nulls of type object to nulls of type string to make the String.Compare call work + left = ConvertNull(left, typeof(string)); + right = ConvertNull(right, typeof(string)); - IEnumerable functionCallArguments = arguments; - if (querySettings.HandleNullPropagation == HandleNullPropagationOption.True) + // Use string.Compare instead of comparison for gt, ge, lt, le between two strings since direct comparisons are not supported + switch (binaryOperator) { - // we don't have to check if the argument is null inside the function call as we do it already - // before calling the function. So remove the redundant null checks. - functionCallArguments = arguments.Select(a => RemoveInnerNullPropagation(a, querySettings)); + case BinaryOperatorKind.GreaterThan: + case BinaryOperatorKind.GreaterThanOrEqual: + case BinaryOperatorKind.LessThan: + case BinaryOperatorKind.LessThanOrEqual: + left = Expression.Call(StringCompareMethodInfo, left, right); + right = ZeroConstant; + break; + default: + break; } + } - // if the argument is of type Nullable, then translate the argument to Nullable.Value as none - // of the canonical functions have overloads for Nullable<> arguments. - functionCallArguments = ExtractValueFromNullableArguments(functionCallArguments); - - Expression functionCall; - if (member.MemberType == MemberTypes.Method) + if (BinaryOperatorMapping.TryGetValue(binaryOperator, out binaryExpressionType)) + { + if (left.Type == typeof(byte[]) || right.Type == typeof(byte[])) { - MethodInfo method = member as MethodInfo; - if (method.IsStatic) - { - functionCall = Expression.Call(null, method, functionCallArguments); - } - else + left = ConvertNull(left, typeof(byte[])); + right = ConvertNull(right, typeof(byte[])); + + switch (binaryExpressionType) { - functionCall = Expression.Call(functionCallArguments.First(), method, functionCallArguments.Skip(1)); + case ExpressionType.Equal: + return Expression.MakeBinary(binaryExpressionType, left, right, liftToNull, method: Linq2ObjectsComparisonMethods.AreByteArraysEqualMethodInfo); + case ExpressionType.NotEqual: + return Expression.MakeBinary(binaryExpressionType, left, right, liftToNull, method: Linq2ObjectsComparisonMethods.AreByteArraysNotEqualMethodInfo); + default: + throw new ODataException(Error.Format(SRResources.BinaryOperatorNotSupported, "Edm.Binary", "Edm.Binary", binaryOperator)); } } else { - // property - functionCall = Expression.Property(functionCallArguments.First(), member as PropertyInfo); + return Expression.MakeBinary(binaryExpressionType, left, right, liftToNull, method: null); } - - return CreateFunctionCallWithNullPropagation(functionCall, arguments, querySettings); } - - public static Expression CreateFunctionCallWithNullPropagation(Expression functionCall, Expression[] arguments, ODataQuerySettings querySettings) + else { - if (querySettings.HandleNullPropagation == HandleNullPropagationOption.True) + // Enum has a "has" operator + // {(c1, c2) => c1.HasFlag(Convert(c2))} + if (TypeHelper.IsEnum(left.Type) && TypeHelper.IsEnum(right.Type) && binaryOperator == BinaryOperatorKind.Has) { - Expression test = CheckIfArgumentsAreNull(arguments); - - if (test == FalseConstant) - { - // none of the arguments are/can be null. - // so no need to do any null propagation - return functionCall; - } - else - { - // if one of the arguments is null, result is null (not defined) - return - Expression.Condition( - test: test, - ifTrue: Expression.Constant(null, ToNullable(functionCall.Type)), - ifFalse: ToNullable(functionCall)); - } + UnaryExpression flag = Expression.Convert(right, typeof(Enum)); + return BindHas(left, flag, querySettings); } else { - return functionCall; + throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, binaryOperator, typeof(ExpressionBinderBase).Name); } } + } - // we don't have to do null checks inside the function for arguments as we do the null checks before calling - // the function when null propagation is enabled. - // this method converts back "arg == null ? null : convert(arg)" to "arg" - // Also, note that we can do this generically only because none of the odata functions that we support can take null - // as an argument. - public static Expression RemoveInnerNullPropagation(Expression expression, ODataQuerySettings querySettings) + public static Expression MakePropertyAccess(PropertyInfo propertyInfo, Expression argument, ODataQuerySettings querySettings) + { + Expression propertyArgument = argument; + if (querySettings.HandleNullPropagation == HandleNullPropagationOption.True) { - Contract.Assert(expression != null); + // we don't have to check if the argument is null inside the function call as we do it already + // before calling the function. So remove the redundant null checks. + propertyArgument = RemoveInnerNullPropagation(argument, querySettings); + } - if (querySettings.HandleNullPropagation == HandleNullPropagationOption.True) - { - // only null propagation generates conditional expressions - if (expression.NodeType == ExpressionType.Conditional) - { - // make sure to skip the DateTime IFF clause - ConditionalExpression conditionalExpr = (ConditionalExpression)expression; - if (conditionalExpr.Test.NodeType != ExpressionType.OrElse) - { - expression = conditionalExpr.IfFalse; - Contract.Assert(expression != null); + // if the argument is of type Nullable, then translate the argument to Nullable.Value as none + // of the canonical functions have overloads for Nullable<> arguments. + propertyArgument = ExtractValueFromNullableExpression(propertyArgument); - if (expression.NodeType == ExpressionType.Convert) - { - UnaryExpression unaryExpression = expression as UnaryExpression; - Contract.Assert(unaryExpression != null); - - if (Nullable.GetUnderlyingType(unaryExpression.Type) == unaryExpression.Operand.Type) - { - // this is a cast from T to Nullable which is redundant. - expression = unaryExpression.Operand; - } - } - } - } - } + return Expression.Property(propertyArgument, propertyInfo); + } - return expression; - } + // creates an expression for the corresponding OData function. + public static Expression MakeFunctionCall(MemberInfo member, ODataQuerySettings querySettings, params Expression[] arguments) + { + Contract.Assert(member.MemberType == MemberTypes.Property || member.MemberType == MemberTypes.Method); - private static Expression CheckIfArgumentsAreNull(Expression[] arguments) + IEnumerable functionCallArguments = arguments; + if (querySettings.HandleNullPropagation == HandleNullPropagationOption.True) { - if (arguments.Any(arg => arg == NullConstant)) - { - return TrueConstant; - } - - arguments = - arguments - .Select(arg => CheckForNull(arg)) - .Where(arg => arg != null) - .ToArray(); - - if (arguments.Any()) - { - return arguments - .Aggregate((left, right) => Expression.OrElse(left, right)); - } - else - { - return FalseConstant; - } + // we don't have to check if the argument is null inside the function call as we do it already + // before calling the function. So remove the redundant null checks. + functionCallArguments = arguments.Select(a => RemoveInnerNullPropagation(a, querySettings)); } - public static Expression CheckForNull(Expression expression) + // if the argument is of type Nullable, then translate the argument to Nullable.Value as none + // of the canonical functions have overloads for Nullable<> arguments. + functionCallArguments = ExtractValueFromNullableArguments(functionCallArguments); + + Expression functionCall; + if (member.MemberType == MemberTypes.Method) { - if (IsNullable(expression.Type) && expression.NodeType != ExpressionType.Constant) + MethodInfo method = member as MethodInfo; + if (method.IsStatic) { - return Expression.Equal(expression, Expression.Constant(null)); + functionCall = Expression.Call(null, method, functionCallArguments); } else { - return null; + functionCall = Expression.Call(functionCallArguments.First(), method, functionCallArguments.Skip(1)); } } - - private static IEnumerable ExtractValueFromNullableArguments(IEnumerable arguments) + else { - return arguments.Select(arg => ExtractValueFromNullableExpression(arg)); + // property + functionCall = Expression.Property(functionCallArguments.First(), member as PropertyInfo); } - public static Expression ExtractValueFromNullableExpression(Expression source) - { - return Nullable.GetUnderlyingType(source.Type) != null ? Expression.Property(source, "Value") : source; - } + return CreateFunctionCallWithNullPropagation(functionCall, arguments, querySettings); + } - public static Expression BindHas(Expression left, Expression flag, ODataQuerySettings querySettings) + public static Expression CreateFunctionCallWithNullPropagation(Expression functionCall, Expression[] arguments, ODataQuerySettings querySettings) + { + if (querySettings.HandleNullPropagation == HandleNullPropagationOption.True) { - Contract.Assert(TypeHelper.IsEnum(left.Type)); - Contract.Assert(flag.Type == typeof(Enum)); - - Expression[] arguments = new[] { left, flag }; - return MakeFunctionCall(ClrCanonicalFunctions.HasFlag, querySettings, arguments); - } + Expression test = CheckIfArgumentsAreNull(arguments); - private static Expression GetProperty(Expression source, string propertyName, ODataQuerySettings querySettings) - { - if (IsDateOrOffset(source.Type)) - { - if (IsDateTime(source.Type)) - { - return MakePropertyAccess(ClrCanonicalFunctions.DateTimeProperties[propertyName], source, querySettings); - } - else - { - return MakePropertyAccess(ClrCanonicalFunctions.DateTimeOffsetProperties[propertyName], source, querySettings); - } - } - else if (IsDate(source.Type)) - { - return MakePropertyAccess(ClrCanonicalFunctions.DateProperties[propertyName], source, querySettings); - } - else if (IsTimeOfDay(source.Type)) + if (test == FalseConstant) { - return MakePropertyAccess(ClrCanonicalFunctions.TimeOfDayProperties[propertyName], source, querySettings); - } - else if (IsTimeSpan(source.Type)) - { - return MakePropertyAccess(ClrCanonicalFunctions.TimeSpanProperties[propertyName], source, querySettings); - } - else if (IsType(source.Type)) - { - return MakePropertyAccess(ClrCanonicalFunctions.DateOnlyProperties[propertyName], source, querySettings); + // none of the arguments are/can be null. + // so no need to do any null propagation + return functionCall; } - else if (IsType(source.Type)) + else { - return MakePropertyAccess(ClrCanonicalFunctions.TimeOnlyProperties[propertyName], source, querySettings); + // if one of the arguments is null, result is null (not defined) + return + Expression.Condition( + test: test, + ifTrue: Expression.Constant(null, ToNullable(functionCall.Type)), + ifFalse: ToNullable(functionCall)); } - - return source; } - - private static Expression CreateDateBinaryExpression(Expression source, ODataQuerySettings querySettings) + else { - source = ConvertToDateTimeRelatedConstExpression(source); - - // Year, Month, Day - Expression year = GetProperty(source, ClrCanonicalFunctions.YearFunctionName, querySettings); - Expression month = GetProperty(source, ClrCanonicalFunctions.MonthFunctionName, querySettings); - Expression day = GetProperty(source, ClrCanonicalFunctions.DayFunctionName, querySettings); - - // return (year * 10000 + month * 100 + day) - Expression result = - Expression.Add( - Expression.Add(Expression.Multiply(year, Expression.Constant(10000)), - Expression.Multiply(month, Expression.Constant(100))), day); - - return CreateFunctionCallWithNullPropagation(result, new[] { source }, querySettings); + return functionCall; } + } - private static Expression CreateTimeBinaryExpression(Expression source, ODataQuerySettings querySettings) - { - source = ConvertToDateTimeRelatedConstExpression(source); - - // Hour, Minute, Second, Millisecond - Expression hour = GetProperty(source, ClrCanonicalFunctions.HourFunctionName, querySettings); - Expression minute = GetProperty(source, ClrCanonicalFunctions.MinuteFunctionName, querySettings); - Expression second = GetProperty(source, ClrCanonicalFunctions.SecondFunctionName, querySettings); - Expression milliSecond = GetProperty(source, ClrCanonicalFunctions.MillisecondFunctionName, querySettings); - - Expression hourTicks = Expression.Multiply(Expression.Convert(hour, typeof(long)), Expression.Constant(TimeSpan.TicksPerHour, typeof(long))); - Expression minuteTicks = Expression.Multiply(Expression.Convert(minute, typeof(long)), Expression.Constant(TimeSpan.TicksPerMinute, typeof(long))); - Expression secondTicks = Expression.Multiply(Expression.Convert(second, typeof(long)), Expression.Constant(TimeSpan.TicksPerSecond, typeof(long))); - - // return (hour * TicksPerHour + minute * TicksPerMinute + second * TicksPerSecond + millisecond) - Expression result = Expression.Add(hourTicks, Expression.Add(minuteTicks, Expression.Add(secondTicks, Expression.Convert(milliSecond, typeof(long))))); - - return CreateFunctionCallWithNullPropagation(result, new[] { source }, querySettings); - } + // we don't have to do null checks inside the function for arguments as we do the null checks before calling + // the function when null propagation is enabled. + // this method converts back "arg == null ? null : convert(arg)" to "arg" + // Also, note that we can do this generically only because none of the odata functions that we support can take null + // as an argument. + public static Expression RemoveInnerNullPropagation(Expression expression, ODataQuerySettings querySettings) + { + Contract.Assert(expression != null); - private static Expression ConvertToDateTimeRelatedConstExpression(Expression source) + if (querySettings.HandleNullPropagation == HandleNullPropagationOption.True) { - var parameterizedConstantValue = ExtractParameterizedConstant(source); - if (parameterizedConstantValue != null && TypeHelper.IsNullable(source.Type)) + // only null propagation generates conditional expressions + if (expression.NodeType == ExpressionType.Conditional) { - var dateTimeOffset = parameterizedConstantValue as DateTimeOffset?; - if (dateTimeOffset != null) - { - return Expression.Constant(dateTimeOffset.Value, typeof(DateTimeOffset)); - } - - var dateTime = parameterizedConstantValue as DateTime?; - if (dateTime != null) + // make sure to skip the DateTime IFF clause + ConditionalExpression conditionalExpr = (ConditionalExpression)expression; + if (conditionalExpr.Test.NodeType != ExpressionType.OrElse) { - return Expression.Constant(dateTime.Value, typeof(DateTime)); - } - - var date = parameterizedConstantValue as Date?; - if (date != null) - { - return Expression.Constant(date.Value, typeof(Date)); - } + expression = conditionalExpr.IfFalse; + Contract.Assert(expression != null); - var timeOfDay = parameterizedConstantValue as TimeOfDay?; - if (timeOfDay != null) - { - return Expression.Constant(timeOfDay.Value, typeof(TimeOfDay)); - } - - if (parameterizedConstantValue is DateOnly dateOnly) - { - return Expression.Constant(dateOnly, typeof(DateOnly)); - } + if (expression.NodeType == ExpressionType.Convert) + { + UnaryExpression unaryExpression = expression as UnaryExpression; + Contract.Assert(unaryExpression != null); - else if (parameterizedConstantValue is TimeOnly timeOnly) - { - return Expression.Constant(timeOnly, typeof(TimeOnly)); + if (Nullable.GetUnderlyingType(unaryExpression.Type) == unaryExpression.Operand.Type) + { + // this is a cast from T to Nullable which is redundant. + expression = unaryExpression.Operand; + } + } } } - - return source; } - public static bool IsIQueryable(Type type) - { - return typeof(IQueryable).IsAssignableFrom(type); - } + return expression; + } - public static bool IsDoubleOrDecimal(Type type) + private static Expression CheckIfArgumentsAreNull(Expression[] arguments) + { + if (arguments.Any(arg => arg == NullConstant)) { - return IsType(type) || IsType(type); + return TrueConstant; } - public static bool IsDateAndTimeRelated(Type type) - { - return IsType(type) - || IsType(type) - || IsType(type) - || IsType(type) - || IsType(type) - || IsType(type) - || IsType(type) - ; - } + arguments = + arguments + .Select(arg => CheckForNull(arg)) + .Where(arg => arg != null) + .ToArray(); - public static bool IsDateRelated(Type type) + if (arguments.Any()) { - return IsType(type) || IsType(type) || IsType(type) || IsType(type); + return arguments + .Aggregate((left, right) => Expression.OrElse(left, right)); } - - public static bool IsTimeRelated(Type type) + else { - return IsType(type) || IsType(type) || IsType(type) || IsType(type) || IsType(type); + return FalseConstant; } + } - public static bool IsDateOrOffset(Type type) + public static Expression CheckForNull(Expression expression) + { + if (IsNullable(expression.Type) && expression.NodeType != ExpressionType.Constant) { - return IsType(type) || IsType(type); + return Expression.Equal(expression, Expression.Constant(null)); } - - public static bool IsDateTime(Type type) + else { - return IsType(type); + return null; } + } - public static bool IsTimeSpan(Type type) - { - return IsType(type); - } + private static IEnumerable ExtractValueFromNullableArguments(IEnumerable arguments) + { + return arguments.Select(arg => ExtractValueFromNullableExpression(arg)); + } - public static bool IsTimeOfDay(Type type) + public static Expression ExtractValueFromNullableExpression(Expression source) + { + return Nullable.GetUnderlyingType(source.Type) != null ? Expression.Property(source, "Value") : source; + } + + public static Expression BindHas(Expression left, Expression flag, ODataQuerySettings querySettings) + { + Contract.Assert(TypeHelper.IsEnum(left.Type)); + Contract.Assert(flag.Type == typeof(Enum)); + + Expression[] arguments = new[] { left, flag }; + return MakeFunctionCall(ClrCanonicalFunctions.HasFlag, querySettings, arguments); + } + + private static Expression GetProperty(Expression source, string propertyName, ODataQuerySettings querySettings) + { + if (IsDateOrOffset(source.Type)) { - return IsType(type); + if (IsDateTime(source.Type)) + { + return MakePropertyAccess(ClrCanonicalFunctions.DateTimeProperties[propertyName], source, querySettings); + } + else + { + return MakePropertyAccess(ClrCanonicalFunctions.DateTimeOffsetProperties[propertyName], source, querySettings); + } } - - public static bool IsDate(Type type) + else if (IsDate(source.Type)) { - return IsType(type); + return MakePropertyAccess(ClrCanonicalFunctions.DateProperties[propertyName], source, querySettings); } - - public static bool IsDateOnly(this Type type) + else if (IsTimeOfDay(source.Type)) { - return IsType(type); + return MakePropertyAccess(ClrCanonicalFunctions.TimeOfDayProperties[propertyName], source, querySettings); } - - public static bool IsTimeOnly(this Type type) + else if (IsTimeSpan(source.Type)) { - return IsType(type); + return MakePropertyAccess(ClrCanonicalFunctions.TimeSpanProperties[propertyName], source, querySettings); } - - public static bool IsInteger(Type type) + else if (IsType(source.Type)) { - return IsType(type) || IsType(type) || IsType(type); + return MakePropertyAccess(ClrCanonicalFunctions.DateOnlyProperties[propertyName], source, querySettings); } - - public static bool IsType(Type type) where T : struct + else if (IsType(source.Type)) { - return type == typeof(T) || type == typeof(T?); + return MakePropertyAccess(ClrCanonicalFunctions.TimeOnlyProperties[propertyName], source, querySettings); } - public static Expression ConvertToEnumUnderlyingType(Expression expression, Type enumType, Type enumUnderlyingType) + return source; + } + + private static Expression CreateDateBinaryExpression(Expression source, ODataQuerySettings querySettings) + { + source = ConvertToDateTimeRelatedConstExpression(source); + + // Year, Month, Day + Expression year = GetProperty(source, ClrCanonicalFunctions.YearFunctionName, querySettings); + Expression month = GetProperty(source, ClrCanonicalFunctions.MonthFunctionName, querySettings); + Expression day = GetProperty(source, ClrCanonicalFunctions.DayFunctionName, querySettings); + + // return (year * 10000 + month * 100 + day) + Expression result = + Expression.Add( + Expression.Add(Expression.Multiply(year, Expression.Constant(10000)), + Expression.Multiply(month, Expression.Constant(100))), day); + + return CreateFunctionCallWithNullPropagation(result, new[] { source }, querySettings); + } + + private static Expression CreateTimeBinaryExpression(Expression source, ODataQuerySettings querySettings) + { + source = ConvertToDateTimeRelatedConstExpression(source); + + // Hour, Minute, Second, Millisecond + Expression hour = GetProperty(source, ClrCanonicalFunctions.HourFunctionName, querySettings); + Expression minute = GetProperty(source, ClrCanonicalFunctions.MinuteFunctionName, querySettings); + Expression second = GetProperty(source, ClrCanonicalFunctions.SecondFunctionName, querySettings); + Expression milliSecond = GetProperty(source, ClrCanonicalFunctions.MillisecondFunctionName, querySettings); + + Expression hourTicks = Expression.Multiply(Expression.Convert(hour, typeof(long)), Expression.Constant(TimeSpan.TicksPerHour, typeof(long))); + Expression minuteTicks = Expression.Multiply(Expression.Convert(minute, typeof(long)), Expression.Constant(TimeSpan.TicksPerMinute, typeof(long))); + Expression secondTicks = Expression.Multiply(Expression.Convert(second, typeof(long)), Expression.Constant(TimeSpan.TicksPerSecond, typeof(long))); + + // return (hour * TicksPerHour + minute * TicksPerMinute + second * TicksPerSecond + millisecond) + Expression result = Expression.Add(hourTicks, Expression.Add(minuteTicks, Expression.Add(secondTicks, Expression.Convert(milliSecond, typeof(long))))); + + return CreateFunctionCallWithNullPropagation(result, new[] { source }, querySettings); + } + + private static Expression ConvertToDateTimeRelatedConstExpression(Expression source) + { + var parameterizedConstantValue = ExtractParameterizedConstant(source); + if (parameterizedConstantValue != null && TypeHelper.IsNullable(source.Type)) { - object parameterizedConstantValue = ExtractParameterizedConstant(expression); - if (parameterizedConstantValue != null) + var dateTimeOffset = parameterizedConstantValue as DateTimeOffset?; + if (dateTimeOffset != null) { - string enumStringValue = parameterizedConstantValue as string; - if (enumStringValue != null) - { - return Expression.Constant( - Convert.ChangeType( - Enum.Parse(enumType, enumStringValue), enumUnderlyingType, CultureInfo.InvariantCulture)); - } - else - { - // enum member value - return Expression.Constant( - Convert.ChangeType( - parameterizedConstantValue, enumUnderlyingType, CultureInfo.InvariantCulture)); - } + return Expression.Constant(dateTimeOffset.Value, typeof(DateTimeOffset)); } - else if (expression.Type == enumType) + + var dateTime = parameterizedConstantValue as DateTime?; + if (dateTime != null) { - return Expression.Convert(expression, enumUnderlyingType); + return Expression.Constant(dateTime.Value, typeof(DateTime)); } - else if (Nullable.GetUnderlyingType(expression.Type) == enumType) + + var date = parameterizedConstantValue as Date?; + if (date != null) { - return Expression.Convert(expression, typeof(Nullable<>).MakeGenericType(enumUnderlyingType)); + return Expression.Constant(date.Value, typeof(Date)); } - else if (expression.NodeType == ExpressionType.Constant && ((ConstantExpression)expression).Value == null) + + var timeOfDay = parameterizedConstantValue as TimeOfDay?; + if (timeOfDay != null) { - return expression; + return Expression.Constant(timeOfDay.Value, typeof(TimeOfDay)); } - else + + if (parameterizedConstantValue is DateOnly dateOnly) { - throw Error.NotSupported(SRResources.ConvertToEnumFailed, enumType, expression.Type); + return Expression.Constant(dateOnly, typeof(DateOnly)); } - } - // Extract the constant that would have been encapsulated into LinqParameterContainer if this - // expression represents it else return null. - public static object ExtractParameterizedConstant(Expression expression) - { - if (expression.NodeType == ExpressionType.MemberAccess) + else if (parameterizedConstantValue is TimeOnly timeOnly) { - MemberExpression memberAccess = expression as MemberExpression; - Contract.Assert(memberAccess != null); + return Expression.Constant(timeOnly, typeof(TimeOnly)); + } + } - PropertyInfo propertyInfo = memberAccess.Member as PropertyInfo; - if (propertyInfo != null && propertyInfo.GetMethod.IsStatic) - { - return propertyInfo.GetValue(new object()); - } + return source; + } - if (memberAccess.Expression.NodeType == ExpressionType.Constant) - { - ConstantExpression constant = memberAccess.Expression as ConstantExpression; - Contract.Assert(constant != null); - Contract.Assert(constant.Value != null); - LinqParameterContainer value = constant.Value as LinqParameterContainer; - Contract.Assert(value != null, "Constants are already embedded into LinqParameterContainer"); + public static bool IsIQueryable(Type type) + { + return typeof(IQueryable).IsAssignableFrom(type); + } - return value.Property; - } - } + public static bool IsDoubleOrDecimal(Type type) + { + return IsType(type) || IsType(type); + } - return null; - } + public static bool IsDateAndTimeRelated(Type type) + { + return IsType(type) + || IsType(type) + || IsType(type) + || IsType(type) + || IsType(type) + || IsType(type) + || IsType(type) + ; + } + + public static bool IsDateRelated(Type type) + { + return IsType(type) || IsType(type) || IsType(type) || IsType(type); + } + + public static bool IsTimeRelated(Type type) + { + return IsType(type) || IsType(type) || IsType(type) || IsType(type) || IsType(type); + } + + public static bool IsDateOrOffset(Type type) + { + return IsType(type) || IsType(type); + } + + public static bool IsDateTime(Type type) + { + return IsType(type); + } + + public static bool IsTimeSpan(Type type) + { + return IsType(type); + } + + public static bool IsTimeOfDay(Type type) + { + return IsType(type); + } - public static Expression DateTimeOffsetToDateTime(Expression expression, TimeZoneInfo timeZoneInfo, ODataQuerySettings settings) + public static bool IsDate(Type type) + { + return IsType(type); + } + + public static bool IsDateOnly(this Type type) + { + return IsType(type); + } + + public static bool IsTimeOnly(this Type type) + { + return IsType(type); + } + + public static bool IsInteger(Type type) + { + return IsType(type) || IsType(type) || IsType(type); + } + + public static bool IsType(Type type) where T : struct + { + return type == typeof(T) || type == typeof(T?); + } + + public static Expression ConvertToEnumUnderlyingType(Expression expression, Type enumType, Type enumUnderlyingType) + { + object parameterizedConstantValue = ExtractParameterizedConstant(expression); + if (parameterizedConstantValue != null) { - var unaryExpression = expression as UnaryExpression; - if (unaryExpression != null) + string enumStringValue = parameterizedConstantValue as string; + if (enumStringValue != null) { - if (Nullable.GetUnderlyingType(unaryExpression.Type) == unaryExpression.Operand.Type) - { - // this is a cast from T to Nullable which is redundant. - expression = unaryExpression.Operand; - } + return Expression.Constant( + Convert.ChangeType( + Enum.Parse(enumType, enumStringValue), enumUnderlyingType, CultureInfo.InvariantCulture)); } - var parameterizedConstantValue = ExtractParameterizedConstant(expression); - var dto = parameterizedConstantValue as DateTimeOffset?; - if (dto != null) + else { - if (settings.EnableConstantParameterization) - { - return LinqParameterContainer.Parameterize(typeof(DateTime), EdmPrimitiveHelper.ConvertPrimitiveValue(dto.Value, typeof(DateTime), timeZoneInfo)); - } - else - { - return Expression.Constant(EdmPrimitiveHelper.ConvertPrimitiveValue(dto.Value, typeof(DateTime), timeZoneInfo), typeof(DateTime)); - } + // enum member value + return Expression.Constant( + Convert.ChangeType( + parameterizedConstantValue, enumUnderlyingType, CultureInfo.InvariantCulture)); } + } + else if (expression.Type == enumType) + { + return Expression.Convert(expression, enumUnderlyingType); + } + else if (Nullable.GetUnderlyingType(expression.Type) == enumType) + { + return Expression.Convert(expression, typeof(Nullable<>).MakeGenericType(enumUnderlyingType)); + } + else if (expression.NodeType == ExpressionType.Constant && ((ConstantExpression)expression).Value == null) + { return expression; } - - public static bool IsNullable(Type t) + else { - if (!t.IsValueType || (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>))) - { - return true; - } - - return false; + throw Error.NotSupported(SRResources.ConvertToEnumFailed, enumType, expression.Type); } + } - public static Type ToNullable(Type t) + // Extract the constant that would have been encapsulated into LinqParameterContainer if this + // expression represents it else return null. + public static object ExtractParameterizedConstant(Expression expression) + { + if (expression.NodeType == ExpressionType.MemberAccess) { - if (IsNullable(t)) + MemberExpression memberAccess = expression as MemberExpression; + Contract.Assert(memberAccess != null); + + PropertyInfo propertyInfo = memberAccess.Member as PropertyInfo; + if (propertyInfo != null && propertyInfo.GetMethod.IsStatic) { - return t; + return propertyInfo.GetValue(new object()); } - else + + if (memberAccess.Expression.NodeType == ExpressionType.Constant) { - return typeof(Nullable<>).MakeGenericType(t); + ConstantExpression constant = memberAccess.Expression as ConstantExpression; + Contract.Assert(constant != null); + Contract.Assert(constant.Value != null); + LinqParameterContainer value = constant.Value as LinqParameterContainer; + Contract.Assert(value != null, "Constants are already embedded into LinqParameterContainer"); + + return value.Property; } } - public static Expression ToNullable(Expression expression) + return null; + } + + public static Expression DateTimeOffsetToDateTime(Expression expression, TimeZoneInfo timeZoneInfo, ODataQuerySettings settings) + { + var unaryExpression = expression as UnaryExpression; + if (unaryExpression != null) { - if (!IsNullable(expression.Type)) + if (Nullable.GetUnderlyingType(unaryExpression.Type) == unaryExpression.Operand.Type) { - return Expression.Convert(expression, ToNullable(expression.Type)); + // this is a cast from T to Nullable which is redundant. + expression = unaryExpression.Operand; } - - return expression; } - - public static Expression ConvertNull(Expression expression, Type type) + var parameterizedConstantValue = ExtractParameterizedConstant(expression); + var dto = parameterizedConstantValue as DateTimeOffset?; + if (dto != null) { - ConstantExpression constantExpression = expression as ConstantExpression; - if (constantExpression != null && constantExpression.Value == null) + if (settings.EnableConstantParameterization) { - return Expression.Constant(null, type); + return LinqParameterContainer.Parameterize(typeof(DateTime), EdmPrimitiveHelper.ConvertPrimitiveValue(dto.Value, typeof(DateTime), timeZoneInfo)); } else { - return expression; + return Expression.Constant(EdmPrimitiveHelper.ConvertPrimitiveValue(dto.Value, typeof(DateTime), timeZoneInfo), typeof(DateTime)); } } + return expression; + } - public static Expression BindCastToStringType(Expression source) + public static bool IsNullable(Type t) + { + if (!t.IsValueType || (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>))) { - Expression sourceValue; + return true; + } - if (source.Type.IsGenericType && source.Type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - if (TypeHelper.IsEnum(source.Type)) - { - // Entity Framework doesn't have ToString method for enum types. - // Convert enum types to their underlying numeric types. - sourceValue = Expression.Convert( - Expression.Property(source, "Value"), - Enum.GetUnderlyingType(TypeHelper.GetUnderlyingTypeOrSelf(source.Type))); - } - else - { - // Entity Framework has ToString method for numeric types. - sourceValue = Expression.Property(source, "Value"); - } + return false; + } + + public static Type ToNullable(Type t) + { + if (IsNullable(t)) + { + return t; + } + else + { + return typeof(Nullable<>).MakeGenericType(t); + } + } - // Entity Framework doesn't have ToString method for nullable numeric types. - // Call ToString method on non-nullable numeric types. - return Expression.Condition( - Expression.Property(source, "HasValue"), - Expression.Call(sourceValue, "ToString", typeArguments: null, arguments: null), - Expression.Constant(null, typeof(string))); + public static Expression ToNullable(Expression expression) + { + if (!IsNullable(expression.Type)) + { + return Expression.Convert(expression, ToNullable(expression.Type)); + } + + return expression; + } + + public static Expression ConvertNull(Expression expression, Type type) + { + ConstantExpression constantExpression = expression as ConstantExpression; + if (constantExpression != null && constantExpression.Value == null) + { + return Expression.Constant(null, type); + } + else + { + return expression; + } + } + + public static Expression BindCastToStringType(Expression source) + { + Expression sourceValue; + + if (source.Type.IsGenericType && source.Type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + if (TypeHelper.IsEnum(source.Type)) + { + // Entity Framework doesn't have ToString method for enum types. + // Convert enum types to their underlying numeric types. + sourceValue = Expression.Convert( + Expression.Property(source, "Value"), + Enum.GetUnderlyingType(TypeHelper.GetUnderlyingTypeOrSelf(source.Type))); } else { - sourceValue = TypeHelper.IsEnum(source.Type) ? - Expression.Convert(source, Enum.GetUnderlyingType(source.Type)) : - source; - return Expression.Call(sourceValue, "ToString", typeArguments: null, arguments: null); + // Entity Framework has ToString method for numeric types. + sourceValue = Expression.Property(source, "Value"); } + + // Entity Framework doesn't have ToString method for nullable numeric types. + // Call ToString method on non-nullable numeric types. + return Expression.Condition( + Expression.Property(source, "HasValue"), + Expression.Call(sourceValue, "ToString", typeArguments: null, arguments: null), + Expression.Constant(null, typeof(string))); + } + else + { + sourceValue = TypeHelper.IsEnum(source.Type) ? + Expression.Convert(source, Enum.GetUnderlyingType(source.Type)) : + source; + return Expression.Call(sourceValue, "ToString", typeArguments: null, arguments: null); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/FilterBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/FilterBinder.cs index c074926c6..7d9203331 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/FilterBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/FilterBinder.cs @@ -9,50 +9,49 @@ using System.Linq.Expressions; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// The default implementation to bind an OData $filter represented by to a . +/// +public class FilterBinder : QueryBinder, IFilterBinder { /// - /// The default implementation to bind an OData $filter represented by to a . + /// Translates an OData $filter represented by to . + /// $filter=Name eq 'Sam' + /// |-- $it => $it.Name == "Sam" /// - public class FilterBinder : QueryBinder, IFilterBinder + /// The filter clause. + /// The query binder context. + /// The filter binder result. + public virtual Expression BindFilter(FilterClause filterClause, QueryBinderContext context) { - /// - /// Translates an OData $filter represented by to . - /// $filter=Name eq 'Sam' - /// |-- $it => $it.Name == "Sam" - /// - /// The filter clause. - /// The query binder context. - /// The filter binder result. - public virtual Expression BindFilter(FilterClause filterClause, QueryBinderContext context) + if (filterClause == null) { - if (filterClause == null) - { - throw Error.ArgumentNull(nameof(filterClause)); - } - - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(filterClause)); + } - Expression body = Bind(filterClause.Expression, context); + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - ParameterExpression filterParameter = context.CurrentParameter; + Expression body = Bind(filterClause.Expression, context); - LambdaExpression filterExpr = Expression.Lambda(body, filterParameter); + ParameterExpression filterParameter = context.CurrentParameter; - filterExpr = Expression.Lambda(ApplyNullPropagationForFilterBody(filterExpr.Body, context), filterExpr.Parameters); + LambdaExpression filterExpr = Expression.Lambda(body, filterParameter); - Type elementType = context.ElementClrType; + filterExpr = Expression.Lambda(ApplyNullPropagationForFilterBody(filterExpr.Body, context), filterExpr.Parameters); - Type expectedFilterType = typeof(Func<,>).MakeGenericType(elementType, typeof(bool)); - if (filterExpr.Type != expectedFilterType) - { - throw Error.Argument("filterType", SRResources.CannotCastFilter, filterExpr.Type.FullName, expectedFilterType.FullName); - } + Type elementType = context.ElementClrType; - return filterExpr; + Type expectedFilterType = typeof(Func<,>).MakeGenericType(elementType, typeof(bool)); + if (filterExpr.Type != expectedFilterType) + { + throw Error.Argument("filterType", SRResources.CannotCastFilter, filterExpr.Type.FullName, expectedFilterType.FullName); } + + return filterExpr; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/IFilterBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/IFilterBinder.cs index b2255ec39..0878a2fb3 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/IFilterBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/IFilterBinder.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -8,22 +8,21 @@ using System.Linq.Expressions; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// Exposes the ability to translate an OData $filter represented by to the . +/// +public interface IFilterBinder { /// - /// Exposes the ability to translate an OData $filter represented by to the . + /// Translates an OData $filter represented by to . + /// $filter=Name eq 'Sam' + /// |-- $it => $it.Name == "Sam" /// - public interface IFilterBinder - { - /// - /// Translates an OData $filter represented by to . - /// $filter=Name eq 'Sam' - /// |-- $it => $it.Name == "Sam" - /// - /// The filter clause. - /// The query binder context. - /// The filter binder result. - /// reconsider to return "LambdaExpression"? - Expression BindFilter(FilterClause filterClause, QueryBinderContext context); - } + /// The filter clause. + /// The query binder context. + /// The filter binder result. + /// reconsider to return "LambdaExpression"? + Expression BindFilter(FilterClause filterClause, QueryBinderContext context); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/IOrderByBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/IOrderByBinder.cs index ad1c11f91..fd1f2e3fa 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/IOrderByBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/IOrderByBinder.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -8,23 +8,22 @@ using System.Linq.Expressions; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// Exposes the ability to translate an OData $orderby represented by to the +/// wrappered in . +/// +public interface IOrderByBinder { /// - /// Exposes the ability to translate an OData $orderby represented by to the - /// wrappered in . + /// Translates an OData $orderby represented by to . + /// $orderby=Age,Name + /// |-- x => x.Age + /// |-- x => x.Name /// - public interface IOrderByBinder - { - /// - /// Translates an OData $orderby represented by to . - /// $orderby=Age,Name - /// |-- x => x.Age - /// |-- x => x.Name - /// - /// The orderby clause. - /// The query binder context. - /// The OrderBy binder result, . - OrderByBinderResult BindOrderBy(OrderByClause orderByClause, QueryBinderContext context); - } + /// The orderby clause. + /// The query binder context. + /// The OrderBy binder result, . + OrderByBinderResult BindOrderBy(OrderByClause orderByClause, QueryBinderContext context); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/ISearchBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/ISearchBinder.cs index e86f1efe6..42e5d73af 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/ISearchBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/ISearchBinder.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -8,24 +8,23 @@ using System.Linq.Expressions; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// Exposes the ability to translate an OData $search represented by to the . +/// The $search system query option restricts the result to include only those items matching the specified search expression. +/// The definition of what it means to match is dependent upon the implementation. +/// Therefore, there's no default implementation of $search binder. +/// Developer should implement this interface and inject the search binder into service collection. +/// +public interface ISearchBinder { /// - /// Exposes the ability to translate an OData $search represented by to the . - /// The $search system query option restricts the result to include only those items matching the specified search expression. - /// The definition of what it means to match is dependent upon the implementation. - /// Therefore, there's no default implementation of $search binder. - /// Developer should implement this interface and inject the search binder into service collection. + /// Translates an OData $search represented by to . + /// ~/Products?$search=mountain AND bike /// - public interface ISearchBinder - { - /// - /// Translates an OData $search represented by to . - /// ~/Products?$search=mountain AND bike - /// - /// The search clause. - /// The query binder context. - /// The search clause binder result. It should be a lambda expression. - Expression BindSearch(SearchClause searchClause, QueryBinderContext context); - } + /// The search clause. + /// The query binder context. + /// The search clause binder result. It should be a lambda expression. + Expression BindSearch(SearchClause searchClause, QueryBinderContext context); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/ISelectExpandBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/ISelectExpandBinder.cs index 0adab906f..c37d02b4c 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/ISelectExpandBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/ISelectExpandBinder.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -8,22 +8,21 @@ using System.Linq.Expressions; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// Exposes the ability to translate an OData $select or $expand parse tree represented by to +/// an . +/// +public interface ISelectExpandBinder { /// - /// Exposes the ability to translate an OData $select or $expand parse tree represented by to - /// an . + /// Translate an OData $select or $expand tree represented by to an . + /// $select=Name&$expand=Orders + /// $it => new { .... } /// - public interface ISelectExpandBinder - { - /// - /// Translate an OData $select or $expand tree represented by to an . - /// $select=Name&$expand=Orders - /// $it => new { .... } - /// - /// The original . - /// An instance of the . - /// The $select and $expand binder result. - Expression BindSelectExpand(SelectExpandClause selectExpandClause, QueryBinderContext context); - } + /// The original . + /// An instance of the . + /// The $select and $expand binder result. + Expression BindSelectExpand(SelectExpandClause selectExpandClause, QueryBinderContext context); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/Linq2ObjectsComparisonMethods.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/Linq2ObjectsComparisonMethods.cs index e5d74bac6..0dc00ce4f 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/Linq2ObjectsComparisonMethods.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/Linq2ObjectsComparisonMethods.cs @@ -8,57 +8,56 @@ using System; using System.Reflection; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +internal static class Linq2ObjectsComparisonMethods { - internal static class Linq2ObjectsComparisonMethods + /// Method info for byte array comparison. + public static readonly MethodInfo AreByteArraysEqualMethodInfo = + typeof(Linq2ObjectsComparisonMethods).GetMethod("AreByteArraysEqual"); + + /// Method info for byte array comparison. + public static readonly MethodInfo AreByteArraysNotEqualMethodInfo = + typeof(Linq2ObjectsComparisonMethods).GetMethod("AreByteArraysNotEqual"); + + /// Compares two byte arrays for equality. + /// First byte array. + /// Second byte array. + /// true if the arrays are equal; false otherwise. + public static bool AreByteArraysEqual(byte[] left, byte[] right) { - /// Method info for byte array comparison. - public static readonly MethodInfo AreByteArraysEqualMethodInfo = - typeof(Linq2ObjectsComparisonMethods).GetMethod("AreByteArraysEqual"); - - /// Method info for byte array comparison. - public static readonly MethodInfo AreByteArraysNotEqualMethodInfo = - typeof(Linq2ObjectsComparisonMethods).GetMethod("AreByteArraysNotEqual"); + if (Object.ReferenceEquals(left, right)) + { + return true; + } - /// Compares two byte arrays for equality. - /// First byte array. - /// Second byte array. - /// true if the arrays are equal; false otherwise. - public static bool AreByteArraysEqual(byte[] left, byte[] right) + if (left == null || right == null) { - if (Object.ReferenceEquals(left, right)) - { - return true; - } + return false; + } - if (left == null || right == null) - { - return false; - } + if (left.Length != right.Length) + { + return false; + } - if (left.Length != right.Length) + for (int i = 0; i < left.Length; i++) + { + if (left[i] != right[i]) { return false; } - - for (int i = 0; i < left.Length; i++) - { - if (left[i] != right[i]) - { - return false; - } - } - - return true; } - /// Compares two byte arrays for equality. - /// First byte array. - /// Second byte array. - /// true if the arrays are not equal; false otherwise. - public static bool AreByteArraysNotEqual(byte[] left, byte[] right) - { - return !AreByteArraysEqual(left, right); - } + return true; + } + + /// Compares two byte arrays for equality. + /// First byte array. + /// Second byte array. + /// true if the arrays are not equal; false otherwise. + public static bool AreByteArraysNotEqual(byte[] left, byte[] right) + { + return !AreByteArraysEqual(left, right); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/OrderByBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/OrderByBinder.cs index 3e4ddc7f1..b3cd3b364 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/OrderByBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/OrderByBinder.cs @@ -9,60 +9,59 @@ using System.Linq.Expressions; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// The default implementation to bind an OData $orderby represented by +/// an wrappered in . +/// +public class OrderByBinder : QueryBinder, IOrderByBinder { /// - /// The default implementation to bind an OData $orderby represented by - /// an wrappered in . + /// Translates an OData $orderby represented by to . + /// $orderby=Age + /// |-- x => x.Age /// - public class OrderByBinder : QueryBinder, IOrderByBinder + /// The orderby clause. + /// The query binder context. + /// The OrderBy binder result, . + public virtual OrderByBinderResult BindOrderBy(OrderByClause orderByClause, QueryBinderContext context) { - /// - /// Translates an OData $orderby represented by to . - /// $orderby=Age - /// |-- x => x.Age - /// - /// The orderby clause. - /// The query binder context. - /// The OrderBy binder result, . - public virtual OrderByBinderResult BindOrderBy(OrderByClause orderByClause, QueryBinderContext context) + if (orderByClause == null) { - if (orderByClause == null) - { - throw Error.ArgumentNull(nameof(orderByClause)); - } + throw Error.ArgumentNull(nameof(orderByClause)); + } - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - OrderByBinderResult head = null; - OrderByBinderResult last = null; - for (OrderByClause clause = orderByClause; clause != null; clause = clause.ThenBy) - { - Expression body = Bind(clause.Expression, context); + OrderByBinderResult head = null; + OrderByBinderResult last = null; + for (OrderByClause clause = orderByClause; clause != null; clause = clause.ThenBy) + { + Expression body = Bind(clause.Expression, context); - ParameterExpression parameter = context.CurrentParameter; + ParameterExpression parameter = context.CurrentParameter; - LambdaExpression orderByLambda = Expression.Lambda(body, parameter); + LambdaExpression orderByLambda = Expression.Lambda(body, parameter); - OrderByBinderResult result = new OrderByBinderResult(orderByLambda, clause.Direction); + OrderByBinderResult result = new OrderByBinderResult(orderByLambda, clause.Direction); - if (head == null) - { - head = result; - last = result; - } - else - { - Contract.Assert(last != null); - last.ThenBy = result; - last = result; - } + if (head == null) + { + head = result; + last = result; + } + else + { + Contract.Assert(last != null); + last.ThenBy = result; + last = result; } - - return head; } + + return head; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/OrderByBinderResult.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/OrderByBinderResult.cs index 2ea559f94..f3f3edc26 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/OrderByBinderResult.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/OrderByBinderResult.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -8,37 +8,36 @@ using System.Linq.Expressions; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// Represents a single order by expression in the $orderby clause. +/// +public class OrderByBinderResult { /// - /// Represents a single order by expression in the $orderby clause. + /// Initializes a new instance of the class. /// - public class OrderByBinderResult + /// The orderby expression. + /// The orderby direction. + public OrderByBinderResult(Expression orderByExpression, OrderByDirection direction) { - /// - /// Initializes a new instance of the class. - /// - /// The orderby expression. - /// The orderby direction. - public OrderByBinderResult(Expression orderByExpression, OrderByDirection direction) - { - OrderByExpression = orderByExpression ?? throw Error.ArgumentNull(nameof(orderByExpression)); - Direction = direction; - } + OrderByExpression = orderByExpression ?? throw Error.ArgumentNull(nameof(orderByExpression)); + Direction = direction; + } - /// - /// Gets the orderby expression. - /// - public Expression OrderByExpression { get; } + /// + /// Gets the orderby expression. + /// + public Expression OrderByExpression { get; } - /// - /// Gets the orderby direction. - /// - public OrderByDirection Direction { get; } + /// + /// Gets the orderby direction. + /// + public OrderByDirection Direction { get; } - /// - /// Gets or sets the thenby result. - /// - public OrderByBinderResult ThenBy { get; set; } - } + /// + /// Gets or sets the thenby result. + /// + public OrderByBinderResult ThenBy { get; set; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs index 9d9df0a4c..3a6ecfc0a 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs @@ -19,607 +19,571 @@ using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// The base class for all expression binders. +/// +public abstract partial class QueryBinder { /// - /// The base class for all expression binders. + /// Binds a to create a LINQ that + /// represents the semantics of the . /// - public abstract partial class QueryBinder + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindSingleValueFunctionCallNode(SingleValueFunctionCallNode node, QueryBinderContext context) { - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindSingleValueFunctionCallNode(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context); + CheckArgumentNull(node, context); - switch (node.Name) - { - case ClrCanonicalFunctions.StartswithFunctionName: - return BindStartsWith(node, context); + switch (node.Name) + { + case ClrCanonicalFunctions.StartswithFunctionName: + return BindStartsWith(node, context); - case ClrCanonicalFunctions.EndswithFunctionName: - return BindEndsWith(node, context); + case ClrCanonicalFunctions.EndswithFunctionName: + return BindEndsWith(node, context); - case ClrCanonicalFunctions.ContainsFunctionName: - return BindContains(node, context); + case ClrCanonicalFunctions.ContainsFunctionName: + return BindContains(node, context); - case ClrCanonicalFunctions.SubstringFunctionName: - return BindSubstring(node, context); + case ClrCanonicalFunctions.SubstringFunctionName: + return BindSubstring(node, context); - case ClrCanonicalFunctions.LengthFunctionName: - return BindLength(node, context); + case ClrCanonicalFunctions.LengthFunctionName: + return BindLength(node, context); - case ClrCanonicalFunctions.IndexofFunctionName: - return BindIndexOf(node, context); + case ClrCanonicalFunctions.IndexofFunctionName: + return BindIndexOf(node, context); - case ClrCanonicalFunctions.TolowerFunctionName: - return BindToLower(node, context); + case ClrCanonicalFunctions.TolowerFunctionName: + return BindToLower(node, context); - case ClrCanonicalFunctions.ToupperFunctionName: - return BindToUpper(node, context); + case ClrCanonicalFunctions.ToupperFunctionName: + return BindToUpper(node, context); - case ClrCanonicalFunctions.TrimFunctionName: - return BindTrim(node, context); + case ClrCanonicalFunctions.TrimFunctionName: + return BindTrim(node, context); - case ClrCanonicalFunctions.ConcatFunctionName: - return BindConcat(node, context); + case ClrCanonicalFunctions.ConcatFunctionName: + return BindConcat(node, context); - case ClrCanonicalFunctions.MatchesPatternFunctionName: - return BindMatchesPattern(node, context); + case ClrCanonicalFunctions.MatchesPatternFunctionName: + return BindMatchesPattern(node, context); - case ClrCanonicalFunctions.YearFunctionName: - case ClrCanonicalFunctions.MonthFunctionName: - case ClrCanonicalFunctions.DayFunctionName: - return BindDateRelatedProperty(node, context); // Date & DateTime & DateTimeOffset + case ClrCanonicalFunctions.YearFunctionName: + case ClrCanonicalFunctions.MonthFunctionName: + case ClrCanonicalFunctions.DayFunctionName: + return BindDateRelatedProperty(node, context); // Date & DateTime & DateTimeOffset - case ClrCanonicalFunctions.HourFunctionName: - case ClrCanonicalFunctions.MinuteFunctionName: - case ClrCanonicalFunctions.SecondFunctionName: - return BindTimeRelatedProperty(node, context); // TimeOfDay & DateTime & DateTimeOffset + case ClrCanonicalFunctions.HourFunctionName: + case ClrCanonicalFunctions.MinuteFunctionName: + case ClrCanonicalFunctions.SecondFunctionName: + return BindTimeRelatedProperty(node, context); // TimeOfDay & DateTime & DateTimeOffset - case ClrCanonicalFunctions.FractionalSecondsFunctionName: - return BindFractionalSeconds(node, context); + case ClrCanonicalFunctions.FractionalSecondsFunctionName: + return BindFractionalSeconds(node, context); - case ClrCanonicalFunctions.RoundFunctionName: - return BindRound(node, context); + case ClrCanonicalFunctions.RoundFunctionName: + return BindRound(node, context); - case ClrCanonicalFunctions.FloorFunctionName: - return BindFloor(node, context); + case ClrCanonicalFunctions.FloorFunctionName: + return BindFloor(node, context); - case ClrCanonicalFunctions.CeilingFunctionName: - return BindCeiling(node, context); + case ClrCanonicalFunctions.CeilingFunctionName: + return BindCeiling(node, context); - case ClrCanonicalFunctions.CastFunctionName: - return BindCastSingleValue(node, context); + case ClrCanonicalFunctions.CastFunctionName: + return BindCastSingleValue(node, context); - case ClrCanonicalFunctions.IsofFunctionName: - return BindIsOf(node, context); + case ClrCanonicalFunctions.IsofFunctionName: + return BindIsOf(node, context); - case ClrCanonicalFunctions.DateFunctionName: - return BindDate(node, context); + case ClrCanonicalFunctions.DateFunctionName: + return BindDate(node, context); - case ClrCanonicalFunctions.TimeFunctionName: - return BindTime(node, context); + case ClrCanonicalFunctions.TimeFunctionName: + return BindTime(node, context); - case ClrCanonicalFunctions.NowFunctionName: - return BindNow(node, context); + case ClrCanonicalFunctions.NowFunctionName: + return BindNow(node, context); - default: - // Get Expression of custom binded method. - Expression expression = BindCustomMethodExpressionOrNull(node, context); - if (expression != null) - { - return expression; - } + default: + // Get Expression of custom binded method. + Expression expression = BindCustomMethodExpressionOrNull(node, context); + if (expression != null) + { + return expression; + } - throw new NotImplementedException(Error.Format(SRResources.ODataFunctionNotSupported, node.Name)); - } + throw new NotImplementedException(Error.Format(SRResources.ODataFunctionNotSupported, node.Name)); } + } - /// - /// Binds a 'startswith' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindStartsWith(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "startswith"); + /// + /// Binds a 'startswith' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindStartsWith(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "startswith"); - Expression[] arguments = BindArguments(node.Parameters, context); - ValidateAllStringArguments(node.Name, arguments); + Expression[] arguments = BindArguments(node.Parameters, context); + ValidateAllStringArguments(node.Name, arguments); - Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.StartsWith, context.QuerySettings, arguments); - } + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.StartsWith, context.QuerySettings, arguments); + } - /// - /// Binds a 'endswith' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindEndsWith(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "endswith"); + /// + /// Binds a 'endswith' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindEndsWith(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "endswith"); - Expression[] arguments = BindArguments(node.Parameters, context); - ValidateAllStringArguments(node.Name, arguments); + Expression[] arguments = BindArguments(node.Parameters, context); + ValidateAllStringArguments(node.Name, arguments); - Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.EndsWith, context.QuerySettings, arguments); - } + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.EndsWith, context.QuerySettings, arguments); + } - /// - /// Binds a 'contains' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindContains(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "contains"); + /// + /// Binds a 'contains' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindContains(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "contains"); + + Expression[] arguments = BindArguments(node.Parameters, context); + ValidateAllStringArguments(node.Name, arguments); - Expression[] arguments = BindArguments(node.Parameters, context); - ValidateAllStringArguments(node.Name, arguments); + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); - Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Contains, context.QuerySettings, arguments[0], arguments[1]); + } + + /// + /// Binds a 'substring' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindSubstring(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "substring"); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Contains, context.QuerySettings, arguments[0], arguments[1]); + Expression[] arguments = BindArguments(node.Parameters, context); + if (arguments[0].Type != typeof(string)) + { + throw new ODataException(Error.Format(SRResources.FunctionNotSupportedOnEnum, node.Name)); } - /// - /// Binds a 'substring' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindSubstring(SingleValueFunctionCallNode node, QueryBinderContext context) + ODataQuerySettings querySettings = context.QuerySettings; + + Expression functionCall; + if (arguments.Length == 2) { - CheckArgumentNull(node, context, "substring"); + Contract.Assert(ExpressionBinderHelper.IsInteger(arguments[1].Type)); - Expression[] arguments = BindArguments(node.Parameters, context); - if (arguments[0].Type != typeof(string)) + // When null propagation is allowed, we use a safe version of String.Substring(int). + // But for providers that would not recognize custom expressions like this, we map + // directly to String.Substring(int) + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) { - throw new ODataException(Error.Format(SRResources.FunctionNotSupportedOnEnum, node.Name)); + // Safe function is static and takes string "this" as first argument + functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStartNoThrow, querySettings, arguments); } - - ODataQuerySettings querySettings = context.QuerySettings; - - Expression functionCall; - if (arguments.Length == 2) + else { - Contract.Assert(ExpressionBinderHelper.IsInteger(arguments[1].Type)); + functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStart, querySettings, arguments); + } + } + else + { + // arguments.Length == 3 implies String.Substring(int, int) + Contract.Assert(arguments.Length == 3 && ExpressionBinderHelper.IsInteger(arguments[1].Type) && ExpressionBinderHelper.IsInteger(arguments[2].Type)); - // When null propagation is allowed, we use a safe version of String.Substring(int). - // But for providers that would not recognize custom expressions like this, we map - // directly to String.Substring(int) - if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) - { - // Safe function is static and takes string "this" as first argument - functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStartNoThrow, querySettings, arguments); - } - else - { - functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStart, querySettings, arguments); - } + // When null propagation is allowed, we use a safe version of String.Substring(int, int). + // But for providers that would not recognize custom expressions like this, we map + // directly to String.Substring(int, int) + if (querySettings.HandleNullPropagation == HandleNullPropagationOption.True) + { + // Safe function is static and takes string "this" as first argument + functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStartAndLengthNoThrow, querySettings, arguments); } else { - // arguments.Length == 3 implies String.Substring(int, int) - Contract.Assert(arguments.Length == 3 && ExpressionBinderHelper.IsInteger(arguments[1].Type) && ExpressionBinderHelper.IsInteger(arguments[2].Type)); - - // When null propagation is allowed, we use a safe version of String.Substring(int, int). - // But for providers that would not recognize custom expressions like this, we map - // directly to String.Substring(int, int) - if (querySettings.HandleNullPropagation == HandleNullPropagationOption.True) - { - // Safe function is static and takes string "this" as first argument - functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStartAndLengthNoThrow, querySettings, arguments); - } - else - { - functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStartAndLength, querySettings, arguments); - } + functionCall = ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.SubstringStartAndLength, querySettings, arguments); } - - return functionCall; } - /// - /// Binds a 'length' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindLength(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "length"); + return functionCall; + } - Expression[] arguments = BindArguments(node.Parameters, context); - ValidateAllStringArguments(node.Name, arguments); + /// + /// Binds a 'length' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindLength(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "length"); - Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); + Expression[] arguments = BindArguments(node.Parameters, context); + ValidateAllStringArguments(node.Name, arguments); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Length, context.QuerySettings, arguments); - } + Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); - /// - /// Binds a 'indexof' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindIndexOf(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "indexof"); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Length, context.QuerySettings, arguments); + } - Expression[] arguments = BindArguments(node.Parameters, context); - ValidateAllStringArguments(node.Name, arguments); + /// + /// Binds a 'indexof' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindIndexOf(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "indexof"); - Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + Expression[] arguments = BindArguments(node.Parameters, context); + ValidateAllStringArguments(node.Name, arguments); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.IndexOf, context.QuerySettings, arguments); - } + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); - /// - /// Binds a 'tolower' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindToLower(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "tolower"); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.IndexOf, context.QuerySettings, arguments); + } - Expression[] arguments = BindArguments(node.Parameters, context); - ValidateAllStringArguments(node.Name, arguments); + /// + /// Binds a 'tolower' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindToLower(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "tolower"); - Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); + Expression[] arguments = BindArguments(node.Parameters, context); + ValidateAllStringArguments(node.Name, arguments); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.ToLower, context.QuerySettings, arguments); - } + Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); - /// - /// Binds a 'toupper' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindToUpper(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "toupper"); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.ToLower, context.QuerySettings, arguments); + } - Expression[] arguments = BindArguments(node.Parameters, context); - ValidateAllStringArguments(node.Name, arguments); + /// + /// Binds a 'toupper' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindToUpper(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "toupper"); - Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); + Expression[] arguments = BindArguments(node.Parameters, context); + ValidateAllStringArguments(node.Name, arguments); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.ToUpper, context.QuerySettings, arguments); - } + Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); - /// - /// Binds a 'trim' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindTrim(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "trim"); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.ToUpper, context.QuerySettings, arguments); + } - Expression[] arguments = BindArguments(node.Parameters, context); - ValidateAllStringArguments(node.Name, arguments); + /// + /// Binds a 'trim' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindTrim(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "trim"); - Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); + Expression[] arguments = BindArguments(node.Parameters, context); + ValidateAllStringArguments(node.Name, arguments); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Trim, context.QuerySettings, arguments); - } + Contract.Assert(arguments.Length == 1 && arguments[0].Type == typeof(string)); - /// - /// Binds a 'concat' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindConcat(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "concat"); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Trim, context.QuerySettings, arguments); + } - Expression[] arguments = BindArguments(node.Parameters, context); - ValidateAllStringArguments(node.Name, arguments); + /// + /// Binds a 'concat' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindConcat(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "concat"); - Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + Expression[] arguments = BindArguments(node.Parameters, context); + ValidateAllStringArguments(node.Name, arguments); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Concat, context.QuerySettings, arguments); - } + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); - /// - /// Binds a 'matchesPattern' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindMatchesPattern(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "matchesPattern"); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.Concat, context.QuerySettings, arguments); + } - Expression[] arguments = BindArguments(node.Parameters, context); - ValidateAllStringArguments(node.Name, arguments); + /// + /// Binds a 'matchesPattern' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindMatchesPattern(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "matchesPattern"); - Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); + Expression[] arguments = BindArguments(node.Parameters, context); + ValidateAllStringArguments(node.Name, arguments); - //add argument that must be ECMAScript compatible regex - arguments = new[] { arguments[0], arguments[1], Expression.Constant(RegexOptions.ECMAScript) }; + Contract.Assert(arguments.Length == 2 && arguments[0].Type == typeof(string) && arguments[1].Type == typeof(string)); - return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.MatchesMattern, context.QuerySettings, arguments); - } + //add argument that must be ECMAScript compatible regex + arguments = new[] { arguments[0], arguments[1], Expression.Constant(RegexOptions.ECMAScript) }; - /// - /// Binds date related functions to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindDateRelatedProperty(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context); + return ExpressionBinderHelper.MakeFunctionCall(ClrCanonicalFunctions.MatchesMattern, context.QuerySettings, arguments); + } - Expression[] arguments = BindArguments(node.Parameters, context); - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateRelated(arguments[0].Type)); + /// + /// Binds date related functions to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindDateRelatedProperty(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context); - // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. - Expression parameter = arguments[0]; + Expression[] arguments = BindArguments(node.Parameters, context); + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateRelated(arguments[0].Type)); - PropertyInfo property; - if (ExpressionBinderHelper.IsDate(parameter.Type)) - { - Contract.Assert(ClrCanonicalFunctions.DateProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.DateProperties[node.Name]; - } - else if (parameter.Type.IsDateOnly()) - { - Contract.Assert(ClrCanonicalFunctions.DateOnlyProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.DateOnlyProperties[node.Name]; - } - else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) - { - Contract.Assert(ClrCanonicalFunctions.DateTimeProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.DateTimeProperties[node.Name]; - } - else - { - Contract.Assert(ClrCanonicalFunctions.DateTimeOffsetProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.DateTimeOffsetProperties[node.Name]; - } + // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. + Expression parameter = arguments[0]; - return ExpressionBinderHelper.MakeFunctionCall(property, context.QuerySettings, parameter); + PropertyInfo property; + if (ExpressionBinderHelper.IsDate(parameter.Type)) + { + Contract.Assert(ClrCanonicalFunctions.DateProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.DateProperties[node.Name]; } - - /// - /// Binds time related functions to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindTimeRelatedProperty(SingleValueFunctionCallNode node, QueryBinderContext context) + else if (parameter.Type.IsDateOnly()) { - CheckArgumentNull(node, context); + Contract.Assert(ClrCanonicalFunctions.DateOnlyProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.DateOnlyProperties[node.Name]; + } + else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) + { + Contract.Assert(ClrCanonicalFunctions.DateTimeProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.DateTimeProperties[node.Name]; + } + else + { + Contract.Assert(ClrCanonicalFunctions.DateTimeOffsetProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.DateTimeOffsetProperties[node.Name]; + } + + return ExpressionBinderHelper.MakeFunctionCall(property, context.QuerySettings, parameter); + } - Expression[] arguments = BindArguments(node.Parameters, context); + /// + /// Binds time related functions to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindTimeRelatedProperty(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context); - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsTimeRelated(arguments[0].Type)); + Expression[] arguments = BindArguments(node.Parameters, context); - // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. - Expression parameter = arguments[0]; + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsTimeRelated(arguments[0].Type)); - PropertyInfo property; - if (ExpressionBinderHelper.IsTimeOfDay(parameter.Type)) - { - Contract.Assert(ClrCanonicalFunctions.TimeOfDayProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.TimeOfDayProperties[node.Name]; - } - else if (parameter.Type.IsTimeOnly()) - { - Contract.Assert(ClrCanonicalFunctions.TimeOnlyProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.TimeOnlyProperties[node.Name]; - } - else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) - { - Contract.Assert(ClrCanonicalFunctions.DateTimeProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.DateTimeProperties[node.Name]; - } - else if (ExpressionBinderHelper.IsTimeSpan(parameter.Type)) - { - Contract.Assert(ClrCanonicalFunctions.TimeSpanProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.TimeSpanProperties[node.Name]; - } - else - { - Contract.Assert(ClrCanonicalFunctions.DateTimeOffsetProperties.ContainsKey(node.Name)); - property = ClrCanonicalFunctions.DateTimeOffsetProperties[node.Name]; - } + // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. + Expression parameter = arguments[0]; - return ExpressionBinderHelper.MakeFunctionCall(property, context.QuerySettings, parameter); + PropertyInfo property; + if (ExpressionBinderHelper.IsTimeOfDay(parameter.Type)) + { + Contract.Assert(ClrCanonicalFunctions.TimeOfDayProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.TimeOfDayProperties[node.Name]; } - - /// - /// Binds 'fractionalseconds' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindFractionalSeconds(SingleValueFunctionCallNode node, QueryBinderContext context) + else if (parameter.Type.IsTimeOnly()) { - CheckArgumentNull(node, context, "fractionalseconds"); + Contract.Assert(ClrCanonicalFunctions.TimeOnlyProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.TimeOnlyProperties[node.Name]; + } + else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) + { + Contract.Assert(ClrCanonicalFunctions.DateTimeProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.DateTimeProperties[node.Name]; + } + else if (ExpressionBinderHelper.IsTimeSpan(parameter.Type)) + { + Contract.Assert(ClrCanonicalFunctions.TimeSpanProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.TimeSpanProperties[node.Name]; + } + else + { + Contract.Assert(ClrCanonicalFunctions.DateTimeOffsetProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.DateTimeOffsetProperties[node.Name]; + } - Expression[] arguments = BindArguments(node.Parameters, context); - Contract.Assert(arguments.Length == 1 && (ExpressionBinderHelper.IsTimeRelated(arguments[0].Type))); + return ExpressionBinderHelper.MakeFunctionCall(property, context.QuerySettings, parameter); + } - // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. - Expression parameter = arguments[0]; + /// + /// Binds 'fractionalseconds' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindFractionalSeconds(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "fractionalseconds"); - PropertyInfo property; - if (ExpressionBinderHelper.IsTimeOfDay(parameter.Type)) - { - property = ClrCanonicalFunctions.TimeOfDayProperties[ClrCanonicalFunctions.MillisecondFunctionName]; - } - else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) - { - property = ClrCanonicalFunctions.DateTimeProperties[ClrCanonicalFunctions.MillisecondFunctionName]; - } - else if (ExpressionBinderHelper.IsTimeSpan(parameter.Type)) - { - property = ClrCanonicalFunctions.TimeSpanProperties[ClrCanonicalFunctions.MillisecondFunctionName]; - } - else - { - property = ClrCanonicalFunctions.DateTimeOffsetProperties[ClrCanonicalFunctions.MillisecondFunctionName]; - } + Expression[] arguments = BindArguments(node.Parameters, context); + Contract.Assert(arguments.Length == 1 && (ExpressionBinderHelper.IsTimeRelated(arguments[0].Type))); - // Millisecond - Expression milliSecond = ExpressionBinderHelper.MakePropertyAccess(property, parameter, context.QuerySettings); - Expression decimalMilliSecond = Expression.Convert(milliSecond, typeof(decimal)); - Expression fractionalSeconds = Expression.Divide(decimalMilliSecond, Expression.Constant(1000m, typeof(decimal))); + // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. + Expression parameter = arguments[0]; - return ExpressionBinderHelper.CreateFunctionCallWithNullPropagation(fractionalSeconds, arguments, context.QuerySettings); + PropertyInfo property; + if (ExpressionBinderHelper.IsTimeOfDay(parameter.Type)) + { + property = ClrCanonicalFunctions.TimeOfDayProperties[ClrCanonicalFunctions.MillisecondFunctionName]; } - - /// - /// Binds 'round' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindRound(SingleValueFunctionCallNode node, QueryBinderContext context) + else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) + { + property = ClrCanonicalFunctions.DateTimeProperties[ClrCanonicalFunctions.MillisecondFunctionName]; + } + else if (ExpressionBinderHelper.IsTimeSpan(parameter.Type)) + { + property = ClrCanonicalFunctions.TimeSpanProperties[ClrCanonicalFunctions.MillisecondFunctionName]; + } + else { - CheckArgumentNull(node, context, "round"); + property = ClrCanonicalFunctions.DateTimeOffsetProperties[ClrCanonicalFunctions.MillisecondFunctionName]; + } - Expression[] arguments = BindArguments(node.Parameters, context); + // Millisecond + Expression milliSecond = ExpressionBinderHelper.MakePropertyAccess(property, parameter, context.QuerySettings); + Expression decimalMilliSecond = Expression.Convert(milliSecond, typeof(decimal)); + Expression fractionalSeconds = Expression.Divide(decimalMilliSecond, Expression.Constant(1000m, typeof(decimal))); - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDoubleOrDecimal(arguments[0].Type)); + return ExpressionBinderHelper.CreateFunctionCallWithNullPropagation(fractionalSeconds, arguments, context.QuerySettings); + } - MethodInfo round = ExpressionBinderHelper.IsType(arguments[0].Type) - ? ClrCanonicalFunctions.RoundOfDouble - : ClrCanonicalFunctions.RoundOfDecimal; - return ExpressionBinderHelper.MakeFunctionCall(round, context.QuerySettings, arguments); - } + /// + /// Binds 'round' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindRound(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "round"); - /// - /// Binds 'floor' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindFloor(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "floor"); + Expression[] arguments = BindArguments(node.Parameters, context); - Expression[] arguments = BindArguments(node.Parameters, context); + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDoubleOrDecimal(arguments[0].Type)); - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDoubleOrDecimal(arguments[0].Type)); + MethodInfo round = ExpressionBinderHelper.IsType(arguments[0].Type) + ? ClrCanonicalFunctions.RoundOfDouble + : ClrCanonicalFunctions.RoundOfDecimal; + return ExpressionBinderHelper.MakeFunctionCall(round, context.QuerySettings, arguments); + } - MethodInfo floor = ExpressionBinderHelper.IsType(arguments[0].Type) - ? ClrCanonicalFunctions.FloorOfDouble - : ClrCanonicalFunctions.FloorOfDecimal; - return ExpressionBinderHelper.MakeFunctionCall(floor, context.QuerySettings, arguments); - } + /// + /// Binds 'floor' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindFloor(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "floor"); - /// - /// Binds 'ceiling' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindCeiling(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "ceiling"); + Expression[] arguments = BindArguments(node.Parameters, context); - Expression[] arguments = BindArguments(node.Parameters, context); + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDoubleOrDecimal(arguments[0].Type)); - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDoubleOrDecimal(arguments[0].Type)); + MethodInfo floor = ExpressionBinderHelper.IsType(arguments[0].Type) + ? ClrCanonicalFunctions.FloorOfDouble + : ClrCanonicalFunctions.FloorOfDecimal; + return ExpressionBinderHelper.MakeFunctionCall(floor, context.QuerySettings, arguments); + } - MethodInfo ceiling = ExpressionBinderHelper.IsType(arguments[0].Type) - ? ClrCanonicalFunctions.CeilingOfDouble - : ClrCanonicalFunctions.CeilingOfDecimal; - return ExpressionBinderHelper.MakeFunctionCall(ceiling, context.QuerySettings, arguments); - } + /// + /// Binds 'ceiling' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindCeiling(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "ceiling"); - /// - /// Binds 'cast' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindCastSingleValue(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "cast"); + Expression[] arguments = BindArguments(node.Parameters, context); - Expression[] arguments = BindArguments(node.Parameters, context); - Contract.Assert(arguments.Length == 1 || arguments.Length == 2); + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDoubleOrDecimal(arguments[0].Type)); - Expression source = arguments.Length == 1 ? context.CurrentParameter : arguments[0]; - string targetTypeName = (string)((ConstantNode)node.Parameters.Last()).Value; - IEdmType targetEdmType = context.Model.FindType(targetTypeName); - Type targetClrType = null; + MethodInfo ceiling = ExpressionBinderHelper.IsType(arguments[0].Type) + ? ClrCanonicalFunctions.CeilingOfDouble + : ClrCanonicalFunctions.CeilingOfDecimal; + return ExpressionBinderHelper.MakeFunctionCall(ceiling, context.QuerySettings, arguments); + } - if (targetEdmType != null) - { - IEdmTypeReference targetEdmTypeReference = targetEdmType.ToEdmTypeReference(false); - targetClrType = context.Model.GetClrType(targetEdmTypeReference); + /// + /// Binds 'cast' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindCastSingleValue(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "cast"); - if (source != NullConstant) - { - if (source.Type == targetClrType) - { - return source; - } - - if ((!targetEdmTypeReference.IsPrimitive() && !targetEdmTypeReference.IsEnum()) || - (context.Model.GetEdmPrimitiveTypeReference(source.Type) == null && !TypeHelper.IsEnum(source.Type))) - { - // Cast fails and return null. - return NullConstant; - } - } - } + Expression[] arguments = BindArguments(node.Parameters, context); + Contract.Assert(arguments.Length == 1 || arguments.Length == 2); - if (targetClrType == null || source == NullConstant) - { - return NullConstant; - } + Expression source = arguments.Length == 1 ? context.CurrentParameter : arguments[0]; + string targetTypeName = (string)((ConstantNode)node.Parameters.Last()).Value; + IEdmType targetEdmType = context.Model.FindType(targetTypeName); + Type targetClrType = null; - if (targetClrType == typeof(string)) - { - return ExpressionBinderHelper.BindCastToStringType(source); - } - else if (TypeHelper.IsEnum(targetClrType)) - { - return BindCastToEnumType(source.Type, targetClrType, node.Parameters.First(), arguments.Length, context); - } - else + if (targetEdmType != null) + { + IEdmTypeReference targetEdmTypeReference = targetEdmType.ToEdmTypeReference(false); + targetClrType = context.Model.GetClrType(targetEdmTypeReference); + + if (source != NullConstant) { - if (TypeHelper.IsNullable(source.Type) && !TypeHelper.IsNullable(targetClrType)) + if (source.Type == targetClrType) { - // Make the target Clr type nullable to avoid failure while casting - // nullable source, whose value may be null, to a non-nullable type. - // For example: cast(NullableInt32Property,Edm.Int64) - // The target Clr type should be Nullable rather than Int64. - targetClrType = typeof(Nullable<>).MakeGenericType(targetClrType); + return source; } - try - { - return Expression.Convert(source, targetClrType); - } - catch (InvalidOperationException) + if ((!targetEdmTypeReference.IsPrimitive() && !targetEdmTypeReference.IsEnum()) || + (context.Model.GetEdmPrimitiveTypeReference(source.Type) == null && !TypeHelper.IsEnum(source.Type))) { // Cast fails and return null. return NullConstant; @@ -627,153 +591,188 @@ protected virtual Expression BindCastSingleValue(SingleValueFunctionCallNode nod } } - /// - /// Binds a 'isof' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindIsOf(SingleValueFunctionCallNode node, QueryBinderContext context) + if (targetClrType == null || source == NullConstant) { - CheckArgumentNull(node, context, "isof"); - - Expression[] arguments = BindArguments(node.Parameters, context); - - // Edm.Boolean isof(type) or - // Edm.Boolean isof(expression,type) - Contract.Assert(arguments.Length == 1 || arguments.Length == 2); + return NullConstant; + } - Expression source = arguments.Length == 1 ? context.CurrentParameter : arguments[0]; - if (source == NullConstant) + if (targetClrType == typeof(string)) + { + return ExpressionBinderHelper.BindCastToStringType(source); + } + else if (TypeHelper.IsEnum(targetClrType)) + { + return BindCastToEnumType(source.Type, targetClrType, node.Parameters.First(), arguments.Length, context); + } + else + { + if (TypeHelper.IsNullable(source.Type) && !TypeHelper.IsNullable(targetClrType)) { - return FalseConstant; + // Make the target Clr type nullable to avoid failure while casting + // nullable source, whose value may be null, to a non-nullable type. + // For example: cast(NullableInt32Property,Edm.Int64) + // The target Clr type should be Nullable rather than Int64. + targetClrType = typeof(Nullable<>).MakeGenericType(targetClrType); } - string typeName = (string)((ConstantNode)node.Parameters.Last()).Value; - - IEdmType edmType = context.Model.FindType(typeName); - Type clrType = null; - if (edmType != null) + try { - // bool nullable = source.Type.IsNullable(); - IEdmTypeReference edmTypeReference = edmType.ToEdmTypeReference(false); - clrType = context.Model.GetClrType(edmTypeReference); + return Expression.Convert(source, targetClrType); } - - if (clrType == null) + catch (InvalidOperationException) { - return FalseConstant; + // Cast fails and return null. + return NullConstant; } + } + } - bool isSourcePrimitiveOrEnum = context.Model.GetEdmPrimitiveTypeReference(source.Type) != null || - TypeHelper.IsEnum(source.Type); + /// + /// Binds a 'isof' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindIsOf(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "isof"); - bool isTargetPrimitiveOrEnum = context.Model.GetEdmPrimitiveTypeReference(clrType) != null || - TypeHelper.IsEnum(clrType); + Expression[] arguments = BindArguments(node.Parameters, context); - if (isSourcePrimitiveOrEnum && isTargetPrimitiveOrEnum) - { - if (TypeHelper.IsNullable(source.Type)) - { - clrType = TypeHelper.ToNullable(clrType); - } - } + // Edm.Boolean isof(type) or + // Edm.Boolean isof(expression,type) + Contract.Assert(arguments.Length == 1 || arguments.Length == 2); - // Be caution: Type method of LINQ to Entities only supports entity type. - return Expression.Condition(Expression.TypeIs(source, clrType), TrueConstant, FalseConstant); + Expression source = arguments.Length == 1 ? context.CurrentParameter : arguments[0]; + if (source == NullConstant) + { + return FalseConstant; } - /// - /// Binds a 'date' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindDate(SingleValueFunctionCallNode node, QueryBinderContext context) + string typeName = (string)((ConstantNode)node.Parameters.Last()).Value; + + IEdmType edmType = context.Model.FindType(typeName); + Type clrType = null; + if (edmType != null) { - CheckArgumentNull(node, context, "date"); + // bool nullable = source.Type.IsNullable(); + IEdmTypeReference edmTypeReference = edmType.ToEdmTypeReference(false); + clrType = context.Model.GetClrType(edmTypeReference); + } - Expression[] arguments = BindArguments(node.Parameters, context); + if (clrType == null) + { + return FalseConstant; + } - // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateOrOffset(arguments[0].Type)); + bool isSourcePrimitiveOrEnum = context.Model.GetEdmPrimitiveTypeReference(source.Type) != null || + TypeHelper.IsEnum(source.Type); - // EF doesn't support new Date(int, int, int), also doesn't support other property access, for example DateTime.Date. - // Therefore, we just return the source (DateTime or DateTimeOffset). - return arguments[0]; - } + bool isTargetPrimitiveOrEnum = context.Model.GetEdmPrimitiveTypeReference(clrType) != null || + TypeHelper.IsEnum(clrType); - /// - /// Binds a 'time' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindTime(SingleValueFunctionCallNode node, QueryBinderContext context) + if (isSourcePrimitiveOrEnum && isTargetPrimitiveOrEnum) { - CheckArgumentNull(node, context, "time"); + if (TypeHelper.IsNullable(source.Type)) + { + clrType = TypeHelper.ToNullable(clrType); + } + } - Expression[] arguments = BindArguments(node.Parameters, context); + // Be caution: Type method of LINQ to Entities only supports entity type. + return Expression.Condition(Expression.TypeIs(source, clrType), TrueConstant, FalseConstant); + } - // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateOrOffset(arguments[0].Type)); + /// + /// Binds a 'date' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindDate(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "date"); - // EF doesn't support new TimeOfDay(int, int, int, int), also doesn't support other property access, for example DateTimeOffset.DateTime. - // Therefore, we just return the source (DateTime or DateTimeOffset). - return arguments[0]; - } + Expression[] arguments = BindArguments(node.Parameters, context); - /// - /// Binds a 'now' function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindNow(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context, "now"); + // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateOrOffset(arguments[0].Type)); - // Function Now() does not take any arguments. - Expression[] arguments = BindArguments(node.Parameters, context); - Contract.Assert(arguments.Length == 0); + // EF doesn't support new Date(int, int, int), also doesn't support other property access, for example DateTime.Date. + // Therefore, we just return the source (DateTime or DateTimeOffset). + return arguments[0]; + } - return Expression.Property(null, typeof(DateTimeOffset), "UtcNow"); - } + /// + /// Binds a 'time' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindTime(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "time"); - /// - /// Binds customized function to create a LINQ . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - protected virtual Expression BindCustomMethodExpressionOrNull(SingleValueFunctionCallNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context); + Expression[] arguments = BindArguments(node.Parameters, context); - Expression[] arguments = BindArguments(node.Parameters, context); - IEnumerable methodArgumentsType = arguments.Select(argument => argument.Type); + // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateOrOffset(arguments[0].Type)); - // Search for custom method info that are binded to the node name - MethodInfo methodInfo; - if (UriFunctionsBinder.TryGetMethodInfo(node.Name, methodArgumentsType, out methodInfo)) - { - return ExpressionBinderHelper.MakeFunctionCall(methodInfo, context.QuerySettings, arguments); - } + // EF doesn't support new TimeOfDay(int, int, int, int), also doesn't support other property access, for example DateTimeOffset.DateTime. + // Therefore, we just return the source (DateTime or DateTimeOffset). + return arguments[0]; + } + + /// + /// Binds a 'now' function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindNow(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context, "now"); + + // Function Now() does not take any arguments. + Expression[] arguments = BindArguments(node.Parameters, context); + Contract.Assert(arguments.Length == 0); + + return Expression.Property(null, typeof(DateTimeOffset), "UtcNow"); + } + + /// + /// Binds customized function to create a LINQ . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + protected virtual Expression BindCustomMethodExpressionOrNull(SingleValueFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context); + + Expression[] arguments = BindArguments(node.Parameters, context); + IEnumerable methodArgumentsType = arguments.Select(argument => argument.Type); - return null; + // Search for custom method info that are binded to the node name + MethodInfo methodInfo; + if (UriFunctionsBinder.TryGetMethodInfo(node.Name, methodArgumentsType, out methodInfo)) + { + return ExpressionBinderHelper.MakeFunctionCall(methodInfo, context.QuerySettings, arguments); } - private static void CheckArgumentNull(SingleValueFunctionCallNode node, QueryBinderContext context, string nodeName) + return null; + } + + private static void CheckArgumentNull(SingleValueFunctionCallNode node, QueryBinderContext context, string nodeName) + { + if (node == null || node.Name != nodeName) { - if (node == null || node.Name != nodeName) - { - throw Error.ArgumentNull(nameof(node)); - } + throw Error.ArgumentNull(nameof(node)); + } - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs index 48674407c..9b590d4f3 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs @@ -24,383 +24,378 @@ using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// The base class for all expression query binders. +/// +public abstract partial class QueryBinder { + #region Static Properties + internal static readonly string DictionaryStringObjectIndexerName = typeof(Dictionary).GetDefaultMembers()[0].Name; + + internal static readonly Expression NullConstant = Expression.Constant(null); + internal static readonly Expression FalseConstant = Expression.Constant(false); + internal static readonly Expression TrueConstant = Expression.Constant(true); + + // .NET 6 adds a new overload: TryParse(ReadOnlySpan, TEnum) + // Now, with `TryParse(String, TEnum)`, there will have two versions with two parameters + // So, the previous Single() will throw exception. + internal static readonly MethodInfo EnumTryParseMethod = typeof(Enum).GetMethod("TryParse", + new[] + { + typeof(string), + Type.MakeGenericMethodParameter(0).MakeByRefType() + }); + #endregion + + #region Bind methods /// - /// The base class for all expression query binders. + /// Binds a to create a LINQ that represents the semantics of the . /// - public abstract partial class QueryBinder + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression Bind(QueryNode node, QueryBinderContext context) { - #region Static Properties - internal static readonly string DictionaryStringObjectIndexerName = typeof(Dictionary).GetDefaultMembers()[0].Name; - - internal static readonly Expression NullConstant = Expression.Constant(null); - internal static readonly Expression FalseConstant = Expression.Constant(false); - internal static readonly Expression TrueConstant = Expression.Constant(true); - - // .NET 6 adds a new overload: TryParse(ReadOnlySpan, TEnum) - // Now, with `TryParse(String, TEnum)`, there will have two versions with two parameters - // So, the previous Single() will throw exception. - internal static readonly MethodInfo EnumTryParseMethod = typeof(Enum).GetMethod("TryParse", - new[] - { - typeof(string), - Type.MakeGenericMethodParameter(0).MakeByRefType() - }); - #endregion - - #region Bind methods - /// - /// Binds a to create a LINQ that represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression Bind(QueryNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context); - - // Recursion guard to avoid stack overflows - RuntimeHelpers.EnsureSufficientExecutionStack(); - - if (node is CollectionNode collectionNode) - { - return BindCollectionNode(collectionNode, context); - } - else if (node is SingleValueNode singleValueNode) - { - return BindSingleValueNode(singleValueNode, context); - } - else - { - throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, node.Kind, typeof(QueryBinder).Name); - } - } + CheckArgumentNull(node, context); + + // Recursion guard to avoid stack overflows + RuntimeHelpers.EnsureSufficientExecutionStack(); - /// - /// Binds a to create a LINQ that represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindCollectionNode(CollectionNode node, QueryBinderContext context) + if (node is CollectionNode collectionNode) + { + return BindCollectionNode(collectionNode, context); + } + else if (node is SingleValueNode singleValueNode) { - CheckArgumentNull(node, context); + return BindSingleValueNode(singleValueNode, context); + } + else + { + throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, node.Kind, typeof(QueryBinder).Name); + } + } - switch (node.Kind) - { - case QueryNodeKind.CollectionNavigationNode: - CollectionNavigationNode navigationNode = node as CollectionNavigationNode; - return BindNavigationPropertyNode(navigationNode.Source, navigationNode.NavigationProperty, null, context); + /// + /// Binds a to create a LINQ that represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindCollectionNode(CollectionNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context); - case QueryNodeKind.CollectionPropertyAccess: - return BindCollectionPropertyAccessNode(node as CollectionPropertyAccessNode, context); + switch (node.Kind) + { + case QueryNodeKind.CollectionNavigationNode: + CollectionNavigationNode navigationNode = node as CollectionNavigationNode; + return BindNavigationPropertyNode(navigationNode.Source, navigationNode.NavigationProperty, null, context); - case QueryNodeKind.CollectionComplexNode: - return BindCollectionComplexNode(node as CollectionComplexNode, context); + case QueryNodeKind.CollectionPropertyAccess: + return BindCollectionPropertyAccessNode(node as CollectionPropertyAccessNode, context); - case QueryNodeKind.CollectionResourceCast: - return BindCollectionResourceCastNode(node as CollectionResourceCastNode, context); + case QueryNodeKind.CollectionComplexNode: + return BindCollectionComplexNode(node as CollectionComplexNode, context); - case QueryNodeKind.CollectionConstant: - return BindCollectionConstantNode(node as CollectionConstantNode, context); + case QueryNodeKind.CollectionResourceCast: + return BindCollectionResourceCastNode(node as CollectionResourceCastNode, context); - case QueryNodeKind.CollectionFunctionCall: - case QueryNodeKind.CollectionResourceFunctionCall: - case QueryNodeKind.CollectionOpenPropertyAccess: - default: - throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, node.Kind, typeof(QueryBinder).Name); - } + case QueryNodeKind.CollectionConstant: + return BindCollectionConstantNode(node as CollectionConstantNode, context); + + case QueryNodeKind.CollectionFunctionCall: + case QueryNodeKind.CollectionResourceFunctionCall: + case QueryNodeKind.CollectionOpenPropertyAccess: + default: + throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, node.Kind, typeof(QueryBinder).Name); } + } - /// - /// Binds a to create a LINQ that represents the semantics of the . - /// - /// The node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindSingleValueNode(SingleValueNode node, QueryBinderContext context) - { - CheckArgumentNull(node, context); + /// + /// Binds a to create a LINQ that represents the semantics of the . + /// + /// The node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindSingleValueNode(SingleValueNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context); - switch (node.Kind) - { - case QueryNodeKind.BinaryOperator: - return BindBinaryOperatorNode(node as BinaryOperatorNode, context); + switch (node.Kind) + { + case QueryNodeKind.BinaryOperator: + return BindBinaryOperatorNode(node as BinaryOperatorNode, context); - case QueryNodeKind.Constant: - return BindConstantNode(node as ConstantNode, context); + case QueryNodeKind.Constant: + return BindConstantNode(node as ConstantNode, context); - case QueryNodeKind.Convert: - return BindConvertNode(node as ConvertNode, context); + case QueryNodeKind.Convert: + return BindConvertNode(node as ConvertNode, context); - case QueryNodeKind.ResourceRangeVariableReference: - return BindRangeVariable((node as ResourceRangeVariableReferenceNode).RangeVariable, context); + case QueryNodeKind.ResourceRangeVariableReference: + return BindRangeVariable((node as ResourceRangeVariableReferenceNode).RangeVariable, context); - case QueryNodeKind.NonResourceRangeVariableReference: - return BindRangeVariable((node as NonResourceRangeVariableReferenceNode).RangeVariable, context); + case QueryNodeKind.NonResourceRangeVariableReference: + return BindRangeVariable((node as NonResourceRangeVariableReferenceNode).RangeVariable, context); - case QueryNodeKind.SingleValuePropertyAccess: - return BindPropertyAccessQueryNode(node as SingleValuePropertyAccessNode, context); + case QueryNodeKind.SingleValuePropertyAccess: + return BindPropertyAccessQueryNode(node as SingleValuePropertyAccessNode, context); - case QueryNodeKind.SingleComplexNode: - return BindSingleComplexNode(node as SingleComplexNode, context); + case QueryNodeKind.SingleComplexNode: + return BindSingleComplexNode(node as SingleComplexNode, context); - case QueryNodeKind.SingleValueOpenPropertyAccess: - return BindDynamicPropertyAccessQueryNode(node as SingleValueOpenPropertyAccessNode, context); + case QueryNodeKind.SingleValueOpenPropertyAccess: + return BindDynamicPropertyAccessQueryNode(node as SingleValueOpenPropertyAccessNode, context); - case QueryNodeKind.UnaryOperator: - return BindUnaryOperatorNode(node as UnaryOperatorNode, context); + case QueryNodeKind.UnaryOperator: + return BindUnaryOperatorNode(node as UnaryOperatorNode, context); - case QueryNodeKind.SingleValueFunctionCall: - return BindSingleValueFunctionCallNode(node as SingleValueFunctionCallNode, context); + case QueryNodeKind.SingleValueFunctionCall: + return BindSingleValueFunctionCallNode(node as SingleValueFunctionCallNode, context); - case QueryNodeKind.SingleNavigationNode: - SingleNavigationNode navigationNode = node as SingleNavigationNode; - return BindNavigationPropertyNode(navigationNode.Source, navigationNode.NavigationProperty, GetFullPropertyPath(navigationNode), context); + case QueryNodeKind.SingleNavigationNode: + SingleNavigationNode navigationNode = node as SingleNavigationNode; + return BindNavigationPropertyNode(navigationNode.Source, navigationNode.NavigationProperty, GetFullPropertyPath(navigationNode), context); - case QueryNodeKind.Any: - return BindAnyNode(node as AnyNode, context); + case QueryNodeKind.Any: + return BindAnyNode(node as AnyNode, context); - case QueryNodeKind.All: - return BindAllNode(node as AllNode, context); + case QueryNodeKind.All: + return BindAllNode(node as AllNode, context); - case QueryNodeKind.SingleResourceCast: - return BindSingleResourceCastNode(node as SingleResourceCastNode, context); + case QueryNodeKind.SingleResourceCast: + return BindSingleResourceCastNode(node as SingleResourceCastNode, context); - case QueryNodeKind.SingleResourceFunctionCall: - return BindSingleResourceFunctionCallNode(node as SingleResourceFunctionCallNode, context); + case QueryNodeKind.SingleResourceFunctionCall: + return BindSingleResourceFunctionCallNode(node as SingleResourceFunctionCallNode, context); - case QueryNodeKind.In: - return BindInNode(node as InNode, context); + case QueryNodeKind.In: + return BindInNode(node as InNode, context); - case QueryNodeKind.Count: - return BindCountNode(node as CountNode, context); + case QueryNodeKind.Count: + return BindCountNode(node as CountNode, context); - case QueryNodeKind.NamedFunctionParameter: - case QueryNodeKind.ParameterAlias: - case QueryNodeKind.EntitySet: - case QueryNodeKind.KeyLookup: - case QueryNodeKind.SearchTerm: - // Unused or have unknown uses. - default: - throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, node.Kind, typeof(QueryBinder).Name); - } + case QueryNodeKind.NamedFunctionParameter: + case QueryNodeKind.ParameterAlias: + case QueryNodeKind.EntitySet: + case QueryNodeKind.KeyLookup: + case QueryNodeKind.SearchTerm: + // Unused or have unknown uses. + default: + throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, node.Kind, typeof(QueryBinder).Name); } + } + + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The node that represents the navigation source. + /// The navigation property to bind. + /// The property path. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindNavigationPropertyNode(QueryNode sourceNode, IEdmNavigationProperty navigationProperty, string propertyPath, QueryBinderContext context) + { + CheckArgumentNull(sourceNode, context); - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The node that represents the navigation source. - /// The navigation property to bind. - /// The property path. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindNavigationPropertyNode(QueryNode sourceNode, IEdmNavigationProperty navigationProperty, string propertyPath, QueryBinderContext context) + Expression source; + + // TODO: bug in uri parser is causing this property to be null for the root property. + if (sourceNode == null) + { + // It means we should use current parameter at current context. + source = context.CurrentParameter; + } + else { - CheckArgumentNull(sourceNode, context); + source = Bind(sourceNode, context); + } - Expression source; + return CreatePropertyAccessExpression(source, context, navigationProperty, propertyPath); + } - // TODO: bug in uri parser is causing this property to be null for the root property. - if (sourceNode == null) - { - // It means we should use current parameter at current context. - source = context.CurrentParameter; - } - else - { - source = Bind(sourceNode, context); - } + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindCollectionResourceCastNode(CollectionResourceCastNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context); - return CreatePropertyAccessExpression(source, context, navigationProperty, propertyPath); - } + IEdmStructuredTypeReference structured = node.ItemStructuredType; + Contract.Assert(structured != null, "NS casts can contain only structured types"); + + Type clrType = context.Model.GetClrType(structured); - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindCollectionResourceCastNode(CollectionResourceCastNode node, QueryBinderContext context) + Expression source = BindCastSourceNode(node.Source, context); + + if (ExpressionBinderHelper.IsIQueryable(source.Type)) + { + return Expression.Call(null, ExpressionHelperMethods.QueryableOfType.MakeGenericMethod(clrType), source); + } + else { - CheckArgumentNull(node, context); + return Expression.Call(null, ExpressionHelperMethods.EnumerableOfType.MakeGenericMethod(clrType), source); + } + } + + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindCollectionComplexNode(CollectionComplexNode collectionComplexNode, QueryBinderContext context) + { + CheckArgumentNull(collectionComplexNode, context); - IEdmStructuredTypeReference structured = node.ItemStructuredType; - Contract.Assert(structured != null, "NS casts can contain only structured types"); + Expression source = Bind(collectionComplexNode.Source, context); + return CreatePropertyAccessExpression(source, context, collectionComplexNode.Property); + } - Type clrType = context.Model.GetClrType(structured); + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindCollectionPropertyAccessNode(CollectionPropertyAccessNode propertyAccessNode, QueryBinderContext context) + { + CheckArgumentNull(propertyAccessNode, context); - Expression source = BindCastSourceNode(node.Source, context); + Expression source = Bind(propertyAccessNode.Source, context); + return CreatePropertyAccessExpression(source, context, propertyAccessNode.Property); + } - if (ExpressionBinderHelper.IsIQueryable(source.Type)) - { - return Expression.Call(null, ExpressionHelperMethods.QueryableOfType.MakeGenericMethod(clrType), source); - } - else - { - return Expression.Call(null, ExpressionHelperMethods.EnumerableOfType.MakeGenericMethod(clrType), source); - } - } + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindDynamicPropertyAccessQueryNode(SingleValueOpenPropertyAccessNode openNode, QueryBinderContext context) + { + CheckArgumentNull(openNode, context); - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindCollectionComplexNode(CollectionComplexNode collectionComplexNode, QueryBinderContext context) + if (context.ElementClrType.IsDynamicTypeWrapper()) { - CheckArgumentNull(collectionComplexNode, context); - - Expression source = Bind(collectionComplexNode.Source, context); - return CreatePropertyAccessExpression(source, context, collectionComplexNode.Property); + return GetFlattenedPropertyExpression(openNode.Name, context) ?? Expression.Property(Bind(openNode.Source, context), openNode.Name); } - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindCollectionPropertyAccessNode(CollectionPropertyAccessNode propertyAccessNode, QueryBinderContext context) + if (context.ComputedProperties.TryGetValue(openNode.Name, out var computedProperty)) { - CheckArgumentNull(propertyAccessNode, context); - - Expression source = Bind(propertyAccessNode.Source, context); - return CreatePropertyAccessExpression(source, context, propertyAccessNode.Property); + return Bind(computedProperty.Expression, context); } - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindDynamicPropertyAccessQueryNode(SingleValueOpenPropertyAccessNode openNode, QueryBinderContext context) + PropertyInfo prop = GetDynamicPropertyContainer(openNode, context); + + var propertyAccessExpression = BindPropertyAccessExpression(openNode, prop, context); + var readDictionaryIndexerExpression = Expression.Property(propertyAccessExpression, + DictionaryStringObjectIndexerName, Expression.Constant(openNode.Name)); + var containsKeyExpression = Expression.Call(propertyAccessExpression, + propertyAccessExpression.Type.GetMethod("ContainsKey"), Expression.Constant(openNode.Name)); + var nullExpression = Expression.Constant(null); + + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) + { + var dynamicDictIsNotNull = Expression.NotEqual(propertyAccessExpression, Expression.Constant(null)); + var dynamicDictIsNotNullAndContainsKey = Expression.AndAlso(dynamicDictIsNotNull, containsKeyExpression); + return Expression.Condition( + dynamicDictIsNotNullAndContainsKey, + readDictionaryIndexerExpression, + nullExpression); + } + else { - CheckArgumentNull(openNode, context); + return Expression.Condition( + containsKeyExpression, + readDictionaryIndexerExpression, + nullExpression); + } + } - if (context.ElementClrType.IsDynamicTypeWrapper()) - { - return GetFlattenedPropertyExpression(openNode.Name, context) ?? Expression.Property(Bind(openNode.Source, context), openNode.Name); - } + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindPropertyAccessQueryNode(SingleValuePropertyAccessNode propertyAccessNode, QueryBinderContext context) + { + CheckArgumentNull(propertyAccessNode, context); - if (context.ComputedProperties.TryGetValue(openNode.Name, out var computedProperty)) - { - return Bind(computedProperty.Expression, context); - } + Expression source = Bind(propertyAccessNode.Source, context); + return CreatePropertyAccessExpression(source, context, propertyAccessNode.Property, GetFullPropertyPath(propertyAccessNode)); + } - PropertyInfo prop = GetDynamicPropertyContainer(openNode, context); + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindSingleComplexNode(SingleComplexNode singleComplexNode, QueryBinderContext context) + { + CheckArgumentNull(singleComplexNode, context); - var propertyAccessExpression = BindPropertyAccessExpression(openNode, prop, context); - var readDictionaryIndexerExpression = Expression.Property(propertyAccessExpression, - DictionaryStringObjectIndexerName, Expression.Constant(openNode.Name)); - var containsKeyExpression = Expression.Call(propertyAccessExpression, - propertyAccessExpression.Type.GetMethod("ContainsKey"), Expression.Constant(openNode.Name)); - var nullExpression = Expression.Constant(null); + Expression source = Bind(singleComplexNode.Source, context); + return CreatePropertyAccessExpression(source, context, singleComplexNode.Property, GetFullPropertyPath(singleComplexNode)); + } - if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) - { - var dynamicDictIsNotNull = Expression.NotEqual(propertyAccessExpression, Expression.Constant(null)); - var dynamicDictIsNotNullAndContainsKey = Expression.AndAlso(dynamicDictIsNotNull, containsKeyExpression); - return Expression.Condition( - dynamicDictIsNotNullAndContainsKey, - readDictionaryIndexerExpression, - nullExpression); - } - else - { - return Expression.Condition( - containsKeyExpression, - readDictionaryIndexerExpression, - nullExpression); - } + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The range variable to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindRangeVariable(RangeVariable rangeVariable, QueryBinderContext context) + { + if (rangeVariable == null) + { + throw Error.ArgumentNull(nameof(rangeVariable)); } - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindPropertyAccessQueryNode(SingleValuePropertyAccessNode propertyAccessNode, QueryBinderContext context) + if (context == null) { - CheckArgumentNull(propertyAccessNode, context); - - Expression source = Bind(propertyAccessNode.Source, context); - return CreatePropertyAccessExpression(source, context, propertyAccessNode.Property, GetFullPropertyPath(propertyAccessNode)); + throw Error.ArgumentNull(nameof(context)); } - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindSingleComplexNode(SingleComplexNode singleComplexNode, QueryBinderContext context) + // Be noted: it's used in $compute for $select and $expand. + if (context.Source != null && rangeVariable.Name == "$it") { - CheckArgumentNull(singleComplexNode, context); - - Expression source = Bind(singleComplexNode.Source, context); - return CreatePropertyAccessExpression(source, context, singleComplexNode.Property, GetFullPropertyPath(singleComplexNode)); + return context.Source; } - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The range variable to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindRangeVariable(RangeVariable rangeVariable, QueryBinderContext context) + Expression parameter; + // When we have a $this RangeVariable, it's refer to "current parameter" in current context. + if (rangeVariable.Name == "$this") { - if (rangeVariable == null) - { - throw Error.ArgumentNull(nameof(rangeVariable)); - } - - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - // Be noted: it's used in $compute for $select and $expand. - if (context.Source != null && rangeVariable.Name == "$it") - { - return context.Source; - } - - Expression parameter; - // When we have a $this RangeVariable, it's refer to "current parameter" in current context. - if (rangeVariable.Name == "$this") - { - parameter = context.CurrentParameter; - } - else if (rangeVariable.Name == "$it") - { - // ~/Customers?$select=Addresses($filter=$it/City eq City) - // Both Expression.Source is the RangeVariableRefereneNode, and the name is "$it". - // But first refers to "Addresses" in $it/City, and the second (City) refers to "Customers". - // We can't simply identify using the "$it" so there's a workaround. - if (context.IsNested) + parameter = context.CurrentParameter; + } + else if (rangeVariable.Name == "$it") + { + // ~/Customers?$select=Addresses($filter=$it/City eq City) + // Both Expression.Source is the RangeVariableRefereneNode, and the name is "$it". + // But first refers to "Addresses" in $it/City, and the second (City) refers to "Customers". + // We can't simply identify using the "$it" so there's a workaround. + if (context.IsNested) + { + Type clrType = context.Model.GetClrType(rangeVariable.TypeReference); + if (clrType == context.CurrentParameter.Type) { - Type clrType = context.Model.GetClrType(rangeVariable.TypeReference); - if (clrType == context.CurrentParameter.Type) - { - parameter = context.CurrentParameter; - } - else - { - parameter = context.GetParameter("$it"); - } + parameter = context.CurrentParameter; } else { @@ -409,903 +404,903 @@ public virtual Expression BindRangeVariable(RangeVariable rangeVariable, QueryBi } else { - parameter = context.GetParameter(rangeVariable.Name); + parameter = context.GetParameter("$it"); } - - return ConvertNonStandardPrimitives(parameter, context); } - - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindBinaryOperatorNode(BinaryOperatorNode binaryOperatorNode, QueryBinderContext context) + else { - CheckArgumentNull(binaryOperatorNode, context); + parameter = context.GetParameter(rangeVariable.Name); + } - ODataQuerySettings querySettings = context.QuerySettings; + return ConvertNonStandardPrimitives(parameter, context); + } - Expression left = Bind(binaryOperatorNode.Left, context); - Expression right = Bind(binaryOperatorNode.Right, context); + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindBinaryOperatorNode(BinaryOperatorNode binaryOperatorNode, QueryBinderContext context) + { + CheckArgumentNull(binaryOperatorNode, context); - // handle null propagation only if either of the operands can be null - bool isNullPropagationRequired = querySettings.HandleNullPropagation == HandleNullPropagationOption.True && (ExpressionBinderHelper.IsNullable(left.Type) || ExpressionBinderHelper.IsNullable(right.Type)); - if (isNullPropagationRequired) - { - // |----------------------------------------------------------------| - // |SQL 3VL truth table. | - // |----------------------------------------------------------------| - // |p | q | p OR q | p AND q | p = q | - // |----------------------------------------------------------------| - // |True | True | True | True | True | - // |True | False | True | False | False | - // |True | NULL | True | NULL | NULL | - // |False | True | True | False | False | - // |False | False | False | False | True | - // |False | NULL | NULL | False | NULL | - // |NULL | True | True | NULL | NULL | - // |NULL | False | NULL | False | NULL | - // |NULL | NULL | Null | NULL | NULL | - // |--------|-----------|---------------|---------------|-----------| - - // before we start with null propagation, convert the operators to nullable if already not. - left = ExpressionBinderHelper.ToNullable(left); - right = ExpressionBinderHelper.ToNullable(right); - - bool liftToNull = true; - if (left == NullConstant || right == NullConstant) - { - liftToNull = false; - } + ODataQuerySettings querySettings = context.QuerySettings; - // Expression trees do a very good job of handling the 3VL truth table if we pass liftToNull true. - return ExpressionBinderHelper.CreateBinaryExpression(binaryOperatorNode.OperatorKind, left, right, liftToNull: liftToNull, querySettings); - } - else - { - return ExpressionBinderHelper.CreateBinaryExpression(binaryOperatorNode.OperatorKind, left, right, liftToNull: false, querySettings); - } - } + Expression left = Bind(binaryOperatorNode.Left, context); + Expression right = Bind(binaryOperatorNode.Right, context); - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindConvertNode(ConvertNode convertNode, QueryBinderContext context) + // handle null propagation only if either of the operands can be null + bool isNullPropagationRequired = querySettings.HandleNullPropagation == HandleNullPropagationOption.True && (ExpressionBinderHelper.IsNullable(left.Type) || ExpressionBinderHelper.IsNullable(right.Type)); + if (isNullPropagationRequired) { - CheckArgumentNull(convertNode, context); - - Contract.Assert(convertNode.TypeReference != null); - - Expression source = Bind(convertNode.Source, context); - - return CreateConvertExpression(convertNode, source, context); + // |----------------------------------------------------------------| + // |SQL 3VL truth table. | + // |----------------------------------------------------------------| + // |p | q | p OR q | p AND q | p = q | + // |----------------------------------------------------------------| + // |True | True | True | True | True | + // |True | False | True | False | False | + // |True | NULL | True | NULL | NULL | + // |False | True | True | False | False | + // |False | False | False | False | True | + // |False | NULL | NULL | False | NULL | + // |NULL | True | True | NULL | NULL | + // |NULL | False | NULL | False | NULL | + // |NULL | NULL | Null | NULL | NULL | + // |--------|-----------|---------------|---------------|-----------| + + // before we start with null propagation, convert the operators to nullable if already not. + left = ExpressionBinderHelper.ToNullable(left); + right = ExpressionBinderHelper.ToNullable(right); + + bool liftToNull = true; + if (left == NullConstant || right == NullConstant) + { + liftToNull = false; + } + + // Expression trees do a very good job of handling the 3VL truth table if we pass liftToNull true. + return ExpressionBinderHelper.CreateBinaryExpression(binaryOperatorNode.OperatorKind, left, right, liftToNull: liftToNull, querySettings); } - - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindCountNode(CountNode node, QueryBinderContext context) + else { - // $filter=navProp/$count($filter=prop gt 1) gt 2 + return ExpressionBinderHelper.CreateBinaryExpression(binaryOperatorNode.OperatorKind, left, right, liftToNull: false, querySettings); + } + } - CheckArgumentNull(node, context); + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindConvertNode(ConvertNode convertNode, QueryBinderContext context) + { + CheckArgumentNull(convertNode, context); - Expression source = Bind(node.Source, context); - Expression countExpression = Expression.Constant(null, typeof(long?)); - Type elementType; - if (!TypeHelper.IsCollection(source.Type, out elementType)) - { - return countExpression; - } + Contract.Assert(convertNode.TypeReference != null); - MethodInfo countMethod; - if (typeof(IQueryable).IsAssignableFrom(source.Type)) - { - countMethod = ExpressionHelperMethods.QueryableCountGeneric.MakeGenericMethod(elementType); - } - else - { - countMethod = ExpressionHelperMethods.EnumerableCountGeneric.MakeGenericMethod(elementType); - } + Expression source = Bind(convertNode.Source, context); - // Bind the inner $filter clause within the $count segment. - // e.g Books?$filter=Authors/$count($filter=Id gt 1) gt 1 - if (node.FilterClause != null) - { - QueryBinderContext nextBinderContext = new QueryBinderContext(context, context.QuerySettings, elementType); + return CreateConvertExpression(convertNode, source, context); + } + + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindCountNode(CountNode node, QueryBinderContext context) + { + // $filter=navProp/$count($filter=prop gt 1) gt 2 + + CheckArgumentNull(node, context); - Expression body = Bind(node.FilterClause.Expression, nextBinderContext); + Expression source = Bind(node.Source, context); + Expression countExpression = Expression.Constant(null, typeof(long?)); + Type elementType; + if (!TypeHelper.IsCollection(source.Type, out elementType)) + { + return countExpression; + } - ParameterExpression filterParameter = nextBinderContext.CurrentParameter; + MethodInfo countMethod; + if (typeof(IQueryable).IsAssignableFrom(source.Type)) + { + countMethod = ExpressionHelperMethods.QueryableCountGeneric.MakeGenericMethod(elementType); + } + else + { + countMethod = ExpressionHelperMethods.EnumerableCountGeneric.MakeGenericMethod(elementType); + } - LambdaExpression filterExpr = Expression.Lambda(body, filterParameter); + // Bind the inner $filter clause within the $count segment. + // e.g Books?$filter=Authors/$count($filter=Id gt 1) gt 1 + if (node.FilterClause != null) + { + QueryBinderContext nextBinderContext = new QueryBinderContext(context, context.QuerySettings, elementType); - filterExpr = Expression.Lambda(ApplyNullPropagationForFilterBody(filterExpr.Body, nextBinderContext), filterExpr.Parameters); + Expression body = Bind(node.FilterClause.Expression, nextBinderContext); - MethodInfo whereMethod; - if (typeof(IQueryable).IsAssignableFrom(source.Type)) - { - whereMethod = ExpressionHelperMethods.QueryableWhereGeneric.MakeGenericMethod(elementType); - } - else - { - whereMethod = ExpressionHelperMethods.EnumerableWhereGeneric.MakeGenericMethod(elementType); - } + ParameterExpression filterParameter = nextBinderContext.CurrentParameter; - // The source expression looks like: $it.Authors - // So the generated countExpression below will look like: $it.Authors.Where($it => $it.Id > 1) - source = Expression.Call(null, whereMethod, new[] { source, filterExpr }); - } + LambdaExpression filterExpr = Expression.Lambda(body, filterParameter); - // append LongCount() method. - // The final countExpression with the nested $filter clause will look like: $it.Authors.Where($it => $it.Id > 1).LongCount() - // The final countExpression without the nested $filter clause will look like: $it.Authors.LongCount() - countExpression = Expression.Call(null, countMethod, new[] { source }); + filterExpr = Expression.Lambda(ApplyNullPropagationForFilterBody(filterExpr.Body, nextBinderContext), filterExpr.Parameters); - if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) + MethodInfo whereMethod; + if (typeof(IQueryable).IsAssignableFrom(source.Type)) { - // source == null ? null : countExpression - return Expression.Condition( - test: Expression.Equal(source, Expression.Constant(null)), - ifTrue: Expression.Constant(null, typeof(long?)), - ifFalse: ExpressionHelpers.ToNullable(countExpression)); + whereMethod = ExpressionHelperMethods.QueryableWhereGeneric.MakeGenericMethod(elementType); } else { - return countExpression; + whereMethod = ExpressionHelperMethods.EnumerableWhereGeneric.MakeGenericMethod(elementType); } + + // The source expression looks like: $it.Authors + // So the generated countExpression below will look like: $it.Authors.Where($it => $it.Id > 1) + source = Expression.Call(null, whereMethod, new[] { source, filterExpr }); } - /// - /// Binds an to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindInNode(InNode inNode, QueryBinderContext context) + // append LongCount() method. + // The final countExpression with the nested $filter clause will look like: $it.Authors.Where($it => $it.Id > 1).LongCount() + // The final countExpression without the nested $filter clause will look like: $it.Authors.LongCount() + countExpression = Expression.Call(null, countMethod, new[] { source }); + + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) { - CheckArgumentNull(inNode, context); + // source == null ? null : countExpression + return Expression.Condition( + test: Expression.Equal(source, Expression.Constant(null)), + ifTrue: Expression.Constant(null, typeof(long?)), + ifFalse: ExpressionHelpers.ToNullable(countExpression)); + } + else + { + return countExpression; + } + } - Expression singleValue = Bind(inNode.Left, context); - Expression collection = Bind(inNode.Right, context); + /// + /// Binds an to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindInNode(InNode inNode, QueryBinderContext context) + { + CheckArgumentNull(inNode, context); - Type collectionItemType = collection.Type.GetElementType(); - if (collectionItemType == null) - { - Type[] genericArgs = collection.Type.GetGenericArguments(); - // The model builder does not support non-generic collections like ArrayList - // or generic collections with generic arguments > 1 like IDictionary<,> - Contract.Assert(genericArgs.Length == 1); - collectionItemType = genericArgs[0]; - } + Expression singleValue = Bind(inNode.Left, context); + Expression collection = Bind(inNode.Right, context); - if (ExpressionBinderHelper.IsIQueryable(collection.Type)) - { - Expression containsExpression = singleValue.Type != collectionItemType ? Expression.Call(null, ExpressionHelperMethods.QueryableCastGeneric.MakeGenericMethod(singleValue.Type), collection) : collection; - return Expression.Call(null, ExpressionHelperMethods.QueryableContainsGeneric.MakeGenericMethod(singleValue.Type), containsExpression, singleValue); - } - else - { - Expression containsExpression = singleValue.Type != collectionItemType ? Expression.Call(null, ExpressionHelperMethods.EnumerableCastGeneric.MakeGenericMethod(singleValue.Type), collection) : collection; - return Expression.Call(null, ExpressionHelperMethods.EnumerableContainsGeneric.MakeGenericMethod(singleValue.Type), containsExpression, singleValue); - } + Type collectionItemType = collection.Type.GetElementType(); + if (collectionItemType == null) + { + Type[] genericArgs = collection.Type.GetGenericArguments(); + // The model builder does not support non-generic collections like ArrayList + // or generic collections with generic arguments > 1 like IDictionary<,> + Contract.Assert(genericArgs.Length == 1); + collectionItemType = genericArgs[0]; } - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindSingleResourceFunctionCallNode(SingleResourceFunctionCallNode node, QueryBinderContext context) + if (ExpressionBinderHelper.IsIQueryable(collection.Type)) { - CheckArgumentNull(node, context); - - switch (node.Name) - { - case ClrCanonicalFunctions.CastFunctionName: - return BindSingleResourceCastFunctionCall(node, context); - default: - throw Error.NotSupported(SRResources.ODataFunctionNotSupported, node.Name); - } + Expression containsExpression = singleValue.Type != collectionItemType ? Expression.Call(null, ExpressionHelperMethods.QueryableCastGeneric.MakeGenericMethod(singleValue.Type), collection) : collection; + return Expression.Call(null, ExpressionHelperMethods.QueryableContainsGeneric.MakeGenericMethod(singleValue.Type), containsExpression, singleValue); + } + else + { + Expression containsExpression = singleValue.Type != collectionItemType ? Expression.Call(null, ExpressionHelperMethods.EnumerableCastGeneric.MakeGenericMethod(singleValue.Type), collection) : collection; + return Expression.Call(null, ExpressionHelperMethods.EnumerableContainsGeneric.MakeGenericMethod(singleValue.Type), containsExpression, singleValue); } + } + + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindSingleResourceFunctionCallNode(SingleResourceFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context); - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindSingleResourceCastFunctionCall(SingleResourceFunctionCallNode node, QueryBinderContext context) + switch (node.Name) { - CheckArgumentNull(node, context); + case ClrCanonicalFunctions.CastFunctionName: + return BindSingleResourceCastFunctionCall(node, context); + default: + throw Error.NotSupported(SRResources.ODataFunctionNotSupported, node.Name); + } + } - Contract.Assert(ClrCanonicalFunctions.CastFunctionName == node.Name); + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindSingleResourceCastFunctionCall(SingleResourceFunctionCallNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context); - Expression[] arguments = BindArguments(node.Parameters, context); + Contract.Assert(ClrCanonicalFunctions.CastFunctionName == node.Name); - Contract.Assert(arguments.Length == 2); + Expression[] arguments = BindArguments(node.Parameters, context); - IEdmModel model = context.Model; + Contract.Assert(arguments.Length == 2); - string targetEdmTypeName = (string)((ConstantNode)node.Parameters.Last()).Value; - IEdmType targetEdmType = model.FindType(targetEdmTypeName); - Type targetClrType = null; + IEdmModel model = context.Model; - if (targetEdmType != null) - { - targetClrType = model.GetClrType(targetEdmType.ToEdmTypeReference(false)); - } + string targetEdmTypeName = (string)((ConstantNode)node.Parameters.Last()).Value; + IEdmType targetEdmType = model.FindType(targetEdmTypeName); + Type targetClrType = null; - if (arguments[0].Type == targetClrType) - { - // We only support to cast Entity type to the same type now. - return arguments[0]; - } - else if (arguments[0].Type.IsAssignableFrom(targetClrType)) - { - // To support to cast Entity/Complex type to the sub type now. - Expression source; - if (node.Source != null) - { - source = BindCastSourceNode(node.Source, context); - } - else - { - // if the cast is on the root i.e $it (~/Products?$filter=NS.PopularProducts/.....), - // node.Source would be null. Calling BindCastSourceNode will always return '$it'. - // In scenarios where we are casting a navigation property to return an expression that queries against the parent property, - // we need to have a memberAccess expression e.g '$it.Category'. We can get this from arguments[0]. - source = arguments[0]; - } + if (targetEdmType != null) + { + targetClrType = model.GetClrType(targetEdmType.ToEdmTypeReference(false)); + } - return Expression.TypeAs(source, targetClrType); + if (arguments[0].Type == targetClrType) + { + // We only support to cast Entity type to the same type now. + return arguments[0]; + } + else if (arguments[0].Type.IsAssignableFrom(targetClrType)) + { + // To support to cast Entity/Complex type to the sub type now. + Expression source; + if (node.Source != null) + { + source = BindCastSourceNode(node.Source, context); } else { - // Cast fails and return null. - return NullConstant; + // if the cast is on the root i.e $it (~/Products?$filter=NS.PopularProducts/.....), + // node.Source would be null. Calling BindCastSourceNode will always return '$it'. + // In scenarios where we are casting a navigation property to return an expression that queries against the parent property, + // we need to have a memberAccess expression e.g '$it.Category'. We can get this from arguments[0]. + source = arguments[0]; } - } - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindSingleResourceCastNode(SingleResourceCastNode node, QueryBinderContext context) + return Expression.TypeAs(source, targetClrType); + } + else { - CheckArgumentNull(node, context); + // Cast fails and return null. + return NullConstant; + } + } - IEdmStructuredTypeReference structured = node.StructuredTypeReference; - Contract.Assert(structured != null, "NS casts can contain only structured types"); + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindSingleResourceCastNode(SingleResourceCastNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context); - Type clrType = context.Model.GetClrType(structured); + IEdmStructuredTypeReference structured = node.StructuredTypeReference; + Contract.Assert(structured != null, "NS casts can contain only structured types"); - Expression source = BindCastSourceNode(node.Source, context); - return Expression.TypeAs(source, clrType); - } + Type clrType = context.Model.GetClrType(structured); - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindAllNode(AllNode allNode, QueryBinderContext context) - { - CheckArgumentNull(allNode, context); + Expression source = BindCastSourceNode(node.Source, context); + return Expression.TypeAs(source, clrType); + } - // context.EnterLambdaScope(); + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindAllNode(AllNode allNode, QueryBinderContext context) + { + CheckArgumentNull(allNode, context); - (string name, ParameterExpression allIt) = context.HandleLambdaParameters(allNode.RangeVariables); + // context.EnterLambdaScope(); - Expression source; - Contract.Assert(allNode.Source != null); - source = Bind(allNode.Source, context); + (string name, ParameterExpression allIt) = context.HandleLambdaParameters(allNode.RangeVariables); - Expression body = source; - Contract.Assert(allNode.Body != null); + Expression source; + Contract.Assert(allNode.Source != null); + source = Bind(allNode.Source, context); - body = Bind(allNode.Body, context); - body = ApplyNullPropagationForFilterBody(body, context); - body = Expression.Lambda(body, allIt); + Expression body = source; + Contract.Assert(allNode.Body != null); - Expression all = All(source, body); + body = Bind(allNode.Body, context); + body = ApplyNullPropagationForFilterBody(body, context); + body = Expression.Lambda(body, allIt); - context.RemoveParameter(name); - //context.ExitLamdbaScope(); + Expression all = All(source, body); - if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && ExpressionBinderHelper.IsNullable(source.Type)) - { - // IFF(source == null) null; else Any(body); - all = ExpressionBinderHelper.ToNullable(all); - return Expression.Condition( - test: Expression.Equal(source, NullConstant), - ifTrue: Expression.Constant(null, all.Type), - ifFalse: all); - } - else - { - return all; - } - } + context.RemoveParameter(name); + //context.ExitLamdbaScope(); - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindAnyNode(AnyNode anyNode, QueryBinderContext context) + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && ExpressionBinderHelper.IsNullable(source.Type)) { - CheckArgumentNull(anyNode, context); - - //context.EnterLambdaScope(); - - (string name, ParameterExpression anyIt) = context.HandleLambdaParameters(anyNode.RangeVariables); - - Expression source; - Contract.Assert(anyNode.Source != null); - source = Bind(anyNode.Source, context); + // IFF(source == null) null; else Any(body); + all = ExpressionBinderHelper.ToNullable(all); + return Expression.Condition( + test: Expression.Equal(source, NullConstant), + ifTrue: Expression.Constant(null, all.Type), + ifFalse: all); + } + else + { + return all; + } + } - Expression body = null; - // uri parser places an Constant node with value true for empty any() body - if (anyNode.Body != null && anyNode.Body.Kind != QueryNodeKind.Constant) - { - body = Bind(anyNode.Body, context); - body = ApplyNullPropagationForFilterBody(body, context); - body = Expression.Lambda(body, anyIt); - } - else if (anyNode.Body != null && anyNode.Body.Kind == QueryNodeKind.Constant - && (bool)(anyNode.Body as ConstantNode).Value == false) - { - // any(false) is the same as just false - context.RemoveParameter(name); - return FalseConstant; - } + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindAnyNode(AnyNode anyNode, QueryBinderContext context) + { + CheckArgumentNull(anyNode, context); - Expression any = Any(source, body); + //context.EnterLambdaScope(); - context.RemoveParameter(name); + (string name, ParameterExpression anyIt) = context.HandleLambdaParameters(anyNode.RangeVariables); - if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && ExpressionBinderHelper.IsNullable(source.Type)) - { - // IFF(source == null) null; else Any(body); - any = ExpressionBinderHelper.ToNullable(any); - return Expression.Condition( - test: Expression.Equal(source, NullConstant), - ifTrue: Expression.Constant(null, any.Type), - ifFalse: any); - } - else - { - return any; - } - } + Expression source; + Contract.Assert(anyNode.Source != null); + source = Bind(anyNode.Source, context); - private Expression BindCastSourceNode(QueryNode sourceNode, QueryBinderContext context) + Expression body = null; + // uri parser places an Constant node with value true for empty any() body + if (anyNode.Body != null && anyNode.Body.Kind != QueryNodeKind.Constant) { - Expression source; - if (sourceNode == null) - { - // if the cast is on the root i.e $it (~/Products?$filter=NS.PopularProducts/.....), - // source would be null. So bind null to 'current parameter' at context. - source = context.CurrentParameter; - } - else - { - source = Bind(sourceNode, context); - } - - return source; + body = Bind(anyNode.Body, context); + body = ApplyNullPropagationForFilterBody(body, context); + body = Expression.Lambda(body, anyIt); } - - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindUnaryOperatorNode(UnaryOperatorNode unaryOperatorNode, QueryBinderContext context) + else if (anyNode.Body != null && anyNode.Body.Kind == QueryNodeKind.Constant + && (bool)(anyNode.Body as ConstantNode).Value == false) { - CheckArgumentNull(unaryOperatorNode, context); + // any(false) is the same as just false + context.RemoveParameter(name); + return FalseConstant; + } - // No need to handle null-propagation here as CLR already handles it. - // !(null) = null - // -(null) = null - Expression inner = Bind(unaryOperatorNode.Operand, context); - switch (unaryOperatorNode.OperatorKind) - { - case UnaryOperatorKind.Negate: - return Expression.Negate(inner); + Expression any = Any(source, body); - case UnaryOperatorKind.Not: - return Expression.Not(inner); + context.RemoveParameter(name); - default: - throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, unaryOperatorNode.Kind, typeof(QueryBinder).Name); - } + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && ExpressionBinderHelper.IsNullable(source.Type)) + { + // IFF(source == null) null; else Any(body); + any = ExpressionBinderHelper.ToNullable(any); + return Expression.Condition( + test: Expression.Equal(source, NullConstant), + ifTrue: Expression.Constant(null, any.Type), + ifFalse: any); } - - #endregion - - #region Bind Node methods - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindConstantNode(ConstantNode constantNode, QueryBinderContext context) + else { - CheckArgumentNull(constantNode, context); + return any; + } + } - // no need to parameterize null's as there cannot be multiple values for null. - if (constantNode.Value == null) - { - return NullConstant; - } + private Expression BindCastSourceNode(QueryNode sourceNode, QueryBinderContext context) + { + Expression source; + if (sourceNode == null) + { + // if the cast is on the root i.e $it (~/Products?$filter=NS.PopularProducts/.....), + // source would be null. So bind null to 'current parameter' at context. + source = context.CurrentParameter; + } + else + { + source = Bind(sourceNode, context); + } - object value = constantNode.Value; - Type constantType = RetrieveClrTypeForConstant(constantNode.TypeReference, context, ref value); + return source; + } - if (context.QuerySettings.EnableConstantParameterization) - { - return LinqParameterContainer.Parameterize(constantType, value); - } - else - { - return Expression.Constant(value, constantType); - } - } + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindUnaryOperatorNode(UnaryOperatorNode unaryOperatorNode, QueryBinderContext context) + { + CheckArgumentNull(unaryOperatorNode, context); - /// - /// Binds a to create a LINQ that - /// represents the semantics of the . - /// - /// The query node to bind. - /// The query binder context. - /// The LINQ created. - public virtual Expression BindCollectionConstantNode(CollectionConstantNode node, QueryBinderContext context) + // No need to handle null-propagation here as CLR already handles it. + // !(null) = null + // -(null) = null + Expression inner = Bind(unaryOperatorNode.Operand, context); + switch (unaryOperatorNode.OperatorKind) { - CheckArgumentNull(node, context); + case UnaryOperatorKind.Negate: + return Expression.Negate(inner); - // It's fine if the collection is empty; the returned value will be an empty list. - ConstantNode firstNode = node.Collection.FirstOrDefault(); - object value = null; - if (firstNode != null) - { - value = firstNode.Value; - } + case UnaryOperatorKind.Not: + return Expression.Not(inner); - Type constantType = RetrieveClrTypeForConstant(node.ItemType, context, ref value); - Type nullableConstantType = node.ItemType.IsNullable && constantType.IsValueType && Nullable.GetUnderlyingType(constantType) == null - ? typeof(Nullable<>).MakeGenericType(constantType) - : constantType; - Type listType = typeof(List<>).MakeGenericType(nullableConstantType); - IList castedList = Activator.CreateInstance(listType) as IList; - - // Getting a LINQ expression to dynamically cast each item in the Collection during runtime is tricky, - // so using a foreach loop and doing an implicit cast from object to the CLR type of ItemType. - foreach (ConstantNode item in node.Collection) - { - object member; - if (item.Value == null) - { - member = null; - } - else if (constantType.IsEnum) - { - member = EnumDeserializationHelpers.ConvertEnumValue(item.Value, constantType); - } - else - { - member = item.Value; - } + default: + throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, unaryOperatorNode.Kind, typeof(QueryBinder).Name); + } + } - castedList.Add(member); - } + #endregion - if (context.QuerySettings.EnableConstantParameterization) - { - return LinqParameterContainer.Parameterize(listType, castedList); - } + #region Bind Node methods + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindConstantNode(ConstantNode constantNode, QueryBinderContext context) + { + CheckArgumentNull(constantNode, context); - return Expression.Constant(castedList, listType); + // no need to parameterize null's as there cannot be multiple values for null. + if (constantNode.Value == null) + { + return NullConstant; } - #endregion - #region Private helper methods - private static void ValidateAllStringArguments(string functionName, Expression[] arguments) + object value = constantNode.Value; + Type constantType = RetrieveClrTypeForConstant(constantNode.TypeReference, context, ref value); + + if (context.QuerySettings.EnableConstantParameterization) { - if (arguments.Any(arg => arg.Type != typeof(string))) - { - throw new ODataException(Error.Format(SRResources.FunctionNotSupportedOnEnum, functionName)); - } + return LinqParameterContainer.Parameterize(constantType, value); } + else + { + return Expression.Constant(value, constantType); + } + } - /// - /// Recognize $it.Source where $it is FlatteningWrapper - /// Using that do avoid wrapping it redundant into Null propagation - /// - /// The source. - /// The query binder context. - /// true/false. - private static bool IsFlatteningSource(Expression source, QueryBinderContext context) - { - var member = source as MemberExpression; - return member != null - && context.CurrentParameter.Type.IsGenericType - && context.CurrentParameter.Type.GetGenericTypeDefinition() == typeof(FlatteningWrapper<>) - && member.Expression == context.CurrentParameter; - } - #endregion - - #region Protected methods - /// - /// Bind function arguments - /// - /// The nodes. - /// The query binder context. - /// The LINQ created. - protected Expression[] BindArguments(IEnumerable nodes, QueryBinderContext context) - { - return nodes.OfType().Select(n => Bind(n, context)).ToArray(); - } - - /// - /// Gets property for dynamic properties dictionary. - /// - /// - /// The query binder context. - /// Returns CLR property for dynamic properties container. - protected static PropertyInfo GetDynamicPropertyContainer(SingleValueOpenPropertyAccessNode openNode, QueryBinderContext context) - { - if (openNode == null) - { - throw Error.ArgumentNull(nameof(openNode)); - } + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + public virtual Expression BindCollectionConstantNode(CollectionConstantNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context); - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + // It's fine if the collection is empty; the returned value will be an empty list. + ConstantNode firstNode = node.Collection.FirstOrDefault(); + object value = null; + if (firstNode != null) + { + value = firstNode.Value; + } - IEdmStructuredType edmStructuredType; - IEdmTypeReference edmTypeReference = openNode.Source.TypeReference; - if (edmTypeReference.IsEntity()) + Type constantType = RetrieveClrTypeForConstant(node.ItemType, context, ref value); + Type nullableConstantType = node.ItemType.IsNullable && constantType.IsValueType && Nullable.GetUnderlyingType(constantType) == null + ? typeof(Nullable<>).MakeGenericType(constantType) + : constantType; + Type listType = typeof(List<>).MakeGenericType(nullableConstantType); + IList castedList = Activator.CreateInstance(listType) as IList; + + // Getting a LINQ expression to dynamically cast each item in the Collection during runtime is tricky, + // so using a foreach loop and doing an implicit cast from object to the CLR type of ItemType. + foreach (ConstantNode item in node.Collection) + { + object member; + if (item.Value == null) { - edmStructuredType = edmTypeReference.AsEntity().EntityDefinition(); + member = null; } - else if (edmTypeReference.IsComplex()) + else if (constantType.IsEnum) { - edmStructuredType = edmTypeReference.AsComplex().ComplexDefinition(); + member = EnumDeserializationHelpers.ConvertEnumValue(item.Value, constantType); } else { - throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, openNode.Kind, typeof(QueryBinder).Name); + member = item.Value; } - return context.Model.GetDynamicPropertyDictionary(edmStructuredType); + castedList.Add(member); } - /// - /// Gets expression for property from previously aggregated query - /// - /// The property path. - /// The query binder context. - /// Returns null if no aggregations were used so far - protected Expression GetFlattenedPropertyExpression(string propertyPath, QueryBinderContext context) + if (context.QuerySettings.EnableConstantParameterization) { - if (context == null || context.FlattenedProperties == null || !context.FlattenedProperties.Any()) - { - return null; - } + return LinqParameterContainer.Parameterize(listType, castedList); + } - if (context.FlattenedProperties.TryGetValue(propertyPath, out var expression)) - { - return expression; - } + return Expression.Constant(castedList, listType); + } + #endregion - return null; - // TODO: sam xu, return null? - // throw new ODataException(Error.Format(SRResources.PropertyOrPathWasRemovedFromContext, propertyPath)); + #region Private helper methods + private static void ValidateAllStringArguments(string functionName, Expression[] arguments) + { + if (arguments.Any(arg => arg.Type != typeof(string))) + { + throw new ODataException(Error.Format(SRResources.FunctionNotSupportedOnEnum, functionName)); } - #endregion + } + + /// + /// Recognize $it.Source where $it is FlatteningWrapper + /// Using that do avoid wrapping it redundant into Null propagation + /// + /// The source. + /// The query binder context. + /// true/false. + private static bool IsFlatteningSource(Expression source, QueryBinderContext context) + { + var member = source as MemberExpression; + return member != null + && context.CurrentParameter.Type.IsGenericType + && context.CurrentParameter.Type.GetGenericTypeDefinition() == typeof(FlatteningWrapper<>) + && member.Expression == context.CurrentParameter; + } + #endregion - internal string GetFullPropertyPath(SingleValueNode node) + #region Protected methods + /// + /// Bind function arguments + /// + /// The nodes. + /// The query binder context. + /// The LINQ created. + protected Expression[] BindArguments(IEnumerable nodes, QueryBinderContext context) + { + return nodes.OfType().Select(n => Bind(n, context)).ToArray(); + } + + /// + /// Gets property for dynamic properties dictionary. + /// + /// + /// The query binder context. + /// Returns CLR property for dynamic properties container. + protected static PropertyInfo GetDynamicPropertyContainer(SingleValueOpenPropertyAccessNode openNode, QueryBinderContext context) + { + if (openNode == null) { - string path = null; - SingleValueNode parent = null; - switch (node.Kind) - { - case QueryNodeKind.SingleComplexNode: - var complexNode = (SingleComplexNode)node; - path = complexNode.Property.Name; - parent = complexNode.Source; - break; - case QueryNodeKind.SingleValuePropertyAccess: - var propertyNode = ((SingleValuePropertyAccessNode)node); - path = propertyNode.Property.Name; - parent = propertyNode.Source; - break; - case QueryNodeKind.SingleNavigationNode: - var navNode = ((SingleNavigationNode)node); - path = navNode.NavigationProperty.Name; - parent = navNode.Source; - break; - } + throw Error.ArgumentNull(nameof(openNode)); + } - if (parent != null) - { - var parentPath = GetFullPropertyPath(parent); - if (parentPath != null) - { - path = parentPath + "\\" + path; - } - } + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - return path; + IEdmStructuredType edmStructuredType; + IEdmTypeReference edmTypeReference = openNode.Source.TypeReference; + if (edmTypeReference.IsEntity()) + { + edmStructuredType = edmTypeReference.AsEntity().EntityDefinition(); } + else if (edmTypeReference.IsComplex()) + { + edmStructuredType = edmTypeReference.AsComplex().ComplexDefinition(); + } + else + { + throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, openNode.Kind, typeof(QueryBinder).Name); + } + + return context.Model.GetDynamicPropertyDictionary(edmStructuredType); + } - internal Expression CreatePropertyAccessExpression(Expression source, QueryBinderContext context, IEdmProperty property, string propertyPath = null) + /// + /// Gets expression for property from previously aggregated query + /// + /// The property path. + /// The query binder context. + /// Returns null if no aggregations were used so far + protected Expression GetFlattenedPropertyExpression(string propertyPath, QueryBinderContext context) + { + if (context == null || context.FlattenedProperties == null || !context.FlattenedProperties.Any()) { - string propertyName = context.Model.GetClrPropertyName(property); - propertyPath = propertyPath ?? propertyName; - if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && ExpressionBinderHelper.IsNullable(source.Type) && - source != context.CurrentParameter && - !IsFlatteningSource(source, context)) - { - Expression cleanSource = ExpressionBinderHelper.RemoveInnerNullPropagation(source, context.QuerySettings); - Expression propertyAccessExpression = null; - propertyAccessExpression = GetFlattenedPropertyExpression(propertyPath, context) ?? Expression.Property(cleanSource, propertyName); - - // source.property => source == null ? null : [CastToNullable]RemoveInnerNullPropagation(source).property - // Notice that we are checking if source is null already. so we can safely remove any null checks when doing source.Property - - Expression ifFalse = ExpressionBinderHelper.ToNullable(ConvertNonStandardPrimitives(propertyAccessExpression, context)); - return - Expression.Condition( - test: Expression.Equal(source, NullConstant), - ifTrue: Expression.Constant(null, ifFalse.Type), - ifFalse: ifFalse); - } - else - { - // return GetFlattenedPropertyExpression(propertyPath, context) - // ?? ConvertNonStandardPrimitives(GetPropertyExpression(source, (!propertyPath.Contains("\\", StringComparison.Ordinal) ? "Instance\\" : String.Empty) + propertyName), context); + return null; + } - bool isAggregated = context.ElementClrType == typeof(AggregationWrapper); + if (context.FlattenedProperties.TryGetValue(propertyPath, out var expression)) + { + return expression; + } - return GetFlattenedPropertyExpression(propertyPath, context) - ?? ConvertNonStandardPrimitives(GetPropertyExpression(source, propertyName, isAggregated), context); - } + return null; + // TODO: sam xu, return null? + // throw new ODataException(Error.Format(SRResources.PropertyOrPathWasRemovedFromContext, propertyPath)); + } + #endregion + + internal string GetFullPropertyPath(SingleValueNode node) + { + string path = null; + SingleValueNode parent = null; + switch (node.Kind) + { + case QueryNodeKind.SingleComplexNode: + var complexNode = (SingleComplexNode)node; + path = complexNode.Property.Name; + parent = complexNode.Source; + break; + case QueryNodeKind.SingleValuePropertyAccess: + var propertyNode = ((SingleValuePropertyAccessNode)node); + path = propertyNode.Property.Name; + parent = propertyNode.Source; + break; + case QueryNodeKind.SingleNavigationNode: + var navNode = ((SingleNavigationNode)node); + path = navNode.NavigationProperty.Name; + parent = navNode.Source; + break; } - internal static Expression GetPropertyExpression(Expression source, string propertyPath, bool isAggregated = false) + if (parent != null) { - string[] propertyNameParts = propertyPath.Split('\\'); - Expression propertyValue = source; - foreach (var propertyName in propertyNameParts) + var parentPath = GetFullPropertyPath(parent); + if (parentPath != null) { - // Trying to fix problem with $apply and $orderby. https://github.com/OData/AspNetCoreOData/issues/420 - if (isAggregated) - { - propertyValue = Expression.Property(propertyValue, "Values"); - var propertyInfo = typeof(Dictionary).GetProperty("Item"); - var arguments = new List { Expression.Constant(propertyName) }; - - propertyValue = Expression.MakeIndex(propertyValue, propertyInfo, arguments); - } - else - { - propertyValue = Expression.Property(propertyValue, propertyName); - } + path = parentPath + "\\" + path; } - return propertyValue; } - // If the expression is of non-standard edm primitive type (like uint), convert the expression to its standard edm type. - // Also, note that only expressions generated for ushort, uint and ulong can be understood by linq2sql and EF. - // The rest (char, char[], Binary) would cause issues with linq2sql and EF. - internal static Expression ConvertNonStandardPrimitives(Expression source, QueryBinderContext context) + return path; + } + + internal Expression CreatePropertyAccessExpression(Expression source, QueryBinderContext context, IEdmProperty property, string propertyPath = null) + { + string propertyName = context.Model.GetClrPropertyName(property); + propertyPath = propertyPath ?? propertyName; + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && ExpressionBinderHelper.IsNullable(source.Type) && + source != context.CurrentParameter && + !IsFlatteningSource(source, context)) { - bool isNonstandardEdmPrimitive; - Type conversionType = context.Model.IsNonstandardEdmPrimitive(source.Type, out isNonstandardEdmPrimitive); + Expression cleanSource = ExpressionBinderHelper.RemoveInnerNullPropagation(source, context.QuerySettings); + Expression propertyAccessExpression = null; + propertyAccessExpression = GetFlattenedPropertyExpression(propertyPath, context) ?? Expression.Property(cleanSource, propertyName); - if (isNonstandardEdmPrimitive) - { - Type sourceType = TypeHelper.GetUnderlyingTypeOrSelf(source.Type); + // source.property => source == null ? null : [CastToNullable]RemoveInnerNullPropagation(source).property + // Notice that we are checking if source is null already. so we can safely remove any null checks when doing source.Property - Contract.Assert(sourceType != conversionType); + Expression ifFalse = ExpressionBinderHelper.ToNullable(ConvertNonStandardPrimitives(propertyAccessExpression, context)); + return + Expression.Condition( + test: Expression.Equal(source, NullConstant), + ifTrue: Expression.Constant(null, ifFalse.Type), + ifFalse: ifFalse); + } + else + { + // return GetFlattenedPropertyExpression(propertyPath, context) + // ?? ConvertNonStandardPrimitives(GetPropertyExpression(source, (!propertyPath.Contains("\\", StringComparison.Ordinal) ? "Instance\\" : String.Empty) + propertyName), context); - Expression convertedExpression = null; + bool isAggregated = context.ElementClrType == typeof(AggregationWrapper); - if (TypeHelper.IsEnum(sourceType)) - { - // we handle enum conversions ourselves - convertedExpression = source; - } - else if (TypeHelper.IsDateOnly(sourceType) || TypeHelper.IsTimeOnly(sourceType)) - { - convertedExpression = source; - } - else - { - switch (Type.GetTypeCode(sourceType)) - { - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - convertedExpression = Expression.Convert(ExpressionBinderHelper.ExtractValueFromNullableExpression(source), conversionType); - break; - - case TypeCode.Char: - convertedExpression = Expression.Call(ExpressionBinderHelper.ExtractValueFromNullableExpression(source), "ToString", typeArguments: null, arguments: null); - break; - - case TypeCode.DateTime: - convertedExpression = source; - break; - - case TypeCode.Object: - if (sourceType == typeof(char[])) - { - convertedExpression = Expression.New(typeof(string).GetConstructor(new[] { typeof(char[]) }), source); - } - else if (sourceType == typeof(XElement)) - { - convertedExpression = Expression.Call(source, "ToString", typeArguments: null, arguments: null); - } -#if NETFX // System.Data.Linq.Binary is only supported in the AspNet version. - else if (sourceType == typeof(Binary)) - { - convertedExpression = Expression.Call(source, "ToArray", typeArguments: null, arguments: null); - } -#endif - break; + return GetFlattenedPropertyExpression(propertyPath, context) + ?? ConvertNonStandardPrimitives(GetPropertyExpression(source, propertyName, isAggregated), context); + } + } - default: - Contract.Assert(false, Error.Format("missing non-standard type support for {0}", sourceType.Name)); - break; - } - } + internal static Expression GetPropertyExpression(Expression source, string propertyPath, bool isAggregated = false) + { + string[] propertyNameParts = propertyPath.Split('\\'); + Expression propertyValue = source; + foreach (var propertyName in propertyNameParts) + { + // Trying to fix problem with $apply and $orderby. https://github.com/OData/AspNetCoreOData/issues/420 + if (isAggregated) + { + propertyValue = Expression.Property(propertyValue, "Values"); + var propertyInfo = typeof(Dictionary).GetProperty("Item"); + var arguments = new List { Expression.Constant(propertyName) }; - if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && ExpressionBinderHelper.IsNullable(source.Type)) - { - // source == null ? null : source - return Expression.Condition( - ExpressionBinderHelper.CheckForNull(source), - ifTrue: Expression.Constant(null, ExpressionBinderHelper.ToNullable(convertedExpression.Type)), - ifFalse: ExpressionBinderHelper.ToNullable(convertedExpression)); - } - else - { - return convertedExpression; - } + propertyValue = Expression.MakeIndex(propertyValue, propertyInfo, arguments); + } + else + { + propertyValue = Expression.Property(propertyValue, propertyName); } - - return source; } + return propertyValue; + } - internal Expression CreateConvertExpression(ConvertNode convertNode, Expression source, QueryBinderContext context) + // If the expression is of non-standard edm primitive type (like uint), convert the expression to its standard edm type. + // Also, note that only expressions generated for ushort, uint and ulong can be understood by linq2sql and EF. + // The rest (char, char[], Binary) would cause issues with linq2sql and EF. + internal static Expression ConvertNonStandardPrimitives(Expression source, QueryBinderContext context) + { + bool isNonstandardEdmPrimitive; + Type conversionType = context.Model.IsNonstandardEdmPrimitive(source.Type, out isNonstandardEdmPrimitive); + + if (isNonstandardEdmPrimitive) { - Type conversionType = context.Model.GetClrType(convertNode.TypeReference, context.AssembliesResolver); + Type sourceType = TypeHelper.GetUnderlyingTypeOrSelf(source.Type); + + Contract.Assert(sourceType != conversionType); - if (conversionType == typeof(bool?) && source.Type == typeof(bool)) + Expression convertedExpression = null; + + if (TypeHelper.IsEnum(sourceType)) { - // we handle null propagation ourselves. So, if converting from bool to Nullable ignore. - return source; + // we handle enum conversions ourselves + convertedExpression = source; } - else if (conversionType == typeof(Date?) && - (source.Type == typeof(DateTimeOffset?) || source.Type == typeof(DateTime?))) + else if (TypeHelper.IsDateOnly(sourceType) || TypeHelper.IsTimeOnly(sourceType)) { - return source; + convertedExpression = source; } - if ((conversionType == typeof(TimeOfDay?) && source.Type == typeof(TimeOfDay)) || - ((conversionType == typeof(Date?) && source.Type == typeof(Date)))) + else { - return source; + switch (Type.GetTypeCode(sourceType)) + { + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + convertedExpression = Expression.Convert(ExpressionBinderHelper.ExtractValueFromNullableExpression(source), conversionType); + break; + + case TypeCode.Char: + convertedExpression = Expression.Call(ExpressionBinderHelper.ExtractValueFromNullableExpression(source), "ToString", typeArguments: null, arguments: null); + break; + + case TypeCode.DateTime: + convertedExpression = source; + break; + + case TypeCode.Object: + if (sourceType == typeof(char[])) + { + convertedExpression = Expression.New(typeof(string).GetConstructor(new[] { typeof(char[]) }), source); + } + else if (sourceType == typeof(XElement)) + { + convertedExpression = Expression.Call(source, "ToString", typeArguments: null, arguments: null); + } +#if NETFX // System.Data.Linq.Binary is only supported in the AspNet version. + else if (sourceType == typeof(Binary)) + { + convertedExpression = Expression.Call(source, "ToArray", typeArguments: null, arguments: null); + } +#endif + break; + + default: + Contract.Assert(false, Error.Format("missing non-standard type support for {0}", sourceType.Name)); + break; + } } - else if (conversionType == typeof(TimeOfDay?) && - (source.Type == typeof(DateTimeOffset?) || source.Type == typeof(DateTime?) || source.Type == typeof(TimeSpan?))) + + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && ExpressionBinderHelper.IsNullable(source.Type)) { - return source; + // source == null ? null : source + return Expression.Condition( + ExpressionBinderHelper.CheckForNull(source), + ifTrue: Expression.Constant(null, ExpressionBinderHelper.ToNullable(convertedExpression.Type)), + ifFalse: ExpressionBinderHelper.ToNullable(convertedExpression)); } - else if (ExpressionBinderHelper.IsDateAndTimeRelated(conversionType) && ExpressionBinderHelper.IsDateAndTimeRelated(source.Type)) + else { - return source; + return convertedExpression; } - else if (source == NullConstant) + } + + return source; + } + + internal Expression CreateConvertExpression(ConvertNode convertNode, Expression source, QueryBinderContext context) + { + Type conversionType = context.Model.GetClrType(convertNode.TypeReference, context.AssembliesResolver); + + if (conversionType == typeof(bool?) && source.Type == typeof(bool)) + { + // we handle null propagation ourselves. So, if converting from bool to Nullable ignore. + return source; + } + else if (conversionType == typeof(Date?) && + (source.Type == typeof(DateTimeOffset?) || source.Type == typeof(DateTime?))) + { + return source; + } + if ((conversionType == typeof(TimeOfDay?) && source.Type == typeof(TimeOfDay)) || + ((conversionType == typeof(Date?) && source.Type == typeof(Date)))) + { + return source; + } + else if (conversionType == typeof(TimeOfDay?) && + (source.Type == typeof(DateTimeOffset?) || source.Type == typeof(DateTime?) || source.Type == typeof(TimeSpan?))) + { + return source; + } + else if (ExpressionBinderHelper.IsDateAndTimeRelated(conversionType) && ExpressionBinderHelper.IsDateAndTimeRelated(source.Type)) + { + return source; + } + else if (source == NullConstant) + { + return source; + } + else + { + if (TypeHelper.IsEnum(source.Type)) { + // we handle enum conversions ourselves return source; } else { - if (TypeHelper.IsEnum(source.Type)) + // if a cast is from Nullable to Non-Nullable we need to check if source is null + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True + && ExpressionBinderHelper.IsNullable(source.Type) && !ExpressionBinderHelper.IsNullable(conversionType)) { - // we handle enum conversions ourselves - return source; + // source == null ? null : source.Value + return + Expression.Condition( + test: ExpressionBinderHelper.CheckForNull(source), + ifTrue: Expression.Constant(null, ExpressionBinderHelper.ToNullable(conversionType)), + ifFalse: Expression.Convert(ExpressionBinderHelper.ExtractValueFromNullableExpression(source), ExpressionBinderHelper.ToNullable(conversionType))); } else { - // if a cast is from Nullable to Non-Nullable we need to check if source is null - if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True - && ExpressionBinderHelper.IsNullable(source.Type) && !ExpressionBinderHelper.IsNullable(conversionType)) - { - // source == null ? null : source.Value - return - Expression.Condition( - test: ExpressionBinderHelper.CheckForNull(source), - ifTrue: Expression.Constant(null, ExpressionBinderHelper.ToNullable(conversionType)), - ifFalse: Expression.Convert(ExpressionBinderHelper.ExtractValueFromNullableExpression(source), ExpressionBinderHelper.ToNullable(conversionType))); - } - else - { - return Expression.Convert(source, conversionType); - } + return Expression.Convert(source, conversionType); } } } + } - internal Type RetrieveClrTypeForConstant(IEdmTypeReference edmTypeReference, QueryBinderContext context, ref object value) - { - Type constantType = context.Model.GetClrType(edmTypeReference, context.AssembliesResolver); + internal Type RetrieveClrTypeForConstant(IEdmTypeReference edmTypeReference, QueryBinderContext context, ref object value) + { + Type constantType = context.Model.GetClrType(edmTypeReference, context.AssembliesResolver); - if (value != null && edmTypeReference != null && edmTypeReference.IsEnum()) - { - ODataEnumValue odataEnumValue = (ODataEnumValue)value; - string strValue = odataEnumValue.Value; - Contract.Assert(strValue != null); + if (value != null && edmTypeReference != null && edmTypeReference.IsEnum()) + { + ODataEnumValue odataEnumValue = (ODataEnumValue)value; + string strValue = odataEnumValue.Value; + Contract.Assert(strValue != null); - constantType = Nullable.GetUnderlyingType(constantType) ?? constantType; + constantType = Nullable.GetUnderlyingType(constantType) ?? constantType; - IEdmEnumType enumType = edmTypeReference.AsEnum().EnumDefinition(); - ClrEnumMemberAnnotation memberMapAnnotation = context.Model.GetClrEnumMemberAnnotation(enumType); - if (memberMapAnnotation != null) + IEdmEnumType enumType = edmTypeReference.AsEnum().EnumDefinition(); + ClrEnumMemberAnnotation memberMapAnnotation = context.Model.GetClrEnumMemberAnnotation(enumType); + if (memberMapAnnotation != null) + { + IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name == strValue); + if (enumMember == null) { - IEdmEnumMember enumMember = enumType.Members.FirstOrDefault(m => m.Name == strValue); - if (enumMember == null) - { - enumMember = enumType.Members.FirstOrDefault(m => m.Value.ToString() == strValue); - } + enumMember = enumType.Members.FirstOrDefault(m => m.Value.ToString() == strValue); + } - if (enumMember != null) + if (enumMember != null) + { + Enum clrMember = memberMapAnnotation.GetClrEnumMember(enumMember); + if (clrMember != null) { - Enum clrMember = memberMapAnnotation.GetClrEnumMember(enumMember); - if (clrMember != null) - { - value = clrMember; - } - else - { - throw new ODataException(Error.Format(SRResources.CannotGetEnumClrMember, enumMember.Name)); - } + value = clrMember; } else { - value = Enum.Parse(constantType, strValue); + throw new ODataException(Error.Format(SRResources.CannotGetEnumClrMember, enumMember.Name)); } } else @@ -1313,156 +1308,160 @@ internal Type RetrieveClrTypeForConstant(IEdmTypeReference edmTypeReference, Que value = Enum.Parse(constantType, strValue); } } - - if (edmTypeReference != null && - edmTypeReference.IsNullable && - (edmTypeReference.IsDate() || edmTypeReference.IsTimeOfDay())) + else { - constantType = Nullable.GetUnderlyingType(constantType) ?? constantType; + value = Enum.Parse(constantType, strValue); } - - return constantType; } - internal Expression BindCastToEnumType(Type sourceType, Type targetClrType, QueryNode firstParameter, int parameterLength, QueryBinderContext context) + if (edmTypeReference != null && + edmTypeReference.IsNullable && + (edmTypeReference.IsDate() || edmTypeReference.IsTimeOfDay())) { - Type enumType = TypeHelper.GetUnderlyingTypeOrSelf(targetClrType); - ConstantNode sourceNode = firstParameter as ConstantNode; + constantType = Nullable.GetUnderlyingType(constantType) ?? constantType; + } - if (parameterLength == 1 || sourceNode == null || sourceType != typeof(string)) - { - // We only support to cast Enumeration type from constant string now, - // because LINQ to Entities does not recognize the method Enum.TryParse. - return NullConstant; - } - else - { - object[] parameters = new[] { sourceNode.Value, Enum.ToObject(enumType, 0) }; - bool isSuccessful = (bool)EnumTryParseMethod.MakeGenericMethod(enumType).Invoke(null, parameters); + return constantType; + } - if (isSuccessful) - { - if (context.QuerySettings.EnableConstantParameterization) - { - return LinqParameterContainer.Parameterize(targetClrType, parameters[1]); - } - else - { - return Expression.Constant(parameters[1], targetClrType); - } - } - else - { - return NullConstant; - } - } - } + internal Expression BindCastToEnumType(Type sourceType, Type targetClrType, QueryNode firstParameter, int parameterLength, QueryBinderContext context) + { + Type enumType = TypeHelper.GetUnderlyingTypeOrSelf(targetClrType); + ConstantNode sourceNode = firstParameter as ConstantNode; - private static Expression Any(Expression source, Expression filter) + if (parameterLength == 1 || sourceNode == null || sourceType != typeof(string)) + { + // We only support to cast Enumeration type from constant string now, + // because LINQ to Entities does not recognize the method Enum.TryParse. + return NullConstant; + } + else { - Contract.Assert(source != null); - Type elementType; - TypeHelper.IsCollection(source.Type, out elementType); - Contract.Assert(elementType != null); + object[] parameters = new[] { sourceNode.Value, Enum.ToObject(enumType, 0) }; + bool isSuccessful = (bool)EnumTryParseMethod.MakeGenericMethod(enumType).Invoke(null, parameters); - if (filter == null) + if (isSuccessful) { - if (ExpressionBinderHelper.IsIQueryable(source.Type)) + if (context.QuerySettings.EnableConstantParameterization) { - return Expression.Call(null, ExpressionHelperMethods.QueryableEmptyAnyGeneric.MakeGenericMethod(elementType), source); + return LinqParameterContainer.Parameterize(targetClrType, parameters[1]); } else { - return Expression.Call(null, ExpressionHelperMethods.EnumerableEmptyAnyGeneric.MakeGenericMethod(elementType), source); + return Expression.Constant(parameters[1], targetClrType); } } else { - if (ExpressionBinderHelper.IsIQueryable(source.Type)) - { - return Expression.Call(null, ExpressionHelperMethods.QueryableNonEmptyAnyGeneric.MakeGenericMethod(elementType), source, filter); - } - else - { - return Expression.Call(null, ExpressionHelperMethods.EnumerableNonEmptyAnyGeneric.MakeGenericMethod(elementType), source, filter); - } + return NullConstant; } } + } - private static Expression All(Expression source, Expression filter) - { - Contract.Assert(source != null); - Contract.Assert(filter != null); - - Type elementType; - TypeHelper.IsCollection(source.Type, out elementType); - Contract.Assert(elementType != null); + private static Expression Any(Expression source, Expression filter) + { + Contract.Assert(source != null); + Type elementType; + TypeHelper.IsCollection(source.Type, out elementType); + Contract.Assert(elementType != null); + if (filter == null) + { if (ExpressionBinderHelper.IsIQueryable(source.Type)) { - return Expression.Call(null, ExpressionHelperMethods.QueryableAllGeneric.MakeGenericMethod(elementType), source, filter); + return Expression.Call(null, ExpressionHelperMethods.QueryableEmptyAnyGeneric.MakeGenericMethod(elementType), source); } else { - return Expression.Call(null, ExpressionHelperMethods.EnumerableAllGeneric.MakeGenericMethod(elementType), source, filter); + return Expression.Call(null, ExpressionHelperMethods.EnumerableEmptyAnyGeneric.MakeGenericMethod(elementType), source); } } - - private Expression BindPropertyAccessExpression(SingleValueOpenPropertyAccessNode openNode, PropertyInfo prop, QueryBinderContext context) + else { - var source = Bind(openNode.Source, context); - Expression propertyAccessExpression; - if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && - ExpressionBinderHelper.IsNullable(source.Type) && source != context.CurrentParameter) + if (ExpressionBinderHelper.IsIQueryable(source.Type)) { - propertyAccessExpression = Expression.Property(ExpressionBinderHelper.RemoveInnerNullPropagation(source, context.QuerySettings), prop.Name); + return Expression.Call(null, ExpressionHelperMethods.QueryableNonEmptyAnyGeneric.MakeGenericMethod(elementType), source, filter); } else { - propertyAccessExpression = Expression.Property(source, prop.Name); + return Expression.Call(null, ExpressionHelperMethods.EnumerableNonEmptyAnyGeneric.MakeGenericMethod(elementType), source, filter); } - return propertyAccessExpression; } + } - /// - /// Apply null propagation for filter body. - /// - /// The body. - /// The query binder context. - /// The LINQ created. - protected static Expression ApplyNullPropagationForFilterBody(Expression body, QueryBinderContext context) - { - Contract.Assert(body != null); - Contract.Assert(context != null); + private static Expression All(Expression source, Expression filter) + { + Contract.Assert(source != null); + Contract.Assert(filter != null); - if (ExpressionBinderHelper.IsNullable(body.Type)) - { - if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) - { - // handle null as false - // body => body == true. passing liftToNull:false would convert null to false. - body = Expression.Equal(body, Expression.Constant(true, typeof(bool?)), liftToNull: false, method: null); - } - else - { - body = Expression.Convert(body, typeof(bool)); - } - } + Type elementType; + TypeHelper.IsCollection(source.Type, out elementType); + Contract.Assert(elementType != null); - return body; + if (ExpressionBinderHelper.IsIQueryable(source.Type)) + { + return Expression.Call(null, ExpressionHelperMethods.QueryableAllGeneric.MakeGenericMethod(elementType), source, filter); } + else + { + return Expression.Call(null, ExpressionHelperMethods.EnumerableAllGeneric.MakeGenericMethod(elementType), source, filter); + } + } - private static void CheckArgumentNull(T node, QueryBinderContext context) where T : QueryNode + private Expression BindPropertyAccessExpression(SingleValueOpenPropertyAccessNode openNode, PropertyInfo prop, QueryBinderContext context) + { + var source = Bind(openNode.Source, context); + Expression propertyAccessExpression; + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True && + ExpressionBinderHelper.IsNullable(source.Type) && source != context.CurrentParameter) { - if (node == null) + propertyAccessExpression = Expression.Property(ExpressionBinderHelper.RemoveInnerNullPropagation(source, context.QuerySettings), prop.Name); + } + else + { + propertyAccessExpression = Expression.Property(source, prop.Name); + } + return propertyAccessExpression; + } + + /// + /// Apply null propagation for filter body. + /// + /// The body. + /// The query binder context. + /// The LINQ created. + protected static Expression ApplyNullPropagationForFilterBody(Expression body, QueryBinderContext context) + { + Contract.Assert(body != null); + Contract.Assert(context != null); + + if (ExpressionBinderHelper.IsNullable(body.Type)) + { + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) { - throw Error.ArgumentNull(nameof(node)); + // handle null as false + // body => body == true. passing liftToNull:false would convert null to false. + body = Expression.Equal(body, Expression.Constant(true, typeof(bool?)), liftToNull: false, method: null); } - - if (context == null) + else { - throw Error.ArgumentNull(nameof(context)); + body = Expression.Convert(body, typeof(bool)); } } + + return body; + } + + private static void CheckArgumentNull(T node, QueryBinderContext context) where T : QueryNode + { + if (node == null) + { + throw Error.ArgumentNull(nameof(node)); + } + + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinderContext.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinderContext.cs index ef5e6f26d..c28c04975 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinderContext.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinderContext.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -17,262 +17,261 @@ using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// Encapsulates all binder information about an individual OData query option binding. +/// +public class QueryBinderContext { /// - /// Encapsulates all binder information about an individual OData query option binding. + /// The parameter name for root type.(it could be renamed as $root). + /// + private const string DollarIt = "$it"; + + /// + /// The parameter name for current type. + /// + private const string DollarThis = "$this"; + + /// + /// All parameters present in current context. + /// + private IDictionary _lambdaParameters; + + /// + /// Initializes a new instance of the class. + /// For unit-test only + /// + internal QueryBinderContext() + { } + + /// + /// Initializes a new instance of the class. /// - public class QueryBinderContext + /// The Edm model. + /// The query setting. + /// The current element CLR type in this context (scope). + public QueryBinderContext(IEdmModel model, ODataQuerySettings querySettings, Type clrType) { - /// - /// The parameter name for root type.(it could be renamed as $root). - /// - private const string DollarIt = "$it"; - - /// - /// The parameter name for current type. - /// - private const string DollarThis = "$this"; - - /// - /// All parameters present in current context. - /// - private IDictionary _lambdaParameters; - - /// - /// Initializes a new instance of the class. - /// For unit-test only - /// - internal QueryBinderContext() - { } - - /// - /// Initializes a new instance of the class. - /// - /// The Edm model. - /// The query setting. - /// The current element CLR type in this context (scope). - public QueryBinderContext(IEdmModel model, ODataQuerySettings querySettings, Type clrType) - { - Model = model ?? throw Error.ArgumentNull(nameof(model)); + Model = model ?? throw Error.ArgumentNull(nameof(model)); - QuerySettings = querySettings ?? throw Error.ArgumentNull(nameof(querySettings)); + QuerySettings = querySettings ?? throw Error.ArgumentNull(nameof(querySettings)); - ElementClrType = clrType ?? throw Error.ArgumentNull(nameof(clrType)); + ElementClrType = clrType ?? throw Error.ArgumentNull(nameof(clrType)); - ElementType = Model.GetEdmTypeReference(ElementClrType)?.Definition; + ElementType = Model.GetEdmTypeReference(ElementClrType)?.Definition; - // Check if element type is null and not of AggregationWrapper type and not of NoGroupByAggregationWrapper type. - if (ElementType == null && ElementClrType != typeof(AggregationWrapper) && ElementClrType != typeof(NoGroupByAggregationWrapper)) - { - throw new ODataException(Error.Format(SRResources.ClrTypeNotInModel, ElementClrType.FullName)); - } + // Check if element type is null and not of AggregationWrapper type and not of NoGroupByAggregationWrapper type. + if (ElementType == null && ElementClrType != typeof(AggregationWrapper) && ElementClrType != typeof(NoGroupByAggregationWrapper)) + { + throw new ODataException(Error.Format(SRResources.ClrTypeNotInModel, ElementClrType.FullName)); + } - // Customers?$select=EmailAddresses($filter=endswith($this,'.com') and starswith($it/Name, 'Sam')) - // Here: - // $this -> instance in EmailAddresses - // $it -> instance in Customers - // When we process $select=..., we create QueryBindContext, the input clrType is "Customer". - // When we process nested $filter, we create another QueryBindContext, the input clrType is "string". - ParameterExpression thisParameters = Expression.Parameter(clrType, DollarIt); - _lambdaParameters = new Dictionary(); + // Customers?$select=EmailAddresses($filter=endswith($this,'.com') and starswith($it/Name, 'Sam')) + // Here: + // $this -> instance in EmailAddresses + // $it -> instance in Customers + // When we process $select=..., we create QueryBindContext, the input clrType is "Customer". + // When we process nested $filter, we create another QueryBindContext, the input clrType is "string". + ParameterExpression thisParameters = Expression.Parameter(clrType, DollarIt); + _lambdaParameters = new Dictionary(); - // So, from top level, $it and $this are the same parameters - _lambdaParameters[DollarIt] = thisParameters; - _lambdaParameters[DollarThis] = thisParameters; + // So, from top level, $it and $this are the same parameters + _lambdaParameters[DollarIt] = thisParameters; + _lambdaParameters[DollarThis] = thisParameters; - // Categories?$expand=Products($filter=OrderItems/any(oi:oi/UnitPrice ne UnitPrice) + // Categories?$expand=Products($filter=OrderItems/any(oi:oi/UnitPrice ne UnitPrice) + } + + /// + /// Initializes a new instance of the class. + /// + /// The parent query binder context. + /// The query setting. + /// The current element CLR type in this context (scope). + public QueryBinderContext(QueryBinderContext context, ODataQuerySettings querySettings, Type clrType) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); } - /// - /// Initializes a new instance of the class. - /// - /// The parent query binder context. - /// The query setting. - /// The current element CLR type in this context (scope). - public QueryBinderContext(QueryBinderContext context, ODataQuerySettings querySettings, Type clrType) + QuerySettings = querySettings ?? throw Error.ArgumentNull(nameof(querySettings)); + + // ~/Customers?$select=Addresses($orderby=$this/ZipCode,$it/Age;$select=Codes($orderby=$this desc,$it/Name)) + + ElementClrType = clrType ?? throw Error.ArgumentNull(nameof(clrType)); + + Model = context.Model; + + ElementType = Model.GetEdmTypeReference(ElementClrType)?.Definition; + + if (ElementType == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw new ODataException(Error.Format(SRResources.ClrTypeNotInModel, ElementClrType.FullName)); + } - QuerySettings = querySettings ?? throw Error.ArgumentNull(nameof(querySettings)); + // Inherit the lambda parameters, $it, $this, etc. + _lambdaParameters = new Dictionary(context._lambdaParameters); - // ~/Customers?$select=Addresses($orderby=$this/ZipCode,$it/Age;$select=Codes($orderby=$this desc,$it/Name)) + // Only update $this parameter. + ParameterExpression thisParameters = Expression.Parameter(clrType, DollarIt); + _lambdaParameters[DollarThis] = thisParameters; - ElementClrType = clrType ?? throw Error.ArgumentNull(nameof(clrType)); + IsNested = true; + EnableSkipToken = context.EnableSkipToken; + } - Model = context.Model; + /// + /// Gets the Edm model. + /// + public IEdmModel Model { get; } - ElementType = Model.GetEdmTypeReference(ElementClrType)?.Definition; + /// + /// Gets the query settings. + /// + public ODataQuerySettings QuerySettings { get; } - if (ElementType == null) - { - throw new ODataException(Error.Format(SRResources.ClrTypeNotInModel, ElementClrType.FullName)); - } + /// + /// Gets the Element Clr type. + /// + public Type ElementClrType { get; } - // Inherit the lambda parameters, $it, $this, etc. - _lambdaParameters = new Dictionary(context._lambdaParameters); + /// + /// Gets or sets the assembly resolver. + /// + public IAssemblyResolver AssembliesResolver { get; set; } - // Only update $this parameter. - ParameterExpression thisParameters = Expression.Parameter(clrType, DollarIt); - _lambdaParameters[DollarThis] = thisParameters; + /// + /// Gets the compute expressions. + /// + public IDictionary ComputedProperties { get; } = new Dictionary(); - IsNested = true; - EnableSkipToken = context.EnableSkipToken; - } + /// + /// Gets/sets the orderby clause. + /// Since we 'overlap' the orderby clause to list, then the 'ThenBy' does NOT matter again, please ignore it. + /// + internal List OrderByClauses { get; set; } - /// - /// Gets the Edm model. - /// - public IEdmModel Model { get; } - - /// - /// Gets the query settings. - /// - public ODataQuerySettings QuerySettings { get; } - - /// - /// Gets the Element Clr type. - /// - public Type ElementClrType { get; } - - /// - /// Gets or sets the assembly resolver. - /// - public IAssemblyResolver AssembliesResolver { get; set; } - - /// - /// Gets the compute expressions. - /// - public IDictionary ComputedProperties { get; } = new Dictionary(); - - /// - /// Gets/sets the orderby clause. - /// Since we 'overlap' the orderby clause to list, then the 'ThenBy' does NOT matter again, please ignore it. - /// - internal List OrderByClauses { get; set; } - - /// - /// Gets/sets the enable skip token, it's for nested orderby. - /// - internal bool EnableSkipToken { get; set; } = false; - - /// - /// Flattened list of properties from base query, for case when binder is applied for aggregated query. - /// - internal IDictionary FlattenedProperties { get; private set; } = new Dictionary(); - - /// - /// Gets the of the element type. - /// - public IEdmType ElementType { get; } - - /// - /// Gets the that contains the element. - /// - public IEdmNavigationSource NavigationSource { get; set; } - - internal bool IsNested { get; } = false; - - /// - /// Gets the current parameter. Current parameter is the parameter at root of this context. - /// - public ParameterExpression CurrentParameter => _lambdaParameters[DollarThis]; - - /// - /// Gets or sets the source. - /// Basically for $compute in $select and $expand - /// - public Expression Source { get;set; } - - /// - /// Gets the parameter using parameter name. - /// - /// The parameter name. - /// The parameter expression. - public ParameterExpression GetParameter(string name) + /// + /// Gets/sets the enable skip token, it's for nested orderby. + /// + internal bool EnableSkipToken { get; set; } = false; + + /// + /// Flattened list of properties from base query, for case when binder is applied for aggregated query. + /// + internal IDictionary FlattenedProperties { get; private set; } = new Dictionary(); + + /// + /// Gets the of the element type. + /// + public IEdmType ElementType { get; } + + /// + /// Gets the that contains the element. + /// + public IEdmNavigationSource NavigationSource { get; set; } + + internal bool IsNested { get; } = false; + + /// + /// Gets the current parameter. Current parameter is the parameter at root of this context. + /// + public ParameterExpression CurrentParameter => _lambdaParameters[DollarThis]; + + /// + /// Gets or sets the source. + /// Basically for $compute in $select and $expand + /// + public Expression Source { get;set; } + + /// + /// Gets the parameter using parameter name. + /// + /// The parameter name. + /// The parameter expression. + public ParameterExpression GetParameter(string name) + { + return _lambdaParameters[name]; + } + + /// + /// Remove the parameter. + /// + /// The parameter name. + public void RemoveParameter(string name) + { + if (name != null) { - return _lambdaParameters[name]; + _lambdaParameters.Remove(name); } + } - /// - /// Remove the parameter. - /// - /// The parameter name. - public void RemoveParameter(string name) + internal (string, ParameterExpression) HandleLambdaParameters(IEnumerable rangeVariables) + { + ParameterExpression lambdaIt = null; + string name = null; + foreach (RangeVariable rangeVariable in rangeVariables) { - if (name != null) + // If the range variable exists, it's from upper layer, skip it. + if (_lambdaParameters.ContainsKey(rangeVariable.Name)) { - _lambdaParameters.Remove(name); + continue; } - } - internal (string, ParameterExpression) HandleLambdaParameters(IEnumerable rangeVariables) - { - ParameterExpression lambdaIt = null; - string name = null; - foreach (RangeVariable rangeVariable in rangeVariables) + // Work-around issue 481323 where UriParser yields a collection parameter type + // for primitive collections rather than the inner element type of the collection. + // Remove this block of code when 481323 is resolved. + IEdmTypeReference edmTypeReference = rangeVariable.TypeReference; + IEdmCollectionTypeReference collectionTypeReference = edmTypeReference as IEdmCollectionTypeReference; + if (collectionTypeReference != null) { - // If the range variable exists, it's from upper layer, skip it. - if (_lambdaParameters.ContainsKey(rangeVariable.Name)) + IEdmCollectionType collectionType = collectionTypeReference.Definition as IEdmCollectionType; + if (collectionType != null) { - continue; + edmTypeReference = collectionType.ElementType; } + } - // Work-around issue 481323 where UriParser yields a collection parameter type - // for primitive collections rather than the inner element type of the collection. - // Remove this block of code when 481323 is resolved. - IEdmTypeReference edmTypeReference = rangeVariable.TypeReference; - IEdmCollectionTypeReference collectionTypeReference = edmTypeReference as IEdmCollectionTypeReference; - if (collectionTypeReference != null) - { - IEdmCollectionType collectionType = collectionTypeReference.Definition as IEdmCollectionType; - if (collectionType != null) - { - edmTypeReference = collectionType.ElementType; - } - } + ParameterExpression parameter = Expression.Parameter(Model.GetClrType(edmTypeReference, AssembliesResolver), rangeVariable.Name); + Contract.Assert(lambdaIt == null, "There can be only one parameter in an Any/All lambda"); + lambdaIt = parameter; - ParameterExpression parameter = Expression.Parameter(Model.GetClrType(edmTypeReference, AssembliesResolver), rangeVariable.Name); - Contract.Assert(lambdaIt == null, "There can be only one parameter in an Any/All lambda"); - lambdaIt = parameter; + _lambdaParameters.Add(rangeVariable.Name, parameter); + name = rangeVariable.Name; + } - _lambdaParameters.Add(rangeVariable.Name, parameter); - name = rangeVariable.Name; - } + // OData spec supports any() / all() + //if (lambdaIt == null) + //{ + // throw new ODataException("TODO: There can be only one parameter in an Any/All lambda"); + //} - // OData spec supports any() / all() - //if (lambdaIt == null) - //{ - // throw new ODataException("TODO: There can be only one parameter in an Any/All lambda"); - //} + return (name, lambdaIt); + } - return (name, lambdaIt); + internal void AddComputedProperties(IEnumerable computedProperties) + { + if (computedProperties == null) + { + return; } - internal void AddComputedProperties(IEnumerable computedProperties) + foreach (var property in computedProperties) { - if (computedProperties == null) - { - return; - } - - foreach (var property in computedProperties) - { - ComputedProperties[property.Alias] = property; - } + ComputedProperties[property.Alias] = property; } + } - internal void EnsureFlattenedProperties(ParameterExpression source, IQueryable query) + internal void EnsureFlattenedProperties(ParameterExpression source, IQueryable query) + { + TransformationBinderBase binder = new TransformationBinderBase(this.QuerySettings, this.AssembliesResolver, this.ElementClrType, this.Model) { - TransformationBinderBase binder = new TransformationBinderBase(this.QuerySettings, this.AssembliesResolver, this.ElementClrType, this.Model) - { - BaseQuery = query - }; + BaseQuery = query + }; - this.FlattenedProperties = binder.GetFlattenedProperties(source); - } + this.FlattenedProperties = binder.GetFlattenedProperties(source); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/SelectExpandBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/SelectExpandBinder.cs index d49e97546..62b592515 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/SelectExpandBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/SelectExpandBinder.cs @@ -21,1520 +21,1519 @@ using Microsoft.OData.ModelBuilder.Config; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// Exposes the ability to translate an OData $select or $expand parse tree represented by to +/// an . +/// +public class SelectExpandBinder : QueryBinder, ISelectExpandBinder { /// - /// Exposes the ability to translate an OData $select or $expand parse tree represented by to - /// an . + /// Initializes a new instance of the class. + /// Select and Expand binder depends on and to process inner $filter and $orderby. /// - public class SelectExpandBinder : QueryBinder, ISelectExpandBinder + /// The injected filter binder. + /// The injected orderby binder. + public SelectExpandBinder(IFilterBinder filterBinder, IOrderByBinder orderByBinder) { - /// - /// Initializes a new instance of the class. - /// Select and Expand binder depends on and to process inner $filter and $orderby. - /// - /// The injected filter binder. - /// The injected orderby binder. - public SelectExpandBinder(IFilterBinder filterBinder, IOrderByBinder orderByBinder) - { - FilterBinder = filterBinder ?? throw Error.ArgumentNull(nameof(filterBinder)); - OrderByBinder = orderByBinder ?? throw Error.ArgumentNull(nameof(orderByBinder)); - } - - /// - /// For unit test only. - /// - internal SelectExpandBinder() - : this(new FilterBinder(), new OrderByBinder()) - { } - - /// - /// Gets the filter binder. - /// - public IFilterBinder FilterBinder { get; } - - /// - /// Gets the orderby binder. - /// - public IOrderByBinder OrderByBinder { get; } - - /// - /// Translate an OData $select or $expand tree represented by to an . - /// - /// The original . - /// An instance of the . - /// The $select and $expand binder result. - public virtual Expression BindSelectExpand(SelectExpandClause selectExpandClause, QueryBinderContext context) - { - if (selectExpandClause == null) - { - throw Error.ArgumentNull(nameof(selectExpandClause)); - } + FilterBinder = filterBinder ?? throw Error.ArgumentNull(nameof(filterBinder)); + OrderByBinder = orderByBinder ?? throw Error.ArgumentNull(nameof(orderByBinder)); + } - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + /// + /// For unit test only. + /// + internal SelectExpandBinder() + : this(new FilterBinder(), new OrderByBinder()) + { } - IEdmStructuredType structuredType = context.ElementType as IEdmStructuredType; - IEdmNavigationSource navigationSource = context.NavigationSource; - ParameterExpression source = context.CurrentParameter; + /// + /// Gets the filter binder. + /// + public IFilterBinder FilterBinder { get; } - // expression looks like -> new Wrapper { Instance = source , Properties = "...", Container = new PropertyContainer { ... } } - Expression projectionExpression = ProjectElement(context, source, selectExpandClause, structuredType, navigationSource); + /// + /// Gets the orderby binder. + /// + public IOrderByBinder OrderByBinder { get; } - // expression looks like -> source => new Wrapper { Instance = source .... } - LambdaExpression projectionLambdaExpression = Expression.Lambda(projectionExpression, source); + /// + /// Translate an OData $select or $expand tree represented by to an . + /// + /// The original . + /// An instance of the . + /// The $select and $expand binder result. + public virtual Expression BindSelectExpand(SelectExpandClause selectExpandClause, QueryBinderContext context) + { + if (selectExpandClause == null) + { + throw Error.ArgumentNull(nameof(selectExpandClause)); + } - return projectionLambdaExpression; + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); } - internal Expression ProjectAsWrapper(QueryBinderContext context, Expression source, SelectExpandClause selectExpandClause, - IEdmStructuredType structuredType, IEdmNavigationSource navigationSource, OrderByClause orderByClause = null, - ComputeClause computeClause = null, - long? topOption = null, - long? skipOption = null, - int? modelBoundPageSize = null) + IEdmStructuredType structuredType = context.ElementType as IEdmStructuredType; + IEdmNavigationSource navigationSource = context.NavigationSource; + ParameterExpression source = context.CurrentParameter; + + // expression looks like -> new Wrapper { Instance = source , Properties = "...", Container = new PropertyContainer { ... } } + Expression projectionExpression = ProjectElement(context, source, selectExpandClause, structuredType, navigationSource); + + // expression looks like -> source => new Wrapper { Instance = source .... } + LambdaExpression projectionLambdaExpression = Expression.Lambda(projectionExpression, source); + + return projectionLambdaExpression; + } + + internal Expression ProjectAsWrapper(QueryBinderContext context, Expression source, SelectExpandClause selectExpandClause, + IEdmStructuredType structuredType, IEdmNavigationSource navigationSource, OrderByClause orderByClause = null, + ComputeClause computeClause = null, + long? topOption = null, + long? skipOption = null, + int? modelBoundPageSize = null) + { + Type elementType; + bool isCollection = TypeHelper.IsCollection(source.Type, out elementType); + QueryBinderContext subContext = new QueryBinderContext(context, context.QuerySettings, elementType); + if (computeClause != null && IsAvailableODataQueryOption(context.QuerySettings, AllowedQueryOptions.Compute)) { - Type elementType; - bool isCollection = TypeHelper.IsCollection(source.Type, out elementType); - QueryBinderContext subContext = new QueryBinderContext(context, context.QuerySettings, elementType); - if (computeClause != null && IsAvailableODataQueryOption(context.QuerySettings, AllowedQueryOptions.Compute)) - { - subContext.AddComputedProperties(computeClause.ComputedItems); - } + subContext.AddComputedProperties(computeClause.ComputedItems); + } - if (orderByClause != null && context.EnableSkipToken) - { - subContext.OrderByClauses = orderByClause.ToList(); - } + if (orderByClause != null && context.EnableSkipToken) + { + subContext.OrderByClauses = orderByClause.ToList(); + } - if (isCollection) - { - // new CollectionWrapper { Instance = source.Select(s => new Wrapper { ... }) }; - return ProjectCollection(subContext, source, elementType, selectExpandClause, structuredType, navigationSource, orderByClause, - topOption, - skipOption, - modelBoundPageSize); - } - else - { - // new Wrapper { v1 = source.property ... } - return ProjectElement(subContext, source, selectExpandClause, /*computeClause,*/ structuredType, navigationSource); - } + if (isCollection) + { + // new CollectionWrapper { Instance = source.Select(s => new Wrapper { ... }) }; + return ProjectCollection(subContext, source, elementType, selectExpandClause, structuredType, navigationSource, orderByClause, + topOption, + skipOption, + modelBoundPageSize); + } + else + { + // new Wrapper { v1 = source.property ... } + return ProjectElement(subContext, source, selectExpandClause, /*computeClause,*/ structuredType, navigationSource); } + } - /// - /// Creates an from an name. - /// - /// The . - /// The that contains the edmProperty. - /// The from which we are creating an . - /// The source . - /// The created . - public virtual Expression CreatePropertyNameExpression(QueryBinderContext context, IEdmStructuredType elementType, IEdmProperty edmProperty, Expression source) + /// + /// Creates an from an name. + /// + /// The . + /// The that contains the edmProperty. + /// The from which we are creating an . + /// The source . + /// The created . + public virtual Expression CreatePropertyNameExpression(QueryBinderContext context, IEdmStructuredType elementType, IEdmProperty edmProperty, Expression source) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - if (elementType == null) - { - throw Error.ArgumentNull(nameof(elementType)); - } + if (elementType == null) + { + throw Error.ArgumentNull(nameof(elementType)); + } + + if (edmProperty == null) + { + throw Error.ArgumentNull(nameof(edmProperty)); + } + + if (source == null) + { + throw Error.ArgumentNull(nameof(source)); + } - if (edmProperty == null) + IEdmStructuredType declaringType = edmProperty.DeclaringType; + IEdmModel model = context.Model; + + // derived property using cast + if (elementType != declaringType) + { + Type originalType = model.GetClrType(elementType); + Type castType = model.GetClrType(declaringType); + if (castType == null) { - throw Error.ArgumentNull(nameof(edmProperty)); + throw new ODataException(Error.Format(SRResources.MappingDoesNotContainResourceType, declaringType.FullTypeName())); } - if (source == null) + if (!castType.IsAssignableFrom(originalType)) { - throw Error.ArgumentNull(nameof(source)); + // Expression + // source is navigationPropertyDeclaringType ? propertyName : null + return Expression.Condition( + test: Expression.TypeIs(source, castType), + ifTrue: Expression.Constant(edmProperty.Name), + ifFalse: Expression.Constant(null, typeof(string))); } + } - IEdmStructuredType declaringType = edmProperty.DeclaringType; - IEdmModel model = context.Model; + // Expression + // "propertyName" + return Expression.Constant(edmProperty.Name); + } - // derived property using cast - if (elementType != declaringType) - { - Type originalType = model.GetClrType(elementType); - Type castType = model.GetClrType(declaringType); - if (castType == null) - { - throw new ODataException(Error.Format(SRResources.MappingDoesNotContainResourceType, declaringType.FullTypeName())); - } + /// + /// Creates an from an name. + /// + /// The . + /// The that contains the edmProperty. + /// The from which we are creating an . + /// The source . + /// The $filter query represented by . + /// The $compute query represented by . + /// The created . + public virtual Expression CreatePropertyValueExpression(QueryBinderContext context, IEdmStructuredType elementType, IEdmProperty edmProperty, Expression source, FilterClause filterClause, ComputeClause computeClause = null) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - if (!castType.IsAssignableFrom(originalType)) - { - // Expression - // source is navigationPropertyDeclaringType ? propertyName : null - return Expression.Condition( - test: Expression.TypeIs(source, castType), - ifTrue: Expression.Constant(edmProperty.Name), - ifFalse: Expression.Constant(null, typeof(string))); - } - } + if (elementType == null) + { + throw Error.ArgumentNull(nameof(elementType)); + } - // Expression - // "propertyName" - return Expression.Constant(edmProperty.Name); + if (edmProperty == null) + { + throw Error.ArgumentNull(nameof(edmProperty)); } - /// - /// Creates an from an name. - /// - /// The . - /// The that contains the edmProperty. - /// The from which we are creating an . - /// The source . - /// The $filter query represented by . - /// The $compute query represented by . - /// The created . - public virtual Expression CreatePropertyValueExpression(QueryBinderContext context, IEdmStructuredType elementType, IEdmProperty edmProperty, Expression source, FilterClause filterClause, ComputeClause computeClause = null) + if (source == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(source)); + } - if (elementType == null) - { - throw Error.ArgumentNull(nameof(elementType)); - } + IEdmModel model = context.Model; + ODataQuerySettings settings = context.QuerySettings; - if (edmProperty == null) + // Expression: source = source as propertyDeclaringType + if (elementType != edmProperty.DeclaringType) + { + Type castType = model.GetClrType(edmProperty.DeclaringType); + if (castType == null) { - throw Error.ArgumentNull(nameof(edmProperty)); + throw new ODataException(Error.Format(SRResources.MappingDoesNotContainResourceType, edmProperty.DeclaringType.FullTypeName())); } - if (source == null) - { - throw Error.ArgumentNull(nameof(source)); - } + source = Expression.TypeAs(source, castType); + } - IEdmModel model = context.Model; - ODataQuerySettings settings = context.QuerySettings; + // Expression: source.Property + string propertyName = model.GetClrPropertyName(edmProperty); + + PropertyInfo propertyInfo = source.Type.GetProperty(propertyName, BindingFlags.DeclaredOnly); + if (propertyInfo == null) + { + /* + History of code: + propertyInfo = source.Type.GetProperty(propertyName); + This code fixes an issue where 'History of code' was unable to obtain attributes with the same name as the parent class after the subclass hid the parent class member. + Such as: + 'History of code' cannot get the Key property of the Child. + public class Father + { + public string Key { get; set; } + } + public class Child : Father + { + public new Guid Key { get; set; } + } + */ + propertyInfo = source.Type.GetProperties().Where(m => m.Name.Equals(propertyName, StringComparison.Ordinal)).FirstOrDefault(); + } + + Expression propertyValue = Expression.Property(source, propertyInfo); + Type nullablePropertyType = TypeHelper.ToNullable(propertyValue.Type); + Expression nullablePropertyValue = ExpressionHelpers.ToNullable(propertyValue); - // Expression: source = source as propertyDeclaringType - if (elementType != edmProperty.DeclaringType) - { - Type castType = model.GetClrType(edmProperty.DeclaringType); - if (castType == null) - { - throw new ODataException(Error.Format(SRResources.MappingDoesNotContainResourceType, edmProperty.DeclaringType.FullTypeName())); - } + if (filterClause != null && IsAvailableODataQueryOption(context.QuerySettings, AllowedQueryOptions.Filter)) + { + bool isCollection = edmProperty.Type.IsCollection(); - source = Expression.TypeAs(source, castType); + IEdmTypeReference edmElementType = (isCollection ? edmProperty.Type.AsCollection().ElementType() : edmProperty.Type); + Type clrElementType = model.GetClrType(edmElementType); + if (clrElementType == null) + { + throw new ODataException(Error.Format(SRResources.MappingDoesNotContainResourceType, edmElementType.FullName())); } - // Expression: source.Property - string propertyName = model.GetClrPropertyName(edmProperty); - - PropertyInfo propertyInfo = source.Type.GetProperty(propertyName, BindingFlags.DeclaredOnly); - if (propertyInfo == null) + Expression filterResult = nullablePropertyValue; + + ODataQuerySettings newSettings = new ODataQuerySettings(); + newSettings.CopyFrom(context.QuerySettings); + newSettings.HandleNullPropagation = HandleNullPropagationOption.True; + QueryBinderContext binderContext = new QueryBinderContext(context, newSettings, clrElementType); + if (computeClause != null) { - /* - History of code: - propertyInfo = source.Type.GetProperty(propertyName); - This code fixes an issue where 'History of code' was unable to obtain attributes with the same name as the parent class after the subclass hid the parent class member. - Such as: - 'History of code' cannot get the Key property of the Child. - public class Father - { - public string Key { get; set; } - } - public class Child : Father - { - public new Guid Key { get; set; } - } - */ - propertyInfo = source.Type.GetProperties().Where(m => m.Name.Equals(propertyName, StringComparison.Ordinal)).FirstOrDefault(); + binderContext.AddComputedProperties(computeClause.ComputedItems); } - - Expression propertyValue = Expression.Property(source, propertyInfo); - Type nullablePropertyType = TypeHelper.ToNullable(propertyValue.Type); - Expression nullablePropertyValue = ExpressionHelpers.ToNullable(propertyValue); - if (filterClause != null && IsAvailableODataQueryOption(context.QuerySettings, AllowedQueryOptions.Filter)) + if (isCollection) { - bool isCollection = edmProperty.Type.IsCollection(); - - IEdmTypeReference edmElementType = (isCollection ? edmProperty.Type.AsCollection().ElementType() : edmProperty.Type); - Type clrElementType = model.GetClrType(edmElementType); - if (clrElementType == null) - { - throw new ODataException(Error.Format(SRResources.MappingDoesNotContainResourceType, edmElementType.FullName())); - } + Expression filterSource = nullablePropertyValue; - Expression filterResult = nullablePropertyValue; - - ODataQuerySettings newSettings = new ODataQuerySettings(); - newSettings.CopyFrom(context.QuerySettings); - newSettings.HandleNullPropagation = HandleNullPropagationOption.True; - QueryBinderContext binderContext = new QueryBinderContext(context, newSettings, clrElementType); - if (computeClause != null) - { - binderContext.AddComputedProperties(computeClause.ComputedItems); - } + // TODO: Implement proper support for $select/$expand after $apply + // Expression filterPredicate = FilterBinder.Bind(null, filterClause, clrElementType, queryContext, querySettings); + filterResult = FilterBinder.ApplyBind(filterSource, filterClause, binderContext); - if (isCollection) + nullablePropertyType = filterResult.Type; + } + else if (settings.HandleReferenceNavigationPropertyExpandFilter) + { + LambdaExpression filterLambdaExpression = FilterBinder.BindFilter(filterClause, binderContext) as LambdaExpression; + if (filterLambdaExpression == null) { - Expression filterSource = nullablePropertyValue; - - // TODO: Implement proper support for $select/$expand after $apply - // Expression filterPredicate = FilterBinder.Bind(null, filterClause, clrElementType, queryContext, querySettings); - filterResult = FilterBinder.ApplyBind(filterSource, filterClause, binderContext); - - nullablePropertyType = filterResult.Type; + throw new ODataException(Error.Format(SRResources.ExpandFilterExpressionNotLambdaExpression, edmProperty.Name, "LambdaExpression")); } - else if (settings.HandleReferenceNavigationPropertyExpandFilter) - { - LambdaExpression filterLambdaExpression = FilterBinder.BindFilter(filterClause, binderContext) as LambdaExpression; - if (filterLambdaExpression == null) - { - throw new ODataException(Error.Format(SRResources.ExpandFilterExpressionNotLambdaExpression, edmProperty.Name, "LambdaExpression")); - } - - ParameterExpression filterParameter = filterLambdaExpression.Parameters.First(); - Expression predicateExpression = new ReferenceNavigationPropertyExpandFilterVisitor(filterParameter, nullablePropertyValue).Visit(filterLambdaExpression.Body); - // create expression similar to: 'predicateExpression == true ? nullablePropertyValue : null' - filterResult = Expression.Condition( - test: predicateExpression, - ifTrue: nullablePropertyValue, - ifFalse: Expression.Constant(value: null, type: nullablePropertyType)); - } + ParameterExpression filterParameter = filterLambdaExpression.Parameters.First(); + Expression predicateExpression = new ReferenceNavigationPropertyExpandFilterVisitor(filterParameter, nullablePropertyValue).Visit(filterLambdaExpression.Body); - if (settings.HandleNullPropagation == HandleNullPropagationOption.True) - { - // create expression similar to: 'nullablePropertyValue == null ? null : filterResult' - nullablePropertyValue = Expression.Condition( - test: Expression.Equal(nullablePropertyValue, Expression.Constant(value: null)), - ifTrue: Expression.Constant(value: null, type: nullablePropertyType), - ifFalse: filterResult); - } - else - { - nullablePropertyValue = filterResult; - } + // create expression similar to: 'predicateExpression == true ? nullablePropertyValue : null' + filterResult = Expression.Condition( + test: predicateExpression, + ifTrue: nullablePropertyValue, + ifFalse: Expression.Constant(value: null, type: nullablePropertyType)); } if (settings.HandleNullPropagation == HandleNullPropagationOption.True) { - // create expression similar to: 'source == null ? null : propertyValue' - propertyValue = Expression.Condition( - test: Expression.Equal(source, Expression.Constant(value: null)), + // create expression similar to: 'nullablePropertyValue == null ? null : filterResult' + nullablePropertyValue = Expression.Condition( + test: Expression.Equal(nullablePropertyValue, Expression.Constant(value: null)), ifTrue: Expression.Constant(value: null, type: nullablePropertyType), - ifFalse: nullablePropertyValue); + ifFalse: filterResult); } else { - // need to cast this to nullable as EF would fail while materializing if the property is not nullable and source is null. - propertyValue = nullablePropertyValue; + nullablePropertyValue = filterResult; } - - return propertyValue; } - // Generates the expression - // source => new Wrapper { Instance = source, Container = new PropertyContainer { ..expanded properties.. } } - internal Expression ProjectElement(QueryBinderContext context, Expression source, SelectExpandClause selectExpandClause, IEdmStructuredType structuredType, IEdmNavigationSource navigationSource) + if (settings.HandleNullPropagation == HandleNullPropagationOption.True) + { + // create expression similar to: 'source == null ? null : propertyValue' + propertyValue = Expression.Condition( + test: Expression.Equal(source, Expression.Constant(value: null)), + ifTrue: Expression.Constant(value: null, type: nullablePropertyType), + ifFalse: nullablePropertyValue); + } + else { - Contract.Assert(source != null); + // need to cast this to nullable as EF would fail while materializing if the property is not nullable and source is null. + propertyValue = nullablePropertyValue; + } - IEdmModel model = context.Model; + return propertyValue; + } - // If it's not a structural type, just return the source. - if (structuredType == null) - { - return source; - } + // Generates the expression + // source => new Wrapper { Instance = source, Container = new PropertyContainer { ..expanded properties.. } } + internal Expression ProjectElement(QueryBinderContext context, Expression source, SelectExpandClause selectExpandClause, IEdmStructuredType structuredType, IEdmNavigationSource navigationSource) + { + Contract.Assert(source != null); - Type elementType = source.Type; - Type wrapperType = typeof(SelectExpandWrapper<>).MakeGenericType(elementType); - List wrapperTypeMemberAssignments = new List(); - - PropertyInfo wrapperProperty; - Expression wrapperPropertyValueExpression; - bool isInstancePropertySet = false; - bool isTypeNamePropertySet = false; - bool isContainerPropertySet = false; - - // Initialize property 'Model' on the wrapper class. - // source = new Wrapper { Model = parameterized(a-edm-model) } - // Always parameterize as EntityFramework does not let you inject non primitive constant values (like IEdmModel). - wrapperProperty = wrapperType.GetProperty("Model"); - wrapperPropertyValueExpression = LinqParameterContainer.Parameterize(typeof(IEdmModel), model); - wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, wrapperPropertyValueExpression)); - - bool isSelectedAll = IsSelectAll(selectExpandClause); - if (isSelectedAll) - { - // Initialize property 'Instance' on the wrapper class - wrapperProperty = wrapperType.GetProperty("Instance"); - wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, source)); + IEdmModel model = context.Model; - wrapperProperty = wrapperType.GetProperty("UseInstanceForProperties"); - wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, Expression.Constant(true))); - isInstancePropertySet = true; - } - else + // If it's not a structural type, just return the source. + if (structuredType == null) + { + return source; + } + + Type elementType = source.Type; + Type wrapperType = typeof(SelectExpandWrapper<>).MakeGenericType(elementType); + List wrapperTypeMemberAssignments = new List(); + + PropertyInfo wrapperProperty; + Expression wrapperPropertyValueExpression; + bool isInstancePropertySet = false; + bool isTypeNamePropertySet = false; + bool isContainerPropertySet = false; + + // Initialize property 'Model' on the wrapper class. + // source = new Wrapper { Model = parameterized(a-edm-model) } + // Always parameterize as EntityFramework does not let you inject non primitive constant values (like IEdmModel). + wrapperProperty = wrapperType.GetProperty("Model"); + wrapperPropertyValueExpression = LinqParameterContainer.Parameterize(typeof(IEdmModel), model); + wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, wrapperPropertyValueExpression)); + + bool isSelectedAll = IsSelectAll(selectExpandClause); + if (isSelectedAll) + { + // Initialize property 'Instance' on the wrapper class + wrapperProperty = wrapperType.GetProperty("Instance"); + wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, source)); + + wrapperProperty = wrapperType.GetProperty("UseInstanceForProperties"); + wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, Expression.Constant(true))); + isInstancePropertySet = true; + } + else + { + // Initialize property 'TypeName' on the wrapper class as we don't have the instance. + Expression typeName = CreateTypeNameExpression(source, structuredType, model); + if (typeName != null) { - // Initialize property 'TypeName' on the wrapper class as we don't have the instance. - Expression typeName = CreateTypeNameExpression(source, structuredType, model); - if (typeName != null) - { - isTypeNamePropertySet = true; - wrapperProperty = wrapperType.GetProperty("InstanceType"); - wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, typeName)); - } + isTypeNamePropertySet = true; + wrapperProperty = wrapperType.GetProperty("InstanceType"); + wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, typeName)); } + } - // Initialize the property 'Container' on the wrapper class - // source => new Wrapper { Container = new PropertyContainer { .... } } - if (selectExpandClause != null) - { - IDictionary propertiesToInclude; - IDictionary propertiesToExpand; - ISet autoSelectedProperties; - IList computedProperties; + // Initialize the property 'Container' on the wrapper class + // source => new Wrapper { Container = new PropertyContainer { .... } } + if (selectExpandClause != null) + { + IDictionary propertiesToInclude; + IDictionary propertiesToExpand; + ISet autoSelectedProperties; + IList computedProperties; - IList dynamicPathSegments = GetSelectExpandProperties(model, structuredType, navigationSource, selectExpandClause, - out propertiesToInclude, - out propertiesToExpand, - out autoSelectedProperties); + IList dynamicPathSegments = GetSelectExpandProperties(model, structuredType, navigationSource, selectExpandClause, + out propertiesToInclude, + out propertiesToExpand, + out autoSelectedProperties); - bool isContainDynamicPropertySelection = ParseComputedDynamicProperties(context, dynamicPathSegments, isSelectedAll, out computedProperties); + bool isContainDynamicPropertySelection = ParseComputedDynamicProperties(context, dynamicPathSegments, isSelectedAll, out computedProperties); - bool isSelectingOpenTypeSegments = isContainDynamicPropertySelection || IsSelectAllOnOpenType(selectExpandClause, structuredType); + bool isSelectingOpenTypeSegments = isContainDynamicPropertySelection || IsSelectAllOnOpenType(selectExpandClause, structuredType); - if (propertiesToExpand != null || propertiesToInclude != null || computedProperties != null || autoSelectedProperties != null || isSelectingOpenTypeSegments || context.OrderByClauses != null) - { - Expression propertyContainerCreation = - BuildPropertyContainer(context, source, structuredType, propertiesToExpand, propertiesToInclude, computedProperties, autoSelectedProperties, isSelectingOpenTypeSegments, isSelectedAll); + if (propertiesToExpand != null || propertiesToInclude != null || computedProperties != null || autoSelectedProperties != null || isSelectingOpenTypeSegments || context.OrderByClauses != null) + { + Expression propertyContainerCreation = + BuildPropertyContainer(context, source, structuredType, propertiesToExpand, propertiesToInclude, computedProperties, autoSelectedProperties, isSelectingOpenTypeSegments, isSelectedAll); - if (propertyContainerCreation != null) - { - wrapperProperty = wrapperType.GetProperty("Container"); - Contract.Assert(wrapperProperty != null); + if (propertyContainerCreation != null) + { + wrapperProperty = wrapperType.GetProperty("Container"); + Contract.Assert(wrapperProperty != null); - wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, propertyContainerCreation)); - isContainerPropertySet = true; - } + wrapperTypeMemberAssignments.Add(Expression.Bind(wrapperProperty, propertyContainerCreation)); + isContainerPropertySet = true; } } + } + + Type wrapperGenericType = GetWrapperGenericType(isInstancePropertySet, isTypeNamePropertySet, isContainerPropertySet); + wrapperType = wrapperGenericType.MakeGenericType(elementType); + return Expression.MemberInit(Expression.New(wrapperType), wrapperTypeMemberAssignments); + } + + private static bool ParseComputedDynamicProperties(QueryBinderContext context, IList dynamicPathSegments, bool isSelectedAll, + out IList computedProperties) + { + computedProperties = null; - Type wrapperGenericType = GetWrapperGenericType(isInstancePropertySet, isTypeNamePropertySet, isContainerPropertySet); - wrapperType = wrapperGenericType.MakeGenericType(elementType); - return Expression.MemberInit(Expression.New(wrapperType), wrapperTypeMemberAssignments); + // If $select=*, then we should include all computed properties. + if (isSelectedAll) + { + computedProperties = context.ComputedProperties.Select(c => c.Key).ToList(); + return true; // select all means to include all dynamic properties. } - private static bool ParseComputedDynamicProperties(QueryBinderContext context, IList dynamicPathSegments, bool isSelectedAll, - out IList computedProperties) + if (context.ComputedProperties == null || context.ComputedProperties.Count == 0) { - computedProperties = null; + return dynamicPathSegments.Count > 0; + } - // If $select=*, then we should include all computed properties. - if (isSelectedAll) + bool hasDynamic = false; + foreach (var segment in dynamicPathSegments) + { + if (context.ComputedProperties.ContainsKey(segment.Identifier)) { - computedProperties = context.ComputedProperties.Select(c => c.Key).ToList(); - return true; // select all means to include all dynamic properties. - } + if (computedProperties == null) + { + computedProperties = new List(); + } - if (context.ComputedProperties == null || context.ComputedProperties.Count == 0) + computedProperties.Add(segment.Identifier); + } + else { - return dynamicPathSegments.Count > 0; + hasDynamic = true; } + } - bool hasDynamic = false; - foreach (var segment in dynamicPathSegments) - { - if (context.ComputedProperties.ContainsKey(segment.Identifier)) - { - if (computedProperties == null) - { - computedProperties = new List(); - } + return hasDynamic; + } - computedProperties.Add(segment.Identifier); - } - else - { - hasDynamic = true; - } + /// + /// Gets the $select and $expand properties from the given + /// + /// The Edm model. + /// The current structural type. + /// The current navigation source. + /// The given select and expand clause. + /// The out properties to include at current level, could be null. + /// The out properties to expand at current level, could be null. + /// The out auto selected properties to include at current level, could be null. + /// true if the select contains dynamic property selection, false if it's not. + internal static IList GetSelectExpandProperties(IEdmModel model, IEdmStructuredType structuredType, IEdmNavigationSource navigationSource, + SelectExpandClause selectExpandClause, + out IDictionary propertiesToInclude, + out IDictionary propertiesToExpand, + out ISet autoSelectedProperties) + { + Contract.Assert(selectExpandClause != null); + + // Properties to be included includes all the properties selected or in the middle of a $select and $expand path. + // for example: "$expand=abc/xyz/nav", "abc" and "xyz" are the middle properties that should be included. + // meanwhile, "nav" is the property that should be expanded. + // If it's a type cast path, for example: $select=NS.TypeCast/abc, "abc" should be included also. + propertiesToInclude = null; + propertiesToExpand = null; + autoSelectedProperties = null; + + IList dynamicsSegments = new List(); + + var currentLevelPropertiesInclude = new Dictionary(); + foreach (SelectItem selectItem in selectExpandClause.SelectedItems) + { + // $expand=... + ExpandedReferenceSelectItem expandedItem = selectItem as ExpandedReferenceSelectItem; + if (expandedItem != null) + { + ProcessExpandedItem(expandedItem, navigationSource, currentLevelPropertiesInclude, ref propertiesToExpand); + continue; } - return hasDynamic; - } - - /// - /// Gets the $select and $expand properties from the given - /// - /// The Edm model. - /// The current structural type. - /// The current navigation source. - /// The given select and expand clause. - /// The out properties to include at current level, could be null. - /// The out properties to expand at current level, could be null. - /// The out auto selected properties to include at current level, could be null. - /// true if the select contains dynamic property selection, false if it's not. - internal static IList GetSelectExpandProperties(IEdmModel model, IEdmStructuredType structuredType, IEdmNavigationSource navigationSource, - SelectExpandClause selectExpandClause, - out IDictionary propertiesToInclude, - out IDictionary propertiesToExpand, - out ISet autoSelectedProperties) - { - Contract.Assert(selectExpandClause != null); - - // Properties to be included includes all the properties selected or in the middle of a $select and $expand path. - // for example: "$expand=abc/xyz/nav", "abc" and "xyz" are the middle properties that should be included. - // meanwhile, "nav" is the property that should be expanded. - // If it's a type cast path, for example: $select=NS.TypeCast/abc, "abc" should be included also. - propertiesToInclude = null; - propertiesToExpand = null; - autoSelectedProperties = null; - - IList dynamicsSegments = new List(); - - var currentLevelPropertiesInclude = new Dictionary(); - foreach (SelectItem selectItem in selectExpandClause.SelectedItems) + // $select=... + PathSelectItem pathItem = selectItem as PathSelectItem; + if (pathItem != null) { - // $expand=... - ExpandedReferenceSelectItem expandedItem = selectItem as ExpandedReferenceSelectItem; - if (expandedItem != null) + DynamicPathSegment dynamicSegment = ProcessSelectedItem(pathItem, navigationSource, currentLevelPropertiesInclude); + if (dynamicSegment != null) { - ProcessExpandedItem(expandedItem, navigationSource, currentLevelPropertiesInclude, ref propertiesToExpand); - continue; + dynamicsSegments.Add(dynamicSegment); } + continue; + } + + // Skip processing the "WildcardSelectItem and NamespaceQualifiedWildcardSelectItem" + // ODL now doesn't support "$select=property/*" and "$select=property/NS.*" + } - // $select=... - PathSelectItem pathItem = selectItem as PathSelectItem; - if (pathItem != null) + if (!IsSelectAll(selectExpandClause)) + { + // We should include the keys if it's an entity. + IEdmEntityType entityType = structuredType as IEdmEntityType; + if (entityType != null) + { + foreach (IEdmStructuralProperty keyProperty in entityType.Key()) { - DynamicPathSegment dynamicSegment = ProcessSelectedItem(pathItem, navigationSource, currentLevelPropertiesInclude); - if (dynamicSegment != null) + if (!currentLevelPropertiesInclude.Keys.Contains(keyProperty)) { - dynamicsSegments.Add(dynamicSegment); + if (autoSelectedProperties == null) + { + autoSelectedProperties = new HashSet(); + } + + autoSelectedProperties.Add(keyProperty); } - continue; } - - // Skip processing the "WildcardSelectItem and NamespaceQualifiedWildcardSelectItem" - // ODL now doesn't support "$select=property/*" and "$select=property/NS.*" } - if (!IsSelectAll(selectExpandClause)) + // We should add concurrency properties, if not added + if (navigationSource != null && model != null) { - // We should include the keys if it's an entity. - IEdmEntityType entityType = structuredType as IEdmEntityType; - if (entityType != null) + IEnumerable concurrencyProperties = model.GetConcurrencyProperties(navigationSource); + foreach (IEdmStructuralProperty concurrencyProperty in concurrencyProperties) { - foreach (IEdmStructuralProperty keyProperty in entityType.Key()) + if (structuredType.Properties().Any(p => p == concurrencyProperty)) { - if (!currentLevelPropertiesInclude.Keys.Contains(keyProperty)) + if (!currentLevelPropertiesInclude.Keys.Contains(concurrencyProperty)) { if (autoSelectedProperties == null) { autoSelectedProperties = new HashSet(); } - autoSelectedProperties.Add(keyProperty); - } - } - } - - // We should add concurrency properties, if not added - if (navigationSource != null && model != null) - { - IEnumerable concurrencyProperties = model.GetConcurrencyProperties(navigationSource); - foreach (IEdmStructuralProperty concurrencyProperty in concurrencyProperties) - { - if (structuredType.Properties().Any(p => p == concurrencyProperty)) - { - if (!currentLevelPropertiesInclude.Keys.Contains(concurrencyProperty)) - { - if (autoSelectedProperties == null) - { - autoSelectedProperties = new HashSet(); - } - - autoSelectedProperties.Add(concurrencyProperty); - } + autoSelectedProperties.Add(concurrencyProperty); } } } } + } - if (currentLevelPropertiesInclude.Any()) + if (currentLevelPropertiesInclude.Any()) + { + propertiesToInclude = new Dictionary(); + foreach (var propertiesInclude in currentLevelPropertiesInclude) { - propertiesToInclude = new Dictionary(); - foreach (var propertiesInclude in currentLevelPropertiesInclude) - { - propertiesToInclude[propertiesInclude.Key] = propertiesInclude.Value == null ? null : propertiesInclude.Value.ToPathSelectItem(); - } + propertiesToInclude[propertiesInclude.Key] = propertiesInclude.Value == null ? null : propertiesInclude.Value.ToPathSelectItem(); } - - return dynamicsSegments; } - /// - /// Process the . - /// - /// The expanded item. - /// The navigation source. - /// The current level properties included. - /// out/ref, the property expanded. - private static void ProcessExpandedItem(ExpandedReferenceSelectItem expandedItem, - IEdmNavigationSource navigationSource, - IDictionary currentLevelPropertiesInclude, - ref IDictionary propertiesToExpand) - { - Contract.Assert(expandedItem != null && expandedItem.PathToNavigationProperty != null); - Contract.Assert(currentLevelPropertiesInclude != null); + return dynamicsSegments; + } - // Verify and process the $expand=... path. - IList remainingSegments; - ODataPathSegment firstNonTypeSegment = expandedItem.PathToNavigationProperty.GetFirstNonTypeCastSegment(out remainingSegments); + /// + /// Process the . + /// + /// The expanded item. + /// The navigation source. + /// The current level properties included. + /// out/ref, the property expanded. + private static void ProcessExpandedItem(ExpandedReferenceSelectItem expandedItem, + IEdmNavigationSource navigationSource, + IDictionary currentLevelPropertiesInclude, + ref IDictionary propertiesToExpand) + { + Contract.Assert(expandedItem != null && expandedItem.PathToNavigationProperty != null); + Contract.Assert(currentLevelPropertiesInclude != null); - // for $expand=NS.SubType/Nav, we don't care about the leading type segment, because with or without the type segment - // the "nav" property value expression should be built into the property container. + // Verify and process the $expand=... path. + IList remainingSegments; + ODataPathSegment firstNonTypeSegment = expandedItem.PathToNavigationProperty.GetFirstNonTypeCastSegment(out remainingSegments); - PropertySegment firstStructuralPropertySegment = firstNonTypeSegment as PropertySegment; - if (firstStructuralPropertySegment != null) - { - // for example: $expand=abc/nav, the remaining segments should never be null because at least the last navigation segment is there. - Contract.Assert(remainingSegments != null); + // for $expand=NS.SubType/Nav, we don't care about the leading type segment, because with or without the type segment + // the "nav" property value expression should be built into the property container. - SelectExpandIncludedProperty newPropertySelectItem; - if (!currentLevelPropertiesInclude.TryGetValue(firstStructuralPropertySegment.Property, out newPropertySelectItem)) - { - newPropertySelectItem = new SelectExpandIncludedProperty(firstStructuralPropertySegment, navigationSource); - currentLevelPropertiesInclude[firstStructuralPropertySegment.Property] = newPropertySelectItem; - } + PropertySegment firstStructuralPropertySegment = firstNonTypeSegment as PropertySegment; + if (firstStructuralPropertySegment != null) + { + // for example: $expand=abc/nav, the remaining segments should never be null because at least the last navigation segment is there. + Contract.Assert(remainingSegments != null); - newPropertySelectItem.AddSubExpandItem(remainingSegments, expandedItem); - } - else + SelectExpandIncludedProperty newPropertySelectItem; + if (!currentLevelPropertiesInclude.TryGetValue(firstStructuralPropertySegment.Property, out newPropertySelectItem)) { - // for example: $expand=nav, if we couldn't find a structural property in the path, it means we get the last navigation segment. - // So, the remaining segments should be null and the last segment should be "NavigationPropertySegment". - Contract.Assert(remainingSegments == null); + newPropertySelectItem = new SelectExpandIncludedProperty(firstStructuralPropertySegment, navigationSource); + currentLevelPropertiesInclude[firstStructuralPropertySegment.Property] = newPropertySelectItem; + } - NavigationPropertySegment firstNavigationPropertySegment = firstNonTypeSegment as NavigationPropertySegment; - Contract.Assert(firstNavigationPropertySegment != null); + newPropertySelectItem.AddSubExpandItem(remainingSegments, expandedItem); + } + else + { + // for example: $expand=nav, if we couldn't find a structural property in the path, it means we get the last navigation segment. + // So, the remaining segments should be null and the last segment should be "NavigationPropertySegment". + Contract.Assert(remainingSegments == null); - // Needn't add this navigation property into the include property. - // Because this navigation property will be included separately. - if (propertiesToExpand == null) - { - propertiesToExpand = new Dictionary(); - } + NavigationPropertySegment firstNavigationPropertySegment = firstNonTypeSegment as NavigationPropertySegment; + Contract.Assert(firstNavigationPropertySegment != null); - propertiesToExpand[firstNavigationPropertySegment.NavigationProperty] = expandedItem; + // Needn't add this navigation property into the include property. + // Because this navigation property will be included separately. + if (propertiesToExpand == null) + { + propertiesToExpand = new Dictionary(); } - } - /// - /// Process the . - /// - /// The selected item. - /// The navigation source. - /// The current level properties included. - /// true if it's dynamic property selection, false if it's not. - private static DynamicPathSegment ProcessSelectedItem(PathSelectItem pathSelectItem, - IEdmNavigationSource navigationSource, - IDictionary currentLevelPropertiesInclude) - { - Contract.Assert(pathSelectItem != null && pathSelectItem.SelectedPath != null); - Contract.Assert(currentLevelPropertiesInclude != null); + propertiesToExpand[firstNavigationPropertySegment.NavigationProperty] = expandedItem; + } + } - // Verify and process the $select path - IList remainingSegments; - ODataPathSegment firstNonTypeSegment = pathSelectItem.SelectedPath.GetFirstNonTypeCastSegment(out remainingSegments); + /// + /// Process the . + /// + /// The selected item. + /// The navigation source. + /// The current level properties included. + /// true if it's dynamic property selection, false if it's not. + private static DynamicPathSegment ProcessSelectedItem(PathSelectItem pathSelectItem, + IEdmNavigationSource navigationSource, + IDictionary currentLevelPropertiesInclude) + { + Contract.Assert(pathSelectItem != null && pathSelectItem.SelectedPath != null); + Contract.Assert(currentLevelPropertiesInclude != null); - // for $select=NS.SubType/Property, we don't care about the leading type segment, because with or without the type segment - // the "Property" property value expression should be built into the property container. + // Verify and process the $select path + IList remainingSegments; + ODataPathSegment firstNonTypeSegment = pathSelectItem.SelectedPath.GetFirstNonTypeCastSegment(out remainingSegments); - PropertySegment firstStructuralPropertySegment = firstNonTypeSegment as PropertySegment; - if (firstStructuralPropertySegment != null) - { - // $select=abc/..../xyz - SelectExpandIncludedProperty newPropertySelectItem; - if (!currentLevelPropertiesInclude.TryGetValue(firstStructuralPropertySegment.Property, out newPropertySelectItem)) - { - newPropertySelectItem = new SelectExpandIncludedProperty(firstStructuralPropertySegment, navigationSource); - currentLevelPropertiesInclude[firstStructuralPropertySegment.Property] = newPropertySelectItem; - } + // for $select=NS.SubType/Property, we don't care about the leading type segment, because with or without the type segment + // the "Property" property value expression should be built into the property container. - newPropertySelectItem.AddSubSelectItem(remainingSegments, pathSelectItem); - } - else + PropertySegment firstStructuralPropertySegment = firstNonTypeSegment as PropertySegment; + if (firstStructuralPropertySegment != null) + { + // $select=abc/..../xyz + SelectExpandIncludedProperty newPropertySelectItem; + if (!currentLevelPropertiesInclude.TryGetValue(firstStructuralPropertySegment.Property, out newPropertySelectItem)) { - // If we can't find a PropertySegment, the $select path maybe selecting an operation, a navigation or dynamic property. - // And the remaining segments should be null. - Contract.Assert(remainingSegments == null); - - // For operation (action/function), needn't process it. - // For navigation property, needn't process it here. - - // For dynamic property, let's test the last segment for this path select item. - if (firstNonTypeSegment is DynamicPathSegment) - { - // for dynamic segment, there's no leading segment. - return (DynamicPathSegment)firstNonTypeSegment; - } + newPropertySelectItem = new SelectExpandIncludedProperty(firstStructuralPropertySegment, navigationSource); + currentLevelPropertiesInclude[firstStructuralPropertySegment.Property] = newPropertySelectItem; } - return null; + newPropertySelectItem.AddSubSelectItem(remainingSegments, pathSelectItem); } - - // To test whether the current selection is SelectAll on an open type - private static bool IsSelectAllOnOpenType(SelectExpandClause selectExpandClause, IEdmStructuredType structuredType) + else { - if (structuredType == null || !structuredType.IsOpen) + // If we can't find a PropertySegment, the $select path maybe selecting an operation, a navigation or dynamic property. + // And the remaining segments should be null. + Contract.Assert(remainingSegments == null); + + // For operation (action/function), needn't process it. + // For navigation property, needn't process it here. + + // For dynamic property, let's test the last segment for this path select item. + if (firstNonTypeSegment is DynamicPathSegment) { - return false; + // for dynamic segment, there's no leading segment. + return (DynamicPathSegment)firstNonTypeSegment; } + } - if (IsSelectAll(selectExpandClause)) - { - return true; - } + return null; + } + // To test whether the current selection is SelectAll on an open type + private static bool IsSelectAllOnOpenType(SelectExpandClause selectExpandClause, IEdmStructuredType structuredType) + { + if (structuredType == null || !structuredType.IsOpen) + { return false; } - /// - /// Create an for the $count in $select or $expand - /// - /// The - /// Original which we will be appending the count expression. - /// Boolean to indicate if count value is present in $expand or $select item. - /// The to create. - public virtual Expression CreateTotalCountExpression(QueryBinderContext context, Expression source, bool? countOption) + if (IsSelectAll(selectExpandClause)) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + return true; + } - if (source == null) - { - throw Error.ArgumentNull(nameof(source)); - } + return false; + } - Expression countExpression = Expression.Constant(null, typeof(long?)); - if (countOption == null || !countOption.Value) - { - return countExpression; - } + /// + /// Create an for the $count in $select or $expand + /// + /// The + /// Original which we will be appending the count expression. + /// Boolean to indicate if count value is present in $expand or $select item. + /// The to create. + public virtual Expression CreateTotalCountExpression(QueryBinderContext context, Expression source, bool? countOption) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - Type elementType; - if (!TypeHelper.IsCollection(source.Type, out elementType)) - { - return countExpression; - } + if (source == null) + { + throw Error.ArgumentNull(nameof(source)); + } - MethodInfo countMethod; - if (typeof(IQueryable).IsAssignableFrom(source.Type)) - { - countMethod = ExpressionHelperMethods.QueryableCountGeneric.MakeGenericMethod(elementType); - } - else - { - countMethod = ExpressionHelperMethods.EnumerableCountGeneric.MakeGenericMethod(elementType); - } + Expression countExpression = Expression.Constant(null, typeof(long?)); + if (countOption == null || !countOption.Value) + { + return countExpression; + } - // call Count() method. - countExpression = Expression.Call(null, countMethod, new[] { source }); + Type elementType; + if (!TypeHelper.IsCollection(source.Type, out elementType)) + { + return countExpression; + } - if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) - { - // source == null ? null : countExpression - return Expression.Condition( - test: Expression.Equal(source, Expression.Constant(null)), - ifTrue: Expression.Constant(null, typeof(long?)), - ifFalse: ExpressionHelpers.ToNullable(countExpression)); - } - else - { - return countExpression; - } + MethodInfo countMethod; + if (typeof(IQueryable).IsAssignableFrom(source.Type)) + { + countMethod = ExpressionHelperMethods.QueryableCountGeneric.MakeGenericMethod(elementType); + } + else + { + countMethod = ExpressionHelperMethods.EnumerableCountGeneric.MakeGenericMethod(elementType); } - private Expression BuildPropertyContainer(QueryBinderContext context, Expression source, - IEdmStructuredType structuredType, - IDictionary propertiesToExpand, - IDictionary propertiesToInclude, - IList computedProperties, - ISet autoSelectedProperties, - bool isSelectingOpenTypeSegments, - bool isSelectedAll) + // call Count() method. + countExpression = Expression.Call(null, countMethod, new[] { source }); + + if (context.QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) + { + // source == null ? null : countExpression + return Expression.Condition( + test: Expression.Equal(source, Expression.Constant(null)), + ifTrue: Expression.Constant(null, typeof(long?)), + ifFalse: ExpressionHelpers.ToNullable(countExpression)); + } + else { - IList includedProperties = new List(); + return countExpression; + } + } - if (propertiesToExpand != null) + private Expression BuildPropertyContainer(QueryBinderContext context, Expression source, + IEdmStructuredType structuredType, + IDictionary propertiesToExpand, + IDictionary propertiesToInclude, + IList computedProperties, + ISet autoSelectedProperties, + bool isSelectingOpenTypeSegments, + bool isSelectedAll) + { + IList includedProperties = new List(); + + if (propertiesToExpand != null) + { + foreach (var propertyToExpand in propertiesToExpand) { - foreach (var propertyToExpand in propertiesToExpand) - { - // $expand=abc or $expand=abc/$ref - BuildExpandedProperty(context, source, structuredType, propertyToExpand.Key, propertyToExpand.Value, includedProperties); - } + // $expand=abc or $expand=abc/$ref + BuildExpandedProperty(context, source, structuredType, propertyToExpand.Key, propertyToExpand.Value, includedProperties); } + } - if (propertiesToInclude != null) + if (propertiesToInclude != null) + { + foreach (var propertyToInclude in propertiesToInclude) { - foreach (var propertyToInclude in propertiesToInclude) - { - // $select=abc($select=...,$filter=...,$compute=...).... - BuildSelectedProperty(context, source, structuredType, propertyToInclude.Key, propertyToInclude.Value, includedProperties); - } + // $select=abc($select=...,$filter=...,$compute=...).... + BuildSelectedProperty(context, source, structuredType, propertyToInclude.Key, propertyToInclude.Value, includedProperties); } + } - if (computedProperties != null) + if (computedProperties != null) + { + foreach (var computedProperty in computedProperties) { - foreach (var computedProperty in computedProperties) - { - // $select=computed&$compute=.... as computed - BindComputedProperty(source, context, computedProperty, includedProperties); - } + // $select=computed&$compute=.... as computed + BindComputedProperty(source, context, computedProperty, includedProperties); } + } - if (autoSelectedProperties != null) + if (autoSelectedProperties != null) + { + foreach (IEdmStructuralProperty propertyToInclude in autoSelectedProperties) { - foreach (IEdmStructuralProperty propertyToInclude in autoSelectedProperties) + Expression propertyName = CreatePropertyNameExpression(context, structuredType, propertyToInclude, source); + Expression propertyValue = CreatePropertyValueExpression(context, structuredType, propertyToInclude, source, filterClause: null, computeClause: null); + includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue) { - Expression propertyName = CreatePropertyNameExpression(context, structuredType, propertyToInclude, source); - Expression propertyValue = CreatePropertyValueExpression(context, structuredType, propertyToInclude, source, filterClause: null, computeClause: null); - includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue) - { - AutoSelected = true - }); - } + AutoSelected = true + }); } + } - if (isSelectingOpenTypeSegments) - { - BuildDynamicProperty(context, source, structuredType, includedProperties); - } + if (isSelectingOpenTypeSegments) + { + BuildDynamicProperty(context, source, structuredType, includedProperties); + } - BindOrderByProperties(context, source, structuredType, includedProperties, isSelectedAll); + BindOrderByProperties(context, source, structuredType, includedProperties, isSelectedAll); - // create a property container that holds all these property names and values. - return PropertyContainer.CreatePropertyContainer(includedProperties); - } + // create a property container that holds all these property names and values. + return PropertyContainer.CreatePropertyContainer(includedProperties); + } - /// - /// Build the navigation property into the included properties. - /// The property name is the navigation property name. - /// The property value is the navigation property value from the source and applied the nested query options. - /// - /// Wrapper for properties used by the . - /// The source contains the navigation property. - /// The structured type or its derived type contains the navigation property. - /// The expanded navigation property. - /// The expanded navigation select item. It may contain the nested query options. - /// The container to hold the created property. - internal void BuildExpandedProperty(QueryBinderContext context, Expression source, IEdmStructuredType structuredType, - IEdmNavigationProperty navigationProperty, ExpandedReferenceSelectItem expandedItem, - IList includedProperties) - { - Contract.Assert(source != null); - Contract.Assert(context != null); - Contract.Assert(structuredType != null); - Contract.Assert(navigationProperty != null); - Contract.Assert(expandedItem != null); - Contract.Assert(includedProperties != null); + /// + /// Build the navigation property into the included properties. + /// The property name is the navigation property name. + /// The property value is the navigation property value from the source and applied the nested query options. + /// + /// Wrapper for properties used by the . + /// The source contains the navigation property. + /// The structured type or its derived type contains the navigation property. + /// The expanded navigation property. + /// The expanded navigation select item. It may contain the nested query options. + /// The container to hold the created property. + internal void BuildExpandedProperty(QueryBinderContext context, Expression source, IEdmStructuredType structuredType, + IEdmNavigationProperty navigationProperty, ExpandedReferenceSelectItem expandedItem, + IList includedProperties) + { + Contract.Assert(source != null); + Contract.Assert(context != null); + Contract.Assert(structuredType != null); + Contract.Assert(navigationProperty != null); + Contract.Assert(expandedItem != null); + Contract.Assert(includedProperties != null); - IEdmEntityType edmEntityType = navigationProperty.ToEntityType(); - IEdmModel model = context.Model; - ODataQuerySettings settings = context.QuerySettings; + IEdmEntityType edmEntityType = navigationProperty.ToEntityType(); + IEdmModel model = context.Model; + ODataQuerySettings settings = context.QuerySettings; - ModelBoundQuerySettings querySettings = model.GetModelBoundQuerySettings(navigationProperty, edmEntityType); + ModelBoundQuerySettings querySettings = model.GetModelBoundQuerySettings(navigationProperty, edmEntityType); - // TODO: Process $apply and $compute in the $expand here, will support later. - // $apply=...; $compute=... + // TODO: Process $apply and $compute in the $expand here, will support later. + // $apply=...; $compute=... - // Expression: - // "navigation property name" - Expression propertyName = CreatePropertyNameExpression(context, structuredType, navigationProperty, source); + // Expression: + // "navigation property name" + Expression propertyName = CreatePropertyNameExpression(context, structuredType, navigationProperty, source); - // Expression: - // source.NavigationProperty - Expression propertyValue = CreatePropertyValueExpression(context, structuredType, navigationProperty, source, expandedItem.FilterOption, expandedItem.ComputeOption); + // Expression: + // source.NavigationProperty + Expression propertyValue = CreatePropertyValueExpression(context, structuredType, navigationProperty, source, expandedItem.FilterOption, expandedItem.ComputeOption); - // Sub select and expand could be null if the expanded navigation property is not further projected or expanded. - SelectExpandClause subSelectExpandClause = GetOrCreateSelectExpandClause(navigationProperty, expandedItem); + // Sub select and expand could be null if the expanded navigation property is not further projected or expanded. + SelectExpandClause subSelectExpandClause = GetOrCreateSelectExpandClause(navigationProperty, expandedItem); - Expression nullCheck = GetNullCheckExpression(context, navigationProperty, propertyValue, subSelectExpandClause); + Expression nullCheck = GetNullCheckExpression(context, navigationProperty, propertyValue, subSelectExpandClause); - Expression countExpression = CreateTotalCountExpression(context, propertyValue, expandedItem.CountOption); + Expression countExpression = CreateTotalCountExpression(context, propertyValue, expandedItem.CountOption); - int? modelBoundPageSize = querySettings == null ? null : querySettings.PageSize; - propertyValue = ProjectAsWrapper(context, propertyValue, subSelectExpandClause, edmEntityType, expandedItem.NavigationSource, - expandedItem.OrderByOption, // $orderby=... - expandedItem.ComputeOption, - expandedItem.TopOption, // $top=... - expandedItem.SkipOption, // $skip=... - modelBoundPageSize); + int? modelBoundPageSize = querySettings == null ? null : querySettings.PageSize; + propertyValue = ProjectAsWrapper(context, propertyValue, subSelectExpandClause, edmEntityType, expandedItem.NavigationSource, + expandedItem.OrderByOption, // $orderby=... + expandedItem.ComputeOption, + expandedItem.TopOption, // $top=... + expandedItem.SkipOption, // $skip=... + modelBoundPageSize); - NamedPropertyExpression propertyExpression = new NamedPropertyExpression(propertyName, propertyValue); - if (subSelectExpandClause != null) + NamedPropertyExpression propertyExpression = new NamedPropertyExpression(propertyName, propertyValue); + if (subSelectExpandClause != null) + { + if (!navigationProperty.Type.IsCollection()) { - if (!navigationProperty.Type.IsCollection()) - { - propertyExpression.NullCheck = nullCheck; - } - else if (settings.PageSize.HasValue) - { - propertyExpression.PageSize = settings.PageSize.Value; - } - else - { - if (querySettings != null && querySettings.PageSize.HasValue) - { - propertyExpression.PageSize = querySettings.PageSize.Value; - } - } - - propertyExpression.TotalCount = countExpression; - propertyExpression.CountOption = expandedItem.CountOption; + propertyExpression.NullCheck = nullCheck; } - - includedProperties.Add(propertyExpression); - } - - /// - /// Build the structural property into the included properties. - /// The property name is the structural property name. - /// The property value is the structural property value from the source and applied the nested query options. - /// - /// Wrapper for properties used by the . - /// The source contains the structural property. - /// The structured type or its derived type contains the structural property. - /// The selected structural property. - /// The selected item. It may contain the nested query options and could be null. - /// The container to hold the created property. - internal void BuildSelectedProperty(QueryBinderContext context, Expression source, IEdmStructuredType structuredType, - IEdmStructuralProperty structuralProperty, PathSelectItem pathSelectItem, - IList includedProperties) - { - Contract.Assert(source != null); - Contract.Assert(context != null); - Contract.Assert(structuredType != null); - Contract.Assert(structuralProperty != null); - Contract.Assert(includedProperties != null); - - IEdmModel model = context.Model; - ODataQuerySettings settings = context.QuerySettings; - - // // Expression: - // "navigation property name" - Expression propertyName = CreatePropertyNameExpression(context, structuredType, structuralProperty, source); - - // Expression: - // source.NavigationProperty - Expression propertyValue; - if (pathSelectItem == null) + else if (settings.PageSize.HasValue) { - propertyValue = CreatePropertyValueExpression(context, structuredType, structuralProperty, source, filterClause: null, computeClause: null); - includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue)); - return; + propertyExpression.PageSize = settings.PageSize.Value; } - - SelectExpandClause subSelectExpandClause = pathSelectItem.SelectAndExpand; - - // TODO: Process $compute in the $select ahead. - // $compute=... - - propertyValue = CreatePropertyValueExpression(context, structuredType, structuralProperty, source, pathSelectItem.FilterOption, pathSelectItem.ComputeOption); - Type propertyValueType = propertyValue.Type; - if (propertyValueType == typeof(char[]) || propertyValueType == typeof(byte[])) + else { - includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue)); - return; + if (querySettings != null && querySettings.PageSize.HasValue) + { + propertyExpression.PageSize = querySettings.PageSize.Value; + } } - Expression nullCheck = GetNullCheckExpression(structuralProperty, propertyValue, subSelectExpandClause); - - Expression countExpression = CreateTotalCountExpression(context, propertyValue, pathSelectItem.CountOption); + propertyExpression.TotalCount = countExpression; + propertyExpression.CountOption = expandedItem.CountOption; + } - // be noted: the property structured type could be null, because the property maybe not a complex property. - IEdmStructuredType propertyStructuredType = structuralProperty.Type.ToStructuredType(); - ModelBoundQuerySettings querySettings = null; - if (propertyStructuredType != null) - { - querySettings = model.GetModelBoundQuerySettings(structuralProperty, propertyStructuredType); - } + includedProperties.Add(propertyExpression); + } - int? modelBoundPageSize = querySettings == null ? null : querySettings.PageSize; - propertyValue = ProjectAsWrapper(context, propertyValue, subSelectExpandClause, propertyStructuredType, pathSelectItem.NavigationSource, - pathSelectItem.OrderByOption, // $orderby=... - pathSelectItem.ComputeOption, - pathSelectItem.TopOption, // $top=... - pathSelectItem.SkipOption, // $skip=... - modelBoundPageSize); + /// + /// Build the structural property into the included properties. + /// The property name is the structural property name. + /// The property value is the structural property value from the source and applied the nested query options. + /// + /// Wrapper for properties used by the . + /// The source contains the structural property. + /// The structured type or its derived type contains the structural property. + /// The selected structural property. + /// The selected item. It may contain the nested query options and could be null. + /// The container to hold the created property. + internal void BuildSelectedProperty(QueryBinderContext context, Expression source, IEdmStructuredType structuredType, + IEdmStructuralProperty structuralProperty, PathSelectItem pathSelectItem, + IList includedProperties) + { + Contract.Assert(source != null); + Contract.Assert(context != null); + Contract.Assert(structuredType != null); + Contract.Assert(structuralProperty != null); + Contract.Assert(includedProperties != null); + + IEdmModel model = context.Model; + ODataQuerySettings settings = context.QuerySettings; + + // // Expression: + // "navigation property name" + Expression propertyName = CreatePropertyNameExpression(context, structuredType, structuralProperty, source); + + // Expression: + // source.NavigationProperty + Expression propertyValue; + if (pathSelectItem == null) + { + propertyValue = CreatePropertyValueExpression(context, structuredType, structuralProperty, source, filterClause: null, computeClause: null); + includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue)); + return; + } - NamedPropertyExpression propertyExpression = new NamedPropertyExpression(propertyName, propertyValue); - if (subSelectExpandClause != null) - { - if (!structuralProperty.Type.IsCollection()) - { - propertyExpression.NullCheck = nullCheck; - } + SelectExpandClause subSelectExpandClause = pathSelectItem.SelectAndExpand; - propertyExpression.TotalCount = countExpression; - propertyExpression.CountOption = pathSelectItem.CountOption; - } + // TODO: Process $compute in the $select ahead. + // $compute=... - includedProperties.Add(propertyExpression); + propertyValue = CreatePropertyValueExpression(context, structuredType, structuralProperty, source, pathSelectItem.FilterOption, pathSelectItem.ComputeOption); + Type propertyValueType = propertyValue.Type; + if (propertyValueType == typeof(char[]) || propertyValueType == typeof(byte[])) + { + includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue)); + return; } - /// - /// Bind the computed property. - /// - /// The source contains the compute property. - /// Wrapper for properties used by the . - /// The compute property name. - /// The container to hold the created property. - public virtual void BindComputedProperty(Expression source, QueryBinderContext context, string computedProperty, - IList includedProperties) + Expression nullCheck = GetNullCheckExpression(structuralProperty, propertyValue, subSelectExpandClause); + + Expression countExpression = CreateTotalCountExpression(context, propertyValue, pathSelectItem.CountOption); + + // be noted: the property structured type could be null, because the property maybe not a complex property. + IEdmStructuredType propertyStructuredType = structuralProperty.Type.ToStructuredType(); + ModelBoundQuerySettings querySettings = null; + if (propertyStructuredType != null) { - if (source == null) - { - throw Error.ArgumentNull(nameof(source)); - } + querySettings = model.GetModelBoundQuerySettings(structuralProperty, propertyStructuredType); + } - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + int? modelBoundPageSize = querySettings == null ? null : querySettings.PageSize; + propertyValue = ProjectAsWrapper(context, propertyValue, subSelectExpandClause, propertyStructuredType, pathSelectItem.NavigationSource, + pathSelectItem.OrderByOption, // $orderby=... + pathSelectItem.ComputeOption, + pathSelectItem.TopOption, // $top=... + pathSelectItem.SkipOption, // $skip=... + modelBoundPageSize); - if (includedProperties == null) + NamedPropertyExpression propertyExpression = new NamedPropertyExpression(propertyName, propertyValue); + if (subSelectExpandClause != null) + { + if (!structuralProperty.Type.IsCollection()) { - throw Error.ArgumentNull(nameof(includedProperties)); + propertyExpression.NullCheck = nullCheck; } - if (!context.ComputedProperties.TryGetValue(computedProperty, out var computeExpression)) - { - return; - } + propertyExpression.TotalCount = countExpression; + propertyExpression.CountOption = pathSelectItem.CountOption; + } - Expression backSource = context.Source; - context.Source = source; + includedProperties.Add(propertyExpression); + } - // Pay attention: When it's nested $compute, the reference range variable is the source here. - Expression propertyValue = Bind(computeExpression.Expression, context); - Expression propertyName = Expression.Constant(computedProperty); - includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue)); + /// + /// Bind the computed property. + /// + /// The source contains the compute property. + /// Wrapper for properties used by the . + /// The compute property name. + /// The container to hold the created property. + public virtual void BindComputedProperty(Expression source, QueryBinderContext context, string computedProperty, + IList includedProperties) + { + if (source == null) + { + throw Error.ArgumentNull(nameof(source)); + } - context.Source = backSource; + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); } - /// - /// Build the dynamic properties into the included properties. - /// - /// The wrapper for properties used by the . - /// The source contains the dynamic property. - /// The structured type contains the dynamic property. - /// The container to hold the created property. - public virtual void BuildDynamicProperty(QueryBinderContext context, Expression source, IEdmStructuredType structuredType, - IList includedProperties) + if (includedProperties == null) { - Contract.Assert(source != null); - Contract.Assert(context != null); - Contract.Assert(structuredType != null); - Contract.Assert(includedProperties != null); + throw Error.ArgumentNull(nameof(includedProperties)); + } - IEdmModel model = context.Model; - ODataQuerySettings settings = context.QuerySettings; + if (!context.ComputedProperties.TryGetValue(computedProperty, out var computeExpression)) + { + return; + } - PropertyInfo dynamicPropertyDictionary = model.GetDynamicPropertyDictionary(structuredType); - if (dynamicPropertyDictionary != null) - { - Expression propertyName = Expression.Constant(dynamicPropertyDictionary.Name); - Expression propertyValue = Expression.Property(source, dynamicPropertyDictionary.Name); - Expression nullablePropertyValue = ExpressionHelpers.ToNullable(propertyValue); - if (settings.HandleNullPropagation == HandleNullPropagationOption.True) - { - // source == null ? null : propertyValue - propertyValue = Expression.Condition( - test: Expression.Equal(source, Expression.Constant(value: null)), - ifTrue: Expression.Constant(value: null, type: TypeHelper.ToNullable(propertyValue.Type)), - ifFalse: nullablePropertyValue); - } - else - { - propertyValue = nullablePropertyValue; - } + Expression backSource = context.Source; + context.Source = source; - includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue)); - } - } + // Pay attention: When it's nested $compute, the reference range variable is the source here. + Expression propertyValue = Bind(computeExpression.Expression, context); + Expression propertyName = Expression.Constant(computedProperty); + includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue)); + + context.Source = backSource; + } + + /// + /// Build the dynamic properties into the included properties. + /// + /// The wrapper for properties used by the . + /// The source contains the dynamic property. + /// The structured type contains the dynamic property. + /// The container to hold the created property. + public virtual void BuildDynamicProperty(QueryBinderContext context, Expression source, IEdmStructuredType structuredType, + IList includedProperties) + { + Contract.Assert(source != null); + Contract.Assert(context != null); + Contract.Assert(structuredType != null); + Contract.Assert(includedProperties != null); + + IEdmModel model = context.Model; + ODataQuerySettings settings = context.QuerySettings; - /// - /// Build the orderby clause into the included properties. - /// For example: $orderby=tolower(substring(name,1,2)) - /// - /// The query binder context. - /// The source. - /// The structured type. - /// The container to hold the created property. - /// Is select all flag. - protected virtual void BindOrderByProperties(QueryBinderContext context, Expression source, IEdmStructuredType structuredType, - IList includedProperties, bool isSelectedAll) + PropertyInfo dynamicPropertyDictionary = model.GetDynamicPropertyDictionary(structuredType); + if (dynamicPropertyDictionary != null) { - if (context == null || context.OrderByClauses == null || source == null || structuredType == null || includedProperties == null) + Expression propertyName = Expression.Constant(dynamicPropertyDictionary.Name); + Expression propertyValue = Expression.Property(source, dynamicPropertyDictionary.Name); + Expression nullablePropertyValue = ExpressionHelpers.ToNullable(propertyValue); + if (settings.HandleNullPropagation == HandleNullPropagationOption.True) { - return; + // source == null ? null : propertyValue + propertyValue = Expression.Condition( + test: Expression.Equal(source, Expression.Constant(value: null)), + ifTrue: Expression.Constant(value: null, type: TypeHelper.ToNullable(propertyValue.Type)), + ifFalse: nullablePropertyValue); } - - // Avoid duplicated binding. - HashSet usedPropertyNames = new HashSet(includedProperties.Count); - foreach (var p in includedProperties) + else { - if (p.Name is ConstantExpression constExp && constExp.Type == typeof(string)) - { - usedPropertyNames.Add(constExp.Value.ToString()); - } + propertyValue = nullablePropertyValue; } - Expression propertyName; - Expression propertyValue; - List names = new List(context.OrderByClauses.Count); - string name; - int index = 1; - foreach (OrderByClause clause in context.OrderByClauses) - { - // It could do things duplicated, for example if we have: - // $orderby=tolower(dynamic)$compute=...as dynamic&$select=dynamic - // dynamic is bind already since it's in $select, ideally we don't need to bind 'dynamic' in $orderby again (even we cached the compute binding once) - // Besides, for 'top-level' key orderby, since the key properties are auto-select, we don't need to bind them again. - if (clause.IsTopLevelSingleProperty(out IEdmProperty edmProperty, out name)) - { - // $orderby=Id or - // $orderby=computedProp&$select=computedProp - if ((edmProperty != null && isSelectedAll) || usedPropertyNames.Contains(name)) - { - continue; - } - } - - name = GetOrderByName(usedPropertyNames, ref index); - names.Add(name); - - // Since we generate the Unique name, it's safe NOT adding the generated name into set. - // Leave this code here for reference. - //usedPropertyNames.Add(name); + includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue)); + } + } - propertyName = Expression.Constant(name); - propertyValue = Bind(clause.Expression, context); - includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue) - { - AutoSelected = true - }); - } + /// + /// Build the orderby clause into the included properties. + /// For example: $orderby=tolower(substring(name,1,2)) + /// + /// The query binder context. + /// The source. + /// The structured type. + /// The container to hold the created property. + /// Is select all flag. + protected virtual void BindOrderByProperties(QueryBinderContext context, Expression source, IEdmStructuredType structuredType, + IList includedProperties, bool isSelectedAll) + { + if (context == null || context.OrderByClauses == null || source == null || structuredType == null || includedProperties == null) + { + return; + } - if (names.Count > 0) + // Avoid duplicated binding. + HashSet usedPropertyNames = new HashSet(includedProperties.Count); + foreach (var p in includedProperties) + { + if (p.Name is ConstantExpression constExp && constExp.Type == typeof(string)) { - // We use this to keep the order and naming mapping. - propertyName = Expression.Constant(OrderByClauseHelpers.OrderByGlobalNameKey); - propertyValue = Expression.Constant(string.Join(",", names)); - includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue) - { - AutoSelected = true - }); + usedPropertyNames.Add(constExp.Value.ToString()); } } - private static string GetOrderByName(HashSet usedPropertyNames, ref int start) + Expression propertyName; + Expression propertyValue; + List names = new List(context.OrderByClauses.Count); + string name; + int index = 1; + foreach (OrderByClause clause in context.OrderByClauses) { - do - { - // For advanced orderby, for example: $orderby=tolower(substring(location/street,1,2)) - // we want to create a 'propertyName: propertyValue' binding. - // Since we cannot use the raw orderby literal to naming the property name, since it could be complicated and invalid - // We create a random name for each orderby clause. - string name = $"{OrderByClauseHelpers.OrderByPropertyNamePrefix}{start++}"; - if (!usedPropertyNames.Contains(name)) + // It could do things duplicated, for example if we have: + // $orderby=tolower(dynamic)$compute=...as dynamic&$select=dynamic + // dynamic is bind already since it's in $select, ideally we don't need to bind 'dynamic' in $orderby again (even we cached the compute binding once) + // Besides, for 'top-level' key orderby, since the key properties are auto-select, we don't need to bind them again. + if (clause.IsTopLevelSingleProperty(out IEdmProperty edmProperty, out name)) + { + // $orderby=Id or + // $orderby=computedProp&$select=computedProp + if ((edmProperty != null && isSelectedAll) || usedPropertyNames.Contains(name)) { - return name; + continue; } } - while (true); + + name = GetOrderByName(usedPropertyNames, ref index); + names.Add(name); + + // Since we generate the Unique name, it's safe NOT adding the generated name into set. + // Leave this code here for reference. + //usedPropertyNames.Add(name); + + propertyName = Expression.Constant(name); + propertyValue = Bind(clause.Expression, context); + includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue) + { + AutoSelected = true + }); } - private static SelectExpandClause GetOrCreateSelectExpandClause(IEdmNavigationProperty navigationProperty, ExpandedReferenceSelectItem expandedItem) + if (names.Count > 0) { - // for normal $expand=.... - ExpandedNavigationSelectItem expandNavigationSelectItem = expandedItem as ExpandedNavigationSelectItem; - if (expandNavigationSelectItem != null) + // We use this to keep the order and naming mapping. + propertyName = Expression.Constant(OrderByClauseHelpers.OrderByGlobalNameKey); + propertyValue = Expression.Constant(string.Join(",", names)); + includedProperties.Add(new NamedPropertyExpression(propertyName, propertyValue) { - return expandNavigationSelectItem.SelectAndExpand; - } + AutoSelected = true + }); + } + } - // for $expand=..../$ref, just includes the keys properties - IList selectItems = new List(); - foreach (IEdmStructuralProperty keyProperty in navigationProperty.ToEntityType().Key()) + private static string GetOrderByName(HashSet usedPropertyNames, ref int start) + { + do + { + // For advanced orderby, for example: $orderby=tolower(substring(location/street,1,2)) + // we want to create a 'propertyName: propertyValue' binding. + // Since we cannot use the raw orderby literal to naming the property name, since it could be complicated and invalid + // We create a random name for each orderby clause. + string name = $"{OrderByClauseHelpers.OrderByPropertyNamePrefix}{start++}"; + if (!usedPropertyNames.Contains(name)) { - selectItems.Add(new PathSelectItem(new ODataSelectPath(new PropertySegment(keyProperty)))); + return name; } - - return new SelectExpandClause(selectItems, false); } + while (true); + } - private Expression AddOrderByQueryForSource(QueryBinderContext context, Expression source, OrderByClause orderbyClause, Type elementType) + private static SelectExpandClause GetOrCreateSelectExpandClause(IEdmNavigationProperty navigationProperty, ExpandedReferenceSelectItem expandedItem) + { + // for normal $expand=.... + ExpandedNavigationSelectItem expandNavigationSelectItem = expandedItem as ExpandedNavigationSelectItem; + if (expandNavigationSelectItem != null) { - if (orderbyClause != null && IsAvailableODataQueryOption(context.QuerySettings, AllowedQueryOptions.OrderBy)) - { - // TODO: Implement proper support for $select/$expand after $apply - ODataQuerySettings newSettings = new ODataQuerySettings(); - newSettings.CopyFrom(context.QuerySettings); - newSettings.HandleNullPropagation = HandleNullPropagationOption.True; - QueryBinderContext binderContext = new QueryBinderContext(context, newSettings, elementType); - - source = OrderByBinder.ApplyBind(source, orderbyClause, binderContext, false); - } + return expandNavigationSelectItem.SelectAndExpand; + } - return source; + // for $expand=..../$ref, just includes the keys properties + IList selectItems = new List(); + foreach (IEdmStructuralProperty keyProperty in navigationProperty.ToEntityType().Key()) + { + selectItems.Add(new PathSelectItem(new ODataSelectPath(new PropertySegment(keyProperty)))); } - private static Expression GetNullCheckExpression(IEdmStructuralProperty propertyToInclude, Expression propertyValue, - SelectExpandClause projection) + return new SelectExpandClause(selectItems, false); + } + + private Expression AddOrderByQueryForSource(QueryBinderContext context, Expression source, OrderByClause orderbyClause, Type elementType) + { + if (orderbyClause != null && IsAvailableODataQueryOption(context.QuerySettings, AllowedQueryOptions.OrderBy)) { - if (projection == null || propertyToInclude.Type.IsCollection()) - { - return null; - } + // TODO: Implement proper support for $select/$expand after $apply + ODataQuerySettings newSettings = new ODataQuerySettings(); + newSettings.CopyFrom(context.QuerySettings); + newSettings.HandleNullPropagation = HandleNullPropagationOption.True; + QueryBinderContext binderContext = new QueryBinderContext(context, newSettings, elementType); - if (IsSelectAll(projection) && propertyToInclude.Type.IsComplex()) - { - // for Collections (Primitive, Enum, Complex collection), that's check above. - return Expression.Equal(propertyValue, Expression.Constant(null)); - } + source = OrderByBinder.ApplyBind(source, orderbyClause, binderContext, false); + } + + return source; + } + private static Expression GetNullCheckExpression(IEdmStructuralProperty propertyToInclude, Expression propertyValue, + SelectExpandClause projection) + { + if (projection == null || propertyToInclude.Type.IsCollection()) + { return null; } - private Expression GetNullCheckExpression(QueryBinderContext context, IEdmNavigationProperty propertyToExpand, Expression propertyValue, - SelectExpandClause projection) + if (IsSelectAll(projection) && propertyToInclude.Type.IsComplex()) { - if (projection == null || propertyToExpand.Type.IsCollection()) - { - return null; - } + // for Collections (Primitive, Enum, Complex collection), that's check above. + return Expression.Equal(propertyValue, Expression.Constant(null)); + } - if (IsSelectAll(projection) || !propertyToExpand.ToEntityType().Key().Any()) - { - return Expression.Equal(propertyValue, Expression.Constant(null)); - } + return null; + } - Expression keysNullCheckExpression = null; - foreach (var key in propertyToExpand.ToEntityType().Key()) - { - var propertyValueExpression = CreatePropertyValueExpression(context, propertyToExpand.ToEntityType(), key, propertyValue, filterClause: null); - var keyExpression = Expression.Equal( - propertyValueExpression, - Expression.Constant(null, propertyValueExpression.Type)); - - keysNullCheckExpression = keysNullCheckExpression == null - ? keyExpression - : Expression.And(keysNullCheckExpression, keyExpression); - } + private Expression GetNullCheckExpression(QueryBinderContext context, IEdmNavigationProperty propertyToExpand, Expression propertyValue, + SelectExpandClause projection) + { + if (projection == null || propertyToExpand.Type.IsCollection()) + { + return null; + } - return keysNullCheckExpression; + if (IsSelectAll(projection) || !propertyToExpand.ToEntityType().Key().Any()) + { + return Expression.Equal(propertyValue, Expression.Constant(null)); } - // new CollectionWrapper { Instance = source.Select((ElementType element) => new Wrapper { }) } - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "These are simple conversion function and cannot be split up.")] - private Expression ProjectCollection(QueryBinderContext context, Expression source, Type elementType, - SelectExpandClause selectExpandClause, IEdmStructuredType structuredType, IEdmNavigationSource navigationSource, - OrderByClause orderByClause, - long? topOption, - long? skipOption, - int? modelBoundPageSize) + Expression keysNullCheckExpression = null; + foreach (var key in propertyToExpand.ToEntityType().Key()) { - ODataQuerySettings settings = context.QuerySettings; + var propertyValueExpression = CreatePropertyValueExpression(context, propertyToExpand.ToEntityType(), key, propertyValue, filterClause: null); + var keyExpression = Expression.Equal( + propertyValueExpression, + Expression.Constant(null, propertyValueExpression.Type)); + + keysNullCheckExpression = keysNullCheckExpression == null + ? keyExpression + : Expression.And(keysNullCheckExpression, keyExpression); + } - // structuralType could be null, because it can be primitive collection. + return keysNullCheckExpression; + } - ParameterExpression element = context.CurrentParameter; + // new CollectionWrapper { Instance = source.Select((ElementType element) => new Wrapper { }) } + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "These are simple conversion function and cannot be split up.")] + private Expression ProjectCollection(QueryBinderContext context, Expression source, Type elementType, + SelectExpandClause selectExpandClause, IEdmStructuredType structuredType, IEdmNavigationSource navigationSource, + OrderByClause orderByClause, + long? topOption, + long? skipOption, + int? modelBoundPageSize) + { + ODataQuerySettings settings = context.QuerySettings; - Expression projection; - // expression - // new Wrapper { } - if (structuredType != null) - { - projection = ProjectElement(context, element, selectExpandClause, structuredType, navigationSource); - } - else - { - projection = element; - } + // structuralType could be null, because it can be primitive collection. - // expression - // (ElementType element) => new Wrapper { } - LambdaExpression selector = Expression.Lambda(projection, element); + ParameterExpression element = context.CurrentParameter; - if (orderByClause != null) - { - source = AddOrderByQueryForSource(context, source, orderByClause, elementType); - } + Expression projection; + // expression + // new Wrapper { } + if (structuredType != null) + { + projection = ProjectElement(context, element, selectExpandClause, structuredType, navigationSource); + } + else + { + projection = element; + } - bool hasTopValue = topOption != null && topOption.HasValue; - bool hasSkipvalue = skipOption != null && skipOption.HasValue; + // expression + // (ElementType element) => new Wrapper { } + LambdaExpression selector = Expression.Lambda(projection, element); - if (structuredType is IEdmEntityType entityType) - { - if (settings.PageSize.HasValue || modelBoundPageSize.HasValue || hasTopValue || hasSkipvalue) + if (orderByClause != null) + { + source = AddOrderByQueryForSource(context, source, orderByClause, elementType); + } + + bool hasTopValue = topOption != null && topOption.HasValue; + bool hasSkipvalue = skipOption != null && skipOption.HasValue; + + if (structuredType is IEdmEntityType entityType) + { + if (settings.PageSize.HasValue || modelBoundPageSize.HasValue || hasTopValue || hasSkipvalue) + { + // nested paging. Need to apply order by first, and take one more than page size as we need to know + // whether the collection was truncated or not while generating next page links. + IEnumerable properties = + entityType.Key().Any() + ? entityType.Key() + : entityType + .StructuralProperties() + .Where(property => property.Type.IsPrimitive() && !property.Type.IsStream()) + .OrderBy(property => property.Name); + + if (orderByClause == null) { - // nested paging. Need to apply order by first, and take one more than page size as we need to know - // whether the collection was truncated or not while generating next page links. - IEnumerable properties = - entityType.Key().Any() - ? entityType.Key() - : entityType - .StructuralProperties() - .Where(property => property.Type.IsPrimitive() && !property.Type.IsStream()) - .OrderBy(property => property.Name); - - if (orderByClause == null) + bool alreadyOrdered = false; + foreach (var prop in properties) { - bool alreadyOrdered = false; - foreach (var prop in properties) - { - string propertyName = context.Model.GetClrPropertyName(prop); - source = ExpressionHelpers.OrderByPropertyExpression(source, propertyName, elementType, - alreadyOrdered); + string propertyName = context.Model.GetClrPropertyName(prop); + source = ExpressionHelpers.OrderByPropertyExpression(source, propertyName, elementType, + alreadyOrdered); - if (!alreadyOrdered) - { - alreadyOrdered = true; - } + if (!alreadyOrdered) + { + alreadyOrdered = true; } } } } + } - if (hasSkipvalue && IsAvailableODataQueryOption(context.QuerySettings, AllowedQueryOptions.Skip)) - { - Contract.Assert(skipOption.Value <= Int32.MaxValue); - source = ExpressionHelpers.Skip(source, (int)skipOption.Value, elementType, - settings.EnableConstantParameterization); - } + if (hasSkipvalue && IsAvailableODataQueryOption(context.QuerySettings, AllowedQueryOptions.Skip)) + { + Contract.Assert(skipOption.Value <= Int32.MaxValue); + source = ExpressionHelpers.Skip(source, (int)skipOption.Value, elementType, + settings.EnableConstantParameterization); + } - if (hasTopValue && IsAvailableODataQueryOption(context.QuerySettings, AllowedQueryOptions.Top)) - { - Contract.Assert(topOption.Value <= Int32.MaxValue); - source = ExpressionHelpers.Take(source, (int)topOption.Value, elementType, - settings.EnableConstantParameterization); - } + if (hasTopValue && IsAvailableODataQueryOption(context.QuerySettings, AllowedQueryOptions.Top)) + { + Contract.Assert(topOption.Value <= Int32.MaxValue); + source = ExpressionHelpers.Take(source, (int)topOption.Value, elementType, + settings.EnableConstantParameterization); + } - if (structuredType is IEdmEntityType) + if (structuredType is IEdmEntityType) + { + if (settings.PageSize.HasValue || modelBoundPageSize.HasValue) { - if (settings.PageSize.HasValue || modelBoundPageSize.HasValue) + // don't page nested collections if EnableCorrelatedSubqueryBuffering is enabled + if (!settings.EnableCorrelatedSubqueryBuffering) { - // don't page nested collections if EnableCorrelatedSubqueryBuffering is enabled - if (!settings.EnableCorrelatedSubqueryBuffering) + if (settings.PageSize.HasValue) { - if (settings.PageSize.HasValue) - { - source = ExpressionHelpers.Take(source, settings.PageSize.Value + 1, elementType, - settings.EnableConstantParameterization); - } - else if (settings.ModelBoundPageSize.HasValue) - { - source = ExpressionHelpers.Take(source, modelBoundPageSize.Value + 1, elementType, - settings.EnableConstantParameterization); - } + source = ExpressionHelpers.Take(source, settings.PageSize.Value + 1, elementType, + settings.EnableConstantParameterization); + } + else if (settings.ModelBoundPageSize.HasValue) + { + source = ExpressionHelpers.Take(source, modelBoundPageSize.Value + 1, elementType, + settings.EnableConstantParameterization); } } } + } - // expression - // source.Select((ElementType element) => new Wrapper { }) - var selectMethod = GetSelectMethod(elementType, projection.Type); - Expression selectedExpresion = Expression.Call(selectMethod, source, selector); + // expression + // source.Select((ElementType element) => new Wrapper { }) + var selectMethod = GetSelectMethod(elementType, projection.Type); + Expression selectedExpresion = Expression.Call(selectMethod, source, selector); - // Append ToList() to collection as a hint to LINQ provider to buffer correlated sub-queries in memory and avoid executing N+1 queries - if (settings.EnableCorrelatedSubqueryBuffering) - { - selectedExpresion = Expression.Call(ExpressionHelperMethods.QueryableToList.MakeGenericMethod(projection.Type), selectedExpresion); - } + // Append ToList() to collection as a hint to LINQ provider to buffer correlated sub-queries in memory and avoid executing N+1 queries + if (settings.EnableCorrelatedSubqueryBuffering) + { + selectedExpresion = Expression.Call(ExpressionHelperMethods.QueryableToList.MakeGenericMethod(projection.Type), selectedExpresion); + } - if (settings.HandleNullPropagation == HandleNullPropagationOption.True) - { - // source == null ? null : projectedCollection - return Expression.Condition( - test: Expression.Equal(source, Expression.Constant(null)), - ifTrue: Expression.Constant(null, selectedExpresion.Type), - ifFalse: selectedExpresion); - } - else - { - return selectedExpresion; - } + if (settings.HandleNullPropagation == HandleNullPropagationOption.True) + { + // source == null ? null : projectedCollection + return Expression.Condition( + test: Expression.Equal(source, Expression.Constant(null)), + ifTrue: Expression.Constant(null, selectedExpresion.Type), + ifFalse: selectedExpresion); + } + else + { + return selectedExpresion; } + } - // OData formatter requires the type name of the entity that is being written if the type has derived types. - // Expression - // source is GrandChild ? "GrandChild" : ( source is Child ? "Child" : "Root" ) - // Notice that the order is important here. The most derived type must be the first to check. - // If entity framework had a way to figure out the type name without selecting the whole object, we don't have to do this magic. - /// - /// Create for Derived types. - /// - /// The original . - /// The which may contain the derived types. - /// The . - /// The with derived types if any are present. - public virtual Expression CreateTypeNameExpression(Expression source, IEdmStructuredType elementType, IEdmModel model) - { - if (source == null) - { - throw Error.ArgumentNull(nameof(source)); - } + // OData formatter requires the type name of the entity that is being written if the type has derived types. + // Expression + // source is GrandChild ? "GrandChild" : ( source is Child ? "Child" : "Root" ) + // Notice that the order is important here. The most derived type must be the first to check. + // If entity framework had a way to figure out the type name without selecting the whole object, we don't have to do this magic. + /// + /// Create for Derived types. + /// + /// The original . + /// The which may contain the derived types. + /// The . + /// The with derived types if any are present. + public virtual Expression CreateTypeNameExpression(Expression source, IEdmStructuredType elementType, IEdmModel model) + { + if (source == null) + { + throw Error.ArgumentNull(nameof(source)); + } - if (elementType == null) - { - throw Error.ArgumentNull(nameof(elementType)); - } + if (elementType == null) + { + throw Error.ArgumentNull(nameof(elementType)); + } - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); + } - IReadOnlyList derivedTypes = GetAllDerivedTypes(elementType, model); - if (derivedTypes.Count == 0) - { - // no inheritance. - return null; - } - else + IReadOnlyList derivedTypes = GetAllDerivedTypes(elementType, model); + if (derivedTypes.Count == 0) + { + // no inheritance. + return null; + } + else + { + Expression expression = Expression.Constant(elementType.FullTypeName()); + for (int i = 0; i < derivedTypes.Count; i++) { - Expression expression = Expression.Constant(elementType.FullTypeName()); - for (int i = 0; i < derivedTypes.Count; i++) + Type clrType = model.GetClrType(derivedTypes[i]); + if (clrType == null) { - Type clrType = model.GetClrType(derivedTypes[i]); - if (clrType == null) - { - throw new ODataException(Error.Format(SRResources.MappingDoesNotContainResourceType, derivedTypes[0].FullTypeName())); - } - - expression = Expression.Condition( - test: Expression.TypeIs(source, clrType), - ifTrue: Expression.Constant(derivedTypes[i].FullTypeName()), - ifFalse: expression); + throw new ODataException(Error.Format(SRResources.MappingDoesNotContainResourceType, derivedTypes[0].FullTypeName())); } - return expression; + expression = Expression.Condition( + test: Expression.TypeIs(source, clrType), + ifTrue: Expression.Constant(derivedTypes[i].FullTypeName()), + ifFalse: expression); } - } - private static bool IsAvailableODataQueryOption(ODataQuerySettings querySettings, AllowedQueryOptions queryOptionFlag) - { - return (querySettings.IgnoredNestedQueryOptions & queryOptionFlag) == AllowedQueryOptions.None; + return expression; } + } - // returns all the derived types (direct and indirect) of baseType ordered according to their depth. The direct children - // are the first in the list. - private static IReadOnlyList GetAllDerivedTypes(IEdmStructuredType baseType, IEdmModel model) - { - IEnumerable allStructuredTypes = model.SchemaElements.OfType(); + private static bool IsAvailableODataQueryOption(ODataQuerySettings querySettings, AllowedQueryOptions queryOptionFlag) + { + return (querySettings.IgnoredNestedQueryOptions & queryOptionFlag) == AllowedQueryOptions.None; + } - List> derivedTypes = new List>(); - foreach (IEdmStructuredType structuredType in allStructuredTypes) + // returns all the derived types (direct and indirect) of baseType ordered according to their depth. The direct children + // are the first in the list. + private static IReadOnlyList GetAllDerivedTypes(IEdmStructuredType baseType, IEdmModel model) + { + IEnumerable allStructuredTypes = model.SchemaElements.OfType(); + + List> derivedTypes = new List>(); + foreach (IEdmStructuredType structuredType in allStructuredTypes) + { + int distance = IsDerivedTypeOf(structuredType, baseType); + if (distance > 0) { - int distance = IsDerivedTypeOf(structuredType, baseType); - if (distance > 0) - { - derivedTypes.Add(Tuple.Create(distance, structuredType)); - } + derivedTypes.Add(Tuple.Create(distance, structuredType)); } - - return derivedTypes.OrderBy(tuple => tuple.Item1).Select(tuple => tuple.Item2).ToList(); } - // returns -1 if type does not derive from baseType and a positive number representing the distance - // between them if it does. - private static int IsDerivedTypeOf(IEdmStructuredType type, IEdmStructuredType baseType) + return derivedTypes.OrderBy(tuple => tuple.Item1).Select(tuple => tuple.Item2).ToList(); + } + + // returns -1 if type does not derive from baseType and a positive number representing the distance + // between them if it does. + private static int IsDerivedTypeOf(IEdmStructuredType type, IEdmStructuredType baseType) + { + int distance = 0; + while (type != null) { - int distance = 0; - while (type != null) + if (baseType == type) { - if (baseType == type) - { - return distance; - } - - type = type.BaseType(); - distance++; + return distance; } - return -1; + type = type.BaseType(); + distance++; } - private static MethodInfo GetSelectMethod(Type elementType, Type resultType) + return -1; + } + + private static MethodInfo GetSelectMethod(Type elementType, Type resultType) + { + return ExpressionHelperMethods.EnumerableSelectGeneric.MakeGenericMethod(elementType, resultType); + } + + private static bool IsSelectAll(SelectExpandClause selectExpandClause) + { + if (selectExpandClause == null) { - return ExpressionHelperMethods.EnumerableSelectGeneric.MakeGenericMethod(elementType, resultType); + return true; } - private static bool IsSelectAll(SelectExpandClause selectExpandClause) + if (selectExpandClause.AllSelected || selectExpandClause.SelectedItems.OfType().Any()) { - if (selectExpandClause == null) - { - return true; - } - - if (selectExpandClause.AllSelected || selectExpandClause.SelectedItems.OfType().Any()) - { - return true; - } - - return false; + return true; } - private static Type GetWrapperGenericType(bool isInstancePropertySet, bool isTypeNamePropertySet, bool isContainerPropertySet) + return false; + } + + private static Type GetWrapperGenericType(bool isInstancePropertySet, bool isTypeNamePropertySet, bool isContainerPropertySet) + { + if (isInstancePropertySet) { - if (isInstancePropertySet) - { - // select all - Contract.Assert(!isTypeNamePropertySet, "we don't set type name if we set instance as it can be figured from instance"); + // select all + Contract.Assert(!isTypeNamePropertySet, "we don't set type name if we set instance as it can be figured from instance"); - return isContainerPropertySet ? typeof(SelectAllAndExpand<>) : typeof(SelectAll<>); - } - else - { - Contract.Assert(isContainerPropertySet, "if it is not select all, container should hold something"); + return isContainerPropertySet ? typeof(SelectAllAndExpand<>) : typeof(SelectAll<>); + } + else + { + Contract.Assert(isContainerPropertySet, "if it is not select all, container should hold something"); - return isTypeNamePropertySet ? typeof(SelectSomeAndInheritance<>) : typeof(SelectSome<>); - } + return isTypeNamePropertySet ? typeof(SelectSomeAndInheritance<>) : typeof(SelectSome<>); } + } + + private class ReferenceNavigationPropertyExpandFilterVisitor : ExpressionVisitor + { + private Expression _source; + private ParameterExpression _parameterExpression; - private class ReferenceNavigationPropertyExpandFilterVisitor : ExpressionVisitor + public ReferenceNavigationPropertyExpandFilterVisitor(ParameterExpression parameterExpression, Expression source) { - private Expression _source; - private ParameterExpression _parameterExpression; + _source = source; + _parameterExpression = parameterExpression; + } - public ReferenceNavigationPropertyExpandFilterVisitor(ParameterExpression parameterExpression, Expression source) + protected override Expression VisitParameter(ParameterExpression node) + { + if (node != _parameterExpression) { - _source = source; - _parameterExpression = parameterExpression; + throw new ODataException(Error.Format(SRResources.ReferenceNavigationPropertyExpandFilterVisitorUnexpectedParameter, node.Name)); } - protected override Expression VisitParameter(ParameterExpression node) - { - if (node != _parameterExpression) - { - throw new ODataException(Error.Format(SRResources.ReferenceNavigationPropertyExpandFilterVisitorUnexpectedParameter, node.Name)); - } - - return _source; - } + return _source; } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/TransformationBinderBase.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/TransformationBinderBase.cs index 683b7b5f4..eeb07a68a 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/TransformationBinderBase.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/TransformationBinderBase.cs @@ -15,162 +15,161 @@ using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +internal class TransformationBinderBase : ExpressionBinderBase { - internal class TransformationBinderBase : ExpressionBinderBase + internal TransformationBinderBase(ODataQuerySettings settings, IAssemblyResolver assembliesResolver, Type elementType, + IEdmModel model) : base(model, assembliesResolver, settings) { - internal TransformationBinderBase(ODataQuerySettings settings, IAssemblyResolver assembliesResolver, Type elementType, - IEdmModel model) : base(model, assembliesResolver, settings) - { - Contract.Assert(elementType != null); - LambdaParameter = Expression.Parameter(elementType, "$it"); - } + Contract.Assert(elementType != null); + LambdaParameter = Expression.Parameter(elementType, "$it"); + } - protected Type ElementType { get { return this.LambdaParameter.Type; } } + protected Type ElementType { get { return this.LambdaParameter.Type; } } - protected ParameterExpression LambdaParameter { get; set; } + protected ParameterExpression LambdaParameter { get; set; } - protected bool ClassicEF { get; private set; } + protected bool ClassicEF { get; private set; } - /// - /// Gets CLR type returned from the query. - /// - public Type ResultClrType - { - get; protected set; - } + /// + /// Gets CLR type returned from the query. + /// + public Type ResultClrType + { + get; protected set; + } - /// - /// Checks IQueryable provider for need of EF6 optimization - /// - /// - /// True if EF6 optimization are needed. - internal virtual bool IsClassicEF(IQueryable query) - { - var providerNS = query.Provider.GetType().Namespace; - return (providerNS == HandleNullPropagationOptionHelper.ObjectContextQueryProviderNamespaceEF6 - || providerNS == HandleNullPropagationOptionHelper.EntityFrameworkQueryProviderNamespace); - } + /// + /// Checks IQueryable provider for need of EF6 optimization + /// + /// + /// True if EF6 optimization are needed. + internal virtual bool IsClassicEF(IQueryable query) + { + var providerNS = query.Provider.GetType().Namespace; + return (providerNS == HandleNullPropagationOptionHelper.ObjectContextQueryProviderNamespaceEF6 + || providerNS == HandleNullPropagationOptionHelper.EntityFrameworkQueryProviderNamespace); + } - protected void PreprocessQuery(IQueryable query) - { - Contract.Assert(query != null); + protected void PreprocessQuery(IQueryable query) + { + Contract.Assert(query != null); - this.ClassicEF = IsClassicEF(query); - this.BaseQuery = query; - EnsureFlattenedPropertyContainer(this.LambdaParameter); - } + this.ClassicEF = IsClassicEF(query); + this.BaseQuery = query; + EnsureFlattenedPropertyContainer(this.LambdaParameter); + } - protected Expression WrapConvert(Expression expression) + protected Expression WrapConvert(Expression expression) + { + // Expression that we are generating looks like Value = $it.PropertyName where Value is defined as object and PropertyName can be value + // Proper .NET expression must look like as Value = (object) $it.PropertyName for proper boxing or AccessViolationException will be thrown + // Cast to object isn't translatable by EF6 as a result skipping (object) in that case + return (this.ClassicEF || !expression.Type.IsValueType) + ? expression + : Expression.Convert(expression, typeof(object)); + } + + public override Expression Bind(QueryNode node) + { + SingleValueNode singleValueNode = node as SingleValueNode; + if (node != null) { - // Expression that we are generating looks like Value = $it.PropertyName where Value is defined as object and PropertyName can be value - // Proper .NET expression must look like as Value = (object) $it.PropertyName for proper boxing or AccessViolationException will be thrown - // Cast to object isn't translatable by EF6 as a result skipping (object) in that case - return (this.ClassicEF || !expression.Type.IsValueType) - ? expression - : Expression.Convert(expression, typeof(object)); + return BindAccessor(singleValueNode); } - public override Expression Bind(QueryNode node) - { - SingleValueNode singleValueNode = node as SingleValueNode; - if (node != null) - { - return BindAccessor(singleValueNode); - } + throw Error.Argument(nameof(node), SRResources.OnlySingleValueNodeSupported); + } - throw Error.Argument(nameof(node), SRResources.OnlySingleValueNodeSupported); + protected override ParameterExpression Parameter + { + get + { + return this.LambdaParameter; } + } - protected override ParameterExpression Parameter + protected Expression BindAccessor(QueryNode node, Expression baseElement = null) + { + switch (node.Kind) { - get - { - return this.LambdaParameter; - } + case QueryNodeKind.ResourceRangeVariableReference: + return this.LambdaParameter.Type.IsGenericType && this.LambdaParameter.Type.GetGenericTypeDefinition() == typeof(FlatteningWrapper<>) + ? (Expression)Expression.Property(this.LambdaParameter, "Source") + : this.LambdaParameter; + case QueryNodeKind.SingleValuePropertyAccess: + var propAccessNode = node as SingleValuePropertyAccessNode; + return CreatePropertyAccessExpression(BindAccessor(propAccessNode.Source, baseElement), propAccessNode.Property, GetFullPropertyPath(propAccessNode)); + case QueryNodeKind.AggregatedCollectionPropertyNode: + var aggPropAccessNode = node as AggregatedCollectionPropertyNode; + return CreatePropertyAccessExpression(BindAccessor(aggPropAccessNode.Source, baseElement), aggPropAccessNode.Property); + case QueryNodeKind.SingleComplexNode: + var singleComplexNode = node as SingleComplexNode; + return CreatePropertyAccessExpression(BindAccessor(singleComplexNode.Source, baseElement), singleComplexNode.Property, GetFullPropertyPath(singleComplexNode)); + case QueryNodeKind.SingleValueOpenPropertyAccess: + var openNode = node as SingleValueOpenPropertyAccessNode; + return GetFlattenedPropertyExpression(openNode.Name) ?? CreateOpenPropertyAccessExpression(openNode); + case QueryNodeKind.None: + case QueryNodeKind.SingleNavigationNode: + var navNode = (SingleNavigationNode)node; + return CreatePropertyAccessExpression(BindAccessor(navNode.Source), navNode.NavigationProperty, GetFullPropertyPath(navNode)); + case QueryNodeKind.BinaryOperator: + var binaryNode = (BinaryOperatorNode)node; + var leftExpression = BindAccessor(binaryNode.Left, baseElement); + var rightExpression = BindAccessor(binaryNode.Right, baseElement); + return ExpressionBinderHelper.CreateBinaryExpression(binaryNode.OperatorKind, leftExpression, rightExpression, + liftToNull: true, QuerySettings); + case QueryNodeKind.Convert: + var convertNode = (ConvertNode)node; + return CreateConvertExpression(convertNode, BindAccessor(convertNode.Source, baseElement)); + case QueryNodeKind.CollectionNavigationNode: + return baseElement ?? this.LambdaParameter; + case QueryNodeKind.SingleValueFunctionCall: + return BindSingleValueFunctionCallNode(node as SingleValueFunctionCallNode); + case QueryNodeKind.Constant: + return BindConstantNode(node as ConstantNode); + default: + throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, node.Kind, + typeof(AggregationBinder).Name); } + } + + private Expression CreateOpenPropertyAccessExpression(SingleValueOpenPropertyAccessNode openNode) + { + Expression sourceAccessor = BindAccessor(openNode.Source); - protected Expression BindAccessor(QueryNode node, Expression baseElement = null) + // First check that property exists in source + // It's the case when we are apply transformation based on earlier transformation + if (sourceAccessor.Type.GetProperty(openNode.Name) != null) { - switch (node.Kind) - { - case QueryNodeKind.ResourceRangeVariableReference: - return this.LambdaParameter.Type.IsGenericType && this.LambdaParameter.Type.GetGenericTypeDefinition() == typeof(FlatteningWrapper<>) - ? (Expression)Expression.Property(this.LambdaParameter, "Source") - : this.LambdaParameter; - case QueryNodeKind.SingleValuePropertyAccess: - var propAccessNode = node as SingleValuePropertyAccessNode; - return CreatePropertyAccessExpression(BindAccessor(propAccessNode.Source, baseElement), propAccessNode.Property, GetFullPropertyPath(propAccessNode)); - case QueryNodeKind.AggregatedCollectionPropertyNode: - var aggPropAccessNode = node as AggregatedCollectionPropertyNode; - return CreatePropertyAccessExpression(BindAccessor(aggPropAccessNode.Source, baseElement), aggPropAccessNode.Property); - case QueryNodeKind.SingleComplexNode: - var singleComplexNode = node as SingleComplexNode; - return CreatePropertyAccessExpression(BindAccessor(singleComplexNode.Source, baseElement), singleComplexNode.Property, GetFullPropertyPath(singleComplexNode)); - case QueryNodeKind.SingleValueOpenPropertyAccess: - var openNode = node as SingleValueOpenPropertyAccessNode; - return GetFlattenedPropertyExpression(openNode.Name) ?? CreateOpenPropertyAccessExpression(openNode); - case QueryNodeKind.None: - case QueryNodeKind.SingleNavigationNode: - var navNode = (SingleNavigationNode)node; - return CreatePropertyAccessExpression(BindAccessor(navNode.Source), navNode.NavigationProperty, GetFullPropertyPath(navNode)); - case QueryNodeKind.BinaryOperator: - var binaryNode = (BinaryOperatorNode)node; - var leftExpression = BindAccessor(binaryNode.Left, baseElement); - var rightExpression = BindAccessor(binaryNode.Right, baseElement); - return ExpressionBinderHelper.CreateBinaryExpression(binaryNode.OperatorKind, leftExpression, rightExpression, - liftToNull: true, QuerySettings); - case QueryNodeKind.Convert: - var convertNode = (ConvertNode)node; - return CreateConvertExpression(convertNode, BindAccessor(convertNode.Source, baseElement)); - case QueryNodeKind.CollectionNavigationNode: - return baseElement ?? this.LambdaParameter; - case QueryNodeKind.SingleValueFunctionCall: - return BindSingleValueFunctionCallNode(node as SingleValueFunctionCallNode); - case QueryNodeKind.Constant: - return BindConstantNode(node as ConstantNode); - default: - throw Error.NotSupported(SRResources.QueryNodeBindingNotSupported, node.Kind, - typeof(AggregationBinder).Name); - } + return Expression.Property(sourceAccessor, openNode.Name); } - private Expression CreateOpenPropertyAccessExpression(SingleValueOpenPropertyAccessNode openNode) + // Property doesn't exists go for dynamic properties dictionary + PropertyInfo prop = GetDynamicPropertyContainer(openNode); + MemberExpression propertyAccessExpression = Expression.Property(sourceAccessor, prop.Name); + IndexExpression readDictionaryIndexerExpression = Expression.Property(propertyAccessExpression, + DictionaryStringObjectIndexerName, Expression.Constant(openNode.Name)); + MethodCallExpression containsKeyExpression = Expression.Call(propertyAccessExpression, + propertyAccessExpression.Type.GetMethod("ContainsKey"), Expression.Constant(openNode.Name)); + ConstantExpression nullExpression = Expression.Constant(null); + + if (QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) + { + var dynamicDictIsNotNull = Expression.NotEqual(propertyAccessExpression, Expression.Constant(null)); + var dynamicDictIsNotNullAndContainsKey = Expression.AndAlso(dynamicDictIsNotNull, containsKeyExpression); + return Expression.Condition( + dynamicDictIsNotNullAndContainsKey, + readDictionaryIndexerExpression, + nullExpression); + } + else { - Expression sourceAccessor = BindAccessor(openNode.Source); - - // First check that property exists in source - // It's the case when we are apply transformation based on earlier transformation - if (sourceAccessor.Type.GetProperty(openNode.Name) != null) - { - return Expression.Property(sourceAccessor, openNode.Name); - } - - // Property doesn't exists go for dynamic properties dictionary - PropertyInfo prop = GetDynamicPropertyContainer(openNode); - MemberExpression propertyAccessExpression = Expression.Property(sourceAccessor, prop.Name); - IndexExpression readDictionaryIndexerExpression = Expression.Property(propertyAccessExpression, - DictionaryStringObjectIndexerName, Expression.Constant(openNode.Name)); - MethodCallExpression containsKeyExpression = Expression.Call(propertyAccessExpression, - propertyAccessExpression.Type.GetMethod("ContainsKey"), Expression.Constant(openNode.Name)); - ConstantExpression nullExpression = Expression.Constant(null); - - if (QuerySettings.HandleNullPropagation == HandleNullPropagationOption.True) - { - var dynamicDictIsNotNull = Expression.NotEqual(propertyAccessExpression, Expression.Constant(null)); - var dynamicDictIsNotNullAndContainsKey = Expression.AndAlso(dynamicDictIsNotNull, containsKeyExpression); - return Expression.Condition( - dynamicDictIsNotNullAndContainsKey, - readDictionaryIndexerExpression, - nullExpression); - } - else - { - return Expression.Condition( - containsKeyExpression, - readDictionaryIndexerExpression, - nullExpression); - } + return Expression.Condition( + containsKeyExpression, + readDictionaryIndexerExpression, + nullExpression); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/UriFunctionsBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/UriFunctionsBinder.cs index 339955e42..81c57989a 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/UriFunctionsBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/UriFunctionsBinder.cs @@ -13,152 +13,151 @@ using System.Text; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Query.Expressions +namespace Microsoft.AspNetCore.OData.Query.Expressions; + +/// +/// This class helps to bind uri functions to CLR. +/// For creating an Expression and apply it on a Queryable collection, we must get the CLR information, +/// i.e MethodInfo of each EdmFunction which is mentioned in the EdmModel. +/// If you add a custom uri function in OData.Core via 'CustomUriFunctions' class, you must bind it to it's MethodInfo. +/// +internal static class UriFunctionsBinder { + private static Dictionary methodLiteralSignaturesToMethodInfo = new Dictionary(); + + private static object locker = new object(); + /// - /// This class helps to bind uri functions to CLR. - /// For creating an Expression and apply it on a Queryable collection, we must get the CLR information, - /// i.e MethodInfo of each EdmFunction which is mentioned in the EdmModel. - /// If you add a custom uri function in OData.Core via 'CustomUriFunctions' class, you must bind it to it's MethodInfo. + /// Bind the given function name to the given MethodInfo. + /// The binding helps to create an Expression out of the method. + /// You can bind a static method, a static extension method, and an instance method. + /// You should be careful about binding instance methods - the declaring type of the method i.e the instance type, + /// will be considered as the first argument of the function. /// - internal static class UriFunctionsBinder + /// The uri function name that appears in the OData request uri. + /// The MethodInfo to bind the given function name. + /// Function name argument is Null or empty + /// MethodInfo argument is Null + /// The given FunctionName is already binded to another MethodInfo. + public static void BindUriFunctionName(string functionName, MethodInfo methodInfo) { - private static Dictionary methodLiteralSignaturesToMethodInfo = new Dictionary(); - - private static object locker = new object(); - - /// - /// Bind the given function name to the given MethodInfo. - /// The binding helps to create an Expression out of the method. - /// You can bind a static method, a static extension method, and an instance method. - /// You should be careful about binding instance methods - the declaring type of the method i.e the instance type, - /// will be considered as the first argument of the function. - /// - /// The uri function name that appears in the OData request uri. - /// The MethodInfo to bind the given function name. - /// Function name argument is Null or empty - /// MethodInfo argument is Null - /// The given FunctionName is already binded to another MethodInfo. - public static void BindUriFunctionName(string functionName, MethodInfo methodInfo) + // Validation + if (String.IsNullOrEmpty(functionName)) { - // Validation - if (String.IsNullOrEmpty(functionName)) - { - throw Error.ArgumentNull("functionName"); - } - - if (methodInfo == null) - { - throw Error.ArgumentNull("methodInfo"); - } - - // Get literal description of the method - string methodLiteralSignature = GetMethodLiteralSignature(functionName, methodInfo); - - lock (locker) - { - if (methodLiteralSignaturesToMethodInfo.ContainsKey(methodLiteralSignature)) - { - throw new ODataException(String.Format(CultureInfo.InvariantCulture, - SRResources.UriFunctionClrBinderAlreadyBound, - methodLiteralSignature)); - } - - methodLiteralSignaturesToMethodInfo.Add(methodLiteralSignature, methodInfo); - } + throw Error.ArgumentNull("functionName"); } - /// - /// Unbind the given function name from the given MethodInfo. - /// - /// The uri function name to unbind. - /// The MethodInfo to unbind from the given function name. - /// Function name argument is Null or empty - /// MethodInfo argument is Null - /// 'True' if function has unbind. 'False' otherwise. - public static bool UnbindUriFunctionName(string functionName, MethodInfo methodInfo) + if (methodInfo == null) { - // Validation - if (String.IsNullOrEmpty(functionName)) - { - throw Error.ArgumentNull("functionName"); - } + throw Error.ArgumentNull("methodInfo"); + } - if (methodInfo == null) + // Get literal description of the method + string methodLiteralSignature = GetMethodLiteralSignature(functionName, methodInfo); + + lock (locker) + { + if (methodLiteralSignaturesToMethodInfo.ContainsKey(methodLiteralSignature)) { - throw Error.ArgumentNull("methodInfo"); + throw new ODataException(String.Format(CultureInfo.InvariantCulture, + SRResources.UriFunctionClrBinderAlreadyBound, + methodLiteralSignature)); } - // Get literal description of the method - string methodLiteralSignature = GetMethodLiteralSignature(functionName, methodInfo); + methodLiteralSignaturesToMethodInfo.Add(methodLiteralSignature, methodInfo); + } + } - lock (locker) - { - return methodLiteralSignaturesToMethodInfo.Remove(methodLiteralSignature); - } + /// + /// Unbind the given function name from the given MethodInfo. + /// + /// The uri function name to unbind. + /// The MethodInfo to unbind from the given function name. + /// Function name argument is Null or empty + /// MethodInfo argument is Null + /// 'True' if function has unbind. 'False' otherwise. + public static bool UnbindUriFunctionName(string functionName, MethodInfo methodInfo) + { + // Validation + if (String.IsNullOrEmpty(functionName)) + { + throw Error.ArgumentNull("functionName"); } - public static bool TryGetMethodInfo(string functionName, IEnumerable methodArgumentsType, - out MethodInfo methodInfo) + if (methodInfo == null) { - // Validation - if (String.IsNullOrEmpty(functionName)) - { - throw Error.ArgumentNull("functionName"); - } + throw Error.ArgumentNull("methodInfo"); + } - if (methodArgumentsType == null) - { - throw Error.ArgumentNull("methodArgumentsType"); - } + // Get literal description of the method + string methodLiteralSignature = GetMethodLiteralSignature(functionName, methodInfo); - // Get literal description of the method - string methodLiteralSignature = GetMethodLiteralSignature(functionName, methodArgumentsType); + lock (locker) + { + return methodLiteralSignaturesToMethodInfo.Remove(methodLiteralSignature); + } + } - lock (locker) - { - return methodLiteralSignaturesToMethodInfo.TryGetValue(methodLiteralSignature, out methodInfo); - } + public static bool TryGetMethodInfo(string functionName, IEnumerable methodArgumentsType, + out MethodInfo methodInfo) + { + // Validation + if (String.IsNullOrEmpty(functionName)) + { + throw Error.ArgumentNull("functionName"); } - /// - /// Get a string describing the given method. - /// - private static string GetMethodLiteralSignature(string methodName, MethodInfo methodInfo) + if (methodArgumentsType == null) { - // Get the arguments type of the given MethodInfo - IEnumerable methodArgumentsType = - methodInfo.GetParameters().Select(parameter => parameter.ParameterType); + throw Error.ArgumentNull("methodArgumentsType"); + } - // If method is not static - instance, the declaring type is the first argument. - if (!methodInfo.IsStatic) - { - methodArgumentsType = new Type[] { methodInfo.DeclaringType }.Concat(methodArgumentsType); - } + // Get literal description of the method + string methodLiteralSignature = GetMethodLiteralSignature(functionName, methodArgumentsType); - return GetMethodLiteralSignature(methodName, methodArgumentsType); + lock (locker) + { + return methodLiteralSignaturesToMethodInfo.TryGetValue(methodLiteralSignature, out methodInfo); } + } - /// - /// Creates a string describing the function signature. - /// 'methodName(argTypeName1,argTypeName2,argTypeName3..)' - /// - private static string GetMethodLiteralSignature(string methodName, IEnumerable methodArgumentsType) + /// + /// Get a string describing the given method. + /// + private static string GetMethodLiteralSignature(string methodName, MethodInfo methodInfo) + { + // Get the arguments type of the given MethodInfo + IEnumerable methodArgumentsType = + methodInfo.GetParameters().Select(parameter => parameter.ParameterType); + + // If method is not static - instance, the declaring type is the first argument. + if (!methodInfo.IsStatic) { - StringBuilder builder = new StringBuilder(); - string parameterSeparator = String.Empty; - builder.Append(methodName); - builder.Append('('); - foreach (Type type in methodArgumentsType) - { - builder.Append(parameterSeparator); - parameterSeparator = ", "; + methodArgumentsType = new Type[] { methodInfo.DeclaringType }.Concat(methodArgumentsType); + } - builder.Append(type.FullName); - } + return GetMethodLiteralSignature(methodName, methodArgumentsType); + } - builder.Append(')'); - return builder.ToString(); + /// + /// Creates a string describing the function signature. + /// 'methodName(argTypeName1,argTypeName2,argTypeName3..)' + /// + private static string GetMethodLiteralSignature(string methodName, IEnumerable methodArgumentsType) + { + StringBuilder builder = new StringBuilder(); + string parameterSeparator = String.Empty; + builder.Append(methodName); + builder.Append('('); + foreach (Type type in methodArgumentsType) + { + builder.Append(parameterSeparator); + parameterSeparator = ", "; + + builder.Append(type.FullName); } + + builder.Append(')'); + return builder.ToString(); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/HandleNullPropagationOption.cs b/src/Microsoft.AspNetCore.OData/Query/HandleNullPropagationOption.cs index 7da39a3c2..40698551f 100644 --- a/src/Microsoft.AspNetCore.OData/Query/HandleNullPropagationOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/HandleNullPropagationOption.cs @@ -5,28 +5,27 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This enum defines how to handle null propagation in queryable support. +/// +public enum HandleNullPropagationOption { /// - /// This enum defines how to handle null propagation in queryable support. + /// Determine how to handle null propagation based on the + /// query provider during query composition. This is the + /// default value used in /// - public enum HandleNullPropagationOption - { - /// - /// Determine how to handle null propagation based on the - /// query provider during query composition. This is the - /// default value used in - /// - Default = 0, + Default = 0, - /// - /// Handle null propagation during query composition. - /// - True = 1, + /// + /// Handle null propagation during query composition. + /// + True = 1, - /// - /// Do not handle null propagation during query composition. - /// - False = 2 - } + /// + /// Do not handle null propagation during query composition. + /// + False = 2 } diff --git a/src/Microsoft.AspNetCore.OData/Query/HandleNullPropagationOptionHelper.cs b/src/Microsoft.AspNetCore.OData/Query/HandleNullPropagationOptionHelper.cs index a1aa79b69..8f821e190 100644 --- a/src/Microsoft.AspNetCore.OData/Query/HandleNullPropagationOptionHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/HandleNullPropagationOptionHelper.cs @@ -8,63 +8,62 @@ using System.Diagnostics.Contracts; using System.Linq; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +internal static class HandleNullPropagationOptionHelper { - internal static class HandleNullPropagationOptionHelper - { - internal const string EntityFrameworkQueryProviderNamespace = "System.Data.Entity.Internal.Linq"; + internal const string EntityFrameworkQueryProviderNamespace = "System.Data.Entity.Internal.Linq"; - internal const string ObjectContextQueryProviderNamespaceEF5 = "System.Data.Objects.ELinq"; - internal const string ObjectContextQueryProviderNamespaceEF6 = "System.Data.Entity.Core.Objects.ELinq"; - internal const string ObjectContextQueryProviderNamespaceEFCore2 = "Microsoft.EntityFrameworkCore.Query.Internal"; + internal const string ObjectContextQueryProviderNamespaceEF5 = "System.Data.Objects.ELinq"; + internal const string ObjectContextQueryProviderNamespaceEF6 = "System.Data.Entity.Core.Objects.ELinq"; + internal const string ObjectContextQueryProviderNamespaceEFCore2 = "Microsoft.EntityFrameworkCore.Query.Internal"; - internal const string Linq2SqlQueryProviderNamespace = "System.Data.Linq"; - internal const string Linq2ObjectsQueryProviderNamespace = "System.Linq"; + internal const string Linq2SqlQueryProviderNamespace = "System.Data.Linq"; + internal const string Linq2ObjectsQueryProviderNamespace = "System.Linq"; - public static bool IsDefined(HandleNullPropagationOption value) - { - return value == HandleNullPropagationOption.Default || - value == HandleNullPropagationOption.True || - value == HandleNullPropagationOption.False; - } + public static bool IsDefined(HandleNullPropagationOption value) + { + return value == HandleNullPropagationOption.Default || + value == HandleNullPropagationOption.True || + value == HandleNullPropagationOption.False; + } - public static void Validate(HandleNullPropagationOption value, string parameterValue) + public static void Validate(HandleNullPropagationOption value, string parameterValue) + { + if (!IsDefined(value)) { - if (!IsDefined(value)) - { - throw Error.InvalidEnumArgument(parameterValue, (int)value, typeof(HandleNullPropagationOption)); - } + throw Error.InvalidEnumArgument(parameterValue, (int)value, typeof(HandleNullPropagationOption)); } + } - public static HandleNullPropagationOption GetDefaultHandleNullPropagationOption(IQueryable query) - { - Contract.Assert(query != null); - - HandleNullPropagationOption options; + public static HandleNullPropagationOption GetDefaultHandleNullPropagationOption(IQueryable query) + { + Contract.Assert(query != null); - string queryProviderNamespace = query.Provider.GetType().Namespace; - switch (queryProviderNamespace) - { - case EntityFrameworkQueryProviderNamespace: - case Linq2SqlQueryProviderNamespace: - case ObjectContextQueryProviderNamespaceEF5: - case ObjectContextQueryProviderNamespaceEF6: + HandleNullPropagationOption options; - // EF Core before 3.0 does a lot of client evaluations and has InMemory support which is the same as Linq2Objects - // We have to keep the null propagation for EF Core 3.0, otherwise, there's some issues for example: - // $expand=Collection, $select=Collection - case ObjectContextQueryProviderNamespaceEFCore2: - options = HandleNullPropagationOption.False; - break; + string queryProviderNamespace = query.Provider.GetType().Namespace; + switch (queryProviderNamespace) + { + case EntityFrameworkQueryProviderNamespace: + case Linq2SqlQueryProviderNamespace: + case ObjectContextQueryProviderNamespaceEF5: + case ObjectContextQueryProviderNamespaceEF6: - case Linq2ObjectsQueryProviderNamespace: + // EF Core before 3.0 does a lot of client evaluations and has InMemory support which is the same as Linq2Objects + // We have to keep the null propagation for EF Core 3.0, otherwise, there's some issues for example: + // $expand=Collection, $select=Collection + case ObjectContextQueryProviderNamespaceEFCore2: + options = HandleNullPropagationOption.False; + break; - default: - options = HandleNullPropagationOption.True; - break; - } + case Linq2ObjectsQueryProviderNamespace: - return options; + default: + options = HandleNullPropagationOption.True; + break; } + + return options; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/HttpRequestODataQueryExtensions.cs b/src/Microsoft.AspNetCore.OData/Query/HttpRequestODataQueryExtensions.cs index 5e997e50e..4c7ec9e0b 100644 --- a/src/Microsoft.AspNetCore.OData/Query/HttpRequestODataQueryExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/HttpRequestODataQueryExtensions.cs @@ -21,102 +21,101 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Extensions for ETag. +/// +public static class HttpRequestODataQueryExtensions { /// - /// Extensions for ETag. + /// Gets the OData from the given request. /// - public static class HttpRequestODataQueryExtensions + /// The request. + /// The entity tag header value. + /// The parsed . + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many ODataLib classes.")] + public static ETag GetETag(this HttpRequest request, EntityTagHeaderValue entityTagHeaderValue) { - /// - /// Gets the OData from the given request. - /// - /// The request. - /// The entity tag header value. - /// The parsed . - [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many ODataLib classes.")] - public static ETag GetETag(this HttpRequest request, EntityTagHeaderValue entityTagHeaderValue) + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); + } + + if (entityTagHeaderValue != null) { - if (request == null) + if (entityTagHeaderValue.Equals(EntityTagHeaderValue.Any)) { - throw Error.ArgumentNull(nameof(request)); + return new ETag { IsAny = true }; } - if (entityTagHeaderValue != null) + // get the etag handler, and parse the etag + IETagHandler etagHandler = request.GetRouteServices().GetRequiredService(); + IDictionary properties = etagHandler.ParseETag(entityTagHeaderValue) ?? new Dictionary(); + IList parsedETagValues = properties.Select(property => property.Value).ToList(); + + // get property names from request + ODataPath odataPath = request.ODataFeature().Path; + IEdmModel model = request.GetModel(); + IEdmNavigationSource source = odataPath.GetNavigationSource(); + if (model != null && source != null) { - if (entityTagHeaderValue.Equals(EntityTagHeaderValue.Any)) + IList concurrencyProperties = model.GetConcurrencyProperties(source).ToList(); + IList concurrencyPropertyNames = concurrencyProperties.OrderBy(c => c.Name).Select(c => c.Name).ToList(); + ETag etag = new ETag(); + + if (parsedETagValues.Count != concurrencyPropertyNames.Count) { - return new ETag { IsAny = true }; + etag.IsWellFormed = false; } - // get the etag handler, and parse the etag - IETagHandler etagHandler = request.GetRouteServices().GetRequiredService(); - IDictionary properties = etagHandler.ParseETag(entityTagHeaderValue) ?? new Dictionary(); - IList parsedETagValues = properties.Select(property => property.Value).ToList(); - - // get property names from request - ODataPath odataPath = request.ODataFeature().Path; - IEdmModel model = request.GetModel(); - IEdmNavigationSource source = odataPath.GetNavigationSource(); - if (model != null && source != null) + IEnumerable> nameValues = concurrencyPropertyNames.Zip( + parsedETagValues, + (name, value) => new KeyValuePair(name, value)); + foreach (var nameValue in nameValues) { - IList concurrencyProperties = model.GetConcurrencyProperties(source).ToList(); - IList concurrencyPropertyNames = concurrencyProperties.OrderBy(c => c.Name).Select(c => c.Name).ToList(); - ETag etag = new ETag(); + IEdmStructuralProperty property = concurrencyProperties.SingleOrDefault(e => e.Name == nameValue.Key); + Contract.Assert(property != null); - if (parsedETagValues.Count != concurrencyPropertyNames.Count) + Type clrType = model.GetClrType(property.Type); + Contract.Assert(clrType != null); + + if (nameValue.Value != null) { - etag.IsWellFormed = false; + Type valueType = nameValue.Value.GetType(); + etag[nameValue.Key] = valueType != clrType + ? Convert.ChangeType(nameValue.Value, clrType, CultureInfo.InvariantCulture) + : nameValue.Value; } - - IEnumerable> nameValues = concurrencyPropertyNames.Zip( - parsedETagValues, - (name, value) => new KeyValuePair(name, value)); - foreach (var nameValue in nameValues) + else { - IEdmStructuralProperty property = concurrencyProperties.SingleOrDefault(e => e.Name == nameValue.Key); - Contract.Assert(property != null); - - Type clrType = model.GetClrType(property.Type); - Contract.Assert(clrType != null); - - if (nameValue.Value != null) - { - Type valueType = nameValue.Value.GetType(); - etag[nameValue.Key] = valueType != clrType - ? Convert.ChangeType(nameValue.Value, clrType, CultureInfo.InvariantCulture) - : nameValue.Value; - } - else - { - etag[nameValue.Key] = nameValue.Value; - } + etag[nameValue.Key] = nameValue.Value; } - - return etag; } - } - return null; + return etag; + } } - /// - /// Gets the from the given request. - /// - /// The request. - /// The entity tag header value. - /// The parsed . - public static ETag GetETag(this HttpRequest request, EntityTagHeaderValue entityTagHeaderValue) - { - ETag etag = request.GetETag(entityTagHeaderValue); - return etag != null - ? new ETag - { - ConcurrencyProperties = etag.ConcurrencyProperties, - IsWellFormed = etag.IsWellFormed, - IsAny = etag.IsAny, - } - : null; - } + return null; + } + + /// + /// Gets the from the given request. + /// + /// The request. + /// The entity tag header value. + /// The parsed . + public static ETag GetETag(this HttpRequest request, EntityTagHeaderValue entityTagHeaderValue) + { + ETag etag = request.GetETag(entityTagHeaderValue); + return etag != null + ? new ETag + { + ConcurrencyProperties = etag.ConcurrencyProperties, + IsWellFormed = etag.IsWellFormed, + IsAny = etag.IsAny, + } + : null; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/ICountOptionCollection.cs b/src/Microsoft.AspNetCore.OData/Query/ICountOptionCollection.cs index 3d96f2f79..809546e13 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ICountOptionCollection.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ICountOptionCollection.cs @@ -7,16 +7,15 @@ using System.Collections; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Represents a collection that has total count. +/// +internal interface ICountOptionCollection : IEnumerable { /// - /// Represents a collection that has total count. + /// Gets a value representing the total count of the collection. /// - internal interface ICountOptionCollection : IEnumerable - { - /// - /// Gets a value representing the total count of the collection. - /// - long? TotalCount { get; } - } + long? TotalCount { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/IODataQueryRequestParser.cs b/src/Microsoft.AspNetCore.OData/Query/IODataQueryRequestParser.cs index 30209b757..e02ce04a0 100644 --- a/src/Microsoft.AspNetCore.OData/Query/IODataQueryRequestParser.cs +++ b/src/Microsoft.AspNetCore.OData/Query/IODataQueryRequestParser.cs @@ -8,28 +8,27 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Exposes the ability to read and parse the content of a +/// into a query options part of an OData URL. Query options may be passed +/// in the request body to a resource path ending in /$query. +/// +public interface IODataQueryRequestParser { /// - /// Exposes the ability to read and parse the content of a - /// into a query options part of an OData URL. Query options may be passed - /// in the request body to a resource path ending in /$query. + /// Determines whether this can parse the http request. /// - public interface IODataQueryRequestParser - { - /// - /// Determines whether this can parse the http request. - /// - /// The http request. - /// true if this can parse the http request; false otherwise. - bool CanParse(HttpRequest request); + /// The http request. + /// true if this can parse the http request; false otherwise. + bool CanParse(HttpRequest request); - /// - /// Reads and parses the content of a - /// into a query options part of an OData URL. - /// - /// A http request containing the query options. - /// A string representing the query options part of an OData URL. - Task ParseAsync(HttpRequest request); - } + /// + /// Reads and parses the content of a + /// into a query options part of an OData URL. + /// + /// A http request containing the query options. + /// A string representing the query options part of an OData URL. + Task ParseAsync(HttpRequest request); } diff --git a/src/Microsoft.AspNetCore.OData/Query/ModelBoundQuerySettings.cs b/src/Microsoft.AspNetCore.OData/Query/ModelBoundQuerySettings.cs index 03794ec88..5d403424f 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ModelBoundQuerySettings.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ModelBoundQuerySettings.cs @@ -9,137 +9,136 @@ using Microsoft.OData.ModelBuilder; using Microsoft.OData.ModelBuilder.Config; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This class describes the model bound settings to use during query composition. +/// +internal static class ModelBoundQuerySettingsExtensions { /// - /// This class describes the model bound settings to use during query composition. + /// Copy the s of navigation properties. /// - internal static class ModelBoundQuerySettingsExtensions + internal static void CopyExpandConfigurations(this ModelBoundQuerySettings settings, Dictionary expandConfigurations) { - /// - /// Copy the s of navigation properties. - /// - internal static void CopyExpandConfigurations(this ModelBoundQuerySettings settings, Dictionary expandConfigurations) + settings.ExpandConfigurations.Clear(); + foreach (var expandConfiguration in expandConfigurations) { - settings.ExpandConfigurations.Clear(); - foreach (var expandConfiguration in expandConfigurations) - { - settings.ExpandConfigurations.Add(expandConfiguration.Key, expandConfiguration.Value); - } + settings.ExpandConfigurations.Add(expandConfiguration.Key, expandConfiguration.Value); } + } - /// - /// Copy the $orderby configuration of properties. - /// - internal static void CopyOrderByConfigurations(this ModelBoundQuerySettings settings, Dictionary orderByConfigurations) + /// + /// Copy the $orderby configuration of properties. + /// + internal static void CopyOrderByConfigurations(this ModelBoundQuerySettings settings, Dictionary orderByConfigurations) + { + settings.OrderByConfigurations.Clear(); + foreach (var orderByConfiguration in orderByConfigurations) { - settings.OrderByConfigurations.Clear(); - foreach (var orderByConfiguration in orderByConfigurations) - { - settings.OrderByConfigurations.Add(orderByConfiguration.Key, orderByConfiguration.Value); - } + settings.OrderByConfigurations.Add(orderByConfiguration.Key, orderByConfiguration.Value); } + } - /// - /// Copy the $select configuration of properties. - /// - internal static void CopySelectConfigurations(this ModelBoundQuerySettings settings, Dictionary selectConfigurations) + /// + /// Copy the $select configuration of properties. + /// + internal static void CopySelectConfigurations(this ModelBoundQuerySettings settings, Dictionary selectConfigurations) + { + settings.SelectConfigurations.Clear(); + foreach (var selectConfiguration in selectConfigurations) { - settings.SelectConfigurations.Clear(); - foreach (var selectConfiguration in selectConfigurations) - { - settings.SelectConfigurations.Add(selectConfiguration.Key, selectConfiguration.Value); - } + settings.SelectConfigurations.Add(selectConfiguration.Key, selectConfiguration.Value); } + } - /// - /// Copy the $filter configuration of properties. - /// - internal static void CopyFilterConfigurations(this ModelBoundQuerySettings settings, Dictionary filterConfigurations) + /// + /// Copy the $filter configuration of properties. + /// + internal static void CopyFilterConfigurations(this ModelBoundQuerySettings settings, Dictionary filterConfigurations) + { + settings.FilterConfigurations.Clear(); + foreach (var filterConfiguration in filterConfigurations) { - settings.FilterConfigurations.Clear(); - foreach (var filterConfiguration in filterConfigurations) - { - settings.FilterConfigurations.Add(filterConfiguration.Key, filterConfiguration.Value); - } + settings.FilterConfigurations.Add(filterConfiguration.Key, filterConfiguration.Value); } + } - internal static bool IsAutomaticExpand(this ModelBoundQuerySettings settings, string propertyName) - { - ExpandConfiguration expandConfiguration; - if (settings.ExpandConfigurations.TryGetValue(propertyName, out expandConfiguration)) - { - return expandConfiguration.ExpandType == SelectExpandType.Automatic; - } - else - { - return settings.DefaultExpandType.HasValue && settings.DefaultExpandType == SelectExpandType.Automatic; - } + internal static bool IsAutomaticExpand(this ModelBoundQuerySettings settings, string propertyName) + { + ExpandConfiguration expandConfiguration; + if (settings.ExpandConfigurations.TryGetValue(propertyName, out expandConfiguration)) + { + return expandConfiguration.ExpandType == SelectExpandType.Automatic; } + else + { + return settings.DefaultExpandType.HasValue && settings.DefaultExpandType == SelectExpandType.Automatic; + } + } - internal static bool IsAutomaticSelect(this ModelBoundQuerySettings settings, string propertyName) - { - SelectExpandType selectExpandType; - if (settings.SelectConfigurations.TryGetValue(propertyName, out selectExpandType)) - { - return selectExpandType == SelectExpandType.Automatic; - } - else - { - return settings.DefaultSelectType.HasValue && settings.DefaultSelectType == SelectExpandType.Automatic; - } + internal static bool IsAutomaticSelect(this ModelBoundQuerySettings settings, string propertyName) + { + SelectExpandType selectExpandType; + if (settings.SelectConfigurations.TryGetValue(propertyName, out selectExpandType)) + { + return selectExpandType == SelectExpandType.Automatic; } + else + { + return settings.DefaultSelectType.HasValue && settings.DefaultSelectType == SelectExpandType.Automatic; + } + } - internal static bool Expandable(this ModelBoundQuerySettings settings, string propertyName) - { - ExpandConfiguration expandConfiguration; - if (settings.ExpandConfigurations.TryGetValue(propertyName, out expandConfiguration)) - { - return expandConfiguration.ExpandType != SelectExpandType.Disabled; - } - else - { - return settings.DefaultExpandType.HasValue && settings.DefaultExpandType != SelectExpandType.Disabled; - } + internal static bool Expandable(this ModelBoundQuerySettings settings, string propertyName) + { + ExpandConfiguration expandConfiguration; + if (settings.ExpandConfigurations.TryGetValue(propertyName, out expandConfiguration)) + { + return expandConfiguration.ExpandType != SelectExpandType.Disabled; + } + else + { + return settings.DefaultExpandType.HasValue && settings.DefaultExpandType != SelectExpandType.Disabled; } + } - internal static bool Selectable(this ModelBoundQuerySettings settings, string propertyName) - { - SelectExpandType selectExpandType; - if (settings.SelectConfigurations.TryGetValue(propertyName, out selectExpandType)) - { - return selectExpandType != SelectExpandType.Disabled; - } - else - { - return settings.DefaultSelectType.HasValue && settings.DefaultSelectType != SelectExpandType.Disabled; - } + internal static bool Selectable(this ModelBoundQuerySettings settings, string propertyName) + { + SelectExpandType selectExpandType; + if (settings.SelectConfigurations.TryGetValue(propertyName, out selectExpandType)) + { + return selectExpandType != SelectExpandType.Disabled; + } + else + { + return settings.DefaultSelectType.HasValue && settings.DefaultSelectType != SelectExpandType.Disabled; } + } - internal static bool Sortable(this ModelBoundQuerySettings settings, string propertyName) - { - bool enable; - if (settings.OrderByConfigurations.TryGetValue(propertyName, out enable)) - { - return enable; - } - else - { - return settings.DefaultEnableOrderBy == true; - } + internal static bool Sortable(this ModelBoundQuerySettings settings, string propertyName) + { + bool enable; + if (settings.OrderByConfigurations.TryGetValue(propertyName, out enable)) + { + return enable; + } + else + { + return settings.DefaultEnableOrderBy == true; } + } - internal static bool Filterable(this ModelBoundQuerySettings settings, string propertyName) - { - bool enable; - if (settings.FilterConfigurations.TryGetValue(propertyName, out enable)) - { - return enable; - } - else - { - return settings.DefaultEnableFilter == true; - } + internal static bool Filterable(this ModelBoundQuerySettings settings, string propertyName) + { + bool enable; + if (settings.FilterConfigurations.TryGetValue(propertyName, out enable)) + { + return enable; + } + else + { + return settings.DefaultEnableFilter == true; } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByClauseNode.cs b/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByClauseNode.cs index 1407160ef..b97948958 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByClauseNode.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByClauseNode.cs @@ -7,31 +7,30 @@ using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Represents the order by expression in the $orderby clause. +/// Use this to represent other $orderby except 'Property,OpenProperty,$count, $it' orderBy expression. +/// +/// +/// Again, in the next major release, we don't need this class. +/// Track on it at: https://github.com/OData/AspNetCoreOData/issues/1153 +/// +public class OrderByClauseNode : OrderByNode { /// - /// Represents the order by expression in the $orderby clause. - /// Use this to represent other $orderby except 'Property,OpenProperty,$count, $it' orderBy expression. + /// Instantiates a new instance of class. /// - /// - /// Again, in the next major release, we don't need this class. - /// Track on it at: https://github.com/OData/AspNetCoreOData/issues/1153 - /// - public class OrderByClauseNode : OrderByNode + /// The order by clause. + public OrderByClauseNode(OrderByClause orderByClause) + : base(orderByClause) { - /// - /// Instantiates a new instance of class. - /// - /// The order by clause. - public OrderByClauseNode(OrderByClause orderByClause) - : base(orderByClause) - { - OrderByClause = orderByClause; - } - - /// - /// Gets the of this node. - /// - public OrderByClause OrderByClause { get; } + OrderByClause = orderByClause; } + + /// + /// Gets the of this node. + /// + public OrderByClause OrderByClause { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByCountNode.cs b/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByCountNode.cs index 2927040b2..ed6c2cf64 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByCountNode.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByCountNode.cs @@ -9,30 +9,29 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Represents an order by expression. +/// +public class OrderByCountNode : OrderByNode { /// - /// Represents an order by expression. + /// Initializes a new instance of the class. /// - public class OrderByCountNode : OrderByNode + /// The orderby clause representing property access. + public OrderByCountNode(OrderByClause orderByClause) + : base(orderByClause) { - /// - /// Initializes a new instance of the class. - /// - /// The orderby clause representing property access. - public OrderByCountNode(OrderByClause orderByClause) - : base(orderByClause) + OrderByClause = orderByClause; + if (!(orderByClause.Expression is CountNode)) { - OrderByClause = orderByClause; - if (!(orderByClause.Expression is CountNode)) - { - throw new ODataException(string.Format(SRResources.OrderByClauseInvalid, orderByClause.Expression.Kind, QueryNodeKind.Count)); - } + throw new ODataException(string.Format(SRResources.OrderByClauseInvalid, orderByClause.Expression.Kind, QueryNodeKind.Count)); } - - /// - /// Gets the of this node. - /// - public OrderByClause OrderByClause { get; } } + + /// + /// Gets the of this node. + /// + public OrderByClause OrderByClause { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByItNode.cs b/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByItNode.cs index 4fb83e538..1cf0c0e23 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByItNode.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByItNode.cs @@ -8,53 +8,52 @@ using Microsoft.OData; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Represents the order by expression '$it' in the $orderby clause. +/// +public class OrderByItNode : OrderByNode { /// - /// Represents the order by expression '$it' in the $orderby clause. + /// Instantiates a new instance of class. + /// + /// The for this node. + public OrderByItNode(OrderByDirection direction) + : base(direction) + { + Name = "$it"; + } + + /// + /// Instantiates a new instance of class. /// - public class OrderByItNode : OrderByNode + /// The orderby clause. + public OrderByItNode(OrderByClause clause) + : base(clause) { - /// - /// Instantiates a new instance of class. - /// - /// The for this node. - public OrderByItNode(OrderByDirection direction) - : base(direction) + if (clause == null) { - Name = "$it"; + throw Error.ArgumentNull(nameof(clause)); } - /// - /// Instantiates a new instance of class. - /// - /// The orderby clause. - public OrderByItNode(OrderByClause clause) - : base(clause) + if (clause.Expression is NonResourceRangeVariableReferenceNode nonResourceVarNode) { - if (clause == null) - { - throw Error.ArgumentNull(nameof(clause)); - } - - if (clause.Expression is NonResourceRangeVariableReferenceNode nonResourceVarNode) - { - Name = nonResourceVarNode.Name; - } - else if (clause.Expression is ResourceRangeVariableReferenceNode resourceVarNode) - { - Name = resourceVarNode.Name; - } - else - { - throw new ODataException(string.Format(SRResources.OrderByClauseInvalid, clause.Expression.Kind, - "NonResourceRangeVariableReferenceNode or ResourceRangeVariableReferenceNode")); - } + Name = nonResourceVarNode.Name; + } + else if (clause.Expression is ResourceRangeVariableReferenceNode resourceVarNode) + { + Name = resourceVarNode.Name; + } + else + { + throw new ODataException(string.Format(SRResources.OrderByClauseInvalid, clause.Expression.Kind, + "NonResourceRangeVariableReferenceNode or ResourceRangeVariableReferenceNode")); } - - /// - /// Gets the range variable name - /// - public string Name { get; } } + + /// + /// Gets the range variable name + /// + public string Name { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByNode.cs b/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByNode.cs index 7ca6a1730..f3c08f752 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByNode.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByNode.cs @@ -10,130 +10,129 @@ using System.Globalization; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Represents a single order by expression in the $orderby clause. +/// +/// +/// Why do we need this class and its derived type? only fetch the PropertyPath? In the next major release, we can consider to remove all of these. +/// Track on it at: https://github.com/OData/AspNetCoreOData/issues/1153 +/// +public abstract class OrderByNode { /// - /// Represents a single order by expression in the $orderby clause. + /// Initializes a new instance of the class. /// - /// - /// Why do we need this class and its derived type? only fetch the PropertyPath? In the next major release, we can consider to remove all of these. - /// Track on it at: https://github.com/OData/AspNetCoreOData/issues/1153 - /// - public abstract class OrderByNode + /// The direction of the sort order. + protected OrderByNode(OrderByDirection direction) { - /// - /// Initializes a new instance of the class. - /// - /// The direction of the sort order. - protected OrderByNode(OrderByDirection direction) - { - Direction = direction; - PropertyPath = String.Empty; - } + Direction = direction; + PropertyPath = String.Empty; + } - /// - /// Initializes a new instance of the class. - /// - /// The clause of the sort order. - protected OrderByNode(OrderByClause orderByClause) + /// + /// Initializes a new instance of the class. + /// + /// The clause of the sort order. + protected OrderByNode(OrderByClause orderByClause) + { + if (orderByClause == null) { - if (orderByClause == null) - { - throw Error.ArgumentNull(nameof(orderByClause)); - } - - Direction = orderByClause.Direction; - PropertyPath = RestorePropertyPath(orderByClause.Expression); + throw Error.ArgumentNull(nameof(orderByClause)); } - /// - /// Gets the for the current node. - /// - public OrderByDirection Direction { get; internal set; } + Direction = orderByClause.Direction; + PropertyPath = RestorePropertyPath(orderByClause.Expression); + } - internal string PropertyPath { get; set; } + /// + /// Gets the for the current node. + /// + public OrderByDirection Direction { get; internal set; } + + internal string PropertyPath { get; set; } - /// - /// Creates a list of instances from a linked list of instances. - /// - /// The head of the linked list. - /// The list of new instances. - public static IList CreateCollection(OrderByClause orderByClause) + /// + /// Creates a list of instances from a linked list of instances. + /// + /// The head of the linked list. + /// The list of new instances. + public static IList CreateCollection(OrderByClause orderByClause) + { + List result = new List(); + for (OrderByClause clause = orderByClause; clause != null; clause = clause.ThenBy) { - List result = new List(); - for (OrderByClause clause = orderByClause; clause != null; clause = clause.ThenBy) + if (clause.Expression is CountNode) { - if (clause.Expression is CountNode) - { - result.Add(new OrderByCountNode(clause)); - } - else if (clause.Expression is NonResourceRangeVariableReferenceNode || - clause.Expression is ResourceRangeVariableReferenceNode) - { - result.Add(new OrderByItNode(clause)); - } - else if (clause.Expression is SingleValueOpenPropertyAccessNode) - { - result.Add(new OrderByOpenPropertyNode(clause)); - } - else if(clause.Expression is SingleValuePropertyAccessNode) - { - result.Add(new OrderByPropertyNode(clause)); - } - else - { - // For other, let's create a wrapper. In next major release, we don't need this wrapper. - result.Add(new OrderByClauseNode(clause)); - } + result.Add(new OrderByCountNode(clause)); } - - return result; - } - - internal static string RestorePropertyPath(SingleValueNode expression) - { - if (expression == null) + else if (clause.Expression is NonResourceRangeVariableReferenceNode || + clause.Expression is ResourceRangeVariableReferenceNode) { - return string.Empty; + result.Add(new OrderByItNode(clause)); } - - string propertyName = string.Empty; - SingleValueNode source = null; - - var accessNode = expression as SingleValuePropertyAccessNode; - if (accessNode != null) + else if (clause.Expression is SingleValueOpenPropertyAccessNode) { - propertyName = accessNode.Property.Name; - source = accessNode.Source; + result.Add(new OrderByOpenPropertyNode(clause)); + } + else if(clause.Expression is SingleValuePropertyAccessNode) + { + result.Add(new OrderByPropertyNode(clause)); } else { - var complexNode = expression as SingleComplexNode; - if (complexNode != null) - { - propertyName = complexNode.Property.Name; - source = complexNode.Source; - } - else - { - var navNode = expression as SingleNavigationNode; - if (navNode != null) - { - propertyName = navNode.NavigationProperty.Name; - source = navNode.Source; - } - } + // For other, let's create a wrapper. In next major release, we don't need this wrapper. + result.Add(new OrderByClauseNode(clause)); } + } + + return result; + } + + internal static string RestorePropertyPath(SingleValueNode expression) + { + if (expression == null) + { + return string.Empty; + } + + string propertyName = string.Empty; + SingleValueNode source = null; - var parentPath = RestorePropertyPath(source); - if (string.IsNullOrEmpty(parentPath)) + var accessNode = expression as SingleValuePropertyAccessNode; + if (accessNode != null) + { + propertyName = accessNode.Property.Name; + source = accessNode.Source; + } + else + { + var complexNode = expression as SingleComplexNode; + if (complexNode != null) { - return propertyName; + propertyName = complexNode.Property.Name; + source = complexNode.Source; } else { - return string.Format(CultureInfo.CurrentCulture, "{0}/{1}", parentPath, propertyName); + var navNode = expression as SingleNavigationNode; + if (navNode != null) + { + propertyName = navNode.NavigationProperty.Name; + source = navNode.Source; + } } } + + var parentPath = RestorePropertyPath(source); + if (string.IsNullOrEmpty(parentPath)) + { + return propertyName; + } + else + { + return string.Format(CultureInfo.CurrentCulture, "{0}/{1}", parentPath, propertyName); + } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByOpenPropertyNode.cs b/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByOpenPropertyNode.cs index a2fec3ba4..8ff39e461 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByOpenPropertyNode.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByOpenPropertyNode.cs @@ -8,39 +8,38 @@ using Microsoft.OData; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Represents ordering on a dynamic property +/// +public class OrderByOpenPropertyNode : OrderByNode { /// - /// Represents ordering on a dynamic property + /// Initializes a new instance of the class. /// - public class OrderByOpenPropertyNode : OrderByNode + /// The order by clause for this open property. + public OrderByOpenPropertyNode(OrderByClause orderByClause) + : base(orderByClause) { - /// - /// Initializes a new instance of the class. - /// - /// The order by clause for this open property. - public OrderByOpenPropertyNode(OrderByClause orderByClause) - : base(orderByClause) - { - OrderByClause = orderByClause ?? throw Error.ArgumentNull(nameof(orderByClause)); - - var openPropertyExpression = orderByClause.Expression as SingleValueOpenPropertyAccessNode; - if (openPropertyExpression == null) - { - throw new ODataException(string.Format(SRResources.OrderByClauseInvalid, orderByClause.Expression.Kind, QueryNodeKind.SingleValueOpenPropertyAccess)); - } + OrderByClause = orderByClause ?? throw Error.ArgumentNull(nameof(orderByClause)); - PropertyName = openPropertyExpression.Name; + var openPropertyExpression = orderByClause.Expression as SingleValueOpenPropertyAccessNode; + if (openPropertyExpression == null) + { + throw new ODataException(string.Format(SRResources.OrderByClauseInvalid, orderByClause.Expression.Kind, QueryNodeKind.SingleValueOpenPropertyAccess)); } - /// - /// The order by clause - /// - public OrderByClause OrderByClause { get; } - - /// - /// The name of the dynamic property - /// - public string PropertyName { get; } + PropertyName = openPropertyExpression.Name; } + + /// + /// The order by clause + /// + public OrderByClause OrderByClause { get; } + + /// + /// The name of the dynamic property + /// + public string PropertyName { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByPropertyNode.cs b/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByPropertyNode.cs index 14038a7c2..c50bfada4 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByPropertyNode.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Nodes/OrderByPropertyNode.cs @@ -9,63 +9,62 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Represents an order by expression. +/// +public class OrderByPropertyNode : OrderByNode { /// - /// Represents an order by expression. + /// Initializes a new instance of the class. /// - public class OrderByPropertyNode : OrderByNode + /// The orderby clause representing property access. + public OrderByPropertyNode(OrderByClause orderByClause) + : base(orderByClause) { - /// - /// Initializes a new instance of the class. - /// - /// The orderby clause representing property access. - public OrderByPropertyNode(OrderByClause orderByClause) - : base(orderByClause) + if (orderByClause == null) { - if (orderByClause == null) - { - throw Error.ArgumentNull(nameof(orderByClause)); - } + throw Error.ArgumentNull(nameof(orderByClause)); + } - OrderByClause = orderByClause; - Direction = orderByClause.Direction; + OrderByClause = orderByClause; + Direction = orderByClause.Direction; - SingleValuePropertyAccessNode propertyExpression = orderByClause.Expression as SingleValuePropertyAccessNode; - if (propertyExpression == null) - { - throw new ODataException(string.Format(SRResources.OrderByClauseInvalid, orderByClause.Expression.Kind, QueryNodeKind.SingleValuePropertyAccess)); - } - else - { - Property = propertyExpression.Property; - } + SingleValuePropertyAccessNode propertyExpression = orderByClause.Expression as SingleValuePropertyAccessNode; + if (propertyExpression == null) + { + throw new ODataException(string.Format(SRResources.OrderByClauseInvalid, orderByClause.Expression.Kind, QueryNodeKind.SingleValuePropertyAccess)); } - - /// - /// Initializes a new instance of the class. - /// - /// The for this node. - /// The for this node. - public OrderByPropertyNode(IEdmProperty property, OrderByDirection direction) - : base(direction) + else { - if (property == null) - { - throw Error.ArgumentNull(nameof(property)); - } - - Property = property; + Property = propertyExpression.Property; } + } - /// - /// Gets the of this node. - /// - public OrderByClause OrderByClause { get; private set; } + /// + /// Initializes a new instance of the class. + /// + /// The for this node. + /// The for this node. + public OrderByPropertyNode(IEdmProperty property, OrderByDirection direction) + : base(direction) + { + if (property == null) + { + throw Error.ArgumentNull(nameof(property)); + } - /// - /// Gets the for the current node. - /// - public IEdmProperty Property { get; private set; } + Property = property; } + + /// + /// Gets the of this node. + /// + public OrderByClause OrderByClause { get; private set; } + + /// + /// Gets the for the current node. + /// + public IEdmProperty Property { get; private set; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/ODataQueryContext.cs b/src/Microsoft.AspNetCore.OData/Query/ODataQueryContext.cs index 6a77f055b..af7e82488 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ODataQueryContext.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ODataQueryContext.cs @@ -19,205 +19,204 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This defines some context information used to perform query composition. +/// +public class ODataQueryContext { + internal static readonly ODataUriResolver DefaultCaseInsensitiveResolver = new ODataUriResolver { EnableCaseInsensitive = true }; + + private DefaultQueryConfigurations _defaultQueryConfigurations; + /// - /// This defines some context information used to perform query composition. + /// Constructs an instance of with , element CLR type, + /// and . /// - public class ODataQueryContext + /// The EdmModel that includes the corresponding to + /// the given . + /// The CLR type of the element of the collection being queried. + /// The parsed . + /// + /// This is a public constructor used for stand-alone scenario; in this case, the services + /// container may not be present. + /// + public ODataQueryContext(IEdmModel model, Type elementClrType, ODataPath path) { - internal static readonly ODataUriResolver DefaultCaseInsensitiveResolver = new ODataUriResolver { EnableCaseInsensitive = true }; - - private DefaultQueryConfigurations _defaultQueryConfigurations; - - /// - /// Constructs an instance of with , element CLR type, - /// and . - /// - /// The EdmModel that includes the corresponding to - /// the given . - /// The CLR type of the element of the collection being queried. - /// The parsed . - /// - /// This is a public constructor used for stand-alone scenario; in this case, the services - /// container may not be present. - /// - public ODataQueryContext(IEdmModel model, Type elementClrType, ODataPath path) + if (model == null) { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } - - if (elementClrType == null) - { - throw Error.ArgumentNull(nameof(elementClrType)); - } - - ElementType = model.GetEdmTypeReference(elementClrType)?.Definition; - - if (ElementType == null) - { - throw Error.Argument(nameof(elementClrType), SRResources.ClrTypeNotInModel, elementClrType.FullName); - } - - ElementClrType = elementClrType; - Model = model; - Path = path; - NavigationSource = GetNavigationSource(Model, ElementType, path); - GetPathContext(); + throw Error.ArgumentNull(nameof(model)); } - /// - /// Constructs an instance of with , element EDM type, - /// and . - /// - /// The EDM model the given EDM type belongs to. - /// The EDM type of the element of the collection being queried. - /// The parsed . - public ODataQueryContext(IEdmModel model, IEdmType elementType, ODataPath path) + if (elementClrType == null) { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } + throw Error.ArgumentNull(nameof(elementClrType)); + } - if (elementType == null) - { - throw Error.ArgumentNull(nameof(elementType)); - } + ElementType = model.GetEdmTypeReference(elementClrType)?.Definition; - Model = model; - ElementType = elementType; - Path = path; - NavigationSource = GetNavigationSource(Model, ElementType, path); - GetPathContext(); + if (ElementType == null) + { + throw Error.Argument(nameof(elementClrType), SRResources.ClrTypeNotInModel, elementClrType.FullName); } - internal ODataQueryContext(IEdmModel model, Type elementClrType) - : this(model, elementClrType, path: null) + ElementClrType = elementClrType; + Model = model; + Path = path; + NavigationSource = GetNavigationSource(Model, ElementType, path); + GetPathContext(); + } + + /// + /// Constructs an instance of with , element EDM type, + /// and . + /// + /// The EDM model the given EDM type belongs to. + /// The EDM type of the element of the collection being queried. + /// The parsed . + public ODataQueryContext(IEdmModel model, IEdmType elementType, ODataPath path) + { + if (model == null) { + throw Error.ArgumentNull(nameof(model)); } - internal ODataQueryContext(IEdmModel model, IEdmType elementType) - : this(model, elementType, path: null) + if (elementType == null) { + throw Error.ArgumentNull(nameof(elementType)); } - internal ODataQueryContext() - { } + Model = model; + ElementType = elementType; + Path = path; + NavigationSource = GetNavigationSource(Model, ElementType, path); + GetPathContext(); + } - /// - /// Gets the given . - /// - public DefaultQueryConfigurations DefaultQueryConfigurations + internal ODataQueryContext(IEdmModel model, Type elementClrType) + : this(model, elementClrType, path: null) + { + } + + internal ODataQueryContext(IEdmModel model, IEdmType elementType) + : this(model, elementType, path: null) + { + } + + internal ODataQueryContext() + { } + + /// + /// Gets the given . + /// + public DefaultQueryConfigurations DefaultQueryConfigurations + { + get { - get + if (_defaultQueryConfigurations == null) { - if (_defaultQueryConfigurations == null) - { - _defaultQueryConfigurations = RequestContainer == null - ? GetDefaultQuerySettings() - : RequestContainer.GetRequiredService(); - } - - return _defaultQueryConfigurations; + _defaultQueryConfigurations = RequestContainer == null + ? GetDefaultQuerySettings() + : RequestContainer.GetRequiredService(); } + + return _defaultQueryConfigurations; } + } + + /// + /// Gets the given that contains the EntitySet. + /// + public IEdmModel Model { get; internal set; } - /// - /// Gets the given that contains the EntitySet. - /// - public IEdmModel Model { get; internal set; } + /// + /// Gets the of the element. + /// + public IEdmType ElementType { get; private set; } - /// - /// Gets the of the element. - /// - public IEdmType ElementType { get; private set; } + /// + /// Gets the that contains the element. + /// + public IEdmNavigationSource NavigationSource { get; private set; } - /// - /// Gets the that contains the element. - /// - public IEdmNavigationSource NavigationSource { get; private set; } + /// + /// Gets the CLR type of the element. + /// + public Type ElementClrType { get; internal set; } - /// - /// Gets the CLR type of the element. - /// - public Type ElementClrType { get; internal set; } + /// + /// Gets the . + /// + public ODataPath Path { get; private set; } - /// - /// Gets the . - /// - public ODataPath Path { get; private set; } + /// + /// Gets the request container. + /// + /// + /// The services container may not be present. See the constructor in this file for + /// use in stand-alone scenarios. + /// + public IServiceProvider RequestContainer { get; internal set; } - /// - /// Gets the request container. - /// - /// - /// The services container may not be present. See the constructor in this file for - /// use in stand-alone scenarios. - /// - public IServiceProvider RequestContainer { get; internal set; } + internal HttpRequest Request { get; set; } - internal HttpRequest Request { get; set; } + internal IEdmProperty TargetProperty { get; set; } - internal IEdmProperty TargetProperty { get; set; } + internal IEdmStructuredType TargetStructuredType { get; set; } - internal IEdmStructuredType TargetStructuredType { get; set; } + internal string TargetName { get; set; } - internal string TargetName { get; set; } + internal ODataValidationSettings ValidationSettings { get; set; } - internal ODataValidationSettings ValidationSettings { get; set; } + private static IEdmNavigationSource GetNavigationSource(IEdmModel model, IEdmType elementType, ODataPath odataPath) + { + Contract.Assert(model != null); + Contract.Assert(elementType != null); - private static IEdmNavigationSource GetNavigationSource(IEdmModel model, IEdmType elementType, ODataPath odataPath) + IEdmNavigationSource navigationSource = (odataPath != null) ? odataPath.GetNavigationSource() : null; + if (navigationSource != null) { - Contract.Assert(model != null); - Contract.Assert(elementType != null); + return navigationSource; + } - IEdmNavigationSource navigationSource = (odataPath != null) ? odataPath.GetNavigationSource() : null; - if (navigationSource != null) - { - return navigationSource; - } + IEdmEntityContainer entityContainer = model.EntityContainer; + if (entityContainer == null) + { + return null; + } - IEdmEntityContainer entityContainer = model.EntityContainer; - if (entityContainer == null) - { - return null; - } + List matchedNavigationSources = + entityContainer.EntitySets().Where(e => e.EntityType == elementType).ToList(); - List matchedNavigationSources = - entityContainer.EntitySets().Where(e => e.EntityType == elementType).ToList(); + return (matchedNavigationSources.Count != 1) ? null : matchedNavigationSources[0]; + } - return (matchedNavigationSources.Count != 1) ? null : matchedNavigationSources[0]; + private void GetPathContext() + { + if (Path != null) + { + (TargetProperty, TargetStructuredType, TargetName) = Path.GetPropertyAndStructuredTypeFromPath(); } - - private void GetPathContext() + else { - if (Path != null) - { - (TargetProperty, TargetStructuredType, TargetName) = Path.GetPropertyAndStructuredTypeFromPath(); - } - else - { - TargetStructuredType = ElementType as IEdmStructuredType; - } + TargetStructuredType = ElementType as IEdmStructuredType; } + } - private DefaultQueryConfigurations GetDefaultQuerySettings() + private DefaultQueryConfigurations GetDefaultQuerySettings() + { + if (Request is null) { - if (Request is null) - { - return new DefaultQueryConfigurations(); - } - - IOptions odataOptions = Request.HttpContext?.RequestServices?.GetService>(); - if (odataOptions is null || odataOptions.Value is null) - { - return new DefaultQueryConfigurations(); - } + return new DefaultQueryConfigurations(); + } - return odataOptions.Value.QueryConfigurations; + IOptions odataOptions = Request.HttpContext?.RequestServices?.GetService>(); + if (odataOptions is null || odataOptions.Value is null) + { + return new DefaultQueryConfigurations(); } + + return odataOptions.Value.QueryConfigurations; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/ODataQueryContextExtensions.cs b/src/Microsoft.AspNetCore.OData/Query/ODataQueryContextExtensions.cs index b6cceb486..256acedf7 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ODataQueryContextExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ODataQueryContextExtensions.cs @@ -12,229 +12,228 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +internal static class ODataQueryContextExtensions { - internal static class ODataQueryContextExtensions + public static ODataQuerySettings GetODataQuerySettings(this ODataQueryContext context) { - public static ODataQuerySettings GetODataQuerySettings(this ODataQueryContext context) + ODataQuerySettings returnSettings = new ODataQuerySettings(); + ODataQuerySettings settings = context?.RequestContainer?.GetRequiredService(); + if (settings != null) { - ODataQuerySettings returnSettings = new ODataQuerySettings(); - ODataQuerySettings settings = context?.RequestContainer?.GetRequiredService(); - if (settings != null) - { - returnSettings.CopyFrom(settings); - } - - return returnSettings; + returnSettings.CopyFrom(settings); } - public static ODataQuerySettings UpdateQuerySettings(this ODataQueryContext context, ODataQuerySettings querySettings, IQueryable query) + return returnSettings; + } + + public static ODataQuerySettings UpdateQuerySettings(this ODataQueryContext context, ODataQuerySettings querySettings, IQueryable query) + { + ODataQuerySettings updatedSettings = new ODataQuerySettings(); + ODataQuerySettings settings = context?.RequestContainer?.GetRequiredService(); + if (settings != null) { - ODataQuerySettings updatedSettings = new ODataQuerySettings(); - ODataQuerySettings settings = context?.RequestContainer?.GetRequiredService(); - if (settings != null) - { - updatedSettings.CopyFrom(settings); - } - - updatedSettings.CopyFrom(querySettings); - - if (updatedSettings.HandleNullPropagation == HandleNullPropagationOption.Default) - { - updatedSettings.HandleNullPropagation = query != null - ? HandleNullPropagationOptionHelper.GetDefaultHandleNullPropagationOption(query) - : HandleNullPropagationOption.True; - } - - return updatedSettings; + updatedSettings.CopyFrom(settings); } - public static SkipTokenHandler GetSkipTokenHandler(this ODataQueryContext context) + updatedSettings.CopyFrom(querySettings); + + if (updatedSettings.HandleNullPropagation == HandleNullPropagationOption.Default) { - return context?.RequestContainer?.GetRequiredService() ?? DefaultSkipTokenHandler.Instance; + updatedSettings.HandleNullPropagation = query != null + ? HandleNullPropagationOptionHelper.GetDefaultHandleNullPropagationOption(query) + : HandleNullPropagationOption.True; } - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static IFilterBinder GetFilterBinder(this ODataQueryContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + return updatedSettings; + } - IFilterBinder binder = context.RequestContainer?.GetService(); - return binder ?? new FilterBinder(); - } + public static SkipTokenHandler GetSkipTokenHandler(this ODataQueryContext context) + { + return context?.RequestContainer?.GetRequiredService() ?? DefaultSkipTokenHandler.Instance; + } - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static ISearchBinder GetSearchBinder(this ODataQueryContext context) + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static IFilterBinder GetFilterBinder(this ODataQueryContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - // We don't provide the default implementation of ISearchBinder, - // Actually, how to match is dependent upon the implementation. - return context.RequestContainer?.GetService(); + throw Error.ArgumentNull(nameof(context)); } - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static ISelectExpandBinder GetSelectExpandBinder(this ODataQueryContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - ISelectExpandBinder binder = context.RequestContainer?.GetService(); + IFilterBinder binder = context.RequestContainer?.GetService(); + return binder ?? new FilterBinder(); + } - return binder ?? new SelectExpandBinder(context.GetFilterBinder(), context.GetOrderByBinder()); + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static ISearchBinder GetSearchBinder(this ODataQueryContext context) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); } - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static IOrderByBinder GetOrderByBinder(this ODataQueryContext context) + // We don't provide the default implementation of ISearchBinder, + // Actually, how to match is dependent upon the implementation. + return context.RequestContainer?.GetService(); + } + + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static ISelectExpandBinder GetSelectExpandBinder(this ODataQueryContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - IOrderByBinder binder = context.RequestContainer?.GetService(); + ISelectExpandBinder binder = context.RequestContainer?.GetService(); - return binder ?? new OrderByBinder(); - } + return binder ?? new SelectExpandBinder(context.GetFilterBinder(), context.GetOrderByBinder()); + } - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static IAssemblyResolver GetAssemblyResolver(this ODataQueryContext context) + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static IOrderByBinder GetOrderByBinder(this ODataQueryContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - IAssemblyResolver resolver = context.RequestContainer?.GetService(); + IOrderByBinder binder = context.RequestContainer?.GetService(); - return resolver ?? AssemblyResolverHelper.Default; - } + return binder ?? new OrderByBinder(); + } - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static IODataQueryValidator GetODataQueryValidator(this ODataQueryContext context) + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static IAssemblyResolver GetAssemblyResolver(this ODataQueryContext context) + { + if (context == null) { - return context?.RequestContainer?.GetService() - ?? new ODataQueryValidator(); + throw Error.ArgumentNull(nameof(context)); } - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static IComputeQueryValidator GetComputeQueryValidator(this ODataQueryContext context) - { - return context?.RequestContainer?.GetService() - ?? new ComputeQueryValidator(); - } + IAssemblyResolver resolver = context.RequestContainer?.GetService(); - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static ICountQueryValidator GetCountQueryValidator(this ODataQueryContext context) - { - return context?.RequestContainer?.GetService() - ?? new CountQueryValidator(); - } + return resolver ?? AssemblyResolverHelper.Default; + } - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static IFilterQueryValidator GetFilterQueryValidator(this ODataQueryContext context) - { - return context?.RequestContainer?.GetService() - ?? new FilterQueryValidator(); - } + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static IODataQueryValidator GetODataQueryValidator(this ODataQueryContext context) + { + return context?.RequestContainer?.GetService() + ?? new ODataQueryValidator(); + } - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static IOrderByQueryValidator GetOrderByQueryValidator(this ODataQueryContext context) - { - return context?.RequestContainer?.GetService() - ?? new OrderByQueryValidator(); - } + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static IComputeQueryValidator GetComputeQueryValidator(this ODataQueryContext context) + { + return context?.RequestContainer?.GetService() + ?? new ComputeQueryValidator(); + } - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static ISkipQueryValidator GetSkipQueryValidator(this ODataQueryContext context) - { - return context?.RequestContainer?.GetService() - ?? new SkipQueryValidator(); - } + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static ICountQueryValidator GetCountQueryValidator(this ODataQueryContext context) + { + return context?.RequestContainer?.GetService() + ?? new CountQueryValidator(); + } - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static ISkipTokenQueryValidator GetSkipTokenQueryValidator(this ODataQueryContext context) - { - return context?.RequestContainer?.GetService() - ?? new SkipTokenQueryValidator(); - } + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static IFilterQueryValidator GetFilterQueryValidator(this ODataQueryContext context) + { + return context?.RequestContainer?.GetService() + ?? new FilterQueryValidator(); + } - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static ITopQueryValidator GetTopQueryValidator(this ODataQueryContext context) - { - return context?.RequestContainer?.GetService() - ?? new TopQueryValidator(); - } + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static IOrderByQueryValidator GetOrderByQueryValidator(this ODataQueryContext context) + { + return context?.RequestContainer?.GetService() + ?? new OrderByQueryValidator(); + } - /// - /// Gets the . - /// - /// The query context. - /// The built . - public static ISelectExpandQueryValidator GetSelectExpandQueryValidator(this ODataQueryContext context) - { - return context?.RequestContainer?.GetService() - ?? new SelectExpandQueryValidator(); - } + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static ISkipQueryValidator GetSkipQueryValidator(this ODataQueryContext context) + { + return context?.RequestContainer?.GetService() + ?? new SkipQueryValidator(); + } + + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static ISkipTokenQueryValidator GetSkipTokenQueryValidator(this ODataQueryContext context) + { + return context?.RequestContainer?.GetService() + ?? new SkipTokenQueryValidator(); + } + + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static ITopQueryValidator GetTopQueryValidator(this ODataQueryContext context) + { + return context?.RequestContainer?.GetService() + ?? new TopQueryValidator(); + } + + /// + /// Gets the . + /// + /// The query context. + /// The built . + public static ISelectExpandQueryValidator GetSelectExpandQueryValidator(this ODataQueryContext context) + { + return context?.RequestContainer?.GetService() + ?? new SelectExpandQueryValidator(); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs b/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs index 293ce063d..f82353063 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptions.cs @@ -27,1152 +27,1151 @@ using Microsoft.OData.UriParser; using Microsoft.OData.UriParser.Aggregation; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This defines a composite OData query options that can be used to perform query composition. +/// Currently this only supports $filter, $orderby, $top, $skip, and $count. +/// +[NonValidatingParameterBinding] +[ODataQueryParameterBinding] +[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many ODataLib classes.")] +public class ODataQueryOptions { + private static readonly MethodInfo _limitResultsGenericMethod = typeof(ODataQueryOptions).GetMethods(BindingFlags.Public | BindingFlags.Static) + .Single(mi => mi.Name == "LimitResults" && mi.ContainsGenericParameters && mi.GetParameters().Length == 4); + + private ODataQueryOptionParser _queryOptionParser; + + private ETag _etagIfMatch; + + private bool _etagIfMatchChecked; + + private ETag _etagIfNoneMatch; + + private bool _etagIfNoneMatchChecked; + + private bool _enableNoDollarSignQueryOptions = false; + + private OrderByQueryOption _stableOrderBy; + /// - /// This defines a composite OData query options that can be used to perform query composition. - /// Currently this only supports $filter, $orderby, $top, $skip, and $count. + /// Initializes a new instance of the class based on the incoming request and some metadata information from + /// the . /// - [NonValidatingParameterBinding] - [ODataQueryParameterBinding] - [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Relies on many ODataLib classes.")] - public class ODataQueryOptions + /// The which contains the and some type information. + /// The incoming request message. + public ODataQueryOptions(ODataQueryContext context, HttpRequest request) { - private static readonly MethodInfo _limitResultsGenericMethod = typeof(ODataQueryOptions).GetMethods(BindingFlags.Public | BindingFlags.Static) - .Single(mi => mi.Name == "LimitResults" && mi.ContainsGenericParameters && mi.GetParameters().Length == 4); + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - private ODataQueryOptionParser _queryOptionParser; + if (request == null) + { + throw Error.ArgumentNull(nameof(request)); + } - private ETag _etagIfMatch; + Contract.Assert(context.RequestContainer == null); + context.RequestContainer = request.GetRouteServices(); + context.Request = request; - private bool _etagIfMatchChecked; + Context = context; + Request = request; - private ETag _etagIfNoneMatch; + Initialize(context); + } - private bool _etagIfNoneMatchChecked; + /// + /// Gets the request message associated with this instance. + /// + public HttpRequest Request { get; private set; } - private bool _enableNoDollarSignQueryOptions = false; + /// + /// Gets the given + /// + public ODataQueryContext Context { get; private set; } - private OrderByQueryOption _stableOrderBy; + /// + /// Gets the raw string of all the OData query options + /// + public ODataRawQueryOptions RawValues { get; private set; } - /// - /// Initializes a new instance of the class based on the incoming request and some metadata information from - /// the . - /// - /// The which contains the and some type information. - /// The incoming request message. - public ODataQueryOptions(ODataQueryContext context, HttpRequest request) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + /// + /// Gets the . + /// + public SelectExpandQueryOption SelectExpand { get; private set; } - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + /// + /// Gets the . + /// + public ApplyQueryOption Apply { get; private set; } - Contract.Assert(context.RequestContainer == null); - context.RequestContainer = request.GetRouteServices(); - context.Request = request; - - Context = context; - Request = request; - - Initialize(context); - } - - /// - /// Gets the request message associated with this instance. - /// - public HttpRequest Request { get; private set; } - - /// - /// Gets the given - /// - public ODataQueryContext Context { get; private set; } - - /// - /// Gets the raw string of all the OData query options - /// - public ODataRawQueryOptions RawValues { get; private set; } - - /// - /// Gets the . - /// - public SelectExpandQueryOption SelectExpand { get; private set; } - - /// - /// Gets the . - /// - public ApplyQueryOption Apply { get; private set; } - - /// - /// Gets the . - /// - public ComputeQueryOption Compute { get; private set; } - - /// - /// Gets the . - /// - public FilterQueryOption Filter { get; private set; } - - /// - /// Gets the . - /// - public SearchQueryOption Search { get; private set; } - - /// - /// Gets the . - /// - public OrderByQueryOption OrderBy { get; private set; } - - /// - /// Gets the . - /// - public SkipQueryOption Skip { get; private set; } - - /// - /// Gets the . - /// - public SkipTokenQueryOption SkipToken { get; private set; } - - /// - /// Gets the . - /// - public TopQueryOption Top { get; private set; } - - /// - /// Gets the . - /// - public CountQueryOption Count { get; private set; } - - /// - /// Gets or sets the query validator. - /// - public IODataQueryValidator Validator { get; set; } - - /// - /// Check if the given query option is an OData system query option using $-prefix-required theme. - /// - /// The name of the query option. - /// Returns true if the query option is an OData system query option. - public static bool IsSystemQueryOption(string queryOptionName) - { - return IsSystemQueryOption(queryOptionName, false); - } - - /// - /// Check if the given query option is an OData system query option. - /// - /// The name of the query option. - /// Whether the optional-$-prefix scheme is used for OData system query. - /// Returns true if the query option is an OData system query option. - public static bool IsSystemQueryOption(string queryOptionName, bool isDollarSignOptional) - { - if (string.IsNullOrEmpty(queryOptionName)) - { - throw Error.ArgumentNullOrEmpty(nameof(queryOptionName)); - } + /// + /// Gets the . + /// + public ComputeQueryOption Compute { get; private set; } - string fixedQueryOptionName = queryOptionName; - if (isDollarSignOptional && !queryOptionName.StartsWith("$", StringComparison.Ordinal)) - { - fixedQueryOptionName = "$" + queryOptionName; - } + /// + /// Gets the . + /// + public FilterQueryOption Filter { get; private set; } - return fixedQueryOptionName.Equals("$orderby", StringComparison.Ordinal) || - fixedQueryOptionName.Equals("$filter", StringComparison.Ordinal) || - fixedQueryOptionName.Equals("$top", StringComparison.Ordinal) || - fixedQueryOptionName.Equals("$skip", StringComparison.Ordinal) || - fixedQueryOptionName.Equals("$count", StringComparison.Ordinal) || - fixedQueryOptionName.Equals("$expand", StringComparison.Ordinal) || - fixedQueryOptionName.Equals("$select", StringComparison.Ordinal) || - fixedQueryOptionName.Equals("$format", StringComparison.Ordinal) || - fixedQueryOptionName.Equals("$skiptoken", StringComparison.Ordinal) || - fixedQueryOptionName.Equals("$deltatoken", StringComparison.Ordinal) || - fixedQueryOptionName.Equals("$search", StringComparison.Ordinal) || - fixedQueryOptionName.Equals("$compute", StringComparison.Ordinal) || - fixedQueryOptionName.Equals("$apply", StringComparison.Ordinal); - } - - /// - /// Gets the from IfMatch header. - /// - public virtual ETag IfMatch - { - get + /// + /// Gets the . + /// + public SearchQueryOption Search { get; private set; } + + /// + /// Gets the . + /// + public OrderByQueryOption OrderBy { get; private set; } + + /// + /// Gets the . + /// + public SkipQueryOption Skip { get; private set; } + + /// + /// Gets the . + /// + public SkipTokenQueryOption SkipToken { get; private set; } + + /// + /// Gets the . + /// + public TopQueryOption Top { get; private set; } + + /// + /// Gets the . + /// + public CountQueryOption Count { get; private set; } + + /// + /// Gets or sets the query validator. + /// + public IODataQueryValidator Validator { get; set; } + + /// + /// Check if the given query option is an OData system query option using $-prefix-required theme. + /// + /// The name of the query option. + /// Returns true if the query option is an OData system query option. + public static bool IsSystemQueryOption(string queryOptionName) + { + return IsSystemQueryOption(queryOptionName, false); + } + + /// + /// Check if the given query option is an OData system query option. + /// + /// The name of the query option. + /// Whether the optional-$-prefix scheme is used for OData system query. + /// Returns true if the query option is an OData system query option. + public static bool IsSystemQueryOption(string queryOptionName, bool isDollarSignOptional) + { + if (string.IsNullOrEmpty(queryOptionName)) + { + throw Error.ArgumentNullOrEmpty(nameof(queryOptionName)); + } + + string fixedQueryOptionName = queryOptionName; + if (isDollarSignOptional && !queryOptionName.StartsWith("$", StringComparison.Ordinal)) + { + fixedQueryOptionName = "$" + queryOptionName; + } + + return fixedQueryOptionName.Equals("$orderby", StringComparison.Ordinal) || + fixedQueryOptionName.Equals("$filter", StringComparison.Ordinal) || + fixedQueryOptionName.Equals("$top", StringComparison.Ordinal) || + fixedQueryOptionName.Equals("$skip", StringComparison.Ordinal) || + fixedQueryOptionName.Equals("$count", StringComparison.Ordinal) || + fixedQueryOptionName.Equals("$expand", StringComparison.Ordinal) || + fixedQueryOptionName.Equals("$select", StringComparison.Ordinal) || + fixedQueryOptionName.Equals("$format", StringComparison.Ordinal) || + fixedQueryOptionName.Equals("$skiptoken", StringComparison.Ordinal) || + fixedQueryOptionName.Equals("$deltatoken", StringComparison.Ordinal) || + fixedQueryOptionName.Equals("$search", StringComparison.Ordinal) || + fixedQueryOptionName.Equals("$compute", StringComparison.Ordinal) || + fixedQueryOptionName.Equals("$apply", StringComparison.Ordinal); + } + + /// + /// Gets the from IfMatch header. + /// + public virtual ETag IfMatch + { + get + { + if (!_etagIfMatchChecked && _etagIfMatch == null) { - if (!_etagIfMatchChecked && _etagIfMatch == null) + StringValues ifMatchValues; + if (Request.Headers.TryGetValue("If-Match", out ifMatchValues)) { - StringValues ifMatchValues; - if (Request.Headers.TryGetValue("If-Match", out ifMatchValues)) - { - EntityTagHeaderValue etagHeaderValue = EntityTagHeaderValue.Parse(ifMatchValues.SingleOrDefault()); - _etagIfMatch = GetETag(etagHeaderValue); - _etagIfMatchChecked = true; - } + EntityTagHeaderValue etagHeaderValue = EntityTagHeaderValue.Parse(ifMatchValues.SingleOrDefault()); + _etagIfMatch = GetETag(etagHeaderValue); + _etagIfMatchChecked = true; } - - return _etagIfMatch; } + + return _etagIfMatch; } + } - /// - /// Gets the from IfNoneMatch header. - /// - public virtual ETag IfNoneMatch + /// + /// Gets the from IfNoneMatch header. + /// + public virtual ETag IfNoneMatch + { + get { - get + if (!_etagIfNoneMatchChecked && _etagIfNoneMatch == null) { - if (!_etagIfNoneMatchChecked && _etagIfNoneMatch == null) + StringValues ifNoneMatchValues; + if (Request.Headers.TryGetValue("If-None-Match", out ifNoneMatchValues)) { - StringValues ifNoneMatchValues; - if (Request.Headers.TryGetValue("If-None-Match", out ifNoneMatchValues)) + EntityTagHeaderValue etagHeaderValue = EntityTagHeaderValue.Parse(ifNoneMatchValues.SingleOrDefault()); + _etagIfNoneMatch = GetETag(etagHeaderValue); + if (_etagIfNoneMatch != null) { - EntityTagHeaderValue etagHeaderValue = EntityTagHeaderValue.Parse(ifNoneMatchValues.SingleOrDefault()); - _etagIfNoneMatch = GetETag(etagHeaderValue); - if (_etagIfNoneMatch != null) - { - _etagIfNoneMatch.IsIfNoneMatch = true; - } - _etagIfNoneMatchChecked = true; + _etagIfNoneMatch.IsIfNoneMatch = true; } - _etagIfNoneMatchChecked = true; } - return _etagIfNoneMatch; + _etagIfNoneMatchChecked = true; } + + return _etagIfNoneMatch; } + } - /// - /// Gets the EntityTagHeaderValue ETag. - /// - internal virtual ETag GetETag(EntityTagHeaderValue etagHeaderValue) + /// + /// Gets the EntityTagHeaderValue ETag. + /// + internal virtual ETag GetETag(EntityTagHeaderValue etagHeaderValue) + { + return Request.GetETag(etagHeaderValue); + } + + /// + /// Check if the given query option is the supported query option. + /// + /// The name of the query option. + /// Returns true if the query option is the supported query option. + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", + Justification = "Need lower case string here.")] + public bool IsSupportedQueryOption(string queryOptionName) + { + if (string.IsNullOrEmpty(queryOptionName)) { - return Request.GetETag(etagHeaderValue); + throw Error.ArgumentNullOrEmpty(nameof(queryOptionName)); } - /// - /// Check if the given query option is the supported query option. - /// - /// The name of the query option. - /// Returns true if the query option is the supported query option. - [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", - Justification = "Need lower case string here.")] - public bool IsSupportedQueryOption(string queryOptionName) + ODataUriResolver resolver = null; + if (_queryOptionParser != null) { - if (string.IsNullOrEmpty(queryOptionName)) - { - throw Error.ArgumentNullOrEmpty(nameof(queryOptionName)); - } - - ODataUriResolver resolver = null; - if (_queryOptionParser != null) - { - resolver = _queryOptionParser.Resolver; - } - - if (resolver == null && Context.RequestContainer != null) - { - resolver = Context.RequestContainer.GetService(); - } - - // If we don't have the resolver setting, by default we support case-insensitive - if (resolver != null && !resolver.EnableCaseInsensitive) - { - return IsSystemQueryOption(queryOptionName, this._enableNoDollarSignQueryOptions); - } - - string lowcaseQueryOptionName = queryOptionName.ToLowerInvariant(); - return IsSystemQueryOption(lowcaseQueryOptionName, this._enableNoDollarSignQueryOptions); + resolver = _queryOptionParser.Resolver; } - /// - /// Apply the individual query to the given IQueryable in the right order. - /// - /// The original . - /// The new after the query has been applied to. - public virtual IQueryable ApplyTo(IQueryable query) + if (resolver == null && Context.RequestContainer != null) { - ODataQuerySettings querySettings = Context.GetODataQuerySettings(); - return ApplyTo(query, querySettings); + resolver = Context.RequestContainer.GetService(); } - /// - /// Apply the individual query to the given IQueryable in the right order. - /// - /// The original . - /// The query parameters that are already applied in queries. - /// The new after the query has been applied to. - public virtual IQueryable ApplyTo(IQueryable query, AllowedQueryOptions ignoreQueryOptions) + // If we don't have the resolver setting, by default we support case-insensitive + if (resolver != null && !resolver.EnableCaseInsensitive) { - ODataQuerySettings querySettings = Context.GetODataQuerySettings(); - querySettings.IgnoredQueryOptions = ignoreQueryOptions; - - return ApplyTo(query, querySettings); + return IsSystemQueryOption(queryOptionName, this._enableNoDollarSignQueryOptions); } - /// - /// Apply the individual query to the given IQueryable in the right order. - /// - /// The original . - /// The settings to use in query composition. - /// The query parameters that are already applied in queries. - /// The new after the query has been applied to. - public virtual IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings, AllowedQueryOptions ignoreQueryOptions) - { - ODataQuerySettings settings = new ODataQuerySettings(); - settings.CopyFrom(querySettings); - settings.IgnoredQueryOptions = ignoreQueryOptions; + string lowcaseQueryOptionName = queryOptionName.ToLowerInvariant(); + return IsSystemQueryOption(lowcaseQueryOptionName, this._enableNoDollarSignQueryOptions); + } + + /// + /// Apply the individual query to the given IQueryable in the right order. + /// + /// The original . + /// The new after the query has been applied to. + public virtual IQueryable ApplyTo(IQueryable query) + { + ODataQuerySettings querySettings = Context.GetODataQuerySettings(); + return ApplyTo(query, querySettings); + } + + /// + /// Apply the individual query to the given IQueryable in the right order. + /// + /// The original . + /// The query parameters that are already applied in queries. + /// The new after the query has been applied to. + public virtual IQueryable ApplyTo(IQueryable query, AllowedQueryOptions ignoreQueryOptions) + { + ODataQuerySettings querySettings = Context.GetODataQuerySettings(); + querySettings.IgnoredQueryOptions = ignoreQueryOptions; + + return ApplyTo(query, querySettings); + } - return ApplyTo(query, settings); + /// + /// Apply the individual query to the given IQueryable in the right order. + /// + /// The original . + /// The settings to use in query composition. + /// The query parameters that are already applied in queries. + /// The new after the query has been applied to. + public virtual IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings, AllowedQueryOptions ignoreQueryOptions) + { + ODataQuerySettings settings = new ODataQuerySettings(); + settings.CopyFrom(querySettings); + settings.IgnoredQueryOptions = ignoreQueryOptions; + + return ApplyTo(query, settings); + } + + /// + /// Apply the individual query to the given IQueryable in the right order. + /// + /// The original . + /// The settings to use in query composition. + /// The new after the query has been applied to. + public virtual IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + { + if (query == null) + { + throw Error.ArgumentNull(nameof(query)); } - /// - /// Apply the individual query to the given IQueryable in the right order. - /// - /// The original . - /// The settings to use in query composition. - /// The new after the query has been applied to. - public virtual IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + if (querySettings == null) { - if (query == null) - { - throw Error.ArgumentNull(nameof(query)); - } + throw Error.ArgumentNull(nameof(querySettings)); + } - if (querySettings == null) - { - throw Error.ArgumentNull(nameof(querySettings)); - } + IQueryable result = query; + IODataFeature odataFeature = Request.ODataFeature(); - IQueryable result = query; - IODataFeature odataFeature = Request.ODataFeature(); + // Update the query setting + querySettings = Context.UpdateQuerySettings(querySettings, query); - // Update the query setting - querySettings = Context.UpdateQuerySettings(querySettings, query); + // First apply $apply + // Section 3.15 of the spec http://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/cs01/odata-data-aggregation-ext-v4.0-cs01.html#_Toc378326311 + if (IsAvailableODataQueryOption(Apply, querySettings, AllowedQueryOptions.Apply)) + { + result = Apply.ApplyTo(result, querySettings); + odataFeature.ApplyClause = Apply.ApplyClause; + this.Context.ElementClrType = Apply.ResultClrType; + } - // First apply $apply - // Section 3.15 of the spec http://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/cs01/odata-data-aggregation-ext-v4.0-cs01.html#_Toc378326311 - if (IsAvailableODataQueryOption(Apply, querySettings, AllowedQueryOptions.Apply)) - { - result = Apply.ApplyTo(result, querySettings); - odataFeature.ApplyClause = Apply.ApplyClause; - this.Context.ElementClrType = Apply.ResultClrType; - } + // We need this code to let 'ODataQueryOptionParser' to parse the 'ComputeClause' + // By parsing the compute clause, we give the parser opportunity to gather the 'computed' properties + // which could be required in $filter, $order and $select, etc. + // Without this code, it will be failed when customer only uses 'ODataQueryOptions' as the parameter in action. + // It should work if customer uses [EnableQuery], this is because [EnableQuery] calls validators + // and the validators parse the 'ComputeClause' already. + if (IsAvailableODataQueryOption(Compute, querySettings, AllowedQueryOptions.Compute)) + { + _ = Compute.ComputeClause; + } - // We need this code to let 'ODataQueryOptionParser' to parse the 'ComputeClause' - // By parsing the compute clause, we give the parser opportunity to gather the 'computed' properties - // which could be required in $filter, $order and $select, etc. - // Without this code, it will be failed when customer only uses 'ODataQueryOptions' as the parameter in action. - // It should work if customer uses [EnableQuery], this is because [EnableQuery] calls validators - // and the validators parse the 'ComputeClause' already. + // TODO: need pass the result from $compute to the remaining query options + // Construct the actual query and apply them in the following order: filter, orderby, skip, top + if (IsAvailableODataQueryOption(Filter, querySettings, AllowedQueryOptions.Filter)) + { if (IsAvailableODataQueryOption(Compute, querySettings, AllowedQueryOptions.Compute)) { - _ = Compute.ComputeClause; + Filter.Compute = Compute; } - // TODO: need pass the result from $compute to the remaining query options - // Construct the actual query and apply them in the following order: filter, orderby, skip, top - if (IsAvailableODataQueryOption(Filter, querySettings, AllowedQueryOptions.Filter)) - { - if (IsAvailableODataQueryOption(Compute, querySettings, AllowedQueryOptions.Compute)) - { - Filter.Compute = Compute; - } - - result = Filter.ApplyTo(result, querySettings); - } + result = Filter.ApplyTo(result, querySettings); + } - // If both $search and $filter are specified in the same request, only those items satisfying both criteria are returned - // apply $search - if (IsAvailableODataQueryOption(Search, querySettings, AllowedQueryOptions.Search)) - { - result = Search.ApplyTo(result, querySettings); - } + // If both $search and $filter are specified in the same request, only those items satisfying both criteria are returned + // apply $search + if (IsAvailableODataQueryOption(Search, querySettings, AllowedQueryOptions.Search)) + { + result = Search.ApplyTo(result, querySettings); + } - if (IsAvailableODataQueryOption(Count, querySettings, AllowedQueryOptions.Count)) + if (IsAvailableODataQueryOption(Count, querySettings, AllowedQueryOptions.Count)) + { + if (odataFeature.TotalCountFunc == null) { - if (odataFeature.TotalCountFunc == null) + Func countFunc = Count.GetEntityCountFunc(result); + if (countFunc != null) { - Func countFunc = Count.GetEntityCountFunc(result); - if (countFunc != null) - { - odataFeature.TotalCountFunc = countFunc; - } - } - - if (Request.IsCountRequest()) - { - return result; + odataFeature.TotalCountFunc = countFunc; } } - OrderByQueryOption orderBy = OrderBy; - - // $skip or $top require a stable sort for predictable results. - // Result limits require a stable sort to be able to generate a next page link. - // If either is present in the query and we have permission, - // generate an $orderby that will produce a stable sort. - if (querySettings.EnsureStableOrdering && - (IsAvailableODataQueryOption(Skip, querySettings, AllowedQueryOptions.Skip) || - IsAvailableODataQueryOption(Top, querySettings, AllowedQueryOptions.Top) || - querySettings.PageSize.HasValue || - querySettings.ModelBoundPageSize.HasValue)) + if (Request.IsCountRequest()) { - // If there is no OrderBy present, we manufacture a default. - // If an OrderBy is already present, we add any missing - // properties necessary to make a stable sort. - // Instead of failing early here if we cannot generate the OrderBy, - // let the IQueryable backend fail (if it has to). - - orderBy = GenerateStableOrder(); - OrderBy = orderBy; // update the orderby + return result; } + } - if (IsAvailableODataQueryOption(orderBy, querySettings, AllowedQueryOptions.OrderBy)) - { - if (IsAvailableODataQueryOption(Compute, querySettings, AllowedQueryOptions.Compute)) - { - orderBy.Compute = Compute; - } - - result = orderBy.ApplyTo(result, querySettings); - } + OrderByQueryOption orderBy = OrderBy; + + // $skip or $top require a stable sort for predictable results. + // Result limits require a stable sort to be able to generate a next page link. + // If either is present in the query and we have permission, + // generate an $orderby that will produce a stable sort. + if (querySettings.EnsureStableOrdering && + (IsAvailableODataQueryOption(Skip, querySettings, AllowedQueryOptions.Skip) || + IsAvailableODataQueryOption(Top, querySettings, AllowedQueryOptions.Top) || + querySettings.PageSize.HasValue || + querySettings.ModelBoundPageSize.HasValue)) + { + // If there is no OrderBy present, we manufacture a default. + // If an OrderBy is already present, we add any missing + // properties necessary to make a stable sort. + // Instead of failing early here if we cannot generate the OrderBy, + // let the IQueryable backend fail (if it has to). + + orderBy = GenerateStableOrder(); + OrderBy = orderBy; // update the orderby + } - if (IsAvailableODataQueryOption(SkipToken, querySettings, AllowedQueryOptions.SkipToken)) + if (IsAvailableODataQueryOption(orderBy, querySettings, AllowedQueryOptions.OrderBy)) + { + if (IsAvailableODataQueryOption(Compute, querySettings, AllowedQueryOptions.Compute)) { - result = SkipToken.ApplyTo(result, querySettings, this); + orderBy.Compute = Compute; } - AddAutoSelectExpandProperties(); + result = orderBy.ApplyTo(result, querySettings); + } - if (SelectExpand != null) - { - var tempResult = ApplySelectExpand(result, querySettings); - if (tempResult != default(IQueryable)) - { - result = tempResult; - } - } + if (IsAvailableODataQueryOption(SkipToken, querySettings, AllowedQueryOptions.SkipToken)) + { + result = SkipToken.ApplyTo(result, querySettings, this); + } - if (IsAvailableODataQueryOption(Skip, querySettings, AllowedQueryOptions.Skip)) - { - result = Skip.ApplyTo(result, querySettings); - } + AddAutoSelectExpandProperties(); - if (IsAvailableODataQueryOption(Top, querySettings, AllowedQueryOptions.Top)) + if (SelectExpand != null) + { + var tempResult = ApplySelectExpand(result, querySettings); + if (tempResult != default(IQueryable)) { - result = Top.ApplyTo(result, querySettings); + result = tempResult; } + } - result = ApplyPaging(result, querySettings); - - return result; + if (IsAvailableODataQueryOption(Skip, querySettings, AllowedQueryOptions.Skip)) + { + result = Skip.ApplyTo(result, querySettings); } - internal IQueryable ApplyPaging(IQueryable result, ODataQuerySettings querySettings) + if (IsAvailableODataQueryOption(Top, querySettings, AllowedQueryOptions.Top)) { - int pageSize = -1; - if (querySettings.PageSize.HasValue) - { - pageSize = querySettings.PageSize.Value; - } - else if (querySettings.ModelBoundPageSize.HasValue) - { - pageSize = querySettings.ModelBoundPageSize.Value; - } + result = Top.ApplyTo(result, querySettings); + } - int preferredPageSize = -1; - if (RequestPreferenceHelpers.RequestPrefersMaxPageSize(Request.Headers, out preferredPageSize)) - { - pageSize = Math.Min(pageSize, preferredPageSize); - } + result = ApplyPaging(result, querySettings); - ODataFeature odataFeature = Request.ODataFeature() as ODataFeature; - if (pageSize > 0) - { - bool resultsLimited; - result = LimitResults(result, pageSize, querySettings.EnableConstantParameterization, out resultsLimited); - if (resultsLimited && Request.GetEncodedUrl() != null && - odataFeature.NextLink == null) - { - odataFeature.PageSize = pageSize; - } - } + return result; + } - odataFeature.QueryOptions = this; + internal IQueryable ApplyPaging(IQueryable result, ODataQuerySettings querySettings) + { + int pageSize = -1; + if (querySettings.PageSize.HasValue) + { + pageSize = querySettings.PageSize.Value; + } + else if (querySettings.ModelBoundPageSize.HasValue) + { + pageSize = querySettings.ModelBoundPageSize.Value; + } - return result; + int preferredPageSize = -1; + if (RequestPreferenceHelpers.RequestPrefersMaxPageSize(Request.Headers, out preferredPageSize)) + { + pageSize = Math.Min(pageSize, preferredPageSize); } - /// - /// Generates the Stable OrderBy query option based on the existing OrderBy and other query options. - /// - /// An order by query option that ensures stable ordering of the results. - public virtual OrderByQueryOption GenerateStableOrder() + ODataFeature odataFeature = Request.ODataFeature() as ODataFeature; + if (pageSize > 0) { - if (_stableOrderBy != null) + bool resultsLimited; + result = LimitResults(result, pageSize, querySettings.EnableConstantParameterization, out resultsLimited); + if (resultsLimited && Request.GetEncodedUrl() != null && + odataFeature.NextLink == null) { - return _stableOrderBy; + odataFeature.PageSize = pageSize; } + } - ApplyClause apply = Apply != null ? Apply.ApplyClause : null; - List applySortOptions = GetApplySortOptions(apply); + odataFeature.QueryOptions = this; - _stableOrderBy = OrderBy == null - ? GenerateDefaultOrderBy(Context, applySortOptions) - : EnsureStableSortOrderBy(OrderBy, Context, applySortOptions); + return result; + } + /// + /// Generates the Stable OrderBy query option based on the existing OrderBy and other query options. + /// + /// An order by query option that ensures stable ordering of the results. + public virtual OrderByQueryOption GenerateStableOrder() + { + if (_stableOrderBy != null) + { return _stableOrderBy; } - private static List GetApplySortOptions(ApplyClause apply) + ApplyClause apply = Apply != null ? Apply.ApplyClause : null; + List applySortOptions = GetApplySortOptions(apply); + + _stableOrderBy = OrderBy == null + ? GenerateDefaultOrderBy(Context, applySortOptions) + : EnsureStableSortOrderBy(OrderBy, Context, applySortOptions); + + return _stableOrderBy; + } + + private static List GetApplySortOptions(ApplyClause apply) + { + Func transformPredicate = t => t.Kind == TransformationNodeKind.Aggregate || t.Kind == TransformationNodeKind.GroupBy; + if (apply == null || !apply.Transformations.Any(transformPredicate)) + { + return null; + } + + var result = new List(); + var lastTransform = apply.Transformations.Last(transformPredicate); + if (lastTransform.Kind == TransformationNodeKind.Aggregate) { - Func transformPredicate = t => t.Kind == TransformationNodeKind.Aggregate || t.Kind == TransformationNodeKind.GroupBy; - if (apply == null || !apply.Transformations.Any(transformPredicate)) + var aggregateClause = lastTransform as AggregateTransformationNode; + foreach (var expr in aggregateClause.AggregateExpressions.OfType()) { - return null; + result.Add(expr.Alias); } + } + else if (lastTransform.Kind == TransformationNodeKind.GroupBy) + { + var groupByClause = lastTransform as GroupByTransformationNode; + var groupingProperties = groupByClause.GroupingProperties; + ExtractGroupingProperties(result, groupingProperties); + } - var result = new List(); - var lastTransform = apply.Transformations.Last(transformPredicate); - if (lastTransform.Kind == TransformationNodeKind.Aggregate) + return result; + } + + private static void ExtractGroupingProperties(List result, IEnumerable groupingProperties, string prefix = null) + { + foreach (var gp in groupingProperties) + { + var fullPath = prefix != null ? prefix + "/" + gp.Name : gp.Name; + if (gp.ChildTransformations != null && gp.ChildTransformations.Any()) { - var aggregateClause = lastTransform as AggregateTransformationNode; - foreach (var expr in aggregateClause.AggregateExpressions.OfType()) - { - result.Add(expr.Alias); - } + ExtractGroupingProperties(result, gp.ChildTransformations, fullPath); } - else if (lastTransform.Kind == TransformationNodeKind.GroupBy) + else { - var groupByClause = lastTransform as GroupByTransformationNode; - var groupingProperties = groupByClause.GroupingProperties; - ExtractGroupingProperties(result, groupingProperties); + result.Add(fullPath); } - - return result; } + } - private static void ExtractGroupingProperties(List result, IEnumerable groupingProperties, string prefix = null) + /// + /// Apply the individual query to the given IQueryable in the right order. + /// + /// The original entity. + /// The that contains all the query application related settings. + /// The query parameters that are already applied in queries. + /// The new entity after the $select and $expand query has been applied to. + /// Only $select and $expand query options can be applied on single entities. This method throws if the query contains any other + /// query options. + public virtual object ApplyTo(object entity, ODataQuerySettings querySettings, AllowedQueryOptions ignoreQueryOptions) + { + ODataQuerySettings settings = new ODataQuerySettings(); + settings.CopyFrom(querySettings); + settings.IgnoredQueryOptions = ignoreQueryOptions; + + return ApplyTo(entity, settings); + } + + /// + /// Applies the query to the given entity using the given . + /// + /// The original entity. + /// The that contains all the query application related settings. + /// The new entity after the $select and $expand query has been applied to. + /// Only $select and $expand query options can be applied on single entities. This method throws if the query contains any other + /// query options. + public virtual object ApplyTo(object entity, ODataQuerySettings querySettings) + { + if (entity == null) { - foreach (var gp in groupingProperties) - { - var fullPath = prefix != null ? prefix + "/" + gp.Name : gp.Name; - if (gp.ChildTransformations != null && gp.ChildTransformations.Any()) - { - ExtractGroupingProperties(result, gp.ChildTransformations, fullPath); - } - else - { - result.Add(fullPath); - } - } + throw Error.ArgumentNull("entity"); } - /// - /// Apply the individual query to the given IQueryable in the right order. - /// - /// The original entity. - /// The that contains all the query application related settings. - /// The query parameters that are already applied in queries. - /// The new entity after the $select and $expand query has been applied to. - /// Only $select and $expand query options can be applied on single entities. This method throws if the query contains any other - /// query options. - public virtual object ApplyTo(object entity, ODataQuerySettings querySettings, AllowedQueryOptions ignoreQueryOptions) + if (querySettings == null) { - ODataQuerySettings settings = new ODataQuerySettings(); - settings.CopyFrom(querySettings); - settings.IgnoredQueryOptions = ignoreQueryOptions; - - return ApplyTo(entity, settings); + throw Error.ArgumentNull("querySettings"); } - /// - /// Applies the query to the given entity using the given . - /// - /// The original entity. - /// The that contains all the query application related settings. - /// The new entity after the $select and $expand query has been applied to. - /// Only $select and $expand query options can be applied on single entities. This method throws if the query contains any other - /// query options. - public virtual object ApplyTo(object entity, ODataQuerySettings querySettings) + if (Filter != null || OrderBy != null || Top != null || Skip != null || Count != null) { - if (entity == null) - { - throw Error.ArgumentNull("entity"); - } + throw Error.InvalidOperation(SRResources.NonSelectExpandOnSingleEntity); + } - if (querySettings == null) - { - throw Error.ArgumentNull("querySettings"); - } + // Update the query setting + querySettings = Context.UpdateQuerySettings(querySettings, query: null); + + AddAutoSelectExpandProperties(); - if (Filter != null || OrderBy != null || Top != null || Skip != null || Count != null) + if (SelectExpand != null) + { + var result = ApplySelectExpand(entity, querySettings); + if (result != default(object)) { - throw Error.InvalidOperation(SRResources.NonSelectExpandOnSingleEntity); + return result; } + } - // Update the query setting - querySettings = Context.UpdateQuerySettings(querySettings, query: null); + return entity; + } - AddAutoSelectExpandProperties(); + /// + /// Validate all OData queries, including $skip, $top, $orderby and $filter, based on the given . + /// It throws an ODataException if validation failed. + /// + /// The instance which contains all the validation settings. + public virtual void Validate(ODataValidationSettings validationSettings) + { + if (validationSettings == null) + { + throw Error.ArgumentNull(nameof(validationSettings)); + } - if (SelectExpand != null) - { - var result = ApplySelectExpand(entity, querySettings); - if (result != default(object)) - { - return result; - } - } + this.Context.ValidationSettings = validationSettings; - return entity; + if (Validator != null) + { + Validator.Validate(this, validationSettings); } + } - /// - /// Validate all OData queries, including $skip, $top, $orderby and $filter, based on the given . - /// It throws an ODataException if validation failed. - /// - /// The instance which contains all the validation settings. - public virtual void Validate(ODataValidationSettings validationSettings) + private static void ThrowIfEmpty(string queryValue, string queryName) + { + if (String.IsNullOrWhiteSpace(queryValue)) { - if (validationSettings == null) - { - throw Error.ArgumentNull(nameof(validationSettings)); - } + throw new ODataException(Error.Format(SRResources.QueryCannotBeEmpty, queryName)); + } + } - this.Context.ValidationSettings = validationSettings; + // Returns a sorted list of all properties that may legally appear + // in an OrderBy. If the entity type has keys, all are returned. + // Otherwise, when no keys are present, all primitive properties are returned. + private static IEnumerable GetAvailableOrderByProperties(ODataQueryContext context) + { + Contract.Assert(context != null); - if (Validator != null) - { - Validator.Validate(this, validationSettings); - } + var entityType = context.ElementType as IEdmEntityType; + if (entityType == null) + { + return Enumerable.Empty(); } + var properties = + entityType.Key().Any() + ? entityType.Key() + : entityType + .StructuralProperties() + .Where(property => property.Type.IsPrimitive() && !property.Type.IsStream()) + + // @discuss: Orderby the keys using the key name alphabet order doesn't make sense for me. + // Since we don't have the 'Order' value, we should keep the order same as definition order in the schema? + .OrderBy(p => p.Name); + + return properties.ToList(); + } - private static void ThrowIfEmpty(string queryValue, string queryName) + // Generates the OrderByQueryOption to use by default for $skip or $top + // when no other $orderby is available. It will produce a stable sort. + // This may return a null if there are no available properties. + private OrderByQueryOption GenerateDefaultOrderBy(ODataQueryContext context, List applySortOptions) + { + string orderByRaw = String.Empty; + if (applySortOptions != null) { - if (String.IsNullOrWhiteSpace(queryValue)) - { - throw new ODataException(Error.Format(SRResources.QueryCannotBeEmpty, queryName)); - } + orderByRaw = String.Join(",", applySortOptions); + return new OrderByQueryOption(orderByRaw, context, Apply.RawValue); } - - // Returns a sorted list of all properties that may legally appear - // in an OrderBy. If the entity type has keys, all are returned. - // Otherwise, when no keys are present, all primitive properties are returned. - private static IEnumerable GetAvailableOrderByProperties(ODataQueryContext context) + else { - Contract.Assert(context != null); + orderByRaw = String.Join(",", + GetAvailableOrderByProperties(context) + .Select(property => property.Name)); + } - var entityType = context.ElementType as IEdmEntityType; - if (entityType == null) - { - return Enumerable.Empty(); - } - var properties = - entityType.Key().Any() - ? entityType.Key() - : entityType - .StructuralProperties() - .Where(property => property.Type.IsPrimitive() && !property.Type.IsStream()) + return String.IsNullOrEmpty(orderByRaw) + ? null + : new OrderByQueryOption(orderByRaw, context); + } - // @discuss: Orderby the keys using the key name alphabet order doesn't make sense for me. - // Since we don't have the 'Order' value, we should keep the order same as definition order in the schema? - .OrderBy(p => p.Name); + /// + /// Ensures the given will produce a stable sort. + /// If it will, the input will be returned + /// unmodified. If the given will not produce a + /// stable sort, a new instance will be created + /// and returned. + /// + /// The to evaluate. + /// The . + /// + /// An that will produce a stable sort. + private OrderByQueryOption EnsureStableSortOrderBy(OrderByQueryOption orderBy, ODataQueryContext context, List applySortOptions) + { + Contract.Assert(orderBy != null); + Contract.Assert(context != null); - return properties.ToList(); + // Strategy: create a hash of all properties already used in the given OrderBy + // and remove them from the list of properties we need to add to make the sort stable. + Func propertyFunc = null; + if (applySortOptions != null) + { + propertyFunc = node => node.PropertyPath; } + else + { + propertyFunc = node => node.Property.Name; + } + + HashSet usedPropertyNames = new HashSet(orderBy.OrderByNodes + .OfType().Select(propertyFunc) + .Concat(orderBy.OrderByNodes.OfType().Select(p => p.PropertyName))); - // Generates the OrderByQueryOption to use by default for $skip or $top - // when no other $orderby is available. It will produce a stable sort. - // This may return a null if there are no available properties. - private OrderByQueryOption GenerateDefaultOrderBy(ODataQueryContext context, List applySortOptions) + if (applySortOptions != null) { - string orderByRaw = String.Empty; - if (applySortOptions != null) + var propertyPathsToAdd = applySortOptions.Where(p => !usedPropertyNames.Contains(p)).OrderBy(p => p); + if (propertyPathsToAdd.Any()) { - orderByRaw = String.Join(",", applySortOptions); - return new OrderByQueryOption(orderByRaw, context, Apply.RawValue); + var orderByRaw = orderBy.RawValue + "," + String.Join(",", propertyPathsToAdd); + orderBy = new OrderByQueryOption(orderByRaw, context, Apply.RawValue); } - else + } + else + { + IEnumerable propertiesToAdd = GetAvailableOrderByProperties(context).Where(prop => !usedPropertyNames.Contains(prop.Name)); + if (propertiesToAdd.Any()) { - orderByRaw = String.Join(",", - GetAvailableOrderByProperties(context) - .Select(property => property.Name)); + // The existing query options has too few properties to create a stable sort. + // Clone the given one and add the remaining properties to end, thereby making + // the sort stable but preserving the user's original intent for the major + // sort order. + var orderByRaw = orderBy.RawValue + "," + string.Join(",", propertiesToAdd.Select(p => p.Name)); + orderBy = new OrderByQueryOption(orderByRaw, context); } + } - return String.IsNullOrEmpty(orderByRaw) - ? null - : new OrderByQueryOption(orderByRaw, context); - } - - /// - /// Ensures the given will produce a stable sort. - /// If it will, the input will be returned - /// unmodified. If the given will not produce a - /// stable sort, a new instance will be created - /// and returned. - /// - /// The to evaluate. - /// The . - /// - /// An that will produce a stable sort. - private OrderByQueryOption EnsureStableSortOrderBy(OrderByQueryOption orderBy, ODataQueryContext context, List applySortOptions) - { - Contract.Assert(orderBy != null); - Contract.Assert(context != null); - - // Strategy: create a hash of all properties already used in the given OrderBy - // and remove them from the list of properties we need to add to make the sort stable. - Func propertyFunc = null; - if (applySortOptions != null) - { - propertyFunc = node => node.PropertyPath; - } - else - { - propertyFunc = node => node.Property.Name; - } + return orderBy; + } - HashSet usedPropertyNames = new HashSet(orderBy.OrderByNodes - .OfType().Select(propertyFunc) - .Concat(orderBy.OrderByNodes.OfType().Select(p => p.PropertyName))); + internal static IQueryable LimitResults(IQueryable queryable, int limit, bool parameterize, out bool resultsLimited) + { + MethodInfo genericMethod = _limitResultsGenericMethod.MakeGenericMethod(queryable.ElementType); + object[] args = new object[] { queryable, limit, parameterize, null }; + IQueryable results = genericMethod.Invoke(null, args) as IQueryable; + resultsLimited = (bool)args[3]; + return results; + } - if (applySortOptions != null) - { - var propertyPathsToAdd = applySortOptions.Where(p => !usedPropertyNames.Contains(p)).OrderBy(p => p); - if (propertyPathsToAdd.Any()) - { - var orderByRaw = orderBy.RawValue + "," + String.Join(",", propertyPathsToAdd); - orderBy = new OrderByQueryOption(orderByRaw, context, Apply.RawValue); - } - } - else - { - IEnumerable propertiesToAdd = GetAvailableOrderByProperties(context).Where(prop => !usedPropertyNames.Contains(prop.Name)); - if (propertiesToAdd.Any()) - { - // The existing query options has too few properties to create a stable sort. - // Clone the given one and add the remaining properties to end, thereby making - // the sort stable but preserving the user's original intent for the major - // sort order. - var orderByRaw = orderBy.RawValue + "," + string.Join(",", propertiesToAdd.Select(p => p.Name)); - orderBy = new OrderByQueryOption(orderByRaw, context); - } - } + /// + /// Limits the query results to a maximum number of results. + /// + /// The entity CLR type + /// The queryable to limit. + /// The query result limit. + /// true if the query results were limited; false otherwise + /// The limited query results. + public static IQueryable LimitResults(IQueryable queryable, int limit, out bool resultsLimited) + { + return LimitResults(queryable, limit, false, out resultsLimited); + } - return orderBy; - } + /// + /// Limits the query results to a maximum number of results. + /// + /// The entity CLR type + /// The queryable to limit. + /// The query result limit. + /// Flag indicating whether constants should be parameterized + /// true if the query results were limited; false otherwise + /// The limited query results. + public static IQueryable LimitResults(IQueryable queryable, int limit, bool parameterize, out bool resultsLimited) + { + TruncatedCollection truncatedCollection = new TruncatedCollection(queryable, limit, parameterize); + resultsLimited = truncatedCollection.IsTruncated; + return truncatedCollection.AsQueryable(); + } - internal static IQueryable LimitResults(IQueryable queryable, int limit, bool parameterize, out bool resultsLimited) + internal void AddAutoSelectExpandProperties() + { + bool containsAutoSelectExpandProperties = false; + var autoExpandRawValue = GetAutoExpandRawValue(); + var autoSelectRawValue = GetAutoSelectRawValue(); + + IDictionary queryParameters = GetODataQueryParameters(); + + if (!String.IsNullOrEmpty(autoExpandRawValue) && !autoExpandRawValue.Equals(RawValues.Expand, StringComparison.Ordinal)) { - MethodInfo genericMethod = _limitResultsGenericMethod.MakeGenericMethod(queryable.ElementType); - object[] args = new object[] { queryable, limit, parameterize, null }; - IQueryable results = genericMethod.Invoke(null, args) as IQueryable; - resultsLimited = (bool)args[3]; - return results; + queryParameters["$expand"] = autoExpandRawValue; + containsAutoSelectExpandProperties = true; } - - /// - /// Limits the query results to a maximum number of results. - /// - /// The entity CLR type - /// The queryable to limit. - /// The query result limit. - /// true if the query results were limited; false otherwise - /// The limited query results. - public static IQueryable LimitResults(IQueryable queryable, int limit, out bool resultsLimited) + else { - return LimitResults(queryable, limit, false, out resultsLimited); + autoExpandRawValue = RawValues.Expand; } - /// - /// Limits the query results to a maximum number of results. - /// - /// The entity CLR type - /// The queryable to limit. - /// The query result limit. - /// Flag indicating whether constants should be parameterized - /// true if the query results were limited; false otherwise - /// The limited query results. - public static IQueryable LimitResults(IQueryable queryable, int limit, bool parameterize, out bool resultsLimited) + if (!String.IsNullOrEmpty(autoSelectRawValue) && !autoSelectRawValue.Equals(RawValues.Select, StringComparison.Ordinal)) { - TruncatedCollection truncatedCollection = new TruncatedCollection(queryable, limit, parameterize); - resultsLimited = truncatedCollection.IsTruncated; - return truncatedCollection.AsQueryable(); + queryParameters["$select"] = autoSelectRawValue; + containsAutoSelectExpandProperties = true; } - - internal void AddAutoSelectExpandProperties() + else { - bool containsAutoSelectExpandProperties = false; - var autoExpandRawValue = GetAutoExpandRawValue(); - var autoSelectRawValue = GetAutoSelectRawValue(); + autoSelectRawValue = RawValues.Select; + } - IDictionary queryParameters = GetODataQueryParameters(); + if (containsAutoSelectExpandProperties) + { + _queryOptionParser = new ODataQueryOptionParser( + Context.Model, + Context.ElementType, + Context.NavigationSource, + queryParameters, + Context.RequestContainer); // the Context.RequestContainer could be null for non-edm model - if (!String.IsNullOrEmpty(autoExpandRawValue) && !autoExpandRawValue.Equals(RawValues.Expand, StringComparison.Ordinal)) - { - queryParameters["$expand"] = autoExpandRawValue; - containsAutoSelectExpandProperties = true; - } - else + if (Context.RequestContainer == null) { - autoExpandRawValue = RawValues.Expand; + // By default, let's enable the property name case-insensitive + _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; } - if (!String.IsNullOrEmpty(autoSelectRawValue) && !autoSelectRawValue.Equals(RawValues.Select, StringComparison.Ordinal)) + var originalSelectExpand = SelectExpand; + SelectExpand = new SelectExpandQueryOption( + autoSelectRawValue, + autoExpandRawValue, + Context, + _queryOptionParser); + if (originalSelectExpand != null && originalSelectExpand.LevelsMaxLiteralExpansionDepth > 0) { - queryParameters["$select"] = autoSelectRawValue; - containsAutoSelectExpandProperties = true; + SelectExpand.LevelsMaxLiteralExpansionDepth = originalSelectExpand.LevelsMaxLiteralExpansionDepth; } - else + else if (Context.ValidationSettings != null && Context.ValidationSettings.MaxExpansionDepth > 0) { - autoSelectRawValue = RawValues.Select; - } - - if (containsAutoSelectExpandProperties) - { - _queryOptionParser = new ODataQueryOptionParser( - Context.Model, - Context.ElementType, - Context.NavigationSource, - queryParameters, - Context.RequestContainer); // the Context.RequestContainer could be null for non-edm model - - if (Context.RequestContainer == null) - { - // By default, let's enable the property name case-insensitive - _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; - } - - var originalSelectExpand = SelectExpand; - SelectExpand = new SelectExpandQueryOption( - autoSelectRawValue, - autoExpandRawValue, - Context, - _queryOptionParser); - if (originalSelectExpand != null && originalSelectExpand.LevelsMaxLiteralExpansionDepth > 0) - { - SelectExpand.LevelsMaxLiteralExpansionDepth = originalSelectExpand.LevelsMaxLiteralExpansionDepth; - } - else if (Context.ValidationSettings != null && Context.ValidationSettings.MaxExpansionDepth > 0) - { - SelectExpand.LevelsMaxLiteralExpansionDepth = Context.ValidationSettings.MaxExpansionDepth; - } + SelectExpand.LevelsMaxLiteralExpansionDepth = Context.ValidationSettings.MaxExpansionDepth; } } + } - private IDictionary GetODataQueryParameters() - { - Dictionary result = new Dictionary(); + private IDictionary GetODataQueryParameters() + { + Dictionary result = new Dictionary(); - foreach (var query in Request.Query) + foreach (var query in Request.Query) + { + string key = query.Key.Trim(); + string value = query.Value.ToString(); + // Check supported system query options per $-sign-prefix option. + if (!_enableNoDollarSignQueryOptions) { - string key = query.Key.Trim(); - string value = query.Value.ToString(); - // Check supported system query options per $-sign-prefix option. - if (!_enableNoDollarSignQueryOptions) - { - // This is the original case for required $-sign prefix. - if (key.StartsWith("$", StringComparison.Ordinal)) - { - result.Add(key, value); - } - } - else - { - if (IsSupportedQueryOption(key)) - { - // Normalized the supported system query key by adding the $-prefix if needed. - result.Add( - !key.StartsWith("$", StringComparison.Ordinal) ? "$" + key : key, - value); - } - } - - // check parameter alias - if (key.StartsWith("@", StringComparison.Ordinal)) + // This is the original case for required $-sign prefix. + if (key.StartsWith("$", StringComparison.Ordinal)) { result.Add(key, value); } } - - return result; - } - - private string GetAutoSelectRawValue() - { - var selectRawValue = RawValues.Select; - if (String.IsNullOrEmpty(selectRawValue)) + else { - IList autoSelectProperties = null; - - if (Context.TargetStructuredType != null && Context.TargetProperty != null) - { - autoSelectProperties = Context.Model.GetAutoSelectPaths(Context.TargetStructuredType, Context.TargetProperty); - } - else if (Context.ElementType is IEdmStructuredType structuredType) + if (IsSupportedQueryOption(key)) { - autoSelectProperties = Context.Model.GetAutoSelectPaths(structuredType, null); - } - - string autoSelectRawValue = autoSelectProperties == null ? null : string.Join(",", autoSelectProperties.Select(a => a.SelectPath)); - - // TODO: so far, it's ok to duplicate the select property in one $select from ODL. - // But, it's better to remove the duplicate select property. - if (!String.IsNullOrEmpty(autoSelectRawValue)) - { - if (!String.IsNullOrEmpty(selectRawValue)) - { - selectRawValue = String.Format(CultureInfo.InvariantCulture, "{0},{1}", - autoSelectRawValue, selectRawValue); - } - else - { - selectRawValue = autoSelectRawValue; - } + // Normalized the supported system query key by adding the $-prefix if needed. + result.Add( + !key.StartsWith("$", StringComparison.Ordinal) ? "$" + key : key, + value); } } - return selectRawValue; + // check parameter alias + if (key.StartsWith("@", StringComparison.Ordinal)) + { + result.Add(key, value); + } } - private string GetAutoExpandRawValue() + return result; + } + + private string GetAutoSelectRawValue() + { + var selectRawValue = RawValues.Select; + if (String.IsNullOrEmpty(selectRawValue)) { - var expandRawValue = RawValues.Expand; + IList autoSelectProperties = null; - IList autoExpandNavigationProperties = null; if (Context.TargetStructuredType != null && Context.TargetProperty != null) { - autoExpandNavigationProperties = Context.Model.GetAutoExpandPaths(Context.TargetStructuredType, Context.TargetProperty, !string.IsNullOrEmpty(RawValues.Select)); + autoSelectProperties = Context.Model.GetAutoSelectPaths(Context.TargetStructuredType, Context.TargetProperty); } - else if (Context.ElementType is IEdmStructuredType elementType) + else if (Context.ElementType is IEdmStructuredType structuredType) { - autoExpandNavigationProperties = Context.Model.GetAutoExpandPaths(elementType, null, !string.IsNullOrEmpty(RawValues.Select)); + autoSelectProperties = Context.Model.GetAutoSelectPaths(structuredType, null); } - string autoExpandRawValue = autoExpandNavigationProperties == null ? null : string.Join(",", autoExpandNavigationProperties.Select(c => c.ExpandPath)); + string autoSelectRawValue = autoSelectProperties == null ? null : string.Join(",", autoSelectProperties.Select(a => a.SelectPath)); - // TODO: so far, it's ok to duplicate the expand property in one $expand from ODL. - // But, it's better to remove the duplicate expand property. - if (!string.IsNullOrEmpty(autoExpandRawValue)) + // TODO: so far, it's ok to duplicate the select property in one $select from ODL. + // But, it's better to remove the duplicate select property. + if (!String.IsNullOrEmpty(autoSelectRawValue)) { - if (!string.IsNullOrEmpty(expandRawValue)) + if (!String.IsNullOrEmpty(selectRawValue)) { - expandRawValue = string.Format(CultureInfo.InvariantCulture, "{0},{1}", - autoExpandRawValue, expandRawValue); + selectRawValue = String.Format(CultureInfo.InvariantCulture, "{0},{1}", + autoSelectRawValue, selectRawValue); } else { - expandRawValue = autoExpandRawValue; + selectRawValue = autoSelectRawValue; } } + } + + return selectRawValue; + } + + private string GetAutoExpandRawValue() + { + var expandRawValue = RawValues.Expand; - return expandRawValue; + IList autoExpandNavigationProperties = null; + if (Context.TargetStructuredType != null && Context.TargetProperty != null) + { + autoExpandNavigationProperties = Context.Model.GetAutoExpandPaths(Context.TargetStructuredType, Context.TargetProperty, !string.IsNullOrEmpty(RawValues.Select)); + } + else if (Context.ElementType is IEdmStructuredType elementType) + { + autoExpandNavigationProperties = Context.Model.GetAutoExpandPaths(elementType, null, !string.IsNullOrEmpty(RawValues.Select)); } - [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", - Justification = "Need lower case string here.")] - private void BuildQueryOptions(IDictionary queryParameters) + string autoExpandRawValue = autoExpandNavigationProperties == null ? null : string.Join(",", autoExpandNavigationProperties.Select(c => c.ExpandPath)); + + // TODO: so far, it's ok to duplicate the expand property in one $expand from ODL. + // But, it's better to remove the duplicate expand property. + if (!string.IsNullOrEmpty(autoExpandRawValue)) { - foreach (KeyValuePair kvp in queryParameters) + if (!string.IsNullOrEmpty(expandRawValue)) { - switch (kvp.Key.ToLowerInvariant()) - { - case "$filter": - ThrowIfEmpty(kvp.Value, "$filter"); - RawValues.Filter = kvp.Value; - Filter = new FilterQueryOption(kvp.Value, Context, _queryOptionParser); - break; - case "$orderby": - ThrowIfEmpty(kvp.Value, "$orderby"); - RawValues.OrderBy = kvp.Value; - OrderBy = new OrderByQueryOption(kvp.Value, Context, _queryOptionParser); - break; - case "$top": - ThrowIfEmpty(kvp.Value, "$top"); - RawValues.Top = kvp.Value; - Top = new TopQueryOption(kvp.Value, Context, _queryOptionParser); - break; - case "$skip": - ThrowIfEmpty(kvp.Value, "$skip"); - RawValues.Skip = kvp.Value; - Skip = new SkipQueryOption(kvp.Value, Context, _queryOptionParser); - break; - case "$select": - RawValues.Select = kvp.Value; - break; - case "$count": - ThrowIfEmpty(kvp.Value, "$count"); - RawValues.Count = kvp.Value; - Count = new CountQueryOption(kvp.Value, Context, _queryOptionParser); - break; - case "$expand": - RawValues.Expand = kvp.Value; - break; - case "$format": - RawValues.Format = kvp.Value; - break; - case "$skiptoken": - RawValues.SkipToken = kvp.Value; - SkipToken = new SkipTokenQueryOption(kvp.Value, Context); - break; - case "$deltatoken": - RawValues.DeltaToken = kvp.Value; - break; - case "$apply": - ThrowIfEmpty(kvp.Value, "$apply"); - RawValues.Apply = kvp.Value; - Apply = new ApplyQueryOption(kvp.Value, Context, _queryOptionParser); - break; - case "$compute": - ThrowIfEmpty(kvp.Value, "$compute"); - RawValues.Compute = kvp.Value; - Compute = new ComputeQueryOption(kvp.Value, Context, _queryOptionParser); - break; - case "$search": - ThrowIfEmpty(kvp.Value, "$search"); - RawValues.Search = kvp.Value; - Search = new SearchQueryOption(kvp.Value, Context, _queryOptionParser); - break; - default: - // we don't throw if we can't recognize the query - break; - } + expandRawValue = string.Format(CultureInfo.InvariantCulture, "{0},{1}", + autoExpandRawValue, expandRawValue); } - - if (!string.IsNullOrWhiteSpace(RawValues.Select) || !string.IsNullOrWhiteSpace(RawValues.Expand)) + else { - SelectExpand = new SelectExpandQueryOption(RawValues.Select, RawValues.Expand, - Context, _queryOptionParser); + expandRawValue = autoExpandRawValue; } + } - if (Request.IsCountRequest()) - { - Count = new CountQueryOption( - "true", - Context, - new ODataQueryOptionParser( - Context.Model, - Context.ElementType, - Context.NavigationSource, - new Dictionary { { "$count", "true" } }, - Context.RequestContainer)); + return expandRawValue; + } + + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", + Justification = "Need lower case string here.")] + private void BuildQueryOptions(IDictionary queryParameters) + { + foreach (KeyValuePair kvp in queryParameters) + { + switch (kvp.Key.ToLowerInvariant()) + { + case "$filter": + ThrowIfEmpty(kvp.Value, "$filter"); + RawValues.Filter = kvp.Value; + Filter = new FilterQueryOption(kvp.Value, Context, _queryOptionParser); + break; + case "$orderby": + ThrowIfEmpty(kvp.Value, "$orderby"); + RawValues.OrderBy = kvp.Value; + OrderBy = new OrderByQueryOption(kvp.Value, Context, _queryOptionParser); + break; + case "$top": + ThrowIfEmpty(kvp.Value, "$top"); + RawValues.Top = kvp.Value; + Top = new TopQueryOption(kvp.Value, Context, _queryOptionParser); + break; + case "$skip": + ThrowIfEmpty(kvp.Value, "$skip"); + RawValues.Skip = kvp.Value; + Skip = new SkipQueryOption(kvp.Value, Context, _queryOptionParser); + break; + case "$select": + RawValues.Select = kvp.Value; + break; + case "$count": + ThrowIfEmpty(kvp.Value, "$count"); + RawValues.Count = kvp.Value; + Count = new CountQueryOption(kvp.Value, Context, _queryOptionParser); + break; + case "$expand": + RawValues.Expand = kvp.Value; + break; + case "$format": + RawValues.Format = kvp.Value; + break; + case "$skiptoken": + RawValues.SkipToken = kvp.Value; + SkipToken = new SkipTokenQueryOption(kvp.Value, Context); + break; + case "$deltatoken": + RawValues.DeltaToken = kvp.Value; + break; + case "$apply": + ThrowIfEmpty(kvp.Value, "$apply"); + RawValues.Apply = kvp.Value; + Apply = new ApplyQueryOption(kvp.Value, Context, _queryOptionParser); + break; + case "$compute": + ThrowIfEmpty(kvp.Value, "$compute"); + RawValues.Compute = kvp.Value; + Compute = new ComputeQueryOption(kvp.Value, Context, _queryOptionParser); + break; + case "$search": + ThrowIfEmpty(kvp.Value, "$search"); + RawValues.Search = kvp.Value; + Search = new SearchQueryOption(kvp.Value, Context, _queryOptionParser); + break; + default: + // we don't throw if we can't recognize the query + break; } } - private static bool IsAvailableODataQueryOption(object queryOption, ODataQuerySettings querySettings, AllowedQueryOptions queryOptionFlag) + if (!string.IsNullOrWhiteSpace(RawValues.Select) || !string.IsNullOrWhiteSpace(RawValues.Expand)) { - return (queryOption != null) && - ((querySettings.IgnoredQueryOptions & queryOptionFlag) == AllowedQueryOptions.None); + SelectExpand = new SelectExpandQueryOption(RawValues.Select, RawValues.Expand, + Context, _queryOptionParser); } - private T ApplySelectExpand(T entity, ODataQuerySettings querySettings) + if (Request.IsCountRequest()) { - var result = default(T); - bool computeAvailable = IsAvailableODataQueryOption(Compute?.RawValue, querySettings, AllowedQueryOptions.Compute); - bool selectAvailable = IsAvailableODataQueryOption(SelectExpand.RawSelect, querySettings, AllowedQueryOptions.Select); - bool expandAvailable = IsAvailableODataQueryOption(SelectExpand.RawExpand, querySettings, AllowedQueryOptions.Expand); - if (selectAvailable || expandAvailable) - { - if ((!selectAvailable && SelectExpand.RawSelect != null) || - (!expandAvailable && SelectExpand.RawExpand != null)) - { - SelectExpand = new SelectExpandQueryOption( - selectAvailable ? RawValues.Select : null, - expandAvailable ? RawValues.Expand : null, - SelectExpand.Context); - } + Count = new CountQueryOption( + "true", + Context, + new ODataQueryOptionParser( + Context.Model, + Context.ElementType, + Context.NavigationSource, + new Dictionary { { "$count", "true" } }, + Context.RequestContainer)); + } + } - SelectExpandClause processedClause = SelectExpand.ProcessedSelectExpandClause; - SelectExpandQueryOption newSelectExpand = new SelectExpandQueryOption( - SelectExpand.RawSelect, - SelectExpand.RawExpand, - SelectExpand.Context, - processedClause); + private static bool IsAvailableODataQueryOption(object queryOption, ODataQuerySettings querySettings, AllowedQueryOptions queryOptionFlag) + { + return (queryOption != null) && + ((querySettings.IgnoredQueryOptions & queryOptionFlag) == AllowedQueryOptions.None); + } - Request.ODataFeature().SelectExpandClause = processedClause; - (Request.ODataFeature() as ODataFeature).QueryOptions = this; + private T ApplySelectExpand(T entity, ODataQuerySettings querySettings) + { + var result = default(T); + bool computeAvailable = IsAvailableODataQueryOption(Compute?.RawValue, querySettings, AllowedQueryOptions.Compute); + bool selectAvailable = IsAvailableODataQueryOption(SelectExpand.RawSelect, querySettings, AllowedQueryOptions.Select); + bool expandAvailable = IsAvailableODataQueryOption(SelectExpand.RawExpand, querySettings, AllowedQueryOptions.Expand); + if (selectAvailable || expandAvailable) + { + if ((!selectAvailable && SelectExpand.RawSelect != null) || + (!expandAvailable && SelectExpand.RawExpand != null)) + { + SelectExpand = new SelectExpandQueryOption( + selectAvailable ? RawValues.Select : null, + expandAvailable ? RawValues.Expand : null, + SelectExpand.Context); + } - if (computeAvailable) - { - newSelectExpand.Compute = Compute; - } + SelectExpandClause processedClause = SelectExpand.ProcessedSelectExpandClause; + SelectExpandQueryOption newSelectExpand = new SelectExpandQueryOption( + SelectExpand.RawSelect, + SelectExpand.RawExpand, + SelectExpand.Context, + processedClause); - if (Context.DefaultQueryConfigurations.EnableSkipToken && OrderBy != null) - { - newSelectExpand.OrderBy = OrderBy; - } + Request.ODataFeature().SelectExpandClause = processedClause; + (Request.ODataFeature() as ODataFeature).QueryOptions = this; - var type = typeof(T); - if (type == typeof(IQueryable)) - { - result = (T)newSelectExpand.ApplyTo((IQueryable)entity, querySettings); - } - else if (type == typeof(object)) - { - result = (T)newSelectExpand.ApplyTo(entity, querySettings); - } + if (computeAvailable) + { + newSelectExpand.Compute = Compute; } - return result; - } - - /// - /// Initializes a new instance of the class based on the incoming request and some metadata information from - /// the . - /// - /// The which contains the and some type information. - private void Initialize(ODataQueryContext context) - { - Contract.Assert(context != null); - - ODataUriResolver uriResolver = null; - if (context.RequestContainer != null) + if (Context.DefaultQueryConfigurations.EnableSkipToken && OrderBy != null) { - uriResolver = context.RequestContainer.GetService(); + newSelectExpand.OrderBy = OrderBy; } - if (uriResolver != null) + var type = typeof(T); + if (type == typeof(IQueryable)) { - _enableNoDollarSignQueryOptions = uriResolver.EnableNoDollarQueryOptions; + result = (T)newSelectExpand.ApplyTo((IQueryable)entity, querySettings); } - else + else if (type == typeof(object)) { - // Use the global setting - _enableNoDollarSignQueryOptions = context.Request.IsNoDollarQueryEnable(); + result = (T)newSelectExpand.ApplyTo(entity, querySettings); } + } - // Parse the query from request Uri, including only keys which are OData query parameters or parameter alias - // OData query parameters are normalized with the $-sign prefixes when the - // EnableNoDollarSignPrefixSystemQueryOption option is used. - RawValues = new ODataRawQueryOptions(); - IDictionary normalizedQueryParameters = GetODataQueryParameters(); + return result; + } - _queryOptionParser = new ODataQueryOptionParser( - context.Model, - context.ElementType, - context.NavigationSource, - normalizedQueryParameters); + /// + /// Initializes a new instance of the class based on the incoming request and some metadata information from + /// the . + /// + /// The which contains the and some type information. + private void Initialize(ODataQueryContext context) + { + Contract.Assert(context != null); - if (uriResolver != null) - { - _queryOptionParser.Resolver = uriResolver; - } - else - { - // By default, let's enable the property name case-insensitive - _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; - } + ODataUriResolver uriResolver = null; + if (context.RequestContainer != null) + { + uriResolver = context.RequestContainer.GetService(); + } + + if (uriResolver != null) + { + _enableNoDollarSignQueryOptions = uriResolver.EnableNoDollarQueryOptions; + } + else + { + // Use the global setting + _enableNoDollarSignQueryOptions = context.Request.IsNoDollarQueryEnable(); + } + + // Parse the query from request Uri, including only keys which are OData query parameters or parameter alias + // OData query parameters are normalized with the $-sign prefixes when the + // EnableNoDollarSignPrefixSystemQueryOption option is used. + RawValues = new ODataRawQueryOptions(); + IDictionary normalizedQueryParameters = GetODataQueryParameters(); - BuildQueryOptions(normalizedQueryParameters); + _queryOptionParser = new ODataQueryOptionParser( + context.Model, + context.ElementType, + context.NavigationSource, + normalizedQueryParameters); - Validator = context.GetODataQueryValidator(); + if (uriResolver != null) + { + _queryOptionParser.Resolver = uriResolver; } + else + { + // By default, let's enable the property name case-insensitive + _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; + } + + BuildQueryOptions(normalizedQueryParameters); + + Validator = context.GetODataQueryValidator(); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptionsOfT.cs b/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptionsOfT.cs index b19bcbce9..db5971a55 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptionsOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptionsOfT.cs @@ -11,101 +11,100 @@ using Microsoft.Net.Http.Headers; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This defines a composite OData query options that can be used to perform query composition. +/// Currently this only supports $filter, $orderby, $top, $skip. +/// +[ODataQueryParameterBinding] +public class ODataQueryOptions : ODataQueryOptions { /// - /// This defines a composite OData query options that can be used to perform query composition. - /// Currently this only supports $filter, $orderby, $top, $skip. + /// Initializes a new instance of the class based on the incoming request and some metadata information from + /// the . /// - [ODataQueryParameterBinding] - public class ODataQueryOptions : ODataQueryOptions + /// The which contains the and some type information + /// The incoming request message + /// This signature uses types that are AspNetCore-specific. + public ODataQueryOptions(ODataQueryContext context, HttpRequest request) + : base(context, request) { - /// - /// Initializes a new instance of the class based on the incoming request and some metadata information from - /// the . - /// - /// The which contains the and some type information - /// The incoming request message - /// This signature uses types that are AspNetCore-specific. - public ODataQueryOptions(ODataQueryContext context, HttpRequest request) - : base(context, request) + if (Context.ElementClrType == null) { - if (Context.ElementClrType == null) - { - throw Error.Argument("context", SRResources.ElementClrTypeNull, typeof(ODataQueryContext).Name); - } - - if (context.ElementClrType != typeof(TEntity)) - { - throw Error.Argument("context", SRResources.EntityTypeMismatch, context.ElementClrType.FullName, typeof(TEntity).FullName); - } + throw Error.Argument("context", SRResources.ElementClrTypeNull, typeof(ODataQueryContext).Name); } - /// - /// Gets the from IfMatch header. - /// - public new ETag IfMatch + if (context.ElementClrType != typeof(TEntity)) { - get - { - return base.IfMatch as ETag; - } + throw Error.Argument("context", SRResources.EntityTypeMismatch, context.ElementClrType.FullName, typeof(TEntity).FullName); } + } - /// - /// Gets the from IfNoneMatch header. - /// - public new ETag IfNoneMatch + /// + /// Gets the from IfMatch header. + /// + public new ETag IfMatch + { + get { - get - { - return base.IfNoneMatch as ETag; - } + return base.IfMatch as ETag; } + } - /// - /// Gets the EntityTagHeaderValue ETag>. - /// - /// This signature uses types that are AspNetCore-specific. - internal override ETag GetETag(EntityTagHeaderValue etagHeaderValue) + /// + /// Gets the from IfNoneMatch header. + /// + public new ETag IfNoneMatch + { + get { - return Request.GetETag(etagHeaderValue); + return base.IfNoneMatch as ETag; } + } - /// - /// Apply the individual query to the given IQueryable in the right order. - /// - /// The original . - /// The new after the query has been applied to. - public override IQueryable ApplyTo(IQueryable query) - { - ValidateQuery(query); - return base.ApplyTo(query); - } + /// + /// Gets the EntityTagHeaderValue ETag>. + /// + /// This signature uses types that are AspNetCore-specific. + internal override ETag GetETag(EntityTagHeaderValue etagHeaderValue) + { + return Request.GetETag(etagHeaderValue); + } + + /// + /// Apply the individual query to the given IQueryable in the right order. + /// + /// The original . + /// The new after the query has been applied to. + public override IQueryable ApplyTo(IQueryable query) + { + ValidateQuery(query); + return base.ApplyTo(query); + } - /// - /// Apply the individual query to the given IQueryable in the right order. - /// - /// The original . - /// The settings to use in query composition. - /// The new after the query has been applied to. - public override IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + /// + /// Apply the individual query to the given IQueryable in the right order. + /// + /// The original . + /// The settings to use in query composition. + /// The new after the query has been applied to. + public override IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + { + ValidateQuery(query); + return base.ApplyTo(query, querySettings); + } + + private static void ValidateQuery(IQueryable query) + { + if (query == null) { - ValidateQuery(query); - return base.ApplyTo(query, querySettings); + throw Error.ArgumentNull("query"); } - private static void ValidateQuery(IQueryable query) + if (!TypeHelper.IsTypeAssignableFrom(typeof(TEntity), query.ElementType)) { - if (query == null) - { - throw Error.ArgumentNull("query"); - } - - if (!TypeHelper.IsTypeAssignableFrom(typeof(TEntity), query.ElementType)) - { - throw Error.Argument("query", SRResources.CannotApplyODataQueryOptionsOfT, typeof(ODataQueryOptions).Name, typeof(TEntity).FullName, typeof(IQueryable).Name, query.ElementType.FullName); - } + throw Error.Argument("query", SRResources.CannotApplyODataQueryOptionsOfT, typeof(ODataQueryOptions).Name, typeof(TEntity).FullName, typeof(IQueryable).Name, query.ElementType.FullName); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/ODataQueryParameterBindingAttribute.cs b/src/Microsoft.AspNetCore.OData/Query/ODataQueryParameterBindingAttribute.cs index c1cc6710d..5810adda6 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ODataQueryParameterBindingAttribute.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ODataQueryParameterBindingAttribute.cs @@ -19,159 +19,158 @@ using Microsoft.AspNetCore.OData.Extensions; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// A to bind parameters of type to the OData query from the incoming request. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)] +public sealed class ODataQueryParameterBindingAttribute : ModelBinderAttribute { /// - /// A to bind parameters of type to the OData query from the incoming request. + /// Instantiates a new instance of the class. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)] - public sealed class ODataQueryParameterBindingAttribute : ModelBinderAttribute + public ODataQueryParameterBindingAttribute() + : base(typeof(ODataQueryParameterBinding)) + { + } + + internal static Type GetEntityClrTypeFromParameterType(Type parameterType) { - /// - /// Instantiates a new instance of the class. - /// - public ODataQueryParameterBindingAttribute() - : base(typeof(ODataQueryParameterBinding)) + Contract.Assert(parameterType != null); + + if (parameterType.IsGenericType && + parameterType.GetGenericTypeDefinition() == typeof(ODataQueryOptions<>)) { + return parameterType.GetGenericArguments().Single(); } - internal static Type GetEntityClrTypeFromParameterType(Type parameterType) + return null; + } + + internal class ODataQueryParameterBinding : IModelBinder + { + private static MethodInfo _createODataQueryOptions = typeof(ODataQueryParameterBinding).GetMethod("CreateODataQueryOptions"); + private const string CreateODataQueryOptionsCtorKey = "MS_CreateODataQueryOptionsOfT"; + + public Task BindModelAsync(ModelBindingContext bindingContext) { - Contract.Assert(parameterType != null); + if (bindingContext == null) + { + throw Error.ArgumentNull(nameof(bindingContext)); + } - if (parameterType.IsGenericType && - parameterType.GetGenericTypeDefinition() == typeof(ODataQueryOptions<>)) + HttpRequest request = bindingContext.HttpContext.Request; + if (request == null) { - return parameterType.GetGenericArguments().Single(); + throw Error.Argument("bindingContext", SRResources.ModelBindingContextMustHaveRequest); } - return null; - } + ActionDescriptor actionDescriptor = bindingContext.ActionContext.ActionDescriptor; + if (actionDescriptor == null) + { + throw Error.Argument("actionContext", SRResources.ActionContextMustHaveDescriptor); + } - internal class ODataQueryParameterBinding : IModelBinder - { - private static MethodInfo _createODataQueryOptions = typeof(ODataQueryParameterBinding).GetMethod("CreateODataQueryOptions"); - private const string CreateODataQueryOptionsCtorKey = "MS_CreateODataQueryOptionsOfT"; + // Get the parameter description of the parameter to bind. + ParameterDescriptor paramDescriptor = bindingContext.ActionContext.ActionDescriptor.Parameters + .Where(p => p.Name == bindingContext.FieldName) + .FirstOrDefault(); - public Task BindModelAsync(ModelBindingContext bindingContext) + // Now make sure parameter type is ODataQueryOptions or ODataQueryOptions. + Type parameterType = paramDescriptor?.ParameterType; + if (IsODataQueryOptions(parameterType)) { - if (bindingContext == null) - { - throw Error.ArgumentNull(nameof(bindingContext)); - } - - HttpRequest request = bindingContext.HttpContext.Request; - if (request == null) + // Get the entity type from the parameter type if it is ODataQueryOptions. + // Fall back to the return type if not. Also, note that the entity type from the return type and ODataQueryOptions + // can be different (example implementing $select or $expand). + Type entityClrType = null; + if (paramDescriptor != null) { - throw Error.Argument("bindingContext", SRResources.ModelBindingContextMustHaveRequest); + entityClrType = GetEntityClrTypeFromParameterType(parameterType); } - ActionDescriptor actionDescriptor = bindingContext.ActionContext.ActionDescriptor; - if (actionDescriptor == null) + if (entityClrType == null) { - throw Error.Argument("actionContext", SRResources.ActionContextMustHaveDescriptor); + entityClrType = GetEntityClrTypeFromActionReturnType(actionDescriptor); } - // Get the parameter description of the parameter to bind. - ParameterDescriptor paramDescriptor = bindingContext.ActionContext.ActionDescriptor.Parameters - .Where(p => p.Name == bindingContext.FieldName) - .FirstOrDefault(); + // In 7.x, GetModel() from request will return "EdmCoreModel.Instance" for non-model scenario + // In 8.x, GetModel() return null. + IEdmModel userModel = request.GetModel(); + IEdmModel model = userModel ?? actionDescriptor.GetEdmModel(request, entityClrType); + ODataQueryContext entitySetContext = new ODataQueryContext(model, entityClrType, request.ODataFeature().Path); - // Now make sure parameter type is ODataQueryOptions or ODataQueryOptions. - Type parameterType = paramDescriptor?.ParameterType; - if (IsODataQueryOptions(parameterType)) + Func createODataQueryOptions; + object constructorAsObject = null; + if (actionDescriptor.Properties.TryGetValue(CreateODataQueryOptionsCtorKey, out constructorAsObject)) { - // Get the entity type from the parameter type if it is ODataQueryOptions. - // Fall back to the return type if not. Also, note that the entity type from the return type and ODataQueryOptions - // can be different (example implementing $select or $expand). - Type entityClrType = null; - if (paramDescriptor != null) - { - entityClrType = GetEntityClrTypeFromParameterType(parameterType); - } - - if (entityClrType == null) - { - entityClrType = GetEntityClrTypeFromActionReturnType(actionDescriptor); - } - - // In 7.x, GetModel() from request will return "EdmCoreModel.Instance" for non-model scenario - // In 8.x, GetModel() return null. - IEdmModel userModel = request.GetModel(); - IEdmModel model = userModel ?? actionDescriptor.GetEdmModel(request, entityClrType); - ODataQueryContext entitySetContext = new ODataQueryContext(model, entityClrType, request.ODataFeature().Path); - - Func createODataQueryOptions; - object constructorAsObject = null; - if (actionDescriptor.Properties.TryGetValue(CreateODataQueryOptionsCtorKey, out constructorAsObject)) - { - createODataQueryOptions = (Func)constructorAsObject; - } - else - { - createODataQueryOptions = (Func) - Delegate.CreateDelegate(typeof(Func), - _createODataQueryOptions.MakeGenericMethod(entityClrType)); - }; - - ODataQueryOptions parameterValue = createODataQueryOptions(entitySetContext, request); - bindingContext.Result = ModelBindingResult.Success(parameterValue); + createODataQueryOptions = (Func)constructorAsObject; } + else + { + createODataQueryOptions = (Func) + Delegate.CreateDelegate(typeof(Func), + _createODataQueryOptions.MakeGenericMethod(entityClrType)); + }; - return Task.CompletedTask; + ODataQueryOptions parameterValue = createODataQueryOptions(entitySetContext, request); + bindingContext.Result = ModelBindingResult.Success(parameterValue); } - public static bool IsODataQueryOptions(Type parameterType) - { - if (parameterType == null) - { - return false; - } + return Task.CompletedTask; + } - return ((parameterType == typeof(ODataQueryOptions)) || - (parameterType.IsGenericType && - parameterType.GetGenericTypeDefinition() == typeof(ODataQueryOptions<>))); + public static bool IsODataQueryOptions(Type parameterType) + { + if (parameterType == null) + { + return false; } - public static ODataQueryOptions CreateODataQueryOptions(ODataQueryContext context, HttpRequest request) + return ((parameterType == typeof(ODataQueryOptions)) || + (parameterType.IsGenericType && + parameterType.GetGenericTypeDefinition() == typeof(ODataQueryOptions<>))); + } + + public static ODataQueryOptions CreateODataQueryOptions(ODataQueryContext context, HttpRequest request) + { + return new ODataQueryOptions(context, request); + } + + internal static Type GetEntityClrTypeFromActionReturnType(ActionDescriptor actionDescriptor) + { + // It is a developer programming error to use this binding attribute + // on actions that return void. + ControllerActionDescriptor controllerActionDescriptor = actionDescriptor as ControllerActionDescriptor; + if (controllerActionDescriptor == null) { - return new ODataQueryOptions(context, request); + throw Error.InvalidOperation(SRResources.ActionDescriptorNotControllerActionDescriptor); } - internal static Type GetEntityClrTypeFromActionReturnType(ActionDescriptor actionDescriptor) + if (controllerActionDescriptor.MethodInfo.ReturnType == null) { - // It is a developer programming error to use this binding attribute - // on actions that return void. - ControllerActionDescriptor controllerActionDescriptor = actionDescriptor as ControllerActionDescriptor; - if (controllerActionDescriptor == null) - { - throw Error.InvalidOperation(SRResources.ActionDescriptorNotControllerActionDescriptor); - } - - if (controllerActionDescriptor.MethodInfo.ReturnType == null) - { - throw Error.InvalidOperation( - SRResources.FailedToBuildEdmModelBecauseReturnTypeIsNull, - controllerActionDescriptor.ActionName, - controllerActionDescriptor.ControllerName); - } + throw Error.InvalidOperation( + SRResources.FailedToBuildEdmModelBecauseReturnTypeIsNull, + controllerActionDescriptor.ActionName, + controllerActionDescriptor.ControllerName); + } - Type entityClrType = TypeHelper.GetImplementedIEnumerableType(controllerActionDescriptor.MethodInfo.ReturnType); + Type entityClrType = TypeHelper.GetImplementedIEnumerableType(controllerActionDescriptor.MethodInfo.ReturnType); - if (entityClrType == null) - { - // It is a developer programming error to use this binding attribute - // on actions that return a collection whose element type cannot be - // determined, such as a non-generic IQueryable or IEnumerable. - throw Error.InvalidOperation( - SRResources.FailedToRetrieveTypeToBuildEdmModel, - controllerActionDescriptor.ActionName, - controllerActionDescriptor.ControllerName, - controllerActionDescriptor.MethodInfo.ReturnType.FullName); - } - - return entityClrType; + if (entityClrType == null) + { + // It is a developer programming error to use this binding attribute + // on actions that return a collection whose element type cannot be + // determined, such as a non-generic IQueryable or IEnumerable. + throw Error.InvalidOperation( + SRResources.FailedToRetrieveTypeToBuildEdmModel, + controllerActionDescriptor.ActionName, + controllerActionDescriptor.ControllerName, + controllerActionDescriptor.MethodInfo.ReturnType.FullName); } + + return entityClrType; } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/ODataQueryRequestMiddleware.cs b/src/Microsoft.AspNetCore.OData/Query/ODataQueryRequestMiddleware.cs index f7c1225ea..f667c70df 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ODataQueryRequestMiddleware.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ODataQueryRequestMiddleware.cs @@ -11,99 +11,98 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OData.Extensions; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Defines the middleware for handling OData $query requests. +/// This middleware essentially transforms the incoming request (Post) to a "Get" request. +/// Be noted: should put this middle ware before "UseRouting()". +/// +public class ODataQueryRequestMiddleware { + private IEnumerable _parsers; + private readonly RequestDelegate _next; + /// - /// Defines the middleware for handling OData $query requests. - /// This middleware essentially transforms the incoming request (Post) to a "Get" request. - /// Be noted: should put this middle ware before "UseRouting()". + /// Instantiates a new instance of . /// - public class ODataQueryRequestMiddleware + /// The query request parsers. + /// The next middleware. + public ODataQueryRequestMiddleware(IEnumerable queryRequestParsers, RequestDelegate next) { - private IEnumerable _parsers; - private readonly RequestDelegate _next; + _parsers = queryRequestParsers; + _next = next; + } - /// - /// Instantiates a new instance of . - /// - /// The query request parsers. - /// The next middleware. - public ODataQueryRequestMiddleware(IEnumerable queryRequestParsers, RequestDelegate next) + /// + /// Invoke the OData $query middleware. + /// + /// The http context. + /// A task that can be awaited. + public async Task Invoke(HttpContext context) + { + if (context == null) { - _parsers = queryRequestParsers; - _next = next; + throw Error.ArgumentNull(nameof(context)); } - /// - /// Invoke the OData $query middleware. - /// - /// The http context. - /// A task that can be awaited. - public async Task Invoke(HttpContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - // shall we verify it? - //if (context.GetEndpoint() != null) - //{ - // throw new ODataException("Should put this middleware before UseRouting"); - //} + // shall we verify it? + //if (context.GetEndpoint() != null) + //{ + // throw new ODataException("Should put this middleware before UseRouting"); + //} - HttpRequest request = context.Request; - if (request.IsODataQueryRequest()) + HttpRequest request = context.Request; + if (request.IsODataQueryRequest()) + { + foreach (IODataQueryRequestParser parser in _parsers) { - foreach (IODataQueryRequestParser parser in _parsers) + if (parser.CanParse(request)) { - if (parser.CanParse(request)) - { - await TransformQueryRequestAsync(parser, request).ConfigureAwait(false); - break; - } + await TransformQueryRequestAsync(parser, request).ConfigureAwait(false); + break; } } - - await _next(context).ConfigureAwait(false); } - /// - /// Transforms a POST request targeted at a resource path ending in $query into a GET request. - /// The query options are parsed from the request body and appended to the request URL. - /// - /// The query request parser. - /// The Http request. - internal static async Task TransformQueryRequestAsync(IODataQueryRequestParser parser, HttpRequest request) + await _next(context).ConfigureAwait(false); + } + + /// + /// Transforms a POST request targeted at a resource path ending in $query into a GET request. + /// The query options are parsed from the request body and appended to the request URL. + /// + /// The query request parser. + /// The Http request. + internal static async Task TransformQueryRequestAsync(IODataQueryRequestParser parser, HttpRequest request) + { + if (request == null) { - if (request == null) - { - throw Error.ArgumentNull(nameof(request)); - } + throw Error.ArgumentNull(nameof(request)); + } - // Parse query options in request body - string queryOptions = await parser.ParseAsync(request).ConfigureAwait(false); + // Parse query options in request body + string queryOptions = await parser.ParseAsync(request).ConfigureAwait(false); - string requestPath = request.Path.Value.TrimEnd('/'); - string queryString = request.QueryString.Value; + string requestPath = request.Path.Value.TrimEnd('/'); + string queryString = request.QueryString.Value; - // Strip off the /$query part - requestPath = requestPath.Substring(0, requestPath.LastIndexOf("/$query", StringComparison.OrdinalIgnoreCase)); - if (!string.IsNullOrWhiteSpace(queryOptions)) + // Strip off the /$query part + requestPath = requestPath.Substring(0, requestPath.LastIndexOf("/$query", StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(queryOptions)) + { + if (string.IsNullOrWhiteSpace(queryString)) { - if (string.IsNullOrWhiteSpace(queryString)) - { - queryString = '?' + queryOptions; - } - else - { - queryString += '&' + queryOptions; - } + queryString = '?' + queryOptions; + } + else + { + queryString += '&' + queryOptions; } - - request.Path = new PathString(requestPath); - request.QueryString = new QueryString(queryString); - request.Method = HttpMethods.Get; } + + request.Path = new PathString(requestPath); + request.QueryString = new QueryString(queryString); + request.Method = HttpMethods.Get; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/ODataQuerySettings.cs b/src/Microsoft.AspNetCore.OData/Query/ODataQuerySettings.cs index 91ea2937e..57df54608 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ODataQuerySettings.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ODataQuerySettings.cs @@ -7,155 +7,154 @@ using System; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This class describes the settings to use during query composition. +/// +public class ODataQuerySettings { + private HandleNullPropagationOption _handleNullPropagationOption = HandleNullPropagationOption.Default; + private int? _pageSize; + private int? _modelBoundPageSize; + /// - /// This class describes the settings to use during query composition. + /// Initializes a new instance of the class. /// - public class ODataQuerySettings + public ODataQuerySettings() { - private HandleNullPropagationOption _handleNullPropagationOption = HandleNullPropagationOption.Default; - private int? _pageSize; - private int? _modelBoundPageSize; - - /// - /// Initializes a new instance of the class. - /// - public ODataQuerySettings() + EnsureStableOrdering = true; + EnableConstantParameterization = true; + } + + /// + /// Gets or sets the . + /// + public TimeZoneInfo TimeZone { get; set; } + + /// + /// Gets or sets the maximum number of query results to return based on the type or property. + /// + /// + /// The maximum number of query results to return based on the type or property, + /// or null if there is no limit. + /// + internal int? ModelBoundPageSize + { + get { - EnsureStableOrdering = true; - EnableConstantParameterization = true; + return _modelBoundPageSize; } - - /// - /// Gets or sets the . - /// - public TimeZoneInfo TimeZone { get; set; } - - /// - /// Gets or sets the maximum number of query results to return based on the type or property. - /// - /// - /// The maximum number of query results to return based on the type or property, - /// or null if there is no limit. - /// - internal int? ModelBoundPageSize + set { - get + if (value.HasValue && value <= 0) { - return _modelBoundPageSize; + throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, 1); } - set - { - if (value.HasValue && value <= 0) - { - throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, 1); - } - _modelBoundPageSize = value; - } + _modelBoundPageSize = value; } + } + + /// + /// Gets or sets a value indicating whether query composition should + /// alter the original query when necessary to ensure a stable sort order. + /// + /// A true value indicates the original query should + /// be modified when necessary to guarantee a stable sort order. + /// A false value indicates the sort order can be considered + /// stable without modifying the query. Query providers that ensure + /// a stable sort order should set this value to false. + /// The default value is true. + public bool EnsureStableOrdering { get; set; } - /// - /// Gets or sets a value indicating whether query composition should - /// alter the original query when necessary to ensure a stable sort order. - /// - /// A true value indicates the original query should - /// be modified when necessary to guarantee a stable sort order. - /// A false value indicates the sort order can be considered - /// stable without modifying the query. Query providers that ensure - /// a stable sort order should set this value to false. - /// The default value is true. - public bool EnsureStableOrdering { get; set; } - - /// - /// Gets or sets a value indicating how null propagation should - /// be handled during query composition. - /// - /// - /// The default is . - /// - public HandleNullPropagationOption HandleNullPropagation + /// + /// Gets or sets a value indicating how null propagation should + /// be handled during query composition. + /// + /// + /// The default is . + /// + public HandleNullPropagationOption HandleNullPropagation + { + get { - get - { - return _handleNullPropagationOption; - } - set - { - HandleNullPropagationOptionHelper.Validate(value, "value"); - _handleNullPropagationOption = value; - } + return _handleNullPropagationOption; + } + set + { + HandleNullPropagationOptionHelper.Validate(value, "value"); + _handleNullPropagationOption = value; } + } + + /// + /// Gets or sets a value indicating whether constants should be parameterized. Parameterizing constants + /// would result in better performance with Entity framework. + /// + /// The default value is true. + public bool EnableConstantParameterization { get; set; } + + /// + /// Gets or sets a value indicating whether queries with expanded navigations should be formulated + /// to encourage correlated sub-query results to be buffered. + /// Buffering correlated sub-query results can reduce the number of queries from N + 1 to 2 + /// by buffering results from the sub-query. + /// + /// The default value is false. + public bool EnableCorrelatedSubqueryBuffering { get; set; } + + /// + /// Gets or sets a value indicating which query options should be ignored when applying queries. + /// + public AllowedQueryOptions IgnoredQueryOptions { get; set; } = AllowedQueryOptions.None; + + /// + /// Gets or sets a value indicating which nested query options should be ignored typically within select and expand. + /// + public AllowedQueryOptions IgnoredNestedQueryOptions { get; set; } = AllowedQueryOptions.None; - /// - /// Gets or sets a value indicating whether constants should be parameterized. Parameterizing constants - /// would result in better performance with Entity framework. - /// - /// The default value is true. - public bool EnableConstantParameterization { get; set; } - - /// - /// Gets or sets a value indicating whether queries with expanded navigations should be formulated - /// to encourage correlated sub-query results to be buffered. - /// Buffering correlated sub-query results can reduce the number of queries from N + 1 to 2 - /// by buffering results from the sub-query. - /// - /// The default value is false. - public bool EnableCorrelatedSubqueryBuffering { get; set; } - - /// - /// Gets or sets a value indicating which query options should be ignored when applying queries. - /// - public AllowedQueryOptions IgnoredQueryOptions { get; set; } = AllowedQueryOptions.None; - - /// - /// Gets or sets a value indicating which nested query options should be ignored typically within select and expand. - /// - public AllowedQueryOptions IgnoredNestedQueryOptions { get; set; } = AllowedQueryOptions.None; - - /// - /// Gets or sets the maximum number of query results to return. - /// - /// - /// The maximum number of query results to return, or null if there is no limit. - /// - public int? PageSize + /// + /// Gets or sets the maximum number of query results to return. + /// + /// + /// The maximum number of query results to return, or null if there is no limit. + /// + public int? PageSize + { + get { - get + return _pageSize; + } + set + { + if (value.HasValue && value <= 0) { - return _pageSize; + throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, 1); } - set - { - if (value.HasValue && value <= 0) - { - throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, 1); - } - _pageSize = value; - } + _pageSize = value; } + } - /// - /// Honor $filter inside $expand of non-collection navigation property. - /// The expanded property is only populated when the filter evaluates to true. - /// This setting is false by default. - /// - public bool HandleReferenceNavigationPropertyExpandFilter { get; set; } + /// + /// Honor $filter inside $expand of non-collection navigation property. + /// The expanded property is only populated when the filter evaluates to true. + /// This setting is false by default. + /// + public bool HandleReferenceNavigationPropertyExpandFilter { get; set; } - internal void CopyFrom(ODataQuerySettings settings) - { - TimeZone = settings.TimeZone; - EnsureStableOrdering = settings.EnsureStableOrdering; - EnableConstantParameterization = settings.EnableConstantParameterization; - HandleNullPropagation = settings.HandleNullPropagation; - PageSize = settings.PageSize; - ModelBoundPageSize = settings.ModelBoundPageSize; - HandleReferenceNavigationPropertyExpandFilter = settings.HandleReferenceNavigationPropertyExpandFilter; - EnableCorrelatedSubqueryBuffering = settings.EnableCorrelatedSubqueryBuffering; - IgnoredQueryOptions = settings.IgnoredQueryOptions; - IgnoredNestedQueryOptions = settings.IgnoredNestedQueryOptions; - } + internal void CopyFrom(ODataQuerySettings settings) + { + TimeZone = settings.TimeZone; + EnsureStableOrdering = settings.EnsureStableOrdering; + EnableConstantParameterization = settings.EnableConstantParameterization; + HandleNullPropagation = settings.HandleNullPropagation; + PageSize = settings.PageSize; + ModelBoundPageSize = settings.ModelBoundPageSize; + HandleReferenceNavigationPropertyExpandFilter = settings.HandleReferenceNavigationPropertyExpandFilter; + EnableCorrelatedSubqueryBuffering = settings.EnableCorrelatedSubqueryBuffering; + IgnoredQueryOptions = settings.IgnoredQueryOptions; + IgnoredNestedQueryOptions = settings.IgnoredNestedQueryOptions; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/ParameterAliasNodeTranslator.cs b/src/Microsoft.AspNetCore.OData/Query/ParameterAliasNodeTranslator.cs index 4de4aef76..6940418c9 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ParameterAliasNodeTranslator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ParameterAliasNodeTranslator.cs @@ -12,441 +12,440 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This defines a translator to translate parameter alias nodes. +/// +internal class ParameterAliasNodeTranslator : QueryNodeVisitor { + private IDictionary _parameterAliasNode; + /// - /// This defines a translator to translate parameter alias nodes. + /// Initialize a new instance of . /// - internal class ParameterAliasNodeTranslator : QueryNodeVisitor + /// Parameter alias nodes mapping. + public ParameterAliasNodeTranslator(IDictionary parameterAliasNodes) { - private IDictionary _parameterAliasNode; - - /// - /// Initialize a new instance of . - /// - /// Parameter alias nodes mapping. - public ParameterAliasNodeTranslator(IDictionary parameterAliasNodes) + if (parameterAliasNodes == null) { - if (parameterAliasNodes == null) - { - throw Error.ArgumentNull(nameof(parameterAliasNodes)); - } - - _parameterAliasNode = parameterAliasNodes; + throw Error.ArgumentNull(nameof(parameterAliasNodes)); } - /// - /// Translate an AllNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(AllNode nodeIn) - { - Contract.Assert(nodeIn != null); - - AllNode allNode = new AllNode(nodeIn.RangeVariables, nodeIn.CurrentRangeVariable); + _parameterAliasNode = parameterAliasNodes; + } - if (nodeIn.Source != null) - { - allNode.Source = (CollectionNode)nodeIn.Source.Accept(this); - } + /// + /// Translate an AllNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(AllNode nodeIn) + { + Contract.Assert(nodeIn != null); - if (nodeIn.Body != null) - { - allNode.Body = (SingleValueNode)nodeIn.Body.Accept(this); - } + AllNode allNode = new AllNode(nodeIn.RangeVariables, nodeIn.CurrentRangeVariable); - return allNode; + if (nodeIn.Source != null) + { + allNode.Source = (CollectionNode)nodeIn.Source.Accept(this); } - /// - /// Translate an AnyNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(AnyNode nodeIn) + if (nodeIn.Body != null) { - Contract.Assert(nodeIn != null); + allNode.Body = (SingleValueNode)nodeIn.Body.Accept(this); + } - AnyNode anyNode = new AnyNode(nodeIn.RangeVariables, nodeIn.CurrentRangeVariable); + return allNode; + } - if (nodeIn.Source != null) - { - anyNode.Source = (CollectionNode)nodeIn.Source.Accept(this); - } + /// + /// Translate an AnyNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(AnyNode nodeIn) + { + Contract.Assert(nodeIn != null); - if (nodeIn.Body != null) - { - anyNode.Body = (SingleValueNode)nodeIn.Body.Accept(this); - } + AnyNode anyNode = new AnyNode(nodeIn.RangeVariables, nodeIn.CurrentRangeVariable); - return anyNode; + if (nodeIn.Source != null) + { + anyNode.Source = (CollectionNode)nodeIn.Source.Accept(this); } - /// - /// Translate a BinaryOperatorNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(BinaryOperatorNode nodeIn) + if (nodeIn.Body != null) { - Contract.Assert(nodeIn != null); - - return new BinaryOperatorNode( - nodeIn.OperatorKind, - (SingleValueNode)nodeIn.Left.Accept(this), - (SingleValueNode)nodeIn.Right.Accept(this)); + anyNode.Body = (SingleValueNode)nodeIn.Body.Accept(this); } - /// - /// Translate an InNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(InNode nodeIn) - { - Contract.Assert(nodeIn != null); + return anyNode; + } - return new InNode( - (SingleValueNode)nodeIn.Left.Accept(this), - (CollectionNode)nodeIn.Right.Accept(this)); - } + /// + /// Translate a BinaryOperatorNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(BinaryOperatorNode nodeIn) + { + Contract.Assert(nodeIn != null); - /// - /// Translate a CollectionFunctionCallNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(CollectionFunctionCallNode nodeIn) - { - Contract.Assert(nodeIn != null); - - return new CollectionFunctionCallNode( - nodeIn.Name, - nodeIn.Functions, - nodeIn.Parameters.Select(p => p.Accept(this)), - nodeIn.CollectionType, - nodeIn.Source == null ? null : nodeIn.Source.Accept(this)); - } + return new BinaryOperatorNode( + nodeIn.OperatorKind, + (SingleValueNode)nodeIn.Left.Accept(this), + (SingleValueNode)nodeIn.Right.Accept(this)); + } - /// - /// Translate a CollectionNavigationNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(CollectionNavigationNode nodeIn) - { - Contract.Assert(nodeIn != null); - - return nodeIn.Source == null ? - nodeIn : - new CollectionNavigationNode( - (SingleResourceNode)nodeIn.Source.Accept(this), - nodeIn.NavigationProperty, - nodeIn.BindingPath ?? new EdmPathExpression(nodeIn.NavigationProperty.Name)); - } + /// + /// Translate an InNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(InNode nodeIn) + { + Contract.Assert(nodeIn != null); - /// - /// Translate a CollectionOpenPropertyAccessNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(CollectionOpenPropertyAccessNode nodeIn) - { - Contract.Assert(nodeIn != null); + return new InNode( + (SingleValueNode)nodeIn.Left.Accept(this), + (CollectionNode)nodeIn.Right.Accept(this)); + } - return new CollectionOpenPropertyAccessNode( - (SingleValueNode)nodeIn.Source.Accept(this), - nodeIn.Name); - } + /// + /// Translate a CollectionFunctionCallNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(CollectionFunctionCallNode nodeIn) + { + Contract.Assert(nodeIn != null); + + return new CollectionFunctionCallNode( + nodeIn.Name, + nodeIn.Functions, + nodeIn.Parameters.Select(p => p.Accept(this)), + nodeIn.CollectionType, + nodeIn.Source == null ? null : nodeIn.Source.Accept(this)); + } - /// - /// Translate a CollectionComplexNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(CollectionComplexNode nodeIn) - { - Contract.Assert(nodeIn != null); + /// + /// Translate a CollectionNavigationNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(CollectionNavigationNode nodeIn) + { + Contract.Assert(nodeIn != null); - return new CollectionComplexNode( + return nodeIn.Source == null ? + nodeIn : + new CollectionNavigationNode( (SingleResourceNode)nodeIn.Source.Accept(this), - nodeIn.Property); - } + nodeIn.NavigationProperty, + nodeIn.BindingPath ?? new EdmPathExpression(nodeIn.NavigationProperty.Name)); + } - /// - /// Translate a CollectionPropertyAccessNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(CollectionPropertyAccessNode nodeIn) - { - Contract.Assert(nodeIn != null); + /// + /// Translate a CollectionOpenPropertyAccessNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(CollectionOpenPropertyAccessNode nodeIn) + { + Contract.Assert(nodeIn != null); - return new CollectionPropertyAccessNode( - (SingleValueNode)nodeIn.Source.Accept(this), - nodeIn.Property); - } + return new CollectionOpenPropertyAccessNode( + (SingleValueNode)nodeIn.Source.Accept(this), + nodeIn.Name); + } - /// - /// Translate a ConstantNode. - /// - /// The node to be translated. - /// The original node. - public override QueryNode Visit(ConstantNode nodeIn) - { - return nodeIn; - } + /// + /// Translate a CollectionComplexNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(CollectionComplexNode nodeIn) + { + Contract.Assert(nodeIn != null); - /// - /// Translate a CollectionConstantNode. - /// - /// The node to be translated. - /// The original node. - public override QueryNode Visit(CollectionConstantNode nodeIn) - { - return nodeIn; - } + return new CollectionComplexNode( + (SingleResourceNode)nodeIn.Source.Accept(this), + nodeIn.Property); + } - /// - /// Translate a ConvertNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(ConvertNode nodeIn) - { - Contract.Assert(nodeIn != null); + /// + /// Translate a CollectionPropertyAccessNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(CollectionPropertyAccessNode nodeIn) + { + Contract.Assert(nodeIn != null); - return new ConvertNode((SingleValueNode)nodeIn.Source.Accept(this), nodeIn.TypeReference); - } + return new CollectionPropertyAccessNode( + (SingleValueNode)nodeIn.Source.Accept(this), + nodeIn.Property); + } - /// - /// Translate an CollectionResourceCastNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(CollectionResourceCastNode nodeIn) - { - Contract.Assert(nodeIn != null); + /// + /// Translate a ConstantNode. + /// + /// The node to be translated. + /// The original node. + public override QueryNode Visit(ConstantNode nodeIn) + { + return nodeIn; + } - return new CollectionResourceCastNode( - (CollectionResourceNode)nodeIn.Source.Accept(this), - (IEdmStructuredType)nodeIn.ItemType.Definition); - } + /// + /// Translate a CollectionConstantNode. + /// + /// The node to be translated. + /// The original node. + public override QueryNode Visit(CollectionConstantNode nodeIn) + { + return nodeIn; + } - /// - /// Translate an CollectionResourceFunctionCallNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(CollectionResourceFunctionCallNode nodeIn) - { - Contract.Assert(nodeIn != null); - - return new CollectionResourceFunctionCallNode( - nodeIn.Name, - nodeIn.Functions, - nodeIn.Parameters.Select(p => p.Accept(this)), - nodeIn.CollectionType, - (IEdmEntitySetBase)nodeIn.NavigationSource, - nodeIn.Source == null ? null : nodeIn.Source.Accept(this)); - } + /// + /// Translate a ConvertNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(ConvertNode nodeIn) + { + Contract.Assert(nodeIn != null); - /// - /// Translate an ResourceRangeVariableReferenceNode. - /// - /// The node to be translated. - /// The original node. - public override QueryNode Visit(ResourceRangeVariableReferenceNode nodeIn) - { - return nodeIn; - } + return new ConvertNode((SingleValueNode)nodeIn.Source.Accept(this), nodeIn.TypeReference); + } - /// - /// Translate a NamedFunctionParameterNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(NamedFunctionParameterNode nodeIn) - { - Contract.Assert(nodeIn != null); + /// + /// Translate an CollectionResourceCastNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(CollectionResourceCastNode nodeIn) + { + Contract.Assert(nodeIn != null); - return new NamedFunctionParameterNode( - nodeIn.Name, - nodeIn.Value == null ? null : nodeIn.Value.Accept(this)); - } + return new CollectionResourceCastNode( + (CollectionResourceNode)nodeIn.Source.Accept(this), + (IEdmStructuredType)nodeIn.ItemType.Definition); + } - /// - /// Translate a NonResourceRangeVariableReferenceNode. - /// - /// The node to be translated. - /// The original node. - public override QueryNode Visit(NonResourceRangeVariableReferenceNode nodeIn) - { - return nodeIn; - } + /// + /// Translate an CollectionResourceFunctionCallNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(CollectionResourceFunctionCallNode nodeIn) + { + Contract.Assert(nodeIn != null); + + return new CollectionResourceFunctionCallNode( + nodeIn.Name, + nodeIn.Functions, + nodeIn.Parameters.Select(p => p.Accept(this)), + nodeIn.CollectionType, + (IEdmEntitySetBase)nodeIn.NavigationSource, + nodeIn.Source == null ? null : nodeIn.Source.Accept(this)); + } - /// - /// Translate a ParameterAliasNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(ParameterAliasNode nodeIn) - { - SingleValueNode node = ODataPathSegmentTranslator.TranslateParameterAlias(nodeIn, _parameterAliasNode); - - if (node == null) - { - return new ConstantNode(null); - } - else - { - return node.Accept(this); - } - } + /// + /// Translate an ResourceRangeVariableReferenceNode. + /// + /// The node to be translated. + /// The original node. + public override QueryNode Visit(ResourceRangeVariableReferenceNode nodeIn) + { + return nodeIn; + } - /// - /// Translate a SearchTermNode. - /// - /// The node to be translated. - /// The original node. - public override QueryNode Visit(SearchTermNode nodeIn) - { - return nodeIn; - } + /// + /// Translate a NamedFunctionParameterNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(NamedFunctionParameterNode nodeIn) + { + Contract.Assert(nodeIn != null); - /// - /// Translate a SingleResourceCastNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(SingleResourceCastNode nodeIn) - { - Contract.Assert(nodeIn != null); + return new NamedFunctionParameterNode( + nodeIn.Name, + nodeIn.Value == null ? null : nodeIn.Value.Accept(this)); + } - return nodeIn.Source == null ? - nodeIn : - new SingleResourceCastNode( - (SingleResourceNode)nodeIn.Source.Accept(this), - (IEdmStructuredType)nodeIn.TypeReference.Definition); - } + /// + /// Translate a NonResourceRangeVariableReferenceNode. + /// + /// The node to be translated. + /// The original node. + public override QueryNode Visit(NonResourceRangeVariableReferenceNode nodeIn) + { + return nodeIn; + } - /// - /// Translate a SingleResourceFunctionCallNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(SingleResourceFunctionCallNode nodeIn) - { - Contract.Assert(nodeIn != null); - - return new SingleResourceFunctionCallNode( - nodeIn.Name, - nodeIn.Functions, - nodeIn.Parameters.Select(p => p.Accept(this)), - nodeIn.StructuredTypeReference, - nodeIn.NavigationSource, - nodeIn.Source == null ? null : nodeIn.Source.Accept(this)); - } + /// + /// Translate a ParameterAliasNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(ParameterAliasNode nodeIn) + { + SingleValueNode node = ODataPathSegmentTranslator.TranslateParameterAlias(nodeIn, _parameterAliasNode); - /// - /// Translate a SingleNavigationNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(SingleNavigationNode nodeIn) + if (node == null) { - Contract.Assert(nodeIn != null); - - return nodeIn.Source == null ? - nodeIn : - new SingleNavigationNode( - (SingleResourceNode)nodeIn.Source.Accept(this), - nodeIn.NavigationProperty, - nodeIn.BindingPath ?? new EdmPathExpression(nodeIn.NavigationProperty.Name)); + return new ConstantNode(null); } - - /// - /// Translate a SingleValueFunctionCallNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(SingleValueFunctionCallNode nodeIn) + else { - Contract.Assert(nodeIn != null); - - return new SingleValueFunctionCallNode( - nodeIn.Name, - nodeIn.Functions, - nodeIn.Parameters.Select(p => p.Accept(this)), - nodeIn.TypeReference, - nodeIn.Source == null ? null : nodeIn.Source.Accept(this)); + return node.Accept(this); } + } - /// - /// Translate a SingleValueOpenPropertyAccessNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(SingleValueOpenPropertyAccessNode nodeIn) - { - Contract.Assert(nodeIn != null); + /// + /// Translate a SearchTermNode. + /// + /// The node to be translated. + /// The original node. + public override QueryNode Visit(SearchTermNode nodeIn) + { + return nodeIn; + } - return new SingleValueOpenPropertyAccessNode( - (SingleValueNode)nodeIn.Source.Accept(this), - nodeIn.Name); - } + /// + /// Translate a SingleResourceCastNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(SingleResourceCastNode nodeIn) + { + Contract.Assert(nodeIn != null); - /// - /// Translate a SingleValuePropertyAccessNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(SingleValuePropertyAccessNode nodeIn) - { - Contract.Assert(nodeIn != null); + return nodeIn.Source == null ? + nodeIn : + new SingleResourceCastNode( + (SingleResourceNode)nodeIn.Source.Accept(this), + (IEdmStructuredType)nodeIn.TypeReference.Definition); + } - return new SingleValuePropertyAccessNode( - (SingleValueNode)nodeIn.Source.Accept(this), - nodeIn.Property); - } + /// + /// Translate a SingleResourceFunctionCallNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(SingleResourceFunctionCallNode nodeIn) + { + Contract.Assert(nodeIn != null); + + return new SingleResourceFunctionCallNode( + nodeIn.Name, + nodeIn.Functions, + nodeIn.Parameters.Select(p => p.Accept(this)), + nodeIn.StructuredTypeReference, + nodeIn.NavigationSource, + nodeIn.Source == null ? null : nodeIn.Source.Accept(this)); + } - /// - /// Translate a SingleComplexNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(SingleComplexNode nodeIn) - { - Contract.Assert(nodeIn != null); + /// + /// Translate a SingleNavigationNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(SingleNavigationNode nodeIn) + { + Contract.Assert(nodeIn != null); - return new SingleComplexNode( + return nodeIn.Source == null ? + nodeIn : + new SingleNavigationNode( (SingleResourceNode)nodeIn.Source.Accept(this), - nodeIn.Property); - } + nodeIn.NavigationProperty, + nodeIn.BindingPath ?? new EdmPathExpression(nodeIn.NavigationProperty.Name)); + } - /// - /// Translate an UnaryOperatorNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(UnaryOperatorNode nodeIn) - { - Contract.Assert(nodeIn != null); + /// + /// Translate a SingleValueFunctionCallNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(SingleValueFunctionCallNode nodeIn) + { + Contract.Assert(nodeIn != null); + + return new SingleValueFunctionCallNode( + nodeIn.Name, + nodeIn.Functions, + nodeIn.Parameters.Select(p => p.Accept(this)), + nodeIn.TypeReference, + nodeIn.Source == null ? null : nodeIn.Source.Accept(this)); + } - return new UnaryOperatorNode(nodeIn.OperatorKind, (SingleValueNode)nodeIn.Operand.Accept(this)); - } + /// + /// Translate a SingleValueOpenPropertyAccessNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(SingleValueOpenPropertyAccessNode nodeIn) + { + Contract.Assert(nodeIn != null); - /// - /// Translate a CountNode. - /// - /// The node to be translated. - /// The translated node. - public override QueryNode Visit(CountNode nodeIn) - { - Contract.Assert(nodeIn != null); + return new SingleValueOpenPropertyAccessNode( + (SingleValueNode)nodeIn.Source.Accept(this), + nodeIn.Name); + } - return new CountNode((CollectionNode)nodeIn.Source.Accept(this), nodeIn.FilterClause, nodeIn.SearchClause); - } + /// + /// Translate a SingleValuePropertyAccessNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(SingleValuePropertyAccessNode nodeIn) + { + Contract.Assert(nodeIn != null); + + return new SingleValuePropertyAccessNode( + (SingleValueNode)nodeIn.Source.Accept(this), + nodeIn.Property); + } + + /// + /// Translate a SingleComplexNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(SingleComplexNode nodeIn) + { + Contract.Assert(nodeIn != null); + + return new SingleComplexNode( + (SingleResourceNode)nodeIn.Source.Accept(this), + nodeIn.Property); + } + + /// + /// Translate an UnaryOperatorNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(UnaryOperatorNode nodeIn) + { + Contract.Assert(nodeIn != null); + + return new UnaryOperatorNode(nodeIn.OperatorKind, (SingleValueNode)nodeIn.Operand.Accept(this)); + } + + /// + /// Translate a CountNode. + /// + /// The node to be translated. + /// The translated node. + public override QueryNode Visit(CountNode nodeIn) + { + Contract.Assert(nodeIn != null); + + return new CountNode((CollectionNode)nodeIn.Source.Accept(this), nodeIn.FilterClause, nodeIn.SearchClause); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/ApplyQueryOptions.cs b/src/Microsoft.AspNetCore.OData/Query/Query/ApplyQueryOptions.cs index 69aef541c..39a084493 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/ApplyQueryOptions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/ApplyQueryOptions.cs @@ -17,161 +17,160 @@ using Microsoft.OData.UriParser; using Microsoft.OData.UriParser.Aggregation; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This defines a $apply OData query option for querying. +/// +public class ApplyQueryOption { + private ApplyClause _applyClause; + private ODataQueryOptionParser _queryOptionParser; + /// - /// This defines a $apply OData query option for querying. + /// Initialize a new instance of based on the raw $apply value and + /// an EdmModel from . /// - public class ApplyQueryOption + /// The raw value for $filter query. It can be null or empty. + /// The which contains the and some type information + /// The which is used to parse the query option. + public ApplyQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) { - private ApplyClause _applyClause; - private ODataQueryOptionParser _queryOptionParser; - - /// - /// Initialize a new instance of based on the raw $apply value and - /// an EdmModel from . - /// - /// The raw value for $filter query. It can be null or empty. - /// The which contains the and some type information - /// The which is used to parse the query option. - public ApplyQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) + if (string.IsNullOrEmpty(rawValue)) { - if (string.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty(nameof(rawValue)); - } + throw Error.ArgumentNullOrEmpty(nameof(rawValue)); + } - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (queryOptionParser == null) + { + throw Error.ArgumentNull(nameof(queryOptionParser)); + } + + RawValue = rawValue; + Context = context; + // TODO: Implement and add validator + //Validator = new FilterQueryValidator(); + _queryOptionParser = queryOptionParser; + ResultClrType = Context.ElementClrType; + } - if (queryOptionParser == null) + // for unit test only + internal ApplyQueryOption(string rawValue, ODataQueryContext context) + { + RawValue = rawValue; + Context = context; + } + + /// + /// Gets the given . + /// + public ODataQueryContext Context { get; private set; } + + /// + /// ClrType for result of transformations + /// + public Type ResultClrType { get; private set; } + + /// + /// Gets the parsed for this query option. + /// + public ApplyClause ApplyClause + { + get + { + if (_applyClause == null) { - throw Error.ArgumentNull(nameof(queryOptionParser)); + _applyClause = _queryOptionParser.ParseApply(); } - RawValue = rawValue; - Context = context; - // TODO: Implement and add validator - //Validator = new FilterQueryValidator(); - _queryOptionParser = queryOptionParser; - ResultClrType = Context.ElementClrType; + return _applyClause; } + } + + /// + /// Gets the raw $apply value. + /// + public string RawValue { get; private set; } - // for unit test only - internal ApplyQueryOption(string rawValue, ODataQueryContext context) + /// + /// Apply the apply query to the given IQueryable. + /// + /// + /// The property specifies + /// how this method should handle null propagation. + /// + /// The original . + /// The that contains all the query application related settings. + /// The new after the filter query has been applied to. + public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + { + if (query == null) { - RawValue = rawValue; - Context = context; + throw Error.ArgumentNull(nameof(query)); } - /// - /// Gets the given . - /// - public ODataQueryContext Context { get; private set; } - - /// - /// ClrType for result of transformations - /// - public Type ResultClrType { get; private set; } + if (querySettings == null) + { + throw Error.ArgumentNull(nameof(querySettings)); + } - /// - /// Gets the parsed for this query option. - /// - public ApplyClause ApplyClause + if (Context.ElementClrType == null) { - get - { - if (_applyClause == null) - { - _applyClause = _queryOptionParser.ParseApply(); - } + throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); + } - return _applyClause; - } + // Linq to SQL not supported for $apply + if (query.Provider.GetType().Namespace == HandleNullPropagationOptionHelper.Linq2SqlQueryProviderNamespace) + { + throw Error.NotSupported(SRResources.ApplyQueryOptionNotSupportedForLinq2SQL); } - /// - /// Gets the raw $apply value. - /// - public string RawValue { get; private set; } - - /// - /// Apply the apply query to the given IQueryable. - /// - /// - /// The property specifies - /// how this method should handle null propagation. - /// - /// The original . - /// The that contains all the query application related settings. - /// The new after the filter query has been applied to. - public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + ApplyClause applyClause = ApplyClause; + Contract.Assert(applyClause != null); + + // The IWebApiAssembliesResolver service is internal and can only be injected by WebApi. + // This code path may be used in cases when the service container is not available + // and the service container is available but may not contain an instance of IWebApiAssembliesResolver. + IAssemblyResolver assembliesResolver = AssemblyResolverHelper.Default; + if (Context.RequestContainer != null) { - if (query == null) + IAssemblyResolver injectedResolver = Context.RequestContainer.GetService(); + if (injectedResolver != null) { - throw Error.ArgumentNull(nameof(query)); + assembliesResolver = injectedResolver; } + } - if (querySettings == null) + foreach (var transformation in applyClause.Transformations) + { + if (transformation.Kind == TransformationNodeKind.Aggregate || transformation.Kind == TransformationNodeKind.GroupBy) { - throw Error.ArgumentNull(nameof(querySettings)); + var binder = new AggregationBinder(querySettings, assembliesResolver, ResultClrType, Context.Model, transformation); + query = binder.Bind(query); + this.ResultClrType = binder.ResultClrType; } - - if (Context.ElementClrType == null) + else if (transformation.Kind == TransformationNodeKind.Compute) { - throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); + var binder = new ComputeBinder(querySettings, assembliesResolver, ResultClrType, Context.Model, (ComputeTransformationNode)transformation); + query = binder.Bind(query); + this.ResultClrType = binder.ResultClrType; } - - // Linq to SQL not supported for $apply - if (query.Provider.GetType().Namespace == HandleNullPropagationOptionHelper.Linq2SqlQueryProviderNamespace) + else if (transformation.Kind == TransformationNodeKind.Filter) { - throw Error.NotSupported(SRResources.ApplyQueryOptionNotSupportedForLinq2SQL); - } + var filterTransformation = transformation as FilterTransformationNode; - ApplyClause applyClause = ApplyClause; - Contract.Assert(applyClause != null); + IFilterBinder binder = Context.GetFilterBinder(); + QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, ResultClrType); - // The IWebApiAssembliesResolver service is internal and can only be injected by WebApi. - // This code path may be used in cases when the service container is not available - // and the service container is available but may not contain an instance of IWebApiAssembliesResolver. - IAssemblyResolver assembliesResolver = AssemblyResolverHelper.Default; - if (Context.RequestContainer != null) - { - IAssemblyResolver injectedResolver = Context.RequestContainer.GetService(); - if (injectedResolver != null) - { - assembliesResolver = injectedResolver; - } - } - - foreach (var transformation in applyClause.Transformations) - { - if (transformation.Kind == TransformationNodeKind.Aggregate || transformation.Kind == TransformationNodeKind.GroupBy) - { - var binder = new AggregationBinder(querySettings, assembliesResolver, ResultClrType, Context.Model, transformation); - query = binder.Bind(query); - this.ResultClrType = binder.ResultClrType; - } - else if (transformation.Kind == TransformationNodeKind.Compute) - { - var binder = new ComputeBinder(querySettings, assembliesResolver, ResultClrType, Context.Model, (ComputeTransformationNode)transformation); - query = binder.Bind(query); - this.ResultClrType = binder.ResultClrType; - } - else if (transformation.Kind == TransformationNodeKind.Filter) - { - var filterTransformation = transformation as FilterTransformationNode; - - IFilterBinder binder = Context.GetFilterBinder(); - QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, ResultClrType); - - query = binder.ApplyBind(query, filterTransformation.FilterClause, binderContext); - } + query = binder.ApplyBind(query, filterTransformation.FilterClause, binderContext); } - - return query; } + + return query; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/ComputeQueryOption.cs b/src/Microsoft.AspNetCore.OData/Query/Query/ComputeQueryOption.cs index 64608ea8c..0242ceb35 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/ComputeQueryOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/ComputeQueryOption.cs @@ -11,131 +11,130 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This defines a $compute OData query option for querying. +/// The $compute system query option allows clients to define computed properties that can be used in a $select or within a $filter or $orderby expression. +/// Computed properties SHOULD be included as dynamic properties in the result and MUST be included if $select is specified with the computed property name, or star (*). +/// +public class ComputeQueryOption { + private ComputeClause _computeClause; + private ODataQueryOptionParser _queryOptionParser; + /// - /// This defines a $compute OData query option for querying. - /// The $compute system query option allows clients to define computed properties that can be used in a $select or within a $filter or $orderby expression. - /// Computed properties SHOULD be included as dynamic properties in the result and MUST be included if $select is specified with the computed property name, or star (*). + /// Initialize a new instance of based on the raw $compute value and + /// an EdmModel from . /// - public class ComputeQueryOption + /// The raw value for $compute query. It can be null or empty. + /// The which contains the and some type information + /// The which is used to parse the query option. + public ComputeQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) { - private ComputeClause _computeClause; - private ODataQueryOptionParser _queryOptionParser; - - /// - /// Initialize a new instance of based on the raw $compute value and - /// an EdmModel from . - /// - /// The raw value for $compute query. It can be null or empty. - /// The which contains the and some type information - /// The which is used to parse the query option. - public ComputeQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) + if (string.IsNullOrEmpty(rawValue)) { - if (string.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty(nameof(rawValue)); - } - - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNullOrEmpty(nameof(rawValue)); + } - if (queryOptionParser == null) - { - throw Error.ArgumentNull(nameof(queryOptionParser)); - } + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - Context = context; - RawValue = rawValue; - Validator = context.GetComputeQueryValidator(); - _queryOptionParser = queryOptionParser; - ResultClrType = Context.ElementClrType; + if (queryOptionParser == null) + { + throw Error.ArgumentNull(nameof(queryOptionParser)); } - // This constructor is intended for unit testing only. - internal ComputeQueryOption(string rawValue, ODataQueryContext context) + Context = context; + RawValue = rawValue; + Validator = context.GetComputeQueryValidator(); + _queryOptionParser = queryOptionParser; + ResultClrType = Context.ElementClrType; + } + + // This constructor is intended for unit testing only. + internal ComputeQueryOption(string rawValue, ODataQueryContext context) + { + if (string.IsNullOrEmpty(rawValue)) { - if (string.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty(nameof(rawValue)); - } + throw Error.ArgumentNullOrEmpty(nameof(rawValue)); + } - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - Context = context; - RawValue = rawValue; + Context = context; + RawValue = rawValue; - Validator = context.GetComputeQueryValidator(); - _queryOptionParser = new ODataQueryOptionParser( - context.Model, - context.ElementType, - context.NavigationSource, - new Dictionary { { "$compute", rawValue } }, - context.RequestContainer); + Validator = context.GetComputeQueryValidator(); + _queryOptionParser = new ODataQueryOptionParser( + context.Model, + context.ElementType, + context.NavigationSource, + new Dictionary { { "$compute", rawValue } }, + context.RequestContainer); - if (context.RequestContainer == null) - { - // By default, let's enable the property name case-insensitive - _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; - } + if (context.RequestContainer == null) + { + // By default, let's enable the property name case-insensitive + _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; } + } - /// - /// Gets the given . - /// - public ODataQueryContext Context { get; } + /// + /// Gets the given . + /// + public ODataQueryContext Context { get; } - /// - /// ClrType for result of transformations - /// - public Type ResultClrType { get; } + /// + /// ClrType for result of transformations + /// + public Type ResultClrType { get; } - /// - /// Gets the parsed for this query option. - /// - public ComputeClause ComputeClause + /// + /// Gets the parsed for this query option. + /// + public ComputeClause ComputeClause + { + get { - get + if (_computeClause == null) { - if (_computeClause == null) - { - _computeClause = _queryOptionParser.ParseCompute(); - } - - return _computeClause; + _computeClause = _queryOptionParser.ParseCompute(); } + + return _computeClause; } + } + + /// + /// Gets the raw $compute value. + /// + public string RawValue { get; } - /// - /// Gets the raw $compute value. - /// - public string RawValue { get; } - - /// - /// Gets or sets the $compute Query Validator. - /// - public IComputeQueryValidator Validator { get; set; } - - /// - /// Validate the $compute query based on the given . It throws an ODataException if validation failed. - /// - /// The instance which contains all the validation settings. - public void Validate(ODataValidationSettings validationSettings) + /// + /// Gets or sets the $compute Query Validator. + /// + public IComputeQueryValidator Validator { get; set; } + + /// + /// Validate the $compute query based on the given . It throws an ODataException if validation failed. + /// + /// The instance which contains all the validation settings. + public void Validate(ODataValidationSettings validationSettings) + { + if (validationSettings == null) { - if (validationSettings == null) - { - throw Error.ArgumentNull(nameof(validationSettings)); - } + throw Error.ArgumentNull(nameof(validationSettings)); + } - if (Validator != null) - { - Validator.Validate(this, validationSettings); - } + if (Validator != null) + { + Validator.Validate(this, validationSettings); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/CountQueryOption.cs b/src/Microsoft.AspNetCore.OData/Query/Query/CountQueryOption.cs index 5e13d0253..ad291c87d 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/CountQueryOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/CountQueryOption.cs @@ -12,168 +12,167 @@ using Microsoft.AspNetCore.OData.Query.Validator; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Represents the value of the $count query option and exposes a way to retrieve the number of entities that satisfy a query. +/// +public class CountQueryOption { + private bool? _value; + private ODataQueryOptionParser _queryOptionParser; + /// - /// Represents the value of the $count query option and exposes a way to retrieve the number of entities that satisfy a query. + /// Initializes a new instance of the class. /// - public class CountQueryOption + /// The raw value for the $count query option. + /// The which contains the query context. + /// The which is used to parse the query option. + public CountQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) { - private bool? _value; - private ODataQueryOptionParser _queryOptionParser; - - /// - /// Initializes a new instance of the class. - /// - /// The raw value for the $count query option. - /// The which contains the query context. - /// The which is used to parse the query option. - public CountQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) + if (String.IsNullOrEmpty(rawValue)) { - if (String.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty("rawValue"); - } - - if (context == null) - { - throw Error.ArgumentNull("context"); - } + throw Error.ArgumentNullOrEmpty("rawValue"); + } - if (queryOptionParser == null) - { - throw Error.ArgumentNull("queryOptionParser"); - } + if (context == null) + { + throw Error.ArgumentNull("context"); + } - Context = context; - RawValue = rawValue; - Validator = context.GetCountQueryValidator(); - _queryOptionParser = queryOptionParser; + if (queryOptionParser == null) + { + throw Error.ArgumentNull("queryOptionParser"); } - // This constructor is intended for unit testing only. - internal CountQueryOption(string rawValue, ODataQueryContext context) + Context = context; + RawValue = rawValue; + Validator = context.GetCountQueryValidator(); + _queryOptionParser = queryOptionParser; + } + + // This constructor is intended for unit testing only. + internal CountQueryOption(string rawValue, ODataQueryContext context) + { + if (String.IsNullOrEmpty(rawValue)) { - if (String.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty("rawValue"); - } + throw Error.ArgumentNullOrEmpty("rawValue"); + } - if (context == null) - { - throw Error.ArgumentNull("context"); - } + if (context == null) + { + throw Error.ArgumentNull("context"); + } - Context = context; - RawValue = rawValue; - Validator = context.GetCountQueryValidator(); - _queryOptionParser = new ODataQueryOptionParser( - context.Model, - context.ElementType, - context.NavigationSource, - new Dictionary { { "$count", rawValue } }, - context.RequestContainer); - - if (context.RequestContainer == null) - { - // By default, let's enable the property name case-insensitive - _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; - } + Context = context; + RawValue = rawValue; + Validator = context.GetCountQueryValidator(); + _queryOptionParser = new ODataQueryOptionParser( + context.Model, + context.ElementType, + context.NavigationSource, + new Dictionary { { "$count", rawValue } }, + context.RequestContainer); + + if (context.RequestContainer == null) + { + // By default, let's enable the property name case-insensitive + _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; } + } - /// - /// Gets the given . - /// - public ODataQueryContext Context { get; private set; } + /// + /// Gets the given . + /// + public ODataQueryContext Context { get; private set; } - /// - /// Gets the raw $count value. - /// - public string RawValue { get; private set; } + /// + /// Gets the raw $count value. + /// + public string RawValue { get; private set; } - /// - /// Gets the value of the $count in a parsed form. - /// - public bool Value + /// + /// Gets the value of the $count in a parsed form. + /// + public bool Value + { + get { - get + if (_value == null) { - if (_value == null) - { - _value = _queryOptionParser.ParseCount(); - } - - Contract.Assert(_value.HasValue); - return _value.Value; + _value = _queryOptionParser.ParseCount(); } + + Contract.Assert(_value.HasValue); + return _value.Value; } + } - /// - /// Gets or sets the $count query validator. - /// - public ICountQueryValidator Validator { get; set; } - - /// - /// Validate the count query based on the given . - /// It throws an ODataException if validation failed. - /// - /// The instance - /// which contains all the validation settings. - public void Validate(ODataValidationSettings validationSettings) + /// + /// Gets or sets the $count query validator. + /// + public ICountQueryValidator Validator { get; set; } + + /// + /// Validate the count query based on the given . + /// It throws an ODataException if validation failed. + /// + /// The instance + /// which contains all the validation settings. + public void Validate(ODataValidationSettings validationSettings) + { + if (validationSettings == null) { - if (validationSettings == null) - { - throw Error.ArgumentNull("validationSettings"); - } + throw Error.ArgumentNull("validationSettings"); + } - if (Validator != null) - { - Validator.Validate(this, validationSettings); - } + if (Validator != null) + { + Validator.Validate(this, validationSettings); } + } - /// - /// Gets the number of entities that satisfy the given query if the response should include a count query option, or null otherwise. - /// - /// The query to compute the count for. - /// The number of entities that satisfy the specified query if the response should include a count query option, or null otherwise. - public long? GetEntityCount(IQueryable query) + /// + /// Gets the number of entities that satisfy the given query if the response should include a count query option, or null otherwise. + /// + /// The query to compute the count for. + /// The number of entities that satisfy the specified query if the response should include a count query option, or null otherwise. + public long? GetEntityCount(IQueryable query) + { + if (Context.ElementClrType == null) { - if (Context.ElementClrType == null) - { - throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "GetEntityCount"); - } + throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "GetEntityCount"); + } - if (Value) - { - return ExpressionHelpers.Count(query, Context.ElementClrType)(); - } - else - { - return null; - } + if (Value) + { + return ExpressionHelpers.Count(query, Context.ElementClrType)(); + } + else + { + return null; } + } - /// - /// Gets the Func of entities number that satisfy the given query if the response should include a count query option, or null otherwise. - /// - /// The query to compute the count for. - /// The Func of entities number that satisfy the specified query if the response should include a count query option, or null otherwise. - internal Func GetEntityCountFunc(IQueryable query) + /// + /// Gets the Func of entities number that satisfy the given query if the response should include a count query option, or null otherwise. + /// + /// The query to compute the count for. + /// The Func of entities number that satisfy the specified query if the response should include a count query option, or null otherwise. + internal Func GetEntityCountFunc(IQueryable query) + { + if (Context.ElementClrType == null) { - if (Context.ElementClrType == null) - { - throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "GetEntityCount"); - } + throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "GetEntityCount"); + } - if (Value) - { - return ExpressionHelpers.Count(query, Context.ElementClrType); - } - else - { - return null; - } + if (Value) + { + return ExpressionHelpers.Count(query, Context.ElementClrType); + } + else + { + return null; } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/DefaultSkipTokenHandler.cs b/src/Microsoft.AspNetCore.OData/Query/Query/DefaultSkipTokenHandler.cs index 4ffa1c5d2..180878ba8 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/DefaultSkipTokenHandler.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/DefaultSkipTokenHandler.cs @@ -23,527 +23,526 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Default implementation of SkipTokenHandler for the service. +/// +public class DefaultSkipTokenHandler : SkipTokenHandler { + private const char CommaDelimiter = ','; + private static char propertyDelimiter = '-'; + internal static DefaultSkipTokenHandler Instance = new DefaultSkipTokenHandler(); + /// - /// Default implementation of SkipTokenHandler for the service. + /// Returns the URI for NextPageLink /// - public class DefaultSkipTokenHandler : SkipTokenHandler + /// BaseUri for nextlink. It should be request URI for top level resource and navigation link for nested resource. + /// Maximum number of records in the set of partial results for a resource. + /// Instance based on which SkipToken value will be generated. + /// Serializer context + /// Returns the URI for NextPageLink. If a null object is passed for the instance, resorts to the default paging mechanism of using $skip and $top. + public override Uri GenerateNextPageLink(Uri baseUri, int pageSize, Object instance, ODataSerializerContext context) { - private const char CommaDelimiter = ','; - private static char propertyDelimiter = '-'; - internal static DefaultSkipTokenHandler Instance = new DefaultSkipTokenHandler(); - - /// - /// Returns the URI for NextPageLink - /// - /// BaseUri for nextlink. It should be request URI for top level resource and navigation link for nested resource. - /// Maximum number of records in the set of partial results for a resource. - /// Instance based on which SkipToken value will be generated. - /// Serializer context - /// Returns the URI for NextPageLink. If a null object is passed for the instance, resorts to the default paging mechanism of using $skip and $top. - public override Uri GenerateNextPageLink(Uri baseUri, int pageSize, Object instance, ODataSerializerContext context) + if (context == null || pageSize <= 0) { - if (context == null || pageSize <= 0) - { - return null; - } + return null; + } - Func skipTokenGenerator = null; - OrderByClause orderByClause = null; - ExpandedReferenceSelectItem expandedItem = context.CurrentSelectItem as ExpandedReferenceSelectItem; - IEdmModel model = context.Model; + Func skipTokenGenerator = null; + OrderByClause orderByClause = null; + ExpandedReferenceSelectItem expandedItem = context.CurrentSelectItem as ExpandedReferenceSelectItem; + IEdmModel model = context.Model; - DefaultQueryConfigurations queryConfigs = context.QueryContext.DefaultQueryConfigurations; - if (queryConfigs.EnableSkipToken) + DefaultQueryConfigurations queryConfigs = context.QueryContext.DefaultQueryConfigurations; + if (queryConfigs.EnableSkipToken) + { + if (expandedItem != null) { - if (expandedItem != null) + // Handle Delta resource; currently not value based. + if (DeltaHelper.IsDeltaOfT(context.ExpandedResource.GetType())) { - // Handle Delta resource; currently not value based. - if (DeltaHelper.IsDeltaOfT(context.ExpandedResource.GetType())) - { - return GetNextPageHelper.GetNextPageLink(baseUri, pageSize); - } - - if (expandedItem.OrderByOption != null) - { - orderByClause = expandedItem.OrderByOption; - } - - skipTokenGenerator = (obj) => - { - return GenerateSkipTokenValue(obj, model, orderByClause, context); - }; - - return GetNextPageHelper.GetNextPageLink(baseUri, pageSize, instance, skipTokenGenerator); + return GetNextPageHelper.GetNextPageLink(baseUri, pageSize); } - if (context.QueryOptions != null && context.QueryOptions.OrderBy != null) + if (expandedItem.OrderByOption != null) { - orderByClause = context.QueryOptions.OrderBy.OrderByClause; + orderByClause = expandedItem.OrderByOption; } skipTokenGenerator = (obj) => { return GenerateSkipTokenValue(obj, model, orderByClause, context); }; - } - return GetNextPageHelper.GetNextPageLink(baseUri, pageSize, instance, skipTokenGenerator); - } - - /// - /// Generates a string to be used as the skip token value within the next link. - /// - /// Object based on which SkipToken value will be generated. - /// The edm model. - /// The orderby clause to generate the skiptoken value. - /// The serializer context. - /// Value for the skiptoken to be used in the next link. - internal static string GenerateSkipTokenValue(object lastMember, IEdmModel model, OrderByClause clause, ODataSerializerContext context = null) - { - if (lastMember == null) - { - return string.Empty; + return GetNextPageHelper.GetNextPageLink(baseUri, pageSize, instance, skipTokenGenerator); } - List clauses = GetOrderByClauses(lastMember, model, clause); - - TimeZoneInfo timeZoneInfo = context?.TimeZone; - List> values = GetPropertyValues(lastMember, model, clauses, context); - if (values == null || values.Count == 0 || values.Count != clauses.Count) + if (context.QueryOptions != null && context.QueryOptions.OrderBy != null) { - return null; + orderByClause = context.QueryOptions.OrderBy.OrderByClause; } - StringBuilder skipTokenBuilder = new StringBuilder(); - int index = 0; - foreach (OrderByClause orderBy in clauses) + skipTokenGenerator = (obj) => { - object value = values[index].Value; - string name = values[index].Key; - IEdmTypeReference typeReference = orderBy.Expression.TypeReference; - - string uriLiteral; - if (value == null) - { - uriLiteral = ODataUriUtils.ConvertToUriLiteral(value, ODataVersion.V401); - } - else if (typeReference != null && typeReference.IsEnum()) - { - ODataEnumValue enumValue = new ODataEnumValue(value.ToString(), value.GetType().FullName); - uriLiteral = ODataUriUtils.ConvertToUriLiteral(enumValue, ODataVersion.V401, model); - } - else if (typeReference != null && typeReference.IsDateTimeOffset() && value is DateTime) - { - var dateTime = (DateTime)value; - var dateTimeOffsetValue = TimeZoneInfoHelper.ConvertToDateTimeOffset(dateTime, timeZoneInfo); - uriLiteral = ODataUriUtils.ConvertToUriLiteral(dateTimeOffsetValue, ODataVersion.V401, model); - } - else - { - uriLiteral = ODataUriUtils.ConvertToUriLiteral(value, ODataVersion.V401, model); - } + return GenerateSkipTokenValue(obj, model, orderByClause, context); + }; + } - var encodedUriLiteral = WebUtility.UrlEncode(uriLiteral); + return GetNextPageHelper.GetNextPageLink(baseUri, pageSize, instance, skipTokenGenerator); + } - // By design: We should only include the value? - // For back-compatibility, let's keep the key-value pair for top-level property orderby - // For other advance orderby clause, only include the value. - if (!string.IsNullOrEmpty(name)) - { - skipTokenBuilder.Append(name).Append(propertyDelimiter); - } - skipTokenBuilder.Append(encodedUriLiteral); + /// + /// Generates a string to be used as the skip token value within the next link. + /// + /// Object based on which SkipToken value will be generated. + /// The edm model. + /// The orderby clause to generate the skiptoken value. + /// The serializer context. + /// Value for the skiptoken to be used in the next link. + internal static string GenerateSkipTokenValue(object lastMember, IEdmModel model, OrderByClause clause, ODataSerializerContext context = null) + { + if (lastMember == null) + { + return string.Empty; + } - index++; - if (index != clauses.Count) - { - skipTokenBuilder.Append(','); - } - } + List clauses = GetOrderByClauses(lastMember, model, clause); - return skipTokenBuilder.ToString(); + TimeZoneInfo timeZoneInfo = context?.TimeZone; + List> values = GetPropertyValues(lastMember, model, clauses, context); + if (values == null || values.Count == 0 || values.Count != clauses.Count) + { + return null; } - private static List> GetPropertyValues(object source, IEdmModel model, List clauses, - ODataSerializerContext context) + StringBuilder skipTokenBuilder = new StringBuilder(); + int index = 0; + foreach (OrderByClause orderBy in clauses) { - if (source == null || clauses == null || clauses.Count == 0) + object value = values[index].Value; + string name = values[index].Key; + IEdmTypeReference typeReference = orderBy.Expression.TypeReference; + + string uriLiteral; + if (value == null) + { + uriLiteral = ODataUriUtils.ConvertToUriLiteral(value, ODataVersion.V401); + } + else if (typeReference != null && typeReference.IsEnum()) + { + ODataEnumValue enumValue = new ODataEnumValue(value.ToString(), value.GetType().FullName); + uriLiteral = ODataUriUtils.ConvertToUriLiteral(enumValue, ODataVersion.V401, model); + } + else if (typeReference != null && typeReference.IsDateTimeOffset() && value is DateTime) + { + var dateTime = (DateTime)value; + var dateTimeOffsetValue = TimeZoneInfoHelper.ConvertToDateTimeOffset(dateTime, timeZoneInfo); + uriLiteral = ODataUriUtils.ConvertToUriLiteral(dateTimeOffsetValue, ODataVersion.V401, model); + } + else { - return null; + uriLiteral = ODataUriUtils.ConvertToUriLiteral(value, ODataVersion.V401, model); } - // When page size enabled, we will generate the next link no matter what the following scenarios: - // 1) Without $orderby, $select, $expand, for example: ~/customers - // 2) Without $select =..., for example: ~/customers?$orderby=.... - // 3) Without $orderby=..., for example: ~/customers?$select=.... - // 4) With $orderby, $select..., for example: ~/customers?$select=....&$orderby=..... + var encodedUriLiteral = WebUtility.UrlEncode(uriLiteral); - // If the request contains $select, the SelectExpandBinder at least contains the 'key(s)', since they are auto-selected - // If the request doesn't contain $select, the below codes can return all properties - IEdmStructuredObject structuredObj = source as IEdmStructuredObject; - if (structuredObj == null) + // By design: We should only include the value? + // For back-compatibility, let's keep the key-value pair for top-level property orderby + // For other advance orderby clause, only include the value. + if (!string.IsNullOrEmpty(name)) { - Type sourceType = source.GetType(); - QueryBinderContext binderContext = new QueryBinderContext(model, new ODataQuerySettings(), sourceType); - binderContext.AddComputedProperties(context?.QueryOptions?.Compute?.ComputeClause?.ComputedItems); - binderContext.OrderByClauses = clauses; - ISelectExpandBinder selectExpandBinder = context != null && context.QueryContext != null ? - context.QueryContext.GetSelectExpandBinder() : - new SelectExpandBinder(new FilterBinder(), new OrderByBinder()); - - SelectExpandClause selectAll = new SelectExpandClause(null, true); - structuredObj = selectExpandBinder.ApplyBind(source, selectAll, binderContext) as IEdmStructuredObject; + skipTokenBuilder.Append(name).Append(propertyDelimiter); } + skipTokenBuilder.Append(encodedUriLiteral); - if (structuredObj == null) + index++; + if (index != clauses.Count) { - return null; + skipTokenBuilder.Append(','); } + } + + return skipTokenBuilder.ToString(); + } + + private static List> GetPropertyValues(object source, IEdmModel model, List clauses, + ODataSerializerContext context) + { + if (source == null || clauses == null || clauses.Count == 0) + { + return null; + } + + // When page size enabled, we will generate the next link no matter what the following scenarios: + // 1) Without $orderby, $select, $expand, for example: ~/customers + // 2) Without $select =..., for example: ~/customers?$orderby=.... + // 3) Without $orderby=..., for example: ~/customers?$select=.... + // 4) With $orderby, $select..., for example: ~/customers?$select=....&$orderby=..... + + // If the request contains $select, the SelectExpandBinder at least contains the 'key(s)', since they are auto-selected + // If the request doesn't contain $select, the below codes can return all properties + IEdmStructuredObject structuredObj = source as IEdmStructuredObject; + if (structuredObj == null) + { + Type sourceType = source.GetType(); + QueryBinderContext binderContext = new QueryBinderContext(model, new ODataQuerySettings(), sourceType); + binderContext.AddComputedProperties(context?.QueryOptions?.Compute?.ComputeClause?.ComputedItems); + binderContext.OrderByClauses = clauses; + ISelectExpandBinder selectExpandBinder = context != null && context.QueryContext != null ? + context.QueryContext.GetSelectExpandBinder() : + new SelectExpandBinder(new FilterBinder(), new OrderByBinder()); + + SelectExpandClause selectAll = new SelectExpandClause(null, true); + structuredObj = selectExpandBinder.ApplyBind(source, selectAll, binderContext) as IEdmStructuredObject; + } + + if (structuredObj == null) + { + return null; + } - structuredObj.TryGetPropertyValue(OrderByClauseHelpers.OrderByGlobalNameKey, out object orderByNameObject); + structuredObj.TryGetPropertyValue(OrderByClauseHelpers.OrderByGlobalNameKey, out object orderByNameObject); - List> values = new List>(); - string[] orderByNames = orderByNameObject == null ? null : (orderByNameObject as string).Split(","); - int index = 0; - object value; - foreach (OrderByClause clause in clauses) + List> values = new List>(); + string[] orderByNames = orderByNameObject == null ? null : (orderByNameObject as string).Split(","); + int index = 0; + object value; + foreach (OrderByClause clause in clauses) + { + if (clause.IsTopLevelSingleProperty(out _, out string name)) { - if (clause.IsTopLevelSingleProperty(out _, out string name)) + // Let's first retrieve the value using property name, for example: "Id" + if (structuredObj.TryGetPropertyValue(name, out value)) { - // Let's first retrieve the value using property name, for example: "Id" - if (structuredObj.TryGetPropertyValue(name, out value)) - { - values.Add(new KeyValuePair(name, value)); - } - else - { - value = GetValue(structuredObj, orderByNames, index++, out string _); - values.Add(new KeyValuePair(name, value)); - } + values.Add(new KeyValuePair(name, value)); } else { value = GetValue(structuredObj, orderByNames, index++, out string _); - values.Add(new KeyValuePair(string.Empty, value)); + values.Add(new KeyValuePair(name, value)); } } - - return values; + else + { + value = GetValue(structuredObj, orderByNames, index++, out string _); + values.Add(new KeyValuePair(string.Empty, value)); + } } - private static object GetValue(IEdmStructuredObject source, string[] orderByNames, int index, out string name) + return values; + } + + private static object GetValue(IEdmStructuredObject source, string[] orderByNames, int index, out string name) + { + name = string.Empty; + if (orderByNames != null && index < orderByNames.Length) { - name = string.Empty; - if (orderByNames != null && index < orderByNames.Length) + if (source.TryGetPropertyValue(orderByNames[index], out object value)) { - if (source.TryGetPropertyValue(orderByNames[index], out object value)) - { - name = orderByNames[index]; - return value; - } + name = orderByNames[index]; + return value; } + } - return null; + return null; + } + + /// + /// Apply the $skiptoken query to the given IQueryable. + /// + /// The original . + /// The skiptoken query option which needs to be applied to this query option. + /// The query settings to use while applying this query option. + /// Information about the other query options. + /// The new after the skiptoken query has been applied to, could be null. + public override IQueryable ApplyTo(IQueryable query, SkipTokenQueryOption skipTokenQueryOption, + ODataQuerySettings querySettings, ODataQueryOptions queryOptions) + { + return ApplyToImplementation(query, skipTokenQueryOption, querySettings, queryOptions) as IQueryable; + } + + /// + /// Apply the $skiptoken query to the given IQueryable. + /// + /// The original , not null. + /// The skiptoken query option which needs to be applied to this query option, not null. + /// The query settings to use while applying this query option, not null. + /// Information about the other query options, could be null. + /// The new after the skiptoken query has been applied to. + public override IQueryable ApplyTo(IQueryable query, SkipTokenQueryOption skipTokenQueryOption, + ODataQuerySettings querySettings, ODataQueryOptions queryOptions) + { + return ApplyToImplementation(query, skipTokenQueryOption, querySettings, queryOptions); + } + + private static IQueryable ApplyToImplementation(IQueryable query, SkipTokenQueryOption skipTokenQueryOption, + ODataQuerySettings querySettings, ODataQueryOptions queryOptions) + { + if (query == null) + { + throw Error.ArgumentNull(nameof(query)); } - /// - /// Apply the $skiptoken query to the given IQueryable. - /// - /// The original . - /// The skiptoken query option which needs to be applied to this query option. - /// The query settings to use while applying this query option. - /// Information about the other query options. - /// The new after the skiptoken query has been applied to, could be null. - public override IQueryable ApplyTo(IQueryable query, SkipTokenQueryOption skipTokenQueryOption, - ODataQuerySettings querySettings, ODataQueryOptions queryOptions) + if (skipTokenQueryOption == null) { - return ApplyToImplementation(query, skipTokenQueryOption, querySettings, queryOptions) as IQueryable; + throw Error.ArgumentNull(nameof(skipTokenQueryOption)); } - /// - /// Apply the $skiptoken query to the given IQueryable. - /// - /// The original , not null. - /// The skiptoken query option which needs to be applied to this query option, not null. - /// The query settings to use while applying this query option, not null. - /// Information about the other query options, could be null. - /// The new after the skiptoken query has been applied to. - public override IQueryable ApplyTo(IQueryable query, SkipTokenQueryOption skipTokenQueryOption, - ODataQuerySettings querySettings, ODataQueryOptions queryOptions) + if (querySettings == null) { - return ApplyToImplementation(query, skipTokenQueryOption, querySettings, queryOptions); + throw Error.ArgumentNull(nameof(querySettings)); } - private static IQueryable ApplyToImplementation(IQueryable query, SkipTokenQueryOption skipTokenQueryOption, - ODataQuerySettings querySettings, ODataQueryOptions queryOptions) + ODataQueryContext context = skipTokenQueryOption.Context; + if (context.ElementClrType == null) { - if (query == null) - { - throw Error.ArgumentNull(nameof(query)); - } + throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); + } - if (skipTokenQueryOption == null) - { - throw Error.ArgumentNull(nameof(skipTokenQueryOption)); - } + return ApplyToCore(query, querySettings, queryOptions, skipTokenQueryOption.RawValue); + } - if (querySettings == null) - { - throw Error.ArgumentNull(nameof(querySettings)); - } + private static IQueryable ApplyToCore(IQueryable query, ODataQuerySettings querySettings, + ODataQueryOptions queryOptions, string skipTokenRawValue) + { + OrderByQueryOption orderByOption = queryOptions?.GenerateStableOrder(); + if (orderByOption == null) + { + return query; + } - ODataQueryContext context = skipTokenQueryOption.Context; - if (context.ElementClrType == null) - { - throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); - } + IList orderBys = orderByOption.GetOrderByRawValues(); + IList orderByClauses = orderByOption.OrderByClause.ToList(); + IList tokenValues = PopulatePropertyValues(skipTokenRawValue); - return ApplyToCore(query, querySettings, queryOptions, skipTokenQueryOption.RawValue); + if (orderBys.Count != orderByClauses.Count && orderByClauses.Count != tokenValues.Count) + { + throw Error.InvalidOperation(SRResources.SkipTokenProcessingError); } - private static IQueryable ApplyToCore(IQueryable query, ODataQuerySettings querySettings, - ODataQueryOptions queryOptions, string skipTokenRawValue) + string where = string.Empty; + string previousEquality = null; + bool isFirst = true; + for (int i = 0; i < orderBys.Count; ++i) { - OrderByQueryOption orderByOption = queryOptions?.GenerateStableOrder(); - if (orderByOption == null) - { - return query; - } - - IList orderBys = orderByOption.GetOrderByRawValues(); - IList orderByClauses = orderByOption.OrderByClause.ToList(); - IList tokenValues = PopulatePropertyValues(skipTokenRawValue); - - if (orderBys.Count != orderByClauses.Count && orderByClauses.Count != tokenValues.Count) - { - throw Error.InvalidOperation(SRResources.SkipTokenProcessingError); - } + OrderByClause orderBy = orderByClauses[i]; + string orderByRaw = orderBys[i]; + string orderByValue = tokenValues[i]; + bool isNullValue = string.Equals(orderByValue, "null", StringComparison.OrdinalIgnoreCase); - string where = string.Empty; - string previousEquality = null; - bool isFirst = true; - for (int i = 0; i < orderBys.Count; ++i) + string compare; + if (orderBy.Direction == OrderByDirection.Descending) { - OrderByClause orderBy = orderByClauses[i]; - string orderByRaw = orderBys[i]; - string orderByValue = tokenValues[i]; - bool isNullValue = string.Equals(orderByValue, "null", StringComparison.OrdinalIgnoreCase); + // In descending ordering, the 'null' value goes later. + orderByRaw = orderByRaw.RemoveDirection(" desc"); - string compare; - if (orderBy.Direction == OrderByDirection.Descending) + if (isNullValue) { - // In descending ordering, the 'null' value goes later. - orderByRaw = orderByRaw.RemoveDirection(" desc"); - - if (isNullValue) - { - compare = "false"; // Dummy compare, always false - } - else - { - // (prop < value) OR (prop == null) - compare = $"(({orderByRaw} lt {orderByValue}) or ({orderByRaw} eq null))"; - } + compare = "false"; // Dummy compare, always false } else { - orderByRaw = orderByRaw.RemoveDirection(" asc"); - - if (isNullValue) - { - // We are aiming for the following expression - // when value is null in the ascending order scenario: - // (Prop1 != null) OR ((Prop1 == null) AND (Prop2 > Value2)) ... - compare = $"({orderByRaw} ne null)"; - } - else - { - // We are aiming for the following expression - // when value is NOT null in the ascending order scenario: - // (Prop1 > Value1) OR ((Prop1 == Value1) AND (Prop2 > Value2)) ... - compare = $"({orderByRaw} gt {orderByValue})"; - } + // (prop < value) OR (prop == null) + compare = $"(({orderByRaw} lt {orderByValue}) or ({orderByRaw} eq null))"; } + } + else + { + orderByRaw = orderByRaw.RemoveDirection(" asc"); - if (isFirst) + if (isNullValue) { - previousEquality = $"({orderByRaw} eq {orderByValue})"; - where = compare; - isFirst = false; + // We are aiming for the following expression + // when value is null in the ascending order scenario: + // (Prop1 != null) OR ((Prop1 == null) AND (Prop2 > Value2)) ... + compare = $"({orderByRaw} ne null)"; } else { - string condition = $"({previousEquality} and {compare})"; - where = $"({where} or {condition})"; - previousEquality = $"({previousEquality} and ({orderByRaw} eq {orderByValue}))"; + // We are aiming for the following expression + // when value is NOT null in the ascending order scenario: + // (Prop1 > Value1) OR ((Prop1 == Value1) AND (Prop2 > Value2)) ... + compare = $"({orderByRaw} gt {orderByValue})"; } } - FilterQueryOption filter = new FilterQueryOption(where, queryOptions.Context); - filter.Compute = queryOptions.Compute; - - return filter.ApplyTo(query, querySettings); - } - - internal static string[] PopulatePropertyValues(string value) - { - IList keyValuesPairs = ParseValue(value, CommaDelimiter); - string[] items = new string[keyValuesPairs.Count]; - int index = 0; - foreach (string pair in keyValuesPairs) + if (isFirst) { - // Since the original design is to use '-' to split the "propertyName" and "PropertyValue". - // Now, the keyValuePairs should only contain the property value and the property value itself could contain '-'. - // So far, the possible problem is for 'DateTimeOffset', 'Date', 'Guid' or a negative value (-42). - // Actually, I think it's better to use '=' to split the name and value. - // For back-compatibility, let's keep use '-' - string trimmedPair = pair.Trim(); - if (trimmedPair.StartsWith('-')) - { - items[index] = trimmedPair; - ++index; - continue; - } + previousEquality = $"({orderByRaw} eq {orderByValue})"; + where = compare; + isFirst = false; + } + else + { + string condition = $"({previousEquality} and {compare})"; + where = $"({where} or {condition})"; + previousEquality = $"({previousEquality} and ({orderByRaw} eq {orderByValue}))"; + } + } - if (DateTimeOffset.TryParse(trimmedPair, out _) || - Date.TryParse(trimmedPair, out _) || - Guid.TryParse(trimmedPair, out _)) - { - items[index] = trimmedPair; - ++index; - continue; - } + FilterQueryOption filter = new FilterQueryOption(where, queryOptions.Context); + filter.Compute = queryOptions.Compute; - // It should work for: abc--42 - IList pieces = trimmedPair.Split(new char[] { propertyDelimiter }, 2); - if (pieces.Count == 1) - { - // without the property name, only contains the value - items[index] = pieces[0]; - } - else if (pieces.Count == 2 && !string.IsNullOrWhiteSpace(pieces[0])) - { - // with the property name, only contains the value - items[index] = pieces[1]; - } - else - { - throw new ODataException(Error.Format(SRResources.SkipTokenParseError, value)); - } + return filter.ApplyTo(query, querySettings); + } + internal static string[] PopulatePropertyValues(string value) + { + IList keyValuesPairs = ParseValue(value, CommaDelimiter); + string[] items = new string[keyValuesPairs.Count]; + int index = 0; + foreach (string pair in keyValuesPairs) + { + // Since the original design is to use '-' to split the "propertyName" and "PropertyValue". + // Now, the keyValuePairs should only contain the property value and the property value itself could contain '-'. + // So far, the possible problem is for 'DateTimeOffset', 'Date', 'Guid' or a negative value (-42). + // Actually, I think it's better to use '=' to split the name and value. + // For back-compatibility, let's keep use '-' + string trimmedPair = pair.Trim(); + if (trimmedPair.StartsWith('-')) + { + items[index] = trimmedPair; ++index; + continue; } - return items; - } - - /// - /// Get all orderby clause and append the keys if apply - /// - /// The last object. - /// The edm model. - /// The original orderby clause. - /// The orderby clauses, since we don't need the 'ThenBy' for each clause, we didn't need to change it. - internal static List GetOrderByClauses(object lastMember, IEdmModel model, OrderByClause clause) - { - IEdmType edmType = GetTypeFromObject(lastMember, model); - IEdmEntityType entity = edmType as IEdmEntityType; - if (entity == null) + if (DateTimeOffset.TryParse(trimmedPair, out _) || + Date.TryParse(trimmedPair, out _) || + Guid.TryParse(trimmedPair, out _)) { - return null; + items[index] = trimmedPair; + ++index; + continue; } - List orderByClauses = new List(); - HashSet properties = new HashSet(); - while (clause != null) + // It should work for: abc--42 + IList pieces = trimmedPair.Split(new char[] { propertyDelimiter }, 2); + if (pieces.Count == 1) + { + // without the property name, only contains the value + items[index] = pieces[0]; + } + else if (pieces.Count == 2 && !string.IsNullOrWhiteSpace(pieces[0])) + { + // with the property name, only contains the value + items[index] = pieces[1]; + } + else { - // Be noted, the 'ThenBy' doesn't reset to null. It doesn't matter since we don't use it. - orderByClauses.Add(clause); + throw new ODataException(Error.Format(SRResources.SkipTokenParseError, value)); + } - if (clause.IsTopLevelSingleProperty(out IEdmProperty property, out _)) - { - properties.Add(property); - } + ++index; + } - clause = clause.ThenBy; - } + return items; + } - ResourceRangeVariable rangeVar = new ResourceRangeVariable("$it", new EdmEntityTypeReference(entity, true), navigationSource: null); - ResourceRangeVariableReferenceNode rangeNode = new ResourceRangeVariableReferenceNode("$it", rangeVar); - IEnumerable key = entity.Key(); - foreach (IEdmProperty subKey in key) + /// + /// Get all orderby clause and append the keys if apply + /// + /// The last object. + /// The edm model. + /// The original orderby clause. + /// The orderby clauses, since we don't need the 'ThenBy' for each clause, we didn't need to change it. + internal static List GetOrderByClauses(object lastMember, IEdmModel model, OrderByClause clause) + { + IEdmType edmType = GetTypeFromObject(lastMember, model); + IEdmEntityType entity = edmType as IEdmEntityType; + if (entity == null) + { + return null; + } + + List orderByClauses = new List(); + HashSet properties = new HashSet(); + while (clause != null) + { + // Be noted, the 'ThenBy' doesn't reset to null. It doesn't matter since we don't use it. + orderByClauses.Add(clause); + + if (clause.IsTopLevelSingleProperty(out IEdmProperty property, out _)) { - if (!properties.Contains(subKey)) - { - SingleValuePropertyAccessNode node = new SingleValuePropertyAccessNode(rangeNode, subKey); - OrderByClause keyOrderBy = new OrderByClause(null, node, OrderByDirection.Ascending, rangeVar); - orderByClauses.Add(keyOrderBy); - } + properties.Add(property); } - return orderByClauses; + clause = clause.ThenBy; } - /// - /// Gets the EdmType from the Instance which may be a select expand wrapper. - /// - /// Instance for which the edmType needs to be computed. - /// IEdmModel - /// The EdmType of the underlying instance. - internal static IEdmType GetTypeFromObject(object value, IEdmModel model) + ResourceRangeVariable rangeVar = new ResourceRangeVariable("$it", new EdmEntityTypeReference(entity, true), navigationSource: null); + ResourceRangeVariableReferenceNode rangeNode = new ResourceRangeVariableReferenceNode("$it", rangeVar); + IEnumerable key = entity.Key(); + foreach (IEdmProperty subKey in key) { - SelectExpandWrapper selectExpand = value as SelectExpandWrapper; - if (selectExpand != null) + if (!properties.Contains(subKey)) { - IEdmTypeReference typeReference = selectExpand.GetEdmType(); - return typeReference.Definition; + SingleValuePropertyAccessNode node = new SingleValuePropertyAccessNode(rangeNode, subKey); + OrderByClause keyOrderBy = new OrderByClause(null, node, OrderByDirection.Ascending, rangeVar); + orderByClauses.Add(keyOrderBy); } + } + + return orderByClauses; + } - Type clrType = value.GetType(); - return model.GetEdmTypeReference(clrType)?.Definition; + /// + /// Gets the EdmType from the Instance which may be a select expand wrapper. + /// + /// Instance for which the edmType needs to be computed. + /// IEdmModel + /// The EdmType of the underlying instance. + internal static IEdmType GetTypeFromObject(object value, IEdmModel model) + { + SelectExpandWrapper selectExpand = value as SelectExpandWrapper; + if (selectExpand != null) + { + IEdmTypeReference typeReference = selectExpand.GetEdmType(); + return typeReference.Definition; } - private static IList ParseValue(string value, char delim) + Type clrType = value.GetType(); + return model.GetEdmTypeReference(clrType)?.Definition; + } + + private static IList ParseValue(string value, char delim) + { + IList results = new List(); + StringBuilder escapedStringBuilder = new StringBuilder(); + for (int i = 0; i < value.Length; i++) { - IList results = new List(); - StringBuilder escapedStringBuilder = new StringBuilder(); - for (int i = 0; i < value.Length; i++) + if (value[i] == '\'' || value[i] == '"') { - if (value[i] == '\'' || value[i] == '"') + escapedStringBuilder.Append(value[i]); + char openingQuoteChar = value[i]; + i++; + while (i < value.Length && value[i] != openingQuoteChar) { - escapedStringBuilder.Append(value[i]); - char openingQuoteChar = value[i]; - i++; - while (i < value.Length && value[i] != openingQuoteChar) - { - escapedStringBuilder.Append(value[i++]); - } - - if (i != value.Length) - { - escapedStringBuilder.Append(value[i]); - } - } - else if (value[i] == delim) - { - results.Add(escapedStringBuilder.ToString()); - escapedStringBuilder.Clear(); + escapedStringBuilder.Append(value[i++]); } - else + + if (i != value.Length) { escapedStringBuilder.Append(value[i]); } } - - string lastPair = escapedStringBuilder.ToString(); - if (!String.IsNullOrWhiteSpace(lastPair)) + else if (value[i] == delim) { - results.Add(lastPair); + results.Add(escapedStringBuilder.ToString()); + escapedStringBuilder.Clear(); } + else + { + escapedStringBuilder.Append(value[i]); + } + } - return results; + string lastPair = escapedStringBuilder.ToString(); + if (!String.IsNullOrWhiteSpace(lastPair)) + { + results.Add(lastPair); } + + return results; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/FilterQueryOption.cs b/src/Microsoft.AspNetCore.OData/Query/Query/FilterQueryOption.cs index 23319447b..21599e3e9 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/FilterQueryOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/FilterQueryOption.cs @@ -14,179 +14,178 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This defines a $filter OData query option for querying. +/// +public class FilterQueryOption { + private FilterClause _filterClause; + private ODataQueryOptionParser _queryOptionParser; + /// - /// This defines a $filter OData query option for querying. + /// Initialize a new instance of based on the raw $filter value and + /// an EdmModel from . /// - public class FilterQueryOption + /// The raw value for $filter query. It can be null or empty. + /// The which contains the and some type information + /// The which is used to parse the query option. + public FilterQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) { - private FilterClause _filterClause; - private ODataQueryOptionParser _queryOptionParser; - - /// - /// Initialize a new instance of based on the raw $filter value and - /// an EdmModel from . - /// - /// The raw value for $filter query. It can be null or empty. - /// The which contains the and some type information - /// The which is used to parse the query option. - public FilterQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull("context"); - } + throw Error.ArgumentNull("context"); + } - if (String.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty("rawValue"); - } + if (String.IsNullOrEmpty(rawValue)) + { + throw Error.ArgumentNullOrEmpty("rawValue"); + } - if (queryOptionParser == null) - { - throw Error.ArgumentNull("queryOptionParser"); - } + if (queryOptionParser == null) + { + throw Error.ArgumentNull("queryOptionParser"); + } + + Context = context; + RawValue = rawValue; + Validator = context.GetFilterQueryValidator(); + _queryOptionParser = queryOptionParser; + } + + internal FilterQueryOption(ODataQueryContext context, FilterClause filterClause) + { + _filterClause = filterClause; + Context = context; + Validator = context.GetFilterQueryValidator(); + } - Context = context; - RawValue = rawValue; - Validator = context.GetFilterQueryValidator(); - _queryOptionParser = queryOptionParser; + // This constructor is intended for unit testing only. + internal FilterQueryOption(string rawValue, ODataQueryContext context) + { + if (context == null) + { + throw Error.ArgumentNull("context"); } - internal FilterQueryOption(ODataQueryContext context, FilterClause filterClause) + if (String.IsNullOrEmpty(rawValue)) { - _filterClause = filterClause; - Context = context; - Validator = context.GetFilterQueryValidator(); + throw Error.ArgumentNullOrEmpty("rawValue"); } - // This constructor is intended for unit testing only. - internal FilterQueryOption(string rawValue, ODataQueryContext context) + Context = context; + RawValue = rawValue; + Validator = context.GetFilterQueryValidator(); + _queryOptionParser = new ODataQueryOptionParser( + context.Model, + context.ElementType, + context.NavigationSource, + new Dictionary { { "$filter", rawValue } }, + context.RequestContainer); + + if (context.RequestContainer == null) { - if (context == null) - { - throw Error.ArgumentNull("context"); - } + // By default, let's enable the property name case-insensitive + _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; + } + } - if (String.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty("rawValue"); - } + /// + /// Gets the given . + /// + public ODataQueryContext Context { get; private set; } - Context = context; - RawValue = rawValue; - Validator = context.GetFilterQueryValidator(); - _queryOptionParser = new ODataQueryOptionParser( - context.Model, - context.ElementType, - context.NavigationSource, - new Dictionary { { "$filter", rawValue } }, - context.RequestContainer); - - if (context.RequestContainer == null) + /// + /// Gets or sets the Filter Query Validator + /// + public IFilterQueryValidator Validator { get; set; } + + /// + /// Gets or sets the . + /// + public ComputeQueryOption Compute { get; set; } + + /// + /// Gets the parsed for this query option. + /// + public FilterClause FilterClause + { + get + { + if (_filterClause == null) { - // By default, let's enable the property name case-insensitive - _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; + _filterClause = _queryOptionParser.ParseFilter(); + SingleValueNode filterExpression = _filterClause.Expression.Accept( + new ParameterAliasNodeTranslator(_queryOptionParser.ParameterAliasNodes)) as SingleValueNode; + filterExpression = filterExpression ?? new ConstantNode(null); + _filterClause = new FilterClause(filterExpression, _filterClause.RangeVariable); } + + return _filterClause; } + internal set { _filterClause = value; } + } - /// - /// Gets the given . - /// - public ODataQueryContext Context { get; private set; } - - /// - /// Gets or sets the Filter Query Validator - /// - public IFilterQueryValidator Validator { get; set; } - - /// - /// Gets or sets the . - /// - public ComputeQueryOption Compute { get; set; } - - /// - /// Gets the parsed for this query option. - /// - public FilterClause FilterClause + /// + /// Gets the raw $filter value. + /// + public string RawValue { get; private set; } + + /// + /// Apply the filter query to the given IQueryable. + /// + /// + /// The property specifies + /// how this method should handle null propagation. + /// + /// The original . + /// The that contains all the query application related settings. + /// The new after the filter query has been applied to. + public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + { + if (query == null) { - get - { - if (_filterClause == null) - { - _filterClause = _queryOptionParser.ParseFilter(); - SingleValueNode filterExpression = _filterClause.Expression.Accept( - new ParameterAliasNodeTranslator(_queryOptionParser.ParameterAliasNodes)) as SingleValueNode; - filterExpression = filterExpression ?? new ConstantNode(null); - _filterClause = new FilterClause(filterExpression, _filterClause.RangeVariable); - } - - return _filterClause; - } - internal set { _filterClause = value; } + throw Error.ArgumentNull("query"); + } + if (querySettings == null) + { + throw Error.ArgumentNull("querySettings"); } - /// - /// Gets the raw $filter value. - /// - public string RawValue { get; private set; } - - /// - /// Apply the filter query to the given IQueryable. - /// - /// - /// The property specifies - /// how this method should handle null propagation. - /// - /// The original . - /// The that contains all the query application related settings. - /// The new after the filter query has been applied to. - public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + if (Context.ElementClrType == null) { - if (query == null) - { - throw Error.ArgumentNull("query"); - } - if (querySettings == null) - { - throw Error.ArgumentNull("querySettings"); - } + throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); + } - if (Context.ElementClrType == null) - { - throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); - } + FilterClause filterClause = FilterClause; + Contract.Assert(filterClause != null); - FilterClause filterClause = FilterClause; - Contract.Assert(filterClause != null); + QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, Context.ElementClrType); - QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, Context.ElementClrType); + if (Compute != null) + { + binderContext.AddComputedProperties(Compute.ComputeClause.ComputedItems); + } - if (Compute != null) - { - binderContext.AddComputedProperties(Compute.ComputeClause.ComputedItems); - } + IFilterBinder binder = Context.GetFilterBinder(); + return binder.ApplyBind(query, filterClause, binderContext); + } - IFilterBinder binder = Context.GetFilterBinder(); - return binder.ApplyBind(query, filterClause, binderContext); + /// + /// Validate the filter query based on the given . It throws an ODataException if validation failed. + /// + /// The instance which contains all the validation settings. + public void Validate(ODataValidationSettings validationSettings) + { + if (validationSettings == null) + { + throw Error.ArgumentNull(nameof(validationSettings)); } - /// - /// Validate the filter query based on the given . It throws an ODataException if validation failed. - /// - /// The instance which contains all the validation settings. - public void Validate(ODataValidationSettings validationSettings) + if (Validator != null) { - if (validationSettings == null) - { - throw Error.ArgumentNull(nameof(validationSettings)); - } - - if (Validator != null) - { - Validator.Validate(this, validationSettings); - } + Validator.Validate(this, validationSettings); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/ODataRawQueryOptions.cs b/src/Microsoft.AspNetCore.OData/Query/Query/ODataRawQueryOptions.cs index ea052f278..297856181 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/ODataRawQueryOptions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/ODataRawQueryOptions.cs @@ -5,76 +5,75 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Represents the raw query values in the string format from the incoming request. +/// +public class ODataRawQueryOptions { /// - /// Represents the raw query values in the string format from the incoming request. + /// Gets the raw $filter query value from the incoming request Uri if exists. /// - public class ODataRawQueryOptions - { - /// - /// Gets the raw $filter query value from the incoming request Uri if exists. - /// - public string Filter { get; internal set; } + public string Filter { get; internal set; } - /// - /// Gets the raw $apply query value from the incoming request Uri if exists. - /// - public string Apply { get; internal set; } + /// + /// Gets the raw $apply query value from the incoming request Uri if exists. + /// + public string Apply { get; internal set; } - /// - /// Gets the raw $compute query value from the incoming request Uri if exists. - /// - public string Compute { get; internal set; } + /// + /// Gets the raw $compute query value from the incoming request Uri if exists. + /// + public string Compute { get; internal set; } - /// - /// Gets the raw $search query value from the incoming request Uri if exists. - /// - public string Search { get; internal set; } + /// + /// Gets the raw $search query value from the incoming request Uri if exists. + /// + public string Search { get; internal set; } - /// - /// Gets the raw $orderby query value from the incoming request Uri if exists. - /// - public string OrderBy { get; internal set; } + /// + /// Gets the raw $orderby query value from the incoming request Uri if exists. + /// + public string OrderBy { get; internal set; } - /// - /// Gets the raw $top query value from the incoming request Uri if exists. - /// - public string Top { get; internal set; } + /// + /// Gets the raw $top query value from the incoming request Uri if exists. + /// + public string Top { get; internal set; } - /// - /// Gets the raw $skip query value from the incoming request Uri if exists. - /// - public string Skip { get; internal set; } + /// + /// Gets the raw $skip query value from the incoming request Uri if exists. + /// + public string Skip { get; internal set; } - /// - /// Gets the raw $select query value from the incoming request Uri if exists. - /// - public string Select { get; internal set; } + /// + /// Gets the raw $select query value from the incoming request Uri if exists. + /// + public string Select { get; internal set; } - /// - /// Gets the raw $expand query value from the incoming request Uri if exists. - /// - public string Expand { get; internal set; } + /// + /// Gets the raw $expand query value from the incoming request Uri if exists. + /// + public string Expand { get; internal set; } - /// - /// Gets the raw $count query value from the incoming request Uri if exists. - /// - public string Count { get; internal set; } + /// + /// Gets the raw $count query value from the incoming request Uri if exists. + /// + public string Count { get; internal set; } - /// - /// Gets the raw $format query value from the incoming request Uri if exists. - /// - public string Format { get; internal set; } + /// + /// Gets the raw $format query value from the incoming request Uri if exists. + /// + public string Format { get; internal set; } - /// - /// Gets the raw $skiptoken query value from the incoming request Uri if exists. - /// - public string SkipToken { get; internal set; } + /// + /// Gets the raw $skiptoken query value from the incoming request Uri if exists. + /// + public string SkipToken { get; internal set; } - /// - /// Gets the raw $deltatoken query value from the incoming request Uri if exists. - /// - public string DeltaToken { get; internal set; } - } + /// + /// Gets the raw $deltatoken query value from the incoming request Uri if exists. + /// + public string DeltaToken { get; internal set; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/OrderByQueryOption.cs b/src/Microsoft.AspNetCore.OData/Query/Query/OrderByQueryOption.cs index f680e7e33..94fb56753 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/OrderByQueryOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/OrderByQueryOption.cs @@ -15,389 +15,388 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This defines a $orderby OData query option for querying. +/// +public class OrderByQueryOption { + private OrderByClause _orderByClause; + private IList _orderByNodes; + private ODataQueryOptionParser _queryOptionParser; + /// - /// This defines a $orderby OData query option for querying. + /// Initialize a new instance of based on the raw $orderby value and + /// an EdmModel from . /// - public class OrderByQueryOption + /// The raw value for $orderby query. It can be null or empty. + /// The which contains the and some type information + /// The which is used to parse the query option. + public OrderByQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) { - private OrderByClause _orderByClause; - private IList _orderByNodes; - private ODataQueryOptionParser _queryOptionParser; - - /// - /// Initialize a new instance of based on the raw $orderby value and - /// an EdmModel from . - /// - /// The raw value for $orderby query. It can be null or empty. - /// The which contains the and some type information - /// The which is used to parse the query option. - public OrderByQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull("context"); - } - - if (String.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty("rawValue"); - } - - if (queryOptionParser == null) - { - throw Error.ArgumentNull("queryOptionParser"); - } + throw Error.ArgumentNull("context"); + } - Context = context; - RawValue = rawValue; - Validator = context.GetOrderByQueryValidator(); - _queryOptionParser = queryOptionParser; + if (String.IsNullOrEmpty(rawValue)) + { + throw Error.ArgumentNullOrEmpty("rawValue"); } - internal OrderByQueryOption(string rawValue, ODataQueryContext context, string applyRaw) + if (queryOptionParser == null) { - if (context == null) - { - throw Error.ArgumentNull("context"); - } + throw Error.ArgumentNull("queryOptionParser"); + } - if (String.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty("rawValue"); - } + Context = context; + RawValue = rawValue; + Validator = context.GetOrderByQueryValidator(); + _queryOptionParser = queryOptionParser; + } - if (applyRaw == null) - { - throw Error.ArgumentNullOrEmpty("applyRaw"); - } + internal OrderByQueryOption(string rawValue, ODataQueryContext context, string applyRaw) + { + if (context == null) + { + throw Error.ArgumentNull("context"); + } - Context = context; - RawValue = rawValue; - Validator = context.GetOrderByQueryValidator(); - _queryOptionParser = new ODataQueryOptionParser( - context.Model, - context.ElementType, - context.NavigationSource, - new Dictionary { { "$orderby", rawValue }, { "$apply", applyRaw } }, - context.RequestContainer); - - if (context.RequestContainer == null) - { - // By default, let's enable the property name case-insensitive - _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; - } + if (String.IsNullOrEmpty(rawValue)) + { + throw Error.ArgumentNullOrEmpty("rawValue"); + } - _queryOptionParser.ParseApply(); + if (applyRaw == null) + { + throw Error.ArgumentNullOrEmpty("applyRaw"); } - // This constructor is intended for unit testing only. - internal OrderByQueryOption(string rawValue, ODataQueryContext context) + Context = context; + RawValue = rawValue; + Validator = context.GetOrderByQueryValidator(); + _queryOptionParser = new ODataQueryOptionParser( + context.Model, + context.ElementType, + context.NavigationSource, + new Dictionary { { "$orderby", rawValue }, { "$apply", applyRaw } }, + context.RequestContainer); + + if (context.RequestContainer == null) { - if (context == null) - { - throw Error.ArgumentNull("context"); - } + // By default, let's enable the property name case-insensitive + _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; + } - if (String.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty("rawValue"); - } + _queryOptionParser.ParseApply(); + } - Context = context; - RawValue = rawValue; - Validator = context.GetOrderByQueryValidator(); - _queryOptionParser = new ODataQueryOptionParser( - context.Model, - context.ElementType, - context.NavigationSource, - new Dictionary { { "$orderby", rawValue } }, - context.RequestContainer); - - if (context.RequestContainer == null) - { - // By default, let's enable the property name case-insensitive - _queryOptionParser.Resolver.EnableCaseInsensitive = true; - } + // This constructor is intended for unit testing only. + internal OrderByQueryOption(string rawValue, ODataQueryContext context) + { + if (context == null) + { + throw Error.ArgumentNull("context"); } - internal OrderByQueryOption(OrderByQueryOption orderBy) + if (String.IsNullOrEmpty(rawValue)) { - Context = orderBy.Context; - RawValue = orderBy.RawValue; - Validator = orderBy.Validator; - _queryOptionParser = orderBy._queryOptionParser; - _orderByClause = orderBy._orderByClause; - _orderByNodes = orderBy._orderByNodes; + throw Error.ArgumentNullOrEmpty("rawValue"); } - /// - /// Gets the given . - /// - public ODataQueryContext Context { get; private set; } + Context = context; + RawValue = rawValue; + Validator = context.GetOrderByQueryValidator(); + _queryOptionParser = new ODataQueryOptionParser( + context.Model, + context.ElementType, + context.NavigationSource, + new Dictionary { { "$orderby", rawValue } }, + context.RequestContainer); + + if (context.RequestContainer == null) + { + // By default, let's enable the property name case-insensitive + _queryOptionParser.Resolver.EnableCaseInsensitive = true; + } + } + + internal OrderByQueryOption(OrderByQueryOption orderBy) + { + Context = orderBy.Context; + RawValue = orderBy.RawValue; + Validator = orderBy.Validator; + _queryOptionParser = orderBy._queryOptionParser; + _orderByClause = orderBy._orderByClause; + _orderByNodes = orderBy._orderByNodes; + } - /// - /// Gets the mutable list of instances for this query option. - /// - public IList OrderByNodes + /// + /// Gets the given . + /// + public ODataQueryContext Context { get; private set; } + + /// + /// Gets the mutable list of instances for this query option. + /// + public IList OrderByNodes + { + get { - get + if (_orderByNodes == null) { - if (_orderByNodes == null) - { - _orderByNodes = OrderByNode.CreateCollection(OrderByClause); - } - return _orderByNodes; + _orderByNodes = OrderByNode.CreateCollection(OrderByClause); } + return _orderByNodes; } + } - /// - /// Gets the raw $orderby value. - /// - public string RawValue { get; private set; } - - /// - /// Gets or sets the OrderBy Query Validator. - /// - public IOrderByQueryValidator Validator { get; set; } - - /// - /// Gets or sets the . - /// - public ComputeQueryOption Compute { get; set; } - - /// - /// Gets the parsed for this query option. - /// - public OrderByClause OrderByClause + /// + /// Gets the raw $orderby value. + /// + public string RawValue { get; private set; } + + /// + /// Gets or sets the OrderBy Query Validator. + /// + public IOrderByQueryValidator Validator { get; set; } + + /// + /// Gets or sets the . + /// + public ComputeQueryOption Compute { get; set; } + + /// + /// Gets the parsed for this query option. + /// + public OrderByClause OrderByClause + { + get { - get + if (_orderByClause == null) { - if (_orderByClause == null) - { - _orderByClause = _queryOptionParser.ParseOrderBy(); - _orderByClause = TranslateParameterAlias(_orderByClause); - } - - return _orderByClause; + _orderByClause = _queryOptionParser.ParseOrderBy(); + _orderByClause = TranslateParameterAlias(_orderByClause); } - } - /// - /// Apply the $orderby query to the given IQueryable. - /// - /// The original . - /// The new after the orderby query has been applied to. - public IOrderedQueryable ApplyTo(IQueryable query) - { - ODataQuerySettings querySettings = Context.GetODataQuerySettings(); - return ApplyToCore(query, querySettings) as IOrderedQueryable; + return _orderByClause; } + } - /// - /// Apply the $orderby query to the given IQueryable. - /// - /// The original . - /// The that contains all the query application related settings. - /// The new after the orderby query has been applied to. - public IOrderedQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) - { - return ApplyToCore(query, querySettings) as IOrderedQueryable; - } + /// + /// Apply the $orderby query to the given IQueryable. + /// + /// The original . + /// The new after the orderby query has been applied to. + public IOrderedQueryable ApplyTo(IQueryable query) + { + ODataQuerySettings querySettings = Context.GetODataQuerySettings(); + return ApplyToCore(query, querySettings) as IOrderedQueryable; + } - /// - /// Apply the $orderby query to the given IQueryable. - /// - /// The original . - /// The new after the orderby query has been applied to. - public IOrderedQueryable ApplyTo(IQueryable query) - { - ODataQuerySettings querySettings = Context.GetODataQuerySettings(); - return ApplyToCore(query, querySettings); - } + /// + /// Apply the $orderby query to the given IQueryable. + /// + /// The original . + /// The that contains all the query application related settings. + /// The new after the orderby query has been applied to. + public IOrderedQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + { + return ApplyToCore(query, querySettings) as IOrderedQueryable; + } - /// - /// Apply the $orderby query to the given IQueryable. - /// - /// The original . - /// The that contains all the query application related settings. - /// The new after the orderby query has been applied to. - public IOrderedQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + /// + /// Apply the $orderby query to the given IQueryable. + /// + /// The original . + /// The new after the orderby query has been applied to. + public IOrderedQueryable ApplyTo(IQueryable query) + { + ODataQuerySettings querySettings = Context.GetODataQuerySettings(); + return ApplyToCore(query, querySettings); + } + + /// + /// Apply the $orderby query to the given IQueryable. + /// + /// The original . + /// The that contains all the query application related settings. + /// The new after the orderby query has been applied to. + public IOrderedQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + { + return ApplyToCore(query, querySettings); + } + + /// + /// Validate the orderby query based on the given . It throws an ODataException if validation failed. + /// + /// The instance which contains all the validation settings. + public void Validate(ODataValidationSettings validationSettings) + { + if (validationSettings == null) { - return ApplyToCore(query, querySettings); + throw Error.ArgumentNull("validationSettings"); } - /// - /// Validate the orderby query based on the given . It throws an ODataException if validation failed. - /// - /// The instance which contains all the validation settings. - public void Validate(ODataValidationSettings validationSettings) + if (Validator != null) { - if (validationSettings == null) - { - throw Error.ArgumentNull("validationSettings"); - } - - if (Validator != null) - { - Validator.Validate(this, validationSettings); - } + Validator.Validate(this, validationSettings); } + } - private IOrderedQueryable ApplyToCore(IQueryable query, ODataQuerySettings querySettings) + private IOrderedQueryable ApplyToCore(IQueryable query, ODataQuerySettings querySettings) + { + if (Context.ElementClrType == null) { - if (Context.ElementClrType == null) - { - throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); - } + throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); + } - ICollection nodes = OrderByNodes; + ICollection nodes = OrderByNodes; - bool alreadyOrdered = false; - IQueryable querySoFar = query; + bool alreadyOrdered = false; + IQueryable querySoFar = query; - HashSet propertiesSoFar = new HashSet(); - HashSet openPropertiesSoFar = new HashSet(); - bool orderByItSeen = false; + HashSet propertiesSoFar = new HashSet(); + HashSet openPropertiesSoFar = new HashSet(); + bool orderByItSeen = false; - IOrderByBinder binder = Context.GetOrderByBinder(); - QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, Context.ElementClrType); - if (Compute != null) - { - binderContext.AddComputedProperties(Compute.ComputeClause.ComputedItems); - } + IOrderByBinder binder = Context.GetOrderByBinder(); + QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, Context.ElementClrType); + if (Compute != null) + { + binderContext.AddComputedProperties(Compute.ComputeClause.ComputedItems); + } - binderContext.EnsureFlattenedProperties(binderContext.CurrentParameter, query); + binderContext.EnsureFlattenedProperties(binderContext.CurrentParameter, query); - foreach (OrderByNode node in nodes) + foreach (OrderByNode node in nodes) + { + if (node is OrderByPropertyNode propertyNode) { - if (node is OrderByPropertyNode propertyNode) - { - // Use autonomy class to achieve value equality for HasSet. - var edmPropertyWithPath = new { propertyNode.Property, propertyNode.PropertyPath }; - OrderByDirection direction = propertyNode.Direction; - - // This check prevents queries with duplicate properties (e.g. $orderby=Id,Id,Id,Id...) from causing stack overflows - if (propertiesSoFar.Contains(edmPropertyWithPath)) - { - throw new ODataException(Error.Format(SRResources.OrderByDuplicateProperty, edmPropertyWithPath.PropertyPath)); - } - - propertiesSoFar.Add(edmPropertyWithPath); - - if (propertyNode.OrderByClause != null) - { - querySoFar = AddOrderByQueryForProperty(binder, propertyNode.OrderByClause, querySoFar, binderContext, alreadyOrdered); - } - else - { - // could have ensure stable orderby property added - querySoFar = ExpressionHelpers.OrderByProperty(querySoFar, Context.Model, edmPropertyWithPath.Property, direction, Context.ElementClrType, alreadyOrdered); - } - - alreadyOrdered = true; - } - else if (node is OrderByOpenPropertyNode openPropertyNode) - { - // This check prevents queries with duplicate properties (e.g. $orderby=Id,Id,Id,Id...) from causing stack overflows - if (openPropertiesSoFar.Contains(openPropertyNode.PropertyName)) - { - throw new ODataException(Error.Format(SRResources.OrderByDuplicateProperty, openPropertyNode.PropertyPath)); - } - - openPropertiesSoFar.Add(openPropertyNode.PropertyName); - Contract.Assert(openPropertyNode.OrderByClause != null); - querySoFar = AddOrderByQueryForProperty(binder, openPropertyNode.OrderByClause, querySoFar, binderContext, alreadyOrdered); - alreadyOrdered = true; - } - else if (node is OrderByCountNode countNode) + // Use autonomy class to achieve value equality for HasSet. + var edmPropertyWithPath = new { propertyNode.Property, propertyNode.PropertyPath }; + OrderByDirection direction = propertyNode.Direction; + + // This check prevents queries with duplicate properties (e.g. $orderby=Id,Id,Id,Id...) from causing stack overflows + if (propertiesSoFar.Contains(edmPropertyWithPath)) { - Contract.Assert(countNode.OrderByClause != null); - querySoFar = AddOrderByQueryForProperty(binder, countNode.OrderByClause, querySoFar, binderContext, alreadyOrdered); - alreadyOrdered = true; + throw new ODataException(Error.Format(SRResources.OrderByDuplicateProperty, edmPropertyWithPath.PropertyPath)); } - else if (node is OrderByClauseNode clauseNode) + + propertiesSoFar.Add(edmPropertyWithPath); + + if (propertyNode.OrderByClause != null) { - querySoFar = AddOrderByQueryForProperty(binder, clauseNode.OrderByClause, querySoFar, binderContext, alreadyOrdered); - alreadyOrdered = true; + querySoFar = AddOrderByQueryForProperty(binder, propertyNode.OrderByClause, querySoFar, binderContext, alreadyOrdered); } else { - // This check prevents queries with duplicate nodes (e.g. $orderby=$it,$it,$it,$it...) from causing stack overflows - if (orderByItSeen) - { - throw new ODataException(Error.Format(SRResources.OrderByDuplicateIt)); - } - - querySoFar = ExpressionHelpers.OrderByIt(querySoFar, node.Direction, Context.ElementClrType, alreadyOrdered); - alreadyOrdered = true; - orderByItSeen = true; + // could have ensure stable orderby property added + querySoFar = ExpressionHelpers.OrderByProperty(querySoFar, Context.Model, edmPropertyWithPath.Property, direction, Context.ElementClrType, alreadyOrdered); } - } - return querySoFar as IOrderedQueryable; - } + alreadyOrdered = true; + } + else if (node is OrderByOpenPropertyNode openPropertyNode) + { + // This check prevents queries with duplicate properties (e.g. $orderby=Id,Id,Id,Id...) from causing stack overflows + if (openPropertiesSoFar.Contains(openPropertyNode.PropertyName)) + { + throw new ODataException(Error.Format(SRResources.OrderByDuplicateProperty, openPropertyNode.PropertyPath)); + } - internal List GetOrderByRawValues() - { - // If the raw value doesn't contain ',', we don't need to process more. - // If only one expression (no matter whether it contains ','), we don't need to process more. - if (!RawValue.Contains(',') || OrderByClause.ThenBy == null) + openPropertiesSoFar.Add(openPropertyNode.PropertyName); + Contract.Assert(openPropertyNode.OrderByClause != null); + querySoFar = AddOrderByQueryForProperty(binder, openPropertyNode.OrderByClause, querySoFar, binderContext, alreadyOrdered); + alreadyOrdered = true; + } + else if (node is OrderByCountNode countNode) { - return new List { RawValue }; + Contract.Assert(countNode.OrderByClause != null); + querySoFar = AddOrderByQueryForProperty(binder, countNode.OrderByClause, querySoFar, binderContext, alreadyOrdered); + alreadyOrdered = true; } - - ODataUri oDataUri = new ODataUri + else if (node is OrderByClauseNode clauseNode) { - ServiceRoot = new Uri("http://localhost"), - Path = new ODataPath() - }; - List clauses = new List(); - OrderByClause clause = OrderByClause; - while (clause != null) + querySoFar = AddOrderByQueryForProperty(binder, clauseNode.OrderByClause, querySoFar, binderContext, alreadyOrdered); + alreadyOrdered = true; + } + else { - // Simply remove the 'thenBy' - OrderByClause newClause = new OrderByClause(null, clause.Expression, clause.Direction, clause.RangeVariable); - oDataUri.OrderBy = newClause; - Uri uri = oDataUri.BuildUri(ODataUrlKeyDelimiter.Parentheses); - string orderbyClause = uri.Query.Substring(10);// the length of "?$orderby=" is 10; - clauses.Add(Uri.UnescapeDataString(orderbyClause)); - - clause = clause.ThenBy; + // This check prevents queries with duplicate nodes (e.g. $orderby=$it,$it,$it,$it...) from causing stack overflows + if (orderByItSeen) + { + throw new ODataException(Error.Format(SRResources.OrderByDuplicateIt)); + } + + querySoFar = ExpressionHelpers.OrderByIt(querySoFar, node.Direction, Context.ElementClrType, alreadyOrdered); + alreadyOrdered = true; + orderByItSeen = true; } + } + + return querySoFar as IOrderedQueryable; + } - return clauses; + internal List GetOrderByRawValues() + { + // If the raw value doesn't contain ',', we don't need to process more. + // If only one expression (no matter whether it contains ','), we don't need to process more. + if (!RawValue.Contains(',') || OrderByClause.ThenBy == null) + { + return new List { RawValue }; } - private static IQueryable AddOrderByQueryForProperty(IOrderByBinder orderByBinder, - OrderByClause orderbyClause, IQueryable querySoFar, QueryBinderContext binderContext, bool alreadyOrdered) + ODataUri oDataUri = new ODataUri { - // Remove Thenby (make Thenby == null) to make sure we only apply the top orderby - // TODO: need to refactor it later. - orderbyClause = new OrderByClause(null, orderbyClause.Expression, orderbyClause.Direction, orderbyClause.RangeVariable); + ServiceRoot = new Uri("http://localhost"), + Path = new ODataPath() + }; + List clauses = new List(); + OrderByClause clause = OrderByClause; + while (clause != null) + { + // Simply remove the 'thenBy' + OrderByClause newClause = new OrderByClause(null, clause.Expression, clause.Direction, clause.RangeVariable); + oDataUri.OrderBy = newClause; + Uri uri = oDataUri.BuildUri(ODataUrlKeyDelimiter.Parentheses); + string orderbyClause = uri.Query.Substring(10);// the length of "?$orderby=" is 10; + clauses.Add(Uri.UnescapeDataString(orderbyClause)); + + clause = clause.ThenBy; + } - querySoFar = orderByBinder.ApplyBind(querySoFar, orderbyClause, binderContext, alreadyOrdered); + return clauses; + } - return querySoFar; - } + private static IQueryable AddOrderByQueryForProperty(IOrderByBinder orderByBinder, + OrderByClause orderbyClause, IQueryable querySoFar, QueryBinderContext binderContext, bool alreadyOrdered) + { + // Remove Thenby (make Thenby == null) to make sure we only apply the top orderby + // TODO: need to refactor it later. + orderbyClause = new OrderByClause(null, orderbyClause.Expression, orderbyClause.Direction, orderbyClause.RangeVariable); - private OrderByClause TranslateParameterAlias(OrderByClause orderBy) - { - if (orderBy == null) - { - return null; - } + querySoFar = orderByBinder.ApplyBind(querySoFar, orderbyClause, binderContext, alreadyOrdered); - SingleValueNode orderByExpression = orderBy.Expression.Accept( - new ParameterAliasNodeTranslator(_queryOptionParser.ParameterAliasNodes)) as SingleValueNode; - orderByExpression = orderByExpression ?? new ConstantNode(null, "null"); + return querySoFar; + } - return new OrderByClause( - TranslateParameterAlias(orderBy.ThenBy), - orderByExpression, - orderBy.Direction, - orderBy.RangeVariable); + private OrderByClause TranslateParameterAlias(OrderByClause orderBy) + { + if (orderBy == null) + { + return null; } + + SingleValueNode orderByExpression = orderBy.Expression.Accept( + new ParameterAliasNodeTranslator(_queryOptionParser.ParameterAliasNodes)) as SingleValueNode; + orderByExpression = orderByExpression ?? new ConstantNode(null, "null"); + + return new OrderByClause( + TranslateParameterAlias(orderBy.ThenBy), + orderByExpression, + orderBy.Direction, + orderBy.RangeVariable); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/SearchQueryOption.cs b/src/Microsoft.AspNetCore.OData/Query/Query/SearchQueryOption.cs index c2a5bf125..53e1f5f07 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/SearchQueryOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/SearchQueryOption.cs @@ -12,145 +12,144 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This defines a $search OData query option for querying. +/// The $search system query option restricts the result to include only those items matching the specified search expression. +/// The definition of what it means to match is dependent upon the implementation. +/// +public class SearchQueryOption { + private SearchClause _searchClause; + private ODataQueryOptionParser _queryOptionParser; + /// - /// This defines a $search OData query option for querying. - /// The $search system query option restricts the result to include only those items matching the specified search expression. - /// The definition of what it means to match is dependent upon the implementation. + /// Initialize a new instance of based on the raw $search value and + /// an EdmModel from . /// - public class SearchQueryOption + /// The raw value for $search query. It can be null or empty. + /// The which contains the and some type information + /// The which is used to parse the query option. + public SearchQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) { - private SearchClause _searchClause; - private ODataQueryOptionParser _queryOptionParser; - - /// - /// Initialize a new instance of based on the raw $search value and - /// an EdmModel from . - /// - /// The raw value for $search query. It can be null or empty. - /// The which contains the and some type information - /// The which is used to parse the query option. - public SearchQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) + if (string.IsNullOrEmpty(rawValue)) { - if (string.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty(nameof(rawValue)); - } - - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNullOrEmpty(nameof(rawValue)); + } - if (queryOptionParser == null) - { - throw Error.ArgumentNull(nameof(queryOptionParser)); - } + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - Context = context; - RawValue = rawValue; - _queryOptionParser = queryOptionParser; - ResultClrType = Context.ElementClrType; + if (queryOptionParser == null) + { + throw Error.ArgumentNull(nameof(queryOptionParser)); } - // This constructor is intended for unit testing only. - internal SearchQueryOption(string rawValue, ODataQueryContext context) + Context = context; + RawValue = rawValue; + _queryOptionParser = queryOptionParser; + ResultClrType = Context.ElementClrType; + } + + // This constructor is intended for unit testing only. + internal SearchQueryOption(string rawValue, ODataQueryContext context) + { + if (string.IsNullOrEmpty(rawValue)) { - if (string.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty(nameof(rawValue)); - } + throw Error.ArgumentNullOrEmpty(nameof(rawValue)); + } - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - Context = context; - RawValue = rawValue; + Context = context; + RawValue = rawValue; - _queryOptionParser = new ODataQueryOptionParser( - context.Model, - context.ElementType, - context.NavigationSource, - new Dictionary { { "$search", rawValue } }, - context.RequestContainer); + _queryOptionParser = new ODataQueryOptionParser( + context.Model, + context.ElementType, + context.NavigationSource, + new Dictionary { { "$search", rawValue } }, + context.RequestContainer); - if (context.RequestContainer == null) - { - // By default, let's enable the property name case-insensitive - _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; - } + if (context.RequestContainer == null) + { + // By default, let's enable the property name case-insensitive + _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; } + } - /// - /// Gets the given . - /// - public ODataQueryContext Context { get; } + /// + /// Gets the given . + /// + public ODataQueryContext Context { get; } - /// - /// ClrType for result of transformations - /// - public Type ResultClrType { get; } + /// + /// ClrType for result of transformations + /// + public Type ResultClrType { get; } - /// - /// Gets the parsed for this query option. - /// - public SearchClause SearchClause + /// + /// Gets the parsed for this query option. + /// + public SearchClause SearchClause + { + get { - get + if (_searchClause == null) { - if (_searchClause == null) - { - _searchClause = _queryOptionParser.ParseSearch(); - } - - return _searchClause; + _searchClause = _queryOptionParser.ParseSearch(); } + + return _searchClause; } + } - /// - /// Gets the raw $search value. - /// - public string RawValue { get; } - - /// - /// Apply the $search query to the given IQueryable. - /// - /// - /// The property specifies how this method should handle null propagation. - /// - /// The original . - /// The that contains all the query application related settings. - /// The new after the filter query has been applied to. - public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + /// + /// Gets the raw $search value. + /// + public string RawValue { get; } + + /// + /// Apply the $search query to the given IQueryable. + /// + /// + /// The property specifies how this method should handle null propagation. + /// + /// The original . + /// The that contains all the query application related settings. + /// The new after the filter query has been applied to. + public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + { + if (query == null) { - if (query == null) - { - throw Error.ArgumentNull(nameof(query)); - } + throw Error.ArgumentNull(nameof(query)); + } - if (querySettings == null) - { - throw Error.ArgumentNull(nameof(querySettings)); - } + if (querySettings == null) + { + throw Error.ArgumentNull(nameof(querySettings)); + } - ISearchBinder binder = Context.GetSearchBinder(); - if (binder == null) - { - // If the developer doesn't provide the search binder, let's ignore the $search clause. - return query; - } + ISearchBinder binder = Context.GetSearchBinder(); + if (binder == null) + { + // If the developer doesn't provide the search binder, let's ignore the $search clause. + return query; + } - if (Context.ElementClrType == null) - { - throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); - } + if (Context.ElementClrType == null) + { + throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); + } - QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, Context.ElementClrType); + QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, Context.ElementClrType); - return binder.ApplyBind(query, SearchClause, binderContext); - } + return binder.ApplyBind(query, SearchClause, binderContext); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/SelectExpandQueryOption.cs b/src/Microsoft.AspNetCore.OData/Query/Query/SelectExpandQueryOption.cs index f847c6752..41bf20f32 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/SelectExpandQueryOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/SelectExpandQueryOption.cs @@ -16,772 +16,771 @@ using Microsoft.OData.ModelBuilder.Config; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Represents the OData $select and $expand query options. +/// +public class SelectExpandQueryOption { + private SelectExpandClause _selectExpandClause; + private ODataQueryOptionParser _queryOptionParser; + private SelectExpandClause _processedSelectExpandClause; + + // Give _levelsMaxLiteralExpansionDepth a negative value meaning it is uninitialized, and it will be set to: + // 1. LevelsMaxLiteralExpansionDepth or + // 2. ODataValidationSettings.MaxExpansionDepth + private int _levelsMaxLiteralExpansionDepth = -1; + /// - /// Represents the OData $select and $expand query options. + /// Initializes a new instance of the class. /// - public class SelectExpandQueryOption + /// The $select query parameter value. + /// The $expand query parameter value. + /// The which contains the and some type information. + /// The which is used to parse the query option. + public SelectExpandQueryOption(string select, string expand, ODataQueryContext context, + ODataQueryOptionParser queryOptionParser) { - private SelectExpandClause _selectExpandClause; - private ODataQueryOptionParser _queryOptionParser; - private SelectExpandClause _processedSelectExpandClause; - - // Give _levelsMaxLiteralExpansionDepth a negative value meaning it is uninitialized, and it will be set to: - // 1. LevelsMaxLiteralExpansionDepth or - // 2. ODataValidationSettings.MaxExpansionDepth - private int _levelsMaxLiteralExpansionDepth = -1; - - /// - /// Initializes a new instance of the class. - /// - /// The $select query parameter value. - /// The $expand query parameter value. - /// The which contains the and some type information. - /// The which is used to parse the query option. - public SelectExpandQueryOption(string select, string expand, ODataQueryContext context, - ODataQueryOptionParser queryOptionParser) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - if (string.IsNullOrWhiteSpace(select) && string.IsNullOrWhiteSpace(expand)) - { - throw Error.Argument(SRResources.SelectExpandEmptyOrNull); - } + if (string.IsNullOrWhiteSpace(select) && string.IsNullOrWhiteSpace(expand)) + { + throw Error.Argument(SRResources.SelectExpandEmptyOrNull); + } - if (queryOptionParser == null) - { - throw Error.ArgumentNull(nameof(queryOptionParser)); - } + if (queryOptionParser == null) + { + throw Error.ArgumentNull(nameof(queryOptionParser)); + } - if (!(context.ElementType is IEdmStructuredType)) - { - throw Error.Argument("context", SRResources.SelectNonStructured, context.ElementType.ToTraceString()); - } + if (!(context.ElementType is IEdmStructuredType)) + { + throw Error.Argument("context", SRResources.SelectNonStructured, context.ElementType.ToTraceString()); + } - Context = context; - RawSelect = select; - RawExpand = expand; - Validator = context.GetSelectExpandQueryValidator(); - _queryOptionParser = queryOptionParser; - } - - internal SelectExpandQueryOption( - string select, - string expand, - ODataQueryContext context, - SelectExpandClause selectExpandClause) - : this(select, expand, context) - { - _selectExpandClause = selectExpandClause; - } - - // This constructor is intended for unit testing only. - internal SelectExpandQueryOption(string select, string expand, ODataQueryContext context) - : this(select, expand, context, queryOptionParser: context != null - ? new ODataQueryOptionParser( - context.Model, - context.ElementType, - context.NavigationSource, - new Dictionary - { - { "$select", select }, - { "$expand", expand } - }, - context.RequestContainer) - : null) - { - if (_queryOptionParser != null && context.RequestContainer == null) - { - // By default, let's enable the property name case-insensitive - _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; - } + Context = context; + RawSelect = select; + RawExpand = expand; + Validator = context.GetSelectExpandQueryValidator(); + _queryOptionParser = queryOptionParser; + } + + internal SelectExpandQueryOption( + string select, + string expand, + ODataQueryContext context, + SelectExpandClause selectExpandClause) + : this(select, expand, context) + { + _selectExpandClause = selectExpandClause; + } + + // This constructor is intended for unit testing only. + internal SelectExpandQueryOption(string select, string expand, ODataQueryContext context) + : this(select, expand, context, queryOptionParser: context != null + ? new ODataQueryOptionParser( + context.Model, + context.ElementType, + context.NavigationSource, + new Dictionary + { + { "$select", select }, + { "$expand", expand } + }, + context.RequestContainer) + : null) + { + if (_queryOptionParser != null && context.RequestContainer == null) + { + // By default, let's enable the property name case-insensitive + _queryOptionParser.Resolver = ODataQueryContext.DefaultCaseInsensitiveResolver; } + } - /// - /// Gets the given . - /// - public ODataQueryContext Context { get; private set; } + /// + /// Gets the given . + /// + public ODataQueryContext Context { get; private set; } - /// - /// Gets the raw $select value. - /// - public string RawSelect { get; private set; } + /// + /// Gets the raw $select value. + /// + public string RawSelect { get; private set; } - /// - /// Gets the raw $expand value. - /// - public string RawExpand { get; private set; } + /// + /// Gets the raw $expand value. + /// + public string RawExpand { get; private set; } - /// - /// Gets or sets the . - /// - public ComputeQueryOption Compute { get; set; } + /// + /// Gets or sets the . + /// + public ComputeQueryOption Compute { get; set; } - /// - /// Gets or sets the $select and $expand query validator. - /// - public ISelectExpandQueryValidator Validator { get; set; } + /// + /// Gets or sets the $select and $expand query validator. + /// + public ISelectExpandQueryValidator Validator { get; set; } - /// - /// Gets or sets the . - /// - internal OrderByQueryOption OrderBy { get; set; } + /// + /// Gets or sets the . + /// + internal OrderByQueryOption OrderBy { get; set; } - /// - /// Gets the parsed for this query option. - /// - public SelectExpandClause SelectExpandClause + /// + /// Gets the parsed for this query option. + /// + public SelectExpandClause SelectExpandClause + { + get { - get + if (_selectExpandClause == null) { - if (_selectExpandClause == null) - { - _selectExpandClause = _queryOptionParser.ParseSelectAndExpand(); - } - - return _selectExpandClause; + _selectExpandClause = _queryOptionParser.ParseSelectAndExpand(); } + + return _selectExpandClause; } + } - internal SelectExpandClause ProcessedSelectExpandClause + internal SelectExpandClause ProcessedSelectExpandClause + { + get { - get + if (_processedSelectExpandClause != null) { - if (_processedSelectExpandClause != null) - { - return _processedSelectExpandClause; - } - - _processedSelectExpandClause = this.ProcessLevels(); return _processedSelectExpandClause; } + + _processedSelectExpandClause = this.ProcessLevels(); + return _processedSelectExpandClause; } + } - /// - /// Gets or sets the number of levels that a top level $expand=NavigationProperty($levels=max) - /// will be expanded. - /// This value will decrease by one with each nesting level in the $expand clause. - /// For example, with a property value 5, the following query $expand=A($expand=B($expand=C($levels=max))) - /// will be interpreted as $expand=A($expand=B($expand=C($levels=3))). - /// If the query gets validated, the value - /// must be greater than or equal to this value. - /// - public int LevelsMaxLiteralExpansionDepth + /// + /// Gets or sets the number of levels that a top level $expand=NavigationProperty($levels=max) + /// will be expanded. + /// This value will decrease by one with each nesting level in the $expand clause. + /// For example, with a property value 5, the following query $expand=A($expand=B($expand=C($levels=max))) + /// will be interpreted as $expand=A($expand=B($expand=C($levels=3))). + /// If the query gets validated, the value + /// must be greater than or equal to this value. + /// + public int LevelsMaxLiteralExpansionDepth + { + get { - get - { - return _levelsMaxLiteralExpansionDepth; - } - set - { - if (value < 0) - { - throw Error.ArgumentMustBeGreaterThanOrEqualTo("LevelsMaxLiteralExpansionDepth", value, 0); - } - - _levelsMaxLiteralExpansionDepth = value; - } + return _levelsMaxLiteralExpansionDepth; } - - /// - /// Applies the $select and $expand query options to the given using the given - /// . - /// - /// The original . - /// The that contains all the query application related settings. - /// The new after the filter query has been applied to. - public IQueryable ApplyTo(IQueryable queryable, ODataQuerySettings settings) + set { - if (queryable == null) - { - throw Error.ArgumentNull(nameof(queryable)); - } - if (settings == null) - { - throw Error.ArgumentNull(nameof(settings)); - } - if (Context.ElementClrType == null) + if (value < 0) { - throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); + throw Error.ArgumentMustBeGreaterThanOrEqualTo("LevelsMaxLiteralExpansionDepth", value, 0); } - ISelectExpandBinder binder = Context.GetSelectExpandBinder(); + _levelsMaxLiteralExpansionDepth = value; + } + } - QueryBinderContext binderContext = new QueryBinderContext(Context.Model, settings, Context.ElementClrType) - { - NavigationSource = Context.NavigationSource, - }; + /// + /// Applies the $select and $expand query options to the given using the given + /// . + /// + /// The original . + /// The that contains all the query application related settings. + /// The new after the filter query has been applied to. + public IQueryable ApplyTo(IQueryable queryable, ODataQuerySettings settings) + { + if (queryable == null) + { + throw Error.ArgumentNull(nameof(queryable)); + } + if (settings == null) + { + throw Error.ArgumentNull(nameof(settings)); + } + if (Context.ElementClrType == null) + { + throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); + } - if (Compute != null) - { - binderContext.AddComputedProperties(Compute.ComputeClause.ComputedItems); - } + ISelectExpandBinder binder = Context.GetSelectExpandBinder(); - if (Context.DefaultQueryConfigurations.EnableSkipToken && OrderBy != null) - { - binderContext.OrderByClauses = OrderBy.OrderByClause.ToList(); - binderContext.EnableSkipToken = true; - } + QueryBinderContext binderContext = new QueryBinderContext(Context.Model, settings, Context.ElementClrType) + { + NavigationSource = Context.NavigationSource, + }; - return binder.ApplyBind(queryable, SelectExpandClause, binderContext); + if (Compute != null) + { + binderContext.AddComputedProperties(Compute.ComputeClause.ComputedItems); } - /// - /// Applies the $select and $expand query options to the given entity using the given . - /// - /// The original entity. - /// The that contains all the query application related settings. - /// The new entity after the $select and $expand query has been applied to. - public object ApplyTo(object entity, ODataQuerySettings settings) + if (Context.DefaultQueryConfigurations.EnableSkipToken && OrderBy != null) { - if (entity == null) - { - throw Error.ArgumentNull(nameof(entity)); - } - if (settings == null) - { - throw Error.ArgumentNull(nameof(settings)); - } - if (Context.ElementClrType == null) - { - throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); - } + binderContext.OrderByClauses = OrderBy.OrderByClause.ToList(); + binderContext.EnableSkipToken = true; + } - ISelectExpandBinder binder = Context.GetSelectExpandBinder(); + return binder.ApplyBind(queryable, SelectExpandClause, binderContext); + } - QueryBinderContext binderContext = new QueryBinderContext(Context.Model, settings, Context.ElementClrType) - { - NavigationSource = Context.NavigationSource, - }; + /// + /// Applies the $select and $expand query options to the given entity using the given . + /// + /// The original entity. + /// The that contains all the query application related settings. + /// The new entity after the $select and $expand query has been applied to. + public object ApplyTo(object entity, ODataQuerySettings settings) + { + if (entity == null) + { + throw Error.ArgumentNull(nameof(entity)); + } + if (settings == null) + { + throw Error.ArgumentNull(nameof(settings)); + } + if (Context.ElementClrType == null) + { + throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); + } - if (Compute != null) - { - binderContext.AddComputedProperties(Compute.ComputeClause.ComputedItems); - } + ISelectExpandBinder binder = Context.GetSelectExpandBinder(); - if (Context.DefaultQueryConfigurations.EnableSkipToken && OrderBy != null) - { - binderContext.OrderByClauses = OrderBy.OrderByClause.ToList(); - binderContext.EnableSkipToken = true; - } + QueryBinderContext binderContext = new QueryBinderContext(Context.Model, settings, Context.ElementClrType) + { + NavigationSource = Context.NavigationSource, + }; - return binder.ApplyBind(entity, SelectExpandClause, binderContext); + if (Compute != null) + { + binderContext.AddComputedProperties(Compute.ComputeClause.ComputedItems); } - /// - /// Validate the $select and $expand query based on the given . It throws an ODataException if validation failed. - /// - /// The instance which contains all the validation settings. - public void Validate(ODataValidationSettings validationSettings) + if (Context.DefaultQueryConfigurations.EnableSkipToken && OrderBy != null) { - if (validationSettings == null) - { - throw Error.ArgumentNull("validationSettings"); - } - - if (Validator != null) - { - Validator.Validate(this, validationSettings); - } + binderContext.OrderByClauses = OrderBy.OrderByClause.ToList(); + binderContext.EnableSkipToken = true; } - internal SelectExpandClause ProcessLevels() + return binder.ApplyBind(entity, SelectExpandClause, binderContext); + } + + /// + /// Validate the $select and $expand query based on the given . It throws an ODataException if validation failed. + /// + /// The instance which contains all the validation settings. + public void Validate(ODataValidationSettings validationSettings) + { + if (validationSettings == null) { - bool levelsEncountered; - bool isMaxLevel; - ModelBoundQuerySettings querySettings = Context.Model.GetModelBoundQuerySettings(Context.TargetProperty, - Context.TargetStructuredType, Context.DefaultQueryConfigurations); - return ProcessLevels(SelectExpandClause, - LevelsMaxLiteralExpansionDepth < 0 ? ODataValidationSettings.DefaultMaxExpansionDepth : LevelsMaxLiteralExpansionDepth, - querySettings, - out levelsEncountered, - out isMaxLevel); + throw Error.ArgumentNull("validationSettings"); } - // Process $levels in SelectExpandClause. - private SelectExpandClause ProcessLevels( - SelectExpandClause selectExpandClause, - int levelsMaxLiteralExpansionDepth, - ModelBoundQuerySettings querySettings, - out bool levelsEncountered, - out bool isMaxLevel) + if (Validator != null) { - levelsEncountered = false; - isMaxLevel = false; + Validator.Validate(this, validationSettings); + } + } - if (selectExpandClause == null) - { - return null; - } + internal SelectExpandClause ProcessLevels() + { + bool levelsEncountered; + bool isMaxLevel; + ModelBoundQuerySettings querySettings = Context.Model.GetModelBoundQuerySettings(Context.TargetProperty, + Context.TargetStructuredType, Context.DefaultQueryConfigurations); + return ProcessLevels(SelectExpandClause, + LevelsMaxLiteralExpansionDepth < 0 ? ODataValidationSettings.DefaultMaxExpansionDepth : LevelsMaxLiteralExpansionDepth, + querySettings, + out levelsEncountered, + out isMaxLevel); + } - // Process $levels in SelectItems of SelectExpandClause. - IEnumerable selectItems = ProcessLevels( - selectExpandClause.SelectedItems, - levelsMaxLiteralExpansionDepth, - querySettings, - out levelsEncountered, - out isMaxLevel); + // Process $levels in SelectExpandClause. + private SelectExpandClause ProcessLevels( + SelectExpandClause selectExpandClause, + int levelsMaxLiteralExpansionDepth, + ModelBoundQuerySettings querySettings, + out bool levelsEncountered, + out bool isMaxLevel) + { + levelsEncountered = false; + isMaxLevel = false; - if (selectItems == null) - { - return null; - } - else if (levelsEncountered) - { - return new SelectExpandClause(selectItems, selectExpandClause.AllSelected); - } - else - { - // Return the original SelectExpandClause if no $levels is found. - return selectExpandClause; - } + if (selectExpandClause == null) + { + return null; } - // Process $levels in SelectedItems. - private IEnumerable ProcessLevels( - IEnumerable selectItems, - int levelsMaxLiteralExpansionDepth, - ModelBoundQuerySettings querySettings, - out bool levelsEncountered, - out bool isMaxLevel) + // Process $levels in SelectItems of SelectExpandClause. + IEnumerable selectItems = ProcessLevels( + selectExpandClause.SelectedItems, + levelsMaxLiteralExpansionDepth, + querySettings, + out levelsEncountered, + out isMaxLevel); + + if (selectItems == null) + { + return null; + } + else if (levelsEncountered) + { + return new SelectExpandClause(selectItems, selectExpandClause.AllSelected); + } + else { - levelsEncountered = false; - isMaxLevel = false; - IList items = new List(); + // Return the original SelectExpandClause if no $levels is found. + return selectExpandClause; + } + } - foreach (SelectItem selectItem in selectItems) - { - ExpandedNavigationSelectItem item = selectItem as ExpandedNavigationSelectItem; + // Process $levels in SelectedItems. + private IEnumerable ProcessLevels( + IEnumerable selectItems, + int levelsMaxLiteralExpansionDepth, + ModelBoundQuerySettings querySettings, + out bool levelsEncountered, + out bool isMaxLevel) + { + levelsEncountered = false; + isMaxLevel = false; + IList items = new List(); + + foreach (SelectItem selectItem in selectItems) + { + ExpandedNavigationSelectItem item = selectItem as ExpandedNavigationSelectItem; - if (item == null) + if (item == null) + { + // There is no $levels in non-ExpandedNavigationSelectItem. + items.Add(selectItem); + } + else + { + bool levelsEncouteredInExpand; + bool isMaxLevelInExpand; + // Process $levels in ExpandedNavigationSelectItem. + ExpandedNavigationSelectItem expandItem = ProcessLevels( + item, + levelsMaxLiteralExpansionDepth, + querySettings, + out levelsEncouteredInExpand, + out isMaxLevelInExpand); + + if (item.LevelsOption != null && item.LevelsOption.Level > 0 && expandItem == null) { - // There is no $levels in non-ExpandedNavigationSelectItem. - items.Add(selectItem); + // Abandon this attempt if any of the items failed to expand + return null; } - else + else if (item.LevelsOption != null) { - bool levelsEncouteredInExpand; - bool isMaxLevelInExpand; - // Process $levels in ExpandedNavigationSelectItem. - ExpandedNavigationSelectItem expandItem = ProcessLevels( - item, - levelsMaxLiteralExpansionDepth, - querySettings, - out levelsEncouteredInExpand, - out isMaxLevelInExpand); - - if (item.LevelsOption != null && item.LevelsOption.Level > 0 && expandItem == null) - { - // Abandon this attempt if any of the items failed to expand - return null; - } - else if (item.LevelsOption != null) - { - // The expansion would be volatile if any of the expand item is max level - isMaxLevel = isMaxLevel || isMaxLevelInExpand; - } + // The expansion would be volatile if any of the expand item is max level + isMaxLevel = isMaxLevel || isMaxLevelInExpand; + } - levelsEncountered = levelsEncountered || levelsEncouteredInExpand; + levelsEncountered = levelsEncountered || levelsEncouteredInExpand; - if (expandItem != null) - { - items.Add(expandItem); - } + if (expandItem != null) + { + items.Add(expandItem); } } - return items; } + return items; + } + + private void GetAutoSelectExpandItems(IEdmEntityType baseEntityType, IEdmModel model, IEdmNavigationSource navigationSource, bool isAllSelected, + ModelBoundQuerySettings modelBoundQuerySettings, int depth, out List autoSelectItems, out List autoExpandItems) + { + autoSelectItems = new List(); + autoExpandItems = new List(); - private void GetAutoSelectExpandItems(IEdmEntityType baseEntityType, IEdmModel model, IEdmNavigationSource navigationSource, bool isAllSelected, - ModelBoundQuerySettings modelBoundQuerySettings, int depth, out List autoSelectItems, out List autoExpandItems) + if (baseEntityType == null) { - autoSelectItems = new List(); - autoExpandItems = new List(); + return; + } - if (baseEntityType == null) - { - return; - } + IList autoSelectProperties = model.GetAutoSelectPaths(baseEntityType, null, modelBoundQuerySettings); + foreach (var autoSelectProperty in autoSelectProperties) + { + ODataSelectPath odataSelectPath = BuildSelectPath(autoSelectProperty, navigationSource); + PathSelectItem pathSelectItem = new PathSelectItem(odataSelectPath); + autoSelectItems.Add(pathSelectItem); + } - IList autoSelectProperties = model.GetAutoSelectPaths(baseEntityType, null, modelBoundQuerySettings); - foreach (var autoSelectProperty in autoSelectProperties) + depth--; + if (depth < 0) + { + return; + } + + IList autoExpandNavigationProperties = model.GetAutoExpandPaths(baseEntityType, null, !isAllSelected, modelBoundQuerySettings); + foreach (ExpandModelPath itemPath in autoExpandNavigationProperties) + { + string navigationPath = itemPath.NavigationPropertyPath; + IEdmNavigationProperty navigationProperty = itemPath.Navigation; + + IEdmNavigationSource currentEdmNavigationSource; + if (navigationPath != null) { - ODataSelectPath odataSelectPath = BuildSelectPath(autoSelectProperty, navigationSource); - PathSelectItem pathSelectItem = new PathSelectItem(odataSelectPath); - autoSelectItems.Add(pathSelectItem); + currentEdmNavigationSource = navigationSource.FindNavigationTarget(navigationProperty); } - - depth--; - if (depth < 0) + else { - return; + currentEdmNavigationSource = navigationSource.FindNavigationTarget(navigationProperty, new EdmPathExpression(navigationPath)); } - IList autoExpandNavigationProperties = model.GetAutoExpandPaths(baseEntityType, null, !isAllSelected, modelBoundQuerySettings); - foreach (ExpandModelPath itemPath in autoExpandNavigationProperties) + if (currentEdmNavigationSource != null) { - string navigationPath = itemPath.NavigationPropertyPath; - IEdmNavigationProperty navigationProperty = itemPath.Navigation; + ODataExpandPath expandPath = BuildExpandPath(itemPath, navigationSource, currentEdmNavigationSource); - IEdmNavigationSource currentEdmNavigationSource; - if (navigationPath != null) - { - currentEdmNavigationSource = navigationSource.FindNavigationTarget(navigationProperty); - } - else + SelectExpandClause selectExpandClause = new SelectExpandClause(new List(), true); + ExpandedNavigationSelectItem item = new ExpandedNavigationSelectItem(expandPath, currentEdmNavigationSource, selectExpandClause); + modelBoundQuerySettings = model.GetModelBoundQuerySettings(navigationProperty, navigationProperty.ToEntityType()); + List nestedSelectItems; + List nestedExpandItems; + + int maxExpandDepth = GetMaxExpandDepth(modelBoundQuerySettings, navigationProperty.Name); + if (maxExpandDepth != 0 && maxExpandDepth < depth) { - currentEdmNavigationSource = navigationSource.FindNavigationTarget(navigationProperty, new EdmPathExpression(navigationPath)); + depth = maxExpandDepth; } - if (currentEdmNavigationSource != null) + GetAutoSelectExpandItems( + currentEdmNavigationSource.EntityType, + model, + item.NavigationSource, + true, + modelBoundQuerySettings, + depth, + out nestedSelectItems, + out nestedExpandItems); + + selectExpandClause = new SelectExpandClause(nestedSelectItems.Concat(nestedExpandItems), nestedSelectItems.Count == 0); + item = new ExpandedNavigationSelectItem(expandPath, currentEdmNavigationSource, selectExpandClause); + + autoExpandItems.Add(item); + if (!isAllSelected || autoSelectProperties.Any()) { - ODataExpandPath expandPath = BuildExpandPath(itemPath, navigationSource, currentEdmNavigationSource); - - SelectExpandClause selectExpandClause = new SelectExpandClause(new List(), true); - ExpandedNavigationSelectItem item = new ExpandedNavigationSelectItem(expandPath, currentEdmNavigationSource, selectExpandClause); - modelBoundQuerySettings = model.GetModelBoundQuerySettings(navigationProperty, navigationProperty.ToEntityType()); - List nestedSelectItems; - List nestedExpandItems; - - int maxExpandDepth = GetMaxExpandDepth(modelBoundQuerySettings, navigationProperty.Name); - if (maxExpandDepth != 0 && maxExpandDepth < depth) - { - depth = maxExpandDepth; - } - - GetAutoSelectExpandItems( - currentEdmNavigationSource.EntityType, - model, - item.NavigationSource, - true, - modelBoundQuerySettings, - depth, - out nestedSelectItems, - out nestedExpandItems); - - selectExpandClause = new SelectExpandClause(nestedSelectItems.Concat(nestedExpandItems), nestedSelectItems.Count == 0); - item = new ExpandedNavigationSelectItem(expandPath, currentEdmNavigationSource, selectExpandClause); - - autoExpandItems.Add(item); - if (!isAllSelected || autoSelectProperties.Any()) - { - PathSelectItem pathSelectItem = new PathSelectItem(new ODataSelectPath(expandPath)); - autoExpandItems.Add(pathSelectItem); - } + PathSelectItem pathSelectItem = new PathSelectItem(new ODataSelectPath(expandPath)); + autoExpandItems.Add(pathSelectItem); } } } + } - private static ODataSelectPath BuildSelectPath(SelectModelPath path, IEdmNavigationSource navigationSource) + private static ODataSelectPath BuildSelectPath(SelectModelPath path, IEdmNavigationSource navigationSource) + { + IList segments = new List(); + IEdmType previousPropertyType = null; + foreach (var node in path) { - IList segments = new List(); - IEdmType previousPropertyType = null; - foreach (var node in path) + if (node is IEdmStructuralProperty property) { - if (node is IEdmStructuralProperty property) - { - segments.Add(new PropertySegment(property)); - previousPropertyType = property.Type.GetElementType(); - } - else if (node is IEdmStructuredType typeNode) - { - if (previousPropertyType == null) - { - segments.Add(new TypeSegment(typeNode, navigationSource)); - } - else - { - segments.Add(new TypeSegment(typeNode, previousPropertyType, navigationSource)); - } - } + segments.Add(new PropertySegment(property)); + previousPropertyType = property.Type.GetElementType(); } - - return new ODataSelectPath(segments); - } - - private static ODataExpandPath BuildExpandPath(ExpandModelPath path, IEdmNavigationSource navigationSource, IEdmNavigationSource currentEdmNavigationSource) - { - IList segments = new List(); - IEdmType previousPropertyType = null; - foreach (var node in path) + else if (node is IEdmStructuredType typeNode) { - if (node is IEdmStructuralProperty property) + if (previousPropertyType == null) { - segments.Add(new PropertySegment(property)); - previousPropertyType = property.Type.GetElementType(); + segments.Add(new TypeSegment(typeNode, navigationSource)); } - else if (node is IEdmStructuredType typeNode) - { - if (previousPropertyType == null) - { - segments.Add(new TypeSegment(typeNode, navigationSource)); - } - else - { - segments.Add(new TypeSegment(typeNode, previousPropertyType, navigationSource)); - } - } - else if (node is IEdmNavigationProperty navigation) + else { - segments.Add(new NavigationPropertySegment(navigation, currentEdmNavigationSource)); + segments.Add(new TypeSegment(typeNode, previousPropertyType, navigationSource)); } } - - return new ODataExpandPath(segments); } - // Process $levels in ExpandedNavigationSelectItem. - private ExpandedNavigationSelectItem ProcessLevels( - ExpandedNavigationSelectItem expandItem, - int levelsMaxLiteralExpansionDepth, - ModelBoundQuerySettings querySettings, - out bool levelsEncounteredInExpand, - out bool isMaxLevelInExpand) - { - int level; - isMaxLevelInExpand = false; + return new ODataSelectPath(segments); + } - if (expandItem.LevelsOption == null) + private static ODataExpandPath BuildExpandPath(ExpandModelPath path, IEdmNavigationSource navigationSource, IEdmNavigationSource currentEdmNavigationSource) + { + IList segments = new List(); + IEdmType previousPropertyType = null; + foreach (var node in path) + { + if (node is IEdmStructuralProperty property) { - levelsEncounteredInExpand = false; - level = 1; + segments.Add(new PropertySegment(property)); + previousPropertyType = property.Type.GetElementType(); } - else + else if (node is IEdmStructuredType typeNode) { - levelsEncounteredInExpand = true; - if (expandItem.LevelsOption.IsMaxLevel) + if (previousPropertyType == null) { - isMaxLevelInExpand = true; - level = levelsMaxLiteralExpansionDepth; + segments.Add(new TypeSegment(typeNode, navigationSource)); } else { - level = (int)expandItem.LevelsOption.Level; + segments.Add(new TypeSegment(typeNode, previousPropertyType, navigationSource)); } } - - // Do not expand when: - // 1. $levels is equal to or less than 0. - // 2. $levels value is greater than current MaxExpansionDepth - if (level <= 0 || level > levelsMaxLiteralExpansionDepth) + else if (node is IEdmNavigationProperty navigation) { - return null; + segments.Add(new NavigationPropertySegment(navigation, currentEdmNavigationSource)); } + } - ExpandedNavigationSelectItem item = null; - SelectExpandClause currentSelectExpandClause = null; - SelectExpandClause selectExpandClause = null; - bool levelsEncounteredInInnerExpand = false; - bool isMaxLevelInInnerExpand = false; - var entityType = expandItem.NavigationSource.EntityType; - IEdmNavigationProperty navigationProperty = - (expandItem.PathToNavigationProperty.LastSegment as NavigationPropertySegment).NavigationProperty; - ModelBoundQuerySettings nestQuerySettings = Context.Model.GetModelBoundQuerySettings(navigationProperty, navigationProperty.ToEntityType()); + return new ODataExpandPath(segments); + } + + // Process $levels in ExpandedNavigationSelectItem. + private ExpandedNavigationSelectItem ProcessLevels( + ExpandedNavigationSelectItem expandItem, + int levelsMaxLiteralExpansionDepth, + ModelBoundQuerySettings querySettings, + out bool levelsEncounteredInExpand, + out bool isMaxLevelInExpand) + { + int level; + isMaxLevelInExpand = false; - // Try different expansion depth until expandItem.SelectAndExpand is successfully expanded - while (selectExpandClause == null && level > 0) + if (expandItem.LevelsOption == null) + { + levelsEncounteredInExpand = false; + level = 1; + } + else + { + levelsEncounteredInExpand = true; + if (expandItem.LevelsOption.IsMaxLevel) { - selectExpandClause = ProcessLevels( - expandItem.SelectAndExpand, - levelsMaxLiteralExpansionDepth - level, - nestQuerySettings, - out levelsEncounteredInInnerExpand, - out isMaxLevelInInnerExpand); - level--; + isMaxLevelInExpand = true; + level = levelsMaxLiteralExpansionDepth; } - - if (selectExpandClause == null) + else { - return null; + level = (int)expandItem.LevelsOption.Level; } + } - // Correct level value - level++; - List originAutoSelectItems; - List originAutoExpandItems; + // Do not expand when: + // 1. $levels is equal to or less than 0. + // 2. $levels value is greater than current MaxExpansionDepth + if (level <= 0 || level > levelsMaxLiteralExpansionDepth) + { + return null; + } - int maxDepth = GetMaxExpandDepth(querySettings, navigationProperty.Name); - if (maxDepth == 0 || levelsMaxLiteralExpansionDepth > maxDepth) - { - maxDepth = levelsMaxLiteralExpansionDepth; - } + ExpandedNavigationSelectItem item = null; + SelectExpandClause currentSelectExpandClause = null; + SelectExpandClause selectExpandClause = null; + bool levelsEncounteredInInnerExpand = false; + bool isMaxLevelInInnerExpand = false; + var entityType = expandItem.NavigationSource.EntityType; + IEdmNavigationProperty navigationProperty = + (expandItem.PathToNavigationProperty.LastSegment as NavigationPropertySegment).NavigationProperty; + ModelBoundQuerySettings nestQuerySettings = Context.Model.GetModelBoundQuerySettings(navigationProperty, navigationProperty.ToEntityType()); + + // Try different expansion depth until expandItem.SelectAndExpand is successfully expanded + while (selectExpandClause == null && level > 0) + { + selectExpandClause = ProcessLevels( + expandItem.SelectAndExpand, + levelsMaxLiteralExpansionDepth - level, + nestQuerySettings, + out levelsEncounteredInInnerExpand, + out isMaxLevelInInnerExpand); + level--; + } - GetAutoSelectExpandItems( - entityType, - Context.Model, - expandItem.NavigationSource, - selectExpandClause.AllSelected, - nestQuerySettings, - maxDepth - 1, - out originAutoSelectItems, - out originAutoExpandItems); - if (expandItem.SelectAndExpand.SelectedItems.Any(it => it is PathSelectItem)) - { - originAutoSelectItems.Clear(); - } + if (selectExpandClause == null) + { + return null; + } - if (level > 1) - { - RemoveSameExpandItem(navigationProperty, originAutoExpandItems); - } + // Correct level value + level++; + List originAutoSelectItems; + List originAutoExpandItems; + + int maxDepth = GetMaxExpandDepth(querySettings, navigationProperty.Name); + if (maxDepth == 0 || levelsMaxLiteralExpansionDepth > maxDepth) + { + maxDepth = levelsMaxLiteralExpansionDepth; + } + + GetAutoSelectExpandItems( + entityType, + Context.Model, + expandItem.NavigationSource, + selectExpandClause.AllSelected, + nestQuerySettings, + maxDepth - 1, + out originAutoSelectItems, + out originAutoExpandItems); + if (expandItem.SelectAndExpand.SelectedItems.Any(it => it is PathSelectItem)) + { + originAutoSelectItems.Clear(); + } + + if (level > 1) + { + RemoveSameExpandItem(navigationProperty, originAutoExpandItems); + } - List autoExpandItems = new List(originAutoExpandItems); - bool hasAutoSelectExpandInExpand = (originAutoSelectItems.Count + originAutoExpandItems.Count != 0); - bool allSelected = originAutoSelectItems.Count == 0 && selectExpandClause.AllSelected; + List autoExpandItems = new List(originAutoExpandItems); + bool hasAutoSelectExpandInExpand = (originAutoSelectItems.Count + originAutoExpandItems.Count != 0); + bool allSelected = originAutoSelectItems.Count == 0 && selectExpandClause.AllSelected; - while (level > 0) + while (level > 0) + { + autoExpandItems = RemoveExpandItemExceedMaxDepth(maxDepth - level, originAutoExpandItems); + if (item == null) { - autoExpandItems = RemoveExpandItemExceedMaxDepth(maxDepth - level, originAutoExpandItems); - if (item == null) - { - if (hasAutoSelectExpandInExpand) - { - currentSelectExpandClause = new SelectExpandClause( - Array.Empty().Concat(selectExpandClause.SelectedItems) - .Concat(originAutoSelectItems).Concat(autoExpandItems), - allSelected); - } - else - { - currentSelectExpandClause = selectExpandClause; - } - } - else if (selectExpandClause.AllSelected) - { - // Concat the processed items - currentSelectExpandClause = new SelectExpandClause( - new SelectItem[] { item }.Concat(selectExpandClause.SelectedItems) - .Concat(originAutoSelectItems).Concat(autoExpandItems), - allSelected); - } - else + if (hasAutoSelectExpandInExpand) { - // PathSelectItem is needed for the expanded item if AllSelected is false. - PathSelectItem pathSelectItem = new PathSelectItem( - new ODataSelectPath(expandItem.PathToNavigationProperty)); - - // Keep default SelectItems before expanded item to keep consistent with normal SelectExpandClause - SelectItem[] items = new SelectItem[] { item, pathSelectItem }; currentSelectExpandClause = new SelectExpandClause( Array.Empty().Concat(selectExpandClause.SelectedItems) - .Concat(items) .Concat(originAutoSelectItems).Concat(autoExpandItems), allSelected); } - - // Construct a new ExpandedNavigationSelectItem with current SelectExpandClause. - item = new ExpandedNavigationSelectItem( - expandItem.PathToNavigationProperty, - expandItem.NavigationSource, - currentSelectExpandClause, - expandItem.FilterOption, - expandItem.OrderByOption, - expandItem.TopOption, - expandItem.SkipOption, - expandItem.CountOption, - expandItem.SearchOption, - null, - expandItem.ComputeOption, - expandItem.ApplyOption); - - level--; - - // Need expand and construct selectExpandClause every time if it is max level in inner expand - if (isMaxLevelInInnerExpand) + else { - selectExpandClause = ProcessLevels( - expandItem.SelectAndExpand, - levelsMaxLiteralExpansionDepth - level, - nestQuerySettings, - out levelsEncounteredInInnerExpand, - out isMaxLevelInInnerExpand); + currentSelectExpandClause = selectExpandClause; } } + else if (selectExpandClause.AllSelected) + { + // Concat the processed items + currentSelectExpandClause = new SelectExpandClause( + new SelectItem[] { item }.Concat(selectExpandClause.SelectedItems) + .Concat(originAutoSelectItems).Concat(autoExpandItems), + allSelected); + } + else + { + // PathSelectItem is needed for the expanded item if AllSelected is false. + PathSelectItem pathSelectItem = new PathSelectItem( + new ODataSelectPath(expandItem.PathToNavigationProperty)); - levelsEncounteredInExpand = levelsEncounteredInExpand || levelsEncounteredInInnerExpand || hasAutoSelectExpandInExpand; - isMaxLevelInExpand = isMaxLevelInExpand || isMaxLevelInInnerExpand; + // Keep default SelectItems before expanded item to keep consistent with normal SelectExpandClause + SelectItem[] items = new SelectItem[] { item, pathSelectItem }; + currentSelectExpandClause = new SelectExpandClause( + Array.Empty().Concat(selectExpandClause.SelectedItems) + .Concat(items) + .Concat(originAutoSelectItems).Concat(autoExpandItems), + allSelected); + } - return item; + // Construct a new ExpandedNavigationSelectItem with current SelectExpandClause. + item = new ExpandedNavigationSelectItem( + expandItem.PathToNavigationProperty, + expandItem.NavigationSource, + currentSelectExpandClause, + expandItem.FilterOption, + expandItem.OrderByOption, + expandItem.TopOption, + expandItem.SkipOption, + expandItem.CountOption, + expandItem.SearchOption, + null, + expandItem.ComputeOption, + expandItem.ApplyOption); + + level--; + + // Need expand and construct selectExpandClause every time if it is max level in inner expand + if (isMaxLevelInInnerExpand) + { + selectExpandClause = ProcessLevels( + expandItem.SelectAndExpand, + levelsMaxLiteralExpansionDepth - level, + nestQuerySettings, + out levelsEncounteredInInnerExpand, + out isMaxLevelInInnerExpand); + } } - private static List RemoveExpandItemExceedMaxDepth(int depth, IEnumerable autoExpandItems) + levelsEncounteredInExpand = levelsEncounteredInExpand || levelsEncounteredInInnerExpand || hasAutoSelectExpandInExpand; + isMaxLevelInExpand = isMaxLevelInExpand || isMaxLevelInInnerExpand; + + return item; + } + + private static List RemoveExpandItemExceedMaxDepth(int depth, IEnumerable autoExpandItems) + { + List selectItems = new List(); + if (depth <= 0) { - List selectItems = new List(); - if (depth <= 0) + foreach (SelectItem autoSelectItem in autoExpandItems) { - foreach (SelectItem autoSelectItem in autoExpandItems) + if (!(autoSelectItem is ExpandedNavigationSelectItem)) { - if (!(autoSelectItem is ExpandedNavigationSelectItem)) - { - selectItems.Add(autoSelectItem); - } + selectItems.Add(autoSelectItem); } } - else + } + else + { + foreach (var autoExpandItem in autoExpandItems) { - foreach (var autoExpandItem in autoExpandItems) + ExpandedNavigationSelectItem expandItem = autoExpandItem as ExpandedNavigationSelectItem; + if (expandItem != null) { - ExpandedNavigationSelectItem expandItem = autoExpandItem as ExpandedNavigationSelectItem; - if (expandItem != null) - { - SelectExpandClause selectExpandClause = - new SelectExpandClause( - RemoveExpandItemExceedMaxDepth(depth - 1, expandItem.SelectAndExpand.SelectedItems), - expandItem.SelectAndExpand.AllSelected); - expandItem = new ExpandedNavigationSelectItem(expandItem.PathToNavigationProperty, - expandItem.NavigationSource, selectExpandClause); - selectItems.Add(expandItem); - } - else - { - selectItems.Add(autoExpandItem); - } + SelectExpandClause selectExpandClause = + new SelectExpandClause( + RemoveExpandItemExceedMaxDepth(depth - 1, expandItem.SelectAndExpand.SelectedItems), + expandItem.SelectAndExpand.AllSelected); + expandItem = new ExpandedNavigationSelectItem(expandItem.PathToNavigationProperty, + expandItem.NavigationSource, selectExpandClause); + selectItems.Add(expandItem); + } + else + { + selectItems.Add(autoExpandItem); } } - - return selectItems; } - private static void RemoveSameExpandItem(IEdmNavigationProperty navigationProperty, List autoExpandItems) + return selectItems; + } + + private static void RemoveSameExpandItem(IEdmNavigationProperty navigationProperty, List autoExpandItems) + { + for (int i = 0; i < autoExpandItems.Count; i++) { - for (int i = 0; i < autoExpandItems.Count; i++) + ExpandedNavigationSelectItem expandItem = autoExpandItems[i] as ExpandedNavigationSelectItem; + IEdmNavigationProperty autoExpandNavigationProperty = + (expandItem.PathToNavigationProperty.LastSegment as NavigationPropertySegment).NavigationProperty; + if (navigationProperty.Name.Equals(autoExpandNavigationProperty.Name, StringComparison.Ordinal)) { - ExpandedNavigationSelectItem expandItem = autoExpandItems[i] as ExpandedNavigationSelectItem; - IEdmNavigationProperty autoExpandNavigationProperty = - (expandItem.PathToNavigationProperty.LastSegment as NavigationPropertySegment).NavigationProperty; - if (navigationProperty.Name.Equals(autoExpandNavigationProperty.Name, StringComparison.Ordinal)) - { - autoExpandItems.RemoveAt(i); - return; - } + autoExpandItems.RemoveAt(i); + return; } } + } - private static int GetMaxExpandDepth(ModelBoundQuerySettings querySettings, string propertyName) + private static int GetMaxExpandDepth(ModelBoundQuerySettings querySettings, string propertyName) + { + int result = 0; + if (querySettings != null) { - int result = 0; - if (querySettings != null) + ExpandConfiguration expandConfiguration; + if (querySettings.ExpandConfigurations.TryGetValue(propertyName, out expandConfiguration)) { - ExpandConfiguration expandConfiguration; - if (querySettings.ExpandConfigurations.TryGetValue(propertyName, out expandConfiguration)) - { - result = expandConfiguration.MaxDepth; - } - else + result = expandConfiguration.MaxDepth; + } + else + { + if (querySettings.DefaultExpandType.HasValue && + querySettings.DefaultExpandType != SelectExpandType.Disabled) { - if (querySettings.DefaultExpandType.HasValue && - querySettings.DefaultExpandType != SelectExpandType.Disabled) - { - result = querySettings.DefaultMaxDepth; - } + result = querySettings.DefaultMaxDepth; } } - - return result; } + + return result; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/SkipQueryOption.cs b/src/Microsoft.AspNetCore.OData/Query/Query/SkipQueryOption.cs index 8f71f5452..fd040240b 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/SkipQueryOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/SkipQueryOption.cs @@ -14,169 +14,168 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This defines a $skip OData query option for querying. +/// +public class SkipQueryOption { + private int? _value; + private ODataQueryOptionParser _queryOptionParser; + /// - /// This defines a $skip OData query option for querying. + /// Initialize a new instance of based on the raw $skip value and + /// an EdmModel from . /// - public class SkipQueryOption + /// The raw value for $skip query. It can be null or empty. + /// The which contains the and some type information + /// The which is used to parse the query option. + public SkipQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) { - private int? _value; - private ODataQueryOptionParser _queryOptionParser; - - /// - /// Initialize a new instance of based on the raw $skip value and - /// an EdmModel from . - /// - /// The raw value for $skip query. It can be null or empty. - /// The which contains the and some type information - /// The which is used to parse the query option. - public SkipQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - if (String.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty(nameof(rawValue)); - } - - if (queryOptionParser == null) - { - throw Error.ArgumentNull(nameof(queryOptionParser)); - } + throw Error.ArgumentNull(nameof(context)); + } - Context = context; - RawValue = rawValue; - Validator = context.GetSkipQueryValidator(); - _queryOptionParser = queryOptionParser; + if (String.IsNullOrEmpty(rawValue)) + { + throw Error.ArgumentNullOrEmpty(nameof(rawValue)); } - // This constructor is intended for unit testing only. - internal SkipQueryOption(string rawValue, ODataQueryContext context) + if (queryOptionParser == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(queryOptionParser)); + } - if (string.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty(nameof(rawValue)); - } + Context = context; + RawValue = rawValue; + Validator = context.GetSkipQueryValidator(); + _queryOptionParser = queryOptionParser; + } - Context = context; - RawValue = rawValue; - Validator = context.GetSkipQueryValidator(); - _queryOptionParser = new ODataQueryOptionParser( - context.Model, - context.ElementType, - context.NavigationSource, - new Dictionary { { "$skip", rawValue } }); + // This constructor is intended for unit testing only. + internal SkipQueryOption(string rawValue, ODataQueryContext context) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); } - /// - /// Gets the given . - /// - public ODataQueryContext Context { get; private set; } + if (string.IsNullOrEmpty(rawValue)) + { + throw Error.ArgumentNullOrEmpty(nameof(rawValue)); + } - /// - /// Gets the raw $skip value. - /// - public string RawValue { get; private set; } + Context = context; + RawValue = rawValue; + Validator = context.GetSkipQueryValidator(); + _queryOptionParser = new ODataQueryOptionParser( + context.Model, + context.ElementType, + context.NavigationSource, + new Dictionary { { "$skip", rawValue } }); + } - /// - /// Gets the value of the $skip as a parsed integer. - /// - public int Value + /// + /// Gets the given . + /// + public ODataQueryContext Context { get; private set; } + + /// + /// Gets the raw $skip value. + /// + public string RawValue { get; private set; } + + /// + /// Gets the value of the $skip as a parsed integer. + /// + public int Value + { + get { - get + if (_value == null) { - if (_value == null) + long? skipValue = _queryOptionParser.ParseSkip(); + + if (skipValue.HasValue && skipValue > Int32.MaxValue) { - long? skipValue = _queryOptionParser.ParseSkip(); - - if (skipValue.HasValue && skipValue > Int32.MaxValue) - { - throw new ODataException(Error.Format( - SRResources.SkipTopLimitExceeded, - Int32.MaxValue, - AllowedQueryOptions.Skip, - RawValue)); - } - - _value = (int?)skipValue; + throw new ODataException(Error.Format( + SRResources.SkipTopLimitExceeded, + Int32.MaxValue, + AllowedQueryOptions.Skip, + RawValue)); } - Contract.Assert(_value.HasValue); - return _value.Value; + _value = (int?)skipValue; } + + Contract.Assert(_value.HasValue); + return _value.Value; } + } - /// - /// Gets or sets the Skip Query Validator. - /// - public ISkipQueryValidator Validator { get; set; } - - /// - /// Apply the $skip query to the given IQueryable. - /// - /// The original . - /// The query settings to use while applying this query option. - /// The new after the skip query has been applied to. - public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + /// + /// Gets or sets the Skip Query Validator. + /// + public ISkipQueryValidator Validator { get; set; } + + /// + /// Apply the $skip query to the given IQueryable. + /// + /// The original . + /// The query settings to use while applying this query option. + /// The new after the skip query has been applied to. + public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + { + return ApplyToCore(query, querySettings) as IOrderedQueryable; + } + + /// + /// Apply the $skip query to the given IQueryable. + /// + /// The original . + /// The query settings to use while applying this query option. + /// The new after the skip query has been applied to. + public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + { + return ApplyToCore(query, querySettings); + } + + /// + /// Validate the skip query based on the given . It throws an ODataException if validation failed. + /// + /// The instance which contains all the validation settings. + public void Validate(ODataValidationSettings validationSettings) + { + if (validationSettings == null) { - return ApplyToCore(query, querySettings) as IOrderedQueryable; + throw Error.ArgumentNull(nameof(validationSettings)); } - /// - /// Apply the $skip query to the given IQueryable. - /// - /// The original . - /// The query settings to use while applying this query option. - /// The new after the skip query has been applied to. - public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + if (Validator != null) { - return ApplyToCore(query, querySettings); + Validator.Validate(this, validationSettings); } + } - /// - /// Validate the skip query based on the given . It throws an ODataException if validation failed. - /// - /// The instance which contains all the validation settings. - public void Validate(ODataValidationSettings validationSettings) + private IQueryable ApplyToCore(IQueryable query, ODataQuerySettings querySettings) + { + if (query == null) { - if (validationSettings == null) - { - throw Error.ArgumentNull(nameof(validationSettings)); - } - - if (Validator != null) - { - Validator.Validate(this, validationSettings); - } + throw Error.ArgumentNull(nameof(query)); } - private IQueryable ApplyToCore(IQueryable query, ODataQuerySettings querySettings) + if (querySettings == null) { - if (query == null) - { - throw Error.ArgumentNull(nameof(query)); - } - - if (querySettings == null) - { - throw Error.ArgumentNull(nameof(querySettings)); - } - - if (Context.ElementClrType == null) - { - throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); - } + throw Error.ArgumentNull(nameof(querySettings)); + } - return ExpressionHelpers.Skip(query, Value, query.ElementType, querySettings.EnableConstantParameterization); + if (Context.ElementClrType == null) + { + throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); } + + return ExpressionHelpers.Skip(query, Value, query.ElementType, querySettings.EnableConstantParameterization); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/SkipTokenHandler.cs b/src/Microsoft.AspNetCore.OData/Query/Query/SkipTokenHandler.cs index 81650752d..871dbf849 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/SkipTokenHandler.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/SkipTokenHandler.cs @@ -9,43 +9,42 @@ using System.Linq; using Microsoft.AspNetCore.OData.Formatter.Serialization; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// Represents how NextLink for paging is generated. +/// +public abstract class SkipTokenHandler { /// - /// Represents how NextLink for paging is generated. + /// Apply the $skiptoken query to the given IQueryable. /// - public abstract class SkipTokenHandler - { - /// - /// Apply the $skiptoken query to the given IQueryable. - /// - /// The original . - /// The query option that contains all the relevant information for applying skiptoken. - /// The query settings to use while applying this query option. - /// Information about the other query options. - /// The new after the skiptoken query has been applied to. - public abstract IQueryable ApplyTo(IQueryable query, SkipTokenQueryOption skipTokenQueryOption, - ODataQuerySettings querySettings, ODataQueryOptions queryOptions); + /// The original . + /// The query option that contains all the relevant information for applying skiptoken. + /// The query settings to use while applying this query option. + /// Information about the other query options. + /// The new after the skiptoken query has been applied to. + public abstract IQueryable ApplyTo(IQueryable query, SkipTokenQueryOption skipTokenQueryOption, + ODataQuerySettings querySettings, ODataQueryOptions queryOptions); - /// - /// Apply the $skiptoken query to the given IQueryable. - /// - /// The original . - /// The query option that contains all the relevant information for applying skiptoken. - /// The query settings to use while applying this query option. - /// Information about the other query options. - /// The new after the skiptoken query has been applied to. - public abstract IQueryable ApplyTo(IQueryable query, SkipTokenQueryOption skipTokenQueryOption, - ODataQuerySettings querySettings, ODataQueryOptions queryOptions); + /// + /// Apply the $skiptoken query to the given IQueryable. + /// + /// The original . + /// The query option that contains all the relevant information for applying skiptoken. + /// The query settings to use while applying this query option. + /// Information about the other query options. + /// The new after the skiptoken query has been applied to. + public abstract IQueryable ApplyTo(IQueryable query, SkipTokenQueryOption skipTokenQueryOption, + ODataQuerySettings querySettings, ODataQueryOptions queryOptions); - /// - /// Returns the URI for NextPageLink - /// - /// BaseUri for nextlink. - /// Maximum number of records in the set of partial results for a resource. - /// Instance based on which SkipToken value will be generated. - /// Serializer context - /// URI for the NextPageLink. - public abstract Uri GenerateNextPageLink(Uri baseUri, int pageSize, object instance, ODataSerializerContext context); - } + /// + /// Returns the URI for NextPageLink + /// + /// BaseUri for nextlink. + /// Maximum number of records in the set of partial results for a resource. + /// Instance based on which SkipToken value will be generated. + /// Serializer context + /// URI for the NextPageLink. + public abstract Uri GenerateNextPageLink(Uri baseUri, int pageSize, object instance, ODataSerializerContext context); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/SkipTokenQueryOption.cs b/src/Microsoft.AspNetCore.OData/Query/Query/SkipTokenQueryOption.cs index eedff67fe..1bc7b9c09 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/SkipTokenQueryOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/SkipTokenQueryOption.cs @@ -9,96 +9,95 @@ using Microsoft.AspNetCore.OData.Query.Validator; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This defines a $skiptoken OData query option for querying. +/// +public class SkipTokenQueryOption { /// - /// This defines a $skiptoken OData query option for querying. + /// Initialize a new instance of based on the raw $skiptoken value and + /// an EdmModel from . /// - public class SkipTokenQueryOption + /// The raw value for $skiptoken query. + /// The which contains the and some type information. + public SkipTokenQueryOption(string rawValue, ODataQueryContext context) { - /// - /// Initialize a new instance of based on the raw $skiptoken value and - /// an EdmModel from . - /// - /// The raw value for $skiptoken query. - /// The which contains the and some type information. - public SkipTokenQueryOption(string rawValue, ODataQueryContext context) + if (string.IsNullOrEmpty(rawValue)) { - if (string.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty(nameof(rawValue)); - } - - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNullOrEmpty(nameof(rawValue)); + } - RawValue = rawValue; - Validator = context.GetSkipTokenQueryValidator(); - Handler = context.GetSkipTokenHandler(); - Context = context; + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); } - /// - /// Gets the raw $skiptoken value. - /// - public string RawValue { get; } + RawValue = rawValue; + Validator = context.GetSkipTokenQueryValidator(); + Handler = context.GetSkipTokenHandler(); + Context = context; + } - /// - /// Gets the given . - /// - public ODataQueryContext Context { get; } + /// + /// Gets the raw $skiptoken value. + /// + public string RawValue { get; } - /// - /// Gets the SkipToken Query Validator. - /// - public ISkipTokenQueryValidator Validator { get; } + /// + /// Gets the given . + /// + public ODataQueryContext Context { get; } - /// - /// Gets the skip token handler. - /// - public SkipTokenHandler Handler { get; } + /// + /// Gets the SkipToken Query Validator. + /// + public ISkipTokenQueryValidator Validator { get; } - /// - /// Apply the $skiptoken query to the given IQueryable. - /// - /// The original . - /// The query settings to use while applying this query option. - /// Information about the other query options. - /// The new after the skiptoken query has been applied to. - public virtual IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings, ODataQueryOptions queryOptions) - { - return Handler.ApplyTo(query, this, querySettings, queryOptions) as IOrderedQueryable; - } + /// + /// Gets the skip token handler. + /// + public SkipTokenHandler Handler { get; } - /// - /// Apply the $skiptoken query to the given IQueryable. - /// - /// The original . - /// The query settings to use while applying this query option. - /// Information about the other query options. - /// The new after the skiptoken query has been applied to. - public virtual IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings, ODataQueryOptions queryOptions) + /// + /// Apply the $skiptoken query to the given IQueryable. + /// + /// The original . + /// The query settings to use while applying this query option. + /// Information about the other query options. + /// The new after the skiptoken query has been applied to. + public virtual IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings, ODataQueryOptions queryOptions) + { + return Handler.ApplyTo(query, this, querySettings, queryOptions) as IOrderedQueryable; + } + + /// + /// Apply the $skiptoken query to the given IQueryable. + /// + /// The original . + /// The query settings to use while applying this query option. + /// Information about the other query options. + /// The new after the skiptoken query has been applied to. + public virtual IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings, ODataQueryOptions queryOptions) + { + return Handler.ApplyTo(query, this, querySettings, queryOptions); + } + + /// + /// Validate the skiptoken query based on the given . It throws an ODataException if validation failed. + /// + /// The instance which contains all the validation settings. + public void Validate(ODataValidationSettings validationSettings) + { + if (validationSettings == null) { - return Handler.ApplyTo(query, this, querySettings, queryOptions); + throw Error.ArgumentNull(nameof(validationSettings)); } - /// - /// Validate the skiptoken query based on the given . It throws an ODataException if validation failed. - /// - /// The instance which contains all the validation settings. - public void Validate(ODataValidationSettings validationSettings) + if (Validator != null) { - if (validationSettings == null) - { - throw Error.ArgumentNull(nameof(validationSettings)); - } - - if (Validator != null) - { - Validator.Validate(this, validationSettings); - } + Validator.Validate(this, validationSettings); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/TopQueryOption.cs b/src/Microsoft.AspNetCore.OData/Query/Query/TopQueryOption.cs index e42757af8..f24b3e9d2 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/TopQueryOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/TopQueryOption.cs @@ -14,169 +14,168 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// This defines a $top OData query option for querying. +/// +public class TopQueryOption { + private int? _value; + private ODataQueryOptionParser _queryOptionParser; + /// - /// This defines a $top OData query option for querying. + /// Initialize a new instance of based on the raw $top value and + /// an EdmModel from . /// - public class TopQueryOption + /// The raw value for $top query. It can be null or empty. + /// The which contains the and some type information + /// The which is used to parse the query option. + public TopQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) { - private int? _value; - private ODataQueryOptionParser _queryOptionParser; - - /// - /// Initialize a new instance of based on the raw $top value and - /// an EdmModel from . - /// - /// The raw value for $top query. It can be null or empty. - /// The which contains the and some type information - /// The which is used to parse the query option. - public TopQueryOption(string rawValue, ODataQueryContext context, ODataQueryOptionParser queryOptionParser) + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - if (String.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty(nameof(rawValue)); - } - - if (queryOptionParser == null) - { - throw Error.ArgumentNull(nameof(queryOptionParser)); - } + throw Error.ArgumentNull(nameof(context)); + } - Context = context; - RawValue = rawValue; - Validator = context.GetTopQueryValidator(); - _queryOptionParser = queryOptionParser; + if (String.IsNullOrEmpty(rawValue)) + { + throw Error.ArgumentNullOrEmpty(nameof(rawValue)); } - // This constructor is intended for unit testing only. - internal TopQueryOption(string rawValue, ODataQueryContext context) + if (queryOptionParser == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(queryOptionParser)); + } - if (String.IsNullOrEmpty(rawValue)) - { - throw Error.ArgumentNullOrEmpty(nameof(rawValue)); - } + Context = context; + RawValue = rawValue; + Validator = context.GetTopQueryValidator(); + _queryOptionParser = queryOptionParser; + } - Context = context; - RawValue = rawValue; - Validator = context.GetTopQueryValidator(); - _queryOptionParser = new ODataQueryOptionParser( - context.Model, - context.ElementType, - context.NavigationSource, - new Dictionary { { "$top", rawValue } }); + // This constructor is intended for unit testing only. + internal TopQueryOption(string rawValue, ODataQueryContext context) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); } - /// - /// Gets the given . - /// - public ODataQueryContext Context { get; private set; } + if (String.IsNullOrEmpty(rawValue)) + { + throw Error.ArgumentNullOrEmpty(nameof(rawValue)); + } - /// - /// Gets the raw $top value. - /// - public string RawValue { get; private set; } + Context = context; + RawValue = rawValue; + Validator = context.GetTopQueryValidator(); + _queryOptionParser = new ODataQueryOptionParser( + context.Model, + context.ElementType, + context.NavigationSource, + new Dictionary { { "$top", rawValue } }); + } - /// - /// Gets the value of the $top as a parsed integer. - /// - public int Value + /// + /// Gets the given . + /// + public ODataQueryContext Context { get; private set; } + + /// + /// Gets the raw $top value. + /// + public string RawValue { get; private set; } + + /// + /// Gets the value of the $top as a parsed integer. + /// + public int Value + { + get { - get + if (_value == null) { - if (_value == null) + long? topValue = _queryOptionParser.ParseTop(); + + if (topValue.HasValue && topValue > Int32.MaxValue) { - long? topValue = _queryOptionParser.ParseTop(); - - if (topValue.HasValue && topValue > Int32.MaxValue) - { - throw new ODataException(Error.Format( - SRResources.SkipTopLimitExceeded, - Int32.MaxValue, - AllowedQueryOptions.Top, - RawValue)); - } - - _value = (int?)topValue; + throw new ODataException(Error.Format( + SRResources.SkipTopLimitExceeded, + Int32.MaxValue, + AllowedQueryOptions.Top, + RawValue)); } - Contract.Assert(_value.HasValue); - return _value.Value; + _value = (int?)topValue; } + + Contract.Assert(_value.HasValue); + return _value.Value; } + } - /// - /// Gets or sets the Top Query Validator. - /// - public ITopQueryValidator Validator { get; set; } - - /// - /// Apply the $top query to the given IQueryable. - /// - /// The original . - /// The query settings to use while applying this query option. - /// The new after the top query has been applied to. - public IOrderedQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + /// + /// Gets or sets the Top Query Validator. + /// + public ITopQueryValidator Validator { get; set; } + + /// + /// Apply the $top query to the given IQueryable. + /// + /// The original . + /// The query settings to use while applying this query option. + /// The new after the top query has been applied to. + public IOrderedQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + { + return ApplyToCore(query, querySettings) as IOrderedQueryable; + } + + /// + /// Apply the $top query to the given IQueryable. + /// + /// The original . + /// The query settings to use while applying this query option. + /// The new after the top query has been applied to. + public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + { + return ApplyToCore(query, querySettings); + } + + /// + /// Validate the top query based on the given . It throws an ODataException if validation failed. + /// + /// The instance which contains all the validation settings. + public void Validate(ODataValidationSettings validationSettings) + { + if (validationSettings == null) { - return ApplyToCore(query, querySettings) as IOrderedQueryable; + throw Error.ArgumentNull(nameof(validationSettings)); } - /// - /// Apply the $top query to the given IQueryable. - /// - /// The original . - /// The query settings to use while applying this query option. - /// The new after the top query has been applied to. - public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) + if (Validator != null) { - return ApplyToCore(query, querySettings); + Validator.Validate(this, validationSettings); } + } - /// - /// Validate the top query based on the given . It throws an ODataException if validation failed. - /// - /// The instance which contains all the validation settings. - public void Validate(ODataValidationSettings validationSettings) + private IQueryable ApplyToCore(IQueryable query, ODataQuerySettings querySettings) + { + if (query == null) { - if (validationSettings == null) - { - throw Error.ArgumentNull(nameof(validationSettings)); - } - - if (Validator != null) - { - Validator.Validate(this, validationSettings); - } + throw Error.ArgumentNull(nameof(query)); } - private IQueryable ApplyToCore(IQueryable query, ODataQuerySettings querySettings) + if (querySettings == null) { - if (query == null) - { - throw Error.ArgumentNull(nameof(query)); - } - - if (querySettings == null) - { - throw Error.ArgumentNull(nameof(querySettings)); - } - - if (Context.ElementClrType == null) - { - throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); - } + throw Error.ArgumentNull(nameof(querySettings)); + } - return ExpressionHelpers.Take(query, Value, query.ElementType, querySettings.EnableConstantParameterization); + if (Context.ElementClrType == null) + { + throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); } + + return ExpressionHelpers.Take(query, Value, query.ElementType, querySettings.EnableConstantParameterization); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/QueryFilterProvider.cs b/src/Microsoft.AspNetCore.OData/Query/QueryFilterProvider.cs index 40f7c7252..d5fe762bb 100644 --- a/src/Microsoft.AspNetCore.OData/Query/QueryFilterProvider.cs +++ b/src/Microsoft.AspNetCore.OData/Query/QueryFilterProvider.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -13,148 +13,147 @@ using Microsoft.AspNetCore.OData.Common; using Microsoft.AspNetCore.OData.Results; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +/// +/// An implementation of that applies an action filter to +/// any action with an or return type +/// that doesn't bind a parameter of type . +/// +public class QueryFilterProvider : IFilterProvider { /// - /// An implementation of that applies an action filter to - /// any action with an or return type - /// that doesn't bind a parameter of type . + /// Initializes a new instance of the class. /// - public class QueryFilterProvider : IFilterProvider + /// The action filter that executes the query. + public QueryFilterProvider(IActionFilter queryFilter) { - /// - /// Initializes a new instance of the class. - /// - /// The action filter that executes the query. - public QueryFilterProvider(IActionFilter queryFilter) + if (queryFilter == null) { - if (queryFilter == null) - { - throw Error.ArgumentNull(nameof(queryFilter)); - } - - QueryFilter = queryFilter; + throw Error.ArgumentNull(nameof(queryFilter)); } - /// - /// Gets the action filter that executes the query. - /// - public IActionFilter QueryFilter { get; } - - /// - /// Gets the order value for determining the order of execution of providers. Providers - /// execute in ascending numeric value of the Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order - /// property. - /// - public int Order - { - get - { - // Providers are executed in an ordering determined by an ascending sort of the - // Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order property. A provider with - // a lower numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order - // will have its Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.OnProvidersExecuting(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext) - // called before that of a provider with a higher numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order. - // The Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.OnProvidersExecuted(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext) - // method is called in the reverse ordering after all calls to Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.OnProvidersExecuting(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext). - // A provider with a lower numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order - // will have its Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.OnProvidersExecuted(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext) - // method called after that of a provider with a higher numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order. - // If two providers have the same numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order, - // then their relative execution order is undefined. - return 0; - } - } + QueryFilter = queryFilter; + } - /// - /// Provides filters to apply to the specified action. - /// - /// The filter context. - public void OnProvidersExecuting(FilterProviderContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + /// + /// Gets the action filter that executes the query. + /// + public IActionFilter QueryFilter { get; } - // Actions with a bound parameter of type ODataQueryOptions do not support the query filter - // The assumption is that the action will handle the querying within the action implementation - ControllerActionDescriptor controllerActionDescriptor = context.ActionContext.ActionDescriptor as ControllerActionDescriptor; - if (controllerActionDescriptor != null) - { - Type returnType = controllerActionDescriptor.MethodInfo.ReturnType; - if (ShouldAddFilter(context, returnType, controllerActionDescriptor)) - { - var filterDesc = new FilterDescriptor(QueryFilter, FilterScope.Global); - context.Results.Add(new FilterItem(filterDesc, QueryFilter)); - } - } + /// + /// Gets the order value for determining the order of execution of providers. Providers + /// execute in ascending numeric value of the Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order + /// property. + /// + public int Order + { + get + { + // Providers are executed in an ordering determined by an ascending sort of the + // Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order property. A provider with + // a lower numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order + // will have its Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.OnProvidersExecuting(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext) + // called before that of a provider with a higher numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order. + // The Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.OnProvidersExecuted(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext) + // method is called in the reverse ordering after all calls to Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.OnProvidersExecuting(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext). + // A provider with a lower numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order + // will have its Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.OnProvidersExecuted(Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext) + // method called after that of a provider with a higher numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order. + // If two providers have the same numeric value of Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order, + // then their relative execution order is undefined. + return 0; } + } - /// - /// Summary: - /// Called in decreasing Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order, - /// after all Microsoft.AspNetCore.Mvc.Filters.IFilterProviders have executed once. - /// - /// The Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext. - public void OnProvidersExecuted(FilterProviderContext context) + /// + /// Provides filters to apply to the specified action. + /// + /// The filter context. + public void OnProvidersExecuting(FilterProviderContext context) + { + if (context == null) { + throw Error.ArgumentNull(nameof(context)); } - private bool ShouldAddFilter(FilterProviderContext context, Type returnType, ControllerActionDescriptor controllerActionDescriptor) + // Actions with a bound parameter of type ODataQueryOptions do not support the query filter + // The assumption is that the action will handle the querying within the action implementation + ControllerActionDescriptor controllerActionDescriptor = context.ActionContext.ActionDescriptor as ControllerActionDescriptor; + if (controllerActionDescriptor != null) { - if (returnType == null) + Type returnType = controllerActionDescriptor.MethodInfo.ReturnType; + if (ShouldAddFilter(context, returnType, controllerActionDescriptor)) { - return false; + var filterDesc = new FilterDescriptor(QueryFilter, FilterScope.Global); + context.Results.Add(new FilterItem(filterDesc, QueryFilter)); } + } + } - // Get the inner return type if type is a task. - Type innerReturnType = returnType; - if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) - { - innerReturnType = returnType.GetGenericArguments().First(); - } + /// + /// Summary: + /// Called in decreasing Microsoft.AspNetCore.Mvc.Filters.IFilterProvider.Order, + /// after all Microsoft.AspNetCore.Mvc.Filters.IFilterProviders have executed once. + /// + /// The Microsoft.AspNetCore.Mvc.Filters.FilterProviderContext. + public void OnProvidersExecuted(FilterProviderContext context) + { + } - // See if this type is a SingleResult or is derived from SingleResult. - bool isSingleResult = false; - if (innerReturnType.IsGenericType) - { - Type genericType = innerReturnType.GetGenericTypeDefinition(); - Type baseType = innerReturnType.BaseType; - isSingleResult = (genericType == typeof(SingleResult<>) || baseType == typeof(SingleResult)); - } + private bool ShouldAddFilter(FilterProviderContext context, Type returnType, ControllerActionDescriptor controllerActionDescriptor) + { + if (returnType == null) + { + return false; + } - // Don't apply the filter if the result is not IQueryable() or SingleReult(). - if (!IsIQueryable(innerReturnType) && !isSingleResult) - { - return false; - } + // Get the inner return type if type is a task. + Type innerReturnType = returnType; + if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) + { + innerReturnType = returnType.GetGenericArguments().First(); + } - // If the controller takes a ODataQueryOptions, don't apply the filter. - if (controllerActionDescriptor.Parameters - .Any(parameter => TypeHelper.IsTypeAssignableFrom(typeof(ODataQueryOptions), parameter.ParameterType))) - { - return false; - } + // See if this type is a SingleResult or is derived from SingleResult. + bool isSingleResult = false; + if (innerReturnType.IsGenericType) + { + Type genericType = innerReturnType.GetGenericTypeDefinition(); + Type baseType = innerReturnType.BaseType; + isSingleResult = (genericType == typeof(SingleResult<>) || baseType == typeof(SingleResult)); + } - // Don't apply a global filter if one of the same type exists. - if (context.Results.Where(f => f.Filter?.GetType() == QueryFilter.GetType()).Any()) - { - return false; - } + // Don't apply the filter if the result is not IQueryable() or SingleReult(). + if (!IsIQueryable(innerReturnType) && !isSingleResult) + { + return false; + } - return true; + // If the controller takes a ODataQueryOptions, don't apply the filter. + if (controllerActionDescriptor.Parameters + .Any(parameter => TypeHelper.IsTypeAssignableFrom(typeof(ODataQueryOptions), parameter.ParameterType))) + { + return false; } - /// - /// Determines whether the given type is IQueryable. - /// - /// The type - /// true if the type is IQueryable. - internal static bool IsIQueryable(Type type) + // Don't apply a global filter if one of the same type exists. + if (context.Results.Where(f => f.Filter?.GetType() == QueryFilter.GetType()).Any()) { - return type == typeof(IQueryable) || - (type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IQueryable<>)); + return false; } + + return true; + } + + /// + /// Determines whether the given type is IQueryable. + /// + /// The type + /// true if the type is IQueryable. + internal static bool IsIQueryable(Type type) + { + return type == typeof(IQueryable) || + (type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IQueryable<>)); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/SelectExpandIncludedProperty.cs b/src/Microsoft.AspNetCore.OData/Query/SelectExpandIncludedProperty.cs index 6c42d862f..9f297010d 100644 --- a/src/Microsoft.AspNetCore.OData/Query/SelectExpandIncludedProperty.cs +++ b/src/Microsoft.AspNetCore.OData/Query/SelectExpandIncludedProperty.cs @@ -11,209 +11,208 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +internal class SelectExpandIncludedProperty { - internal class SelectExpandIncludedProperty + /// + /// the corresponding property segment. + /// + private PropertySegment _propertySegment; + + /// + /// the corresponding navigation source. maybe useless + /// + private IEdmNavigationSource _navigationSource; + + /// + /// the path select item for this property. + /// for example: $select=abc or $select=NS.Type/abc + /// + private PathSelectItem _propertySelectItem; + + /// + /// the sub $select and $expand for this property. + /// + private IList _subSelectItems; + + /// + /// Creates a new instance of the class. + /// + /// The property segment that has this select expand item. + public SelectExpandIncludedProperty(PropertySegment propertySegment) + : this(propertySegment, null) + { + } + + /// + /// Creates a new instance of the class. + /// + /// The property segment that has this select expand item. + /// The target navigation source of this property segment. + public SelectExpandIncludedProperty(PropertySegment propertySegment, IEdmNavigationSource navigationSource) { - /// - /// the corresponding property segment. - /// - private PropertySegment _propertySegment; - - /// - /// the corresponding navigation source. maybe useless - /// - private IEdmNavigationSource _navigationSource; - - /// - /// the path select item for this property. - /// for example: $select=abc or $select=NS.Type/abc - /// - private PathSelectItem _propertySelectItem; - - /// - /// the sub $select and $expand for this property. - /// - private IList _subSelectItems; - - /// - /// Creates a new instance of the class. - /// - /// The property segment that has this select expand item. - public SelectExpandIncludedProperty(PropertySegment propertySegment) - : this(propertySegment, null) + if (propertySegment == null) { + throw new ArgumentNullException(nameof(propertySegment)); } - /// - /// Creates a new instance of the class. - /// - /// The property segment that has this select expand item. - /// The target navigation source of this property segment. - public SelectExpandIncludedProperty(PropertySegment propertySegment, IEdmNavigationSource navigationSource) - { - if (propertySegment == null) - { - throw new ArgumentNullException(nameof(propertySegment)); - } + _propertySegment = propertySegment; + _navigationSource = navigationSource; + } - _propertySegment = propertySegment; - _navigationSource = navigationSource; + /// + /// Gets the merged for this property. + /// + /// Null or the created . + public PathSelectItem ToPathSelectItem() + { + if (_subSelectItems == null) + { + return _propertySelectItem; } - /// - /// Gets the merged for this property. - /// - /// Null or the created . - public PathSelectItem ToPathSelectItem() + // so, _subSelectItems is not null, merge the select and expand from the property into the _subSelectItems + bool isSelectAll = false; + if (_propertySelectItem != null && _propertySelectItem.SelectAndExpand != null) { - if (_subSelectItems == null) + // Retrieve the "IsSelectAll" from the property sub select expand clause. + isSelectAll = _propertySelectItem.SelectAndExpand.AllSelected; + foreach (var selectItem in _propertySelectItem.SelectAndExpand.SelectedItems) { - return _propertySelectItem; - } - - // so, _subSelectItems is not null, merge the select and expand from the property into the _subSelectItems - bool isSelectAll = false; - if (_propertySelectItem != null && _propertySelectItem.SelectAndExpand != null) - { - // Retrieve the "IsSelectAll" from the property sub select expand clause. - isSelectAll = _propertySelectItem.SelectAndExpand.AllSelected; - foreach (var selectItem in _propertySelectItem.SelectAndExpand.SelectedItems) - { - _subSelectItems.Add(selectItem); - } + _subSelectItems.Add(selectItem); } + } - if (isSelectAll) - { - // We do nothing here, because the property itself tells us to select all. - // Meanwhile, ODL doesn't allow $select=abc,abc(...), so it's safe to use "SelectAll". - } - else + if (isSelectAll) + { + // We do nothing here, because the property itself tells us to select all. + // Meanwhile, ODL doesn't allow $select=abc,abc(...), so it's safe to use "SelectAll". + } + else + { + // Mark select-all equals "true" if only include $expand + // So, if only "$expand=abc/nav", it means to select all for "abc" then expand "nav". + isSelectAll = true; + foreach (var item in _subSelectItems) { - // Mark select-all equals "true" if only include $expand - // So, if only "$expand=abc/nav", it means to select all for "abc" then expand "nav". - isSelectAll = true; - foreach (var item in _subSelectItems) + // only include $expand=...., means selectAll as true + if (!(item is ExpandedNavigationSelectItem || item is ExpandedReferenceSelectItem)) { - // only include $expand=...., means selectAll as true - if (!(item is ExpandedNavigationSelectItem || item is ExpandedReferenceSelectItem)) - { - isSelectAll = false; - break; - } + isSelectAll = false; + break; } } + } - SelectExpandClause subSelectExpandClause = new SelectExpandClause(_subSelectItems, isSelectAll); + SelectExpandClause subSelectExpandClause = new SelectExpandClause(_subSelectItems, isSelectAll); - if (_propertySelectItem == null && subSelectExpandClause == null) - { - return null; - } - else if (_propertySelectItem == null) - { - return new PathSelectItem(new ODataSelectPath(_propertySegment), _navigationSource, subSelectExpandClause, - null, - null, - null, - null, - null, - null, - null); - } - else - { - return new PathSelectItem(new ODataSelectPath(_propertySegment), _navigationSource, subSelectExpandClause, - _propertySelectItem.FilterOption, - _propertySelectItem.OrderByOption, - _propertySelectItem.TopOption, - _propertySelectItem.SkipOption, - _propertySelectItem.CountOption, - _propertySelectItem.SearchOption, - _propertySelectItem.ComputeOption); - } + if (_propertySelectItem == null && subSelectExpandClause == null) + { + return null; } - - /// - /// Add sub $select item for this include property. - /// - /// The remaining segments star from this include property. - /// The old $select item. - public void AddSubSelectItem(IList remainingSegments, PathSelectItem oldSelectItem) + else if (_propertySelectItem == null) { - if (remainingSegments == null) - { - // Be noted: In ODL v7.6.1, it's not allowed duplicated properties in $select. - // for example: "$select=abc($top=2),abc($skip=2)" is not allowed in ODL library. - // So, don't worry about the previous setting overridden by other same path. - // However, it's possibility in later ODL version (>=7.6.2) to allow duplicated properties in $select. - // It that case, please update the codes here otherwise the latter will win. - - // Besides, $select=abc,abc($top=2) is not allowed in ODL 7.6.1. - Contract.Assert(_propertySelectItem == null); - _propertySelectItem = oldSelectItem; - } - else - { - if (_subSelectItems == null) - { - _subSelectItems = new List(); - } - - _subSelectItems.Add(new PathSelectItem(new ODataSelectPath(remainingSegments), oldSelectItem.NavigationSource, - oldSelectItem.SelectAndExpand, oldSelectItem.FilterOption, - oldSelectItem.OrderByOption, oldSelectItem.TopOption, - oldSelectItem.SkipOption, oldSelectItem.CountOption, - oldSelectItem.SearchOption, oldSelectItem.ComputeOption)); - } + return new PathSelectItem(new ODataSelectPath(_propertySegment), _navigationSource, subSelectExpandClause, + null, + null, + null, + null, + null, + null, + null); } - - /// - /// Add sub $expand item for this include property. - /// - /// The remaining segments star from this include property. - /// The old $expand item. - public void AddSubExpandItem(IList remainingSegments, ExpandedReferenceSelectItem oldRefItem) + else { - // remainingSegments should never be null, because at least a navigation property segment in it. - Contract.Assert(remainingSegments != null); + return new PathSelectItem(new ODataSelectPath(_propertySegment), _navigationSource, subSelectExpandClause, + _propertySelectItem.FilterOption, + _propertySelectItem.OrderByOption, + _propertySelectItem.TopOption, + _propertySelectItem.SkipOption, + _propertySelectItem.CountOption, + _propertySelectItem.SearchOption, + _propertySelectItem.ComputeOption); + } + } + /// + /// Add sub $select item for this include property. + /// + /// The remaining segments star from this include property. + /// The old $select item. + public void AddSubSelectItem(IList remainingSegments, PathSelectItem oldSelectItem) + { + if (remainingSegments == null) + { + // Be noted: In ODL v7.6.1, it's not allowed duplicated properties in $select. + // for example: "$select=abc($top=2),abc($skip=2)" is not allowed in ODL library. + // So, don't worry about the previous setting overridden by other same path. + // However, it's possibility in later ODL version (>=7.6.2) to allow duplicated properties in $select. + // It that case, please update the codes here otherwise the latter will win. + + // Besides, $select=abc,abc($top=2) is not allowed in ODL 7.6.1. + Contract.Assert(_propertySelectItem == null); + _propertySelectItem = oldSelectItem; + } + else + { if (_subSelectItems == null) { _subSelectItems = new List(); } - ODataExpandPath newPath = new ODataExpandPath(remainingSegments); - ExpandedNavigationSelectItem expandedNav = oldRefItem as ExpandedNavigationSelectItem; - if (expandedNav != null) - { - _subSelectItems.Add(new ExpandedNavigationSelectItem(newPath, - expandedNav.NavigationSource, - expandedNav.SelectAndExpand, - expandedNav.FilterOption, - expandedNav.OrderByOption, - expandedNav.TopOption, - expandedNav.SkipOption, - expandedNav.CountOption, - expandedNav.SearchOption, - expandedNav.LevelsOption, - expandedNav.ComputeOption, - expandedNav.ApplyOption)); - } - else - { - _subSelectItems.Add(new ExpandedReferenceSelectItem(newPath, - oldRefItem.NavigationSource, - oldRefItem.FilterOption, - oldRefItem.OrderByOption, - oldRefItem.TopOption, - oldRefItem.SkipOption, - oldRefItem.CountOption, - oldRefItem.SearchOption, - oldRefItem.ComputeOption, - oldRefItem.ApplyOption)); - } + _subSelectItems.Add(new PathSelectItem(new ODataSelectPath(remainingSegments), oldSelectItem.NavigationSource, + oldSelectItem.SelectAndExpand, oldSelectItem.FilterOption, + oldSelectItem.OrderByOption, oldSelectItem.TopOption, + oldSelectItem.SkipOption, oldSelectItem.CountOption, + oldSelectItem.SearchOption, oldSelectItem.ComputeOption)); + } + } + + /// + /// Add sub $expand item for this include property. + /// + /// The remaining segments star from this include property. + /// The old $expand item. + public void AddSubExpandItem(IList remainingSegments, ExpandedReferenceSelectItem oldRefItem) + { + // remainingSegments should never be null, because at least a navigation property segment in it. + Contract.Assert(remainingSegments != null); + + if (_subSelectItems == null) + { + _subSelectItems = new List(); + } + + ODataExpandPath newPath = new ODataExpandPath(remainingSegments); + ExpandedNavigationSelectItem expandedNav = oldRefItem as ExpandedNavigationSelectItem; + if (expandedNav != null) + { + _subSelectItems.Add(new ExpandedNavigationSelectItem(newPath, + expandedNav.NavigationSource, + expandedNav.SelectAndExpand, + expandedNav.FilterOption, + expandedNav.OrderByOption, + expandedNav.TopOption, + expandedNav.SkipOption, + expandedNav.CountOption, + expandedNav.SearchOption, + expandedNav.LevelsOption, + expandedNav.ComputeOption, + expandedNav.ApplyOption)); + } + else + { + _subSelectItems.Add(new ExpandedReferenceSelectItem(newPath, + oldRefItem.NavigationSource, + oldRefItem.FilterOption, + oldRefItem.OrderByOption, + oldRefItem.TopOption, + oldRefItem.SkipOption, + oldRefItem.CountOption, + oldRefItem.SearchOption, + oldRefItem.ComputeOption, + oldRefItem.ApplyOption)); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/SelectExpandPathExtensions.cs b/src/Microsoft.AspNetCore.OData/Query/SelectExpandPathExtensions.cs index 4b96194a1..a3b7c669f 100644 --- a/src/Microsoft.AspNetCore.OData/Query/SelectExpandPathExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/SelectExpandPathExtensions.cs @@ -11,121 +11,120 @@ using Microsoft.OData; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query +namespace Microsoft.AspNetCore.OData.Query; + +internal static class SelectExpandPathExtensions { - internal static class SelectExpandPathExtensions + /// + /// Verify the $select path and gets the first non type cast segment in a select path. + /// For example: $select=NS.SubType1/abc/NS.SubType2/xyz + /// => firstPropertySegment: "abc" + /// => remainingSegments: NS.SubType2/xyz + /// + /// The input $select path. + /// The remaining segments after the first non type segment. + /// First non-type cast segment. + public static ODataPathSegment GetFirstNonTypeCastSegment(this ODataSelectPath selectPath, + out IList remainingSegments) { - /// - /// Verify the $select path and gets the first non type cast segment in a select path. - /// For example: $select=NS.SubType1/abc/NS.SubType2/xyz - /// => firstPropertySegment: "abc" - /// => remainingSegments: NS.SubType2/xyz - /// - /// The input $select path. - /// The remaining segments after the first non type segment. - /// First non-type cast segment. - public static ODataPathSegment GetFirstNonTypeCastSegment(this ODataSelectPath selectPath, - out IList remainingSegments) + if (selectPath == null) { - if (selectPath == null) - { - throw new ArgumentNullException(nameof(selectPath)); - } - - // In fact, ODataSelectPath constructor verifies the supporting segments, we add the verification here for double check. - return GetFirstNonTypeCastSegment(selectPath, - //The middle segment should be "TypeSegment" or "PropertySegment". - m => m is PropertySegment || m is TypeSegment, - // The last segment could be "NavigationPropertySegment, PropertySegment, OperationSegment, DynamicPathSegment" - s => s is NavigationPropertySegment || s is PropertySegment || s is OperationSegment || s is DynamicPathSegment, - out remainingSegments); + throw new ArgumentNullException(nameof(selectPath)); } - /// - /// Verify the $expand path and gets the first non type cast segment in this expand path. - /// For example: $expand=NS.SubType1/abc/NS.SubType2/nav - /// => firstPropertySegment: "abc" - /// => remainingSegments: NS.SubType2/nav - /// => leadingTypeSegment: NS.SubType1 - /// - /// The input $expand path. - /// The remaining segments after the first non type segment. - /// First non-type cast segment. - public static ODataPathSegment GetFirstNonTypeCastSegment(this ODataExpandPath expandPath, - out IList remainingSegments) - { - if (expandPath == null) - { - throw new ArgumentNullException(nameof(expandPath)); - } + // In fact, ODataSelectPath constructor verifies the supporting segments, we add the verification here for double check. + return GetFirstNonTypeCastSegment(selectPath, + //The middle segment should be "TypeSegment" or "PropertySegment". + m => m is PropertySegment || m is TypeSegment, + // The last segment could be "NavigationPropertySegment, PropertySegment, OperationSegment, DynamicPathSegment" + s => s is NavigationPropertySegment || s is PropertySegment || s is OperationSegment || s is DynamicPathSegment, + out remainingSegments); + } - // In fact, ODataExpandPath constructor verifies the supporting segments, we add the verification here for double check. - return GetFirstNonTypeCastSegment(expandPath, - //The middle segment should be "TypeSegment" or "PropertySegment". - m => m is PropertySegment || m is TypeSegment, - // The last segment could be "NavigationPropertySegment" - s => s is NavigationPropertySegment, - out remainingSegments); + /// + /// Verify the $expand path and gets the first non type cast segment in this expand path. + /// For example: $expand=NS.SubType1/abc/NS.SubType2/nav + /// => firstPropertySegment: "abc" + /// => remainingSegments: NS.SubType2/nav + /// => leadingTypeSegment: NS.SubType1 + /// + /// The input $expand path. + /// The remaining segments after the first non type segment. + /// First non-type cast segment. + public static ODataPathSegment GetFirstNonTypeCastSegment(this ODataExpandPath expandPath, + out IList remainingSegments) + { + if (expandPath == null) + { + throw new ArgumentNullException(nameof(expandPath)); } - private static ODataPathSegment GetFirstNonTypeCastSegment(ODataPath path, - Func middleSegmentPredicte, - Func lastSegmentPredicte, - out IList remainingSegments) // could be null - { - Contract.Assert(path != null); + // In fact, ODataExpandPath constructor verifies the supporting segments, we add the verification here for double check. + return GetFirstNonTypeCastSegment(expandPath, + //The middle segment should be "TypeSegment" or "PropertySegment". + m => m is PropertySegment || m is TypeSegment, + // The last segment could be "NavigationPropertySegment" + s => s is NavigationPropertySegment, + out remainingSegments); + } - remainingSegments = null; - ODataPathSegment firstNonTypeSegment = null; - int lastIndex = path.Count - 1; - int index = 0; - foreach (var segment in path) + private static ODataPathSegment GetFirstNonTypeCastSegment(ODataPath path, + Func middleSegmentPredicte, + Func lastSegmentPredicte, + out IList remainingSegments) // could be null + { + Contract.Assert(path != null); + + remainingSegments = null; + ODataPathSegment firstNonTypeSegment = null; + int lastIndex = path.Count - 1; + int index = 0; + foreach (var segment in path) + { + if (index == lastIndex) { - if (index == lastIndex) + // Last segment + if (!lastSegmentPredicte(segment)) { - // Last segment - if (!lastSegmentPredicte(segment)) - { - throw new ODataException(Error.Format(SRResources.InvalidLastSegmentInSelectExpandPath, segment.GetType().Name)); - } + throw new ODataException(Error.Format(SRResources.InvalidLastSegmentInSelectExpandPath, segment.GetType().Name)); } - else + } + else + { + // middle segment + if (!middleSegmentPredicte(segment)) { - // middle segment - if (!middleSegmentPredicte(segment)) - { - throw new ODataException(Error.Format(SRResources.InvalidSegmentInSelectExpandPath, segment.GetType().Name)); - } + throw new ODataException(Error.Format(SRResources.InvalidSegmentInSelectExpandPath, segment.GetType().Name)); } + } - index++; + index++; - if (firstNonTypeSegment != null) + if (firstNonTypeSegment != null) + { + if (remainingSegments == null) { - if (remainingSegments == null) - { - remainingSegments = new List(); - } - - remainingSegments.Add(segment); - continue; + remainingSegments = new List(); } - // Theoretically, a path like: "~/NS.BaseType/NS.SubType1/NS.SubType2/PropertyOnSubType2" is valid(?) but not allowed. - // However, the functionality of the above path is same as "~/NS.SubType2/PropertyOnSubType2" (omit the middle type cast). - // So, Let's only care about the last segment in the leading segments. - // if we have the leading segments, and the last segment must be the type segment and it's verified. - if (segment is TypeSegment) - { - // do nothing here, just skip the leading type segment - } - else - { - firstNonTypeSegment = segment; - } + remainingSegments.Add(segment); + continue; } - return firstNonTypeSegment; + // Theoretically, a path like: "~/NS.BaseType/NS.SubType1/NS.SubType2/PropertyOnSubType2" is valid(?) but not allowed. + // However, the functionality of the above path is same as "~/NS.SubType2/PropertyOnSubType2" (omit the middle type cast). + // So, Let's only care about the last segment in the leading segments. + // if we have the leading segments, and the last segment must be the type segment and it's verified. + if (segment is TypeSegment) + { + // do nothing here, just skip the leading type segment + } + else + { + firstNonTypeSegment = segment; + } } + + return firstNonTypeSegment; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/ComputeQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/ComputeQueryValidator.cs index e16b14a64..75921c8fd 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/ComputeQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/ComputeQueryValidator.cs @@ -5,35 +5,34 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Represents a validator used to validate a based on the . +/// +public class ComputeQueryValidator : IComputeQueryValidator { /// - /// Represents a validator used to validate a based on the . + /// Validates a . /// - public class ComputeQueryValidator : IComputeQueryValidator + /// The $compute query. + /// The validation settings. + public virtual void Validate(ComputeQueryOption computeQueryOption, ODataValidationSettings validationSettings) { - /// - /// Validates a . - /// - /// The $compute query. - /// The validation settings. - public virtual void Validate(ComputeQueryOption computeQueryOption, ODataValidationSettings validationSettings) + if (computeQueryOption == null) { - if (computeQueryOption == null) - { - throw Error.ArgumentNull(nameof(computeQueryOption)); - } - - if (validationSettings == null) - { - throw Error.ArgumentNull(nameof(validationSettings)); - } + throw Error.ArgumentNull(nameof(computeQueryOption)); + } - // so far, we don't have validation rules here for $compute - // because 'DefaultQuerySetting' doesn't have configuration for $compute - // we can only let ODL to parse and verify the compute clause, - // however, developer can override this method add his own rules - _ = computeQueryOption.ComputeClause; + if (validationSettings == null) + { + throw Error.ArgumentNull(nameof(validationSettings)); } + + // so far, we don't have validation rules here for $compute + // because 'DefaultQuerySetting' doesn't have configuration for $compute + // we can only let ODL to parse and verify the compute clause, + // however, developer can override this method add his own rules + _ = computeQueryOption.ComputeClause; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/CountQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/CountQueryValidator.cs index 626387fe3..e50396479 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/CountQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/CountQueryValidator.cs @@ -10,50 +10,49 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Represents a validator used to validate a +/// based on the . +/// +public class CountQueryValidator : ICountQueryValidator { /// - /// Represents a validator used to validate a - /// based on the . + /// Validates a . /// - public class CountQueryValidator : ICountQueryValidator + /// The $count query. + /// The validation settings. + public virtual void Validate(CountQueryOption countQueryOption, ODataValidationSettings validationSettings) { - /// - /// Validates a . - /// - /// The $count query. - /// The validation settings. - public virtual void Validate(CountQueryOption countQueryOption, ODataValidationSettings validationSettings) + if (countQueryOption == null) { - if (countQueryOption == null) - { - throw Error.ArgumentNull(nameof(countQueryOption)); - } + throw Error.ArgumentNull(nameof(countQueryOption)); + } - if (validationSettings == null) - { - throw Error.ArgumentNull(nameof(validationSettings)); - } + if (validationSettings == null) + { + throw Error.ArgumentNull(nameof(validationSettings)); + } - ODataPath path = countQueryOption.Context.Path; + ODataPath path = countQueryOption.Context.Path; - if (path != null && path.Count > 0) + if (path != null && path.Count > 0) + { + IEdmProperty property = countQueryOption.Context.TargetProperty; + IEdmStructuredType structuredType = countQueryOption.Context.TargetStructuredType; + string name = countQueryOption.Context.TargetName; + if (EdmHelpers.IsNotCountable(property, structuredType, + countQueryOption.Context.Model, + countQueryOption.Context.DefaultQueryConfigurations.EnableCount)) { - IEdmProperty property = countQueryOption.Context.TargetProperty; - IEdmStructuredType structuredType = countQueryOption.Context.TargetStructuredType; - string name = countQueryOption.Context.TargetName; - if (EdmHelpers.IsNotCountable(property, structuredType, - countQueryOption.Context.Model, - countQueryOption.Context.DefaultQueryConfigurations.EnableCount)) + if (property == null) + { + throw new InvalidOperationException(Error.Format(SRResources.NotCountableEntitySetUsedForCount, name)); + } + else { - if (property == null) - { - throw new InvalidOperationException(Error.Format(SRResources.NotCountableEntitySetUsedForCount, name)); - } - else - { - throw new InvalidOperationException(Error.Format(SRResources.NotCountablePropertyUsedForCount, name)); - } + throw new InvalidOperationException(Error.Format(SRResources.NotCountablePropertyUsedForCount, name)); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/FilterQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/FilterQueryValidator.cs index 54744654f..44e22733f 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/FilterQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/FilterQueryValidator.cs @@ -12,915 +12,914 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Represents a validator used to validate a based on the . +/// +public class FilterQueryValidator : IFilterQueryValidator { /// - /// Represents a validator used to validate a based on the . + /// Validates a . /// - public class FilterQueryValidator : IFilterQueryValidator + /// The $filter query. + /// The validation settings. + public virtual void Validate(FilterQueryOption filterQueryOption, ODataValidationSettings settings) { - /// - /// Validates a . - /// - /// The $filter query. - /// The validation settings. - public virtual void Validate(FilterQueryOption filterQueryOption, ODataValidationSettings settings) + if (filterQueryOption == null) { - if (filterQueryOption == null) - { - throw Error.ArgumentNull(nameof(filterQueryOption)); - } - - if (settings == null) - { - throw Error.ArgumentNull(nameof(settings)); - } - - FilterValidatorContext validatorContext = new FilterValidatorContext - { - Filter = filterQueryOption, - Context = filterQueryOption.Context, - ValidationSettings = settings, - Property = filterQueryOption.Context.TargetProperty, - StructuredType = filterQueryOption.Context.TargetStructuredType, - CurrentDepth = 0 - }; - - ValidateFilter(filterQueryOption.FilterClause, validatorContext); + throw Error.ArgumentNull(nameof(filterQueryOption)); } - /// - /// Validates a . - /// - /// The . - /// The validation context. - protected virtual void ValidateFilter(FilterClause filterClause, FilterValidatorContext validatorContext) + if (settings == null) { - Contract.Assert(filterClause != null); - - ValidateQueryNode(filterClause.Expression, validatorContext); + throw Error.ArgumentNull(nameof(settings)); } - /// - /// Override this method to restrict the 'all' query inside the filter query. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The all node to validate. - /// The validation context. - protected virtual void ValidateAllNode(AllNode allNode, FilterValidatorContext validatorContext) + FilterValidatorContext validatorContext = new FilterValidatorContext { - Contract.Assert(allNode != null); + Filter = filterQueryOption, + Context = filterQueryOption.Context, + ValidationSettings = settings, + Property = filterQueryOption.Context.TargetProperty, + StructuredType = filterQueryOption.Context.TargetStructuredType, + CurrentDepth = 0 + }; + + ValidateFilter(filterQueryOption.FilterClause, validatorContext); + } - ValidateFunction("all", validatorContext); - validatorContext.EnterLambda(); + /// + /// Validates a . + /// + /// The . + /// The validation context. + protected virtual void ValidateFilter(FilterClause filterClause, FilterValidatorContext validatorContext) + { + Contract.Assert(filterClause != null); - try - { - ValidateQueryNode(allNode.Source, validatorContext); + ValidateQueryNode(filterClause.Expression, validatorContext); + } - ValidateQueryNode(allNode.Body, validatorContext); - } - finally - { - validatorContext.ExitLambda(); - } - } + /// + /// Override this method to restrict the 'all' query inside the filter query. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The all node to validate. + /// The validation context. + protected virtual void ValidateAllNode(AllNode allNode, FilterValidatorContext validatorContext) + { + Contract.Assert(allNode != null); - /// - /// Override this method to restrict the 'any' query inside the filter query. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The any node to validate. - /// The validation context. - protected virtual void ValidateAnyNode(AnyNode anyNode, FilterValidatorContext validatorContext) + ValidateFunction("all", validatorContext); + validatorContext.EnterLambda(); + + try { - Contract.Assert(anyNode != null); + ValidateQueryNode(allNode.Source, validatorContext); - ValidateFunction("any", validatorContext); - validatorContext.EnterLambda(); + ValidateQueryNode(allNode.Body, validatorContext); + } + finally + { + validatorContext.ExitLambda(); + } + } - try - { - ValidateQueryNode(anyNode.Source, validatorContext); + /// + /// Override this method to restrict the 'any' query inside the filter query. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The any node to validate. + /// The validation context. + protected virtual void ValidateAnyNode(AnyNode anyNode, FilterValidatorContext validatorContext) + { + Contract.Assert(anyNode != null); - if (anyNode.Body != null && anyNode.Body.Kind != QueryNodeKind.Constant) - { - ValidateQueryNode(anyNode.Body, validatorContext); - } - } - finally - { - validatorContext.ExitLambda(); - } - } + ValidateFunction("any", validatorContext); + validatorContext.EnterLambda(); - /// - /// override this method to restrict the binary operators inside the filter query. That includes all the logical operators except 'not' and all math operators. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The binary operator node to validate. - /// The validation context. - protected virtual void ValidateBinaryOperatorNode(BinaryOperatorNode binaryOperatorNode, FilterValidatorContext validatorContext) + try { - Contract.Assert(binaryOperatorNode != null); + ValidateQueryNode(anyNode.Source, validatorContext); - // base case goes - switch (binaryOperatorNode.OperatorKind) + if (anyNode.Body != null && anyNode.Body.Kind != QueryNodeKind.Constant) { - case BinaryOperatorKind.Equal: - case BinaryOperatorKind.NotEqual: - case BinaryOperatorKind.And: - case BinaryOperatorKind.GreaterThan: - case BinaryOperatorKind.GreaterThanOrEqual: - case BinaryOperatorKind.LessThan: - case BinaryOperatorKind.LessThanOrEqual: - case BinaryOperatorKind.Or: - case BinaryOperatorKind.Has: - // binary logical operators - ValidateLogicalOperator(binaryOperatorNode, validatorContext); - break; - default: - // math operators - ValidateArithmeticOperator(binaryOperatorNode, validatorContext); - break; + ValidateQueryNode(anyNode.Body, validatorContext); } } - - /// - /// Override this method to validate the LogicalOperators such as 'eq', 'ne', 'gt', 'ge', 'lt', 'le', 'and', 'or'. - /// - /// Please note that 'not' is not included here. Please override ValidateUnaryOperatorNode to customize 'not'. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The binary operator node to validate. - /// The validation context. - protected virtual void ValidateLogicalOperator(BinaryOperatorNode binaryNode, FilterValidatorContext validatorContext) + finally { - Contract.Assert(binaryNode != null); - Contract.Assert(validatorContext != null); - - AllowedLogicalOperators logicalOperator = ToLogicalOperator(binaryNode); + validatorContext.ExitLambda(); + } + } - if ((validatorContext.ValidationSettings.AllowedLogicalOperators & logicalOperator) != logicalOperator) - { - // this means the given logical operator is not allowed - throw new ODataException(Error.Format(SRResources.NotAllowedLogicalOperator, logicalOperator, "AllowedLogicalOperators")); - } + /// + /// override this method to restrict the binary operators inside the filter query. That includes all the logical operators except 'not' and all math operators. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The binary operator node to validate. + /// The validation context. + protected virtual void ValidateBinaryOperatorNode(BinaryOperatorNode binaryOperatorNode, FilterValidatorContext validatorContext) + { + Contract.Assert(binaryOperatorNode != null); - // recursion case goes here - ValidateQueryNode(binaryNode.Left, validatorContext); - ValidateQueryNode(binaryNode.Right, validatorContext); + // base case goes + switch (binaryOperatorNode.OperatorKind) + { + case BinaryOperatorKind.Equal: + case BinaryOperatorKind.NotEqual: + case BinaryOperatorKind.And: + case BinaryOperatorKind.GreaterThan: + case BinaryOperatorKind.GreaterThanOrEqual: + case BinaryOperatorKind.LessThan: + case BinaryOperatorKind.LessThanOrEqual: + case BinaryOperatorKind.Or: + case BinaryOperatorKind.Has: + // binary logical operators + ValidateLogicalOperator(binaryOperatorNode, validatorContext); + break; + default: + // math operators + ValidateArithmeticOperator(binaryOperatorNode, validatorContext); + break; } + } + + /// + /// Override this method to validate the LogicalOperators such as 'eq', 'ne', 'gt', 'ge', 'lt', 'le', 'and', 'or'. + /// + /// Please note that 'not' is not included here. Please override ValidateUnaryOperatorNode to customize 'not'. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The binary operator node to validate. + /// The validation context. + protected virtual void ValidateLogicalOperator(BinaryOperatorNode binaryNode, FilterValidatorContext validatorContext) + { + Contract.Assert(binaryNode != null); + Contract.Assert(validatorContext != null); + + AllowedLogicalOperators logicalOperator = ToLogicalOperator(binaryNode); - /// - /// Override this method for the Arithmetic operators, including add, sub, mul, div, mod. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The binary operator node to validate. - /// The validation context. - protected virtual void ValidateArithmeticOperator(BinaryOperatorNode binaryNode, FilterValidatorContext validatorContext) + if ((validatorContext.ValidationSettings.AllowedLogicalOperators & logicalOperator) != logicalOperator) { - Contract.Assert(binaryNode != null); - Contract.Assert(validatorContext != null); + // this means the given logical operator is not allowed + throw new ODataException(Error.Format(SRResources.NotAllowedLogicalOperator, logicalOperator, "AllowedLogicalOperators")); + } - AllowedArithmeticOperators arithmeticOperator = ToArithmeticOperator(binaryNode); + // recursion case goes here + ValidateQueryNode(binaryNode.Left, validatorContext); + ValidateQueryNode(binaryNode.Right, validatorContext); + } - if ((validatorContext.ValidationSettings.AllowedArithmeticOperators & arithmeticOperator) != arithmeticOperator) - { - // this means the given logical operator is not allowed - throw new ODataException(Error.Format(SRResources.NotAllowedArithmeticOperator, arithmeticOperator, "AllowedArithmeticOperators")); - } + /// + /// Override this method for the Arithmetic operators, including add, sub, mul, div, mod. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The binary operator node to validate. + /// The validation context. + protected virtual void ValidateArithmeticOperator(BinaryOperatorNode binaryNode, FilterValidatorContext validatorContext) + { + Contract.Assert(binaryNode != null); + Contract.Assert(validatorContext != null); - // recursion case goes here - ValidateQueryNode(binaryNode.Left, validatorContext); - ValidateQueryNode(binaryNode.Right, validatorContext); - } + AllowedArithmeticOperators arithmeticOperator = ToArithmeticOperator(binaryNode); - /// - /// Override this method to restrict the 'constant' inside the filter query. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The constant node to validate. - /// The validation context. - protected virtual void ValidateConstantNode(ConstantNode constantNode, FilterValidatorContext validatorContext) + if ((validatorContext.ValidationSettings.AllowedArithmeticOperators & arithmeticOperator) != arithmeticOperator) { - // No default validation logic here. + // this means the given logical operator is not allowed + throw new ODataException(Error.Format(SRResources.NotAllowedArithmeticOperator, arithmeticOperator, "AllowedArithmeticOperators")); } - /// - /// Override this method to restrict the 'cast' inside the filter query. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The convert node to validate. - /// The validation context. - protected virtual void ValidateConvertNode(ConvertNode convertNode, FilterValidatorContext validatorContext) - { - Contract.Assert(convertNode != null); + // recursion case goes here + ValidateQueryNode(binaryNode.Left, validatorContext); + ValidateQueryNode(binaryNode.Right, validatorContext); + } - // Validate child nodes but not the ConvertNode itself. - ValidateQueryNode(convertNode.Source, validatorContext); - } + /// + /// Override this method to restrict the 'constant' inside the filter query. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The constant node to validate. + /// The validation context. + protected virtual void ValidateConstantNode(ConstantNode constantNode, FilterValidatorContext validatorContext) + { + // No default validation logic here. + } - /// - /// Override this method to restrict the '$count' inside the filter query. - /// - /// The count node to validate. - /// The validation context. - protected virtual void ValidateCountNode(CountNode countNode, FilterValidatorContext validatorContext) - { - Contract.Assert(countNode != null); - Contract.Assert(validatorContext != null); + /// + /// Override this method to restrict the 'cast' inside the filter query. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The convert node to validate. + /// The validation context. + protected virtual void ValidateConvertNode(ConvertNode convertNode, FilterValidatorContext validatorContext) + { + Contract.Assert(convertNode != null); - ValidateQueryNode(countNode.Source, validatorContext); + // Validate child nodes but not the ConvertNode itself. + ValidateQueryNode(convertNode.Source, validatorContext); + } - if (countNode.FilterClause != null) - { - ValidateQueryNode(countNode.FilterClause.Expression, validatorContext); - } + /// + /// Override this method to restrict the '$count' inside the filter query. + /// + /// The count node to validate. + /// The validation context. + protected virtual void ValidateCountNode(CountNode countNode, FilterValidatorContext validatorContext) + { + Contract.Assert(countNode != null); + Contract.Assert(validatorContext != null); - if (countNode.SearchClause != null) - { - ValidateQueryNode(countNode.SearchClause.Expression, validatorContext); - } - } + ValidateQueryNode(countNode.Source, validatorContext); - /// - /// Override this method for the navigation property node. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The source node to validate. - /// The navigation property. - /// The validation context. - protected virtual void ValidateNavigationPropertyNode(QueryNode sourceNode, IEdmNavigationProperty navigationProperty, FilterValidatorContext validatorContext) + if (countNode.FilterClause != null) { - Contract.Assert(navigationProperty != null); - - // Check whether the property is not filterable - if (EdmHelpers.IsNotFilterable(navigationProperty, - validatorContext.Property, - validatorContext.StructuredType, - validatorContext.Model, - validatorContext.Context.DefaultQueryConfigurations.EnableFilter)) - { - throw new ODataException(Error.Format(SRResources.NotFilterablePropertyUsedInFilter, - navigationProperty.Name)); - } + ValidateQueryNode(countNode.FilterClause.Expression, validatorContext); + } - // recursion - if (sourceNode != null) - { - ValidateQueryNode(sourceNode, validatorContext); - } + if (countNode.SearchClause != null) + { + ValidateQueryNode(countNode.SearchClause.Expression, validatorContext); } + } - /// - /// Override this method to validate the parameter used in the filter query. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The range variable node to validate. - /// The validation context. - protected virtual void ValidateRangeVariable(RangeVariable rangeVariable, FilterValidatorContext validatorContext) + /// + /// Override this method for the navigation property node. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The source node to validate. + /// The navigation property. + /// The validation context. + protected virtual void ValidateNavigationPropertyNode(QueryNode sourceNode, IEdmNavigationProperty navigationProperty, FilterValidatorContext validatorContext) + { + Contract.Assert(navigationProperty != null); + + // Check whether the property is not filterable + if (EdmHelpers.IsNotFilterable(navigationProperty, + validatorContext.Property, + validatorContext.StructuredType, + validatorContext.Model, + validatorContext.Context.DefaultQueryConfigurations.EnableFilter)) { - // No default validation logic here. + throw new ODataException(Error.Format(SRResources.NotFilterablePropertyUsedInFilter, + navigationProperty.Name)); } - /// - /// Override this method to validate property accessors. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The single value property access node. - /// The validation context. - protected virtual void ValidateSingleValuePropertyAccessNode(SingleValuePropertyAccessNode propertyAccessNode, FilterValidatorContext validatorContext) + // recursion + if (sourceNode != null) { - Contract.Assert(propertyAccessNode != null); - Contract.Assert(validatorContext != null); + ValidateQueryNode(sourceNode, validatorContext); + } + } + + /// + /// Override this method to validate the parameter used in the filter query. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The range variable node to validate. + /// The validation context. + protected virtual void ValidateRangeVariable(RangeVariable rangeVariable, FilterValidatorContext validatorContext) + { + // No default validation logic here. + } + + /// + /// Override this method to validate property accessors. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The single value property access node. + /// The validation context. + protected virtual void ValidateSingleValuePropertyAccessNode(SingleValuePropertyAccessNode propertyAccessNode, FilterValidatorContext validatorContext) + { + Contract.Assert(propertyAccessNode != null); + Contract.Assert(validatorContext != null); - IEdmModel model = validatorContext.Model; - var defaultQueryConfigs = validatorContext.Context.DefaultQueryConfigurations; + IEdmModel model = validatorContext.Model; + var defaultQueryConfigs = validatorContext.Context.DefaultQueryConfigurations; - // Check whether the property is filterable. - IEdmProperty property = propertyAccessNode.Property; - bool notFilterable = false; - if (propertyAccessNode.Source != null) + // Check whether the property is filterable. + IEdmProperty property = propertyAccessNode.Property; + bool notFilterable = false; + if (propertyAccessNode.Source != null) + { + if (propertyAccessNode.Source.Kind == QueryNodeKind.SingleNavigationNode) { - if (propertyAccessNode.Source.Kind == QueryNodeKind.SingleNavigationNode) - { - SingleNavigationNode singleNavigationNode = propertyAccessNode.Source as SingleNavigationNode; - notFilterable = EdmHelpers.IsNotFilterable(property, - singleNavigationNode.NavigationProperty, - singleNavigationNode.NavigationProperty.ToEntityType(), - model, - defaultQueryConfigs.EnableFilter); - } - else if (propertyAccessNode.Source.Kind == QueryNodeKind.SingleComplexNode) - { - SingleComplexNode singleComplexNode = propertyAccessNode.Source as SingleComplexNode; - notFilterable = EdmHelpers.IsNotFilterable(property, - singleComplexNode.Property, - property.DeclaringType, - model, - defaultQueryConfigs.EnableFilter); - } - else if (propertyAccessNode.Source.Kind == QueryNodeKind.ResourceRangeVariableReference) - { - ResourceRangeVariableReferenceNode resourceRangeVariableReferenceNode = propertyAccessNode.Source as ResourceRangeVariableReferenceNode; - notFilterable = EdmHelpers.IsNotFilterable( - property, - validatorContext.Property, - resourceRangeVariableReferenceNode.RangeVariable.StructuredTypeReference.StructuredDefinition(), - model, - defaultQueryConfigs.EnableFilter); - } - else - { - notFilterable = EdmHelpers.IsNotFilterable(property, - validatorContext.Property, - validatorContext.StructuredType, - model, - defaultQueryConfigs.EnableFilter); - } + SingleNavigationNode singleNavigationNode = propertyAccessNode.Source as SingleNavigationNode; + notFilterable = EdmHelpers.IsNotFilterable(property, + singleNavigationNode.NavigationProperty, + singleNavigationNode.NavigationProperty.ToEntityType(), + model, + defaultQueryConfigs.EnableFilter); } - - if (notFilterable) + else if (propertyAccessNode.Source.Kind == QueryNodeKind.SingleComplexNode) { - throw new ODataException(Error.Format(SRResources.NotFilterablePropertyUsedInFilter, property.Name)); + SingleComplexNode singleComplexNode = propertyAccessNode.Source as SingleComplexNode; + notFilterable = EdmHelpers.IsNotFilterable(property, + singleComplexNode.Property, + property.DeclaringType, + model, + defaultQueryConfigs.EnableFilter); } - - ValidateQueryNode(propertyAccessNode.Source, validatorContext); - } - - /// - /// Override this method to validate single complex property accessors. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The single complex node to validate. - /// The validation context. - protected virtual void ValidateSingleComplexNode(SingleComplexNode singleComplexNode, FilterValidatorContext validatorContext) - { - Contract.Assert(singleComplexNode != null); - Contract.Assert(validatorContext != null); - - // Check whether the property is filterable. - IEdmProperty property = singleComplexNode.Property; - if (EdmHelpers.IsNotFilterable(property, - validatorContext.Property, - validatorContext.StructuredType, - validatorContext.Model, - validatorContext.Context.DefaultQueryConfigurations.EnableFilter)) + else if (propertyAccessNode.Source.Kind == QueryNodeKind.ResourceRangeVariableReference) { - throw new ODataException(Error.Format(SRResources.NotFilterablePropertyUsedInFilter, property.Name)); + ResourceRangeVariableReferenceNode resourceRangeVariableReferenceNode = propertyAccessNode.Source as ResourceRangeVariableReferenceNode; + notFilterable = EdmHelpers.IsNotFilterable( + property, + validatorContext.Property, + resourceRangeVariableReferenceNode.RangeVariable.StructuredTypeReference.StructuredDefinition(), + model, + defaultQueryConfigs.EnableFilter); } - - ValidateQueryNode(singleComplexNode.Source, validatorContext); - } - - /// - /// Override this method to validate collection property accessors. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The collection property access node to validate. - /// The validation context. - protected virtual void ValidateCollectionPropertyAccessNode(CollectionPropertyAccessNode propertyAccessNode, FilterValidatorContext validatorContext) - { - Contract.Assert(propertyAccessNode != null); - Contract.Assert(validatorContext != null); - - // Check whether the property is filterable. - IEdmProperty property = propertyAccessNode.Property; - if (EdmHelpers.IsNotFilterable(property, - validatorContext.Property, - validatorContext.StructuredType, - validatorContext.Model, - validatorContext.Context.DefaultQueryConfigurations.EnableFilter)) + else { - throw new ODataException(Error.Format(SRResources.NotFilterablePropertyUsedInFilter, property.Name)); + notFilterable = EdmHelpers.IsNotFilterable(property, + validatorContext.Property, + validatorContext.StructuredType, + model, + defaultQueryConfigs.EnableFilter); } - - ValidateQueryNode(propertyAccessNode.Source, validatorContext); } - /// - /// Override this method to validate collection complex property accessors. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The collection complex node to validate. - /// The validation context. - protected virtual void ValidateCollectionComplexNode(CollectionComplexNode collectionComplexNode, FilterValidatorContext validatorContext) + if (notFilterable) { - Contract.Assert(collectionComplexNode != null); - Contract.Assert(validatorContext != null); - - // Check whether the property is filterable. - IEdmProperty property = collectionComplexNode.Property; - if (EdmHelpers.IsNotFilterable(property, validatorContext.Property, - validatorContext.StructuredType, - validatorContext.Model, - validatorContext.Context.DefaultQueryConfigurations.EnableFilter)) - { - throw new ODataException(Error.Format(SRResources.NotFilterablePropertyUsedInFilter, property.Name)); - } - - ValidateQueryNode(collectionComplexNode.Source, validatorContext); + throw new ODataException(Error.Format(SRResources.NotFilterablePropertyUsedInFilter, property.Name)); } - /// - /// Override this method to validate Function calls, such as 'length', 'year', etc. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The single value function call node to validate. - /// The validation context. - protected virtual void ValidateSingleValueFunctionCallNode(SingleValueFunctionCallNode node, FilterValidatorContext validatorContext) + ValidateQueryNode(propertyAccessNode.Source, validatorContext); + } + + /// + /// Override this method to validate single complex property accessors. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The single complex node to validate. + /// The validation context. + protected virtual void ValidateSingleComplexNode(SingleComplexNode singleComplexNode, FilterValidatorContext validatorContext) + { + Contract.Assert(singleComplexNode != null); + Contract.Assert(validatorContext != null); + + // Check whether the property is filterable. + IEdmProperty property = singleComplexNode.Property; + if (EdmHelpers.IsNotFilterable(property, + validatorContext.Property, + validatorContext.StructuredType, + validatorContext.Model, + validatorContext.Context.DefaultQueryConfigurations.EnableFilter)) { - Contract.Assert(node != null); - Contract.Assert(validatorContext != null); + throw new ODataException(Error.Format(SRResources.NotFilterablePropertyUsedInFilter, property.Name)); + } - ValidateFunction(node.Name, validatorContext); + ValidateQueryNode(singleComplexNode.Source, validatorContext); + } - foreach (QueryNode argumentNode in node.Parameters) - { - ValidateQueryNode(argumentNode, validatorContext); - } + /// + /// Override this method to validate collection property accessors. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The collection property access node to validate. + /// The validation context. + protected virtual void ValidateCollectionPropertyAccessNode(CollectionPropertyAccessNode propertyAccessNode, FilterValidatorContext validatorContext) + { + Contract.Assert(propertyAccessNode != null); + Contract.Assert(validatorContext != null); + + // Check whether the property is filterable. + IEdmProperty property = propertyAccessNode.Property; + if (EdmHelpers.IsNotFilterable(property, + validatorContext.Property, + validatorContext.StructuredType, + validatorContext.Model, + validatorContext.Context.DefaultQueryConfigurations.EnableFilter)) + { + throw new ODataException(Error.Format(SRResources.NotFilterablePropertyUsedInFilter, property.Name)); } - /// - /// Override this method to validate single resource function calls, such as 'cast'. - /// - /// The node to validate. - /// The validation context. - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit - /// testing scenarios and is not intended to be called from user code. Call the Validate method to validate a - /// instance. - /// - protected virtual void ValidateSingleResourceFunctionCallNode(SingleResourceFunctionCallNode node, FilterValidatorContext validatorContext) + ValidateQueryNode(propertyAccessNode.Source, validatorContext); + } + + /// + /// Override this method to validate collection complex property accessors. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The collection complex node to validate. + /// The validation context. + protected virtual void ValidateCollectionComplexNode(CollectionComplexNode collectionComplexNode, FilterValidatorContext validatorContext) + { + Contract.Assert(collectionComplexNode != null); + Contract.Assert(validatorContext != null); + + // Check whether the property is filterable. + IEdmProperty property = collectionComplexNode.Property; + if (EdmHelpers.IsNotFilterable(property, validatorContext.Property, + validatorContext.StructuredType, + validatorContext.Model, + validatorContext.Context.DefaultQueryConfigurations.EnableFilter)) { - Contract.Assert(node != null); - Contract.Assert(validatorContext != null); + throw new ODataException(Error.Format(SRResources.NotFilterablePropertyUsedInFilter, property.Name)); + } + + ValidateQueryNode(collectionComplexNode.Source, validatorContext); + } - ValidateFunction(node.Name, validatorContext); + /// + /// Override this method to validate Function calls, such as 'length', 'year', etc. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The single value function call node to validate. + /// The validation context. + protected virtual void ValidateSingleValueFunctionCallNode(SingleValueFunctionCallNode node, FilterValidatorContext validatorContext) + { + Contract.Assert(node != null); + Contract.Assert(validatorContext != null); - foreach (QueryNode argumentNode in node.Parameters) - { - ValidateQueryNode(argumentNode, validatorContext); - } - } + ValidateFunction(node.Name, validatorContext); - /// - /// Override this method to validate the Not operator. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The unary operator node. - /// The validation context. - protected virtual void ValidateUnaryOperatorNode(UnaryOperatorNode unaryOperatorNode, FilterValidatorContext validatorContext) + foreach (QueryNode argumentNode in node.Parameters) { - Contract.Assert(unaryOperatorNode != null); - Contract.Assert(validatorContext != null); + ValidateQueryNode(argumentNode, validatorContext); + } + } - ValidateQueryNode(unaryOperatorNode.Operand, validatorContext); + /// + /// Override this method to validate single resource function calls, such as 'cast'. + /// + /// The node to validate. + /// The validation context. + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit + /// testing scenarios and is not intended to be called from user code. Call the Validate method to validate a + /// instance. + /// + protected virtual void ValidateSingleResourceFunctionCallNode(SingleResourceFunctionCallNode node, FilterValidatorContext validatorContext) + { + Contract.Assert(node != null); + Contract.Assert(validatorContext != null); - switch (unaryOperatorNode.OperatorKind) - { - case UnaryOperatorKind.Negate: - case UnaryOperatorKind.Not: - if ((validatorContext.ValidationSettings.AllowedLogicalOperators & AllowedLogicalOperators.Not) != AllowedLogicalOperators.Not) - { - throw new ODataException(Error.Format(SRResources.NotAllowedLogicalOperator, unaryOperatorNode.OperatorKind, "AllowedLogicalOperators")); - } - break; - - default: - throw Error.NotSupported(SRResources.UnaryNodeValidationNotSupported, unaryOperatorNode.OperatorKind, typeof(FilterQueryValidator).Name); - } - } + ValidateFunction(node.Name, validatorContext); - /// - /// Override this method if you want to visit each query node. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The query node. - /// The validation context. - protected virtual void ValidateQueryNode(QueryNode node, FilterValidatorContext validatorContext) + foreach (QueryNode argumentNode in node.Parameters) { - Contract.Assert(validatorContext != null); + ValidateQueryNode(argumentNode, validatorContext); + } + } - // Recursion guard to avoid stack overflows - RuntimeHelpers.EnsureSufficientExecutionStack(); + /// + /// Override this method to validate the Not operator. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The unary operator node. + /// The validation context. + protected virtual void ValidateUnaryOperatorNode(UnaryOperatorNode unaryOperatorNode, FilterValidatorContext validatorContext) + { + Contract.Assert(unaryOperatorNode != null); + Contract.Assert(validatorContext != null); - SingleValueNode singleNode = node as SingleValueNode; - CollectionNode collectionNode = node as CollectionNode; + ValidateQueryNode(unaryOperatorNode.Operand, validatorContext); - validatorContext.IncrementNodeCount(); + switch (unaryOperatorNode.OperatorKind) + { + case UnaryOperatorKind.Negate: + case UnaryOperatorKind.Not: + if ((validatorContext.ValidationSettings.AllowedLogicalOperators & AllowedLogicalOperators.Not) != AllowedLogicalOperators.Not) + { + throw new ODataException(Error.Format(SRResources.NotAllowedLogicalOperator, unaryOperatorNode.OperatorKind, "AllowedLogicalOperators")); + } + break; - if (singleNode != null) - { - ValidateSingleValueNode(singleNode, validatorContext); - } - else if (collectionNode != null) - { - ValidateCollectionNode(collectionNode, validatorContext); - } + default: + throw Error.NotSupported(SRResources.UnaryNodeValidationNotSupported, unaryOperatorNode.OperatorKind, typeof(FilterQueryValidator).Name); } + } - /// - /// Override this method if you want to validate casts on resource collections. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The collection resource cast node. - /// The validation context. - protected virtual void ValidateCollectionResourceCastNode(CollectionResourceCastNode collectionResourceCastNode, FilterValidatorContext validatorContext) - { - Contract.Assert(collectionResourceCastNode != null); - Contract.Assert(validatorContext != null); + /// + /// Override this method if you want to visit each query node. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The query node. + /// The validation context. + protected virtual void ValidateQueryNode(QueryNode node, FilterValidatorContext validatorContext) + { + Contract.Assert(validatorContext != null); - ValidateQueryNode(collectionResourceCastNode.Source, validatorContext); - } + // Recursion guard to avoid stack overflows + RuntimeHelpers.EnsureSufficientExecutionStack(); - /// - /// Override this method if you want to validate casts on single resource. - /// - /// - /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. - /// Call the Validate method to validate a instance. - /// - /// The single resource cast node. - /// The validation context. - protected virtual void ValidateSingleResourceCastNode(SingleResourceCastNode singleResourceCastNode, FilterValidatorContext validatorContext) - { - Contract.Assert(singleResourceCastNode != null); - Contract.Assert(validatorContext != null); + SingleValueNode singleNode = node as SingleValueNode; + CollectionNode collectionNode = node as CollectionNode; - ValidateQueryNode(singleResourceCastNode.Source, validatorContext); - } + validatorContext.IncrementNodeCount(); - /// - /// The recursive method that validate most of the query node type is of CollectionNode type. - /// - /// The single value node. - /// The validator context. - protected virtual void ValidateCollectionNode(CollectionNode node, FilterValidatorContext validatorContext) + if (singleNode != null) { - switch (node.Kind) - { - case QueryNodeKind.CollectionPropertyAccess: - CollectionPropertyAccessNode propertyAccessNode = node as CollectionPropertyAccessNode; - ValidateCollectionPropertyAccessNode(propertyAccessNode, validatorContext); - break; - - case QueryNodeKind.CollectionComplexNode: - CollectionComplexNode collectionComplexNode = node as CollectionComplexNode; - ValidateCollectionComplexNode(collectionComplexNode, validatorContext); - break; - - case QueryNodeKind.CollectionNavigationNode: - CollectionNavigationNode navigationNode = node as CollectionNavigationNode; - ValidateNavigationPropertyNode(navigationNode.Source, navigationNode.NavigationProperty, validatorContext); - break; - - case QueryNodeKind.CollectionResourceCast: - ValidateCollectionResourceCastNode(node as CollectionResourceCastNode, validatorContext); - break; - - case QueryNodeKind.CollectionFunctionCall: - case QueryNodeKind.CollectionResourceFunctionCall: - case QueryNodeKind.CollectionOpenPropertyAccess: - // Unused or have unknown uses. - default: - throw Error.NotSupported(SRResources.QueryNodeValidationNotSupported, node.Kind, typeof(FilterQueryValidator).Name); - } + ValidateSingleValueNode(singleNode, validatorContext); } - - /// - /// The recursive method that validate most of the query node type is of SingleValueNode type. - /// - /// The single value node. - /// The validator context. - protected virtual void ValidateSingleValueNode(SingleValueNode node, FilterValidatorContext validatorContext) + else if (collectionNode != null) { - switch (node.Kind) - { - case QueryNodeKind.BinaryOperator: - ValidateBinaryOperatorNode(node as BinaryOperatorNode, validatorContext); - break; - - case QueryNodeKind.Constant: - ValidateConstantNode(node as ConstantNode, validatorContext); - break; - - case QueryNodeKind.Convert: - ValidateConvertNode(node as ConvertNode, validatorContext); - break; - - case QueryNodeKind.Count: - ValidateCountNode(node as CountNode, validatorContext); - break; - - case QueryNodeKind.ResourceRangeVariableReference: - ValidateRangeVariable((node as ResourceRangeVariableReferenceNode).RangeVariable, validatorContext); - break; - - case QueryNodeKind.NonResourceRangeVariableReference: - ValidateRangeVariable((node as NonResourceRangeVariableReferenceNode).RangeVariable, validatorContext); - break; - - case QueryNodeKind.SingleValuePropertyAccess: - ValidateSingleValuePropertyAccessNode(node as SingleValuePropertyAccessNode, validatorContext); - break; - - case QueryNodeKind.SingleComplexNode: - ValidateSingleComplexNode(node as SingleComplexNode, validatorContext); - break; - - case QueryNodeKind.UnaryOperator: - ValidateUnaryOperatorNode(node as UnaryOperatorNode, validatorContext); - break; - - case QueryNodeKind.SingleValueFunctionCall: - ValidateSingleValueFunctionCallNode(node as SingleValueFunctionCallNode, validatorContext); - break; - - case QueryNodeKind.SingleResourceFunctionCall: - ValidateSingleResourceFunctionCallNode((SingleResourceFunctionCallNode)node, validatorContext); - break; - - case QueryNodeKind.SingleNavigationNode: - SingleNavigationNode navigationNode = node as SingleNavigationNode; - ValidateNavigationPropertyNode(navigationNode.Source, navigationNode.NavigationProperty, validatorContext); - break; - - case QueryNodeKind.SingleResourceCast: - ValidateSingleResourceCastNode(node as SingleResourceCastNode, validatorContext); - break; - - case QueryNodeKind.Any: - ValidateAnyNode(node as AnyNode, validatorContext); - break; - - case QueryNodeKind.All: - ValidateAllNode(node as AllNode, validatorContext); - break; - - case QueryNodeKind.SingleValueOpenPropertyAccess: - //no validation on open values? - break; - - case QueryNodeKind.In: - // No setting validations - break; - - case QueryNodeKind.NamedFunctionParameter: - case QueryNodeKind.ParameterAlias: - case QueryNodeKind.EntitySet: - case QueryNodeKind.KeyLookup: - case QueryNodeKind.SearchTerm: - // Unused or have unknown uses. - default: - throw Error.NotSupported(SRResources.QueryNodeValidationNotSupported, node.Kind, typeof(FilterQueryValidator).Name); - } + ValidateCollectionNode(collectionNode, validatorContext); } + } - private static void ValidateFunction(string functionName, FilterValidatorContext validatorContext) - { - Contract.Assert(validatorContext != null); + /// + /// Override this method if you want to validate casts on resource collections. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The collection resource cast node. + /// The validation context. + protected virtual void ValidateCollectionResourceCastNode(CollectionResourceCastNode collectionResourceCastNode, FilterValidatorContext validatorContext) + { + Contract.Assert(collectionResourceCastNode != null); + Contract.Assert(validatorContext != null); - AllowedFunctions convertedFunction = ToODataFunction(functionName); - if ((validatorContext.ValidationSettings.AllowedFunctions & convertedFunction) != convertedFunction) - { - // this means the given function is not allowed - throw new ODataException(Error.Format(SRResources.NotAllowedFunction, functionName, "AllowedFunctions")); - } + ValidateQueryNode(collectionResourceCastNode.Source, validatorContext); + } + + /// + /// Override this method if you want to validate casts on single resource. + /// + /// + /// This method is intended to be called from method overrides in subclasses. This method also supports unit-testing scenarios and is not intended to be called from user code. + /// Call the Validate method to validate a instance. + /// + /// The single resource cast node. + /// The validation context. + protected virtual void ValidateSingleResourceCastNode(SingleResourceCastNode singleResourceCastNode, FilterValidatorContext validatorContext) + { + Contract.Assert(singleResourceCastNode != null); + Contract.Assert(validatorContext != null); + + ValidateQueryNode(singleResourceCastNode.Source, validatorContext); + } + + /// + /// The recursive method that validate most of the query node type is of CollectionNode type. + /// + /// The single value node. + /// The validator context. + protected virtual void ValidateCollectionNode(CollectionNode node, FilterValidatorContext validatorContext) + { + switch (node.Kind) + { + case QueryNodeKind.CollectionPropertyAccess: + CollectionPropertyAccessNode propertyAccessNode = node as CollectionPropertyAccessNode; + ValidateCollectionPropertyAccessNode(propertyAccessNode, validatorContext); + break; + + case QueryNodeKind.CollectionComplexNode: + CollectionComplexNode collectionComplexNode = node as CollectionComplexNode; + ValidateCollectionComplexNode(collectionComplexNode, validatorContext); + break; + + case QueryNodeKind.CollectionNavigationNode: + CollectionNavigationNode navigationNode = node as CollectionNavigationNode; + ValidateNavigationPropertyNode(navigationNode.Source, navigationNode.NavigationProperty, validatorContext); + break; + + case QueryNodeKind.CollectionResourceCast: + ValidateCollectionResourceCastNode(node as CollectionResourceCastNode, validatorContext); + break; + + case QueryNodeKind.CollectionFunctionCall: + case QueryNodeKind.CollectionResourceFunctionCall: + case QueryNodeKind.CollectionOpenPropertyAccess: + // Unused or have unknown uses. + default: + throw Error.NotSupported(SRResources.QueryNodeValidationNotSupported, node.Kind, typeof(FilterQueryValidator).Name); } + } - private static AllowedFunctions ToODataFunction(string functionName) + /// + /// The recursive method that validate most of the query node type is of SingleValueNode type. + /// + /// The single value node. + /// The validator context. + protected virtual void ValidateSingleValueNode(SingleValueNode node, FilterValidatorContext validatorContext) + { + switch (node.Kind) { - AllowedFunctions result = AllowedFunctions.None; + case QueryNodeKind.BinaryOperator: + ValidateBinaryOperatorNode(node as BinaryOperatorNode, validatorContext); + break; + + case QueryNodeKind.Constant: + ValidateConstantNode(node as ConstantNode, validatorContext); + break; + + case QueryNodeKind.Convert: + ValidateConvertNode(node as ConvertNode, validatorContext); + break; + + case QueryNodeKind.Count: + ValidateCountNode(node as CountNode, validatorContext); + break; + + case QueryNodeKind.ResourceRangeVariableReference: + ValidateRangeVariable((node as ResourceRangeVariableReferenceNode).RangeVariable, validatorContext); + break; + + case QueryNodeKind.NonResourceRangeVariableReference: + ValidateRangeVariable((node as NonResourceRangeVariableReferenceNode).RangeVariable, validatorContext); + break; + + case QueryNodeKind.SingleValuePropertyAccess: + ValidateSingleValuePropertyAccessNode(node as SingleValuePropertyAccessNode, validatorContext); + break; + + case QueryNodeKind.SingleComplexNode: + ValidateSingleComplexNode(node as SingleComplexNode, validatorContext); + break; + + case QueryNodeKind.UnaryOperator: + ValidateUnaryOperatorNode(node as UnaryOperatorNode, validatorContext); + break; + + case QueryNodeKind.SingleValueFunctionCall: + ValidateSingleValueFunctionCallNode(node as SingleValueFunctionCallNode, validatorContext); + break; + + case QueryNodeKind.SingleResourceFunctionCall: + ValidateSingleResourceFunctionCallNode((SingleResourceFunctionCallNode)node, validatorContext); + break; + + case QueryNodeKind.SingleNavigationNode: + SingleNavigationNode navigationNode = node as SingleNavigationNode; + ValidateNavigationPropertyNode(navigationNode.Source, navigationNode.NavigationProperty, validatorContext); + break; + + case QueryNodeKind.SingleResourceCast: + ValidateSingleResourceCastNode(node as SingleResourceCastNode, validatorContext); + break; + + case QueryNodeKind.Any: + ValidateAnyNode(node as AnyNode, validatorContext); + break; + + case QueryNodeKind.All: + ValidateAllNode(node as AllNode, validatorContext); + break; + + case QueryNodeKind.SingleValueOpenPropertyAccess: + //no validation on open values? + break; + + case QueryNodeKind.In: + // No setting validations + break; + + case QueryNodeKind.NamedFunctionParameter: + case QueryNodeKind.ParameterAlias: + case QueryNodeKind.EntitySet: + case QueryNodeKind.KeyLookup: + case QueryNodeKind.SearchTerm: + // Unused or have unknown uses. + default: + throw Error.NotSupported(SRResources.QueryNodeValidationNotSupported, node.Kind, typeof(FilterQueryValidator).Name); + } + } - switch (functionName) - { - case "any": - result = AllowedFunctions.Any; - break; - case "all": - result = AllowedFunctions.All; - break; - case "cast": - result = AllowedFunctions.Cast; - break; - case ClrCanonicalFunctions.CeilingFunctionName: - result = AllowedFunctions.Ceiling; - break; - case ClrCanonicalFunctions.ConcatFunctionName: - result = AllowedFunctions.Concat; - break; - case ClrCanonicalFunctions.ContainsFunctionName: - result = AllowedFunctions.Contains; - break; - case ClrCanonicalFunctions.DayFunctionName: - result = AllowedFunctions.Day; - break; - case ClrCanonicalFunctions.EndswithFunctionName: - result = AllowedFunctions.EndsWith; - break; - case ClrCanonicalFunctions.FloorFunctionName: - result = AllowedFunctions.Floor; - break; - case ClrCanonicalFunctions.HourFunctionName: - result = AllowedFunctions.Hour; - break; - case ClrCanonicalFunctions.IndexofFunctionName: - result = AllowedFunctions.IndexOf; - break; - case "isof": - result = AllowedFunctions.IsOf; - break; - case ClrCanonicalFunctions.LengthFunctionName: - result = AllowedFunctions.Length; - break; - case ClrCanonicalFunctions.MatchesPatternFunctionName: - result = AllowedFunctions.MatchesPattern; - break; - case ClrCanonicalFunctions.MinuteFunctionName: - result = AllowedFunctions.Minute; - break; - case ClrCanonicalFunctions.MonthFunctionName: - result = AllowedFunctions.Month; - break; - case ClrCanonicalFunctions.RoundFunctionName: - result = AllowedFunctions.Round; - break; - case ClrCanonicalFunctions.SecondFunctionName: - result = AllowedFunctions.Second; - break; - case ClrCanonicalFunctions.StartswithFunctionName: - result = AllowedFunctions.StartsWith; - break; - case ClrCanonicalFunctions.SubstringFunctionName: - result = AllowedFunctions.Substring; - break; - case ClrCanonicalFunctions.TolowerFunctionName: - result = AllowedFunctions.ToLower; - break; - case ClrCanonicalFunctions.ToupperFunctionName: - result = AllowedFunctions.ToUpper; - break; - case ClrCanonicalFunctions.TrimFunctionName: - result = AllowedFunctions.Trim; - break; - case ClrCanonicalFunctions.YearFunctionName: - result = AllowedFunctions.Year; - break; - case ClrCanonicalFunctions.DateFunctionName: - result = AllowedFunctions.Date; - break; - case ClrCanonicalFunctions.TimeFunctionName: - result = AllowedFunctions.Time; - break; - case ClrCanonicalFunctions.FractionalSecondsFunctionName: - result = AllowedFunctions.FractionalSeconds; - break; - default: - // should never be here - Contract.Assert(true, "ToODataFunction should never be here."); - break; - } + private static void ValidateFunction(string functionName, FilterValidatorContext validatorContext) + { + Contract.Assert(validatorContext != null); - return result; + AllowedFunctions convertedFunction = ToODataFunction(functionName); + if ((validatorContext.ValidationSettings.AllowedFunctions & convertedFunction) != convertedFunction) + { + // this means the given function is not allowed + throw new ODataException(Error.Format(SRResources.NotAllowedFunction, functionName, "AllowedFunctions")); } + } + + private static AllowedFunctions ToODataFunction(string functionName) + { + AllowedFunctions result = AllowedFunctions.None; - private static AllowedLogicalOperators ToLogicalOperator(BinaryOperatorNode binaryNode) + switch (functionName) { - AllowedLogicalOperators result = AllowedLogicalOperators.None; + case "any": + result = AllowedFunctions.Any; + break; + case "all": + result = AllowedFunctions.All; + break; + case "cast": + result = AllowedFunctions.Cast; + break; + case ClrCanonicalFunctions.CeilingFunctionName: + result = AllowedFunctions.Ceiling; + break; + case ClrCanonicalFunctions.ConcatFunctionName: + result = AllowedFunctions.Concat; + break; + case ClrCanonicalFunctions.ContainsFunctionName: + result = AllowedFunctions.Contains; + break; + case ClrCanonicalFunctions.DayFunctionName: + result = AllowedFunctions.Day; + break; + case ClrCanonicalFunctions.EndswithFunctionName: + result = AllowedFunctions.EndsWith; + break; + case ClrCanonicalFunctions.FloorFunctionName: + result = AllowedFunctions.Floor; + break; + case ClrCanonicalFunctions.HourFunctionName: + result = AllowedFunctions.Hour; + break; + case ClrCanonicalFunctions.IndexofFunctionName: + result = AllowedFunctions.IndexOf; + break; + case "isof": + result = AllowedFunctions.IsOf; + break; + case ClrCanonicalFunctions.LengthFunctionName: + result = AllowedFunctions.Length; + break; + case ClrCanonicalFunctions.MatchesPatternFunctionName: + result = AllowedFunctions.MatchesPattern; + break; + case ClrCanonicalFunctions.MinuteFunctionName: + result = AllowedFunctions.Minute; + break; + case ClrCanonicalFunctions.MonthFunctionName: + result = AllowedFunctions.Month; + break; + case ClrCanonicalFunctions.RoundFunctionName: + result = AllowedFunctions.Round; + break; + case ClrCanonicalFunctions.SecondFunctionName: + result = AllowedFunctions.Second; + break; + case ClrCanonicalFunctions.StartswithFunctionName: + result = AllowedFunctions.StartsWith; + break; + case ClrCanonicalFunctions.SubstringFunctionName: + result = AllowedFunctions.Substring; + break; + case ClrCanonicalFunctions.TolowerFunctionName: + result = AllowedFunctions.ToLower; + break; + case ClrCanonicalFunctions.ToupperFunctionName: + result = AllowedFunctions.ToUpper; + break; + case ClrCanonicalFunctions.TrimFunctionName: + result = AllowedFunctions.Trim; + break; + case ClrCanonicalFunctions.YearFunctionName: + result = AllowedFunctions.Year; + break; + case ClrCanonicalFunctions.DateFunctionName: + result = AllowedFunctions.Date; + break; + case ClrCanonicalFunctions.TimeFunctionName: + result = AllowedFunctions.Time; + break; + case ClrCanonicalFunctions.FractionalSecondsFunctionName: + result = AllowedFunctions.FractionalSeconds; + break; + default: + // should never be here + Contract.Assert(true, "ToODataFunction should never be here."); + break; + } - switch (binaryNode.OperatorKind) - { - case BinaryOperatorKind.Equal: - result = AllowedLogicalOperators.Equal; - break; - - case BinaryOperatorKind.NotEqual: - result = AllowedLogicalOperators.NotEqual; - break; - - case BinaryOperatorKind.And: - result = AllowedLogicalOperators.And; - break; - - case BinaryOperatorKind.GreaterThan: - result = AllowedLogicalOperators.GreaterThan; - break; - - case BinaryOperatorKind.GreaterThanOrEqual: - result = AllowedLogicalOperators.GreaterThanOrEqual; - break; - - case BinaryOperatorKind.LessThan: - result = AllowedLogicalOperators.LessThan; - break; - - case BinaryOperatorKind.LessThanOrEqual: - result = AllowedLogicalOperators.LessThanOrEqual; - break; - - case BinaryOperatorKind.Or: - result = AllowedLogicalOperators.Or; - break; - - case BinaryOperatorKind.Has: - result = AllowedLogicalOperators.Has; - break; - - default: - // should never be here - Contract.Assert(false, "ToLogicalOperator should never be here."); - break; - } + return result; + } - return result; - } + private static AllowedLogicalOperators ToLogicalOperator(BinaryOperatorNode binaryNode) + { + AllowedLogicalOperators result = AllowedLogicalOperators.None; - private static AllowedArithmeticOperators ToArithmeticOperator(BinaryOperatorNode binaryNode) + switch (binaryNode.OperatorKind) { - AllowedArithmeticOperators result = AllowedArithmeticOperators.None; + case BinaryOperatorKind.Equal: + result = AllowedLogicalOperators.Equal; + break; + + case BinaryOperatorKind.NotEqual: + result = AllowedLogicalOperators.NotEqual; + break; + + case BinaryOperatorKind.And: + result = AllowedLogicalOperators.And; + break; + + case BinaryOperatorKind.GreaterThan: + result = AllowedLogicalOperators.GreaterThan; + break; + + case BinaryOperatorKind.GreaterThanOrEqual: + result = AllowedLogicalOperators.GreaterThanOrEqual; + break; + + case BinaryOperatorKind.LessThan: + result = AllowedLogicalOperators.LessThan; + break; + + case BinaryOperatorKind.LessThanOrEqual: + result = AllowedLogicalOperators.LessThanOrEqual; + break; + + case BinaryOperatorKind.Or: + result = AllowedLogicalOperators.Or; + break; + + case BinaryOperatorKind.Has: + result = AllowedLogicalOperators.Has; + break; + + default: + // should never be here + Contract.Assert(false, "ToLogicalOperator should never be here."); + break; + } - switch (binaryNode.OperatorKind) - { - case BinaryOperatorKind.Add: - result = AllowedArithmeticOperators.Add; - break; - - case BinaryOperatorKind.Divide: - result = AllowedArithmeticOperators.Divide; - break; - - case BinaryOperatorKind.Modulo: - result = AllowedArithmeticOperators.Modulo; - break; - - case BinaryOperatorKind.Multiply: - result = AllowedArithmeticOperators.Multiply; - break; - - case BinaryOperatorKind.Subtract: - result = AllowedArithmeticOperators.Subtract; - break; - - default: - // should never be here - Contract.Assert(false, "ToArithmeticOperator should never be here."); - break; - } + return result; + } - return result; + private static AllowedArithmeticOperators ToArithmeticOperator(BinaryOperatorNode binaryNode) + { + AllowedArithmeticOperators result = AllowedArithmeticOperators.None; + + switch (binaryNode.OperatorKind) + { + case BinaryOperatorKind.Add: + result = AllowedArithmeticOperators.Add; + break; + + case BinaryOperatorKind.Divide: + result = AllowedArithmeticOperators.Divide; + break; + + case BinaryOperatorKind.Modulo: + result = AllowedArithmeticOperators.Modulo; + break; + + case BinaryOperatorKind.Multiply: + result = AllowedArithmeticOperators.Multiply; + break; + + case BinaryOperatorKind.Subtract: + result = AllowedArithmeticOperators.Subtract; + break; + + default: + // should never be here + Contract.Assert(false, "ToArithmeticOperator should never be here."); + break; } + + return result; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/FilterValidatorContext.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/FilterValidatorContext.cs index f6e271689..4a46e5373 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/FilterValidatorContext.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/FilterValidatorContext.cs @@ -8,86 +8,85 @@ using System.Diagnostics.Contracts; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// The metadata context for $filter validator. +/// +public class FilterValidatorContext : QueryValidatorContext { + private int _currentNodeCount = 0; + private int _currentAnyAllExpressionDepth = 0; + /// - /// The metadata context for $filter validator. + /// The top level $filter query option. /// - public class FilterValidatorContext : QueryValidatorContext - { - private int _currentNodeCount = 0; - private int _currentAnyAllExpressionDepth = 0; - - /// - /// The top level $filter query option. - /// - public FilterQueryOption Filter { get; set; } + public FilterQueryOption Filter { get; set; } - /// - /// Gets current node count. - /// - public int CurrentNodeCount => _currentNodeCount; + /// + /// Gets current node count. + /// + public int CurrentNodeCount => _currentNodeCount; - /// - /// Gets current any and all expression depth. - /// - public int CurrentAnyAllExpressionDepth => _currentAnyAllExpressionDepth; + /// + /// Gets current any and all expression depth. + /// + public int CurrentAnyAllExpressionDepth => _currentAnyAllExpressionDepth; - /// - /// Clone the context. - /// - /// The cloned context. - public FilterValidatorContext Clone() + /// + /// Clone the context. + /// + /// The cloned context. + public FilterValidatorContext Clone() + { + return new FilterValidatorContext { - return new FilterValidatorContext - { - Filter = this.Filter, - Context = this.Context, - ValidationSettings = this.ValidationSettings, - Property = this.Property, - StructuredType = this.StructuredType, - CurrentDepth = this.CurrentDepth, - _currentNodeCount = this._currentNodeCount, - _currentAnyAllExpressionDepth = this._currentAnyAllExpressionDepth - }; - } + Filter = this.Filter, + Context = this.Context, + ValidationSettings = this.ValidationSettings, + Property = this.Property, + StructuredType = this.StructuredType, + CurrentDepth = this.CurrentDepth, + _currentNodeCount = this._currentNodeCount, + _currentAnyAllExpressionDepth = this._currentAnyAllExpressionDepth + }; + } - /// - /// Increment node count. - /// - /// Throw OData exception. - public void IncrementNodeCount() + /// + /// Increment node count. + /// + /// Throw OData exception. + public void IncrementNodeCount() + { + if (_currentNodeCount >= ValidationSettings.MaxNodeCount) { - if (_currentNodeCount >= ValidationSettings.MaxNodeCount) - { - throw new ODataException(Error.Format(SRResources.MaxNodeLimitExceeded, ValidationSettings.MaxNodeCount, "MaxNodeCount")); - } - - _currentNodeCount++; + throw new ODataException(Error.Format(SRResources.MaxNodeLimitExceeded, ValidationSettings.MaxNodeCount, "MaxNodeCount")); } - /// - /// Enter lambda expression. - /// - /// Throw OData exception. - public void EnterLambda() - { - if (_currentAnyAllExpressionDepth >= ValidationSettings.MaxAnyAllExpressionDepth) - { - throw new ODataException( - Error.Format(SRResources.MaxAnyAllExpressionLimitExceeded, ValidationSettings.MaxAnyAllExpressionDepth, "MaxAnyAllExpressionDepth")); - } - - _currentAnyAllExpressionDepth++; - } + _currentNodeCount++; + } - /// - /// Exit lambda expression. - /// - public void ExitLambda() + /// + /// Enter lambda expression. + /// + /// Throw OData exception. + public void EnterLambda() + { + if (_currentAnyAllExpressionDepth >= ValidationSettings.MaxAnyAllExpressionDepth) { - Contract.Assert(_currentAnyAllExpressionDepth > 0); - _currentAnyAllExpressionDepth--; + throw new ODataException( + Error.Format(SRResources.MaxAnyAllExpressionLimitExceeded, ValidationSettings.MaxAnyAllExpressionDepth, "MaxAnyAllExpressionDepth")); } + + _currentAnyAllExpressionDepth++; + } + + /// + /// Exit lambda expression. + /// + public void ExitLambda() + { + Contract.Assert(_currentAnyAllExpressionDepth > 0); + _currentAnyAllExpressionDepth--; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IComputeQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IComputeQueryValidator.cs index 03c540ca9..72c300ae8 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IComputeQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IComputeQueryValidator.cs @@ -5,19 +5,18 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Provide the interface used to validate a +/// based on the . +/// +public interface IComputeQueryValidator { /// - /// Provide the interface used to validate a - /// based on the . + /// Validates the OData query. /// - public interface IComputeQueryValidator - { - /// - /// Validates the OData query. - /// - /// The $compute query. - /// The validation settings. - void Validate(ComputeQueryOption computeQueryOption, ODataValidationSettings validationSettings); - } + /// The $compute query. + /// The validation settings. + void Validate(ComputeQueryOption computeQueryOption, ODataValidationSettings validationSettings); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ICountQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ICountQueryValidator.cs index 15c497035..7673b2004 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ICountQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ICountQueryValidator.cs @@ -5,19 +5,18 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Provide the interface used to validate a +/// based on the . +/// +public interface ICountQueryValidator { /// - /// Provide the interface used to validate a - /// based on the . + /// Validates the OData query. /// - public interface ICountQueryValidator - { - /// - /// Validates the OData query. - /// - /// The $count query. - /// The validation settings. - void Validate(CountQueryOption countQueryOption, ODataValidationSettings validationSettings); - } + /// The $count query. + /// The validation settings. + void Validate(CountQueryOption countQueryOption, ODataValidationSettings validationSettings); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IFilterQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IFilterQueryValidator.cs index f0598b930..f3152730d 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IFilterQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IFilterQueryValidator.cs @@ -5,19 +5,18 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Provide the interface used to validate a +/// based on the . +/// +public interface IFilterQueryValidator { /// - /// Provide the interface used to validate a - /// based on the . + /// Validates the OData query. /// - public interface IFilterQueryValidator - { - /// - /// Validates the OData query. - /// - /// The $filter query. - /// The validation settings. - void Validate(FilterQueryOption filterQueryOption, ODataValidationSettings validationSettings); - } + /// The $filter query. + /// The validation settings. + void Validate(FilterQueryOption filterQueryOption, ODataValidationSettings validationSettings); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IODataQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IODataQueryValidator.cs index 69cc326e8..455f5171c 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IODataQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IODataQueryValidator.cs @@ -5,19 +5,18 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Provide the interface used to validate a +/// based on the . +/// +public interface IODataQueryValidator { /// - /// Provide the interface used to validate a - /// based on the . + /// Validates the OData query. /// - public interface IODataQueryValidator - { - /// - /// Validates the OData query. - /// - /// The OData query options to validate. - /// The validation settings. - void Validate(ODataQueryOptions options, ODataValidationSettings validationSettings); - } + /// The OData query options to validate. + /// The validation settings. + void Validate(ODataQueryOptions options, ODataValidationSettings validationSettings); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IOrderByQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IOrderByQueryValidator.cs index 36003c1c3..8703ed242 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IOrderByQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/IOrderByQueryValidator.cs @@ -5,19 +5,18 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Provide the interface used to validate a +/// based on the . +/// +public interface IOrderByQueryValidator { /// - /// Provide the interface used to validate a - /// based on the . + /// Validates a . /// - public interface IOrderByQueryValidator - { - /// - /// Validates a . - /// - /// The $orderby query. - /// The validation settings. - void Validate(OrderByQueryOption orderByOption, ODataValidationSettings validationSettings); - } + /// The $orderby query. + /// The validation settings. + void Validate(OrderByQueryOption orderByOption, ODataValidationSettings validationSettings); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ISelectExpandQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ISelectExpandQueryValidator.cs index abd877fe9..66e8ee8d8 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ISelectExpandQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ISelectExpandQueryValidator.cs @@ -5,19 +5,18 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Provide the interface used to validate a +/// based on the . +/// +public interface ISelectExpandQueryValidator { /// - /// Provide the interface used to validate a - /// based on the . + /// Validates the OData query. /// - public interface ISelectExpandQueryValidator - { - /// - /// Validates the OData query. - /// - /// The $select and $expand query. - /// The validation settings. - void Validate(SelectExpandQueryOption selectExpandQueryOption, ODataValidationSettings validationSettings); - } + /// The $select and $expand query. + /// The validation settings. + void Validate(SelectExpandQueryOption selectExpandQueryOption, ODataValidationSettings validationSettings); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ISkipQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ISkipQueryValidator.cs index 3261a924b..124c2de25 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ISkipQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ISkipQueryValidator.cs @@ -5,19 +5,18 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Provide the interface used to validate a +/// based on the . +/// +public interface ISkipQueryValidator { /// - /// Provide the interface used to validate a - /// based on the . + /// Validates a . /// - public interface ISkipQueryValidator - { - /// - /// Validates a . - /// - /// The $skip query. - /// The validation settings. - void Validate(SkipQueryOption skipQueryOption, ODataValidationSettings validationSettings); - } + /// The $skip query. + /// The validation settings. + void Validate(SkipQueryOption skipQueryOption, ODataValidationSettings validationSettings); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ISkipTokenQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ISkipTokenQueryValidator.cs index ec4a34303..1f6d9b5ad 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ISkipTokenQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ISkipTokenQueryValidator.cs @@ -5,19 +5,18 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Provide the interface used to validate a +/// based on the . +/// +public interface ISkipTokenQueryValidator { /// - /// Provide the interface used to validate a - /// based on the . + /// Validates a . /// - public interface ISkipTokenQueryValidator - { - /// - /// Validates a . - /// - /// The $skiptoken query. - /// The validation settings. - void Validate(SkipTokenQueryOption skipToken, ODataValidationSettings validationSettings); - } + /// The $skiptoken query. + /// The validation settings. + void Validate(SkipTokenQueryOption skipToken, ODataValidationSettings validationSettings); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ITopQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ITopQueryValidator.cs index fe8a088ce..189361e41 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ITopQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/Interfaces/ITopQueryValidator.cs @@ -5,19 +5,18 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Provide the interface used to validate a +/// based on the . +/// +public interface ITopQueryValidator { /// - /// Provide the interface used to validate a - /// based on the . + /// Validates a . /// - public interface ITopQueryValidator - { - /// - /// Validates a . - /// - /// The $top query. - /// The validation settings. - void Validate(TopQueryOption topQueryOption, ODataValidationSettings validationSettings); - } + /// The $top query. + /// The validation settings. + void Validate(TopQueryOption topQueryOption, ODataValidationSettings validationSettings); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/ODataQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/ODataQueryValidator.cs index 81ca329fa..9c911cb96 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/ODataQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/ODataQueryValidator.cs @@ -8,134 +8,133 @@ using Microsoft.AspNetCore.OData.Extensions; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Represents a validator used to validate OData queries based on the . +/// +public class ODataQueryValidator : IODataQueryValidator { /// - /// Represents a validator used to validate OData queries based on the . + /// Validates the OData query. /// - public class ODataQueryValidator : IODataQueryValidator + /// The OData query to validate. + /// The validation settings. + public virtual void Validate(ODataQueryOptions options, ODataValidationSettings validationSettings) { - /// - /// Validates the OData query. - /// - /// The OData query to validate. - /// The validation settings. - public virtual void Validate(ODataQueryOptions options, ODataValidationSettings validationSettings) - { - if (options == null) - { - throw Error.ArgumentNull("options"); - } - - if (validationSettings == null) - { - throw Error.ArgumentNull("validationSettings"); - } + if (options == null) + { + throw Error.ArgumentNull("options"); + } - // Validate each query options - if (options.Compute != null) - { - ValidateQueryOptionAllowed(AllowedQueryOptions.Compute, validationSettings.AllowedQueryOptions); - options.Compute.Validate(validationSettings); - } + if (validationSettings == null) + { + throw Error.ArgumentNull("validationSettings"); + } - if (options.Apply?.ApplyClause != null) - { - ValidateQueryOptionAllowed(AllowedQueryOptions.Apply, validationSettings.AllowedQueryOptions); - } + // Validate each query options + if (options.Compute != null) + { + ValidateQueryOptionAllowed(AllowedQueryOptions.Compute, validationSettings.AllowedQueryOptions); + options.Compute.Validate(validationSettings); + } - if (options.Skip != null) - { - ValidateQueryOptionAllowed(AllowedQueryOptions.Skip, validationSettings.AllowedQueryOptions); - options.Skip.Validate(validationSettings); - } + if (options.Apply?.ApplyClause != null) + { + ValidateQueryOptionAllowed(AllowedQueryOptions.Apply, validationSettings.AllowedQueryOptions); + } - if (options.Top != null) - { - ValidateQueryOptionAllowed(AllowedQueryOptions.Top, validationSettings.AllowedQueryOptions); - options.Top.Validate(validationSettings); - } + if (options.Skip != null) + { + ValidateQueryOptionAllowed(AllowedQueryOptions.Skip, validationSettings.AllowedQueryOptions); + options.Skip.Validate(validationSettings); + } - if (options.OrderBy != null) - { - ValidateQueryOptionAllowed(AllowedQueryOptions.OrderBy, validationSettings.AllowedQueryOptions); - options.OrderBy.Validate(validationSettings); - } + if (options.Top != null) + { + ValidateQueryOptionAllowed(AllowedQueryOptions.Top, validationSettings.AllowedQueryOptions); + options.Top.Validate(validationSettings); + } - if (options.Filter != null) - { - ValidateQueryOptionAllowed(AllowedQueryOptions.Filter, validationSettings.AllowedQueryOptions); - options.Filter.Validate(validationSettings); - } + if (options.OrderBy != null) + { + ValidateQueryOptionAllowed(AllowedQueryOptions.OrderBy, validationSettings.AllowedQueryOptions); + options.OrderBy.Validate(validationSettings); + } - if (options.Search != null) - { - ValidateQueryOptionAllowed(AllowedQueryOptions.Search, validationSettings.AllowedQueryOptions); - } + if (options.Filter != null) + { + ValidateQueryOptionAllowed(AllowedQueryOptions.Filter, validationSettings.AllowedQueryOptions); + options.Filter.Validate(validationSettings); + } - if (options.Count != null || options.Request.IsCountRequest()) - { - ValidateQueryOptionAllowed(AllowedQueryOptions.Count, validationSettings.AllowedQueryOptions); + if (options.Search != null) + { + ValidateQueryOptionAllowed(AllowedQueryOptions.Search, validationSettings.AllowedQueryOptions); + } - if (options.Count != null) - { - options.Count.Validate(validationSettings); - } - } + if (options.Count != null || options.Request.IsCountRequest()) + { + ValidateQueryOptionAllowed(AllowedQueryOptions.Count, validationSettings.AllowedQueryOptions); - if (options.SkipToken != null) + if (options.Count != null) { - ValidateQueryOptionAllowed(AllowedQueryOptions.SkipToken, validationSettings.AllowedQueryOptions); - options.SkipToken.Validate(validationSettings); + options.Count.Validate(validationSettings); } + } - if (options.RawValues.Expand != null) - { - ValidateNotEmptyOrWhitespace(options.RawValues.Expand); - ValidateQueryOptionAllowed(AllowedQueryOptions.Expand, validationSettings.AllowedQueryOptions); - } + if (options.SkipToken != null) + { + ValidateQueryOptionAllowed(AllowedQueryOptions.SkipToken, validationSettings.AllowedQueryOptions); + options.SkipToken.Validate(validationSettings); + } - if (options.RawValues.Select != null) - { - ValidateNotEmptyOrWhitespace(options.RawValues.Select); - ValidateQueryOptionAllowed(AllowedQueryOptions.Select, validationSettings.AllowedQueryOptions); - } + if (options.RawValues.Expand != null) + { + ValidateNotEmptyOrWhitespace(options.RawValues.Expand); + ValidateQueryOptionAllowed(AllowedQueryOptions.Expand, validationSettings.AllowedQueryOptions); + } - if (options.SelectExpand != null) - { - options.SelectExpand.Validate(validationSettings); - } + if (options.RawValues.Select != null) + { + ValidateNotEmptyOrWhitespace(options.RawValues.Select); + ValidateQueryOptionAllowed(AllowedQueryOptions.Select, validationSettings.AllowedQueryOptions); + } - if (options.RawValues.Format != null) - { - ValidateQueryOptionAllowed(AllowedQueryOptions.Format, validationSettings.AllowedQueryOptions); - } + if (options.SelectExpand != null) + { + options.SelectExpand.Validate(validationSettings); + } - if (options.RawValues.SkipToken != null) - { - ValidateQueryOptionAllowed(AllowedQueryOptions.SkipToken, validationSettings.AllowedQueryOptions); - } + if (options.RawValues.Format != null) + { + ValidateQueryOptionAllowed(AllowedQueryOptions.Format, validationSettings.AllowedQueryOptions); + } - if (options.RawValues.DeltaToken != null) - { - ValidateQueryOptionAllowed(AllowedQueryOptions.DeltaToken, validationSettings.AllowedQueryOptions); - } + if (options.RawValues.SkipToken != null) + { + ValidateQueryOptionAllowed(AllowedQueryOptions.SkipToken, validationSettings.AllowedQueryOptions); } - private static void ValidateQueryOptionAllowed(AllowedQueryOptions queryOption, AllowedQueryOptions allowed) + if (options.RawValues.DeltaToken != null) { - if ((queryOption & allowed) == AllowedQueryOptions.None) - { - throw new ODataException(Error.Format(SRResources.NotAllowedQueryOption, queryOption, "AllowedQueryOptions")); - } + ValidateQueryOptionAllowed(AllowedQueryOptions.DeltaToken, validationSettings.AllowedQueryOptions); } + } + + private static void ValidateQueryOptionAllowed(AllowedQueryOptions queryOption, AllowedQueryOptions allowed) + { + if ((queryOption & allowed) == AllowedQueryOptions.None) + { + throw new ODataException(Error.Format(SRResources.NotAllowedQueryOption, queryOption, "AllowedQueryOptions")); + } + } - private static void ValidateNotEmptyOrWhitespace(string rawValue) + private static void ValidateNotEmptyOrWhitespace(string rawValue) + { + if (rawValue != null && string.IsNullOrWhiteSpace(rawValue)) { - if (rawValue != null && string.IsNullOrWhiteSpace(rawValue)) - { - throw new ODataException(SRResources.SelectExpandEmptyOrWhitespace); - } + throw new ODataException(SRResources.SelectExpandEmptyOrWhitespace); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/ODataValidationSettings.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/ODataValidationSettings.cs index d66200817..57846156b 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/ODataValidationSettings.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/ODataValidationSettings.cs @@ -8,238 +8,237 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// This class describes the validation settings for querying. +/// +public class ODataValidationSettings { + private const int MinMaxSkip = 0; + private const int MinMaxTop = 0; + private const int MinMaxExpansionDepth = 0; + private const int MinMaxNodeCount = 1; + private const int MinMaxAnyAllExpressionDepth = 1; + private const int MinMaxOrderByNodeCount = 1; + internal const int DefaultMaxExpansionDepth = 2; + + private AllowedArithmeticOperators _allowedArithmeticOperators = AllowedArithmeticOperators.All; + private AllowedFunctions _allowedFunctions = AllowedFunctions.AllFunctions; + private AllowedLogicalOperators _allowedLogicalOperators = AllowedLogicalOperators.All; + private AllowedQueryOptions _allowedQueryParameters = AllowedQueryOptions.Supported; + private ISet _allowedOrderByProperties = new HashSet(); + private int? _maxSkip; + private int? _maxTop; + private int _maxAnyAllExpressionDepth = 1; + private int _maxNodeCount = 100; + private int _maxExpansionDepth = DefaultMaxExpansionDepth; + private int _maxOrderByNodeCount = 5; + /// - /// This class describes the validation settings for querying. + /// Gets or sets a list of allowed arithmetic operators including 'add', 'sub', 'mul', 'div', 'mod'. /// - public class ODataValidationSettings + public AllowedArithmeticOperators AllowedArithmeticOperators { - private const int MinMaxSkip = 0; - private const int MinMaxTop = 0; - private const int MinMaxExpansionDepth = 0; - private const int MinMaxNodeCount = 1; - private const int MinMaxAnyAllExpressionDepth = 1; - private const int MinMaxOrderByNodeCount = 1; - internal const int DefaultMaxExpansionDepth = 2; - - private AllowedArithmeticOperators _allowedArithmeticOperators = AllowedArithmeticOperators.All; - private AllowedFunctions _allowedFunctions = AllowedFunctions.AllFunctions; - private AllowedLogicalOperators _allowedLogicalOperators = AllowedLogicalOperators.All; - private AllowedQueryOptions _allowedQueryParameters = AllowedQueryOptions.Supported; - private ISet _allowedOrderByProperties = new HashSet(); - private int? _maxSkip; - private int? _maxTop; - private int _maxAnyAllExpressionDepth = 1; - private int _maxNodeCount = 100; - private int _maxExpansionDepth = DefaultMaxExpansionDepth; - private int _maxOrderByNodeCount = 5; - - /// - /// Gets or sets a list of allowed arithmetic operators including 'add', 'sub', 'mul', 'div', 'mod'. - /// - public AllowedArithmeticOperators AllowedArithmeticOperators + get => _allowedArithmeticOperators; + set { - get => _allowedArithmeticOperators; - set + if (value > AllowedArithmeticOperators.All || value < AllowedArithmeticOperators.None) { - if (value > AllowedArithmeticOperators.All || value < AllowedArithmeticOperators.None) - { - throw Error.InvalidEnumArgument("value", (Int32)value, typeof(AllowedArithmeticOperators)); - } - - _allowedArithmeticOperators = value; + throw Error.InvalidEnumArgument("value", (Int32)value, typeof(AllowedArithmeticOperators)); } + + _allowedArithmeticOperators = value; } + } - /// - /// Gets or sets a list of allowed functions used in the $filter query. - /// - /// The allowed functions include the following: - /// - /// String related: contains, endswith, startswith, length, indexof, substring, tolower, toupper, trim, concat, matchesPattern - /// - /// e.g. ~/Customers?$filter=length(CompanyName) eq 19 - /// - /// Date and Time related: year, month, day, hour, minute, second, fractionalseconds, date, time - /// - /// e.g. ~/Employees?$filter=year(BirthDate) eq 1971 - /// - /// Math related: round, floor, ceiling - /// - /// Type related:isof, cast, - /// - /// Collection related: any, all - /// - /// - public AllowedFunctions AllowedFunctions + /// + /// Gets or sets a list of allowed functions used in the $filter query. + /// + /// The allowed functions include the following: + /// + /// String related: contains, endswith, startswith, length, indexof, substring, tolower, toupper, trim, concat, matchesPattern + /// + /// e.g. ~/Customers?$filter=length(CompanyName) eq 19 + /// + /// Date and Time related: year, month, day, hour, minute, second, fractionalseconds, date, time + /// + /// e.g. ~/Employees?$filter=year(BirthDate) eq 1971 + /// + /// Math related: round, floor, ceiling + /// + /// Type related:isof, cast, + /// + /// Collection related: any, all + /// + /// + public AllowedFunctions AllowedFunctions + { + get => _allowedFunctions; + set { - get => _allowedFunctions; - set + if (value > AllowedFunctions.AllFunctions || value < AllowedFunctions.None) { - if (value > AllowedFunctions.AllFunctions || value < AllowedFunctions.None) - { - throw Error.InvalidEnumArgument("value", (Int32)value, typeof(AllowedFunctions)); - } - - _allowedFunctions = value; + throw Error.InvalidEnumArgument("value", (Int32)value, typeof(AllowedFunctions)); } + + _allowedFunctions = value; } + } - /// - /// Gets or sets a list of allowed logical operators such as 'eq', 'ne', 'gt', 'ge', 'lt', 'le', 'and', 'or', 'not'. - /// - public AllowedLogicalOperators AllowedLogicalOperators + /// + /// Gets or sets a list of allowed logical operators such as 'eq', 'ne', 'gt', 'ge', 'lt', 'le', 'and', 'or', 'not'. + /// + public AllowedLogicalOperators AllowedLogicalOperators + { + get => _allowedLogicalOperators; + set { - get => _allowedLogicalOperators; - set + if (value > AllowedLogicalOperators.All || value < AllowedLogicalOperators.None) { - if (value > AllowedLogicalOperators.All || value < AllowedLogicalOperators.None) - { - throw Error.InvalidEnumArgument("value", (Int32)value, typeof(AllowedLogicalOperators)); - } - - _allowedLogicalOperators = value; + throw Error.InvalidEnumArgument("value", (Int32)value, typeof(AllowedLogicalOperators)); } + + _allowedLogicalOperators = value; } + } + + /// + /// Gets a list of properties one can orderby the result with. Note, by default this list is empty, + /// it means it can be ordered by any property. + /// + /// For example, having an empty collection means client can order the queryable result by any properties. + /// Adding "Name" to this list means that it only allows queryable result to be ordered by Name property. + /// + public ISet AllowedOrderByProperties { get => _allowedOrderByProperties; } - /// - /// Gets a list of properties one can orderby the result with. Note, by default this list is empty, - /// it means it can be ordered by any property. - /// - /// For example, having an empty collection means client can order the queryable result by any properties. - /// Adding "Name" to this list means that it only allows queryable result to be ordered by Name property. - /// - public ISet AllowedOrderByProperties { get => _allowedOrderByProperties; } - - /// - /// Gets or sets the query parameters that are allowed inside query. The default is all query options, - /// including $filter, $skip, $top, $orderby, $expand, $select, $count, $format, $skiptoken and $deltatoken. - /// - public AllowedQueryOptions AllowedQueryOptions + /// + /// Gets or sets the query parameters that are allowed inside query. The default is all query options, + /// including $filter, $skip, $top, $orderby, $expand, $select, $count, $format, $skiptoken and $deltatoken. + /// + public AllowedQueryOptions AllowedQueryOptions + { + get => _allowedQueryParameters; + set { - get => _allowedQueryParameters; - set + if (value > AllowedQueryOptions.All || value < AllowedQueryOptions.None) { - if (value > AllowedQueryOptions.All || value < AllowedQueryOptions.None) - { - throw Error.InvalidEnumArgument("value", (Int32)value, typeof(AllowedQueryOptions)); - } - - _allowedQueryParameters = value; + throw Error.InvalidEnumArgument("value", (Int32)value, typeof(AllowedQueryOptions)); } + + _allowedQueryParameters = value; } + } - /// - /// Gets or sets the maximum number of expressions that can be present in the $orderby. - /// - public int MaxOrderByNodeCount + /// + /// Gets or sets the maximum number of expressions that can be present in the $orderby. + /// + public int MaxOrderByNodeCount + { + get => _maxOrderByNodeCount; + set { - get => _maxOrderByNodeCount; - set + if (value < MinMaxOrderByNodeCount) { - if (value < MinMaxOrderByNodeCount) - { - throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinMaxOrderByNodeCount); - } - - _maxOrderByNodeCount = value; + throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinMaxOrderByNodeCount); } + + _maxOrderByNodeCount = value; } + } - /// - /// Gets or sets the maximum depth of the Any or All elements nested inside the query. - /// - /// - /// The default value is 1. - /// - /// - /// The maximum depth of the Any or All elements nested inside the query. - /// - public int MaxAnyAllExpressionDepth + /// + /// Gets or sets the maximum depth of the Any or All elements nested inside the query. + /// + /// + /// The default value is 1. + /// + /// + /// The maximum depth of the Any or All elements nested inside the query. + /// + public int MaxAnyAllExpressionDepth + { + get => _maxAnyAllExpressionDepth; + set { - get => _maxAnyAllExpressionDepth; - set + if (value < MinMaxAnyAllExpressionDepth) { - if (value < MinMaxAnyAllExpressionDepth) - { - throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinMaxAnyAllExpressionDepth); - } - - _maxAnyAllExpressionDepth = value; + throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinMaxAnyAllExpressionDepth); } + + _maxAnyAllExpressionDepth = value; } + } - /// - /// Gets or sets the maximum number of the nodes inside the $filter syntax tree. - /// - /// - /// The default value is 100. - /// - public int MaxNodeCount + /// + /// Gets or sets the maximum number of the nodes inside the $filter syntax tree. + /// + /// + /// The default value is 100. + /// + public int MaxNodeCount + { + get => _maxNodeCount; + set { - get => _maxNodeCount; - set + if (value < MinMaxNodeCount) { - if (value < MinMaxNodeCount) - { - throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinMaxNodeCount); - } - - _maxNodeCount = value; + throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinMaxNodeCount); } + + _maxNodeCount = value; } + } - /// - /// Gets or sets the max value of $skip that a client can request. - /// - public int? MaxSkip + /// + /// Gets or sets the max value of $skip that a client can request. + /// + public int? MaxSkip + { + get => _maxSkip; + set { - get => _maxSkip; - set + if (value.HasValue && value < MinMaxSkip) { - if (value.HasValue && value < MinMaxSkip) - { - throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinMaxSkip); - } - - _maxSkip = value; + throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinMaxSkip); } + + _maxSkip = value; } + } - /// - /// Gets or sets the max value of $top that a client can request. - /// - public int? MaxTop + /// + /// Gets or sets the max value of $top that a client can request. + /// + public int? MaxTop + { + get => _maxTop; + set { - get => _maxTop; - set + if (value.HasValue && value < MinMaxTop) { - if (value.HasValue && value < MinMaxTop) - { - throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinMaxTop); - } - - _maxTop = value; + throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinMaxTop); } + + _maxTop = value; } + } - /// - /// Gets or sets the max expansion depth for the $expand query option. - /// - /// To disable the maximum expansion depth check, set this property to 0. - public int MaxExpansionDepth + /// + /// Gets or sets the max expansion depth for the $expand query option. + /// + /// To disable the maximum expansion depth check, set this property to 0. + public int MaxExpansionDepth + { + get => _maxExpansionDepth; + set { - get => _maxExpansionDepth; - set + if (value < MinMaxExpansionDepth) { - if (value < MinMaxExpansionDepth) - { - throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinMaxExpansionDepth); - } - - _maxExpansionDepth = value; + throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinMaxExpansionDepth); } + + _maxExpansionDepth = value; } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/OrderByModelLimitationsValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/OrderByModelLimitationsValidator.cs index 0c3912461..8ecfc491b 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/OrderByModelLimitationsValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/OrderByModelLimitationsValidator.cs @@ -10,161 +10,160 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +internal class OrderByModelLimitationsValidator : QueryNodeVisitor { - internal class OrderByModelLimitationsValidator : QueryNodeVisitor + private readonly IEdmModel _model; + private readonly bool _enableOrderBy; + private IEdmProperty _property; + private IEdmStructuredType _structuredType; + + public OrderByModelLimitationsValidator(ODataQueryContext context, bool enableOrderBy) { - private readonly IEdmModel _model; - private readonly bool _enableOrderBy; - private IEdmProperty _property; - private IEdmStructuredType _structuredType; + _model = context.Model; + _enableOrderBy = enableOrderBy; - public OrderByModelLimitationsValidator(ODataQueryContext context, bool enableOrderBy) + if (context.Path != null) { - _model = context.Model; - _enableOrderBy = enableOrderBy; - - if (context.Path != null) - { - _property = context.TargetProperty; - _structuredType = context.TargetStructuredType; - } + _property = context.TargetProperty; + _structuredType = context.TargetStructuredType; } + } - public bool TryValidate(IEdmProperty property, IEdmStructuredType structuredType, OrderByClause orderByClause, - bool explicitPropertiesDefined) - { - _property = property; - _structuredType = structuredType; - return TryValidate(orderByClause, explicitPropertiesDefined); - } + public bool TryValidate(IEdmProperty property, IEdmStructuredType structuredType, OrderByClause orderByClause, + bool explicitPropertiesDefined) + { + _property = property; + _structuredType = structuredType; + return TryValidate(orderByClause, explicitPropertiesDefined); + } - // Visits the expression to find the first node if any, that is not sortable and throws - // an exception only if no explicit properties have been defined in AllowedOrderByProperties - // on the ODataValidationSettings instance associated with this OrderByValidator. - public bool TryValidate(OrderByClause orderByClause, bool explicitPropertiesDefined) + // Visits the expression to find the first node if any, that is not sortable and throws + // an exception only if no explicit properties have been defined in AllowedOrderByProperties + // on the ODataValidationSettings instance associated with this OrderByValidator. + public bool TryValidate(OrderByClause orderByClause, bool explicitPropertiesDefined) + { + SingleValueNode invalidNode = orderByClause.Expression.Accept(this); + if (invalidNode != null && !explicitPropertiesDefined) { - SingleValueNode invalidNode = orderByClause.Expression.Accept(this); - if (invalidNode != null && !explicitPropertiesDefined) - { - throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, - GetPropertyName(invalidNode))); - } - return invalidNode == null; + throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, + GetPropertyName(invalidNode))); } + return invalidNode == null; + } - public override SingleValueNode Visit(SingleValuePropertyAccessNode nodeIn) + public override SingleValueNode Visit(SingleValuePropertyAccessNode nodeIn) + { + if (nodeIn.Source != null) { - if (nodeIn.Source != null) + if (nodeIn.Source.Kind == QueryNodeKind.SingleNavigationNode) { - if (nodeIn.Source.Kind == QueryNodeKind.SingleNavigationNode) - { - SingleNavigationNode singleNavigationNode = nodeIn.Source as SingleNavigationNode; - if (EdmHelpers.IsNotSortable(nodeIn.Property, singleNavigationNode.NavigationProperty, - singleNavigationNode.NavigationProperty.ToEntityType(), _model, _enableOrderBy)) - { - return nodeIn; - } - } - else if (nodeIn.Source.Kind == QueryNodeKind.SingleComplexNode) + SingleNavigationNode singleNavigationNode = nodeIn.Source as SingleNavigationNode; + if (EdmHelpers.IsNotSortable(nodeIn.Property, singleNavigationNode.NavigationProperty, + singleNavigationNode.NavigationProperty.ToEntityType(), _model, _enableOrderBy)) { - SingleComplexNode singleComplexNode = nodeIn.Source as SingleComplexNode; - if (EdmHelpers.IsNotSortable(nodeIn.Property, singleComplexNode.Property, - nodeIn.Property.DeclaringType, _model, _enableOrderBy)) - { - return nodeIn; - } + return nodeIn; } - else if (EdmHelpers.IsNotSortable(nodeIn.Property, _property, _structuredType, _model, _enableOrderBy)) + } + else if (nodeIn.Source.Kind == QueryNodeKind.SingleComplexNode) + { + SingleComplexNode singleComplexNode = nodeIn.Source as SingleComplexNode; + if (EdmHelpers.IsNotSortable(nodeIn.Property, singleComplexNode.Property, + nodeIn.Property.DeclaringType, _model, _enableOrderBy)) { return nodeIn; } } - - if (nodeIn.Source != null) + else if (EdmHelpers.IsNotSortable(nodeIn.Property, _property, _structuredType, _model, _enableOrderBy)) { - return nodeIn.Source.Accept(this); + return nodeIn; } - - return null; } - public override SingleValueNode Visit(SingleValueOpenPropertyAccessNode nodeIn) + if (nodeIn.Source != null) { - return null; + return nodeIn.Source.Accept(this); } - public override SingleValueNode Visit(SingleComplexNode nodeIn) - { - if (EdmHelpers.IsNotSortable(nodeIn.Property, _property, _structuredType, _model, _enableOrderBy)) - { - return nodeIn; - } + return null; + } - if (nodeIn.Source != null) - { - return nodeIn.Source.Accept(this); - } + public override SingleValueNode Visit(SingleValueOpenPropertyAccessNode nodeIn) + { + return null; + } - return null; + public override SingleValueNode Visit(SingleComplexNode nodeIn) + { + if (EdmHelpers.IsNotSortable(nodeIn.Property, _property, _structuredType, _model, _enableOrderBy)) + { + return nodeIn; } - public override SingleValueNode Visit(SingleNavigationNode nodeIn) + if (nodeIn.Source != null) { - if (EdmHelpers.IsNotSortable(nodeIn.NavigationProperty, _property, _structuredType, _model, - _enableOrderBy)) - { - return nodeIn; - } + return nodeIn.Source.Accept(this); + } - if (nodeIn.Source != null) - { - return nodeIn.Source.Accept(this); - } + return null; + } - return null; + public override SingleValueNode Visit(SingleNavigationNode nodeIn) + { + if (EdmHelpers.IsNotSortable(nodeIn.NavigationProperty, _property, _structuredType, _model, + _enableOrderBy)) + { + return nodeIn; } - public override SingleValueNode Visit(ResourceRangeVariableReferenceNode nodeIn) + if (nodeIn.Source != null) { - return null; + return nodeIn.Source.Accept(this); } - public override SingleValueNode Visit(NonResourceRangeVariableReferenceNode nodeIn) + return null; + } + + public override SingleValueNode Visit(ResourceRangeVariableReferenceNode nodeIn) + { + return null; + } + + public override SingleValueNode Visit(NonResourceRangeVariableReferenceNode nodeIn) + { + return null; + } + + public override SingleValueNode Visit(SingleValueFunctionCallNode nodeIn) + { + foreach (var parameter in nodeIn.Parameters) { - return null; + parameter.Accept(this); } - public override SingleValueNode Visit(SingleValueFunctionCallNode nodeIn) + if (nodeIn.Source != null) { - foreach (var parameter in nodeIn.Parameters) - { - parameter.Accept(this); - } + return nodeIn.Source.Accept(this); + } - if (nodeIn.Source != null) - { - return nodeIn.Source.Accept(this); - } + return null; + } - return null; + private static string GetPropertyName(SingleValueNode node) + { + if (node.Kind == QueryNodeKind.SingleNavigationNode) + { + return ((SingleNavigationNode)node).NavigationProperty.Name; } - - private static string GetPropertyName(SingleValueNode node) + else if (node.Kind == QueryNodeKind.SingleValuePropertyAccess) { - if (node.Kind == QueryNodeKind.SingleNavigationNode) - { - return ((SingleNavigationNode)node).NavigationProperty.Name; - } - else if (node.Kind == QueryNodeKind.SingleValuePropertyAccess) - { - return ((SingleValuePropertyAccessNode)node).Property.Name; - } - else if (node.Kind == QueryNodeKind.SingleComplexNode) - { - return ((SingleComplexNode)node).Property.Name; - } - return null; + return ((SingleValuePropertyAccessNode)node).Property.Name; + } + else if (node.Kind == QueryNodeKind.SingleComplexNode) + { + return ((SingleComplexNode)node).Property.Name; } + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/OrderByQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/OrderByQueryValidator.cs index 3f35147bd..872d54b47 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/OrderByQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/OrderByQueryValidator.cs @@ -10,636 +10,635 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Represents a validator used to validate an based on the . +/// +public class OrderByQueryValidator : IOrderByQueryValidator { /// - /// Represents a validator used to validate an based on the . + /// Validates an . /// - public class OrderByQueryValidator : IOrderByQueryValidator + /// The $orderby query. + /// The validation settings. + public virtual void Validate(OrderByQueryOption orderByOption, ODataValidationSettings validationSettings) { - /// - /// Validates an . - /// - /// The $orderby query. - /// The validation settings. - public virtual void Validate(OrderByQueryOption orderByOption, ODataValidationSettings validationSettings) - { - if (orderByOption == null) - { - throw Error.ArgumentNull("orderByOption"); - } - - if (validationSettings == null) - { - throw Error.ArgumentNull("validationSettings"); - } - - OrderByValidatorContext validatorContext = new OrderByValidatorContext - { - OrderBy = orderByOption, - Context = orderByOption.Context, - ValidationSettings = validationSettings, - Property = orderByOption.Context.TargetProperty, - StructuredType = orderByOption.Context.TargetStructuredType, - CurrentDepth = 0 - }; - - OrderByClause clause = orderByOption.OrderByClause; - while (clause != null) - { - validatorContext.IncrementNodeCount(); - - ValidateOrderBy(clause, validatorContext); - - clause = clause.ThenBy; - } - } - - /// - /// Validates a . - /// - /// The . - /// The validation context. - protected virtual void ValidateOrderBy(OrderByClause orderByClause, OrderByValidatorContext validatorContext) + if (orderByOption == null) { - if (orderByClause != null) - { - ValidateSingleValueNode(orderByClause.Expression, validatorContext, skipRangeVariable: false); - } + throw Error.ArgumentNull("orderByOption"); } - /// - /// Override this method if you want to visit each query node. - /// - /// The query node. - /// The validation context. - protected virtual void ValidateQueryNode(QueryNode node, OrderByValidatorContext validatorContext) + if (validationSettings == null) { - if (node is SingleValueNode singleNode) - { - ValidateSingleValueNode(singleNode, validatorContext, skipRangeVariable: true); - } - else if (node is CollectionNode collectionNode) - { - ValidateCollectionNode(collectionNode, validatorContext); - } + throw Error.ArgumentNull("validationSettings"); } - /// - /// The recursive method that validate most of the SingleValueNode type. - /// - /// The single value node. - /// The validator context. - /// The boolean value indicating skip the range variable validation. Typically, if it's 'Source' of a query node, we should skip it. - protected virtual void ValidateSingleValueNode(SingleValueNode node, OrderByValidatorContext validatorContext, bool skipRangeVariable) + OrderByValidatorContext validatorContext = new OrderByValidatorContext { - if (node == null) - { - return; - } + OrderBy = orderByOption, + Context = orderByOption.Context, + ValidationSettings = validationSettings, + Property = orderByOption.Context.TargetProperty, + StructuredType = orderByOption.Context.TargetStructuredType, + CurrentDepth = 0 + }; + + OrderByClause clause = orderByOption.OrderByClause; + while (clause != null) + { + validatorContext.IncrementNodeCount(); - switch (node.Kind) - { - case QueryNodeKind.BinaryOperator: - ValidateBinaryOperatorNode(node as BinaryOperatorNode, validatorContext); - break; - - case QueryNodeKind.Constant: - ValidateConstantNode(node as ConstantNode, validatorContext); - break; - - case QueryNodeKind.Convert: - ValidateConvertNode(node as ConvertNode, validatorContext); - break; - - case QueryNodeKind.Count: - ValidateCountNode(node as CountNode, validatorContext); - break; - - case QueryNodeKind.ResourceRangeVariableReference: - if (!skipRangeVariable) - { - ValidateRangeVariable((node as ResourceRangeVariableReferenceNode).RangeVariable, validatorContext); - } - break; - - case QueryNodeKind.NonResourceRangeVariableReference: - if (!skipRangeVariable) - { - ValidateRangeVariable((node as NonResourceRangeVariableReferenceNode).RangeVariable, validatorContext); - } - break; - - case QueryNodeKind.SingleValuePropertyAccess: - ValidateSingleValuePropertyAccessNode(node as SingleValuePropertyAccessNode, validatorContext); - break; - - case QueryNodeKind.SingleComplexNode: - ValidateSingleComplexNode(node as SingleComplexNode, validatorContext); - break; - - case QueryNodeKind.UnaryOperator: - ValidateUnaryOperatorNode(node as UnaryOperatorNode, validatorContext); - break; - - case QueryNodeKind.SingleValueFunctionCall: - ValidateSingleValueFunctionCallNode(node as SingleValueFunctionCallNode, validatorContext); - break; - - case QueryNodeKind.SingleResourceFunctionCall: - ValidateSingleResourceFunctionCallNode((SingleResourceFunctionCallNode)node, validatorContext); - break; - - case QueryNodeKind.SingleNavigationNode: - ValidateNavigationPropertyNode((SingleNavigationNode)node, validatorContext); - break; - - case QueryNodeKind.SingleResourceCast: - ValidateSingleResourceCastNode(node as SingleResourceCastNode, validatorContext); - break; - - case QueryNodeKind.Any: - ValidateAnyNode(node as AnyNode, validatorContext); - break; - - case QueryNodeKind.All: - ValidateAllNode(node as AllNode, validatorContext); - break; - - case QueryNodeKind.In: - ValidateInNode(node as InNode, validatorContext); - break; - - case QueryNodeKind.SingleValueOpenPropertyAccess: - ValidateSingleValueOpenPropertyNode(node as SingleValueOpenPropertyAccessNode, validatorContext); - break; - - case QueryNodeKind.NamedFunctionParameter: - case QueryNodeKind.ParameterAlias: - case QueryNodeKind.EntitySet: - case QueryNodeKind.KeyLookup: - case QueryNodeKind.SearchTerm: - default: - // No default validation logic for others. - break; - } + ValidateOrderBy(clause, validatorContext); + + clause = clause.ThenBy; } + } - /// - /// Override this method to restrict the dynamic property inside the $orderby query. - /// - /// The in operator node to validate. - /// The validation context. - protected virtual void ValidateSingleValueOpenPropertyNode(SingleValueOpenPropertyAccessNode openPropertyNode, OrderByValidatorContext validatorContext) + /// + /// Validates a . + /// + /// The . + /// The validation context. + protected virtual void ValidateOrderBy(OrderByClause orderByClause, OrderByValidatorContext validatorContext) + { + if (orderByClause != null) { - // No default validation logic here. + ValidateSingleValueNode(orderByClause.Expression, validatorContext, skipRangeVariable: false); } + } - /// - /// Override this method to restrict the 'in' operator in the $orderby query. - /// - /// The in operator node to validate. - /// The validation context. - protected virtual void ValidateInNode(InNode inNode, OrderByValidatorContext validatorContext) + /// + /// Override this method if you want to visit each query node. + /// + /// The query node. + /// The validation context. + protected virtual void ValidateQueryNode(QueryNode node, OrderByValidatorContext validatorContext) + { + if (node is SingleValueNode singleNode) { - ValidateQueryNode(inNode?.Left, validatorContext); - ValidateQueryNode(inNode?.Right, validatorContext); + ValidateSingleValueNode(singleNode, validatorContext, skipRangeVariable: true); } - - /// - /// Override this method to restrict the 'constant' inside the $orderby query. - /// - /// The constant node to validate. - /// The validation context. - protected virtual void ValidateConstantNode(ConstantNode constantNode, OrderByValidatorContext validatorContext) + else if (node is CollectionNode collectionNode) { - // No default validation logic here. + ValidateCollectionNode(collectionNode, validatorContext); } + } - /// - /// Override this method to restrict the 'cast' inside the $orderby query. - /// - /// The convert node to validate. - /// The validation context. - protected virtual void ValidateConvertNode(ConvertNode convertNode, OrderByValidatorContext validatorContext) + /// + /// The recursive method that validate most of the SingleValueNode type. + /// + /// The single value node. + /// The validator context. + /// The boolean value indicating skip the range variable validation. Typically, if it's 'Source' of a query node, we should skip it. + protected virtual void ValidateSingleValueNode(SingleValueNode node, OrderByValidatorContext validatorContext, bool skipRangeVariable) + { + if (node == null) { - // Validate child nodes but not the ConvertNode itself. - ValidateQueryNode(convertNode?.Source, validatorContext); + return; } - /// - /// Override this method to restrict the '$count' inside the $orderBy query. - /// - /// The count node to validate. - /// The validation context. - protected virtual void ValidateCountNode(CountNode countNode, OrderByValidatorContext validatorContext) + switch (node.Kind) { - ValidateQueryNode(countNode?.Source, validatorContext); + case QueryNodeKind.BinaryOperator: + ValidateBinaryOperatorNode(node as BinaryOperatorNode, validatorContext); + break; - if (countNode?.FilterClause != null) - { - ValidateQueryNode(countNode.FilterClause.Expression, validatorContext); - } + case QueryNodeKind.Constant: + ValidateConstantNode(node as ConstantNode, validatorContext); + break; - if (countNode?.SearchClause != null) - { - ValidateQueryNode(countNode.SearchClause.Expression, validatorContext); - } - } + case QueryNodeKind.Convert: + ValidateConvertNode(node as ConvertNode, validatorContext); + break; - /// - /// Override this method to restrict the binary operators inside the $orderby query. - /// That includes all the logical operators except 'not' and all math operators. - /// - /// The binary operator node to validate. - /// The validation context. - protected virtual void ValidateBinaryOperatorNode(BinaryOperatorNode binaryOperatorNode, OrderByValidatorContext validatorContext) - { - // recursion case goes here - ValidateQueryNode(binaryOperatorNode?.Left, validatorContext); - ValidateQueryNode(binaryOperatorNode?.Right, validatorContext); - } + case QueryNodeKind.Count: + ValidateCountNode(node as CountNode, validatorContext); + break; - /// - /// Override this method to validate the parameter used in the $orderby query. - /// - /// The range variable node to validate. - /// The validation context. - protected virtual void ValidateRangeVariable(RangeVariable rangeVariable, OrderByValidatorContext validatorContext) - { - if (rangeVariable != null) - { - string variableName = rangeVariable.Name; - if (!IsAllowed(validatorContext.ValidationSettings, variableName)) + case QueryNodeKind.ResourceRangeVariableReference: + if (!skipRangeVariable) { - throw new ODataException(Error.Format(SRResources.NotAllowedOrderByProperty, variableName, "AllowedOrderByProperties")); + ValidateRangeVariable((node as ResourceRangeVariableReferenceNode).RangeVariable, validatorContext); } - } + break; + + case QueryNodeKind.NonResourceRangeVariableReference: + if (!skipRangeVariable) + { + ValidateRangeVariable((node as NonResourceRangeVariableReferenceNode).RangeVariable, validatorContext); + } + break; + + case QueryNodeKind.SingleValuePropertyAccess: + ValidateSingleValuePropertyAccessNode(node as SingleValuePropertyAccessNode, validatorContext); + break; + + case QueryNodeKind.SingleComplexNode: + ValidateSingleComplexNode(node as SingleComplexNode, validatorContext); + break; + + case QueryNodeKind.UnaryOperator: + ValidateUnaryOperatorNode(node as UnaryOperatorNode, validatorContext); + break; + + case QueryNodeKind.SingleValueFunctionCall: + ValidateSingleValueFunctionCallNode(node as SingleValueFunctionCallNode, validatorContext); + break; + + case QueryNodeKind.SingleResourceFunctionCall: + ValidateSingleResourceFunctionCallNode((SingleResourceFunctionCallNode)node, validatorContext); + break; + + case QueryNodeKind.SingleNavigationNode: + ValidateNavigationPropertyNode((SingleNavigationNode)node, validatorContext); + break; + + case QueryNodeKind.SingleResourceCast: + ValidateSingleResourceCastNode(node as SingleResourceCastNode, validatorContext); + break; + + case QueryNodeKind.Any: + ValidateAnyNode(node as AnyNode, validatorContext); + break; + + case QueryNodeKind.All: + ValidateAllNode(node as AllNode, validatorContext); + break; + + case QueryNodeKind.In: + ValidateInNode(node as InNode, validatorContext); + break; + + case QueryNodeKind.SingleValueOpenPropertyAccess: + ValidateSingleValueOpenPropertyNode(node as SingleValueOpenPropertyAccessNode, validatorContext); + break; + + case QueryNodeKind.NamedFunctionParameter: + case QueryNodeKind.ParameterAlias: + case QueryNodeKind.EntitySet: + case QueryNodeKind.KeyLookup: + case QueryNodeKind.SearchTerm: + default: + // No default validation logic for others. + break; } + } - /// - /// Override this method to validate property accessors. - /// - /// The single value property access node. - /// The validation context. - protected virtual void ValidateSingleValuePropertyAccessNode(SingleValuePropertyAccessNode propertyAccessNode, OrderByValidatorContext validatorContext) - { - if (propertyAccessNode == null || validatorContext == null) - { - return; - } + /// + /// Override this method to restrict the dynamic property inside the $orderby query. + /// + /// The in operator node to validate. + /// The validation context. + protected virtual void ValidateSingleValueOpenPropertyNode(SingleValueOpenPropertyAccessNode openPropertyNode, OrderByValidatorContext validatorContext) + { + // No default validation logic here. + } - IEdmModel model = validatorContext.Model; - bool enableOrderBy = validatorContext.Context.DefaultQueryConfigurations.EnableOrderBy; - IEdmProperty property = propertyAccessNode.Property; + /// + /// Override this method to restrict the 'in' operator in the $orderby query. + /// + /// The in operator node to validate. + /// The validation context. + protected virtual void ValidateInNode(InNode inNode, OrderByValidatorContext validatorContext) + { + ValidateQueryNode(inNode?.Left, validatorContext); + ValidateQueryNode(inNode?.Right, validatorContext); + } - // Check whether the property is sortable. - if (validatorContext.ValidationSettings.AllowedOrderByProperties.Count == 0) - { - bool notSortable = false; - if (propertyAccessNode.Source != null) - { - if (propertyAccessNode.Source.Kind == QueryNodeKind.SingleNavigationNode) - { - SingleNavigationNode singleNavigationNode = propertyAccessNode.Source as SingleNavigationNode; - notSortable = EdmHelpers.IsNotSortable(property, singleNavigationNode.NavigationProperty, singleNavigationNode.NavigationProperty.ToEntityType(), model, enableOrderBy); - } - else if (propertyAccessNode.Source.Kind == QueryNodeKind.SingleComplexNode) - { - SingleComplexNode singleComplexNode = propertyAccessNode.Source as SingleComplexNode; - notSortable = EdmHelpers.IsNotSortable(property, singleComplexNode.Property, property.DeclaringType, model, enableOrderBy); - } - else - { - notSortable = EdmHelpers.IsNotSortable(property, validatorContext.Property, validatorContext.StructuredType, model, enableOrderBy); - } - } + /// + /// Override this method to restrict the 'constant' inside the $orderby query. + /// + /// The constant node to validate. + /// The validation context. + protected virtual void ValidateConstantNode(ConstantNode constantNode, OrderByValidatorContext validatorContext) + { + // No default validation logic here. + } - if (notSortable) - { - throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, property.Name)); - } - } - else - { - ValidatePropertyAllowed(property.Name, validatorContext.ValidationSettings); - } + /// + /// Override this method to restrict the 'cast' inside the $orderby query. + /// + /// The convert node to validate. + /// The validation context. + protected virtual void ValidateConvertNode(ConvertNode convertNode, OrderByValidatorContext validatorContext) + { + // Validate child nodes but not the ConvertNode itself. + ValidateQueryNode(convertNode?.Source, validatorContext); + } + + /// + /// Override this method to restrict the '$count' inside the $orderBy query. + /// + /// The count node to validate. + /// The validation context. + protected virtual void ValidateCountNode(CountNode countNode, OrderByValidatorContext validatorContext) + { + ValidateQueryNode(countNode?.Source, validatorContext); - ValidateQueryNode(propertyAccessNode.Source, validatorContext); + if (countNode?.FilterClause != null) + { + ValidateQueryNode(countNode.FilterClause.Expression, validatorContext); } - /// - /// Override this method to validate Function calls, such as 'length', 'year', etc. - /// - /// The single value function call node to validate. - /// The validation context. - protected virtual void ValidateSingleValueFunctionCallNode(SingleValueFunctionCallNode node, OrderByValidatorContext validatorContext) + if (countNode?.SearchClause != null) { - if (node == null || validatorContext == null) - { - return; - } + ValidateQueryNode(countNode.SearchClause.Expression, validatorContext); + } + } - QueryValidatorHelpers.ValidateFunction(node, validatorContext.ValidationSettings); + /// + /// Override this method to restrict the binary operators inside the $orderby query. + /// That includes all the logical operators except 'not' and all math operators. + /// + /// The binary operator node to validate. + /// The validation context. + protected virtual void ValidateBinaryOperatorNode(BinaryOperatorNode binaryOperatorNode, OrderByValidatorContext validatorContext) + { + // recursion case goes here + ValidateQueryNode(binaryOperatorNode?.Left, validatorContext); + ValidateQueryNode(binaryOperatorNode?.Right, validatorContext); + } - foreach (QueryNode argumentNode in node.Parameters) + /// + /// Override this method to validate the parameter used in the $orderby query. + /// + /// The range variable node to validate. + /// The validation context. + protected virtual void ValidateRangeVariable(RangeVariable rangeVariable, OrderByValidatorContext validatorContext) + { + if (rangeVariable != null) + { + string variableName = rangeVariable.Name; + if (!IsAllowed(validatorContext.ValidationSettings, variableName)) { - ValidateQueryNode(argumentNode, validatorContext); + throw new ODataException(Error.Format(SRResources.NotAllowedOrderByProperty, variableName, "AllowedOrderByProperties")); } } + } - /// - /// Override this method to validate single complex property accessors. - /// - /// The single complex node to validate. - /// The validation context. - protected virtual void ValidateSingleComplexNode(SingleComplexNode singleComplexNode, OrderByValidatorContext validatorContext) + /// + /// Override this method to validate property accessors. + /// + /// The single value property access node. + /// The validation context. + protected virtual void ValidateSingleValuePropertyAccessNode(SingleValuePropertyAccessNode propertyAccessNode, OrderByValidatorContext validatorContext) + { + if (propertyAccessNode == null || validatorContext == null) { - if (singleComplexNode == null || validatorContext == null) - { - return; - } + return; + } - // Check whether the property is sortable. - if (validatorContext.ValidationSettings.AllowedOrderByProperties.Count == 0) + IEdmModel model = validatorContext.Model; + bool enableOrderBy = validatorContext.Context.DefaultQueryConfigurations.EnableOrderBy; + IEdmProperty property = propertyAccessNode.Property; + + // Check whether the property is sortable. + if (validatorContext.ValidationSettings.AllowedOrderByProperties.Count == 0) + { + bool notSortable = false; + if (propertyAccessNode.Source != null) { - IEdmProperty property = singleComplexNode.Property; - if (EdmHelpers.IsNotSortable(property, - validatorContext.Property, - validatorContext.StructuredType, - validatorContext.Model, - validatorContext.Context.DefaultQueryConfigurations.EnableOrderBy)) + if (propertyAccessNode.Source.Kind == QueryNodeKind.SingleNavigationNode) + { + SingleNavigationNode singleNavigationNode = propertyAccessNode.Source as SingleNavigationNode; + notSortable = EdmHelpers.IsNotSortable(property, singleNavigationNode.NavigationProperty, singleNavigationNode.NavigationProperty.ToEntityType(), model, enableOrderBy); + } + else if (propertyAccessNode.Source.Kind == QueryNodeKind.SingleComplexNode) + { + SingleComplexNode singleComplexNode = propertyAccessNode.Source as SingleComplexNode; + notSortable = EdmHelpers.IsNotSortable(property, singleComplexNode.Property, property.DeclaringType, model, enableOrderBy); + } + else { - throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, property.Name)); + notSortable = EdmHelpers.IsNotSortable(property, validatorContext.Property, validatorContext.StructuredType, model, enableOrderBy); } } - else + + if (notSortable) { - ValidatePropertyAllowed(singleComplexNode.Property.Name, validatorContext.ValidationSettings); + throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, property.Name)); } - - ValidateQueryNode(singleComplexNode.Source, validatorContext); } - - /// - /// Override this method to validate single resource function calls. - /// - /// The node to validate. - /// The validation context. - protected virtual void ValidateSingleResourceFunctionCallNode(SingleResourceFunctionCallNode node, OrderByValidatorContext validatorContext) + else { - if (node == null || validatorContext == null) - { - return; - } + ValidatePropertyAllowed(property.Name, validatorContext.ValidationSettings); + } - // Ideally, we should validate the function call name here. - // But, there's no validation setting for SingleResourceFunctionCall. So, let's skip the name validation - // Customer can override this method to add name validation. + ValidateQueryNode(propertyAccessNode.Source, validatorContext); + } - foreach (QueryNode argumentNode in node.Parameters) - { - ValidateQueryNode(argumentNode, validatorContext); - } + /// + /// Override this method to validate Function calls, such as 'length', 'year', etc. + /// + /// The single value function call node to validate. + /// The validation context. + protected virtual void ValidateSingleValueFunctionCallNode(SingleValueFunctionCallNode node, OrderByValidatorContext validatorContext) + { + if (node == null || validatorContext == null) + { + return; } - /// - /// Override this method if you want to validate casts on single resource. - /// - /// The single resource cast node. - /// The validation context. - protected virtual void ValidateSingleResourceCastNode(SingleResourceCastNode singleResourceCastNode, OrderByValidatorContext validatorContext) + QueryValidatorHelpers.ValidateFunction(node, validatorContext.ValidationSettings); + + foreach (QueryNode argumentNode in node.Parameters) { - ValidateQueryNode(singleResourceCastNode?.Source, validatorContext); + ValidateQueryNode(argumentNode, validatorContext); } + } - /// - /// Override this method to validate the Not operator. - /// - /// The unary operator node. - /// The validation context. - protected virtual void ValidateUnaryOperatorNode(UnaryOperatorNode unaryOperatorNode, OrderByValidatorContext validatorContext) + /// + /// Override this method to validate single complex property accessors. + /// + /// The single complex node to validate. + /// The validation context. + protected virtual void ValidateSingleComplexNode(SingleComplexNode singleComplexNode, OrderByValidatorContext validatorContext) + { + if (singleComplexNode == null || validatorContext == null) { - // no default validation here + return; } - /// - /// Override this method for the collection value navigation property node. - /// - /// The source node to validate. - /// The validation context. - protected virtual void ValidateNavigationPropertyNode(CollectionNavigationNode collectionNavigation, OrderByValidatorContext validatorContext) + // Check whether the property is sortable. + if (validatorContext.ValidationSettings.AllowedOrderByProperties.Count == 0) { - if (collectionNavigation == null || validatorContext == null) - { - return; - } - - // Check whether the property is not sortable - if (validatorContext.ValidationSettings.AllowedOrderByProperties.Count == 0) - { - if (EdmHelpers.IsNotSortable(collectionNavigation.NavigationProperty, + IEdmProperty property = singleComplexNode.Property; + if (EdmHelpers.IsNotSortable(property, validatorContext.Property, validatorContext.StructuredType, validatorContext.Model, validatorContext.Context.DefaultQueryConfigurations.EnableOrderBy)) - { - throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, collectionNavigation.NavigationProperty.Name)); - } - } - else { - ValidatePropertyAllowed(collectionNavigation.NavigationProperty.Name, validatorContext.ValidationSettings); + throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, property.Name)); } - - ValidateQueryNode(collectionNavigation.Source, validatorContext); } - - /// - /// Override this method for the single value navigation property node. - /// - /// The source node to validate. - /// The validation context. - protected virtual void ValidateNavigationPropertyNode(SingleNavigationNode singleNavigation, OrderByValidatorContext validatorContext) + else { - if (singleNavigation == null || validatorContext == null) - { - return; - } + ValidatePropertyAllowed(singleComplexNode.Property.Name, validatorContext.ValidationSettings); + } - // Check whether the property is not sortable - if (validatorContext.ValidationSettings.AllowedOrderByProperties.Count == 0) - { - if (EdmHelpers.IsNotSortable(singleNavigation.NavigationProperty, - validatorContext.Property, - validatorContext.StructuredType, - validatorContext.Model, - validatorContext.Context.DefaultQueryConfigurations.EnableOrderBy)) - { - throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, singleNavigation.NavigationProperty.Name)); - } - } - else - { - ValidatePropertyAllowed(singleNavigation.NavigationProperty.Name, validatorContext.ValidationSettings); - } + ValidateQueryNode(singleComplexNode.Source, validatorContext); + } - ValidateQueryNode(singleNavigation.Source, validatorContext); + /// + /// Override this method to validate single resource function calls. + /// + /// The node to validate. + /// The validation context. + protected virtual void ValidateSingleResourceFunctionCallNode(SingleResourceFunctionCallNode node, OrderByValidatorContext validatorContext) + { + if (node == null || validatorContext == null) + { + return; } - /// - /// Override this method to restrict the 'all' query inside the $orderby query. - /// - /// The all node to validate. - /// The validation context. - protected virtual void ValidateAllNode(AllNode allNode, OrderByValidatorContext validatorContext) + // Ideally, we should validate the function call name here. + // But, there's no validation setting for SingleResourceFunctionCall. So, let's skip the name validation + // Customer can override this method to add name validation. + + foreach (QueryNode argumentNode in node.Parameters) { - // no default validation here + ValidateQueryNode(argumentNode, validatorContext); } + } - /// - /// Override this method to restrict the 'any' query inside the $orderby query. - /// - /// The any node to validate. - /// The validation context. - protected virtual void ValidateAnyNode(AnyNode anyNode, OrderByValidatorContext validatorContext) + /// + /// Override this method if you want to validate casts on single resource. + /// + /// The single resource cast node. + /// The validation context. + protected virtual void ValidateSingleResourceCastNode(SingleResourceCastNode singleResourceCastNode, OrderByValidatorContext validatorContext) + { + ValidateQueryNode(singleResourceCastNode?.Source, validatorContext); + } + + /// + /// Override this method to validate the Not operator. + /// + /// The unary operator node. + /// The validation context. + protected virtual void ValidateUnaryOperatorNode(UnaryOperatorNode unaryOperatorNode, OrderByValidatorContext validatorContext) + { + // no default validation here + } + + /// + /// Override this method for the collection value navigation property node. + /// + /// The source node to validate. + /// The validation context. + protected virtual void ValidateNavigationPropertyNode(CollectionNavigationNode collectionNavigation, OrderByValidatorContext validatorContext) + { + if (collectionNavigation == null || validatorContext == null) { - // no default validation here + return; } - /// - /// The recursive method that validate most of the query node type is of CollectionNode type. - /// - /// The collection value node. - /// The validator context. - protected virtual void ValidateCollectionNode(CollectionNode node, OrderByValidatorContext validatorContext) + // Check whether the property is not sortable + if (validatorContext.ValidationSettings.AllowedOrderByProperties.Count == 0) { - if (node == null) + if (EdmHelpers.IsNotSortable(collectionNavigation.NavigationProperty, + validatorContext.Property, + validatorContext.StructuredType, + validatorContext.Model, + validatorContext.Context.DefaultQueryConfigurations.EnableOrderBy)) { - return; + throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, collectionNavigation.NavigationProperty.Name)); } + } + else + { + ValidatePropertyAllowed(collectionNavigation.NavigationProperty.Name, validatorContext.ValidationSettings); + } - switch (node.Kind) + ValidateQueryNode(collectionNavigation.Source, validatorContext); + } + + /// + /// Override this method for the single value navigation property node. + /// + /// The source node to validate. + /// The validation context. + protected virtual void ValidateNavigationPropertyNode(SingleNavigationNode singleNavigation, OrderByValidatorContext validatorContext) + { + if (singleNavigation == null || validatorContext == null) + { + return; + } + + // Check whether the property is not sortable + if (validatorContext.ValidationSettings.AllowedOrderByProperties.Count == 0) + { + if (EdmHelpers.IsNotSortable(singleNavigation.NavigationProperty, + validatorContext.Property, + validatorContext.StructuredType, + validatorContext.Model, + validatorContext.Context.DefaultQueryConfigurations.EnableOrderBy)) { - case QueryNodeKind.CollectionPropertyAccess: - ValidateCollectionPropertyAccessNode((CollectionPropertyAccessNode)node, validatorContext); - break; - - case QueryNodeKind.CollectionComplexNode: - ValidateCollectionComplexNode((CollectionComplexNode)node, validatorContext); - break; - - case QueryNodeKind.CollectionNavigationNode: - ValidateNavigationPropertyNode((CollectionNavigationNode)node, validatorContext); - break; - - case QueryNodeKind.CollectionResourceCast: - ValidateCollectionResourceCastNode((CollectionResourceCastNode)node, validatorContext); - break; - - case QueryNodeKind.CollectionFunctionCall: - case QueryNodeKind.CollectionResourceFunctionCall: - case QueryNodeKind.CollectionOpenPropertyAccess: - default: - // by default no validation for them. - break; + throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, singleNavigation.NavigationProperty.Name)); } } + else + { + ValidatePropertyAllowed(singleNavigation.NavigationProperty.Name, validatorContext.ValidationSettings); + } + + ValidateQueryNode(singleNavigation.Source, validatorContext); + } + + /// + /// Override this method to restrict the 'all' query inside the $orderby query. + /// + /// The all node to validate. + /// The validation context. + protected virtual void ValidateAllNode(AllNode allNode, OrderByValidatorContext validatorContext) + { + // no default validation here + } + + /// + /// Override this method to restrict the 'any' query inside the $orderby query. + /// + /// The any node to validate. + /// The validation context. + protected virtual void ValidateAnyNode(AnyNode anyNode, OrderByValidatorContext validatorContext) + { + // no default validation here + } - /// - /// Override this method if you want to validate on resource collections cast node. - /// - /// The collection resource cast node. - /// The validation context. - protected virtual void ValidateCollectionResourceCastNode(CollectionResourceCastNode collectionResourceCastNode, OrderByValidatorContext validatorContext) + /// + /// The recursive method that validate most of the query node type is of CollectionNode type. + /// + /// The collection value node. + /// The validator context. + protected virtual void ValidateCollectionNode(CollectionNode node, OrderByValidatorContext validatorContext) + { + if (node == null) { - ValidateQueryNode(collectionResourceCastNode?.Source, validatorContext); + return; } - /// - /// Override this method to validate collection property accessors. - /// - /// The collection property access node to validate. - /// The validation context. - protected virtual void ValidateCollectionPropertyAccessNode(CollectionPropertyAccessNode propertyAccessNode, OrderByValidatorContext validatorContext) + switch (node.Kind) { - if (propertyAccessNode == null || validatorContext == null) - { - return; - } + case QueryNodeKind.CollectionPropertyAccess: + ValidateCollectionPropertyAccessNode((CollectionPropertyAccessNode)node, validatorContext); + break; + + case QueryNodeKind.CollectionComplexNode: + ValidateCollectionComplexNode((CollectionComplexNode)node, validatorContext); + break; + + case QueryNodeKind.CollectionNavigationNode: + ValidateNavigationPropertyNode((CollectionNavigationNode)node, validatorContext); + break; + + case QueryNodeKind.CollectionResourceCast: + ValidateCollectionResourceCastNode((CollectionResourceCastNode)node, validatorContext); + break; + + case QueryNodeKind.CollectionFunctionCall: + case QueryNodeKind.CollectionResourceFunctionCall: + case QueryNodeKind.CollectionOpenPropertyAccess: + default: + // by default no validation for them. + break; + } + } - // Check whether the property is sortable. - if (validatorContext.ValidationSettings.AllowedOrderByProperties.Count == 0) - { - IEdmProperty property = propertyAccessNode.Property; - if (EdmHelpers.IsNotSortable(property, - validatorContext.Property, - validatorContext.StructuredType, - validatorContext.Model, - validatorContext.Context.DefaultQueryConfigurations.EnableOrderBy)) - { - throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, property.Name)); - } - } - else - { - ValidatePropertyAllowed(propertyAccessNode.Property.Name, validatorContext.ValidationSettings); - } + /// + /// Override this method if you want to validate on resource collections cast node. + /// + /// The collection resource cast node. + /// The validation context. + protected virtual void ValidateCollectionResourceCastNode(CollectionResourceCastNode collectionResourceCastNode, OrderByValidatorContext validatorContext) + { + ValidateQueryNode(collectionResourceCastNode?.Source, validatorContext); + } - ValidateQueryNode(propertyAccessNode.Source, validatorContext); + /// + /// Override this method to validate collection property accessors. + /// + /// The collection property access node to validate. + /// The validation context. + protected virtual void ValidateCollectionPropertyAccessNode(CollectionPropertyAccessNode propertyAccessNode, OrderByValidatorContext validatorContext) + { + if (propertyAccessNode == null || validatorContext == null) + { + return; } - /// - /// Override this method to validate collection complex property accessors. - /// - /// The collection complex node to validate. - /// The validation context. - protected virtual void ValidateCollectionComplexNode(CollectionComplexNode collectionComplexNode, OrderByValidatorContext validatorContext) + // Check whether the property is sortable. + if (validatorContext.ValidationSettings.AllowedOrderByProperties.Count == 0) { - if (collectionComplexNode == null || validatorContext == null) + IEdmProperty property = propertyAccessNode.Property; + if (EdmHelpers.IsNotSortable(property, + validatorContext.Property, + validatorContext.StructuredType, + validatorContext.Model, + validatorContext.Context.DefaultQueryConfigurations.EnableOrderBy)) { - return; + throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, property.Name)); } + } + else + { + ValidatePropertyAllowed(propertyAccessNode.Property.Name, validatorContext.ValidationSettings); + } - // Check whether the property is sortable. - if (validatorContext.ValidationSettings.AllowedOrderByProperties.Count == 0) - { - IEdmProperty property = collectionComplexNode.Property; - if (EdmHelpers.IsNotSortable(property, validatorContext.Property, - validatorContext.StructuredType, - validatorContext.Model, - validatorContext.Context.DefaultQueryConfigurations.EnableOrderBy)) - { - throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, property.Name)); - } - } - else - { - ValidatePropertyAllowed(collectionComplexNode.Property.Name, validatorContext.ValidationSettings); - } + ValidateQueryNode(propertyAccessNode.Source, validatorContext); + } - ValidateQueryNode(collectionComplexNode.Source, validatorContext); + /// + /// Override this method to validate collection complex property accessors. + /// + /// The collection complex node to validate. + /// The validation context. + protected virtual void ValidateCollectionComplexNode(CollectionComplexNode collectionComplexNode, OrderByValidatorContext validatorContext) + { + if (collectionComplexNode == null || validatorContext == null) + { + return; } - private static void ValidatePropertyAllowed(string propertyName, ODataValidationSettings validationSettings) + // Check whether the property is sortable. + if (validatorContext.ValidationSettings.AllowedOrderByProperties.Count == 0) { - // An empty collection means client can order the queryable result by any properties - if (validationSettings.AllowedOrderByProperties.Count == 0) + IEdmProperty property = collectionComplexNode.Property; + if (EdmHelpers.IsNotSortable(property, validatorContext.Property, + validatorContext.StructuredType, + validatorContext.Model, + validatorContext.Context.DefaultQueryConfigurations.EnableOrderBy)) { - return; + throw new ODataException(Error.Format(SRResources.NotSortablePropertyUsedInOrderBy, property.Name)); } + } + else + { + ValidatePropertyAllowed(collectionComplexNode.Property.Name, validatorContext.ValidationSettings); + } - // Then if the property name is not set in the collection, it means client can't order by it. - if (!validationSettings.AllowedOrderByProperties.Contains(propertyName)) - { - throw new ODataException(Error.Format(SRResources.NotAllowedOrderByProperty, propertyName, "AllowedOrderByProperties")); - } + ValidateQueryNode(collectionComplexNode.Source, validatorContext); + } + + private static void ValidatePropertyAllowed(string propertyName, ODataValidationSettings validationSettings) + { + // An empty collection means client can order the queryable result by any properties + if (validationSettings.AllowedOrderByProperties.Count == 0) + { + return; } - private static bool IsAllowed(ODataValidationSettings validationSettings, string propertyName) + // Then if the property name is not set in the collection, it means client can't order by it. + if (!validationSettings.AllowedOrderByProperties.Contains(propertyName)) { - return validationSettings.AllowedOrderByProperties.Count == 0 || - validationSettings.AllowedOrderByProperties.Contains(propertyName); + throw new ODataException(Error.Format(SRResources.NotAllowedOrderByProperty, propertyName, "AllowedOrderByProperties")); } } + + private static bool IsAllowed(ODataValidationSettings validationSettings, string propertyName) + { + return validationSettings.AllowedOrderByProperties.Count == 0 || + validationSettings.AllowedOrderByProperties.Contains(propertyName); + } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/OrderByValidatorContext.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/OrderByValidatorContext.cs index 3d041cdac..e4cb07ab9 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/OrderByValidatorContext.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/OrderByValidatorContext.cs @@ -7,37 +7,36 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// The metadata context for $orderby validator. +/// +public class OrderByValidatorContext : QueryValidatorContext { + private int _orderByNodeCount = 0; + /// - /// The metadata context for $orderby validator. + /// The top level $orderby query option. /// - public class OrderByValidatorContext : QueryValidatorContext - { - private int _orderByNodeCount = 0; + public OrderByQueryOption OrderBy { get; set; } - /// - /// The top level $orderby query option. - /// - public OrderByQueryOption OrderBy { get; set; } + /// + /// Gets current orderby node count. + /// + public int OrderByNodeCount => _orderByNodeCount; - /// - /// Gets current orderby node count. - /// - public int OrderByNodeCount => _orderByNodeCount; + /// + /// Increment orderby node count. + /// + /// Throw OData exception. + public void IncrementNodeCount() + { + ++_orderByNodeCount; - /// - /// Increment orderby node count. - /// - /// Throw OData exception. - public void IncrementNodeCount() + if (_orderByNodeCount > ValidationSettings.MaxOrderByNodeCount) { - ++_orderByNodeCount; - - if (_orderByNodeCount > ValidationSettings.MaxOrderByNodeCount) - { - throw new ODataException(Error.Format(SRResources.OrderByNodeCountExceeded, ValidationSettings.MaxOrderByNodeCount)); - } + throw new ODataException(Error.Format(SRResources.OrderByNodeCountExceeded, ValidationSettings.MaxOrderByNodeCount)); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/QueryValidatorContext.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/QueryValidatorContext.cs index af5b49131..2d3e6fe1e 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/QueryValidatorContext.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/QueryValidatorContext.cs @@ -7,41 +7,40 @@ using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// The base for validator context. +/// +public abstract class QueryValidatorContext { /// - /// The base for validator context. + /// The Query context. + /// + public ODataQueryContext Context { get; set; } + + /// + /// The Query validation settings. + /// + public ODataValidationSettings ValidationSettings { get; set; } + + /// + /// The applied property, It could be null. + /// + public IEdmProperty Property { get; set; } + + /// + /// The applied structured type. + /// + public IEdmStructuredType StructuredType { get; set; } + + /// + /// The current depth. + /// + public int CurrentDepth { get; set; } + + /// + /// Gets the Edm model. /// - public abstract class QueryValidatorContext - { - /// - /// The Query context. - /// - public ODataQueryContext Context { get; set; } - - /// - /// The Query validation settings. - /// - public ODataValidationSettings ValidationSettings { get; set; } - - /// - /// The applied property, It could be null. - /// - public IEdmProperty Property { get; set; } - - /// - /// The applied structured type. - /// - public IEdmStructuredType StructuredType { get; set; } - - /// - /// The current depth. - /// - public int CurrentDepth { get; set; } - - /// - /// Gets the Edm model. - /// - public IEdmModel Model => Context.Model; - } + public IEdmModel Model => Context.Model; } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/QueryValidatorHelpers.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/QueryValidatorHelpers.cs index 4d2e2101d..345835edf 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/QueryValidatorHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/QueryValidatorHelpers.cs @@ -8,135 +8,134 @@ using Microsoft.OData; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Helper methods for validation. +/// +internal class QueryValidatorHelpers { /// - /// Helper methods for validation. + /// Validates a single value function call. /// - internal class QueryValidatorHelpers + /// The function name. + /// The validation settings. + public static void ValidateFunction(SingleValueFunctionCallNode functionCallNode, ODataValidationSettings settings) { - /// - /// Validates a single value function call. - /// - /// The function name. - /// The validation settings. - public static void ValidateFunction(SingleValueFunctionCallNode functionCallNode, ODataValidationSettings settings) + if (functionCallNode == null) { - if (functionCallNode == null) - { - throw Error.ArgumentNull(nameof(functionCallNode)); - } + throw Error.ArgumentNull(nameof(functionCallNode)); + } - if (settings == null) - { - throw Error.ArgumentNull(nameof(settings)); - } + if (settings == null) + { + throw Error.ArgumentNull(nameof(settings)); + } - string functionName = functionCallNode.Name; + string functionName = functionCallNode.Name; - AllowedFunctions convertedFunction = ToODataFunction(functionName); - if (convertedFunction != AllowedFunctions.None && - (settings.AllowedFunctions & convertedFunction) != convertedFunction) - { - // this means the given function is not allowed - throw new ODataException(Error.Format(SRResources.NotAllowedFunction, functionName, "AllowedFunctions")); - } + AllowedFunctions convertedFunction = ToODataFunction(functionName); + if (convertedFunction != AllowedFunctions.None && + (settings.AllowedFunctions & convertedFunction) != convertedFunction) + { + // this means the given function is not allowed + throw new ODataException(Error.Format(SRResources.NotAllowedFunction, functionName, "AllowedFunctions")); } + } - private static AllowedFunctions ToODataFunction(string functionName) + private static AllowedFunctions ToODataFunction(string functionName) + { + AllowedFunctions result = AllowedFunctions.None; + switch (functionName) { - AllowedFunctions result = AllowedFunctions.None; - switch (functionName) - { - case "any": - result = AllowedFunctions.Any; - break; - case "all": - result = AllowedFunctions.All; - break; - case "cast": - result = AllowedFunctions.Cast; - break; - case ClrCanonicalFunctions.CeilingFunctionName: - result = AllowedFunctions.Ceiling; - break; - case ClrCanonicalFunctions.ConcatFunctionName: - result = AllowedFunctions.Concat; - break; - case ClrCanonicalFunctions.ContainsFunctionName: - result = AllowedFunctions.Contains; - break; - case ClrCanonicalFunctions.DayFunctionName: - result = AllowedFunctions.Day; - break; - case ClrCanonicalFunctions.EndswithFunctionName: - result = AllowedFunctions.EndsWith; - break; - case ClrCanonicalFunctions.FloorFunctionName: - result = AllowedFunctions.Floor; - break; - case ClrCanonicalFunctions.HourFunctionName: - result = AllowedFunctions.Hour; - break; - case ClrCanonicalFunctions.IndexofFunctionName: - result = AllowedFunctions.IndexOf; - break; - case ClrCanonicalFunctions.IsofFunctionName: - result = AllowedFunctions.IsOf; - break; - case ClrCanonicalFunctions.LengthFunctionName: - result = AllowedFunctions.Length; - break; - case ClrCanonicalFunctions.MatchesPatternFunctionName: - result = AllowedFunctions.MatchesPattern; - break; - case ClrCanonicalFunctions.MinuteFunctionName: - result = AllowedFunctions.Minute; - break; - case ClrCanonicalFunctions.MonthFunctionName: - result = AllowedFunctions.Month; - break; - case ClrCanonicalFunctions.RoundFunctionName: - result = AllowedFunctions.Round; - break; - case ClrCanonicalFunctions.SecondFunctionName: - result = AllowedFunctions.Second; - break; - case ClrCanonicalFunctions.StartswithFunctionName: - result = AllowedFunctions.StartsWith; - break; - case ClrCanonicalFunctions.SubstringFunctionName: - result = AllowedFunctions.Substring; - break; - case ClrCanonicalFunctions.TolowerFunctionName: - result = AllowedFunctions.ToLower; - break; - case ClrCanonicalFunctions.ToupperFunctionName: - result = AllowedFunctions.ToUpper; - break; - case ClrCanonicalFunctions.TrimFunctionName: - result = AllowedFunctions.Trim; - break; - case ClrCanonicalFunctions.YearFunctionName: - result = AllowedFunctions.Year; - break; - case ClrCanonicalFunctions.DateFunctionName: - result = AllowedFunctions.Date; - break; - case ClrCanonicalFunctions.TimeFunctionName: - result = AllowedFunctions.Time; - break; - case ClrCanonicalFunctions.FractionalSecondsFunctionName: - result = AllowedFunctions.FractionalSeconds; - break; - default: - // Originally, we think it should never be here. - // But, ODL supports the customized function, if we are here, it might mean it's a customized function. - // Since we don't have configuration for customized function, let's return the 'None'. - break; - } - - return result; + case "any": + result = AllowedFunctions.Any; + break; + case "all": + result = AllowedFunctions.All; + break; + case "cast": + result = AllowedFunctions.Cast; + break; + case ClrCanonicalFunctions.CeilingFunctionName: + result = AllowedFunctions.Ceiling; + break; + case ClrCanonicalFunctions.ConcatFunctionName: + result = AllowedFunctions.Concat; + break; + case ClrCanonicalFunctions.ContainsFunctionName: + result = AllowedFunctions.Contains; + break; + case ClrCanonicalFunctions.DayFunctionName: + result = AllowedFunctions.Day; + break; + case ClrCanonicalFunctions.EndswithFunctionName: + result = AllowedFunctions.EndsWith; + break; + case ClrCanonicalFunctions.FloorFunctionName: + result = AllowedFunctions.Floor; + break; + case ClrCanonicalFunctions.HourFunctionName: + result = AllowedFunctions.Hour; + break; + case ClrCanonicalFunctions.IndexofFunctionName: + result = AllowedFunctions.IndexOf; + break; + case ClrCanonicalFunctions.IsofFunctionName: + result = AllowedFunctions.IsOf; + break; + case ClrCanonicalFunctions.LengthFunctionName: + result = AllowedFunctions.Length; + break; + case ClrCanonicalFunctions.MatchesPatternFunctionName: + result = AllowedFunctions.MatchesPattern; + break; + case ClrCanonicalFunctions.MinuteFunctionName: + result = AllowedFunctions.Minute; + break; + case ClrCanonicalFunctions.MonthFunctionName: + result = AllowedFunctions.Month; + break; + case ClrCanonicalFunctions.RoundFunctionName: + result = AllowedFunctions.Round; + break; + case ClrCanonicalFunctions.SecondFunctionName: + result = AllowedFunctions.Second; + break; + case ClrCanonicalFunctions.StartswithFunctionName: + result = AllowedFunctions.StartsWith; + break; + case ClrCanonicalFunctions.SubstringFunctionName: + result = AllowedFunctions.Substring; + break; + case ClrCanonicalFunctions.TolowerFunctionName: + result = AllowedFunctions.ToLower; + break; + case ClrCanonicalFunctions.ToupperFunctionName: + result = AllowedFunctions.ToUpper; + break; + case ClrCanonicalFunctions.TrimFunctionName: + result = AllowedFunctions.Trim; + break; + case ClrCanonicalFunctions.YearFunctionName: + result = AllowedFunctions.Year; + break; + case ClrCanonicalFunctions.DateFunctionName: + result = AllowedFunctions.Date; + break; + case ClrCanonicalFunctions.TimeFunctionName: + result = AllowedFunctions.Time; + break; + case ClrCanonicalFunctions.FractionalSecondsFunctionName: + result = AllowedFunctions.FractionalSeconds; + break; + default: + // Originally, we think it should never be here. + // But, ODL supports the customized function, if we are here, it might mean it's a customized function. + // Since we don't have configuration for customized function, let's return the 'None'. + break; } + + return result; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/SelectExpandQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/SelectExpandQueryValidator.cs index a1f0fe06b..9826814ed 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/SelectExpandQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/SelectExpandQueryValidator.cs @@ -17,564 +17,563 @@ using Microsoft.OData.UriParser; using Microsoft.OData.UriParser.Aggregation; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Represents a validator used to validate a based on the . +/// +public class SelectExpandQueryValidator : ISelectExpandQueryValidator { /// - /// Represents a validator used to validate a based on the . + /// Validates a . /// - public class SelectExpandQueryValidator : ISelectExpandQueryValidator + /// The $select and $expand query. + /// The validation settings. + public virtual void Validate(SelectExpandQueryOption selectExpandQueryOption, ODataValidationSettings validationSettings) { - /// - /// Validates a . - /// - /// The $select and $expand query. - /// The validation settings. - public virtual void Validate(SelectExpandQueryOption selectExpandQueryOption, ODataValidationSettings validationSettings) + if (selectExpandQueryOption == null) { - if (selectExpandQueryOption == null) - { - throw Error.ArgumentNull(nameof(selectExpandQueryOption)); - } - - if (validationSettings == null) - { - throw Error.ArgumentNull(nameof(validationSettings)); - } - - SelectExpandValidatorContext validatorContext = new SelectExpandValidatorContext - { - SelectExpand = selectExpandQueryOption, - Context = selectExpandQueryOption.Context, - ValidationSettings = validationSettings, - Property = selectExpandQueryOption.Context.TargetProperty, - StructuredType = selectExpandQueryOption.Context.TargetStructuredType, - CurrentDepth = 0 - }; + throw Error.ArgumentNull(nameof(selectExpandQueryOption)); + } - ValidateSelectExpand(selectExpandQueryOption.SelectExpandClause, validatorContext); + if (validationSettings == null) + { + throw Error.ArgumentNull(nameof(validationSettings)); + } - if (validationSettings.MaxExpansionDepth > 0) - { - if (selectExpandQueryOption.LevelsMaxLiteralExpansionDepth < 0) - { - selectExpandQueryOption.LevelsMaxLiteralExpansionDepth = validationSettings.MaxExpansionDepth; - } - else if (selectExpandQueryOption.LevelsMaxLiteralExpansionDepth > validationSettings.MaxExpansionDepth) - { - throw new ODataException(Error.Format( - SRResources.InvalidExpansionDepthValue, - "LevelsMaxLiteralExpansionDepth", - "MaxExpansionDepth")); - } + SelectExpandValidatorContext validatorContext = new SelectExpandValidatorContext + { + SelectExpand = selectExpandQueryOption, + Context = selectExpandQueryOption.Context, + ValidationSettings = validationSettings, + Property = selectExpandQueryOption.Context.TargetProperty, + StructuredType = selectExpandQueryOption.Context.TargetStructuredType, + CurrentDepth = 0 + }; - ValidateDepth(selectExpandQueryOption.SelectExpandClause, validationSettings.MaxExpansionDepth); - } - } + ValidateSelectExpand(selectExpandQueryOption.SelectExpandClause, validatorContext); - /// - /// Validates all select and expand items in $select and $expand. - /// For example, ~/Customers?$expand=Nav($expand=subNav;$select=Prop;$top=2)&$select=Addresses($select=City;$top=1) - /// - /// The $select and $expand clause. - /// The validator context. - protected virtual void ValidateSelectExpand(SelectExpandClause selectExpandClause, SelectExpandValidatorContext validatorContext) + if (validationSettings.MaxExpansionDepth > 0) { - if (selectExpandClause == null) + if (selectExpandQueryOption.LevelsMaxLiteralExpansionDepth < 0) { - return; + selectExpandQueryOption.LevelsMaxLiteralExpansionDepth = validationSettings.MaxExpansionDepth; } - - foreach (SelectItem selectItem in selectExpandClause.SelectedItems) + else if (selectExpandQueryOption.LevelsMaxLiteralExpansionDepth > validationSettings.MaxExpansionDepth) { - if (selectItem is ExpandedNavigationSelectItem expandedSelectItem) - { - // $expand=Nav - ValidateExpandedNavigationSelectItem(expandedSelectItem, validatorContext); - } - else if (selectItem is ExpandedCountSelectItem expandedCountSelectItem) - { - // $expand=Nav/$count - ValidateExpandedCountSelectItem(expandedCountSelectItem, validatorContext); - } - else if (selectItem is PathSelectItem pathSelectItem) - { - // $select=Prop - ValidatePathSelectItem(pathSelectItem, validatorContext); - } - else if (selectItem is WildcardSelectItem wildCardSelectItem) - { - // $select=* - ValidateWildcardSelectItem(wildCardSelectItem, validatorContext); - } - else if (selectItem is NamespaceQualifiedWildcardSelectItem namespaceQualifiedWildcardSelectItem) - { - // $select=NS.* - ValidateNamespaceQualifiedWildcardSelectItem(namespaceQualifiedWildcardSelectItem, validatorContext); - } - else if (selectItem is ExpandedReferenceSelectItem referSelectItem) // Let ExpandedReferenceSelectItem be the last one. - { - // $expand=Nav/$ref - ValidateExpandedReferenceSelectItem(referSelectItem, validatorContext); - } + throw new ODataException(Error.Format( + SRResources.InvalidExpansionDepthValue, + "LevelsMaxLiteralExpansionDepth", + "MaxExpansionDepth")); } + + ValidateDepth(selectExpandQueryOption.SelectExpandClause, validationSettings.MaxExpansionDepth); + } + } + + /// + /// Validates all select and expand items in $select and $expand. + /// For example, ~/Customers?$expand=Nav($expand=subNav;$select=Prop;$top=2)&$select=Addresses($select=City;$top=1) + /// + /// The $select and $expand clause. + /// The validator context. + protected virtual void ValidateSelectExpand(SelectExpandClause selectExpandClause, SelectExpandValidatorContext validatorContext) + { + if (selectExpandClause == null) + { + return; } - /// - /// Validates one $expand. For example, ~/Customers?$expand=Nav($expand=subNav;$select=Prop;$top=2) - /// - /// One $expand clause. - /// The validator context. - /// The thrown exception. - protected virtual void ValidateExpandedNavigationSelectItem(ExpandedNavigationSelectItem expandItem, SelectExpandValidatorContext validatorContext) + foreach (SelectItem selectItem in selectExpandClause.SelectedItems) { - if (expandItem == null) + if (selectItem is ExpandedNavigationSelectItem expandedSelectItem) { - return; + // $expand=Nav + ValidateExpandedNavigationSelectItem(expandedSelectItem, validatorContext); } - - if (validatorContext.RemainingDepth != null && validatorContext.RemainingDepth <= 0) + else if (selectItem is ExpandedCountSelectItem expandedCountSelectItem) { - throw new ODataException( - Error.Format(SRResources.MaxExpandDepthExceeded, validatorContext.CurrentDepth, "MaxExpansionDepth")); + // $expand=Nav/$count + ValidateExpandedCountSelectItem(expandedCountSelectItem, validatorContext); } - - ODataValidationSettings validationSettings = validatorContext.ValidationSettings; - int currentDepth = validatorContext.CurrentDepth; - - // if validationSettings.MaxExpansionDepth <= 0, It means to disable the maximum expansion depth check. - // if currentDepth (starting 0) is bigger than max depth, throw exception. - if (validationSettings.MaxExpansionDepth > 0 && currentDepth > validationSettings.MaxExpansionDepth) + else if (selectItem is PathSelectItem pathSelectItem) { - throw new ODataException(Error.Format(SRResources.MaxExpandDepthExceeded, validationSettings.MaxExpansionDepth, "MaxExpansionDepth")); + // $select=Prop + ValidatePathSelectItem(pathSelectItem, validatorContext); } - - // $expand=a/b/NS.C/NavProp - NavigationPropertySegment navigationSegment = (NavigationPropertySegment)expandItem.PathToNavigationProperty.LastSegment; - IEdmNavigationProperty property = navigationSegment.NavigationProperty; - - IEdmModel edmModel = validatorContext.Context.Model; - // Check the "old" NotExpandable configuration on the property. Could be remove later. - if (EdmHelpers.IsNotExpandable(property, edmModel)) + else if (selectItem is WildcardSelectItem wildCardSelectItem) { - throw new ODataException(Error.Format(SRResources.NotExpandablePropertyUsedInExpand, property.Name)); + // $select=* + ValidateWildcardSelectItem(wildCardSelectItem, validatorContext); } - - int? remainDepth = validatorContext.RemainingDepth; - // Check the "new" model query configuation on the type and property. - // The following logic looks weird (I copied from existing codes with small changes). - // TODO: We should combine them (old/new) together and figure out a whole solution for the query configuration on the model. - bool isExpandable; - ExpandConfiguration expandConfiguration; - isExpandable = EdmHelpers.IsExpandable(property.Name, validatorContext.Property, validatorContext.StructuredType, edmModel, out expandConfiguration); - if (isExpandable) + else if (selectItem is NamespaceQualifiedWildcardSelectItem namespaceQualifiedWildcardSelectItem) { - int maxDepth = expandConfiguration.MaxDepth; - if (maxDepth > 0 && (validatorContext.RemainingDepth == null || maxDepth < validatorContext.RemainingDepth)) - { - remainDepth = maxDepth; - } + // $select=NS.* + ValidateNamespaceQualifiedWildcardSelectItem(namespaceQualifiedWildcardSelectItem, validatorContext); } - else if (!isExpandable) + else if (selectItem is ExpandedReferenceSelectItem referSelectItem) // Let ExpandedReferenceSelectItem be the last one. { - if (!validatorContext.Context.DefaultQueryConfigurations.EnableExpand || - (expandConfiguration != null && expandConfiguration.ExpandType == SelectExpandType.Disabled)) - { - throw new ODataException(Error.Format(SRResources.NotExpandablePropertyUsedInExpand, property.Name)); - } + // $expand=Nav/$ref + ValidateExpandedReferenceSelectItem(referSelectItem, validatorContext); } + } + } - // Move to next level within $expand. First, let's update the applied property and related structured type. - SelectExpandValidatorContext subValidatorContext = validatorContext.Clone(); - subValidatorContext.Property = property; - subValidatorContext.StructuredType = property.ToEntityType(); - subValidatorContext.CurrentDepth = validatorContext.CurrentDepth + 1; - subValidatorContext.RemainingDepth = remainDepth != null ? remainDepth - 1 : null; - - // Validate the nested $select and $expand within $expand - ValidateSelectExpand(expandItem.SelectAndExpand, subValidatorContext); - - // Validate the nested $filter within $expand - ValidateNestedFilter(expandItem.FilterOption, subValidatorContext); - - // Validate the nested $orderby within $expand - ValidateNestedOrderby(expandItem.OrderByOption, subValidatorContext); - - // Validate the nested $top within $expand - ValidateNestedTop(expandItem.TopOption, subValidatorContext); - - // Validate the nested $skip within $expand - ValidateNestedSkip(expandItem.SkipOption, subValidatorContext); - - // Validate the nested $count within $expand - ValidateNestedCount(expandItem.CountOption, subValidatorContext); - - // Validate the nested $search within $expand - ValidateNestedSearch(expandItem.SearchOption, subValidatorContext); + /// + /// Validates one $expand. For example, ~/Customers?$expand=Nav($expand=subNav;$select=Prop;$top=2) + /// + /// One $expand clause. + /// The validator context. + /// The thrown exception. + protected virtual void ValidateExpandedNavigationSelectItem(ExpandedNavigationSelectItem expandItem, SelectExpandValidatorContext validatorContext) + { + if (expandItem == null) + { + return; + } - // Validate the nested $levels within $expand - ValidateNestedLevels(expandItem.LevelsOption, subValidatorContext); + if (validatorContext.RemainingDepth != null && validatorContext.RemainingDepth <= 0) + { + throw new ODataException( + Error.Format(SRResources.MaxExpandDepthExceeded, validatorContext.CurrentDepth, "MaxExpansionDepth")); + } - // Validate the nested $compute within $expand - ValidateNestedCompute(expandItem.ComputeOption, subValidatorContext); + ODataValidationSettings validationSettings = validatorContext.ValidationSettings; + int currentDepth = validatorContext.CurrentDepth; - // Validate the nested $apply within $expand - ValidateNestedApply(expandItem.ApplyOption, subValidatorContext); + // if validationSettings.MaxExpansionDepth <= 0, It means to disable the maximum expansion depth check. + // if currentDepth (starting 0) is bigger than max depth, throw exception. + if (validationSettings.MaxExpansionDepth > 0 && currentDepth > validationSettings.MaxExpansionDepth) + { + throw new ODataException(Error.Format(SRResources.MaxExpandDepthExceeded, validationSettings.MaxExpansionDepth, "MaxExpansionDepth")); } - /// - /// Validates one expand count. For example, ~/Customers?$expand=Nav/$count - /// - /// The expand count item. - /// The validator context. - protected virtual void ValidateExpandedCountSelectItem(ExpandedCountSelectItem expandCountItem, SelectExpandValidatorContext validatorContext) + // $expand=a/b/NS.C/NavProp + NavigationPropertySegment navigationSegment = (NavigationPropertySegment)expandItem.PathToNavigationProperty.LastSegment; + IEdmNavigationProperty property = navigationSegment.NavigationProperty; + + IEdmModel edmModel = validatorContext.Context.Model; + // Check the "old" NotExpandable configuration on the property. Could be remove later. + if (EdmHelpers.IsNotExpandable(property, edmModel)) { - // So far, No default validation logic here. + throw new ODataException(Error.Format(SRResources.NotExpandablePropertyUsedInExpand, property.Name)); } - /// - /// Validates one expand reference. For example, ~/Customers?$expand=Nav/$ref - /// - /// The expand reference item. - /// The validator context. - protected virtual void ValidateExpandedReferenceSelectItem(ExpandedReferenceSelectItem expandReferItem, SelectExpandValidatorContext validatorContext) + int? remainDepth = validatorContext.RemainingDepth; + // Check the "new" model query configuation on the type and property. + // The following logic looks weird (I copied from existing codes with small changes). + // TODO: We should combine them (old/new) together and figure out a whole solution for the query configuration on the model. + bool isExpandable; + ExpandConfiguration expandConfiguration; + isExpandable = EdmHelpers.IsExpandable(property.Name, validatorContext.Property, validatorContext.StructuredType, edmModel, out expandConfiguration); + if (isExpandable) { - // So far, No default validation logic here. + int maxDepth = expandConfiguration.MaxDepth; + if (maxDepth > 0 && (validatorContext.RemainingDepth == null || maxDepth < validatorContext.RemainingDepth)) + { + remainDepth = maxDepth; + } } - - /// - /// Validates $select. For example, ~/Customers?$select=Prop($select=SubProp;$top=2) - /// - /// The $select item. - /// The validator context. - /// The thrown exception. - protected virtual void ValidatePathSelectItem(PathSelectItem pathSelectItem, SelectExpandValidatorContext validatorContext) + else if (!isExpandable) { - if (pathSelectItem == null) + if (!validatorContext.Context.DefaultQueryConfigurations.EnableExpand || + (expandConfiguration != null && expandConfiguration.ExpandType == SelectExpandType.Disabled)) { - return; + throw new ODataException(Error.Format(SRResources.NotExpandablePropertyUsedInExpand, property.Name)); } + } - IEdmModel edmModel = validatorContext.Context.Model; - bool enableSelect = validatorContext.Context.DefaultQueryConfigurations.EnableSelect; - ODataPathSegment segment = pathSelectItem.SelectedPath.LastSegment; + // Move to next level within $expand. First, let's update the applied property and related structured type. + SelectExpandValidatorContext subValidatorContext = validatorContext.Clone(); + subValidatorContext.Property = property; + subValidatorContext.StructuredType = property.ToEntityType(); + subValidatorContext.CurrentDepth = validatorContext.CurrentDepth + 1; + subValidatorContext.RemainingDepth = remainDepth != null ? remainDepth - 1 : null; - IEdmProperty property = validatorContext.Property; - IEdmStructuredType structuredType = validatorContext.StructuredType; + // Validate the nested $select and $expand within $expand + ValidateSelectExpand(expandItem.SelectAndExpand, subValidatorContext); - if (segment is NavigationPropertySegment navigationPropertySegment) - { - IEdmNavigationProperty navProperty = navigationPropertySegment.NavigationProperty; - if (EdmHelpers.IsNotNavigable(navProperty, edmModel)) - { - throw new ODataException(Error.Format(SRResources.NotNavigablePropertyUsedInNavigation, navProperty.Name)); - } + // Validate the nested $filter within $expand + ValidateNestedFilter(expandItem.FilterOption, subValidatorContext); - property = navProperty; - structuredType = navProperty.ToEntityType(); - } - else if (segment is PropertySegment propertySegment) - { - if (EdmHelpers.IsNotSelectable(propertySegment.Property, property, structuredType, edmModel, enableSelect)) - { - throw new ODataException(Error.Format(SRResources.NotSelectablePropertyUsedInSelect, propertySegment.Property.Name)); - } + // Validate the nested $orderby within $expand + ValidateNestedOrderby(expandItem.OrderByOption, subValidatorContext); - property = propertySegment.Property; - structuredType = propertySegment.Property.Type.ToStructuredType(); - } - else - { - return; - } + // Validate the nested $top within $expand + ValidateNestedTop(expandItem.TopOption, subValidatorContext); - // Move to next level within $select. Let's update the applied property and related structured type. - SelectExpandValidatorContext subValidatorContext = validatorContext.Clone(); - subValidatorContext.Property = property; - subValidatorContext.StructuredType = structuredType; - subValidatorContext.CurrentDepth = validatorContext.CurrentDepth + 1; + // Validate the nested $skip within $expand + ValidateNestedSkip(expandItem.SkipOption, subValidatorContext); - // Validate the nested $select within $select - ValidateSelectExpand(pathSelectItem.SelectAndExpand, subValidatorContext); + // Validate the nested $count within $expand + ValidateNestedCount(expandItem.CountOption, subValidatorContext); - // Validate the nested $filter within $select - ValidateNestedFilter(pathSelectItem.FilterOption, subValidatorContext); + // Validate the nested $search within $expand + ValidateNestedSearch(expandItem.SearchOption, subValidatorContext); - // Validate the nested $orderby within $select - ValidateNestedOrderby(pathSelectItem.OrderByOption, subValidatorContext); + // Validate the nested $levels within $expand + ValidateNestedLevels(expandItem.LevelsOption, subValidatorContext); - // Validate the nested $top within $select - ValidateNestedTop(pathSelectItem.TopOption, subValidatorContext); + // Validate the nested $compute within $expand + ValidateNestedCompute(expandItem.ComputeOption, subValidatorContext); - // Validate the nested $skip within $select - ValidateNestedSkip(pathSelectItem.SkipOption, subValidatorContext); + // Validate the nested $apply within $expand + ValidateNestedApply(expandItem.ApplyOption, subValidatorContext); + } - // Validate the nested $count within $select - ValidateNestedCount(pathSelectItem.CountOption, subValidatorContext); + /// + /// Validates one expand count. For example, ~/Customers?$expand=Nav/$count + /// + /// The expand count item. + /// The validator context. + protected virtual void ValidateExpandedCountSelectItem(ExpandedCountSelectItem expandCountItem, SelectExpandValidatorContext validatorContext) + { + // So far, No default validation logic here. + } - // Validate the nested $search within $select - ValidateNestedSearch(pathSelectItem.SearchOption, subValidatorContext); + /// + /// Validates one expand reference. For example, ~/Customers?$expand=Nav/$ref + /// + /// The expand reference item. + /// The validator context. + protected virtual void ValidateExpandedReferenceSelectItem(ExpandedReferenceSelectItem expandReferItem, SelectExpandValidatorContext validatorContext) + { + // So far, No default validation logic here. + } - // Validate the nested $compute within $select - ValidateNestedCompute(pathSelectItem.ComputeOption, subValidatorContext); + /// + /// Validates $select. For example, ~/Customers?$select=Prop($select=SubProp;$top=2) + /// + /// The $select item. + /// The validator context. + /// The thrown exception. + protected virtual void ValidatePathSelectItem(PathSelectItem pathSelectItem, SelectExpandValidatorContext validatorContext) + { + if (pathSelectItem == null) + { + return; } - /// - /// Validates $select wildcard. For example, ~/Customers?$select=* - /// - /// The wildcard select item. - /// The validator context. - /// The thrown exception. - protected virtual void ValidateWildcardSelectItem(WildcardSelectItem wildCardSelectItem, SelectExpandValidatorContext validatorContext) + IEdmModel edmModel = validatorContext.Context.Model; + bool enableSelect = validatorContext.Context.DefaultQueryConfigurations.EnableSelect; + ODataPathSegment segment = pathSelectItem.SelectedPath.LastSegment; + + IEdmProperty property = validatorContext.Property; + IEdmStructuredType structuredType = validatorContext.StructuredType; + + if (segment is NavigationPropertySegment navigationPropertySegment) { - if (wildCardSelectItem == null) + IEdmNavigationProperty navProperty = navigationPropertySegment.NavigationProperty; + if (EdmHelpers.IsNotNavigable(navProperty, edmModel)) { - return; + throw new ODataException(Error.Format(SRResources.NotNavigablePropertyUsedInNavigation, navProperty.Name)); } - IEdmStructuredType structuredType = validatorContext.StructuredType; - IEdmModel edmModel = validatorContext.Context.Model; - IEdmProperty pathProperty = validatorContext.Property; - - foreach (var property in structuredType.StructuralProperties()) + property = navProperty; + structuredType = navProperty.ToEntityType(); + } + else if (segment is PropertySegment propertySegment) + { + if (EdmHelpers.IsNotSelectable(propertySegment.Property, property, structuredType, edmModel, enableSelect)) { - if (EdmHelpers.IsNotSelectable(property, pathProperty, structuredType, edmModel, - validatorContext.Context.DefaultQueryConfigurations.EnableSelect)) - { - throw new ODataException(Error.Format(SRResources.NotSelectablePropertyUsedInSelect, property.Name)); - } + throw new ODataException(Error.Format(SRResources.NotSelectablePropertyUsedInSelect, propertySegment.Property.Name)); } - } - /// - /// Validates $select namespace wildcard. For example, ~/Customers?$select=NS.* - /// - /// The namespace wildcard select item. - /// The validator context. - /// The thrown exception. - protected virtual void ValidateNamespaceQualifiedWildcardSelectItem( - NamespaceQualifiedWildcardSelectItem namespaceQualifiedWildcardSelectItem, - SelectExpandValidatorContext validatorContext) + property = propertySegment.Property; + structuredType = propertySegment.Property.Type.ToStructuredType(); + } + else { - // So far, No default validation logic here. + return; } - /// - /// Validates $filter within $select or $expand - /// - /// The nested $filter clause. - /// The validator context. - protected virtual void ValidateNestedFilter(FilterClause filterClause, SelectExpandValidatorContext validatorContext) - { - if (filterClause == null) - { - return; - } + // Move to next level within $select. Let's update the applied property and related structured type. + SelectExpandValidatorContext subValidatorContext = validatorContext.Clone(); + subValidatorContext.Property = property; + subValidatorContext.StructuredType = structuredType; + subValidatorContext.CurrentDepth = validatorContext.CurrentDepth + 1; - // It seems the query validator interface should take the AST as input, but now we have the XXXQueryOption. - // Here's the workaround, we should change it later. - IFilterQueryValidator filterValidator = validatorContext.Context.GetFilterQueryValidator(); + // Validate the nested $select within $select + ValidateSelectExpand(pathSelectItem.SelectAndExpand, subValidatorContext); - ODataQueryContext queryContext = new ODataQueryContext - { - Request = validatorContext.Context.Request, - RequestContainer = validatorContext.Context.RequestContainer, - Model = validatorContext.Context.Model, - TargetProperty = validatorContext.Property, - TargetStructuredType = validatorContext.StructuredType - }; + // Validate the nested $filter within $select + ValidateNestedFilter(pathSelectItem.FilterOption, subValidatorContext); - FilterQueryOption filterQueryOption = new FilterQueryOption(queryContext, filterClause); + // Validate the nested $orderby within $select + ValidateNestedOrderby(pathSelectItem.OrderByOption, subValidatorContext); - filterValidator.Validate(filterQueryOption, validatorContext.ValidationSettings); - } + // Validate the nested $top within $select + ValidateNestedTop(pathSelectItem.TopOption, subValidatorContext); - /// - /// Validates $orderby within $select or $expand - /// - /// The nested $orderby clause. - /// The validator context. - protected virtual void ValidateNestedOrderby(OrderByClause orderByClause, SelectExpandValidatorContext validatorContext) - { - if (orderByClause != null) - { - // TODO: OrderByModelLimitationsValidator is used already. but we should use IOrderbyQueryValidator to validate. - // Should change it later. - OrderByModelLimitationsValidator orderByQueryValidator = - new OrderByModelLimitationsValidator(validatorContext.Context, validatorContext.Context.DefaultQueryConfigurations.EnableOrderBy); + // Validate the nested $skip within $select + ValidateNestedSkip(pathSelectItem.SkipOption, subValidatorContext); - orderByQueryValidator.TryValidate(validatorContext.Property, validatorContext.StructuredType, orderByClause, false); - } - } + // Validate the nested $count within $select + ValidateNestedCount(pathSelectItem.CountOption, subValidatorContext); + + // Validate the nested $search within $select + ValidateNestedSearch(pathSelectItem.SearchOption, subValidatorContext); + + // Validate the nested $compute within $select + ValidateNestedCompute(pathSelectItem.ComputeOption, subValidatorContext); + } - /// - /// Validates $top within $select or $expand - /// - /// The nested $top clause. - /// The validator context. - protected virtual void ValidateNestedTop(long? topOption, SelectExpandValidatorContext validatorContext) + /// + /// Validates $select wildcard. For example, ~/Customers?$select=* + /// + /// The wildcard select item. + /// The validator context. + /// The thrown exception. + protected virtual void ValidateWildcardSelectItem(WildcardSelectItem wildCardSelectItem, SelectExpandValidatorContext validatorContext) + { + if (wildCardSelectItem == null) { - if (topOption != null) - { - Contract.Assert(topOption.Value <= Int32.MaxValue); + return; + } - IEdmModel edmModel = validatorContext.Context.Model; - IEdmProperty property = validatorContext.Property; - IEdmStructuredType structuredType = validatorContext.StructuredType; - DefaultQueryConfigurations configs = validatorContext.Context.DefaultQueryConfigurations; + IEdmStructuredType structuredType = validatorContext.StructuredType; + IEdmModel edmModel = validatorContext.Context.Model; + IEdmProperty pathProperty = validatorContext.Property; - int maxTop; - if (EdmHelpers.IsTopLimitExceeded(property, structuredType, edmModel, (int)topOption.Value, configs, out maxTop)) - { - throw new ODataException(Error.Format(SRResources.SkipTopLimitExceeded, maxTop, AllowedQueryOptions.Top, topOption.Value)); - } + foreach (var property in structuredType.StructuralProperties()) + { + if (EdmHelpers.IsNotSelectable(property, pathProperty, structuredType, edmModel, + validatorContext.Context.DefaultQueryConfigurations.EnableSelect)) + { + throw new ODataException(Error.Format(SRResources.NotSelectablePropertyUsedInSelect, property.Name)); } } + } - /// - /// Validates $skip within $select or $expand - /// - /// The nested $skip clause. - /// The validator context. - protected virtual void ValidateNestedSkip(long? skipOption, SelectExpandValidatorContext validatorContext) + /// + /// Validates $select namespace wildcard. For example, ~/Customers?$select=NS.* + /// + /// The namespace wildcard select item. + /// The validator context. + /// The thrown exception. + protected virtual void ValidateNamespaceQualifiedWildcardSelectItem( + NamespaceQualifiedWildcardSelectItem namespaceQualifiedWildcardSelectItem, + SelectExpandValidatorContext validatorContext) + { + // So far, No default validation logic here. + } + + /// + /// Validates $filter within $select or $expand + /// + /// The nested $filter clause. + /// The validator context. + protected virtual void ValidateNestedFilter(FilterClause filterClause, SelectExpandValidatorContext validatorContext) + { + if (filterClause == null) { - // Nothing here. + return; } - /// - /// Validates $count within $select or $expand - /// - /// The nested $count clause. - /// The validator context. - protected virtual void ValidateNestedCount(bool? countOption, SelectExpandValidatorContext validatorContext) + // It seems the query validator interface should take the AST as input, but now we have the XXXQueryOption. + // Here's the workaround, we should change it later. + IFilterQueryValidator filterValidator = validatorContext.Context.GetFilterQueryValidator(); + + ODataQueryContext queryContext = new ODataQueryContext { - if (countOption != null && countOption.Value) - { - IEdmModel edmModel = validatorContext.Context.Model; - IEdmProperty property = validatorContext.Property; - IEdmStructuredType structuredType = validatorContext.StructuredType; - DefaultQueryConfigurations configs = validatorContext.Context.DefaultQueryConfigurations; + Request = validatorContext.Context.Request, + RequestContainer = validatorContext.Context.RequestContainer, + Model = validatorContext.Context.Model, + TargetProperty = validatorContext.Property, + TargetStructuredType = validatorContext.StructuredType + }; - if (EdmHelpers.IsNotCountable(property, structuredType, edmModel, configs.EnableCount)) - { - throw new ODataException(Error.Format(SRResources.NotCountablePropertyUsedForCount, property.Name)); - } - } + FilterQueryOption filterQueryOption = new FilterQueryOption(queryContext, filterClause); + + filterValidator.Validate(filterQueryOption, validatorContext.ValidationSettings); + } + + /// + /// Validates $orderby within $select or $expand + /// + /// The nested $orderby clause. + /// The validator context. + protected virtual void ValidateNestedOrderby(OrderByClause orderByClause, SelectExpandValidatorContext validatorContext) + { + if (orderByClause != null) + { + // TODO: OrderByModelLimitationsValidator is used already. but we should use IOrderbyQueryValidator to validate. + // Should change it later. + OrderByModelLimitationsValidator orderByQueryValidator = + new OrderByModelLimitationsValidator(validatorContext.Context, validatorContext.Context.DefaultQueryConfigurations.EnableOrderBy); + + orderByQueryValidator.TryValidate(validatorContext.Property, validatorContext.StructuredType, orderByClause, false); } + } - /// - /// Validates $levels within $expand - /// - /// The nested $levels clause. - /// The validator context. - protected virtual void ValidateNestedLevels(LevelsClause levelsClause, SelectExpandValidatorContext validatorContext) + /// + /// Validates $top within $select or $expand + /// + /// The nested $top clause. + /// The validator context. + protected virtual void ValidateNestedTop(long? topOption, SelectExpandValidatorContext validatorContext) + { + if (topOption != null) { - if (levelsClause == null) - { - return; - } + Contract.Assert(topOption.Value <= Int32.MaxValue); IEdmModel edmModel = validatorContext.Context.Model; - int depth = validatorContext.ValidationSettings.MaxExpansionDepth; IEdmProperty property = validatorContext.Property; - //Contract.Assert(property != null); // $levels only on navigation property ? - int currentDepth = validatorContext.CurrentDepth; + IEdmStructuredType structuredType = validatorContext.StructuredType; + DefaultQueryConfigurations configs = validatorContext.Context.DefaultQueryConfigurations; - ExpandConfiguration expandConfiguration; - bool isExpandable = EdmHelpers.IsExpandable(property.Name, property, validatorContext.StructuredType, edmModel, out expandConfiguration); - if (isExpandable) + int maxTop; + if (EdmHelpers.IsTopLimitExceeded(property, structuredType, edmModel, (int)topOption.Value, configs, out maxTop)) { - int maxDepth = expandConfiguration.MaxDepth; - if (maxDepth > 0 && maxDepth < depth) - { - depth = maxDepth; - } - - if ((depth == 0 && levelsClause.IsMaxLevel) || (depth < levelsClause.Level)) - { - throw new ODataException(Error.Format(SRResources.MaxExpandDepthExceeded, currentDepth + depth, "MaxExpansionDepth")); - } + throw new ODataException(Error.Format(SRResources.SkipTopLimitExceeded, maxTop, AllowedQueryOptions.Top, topOption.Value)); } - else + } + } + + /// + /// Validates $skip within $select or $expand + /// + /// The nested $skip clause. + /// The validator context. + protected virtual void ValidateNestedSkip(long? skipOption, SelectExpandValidatorContext validatorContext) + { + // Nothing here. + } + + /// + /// Validates $count within $select or $expand + /// + /// The nested $count clause. + /// The validator context. + protected virtual void ValidateNestedCount(bool? countOption, SelectExpandValidatorContext validatorContext) + { + if (countOption != null && countOption.Value) + { + IEdmModel edmModel = validatorContext.Context.Model; + IEdmProperty property = validatorContext.Property; + IEdmStructuredType structuredType = validatorContext.StructuredType; + DefaultQueryConfigurations configs = validatorContext.Context.DefaultQueryConfigurations; + + if (EdmHelpers.IsNotCountable(property, structuredType, edmModel, configs.EnableCount)) { - if (!validatorContext.Context.DefaultQueryConfigurations.EnableExpand || - (expandConfiguration != null && expandConfiguration.ExpandType == SelectExpandType.Disabled)) - { - throw new ODataException(Error.Format(SRResources.NotExpandablePropertyUsedInExpand, property.Name)); - } + throw new ODataException(Error.Format(SRResources.NotCountablePropertyUsedForCount, property.Name)); } } + } - /// - /// Validates $search within $select or $expand - /// - /// The nested $search clause. - /// The validator context. - protected virtual void ValidateNestedSearch(SearchClause searchClause, SelectExpandValidatorContext validatorContext) + /// + /// Validates $levels within $expand + /// + /// The nested $levels clause. + /// The validator context. + protected virtual void ValidateNestedLevels(LevelsClause levelsClause, SelectExpandValidatorContext validatorContext) + { + if (levelsClause == null) { - // Add logics here to verify nested $search. So far, No default validation logic here. + return; } - /// - /// Validates $compute within $expand - /// - /// The nested $compute clause. - /// The validator context. - protected virtual void ValidateNestedCompute(ComputeClause computeClause, SelectExpandValidatorContext validatorContext) + IEdmModel edmModel = validatorContext.Context.Model; + int depth = validatorContext.ValidationSettings.MaxExpansionDepth; + IEdmProperty property = validatorContext.Property; + //Contract.Assert(property != null); // $levels only on navigation property ? + int currentDepth = validatorContext.CurrentDepth; + + ExpandConfiguration expandConfiguration; + bool isExpandable = EdmHelpers.IsExpandable(property.Name, property, validatorContext.StructuredType, edmModel, out expandConfiguration); + if (isExpandable) { - // Add logics here to verify nested $compute. So far, No default validation logic here. - } + int maxDepth = expandConfiguration.MaxDepth; + if (maxDepth > 0 && maxDepth < depth) + { + depth = maxDepth; + } - /// - /// Validates $apply within $expand - /// - /// The nested $apply clause. - /// The validator context. - protected virtual void ValidateNestedApply(ApplyClause applyClause, SelectExpandValidatorContext validatorContext) + if ((depth == 0 && levelsClause.IsMaxLevel) || (depth < levelsClause.Level)) + { + throw new ODataException(Error.Format(SRResources.MaxExpandDepthExceeded, currentDepth + depth, "MaxExpansionDepth")); + } + } + else { - // Add logics here to verify nested $apply. So far, No default validation logic here. + if (!validatorContext.Context.DefaultQueryConfigurations.EnableExpand || + (expandConfiguration != null && expandConfiguration.ExpandType == SelectExpandType.Disabled)) + { + throw new ODataException(Error.Format(SRResources.NotExpandablePropertyUsedInExpand, property.Name)); + } } + } + + /// + /// Validates $search within $select or $expand + /// + /// The nested $search clause. + /// The validator context. + protected virtual void ValidateNestedSearch(SearchClause searchClause, SelectExpandValidatorContext validatorContext) + { + // Add logics here to verify nested $search. So far, No default validation logic here. + } + + /// + /// Validates $compute within $expand + /// + /// The nested $compute clause. + /// The validator context. + protected virtual void ValidateNestedCompute(ComputeClause computeClause, SelectExpandValidatorContext validatorContext) + { + // Add logics here to verify nested $compute. So far, No default validation logic here. + } + + /// + /// Validates $apply within $expand + /// + /// The nested $apply clause. + /// The validator context. + protected virtual void ValidateNestedApply(ApplyClause applyClause, SelectExpandValidatorContext validatorContext) + { + // Add logics here to verify nested $apply. So far, No default validation logic here. + } - private static void ValidateDepth(SelectExpandClause selectExpand, int maxDepth) + private static void ValidateDepth(SelectExpandClause selectExpand, int maxDepth) + { + // do a DFS to see if there is any node that is too deep. + Stack> nodesToVisit = new Stack>(); + nodesToVisit.Push(Tuple.Create(0, selectExpand)); + while (nodesToVisit.Count > 0) { - // do a DFS to see if there is any node that is too deep. - Stack> nodesToVisit = new Stack>(); - nodesToVisit.Push(Tuple.Create(0, selectExpand)); - while (nodesToVisit.Count > 0) + Tuple tuple = nodesToVisit.Pop(); + int currentDepth = tuple.Item1; + SelectExpandClause currentNode = tuple.Item2; + + ExpandedNavigationSelectItem[] expandItems = currentNode.SelectedItems.OfType().ToArray(); + + if (expandItems.Length > 0 && + ((currentDepth == maxDepth && + expandItems.Any(expandItem => + expandItem.LevelsOption == null || + expandItem.LevelsOption.IsMaxLevel || + expandItem.LevelsOption.Level != 0)) || + expandItems.Any(expandItem => + expandItem.LevelsOption != null && + !expandItem.LevelsOption.IsMaxLevel && + (expandItem.LevelsOption.Level > Int32.MaxValue || + expandItem.LevelsOption.Level + currentDepth > maxDepth)))) { - Tuple tuple = nodesToVisit.Pop(); - int currentDepth = tuple.Item1; - SelectExpandClause currentNode = tuple.Item2; - - ExpandedNavigationSelectItem[] expandItems = currentNode.SelectedItems.OfType().ToArray(); - - if (expandItems.Length > 0 && - ((currentDepth == maxDepth && - expandItems.Any(expandItem => - expandItem.LevelsOption == null || - expandItem.LevelsOption.IsMaxLevel || - expandItem.LevelsOption.Level != 0)) || - expandItems.Any(expandItem => - expandItem.LevelsOption != null && - !expandItem.LevelsOption.IsMaxLevel && - (expandItem.LevelsOption.Level > Int32.MaxValue || - expandItem.LevelsOption.Level + currentDepth > maxDepth)))) - { - throw new ODataException( - Error.Format(SRResources.MaxExpandDepthExceeded, maxDepth, "MaxExpansionDepth")); - } - - foreach (ExpandedNavigationSelectItem expandItem in expandItems) - { - int depth = currentDepth + 1; + throw new ODataException( + Error.Format(SRResources.MaxExpandDepthExceeded, maxDepth, "MaxExpansionDepth")); + } - if (expandItem.LevelsOption != null && !expandItem.LevelsOption.IsMaxLevel) - { - // Add the value of $levels for next depth. - depth = depth + (int)expandItem.LevelsOption.Level - 1; - } + foreach (ExpandedNavigationSelectItem expandItem in expandItems) + { + int depth = currentDepth + 1; - nodesToVisit.Push(Tuple.Create(depth, expandItem.SelectAndExpand)); + if (expandItem.LevelsOption != null && !expandItem.LevelsOption.IsMaxLevel) + { + // Add the value of $levels for next depth. + depth = depth + (int)expandItem.LevelsOption.Level - 1; } + + nodesToVisit.Push(Tuple.Create(depth, expandItem.SelectAndExpand)); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/SelectExpandValidatorContext.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/SelectExpandValidatorContext.cs index 12bb12530..a4004f862 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/SelectExpandValidatorContext.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/SelectExpandValidatorContext.cs @@ -5,40 +5,39 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// The metadata context for $select and $expand validator. +/// +public class SelectExpandValidatorContext : QueryValidatorContext { /// - /// The metadata context for $select and $expand validator. + /// The top level $select and $expand query option. /// - public class SelectExpandValidatorContext : QueryValidatorContext - { - /// - /// The top level $select and $expand query option. - /// - public SelectExpandQueryOption SelectExpand { get; set; } + public SelectExpandQueryOption SelectExpand { get; set; } - /// - /// The remaining depth on property. - /// It's weird logic in current implementation. Need to improve it later. - /// - public int? RemainingDepth { get; set; } = null; + /// + /// The remaining depth on property. + /// It's weird logic in current implementation. Need to improve it later. + /// + public int? RemainingDepth { get; set; } = null; - /// - /// Clone the context. - /// - /// The cloned context. - public SelectExpandValidatorContext Clone() + /// + /// Clone the context. + /// + /// The cloned context. + public SelectExpandValidatorContext Clone() + { + return new SelectExpandValidatorContext { - return new SelectExpandValidatorContext - { - SelectExpand = this.SelectExpand, - Context = this.Context, - ValidationSettings = this.ValidationSettings, - Property = this.Property, - StructuredType = this.StructuredType, - RemainingDepth = this.RemainingDepth, - CurrentDepth = this.CurrentDepth - }; - } + SelectExpand = this.SelectExpand, + Context = this.Context, + ValidationSettings = this.ValidationSettings, + Property = this.Property, + StructuredType = this.StructuredType, + RemainingDepth = this.RemainingDepth, + CurrentDepth = this.CurrentDepth + }; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/SkipQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/SkipQueryValidator.cs index 00a23ed0a..ca5aa126d 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/SkipQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/SkipQueryValidator.cs @@ -7,34 +7,33 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Represents a validator used to validate a based on the . +/// +public class SkipQueryValidator : ISkipQueryValidator { /// - /// Represents a validator used to validate a based on the . + /// Validates a . /// - public class SkipQueryValidator : ISkipQueryValidator + /// The $skip query. + /// The validation settings. + public virtual void Validate(SkipQueryOption skipQueryOption, ODataValidationSettings validationSettings) { - /// - /// Validates a . - /// - /// The $skip query. - /// The validation settings. - public virtual void Validate(SkipQueryOption skipQueryOption, ODataValidationSettings validationSettings) + if (skipQueryOption == null) { - if (skipQueryOption == null) - { - throw Error.ArgumentNull(nameof(skipQueryOption)); - } + throw Error.ArgumentNull(nameof(skipQueryOption)); + } - if (validationSettings == null) - { - throw Error.ArgumentNull(nameof(validationSettings)); - } + if (validationSettings == null) + { + throw Error.ArgumentNull(nameof(validationSettings)); + } - if (skipQueryOption.Value > validationSettings.MaxSkip) - { - throw new ODataException(Error.Format(SRResources.SkipTopLimitExceeded, validationSettings.MaxSkip, AllowedQueryOptions.Skip, skipQueryOption.Value)); - } + if (skipQueryOption.Value > validationSettings.MaxSkip) + { + throw new ODataException(Error.Format(SRResources.SkipTopLimitExceeded, validationSettings.MaxSkip, AllowedQueryOptions.Skip, skipQueryOption.Value)); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/SkipTokenQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/SkipTokenQueryValidator.cs index 50db52672..1fd6f7ea3 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/SkipTokenQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/SkipTokenQueryValidator.cs @@ -8,37 +8,36 @@ using Microsoft.OData; using Microsoft.OData.ModelBuilder.Config; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Represents a validator used to validate a based on the . +/// +public class SkipTokenQueryValidator : ISkipTokenQueryValidator { /// - /// Represents a validator used to validate a based on the . + /// Validates a . /// - public class SkipTokenQueryValidator : ISkipTokenQueryValidator + /// The $skiptoken query. + /// The validation settings. + public virtual void Validate(SkipTokenQueryOption skipToken, ODataValidationSettings validationSettings) { - /// - /// Validates a . - /// - /// The $skiptoken query. - /// The validation settings. - public virtual void Validate(SkipTokenQueryOption skipToken, ODataValidationSettings validationSettings) + if (skipToken == null) { - if (skipToken == null) - { - throw Error.ArgumentNull(nameof(skipToken)); - } + throw Error.ArgumentNull(nameof(skipToken)); + } - if (validationSettings == null) - { - throw Error.ArgumentNull(nameof(validationSettings)); - } + if (validationSettings == null) + { + throw Error.ArgumentNull(nameof(validationSettings)); + } - if (skipToken.Context != null) + if (skipToken.Context != null) + { + DefaultQueryConfigurations defaultConfigs = skipToken.Context.DefaultQueryConfigurations; + if (!defaultConfigs.EnableSkipToken) { - DefaultQueryConfigurations defaultConfigs = skipToken.Context.DefaultQueryConfigurations; - if (!defaultConfigs.EnableSkipToken) - { - throw new ODataException(Error.Format(SRResources.NotAllowedQueryOption, AllowedQueryOptions.SkipToken, "AllowedQueryOptions")); - } + throw new ODataException(Error.Format(SRResources.NotAllowedQueryOption, AllowedQueryOptions.SkipToken, "AllowedQueryOptions")); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/TopQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/TopQueryValidator.cs index fbc52db61..ec50c974d 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/TopQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/TopQueryValidator.cs @@ -9,50 +9,49 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Query.Validator +namespace Microsoft.AspNetCore.OData.Query.Validator; + +/// +/// Represents a validator used to validate a based on the . +/// +public class TopQueryValidator : ITopQueryValidator { /// - /// Represents a validator used to validate a based on the . + /// Validates a . /// - public class TopQueryValidator : ITopQueryValidator + /// The $top query. + /// The validation settings. + public virtual void Validate(TopQueryOption topQueryOption, ODataValidationSettings validationSettings) { - /// - /// Validates a . - /// - /// The $top query. - /// The validation settings. - public virtual void Validate(TopQueryOption topQueryOption, ODataValidationSettings validationSettings) + if (topQueryOption == null) { - if (topQueryOption == null) - { - throw Error.ArgumentNull(nameof(topQueryOption)); - } + throw Error.ArgumentNull(nameof(topQueryOption)); + } - if (validationSettings == null) - { - throw Error.ArgumentNull(nameof(validationSettings)); - } + if (validationSettings == null) + { + throw Error.ArgumentNull(nameof(validationSettings)); + } - if (topQueryOption.Value > validationSettings.MaxTop) - { - throw new ODataException(Error.Format(SRResources.SkipTopLimitExceeded, validationSettings.MaxTop, - AllowedQueryOptions.Top, topQueryOption.Value)); - } + if (topQueryOption.Value > validationSettings.MaxTop) + { + throw new ODataException(Error.Format(SRResources.SkipTopLimitExceeded, validationSettings.MaxTop, + AllowedQueryOptions.Top, topQueryOption.Value)); + } - int maxTop; - IEdmProperty property = topQueryOption.Context.TargetProperty; - IEdmStructuredType structuredType = topQueryOption.Context.TargetStructuredType; + int maxTop; + IEdmProperty property = topQueryOption.Context.TargetProperty; + IEdmStructuredType structuredType = topQueryOption.Context.TargetStructuredType; - if (EdmHelpers.IsTopLimitExceeded( - property, - structuredType, - topQueryOption.Context.Model, - topQueryOption.Value, topQueryOption.Context.DefaultQueryConfigurations, - out maxTop)) - { - throw new ODataException(Error.Format(SRResources.SkipTopLimitExceeded, maxTop, - AllowedQueryOptions.Top, topQueryOption.Value)); - } + if (EdmHelpers.IsTopLimitExceeded( + property, + structuredType, + topQueryOption.Context.Model, + topQueryOption.Value, topQueryOption.Context.DefaultQueryConfigurations, + out maxTop)) + { + throw new ODataException(Error.Format(SRResources.SkipTopLimitExceeded, maxTop, + AllowedQueryOptions.Top, topQueryOption.Value)); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/AggregationWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/AggregationWrapper.cs index 5548f582a..586bfbe17 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/AggregationWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/AggregationWrapper.cs @@ -9,25 +9,24 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +internal class AggregationWrapper : GroupByWrapper +{ +} + +internal class AggregationWrapperConverter : JsonConverter { - internal class AggregationWrapper : GroupByWrapper + public override AggregationWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, nameof(AggregationWrapper))); } - internal class AggregationWrapperConverter : JsonConverter + public override void Write(Utf8JsonWriter writer, AggregationWrapper value, JsonSerializerOptions options) { - public override AggregationWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, nameof(AggregationWrapper))); - } - - public override void Write(Utf8JsonWriter writer, AggregationWrapper value, JsonSerializerOptions options) + if (value != null) { - if (value != null) - { - JsonSerializer.Serialize(writer, value.Values, options); - } + JsonSerializer.Serialize(writer, value.Values, options); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/ComputeWrapperOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/ComputeWrapperOfT.cs index 01b8baff7..8c49e79f8 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/ComputeWrapperOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/ComputeWrapperOfT.cs @@ -16,92 +16,91 @@ using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +internal class ComputeWrapper : GroupByWrapper, IEdmEntityObject { - internal class ComputeWrapper : GroupByWrapper, IEdmEntityObject - { - public T Instance { get; set; } + public T Instance { get; set; } - /// - /// The Edm Model associated with the wrapper. - /// - public IEdmModel Model { get; set; } + /// + /// The Edm Model associated with the wrapper. + /// + public IEdmModel Model { get; set; } - public override Dictionary Values + public override Dictionary Values + { + get { - get - { - EnsureValues(); - return base.Values; - } + EnsureValues(); + return base.Values; } + } - private bool _merged; - private void EnsureValues() + private bool _merged; + private void EnsureValues() + { + if (!this._merged) { - if (!this._merged) + // Base properties available via Instance can be real OData properties or generated in previous transformations + + var instanceContainer = this.Instance as DynamicTypeWrapper; + if (instanceContainer != null) { - // Base properties available via Instance can be real OData properties or generated in previous transformations + // Add properties generated in previous transformations to the collection + base.Values.MergeWithReplace(instanceContainer.Values); + } + else + { + // Add real OData properties to the collection + // We need to use injected Model to real property names + var edmType = GetEdmType() as IEdmStructuredTypeReference; - var instanceContainer = this.Instance as DynamicTypeWrapper; - if (instanceContainer != null) + if (edmType is IEdmComplexTypeReference t) { - // Add properties generated in previous transformations to the collection - base.Values.MergeWithReplace(instanceContainer.Values); + _typedEdmStructuredObject = _typedEdmStructuredObject ?? + new TypedEdmComplexObject(Instance, t, Model); } else { - // Add real OData properties to the collection - // We need to use injected Model to real property names - var edmType = GetEdmType() as IEdmStructuredTypeReference; - - if (edmType is IEdmComplexTypeReference t) - { - _typedEdmStructuredObject = _typedEdmStructuredObject ?? - new TypedEdmComplexObject(Instance, t, Model); - } - else - { - _typedEdmStructuredObject = _typedEdmStructuredObject ?? - new TypedEdmEntityObject(Instance, edmType as IEdmEntityTypeReference, Model); - } + _typedEdmStructuredObject = _typedEdmStructuredObject ?? + new TypedEdmEntityObject(Instance, edmType as IEdmEntityTypeReference, Model); + } - var props = edmType.DeclaredStructuralProperties().Where(p => p.Type.IsPrimitive()).Select(p => p.Name); - foreach (var propertyName in props) + var props = edmType.DeclaredStructuralProperties().Where(p => p.Type.IsPrimitive()).Select(p => p.Name); + foreach (var propertyName in props) + { + object value; + if (_typedEdmStructuredObject.TryGetPropertyValue(propertyName, out value)) { - object value; - if (_typedEdmStructuredObject.TryGetPropertyValue(propertyName, out value)) - { - base.Values[propertyName] = value; - } + base.Values[propertyName] = value; } } - - this._merged = true; } + + this._merged = true; } + } - private TypedEdmStructuredObject _typedEdmStructuredObject; + private TypedEdmStructuredObject _typedEdmStructuredObject; - public IEdmTypeReference GetEdmType() - { - return Model.GetEdmTypeReference(typeof(T)); - } + public IEdmTypeReference GetEdmType() + { + return Model.GetEdmTypeReference(typeof(T)); } +} - internal class ComputeWrapperConverter : JsonConverter> +internal class ComputeWrapperConverter : JsonConverter> +{ + public override ComputeWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override ComputeWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(ComputeWrapper<>).Name)); - } + throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(ComputeWrapper<>).Name)); + } - public override void Write(Utf8JsonWriter writer, ComputeWrapper value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ComputeWrapper value, JsonSerializerOptions options) + { + if (value != null) { - if (value != null) - { - JsonSerializer.Serialize(writer, value.Values, options); - } + JsonSerializer.Serialize(writer, value.Values, options); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/DynamicTypeWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/DynamicTypeWrapper.cs index 254182495..d85e5b47b 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/DynamicTypeWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/DynamicTypeWrapper.cs @@ -8,28 +8,27 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +/// +/// Represents a container class that contains properties that are grouped by using $apply. +/// +public abstract class DynamicTypeWrapper { /// - /// Represents a container class that contains properties that are grouped by using $apply. + /// Gets values stored in the wrapper /// - public abstract class DynamicTypeWrapper - { - /// - /// Gets values stored in the wrapper - /// - public abstract Dictionary Values { get; } + public abstract Dictionary Values { get; } - /// - /// Attempts to get the value of the Property called from the underlying Entity. - /// - /// The name of the Property - /// The new value of the Property - /// True if successful - [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification = "Generics not appropriate here")] - public bool TryGetPropertyValue(string propertyName, out object value) - { - return this.Values.TryGetValue(propertyName, out value); - } + /// + /// Attempts to get the value of the Property called from the underlying Entity. + /// + /// The name of the Property + /// The new value of the Property + /// True if successful + [SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification = "Generics not appropriate here")] + public bool TryGetPropertyValue(string propertyName, out object value) + { + return this.Values.TryGetValue(propertyName, out value); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/DynamicTypeWrapperConverter.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/DynamicTypeWrapperConverter.cs index 82c376a45..c74cba5be 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/DynamicTypeWrapperConverter.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/DynamicTypeWrapperConverter.cs @@ -9,86 +9,85 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +/// +/// Supports converting types by using a factory pattern. +/// +internal class DynamicTypeWrapperConverter : JsonConverterFactory { /// - /// Supports converting types by using a factory pattern. + /// determines whether the converter instance can convert the specified object type. /// - internal class DynamicTypeWrapperConverter : JsonConverterFactory + /// The type of the object to check whether it can be converted by this converter instance. + /// true if the instance can convert the specified object type; otherwise, false. + public override bool CanConvert(Type typeToConvert) { - /// - /// determines whether the converter instance can convert the specified object type. - /// - /// The type of the object to check whether it can be converted by this converter instance. - /// true if the instance can convert the specified object type; otherwise, false. - public override bool CanConvert(Type typeToConvert) + if (typeToConvert == null) { - if (typeToConvert == null) - { - return false; - } + return false; + } - return typeof(DynamicTypeWrapper).IsAssignableFrom(typeToConvert); + return typeof(DynamicTypeWrapper).IsAssignableFrom(typeToConvert); + } + + /// + /// Creates a converter for a specified type. + /// + /// The type handled by the converter. + /// The serialization options to use. + /// A converter for which T is compatible with typeToConvert. + public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) + { + if (type == null) + { + return null; } - /// - /// Creates a converter for a specified type. - /// - /// The type handled by the converter. - /// The serialization options to use. - /// A converter for which T is compatible with typeToConvert. - public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) + if (type.IsGenericType) { - if (type == null) + // Since 'type' is tested in 'CanConvert()', it must be a generic type + Type generaticType = type.GetGenericTypeDefinition(); + Type elementType = type.GetGenericArguments()[0]; + + if (generaticType == typeof(ComputeWrapper<>)) { - return null; + return (JsonConverter)Activator.CreateInstance(typeof(ComputeWrapperConverter<>).MakeGenericType(new Type[] { elementType })); } - if (type.IsGenericType) + if (generaticType == typeof(FlatteningWrapper<>)) { - // Since 'type' is tested in 'CanConvert()', it must be a generic type - Type generaticType = type.GetGenericTypeDefinition(); - Type elementType = type.GetGenericArguments()[0]; - - if (generaticType == typeof(ComputeWrapper<>)) - { - return (JsonConverter)Activator.CreateInstance(typeof(ComputeWrapperConverter<>).MakeGenericType(new Type[] { elementType })); - } - - if (generaticType == typeof(FlatteningWrapper<>)) - { - return (JsonConverter)Activator.CreateInstance(typeof(FlatteningWrapperConverter<>).MakeGenericType(new Type[] { elementType })); - } + return (JsonConverter)Activator.CreateInstance(typeof(FlatteningWrapperConverter<>).MakeGenericType(new Type[] { elementType })); } - else + } + else + { + if (type == typeof(AggregationWrapper)) { - if (type == typeof(AggregationWrapper)) - { - return (JsonConverter)Activator.CreateInstance(typeof(AggregationWrapperConverter)); - } - - if (type == typeof(EntitySetAggregationWrapper)) - { - return (JsonConverter)Activator.CreateInstance(typeof(EntitySetAggregationWrapperConverter)); - } + return (JsonConverter)Activator.CreateInstance(typeof(AggregationWrapperConverter)); + } - if (type == typeof(GroupByWrapper)) - { - return (JsonConverter)Activator.CreateInstance(typeof(GroupByWrapperConverter)); - } + if (type == typeof(EntitySetAggregationWrapper)) + { + return (JsonConverter)Activator.CreateInstance(typeof(EntitySetAggregationWrapperConverter)); + } - if (type == typeof(NoGroupByAggregationWrapper)) - { - return (JsonConverter)Activator.CreateInstance(typeof(NoGroupByAggregationWrapperConverter)); - } + if (type == typeof(GroupByWrapper)) + { + return (JsonConverter)Activator.CreateInstance(typeof(GroupByWrapperConverter)); + } - if (type == typeof(NoGroupByWrapper)) - { - return (JsonConverter)Activator.CreateInstance(typeof(NoGroupByWrapperConverter)); - } + if (type == typeof(NoGroupByAggregationWrapper)) + { + return (JsonConverter)Activator.CreateInstance(typeof(NoGroupByAggregationWrapperConverter)); } - return null; + if (type == typeof(NoGroupByWrapper)) + { + return (JsonConverter)Activator.CreateInstance(typeof(NoGroupByWrapperConverter)); + } } + + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/EntitySetAggregationWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/EntitySetAggregationWrapper.cs index a02c08c93..1634c893c 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/EntitySetAggregationWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/EntitySetAggregationWrapper.cs @@ -9,25 +9,24 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +internal class EntitySetAggregationWrapper : GroupByWrapper +{ +} + +internal class EntitySetAggregationWrapperConverter : JsonConverter { - internal class EntitySetAggregationWrapper : GroupByWrapper + public override EntitySetAggregationWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, nameof(EntitySetAggregationWrapper))); } - internal class EntitySetAggregationWrapperConverter : JsonConverter + public override void Write(Utf8JsonWriter writer, EntitySetAggregationWrapper value, JsonSerializerOptions options) { - public override EntitySetAggregationWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, nameof(EntitySetAggregationWrapper))); - } - - public override void Write(Utf8JsonWriter writer, EntitySetAggregationWrapper value, JsonSerializerOptions options) + if (value != null) { - if (value != null) - { - JsonSerializer.Serialize(writer, value.Values, options); - } + JsonSerializer.Serialize(writer, value.Values, options); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/FlatteningWrapperOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/FlatteningWrapperOfT.cs index 4cf37eb9e..dcf238ceb 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/FlatteningWrapperOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/FlatteningWrapperOfT.cs @@ -10,27 +10,26 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +internal class FlatteningWrapper : GroupByWrapper +{ + // TODO: how to use 'Source'? + public T Source { get; set; } +} + +internal class FlatteningWrapperConverter : JsonConverter> { - internal class FlatteningWrapper : GroupByWrapper + public override FlatteningWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - // TODO: how to use 'Source'? - public T Source { get; set; } + throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(FlatteningWrapper<>).Name)); } - internal class FlatteningWrapperConverter : JsonConverter> + public override void Write(Utf8JsonWriter writer, FlatteningWrapper value, JsonSerializerOptions options) { - public override FlatteningWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(FlatteningWrapper<>).Name)); - } - - public override void Write(Utf8JsonWriter writer, FlatteningWrapper value, JsonSerializerOptions options) + if (value != null) { - if (value != null) - { - JsonSerializer.Serialize(writer, value.Values, options); - } + JsonSerializer.Serialize(writer, value.Values, options); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/GroupByWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/GroupByWrapper.cs index 5bc0ff162..badac5249 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/GroupByWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/GroupByWrapper.cs @@ -13,92 +13,91 @@ using Microsoft.AspNetCore.OData.Common; using Microsoft.AspNetCore.OData.Query.Container; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +internal class GroupByWrapper : DynamicTypeWrapper { - internal class GroupByWrapper : DynamicTypeWrapper - { - private Dictionary _values; - protected static readonly IPropertyMapper DefaultPropertyMapper = new IdentityPropertyMapper(); + private Dictionary _values; + protected static readonly IPropertyMapper DefaultPropertyMapper = new IdentityPropertyMapper(); - /// - /// Gets or sets the property container that contains the properties being expanded. - /// - public virtual AggregationPropertyContainer GroupByContainer { get; set; } + /// + /// Gets or sets the property container that contains the properties being expanded. + /// + public virtual AggregationPropertyContainer GroupByContainer { get; set; } - /// - /// Gets or sets the property container that contains the properties being expanded. - /// - public virtual AggregationPropertyContainer Container { get; set; } + /// + /// Gets or sets the property container that contains the properties being expanded. + /// + public virtual AggregationPropertyContainer Container { get; set; } - public override Dictionary Values + public override Dictionary Values + { + get { - get - { - EnsureValues(); - return this._values; - } + EnsureValues(); + return this._values; } + } - /// - public override bool Equals(object obj) + /// + public override bool Equals(object obj) + { + var compareWith = obj as GroupByWrapper; + if (compareWith == null) { - var compareWith = obj as GroupByWrapper; - if (compareWith == null) - { - return false; - } - var dictionary1 = this.Values; - var dictionary2 = compareWith.Values; - return dictionary1.Count == dictionary2.Count && !dictionary1.Except(dictionary2).Any(); + return false; } + var dictionary1 = this.Values; + var dictionary2 = compareWith.Values; + return dictionary1.Count == dictionary2.Count && !dictionary1.Except(dictionary2).Any(); + } - /// - public override int GetHashCode() + /// + public override int GetHashCode() + { + EnsureValues(); + long hash = 1870403278L; //Arbitrary number from Anonymous Type GetHashCode implementation + foreach (var v in this.Values.Values) { - EnsureValues(); - long hash = 1870403278L; //Arbitrary number from Anonymous Type GetHashCode implementation - foreach (var v in this.Values.Values) - { - hash = (hash * -1521134295L) + (v == null ? 0 : v.GetHashCode()); - } - - return (int)hash; + hash = (hash * -1521134295L) + (v == null ? 0 : v.GetHashCode()); } - private void EnsureValues() + return (int)hash; + } + + private void EnsureValues() + { + if (_values == null) { - if (_values == null) + if (this.GroupByContainer != null) + { + this._values = this.GroupByContainer.ToDictionary(DefaultPropertyMapper); + } + else { - if (this.GroupByContainer != null) - { - this._values = this.GroupByContainer.ToDictionary(DefaultPropertyMapper); - } - else - { - this._values = new Dictionary(); - } + this._values = new Dictionary(); + } - if (this.Container != null) - { - _values.MergeWithReplace(this.Container.ToDictionary(DefaultPropertyMapper)); - } + if (this.Container != null) + { + _values.MergeWithReplace(this.Container.ToDictionary(DefaultPropertyMapper)); } } } +} - internal class GroupByWrapperConverter : JsonConverter +internal class GroupByWrapperConverter : JsonConverter +{ + public override GroupByWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override GroupByWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, nameof(GroupByWrapper))); - } + throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, nameof(GroupByWrapper))); + } - public override void Write(Utf8JsonWriter writer, GroupByWrapper value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, GroupByWrapper value, JsonSerializerOptions options) + { + if (value != null) { - if (value != null) - { - JsonSerializer.Serialize(writer, value.Values, options); - } + JsonSerializer.Serialize(writer, value.Values, options); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/ISelectExpandWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/ISelectExpandWrapper.cs index 9936044ea..eee676d2c 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/ISelectExpandWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/ISelectExpandWrapper.cs @@ -10,34 +10,33 @@ using Microsoft.AspNetCore.OData.Query.Container; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +/// +/// Represents the result of a $select and $expand query operation. +/// +public interface ISelectExpandWrapper { /// - /// Represents the result of a $select and $expand query operation. + /// Projects the result of a $select and $expand query to a . /// - public interface ISelectExpandWrapper - { - /// - /// Projects the result of a $select and $expand query to a . - /// - /// An representing the $select and $expand result. - IDictionary ToDictionary(); + /// An representing the $select and $expand result. + IDictionary ToDictionary(); - /// - /// Projects the result of a $select and/or $expand query to an using - /// the given . The is used - /// to obtain an for the that this - /// instance represents. This will be used to - /// map the properties of the instance to the keys of the - /// returned . This method can be used, for example, to map the property - /// names in the to the names that should be used to serialize the properties - /// that this projection contains. - /// - /// - /// A function that provides a new instance of an for a given - /// and a given . - /// - /// An representing the $select and $expand result. - IDictionary ToDictionary(Func propertyMapperProvider); - } + /// + /// Projects the result of a $select and/or $expand query to an using + /// the given . The is used + /// to obtain an for the that this + /// instance represents. This will be used to + /// map the properties of the instance to the keys of the + /// returned . This method can be used, for example, to map the property + /// names in the to the names that should be used to serialize the properties + /// that this projection contains. + /// + /// + /// A function that provides a new instance of an for a given + /// and a given . + /// + /// An representing the $select and $expand result. + IDictionary ToDictionary(Func propertyMapperProvider); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByAggregationWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByAggregationWrapper.cs index 972b817e2..681116680 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByAggregationWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByAggregationWrapper.cs @@ -9,25 +9,24 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +internal class NoGroupByAggregationWrapper : GroupByWrapper +{ +} + +internal class NoGroupByAggregationWrapperConverter : JsonConverter { - internal class NoGroupByAggregationWrapper : GroupByWrapper + public override NoGroupByAggregationWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, nameof(NoGroupByAggregationWrapper))); } - internal class NoGroupByAggregationWrapperConverter : JsonConverter + public override void Write(Utf8JsonWriter writer, NoGroupByAggregationWrapper value, JsonSerializerOptions options) { - public override NoGroupByAggregationWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, nameof(NoGroupByAggregationWrapper))); - } - - public override void Write(Utf8JsonWriter writer, NoGroupByAggregationWrapper value, JsonSerializerOptions options) + if (value != null) { - if (value != null) - { - JsonSerializer.Serialize(writer, value.Values, options); - } + JsonSerializer.Serialize(writer, value.Values, options); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByWrapper.cs index 9d90c7a7a..b14cf7fc1 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByWrapper.cs @@ -9,25 +9,24 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +internal class NoGroupByWrapper : GroupByWrapper +{ +} + +internal class NoGroupByWrapperConverter : JsonConverter { - internal class NoGroupByWrapper : GroupByWrapper + public override NoGroupByWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, nameof(NoGroupByWrapper))); } - internal class NoGroupByWrapperConverter : JsonConverter + public override void Write(Utf8JsonWriter writer, NoGroupByWrapper value, JsonSerializerOptions options) { - public override NoGroupByWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, nameof(NoGroupByWrapper))); - } - - public override void Write(Utf8JsonWriter writer, NoGroupByWrapper value, JsonSerializerOptions options) + if (value != null) { - if (value != null) - { - JsonSerializer.Serialize(writer, value.Values, options); - } + JsonSerializer.Serialize(writer, value.Values, options); } } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllAndExpandOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllAndExpandOfT.cs index 2325c243b..c45fa4025 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllAndExpandOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllAndExpandOfT.cs @@ -9,22 +9,21 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +internal class SelectAllAndExpand : SelectExpandWrapper { - internal class SelectAllAndExpand : SelectExpandWrapper +} + +internal class SelectAllAndExpandConverter : JsonConverter> +{ + public override SelectAllAndExpand Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(SelectAllAndExpand<>).Name)); } - internal class SelectAllAndExpandConverter : JsonConverter> + public override void Write(Utf8JsonWriter writer, SelectAllAndExpand value, JsonSerializerOptions options) { - public override SelectAllAndExpand Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(SelectAllAndExpand<>).Name)); - } - - public override void Write(Utf8JsonWriter writer, SelectAllAndExpand value, JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, value.ToDictionary(SelectExpandWrapperConverter.MapperProvider), options); - } + JsonSerializer.Serialize(writer, value.ToDictionary(SelectExpandWrapperConverter.MapperProvider), options); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllOfT.cs index c4d5b22b0..3b99c5e19 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllOfT.cs @@ -9,22 +9,21 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +internal class SelectAll : SelectExpandWrapper { - internal class SelectAll : SelectExpandWrapper +} + +internal class SelectAllConverter : JsonConverter> +{ + public override SelectAll Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(SelectAll<>).Name)); } - internal class SelectAllConverter : JsonConverter> + public override void Write(Utf8JsonWriter writer, SelectAll value, JsonSerializerOptions options) { - public override SelectAll Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(SelectAll<>).Name)); - } - - public override void Write(Utf8JsonWriter writer, SelectAll value, JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, value.ToDictionary(SelectExpandWrapperConverter.MapperProvider), options); - } + JsonSerializer.Serialize(writer, value.ToDictionary(SelectExpandWrapperConverter.MapperProvider), options); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs index 194155a62..77f79189d 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs @@ -12,154 +12,153 @@ using Microsoft.AspNetCore.OData.Query.Container; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +internal abstract class SelectExpandWrapper : IEdmEntityObject, ISelectExpandWrapper { - internal abstract class SelectExpandWrapper : IEdmEntityObject, ISelectExpandWrapper + private static readonly IPropertyMapper DefaultPropertyMapper = new IdentityPropertyMapper(); + private static readonly Func _mapperProvider = + (IEdmModel m, IEdmStructuredType t) => DefaultPropertyMapper; + + private Dictionary _containerDict; + private TypedEdmStructuredObject _typedEdmStructuredObject; + + /// + /// Gets or sets the property container that contains the properties being expanded. + /// + public PropertyContainer Container { get; set; } + + /// + /// The Edm Model associated with the wrapper. + /// EntityFramework does not let us inject non primitive constant values (like IEdmModel). + /// However, we can always 'parameterize" this non-constant value. + /// + public IEdmModel Model { get; set; } + + /// + public object UntypedInstance { get; set; } + + /// + /// Gets or sets the instance type name + /// + public string InstanceType { get; set; } + + /// + /// Indicates whether the underlying instance can be used to obtain property values. + /// + public bool UseInstanceForProperties { get; set; } + + /// + public IEdmTypeReference GetEdmType() { - private static readonly IPropertyMapper DefaultPropertyMapper = new IdentityPropertyMapper(); - private static readonly Func _mapperProvider = - (IEdmModel m, IEdmStructuredType t) => DefaultPropertyMapper; - - private Dictionary _containerDict; - private TypedEdmStructuredObject _typedEdmStructuredObject; - - /// - /// Gets or sets the property container that contains the properties being expanded. - /// - public PropertyContainer Container { get; set; } - - /// - /// The Edm Model associated with the wrapper. - /// EntityFramework does not let us inject non primitive constant values (like IEdmModel). - /// However, we can always 'parameterize" this non-constant value. - /// - public IEdmModel Model { get; set; } - - /// - public object UntypedInstance { get; set; } - - /// - /// Gets or sets the instance type name - /// - public string InstanceType { get; set; } - - /// - /// Indicates whether the underlying instance can be used to obtain property values. - /// - public bool UseInstanceForProperties { get; set; } - - /// - public IEdmTypeReference GetEdmType() + IEdmModel model = Model; + + if (InstanceType != null) { - IEdmModel model = Model; + IEdmStructuredType structuredType = model.FindType(InstanceType) as IEdmStructuredType; + IEdmEntityType entityType = structuredType as IEdmEntityType; - if (InstanceType != null) + if (entityType != null) { - IEdmStructuredType structuredType = model.FindType(InstanceType) as IEdmStructuredType; - IEdmEntityType entityType = structuredType as IEdmEntityType; + return entityType.ToEdmTypeReference(true); + } - if (entityType != null) - { - return entityType.ToEdmTypeReference(true); - } + return structuredType.ToEdmTypeReference(true); + } - return structuredType.ToEdmTypeReference(true); - } + Type elementType = GetElementType(); - Type elementType = GetElementType(); + return model.GetEdmTypeReference(elementType); + } - return model.GetEdmTypeReference(elementType); + /// + public bool TryGetPropertyValue(string propertyName, out object value) + { + // look into the container first to see if it has that property. container would have it + // if the property was expanded. + if (Container != null) + { + _containerDict = _containerDict ?? Container.ToDictionary(DefaultPropertyMapper, includeAutoSelected: true); + if (_containerDict.TryGetValue(propertyName, out value)) + { + return true; + } } - /// - public bool TryGetPropertyValue(string propertyName, out object value) + // fall back to the instance. + if (UseInstanceForProperties && UntypedInstance != null) { - // look into the container first to see if it has that property. container would have it - // if the property was expanded. - if (Container != null) + IEdmTypeReference edmTypeReference = GetEdmType(); + IEdmModel model = Model; + if (edmTypeReference is IEdmComplexTypeReference) { - _containerDict = _containerDict ?? Container.ToDictionary(DefaultPropertyMapper, includeAutoSelected: true); - if (_containerDict.TryGetValue(propertyName, out value)) - { - return true; - } + _typedEdmStructuredObject = _typedEdmStructuredObject ?? + new TypedEdmComplexObject(UntypedInstance, edmTypeReference as IEdmComplexTypeReference, model); } - - // fall back to the instance. - if (UseInstanceForProperties && UntypedInstance != null) + else { - IEdmTypeReference edmTypeReference = GetEdmType(); - IEdmModel model = Model; - if (edmTypeReference is IEdmComplexTypeReference) - { - _typedEdmStructuredObject = _typedEdmStructuredObject ?? - new TypedEdmComplexObject(UntypedInstance, edmTypeReference as IEdmComplexTypeReference, model); - } - else - { - _typedEdmStructuredObject = _typedEdmStructuredObject ?? - new TypedEdmEntityObject(UntypedInstance, edmTypeReference as IEdmEntityTypeReference, model); - } - - return _typedEdmStructuredObject.TryGetPropertyValue(propertyName, out value); + _typedEdmStructuredObject = _typedEdmStructuredObject ?? + new TypedEdmEntityObject(UntypedInstance, edmTypeReference as IEdmEntityTypeReference, model); } - value = null; - return false; + return _typedEdmStructuredObject.TryGetPropertyValue(propertyName, out value); } - public IDictionary ToDictionary() - { - return ToDictionary(_mapperProvider); - } + value = null; + return false; + } + + public IDictionary ToDictionary() + { + return ToDictionary(_mapperProvider); + } - public IDictionary ToDictionary(Func mapperProvider) + public IDictionary ToDictionary(Func mapperProvider) + { + if (mapperProvider == null) { - if (mapperProvider == null) - { - throw Error.ArgumentNull("mapperProvider"); - } + throw Error.ArgumentNull("mapperProvider"); + } - Dictionary dictionary = new Dictionary(); - IEdmStructuredType type = GetEdmType().AsStructured().StructuredDefinition(); + Dictionary dictionary = new Dictionary(); + IEdmStructuredType type = GetEdmType().AsStructured().StructuredDefinition(); - IPropertyMapper mapper = mapperProvider(Model, type); - if (mapper == null) - { - throw Error.InvalidOperation(SRResources.InvalidPropertyMapper, typeof(IPropertyMapper).FullName, - type.FullTypeName()); - } + IPropertyMapper mapper = mapperProvider(Model, type); + if (mapper == null) + { + throw Error.InvalidOperation(SRResources.InvalidPropertyMapper, typeof(IPropertyMapper).FullName, + type.FullTypeName()); + } - if (Container != null) - { - dictionary = Container.ToDictionary(mapper, includeAutoSelected: false); - } + if (Container != null) + { + dictionary = Container.ToDictionary(mapper, includeAutoSelected: false); + } - // The user asked for all the structural properties on this instance. - if (UseInstanceForProperties && UntypedInstance != null) + // The user asked for all the structural properties on this instance. + if (UseInstanceForProperties && UntypedInstance != null) + { + foreach (IEdmStructuralProperty property in type.StructuralProperties()) { - foreach (IEdmStructuralProperty property in type.StructuralProperties()) + object propertyValue; + if (TryGetPropertyValue(property.Name, out propertyValue)) { - object propertyValue; - if (TryGetPropertyValue(property.Name, out propertyValue)) + string mappingName = mapper.MapProperty(property.Name); + if (mappingName != null) { - string mappingName = mapper.MapProperty(property.Name); - if (mappingName != null) + if (String.IsNullOrWhiteSpace(mappingName)) { - if (String.IsNullOrWhiteSpace(mappingName)) - { - throw Error.InvalidOperation(SRResources.InvalidPropertyMapping, property.Name); - } - - dictionary[mappingName] = propertyValue; + throw Error.InvalidOperation(SRResources.InvalidPropertyMapping, property.Name); } + + dictionary[mappingName] = propertyValue; } } } - - return dictionary; } - protected abstract Type GetElementType(); + return dictionary; } + + protected abstract Type GetElementType(); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperConverter.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperConverter.cs index 332bedd32..f83b1162e 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperConverter.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperConverter.cs @@ -11,89 +11,88 @@ using Microsoft.AspNetCore.OData.Query.Container; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +/// +/// Supports converting types by using a factory pattern. +/// +internal class SelectExpandWrapperConverter : JsonConverterFactory { + public static readonly Func MapperProvider = + (IEdmModel model, IEdmStructuredType type) => new JsonPropertyNameMapper(model, type); + /// - /// Supports converting types by using a factory pattern. + /// determines whether the converter instance can convert the specified object type. /// - internal class SelectExpandWrapperConverter : JsonConverterFactory + /// The type of the object to check whether it can be converted by this converter instance. + /// true if the instance can convert the specified object type; otherwise, false. + public override bool CanConvert(Type typeToConvert) { - public static readonly Func MapperProvider = - (IEdmModel model, IEdmStructuredType type) => new JsonPropertyNameMapper(model, type); - - /// - /// determines whether the converter instance can convert the specified object type. - /// - /// The type of the object to check whether it can be converted by this converter instance. - /// true if the instance can convert the specified object type; otherwise, false. - public override bool CanConvert(Type typeToConvert) + if (typeToConvert == null || !typeToConvert.IsGenericType) { - if (typeToConvert == null || !typeToConvert.IsGenericType) - { - return false; - } - - return typeof(ISelectExpandWrapper).IsAssignableFrom(typeToConvert); - - /* We can use the following codes to limit the compare. - * But, use the above ISelectExpandWrapper can unblock the new type later. - Type generaticType = typeToConvert.GetGenericTypeDefinition(); - if (generaticType == typeof(SelectSome<>) || - generaticType == typeof(SelectSomeAndInheritance<>) || - generaticType == typeof(SelectAllAndExpand<>) || - generaticType == typeof(SelectAll<>) || - generaticType == typeof(SelectExpandWrapper<>)) - { - return true; - } - return false; - */ } - /// - /// Creates a converter for a specified type. - /// - /// The type handled by the converter. - /// The serialization options to use. - /// A converter for which T is compatible with typeToConvert. - public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) + return typeof(ISelectExpandWrapper).IsAssignableFrom(typeToConvert); + + /* We can use the following codes to limit the compare. + * But, use the above ISelectExpandWrapper can unblock the new type later. + Type generaticType = typeToConvert.GetGenericTypeDefinition(); + if (generaticType == typeof(SelectSome<>) || + generaticType == typeof(SelectSomeAndInheritance<>) || + generaticType == typeof(SelectAllAndExpand<>) || + generaticType == typeof(SelectAll<>) || + generaticType == typeof(SelectExpandWrapper<>)) { - if (type == null || !type.IsGenericType) - { - return null; - } + return true; + } - // Since 'type' is tested in 'CanConvert()', it must be a generic type - Type generaticType = type.GetGenericTypeDefinition(); - Type entityType = type.GetGenericArguments()[0]; + return false; + */ + } - if (generaticType == typeof(SelectSome<>)) - { - return (JsonConverter)Activator.CreateInstance(typeof(SelectSomeConverter<>).MakeGenericType(new Type[] { entityType })); - } + /// + /// Creates a converter for a specified type. + /// + /// The type handled by the converter. + /// The serialization options to use. + /// A converter for which T is compatible with typeToConvert. + public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) + { + if (type == null || !type.IsGenericType) + { + return null; + } - if (generaticType == typeof(SelectSomeAndInheritance<>)) - { - return (JsonConverter)Activator.CreateInstance(typeof(SelectSomeAndInheritanceConverter<>).MakeGenericType(new Type[] { entityType })); - } + // Since 'type' is tested in 'CanConvert()', it must be a generic type + Type generaticType = type.GetGenericTypeDefinition(); + Type entityType = type.GetGenericArguments()[0]; - if (generaticType == typeof(SelectAll<>)) - { - return (JsonConverter)Activator.CreateInstance(typeof(SelectAllConverter<>).MakeGenericType(new Type[] { entityType })); - } + if (generaticType == typeof(SelectSome<>)) + { + return (JsonConverter)Activator.CreateInstance(typeof(SelectSomeConverter<>).MakeGenericType(new Type[] { entityType })); + } - if (generaticType == typeof(SelectAllAndExpand<>)) - { - return (JsonConverter)Activator.CreateInstance(typeof(SelectAllAndExpandConverter<>).MakeGenericType(new Type[] { entityType })); - } + if (generaticType == typeof(SelectSomeAndInheritance<>)) + { + return (JsonConverter)Activator.CreateInstance(typeof(SelectSomeAndInheritanceConverter<>).MakeGenericType(new Type[] { entityType })); + } - if (generaticType == typeof(SelectExpandWrapper<>)) - { - return (JsonConverter)Activator.CreateInstance(typeof(SelectExpandWrapperConverter<>).MakeGenericType(new Type[] { entityType })); - } + if (generaticType == typeof(SelectAll<>)) + { + return (JsonConverter)Activator.CreateInstance(typeof(SelectAllConverter<>).MakeGenericType(new Type[] { entityType })); + } - return null; + if (generaticType == typeof(SelectAllAndExpand<>)) + { + return (JsonConverter)Activator.CreateInstance(typeof(SelectAllAndExpandConverter<>).MakeGenericType(new Type[] { entityType })); + } + + if (generaticType == typeof(SelectExpandWrapper<>)) + { + return (JsonConverter)Activator.CreateInstance(typeof(SelectExpandWrapperConverter<>).MakeGenericType(new Type[] { entityType })); } + + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperOfT.cs index 22ce62991..3d8da440b 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperOfT.cs @@ -9,9 +9,9 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Query.Wrapper -{ - /* Entityframework requires that the two different type initializers for a given type in the same query have the +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +/* Entityframework requires that the two different type initializers for a given type in the same query have the same set of properties in the same order. A ~/People?$select=Name&$expand=Friend results in a select expression that has two SelectExpandWrapper @@ -21,37 +21,36 @@ has the Instance property set as it contains all the properties of the expanded The below four classes workaround that entity framework limitation by defining a separate type for each property selection combination possible. */ +/// +/// Represents a container class that contains properties that are either selected or expanded using $select and $expand. +/// +/// The element being selected and expanded. +internal class SelectExpandWrapper : SelectExpandWrapper +{ /// - /// Represents a container class that contains properties that are either selected or expanded using $select and $expand. + /// Gets or sets the instance of the element being selected and expanded. /// - /// The element being selected and expanded. - internal class SelectExpandWrapper : SelectExpandWrapper + public TElement Instance + { + get { return (TElement)UntypedInstance; } + set { UntypedInstance = value; } + } + + protected override Type GetElementType() + { + return UntypedInstance == null ? typeof(TElement) : UntypedInstance.GetType(); + } +} + +internal class SelectExpandWrapperConverter : JsonConverter> +{ + public override SelectExpandWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - /// - /// Gets or sets the instance of the element being selected and expanded. - /// - public TElement Instance - { - get { return (TElement)UntypedInstance; } - set { UntypedInstance = value; } - } - - protected override Type GetElementType() - { - return UntypedInstance == null ? typeof(TElement) : UntypedInstance.GetType(); - } + throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(SelectExpandWrapper<>).Name)); } - internal class SelectExpandWrapperConverter : JsonConverter> + public override void Write(Utf8JsonWriter writer, SelectExpandWrapper value, JsonSerializerOptions options) { - public override SelectExpandWrapper Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(SelectExpandWrapper<>).Name)); - } - - public override void Write(Utf8JsonWriter writer, SelectExpandWrapper value, JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, value.ToDictionary(SelectExpandWrapperConverter.MapperProvider), options); - } + JsonSerializer.Serialize(writer, value.ToDictionary(SelectExpandWrapperConverter.MapperProvider), options); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeAndInheritanceOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeAndInheritanceOfT.cs index 24b78bddb..73c408e62 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeAndInheritanceOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeAndInheritanceOfT.cs @@ -9,22 +9,21 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +internal class SelectSomeAndInheritance : SelectExpandWrapper { - internal class SelectSomeAndInheritance : SelectExpandWrapper +} + +internal class SelectSomeAndInheritanceConverter : JsonConverter> +{ + public override SelectSomeAndInheritance Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(SelectSomeAndInheritance<>).Name)); } - internal class SelectSomeAndInheritanceConverter : JsonConverter> + public override void Write(Utf8JsonWriter writer, SelectSomeAndInheritance value, JsonSerializerOptions options) { - public override SelectSomeAndInheritance Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(SelectSomeAndInheritance<>).Name)); - } - - public override void Write(Utf8JsonWriter writer, SelectSomeAndInheritance value, JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, value.ToDictionary(SelectExpandWrapperConverter.MapperProvider), options); - } + JsonSerializer.Serialize(writer, value.ToDictionary(SelectExpandWrapperConverter.MapperProvider), options); } } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeOfT.cs index 0f224f59e..bd23a0eb7 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeOfT.cs @@ -9,22 +9,21 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Query.Wrapper; + +internal class SelectSome : SelectAllAndExpand { - internal class SelectSome : SelectAllAndExpand +} + +internal class SelectSomeConverter : JsonConverter> +{ + public override SelectSome Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(SelectSome<>).Name)); } - internal class SelectSomeConverter : JsonConverter> + public override void Write(Utf8JsonWriter writer, SelectSome value, JsonSerializerOptions options) { - public override SelectSome Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(Error.Format(SRResources.JsonConverterDoesnotSupportRead, typeof(SelectSome<>).Name)); - } - - public override void Write(Utf8JsonWriter writer, SelectSome value, JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, value.ToDictionary(SelectExpandWrapperConverter.MapperProvider), options); - } + JsonSerializer.Serialize(writer, value.ToDictionary(SelectExpandWrapperConverter.MapperProvider), options); } } diff --git a/src/Microsoft.AspNetCore.OData/Results/BadRequestODataResult.cs b/src/Microsoft.AspNetCore.OData/Results/BadRequestODataResult.cs index 394b1a31d..28c5d02eb 100644 --- a/src/Microsoft.AspNetCore.OData/Results/BadRequestODataResult.cs +++ b/src/Microsoft.AspNetCore.OData/Results/BadRequestODataResult.cs @@ -11,62 +11,61 @@ using Microsoft.OData; using ErrorUtils = Microsoft.AspNetCore.OData.Error; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +/// +/// Represents a result that when executed will produce a Bad Request (400) response. +/// +/// This result creates an with status code: 400. +public class BadRequestODataResult : BadRequestResult, IODataErrorResult { + private const string errorCode = "400"; + /// - /// Represents a result that when executed will produce a Bad Request (400) response. + /// OData error. /// - /// This result creates an with status code: 400. - public class BadRequestODataResult : BadRequestResult, IODataErrorResult - { - private const string errorCode = "400"; + public ODataError Error { get; } - /// - /// OData error. - /// - public ODataError Error { get; } - - /// - /// Initializes a new instance of the class. - /// - /// Error message. - public BadRequestODataResult(string message) + /// + /// Initializes a new instance of the class. + /// + /// Error message. + public BadRequestODataResult(string message) + { + if (string.IsNullOrEmpty(message)) { - if (string.IsNullOrEmpty(message)) - { - throw ErrorUtils.ArgumentNullOrEmpty(nameof(message)); - } - - Error = new ODataError - { - Message = message, - Code = errorCode - }; + throw ErrorUtils.ArgumentNullOrEmpty(nameof(message)); } - /// - /// Initializes a new instance of the class. - /// - /// An object. - public BadRequestODataResult(ODataError odataError) + Error = new ODataError { - if (odataError == null) - { - throw ErrorUtils.ArgumentNull(nameof(odataError)); - } + Message = message, + Code = errorCode + }; + } - Error = odataError; + /// + /// Initializes a new instance of the class. + /// + /// An object. + public BadRequestODataResult(ODataError odataError) + { + if (odataError == null) + { + throw ErrorUtils.ArgumentNull(nameof(odataError)); } - /// - public async override Task ExecuteResultAsync(ActionContext context) + Error = odataError; + } + + /// + public async override Task ExecuteResultAsync(ActionContext context) + { + ObjectResult objectResult = new ObjectResult(Error) { - ObjectResult objectResult = new ObjectResult(Error) - { - StatusCode = StatusCodes.Status400BadRequest - }; + StatusCode = StatusCodes.Status400BadRequest + }; - await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); - } + await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); } } diff --git a/src/Microsoft.AspNetCore.OData/Results/ConflictODataResult.cs b/src/Microsoft.AspNetCore.OData/Results/ConflictODataResult.cs index df400abd6..acf667303 100644 --- a/src/Microsoft.AspNetCore.OData/Results/ConflictODataResult.cs +++ b/src/Microsoft.AspNetCore.OData/Results/ConflictODataResult.cs @@ -11,62 +11,61 @@ using Microsoft.OData; using ErrorUtils = Microsoft.AspNetCore.OData.Error; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +/// +/// Represents a result that when executed will produce a Conflict (409) response. +/// +/// This result creates an with status code: 409. +public class ConflictODataResult : ConflictResult, IODataErrorResult { + private const string errorCode = "409"; + /// - /// Represents a result that when executed will produce a Conflict (409) response. + /// OData error. /// - /// This result creates an with status code: 409. - public class ConflictODataResult : ConflictResult, IODataErrorResult - { - private const string errorCode = "409"; + public ODataError Error { get; } - /// - /// OData error. - /// - public ODataError Error { get; } - - /// - /// Initializes a new instance of the class. - /// - /// Error message. - public ConflictODataResult(string message) + /// + /// Initializes a new instance of the class. + /// + /// Error message. + public ConflictODataResult(string message) + { + if (string.IsNullOrEmpty(message)) { - if (string.IsNullOrEmpty(message)) - { - throw ErrorUtils.ArgumentNullOrEmpty(nameof(message)); - } - - Error = new ODataError - { - Message = message, - Code = errorCode - }; + throw ErrorUtils.ArgumentNullOrEmpty(nameof(message)); } - /// - /// Initializes a new instance of the class. - /// - /// An object. - public ConflictODataResult(ODataError odataError) + Error = new ODataError { - if (odataError == null) - { - throw ErrorUtils.ArgumentNull(nameof(odataError)); - } + Message = message, + Code = errorCode + }; + } - Error = odataError; + /// + /// Initializes a new instance of the class. + /// + /// An object. + public ConflictODataResult(ODataError odataError) + { + if (odataError == null) + { + throw ErrorUtils.ArgumentNull(nameof(odataError)); } - /// - public async override Task ExecuteResultAsync(ActionContext context) + Error = odataError; + } + + /// + public async override Task ExecuteResultAsync(ActionContext context) + { + ObjectResult objectResult = new ObjectResult(Error) { - ObjectResult objectResult = new ObjectResult(Error) - { - StatusCode = StatusCodes.Status409Conflict - }; + StatusCode = StatusCodes.Status409Conflict + }; - await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); - } + await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); } } diff --git a/src/Microsoft.AspNetCore.OData/Results/CreatedODataResult.cs b/src/Microsoft.AspNetCore.OData/Results/CreatedODataResult.cs index 0bcbe4f59..45f6d7f56 100644 --- a/src/Microsoft.AspNetCore.OData/Results/CreatedODataResult.cs +++ b/src/Microsoft.AspNetCore.OData/Results/CreatedODataResult.cs @@ -13,81 +13,80 @@ using Microsoft.AspNetCore.OData.Extensions; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +/// +/// Represents an action result that is a response to a create operation that adds an entity to an entity set. +/// +/// The entity type. +/// This action result handles content negotiation and the HTTP prefer header. It generates a location +/// header containing the edit link of the created entity and, if response has status code: NoContent, also +/// generates an OData-EntityId header. +public class CreatedODataResult : ObjectResult { /// - /// Represents an action result that is a response to a create operation that adds an entity to an entity set. + /// Initializes a new instance of the class. /// - /// The entity type. - /// This action result handles content negotiation and the HTTP prefer header. It generates a location - /// header containing the edit link of the created entity and, if response has status code: NoContent, also - /// generates an OData-EntityId header. - public class CreatedODataResult : ObjectResult + /// The created entity. + public CreatedODataResult(T entity) + : base(entity) { - /// - /// Initializes a new instance of the class. - /// - /// The created entity. - public CreatedODataResult(T entity) - : base(entity) - { - Entity = entity ?? throw Error.ArgumentNull(nameof(entity)); - } + Entity = entity ?? throw Error.ArgumentNull(nameof(entity)); + } - /// - /// Gets the entity that was created. - /// - public virtual T Entity { get; } + /// + /// Gets the entity that was created. + /// + public virtual T Entity { get; } - /// - public async override Task ExecuteResultAsync(ActionContext context) + /// + public async override Task ExecuteResultAsync(ActionContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - HttpRequest request = context.HttpContext.Request; - HttpResponse response = context.HttpContext.Response; - IActionResult result = GetInnerActionResult(request); - Uri location = GenerateLocationHeader(request); + HttpRequest request = context.HttpContext.Request; + HttpResponse response = context.HttpContext.Response; + IActionResult result = GetInnerActionResult(request); + Uri location = GenerateLocationHeader(request); - response.Headers["Location"] = location.AbsoluteUri; - // Since AddEntityId relies on the response, make sure to execute the result - // before calling AddEntityId() to ensure the response code is set correctly. - await result.ExecuteResultAsync(context).ConfigureAwait(false); - ResultHelpers.AddEntityId(response, () => GenerateEntityId(request)); - ResultHelpers.AddServiceVersion(response, () => ODataUtils.ODataVersionToString(request.GetODataVersion())); - } + response.Headers["Location"] = location.AbsoluteUri; + // Since AddEntityId relies on the response, make sure to execute the result + // before calling AddEntityId() to ensure the response code is set correctly. + await result.ExecuteResultAsync(context).ConfigureAwait(false); + ResultHelpers.AddEntityId(response, () => GenerateEntityId(request)); + ResultHelpers.AddServiceVersion(response, () => ODataUtils.ODataVersionToString(request.GetODataVersion())); + } - // internal just for unit test. - internal IActionResult GetInnerActionResult(HttpRequest request) + // internal just for unit test. + internal IActionResult GetInnerActionResult(HttpRequest request) + { + if (RequestPreferenceHelpers.RequestPrefersReturnNoContent(request.Headers)) { - if (RequestPreferenceHelpers.RequestPrefersReturnNoContent(request.Headers)) - { - return new StatusCodeResult((int)HttpStatusCode.NoContent); - } - else + return new StatusCodeResult((int)HttpStatusCode.NoContent); + } + else + { + ObjectResult objectResult = new ObjectResult(Entity) { - ObjectResult objectResult = new ObjectResult(Entity) - { - StatusCode = StatusCodes.Status201Created - }; + StatusCode = StatusCodes.Status201Created + }; - return objectResult; - } + return objectResult; } + } - // internal just for unit test. - internal Uri GenerateEntityId(HttpRequest request) - { - return ResultHelpers.GenerateODataLink(request, Entity, isEntityId: true); - } + // internal just for unit test. + internal Uri GenerateEntityId(HttpRequest request) + { + return ResultHelpers.GenerateODataLink(request, Entity, isEntityId: true); + } - // internal just for unit test. - internal Uri GenerateLocationHeader(HttpRequest request) - { - return ResultHelpers.GenerateODataLink(request, Entity, isEntityId: false); - } + // internal just for unit test. + internal Uri GenerateLocationHeader(HttpRequest request) + { + return ResultHelpers.GenerateODataLink(request, Entity, isEntityId: false); } } diff --git a/src/Microsoft.AspNetCore.OData/Results/IODataErrorResult.cs b/src/Microsoft.AspNetCore.OData/Results/IODataErrorResult.cs index 307a59e41..8ef45ddfa 100644 --- a/src/Microsoft.AspNetCore.OData/Results/IODataErrorResult.cs +++ b/src/Microsoft.AspNetCore.OData/Results/IODataErrorResult.cs @@ -7,18 +7,17 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +/// +/// Provide the interface for the details of a given OData error result. +/// +public interface IODataErrorResult { /// - /// Provide the interface for the details of a given OData error result. + /// OData error. /// - public interface IODataErrorResult - { - /// - /// OData error. - /// #pragma warning disable CA1716 // Identifiers should not match keywords - ODataError Error { get; } + ODataError Error { get; } #pragma warning restore CA1716 // Identifiers should not match keywords - } } diff --git a/src/Microsoft.AspNetCore.OData/Results/NotFoundODataResult.cs b/src/Microsoft.AspNetCore.OData/Results/NotFoundODataResult.cs index 7c88f87c4..e1dd389b0 100644 --- a/src/Microsoft.AspNetCore.OData/Results/NotFoundODataResult.cs +++ b/src/Microsoft.AspNetCore.OData/Results/NotFoundODataResult.cs @@ -11,62 +11,61 @@ using Microsoft.OData; using ErrorUtils = Microsoft.AspNetCore.OData.Error; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +/// +/// Represents a result that when executed will produce a Not Found (404) response. +/// +/// This result creates an with status code: 404. +public class NotFoundODataResult : NotFoundResult, IODataErrorResult { + private const string errorCode = "404"; + /// - /// Represents a result that when executed will produce a Not Found (404) response. + /// OData error. /// - /// This result creates an with status code: 404. - public class NotFoundODataResult : NotFoundResult, IODataErrorResult - { - private const string errorCode = "404"; + public ODataError Error { get; } - /// - /// OData error. - /// - public ODataError Error { get; } - - /// - /// Initializes a new instance of the class. - /// - /// Error message. - public NotFoundODataResult(string message) + /// + /// Initializes a new instance of the class. + /// + /// Error message. + public NotFoundODataResult(string message) + { + if (string.IsNullOrEmpty(message)) { - if (string.IsNullOrEmpty(message)) - { - throw ErrorUtils.ArgumentNullOrEmpty(nameof(message)); - } - - Error = new ODataError - { - Message = message, - Code = errorCode - }; + throw ErrorUtils.ArgumentNullOrEmpty(nameof(message)); } - /// - /// Initializes a new instance of the class. - /// - /// An object. - public NotFoundODataResult(ODataError odataError) + Error = new ODataError { - if (odataError == null) - { - throw ErrorUtils.ArgumentNull(nameof(odataError)); - } + Message = message, + Code = errorCode + }; + } - Error = odataError; + /// + /// Initializes a new instance of the class. + /// + /// An object. + public NotFoundODataResult(ODataError odataError) + { + if (odataError == null) + { + throw ErrorUtils.ArgumentNull(nameof(odataError)); } - /// - public async override Task ExecuteResultAsync(ActionContext context) + Error = odataError; + } + + /// + public async override Task ExecuteResultAsync(ActionContext context) + { + ObjectResult objectResult = new ObjectResult(Error) { - ObjectResult objectResult = new ObjectResult(Error) - { - StatusCode = StatusCodes.Status404NotFound - }; + StatusCode = StatusCodes.Status404NotFound + }; - await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); - } + await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); } } diff --git a/src/Microsoft.AspNetCore.OData/Results/ODataErrorResult.cs b/src/Microsoft.AspNetCore.OData/Results/ODataErrorResult.cs index 8c41f72c3..ffc482d84 100644 --- a/src/Microsoft.AspNetCore.OData/Results/ODataErrorResult.cs +++ b/src/Microsoft.AspNetCore.OData/Results/ODataErrorResult.cs @@ -12,66 +12,65 @@ using Microsoft.OData; using ErrorUtils = Microsoft.AspNetCore.OData.Error; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +/// +/// Represents a result that when executed will produce an . +/// +/// This result creates an response. +public class ODataErrorResult : ActionResult, IODataErrorResult { /// - /// Represents a result that when executed will produce an . + /// OData error. /// - /// This result creates an response. - public class ODataErrorResult : ActionResult, IODataErrorResult - { - /// - /// OData error. - /// - public ODataError Error { get; } + public ODataError Error { get; } - /// - /// Initializes a new instance of the class. - /// - /// Error code. - /// Error message. - public ODataErrorResult(string errorCode, string message) + /// + /// Initializes a new instance of the class. + /// + /// Error code. + /// Error message. + public ODataErrorResult(string errorCode, string message) + { + if (string.IsNullOrEmpty(errorCode)) { - if (string.IsNullOrEmpty(errorCode)) - { - throw ErrorUtils.ArgumentNullOrEmpty(nameof(errorCode)); - } - - if (string.IsNullOrEmpty(message)) - { - throw ErrorUtils.ArgumentNullOrEmpty(nameof(message)); - } + throw ErrorUtils.ArgumentNullOrEmpty(nameof(errorCode)); + } - Error = new ODataError - { - Code = errorCode, - Message = message - }; + if (string.IsNullOrEmpty(message)) + { + throw ErrorUtils.ArgumentNullOrEmpty(nameof(message)); } - /// - /// Initializes a new instance of the class. - /// - /// An object. - public ODataErrorResult(ODataError odataError) + Error = new ODataError { - if (odataError == null) - { - throw ErrorUtils.ArgumentNull(nameof(odataError)); - } + Code = errorCode, + Message = message + }; + } - Error = odataError; + /// + /// Initializes a new instance of the class. + /// + /// An object. + public ODataErrorResult(ODataError odataError) + { + if (odataError == null) + { + throw ErrorUtils.ArgumentNull(nameof(odataError)); } - /// - public async override Task ExecuteResultAsync(ActionContext context) + Error = odataError; + } + + /// + public async override Task ExecuteResultAsync(ActionContext context) + { + ObjectResult objectResult = new ObjectResult(Error) { - ObjectResult objectResult = new ObjectResult(Error) - { - StatusCode = Convert.ToInt32(Error.Code, CultureInfo.InvariantCulture) - }; + StatusCode = Convert.ToInt32(Error.Code, CultureInfo.InvariantCulture) + }; - await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); - } + await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); } } diff --git a/src/Microsoft.AspNetCore.OData/Results/PageResult.cs b/src/Microsoft.AspNetCore.OData/Results/PageResult.cs index f0c6711df..fd862e711 100644 --- a/src/Microsoft.AspNetCore.OData/Results/PageResult.cs +++ b/src/Microsoft.AspNetCore.OData/Results/PageResult.cs @@ -9,65 +9,64 @@ using System.Collections.Generic; using System.Runtime.Serialization; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +/// +/// Represents a feed of entities that includes additional information that OData formats support. +/// +/// +/// Currently limited to: +/// +/// The Count of all matching entities on the server (requested using $count=true). +/// The NextLink to retrieve the next page of results (added if the server enforces Server Driven Paging). +/// +/// +[DataContract] +public abstract class PageResult { + private long? _count; + /// - /// Represents a feed of entities that includes additional information that OData formats support. + /// Initializes a new instance of the class. /// - /// - /// Currently limited to: - /// - /// The Count of all matching entities on the server (requested using $count=true). - /// The NextLink to retrieve the next page of results (added if the server enforces Server Driven Paging). - /// - /// - [DataContract] - public abstract class PageResult + /// The link for the next page of items in the feed. + /// The total count of items in the feed. + protected PageResult(Uri nextPageLink, long? count) { - private long? _count; + NextPageLink = nextPageLink; + Count = count; + } - /// - /// Initializes a new instance of the class. - /// - /// The link for the next page of items in the feed. - /// The total count of items in the feed. - protected PageResult(Uri nextPageLink, long? count) + /// + /// Gets the link for the next page of items in the feed. + /// + [DataMember] + public Uri NextPageLink { get; } + + /// + /// Gets the total count of items in the feed. + /// + [DataMember] + public long? Count + { + get { - NextPageLink = nextPageLink; - Count = count; + return _count; } - - /// - /// Gets the link for the next page of items in the feed. - /// - [DataMember] - public Uri NextPageLink { get; } - - /// - /// Gets the total count of items in the feed. - /// - [DataMember] - public long? Count + private set { - get + if (value.HasValue && value.Value < 0) { - return _count; + throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value.Value, 0); } - private set - { - if (value.HasValue && value.Value < 0) - { - throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value.Value, 0); - } - _count = value; - } + _count = value; } - - /// - /// Projects the result to a . - /// - /// An representing the page result. - public abstract IDictionary ToDictionary(); } + + /// + /// Projects the result to a . + /// + /// An representing the page result. + public abstract IDictionary ToDictionary(); } diff --git a/src/Microsoft.AspNetCore.OData/Results/PageResultOfT.cs b/src/Microsoft.AspNetCore.OData/Results/PageResultOfT.cs index 277509909..d7607f149 100644 --- a/src/Microsoft.AspNetCore.OData/Results/PageResultOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Results/PageResultOfT.cs @@ -11,76 +11,75 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +/// Represents a feed of entities that includes additional information that OData formats support. +/// +/// Currently limited to: +/// +/// The Count of all matching entities on the server (requested using $count=true). +/// The NextLink to retrieve the next page of results (added if the server enforces Server Driven Paging). +/// +/// +[SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "Collection suffix not appropriate")] +[DataContract] +public class PageResult : PageResult, IEnumerable { - /// Represents a feed of entities that includes additional information that OData formats support. - /// - /// Currently limited to: - /// - /// The Count of all matching entities on the server (requested using $count=true). - /// The NextLink to retrieve the next page of results (added if the server enforces Server Driven Paging). - /// - /// - [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "Collection suffix not appropriate")] - [DataContract] - public class PageResult : PageResult, IEnumerable + /// + /// Creates a partial set of results - used when server driven paging is enabled. + /// + /// The subset of matching results that should be serialized in this page. + /// A link to the next page of matching results (if more exists). + /// A total count of matching results so clients can know the number of matches on the server. + public PageResult(IEnumerable items, Uri nextPageLink, long? count) + : base(nextPageLink, count) { - /// - /// Creates a partial set of results - used when server driven paging is enabled. - /// - /// The subset of matching results that should be serialized in this page. - /// A link to the next page of matching results (if more exists). - /// A total count of matching results so clients can know the number of matches on the server. - public PageResult(IEnumerable items, Uri nextPageLink, long? count) - : base(nextPageLink, count) - { - Items = items ?? throw Error.ArgumentNull(nameof(items)); - } + Items = items ?? throw Error.ArgumentNull(nameof(items)); + } - /// - /// Gets the collection of entities for this feed. - /// - [DataMember] - public IEnumerable Items { get; private set; } + /// + /// Gets the collection of entities for this feed. + /// + [DataMember] + public IEnumerable Items { get; private set; } - /// - /// Returns an enumerator that iterates through a collection. - /// - /// An object that can be used to iterate through the collection. - public IEnumerator GetEnumerator() - { - return Items.GetEnumerator(); - } + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + public IEnumerator GetEnumerator() + { + return Items.GetEnumerator(); + } - /// - /// Returns an enumerator that iterates through a collection. - /// - /// An object that can be used to iterate through the collection. - IEnumerator IEnumerable.GetEnumerator() + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return Items.GetEnumerator(); + } + + /// + /// Projects the result to a . + /// + /// An representing the page result. + public override IDictionary ToDictionary() + { + Dictionary dictionary = new Dictionary(); + dictionary["items"] = Items; + + if (NextPageLink != null) { - return Items.GetEnumerator(); + dictionary["nextpagelink"] = NextPageLink.OriginalString; } - /// - /// Projects the result to a . - /// - /// An representing the page result. - public override IDictionary ToDictionary() + if (Count != null) { - Dictionary dictionary = new Dictionary(); - dictionary["items"] = Items; - - if (NextPageLink != null) - { - dictionary["nextpagelink"] = NextPageLink.OriginalString; - } - - if (Count != null) - { - dictionary["count"]= Count.Value; - } - - return dictionary; + dictionary["count"]= Count.Value; } + + return dictionary; } } diff --git a/src/Microsoft.AspNetCore.OData/Results/PageResultValueConverter.cs b/src/Microsoft.AspNetCore.OData/Results/PageResultValueConverter.cs index 0eea8aea8..53811754c 100644 --- a/src/Microsoft.AspNetCore.OData/Results/PageResultValueConverter.cs +++ b/src/Microsoft.AspNetCore.OData/Results/PageResultValueConverter.cs @@ -10,69 +10,68 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +internal class PageResultValueConverter : JsonConverterFactory { - internal class PageResultValueConverter : JsonConverterFactory + public override bool CanConvert(Type typeToConvert) { - public override bool CanConvert(Type typeToConvert) + if (typeToConvert == null || !typeToConvert.IsGenericType) { - if (typeToConvert == null || !typeToConvert.IsGenericType) - { - return false; - } - - Type generaticType = typeToConvert.GetGenericTypeDefinition(); - return generaticType == typeof(PageResult<>); + return false; } - /// - /// Creates a converter for a specified type. - /// - /// The type handled by the converter. - /// The serialization options to use. - /// A converter for which T is compatible with typeToConvert. - public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) - { - // Since 'type' is tested in 'CanConvert()', it must be a generic type - Type generaticType = type.GetGenericTypeDefinition(); - Type entityType = type.GetGenericArguments()[0]; + Type generaticType = typeToConvert.GetGenericTypeDefinition(); + return generaticType == typeof(PageResult<>); + } - if (generaticType == typeof(PageResult<>)) - { - return (JsonConverter)Activator.CreateInstance(typeof(PageResultConverter<>).MakeGenericType(new Type[] { entityType })); - } + /// + /// Creates a converter for a specified type. + /// + /// The type handled by the converter. + /// The serialization options to use. + /// A converter for which T is compatible with typeToConvert. + public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) + { + // Since 'type' is tested in 'CanConvert()', it must be a generic type + Type generaticType = type.GetGenericTypeDefinition(); + Type entityType = type.GetGenericArguments()[0]; - return null; + if (generaticType == typeof(PageResult<>)) + { + return (JsonConverter)Activator.CreateInstance(typeof(PageResultConverter<>).MakeGenericType(new Type[] { entityType })); } + + return null; } +} - internal class PageResultConverter : JsonConverter> +internal class PageResultConverter : JsonConverter> +{ + public override PageResult Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override PageResult Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + Contract.Assert(false, "PageResult{TEntity} should never be deserialized into"); + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, PageResult value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WritePropertyName("items"); + JsonSerializer.Serialize(writer, value.Items, options); + + if (value.NextPageLink != null) { - Contract.Assert(false, "PageResult{TEntity} should never be deserialized into"); - throw new NotImplementedException(); + writer.WritePropertyName("nextpagelink"); + writer.WriteStringValue(value.NextPageLink.OriginalString); } - public override void Write(Utf8JsonWriter writer, PageResult value, JsonSerializerOptions options) + if (value.Count != null) { - writer.WriteStartObject(); - writer.WritePropertyName("items"); - JsonSerializer.Serialize(writer, value.Items, options); - - if (value.NextPageLink != null) - { - writer.WritePropertyName("nextpagelink"); - writer.WriteStringValue(value.NextPageLink.OriginalString); - } - - if (value.Count != null) - { - writer.WritePropertyName("count"); - writer.WriteNumberValue(value.Count.Value); - } - - writer.WriteEndObject(); + writer.WritePropertyName("count"); + writer.WriteNumberValue(value.Count.Value); } + + writer.WriteEndObject(); } } diff --git a/src/Microsoft.AspNetCore.OData/Results/ResultHelpers.cs b/src/Microsoft.AspNetCore.OData/Results/ResultHelpers.cs index cd0162ba6..711311e45 100644 --- a/src/Microsoft.AspNetCore.OData/Results/ResultHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Results/ResultHelpers.cs @@ -22,175 +22,174 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +internal static class ResultHelpers { - internal static class ResultHelpers - { - public const string EntityIdHeaderName = "OData-EntityId"; + public const string EntityIdHeaderName = "OData-EntityId"; - /// This signature uses types that are AspNetCore-specific. - public static Uri GenerateODataLink(HttpRequest request, object entity, bool isEntityId) + /// This signature uses types that are AspNetCore-specific. + public static Uri GenerateODataLink(HttpRequest request, object entity, bool isEntityId) + { + IEdmModel model = request.GetModel(); + if (model == null) { - IEdmModel model = request.GetModel(); - if (model == null) - { - throw new InvalidOperationException(SRResources.RequestMustHaveModel); - } + throw new InvalidOperationException(SRResources.RequestMustHaveModel); + } - ODataPath path = request.ODataFeature().Path; - if (path == null) - { - throw new InvalidOperationException(SRResources.ODataPathMissing); - } + ODataPath path = request.ODataFeature().Path; + if (path == null) + { + throw new InvalidOperationException(SRResources.ODataPathMissing); + } - IEdmNavigationSource navigationSource = path.GetNavigationSource(); - if (navigationSource == null) - { - throw new InvalidOperationException(SRResources.NavigationSourceMissingDuringSerialization); - } + IEdmNavigationSource navigationSource = path.GetNavigationSource(); + if (navigationSource == null) + { + throw new InvalidOperationException(SRResources.NavigationSourceMissingDuringSerialization); + } - ODataSerializerContext serializerContext = new ODataSerializerContext - { - NavigationSource = navigationSource, - Model = model, - MetadataLevel = ODataMetadataLevel.Full, // Used internally to always calculate the links. - Request = request, - Path = path - }; + ODataSerializerContext serializerContext = new ODataSerializerContext + { + NavigationSource = navigationSource, + Model = model, + MetadataLevel = ODataMetadataLevel.Full, // Used internally to always calculate the links. + Request = request, + Path = path + }; - IEdmEntityTypeReference entityType = GetEntityType(model, entity); - ResourceContext resourceContext = new ResourceContext(serializerContext, entityType, entity); + IEdmEntityTypeReference entityType = GetEntityType(model, entity); + ResourceContext resourceContext = new ResourceContext(serializerContext, entityType, entity); - return GenerateODataLink(resourceContext, isEntityId); - } + return GenerateODataLink(resourceContext, isEntityId); + } - /// This signature uses types that are AspNetCore-specific. - public static void AddEntityId(HttpResponse response, Func entityId) + /// This signature uses types that are AspNetCore-specific. + public static void AddEntityId(HttpResponse response, Func entityId) + { + if (response.StatusCode == (int)HttpStatusCode.NoContent) { - if (response.StatusCode == (int)HttpStatusCode.NoContent) - { - response.Headers.Append(EntityIdHeaderName, entityId().AbsoluteUri); - } + response.Headers.Append(EntityIdHeaderName, entityId().AbsoluteUri); } + } - public static void AddServiceVersion(HttpResponse response, Func version) + public static void AddServiceVersion(HttpResponse response, Func version) + { + if (response.StatusCode == (int)HttpStatusCode.NoContent) { - if (response.StatusCode == (int)HttpStatusCode.NoContent) - { - response.Headers[ODataVersionConstraint.ODataServiceVersionHeader] = version(); - } + response.Headers[ODataVersionConstraint.ODataServiceVersionHeader] = version(); } + } - public static Uri GenerateODataLink(ResourceContext resourceContext, bool isEntityId) - { - Contract.Assert(resourceContext != null); - - // Generate location or entityId header from request Uri and key, if Post to a containment. - // Link builder is not used, since it is also for generating ID, Edit, Read links, etc. scenarios, where - // request Uri is not used. - if (resourceContext.NavigationSource.NavigationSourceKind() == EdmNavigationSourceKind.ContainedEntitySet) - { - return GenerateContainmentODataPathSegments(resourceContext, isEntityId); - } + public static Uri GenerateODataLink(ResourceContext resourceContext, bool isEntityId) + { + Contract.Assert(resourceContext != null); - NavigationSourceLinkBuilderAnnotation linkBuilder = - resourceContext.EdmModel.GetNavigationSourceLinkBuilder(resourceContext.NavigationSource); - Contract.Assert(linkBuilder != null); + // Generate location or entityId header from request Uri and key, if Post to a containment. + // Link builder is not used, since it is also for generating ID, Edit, Read links, etc. scenarios, where + // request Uri is not used. + if (resourceContext.NavigationSource.NavigationSourceKind() == EdmNavigationSourceKind.ContainedEntitySet) + { + return GenerateContainmentODataPathSegments(resourceContext, isEntityId); + } - Uri idLink = linkBuilder.BuildIdLink(resourceContext); - if (isEntityId) - { - if (idLink == null) - { - throw Error.InvalidOperation( - SRResources.IdLinkNullForEntityIdHeader, - resourceContext.NavigationSource.Name); - } + NavigationSourceLinkBuilderAnnotation linkBuilder = + resourceContext.EdmModel.GetNavigationSourceLinkBuilder(resourceContext.NavigationSource); + Contract.Assert(linkBuilder != null); - return idLink; - } - - Uri editLink = linkBuilder.BuildEditLink(resourceContext); - if (editLink == null) + Uri idLink = linkBuilder.BuildIdLink(resourceContext); + if (isEntityId) + { + if (idLink == null) { - if (idLink != null) - { - return idLink; - } - throw Error.InvalidOperation( - SRResources.EditLinkNullForLocationHeader, + SRResources.IdLinkNullForEntityIdHeader, resourceContext.NavigationSource.Name); } - return editLink; + return idLink; } - private static Uri GenerateContainmentODataPathSegments(ResourceContext resourceContext, bool isEntityId) + Uri editLink = linkBuilder.BuildEditLink(resourceContext); + if (editLink == null) { - Contract.Assert(resourceContext != null); - Contract.Assert( - resourceContext.NavigationSource.NavigationSourceKind() == EdmNavigationSourceKind.ContainedEntitySet); - Contract.Assert(resourceContext.Request != null); - - ODataPath path = resourceContext.Request.ODataFeature().Path; - if (path == null) + if (idLink != null) { - throw Error.InvalidOperation(SRResources.ODataPathMissing); + return idLink; } - path = new ContainmentPathBuilder().TryComputeCanonicalContainingPath(path); + throw Error.InvalidOperation( + SRResources.EditLinkNullForLocationHeader, + resourceContext.NavigationSource.Name); + } - List odataPath = path.ToList(); + return editLink; + } - // create a template entity set if it's contained entity set - IEdmEntitySet entitySet = resourceContext.NavigationSource as IEdmEntitySet; - if (entitySet == null) - { - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - entitySet = new EdmEntitySet(container, resourceContext.NavigationSource.Name, resourceContext.NavigationSource.EntityType); - } + private static Uri GenerateContainmentODataPathSegments(ResourceContext resourceContext, bool isEntityId) + { + Contract.Assert(resourceContext != null); + Contract.Assert( + resourceContext.NavigationSource.NavigationSourceKind() == EdmNavigationSourceKind.ContainedEntitySet); + Contract.Assert(resourceContext.Request != null); - odataPath.Add(new EntitySetSegment(entitySet)); - odataPath.Add(new KeySegment(ConventionsHelpers.GetEntityKey(resourceContext), - resourceContext.StructuredType as IEdmEntityType, resourceContext.NavigationSource)); + ODataPath path = resourceContext.Request.ODataFeature().Path; + if (path == null) + { + throw Error.InvalidOperation(SRResources.ODataPathMissing); + } - if (!isEntityId) - { - bool isSameType = resourceContext.StructuredType == resourceContext.NavigationSource.EntityType; - if (!isSameType) - { - odataPath.Add(new TypeSegment(resourceContext.StructuredType, resourceContext.NavigationSource)); - } - } + path = new ContainmentPathBuilder().TryComputeCanonicalContainingPath(path); - string odataLink = resourceContext.Request.CreateODataLink(odataPath); - return odataLink == null ? null : new Uri(odataLink); + List odataPath = path.ToList(); + + // create a template entity set if it's contained entity set + IEdmEntitySet entitySet = resourceContext.NavigationSource as IEdmEntitySet; + if (entitySet == null) + { + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + entitySet = new EdmEntitySet(container, resourceContext.NavigationSource.Name, resourceContext.NavigationSource.EntityType); } + odataPath.Add(new EntitySetSegment(entitySet)); + odataPath.Add(new KeySegment(ConventionsHelpers.GetEntityKey(resourceContext), + resourceContext.StructuredType as IEdmEntityType, resourceContext.NavigationSource)); - private static IEdmEntityTypeReference GetEntityType(IEdmModel model, object entity) + if (!isEntityId) { - Type entityType = entity.GetType(); - IEdmTypeReference edmType; - if (entity is IEdmObject edmObject) - { - edmType = edmObject.GetEdmType(); - } - else + bool isSameType = resourceContext.StructuredType == resourceContext.NavigationSource.EntityType; + if (!isSameType) { - edmType = model.GetEdmTypeReference(entityType); - } - if (edmType == null) - { - throw Error.InvalidOperation(SRResources.ResourceTypeNotInModel, entityType.FullName); - } - if (!edmType.IsEntity()) - { - throw Error.InvalidOperation(SRResources.TypeMustBeEntity, edmType.FullName()); + odataPath.Add(new TypeSegment(resourceContext.StructuredType, resourceContext.NavigationSource)); } + } - return edmType.AsEntity(); + string odataLink = resourceContext.Request.CreateODataLink(odataPath); + return odataLink == null ? null : new Uri(odataLink); + } + + + private static IEdmEntityTypeReference GetEntityType(IEdmModel model, object entity) + { + Type entityType = entity.GetType(); + IEdmTypeReference edmType; + if (entity is IEdmObject edmObject) + { + edmType = edmObject.GetEdmType(); + } + else + { + edmType = model.GetEdmTypeReference(entityType); + } + if (edmType == null) + { + throw Error.InvalidOperation(SRResources.ResourceTypeNotInModel, entityType.FullName); + } + if (!edmType.IsEntity()) + { + throw Error.InvalidOperation(SRResources.TypeMustBeEntity, edmType.FullName()); } + + return edmType.AsEntity(); } } diff --git a/src/Microsoft.AspNetCore.OData/Results/SingleResult.cs b/src/Microsoft.AspNetCore.OData/Results/SingleResult.cs index 490627fb6..a8cb5e861 100644 --- a/src/Microsoft.AspNetCore.OData/Results/SingleResult.cs +++ b/src/Microsoft.AspNetCore.OData/Results/SingleResult.cs @@ -8,39 +8,38 @@ using System; using System.Linq; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +/// +/// Represents an containing zero or one entities. Use together with an +/// [EnableQuery]. +/// +public abstract class SingleResult { /// - /// Represents an containing zero or one entities. Use together with an - /// [EnableQuery]. + /// Initializes a new instance of the class. /// - public abstract class SingleResult + /// The containing zero or one entities. + protected SingleResult(IQueryable queryable) { - /// - /// Initializes a new instance of the class. - /// - /// The containing zero or one entities. - protected SingleResult(IQueryable queryable) - { - Queryable = queryable ?? throw new ArgumentNullException(nameof(queryable)); - } + Queryable = queryable ?? throw new ArgumentNullException(nameof(queryable)); + } - /// - /// The containing zero or one entities. - /// - public IQueryable Queryable { get;} + /// + /// The containing zero or one entities. + /// + public IQueryable Queryable { get;} - /// - /// Creates a from an . A helper method to instantiate - /// a object without having to explicitly specify the type - /// . - /// - /// The type of the data in the data source. - /// The containing zero or one entities. - /// The created . - public static SingleResult Create(IQueryable queryable) - { - return new SingleResult(queryable); - } + /// + /// Creates a from an . A helper method to instantiate + /// a object without having to explicitly specify the type + /// . + /// + /// The type of the data in the data source. + /// The containing zero or one entities. + /// The created . + public static SingleResult Create(IQueryable queryable) + { + return new SingleResult(queryable); } } diff --git a/src/Microsoft.AspNetCore.OData/Results/SingleResultOfT.cs b/src/Microsoft.AspNetCore.OData/Results/SingleResultOfT.cs index 45f25ca58..4b823a8c7 100644 --- a/src/Microsoft.AspNetCore.OData/Results/SingleResultOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Results/SingleResultOfT.cs @@ -7,33 +7,32 @@ using System.Linq; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +/// +/// Represents an containing zero or one entities. Use together with an +/// [EnableQuery]. +/// +/// The type of the data in the data source. +public sealed class SingleResult : SingleResult { /// - /// Represents an containing zero or one entities. Use together with an - /// [EnableQuery]. + /// Initializes a new instance of the class. /// - /// The type of the data in the data source. - public sealed class SingleResult : SingleResult + /// The containing zero or one entities. + public SingleResult(IQueryable queryable) + : base(queryable) { - /// - /// Initializes a new instance of the class. - /// - /// The containing zero or one entities. - public SingleResult(IQueryable queryable) - : base(queryable) - { - } + } - /// - /// The containing zero or one entities. - /// - public new IQueryable Queryable + /// + /// The containing zero or one entities. + /// + public new IQueryable Queryable + { + get { - get - { - return base.Queryable as IQueryable; - } + return base.Queryable as IQueryable; } } } diff --git a/src/Microsoft.AspNetCore.OData/Results/SingleResultValueConverter.cs b/src/Microsoft.AspNetCore.OData/Results/SingleResultValueConverter.cs index 089bd5420..3e71a9fd7 100644 --- a/src/Microsoft.AspNetCore.OData/Results/SingleResultValueConverter.cs +++ b/src/Microsoft.AspNetCore.OData/Results/SingleResultValueConverter.cs @@ -11,60 +11,59 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +internal class SingleResultValueConverter : JsonConverterFactory { - internal class SingleResultValueConverter : JsonConverterFactory + /// + public override bool CanConvert(Type typeToConvert) { - /// - public override bool CanConvert(Type typeToConvert) + if (typeToConvert == null || !typeToConvert.IsGenericType) { - if (typeToConvert == null || !typeToConvert.IsGenericType) - { - return false; - } - - Type generaticType = typeToConvert.GetGenericTypeDefinition(); - return generaticType == typeof(SingleResult<>); + return false; } - /// - /// Creates a converter for a specified type. - /// - /// The type handled by the converter. - /// The serialization options to use. - /// A converter for which T is compatible with typeToConvert. - public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) - { - // Since 'type' is tested in 'CanConvert()', it must be a generic type - Type generaticType = type.GetGenericTypeDefinition(); - Type entityType = type.GetGenericArguments()[0]; + Type generaticType = typeToConvert.GetGenericTypeDefinition(); + return generaticType == typeof(SingleResult<>); + } - if (generaticType == typeof(SingleResult<>)) - { - return (JsonConverter)Activator.CreateInstance(typeof(SingleResultConverter<>).MakeGenericType(new Type[] { entityType })); - } + /// + /// Creates a converter for a specified type. + /// + /// The type handled by the converter. + /// The serialization options to use. + /// A converter for which T is compatible with typeToConvert. + public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) + { + // Since 'type' is tested in 'CanConvert()', it must be a generic type + Type generaticType = type.GetGenericTypeDefinition(); + Type entityType = type.GetGenericArguments()[0]; - return null; + if (generaticType == typeof(SingleResult<>)) + { + return (JsonConverter)Activator.CreateInstance(typeof(SingleResultConverter<>).MakeGenericType(new Type[] { entityType })); } + + return null; } +} - internal class SingleResultConverter : JsonConverter> +internal class SingleResultConverter : JsonConverter> +{ + public override SingleResult Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override SingleResult Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - Contract.Assert(false, "SingleResult{TEntity} should never be deserialized into"); - throw new NotImplementedException(); - } + Contract.Assert(false, "SingleResult{TEntity} should never be deserialized into"); + throw new NotImplementedException(); + } - public override void Write(Utf8JsonWriter writer, SingleResult value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SingleResult value, JsonSerializerOptions options) + { + if (value != null) { - if (value != null) + var singleObject = value.Queryable.FirstOrDefault(); + if (singleObject != null) { - var singleObject = value.Queryable.FirstOrDefault(); - if (singleObject != null) - { - JsonSerializer.Serialize(writer, singleObject, options); - } + JsonSerializer.Serialize(writer, singleObject, options); } } } diff --git a/src/Microsoft.AspNetCore.OData/Results/UnauthorizedODataResult.cs b/src/Microsoft.AspNetCore.OData/Results/UnauthorizedODataResult.cs index 3196b87b2..7f55f43dd 100644 --- a/src/Microsoft.AspNetCore.OData/Results/UnauthorizedODataResult.cs +++ b/src/Microsoft.AspNetCore.OData/Results/UnauthorizedODataResult.cs @@ -11,62 +11,61 @@ using Microsoft.OData; using ErrorUtils = Microsoft.AspNetCore.OData.Error; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +/// +/// Represents a result that when executed will produce an Unauthorized (401) response. +/// +/// This result creates an with status code: 401. +public class UnauthorizedODataResult : UnauthorizedResult, IODataErrorResult { + private const string errorCode = "401"; + /// - /// Represents a result that when executed will produce an Unauthorized (401) response. + /// OData error. /// - /// This result creates an with status code: 401. - public class UnauthorizedODataResult : UnauthorizedResult, IODataErrorResult - { - private const string errorCode = "401"; + public ODataError Error { get; } - /// - /// OData error. - /// - public ODataError Error { get; } - - /// - /// Initializes a new instance of the class. - /// - /// Error message. - public UnauthorizedODataResult(string message) + /// + /// Initializes a new instance of the class. + /// + /// Error message. + public UnauthorizedODataResult(string message) + { + if (string.IsNullOrEmpty(message)) { - if (string.IsNullOrEmpty(message)) - { - throw ErrorUtils.ArgumentNullOrEmpty(nameof(message)); - } - - Error = new ODataError - { - Message = message, - Code = errorCode - }; + throw ErrorUtils.ArgumentNullOrEmpty(nameof(message)); } - /// - /// Initializes a new instance of the class. - /// - /// An object. - public UnauthorizedODataResult(ODataError odataError) + Error = new ODataError { - if (odataError == null) - { - throw ErrorUtils.ArgumentNull(nameof(odataError)); - } + Message = message, + Code = errorCode + }; + } - Error = odataError; + /// + /// Initializes a new instance of the class. + /// + /// An object. + public UnauthorizedODataResult(ODataError odataError) + { + if (odataError == null) + { + throw ErrorUtils.ArgumentNull(nameof(odataError)); } - /// - public async override Task ExecuteResultAsync(ActionContext context) + Error = odataError; + } + + /// + public async override Task ExecuteResultAsync(ActionContext context) + { + ObjectResult objectResult = new ObjectResult(Error) { - ObjectResult objectResult = new ObjectResult(Error) - { - StatusCode = StatusCodes.Status401Unauthorized - }; + StatusCode = StatusCodes.Status401Unauthorized + }; - await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); - } + await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); } } diff --git a/src/Microsoft.AspNetCore.OData/Results/UnprocessableEntityODataResult .cs b/src/Microsoft.AspNetCore.OData/Results/UnprocessableEntityODataResult .cs index 314c35574..6f35bcfeb 100644 --- a/src/Microsoft.AspNetCore.OData/Results/UnprocessableEntityODataResult .cs +++ b/src/Microsoft.AspNetCore.OData/Results/UnprocessableEntityODataResult .cs @@ -11,62 +11,61 @@ using Microsoft.OData; using ErrorUtils = Microsoft.AspNetCore.OData.Error; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +/// +/// Represents a result that when executed will produce an UnprocessableEntity (422) response. +/// +/// This result creates an with status code: 422. +public class UnprocessableEntityODataResult : UnprocessableEntityResult, IODataErrorResult { + private const string errorCode = "422"; + /// - /// Represents a result that when executed will produce an UnprocessableEntity (422) response. + /// OData error. /// - /// This result creates an with status code: 422. - public class UnprocessableEntityODataResult : UnprocessableEntityResult, IODataErrorResult - { - private const string errorCode = "422"; + public ODataError Error { get; } - /// - /// OData error. - /// - public ODataError Error { get; } - - /// - /// Initializes a new instance of the class. - /// - /// Error message. - public UnprocessableEntityODataResult(string message) + /// + /// Initializes a new instance of the class. + /// + /// Error message. + public UnprocessableEntityODataResult(string message) + { + if (string.IsNullOrEmpty(message)) { - if (string.IsNullOrEmpty(message)) - { - throw ErrorUtils.ArgumentNullOrEmpty(nameof(message)); - } - - Error = new ODataError - { - Message = message, - Code = errorCode - }; + throw ErrorUtils.ArgumentNullOrEmpty(nameof(message)); } - /// - /// Initializes a new instance of the class. - /// - /// An object. - public UnprocessableEntityODataResult(ODataError odataError) + Error = new ODataError { - if (odataError == null) - { - throw ErrorUtils.ArgumentNull(nameof(odataError)); - } + Message = message, + Code = errorCode + }; + } - Error = odataError; + /// + /// Initializes a new instance of the class. + /// + /// An object. + public UnprocessableEntityODataResult(ODataError odataError) + { + if (odataError == null) + { + throw ErrorUtils.ArgumentNull(nameof(odataError)); } - /// - public async override Task ExecuteResultAsync(ActionContext context) + Error = odataError; + } + + /// + public async override Task ExecuteResultAsync(ActionContext context) + { + ObjectResult objectResult = new ObjectResult(Error) { - ObjectResult objectResult = new ObjectResult(Error) - { - StatusCode = StatusCodes.Status422UnprocessableEntity - }; + StatusCode = StatusCodes.Status422UnprocessableEntity + }; - await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); - } + await objectResult.ExecuteResultAsync(context).ConfigureAwait(false); } } diff --git a/src/Microsoft.AspNetCore.OData/Results/UpdatedODataResult.cs b/src/Microsoft.AspNetCore.OData/Results/UpdatedODataResult.cs index d5eea0d0d..c4dae913e 100644 --- a/src/Microsoft.AspNetCore.OData/Results/UpdatedODataResult.cs +++ b/src/Microsoft.AspNetCore.OData/Results/UpdatedODataResult.cs @@ -13,60 +13,59 @@ using Microsoft.AspNetCore.OData.Extensions; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Results +namespace Microsoft.AspNetCore.OData.Results; + +/// +/// Represents an action result that is a response to a PUT, PATCH, or a MERGE operation on an OData entity. +/// +/// The entity type. +/// This action result handles content negotiation and the HTTP prefer header. +public class UpdatedODataResult : ObjectResult { /// - /// Represents an action result that is a response to a PUT, PATCH, or a MERGE operation on an OData entity. + /// Initializes a new instance of the class. /// - /// The entity type. - /// This action result handles content negotiation and the HTTP prefer header. - public class UpdatedODataResult : ObjectResult + /// The updated entity. + public UpdatedODataResult(T entity) + : base(entity) + { + Entity = entity ?? throw new ArgumentNullException(nameof(entity)); + } + + /// + /// Gets the entity that was updated. + /// + public virtual T Entity { get; } + + /// + public async override Task ExecuteResultAsync(ActionContext context) { - /// - /// Initializes a new instance of the class. - /// - /// The updated entity. - public UpdatedODataResult(T entity) - : base(entity) + if (context == null) { - Entity = entity ?? throw new ArgumentNullException(nameof(entity)); + throw new ArgumentNullException(nameof(context)); } - /// - /// Gets the entity that was updated. - /// - public virtual T Entity { get; } + HttpResponse response = context.HttpContext.Response; + HttpRequest request = context.HttpContext.Request; + IActionResult result = GetInnerActionResult(request); + await result.ExecuteResultAsync(context).ConfigureAwait(false); + ResultHelpers.AddServiceVersion(response, () => ODataUtils.ODataVersionToString(request.GetODataVersion())); + } - /// - public async override Task ExecuteResultAsync(ActionContext context) + internal IActionResult GetInnerActionResult(HttpRequest request) + { + if (RequestPreferenceHelpers.RequestPrefersReturnContent(request.Headers)) { - if (context == null) + ObjectResult objectResult = new ObjectResult(Entity) { - throw new ArgumentNullException(nameof(context)); - } + StatusCode = StatusCodes.Status200OK + }; - HttpResponse response = context.HttpContext.Response; - HttpRequest request = context.HttpContext.Request; - IActionResult result = GetInnerActionResult(request); - await result.ExecuteResultAsync(context).ConfigureAwait(false); - ResultHelpers.AddServiceVersion(response, () => ODataUtils.ODataVersionToString(request.GetODataVersion())); + return objectResult; } - - internal IActionResult GetInnerActionResult(HttpRequest request) + else { - if (RequestPreferenceHelpers.RequestPrefersReturnContent(request.Headers)) - { - ObjectResult objectResult = new ObjectResult(Entity) - { - StatusCode = StatusCodes.Status200OK - }; - - return objectResult; - } - else - { - return new StatusCodeResult((int)HttpStatusCode.NoContent); - } + return new StatusCodeResult((int)HttpStatusCode.NoContent); } } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Attributes/ODataAttributeRoutingAttribute.cs b/src/Microsoft.AspNetCore.OData/Routing/Attributes/ODataAttributeRoutingAttribute.cs index 76e3bad2e..8c04e32eb 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Attributes/ODataAttributeRoutingAttribute.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Attributes/ODataAttributeRoutingAttribute.cs @@ -9,33 +9,32 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.Routing.Attributes -{ +namespace Microsoft.AspNetCore.OData.Routing.Attributes; - /// - /// When used to decorate a or Controller method, automatically opts that item into - /// OData routing conventions. - /// - /// - /// - /// NOTE: If your Controller inherits from , this attribute is NOT required. - /// - /// - /// To allow individual methods to opt out of the OData routing conventions, add the [ODataIgnored] attribute - /// to that method. - /// - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] - public sealed class ODataAttributeRoutingAttribute : Attribute - { - // The design: - // ODataController has this attribute, all controllers derived from ODataController will be considered as OData attribute routing. - // If any other controller decorated with this attribute, the route template of all actions will be considered as OData attribute routing. - // If any action decorated with this attribute, the route template of this action will be considered as OData attribute routing. +/// +/// When used to decorate a or Controller method, automatically opts that item into +/// OData routing conventions. +/// +/// +/// +/// NOTE: If your Controller inherits from , this attribute is NOT required. +/// +/// +/// To allow individual methods to opt out of the OData routing conventions, add the [ODataIgnored] attribute +/// to that method. +/// +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] +public sealed class ODataAttributeRoutingAttribute : Attribute +{ + // The design: + // ODataController has this attribute, all controllers derived from ODataController will be considered as OData attribute routing. - // If you want to mix asp.net core attribute and odata attribute routing, consider to create two methods in the controller. - // If you want to opt one action out OData attribute routing, using [NonODataAction] attribute. - } + // If any other controller decorated with this attribute, the route template of all actions will be considered as OData attribute routing. + // If any action decorated with this attribute, the route template of this action will be considered as OData attribute routing. + // If you want to mix asp.net core attribute and odata attribute routing, consider to create two methods in the controller. + // If you want to opt one action out OData attribute routing, using [NonODataAction] attribute. } + diff --git a/src/Microsoft.AspNetCore.OData/Routing/Attributes/ODataIgnoredAttribute.cs b/src/Microsoft.AspNetCore.OData/Routing/Attributes/ODataIgnoredAttribute.cs index 3cab16318..2e4b14d62 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Attributes/ODataIgnoredAttribute.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Attributes/ODataIgnoredAttribute.cs @@ -8,16 +8,15 @@ using System; using Microsoft.AspNetCore.Mvc; -namespace Microsoft.AspNetCore.OData.Routing.Attributes -{ +namespace Microsoft.AspNetCore.OData.Routing.Attributes; - /// - /// When used to decorate a or Controller method, instructs OData to exclude that - /// item from the OData routing conventions. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] - public sealed class ODataIgnoredAttribute : Attribute - { - } +/// +/// When used to decorate a or Controller method, instructs OData to exclude that +/// item from the OData routing conventions. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] +public sealed class ODataIgnoredAttribute : Attribute +{ } + diff --git a/src/Microsoft.AspNetCore.OData/Routing/Attributes/ODataRouteComponentAttribute.cs b/src/Microsoft.AspNetCore.OData/Routing/Attributes/ODataRouteComponentAttribute.cs index acf54dac0..2a7c1d866 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Attributes/ODataRouteComponentAttribute.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Attributes/ODataRouteComponentAttribute.cs @@ -9,44 +9,43 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.Routing.Attributes +namespace Microsoft.AspNetCore.OData.Routing.Attributes; + + +/// +/// When used to decorate a or Controller method (including ), +/// instructs OData which set of to use. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +public class ODataRouteComponentAttribute : Attribute { /// - /// When used to decorate a or Controller method (including ), - /// instructs OData which set of to use. + /// Instructs OData to use the default RoutePrefix, which equals + /// , for the decorated Controller or Controller method. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] - public class ODataRouteComponentAttribute : Attribute + public ODataRouteComponentAttribute() + : this(string.Empty) { + } - /// - /// Instructs OData to use the default RoutePrefix, which equals - /// , for the decorated Controller or Controller method. - /// - public ODataRouteComponentAttribute() - : this(string.Empty) - { - } - - /// - /// Instructs OData to use the specified for the decorated Controller or Controller method. - /// - /// The key in to use . - /// - /// Ensure this model is registered with OData by calling - /// services.AddOData(options => options.AddRouteComponent()). - /// - public ODataRouteComponentAttribute(string routePrefix) - { - RoutePrefix = routePrefix ?? throw Error.ArgumentNull(nameof(routePrefix)); - } - - /// - /// Gets the specified model name. - /// - public string RoutePrefix { get; } - + /// + /// Instructs OData to use the specified for the decorated Controller or Controller method. + /// + /// The key in to use . + /// + /// Ensure this model is registered with OData by calling + /// services.AddOData(options => options.AddRouteComponent()). + /// + public ODataRouteComponentAttribute(string routePrefix) + { + RoutePrefix = routePrefix ?? throw Error.ArgumentNull(nameof(routePrefix)); } + /// + /// Gets the specified model name. + /// + public string RoutePrefix { get; } + } + diff --git a/src/Microsoft.AspNetCore.OData/Routing/Controllers/MetadataController.cs b/src/Microsoft.AspNetCore.OData/Routing/Controllers/MetadataController.cs index 9339f15ff..612ddb399 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Controllers/MetadataController.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Controllers/MetadataController.cs @@ -12,110 +12,109 @@ using Microsoft.OData.Edm; using Microsoft.OData.Edm.Csdl; -namespace Microsoft.AspNetCore.OData.Routing.Controllers +namespace Microsoft.AspNetCore.OData.Routing.Controllers; + +/// +/// Represents a controller for generating OData service and metadata ($metadata) documents. +/// +public class MetadataController : ControllerBase { + private static readonly Version _defaultEdmxVersion = new Version(4, 0); + /// - /// Represents a controller for generating OData service and metadata ($metadata) documents. + /// Generates the OData $metadata document. /// - public class MetadataController : ControllerBase + /// The representing $metadata. + [HttpGet] + public IEdmModel GetMetadata() { - private static readonly Version _defaultEdmxVersion = new Version(4, 0); - - /// - /// Generates the OData $metadata document. - /// - /// The representing $metadata. - [HttpGet] - public IEdmModel GetMetadata() - { - return GetModel(); - } + return GetModel(); + } - /// - /// Generates the OData service document. - /// - /// The service document for the service. - [HttpGet] - public ODataServiceDocument GetServiceDocument() - { - IEdmModel model = GetModel(); - return model.GenerateServiceDocument(); + /// + /// Generates the OData service document. + /// + /// The service document for the service. + [HttpGet] + public ODataServiceDocument GetServiceDocument() + { + IEdmModel model = GetModel(); + return model.GenerateServiceDocument(); #if false - ODataServiceDocument serviceDocument = new ODataServiceDocument(); - IEdmEntityContainer container = model.EntityContainer; + ODataServiceDocument serviceDocument = new ODataServiceDocument(); + IEdmEntityContainer container = model.EntityContainer; - // Add EntitySets into service document - serviceDocument.EntitySets = container.EntitySets().Select( - e => GetODataEntitySetInfo(model.GetNavigationSourceUrl(e).ToString(), e.Name)); + // Add EntitySets into service document + serviceDocument.EntitySets = container.EntitySets().Select( + e => GetODataEntitySetInfo(model.GetNavigationSourceUrl(e).ToString(), e.Name)); - // Add Singletons into the service document - IEnumerable singletons = container.Elements.OfType(); - serviceDocument.Singletons = singletons.Select( - e => GetODataSingletonInfo(model.GetNavigationSourceUrl(e).ToString(), e.Name)); + // Add Singletons into the service document + IEnumerable singletons = container.Elements.OfType(); + serviceDocument.Singletons = singletons.Select( + e => GetODataSingletonInfo(model.GetNavigationSourceUrl(e).ToString(), e.Name)); - // Add FunctionImports into service document - // ODL spec says: - // The edm:FunctionImport for a parameterless function MAY include the IncludeInServiceDocument attribute - // whose Boolean value indicates whether the function import is advertised in the service document. - // If no value is specified for this attribute, its value defaults to false. + // Add FunctionImports into service document + // ODL spec says: + // The edm:FunctionImport for a parameterless function MAY include the IncludeInServiceDocument attribute + // whose Boolean value indicates whether the function import is advertised in the service document. + // If no value is specified for this attribute, its value defaults to false. - // Find all parameterless functions with "IncludeInServiceDocument = true" - IEnumerable functionImports = container.Elements.OfType() - .Where(f => !f.Function.Parameters.Any() && f.IncludeInServiceDocument); + // Find all parameterless functions with "IncludeInServiceDocument = true" + IEnumerable functionImports = container.Elements.OfType() + .Where(f => !f.Function.Parameters.Any() && f.IncludeInServiceDocument); - serviceDocument.FunctionImports = functionImports.Distinct(new FunctionImportComparer()) - .Select(f => GetODataFunctionImportInfo(f.Name)); + serviceDocument.FunctionImports = functionImports.Distinct(new FunctionImportComparer()) + .Select(f => GetODataFunctionImportInfo(f.Name)); - return serviceDocument; + return serviceDocument; #endif - } + } - /* - private static ODataEntitySetInfo GetODataEntitySetInfo(string url, string name) + /* + private static ODataEntitySetInfo GetODataEntitySetInfo(string url, string name) + { + ODataEntitySetInfo info = new ODataEntitySetInfo { - ODataEntitySetInfo info = new ODataEntitySetInfo - { - Name = name, // Required for JSON support - Url = new Uri(url, UriKind.Relative) - }; + Name = name, // Required for JSON support + Url = new Uri(url, UriKind.Relative) + }; - return info; - } + return info; + } - private static ODataSingletonInfo GetODataSingletonInfo(string url, string name) + private static ODataSingletonInfo GetODataSingletonInfo(string url, string name) + { + ODataSingletonInfo info = new ODataSingletonInfo { - ODataSingletonInfo info = new ODataSingletonInfo - { - Name = name, - Url = new Uri(url, UriKind.Relative) - }; + Name = name, + Url = new Uri(url, UriKind.Relative) + }; - return info; - } + return info; + } - private static ODataFunctionImportInfo GetODataFunctionImportInfo(string name) + private static ODataFunctionImportInfo GetODataFunctionImportInfo(string name) + { + ODataFunctionImportInfo info = new ODataFunctionImportInfo { - ODataFunctionImportInfo info = new ODataFunctionImportInfo - { - Name = name, - Url = new Uri(name, UriKind.Relative) // Relative to the OData root - }; + Name = name, + Url = new Uri(name, UriKind.Relative) // Relative to the OData root + }; - return info; - } - */ + return info; + } + */ - private IEdmModel GetModel() + private IEdmModel GetModel() + { + IEdmModel model = Request.GetModel(); + if (model == null) { - IEdmModel model = Request.GetModel(); - if (model == null) - { - throw new InvalidOperationException(SRResources.RequestMustHaveModel); - } - - model.SetEdmxVersion(_defaultEdmxVersion); - return model; + throw new InvalidOperationException(SRResources.RequestMustHaveModel); } + + model.SetEdmxVersion(_defaultEdmxVersion); + return model; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Controllers/ODataController.cs b/src/Microsoft.AspNetCore.OData/Routing/Controllers/ODataController.cs index ab100bc65..5b341b30b 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Controllers/ODataController.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Controllers/ODataController.cs @@ -10,169 +10,168 @@ using Microsoft.AspNetCore.OData.Routing.Attributes; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Routing.Controllers +namespace Microsoft.AspNetCore.OData.Routing.Controllers; + +/// +/// The base controller class for OData. +/// +[ODataAttributeRouting] +public abstract class ODataController : ControllerBase { /// - /// The base controller class for OData. + /// Creates an action result with the specified values that is a response to a POST operation with an entity + /// to an entity set. /// - [ODataAttributeRouting] - public abstract class ODataController : ControllerBase + /// The created entity type. + /// The created entity. + /// A with the specified values. + /// This function uses types that are AspNetCore-specific. + protected virtual CreatedODataResult Created(TEntity entity) { - /// - /// Creates an action result with the specified values that is a response to a POST operation with an entity - /// to an entity set. - /// - /// The created entity type. - /// The created entity. - /// A with the specified values. - /// This function uses types that are AspNetCore-specific. - protected virtual CreatedODataResult Created(TEntity entity) + if (entity == null) { - if (entity == null) - { - throw Error.ArgumentNull(nameof(entity)); - } - - return new CreatedODataResult(entity); + throw Error.ArgumentNull(nameof(entity)); } - /// - /// Creates an action result with the specified values that is a response to a PUT, PATCH, or a MERGE operation - /// on an OData entity. - /// - /// The updated entity type. - /// The updated entity. - /// An with the specified values. - /// This function uses types that are AspNetCore-specific. - protected virtual UpdatedODataResult Updated(TEntity entity) - { - if (entity == null) - { - throw Error.ArgumentNull(nameof(entity)); - } - - return new UpdatedODataResult(entity); - } + return new CreatedODataResult(entity); + } - /// - /// Creates a that when executed will produce a Bad Request (400) response. - /// - /// Error message. - /// A with the specified values. - protected virtual BadRequestODataResult BadRequest(string message) + /// + /// Creates an action result with the specified values that is a response to a PUT, PATCH, or a MERGE operation + /// on an OData entity. + /// + /// The updated entity type. + /// The updated entity. + /// An with the specified values. + /// This function uses types that are AspNetCore-specific. + protected virtual UpdatedODataResult Updated(TEntity entity) + { + if (entity == null) { - return new BadRequestODataResult(message); + throw Error.ArgumentNull(nameof(entity)); } - /// - /// Creates a that when executed will produce a Bad Request (400) response. - /// - /// An object. - /// A with the specified values. - protected virtual BadRequestODataResult BadRequest(ODataError odataError) - { - return new BadRequestODataResult(odataError); - } + return new UpdatedODataResult(entity); + } - /// - /// Creates a that when executed will produce a Not Found (404) response. - /// - /// Error message. - /// A with the specified values. - protected virtual NotFoundODataResult NotFound(string message) - { - return new NotFoundODataResult(message); - } + /// + /// Creates a that when executed will produce a Bad Request (400) response. + /// + /// Error message. + /// A with the specified values. + protected virtual BadRequestODataResult BadRequest(string message) + { + return new BadRequestODataResult(message); + } - /// - /// Creates a that when executed will produce a Not Found (404) response. - /// - /// An object. - /// A with the specified values. - protected virtual NotFoundODataResult NotFound(ODataError odataError) - { - return new NotFoundODataResult(odataError); - } + /// + /// Creates a that when executed will produce a Bad Request (400) response. + /// + /// An object. + /// A with the specified values. + protected virtual BadRequestODataResult BadRequest(ODataError odataError) + { + return new BadRequestODataResult(odataError); + } - /// - /// Creates a that when executed will produce a Unauthorized (401) response. - /// - /// Error message. - /// An with the specified values. - protected virtual UnauthorizedODataResult Unauthorized(string message) - { - return new UnauthorizedODataResult(message); - } + /// + /// Creates a that when executed will produce a Not Found (404) response. + /// + /// Error message. + /// A with the specified values. + protected virtual NotFoundODataResult NotFound(string message) + { + return new NotFoundODataResult(message); + } - /// - /// Creates a that when executed will produce a Unauthorized (401) response. - /// - /// An object. - /// An with the specified values. - protected virtual UnauthorizedODataResult Unauthorized(ODataError odataError) - { - return new UnauthorizedODataResult(odataError); - } + /// + /// Creates a that when executed will produce a Not Found (404) response. + /// + /// An object. + /// A with the specified values. + protected virtual NotFoundODataResult NotFound(ODataError odataError) + { + return new NotFoundODataResult(odataError); + } - /// - /// Creates a that when executed will produce a Conflict (409) response. - /// - /// Error message. - /// A with the specified values. - protected virtual ConflictODataResult Conflict(string message) - { - return new ConflictODataResult(message); - } + /// + /// Creates a that when executed will produce a Unauthorized (401) response. + /// + /// Error message. + /// An with the specified values. + protected virtual UnauthorizedODataResult Unauthorized(string message) + { + return new UnauthorizedODataResult(message); + } - /// - /// Creates a that when executed will produce a Conflict (409) response. - /// - /// An object. - /// A with the specified values. - protected virtual ConflictODataResult Conflict(ODataError odataError) - { - return new ConflictODataResult(odataError); - } + /// + /// Creates a that when executed will produce a Unauthorized (401) response. + /// + /// An object. + /// An with the specified values. + protected virtual UnauthorizedODataResult Unauthorized(ODataError odataError) + { + return new UnauthorizedODataResult(odataError); + } - /// - /// Creates a that when executed will produce an UnprocessableEntity (422) response. - /// - /// Error message. - /// An with the specified values. - protected virtual UnprocessableEntityODataResult UnprocessableEntity(string message) - { - return new UnprocessableEntityODataResult(message); - } + /// + /// Creates a that when executed will produce a Conflict (409) response. + /// + /// Error message. + /// A with the specified values. + protected virtual ConflictODataResult Conflict(string message) + { + return new ConflictODataResult(message); + } - /// - /// Creates a that when executed will produce an UnprocessableEntity (422) response. - /// - /// An object. - /// An with the specified values. - protected virtual UnprocessableEntityODataResult UnprocessableEntity(ODataError odataError) - { - return new UnprocessableEntityODataResult(odataError); - } + /// + /// Creates a that when executed will produce a Conflict (409) response. + /// + /// An object. + /// A with the specified values. + protected virtual ConflictODataResult Conflict(ODataError odataError) + { + return new ConflictODataResult(odataError); + } - /// - /// Creates an that when executed will produce an response. - /// - /// Http error code. - /// Error message. - /// An with the specified values. - protected virtual ODataErrorResult ODataErrorResult(string errorCode, string message) - { - return new ODataErrorResult(errorCode, message); - } + /// + /// Creates a that when executed will produce an UnprocessableEntity (422) response. + /// + /// Error message. + /// An with the specified values. + protected virtual UnprocessableEntityODataResult UnprocessableEntity(string message) + { + return new UnprocessableEntityODataResult(message); + } - /// - /// Creates an that when executed will produce an response. - /// - /// An object. - /// An with the specified values. - protected virtual ODataErrorResult ODataErrorResult(ODataError odataError) - { - return new ODataErrorResult(odataError); - } + /// + /// Creates a that when executed will produce an UnprocessableEntity (422) response. + /// + /// An object. + /// An with the specified values. + protected virtual UnprocessableEntityODataResult UnprocessableEntity(ODataError odataError) + { + return new UnprocessableEntityODataResult(odataError); + } + + /// + /// Creates an that when executed will produce an response. + /// + /// Http error code. + /// Error message. + /// An with the specified values. + protected virtual ODataErrorResult ODataErrorResult(string errorCode, string message) + { + return new ODataErrorResult(errorCode, message); + } + + /// + /// Creates an that when executed will produce an response. + /// + /// An object. + /// An with the specified values. + protected virtual ODataErrorResult ODataErrorResult(ODataError odataError) + { + return new ODataErrorResult(odataError); } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/ActionRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/ActionRoutingConvention.cs index 74541f397..8b2d83c7e 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/ActionRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/ActionRoutingConvention.cs @@ -12,66 +12,65 @@ using Microsoft.AspNetCore.OData.Formatter; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// The convention for . +/// Post ~/entityset|singleton/action, ~/entityset|singleton/cast/action +/// Post ~/entityset/key/action, ~/entityset/key/cast/action +/// +public class ActionRoutingConvention : OperationRoutingConvention { - /// - /// The convention for . - /// Post ~/entityset|singleton/action, ~/entityset|singleton/cast/action - /// Post ~/entityset/key/action, ~/entityset/key/cast/action - /// - public class ActionRoutingConvention : OperationRoutingConvention + /// + public override int Order => 700; + + /// + public override bool AppliesToAction(ODataControllerActionContext context) { - /// - public override int Order => 700; + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + IEdmNavigationSource navigationSource = context.NavigationSource; + IEdmEntityType entityType = navigationSource.EntityType; - /// - public override bool AppliesToAction(ODataControllerActionContext context) + // action should have the [HttpPost] + if (!context.Action.Attributes.Any(a => a is HttpPostAttribute)) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + return false; + } - IEdmNavigationSource navigationSource = context.NavigationSource; - IEdmEntityType entityType = navigationSource.EntityType; + // action overload on binding type, only one action overload on the same binding type. + // however, it supports the bound action on derived type. + ProcessOperations(context, entityType, navigationSource); - // action should have the [HttpPost] - if (!context.Action.Attributes.Any(a => a is HttpPostAttribute)) - { - return false; - } + // in OData operationImport routing convention, all action are processed by default + // even it's not a really edm operation import call. + return false; + } - // action overload on binding type, only one action overload on the same binding type. - // however, it supports the bound action on derived type. - ProcessOperations(context, entityType, navigationSource); + /// + protected override bool IsOperationParameterMatched(IEdmOperation operation, ActionModel action) + { + Contract.Assert(operation != null); + Contract.Assert(action != null); - // in OData operationImport routing convention, all action are processed by default - // even it's not a really edm operation import call. + if (!operation.IsAction()) + { return false; } - /// - protected override bool IsOperationParameterMatched(IEdmOperation operation, ActionModel action) + // So far, we use the "ODataActionParameters" and "ODataUntypedActionParameters" to hold the action parameter values. + // TODO: consider using [FromODataBody] to separate the parameters to each corresponding + if (operation.Parameters.Count() > 1) { - Contract.Assert(operation != null); - Contract.Assert(action != null); - - if (!operation.IsAction()) + if (!action.Parameters.Any(p => p.ParameterType == typeof(ODataActionParameters) || p.ParameterType == typeof(ODataUntypedActionParameters))) { return false; } - - // So far, we use the "ODataActionParameters" and "ODataUntypedActionParameters" to hold the action parameter values. - // TODO: consider using [FromODataBody] to separate the parameters to each corresponding - if (operation.Parameters.Count() > 1) - { - if (!action.Parameters.Any(p => p.ParameterType == typeof(ODataActionParameters) || p.ParameterType == typeof(ODataUntypedActionParameters))) - { - return false; - } - } - - return true; } + + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/AttributeRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/AttributeRoutingConvention.cs index 180af351c..d2e95bec0 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/AttributeRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/AttributeRoutingConvention.cs @@ -19,279 +19,278 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// The convention for an OData template string. +/// It looks for the on controller +/// and or other Http Verb attribute, for example on action. +/// +public class AttributeRoutingConvention : IODataControllerActionConvention { + private readonly ILogger _logger; + private IODataPathTemplateParser _templateParser; + /// - /// The convention for an OData template string. - /// It looks for the on controller - /// and or other Http Verb attribute, for example on action. + /// Initializes a new instance of the class. /// - public class AttributeRoutingConvention : IODataControllerActionConvention + /// The registered logger. + /// The registered parser. + public AttributeRoutingConvention(ILogger logger, + IODataPathTemplateParser parser) { - private readonly ILogger _logger; - private IODataPathTemplateParser _templateParser; - - /// - /// Initializes a new instance of the class. - /// - /// The registered logger. - /// The registered parser. - public AttributeRoutingConvention(ILogger logger, - IODataPathTemplateParser parser) - { - _logger = logger; - _templateParser = parser; - } + _logger = logger; + _templateParser = parser; + } - /// - public virtual int Order => -100; + /// + public virtual int Order => -100; - /// - public virtual bool AppliesToController(ODataControllerActionContext context) + /// + public virtual bool AppliesToController(ODataControllerActionContext context) + { + return true; + } + + /// + public virtual bool AppliesToAction(ODataControllerActionContext context) + { + if (context == null) { - return true; + throw Error.ArgumentNull(nameof(context)); } - /// - public virtual bool AppliesToAction(ODataControllerActionContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + // Be noted, the validation checks (non OData controller, non OData action) are done before calling this method. + ControllerModel controllerModel = context.Controller; + ActionModel actionModel = context.Action; - // Be noted, the validation checks (non OData controller, non OData action) are done before calling this method. - ControllerModel controllerModel = context.Controller; - ActionModel actionModel = context.Action; + bool isODataController = controllerModel.Attributes.Any(a => a is ODataAttributeRoutingAttribute); + bool isODataAction = actionModel.Attributes.Any(a => a is ODataAttributeRoutingAttribute); - bool isODataController = controllerModel.Attributes.Any(a => a is ODataAttributeRoutingAttribute); - bool isODataAction = actionModel.Attributes.Any(a => a is ODataAttributeRoutingAttribute); + // At least one of controller or action has "ODataRoutingAttribute" + // The best way is to derive your controller from ODataController. + if (!isODataController && !isODataAction) + { + return false; + } - // At least one of controller or action has "ODataRoutingAttribute" - // The best way is to derive your controller from ODataController. - if (!isODataController && !isODataAction) - { - return false; - } + // TODO: Which one is better? input from context or inject from constructor? + IEnumerable prefixes = context.Options.RouteComponents.Keys; - // TODO: Which one is better? input from context or inject from constructor? - IEnumerable prefixes = context.Options.RouteComponents.Keys; + // Loop through all attribute routes defined on the controller. + var controllerSelectors = controllerModel.Selectors.Where(sm => sm.AttributeRouteModel != null).ToList(); + if (controllerSelectors.Count == 0) + { + // If no controller route template, we still need to go through action to process the action route template. + controllerSelectors.Add(null); + } - // Loop through all attribute routes defined on the controller. - var controllerSelectors = controllerModel.Selectors.Where(sm => sm.AttributeRouteModel != null).ToList(); - if (controllerSelectors.Count == 0) + // In order to avoiding polluting the action selectors, we use a Dictionary to save the intermediate results. + IDictionary> updatedSelectors = new Dictionary>(); + foreach (var actionSelector in actionModel.Selectors) + { + if (actionSelector.AttributeRouteModel != null && actionSelector.AttributeRouteModel.IsAbsoluteTemplate) { - // If no controller route template, we still need to go through action to process the action route template. - controllerSelectors.Add(null); + ProcessAttributeModel(actionSelector.AttributeRouteModel, prefixes, context, actionSelector, actionModel, controllerModel, updatedSelectors); } - - // In order to avoiding polluting the action selectors, we use a Dictionary to save the intermediate results. - IDictionary> updatedSelectors = new Dictionary>(); - foreach (var actionSelector in actionModel.Selectors) + else { - if (actionSelector.AttributeRouteModel != null && actionSelector.AttributeRouteModel.IsAbsoluteTemplate) - { - ProcessAttributeModel(actionSelector.AttributeRouteModel, prefixes, context, actionSelector, actionModel, controllerModel, updatedSelectors); - } - else + foreach (var controllerSelector in controllerSelectors) { - foreach (var controllerSelector in controllerSelectors) - { - var combinedRouteModel = AttributeRouteModel.CombineAttributeRouteModel(controllerSelector?.AttributeRouteModel, actionSelector.AttributeRouteModel); - ProcessAttributeModel(combinedRouteModel, prefixes, context, actionSelector, actionModel, controllerModel, updatedSelectors); - } + var combinedRouteModel = AttributeRouteModel.CombineAttributeRouteModel(controllerSelector?.AttributeRouteModel, actionSelector.AttributeRouteModel); + ProcessAttributeModel(combinedRouteModel, prefixes, context, actionSelector, actionModel, controllerModel, updatedSelectors); } } + } - // remove the old one. - foreach (var selector in updatedSelectors) - { - actionModel.Selectors.Remove(selector.Key); - } + // remove the old one. + foreach (var selector in updatedSelectors) + { + actionModel.Selectors.Remove(selector.Key); + } - // add new one. - foreach (var selector in updatedSelectors) + // add new one. + foreach (var selector in updatedSelectors) + { + foreach (var newSelector in selector.Value) { - foreach (var newSelector in selector.Value) - { - actionModel.Selectors.Add(newSelector); - } + actionModel.Selectors.Add(newSelector); } + } - // let's just return false to let this action go to other conventions. - return false; + // let's just return false to let this action go to other conventions. + return false; + } + + private void ProcessAttributeModel(AttributeRouteModel attributeRouteModel, IEnumerable prefixes, + ODataControllerActionContext context, SelectorModel actionSelector, ActionModel actionModel, ControllerModel controllerModel, + IDictionary> updatedSelectors) + { + if (attributeRouteModel == null) + { + // not an attribute routing, skip it. + return; } - private void ProcessAttributeModel(AttributeRouteModel attributeRouteModel, IEnumerable prefixes, - ODataControllerActionContext context, SelectorModel actionSelector, ActionModel actionModel, ControllerModel controllerModel, - IDictionary> updatedSelectors) + string prefix = FindRelatedODataPrefix(attributeRouteModel.Template, prefixes, out string newRouteTemplate); + if (prefix == null) { - if (attributeRouteModel == null) - { - // not an attribute routing, skip it. - return; - } + return; + } - string prefix = FindRelatedODataPrefix(attributeRouteModel.Template, prefixes, out string newRouteTemplate); - if (prefix == null) + IEdmModel model = context.Options.RouteComponents[prefix].EdmModel; + IServiceProvider sp = context.Options.RouteComponents[prefix].ServiceProvider; + + SelectorModel newSelectorModel = CreateActionSelectorModel(prefix, model, sp, newRouteTemplate, actionSelector, + attributeRouteModel.Template, actionModel.ActionName, controllerModel.ControllerName, attributeRouteModel.Order); + if (newSelectorModel != null) + { + IList selectors; + if (!updatedSelectors.TryGetValue(actionSelector, out selectors)) { - return; + selectors = new List(); + updatedSelectors[actionSelector] = selectors; } - IEdmModel model = context.Options.RouteComponents[prefix].EdmModel; - IServiceProvider sp = context.Options.RouteComponents[prefix].ServiceProvider; + selectors.Add(newSelectorModel); + } + } - SelectorModel newSelectorModel = CreateActionSelectorModel(prefix, model, sp, newRouteTemplate, actionSelector, - attributeRouteModel.Template, actionModel.ActionName, controllerModel.ControllerName, attributeRouteModel.Order); - if (newSelectorModel != null) + private SelectorModel CreateActionSelectorModel(string prefix, IEdmModel model, IServiceProvider sp, + string routeTemplate, SelectorModel actionSelectorModel, + string originalTemplate, string actionName, string controllerName, int? order) + { + try + { + // Due the uri parser, it will throw exception if the route template is not a OData path. + ODataPathTemplate pathTemplate = _templateParser.Parse(model, routeTemplate, sp); + if (pathTemplate != null) { - IList selectors; - if (!updatedSelectors.TryGetValue(actionSelector, out selectors)) + // Create a new selector model? + SelectorModel newSelectorModel = new SelectorModel(actionSelectorModel); + // Shall we remove any certain attributes/metadata? + ClearMetadata(newSelectorModel); + + // Add OData routing metadata + ODataRoutingMetadata odataMetadata = new ODataRoutingMetadata(prefix, model, pathTemplate) { - selectors = new List(); - updatedSelectors[actionSelector] = selectors; - } + IsConventional = false + }; - selectors.Add(newSelectorModel); - } - } + newSelectorModel.EndpointMetadata.Add(odataMetadata); - private SelectorModel CreateActionSelectorModel(string prefix, IEdmModel model, IServiceProvider sp, - string routeTemplate, SelectorModel actionSelectorModel, - string originalTemplate, string actionName, string controllerName, int? order) - { - try - { - // Due the uri parser, it will throw exception if the route template is not a OData path. - ODataPathTemplate pathTemplate = _templateParser.Parse(model, routeTemplate, sp); - if (pathTemplate != null) + // replace the attribute routing template using absolute routing template to avoid appending any controller route template + newSelectorModel.AttributeRouteModel = new AttributeRouteModel() { - // Create a new selector model? - SelectorModel newSelectorModel = new SelectorModel(actionSelectorModel); - // Shall we remove any certain attributes/metadata? - ClearMetadata(newSelectorModel); - - // Add OData routing metadata - ODataRoutingMetadata odataMetadata = new ODataRoutingMetadata(prefix, model, pathTemplate) - { - IsConventional = false - }; - - newSelectorModel.EndpointMetadata.Add(odataMetadata); - - // replace the attribute routing template using absolute routing template to avoid appending any controller route template - newSelectorModel.AttributeRouteModel = new AttributeRouteModel() - { - Template = $"/{originalTemplate}", // add a "/" to make sure it's absolute template, don't combine with controller - Order = order - }; - - return newSelectorModel; - } + Template = $"/{originalTemplate}", // add a "/" to make sure it's absolute template, don't combine with controller + Order = order + }; - return null; - } - catch (ODataException ex) - { - // use the logger to log the wrong odata attribute template. Shall we log the others? - string warning = string.Format(CultureInfo.CurrentCulture, SRResources.InvalidODataRouteOnAction, - originalTemplate, actionName, controllerName, ex.Message); - - // Whether we throw exception or mark it as warning is a design pattern. - // throw new ODataException(warning); - _logger.LogWarning(warning); - return null; + return newSelectorModel; } + + return null; + } + catch (ODataException ex) + { + // use the logger to log the wrong odata attribute template. Shall we log the others? + string warning = string.Format(CultureInfo.CurrentCulture, SRResources.InvalidODataRouteOnAction, + originalTemplate, actionName, controllerName, ex.Message); + + // Whether we throw exception or mark it as warning is a design pattern. + // throw new ODataException(warning); + _logger.LogWarning(warning); + return null; } + } - private static void ClearMetadata(SelectorModel selectorModel) + private static void ClearMetadata(SelectorModel selectorModel) + { + for (var i = selectorModel.ActionConstraints.Count - 1; i >= 0; i--) { - for (var i = selectorModel.ActionConstraints.Count - 1; i >= 0; i--) + if (selectorModel.ActionConstraints[i] is IRouteTemplateProvider) { - if (selectorModel.ActionConstraints[i] is IRouteTemplateProvider) - { - selectorModel.ActionConstraints.RemoveAt(i); - } + selectorModel.ActionConstraints.RemoveAt(i); } + } - //for (var i = selectorModel.ActionConstraints.Count - 1; i >= 0; i--) - //{ - // if (selectorModel.ActionConstraints[i] is HttpMethodActionConstraint) - // { - // selectorModel.ActionConstraints.RemoveAt(i); - // } - //} + //for (var i = selectorModel.ActionConstraints.Count - 1; i >= 0; i--) + //{ + // if (selectorModel.ActionConstraints[i] is HttpMethodActionConstraint) + // { + // selectorModel.ActionConstraints.RemoveAt(i); + // } + //} - // remove the unused metadata - for (var i = selectorModel.EndpointMetadata.Count - 1; i >= 0; i--) + // remove the unused metadata + for (var i = selectorModel.EndpointMetadata.Count - 1; i >= 0; i--) + { + if (selectorModel.EndpointMetadata[i] is IRouteTemplateProvider) { - if (selectorModel.EndpointMetadata[i] is IRouteTemplateProvider) - { - selectorModel.EndpointMetadata.RemoveAt(i); - } + selectorModel.EndpointMetadata.RemoveAt(i); } + } - //for (var i = selectorModel.EndpointMetadata.Count - 1; i >= 0; i--) - //{ - // if (selectorModel.EndpointMetadata[i] is IHttpMethodMetadata) - // { - // selectorModel.EndpointMetadata.RemoveAt(i); - // } - //} + //for (var i = selectorModel.EndpointMetadata.Count - 1; i >= 0; i--) + //{ + // if (selectorModel.EndpointMetadata[i] is IHttpMethodMetadata) + // { + // selectorModel.EndpointMetadata.RemoveAt(i); + // } + //} + } + + private static string FindRelatedODataPrefix(string routeTemplate, IEnumerable prefixes, out string newRouteTemplate) + { + if (routeTemplate.StartsWith('/')) + { + routeTemplate = routeTemplate.Substring(1); + } + else if (routeTemplate.StartsWith("~/", StringComparison.Ordinal)) + { + routeTemplate = routeTemplate.Substring(2); } - private static string FindRelatedODataPrefix(string routeTemplate, IEnumerable prefixes, out string newRouteTemplate) + // the input route template could be: + // #1) odata/Customers/{key} + // #2) orders({key}) + // So, #1 matches the "odata" prefix route + // #2 matches the non-odata prefix route + // Since #1 and #2 can be considered starting with "", + // In order to avoiding ambiguous, let's compare non-empty route prefix first, + // If no match, then compare empty route prefix. + string emptyPrefix = null; + foreach (var prefix in prefixes) { - if (routeTemplate.StartsWith('/')) + if (string.IsNullOrEmpty(prefix)) { - routeTemplate = routeTemplate.Substring(1); + emptyPrefix = prefix; + continue; } - else if (routeTemplate.StartsWith("~/", StringComparison.Ordinal)) + else if (routeTemplate.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { - routeTemplate = routeTemplate.Substring(2); - } + // we hit: "odata/Customers/{key}" scenario, let's remove the "odata" route prefix + newRouteTemplate = routeTemplate.Substring(prefix.Length); - // the input route template could be: - // #1) odata/Customers/{key} - // #2) orders({key}) - // So, #1 matches the "odata" prefix route - // #2 matches the non-odata prefix route - // Since #1 and #2 can be considered starting with "", - // In order to avoiding ambiguous, let's compare non-empty route prefix first, - // If no match, then compare empty route prefix. - string emptyPrefix = null; - foreach (var prefix in prefixes) - { - if (string.IsNullOrEmpty(prefix)) + // why do like this: because the input route template could be "odata", after remove the prefix, it's empty string. + if (newRouteTemplate.StartsWith("/", StringComparison.Ordinal)) { - emptyPrefix = prefix; - continue; + newRouteTemplate = newRouteTemplate.Substring(1); } - else if (routeTemplate.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - // we hit: "odata/Customers/{key}" scenario, let's remove the "odata" route prefix - newRouteTemplate = routeTemplate.Substring(prefix.Length); - - // why do like this: because the input route template could be "odata", after remove the prefix, it's empty string. - if (newRouteTemplate.StartsWith("/", StringComparison.Ordinal)) - { - newRouteTemplate = newRouteTemplate.Substring(1); - } - - return prefix; - } - } - // we are here because no non-empty prefix matches. - if (emptyPrefix != null) - { - // So, if we have empty prefix route, it could match all OData route template. - newRouteTemplate = routeTemplate; - return emptyPrefix; + return prefix; } + } - newRouteTemplate = null; - return null; + // we are here because no non-empty prefix matches. + if (emptyPrefix != null) + { + // So, if we have empty prefix route, it could match all OData route template. + newRouteTemplate = routeTemplate; + return emptyPrefix; } + + newRouteTemplate = null; + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntityRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntityRoutingConvention.cs index 6dc9bb7b5..056283ea7 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntityRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntityRoutingConvention.cs @@ -13,152 +13,151 @@ using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// The convention for with key. +/// It supports key in parenthesis and key as segment if it's a single key. +/// Conventions: +/// GET ~/entityset/key +/// GET ~/entityset/key/cast +/// PUT ~/entityset/key +/// PUT ~/entityset/key/cast +/// PATCH ~/entityset/key +/// PATCH ~/entityset/key/cast +/// DELETE ~/entityset/key +/// DELETE ~/entityset/key/cast +/// +public class EntityRoutingConvention : IODataControllerActionConvention { - /// - /// The convention for with key. - /// It supports key in parenthesis and key as segment if it's a single key. - /// Conventions: - /// GET ~/entityset/key - /// GET ~/entityset/key/cast - /// PUT ~/entityset/key - /// PUT ~/entityset/key/cast - /// PATCH ~/entityset/key - /// PATCH ~/entityset/key/cast - /// DELETE ~/entityset/key - /// DELETE ~/entityset/key/cast - /// - public class EntityRoutingConvention : IODataControllerActionConvention - { - /// - public virtual int Order => 300; + /// + public virtual int Order => 300; - /// - public virtual bool AppliesToController(ODataControllerActionContext context) + /// + public virtual bool AppliesToController(ODataControllerActionContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - return context.EntitySet != null; + throw Error.ArgumentNull(nameof(context)); } - /// - public virtual bool AppliesToAction(ODataControllerActionContext context) + return context.EntitySet != null; + } + + /// + public virtual bool AppliesToAction(ODataControllerActionContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - ActionModel action = context.Action; - IEdmEntitySet entitySet = context.EntitySet; - IEdmEntityType entityType = entitySet.EntityType; + ActionModel action = context.Action; + IEdmEntitySet entitySet = context.EntitySet; + IEdmEntityType entityType = entitySet.EntityType; - // if the action has no key parameter, skip it. - if (!action.HasODataKeyParameter(entityType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false)) - { - return false; - } + // if the action has no key parameter, skip it. + if (!action.HasODataKeyParameter(entityType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false)) + { + return false; + } - string actionName = action.ActionName; + string actionName = action.ActionName; - // We care about the action in this pattern: {HttpMethod}{EntityTypeName} - (string httpMethod, string castTypeName) = Split(actionName); - if (httpMethod == null) + // We care about the action in this pattern: {HttpMethod}{EntityTypeName} + (string httpMethod, string castTypeName) = Split(actionName); + if (httpMethod == null) + { + return false; + } + + IEdmStructuredType castType = null; + if (castTypeName != null) + { + castType = entityType.FindTypeInInheritance(context.Model, castTypeName, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true); + if (castType == null) { return false; } + } - IEdmStructuredType castType = null; - if (castTypeName != null) - { - castType = entityType.FindTypeInInheritance(context.Model, castTypeName, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true); - if (castType == null) - { - return false; - } - } + AddSelector(entitySet, entityType, castType, context.Prefix, context.Model, action, httpMethod, context.Options?.RouteOptions); + return true; + } - AddSelector(entitySet, entityType, castType, context.Prefix, context.Model, action, httpMethod, context.Options?.RouteOptions); - return true; + // internal for unit test + internal static (string, string) Split(string actionName) + { + string typeName; + string methodName = null; + if (actionName.StartsWith("Get", StringComparison.Ordinal)) + { + methodName = "Get"; + } + else if (actionName.StartsWith("Put", StringComparison.Ordinal)) + { + methodName = "Put"; + } + else if (actionName.StartsWith("Patch", StringComparison.Ordinal)) + { + methodName = "Patch"; + } + else if (actionName.StartsWith("Delete", StringComparison.Ordinal)) + { + methodName = "Delete"; } - // internal for unit test - internal static (string, string) Split(string actionName) + if (methodName != null) { - string typeName; - string methodName = null; - if (actionName.StartsWith("Get", StringComparison.Ordinal)) - { - methodName = "Get"; - } - else if (actionName.StartsWith("Put", StringComparison.Ordinal)) - { - methodName = "Put"; - } - else if (actionName.StartsWith("Patch", StringComparison.Ordinal)) - { - methodName = "Patch"; - } - else if (actionName.StartsWith("Delete", StringComparison.Ordinal)) - { - methodName = "Delete"; - } + typeName = actionName.Substring(methodName.Length); + } + else + { + return (null, null); + } - if (methodName != null) - { - typeName = actionName.Substring(methodName.Length); - } - else - { - return (null, null); - } + if (string.IsNullOrEmpty(typeName)) + { + return (methodName, null); + } - if (string.IsNullOrEmpty(typeName)) - { - return (methodName, null); - } + return (methodName, typeName); + } - return (methodName, typeName); - } + private static void AddSelector(IEdmEntitySet entitySet, IEdmEntityType entityType, + IEdmStructuredType castType, string prefix, IEdmModel model, ActionModel action, string httpMethod, ODataRouteOptions options) + { + IList segments = new List + { + new EntitySetSegmentTemplate(entitySet), + KeySegmentTemplate.CreateKeySegment(entityType, entitySet) + }; - private static void AddSelector(IEdmEntitySet entitySet, IEdmEntityType entityType, - IEdmStructuredType castType, string prefix, IEdmModel model, ActionModel action, string httpMethod, ODataRouteOptions options) + // If we have the type cast + if (castType != null) { - IList segments = new List + if (castType == entityType) { - new EntitySetSegmentTemplate(entitySet), - KeySegmentTemplate.CreateKeySegment(entityType, entitySet) - }; + // If cast type is the entity type of the entity set. + // we support two templates + // ~/Customers({key}) + action.AddSelector(httpMethod, prefix, model, new ODataPathTemplate(segments), options); - // If we have the type cast - if (castType != null) - { - if (castType == entityType) - { - // If cast type is the entity type of the entity set. - // we support two templates - // ~/Customers({key}) - action.AddSelector(httpMethod, prefix, model, new ODataPathTemplate(segments), options); - - // ~/Customers({key})/Ns.Customer - segments.Add(new CastSegmentTemplate(castType, entityType, entitySet)); - action.AddSelector(httpMethod, prefix, model, new ODataPathTemplate(segments), options); - } - else - { - // ~/Customers({key})/Ns.VipCustomer - segments.Add(new CastSegmentTemplate(castType, entityType, entitySet)); - action.AddSelector(httpMethod, prefix, model, new ODataPathTemplate(segments), options); - } + // ~/Customers({key})/Ns.Customer + segments.Add(new CastSegmentTemplate(castType, entityType, entitySet)); + action.AddSelector(httpMethod, prefix, model, new ODataPathTemplate(segments), options); } else { - // ~/Customers({key}) + // ~/Customers({key})/Ns.VipCustomer + segments.Add(new CastSegmentTemplate(castType, entityType, entitySet)); action.AddSelector(httpMethod, prefix, model, new ODataPathTemplate(segments), options); } } + else + { + // ~/Customers({key}) + action.AddSelector(httpMethod, prefix, model, new ODataPathTemplate(segments), options); + } } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntitySetRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntitySetRoutingConvention.cs index 19078c9ad..d0262b8e9 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntitySetRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/EntitySetRoutingConvention.cs @@ -13,107 +13,123 @@ using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// The convention for . +/// Conventions: +/// GET ~/entityset +/// GET ~/entityset/$count +/// GET ~/entityset/cast +/// GET ~/entityset/cast/$count +/// POST ~/entityset +/// POST ~/entityset/cast +/// PATCH ~/entityset ==> Delta resource set patch +/// +public class EntitySetRoutingConvention : IODataControllerActionConvention { - /// - /// The convention for . - /// Conventions: - /// GET ~/entityset - /// GET ~/entityset/$count - /// GET ~/entityset/cast - /// GET ~/entityset/cast/$count - /// POST ~/entityset - /// POST ~/entityset/cast - /// PATCH ~/entityset ==> Delta resource set patch - /// - public class EntitySetRoutingConvention : IODataControllerActionConvention + /// + public virtual int Order => 100; + + /// + public virtual bool AppliesToController(ODataControllerActionContext context) { - /// - public virtual int Order => 100; + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - /// - public virtual bool AppliesToController(ODataControllerActionContext context) + return context.EntitySet != null; + } + + /// + public virtual bool AppliesToAction(ODataControllerActionContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - return context.EntitySet != null; + ActionModel action = context.Action; + IEdmEntitySet entitySet = context.EntitySet; + IEdmEntityType entityType = entitySet.EntityType; + + // if the action has key parameter, skip it. + if (action.HasODataKeyParameter(entityType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false)) + { + return false; } - /// - public virtual bool AppliesToAction(ODataControllerActionContext context) + string actionName = action.ActionName; + + // 1. Without type case + if (ProcessEntitySetAction(actionName, entitySet, null, context, action)) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + return true; + } - ActionModel action = context.Action; - IEdmEntitySet entitySet = context.EntitySet; - IEdmEntityType entityType = entitySet.EntityType; + // 2. process the derived type (cast) by searching all derived types + // GetFrom{EntityTypeName} or Get{EntitySet}From{EntityTypeName} + int index = actionName.IndexOf("From", StringComparison.Ordinal); + if (index == -1) + { + return false; + } - // if the action has key parameter, skip it. - if (action.HasODataKeyParameter(entityType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false)) - { - return false; - } + string castTypeName = actionName.Substring(index + 4); // + 4 means to skip the "From" - string actionName = action.ActionName; + if (castTypeName.Length == 0) + { + // Early return for the following cases: + // - Get|Post|PatchFrom + // - Get|Patch{EntitySet}From + // - Post{EntityType}From + return false; + } - // 1. Without type case - if (ProcessEntitySetAction(actionName, entitySet, null, context, action)) - { - return true; - } + IEdmStructuredType castType = entityType.FindTypeInInheritance(context.Model, castTypeName, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true); + if (castType == null) + { + return false; + } - // 2. process the derived type (cast) by searching all derived types - // GetFrom{EntityTypeName} or Get{EntitySet}From{EntityTypeName} - int index = actionName.IndexOf("From", StringComparison.Ordinal); - if (index == -1) + string actionPrefix = actionName.Substring(0, index); + return ProcessEntitySetAction(actionPrefix, entitySet, castType, context, action); + } + + private bool ProcessEntitySetAction(string actionName, IEdmEntitySet entitySet, IEdmStructuredType castType, + ODataControllerActionContext context, ActionModel action) + { + StringComparison actionNameComparison = context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + + if (actionName.Equals("Get", actionNameComparison) || actionName.Equals($"Get{entitySet.Name}", actionNameComparison)) + { + IEdmCollectionType castCollectionType = null; + if (castType != null) { - return false; + castCollectionType = castType.ToCollection(true); } - string castTypeName = actionName.Substring(index + 4); // + 4 means to skip the "From" + IEdmCollectionType entityCollectionType = entitySet.EntityType.ToCollection(true); - if (castTypeName.Length == 0) + // GET ~/Customers or GET ~/Customers/Ns.VipCustomer + IList segments = new List { - // Early return for the following cases: - // - Get|Post|PatchFrom - // - Get|Patch{EntitySet}From - // - Post{EntityType}From - return false; - } + new EntitySetSegmentTemplate(entitySet) + }; - IEdmStructuredType castType = entityType.FindTypeInInheritance(context.Model, castTypeName, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true); - if (castType == null) + if (castType != null) { - return false; + segments.Add(new CastSegmentTemplate(castCollectionType, entityCollectionType, entitySet)); } - string actionPrefix = actionName.Substring(0, index); - return ProcessEntitySetAction(actionPrefix, entitySet, castType, context, action); - } + ODataPathTemplate template = new ODataPathTemplate(segments); + action.AddSelector("Get", context.Prefix, context.Model, template, context.Options?.RouteOptions); - private bool ProcessEntitySetAction(string actionName, IEdmEntitySet entitySet, IEdmStructuredType castType, - ODataControllerActionContext context, ActionModel action) - { - StringComparison actionNameComparison = context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - - if (actionName.Equals("Get", actionNameComparison) || actionName.Equals($"Get{entitySet.Name}", actionNameComparison)) + if (CanApplyDollarCount(entitySet, context.Options?.RouteOptions)) { - IEdmCollectionType castCollectionType = null; - if (castType != null) - { - castCollectionType = castType.ToCollection(true); - } - - IEdmCollectionType entityCollectionType = entitySet.EntityType.ToCollection(true); - - // GET ~/Customers or GET ~/Customers/Ns.VipCustomer - IList segments = new List + // GET ~/Customers/$count or GET ~/Customers/Ns.VipCustomer/$count + segments = new List { new EntitySetSegmentTemplate(entitySet) }; @@ -123,78 +139,61 @@ private bool ProcessEntitySetAction(string actionName, IEdmEntitySet entitySet, segments.Add(new CastSegmentTemplate(castCollectionType, entityCollectionType, entitySet)); } - ODataPathTemplate template = new ODataPathTemplate(segments); - action.AddSelector("Get", context.Prefix, context.Model, template, context.Options?.RouteOptions); + segments.Add(CountSegmentTemplate.Instance); - if (CanApplyDollarCount(entitySet, context.Options?.RouteOptions)) - { - // GET ~/Customers/$count or GET ~/Customers/Ns.VipCustomer/$count - segments = new List - { - new EntitySetSegmentTemplate(entitySet) - }; - - if (castType != null) - { - segments.Add(new CastSegmentTemplate(castCollectionType, entityCollectionType, entitySet)); - } - - segments.Add(CountSegmentTemplate.Instance); - - template = new ODataPathTemplate(segments); - action.AddSelector("Get", context.Prefix, context.Model, template, context.Options?.RouteOptions); - } - - return true; + template = new ODataPathTemplate(segments); + action.AddSelector("Get", context.Prefix, context.Model, template, context.Options?.RouteOptions); } - else if (actionName.Equals("Post", actionNameComparison) || actionName.Equals($"Post{entitySet.EntityType.Name}", actionNameComparison)) + + return true; + } + else if (actionName.Equals("Post", actionNameComparison) || actionName.Equals($"Post{entitySet.EntityType.Name}", actionNameComparison)) + { + // POST ~/Customers + IList segments = new List { - // POST ~/Customers - IList segments = new List - { - new EntitySetSegmentTemplate(entitySet) - }; + new EntitySetSegmentTemplate(entitySet) + }; - if (castType != null) - { - IEdmCollectionType castCollectionType = castType.ToCollection(true); - IEdmCollectionType entityCollectionType = entitySet.EntityType.ToCollection(true); - segments.Add(new CastSegmentTemplate(castCollectionType, entityCollectionType, entitySet)); - } - ODataPathTemplate template = new ODataPathTemplate(segments); - action.AddSelector("Post", context.Prefix, context.Model, template, context.Options?.RouteOptions); - return true; + if (castType != null) + { + IEdmCollectionType castCollectionType = castType.ToCollection(true); + IEdmCollectionType entityCollectionType = entitySet.EntityType.ToCollection(true); + segments.Add(new CastSegmentTemplate(castCollectionType, entityCollectionType, entitySet)); } - else if (actionName.Equals("Patch", actionNameComparison) || actionName.Equals($"Patch{entitySet.Name}", actionNameComparison)) + ODataPathTemplate template = new ODataPathTemplate(segments); + action.AddSelector("Post", context.Prefix, context.Model, template, context.Options?.RouteOptions); + return true; + } + else if (actionName.Equals("Patch", actionNameComparison) || actionName.Equals($"Patch{entitySet.Name}", actionNameComparison)) + { + // PATCH ~/Patch , ~/PatchCustomers + IList segments = new List { - // PATCH ~/Patch , ~/PatchCustomers - IList segments = new List - { - new EntitySetSegmentTemplate(entitySet) - }; - - if (castType != null) - { - IEdmCollectionType castCollectionType = castType.ToCollection(true); - IEdmCollectionType entityCollectionType = entitySet.EntityType.ToCollection(true); - segments.Add(new CastSegmentTemplate(castCollectionType, entityCollectionType, entitySet)); - } + new EntitySetSegmentTemplate(entitySet) + }; - ODataPathTemplate template = new ODataPathTemplate(segments); - action.AddSelector("Patch", context.Prefix, context.Model, template, context.Options?.RouteOptions); - return true; + if (castType != null) + { + IEdmCollectionType castCollectionType = castType.ToCollection(true); + IEdmCollectionType entityCollectionType = entitySet.EntityType.ToCollection(true); + segments.Add(new CastSegmentTemplate(castCollectionType, entityCollectionType, entitySet)); } - return false; + ODataPathTemplate template = new ODataPathTemplate(segments); + action.AddSelector("Patch", context.Prefix, context.Model, template, context.Options?.RouteOptions); + return true; } - /// - /// Tests whether to apply $count on the . - /// - /// The entity set to test. - /// The route options. - /// True/false to identify whether to apply $count. - protected virtual bool CanApplyDollarCount(IEdmEntitySet entitySet, ODataRouteOptions routeOptions) - => routeOptions != null ? routeOptions.EnableDollarCountRouting : false; + return false; } + + /// + /// Tests whether to apply $count on the . + /// + /// The entity set to test. + /// The route options. + /// True/false to identify whether to apply $count. + protected virtual bool CanApplyDollarCount(IEdmEntitySet entitySet, ODataRouteOptions routeOptions) + => routeOptions != null ? routeOptions.EnableDollarCountRouting : false; } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/FunctionRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/FunctionRoutingConvention.cs index 68b317a7b..f47d5e2c3 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/FunctionRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/FunctionRoutingConvention.cs @@ -12,69 +12,68 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// The convention for . +/// Get ~/entityset|singleton/function, ~/entityset|singleton/cast/function +/// Get ~/entityset/key/function, ~/entityset/key/cast/function +/// +public class FunctionRoutingConvention : OperationRoutingConvention { - /// - /// The convention for . - /// Get ~/entityset|singleton/function, ~/entityset|singleton/cast/function - /// Get ~/entityset/key/function, ~/entityset/key/cast/function - /// - public class FunctionRoutingConvention : OperationRoutingConvention + /// + public override int Order => 600; + + /// + public override bool AppliesToAction(ODataControllerActionContext context) { - /// - public override int Order => 600; + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + IEdmNavigationSource navigationSource = context.NavigationSource; + IEdmEntityType entityType = navigationSource.EntityType; - /// - public override bool AppliesToAction(ODataControllerActionContext context) + // function should have the [HttpGet] + if (!context.Action.Attributes.Any(a => a is HttpGetAttribute)) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + return false; + } - IEdmNavigationSource navigationSource = context.NavigationSource; - IEdmEntityType entityType = navigationSource.EntityType; + ProcessOperations(context, entityType, navigationSource); + return false; + } - // function should have the [HttpGet] - if (!context.Action.Attributes.Any(a => a is HttpGetAttribute)) - { - return false; - } + /// + protected override bool IsOperationParameterMatched(IEdmOperation operation, ActionModel action) + { + Contract.Assert(operation != null); + Contract.Assert(action != null); - ProcessOperations(context, entityType, navigationSource); + if (!operation.IsFunction()) + { return false; } - /// - protected override bool IsOperationParameterMatched(IEdmOperation operation, ActionModel action) + // we can allow the action has other parameters except the function parameters. + foreach (var parameter in operation.Parameters.Skip(1)) { - Contract.Assert(operation != null); - Contract.Assert(action != null); - - if (!operation.IsFunction()) + // It seems we don't need to distinguish the optional parameter here + // It means whether it's optional parameter or not, the action descriptor should have such parameter defined. + // Meanwhile, the send request may or may not have such parameter value. + IEdmOptionalParameter optionalParameter = parameter as IEdmOptionalParameter; + if (optionalParameter != null) { - return false; + continue; } - // we can allow the action has other parameters except the function parameters. - foreach (var parameter in operation.Parameters.Skip(1)) + if (!action.Parameters.Any(p => p.ParameterInfo.Name == parameter.Name)) { - // It seems we don't need to distinguish the optional parameter here - // It means whether it's optional parameter or not, the action descriptor should have such parameter defined. - // Meanwhile, the send request may or may not have such parameter value. - IEdmOptionalParameter optionalParameter = parameter as IEdmOptionalParameter; - if (optionalParameter != null) - { - continue; - } - - if (!action.Parameters.Any(p => p.ParameterInfo.Name == parameter.Name)) - { - return false; - } + return false; } - - return true; } + + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/IODataControllerActionConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/IODataControllerActionConvention.cs index a5c37283b..4306bc66f 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/IODataControllerActionConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/IODataControllerActionConvention.cs @@ -5,47 +5,46 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// The interface for all OData convention routing. +/// +public interface IODataControllerActionConvention { /// - /// The interface for all OData convention routing. + /// Gets the order value for determining the order of execution of conventions. + /// Conventions execute in ascending numeric value of the property. /// - public interface IODataControllerActionConvention - { - /// - /// Gets the order value for determining the order of execution of conventions. - /// Conventions execute in ascending numeric value of the property. - /// - /// - /// If two conventions have the same numeric value of , then their relative execution order - /// is undefined. - /// - int Order { get; } + /// + /// If two conventions have the same numeric value of , then their relative execution order + /// is undefined. + /// + int Order { get; } - /// - /// Applies the convention on controller - /// - /// The controller action context. - /// - /// True: applies the convention on the actions of this controller. - /// False: no, please skip this convention on the actions of this controller. - /// - bool AppliesToController(ODataControllerActionContext context); + /// + /// Applies the convention on controller + /// + /// The controller action context. + /// + /// True: applies the convention on the actions of this controller. + /// False: no, please skip this convention on the actions of this controller. + /// + bool AppliesToController(ODataControllerActionContext context); - /// - /// Applies the convention on action of this controller. - /// - /// The controller action context. - /// - /// True: yes, apply the convention, please stop executing the remaining conventions. - /// False: no, please continue to execute the remaining conventions. - /// - /// - /// The OData action convention should not put limitation on the action parameters. - /// That's, if an action has extra parameter that's not required for a certain convention, - /// we consider this action is valid for this convention. - /// For example, entity convention requires the key(s) parameters, doesn't care about other parameters. - /// - bool AppliesToAction(ODataControllerActionContext context); - } + /// + /// Applies the convention on action of this controller. + /// + /// The controller action context. + /// + /// True: yes, apply the convention, please stop executing the remaining conventions. + /// False: no, please continue to execute the remaining conventions. + /// + /// + /// The OData action convention should not put limitation on the action parameters. + /// That's, if an action has extra parameter that's not required for a certain convention, + /// we consider this action is valid for this convention. + /// For example, entity convention requires the key(s) parameters, doesn't care about other parameters. + /// + bool AppliesToAction(ODataControllerActionContext context); } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/MetadataRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/MetadataRoutingConvention.cs index 62a717309..efa54d92f 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/MetadataRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/MetadataRoutingConvention.cs @@ -13,63 +13,62 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using Microsoft.AspNetCore.OData.Routing.Template; -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// The convention for $metadata. +/// +public class MetadataRoutingConvention : IODataControllerActionConvention { + private static TypeInfo metadataTypeInfo = typeof(MetadataController).GetTypeInfo(); + /// - /// The convention for $metadata. + /// Gets the order value for determining the order of execution of conventions. + /// Metadata routing convention has 0 order. /// - public class MetadataRoutingConvention : IODataControllerActionConvention - { - private static TypeInfo metadataTypeInfo = typeof(MetadataController).GetTypeInfo(); - - /// - /// Gets the order value for determining the order of execution of conventions. - /// Metadata routing convention has 0 order. - /// - public virtual int Order => 0; + public virtual int Order => 0; - /// - public virtual bool AppliesToController(ODataControllerActionContext context) + /// + public virtual bool AppliesToController(ODataControllerActionContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - // This convention only applies to "MetadataController". - return context.Controller.ControllerType == metadataTypeInfo; + throw Error.ArgumentNull(nameof(context)); } - /// - public virtual bool AppliesToAction(ODataControllerActionContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + // This convention only applies to "MetadataController". + return context.Controller.ControllerType == metadataTypeInfo; + } - Debug.Assert(context.Controller != null); - Debug.Assert(context.Action != null); - ActionModel action = context.Action; - string actionName = action.ActionName; + /// + public virtual bool AppliesToAction(ODataControllerActionContext context) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - // for ~$metadata - if (actionName == "GetMetadata") - { - ODataPathTemplate template = new ODataPathTemplate(MetadataSegmentTemplate.Instance); - action.AddSelector("Get", context.Prefix, context.Model, template, context.Options?.RouteOptions); - return true; - } + Debug.Assert(context.Controller != null); + Debug.Assert(context.Action != null); + ActionModel action = context.Action; + string actionName = action.ActionName; - // for ~/ - if (actionName == "GetServiceDocument") - { - ODataPathTemplate template = new ODataPathTemplate(); - action.AddSelector("Get", context.Prefix, context.Model, template, context.Options?.RouteOptions); - return true; - } + // for ~$metadata + if (actionName == "GetMetadata") + { + ODataPathTemplate template = new ODataPathTemplate(MetadataSegmentTemplate.Instance); + action.AddSelector("Get", context.Prefix, context.Model, template, context.Options?.RouteOptions); + return true; + } - return false; + // for ~/ + if (actionName == "GetServiceDocument") + { + ODataPathTemplate template = new ODataPathTemplate(); + action.AddSelector("Get", context.Prefix, context.Model, template, context.Options?.RouteOptions); + return true; } + + return false; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/NavigationRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/NavigationRoutingConvention.cs index 1429831eb..b3ae6432e 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/NavigationRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/NavigationRoutingConvention.cs @@ -17,268 +17,267 @@ using Microsoft.Extensions.Logging; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// Conventions for . +/// Action name convention should follow this: {HttpMethodName}{NavigationPropertyName}[From{DeclaringTypeName}] +/// +public class NavigationRoutingConvention : IODataControllerActionConvention { + private readonly ILogger _logger; + /// - /// Conventions for . - /// Action name convention should follow this: {HttpMethodName}{NavigationPropertyName}[From{DeclaringTypeName}] + /// Initializes a new instance of the class. /// - public class NavigationRoutingConvention : IODataControllerActionConvention + /// The injected logger. + public NavigationRoutingConvention(ILogger logger) { - private readonly ILogger _logger; + _logger = logger ?? throw Error.ArgumentNull(nameof(logger)); + } - /// - /// Initializes a new instance of the class. - /// - /// The injected logger. - public NavigationRoutingConvention(ILogger logger) + /// + public virtual int Order => 500; + + /// + public virtual bool AppliesToController(ODataControllerActionContext context) + { + if (context == null) { - _logger = logger ?? throw Error.ArgumentNull(nameof(logger)); + throw Error.ArgumentNull(nameof(context)); } - /// - public virtual int Order => 500; + // structural property supports for entity set and singleton + return context.NavigationSource != null; + } - /// - public virtual bool AppliesToController(ODataControllerActionContext context) + /// + public virtual bool AppliesToAction(ODataControllerActionContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - // structural property supports for entity set and singleton - return context.NavigationSource != null; + throw Error.ArgumentNull(nameof(context)); } - /// - public virtual bool AppliesToAction(ODataControllerActionContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + ActionModel action = context.Action; - ActionModel action = context.Action; + IEdmNavigationSource navigationSource = context.NavigationSource; - IEdmNavigationSource navigationSource = context.NavigationSource; + // filter by action parameter + IEdmEntityType entityType = navigationSource.EntityType; + bool hasKeyParameter = action.HasODataKeyParameter(entityType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false); + if (!(context.Singleton != null ^ hasKeyParameter)) + { + // Singleton, doesn't allow to query property with key + // entityset, doesn't allow for non-key to query property + return false; + } - // filter by action parameter - IEdmEntityType entityType = navigationSource.EntityType; - bool hasKeyParameter = action.HasODataKeyParameter(entityType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false); - if (!(context.Singleton != null ^ hasKeyParameter)) - { - // Singleton, doesn't allow to query property with key - // entityset, doesn't allow for non-key to query property - return false; - } + // Filter by the action name. + // The action for navigation property request should follow up {httpMethod}{PropertyName}[From{Declaring}] + string actionName = action.ActionName; + string method = SplitActionName(actionName, out string property, out string declared); + if (method == null || string.IsNullOrEmpty(property)) + { + return false; + } - // Filter by the action name. - // The action for navigation property request should follow up {httpMethod}{PropertyName}[From{Declaring}] - string actionName = action.ActionName; - string method = SplitActionName(actionName, out string property, out string declared); - if (method == null || string.IsNullOrEmpty(property)) + // Find the declaring type of the property if we have the declaring type name in the action name. + // Otherwise, it means the property is defined on the entity type of the navigation source. + IEdmEntityType declaringEntityType = entityType; + if (declared != null) + { + if (declared.Length == 0) { + // Early return for the following cases: Get|PostTo|PutTo|PatchTo{NavigationProperty}From return false; } - // Find the declaring type of the property if we have the declaring type name in the action name. - // Otherwise, it means the property is defined on the entity type of the navigation source. - IEdmEntityType declaringEntityType = entityType; - if (declared != null) - { - if (declared.Length == 0) - { - // Early return for the following cases: Get|PostTo|PutTo|PatchTo{NavigationProperty}From - return false; - } - - declaringEntityType = entityType.FindTypeInInheritance(context.Model, declared) as IEdmEntityType; - if (declaringEntityType == null) - { - return false; - } - } - - // Find the property, and we only care about the navigation property. - bool enablePropertyNameCaseInsensitive = context?.Options?.RouteOptions.EnablePropertyNameCaseInsensitive ?? false; - - IEdmProperty edmProperty = declaringEntityType.FindProperty(property, enablePropertyNameCaseInsensitive); - if (edmProperty == null || edmProperty.PropertyKind == EdmPropertyKind.Structural) + declaringEntityType = entityType.FindTypeInInheritance(context.Model, declared) as IEdmEntityType; + if (declaringEntityType == null) { return false; } + } - // Starts the routing template - //IList segments = new List(); - //if (context.EntitySet != null) - //{ - // segments.Add(new EntitySetSegmentTemplate(context.EntitySet)); - //} - //else - //{ - // segments.Add(new SingletonSegmentTemplate(context.Singleton)); - //} + // Find the property, and we only care about the navigation property. + bool enablePropertyNameCaseInsensitive = context?.Options?.RouteOptions.EnablePropertyNameCaseInsensitive ?? false; - //if (hasKeyParameter) - //{ - // segments.Add(new KeySegmentTemplate(entityType)); - //} + IEdmProperty edmProperty = declaringEntityType.FindProperty(property, enablePropertyNameCaseInsensitive); + if (edmProperty == null || edmProperty.PropertyKind == EdmPropertyKind.Structural) + { + return false; + } - //if (declared != null) - //{ - // // It should be always single type - // segments.Add(new CastSegmentTemplate(declaringEntityType, entityType, navigationSource)); - //} + // Starts the routing template + //IList segments = new List(); + //if (context.EntitySet != null) + //{ + // segments.Add(new EntitySetSegmentTemplate(context.EntitySet)); + //} + //else + //{ + // segments.Add(new SingletonSegmentTemplate(context.Singleton)); + //} - IEdmNavigationProperty navigationProperty = (IEdmNavigationProperty)edmProperty; - //IEdmNavigationSource targetNavigationSource = navigationSource.FindNavigationTarget(navigationProperty, segments, out _); + //if (hasKeyParameter) + //{ + // segments.Add(new KeySegmentTemplate(entityType)); + //} - //segments.Add(new NavigationSegmentTemplate(navigationProperty, targetNavigationSource)); + //if (declared != null) + //{ + // // It should be always single type + // segments.Add(new CastSegmentTemplate(declaringEntityType, entityType, navigationSource)); + //} - //ODataPathTemplate template = new ODataPathTemplate(segments); - //action.AddSelector(method, context.Prefix, context.Model, template); + IEdmNavigationProperty navigationProperty = (IEdmNavigationProperty)edmProperty; + //IEdmNavigationSource targetNavigationSource = navigationSource.FindNavigationTarget(navigationProperty, segments, out _); - AddSelector(method, context, action, navigationSource, declared, declaringEntityType, navigationProperty, hasKeyParameter, false); + //segments.Add(new NavigationSegmentTemplate(navigationProperty, targetNavigationSource)); - if (CanApplyDollarCount(navigationProperty, method, context.Options?.RouteOptions)) - { - AddSelector(method, context, action, navigationSource, declared, declaringEntityType, navigationProperty, hasKeyParameter, true); - } + //ODataPathTemplate template = new ODataPathTemplate(segments); + //action.AddSelector(method, context.Prefix, context.Model, template); - return true; - } + AddSelector(method, context, action, navigationSource, declared, declaringEntityType, navigationProperty, hasKeyParameter, false); - private void AddSelector(string httpMethod, ODataControllerActionContext context, ActionModel action, - IEdmNavigationSource navigationSource, string declared, IEdmEntityType declaringEntityType, - IEdmNavigationProperty navigationProperty, bool hasKey, bool dollarCount) + if (CanApplyDollarCount(navigationProperty, method, context.Options?.RouteOptions)) { - IEdmEntitySet entitySet = context.EntitySet; - IEdmEntityType entityType = navigationSource.EntityType; - - // Starts the routing template - IList segments = new List(); - if (entitySet != null) - { - segments.Add(new EntitySetSegmentTemplate(entitySet)); - } - else - { - segments.Add(new SingletonSegmentTemplate(navigationSource as IEdmSingleton)); - } + AddSelector(method, context, action, navigationSource, declared, declaringEntityType, navigationProperty, hasKeyParameter, true); + } - if (hasKey) - { - segments.Add(KeySegmentTemplate.CreateKeySegment(entityType, navigationSource)); - } + return true; + } - if (declared != null) - { - // It should be always single type - if (entityType != declaringEntityType) - { - segments.Add(new CastSegmentTemplate(declaringEntityType, entityType, navigationSource)); - } - } + private void AddSelector(string httpMethod, ODataControllerActionContext context, ActionModel action, + IEdmNavigationSource navigationSource, string declared, IEdmEntityType declaringEntityType, + IEdmNavigationProperty navigationProperty, bool hasKey, bool dollarCount) + { + IEdmEntitySet entitySet = context.EntitySet; + IEdmEntityType entityType = navigationSource.EntityType; - IEdmNavigationSource targetNavigationSource = navigationSource.FindNavigationTarget(navigationProperty, segments, out _); + // Starts the routing template + IList segments = new List(); + if (entitySet != null) + { + segments.Add(new EntitySetSegmentTemplate(entitySet)); + } + else + { + segments.Add(new SingletonSegmentTemplate(navigationSource as IEdmSingleton)); + } - segments.Add(new NavigationSegmentTemplate(navigationProperty, targetNavigationSource)); + if (hasKey) + { + segments.Add(KeySegmentTemplate.CreateKeySegment(entityType, navigationSource)); + } - if (dollarCount) + if (declared != null) + { + // It should be always single type + if (entityType != declaringEntityType) { - segments.Add(CountSegmentTemplate.Instance); + segments.Add(new CastSegmentTemplate(declaringEntityType, entityType, navigationSource)); } + } - ODataPathTemplate template = new ODataPathTemplate(segments); - action.AddSelector(httpMethod.NormalizeHttpMethod(), context.Prefix, context.Model, template, context.Options?.RouteOptions); + IEdmNavigationSource targetNavigationSource = navigationSource.FindNavigationTarget(navigationProperty, segments, out _); - Log.AddedODataSelector(_logger, action, template); - } + segments.Add(new NavigationSegmentTemplate(navigationProperty, targetNavigationSource)); - /// - /// split action using navigation action name convention. - /// For example: PostToOrdersFromVipOrder - /// => Method Name: PostTo - /// => property : Orders - /// => declaring: VipOrder - /// - /// The input action name. - /// The property name (out). - /// The declaring name (out). - /// The http method name or null. - internal static string SplitActionName(string actionName, out string property, out string declaring) + if (dollarCount) { - string method = null; - string text = null; + segments.Add(CountSegmentTemplate.Instance); + } - // HttpMethodName{NavigationPropertyName}From - foreach (var prefix in new[] { "Get", "PostTo", "PutTo", "PatchTo" }) - { - if (actionName.StartsWith(prefix, StringComparison.Ordinal)) - { - method = prefix; - text = actionName.Substring(prefix.Length); - break; - } - } + ODataPathTemplate template = new ODataPathTemplate(segments); + action.AddSelector(httpMethod.NormalizeHttpMethod(), context.Prefix, context.Model, template, context.Options?.RouteOptions); - property = null; - declaring = null; - if (method == null) - { - return null; - } + Log.AddedODataSelector(_logger, action, template); + } - int index = text.IndexOf("From", StringComparison.Ordinal); - if (index > 0) - { - property = text.Substring(0, index); - declaring = text.Substring(index + 4); - } - else + /// + /// split action using navigation action name convention. + /// For example: PostToOrdersFromVipOrder + /// => Method Name: PostTo + /// => property : Orders + /// => declaring: VipOrder + /// + /// The input action name. + /// The property name (out). + /// The declaring name (out). + /// The http method name or null. + internal static string SplitActionName(string actionName, out string property, out string declaring) + { + string method = null; + string text = null; + + // HttpMethodName{NavigationPropertyName}From + foreach (var prefix in new[] { "Get", "PostTo", "PutTo", "PatchTo" }) + { + if (actionName.StartsWith(prefix, StringComparison.Ordinal)) { - property = text; + method = prefix; + text = actionName.Substring(prefix.Length); + break; } + } - return method; + property = null; + declaring = null; + if (method == null) + { + return null; } - /// - /// OData spec: To request only the number of items of a collection of entities or items of a collection-valued property, - /// the client issues a GET request with /$count appended to the resource path of the collection. - /// - /// The property to test. - /// The http method. - /// The route options. - /// True/false to identify whether to apply $count. - protected virtual bool CanApplyDollarCount(IEdmNavigationProperty edmProperty, string method, ODataRouteOptions routeOptions) + int index = text.IndexOf("From", StringComparison.Ordinal); + if (index > 0) { - if(edmProperty == null) - { - throw Error.ArgumentNull(nameof(edmProperty)); - } + property = text.Substring(0, index); + declaring = text.Substring(index + 4); + } + else + { + property = text; + } - if (routeOptions != null && !routeOptions.EnableDollarCountRouting) - { - return false; - } + return method; + } - return method == "Get" && edmProperty.Type.IsCollection(); + /// + /// OData spec: To request only the number of items of a collection of entities or items of a collection-valued property, + /// the client issues a GET request with /$count appended to the resource path of the collection. + /// + /// The property to test. + /// The http method. + /// The route options. + /// True/false to identify whether to apply $count. + protected virtual bool CanApplyDollarCount(IEdmNavigationProperty edmProperty, string method, ODataRouteOptions routeOptions) + { + if(edmProperty == null) + { + throw Error.ArgumentNull(nameof(edmProperty)); } - private static class Log + if (routeOptions != null && !routeOptions.EnableDollarCountRouting) { - private static readonly Action _addedODataSelector = LoggerMessage.Define( - LogLevel.Information, - new EventId(1, "AddODataNavigationConvention"), - "Added OData Convention '{ConventionMessage}'"); + return false; + } - public static void AddedODataSelector(ILogger logger, ActionModel action, ODataPathTemplate template) - { - string message = action.DisplayName + ": " + template.GetTemplates().FirstOrDefault(); - _addedODataSelector(logger, message, null); - } + return method == "Get" && edmProperty.Type.IsCollection(); + } + + private static class Log + { + private static readonly Action _addedODataSelector = LoggerMessage.Define( + LogLevel.Information, + new EventId(1, "AddODataNavigationConvention"), + "Added OData Convention '{ConventionMessage}'"); + + public static void AddedODataSelector(ILogger logger, ActionModel action, ODataPathTemplate template) + { + string message = action.DisplayName + ": " + template.GetTemplates().FirstOrDefault(); + _addedODataSelector(logger, message, null); } } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/ODataControllerActionContext.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/ODataControllerActionContext.cs index 32c1a1007..b2df12875 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/ODataControllerActionContext.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/ODataControllerActionContext.cs @@ -8,86 +8,85 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// A context object for . +/// +/// +/// Why do i design "ControllerActionContext", not "ControllerContext" and "ActionContext". +/// It's because a controller may have a bound of actions, and i don't want to create an ActionContext for all of these actions. +/// +public class ODataControllerActionContext { /// - /// A context object for . + /// Initializes a new instance of the class. + /// For unit test only /// - /// - /// Why do i design "ControllerActionContext", not "ControllerContext" and "ActionContext". - /// It's because a controller may have a bound of actions, and i don't want to create an ActionContext for all of these actions. - /// - public class ODataControllerActionContext + internal ODataControllerActionContext() { - /// - /// Initializes a new instance of the class. - /// For unit test only - /// - internal ODataControllerActionContext() - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The prefix. - /// The Edm model. - /// The controller model. - public ODataControllerActionContext(string prefix, IEdmModel model, ControllerModel controller) - { - Prefix = prefix ?? throw Error.ArgumentNull(nameof(prefix)); + /// + /// Initializes a new instance of the class. + /// + /// The prefix. + /// The Edm model. + /// The controller model. + public ODataControllerActionContext(string prefix, IEdmModel model, ControllerModel controller) + { + Prefix = prefix ?? throw Error.ArgumentNull(nameof(prefix)); - Model = model ?? throw Error.ArgumentNull(nameof(model)); + Model = model ?? throw Error.ArgumentNull(nameof(model)); - Controller = controller ?? throw Error.ArgumentNull(nameof(controller)); - } + Controller = controller ?? throw Error.ArgumentNull(nameof(controller)); + } - /// - /// Gets the associated model name for this model, it's also used as the routing prefix. - /// - public string Prefix { get; internal set; } + /// + /// Gets the associated model name for this model, it's also used as the routing prefix. + /// + public string Prefix { get; internal set; } - /// - /// Gets the Edm model. - /// - public IEdmModel Model { get; internal set; } + /// + /// Gets the Edm model. + /// + public IEdmModel Model { get; internal set; } - /// - /// Gets the related controller model in this context. This property should never be "null". - /// - public ControllerModel Controller { get; internal set; } + /// + /// Gets the related controller model in this context. This property should never be "null". + /// + public ControllerModel Controller { get; internal set; } - /// - /// Gets/sets the navigation source associated with controller. - /// - public IEdmNavigationSource NavigationSource { get; set; } + /// + /// Gets/sets the navigation source associated with controller. + /// + public IEdmNavigationSource NavigationSource { get; set; } - /// - /// Gets/sets the related action model in this context. - /// - public ActionModel Action { get; set; } + /// + /// Gets/sets the related action model in this context. + /// + public ActionModel Action { get; set; } - /// - /// Gets the associated for this controller. - /// It might be null. - /// - public IEdmEntitySet EntitySet => NavigationSource as IEdmEntitySet; + /// + /// Gets the associated for this controller. + /// It might be null. + /// + public IEdmEntitySet EntitySet => NavigationSource as IEdmEntitySet; - /// - /// Gets the associated . - /// It might be null. - /// - public IEdmEntityType EntityType => NavigationSource?.EntityType; + /// + /// Gets the associated . + /// It might be null. + /// + public IEdmEntityType EntityType => NavigationSource?.EntityType; - /// - /// Gets the associated for this controller. - /// It might be null. - /// - public IEdmSingleton Singleton => NavigationSource as IEdmSingleton; + /// + /// Gets the associated for this controller. + /// It might be null. + /// + public IEdmSingleton Singleton => NavigationSource as IEdmSingleton; - /// - /// Gets/sets the OData Options. - /// - public ODataOptions Options { get; set; } = new ODataOptions(); - } + /// + /// Gets/sets the OData Options. + /// + public ODataOptions Options { get; set; } = new ODataOptions(); } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationImportRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationImportRoutingConvention.cs index d9eac67b3..2e9cc767a 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationImportRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationImportRoutingConvention.cs @@ -16,130 +16,129 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// The convention for . +/// Get ~/functionimport(....) +/// Post ~/actionimport +/// +public class OperationImportRoutingConvention : IODataControllerActionConvention { - /// - /// The convention for . - /// Get ~/functionimport(....) - /// Post ~/actionimport - /// - public class OperationImportRoutingConvention : IODataControllerActionConvention - { - /// - public virtual int Order => 900; + /// + public virtual int Order => 900; - /// - public virtual bool AppliesToController(ODataControllerActionContext context) + /// + public virtual bool AppliesToController(ODataControllerActionContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - // By convention, we look for the controller name as "ODataOperationImportController" - // Each operation import will be handled by the same action name in this controller. - return context.Controller.ControllerName == "ODataOperationImport"; + throw Error.ArgumentNull(nameof(context)); } - /// - public virtual bool AppliesToAction(ODataControllerActionContext context) + // By convention, we look for the controller name as "ODataOperationImportController" + // Each operation import will be handled by the same action name in this controller. + return context.Controller.ControllerName == "ODataOperationImport"; + } + + /// + public virtual bool AppliesToAction(ODataControllerActionContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - ActionModel action = context.Action; - IEdmModel model = context.Model; + ActionModel action = context.Action; + IEdmModel model = context.Model; - // By convention, we use the operation import name as the action name in the controller - string actionMethodName = action.ActionName; + // By convention, we use the operation import name as the action name in the controller + string actionMethodName = action.ActionName; - var edmOperationImports = model.ResolveOperationImports(actionMethodName, enableCaseInsensitive: true); - if (!edmOperationImports.Any()) - { - return true; - } + var edmOperationImports = model.ResolveOperationImports(actionMethodName, enableCaseInsensitive: true); + if (!edmOperationImports.Any()) + { + return true; + } - (var actionImports, var functionImports) = edmOperationImports.SplitOperationImports(); + (var actionImports, var functionImports) = edmOperationImports.SplitOperationImports(); - // It's not allowed to have an action import and function import with the same name. - if (actionImports.Count > 0 && functionImports.Count > 0) + // It's not allowed to have an action import and function import with the same name. + if (actionImports.Count > 0 && functionImports.Count > 0) + { + throw new ODataException(Error.Format(SRResources.OperationMustBeUniqueInEntitySetContainer, actionMethodName)); + } + else if (actionImports.Count > 0 && context.Action.Attributes.Any(a => a is HttpPostAttribute)) + { + if (actionImports.Count != 1) { - throw new ODataException(Error.Format(SRResources.OperationMustBeUniqueInEntitySetContainer, actionMethodName)); + throw new ODataException(Error.Format(SRResources.MultipleActionImportFound, actionMethodName)); } - else if (actionImports.Count > 0 && context.Action.Attributes.Any(a => a is HttpPostAttribute)) - { - if (actionImports.Count != 1) - { - throw new ODataException(Error.Format(SRResources.MultipleActionImportFound, actionMethodName)); - } - IEdmActionImport actionImport = actionImports[0]; + IEdmActionImport actionImport = actionImports[0]; - IEdmEntitySetBase targetEntitySet; - actionImport.TryGetStaticEntitySet(model, out targetEntitySet); + IEdmEntitySetBase targetEntitySet; + actionImport.TryGetStaticEntitySet(model, out targetEntitySet); - // TODO: - // 1. shall we check the [HttpPost] attribute, or does the ASP.NET Core have the default? - // 2) shall we check the action has "ODataActionParameters" parameter type? - ODataPathTemplate template = new ODataPathTemplate(new ActionImportSegmentTemplate(actionImport, targetEntitySet)); - action.AddSelector("Post", context.Prefix, context.Model, template, context.Options?.RouteOptions); - return true; - } - else if (functionImports.Count > 0 && context.Action.Attributes.Any(a => a is HttpGetAttribute)) - { - IEdmFunctionImport functionImport = FindFunctionImport(functionImports, action); - if (functionImport == null) - { - return false; - } - - IEdmEntitySetBase targetSet; - functionImport.TryGetStaticEntitySet(model, out targetSet); - - // TODO: - // 1) shall we check the [HttpGet] attribute, or does the ASP.NET Core have the default? - ODataPathTemplate template = new ODataPathTemplate(new FunctionImportSegmentTemplate(functionImport, targetSet)); - action.AddSelector("Get", context.Prefix, context.Model, template, context.Options?.RouteOptions); - return true; - } - else + // TODO: + // 1. shall we check the [HttpPost] attribute, or does the ASP.NET Core have the default? + // 2) shall we check the action has "ODataActionParameters" parameter type? + ODataPathTemplate template = new ODataPathTemplate(new ActionImportSegmentTemplate(actionImport, targetEntitySet)); + action.AddSelector("Post", context.Prefix, context.Model, template, context.Options?.RouteOptions); + return true; + } + else if (functionImports.Count > 0 && context.Action.Attributes.Any(a => a is HttpGetAttribute)) + { + IEdmFunctionImport functionImport = FindFunctionImport(functionImports, action); + if (functionImport == null) { - // doesn't find an operation, return true means to skip the remaining conventions. return false; } + + IEdmEntitySetBase targetSet; + functionImport.TryGetStaticEntitySet(model, out targetSet); + + // TODO: + // 1) shall we check the [HttpGet] attribute, or does the ASP.NET Core have the default? + ODataPathTemplate template = new ODataPathTemplate(new FunctionImportSegmentTemplate(functionImport, targetSet)); + action.AddSelector("Get", context.Prefix, context.Model, template, context.Options?.RouteOptions); + return true; + } + else + { + // doesn't find an operation, return true means to skip the remaining conventions. + return false; } + } - private static IEdmFunctionImport FindFunctionImport(IList functionImports, ActionModel action) + private static IEdmFunctionImport FindFunctionImport(IList functionImports, ActionModel action) + { + foreach (var functionImport in functionImports) { - foreach (var functionImport in functionImports) + if (functionImport.Function.IsBound) { - if (functionImport.Function.IsBound) - { - continue; - } + continue; + } - bool match = true; - foreach (var parameter in functionImport.Function.Parameters) + bool match = true; + foreach (var parameter in functionImport.Function.Parameters) + { + if (!action.Parameters.Any(p => p.ParameterName == parameter.Name)) { - if (!action.Parameters.Any(p => p.ParameterName == parameter.Name)) - { - // if any parameter is not in the action parameters, skip this. - match = false; - break; - } - - // TODO: shall we check each parameter has the [FromODataUri] attribute? + // if any parameter is not in the action parameters, skip this. + match = false; + break; } - if (match) - { - return functionImport; - } + // TODO: shall we check each parameter has the [FromODataUri] attribute? } - return null; + if (match) + { + return functionImport; + } } + + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs index 04598b8c4..a80f133c7 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/OperationRoutingConvention.cs @@ -15,332 +15,331 @@ using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// Conventions for and . +/// Get ~/entityset|singleton/function, ~/entityset|singleton/cast/function +/// Get ~/entityset/key/function, ~/entityset/key/cast/function +/// Post ~/entityset|singleton/action, ~/entityset|singleton/cast/action +/// Post ~/entityset/key/action, ~/entityset/key/cast/action +/// +public abstract class OperationRoutingConvention : IODataControllerActionConvention { + /// + public abstract int Order { get; } + + /// + public virtual bool AppliesToController(ODataControllerActionContext context) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + // bound operation supports for entity set and singleton + return context.NavigationSource != null; + } + + /// + public abstract bool AppliesToAction(ODataControllerActionContext context); + /// - /// Conventions for and . - /// Get ~/entityset|singleton/function, ~/entityset|singleton/cast/function - /// Get ~/entityset/key/function, ~/entityset/key/cast/function - /// Post ~/entityset|singleton/action, ~/entityset|singleton/cast/action - /// Post ~/entityset/key/action, ~/entityset/key/cast/action + /// Process the operation candidates using the information. /// - public abstract class OperationRoutingConvention : IODataControllerActionConvention + /// The controller and action context. + /// The Edm entity type. + /// The Edm navigation source. + protected void ProcessOperations(ODataControllerActionContext context, IEdmEntityType entityType, IEdmNavigationSource navigationSource) { - /// - public abstract int Order { get; } + Contract.Assert(context != null); + Contract.Assert(entityType != null); + Contract.Assert(navigationSource != null); - /// - public virtual bool AppliesToController(ODataControllerActionContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + string actionName = context.Action.ActionName; - // bound operation supports for entity set and singleton - return context.NavigationSource != null; + bool hasKeyParameter = context.Action.HasODataKeyParameter(entityType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false); + if (context.Singleton != null && hasKeyParameter) + { + // Singleton doesn't allow to call action with key. + return; } - /// - public abstract bool AppliesToAction(ODataControllerActionContext context); + bool isOnCollection = false; + IEdmEntityType castTypeFromActionName = null; - /// - /// Process the operation candidates using the information. - /// - /// The controller and action context. - /// The Edm entity type. - /// The Edm navigation source. - protected void ProcessOperations(ODataControllerActionContext context, IEdmEntityType entityType, IEdmNavigationSource navigationSource) + IEdmOperation[] candidates = FindCandidates(context, actionName); + if (candidates.Length == 0) { - Contract.Assert(context != null); - Contract.Assert(entityType != null); - Contract.Assert(navigationSource != null); - - string actionName = context.Action.ActionName; + // If we can't find any Edm operation using the action name directly, + // Let's split the action name and use part of it to search again. + candidates = FindCandidates(context, entityType, actionName, out castTypeFromActionName, out isOnCollection); + } - bool hasKeyParameter = context.Action.HasODataKeyParameter(entityType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false); - if (context.Singleton != null && hasKeyParameter) + foreach (IEdmOperation edmOperation in candidates) + { + IEdmOperationParameter bindingParameter = edmOperation.Parameters.FirstOrDefault(); + if (bindingParameter == null) { - // Singleton doesn't allow to call action with key. - return; + // bound operation at least has one parameter which type is the binding type. + continue; } - bool isOnCollection = false; - IEdmEntityType castTypeFromActionName = null; - - IEdmOperation[] candidates = FindCandidates(context, actionName); - if (candidates.Length == 0) + IEdmTypeReference bindingType = bindingParameter.Type; + bool bindToCollection = bindingType.TypeKind() == EdmTypeKind.Collection; + if (bindToCollection) { - // If we can't find any Edm operation using the action name directly, - // Let's split the action name and use part of it to search again. - candidates = FindCandidates(context, entityType, actionName, out castTypeFromActionName, out isOnCollection); + // if binding to collection the action has key parameter or a singleton, skip + if (context.Singleton != null || hasKeyParameter) + { + continue; + } } - - foreach (IEdmOperation edmOperation in candidates) + else { - IEdmOperationParameter bindingParameter = edmOperation.Parameters.FirstOrDefault(); - if (bindingParameter == null) + // if binding to non-collection and the action hasn't key parameter, skip + if (isOnCollection || (context.EntitySet != null && !hasKeyParameter)) { - // bound operation at least has one parameter which type is the binding type. continue; } + } + + // We only allow the binding type is entity type or collection of entity type. + if (!bindingType.Definition.IsEntityOrEntityCollectionType(out IEdmEntityType bindingEntityType)) + { + continue; + } - IEdmTypeReference bindingType = bindingParameter.Type; - bool bindToCollection = bindingType.TypeKind() == EdmTypeKind.Collection; - if (bindToCollection) + IEdmEntityType castType = null; + if (castTypeFromActionName == null) + { + if (entityType.IsOrInheritsFrom(bindingEntityType)) { - // if binding to collection the action has key parameter or a singleton, skip - if (context.Singleton != null || hasKeyParameter) - { - continue; - } + // True if and only if the thisType is equivalent to or inherits from otherType. + castType = null; } - else + else if (bindingEntityType.InheritsFrom(entityType)) { - // if binding to non-collection and the action hasn't key parameter, skip - if (isOnCollection || (context.EntitySet != null && !hasKeyParameter)) - { - continue; - } + // True if and only if the type inherits from the potential base type. + castType = bindingEntityType; } - - // We only allow the binding type is entity type or collection of entity type. - if (!bindingType.Definition.IsEntityOrEntityCollectionType(out IEdmEntityType bindingEntityType)) + else { continue; } - - IEdmEntityType castType = null; - if (castTypeFromActionName == null) - { - if (entityType.IsOrInheritsFrom(bindingEntityType)) - { - // True if and only if the thisType is equivalent to or inherits from otherType. - castType = null; - } - else if (bindingEntityType.InheritsFrom(entityType)) - { - // True if and only if the type inherits from the potential base type. - castType = bindingEntityType; - } - else - { - continue; - } - } - else + } + else + { + if (isOnCollection && !bindToCollection) { - if (isOnCollection && !bindToCollection) - { - continue; - } - - if (bindingEntityType != castTypeFromActionName) - { - continue; - } - - if (castTypeFromActionName != entityType) - { - castType = castTypeFromActionName; - } + continue; } - // TODO: need discussion about: - // 1) Do we need to match the whole parameter count? - // 2) Do we need to select the best match? So far, i don't think and let it go. - if (!IsOperationParameterMatched(edmOperation, context.Action)) + if (bindingEntityType != castTypeFromActionName) { continue; } - AddSelector(context, edmOperation, hasKeyParameter, entityType, navigationSource, castType); + if (castTypeFromActionName != entityType) + { + castType = castTypeFromActionName; + } } - } - - private static IEdmOperation[] FindCandidates(ODataControllerActionContext context, string operationName) - { - // TODO: refactor here - // If we have multiple same function defined, we should match the best one? - StringComparison actionNameComparison = context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true ? - StringComparison.OrdinalIgnoreCase : - StringComparison.Ordinal; + // TODO: need discussion about: + // 1) Do we need to match the whole parameter count? + // 2) Do we need to select the best match? So far, i don't think and let it go. + if (!IsOperationParameterMatched(edmOperation, context.Action)) + { + continue; + } - return context.Model.SchemaElements - .OfType() - .Where(f => f.IsBound && f.Name.Equals(operationName, actionNameComparison)) - .ToArray(); + AddSelector(context, edmOperation, hasKeyParameter, entityType, navigationSource, castType); } + } - private static IEdmOperation[] FindCandidates(ODataControllerActionContext context, IEdmEntityType entityType, string actionName, - out IEdmEntityType castTypeFromActionName, out bool isOnCollection) - { - // OperationNameOnCollectionOfEntityType - StringComparison caseComparision = context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true ? - StringComparison.OrdinalIgnoreCase : - StringComparison.Ordinal; + private static IEdmOperation[] FindCandidates(ODataControllerActionContext context, string operationName) + { + // TODO: refactor here + // If we have multiple same function defined, we should match the best one? - string operationName = SplitActionName(actionName, out string cast, out isOnCollection, caseComparision); + StringComparison actionNameComparison = context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true ? + StringComparison.OrdinalIgnoreCase : + StringComparison.Ordinal; - castTypeFromActionName = null; - if (cast != null) - { - if (cast.Length == 0) - { - // Early return for the following cases: - // - {OperationName}On - // - {OperationName}OnCollectionOf - return Array.Empty(); - } + return context.Model.SchemaElements + .OfType() + .Where(f => f.IsBound && f.Name.Equals(operationName, actionNameComparison)) + .ToArray(); + } - castTypeFromActionName = entityType.FindTypeInInheritance(context.Model, cast, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true) as IEdmEntityType; - if (castTypeFromActionName == null) - { - return Array.Empty(); - } - } + private static IEdmOperation[] FindCandidates(ODataControllerActionContext context, IEdmEntityType entityType, string actionName, + out IEdmEntityType castTypeFromActionName, out bool isOnCollection) + { + // OperationNameOnCollectionOfEntityType + StringComparison caseComparision = context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true ? + StringComparison.OrdinalIgnoreCase : + StringComparison.Ordinal; - return FindCandidates(context, operationName); - } + string operationName = SplitActionName(actionName, out string cast, out isOnCollection, caseComparision); - /// - /// Split the action based on supporting pattern. - /// - /// The input action name. - /// The out of cast type name. - /// The out of collection binding flag. - /// The case comparision flag. - /// The operation name. - internal static string SplitActionName(string actionName, out string cast, out bool isOnCollection, - StringComparison comparison = StringComparison.Ordinal) + castTypeFromActionName = null; + if (cast != null) { - Contract.Assert(actionName != null); - - // We support the following function/action name pattern: - // OperationNameOnCollectionOfEntityType - // OperationNameOnEntityType - // OperationName - cast = null; - isOnCollection = false; - string operation; - int index = actionName.LastIndexOf("OnCollectionOf", comparison); - if (index > 0) + if (cast.Length == 0) { - operation = actionName.Substring(0, index); - cast = actionName.Substring(index + "OnCollectionOf".Length); - isOnCollection = true; - return operation; + // Early return for the following cases: + // - {OperationName}On + // - {OperationName}OnCollectionOf + return Array.Empty(); } - index = actionName.LastIndexOf("On", comparison); - if (index > 0) + castTypeFromActionName = entityType.FindTypeInInheritance(context.Model, cast, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true) as IEdmEntityType; + if (castTypeFromActionName == null) { - operation = actionName.Substring(0, index); - cast = actionName.Substring(index + "On".Length); - return operation; + return Array.Empty(); } + } + + return FindCandidates(context, operationName); + } - return actionName; + /// + /// Split the action based on supporting pattern. + /// + /// The input action name. + /// The out of cast type name. + /// The out of collection binding flag. + /// The case comparision flag. + /// The operation name. + internal static string SplitActionName(string actionName, out string cast, out bool isOnCollection, + StringComparison comparison = StringComparison.Ordinal) + { + Contract.Assert(actionName != null); + + // We support the following function/action name pattern: + // OperationNameOnCollectionOfEntityType + // OperationNameOnEntityType + // OperationName + cast = null; + isOnCollection = false; + string operation; + int index = actionName.LastIndexOf("OnCollectionOf", comparison); + if (index > 0) + { + operation = actionName.Substring(0, index); + cast = actionName.Substring(index + "OnCollectionOf".Length); + isOnCollection = true; + return operation; } - /// - /// Verify the parameter of the Edm operation matches the parameter defined in action. - /// - /// The Edm operation. - /// The action model. - /// true if the parameter of the Edm operation matches the parameter defined in the action; otherwise, false. - protected abstract bool IsOperationParameterMatched(IEdmOperation operation, ActionModel action); - - /// - /// Add the template to the action - /// - /// The context. - /// The Edm operation. - /// Has key parameter or not. - /// The entity type. - /// The navigation source. - /// The type cast. - protected static void AddSelector(ODataControllerActionContext context, - IEdmOperation edmOperation, - bool hasKeyParameter, - IEdmEntityType entityType, - IEdmNavigationSource navigationSource, - IEdmEntityType castType) + index = actionName.LastIndexOf("On", comparison); + if (index > 0) { - Contract.Assert(context != null); - Contract.Assert(entityType != null); - Contract.Assert(navigationSource != null); - Contract.Assert(edmOperation != null); - - // Now, let's add the selector model. - IList segments = new List(); - if (context.EntitySet != null) - { - segments.Add(new EntitySetSegmentTemplate(context.EntitySet)); - if (hasKeyParameter) - { - segments.Add(KeySegmentTemplate.CreateKeySegment(entityType, navigationSource)); - } - } - else - { - segments.Add(new SingletonSegmentTemplate(context.Singleton)); - } + operation = actionName.Substring(0, index); + cast = actionName.Substring(index + "On".Length); + return operation; + } - if (castType != null) - { - if (context.Singleton != null || !hasKeyParameter) - { - segments.Add(new CastSegmentTemplate(castType, entityType, navigationSource)); - } - else - { - segments.Add(new CastSegmentTemplate(new EdmCollectionType(castType.ToEdmTypeReference(false)), - new EdmCollectionType(entityType.ToEdmTypeReference(false)), navigationSource)); - } - } + return actionName; + } + + /// + /// Verify the parameter of the Edm operation matches the parameter defined in action. + /// + /// The Edm operation. + /// The action model. + /// true if the parameter of the Edm operation matches the parameter defined in the action; otherwise, false. + protected abstract bool IsOperationParameterMatched(IEdmOperation operation, ActionModel action); - IEdmNavigationSource targetEntitySet = null; - if (edmOperation.ReturnType != null) + /// + /// Add the template to the action + /// + /// The context. + /// The Edm operation. + /// Has key parameter or not. + /// The entity type. + /// The navigation source. + /// The type cast. + protected static void AddSelector(ODataControllerActionContext context, + IEdmOperation edmOperation, + bool hasKeyParameter, + IEdmEntityType entityType, + IEdmNavigationSource navigationSource, + IEdmEntityType castType) + { + Contract.Assert(context != null); + Contract.Assert(entityType != null); + Contract.Assert(navigationSource != null); + Contract.Assert(edmOperation != null); + + // Now, let's add the selector model. + IList segments = new List(); + if (context.EntitySet != null) + { + segments.Add(new EntitySetSegmentTemplate(context.EntitySet)); + if (hasKeyParameter) { - targetEntitySet = edmOperation.GetTargetEntitySet(navigationSource, context.Model); + segments.Add(KeySegmentTemplate.CreateKeySegment(entityType, navigationSource)); } + } + else + { + segments.Add(new SingletonSegmentTemplate(context.Singleton)); + } - string httpMethod; - if (edmOperation.IsAction()) + if (castType != null) + { + if (context.Singleton != null || !hasKeyParameter) { - segments.Add(new ActionSegmentTemplate((IEdmAction)edmOperation, targetEntitySet)); - httpMethod = "Post"; + segments.Add(new CastSegmentTemplate(castType, entityType, navigationSource)); } else { - IDictionary required = GetRequiredFunctionParamters(edmOperation, context.Action); - segments.Add(new FunctionSegmentTemplate(required, (IEdmFunction)edmOperation, targetEntitySet)); - httpMethod = "Get"; + segments.Add(new CastSegmentTemplate(new EdmCollectionType(castType.ToEdmTypeReference(false)), + new EdmCollectionType(entityType.ToEdmTypeReference(false)), navigationSource)); } + } - ODataPathTemplate template = new ODataPathTemplate(segments); - context.Action.AddSelector(httpMethod, context.Prefix, context.Model, template, context.Options?.RouteOptions); + IEdmNavigationSource targetEntitySet = null; + if (edmOperation.ReturnType != null) + { + targetEntitySet = edmOperation.GetTargetEntitySet(navigationSource, context.Model); } - private static IDictionary GetRequiredFunctionParamters(IEdmOperation operation, ActionModel action) + string httpMethod; + if (edmOperation.IsAction()) + { + segments.Add(new ActionSegmentTemplate((IEdmAction)edmOperation, targetEntitySet)); + httpMethod = "Post"; + } + else { - Contract.Assert(operation != null); - Contract.Assert(operation.IsFunction()); - Contract.Assert(action != null); + IDictionary required = GetRequiredFunctionParamters(edmOperation, context.Action); + segments.Add(new FunctionSegmentTemplate(required, (IEdmFunction)edmOperation, targetEntitySet)); + httpMethod = "Get"; + } - IDictionary requiredParameters = new Dictionary(); - // we can allow the action has other parameters except the function parameters. - foreach (var parameter in operation.Parameters.Skip(1)) + ODataPathTemplate template = new ODataPathTemplate(segments); + context.Action.AddSelector(httpMethod, context.Prefix, context.Model, template, context.Options?.RouteOptions); + } + + private static IDictionary GetRequiredFunctionParamters(IEdmOperation operation, ActionModel action) + { + Contract.Assert(operation != null); + Contract.Assert(operation.IsFunction()); + Contract.Assert(action != null); + + IDictionary requiredParameters = new Dictionary(); + // we can allow the action has other parameters except the function parameters. + foreach (var parameter in operation.Parameters.Skip(1)) + { + if (action.Parameters.Any(p => p.ParameterInfo.Name == parameter.Name)) { - if (action.Parameters.Any(p => p.ParameterInfo.Name == parameter.Name)) - { - requiredParameters[parameter.Name] = $"{{{parameter.Name}}}"; - } + requiredParameters[parameter.Name] = $"{{{parameter.Name}}}"; } - - return requiredParameters; } + + return requiredParameters; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/PropertyRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/PropertyRoutingConvention.cs index 7c15b37a9..40ee04651 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/PropertyRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/PropertyRoutingConvention.cs @@ -15,356 +15,355 @@ using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// The convention for the property access. +/// Get|PostTo|PutTo|PatchTo|DeleteTo ~/entityset/key/property +/// GET|PostTo|PutTo|PatchTo|DeleteTo ~/singleton/property +/// Get|PostTo|PutTo|PatchTo|DeleteTo ~/entityset/key/cast/property/ +/// GET|PostTo|PutTo|PatchTo|DeleteTo ~/singleton/cast/property +/// Get|PostTo|PutTo|PatchTo|DeleteTo ~/entityset/key/property/cast +/// GET|PostTo|PutTo|PatchTo|DeleteTo ~/singleton/property/cast +/// Get|PostTo|PutTo|PatchTo|DeleteTo ~/entityset/key/cast/property/cast +/// GET|PostTo|PutTo|PatchTo|DeleteTo ~/singleton/cast/property/cast +/// GET ~/entityset/key/property/$value +/// GET ~/entityset/key/cast/property/$value +/// GET ~/singleton/property/$value +/// GET ~/singleton/cast/property/$value +/// GET ~/entityset/key/property/$count +/// GET ~/entityset/key/cast/property/$count +/// GET ~/singleton/property/$count +/// GET ~/singleton/cast/property/$count +/// +public class PropertyRoutingConvention : IODataControllerActionConvention { - /// - /// The convention for the property access. - /// Get|PostTo|PutTo|PatchTo|DeleteTo ~/entityset/key/property - /// GET|PostTo|PutTo|PatchTo|DeleteTo ~/singleton/property - /// Get|PostTo|PutTo|PatchTo|DeleteTo ~/entityset/key/cast/property/ - /// GET|PostTo|PutTo|PatchTo|DeleteTo ~/singleton/cast/property - /// Get|PostTo|PutTo|PatchTo|DeleteTo ~/entityset/key/property/cast - /// GET|PostTo|PutTo|PatchTo|DeleteTo ~/singleton/property/cast - /// Get|PostTo|PutTo|PatchTo|DeleteTo ~/entityset/key/cast/property/cast - /// GET|PostTo|PutTo|PatchTo|DeleteTo ~/singleton/cast/property/cast - /// GET ~/entityset/key/property/$value - /// GET ~/entityset/key/cast/property/$value - /// GET ~/singleton/property/$value - /// GET ~/singleton/cast/property/$value - /// GET ~/entityset/key/property/$count - /// GET ~/entityset/key/cast/property/$count - /// GET ~/singleton/property/$count - /// GET ~/singleton/cast/property/$count - /// - public class PropertyRoutingConvention : IODataControllerActionConvention + /// + public virtual int Order => 400; + + /// + public virtual bool AppliesToController(ODataControllerActionContext context) { - /// - public virtual int Order => 400; + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - /// - public virtual bool AppliesToController(ODataControllerActionContext context) + // structural property supports for entity set and singleton + return context.NavigationSource != null; + } + + /// + public virtual bool AppliesToAction(ODataControllerActionContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - // structural property supports for entity set and singleton - return context.NavigationSource != null; + IEdmNavigationSource navigationSource = context.NavigationSource; + if (navigationSource == null) + { + return false; } - /// - public virtual bool AppliesToAction(ODataControllerActionContext context) + ActionModel action = context.Action; + string actionName = action.ActionName; + + string method = SplitActionName(actionName, out string property, out string cast, out string declared); + if (method == null || string.IsNullOrEmpty(property)) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + return false; + } - IEdmNavigationSource navigationSource = context.NavigationSource; - if (navigationSource == null) + // filter by action parameter + IEdmEntityType entityType = navigationSource.EntityType; + bool hasKeyParameter = action.HasODataKeyParameter(entityType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false); + if (!(context.Singleton != null ^ hasKeyParameter)) + { + // Singleton, doesn't allow to query property with key + // entityset, doesn't allow for non-key to query property + return false; + } + + // Find the declaring type of the property if we have the declaring type name in the action name. + // otherwise, it means the property is defined on the entity type of the navigation source. + IEdmEntityType declaringEntityType = entityType; + if (declared != null) + { + if (declared.Length == 0) { + // Early return for the following cases: + // - Get|PostTo|PutTo|PatchTo|DeleteTo{PropertyName}From + // - Get|PostTo|PutTo|PatchTo|DeleteTo{PropertyName}Of{Cast}From return false; } - ActionModel action = context.Action; - string actionName = action.ActionName; - - string method = SplitActionName(actionName, out string property, out string cast, out string declared); - if (method == null || string.IsNullOrEmpty(property)) + declaringEntityType = entityType.FindTypeInInheritance(context.Model, declared) as IEdmEntityType; + if (declaringEntityType == null) { return false; } + } + + bool enablePropertyNameCaseInsensitive = context?.Options?.RouteOptions.EnablePropertyNameCaseInsensitive ?? false; + IEdmProperty edmProperty = declaringEntityType.FindProperty(property, enablePropertyNameCaseInsensitive); + if (edmProperty == null || edmProperty.PropertyKind != EdmPropertyKind.Structural) + { + return false; + } - // filter by action parameter - IEdmEntityType entityType = navigationSource.EntityType; - bool hasKeyParameter = action.HasODataKeyParameter(entityType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false); - if (!(context.Singleton != null ^ hasKeyParameter)) + if (!CanApply(edmProperty, method, context.Options?.RouteOptions)) + { + return false; + } + + IEdmComplexType castType; + // Only process structural property + IEdmStructuredType castComplexType = null; + if (cast != null) + { + if (cast.Length == 0) { - // Singleton, doesn't allow to query property with key - // entityset, doesn't allow for non-key to query property + // Avoid unnecessary call to FindTypeInheritance + // Cases handled: Get|PostTo|PutTo|PatchTo|DeleteTo{PropertyName}Of return false; } - // Find the declaring type of the property if we have the declaring type name in the action name. - // otherwise, it means the property is defined on the entity type of the navigation source. - IEdmEntityType declaringEntityType = entityType; - if (declared != null) + IEdmType propertyElementType = edmProperty.Type.Definition.AsElementType(); + if (propertyElementType.TypeKind == EdmTypeKind.Complex) { - if (declared.Length == 0) - { - // Early return for the following cases: - // - Get|PostTo|PutTo|PatchTo|DeleteTo{PropertyName}From - // - Get|PostTo|PutTo|PatchTo|DeleteTo{PropertyName}Of{Cast}From - return false; - } - - declaringEntityType = entityType.FindTypeInInheritance(context.Model, declared) as IEdmEntityType; - if (declaringEntityType == null) + IEdmComplexType complexType = (IEdmComplexType)propertyElementType; + castType = complexType.FindTypeInInheritance(context.Model, cast) as IEdmComplexType; + if (castType == null) { return false; } } - - bool enablePropertyNameCaseInsensitive = context?.Options?.RouteOptions.EnablePropertyNameCaseInsensitive ?? false; - IEdmProperty edmProperty = declaringEntityType.FindProperty(property, enablePropertyNameCaseInsensitive); - if (edmProperty == null || edmProperty.PropertyKind != EdmPropertyKind.Structural) + else { + // only support complex type cast, (TODO: maybe consider to support Edm.PrimitiveType cast) return false; } - if (!CanApply(edmProperty, method, context.Options?.RouteOptions)) + IEdmTypeReference propertyType = edmProperty.Type; + if (propertyType.IsCollection()) { - return false; + propertyType = propertyType.AsCollection().ElementType(); } - IEdmComplexType castType; - // Only process structural property - IEdmStructuredType castComplexType = null; - if (cast != null) + if (!propertyType.IsComplex()) { - if (cast.Length == 0) - { - // Avoid unnecessary call to FindTypeInheritance - // Cases handled: Get|PostTo|PutTo|PatchTo|DeleteTo{PropertyName}Of - return false; - } - - IEdmType propertyElementType = edmProperty.Type.Definition.AsElementType(); - if (propertyElementType.TypeKind == EdmTypeKind.Complex) - { - IEdmComplexType complexType = (IEdmComplexType)propertyElementType; - castType = complexType.FindTypeInInheritance(context.Model, cast) as IEdmComplexType; - if (castType == null) - { - return false; - } - } - else - { - // only support complex type cast, (TODO: maybe consider to support Edm.PrimitiveType cast) - return false; - } - - IEdmTypeReference propertyType = edmProperty.Type; - if (propertyType.IsCollection()) - { - propertyType = propertyType.AsCollection().ElementType(); - } - - if (!propertyType.IsComplex()) - { - return false; - } - - castComplexType = propertyType.ToStructuredType().FindTypeInInheritance(context.Model, cast); - if (castComplexType == null) - { - return false; - } + return false; } - AddSelector(method, context, action, navigationSource, (IEdmStructuralProperty)edmProperty, castComplexType, declaringEntityType, false, false); - - if (CanApplyDollarCount(edmProperty, method, context.Options?.RouteOptions)) + castComplexType = propertyType.ToStructuredType().FindTypeInInheritance(context.Model, cast); + if (castComplexType == null) { - AddSelector(method, context, action, navigationSource, (IEdmStructuralProperty)edmProperty, castComplexType, declaringEntityType, false, true); + return false; } + } - if (CanApplyDollarValue(edmProperty, method, context.Options?.RouteOptions)) - { - AddSelector(method, context, action, navigationSource, (IEdmStructuralProperty)edmProperty, castComplexType, declaringEntityType, true, false); - } + AddSelector(method, context, action, navigationSource, (IEdmStructuralProperty)edmProperty, castComplexType, declaringEntityType, false, false); - return true; + if (CanApplyDollarCount(edmProperty, method, context.Options?.RouteOptions)) + { + AddSelector(method, context, action, navigationSource, (IEdmStructuralProperty)edmProperty, castComplexType, declaringEntityType, false, true); } - private static void AddSelector(string httpMethod, ODataControllerActionContext context, ActionModel action, - IEdmNavigationSource navigationSource, - IEdmStructuralProperty edmProperty, - IEdmType cast, IEdmEntityType declaringType, bool dollarValue, bool dollarCount) + if (CanApplyDollarValue(edmProperty, method, context.Options?.RouteOptions)) { - IEdmEntitySet entitySet = navigationSource as IEdmEntitySet; - IEdmEntityType entityType = navigationSource.EntityType; + AddSelector(method, context, action, navigationSource, (IEdmStructuralProperty)edmProperty, castComplexType, declaringEntityType, true, false); + } - IList segments = new List(); - if (entitySet != null) - { - segments.Add(new EntitySetSegmentTemplate(entitySet)); - segments.Add(KeySegmentTemplate.CreateKeySegment(entityType, navigationSource)); - } - else - { - segments.Add(new SingletonSegmentTemplate(navigationSource as IEdmSingleton)); - } + return true; + } - if (declaringType != null && declaringType != entityType) - { - segments.Add(new CastSegmentTemplate(declaringType, entityType, navigationSource)); - } + private static void AddSelector(string httpMethod, ODataControllerActionContext context, ActionModel action, + IEdmNavigationSource navigationSource, + IEdmStructuralProperty edmProperty, + IEdmType cast, IEdmEntityType declaringType, bool dollarValue, bool dollarCount) + { + IEdmEntitySet entitySet = navigationSource as IEdmEntitySet; + IEdmEntityType entityType = navigationSource.EntityType; - segments.Add(new PropertySegmentTemplate(edmProperty)); + IList segments = new List(); + if (entitySet != null) + { + segments.Add(new EntitySetSegmentTemplate(entitySet)); + segments.Add(KeySegmentTemplate.CreateKeySegment(entityType, navigationSource)); + } + else + { + segments.Add(new SingletonSegmentTemplate(navigationSource as IEdmSingleton)); + } - if (cast != null) - { - if (edmProperty.Type.IsCollection()) - { - cast = new EdmCollectionType(cast.ToEdmTypeReference(edmProperty.Type.IsNullable)); - } + if (declaringType != null && declaringType != entityType) + { + segments.Add(new CastSegmentTemplate(declaringType, entityType, navigationSource)); + } - // TODO: maybe create the collection type for the collection???? - segments.Add(new CastSegmentTemplate(cast, edmProperty.Type.Definition, navigationSource)); - } + segments.Add(new PropertySegmentTemplate(edmProperty)); - if (dollarValue) + if (cast != null) + { + if (edmProperty.Type.IsCollection()) { - segments.Add(new ValueSegmentTemplate(edmProperty.Type.Definition)); + cast = new EdmCollectionType(cast.ToEdmTypeReference(edmProperty.Type.IsNullable)); } - if (dollarCount) - { - segments.Add(CountSegmentTemplate.Instance); - } + // TODO: maybe create the collection type for the collection???? + segments.Add(new CastSegmentTemplate(cast, edmProperty.Type.Definition, navigationSource)); + } - ODataPathTemplate template = new ODataPathTemplate(segments); - action.AddSelector(httpMethod.NormalizeHttpMethod(), context.Prefix, context.Model, template, context.Options?.RouteOptions); + if (dollarValue) + { + segments.Add(new ValueSegmentTemplate(edmProperty.Type.Definition)); } - // Split the property such as "GetCityOfSubAddressFromVipCustomer" - private static string SplitActionName(string actionName, out string property, out string cast, out string declared) + if (dollarCount) { - string method = null; - string text = ""; - // Get{PropertyName}OfFrom: GetCityOfSubAddressFromVipCustomer - foreach (var prefix in new[] { "Get", "PostTo", "PutTo", "PatchTo", "DeleteTo" }) - { - if (actionName.StartsWith(prefix, StringComparison.Ordinal)) - { - method = prefix; - text = actionName.Substring(prefix.Length); - break; - } - } + segments.Add(CountSegmentTemplate.Instance); + } - property = null; - cast = null; - declared = null; - if (method == null) - { - return null; - } + ODataPathTemplate template = new ODataPathTemplate(segments); + action.AddSelector(httpMethod.NormalizeHttpMethod(), context.Prefix, context.Model, template, context.Options?.RouteOptions); + } - int index = text.IndexOf("Of", StringComparison.Ordinal); - if (index > 0) - { - property = text.Substring(0, index); - text = text.Substring(index + 2); - cast = Match(text, out declared); - } - else + // Split the property such as "GetCityOfSubAddressFromVipCustomer" + private static string SplitActionName(string actionName, out string property, out string cast, out string declared) + { + string method = null; + string text = ""; + // Get{PropertyName}OfFrom: GetCityOfSubAddressFromVipCustomer + foreach (var prefix in new[] { "Get", "PostTo", "PutTo", "PatchTo", "DeleteTo" }) + { + if (actionName.StartsWith(prefix, StringComparison.Ordinal)) { - property = Match(text, out declared); + method = prefix; + text = actionName.Substring(prefix.Length); + break; } - - return method; } - private static string Match(string text, out string declared) + property = null; + cast = null; + declared = null; + if (method == null) { - declared = null; - int index = text.IndexOf("From", StringComparison.Ordinal); - if (index > 0) - { - declared = text.Substring(index + 4); - return text.Substring(0, index); - } + return null; + } - return text; + int index = text.IndexOf("Of", StringComparison.Ordinal); + if (index > 0) + { + property = text.Substring(0, index); + text = text.Substring(index + 2); + cast = Match(text, out declared); } + else + { + property = Match(text, out declared); + } + + return method; + } - /// - /// Tests whether to apply routings for the given property. - /// - /// The property to test. - /// The http method. - /// The route options. - /// True/false to identify whether to apply routings for the given property. - protected virtual bool CanApply(IEdmProperty edmProperty, string method, ODataRouteOptions routeOptions) + private static string Match(string text, out string declared) + { + declared = null; + int index = text.IndexOf("From", StringComparison.Ordinal); + if (index > 0) { - if (edmProperty == null) - { - throw Error.ArgumentNull(nameof(edmProperty)); - } + declared = text.Substring(index + 4); + return text.Substring(0, index); + } - bool isCollection = edmProperty.Type.IsCollection(); + return text; + } - // OData Spec: PATCH is not supported for collection properties. - if (isCollection && method == "PatchTo") - { - return false; - } + /// + /// Tests whether to apply routings for the given property. + /// + /// The property to test. + /// The http method. + /// The route options. + /// True/false to identify whether to apply routings for the given property. + protected virtual bool CanApply(IEdmProperty edmProperty, string method, ODataRouteOptions routeOptions) + { + if (edmProperty == null) + { + throw Error.ArgumentNull(nameof(edmProperty)); + } - // Allow post only to collection properties - if (!isCollection && method == "PostTo") - { - return false; - } + bool isCollection = edmProperty.Type.IsCollection(); - // OData spec: A successful DELETE request to the edit URL for a structural property, ... sets the property to null. - // The request body is ignored and should be empty. - // DELETE request to a non-nullable value MUST fail and the service respond with 400 Bad Request or other appropriate error. - if (!edmProperty.Type.IsNullable && method == "DeleteTo") - { - return false; - } + // OData Spec: PATCH is not supported for collection properties. + if (isCollection && method == "PatchTo") + { + return false; + } - return true; + // Allow post only to collection properties + if (!isCollection && method == "PostTo") + { + return false; } - /// - /// OData spec: To retrieve the raw value of a primitive type property, the client sends a GET request to the property value URL. - /// So, let's apply $value for the "Get" and non-collection primitive property - /// - /// The property to test. - /// The http method. - /// The route options. - /// True/false to identify whether to apply $value. - protected virtual bool CanApplyDollarValue(IEdmProperty edmProperty, string method, ODataRouteOptions routeOptions) + // OData spec: A successful DELETE request to the edit URL for a structural property, ... sets the property to null. + // The request body is ignored and should be empty. + // DELETE request to a non-nullable value MUST fail and the service respond with 400 Bad Request or other appropriate error. + if (!edmProperty.Type.IsNullable && method == "DeleteTo") { - if (edmProperty == null) - { - throw Error.ArgumentNull(nameof(edmProperty)); - } + return false; + } - if (routeOptions != null && !routeOptions.EnableDollarValueRouting) - { - return false; - } + return true; + } - return method == "Get" && !edmProperty.Type.IsCollection() && (edmProperty.Type.IsPrimitive() || edmProperty.Type.IsEnum()); + /// + /// OData spec: To retrieve the raw value of a primitive type property, the client sends a GET request to the property value URL. + /// So, let's apply $value for the "Get" and non-collection primitive property + /// + /// The property to test. + /// The http method. + /// The route options. + /// True/false to identify whether to apply $value. + protected virtual bool CanApplyDollarValue(IEdmProperty edmProperty, string method, ODataRouteOptions routeOptions) + { + if (edmProperty == null) + { + throw Error.ArgumentNull(nameof(edmProperty)); } - /// - /// OData spec: To request only the number of items of a collection of entities or items of a collection-valued property, - /// the client issues a GET request with /$count appended to the resource path of the collection. - /// - /// The property to test. - /// The http method. - /// The route options. - /// True/false to identify whether to apply $count. - protected virtual bool CanApplyDollarCount(IEdmProperty edmProperty, string method, ODataRouteOptions routeOptions) + if (routeOptions != null && !routeOptions.EnableDollarValueRouting) { - if (edmProperty == null) - { - throw Error.ArgumentNull(nameof(edmProperty)); - } + return false; + } - if (routeOptions != null && !routeOptions.EnableDollarCountRouting) - { - return false; - } + return method == "Get" && !edmProperty.Type.IsCollection() && (edmProperty.Type.IsPrimitive() || edmProperty.Type.IsEnum()); + } - return method == "Get" && edmProperty.Type.IsCollection(); + /// + /// OData spec: To request only the number of items of a collection of entities or items of a collection-valued property, + /// the client issues a GET request with /$count appended to the resource path of the collection. + /// + /// The property to test. + /// The http method. + /// The route options. + /// True/false to identify whether to apply $count. + protected virtual bool CanApplyDollarCount(IEdmProperty edmProperty, string method, ODataRouteOptions routeOptions) + { + if (edmProperty == null) + { + throw Error.ArgumentNull(nameof(edmProperty)); } + + if (routeOptions != null && !routeOptions.EnableDollarCountRouting) + { + return false; + } + + return method == "Get" && edmProperty.Type.IsCollection(); } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/RefRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/RefRoutingConvention.cs index 3bb048202..a42694d20 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/RefRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/RefRoutingConvention.cs @@ -15,267 +15,266 @@ using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// An implementation of that handles entity reference manipulations. +/// Conventions: +/// GET|DELETE ~/entityset/key/navigationproperty/$ref +/// GET|POST|PUT|DELETE ~/entityset/key/navigationproperty/key/$ref +/// GET|POST|PUT|DELETE ~/entityset/key/cast/navigationproperty/key/$ref +/// +public class RefRoutingConvention : IODataControllerActionConvention { - /// - /// An implementation of that handles entity reference manipulations. - /// Conventions: - /// GET|DELETE ~/entityset/key/navigationproperty/$ref - /// GET|POST|PUT|DELETE ~/entityset/key/navigationproperty/key/$ref - /// GET|POST|PUT|DELETE ~/entityset/key/cast/navigationproperty/key/$ref - /// - public class RefRoutingConvention : IODataControllerActionConvention - { - /// - public virtual int Order => 1000; + /// + public virtual int Order => 1000; - /// - public virtual bool AppliesToController(ODataControllerActionContext context) + /// + public virtual bool AppliesToController(ODataControllerActionContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - // $ref supports for entity set and singleton - return context.NavigationSource != null; + throw Error.ArgumentNull(nameof(context)); } - /// - public bool AppliesToAction(ODataControllerActionContext context) + // $ref supports for entity set and singleton + return context.NavigationSource != null; + } + + /// + public bool AppliesToAction(ODataControllerActionContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - Debug.Assert(context.Action != null); + Debug.Assert(context.Action != null); - ActionModel action = context.Action; - string actionMethodName = action.ActionName; + ActionModel action = context.Action; + string actionMethodName = action.ActionName; - // Need to refactor the following - // for example: CreateRef( with the navigation property parameter) should for all navigation properties - // CreateRefToOrdersFromCustomer, CreateRefToOrders, CreateRef. - string method = SplitRefActionName(actionMethodName, out string httpMethod, out string property, out string declaring); - if (method == null || (property != null && property.Length == 0)) - { - // Early return for the following cases: Get|Create|DeleteRefTo - return false; - } + // Need to refactor the following + // for example: CreateRef( with the navigation property parameter) should for all navigation properties + // CreateRefToOrdersFromCustomer, CreateRefToOrders, CreateRef. + string method = SplitRefActionName(actionMethodName, out string httpMethod, out string property, out string declaring); + if (method == null || (property != null && property.Length == 0)) + { + // Early return for the following cases: Get|Create|DeleteRefTo + return false; + } - IEdmNavigationSource navigationSource = context.NavigationSource; - IEdmEntityType entityType = context.EntityType; + IEdmNavigationSource navigationSource = context.NavigationSource; + IEdmEntityType entityType = context.EntityType; - // For entity set, we should have the key parameter - // For Singleton, we should not have the key parameter - bool hasODataKeyParameter = action.HasODataKeyParameter(entityType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false); - if ((context.EntitySet != null && !hasODataKeyParameter) || - (context.Singleton != null && hasODataKeyParameter)) - { - return false; - } + // For entity set, we should have the key parameter + // For Singleton, we should not have the key parameter + bool hasODataKeyParameter = action.HasODataKeyParameter(entityType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false); + if ((context.EntitySet != null && !hasODataKeyParameter) || + (context.Singleton != null && hasODataKeyParameter)) + { + return false; + } - // Find the navigation property declaring type - IEdmStructuredType declaringType = entityType; - if (declaring != null) + // Find the navigation property declaring type + IEdmStructuredType declaringType = entityType; + if (declaring != null) + { + if (declaring.Length == 0) { - if (declaring.Length == 0) - { - // Early return for the following cases: Get|Create|DeleteRefTo{NavigationProperty}From - return false; - } - - declaringType = entityType.FindTypeInInheritance(context.Model, declaring); - if (declaringType == null) - { - return false; - } + // Early return for the following cases: Get|Create|DeleteRefTo{NavigationProperty}From + return false; } - // Process the generic scenario - if (property == null) + declaringType = entityType.FindTypeInInheritance(context.Model, declaring); + if (declaringType == null) { - return ProcessNonNavigationProperty(httpMethod, context, action, navigationSource, entityType, declaringType); + return false; } + } - // Find the navigation property if have - IEdmNavigationProperty navigationProperty = null; - navigationProperty = declaringType.DeclaredNavigationProperties().FirstOrDefault(p => p.Name.Equals(property, StringComparison.Ordinal)); - - if (navigationProperty is null && (context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false)) - { - IEdmNavigationProperty[] navigationProperties = declaringType.DeclaredNavigationProperties().Where(p => p.Name.Equals(property, StringComparison.OrdinalIgnoreCase)).ToArray(); - if (navigationProperties.Length > 1) - { - throw Error.InvalidOperation(SRResources.UnableToIdentifyUniqueProperty, property); - } - - navigationProperty = navigationProperties.FirstOrDefault(); - } + // Process the generic scenario + if (property == null) + { + return ProcessNonNavigationProperty(httpMethod, context, action, navigationSource, entityType, declaringType); + } - if (navigationProperty == null) - { - return false; - } + // Find the navigation property if have + IEdmNavigationProperty navigationProperty = null; + navigationProperty = declaringType.DeclaredNavigationProperties().FirstOrDefault(p => p.Name.Equals(property, StringComparison.Ordinal)); - IList segments = new List(); - if (context.EntitySet != null) - { - segments.Add(new EntitySetSegmentTemplate(context.EntitySet)); - segments.Add(KeySegmentTemplate.CreateKeySegment(entityType, context.EntitySet)); - } - else + if (navigationProperty is null && (context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false)) + { + IEdmNavigationProperty[] navigationProperties = declaringType.DeclaredNavigationProperties().Where(p => p.Name.Equals(property, StringComparison.OrdinalIgnoreCase)).ToArray(); + if (navigationProperties.Length > 1) { - segments.Add(new SingletonSegmentTemplate(context.Singleton)); + throw Error.InvalidOperation(SRResources.UnableToIdentifyUniqueProperty, property); } - if (entityType != declaringType) - { - segments.Add(new CastSegmentTemplate(declaringType, entityType, navigationSource)); - } + navigationProperty = navigationProperties.FirstOrDefault(); + } - IEdmNavigationSource targetNavigationSource = navigationSource.FindNavigationTarget(navigationProperty, segments, out _); - NavigationLinkSegmentTemplate linkTemplate = new NavigationLinkSegmentTemplate(navigationProperty, targetNavigationSource); + if (navigationProperty == null) + { + return false; + } - IEdmEntityType navigationPropertyType = navigationProperty.Type.GetElementTypeOrSelf().AsEntity().EntityDefinition(); - bool hasNavigationPropertyKeyParameter = action.HasODataKeyParameter(navigationPropertyType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false, "relatedKey"); - if (hasNavigationPropertyKeyParameter) - { - linkTemplate.Key = KeySegmentTemplate.CreateKeySegment(navigationPropertyType, targetNavigationSource, "relatedKey"); - } - else - { - hasNavigationPropertyKeyParameter = action.HasODataKeyParameter(navigationPropertyType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false, "relatedId"); - if (hasNavigationPropertyKeyParameter) - { - linkTemplate.Key = KeySegmentTemplate.CreateKeySegment(navigationPropertyType, targetNavigationSource, "relatedId"); - } - } + IList segments = new List(); + if (context.EntitySet != null) + { + segments.Add(new EntitySetSegmentTemplate(context.EntitySet)); + segments.Add(KeySegmentTemplate.CreateKeySegment(entityType, context.EntitySet)); + } + else + { + segments.Add(new SingletonSegmentTemplate(context.Singleton)); + } - segments.Add(linkTemplate); + if (entityType != declaringType) + { + segments.Add(new CastSegmentTemplate(declaringType, entityType, navigationSource)); + } - ODataPathTemplate template = new ODataPathTemplate(segments); - action.AddSelector(httpMethod, context.Prefix, context.Model, template, context.Options?.RouteOptions); + IEdmNavigationSource targetNavigationSource = navigationSource.FindNavigationTarget(navigationProperty, segments, out _); + NavigationLinkSegmentTemplate linkTemplate = new NavigationLinkSegmentTemplate(navigationProperty, targetNavigationSource); - // processed - return true; + IEdmEntityType navigationPropertyType = navigationProperty.Type.GetElementTypeOrSelf().AsEntity().EntityDefinition(); + bool hasNavigationPropertyKeyParameter = action.HasODataKeyParameter(navigationPropertyType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false, "relatedKey"); + if (hasNavigationPropertyKeyParameter) + { + linkTemplate.Key = KeySegmentTemplate.CreateKeySegment(navigationPropertyType, targetNavigationSource, "relatedKey"); } - - internal static bool ProcessNonNavigationProperty(string httpMethod, ODataControllerActionContext context, - ActionModel action, - IEdmNavigationSource navigationSource, - IEdmEntityType entityType, IEdmStructuredType castType) + else { - // Action parameter should have a (string navigationProperty) parameter - if (!action.HasParameter("navigationProperty")) + hasNavigationPropertyKeyParameter = action.HasODataKeyParameter(navigationPropertyType, context.Options?.RouteOptions?.EnablePropertyNameCaseInsensitive ?? false, "relatedId"); + if (hasNavigationPropertyKeyParameter) { - return false; + linkTemplate.Key = KeySegmentTemplate.CreateKeySegment(navigationPropertyType, targetNavigationSource, "relatedId"); } + } - // Let's only handle single-key convention, for composite key, use attribute routing or non-generic navigation. - bool hasRelatedKey = action.Parameters.Any(p => p.Name == "relatedKey"); // case sensitive? - bool hasRelatedId = action.Parameters.Any(p => p.Name == "relatedId"); + segments.Add(linkTemplate); - IList segments = new List(); - if (context.EntitySet != null) - { - segments.Add(new EntitySetSegmentTemplate(context.EntitySet)); - segments.Add(KeySegmentTemplate.CreateKeySegment(entityType, context.EntitySet)); - } - else - { - segments.Add(new SingletonSegmentTemplate(context.Singleton)); - } + ODataPathTemplate template = new ODataPathTemplate(segments); + action.AddSelector(httpMethod, context.Prefix, context.Model, template, context.Options?.RouteOptions); - if (entityType != castType) - { - segments.Add(new CastSegmentTemplate(castType, entityType, navigationSource)); - } + // processed + return true; + } - if (hasRelatedKey) - { - segments.Add(new NavigationLinkTemplateSegmentTemplate(entityType, navigationSource) - { - RelatedKey = "relatedKey" - }); - } - else if (hasRelatedId) - { - segments.Add(new NavigationLinkTemplateSegmentTemplate(entityType, navigationSource) - { - RelatedKey = "relatedId" - }); - } - else - { - segments.Add(new NavigationLinkTemplateSegmentTemplate(entityType, navigationSource)); - } + internal static bool ProcessNonNavigationProperty(string httpMethod, ODataControllerActionContext context, + ActionModel action, + IEdmNavigationSource navigationSource, + IEdmEntityType entityType, IEdmStructuredType castType) + { + // Action parameter should have a (string navigationProperty) parameter + if (!action.HasParameter("navigationProperty")) + { + return false; + } - ODataPathTemplate template = new ODataPathTemplate(segments); - action.AddSelector(httpMethod, context.Prefix, context.Model, template, context.Options?.RouteOptions); + // Let's only handle single-key convention, for composite key, use attribute routing or non-generic navigation. + bool hasRelatedKey = action.Parameters.Any(p => p.Name == "relatedKey"); // case sensitive? + bool hasRelatedId = action.Parameters.Any(p => p.Name == "relatedId"); - return true; + IList segments = new List(); + if (context.EntitySet != null) + { + segments.Add(new EntitySetSegmentTemplate(context.EntitySet)); + segments.Add(KeySegmentTemplate.CreateKeySegment(entityType, context.EntitySet)); + } + else + { + segments.Add(new SingletonSegmentTemplate(context.Singleton)); } - internal static string SplitRefActionName(string actionName, out string httpMethod, out string property, out string declaring) + if (entityType != castType) { - string method; - httpMethod = null; - property = null; - declaring = null; - string remaining; - - // CreateRefToOrdersFromCustomer, CreateRefToOrders, CreateRef. - if (actionName.StartsWith("CreateRef", StringComparison.Ordinal)) - { - method = "CreateRef"; - httpMethod = "Post,Put"; - remaining = actionName.Substring(9); - } - else if (actionName.StartsWith("GetRef", StringComparison.Ordinal)) - { - method = "GetRef"; - httpMethod = "Get"; - remaining = actionName.Substring(6); - } - else if (actionName.StartsWith("DeleteRef", StringComparison.Ordinal)) + segments.Add(new CastSegmentTemplate(castType, entityType, navigationSource)); + } + + if (hasRelatedKey) + { + segments.Add(new NavigationLinkTemplateSegmentTemplate(entityType, navigationSource) { - method = "DeleteRef"; - httpMethod = "Delete"; - remaining = actionName.Substring(9); - } - else + RelatedKey = "relatedKey" + }); + } + else if (hasRelatedId) + { + segments.Add(new NavigationLinkTemplateSegmentTemplate(entityType, navigationSource) { - return null; - } + RelatedKey = "relatedId" + }); + } + else + { + segments.Add(new NavigationLinkTemplateSegmentTemplate(entityType, navigationSource)); + } - if (string.IsNullOrEmpty(remaining)) - { - return method; - } + ODataPathTemplate template = new ODataPathTemplate(segments); + action.AddSelector(httpMethod, context.Prefix, context.Model, template, context.Options?.RouteOptions); - if (remaining.StartsWith("To", StringComparison.OrdinalIgnoreCase)) - { - remaining = remaining.Substring(2); - } - else - { - return null; - } + return true; + } - int index = remaining.IndexOf("From", StringComparison.OrdinalIgnoreCase); - if (index > 0) - { - property = remaining.Substring(0, index); - declaring = remaining.Substring(index + 4); - } - else - { - property = remaining; - } + internal static string SplitRefActionName(string actionName, out string httpMethod, out string property, out string declaring) + { + string method; + httpMethod = null; + property = null; + declaring = null; + string remaining; + + // CreateRefToOrdersFromCustomer, CreateRefToOrders, CreateRef. + if (actionName.StartsWith("CreateRef", StringComparison.Ordinal)) + { + method = "CreateRef"; + httpMethod = "Post,Put"; + remaining = actionName.Substring(9); + } + else if (actionName.StartsWith("GetRef", StringComparison.Ordinal)) + { + method = "GetRef"; + httpMethod = "Get"; + remaining = actionName.Substring(6); + } + else if (actionName.StartsWith("DeleteRef", StringComparison.Ordinal)) + { + method = "DeleteRef"; + httpMethod = "Delete"; + remaining = actionName.Substring(9); + } + else + { + return null; + } + if (string.IsNullOrEmpty(remaining)) + { return method; } + + if (remaining.StartsWith("To", StringComparison.OrdinalIgnoreCase)) + { + remaining = remaining.Substring(2); + } + else + { + return null; + } + + int index = remaining.IndexOf("From", StringComparison.OrdinalIgnoreCase); + if (index > 0) + { + property = remaining.Substring(0, index); + declaring = remaining.Substring(index + 4); + } + else + { + property = remaining; + } + + return method; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Conventions/SingletonRoutingConvention.cs b/src/Microsoft.AspNetCore.OData/Routing/Conventions/SingletonRoutingConvention.cs index bc8d4602d..03bd6f407 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Conventions/SingletonRoutingConvention.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Conventions/SingletonRoutingConvention.cs @@ -13,108 +13,107 @@ using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Routing.Conventions; + +/// +/// The convention for . +/// The Conventions: +/// Get|Put|Patch ~/singleton +/// Get|Put|Patch ~/singleton/cast +/// +public class SingletonRoutingConvention : IODataControllerActionConvention { - /// - /// The convention for . - /// The Conventions: - /// Get|Put|Patch ~/singleton - /// Get|Put|Patch ~/singleton/cast - /// - public class SingletonRoutingConvention : IODataControllerActionConvention + /// + public virtual int Order => 200; + + /// + public virtual bool AppliesToController(ODataControllerActionContext context) { - /// - public virtual int Order => 200; + return context?.Singleton != null; + } - /// - public virtual bool AppliesToController(ODataControllerActionContext context) + /// + public bool AppliesToAction(ODataControllerActionContext context) + { + if (context == null) { - return context?.Singleton != null; + throw Error.ArgumentNull(nameof(context)); } - /// - public bool AppliesToAction(ODataControllerActionContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + Debug.Assert(context.Singleton != null); + Debug.Assert(context.Action != null); - Debug.Assert(context.Singleton != null); - Debug.Assert(context.Action != null); + ActionModel action = context.Action; + string singletonName = context.Singleton.Name; - ActionModel action = context.Action; - string singletonName = context.Singleton.Name; - - string actionMethodName = action.ActionName; - if (IsSupportedActionName(context, actionMethodName, singletonName, out string httpMethod)) - { - // ~/Me - ODataPathTemplate template = new ODataPathTemplate(new SingletonSegmentTemplate(context.Singleton)); - action.AddSelector(httpMethod, context.Prefix, context.Model, template, context.Options?.RouteOptions); - - // processed - return true; - } - - // type cast - // GetFrom{EntityTypeName} or Get{SingletonName}From{EntityTypeName} - int index = actionMethodName.IndexOf("From", StringComparison.Ordinal); - if (index == -1) - { - return false; - } + string actionMethodName = action.ActionName; + if (IsSupportedActionName(context, actionMethodName, singletonName, out string httpMethod)) + { + // ~/Me + ODataPathTemplate template = new ODataPathTemplate(new SingletonSegmentTemplate(context.Singleton)); + action.AddSelector(httpMethod, context.Prefix, context.Model, template, context.Options?.RouteOptions); - string actionPrefix = actionMethodName.Substring(0, index); - if (IsSupportedActionName(context, actionPrefix, singletonName, out httpMethod)) - { - string castTypeName = actionMethodName.Substring(index + 4); - if (castTypeName.Length == 0) - { - // Early return for the following cases: Get|Put|PatchFrom - return false; - } - - IEdmEntityType entityType = context.Singleton.EntityType; - - // Shall we cast to base type and the type itself? I think yes. - IEdmStructuredType castType = entityType.FindTypeInInheritance(context.Model, castTypeName, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true); - if (castType != null) - { - // ~/Me/Namespace.TypeCast - ODataPathTemplate template = new ODataPathTemplate( - new SingletonSegmentTemplate(context.Singleton), - new CastSegmentTemplate(castType, entityType, context.Singleton)); - - action.AddSelector(httpMethod, context.Prefix, context.Model, template, context.Options?.RouteOptions); - return true; - } - } + // processed + return true; + } + // type cast + // GetFrom{EntityTypeName} or Get{SingletonName}From{EntityTypeName} + int index = actionMethodName.IndexOf("From", StringComparison.Ordinal); + if (index == -1) + { return false; } - private static bool IsSupportedActionName(ODataControllerActionContext context, string actionName, string singletonName, out string httpMethod) + string actionPrefix = actionMethodName.Substring(0, index); + if (IsSupportedActionName(context, actionPrefix, singletonName, out httpMethod)) { - StringComparison actionNameComparison = context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - if (actionName.Equals("Get", actionNameComparison) || actionName.Equals($"Get{singletonName}", actionNameComparison)) + string castTypeName = actionMethodName.Substring(index + 4); + if (castTypeName.Length == 0) { - httpMethod = "Get"; - return true; - } - else if (actionName.Equals("Put", actionNameComparison) || actionName.Equals($"Put{singletonName}", actionNameComparison)) - { - httpMethod = "Put"; - return true; + // Early return for the following cases: Get|Put|PatchFrom + return false; } - else if (actionName.Equals("Patch", actionNameComparison) || actionName.Equals($"Patch{singletonName}", actionNameComparison)) + + IEdmEntityType entityType = context.Singleton.EntityType; + + // Shall we cast to base type and the type itself? I think yes. + IEdmStructuredType castType = entityType.FindTypeInInheritance(context.Model, castTypeName, context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true); + if (castType != null) { - httpMethod = "Patch"; + // ~/Me/Namespace.TypeCast + ODataPathTemplate template = new ODataPathTemplate( + new SingletonSegmentTemplate(context.Singleton), + new CastSegmentTemplate(castType, entityType, context.Singleton)); + + action.AddSelector(httpMethod, context.Prefix, context.Model, template, context.Options?.RouteOptions); return true; } + } - httpMethod = ""; - return false; + return false; + } + + private static bool IsSupportedActionName(ODataControllerActionContext context, string actionName, string singletonName, out string httpMethod) + { + StringComparison actionNameComparison = context.Options?.RouteOptions?.EnableActionNameCaseInsensitive == true ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + if (actionName.Equals("Get", actionNameComparison) || actionName.Equals($"Get{singletonName}", actionNameComparison)) + { + httpMethod = "Get"; + return true; } + else if (actionName.Equals("Put", actionNameComparison) || actionName.Equals($"Put{singletonName}", actionNameComparison)) + { + httpMethod = "Put"; + return true; + } + else if (actionName.Equals("Patch", actionNameComparison) || actionName.Equals($"Patch{singletonName}", actionNameComparison)) + { + httpMethod = "Patch"; + return true; + } + + httpMethod = ""; + return false; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/IODataRoutingMetadata.cs b/src/Microsoft.AspNetCore.OData/Routing/IODataRoutingMetadata.cs index 050bb2bfc..889f17725 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/IODataRoutingMetadata.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/IODataRoutingMetadata.cs @@ -9,31 +9,30 @@ using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing +namespace Microsoft.AspNetCore.OData.Routing; + +/// +/// Defines a contract use to specify the OData routing related metadata in . +/// +public interface IODataRoutingMetadata { /// - /// Defines a contract use to specify the OData routing related metadata in . + /// Gets the prefix string. /// - public interface IODataRoutingMetadata - { - /// - /// Gets the prefix string. - /// - string Prefix { get; } + string Prefix { get; } - /// - /// Gets the Edm model. - /// - IEdmModel Model { get; } + /// + /// Gets the Edm model. + /// + IEdmModel Model { get; } - /// - /// Gets the OData path template - /// - ODataPathTemplate Template { get; } + /// + /// Gets the OData path template + /// + ODataPathTemplate Template { get; } - /// - /// Gets the boolean value indicating whether it's from OData conventional routing, false means from attribute routing. - /// - bool IsConventional { get; } - } + /// + /// Gets the boolean value indicating whether it's from OData conventional routing, false means from attribute routing. + /// + bool IsConventional { get; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/ODataPathExtensions.cs b/src/Microsoft.AspNetCore.OData/Routing/ODataPathExtensions.cs index 24cf019f1..53b93300b 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/ODataPathExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/ODataPathExtensions.cs @@ -11,319 +11,318 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing +namespace Microsoft.AspNetCore.OData.Routing; + +/// +/// Extension methods for . +/// +public static class ODataPathExtensions { /// - /// Extension methods for . + /// Gets a boolean value indicating whether the given path is a stream property path. /// - public static class ODataPathExtensions + /// The given odata path. + /// true/false + public static bool IsStreamPropertyPath(this ODataPath path) { - /// - /// Gets a boolean value indicating whether the given path is a stream property path. - /// - /// The given odata path. - /// true/false - public static bool IsStreamPropertyPath(this ODataPath path) + if (path == null) { - if (path == null) - { - return false; - } + return false; + } - PropertySegment propertySegment = path.LastSegment as PropertySegment; - if (propertySegment == null) - { - return false; - } + PropertySegment propertySegment = path.LastSegment as PropertySegment; + if (propertySegment == null) + { + return false; + } - IEdmTypeReference propertyType = propertySegment.Property.Type; + IEdmTypeReference propertyType = propertySegment.Property.Type; - // Edm.Stream, or a type definition whose underlying type is Edm.Stream, - // cannot be used in collections or for non-binding parameters to functions or actions. - // So, we don't need to test it but leave the codes here for awareness. - //if (propertyType.IsCollection()) - //{ - // propertyType = propertyType.AsCollection().ElementType(); - //} + // Edm.Stream, or a type definition whose underlying type is Edm.Stream, + // cannot be used in collections or for non-binding parameters to functions or actions. + // So, we don't need to test it but leave the codes here for awareness. + //if (propertyType.IsCollection()) + //{ + // propertyType = propertyType.AsCollection().ElementType(); + //} - return propertyType.IsStream(); - } + return propertyType.IsStream(); + } - /// - /// Computes the of the resource identified by this . - /// - /// Path to compute the type for. - /// The of the resource, or null if the path does not identify a resource with a type. - public static IEdmType GetEdmType(this ODataPath path) + /// + /// Computes the of the resource identified by this . + /// + /// Path to compute the type for. + /// The of the resource, or null if the path does not identify a resource with a type. + public static IEdmType GetEdmType(this ODataPath path) + { + if (path == null) { - if (path == null) - { - throw Error.ArgumentNull(nameof(path)); - } + throw Error.ArgumentNull(nameof(path)); + } - ODataPathSegment lastSegment = path.LastSegment; - return lastSegment.EdmType; + ODataPathSegment lastSegment = path.LastSegment; + return lastSegment.EdmType; + } + + /// + /// Computes the of the resource identified by this . + /// + /// Path to compute the set for. + /// The of the resource, or null if the path does not identify a resource that is part of a set. + public static IEdmNavigationSource GetNavigationSource(this ODataPath path) + { + if (path == null) + { + throw Error.ArgumentNull(nameof(path)); } - /// - /// Computes the of the resource identified by this . - /// - /// Path to compute the set for. - /// The of the resource, or null if the path does not identify a resource that is part of a set. - public static IEdmNavigationSource GetNavigationSource(this ODataPath path) + for (int i = path.Count - 1; i >= 0; --i) { - if (path == null) + ODataPathSegment segment = path[i]; + if (segment is EntitySetSegment entitySetSegment) { - throw Error.ArgumentNull(nameof(path)); + return entitySetSegment.EntitySet; } - for (int i = path.Count - 1; i >= 0; --i) + if (segment is KeySegment keySegment) { - ODataPathSegment segment = path[i]; - if (segment is EntitySetSegment entitySetSegment) - { - return entitySetSegment.EntitySet; - } - - if (segment is KeySegment keySegment) - { - return keySegment.NavigationSource; - } - - if (segment is NavigationPropertyLinkSegment navigationPropertyLinkSegment) - { - return navigationPropertyLinkSegment.NavigationSource; - } + return keySegment.NavigationSource; + } - if (segment is NavigationPropertySegment navigationPropertySegment) - { - return navigationPropertySegment.NavigationSource; - } + if (segment is NavigationPropertyLinkSegment navigationPropertyLinkSegment) + { + return navigationPropertyLinkSegment.NavigationSource; + } - if (segment is OperationImportSegment operationImportSegment) - { - return operationImportSegment.EntitySet; - } + if (segment is NavigationPropertySegment navigationPropertySegment) + { + return navigationPropertySegment.NavigationSource; + } - if (segment is OperationSegment operationSegment) - { - return operationSegment.EntitySet; - } + if (segment is OperationImportSegment operationImportSegment) + { + return operationImportSegment.EntitySet; + } - if (segment is SingletonSegment singleton) - { - return singleton.Singleton; - } + if (segment is OperationSegment operationSegment) + { + return operationSegment.EntitySet; + } - if (segment is TypeSegment typeSegment) - { - return typeSegment.NavigationSource; - } + if (segment is SingletonSegment singleton) + { + return singleton.Singleton; + } - if (segment is PropertySegment) - { - continue; - } + if (segment is TypeSegment typeSegment) + { + return typeSegment.NavigationSource; + } - return null; + if (segment is PropertySegment) + { + continue; } return null; } - /// - /// Get the string representation of mainly translate Context Url path. - /// - /// Path to compute the set for. - /// The string representation of the Context Url path. - public static string GetPathString(this ODataPath path) + return null; + } + + /// + /// Get the string representation of mainly translate Context Url path. + /// + /// Path to compute the set for. + /// The string representation of the Context Url path. + public static string GetPathString(this ODataPath path) + { + if (path == null) { - if (path == null) - { - throw Error.ArgumentNull(nameof(path)); - } + throw Error.ArgumentNull(nameof(path)); + } - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - foreach (var segment in path) - { - segment.HandleWith(handler); - } + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + foreach (var segment in path) + { + segment.HandleWith(handler); + } + + return handler.PathLiteral; + } - return handler.PathLiteral; + /// + /// Get the string representation of mainly translate Context Url path. + /// + /// The path segments. + /// The string representation of the Context Url path. + public static string GetPathString(this IList segments) + { + if (segments == null) + { + throw Error.ArgumentNull(nameof(segments)); } - /// - /// Get the string representation of mainly translate Context Url path. - /// - /// The path segments. - /// The string representation of the Context Url path. - public static string GetPathString(this IList segments) + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + foreach (var segment in segments) { - if (segments == null) - { - throw Error.ArgumentNull(nameof(segments)); - } + segment.HandleWith(handler); + } - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - foreach (var segment in segments) - { - segment.HandleWith(handler); - } + return handler.PathLiteral; + } - return handler.PathLiteral; + internal static bool IsUntypedPropertyPath(this ODataPath path) + { + if (path == null) + { + return false; } - internal static bool IsUntypedPropertyPath(this ODataPath path) + // TODO: do we need take the type cast into consideration? + if (path.LastSegment is PropertySegment propertySegment) { - if (path == null) - { - return false; - } + return propertySegment.Property.Type.IsUntypedOrCollectionUntyped(); + } - // TODO: do we need take the type cast into consideration? - if (path.LastSegment is PropertySegment propertySegment) - { - return propertySegment.Property.Type.IsUntypedOrCollectionUntyped(); - } + // TODO, Shall we take the dynamic property path segment into consideration? + return false; + } - // TODO, Shall we take the dynamic property path segment into consideration? - return false; + /// + /// Gets the property and structured type from . + /// TODO: The logic implenetation is not good and do need refactor it later. + /// + /// The OData path. + /// The property, structured type and the name. + internal static (IEdmProperty, IEdmStructuredType, string) GetPropertyAndStructuredTypeFromPath(this ODataPath path) + { + if (path == null) + { + return (null, null, string.Empty); } - /// - /// Gets the property and structured type from . - /// TODO: The logic implenetation is not good and do need refactor it later. - /// - /// The OData path. - /// The property, structured type and the name. - internal static (IEdmProperty, IEdmStructuredType, string) GetPropertyAndStructuredTypeFromPath(this ODataPath path) + IEdmStructuredType structuredType = null; + string typeCast = string.Empty; + IEnumerable reverseSegments = path.Reverse(); + foreach (var segment in reverseSegments) { - if (path == null) + if (segment is NavigationPropertySegment navigationPathSegment) { - return (null, null, string.Empty); + IEdmProperty property = navigationPathSegment.NavigationProperty; + if (structuredType == null) + { + structuredType = navigationPathSegment.NavigationProperty.ToEntityType(); + } + + string name = navigationPathSegment.NavigationProperty.Name + typeCast; + return (property, structuredType, name); } - IEdmStructuredType structuredType = null; - string typeCast = string.Empty; - IEnumerable reverseSegments = path.Reverse(); - foreach (var segment in reverseSegments) + if (segment is OperationSegment operationSegment) { - if (segment is NavigationPropertySegment navigationPathSegment) + if (structuredType == null) { - IEdmProperty property = navigationPathSegment.NavigationProperty; - if (structuredType == null) - { - structuredType = navigationPathSegment.NavigationProperty.ToEntityType(); - } - - string name = navigationPathSegment.NavigationProperty.Name + typeCast; - return (property, structuredType, name); + structuredType = operationSegment.EdmType.AsElementType() as IEdmStructuredType; } - if (segment is OperationSegment operationSegment) - { - if (structuredType == null) - { - structuredType = operationSegment.EdmType.AsElementType() as IEdmStructuredType; - } - - string name = operationSegment.Operations.First().FullName() + typeCast; - return (null, structuredType, name); - } + string name = operationSegment.Operations.First().FullName() + typeCast; + return (null, structuredType, name); + } - if (segment is PropertySegment propertyAccessPathSegment) + if (segment is PropertySegment propertyAccessPathSegment) + { + IEdmProperty property = propertyAccessPathSegment.Property; + if (structuredType == null) { - IEdmProperty property = propertyAccessPathSegment.Property; - if (structuredType == null) - { - structuredType = property.Type.GetElementType() as IEdmStructuredType; - } - - string name = property.Name + typeCast; - return (property, structuredType, name); + structuredType = property.Type.GetElementType() as IEdmStructuredType; } - if (segment is EntitySetSegment entitySetSegment) - { - if (structuredType == null) - { - structuredType = entitySetSegment.EntitySet.EntityType; - } - - string name = entitySetSegment.EntitySet.Name + typeCast; - return (null, structuredType, name); - } + string name = property.Name + typeCast; + return (property, structuredType, name); + } - if (segment is SingletonSegment singletonSegment) + if (segment is EntitySetSegment entitySetSegment) + { + if (structuredType == null) { - if (structuredType == null) - { - structuredType = singletonSegment.Singleton.EntityType; - } - - string name = singletonSegment.Singleton.Name + typeCast; - return (null, structuredType, name); + structuredType = entitySetSegment.EntitySet.EntityType; } - if (segment is TypeSegment typeSegment) - { - structuredType = typeSegment.EdmType.AsElementType() as IEdmStructuredType; - typeCast = "/" + structuredType; - } - else if (segment is KeySegment || segment is CountSegment) - { - // do nothing, just go to next segment, what about if meet OperationSegment? - } - else + string name = entitySetSegment.EntitySet.Name + typeCast; + return (null, structuredType, name); + } + + if (segment is SingletonSegment singletonSegment) + { + if (structuredType == null) { - // if we meet any other segments, just return (null, null, string.Empty); - break; + structuredType = singletonSegment.Singleton.EntityType; } + + string name = singletonSegment.Singleton.Name + typeCast; + return (null, structuredType, name); } - return (null, null, string.Empty); + if (segment is TypeSegment typeSegment) + { + structuredType = typeSegment.EdmType.AsElementType() as IEdmStructuredType; + typeCast = "/" + structuredType; + } + else if (segment is KeySegment || segment is CountSegment) + { + // do nothing, just go to next segment, what about if meet OperationSegment? + } + else + { + // if we meet any other segments, just return (null, null, string.Empty); + break; + } } - #region BACKUP + return (null, null, string.Empty); + } + + #region BACKUP #if false - /// - /// - /// - /// - /// - /// - public static string TranslatePathTemplateSegment(this PathTemplateSegment pathTemplatesegment, out string value) + /// + /// + /// + /// + /// + /// + public static string TranslatePathTemplateSegment(this PathTemplateSegment pathTemplatesegment, out string value) + { + if (pathTemplatesegment == null) { - if (pathTemplatesegment == null) - { - throw Error.ArgumentNull("pathTemplatesegment"); - } + throw Error.ArgumentNull("pathTemplatesegment"); + } - string pathTemplateSegmentLiteralText = pathTemplatesegment.LiteralText; - if (pathTemplateSegmentLiteralText == null) - { - throw new ODataException(Error.Format(SRResources.InvalidAttributeRoutingTemplateSegment, string.Empty)); - } + string pathTemplateSegmentLiteralText = pathTemplatesegment.LiteralText; + if (pathTemplateSegmentLiteralText == null) + { + throw new ODataException(Error.Format(SRResources.InvalidAttributeRoutingTemplateSegment, string.Empty)); + } - if (pathTemplateSegmentLiteralText.StartsWith("{", StringComparison.Ordinal) - && pathTemplateSegmentLiteralText.EndsWith("}", StringComparison.Ordinal)) + if (pathTemplateSegmentLiteralText.StartsWith("{", StringComparison.Ordinal) + && pathTemplateSegmentLiteralText.EndsWith("}", StringComparison.Ordinal)) + { + string[] keyValuePair = pathTemplateSegmentLiteralText.Substring(1, + pathTemplateSegmentLiteralText.Length - 2).Split(':'); + if (keyValuePair.Length != 2) { - string[] keyValuePair = pathTemplateSegmentLiteralText.Substring(1, - pathTemplateSegmentLiteralText.Length - 2).Split(':'); - if (keyValuePair.Length != 2) - { - throw new ODataException(Error.Format( - SRResources.InvalidAttributeRoutingTemplateSegment, - pathTemplateSegmentLiteralText)); - } - value = "{" + keyValuePair[0] + "}"; - return keyValuePair[1]; + throw new ODataException(Error.Format( + SRResources.InvalidAttributeRoutingTemplateSegment, + pathTemplateSegmentLiteralText)); } - - value = string.Empty; - return string.Empty; + value = "{" + keyValuePair[0] + "}"; + return keyValuePair[1]; } -#endif - #endregion + + value = string.Empty; + return string.Empty; } +#endif + #endregion } diff --git a/src/Microsoft.AspNetCore.OData/Routing/ODataPathNavigationSourceHandler.cs b/src/Microsoft.AspNetCore.OData/Routing/ODataPathNavigationSourceHandler.cs index 19f1df6e6..08c414ca2 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/ODataPathNavigationSourceHandler.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/ODataPathNavigationSourceHandler.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -14,288 +14,287 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing +namespace Microsoft.AspNetCore.OData.Routing; + +/// +/// A handler used to calculate some values based on the odata path. +/// +public class ODataPathNavigationSourceHandler : PathSegmentHandler { + private readonly IList _path; + /// - /// A handler used to calculate some values based on the odata path. + /// Initializes a new instance of the class. /// - public class ODataPathNavigationSourceHandler : PathSegmentHandler + public ODataPathNavigationSourceHandler() { - private readonly IList _path; + _path = new List(); + } - /// - /// Initializes a new instance of the class. - /// - public ODataPathNavigationSourceHandler() - { - _path = new List(); - } + /// + /// Gets the path navigation source. + /// + public IEdmNavigationSource NavigationSource { get; private set; } - /// - /// Gets the path navigation source. - /// - public IEdmNavigationSource NavigationSource { get; private set; } + /// + /// Gets the path template. + /// + public string Path + { + get { return string.Join("/", _path); } + } - /// - /// Gets the path template. - /// - public string Path - { - get { return string.Join("/", _path); } - } + /// + /// Handle an . + /// + /// The segment to handle. + public override void Handle(EntitySetSegment segment) + { + Contract.Assert(segment != null); - /// - /// Handle an . - /// - /// The segment to handle. - public override void Handle(EntitySetSegment segment) - { - Contract.Assert(segment != null); + NavigationSource = segment.EntitySet; + _path.Add(segment.EntitySet.Name); + } - NavigationSource = segment.EntitySet; - _path.Add(segment.EntitySet.Name); - } + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(KeySegment segment) + { + Contract.Assert(segment != null); - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(KeySegment segment) - { - Contract.Assert(segment != null); + NavigationSource = segment.NavigationSource; + } - NavigationSource = segment.NavigationSource; - } + /// + /// Handle a . + /// + /// The segment to handle + public override void Handle(NavigationPropertyLinkSegment segment) + { + Contract.Assert(segment != null); - /// - /// Handle a . - /// - /// The segment to handle - public override void Handle(NavigationPropertyLinkSegment segment) - { - Contract.Assert(segment != null); + NavigationSource = segment.NavigationSource; - NavigationSource = segment.NavigationSource; + _path.Add(segment.NavigationProperty.Name); + _path.Add(ODataSegmentKinds.Ref); + } - _path.Add(segment.NavigationProperty.Name); - _path.Add(ODataSegmentKinds.Ref); - } + /// + /// Handle a . + /// + /// the segment to Handle + public override void Handle(NavigationPropertySegment segment) + { + Contract.Assert(segment != null); - /// - /// Handle a . - /// - /// the segment to Handle - public override void Handle(NavigationPropertySegment segment) - { - Contract.Assert(segment != null); + NavigationSource = segment.NavigationSource; + _path.Add(segment.NavigationProperty.Name); + } - NavigationSource = segment.NavigationSource; - _path.Add(segment.NavigationProperty.Name); - } + /// + /// Handle a . + /// + /// the segment to Handle + public override void Handle(DynamicPathSegment segment) + { + Contract.Assert(segment != null); - /// - /// Handle a . - /// - /// the segment to Handle - public override void Handle(DynamicPathSegment segment) - { - Contract.Assert(segment != null); + NavigationSource = null; + _path.Add(segment.Identifier); + } - NavigationSource = null; - _path.Add(segment.Identifier); - } + /// + /// Handle an . + /// + /// the segment to Handle + public override void Handle(OperationImportSegment segment) + { + Contract.Assert(segment != null); - /// - /// Handle an . - /// - /// the segment to Handle - public override void Handle(OperationImportSegment segment) - { - Contract.Assert(segment != null); + NavigationSource = segment.EntitySet; - NavigationSource = segment.EntitySet; + IEdmActionImport actionImport = segment.OperationImports.Single() as IEdmActionImport; - IEdmActionImport actionImport = segment.OperationImports.Single() as IEdmActionImport; + if (actionImport != null) + { + _path.Add(actionImport.Name); + } + else + { + IEdmFunctionImport function = (IEdmFunctionImport)segment.OperationImports.Single(); - if (actionImport != null) - { - _path.Add(actionImport.Name); - } - else + IList parameterValues = new List(); + foreach (var parameter in segment.Parameters) { - IEdmFunctionImport function = (IEdmFunctionImport)segment.OperationImports.Single(); - - IList parameterValues = new List(); - foreach (var parameter in segment.Parameters) + var functionParameter = function.Function.Parameters.FirstOrDefault(p => p.Name == parameter.Name); + if (functionParameter == null) { - var functionParameter = function.Function.Parameters.FirstOrDefault(p => p.Name == parameter.Name); - if (functionParameter == null) - { - continue; - } - - parameterValues.Add(functionParameter.Type.FullName()); + continue; } - string literal = string.Format(CultureInfo.InvariantCulture, "{0}({1})", function.Name, string.Join(",", parameterValues)); - - _path.Add(literal); + parameterValues.Add(functionParameter.Type.FullName()); } + + string literal = string.Format(CultureInfo.InvariantCulture, "{0}({1})", function.Name, string.Join(",", parameterValues)); + + _path.Add(literal); } + } - /// - /// Handle an . - /// - /// The segment to handle. - public override void Handle(OperationSegment segment) - { - Contract.Assert(segment != null); - NavigationSource = segment.EntitySet; + /// + /// Handle an . + /// + /// The segment to handle. + public override void Handle(OperationSegment segment) + { + Contract.Assert(segment != null); + NavigationSource = segment.EntitySet; - IEdmAction action = segment.Operations.Single() as IEdmAction; + IEdmAction action = segment.Operations.Single() as IEdmAction; - if (action != null) - { - _path.Add(action.FullName()); - } - else - { - IEdmFunction function = (IEdmFunction)segment.Operations.Single(); + if (action != null) + { + _path.Add(action.FullName()); + } + else + { + IEdmFunction function = (IEdmFunction)segment.Operations.Single(); - IList parameterValues = new List(); - foreach (var parameter in segment.Parameters) + IList parameterValues = new List(); + foreach (var parameter in segment.Parameters) + { + var functionParameter = function.Parameters.FirstOrDefault(p => p.Name == parameter.Name); + if (functionParameter == null) { - var functionParameter = function.Parameters.FirstOrDefault(p => p.Name == parameter.Name); - if (functionParameter == null) - { - continue; - } - - parameterValues.Add(functionParameter.Type.FullName()); + continue; } - string literal = string.Format(CultureInfo.InvariantCulture, "{0}({1})", function.FullName(), string.Join(",", parameterValues)); - - _path.Add(literal); + parameterValues.Add(functionParameter.Type.FullName()); } - } - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(PathTemplateSegment segment) - { - Contract.Assert(segment != null); + string literal = string.Format(CultureInfo.InvariantCulture, "{0}({1})", function.FullName(), string.Join(",", parameterValues)); - NavigationSource = null; - _path.Add(segment.LiteralText); + _path.Add(literal); } + } - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(PropertySegment segment) - { - Contract.Assert(segment != null); + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(PathTemplateSegment segment) + { + Contract.Assert(segment != null); - // Not set navigation source to null as the relevant navigation source for the path will be the previous navigation source. + NavigationSource = null; + _path.Add(segment.LiteralText); + } - _path.Add(segment.Property.Name); - } + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(PropertySegment segment) + { + Contract.Assert(segment != null); - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(SingletonSegment segment) - { - Contract.Assert(segment != null); + // Not set navigation source to null as the relevant navigation source for the path will be the previous navigation source. - NavigationSource = segment.Singleton; - _path.Add(segment.Singleton.Name); - } + _path.Add(segment.Property.Name); + } - /// - /// Handle a , we use "cast" for type segment. - /// - /// The segment to handle. - public override void Handle(TypeSegment segment) - { - Contract.Assert(segment != null); + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(SingletonSegment segment) + { + Contract.Assert(segment != null); - NavigationSource = segment.NavigationSource; + NavigationSource = segment.Singleton; + _path.Add(segment.Singleton.Name); + } - // Uri literal does not use the collection type. - IEdmType elementType = segment.EdmType; - if (segment.EdmType.TypeKind == EdmTypeKind.Collection) - { - elementType = ((IEdmCollectionType)segment.EdmType).ElementType.Definition; - } + /// + /// Handle a , we use "cast" for type segment. + /// + /// The segment to handle. + public override void Handle(TypeSegment segment) + { + Contract.Assert(segment != null); - _path.Add(elementType.FullTypeName()); - } + NavigationSource = segment.NavigationSource; - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(ValueSegment segment) + // Uri literal does not use the collection type. + IEdmType elementType = segment.EdmType; + if (segment.EdmType.TypeKind == EdmTypeKind.Collection) { - Contract.Assert(segment != null); - - NavigationSource = null; - _path.Add(ODataSegmentKinds.Value); + elementType = ((IEdmCollectionType)segment.EdmType).ElementType.Definition; } - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(CountSegment segment) - { - Contract.Assert(segment != null); + _path.Add(elementType.FullTypeName()); + } - NavigationSource = null; - _path.Add(ODataSegmentKinds.Count); - } + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(ValueSegment segment) + { + Contract.Assert(segment != null); - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(BatchSegment segment) - { - Contract.Assert(segment != null); + NavigationSource = null; + _path.Add(ODataSegmentKinds.Value); + } - NavigationSource = null; - _path.Add(ODataSegmentKinds.Batch); - } + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(CountSegment segment) + { + Contract.Assert(segment != null); - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(MetadataSegment segment) - { - Contract.Assert(segment != null); + NavigationSource = null; + _path.Add(ODataSegmentKinds.Count); + } - NavigationSource = null; - _path.Add(ODataSegmentKinds.Metadata); - } + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(BatchSegment segment) + { + Contract.Assert(segment != null); - /// - /// Handle a general path segment. - /// - /// The segment to handle. - public override void Handle(ODataPathSegment segment) - { - // ODL doesn't provide the handle function for general path segment - Contract.Assert(segment != null); + NavigationSource = null; + _path.Add(ODataSegmentKinds.Batch); + } - NavigationSource = null; - _path.Add(segment.ToString()); - } + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(MetadataSegment segment) + { + Contract.Assert(segment != null); + + NavigationSource = null; + _path.Add(ODataSegmentKinds.Metadata); + } + + /// + /// Handle a general path segment. + /// + /// The segment to handle. + public override void Handle(ODataPathSegment segment) + { + // ODL doesn't provide the handle function for general path segment + Contract.Assert(segment != null); + + NavigationSource = null; + _path.Add(segment.ToString()); } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/ODataPathSegmentHandler.cs b/src/Microsoft.AspNetCore.OData/Routing/ODataPathSegmentHandler.cs index 1b1c2cb90..33374213f 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/ODataPathSegmentHandler.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/ODataPathSegmentHandler.cs @@ -15,356 +15,355 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing +namespace Microsoft.AspNetCore.OData.Routing; + +/// +/// A handler used to calculate some values based on the odata path. +/// +public class ODataPathSegmentHandler : PathSegmentHandler { + private readonly IList _pathUriLiteral; + private IEdmNavigationSource _navigationSource; // used to record the navigation source in the last segment. + /// - /// A handler used to calculate some values based on the odata path. + /// Initializes a new instance of the class. /// - public class ODataPathSegmentHandler : PathSegmentHandler + public ODataPathSegmentHandler() { - private readonly IList _pathUriLiteral; - private IEdmNavigationSource _navigationSource; // used to record the navigation source in the last segment. + _navigationSource = null; + _pathUriLiteral = new List(); + } - /// - /// Initializes a new instance of the class. - /// - public ODataPathSegmentHandler() - { - _navigationSource = null; - _pathUriLiteral = new List(); - } + /// + /// Gets the path navigation source. + /// + public IEdmNavigationSource NavigationSource => _navigationSource; - /// - /// Gets the path navigation source. - /// - public IEdmNavigationSource NavigationSource => _navigationSource; - - /// - /// Gets the path Uri literal. - /// - public string PathLiteral => string.Join("/", _pathUriLiteral); - - /// - /// Handle an . - /// - /// The segment to handle. - public override void Handle(EntitySetSegment segment) - { - Contract.Assert(segment != null); + /// + /// Gets the path Uri literal. + /// + public string PathLiteral => string.Join("/", _pathUriLiteral); - _navigationSource = segment.EntitySet; - _pathUriLiteral.Add(segment.EntitySet.Name); - } + /// + /// Handle an . + /// + /// The segment to handle. + public override void Handle(EntitySetSegment segment) + { + Contract.Assert(segment != null); - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(KeySegment segment) - { - Contract.Assert(segment != null); + _navigationSource = segment.EntitySet; + _pathUriLiteral.Add(segment.EntitySet.Name); + } - _navigationSource = segment.NavigationSource; + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(KeySegment segment) + { + Contract.Assert(segment != null); - string value = ConvertKeysToString(segment.Keys, segment.EdmType); + _navigationSource = segment.NavigationSource; - // update the previous segment Uri literal - if (!_pathUriLiteral.Any()) - { - _pathUriLiteral.Add("(" + value + ")"); - return; - } + string value = ConvertKeysToString(segment.Keys, segment.EdmType); - if (_pathUriLiteral.Last() == ODataSegmentKinds.Ref) - { - _pathUriLiteral[_pathUriLiteral.Count - 2] = - _pathUriLiteral[_pathUriLiteral.Count - 2] + "(" + value + ")"; - } - else - { - _pathUriLiteral[_pathUriLiteral.Count - 1] = - _pathUriLiteral[_pathUriLiteral.Count - 1] + "(" + value + ")"; - } + // update the previous segment Uri literal + if (!_pathUriLiteral.Any()) + { + _pathUriLiteral.Add("(" + value + ")"); + return; } - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(NavigationPropertyLinkSegment segment) + if (_pathUriLiteral.Last() == ODataSegmentKinds.Ref) { - Contract.Assert(segment != null); - _navigationSource = segment.NavigationSource; - - _pathUriLiteral.Add(segment.NavigationProperty.Name); - _pathUriLiteral.Add(ODataSegmentKinds.Ref); + _pathUriLiteral[_pathUriLiteral.Count - 2] = + _pathUriLiteral[_pathUriLiteral.Count - 2] + "(" + value + ")"; } - - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(NavigationPropertySegment segment) + else { - Contract.Assert(segment != null); - _navigationSource = segment.NavigationSource; - - _pathUriLiteral.Add(segment.NavigationProperty.Name); + _pathUriLiteral[_pathUriLiteral.Count - 1] = + _pathUriLiteral[_pathUriLiteral.Count - 1] + "(" + value + ")"; } + } - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(DynamicPathSegment segment) - { - Contract.Assert(segment != null); - _navigationSource = null; + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(NavigationPropertyLinkSegment segment) + { + Contract.Assert(segment != null); + _navigationSource = segment.NavigationSource; - _pathUriLiteral.Add(segment.Identifier); - } + _pathUriLiteral.Add(segment.NavigationProperty.Name); + _pathUriLiteral.Add(ODataSegmentKinds.Ref); + } - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(OperationImportSegment segment) - { - Contract.Assert(segment != null); + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(NavigationPropertySegment segment) + { + Contract.Assert(segment != null); + _navigationSource = segment.NavigationSource; - _navigationSource = segment.EntitySet; + _pathUriLiteral.Add(segment.NavigationProperty.Name); + } - IEdmActionImport actionImport = segment.OperationImports.Single() as IEdmActionImport; + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(DynamicPathSegment segment) + { + Contract.Assert(segment != null); + _navigationSource = null; - if (actionImport != null) - { - _pathUriLiteral.Add(actionImport.Name); - } - else - { - // Translate the nodes in ODL path to string literals as parameter of UnboundFunctionPathSegment. - Dictionary parameterValues = segment.Parameters.ToDictionary( - parameterValue => parameterValue.Name, - parameterValue => TranslateNode(parameterValue.Value)); + _pathUriLiteral.Add(segment.Identifier); + } - IEdmFunctionImport function = (IEdmFunctionImport)segment.OperationImports.Single(); + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(OperationImportSegment segment) + { + Contract.Assert(segment != null); - IEnumerable parameters = parameterValues.Select(v => String.Format(CultureInfo.InvariantCulture, "{0}={1}", v.Key, v.Value)); - string literal = string.Format(CultureInfo.InvariantCulture, "{0}({1})", function.Name, String.Join(",", parameters)); + _navigationSource = segment.EntitySet; - _pathUriLiteral.Add(literal); - } - } + IEdmActionImport actionImport = segment.OperationImports.Single() as IEdmActionImport; - /// - /// Handle an . - /// - /// The segment to handle. - public override void Handle(OperationSegment segment) + if (actionImport != null) + { + _pathUriLiteral.Add(actionImport.Name); + } + else { - Contract.Assert(segment != null); - _navigationSource = segment.EntitySet; + // Translate the nodes in ODL path to string literals as parameter of UnboundFunctionPathSegment. + Dictionary parameterValues = segment.Parameters.ToDictionary( + parameterValue => parameterValue.Name, + parameterValue => TranslateNode(parameterValue.Value)); - IEdmAction action = segment.Operations.Single() as IEdmAction; + IEdmFunctionImport function = (IEdmFunctionImport)segment.OperationImports.Single(); - if (action != null) - { - _pathUriLiteral.Add(action.FullName()); - } - else - { - // Translate the nodes in ODL path to string literals as parameter of BoundFunctionPathSegment. - Dictionary parameterValues = segment.Parameters.ToDictionary( - parameterValue => parameterValue.Name, - parameterValue => TranslateNode(parameterValue.Value)); + IEnumerable parameters = parameterValues.Select(v => String.Format(CultureInfo.InvariantCulture, "{0}={1}", v.Key, v.Value)); + string literal = string.Format(CultureInfo.InvariantCulture, "{0}({1})", function.Name, String.Join(",", parameters)); - // TODO: refactor the function literal for parameter alias - IEdmFunction function = (IEdmFunction)segment.Operations.Single(); + _pathUriLiteral.Add(literal); + } + } - IEnumerable parameters = parameterValues.Select(v => String.Format(CultureInfo.InvariantCulture, "{0}={1}", v.Key, v.Value)); - string literal = String.Format(CultureInfo.InvariantCulture, "{0}({1})", function.FullName(), String.Join(",", parameters)); + /// + /// Handle an . + /// + /// The segment to handle. + public override void Handle(OperationSegment segment) + { + Contract.Assert(segment != null); + _navigationSource = segment.EntitySet; - _pathUriLiteral.Add(literal); - } - } + IEdmAction action = segment.Operations.Single() as IEdmAction; - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(PathTemplateSegment segment) + if (action != null) { - Contract.Assert(segment != null); - _navigationSource = null; - - _pathUriLiteral.Add(segment.LiteralText); + _pathUriLiteral.Add(action.FullName()); } - - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(PropertySegment segment) + else { - Contract.Assert(segment != null); - // Not setting navigation source to null as the relevant navigation source for the path will be the previous navigation source. + // Translate the nodes in ODL path to string literals as parameter of BoundFunctionPathSegment. + Dictionary parameterValues = segment.Parameters.ToDictionary( + parameterValue => parameterValue.Name, + parameterValue => TranslateNode(parameterValue.Value)); - _pathUriLiteral.Add(segment.Property.Name); - } + // TODO: refactor the function literal for parameter alias + IEdmFunction function = (IEdmFunction)segment.Operations.Single(); - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(SingletonSegment segment) - { - Contract.Assert(segment != null); - _navigationSource = segment.Singleton; + IEnumerable parameters = parameterValues.Select(v => String.Format(CultureInfo.InvariantCulture, "{0}={1}", v.Key, v.Value)); + string literal = String.Format(CultureInfo.InvariantCulture, "{0}({1})", function.FullName(), String.Join(",", parameters)); - _pathUriLiteral.Add(segment.Singleton.Name); + _pathUriLiteral.Add(literal); } + } - /// - /// Handle a , we use "cast" for type segment. - /// - /// The segment to handle. - public override void Handle(TypeSegment segment) - { - Contract.Assert(segment != null); - _navigationSource = segment.NavigationSource; + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(PathTemplateSegment segment) + { + Contract.Assert(segment != null); + _navigationSource = null; - // Uri literal does not use the collection type. - IEdmType elementType = segment.EdmType; - if (segment.EdmType.TypeKind == EdmTypeKind.Collection) - { - elementType = ((IEdmCollectionType)segment.EdmType).ElementType.Definition; - } + _pathUriLiteral.Add(segment.LiteralText); + } - _pathUriLiteral.Add(elementType.FullTypeName()); - } + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(PropertySegment segment) + { + Contract.Assert(segment != null); + // Not setting navigation source to null as the relevant navigation source for the path will be the previous navigation source. - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(ValueSegment segment) - { - // do nothing for the navigation source for $value. - // It means to use the previous the navigation source - Contract.Assert(segment != null); + _pathUriLiteral.Add(segment.Property.Name); + } - _pathUriLiteral.Add(ODataSegmentKinds.Value); - } + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(SingletonSegment segment) + { + Contract.Assert(segment != null); + _navigationSource = segment.Singleton; - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(CountSegment segment) - { - Contract.Assert(segment != null); - _navigationSource = null; + _pathUriLiteral.Add(segment.Singleton.Name); + } - _pathUriLiteral.Add(ODataSegmentKinds.Count); - } + /// + /// Handle a , we use "cast" for type segment. + /// + /// The segment to handle. + public override void Handle(TypeSegment segment) + { + Contract.Assert(segment != null); + _navigationSource = segment.NavigationSource; - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(BatchSegment segment) + // Uri literal does not use the collection type. + IEdmType elementType = segment.EdmType; + if (segment.EdmType.TypeKind == EdmTypeKind.Collection) { - Contract.Assert(segment != null); - _navigationSource = null; - - _pathUriLiteral.Add(ODataSegmentKinds.Batch); + elementType = ((IEdmCollectionType)segment.EdmType).ElementType.Definition; } - /// - /// Handle a . - /// - /// The segment to handle. - public override void Handle(MetadataSegment segment) - { - Contract.Assert(segment != null); - _navigationSource = null; + _pathUriLiteral.Add(elementType.FullTypeName()); + } - _pathUriLiteral.Add(ODataSegmentKinds.Metadata); - } + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(ValueSegment segment) + { + // do nothing for the navigation source for $value. + // It means to use the previous the navigation source + Contract.Assert(segment != null); - // Convert the objects of keys in ODL path to string literals. - internal static string ConvertKeysToString(IEnumerable> keys, IEdmType edmType) - { - Contract.Assert(keys != null); + _pathUriLiteral.Add(ODataSegmentKinds.Value); + } - IEdmEntityType entityType = edmType as IEdmEntityType; - Contract.Assert(entityType != null); + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(CountSegment segment) + { + Contract.Assert(segment != null); + _navigationSource = null; - IList> keyValuePairs = keys as IList> ?? keys.ToList(); - if (keyValuePairs.Count < 1) - { - return string.Empty; - } + _pathUriLiteral.Add(ODataSegmentKinds.Count); + } - // only one key - if (keyValuePairs.Count < 2) - { - var keyValue = keyValuePairs.First(); - bool isDeclaredKey = entityType.Key().Any(k => k.Name == keyValue.Key); - - // To support the alternate key - if (isDeclaredKey) - { - return string.Join( - ",", - keyValuePairs.Select(keyValuePair => - TranslateNode(keyValuePair.Value).EscapeBackSlashUriString()).ToArray()); - } - } + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(BatchSegment segment) + { + Contract.Assert(segment != null); + _navigationSource = null; + + _pathUriLiteral.Add(ODataSegmentKinds.Batch); + } + + /// + /// Handle a . + /// + /// The segment to handle. + public override void Handle(MetadataSegment segment) + { + Contract.Assert(segment != null); + _navigationSource = null; + + _pathUriLiteral.Add(ODataSegmentKinds.Metadata); + } - // composite keys or alternate key(s) - return string.Join( - ",", - keyValuePairs.Select(keyValuePair => - (keyValuePair.Key + - "=" + - TranslateNode(keyValuePair.Value).EscapeBackSlashUriString())).ToArray()); + // Convert the objects of keys in ODL path to string literals. + internal static string ConvertKeysToString(IEnumerable> keys, IEdmType edmType) + { + Contract.Assert(keys != null); + + IEdmEntityType entityType = edmType as IEdmEntityType; + Contract.Assert(entityType != null); + + IList> keyValuePairs = keys as IList> ?? keys.ToList(); + if (keyValuePairs.Count < 1) + { + return string.Empty; } - internal static string TranslateNode(object node) + // only one key + if (keyValuePairs.Count < 2) { - ConstantNode constantNode = node as ConstantNode; - if (constantNode != null) + var keyValue = keyValuePairs.First(); + bool isDeclaredKey = entityType.Key().Any(k => k.Name == keyValue.Key); + + // To support the alternate key + if (isDeclaredKey) { - UriTemplateExpression uriTemplateExpression = constantNode.Value as UriTemplateExpression; - if (uriTemplateExpression != null) - { - return uriTemplateExpression.LiteralText; - } - - // Make the enum prefix free to work. - ODataEnumValue enumValue = constantNode.Value as ODataEnumValue; - if (enumValue != null) - { - return ODataUriUtils.ConvertToUriLiteral(enumValue, ODataVersion.V4); - } - - return constantNode.LiteralText; + return string.Join( + ",", + keyValuePairs.Select(keyValuePair => + TranslateNode(keyValuePair.Value).EscapeBackSlashUriString()).ToArray()); } + } + + // composite keys or alternate key(s) + return string.Join( + ",", + keyValuePairs.Select(keyValuePair => + (keyValuePair.Key + + "=" + + TranslateNode(keyValuePair.Value).EscapeBackSlashUriString())).ToArray()); + } - ConvertNode convertNode = node as ConvertNode; - if (convertNode != null) + internal static string TranslateNode(object node) + { + ConstantNode constantNode = node as ConstantNode; + if (constantNode != null) + { + UriTemplateExpression uriTemplateExpression = constantNode.Value as UriTemplateExpression; + if (uriTemplateExpression != null) { - return TranslateNode(convertNode.Source); + return uriTemplateExpression.LiteralText; } - ParameterAliasNode parameterAliasNode = node as ParameterAliasNode; - if (parameterAliasNode != null) + // Make the enum prefix free to work. + ODataEnumValue enumValue = constantNode.Value as ODataEnumValue; + if (enumValue != null) { - return parameterAliasNode.Alias; + return ODataUriUtils.ConvertToUriLiteral(enumValue, ODataVersion.V4); } - return ODataUriUtils.ConvertToUriLiteral(node, ODataVersion.V4); + return constantNode.LiteralText; + } + + ConvertNode convertNode = node as ConvertNode; + if (convertNode != null) + { + return TranslateNode(convertNode.Source); + } + + ParameterAliasNode parameterAliasNode = node as ParameterAliasNode; + if (parameterAliasNode != null) + { + return parameterAliasNode.Alias; } + + return ODataUriUtils.ConvertToUriLiteral(node, ODataVersion.V4); } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/ODataPathSegmentTranslator.cs b/src/Microsoft.AspNetCore.OData/Routing/ODataPathSegmentTranslator.cs index e0058ab3a..72a367aa8 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/ODataPathSegmentTranslator.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/ODataPathSegmentTranslator.cs @@ -8,57 +8,56 @@ using System.Collections.Generic; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing +namespace Microsoft.AspNetCore.OData.Routing; + +/// +/// Translator the parameter alias, convert node, returned entity set into OData path segment. +/// +public class ODataPathSegmentTranslator : PathSegmentTranslator { /// - /// Translator the parameter alias, convert node, returned entity set into OData path segment. + /// /// - public class ODataPathSegmentTranslator : PathSegmentTranslator + /// + /// + /// + public static SingleValueNode TranslateParameterAlias( + SingleValueNode node, + IDictionary parameterAliasNodes) { - /// - /// - /// - /// - /// - /// - public static SingleValueNode TranslateParameterAlias( - SingleValueNode node, - IDictionary parameterAliasNodes) + if (node == null) { - if (node == null) - { - throw Error.ArgumentNull(nameof(node)); - } + throw Error.ArgumentNull(nameof(node)); + } - if (parameterAliasNodes == null) - { - throw Error.ArgumentNull(nameof(parameterAliasNodes)); - } + if (parameterAliasNodes == null) + { + throw Error.ArgumentNull(nameof(parameterAliasNodes)); + } - ParameterAliasNode parameterAliasNode = node as ParameterAliasNode; + ParameterAliasNode parameterAliasNode = node as ParameterAliasNode; - if (parameterAliasNode == null) - { - return node; - } + if (parameterAliasNode == null) + { + return node; + } - SingleValueNode singleValueNode; + SingleValueNode singleValueNode; - if (parameterAliasNodes.TryGetValue(parameterAliasNode.Alias, out singleValueNode) && - singleValueNode != null) + if (parameterAliasNodes.TryGetValue(parameterAliasNode.Alias, out singleValueNode) && + singleValueNode != null) + { + if (singleValueNode is ParameterAliasNode) { - if (singleValueNode is ParameterAliasNode) - { - singleValueNode = TranslateParameterAlias(singleValueNode, parameterAliasNodes); - } - - return singleValueNode; + singleValueNode = TranslateParameterAlias(singleValueNode, parameterAliasNodes); } - // Parameter alias value is assumed to be null if it is not found. - // Do not need to translate the parameter alias node from the query string - // because this method only deals with the parameter alias node mapping from ODL parser. - return null; + return singleValueNode; } + + // Parameter alias value is assumed to be null if it is not found. + // Do not need to translate the parameter alias node from the query string + // because this method only deals with the parameter alias node mapping from ODL parser. + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/ODataRouteDebugMiddleware.cs b/src/Microsoft.AspNetCore.OData/Routing/ODataRouteDebugMiddleware.cs index 0f682514d..30f52cd56 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/ODataRouteDebugMiddleware.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/ODataRouteDebugMiddleware.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -20,241 +20,240 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.OData.Routing +namespace Microsoft.AspNetCore.OData.Routing; + +/// +/// An OData route debug middleware +/// +[ExcludeFromCodeCoverage] +internal class ODataRouteDebugMiddleware { + private static IReadOnlyList EmptyHeaders = Array.Empty(); + private readonly RequestDelegate _next; + private readonly string _routePattern; + /// - /// An OData route debug middleware + /// Initializes a new instance of the class. /// - [ExcludeFromCodeCoverage] - internal class ODataRouteDebugMiddleware + /// The route pattern. + /// The next middleware. + public ODataRouteDebugMiddleware(string routePattern, RequestDelegate next) { - private static IReadOnlyList EmptyHeaders = Array.Empty(); - private readonly RequestDelegate _next; - private readonly string _routePattern; - - /// - /// Initializes a new instance of the class. - /// - /// The route pattern. - /// The next middleware. - public ODataRouteDebugMiddleware(string routePattern, RequestDelegate next) + if (routePattern == null) { - if (routePattern == null) - { - throw Error.ArgumentNull(nameof(routePattern)); - } - - // ensure _routePattern starts with / - _routePattern = routePattern.StartsWith('/') ? routePattern : $"/{routePattern}"; - _next = next ?? throw Error.ArgumentNull(nameof(next)); ; + throw Error.ArgumentNull(nameof(routePattern)); } - /// - /// Invoke the OData Route debug middleware. - /// - /// The http context. - /// A task that can be awaited. - public async Task Invoke(HttpContext context) + // ensure _routePattern starts with / + _routePattern = routePattern.StartsWith('/') ? routePattern : $"/{routePattern}"; + _next = next ?? throw Error.ArgumentNull(nameof(next)); ; + } + + /// + /// Invoke the OData Route debug middleware. + /// + /// The http context. + /// A task that can be awaited. + public async Task Invoke(HttpContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - HttpRequest request = context.Request; + HttpRequest request = context.Request; - if (string.Equals(request.Path.Value, _routePattern, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(request.Path.Value, _routePattern, StringComparison.OrdinalIgnoreCase)) + { + var routeInfoList = GetRouteInfo(context); + if (AcceptsJson(request.Headers)) { - var routeInfoList = GetRouteInfo(context); - if (AcceptsJson(request.Headers)) - { - await WriteRoutesAsJson(context, routeInfoList).ConfigureAwait(false); - } - else - { - await WriteRoutesAsHtml(context, routeInfoList).ConfigureAwait(false); - } + await WriteRoutesAsJson(context, routeInfoList).ConfigureAwait(false); } else { - await _next(context).ConfigureAwait(false); + await WriteRoutesAsHtml(context, routeInfoList).ConfigureAwait(false); } } - - internal static IList GetRouteInfo(HttpContext context) + else { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - var routInfoList = new List(); - var dataSource = context.RequestServices.GetRequiredService(); - foreach (var endpoint in dataSource.Endpoints) - { - ControllerActionDescriptor controllerActionDescriptor = endpoint.Metadata.GetMetadata(); - if (controllerActionDescriptor == null) - { - continue; - } - - var routeEndpoint = endpoint as RouteEndpoint; - var metadata = endpoint.Metadata.GetMetadata(); - - var info = new EndpointRouteInfo - { - DisplayName = endpoint.DisplayName, - HttpMethods = endpoint.Metadata.GetMetadata()?.HttpMethods ?? EmptyHeaders, - Pattern = routeEndpoint?.RoutePattern?.RawText ?? "N/A", - IsODataRoute = metadata != null, - IsConventional = metadata != null ? metadata.IsConventional : false - }; - - routInfoList.Add(info); - } - - return routInfoList; + await _next(context).ConfigureAwait(false); } + } - internal static async Task WriteRoutesAsJson(HttpContext context, IList routeInfoList) + internal static IList GetRouteInfo(HttpContext context) + { + if (context == null) { - var options = new JsonSerializerOptions() - { - WriteIndented = true - }; - string output = JsonSerializer.Serialize(routeInfoList, options); - context.Response.ContentType = MediaTypeNames.Application.Json; - await context.Response.WriteAsync(output).ConfigureAwait(false); + throw Error.ArgumentNull(nameof(context)); } - internal static async Task WriteRoutesAsHtml(HttpContext context, IList routeInfoList) + var routInfoList = new List(); + var dataSource = context.RequestServices.GetRequiredService(); + foreach (var endpoint in dataSource.Endpoints) { - if (context == null) + ControllerActionDescriptor controllerActionDescriptor = endpoint.Metadata.GetMetadata(); + if (controllerActionDescriptor == null) { - throw Error.ArgumentNull(nameof(context)); + continue; } - var stdRouteTable = new StringBuilder(); - var odataRouteTable = new StringBuilder(); - foreach (var routeInfo in routeInfoList) - { - if (routeInfo.IsODataRoute) - { - AppendRoute(odataRouteTable, routeInfo); - } - else - { - AppendRoute(stdRouteTable, routeInfo); - } - } + var routeEndpoint = endpoint as RouteEndpoint; + var metadata = endpoint.Metadata.GetMetadata(); - string output = ODataRouteMappingHtmlTemplate; - output = output.Replace("ODATA_ROUTE_CONTENT", odataRouteTable.ToString(), StringComparison.Ordinal); - output = output.Replace("STD_ROUTE_CONTENT", stdRouteTable.ToString(), StringComparison.Ordinal); + var info = new EndpointRouteInfo + { + DisplayName = endpoint.DisplayName, + HttpMethods = endpoint.Metadata.GetMetadata()?.HttpMethods ?? EmptyHeaders, + Pattern = routeEndpoint?.RoutePattern?.RawText ?? "N/A", + IsODataRoute = metadata != null, + IsConventional = metadata != null ? metadata.IsConventional : false + }; - context.Response.ContentType = "text/html"; - await context.Response.WriteAsync(output).ConfigureAwait(false); + routInfoList.Add(info); } - internal static bool AcceptsJson(IHeaderDictionary headers) + return routInfoList; + } + + internal static async Task WriteRoutesAsJson(HttpContext context, IList routeInfoList) + { + var options = new JsonSerializerOptions() { - var acceptHeaders = MediaTypeHeaderValue.ParseList(headers[HeaderNames.Accept]); + WriteIndented = true + }; + string output = JsonSerializer.Serialize(routeInfoList, options); + context.Response.ContentType = MediaTypeNames.Application.Json; + await context.Response.WriteAsync(output).ConfigureAwait(false); + } - var result = acceptHeaders.Any(h => - h.IsSubsetOf(new MediaTypeHeaderValue(MediaTypeNames.Application.Json))); - return result; + internal static async Task WriteRoutesAsHtml(HttpContext context, IList routeInfoList) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); } - [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "The default format provider is fine here.")] - private static void AppendRoute(StringBuilder builder, EndpointRouteInfo routeInfo) + var stdRouteTable = new StringBuilder(); + var odataRouteTable = new StringBuilder(); + foreach (var routeInfo in routeInfoList) { - builder.Append(""); - builder.Append($"{routeInfo.DisplayName}"); - builder.Append($"{string.Join(",", routeInfo.HttpMethods)}"); - - if (routeInfo.Pattern == null) - { - builder.Append($"N/A"); - } - else if (routeInfo.HttpMethods.Contains("GET")) + if (routeInfo.IsODataRoute) { - builder.Append($"{routeInfo.Pattern}"); + AppendRoute(odataRouteTable, routeInfo); } else { - builder.Append($"{routeInfo.Pattern}"); + AppendRoute(stdRouteTable, routeInfo); } + } - if (routeInfo.IsODataRoute) - { - var isConventional = routeInfo.IsConventional ? "Yes" : "-"; - builder.Append($"{isConventional}"); - } + string output = ODataRouteMappingHtmlTemplate; + output = output.Replace("ODATA_ROUTE_CONTENT", odataRouteTable.ToString(), StringComparison.Ordinal); + output = output.Replace("STD_ROUTE_CONTENT", stdRouteTable.ToString(), StringComparison.Ordinal); - builder.AppendLine(""); - } + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync(output).ConfigureAwait(false); + } + internal static bool AcceptsJson(IHeaderDictionary headers) + { + var acceptHeaders = MediaTypeHeaderValue.ParseList(headers[HeaderNames.Accept]); - private static string ODataRouteMappingHtmlTemplate = @" - - OData Endpoint Routing Debugger - + + if (routeInfo.IsODataRoute) + { + var isConventional = routeInfo.IsConventional ? "Yes" : "-"; + builder.Append($"{isConventional}"); + } + + builder.AppendLine(""); + } + + + private static string ODataRouteMappingHtmlTemplate = @" + +OData Endpoint Routing Debugger + -

OData Endpoint Mappings

-

- Go to non-OData endpoint mappings -

- - - - - - - - ODATA_ROUTE_CONTENT -
Controller & Action HttpMethods Template IsConventional
-

Non-OData Endpoint Mappings

-

- Go to OData endpoint mappings -

- - - - - - - STD_ROUTE_CONTENT -
Controller HttpMethods Template
+

OData Endpoint Mappings

+

+ Go to non-OData endpoint mappings +

+ + + + + + + + ODATA_ROUTE_CONTENT +
Controller & Action HttpMethods Template IsConventional
+

Non-OData Endpoint Mappings

+

+ Go to OData endpoint mappings +

+ + + + + + + STD_ROUTE_CONTENT +
Controller HttpMethods Template
"; - internal class EndpointRouteInfo - { - public string DisplayName { get; set; } + internal class EndpointRouteInfo + { + public string DisplayName { get; set; } - public IReadOnlyList HttpMethods { get; set; } + public IReadOnlyList HttpMethods { get; set; } - public string Pattern { get; set; } + public string Pattern { get; set; } - public bool IsODataRoute { get; set; } + public bool IsODataRoute { get; set; } - public bool IsConventional { get; set; } - } + public bool IsConventional { get; set; } } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/ODataRouteOptions.cs b/src/Microsoft.AspNetCore.OData/Routing/ODataRouteOptions.cs index e5ecc9fb5..82413b81e 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/ODataRouteOptions.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/ODataRouteOptions.cs @@ -7,138 +7,137 @@ using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Routing +namespace Microsoft.AspNetCore.OData.Routing; + +/// +/// Represents the configurable options on a conventional routing template building. +/// +public class ODataRouteOptions { + // The default route options. + internal static readonly ODataRouteOptions Default = new ODataRouteOptions(); + + private bool _enableKeyInParenthesis; + private bool _enableKeyAsSegment; + private bool _enableQualifiedOperationCall; + private bool _enableUnqualifiedOperationCall; + /// - /// Represents the configurable options on a conventional routing template building. + /// Initializes a new instance of the class. /// - public class ODataRouteOptions + public ODataRouteOptions() { - // The default route options. - internal static readonly ODataRouteOptions Default = new ODataRouteOptions(); - - private bool _enableKeyInParenthesis; - private bool _enableKeyAsSegment; - private bool _enableQualifiedOperationCall; - private bool _enableUnqualifiedOperationCall; - - /// - /// Initializes a new instance of the class. - /// - public ODataRouteOptions() - { - _enableKeyInParenthesis = true; - _enableKeyAsSegment = true; - _enableQualifiedOperationCall = true; - _enableUnqualifiedOperationCall = true; - } + _enableKeyInParenthesis = true; + _enableKeyAsSegment = true; + _enableQualifiedOperationCall = true; + _enableUnqualifiedOperationCall = true; + } - /// - /// Gets or sets the route order in conventional routing. - /// - public int? Order { get; set; } - - /// - /// Gets/sets a value indicating whether to enable $count in conventional routing. - /// - public bool EnableDollarCountRouting { get; set; } = true; - - /// - /// Gets/sets a value indicating whether to enable $value in conventional routing. - /// - public bool EnableDollarValueRouting { get; set; } = true; - - /// - /// Gets/sets a value indicating whether to enable case insensitive for the action name in conventional routing. - /// - public bool EnableActionNameCaseInsensitive { get; set; } = false; + /// + /// Gets or sets the route order in conventional routing. + /// + public int? Order { get; set; } + + /// + /// Gets/sets a value indicating whether to enable $count in conventional routing. + /// + public bool EnableDollarCountRouting { get; set; } = true; + + /// + /// Gets/sets a value indicating whether to enable $value in conventional routing. + /// + public bool EnableDollarValueRouting { get; set; } = true; + + /// + /// Gets/sets a value indicating whether to enable case insensitive for the action name in conventional routing. + /// + public bool EnableActionNameCaseInsensitive { get; set; } = false; - /// - /// Gets/sets a value indicating whether to enable case insensitive for the controller name in conventional routing. - /// - public bool EnableControllerNameCaseInsensitive { get; set; } = false; - - /// - /// Gets/sets a value indicating whether to enable case insensitive for the property name in conventional routing. - /// - public bool EnablePropertyNameCaseInsensitive { get; set; } = false; - - /// - /// Gets or sets a value indicating whether to generate odata path template as ~/entityset({key}). - /// Used in conventional routing. - /// - public bool EnableKeyInParenthesis + /// + /// Gets/sets a value indicating whether to enable case insensitive for the controller name in conventional routing. + /// + public bool EnableControllerNameCaseInsensitive { get; set; } = false; + + /// + /// Gets/sets a value indicating whether to enable case insensitive for the property name in conventional routing. + /// + public bool EnablePropertyNameCaseInsensitive { get; set; } = false; + + /// + /// Gets or sets a value indicating whether to generate odata path template as ~/entityset({key}). + /// Used in conventional routing. + /// + public bool EnableKeyInParenthesis + { + get => _enableKeyInParenthesis; + set { - get => _enableKeyInParenthesis; - set + if (!value && !_enableKeyAsSegment) { - if (!value && !_enableKeyAsSegment) - { - throw new ODataException(SRResources.RouteOptionDisabledKeySegment); - } - - _enableKeyInParenthesis = value; + throw new ODataException(SRResources.RouteOptionDisabledKeySegment); } + + _enableKeyInParenthesis = value; } + } - /// - /// Gets or sets a value indicating whether to generate odata path template as ~/entityset/{key} - /// Used in conventional routing. - /// - public bool EnableKeyAsSegment + /// + /// Gets or sets a value indicating whether to generate odata path template as ~/entityset/{key} + /// Used in conventional routing. + /// + public bool EnableKeyAsSegment + { + get => _enableKeyAsSegment; + set { - get => _enableKeyAsSegment; - set + if (!value && !_enableKeyInParenthesis) { - if (!value && !_enableKeyInParenthesis) - { - throw new ODataException(SRResources.RouteOptionDisabledKeySegment); - } - - _enableKeyAsSegment = value; + throw new ODataException(SRResources.RouteOptionDisabledKeySegment); } + + _enableKeyAsSegment = value; } + } - /// - /// Gets or sets a value indicating whether to generate odata path template as ~/Namespace.MyFunction(parameters...) - /// Used in conventional routing. - /// - public bool EnableQualifiedOperationCall + /// + /// Gets or sets a value indicating whether to generate odata path template as ~/Namespace.MyFunction(parameters...) + /// Used in conventional routing. + /// + public bool EnableQualifiedOperationCall + { + get => _enableQualifiedOperationCall; + set { - get => _enableQualifiedOperationCall; - set + if (!value && !_enableUnqualifiedOperationCall) { - if (!value && !_enableUnqualifiedOperationCall) - { - throw new ODataException(SRResources.RouteOptionDisabledOperationSegment); - } - - _enableQualifiedOperationCall = value; + throw new ODataException(SRResources.RouteOptionDisabledOperationSegment); } + + _enableQualifiedOperationCall = value; } + } - /// - /// Gets or sets a value indicating whether to generate odata path template as ~/MyFunction(parameters...) - /// Used in conventional routing. - /// - public bool EnableUnqualifiedOperationCall + /// + /// Gets or sets a value indicating whether to generate odata path template as ~/MyFunction(parameters...) + /// Used in conventional routing. + /// + public bool EnableUnqualifiedOperationCall + { + get => _enableUnqualifiedOperationCall; + set { - get => _enableUnqualifiedOperationCall; - set + if (!value && !_enableQualifiedOperationCall) { - if (!value && !_enableQualifiedOperationCall) - { - throw new ODataException(SRResources.RouteOptionDisabledOperationSegment); - } - - _enableUnqualifiedOperationCall = value; + throw new ODataException(SRResources.RouteOptionDisabledOperationSegment); } - } - /// - /// Gets or sets a value indicating whether to generate non parenthesis for non-parameter function. - /// Used in conventional routing. - /// - public bool EnableNonParenthesisForEmptyParameterFunction { get; set; } = false; + _enableUnqualifiedOperationCall = value; + } } + + /// + /// Gets or sets a value indicating whether to generate non parenthesis for non-parameter function. + /// Used in conventional routing. + /// + public bool EnableNonParenthesisForEmptyParameterFunction { get; set; } = false; } diff --git a/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingApplicationModelProvider.cs b/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingApplicationModelProvider.cs index f26e748c1..ab46cc8a8 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingApplicationModelProvider.cs @@ -16,154 +16,153 @@ using Microsoft.Extensions.Options; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing +namespace Microsoft.AspNetCore.OData.Routing; + +/// +/// Builds or modifies for OData convention action discovery. +/// +internal class ODataRoutingApplicationModelProvider : IApplicationModelProvider { + private readonly IODataControllerActionConvention[] _controllerActionConventions; + private readonly ODataOptions _options; + /// - /// Builds or modifies for OData convention action discovery. + /// Initializes a new instance of the class. /// - internal class ODataRoutingApplicationModelProvider : IApplicationModelProvider + /// The registered OData options. + public ODataRoutingApplicationModelProvider(IOptions options) { - private readonly IODataControllerActionConvention[] _controllerActionConventions; - private readonly ODataOptions _options; - - /// - /// Initializes a new instance of the class. - /// - /// The registered OData options. - public ODataRoutingApplicationModelProvider(IOptions options) + _options = options.Value; + _controllerActionConventions = _options.Conventions + .Where(c => c.GetType() != typeof(AttributeRoutingConvention)).OrderBy(p => p.Order).ToArray(); + } + + /// + /// Gets the order value for determining the order of execution of providers. + /// + public int Order => 100; + + /// + /// Executed for the second pass of built. + /// + /// The . + public void OnProvidersExecuted(ApplicationModelProviderContext context) + { + // apply attribute routing. + if (_options.EnableAttributeRouting) { - _options = options.Value; - _controllerActionConventions = _options.Conventions - .Where(c => c.GetType() != typeof(AttributeRoutingConvention)).OrderBy(p => p.Order).ToArray(); + ApplyAttributeRouting(context.Result.Controllers); } - /// - /// Gets the order value for determining the order of execution of providers. - /// - public int Order => 100; - - /// - /// Executed for the second pass of built. - /// - /// The . - public void OnProvidersExecuted(ApplicationModelProviderContext context) + // apply non-attribute convention routing. + foreach (var route in _options.RouteComponents) { - // apply attribute routing. - if (_options.EnableAttributeRouting) + IEdmModel model = route.Value.EdmModel; + if (model == null || model.EntityContainer == null) { - ApplyAttributeRouting(context.Result.Controllers); + continue; } - // apply non-attribute convention routing. - foreach (var route in _options.RouteComponents) + foreach (var controller in context.Result.Controllers) { - IEdmModel model = route.Value.EdmModel; - if (model == null || model.EntityContainer == null) + // Skip the controller with [NonODataController] attribute decorated. + if (controller.HasAttribute()) { continue; } - foreach (var controller in context.Result.Controllers) + // Apply to ODataModelAttribute + if (!CanApply(route.Key, () => controller.GetAttribute())) { - // Skip the controller with [NonODataController] attribute decorated. - if (controller.HasAttribute()) - { - continue; - } - - // Apply to ODataModelAttribute - if (!CanApply(route.Key, () => controller.GetAttribute())) - { - continue; - } + continue; + } - ODataControllerActionContext odataContext = new ODataControllerActionContext(route.Key, model, controller); + ODataControllerActionContext odataContext = new ODataControllerActionContext(route.Key, model, controller); - odataContext.NavigationSource = model.ResolveNavigationSource(controller.ControllerName, - _options.RouteOptions.EnableControllerNameCaseInsensitive); + odataContext.NavigationSource = model.ResolveNavigationSource(controller.ControllerName, + _options.RouteOptions.EnableControllerNameCaseInsensitive); - odataContext.Options = _options; + odataContext.Options = _options; - IODataControllerActionConvention[] conventions = - _controllerActionConventions.Where(c => c.AppliesToController(odataContext)).ToArray(); + IODataControllerActionConvention[] conventions = + _controllerActionConventions.Where(c => c.AppliesToController(odataContext)).ToArray(); - if (conventions.Length > 0) + if (conventions.Length > 0) + { + foreach (var action in controller.Actions.Where(a => !a.IsODataIgnored())) { - foreach (var action in controller.Actions.Where(a => !a.IsODataIgnored())) + if (!CanApply(route.Key, () => action.GetAttribute())) { - if (!CanApply(route.Key, () => action.GetAttribute())) - { - continue; - } + continue; + } - // Reset the action on the context. - odataContext.Action = action; + // Reset the action on the context. + odataContext.Action = action; - foreach (var convention in conventions) + foreach (var convention in conventions) + { + if (convention.AppliesToAction(odataContext)) { - if (convention.AppliesToAction(odataContext)) - { - break; - } + break; } } } } } } + } - /// - /// Executed for the first pass of building. - /// - /// The . - public void OnProvidersExecuting(ApplicationModelProviderContext context) + /// + /// Executed for the first pass of building. + /// + /// The . + public void OnProvidersExecuting(ApplicationModelProviderContext context) + { + // Nothing here. + } + + /// + /// Apply Default OData attribute routing + /// + /// The controller models + internal void ApplyAttributeRouting(IList controllers) + { + AttributeRoutingConvention attributeRouting = _options.Conventions.OfType().FirstOrDefault(); + if (attributeRouting == null) { - // Nothing here. + return; } - /// - /// Apply Default OData attribute routing - /// - /// The controller models - internal void ApplyAttributeRouting(IList controllers) + ODataControllerActionContext controllerActionContext = new ODataControllerActionContext { - AttributeRoutingConvention attributeRouting = _options.Conventions.OfType().FirstOrDefault(); - if (attributeRouting == null) - { - return; - } + Options = _options + }; - ODataControllerActionContext controllerActionContext = new ODataControllerActionContext - { - Options = _options - }; + foreach (var controllerModel in controllers.Where(c => !c.IsODataIgnored())) + { + controllerActionContext.Controller = controllerModel; - foreach (var controllerModel in controllers.Where(c => !c.IsODataIgnored())) + foreach (var actionModel in controllerModel.Actions.Where(a => !a.IsODataIgnored())) { - controllerActionContext.Controller = controllerModel; - - foreach (var actionModel in controllerModel.Actions.Where(a => !a.IsODataIgnored())) - { - controllerActionContext.Action = actionModel; + controllerActionContext.Action = actionModel; - attributeRouting.AppliesToAction(controllerActionContext); - } + attributeRouting.AppliesToAction(controllerActionContext); } } + } - internal static bool CanApply(string prefix, Func func) + internal static bool CanApply(string prefix, Func func) + { + ODataRouteComponentAttribute odataModel = func?.Invoke(); + if (odataModel == null) { - ODataRouteComponentAttribute odataModel = func?.Invoke(); - if (odataModel == null) - { - return true; // apply to all model - } - else if (prefix == odataModel.RoutePrefix) - { - return true; - } - - return false; + return true; // apply to all model } + else if (prefix == odataModel.RoutePrefix) + { + return true; + } + + return false; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingMatcherPolicy.cs b/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingMatcherPolicy.cs index 2e85edbd9..ccfe40f85 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingMatcherPolicy.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingMatcherPolicy.cs @@ -16,117 +16,116 @@ using Microsoft.AspNetCore.Routing.Matching; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing +namespace Microsoft.AspNetCore.OData.Routing; + +/// +/// Defines a policy that applies behaviors to the OData Uri matcher. +/// +internal class ODataRoutingMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy { + private IODataTemplateTranslator _translator; + /// - /// Defines a policy that applies behaviors to the OData Uri matcher. + /// Initializes a new instance of the class. /// - internal class ODataRoutingMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy + /// The registered path template translator. + public ODataRoutingMatcherPolicy(IODataTemplateTranslator translator) { - private IODataTemplateTranslator _translator; + _translator = translator; + } + + /// + /// Gets a value that determines the order of this policy. + /// + public override int Order => 900; - /// - /// Initializes a new instance of the class. - /// - /// The registered path template translator. - public ODataRoutingMatcherPolicy(IODataTemplateTranslator translator) + /// + /// Returns a value that indicates whether the matcher applies to any endpoint in endpoints. + /// + /// The set of candidate values. + /// true if the policy applies to any endpoint in endpoints, otherwise false. + public bool AppliesToEndpoints(IReadOnlyList endpoints) + { + return endpoints.Any(e => e.Metadata.OfType().FirstOrDefault() != null); + } + + /// + /// Applies the policy to the CandidateSet. + /// + /// The context associated with the current request. + /// The CandidateSet. + /// The task. + public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + { + if (httpContext == null) { - _translator = translator; + throw Error.ArgumentNull(nameof(httpContext)); } - /// - /// Gets a value that determines the order of this policy. - /// - public override int Order => 900; - - /// - /// Returns a value that indicates whether the matcher applies to any endpoint in endpoints. - /// - /// The set of candidate values. - /// true if the policy applies to any endpoint in endpoints, otherwise false. - public bool AppliesToEndpoints(IReadOnlyList endpoints) + IODataFeature odataFeature = httpContext.ODataFeature(); + if (odataFeature.Path != null) { - return endpoints.Any(e => e.Metadata.OfType().FirstOrDefault() != null); + // If we have the OData path setting, it means there's some Policy working. + // Let's skip this default OData matcher policy. + return Task.CompletedTask; } - /// - /// Applies the policy to the CandidateSet. - /// - /// The context associated with the current request. - /// The CandidateSet. - /// The task. - public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + // The goal of this method is to perform the final matching: + // Map between route values matched by the template and the ones we want to expose to the action for binding. + // (tweaking the route values is fine here) + // Invalidating the candidate if the key/function values are not valid/missing. + // Perform overload resolution for functions by looking at the candidates and their metadata. + for (var i = 0; i < candidates.Count; i++) { - if (httpContext == null) + ref CandidateState candidate = ref candidates[i]; + if (!candidates.IsValidCandidate(i)) { - throw Error.ArgumentNull(nameof(httpContext)); + continue; } - IODataFeature odataFeature = httpContext.ODataFeature(); - if (odataFeature.Path != null) + IODataRoutingMetadata metadata = candidate.Endpoint.Metadata.OfType().FirstOrDefault(); + if (metadata == null) { - // If we have the OData path setting, it means there's some Policy working. - // Let's skip this default OData matcher policy. - return Task.CompletedTask; + continue; } - // The goal of this method is to perform the final matching: - // Map between route values matched by the template and the ones we want to expose to the action for binding. - // (tweaking the route values is fine here) - // Invalidating the candidate if the key/function values are not valid/missing. - // Perform overload resolution for functions by looking at the candidates and their metadata. - for (var i = 0; i < candidates.Count; i++) + if (odataFeature.Path != null) { - ref CandidateState candidate = ref candidates[i]; - if (!candidates.IsValidCandidate(i)) - { - continue; - } - - IODataRoutingMetadata metadata = candidate.Endpoint.Metadata.OfType().FirstOrDefault(); - if (metadata == null) - { - continue; - } - - if (odataFeature.Path != null) - { - // If it's odata endpoint, and we have a path set, let other odata endpoints invalid. - candidates.SetValidity(i, false); - continue; - } - - ODataTemplateTranslateContext translatorContext = - new ODataTemplateTranslateContext(httpContext, candidate.Endpoint, candidate.Values, metadata.Model); - - ODataPath odataPath = _translator.Translate(metadata.Template, translatorContext); - if (odataPath != null) - { - odataFeature.RoutePrefix = metadata.Prefix; - odataFeature.Model = metadata.Model; - odataFeature.Path = odataPath; - - MergeRouteValues(translatorContext.UpdatedValues, candidate.Values); - - // Shall we break the remaining candidates? - // So far the answer is no. Because we can use this matcher to obsolete the unmatched endpoint. - // break; - } - else - { - candidates.SetValidity(i, false); - } + // If it's odata endpoint, and we have a path set, let other odata endpoints invalid. + candidates.SetValidity(i, false); + continue; } - return Task.CompletedTask; - } + ODataTemplateTranslateContext translatorContext = + new ODataTemplateTranslateContext(httpContext, candidate.Endpoint, candidate.Values, metadata.Model); - private static void MergeRouteValues(RouteValueDictionary updates, RouteValueDictionary source) - { - foreach (var data in updates) + ODataPath odataPath = _translator.Translate(metadata.Template, translatorContext); + if (odataPath != null) { - source[data.Key] = data.Value; + odataFeature.RoutePrefix = metadata.Prefix; + odataFeature.Model = metadata.Model; + odataFeature.Path = odataPath; + + MergeRouteValues(translatorContext.UpdatedValues, candidate.Values); + + // Shall we break the remaining candidates? + // So far the answer is no. Because we can use this matcher to obsolete the unmatched endpoint. + // break; } + else + { + candidates.SetValidity(i, false); + } + } + + return Task.CompletedTask; + } + + private static void MergeRouteValues(RouteValueDictionary updates, RouteValueDictionary source) + { + foreach (var data in updates) + { + source[data.Key] = data.Value; } } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingMetadata.cs b/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingMetadata.cs index bea577157..fde73455a 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingMetadata.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingMetadata.cs @@ -9,52 +9,51 @@ using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing +namespace Microsoft.AspNetCore.OData.Routing; + +/// +/// Represents OData Endpoint metadata used during routing. +/// +public sealed class ODataRoutingMetadata : IODataRoutingMetadata { /// - /// Represents OData Endpoint metadata used during routing. + /// Initializes a new instance of the class. /// - public sealed class ODataRoutingMetadata : IODataRoutingMetadata + /// The prefix string. + /// The Edm model. + /// The Routing path template. + public ODataRoutingMetadata(string prefix, IEdmModel model, ODataPathTemplate template) { - /// - /// Initializes a new instance of the class. - /// - /// The prefix string. - /// The Edm model. - /// The Routing path template. - public ODataRoutingMetadata(string prefix, IEdmModel model, ODataPathTemplate template) - { - Prefix = prefix ?? throw new ArgumentNullException(nameof(prefix)); - Model = model ?? throw new ArgumentNullException(nameof(model)); - Template = template ?? throw new ArgumentNullException(nameof(template)); - } - - /// - /// Initializes a new instance of the class. - /// For unit test only - /// - internal ODataRoutingMetadata() - { - } - - /// - /// Gets the prefix string. - /// - public string Prefix { get; } - - /// - /// Gets the Edm model. - /// - public IEdmModel Model { get; } - - /// - /// Gets the OData path template - /// - public ODataPathTemplate Template { get; } - - /// - /// Gets or sets a boolean value indicating from odata conventional routing. - /// - public bool IsConventional { get; set; } = true; + Prefix = prefix ?? throw new ArgumentNullException(nameof(prefix)); + Model = model ?? throw new ArgumentNullException(nameof(model)); + Template = template ?? throw new ArgumentNullException(nameof(template)); } + + /// + /// Initializes a new instance of the class. + /// For unit test only + /// + internal ODataRoutingMetadata() + { + } + + /// + /// Gets the prefix string. + /// + public string Prefix { get; } + + /// + /// Gets the Edm model. + /// + public IEdmModel Model { get; } + + /// + /// Gets the OData path template + /// + public ODataPathTemplate Template { get; } + + /// + /// Gets or sets a boolean value indicating from odata conventional routing. + /// + public bool IsConventional { get; set; } = true; } diff --git a/src/Microsoft.AspNetCore.OData/Routing/ODataSegmentKinds.cs b/src/Microsoft.AspNetCore.OData/Routing/ODataSegmentKinds.cs index 64e05296a..ccf34e0f2 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/ODataSegmentKinds.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/ODataSegmentKinds.cs @@ -5,106 +5,105 @@ //
//------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Routing +namespace Microsoft.AspNetCore.OData.Routing; + +/// +/// Provides the values of segment kinds for implementations of odata path template. +/// +public static class ODataSegmentKinds { /// - /// Provides the values of segment kinds for implementations of odata path template. - /// - public static class ODataSegmentKinds - { - /// - /// Represents the service root segment (for OData service document). - /// - public const string ServiceBase = "~"; - - /// - /// Represents the OData $batch segment. - /// - public const string Batch = "$batch"; - - /// - /// Represents the OData $ref segment. - /// - public const string Ref = "$ref"; - - /// - /// Represents the OData $metadata segment. - /// - public const string Metadata = "$metadata"; - - /// - /// Represents the OData $value segment. - /// - public const string Value = "$value"; - - /// - /// Represents the OData $count segment. - /// - public const string Count = "$count"; - - /// - /// Represents a segment indicating a bound OData action. - /// - public const string Action = "action"; - - /// - /// Represents a segment indicating a bound OData function. - /// - public const string Function = "function"; - - /// - /// Represents a segment indicating an unbound OData action. - /// - public const string UnboundAction = "unboundaction"; - - /// - /// Represents a segment indicating an unbound OData function. - /// - public const string UnboundFunction = "unboundfunction"; - - /// - /// Represents a segment indicating a type cast. - /// - public const string Cast = "cast"; - - /// - /// Represents a segment indicating an entity set. - /// - public const string EntitySet = "entityset"; - - /// - /// Represents a segment indicating a singleton. - /// - public const string Singleton = "singleton"; - - /// - /// Represents a segment indicating an index by key operation. - /// - public const string Key = "key"; - - /// - /// Represents a segment indicating a navigation. - /// - public const string Navigation = "navigation"; - - /// - /// Represents a segment indicating a navigation link. - /// - public const string PathTemplate = "template"; - - /// - /// Represents a segment indicating a property access. - /// - public const string Property = "property"; - - /// - /// Represents a segment indicating an dynamic property access. - /// - public const string DynamicProperty = "dynamicproperty"; - - /// - /// Represents a segment that is not understood. - /// - public const string Unresolved = "unresolved"; - } + /// Represents the service root segment (for OData service document). + /// + public const string ServiceBase = "~"; + + /// + /// Represents the OData $batch segment. + /// + public const string Batch = "$batch"; + + /// + /// Represents the OData $ref segment. + /// + public const string Ref = "$ref"; + + /// + /// Represents the OData $metadata segment. + /// + public const string Metadata = "$metadata"; + + /// + /// Represents the OData $value segment. + /// + public const string Value = "$value"; + + /// + /// Represents the OData $count segment. + /// + public const string Count = "$count"; + + /// + /// Represents a segment indicating a bound OData action. + /// + public const string Action = "action"; + + /// + /// Represents a segment indicating a bound OData function. + /// + public const string Function = "function"; + + /// + /// Represents a segment indicating an unbound OData action. + /// + public const string UnboundAction = "unboundaction"; + + /// + /// Represents a segment indicating an unbound OData function. + /// + public const string UnboundFunction = "unboundfunction"; + + /// + /// Represents a segment indicating a type cast. + /// + public const string Cast = "cast"; + + /// + /// Represents a segment indicating an entity set. + /// + public const string EntitySet = "entityset"; + + /// + /// Represents a segment indicating a singleton. + /// + public const string Singleton = "singleton"; + + /// + /// Represents a segment indicating an index by key operation. + /// + public const string Key = "key"; + + /// + /// Represents a segment indicating a navigation. + /// + public const string Navigation = "navigation"; + + /// + /// Represents a segment indicating a navigation link. + /// + public const string PathTemplate = "template"; + + /// + /// Represents a segment indicating a property access. + /// + public const string Property = "property"; + + /// + /// Represents a segment indicating an dynamic property access. + /// + public const string DynamicProperty = "dynamicproperty"; + + /// + /// Represents a segment that is not understood. + /// + public const string Unresolved = "unresolved"; } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Parser/DefaultODataPathParser.cs b/src/Microsoft.AspNetCore.OData/Routing/Parser/DefaultODataPathParser.cs index 7bc43e73f..82a4193d5 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Parser/DefaultODataPathParser.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Parser/DefaultODataPathParser.cs @@ -11,40 +11,39 @@ using Microsoft.OData; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Parser +namespace Microsoft.AspNetCore.OData.Routing.Parser; + +/// +/// Exposes the ability to parse an OData path template as an . +/// +internal class DefaultODataPathParser : IODataPathParser { /// - /// Exposes the ability to parse an OData path template as an . + /// Parse the string like "/users/1/contactFolders/..." + /// to segments /// - internal class DefaultODataPathParser : IODataPathParser + /// the Edm model. + /// The service root uri. + /// the setting. + /// The service provider. + /// Null or . + public virtual ODataPath Parse(IEdmModel model, Uri serviceRoot, Uri odataPath, IServiceProvider requestProvider) { - /// - /// Parse the string like "/users/1/contactFolders/..." - /// to segments - /// - /// the Edm model. - /// The service root uri. - /// the setting. - /// The service provider. - /// Null or . - public virtual ODataPath Parse(IEdmModel model, Uri serviceRoot, Uri odataPath, IServiceProvider requestProvider) + ODataUriParser uriParser; + if (serviceRoot != null) + { + uriParser = new ODataUriParser(model, serviceRoot, odataPath, requestProvider); + } + else { - ODataUriParser uriParser; - if (serviceRoot != null) - { - uriParser = new ODataUriParser(model, serviceRoot, odataPath, requestProvider); - } - else - { - uriParser = new ODataUriParser(model, odataPath, requestProvider); - } + uriParser = new ODataUriParser(model, odataPath, requestProvider); + } - uriParser.Resolver = uriParser.Resolver ?? new UnqualifiedODataUriResolver { EnableCaseInsensitive = true }; - uriParser.UrlKeyDelimiter = ODataUrlKeyDelimiter.Slash; // support key in parentheses and key as segment. + uriParser.Resolver = uriParser.Resolver ?? new UnqualifiedODataUriResolver { EnableCaseInsensitive = true }; + uriParser.UrlKeyDelimiter = ODataUrlKeyDelimiter.Slash; // support key in parentheses and key as segment. - // The ParsePath throws OData exceptions if the odata path is not valid. - // That's expected. - return uriParser.ParsePath(); - } + // The ParsePath throws OData exceptions if the odata path is not valid. + // That's expected. + return uriParser.ParsePath(); } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Parser/DefaultODataPathTemplateParser.cs b/src/Microsoft.AspNetCore.OData/Routing/Parser/DefaultODataPathTemplateParser.cs index 394fe0b36..41ba63f16 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Parser/DefaultODataPathTemplateParser.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Parser/DefaultODataPathTemplateParser.cs @@ -11,54 +11,53 @@ using Microsoft.OData; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Parser +namespace Microsoft.AspNetCore.OData.Routing.Parser; + +/// +/// Exposes the ability to parse an OData path template as an . +/// +public class DefaultODataPathTemplateParser : IODataPathTemplateParser { /// - /// Exposes the ability to parse an OData path template as an . + /// Parse the string like "/users/{id}/contactFolders/{contactFolderId}/contacts" + /// to segments /// - public class DefaultODataPathTemplateParser : IODataPathTemplateParser + /// the Edm model. + /// the OData path. + /// The service provider. + /// Null or . + public virtual ODataPathTemplate Parse(IEdmModel model, string odataPath, IServiceProvider requestProvider) { - /// - /// Parse the string like "/users/{id}/contactFolders/{contactFolderId}/contacts" - /// to segments - /// - /// the Edm model. - /// the OData path. - /// The service provider. - /// Null or . - public virtual ODataPathTemplate Parse(IEdmModel model, string odataPath, IServiceProvider requestProvider) + if (model == null || string.IsNullOrEmpty(odataPath)) { - if (model == null || string.IsNullOrEmpty(odataPath)) - { - return null; - } + return null; + } - ODataUriParser uriParser; - if (requestProvider == null) - { - uriParser = new ODataUriParser(model, new Uri(odataPath, UriKind.Relative)); - } - else - { - uriParser = new ODataUriParser(model, new Uri(odataPath, UriKind.Relative), requestProvider); - } + ODataUriParser uriParser; + if (requestProvider == null) + { + uriParser = new ODataUriParser(model, new Uri(odataPath, UriKind.Relative)); + } + else + { + uriParser = new ODataUriParser(model, new Uri(odataPath, UriKind.Relative), requestProvider); + } - uriParser.EnableUriTemplateParsing = true; + uriParser.EnableUriTemplateParsing = true; - uriParser.Resolver = new UnqualifiedCallAndAlternateKeyResolver(model) - { - EnableCaseInsensitive = true - }; + uriParser.Resolver = new UnqualifiedCallAndAlternateKeyResolver(model) + { + EnableCaseInsensitive = true + }; - uriParser.UrlKeyDelimiter = ODataUrlKeyDelimiter.Slash; // support key in parentheses and key as segment. + uriParser.UrlKeyDelimiter = ODataUrlKeyDelimiter.Slash; // support key in parentheses and key as segment. - ODataPath path = uriParser.ParsePath(); + ODataPath path = uriParser.ParsePath(); - // Templatify - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(model); - path.WalkWith(handler); + // Templatify + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(model); + path.WalkWith(handler); - return new ODataPathTemplate(handler.Templates); - } + return new ODataPathTemplate(handler.Templates); } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Parser/IODataPathParser.cs b/src/Microsoft.AspNetCore.OData/Routing/Parser/IODataPathParser.cs index 57c6fd729..37e0bfe45 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Parser/IODataPathParser.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Parser/IODataPathParser.cs @@ -11,18 +11,17 @@ using Microsoft.OData; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Parser +namespace Microsoft.AspNetCore.OData.Routing.Parser; + +internal interface IODataPathParser { - internal interface IODataPathParser - { - /// - /// Parses the specified OData path template as an . - /// - /// The Edm model. - /// The service root uri. - /// The OData path template to parse. - /// The OData service provider. - /// A parsed representation of the template, or null if the template does not match the model. - ODataPath Parse(IEdmModel model, Uri serviceRoot, Uri odataPath, IServiceProvider requestProvider); - } + /// + /// Parses the specified OData path template as an . + /// + /// The Edm model. + /// The service root uri. + /// The OData path template to parse. + /// The OData service provider. + /// A parsed representation of the template, or null if the template does not match the model. + ODataPath Parse(IEdmModel model, Uri serviceRoot, Uri odataPath, IServiceProvider requestProvider); } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Parser/IODataPathTemplateParser.cs b/src/Microsoft.AspNetCore.OData/Routing/Parser/IODataPathTemplateParser.cs index 3f3cefe0f..234602477 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Parser/IODataPathTemplateParser.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Parser/IODataPathTemplateParser.cs @@ -9,20 +9,19 @@ using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Routing.Parser +namespace Microsoft.AspNetCore.OData.Routing.Parser; + +/// +/// Exposes the ability to parse an OData path template as an . +/// +public interface IODataPathTemplateParser { /// - /// Exposes the ability to parse an OData path template as an . + /// Parses the specified OData path template as an . /// - public interface IODataPathTemplateParser - { - /// - /// Parses the specified OData path template as an . - /// - /// The Edm model. - /// The OData path template to parse. - /// The OData service provider. - /// A parsed representation of the template, or null if the template does not match the model. - ODataPathTemplate Parse(IEdmModel model, string odataPath, IServiceProvider requestProvider); - } + /// The Edm model. + /// The OData path template to parse. + /// The OData service provider. + /// A parsed representation of the template, or null if the template does not match the model. + ODataPathTemplate Parse(IEdmModel model, string odataPath, IServiceProvider requestProvider); } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Parser/ODataPathSegmentToTemplateHandler.cs b/src/Microsoft.AspNetCore.OData/Routing/Parser/ODataPathSegmentToTemplateHandler.cs index 25860ad1c..2befc99f6 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Parser/ODataPathSegmentToTemplateHandler.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Parser/ODataPathSegmentToTemplateHandler.cs @@ -14,222 +14,221 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Parser +namespace Microsoft.AspNetCore.OData.Routing.Parser; + +/// +/// Handle an OData path to a path segment templates. +/// +internal class ODataPathSegmentToTemplateHandler : PathSegmentHandler { + private IEdmModel _model; + private IList _segmentTemplates; + /// - /// Handle an OData path to a path segment templates. + /// Initializes a new instance of the class. /// - internal class ODataPathSegmentToTemplateHandler : PathSegmentHandler + /// The Edm model. + public ODataPathSegmentToTemplateHandler(IEdmModel model) { - private IEdmModel _model; - private IList _segmentTemplates; + _model = model; + _segmentTemplates = new List(); + } - /// - /// Initializes a new instance of the class. - /// - /// The Edm model. - public ODataPathSegmentToTemplateHandler(IEdmModel model) - { - _model = model; - _segmentTemplates = new List(); - } + /// + /// Gets the templates. + /// + public IList Templates => _segmentTemplates; - /// - /// Gets the templates. - /// - public IList Templates => _segmentTemplates; + /// + /// Translate a + /// + /// the segment to Translate + public override void Handle(MetadataSegment segment) + { + _segmentTemplates.Add(MetadataSegmentTemplate.Instance); + } - /// - /// Translate a - /// - /// the segment to Translate - public override void Handle(MetadataSegment segment) - { - _segmentTemplates.Add(MetadataSegmentTemplate.Instance); - } + /// + /// Translate a . + /// + /// the segment to Translate + public override void Handle(ValueSegment segment) + { + _segmentTemplates.Add(new ValueSegmentTemplate(segment)); + } - /// - /// Translate a . - /// - /// the segment to Translate - public override void Handle(ValueSegment segment) - { - _segmentTemplates.Add(new ValueSegmentTemplate(segment)); - } + /// + /// Translate a . + /// + /// the segment to Translate + public override void Handle(NavigationPropertyLinkSegment segment) + { + _segmentTemplates.Add(new NavigationLinkSegmentTemplate(segment)); + } - /// - /// Translate a . - /// - /// the segment to Translate - public override void Handle(NavigationPropertyLinkSegment segment) - { - _segmentTemplates.Add(new NavigationLinkSegmentTemplate(segment)); - } + /// + /// Translate a . + /// + /// the segment to Translate + public override void Handle(CountSegment segment) + { + _segmentTemplates.Add(CountSegmentTemplate.Instance); + } - /// - /// Translate a . - /// - /// the segment to Translate - public override void Handle(CountSegment segment) - { - _segmentTemplates.Add(CountSegmentTemplate.Instance); - } + /// + /// Translate a . + /// + /// the segment to Translate + public override void Handle(DynamicPathSegment segment) + { + _segmentTemplates.Add(new DynamicSegmentTemplate(segment)); + } - /// - /// Translate a . - /// - /// the segment to Translate - public override void Handle(DynamicPathSegment segment) + /// + /// Translate a . + /// + /// the segment to Translate + public override void Handle(OperationSegment segment) + { + IEdmOperation operation = segment.Operations.First(); + if (operation.IsAction()) { - _segmentTemplates.Add(new DynamicSegmentTemplate(segment)); + _segmentTemplates.Add(new ActionSegmentTemplate(segment)); } - - /// - /// Translate a . - /// - /// the segment to Translate - public override void Handle(OperationSegment segment) + else { - IEdmOperation operation = segment.Operations.First(); - if (operation.IsAction()) - { - _segmentTemplates.Add(new ActionSegmentTemplate(segment)); - } - else - { - _segmentTemplates.Add(new FunctionSegmentTemplate(segment)); - } + _segmentTemplates.Add(new FunctionSegmentTemplate(segment)); } + } - /// - /// Translate a . - /// - /// the segment to Translate - public override void Handle(OperationImportSegment segment) + /// + /// Translate a . + /// + /// the segment to Translate + public override void Handle(OperationImportSegment segment) + { + if (segment.OperationImports.First().IsActionImport()) { - if (segment.OperationImports.First().IsActionImport()) - { - _segmentTemplates.Add(new ActionImportSegmentTemplate(segment)); - } - else - { - _segmentTemplates.Add(new FunctionImportSegmentTemplate(segment)); - } + _segmentTemplates.Add(new ActionImportSegmentTemplate(segment)); } - - /// - /// Translate a . - /// - /// the segment to Translate - public override void Handle(PropertySegment segment) + else { - _segmentTemplates.Add(new PropertySegmentTemplate(segment)); + _segmentTemplates.Add(new FunctionImportSegmentTemplate(segment)); } + } - /// - /// Translate a . - /// - /// the segment to Translate - public override void Handle(KeySegment segment) + /// + /// Translate a . + /// + /// the segment to Translate + public override void Handle(PropertySegment segment) + { + _segmentTemplates.Add(new PropertySegmentTemplate(segment)); + } + + /// + /// Translate a . + /// + /// the segment to Translate + public override void Handle(KeySegment segment) + { + Func BuildKeyTemplate = () => { - Func BuildKeyTemplate = () => + try { - try - { - return new KeySegmentTemplate(segment); - } - catch + return new KeySegmentTemplate(segment); + } + catch + { + if (_model != null) { - if (_model != null) + var alternateKeys = _model.ResolveAlternateKeyProperties(segment); + if (alternateKeys != null) { - var alternateKeys = _model.ResolveAlternateKeyProperties(segment); - if (alternateKeys != null) - { - return new KeySegmentTemplate(segment, alternateKeys); - } + return new KeySegmentTemplate(segment, alternateKeys); } - - throw; } - }; - KeySegmentTemplate keyTemplate = BuildKeyTemplate(); - - ODataSegmentTemplate previous = _segmentTemplates.LastOrDefault(); - NavigationLinkSegmentTemplate preRef = previous as NavigationLinkSegmentTemplate; - if (preRef != null) - { - preRef.Key = keyTemplate; + throw; } - else - { - _segmentTemplates.Add(keyTemplate); - } - } + }; - /// - /// Translate a . - /// - /// the segment to Translate - public override void Handle(SingletonSegment segment) - { - _segmentTemplates.Add(new SingletonSegmentTemplate(segment)); - } + KeySegmentTemplate keyTemplate = BuildKeyTemplate(); - /// - /// Translate a . - /// - /// the segment to Translate - public override void Handle(EntitySetSegment segment) + ODataSegmentTemplate previous = _segmentTemplates.LastOrDefault(); + NavigationLinkSegmentTemplate preRef = previous as NavigationLinkSegmentTemplate; + if (preRef != null) { - _segmentTemplates.Add(new EntitySetSegmentTemplate(segment)); + preRef.Key = keyTemplate; } - - /// - /// Translate a . - /// - /// the segment to Translate - public override void Handle(NavigationPropertySegment segment) + else { - _segmentTemplates.Add(new NavigationSegmentTemplate(segment)); + _segmentTemplates.Add(keyTemplate); } + } - /// - /// Translate a . - /// - /// the segment to Translate - public override void Handle(TypeSegment segment) - { - _segmentTemplates.Add(new CastSegmentTemplate(segment)); - } + /// + /// Translate a . + /// + /// the segment to Translate + public override void Handle(SingletonSegment segment) + { + _segmentTemplates.Add(new SingletonSegmentTemplate(segment)); + } - /// - /// Translate a . - /// - /// the segment to Translate - public override void Handle(PathTemplateSegment segment) - { - _segmentTemplates.Add(new PathTemplateSegmentTemplate(segment)); - } + /// + /// Translate a . + /// + /// the segment to Translate + public override void Handle(EntitySetSegment segment) + { + _segmentTemplates.Add(new EntitySetSegmentTemplate(segment)); + } - /// - /// Translate a . - /// - /// the segment to Translate - /// Translated the path segment template. - public override void Handle(BatchSegment segment) - { - throw new ODataException(Error.Format(SRResources.TargetKindNotImplemented, "ODataPathSegment", "BatchSegment")); - } + /// + /// Translate a . + /// + /// the segment to Translate + public override void Handle(NavigationPropertySegment segment) + { + _segmentTemplates.Add(new NavigationSegmentTemplate(segment)); + } - /// - /// Translate a . - /// - /// the segment to Translate - /// Translated the path segment template. - public override void Handle(BatchReferenceSegment segment) - { - throw new ODataException(Error.Format(SRResources.TargetKindNotImplemented, "ODataPathSegment", "BatchReferenceSegment")); - } + /// + /// Translate a . + /// + /// the segment to Translate + public override void Handle(TypeSegment segment) + { + _segmentTemplates.Add(new CastSegmentTemplate(segment)); + } + + /// + /// Translate a . + /// + /// the segment to Translate + public override void Handle(PathTemplateSegment segment) + { + _segmentTemplates.Add(new PathTemplateSegmentTemplate(segment)); + } + + /// + /// Translate a . + /// + /// the segment to Translate + /// Translated the path segment template. + public override void Handle(BatchSegment segment) + { + throw new ODataException(Error.Format(SRResources.TargetKindNotImplemented, "ODataPathSegment", "BatchSegment")); + } + + /// + /// Translate a . + /// + /// the segment to Translate + /// Translated the path segment template. + public override void Handle(BatchReferenceSegment segment) + { + throw new ODataException(Error.Format(SRResources.TargetKindNotImplemented, "ODataPathSegment", "BatchReferenceSegment")); } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Parser/UnqualifiedCallAndAlternateKeyResolver.cs b/src/Microsoft.AspNetCore.OData/Routing/Parser/UnqualifiedCallAndAlternateKeyResolver.cs index defc8f2b2..07892aac7 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Parser/UnqualifiedCallAndAlternateKeyResolver.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Parser/UnqualifiedCallAndAlternateKeyResolver.cs @@ -10,61 +10,60 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Parser +namespace Microsoft.AspNetCore.OData.Routing.Parser; + +/// +/// The OData uri resolver wrapper for alternate key and unqualified function call. +/// +internal class UnqualifiedCallAndAlternateKeyResolver : ODataUriResolver { - /// - /// The OData uri resolver wrapper for alternate key and unqualified function call. - /// - internal class UnqualifiedCallAndAlternateKeyResolver : ODataUriResolver - { - private readonly AlternateKeysODataUriResolver _alternateKey; - private readonly UnqualifiedODataUriResolver _unqualified; + private readonly AlternateKeysODataUriResolver _alternateKey; + private readonly UnqualifiedODataUriResolver _unqualified; - private bool _enableCaseInsensitive; + private bool _enableCaseInsensitive; - public UnqualifiedCallAndAlternateKeyResolver(IEdmModel model) + public UnqualifiedCallAndAlternateKeyResolver(IEdmModel model) + { + if (model == null) { - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } - - _alternateKey = new AlternateKeysODataUriResolver(model); - _unqualified = new UnqualifiedODataUriResolver(); + throw Error.ArgumentNull(nameof(model)); } - /// - public override bool EnableCaseInsensitive - { - get - { - return _enableCaseInsensitive; - } - set - { - _enableCaseInsensitive = value; - _alternateKey.EnableCaseInsensitive = this._enableCaseInsensitive; - _unqualified.EnableCaseInsensitive = this._enableCaseInsensitive; - } - } + _alternateKey = new AlternateKeysODataUriResolver(model); + _unqualified = new UnqualifiedODataUriResolver(); + } - /// - public override IEnumerable ResolveUnboundOperations(IEdmModel model, string identifier) + /// + public override bool EnableCaseInsensitive + { + get { - return _unqualified.ResolveUnboundOperations(model, identifier); + return _enableCaseInsensitive; } - - /// - public override IEnumerable ResolveBoundOperations(IEdmModel model, string identifier, - IEdmType bindingType) + set { - return _unqualified.ResolveBoundOperations(model, identifier, bindingType); + _enableCaseInsensitive = value; + _alternateKey.EnableCaseInsensitive = this._enableCaseInsensitive; + _unqualified.EnableCaseInsensitive = this._enableCaseInsensitive; } + } - /// - public override IEnumerable> ResolveKeys(IEdmEntityType type, IDictionary namedValues, Func convertFunc) - { - return _alternateKey.ResolveKeys(type, namedValues, convertFunc); - } + /// + public override IEnumerable ResolveUnboundOperations(IEdmModel model, string identifier) + { + return _unqualified.ResolveUnboundOperations(model, identifier); + } + + /// + public override IEnumerable ResolveBoundOperations(IEdmModel model, string identifier, + IEdmType bindingType) + { + return _unqualified.ResolveBoundOperations(model, identifier, bindingType); + } + + /// + public override IEnumerable> ResolveKeys(IEdmEntityType type, IDictionary namedValues, Func convertFunc) + { + return _alternateKey.ResolveKeys(type, namedValues, convertFunc); } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/ActionImportSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/ActionImportSegmentTemplate.cs index f8c156d09..e80d7b395 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/ActionImportSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/ActionImportSegmentTemplate.cs @@ -11,67 +11,66 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match an . +/// +public class ActionImportSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that could match an . + /// Initializes a new instance of the class. /// - public class ActionImportSegmentTemplate : ODataSegmentTemplate + /// The wrapper action import. + /// The target navigation source. it could be null. + public ActionImportSegmentTemplate(IEdmActionImport actionImport, IEdmNavigationSource navigationSource) { - /// - /// Initializes a new instance of the class. - /// - /// The wrapper action import. - /// The target navigation source. it could be null. - public ActionImportSegmentTemplate(IEdmActionImport actionImport, IEdmNavigationSource navigationSource) - { - ActionImport = actionImport ?? throw Error.ArgumentNull(nameof(actionImport)); - Segment = new OperationImportSegment(actionImport, navigationSource as IEdmEntitySetBase); - } + ActionImport = actionImport ?? throw Error.ArgumentNull(nameof(actionImport)); + Segment = new OperationImportSegment(actionImport, navigationSource as IEdmEntitySetBase); + } - /// - /// Initializes a new instance of the class. - /// - /// The operation import segment. - public ActionImportSegmentTemplate(OperationImportSegment segment) + /// + /// Initializes a new instance of the class. + /// + /// The operation import segment. + public ActionImportSegmentTemplate(OperationImportSegment segment) + { + Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); + + IEdmOperationImport operationImport = segment.OperationImports.First(); + if (!operationImport.IsActionImport()) { - Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); + throw new ODataException(Error.Format(SRResources.SegmentShouldBeKind, "ActionImport", "ActionImportSegmentTemplate")); + } - IEdmOperationImport operationImport = segment.OperationImports.First(); - if (!operationImport.IsActionImport()) - { - throw new ODataException(Error.Format(SRResources.SegmentShouldBeKind, "ActionImport", "ActionImportSegmentTemplate")); - } + ActionImport = (IEdmActionImport)operationImport; + } - ActionImport = (IEdmActionImport)operationImport; - } + /// + /// Gets the wrapped action import. + /// + public IEdmActionImport ActionImport { get; } - /// - /// Gets the wrapped action import. - /// - public IEdmActionImport ActionImport { get; } + /// + /// Gets the action import segment. + /// + public OperationImportSegment Segment { get; } - /// - /// Gets the action import segment. - /// - public OperationImportSegment Segment { get; } + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return $"/{ActionImport.Name}"; + } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) { - yield return $"/{ActionImport.Name}"; + throw Error.ArgumentNull(nameof(context)); } - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - context.Segments.Add(Segment); - return true; - } + context.Segments.Add(Segment); + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/ActionSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/ActionSegmentTemplate.cs index 0329c8cf5..766934937 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/ActionSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/ActionSegmentTemplate.cs @@ -13,108 +13,107 @@ using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match an . +/// +public class ActionSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that could match an . + /// Initializes a new instance of the class. /// - public class ActionSegmentTemplate : ODataSegmentTemplate + /// The Edm action. + /// The Edm navigation source. + public ActionSegmentTemplate(IEdmAction action, IEdmNavigationSource navigationSource) { - /// - /// Initializes a new instance of the class. - /// - /// The Edm action. - /// The Edm navigation source. - public ActionSegmentTemplate(IEdmAction action, IEdmNavigationSource navigationSource) + Action = action ?? throw Error.ArgumentNull(nameof(action)); + + // Only accept the bound action + if (!action.IsBound) { - Action = action ?? throw Error.ArgumentNull(nameof(action)); + throw new ODataException(Error.Format(SRResources.OperationIsNotBound, action.Name, "action")); + } - // Only accept the bound action - if (!action.IsBound) - { - throw new ODataException(Error.Format(SRResources.OperationIsNotBound, action.Name, "action")); - } + NavigationSource = navigationSource; + Segment = new OperationSegment(Action, NavigationSource as IEdmEntitySetBase); + } - NavigationSource = navigationSource; - Segment = new OperationSegment(Action, NavigationSource as IEdmEntitySetBase); + /// + /// Initializes a new instance of the class. + /// + /// The operation segment. + public ActionSegmentTemplate(OperationSegment segment) + { + if (segment == null) + { + throw Error.ArgumentNull(nameof(segment)); } - /// - /// Initializes a new instance of the class. - /// - /// The operation segment. - public ActionSegmentTemplate(OperationSegment segment) + IEdmOperation operation = segment.Operations.First(); + if (!operation.IsAction()) { - if (segment == null) - { - throw Error.ArgumentNull(nameof(segment)); - } - - IEdmOperation operation = segment.Operations.First(); - if (!operation.IsAction()) - { - throw new ODataException(Error.Format(SRResources.SegmentShouldBeKind, "Action", "ActionSegmentTemplate")); - } - - Action = (IEdmAction)operation; - NavigationSource = segment.EntitySet; - Segment = segment; + throw new ODataException(Error.Format(SRResources.SegmentShouldBeKind, "Action", "ActionSegmentTemplate")); } - /// - /// Gets the wrapped Edm action. - /// - public IEdmAction Action { get; } + Action = (IEdmAction)operation; + NavigationSource = segment.EntitySet; + Segment = segment; + } + + /// + /// Gets the wrapped Edm action. + /// + public IEdmAction Action { get; } + + /// + /// Gets the wrapped Edm navigation source. + /// + public IEdmNavigationSource NavigationSource { get; } + + /// + /// Gets the action segment. + /// + public OperationSegment Segment { get; } - /// - /// Gets the wrapped Edm navigation source. - /// - public IEdmNavigationSource NavigationSource { get; } + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + options = options ?? ODataRouteOptions.Default; + Contract.Assert(options.EnableQualifiedOperationCall || options.EnableUnqualifiedOperationCall); - /// - /// Gets the action segment. - /// - public OperationSegment Segment { get; } + if (options.EnableQualifiedOperationCall && options.EnableUnqualifiedOperationCall) + { + yield return $"/{Action.FullName()}"; + yield return $"/{Action.Name}"; + } + else if (options.EnableQualifiedOperationCall) + { + yield return $"/{Action.FullName()}"; + } + else + { + yield return $"/{Action.Name}"; + } + } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) { - options = options ?? ODataRouteOptions.Default; - Contract.Assert(options.EnableQualifiedOperationCall || options.EnableUnqualifiedOperationCall); - - if (options.EnableQualifiedOperationCall && options.EnableUnqualifiedOperationCall) - { - yield return $"/{Action.FullName()}"; - yield return $"/{Action.Name}"; - } - else if (options.EnableQualifiedOperationCall) - { - yield return $"/{Action.FullName()}"; - } - else - { - yield return $"/{Action.Name}"; - } + throw Error.ArgumentNull(nameof(context)); } - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + if (NavigationSource != null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - if (NavigationSource != null) - { - context.Segments.Add(Segment); - return true; - } - - IEdmNavigationSource navigationSource = SegmentTemplateHelpers.GetNavigationSourceFromEdmOperation(context.Model, Action); - OperationSegment actionSegment = new OperationSegment(Action, navigationSource as IEdmEntitySetBase); - context.Segments.Add(actionSegment); + context.Segments.Add(Segment); return true; } + + IEdmNavigationSource navigationSource = SegmentTemplateHelpers.GetNavigationSourceFromEdmOperation(context.Model, Action); + OperationSegment actionSegment = new OperationSegment(Action, navigationSource as IEdmEntitySetBase); + context.Segments.Add(actionSegment); + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/CastSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/CastSegmentTemplate.cs index 8d6e00c15..9f8b2933e 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/CastSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/CastSegmentTemplate.cs @@ -9,74 +9,73 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match a type cast segment. +/// +public class CastSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that could match a type cast segment. + /// Initializes a new instance of the class. /// - public class CastSegmentTemplate : ODataSegmentTemplate + /// The cast Edm type. + /// The expected Edm type. + /// The target navigation source. it could be null. + public CastSegmentTemplate(IEdmType castType, IEdmType expectedType, IEdmNavigationSource navigationSource) { - /// - /// Initializes a new instance of the class. - /// - /// The cast Edm type. - /// The expected Edm type. - /// The target navigation source. it could be null. - public CastSegmentTemplate(IEdmType castType, IEdmType expectedType, IEdmNavigationSource navigationSource) + if (castType == null) { - if (castType == null) - { - throw Error.ArgumentNull(nameof(castType)); - } - - if (expectedType == null) - { - throw Error.ArgumentNull(nameof(expectedType)); - } - - TypeSegment = new TypeSegment(castType, expectedType, navigationSource); + throw Error.ArgumentNull(nameof(castType)); } - /// - /// Initializes a new instance of the class. - /// - /// The type segment. - public CastSegmentTemplate(TypeSegment typeSegment) + if (expectedType == null) { - TypeSegment = typeSegment ?? throw Error.ArgumentNull(nameof(typeSegment)); + throw Error.ArgumentNull(nameof(expectedType)); } - /// - /// Gets the cast type. - /// - public IEdmType CastType => TypeSegment.EdmType; + TypeSegment = new TypeSegment(castType, expectedType, navigationSource); + } - /// - /// Gets the expected type. - /// - public IEdmType ExpectedType => TypeSegment.ExpectedType; + /// + /// Initializes a new instance of the class. + /// + /// The type segment. + public CastSegmentTemplate(TypeSegment typeSegment) + { + TypeSegment = typeSegment ?? throw Error.ArgumentNull(nameof(typeSegment)); + } - /// - /// Gets the expected type. - /// - public TypeSegment TypeSegment { get; } + /// + /// Gets the cast type. + /// + public IEdmType CastType => TypeSegment.EdmType; - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) - { - yield return $"/{CastType.AsElementType().FullTypeName()}"; - } + /// + /// Gets the expected type. + /// + public IEdmType ExpectedType => TypeSegment.ExpectedType; - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + /// + /// Gets the expected type. + /// + public TypeSegment TypeSegment { get; } - context.Segments.Add(TypeSegment); - return true; + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return $"/{CastType.AsElementType().FullTypeName()}"; + } + + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); } + + context.Segments.Add(TypeSegment); + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/CountSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/CountSegmentTemplate.cs index d5d3e3216..a0cff6edf 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/CountSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/CountSegmentTemplate.cs @@ -8,41 +8,40 @@ using System.Collections.Generic; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match a $count segment. +/// +public class CountSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that could match a $count segment. + /// Gets the static instance of + /// + public static CountSegmentTemplate Instance { get; } = new CountSegmentTemplate(); + + /// + /// Initializes a new instance of the class. /// - public class CountSegmentTemplate : ODataSegmentTemplate + private CountSegmentTemplate() { - /// - /// Gets the static instance of - /// - public static CountSegmentTemplate Instance { get; } = new CountSegmentTemplate(); + } - /// - /// Initializes a new instance of the class. - /// - private CountSegmentTemplate() - { - } + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return "/$count"; + } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) { - yield return "/$count"; + throw Error.ArgumentNull(nameof(context)); } - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - context.Segments.Add(CountSegment.Instance); - return true; - } + context.Segments.Add(CountSegment.Instance); + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/DefaultODataTemplateTranslator.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/DefaultODataTemplateTranslator.cs index 113e03875..e3d5774fb 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/DefaultODataTemplateTranslator.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/DefaultODataTemplateTranslator.cs @@ -7,36 +7,35 @@ using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Default implementation for . +/// +internal class DefaultODataTemplateTranslator : IODataTemplateTranslator { - /// - /// Default implementation for . - /// - internal class DefaultODataTemplateTranslator : IODataTemplateTranslator + /// + public virtual ODataPath Translate(ODataPathTemplate path, ODataTemplateTranslateContext context) { - /// - public virtual ODataPath Translate(ODataPathTemplate path, ODataTemplateTranslateContext context) + if (path == null) { - if (path == null) - { - throw Error.ArgumentNull(nameof(path)); - } + throw Error.ArgumentNull(nameof(path)); + } - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - // calculate every time - foreach (var segment in path) + // calculate every time + foreach (var segment in path) + { + if (!segment.TryTranslate(context)) { - if (!segment.TryTranslate(context)) - { - return null; - } + return null; } - - return new ODataPath(context.Segments); } + + return new ODataPath(context.Segments); } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/DynamicSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/DynamicSegmentTemplate.cs index 797fb1f31..83e09174c 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/DynamicSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/DynamicSegmentTemplate.cs @@ -9,45 +9,44 @@ using System.Collections.Generic; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that can match a . +/// Be noted: +/// a dynamic path segment is a real segment (not a template), its literal is dynamic property name. +/// +public class DynamicSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that can match a . - /// Be noted: - /// a dynamic path segment is a real segment (not a template), its literal is dynamic property name. + /// Initializes a new instance of the class. /// - public class DynamicSegmentTemplate : ODataSegmentTemplate + /// The open property segment + public DynamicSegmentTemplate(DynamicPathSegment segment) { - /// - /// Initializes a new instance of the class. - /// - /// The open property segment - public DynamicSegmentTemplate(DynamicPathSegment segment) - { - Segment = segment ?? throw new ArgumentNullException(nameof(segment)); - } + Segment = segment ?? throw new ArgumentNullException(nameof(segment)); + } - /// - /// Gets or sets the open property segment. - /// - public DynamicPathSegment Segment { get; } + /// + /// Gets or sets the open property segment. + /// + public DynamicPathSegment Segment { get; } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) - { - yield return $"/{Segment.Identifier}"; - } + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return $"/{Segment.Identifier}"; + } - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - context.Segments.Add(Segment); - return true; + throw Error.ArgumentNull(nameof(context)); } + + context.Segments.Add(Segment); + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/EntitySetSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/EntitySetSegmentTemplate.cs index 6d4d45214..c61e688a6 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/EntitySetSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/EntitySetSegmentTemplate.cs @@ -9,57 +9,56 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match an . +/// +public class EntitySetSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that could match an . + /// Initializes a new instance of the class. /// - public class EntitySetSegmentTemplate : ODataSegmentTemplate + /// The Edm entity set. + public EntitySetSegmentTemplate(IEdmEntitySet entitySet) + : this(new EntitySetSegment(entitySet)) { - /// - /// Initializes a new instance of the class. - /// - /// The Edm entity set. - public EntitySetSegmentTemplate(IEdmEntitySet entitySet) - : this(new EntitySetSegment(entitySet)) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The entity set segment. - public EntitySetSegmentTemplate(EntitySetSegment segment) - { - Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); - } + /// + /// Initializes a new instance of the class. + /// + /// The entity set segment. + public EntitySetSegmentTemplate(EntitySetSegment segment) + { + Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); + } - /// - /// Gets the wrapped Edm entityset. - /// - public IEdmEntitySet EntitySet => Segment.EntitySet; + /// + /// Gets the wrapped Edm entityset. + /// + public IEdmEntitySet EntitySet => Segment.EntitySet; - /// - /// Gets the entity set segment. - /// - public EntitySetSegment Segment { get; } + /// + /// Gets the entity set segment. + /// + public EntitySetSegment Segment { get; } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) - { - yield return $"/{Segment.EntitySet.Name}"; - } + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return $"/{Segment.EntitySet.Name}"; + } - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - context.Segments.Add(Segment); - return true; + throw Error.ArgumentNull(nameof(context)); } + + context.Segments.Add(Segment); + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/FunctionImportSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/FunctionImportSegmentTemplate.cs index 2eaa79414..769db0b36 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/FunctionImportSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/FunctionImportSegmentTemplate.cs @@ -12,142 +12,141 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match an . +/// +public class FunctionImportSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that could match an . + /// Initializes a new instance of the class. /// - public class FunctionImportSegmentTemplate : ODataSegmentTemplate + /// The Edm function import. + /// The target navigation source, it could be null. + public FunctionImportSegmentTemplate(IEdmFunctionImport functionImport, IEdmNavigationSource navigationSource) + : this(functionImport.GetFunctionParamterMappings(), functionImport, navigationSource) { - /// - /// Initializes a new instance of the class. - /// - /// The Edm function import. - /// The target navigation source, it could be null. - public FunctionImportSegmentTemplate(IEdmFunctionImport functionImport, IEdmNavigationSource navigationSource) - : this(functionImport.GetFunctionParamterMappings(), functionImport, navigationSource) + } + + /// + /// Initializes a new instance of the class. + /// + /// The function parameter template mappings.The key string is case-sensitive, the value string should wrapper with { and }. + /// The Edm function import. + /// The target navigation source, it could be null. + public FunctionImportSegmentTemplate(IDictionary parameters, IEdmFunctionImport functionImport, IEdmNavigationSource navigationSource) + { + if (parameters == null) { + throw Error.ArgumentNull(nameof(parameters)); } - /// - /// Initializes a new instance of the class. - /// - /// The function parameter template mappings.The key string is case-sensitive, the value string should wrapper with { and }. - /// The Edm function import. - /// The target navigation source, it could be null. - public FunctionImportSegmentTemplate(IDictionary parameters, IEdmFunctionImport functionImport, IEdmNavigationSource navigationSource) - { - if (parameters == null) - { - throw Error.ArgumentNull(nameof(parameters)); - } + FunctionImport = functionImport ?? throw Error.ArgumentNull(nameof(functionImport)); + NavigationSource = navigationSource; - FunctionImport = functionImport ?? throw Error.ArgumentNull(nameof(functionImport)); - NavigationSource = navigationSource; + // parameters should include all required parameters, but maybe include the optional parameters. + ParameterMappings = functionImport.Function.VerifyAndBuildParameterMappings(parameters); + } - // parameters should include all required parameters, but maybe include the optional parameters. - ParameterMappings = functionImport.Function.VerifyAndBuildParameterMappings(parameters); + /// + /// Initializes a new instance of the class. + /// + /// The input function import segment. + public FunctionImportSegmentTemplate(OperationImportSegment segment) + { + if (segment == null) + { + throw Error.ArgumentNull(nameof(segment)); } - /// - /// Initializes a new instance of the class. - /// - /// The input function import segment. - public FunctionImportSegmentTemplate(OperationImportSegment segment) + IEdmOperationImport operationImport = segment.OperationImports.First(); + if (!operationImport.IsFunctionImport()) { - if (segment == null) - { - throw Error.ArgumentNull(nameof(segment)); - } + throw new ODataException(Error.Format(SRResources.SegmentShouldBeKind, "FunctionImport", "FunctionImportSegmentTemplate")); + } - IEdmOperationImport operationImport = segment.OperationImports.First(); - if (!operationImport.IsFunctionImport()) - { - throw new ODataException(Error.Format(SRResources.SegmentShouldBeKind, "FunctionImport", "FunctionImportSegmentTemplate")); - } + FunctionImport = (IEdmFunctionImport)operationImport; - FunctionImport = (IEdmFunctionImport)operationImport; + NavigationSource = segment.EntitySet; - NavigationSource = segment.EntitySet; + ParameterMappings = segment.Parameters.BuildParameterMappings(operationImport.Name); + } - ParameterMappings = segment.Parameters.BuildParameterMappings(operationImport.Name); - } + /// + /// Gets the dictionary representing the mappings from the parameter names in the current function segment to the + /// parameter names in route data. + /// + public IDictionary ParameterMappings { get; } - /// - /// Gets the dictionary representing the mappings from the parameter names in the current function segment to the - /// parameter names in route data. - /// - public IDictionary ParameterMappings { get; } + /// + /// Gets the target Navigation source of this segment. + /// + public IEdmNavigationSource NavigationSource { get; } - /// - /// Gets the target Navigation source of this segment. - /// - public IEdmNavigationSource NavigationSource { get; } + /// + /// Gets the wrapped Edm function import. + /// + public IEdmFunctionImport FunctionImport { get; } - /// - /// Gets the wrapped Edm function import. - /// - public IEdmFunctionImport FunctionImport { get; } + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + options = options ?? ODataRouteOptions.Default; - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) + if (ParameterMappings.Count == 0 && options.EnableNonParenthesisForEmptyParameterFunction) { - options = options ?? ODataRouteOptions.Default; - - if (ParameterMappings.Count == 0 && options.EnableNonParenthesisForEmptyParameterFunction) - { - yield return $"/{FunctionImport.Name}"; - } - else - { - string parameters = string.Join(",", ParameterMappings.Select(a => $"{a.Key}={{{a.Value}}}")); - yield return $"/{FunctionImport.Name}({parameters})"; - } + yield return $"/{FunctionImport.Name}"; } - - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + else { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + string parameters = string.Join(",", ParameterMappings.Select(a => $"{a.Key}={{{a.Value}}}")); + yield return $"/{FunctionImport.Name}({parameters})"; + } + } - // If the function has no parameter, we don't need to do anything and just return an operation import segment. - if (ParameterMappings.Count == 0) - { - context.Segments.Add(new OperationImportSegment(FunctionImport, NavigationSource as IEdmEntitySetBase)); - return true; - } + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - if (HasOptionalMissing()) - { - // If this function template has the optional parameter missing, - // for example: ~/GetSalary(min={min},max={max}), without ave={ave} - // We should avoid this template matching with "~/GetSalary(min=1,max=2,ave=3)" - // Because, In this request, the coming route data has the following: - // min = 1 - // max = 2,ave=3 - // Therefore, we need to combine the route data together and separate them using "," again. - if (!SegmentTemplateHelpers.IsMatchParameters(context.RouteValues, ParameterMappings)) - { - return false; - } - } + // If the function has no parameter, we don't need to do anything and just return an operation import segment. + if (ParameterMappings.Count == 0) + { + context.Segments.Add(new OperationImportSegment(FunctionImport, NavigationSource as IEdmEntitySetBase)); + return true; + } - IList parameters = SegmentTemplateHelpers.Match(context, FunctionImport.Function, ParameterMappings); - if (parameters == null) + if (HasOptionalMissing()) + { + // If this function template has the optional parameter missing, + // for example: ~/GetSalary(min={min},max={max}), without ave={ave} + // We should avoid this template matching with "~/GetSalary(min=1,max=2,ave=3)" + // Because, In this request, the coming route data has the following: + // min = 1 + // max = 2,ave=3 + // Therefore, we need to combine the route data together and separate them using "," again. + if (!SegmentTemplateHelpers.IsMatchParameters(context.RouteValues, ParameterMappings)) { return false; } - - context.Segments.Add(new OperationImportSegment(FunctionImport, NavigationSource as IEdmEntitySetBase, parameters)); - return true; } - private bool HasOptionalMissing() + IList parameters = SegmentTemplateHelpers.Match(context, FunctionImport.Function, ParameterMappings); + if (parameters == null) { - return ParameterMappings.Count != FunctionImport.Function.Parameters.Count(); + return false; } + + context.Segments.Add(new OperationImportSegment(FunctionImport, NavigationSource as IEdmEntitySetBase, parameters)); + return true; + } + + private bool HasOptionalMissing() + { + return ParameterMappings.Count != FunctionImport.Function.Parameters.Count(); } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/FunctionSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/FunctionSegmentTemplate.cs index ea1a676a3..91ce876ca 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/FunctionSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/FunctionSegmentTemplate.cs @@ -14,178 +14,177 @@ using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match a bound . +/// +public class FunctionSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that could match a bound . + /// Initializes a new instance of the class. + /// + /// The Edm function, it should be bound function. + /// The Edm navigation source of this function return. It could be null. + public FunctionSegmentTemplate(IEdmFunction function, IEdmNavigationSource navigationSource) + : this(function.GetFunctionParamterMappings(), function, navigationSource) + { + } + + /// + /// Initializes a new instance of the class. /// - public class FunctionSegmentTemplate : ODataSegmentTemplate + /// The function parameter template mappings.The key string is case-sensitive, the value string should wrapper with { and }. + /// The Edm function, it should be bound function. + /// The Edm navigation source of this function return. It could be null. + public FunctionSegmentTemplate(IDictionary parameters, IEdmFunction function, IEdmNavigationSource navigationSource) { - /// - /// Initializes a new instance of the class. - /// - /// The Edm function, it should be bound function. - /// The Edm navigation source of this function return. It could be null. - public FunctionSegmentTemplate(IEdmFunction function, IEdmNavigationSource navigationSource) - : this(function.GetFunctionParamterMappings(), function, navigationSource) + if (parameters == null) { + throw Error.ArgumentNull(nameof(parameters)); } - /// - /// Initializes a new instance of the class. - /// - /// The function parameter template mappings.The key string is case-sensitive, the value string should wrapper with { and }. - /// The Edm function, it should be bound function. - /// The Edm navigation source of this function return. It could be null. - public FunctionSegmentTemplate(IDictionary parameters, IEdmFunction function, IEdmNavigationSource navigationSource) - { - if (parameters == null) - { - throw Error.ArgumentNull(nameof(parameters)); - } + Function = function ?? throw Error.ArgumentNull(nameof(function)); - Function = function ?? throw Error.ArgumentNull(nameof(function)); + NavigationSource = navigationSource; - NavigationSource = navigationSource; + // Only accept the bound function + if (!function.IsBound) + { + throw new ODataException(Error.Format(SRResources.OperationIsNotBound, function.Name, "function")); + } - // Only accept the bound function - if (!function.IsBound) - { - throw new ODataException(Error.Format(SRResources.OperationIsNotBound, function.Name, "function")); - } + // Parameters should include all required parameter, but maybe include the optional parameter. + ParameterMappings = function.VerifyAndBuildParameterMappings(parameters); + } - // Parameters should include all required parameter, but maybe include the optional parameter. - ParameterMappings = function.VerifyAndBuildParameterMappings(parameters); + /// + /// Initializes a new instance of the class. + /// + /// The operation segment, it should be a function segment and the parameters are template. + public FunctionSegmentTemplate(OperationSegment operationSegment) + { + if (operationSegment == null) + { + throw Error.ArgumentNull(nameof(operationSegment)); } - /// - /// Initializes a new instance of the class. - /// - /// The operation segment, it should be a function segment and the parameters are template. - public FunctionSegmentTemplate(OperationSegment operationSegment) + IEdmOperation operation = operationSegment.Operations.First(); + if (!operation.IsFunction()) { - if (operationSegment == null) - { - throw Error.ArgumentNull(nameof(operationSegment)); - } + throw new ODataException(Error.Format(SRResources.SegmentShouldBeKind, "Function", "FunctionSegmentTemplate")); + } - IEdmOperation operation = operationSegment.Operations.First(); - if (!operation.IsFunction()) - { - throw new ODataException(Error.Format(SRResources.SegmentShouldBeKind, "Function", "FunctionSegmentTemplate")); - } + Function = (IEdmFunction)operation; - Function = (IEdmFunction)operation; + NavigationSource = operationSegment.EntitySet; - NavigationSource = operationSegment.EntitySet; + ParameterMappings = operationSegment.Parameters.BuildParameterMappings(operation.FullName()); + } - ParameterMappings = operationSegment.Parameters.BuildParameterMappings(operation.FullName()); - } + /// + /// Gets the dictionary representing the mappings from the parameter names in the current function segment to the + /// parameter names in route data. + /// + public IDictionary ParameterMappings { get; private set; } - /// - /// Gets the dictionary representing the mappings from the parameter names in the current function segment to the - /// parameter names in route data. - /// - public IDictionary ParameterMappings { get; private set; } + /// + /// Gets the wrapped Edm function. + /// + public IEdmFunction Function { get; } - /// - /// Gets the wrapped Edm function. - /// - public IEdmFunction Function { get; } + /// + /// Gets the wrapped navigation source. + /// + public IEdmNavigationSource NavigationSource { get; } - /// - /// Gets the wrapped navigation source. - /// - public IEdmNavigationSource NavigationSource { get; } + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + options = options ?? ODataRouteOptions.Default; + Contract.Assert(options.EnableQualifiedOperationCall || options.EnableUnqualifiedOperationCall); - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) + string unqualifiedIdentifier, qualifiedIdentifier; + if (ParameterMappings.Count == 0 && options.EnableNonParenthesisForEmptyParameterFunction) { - options = options ?? ODataRouteOptions.Default; - Contract.Assert(options.EnableQualifiedOperationCall || options.EnableUnqualifiedOperationCall); - - string unqualifiedIdentifier, qualifiedIdentifier; - if (ParameterMappings.Count == 0 && options.EnableNonParenthesisForEmptyParameterFunction) - { - unqualifiedIdentifier = "/" + Function.Name; - qualifiedIdentifier = "/" + Function.FullName(); - } - else - { - string parameterStr = "(" + string.Join(",", ParameterMappings.Select(a => $"{a.Key}={{{a.Value}}}")) + ")"; - unqualifiedIdentifier = "/" + Function.Name + parameterStr; - qualifiedIdentifier = "/" + Function.FullName() + parameterStr; - } + unqualifiedIdentifier = "/" + Function.Name; + qualifiedIdentifier = "/" + Function.FullName(); + } + else + { + string parameterStr = "(" + string.Join(",", ParameterMappings.Select(a => $"{a.Key}={{{a.Value}}}")) + ")"; + unqualifiedIdentifier = "/" + Function.Name + parameterStr; + qualifiedIdentifier = "/" + Function.FullName() + parameterStr; + } - if (options.EnableQualifiedOperationCall && options.EnableUnqualifiedOperationCall) - { - // "/NS.Function(...)" - yield return qualifiedIdentifier; + if (options.EnableQualifiedOperationCall && options.EnableUnqualifiedOperationCall) + { + // "/NS.Function(...)" + yield return qualifiedIdentifier; - // "/Function(...)" - yield return unqualifiedIdentifier; - } - else if (options.EnableQualifiedOperationCall) - { - // "/NS.Function(...)" - yield return qualifiedIdentifier; - } - else - { - // "/Function(...)" - yield return unqualifiedIdentifier; - } + // "/Function(...)" + yield return unqualifiedIdentifier; } - - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + else if (options.EnableQualifiedOperationCall) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + // "/NS.Function(...)" + yield return qualifiedIdentifier; + } + else + { + // "/Function(...)" + yield return unqualifiedIdentifier; + } + } - IEdmNavigationSource navigationSource = NavigationSource; - if (navigationSource == null) - { - navigationSource = SegmentTemplateHelpers.GetNavigationSourceFromEdmOperation(context.Model, Function); - } + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - // If the function has no parameter, we don't need to do anything and just return an operation segment. - if (ParameterMappings.Count == 0) - { - context.Segments.Add(new OperationSegment(Function, navigationSource as IEdmEntitySetBase)); - return true; - } + IEdmNavigationSource navigationSource = NavigationSource; + if (navigationSource == null) + { + navigationSource = SegmentTemplateHelpers.GetNavigationSourceFromEdmOperation(context.Model, Function); + } - if (HasOptionalMissing()) - { - // If this function template has the optional parameter missing, - // for example: ~/GetSalary(min={min},max={max}), without ave={ave} - // We should avoid this template matching with "~/GetSalary(min=1,max=2,ave=3)" - // Because, In this request, the coming route data has the following: - // min = 1 - // max = 2,ave=3 - // Therefore, we need to combine the route data together and separate them using "," again. - if (!SegmentTemplateHelpers.IsMatchParameters(context.RouteValues, ParameterMappings)) - { - return false; - } - } + // If the function has no parameter, we don't need to do anything and just return an operation segment. + if (ParameterMappings.Count == 0) + { + context.Segments.Add(new OperationSegment(Function, navigationSource as IEdmEntitySetBase)); + return true; + } - IList parameters = SegmentTemplateHelpers.Match(context, Function, ParameterMappings); - if (parameters == null) + if (HasOptionalMissing()) + { + // If this function template has the optional parameter missing, + // for example: ~/GetSalary(min={min},max={max}), without ave={ave} + // We should avoid this template matching with "~/GetSalary(min=1,max=2,ave=3)" + // Because, In this request, the coming route data has the following: + // min = 1 + // max = 2,ave=3 + // Therefore, we need to combine the route data together and separate them using "," again. + if (!SegmentTemplateHelpers.IsMatchParameters(context.RouteValues, ParameterMappings)) { return false; } - - context.Segments.Add(new OperationSegment(Function, parameters, navigationSource as IEdmEntitySetBase)); - return true; } - private bool HasOptionalMissing() + IList parameters = SegmentTemplateHelpers.Match(context, Function, ParameterMappings); + if (parameters == null) { - return ParameterMappings.Count != Function.Parameters.Count() - 1; + return false; } + + context.Segments.Add(new OperationSegment(Function, parameters, navigationSource as IEdmEntitySetBase)); + return true; + } + + private bool HasOptionalMissing() + { + return ParameterMappings.Count != Function.Parameters.Count() - 1; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/IODataTemplateTranslator.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/IODataTemplateTranslator.cs index 13888bbcb..cc31d7c95 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/IODataTemplateTranslator.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/IODataTemplateTranslator.cs @@ -7,19 +7,18 @@ using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Defines a contract used to translate the OData path template to OData path. +/// +public interface IODataTemplateTranslator { /// - /// Defines a contract used to translate the OData path template to OData path. + /// Translate the to /// - public interface IODataTemplateTranslator - { - /// - /// Translate the to - /// - /// The OData path template. - /// The translate context. - /// The OData Path. - ODataPath Translate(ODataPathTemplate path, ODataTemplateTranslateContext context); - } + /// The OData path template. + /// The translate context. + /// The OData Path. + ODataPath Translate(ODataPathTemplate path, ODataTemplateTranslateContext context); } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/KeySegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/KeySegmentTemplate.cs index e797fac88..57ac84bd9 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/KeySegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/KeySegmentTemplate.cs @@ -17,322 +17,321 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match a key segment. +/// +public class KeySegmentTemplate : ODataSegmentTemplate { + private readonly string _keyLiteral; + /// - /// Represents a template that could match a key segment. + /// Initializes a new instance of the class. /// - public class KeySegmentTemplate : ODataSegmentTemplate + /// The input key mappings, the key string is case-sensitive, the value string should wrapper with { and }. + /// The declaring type contains the key. + /// The navigation source. It could be null. + public KeySegmentTemplate(IDictionary keys, IEdmEntityType entityType, IEdmNavigationSource navigationSource) { - private readonly string _keyLiteral; - - /// - /// Initializes a new instance of the class. - /// - /// The input key mappings, the key string is case-sensitive, the value string should wrapper with { and }. - /// The declaring type contains the key. - /// The navigation source. It could be null. - public KeySegmentTemplate(IDictionary keys, IEdmEntityType entityType, IEdmNavigationSource navigationSource) + if (keys == null) { - if (keys == null) - { - throw Error.ArgumentNull(nameof(keys)); - } + throw Error.ArgumentNull(nameof(keys)); + } - EntityType = entityType ?? throw Error.ArgumentNull(nameof(entityType)); - NavigationSource = navigationSource; - KeyProperties = EntityType.Key().ToDictionary(k => k.Name, k => (IEdmProperty)k); + EntityType = entityType ?? throw Error.ArgumentNull(nameof(entityType)); + NavigationSource = navigationSource; + KeyProperties = EntityType.Key().ToDictionary(k => k.Name, k => (IEdmProperty)k); - KeyMappings = BuildKeyMappings(keys.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)), entityType, KeyProperties); + KeyMappings = BuildKeyMappings(keys.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)), entityType, KeyProperties); - _keyLiteral = KeyMappings.Count == 1 ? - $"{{{KeyMappings.First().Value}}}" : - string.Join(",", KeyMappings.Select(a => $"{a.Key}={{{a.Value}}}")); - } + _keyLiteral = KeyMappings.Count == 1 ? + $"{{{KeyMappings.First().Value}}}" : + string.Join(",", KeyMappings.Select(a => $"{a.Key}={{{a.Value}}}")); + } - /// - /// Initializes a new instance of the class. - /// - /// The key segment, it should be a template key segment. - public KeySegmentTemplate(KeySegment segment) + /// + /// Initializes a new instance of the class. + /// + /// The key segment, it should be a template key segment. + public KeySegmentTemplate(KeySegment segment) + { + if (segment == null) { - if (segment == null) - { - throw Error.ArgumentNull(nameof(segment)); - } + throw Error.ArgumentNull(nameof(segment)); + } + + NavigationSource = segment.NavigationSource; + EntityType = segment.EdmType as IEdmEntityType; + KeyProperties = EntityType.Key().ToDictionary(k => k.Name, k => (IEdmProperty)k); - NavigationSource = segment.NavigationSource; - EntityType = segment.EdmType as IEdmEntityType; - KeyProperties = EntityType.Key().ToDictionary(k => k.Name, k => (IEdmProperty)k); + KeyMappings = BuildKeyMappings(segment.Keys, EntityType, KeyProperties); - KeyMappings = BuildKeyMappings(segment.Keys, EntityType, KeyProperties); + _keyLiteral = KeyMappings.Count == 1 ? + $"{{{KeyMappings.First().Value}}}" : + string.Join(",", KeyMappings.Select(a => $"{a.Key}={{{a.Value}}}")); + } - _keyLiteral = KeyMappings.Count == 1 ? - $"{{{KeyMappings.First().Value}}}" : - string.Join(",", KeyMappings.Select(a => $"{a.Key}={{{a.Value}}}")); + /// + /// Initializes a new instance of the class. + /// Typically, it's for alternate key scenario. + /// + /// The key segment, it should be a template key segment. + /// The key properties, the key is the alias, + /// the value is the property list, it could be a property from the complex property. For example: address/city. + public KeySegmentTemplate(KeySegment segment, IDictionary keyProperties) + { + if (segment == null) + { + throw Error.ArgumentNull(nameof(segment)); } - /// - /// Initializes a new instance of the class. - /// Typically, it's for alternate key scenario. - /// - /// The key segment, it should be a template key segment. - /// The key properties, the key is the alias, - /// the value is the property list, it could be a property from the complex property. For example: address/city. - public KeySegmentTemplate(KeySegment segment, IDictionary keyProperties) + if (keyProperties == null) { - if (segment == null) - { - throw Error.ArgumentNull(nameof(segment)); - } + throw Error.ArgumentNull(nameof(keyProperties)); + } - if (keyProperties == null) - { - throw Error.ArgumentNull(nameof(keyProperties)); - } + NavigationSource = segment.NavigationSource; + EntityType = segment.EdmType as IEdmEntityType; + KeyProperties = keyProperties; - NavigationSource = segment.NavigationSource; - EntityType = segment.EdmType as IEdmEntityType; - KeyProperties = keyProperties; + KeyMappings = BuildKeyMappings(segment.Keys, EntityType, keyProperties); - KeyMappings = BuildKeyMappings(segment.Keys, EntityType, keyProperties); + _keyLiteral = string.Join(",", KeyMappings.Select(a => $"{a.Key}={{{a.Value}}}")); + } - _keyLiteral = string.Join(",", KeyMappings.Select(a => $"{a.Key}={{{a.Value}}}")); - } + /// + /// Gets the dictionary representing the mappings from the key names in the current key segment to the + /// key names in route data. + /// the key in dict could be the string used in request + /// the value in dict could be the string used in action of controller + /// + public IDictionary KeyMappings { get; } - /// - /// Gets the dictionary representing the mappings from the key names in the current key segment to the - /// key names in route data. - /// the key in dict could be the string used in request - /// the value in dict could be the string used in action of controller - /// - public IDictionary KeyMappings { get; } - - /// - /// Gets the keys. - /// The key of dictionary is the key name or alias. - /// The value of dictionary is the key property, it could be property on entity type or sub property on complex property. - /// - public IDictionary KeyProperties { get; } - - /// - public IEdmNavigationSource NavigationSource { get; } - - /// - /// Gets the entity type declaring this key. - /// - public IEdmEntityType EntityType { get; } - - /// - /// Gets the key count - /// - public int Count => KeyMappings.Count; - - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) - { - options = options ?? ODataRouteOptions.Default; + /// + /// Gets the keys. + /// The key of dictionary is the key name or alias. + /// The value of dictionary is the key property, it could be property on entity type or sub property on complex property. + /// + public IDictionary KeyProperties { get; } - Contract.Assert(options.EnableKeyInParenthesis || options.EnableKeyAsSegment); + /// + public IEdmNavigationSource NavigationSource { get; } - if (options.EnableKeyInParenthesis && options.EnableKeyAsSegment) - { - yield return $"({_keyLiteral})"; - yield return $"/{_keyLiteral}"; - } - else if (options.EnableKeyInParenthesis) - { - yield return $"({_keyLiteral})"; - } - else if (options.EnableKeyAsSegment) - { - yield return $"/{_keyLiteral}"; - } + /// + /// Gets the entity type declaring this key. + /// + public IEdmEntityType EntityType { get; } + + /// + /// Gets the key count + /// + public int Count => KeyMappings.Count; + + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + options = options ?? ODataRouteOptions.Default; + + Contract.Assert(options.EnableKeyInParenthesis || options.EnableKeyAsSegment); + + if (options.EnableKeyInParenthesis && options.EnableKeyAsSegment) + { + yield return $"({_keyLiteral})"; + yield return $"/{_keyLiteral}"; + } + else if (options.EnableKeyInParenthesis) + { + yield return $"({_keyLiteral})"; } + else if (options.EnableKeyAsSegment) + { + yield return $"/{_keyLiteral}"; + } + } - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + throw Error.ArgumentNull(nameof(context)); + } - RouteValueDictionary routeValues = context.RouteValues; - RouteValueDictionary updateValues = context.UpdatedValues; + RouteValueDictionary routeValues = context.RouteValues; + RouteValueDictionary updateValues = context.UpdatedValues; - IDictionary keysValues = new Dictionary(); - foreach (var key in KeyMappings) + IDictionary keysValues = new Dictionary(); + foreach (var key in KeyMappings) + { + string keyName = key.Key; + string templateName = key.Value; + if (routeValues.TryGetValue(templateName, out object rawValue)) { - string keyName = key.Key; - string templateName = key.Value; - if (routeValues.TryGetValue(templateName, out object rawValue)) - { - IEdmProperty keyProperty = KeyProperties.FirstOrDefault(k => k.Key == keyName).Value; - Contract.Assert(keyProperty != null); + IEdmProperty keyProperty = KeyProperties.FirstOrDefault(k => k.Key == keyName).Value; + Contract.Assert(keyProperty != null); - IEdmTypeReference edmType = keyProperty.Type; - string strValue = rawValue as string; + IEdmTypeReference edmType = keyProperty.Type; + string strValue = rawValue as string; - string newStrValue = context.GetParameterAliasOrSelf(strValue); + string newStrValue = context.GetParameterAliasOrSelf(strValue); - // rawValue from Request route values, it's unescaped except the back-slash. - newStrValue = newStrValue.UnescapeBackSlashUriString(); - if (newStrValue != strValue) - { - updateValues[templateName] = newStrValue; - strValue = newStrValue; - } + // rawValue from Request route values, it's unescaped except the back-slash. + newStrValue = newStrValue.UnescapeBackSlashUriString(); + if (newStrValue != strValue) + { + updateValues[templateName] = newStrValue; + strValue = newStrValue; + } - // If it's key as segment and the key type is Edm.String, we support non-single quoted string. - // Since we can't identify key as segment and key in parenthesis easy so far, - // we use the key literal with "/" to test in the whole route template. - // Why we can't create two key segment templates, one reason is that in attribute routing template, - // we can't identify key as segment or key in parenthesis also. - if (edmType.IsString() && context.IsPartOfRouteTemplate($"/{_keyLiteral}")) + // If it's key as segment and the key type is Edm.String, we support non-single quoted string. + // Since we can't identify key as segment and key in parenthesis easy so far, + // we use the key literal with "/" to test in the whole route template. + // Why we can't create two key segment templates, one reason is that in attribute routing template, + // we can't identify key as segment or key in parenthesis also. + if (edmType.IsString() && context.IsPartOfRouteTemplate($"/{_keyLiteral}")) + { + if (!strValue.StartsWith('\'') && !strValue.EndsWith('\'')) { - if (!strValue.StartsWith('\'') && !strValue.EndsWith('\'')) - { - strValue = $"'{strValue}'"; // prefix and suffix single quote - } + strValue = $"'{strValue}'"; // prefix and suffix single quote } + } - object newValue; - try + object newValue; + try + { + newValue = ODataUriUtils.ConvertFromUriLiteral(strValue, ODataVersion.V4, context.Model, edmType); + } + catch (ODataException ex) + { + string message = Error.Format(SRResources.InvalidKeyInUriFound, strValue, edmType.FullName()); + ILoggerFactory loggerFactory = context.HttpContext?.RequestServices?.GetService(); + if (loggerFactory != null) { - newValue = ODataUriUtils.ConvertFromUriLiteral(strValue, ODataVersion.V4, context.Model, edmType); + loggerFactory.CreateLogger().LogError(message, ex); + return false; } - catch (ODataException ex) + else { - string message = Error.Format(SRResources.InvalidKeyInUriFound, strValue, edmType.FullName()); - ILoggerFactory loggerFactory = context.HttpContext?.RequestServices?.GetService(); - if (loggerFactory != null) - { - loggerFactory.CreateLogger().LogError(message, ex); - return false; - } - else - { - throw new ODataException(message, ex); - } + throw new ODataException(message, ex); } + } - // for non FromODataUri, so update it, for example, remove the single quote for string value. - updateValues[templateName] = newValue; + // for non FromODataUri, so update it, for example, remove the single quote for string value. + updateValues[templateName] = newValue; - // For FromODataUri, let's refactor it later. - string prefixName = ODataParameterValue.ParameterValuePrefix + templateName; - updateValues[prefixName] = new ODataParameterValue(newValue, edmType); + // For FromODataUri, let's refactor it later. + string prefixName = ODataParameterValue.ParameterValuePrefix + templateName; + updateValues[prefixName] = new ODataParameterValue(newValue, edmType); - keysValues[keyName] = newValue; - } + keysValues[keyName] = newValue; } + } + + context.Segments.Add(new KeySegment(keysValues, EntityType, NavigationSource)); + return true; + } - context.Segments.Add(new KeySegment(keysValues, EntityType, NavigationSource)); - return true; + /// + /// Create based on the given entity type and navigation source. + /// + /// The given entity type. + /// The given navigation source. + /// The prefix used before key template. + /// The built . + internal static KeySegmentTemplate CreateKeySegment(IEdmEntityType entityType, IEdmNavigationSource navigationSource, string keyPrefix = "key") + { + if (entityType == null) + { + throw Error.ArgumentNull(nameof(entityType)); } - /// - /// Create based on the given entity type and navigation source. - /// - /// The given entity type. - /// The given navigation source. - /// The prefix used before key template. - /// The built . - internal static KeySegmentTemplate CreateKeySegment(IEdmEntityType entityType, IEdmNavigationSource navigationSource, string keyPrefix = "key") + IDictionary keyTemplates = new Dictionary(); + var keys = entityType.Key().ToArray(); + if (keys.Length == 1) + { + // Id={key} + keyTemplates[keys[0].Name] = $"{{{keyPrefix}}}"; + } + else { - if (entityType == null) + // Id1={keyId1},Id2={keyId2} + foreach (var key in keys) { - throw Error.ArgumentNull(nameof(entityType)); + keyTemplates[key.Name] = $"{{{keyPrefix}{key.Name}}}"; } + } - IDictionary keyTemplates = new Dictionary(); - var keys = entityType.Key().ToArray(); - if (keys.Length == 1) - { - // Id={key} - keyTemplates[keys[0].Name] = $"{{{keyPrefix}}}"; - } - else - { - // Id1={keyId1},Id2={keyId2} - foreach (var key in keys) - { - keyTemplates[key.Name] = $"{{{keyPrefix}{key.Name}}}"; - } - } + return new KeySegmentTemplate(keyTemplates, entityType, navigationSource); + } - return new KeySegmentTemplate(keyTemplates, entityType, navigationSource); - } + internal static IDictionary BuildKeyMappings(IEnumerable> keys, IEdmEntityType entityType) + { + return BuildKeyMappings(keys, entityType, entityType.Key().ToDictionary(k => k.Name, k => (IEdmProperty)k)); + } - internal static IDictionary BuildKeyMappings(IEnumerable> keys, IEdmEntityType entityType) + /// + /// Build the key mappings + /// + /// The Uri template parsing result. for example: SSN={ssnKey} + /// The Edm entity type. + /// The key properties. + /// The mapping + internal static IDictionary BuildKeyMappings(IEnumerable> keys, + IEdmEntityType entityType, IDictionary keyProperties) + { + Contract.Assert(keys != null); + Contract.Assert(entityType != null); + Contract.Assert(keyProperties != null); + + Dictionary parameterMappings = new Dictionary(); + + int count = keys.Count(); + if (count != keyProperties.Count) { - return BuildKeyMappings(keys, entityType, entityType.Key().ToDictionary(k => k.Name, k => (IEdmProperty)k)); + throw new ODataException(Error.Format(SRResources.InputKeyNotMatchEntityTypeKey, count, keyProperties.Count, entityType.FullName())); } - /// - /// Build the key mappings - /// - /// The Uri template parsing result. for example: SSN={ssnKey} - /// The Edm entity type. - /// The key properties. - /// The mapping - internal static IDictionary BuildKeyMappings(IEnumerable> keys, - IEdmEntityType entityType, IDictionary keyProperties) + // keys have: SSN={ssn},Name={name} + // the key "SSN" or "Name" is the alias in alternate keys + foreach (KeyValuePair key in keys) { - Contract.Assert(keys != null); - Contract.Assert(entityType != null); - Contract.Assert(keyProperties != null); - - Dictionary parameterMappings = new Dictionary(); + string keyName = key.Key; - int count = keys.Count(); - if (count != keyProperties.Count) + // key name is case-sensitive + if (!keyProperties.ContainsKey(key.Key)) { - throw new ODataException(Error.Format(SRResources.InputKeyNotMatchEntityTypeKey, count, keyProperties.Count, entityType.FullName())); + throw new ODataException(Error.Format(SRResources.CannotFindKeyInEntityType, keyName, entityType.FullName())); } - // keys have: SSN={ssn},Name={name} - // the key "SSN" or "Name" is the alias in alternate keys - foreach (KeyValuePair key in keys) - { - string keyName = key.Key; + string nameInRouteData; - // key name is case-sensitive - if (!keyProperties.ContainsKey(key.Key)) - { - throw new ODataException(Error.Format(SRResources.CannotFindKeyInEntityType, keyName, entityType.FullName())); - } - - string nameInRouteData; - - UriTemplateExpression uriTemplateExpression = key.Value as UriTemplateExpression; - if (uriTemplateExpression != null) - { - nameInRouteData = uriTemplateExpression.LiteralText.Trim(); - } - else - { - // just for easy construct the key segment template - // it must start with "{" and end with "}" - nameInRouteData = key.Value as string; - } - - if (nameInRouteData == null || !nameInRouteData.IsValidTemplateLiteral()) - { - throw new ODataException(Error.Format(SRResources.KeyTemplateMustBeInCurlyBraces, key.Value, key.Key)); - } + UriTemplateExpression uriTemplateExpression = key.Value as UriTemplateExpression; + if (uriTemplateExpression != null) + { + nameInRouteData = uriTemplateExpression.LiteralText.Trim(); + } + else + { + // just for easy construct the key segment template + // it must start with "{" and end with "}" + nameInRouteData = key.Value as string; + } - nameInRouteData = nameInRouteData.Substring(1, nameInRouteData.Length - 2); - if (string.IsNullOrEmpty(nameInRouteData)) - { - throw new ODataException(Error.Format(SRResources.EmptyKeyTemplate, key.Value, key.Key)); - } + if (nameInRouteData == null || !nameInRouteData.IsValidTemplateLiteral()) + { + throw new ODataException(Error.Format(SRResources.KeyTemplateMustBeInCurlyBraces, key.Value, key.Key)); + } - parameterMappings[key.Key] = nameInRouteData; + nameInRouteData = nameInRouteData.Substring(1, nameInRouteData.Length - 2); + if (string.IsNullOrEmpty(nameInRouteData)) + { + throw new ODataException(Error.Format(SRResources.EmptyKeyTemplate, key.Value, key.Key)); } - return parameterMappings; + parameterMappings[key.Key] = nameInRouteData; } + + return parameterMappings; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/MetadataSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/MetadataSegmentTemplate.cs index ac1dc035f..789541f56 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/MetadataSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/MetadataSegmentTemplate.cs @@ -8,41 +8,40 @@ using System.Collections.Generic; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match "$metadata". +/// +public class MetadataSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that could match "$metadata". + /// Gets the static instance of $metadata + /// + public static MetadataSegmentTemplate Instance { get; } = new MetadataSegmentTemplate(); + + /// + /// Initializes a new instance of the class. /// - public class MetadataSegmentTemplate : ODataSegmentTemplate + private MetadataSegmentTemplate() { - /// - /// Gets the static instance of $metadata - /// - public static MetadataSegmentTemplate Instance { get; } = new MetadataSegmentTemplate(); + } - /// - /// Initializes a new instance of the class. - /// - private MetadataSegmentTemplate() - { - } + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return "/$metadata"; + } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) { - yield return "/$metadata"; + throw Error.ArgumentNull(nameof(context)); } - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - context.Segments.Add(MetadataSegment.Instance); - return true; - } + context.Segments.Add(MetadataSegment.Instance); + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/NavigationLinkSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/NavigationLinkSegmentTemplate.cs index bf627408d..ddb5e0604 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/NavigationLinkSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/NavigationLinkSegmentTemplate.cs @@ -9,87 +9,86 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that can match a and a potential key. +/// +public class NavigationLinkSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that can match a and a potential key. + /// Initializes a new instance of the class. /// - public class NavigationLinkSegmentTemplate : ODataSegmentTemplate + /// The Edm navigation property. + /// The Edm navigation source. This could be null. + public NavigationLinkSegmentTemplate(IEdmNavigationProperty navigationProperty, IEdmNavigationSource navigationSource) + : this(new NavigationPropertyLinkSegment(navigationProperty, navigationSource)) { - /// - /// Initializes a new instance of the class. - /// - /// The Edm navigation property. - /// The Edm navigation source. This could be null. - public NavigationLinkSegmentTemplate(IEdmNavigationProperty navigationProperty, IEdmNavigationSource navigationSource) - : this(new NavigationPropertyLinkSegment(navigationProperty, navigationSource)) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The navigation link segment. - public NavigationLinkSegmentTemplate(NavigationPropertyLinkSegment segment) - { - Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); - } + /// + /// Initializes a new instance of the class. + /// + /// The navigation link segment. + public NavigationLinkSegmentTemplate(NavigationPropertyLinkSegment segment) + { + Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); + } - /// - /// Gets/sets the related key template. - /// - public KeySegmentTemplate Key { get; set; } + /// + /// Gets/sets the related key template. + /// + public KeySegmentTemplate Key { get; set; } - /// - /// Gets the Edm navigation property. - /// - public IEdmNavigationProperty NavigationProperty => Segment.NavigationProperty; + /// + /// Gets the Edm navigation property. + /// + public IEdmNavigationProperty NavigationProperty => Segment.NavigationProperty; - /// - /// Gets the navigation source. - /// - public IEdmNavigationSource NavigationSource => Segment.NavigationSource; + /// + /// Gets the navigation source. + /// + public IEdmNavigationSource NavigationSource => Segment.NavigationSource; - /// - /// Gets the navigation property link segment. - /// - public NavigationPropertyLinkSegment Segment { get; } + /// + /// Gets the navigation property link segment. + /// + public NavigationPropertyLinkSegment Segment { get; } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + if (Key != null) { - if (Key != null) - { - IEnumerable keyTemplates = Key.GetTemplates(options); - foreach (var keyTemplate in keyTemplates) - { - yield return $"/{NavigationProperty.Name}{keyTemplate}/$ref"; - } - } - else + IEnumerable keyTemplates = Key.GetTemplates(options); + foreach (var keyTemplate in keyTemplates) { - yield return $"/{NavigationProperty.Name}/$ref"; + yield return $"/{NavigationProperty.Name}{keyTemplate}/$ref"; } } - - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + else { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + yield return $"/{NavigationProperty.Name}/$ref"; + } + } - // ODL path has the "NavigationPropertyLinkSegment" first. - context.Segments.Add(Segment); + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - // Then, append the key if apply. - if (Key != null) - { - return Key.TryTranslate(context); - } + // ODL path has the "NavigationPropertyLinkSegment" first. + context.Segments.Add(Segment); - return true; + // Then, append the key if apply. + if (Key != null) + { + return Key.TryTranslate(context); } + + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/NavigationLinkTemplateSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/NavigationLinkTemplateSegmentTemplate.cs index 92ef38ae0..3392862aa 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/NavigationLinkTemplateSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/NavigationLinkTemplateSegmentTemplate.cs @@ -14,122 +14,121 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match a $ref on a generic navigation segment. +/// +public class NavigationLinkTemplateSegmentTemplate : ODataSegmentTemplate { + private readonly string ParameterName = "navigationProperty"; + /// - /// Represents a template that could match a $ref on a generic navigation segment. + /// Initializes a new instance of the class. /// - public class NavigationLinkTemplateSegmentTemplate : ODataSegmentTemplate + /// The declaring type. + /// The Edm navigation source. + public NavigationLinkTemplateSegmentTemplate(IEdmStructuredType declaringType, IEdmNavigationSource navigationSource) { - private readonly string ParameterName = "navigationProperty"; - - /// - /// Initializes a new instance of the class. - /// - /// The declaring type. - /// The Edm navigation source. - public NavigationLinkTemplateSegmentTemplate(IEdmStructuredType declaringType, IEdmNavigationSource navigationSource) - { - DeclaringType = declaringType ?? throw Error.ArgumentNull(nameof(declaringType)); - NavigationSource = navigationSource ?? throw Error.ArgumentNull(nameof(navigationSource)); - } + DeclaringType = declaringType ?? throw Error.ArgumentNull(nameof(declaringType)); + NavigationSource = navigationSource ?? throw Error.ArgumentNull(nameof(navigationSource)); + } - /// - /// Gets the related key mapping. - /// - public string RelatedKey { get; set; } + /// + /// Gets the related key mapping. + /// + public string RelatedKey { get; set; } - /// - /// Gets the declaring type for this property template. - /// - public IEdmStructuredType DeclaringType { get; } + /// + /// Gets the declaring type for this property template. + /// + public IEdmStructuredType DeclaringType { get; } - /// - /// Gets the navigation source. - /// - public IEdmNavigationSource NavigationSource { get; } + /// + /// Gets the navigation source. + /// + public IEdmNavigationSource NavigationSource { get; } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + if (RelatedKey != null) { - if (RelatedKey != null) + options = options ?? ODataRouteOptions.Default; + Contract.Assert(options.EnableKeyInParenthesis || options.EnableKeyAsSegment); + + if (options.EnableKeyInParenthesis && options.EnableKeyAsSegment) + { + yield return $"/{{{ParameterName}}}({{{RelatedKey}}})/$ref"; + yield return $"/{{{ParameterName}}}/{{{RelatedKey}}}/$ref"; + } + else if (options.EnableKeyInParenthesis) { - options = options ?? ODataRouteOptions.Default; - Contract.Assert(options.EnableKeyInParenthesis || options.EnableKeyAsSegment); - - if (options.EnableKeyInParenthesis && options.EnableKeyAsSegment) - { - yield return $"/{{{ParameterName}}}({{{RelatedKey}}})/$ref"; - yield return $"/{{{ParameterName}}}/{{{RelatedKey}}}/$ref"; - } - else if (options.EnableKeyInParenthesis) - { - yield return $"/{{{ParameterName}}}({{{RelatedKey}}})/$ref"; - } - else - { - yield return $"/{{{ParameterName}}}/{{{RelatedKey}}}/$ref"; - } + yield return $"/{{{ParameterName}}}({{{RelatedKey}}})/$ref"; } else { - yield return $"/{{{ParameterName}}}/$ref"; + yield return $"/{{{ParameterName}}}/{{{RelatedKey}}}/$ref"; } } - - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + else { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } + yield return $"/{{{ParameterName}}}/$ref"; + } + } - RouteValueDictionary routeValues = context.RouteValues; + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } - // the request should have the navigation property - if (!routeValues.TryGetValue(ParameterName, out object rawValue)) - { - return false; - } + RouteValueDictionary routeValues = context.RouteValues; - string navigationProperty = rawValue as string; - if (navigationProperty == null) - { - return false; - } + // the request should have the navigation property + if (!routeValues.TryGetValue(ParameterName, out object rawValue)) + { + return false; + } - // Find the navigation property - IEdmNavigationProperty edmNavProperty = DeclaringType.ResolveProperty(navigationProperty) as IEdmNavigationProperty; - if (edmNavProperty == null) - { - return false; - } + string navigationProperty = rawValue as string; + if (navigationProperty == null) + { + return false; + } - IEdmNavigationSource targetNavigationSource = NavigationSource.FindNavigationTarget(edmNavProperty); + // Find the navigation property + IEdmNavigationProperty edmNavProperty = DeclaringType.ResolveProperty(navigationProperty) as IEdmNavigationProperty; + if (edmNavProperty == null) + { + return false; + } - // ODL implementation is complex, here i just use the NavigationPropertyLinkSegment - context.Segments.Add(new NavigationPropertyLinkSegment(edmNavProperty, targetNavigationSource)); + IEdmNavigationSource targetNavigationSource = NavigationSource.FindNavigationTarget(edmNavProperty); - if (RelatedKey != null) - { - IEdmEntityType entityType = edmNavProperty.ToEntityType(); + // ODL implementation is complex, here i just use the NavigationPropertyLinkSegment + context.Segments.Add(new NavigationPropertyLinkSegment(edmNavProperty, targetNavigationSource)); - // only handle the single key - Contract.Assert(entityType.Key().Count() == 1); - IEdmStructuralProperty keyProperty = entityType.Key().SingleOrDefault(); - Contract.Assert(keyProperty != null); + if (RelatedKey != null) + { + IEdmEntityType entityType = edmNavProperty.ToEntityType(); - IDictionary keyValuePairs = new Dictionary - { - { keyProperty.Name, $"{{{RelatedKey}}}" } - }; + // only handle the single key + Contract.Assert(entityType.Key().Count() == 1); + IEdmStructuralProperty keyProperty = entityType.Key().SingleOrDefault(); + Contract.Assert(keyProperty != null); - KeySegmentTemplate keySegment = new KeySegmentTemplate(keyValuePairs, entityType, targetNavigationSource); - return keySegment.TryTranslate(context); - } + IDictionary keyValuePairs = new Dictionary + { + { keyProperty.Name, $"{{{RelatedKey}}}" } + }; - return true; + KeySegmentTemplate keySegment = new KeySegmentTemplate(keyValuePairs, entityType, targetNavigationSource); + return keySegment.TryTranslate(context); } + + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/NavigatonSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/NavigatonSegmentTemplate.cs index 2a01b6852..ba7523ffb 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/NavigatonSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/NavigatonSegmentTemplate.cs @@ -9,58 +9,57 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match an . +/// +public class NavigationSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that could match an . + /// Initializes a new instance of the class. /// - public class NavigationSegmentTemplate : ODataSegmentTemplate + /// The Edm navigation property. + /// The target navigation source. + public NavigationSegmentTemplate(IEdmNavigationProperty navigationProperty, IEdmNavigationSource navigationSource) + : this(new NavigationPropertySegment(navigationProperty, navigationSource)) { - /// - /// Initializes a new instance of the class. - /// - /// The Edm navigation property. - /// The target navigation source. - public NavigationSegmentTemplate(IEdmNavigationProperty navigationProperty, IEdmNavigationSource navigationSource) - : this(new NavigationPropertySegment(navigationProperty, navigationSource)) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The navigation property segment. - public NavigationSegmentTemplate(NavigationPropertySegment segment) - { - Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); - } + /// + /// Initializes a new instance of the class. + /// + /// The navigation property segment. + public NavigationSegmentTemplate(NavigationPropertySegment segment) + { + Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); + } - /// - /// Gets the wrapped Edm navigation property. - /// - public IEdmNavigationProperty NavigationProperty => Segment.NavigationProperty; + /// + /// Gets the wrapped Edm navigation property. + /// + public IEdmNavigationProperty NavigationProperty => Segment.NavigationProperty; - /// - /// Gets the navigation property segment. - /// - public NavigationPropertySegment Segment { get; } + /// + /// Gets the navigation property segment. + /// + public NavigationPropertySegment Segment { get; } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) - { - yield return $"/{NavigationProperty.Name}"; - } + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return $"/{NavigationProperty.Name}"; + } - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - context.Segments.Add(Segment); - return true; + throw Error.ArgumentNull(nameof(context)); } + + context.Segments.Add(Segment); + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/ODataPathTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/ODataPathTemplate.cs index 8cee9285c..b07e838c4 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/ODataPathTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/ODataPathTemplate.cs @@ -9,87 +9,86 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a path template that could contains a list of . +/// +[SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "ODataPathTemplate is ok.")] +public class ODataPathTemplate : List { /// - /// Represents a path template that could contains a list of . + /// Initializes a new instance of the class. /// - [SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "ODataPathTemplate is ok.")] - public class ODataPathTemplate : List + /// The path segment templates for the path. + public ODataPathTemplate(params ODataSegmentTemplate[] segments) + : base(segments) { - /// - /// Initializes a new instance of the class. - /// - /// The path segment templates for the path. - public ODataPathTemplate(params ODataSegmentTemplate[] segments) - : base(segments) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The path segment templates for the path. - public ODataPathTemplate(IEnumerable segments) - : base(segments) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The path segment templates for the path. + public ODataPathTemplate(IEnumerable segments) + : base(segments) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The path segments for the path. - public ODataPathTemplate(IList segments) - : base(segments) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The path segments for the path. + public ODataPathTemplate(IList segments) + : base(segments) + { + } - /// - /// Generates all templates for the given using the given . - /// All templates mean: - /// 1) for key segment, we have key in parenthesis and key as segment. - /// 2) for bound function/action segment, we have qualified function call and unqualified function call. - /// All of such might be based on route options. - /// - /// The route options. - /// All path templates. - public virtual IEnumerable GetTemplates(ODataRouteOptions options = null) - { - options = options ?? ODataRouteOptions.Default; + /// + /// Generates all templates for the given using the given . + /// All templates mean: + /// 1) for key segment, we have key in parenthesis and key as segment. + /// 2) for bound function/action segment, we have qualified function call and unqualified function call. + /// All of such might be based on route options. + /// + /// The route options. + /// All path templates. + public virtual IEnumerable GetTemplates(ODataRouteOptions options = null) + { + options = options ?? ODataRouteOptions.Default; - Stack stack = new Stack(); + Stack stack = new Stack(); - IList templates = new List(); + IList templates = new List(); - ProcessSegment(stack, 0, Count, templates, options); + ProcessSegment(stack, 0, Count, templates, options); - return templates; - } + return templates; + } - private void ProcessSegment(Stack stack, int index, int count, IList templates, ODataRouteOptions options) + private void ProcessSegment(Stack stack, int index, int count, IList templates, ODataRouteOptions options) + { + if (index == count) { - if (index == count) + string pathTemplate = string.Join("", stack.Reverse()); + if (pathTemplate.StartsWith('/')) { - string pathTemplate = string.Join("", stack.Reverse()); - if (pathTemplate.StartsWith('/')) - { - pathTemplate = pathTemplate.Substring(1); // remove the first "/" - } - - templates.Add(pathTemplate); - return; + pathTemplate = pathTemplate.Substring(1); // remove the first "/" } - ODataSegmentTemplate segment = this[index]; + templates.Add(pathTemplate); + return; + } - foreach (string template in segment.GetTemplates(options)) - { - stack.Push(template); + ODataSegmentTemplate segment = this[index]; - ProcessSegment(stack, index + 1, count, templates, options); + foreach (string template in segment.GetTemplates(options)) + { + stack.Push(template); - stack.Pop(); - } + ProcessSegment(stack, index + 1, count, templates, options); + + stack.Pop(); } } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/ODataSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/ODataSegmentTemplate.cs index f50b528b0..0092bb929 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/ODataSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/ODataSegmentTemplate.cs @@ -8,41 +8,40 @@ using System.Collections.Generic; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Base class for OData segment template +/// +public abstract class ODataSegmentTemplate { /// - /// Base class for OData segment template + /// Gets the templates. It's case-insensitive template. + /// It's used to build the routing template in conventional routing. + /// It's not used in attribute routing. + /// The template string should include the leading "/" if apply. /// - public abstract class ODataSegmentTemplate - { - /// - /// Gets the templates. It's case-insensitive template. - /// It's used to build the routing template in conventional routing. - /// It's not used in attribute routing. - /// The template string should include the leading "/" if apply. - /// - /// The route options. - /// The built segment templates. - public abstract IEnumerable GetTemplates(ODataRouteOptions options); + /// The route options. + /// The built segment templates. + public abstract IEnumerable GetTemplates(ODataRouteOptions options); - /// - /// Translate the template into a real OData path segment - /// - /// The translate context. - /// True if translated. False if no. - public abstract bool TryTranslate(ODataTemplateTranslateContext context); + /// + /// Translate the template into a real OData path segment + /// + /// The translate context. + /// True if translated. False if no. + public abstract bool TryTranslate(ODataTemplateTranslateContext context); - /// - /// Gets the templates by default. It's case-insensitive template. - /// It's used to build the routing template in conventional routing. - /// It's not used in attribute routing. - /// The template string should include the leading "/" if apply. - /// - /// Used in Unit test. - /// The built segment templates. - internal IEnumerable GetTemplates() - { - return GetTemplates(null); - } + /// + /// Gets the templates by default. It's case-insensitive template. + /// It's used to build the routing template in conventional routing. + /// It's not used in attribute routing. + /// The template string should include the leading "/" if apply. + /// + /// Used in Unit test. + /// The built segment templates. + internal IEnumerable GetTemplates() + { + return GetTemplates(null); } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/ODataTemplateTranslateContext.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/ODataTemplateTranslateContext.cs index 94e9fd482..808de243a 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/ODataTemplateTranslateContext.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/ODataTemplateTranslateContext.cs @@ -14,137 +14,136 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// The context used to generate the . +/// +public class ODataTemplateTranslateContext { /// - /// The context used to generate the . + /// Initializes a new instance of the class. + /// For Unit test only. + /// + internal ODataTemplateTranslateContext() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The current HttpContext. + /// The current endpoint to match. + /// The current route values. + /// The current Edm model. + public ODataTemplateTranslateContext(HttpContext context, Endpoint endpoint, RouteValueDictionary routeValues, IEdmModel model) + { + HttpContext = context ?? throw Error.ArgumentNull(nameof(context)); + + Endpoint = endpoint ?? throw Error.ArgumentNull(nameof(endpoint)); + + RouteValues = routeValues ?? throw Error.ArgumentNull(nameof(routeValues)); + + Model = model ?? throw Error.ArgumentNull(nameof(model)); + } + + /// + /// Gets the current Endpoint . + /// + /// + /// The internal setter is provided for unit test purposes only. + /// + public Endpoint Endpoint { get; internal set; } + + /// + /// Gets the current HttpContext . + /// + /// + /// The internal setter is provided for unit test purposes only. + /// + public HttpContext HttpContext { get; internal set; } + + /// + /// Gets the route values . + /// + /// + /// The internal setter is provided for unit test purposes only. + /// + public RouteValueDictionary RouteValues { get; internal set; } + + /// + /// Gets the Edm model . /// - public class ODataTemplateTranslateContext + /// + /// The internal setter is provided for unit test purposes only. + /// + public IEdmModel Model { get; internal set; } + + /// + /// Gets the updated route values. This will include the updated route values. + /// + public RouteValueDictionary UpdatedValues { get; } = new RouteValueDictionary(); + + /// + /// Gets the generated path segments. + /// + public IList Segments { get; } = new List(); + + /// + /// Gets the parameter alias or the alias name itself. + /// + /// The potential alias name. + /// The parameter alias name. + public string GetParameterAliasOrSelf(string alias) { - /// - /// Initializes a new instance of the class. - /// For Unit test only. - /// - internal ODataTemplateTranslateContext() - { } - - /// - /// Initializes a new instance of the class. - /// - /// The current HttpContext. - /// The current endpoint to match. - /// The current route values. - /// The current Edm model. - public ODataTemplateTranslateContext(HttpContext context, Endpoint endpoint, RouteValueDictionary routeValues, IEdmModel model) + return GetParameterAliasOrSelf(alias, new HashSet()); + } + + private string GetParameterAliasOrSelf(string alias, ISet visited) + { + if (visited.Contains(alias)) { - HttpContext = context ?? throw Error.ArgumentNull(nameof(context)); + // process "?@p1=@p2&@p2=@p1" infinite loop? + throw new ODataException(Error.Format(SRResources.InfiniteParameterAlias, alias)); + } - Endpoint = endpoint ?? throw Error.ArgumentNull(nameof(endpoint)); + visited.Add(alias); - RouteValues = routeValues ?? throw Error.ArgumentNull(nameof(routeValues)); + if (alias == null) + { + return null; + } - Model = model ?? throw Error.ArgumentNull(nameof(model)); + // parameter alias starts with "@" + if (!alias.StartsWith("@", StringComparison.Ordinal)) + { + return alias; } - /// - /// Gets the current Endpoint . - /// - /// - /// The internal setter is provided for unit test purposes only. - /// - public Endpoint Endpoint { get; internal set; } - - /// - /// Gets the current HttpContext . - /// - /// - /// The internal setter is provided for unit test purposes only. - /// - public HttpContext HttpContext { get; internal set; } - - /// - /// Gets the route values . - /// - /// - /// The internal setter is provided for unit test purposes only. - /// - public RouteValueDictionary RouteValues { get; internal set; } - - /// - /// Gets the Edm model . - /// - /// - /// The internal setter is provided for unit test purposes only. - /// - public IEdmModel Model { get; internal set; } - - /// - /// Gets the updated route values. This will include the updated route values. - /// - public RouteValueDictionary UpdatedValues { get; } = new RouteValueDictionary(); - - /// - /// Gets the generated path segments. - /// - public IList Segments { get; } = new List(); - - /// - /// Gets the parameter alias or the alias name itself. - /// - /// The potential alias name. - /// The parameter alias name. - public string GetParameterAliasOrSelf(string alias) + if (!HttpContext.Request.Query.TryGetValue(alias, out StringValues values)) { - return GetParameterAliasOrSelf(alias, new HashSet()); + throw new ODataException(Error.Format(SRResources.MissingParameterAlias, alias)); } - private string GetParameterAliasOrSelf(string alias, ISet visited) + alias = values; + + // Go to next level of parameter alias "?@p1=@p2&@p2=abc" + return GetParameterAliasOrSelf(alias, visited); + } + + internal bool IsPartOfRouteTemplate(string part) + { + string template = null; + RouteEndpoint routeEndpoint = Endpoint as RouteEndpoint; + if (routeEndpoint != null) { - if (visited.Contains(alias)) - { - // process "?@p1=@p2&@p2=@p1" infinite loop? - throw new ODataException(Error.Format(SRResources.InfiniteParameterAlias, alias)); - } - - visited.Add(alias); - - if (alias == null) - { - return null; - } - - // parameter alias starts with "@" - if (!alias.StartsWith("@", StringComparison.Ordinal)) - { - return alias; - } - - if (!HttpContext.Request.Query.TryGetValue(alias, out StringValues values)) - { - throw new ODataException(Error.Format(SRResources.MissingParameterAlias, alias)); - } - - alias = values; - - // Go to next level of parameter alias "?@p1=@p2&@p2=abc" - return GetParameterAliasOrSelf(alias, visited); + template = routeEndpoint.RoutePattern.RawText; } - internal bool IsPartOfRouteTemplate(string part) + if (template != null) { - string template = null; - RouteEndpoint routeEndpoint = Endpoint as RouteEndpoint; - if (routeEndpoint != null) - { - template = routeEndpoint.RoutePattern.RawText; - } - - if (template != null) - { - return template.Contains(part, StringComparison.OrdinalIgnoreCase); - } - - return false; + return template.Contains(part, StringComparison.OrdinalIgnoreCase); } + + return false; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/PathTemplateSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/PathTemplateSegmentTemplate.cs index a572129bb..32298555a 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/PathTemplateSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/PathTemplateSegmentTemplate.cs @@ -13,163 +13,162 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that can match a . +/// From OData Lib: +/// If template parsing enabled, any literal wrapped with "{" and "}" is considered as PathTemplateSegment. +/// So, here's the design (so far, we can add more later): +/// {property} ==> declared property +/// {dynamicproperty} => dynamic property +/// TODO: we can change to use route constraint, for example: +/// {name:odataproperty} +/// {name:odatadynamic} +/// {name:odatacast} +/// {name:odataentityset} ... +/// +public class PathTemplateSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that can match a . - /// From OData Lib: - /// If template parsing enabled, any literal wrapped with "{" and "}" is considered as PathTemplateSegment. - /// So, here's the design (so far, we can add more later): - /// {property} ==> declared property - /// {dynamicproperty} => dynamic property - /// TODO: we can change to use route constraint, for example: - /// {name:odataproperty} - /// {name:odatadynamic} - /// {name:odatacast} - /// {name:odataentityset} ... + /// Initializes a new instance of the class. /// - public class PathTemplateSegmentTemplate : ODataSegmentTemplate + /// The path template segment to be parsed as a template. + public PathTemplateSegmentTemplate(PathTemplateSegment segment) { - /// - /// Initializes a new instance of the class. - /// - /// The path template segment to be parsed as a template. - public PathTemplateSegmentTemplate(PathTemplateSegment segment) - { - Segment = segment ?? throw new ArgumentNullException(nameof(segment)); + Segment = segment ?? throw new ArgumentNullException(nameof(segment)); - if (!IsRouteParameter(segment.LiteralText)) - { - throw new ODataException(Error.Format(SRResources.InvalidAttributeRoutingTemplateSegment, segment.LiteralText)); - } + if (!IsRouteParameter(segment.LiteralText)) + { + throw new ODataException(Error.Format(SRResources.InvalidAttributeRoutingTemplateSegment, segment.LiteralText)); + } - // ParameterName = property or dynamicproperty - ParameterName = segment.LiteralText.Substring(1, segment.LiteralText.Length - 2); + // ParameterName = property or dynamicproperty + ParameterName = segment.LiteralText.Substring(1, segment.LiteralText.Length - 2); - if (string.IsNullOrEmpty(ParameterName)) - { - throw new ODataException(Error.Format(SRResources.EmptyPathTemplate, segment.LiteralText)); - } + if (string.IsNullOrEmpty(ParameterName)) + { + throw new ODataException(Error.Format(SRResources.EmptyPathTemplate, segment.LiteralText)); } + } - /// - /// Gets the segment name - /// - public string ParameterName { get; } + /// + /// Gets the segment name + /// + public string ParameterName { get; } - /// - /// The parameter name of the dynamic property. - /// - public PathTemplateSegment Segment { get; } + /// + /// The parameter name of the dynamic property. + /// + public PathTemplateSegment Segment { get; } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return $"/{Segment.LiteralText}"; + } + + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) { - yield return $"/{Segment.LiteralText}"; + throw Error.ArgumentNull(nameof(context)); } - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + // {property} + if (ParameterName == "property") { - if (context == null) + ODataPathSegment previous = context.Segments.LastOrDefault(); + ODataPathSegment property = CreatePropertySegment(previous, context); + if (property != null) { - throw Error.ArgumentNull(nameof(context)); + context.Segments.Add(property); + return true; } - - // {property} - if (ParameterName == "property") - { - ODataPathSegment previous = context.Segments.LastOrDefault(); - ODataPathSegment property = CreatePropertySegment(previous, context); - if (property != null) - { - context.Segments.Add(property); - return true; - } - } - else if (ParameterName == "dynamicproperty") - { - // {dynamicproperty} - ODataPathSegment previous = context.Segments.LastOrDefault(); - DynamicPathSegment dynamicSeg = CreateDynamicSegment(previous, context); - if (dynamicSeg != null) - { - context.Segments.Add(dynamicSeg); - return true; - } - } - - return false; } - - private static ODataPathSegment CreatePropertySegment(ODataPathSegment previous, ODataTemplateTranslateContext context) + else if (ParameterName == "dynamicproperty") { - if (previous == null) - { - return null; - } - - IEdmStructuredType previousEdmType = previous.EdmType as IEdmStructuredType; - if (previousEdmType == null) + // {dynamicproperty} + ODataPathSegment previous = context.Segments.LastOrDefault(); + DynamicPathSegment dynamicSeg = CreateDynamicSegment(previous, context); + if (dynamicSeg != null) { - return null; + context.Segments.Add(dynamicSeg); + return true; } + } - if (!context.RouteValues.TryGetValue("property", out object value)) - { - return null; - } + return false; + } - string propertyName = value as string; - IEdmProperty edmProperty = previousEdmType.ResolveProperty(propertyName); - IEdmStructuralProperty structuralProperty = edmProperty as IEdmStructuralProperty; - if (structuralProperty != null) - { - return new PropertySegment(structuralProperty); - } + private static ODataPathSegment CreatePropertySegment(ODataPathSegment previous, ODataTemplateTranslateContext context) + { + if (previous == null) + { + return null; + } - IEdmNavigationProperty navProperty = edmProperty as IEdmNavigationProperty; - if (navProperty != null) - { - // TODO: shall we calculate the navigation source for navigation segment? - return new NavigationPropertySegment(navProperty, null); - } + IEdmStructuredType previousEdmType = previous.EdmType as IEdmStructuredType; + if (previousEdmType == null) + { + return null; + } + if (!context.RouteValues.TryGetValue("property", out object value)) + { return null; } - private static DynamicPathSegment CreateDynamicSegment(ODataPathSegment previous, ODataTemplateTranslateContext context) + string propertyName = value as string; + IEdmProperty edmProperty = previousEdmType.ResolveProperty(propertyName); + IEdmStructuralProperty structuralProperty = edmProperty as IEdmStructuralProperty; + if (structuralProperty != null) { - if (previous == null) - { - return null; - } + return new PropertySegment(structuralProperty); + } - IEdmStructuredType previousEdmType = previous.EdmType as IEdmStructuredType; - if (previousEdmType == null || !previousEdmType.IsOpen) - { - return null; - } + IEdmNavigationProperty navProperty = edmProperty as IEdmNavigationProperty; + if (navProperty != null) + { + // TODO: shall we calculate the navigation source for navigation segment? + return new NavigationPropertySegment(navProperty, null); + } - if (!context.RouteValues.TryGetValue("dynamicproperty", out object value)) - { - return null; - } + return null; + } - string propertyName = value as string; - IEdmProperty edmProperty = previousEdmType.ResolveProperty(propertyName); - if (edmProperty != null) - { - return null; - } + private static DynamicPathSegment CreateDynamicSegment(ODataPathSegment previous, ODataTemplateTranslateContext context) + { + if (previous == null) + { + return null; + } - return new DynamicPathSegment(propertyName); + IEdmStructuredType previousEdmType = previous.EdmType as IEdmStructuredType; + if (previousEdmType == null || !previousEdmType.IsOpen) + { + return null; } - private static bool IsRouteParameter(string parameterName) + if (!context.RouteValues.TryGetValue("dynamicproperty", out object value)) { - return parameterName.StartsWith("{", StringComparison.Ordinal) && - parameterName.EndsWith("}", StringComparison.Ordinal); + return null; } + + string propertyName = value as string; + IEdmProperty edmProperty = previousEdmType.ResolveProperty(propertyName); + if (edmProperty != null) + { + return null; + } + + return new DynamicPathSegment(propertyName); + } + + private static bool IsRouteParameter(string parameterName) + { + return parameterName.StartsWith("{", StringComparison.Ordinal) && + parameterName.EndsWith("}", StringComparison.Ordinal); } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/PropertyCatchAllSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/PropertyCatchAllSegmentTemplate.cs index 64d16de8b..372227244 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/PropertyCatchAllSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/PropertyCatchAllSegmentTemplate.cs @@ -10,53 +10,52 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match '{property}' segment. +/// +public class PropertyCatchAllSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that could match '{property}' segment. + /// Initializes a new instance of the class. /// - public class PropertyCatchAllSegmentTemplate : ODataSegmentTemplate + /// The declared type. + public PropertyCatchAllSegmentTemplate(IEdmStructuredType declaredType) { - /// - /// Initializes a new instance of the class. - /// - /// The declared type. - public PropertyCatchAllSegmentTemplate(IEdmStructuredType declaredType) - { - StructuredType = declaredType ?? throw Error.ArgumentNull(nameof(declaredType)); - } + StructuredType = declaredType ?? throw Error.ArgumentNull(nameof(declaredType)); + } - /// - /// Gets the declared type for this property template. - /// - public IEdmStructuredType StructuredType { get; } + /// + /// Gets the declared type for this property template. + /// + public IEdmStructuredType StructuredType { get; } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return "/{property}"; + } + + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) { - yield return "/{property}"; + throw Error.ArgumentNull(nameof(context)); } - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + if (context.RouteValues.TryGetValue("property", out object value)) { - if (context == null) + string rawValue = value as string; + IEdmProperty edmProperty = StructuredType.ResolveProperty(rawValue); + if (edmProperty != null && edmProperty.PropertyKind == EdmPropertyKind.Structural) { - throw Error.ArgumentNull(nameof(context)); + context.Segments.Add(new PropertySegment((IEdmStructuralProperty)edmProperty)); + return true; } - - if (context.RouteValues.TryGetValue("property", out object value)) - { - string rawValue = value as string; - IEdmProperty edmProperty = StructuredType.ResolveProperty(rawValue); - if (edmProperty != null && edmProperty.PropertyKind == EdmPropertyKind.Structural) - { - context.Segments.Add(new PropertySegment((IEdmStructuralProperty)edmProperty)); - return true; - } - } - - return false; } + + return false; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/PropertySegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/PropertySegmentTemplate.cs index 8bb4c4af3..7bc7da78b 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/PropertySegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/PropertySegmentTemplate.cs @@ -9,57 +9,56 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match an . +/// +public class PropertySegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that could match an . + /// Initializes a new instance of the class. /// - public class PropertySegmentTemplate : ODataSegmentTemplate + /// The wrapped Edm property. + public PropertySegmentTemplate(IEdmStructuralProperty property) + : this(new PropertySegment(property)) { - /// - /// Initializes a new instance of the class. - /// - /// The wrapped Edm property. - public PropertySegmentTemplate(IEdmStructuralProperty property) - : this(new PropertySegment(property)) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The property segment. - public PropertySegmentTemplate(PropertySegment segment) - { - Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); - } + /// + /// Initializes a new instance of the class. + /// + /// The property segment. + public PropertySegmentTemplate(PropertySegment segment) + { + Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); + } - /// - /// Gets the wrapped Edm property. - /// - public IEdmStructuralProperty Property => Segment.Property; + /// + /// Gets the wrapped Edm property. + /// + public IEdmStructuralProperty Property => Segment.Property; - /// - /// Gets the property segment. - /// - public PropertySegment Segment { get; } + /// + /// Gets the property segment. + /// + public PropertySegment Segment { get; } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) - { - yield return $"/{Property.Name}"; - } + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return $"/{Property.Name}"; + } - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - context.Segments.Add(Segment); - return true; + throw Error.ArgumentNull(nameof(context)); } + + context.Segments.Add(Segment); + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/SegmentTemplateHelpers.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/SegmentTemplateHelpers.cs index 7c067381a..27179176f 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/SegmentTemplateHelpers.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/SegmentTemplateHelpers.cs @@ -20,192 +20,191 @@ using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Helper methods for segment template +/// +internal static class SegmentTemplateHelpers { /// - /// Helper methods for segment template + /// Match the function parameter /// - internal static class SegmentTemplateHelpers + /// The context. + /// The Edm function. + /// The parameter mapping. + /// + public static IList Match(ODataTemplateTranslateContext context, + IEdmFunction function, + IDictionary parameterMappings) { - /// - /// Match the function parameter - /// - /// The context. - /// The Edm function. - /// The parameter mapping. - /// - public static IList Match(ODataTemplateTranslateContext context, - IEdmFunction function, - IDictionary parameterMappings) + Contract.Assert(context != null); + Contract.Assert(function != null); + Contract.Assert(parameterMappings != null); + + RouteValueDictionary routeValues = context.RouteValues; + RouteValueDictionary updatedValues = context.UpdatedValues; + + IList parameters = new List(); + foreach (var parameter in parameterMappings) { - Contract.Assert(context != null); - Contract.Assert(function != null); - Contract.Assert(parameterMappings != null); + string parameterName = parameter.Key; + string parameterTemp = parameter.Value; - RouteValueDictionary routeValues = context.RouteValues; - RouteValueDictionary updatedValues = context.UpdatedValues; + IEdmOperationParameter edmParameter = function.Parameters.FirstOrDefault(p => p.Name == parameterName); + Contract.Assert(edmParameter != null); - IList parameters = new List(); - foreach (var parameter in parameterMappings) + // For a parameter mapping like: minSalary={min} + // and a request like: ~/MyFunction(minSalary=2) + // the routeValue includes the [min=2], so we should use the mapping name to retrieve the value. + if (routeValues.TryGetValue(parameterTemp, out object rawValue)) { - string parameterName = parameter.Key; - string parameterTemp = parameter.Value; + string strValue = rawValue as string; + string newStrValue = context.GetParameterAliasOrSelf(strValue); - IEdmOperationParameter edmParameter = function.Parameters.FirstOrDefault(p => p.Name == parameterName); - Contract.Assert(edmParameter != null); + // rawValue from Request route values, it's unescaped except the back-slash. + newStrValue = newStrValue.UnescapeBackSlashUriString(); + if (newStrValue != strValue) + { + updatedValues[parameterTemp] = newStrValue; + strValue = newStrValue; + } + + string originalStrValue = strValue; - // For a parameter mapping like: minSalary={min} - // and a request like: ~/MyFunction(minSalary=2) - // the routeValue includes the [min=2], so we should use the mapping name to retrieve the value. - if (routeValues.TryGetValue(parameterTemp, out object rawValue)) + // for resource or collection resource, this method will return "ODataResourceValue, ..." we should support it. + if (edmParameter.Type.IsResourceOrCollectionResource()) { - string strValue = rawValue as string; - string newStrValue = context.GetParameterAliasOrSelf(strValue); + // For FromODataUri + string prefixName = ODataParameterValue.ParameterValuePrefix + parameterTemp; + updatedValues[prefixName] = new ODataParameterValue(strValue, edmParameter.Type); - // rawValue from Request route values, it's unescaped except the back-slash. - newStrValue = newStrValue.UnescapeBackSlashUriString(); - if (newStrValue != strValue) + parameters.Add(new OperationSegmentParameter(parameterName, strValue)); + } + else + { + if (edmParameter.Type.IsEnum() && strValue.StartsWith("'", StringComparison.Ordinal) && strValue.EndsWith("'", StringComparison.Ordinal)) { - updatedValues[parameterTemp] = newStrValue; - strValue = newStrValue; + // related implementation at: https://github.com/OData/odata.net/blob/master/src/Microsoft.OData.Core/UriParser/Resolver/StringAsEnumResolver.cs#L131 + strValue = edmParameter.Type.FullName() + strValue; } - string originalStrValue = strValue; - - // for resource or collection resource, this method will return "ODataResourceValue, ..." we should support it. - if (edmParameter.Type.IsResourceOrCollectionResource()) + object newValue; + try { - // For FromODataUri - string prefixName = ODataParameterValue.ParameterValuePrefix + parameterTemp; - updatedValues[prefixName] = new ODataParameterValue(strValue, edmParameter.Type); - - parameters.Add(new OperationSegmentParameter(parameterName, strValue)); + newValue = ODataUriUtils.ConvertFromUriLiteral(strValue, ODataVersion.V4, context.Model, edmParameter.Type); } - else + catch (ODataException ex) { - if (edmParameter.Type.IsEnum() && strValue.StartsWith("'", StringComparison.Ordinal) && strValue.EndsWith("'", StringComparison.Ordinal)) - { - // related implementation at: https://github.com/OData/odata.net/blob/master/src/Microsoft.OData.Core/UriParser/Resolver/StringAsEnumResolver.cs#L131 - strValue = edmParameter.Type.FullName() + strValue; - } - - object newValue; - try + string message = Error.Format(SRResources.InvalidParameterValueInUriFound, originalStrValue, edmParameter.Type.FullName()); + ILoggerFactory loggerFactory = context.HttpContext?.RequestServices?.GetService(); + if (loggerFactory != null) { - newValue = ODataUriUtils.ConvertFromUriLiteral(strValue, ODataVersion.V4, context.Model, edmParameter.Type); + loggerFactory.CreateLogger("ODataFunctionParameterMatcher").LogError(message, ex); + return null; } - catch (ODataException ex) + else { - string message = Error.Format(SRResources.InvalidParameterValueInUriFound, originalStrValue, edmParameter.Type.FullName()); - ILoggerFactory loggerFactory = context.HttpContext?.RequestServices?.GetService(); - if (loggerFactory != null) - { - loggerFactory.CreateLogger("ODataFunctionParameterMatcher").LogError(message, ex); - return null; - } - else - { - throw new ODataException(message, ex); - } + throw new ODataException(message, ex); } + } - // for without FromODataUri, so update it, for example, remove the single quote for string value. - updatedValues[parameterTemp] = newValue; + // for without FromODataUri, so update it, for example, remove the single quote for string value. + updatedValues[parameterTemp] = newValue; - // For FromODataUri - string prefixName = ODataParameterValue.ParameterValuePrefix + parameterTemp; - updatedValues[prefixName] = new ODataParameterValue(newValue, edmParameter.Type); + // For FromODataUri + string prefixName = ODataParameterValue.ParameterValuePrefix + parameterTemp; + updatedValues[prefixName] = new ODataParameterValue(newValue, edmParameter.Type); - parameters.Add(new OperationSegmentParameter(parameterName, newValue)); - } - } - else - { - return null; + parameters.Add(new OperationSegmentParameter(parameterName, newValue)); } } - - return parameters; + else + { + return null; + } } - /// - /// Match the parameters - /// - /// The route values - /// The parameter mappings. - /// True/False. - internal static bool IsMatchParameters(RouteValueDictionary routeValues, IDictionary parameterMappings) + return parameters; + } + + /// + /// Match the parameters + /// + /// The route values + /// The parameter mappings. + /// True/False. + internal static bool IsMatchParameters(RouteValueDictionary routeValues, IDictionary parameterMappings) + { + Contract.Assert(routeValues != null); + Contract.Assert(parameterMappings != null); + + // If we have a function(p1, p2, p3), where p3 is optional parameter. + // In controller, we may have two functions: + // IActionResult function(p1, p2) --> #1 + // IActionResult function(p1, p2, p3) --> #2 + // #1 can match request like: ~/function(p1=a, p2=b) , where p1=a, p2=b (----a) + // It also match request like: ~/function(p1=a,p2=b,p3=c), where p2="b,p3=c". (----b) + // However, b request should match the #2 method and skip the #1 method. + // Here is a workaround: + // 1) We get all the parameters from the function and all parameter values from routeValue. + // Combine them as a string. so, actualParameters = "p1=a,p2=b,p3=c" + + IDictionary actualParameters = new Dictionary(); + foreach (var parameter in parameterMappings) { - Contract.Assert(routeValues != null); - Contract.Assert(parameterMappings != null); - - // If we have a function(p1, p2, p3), where p3 is optional parameter. - // In controller, we may have two functions: - // IActionResult function(p1, p2) --> #1 - // IActionResult function(p1, p2, p3) --> #2 - // #1 can match request like: ~/function(p1=a, p2=b) , where p1=a, p2=b (----a) - // It also match request like: ~/function(p1=a,p2=b,p3=c), where p2="b,p3=c". (----b) - // However, b request should match the #2 method and skip the #1 method. - // Here is a workaround: - // 1) We get all the parameters from the function and all parameter values from routeValue. - // Combine them as a string. so, actualParameters = "p1=a,p2=b,p3=c" - - IDictionary actualParameters = new Dictionary(); - foreach (var parameter in parameterMappings) + // For a parameter mapping like: minSalary={min} + // and a request like: ~/MyFunction(minSalary=2) + // the routeValue includes the [min=2], so we should use the mapping name to retrieve the value. + string parameterTemp = parameter.Value; + if (routeValues.TryGetValue(parameterTemp, out object rawValue)) { - // For a parameter mapping like: minSalary={min} - // and a request like: ~/MyFunction(minSalary=2) - // the routeValue includes the [min=2], so we should use the mapping name to retrieve the value. - string parameterTemp = parameter.Value; - if (routeValues.TryGetValue(parameterTemp, out object rawValue)) - { - actualParameters[parameterTemp] = rawValue as string; - } + actualParameters[parameterTemp] = rawValue as string; } + } - if (!actualParameters.Any()) + if (!actualParameters.Any()) + { + if (parameterMappings.Any()) { - if (parameterMappings.Any()) - { - return false; - } - else - { - return true; - } + return false; } - - string combinates = string.Join(",", actualParameters.Select(kvp => kvp.Key + "=" + kvp.Value)); - - // 2) Extract the key/value pairs - // p1=a p2=b p3=c - if (!KeyValuePairParser.TryParse(combinates, out IDictionary parsedKeyValues)) + else { - return false; + return true; } - - // 3) now the parsedKeyValues (p1, p3) is not equal to actualParameters (p1, p2, p3) - return parameterMappings.Count == parsedKeyValues.Count; } - /// - /// Gets the navigation source from an Edm operation. - /// - /// The Edm model. - /// The Edm operation. - /// - /// The navigation source or null if the annotation indicating the mapping from an Edm operation to an entity set is not found. - /// - internal static IEdmNavigationSource GetNavigationSourceFromEdmOperation(IEdmModel model, IEdmOperation operation) + string combinates = string.Join(",", actualParameters.Select(kvp => kvp.Key + "=" + kvp.Value)); + + // 2) Extract the key/value pairs + // p1=a p2=b p3=c + if (!KeyValuePairParser.TryParse(combinates, out IDictionary parsedKeyValues)) { - ReturnedEntitySetAnnotation entitySetAnnotation = model?.GetAnnotationValue(operation); + return false; + } - if (entitySetAnnotation != null) - { - return model.EntityContainer.FindEntitySet(entitySetAnnotation.EntitySetName); - } + // 3) now the parsedKeyValues (p1, p3) is not equal to actualParameters (p1, p2, p3) + return parameterMappings.Count == parsedKeyValues.Count; + } - return null; + /// + /// Gets the navigation source from an Edm operation. + /// + /// The Edm model. + /// The Edm operation. + /// + /// The navigation source or null if the annotation indicating the mapping from an Edm operation to an entity set is not found. + /// + internal static IEdmNavigationSource GetNavigationSourceFromEdmOperation(IEdmModel model, IEdmOperation operation) + { + ReturnedEntitySetAnnotation entitySetAnnotation = model?.GetAnnotationValue(operation); + + if (entitySetAnnotation != null) + { + return model.EntityContainer.FindEntitySet(entitySetAnnotation.EntitySetName); } + + return null; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/SingletonSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/SingletonSegmentTemplate.cs index 2fc0cfb6d..e37188157 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/SingletonSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/SingletonSegmentTemplate.cs @@ -9,57 +9,56 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match an . +/// +public class SingletonSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that could match an . + /// Initializes a new instance of the class. /// - public class SingletonSegmentTemplate : ODataSegmentTemplate + /// The Edm singleton. + public SingletonSegmentTemplate(IEdmSingleton singleton) + : this(new SingletonSegment(singleton)) { - /// - /// Initializes a new instance of the class. - /// - /// The Edm singleton. - public SingletonSegmentTemplate(IEdmSingleton singleton) - : this(new SingletonSegment(singleton)) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The singleton segment. - public SingletonSegmentTemplate(SingletonSegment segment) - { - Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); - } + /// + /// Initializes a new instance of the class. + /// + /// The singleton segment. + public SingletonSegmentTemplate(SingletonSegment segment) + { + Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); + } - /// - /// Gets the wrapped Edm singleton. - /// - public IEdmSingleton Singleton => Segment.Singleton; + /// + /// Gets the wrapped Edm singleton. + /// + public IEdmSingleton Singleton => Segment.Singleton; - /// - /// Gets the wrapped singleton segment. - /// - public SingletonSegment Segment { get; } + /// + /// Gets the wrapped singleton segment. + /// + public SingletonSegment Segment { get; } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) - { - yield return $"/{Segment.Singleton.Name}"; - } + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return $"/{Segment.Singleton.Name}"; + } - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - context.Segments.Add(Segment); - return true; + throw Error.ArgumentNull(nameof(context)); } + + context.Segments.Add(Segment); + return true; } } diff --git a/src/Microsoft.AspNetCore.OData/Routing/Template/ValueSegmentTemplate.cs b/src/Microsoft.AspNetCore.OData/Routing/Template/ValueSegmentTemplate.cs index 6fd3bbe89..913e42c43 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/Template/ValueSegmentTemplate.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/Template/ValueSegmentTemplate.cs @@ -9,52 +9,51 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Routing.Template +namespace Microsoft.AspNetCore.OData.Routing.Template; + +/// +/// Represents a template that could match a "/$value" segment. +/// +public class ValueSegmentTemplate : ODataSegmentTemplate { /// - /// Represents a template that could match a "/$value" segment. + /// Initializes a new instance of the class. /// - public class ValueSegmentTemplate : ODataSegmentTemplate + /// The value segment Edm type. + public ValueSegmentTemplate(IEdmType previousType) + : this(new ValueSegment(previousType)) { - /// - /// Initializes a new instance of the class. - /// - /// The value segment Edm type. - public ValueSegmentTemplate(IEdmType previousType) - : this(new ValueSegment(previousType)) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The value segment. - public ValueSegmentTemplate(ValueSegment segment) - { - Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); - } + /// + /// Initializes a new instance of the class. + /// + /// The value segment. + public ValueSegmentTemplate(ValueSegment segment) + { + Segment = segment ?? throw Error.ArgumentNull(nameof(segment)); + } - /// - /// Gets the value segment. - /// - public ValueSegment Segment { get; } + /// + /// Gets the value segment. + /// + public ValueSegment Segment { get; } - /// - public override IEnumerable GetTemplates(ODataRouteOptions options) - { - yield return "/$value"; - } + /// + public override IEnumerable GetTemplates(ODataRouteOptions options) + { + yield return "/$value"; + } - /// - public override bool TryTranslate(ODataTemplateTranslateContext context) + /// + public override bool TryTranslate(ODataTemplateTranslateContext context) + { + if (context == null) { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - context.Segments.Add(Segment); - return true; + throw Error.ArgumentNull(nameof(context)); } + + context.Segments.Add(Segment); + return true; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultController.cs index 703252dbe..52daddcfb 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultController.cs @@ -11,50 +11,49 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Query; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ActionResults +namespace Microsoft.AspNetCore.OData.E2E.Tests.ActionResults; + +[ApiController] +[Route("api/[controller]")] +public class CustomersController : Controller { - [ApiController] - [Route("api/[controller]")] - public class CustomersController : Controller + [HttpGet] + [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Expand)] + public async Task>> GetCustomers() { - [HttpGet] - [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Expand)] - public async Task>> GetCustomers() - { - return await Task.FromResult(new List - { - new Customer + return await Task.FromResult(new List + { + new Customer + { + Id = "CustId", + Books = new List { - Id = "CustId", - Books = new List + new Book { - new Book - { - Id = "BookId", - }, + Id = "BookId", }, }, - }); - } + }, + }); + } - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; - [HttpGet("/api/weather")] - public IActionResult Get(ODataQueryOptions options) + [HttpGet("/api/weather")] + public IActionResult Get(ODataQueryOptions options) + { + var data = Enumerable.Range(1, 10).Select(index => new Weather { - var data = Enumerable.Range(1, 10).Select(index => new Weather - { - Id = index, - TemperatureC = 22 + index, - Summary = Summaries[index - 1] - }) - .ToArray() - .AsQueryable(); + Id = index, + TemperatureC = 22 + index, + Summary = Summaries[index - 1] + }) + .ToArray() + .AsQueryable(); - return Ok(options.ApplyTo(data)); - } + return Ok(options.ApplyTo(data)); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultEdmModel.cs index 826fa4f01..ef53cddae 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultEdmModel.cs @@ -8,15 +8,14 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ActionResults +namespace Microsoft.AspNetCore.OData.E2E.Tests.ActionResults; + +public class ActionResultEdmModel { - public class ActionResultEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultODataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultODataModel.cs index 12d3f1ddb..e0a7f2781 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultODataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultODataModel.cs @@ -7,28 +7,27 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ActionResults +namespace Microsoft.AspNetCore.OData.E2E.Tests.ActionResults; + +public class Customer { - public class Customer - { - public string Id { get; set; } + public string Id { get; set; } - public IEnumerable Books { get; set; } - } + public IEnumerable Books { get; set; } +} - public class Book - { - public string Id { get; set; } - } +public class Book +{ + public string Id { get; set; } +} - public class Weather - { - public int Id { get; set; } +public class Weather +{ + public int Id { get; set; } - public int TemperatureC { get; set; } + public int TemperatureC { get; set; } - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - public string Summary { get; set; } - } + public string Summary { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultTests.cs index 2655fd201..289538c7a 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ActionResults/ActionResultTests.cs @@ -17,168 +17,167 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ActionResults +namespace Microsoft.AspNetCore.OData.E2E.Tests.ActionResults; + +public class ActionResultTests : WebApiTestBase { - public class ActionResultTests : WebApiTestBase + public ActionResultTests(WebApiTestFixture fixture) + : base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(CustomersController), typeof(ODataEndpointController)); + + services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null) + .AddRouteComponents("actionresult", ActionResultEdmModel.GetEdmModel())); + } + + [Fact] + public async Task TestRoutes() + { + // Arrange + string requestUri = "$odata"; + HttpClient client = CreateClient(); + + // Act + var response = await client.GetAsync(requestUri); + + // Assert + response.EnsureSuccessStatusCode(); + string payload = await response.Content.ReadAsStringAsync(); + } + + /// + /// For Non-OData json based paths. EnableQuery should work. + /// + /// Task tracking operation. + [Fact] + public async Task ActionResultNonODataPathReturnsExpansion() + { + // Arrange + string queryUrl = "api/Customers?$expand=Books"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + List customers = JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()); + + Customer customer = Assert.Single(customers); + Assert.Equal("CustId", customer.Id); + Assert.Single(customer.Books); + Assert.Equal("BookId", customer.Books.First().Id); + } + + /// + /// For OData paths enable query should work with expansion. + /// + /// Task tracking operation. + [Fact] + public async Task ActionResultODataPathReturnsExpansion() + { + // Arrange + string queryUrl = "actionresult/Customers?$expand=books"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + + Customer customer = Assert.Single(customers); + Assert.Equal("CustId", customer.Id); + Assert.Single(customer.Books); + Assert.Equal("BookId", customers.First().Books.First().Id); + } + + /// + /// For OData paths enable query should work without expansion. + /// + /// + [Fact] + public async Task ActionResultODataPathReturnsBaseWithoutExpansion() + { + // Arrange + string queryUrl = "actionresult/Customers"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + + Customer customer = Assert.Single(customers); + Assert.Equal("CustId", customer.Id); + Assert.Null(customer.Books); + } + + [Fact] + public async Task ActionResult_NonODataPathReturnsComputedProperties_UsedInSelect() { - public ActionResultTests(WebApiTestFixture fixture) - : base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(CustomersController), typeof(ODataEndpointController)); - - services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null) - .AddRouteComponents("actionresult", ActionResultEdmModel.GetEdmModel())); - } - - [Fact] - public async Task TestRoutes() - { - // Arrange - string requestUri = "$odata"; - HttpClient client = CreateClient(); - - // Act - var response = await client.GetAsync(requestUri); - - // Assert - response.EnsureSuccessStatusCode(); - string payload = await response.Content.ReadAsStringAsync(); - } - - /// - /// For Non-OData json based paths. EnableQuery should work. - /// - /// Task tracking operation. - [Fact] - public async Task ActionResultNonODataPathReturnsExpansion() - { - // Arrange - string queryUrl = "api/Customers?$expand=Books"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - List customers = JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()); - - Customer customer = Assert.Single(customers); - Assert.Equal("CustId", customer.Id); - Assert.Single(customer.Books); - Assert.Equal("BookId", customer.Books.First().Id); - } - - /// - /// For OData paths enable query should work with expansion. - /// - /// Task tracking operation. - [Fact] - public async Task ActionResultODataPathReturnsExpansion() - { - // Arrange - string queryUrl = "actionresult/Customers?$expand=books"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); - - Customer customer = Assert.Single(customers); - Assert.Equal("CustId", customer.Id); - Assert.Single(customer.Books); - Assert.Equal("BookId", customers.First().Books.First().Id); - } - - /// - /// For OData paths enable query should work without expansion. - /// - /// - [Fact] - public async Task ActionResultODataPathReturnsBaseWithoutExpansion() - { - // Arrange - string queryUrl = "actionresult/Customers"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); - - Customer customer = Assert.Single(customers); - Assert.Equal("CustId", customer.Id); - Assert.Null(customer.Books); - } - - [Fact] - public async Task ActionResult_NonODataPathReturnsComputedProperties_UsedInSelect() - { - // Arrange - string queryUrl = "api/weather?$compute=length(Summary) as len&$select=summary,len&$top=3"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - string payload = await response.Content.ReadAsStringAsync(); - - Assert.Equal("[" + - "{\"Summary\":\"Freezing\",\"len\":8}," + - "{\"Summary\":\"Bracing\",\"len\":7}," + - "{\"Summary\":\"Chilly\",\"len\":6}]", - payload); - } - - [Fact] - public async Task ActionResult_NonODataPathReturnsComputedProperties_UsedInFilterAndOrderByAndSelect() - { - // Arrange - string queryUrl = "api/weather?$compute=length(Summary) as len,substring(Summary,1,1) as SecondChar&" + - "$select=summary,SecondChar&" + - "$filter=len eq 4&" + - "$orderby=SecondChar"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - string payload = await response.Content.ReadAsStringAsync(); - - Assert.Equal("[" + - "{\"Summary\":\"Warm\",\"SecondChar\":\"a\"}," + - "{\"Summary\":\"Mild\",\"SecondChar\":\"i\"}," + - "{\"Summary\":\"Cool\",\"SecondChar\":\"o\"}]", - payload); - } + // Arrange + string queryUrl = "api/weather?$compute=length(Summary) as len&$select=summary,len&$top=3"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + string payload = await response.Content.ReadAsStringAsync(); + + Assert.Equal("[" + + "{\"Summary\":\"Freezing\",\"len\":8}," + + "{\"Summary\":\"Bracing\",\"len\":7}," + + "{\"Summary\":\"Chilly\",\"len\":6}]", + payload); + } + + [Fact] + public async Task ActionResult_NonODataPathReturnsComputedProperties_UsedInFilterAndOrderByAndSelect() + { + // Arrange + string queryUrl = "api/weather?$compute=length(Summary) as len,substring(Summary,1,1) as SecondChar&" + + "$select=summary,SecondChar&" + + "$filter=len eq 4&" + + "$orderby=SecondChar"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + string payload = await response.Content.ReadAsStringAsync(); + + Assert.Equal("[" + + "{\"Summary\":\"Warm\",\"SecondChar\":\"a\"}," + + "{\"Summary\":\"Mild\",\"SecondChar\":\"i\"}," + + "{\"Summary\":\"Cool\",\"SecondChar\":\"o\"}]", + payload); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysController.cs index 0886a9ccd..eeabebe20 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysController.cs @@ -14,272 +14,271 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.AlternateKeys +namespace Microsoft.AspNetCore.OData.E2E.Tests.AlternateKeys; + +[Route("odata")] +public class CustomersController : ODataController { - [Route("odata")] - public class CustomersController : ODataController + public IActionResult Get(int key) { - public IActionResult Get(int key) + foreach (var customer in AlternateKeysDataSource.Customers) { - foreach (var customer in AlternateKeysDataSource.Customers) + object value; + if (customer.TryGetPropertyValue("ID", out value)) { - object value; - if (customer.TryGetPropertyValue("ID", out value)) + int intKey = (int)value; + if (key == intKey) { - int intKey = (int)value; - if (key == intKey) - { - return Ok(customer); - } + return Ok(customer); } } - - return NotFound(); } - // alternate key: SSN - // why set Order = -2 (any number less than 0)? it is because 'Get' method has 'catch-all' template, we should move this template ahead - // Small order goes first. - // We can also leave order value unset, same as 'Get' method and 'PatchCustomerBySSN' method without setting the order value. - // Without setting the order value makes all routes with same order value and catch-all goes latter - [HttpGet("Customers(SSN={ssn})", Order = -2)] - public IActionResult GetCustomerBySSN(string ssn) + return NotFound(); + } + + // alternate key: SSN + // why set Order = -2 (any number less than 0)? it is because 'Get' method has 'catch-all' template, we should move this template ahead + // Small order goes first. + // We can also leave order value unset, same as 'Get' method and 'PatchCustomerBySSN' method without setting the order value. + // Without setting the order value makes all routes with same order value and catch-all goes latter + [HttpGet("Customers(SSN={ssn})", Order = -2)] + public IActionResult GetCustomerBySSN(string ssn) + { + // for special test + if (ssn == "special-SSN") { - // for special test - if (ssn == "special-SSN") - { - return Ok(ssn); - } + return Ok(ssn); + } - foreach (var customer in AlternateKeysDataSource.Customers) + foreach (var customer in AlternateKeysDataSource.Customers) + { + object value; + if (customer.TryGetPropertyValue("SSN", out value)) { - object value; - if (customer.TryGetPropertyValue("SSN", out value)) + string stringKey = (string)value; + if (ssn == stringKey) { - string stringKey = (string)value; - if (ssn == stringKey) - { - return Ok(customer); - } + return Ok(customer); } } - - return NotFound(); } - [HttpPatch("Customers(SSN={ssnKey})")] - public IActionResult PatchCustomerBySSN(string ssnKey, [FromBody]EdmEntityObject delta) - { - Assert.Equal("SSN-6-T-006", ssnKey); + return NotFound(); + } - IList changedPropertyNames = delta.GetChangedPropertyNames().ToList(); - Assert.Single(changedPropertyNames); - Assert.Equal("Name", String.Join(",", changedPropertyNames)); + [HttpPatch("Customers(SSN={ssnKey})")] + public IActionResult PatchCustomerBySSN(string ssnKey, [FromBody]EdmEntityObject delta) + { + Assert.Equal("SSN-6-T-006", ssnKey); + + IList changedPropertyNames = delta.GetChangedPropertyNames().ToList(); + Assert.Single(changedPropertyNames); + Assert.Equal("Name", String.Join(",", changedPropertyNames)); - IEdmEntityObject originalCustomer = null; - foreach (var customer in AlternateKeysDataSource.Customers) + IEdmEntityObject originalCustomer = null; + foreach (var customer in AlternateKeysDataSource.Customers) + { + object value; + if (customer.TryGetPropertyValue("SSN", out value)) { - object value; - if (customer.TryGetPropertyValue("SSN", out value)) + string stringKey = (string)value; + if (ssnKey == stringKey) { - string stringKey = (string)value; - if (ssnKey == stringKey) - { - originalCustomer = customer; - } + originalCustomer = customer; } } + } - if (originalCustomer == null) - { - return NotFound(); - } + if (originalCustomer == null) + { + return NotFound(); + } - object nameValue; - delta.TryGetPropertyValue("Name", out nameValue); - Assert.NotNull(nameValue); - string strName = Assert.IsType(nameValue); - dynamic original = originalCustomer; - original.Name = strName; + object nameValue; + delta.TryGetPropertyValue("Name", out nameValue); + Assert.NotNull(nameValue); + string strName = Assert.IsType(nameValue); + dynamic original = originalCustomer; + original.Name = strName; - return Ok(originalCustomer); - } + return Ok(originalCustomer); } +} - public class OrdersController : ODataController +public class OrdersController : ODataController +{ + [HttpGet("odata/Orders({orderKey})")] + public IActionResult GetOrderByPrimitiveKey(int orderKey) { - [HttpGet("odata/Orders({orderKey})")] - public IActionResult GetOrderByPrimitiveKey(int orderKey) + foreach (var order in AlternateKeysDataSource.Orders) { - foreach (var order in AlternateKeysDataSource.Orders) + object value; + if (order.TryGetPropertyValue("OrderId", out value)) { - object value; - if (order.TryGetPropertyValue("OrderId", out value)) + int intKey = (int)value; + if (orderKey == intKey) { - int intKey = (int)value; - if (orderKey == intKey) - { - return Ok(order); - } + return Ok(order); } } - - return NotFound(); } - [HttpGet("odata/Orders(Name={orderName})")] - public IActionResult GetOrderByName([FromODataUri]string orderName) + return NotFound(); + } + + [HttpGet("odata/Orders(Name={orderName})")] + public IActionResult GetOrderByName([FromODataUri]string orderName) + { + foreach (var order in AlternateKeysDataSource.Orders) { - foreach (var order in AlternateKeysDataSource.Orders) + object value; + if (order.TryGetPropertyValue("Name", out value)) { - object value; - if (order.TryGetPropertyValue("Name", out value)) + string stringKey = (string)value; + if (orderName == stringKey) { - string stringKey = (string)value; - if (orderName == stringKey) - { - return Ok(order); - } + return Ok(order); } } - - return NotFound(); } - [HttpGet("odata/Orders(Token={token})")] - public IActionResult GetOrderByToken([FromODataUri]Guid token) + return NotFound(); + } + + [HttpGet("odata/Orders(Token={token})")] + public IActionResult GetOrderByToken([FromODataUri]Guid token) + { + foreach (var order in AlternateKeysDataSource.Orders) { - foreach (var order in AlternateKeysDataSource.Orders) + object value; + if (order.TryGetPropertyValue("Token", out value)) { - object value; - if (order.TryGetPropertyValue("Token", out value)) + Guid guidKey = (Guid)value; + if (token == guidKey) { - Guid guidKey = (Guid)value; - if (token == guidKey) - { - return Ok(order); - } + return Ok(order); } } - - return NotFound(); } + + return NotFound(); } +} - public class PeopleController : ODataController +public class PeopleController : ODataController +{ + public IActionResult Get(int key) { - public IActionResult Get(int key) + foreach (var person in AlternateKeysDataSource.People) { - foreach (var person in AlternateKeysDataSource.People) + object value; + if (person.TryGetPropertyValue("ID", out value)) { - object value; - if (person.TryGetPropertyValue("ID", out value)) + int intKey = (int)value; + if (key == intKey) { - int intKey = (int)value; - if (key == intKey) - { - return Ok(person); - } + return Ok(person); } } - - return NotFound(); } - [HttpGet("odata/People(Country_Region={countryOrRegion},Passport={passport})")] - public IActionResult FindPeopleByCountryAndPassport([FromODataUri]string countryOrRegion, [FromODataUri]string passport) + return NotFound(); + } + + [HttpGet("odata/People(Country_Region={countryOrRegion},Passport={passport})")] + public IActionResult FindPeopleByCountryAndPassport([FromODataUri]string countryOrRegion, [FromODataUri]string passport) + { + foreach (var person in AlternateKeysDataSource.People) { - foreach (var person in AlternateKeysDataSource.People) + object value; + if (person.TryGetPropertyValue("Country_Region", out value)) { - object value; - if (person.TryGetPropertyValue("Country_Region", out value)) + string countryValue = (string)value; + if (person.TryGetPropertyValue("Passport", out value)) { - string countryValue = (string)value; - if (person.TryGetPropertyValue("Passport", out value)) + string passportValue = (string)value; + if (countryValue == countryOrRegion && passportValue == passport) { - string passportValue = (string)value; - if (countryValue == countryOrRegion && passportValue == passport) - { - return Ok(person); - } + return Ok(person); } } } - - return NotFound(); } + + return NotFound(); } +} - public class CompaniesController : ODataController +public class CompaniesController : ODataController +{ + public IActionResult Get(int key) { - public IActionResult Get(int key) + foreach (var company in AlternateKeysDataSource.Companies) { - foreach (var company in AlternateKeysDataSource.Companies) + object value; + if (company.TryGetPropertyValue("ID", out value)) { - object value; - if (company.TryGetPropertyValue("ID", out value)) + int intKey = (int)value; + if (key == intKey) { - int intKey = (int)value; - if (key == intKey) - { - return Ok(company); - } + return Ok(company); } } - - return NotFound(); } - [HttpGet("odata/Companies(Code={code})")] - public IActionResult GetCompaniesByCode(int code) + return NotFound(); + } + + [HttpGet("odata/Companies(Code={code})")] + public IActionResult GetCompaniesByCode(int code) + { + foreach (var company in AlternateKeysDataSource.Companies) { - foreach (var company in AlternateKeysDataSource.Companies) + object value; + if (company.TryGetPropertyValue("Code", out value)) { - object value; - if (company.TryGetPropertyValue("Code", out value)) + int intCode = (int)value; + if (code == intCode) { - int intCode = (int)value; - if (code == intCode) - { - return Ok(company); - } + return Ok(company); } } - - return NotFound(); } - [HttpGet("odata/Companies(City={city},Street={street})")] - public IActionResult GetCompanyByLocation([FromODataUri]string city, [FromODataUri]string street) + return NotFound(); + } + + [HttpGet("odata/Companies(City={city},Street={street})")] + public IActionResult GetCompanyByLocation([FromODataUri]string city, [FromODataUri]string street) + { + foreach (var company in AlternateKeysDataSource.Companies) { - foreach (var company in AlternateKeysDataSource.Companies) + object value; + if (company.TryGetPropertyValue("Location", out value)) { - object value; - if (company.TryGetPropertyValue("Location", out value)) + IEdmComplexObject location = value as IEdmComplexObject; + if (location == null) { - IEdmComplexObject location = value as IEdmComplexObject; - if (location == null) - { - return NotFound(); - } + return NotFound(); + } - if (location.TryGetPropertyValue("City", out value)) - { - string locCity = (string) value; + if (location.TryGetPropertyValue("City", out value)) + { + string locCity = (string) value; - if (location.TryGetPropertyValue("Street", out value)) + if (location.TryGetPropertyValue("Street", out value)) + { + string locStreet = (string) value; + if (locCity == city && locStreet == street) { - string locStreet = (string) value; - if (locCity == city && locStreet == street) - { - return Ok(company); - } + return Ok(company); } } } } - - return NotFound(); } + + return NotFound(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysDataSource.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysDataSource.cs index 000838821..880786bc4 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysDataSource.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysDataSource.cs @@ -11,149 +11,148 @@ using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.E2E.Tests.AlternateKeys +namespace Microsoft.AspNetCore.OData.E2E.Tests.AlternateKeys; + +public static class AlternateKeysDataSource { - public static class AlternateKeysDataSource - { - public static EdmEntityObjectCollection Customers { get; private set; } + public static EdmEntityObjectCollection Customers { get; private set; } - public static EdmEntityObjectCollection Orders { get; private set; } + public static EdmEntityObjectCollection Orders { get; private set; } - public static EdmEntityObjectCollection People { get; private set; } + public static EdmEntityObjectCollection People { get; private set; } - public static EdmEntityObjectCollection Companies { get; private set; } + public static EdmEntityObjectCollection Companies { get; private set; } - static AlternateKeysDataSource() - { - IEdmModel model = AlternateKeysEdmModel.GetEdmModel(); + static AlternateKeysDataSource() + { + IEdmModel model = AlternateKeysEdmModel.GetEdmModel(); - BuildCustomers(model); + BuildCustomers(model); - BuildOrderss(model); + BuildOrderss(model); - BuildPeople(model); + BuildPeople(model); - BuildCompanies(model); - } + BuildCompanies(model); + } + + private static void BuildCustomers(IEdmModel model) + { + IEdmEntityType customerType = model.SchemaElements.OfType().First(e => e.Name == "Customer"); - private static void BuildCustomers(IEdmModel model) + IEdmEntityObject[] untypedCustomers = new IEdmEntityObject[6]; + for (int i = 1; i <= 5; i++) { - IEdmEntityType customerType = model.SchemaElements.OfType().First(e => e.Name == "Customer"); - - IEdmEntityObject[] untypedCustomers = new IEdmEntityObject[6]; - for (int i = 1; i <= 5; i++) - { - dynamic untypedCustomer = new EdmEntityObject(customerType); - untypedCustomer.ID = i; - untypedCustomer.Name = string.Format("Name {0}", i); - untypedCustomer.SSN = "SSN-" + i + "-" + (100 + i); - untypedCustomers[i-1] = untypedCustomer; - } - - // create a special customer for "PATCH" - dynamic customer = new EdmEntityObject(customerType); - customer.ID = 6; - customer.Name = "Name 6"; - customer.SSN = "SSN-6-T-006"; - untypedCustomers[5] = customer; - - IEdmCollectionTypeReference entityCollectionType = - new EdmCollectionTypeReference( - new EdmCollectionType( - new EdmEntityTypeReference(customerType, isNullable: false))); - - Customers = new EdmEntityObjectCollection(entityCollectionType, untypedCustomers.ToList()); + dynamic untypedCustomer = new EdmEntityObject(customerType); + untypedCustomer.ID = i; + untypedCustomer.Name = string.Format("Name {0}", i); + untypedCustomer.SSN = "SSN-" + i + "-" + (100 + i); + untypedCustomers[i-1] = untypedCustomer; } - private static void BuildOrderss(IEdmModel model) + // create a special customer for "PATCH" + dynamic customer = new EdmEntityObject(customerType); + customer.ID = 6; + customer.Name = "Name 6"; + customer.SSN = "SSN-6-T-006"; + untypedCustomers[5] = customer; + + IEdmCollectionTypeReference entityCollectionType = + new EdmCollectionTypeReference( + new EdmCollectionType( + new EdmEntityTypeReference(customerType, isNullable: false))); + + Customers = new EdmEntityObjectCollection(entityCollectionType, untypedCustomers.ToList()); + } + + private static void BuildOrderss(IEdmModel model) + { + IEdmEntityType orderType = model.SchemaElements.OfType().First(e => e.Name == "Order"); + + Guid[] guids = { - IEdmEntityType orderType = model.SchemaElements.OfType().First(e => e.Name == "Order"); - - Guid[] guids = - { - new Guid("196B3584-EF3D-41FD-90B4-76D59F9B929C"), - new Guid("6CED5600-28BA-40EE-A2DF-E80AFADBE6C7"), - new Guid("75036B94-C836-4946-8CC8-054CF54060EC"), - new Guid("B3FF5460-6E77-4678-B959-DCC1C4937FA7"), - new Guid("ED773C85-4E3C-4FC4-A3E9-9F1DA0A626DA") - }; - - IEdmEntityObject[] untypedOrders = new IEdmEntityObject[5]; - for (int i = 0; i < 5; i++) - { - dynamic untypedOrder = new EdmEntityObject(orderType); - untypedOrder.OrderId = i; - untypedOrder.Name = string.Format("Order-{0}", i); - untypedOrder.Token = guids[i]; - untypedOrder.Amount = 10 + i; - untypedOrders[i] = untypedOrder; - } - - IEdmCollectionTypeReference entityCollectionType = - new EdmCollectionTypeReference( - new EdmCollectionType( - new EdmEntityTypeReference(orderType, isNullable: false))); - - Orders = new EdmEntityObjectCollection(entityCollectionType, untypedOrders.ToList()); + new Guid("196B3584-EF3D-41FD-90B4-76D59F9B929C"), + new Guid("6CED5600-28BA-40EE-A2DF-E80AFADBE6C7"), + new Guid("75036B94-C836-4946-8CC8-054CF54060EC"), + new Guid("B3FF5460-6E77-4678-B959-DCC1C4937FA7"), + new Guid("ED773C85-4E3C-4FC4-A3E9-9F1DA0A626DA") + }; + + IEdmEntityObject[] untypedOrders = new IEdmEntityObject[5]; + for (int i = 0; i < 5; i++) + { + dynamic untypedOrder = new EdmEntityObject(orderType); + untypedOrder.OrderId = i; + untypedOrder.Name = string.Format("Order-{0}", i); + untypedOrder.Token = guids[i]; + untypedOrder.Amount = 10 + i; + untypedOrders[i] = untypedOrder; } - private static void BuildPeople(IEdmModel model) + IEdmCollectionTypeReference entityCollectionType = + new EdmCollectionTypeReference( + new EdmCollectionType( + new EdmEntityTypeReference(orderType, isNullable: false))); + + Orders = new EdmEntityObjectCollection(entityCollectionType, untypedOrders.ToList()); + } + + private static void BuildPeople(IEdmModel model) + { + IEdmEntityType personType = model.SchemaElements.OfType().First(e => e.Name == "Person"); + + IEdmEntityObject[] untypedPeople = new IEdmEntityObject[5]; + for (int i = 0; i < 5; i++) { - IEdmEntityType personType = model.SchemaElements.OfType().First(e => e.Name == "Person"); - - IEdmEntityObject[] untypedPeople = new IEdmEntityObject[5]; - for (int i = 0; i < 5; i++) - { - dynamic untypedPerson = new EdmEntityObject(personType); - untypedPerson.ID = i; - untypedPerson.Country_Region = new[] { "CountryRegion1", "China", "United States", "Russia", "Japan" }[i]; - untypedPerson.Passport = new[] { "1001", "2010", "9999", "3199992", "00001"}[i]; - untypedPeople[i] = untypedPerson; - } - - IEdmCollectionTypeReference entityCollectionType = - new EdmCollectionTypeReference( - new EdmCollectionType( - new EdmEntityTypeReference(personType, isNullable: false))); - - People = new EdmEntityObjectCollection(entityCollectionType, untypedPeople.ToList()); + dynamic untypedPerson = new EdmEntityObject(personType); + untypedPerson.ID = i; + untypedPerson.Country_Region = new[] { "CountryRegion1", "China", "United States", "Russia", "Japan" }[i]; + untypedPerson.Passport = new[] { "1001", "2010", "9999", "3199992", "00001"}[i]; + untypedPeople[i] = untypedPerson; } - private static void BuildCompanies(IEdmModel model) + IEdmCollectionTypeReference entityCollectionType = + new EdmCollectionTypeReference( + new EdmCollectionType( + new EdmEntityTypeReference(personType, isNullable: false))); + + People = new EdmEntityObjectCollection(entityCollectionType, untypedPeople.ToList()); + } + + private static void BuildCompanies(IEdmModel model) + { + IEdmEntityType companyType = model.SchemaElements.OfType().First(e => e.Name == "Company"); + + IList addresses = BuildAddrsses(model); + + IEdmEntityObject[] untypedCompanies = new IEdmEntityObject[5]; + for (int i = 0; i < 5; i++) { - IEdmEntityType companyType = model.SchemaElements.OfType().First(e => e.Name == "Company"); - - IList addresses = BuildAddrsses(model); - - IEdmEntityObject[] untypedCompanies = new IEdmEntityObject[5]; - for (int i = 0; i < 5; i++) - { - dynamic untypedCompany = new EdmEntityObject(companyType); - untypedCompany.ID = i; - untypedCompany.Code = 10 + 10 * i; - untypedCompany.Location = addresses[i]; - untypedCompanies[i] = untypedCompany; - } - - IEdmCollectionTypeReference entityCollectionType = - new EdmCollectionTypeReference( - new EdmCollectionType( - new EdmEntityTypeReference(companyType, isNullable: false))); - - Companies = new EdmEntityObjectCollection(entityCollectionType, untypedCompanies.ToList()); + dynamic untypedCompany = new EdmEntityObject(companyType); + untypedCompany.ID = i; + untypedCompany.Code = 10 + 10 * i; + untypedCompany.Location = addresses[i]; + untypedCompanies[i] = untypedCompany; } - private static IList BuildAddrsses(IEdmModel model) + IEdmCollectionTypeReference entityCollectionType = + new EdmCollectionTypeReference( + new EdmCollectionType( + new EdmEntityTypeReference(companyType, isNullable: false))); + + Companies = new EdmEntityObjectCollection(entityCollectionType, untypedCompanies.ToList()); + } + + private static IList BuildAddrsses(IEdmModel model) + { + IEdmComplexType addressType = model.SchemaElements.OfType().First(e => e.Name == "Address"); + + return Enumerable.Range(1, 5).Select(e => { - IEdmComplexType addressType = model.SchemaElements.OfType().First(e => e.Name == "Address"); - - return Enumerable.Range(1, 5).Select(e => - { - dynamic address = new EdmComplexObject(addressType); - address.Street = new[] {"Fuxing Rd", "Zixing Rd", "Xiaoxiang Rd", "Kehua Rd", "Taoyuan Rd"}[e - 1]; - address.City = new[] {"Beijing", "Shanghai", "Guangzhou", "Chengdu", "Wuhan"}[e - 1]; - return address as IEdmComplexObject; - }).ToList(); - } + dynamic address = new EdmComplexObject(addressType); + address.Street = new[] {"Fuxing Rd", "Zixing Rd", "Xiaoxiang Rd", "Kehua Rd", "Taoyuan Rd"}[e - 1]; + address.City = new[] {"Beijing", "Shanghai", "Guangzhou", "Chengdu", "Wuhan"}[e - 1]; + return address as IEdmComplexObject; + }).ToList(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysEdmModel.cs index 25abd64b5..3e03480f5 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysEdmModel.cs @@ -13,161 +13,160 @@ using Microsoft.OData.Edm.Vocabularies.V1; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.AlternateKeys +namespace Microsoft.AspNetCore.OData.E2E.Tests.AlternateKeys; + +public static class AlternateKeysEdmModel { - public static class AlternateKeysEdmModel + private static IEdmModel _edmModel; + public static IEdmModel GetEdmModel() { - private static IEdmModel _edmModel; - public static IEdmModel GetEdmModel() + if (_edmModel != null) { - if (_edmModel != null) - { - return _edmModel; - } - - EdmModel model = new EdmModel(); - - // entity type 'Customer' with single alternate keys - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - var ssn = customer.AddStructuralProperty("SSN", EdmPrimitiveTypeKind.String); - model.AddAlternateKeyAnnotation(customer, new Dictionary - { - {"SSN", ssn} - }); - model.AddElement(customer); - - // entity type 'Order' with multiple alternate keys - EdmEntityType order = new EdmEntityType("NS", "Order"); - order.AddKeys(order.AddStructuralProperty("OrderId", EdmPrimitiveTypeKind.Int32)); - var orderName = order.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - var orderToken = order.AddStructuralProperty("Token", EdmPrimitiveTypeKind.Guid); - order.AddStructuralProperty("Amount", EdmPrimitiveTypeKind.Int32); - model.AddAlternateKeyAnnotation(order, new Dictionary - { - {"Name", orderName} - }); - - model.AddAlternateKeyAnnotation(order, new Dictionary - { - {"Token", orderToken} - }); - - model.AddElement(order); - - // entity type 'Person' with composed alternate keys - EdmEntityType person = new EdmEntityType("NS", "Person"); - person.AddKeys(person.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - var countryRegion = person.AddStructuralProperty("Country_Region", EdmPrimitiveTypeKind.String); - var passport = person.AddStructuralProperty("Passport", EdmPrimitiveTypeKind.String); - model.AddAlternateKeyAnnotation(person, new Dictionary - { - {"Country_Region", countryRegion}, - {"Passport", passport} - }); - model.AddElement(person); - - // complex type address - EdmComplexType address = new EdmComplexType("NS", "Address"); - var street = address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); - var city = address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); - model.AddElement(address); - - // entity type 'Company' with complex type alternate keys - EdmEntityType company = new EdmEntityType("NS", "Company"); - company.AddKeys(company.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - company.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Int32); - company.AddStructuralProperty("Location", new EdmComplexTypeReference(address, isNullable: true)); - - // ODL extension method doesn't build the path expression - //model.AddAlternateKeyAnnotation(company, new Dictionary - //{ - // {"City", city}, - // {"Street", street} - //}); - - AddComplexPropertyAlternateKey(model, company); - model.AddElement(company); - - // entity sets - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - model.AddElement(container); - container.AddEntitySet("Customers", customer); - container.AddEntitySet("Orders", order); - container.AddEntitySet("People", person); - container.AddEntitySet("Companies", company); - - return _edmModel = model; + return _edmModel; } - private static void AddComplexPropertyAlternateKey(EdmModel model, EdmEntityType company) + EdmModel model = new EdmModel(); + + // entity type 'Customer' with single alternate keys + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + var ssn = customer.AddStructuralProperty("SSN", EdmPrimitiveTypeKind.String); + model.AddAlternateKeyAnnotation(customer, new Dictionary { - // Alternate key 1 -> Code - List propertyRefs = new List(); - IEdmRecordExpression propertyRef = new EdmRecordExpression( - new EdmPropertyConstructor("Alias", new EdmStringConstant("Code")), - new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Code"))); - propertyRefs.Add(propertyRef); - - EdmRecordExpression alternateKey1 = new EdmRecordExpression(new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); - - // Alternate key 2 -> City & Street - propertyRefs = new List(); - propertyRef = new EdmRecordExpression( - new EdmPropertyConstructor("Alias", new EdmStringConstant("City")), - new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Location/City"))); - propertyRefs.Add(propertyRef); - - propertyRef = new EdmRecordExpression( - new EdmPropertyConstructor("Alias", new EdmStringConstant("Street")), - new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Location/Street"))); - propertyRefs.Add(propertyRef); - - EdmRecordExpression alternateKey2 = new EdmRecordExpression(new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); - - IEdmTerm coreAlternateTerm = AlternateKeysVocabularyModel.Instance.FindDeclaredTerm("OData.Community.Keys.V1.AlternateKeys"); - Assert.NotNull(coreAlternateTerm); - - var annotation = new EdmVocabularyAnnotation( - company, - coreAlternateTerm, - new EdmCollectionExpression(alternateKey1, alternateKey2)); - - annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); - model.SetVocabularyAnnotation(annotation); - } + {"SSN", ssn} + }); + model.AddElement(customer); + + // entity type 'Order' with multiple alternate keys + EdmEntityType order = new EdmEntityType("NS", "Order"); + order.AddKeys(order.AddStructuralProperty("OrderId", EdmPrimitiveTypeKind.Int32)); + var orderName = order.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + var orderToken = order.AddStructuralProperty("Token", EdmPrimitiveTypeKind.Guid); + order.AddStructuralProperty("Amount", EdmPrimitiveTypeKind.Int32); + model.AddAlternateKeyAnnotation(order, new Dictionary + { + {"Name", orderName} + }); + + model.AddAlternateKeyAnnotation(order, new Dictionary + { + {"Token", orderToken} + }); + + model.AddElement(order); - // ODL 7.x Uri parser only supports Community.AlternateKeys - // Once ODL supports Core.AlternateKeys, we can consider to use Core.AlternateKeys - private static void AddComplexPropertyAlternateKey1(EdmModel model, EdmEntityType company) + // entity type 'Person' with composed alternate keys + EdmEntityType person = new EdmEntityType("NS", "Person"); + person.AddKeys(person.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + var countryRegion = person.AddStructuralProperty("Country_Region", EdmPrimitiveTypeKind.String); + var passport = person.AddStructuralProperty("Passport", EdmPrimitiveTypeKind.String); + model.AddAlternateKeyAnnotation(person, new Dictionary { - List propertyRefs = new List(); + {"Country_Region", countryRegion}, + {"Passport", passport} + }); + model.AddElement(person); + + // complex type address + EdmComplexType address = new EdmComplexType("NS", "Address"); + var street = address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + var city = address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + model.AddElement(address); + + // entity type 'Company' with complex type alternate keys + EdmEntityType company = new EdmEntityType("NS", "Company"); + company.AddKeys(company.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + company.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Int32); + company.AddStructuralProperty("Location", new EdmComplexTypeReference(address, isNullable: true)); + + // ODL extension method doesn't build the path expression + //model.AddAlternateKeyAnnotation(company, new Dictionary + //{ + // {"City", city}, + // {"Street", street} + //}); + + AddComplexPropertyAlternateKey(model, company); + model.AddElement(company); + + // entity sets + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + model.AddElement(container); + container.AddEntitySet("Customers", customer); + container.AddEntitySet("Orders", order); + container.AddEntitySet("People", person); + container.AddEntitySet("Companies", company); + + return _edmModel = model; + } - IEdmRecordExpression propertyRef = new EdmRecordExpression( - new EdmPropertyConstructor("Alias", new EdmStringConstant("City")), - new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Location/City"))); + private static void AddComplexPropertyAlternateKey(EdmModel model, EdmEntityType company) + { + // Alternate key 1 -> Code + List propertyRefs = new List(); + IEdmRecordExpression propertyRef = new EdmRecordExpression( + new EdmPropertyConstructor("Alias", new EdmStringConstant("Code")), + new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Code"))); + propertyRefs.Add(propertyRef); + + EdmRecordExpression alternateKey1 = new EdmRecordExpression(new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); + + // Alternate key 2 -> City & Street + propertyRefs = new List(); + propertyRef = new EdmRecordExpression( + new EdmPropertyConstructor("Alias", new EdmStringConstant("City")), + new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Location/City"))); + propertyRefs.Add(propertyRef); + + propertyRef = new EdmRecordExpression( + new EdmPropertyConstructor("Alias", new EdmStringConstant("Street")), + new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Location/Street"))); + propertyRefs.Add(propertyRef); + + EdmRecordExpression alternateKey2 = new EdmRecordExpression(new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); + + IEdmTerm coreAlternateTerm = AlternateKeysVocabularyModel.Instance.FindDeclaredTerm("OData.Community.Keys.V1.AlternateKeys"); + Assert.NotNull(coreAlternateTerm); + + var annotation = new EdmVocabularyAnnotation( + company, + coreAlternateTerm, + new EdmCollectionExpression(alternateKey1, alternateKey2)); + + annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); + model.SetVocabularyAnnotation(annotation); + } - propertyRefs.Add(propertyRef); + // ODL 7.x Uri parser only supports Community.AlternateKeys + // Once ODL supports Core.AlternateKeys, we can consider to use Core.AlternateKeys + private static void AddComplexPropertyAlternateKey1(EdmModel model, EdmEntityType company) + { + List propertyRefs = new List(); - propertyRef = new EdmRecordExpression( - new EdmPropertyConstructor("Alias", new EdmStringConstant("Street")), - new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Location/Street"))); + IEdmRecordExpression propertyRef = new EdmRecordExpression( + new EdmPropertyConstructor("Alias", new EdmStringConstant("City")), + new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Location/City"))); - propertyRefs.Add(propertyRef); + propertyRefs.Add(propertyRef); - EdmRecordExpression alternateKeyRecord = new EdmRecordExpression(new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); + propertyRef = new EdmRecordExpression( + new EdmPropertyConstructor("Alias", new EdmStringConstant("Street")), + new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Location/Street"))); - IEdmTerm coreAlternateTerm = CoreVocabularyModel.Instance.FindDeclaredTerm("Org.OData.Core.V1.AlternateKeys"); - Assert.NotNull(coreAlternateTerm); + propertyRefs.Add(propertyRef); - var annotation = new EdmVocabularyAnnotation( - company, - coreAlternateTerm, - new EdmCollectionExpression(alternateKeyRecord)); + EdmRecordExpression alternateKeyRecord = new EdmRecordExpression(new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); - annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); - model.SetVocabularyAnnotation(annotation); - } + IEdmTerm coreAlternateTerm = CoreVocabularyModel.Instance.FindDeclaredTerm("Org.OData.Core.V1.AlternateKeys"); + Assert.NotNull(coreAlternateTerm); + + var annotation = new EdmVocabularyAnnotation( + company, + coreAlternateTerm, + new EdmCollectionExpression(alternateKeyRecord)); + + annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); + model.SetVocabularyAnnotation(annotation); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysTest.cs index 14a0faebe..a7c122349 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/AlternateKeys/AlternateKeysTest.cs @@ -18,36 +18,36 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.AlternateKeys +namespace Microsoft.AspNetCore.OData.E2E.Tests.AlternateKeys; + +public class AlternateKeysTest : WebApiTestBase { - public class AlternateKeysTest : WebApiTestBase + public AlternateKeysTest(WebApiTestFixture fixture) + :base(fixture) { - public AlternateKeysTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) + protected static void UpdateConfigureServices(IServiceCollection services) + { + var controllers = new[] { - var controllers = new[] - { - typeof (CustomersController), typeof (OrdersController), typeof (PeopleController), - typeof (CompaniesController), typeof (MetadataController) - }; + typeof (CustomersController), typeof (OrdersController), typeof (PeopleController), + typeof (CompaniesController), typeof (MetadataController) + }; - services.ConfigureControllers(controllers); + services.ConfigureControllers(controllers); - IEdmModel model = AlternateKeysEdmModel.GetEdmModel(); + IEdmModel model = AlternateKeysEdmModel.GetEdmModel(); - services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null) - .AddRouteComponents("odata", model, - services => services.AddSingleton(sp => new AlternateKeysODataUriResolver(model)))); - } + services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null) + .AddRouteComponents("odata", model, + services => services.AddSingleton(sp => new AlternateKeysODataUriResolver(model)))); + } - [Fact] - public async Task AlteranteKeysMetadata() - { - string expect = "\r\n" + + [Fact] + public async Task AlteranteKeysMetadata() + { + string expect = "\r\n" + "\r\n" + " \r\n" + " \r\n" + @@ -182,186 +182,185 @@ public async Task AlteranteKeysMetadata() " \r\n" + ""; - // Remove indentation - expect = Regex.Replace(expect, @"\r\n\s*<", @"<"); + // Remove indentation + expect = Regex.Replace(expect, @"\r\n\s*<", @"<"); - var requestUri = "odata/$metadata"; - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); + var requestUri = "odata/$metadata"; + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string responseContent = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string responseContent = await response.Content.ReadAsStringAsync(); - Assert.Equal(expect, responseContent); - } + Assert.Equal(expect, responseContent); + } - [Fact] - public async Task QueryEntityWithSingleAlternateKeysWorks() - { - // query with alternate keys - string expect = "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#Edm.String\",\"value\":\"special-SSN\"" + - "}"; + [Fact] + public async Task QueryEntityWithSingleAlternateKeysWorks() + { + // query with alternate keys + string expect = "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#Edm.String\",\"value\":\"special-SSN\"" + + "}"; - var requestUri = "odata/Customers(SSN='special-SSN')"; - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); + var requestUri = "odata/Customers(SSN='special-SSN')"; + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); - response.EnsureSuccessStatusCode(); - string responseContent = await response.Content.ReadAsStringAsync(); + response.EnsureSuccessStatusCode(); + string responseContent = await response.Content.ReadAsStringAsync(); - Assert.Equal(expect, responseContent); - } + Assert.Equal(expect, responseContent); + } - public static TheoryDataSet SingleAlternateKeysCases + public static TheoryDataSet SingleAlternateKeysCases + { + get { - get + var data = new TheoryDataSet(); + for (int i = 1; i <= 5; i++) { - var data = new TheoryDataSet(); - for (int i = 1; i <= 5; i++) - { - data.Add("Customers(" + i + ")", "Customers(SSN='SSN-" + i + "-" + (100 + i) + "')"); - } - - return data; + data.Add("Customers(" + i + ")", "Customers(SSN='SSN-" + i + "-" + (100 + i) + "')"); } + + return data; } + } - [Theory] - [MemberData(nameof(SingleAlternateKeysCases))] - public async Task EntityWithSingleAlternateKeys_ReturnsSame_WithPrimitiveKey(string declaredKeys, string alternatekeys) - { - // query with declared key - var requestUri = $"odata/{declaredKeys}"; - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); + [Theory] + [MemberData(nameof(SingleAlternateKeysCases))] + public async Task EntityWithSingleAlternateKeys_ReturnsSame_WithPrimitiveKey(string declaredKeys, string alternatekeys) + { + // query with declared key + var requestUri = $"odata/{declaredKeys}"; + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); - response.EnsureSuccessStatusCode(); - string primitiveResponse = await response.Content.ReadAsStringAsync(); + response.EnsureSuccessStatusCode(); + string primitiveResponse = await response.Content.ReadAsStringAsync(); - // query with alternate key - requestUri = $"odata/{alternatekeys}"; - response = await client.GetAsync(requestUri); + // query with alternate key + requestUri = $"odata/{alternatekeys}"; + response = await client.GetAsync(requestUri); - response.EnsureSuccessStatusCode(); - string alternatekeyResponse = await response.Content.ReadAsStringAsync(); + response.EnsureSuccessStatusCode(); + string alternatekeyResponse = await response.Content.ReadAsStringAsync(); - Assert.Equal(primitiveResponse, alternatekeyResponse); - } + Assert.Equal(primitiveResponse, alternatekeyResponse); + } - [Fact] - public async Task QueryEntityWithMultipleAlternateKeys_Returns_SameEntityWithPrimitiveKey() - { - // query with declared key - var requestUri = "odata/Orders(2)"; - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); - - response.EnsureSuccessStatusCode(); - string primitiveResponse = await response.Content.ReadAsStringAsync(); - - // query with one alternate key - requestUri = "odata/Orders(Name='Order-2')"; - response = await client.GetAsync(requestUri); - response.EnsureSuccessStatusCode(); - string nameResponse = await response.Content.ReadAsStringAsync(); - - // query with another alternate key - requestUri = "odata/Orders(Token=75036B94-C836-4946-8CC8-054CF54060EC)"; - response = await client.GetAsync(requestUri); - response.EnsureSuccessStatusCode(); - string tokenResponse = await response.Content.ReadAsStringAsync(); - - Assert.Equal(primitiveResponse, nameResponse); - Assert.Equal(primitiveResponse, tokenResponse); - } + [Fact] + public async Task QueryEntityWithMultipleAlternateKeys_Returns_SameEntityWithPrimitiveKey() + { + // query with declared key + var requestUri = "odata/Orders(2)"; + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); + + response.EnsureSuccessStatusCode(); + string primitiveResponse = await response.Content.ReadAsStringAsync(); + + // query with one alternate key + requestUri = "odata/Orders(Name='Order-2')"; + response = await client.GetAsync(requestUri); + response.EnsureSuccessStatusCode(); + string nameResponse = await response.Content.ReadAsStringAsync(); + + // query with another alternate key + requestUri = "odata/Orders(Token=75036B94-C836-4946-8CC8-054CF54060EC)"; + response = await client.GetAsync(requestUri); + response.EnsureSuccessStatusCode(); + string tokenResponse = await response.Content.ReadAsStringAsync(); + + Assert.Equal(primitiveResponse, nameResponse); + Assert.Equal(primitiveResponse, tokenResponse); + } - [Fact] - public async Task QueryEntityWithComposedAlternateKeys_Returns_SameEntityWithPrimitiveKey() - { - // query with declared key - var requestUri = "odata/People(2)"; - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); + [Fact] + public async Task QueryEntityWithComposedAlternateKeys_Returns_SameEntityWithPrimitiveKey() + { + // query with declared key + var requestUri = "odata/People(2)"; + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); - response.EnsureSuccessStatusCode(); - string primitiveResponse = await response.Content.ReadAsStringAsync(); + response.EnsureSuccessStatusCode(); + string primitiveResponse = await response.Content.ReadAsStringAsync(); - // query with composed alternate keys - requestUri = "odata/People(Country_Region='United States',Passport='9999')"; - response = await client.GetAsync(requestUri); - response.EnsureSuccessStatusCode(); - string composedResponse = await response.Content.ReadAsStringAsync(); + // query with composed alternate keys + requestUri = "odata/People(Country_Region='United States',Passport='9999')"; + response = await client.GetAsync(requestUri); + response.EnsureSuccessStatusCode(); + string composedResponse = await response.Content.ReadAsStringAsync(); - Assert.Equal(primitiveResponse, composedResponse); - } + Assert.Equal(primitiveResponse, composedResponse); + } - [Fact] - public async Task QueryFailedIfMissingAnyOfComposedAlternateKeys() - { - // Since this request matched "odata/People({key})", and key value is not valid. - // It throws exception - var requestUri = "odata/People(Country_Region='United States')"; - HttpClient client = CreateClient(); + [Fact] + public async Task QueryFailedIfMissingAnyOfComposedAlternateKeys() + { + // Since this request matched "odata/People({key})", and key value is not valid. + // It throws exception + var requestUri = "odata/People(Country_Region='United States')"; + HttpClient client = CreateClient(); - try - { - var response = await client.GetAsync(requestUri); - } - catch (ODataException ex) - { - Assert.Equal("The key value (Country_Region='United States') from request is not valid. The key value should be format of type 'Edm.Int32'.", ex.Message); - } + try + { + var response = await client.GetAsync(requestUri); } - - /* ODL has the bug to parse the route template with complex property path expression. - [Fact] - public async Task QueryEntityWithComplexPropertyAlternateKeys_Returns_SameEntityWithPrimitiveKey() + catch (ODataException ex) { - HttpClient client = CreateClient(); - - // query with declared key - var requestUri = "odata/Companies(2)"; - HttpResponseMessage response = await client.GetAsync(requestUri); - response.EnsureSuccessStatusCode(); - string responseContent1 = await response.Content.ReadAsStringAsync(); - - // query with complex alternate key - requestUri = "odata/Companies(Code=30)"; - response = await client.GetAsync(requestUri); - response.EnsureSuccessStatusCode(); - string responseContent2 = await response.Content.ReadAsStringAsync(); - Assert.Equal(responseContent1, responseContent2); - - // query with complex alternate key - requestUri = "odata/Companies(City='Guangzhou',Street='Xiaoxiang Rd')"; - response = await client.GetAsync(requestUri); - string responseContent3 = await response.Content.ReadAsStringAsync(); - Assert.Equal(responseContent2, responseContent3); + Assert.Equal("The key value (Country_Region='United States') from request is not valid. The key value should be format of type 'Edm.Int32'.", ex.Message); } - */ + } - [Fact] - public async Task CanUpdateEntityWithSingleAlternateKeys() - { - string expect = "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#Customers/$entity\",\"ID\":6,\"Name\":\"Updated Customer Name\",\"SSN\":\"SSN-6-T-006\"" + - "}"; + /* ODL has the bug to parse the route template with complex property path expression. + [Fact] + public async Task QueryEntityWithComplexPropertyAlternateKeys_Returns_SameEntityWithPrimitiveKey() + { + HttpClient client = CreateClient(); + + // query with declared key + var requestUri = "odata/Companies(2)"; + HttpResponseMessage response = await client.GetAsync(requestUri); + response.EnsureSuccessStatusCode(); + string responseContent1 = await response.Content.ReadAsStringAsync(); + + // query with complex alternate key + requestUri = "odata/Companies(Code=30)"; + response = await client.GetAsync(requestUri); + response.EnsureSuccessStatusCode(); + string responseContent2 = await response.Content.ReadAsStringAsync(); + Assert.Equal(responseContent1, responseContent2); + + // query with complex alternate key + requestUri = "odata/Companies(City='Guangzhou',Street='Xiaoxiang Rd')"; + response = await client.GetAsync(requestUri); + string responseContent3 = await response.Content.ReadAsStringAsync(); + Assert.Equal(responseContent2, responseContent3); + } + */ - var requestUri = "odata/Customers(SSN='SSN-6-T-006')"; + [Fact] + public async Task CanUpdateEntityWithSingleAlternateKeys() + { + string expect = "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#Customers/$entity\",\"ID\":6,\"Name\":\"Updated Customer Name\",\"SSN\":\"SSN-6-T-006\"" + + "}"; - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - const string content = @"{'Name':'Updated Customer Name'}"; - request.Content = new StringContent(content); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - HttpClient client = CreateClient(); + var requestUri = "odata/Customers(SSN='SSN-6-T-006')"; - HttpResponseMessage response = await client.SendAsync(request); + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + const string content = @"{'Name':'Updated Customer Name'}"; + request.Content = new StringContent(content); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + HttpClient client = CreateClient(); - response.EnsureSuccessStatusCode(); - string responseContent = await response.Content.ReadAsStringAsync(); + HttpResponseMessage response = await client.SendAsync(request); - Assert.Equal(expect, responseContent); - } + response.EnsureSuccessStatusCode(); + string responseContent = await response.Content.ReadAsStringAsync(); + + Assert.Equal(expect, responseContent); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandController.cs index f64588633..572c88ece 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandController.cs @@ -12,165 +12,164 @@ using Microsoft.AspNetCore.OData.Query.Validator; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.AutoExpand +namespace Microsoft.AspNetCore.OData.E2E.Tests.AutoExpand; + +public class CustomersController : ODataController { - public class CustomersController : ODataController + [EnableQuery] + public IActionResult Get() + { + return Ok(AutoExpandDataSource.Customers); + } + + [EnableQuery] + public IActionResult Get(int key) { - [EnableQuery] - public IActionResult Get() + Customer c = AutoExpandDataSource.Customers.FirstOrDefault(c => c.Id == key); + if (c == null) { - return Ok(AutoExpandDataSource.Customers); + return NotFound($"Cannot find customer with key = {key}"); } - [EnableQuery] - public IActionResult Get(int key) - { - Customer c = AutoExpandDataSource.Customers.FirstOrDefault(c => c.Id == key); - if (c == null) - { - return NotFound($"Cannot find customer with key = {key}"); - } + return Ok(c); + } - return Ok(c); + [EnableQuery] + public IActionResult GetHomeAddress(int key) + { + Customer c = AutoExpandDataSource.Customers.FirstOrDefault(c => c.Id == key); + if (c == null) + { + return NotFound($"Cannot find customer with key = {key}"); } - [EnableQuery] - public IActionResult GetHomeAddress(int key) - { - Customer c = AutoExpandDataSource.Customers.FirstOrDefault(c => c.Id == key); - if (c == null) - { - return NotFound($"Cannot find customer with key = {key}"); - } + return Ok(c.HomeAddress); + } - return Ok(c.HomeAddress); - } + [EnableQuery] + public IActionResult Post([FromBody] Customer customer) + { + return Created(customer); + } - [EnableQuery] - public IActionResult Post([FromBody] Customer customer) - { - return Created(customer); - } + [EnableQuery] + public IActionResult Put(int key, [FromBody] Customer customer) + { + var existingCustomer = AutoExpandDataSource.Customers.FirstOrDefault(d => d.Id == key); - [EnableQuery] - public IActionResult Put(int key, [FromBody] Customer customer) + if (existingCustomer == null) { - var existingCustomer = AutoExpandDataSource.Customers.FirstOrDefault(d => d.Id == key); + return BadRequest(); + } - if (existingCustomer == null) - { - return BadRequest(); - } + return Updated(existingCustomer); + } +} - return Updated(existingCustomer); - } +public class PeopleController : ODataController +{ + [EnableQuery(MaxExpansionDepth = 4)] + public IQueryable Get() + { + return AutoExpandDataSource.People.AsQueryable(); } +} - public class PeopleController : ODataController +public class NormalOrdersController : ODataController +{ + [EnableQuery] + public IQueryable Get() { - [EnableQuery(MaxExpansionDepth = 4)] - public IQueryable Get() - { - return AutoExpandDataSource.People.AsQueryable(); - } + return AutoExpandDataSource.NormalOrders.AsQueryable(); } - public class NormalOrdersController : ODataController + [EnableQuery] + public IActionResult Get(int key) { - [EnableQuery] - public IQueryable Get() + NormalOrder n = AutoExpandDataSource.NormalOrders.FirstOrDefault(c => c.Id == key); + if (n == null) { - return AutoExpandDataSource.NormalOrders.AsQueryable(); + return NotFound($"Cannot find NormalOrder with key = {key}"); } - [EnableQuery] - public IActionResult Get(int key) - { - NormalOrder n = AutoExpandDataSource.NormalOrders.FirstOrDefault(c => c.Id == key); - if (n == null) - { - return NotFound($"Cannot find NormalOrder with key = {key}"); - } - - return Ok(n); - } + return Ok(n); } +} - public class EnableQueryMenusController : ODataController +public class EnableQueryMenusController : ODataController +{ + private static readonly List menus = new List { - private static readonly List menus = new List + new Menu { - new Menu + Id = 1, + Tabs = new List { - Id = 1, - Tabs = new List + new Tab { - new Tab + Id = 1, + Items = new List { - Id = 1, - Items = new List + new Item { - new Item + Id = 1, + Notes = new List { - Id = 1, - Notes = new List - { - new Note { Id = 1 } - } + new Note { Id = 1 } } } } } } - }; - - [EnableQuery(MaxExpansionDepth = 4)] - public ActionResult Get() - { - return Ok(menus); } + }; + + [EnableQuery(MaxExpansionDepth = 4)] + public ActionResult Get() + { + return Ok(menus); } +} - public class QueryOptionsOfTMenusController : ODataController +public class QueryOptionsOfTMenusController : ODataController +{ + private static readonly List menus = new List { - private static readonly List menus = new List + new Menu { - new Menu + Id = 1, + Tabs = new List { - Id = 1, - Tabs = new List + new Tab { - new Tab + Id = 1, + Items = new List { - Id = 1, - Items = new List + new Item { - new Item + Id = 1, + Notes = new List { - Id = 1, - Notes = new List - { - new Note { Id = 1 } - } + new Note { Id = 1 } } } } } } - }; + } + }; - public ActionResult Get(ODataQueryOptions queryOptions) + public ActionResult Get(ODataQueryOptions queryOptions) + { + var validationSettings = new ODataValidationSettings { - var validationSettings = new ODataValidationSettings - { - MaxExpansionDepth = 4 - }; + MaxExpansionDepth = 4 + }; - queryOptions.Validate(validationSettings); + queryOptions.Validate(validationSettings); - var result = queryOptions.ApplyTo(menus.AsQueryable()); + var result = queryOptions.ApplyTo(menus.AsQueryable()); - return Ok(result); - } + return Ok(result); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandDataModel.cs index c3af5a89f..8739c3b6b 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandDataModel.cs @@ -8,151 +8,150 @@ using System.Collections.Generic; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.AutoExpand +namespace Microsoft.AspNetCore.OData.E2E.Tests.AutoExpand; + +[AutoExpand] +public class Customer +{ + public int Id { get; set; } + + public Address HomeAddress { get; set; } + + public Order Order { get; set; } + + public Customer Friend { get; set; } +} + +public class Address { + public string Street { get; set; } + + public string City { get; set; } + [AutoExpand] - public class Customer - { - public int Id { get; set; } + public CountryOrRegion CountryOrRegion { get; set; } +} - public Address HomeAddress { get; set; } +public class CnAddress : Address +{ + public PostCodeInfo PostCode { get; set; } +} - public Order Order { get; set; } +public class UsAddress : Address +{ + [AutoExpand] + public ZipCodeInfo ZipCode { get; set; } +} - public Customer Friend { get; set; } - } +public class CountryOrRegion +{ + public int Id { get; set; } - public class Address - { - public string Street { get; set; } + public string Name { get; set; } +} - public string City { get; set; } +public class PostCodeInfo +{ + public int Id { get; set; } - [AutoExpand] - public CountryOrRegion CountryOrRegion { get; set; } - } + public string Name { get; set; } +} - public class CnAddress : Address - { - public PostCodeInfo PostCode { get; set; } - } +public class ZipCodeInfo +{ + public int Id { get; set; } - public class UsAddress : Address - { - [AutoExpand] - public ZipCodeInfo ZipCode { get; set; } - } + public string Code { get; set; } +} - public class CountryOrRegion - { - public int Id { get; set; } +public class People +{ + public int Id { get; set; } - public string Name { get; set; } - } + [AutoExpand] + public Order Order { get; set; } - public class PostCodeInfo - { - public int Id { get; set; } + public People Friend { get; set; } +} - public string Name { get; set; } - } +public class Order +{ + public int Id { get; set; } - public class ZipCodeInfo - { - public int Id { get; set; } + [AutoExpand] + public ChoiceOrder Choice { get; set; } +} - public string Code { get; set; } - } +public class ChoiceOrder +{ + public int Id { get; set; } - public class People - { - public int Id { get; set; } + public double Amount { get; set; } +} - [AutoExpand] - public Order Order { get; set; } +public class SpecialOrder : Order +{ + [AutoExpand] + public ChoiceOrder SpecialChoice { get; set; } +} - public People Friend { get; set; } - } +public class VipOrder : SpecialOrder +{ + [AutoExpand] + public ChoiceOrder VipChoice { get; set; } +} - public class Order - { - public int Id { get; set; } +public class NormalOrder +{ + public int Id { get; set; } - [AutoExpand] - public ChoiceOrder Choice { get; set; } - } + public NormalOrder LinkOrder { get; set; } +} - public class ChoiceOrder - { - public int Id { get; set; } +public class DerivedOrder : NormalOrder +{ + [AutoExpand] + public OrderDetail OrderDetail { get; set; } - public double Amount { get; set; } - } + [AutoExpand(DisableWhenSelectPresent = true)] + public OrderDetail NotShownDetail { get; set; } +} - public class SpecialOrder : Order - { - [AutoExpand] - public ChoiceOrder SpecialChoice { get; set; } - } +[AutoExpand(DisableWhenSelectPresent = true)] +public class DerivedOrder2 : NormalOrder +{ + public OrderDetail NotShownDetail { get; set; } +} - public class VipOrder : SpecialOrder - { - [AutoExpand] - public ChoiceOrder VipChoice { get; set; } - } +public class OrderDetail +{ + public int Id { get; set; } - public class NormalOrder - { - public int Id { get; set; } + public string Description { get; set; } +} - public NormalOrder LinkOrder { get; set; } - } +public class Menu +{ + public int Id { get; set; } + [AutoExpand] + public List Tabs { get; set; } +} - public class DerivedOrder : NormalOrder - { - [AutoExpand] - public OrderDetail OrderDetail { get; set; } +public class Tab +{ + public int Id { get; set; } + [AutoExpand] + public List Items { get; set; } +} - [AutoExpand(DisableWhenSelectPresent = true)] - public OrderDetail NotShownDetail { get; set; } - } +public class Item +{ + public int Id { get; set; } + [AutoExpand] + public List Notes { get; set; } +} - [AutoExpand(DisableWhenSelectPresent = true)] - public class DerivedOrder2 : NormalOrder - { - public OrderDetail NotShownDetail { get; set; } - } - - public class OrderDetail - { - public int Id { get; set; } - - public string Description { get; set; } - } - - public class Menu - { - public int Id { get; set; } - [AutoExpand] - public List Tabs { get; set; } - } - - public class Tab - { - public int Id { get; set; } - [AutoExpand] - public List Items { get; set; } - } - - public class Item - { - public int Id { get; set; } - [AutoExpand] - public List Notes { get; set; } - } - - public class Note - { - public int Id { get; set; } - } +public class Note +{ + public int Id { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandDataSource.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandDataSource.cs index a785e3b5a..2c686bd76 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandDataSource.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandDataSource.cs @@ -7,204 +7,203 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.AutoExpand +namespace Microsoft.AspNetCore.OData.E2E.Tests.AutoExpand; + +public class AutoExpandDataSource { - public class AutoExpandDataSource - { - private static IList _customers; - private static IList _people; - private static IList _normalOrders; + private static IList _customers; + private static IList _people; + private static IList _normalOrders; - static AutoExpandDataSource() - { - GenerateCustomers(); + static AutoExpandDataSource() + { + GenerateCustomers(); - GeneratePeople(); + GeneratePeople(); - GenerateOrders(); - } + GenerateOrders(); + } - public static IList Customers => _customers; + public static IList Customers => _customers; - public static IList People => _people; + public static IList People => _people; - public static IList NormalOrders => _normalOrders; + public static IList NormalOrders => _normalOrders; - private static void GenerateCustomers() + private static void GenerateCustomers() + { + _customers = new List(); + Customer previousCustomer = null; + for (int i = 1; i < 10; i++) { - _customers = new List(); - Customer previousCustomer = null; - for (int i = 1; i < 10; i++) + Address address; + if (i % 2 == 0) { - Address address; - if (i % 2 == 0) + address = new CnAddress { - address = new CnAddress - { - Street = $"CnStreet {i}", - City = $"CnCity {i}", - CountryOrRegion = new CountryOrRegion { Id = i + 100, Name = $"C and R {i + 100}" }, - PostCode = new PostCodeInfo { Id = i + 1000, Name = $"PostCode {i}" } - }; - } - else + Street = $"CnStreet {i}", + City = $"CnCity {i}", + CountryOrRegion = new CountryOrRegion { Id = i + 100, Name = $"C and R {i + 100}" }, + PostCode = new PostCodeInfo { Id = i + 1000, Name = $"PostCode {i}" } + }; + } + else + { + address = new UsAddress { - address = new UsAddress - { - Street = $"UsStreet {i}", - City = $"UsCity {i}", - CountryOrRegion = new CountryOrRegion { Id = i + 100, Name = $"C and R {i + 100}" }, - ZipCode = new ZipCodeInfo { Id = i + 2000, Code = $"Code {i}" } - }; - } - - // Order.Id is from 1 ~ 9 - var customer = new Customer + Street = $"UsStreet {i}", + City = $"UsCity {i}", + CountryOrRegion = new CountryOrRegion { Id = i + 100, Name = $"C and R {i + 100}" }, + ZipCode = new ZipCodeInfo { Id = i + 2000, Code = $"Code {i}" } + }; + } + + // Order.Id is from 1 ~ 9 + var customer = new Customer + { + Id = i, + HomeAddress = address, + Order = new Order { Id = i, - HomeAddress = address, - Order = new Order + Choice = new ChoiceOrder { Id = i, - Choice = new ChoiceOrder - { - Id = i, - Amount = i * 1000 - } - }, - }; + Amount = i * 1000 + } + }, + }; - if (i > 1) - { - customer.Friend = previousCustomer; - } + if (i > 1) + { + customer.Friend = previousCustomer; + } - // For customer whose id is 8 will have SpecialOrder with SpecialChoice. - if (i == 8) + // For customer whose id is 8 will have SpecialOrder with SpecialChoice. + if (i == 8) + { + customer.Order = new SpecialOrder { - customer.Order = new SpecialOrder + Id = i, + Choice = new ChoiceOrder { Id = i, - Choice = new ChoiceOrder - { - Id = i, - Amount = i * 1000 - }, - SpecialChoice = new ChoiceOrder() - { - Id = i * 100, - Amount = i * 2000 - } - }; - } - - // For customer whose id is 9 will have VipOrder with SpecialChoice and VipChoice. - if (i == 9) - { - customer.Order = new VipOrder + Amount = i * 1000 + }, + SpecialChoice = new ChoiceOrder() { - Id = i, - Choice = new ChoiceOrder - { - Id = i, - Amount = i * 1000 - }, - SpecialChoice = new ChoiceOrder() - { - Id = i * 100, - Amount = i * 2000 - }, - VipChoice = new ChoiceOrder() - { - Id = i * 1000, - Amount = i * 3000 - } - }; - } - - _customers.Add(customer); - previousCustomer = customer; + Id = i * 100, + Amount = i * 2000 + } + }; } - } - - public static void GeneratePeople() - { - _people = new List(); - People previousPeople = null; - for (int i = 1; i < 10; i++) + // For customer whose id is 9 will have VipOrder with SpecialChoice and VipChoice. + if (i == 9) { - var people = new People + customer.Order = new VipOrder { Id = i, - Order = new Order + Choice = new ChoiceOrder { - Id = i + 10, // Order Id is from 10~19 - Choice = new ChoiceOrder - { - Id = i + 10, - Amount = (i + 10) * 1000 - } + Id = i, + Amount = i * 1000 }, + SpecialChoice = new ChoiceOrder() + { + Id = i * 100, + Amount = i * 2000 + }, + VipChoice = new ChoiceOrder() + { + Id = i * 1000, + Amount = i * 3000 + } }; - - if (i > 1) - { - people.Friend = previousPeople; - } - - _people.Add(people); - previousPeople = people; } + + _customers.Add(customer); + previousCustomer = customer; } + } - private static void GenerateOrders() - { - _normalOrders = new List(); + public static void GeneratePeople() + { + _people = new List(); - var order2 = new DerivedOrder + People previousPeople = null; + for (int i = 1; i < 10; i++) + { + var people = new People { - Id = 2, - OrderDetail = new OrderDetail + Id = i, + Order = new Order { - Id = 3, - Description = "OrderDetail" + Id = i + 10, // Order Id is from 10~19 + Choice = new ChoiceOrder + { + Id = i + 10, + Amount = (i + 10) * 1000 + } }, - NotShownDetail = new OrderDetail - { - Id = 4, - Description = "NotShownOrderDetail4" - } }; - var order1 = new DerivedOrder + if (i > 1) { - Id = 1, - OrderDetail = new OrderDetail - { - Id = 1, - Description = "OrderDetail" - }, - NotShownDetail = new OrderDetail - { - Id = 2, - Description = "NotShownOrderDetail2" - } - }; + people.Friend = previousPeople; + } - var order3 = new DerivedOrder2 + _people.Add(people); + previousPeople = people; + } + } + + private static void GenerateOrders() + { + _normalOrders = new List(); + + var order2 = new DerivedOrder + { + Id = 2, + OrderDetail = new OrderDetail { Id = 3, - NotShownDetail = new OrderDetail - { - Id = 5, - Description = "NotShownOrderDetail4" - } - }; + Description = "OrderDetail" + }, + NotShownDetail = new OrderDetail + { + Id = 4, + Description = "NotShownOrderDetail4" + } + }; - order2.LinkOrder = order1; - _normalOrders.Add(order2); - _normalOrders.Add(order3); - } + var order1 = new DerivedOrder + { + Id = 1, + OrderDetail = new OrderDetail + { + Id = 1, + Description = "OrderDetail" + }, + NotShownDetail = new OrderDetail + { + Id = 2, + Description = "NotShownOrderDetail2" + } + }; + + var order3 = new DerivedOrder2 + { + Id = 3, + NotShownDetail = new OrderDetail + { + Id = 5, + Description = "NotShownOrderDetail4" + } + }; + + order2.LinkOrder = order1; + _normalOrders.Add(order2); + _normalOrders.Add(order3); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandEdmModel.cs index 625145941..3951a7e19 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandEdmModel.cs @@ -8,30 +8,29 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.AutoExpand +namespace Microsoft.AspNetCore.OData.E2E.Tests.AutoExpand; + +public class AutoExpandEdmModel { - public class AutoExpandEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Orders"); - builder.EntityType(); - builder.EntityType(); - builder.EntitySet("OrderChoices"); - builder.EntitySet("NormalOrders"); - builder.EntityType(); - builder.EntityType(); - builder.EntitySet("OrderDetails"); - builder.EntitySet("People"); - builder.EntitySet("EnableQueryMenus"); - builder.EntitySet("QueryOptionsOfTMenus"); - builder.EntitySet("Tabs"); - builder.EntitySet("Items"); - builder.EntitySet("Notes"); - IEdmModel model = builder.GetEdmModel(); - return model; - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); + builder.EntityType(); + builder.EntityType(); + builder.EntitySet("OrderChoices"); + builder.EntitySet("NormalOrders"); + builder.EntityType(); + builder.EntityType(); + builder.EntitySet("OrderDetails"); + builder.EntitySet("People"); + builder.EntitySet("EnableQueryMenus"); + builder.EntitySet("QueryOptionsOfTMenus"); + builder.EntitySet("Tabs"); + builder.EntitySet("Items"); + builder.EntitySet("Notes"); + IEdmModel model = builder.GetEdmModel(); + return model; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandTests.cs index 691c8df5a..bfdbf21e4 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/AutoExpand/AutoExpandTests.cs @@ -20,472 +20,471 @@ using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.OData.E2E.Tests.AutoExpand +namespace Microsoft.AspNetCore.OData.E2E.Tests.AutoExpand; + +public class AutoExpandTests : WebApiTestBase { - public class AutoExpandTests : WebApiTestBase - { - private readonly ITestOutputHelper output; + private readonly ITestOutputHelper output; - public AutoExpandTests(WebApiTestFixture fixture, ITestOutputHelper output) - : base(fixture) - { - this.output = output; - } + public AutoExpandTests(WebApiTestFixture fixture, ITestOutputHelper output) + : base(fixture) + { + this.output = output; + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = AutoExpandEdmModel.GetEdmModel(); + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = AutoExpandEdmModel.GetEdmModel(); - services.ConfigureControllers(typeof(CustomersController), - typeof(PeopleController), - typeof(NormalOrdersController), - typeof(EnableQueryMenusController), - typeof(QueryOptionsOfTMenusController)); + services.ConfigureControllers(typeof(CustomersController), + typeof(PeopleController), + typeof(NormalOrdersController), + typeof(EnableQueryMenusController), + typeof(QueryOptionsOfTMenusController)); - services.AddControllers().AddOData(opt => - opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select().AddRouteComponents("autoexpand", edmModel)); - } + services.AddControllers().AddOData(opt => + opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select().AddRouteComponents("autoexpand", edmModel)); + } - [Theory] - [InlineData("?$select=Order", 3)] - [InlineData("?$select=Id", 4)] - [InlineData("?$expand=Order & $select=Id", 4)] - [InlineData("?$expand=Friend", 4)] - [InlineData("", 4)] - public async Task QueryForAnResource_Includes_AutoExpandNavigationProperty(string url, int propCount) - { - // Arrange - string queryUrl = $"autoexpand/Customers(5){url}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - var customer = await response.Content.ReadAsObject(); - this.output.WriteLine(customer.ToString()); - Assert.Equal(customer.Properties().Count(), propCount); - VerifyOrderAndChoiceOrder(customer); - VerifyHomeAddress(customer); - - // level one - JObject friend = customer["Friend"] as JObject; - JObject order = friend["Order"] as JObject; - Assert.NotNull(order); - Assert.Null(order["Choice"]); - - // level two - friend = friend["Friend"] as JObject; - Assert.Null(friend["Order"]); - } + [Theory] + [InlineData("?$select=Order", 3)] + [InlineData("?$select=Id", 4)] + [InlineData("?$expand=Order & $select=Id", 4)] + [InlineData("?$expand=Friend", 4)] + [InlineData("", 4)] + public async Task QueryForAnResource_Includes_AutoExpandNavigationProperty(string url, int propCount) + { + // Arrange + string queryUrl = $"autoexpand/Customers(5){url}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + var customer = await response.Content.ReadAsObject(); + this.output.WriteLine(customer.ToString()); + Assert.Equal(customer.Properties().Count(), propCount); + VerifyOrderAndChoiceOrder(customer); + VerifyHomeAddress(customer); + + // level one + JObject friend = customer["Friend"] as JObject; + JObject order = friend["Order"] as JObject; + Assert.NotNull(order); + Assert.Null(order["Choice"]); + + // level two + friend = friend["Friend"] as JObject; + Assert.Null(friend["Order"]); + } - private static void VerifyHomeAddress(JObject customer) - { - JObject homeAddress = customer["HomeAddress"] as JObject; - Assert.NotNull(homeAddress); + private static void VerifyHomeAddress(JObject customer) + { + JObject homeAddress = customer["HomeAddress"] as JObject; + Assert.NotNull(homeAddress); - Assert.Equal("UsStreet 5", homeAddress["Street"]); - Assert.Equal("UsCity 5", homeAddress["City"]); + Assert.Equal("UsStreet 5", homeAddress["Street"]); + Assert.Equal("UsCity 5", homeAddress["City"]); - JObject countryOrRegion = homeAddress["CountryOrRegion"] as JObject; - Assert.NotNull(countryOrRegion); + JObject countryOrRegion = homeAddress["CountryOrRegion"] as JObject; + Assert.NotNull(countryOrRegion); - JObject zipCode = homeAddress["ZipCode"] as JObject; - Assert.NotNull(zipCode); - } + JObject zipCode = homeAddress["ZipCode"] as JObject; + Assert.NotNull(zipCode); + } - [Fact] - public async Task QueryForAnResource_Includes_DerivedAutoExpandNavigationProperty() - { - // Arrange - string queryUrl = "autoexpand/Customers(8)"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - string payload = await response.Content.ReadAsStringAsync(); - Assert.Equal("{" + - "\"Id\":8," + - "\"HomeAddress\":{" + - "\"Street\":\"CnStreet 8\",\"City\":\"CnCity 8\"," + - "\"CountryOrRegion\":{\"Id\":108,\"Name\":\"C and R 108\"}" + // CnAddress only auto-expands the CountryOrRegion - "}," + - "\"Order\":{" + - "\"Id\":8," + - "\"Choice\":{\"Id\":8,\"Amount\":8000.0}," + - "\"SpecialChoice\":{\"Id\":800,\"Amount\":16000.0}" + - "}," + - "\"Friend\":{" + - "\"Id\":7," + - "\"HomeAddress\":{" + - "\"Street\":\"UsStreet 7\",\"City\":\"UsCity 7\"," + - "\"CountryOrRegion\":{\"Id\":107,\"Name\":\"C and R 107\"}," + // UsAddress auto-expands the CountryOrRegion & ZipCode - "\"ZipCode\":{\"Id\":2007,\"Code\":\"Code 7\"}" + - "}," + - "\"Order\":{\"Id\":7}," + - "\"Friend\":{" + - "\"Id\":6," + - "\"HomeAddress\":{\"Street\":\"CnStreet 6\",\"City\":\"CnCity 6\"}" + - "}" + - "}" + - "}", payload); - } + [Fact] + public async Task QueryForAnResource_Includes_DerivedAutoExpandNavigationProperty() + { + // Arrange + string queryUrl = "autoexpand/Customers(8)"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payload = await response.Content.ReadAsStringAsync(); + Assert.Equal("{" + + "\"Id\":8," + + "\"HomeAddress\":{" + + "\"Street\":\"CnStreet 8\",\"City\":\"CnCity 8\"," + + "\"CountryOrRegion\":{\"Id\":108,\"Name\":\"C and R 108\"}" + // CnAddress only auto-expands the CountryOrRegion + "}," + + "\"Order\":{" + + "\"Id\":8," + + "\"Choice\":{\"Id\":8,\"Amount\":8000.0}," + + "\"SpecialChoice\":{\"Id\":800,\"Amount\":16000.0}" + + "}," + + "\"Friend\":{" + + "\"Id\":7," + + "\"HomeAddress\":{" + + "\"Street\":\"UsStreet 7\",\"City\":\"UsCity 7\"," + + "\"CountryOrRegion\":{\"Id\":107,\"Name\":\"C and R 107\"}," + // UsAddress auto-expands the CountryOrRegion & ZipCode + "\"ZipCode\":{\"Id\":2007,\"Code\":\"Code 7\"}" + + "}," + + "\"Order\":{\"Id\":7}," + + "\"Friend\":{" + + "\"Id\":6," + + "\"HomeAddress\":{\"Street\":\"CnStreet 6\",\"City\":\"CnCity 6\"}" + + "}" + + "}" + + "}", payload); + } - [Fact] - public async Task QueryForAnResource_Includes_MultiDerivedAutoExpandNavigationProperty() - { - // Arrange - string queryUrl = "autoexpand/Customers(9)"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - var customer = await response.Content.ReadAsObject(); - VerifyOrderAndChoiceOrder(customer, special: true, vip: true); - this.output.WriteLine(customer.ToString()); - - // level one - JObject friend = customer["Friend"] as JObject; - JObject order = friend["Order"] as JObject; - Assert.NotNull(order); - Assert.Null(order["Choice"]); - } + [Fact] + public async Task QueryForAnResource_Includes_MultiDerivedAutoExpandNavigationProperty() + { + // Arrange + string queryUrl = "autoexpand/Customers(9)"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + var customer = await response.Content.ReadAsObject(); + VerifyOrderAndChoiceOrder(customer, special: true, vip: true); + this.output.WriteLine(customer.ToString()); + + // level one + JObject friend = customer["Friend"] as JObject; + JObject order = friend["Order"] as JObject; + Assert.NotNull(order); + Assert.Null(order["Choice"]); + } - [Fact] - public async Task QueryForAnResource_LevelsWithAutoExpandInSameNavigationProperty() - { - // Arrange - string queryUrl = "autoexpand/Customers(5)?$expand=Friend($levels=0)"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var customer = await response.Content.ReadAsObject(); - Assert.NotNull(customer); - VerifyOrderAndChoiceOrder(customer); - Assert.Null(customer["Friend"]); - } + [Fact] + public async Task QueryForAnResource_LevelsWithAutoExpandInSameNavigationProperty() + { + // Arrange + string queryUrl = "autoexpand/Customers(5)?$expand=Friend($levels=0)"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var customer = await response.Content.ReadAsObject(); + Assert.NotNull(customer); + VerifyOrderAndChoiceOrder(customer); + Assert.Null(customer["Friend"]); + } - [Theory] - [InlineData("1", 1)] - [InlineData("3", 3)] - [InlineData("max", 4)] - public async Task LevelsWithAutoExpandInDifferentNavigationProperty(string level, int levelNumber) + [Theory] + [InlineData("1", 1)] + [InlineData("3", 3)] + [InlineData("max", 4)] + public async Task LevelsWithAutoExpandInDifferentNavigationProperty(string level, int levelNumber) + { + // Arrange + string queryUrl = $"autoexpand/People?$expand=Friend($levels={level})"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseJson = await response.Content.ReadAsObject(); + this.output.WriteLine(responseJson.ToString()); + var people = responseJson["value"] as JArray; + var he = people[8] as JObject; + JObject friend = he; + for (int i = 1; i <= levelNumber; i++) { - // Arrange - string queryUrl = $"autoexpand/People?$expand=Friend($levels={level})"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var responseJson = await response.Content.ReadAsObject(); - this.output.WriteLine(responseJson.ToString()); - var people = responseJson["value"] as JArray; - var he = people[8] as JObject; - JObject friend = he; - for (int i = 1; i <= levelNumber; i++) + friend = friend["Friend"] as JObject; + Assert.NotNull(friend); + if (i + 2 <= levelNumber) { - friend = friend["Friend"] as JObject; - Assert.NotNull(friend); - if (i + 2 <= levelNumber) - { - VerifyOrderAndChoiceOrder(friend); - } + VerifyOrderAndChoiceOrder(friend); } - Assert.Null(friend["Friend"]); } + Assert.Null(friend["Friend"]); + } - private static void VerifyOrderAndChoiceOrder(JObject customer, bool special = false, bool vip = false) - { - JObject order = customer["Order"] as JObject; - Assert.NotNull(order); - - JObject choice = order["Choice"] as JObject; - Assert.NotNull(choice); - Assert.Equal((int)order["Id"] * 1000, choice["Amount"]); + private static void VerifyOrderAndChoiceOrder(JObject customer, bool special = false, bool vip = false) + { + JObject order = customer["Order"] as JObject; + Assert.NotNull(order); - if (special) - { - choice = order["SpecialChoice"] as JObject; - Assert.NotNull(choice); - Assert.Equal((int)order["Id"] * 2000, choice["Amount"]); - } + JObject choice = order["Choice"] as JObject; + Assert.NotNull(choice); + Assert.Equal((int)order["Id"] * 1000, choice["Amount"]); - if (vip) - { - choice = order["VipChoice"] as JObject; - Assert.NotNull(choice); - Assert.Equal((int)order["Id"] * 3000, choice["Amount"]); - } + if (special) + { + choice = order["SpecialChoice"] as JObject; + Assert.NotNull(choice); + Assert.Equal((int)order["Id"] * 2000, choice["Amount"]); } - [Theory] - [InlineData("autoexpand/NormalOrders")] - [InlineData("autoexpand/NormalOrders(2)")] - public async Task DerivedAutoExpandNavigationPropertyTest(string url) + if (vip) { - // Arrange - string queryUrl = url; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - string result = await response.Content.ReadAsStringAsync(); - Assert.Contains("OrderDetail", result); + choice = order["VipChoice"] as JObject; + Assert.NotNull(choice); + Assert.Equal((int)order["Id"] * 3000, choice["Amount"]); } + } + + [Theory] + [InlineData("autoexpand/NormalOrders")] + [InlineData("autoexpand/NormalOrders(2)")] + public async Task DerivedAutoExpandNavigationPropertyTest(string url) + { + // Arrange + string queryUrl = url; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + string result = await response.Content.ReadAsStringAsync(); + Assert.Contains("OrderDetail", result); + } - [Theory] - [InlineData("autoexpand/NormalOrders?$select=Id", true)] - [InlineData("autoexpand/NormalOrders", false)] - [InlineData("autoexpand/NormalOrders(2)?$select=Id", true)] - [InlineData("autoexpand/NormalOrders(2)", false)] - [InlineData("autoexpand/NormalOrders(3)?$select=Id", true)] - [InlineData("autoexpand/NormalOrders(3)", false)] - public async Task DisableAutoExpandWhenSelectIsPresentTest(string url, bool isSelectPresent) + [Theory] + [InlineData("autoexpand/NormalOrders?$select=Id", true)] + [InlineData("autoexpand/NormalOrders", false)] + [InlineData("autoexpand/NormalOrders(2)?$select=Id", true)] + [InlineData("autoexpand/NormalOrders(2)", false)] + [InlineData("autoexpand/NormalOrders(3)?$select=Id", true)] + [InlineData("autoexpand/NormalOrders(3)", false)] + public async Task DisableAutoExpandWhenSelectIsPresentTest(string url, bool isSelectPresent) + { + // Arrange + string queryUrl = url; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + string result = await response.Content.ReadAsStringAsync(); + if (isSelectPresent) { - // Arrange - string queryUrl = url; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - string result = await response.Content.ReadAsStringAsync(); - if (isSelectPresent) - { - Assert.DoesNotContain("NotShownOrderDetail4", result); - } - else - { - Assert.Contains("NotShownOrderDetail4", result); - } + Assert.DoesNotContain("NotShownOrderDetail4", result); } - - [Theory] - [InlineData("autoexpand/NormalOrders(2)?$expand=LinkOrder($select=Id)", true)] - [InlineData("autoexpand/NormalOrders(2)?$expand=LinkOrder", false)] - public async Task DisableAutoExpandWhenSelectIsPresentDollarExpandTest(string url, bool isSelectPresent) + else { - // Arrange - string queryUrl = url; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - string result = await response.Content.ReadAsStringAsync(); Assert.Contains("NotShownOrderDetail4", result); - if (isSelectPresent) - { - Assert.DoesNotContain("NotShownOrderDetail2", result); - } - else - { - Assert.Contains("NotShownOrderDetail2", result); - } } + } - [Fact] - public async Task QueryForProperty_Includes_AutoExpandNavigationProperty() + [Theory] + [InlineData("autoexpand/NormalOrders(2)?$expand=LinkOrder($select=Id)", true)] + [InlineData("autoexpand/NormalOrders(2)?$expand=LinkOrder", false)] + public async Task DisableAutoExpandWhenSelectIsPresentDollarExpandTest(string url, bool isSelectPresent) + { + // Arrange + string queryUrl = url; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + string result = await response.Content.ReadAsStringAsync(); + Assert.Contains("NotShownOrderDetail4", result); + if (isSelectPresent) { - // Arrange - string queryUrl = "autoexpand/Customers(8)/HomeAddress"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - string payload = await response.Content.ReadAsStringAsync(); - this.output.WriteLine(payload); - Assert.Equal("{" + - "\"Street\":\"CnStreet 8\"," + - "\"City\":\"CnCity 8\"," + - "\"CountryOrRegion\":{\"Id\":108,\"Name\":\"C and R 108\"}" + - "}", payload); + Assert.DoesNotContain("NotShownOrderDetail2", result); } - - [Fact] - public async Task QueryForProperty_Includes_AutoExpandNavigationPropertyOnDerivedType() + else { - // Arrange - string queryUrl = "autoexpand/Customers(9)/HomeAddress"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - string payload = await response.Content.ReadAsStringAsync(); - this.output.WriteLine(payload); - Assert.Equal("{" + - "\"Street\":\"UsStreet 9\"," + - "\"City\":\"UsCity 9\"," + - "\"CountryOrRegion\":{\"Id\":109,\"Name\":\"C and R 109\"}," + - "\"ZipCode\":{\"Id\":2009,\"Code\":\"Code 9\"}" + - "}", payload); + Assert.Contains("NotShownOrderDetail2", result); } + } - [Theory] - [InlineData("EnableQueryMenus")] - [InlineData("QueryOptionsOfTMenus")] - public async Task NonDefaultMaxExpansionDepthAppliesToAutoExpand(string entitySet) - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, $"autoexpand/{entitySet}"); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); - var client = CreateClient(); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - var content = await response.Content.ReadAsStringAsync(); - Assert.Equal( - "{" + - $"\"@odata.context\":\"http://localhost/autoexpand/$metadata#{entitySet}(Tabs(Items(Notes())))\"," + - "\"value\":[{\"Id\":1,\"Tabs\":[{\"Id\":1,\"Items\":[{\"Id\":1,\"Notes\":[{\"Id\":1}]}]}]}]" + - "}", - content); - } + [Fact] + public async Task QueryForProperty_Includes_AutoExpandNavigationProperty() + { + // Arrange + string queryUrl = "autoexpand/Customers(8)/HomeAddress"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payload = await response.Content.ReadAsStringAsync(); + this.output.WriteLine(payload); + Assert.Equal("{" + + "\"Street\":\"CnStreet 8\"," + + "\"City\":\"CnCity 8\"," + + "\"CountryOrRegion\":{\"Id\":108,\"Name\":\"C and R 108\"}" + + "}", payload); + } - [Fact] - public async Task PostCustomer_AutoExpandNavigationProperties() - { - //Arrange - string requestUri = "autoexpand/Customers"; + [Fact] + public async Task QueryForProperty_Includes_AutoExpandNavigationPropertyOnDerivedType() + { + // Arrange + string queryUrl = "autoexpand/Customers(9)/HomeAddress"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payload = await response.Content.ReadAsStringAsync(); + this.output.WriteLine(payload); + Assert.Equal("{" + + "\"Street\":\"UsStreet 9\"," + + "\"City\":\"UsCity 9\"," + + "\"CountryOrRegion\":{\"Id\":109,\"Name\":\"C and R 109\"}," + + "\"ZipCode\":{\"Id\":2009,\"Code\":\"Code 9\"}" + + "}", payload); + } - var content = @"{ - 'Id':88, - 'Order':{ 'Id':1, 'Choice' : {'Id': 101, 'Amount': 10}}, - 'Friend':{'Id': 99, 'HomeAddress': {'Street': 'Street 1', 'City': 'City 1'}} - }"; + [Theory] + [InlineData("EnableQueryMenus")] + [InlineData("QueryOptionsOfTMenus")] + public async Task NonDefaultMaxExpansionDepthAppliesToAutoExpand(string entitySet) + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, $"autoexpand/{entitySet}"); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal( + "{" + + $"\"@odata.context\":\"http://localhost/autoexpand/$metadata#{entitySet}(Tabs(Items(Notes())))\"," + + "\"value\":[{\"Id\":1,\"Tabs\":[{\"Id\":1,\"Items\":[{\"Id\":1,\"Notes\":[{\"Id\":1}]}]}]}]" + + "}", + content); + } - HttpClient client = CreateClient(); + [Fact] + public async Task PostCustomer_AutoExpandNavigationProperties() + { + //Arrange + string requestUri = "autoexpand/Customers"; - StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + var content = @"{ + 'Id':88, + 'Order':{ 'Id':1, 'Choice' : {'Id': 101, 'Amount': 10}}, + 'Friend':{'Id': 99, 'HomeAddress': {'Street': 'Street 1', 'City': 'City 1'}} + }"; - var expectedOrder = "Order\":{\"Id\":1,\"Choice\":{\"Id\":101,\"Amount\":10.0}}"; - var expectedFriend = "Friend\":{\"Id\":99,\"HomeAddress\":{\"Street\":\"Street 1\",\"City\":\"City 1\",\"CountryOrRegion\":null},\"Order\":null,\"Friend\":null}"; + HttpClient client = CreateClient(); - //Act & Assert - using (HttpRequestMessage requestForPost = new HttpRequestMessage(HttpMethod.Post, requestUri) { Content = stringContent }) - using (HttpResponseMessage response = await client.SendAsync(requestForPost)) - { - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - var json = response.Content.ReadAsStringAsync().Result; - Assert.Contains(expectedFriend, json); - Assert.Contains(expectedOrder, json); - } + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + + var expectedOrder = "Order\":{\"Id\":1,\"Choice\":{\"Id\":101,\"Amount\":10.0}}"; + var expectedFriend = "Friend\":{\"Id\":99,\"HomeAddress\":{\"Street\":\"Street 1\",\"City\":\"City 1\",\"CountryOrRegion\":null},\"Order\":null,\"Friend\":null}"; + + //Act & Assert + using (HttpRequestMessage requestForPost = new HttpRequestMessage(HttpMethod.Post, requestUri) { Content = stringContent }) + using (HttpResponseMessage response = await client.SendAsync(requestForPost)) + { + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains(expectedFriend, json); + Assert.Contains(expectedOrder, json); } + } - [Fact] - public async Task PutCustomer_AutoExpandNavigationProperties() + [Fact] + public async Task PutCustomer_AutoExpandNavigationProperties() + { + //Arrange + var requestUri = "autoexpand/Customers(2)"; + var content = "{\"Id\":2}"; + var client = CreateClient(); + var stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + + var expectedOrder = "\"Order\":{\"Id\":2,\"Choice\":{\"Id\":2,\"Amount\":2000.0}}"; + var expectedFriend = "\"Friend\":{" + + "\"Id\":1," + + "\"HomeAddress\":{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.AutoExpand.UsAddress\"," + + "\"Street\":\"UsStreet 1\"," + + "\"City\":\"UsCity 1\"," + + "\"CountryOrRegion\":{\"Id\":101,\"Name\":\"C and R 101\"}," + + "\"ZipCode\":{\"Id\":2001,\"Code\":\"Code 1\"}}," + + "\"Order\":{\"Id\":1}," + + "\"Friend\":null}}"; + + //Act & Assert + using (var request = new HttpRequestMessage(HttpMethod.Put, requestUri) { Content = stringContent }) { - //Arrange - var requestUri = "autoexpand/Customers(2)"; - var content = "{\"Id\":2}"; - var client = CreateClient(); - var stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); - - var expectedOrder = "\"Order\":{\"Id\":2,\"Choice\":{\"Id\":2,\"Amount\":2000.0}}"; - var expectedFriend = "\"Friend\":{" + - "\"Id\":1," + - "\"HomeAddress\":{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.AutoExpand.UsAddress\"," + - "\"Street\":\"UsStreet 1\"," + - "\"City\":\"UsCity 1\"," + - "\"CountryOrRegion\":{\"Id\":101,\"Name\":\"C and R 101\"}," + - "\"ZipCode\":{\"Id\":2001,\"Code\":\"Code 1\"}}," + - "\"Order\":{\"Id\":1}," + - "\"Friend\":null}}"; - - //Act & Assert - using (var request = new HttpRequestMessage(HttpMethod.Put, requestUri) { Content = stringContent }) + request.Headers.Add("Prefer", "return=representation"); + using (var response = await client.SendAsync(request)) { - request.Headers.Add("Prefer", "return=representation"); - using (var response = await client.SendAsync(request)) - { - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var json = response.Content.ReadAsStringAsync().Result; - Assert.Contains(expectedFriend, json); - Assert.Contains(expectedOrder, json); - } + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Contains(expectedFriend, json); + Assert.Contains(expectedOrder, json); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/ContentIdToLocationMappingTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/ContentIdToLocationMappingTests.cs index 7cce8a56a..2b2e84e97 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/ContentIdToLocationMappingTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/ContentIdToLocationMappingTests.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -24,177 +24,176 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Batch +namespace Microsoft.AspNetCore.OData.E2E.Tests.Batch; + +public class ContentIdToLocationMappingTests : WebApiTestBase { - public class ContentIdToLocationMappingTests : WebApiTestBase + private static IEdmModel edmModel; + + public ContentIdToLocationMappingTests(WebApiTestFixture fixture) + : base(fixture) { - private static IEdmModel edmModel; + } - public ContentIdToLocationMappingTests(WebApiTestFixture fixture) - : base(fixture) - { - } + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers( + typeof(ContentIdToLocationMappingParentsController), + typeof(ContentIdToLocationMappingChildrenController)); - protected static void UpdateConfigureServices(IServiceCollection services) + edmModel = GetEdmModel(); + services.AddControllers().AddOData(opt => { - services.ConfigureControllers( - typeof(ContentIdToLocationMappingParentsController), - typeof(ContentIdToLocationMappingChildrenController)); - - edmModel = GetEdmModel(); - services.AddControllers().AddOData(opt => - { - opt.EnableQueryFeatures(); - opt.EnableContinueOnErrorHeader = true; - opt.AddRouteComponents("ContentIdToLocationMapping", edmModel, new DefaultODataBatchHandler()); - }); - } + opt.EnableQueryFeatures(); + opt.EnableContinueOnErrorHeader = true; + opt.AddRouteComponents("ContentIdToLocationMapping", edmModel, new DefaultODataBatchHandler()); + }); + } - protected static void UpdateConfigure(IApplicationBuilder app) + protected static void UpdateConfigure(IApplicationBuilder app) + { + app.UseODataBatching(); + app.UseRouting(); + app.UseEndpoints(endpoints => { - app.UseODataBatching(); - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + endpoints.MapControllers(); + }); + } - protected static IEdmModel GetEdmModel() - { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("ContentIdToLocationMappingParents"); - builder.EntitySet("ContentIdToLocationMappingChildren"); - builder.Namespace = typeof(ContentIdToLocationMappingParent).Namespace; + protected static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("ContentIdToLocationMappingParents"); + builder.EntitySet("ContentIdToLocationMappingChildren"); + builder.Namespace = typeof(ContentIdToLocationMappingParent).Namespace; - return builder.GetEdmModel(); - } + return builder.GetEdmModel(); + } - [Fact] - public async Task CanResolveContentIdInODataBindAnnotationAsync() + [Fact] + public async Task CanResolveContentIdInODataBindAnnotationAsync() + { + // Arrange + HttpClient client = CreateClient(); + string serviceBase = $"{client.BaseAddress}ContentIdToLocationMapping"; + string requestUri = $"{serviceBase}/$batch"; + string parentsUri = $"{serviceBase}/ContentIdToLocationMappingParents"; + string childrenUri = $"{serviceBase}/ContentIdToLocationMappingChildren"; + string payload = "{" + + " \"requests\": [" + + " {" + + " \"id\": \"1\"," + + " \"method\": \"POST\"," + + $" \"url\": \"{parentsUri}\"," + + " \"headers\": {" + + " \"OData-Version\": \"4.0\"," + + " \"Content-Type\": \"application/json;odata.metadata=minimal\"," + + " \"Accept\": \"application/json;odata.metadata=minimal\"" + + " }," + + " \"body\": {\"ParentId\":123}" + + " }," + + " {" + + " \"id\": \"2\"," + + " \"method\": \"POST\"," + + $" \"url\": \"{childrenUri}\"," + + " \"headers\": {" + + " \"OData-Version\": \"4.0\"," + + " \"Content-Type\": \"application/json;odata.metadata=minimal\"," + + " \"Accept\": \"application/json;odata.metadata=minimal\"" + + " }," + + " \"body\": {" + + " \"Parent@odata.bind\": \"$1\"" + + " }" + + " }" + + " ]" + + "}"; + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); + int subResponseCount = 0; + using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) { - // Arrange - HttpClient client = CreateClient(); - string serviceBase = $"{client.BaseAddress}ContentIdToLocationMapping"; - string requestUri = $"{serviceBase}/$batch"; - string parentsUri = $"{serviceBase}/ContentIdToLocationMappingParents"; - string childrenUri = $"{serviceBase}/ContentIdToLocationMappingChildren"; - string payload = "{" + - " \"requests\": [" + - " {" + - " \"id\": \"1\"," + - " \"method\": \"POST\"," + - $" \"url\": \"{parentsUri}\"," + - " \"headers\": {" + - " \"OData-Version\": \"4.0\"," + - " \"Content-Type\": \"application/json;odata.metadata=minimal\"," + - " \"Accept\": \"application/json;odata.metadata=minimal\"" + - " }," + - " \"body\": {\"ParentId\":123}" + - " }," + - " {" + - " \"id\": \"2\"," + - " \"method\": \"POST\"," + - $" \"url\": \"{childrenUri}\"," + - " \"headers\": {" + - " \"OData-Version\": \"4.0\"," + - " \"Content-Type\": \"application/json;odata.metadata=minimal\"," + - " \"Accept\": \"application/json;odata.metadata=minimal\"" + - " }," + - " \"body\": {" + - " \"Parent@odata.bind\": \"$1\"" + - " }" + - " }" + - " ]" + - "}"; - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); - int subResponseCount = 0; - using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + var batchReader = messageReader.CreateODataBatchReader(); + while (batchReader.Read()) { - var batchReader = messageReader.CreateODataBatchReader(); - while (batchReader.Read()) + switch (batchReader.State) { - switch (batchReader.State) - { - case ODataBatchReaderState.Operation: - var operationMessage = batchReader.CreateOperationResponseMessage(); - subResponseCount++; - Assert.Equal(201, operationMessage.StatusCode); - break; - } + case ODataBatchReaderState.Operation: + var operationMessage = batchReader.CreateOperationResponseMessage(); + subResponseCount++; + Assert.Equal(201, operationMessage.StatusCode); + break; } } - - // NOTE: We assert that $1 is successfully resolved from the controller action - Assert.Equal(2, subResponseCount); } + + // NOTE: We assert that $1 is successfully resolved from the controller action + Assert.Equal(2, subResponseCount); } +} - public class ContentIdToLocationMappingParentsController : ODataController +public class ContentIdToLocationMappingParentsController : ODataController +{ + public ActionResult Post([FromBody] ContentIdToLocationMappingParent parent) { - public ActionResult Post([FromBody] ContentIdToLocationMappingParent parent) - { - return Created(new Uri($"{Request.Scheme}://{Request.Host}{Request.Path}/{parent.ParentId}"), parent); - } + return Created(new Uri($"{Request.Scheme}://{Request.Host}{Request.Path}/{parent.ParentId}"), parent); } +} - public class ContentIdToLocationMappingChildrenController : ODataController +public class ContentIdToLocationMappingChildrenController : ODataController +{ + public ActionResult Post([FromBody] ContentIdToLocationMappingChild child) { - public ActionResult Post([FromBody] ContentIdToLocationMappingChild child) - { - Assert.Equal(123, child.Parent.ParentId); + Assert.Equal(123, child.Parent.ParentId); - return Created(new Uri($"{Request.Scheme}://{Request.Host}{Request.Path}/{child.ChildId}"), child); - } + return Created(new Uri($"{Request.Scheme}://{Request.Host}{Request.Path}/{child.ChildId}"), child); } +} - public class ContentIdToLocationMappingParent +public class ContentIdToLocationMappingParent +{ + public ContentIdToLocationMappingParent() { - public ContentIdToLocationMappingParent() - { - Children = new HashSet(); - } + Children = new HashSet(); + } - [Key] - public int ParentId - { - get; set; - } + [Key] + public int ParentId + { + get; set; + } - public virtual ICollection Children - { - get; set; - } + public virtual ICollection Children + { + get; set; } +} - public class ContentIdToLocationMappingChild +public class ContentIdToLocationMappingChild +{ + [Key] + public int ChildId { - [Key] - public int ChildId - { - get; set; - } + get; set; + } - public int? ParentId - { - get; set; - } + public int? ParentId + { + get; set; + } - public virtual ContentIdToLocationMappingParent Parent - { - get; set; - } + public virtual ContentIdToLocationMappingParent Parent + { + get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/DefaultODataBatchHandlerController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/DefaultODataBatchHandlerController.cs index 142da8778..d2f32c46d 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/DefaultODataBatchHandlerController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/DefaultODataBatchHandlerController.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -15,62 +15,61 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Batch -{ - public class DefaultBatchCustomersController : ODataController - { - private static IList _customers = Enumerable.Range(0, 10).Select(i => - new DefaultBatchCustomer - { - Id = i, - Name = string.Format("Name {0}", i) - }).ToList(); +namespace Microsoft.AspNetCore.OData.E2E.Tests.Batch; - public IActionResult Get(int key) +public class DefaultBatchCustomersController : ODataController +{ + private static IList _customers = Enumerable.Range(0, 10).Select(i => + new DefaultBatchCustomer { - DefaultBatchCustomer customers = _customers.FirstOrDefault(c => c.Id == key); - if (customers == null) - { - return BadRequest(); - } + Id = i, + Name = string.Format("Name {0}", i) + }).ToList(); - return Ok(customers); - } - - [EnableQuery] - public IQueryable OddCustomers() + public IActionResult Get(int key) + { + DefaultBatchCustomer customers = _customers.FirstOrDefault(c => c.Id == key); + if (customers == null) { - return _customers.Where(x => x.Id % 2 == 1).AsQueryable(); + return BadRequest(); } - public IActionResult Post([FromBody] DefaultBatchCustomer customer) - { - _customers.Add(customer); - return Created(customer); - } + return Ok(customers); + } - public IActionResult CreateRef([FromODataUri] int key, string navigationProperty, [FromBody] Uri link) - { - return NoContent(); - } + [EnableQuery] + public IQueryable OddCustomers() + { + return _customers.Where(x => x.Id % 2 == 1).AsQueryable(); } - public class DefaultBatchOrdersController : ODataController + public IActionResult Post([FromBody] DefaultBatchCustomer customer) { - private static IList _orders = Enumerable.Range(0, 13).Select(i => - new DefaultBatchOrder - { - Id = i - }).ToList(); + _customers.Add(customer); + return Created(customer); + } - public DefaultBatchOrdersController() - { - } + public IActionResult CreateRef([FromODataUri] int key, string navigationProperty, [FromBody] Uri link) + { + return NoContent(); + } +} - public IActionResult Post([FromBody] DefaultBatchOrder order) +public class DefaultBatchOrdersController : ODataController +{ + private static IList _orders = Enumerable.Range(0, 13).Select(i => + new DefaultBatchOrder { - _orders.Add(order); - return Created(order); - } + Id = i + }).ToList(); + + public DefaultBatchOrdersController() + { + } + + public IActionResult Post([FromBody] DefaultBatchOrder order) + { + _orders.Add(order); + return Created(order); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/DefaultODataBatchHandlerModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/DefaultODataBatchHandlerModel.cs index 76fe016d5..8576e82c0 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/DefaultODataBatchHandlerModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/DefaultODataBatchHandlerModel.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -8,21 +8,20 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Batch +namespace Microsoft.AspNetCore.OData.E2E.Tests.Batch; + +public class DefaultBatchCustomer { - public class DefaultBatchCustomer - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public IList Orders { get; set; } - } + public IList Orders { get; set; } +} - public class DefaultBatchOrder - { - public int Id { get; set; } +public class DefaultBatchOrder +{ + public int Id { get; set; } - public DateTimeOffset PurchaseDate { get; set; } - } + public DateTimeOffset PurchaseDate { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/DefaultODataBatchHandlerTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/DefaultODataBatchHandlerTests.cs index 528df3bb7..24aa86aea 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/DefaultODataBatchHandlerTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/DefaultODataBatchHandlerTests.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -19,67 +19,67 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Batch +namespace Microsoft.AspNetCore.OData.E2E.Tests.Batch; + +public class DefaultBatchHandlerCUDBatchTests : WebApiTestBase { - public class DefaultBatchHandlerCUDBatchTests : WebApiTestBase + private static IEdmModel edmModel; + + public DefaultBatchHandlerCUDBatchTests(WebApiTestFixture fixture) + : base(fixture) { - private static IEdmModel edmModel; + } - public DefaultBatchHandlerCUDBatchTests(WebApiTestFixture fixture) - : base(fixture) - { - } + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(DefaultBatchCustomersController), typeof(DefaultBatchOrdersController)); - protected static void UpdateConfigureServices(IServiceCollection services) + edmModel = GetEdmModel(); + services.AddControllers().AddOData(opt => { - services.ConfigureControllers(typeof(DefaultBatchCustomersController), typeof(DefaultBatchOrdersController)); - - edmModel = GetEdmModel(); - services.AddControllers().AddOData(opt => - { - opt.EnableQueryFeatures(); - opt.EnableContinueOnErrorHeader = true; - opt.AddRouteComponents("DefaultBatch", edmModel, new DefaultODataBatchHandler()); - }); - } + opt.EnableQueryFeatures(); + opt.EnableContinueOnErrorHeader = true; + opt.AddRouteComponents("DefaultBatch", edmModel, new DefaultODataBatchHandler()); + }); + } - protected static void UpdateConfigure(IApplicationBuilder app) + protected static void UpdateConfigure(IApplicationBuilder app) + { + app.UseODataBatching(); + app.UseRouting(); + app.UseEndpoints(endpoints => { - app.UseODataBatching(); - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + endpoints.MapControllers(); + }); + } - protected static IEdmModel GetEdmModel() - { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - EntitySetConfiguration customers = builder.EntitySet("DefaultBatchCustomers"); - builder.EntitySet("DefaultBatchOrders"); - customers.EntityType.Collection.Action("OddCustomers").ReturnsCollectionFromEntitySet("DefaultBatchCustomers"); - builder.MaxDataServiceVersion = builder.DataServiceVersion; - builder.Namespace = typeof(DefaultBatchCustomer).Namespace; - return builder.GetEdmModel(); - } + protected static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + EntitySetConfiguration customers = builder.EntitySet("DefaultBatchCustomers"); + builder.EntitySet("DefaultBatchOrders"); + customers.EntityType.Collection.Action("OddCustomers").ReturnsCollectionFromEntitySet("DefaultBatchCustomers"); + builder.MaxDataServiceVersion = builder.DataServiceVersion; + builder.Namespace = typeof(DefaultBatchCustomer).Namespace; + return builder.GetEdmModel(); + } - [Fact] - public async Task CanHandleAbsoluteAndRelativeUrls() - { - // Arrange - HttpClient client = CreateClient(); - string requestUri = "DefaultBatch/$batch"; - - string host = client.BaseAddress.Host; - string relativeToServiceRootUri = "DefaultBatchCustomers"; - string relativeToHostUri = "/DefaultBatch/DefaultBatchCustomers"; - string absoluteUri = "http://localhost/DefaultBatch/DefaultBatchCustomers"; - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); - HttpContent content = new StringContent(@" + [Fact] + public async Task CanHandleAbsoluteAndRelativeUrls() + { + // Arrange + HttpClient client = CreateClient(); + string requestUri = "DefaultBatch/$batch"; + + string host = client.BaseAddress.Host; + string relativeToServiceRootUri = "DefaultBatchCustomers"; + string relativeToHostUri = "/DefaultBatch/DefaultBatchCustomers"; + string absoluteUri = "http://localhost/DefaultBatch/DefaultBatchCustomers"; + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); + HttpContent content = new StringContent(@" --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0 Content-Type: multipart/mixed; boundary=changeset_6c67825c-8938-4f11-af6b-a25861ee53cc @@ -126,135 +126,135 @@ public async Task CanHandleAbsoluteAndRelativeUrls() --changeset_6c67825c-8938-4f11-af6b-a25861ee53cc-- --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0-- "); - // string b = await content.ReadAsStringAsync(); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); - request.Content = content; - HttpResponseMessage response = await client.SendAsync(request); + // string b = await content.ReadAsStringAsync(); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); + request.Content = content; + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - //string a = await response.Content.ReadAsStringAsync(); + //string a = await response.Content.ReadAsStringAsync(); - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); - int subResponseCount = 0; - using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); + int subResponseCount = 0; + using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + { + var batchReader = messageReader.CreateODataBatchReader(); + while (batchReader.Read()) { - var batchReader = messageReader.CreateODataBatchReader(); - while (batchReader.Read()) + switch (batchReader.State) { - switch (batchReader.State) - { - case ODataBatchReaderState.Operation: - var operationMessage = batchReader.CreateOperationResponseMessage(); - subResponseCount++; - Assert.Equal(201, operationMessage.StatusCode); - break; - } + case ODataBatchReaderState.Operation: + var operationMessage = batchReader.CreateOperationResponseMessage(); + subResponseCount++; + Assert.Equal(201, operationMessage.StatusCode); + break; } } - - Assert.Equal(3, subResponseCount); } - [Fact] - public async Task CanHandleAutomicityGroupRequestsAndUngroupedRequest_JsonBatch() - { - // Arrange - HttpClient client = CreateClient(); + Assert.Equal(3, subResponseCount); + } - string requestUri = "DefaultBatch/$batch"; - string absoluteUri = "http://localhost/DefaultBatch/DefaultBatchCustomers"; + [Fact] + public async Task CanHandleAutomicityGroupRequestsAndUngroupedRequest_JsonBatch() + { + // Arrange + HttpClient client = CreateClient(); - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - HttpContent content = new StringContent(@" - { - ""requests"": [{ - ""id"": ""1"", - ""atomicityGroup"": ""f7de7314-2f3d-4422-b840-ada6d6de0f18"", - ""method"": ""POST"", - ""url"": """ + absoluteUri + @""", - ""headers"": { - ""OData-Version"": ""4.0"", - ""Content-Type"": ""application/json;odata.metadata=minimal"", - ""Accept"": ""application/json;odata.metadata=minimal"" - }, - ""body"": { - ""Id"":11, - ""Name"":""CreatedByJsonBatch_11"" - } - }, { - ""id"": ""2"", - ""atomicityGroup"": ""f7de7314-2f3d-4422-b840-ada6d6de0f18"", - ""method"": ""POST"", - ""url"": """ + absoluteUri + @""", - ""headers"": { - ""OData-Version"": ""4.0"", - ""Content-Type"": ""application/json;odata.metadata=minimal"", - ""Accept"": ""application/json;odata.metadata=minimal"" - }, - ""body"": { - ""Id"":12, - ""Name"":""CreatedByJsonBatch_12"" - } - }, { - ""id"": ""3"", - ""method"": ""POST"", - ""url"": """ + absoluteUri + @""", - ""headers"": { - ""OData-Version"": ""4.0"", - ""Content-Type"": ""application/json;odata.metadata=minimal"", - ""Accept"": ""application/json;odata.metadata=minimal"" - }, - ""body"": { - ""Id"":13, - ""Name"":""CreatedByJsonBatch_3"" - } + string requestUri = "DefaultBatch/$batch"; + string absoluteUri = "http://localhost/DefaultBatch/DefaultBatchCustomers"; + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + HttpContent content = new StringContent(@" + { + ""requests"": [{ + ""id"": ""1"", + ""atomicityGroup"": ""f7de7314-2f3d-4422-b840-ada6d6de0f18"", + ""method"": ""POST"", + ""url"": """ + absoluteUri + @""", + ""headers"": { + ""OData-Version"": ""4.0"", + ""Content-Type"": ""application/json;odata.metadata=minimal"", + ""Accept"": ""application/json;odata.metadata=minimal"" + }, + ""body"": { + ""Id"":11, + ""Name"":""CreatedByJsonBatch_11"" } - ] - }"); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content = content; - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); - int subResponseCount = 0; - using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + }, { + ""id"": ""2"", + ""atomicityGroup"": ""f7de7314-2f3d-4422-b840-ada6d6de0f18"", + ""method"": ""POST"", + ""url"": """ + absoluteUri + @""", + ""headers"": { + ""OData-Version"": ""4.0"", + ""Content-Type"": ""application/json;odata.metadata=minimal"", + ""Accept"": ""application/json;odata.metadata=minimal"" + }, + ""body"": { + ""Id"":12, + ""Name"":""CreatedByJsonBatch_12"" + } + }, { + ""id"": ""3"", + ""method"": ""POST"", + ""url"": """ + absoluteUri + @""", + ""headers"": { + ""OData-Version"": ""4.0"", + ""Content-Type"": ""application/json;odata.metadata=minimal"", + ""Accept"": ""application/json;odata.metadata=minimal"" + }, + ""body"": { + ""Id"":13, + ""Name"":""CreatedByJsonBatch_3"" + } + } + ] + }"); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content = content; + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); + int subResponseCount = 0; + using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + { + var batchReader = messageReader.CreateODataBatchReader(); + while (batchReader.Read()) { - var batchReader = messageReader.CreateODataBatchReader(); - while (batchReader.Read()) + switch (batchReader.State) { - switch (batchReader.State) - { - case ODataBatchReaderState.Operation: - var operationMessage = batchReader.CreateOperationResponseMessage(); - subResponseCount++; - Assert.Equal(201, operationMessage.StatusCode); - break; - } + case ODataBatchReaderState.Operation: + var operationMessage = batchReader.CreateOperationResponseMessage(); + subResponseCount++; + Assert.Equal(201, operationMessage.StatusCode); + break; } } - - Assert.Equal(3, subResponseCount); } - [Fact] - public async Task CanNotContinueOnErrorWhenHeaderNotSet() - { - // Arrange - HttpClient client = CreateClient(); - var requestUri = "DefaultBatch/$batch"; - string absoluteUri = "http://localhost/DefaultBatch/DefaultBatchCustomers"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); - HttpContent content = new StringContent( + Assert.Equal(3, subResponseCount); + } + + [Fact] + public async Task CanNotContinueOnErrorWhenHeaderNotSet() + { + // Arrange + HttpClient client = CreateClient(); + var requestUri = "DefaultBatch/$batch"; + string absoluteUri = "http://localhost/DefaultBatch/DefaultBatchCustomers"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); + HttpContent content = new StringContent( @"--batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0 Content-Type: application/http Content-Transfer-Encoding: binary @@ -275,52 +275,52 @@ public async Task CanNotContinueOnErrorWhenHeaderNotSet() --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0-- "); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); - request.Content = content; + content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); + request.Content = content; - // Act - HttpResponseMessage response = await client.SendAsync(request); - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); - int subResponseCount = 0; + // Act + HttpResponseMessage response = await client.SendAsync(request); + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); + int subResponseCount = 0; - // Assert - using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + // Assert + using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + { + var batchReader = messageReader.CreateODataBatchReader(); + while (batchReader.Read()) { - var batchReader = messageReader.CreateODataBatchReader(); - while (batchReader.Read()) + switch (batchReader.State) { - switch (batchReader.State) - { - case ODataBatchReaderState.Operation: - var operationMessage = batchReader.CreateOperationResponseMessage(); - subResponseCount++; - if (subResponseCount == 2) - { - Assert.Equal(400, operationMessage.StatusCode); - } - else - { - Assert.Equal(200, operationMessage.StatusCode); - } - break; - } + case ODataBatchReaderState.Operation: + var operationMessage = batchReader.CreateOperationResponseMessage(); + subResponseCount++; + if (subResponseCount == 2) + { + Assert.Equal(400, operationMessage.StatusCode); + } + else + { + Assert.Equal(200, operationMessage.StatusCode); + } + break; } } - Assert.Equal(2, subResponseCount); } + Assert.Equal(2, subResponseCount); + } - [Fact] - public async Task CanContinueOnErrorWhenHeaderSet() - { - // Arrange - HttpClient client = CreateClient(); - var requestUri = "DefaultBatch/$batch"; - string absoluteUri = "http://localhost/DefaultBatch/DefaultBatchCustomers"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); - request.Headers.Add("prefer", "odata.continue-on-error"); - HttpContent content = new StringContent( + [Fact] + public async Task CanContinueOnErrorWhenHeaderSet() + { + // Arrange + HttpClient client = CreateClient(); + var requestUri = "DefaultBatch/$batch"; + string absoluteUri = "http://localhost/DefaultBatch/DefaultBatchCustomers"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); + request.Headers.Add("prefer", "odata.continue-on-error"); + HttpContent content = new StringContent( @"--batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0 Content-Type: application/http Content-Transfer-Encoding: binary @@ -341,55 +341,55 @@ public async Task CanContinueOnErrorWhenHeaderSet() --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0-- "); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); - request.Content = content; + content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); + request.Content = content; - // Act - HttpResponseMessage response = await client.SendAsync(request); - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); - int subResponseCount = 0; + // Act + HttpResponseMessage response = await client.SendAsync(request); + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); + int subResponseCount = 0; - // Assert - using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + // Assert + using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + { + var batchReader = messageReader.CreateODataBatchReader(); + while (batchReader.Read()) { - var batchReader = messageReader.CreateODataBatchReader(); - while (batchReader.Read()) + switch (batchReader.State) { - switch (batchReader.State) - { - case ODataBatchReaderState.Operation: - var operationMessage = batchReader.CreateOperationResponseMessage(); - subResponseCount++; - if (subResponseCount == 2) - { - Assert.Equal(400, operationMessage.StatusCode); - } - else - { - Assert.Equal(200, operationMessage.StatusCode); - } - break; - } + case ODataBatchReaderState.Operation: + var operationMessage = batchReader.CreateOperationResponseMessage(); + subResponseCount++; + if (subResponseCount == 2) + { + Assert.Equal(400, operationMessage.StatusCode); + } + else + { + Assert.Equal(200, operationMessage.StatusCode); + } + break; } } - Assert.Equal(3, subResponseCount); } + Assert.Equal(3, subResponseCount); + } - [Fact] - public async Task CanHandleContentIDInRelativeUrl() - { - // Arrange - HttpClient client = CreateClient(); - string requestUri = "DefaultBatch/$batch"; + [Fact] + public async Task CanHandleContentIDInRelativeUrl() + { + // Arrange + HttpClient client = CreateClient(); + string requestUri = "DefaultBatch/$batch"; - string defaultBatchCustomersAbsoluteUri = "http://localhost/DefaultBatch/DefaultBatchCustomers"; - string defaultBatchOrdersAbsoluteUri = "http://localhost/DefaultBatch/DefaultBatchOrders"; + string defaultBatchCustomersAbsoluteUri = "http://localhost/DefaultBatch/DefaultBatchCustomers"; + string defaultBatchOrdersAbsoluteUri = "http://localhost/DefaultBatch/DefaultBatchOrders"; - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); - HttpContent content = new StringContent(@" + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); + HttpContent content = new StringContent(@" --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0 Content-Type: multipart/mixed; boundary=changeset_6c67825c-8938-4f11-af6b-a25861ee53cc @@ -435,128 +435,127 @@ public async Task CanHandleContentIDInRelativeUrl() --changeset_6c67825c-8938-4f11-af6b-a25861ee53cc-- --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0-- "); - // string b = await content.ReadAsStringAsync(); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); - request.Content = content; - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); - int subResponseCount = 0; - using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) - { - var batchReader = messageReader.CreateODataBatchReader(); - var subResponseStatusCodes = new int[] { 201, 201, 204 /*CreateRef*/ }; + // string b = await content.ReadAsStringAsync(); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); + request.Content = content; + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); + int subResponseCount = 0; + using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + { + var batchReader = messageReader.CreateODataBatchReader(); + var subResponseStatusCodes = new int[] { 201, 201, 204 /*CreateRef*/ }; - while (batchReader.Read()) + while (batchReader.Read()) + { + switch (batchReader.State) { - switch (batchReader.State) - { - case ODataBatchReaderState.Operation: - var operationMessage = batchReader.CreateOperationResponseMessage(); - var subResponseStatusCode = subResponseCount < subResponseStatusCodes.Length ? subResponseStatusCodes[subResponseCount] : 201; - - subResponseCount++; - Assert.Equal(subResponseStatusCode, operationMessage.StatusCode); - break; - } + case ODataBatchReaderState.Operation: + var operationMessage = batchReader.CreateOperationResponseMessage(); + var subResponseStatusCode = subResponseCount < subResponseStatusCodes.Length ? subResponseStatusCodes[subResponseCount] : 201; + + subResponseCount++; + Assert.Equal(subResponseStatusCode, operationMessage.StatusCode); + break; } } - - Assert.Equal(3, subResponseCount); } - [Fact] - public async Task CanHandleReferencingOfRequestsNotSharingAutomicityGroup() - { - // Arrange - var client = CreateClient(); + Assert.Equal(3, subResponseCount); + } - var requestUri = "DefaultBatch/$batch"; - var defaultBatchCustomersRelativeUri = "DefaultBatchCustomers"; - var defaultBatchOrdersRelativeUri = "DefaultBatchOrders"; + [Fact] + public async Task CanHandleReferencingOfRequestsNotSharingAutomicityGroup() + { + // Arrange + var client = CreateClient(); - // Act - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - HttpContent content = new StringContent(@" - { - ""requests"": [{ - ""id"": ""1"", - ""method"": ""POST"", - ""url"": """ + defaultBatchCustomersRelativeUri + @""", - ""headers"": { - ""OData-Version"": ""4.0"", - ""Content-Type"": ""application/json;odata.metadata=minimal"", - ""Accept"": ""application/json;odata.metadata=minimal"" - }, - ""body"": { - ""Id"":7, - ""Name"":""Customer 7"" - } - }, { - ""id"": ""2"", - ""dependsOn"": [""1""], - ""method"": ""POST"", - ""url"": """ + defaultBatchOrdersRelativeUri + @""", - ""headers"": { - ""OData-Version"": ""4.0"", - ""Content-Type"": ""application/json;odata.metadata=minimal"", - ""Accept"": ""application/json;odata.metadata=minimal"" - }, - ""body"": { - ""Id"":7 - } - }, { - ""id"": ""3"", - ""dependsOn"": [""1"", ""2""], - ""method"": ""POST"", - ""url"": ""$1/Orders/$ref"", - ""headers"": { - ""OData-Version"": ""4.0"", - ""Content-Type"": ""application/json;odata.metadata=minimal"", - ""Accept"": ""application/json;odata.metadata=minimal"" - }, - ""body"": { - ""@odata.id"":""$2"" - } + var requestUri = "DefaultBatch/$batch"; + var defaultBatchCustomersRelativeUri = "DefaultBatchCustomers"; + var defaultBatchOrdersRelativeUri = "DefaultBatchOrders"; + + // Act + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + HttpContent content = new StringContent(@" + { + ""requests"": [{ + ""id"": ""1"", + ""method"": ""POST"", + ""url"": """ + defaultBatchCustomersRelativeUri + @""", + ""headers"": { + ""OData-Version"": ""4.0"", + ""Content-Type"": ""application/json;odata.metadata=minimal"", + ""Accept"": ""application/json;odata.metadata=minimal"" + }, + ""body"": { + ""Id"":7, + ""Name"":""Customer 7"" } - ] - }"); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content = content; - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); - int subResponseCount = 0; - using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) - { - var batchReader = messageReader.CreateODataBatchReader(); - var subResponseStatusCodes = new int[] { 201, 201, 204 /*CreateRef*/ }; + }, { + ""id"": ""2"", + ""dependsOn"": [""1""], + ""method"": ""POST"", + ""url"": """ + defaultBatchOrdersRelativeUri + @""", + ""headers"": { + ""OData-Version"": ""4.0"", + ""Content-Type"": ""application/json;odata.metadata=minimal"", + ""Accept"": ""application/json;odata.metadata=minimal"" + }, + ""body"": { + ""Id"":7 + } + }, { + ""id"": ""3"", + ""dependsOn"": [""1"", ""2""], + ""method"": ""POST"", + ""url"": ""$1/Orders/$ref"", + ""headers"": { + ""OData-Version"": ""4.0"", + ""Content-Type"": ""application/json;odata.metadata=minimal"", + ""Accept"": ""application/json;odata.metadata=minimal"" + }, + ""body"": { + ""@odata.id"":""$2"" + } + } + ] + }"); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content = content; + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); + int subResponseCount = 0; + using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + { + var batchReader = messageReader.CreateODataBatchReader(); + var subResponseStatusCodes = new int[] { 201, 201, 204 /*CreateRef*/ }; - while (batchReader.Read()) + while (batchReader.Read()) + { + switch (batchReader.State) { - switch (batchReader.State) - { - case ODataBatchReaderState.Operation: - var operationMessage = batchReader.CreateOperationResponseMessage(); - var subResponseStatusCode = subResponseCount < subResponseStatusCodes.Length ? subResponseStatusCodes[subResponseCount] : 201; + case ODataBatchReaderState.Operation: + var operationMessage = batchReader.CreateOperationResponseMessage(); + var subResponseStatusCode = subResponseCount < subResponseStatusCodes.Length ? subResponseStatusCodes[subResponseCount] : 201; - subResponseCount++; - Assert.Equal(subResponseStatusCode, operationMessage.StatusCode); - break; - } + subResponseCount++; + Assert.Equal(subResponseStatusCode, operationMessage.StatusCode); + break; } } - - Assert.Equal(3, subResponseCount); } + + Assert.Equal(3, subResponseCount); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/UnbufferedODataBatchHandlerTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/UnbufferedODataBatchHandlerTests.cs index ee3999df4..85edf93a9 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/UnbufferedODataBatchHandlerTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Batch/UnbufferedODataBatchHandlerTests.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -19,67 +19,67 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Batch +namespace Microsoft.AspNetCore.OData.E2E.Tests.Batch; + +public class UnbufferedBatchHandlerCUDBatchTests : WebApiTestBase { - public class UnbufferedBatchHandlerCUDBatchTests : WebApiTestBase + private static IEdmModel edmModel; + + public UnbufferedBatchHandlerCUDBatchTests(WebApiTestFixture fixture) + : base(fixture) { - private static IEdmModel edmModel; + } - public UnbufferedBatchHandlerCUDBatchTests(WebApiTestFixture fixture) - : base(fixture) - { - } + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(DefaultBatchCustomersController), typeof(DefaultBatchOrdersController)); - protected static void UpdateConfigureServices(IServiceCollection services) + edmModel = GetEdmModel(); + services.AddControllers().AddOData(opt => { - services.ConfigureControllers(typeof(DefaultBatchCustomersController), typeof(DefaultBatchOrdersController)); - - edmModel = GetEdmModel(); - services.AddControllers().AddOData(opt => - { - opt.EnableQueryFeatures(); - opt.EnableContinueOnErrorHeader = true; - opt.AddRouteComponents("UnbufferedBatch", edmModel, new UnbufferedODataBatchHandler()); - }); - } + opt.EnableQueryFeatures(); + opt.EnableContinueOnErrorHeader = true; + opt.AddRouteComponents("UnbufferedBatch", edmModel, new UnbufferedODataBatchHandler()); + }); + } - protected static void UpdateConfigure(IApplicationBuilder app) + protected static void UpdateConfigure(IApplicationBuilder app) + { + app.UseODataBatching(); + app.UseRouting(); + app.UseEndpoints(endpoints => { - app.UseODataBatching(); - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + endpoints.MapControllers(); + }); + } - protected static IEdmModel GetEdmModel() - { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - EntitySetConfiguration customers = builder.EntitySet("DefaultBatchCustomers"); - builder.EntitySet("DefaultBatchOrders"); - customers.EntityType.Collection.Action("OddCustomers").ReturnsCollectionFromEntitySet("DefaultBatchCustomers"); - builder.MaxDataServiceVersion = builder.DataServiceVersion; - builder.Namespace = typeof(DefaultBatchCustomer).Namespace; - return builder.GetEdmModel(); - } + protected static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + EntitySetConfiguration customers = builder.EntitySet("DefaultBatchCustomers"); + builder.EntitySet("DefaultBatchOrders"); + customers.EntityType.Collection.Action("OddCustomers").ReturnsCollectionFromEntitySet("DefaultBatchCustomers"); + builder.MaxDataServiceVersion = builder.DataServiceVersion; + builder.Namespace = typeof(DefaultBatchCustomer).Namespace; + return builder.GetEdmModel(); + } - [Fact] - public async Task CanHandleAbsoluteAndRelativeUrls() - { - // Arrange - HttpClient client = CreateClient(); - string requestUri = "UnbufferedBatch/$batch"; - - string host = client.BaseAddress.Host; - string relativeToServiceRootUri = "DefaultBatchCustomers"; - string relativeToHostUri = "/UnbufferedBatch/DefaultBatchCustomers"; - string absoluteUri = "http://localhost/UnbufferedBatch/DefaultBatchCustomers"; - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); - HttpContent content = new StringContent(@" + [Fact] + public async Task CanHandleAbsoluteAndRelativeUrls() + { + // Arrange + HttpClient client = CreateClient(); + string requestUri = "UnbufferedBatch/$batch"; + + string host = client.BaseAddress.Host; + string relativeToServiceRootUri = "DefaultBatchCustomers"; + string relativeToHostUri = "/UnbufferedBatch/DefaultBatchCustomers"; + string absoluteUri = "http://localhost/UnbufferedBatch/DefaultBatchCustomers"; + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); + HttpContent content = new StringContent(@" --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0 Content-Type: multipart/mixed; boundary=changeset_6c67825c-8938-4f11-af6b-a25861ee53cc @@ -126,135 +126,135 @@ public async Task CanHandleAbsoluteAndRelativeUrls() --changeset_6c67825c-8938-4f11-af6b-a25861ee53cc-- --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0-- "); - // string b = await content.ReadAsStringAsync(); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); - request.Content = content; - HttpResponseMessage response = await client.SendAsync(request); + // string b = await content.ReadAsStringAsync(); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); + request.Content = content; + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - //string a = await response.Content.ReadAsStringAsync(); + //string a = await response.Content.ReadAsStringAsync(); - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); - int subResponseCount = 0; - using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); + int subResponseCount = 0; + using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + { + var batchReader = messageReader.CreateODataBatchReader(); + while (batchReader.Read()) { - var batchReader = messageReader.CreateODataBatchReader(); - while (batchReader.Read()) + switch (batchReader.State) { - switch (batchReader.State) - { - case ODataBatchReaderState.Operation: - var operationMessage = batchReader.CreateOperationResponseMessage(); - subResponseCount++; - Assert.Equal(201, operationMessage.StatusCode); - break; - } + case ODataBatchReaderState.Operation: + var operationMessage = batchReader.CreateOperationResponseMessage(); + subResponseCount++; + Assert.Equal(201, operationMessage.StatusCode); + break; } } - - Assert.Equal(3, subResponseCount); } - [Fact] - public async Task CanHandleAutomicityGroupRequestsAndUngroupedRequest_JsonBatch() - { - // Arrange - HttpClient client = CreateClient(); + Assert.Equal(3, subResponseCount); + } - string requestUri = "UnbufferedBatch/$batch"; - string absoluteUri = "http://localhost/UnbufferedBatch/DefaultBatchCustomers"; + [Fact] + public async Task CanHandleAutomicityGroupRequestsAndUngroupedRequest_JsonBatch() + { + // Arrange + HttpClient client = CreateClient(); - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - HttpContent content = new StringContent(@" - { - ""requests"": [{ - ""id"": ""1"", - ""atomicityGroup"": ""f7de7314-2f3d-4422-b840-ada6d6de0f18"", - ""method"": ""POST"", - ""url"": """ + absoluteUri + @""", - ""headers"": { - ""OData-Version"": ""4.0"", - ""Content-Type"": ""application/json;odata.metadata=minimal"", - ""Accept"": ""application/json;odata.metadata=minimal"" - }, - ""body"": { - ""Id"":11, - ""Name"":""CreatedByJsonBatch_11"" - } - }, { - ""id"": ""2"", - ""atomicityGroup"": ""f7de7314-2f3d-4422-b840-ada6d6de0f18"", - ""method"": ""POST"", - ""url"": """ + absoluteUri + @""", - ""headers"": { - ""OData-Version"": ""4.0"", - ""Content-Type"": ""application/json;odata.metadata=minimal"", - ""Accept"": ""application/json;odata.metadata=minimal"" - }, - ""body"": { - ""Id"":12, - ""Name"":""CreatedByJsonBatch_12"" - } - }, { - ""id"": ""3"", - ""method"": ""POST"", - ""url"": """ + absoluteUri + @""", - ""headers"": { - ""OData-Version"": ""4.0"", - ""Content-Type"": ""application/json;odata.metadata=minimal"", - ""Accept"": ""application/json;odata.metadata=minimal"" - }, - ""body"": { - ""Id"":13, - ""Name"":""CreatedByJsonBatch_3"" - } + string requestUri = "UnbufferedBatch/$batch"; + string absoluteUri = "http://localhost/UnbufferedBatch/DefaultBatchCustomers"; + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + HttpContent content = new StringContent(@" + { + ""requests"": [{ + ""id"": ""1"", + ""atomicityGroup"": ""f7de7314-2f3d-4422-b840-ada6d6de0f18"", + ""method"": ""POST"", + ""url"": """ + absoluteUri + @""", + ""headers"": { + ""OData-Version"": ""4.0"", + ""Content-Type"": ""application/json;odata.metadata=minimal"", + ""Accept"": ""application/json;odata.metadata=minimal"" + }, + ""body"": { + ""Id"":11, + ""Name"":""CreatedByJsonBatch_11"" } - ] - }"); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content = content; - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); - int subResponseCount = 0; - using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + }, { + ""id"": ""2"", + ""atomicityGroup"": ""f7de7314-2f3d-4422-b840-ada6d6de0f18"", + ""method"": ""POST"", + ""url"": """ + absoluteUri + @""", + ""headers"": { + ""OData-Version"": ""4.0"", + ""Content-Type"": ""application/json;odata.metadata=minimal"", + ""Accept"": ""application/json;odata.metadata=minimal"" + }, + ""body"": { + ""Id"":12, + ""Name"":""CreatedByJsonBatch_12"" + } + }, { + ""id"": ""3"", + ""method"": ""POST"", + ""url"": """ + absoluteUri + @""", + ""headers"": { + ""OData-Version"": ""4.0"", + ""Content-Type"": ""application/json;odata.metadata=minimal"", + ""Accept"": ""application/json;odata.metadata=minimal"" + }, + ""body"": { + ""Id"":13, + ""Name"":""CreatedByJsonBatch_3"" + } + } + ] + }"); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content = content; + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); + int subResponseCount = 0; + using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + { + var batchReader = messageReader.CreateODataBatchReader(); + while (batchReader.Read()) { - var batchReader = messageReader.CreateODataBatchReader(); - while (batchReader.Read()) + switch (batchReader.State) { - switch (batchReader.State) - { - case ODataBatchReaderState.Operation: - var operationMessage = batchReader.CreateOperationResponseMessage(); - subResponseCount++; - Assert.Equal(201, operationMessage.StatusCode); - break; - } + case ODataBatchReaderState.Operation: + var operationMessage = batchReader.CreateOperationResponseMessage(); + subResponseCount++; + Assert.Equal(201, operationMessage.StatusCode); + break; } } - - Assert.Equal(3, subResponseCount); } - [Fact] - public async Task CanNotContinueOnErrorWhenHeaderNotSet() - { - // Arrange - HttpClient client = CreateClient(); - var requestUri = "UnbufferedBatch/$batch"; - string absoluteUri = "http://localhost/UnbufferedBatch/DefaultBatchCustomers"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); - HttpContent content = new StringContent( + Assert.Equal(3, subResponseCount); + } + + [Fact] + public async Task CanNotContinueOnErrorWhenHeaderNotSet() + { + // Arrange + HttpClient client = CreateClient(); + var requestUri = "UnbufferedBatch/$batch"; + string absoluteUri = "http://localhost/UnbufferedBatch/DefaultBatchCustomers"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); + HttpContent content = new StringContent( @"--batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0 Content-Type: application/http Content-Transfer-Encoding: binary @@ -275,52 +275,52 @@ public async Task CanNotContinueOnErrorWhenHeaderNotSet() --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0-- "); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); - request.Content = content; + content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); + request.Content = content; - // Act - HttpResponseMessage response = await client.SendAsync(request); - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); - int subResponseCount = 0; + // Act + HttpResponseMessage response = await client.SendAsync(request); + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); + int subResponseCount = 0; - // Assert - using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + // Assert + using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + { + var batchReader = messageReader.CreateODataBatchReader(); + while (batchReader.Read()) { - var batchReader = messageReader.CreateODataBatchReader(); - while (batchReader.Read()) + switch (batchReader.State) { - switch (batchReader.State) - { - case ODataBatchReaderState.Operation: - var operationMessage = batchReader.CreateOperationResponseMessage(); - subResponseCount++; - if (subResponseCount == 2) - { - Assert.Equal(400, operationMessage.StatusCode); - } - else - { - Assert.Equal(200, operationMessage.StatusCode); - } - break; - } + case ODataBatchReaderState.Operation: + var operationMessage = batchReader.CreateOperationResponseMessage(); + subResponseCount++; + if (subResponseCount == 2) + { + Assert.Equal(400, operationMessage.StatusCode); + } + else + { + Assert.Equal(200, operationMessage.StatusCode); + } + break; } } - Assert.Equal(2, subResponseCount); } + Assert.Equal(2, subResponseCount); + } - [Fact] - public async Task CanContinueOnErrorWhenHeaderSet() - { - // Arrange - HttpClient client = CreateClient(); - var requestUri = "UnbufferedBatch/$batch"; - string absoluteUri = "http://localhost/UnbufferedBatch/DefaultBatchCustomers"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); - request.Headers.Add("prefer", "odata.continue-on-error"); - HttpContent content = new StringContent( + [Fact] + public async Task CanContinueOnErrorWhenHeaderSet() + { + // Arrange + HttpClient client = CreateClient(); + var requestUri = "UnbufferedBatch/$batch"; + string absoluteUri = "http://localhost/UnbufferedBatch/DefaultBatchCustomers"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); + request.Headers.Add("prefer", "odata.continue-on-error"); + HttpContent content = new StringContent( @"--batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0 Content-Type: application/http Content-Transfer-Encoding: binary @@ -341,55 +341,55 @@ public async Task CanContinueOnErrorWhenHeaderSet() --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0-- "); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); - request.Content = content; + content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); + request.Content = content; - // Act - HttpResponseMessage response = await client.SendAsync(request); - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); - int subResponseCount = 0; + // Act + HttpResponseMessage response = await client.SendAsync(request); + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); + int subResponseCount = 0; - // Assert - using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + // Assert + using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + { + var batchReader = messageReader.CreateODataBatchReader(); + while (batchReader.Read()) { - var batchReader = messageReader.CreateODataBatchReader(); - while (batchReader.Read()) + switch (batchReader.State) { - switch (batchReader.State) - { - case ODataBatchReaderState.Operation: - var operationMessage = batchReader.CreateOperationResponseMessage(); - subResponseCount++; - if (subResponseCount == 2) - { - Assert.Equal(400, operationMessage.StatusCode); - } - else - { - Assert.Equal(200, operationMessage.StatusCode); - } - break; - } + case ODataBatchReaderState.Operation: + var operationMessage = batchReader.CreateOperationResponseMessage(); + subResponseCount++; + if (subResponseCount == 2) + { + Assert.Equal(400, operationMessage.StatusCode); + } + else + { + Assert.Equal(200, operationMessage.StatusCode); + } + break; } } - Assert.Equal(3, subResponseCount); } + Assert.Equal(3, subResponseCount); + } - [Fact] - public async Task CanHandleContentIDInRelativeUrl() - { - // Arrange - HttpClient client = CreateClient(); - string requestUri = "UnbufferedBatch/$batch"; + [Fact] + public async Task CanHandleContentIDInRelativeUrl() + { + // Arrange + HttpClient client = CreateClient(); + string requestUri = "UnbufferedBatch/$batch"; - string defaultBatchCustomersAbsoluteUri = "http://localhost/UnbufferedBatch/DefaultBatchCustomers"; - string defaultBatchOrdersAbsoluteUri = "http://localhost/UnbufferedBatch/DefaultBatchOrders"; + string defaultBatchCustomersAbsoluteUri = "http://localhost/UnbufferedBatch/DefaultBatchCustomers"; + string defaultBatchOrdersAbsoluteUri = "http://localhost/UnbufferedBatch/DefaultBatchOrders"; - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); - HttpContent content = new StringContent(@" + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); + HttpContent content = new StringContent(@" --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0 Content-Type: multipart/mixed; boundary=changeset_6c67825c-8938-4f11-af6b-a25861ee53cc @@ -435,128 +435,127 @@ public async Task CanHandleContentIDInRelativeUrl() --changeset_6c67825c-8938-4f11-af6b-a25861ee53cc-- --batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0-- "); - // string b = await content.ReadAsStringAsync(); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); - request.Content = content; - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); - int subResponseCount = 0; - using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) - { - var batchReader = messageReader.CreateODataBatchReader(); - var subResponseStatusCodes = new int[] { 201, 201, 204 /*CreateRef*/ }; + // string b = await content.ReadAsStringAsync(); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/mixed; boundary=batch_abbe2e6f-e45b-4458-9555-5fc70e3aebe0"); + request.Content = content; + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); + int subResponseCount = 0; + using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + { + var batchReader = messageReader.CreateODataBatchReader(); + var subResponseStatusCodes = new int[] { 201, 201, 204 /*CreateRef*/ }; - while (batchReader.Read()) + while (batchReader.Read()) + { + switch (batchReader.State) { - switch (batchReader.State) - { - case ODataBatchReaderState.Operation: - var operationMessage = batchReader.CreateOperationResponseMessage(); - var subResponseStatusCode = subResponseCount < subResponseStatusCodes.Length ? subResponseStatusCodes[subResponseCount] : 201; - - subResponseCount++; - Assert.Equal(subResponseStatusCode, operationMessage.StatusCode); - break; - } + case ODataBatchReaderState.Operation: + var operationMessage = batchReader.CreateOperationResponseMessage(); + var subResponseStatusCode = subResponseCount < subResponseStatusCodes.Length ? subResponseStatusCodes[subResponseCount] : 201; + + subResponseCount++; + Assert.Equal(subResponseStatusCode, operationMessage.StatusCode); + break; } } - - Assert.Equal(3, subResponseCount); } - [Fact] - public async Task CanHandleReferencingOfRequestsNotSharingAutomicityGroup() - { - // Arrange - var client = CreateClient(); + Assert.Equal(3, subResponseCount); + } - var requestUri = "UnbufferedBatch/$batch"; - var defaultBatchCustomersRelativeUri = "DefaultBatchCustomers"; - var defaultBatchOrdersRelativeUri = "DefaultBatchOrders"; + [Fact] + public async Task CanHandleReferencingOfRequestsNotSharingAutomicityGroup() + { + // Arrange + var client = CreateClient(); - // Act - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - HttpContent content = new StringContent(@" - { - ""requests"": [{ - ""id"": ""1"", - ""method"": ""POST"", - ""url"": """ + defaultBatchCustomersRelativeUri + @""", - ""headers"": { - ""OData-Version"": ""4.0"", - ""Content-Type"": ""application/json;odata.metadata=minimal"", - ""Accept"": ""application/json;odata.metadata=minimal"" - }, - ""body"": { - ""Id"":7, - ""Name"":""Customer 7"" - } - }, { - ""id"": ""2"", - ""dependsOn"": [""1""], - ""method"": ""POST"", - ""url"": """ + defaultBatchOrdersRelativeUri + @""", - ""headers"": { - ""OData-Version"": ""4.0"", - ""Content-Type"": ""application/json;odata.metadata=minimal"", - ""Accept"": ""application/json;odata.metadata=minimal"" - }, - ""body"": { - ""Id"":7 - } - }, { - ""id"": ""3"", - ""dependsOn"": [""1"", ""2""], - ""method"": ""POST"", - ""url"": ""$1/Orders/$ref"", - ""headers"": { - ""OData-Version"": ""4.0"", - ""Content-Type"": ""application/json;odata.metadata=minimal"", - ""Accept"": ""application/json;odata.metadata=minimal"" - }, - ""body"": { - ""@odata.id"":""$2"" - } + var requestUri = "UnbufferedBatch/$batch"; + var defaultBatchCustomersRelativeUri = "DefaultBatchCustomers"; + var defaultBatchOrdersRelativeUri = "DefaultBatchOrders"; + + // Act + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + HttpContent content = new StringContent(@" + { + ""requests"": [{ + ""id"": ""1"", + ""method"": ""POST"", + ""url"": """ + defaultBatchCustomersRelativeUri + @""", + ""headers"": { + ""OData-Version"": ""4.0"", + ""Content-Type"": ""application/json;odata.metadata=minimal"", + ""Accept"": ""application/json;odata.metadata=minimal"" + }, + ""body"": { + ""Id"":7, + ""Name"":""Customer 7"" } - ] - }"); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content = content; - var response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); - int subResponseCount = 0; - using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) - { - var batchReader = messageReader.CreateODataBatchReader(); - var subResponseStatusCodes = new int[] { 201, 201, 204 /*CreateRef*/ }; + }, { + ""id"": ""2"", + ""dependsOn"": [""1""], + ""method"": ""POST"", + ""url"": """ + defaultBatchOrdersRelativeUri + @""", + ""headers"": { + ""OData-Version"": ""4.0"", + ""Content-Type"": ""application/json;odata.metadata=minimal"", + ""Accept"": ""application/json;odata.metadata=minimal"" + }, + ""body"": { + ""Id"":7 + } + }, { + ""id"": ""3"", + ""dependsOn"": [""1"", ""2""], + ""method"": ""POST"", + ""url"": ""$1/Orders/$ref"", + ""headers"": { + ""OData-Version"": ""4.0"", + ""Content-Type"": ""application/json;odata.metadata=minimal"", + ""Accept"": ""application/json;odata.metadata=minimal"" + }, + ""body"": { + ""@odata.id"":""$2"" + } + } + ] + }"); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content = content; + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage odataResponseMessage = new ODataMessageWrapper(stream, response.Content.Headers); + int subResponseCount = 0; + using (var messageReader = new ODataMessageReader(odataResponseMessage, new ODataMessageReaderSettings(), edmModel)) + { + var batchReader = messageReader.CreateODataBatchReader(); + var subResponseStatusCodes = new int[] { 201, 201, 204 /*CreateRef*/ }; - while (batchReader.Read()) + while (batchReader.Read()) + { + switch (batchReader.State) { - switch (batchReader.State) - { - case ODataBatchReaderState.Operation: - var operationMessage = batchReader.CreateOperationResponseMessage(); - var subResponseStatusCode = subResponseCount < subResponseStatusCodes.Length ? subResponseStatusCodes[subResponseCount] : 201; + case ODataBatchReaderState.Operation: + var operationMessage = batchReader.CreateOperationResponseMessage(); + var subResponseStatusCode = subResponseCount < subResponseStatusCodes.Length ? subResponseStatusCodes[subResponseCount] : 201; - subResponseCount++; - Assert.Equal(subResponseStatusCode, operationMessage.StatusCode); - break; - } + subResponseCount++; + Assert.Equal(subResponseStatusCode, operationMessage.StatusCode); + break; } } - - Assert.Equal(3, subResponseCount); } + + Assert.Equal(3, subResponseCount); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationController.cs index 66f7d1343..38b670a73 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationController.cs @@ -16,487 +16,486 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.BoundOperation +namespace Microsoft.AspNetCore.OData.E2E.Tests.BoundOperation; + +[Route("AttributeRouting")] +public class EmployeesController : ODataController { - [Route("AttributeRouting")] - public class EmployeesController : ODataController - { - private static List _employees = null; - private static List _managers = null; + private static List _employees = null; + private static List _managers = null; - private static void InitEmployeesAndManagers() + private static void InitEmployeesAndManagers() + { + _managers = Enumerable.Range(1, 5).Select(i => new Manager { - _managers = Enumerable.Range(1, 5).Select(i => new Manager + ID = i, + Name = "Name" + i + "/", + Address = new Address() { - ID = i, - Name = "Name" + i + "/", - Address = new Address() - { - Street = "Street" + i, - City = "City" + i, - }, - Emails = Enumerable.Range(1, i).Select(j => string.Format("Name{0}_{1}@microsoft.com", i, j)).ToList(), - Salary = i * 10, - Heads = i, - }).ToList(); - - _employees = Enumerable.Range(6, 5).Select(i => - new Employee + Street = "Street" + i, + City = "City" + i, + }, + Emails = Enumerable.Range(1, i).Select(j => string.Format("Name{0}_{1}@microsoft.com", i, j)).ToList(), + Salary = i * 10, + Heads = i, + }).ToList(); + + _employees = Enumerable.Range(6, 5).Select(i => + new Employee + { + ID = i, + Name = "Name" + i + "?", + Address = new Address() { - ID = i, - Name = "Name" + i + "?", - Address = new Address() - { - Street = "Street" + i, - City = "City" + i, - }, - Emails = Enumerable.Range(1, i).Select(j => string.Format("Name{0}_{1}@microsoft.com", i, j)).ToList(), - Salary = i * 10, - }).ToList(); - - int k = 20; - _employees.Add( - new Employee + Street = "Street" + i, + City = "City" + i, + }, + Emails = Enumerable.Range(1, i).Select(j => string.Format("Name{0}_{1}@microsoft.com", i, j)).ToList(), + Salary = i * 10, + }).ToList(); + + int k = 20; + _employees.Add( + new Employee + { + ID = k, + Name = "Name" + k + "#", + Address = new Address() { - ID = k, - Name = "Name" + k + "#", - Address = new Address() - { - Street = "Street" + k, - City = "City" + k, - }, - Emails = new List(), - Salary = k * 10, - }); - - - _employees.AddRange(_managers); - } + Street = "Street" + k, + City = "City" + k, + }, + Emails = new List(), + Salary = k * 10, + }); - static EmployeesController() - { - InitEmployeesAndManagers(); - } - public IList Customers { get { return _employees; } } - - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - public IActionResult Get() - { - return Ok(_employees.AsQueryable()); - } + _employees.AddRange(_managers); + } - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - public IActionResult GetFromManager() - { - return Ok(_managers.AsQueryable()); - } + static EmployeesController() + { + InitEmployeesAndManagers(); + } - // ~/Employees/Namespace.GetCount() - [HttpGet] - public int GetCount() - { - return _employees.Count(); - } + public IList Customers { get { return _employees; } } - // ~/Employees/Namespace.GetCount() - [HttpGet("Employees/Default.GetCount()")] - public int GetCountAttributeRouting() - { - return this.GetCount() * 2;// multiplied by 2 is to make it easy to verify if it is returned by attribute routing function. - } + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public IActionResult Get() + { + return Ok(_employees.AsQueryable()); + } - /* - // ~/Employees/Namespace.GetCount(Name='Name1') - [HttpGet] - public int GetCount(string name) - { - name = name.Replace("%2F", "/"); - return _employees.Where(e => e.Name.Contains(name)).Count(); - } + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public IActionResult GetFromManager() + { + return Ok(_managers.AsQueryable()); + } - // ~/Employees/Namespace.GetCount(Name='Name1') - [HttpGet("Employees/Default.GetCount(Name={name})")] - public int GetCountAttributeRouting([FromODataUri]string name) - { - return this.GetCount(name) * 2; - } + // ~/Employees/Namespace.GetCount() + [HttpGet] + public int GetCount() + { + return _employees.Count(); + } - // ~/Employees/Namespace.Manager/Namespace.GetCount() - [HttpGet] - public int GetCountOnCollectionOfManager() - { - return _managers.Count(); - } + // ~/Employees/Namespace.GetCount() + [HttpGet("Employees/Default.GetCount()")] + public int GetCountAttributeRouting() + { + return this.GetCount() * 2;// multiplied by 2 is to make it easy to verify if it is returned by attribute routing function. + } - // ~/Employees/Namesapce.Manager/Namespace.GetCount() - [HttpGet("Employees/NS.Manager/Default.GetCount()")] - public int GetCountOnCollectionOfManagerAttributeRouting() - { - return this.GetCountOnCollectionOfManager() * 2; - } + /* + // ~/Employees/Namespace.GetCount(Name='Name1') + [HttpGet] + public int GetCount(string name) + { + name = name.Replace("%2F", "/"); + return _employees.Where(e => e.Name.Contains(name)).Count(); + } - // ~/Employees(1)/OptionalAddresses - // ~/Employees(1)/OptionalAddresses/$count - // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetOptionalAddresses()")] - // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetOptionalAddresses()/$count")] - [HttpGet] - [EnableQuery] - public IActionResult GetOptionalAddresses(int key) - { - IList
addresses = new List
(); - addresses.Add(new Address { City = "Shanghai", Street = "Zixing" }); - return Ok(addresses); - } + // ~/Employees/Namespace.GetCount(Name='Name1') + [HttpGet("Employees/Default.GetCount(Name={name})")] + public int GetCountAttributeRouting([FromODataUri]string name) + { + return this.GetCount(name) * 2; + } - // ~/Employees(1)/OptionalAddresses - // ~/Employees(1)/OptionalAddresses/$count - // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetOptionalAddresses()")] - // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetOptionalAddresses()/$count")] - [EnableQuery] - [HttpGet("Employees({key})/OptionalAddresses")] - [HttpGet("Employees({key})/OptionalAddresses/$count")] - [HttpGet("Employees({key})/Default.GetOptionalAddresses()")] - [HttpGet("Employees({key})/Default.GetOptionalAddresses()/$count")] - public IActionResult GetOptionalAddressesAttributeRouting(int key) - { - IList
addresses = new List
(); - addresses.Add(new Address { City = "Shanghai", Street = "Zixing" }); - addresses.Add(new Address { City = "Beijing", Street = "Zhongshan" }); - return Ok(addresses); - } + // ~/Employees/Namespace.Manager/Namespace.GetCount() + [HttpGet] + public int GetCountOnCollectionOfManager() + { + return _managers.Count(); + } - // ~/Employees(1)/Emails - // ~/Employees(1)/Emails/$count - // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetEmails() - // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetEmails()/$count - [HttpGet] - [EnableQuery] - public IActionResult GetEmails(int key) - { - IList emails = new List(); - emails.Add("a@a.com"); - return Ok(emails); - } + // ~/Employees/Namesapce.Manager/Namespace.GetCount() + [HttpGet("Employees/NS.Manager/Default.GetCount()")] + public int GetCountOnCollectionOfManagerAttributeRouting() + { + return this.GetCountOnCollectionOfManager() * 2; + } - // ~/Employees(1)/Emails - // ~/Employees(1)/Emails/$count - // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetEmails() - // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetEmails()/$count - [EnableQuery] - [HttpGet("Employees({key})/Emails")] - [HttpGet("Employees({key})/Emails/$count")] - [HttpGet("Employees({key})/Default.GetEmails()")] - public IActionResult GetEmailsAttributeRouting([FromODataUri]int key) - { - IList emails = new List(); - emails.Add("a@a.com"); - emails.Add("b@b.com"); - return Ok(emails); - } + // ~/Employees(1)/OptionalAddresses + // ~/Employees(1)/OptionalAddresses/$count + // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetOptionalAddresses()")] + // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetOptionalAddresses()/$count")] + [HttpGet] + [EnableQuery] + public IActionResult GetOptionalAddresses(int key) + { + IList
addresses = new List
(); + addresses.Add(new Address { City = "Shanghai", Street = "Zixing" }); + return Ok(addresses); + } - // ~/Employees(1)/Namesapce.GetEmailsCount() - [HttpGet] - public int GetEmailsCount(int key) - { - return _employees.Where(e => e.ID == key).First().Emails.Count; - } + // ~/Employees(1)/OptionalAddresses + // ~/Employees(1)/OptionalAddresses/$count + // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetOptionalAddresses()")] + // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetOptionalAddresses()/$count")] + [EnableQuery] + [HttpGet("Employees({key})/OptionalAddresses")] + [HttpGet("Employees({key})/OptionalAddresses/$count")] + [HttpGet("Employees({key})/Default.GetOptionalAddresses()")] + [HttpGet("Employees({key})/Default.GetOptionalAddresses()/$count")] + public IActionResult GetOptionalAddressesAttributeRouting(int key) + { + IList
addresses = new List
(); + addresses.Add(new Address { City = "Shanghai", Street = "Zixing" }); + addresses.Add(new Address { City = "Beijing", Street = "Zhongshan" }); + return Ok(addresses); + } - // ~/Employees(1)/Namesapce.GetEmailsCount() - [HttpGet("Employees({key})/Default.GetEmailsCount()")] - public int GetEmailsCountAttributeRouting([FromODataUri]int key) - { - return this.GetEmailsCount(key) * 2; - } + // ~/Employees(1)/Emails + // ~/Employees(1)/Emails/$count + // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetEmails() + // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetEmails()/$count + [HttpGet] + [EnableQuery] + public IActionResult GetEmails(int key) + { + IList emails = new List(); + emails.Add("a@a.com"); + return Ok(emails); + } - // ~/Employees(1)/Namespace.Manager/Namespace.GetEmailsCount() - [HttpGet] - public int GetEmailsCountOnManager(int key) - { - return _managers.Where(e => e.ID == key).First().Emails.Count; - } + // ~/Employees(1)/Emails + // ~/Employees(1)/Emails/$count + // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetEmails() + // ~/Employees(1)/Microsoft.Test.E2E.AspNet.OData.BoundOperation.GetEmails()/$count + [EnableQuery] + [HttpGet("Employees({key})/Emails")] + [HttpGet("Employees({key})/Emails/$count")] + [HttpGet("Employees({key})/Default.GetEmails()")] + public IActionResult GetEmailsAttributeRouting([FromODataUri]int key) + { + IList emails = new List(); + emails.Add("a@a.com"); + emails.Add("b@b.com"); + return Ok(emails); + } - // ~/Employees(1)/Namespace.Manager/Namesapce.GetEmailsCount() - [HttpGet("Employees({key})/NS.Manager/Default.GetEmailsCount()")] - public int GetEmailsCountOnManagerAttributeRouting(int key) - { - return this.GetEmailsCountOnManager(key) * 2; - } + // ~/Employees(1)/Namesapce.GetEmailsCount() + [HttpGet] + public int GetEmailsCount(int key) + { + return _employees.Where(e => e.ID == key).First().Emails.Count; + } - // using [FromODataUri] or not is non-sense for primitive. except for string type. - [HttpGet("Employees/Default.PrimitiveFunction(param={param},price={price},name={name},names={names})")] - public string PrimitiveFunction(int param, double? price, [FromODataUri]string name, [FromODataUri]IEnumerable names) - { - StringBuilder sb = new StringBuilder("(param=" + param); - sb.Append(",price=").Append(price == null ? "null" : price.ToString()); - sb.Append(",name=").Append(name == null ? "null" : "'" + name + "'"); - - Assert.NotNull(names); - sb.Append(",names=[").Append(String.Join(",", names.Select(n => n == null ? "null" : "'" + n + "'"))); - sb.Append("])"); - return sb.ToString(); - } + // ~/Employees(1)/Namesapce.GetEmailsCount() + [HttpGet("Employees({key})/Default.GetEmailsCount()")] + public int GetEmailsCountAttributeRouting([FromODataUri]int key) + { + return this.GetEmailsCount(key) * 2; + } - // using [FromODataUri] or not is non-sense for primitive. except for string type. - [HttpGet("Employees/Default.EnumFunction(bkColor={bkColor},frColor={frColor},colors={colors})")] - public string EnumFunction([FromODataUri]Color bkColor, [FromODataUri]Color? frColor, [FromODataUri]IEnumerable colors) - { - StringBuilder sb = new StringBuilder("(bkColor='" + bkColor); - sb.Append("',frColor=").Append(frColor == null ? "null" : "'" + frColor.ToString() + "'"); + // ~/Employees(1)/Namespace.Manager/Namespace.GetEmailsCount() + [HttpGet] + public int GetEmailsCountOnManager(int key) + { + return _managers.Where(e => e.ID == key).First().Emails.Count; + } - Assert.NotNull(colors); - sb.Append(",colors=[").Append(String.Join(",", colors.Select(c => "'" + c + "'"))); - sb.Append("])"); - return sb.ToString(); - } + // ~/Employees(1)/Namespace.Manager/Namesapce.GetEmailsCount() + [HttpGet("Employees({key})/NS.Manager/Default.GetEmailsCount()")] + public int GetEmailsCountOnManagerAttributeRouting(int key) + { + return this.GetEmailsCountOnManager(key) * 2; + } - [HttpGet("Employees/Default.ComplexFunction(address={address},location={location},addresses={addresses})")] - public IActionResult ComplexFunction([FromODataUri]Address address, [FromODataUri]Address location, [FromODataUri]IEnumerable
addresses) - { - return Ok(new[] { address, location }.Concat(addresses)); - } + // using [FromODataUri] or not is non-sense for primitive. except for string type. + [HttpGet("Employees/Default.PrimitiveFunction(param={param},price={price},name={name},names={names})")] + public string PrimitiveFunction(int param, double? price, [FromODataUri]string name, [FromODataUri]IEnumerable names) + { + StringBuilder sb = new StringBuilder("(param=" + param); + sb.Append(",price=").Append(price == null ? "null" : price.ToString()); + sb.Append(",name=").Append(name == null ? "null" : "'" + name + "'"); + + Assert.NotNull(names); + sb.Append(",names=[").Append(String.Join(",", names.Select(n => n == null ? "null" : "'" + n + "'"))); + sb.Append("])"); + return sb.ToString(); + } - [HttpGet("Employees/Default.EntityFunction(person={person},guard={guard},staff={staff})")] - public IActionResult EntityFunction([FromODataUri]Employee person, [FromODataUri]Employee guard, [FromODataUri]IEnumerable staff) - { - VerifyEmployee(person); - VerifyEmployee(guard); - foreach (var p in staff) - { - VerifyEmployee(p); - } + // using [FromODataUri] or not is non-sense for primitive. except for string type. + [HttpGet("Employees/Default.EnumFunction(bkColor={bkColor},frColor={frColor},colors={colors})")] + public string EnumFunction([FromODataUri]Color bkColor, [FromODataUri]Color? frColor, [FromODataUri]IEnumerable colors) + { + StringBuilder sb = new StringBuilder("(bkColor='" + bkColor); + sb.Append("',frColor=").Append(frColor == null ? "null" : "'" + frColor.ToString() + "'"); - return Ok(); - } + Assert.NotNull(colors); + sb.Append(",colors=[").Append(String.Join(",", colors.Select(c => "'" + c + "'"))); + sb.Append("])"); + return sb.ToString(); + } - [HttpGet("Employees/Default.GetWholeSalary(minSalary={min})")] - public string GetWholeSalaryWithMin(double min) - { - return GetWholeSalary(min); - } + [HttpGet("Employees/Default.ComplexFunction(address={address},location={location},addresses={addresses})")] + public IActionResult ComplexFunction([FromODataUri]Address address, [FromODataUri]Address location, [FromODataUri]IEnumerable
addresses) + { + return Ok(new[] { address, location }.Concat(addresses)); + } - [HttpGet("Employees/Default.GetWholeSalary(minSalary={min},maxSalary={max})")] - public string GetWholeSalaryWithMinAndMax(double min, double max) + [HttpGet("Employees/Default.EntityFunction(person={person},guard={guard},staff={staff})")] + public IActionResult EntityFunction([FromODataUri]Employee person, [FromODataUri]Employee guard, [FromODataUri]IEnumerable staff) + { + VerifyEmployee(person); + VerifyEmployee(guard); + foreach (var p in staff) { - return GetWholeSalary(min, max); + VerifyEmployee(p); } - [HttpGet("Employees/Default.GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary},aveSalary={aveSalary})")] - public string GetWholeSalary(double minSalary, double maxSalary = 0, double aveSalary = 8.9) - { - return String.Format(CultureInfo.InvariantCulture, "GetWholeSalary({0}, {1}, {2})", minSalary, maxSalary, aveSalary); - } + return Ok(); + } - private static void VerifyEmployee(Employee employee) - { - if (employee == null) - { - return; - } + [HttpGet("Employees/Default.GetWholeSalary(minSalary={min})")] + public string GetWholeSalaryWithMin(double min) + { + return GetWholeSalary(min); + } - // entity reference call - if (employee.ID == 8) - { - Assert.Null(employee.Name); - return; - } + [HttpGet("Employees/Default.GetWholeSalary(minSalary={min},maxSalary={max})")] + public string GetWholeSalaryWithMinAndMax(double min, double max) + { + return GetWholeSalary(min, max); + } - // entity call - var manager = employee as Manager; - if (manager != null) - { - Assert.Equal(901, manager.ID); - Assert.Equal("John", manager.Name); - Assert.Equal(9, manager.Heads); - } - else - { - Assert.Equal(801, employee.ID); - Assert.Equal("Mike", employee.Name); - } - } + [HttpGet("Employees/Default.GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary},aveSalary={aveSalary})")] + public string GetWholeSalary(double minSalary, double maxSalary = 0, double aveSalary = 8.9) + { + return String.Format(CultureInfo.InvariantCulture, "GetWholeSalary({0}, {1}, {2})", minSalary, maxSalary, aveSalary); + } - [HttpPost("ResetDataSource")] - public IActionResult ResetDataSource() + private static void VerifyEmployee(Employee employee) + { + if (employee == null) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - InitEmployeesAndManagers(); - return Ok(); + return; } - // ~/Employees/Namespace.IncreaseSalary - [EnableQuery] - [HttpPost] - public IActionResult IncreaseSalary([FromBody]ODataUntypedActionParameters odataActionParameters) + // entity reference call + if (employee.ID == 8) { - string name = odataActionParameters["Name"] as string; - IEnumerable candidates = _employees.Where(e => e.Name.StartsWith(name)); - candidates.Select(e => e.Salary = e.Salary * 2); - return Ok(candidates); + Assert.Null(employee.Name); + return; } - // ~/Employees/Namespace.IncreaseSalary - [EnableQuery] - [HttpPost("Employees/Default.IncreaseSalary")] - public IActionResult IncreaseSalaryAttributeRouting([FromBody]ODataUntypedActionParameters odataActionParameters) + // entity call + var manager = employee as Manager; + if (manager != null) { - string name = odataActionParameters["Name"] as string; - IEnumerable candidates = _employees.Where(e => e.Name.StartsWith(name)); - candidates.Select(e => e.Salary = e.Salary * 2); - return Ok(candidates.Where(e => e.ID <= 5)); + Assert.Equal(901, manager.ID); + Assert.Equal("John", manager.Name); + Assert.Equal(9, manager.Heads); } - - // ~/Employees/Namespace.Manager/Namespace.IncreaseSalary - [HttpPost] - public IActionResult IncreaseSalaryOnCollectionOfManager([FromBody]ODataUntypedActionParameters odataActionParameters) + else { - string name = odataActionParameters["Name"] as string; - IEnumerable candidates = _managers.Where(e => e.Name.StartsWith(name)); - candidates.Select(e => e.Salary = e.Salary * 2); - return Ok(candidates); + Assert.Equal(801, employee.ID); + Assert.Equal("Mike", employee.Name); } + } - // ~/Employees/Namespace.Manager/Namesapce.IncreaseSalary - [EnableQuery] - [HttpPost("Employees/NS.Manager/Default.IncreaseSalary")] - public IActionResult IncreaseSalaryOnCollectionOfManagerAttributeRouting([FromBody]ODataUntypedActionParameters odataActionParameters) + [HttpPost("ResetDataSource")] + public IActionResult ResetDataSource() + { + if (!ModelState.IsValid) { - string name = odataActionParameters["Name"] as string; - IEnumerable candidates = _managers.Where(e => e.Name.StartsWith(name)); - candidates.Select(e => e.Salary = e.Salary * 2); - return Ok(candidates.Where(m => m.ID % 2 == 0)); + return BadRequest(ModelState); } + InitEmployeesAndManagers(); + return Ok(); + } + // ~/Employees/Namespace.IncreaseSalary + [EnableQuery] + [HttpPost] + public IActionResult IncreaseSalary([FromBody]ODataUntypedActionParameters odataActionParameters) + { + string name = odataActionParameters["Name"] as string; + IEnumerable candidates = _employees.Where(e => e.Name.StartsWith(name)); + candidates.Select(e => e.Salary = e.Salary * 2); + return Ok(candidates); + } - // ~/Employees(1)/Namespace.IncreaseSalary - [HttpPost] - public IActionResult IncreaseSalaryOnEmployee([FromODataUri]int key) - { - var employee = _employees.Where(e => e.ID == key).First(); - employee.Salary *= 2; - return Ok(employee.Salary); - } + // ~/Employees/Namespace.IncreaseSalary + [EnableQuery] + [HttpPost("Employees/Default.IncreaseSalary")] + public IActionResult IncreaseSalaryAttributeRouting([FromBody]ODataUntypedActionParameters odataActionParameters) + { + string name = odataActionParameters["Name"] as string; + IEnumerable candidates = _employees.Where(e => e.Name.StartsWith(name)); + candidates.Select(e => e.Salary = e.Salary * 2); + return Ok(candidates.Where(e => e.ID <= 5)); + } - // ~/Employees(1)/Namespace.IncreaseSalary - [HttpPost("Employees({key})/Default.IncreaseSalary")] - public IActionResult IncreaseSalaryOnEmployeeAttributeRouting([FromODataUri]int key) - { - var employee = _employees.Where(e => e.ID == key).First(); - employee.Salary *= 4; - return Ok(employee.Salary); - } + // ~/Employees/Namespace.Manager/Namespace.IncreaseSalary + [HttpPost] + public IActionResult IncreaseSalaryOnCollectionOfManager([FromBody]ODataUntypedActionParameters odataActionParameters) + { + string name = odataActionParameters["Name"] as string; + IEnumerable candidates = _managers.Where(e => e.Name.StartsWith(name)); + candidates.Select(e => e.Salary = e.Salary * 2); + return Ok(candidates); + } - // ~/Employees(1)/Namespace.Manager/Namespace.IncreaseSalary - [HttpPost] - public IActionResult IncreaseSalaryOnManager([FromODataUri] int key) - { - var manager = _managers.Where(m => m.ID == key).First(); - manager.Salary *= 2; - return Ok(manager.Salary); - } + // ~/Employees/Namespace.Manager/Namesapce.IncreaseSalary + [EnableQuery] + [HttpPost("Employees/NS.Manager/Default.IncreaseSalary")] + public IActionResult IncreaseSalaryOnCollectionOfManagerAttributeRouting([FromBody]ODataUntypedActionParameters odataActionParameters) + { + string name = odataActionParameters["Name"] as string; + IEnumerable candidates = _managers.Where(e => e.Name.StartsWith(name)); + candidates.Select(e => e.Salary = e.Salary * 2); + return Ok(candidates.Where(m => m.ID % 2 == 0)); + } - // ~/Employees(1)/Namespace.Manager/Namespace.IncreaseSalary - [HttpPost("Employees({key})/NS.Manager/Default.IncreaseSalary")] - public IActionResult IncreaseSalaryOnManagerAttributeRouting([FromODataUri] int key) - { - var manager = _managers.Where(m => m.ID == key).First(); - manager.Salary *= 4; - return Ok(manager.Salary); - } - [HttpPost("Employees/Default.PrimitiveAction")] - public IActionResult PrimitiveAction([FromBody]ODataActionParameters parameters) - { - Assert.Equal(4, parameters.Count); - Assert.Equal(7, parameters["param"]); - Assert.Equal(9.9, parameters["price"]); - Assert.Equal("Tony", parameters["name"]); + // ~/Employees(1)/Namespace.IncreaseSalary + [HttpPost] + public IActionResult IncreaseSalaryOnEmployee([FromODataUri]int key) + { + var employee = _employees.Where(e => e.ID == key).First(); + employee.Salary *= 2; + return Ok(employee.Salary); + } - Assert.NotNull(parameters["names"]); - IList names = (parameters["names"] as IEnumerable).ToList(); - Assert.NotNull(names); + // ~/Employees(1)/Namespace.IncreaseSalary + [HttpPost("Employees({key})/Default.IncreaseSalary")] + public IActionResult IncreaseSalaryOnEmployeeAttributeRouting([FromODataUri]int key) + { + var employee = _employees.Where(e => e.ID == key).First(); + employee.Salary *= 4; + return Ok(employee.Salary); + } - Assert.Equal("Mike", names[0]); - Assert.Null(names[1]); - Assert.Equal("John", names[2]); + // ~/Employees(1)/Namespace.Manager/Namespace.IncreaseSalary + [HttpPost] + public IActionResult IncreaseSalaryOnManager([FromODataUri] int key) + { + var manager = _managers.Where(m => m.ID == key).First(); + manager.Salary *= 2; + return Ok(manager.Salary); + } - return Ok(true); - } + // ~/Employees(1)/Namespace.Manager/Namespace.IncreaseSalary + [HttpPost("Employees({key})/NS.Manager/Default.IncreaseSalary")] + public IActionResult IncreaseSalaryOnManagerAttributeRouting([FromODataUri] int key) + { + var manager = _managers.Where(m => m.ID == key).First(); + manager.Salary *= 4; + return Ok(manager.Salary); + } - [HttpPost("Employees/Default.EnumAction")] - public IActionResult EnumAction([FromBody]ODataActionParameters parameters) - { - Assert.Equal(3, parameters.Count); - Assert.Equal(Color.Red, parameters["bkColor"]); - Assert.Equal(Color.Green, parameters["frColor"]); + [HttpPost("Employees/Default.PrimitiveAction")] + public IActionResult PrimitiveAction([FromBody]ODataActionParameters parameters) + { + Assert.Equal(4, parameters.Count); + Assert.Equal(7, parameters["param"]); + Assert.Equal(9.9, parameters["price"]); + Assert.Equal("Tony", parameters["name"]); - Assert.NotNull(parameters["colors"]); - IList colors = (parameters["colors"] as IEnumerable).ToList(); - Assert.NotNull(colors); + Assert.NotNull(parameters["names"]); + IList names = (parameters["names"] as IEnumerable).ToList(); + Assert.NotNull(names); - Assert.Equal(Color.Red, colors[0]); - Assert.Equal(Color.Blue, colors[1]); + Assert.Equal("Mike", names[0]); + Assert.Null(names[1]); + Assert.Equal("John", names[2]); - return Ok(true); - } + return Ok(true); + } - [HttpPost("Employees/Default.ComplexAction")] - public IActionResult ComplexAction([FromBody]ODataActionParameters parameters) - { - Assert.Equal(3, parameters.Count); + [HttpPost("Employees/Default.EnumAction")] + public IActionResult EnumAction([FromBody]ODataActionParameters parameters) + { + Assert.Equal(3, parameters.Count); + Assert.Equal(Color.Red, parameters["bkColor"]); + Assert.Equal(Color.Green, parameters["frColor"]); - Assert.NotNull(parameters["addresses"]); - IList
addresses = (parameters["addresses"] as IEnumerable
).ToList(); - Assert.NotNull(addresses); + Assert.NotNull(parameters["colors"]); + IList colors = (parameters["colors"] as IEnumerable).ToList(); + Assert.NotNull(colors); - foreach (Address address in new [] { parameters["address"], addresses[0]}) - { - Assert.NotNull(address); - Assert.Equal("NE 24th St.", address.Street); - Assert.Equal("Redmond", address.City); - } + Assert.Equal(Color.Red, colors[0]); + Assert.Equal(Color.Blue, colors[1]); - foreach (SubAddress location in new[] { parameters["location"], addresses[1] }) - { - Assert.NotNull(location); - Assert.Equal("LianHua Rd.", location.Street); - Assert.Equal("Shanghai", location.City); - Assert.Equal(9.9, location.Code); - } + return Ok(true); + } + + [HttpPost("Employees/Default.ComplexAction")] + public IActionResult ComplexAction([FromBody]ODataActionParameters parameters) + { + Assert.Equal(3, parameters.Count); + + Assert.NotNull(parameters["addresses"]); + IList
addresses = (parameters["addresses"] as IEnumerable
).ToList(); + Assert.NotNull(addresses); - return Ok(true); + foreach (Address address in new [] { parameters["address"], addresses[0]}) + { + Assert.NotNull(address); + Assert.Equal("NE 24th St.", address.Street); + Assert.Equal("Redmond", address.City); } - [HttpPost("Employees/Default.EntityAction")] - public IActionResult EntityAction([FromBody]ODataActionParameters parameters) + foreach (SubAddress location in new[] { parameters["location"], addresses[1] }) { - Assert.Equal(3, parameters.Count); + Assert.NotNull(location); + Assert.Equal("LianHua Rd.", location.Street); + Assert.Equal("Shanghai", location.City); + Assert.Equal(9.9, location.Code); + } + + return Ok(true); + } - Assert.NotNull(parameters["staff"]); - IList staff = (parameters["staff"] as IEnumerable).ToList(); - Assert.NotNull(staff); + [HttpPost("Employees/Default.EntityAction")] + public IActionResult EntityAction([FromBody]ODataActionParameters parameters) + { + Assert.Equal(3, parameters.Count); - foreach (Employee person in new[] { parameters["person"], staff[0] }) - { - Assert.NotNull(person); - Assert.Equal(801, person.ID); - Assert.Equal("Mike", person.Name); - Assert.Null(person.Address); - } + Assert.NotNull(parameters["staff"]); + IList staff = (parameters["staff"] as IEnumerable).ToList(); + Assert.NotNull(staff); - foreach (Manager guard in new[] {parameters["guard"], staff[1]}) - { - Assert.NotNull(guard); - Assert.Equal(901, guard.ID); - Assert.Equal("John", guard.Name); - Assert.Equal(9, guard.Heads); - Assert.Null(guard.Address); - } + foreach (Employee person in new[] { parameters["person"], staff[0] }) + { + Assert.NotNull(person); + Assert.Equal(801, person.ID); + Assert.Equal("Mike", person.Name); + Assert.Null(person.Address); + } - return Ok(true); - }*/ - } + foreach (Manager guard in new[] {parameters["guard"], staff[1]}) + { + Assert.NotNull(guard); + Assert.Equal(901, guard.ID); + Assert.Equal("John", guard.Name); + Assert.Equal(9, guard.Heads); + Assert.Null(guard.Address); + } + + return Ok(true); + }*/ } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationDataModel.cs index 4c2c25090..fa0df5b84 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationDataModel.cs @@ -7,49 +7,48 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.BoundOperation +namespace Microsoft.AspNetCore.OData.E2E.Tests.BoundOperation; + +public class Employee { - public class Employee + public Employee() { - public Employee() - { - OptionalAddresses = new List
(); - } + OptionalAddresses = new List
(); + } - public int ID { get; set; } + public int ID { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public Address Address { get; set; } + public Address Address { get; set; } - public IList Emails { get; set; } + public IList Emails { get; set; } - public int Salary { get; set; } + public int Salary { get; set; } - public IList
OptionalAddresses { get; set; } - } + public IList
OptionalAddresses { get; set; } +} - public class Address - { - public string Street { get; set; } +public class Address +{ + public string Street { get; set; } - public string City { get; set; } - } + public string City { get; set; } +} - public class Manager : Employee - { - public int Heads { get; set; } - } +public class Manager : Employee +{ + public int Heads { get; set; } +} - public class SubAddress : Address - { - public double Code { get; set; } - } +public class SubAddress : Address +{ + public double Code { get; set; } +} - public enum Color - { - Red, - Blue, - Green - } +public enum Color +{ + Red, + Blue, + Green } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationEdmModel.cs index 9d140d7fb..6a606fef5 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationEdmModel.cs @@ -8,144 +8,143 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.BoundOperation +namespace Microsoft.AspNetCore.OData.E2E.Tests.BoundOperation; + +internal class UnBoundFunctionEdmModel { - internal class UnBoundFunctionEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - EntitySetConfiguration entitySetConfiguration = builder.EntitySet("Employees"); - EntityTypeConfiguration entityTypeConfigurationOfManager = builder.EntityType(); - EntityTypeConfiguration entityTypeConfigurationOfEmployee = builder.EntityType(); - - #region functions - - // Function bound to a collection of base EntityType. - entityTypeConfigurationOfEmployee.Collection.Function("GetCount") - .Returns(); - - // Overload - entityTypeConfigurationOfEmployee.Collection.Function("GetCount") - .Returns() - .Parameter("Name"); - - // Overload with one optional parameter - var salaryRangeCount = entityTypeConfigurationOfEmployee.Collection.Function("GetWholeSalary") - .Returns(); - salaryRangeCount.Parameter("minSalary"); - salaryRangeCount.Parameter("maxSalary").Optional(); - salaryRangeCount.Parameter("aveSalary").HasDefaultValue("8.9"); - - // Function bound to a collection of derived EntityType. - entityTypeConfigurationOfManager.Collection.Function("GetCount") - .Returns(); - - // Function bound to an base EntityType - entityTypeConfigurationOfEmployee.Function("GetEmailsCount") - .Returns(); - - entityTypeConfigurationOfEmployee.Function("GetOptionalAddresses") - .ReturnsCollection
() - .IsComposable = true; - - entityTypeConfigurationOfEmployee.Function("GetEmails") - .ReturnsCollection() - .IsComposable = false; - - // Function bound to a derived EntityType - entityTypeConfigurationOfManager.Function("GetEmailsCount") - .Returns(); - - // Function with primitive and collection of primitive parameters - var function = entityTypeConfigurationOfEmployee.Collection.Function("PrimitiveFunction").Returns(); - function.Parameter("param"); - function.Parameter("price"); // nullable - function.Parameter("name"); // nullable - function.CollectionParameter("names"); // collection with nullable element - - // Function with Enum and collection of Enum parameters - function = entityTypeConfigurationOfEmployee.Collection.Function("EnumFunction").Returns(); - function.Parameter("bkColor"); - function.Parameter("frColor"); // nullable - function.CollectionParameter("colors"); // collection with non-nullable element - - // Function with complex and collection of complex parameters - function = entityTypeConfigurationOfEmployee.Collection.Function("ComplexFunction").ReturnsCollection
(); - function.Parameter
("address").Nullable = false; - function.Parameter
("location"); // nullable - function.CollectionParameter
("addresses"); // collection with nullable element - - // Function with entity and collection of entity parameters - function = entityTypeConfigurationOfEmployee.Collection.Function("EntityFunction").Returns(); - function.EntityParameter("person").Nullable = false; - function.EntityParameter("guard"); // nullable - function.CollectionEntityParameter("staff"); // collection with nullable element - - #endregion - - #region actions - - // Action bound to a collection of base EntityType - entityTypeConfigurationOfEmployee.Collection.Action("IncreaseSalary") - .ReturnsCollectionFromEntitySet(entitySetConfiguration) - .Parameter("Name"); - - // Action bound to a collection of derived EntityType - entityTypeConfigurationOfManager.Collection.Action("IncreaseSalary") - .ReturnsCollectionFromEntitySet(entitySetConfiguration) - .Parameter("Name"); - - // Action bound to a base EntityType - entityTypeConfigurationOfEmployee.Action("IncreaseSalary") - .Returns(); - - // Action bound to a derived EntityType - entityTypeConfigurationOfManager.Action("IncreaseSalary") - .Returns(); - - // Action with optional parameters - var action = entityTypeConfigurationOfManager.Action("IncreaseWholeSalary") - .Returns(); - action.Parameter("minSalary"); - action.Parameter("maxSalary").Optional(); - action.Parameter("aveSalary").HasDefaultValue("8.9"); - - // Action with primitive and collection of primitive parameters - action = entityTypeConfigurationOfEmployee.Collection.Action("PrimitiveAction"); - action.Parameter("param"); - action.Parameter("price"); // nullable - action.Parameter("name"); // nullable - action.CollectionParameter("names"); // collection with nullable element - - // Action with Enum and collection of Enum parameters - action = entityTypeConfigurationOfEmployee.Collection.Action("EnumAction"); - action.Parameter("bkColor"); - action.Parameter("frColor"); // nullable - action.CollectionParameter("colors"); // collection with non-nullable element - - // Action with complex and collection of complex parameters - action = entityTypeConfigurationOfEmployee.Collection.Action("ComplexAction"); - action.Parameter
("address").Nullable = false; - action.Parameter
("location"); // nullable - action.CollectionParameter
("addresses"); // collection with nullable element - - // Action with entity and collection of entity parameters - action = entityTypeConfigurationOfEmployee.Collection.Action("EntityAction"); - action.EntityParameter("person").Nullable = false; - action.EntityParameter("guard"); // nullable - action.CollectionEntityParameter("staff"); // collection with nullable element - #endregion - - builder.Action("ResetDataSource"); - - builder.EnumType().Namespace = "NS"; - builder.ComplexType
().Namespace = "NS"; - builder.ComplexType().Namespace = "NS"; - builder.EntityType().Namespace = "NS"; - builder.EntityType().Namespace = "NS"; - - return builder.GetEdmModel(); - } + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + EntitySetConfiguration entitySetConfiguration = builder.EntitySet("Employees"); + EntityTypeConfiguration entityTypeConfigurationOfManager = builder.EntityType(); + EntityTypeConfiguration entityTypeConfigurationOfEmployee = builder.EntityType(); + + #region functions + + // Function bound to a collection of base EntityType. + entityTypeConfigurationOfEmployee.Collection.Function("GetCount") + .Returns(); + + // Overload + entityTypeConfigurationOfEmployee.Collection.Function("GetCount") + .Returns() + .Parameter("Name"); + + // Overload with one optional parameter + var salaryRangeCount = entityTypeConfigurationOfEmployee.Collection.Function("GetWholeSalary") + .Returns(); + salaryRangeCount.Parameter("minSalary"); + salaryRangeCount.Parameter("maxSalary").Optional(); + salaryRangeCount.Parameter("aveSalary").HasDefaultValue("8.9"); + + // Function bound to a collection of derived EntityType. + entityTypeConfigurationOfManager.Collection.Function("GetCount") + .Returns(); + + // Function bound to an base EntityType + entityTypeConfigurationOfEmployee.Function("GetEmailsCount") + .Returns(); + + entityTypeConfigurationOfEmployee.Function("GetOptionalAddresses") + .ReturnsCollection
() + .IsComposable = true; + + entityTypeConfigurationOfEmployee.Function("GetEmails") + .ReturnsCollection() + .IsComposable = false; + + // Function bound to a derived EntityType + entityTypeConfigurationOfManager.Function("GetEmailsCount") + .Returns(); + + // Function with primitive and collection of primitive parameters + var function = entityTypeConfigurationOfEmployee.Collection.Function("PrimitiveFunction").Returns(); + function.Parameter("param"); + function.Parameter("price"); // nullable + function.Parameter("name"); // nullable + function.CollectionParameter("names"); // collection with nullable element + + // Function with Enum and collection of Enum parameters + function = entityTypeConfigurationOfEmployee.Collection.Function("EnumFunction").Returns(); + function.Parameter("bkColor"); + function.Parameter("frColor"); // nullable + function.CollectionParameter("colors"); // collection with non-nullable element + + // Function with complex and collection of complex parameters + function = entityTypeConfigurationOfEmployee.Collection.Function("ComplexFunction").ReturnsCollection
(); + function.Parameter
("address").Nullable = false; + function.Parameter
("location"); // nullable + function.CollectionParameter
("addresses"); // collection with nullable element + + // Function with entity and collection of entity parameters + function = entityTypeConfigurationOfEmployee.Collection.Function("EntityFunction").Returns(); + function.EntityParameter("person").Nullable = false; + function.EntityParameter("guard"); // nullable + function.CollectionEntityParameter("staff"); // collection with nullable element + + #endregion + + #region actions + + // Action bound to a collection of base EntityType + entityTypeConfigurationOfEmployee.Collection.Action("IncreaseSalary") + .ReturnsCollectionFromEntitySet(entitySetConfiguration) + .Parameter("Name"); + + // Action bound to a collection of derived EntityType + entityTypeConfigurationOfManager.Collection.Action("IncreaseSalary") + .ReturnsCollectionFromEntitySet(entitySetConfiguration) + .Parameter("Name"); + + // Action bound to a base EntityType + entityTypeConfigurationOfEmployee.Action("IncreaseSalary") + .Returns(); + + // Action bound to a derived EntityType + entityTypeConfigurationOfManager.Action("IncreaseSalary") + .Returns(); + + // Action with optional parameters + var action = entityTypeConfigurationOfManager.Action("IncreaseWholeSalary") + .Returns(); + action.Parameter("minSalary"); + action.Parameter("maxSalary").Optional(); + action.Parameter("aveSalary").HasDefaultValue("8.9"); + + // Action with primitive and collection of primitive parameters + action = entityTypeConfigurationOfEmployee.Collection.Action("PrimitiveAction"); + action.Parameter("param"); + action.Parameter("price"); // nullable + action.Parameter("name"); // nullable + action.CollectionParameter("names"); // collection with nullable element + + // Action with Enum and collection of Enum parameters + action = entityTypeConfigurationOfEmployee.Collection.Action("EnumAction"); + action.Parameter("bkColor"); + action.Parameter("frColor"); // nullable + action.CollectionParameter("colors"); // collection with non-nullable element + + // Action with complex and collection of complex parameters + action = entityTypeConfigurationOfEmployee.Collection.Action("ComplexAction"); + action.Parameter
("address").Nullable = false; + action.Parameter
("location"); // nullable + action.CollectionParameter
("addresses"); // collection with nullable element + + // Action with entity and collection of entity parameters + action = entityTypeConfigurationOfEmployee.Collection.Action("EntityAction"); + action.EntityParameter("person").Nullable = false; + action.EntityParameter("guard"); // nullable + action.CollectionEntityParameter("staff"); // collection with nullable element + #endregion + + builder.Action("ResetDataSource"); + + builder.EnumType().Namespace = "NS"; + builder.ComplexType
().Namespace = "NS"; + builder.ComplexType().Namespace = "NS"; + builder.EntityType().Namespace = "NS"; + builder.EntityType().Namespace = "NS"; + + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationTest.cs index f3c994d4c..22b0e0632 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/BoundOperation/BoundOperationTest.cs @@ -19,930 +19,929 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.BoundOperation +namespace Microsoft.AspNetCore.OData.E2E.Tests.BoundOperation; + +public class BoundOperationTest : WebApiTestBase { - public class BoundOperationTest : WebApiTestBase + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) { - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) + services.ConfigureControllers(typeof(EmployeesController), typeof(MetadataController)); + + IEdmModel edmModel = UnBoundFunctionEdmModel.GetEdmModel(); + + services.AddControllers().AddOData(opt => { - services.ConfigureControllers(typeof(EmployeesController), typeof(MetadataController)); + opt.AddRouteComponents("AttributeRouting", edmModel).AddRouteComponents("ConventionRouting", edmModel); + opt.EnableAttributeRouting = false; + }); - IEdmModel edmModel = UnBoundFunctionEdmModel.GetEdmModel(); + services.TryAddEnumerable(ServiceDescriptor.Transient()); + } - services.AddControllers().AddOData(opt => - { - opt.AddRouteComponents("AttributeRouting", edmModel).AddRouteComponents("ConventionRouting", edmModel); - opt.EnableAttributeRouting = false; - }); + private const string CollectionOfEmployee = "Collection(NS.Employee)"; + private const string CollectionOfManager = "Collection(NS.Manager)"; + private const string Employee = "NS.Employee"; + private const string Manager = "NS.Manager"; - services.TryAddEnumerable(ServiceDescriptor.Transient()); - } + public BoundOperationTest(WebApiTestFixture fixture) + : base(fixture) + { + } - private const string CollectionOfEmployee = "Collection(NS.Employee)"; - private const string CollectionOfManager = "Collection(NS.Manager)"; - private const string Employee = "NS.Employee"; - private const string Manager = "NS.Manager"; + private async Task ResetDatasource() + { + var requestUriForPost = "AttributeRouting/ResetDataSource"; + HttpClient client = CreateClient(); + var responseForPost = await client.PostAsync(requestUriForPost, new StringContent("")); + Assert.True(responseForPost.IsSuccessStatusCode); + return responseForPost; + } - public BoundOperationTest(WebApiTestFixture fixture) - : base(fixture) - { - } + [Theory] + [InlineData("AttributeRouting")] + [InlineData("ConventionRouting")] + [Trait("Pioneer", "true")] + public async Task ModelBuilderTest(string routing) + { + // Arrange + var typeOfEmployee = typeof(Employee); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync($"{routing}/$metadata"); + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); + var reader = new ODataMessageReader(message); + var edmModel = reader.ReadMetadataDocument(); + + // Assert + #region functions + // Function GetCount + var iEdmOperationsOfGetCount = edmModel.FindDeclaredOperations("Default.GetCount"); + Assert.Equal(3, iEdmOperationsOfGetCount.Count()); + + // (Collection(Employee)).GetCount(String Name) + var getCount = iEdmOperationsOfGetCount.Where(f => f.Parameters.Count() == 2); + Assert.Single(getCount); - private async Task ResetDatasource() + // (Collection(Manager)).GetCount() + foreach (var t in iEdmOperationsOfGetCount) { - var requestUriForPost = "AttributeRouting/ResetDataSource"; - HttpClient client = CreateClient(); - var responseForPost = await client.PostAsync(requestUriForPost, new StringContent("")); - Assert.True(responseForPost.IsSuccessStatusCode); - return responseForPost; + var t1 = t.Parameters.First().Type.Definition.ToString(); + var t2 = t.Parameters.First().Type; } + getCount = iEdmOperationsOfGetCount.Where(f => f.Parameters.Count() == 1 + && f.Parameters.First().Type.Definition.ToString() + .Equals(string.Format("Collection([{0} Nullable=True])", Manager))); + Assert.Single(getCount); - [Theory] - [InlineData("AttributeRouting")] - [InlineData("ConventionRouting")] - [Trait("Pioneer", "true")] - public async Task ModelBuilderTest(string routing) - { - // Arrange - var typeOfEmployee = typeof(Employee); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync($"{routing}/$metadata"); - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); - var reader = new ODataMessageReader(message); - var edmModel = reader.ReadMetadataDocument(); - - // Assert - #region functions - // Function GetCount - var iEdmOperationsOfGetCount = edmModel.FindDeclaredOperations("Default.GetCount"); - Assert.Equal(3, iEdmOperationsOfGetCount.Count()); - - // (Collection(Employee)).GetCount(String Name) - var getCount = iEdmOperationsOfGetCount.Where(f => f.Parameters.Count() == 2); - Assert.Single(getCount); - - // (Collection(Manager)).GetCount() - foreach (var t in iEdmOperationsOfGetCount) - { - var t1 = t.Parameters.First().Type.Definition.ToString(); - var t2 = t.Parameters.First().Type; - } - getCount = iEdmOperationsOfGetCount.Where(f => f.Parameters.Count() == 1 - && f.Parameters.First().Type.Definition.ToString() - .Equals(string.Format("Collection([{0} Nullable=True])", Manager))); - Assert.Single(getCount); + // (Collection(Employee)).GetCount() + getCount = iEdmOperationsOfGetCount.Where(f => f.Parameters.Count() == 1 + && f.Parameters.First().Type.Definition.ToString() + .Equals(string.Format("Collection([{0} Nullable=True])", Employee))); + Assert.Single(getCount); - // (Collection(Employee)).GetCount() - getCount = iEdmOperationsOfGetCount.Where(f => f.Parameters.Count() == 1 - && f.Parameters.First().Type.Definition.ToString() - .Equals(string.Format("Collection([{0} Nullable=True])", Employee))); - Assert.Single(getCount); + // Function GetEmailsCount() + var iEdmOperationsOfGetEmailsCount = edmModel.FindDeclaredOperations("Default.GetEmailsCount"); + Assert.Equal(2, iEdmOperationsOfGetEmailsCount.Count()); - // Function GetEmailsCount() - var iEdmOperationsOfGetEmailsCount = edmModel.FindDeclaredOperations("Default.GetEmailsCount"); - Assert.Equal(2, iEdmOperationsOfGetEmailsCount.Count()); + // Empllyee.GetEmailCount() + var getEmailsCount = iEdmOperationsOfGetEmailsCount.Where(f => f.Parameters.Count() == 1 + && f.Parameters.First().Type.Definition.ToString().Equals(Employee)); + Assert.Single(getEmailsCount); - // Empllyee.GetEmailCount() - var getEmailsCount = iEdmOperationsOfGetEmailsCount.Where(f => f.Parameters.Count() == 1 - && f.Parameters.First().Type.Definition.ToString().Equals(Employee)); - Assert.Single(getEmailsCount); + // Manager.GetEmailCount() + getEmailsCount = iEdmOperationsOfGetEmailsCount.Where(f => f.Parameters.Count() == 1 + && f.Parameters.First().Type.Definition.ToString().Equals(Manager)); + Assert.Single(getEmailsCount); - // Manager.GetEmailCount() - getEmailsCount = iEdmOperationsOfGetEmailsCount.Where(f => f.Parameters.Count() == 1 - && f.Parameters.First().Type.Definition.ToString().Equals(Manager)); - Assert.Single(getEmailsCount); + // primitive & collection of primitive + AssertPrimitiveOperation(edmModel, "Default.PrimitiveFunction"); - // primitive & collection of primitive - AssertPrimitiveOperation(edmModel, "Default.PrimitiveFunction"); + // Enum & collection of Enum + AssertEnumOperation(edmModel, "Default.EnumFunction"); - // Enum & collection of Enum - AssertEnumOperation(edmModel, "Default.EnumFunction"); + // Complex & collection of Complex + AssertComplexOperation(edmModel, "Default.ComplexFunction"); - // Complex & collection of Complex - AssertComplexOperation(edmModel, "Default.ComplexFunction"); + // Entity & collection of Entity + AssertEntityOperation(edmModel, "Default.EntityFunction"); - // Entity & collection of Entity - AssertEntityOperation(edmModel, "Default.EntityFunction"); + // Function with optional parameters + AssertOperationWithOptionalParameter(edmModel, "Default.GetWholeSalary"); - // Function with optional parameters - AssertOperationWithOptionalParameter(edmModel, "Default.GetWholeSalary"); + #endregion - #endregion + #region actions - #region actions + // Action IncreaseSalary + var iEdmOperationOfIncreaseSalary = edmModel.FindDeclaredOperations("Default.IncreaseSalary"); + Assert.Equal(4, iEdmOperationOfIncreaseSalary.Count()); - // Action IncreaseSalary - var iEdmOperationOfIncreaseSalary = edmModel.FindDeclaredOperations("Default.IncreaseSalary"); - Assert.Equal(4, iEdmOperationOfIncreaseSalary.Count()); + // (Collection(Employee)).IncreaseSalary(String Name) + var increaseSalary = iEdmOperationOfIncreaseSalary.Where(a => a.Parameters.Count() == 2 + && a.Parameters.First().Type.Definition.ToString().Equals(CollectionOfEmployee)); + Assert.NotNull(increaseSalary); - // (Collection(Employee)).IncreaseSalary(String Name) - var increaseSalary = iEdmOperationOfIncreaseSalary.Where(a => a.Parameters.Count() == 2 - && a.Parameters.First().Type.Definition.ToString().Equals(CollectionOfEmployee)); - Assert.NotNull(increaseSalary); + // (Collection(Manager)).IncreaseSalary() + increaseSalary = iEdmOperationOfIncreaseSalary.Where(a => a.Parameters.Count() == 1 + && a.Parameters.First().Type.Definition.ToString().Equals(CollectionOfManager)); + Assert.NotNull(increaseSalary); - // (Collection(Manager)).IncreaseSalary() - increaseSalary = iEdmOperationOfIncreaseSalary.Where(a => a.Parameters.Count() == 1 - && a.Parameters.First().Type.Definition.ToString().Equals(CollectionOfManager)); - Assert.NotNull(increaseSalary); + // Employee.IncreaseSalary() + increaseSalary = iEdmOperationOfIncreaseSalary.Where(a => a.Parameters.Count() == 1 + && a.Parameters.First().Type.Definition.ToString().Equals(Employee)); + Assert.NotNull(increaseSalary); - // Employee.IncreaseSalary() - increaseSalary = iEdmOperationOfIncreaseSalary.Where(a => a.Parameters.Count() == 1 - && a.Parameters.First().Type.Definition.ToString().Equals(Employee)); - Assert.NotNull(increaseSalary); + // Manager.IncreaseSalary() + increaseSalary = iEdmOperationOfIncreaseSalary.Where(a => a.Parameters.Count() == 1 + && a.Parameters.First().Type.Definition.ToString().Equals(Manager)); + Assert.NotNull(increaseSalary); - // Manager.IncreaseSalary() - increaseSalary = iEdmOperationOfIncreaseSalary.Where(a => a.Parameters.Count() == 1 - && a.Parameters.First().Type.Definition.ToString().Equals(Manager)); - Assert.NotNull(increaseSalary); + // primitive & collection of primitive + AssertPrimitiveOperation(edmModel, "Default.PrimitiveAction"); - // primitive & collection of primitive - AssertPrimitiveOperation(edmModel, "Default.PrimitiveAction"); + // Enum & collection of Enum + AssertEnumOperation(edmModel, "Default.EnumAction"); - // Enum & collection of Enum - AssertEnumOperation(edmModel, "Default.EnumAction"); + // Complex & collection of Complex + AssertComplexOperation(edmModel, "Default.ComplexAction"); - // Complex & collection of Complex - AssertComplexOperation(edmModel, "Default.ComplexAction"); + // Entity & collection of Entity + AssertEntityOperation(edmModel, "Default.EntityAction"); - // Entity & collection of Entity - AssertEntityOperation(edmModel, "Default.EntityAction"); + // Action with optional parameters + AssertOperationWithOptionalParameter(edmModel, "Default.IncreaseWholeSalary"); - // Action with optional parameters - AssertOperationWithOptionalParameter(edmModel, "Default.IncreaseWholeSalary"); + #endregion - #endregion + // ActionImport: ResetDataSource + Assert.Single(edmModel.EntityContainer.OperationImports()); + } - // ActionImport: ResetDataSource - Assert.Single(edmModel.EntityContainer.OperationImports()); - } + private static void AssertOperationWithOptionalParameter(IEdmModel edmModel, string opertionName) + { + IEdmOperation primitiveFunc = Assert.Single(edmModel.FindDeclaredOperations(opertionName)); + Assert.Equal(4, primitiveFunc.Parameters.Count()); + + // non-optional parameter + IEdmOperationParameter parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "minSalary")); + Assert.Equal("Edm.Double", parameter.Type.FullName()); + Assert.False(parameter.Type.IsNullable); + + // optional parameter without default value + parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "maxSalary")); + Assert.NotNull(parameter); + Assert.Equal("Edm.Double", parameter.Type.FullName()); + IEdmOptionalParameter optionalParameterInfo = Assert.IsAssignableFrom(parameter); + Assert.NotNull(optionalParameterInfo); + Assert.Null(optionalParameterInfo.DefaultValueString); + + // optional parameter with default value + parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "aveSalary")); + Assert.NotNull(parameter); + Assert.Equal("Edm.Double", parameter.Type.FullName()); + optionalParameterInfo = Assert.IsAssignableFrom(parameter); + Assert.NotNull(optionalParameterInfo); + Assert.Equal("8.9", optionalParameterInfo.DefaultValueString); + } - private static void AssertOperationWithOptionalParameter(IEdmModel edmModel, string opertionName) - { - IEdmOperation primitiveFunc = Assert.Single(edmModel.FindDeclaredOperations(opertionName)); - Assert.Equal(4, primitiveFunc.Parameters.Count()); - - // non-optional parameter - IEdmOperationParameter parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "minSalary")); - Assert.Equal("Edm.Double", parameter.Type.FullName()); - Assert.False(parameter.Type.IsNullable); - - // optional parameter without default value - parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "maxSalary")); - Assert.NotNull(parameter); - Assert.Equal("Edm.Double", parameter.Type.FullName()); - IEdmOptionalParameter optionalParameterInfo = Assert.IsAssignableFrom(parameter); - Assert.NotNull(optionalParameterInfo); - Assert.Null(optionalParameterInfo.DefaultValueString); - - // optional parameter with default value - parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "aveSalary")); - Assert.NotNull(parameter); - Assert.Equal("Edm.Double", parameter.Type.FullName()); - optionalParameterInfo = Assert.IsAssignableFrom(parameter); - Assert.NotNull(optionalParameterInfo); - Assert.Equal("8.9", optionalParameterInfo.DefaultValueString); - } + private static void AssertPrimitiveOperation(IEdmModel edmModel, string opertionName) + { + IEdmOperation primitiveFunc = Assert.Single(edmModel.FindDeclaredOperations(opertionName)); + Assert.Equal(5, primitiveFunc.Parameters.Count()); - private static void AssertPrimitiveOperation(IEdmModel edmModel, string opertionName) - { - IEdmOperation primitiveFunc = Assert.Single(edmModel.FindDeclaredOperations(opertionName)); - Assert.Equal(5, primitiveFunc.Parameters.Count()); + IEdmOperationParameter parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "param")); + Assert.Equal("Edm.Int32", parameter.Type.FullName()); + Assert.False(parameter.Type.IsNullable); - IEdmOperationParameter parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "param")); - Assert.Equal("Edm.Int32", parameter.Type.FullName()); - Assert.False(parameter.Type.IsNullable); + parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "price")); + Assert.Equal("Edm.Double", parameter.Type.FullName()); + Assert.True(parameter.Type.IsNullable); - parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "price")); - Assert.Equal("Edm.Double", parameter.Type.FullName()); - Assert.True(parameter.Type.IsNullable); + parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "name")); + Assert.Equal("Edm.String", parameter.Type.FullName()); + Assert.True(parameter.Type.IsNullable); - parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "name")); - Assert.Equal("Edm.String", parameter.Type.FullName()); - Assert.True(parameter.Type.IsNullable); + parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "names")); + Assert.Equal("Collection(Edm.String)", parameter.Type.FullName()); + Assert.True(parameter.Type.IsNullable); + } - parameter = Assert.Single(primitiveFunc.Parameters.Where(e => e.Name == "names")); - Assert.Equal("Collection(Edm.String)", parameter.Type.FullName()); - Assert.True(parameter.Type.IsNullable); - } + private static void AssertEnumOperation(IEdmModel edmModel, string operationName) + { + IEdmOperation enumFunc = Assert.Single(edmModel.FindDeclaredOperations(operationName)); + Assert.Equal(4, enumFunc.Parameters.Count()); - private static void AssertEnumOperation(IEdmModel edmModel, string operationName) - { - IEdmOperation enumFunc = Assert.Single(edmModel.FindDeclaredOperations(operationName)); - Assert.Equal(4, enumFunc.Parameters.Count()); + IEdmOperationParameter parameter = Assert.Single(enumFunc.Parameters.Where(e => e.Name == "bkColor")); + Assert.Equal("NS.Color", parameter.Type.FullName()); + Assert.False(parameter.Type.IsNullable); - IEdmOperationParameter parameter = Assert.Single(enumFunc.Parameters.Where(e => e.Name == "bkColor")); - Assert.Equal("NS.Color", parameter.Type.FullName()); - Assert.False(parameter.Type.IsNullable); + parameter = Assert.Single(enumFunc.Parameters.Where(e => e.Name == "frColor")); + Assert.Equal("NS.Color", parameter.Type.FullName()); + Assert.True(parameter.Type.IsNullable); - parameter = Assert.Single(enumFunc.Parameters.Where(e => e.Name == "frColor")); - Assert.Equal("NS.Color", parameter.Type.FullName()); - Assert.True(parameter.Type.IsNullable); + parameter = Assert.Single(enumFunc.Parameters.Where(e => e.Name == "colors")); + Assert.Equal("Collection(NS.Color)", parameter.Type.FullName()); + Assert.False(parameter.Type.IsNullable); + } - parameter = Assert.Single(enumFunc.Parameters.Where(e => e.Name == "colors")); - Assert.Equal("Collection(NS.Color)", parameter.Type.FullName()); - Assert.False(parameter.Type.IsNullable); - } + private static void AssertComplexOperation(IEdmModel edmModel, string operationName) + { + IEdmOperation complexFunc = Assert.Single(edmModel.FindDeclaredOperations(operationName)); + Assert.Equal(4, complexFunc.Parameters.Count()); - private static void AssertComplexOperation(IEdmModel edmModel, string operationName) - { - IEdmOperation complexFunc = Assert.Single(edmModel.FindDeclaredOperations(operationName)); - Assert.Equal(4, complexFunc.Parameters.Count()); + IEdmOperationParameter parameter = Assert.Single(complexFunc.Parameters.Where(e => e.Name == "address")); + Assert.Equal("NS.Address", parameter.Type.FullName()); + Assert.False(parameter.Type.IsNullable); - IEdmOperationParameter parameter = Assert.Single(complexFunc.Parameters.Where(e => e.Name == "address")); - Assert.Equal("NS.Address", parameter.Type.FullName()); - Assert.False(parameter.Type.IsNullable); + parameter = Assert.Single(complexFunc.Parameters.Where(e => e.Name == "location")); + Assert.Equal("NS.Address", parameter.Type.FullName()); + Assert.True(parameter.Type.IsNullable); - parameter = Assert.Single(complexFunc.Parameters.Where(e => e.Name == "location")); - Assert.Equal("NS.Address", parameter.Type.FullName()); - Assert.True(parameter.Type.IsNullable); + parameter = Assert.Single(complexFunc.Parameters.Where(e => e.Name == "addresses")); + Assert.Equal("Collection(NS.Address)", parameter.Type.FullName()); + Assert.True(parameter.Type.IsNullable); + } - parameter = Assert.Single(complexFunc.Parameters.Where(e => e.Name == "addresses")); - Assert.Equal("Collection(NS.Address)", parameter.Type.FullName()); - Assert.True(parameter.Type.IsNullable); - } + private static void AssertEntityOperation(IEdmModel edmModel, string operationName) + { + IEdmOperation entityFunc = Assert.Single(edmModel.FindDeclaredOperations(operationName)); + Assert.Equal(4, entityFunc.Parameters.Count()); - private static void AssertEntityOperation(IEdmModel edmModel, string operationName) - { - IEdmOperation entityFunc = Assert.Single(edmModel.FindDeclaredOperations(operationName)); - Assert.Equal(4, entityFunc.Parameters.Count()); + IEdmOperationParameter parameter = Assert.Single(entityFunc.Parameters.Where(e => e.Name == "person")); + Assert.Equal("NS.Employee", parameter.Type.FullName()); + Assert.False(parameter.Type.IsNullable); - IEdmOperationParameter parameter = Assert.Single(entityFunc.Parameters.Where(e => e.Name == "person")); - Assert.Equal("NS.Employee", parameter.Type.FullName()); - Assert.False(parameter.Type.IsNullable); + parameter = Assert.Single(entityFunc.Parameters.Where(e => e.Name == "guard")); + Assert.Equal("NS.Employee", parameter.Type.FullName()); + Assert.True(parameter.Type.IsNullable); - parameter = Assert.Single(entityFunc.Parameters.Where(e => e.Name == "guard")); - Assert.Equal("NS.Employee", parameter.Type.FullName()); - Assert.True(parameter.Type.IsNullable); + parameter = Assert.Single(entityFunc.Parameters.Where(e => e.Name == "staff")); + Assert.Equal("Collection(NS.Employee)", parameter.Type.FullName()); + Assert.True(parameter.Type.IsNullable); + } + #region functions - parameter = Assert.Single(entityFunc.Parameters.Where(e => e.Name == "staff")); - Assert.Equal("Collection(NS.Employee)", parameter.Type.FullName()); - Assert.True(parameter.Type.IsNullable); - } - #region functions + /* + [Theory] + [InlineData("ConventionRouting/Employees/Default.GetCount()")]//Convention routing + [InlineData("AttributeRouting/Employees/Default.GetCount()")]//Attribute routing + public async Task FunctionBoundToEntitySet(string url) + { + // Arrange & Act + HttpResponseMessage response = await Client.GetAsync(url); + string responseString = await response.Content.ReadAsStringAsync(); - /* - [Theory] - [InlineData("ConventionRouting/Employees/Default.GetCount()")]//Convention routing - [InlineData("AttributeRouting/Employees/Default.GetCount()")]//Attribute routing - public async Task FunctionBoundToEntitySet(string url) + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("/$metadata#Edm.Int32", responseString); + if (url.StartsWith("ConventionRouting")) { - // Arrange & Act - HttpResponseMessage response = await Client.GetAsync(url); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("/$metadata#Edm.Int32", responseString); - if (url.StartsWith("ConventionRouting")) - { - Assert.Contains(@"""value"":11", responseString); - } - else - { - Assert.Contains(@"""value"":22", responseString); - } + Assert.Contains(@"""value"":11", responseString); } - - [Theory] - [InlineData("ConventionRouting/Employees/Default.GetCount(Name='Name1%252F')", 1)]// Slash - [InlineData("AttributeRouting/Employees/Default.GetCount(Name='Name6%3F')", 2)]// QuestionMark - [InlineData("AttributeRouting/Employees/Default.GetCount(Name='Name20%23')", 2)]// Pound - public async Task FunctionBoundToEntitySetOverload(string url, int expectedCount) + else { - // Arrange & Act - HttpResponseMessage response = await Client.GetAsync(url); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("/$metadata#Edm.Int32", responseString); - Assert.Contains(string.Format(@"""value"":{0}", expectedCount), responseString); + Assert.Contains(@"""value"":22", responseString); } + } - [Theory] - [InlineData("ConventionRouting/Employees/NS.Manager/Default.GetCount()", 5)]//Convention routing - [InlineData("AttributeRouting/Employees/NS.Manager/Default.GetCount()", 10)]//Attribute routing - public async Task FunctionBoundToEntitySetForDerivedBindingType(string url, int expectedCount) - { - // Arrange & Act - HttpResponseMessage response = await Client.GetAsync(url); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("/$metadata#Edm.Int32", responseString); - Assert.Contains(string.Format(@"""value"":{0}", expectedCount), responseString); - } + [Theory] + [InlineData("ConventionRouting/Employees/Default.GetCount(Name='Name1%252F')", 1)]// Slash + [InlineData("AttributeRouting/Employees/Default.GetCount(Name='Name6%3F')", 2)]// QuestionMark + [InlineData("AttributeRouting/Employees/Default.GetCount(Name='Name20%23')", 2)]// Pound + public async Task FunctionBoundToEntitySetOverload(string url, int expectedCount) + { + // Arrange & Act + HttpResponseMessage response = await Client.GetAsync(url); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("/$metadata#Edm.Int32", responseString); + Assert.Contains(string.Format(@"""value"":{0}", expectedCount), responseString); + } - [Theory] - [InlineData("ConventionRouting/Employees(1)/Default.GetEmailsCount()", 1)]//Convention routing - [InlineData("AttributeRouting/Employees(1)/Default.GetEmailsCount()", 2)]//Attribute routing - public async Task FunctionBoundToEntityType(string url, int expectedCount) - { - // Arrange & Act - HttpResponseMessage response = await Client.GetAsync(url); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("/$metadata#Edm.Int32", responseString); - Assert.Contains(string.Format(@"""value"":{0}", expectedCount), responseString); - } + [Theory] + [InlineData("ConventionRouting/Employees/NS.Manager/Default.GetCount()", 5)]//Convention routing + [InlineData("AttributeRouting/Employees/NS.Manager/Default.GetCount()", 10)]//Attribute routing + public async Task FunctionBoundToEntitySetForDerivedBindingType(string url, int expectedCount) + { + // Arrange & Act + HttpResponseMessage response = await Client.GetAsync(url); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("/$metadata#Edm.Int32", responseString); + Assert.Contains(string.Format(@"""value"":{0}", expectedCount), responseString); + } - [Theory] - [InlineData("ConventionRouting/Employees/Default.GetWholeSalary(minSalary=6.8)", "(6.8, 0, 8.9)")] - [InlineData("AttributeRouting/Employees/Default.GetWholeSalary(minSalary=6.8)", "(6.8, 0, 8.9)")] - [InlineData("ConventionRouting/Employees/Default.GetWholeSalary(minSalary=1.09,maxSalary=7.3)", "(1.09, 7.3, 8.9)")] - [InlineData("AttributeRouting/Employees/Default.GetWholeSalary(minSalary=1.09,maxSalary=7.3)", "(1.09, 7.3, 8.9)")] - [InlineData("ConventionRouting/Employees/Default.GetWholeSalary(minSalary=8.1,maxSalary=1.1,aveSalary=3.3)", "(8.1, 1.1, 3.3)")] - [InlineData("AttributeRouting/Employees/Default.GetWholeSalary(minSalary=8.1,maxSalary=1.1,aveSalary=3.3)", "(8.1, 1.1, 3.3)")] - public async Task FunctionWithOptionalParamsBoundToEntityType(string url, string expected) - { - // Arrange & Act - HttpResponseMessage response = await Client.GetAsync(url); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("/$metadata#Edm.String", responseString); - Assert.Contains(string.Format(@"""value"":""GetWholeSalary{0}""", expected), responseString); - } + [Theory] + [InlineData("ConventionRouting/Employees(1)/Default.GetEmailsCount()", 1)]//Convention routing + [InlineData("AttributeRouting/Employees(1)/Default.GetEmailsCount()", 2)]//Attribute routing + public async Task FunctionBoundToEntityType(string url, int expectedCount) + { + // Arrange & Act + HttpResponseMessage response = await Client.GetAsync(url); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("/$metadata#Edm.Int32", responseString); + Assert.Contains(string.Format(@"""value"":{0}", expectedCount), responseString); + } - [Theory] - [InlineData("ConventionRouting/Employees(1)/NS.Manager/Default.GetEmailsCount()", 1)]//Convention routing - [InlineData("AttributeRouting/Employees(1)/NS.Manager/Default.GetEmailsCount()", 2)]//Attribute routing - public async Task FunctionBoundToDerivedEntityType(string url, int expectedCount) - { - // Arrange & Act - HttpResponseMessage response = await Client.GetAsync(url); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("/$metadata#Edm.Int32", responseString); - Assert.Contains(string.Format(@"""value"":{0}", expectedCount), responseString); - } - */ - - [Theory] - [InlineData("ConventionRouting/Employees?$filter=Default.GetEmailsCount() lt 10")] - [InlineData("ConventionRouting/Employees?$filter=$it/Default.GetEmailsCount() lt 10")] - [InlineData("ConventionRouting/Employees/NS.Manager?$filter=Default.GetEmailsCount() lt 10")] - [InlineData("ConventionRouting/Employees/NS.Manager?$filter=$it/Default.GetEmailsCount() lt 10")] - public async Task BoundFunctionInDollarFilter(string url) - { - // Arrange & Act - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(url); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); // 400 - Assert.Contains(@"Unknown function 'Default.GetEmailsCount'", responseString); - Assert.Contains(@"System.NotImplementedException", responseString); - } + [Theory] + [InlineData("ConventionRouting/Employees/Default.GetWholeSalary(minSalary=6.8)", "(6.8, 0, 8.9)")] + [InlineData("AttributeRouting/Employees/Default.GetWholeSalary(minSalary=6.8)", "(6.8, 0, 8.9)")] + [InlineData("ConventionRouting/Employees/Default.GetWholeSalary(minSalary=1.09,maxSalary=7.3)", "(1.09, 7.3, 8.9)")] + [InlineData("AttributeRouting/Employees/Default.GetWholeSalary(minSalary=1.09,maxSalary=7.3)", "(1.09, 7.3, 8.9)")] + [InlineData("ConventionRouting/Employees/Default.GetWholeSalary(minSalary=8.1,maxSalary=1.1,aveSalary=3.3)", "(8.1, 1.1, 3.3)")] + [InlineData("AttributeRouting/Employees/Default.GetWholeSalary(minSalary=8.1,maxSalary=1.1,aveSalary=3.3)", "(8.1, 1.1, 3.3)")] + public async Task FunctionWithOptionalParamsBoundToEntityType(string url, string expected) + { + // Arrange & Act + HttpResponseMessage response = await Client.GetAsync(url); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("/$metadata#Edm.String", responseString); + Assert.Contains(string.Format(@"""value"":""GetWholeSalary{0}""", expected), responseString); + } - /* - [Theory] - [InlineData("ConventionRouting/Employees(1)/Emails/$count", 1)] - [InlineData("AttributeRouting/Employees(1)/Emails/$count", 2)] - public async Task DollarCount(string url, int expectedCount) - { - // Arrange & Act - HttpResponseMessage response = await Client.GetAsync(url); - string responseString = await response.Content.ReadAsStringAsync(); + [Theory] + [InlineData("ConventionRouting/Employees(1)/NS.Manager/Default.GetEmailsCount()", 1)]//Convention routing + [InlineData("AttributeRouting/Employees(1)/NS.Manager/Default.GetEmailsCount()", 2)]//Attribute routing + public async Task FunctionBoundToDerivedEntityType(string url, int expectedCount) + { + // Arrange & Act + HttpResponseMessage response = await Client.GetAsync(url); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("/$metadata#Edm.Int32", responseString); + Assert.Contains(string.Format(@"""value"":{0}", expectedCount), responseString); + } + */ + + [Theory] + [InlineData("ConventionRouting/Employees?$filter=Default.GetEmailsCount() lt 10")] + [InlineData("ConventionRouting/Employees?$filter=$it/Default.GetEmailsCount() lt 10")] + [InlineData("ConventionRouting/Employees/NS.Manager?$filter=Default.GetEmailsCount() lt 10")] + [InlineData("ConventionRouting/Employees/NS.Manager?$filter=$it/Default.GetEmailsCount() lt 10")] + public async Task BoundFunctionInDollarFilter(string url) + { + // Arrange & Act + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(url); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); // 400 + Assert.Contains(@"Unknown function 'Default.GetEmailsCount'", responseString); + Assert.Contains(@"System.NotImplementedException", responseString); + } - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(expectedCount, int.Parse(responseString)); - } + /* + [Theory] + [InlineData("ConventionRouting/Employees(1)/Emails/$count", 1)] + [InlineData("AttributeRouting/Employees(1)/Emails/$count", 2)] + public async Task DollarCount(string url, int expectedCount) + { + // Arrange & Act + HttpResponseMessage response = await Client.GetAsync(url); + string responseString = await response.Content.ReadAsStringAsync(); - [Theory] - [InlineData("ConventionRouting/Employees(1)/Default.GetOptionalAddresses()/$count", 1)] - [InlineData("AttributeRouting/Employees(1)/Default.GetOptionalAddresses()/$count", 2)] - [InlineData("ConventionRouting/Employees(1)/Default.GetOptionalAddresses()/$count?$filter=City eq 'Beijing'", 0)] - [InlineData("AttributeRouting/Employees(1)/Default.GetOptionalAddresses()/$count?$filter=City eq 'Beijing'", 1)] - [InlineData("ConventionRouting/Employees(1)/OptionalAddresses/$count", 1)] - [InlineData("AttributeRouting/Employees(1)/OptionalAddresses/$count", 2)] - [InlineData("ConventionRouting/Employees(1)/OptionalAddresses/$count?$filter=City eq 'Beijing'", 0)] - [InlineData("AttributeRouting/Employees(1)/OptionalAddresses/$count?$filter=City eq 'Beijing'", 1)] - public async Task DollarCountFollowingComplexCollection(string url, int expectedCount) - { - // Arrange & Act - HttpResponseMessage response = await Client.GetAsync(url); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.True(expectedCount == int.Parse(responseString), - string.Format("Expected: {0}; Actual: {1}; Request URL: {2}", expectedCount, responseString, url)); - } + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedCount, int.Parse(responseString)); + } - [Theory] - [InlineData("ConventionRouting", "(param=7,price=9.9,name='Tony',names=['Mike','John'])")] - [InlineData("ConventionRouting", "(param=8,price=null,name=null,names=['Sun',null,'Mike'])")] - [InlineData("ConventionRouting", "(param=9,price=null,name=null,names=@p)?@p=['Mike',null,'John']")] // parameter alias - [InlineData("AttributeRouting", "(param=1,price=0.9,name='Tony',names=['Mike','John'])")] - [InlineData("AttributeRouting", "(param=2,price=null,name=null,names=['Sun',null,'Mike'])")] - [InlineData("AttributeRouting", "(param=3,price=null,name=@p,names=@q)?@p=null&@q=['Mike',null,'John']")] // parameter alias - public async Task BoundFunction_Works_WithPrimitive_And_CollectionOfPrimitiveParameters(string route, string parameter) - { - // Arrange - var requestUri = string.Format("{0}/Employees/Default.PrimitiveFunction{1}", route, parameter); + [Theory] + [InlineData("ConventionRouting/Employees(1)/Default.GetOptionalAddresses()/$count", 1)] + [InlineData("AttributeRouting/Employees(1)/Default.GetOptionalAddresses()/$count", 2)] + [InlineData("ConventionRouting/Employees(1)/Default.GetOptionalAddresses()/$count?$filter=City eq 'Beijing'", 0)] + [InlineData("AttributeRouting/Employees(1)/Default.GetOptionalAddresses()/$count?$filter=City eq 'Beijing'", 1)] + [InlineData("ConventionRouting/Employees(1)/OptionalAddresses/$count", 1)] + [InlineData("AttributeRouting/Employees(1)/OptionalAddresses/$count", 2)] + [InlineData("ConventionRouting/Employees(1)/OptionalAddresses/$count?$filter=City eq 'Beijing'", 0)] + [InlineData("AttributeRouting/Employees(1)/OptionalAddresses/$count?$filter=City eq 'Beijing'", 1)] + public async Task DollarCountFollowingComplexCollection(string url, int expectedCount) + { + // Arrange & Act + HttpResponseMessage response = await Client.GetAsync(url); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(expectedCount == int.Parse(responseString), + string.Format("Expected: {0}; Actual: {1}; Request URL: {2}", expectedCount, responseString, url)); + } - // Act - HttpResponseMessage response = await Client.GetAsync(requestUri); - string responseString = await response.Content.ReadAsStringAsync(); + [Theory] + [InlineData("ConventionRouting", "(param=7,price=9.9,name='Tony',names=['Mike','John'])")] + [InlineData("ConventionRouting", "(param=8,price=null,name=null,names=['Sun',null,'Mike'])")] + [InlineData("ConventionRouting", "(param=9,price=null,name=null,names=@p)?@p=['Mike',null,'John']")] // parameter alias + [InlineData("AttributeRouting", "(param=1,price=0.9,name='Tony',names=['Mike','John'])")] + [InlineData("AttributeRouting", "(param=2,price=null,name=null,names=['Sun',null,'Mike'])")] + [InlineData("AttributeRouting", "(param=3,price=null,name=@p,names=@q)?@p=null&@q=['Mike',null,'John']")] // parameter alias + public async Task BoundFunction_Works_WithPrimitive_And_CollectionOfPrimitiveParameters(string route, string parameter) + { + // Arrange + var requestUri = string.Format("{0}/Employees/Default.PrimitiveFunction{1}", route, parameter); - // Assert - response.EnsureSuccessStatusCode(); + // Act + HttpResponseMessage response = await Client.GetAsync(requestUri); + string responseString = await response.Content.ReadAsStringAsync(); - Assert.Contains(ReplaceParameterAlias(parameter), responseString); - } + // Assert + response.EnsureSuccessStatusCode(); - [Theory] - [InlineData("ConventionRouting", "(param=null,price=9.9,name='Tony',names=['Mike','John'])")] - [InlineData("AttributeRouting", "(param=null,price=9.9,name='Tony',names=['Mike','John'])")] - public async Task BoundFunction_DoesnotWork_WithNullValue_ForNonNullablePrimitiveParameter(string route, string parameter) - { - // Arrange - var requestUri = string.Format("{0}/Employees/Default.PrimitiveFunction{1}", route, parameter); + Assert.Contains(ReplaceParameterAlias(parameter), responseString); + } - // Act - HttpResponseMessage response = await Client.GetAsync(requestUri); - string responseString = await response.Content.ReadAsStringAsync(); + [Theory] + [InlineData("ConventionRouting", "(param=null,price=9.9,name='Tony',names=['Mike','John'])")] + [InlineData("AttributeRouting", "(param=null,price=9.9,name='Tony',names=['Mike','John'])")] + public async Task BoundFunction_DoesnotWork_WithNullValue_ForNonNullablePrimitiveParameter(string route, string parameter) + { + // Arrange + var requestUri = string.Format("{0}/Employees/Default.PrimitiveFunction{1}", route, parameter); - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); - Assert.Contains("Type verification failed. Expected non-nullable type 'Edm.Int32' but received a null value.", responseString); - } + // Act + HttpResponseMessage response = await Client.GetAsync(requestUri); + string responseString = await response.Content.ReadAsStringAsync(); - [Theory] - [InlineData("ConventionRouting", "(bkColor=NS.Color'Red',frColor=NS.Color'Blue',colors=['Red','Green'])")] - [InlineData("ConventionRouting", "(bkColor=NS.Color'Blue',frColor=null,colors=['Red','Green'])")] - [InlineData("ConventionRouting", "(bkColor=NS.Color'Green',frColor=@x,colors=@y)?@x=null&@y=['Red','Blue']")] // parameter alias - [InlineData("AttributeRouting", "(bkColor=NS.Color'Red',frColor=NS.Color'Blue',colors=['Red','Green'])")] - [InlineData("AttributeRouting", "(bkColor=NS.Color'Blue',frColor=null,colors=['Red','Green'])")] - [InlineData("AttributeRouting", "(bkColor=NS.Color'Green',frColor=@x,colors=@y)?@x=null&@y=['Red','Blue']")] // parameter alias - public async Task BoundFunction_Works_WithEnum_And_CollectionOfEnumParameters(string route, string parameter) - { - // Arrange - var requestUri = string.Format("{0}/Employees/Default.EnumFunction{1}", route, parameter); + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Contains("Type verification failed. Expected non-nullable type 'Edm.Int32' but received a null value.", responseString); + } - // Act - HttpResponseMessage response = await Client.GetAsync(requestUri); - string responseString = await response.Content.ReadAsStringAsync(); + [Theory] + [InlineData("ConventionRouting", "(bkColor=NS.Color'Red',frColor=NS.Color'Blue',colors=['Red','Green'])")] + [InlineData("ConventionRouting", "(bkColor=NS.Color'Blue',frColor=null,colors=['Red','Green'])")] + [InlineData("ConventionRouting", "(bkColor=NS.Color'Green',frColor=@x,colors=@y)?@x=null&@y=['Red','Blue']")] // parameter alias + [InlineData("AttributeRouting", "(bkColor=NS.Color'Red',frColor=NS.Color'Blue',colors=['Red','Green'])")] + [InlineData("AttributeRouting", "(bkColor=NS.Color'Blue',frColor=null,colors=['Red','Green'])")] + [InlineData("AttributeRouting", "(bkColor=NS.Color'Green',frColor=@x,colors=@y)?@x=null&@y=['Red','Blue']")] // parameter alias + public async Task BoundFunction_Works_WithEnum_And_CollectionOfEnumParameters(string route, string parameter) + { + // Arrange + var requestUri = string.Format("{0}/Employees/Default.EnumFunction{1}", route, parameter); - // Assert - response.EnsureSuccessStatusCode(); + // Act + HttpResponseMessage response = await Client.GetAsync(requestUri); + string responseString = await response.Content.ReadAsStringAsync(); - Assert.Contains(ReplaceParameterAlias(parameter.Replace("NS.Color", "")), responseString); - } + // Assert + response.EnsureSuccessStatusCode(); - [Theory] - [InlineData("ConventionRouting", "(bkColor=null,frColor=NS.Color'Red',colors=['Red','Green'])")] - [InlineData("AttributeRouting", "(bkColor=null,frColor=NS.Color'Blue',colors=['Red','Green'])")] - public async Task BoundFunction_DoesnotWork_WithNullValue_ForNonNullableEnumParameter(string route, string parameter) - { - // Arrange - var requestUri = string.Format("{0}/Employees/Default.EnumFunction{1}", route, parameter); + Assert.Contains(ReplaceParameterAlias(parameter.Replace("NS.Color", "")), responseString); + } - // Act - HttpResponseMessage response = await Client.GetAsync(requestUri); - string responseString = await response.Content.ReadAsStringAsync(); + [Theory] + [InlineData("ConventionRouting", "(bkColor=null,frColor=NS.Color'Red',colors=['Red','Green'])")] + [InlineData("AttributeRouting", "(bkColor=null,frColor=NS.Color'Blue',colors=['Red','Green'])")] + public async Task BoundFunction_DoesnotWork_WithNullValue_ForNonNullableEnumParameter(string route, string parameter) + { + // Arrange + var requestUri = string.Format("{0}/Employees/Default.EnumFunction{1}", route, parameter); - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); - Assert.Contains("Type verification failed. Expected non-nullable type 'NS.Color' but received a null value.", responseString); - } + // Act + HttpResponseMessage response = await Client.GetAsync(requestUri); + string responseString = await response.Content.ReadAsStringAsync(); - [Theory] - [InlineData("ConventionRouting", "(bkColor=NS.Color'Red',frColor=NS.Color'Red',colors=[null,'Red'])")] - [InlineData("AttributeRouting", "(bkColor=NS.Color'Green',frColor=NS.Color'Green',colors=[null,'Green'])")] - public async Task BoundFunction_DoesnotWork_WithNullValue_ForNonNullableCollectionEnumParameter(string route, string parameter) - { - // Arrange - var requestUri = string.Format("{0}/Employees/Default.EnumFunction{1}", route, parameter); + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Contains("Type verification failed. Expected non-nullable type 'NS.Color' but received a null value.", responseString); + } - // Act - HttpResponseMessage response = await Client.GetAsync(requestUri); + [Theory] + [InlineData("ConventionRouting", "(bkColor=NS.Color'Red',frColor=NS.Color'Red',colors=[null,'Red'])")] + [InlineData("AttributeRouting", "(bkColor=NS.Color'Green',frColor=NS.Color'Green',colors=[null,'Green'])")] + public async Task BoundFunction_DoesnotWork_WithNullValue_ForNonNullableCollectionEnumParameter(string route, string parameter) + { + // Arrange + var requestUri = string.Format("{0}/Employees/Default.EnumFunction{1}", route, parameter); - // Assert - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } + // Act + HttpResponseMessage response = await Client.GetAsync(requestUri); - // change '(names=@p)?@p=xxx' as '(names=xxx)' - private static string ReplaceParameterAlias(string parameter) - { - string[] segments = parameter.Split('?'); - if (segments.Length == 1) - { - return parameter; - } + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } - string result = segments[0]; - string[] alias = segments[1].Trim().Split('&'); - foreach (var q in alias) - { - string[] keyValue = q.Trim().Split('='); - Assert.Equal(2, keyValue.Length); + // change '(names=@p)?@p=xxx' as '(names=xxx)' + private static string ReplaceParameterAlias(string parameter) + { + string[] segments = parameter.Split('?'); + if (segments.Length == 1) + { + return parameter; + } - string key = keyValue[0].Trim(); - string value = keyValue[1].Trim(); - result = result.Replace(key, value); - } + string result = segments[0]; + string[] alias = segments[1].Trim().Split('&'); + foreach (var q in alias) + { + string[] keyValue = q.Trim().Split('='); + Assert.Equal(2, keyValue.Length); - return result; + string key = keyValue[0].Trim(); + string value = keyValue[1].Trim(); + result = result.Replace(key, value); } - public static TheoryDataSet ComplexTestData + return result; + } + + public static TheoryDataSet ComplexTestData + { + get { - get + const string address = "{\"Street\":\"NE 24th St.\",\"City\":\"Redmond\"}"; + const string subAddress = "{\"@odata.type\":\"%23NS.SubAddress\",\"Street\":\"LianHua Rd.\",\"City\":\"Shanghai\", \"Code\":9.9}"; + string[] results = { - const string address = "{\"Street\":\"NE 24th St.\",\"City\":\"Redmond\"}"; - const string subAddress = "{\"@odata.type\":\"%23NS.SubAddress\",\"Street\":\"LianHua Rd.\",\"City\":\"Shanghai\", \"Code\":9.9}"; - string[] results = - { - @"{""@odata.context"":CONTEXT,""value"":[ADDRESS,SUBADDRESS,ADDRESS,SUBADDRESS]}", - @"{""@odata.context"":CONTEXT,""value"":[ADDRESS,null,ADDRESS,null,SUBADDRESS]}", - }; + @"{""@odata.context"":CONTEXT,""value"":[ADDRESS,SUBADDRESS,ADDRESS,SUBADDRESS]}", + @"{""@odata.context"":CONTEXT,""value"":[ADDRESS,null,ADDRESS,null,SUBADDRESS]}", + }; - for (int i = 0; i< results.Length; i++) - { - results[i] = results[i].Replace("SUBADDRESS", subAddress); // first replace SUBADDRESS - results[i] = results[i].Replace("ADDRESS", address); // then replace ADDRESS - results[i] = results[i].Replace("%23", "#"); - } + for (int i = 0; i< results.Length; i++) + { + results[i] = results[i].Replace("SUBADDRESS", subAddress); // first replace SUBADDRESS + results[i] = results[i].Replace("ADDRESS", address); // then replace ADDRESS + results[i] = results[i].Replace("%23", "#"); + } - IDictionary parameters = new Dictionary - { - {"(address=@x,location=@y,addresses=@z)?@x=" + address + "&@y=" + subAddress + "&@z=[" + address + "," + subAddress + "]", results[0]}, - {"(address=@x,location=@y,addresses=@z)?@x=" + address + "&@y=null&@z=[" + address + ",null," + subAddress + "]", results[1] }, - }; + IDictionary parameters = new Dictionary + { + {"(address=@x,location=@y,addresses=@z)?@x=" + address + "&@y=" + subAddress + "&@z=[" + address + "," + subAddress + "]", results[0]}, + {"(address=@x,location=@y,addresses=@z)?@x=" + address + "&@y=null&@z=[" + address + ",null," + subAddress + "]", results[1] }, + }; - string[] modes = { "ConventionRouting", "AttributeRouting" }; - TheoryDataSet data = new TheoryDataSet(); - foreach (string mode in modes) + string[] modes = { "ConventionRouting", "AttributeRouting" }; + TheoryDataSet data = new TheoryDataSet(); + foreach (string mode in modes) + { + foreach (KeyValuePair f in parameters) { - foreach (KeyValuePair f in parameters) - { - data.Add(mode, f.Key, f.Value); - } + data.Add(mode, f.Key, f.Value); } - return data; } + return data; } + } - [Theory] - [MemberData(nameof(ComplexTestData))] - public async Task BoundFunction_Works_WithComplex_And_CollectionOfComplexParameters(string route, string parameter, string expect) - { - // Arrange - var requestUri = string.Format("{0}/Employees/Default.ComplexFunction{1}", route, parameter); + [Theory] + [MemberData(nameof(ComplexTestData))] + public async Task BoundFunction_Works_WithComplex_And_CollectionOfComplexParameters(string route, string parameter, string expect) + { + // Arrange + var requestUri = string.Format("{0}/Employees/Default.ComplexFunction{1}", route, parameter); - // Act - HttpResponseMessage response = await Client.GetAsync(requestUri); - string responseString = await response.Content.ReadAsStringAsync(); + // Act + HttpResponseMessage response = await Client.GetAsync(requestUri); + string responseString = await response.Content.ReadAsStringAsync(); - // Assert - response.EnsureSuccessStatusCode(); + // Assert + response.EnsureSuccessStatusCode(); - expect = expect.Replace("CONTEXT", String.Format("\"{0}/{1}/$metadata#Collection(NS.Address)\"", BaseAddress.ToLower(), route)); + expect = expect.Replace("CONTEXT", String.Format("\"{0}/{1}/$metadata#Collection(NS.Address)\"", BaseAddress.ToLower(), route)); - Assert.Equal(JObject.Parse(expect), JObject.Parse(responseString)); - } + Assert.Equal(JObject.Parse(expect), JObject.Parse(responseString)); + } - [Theory] - [InlineData("ConventionRouting", "(address=null,location=null,addresses=[])")] - [InlineData("AttributeRouting", "(address=null,location=null,addresses=[])")] - public async Task BoundFunction_DoesnotWork_WithNullValue_ForNonNullableComplexParameter(string route, string parameter) - { - // Arrange - var requestUri = string.Format("{0}/Employees/Default.ComplexFunction{1}", route, parameter); + [Theory] + [InlineData("ConventionRouting", "(address=null,location=null,addresses=[])")] + [InlineData("AttributeRouting", "(address=null,location=null,addresses=[])")] + public async Task BoundFunction_DoesnotWork_WithNullValue_ForNonNullableComplexParameter(string route, string parameter) + { + // Arrange + var requestUri = string.Format("{0}/Employees/Default.ComplexFunction{1}", route, parameter); - // Act - HttpResponseMessage response = await Client.GetAsync(requestUri); - string responseString = await response.Content.ReadAsStringAsync(); + // Act + HttpResponseMessage response = await Client.GetAsync(requestUri); + string responseString = await response.Content.ReadAsStringAsync(); - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); - Assert.Contains("Type verification failed. Expected non-nullable type 'NS.Address' but received a null value.", responseString); - } + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Contains("Type verification failed. Expected non-nullable type 'NS.Address' but received a null value.", responseString); + } - [Theory] - [InlineData("ConventionRouting", "(address=@p,location=null,addresses=null)?@p={\"Street\":\"NE 24th St.\",\"City\":\"Redmond\"}")] - [InlineData("AttributeRouting", "(address=@p,location=null,addresses=null)?@p={\"Street\":\"NE 24th St.\",\"City\":\"Redmond\"}")] - public async Task BoundFunction_DoesnotWork_WithNullValue_ForCollectionComplexParameter(string route, string parameter) - { - // Arrange - var requestUri = string.Format("{0}/Employees/Default.ComplexFunction{1}", route, parameter); + [Theory] + [InlineData("ConventionRouting", "(address=@p,location=null,addresses=null)?@p={\"Street\":\"NE 24th St.\",\"City\":\"Redmond\"}")] + [InlineData("AttributeRouting", "(address=@p,location=null,addresses=null)?@p={\"Street\":\"NE 24th St.\",\"City\":\"Redmond\"}")] + public async Task BoundFunction_DoesnotWork_WithNullValue_ForCollectionComplexParameter(string route, string parameter) + { + // Arrange + var requestUri = string.Format("{0}/Employees/Default.ComplexFunction{1}", route, parameter); - // Act - HttpResponseMessage response = await Client.GetAsync(requestUri); - string responseString = await response.Content.ReadAsStringAsync(); + // Act + HttpResponseMessage response = await Client.GetAsync(requestUri); + string responseString = await response.Content.ReadAsStringAsync(); - // Assert - Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); - Assert.Contains("Value cannot be null.", responseString); - } + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Contains("Value cannot be null.", responseString); + } - public static TheoryDataSet EntityTestData + public static TheoryDataSet EntityTestData + { + get { - get - { - const string person = "{\"@odata.type\":\"%23NS.Manager\",\"ID\":901,\"Name\":\"John\",\"Heads\":9}"; - const string guard = "{\"@odata.type\":\"%23NS.Employee\",\"ID\":801,\"Name\":\"Mike\"}"; - const string odataId = "{\"@odata.id\":\"BASEADDRESS/Employees(8)\"}"; + const string person = "{\"@odata.type\":\"%23NS.Manager\",\"ID\":901,\"Name\":\"John\",\"Heads\":9}"; + const string guard = "{\"@odata.type\":\"%23NS.Employee\",\"ID\":801,\"Name\":\"Mike\"}"; + const string odataId = "{\"@odata.id\":\"BASEADDRESS/Employees(8)\"}"; - string[] parameters = - { - "(person=@x,guard=@y,staff=@z)?@x=" + person + "&@y=" + guard + "&@z=[" + person + "," + guard + "]", - "(person=@x,guard=@y,staff=@z)?@x=" + guard + "&@y=null&@z=[" + guard + "," + person + "]", + string[] parameters = + { + "(person=@x,guard=@y,staff=@z)?@x=" + person + "&@y=" + guard + "&@z=[" + person + "," + guard + "]", + "(person=@x,guard=@y,staff=@z)?@x=" + guard + "&@y=null&@z=[" + guard + "," + person + "]", - // ODL doesn't work for 'null' in collection of entity. https://github.com/OData/odata.net/issues/100 - // "(person=@x,guard=@y,staff=@z)?@x=" + guard + "&@y=null&@z={\"value\":[" + guard + ",null," + person + "]}", + // ODL doesn't work for 'null' in collection of entity. https://github.com/OData/odata.net/issues/100 + // "(person=@x,guard=@y,staff=@z)?@x=" + guard + "&@y=null&@z={\"value\":[" + guard + ",null," + person + "]}", - // Entity Reference - "(person=@x,guard=@y,staff=@z)?@x=" + odataId + "&@y=null&@z=[" + odataId + "," + odataId + "]", - }; + // Entity Reference + "(person=@x,guard=@y,staff=@z)?@x=" + odataId + "&@y=null&@z=[" + odataId + "," + odataId + "]", + }; - string[] modes = { "ConventionRouting", "AttributeRouting" }; - TheoryDataSet data = new TheoryDataSet(); - foreach (string mode in modes) + string[] modes = { "ConventionRouting", "AttributeRouting" }; + TheoryDataSet data = new TheoryDataSet(); + foreach (string mode in modes) + { + foreach (var f in parameters) { - foreach (var f in parameters) - { - data.Add(mode, f); - } + data.Add(mode, f); } - return data; } + return data; } + } - [Theory] - [MemberData(nameof(EntityTestData))] - public async Task BoundFunction_Works_WithEntity_And_CollectionOfEntityParameters(string route, string parameter) - { - // Arrange - parameter = parameter.Replace("BASEADDRESS", string.Format("{0}/{1}", BaseAddress, route)); - var requestUri = string.Format("{0}/Employees/Default.EntityFunction{1}", route, parameter); + [Theory] + [MemberData(nameof(EntityTestData))] + public async Task BoundFunction_Works_WithEntity_And_CollectionOfEntityParameters(string route, string parameter) + { + // Arrange + parameter = parameter.Replace("BASEADDRESS", string.Format("{0}/{1}", BaseAddress, route)); + var requestUri = string.Format("{0}/Employees/Default.EntityFunction{1}", route, parameter); - // Act - HttpResponseMessage response = await Client.GetAsync(requestUri); + // Act + HttpResponseMessage response = await Client.GetAsync(requestUri); - // Assert - response.EnsureSuccessStatusCode(); - } - #endregion + // Assert + response.EnsureSuccessStatusCode(); + } + #endregion - #region actions + #region actions - [Theory] - [InlineData("ConventionRouting/Employees/Default.IncreaseSalary", 2)]//Convention routing - [InlineData("AttributeRouting/Employees/Default.IncreaseSalary", 1)]//Attribute routing - public async Task ActionBountToEntitySet(string url, int expectedCount) - { - // Arrange - var requestForPost = new HttpRequestMessage(HttpMethod.Post, url); - requestForPost.Content = new StringContent(@"{""Name"":""Name1""}"); - requestForPost.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - - //Act - HttpResponseMessage responseForPost = await this.Client.SendAsync(requestForPost); - await this.ResetDatasource(); - - //Assert - Assert.True(responseForPost.IsSuccessStatusCode); - var response = (await responseForPost.Content.ReadAsObject())["value"] as JArray; - Assert.Equal(expectedCount, response.Count()); - } + [Theory] + [InlineData("ConventionRouting/Employees/Default.IncreaseSalary", 2)]//Convention routing + [InlineData("AttributeRouting/Employees/Default.IncreaseSalary", 1)]//Attribute routing + public async Task ActionBountToEntitySet(string url, int expectedCount) + { + // Arrange + var requestForPost = new HttpRequestMessage(HttpMethod.Post, url); + requestForPost.Content = new StringContent(@"{""Name"":""Name1""}"); + requestForPost.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + //Act + HttpResponseMessage responseForPost = await this.Client.SendAsync(requestForPost); + await this.ResetDatasource(); + + //Assert + Assert.True(responseForPost.IsSuccessStatusCode); + var response = (await responseForPost.Content.ReadAsObject())["value"] as JArray; + Assert.Equal(expectedCount, response.Count()); + } - [Theory] - [InlineData("ConventionRouting/Employees/NS.Manager/Default.IncreaseSalary", 5)]//Convention routing - [InlineData("AttributeRouting/Employees/NS.Manager/Default.IncreaseSalary", 2)]//Attribute routing - public async Task ActionBountToEntitySetForDerivedBindingType(string url, int expectedCount) - { - // Arrange - var requestForPost = new HttpRequestMessage(HttpMethod.Post, url); - requestForPost.Content = new StringContent(@"{""Name"":""Name""}"); - requestForPost.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - - //Act - HttpResponseMessage responseForPost = await this.Client.SendAsync(requestForPost); - await this.ResetDatasource(); - - //Assert - Assert.True(responseForPost.IsSuccessStatusCode); - var response = (await responseForPost.Content.ReadAsObject())["value"] as JArray; - Assert.Equal(expectedCount, response.Count()); - } + [Theory] + [InlineData("ConventionRouting/Employees/NS.Manager/Default.IncreaseSalary", 5)]//Convention routing + [InlineData("AttributeRouting/Employees/NS.Manager/Default.IncreaseSalary", 2)]//Attribute routing + public async Task ActionBountToEntitySetForDerivedBindingType(string url, int expectedCount) + { + // Arrange + var requestForPost = new HttpRequestMessage(HttpMethod.Post, url); + requestForPost.Content = new StringContent(@"{""Name"":""Name""}"); + requestForPost.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + //Act + HttpResponseMessage responseForPost = await this.Client.SendAsync(requestForPost); + await this.ResetDatasource(); + + //Assert + Assert.True(responseForPost.IsSuccessStatusCode); + var response = (await responseForPost.Content.ReadAsObject())["value"] as JArray; + Assert.Equal(expectedCount, response.Count()); + } - [Theory] - [InlineData("ConventionRouting/Employees/Default.IncreaseSalary", 3)]//Convention routing - [InlineData("AttributeRouting/Employees/Default.IncreaseSalary", 1)]//Attribute routing - public async Task ActionFollowedByQueryOption(string url, int expectedCount) - { - // Arrange - var requestUri = $"{url}?$filter=ID mod 4 eq 0"; - string payload = @"{""Name"":""Name""}"; - var requestForPost = new HttpRequestMessage(HttpMethod.Post, requestUri); - requestForPost.Content = new StringContent(payload); - requestForPost.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - requestForPost.Content.Headers.ContentLength = payload.Length; - - //Act - HttpResponseMessage responseForPost = await this.Client.SendAsync(requestForPost); - await this.ResetDatasource(); - - //Assert - Assert.True(responseForPost.IsSuccessStatusCode); - var response = (await responseForPost.Content.ReadAsObject())["value"] as JArray; - Assert.Equal(expectedCount, response.Count()); - } + [Theory] + [InlineData("ConventionRouting/Employees/Default.IncreaseSalary", 3)]//Convention routing + [InlineData("AttributeRouting/Employees/Default.IncreaseSalary", 1)]//Attribute routing + public async Task ActionFollowedByQueryOption(string url, int expectedCount) + { + // Arrange + var requestUri = $"{url}?$filter=ID mod 4 eq 0"; + string payload = @"{""Name"":""Name""}"; + var requestForPost = new HttpRequestMessage(HttpMethod.Post, requestUri); + requestForPost.Content = new StringContent(payload); + requestForPost.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + requestForPost.Content.Headers.ContentLength = payload.Length; + + //Act + HttpResponseMessage responseForPost = await this.Client.SendAsync(requestForPost); + await this.ResetDatasource(); + + //Assert + Assert.True(responseForPost.IsSuccessStatusCode); + var response = (await responseForPost.Content.ReadAsObject())["value"] as JArray; + Assert.Equal(expectedCount, response.Count()); + } - [Theory] - [InlineData("ConventionRouting/Employees(1)/Default.IncreaseSalary", 20)]//Convention routing - [InlineData("AttributeRouting/Employees(1)/Default.IncreaseSalary", 40)]//Attribute routing - public async Task ActionBountToBaseEntityType(string url, int expectedCount) - { - // Arrange - var requestForPost = new HttpRequestMessage(HttpMethod.Post, url); + [Theory] + [InlineData("ConventionRouting/Employees(1)/Default.IncreaseSalary", 20)]//Convention routing + [InlineData("AttributeRouting/Employees(1)/Default.IncreaseSalary", 40)]//Attribute routing + public async Task ActionBountToBaseEntityType(string url, int expectedCount) + { + // Arrange + var requestForPost = new HttpRequestMessage(HttpMethod.Post, url); - //Act - HttpResponseMessage responseForPost = await this.Client.SendAsync(requestForPost); - await this.ResetDatasource(); + //Act + HttpResponseMessage responseForPost = await this.Client.SendAsync(requestForPost); + await this.ResetDatasource(); - string responseString = await responseForPost.Content.ReadAsStringAsync(); + string responseString = await responseForPost.Content.ReadAsStringAsync(); - // Assert - Assert.True(responseForPost.IsSuccessStatusCode); - Assert.Contains("/$metadata#Edm.Int32", responseString); - Assert.Contains(@"""value"":" + expectedCount, responseString); - } + // Assert + Assert.True(responseForPost.IsSuccessStatusCode); + Assert.Contains("/$metadata#Edm.Int32", responseString); + Assert.Contains(@"""value"":" + expectedCount, responseString); + } - [Theory] - [InlineData("ConventionRouting/Employees(1)/NS.Manager/Default.IncreaseSalary", 20)]//Convention routing - [InlineData("AttributeRouting/Employees(1)/NS.Manager/Default.IncreaseSalary", 40)]//Attribute routing - public async Task ActionBountToDerivedEntityType(string url, int expectedCount) - { - // Arrange - var requestForPost = new HttpRequestMessage(HttpMethod.Post, url); - - //Act - HttpResponseMessage responseForPost = await this.Client.SendAsync(requestForPost); - await this.ResetDatasource(); - string responseString = await responseForPost.Content.ReadAsStringAsync(); - - // Assert - Assert.True(responseForPost.IsSuccessStatusCode); - Assert.Contains("/$metadata#Edm.Int32", responseString); - Assert.Contains(@"""value"":" + expectedCount, responseString); - } + [Theory] + [InlineData("ConventionRouting/Employees(1)/NS.Manager/Default.IncreaseSalary", 20)]//Convention routing + [InlineData("AttributeRouting/Employees(1)/NS.Manager/Default.IncreaseSalary", 40)]//Attribute routing + public async Task ActionBountToDerivedEntityType(string url, int expectedCount) + { + // Arrange + var requestForPost = new HttpRequestMessage(HttpMethod.Post, url); + + //Act + HttpResponseMessage responseForPost = await this.Client.SendAsync(requestForPost); + await this.ResetDatasource(); + string responseString = await responseForPost.Content.ReadAsStringAsync(); + + // Assert + Assert.True(responseForPost.IsSuccessStatusCode); + Assert.Contains("/$metadata#Edm.Int32", responseString); + Assert.Contains(@"""value"":" + expectedCount, responseString); + } - [Theory] - [InlineData("ConventionRouting")] - [InlineData("AttributeRouting")] - public async Task BoundAction_Works_WithPrimitive_And_CollectionOfPrimitiveParameters(string route) - { - // Arrange - var requestUri = $"{route}/Employees/Default.PrimitiveAction"; - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - - string payload = @"{ - ""param"": 7, - ""price"": 9.9, - ""name"": ""Tony"", - ""names"": [ ""Mike"", null, ""John""] - }"; - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; - - // Act - HttpResponseMessage response = await Client.SendAsync(request); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - response.EnsureSuccessStatusCode(); - - Assert.Contains($"/{route}/$metadata#Edm.Boolean\",\"value\":true", responseString); - } + [Theory] + [InlineData("ConventionRouting")] + [InlineData("AttributeRouting")] + public async Task BoundAction_Works_WithPrimitive_And_CollectionOfPrimitiveParameters(string route) + { + // Arrange + var requestUri = $"{route}/Employees/Default.PrimitiveAction"; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + + string payload = @"{ + ""param"": 7, + ""price"": 9.9, + ""name"": ""Tony"", + ""names"": [ ""Mike"", null, ""John""] + }"; + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + + // Act + HttpResponseMessage response = await Client.SendAsync(request); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + response.EnsureSuccessStatusCode(); + + Assert.Contains($"/{route}/$metadata#Edm.Boolean\",\"value\":true", responseString); + } - [Theory] - [InlineData("ConventionRouting")] - [InlineData("AttributeRouting")] - public async Task BoundAction_Works_WithEnum_And_CollectionOfEnumParameters(string route) - { - // Arrange - var requestUri = $"{route}/Employees/Default.EnumAction"; - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - - string payload = @"{ - ""bkColor"": ""Red"", - ""frColor"": ""Green"", - ""colors"": [""Red"", ""Blue""] - }"; - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; - - // Act - HttpResponseMessage response = await Client.SendAsync(request); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - response.EnsureSuccessStatusCode(); - - Assert.Contains($"/{route}/$metadata#Edm.Boolean\",\"value\":true", responseString); - } + [Theory] + [InlineData("ConventionRouting")] + [InlineData("AttributeRouting")] + public async Task BoundAction_Works_WithEnum_And_CollectionOfEnumParameters(string route) + { + // Arrange + var requestUri = $"{route}/Employees/Default.EnumAction"; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + + string payload = @"{ + ""bkColor"": ""Red"", + ""frColor"": ""Green"", + ""colors"": [""Red"", ""Blue""] + }"; + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + + // Act + HttpResponseMessage response = await Client.SendAsync(request); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + response.EnsureSuccessStatusCode(); + + Assert.Contains($"/{route}/$metadata#Edm.Boolean\",\"value\":true", responseString); + } - [Theory] - [InlineData("ConventionRouting")] - [InlineData("AttributeRouting")] - public async Task BoundAction_Works_WithComplex_And_CollectionOfComplexParameters(string route) - { - // Arrange - var requestUri = $"{route}/Employees/Default.ComplexAction"; - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + [Theory] + [InlineData("ConventionRouting")] + [InlineData("AttributeRouting")] + public async Task BoundAction_Works_WithComplex_And_CollectionOfComplexParameters(string route) + { + // Arrange + var requestUri = $"{route}/Employees/Default.ComplexAction"; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - string payload = @"{ - ""address"": {""Street"":""NE 24th St."",""City"":""Redmond""}, - ""location"": {""@odata.type"":""#NS.SubAddress"",""Street"":""LianHua Rd."",""City"":""Shanghai"", ""Code"":9.9}, - ""addresses"": [{""Street"":""NE 24th St."",""City"":""Redmond""}, {""@odata.type"":""#NS.SubAddress"",""Street"":""LianHua Rd."",""City"":""Shanghai"", ""Code"":9.9}] - }"; + string payload = @"{ + ""address"": {""Street"":""NE 24th St."",""City"":""Redmond""}, + ""location"": {""@odata.type"":""#NS.SubAddress"",""Street"":""LianHua Rd."",""City"":""Shanghai"", ""Code"":9.9}, + ""addresses"": [{""Street"":""NE 24th St."",""City"":""Redmond""}, {""@odata.type"":""#NS.SubAddress"",""Street"":""LianHua Rd."",""City"":""Shanghai"", ""Code"":9.9}] + }"; - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; - // Act - HttpResponseMessage response = await Client.SendAsync(request); - string responseString = await response.Content.ReadAsStringAsync(); + // Act + HttpResponseMessage response = await Client.SendAsync(request); + string responseString = await response.Content.ReadAsStringAsync(); - // Assert - response.EnsureSuccessStatusCode(); + // Assert + response.EnsureSuccessStatusCode(); - Assert.Contains($"/{route}/$metadata#Edm.Boolean\",\"value\":true", responseString); - } + Assert.Contains($"/{route}/$metadata#Edm.Boolean\",\"value\":true", responseString); + } - [Theory] - [InlineData("ConventionRouting")] - [InlineData("AttributeRouting")] - public async Task BoundAction_Works_WithEntity_And_CollectionOfEntityParameters(string route) - { - // Arrange - var requestUri = $"{route}/Employees/Default.EntityAction"; - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + [Theory] + [InlineData("ConventionRouting")] + [InlineData("AttributeRouting")] + public async Task BoundAction_Works_WithEntity_And_CollectionOfEntityParameters(string route) + { + // Arrange + var requestUri = $"{route}/Employees/Default.EntityAction"; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - string payload = @"{ - ""person"": {""@odata.type"":""#NS.Employee"",""ID"":801,""Name"":""Mike""}, - ""guard"": {""@odata.type"":""#NS.Manager"",""ID"":901,""Name"":""John"", ""Heads"":9}, - ""staff"": [{""@odata.type"":""#NS.Employee"",""ID"":801,""Name"":""Mike""}, {""@odata.type"":""#NS.Manager"",""ID"":901,""Name"":""John"", ""Heads"":9}] - }"; + string payload = @"{ + ""person"": {""@odata.type"":""#NS.Employee"",""ID"":801,""Name"":""Mike""}, + ""guard"": {""@odata.type"":""#NS.Manager"",""ID"":901,""Name"":""John"", ""Heads"":9}, + ""staff"": [{""@odata.type"":""#NS.Employee"",""ID"":801,""Name"":""Mike""}, {""@odata.type"":""#NS.Manager"",""ID"":901,""Name"":""John"", ""Heads"":9}] + }"; - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; - // Act - HttpResponseMessage response = await Client.SendAsync(request); - string responseString = await response.Content.ReadAsStringAsync(); + // Act + HttpResponseMessage response = await Client.SendAsync(request); + string responseString = await response.Content.ReadAsStringAsync(); - // Assert - response.EnsureSuccessStatusCode(); + // Assert + response.EnsureSuccessStatusCode(); - Assert.Contains($"/{route}/$metadata#Edm.Boolean\",\"value\":true", responseString); - } - */ - #endregion + Assert.Contains($"/{route}/$metadata#Edm.Boolean\",\"value\":true", responseString); } + */ + #endregion +} - public class MyAttributeRoutingConvention : IODataControllerActionConvention - { - /// - /// Metadata's order is 0, make a order a little bit bigger than 0 can make metadata routing convention go - /// - public int Order => 10; - - public bool AppliesToAction(ODataControllerActionContext context) - { - if (context.Prefix == "AttributeRouting") - { - // stop all others - return true; - } - - return false; - } +public class MyAttributeRoutingConvention : IODataControllerActionConvention +{ + /// + /// Metadata's order is 0, make a order a little bit bigger than 0 can make metadata routing convention go + /// + public int Order => 10; - public bool AppliesToController(ODataControllerActionContext context) + public bool AppliesToAction(ODataControllerActionContext context) + { + if (context.Prefix == "AttributeRouting") { + // stop all others return true; } + + return false; + } + + public bool AppliesToController(ODataControllerActionContext context) + { + return true; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationDataModel.cs index adb8ad4e5..d9ca8d9a1 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationDataModel.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -9,34 +9,33 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Microsoft.AspNetCore.OData.E2E.Tests.BulkOperation +namespace Microsoft.AspNetCore.OData.E2E.Tests.BulkOperation; + +internal class BulkOperationDataModel { - internal class BulkOperationDataModel + public class Employee + { + [Key] + public int ID { get; set; } + public String Name { get; set; } + public List Friends { get; set; } + } + + public class Friend + { + [Key] + public int Id { get; set; } + + public string Name { get; set; } + + public List Orders { get; set; } + } + + public class Order { - public class Employee - { - [Key] - public int ID { get; set; } - public String Name { get; set; } - public List Friends { get; set; } - } - - public class Friend - { - [Key] - public int Id { get; set; } - - public string Name { get; set; } - - public List Orders { get; set; } - } - - public class Order - { - [Key] - public int Id { get; set; } - - public int Price { get; set; } - } + [Key] + public int Id { get; set; } + + public int Price { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationEdmModel.cs index f6e738aae..206f25496 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationEdmModel.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -9,24 +9,23 @@ using Microsoft.OData.ModelBuilder; using static Microsoft.AspNetCore.OData.E2E.Tests.BulkOperation.BulkOperationDataModel; -namespace Microsoft.AspNetCore.OData.E2E.Tests.BulkOperation +namespace Microsoft.AspNetCore.OData.E2E.Tests.BulkOperation; + +internal class BulkOperationEdmModel { - internal class BulkOperationEdmModel + public static IEdmModel GetConventionModel() { - public static IEdmModel GetConventionModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Employees"); - builder.EntitySet("Friends"); - builder.EntitySet("Orders"); + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Employees"); + builder.EntitySet("Friends"); + builder.EntitySet("Orders"); - builder.Namespace = typeof(Employee).Namespace; - builder.MaxDataServiceVersion = EdmConstants.EdmVersion401; - builder.DataServiceVersion = EdmConstants.EdmVersion401; + builder.Namespace = typeof(Employee).Namespace; + builder.MaxDataServiceVersion = EdmConstants.EdmVersion401; + builder.DataServiceVersion = EdmConstants.EdmVersion401; - var edmModel = builder.GetEdmModel(); + var edmModel = builder.GetEdmModel(); - return edmModel; - } + return edmModel; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs index 8a214bd46..2f31510d6 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/BulkOperationTest.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -13,80 +13,79 @@ using System.Threading.Tasks; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.BulkOperation +namespace Microsoft.AspNetCore.OData.E2E.Tests.BulkOperation; + +public class BulkOperationTest : WebApiTestBase { - public class BulkOperationTest : WebApiTestBase + public BulkOperationTest(WebApiTestFixture fixture) + : base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(EmployeesController)); + + var edmModel1 = BulkOperationEdmModel.GetConventionModel(); + + services.AddControllers().AddOData(opt => opt.AddRouteComponents("convention", edmModel1).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); + } + + [Fact] + public async Task DeltaSet_WithNestedFriends_WithNestedOrders_IsSerializedSuccessfully() { - public BulkOperationTest(WebApiTestFixture fixture) - : base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(EmployeesController)); - - var edmModel1 = BulkOperationEdmModel.GetConventionModel(); - - services.AddControllers().AddOData(opt => opt.AddRouteComponents("convention", edmModel1).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); - } - - [Fact] - public async Task DeltaSet_WithNestedFriends_WithNestedOrders_IsSerializedSuccessfully() - { - //Arrange - string requestUri = "/convention/Employees"; - - HttpClient client = CreateClient(); - - var content = @"{ - '@context':'http://host/service/$metadata#Employees/$delta', - 'value':[ - {'ID':1,'Name':'Employee1','Friends@delta':[{'Id':1,'Name':'Friend1','Orders@delta':[{'Id':1,'Price': 10},{'Id':2,'Price':20} ]},{'Id':2,'Name':'Friend2'}]}, - {'ID':2,'Name':'Employee2','Friends@delta':[{'Id':3,'Name':'Friend3','Orders@delta':[{'Id':3,'Price': 30}, {'Id':4,'Price':40} ]},{'Id':4,'Name':'Friend4'}]} - ]}"; - - string expectedResponse = "{" + - "\"@context\":\"http://localhost/convention/$metadata#Employees/$delta\"," + - "\"value\":[" + - "{\"ID\":1,\"Name\":\"Employee1\",\"Friends@delta\":[{\"Id\":1,\"Name\":\"Friend1\",\"Orders@delta\":[{\"Id\":1,\"Price\":10},{\"Id\":2,\"Price\":20}]},{\"Id\":2,\"Name\":\"Friend2\"}]}," + - "{\"ID\":2,\"Name\":\"Employee2\",\"Friends@delta\":[{\"Id\":3,\"Name\":\"Friend3\",\"Orders@delta\":[{\"Id\":3,\"Price\":30},{\"Id\":4,\"Price\":40}]},{\"Id\":4,\"Name\":\"Friend4\"}]}]}"; - - StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); - - // Act & Assert - using HttpResponseMessage response = await client.PatchAsync(requestUri, stringContent); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var json = response.Content.ReadAsStringAsync().Result; - Assert.Equal(expectedResponse.ToString().ToLower(), json.ToString().ToLower()); - } - - [Fact] - public async Task DeltaSet_WithDeletedAndODataId_IsSerializedSuccessfully() - { - //Arrange - string requestUri = "/convention/Employees"; - HttpClient client = CreateClient(); - - var content = @"{ - '@context':'http://host/service/$metadata#Employees/$delta', - 'value':[ - {'ID':1,'Name':'Employee1','Friends@odata.delta':[{'@removed':{'reason':'changed'},'Id':1}]}, - {'ID':2,'Name':'Employee2','Friends@odata.delta':[{'@id':'Friends(1)'}]} - ]}"; - - string expectedResponse = "{\"@context\":\"http://localhost/convention/$metadata#Employees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"Friends@delta\":[{\"@removed\":{\"reason\":\"changed\"},\"@id\":\"http://host/service/Friends(1)\"}]},{\"ID\":2,\"Name\":\"Employee2\",\"Friends@delta\":[{\"Id\":1}]}]}"; - - var requestForUpdate = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - - StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); - requestForUpdate.Content = stringContent; - - //Act & Assert - using HttpResponseMessage response = await client.SendAsync(requestForUpdate); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var json = response.Content.ReadAsStringAsync().Result; - Assert.Equal(expectedResponse.ToString().ToLower(), json.ToString().ToLower()); - } + //Arrange + string requestUri = "/convention/Employees"; + + HttpClient client = CreateClient(); + + var content = @"{ + '@context':'http://host/service/$metadata#Employees/$delta', + 'value':[ + {'ID':1,'Name':'Employee1','Friends@delta':[{'Id':1,'Name':'Friend1','Orders@delta':[{'Id':1,'Price': 10},{'Id':2,'Price':20} ]},{'Id':2,'Name':'Friend2'}]}, + {'ID':2,'Name':'Employee2','Friends@delta':[{'Id':3,'Name':'Friend3','Orders@delta':[{'Id':3,'Price': 30}, {'Id':4,'Price':40} ]},{'Id':4,'Name':'Friend4'}]} + ]}"; + + string expectedResponse = "{" + + "\"@context\":\"http://localhost/convention/$metadata#Employees/$delta\"," + + "\"value\":[" + + "{\"ID\":1,\"Name\":\"Employee1\",\"Friends@delta\":[{\"Id\":1,\"Name\":\"Friend1\",\"Orders@delta\":[{\"Id\":1,\"Price\":10},{\"Id\":2,\"Price\":20}]},{\"Id\":2,\"Name\":\"Friend2\"}]}," + + "{\"ID\":2,\"Name\":\"Employee2\",\"Friends@delta\":[{\"Id\":3,\"Name\":\"Friend3\",\"Orders@delta\":[{\"Id\":3,\"Price\":30},{\"Id\":4,\"Price\":40}]},{\"Id\":4,\"Name\":\"Friend4\"}]}]}"; + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + + // Act & Assert + using HttpResponseMessage response = await client.PatchAsync(requestUri, stringContent); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(expectedResponse.ToString().ToLower(), json.ToString().ToLower()); + } + + [Fact] + public async Task DeltaSet_WithDeletedAndODataId_IsSerializedSuccessfully() + { + //Arrange + string requestUri = "/convention/Employees"; + HttpClient client = CreateClient(); + + var content = @"{ + '@context':'http://host/service/$metadata#Employees/$delta', + 'value':[ + {'ID':1,'Name':'Employee1','Friends@odata.delta':[{'@removed':{'reason':'changed'},'Id':1}]}, + {'ID':2,'Name':'Employee2','Friends@odata.delta':[{'@id':'Friends(1)'}]} + ]}"; + + string expectedResponse = "{\"@context\":\"http://localhost/convention/$metadata#Employees/$delta\",\"value\":[{\"ID\":1,\"Name\":\"Employee1\",\"Friends@delta\":[{\"@removed\":{\"reason\":\"changed\"},\"@id\":\"http://host/service/Friends(1)\"}]},{\"ID\":2,\"Name\":\"Employee2\",\"Friends@delta\":[{\"Id\":1}]}]}"; + + var requestForUpdate = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + requestForUpdate.Content = stringContent; + + //Act & Assert + using HttpResponseMessage response = await client.SendAsync(requestForUpdate); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var json = response.Content.ReadAsStringAsync().Result; + Assert.Equal(expectedResponse.ToString().ToLower(), json.ToString().ToLower()); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/EmployeesController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/EmployeesController.cs index c5a5b3c2d..c88d8ed6a 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/EmployeesController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/BulkOperation/EmployeesController.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -13,84 +13,83 @@ using Xunit; using static Microsoft.AspNetCore.OData.E2E.Tests.BulkOperation.BulkOperationDataModel; -namespace Microsoft.AspNetCore.OData.E2E.Tests.BulkOperation +namespace Microsoft.AspNetCore.OData.E2E.Tests.BulkOperation; + +internal class EmployeesController : ODataController { - internal class EmployeesController : ODataController + public EmployeesController() { - public EmployeesController() + if (null == Employees) { - if (null == Employees) - { - InitEmployees(); - } + InitEmployees(); } + } - /// - /// static so that the data is shared among requests. - /// - public static IList Employees = null; + /// + /// static so that the data is shared among requests. + /// + public static IList Employees = null; - private List Friends = null; + private List Friends = null; - private void InitEmployees() + private void InitEmployees() + { + Friends = new List { - Friends = new List + new Friend { - new Friend - { - Id = 1, - Name = "Test0" - }, - new Friend + Id = 1, + Name = "Test0" + }, + new Friend + { + Id = 2, + Name = "Test1", + Orders = new List() { - Id = 2, - Name = "Test1", - Orders = new List() + new Order { - new Order - { - Id = 1, - Price = 2 - } - } - }, - new Friend - { - Id = 3, - Name = "Test3" - }, - new Friend - { - Id = 4, - Name = "Test4" + Id = 1, + Price = 2 + } } - }; - Employees = new List + }, + new Friend { - new Employee() - { - ID=1, - Name="Name1", - Friends = this.Friends.Where(x=>x.Id ==1 || x.Id==2).ToList() - }, - new Employee() - { - ID=2,Name="Name2", - Friends = this.Friends.Where(x=>x.Id ==3 || x.Id==4).ToList() - }, - new Employee() - { - ID=3, - Name="Name3" - }, - }; - } - - [HttpPatch] - public IActionResult PatchEmployees([FromBody] DeltaSet coll) + Id = 3, + Name = "Test3" + }, + new Friend + { + Id = 4, + Name = "Test4" + } + }; + Employees = new List { - Assert.NotNull(coll); - return Ok(coll); - } + new Employee() + { + ID=1, + Name="Name1", + Friends = this.Friends.Where(x=>x.Id ==1 || x.Id==2).ToList() + }, + new Employee() + { + ID=2,Name="Name2", + Friends = this.Friends.Where(x=>x.Id ==3 || x.Id==4).ToList() + }, + new Employee() + { + ID=3, + Name="Name3" + }, + }; + } + + [HttpPatch] + public IActionResult PatchEmployees([FromBody] DeltaSet coll) + { + Assert.NotNull(coll); + return Ok(coll); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastContext.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastContext.cs index 538b47d84..fb80f5c05 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastContext.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastContext.cs @@ -13,153 +13,152 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Cast +namespace Microsoft.AspNetCore.OData.E2E.Tests.Cast; + +public class DataSource { - public class DataSource - { - private static IQueryable _products = null; + private static IQueryable _products = null; - public static IQueryable InMemoryProducts + public static IQueryable InMemoryProducts + { + get { - get + if (_products == null) { - if (_products == null) + _products = new List() { - _products = new List() + new Product() + { + ID=1, + Name="Name1", + Domain=Domain.Military, + Weight=1.1, + DimensionInCentimeter=new List{1,2,3}, + ManufacturingDate=new DateTimeOffset(2011,1,1,0,0,0,TimeSpan.FromHours(8)), + }, + new Product() + { + ID=2, + Name="Name2", + Domain=Domain.Civil, + Weight=2.2, + DimensionInCentimeter=new List{2,3,4}, + ManufacturingDate=new DateTimeOffset(2012,1,1,0,0,0,TimeSpan.FromHours(8)), + }, + new Product() + { + ID=3, + Name="Name3", + Domain=Domain.Both, + Weight=3.3, + DimensionInCentimeter=new List{3,4,5}, + ManufacturingDate=new DateTimeOffset(2013,1,1,0,0,0,TimeSpan.FromHours(8)), + }, + new AirPlane() + { + ID=4, + Name="Name4", + Domain=Domain.Both, + Weight=4.4, + DimensionInCentimeter=new List{4,5,6}, + ManufacturingDate=new DateTimeOffset(2013,1,1,0,0,0,TimeSpan.FromHours(8)), + Speed=100 + }, + new JetPlane() { - new Product() - { - ID=1, - Name="Name1", - Domain=Domain.Military, - Weight=1.1, - DimensionInCentimeter=new List{1,2,3}, - ManufacturingDate=new DateTimeOffset(2011,1,1,0,0,0,TimeSpan.FromHours(8)), - }, - new Product() - { - ID=2, - Name="Name2", - Domain=Domain.Civil, - Weight=2.2, - DimensionInCentimeter=new List{2,3,4}, - ManufacturingDate=new DateTimeOffset(2012,1,1,0,0,0,TimeSpan.FromHours(8)), - }, - new Product() - { - ID=3, - Name="Name3", - Domain=Domain.Both, - Weight=3.3, - DimensionInCentimeter=new List{3,4,5}, - ManufacturingDate=new DateTimeOffset(2013,1,1,0,0,0,TimeSpan.FromHours(8)), - }, - new AirPlane() - { - ID=4, - Name="Name4", - Domain=Domain.Both, - Weight=4.4, - DimensionInCentimeter=new List{4,5,6}, - ManufacturingDate=new DateTimeOffset(2013,1,1,0,0,0,TimeSpan.FromHours(8)), - Speed=100 - }, - new JetPlane() - { - ID=5, - Name="Name5", - Domain=Domain.Military, - Weight=5.5, - DimensionInCentimeter=new List{6,7,8}, - ManufacturingDate=new DateTimeOffset(2013,1,1,0,0,0,TimeSpan.FromHours(8)), - Speed=100, - Company="Boeing" - }, - new JetPlane() - { - ID=6, - Name="Name6", - Domain=Domain.Civil, - Weight=6.6, - DimensionInCentimeter=new List{7,8,9}, - ManufacturingDate=new DateTimeOffset(2013,1,1,0,0,0,TimeSpan.FromHours(8)), - Speed=500, - Company="AirBus" - }, - - }.AsQueryable(); - } - return _products; + ID=5, + Name="Name5", + Domain=Domain.Military, + Weight=5.5, + DimensionInCentimeter=new List{6,7,8}, + ManufacturingDate=new DateTimeOffset(2013,1,1,0,0,0,TimeSpan.FromHours(8)), + Speed=100, + Company="Boeing" + }, + new JetPlane() + { + ID=6, + Name="Name6", + Domain=Domain.Civil, + Weight=6.6, + DimensionInCentimeter=new List{7,8,9}, + ManufacturingDate=new DateTimeOffset(2013,1,1,0,0,0,TimeSpan.FromHours(8)), + Speed=500, + Company="AirBus" + }, + + }.AsQueryable(); } + return _products; } - - //public static IQueryable EfProducts - //{ - // get - // { - // if (_context == null) - // { - // Database.SetInitializer(new DropCreateDatabaseAlways()); - // string databaseName = "CastTest_" + DateTime.Now.Ticks.ToString(); - // string connectionString = string.Format(@"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog={0}", databaseName); - - // _context = new ProductsContext(); - // foreach (Product product in DataSource.InMemoryProducts) - // { - // _context.Products.Add(product); - // } - - // _context.SaveChanges(); - // } - - // return _context.Products; - // } - //} } - public class ProductsContext : DbContext + //public static IQueryable EfProducts + //{ + // get + // { + // if (_context == null) + // { + // Database.SetInitializer(new DropCreateDatabaseAlways()); + // string databaseName = "CastTest_" + DateTime.Now.Ticks.ToString(); + // string connectionString = string.Format(@"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog={0}", databaseName); + + // _context = new ProductsContext(); + // foreach (Product product in DataSource.InMemoryProducts) + // { + // _context.Products.Add(product); + // } + + // _context.SaveChanges(); + // } + + // return _context.Products; + // } + //} +} + +public class ProductsContext : DbContext +{ + public ProductsContext(DbContextOptions options) + : base(options) { - public ProductsContext(DbContextOptions options) - : base(options) - { - } + } - public DbSet Products { get; set; } + public DbSet Products { get; set; } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - var splitStringConverter1 = new ValueConverter, string>(v => string.Join(";", v), v => v.Split(new[] { ';' })); + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var splitStringConverter1 = new ValueConverter, string>(v => string.Join(";", v), v => v.Split(new[] { ';' })); - Func> convertFrom = a => + Func> convertFrom = a => + { + var items = a.Split(new[] { ';' }); + IList values = new List(); + foreach (var item in items) { - var items = a.Split(new[] { ';' }); - IList values = new List(); - foreach (var item in items) - { - values.Add(Int32.Parse(item)); - } + values.Add(Int32.Parse(item)); + } - return values; - }; + return values; + }; - var splitStringConverter = new ValueConverter, string>(v => string.Join(";", v), FuncToExpression(convertFrom)); - modelBuilder.Entity().Property(nameof(Product.DimensionInCentimeter)).HasConversion(splitStringConverter); + var splitStringConverter = new ValueConverter, string>(v => string.Join(";", v), FuncToExpression(convertFrom)); + modelBuilder.Entity().Property(nameof(Product.DimensionInCentimeter)).HasConversion(splitStringConverter); - modelBuilder.Entity().HasBaseType(); - modelBuilder.Entity().HasBaseType(); - } + modelBuilder.Entity().HasBaseType(); + modelBuilder.Entity().HasBaseType(); + } - private static Expression>> FuncToExpression(Func> f) - { - return x => f(x); - } + private static Expression>> FuncToExpression(Func> f) + { + return x => f(x); + } - //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - //{ - // string databaseName = "CastTest_" + DateTime.Now.Ticks.ToString(); - // string connectionString = string.Format(@"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog={0}", databaseName); + //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + //{ + // string databaseName = "CastTest_" + DateTime.Now.Ticks.ToString(); + // string connectionString = string.Format(@"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog={0}", databaseName); - // optionsBuilder.UseInMemoryDatabase("Data Source=blogging.db"); - //} - } + // optionsBuilder.UseInMemoryDatabase("Data Source=blogging.db"); + //} } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastControllers.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastControllers.cs index ee558a8c4..51ec3feaa 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastControllers.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastControllers.cs @@ -12,60 +12,59 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Cast +namespace Microsoft.AspNetCore.OData.E2E.Tests.Cast; + +public class ProductsController : ODataController { - public class ProductsController : ODataController + private ProductsContext _context; + + public ProductsController(ProductsContext context) { - private ProductsContext _context; + context.Database.EnsureCreated(); + _context = context; - public ProductsController(ProductsContext context) + if (!_context.Products.Any()) { - context.Database.EnsureCreated(); - _context = context; - - if (!_context.Products.Any()) + foreach (Product product in DataSource.InMemoryProducts) { - foreach (Product product in DataSource.InMemoryProducts) - { - _context.Products.Add(product); - } - - _context.SaveChanges(); + _context.Products.Add(product); } + + _context.SaveChanges(); } + } - [EnableQuery] - public IActionResult Get() + [EnableQuery] + public IActionResult Get() + { + if (GetRoutePrefix() == "EF") { - if (GetRoutePrefix() == "EF") - { - return Ok(_context.Products); - } - else - { - return Ok(DataSource.InMemoryProducts); - } + return Ok(_context.Products); } - - [EnableQuery] - public IActionResult GetDimensionInCentimeter(int key) + else { - if (GetRoutePrefix() == "EF") - { - Product product = _context.Products.Single(p => p.ID == key); - return Ok(product.DimensionInCentimeter); - } - else - { - Product product = DataSource.InMemoryProducts.Single(p => p.ID == key); - return Ok(product.DimensionInCentimeter); - } + return Ok(DataSource.InMemoryProducts); } + } - protected string GetRoutePrefix() + [EnableQuery] + public IActionResult GetDimensionInCentimeter(int key) + { + if (GetRoutePrefix() == "EF") { - IODataFeature feature = Request.ODataFeature(); - return feature.RoutePrefix; + Product product = _context.Products.Single(p => p.ID == key); + return Ok(product.DimensionInCentimeter); } + else + { + Product product = DataSource.InMemoryProducts.Single(p => p.ID == key); + return Ok(product.DimensionInCentimeter); + } + } + + protected string GetRoutePrefix() + { + IODataFeature feature = Request.ODataFeature(); + return feature.RoutePrefix; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastDataModel.cs index e8a5eec5b..1b532a7c4 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastDataModel.cs @@ -9,41 +9,40 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Cast +namespace Microsoft.AspNetCore.OData.E2E.Tests.Cast; + +public class Product { - public class Product - { - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public int ID { get; set; } + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int ID { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public Domain Domain { get; set; } + public Domain Domain { get; set; } - public Double Weight { get; set; } + public Double Weight { get; set; } - public IList DimensionInCentimeter { get; set; } + public IList DimensionInCentimeter { get; set; } - public DateTimeOffset ManufacturingDate { get; set; } - } + public DateTimeOffset ManufacturingDate { get; set; } +} - [Flags] - public enum Domain - { - Military = 1, +[Flags] +public enum Domain +{ + Military = 1, - Civil = 2, + Civil = 2, - Both = 3, - } + Both = 3, +} - public class AirPlane : Product - { - public int Speed { get; set; } - } +public class AirPlane : Product +{ + public int Speed { get; set; } +} - public class JetPlane : AirPlane - { - public string Company { get; set; } - } +public class JetPlane : AirPlane +{ + public string Company { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastEdmModel.cs index 90010ae4a..5aec7b84f 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastEdmModel.cs @@ -8,21 +8,20 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Cast +namespace Microsoft.AspNetCore.OData.E2E.Tests.Cast; + +internal class CastEdmModel { - internal class CastEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Products"); - var airPlaneType=builder.EntityType(); - airPlaneType.DerivesFrom(); + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Products"); + var airPlaneType=builder.EntityType(); + airPlaneType.DerivesFrom(); - builder.Namespace = typeof(Product).Namespace; + builder.Namespace = typeof(Product).Namespace; - var edmModel = builder.GetEdmModel(); - return edmModel; - } + var edmModel = builder.GetEdmModel(); + return edmModel; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastTest.cs index 1a1757a2f..f4a4a36cc 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Cast/CastTest.cs @@ -18,105 +18,104 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Cast +namespace Microsoft.AspNetCore.OData.E2E.Tests.Cast; + +public class CastTest : WebODataTestBase { - public class CastTest : WebODataTestBase + public class CastTestStartup : TestStartupBase { - public class CastTestStartup : TestStartupBase + public override void ConfigureServices(IServiceCollection services) { - public override void ConfigureServices(IServiceCollection services) - { - // Use the sql server got the access error. - // services.AddDbContext(opt => opt.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=CastProductsContext;Trusted_Connection=True;")); - services.AddDbContext(opt => opt.UseInMemoryDatabase("CastProductsContext")); + // Use the sql server got the access error. + // services.AddDbContext(opt => opt.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=CastProductsContext;Trusted_Connection=True;")); + services.AddDbContext(opt => opt.UseInMemoryDatabase("CastProductsContext")); - services.ConfigureControllers(typeof(ProductsController), typeof(MetadataController)); + services.ConfigureControllers(typeof(ProductsController), typeof(MetadataController)); - IEdmModel edmModel = CastEdmModel.GetEdmModel(); + IEdmModel edmModel = CastEdmModel.GetEdmModel(); - services.AddControllers().AddOData(opt => - { - opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5); + services.AddControllers().AddOData(opt => + { + opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5); - foreach (string dataSourceType in CastTest.dataSourceTypes) - { - opt.AddRouteComponents(dataSourceType, edmModel); - } - }); - } + foreach (string dataSourceType in CastTest.dataSourceTypes) + { + opt.AddRouteComponents(dataSourceType, edmModel); + } + }); } + } - private static string[] dataSourceTypes = new string[] { "IM", "EF" };// In Memory and Entity Framework + private static string[] dataSourceTypes = new string[] { "IM", "EF" };// In Memory and Entity Framework - public CastTest(WebODataTestFixture factory) - :base(factory) - { - } + public CastTest(WebODataTestFixture factory) + :base(factory) + { + } - public static TheoryDataSet Combinations + public static TheoryDataSet Combinations + { + get { - get + var combinations = new TheoryDataSet(); + foreach (string dataSourceType in dataSourceTypes) { - var combinations = new TheoryDataSet(); - foreach (string dataSourceType in dataSourceTypes) - { - // To Edm.String - combinations.Add(dataSourceType, "?$filter=cast('Name1',Edm.String) eq Name", 1); - combinations.Add(dataSourceType, "?$filter=contains(cast(Name,Edm.String),'Name')", 6); - combinations.Add(dataSourceType, "?$filter=cast(Microsoft.AspNetCore.OData.E2E.Tests.Cast.Domain'Civil',Edm.String) eq '2'", 6); - combinations.Add(dataSourceType, "?$filter=cast(Domain,Edm.String) eq '3'", 2); - combinations.Add(dataSourceType, "?$filter=cast(ID,Edm.String) gt '1'", 5); - // TODO bug 1889: Cast function reports error if it is used against a collection of primitive value. - // Delete $it after the bug if fixed. - combinations.Add(dataSourceType, "(1)/DimensionInCentimeter?$filter=cast($it,Edm.String) gt '1'", 2); - combinations.Add(dataSourceType, "?$filter=cast(Weight,Edm.String) gt '1.1'", 5); - combinations.Add(dataSourceType, "?$filter=contains(cast(ManufacturingDate,Edm.String),'2011')", 1); - // TODO bug 1982: The result of casting a value of DateTimeOffset to String is not always the literal representation used in payloads - // combinations.Add(dataSourceType, "?$filter=contains(cast(2011-01-01T00:00:00%2B08:00,Edm.String),'2011-01-01')", 3); - - // To Edm.Int32 - combinations.Add(dataSourceType, "?$filter=cast(Weight,Edm.Int32) eq 1", 1); - combinations.Add(dataSourceType, "?$filter=cast(cast(Name,Edm.Int32),Edm.Int32) eq null", 6); - - // To DateTimeOffset - combinations.Add(dataSourceType, "?$filter=cast(ManufacturingDate,Edm.DateTimeOffset) eq 2011-01-01T00:00:00%2B08:00", 1); - combinations.Add(dataSourceType, "?$filter=cast(null,Edm.DateTimeOffset) eq null", 6); - - // To Enum - combinations.Add(dataSourceType, "?$filter=cast('Both',Microsoft.AspNetCore.OData.E2E.Tests.Cast.Domain) eq Domain", 2); - combinations.Add(dataSourceType, "?$filter=cast('1',Microsoft.AspNetCore.OData.E2E.Tests.Cast.Domain) eq Domain", 2); - combinations.Add(dataSourceType, "?$filter=cast(null,Microsoft.AspNetCore.OData.E2E.Tests.Cast.Domain) eq Domain", 0); - - //To Derived Structured Types - // combinations.Add(dataSourceType, "?$filter=cast('Microsoft.AspNetCore.OData.E2E.Tests.Cast.AirPlane')/Speed eq 100", 2); - // combinations.Add(dataSourceType, "?$filter=cast('Microsoft.AspNetCore.OData.E2E.Tests.Cast.AirPlane')/Speed eq 500", 1); - // combinations.Add(dataSourceType, "?$filter=cast('Microsoft.AspNetCore.OData.E2E.Tests.Cast.JetPlane')/Company eq 'Boeing'", 1); - } - - return combinations; + // To Edm.String + combinations.Add(dataSourceType, "?$filter=cast('Name1',Edm.String) eq Name", 1); + combinations.Add(dataSourceType, "?$filter=contains(cast(Name,Edm.String),'Name')", 6); + combinations.Add(dataSourceType, "?$filter=cast(Microsoft.AspNetCore.OData.E2E.Tests.Cast.Domain'Civil',Edm.String) eq '2'", 6); + combinations.Add(dataSourceType, "?$filter=cast(Domain,Edm.String) eq '3'", 2); + combinations.Add(dataSourceType, "?$filter=cast(ID,Edm.String) gt '1'", 5); + // TODO bug 1889: Cast function reports error if it is used against a collection of primitive value. + // Delete $it after the bug if fixed. + combinations.Add(dataSourceType, "(1)/DimensionInCentimeter?$filter=cast($it,Edm.String) gt '1'", 2); + combinations.Add(dataSourceType, "?$filter=cast(Weight,Edm.String) gt '1.1'", 5); + combinations.Add(dataSourceType, "?$filter=contains(cast(ManufacturingDate,Edm.String),'2011')", 1); + // TODO bug 1982: The result of casting a value of DateTimeOffset to String is not always the literal representation used in payloads + // combinations.Add(dataSourceType, "?$filter=contains(cast(2011-01-01T00:00:00%2B08:00,Edm.String),'2011-01-01')", 3); + + // To Edm.Int32 + combinations.Add(dataSourceType, "?$filter=cast(Weight,Edm.Int32) eq 1", 1); + combinations.Add(dataSourceType, "?$filter=cast(cast(Name,Edm.Int32),Edm.Int32) eq null", 6); + + // To DateTimeOffset + combinations.Add(dataSourceType, "?$filter=cast(ManufacturingDate,Edm.DateTimeOffset) eq 2011-01-01T00:00:00%2B08:00", 1); + combinations.Add(dataSourceType, "?$filter=cast(null,Edm.DateTimeOffset) eq null", 6); + + // To Enum + combinations.Add(dataSourceType, "?$filter=cast('Both',Microsoft.AspNetCore.OData.E2E.Tests.Cast.Domain) eq Domain", 2); + combinations.Add(dataSourceType, "?$filter=cast('1',Microsoft.AspNetCore.OData.E2E.Tests.Cast.Domain) eq Domain", 2); + combinations.Add(dataSourceType, "?$filter=cast(null,Microsoft.AspNetCore.OData.E2E.Tests.Cast.Domain) eq Domain", 0); + + //To Derived Structured Types + // combinations.Add(dataSourceType, "?$filter=cast('Microsoft.AspNetCore.OData.E2E.Tests.Cast.AirPlane')/Speed eq 100", 2); + // combinations.Add(dataSourceType, "?$filter=cast('Microsoft.AspNetCore.OData.E2E.Tests.Cast.AirPlane')/Speed eq 500", 1); + // combinations.Add(dataSourceType, "?$filter=cast('Microsoft.AspNetCore.OData.E2E.Tests.Cast.JetPlane')/Company eq 'Boeing'", 1); } - } - [Theory] - [MemberData(nameof(Combinations))] - public async Task Cast_Query_WorksAsExpected(string dataSourceMode, string dollarFormat, int expectedEntityCount) - { - // Arrange - var requestUri = string.Format("{0}/Products{1}", dataSourceMode, dollarFormat); - - // Act - HttpResponseMessage response = await Client.GetAsync(requestUri); - JObject responseString = await response.Content.ReadAsObject(); - - // Assert - Assert.True(HttpStatusCode.OK == response.StatusCode, - string.Format("Response status code, expected: {0}, actual: {1}, request url: {2}", - HttpStatusCode.OK, response.StatusCode, requestUri)); - - JArray value = responseString["value"] as JArray; - Assert.True(expectedEntityCount == value.Count, - string.Format("The entity count in response, expected: {0}, actual: {1}, request url: {2}", - expectedEntityCount, value.Count, requestUri)); + return combinations; } } + + [Theory] + [MemberData(nameof(Combinations))] + public async Task Cast_Query_WorksAsExpected(string dataSourceMode, string dollarFormat, int expectedEntityCount) + { + // Arrange + var requestUri = string.Format("{0}/Products{1}", dataSourceMode, dollarFormat); + + // Act + HttpResponseMessage response = await Client.GetAsync(requestUri); + JObject responseString = await response.Content.ReadAsObject(); + + // Assert + Assert.True(HttpStatusCode.OK == response.StatusCode, + string.Format("Response status code, expected: {0}, actual: {1}, request url: {2}", + HttpStatusCode.OK, response.StatusCode, requestUri)); + + JArray value = responseString["value"] as JArray; + Assert.True(expectedEntityCount == value.Count, + string.Format("The entity count in response, expected: {0}, actual: {1}, request url: {2}", + expectedEntityCount, value.Count, requestUri)); + } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/HttpRequestTestExtensions.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/HttpRequestTestExtensions.cs index 77afe3d19..2df2d90bb 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/HttpRequestTestExtensions.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/HttpRequestTestExtensions.cs @@ -12,64 +12,63 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons +namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons; + +public static class HttpRequestTestExtensions { - public static class HttpRequestTestExtensions + /// + /// Helper method to get the key value from a uri. + /// Usually used by $link action to extract the key value from the url in body. + /// + /// The type of the key + /// The request instance in current context + /// OData uri that contains the key value + /// The key value + public static TKey GetKeyValue(this HttpRequest request, Uri uri) { - /// - /// Helper method to get the key value from a uri. - /// Usually used by $link action to extract the key value from the url in body. - /// - /// The type of the key - /// The request instance in current context - /// OData uri that contains the key value - /// The key value - public static TKey GetKeyValue(this HttpRequest request, Uri uri) + if (request == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } - - //get the odata path Ex: ~/entityset/key/$links/navigation - var odataPath = request.CreateODataPath(uri); - var keySegment = odataPath.OfType().FirstOrDefault(); - if (keySegment == null) - { - throw new InvalidOperationException("The link does not contain a key."); - } + throw new ArgumentNullException(nameof(request)); + } - return (TKey)keySegment.Keys.First().Value; + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); } - /// - /// Helper method to get the odata path for an arbitrary odata uri. - /// - /// The request instance in current context - /// OData uri - /// The parsed odata path - public static ODataPath CreateODataPath(this HttpRequest request, Uri uri) + //get the odata path Ex: ~/entityset/key/$links/navigation + var odataPath = request.CreateODataPath(uri); + var keySegment = odataPath.OfType().FirstOrDefault(); + if (keySegment == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + throw new InvalidOperationException("The link does not contain a key."); + } - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } + return (TKey)keySegment.Keys.First().Value; + } - IEdmModel model = request.GetModel(); - IServiceProvider sp = request.GetRouteServices(); - string serviceRoot = request.CreateODataLink(); - ODataUriParser uriParser = new ODataUriParser(model, new Uri(serviceRoot), uri, sp); - return uriParser.ParsePath(); + /// + /// Helper method to get the odata path for an arbitrary odata uri. + /// + /// The request instance in current context + /// OData uri + /// The parsed odata path + public static ODataPath CreateODataPath(this HttpRequest request, Uri uri) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); } + + IEdmModel model = request.GetModel(); + IServiceProvider sp = request.GetRouteServices(); + string serviceRoot = request.CreateODataLink(); + ODataUriParser uriParser = new ODataUriParser(model, new Uri(serviceRoot), uri, sp); + return uriParser.ParsePath(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/IWebHostTestFixture.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/IWebHostTestFixture.cs index f8a1d1442..8100d7a17 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/IWebHostTestFixture.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/IWebHostTestFixture.cs @@ -7,15 +7,14 @@ using System; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons +namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons; + +public interface IWebHostTestFixture { - public interface IWebHostTestFixture - { - /// - /// Gets or sets a value indicating whether error details should be included. - /// - bool IncludeErrorDetail { get; set; } + /// + /// Gets or sets a value indicating whether error details should be included. + /// + bool IncludeErrorDetail { get; set; } - // Action ConfigurationAction { get; } - } + // Action ConfigurationAction { get; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/ODataMessageWrapper.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/ODataMessageWrapper.cs index c0684758e..a392ecc61 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/ODataMessageWrapper.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/ODataMessageWrapper.cs @@ -12,104 +12,103 @@ using System.Net.Http.Headers; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons +namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons; + +internal class ODataMessageWrapper : IODataRequestMessage, IODataResponseMessage, IDisposable { - internal class ODataMessageWrapper : IODataRequestMessage, IODataResponseMessage, IDisposable + private Stream _stream; + private Dictionary _headers; + + public ODataMessageWrapper() + : this(stream: null, headers: null) { - private Stream _stream; - private Dictionary _headers; + } - public ODataMessageWrapper() - : this(stream: null, headers: null) - { - } + public ODataMessageWrapper(Stream stream) + : this(stream: stream, headers: null) + { + } - public ODataMessageWrapper(Stream stream) - : this(stream: stream, headers: null) + public ODataMessageWrapper(Stream stream, HttpHeaders headers) + { + _stream = stream; + if (headers != null) { + _headers = headers.ToDictionary(kvp => kvp.Key, kvp => String.Join(";", kvp.Value)); } - - public ODataMessageWrapper(Stream stream, HttpHeaders headers) + else { - _stream = stream; - if (headers != null) - { - _headers = headers.ToDictionary(kvp => kvp.Key, kvp => String.Join(";", kvp.Value)); - } - else - { - _headers = new Dictionary(); - } + _headers = new Dictionary(); } + } - public IEnumerable> Headers + public IEnumerable> Headers + { + get { - get - { - return _headers; - } + return _headers; } + } - public string Method + public string Method + { + get { - get - { - throw new NotImplementedException(); - } - set - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } - - public Uri Url + set { - get - { - throw new NotImplementedException(); - } - set - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } + } - public int StatusCode + public Uri Url + { + get { - get - { - throw new NotImplementedException(); - } - set - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } - - public string GetHeader(string headerName) + set { - string value; - if (_headers.TryGetValue(headerName, out value)) - { - return value; - } - - return null; + throw new NotImplementedException(); } + } - public Stream GetStream() + public int StatusCode + { + get { - return _stream; + throw new NotImplementedException(); } - - public void SetHeader(string headerName, string headerValue) + set { - _headers.Add(headerName, headerValue); + throw new NotImplementedException(); } + } - public void Dispose() + public string GetHeader(string headerName) + { + string value; + if (_headers.TryGetValue(headerName, out value)) { - _stream.Dispose(); + return value; } + + return null; + } + + public Stream GetStream() + { + return _stream; + } + + public void SetHeader(string headerName, string headerValue) + { + _headers.Add(headerName, headerValue); + } + + public void Dispose() + { + _stream.Dispose(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/PortManager.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/PortManager.cs index bd978a7fe..2d1816e43 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/PortManager.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/PortManager.cs @@ -10,39 +10,38 @@ using System.Net.NetworkInformation; using System.Threading; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons +namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons; + +public class PortArranger { - public class PortArranger - { - private static int nextPort = 11000; + private static int nextPort = 11000; - public static int Reserve() + public static int Reserve() + { + int attempts = 0; + while (attempts++ < 10) { - int attempts = 0; - while (attempts++ < 10) + int port = Interlocked.Increment(ref nextPort); + if (port >= 65535) { - int port = Interlocked.Increment(ref nextPort); - if (port >= 65535) - { - throw new OverflowException("Cannot get an available port, port value overflowed"); - } - - if (IsFree(port)) - { - return port; - } + throw new OverflowException("Cannot get an available port, port value overflowed"); } - throw new TimeoutException(string.Format("Cannot get an available port in {0} attempts.", attempts)); + if (IsFree(port)) + { + return port; + } } - private static bool IsFree(int port) - { - IPGlobalProperties properties = IPGlobalProperties.GetIPGlobalProperties(); - TcpConnectionInformation[] connections = properties.GetActiveTcpConnections(); - var isInUse = connections.Any(c => - c.LocalEndPoint.Port == port || c.RemoteEndPoint.Port == port); - return !isInUse; - } + throw new TimeoutException(string.Format("Cannot get an available port in {0} attempts.", attempts)); + } + + private static bool IsFree(int port) + { + IPGlobalProperties properties = IPGlobalProperties.GetIPGlobalProperties(); + TcpConnectionInformation[] connections = properties.GetActiveTcpConnections(); + var isInUse = connections.Any(c => + c.LocalEndPoint.Port == port || c.RemoteEndPoint.Port == port); + return !isInUse; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/TestAssembly.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/TestAssembly.cs index 9825035cd..a3a48b2c8 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/TestAssembly.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/TestAssembly.cs @@ -8,23 +8,22 @@ using System; using System.Reflection; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons +namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons; + +/// +/// This class is used in AspNetCore to add controllers as an assembly part for discovery. +/// +internal sealed class TestAssembly : Assembly { - /// - /// This class is used in AspNetCore to add controllers as an assembly part for discovery. - /// - internal sealed class TestAssembly : Assembly - { - Type[] _types; + Type[] _types; - public TestAssembly(params Type[] types) - { - _types = types; - } + public TestAssembly(params Type[] types) + { + _types = types; + } - public override Type[] GetTypes() - { - return _types; - } + public override Type[] GetTypes() + { + return _types; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/TestODataController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/TestODataController.cs index 8a24c9599..87c8bb56a 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/TestODataController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/TestODataController.cs @@ -9,149 +9,148 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Results; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons +namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons; + +public class TestODataController : ControllerBase { - public class TestODataController : ControllerBase - { - [NonAction] - public new TestOkResult Ok() => new TestOkResult(base.Ok()); + [NonAction] + public new TestOkResult Ok() => new TestOkResult(base.Ok()); - [NonAction] - public new TestOkObjectResult Ok(object value) => new TestOkObjectResult(value); + [NonAction] + public new TestOkObjectResult Ok(object value) => new TestOkObjectResult(value); - [NonAction] - public TestCreatedODataResult Created(T entity) => new TestCreatedODataResult(entity); + [NonAction] + public TestCreatedODataResult Created(T entity) => new TestCreatedODataResult(entity); - [NonAction] - public new TestCreatedResult Created(string uri, object entity) => new TestCreatedResult(base.Created(uri, entity)); + [NonAction] + public new TestCreatedResult Created(string uri, object entity) => new TestCreatedResult(base.Created(uri, entity)); - [NonAction] - public TestUpdatedODataResult Updated(T entity) => new TestUpdatedODataResult(entity); + [NonAction] + public TestUpdatedODataResult Updated(T entity) => new TestUpdatedODataResult(entity); +} + +/// +/// Wrapper for platform-specific version of object result. +/// +public class TestObjectResult : ObjectResult, IActionResult +{ + public TestObjectResult(object innerResult) + : base(innerResult) + { } +} + +/// +/// Wrapper for platform-specific version of status code result. +/// +public class TestStatusCodeResult : StatusCodeResult, IActionResult +{ + private StatusCodeResult innerResult; - /// - /// Wrapper for platform-specific version of object result. - /// - public class TestObjectResult : ObjectResult, IActionResult + public TestStatusCodeResult(StatusCodeResult innerResult) + : base(innerResult.StatusCode) { - public TestObjectResult(object innerResult) - : base(innerResult) - { - } + this.innerResult = innerResult; } +} + +/// +/// Wrapper for platform-specific version of action result. +/// +public class TestActionResult : ActionResult, IActionResult +{ + private IActionResult innerResult; - /// - /// Wrapper for platform-specific version of status code result. - /// - public class TestStatusCodeResult : StatusCodeResult, IActionResult + public TestActionResult(IActionResult innerResult) { - private StatusCodeResult innerResult; + this.innerResult = innerResult; + } - public TestStatusCodeResult(StatusCodeResult innerResult) - : base(innerResult.StatusCode) - { - this.innerResult = innerResult; - } + public override Task ExecuteResultAsync(ActionContext context) + { + return innerResult.ExecuteResultAsync(context); } +} - /// - /// Wrapper for platform-specific version of action result. - /// - public class TestActionResult : ActionResult, IActionResult +public class TestNotFoundResult : TestStatusCodeResult +{ + public TestNotFoundResult(NotFoundResult innerResult) + : base(innerResult) { - private IActionResult innerResult; + } +} - public TestActionResult(IActionResult innerResult) - { - this.innerResult = innerResult; - } +public class TestNotFoundObjectResult : TestObjectResult +{ + public TestNotFoundObjectResult(NotFoundObjectResult innerResult) + : base(innerResult) + { + } +} - public override Task ExecuteResultAsync(ActionContext context) - { - return innerResult.ExecuteResultAsync(context); - } +public class TestOkResult : TestStatusCodeResult +{ + public TestOkResult(OkResult innerResult) + : base(innerResult) + { } +} - public class TestNotFoundResult : TestStatusCodeResult +public class TestOkObjectResult : TestObjectResult +{ + public TestOkObjectResult(object innerResult) + : base(innerResult) { - public TestNotFoundResult(NotFoundResult innerResult) - : base(innerResult) - { - } + this.StatusCode = 200; } +} - public class TestNotFoundObjectResult : TestObjectResult +public class TestOkObjectResult : TestObjectResult +{ + public TestOkObjectResult(object innerResult) + : base(innerResult) { - public TestNotFoundObjectResult(NotFoundObjectResult innerResult) - : base(innerResult) - { - } + this.StatusCode = 200; } - public class TestOkResult : TestStatusCodeResult + public TestOkObjectResult(T content, TestODataController controller) + : base(content) { - public TestOkResult(OkResult innerResult) - : base(innerResult) - { - } + // Controller is unused. + this.StatusCode = 200; } +} - public class TestOkObjectResult : TestObjectResult +public class TestCreatedResult : TestActionResult +{ + public TestCreatedResult(CreatedResult innerResult) + : base(innerResult) { - public TestOkObjectResult(object innerResult) - : base(innerResult) - { - this.StatusCode = 200; - } } +} - public class TestOkObjectResult : TestObjectResult +public class TestCreatedODataResult : CreatedODataResult, IActionResult +{ + public TestCreatedODataResult(T entity) + : base(entity) { - public TestOkObjectResult(object innerResult) - : base(innerResult) - { - this.StatusCode = 200; - } - - public TestOkObjectResult(T content, TestODataController controller) - : base(content) - { - // Controller is unused. - this.StatusCode = 200; - } } - public class TestCreatedResult : TestActionResult + public TestCreatedODataResult(string uri, T entity) + : base(entity) { - public TestCreatedResult(CreatedResult innerResult) - : base(innerResult) - { - } } +} - public class TestCreatedODataResult : CreatedODataResult, IActionResult +public class TestUpdatedODataResult : UpdatedODataResult, IActionResult +{ + public TestUpdatedODataResult(T entity) + : base(entity) { - public TestCreatedODataResult(T entity) - : base(entity) - { - } - - public TestCreatedODataResult(string uri, T entity) - : base(entity) - { - } } - public class TestUpdatedODataResult : UpdatedODataResult, IActionResult + public TestUpdatedODataResult(string uri, T entity) + : base(entity) { - public TestUpdatedODataResult(T entity) - : base(entity) - { - } - - public TestUpdatedODataResult(string uri, T entity) - : base(entity) - { - } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/WebHostTestBaseOfT.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/WebHostTestBaseOfT.cs index ac5333832..85f793823 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/WebHostTestBaseOfT.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/WebHostTestBaseOfT.cs @@ -9,41 +9,40 @@ using System.Net.Http; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons +namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons; + +/// +/// The WebHostTestBase creates a web host to be used for a test. +/// +public abstract class WebHostTestBase : IClassFixture>, IDisposable where TTest: class { /// - /// The WebHostTestBase creates a web host to be used for a test. + /// Initializes a new instance of the class. /// - public abstract class WebHostTestBase : IClassFixture>, IDisposable where TTest: class + /// The fixture used to initialize the web service. + protected WebHostTestBase(WebHostTestFixture fixture) { - /// - /// Initializes a new instance of the class. - /// - /// The fixture used to initialize the web service. - protected WebHostTestBase(WebHostTestFixture fixture) - { - this.BaseAddress = fixture.BaseAddress; - this.Client = new HttpClient(); - } + this.BaseAddress = fixture.BaseAddress; + this.Client = new HttpClient(); + } - /// - /// The base address of the server. - /// - public string BaseAddress { get; private set; } + /// + /// The base address of the server. + /// + public string BaseAddress { get; private set; } - /// - /// An HttpClient to use with the server. - /// - public HttpClient Client { get; set; } + /// + /// An HttpClient to use with the server. + /// + public HttpClient Client { get; set; } - public void Dispose() + public void Dispose() + { + if (Client != null) { - if (Client != null) - { - Client.Dispose(); - } - - Client = null; + Client.Dispose(); } + + Client = null; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/WebHostTestFixtureOfT.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/WebHostTestFixtureOfT.cs index 40af7fb65..b810d7f3e 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/WebHostTestFixtureOfT.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/WebHostTestFixtureOfT.cs @@ -13,162 +13,161 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons +namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons; + +// https://sourcegraph.com/github.com/dotnet/aspnetcore@bbb851e3ebf40f79531bc13dd5c1b56b332237fc/-/blob/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L24:1 + +/// +/// The WebHostTestFixture is create a web host to be used for a test. +/// +/// +/// This is a Class Fixture (see https://xunit.github.io/docs/shared-context.html). +/// As such, it is instantiated per-class, which is the behavior needed here to ensure +/// each test class has its own web server, as opposed to Collection Fixtures even though +/// there is one assembly-wide collection used for serialization purposes. +/// +public class WebHostTestFixture : IDisposable, IWebHostTestFixture where TTest: class { - // https://sourcegraph.com/github.com/dotnet/aspnetcore@bbb851e3ebf40f79531bc13dd5c1b56b332237fc/-/blob/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L24:1 + private static readonly string NormalBaseAddressTemplate = "http://{0}:{1}"; + + private int _port; + private bool disposedValue = false; + + private IHost _hostedService = null; /// - /// The WebHostTestFixture is create a web host to be used for a test. + /// Initializes a new instance of the class /// - /// - /// This is a Class Fixture (see https://xunit.github.io/docs/shared-context.html). - /// As such, it is instantiated per-class, which is the behavior needed here to ensure - /// each test class has its own web server, as opposed to Collection Fixtures even though - /// there is one assembly-wide collection used for serialization purposes. - /// - public class WebHostTestFixture : IDisposable, IWebHostTestFixture where TTest: class + public WebHostTestFixture() { - private static readonly string NormalBaseAddressTemplate = "http://{0}:{1}"; - - private int _port; - private bool disposedValue = false; - - private IHost _hostedService = null; - - /// - /// Initializes a new instance of the class - /// - public WebHostTestFixture() - { - Initialize(); - } + Initialize(); + } - /// - /// Finalizes an instance of the class. - /// - ~WebHostTestFixture() - { - Dispose(false); - } + /// + /// Finalizes an instance of the class. + /// + ~WebHostTestFixture() + { + Dispose(false); + } - /// - /// The base address of the server. - /// - public string BaseAddress { get; private set; } + /// + /// The base address of the server. + /// + public string BaseAddress { get; private set; } - /// - /// Gets or sets a value indicating whether error details should be included. - /// - public bool IncludeErrorDetail { get; set; } = true; + /// + /// Gets or sets a value indicating whether error details should be included. + /// + public bool IncludeErrorDetail { get; set; } = true; - /// - /// Cleanup the server. - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - } + /// + /// Cleanup the server. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } - /// - /// Cleanup the server. - /// - protected virtual void Dispose(bool disposing) + /// + /// Cleanup the server. + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) { - if (!disposedValue) + if (disposing) { - if (disposing) + if (_hostedService != null) { - if (_hostedService != null) - { - _hostedService.StopAsync(); - _hostedService.WaitForShutdownAsync(); - } + _hostedService.StopAsync(); + _hostedService.WaitForShutdownAsync(); } - - disposedValue = true; } + + disposedValue = true; } + } - private Action GetConfigureServicesMethod() - { - Type testType = typeof(TTest); + private Action GetConfigureServicesMethod() + { + Type testType = typeof(TTest); - MethodInfo method = testType.GetMethod("UpdateConfigureServices", - BindingFlags.NonPublic | BindingFlags.Static, - null, - new Type[] { typeof(IServiceCollection) }, - null); + MethodInfo method = testType.GetMethod("UpdateConfigureServices", + BindingFlags.NonPublic | BindingFlags.Static, + null, + new Type[] { typeof(IServiceCollection) }, + null); - return method == null ? null : (Action)Delegate.CreateDelegate(typeof(Action), method); - } + return method == null ? null : (Action)Delegate.CreateDelegate(typeof(Action), method); + } - private Action GetConfigurationMethod() - { - Type testType = typeof(TTest); + private Action GetConfigurationMethod() + { + Type testType = typeof(TTest); - MethodInfo method = testType.GetMethod("UpdateConfigure", - BindingFlags.NonPublic | BindingFlags.Static, - null, - new Type[] { typeof(IApplicationBuilder)/*, typeof(IWebHostEnvironment)*/ }, - null); + MethodInfo method = testType.GetMethod("UpdateConfigure", + BindingFlags.NonPublic | BindingFlags.Static, + null, + new Type[] { typeof(IApplicationBuilder)/*, typeof(IWebHostEnvironment)*/ }, + null); - return method == null ? null : (Action)Delegate.CreateDelegate(typeof(Action), method); - } + return method == null ? null : (Action)Delegate.CreateDelegate(typeof(Action), method); + } - /// - /// Initialize the fixture. - /// - /// true of the server is initialized, false otherwise. - /// - /// This is done lazily to allow the update configuration - /// function to be passed in from the first test class instance. - /// - private void Initialize() - { - var configServicesDelete = GetConfigureServicesMethod(); - - var configApps = GetConfigurationMethod(); - - string serverName = "localhost"; - - // setup base address - _port = PortArranger.Reserve(); - this.BaseAddress = string.Format(NormalBaseAddressTemplate, serverName, _port.ToString()); - - _hostedService = Host.CreateDefaultBuilder() - .ConfigureAppConfiguration((hostingContext, config) => - { - }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder - .UseKestrel(options => - { - options.Listen(IPAddress.Loopback, _port); - }) - //.UseStartup() - .ConfigureServices(services => - { - // Add ourself to the container so WebHostTestStartup - // can call UpdateConfiguration. - // services.AddSingleton(this); - - configServicesDelete?.Invoke(services); - }) - .Configure(app => - { - configApps?.Invoke(app); - }) - .ConfigureLogging((hostingContext, logging) => - { - //logging.AddDebug(); - //logging.SetMinimumLevel(LogLevel.Warning); - }); - }) - .Build(); - - _hostedService.StartAsync(); - } + /// + /// Initialize the fixture. + /// + /// true of the server is initialized, false otherwise. + /// + /// This is done lazily to allow the update configuration + /// function to be passed in from the first test class instance. + /// + private void Initialize() + { + var configServicesDelete = GetConfigureServicesMethod(); + + var configApps = GetConfigurationMethod(); + + string serverName = "localhost"; + + // setup base address + _port = PortArranger.Reserve(); + this.BaseAddress = string.Format(NormalBaseAddressTemplate, serverName, _port.ToString()); + + _hostedService = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((hostingContext, config) => + { + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder + .UseKestrel(options => + { + options.Listen(IPAddress.Loopback, _port); + }) + //.UseStartup() + .ConfigureServices(services => + { + // Add ourself to the container so WebHostTestStartup + // can call UpdateConfiguration. + // services.AddSingleton(this); + + configServicesDelete?.Invoke(services); + }) + .Configure(app => + { + configApps?.Invoke(app); + }) + .ConfigureLogging((hostingContext, logging) => + { + //logging.AddDebug(); + //logging.SetMinimumLevel(LogLevel.Warning); + }); + }) + .Build(); + + _hostedService.StartAsync(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/WebHostTestStartup.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/WebHostTestStartup.cs index 4227533f9..379cb57c2 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/WebHostTestStartup.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Commons/WebHostTestStartup.cs @@ -9,16 +9,15 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons +namespace Microsoft.AspNetCore.OData.E2E.Tests.Commons; + +public class WebHostTestStartup { - public class WebHostTestStartup + public void ConfigureServices(IServiceCollection services) { - public void ConfigureServices(IServiceCollection services) - { - } + } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - } + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceControllers.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceControllers.cs index 75422b195..83153afad 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceControllers.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceControllers.cs @@ -17,335 +17,334 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance +namespace Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance; + +[Route("convention")] +[Route("explicit")] +public class WindowsController : ODataController { - [Route("convention")] - [Route("explicit")] - public class WindowsController : ODataController + private IList _windows = new List(); + + public WindowsController() { - private IList _windows = new List(); + Polygon triagle = new Polygon() { HasBorder = true, Vertexes = new List() }; + triagle.Vertexes.Add(new Point() { X = 1, Y = 2 }); + triagle.Vertexes.Add(new Point() { X = 2, Y = 3 }); + triagle.Vertexes.Add(new Point() { X = 4, Y = 8 }); + + Rectangle rectangle = new Rectangle(topLeft: new Point(), width: 2, height: 2); + Circle circle = new Circle() { HasBorder = true, Center = new Point(), Radius = 2 }; - public WindowsController() + Window dashboardWindow = new Window { - Polygon triagle = new Polygon() { HasBorder = true, Vertexes = new List() }; - triagle.Vertexes.Add(new Point() { X = 1, Y = 2 }); - triagle.Vertexes.Add(new Point() { X = 2, Y = 3 }); - triagle.Vertexes.Add(new Point() { X = 4, Y = 8 }); - - Rectangle rectangle = new Rectangle(topLeft: new Point(), width: 2, height: 2); - Circle circle = new Circle() { HasBorder = true, Center = new Point(), Radius = 2 }; - - Window dashboardWindow = new Window - { - Id = 1, - Name = "CircleWindow", - CurrentShape = circle, - OptionalShapes = new List(), - }; - dashboardWindow.OptionalShapes.Add(rectangle); - _windows.Add(dashboardWindow); - - Window popupWindow = new Window - { - Id = 2, - Name = "Popup", - CurrentShape = rectangle, - OptionalShapes = new List(), - Parent = dashboardWindow, - }; - - popupWindow.OptionalShapes.Add(triagle); - popupWindow.OptionalShapes.Add(circle); - _windows.Add(popupWindow); - - Window anotherPopupWindow = new Window - { - Id = 3, - Name = "AnotherPopup", - CurrentShape = rectangle, - OptionalShapes = new List(), - Parent = popupWindow, - }; - - anotherPopupWindow.OptionalShapes.Add(triagle); - anotherPopupWindow.OptionalShapes.Add(circle); - _windows.Add(anotherPopupWindow); - } + Id = 1, + Name = "CircleWindow", + CurrentShape = circle, + OptionalShapes = new List(), + }; + dashboardWindow.OptionalShapes.Add(rectangle); + _windows.Add(dashboardWindow); + + Window popupWindow = new Window + { + Id = 2, + Name = "Popup", + CurrentShape = rectangle, + OptionalShapes = new List(), + Parent = dashboardWindow, + }; + + popupWindow.OptionalShapes.Add(triagle); + popupWindow.OptionalShapes.Add(circle); + _windows.Add(popupWindow); + + Window anotherPopupWindow = new Window + { + Id = 3, + Name = "AnotherPopup", + CurrentShape = rectangle, + OptionalShapes = new List(), + Parent = popupWindow, + }; + + anotherPopupWindow.OptionalShapes.Add(triagle); + anotherPopupWindow.OptionalShapes.Add(circle); + _windows.Add(anotherPopupWindow); + } - [EnableQuery] - public IActionResult Get() + [EnableQuery] + public IActionResult Get() + { + return Ok(_windows); + } + + [EnableQuery] + public SingleResult GetWindow([FromODataUri] int key) + { + return SingleResult.Create(_windows.Where(w => w.Id == key).AsQueryable()); + } + + public IActionResult Post([FromBody]Window window) + { + _windows.Add(window); + window.Id = _windows.Count + 1; + Rectangle rectangle = window.CurrentShape as Rectangle; + if(rectangle!=null) { - return Ok(_windows); + rectangle.Fill(); } + window.OptionalShapes.OfType().ToList().ForEach(r => r.Fill()); + return Created(window); + } + + // [HttpPatch("Windows({key})")] + [HttpPatch] + public IActionResult Patch(int key, [FromBody]Delta delta) + { + delta.TrySetPropertyValue("Id", key); // It is the key property, and should not be updated. - [EnableQuery] - public SingleResult GetWindow([FromODataUri] int key) + Window window = _windows.FirstOrDefault(e => e.Id == key); + if (window == null) { - return SingleResult.Create(_windows.Where(w => w.Id == key).AsQueryable()); + window = new Window(); + delta.Patch(window); + return Created(window); } - public IActionResult Post([FromBody]Window window) + try { - _windows.Add(window); - window.Id = _windows.Count + 1; - Rectangle rectangle = window.CurrentShape as Rectangle; - if(rectangle!=null) - { - rectangle.Fill(); - } - window.OptionalShapes.OfType().ToList().ForEach(r => r.Fill()); - return Created(window); + delta.Patch(window); } + catch (ArgumentException ae) + { + return BadRequest(ae.Message); + } + + return Ok(window); + } + + [HttpPatch("Windows({key})/CurrentShape")] + public IActionResult PatchShape(int key, [FromBody] Delta delta) + { + Window window = _windows.First(e => e.Id == key); + var currShape = window.CurrentShape; + Shape newcurrShape = null; - // [HttpPatch("Windows({key})")] - [HttpPatch] - public IActionResult Patch(int key, [FromBody]Delta delta) + try { - delta.TrySetPropertyValue("Id", key); // It is the key property, and should not be updated. - - Window window = _windows.FirstOrDefault(e => e.Id == key); - if (window == null) - { - window = new Window(); - delta.Patch(window); - return Created(window); - } - - try - { - delta.Patch(window); - } - catch (ArgumentException ae) - { - return BadRequest(ae.Message); - } - - return Ok(window); + newcurrShape = delta.Patch(currShape); + } + catch (ArgumentException ae) + { + return BadRequest(ae.Message); } - [HttpPatch("Windows({key})/CurrentShape")] - public IActionResult PatchShape(int key, [FromBody] Delta delta) + return Ok(newcurrShape); + } + + public IActionResult Put(int key, [FromBody]Window window) + { + if (key != window.Id) { - Window window = _windows.First(e => e.Id == key); - var currShape = window.CurrentShape; - Shape newcurrShape = null; - - try - { - newcurrShape = delta.Patch(currShape); - } - catch (ArgumentException ae) - { - return BadRequest(ae.Message); - } - - return Ok(newcurrShape); + return BadRequest(); } + Rectangle rectangle = window.CurrentShape as Rectangle; + if (rectangle != null) + { + rectangle.Fill(); + } + window.OptionalShapes.OfType().ToList().ForEach(r => r.Fill()); - public IActionResult Put(int key, [FromBody]Window window) + Window originalWindow = _windows.FirstOrDefault(e => e.Id == key); + if (window == null) { - if (key != window.Id) - { - return BadRequest(); - } - Rectangle rectangle = window.CurrentShape as Rectangle; - if (rectangle != null) - { - rectangle.Fill(); - } - window.OptionalShapes.OfType().ToList().ForEach(r => r.Fill()); - - Window originalWindow = _windows.FirstOrDefault(e => e.Id == key); - if (window == null) - { - _windows.Add(window); - return Created(window); - } - - _windows.Remove(originalWindow); _windows.Add(window); - - return Ok(window); + return Created(window); } - [EnableQuery] - public IActionResult Delete([FromODataUri] int key) + _windows.Remove(originalWindow); + _windows.Add(window); + + return Ok(window); + } + + [EnableQuery] + public IActionResult Delete([FromODataUri] int key) + { + Window window = _windows.Single(w => w.Id == key); + _windows.Remove(window); + return StatusCode(204); + } + + [HttpGet("Windows({key})/CurrentShape/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle")] + public IActionResult GetCurrentShape(int key) + { + Window window = _windows.FirstOrDefault(w => w.Id == key); + if (window == null) { - Window window = _windows.Single(w => w.Id == key); - _windows.Remove(window); - return StatusCode(204); + return NotFound(); } - [HttpGet("Windows({key})/CurrentShape/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle")] - public IActionResult GetCurrentShape(int key) + Circle circle = window.CurrentShape as Circle; + if (circle == null) { - Window window = _windows.FirstOrDefault(w => w.Id == key); - if (window == null) - { - return NotFound(); - } - - Circle circle = window.CurrentShape as Circle; - if (circle == null) - { - return NotFound(); - } - return Ok(circle); + return NotFound(); } + return Ok(circle); + } - [HttpGet("Windows({key})/CurrentShape/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle/Radius")] - public IActionResult GetRadius(int key) + [HttpGet("Windows({key})/CurrentShape/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle/Radius")] + public IActionResult GetRadius(int key) + { + Window window = _windows.FirstOrDefault(e => e.Id == key); + if (window == null) { - Window window = _windows.FirstOrDefault(e => e.Id == key); - if (window == null) - { - return NotFound(); - } - - return Ok(((Circle)window.CurrentShape).Radius); + return NotFound(); } - [HttpGet("Windows({key})/CurrentShape/HasBorder")] - public IActionResult GetHasBorder(int key) - { - Window window = _windows.FirstOrDefault(e => e.Id == key); - if (window == null) - { - return NotFound(); - } + return Ok(((Circle)window.CurrentShape).Radius); + } - return Ok(window.CurrentShape.HasBorder); + [HttpGet("Windows({key})/CurrentShape/HasBorder")] + public IActionResult GetHasBorder(int key) + { + Window window = _windows.FirstOrDefault(e => e.Id == key); + if (window == null) + { + return NotFound(); } - public IActionResult GetOptionalShapes(int key) - { - Window window = _windows.FirstOrDefault(e => e.Id == key); - if (window == null) - { - return NotFound(); - } + return Ok(window.CurrentShape.HasBorder); + } - return Ok(window.OptionalShapes); + public IActionResult GetOptionalShapes(int key) + { + Window window = _windows.FirstOrDefault(e => e.Id == key); + if (window == null) + { + return NotFound(); } - public IActionResult GetPolygonalShapes(int key) - { - Window window = _windows.FirstOrDefault(e => e.Id == key); - if (window == null) - { - return NotFound(); - } + return Ok(window.OptionalShapes); + } - return Ok(window.PolygonalShapes); + public IActionResult GetPolygonalShapes(int key) + { + Window window = _windows.FirstOrDefault(e => e.Id == key); + if (window == null) + { + return NotFound(); } - // [HttpGet("Windows({key})/OptionalShapes/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle")] - // use convention - public IActionResult GetOptionalShapesOfCircle(int key) + return Ok(window.PolygonalShapes); + } + + // [HttpGet("Windows({key})/OptionalShapes/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle")] + // use convention + public IActionResult GetOptionalShapesOfCircle(int key) + { + Window window = _windows.FirstOrDefault(e => e.Id == key); + if (window == null) { - Window window = _windows.FirstOrDefault(e => e.Id == key); - if (window == null) - { - return NotFound(); - } + return NotFound(); + } - return Ok(window.OptionalShapes.OfType()); + return Ok(window.OptionalShapes.OfType()); + } + + [HttpPut] + public IActionResult PutToCurrentShapeOfCircle(int key, [FromBody]Delta shape) + { + Window window = _windows.FirstOrDefault(e => e.Id == key); + if (window == null) + { + return NotFound(); } - [HttpPut] - public IActionResult PutToCurrentShapeOfCircle(int key, [FromBody]Delta shape) + Circle origin = window.CurrentShape as Circle; + if (origin == null) { - Window window = _windows.FirstOrDefault(e => e.Id == key); - if (window == null) - { - return NotFound(); - } - - Circle origin = window.CurrentShape as Circle; - if (origin == null) - { - return NotFound(); - } - - shape.Put(origin); - return Ok(origin); + return NotFound(); } - [HttpPut("Windows({key})/OptionalShapes")] - public IActionResult ReplaceOptionalShapes(int key, IEnumerable shapes) + shape.Put(origin); + return Ok(origin); + } + + [HttpPut("Windows({key})/OptionalShapes")] + public IActionResult ReplaceOptionalShapes(int key, IEnumerable shapes) + { + Window window = _windows.FirstOrDefault(e => e.Id == key); + if (window == null) { - Window window = _windows.FirstOrDefault(e => e.Id == key); - if (window == null) - { - return NotFound(); - } - - Assert.NotNull(shapes); - window.OptionalShapes = shapes.ToList(); - return Ok(shapes); + return NotFound(); } - [HttpPost] - public IActionResult PostToOptionalShapes(int key, [FromBody]Shape newShape) + Assert.NotNull(shapes); + window.OptionalShapes = shapes.ToList(); + return Ok(shapes); + } + + [HttpPost] + public IActionResult PostToOptionalShapes(int key, [FromBody]Shape newShape) + { + Window window = _windows.FirstOrDefault(w => w.Id == key); + if (window == null) { - Window window = _windows.FirstOrDefault(w => w.Id == key); - if (window == null) - { - return NotFound(); - } - Assert.NotNull(newShape); - window.OptionalShapes.Add(newShape); - return Updated(window.OptionalShapes); + return NotFound(); } + Assert.NotNull(newShape); + window.OptionalShapes.Add(newShape); + return Updated(window.OptionalShapes); + } - [HttpPost] - public IActionResult PostToPolygonalShapes(int key, [FromBody]Polygon newPolygon) + [HttpPost] + public IActionResult PostToPolygonalShapes(int key, [FromBody]Polygon newPolygon) + { + Window window = _windows.FirstOrDefault(w => w.Id == key); + if (window == null) { - Window window = _windows.FirstOrDefault(w => w.Id == key); - if (window == null) - { - return NotFound(); - } - Assert.NotNull(newPolygon); - window.PolygonalShapes.Add(newPolygon); - return Updated(window.PolygonalShapes); + return NotFound(); } + Assert.NotNull(newPolygon); + window.PolygonalShapes.Add(newPolygon); + return Updated(window.PolygonalShapes); + } + + [HttpPatch] + public IActionResult PatchToOptionalShapes(int key, [FromBody]Delta shapes) + { + return Ok("Not Supported"); + } - [HttpPatch] - public IActionResult PatchToOptionalShapes(int key, [FromBody]Delta shapes) + [HttpPatch] + public IActionResult PatchToCurrentShapeOfCircle(int key, [FromBody]Delta shape) + { + Window window = _windows.FirstOrDefault(e => e.Id == key); + if (window == null) { - return Ok("Not Supported"); + return NotFound(); } - [HttpPatch] - public IActionResult PatchToCurrentShapeOfCircle(int key, [FromBody]Delta shape) + Circle origin = window.CurrentShape as Circle; + if (origin == null) { - Window window = _windows.FirstOrDefault(e => e.Id == key); - if (window == null) - { - return NotFound(); - } - - Circle origin = window.CurrentShape as Circle; - if (origin == null) - { - return NotFound(); - } - - shape.Patch(origin); - return Ok(origin); + return NotFound(); } - public IActionResult DeleteToCurrentShape(int key) + shape.Patch(origin); + return Ok(origin); + } + + public IActionResult DeleteToCurrentShape(int key) + { + Window window = _windows.FirstOrDefault(e => e.Id == key); + if (window == null) { - Window window = _windows.FirstOrDefault(e => e.Id == key); - if (window == null) - { - return NotFound(); - } - - window.CurrentShape = null; - return Updated(window); + return NotFound(); } + + window.CurrentShape = null; + return Updated(window); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceDataModels.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceDataModels.cs index c6f7352c6..5c7bd8941 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceDataModels.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceDataModels.cs @@ -7,99 +7,98 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance +namespace Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance; + +public class Window { - public class Window + public Window() { - public Window() - { - OptionalShapes = new List(); - PolygonalShapes = new List(); - } - public int Id { get; set; } - public string Name { get; set; } - public Window Parent { get; set; } - public Shape CurrentShape { get; set; } - public IList OptionalShapes { get; set; } - public IList PolygonalShapes { get; set; } + OptionalShapes = new List(); + PolygonalShapes = new List(); } + public int Id { get; set; } + public string Name { get; set; } + public Window Parent { get; set; } + public Shape CurrentShape { get; set; } + public IList OptionalShapes { get; set; } + public IList PolygonalShapes { get; set; } +} + +public abstract class Shape +{ + public bool HasBorder { get; set; } +} + +public class Circle : Shape +{ + public Point Center { get; set; } + public int Radius { get; set; } - public abstract class Shape + public override string ToString() { - public bool HasBorder { get; set; } + // {centerX, centerY,radius} + return "{" + Center.X + "," + Center.Y + "," + Radius + "}"; } +} - public class Circle : Shape +public class Polygon : Shape +{ + public IList Vertexes { get; set; } + public Polygon() { - public Point Center { get; set; } - public int Radius { get; set; } - - public override string ToString() - { - // {centerX, centerY,radius} - return "{" + Center.X + "," + Center.Y + "," + Radius + "}"; - } + Vertexes = new List(); } +} + +public class Rectangle : Polygon +{ + public Point TopLeft { get; set; } + public int Width { get; set; } + public int Height { get; set; } - public class Polygon : Shape + public Rectangle() { - public IList Vertexes { get; set; } - public Polygon() - { - Vertexes = new List(); - } + } - public class Rectangle : Polygon + public Rectangle(Point topLeft, int width, int height) { - public Point TopLeft { get; set; } - public int Width { get; set; } - public int Height { get; set; } + TopLeft = topLeft; + Width = width; + Height = height; - public Rectangle() - { + this.Fill(); + } + public void Fill() + { + if(Width==0||Height==0) + { + return; } - - public Rectangle(Point topLeft, int width, int height) + Vertexes.Add(TopLeft); + Vertexes.Add(new Point() { - TopLeft = topLeft; - Width = width; - Height = height; + X = TopLeft.X + Width, + Y = TopLeft.Y, + }); - this.Fill(); - } + Vertexes.Add(new Point() + { + X = TopLeft.X + Width, + Y = TopLeft.Y + Height, + }); - public void Fill() + Vertexes.Add(new Point() { - if(Width==0||Height==0) - { - return; - } - Vertexes.Add(TopLeft); - Vertexes.Add(new Point() - { - X = TopLeft.X + Width, - Y = TopLeft.Y, - }); - - Vertexes.Add(new Point() - { - X = TopLeft.X + Width, - Y = TopLeft.Y + Height, - }); - - Vertexes.Add(new Point() - { - X = TopLeft.X, - Y = TopLeft.Y + Height, - }); - } + X = TopLeft.X, + Y = TopLeft.Y + Height, + }); } +} - public class Point - { - public int X { get; set; } - public int Y { get; set; } - } +public class Point +{ + public int X { get; set; } + public int Y { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceEdmModels.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceEdmModels.cs index f75da2dff..252eb72c3 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceEdmModels.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceEdmModels.cs @@ -8,72 +8,71 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance +namespace Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance; + +public class ComplexTypeInheritanceEdmModels { - public class ComplexTypeInheritanceEdmModels + public static IEdmModel GetExplicitModel() { - public static IEdmModel GetExplicitModel() - { - ODataModelBuilder builder = new ODataModelBuilder(); - EntityTypeConfiguration windowType = builder.EntityType(); - windowType.HasKey(a => a.Id); - windowType.Property(a => a.Name).IsRequired(); - windowType.ComplexProperty(w => w.CurrentShape).IsNullable(); - windowType.CollectionProperty(w => w.OptionalShapes); - windowType.CollectionProperty(w => w.PolygonalShapes); - windowType.HasOptional(w => w.Parent); - - ComplexTypeConfiguration shapeType = builder.ComplexType(); - shapeType.Property(s => s.HasBorder); - shapeType.Abstract(); + ODataModelBuilder builder = new ODataModelBuilder(); + EntityTypeConfiguration windowType = builder.EntityType(); + windowType.HasKey(a => a.Id); + windowType.Property(a => a.Name).IsRequired(); + windowType.ComplexProperty(w => w.CurrentShape).IsNullable(); + windowType.CollectionProperty(w => w.OptionalShapes); + windowType.CollectionProperty(w => w.PolygonalShapes); + windowType.HasOptional(w => w.Parent); - ComplexTypeConfiguration circleType = builder.ComplexType(); - circleType.ComplexProperty(c => c.Center); - circleType.Property(c => c.Radius); - circleType.DerivesFrom(); + ComplexTypeConfiguration shapeType = builder.ComplexType(); + shapeType.Property(s => s.HasBorder); + shapeType.Abstract(); - ComplexTypeConfiguration polygonType = builder.ComplexType(); - polygonType.CollectionProperty(p => p.Vertexes); - polygonType.DerivesFrom(); + ComplexTypeConfiguration circleType = builder.ComplexType(); + circleType.ComplexProperty(c => c.Center); + circleType.Property(c => c.Radius); + circleType.DerivesFrom(); - ComplexTypeConfiguration rectangleType = builder.ComplexType(); - rectangleType.ComplexProperty(r => r.TopLeft); - rectangleType.Property(r => r.Width); - rectangleType.Property(r => r.Height); - rectangleType.DerivesFrom(); + ComplexTypeConfiguration polygonType = builder.ComplexType(); + polygonType.CollectionProperty(p => p.Vertexes); + polygonType.DerivesFrom(); - ComplexTypeConfiguration pointType = builder.ComplexType(); - pointType.Property(p => p.X); - pointType.Property(p => p.Y); + ComplexTypeConfiguration rectangleType = builder.ComplexType(); + rectangleType.ComplexProperty(r => r.TopLeft); + rectangleType.Property(r => r.Width); + rectangleType.Property(r => r.Height); + rectangleType.DerivesFrom(); - EntitySetConfiguration windows = builder.EntitySet("Windows"); - // windows.HasEditLink(link, true); - // windows.HasIdLink(link, true); - windows.HasOptionalBinding(c => c.Parent, "Windows"); + ComplexTypeConfiguration pointType = builder.ComplexType(); + pointType.Property(p => p.X); + pointType.Property(p => p.Y); - builder.Namespace = typeof(Window).Namespace; + EntitySetConfiguration windows = builder.EntitySet("Windows"); + // windows.HasEditLink(link, true); + // windows.HasIdLink(link, true); + windows.HasOptionalBinding(c => c.Parent, "Windows"); - return builder.GetEdmModel(); - } + builder.Namespace = typeof(Window).Namespace; - public static IEdmModel GetConventionModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + return builder.GetEdmModel(); + } - builder.EntitySet("Windows"); - builder.Namespace = typeof(Window).Namespace; + public static IEdmModel GetConventionModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - return builder.GetEdmModel(); - } + builder.EntitySet("Windows"); + builder.Namespace = typeof(Window).Namespace; - //private static Func link = entityContext => - // { - // object id; - // entityContext.EdmObject.TryGetPropertyValue("Id", out id); - // string uri = ResourceContextHelper.CreateODataLink(entityContext, - // new EntitySetSegment(entityContext.NavigationSource as IEdmEntitySet), - // new KeySegment(new[] { new KeyValuePair("Id", id) }, entityContext.StructuredType as IEdmEntityType, null)); - // return new Uri(uri); - // }; + return builder.GetEdmModel(); } + + //private static Func link = entityContext => + // { + // object id; + // entityContext.EdmObject.TryGetPropertyValue("Id", out id); + // string uri = ResourceContextHelper.CreateODataLink(entityContext, + // new EntitySetSegment(entityContext.NavigationSource as IEdmEntitySet), + // new KeySegment(new[] { new KeyValuePair("Id", id) }, entityContext.StructuredType as IEdmEntityType, null)); + // return new Uri(uri); + // }; } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceSerializeTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceSerializeTest.cs index efff8bf2d..d8516ca92 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceSerializeTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceSerializeTest.cs @@ -22,139 +22,138 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance +namespace Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance; + +public class ComplexTypeInheritanceSerializeTest : WebApiTestBase { - public class ComplexTypeInheritanceSerializeTest : WebApiTestBase + public ComplexTypeInheritanceSerializeTest(WebApiTestFixture fixture) + :base(fixture) { - public ComplexTypeInheritanceSerializeTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(InheritanceCustomersController)); + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(InheritanceCustomersController)); - var edmModel1 = GetEdmModel(); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel1)); - } + var edmModel1 = GetEdmModel(); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel1)); + } - [Fact] - public async Task CanQueryInheritanceComplexInComplexProperty() - { - // Arrange - string requestUri = "odata/InheritanceCustomers?$format=application/json;odata.metadata=full"; - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); - string contentOfString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(HttpStatusCode.OK == response.StatusCode, - String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.OK, - response.StatusCode, - requestUri, - contentOfString)); - - JObject contentOfJObject = await response.Content.ReadAsObject(); - Assert.Equal(2, contentOfJObject.Count); - Assert.Equal(5, contentOfJObject["value"].Count()); - - Assert.Equal(new[] - { - "#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.InheritanceAddress", - "#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.InheritanceAddress", - "#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.InheritanceUsAddress", - "#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.InheritanceCnAddress", - "#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.InheritanceCnAddress" - }, - contentOfJObject["value"].Select(e => e["Location"]["Address"]["@odata.type"]).Select(c => (string)c)); - } - - public static IEdmModel GetEdmModel() + [Fact] + public async Task CanQueryInheritanceComplexInComplexProperty() + { + // Arrange + string requestUri = "odata/InheritanceCustomers?$format=application/json;odata.metadata=full"; + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + string contentOfString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(HttpStatusCode.OK == response.StatusCode, + String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.OK, + response.StatusCode, + requestUri, + contentOfString)); + + JObject contentOfJObject = await response.Content.ReadAsObject(); + Assert.Equal(2, contentOfJObject.Count); + Assert.Equal(5, contentOfJObject["value"].Count()); + + Assert.Equal(new[] { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("InheritanceCustomers"); - builder.ComplexType(); - return builder.GetEdmModel(); - } + "#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.InheritanceAddress", + "#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.InheritanceAddress", + "#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.InheritanceUsAddress", + "#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.InheritanceCnAddress", + "#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.InheritanceCnAddress" + }, + contentOfJObject["value"].Select(e => e["Location"]["Address"]["@odata.type"]).Select(c => (string)c)); } - public class InheritanceCustomersController : ODataController + public static IEdmModel GetEdmModel() { - private readonly IList _customers; - public InheritanceCustomersController() + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("InheritanceCustomers"); + builder.ComplexType(); + return builder.GetEdmModel(); + } +} + +public class InheritanceCustomersController : ODataController +{ + private readonly IList _customers; + public InheritanceCustomersController() + { + InheritanceAddress address = new InheritanceAddress { - InheritanceAddress address = new InheritanceAddress - { - City = "Tokyo", - Street = "Tokyo Rd" - }; + City = "Tokyo", + Street = "Tokyo Rd" + }; - InheritanceAddress usAddress = new InheritanceUsAddress - { - City = "Redmond", - Street = "One Microsoft Way", - ZipCode = 98052 - }; + InheritanceAddress usAddress = new InheritanceUsAddress + { + City = "Redmond", + Street = "One Microsoft Way", + ZipCode = 98052 + }; - InheritanceAddress cnAddress = new InheritanceCnAddress - { - City = "Shanghai", - Street = "ZiXing Rd", - PostCode = "200241" - }; + InheritanceAddress cnAddress = new InheritanceCnAddress + { + City = "Shanghai", + Street = "ZiXing Rd", + PostCode = "200241" + }; - _customers = Enumerable.Range(1, 5).Select(e => - new InheritanceCustomer + _customers = Enumerable.Range(1, 5).Select(e => + new InheritanceCustomer + { + Id = e, + Location = new InheritanceLocation { - Id = e, - Location = new InheritanceLocation - { - Name = "Location #" + e, - Address = e < 3 ? address : e < 4 ? usAddress : cnAddress - } - }).ToList(); - } - - [EnableQuery] - public IActionResult Get() - { - return Ok(_customers); - } + Name = "Location #" + e, + Address = e < 3 ? address : e < 4 ? usAddress : cnAddress + } + }).ToList(); } - public class InheritanceCustomer + [EnableQuery] + public IActionResult Get() { - public int Id { get; set; } - - public InheritanceLocation Location { get; set; } + return Ok(_customers); } +} - public class InheritanceLocation - { - public string Name { get; set; } +public class InheritanceCustomer +{ + public int Id { get; set; } - public InheritanceAddress Address { get; set; } - } + public InheritanceLocation Location { get; set; } +} - public class InheritanceAddress - { - public string City { get; set; } +public class InheritanceLocation +{ + public string Name { get; set; } - public string Street { get; set; } - } + public InheritanceAddress Address { get; set; } +} - public class InheritanceUsAddress : InheritanceAddress - { - public int ZipCode { get; set; } - } +public class InheritanceAddress +{ + public string City { get; set; } - public class InheritanceCnAddress : InheritanceAddress - { - public string PostCode { get; set; } - } + public string Street { get; set; } +} + +public class InheritanceUsAddress : InheritanceAddress +{ + public int ZipCode { get; set; } +} + +public class InheritanceCnAddress : InheritanceAddress +{ + public string PostCode { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs index 3fef3236a..ddc100f66 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ComplexTypeInheritance/ComplexTypeInheritanceTests.cs @@ -19,59 +19,59 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance +namespace Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance; + +public class ComplexTypeInheritanceTests : WebApiTestBase { - public class ComplexTypeInheritanceTests : WebApiTestBase + public ComplexTypeInheritanceTests(WebApiTestFixture fixture) + :base(fixture) { - public ComplexTypeInheritanceTests(WebApiTestFixture fixture) - :base(fixture) - { - } + } - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(MetadataController), typeof(WindowsController)); + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(MetadataController), typeof(WindowsController)); - var edmModel1 = ComplexTypeInheritanceEdmModels.GetConventionModel(); - var edmModel2 = ComplexTypeInheritanceEdmModels.GetExplicitModel(); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("convention", edmModel1) - .AddRouteComponents("explicit", edmModel2).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); - } + var edmModel1 = ComplexTypeInheritanceEdmModels.GetConventionModel(); + var edmModel2 = ComplexTypeInheritanceEdmModels.GetExplicitModel(); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("convention", edmModel1) + .AddRouteComponents("explicit", edmModel2).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); + } - public static TheoryDataSet MediaTypes + public static TheoryDataSet MediaTypes + { + get { - get + string[] modes = new string[] { "convention", "explicit" }; + string[] mimes = new string[]{ + "json", + "application/json", + "application/json;odata.metadata=none", + "application/json;odata.metadata=minimal", + "application/json;odata.metadata=full"}; + TheoryDataSet data = new TheoryDataSet(); + foreach (string mode in modes) { - string[] modes = new string[] { "convention", "explicit" }; - string[] mimes = new string[]{ - "json", - "application/json", - "application/json;odata.metadata=none", - "application/json;odata.metadata=minimal", - "application/json;odata.metadata=full"}; - TheoryDataSet data = new TheoryDataSet(); - foreach (string mode in modes) + foreach (string mime in mimes) { - foreach (string mime in mimes) - { - data.Add(mode, mime); - } + data.Add(mode, mime); } - return data; } + return data; } + } - public static TheoryDataSet PostToCollectionNewComplexTypeMembers + public static TheoryDataSet PostToCollectionNewComplexTypeMembers + { + get { - get + string[] modes = new string[] { "convention", "explicit" }; + string[] targets = { "OptionalShapes", "PolygonalShapes" }; + bool[] representations = { true, false }; + string[] objects = new string[] { - string[] modes = new string[] { "convention", "explicit" }; - string[] targets = { "OptionalShapes", "PolygonalShapes" }; - bool[] representations = { true, false }; - string[] objects = new string[] - { - @" + @" { '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Polygon', 'HasBorder':true,'Vertexes':[ @@ -80,7 +80,7 @@ public static TheoryDataSet PostToCollectionNewComp {'X':14,'Y':41} ] }", - @" + @" { '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Rectangle', 'HasBorder':true, @@ -88,34 +88,34 @@ public static TheoryDataSet PostToCollectionNewComp 'Height':4, 'TopLeft':{ 'X':1,'Y':2} }", - }; + }; - TheoryDataSet data = new TheoryDataSet(); + TheoryDataSet data = new TheoryDataSet(); - foreach(string mode in modes) + foreach(string mode in modes) + { + foreach(string obj in objects) { - foreach(string obj in objects) - { - foreach(string target in targets) - foreach(bool representation in representations) - { - data.Add(mode, obj, target, representation); - } - } + foreach(string target in targets) + foreach(bool representation in representations) + { + data.Add(mode, obj, target, representation); + } } - return data; } + return data; } + } - #region CRUD on the entity - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - // POST ~/Windows - public async Task CreateWindow(string mode) - { - string requestUri = $"{mode}/Windows"; - string content = @" + #region CRUD on the entity + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + // POST ~/Windows + public async Task CreateWindow(string mode) + { + string requestUri = $"{mode}/Windows"; + string content = @" { 'Id':0, 'Name':'Name4', @@ -137,94 +137,94 @@ public async Task CreateWindow(string mode) } ] }"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Content = new StringContent(content); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = content.Length; - HttpClient client = CreateClient(); - var response = await client.SendAsync(request); - - string contentOfString = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.Created == response.StatusCode, String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.Created, + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Content = new StringContent(content); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = content.Length; + HttpClient client = CreateClient(); + var response = await client.SendAsync(request); + + string contentOfString = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.Created == response.StatusCode, String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.Created, + response.StatusCode, + requestUri, + contentOfString)); + + Assert.Equal("4.0", response.Headers.GetValues("OData-Version").Single()); + JObject contentOfJObject = await response.Content.ReadAsObject(); + string name = (string)contentOfJObject["Name"]; + Assert.True("Name4" == name); + int radius = (int)contentOfJObject["CurrentShape"]["Radius"]; + Assert.True(10 == radius, + String.Format("\nExpected that Radius: 10, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString)); + + JArray optionalShapes = contentOfJObject["OptionalShapes"] as JArray; + Assert.True(1 == optionalShapes.Count, + String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n response payload: {3}", 1, optionalShapes.Count, requestUri, contentOfString)); + JArray vertexes = optionalShapes[0]["Vertexes"] as JArray; + Assert.True(4 == vertexes.Count, "The returned OptionalShapes is not as expected"); + } + + [Theory] + [MemberData(nameof(MediaTypes))] + // GET ~/Windows?$select=...&$orderby=...&$expand=... + public async Task QueryCollectionContainingEntity(string mode, string mime) + { + string requestUri = $"{mode}/Windows?$select=Id,CurrentShape,OptionalShapes&$orderby=CurrentShape/HasBorder&$expand=Parent&$format={mime}"; + + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); + string contentOfString = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode, + String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.Created, response.StatusCode, requestUri, contentOfString)); + JObject content = await response.Content.ReadAsObject(); + JArray windows = content["value"] as JArray; + Assert.True(3 == windows.Count); - Assert.Equal("4.0", response.Headers.GetValues("OData-Version").Single()); - JObject contentOfJObject = await response.Content.ReadAsObject(); - string name = (string)contentOfJObject["Name"]; - Assert.True("Name4" == name); - int radius = (int)contentOfJObject["CurrentShape"]["Radius"]; - Assert.True(10 == radius, - String.Format("\nExpected that Radius: 10, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString)); - - JArray optionalShapes = contentOfJObject["OptionalShapes"] as JArray; - Assert.True(1 == optionalShapes.Count, - String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n response payload: {3}", 1, optionalShapes.Count, requestUri, contentOfString)); - JArray vertexes = optionalShapes[0]["Vertexes"] as JArray; - Assert.True(4 == vertexes.Count, "The returned OptionalShapes is not as expected"); - } - - [Theory] - [MemberData(nameof(MediaTypes))] - // GET ~/Windows?$select=...&$orderby=...&$expand=... - public async Task QueryCollectionContainingEntity(string mode, string mime) - { - string requestUri = $"{mode}/Windows?$select=Id,CurrentShape,OptionalShapes&$orderby=CurrentShape/HasBorder&$expand=Parent&$format={mime}"; - - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); - string contentOfString = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode, - String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.Created, - response.StatusCode, - requestUri, - contentOfString)); - JObject content = await response.Content.ReadAsObject(); - JArray windows = content["value"] as JArray; - Assert.True(3 == windows.Count); - - JObject window1 = (JObject)windows.Single(w => (string)w["Id"] == "1"); - JArray optionalShapes = (JArray)window1["OptionalShapes"]; - Assert.True(1 == optionalShapes.Count); - - JObject window2 = (JObject)windows.Single(w => (string)w["Id"] == "2"); - Assert.Equal("1", (string)window2["Parent"]["Id"]); - } + JObject window1 = (JObject)windows.Single(w => (string)w["Id"] == "1"); + JArray optionalShapes = (JArray)window1["OptionalShapes"]; + Assert.True(1 == optionalShapes.Count); - [Theory] - [MemberData(nameof(MediaTypes))] - // GET ~/Windows?$filter=CurrentShape/HasBorder eq true - public async Task QueryEntitiesFilteredByComplexType(string mode, string mime) - { - string requestUri = $"{mode}/Windows?$filter=CurrentShape/HasBorder eq true&$format={mime}"; + JObject window2 = (JObject)windows.Single(w => (string)w["Id"] == "2"); + Assert.Equal("1", (string)window2["Parent"]["Id"]); + } - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); - string contentOfString = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode, - String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.Created, - response.StatusCode, - requestUri, - contentOfString)); - JObject content = await response.Content.ReadAsObject(); - JArray windows = content["value"] as JArray; - Assert.True(1 == windows.Count, - String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n response payload: {3}", 1, windows.Count, requestUri, contentOfString)); - } + [Theory] + [MemberData(nameof(MediaTypes))] + // GET ~/Windows?$filter=CurrentShape/HasBorder eq true + public async Task QueryEntitiesFilteredByComplexType(string mode, string mime) + { + string requestUri = $"{mode}/Windows?$filter=CurrentShape/HasBorder eq true&$format={mime}"; + + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); + string contentOfString = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode, + String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.Created, + response.StatusCode, + requestUri, + contentOfString)); + JObject content = await response.Content.ReadAsObject(); + JArray windows = content["value"] as JArray; + Assert.True(1 == windows.Count, + String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n response payload: {3}", 1, windows.Count, requestUri, contentOfString)); + } - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - // PUT ~/Windows(3) - public async Task PutContainingEntity(string modelMode) - { - string requestUri = $"{modelMode}/Windows(3)"; + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + // PUT ~/Windows(3) + public async Task PutContainingEntity(string modelMode) + { + string requestUri = $"{modelMode}/Windows(3)"; - string content = @" + string content = @" { 'Id':3, 'Name':'Name30', @@ -246,46 +246,46 @@ public async Task PutContainingEntity(string modelMode) } ] }"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, requestUri); - request.Content = new StringContent(content); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = content.Length; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, requestUri); + request.Content = new StringContent(content); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = content.Length; - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.SendAsync(request); + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.SendAsync(request); - string contentOfString = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode, - String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.Created, - response.StatusCode, - requestUri, - contentOfString)); + string contentOfString = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode, + String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.Created, + response.StatusCode, + requestUri, + contentOfString)); - JObject contentOfJObject = await response.Content.ReadAsObject(); - string name = (string)contentOfJObject["Name"]; - Assert.True("Name30" == name); - int radius = (int)contentOfJObject["CurrentShape"]["Radius"]; - Assert.True(2 == radius, - String.Format("\nExpected that Radius: 2, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString)); - - JArray windows = contentOfJObject["OptionalShapes"] as JArray; - Assert.True(1 == windows.Count, - String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n response payload: {3}", 1, windows.Count, requestUri, contentOfString)); - } + JObject contentOfJObject = await response.Content.ReadAsObject(); + string name = (string)contentOfJObject["Name"]; + Assert.True("Name30" == name); + int radius = (int)contentOfJObject["CurrentShape"]["Radius"]; + Assert.True(2 == radius, + String.Format("\nExpected that Radius: 2, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString)); - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - // Patch ~/Windows(1) - public async Task PatchContainingEntity(string modelMode) - { - string requestUri = $"{modelMode}/Windows(1)"; + JArray windows = contentOfJObject["OptionalShapes"] as JArray; + Assert.True(1 == windows.Count, + String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n response payload: {3}", 1, windows.Count, requestUri, contentOfString)); + } - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + // Patch ~/Windows(1) + public async Task PatchContainingEntity(string modelMode) + { + string requestUri = $"{modelMode}/Windows(1)"; + + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - // We should be able to PATCH nested resource with delta object of the same CLR type. - var content = @" + // We should be able to PATCH nested resource with delta object of the same CLR type. + var content = @" { 'CurrentShape': { @@ -296,45 +296,45 @@ public async Task PatchContainingEntity(string modelMode) }, 'OptionalShapes': [ ] }"; - StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); - request.Content = stringContent; - request.Content.Headers.ContentLength = content.Length; - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.SendAsync(request); - string contentOfString = await response.Content.ReadAsStringAsync(); - if (HttpStatusCode.OK != response.StatusCode) - { - Assert.True(false, String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.Created, - response.StatusCode, - requestUri, - contentOfString)); - } - JObject contentOfJObject = await response.Content.ReadAsObject(); - string name = (string)contentOfJObject["Name"]; - Assert.True("CircleWindow" == name); - int radius = (int)contentOfJObject["CurrentShape"]["Radius"]; - Assert.True(1 == radius, - String.Format("\nExpected that Radius: 2, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString)); - - JArray windows = contentOfJObject["OptionalShapes"] as JArray; - Assert.True(0 == windows.Count, - String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n response payload: {3}", 1, windows.Count, requestUri, contentOfString)); + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + request.Content = stringContent; + request.Content.Headers.ContentLength = content.Length; + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.SendAsync(request); + string contentOfString = await response.Content.ReadAsStringAsync(); + if (HttpStatusCode.OK != response.StatusCode) + { + Assert.True(false, String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.Created, + response.StatusCode, + requestUri, + contentOfString)); } + JObject contentOfJObject = await response.Content.ReadAsObject(); + string name = (string)contentOfJObject["Name"]; + Assert.True("CircleWindow" == name); + int radius = (int)contentOfJObject["CurrentShape"]["Radius"]; + Assert.True(1 == radius, + String.Format("\nExpected that Radius: 2, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString)); + + JArray windows = contentOfJObject["OptionalShapes"] as JArray; + Assert.True(0 == windows.Count, + String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n response payload: {3}", 1, windows.Count, requestUri, contentOfString)); + } - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - // Patch ~/Windows(3) - public async Task PatchContainingEntity_Matched_DerivedType(string modelMode) - { - string requestUri = $"{modelMode}/Windows(3)"; + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + // Patch ~/Windows(3) + public async Task PatchContainingEntity_Matched_DerivedType(string modelMode) + { + string requestUri = $"{modelMode}/Windows(3)"; - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - // PATCH nested resource with delta object of the different CLR type - // will return a success result. - var content = @" + // PATCH nested resource with delta object of the different CLR type + // will return a success result. + var content = @" { 'CurrentShape': { @@ -345,27 +345,27 @@ public async Task PatchContainingEntity_Matched_DerivedType(string modelMode) }, 'OptionalShapes': [ ] }"; - StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); - request.Content = stringContent; - request.Content.Headers.ContentLength = content.Length; - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.SendAsync(request); - string contentOfString = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode); - } + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + request.Content = stringContent; + request.Content.Headers.ContentLength = content.Length; + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.SendAsync(request); + string contentOfString = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode); + } - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - // Patch ~/Windows(3) - public async Task PatchContainingEntity_DeltaIsBaseType(string modelMode) - { - string requestUri = $"{modelMode}/Windows(3)"; + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + // Patch ~/Windows(3) + public async Task PatchContainingEntity_DeltaIsBaseType(string modelMode) + { + string requestUri = $"{modelMode}/Windows(3)"; - // PATCH nested resource with delta object of the base CLR type should work. - // --- PATCH #1 --- - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - var content = @" + // PATCH nested resource with delta object of the base CLR type should work. + // --- PATCH #1 --- + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + var content = @" { 'CurrentShape': { @@ -374,15 +374,15 @@ public async Task PatchContainingEntity_DeltaIsBaseType(string modelMode) }, 'OptionalShapes': [ ] }"; - string contentOfString = await ExecuteAsync(request, content); + string contentOfString = await ExecuteAsync(request, content); - // Only 'HasBoarder' is updated; 'Vertexes' still has the correct value. - Assert.Contains("\"HasBorder\":true", contentOfString); - Assert.Contains("\"Vertexes\":[{\"X\":0,\"Y\":0},{\"X\":2,\"Y\":0},{\"X\":2,\"Y\":2},{\"X\":0,\"Y\":2}]", contentOfString); + // Only 'HasBoarder' is updated; 'Vertexes' still has the correct value. + Assert.Contains("\"HasBorder\":true", contentOfString); + Assert.Contains("\"Vertexes\":[{\"X\":0,\"Y\":0},{\"X\":2,\"Y\":0},{\"X\":2,\"Y\":2},{\"X\":0,\"Y\":2}]", contentOfString); - // --- PATCH #2 --- - request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - content = @" + // --- PATCH #2 --- + request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + content = @" { 'CurrentShape': { @@ -391,27 +391,27 @@ public async Task PatchContainingEntity_DeltaIsBaseType(string modelMode) }, 'OptionalShapes': [ ] }"; - contentOfString = await ExecuteAsync(request, content); + contentOfString = await ExecuteAsync(request, content); - // Only 'Vertexes' is updated; 'HasBoarder' still has the correct value. - Assert.Contains("\"Vertexes\":[{\"X\":1,\"Y\":2},{\"X\":2,\"Y\":3},{\"X\":4,\"Y\":8}]", contentOfString); - Assert.Contains("\"HasBorder\":false", contentOfString); - } + // Only 'Vertexes' is updated; 'HasBoarder' still has the correct value. + Assert.Contains("\"Vertexes\":[{\"X\":1,\"Y\":2},{\"X\":2,\"Y\":3},{\"X\":4,\"Y\":8}]", contentOfString); + Assert.Contains("\"HasBorder\":false", contentOfString); + } - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - // Patch ~/Windows(3) - public async Task Patch_Matched_DerivedComplexType(string modelMode) - { - string requestUri = $"{modelMode}/Windows(3)/CurrentShape"; + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + // Patch ~/Windows(3) + public async Task Patch_Matched_DerivedComplexType(string modelMode) + { + string requestUri = $"{modelMode}/Windows(3)/CurrentShape"; - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - // Attempt to PATCH nested resource with delta object of the different CLR type - // will result an error. + // Attempt to PATCH nested resource with delta object of the different CLR type + // will result an error. - var content = @" + var content = @" { '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle', 'Radius':2, @@ -419,163 +419,163 @@ public async Task Patch_Matched_DerivedComplexType(string modelMode) 'HasBorder':true }"; - StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); - request.Content = stringContent; - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.SendAsync(request); - JObject contentOfJObject = await response.Content.ReadAsObject(); - Assert.Equal(2, (int)contentOfJObject["Radius"]); - Assert.True(HttpStatusCode.OK == response.StatusCode); - } - - private async Task ExecuteAsync(HttpRequestMessage request, string content) - { - StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); - request.Content = stringContent; - request.Content.Headers.ContentLength = content.Length; - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.SendAsync(request); - return await response.Content.ReadAsStringAsync(); - } - - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - // DELETE ~/Windows(1) - public async Task DeleteWindow(string modelMode) - { - string requestUri = $"{modelMode}/Windows(1)"; - - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.DeleteAsync(requestUri); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } - #endregion + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + request.Content = stringContent; + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.SendAsync(request); + JObject contentOfJObject = await response.Content.ReadAsObject(); + Assert.Equal(2, (int)contentOfJObject["Radius"]); + Assert.True(HttpStatusCode.OK == response.StatusCode); + } - #region RUD on complex type + private async Task ExecuteAsync(HttpRequestMessage request, string content) + { + StringContent stringContent = new StringContent(content: content, encoding: Encoding.UTF8, mediaType: "application/json"); + request.Content = stringContent; + request.Content.Headers.ContentLength = content.Length; + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.SendAsync(request); + return await response.Content.ReadAsStringAsync(); + } - [Theory] - [MemberData(nameof(MediaTypes))] - // GET ~/Windows(1)/CurrentShape - public async Task QueryComplexTypeProperty(string mode, string mime) - { - string requestUri = $"{mode}/Windows(1)/CurrentShape?$format={mime}"; + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + // DELETE ~/Windows(1) + public async Task DeleteWindow(string modelMode) + { + string requestUri = $"{modelMode}/Windows(1)"; - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); - string contentOfString = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode, - String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.Created, - response.StatusCode, - requestUri, - contentOfString)); - JObject content = await response.Content.ReadAsObject(); - bool hasBorder = (bool)content["HasBorder"]; - Assert.True(hasBorder, - String.Format("\nExpected that HasBorder is true, but actually not,\n request uri: {0},\n response payload: {1}", requestUri, contentOfString)); - int radius = (int)content["Radius"]; - Assert.True(2 == radius, - String.Format("\nExpected that Radius: 2, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString)); - } + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.DeleteAsync(requestUri); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + #endregion - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - // GET ~/Windows(1)/OptionalShapes/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle - public async Task GetOptionalShapesPlusCast(string modelMode) - { - string requestUri = $"{modelMode}/Windows(3)/OptionalShapes/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle"; + #region RUD on complex type - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); - string contentOfString = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode, String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.OK, - response.StatusCode, - requestUri, - contentOfString)); + [Theory] + [MemberData(nameof(MediaTypes))] + // GET ~/Windows(1)/CurrentShape + public async Task QueryComplexTypeProperty(string mode, string mime) + { + string requestUri = $"{mode}/Windows(1)/CurrentShape?$format={mime}"; + + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); + string contentOfString = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode, + String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.Created, + response.StatusCode, + requestUri, + contentOfString)); + JObject content = await response.Content.ReadAsObject(); + bool hasBorder = (bool)content["HasBorder"]; + Assert.True(hasBorder, + String.Format("\nExpected that HasBorder is true, but actually not,\n request uri: {0},\n response payload: {1}", requestUri, contentOfString)); + int radius = (int)content["Radius"]; + Assert.True(2 == radius, + String.Format("\nExpected that Radius: 2, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString)); + } - JObject contentOfJObject = await response.Content.ReadAsObject(); - JArray optionalShapes = (JArray)contentOfJObject["value"]; - Assert.True(1 == optionalShapes.Count); - } + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + // GET ~/Windows(1)/OptionalShapes/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle + public async Task GetOptionalShapesPlusCast(string modelMode) + { + string requestUri = $"{modelMode}/Windows(3)/OptionalShapes/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle"; - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - // GET ~/Windows(3)/OptionalShapes - public async Task GetOptionalShapes(string modelMode) - { - string requestUri = $"{modelMode}/Windows(3)/OptionalShapes"; + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); + string contentOfString = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode, String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.OK, + response.StatusCode, + requestUri, + contentOfString)); - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); - string contentOfString = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode, String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.OK, - response.StatusCode, - requestUri, - contentOfString)); + JObject contentOfJObject = await response.Content.ReadAsObject(); + JArray optionalShapes = (JArray)contentOfJObject["value"]; + Assert.True(1 == optionalShapes.Count); + } - JObject contentOfJObject = await response.Content.ReadAsObject(); - JArray optionalShapes = (JArray)contentOfJObject["value"]; - Assert.True(2 == optionalShapes.Count); - } + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + // GET ~/Windows(3)/OptionalShapes + public async Task GetOptionalShapes(string modelMode) + { + string requestUri = $"{modelMode}/Windows(3)/OptionalShapes"; - [Theory] - [MemberData(nameof(MediaTypes))] - // GET ~/Windows(1)/CurrentShape/HasBorder - public async Task QueryPropertyDefinedInComplexTypeProperty(string mode, string mime) - { - string requestUri = $"{mode}/Windows(1)/CurrentShape/HasBorder?$format={mime}"; + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); + string contentOfString = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode, String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.OK, + response.StatusCode, + requestUri, + contentOfString)); - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); - string contentOfString = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode, - String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.Created, - response.StatusCode, - requestUri, - contentOfString)); - JObject content = await response.Content.ReadAsObject(); - bool hasBorder = (bool)content["value"]; - Assert.True(hasBorder, - String.Format("\nExpected that HasBorder is true, but actually not,\n request uri: {0},\n response payload: {1}", requestUri, contentOfString)); - } + JObject contentOfJObject = await response.Content.ReadAsObject(); + JArray optionalShapes = (JArray)contentOfJObject["value"]; + Assert.True(2 == optionalShapes.Count); + } - [Theory] - [MemberData(nameof(MediaTypes))] - // GET ~/Windows(1)/CurrentShape/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle/Radius - public async Task QueryComplexTypePropertyDefinedOnDerivedType(string mode, string mime) - { - string requestUri = $"{mode}/Windows(1)/CurrentShape/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle/Radius?$format={mime}"; + [Theory] + [MemberData(nameof(MediaTypes))] + // GET ~/Windows(1)/CurrentShape/HasBorder + public async Task QueryPropertyDefinedInComplexTypeProperty(string mode, string mime) + { + string requestUri = $"{mode}/Windows(1)/CurrentShape/HasBorder?$format={mime}"; + + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); + string contentOfString = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode, + String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.Created, + response.StatusCode, + requestUri, + contentOfString)); + JObject content = await response.Content.ReadAsObject(); + bool hasBorder = (bool)content["value"]; + Assert.True(hasBorder, + String.Format("\nExpected that HasBorder is true, but actually not,\n request uri: {0},\n response payload: {1}", requestUri, contentOfString)); + } - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); - string contentOfString = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode, - String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.Created, - response.StatusCode, - requestUri, - contentOfString)); - JObject content = await response.Content.ReadAsObject(); - int radius = (int)content["value"]; - Assert.True(2 == radius, - String.Format("\nExpected that Radius: 2, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString)); - } + [Theory] + [MemberData(nameof(MediaTypes))] + // GET ~/Windows(1)/CurrentShape/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle/Radius + public async Task QueryComplexTypePropertyDefinedOnDerivedType(string mode, string mime) + { + string requestUri = $"{mode}/Windows(1)/CurrentShape/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle/Radius?$format={mime}"; + + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); + string contentOfString = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode, + String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.Created, + response.StatusCode, + requestUri, + contentOfString)); + JObject content = await response.Content.ReadAsObject(); + int radius = (int)content["value"]; + Assert.True(2 == radius, + String.Format("\nExpected that Radius: 2, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString)); + } - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - // PUT ~/Windows(3)/OptionalShapes - public async Task PutCollectionComplexTypeProperty(string modelMode) - { - string requestUri = $"{modelMode}/Windows(3)/OptionalShapes"; + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + // PUT ~/Windows(3)/OptionalShapes + public async Task PutCollectionComplexTypeProperty(string modelMode) + { + string requestUri = $"{modelMode}/Windows(3)/OptionalShapes"; - var content = new StringContent(content: @" + var content = new StringContent(content: @" { 'value':[ { @@ -596,90 +596,90 @@ public async Task PutCollectionComplexTypeProperty(string modelMode) } ", encoding: Encoding.UTF8, mediaType: "application/json"); - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.PutAsync(requestUri, content); - string contentOfString = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode, - String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.Created, - response.StatusCode, - requestUri, - contentOfString)); - - JObject contentOfJObject = await response.Content.ReadAsObject(); - Assert.True(2 == contentOfJObject.Count, - String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n response payload: {3}", - 2, - contentOfJObject.Count, + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.PutAsync(requestUri, content); + string contentOfString = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode, + String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.Created, + response.StatusCode, requestUri, contentOfString)); - } - [Theory] - [MemberData(nameof(PostToCollectionNewComplexTypeMembers))] - // POST ~/Windows(3)/OptionalShapes - public async Task PostToCollectionComplexTypeProperty(string modelMode, string jObject, string targetPropertyResource, bool returnRepresentation) - { - //Arrange - string requestUri = $"{modelMode}/Windows(3)/"+ targetPropertyResource; - - //send a get request to get the current count - int count = 0; - HttpClient client = CreateClient(); - using (HttpResponseMessage getResponse = await client.GetAsync(requestUri)) - { - getResponse.EnsureSuccessStatusCode(); + JObject contentOfJObject = await response.Content.ReadAsObject(); + Assert.True(2 == contentOfJObject.Count, + String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n response payload: {3}", + 2, + contentOfJObject.Count, + requestUri, + contentOfString)); + } - var json = await getResponse.Content.ReadAsObject(); - var state = json.GetValue("value") as JArray; - count = state.Count; - } + [Theory] + [MemberData(nameof(PostToCollectionNewComplexTypeMembers))] + // POST ~/Windows(3)/OptionalShapes + public async Task PostToCollectionComplexTypeProperty(string modelMode, string jObject, string targetPropertyResource, bool returnRepresentation) + { + //Arrange + string requestUri = $"{modelMode}/Windows(3)/"+ targetPropertyResource; - //Set up the post request - var requestForPost = new HttpRequestMessage(HttpMethod.Post, requestUri); - requestForPost.Content = new StringContent(content:jObject, encoding: Encoding.UTF8, mediaType: "application/json"); - if (returnRepresentation) - { - requestForPost.Headers.Add("Prefer", "return=representation"); - } - requestForPost.Content.Headers.ContentLength = jObject.Length; + //send a get request to get the current count + int count = 0; + HttpClient client = CreateClient(); + using (HttpResponseMessage getResponse = await client.GetAsync(requestUri)) + { + getResponse.EnsureSuccessStatusCode(); - //Act & Assert - HttpResponseMessage response = await client.SendAsync(requestForPost); - string contentOfString = await response.Content.ReadAsStringAsync(); + var json = await getResponse.Content.ReadAsObject(); + var state = json.GetValue("value") as JArray; + count = state.Count; + } - if(returnRepresentation) - { - JObject contentOfJObject = await response.Content.ReadAsObject(); - var result = contentOfJObject.GetValue("value") as JArray; - - Assert.True(count + 1 == result.Count, - String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.NoContent, - result.Count, - requestUri, - contentOfString)); - } - else - { - Assert.True(HttpStatusCode.NoContent == response.StatusCode, - String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.NoContent, - response.StatusCode, - requestUri, - contentOfString)); - } + //Set up the post request + var requestForPost = new HttpRequestMessage(HttpMethod.Post, requestUri); + requestForPost.Content = new StringContent(content:jObject, encoding: Encoding.UTF8, mediaType: "application/json"); + if (returnRepresentation) + { + requestForPost.Headers.Add("Prefer", "return=representation"); } + requestForPost.Content.Headers.ContentLength = jObject.Length; - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - // PUT ~/Windows(1)/CurrentShape - public async Task PutCurrentShape(string modelMode) + //Act & Assert + HttpResponseMessage response = await client.SendAsync(requestForPost); + string contentOfString = await response.Content.ReadAsStringAsync(); + + if(returnRepresentation) + { + JObject contentOfJObject = await response.Content.ReadAsObject(); + var result = contentOfJObject.GetValue("value") as JArray; + + Assert.True(count + 1 == result.Count, + String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.NoContent, + result.Count, + requestUri, + contentOfString)); + } + else { - // Arrange - string requestUri = $"{modelMode}/Windows(1)/CurrentShape/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle"; - string content = @" + Assert.True(HttpStatusCode.NoContent == response.StatusCode, + String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.NoContent, + response.StatusCode, + requestUri, + contentOfString)); + } + } + + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + // PUT ~/Windows(1)/CurrentShape + public async Task PutCurrentShape(string modelMode) + { + // Arrange + string requestUri = $"{modelMode}/Windows(1)/CurrentShape/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle"; + string content = @" { '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle', 'Radius':5, @@ -687,94 +687,93 @@ public async Task PutCurrentShape(string modelMode) 'HasBorder':true }"; - // Act - HttpClient client = CreateClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, requestUri); - request.Content = new StringContent(content); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = content.Length; - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - string contentOfString = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.OK == response.StatusCode, String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", - HttpStatusCode.OK, - response.StatusCode, - requestUri, - contentOfString)); + // Act + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, requestUri); + request.Content = new StringContent(content); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = content.Length; + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + string contentOfString = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode, String.Format("\nExpected status code: {0},\n actual: {1},\n request uri: {2},\n message: {3}", + HttpStatusCode.OK, + response.StatusCode, + requestUri, + contentOfString)); - JObject contentOfJObject = await response.Content.ReadAsObject(); - int radius = (int)contentOfJObject["Radius"]; - Assert.True(5 == radius, - String.Format("\nExpected that Radius: 5, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString)); - } + JObject contentOfJObject = await response.Content.ReadAsObject(); + int radius = (int)contentOfJObject["Radius"]; + Assert.True(5 == radius, + String.Format("\nExpected that Radius: 5, but actually: {0},\n request uri: {1},\n response payload: {2}", radius, requestUri, contentOfString)); + } - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - // PATCH ~/Windows(3)/OptionalShapes - public async Task PatchToCollectionComplexTypePropertyNotSupported(string modelMode) - { - // Arrange - string requestUri = $"{modelMode}/Windows(3)/OptionalShapes"; + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + // PATCH ~/Windows(3)/OptionalShapes + public async Task PatchToCollectionComplexTypePropertyNotSupported(string modelMode) + { + // Arrange + string requestUri = $"{modelMode}/Windows(3)/OptionalShapes"; - // Act - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.PatchAsync(requestUri, ""); + // Act + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.PatchAsync(requestUri, ""); - // Assert - Assert.True(HttpStatusCode.MethodNotAllowed == response.StatusCode); - } + // Assert + Assert.True(HttpStatusCode.MethodNotAllowed == response.StatusCode); + } - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task PatchToSingleComplexTypeProperty(string modelMode) - { - // Arrange - string requestUri = $"{modelMode}/Windows(1)/CurrentShape/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle"; - string content = @" + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task PatchToSingleComplexTypeProperty(string modelMode) + { + // Arrange + string requestUri = $"{modelMode}/Windows(1)/CurrentShape/Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle"; + string content = @" { '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ComplexTypeInheritance.Circle', 'Radius':15, 'HasBorder':true }"; - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("Patch"), requestUri); - request.Content = new StringContent(content); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = content.Length; + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("Patch"), requestUri); + request.Content = new StringContent(content); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = content.Length; - // Act - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - JObject contentOfJObject = await response.Content.ReadAsObject(); - int radius = (int)contentOfJObject["Radius"]; - Assert.Equal(15, radius); - } + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task DeleteToNullableComplexTypeProperty(string modelMode) - { - // Arrange - string requestUri = $"{modelMode}/Windows(1)/CurrentShape"; + JObject contentOfJObject = await response.Content.ReadAsObject(); + int radius = (int)contentOfJObject["Radius"]; + Assert.Equal(15, radius); + } - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("Delete"), requestUri); + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task DeleteToNullableComplexTypeProperty(string modelMode) + { + // Arrange + string requestUri = $"{modelMode}/Windows(1)/CurrentShape"; - // Act - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.SendAsync(request); + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("Delete"), requestUri); - // Assert - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } + // Act + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.SendAsync(request); - #endregion + // Assert + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } + + #endregion } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryController.cs index c64f61b3f..61d445fb3 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryController.cs @@ -9,23 +9,22 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Query; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ConcurrentQuery +namespace Microsoft.AspNetCore.OData.E2E.Tests.ConcurrentQuery; + +public class CustomersController : Controller { - public class CustomersController : Controller + [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Count | AllowedQueryOptions.Filter | AllowedQueryOptions.Expand)] + public IQueryable GetCustomers() { - [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Count | AllowedQueryOptions.Filter | AllowedQueryOptions.Expand)] - public IQueryable GetCustomers() - { - return Enumerable.Range(1, 100) - .Select(i => new Customer + return Enumerable.Range(1, 100) + .Select(i => new Customer + { + Id = i, + Orders = Enumerable.Range(1, 5) + .Select(x => new Order { - Id = i, - Orders = Enumerable.Range(1, 5) - .Select(x => new Order - { - Id = x - }) - }).AsQueryable(); - } + Id = x + }) + }).AsQueryable(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryEdmModel.cs index e43fc65cd..27d59bb92 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryEdmModel.cs @@ -8,15 +8,14 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ConcurrentQuery +namespace Microsoft.AspNetCore.OData.E2E.Tests.ConcurrentQuery; + +public class ConcurrentQueryEdmModel { - public class ConcurrentQueryEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryODataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryODataModel.cs index 3dc9a6bab..2d1c0a4b4 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryODataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryODataModel.cs @@ -8,17 +8,16 @@ using System.Collections.Generic; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ConcurrentQuery +namespace Microsoft.AspNetCore.OData.E2E.Tests.ConcurrentQuery; + +public class Customer { - public class Customer - { - public int Id { get; set; } - [Contained] - public IEnumerable Orders { get; set; } - } + public int Id { get; set; } + [Contained] + public IEnumerable Orders { get; set; } +} - public class Order - { - public int Id { get; set; } - } +public class Order +{ + public int Id { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryTests.cs index d17c9b310..a031a5151 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ConcurrentQuery/ConcurrentQueryTests.cs @@ -17,97 +17,96 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ConcurrentQuery +namespace Microsoft.AspNetCore.OData.E2E.Tests.ConcurrentQuery; + +/// +/// Ensures that concurrent execution of EnableQuery is thread-safe. +/// +public class ConcurrentQueryTests : WebApiTestBase { - /// - /// Ensures that concurrent execution of EnableQuery is thread-safe. - /// - public class ConcurrentQueryTests : WebApiTestBase + public ConcurrentQueryTests(WebApiTestFixture fixture) + : base(fixture) { - public ConcurrentQueryTests(WebApiTestFixture fixture) - : base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(CustomersController)); + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(CustomersController)); - var model = ConcurrentQueryEdmModel.GetEdmModel(); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("concurrentquery", model) - .Count().Filter().OrderBy().Expand().SetMaxTop(null)); - } + var model = ConcurrentQueryEdmModel.GetEdmModel(); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("concurrentquery", model) + .Count().Filter().OrderBy().Expand().SetMaxTop(null)); + } - /// - /// For OData paths enable query should work with expansion. - /// - /// Task tracking operation. - [Fact] - public async Task ConcurrentQueryExecutionIsThreadSafe() - { - // Arrange - HttpClient client = CreateClient(); + /// + /// For OData paths enable query should work with expansion. + /// + /// Task tracking operation. + [Fact] + public async Task ConcurrentQueryExecutionIsThreadSafe() + { + // Arrange + HttpClient client = CreateClient(); - // Bumping thread count to allow higher parallelization. - ThreadPool.SetMinThreads(100, 100); + // Bumping thread count to allow higher parallelization. + ThreadPool.SetMinThreads(100, 100); - // Act - var results = await Task.WhenAll( - Enumerable.Range(1, 100) - .Select(async i => - { - string queryUrl = $"concurrentquery/Customers?$filter=Id gt {i}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + // Act + var results = await Task.WhenAll( + Enumerable.Range(1, 100) + .Select(async i => + { + string queryUrl = $"concurrentquery/Customers?$filter=Id gt {i}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpResponseMessage response = await client.SendAsync(request); + HttpResponseMessage response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); - return (i: i, length: customers.Count); - })); + return (i: i, length: customers.Count); + })); - // Assert - foreach (var result in results) - { - Assert.Equal(100 - result.i, result.length); - } + // Assert + foreach (var result in results) + { + Assert.Equal(100 - result.i, result.length); } + } - [Fact] - public async Task ConcurrentExpandQueryExecutionIsThreadSafe() - { - // Arrange - HttpClient client = CreateClient(); + [Fact] + public async Task ConcurrentExpandQueryExecutionIsThreadSafe() + { + // Arrange + HttpClient client = CreateClient(); - // Bumping thread count to allow higher parallelization. - ThreadPool.SetMinThreads(100, 100); + // Bumping thread count to allow higher parallelization. + ThreadPool.SetMinThreads(100, 100); - // Act - var results = await Task.WhenAll( - Enumerable.Range(1, 100) - .Select(async i => - { - string queryUrl = $"concurrentquery/Customers?$expand=Orders"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + // Act + var results = await Task.WhenAll( + Enumerable.Range(1, 100) + .Select(async i => + { + string queryUrl = $"concurrentquery/Customers?$expand=Orders"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpResponseMessage response = await client.SendAsync(request); + HttpResponseMessage response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); - return customers.Count; - })); + return customers.Count; + })); - // Assert - foreach (var result in results) - { - Assert.Equal(100, result); - } + // Assert + foreach (var result in results) + { + Assert.Equal(100, result); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayContext.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayContext.cs index 6ec9bf435..ee35c8b39 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayContext.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayContext.cs @@ -7,25 +7,24 @@ using Microsoft.EntityFrameworkCore; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay; + +public class DateAndTimeOfDayContext : DbContext { - public class DateAndTimeOfDayContext : DbContext + public DateAndTimeOfDayContext(DbContextOptions options) + : base(options) { - public DateAndTimeOfDayContext(DbContextOptions options) - : base(options) - { - } - - public DbSet Customers { get; set; } } - public class EdmDateWithEfContext : DbContext - { - public EdmDateWithEfContext(DbContextOptions options) - : base(options) - { - } + public DbSet Customers { get; set; } +} - public DbSet People { get; set; } +public class EdmDateWithEfContext : DbContext +{ + public EdmDateWithEfContext(DbContextOptions options) + : base(options) + { } + + public DbSet People { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayController.cs index ed2af3369..2d6401a60 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayController.cs @@ -18,215 +18,214 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay -{ - public class DCustomersController : ODataController - { - private static IList _customers; - - private static void InitCustomers() - { - DateTimeOffset dto = new DateTimeOffset(2015, 1, 1, 1, 2, 3, 4, TimeSpan.Zero); - _customers = Enumerable.Range(1, 5).Select(e => - new DCustomer - { - Id = e, - DateTime = dto.AddYears(e).DateTime, - Offset = e % 2 == 0 ? dto.AddMonths(e) : dto.AddDays(e).AddMilliseconds(10), - Date = e % 2 == 0 ? dto.AddDays(e).Date : dto.AddDays(-e).Date, - TimeOfDay = e % 3 == 0 ? dto.AddHours(e).TimeOfDay : dto.AddHours(-e).AddMilliseconds(10).TimeOfDay, - - NullableDateTime = e % 2 == 0 ? (DateTime?)null : dto.AddYears(e).DateTime, - NullableOffset = e % 3 == 0 ? (DateTimeOffset?)null : dto.AddMonths(e), - NullableDate = e % 2 == 0 ? (Date?)null : dto.AddDays(e).Date, - NullableTimeOfDay = e % 3 == 0 ? (TimeOfDay?)null : dto.AddHours(e).TimeOfDay, - - DateTimes = new [] { dto.AddYears(e).DateTime, dto.AddMonths(e).DateTime }, - Offsets = new [] { dto.AddMonths(e), dto.AddDays(e) }, - Dates = new [] { (Date)dto.AddYears(e).Date, (Date)dto.AddMonths(e).Date }, - TimeOfDays = new [] { (TimeOfDay)dto.AddHours(e).TimeOfDay, (TimeOfDay)dto.AddMinutes(e).TimeOfDay }, - - NullableDateTimes = new [] { dto.AddYears(e).DateTime, (DateTime?)null, dto.AddMonths(e).DateTime }, - NullableOffsets = new [] { dto.AddMonths(e), (DateTimeOffset?)null, dto.AddDays(e) }, - NullableDates = new [] { (Date)dto.AddYears(e).Date, (Date?)null, (Date)dto.AddMonths(e).Date }, - NullableTimeOfDays = new [] { (TimeOfDay)dto.AddHours(e).TimeOfDay, (TimeOfDay?)null, (TimeOfDay)dto.AddMinutes(e).TimeOfDay }, +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay; - }).ToList(); - } +public class DCustomersController : ODataController +{ + private static IList _customers; - public DCustomersController() - { - if (_customers == null) + private static void InitCustomers() + { + DateTimeOffset dto = new DateTimeOffset(2015, 1, 1, 1, 2, 3, 4, TimeSpan.Zero); + _customers = Enumerable.Range(1, 5).Select(e => + new DCustomer { - InitCustomers(); - } - } + Id = e, + DateTime = dto.AddYears(e).DateTime, + Offset = e % 2 == 0 ? dto.AddMonths(e) : dto.AddDays(e).AddMilliseconds(10), + Date = e % 2 == 0 ? dto.AddDays(e).Date : dto.AddDays(-e).Date, + TimeOfDay = e % 3 == 0 ? dto.AddHours(e).TimeOfDay : dto.AddHours(-e).AddMilliseconds(10).TimeOfDay, + + NullableDateTime = e % 2 == 0 ? (DateTime?)null : dto.AddYears(e).DateTime, + NullableOffset = e % 3 == 0 ? (DateTimeOffset?)null : dto.AddMonths(e), + NullableDate = e % 2 == 0 ? (Date?)null : dto.AddDays(e).Date, + NullableTimeOfDay = e % 3 == 0 ? (TimeOfDay?)null : dto.AddHours(e).TimeOfDay, + + DateTimes = new [] { dto.AddYears(e).DateTime, dto.AddMonths(e).DateTime }, + Offsets = new [] { dto.AddMonths(e), dto.AddDays(e) }, + Dates = new [] { (Date)dto.AddYears(e).Date, (Date)dto.AddMonths(e).Date }, + TimeOfDays = new [] { (TimeOfDay)dto.AddHours(e).TimeOfDay, (TimeOfDay)dto.AddMinutes(e).TimeOfDay }, + + NullableDateTimes = new [] { dto.AddYears(e).DateTime, (DateTime?)null, dto.AddMonths(e).DateTime }, + NullableOffsets = new [] { dto.AddMonths(e), (DateTimeOffset?)null, dto.AddDays(e) }, + NullableDates = new [] { (Date)dto.AddYears(e).Date, (Date?)null, (Date)dto.AddMonths(e).Date }, + NullableTimeOfDays = new [] { (TimeOfDay)dto.AddHours(e).TimeOfDay, (TimeOfDay?)null, (TimeOfDay)dto.AddMinutes(e).TimeOfDay }, + + }).ToList(); + } - [EnableQuery] - public IActionResult Get() + public DCustomersController() + { + if (_customers == null) { - return Ok(_customers); + InitCustomers(); } + } - public IActionResult Get(int key) - { - DCustomer customer = _customers.FirstOrDefault(e => e.Id == key); - if (customer == null) - { - return NotFound(); - } - - return Ok(customer); - } + [EnableQuery] + public IActionResult Get() + { + return Ok(_customers); + } - [HttpGet] - public IActionResult BoundFunction(int key, [FromODataUri]Date modifiedDate, [FromODataUri]TimeOfDay modifiedTime, - [FromODataUri]Date? nullableModifiedDate, [FromODataUri]TimeOfDay? nullableModifiedTime) + public IActionResult Get(int key) + { + DCustomer customer = _customers.FirstOrDefault(e => e.Id == key); + if (customer == null) { - return Ok(BuildString(modifiedDate, modifiedTime, nullableModifiedDate, nullableModifiedTime)); + return NotFound(); } - [HttpGet("/convention/UnboundFunction(modifiedDate={p1},modifiedTime={p2},nullableModifiedDate={p3},nullableModifiedTime={p4})")] - [HttpGet("/explicit/UnboundFunction(modifiedDate={p1},modifiedTime={p2},nullableModifiedDate={p3},nullableModifiedTime={p4})")] - public IActionResult UnboundFunction([FromODataUri]Date p1, [FromODataUri]TimeOfDay p2, - [FromODataUri]Date? p3, [FromODataUri]TimeOfDay? p4) - { - return Ok(BuildString(p1,p2,p3,p4)); - } + return Ok(customer); + } - [HttpPost] - public IActionResult BoundAction(int key, [FromBody]ODataActionParameters parameters) - { - VerifyActionParameters(parameters); - return Ok(true); - } + [HttpGet] + public IActionResult BoundFunction(int key, [FromODataUri]Date modifiedDate, [FromODataUri]TimeOfDay modifiedTime, + [FromODataUri]Date? nullableModifiedDate, [FromODataUri]TimeOfDay? nullableModifiedTime) + { + return Ok(BuildString(modifiedDate, modifiedTime, nullableModifiedDate, nullableModifiedTime)); + } - [HttpPost("convention/UnboundAction")] - [HttpPost("explicit/UnboundAction")] - public IActionResult UnboundAction([FromBody]ODataActionParameters parameters) - { - VerifyActionParameters(parameters); - return Ok(true); - } + [HttpGet("/convention/UnboundFunction(modifiedDate={p1},modifiedTime={p2},nullableModifiedDate={p3},nullableModifiedTime={p4})")] + [HttpGet("/explicit/UnboundFunction(modifiedDate={p1},modifiedTime={p2},nullableModifiedDate={p3},nullableModifiedTime={p4})")] + public IActionResult UnboundFunction([FromODataUri]Date p1, [FromODataUri]TimeOfDay p2, + [FromODataUri]Date? p3, [FromODataUri]TimeOfDay? p4) + { + return Ok(BuildString(p1,p2,p3,p4)); + } - private static void VerifyActionParameters([FromBody]ODataActionParameters parameters) - { - Assert.True(parameters.ContainsKey("modifiedDate")); - Assert.True(parameters.ContainsKey("modifiedTime")); - Assert.True(parameters.ContainsKey("nullableModifiedDate")); - Assert.True(parameters.ContainsKey("nullableModifiedTime")); - Assert.True(parameters.ContainsKey("dates")); + [HttpPost] + public IActionResult BoundAction(int key, [FromBody]ODataActionParameters parameters) + { + VerifyActionParameters(parameters); + return Ok(true); + } - Assert.Equal(new Date(2015, 3, 1), parameters["modifiedDate"]); - Assert.Equal(new TimeOfDay(1, 5, 6, 8), parameters["modifiedTime"]); + [HttpPost("convention/UnboundAction")] + [HttpPost("explicit/UnboundAction")] + public IActionResult UnboundAction([FromBody]ODataActionParameters parameters) + { + VerifyActionParameters(parameters); + return Ok(true); + } - Assert.Null(parameters["nullableModifiedDate"]); - Assert.Null(parameters["nullableModifiedTime"]); + private static void VerifyActionParameters([FromBody]ODataActionParameters parameters) + { + Assert.True(parameters.ContainsKey("modifiedDate")); + Assert.True(parameters.ContainsKey("modifiedTime")); + Assert.True(parameters.ContainsKey("nullableModifiedDate")); + Assert.True(parameters.ContainsKey("nullableModifiedTime")); + Assert.True(parameters.ContainsKey("dates")); - IEnumerable dates = parameters["dates"] as IEnumerable; - Assert.NotNull(dates); - Assert.Equal(2, dates.Count()); - } + Assert.Equal(new Date(2015, 3, 1), parameters["modifiedDate"]); + Assert.Equal(new TimeOfDay(1, 5, 6, 8), parameters["modifiedTime"]); - private static string BuildString(Date modifiedDate, TimeOfDay modifiedTime, - Date? nullableModifiedDate, TimeOfDay? nullableModifiedTime) - { - StringBuilder sb = new StringBuilder(); - sb.Append("modifiedDate:").Append(modifiedDate).Append(","); - sb.Append("modifiedTime:").Append(modifiedTime).Append(","); - sb.Append("nullableModifiedDate:").Append(nullableModifiedDate == null ? "null" : nullableModifiedDate.ToString()).Append(","); - sb.Append("nullableModifiedTime:").Append(nullableModifiedTime == null ? "null" : nullableModifiedTime.ToString()); - return sb.ToString(); - } + Assert.Null(parameters["nullableModifiedDate"]); + Assert.Null(parameters["nullableModifiedTime"]); + + IEnumerable dates = parameters["dates"] as IEnumerable; + Assert.NotNull(dates); + Assert.Equal(2, dates.Count()); } - public class EfCustomersController : ODataController + private static string BuildString(Date modifiedDate, TimeOfDay modifiedTime, + Date? nullableModifiedDate, TimeOfDay? nullableModifiedTime) { - private readonly DateAndTimeOfDayContext _db; - - public EfCustomersController(DateAndTimeOfDayContext context) - { - context.Database.EnsureCreated(); - _db = context; + StringBuilder sb = new StringBuilder(); + sb.Append("modifiedDate:").Append(modifiedDate).Append(","); + sb.Append("modifiedTime:").Append(modifiedTime).Append(","); + sb.Append("nullableModifiedDate:").Append(nullableModifiedDate == null ? "null" : nullableModifiedDate.ToString()).Append(","); + sb.Append("nullableModifiedTime:").Append(nullableModifiedTime == null ? "null" : nullableModifiedTime.ToString()); + return sb.ToString(); + } +} - if (!context.Customers.Any()) - { - DateTimeOffset dateTime = new DateTimeOffset(2014, 12, 24, 1, 2, 3, 4, new TimeSpan(-8, 0, 0)); - IEnumerable customers = Enumerable.Range(1, 5).Select(e => - new EfCustomer - { - // Id = e, - DateTime = dateTime.AddYears(e).AddHours(e).AddMilliseconds(e).DateTime, - NullableDateTime = e % 2 == 0 ? (DateTime?)null : dateTime.AddHours(e * 5).AddMilliseconds(e * 5).DateTime, - Offset = dateTime.AddMonths(e).AddHours(e).AddMilliseconds(e), - NullableOffset = e % 3 == 0 ? (DateTimeOffset?)null : dateTime.AddDays(e).AddHours(e * 5) - }).ToList(); - - foreach (EfCustomer customer in customers) - { - context.Customers.Add(customer); - } +public class EfCustomersController : ODataController +{ + private readonly DateAndTimeOfDayContext _db; - context.SaveChanges(); - } - } + public EfCustomersController(DateAndTimeOfDayContext context) + { + context.Database.EnsureCreated(); + _db = context; - [EnableQuery] - public IActionResult Get() + if (!context.Customers.Any()) { - return Ok(_db.Customers); - } + DateTimeOffset dateTime = new DateTimeOffset(2014, 12, 24, 1, 2, 3, 4, new TimeSpan(-8, 0, 0)); + IEnumerable customers = Enumerable.Range(1, 5).Select(e => + new EfCustomer + { + // Id = e, + DateTime = dateTime.AddYears(e).AddHours(e).AddMilliseconds(e).DateTime, + NullableDateTime = e % 2 == 0 ? (DateTime?)null : dateTime.AddHours(e * 5).AddMilliseconds(e * 5).DateTime, + Offset = dateTime.AddMonths(e).AddHours(e).AddMilliseconds(e), + NullableOffset = e % 3 == 0 ? (DateTimeOffset?)null : dateTime.AddDays(e).AddHours(e * 5) + }).ToList(); - public IActionResult Get(int key) - { - EfCustomer customer = _db.Customers.FirstOrDefault(e => e.Id == key); - if (customer == null) + foreach (EfCustomer customer in customers) { - return NotFound(); + context.Customers.Add(customer); } - return Ok(customer); + context.SaveChanges(); } } - public class EfPeopleController : ODataController + [EnableQuery] + public IActionResult Get() { - private readonly EdmDateWithEfContext _db; + return Ok(_db.Customers); + } - public EfPeopleController(EdmDateWithEfContext context) + public IActionResult Get(int key) + { + EfCustomer customer = _db.Customers.FirstOrDefault(e => e.Id == key); + if (customer == null) { - context.Database.EnsureCreated(); - _db = context; + return NotFound(); + } - if (_db.People.Any()) - { - return; - } + return Ok(customer); + } +} - var people = Enumerable.Range(1, 5).Select(e => new EfPerson - { - // Id = e, - Birthday = e % 2 == 0 ? (DateTime?)null : new DateTime(2015, 10, e) - }); +public class EfPeopleController : ODataController +{ + private readonly EdmDateWithEfContext _db; - foreach (var person in people) - { - _db.People.Add(person); - } + public EfPeopleController(EdmDateWithEfContext context) + { + context.Database.EnsureCreated(); + _db = context; - _db.SaveChanges(); + if (_db.People.Any()) + { + return; } - [EnableQuery] - public IActionResult Get() + var people = Enumerable.Range(1, 5).Select(e => new EfPerson { - return Ok(_db.People); - } + // Id = e, + Birthday = e % 2 == 0 ? (DateTime?)null : new DateTime(2015, 10, e) + }); - [EnableQuery] - public async Task> Get(int key) + foreach (var person in people) { - return await Task.FromResult(SingleResult.Create(_db.People.Where(c => c.Id == key))); + _db.People.Add(person); } + + _db.SaveChanges(); + } + + [EnableQuery] + public IActionResult Get() + { + return Ok(_db.People); + } + + [EnableQuery] + public async Task> Get(int key) + { + return await Task.FromResult(SingleResult.Create(_db.People.Where(c => c.Id == key))); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayEdmModel.cs index b2d95fdd1..13b65ff23 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayEdmModel.cs @@ -13,121 +13,120 @@ using EdmPrimitiveTypeKind = Microsoft.OData.Edm.EdmPrimitiveTypeKind; using IEdmModel = Microsoft.OData.Edm.IEdmModel; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay; + +public class DateAndTimeOfDayEdmModel { - public class DateAndTimeOfDayEdmModel + public static IEdmModel GetExplicitModel() + { + ODataModelBuilder builder = new ODataModelBuilder(); + var customerType = builder.EntityType().HasKey(c => c.Id); + customerType.Property(c => c.DateTime); + customerType.Property(c => c.Offset); + customerType.Property(c => c.Date); + customerType.Property(c => c.TimeOfDay); + + customerType.Property(c => c.NullableDateTime); + customerType.Property(c => c.NullableOffset); + customerType.Property(c => c.NullableDate); + customerType.Property(c => c.NullableTimeOfDay); + + customerType.CollectionProperty(c => c.DateTimes); + customerType.CollectionProperty(c => c.Offsets); + customerType.CollectionProperty(c => c.Dates); + customerType.CollectionProperty(c => c.TimeOfDays); + + customerType.CollectionProperty(c => c.NullableDateTimes); + customerType.CollectionProperty(c => c.NullableOffsets); + customerType.CollectionProperty(c => c.NullableDates); + customerType.CollectionProperty(c => c.NullableTimeOfDays); + + var customers = builder.EntitySet("DCustomers"); + // customers.HasIdLink(link, true); + // customers.HasEditLink(link, true); + + BuildFunctions(builder); + BuildActions(builder); + + return builder.GetEdmModel(); + } + + public static IEdmModel GetConventionModel() { - public static IEdmModel GetExplicitModel() - { - ODataModelBuilder builder = new ODataModelBuilder(); - var customerType = builder.EntityType().HasKey(c => c.Id); - customerType.Property(c => c.DateTime); - customerType.Property(c => c.Offset); - customerType.Property(c => c.Date); - customerType.Property(c => c.TimeOfDay); - - customerType.Property(c => c.NullableDateTime); - customerType.Property(c => c.NullableOffset); - customerType.Property(c => c.NullableDate); - customerType.Property(c => c.NullableTimeOfDay); - - customerType.CollectionProperty(c => c.DateTimes); - customerType.CollectionProperty(c => c.Offsets); - customerType.CollectionProperty(c => c.Dates); - customerType.CollectionProperty(c => c.TimeOfDays); - - customerType.CollectionProperty(c => c.NullableDateTimes); - customerType.CollectionProperty(c => c.NullableOffsets); - customerType.CollectionProperty(c => c.NullableDates); - customerType.CollectionProperty(c => c.NullableTimeOfDays); - - var customers = builder.EntitySet("DCustomers"); - // customers.HasIdLink(link, true); - // customers.HasEditLink(link, true); - - BuildFunctions(builder); - BuildActions(builder); - - return builder.GetEdmModel(); - } - - public static IEdmModel GetConventionModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("DCustomers"); - - BuildFunctions(builder); - BuildActions(builder); - - builder.EntitySet("EfCustomers"); - - return builder.GetEdmModel(); - } - - private static void BuildFunctions(ODataModelBuilder builder) - { - FunctionConfiguration function = builder.EntityType().Function("BoundFunction") - .ReturnsCollectionViaEntitySetPath("bindingParameter"); - function.Parameter("modifiedDate"); - function.Parameter("modifiedTime"); - function.Parameter("nullableModifiedDate"); - function.Parameter("nullableModifiedTime"); - - function = builder.Function("UnboundFunction").ReturnsCollectionFromEntitySet("DCustomers"); - function.Parameter("modifiedDate"); - function.Parameter("modifiedTime"); - function.Parameter("nullableModifiedDate"); - function.Parameter("nullableModifiedTime"); - } - - private static void BuildActions(ODataModelBuilder builder) - { - ActionConfiguration action = builder.EntityType().Action("BoundAction"); - action.Parameter("modifiedDate"); - action.Parameter("modifiedTime"); - action.Parameter("nullableModifiedDate"); - action.Parameter("nullableModifiedTime"); - action.CollectionParameter("dates"); - - action = builder.Action("UnboundAction"); - action.Parameter("modifiedDate"); - action.Parameter("modifiedTime"); - action.Parameter("nullableModifiedDate"); - action.Parameter("nullableModifiedTime"); - action.CollectionParameter("dates"); - - // just for reset the data source - builder.Action("ResetDataSource"); - } - - //private static Func link = entityContext => - //{ - // object id; - // entityContext.EdmObject.TryGetPropertyValue("Id", out id); - // string uri = ResourceContextHelper.CreateODataLink(entityContext, - // new EntitySetSegment(entityContext.NavigationSource as IEdmEntitySet), - // new KeySegment(new[] { new KeyValuePair("Id", id) }, entityContext.StructuredType as IEdmEntityType, null)); - // return new Uri(uri); - //}; - - public static IEdmModel BuildEfPersonEdmModel() - { - string Namespace = typeof(EfPerson).Namespace; - - EdmModel model = new EdmModel(); - - EdmEntityType person = new EdmEntityType(Namespace, "Person"); - person.AddKeys(person.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32, isNullable: false)); - person.AddStructuralProperty("Birthday", EdmPrimitiveTypeKind.Date, isNullable: true); - model.AddElement(person); - - EdmEntityContainer container = new EdmEntityContainer(Namespace, "Default"); - container.AddEntitySet("EfPeople", person); - - model.AddElement(container); - model.SetAnnotationValue(person, new ClrTypeAnnotation(typeof(EfPerson))); - - return model; - } + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("DCustomers"); + + BuildFunctions(builder); + BuildActions(builder); + + builder.EntitySet("EfCustomers"); + + return builder.GetEdmModel(); + } + + private static void BuildFunctions(ODataModelBuilder builder) + { + FunctionConfiguration function = builder.EntityType().Function("BoundFunction") + .ReturnsCollectionViaEntitySetPath("bindingParameter"); + function.Parameter("modifiedDate"); + function.Parameter("modifiedTime"); + function.Parameter("nullableModifiedDate"); + function.Parameter("nullableModifiedTime"); + + function = builder.Function("UnboundFunction").ReturnsCollectionFromEntitySet("DCustomers"); + function.Parameter("modifiedDate"); + function.Parameter("modifiedTime"); + function.Parameter("nullableModifiedDate"); + function.Parameter("nullableModifiedTime"); + } + + private static void BuildActions(ODataModelBuilder builder) + { + ActionConfiguration action = builder.EntityType().Action("BoundAction"); + action.Parameter("modifiedDate"); + action.Parameter("modifiedTime"); + action.Parameter("nullableModifiedDate"); + action.Parameter("nullableModifiedTime"); + action.CollectionParameter("dates"); + + action = builder.Action("UnboundAction"); + action.Parameter("modifiedDate"); + action.Parameter("modifiedTime"); + action.Parameter("nullableModifiedDate"); + action.Parameter("nullableModifiedTime"); + action.CollectionParameter("dates"); + + // just for reset the data source + builder.Action("ResetDataSource"); + } + + //private static Func link = entityContext => + //{ + // object id; + // entityContext.EdmObject.TryGetPropertyValue("Id", out id); + // string uri = ResourceContextHelper.CreateODataLink(entityContext, + // new EntitySetSegment(entityContext.NavigationSource as IEdmEntitySet), + // new KeySegment(new[] { new KeyValuePair("Id", id) }, entityContext.StructuredType as IEdmEntityType, null)); + // return new Uri(uri); + //}; + + public static IEdmModel BuildEfPersonEdmModel() + { + string Namespace = typeof(EfPerson).Namespace; + + EdmModel model = new EdmModel(); + + EdmEntityType person = new EdmEntityType(Namespace, "Person"); + person.AddKeys(person.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32, isNullable: false)); + person.AddStructuralProperty("Birthday", EdmPrimitiveTypeKind.Date, isNullable: true); + model.AddElement(person); + + EdmEntityContainer container = new EdmEntityContainer(Namespace, "Default"); + container.AddEntitySet("EfPeople", person); + + model.AddElement(container); + model.SetAnnotationValue(person, new ClrTypeAnnotation(typeof(EfPerson))); + + return model; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayModel.cs index 868b69d68..503a0dd02 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayModel.cs @@ -10,56 +10,55 @@ using System.ComponentModel.DataAnnotations.Schema; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay; + +public class DCustomer { - public class DCustomer - { - public int Id { get; set; } + public int Id { get; set; } - // non-nullable - public DateTime DateTime { get; set; } - public DateTimeOffset Offset { get; set; } - public Date Date { get; set; } - public TimeOfDay TimeOfDay { get; set; } + // non-nullable + public DateTime DateTime { get; set; } + public DateTimeOffset Offset { get; set; } + public Date Date { get; set; } + public TimeOfDay TimeOfDay { get; set; } - // nullable - public DateTime? NullableDateTime { get; set; } - public DateTimeOffset? NullableOffset { get; set; } - public Date? NullableDate { get; set; } - public TimeOfDay? NullableTimeOfDay { get; set; } + // nullable + public DateTime? NullableDateTime { get; set; } + public DateTimeOffset? NullableOffset { get; set; } + public Date? NullableDate { get; set; } + public TimeOfDay? NullableTimeOfDay { get; set; } - // Collection - public IList DateTimes { get; set; } - public IList Offsets { get; set; } - public IList Dates { get; set; } - public IList TimeOfDays { get; set; } + // Collection + public IList DateTimes { get; set; } + public IList Offsets { get; set; } + public IList Dates { get; set; } + public IList TimeOfDays { get; set; } - // Collection of nullable - public IList NullableDateTimes { get; set; } - public IList NullableOffsets { get; set; } - public IList NullableDates { get; set; } - public IList NullableTimeOfDays { get; set; } - } + // Collection of nullable + public IList NullableDateTimes { get; set; } + public IList NullableOffsets { get; set; } + public IList NullableDates { get; set; } + public IList NullableTimeOfDays { get; set; } +} - public class EfCustomer - { - public int Id { get; set; } +public class EfCustomer +{ + public int Id { get; set; } - // non-nullable - [Column(TypeName = "datetime2")] - public DateTime DateTime { get; set; } - public DateTimeOffset Offset { get; set; } + // non-nullable + [Column(TypeName = "datetime2")] + public DateTime DateTime { get; set; } + public DateTimeOffset Offset { get; set; } - // nullable - public DateTime? NullableDateTime { get; set; } - public DateTimeOffset? NullableOffset { get; set; } - } + // nullable + public DateTime? NullableDateTime { get; set; } + public DateTimeOffset? NullableOffset { get; set; } +} - public class EfPerson - { - public int Id { get; set; } +public class EfPerson +{ + public int Id { get; set; } - [Column("Birthday", TypeName = "Date")] - public DateTime? Birthday { get; set; } - } + [Column("Birthday", TypeName = "Date")] + public DateTime? Birthday { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayTest.cs index 5f28943e6..50b408f57 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayTest.cs @@ -24,582 +24,581 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay; + +public class DateAndTimeOfDayTest : WebApiTestBase { - public class DateAndTimeOfDayTest : WebApiTestBase + public DateAndTimeOfDayTest(WebApiTestFixture fixture) + :base(fixture) { - public DateAndTimeOfDayTest(WebApiTestFixture fixture) - :base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=DateAndTimeOfDayEfDbContext8"; - services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); + } - services.ConfigureControllers(typeof(DCustomersController), typeof(MetadataController), typeof(EfCustomersController)); + protected static void UpdateConfigureServices(IServiceCollection services) + { + string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=DateAndTimeOfDayEfDbContext8"; + services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); - TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); // -8:00 + services.ConfigureControllers(typeof(DCustomersController), typeof(MetadataController), typeof(EfCustomersController)); - services.AddControllers().AddOData(opt => - { - opt.Count().Filter().OrderBy().Expand().SetMaxTop(null) - .AddRouteComponents("convention", DateAndTimeOfDayEdmModel.GetConventionModel()) - .AddRouteComponents("explicit", DateAndTimeOfDayEdmModel.GetExplicitModel()); - opt.TimeZone = timeZoneInfo; - }); - } + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); // -8:00 - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task ModelBuilderTest(string modelMode) + services.AddControllers().AddOData(opt => { - string requestUri = $"{modelMode}/$metadata"; - HttpClient client = CreateClient(); - - HttpResponseMessage response = await client.GetAsync(requestUri); - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); - var reader = new ODataMessageReader(message); - var edmModel = reader.ReadMetadataDocument(); - - var customerType = edmModel.SchemaElements.OfType().Single(et => et.Name == "DCustomer"); - Assert.Equal(17, customerType.Properties().Count()); - - // Non-Nullable - AssertHasProperty(customerType, propertyName: "DateTime", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.DateTimeOffset", isNullable: false); - AssertHasProperty(customerType, propertyName: "Offset", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.DateTimeOffset", isNullable: false); - AssertHasProperty(customerType, propertyName: "Date", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.Date", isNullable: false); - AssertHasProperty(customerType, propertyName: "TimeOfDay", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.TimeOfDay", isNullable: false); - - // Nullable - AssertHasProperty(customerType, propertyName: "NullableDateTime", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.DateTimeOffset", isNullable: true); - AssertHasProperty(customerType, propertyName: "NullableOffset", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.DateTimeOffset", isNullable: true); - AssertHasProperty(customerType, propertyName: "NullableDate", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.Date", isNullable: true); - AssertHasProperty(customerType, propertyName: "NullableTimeOfDay", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.TimeOfDay", isNullable: true); - - // Collection - AssertHasProperty(customerType, propertyName: "DateTimes", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.DateTimeOffset)", isNullable: false); - AssertHasProperty(customerType, propertyName: "Offsets", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.DateTimeOffset)", isNullable: false); - AssertHasProperty(customerType, propertyName: "Dates", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.Date)", isNullable: false); - AssertHasProperty(customerType, propertyName: "TimeOfDays", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.TimeOfDay)", isNullable: false); - - // nullable collection - AssertHasProperty(customerType, propertyName: "NullableDateTimes", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.DateTimeOffset)", isNullable: true); - AssertHasProperty(customerType, propertyName: "NullableOffsets", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.DateTimeOffset)", isNullable: true); - AssertHasProperty(customerType, propertyName: "NullableDates", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.Date)", isNullable: true); - AssertHasProperty(customerType, propertyName: "NullableTimeOfDays", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.TimeOfDay)", isNullable: true); - } + opt.Count().Filter().OrderBy().Expand().SetMaxTop(null) + .AddRouteComponents("convention", DateAndTimeOfDayEdmModel.GetConventionModel()) + .AddRouteComponents("explicit", DateAndTimeOfDayEdmModel.GetExplicitModel()); + opt.TimeZone = timeZoneInfo; + }); + } + + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task ModelBuilderTest(string modelMode) + { + string requestUri = $"{modelMode}/$metadata"; + HttpClient client = CreateClient(); + + HttpResponseMessage response = await client.GetAsync(requestUri); + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); + var reader = new ODataMessageReader(message); + var edmModel = reader.ReadMetadataDocument(); + + var customerType = edmModel.SchemaElements.OfType().Single(et => et.Name == "DCustomer"); + Assert.Equal(17, customerType.Properties().Count()); + + // Non-Nullable + AssertHasProperty(customerType, propertyName: "DateTime", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.DateTimeOffset", isNullable: false); + AssertHasProperty(customerType, propertyName: "Offset", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.DateTimeOffset", isNullable: false); + AssertHasProperty(customerType, propertyName: "Date", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.Date", isNullable: false); + AssertHasProperty(customerType, propertyName: "TimeOfDay", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.TimeOfDay", isNullable: false); + + // Nullable + AssertHasProperty(customerType, propertyName: "NullableDateTime", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.DateTimeOffset", isNullable: true); + AssertHasProperty(customerType, propertyName: "NullableOffset", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.DateTimeOffset", isNullable: true); + AssertHasProperty(customerType, propertyName: "NullableDate", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.Date", isNullable: true); + AssertHasProperty(customerType, propertyName: "NullableTimeOfDay", expectKind: EdmTypeKind.Primitive, expectTypeName: "Edm.TimeOfDay", isNullable: true); + + // Collection + AssertHasProperty(customerType, propertyName: "DateTimes", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.DateTimeOffset)", isNullable: false); + AssertHasProperty(customerType, propertyName: "Offsets", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.DateTimeOffset)", isNullable: false); + AssertHasProperty(customerType, propertyName: "Dates", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.Date)", isNullable: false); + AssertHasProperty(customerType, propertyName: "TimeOfDays", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.TimeOfDay)", isNullable: false); + + // nullable collection + AssertHasProperty(customerType, propertyName: "NullableDateTimes", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.DateTimeOffset)", isNullable: true); + AssertHasProperty(customerType, propertyName: "NullableOffsets", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.DateTimeOffset)", isNullable: true); + AssertHasProperty(customerType, propertyName: "NullableDates", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.Date)", isNullable: true); + AssertHasProperty(customerType, propertyName: "NullableTimeOfDays", expectKind: EdmTypeKind.Collection, expectTypeName: "Collection(Edm.TimeOfDay)", isNullable: true); + } - public static TheoryDataSet MediaTypes + public static TheoryDataSet MediaTypes + { + get { - get + string[] modes = new string[] { "convention", "explicit" }; + string[] mimes = new string[]{ + "json", + "application/json", + "application/json;odata.metadata=none", + "application/json;odata.metadata=minimal", + "application/json;odata.metadata=full"}; + TheoryDataSet data = new TheoryDataSet(); + foreach (string mode in modes) { - string[] modes = new string[] { "convention", "explicit" }; - string[] mimes = new string[]{ - "json", - "application/json", - "application/json;odata.metadata=none", - "application/json;odata.metadata=minimal", - "application/json;odata.metadata=full"}; - TheoryDataSet data = new TheoryDataSet(); - foreach (string mode in modes) + foreach (string mime in mimes) { - foreach (string mime in mimes) - { - data.Add(mode, mime); - } + data.Add(mode, mime); } - return data; } + return data; } + } - [Theory] - [MemberData(nameof(MediaTypes))] - public async Task QueryDCustomerEntityTest(string mode, string mime) - { - string requestUri = $"{mode}/DCustomers(2)?$format={mime}"; + [Theory] + [MemberData(nameof(MediaTypes))] + public async Task QueryDCustomerEntityTest(string mode, string mime) + { + string requestUri = $"{mode}/DCustomers(2)?$format={mime}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); - var response = await client.SendAsync(request); + var response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject content = await response.Content.ReadAsObject(); - Assert.Equal(2, content["Id"]); - Assert.Equal(DateTimeOffset.Parse("2017-01-01T17:02:03.004+08:00"), content["DateTime"]); - Assert.Equal(DateTimeOffset.Parse("2015-03-01T01:02:03.004Z"), content["Offset"]); - Assert.Equal("2015-01-03", content["Date"]); - Assert.Equal("23:02:03.0140000", content["TimeOfDay"]); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject content = await response.Content.ReadAsObject(); + Assert.Equal(2, content["Id"]); + Assert.Equal(DateTimeOffset.Parse("2017-01-01T17:02:03.004+08:00"), content["DateTime"]); + Assert.Equal(DateTimeOffset.Parse("2015-03-01T01:02:03.004Z"), content["Offset"]); + Assert.Equal("2015-01-03", content["Date"]); + Assert.Equal("23:02:03.0140000", content["TimeOfDay"]); - Assert.Null((DateTimeOffset?)(content["NullableDateTime"])); - // JsonAssert.PropertyEquals((Date?)null, "NullableDate", content); - } + Assert.Null((DateTimeOffset?)(content["NullableDateTime"])); + // JsonAssert.PropertyEquals((Date?)null, "NullableDate", content); + } - public static TheoryDataSet> FilterData + public static TheoryDataSet> FilterData + { + get { - get + string[] modes = { "convention", "explicit" }; + object[][] orders = { + // DateTime + // new object[] {"$filter=DateTime eq cast(2016-01-01T17:02:03.004%2B08:00,Edm.DateTimeOffset)", new[] {1} }, // TODO: make it working + // new object[] {"$filter=DateTime ge cast(2016-01-01T17:02:03.004%2B08:00,Edm.DateTimeOffset)", new[] {1,2,3,4,5} }, // TODO: make it working + // new object[] {"$filter=DateTime lt cast(2016-01-01T17:02:03.004%2B08:00,Edm.DateTimeOffset)", new int[] {} }, // TODO: make it working + // new object[] {"$filter=DateTime ge cast(2016-01-01T17:03:03.004%2B08:00,Edm.DateTimeOffset)", new[] {2,3,4,5} }, // TODO: make it working + + // DateTimeOffset + new object[] {"$filter=Offset eq cast(2015-05-01T01:02:03.004Z,Edm.DateTimeOffset)", new[] {4} }, + new object[] {"$filter=Offset ge cast(2015-05-01T01:02:03.004Z,Edm.DateTimeOffset)", new[] {4} }, + new object[] {"$filter=Offset lt cast(2015-05-01T01:02:03.004Z,Edm.DateTimeOffset)", new int[] {1,2,3,5} }, + + // Date + new object[] {"$filter=Date eq 2014-12-29", new[] {3} }, + new object[] {"$filter=Date ge 2014-12-29", new[] {1,2,3,4} }, + new object[] {"$filter=Date lt 2014-12-29", new int[] {5} }, + + // TimeOfDay + new object[] {"$filter=TimeOfDay eq 21:02:03.0140000", new[] {4} }, + new object[] {"$filter=TimeOfDay ge 21:02:03.0040000", new[] {2,4} }, + new object[] {"$filter=TimeOfDay lt 21:02:03.0040000", new int[] {1,3,5} }, + + // DateTime? + new object[] {"$filter=NullableDateTime eq null", new[] {2,4} }, + new object[] {"$filter=NullableDateTime ne null", new[] {1,3,5} }, + // new object[] {"$filter=NullableDateTime lt cast(2016-01-01T17:02:03.004%2B08:00,Edm.DateTimeOffset)", new int[] {} }, // TODO: make it working + + // DateTimeOffset? + new object[] {"$filter=NullableOffset eq null", new[] {3} }, + new object[] {"$filter=NullableOffset ne null", new[] {1,2,4,5} }, + new object[] {"$filter=NullableOffset lt cast(2015-05-01T01:02:03.004Z,Edm.DateTimeOffset)", new [] {1,2} }, + + // Date? + new object[] {"$filter=NullableDate eq null", new[] {2,4} }, + new object[] {"$filter=NullableDate ne null", new[] {1,3,5} }, + new object[] {"$filter=NullableDate lt 2015-01-03", new [] {1} }, + + // TimeOfDay? + new object[] {"$filter=NullableTimeOfDay eq null", new[] {3} }, + new object[] {"$filter=NullableTimeOfDay ne null", new[] {1,2,4,5} }, + new object[] {"$filter=NullableTimeOfDay gt 03:02:03.0040000", new [] {4,5} }, + + // fractionalseconds() + new object[] {"$filter=fractionalseconds(DateTime) eq 0.004", new[] {1,2,3,4,5} }, + new object[] {"$filter=fractionalseconds(Offset) gt 0.004", new[] {1,3,5} }, + new object[] {"$filter=fractionalseconds(TimeOfDay) lt 0.014", new [] {3} }, + + new object[] {"$filter=fractionalseconds(NullableDateTime) eq null", new[] {2,4} }, + new object[] {"$filter=fractionalseconds(NullableOffset) eq 0.004", new[] {1,2,4,5} }, + new object[] {"$filter=fractionalseconds(NullableTimeOfDay) ne 0.004", new int[] {} }, + + // date() + new object[] {"$filter=date(DateTime) eq 2016-01-01", new[] {1} }, + new object[] {"$filter=date(Offset) gt 2015-01-02", new[] {2,3,4,5} }, + + new object[] {"$filter=date(NullableDateTime) eq null", new[] {2,4} }, + new object[] {"$filter=date(NullableOffset) eq 2015-03-01", new[] {2} }, + + // time() + new object[] {"$filter=time(DateTime) eq 01:02:03.004", new[] {1,2,3,4,5} }, + new object[] {"$filter=time(Offset) lt 01:02:03.014", new[] {2,4} }, + + new object[] {"$filter=time(NullableDateTime) eq null", new[] {2,4} }, + new object[] {"$filter=time(NullableOffset) ne null", new[] {1,2,4,5} } + }; + TheoryDataSet> data = new TheoryDataSet>(); + foreach (string mode in modes) { - string[] modes = { "convention", "explicit" }; - object[][] orders = { - // DateTime - // new object[] {"$filter=DateTime eq cast(2016-01-01T17:02:03.004%2B08:00,Edm.DateTimeOffset)", new[] {1} }, // TODO: make it working - // new object[] {"$filter=DateTime ge cast(2016-01-01T17:02:03.004%2B08:00,Edm.DateTimeOffset)", new[] {1,2,3,4,5} }, // TODO: make it working - // new object[] {"$filter=DateTime lt cast(2016-01-01T17:02:03.004%2B08:00,Edm.DateTimeOffset)", new int[] {} }, // TODO: make it working - // new object[] {"$filter=DateTime ge cast(2016-01-01T17:03:03.004%2B08:00,Edm.DateTimeOffset)", new[] {2,3,4,5} }, // TODO: make it working - - // DateTimeOffset - new object[] {"$filter=Offset eq cast(2015-05-01T01:02:03.004Z,Edm.DateTimeOffset)", new[] {4} }, - new object[] {"$filter=Offset ge cast(2015-05-01T01:02:03.004Z,Edm.DateTimeOffset)", new[] {4} }, - new object[] {"$filter=Offset lt cast(2015-05-01T01:02:03.004Z,Edm.DateTimeOffset)", new int[] {1,2,3,5} }, - - // Date - new object[] {"$filter=Date eq 2014-12-29", new[] {3} }, - new object[] {"$filter=Date ge 2014-12-29", new[] {1,2,3,4} }, - new object[] {"$filter=Date lt 2014-12-29", new int[] {5} }, - - // TimeOfDay - new object[] {"$filter=TimeOfDay eq 21:02:03.0140000", new[] {4} }, - new object[] {"$filter=TimeOfDay ge 21:02:03.0040000", new[] {2,4} }, - new object[] {"$filter=TimeOfDay lt 21:02:03.0040000", new int[] {1,3,5} }, - - // DateTime? - new object[] {"$filter=NullableDateTime eq null", new[] {2,4} }, - new object[] {"$filter=NullableDateTime ne null", new[] {1,3,5} }, - // new object[] {"$filter=NullableDateTime lt cast(2016-01-01T17:02:03.004%2B08:00,Edm.DateTimeOffset)", new int[] {} }, // TODO: make it working - - // DateTimeOffset? - new object[] {"$filter=NullableOffset eq null", new[] {3} }, - new object[] {"$filter=NullableOffset ne null", new[] {1,2,4,5} }, - new object[] {"$filter=NullableOffset lt cast(2015-05-01T01:02:03.004Z,Edm.DateTimeOffset)", new [] {1,2} }, - - // Date? - new object[] {"$filter=NullableDate eq null", new[] {2,4} }, - new object[] {"$filter=NullableDate ne null", new[] {1,3,5} }, - new object[] {"$filter=NullableDate lt 2015-01-03", new [] {1} }, - - // TimeOfDay? - new object[] {"$filter=NullableTimeOfDay eq null", new[] {3} }, - new object[] {"$filter=NullableTimeOfDay ne null", new[] {1,2,4,5} }, - new object[] {"$filter=NullableTimeOfDay gt 03:02:03.0040000", new [] {4,5} }, - - // fractionalseconds() - new object[] {"$filter=fractionalseconds(DateTime) eq 0.004", new[] {1,2,3,4,5} }, - new object[] {"$filter=fractionalseconds(Offset) gt 0.004", new[] {1,3,5} }, - new object[] {"$filter=fractionalseconds(TimeOfDay) lt 0.014", new [] {3} }, - - new object[] {"$filter=fractionalseconds(NullableDateTime) eq null", new[] {2,4} }, - new object[] {"$filter=fractionalseconds(NullableOffset) eq 0.004", new[] {1,2,4,5} }, - new object[] {"$filter=fractionalseconds(NullableTimeOfDay) ne 0.004", new int[] {} }, - - // date() - new object[] {"$filter=date(DateTime) eq 2016-01-01", new[] {1} }, - new object[] {"$filter=date(Offset) gt 2015-01-02", new[] {2,3,4,5} }, - - new object[] {"$filter=date(NullableDateTime) eq null", new[] {2,4} }, - new object[] {"$filter=date(NullableOffset) eq 2015-03-01", new[] {2} }, - - // time() - new object[] {"$filter=time(DateTime) eq 01:02:03.004", new[] {1,2,3,4,5} }, - new object[] {"$filter=time(Offset) lt 01:02:03.014", new[] {2,4} }, - - new object[] {"$filter=time(NullableDateTime) eq null", new[] {2,4} }, - new object[] {"$filter=time(NullableOffset) ne null", new[] {1,2,4,5} } - }; - TheoryDataSet> data = new TheoryDataSet>(); - foreach (string mode in modes) + foreach (object[] order in orders) { - foreach (object[] order in orders) - { - data.Add(mode, order[0] as string, order[1] as IList); - } + data.Add(mode, order[0] as string, order[1] as IList); } - return data; } + return data; } + } - [Theory] - [MemberData(nameof(FilterData))] - public async Task CanFilterDateAndTimeOfDayProperty(string mode, string filter, IList expect) - { - string requestUri = $"{mode}/DCustomers?{filter}"; - HttpClient client = CreateClient(); - - HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + [Theory] + [MemberData(nameof(FilterData))] + public async Task CanFilterDateAndTimeOfDayProperty(string mode, string filter, IList expect) + { + string requestUri = $"{mode}/DCustomers?{filter}"; + HttpClient client = CreateClient(); - JObject content = await response.Content.ReadAsObject(); + HttpResponseMessage response = await client.GetAsync(requestUri); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(expect.Count, content["value"].Count()); + JObject content = await response.Content.ReadAsObject(); - IList actual = new List(); - for (int i = 0; i < expect.Count; i++) - { - actual.Add((int)content["value"][i]["Id"]); - } + Assert.Equal(expect.Count, content["value"].Count()); - Assert.Equal(expect, actual.ToArray()); + IList actual = new List(); + for (int i = 0; i < expect.Count; i++) + { + actual.Add((int)content["value"][i]["Id"]); } - public static TheoryDataSet OrderByData + Assert.Equal(expect, actual.ToArray()); + } + + public static TheoryDataSet OrderByData + { + get { - get - { - string[] modes = { "convention", "explicit" }; - string[][] orders = { - new[] {"$orderby=DateTime", "1 > 2 > 3 > 4 > 5"}, - new[] {"$orderby=DateTime desc", "5 > 4 > 3 > 2 > 1"}, + string[] modes = { "convention", "explicit" }; + string[][] orders = { + new[] {"$orderby=DateTime", "1 > 2 > 3 > 4 > 5"}, + new[] {"$orderby=DateTime desc", "5 > 4 > 3 > 2 > 1"}, - new[] {"$orderby=Offset", "1 > 3 > 5 > 2 > 4"}, - new[] {"$orderby=Offset desc", "4 > 2 > 5 > 3 > 1"}, + new[] {"$orderby=Offset", "1 > 3 > 5 > 2 > 4"}, + new[] {"$orderby=Offset desc", "4 > 2 > 5 > 3 > 1"}, - new[] {"$orderby=Date", "5 > 3 > 1 > 2 > 4"}, - new[] {"$orderby=Date desc", "4 > 2 > 1 > 3 > 5"}, + new[] {"$orderby=Date", "5 > 3 > 1 > 2 > 4"}, + new[] {"$orderby=Date desc", "4 > 2 > 1 > 3 > 5"}, - new[] {"$orderby=TimeOfDay", "1 > 3 > 5 > 4 > 2"}, - new[] {"$orderby=TimeOfDay desc", "2 > 4 > 5 > 3 > 1"}, + new[] {"$orderby=TimeOfDay", "1 > 3 > 5 > 4 > 2"}, + new[] {"$orderby=TimeOfDay desc", "2 > 4 > 5 > 3 > 1"}, - new[] {"$orderby=NullableDateTime", "2 > 4 > 1 > 3 > 5"}, // Make sure 2 > 4, not 4 > 2 - new[] {"$orderby=NullableDateTime desc", "5 > 3 > 1 > 2 > 4"}, + new[] {"$orderby=NullableDateTime", "2 > 4 > 1 > 3 > 5"}, // Make sure 2 > 4, not 4 > 2 + new[] {"$orderby=NullableDateTime desc", "5 > 3 > 1 > 2 > 4"}, - new[] {"$orderby=NullableOffset", "3 > 1 > 2 > 4 > 5"}, - new[] {"$orderby=NullableOffset desc", "5 > 4 > 2 > 1 > 3"}, + new[] {"$orderby=NullableOffset", "3 > 1 > 2 > 4 > 5"}, + new[] {"$orderby=NullableOffset desc", "5 > 4 > 2 > 1 > 3"}, - new[] {"$orderby=NullableDate", "2 > 4 > 1 > 3 > 5"}, // Make sure 2 > 4, not 4 > 2 - new[] {"$orderby=NullableDate desc", "5 > 3 > 1 > 2 > 4"}, + new[] {"$orderby=NullableDate", "2 > 4 > 1 > 3 > 5"}, // Make sure 2 > 4, not 4 > 2 + new[] {"$orderby=NullableDate desc", "5 > 3 > 1 > 2 > 4"}, - new[] {"$orderby=NullableTimeOfDay", "3 > 1 > 2 > 4 > 5"}, - new[] {"$orderby=NullableTimeOfDay desc", "5 > 4 > 2 > 1 > 3"}, - }; - TheoryDataSet data = new TheoryDataSet(); - foreach (string mode in modes) + new[] {"$orderby=NullableTimeOfDay", "3 > 1 > 2 > 4 > 5"}, + new[] {"$orderby=NullableTimeOfDay desc", "5 > 4 > 2 > 1 > 3"}, + }; + TheoryDataSet data = new TheoryDataSet(); + foreach (string mode in modes) + { + foreach (string[] order in orders) { - foreach (string[] order in orders) - { - data.Add(mode, order[0], order[1]); - } + data.Add(mode, order[0], order[1]); } - return data; } + return data; } + } - [Theory] - [MemberData(nameof(OrderByData))] - public async Task CanOrderByDateAndTimeOfDayProperty(string mode, string orderby, string expect) - { - string requestUri = $"{mode}/DCustomers?{orderby}"; - HttpClient client = CreateClient(); + [Theory] + [MemberData(nameof(OrderByData))] + public async Task CanOrderByDateAndTimeOfDayProperty(string mode, string orderby, string expect) + { + string requestUri = $"{mode}/DCustomers?{orderby}"; + HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + HttpResponseMessage response = await client.GetAsync(requestUri); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject content = await response.Content.ReadAsObject(); + JObject content = await response.Content.ReadAsObject(); - Assert.Equal(5, content["value"].Count()); + Assert.Equal(5, content["value"].Count()); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < 5; i++) - { - sb.Append(content["value"][i]["Id"]).Append(" > "); - } - sb.Remove(sb.Length - 3, 3); // remove the last " > " - - Assert.Equal(expect, sb.ToString()); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 5; i++) + { + sb.Append(content["value"][i]["Id"]).Append(" > "); } + sb.Remove(sb.Length - 3, 3); // remove the last " > " - #region function/action on Date & TimeOfDay + Assert.Equal(expect, sb.ToString()); + } - public static TheoryDataSet FunctionData + #region function/action on Date & TimeOfDay + + public static TheoryDataSet FunctionData + { + get { - get + string[] modes = new string[] { "convention", "explicit" }; + string[] functions = new string[] { - string[] modes = new string[] { "convention", "explicit" }; - string[] functions = new string[] - { - "DCustomers(2)/Default.BoundFunction", - "UnboundFunction", - }; - TheoryDataSet data = new TheoryDataSet(); - foreach (string mode in modes) + "DCustomers(2)/Default.BoundFunction", + "UnboundFunction", + }; + TheoryDataSet data = new TheoryDataSet(); + foreach (string mode in modes) + { + foreach (string f in functions) { - foreach (string f in functions) - { - data.Add(mode, f); - } + data.Add(mode, f); } - return data; } + return data; } + } - [Theory] - [MemberData(nameof(FunctionData))] - public async Task CanCallFunctionWithDateAndTimeOfDayParameters(string mode, string function) - { - string parameter = - "modifiedDate=2015-02-28,modifiedTime=01:02:03.0040000,nullableModifiedDate=null,nullableModifiedTime=null"; - string requestUri = string.Format("{0}/{1}({2})", mode, function, parameter); - HttpClient client = CreateClient(); + [Theory] + [MemberData(nameof(FunctionData))] + public async Task CanCallFunctionWithDateAndTimeOfDayParameters(string mode, string function) + { + string parameter = + "modifiedDate=2015-02-28,modifiedTime=01:02:03.0040000,nullableModifiedDate=null,nullableModifiedTime=null"; + string requestUri = string.Format("{0}/{1}({2})", mode, function, parameter); + HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); + HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject content = await response.Content.ReadAsObject(); + JObject content = await response.Content.ReadAsObject(); - Assert.Equal( - "modifiedDate:2015-02-28,modifiedTime:01:02:03.0040000,nullableModifiedDate:null,nullableModifiedTime:null", - content["value"]); - } + Assert.Equal( + "modifiedDate:2015-02-28,modifiedTime:01:02:03.0040000,nullableModifiedDate:null,nullableModifiedTime:null", + content["value"]); + } - public static TheoryDataSet ActionData + public static TheoryDataSet ActionData + { + get { - get + string[] modes = new string[] { "convention", "explicit" }; + string[] actions = new string[] { - string[] modes = new string[] { "convention", "explicit" }; - string[] actions = new string[] - { - "DCustomers(2)/Default.BoundAction", - "UnboundAction", - }; - TheoryDataSet data = new TheoryDataSet(); - foreach (string mode in modes) + "DCustomers(2)/Default.BoundAction", + "UnboundAction", + }; + TheoryDataSet data = new TheoryDataSet(); + foreach (string mode in modes) + { + foreach (string action in actions) { - foreach (string action in actions) - { - data.Add(mode, action); - } + data.Add(mode, action); } - return data; } + return data; } + } - [Theory] - [MemberData(nameof(ActionData))] - public async Task CanCallActionWithDateAndTimeOfDayParameters(string mode, string action) + [Theory] + [MemberData(nameof(ActionData))] + public async Task CanCallActionWithDateAndTimeOfDayParameters(string mode, string action) + { + string requestUri = $"{mode}/{action}"; + string content = "{'modifiedDate':'2015-03-01','modifiedTime':'01:05:06.0080000','nullableModifiedDate':null,'nullableModifiedTime':null,'dates':['2014-12-21','2015-03-01']}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri) { - string requestUri = $"{mode}/{action}"; - string content = "{'modifiedDate':'2015-03-01','modifiedTime':'01:05:06.0080000','nullableModifiedDate':null,'nullableModifiedTime':null,'dates':['2014-12-21','2015-03-01']}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri) - { - Content = new StringContent(content) - }; + Content = new StringContent(content) + }; - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = content.Length; - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.SendAsync(request); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = content.Length; + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Contains("\"value\":true", await response.Content.ReadAsStringAsync()); - } - #endregion + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("\"value\":true", await response.Content.ReadAsStringAsync()); + } + #endregion - private static void AssertHasProperty(IEdmEntityType entityType, string propertyName, EdmTypeKind expectKind, - string expectTypeName, bool isNullable) - { - Assert.NotNull(entityType); - var property = entityType.DeclaredProperties.Single(p => p.Name == propertyName); - Assert.Equal(expectKind, property.Type.TypeKind()); - Assert.Equal(expectTypeName, property.Type.Definition.FullTypeName()); - Assert.Equal(isNullable, property.Type.IsNullable); - } + private static void AssertHasProperty(IEdmEntityType entityType, string propertyName, EdmTypeKind expectKind, + string expectTypeName, bool isNullable) + { + Assert.NotNull(entityType); + var property = entityType.DeclaredProperties.Single(p => p.Name == propertyName); + Assert.Equal(expectKind, property.Type.TypeKind()); + Assert.Equal(expectTypeName, property.Type.Definition.FullTypeName()); + Assert.Equal(isNullable, property.Type.IsNullable); + } - [Theory] - [InlineData("json")] - [InlineData("application/json")] - [InlineData("application/json;odata.metadata=none")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=full")] - public async Task QueryEfCustomerEntityTest(string mime) - { - string requestUri = $"convention/EfCustomers(2)?$format={mime}"; + [Theory] + [InlineData("json")] + [InlineData("application/json")] + [InlineData("application/json;odata.metadata=none")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=full")] + public async Task QueryEfCustomerEntityTest(string mime) + { + string requestUri = $"convention/EfCustomers(2)?$format={mime}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); - var response = await client.SendAsync(request); + var response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject content = await response.Content.ReadAsObject(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject content = await response.Content.ReadAsObject(); - Assert.Equal(2, content["Id"]); - Assert.Equal(DateTimeOffset.Parse("2016-12-24T03:02:03.006-08:00"), content["DateTime"]); - Assert.Equal(DateTimeOffset.Parse("2015-02-24T03:02:03.006-08:00"), content["Offset"]); - Assert.Equal(DateTimeOffset.Parse("2014-12-26T11:02:03.004-08:00"), content["NullableOffset"]); + Assert.Equal(2, content["Id"]); + Assert.Equal(DateTimeOffset.Parse("2016-12-24T03:02:03.006-08:00"), content["DateTime"]); + Assert.Equal(DateTimeOffset.Parse("2015-02-24T03:02:03.006-08:00"), content["Offset"]); + Assert.Equal(DateTimeOffset.Parse("2014-12-26T11:02:03.004-08:00"), content["NullableOffset"]); - Assert.Null((DateTimeOffset?)(content["NullableDateTime"])); - } + Assert.Null((DateTimeOffset?)(content["NullableDateTime"])); + } - public static TheoryDataSet> FilterDataForEf + public static TheoryDataSet> FilterDataForEf + { + get { - get + object[][] orders = { + // DateTime + // new object[] {"$filter=DateTime eq cast(2017-12-24T12:02:03.007Z,Edm.DateTimeOffset)", new[] {3} }, // TODO: make it working + // new object[] {"$filter=DateTime ge cast(2017-12-24T12:02:03.007Z,Edm.DateTimeOffset)", new[] {3,4,5} }, // TODO: make it working + // new object[] {"$filter=DateTime lt cast(2017-12-24T12:02:03.007Z,Edm.DateTimeOffset)", new[] {1,2} }, // TODO: make it working + + // DateTimeOffset + new object[] {"$filter=Offset eq cast(2015-04-24T13:02:03.008Z,Edm.DateTimeOffset)", new[] {4} }, + new object[] {"$filter=Offset ge cast(2015-04-24T13:02:03.008Z,Edm.DateTimeOffset)", new[] {4,5} }, + new object[] {"$filter=Offset lt cast(2015-04-24T13:02:03.008Z,Edm.DateTimeOffset)", new int[] {1,2,3} }, + + // DateTime? + new object[] {"$filter=NullableDateTime eq null", new[] {2,4} }, + new object[] {"$filter=NullableDateTime ne null", new[] {1,3,5} }, + new object[] {"$filter=NullableDateTime lt cast(2016-01-01T17:02:03.004%2B08:00,Edm.DateTimeOffset)", new[] {1,3,5} }, + + // DateTimeOffset? + new object[] {"$filter=NullableOffset eq null", new[] {3} }, + new object[] {"$filter=NullableOffset ne null", new[] {1,2,4,5} }, + new object[] {"$filter=NullableOffset lt cast(2014-12-29T01:02:03.004Z,Edm.DateTimeOffset)", new [] {1,2} }, + + // fractionalseconds() + // new object[] {"$filter=fractionalseconds(DateTime) eq 0.007", new[] {3} }, // TODO: make it working + new object[] {"$filter=fractionalseconds(Offset) gt 0.004", new[] {1,2,3,4,5} }, + + new object[] {"$filter=fractionalseconds(NullableDateTime) eq null", new[] {2,4} }, + new object[] {"$filter=fractionalseconds(NullableOffset) lt 0.004", new int[] {} }, + + // date(DateTime) + new object[] {"$filter=date(DateTime) eq 2017-12-24", new[] {3} }, + new object[] {"$filter=2017-12-24 eq date(DateTime)", new[] {3} }, + new object[] {"$filter=date(DateTime) lt 2017-12-24", new[] {1,2} }, + new object[] {"$filter=2017-12-24 le date(DateTime)", new[] {3,4,5 } }, + + // date(DateTimeOffset) + new object[] {"$filter=date(Offset) ne 2015-03-24", new[] {1,2,4,5} }, + new object[] {"$filter=2015-03-24 eq date(Offset)", new[] {3} }, + new object[] {"$filter=date(Offset) lt 2015-02-24", new[] {1} }, + new object[] {"$filter=2015-02-24 le date(Offset)", new[] {2,3,4,5} }, + + // date(DateTime?) + new object[] {"$filter=date(NullableDateTime) eq null", new[] {2,4} }, + new object[] {"$filter=null ne date(NullableDateTime)", new[] {1,3,5} }, + new object[] {"$filter=date(NullableDateTime) eq 2014-12-24", new[] {1,3} }, // vary with the time zone setting. + new object[] {"$filter=date(NullableDateTime) gt 2014-12-24", new[] {5} }, // vary with the time zone setting. + new object[] {"$filter=date(NullableDateTime) lt 2014-12-24", new int[] {} }, + + // date(DateTimeOffset?) + new object[] {"$filter=date(NullableOffset) eq null", new[] {3} }, + new object[] {"$filter=null ne date(NullableOffset)", new[] {1,2,4,5} }, + new object[] {"$filter=date(NullableOffset) eq 2014-12-26", new[] {2} }, + new object[] {"$filter=2014-12-28 ne date(NullableOffset)", new[] {1,2,3,5} }, + + // time(DateTime) + new object[] {"$filter=time(DateTime) eq 02:02:03.005", new[] {1} }, + new object[] {"$filter=05:02:03.008 eq time(DateTime)", new[] {4} }, + new object[] {"$filter=time(DateTime) lt 05:02:03.008", new[] {1,2,3} }, + + // time(DateTimeOffset) + new object[] {"$filter=time(Offset) eq 02:02:03.005", new[] {1} }, + new object[] {"$filter=05:02:03.008 eq time(Offset)", new[] {4} }, + new object[] {"$filter=time(Offset) lt 03:02:03.007", new[] {1,2} }, + + // time(DateTime?) + new object[] {"$filter=time(NullableDateTime) eq null", new[] {2,4} }, + new object[] {"$filter=null ne time(NullableDateTime)", new[] {1,3,5} }, + new object[] {"$filter=time(NullableDateTime) lt 06:02:04.005", new[] {1,5} }, + + // time(DateTimeOffset?) + new object[] {"$filter=time(NullableOffset) eq null", new[] {3} }, + new object[] {"$filter=null ne time(NullableOffset)", new[] {1,2,4,5} }, + new object[] {"$filter=time(NullableOffset) eq 21:02:03.004", new[] {4} }, + new object[] {"$filter=21:02:03.004 ne time(NullableOffset)", new[] {1,2,3,5} }, + }; + TheoryDataSet> data = new TheoryDataSet>(); + foreach (object[] order in orders) { - object[][] orders = { - // DateTime - // new object[] {"$filter=DateTime eq cast(2017-12-24T12:02:03.007Z,Edm.DateTimeOffset)", new[] {3} }, // TODO: make it working - // new object[] {"$filter=DateTime ge cast(2017-12-24T12:02:03.007Z,Edm.DateTimeOffset)", new[] {3,4,5} }, // TODO: make it working - // new object[] {"$filter=DateTime lt cast(2017-12-24T12:02:03.007Z,Edm.DateTimeOffset)", new[] {1,2} }, // TODO: make it working - - // DateTimeOffset - new object[] {"$filter=Offset eq cast(2015-04-24T13:02:03.008Z,Edm.DateTimeOffset)", new[] {4} }, - new object[] {"$filter=Offset ge cast(2015-04-24T13:02:03.008Z,Edm.DateTimeOffset)", new[] {4,5} }, - new object[] {"$filter=Offset lt cast(2015-04-24T13:02:03.008Z,Edm.DateTimeOffset)", new int[] {1,2,3} }, - - // DateTime? - new object[] {"$filter=NullableDateTime eq null", new[] {2,4} }, - new object[] {"$filter=NullableDateTime ne null", new[] {1,3,5} }, - new object[] {"$filter=NullableDateTime lt cast(2016-01-01T17:02:03.004%2B08:00,Edm.DateTimeOffset)", new[] {1,3,5} }, - - // DateTimeOffset? - new object[] {"$filter=NullableOffset eq null", new[] {3} }, - new object[] {"$filter=NullableOffset ne null", new[] {1,2,4,5} }, - new object[] {"$filter=NullableOffset lt cast(2014-12-29T01:02:03.004Z,Edm.DateTimeOffset)", new [] {1,2} }, - - // fractionalseconds() - // new object[] {"$filter=fractionalseconds(DateTime) eq 0.007", new[] {3} }, // TODO: make it working - new object[] {"$filter=fractionalseconds(Offset) gt 0.004", new[] {1,2,3,4,5} }, - - new object[] {"$filter=fractionalseconds(NullableDateTime) eq null", new[] {2,4} }, - new object[] {"$filter=fractionalseconds(NullableOffset) lt 0.004", new int[] {} }, - - // date(DateTime) - new object[] {"$filter=date(DateTime) eq 2017-12-24", new[] {3} }, - new object[] {"$filter=2017-12-24 eq date(DateTime)", new[] {3} }, - new object[] {"$filter=date(DateTime) lt 2017-12-24", new[] {1,2} }, - new object[] {"$filter=2017-12-24 le date(DateTime)", new[] {3,4,5 } }, - - // date(DateTimeOffset) - new object[] {"$filter=date(Offset) ne 2015-03-24", new[] {1,2,4,5} }, - new object[] {"$filter=2015-03-24 eq date(Offset)", new[] {3} }, - new object[] {"$filter=date(Offset) lt 2015-02-24", new[] {1} }, - new object[] {"$filter=2015-02-24 le date(Offset)", new[] {2,3,4,5} }, - - // date(DateTime?) - new object[] {"$filter=date(NullableDateTime) eq null", new[] {2,4} }, - new object[] {"$filter=null ne date(NullableDateTime)", new[] {1,3,5} }, - new object[] {"$filter=date(NullableDateTime) eq 2014-12-24", new[] {1,3} }, // vary with the time zone setting. - new object[] {"$filter=date(NullableDateTime) gt 2014-12-24", new[] {5} }, // vary with the time zone setting. - new object[] {"$filter=date(NullableDateTime) lt 2014-12-24", new int[] {} }, - - // date(DateTimeOffset?) - new object[] {"$filter=date(NullableOffset) eq null", new[] {3} }, - new object[] {"$filter=null ne date(NullableOffset)", new[] {1,2,4,5} }, - new object[] {"$filter=date(NullableOffset) eq 2014-12-26", new[] {2} }, - new object[] {"$filter=2014-12-28 ne date(NullableOffset)", new[] {1,2,3,5} }, - - // time(DateTime) - new object[] {"$filter=time(DateTime) eq 02:02:03.005", new[] {1} }, - new object[] {"$filter=05:02:03.008 eq time(DateTime)", new[] {4} }, - new object[] {"$filter=time(DateTime) lt 05:02:03.008", new[] {1,2,3} }, - - // time(DateTimeOffset) - new object[] {"$filter=time(Offset) eq 02:02:03.005", new[] {1} }, - new object[] {"$filter=05:02:03.008 eq time(Offset)", new[] {4} }, - new object[] {"$filter=time(Offset) lt 03:02:03.007", new[] {1,2} }, - - // time(DateTime?) - new object[] {"$filter=time(NullableDateTime) eq null", new[] {2,4} }, - new object[] {"$filter=null ne time(NullableDateTime)", new[] {1,3,5} }, - new object[] {"$filter=time(NullableDateTime) lt 06:02:04.005", new[] {1,5} }, - - // time(DateTimeOffset?) - new object[] {"$filter=time(NullableOffset) eq null", new[] {3} }, - new object[] {"$filter=null ne time(NullableOffset)", new[] {1,2,4,5} }, - new object[] {"$filter=time(NullableOffset) eq 21:02:03.004", new[] {4} }, - new object[] {"$filter=21:02:03.004 ne time(NullableOffset)", new[] {1,2,3,5} }, - }; - TheoryDataSet> data = new TheoryDataSet>(); - foreach (object[] order in orders) - { - data.Add(order[0] as string, order[1] as IList); - } - - return data; + data.Add(order[0] as string, order[1] as IList); } - } - [Theory] - [MemberData(nameof(FilterDataForEf))] - public async Task CanFilterDateAndTimeOfDayPropertyOnEf(string filter, IList expect) - { - string requestUri = $"convention/EfCustomers?{filter}"; - HttpClient client = CreateClient(); + return data; + } + } - HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + [Theory] + [MemberData(nameof(FilterDataForEf))] + public async Task CanFilterDateAndTimeOfDayPropertyOnEf(string filter, IList expect) + { + string requestUri = $"convention/EfCustomers?{filter}"; + HttpClient client = CreateClient(); - JObject content = await response.Content.ReadAsObject(); - Assert.Equal(expect.Count, content["value"].Count()); + HttpResponseMessage response = await client.GetAsync(requestUri); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - IList actual = new List(); - for (int i = 0; i < expect.Count; i++) - { - actual.Add((int)content["value"][i]["Id"]); - } + JObject content = await response.Content.ReadAsObject(); + Assert.Equal(expect.Count, content["value"].Count()); - Assert.Equal(expect, actual.ToArray()); + IList actual = new List(); + for (int i = 0; i < expect.Count; i++) + { + actual.Add((int)content["value"][i]["Id"]); } - public static TheoryDataSet OrderByDataEf + Assert.Equal(expect, actual.ToArray()); + } + + public static TheoryDataSet OrderByDataEf + { + get { - get - { - string[][] orders = { - new[] {"$orderby=DateTime", "1 > 2 > 3 > 4 > 5"}, - new[] {"$orderby=DateTime desc", "5 > 4 > 3 > 2 > 1"}, + string[][] orders = { + new[] {"$orderby=DateTime", "1 > 2 > 3 > 4 > 5"}, + new[] {"$orderby=DateTime desc", "5 > 4 > 3 > 2 > 1"}, - new[] {"$orderby=Offset", "1 > 2 > 3 > 4 > 5"}, - new[] {"$orderby=Offset desc", "5 > 4 > 3 > 2 > 1"}, + new[] {"$orderby=Offset", "1 > 2 > 3 > 4 > 5"}, + new[] {"$orderby=Offset desc", "5 > 4 > 3 > 2 > 1"}, - new[] {"$orderby=NullableDateTime", "2 > 4 > 1 > 3 > 5"}, // Make sure 2 > 4, not 4 > 2 - new[] {"$orderby=NullableDateTime desc", "5 > 3 > 1 > 2 > 4"}, + new[] {"$orderby=NullableDateTime", "2 > 4 > 1 > 3 > 5"}, // Make sure 2 > 4, not 4 > 2 + new[] {"$orderby=NullableDateTime desc", "5 > 3 > 1 > 2 > 4"}, - new[] {"$orderby=NullableOffset", "3 > 1 > 2 > 4 > 5"}, - new[] {"$orderby=NullableOffset desc", "5 > 4 > 2 > 1 > 3"}, - }; - TheoryDataSet data = new TheoryDataSet(); - foreach (string[] order in orders) - { - data.Add(order[0], order[1]); - } - return data; + new[] {"$orderby=NullableOffset", "3 > 1 > 2 > 4 > 5"}, + new[] {"$orderby=NullableOffset desc", "5 > 4 > 2 > 1 > 3"}, + }; + TheoryDataSet data = new TheoryDataSet(); + foreach (string[] order in orders) + { + data.Add(order[0], order[1]); } + return data; } + } - [Theory] - [MemberData(nameof(OrderByDataEf))] - public async Task CanOrderByDateAndTimeOfDayPropertyOnEf(string orderby, string expect) - { - string requestUri = $"convention/EfCustomers?{orderby}"; - HttpClient client = CreateClient(); - - HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + [Theory] + [MemberData(nameof(OrderByDataEf))] + public async Task CanOrderByDateAndTimeOfDayPropertyOnEf(string orderby, string expect) + { + string requestUri = $"convention/EfCustomers?{orderby}"; + HttpClient client = CreateClient(); - JObject content = await response.Content.ReadAsObject(); + HttpResponseMessage response = await client.GetAsync(requestUri); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(5, content["value"].Count()); + JObject content = await response.Content.ReadAsObject(); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < 5; i++) - { - sb.Append(content["value"][i]["Id"]).Append(" > "); - } - sb.Remove(sb.Length - 3, 3); // remove the last " > " + Assert.Equal(5, content["value"].Count()); - Assert.Equal(expect, sb.ToString()); - } - - private async Task ResetDatasource(string mode) + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 5; i++) { - var requestUriForPost = mode + "/ResetDataSource"; - HttpClient client = CreateClient(); - var responseForPost = await client.PostAsync(requestUriForPost, new StringContent("")); - Assert.True(responseForPost.IsSuccessStatusCode); - return responseForPost; + sb.Append(content["value"][i]["Id"]).Append(" > "); } + sb.Remove(sb.Length - 3, 3); // remove the last " > " + + Assert.Equal(expect, sb.ToString()); + } + + private async Task ResetDatasource(string mode) + { + var requestUriForPost = mode + "/ResetDataSource"; + HttpClient client = CreateClient(); + var responseForPost = await client.PostAsync(requestUriForPost, new StringContent("")); + Assert.True(responseForPost.IsSuccessStatusCode); + return responseForPost; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayWithEfTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayWithEfTest.cs index 7275828f8..d193d6c3b 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayWithEfTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayWithEfTest.cs @@ -27,32 +27,32 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay; + +public class DateAndTimeOfDayWithEfTest : WebApiTestBase { - public class DateAndTimeOfDayWithEfTest : WebApiTestBase + public DateAndTimeOfDayWithEfTest(WebApiTestFixture fixture) + :base(fixture) { - public DateAndTimeOfDayWithEfTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=EfDateAndTimeOfDayModelContext8"; - services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); + protected static void UpdateConfigureServices(IServiceCollection services) + { + string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=EfDateAndTimeOfDayModelContext8"; + services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); - services.ConfigureControllers(typeof(MetadataController), typeof(DateAndTimeOfDayModelsController)); + services.ConfigureControllers(typeof(MetadataController), typeof(DateAndTimeOfDayModelsController)); - services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select() - .AddRouteComponents("odata", BuildEdmModel())); - } + services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select() + .AddRouteComponents("odata", BuildEdmModel())); + } - [Fact] - public async Task MetadataDocument_IncludesDateAndTimeOfDayProperties() - { - // Arrange - string Uri = "odata/$metadata"; - string expected = "\r\n" + + [Fact] + public async Task MetadataDocument_IncludesDateAndTimeOfDayProperties() + { + // Arrange + string Uri = "odata/$metadata"; + string expected = "\r\n" + "\r\n" + " \r\n" + " \r\n" + @@ -78,52 +78,52 @@ public async Task MetadataDocument_IncludesDateAndTimeOfDayProperties() " \r\n" + ""; - // Remove indentation - expected = Regex.Replace(expected, @"\r\n\s*<", @"<"); - HttpClient client = CreateClient(); + // Remove indentation + expected = Regex.Replace(expected, @"\r\n\s*<", @"<"); + HttpClient client = CreateClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); - // Act - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(expected, await response.Content.ReadAsStringAsync()); - } + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(expected, await response.Content.ReadAsStringAsync()); + } - [Fact] - public async Task CanQueryEntitySet_WithDateAndTimeOfDayProperties() - { - // Arrange - string Uri = "odata/DateAndTimeOfDayModels"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - var result = JObject.Parse(await response.Content.ReadAsStringAsync()); - - Assert.Equal(5, result["value"].Count()); - - // test one for each entity - Assert.Equal("2015-12-23", result["value"][0]["EndDay"]); - Assert.Equal(JValue.CreateNull(), result["value"][1]["DeliverDay"]); - Assert.Equal("08:06:04.0030000", result["value"][2]["ResumeTime"]); - Assert.Equal(JValue.CreateNull(), result["value"][3]["EndTime"]); - Assert.Equal("05:03:05.0790000", result["value"][4]["CreatedTime"]); - } + [Fact] + public async Task CanQueryEntitySet_WithDateAndTimeOfDayProperties() + { + // Arrange + string Uri = "odata/DateAndTimeOfDayModels"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + + Assert.Equal(5, result["value"].Count()); + + // test one for each entity + Assert.Equal("2015-12-23", result["value"][0]["EndDay"]); + Assert.Equal(JValue.CreateNull(), result["value"][1]["DeliverDay"]); + Assert.Equal("08:06:04.0030000", result["value"][2]["ResumeTime"]); + Assert.Equal(JValue.CreateNull(), result["value"][3]["EndTime"]); + Assert.Equal("05:03:05.0790000", result["value"][4]["CreatedTime"]); + } - [Fact] - public async Task CanQuerySingleEntity_WithDateAndTimeOfDayProperties() - { - // Arrange - string Uri = "odata/DateAndTimeOfDayModels(2)"; + [Fact] + public async Task CanQuerySingleEntity_WithDateAndTimeOfDayProperties() + { + // Arrange + string Uri = "odata/DateAndTimeOfDayModels(2)"; - string expect = @"{ + string expect = @"{ ""@odata.context"": ""{XXXX}/odata/$metadata#DateAndTimeOfDayModels/$entity"", ""@odata.type"": ""#Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay.DateAndTimeOfDayModel"", ""@odata.id"": ""{XXXX}/odata/DateAndTimeOfDayModels(2)"", @@ -142,288 +142,287 @@ public async Task CanQuerySingleEntity_WithDateAndTimeOfDayProperties() ""CreatedTime"": ""02:03:05.0790000"", ""EndTime"": null }"; - expect = expect.Replace("{XXXX}", "http://localhost"); + expect = expect.Replace("{XXXX}", "http://localhost"); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.True(response.IsSuccessStatusCode); - var result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal(JObject.Parse(expect), result); - } - - [Fact] - public async Task CanSelect_OnDateAndTimeOfDayProperties() - { - // Arrange - string Uri = "odata/DateAndTimeOfDayModels(3)?$select=Birthday,PublishDay,DeliverDay,CreatedTime,ResumeTime"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - - var result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal("2018-12-22", result["Birthday"]); - Assert.Equal(JValue.CreateNull(), result["PublishDay"]); - Assert.Equal("2017-12-22", result["DeliverDay"]); - Assert.Equal("03:03:05.0790000", result["CreatedTime"]); - Assert.Equal("08:06:04.0030000", result["ResumeTime"]); - } - - [Theory] - [InlineData("?$filter=year(Birthday) eq 2017", "2")] - [InlineData("?$filter=month(PublishDay) eq 01", "4")] - [InlineData("?$filter=day(EndDay) ne 27", "1,2,3,4")] - [InlineData("?$filter=Birthday gt 2017-12-22", "3,4,5")] - [InlineData("?$filter=PublishDay eq null", "1,3,5")] // the following four cases are for nullable - [InlineData("?$filter=PublishDay eq 2016-03-22", "2")] - [InlineData("?$filter=PublishDay ne 2016-03-22", "1,3,4,5")] - [InlineData("?$filter=PublishDay lt 2016-03-22", "4")] - [InlineData("?$filter=EndTime ne null", "1,3,5")] - // [InlineData("?$filter=CreatedTime eq 04:03:05.0790000", "4")] // EFCore could not be translated. - // [InlineData("?$filter=hour(EndTime) eq 11", "1")] // EFCore could not be translated. - // [InlineData("?$filter=minute(EndTime) eq 06", "3")] // EFCore could not be translated. - // [InlineData("?$filter=second(EndTime) eq 10", "5")] // EFCore could not be translated. - [InlineData("?$filter=EndTime eq null", "2,4")] - // [InlineData("?$filter=EndTime ge 02:03:05.0790000", "1,3,5")] // EFCore could not be translated. - public async Task CanFilter_OnDateAndTimeOfDayProperties(string filter, string expect) - { - // Arrange - string Uri = "odata/DateAndTimeOfDayModels" + filter; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); + // Assert + Assert.True(response.IsSuccessStatusCode); + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal(JObject.Parse(expect), result); + } - // Assert - Assert.True(response.IsSuccessStatusCode); + [Fact] + public async Task CanSelect_OnDateAndTimeOfDayProperties() + { + // Arrange + string Uri = "odata/DateAndTimeOfDayModels(3)?$select=Birthday,PublishDay,DeliverDay,CreatedTime,ResumeTime"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("2018-12-22", result["Birthday"]); + Assert.Equal(JValue.CreateNull(), result["PublishDay"]); + Assert.Equal("2017-12-22", result["DeliverDay"]); + Assert.Equal("03:03:05.0790000", result["CreatedTime"]); + Assert.Equal("08:06:04.0030000", result["ResumeTime"]); + } - JObject result = await response.Content.ReadAsObject(); - Assert.Equal(expect, string.Join(",", result["value"].Select(e => e["Id"].ToString()))); - } + [Theory] + [InlineData("?$filter=year(Birthday) eq 2017", "2")] + [InlineData("?$filter=month(PublishDay) eq 01", "4")] + [InlineData("?$filter=day(EndDay) ne 27", "1,2,3,4")] + [InlineData("?$filter=Birthday gt 2017-12-22", "3,4,5")] + [InlineData("?$filter=PublishDay eq null", "1,3,5")] // the following four cases are for nullable + [InlineData("?$filter=PublishDay eq 2016-03-22", "2")] + [InlineData("?$filter=PublishDay ne 2016-03-22", "1,3,4,5")] + [InlineData("?$filter=PublishDay lt 2016-03-22", "4")] + [InlineData("?$filter=EndTime ne null", "1,3,5")] + // [InlineData("?$filter=CreatedTime eq 04:03:05.0790000", "4")] // EFCore could not be translated. + // [InlineData("?$filter=hour(EndTime) eq 11", "1")] // EFCore could not be translated. + // [InlineData("?$filter=minute(EndTime) eq 06", "3")] // EFCore could not be translated. + // [InlineData("?$filter=second(EndTime) eq 10", "5")] // EFCore could not be translated. + [InlineData("?$filter=EndTime eq null", "2,4")] + // [InlineData("?$filter=EndTime ge 02:03:05.0790000", "1,3,5")] // EFCore could not be translated. + public async Task CanFilter_OnDateAndTimeOfDayProperties(string filter, string expect) + { + // Arrange + string Uri = "odata/DateAndTimeOfDayModels" + filter; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpClient client = CreateClient(); - [Theory] - [InlineData("?$orderby=Birthday", "1,2,3,4,5")] - [InlineData("?$orderby=Birthday desc", "5,4,3,2,1")] - [InlineData("?$orderby=PublishDay", "1,3,5,4,2")] - [InlineData("?$orderby=PublishDay desc", "2,4,5,3,1")] - [InlineData("?$orderby=CreatedTime", "1,2,3,4,5")] - [InlineData("?$orderby=CreatedTime desc", "5,4,3,2,1")] - public async Task CanOrderBy_OnDateAndTimeOfDayProperties(string orderby, string expect) - { - // Arrange - string Uri = "odata/DateAndTimeOfDayModels" + orderby; - var request = new HttpRequestMessage(HttpMethod.Get, Uri); - HttpClient client = CreateClient(); + // Act + HttpResponseMessage response = await client.SendAsync(request); - // Act - var response = await client.SendAsync(request); + // Assert + Assert.True(response.IsSuccessStatusCode); - // Assert - Assert.True(response.IsSuccessStatusCode); + JObject result = await response.Content.ReadAsObject(); + Assert.Equal(expect, string.Join(",", result["value"].Select(e => e["Id"].ToString()))); + } - var result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal(5, result["value"].Count()); + [Theory] + [InlineData("?$orderby=Birthday", "1,2,3,4,5")] + [InlineData("?$orderby=Birthday desc", "5,4,3,2,1")] + [InlineData("?$orderby=PublishDay", "1,3,5,4,2")] + [InlineData("?$orderby=PublishDay desc", "2,4,5,3,1")] + [InlineData("?$orderby=CreatedTime", "1,2,3,4,5")] + [InlineData("?$orderby=CreatedTime desc", "5,4,3,2,1")] + public async Task CanOrderBy_OnDateAndTimeOfDayProperties(string orderby, string expect) + { + // Arrange + string Uri = "odata/DateAndTimeOfDayModels" + orderby; + var request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpClient client = CreateClient(); - Assert.Equal(expect, string.Join(",", result["value"].Select(e => e["Id"].ToString()))); - } + // Act + var response = await client.SendAsync(request); - [Fact] - public async Task PostEntity_WithDateAndTimeOfDayTimeProperties() - { - // Arrange - const string Payload = "{" + - "\"Id\":99," + - "\"Birthday\":\"2099-01-01\"," + - "\"CreatedTime\":\"14:13:15.1790000\"," + - "\"EndDay\":\"1990-12-22\"}"; - - string Uri = "odata/DateAndTimeOfDayModels"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, Uri); - - request.Content = new StringContent(Payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = Payload.Length; - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - } + // Assert + Assert.True(response.IsSuccessStatusCode); - [Fact] - public async Task PutEntity_WithDateAndTimeOfDayProperties() - { - // Arrange - const string Payload = "{" + - "\"Birthday\":\"2199-01-02\"," + - "\"CreatedTime\":\"14:13:15.1790000\"}"; + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal(5, result["value"].Count()); - string Uri = "odata/DateAndTimeOfDayModels(3)"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, Uri); + Assert.Equal(expect, string.Join(",", result["value"].Select(e => e["Id"].ToString()))); + } - request.Content = new StringContent(Payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = Payload.Length; - HttpClient client = CreateClient(); + [Fact] + public async Task PostEntity_WithDateAndTimeOfDayTimeProperties() + { + // Arrange + const string Payload = "{" + + "\"Id\":99," + + "\"Birthday\":\"2099-01-01\"," + + "\"CreatedTime\":\"14:13:15.1790000\"," + + "\"EndDay\":\"1990-12-22\"}"; + + string Uri = "odata/DateAndTimeOfDayModels"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, Uri); + + request.Content = new StringContent(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = Payload.Length; + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } - // Act - HttpResponseMessage response = await client.SendAsync(request); + [Fact] + public async Task PutEntity_WithDateAndTimeOfDayProperties() + { + // Arrange + const string Payload = "{" + + "\"Birthday\":\"2199-01-02\"," + + "\"CreatedTime\":\"14:13:15.1790000\"}"; - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } + string Uri = "odata/DateAndTimeOfDayModels(3)"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, Uri); - private static IEdmModel BuildEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("DateAndTimeOfDayModels"); + request.Content = new StringContent(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = Payload.Length; + HttpClient client = CreateClient(); - var type = builder.EntityType(); - type.Property(c => c.EndDay).AsDate(); - type.Property(c => c.DeliverDay).AsDate(); - type.Property(c => c.ResumeTime).AsTimeOfDay(); + // Act + HttpResponseMessage response = await client.SendAsync(request); - return builder.GetEdmModel(); - } + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } - public class DateAndTimeOfDayModelsController : ODataController + private static IEdmModel BuildEdmModel() { - private EfDateAndTimeOfDayModelContext _db; + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("DateAndTimeOfDayModels"); - public DateAndTimeOfDayModelsController(EfDateAndTimeOfDayModelContext context) - { - context.Database.EnsureCreated(); - if (!context.DateTimes.Any()) - { - DateTime dt = new DateTime(2015, 12, 22); - - IList dateTimes = Enumerable.Range(1, 5).Select(i => - new DateAndTimeOfDayModel - { - // Id = i, - Birthday = dt.AddYears(i), - EndDay = dt.AddDays(i), - DeliverDay = i % 2 == 0 ? (DateTime?)null : dt.AddYears(5 - i), - PublishDay = i % 2 != 0 ? (DateTime?)null : dt.AddMonths(5 - i), - CreatedTime = new TimeSpan(0, i, 3, 5, 79), - EndTime = i % 2 == 0 ? (TimeSpan?)null : new TimeSpan(0, 10 + i, 3 + i, 5 + i, 79 + i), - ResumeTime = new TimeSpan(0, 8, 6, 4, 3) - }).ToList(); - - foreach (var efDateTime in dateTimes) - { - context.DateTimes.Add(efDateTime); - } + var type = builder.EntityType(); + type.Property(c => c.EndDay).AsDate(); + type.Property(c => c.DeliverDay).AsDate(); + type.Property(c => c.ResumeTime).AsTimeOfDay(); - context.SaveChanges(); - } + return builder.GetEdmModel(); + } +} - _db = context; - } +public class DateAndTimeOfDayModelsController : ODataController +{ + private EfDateAndTimeOfDayModelContext _db; - [EnableQuery] - public IActionResult Get() + public DateAndTimeOfDayModelsController(EfDateAndTimeOfDayModelContext context) + { + context.Database.EnsureCreated(); + if (!context.DateTimes.Any()) { - return Ok(_db.DateTimes); - } + DateTime dt = new DateTime(2015, 12, 22); - [EnableQuery] - public IActionResult Get(int key) - { - DateAndTimeOfDayModel dtm = _db.DateTimes.FirstOrDefault(e => e.Id == key); - if (dtm == null) + IList dateTimes = Enumerable.Range(1, 5).Select(i => + new DateAndTimeOfDayModel + { + // Id = i, + Birthday = dt.AddYears(i), + EndDay = dt.AddDays(i), + DeliverDay = i % 2 == 0 ? (DateTime?)null : dt.AddYears(5 - i), + PublishDay = i % 2 != 0 ? (DateTime?)null : dt.AddMonths(5 - i), + CreatedTime = new TimeSpan(0, i, 3, 5, 79), + EndTime = i % 2 == 0 ? (TimeSpan?)null : new TimeSpan(0, 10 + i, 3 + i, 5 + i, 79 + i), + ResumeTime = new TimeSpan(0, 8, 6, 4, 3) + }).ToList(); + + foreach (var efDateTime in dateTimes) { - return NotFound(); + context.DateTimes.Add(efDateTime); } - return Ok(dtm); + context.SaveChanges(); } - public IActionResult Post([FromBody]DateAndTimeOfDayModel dt) - { - Assert.NotNull(dt); - - Assert.Equal(99, dt.Id); - Assert.Equal(new DateTime(2099, 1, 1), dt.Birthday); - Assert.Equal(new TimeSpan(0, 14, 13, 15, 179), dt.CreatedTime); - Assert.Equal(new DateTime(1990, 12, 22), dt.EndDay); + _db = context; + } - return Created(dt); - } + [EnableQuery] + public IActionResult Get() + { + return Ok(_db.DateTimes); + } - public IActionResult Put(int key, [FromBody]Delta dt) + [EnableQuery] + public IActionResult Get(int key) + { + DateAndTimeOfDayModel dtm = _db.DateTimes.FirstOrDefault(e => e.Id == key); + if (dtm == null) { - Assert.Equal(new[] { "Birthday", "CreatedTime" }, dt.GetChangedPropertyNames()); - - // Birthday - object value; - bool success = dt.TryGetPropertyValue("Birthday", out value); - Assert.True(success); - DateTime dateTime = Assert.IsType(value); - Assert.Equal(DateTimeKind.Unspecified, dateTime.Kind); - Assert.Equal(new DateTime(2199, 1, 2), dateTime); - - // CreatedTime - success = dt.TryGetPropertyValue("CreatedTime", out value); - Assert.True(success); - TimeSpan timeSpan = Assert.IsType(value); - Assert.Equal(new TimeSpan(0, 14, 13, 15, 179), timeSpan); - return Updated(dt); + return NotFound(); } + + return Ok(dtm); } - public class EfDateAndTimeOfDayModelContext : DbContext + public IActionResult Post([FromBody]DateAndTimeOfDayModel dt) { - public EfDateAndTimeOfDayModelContext(DbContextOptions options) - : base(options) - { - } + Assert.NotNull(dt); - public DbSet DateTimes { get; set; } + Assert.Equal(99, dt.Id); + Assert.Equal(new DateTime(2099, 1, 1), dt.Birthday); + Assert.Equal(new TimeSpan(0, 14, 13, 15, 179), dt.CreatedTime); + Assert.Equal(new DateTime(1990, 12, 22), dt.EndDay); - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().Property(c => c.EndDay).HasColumnType("date"); - modelBuilder.Entity().Property(c => c.DeliverDay).HasColumnType("date"); - } + return Created(dt); + } + + public IActionResult Put(int key, [FromBody]Delta dt) + { + Assert.Equal(new[] { "Birthday", "CreatedTime" }, dt.GetChangedPropertyNames()); + + // Birthday + object value; + bool success = dt.TryGetPropertyValue("Birthday", out value); + Assert.True(success); + DateTime dateTime = Assert.IsType(value); + Assert.Equal(DateTimeKind.Unspecified, dateTime.Kind); + Assert.Equal(new DateTime(2199, 1, 2), dateTime); + + // CreatedTime + success = dt.TryGetPropertyValue("CreatedTime", out value); + Assert.True(success); + TimeSpan timeSpan = Assert.IsType(value); + Assert.Equal(new TimeSpan(0, 14, 13, 15, 179), timeSpan); + return Updated(dt); + } +} + +public class EfDateAndTimeOfDayModelContext : DbContext +{ + public EfDateAndTimeOfDayModelContext(DbContextOptions options) + : base(options) + { } - public class DateAndTimeOfDayModel + public DbSet DateTimes { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { - public int Id { get; set; } + modelBuilder.Entity().Property(c => c.EndDay).HasColumnType("date"); + modelBuilder.Entity().Property(c => c.DeliverDay).HasColumnType("date"); + } +} - [Column(TypeName = "date")] - public DateTime Birthday { get; set; } +public class DateAndTimeOfDayModel +{ + public int Id { get; set; } - [Column(TypeName = "DaTe")] - public DateTime? PublishDay { get; set; } + [Column(TypeName = "date")] + public DateTime Birthday { get; set; } - public DateTime EndDay { get; set; } // will use the Fluent API + [Column(TypeName = "DaTe")] + public DateTime? PublishDay { get; set; } - public DateTime? DeliverDay { get; set; } // will use the Fluent API + public DateTime EndDay { get; set; } // will use the Fluent API - [Column(TypeName = "time")] - public TimeSpan CreatedTime { get; set; } + public DateTime? DeliverDay { get; set; } // will use the Fluent API - [Column(TypeName = "tIme")] - public TimeSpan? EndTime { get; set; } + [Column(TypeName = "time")] + public TimeSpan CreatedTime { get; set; } - public TimeSpan ResumeTime { get; set; } // will use the Fluent API - } + [Column(TypeName = "tIme")] + public TimeSpan? EndTime { get; set; } + + public TimeSpan ResumeTime { get; set; } // will use the Fluent API } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateTimeFilterTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateTimeFilterTest.cs index 221501d48..512eba975 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateTimeFilterTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateTimeFilterTest.cs @@ -22,112 +22,111 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay; + +public class DateTimeFilterTest : WebApiTestBase { - public class DateTimeFilterTest : WebApiTestBase + public DateTimeFilterTest(WebApiTestFixture fixture) + :base(fixture) { - public DateTimeFilterTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseInMemoryDatabase("TestEntityInMemoryContextList")); + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseInMemoryDatabase("TestEntityInMemoryContextList")); - services.ConfigureControllers(typeof(TestEntitiesController)); + services.ConfigureControllers(typeof(TestEntitiesController)); - services.AddControllers() - .AddOData(o => o - .AddRouteComponents("", GetEdmModel()) - .Count().Expand().Filter().Select().SetMaxTop(1000).OrderBy() - .TimeZone = TimeZoneInfo.Utc); - } + services.AddControllers() + .AddOData(o => o + .AddRouteComponents("", GetEdmModel()) + .Count().Expand().Filter().Select().SetMaxTop(1000).OrderBy() + .TimeZone = TimeZoneInfo.Utc); + } - private static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("TestEntities"); - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("TestEntities"); + return builder.GetEdmModel(); + } - [Fact] - public async Task QueryEntitiesWithDateTimeUsingDifferentTimeZone() - { - // Arrange - string requestUri = $"TestEntities?$select=Label,Stamp&$filter=Stamp ge 2021-01-02T00:00:00Z and Stamp lt 2021-01-03T00:00:00Z"; + [Fact] + public async Task QueryEntitiesWithDateTimeUsingDifferentTimeZone() + { + // Arrange + string requestUri = $"TestEntities?$select=Label,Stamp&$filter=Stamp ge 2021-01-02T00:00:00Z and Stamp lt 2021-01-03T00:00:00Z"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); - // Act - var response = await client.SendAsync(request); + // Act + var response = await client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string payload = await response.Content.ReadAsStringAsync(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string payload = await response.Content.ReadAsStringAsync(); - Assert.Equal("{\"@odata.context\":\"http://localhost/$metadata#TestEntities(Label,Stamp)\"," + - "\"value\":[" + - "{\"Label\":\"DAY 2\",\"Stamp\":\"2021-01-02T00:00:00Z\"}," + - "{\"Label\":\"DAY 2\",\"Stamp\":\"2021-01-02T23:59:59Z\"}" + - "]" + - "}", payload); - } + Assert.Equal("{\"@odata.context\":\"http://localhost/$metadata#TestEntities(Label,Stamp)\"," + + "\"value\":[" + + "{\"Label\":\"DAY 2\",\"Stamp\":\"2021-01-02T00:00:00Z\"}," + + "{\"Label\":\"DAY 2\",\"Stamp\":\"2021-01-02T23:59:59Z\"}" + + "]" + + "}", payload); } +} - public class TestEntity - { - public int Id { get; set; } - public string Label { get; set; } - public DateTime Stamp { get; set; } - } +public class TestEntity +{ + public int Id { get; set; } + public string Label { get; set; } + public DateTime Stamp { get; set; } +} - public class TestEntityInMemoryContext : DbContext - { - public TestEntityInMemoryContext(DbContextOptions options) : base(options) - { } +public class TestEntityInMemoryContext : DbContext +{ + public TestEntityInMemoryContext(DbContextOptions options) : base(options) + { } - public DbSet TestEntities { get; set; } + public DbSet TestEntities { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // SPECIFY ALL DATETIME VALUES AS UTC + var utcConverter = new ValueConverter(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc)); + var allProps = modelBuilder.Model.GetEntityTypes().SelectMany(t => t.GetProperties()); - protected override void OnModelCreating(ModelBuilder modelBuilder) + foreach (var property in allProps) { - // SPECIFY ALL DATETIME VALUES AS UTC - var utcConverter = new ValueConverter(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc)); - var allProps = modelBuilder.Model.GetEntityTypes().SelectMany(t => t.GetProperties()); - - foreach (var property in allProps) - { - if ((property.ClrType == typeof(DateTime)) || (property.ClrType == typeof(DateTime?))) { property.SetValueConverter(utcConverter); } - } + if ((property.ClrType == typeof(DateTime)) || (property.ClrType == typeof(DateTime?))) { property.SetValueConverter(utcConverter); } } } +} - public class TestEntitiesController : ODataController - { - private readonly TestEntityInMemoryContext _ctx; +public class TestEntitiesController : ODataController +{ + private readonly TestEntityInMemoryContext _ctx; - public TestEntitiesController(TestEntityInMemoryContext ctx) + public TestEntitiesController(TestEntityInMemoryContext ctx) + { + _ctx = ctx; + if (!_ctx.TestEntities.Any()) { - _ctx = ctx; - if (!_ctx.TestEntities.Any()) - { - _ctx.TestEntities.Add(new TestEntity { Id = 1, Label = "DAY 1", Stamp = new DateTime(2021, 01, 01, 00, 00, 00) }); - _ctx.TestEntities.Add(new TestEntity { Id = 2, Label = "DAY 1", Stamp = new DateTime(2021, 01, 01, 23, 59, 59) }); - _ctx.TestEntities.Add(new TestEntity { Id = 3, Label = "DAY 2", Stamp = new DateTime(2021, 01, 02, 00, 00, 00) }); - _ctx.TestEntities.Add(new TestEntity { Id = 4, Label = "DAY 2", Stamp = new DateTime(2021, 01, 02, 23, 59, 59) }); - _ctx.TestEntities.Add(new TestEntity { Id = 5, Label = "DAY 3", Stamp = new DateTime(2021, 01, 03, 00, 00, 00) }); - _ctx.TestEntities.Add(new TestEntity { Id = 6, Label = "DAY 3", Stamp = new DateTime(2021, 01, 03, 23, 59, 59) }); - - _ctx.SaveChanges(); - } + _ctx.TestEntities.Add(new TestEntity { Id = 1, Label = "DAY 1", Stamp = new DateTime(2021, 01, 01, 00, 00, 00) }); + _ctx.TestEntities.Add(new TestEntity { Id = 2, Label = "DAY 1", Stamp = new DateTime(2021, 01, 01, 23, 59, 59) }); + _ctx.TestEntities.Add(new TestEntity { Id = 3, Label = "DAY 2", Stamp = new DateTime(2021, 01, 02, 00, 00, 00) }); + _ctx.TestEntities.Add(new TestEntity { Id = 4, Label = "DAY 2", Stamp = new DateTime(2021, 01, 02, 23, 59, 59) }); + _ctx.TestEntities.Add(new TestEntity { Id = 5, Label = "DAY 3", Stamp = new DateTime(2021, 01, 03, 00, 00, 00) }); + _ctx.TestEntities.Add(new TestEntity { Id = 6, Label = "DAY 3", Stamp = new DateTime(2021, 01, 03, 23, 59, 59) }); + + _ctx.SaveChanges(); } + } - [HttpGet] - [EnableQuery] - public IEnumerable Get() - { - return _ctx.TestEntities; - } + [HttpGet] + [EnableQuery] + public IEnumerable Get() + { + return _ctx.TestEntities; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateWithEfTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateWithEfTest.cs index ea3347b4e..100bf4205 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateWithEfTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateWithEfTest.cs @@ -19,101 +19,100 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateAndTimeOfDay; + +public class DateWithEfTest : WebApiTestBase { - public class DateWithEfTest : WebApiTestBase + public DateWithEfTest(WebApiTestFixture fixture) + :base(fixture) { - public DateWithEfTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=EdmDateWithEfDbContext8"; - services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); + protected static void UpdateConfigureServices(IServiceCollection services) + { + string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=EdmDateWithEfDbContext8"; + services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); - services.ConfigureControllers(typeof(EfPeopleController)); + services.ConfigureControllers(typeof(EfPeopleController)); - IEdmModel model = DateAndTimeOfDayEdmModel.BuildEfPersonEdmModel(); + IEdmModel model = DateAndTimeOfDayEdmModel.BuildEfPersonEdmModel(); - // TODO: modify it after implement the DI in Web API. - // model.SetPayloadValueConverter(new MyConverter()); + // TODO: modify it after implement the DI in Web API. + // model.SetPayloadValueConverter(new MyConverter()); - services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null) - .AddRouteComponents("odata", model)); - } + services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null) + .AddRouteComponents("odata", model)); + } - [Theory] - [InlineData("$filter=Birthday eq null", "2,4")] - [InlineData("$filter=Birthday ne null", "1,3,5")] - [InlineData("$filter=Birthday eq 2015-10-01", "1")] - [InlineData("$filter=Birthday eq 2015-10-03", "3")] - [InlineData("$filter=Birthday ne 2015-10-03", "1,2,4,5")] - public async Task CanFilterByDatePropertyForDateTimePropertyOnEf(string filter, string expect) - { - // Arrange - string requestUri = $"odata/EfPeople?{filter}"; - HttpClient client = CreateClient(); + [Theory] + [InlineData("$filter=Birthday eq null", "2,4")] + [InlineData("$filter=Birthday ne null", "1,3,5")] + [InlineData("$filter=Birthday eq 2015-10-01", "1")] + [InlineData("$filter=Birthday eq 2015-10-03", "3")] + [InlineData("$filter=Birthday ne 2015-10-03", "1,2,4,5")] + public async Task CanFilterByDatePropertyForDateTimePropertyOnEf(string filter, string expect) + { + // Arrange + string requestUri = $"odata/EfPeople?{filter}"; + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject content = await response.Content.ReadAsObject(); + JObject content = await response.Content.ReadAsObject(); - Assert.Equal(expect, string.Join(",", content["value"].Select(e => e["Id"].ToString()))); - } + Assert.Equal(expect, string.Join(",", content["value"].Select(e => e["Id"].ToString()))); + } - [Fact(Skip = "TODO: Processing of the LINQ expression failed")] - public async Task CanGroupByDatePropertyForDateTimePropertyOnEf() - { - // Arrange - string requestUri = "odata/EfPeople?$apply=groupby((Birthday), aggregate($count as Cnt))"; - HttpClient client = CreateClient(); + [Fact(Skip = "TODO: Processing of the LINQ expression failed")] + public async Task CanGroupByDatePropertyForDateTimePropertyOnEf() + { + // Arrange + string requestUri = "odata/EfPeople?$apply=groupby((Birthday), aggregate($count as Cnt))"; + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } - [Fact] - public async Task CanQuerySingleEntityFromTaskReturnTypeInControllerOnEf() - { - // Arrange - string requestUri = "odata/EfPeople(1)"; - HttpClient client = CreateClient(); + [Fact] + public async Task CanQuerySingleEntityFromTaskReturnTypeInControllerOnEf() + { + // Arrange + string requestUri = "odata/EfPeople(1)"; + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject content = await response.Content.ReadAsObject(); + JObject content = await response.Content.ReadAsObject(); - Assert.Equal("1", (string)content["Id"]); - } + Assert.Equal("1", (string)content["Id"]); } +} - public class MyConverter : ODataPayloadValueConverter +public class MyConverter : ODataPayloadValueConverter +{ + public override object ConvertToPayloadValue(object value, IEdmTypeReference edmTypeReference) { - public override object ConvertToPayloadValue(object value, IEdmTypeReference edmTypeReference) + if (edmTypeReference != null && edmTypeReference.IsDate()) { - if (edmTypeReference != null && edmTypeReference.IsDate()) + if (value is DateTimeOffset) { - if (value is DateTimeOffset) - { - DateTimeOffset dto = (DateTimeOffset)value; - return new Date(dto.Year, dto.Month, dto.Day); - } + DateTimeOffset dto = (DateTimeOffset)value; + return new Date(dto.Year, dto.Month, dto.Day); } - - return base.ConvertToPayloadValue(value, edmTypeReference); } + + return base.ConvertToPayloadValue(value, edmTypeReference); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateOnlyTimeOnly/DateAndTimeOfDayWithEfTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateOnlyTimeOnly/DateAndTimeOfDayWithEfTest.cs index d99cb59bd..7328d44ee 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateOnlyTimeOnly/DateAndTimeOfDayWithEfTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateOnlyTimeOnly/DateAndTimeOfDayWithEfTest.cs @@ -26,32 +26,32 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DateOnlyTimeOnly +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateOnlyTimeOnly; + +public class DateOnlyAndTimeOnlyWithEfTest : WebApiTestBase { - public class DateOnlyAndTimeOnlyWithEfTest : WebApiTestBase + public DateOnlyAndTimeOnlyWithEfTest(WebApiTestFixture fixture) + :base(fixture) { - public DateOnlyAndTimeOnlyWithEfTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=DateOnlyAndTimeOnlyModelContext8"; - services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); + protected static void UpdateConfigureServices(IServiceCollection services) + { + string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=DateOnlyAndTimeOnlyModelContext8"; + services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); - services.ConfigureControllers(typeof(MetadataController), typeof(DateOnlyTimeOnlyModelsController)); + services.ConfigureControllers(typeof(MetadataController), typeof(DateOnlyTimeOnlyModelsController)); - services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select() - .AddRouteComponents("odata", BuildEdmModel())); - } + services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select() + .AddRouteComponents("odata", BuildEdmModel())); + } - [Fact] - public async Task MetadataDocument_IncludesDateOnlyAndTimeOnlyProperties() - { - // Arrange - string Uri = "odata/$metadata"; - string expected = "\r\n" + + [Fact] + public async Task MetadataDocument_IncludesDateOnlyAndTimeOnlyProperties() + { + // Arrange + string Uri = "odata/$metadata"; + string expected = "\r\n" + "\r\n" + " \r\n" + " \r\n" + @@ -76,52 +76,52 @@ public async Task MetadataDocument_IncludesDateOnlyAndTimeOnlyProperties() " \r\n" + ""; - // Remove indentation - expected = Regex.Replace(expected, @"\r\n\s*<", @"<"); - HttpClient client = CreateClient(); + // Remove indentation + expected = Regex.Replace(expected, @"\r\n\s*<", @"<"); + HttpClient client = CreateClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); - // Act - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(expected, await response.Content.ReadAsStringAsync()); - } + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(expected, await response.Content.ReadAsStringAsync()); + } - [Fact] - public async Task CanQueryEntitySet_WithDateOnlyAndTimeOnlyProperties() - { - // Arrange - string Uri = "odata/DateOnlyTimeOnlyModels"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - var result = JObject.Parse(await response.Content.ReadAsStringAsync()); - - Assert.Equal(5, result["value"].Count()); - - // test one for each entity - Assert.Equal("2012-03-07", result["value"][0]["EndDay"]); - Assert.Equal(JValue.CreateNull(), result["value"][1]["PublishDay"]); - Assert.Equal("03:13:06.0080000", result["value"][2]["ResumeTime"]); - Assert.Equal(JValue.CreateNull(), result["value"][3]["EndTime"]); - Assert.Equal("00:05:03.0050000", result["value"][4]["CreatedTime"]); - } + [Fact] + public async Task CanQueryEntitySet_WithDateOnlyAndTimeOnlyProperties() + { + // Arrange + string Uri = "odata/DateOnlyTimeOnlyModels"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + + Assert.Equal(5, result["value"].Count()); + + // test one for each entity + Assert.Equal("2012-03-07", result["value"][0]["EndDay"]); + Assert.Equal(JValue.CreateNull(), result["value"][1]["PublishDay"]); + Assert.Equal("03:13:06.0080000", result["value"][2]["ResumeTime"]); + Assert.Equal(JValue.CreateNull(), result["value"][3]["EndTime"]); + Assert.Equal("00:05:03.0050000", result["value"][4]["CreatedTime"]); + } - [Fact] - public async Task CanQuerySingleEntity_WithDateOnlyAndTimeOnlyProperties() - { - // Arrange - string Uri = "odata/DateOnlyTimeOnlyModels(2)"; + [Fact] + public async Task CanQuerySingleEntity_WithDateOnlyAndTimeOnlyProperties() + { + // Arrange + string Uri = "odata/DateOnlyTimeOnlyModels(2)"; - string expect = @"{ + string expect = @"{ ""@odata.context"": ""http://localhost/odata/$metadata#DateOnlyTimeOnlyModels/$entity"", ""@odata.type"": ""#Microsoft.AspNetCore.OData.E2E.Tests.DateOnlyTimeOnly.DateOnyTimeOnlyModel"", ""@odata.id"": ""http://localhost/odata/DateOnlyTimeOnlyModels(2)"", @@ -138,260 +138,259 @@ public async Task CanQuerySingleEntity_WithDateOnlyAndTimeOnlyProperties() ""ResumeTime@odata.type"": ""#TimeOfDay"", ""ResumeTime"": ""02:12:05.0070000"" }"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.True(response.IsSuccessStatusCode); - var result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal(JObject.Parse(expect), result); - } + // Assert + Assert.True(response.IsSuccessStatusCode); + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal(JObject.Parse(expect), result); + } - [Fact] - public async Task CanSelect_OnDateOnlyAndTimeOnlyProperties() - { - // Arrange - string Uri = "odata/DateOnlyTimeOnlyModels(3)?$select=Birthday,PublishDay,CreatedTime,ResumeTime"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - - var result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal("2013-04-08", result["Birthday"]); - Assert.Equal("2018-09-18", result["PublishDay"]); - Assert.Equal("00:03:03.0050000", result["CreatedTime"]); - Assert.Equal("03:13:06.0080000", result["ResumeTime"]); - } + [Fact] + public async Task CanSelect_OnDateOnlyAndTimeOnlyProperties() + { + // Arrange + string Uri = "odata/DateOnlyTimeOnlyModels(3)?$select=Birthday,PublishDay,CreatedTime,ResumeTime"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("2013-04-08", result["Birthday"]); + Assert.Equal("2018-09-18", result["PublishDay"]); + Assert.Equal("00:03:03.0050000", result["CreatedTime"]); + Assert.Equal("03:13:06.0080000", result["ResumeTime"]); + } - [Theory] - [InlineData("?$filter=year(Birthday) eq 2015", "5")] - [InlineData("?$filter=month(PublishDay) eq 11", "1")] - [InlineData("?$filter=day(EndDay) ne 09", "1,2,4,5")] - [InlineData("?$filter=Birthday gt 2013-04-08", "4,5")] - [InlineData("?$filter=PublishDay eq null", "2,4")] // the following four cases are for nullable - [InlineData("?$filter=PublishDay eq 2018-09-18", "3")] - [InlineData("?$filter=PublishDay ne 2018-09-18", "1,5")] - [InlineData("?$filter=PublishDay lt 2019-12-31", "1,3")] - [InlineData("?$filter=EndTime ne null", "1,3,5")] - [InlineData("?$filter=CreatedTime eq 00:01:03.0050000", "1")] - [InlineData("?$filter=hour(EndTime) eq 01", "1")] - [InlineData("?$filter=minute(EndTime) eq 15", "5")] - [InlineData("?$filter=second(EndTime) eq 06", "3")] - [InlineData("?$filter=EndTime eq null", "2,4")] - [InlineData("?$filter=EndTime ge 00:03:05.0790000", "1,3,5")] - public async Task CanFilter_OnDateOnlyAndTimeOnlyProperties(string filter, string expect) - { - // Arrange - string Uri = "odata/DateOnlyTimeOnlyModels" + filter; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); - HttpClient client = CreateClient(); + [Theory] + [InlineData("?$filter=year(Birthday) eq 2015", "5")] + [InlineData("?$filter=month(PublishDay) eq 11", "1")] + [InlineData("?$filter=day(EndDay) ne 09", "1,2,4,5")] + [InlineData("?$filter=Birthday gt 2013-04-08", "4,5")] + [InlineData("?$filter=PublishDay eq null", "2,4")] // the following four cases are for nullable + [InlineData("?$filter=PublishDay eq 2018-09-18", "3")] + [InlineData("?$filter=PublishDay ne 2018-09-18", "1,5")] + [InlineData("?$filter=PublishDay lt 2019-12-31", "1,3")] + [InlineData("?$filter=EndTime ne null", "1,3,5")] + [InlineData("?$filter=CreatedTime eq 00:01:03.0050000", "1")] + [InlineData("?$filter=hour(EndTime) eq 01", "1")] + [InlineData("?$filter=minute(EndTime) eq 15", "5")] + [InlineData("?$filter=second(EndTime) eq 06", "3")] + [InlineData("?$filter=EndTime eq null", "2,4")] + [InlineData("?$filter=EndTime ge 00:03:05.0790000", "1,3,5")] + public async Task CanFilter_OnDateOnlyAndTimeOnlyProperties(string filter, string expect) + { + // Arrange + string Uri = "odata/DateOnlyTimeOnlyModels" + filter; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpResponseMessage response = await client.SendAsync(request); - // Assert - string payload = await response.Content.ReadAsStringAsync(); - Assert.True(response.IsSuccessStatusCode); + // Assert + string payload = await response.Content.ReadAsStringAsync(); + Assert.True(response.IsSuccessStatusCode); - JObject result = await response.Content.ReadAsObject(); - Assert.Equal(expect, string.Join(",", result["value"].Select(e => e["Id"].ToString()))); - } - - [Theory] - [InlineData("?$orderby=Birthday", "1,2,3,4,5")] - [InlineData("?$orderby=Birthday desc", "5,4,3,2,1")] - [InlineData("?$orderby=PublishDay", "2,4,1,3,5")] - [InlineData("?$orderby=PublishDay desc", "5,3,1,2,4")] - [InlineData("?$orderby=CreatedTime", "1,2,3,4,5")] - [InlineData("?$orderby=CreatedTime desc", "5,4,3,2,1")] - public async Task CanOrderBy_OnDateOnlyAndTimeOnlyProperties(string orderby, string expect) - { - // Arrange - string Uri = "odata/DateOnlyTimeOnlyModels" + orderby; - var request = new HttpRequestMessage(HttpMethod.Get, Uri); - HttpClient client = CreateClient(); + JObject result = await response.Content.ReadAsObject(); + Assert.Equal(expect, string.Join(",", result["value"].Select(e => e["Id"].ToString()))); + } - // Act - var response = await client.SendAsync(request); + [Theory] + [InlineData("?$orderby=Birthday", "1,2,3,4,5")] + [InlineData("?$orderby=Birthday desc", "5,4,3,2,1")] + [InlineData("?$orderby=PublishDay", "2,4,1,3,5")] + [InlineData("?$orderby=PublishDay desc", "5,3,1,2,4")] + [InlineData("?$orderby=CreatedTime", "1,2,3,4,5")] + [InlineData("?$orderby=CreatedTime desc", "5,4,3,2,1")] + public async Task CanOrderBy_OnDateOnlyAndTimeOnlyProperties(string orderby, string expect) + { + // Arrange + string Uri = "odata/DateOnlyTimeOnlyModels" + orderby; + var request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpClient client = CreateClient(); - // Assert - Assert.True(response.IsSuccessStatusCode); + // Act + var response = await client.SendAsync(request); - var result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal(5, result["value"].Count()); + // Assert + Assert.True(response.IsSuccessStatusCode); - Assert.Equal(expect, string.Join(",", result["value"].Select(e => e["Id"].ToString()))); - } + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal(5, result["value"].Count()); - [Fact] - public async Task PostEntity_WithDateOnlyAndTimeOnlyProperties() - { - // Arrange - const string Payload = "{" + - "\"Id\":99," + - "\"Birthday\":\"2099-01-01\"," + - "\"CreatedTime\":\"14:13:15.1790000\"," + - "\"EndDay\":\"1990-12-22\"}"; - - string Uri = "odata/DateOnlyTimeOnlyModels"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, Uri); - - request.Content = new StringContent(Payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = Payload.Length; - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - } + Assert.Equal(expect, string.Join(",", result["value"].Select(e => e["Id"].ToString()))); + } - [Fact] - public async Task PutEntity_WithDateOnlyAndTimeOnlyProperties() - { - // Arrange - const string Payload = "{" + - "\"Birthday\":\"2199-01-02\"," + - "\"CreatedTime\":\"14:13:15.1790000\"}"; + [Fact] + public async Task PostEntity_WithDateOnlyAndTimeOnlyProperties() + { + // Arrange + const string Payload = "{" + + "\"Id\":99," + + "\"Birthday\":\"2099-01-01\"," + + "\"CreatedTime\":\"14:13:15.1790000\"," + + "\"EndDay\":\"1990-12-22\"}"; + + string Uri = "odata/DateOnlyTimeOnlyModels"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, Uri); + + request.Content = new StringContent(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = Payload.Length; + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } - string Uri = "odata/DateOnlyTimeOnlyModels(3)"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, Uri); + [Fact] + public async Task PutEntity_WithDateOnlyAndTimeOnlyProperties() + { + // Arrange + const string Payload = "{" + + "\"Birthday\":\"2199-01-02\"," + + "\"CreatedTime\":\"14:13:15.1790000\"}"; - request.Content = new StringContent(Payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = Payload.Length; - HttpClient client = CreateClient(); + string Uri = "odata/DateOnlyTimeOnlyModels(3)"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, Uri); - // Act - HttpResponseMessage response = await client.SendAsync(request); + request.Content = new StringContent(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = Payload.Length; + HttpClient client = CreateClient(); - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } + // Act + HttpResponseMessage response = await client.SendAsync(request); - private static IEdmModel BuildEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("DateOnlyTimeOnlyModels"); - return builder.GetEdmModel(); - } + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } - public class DateOnlyTimeOnlyModelsController : ODataController + private static IEdmModel BuildEdmModel() { - // private DateAndOnlyTimeOnlyModelContext _db; - private static IList _dateTimes = Enumerable.Range(1, 5).Select(i => - new DateOnyTimeOnlyModel - { - Id = i, - Birthday = new DateOnly(2010 + i, 1 + i, 5 + i), - - PublishDay = i % 2 == 0 ? null : new DateOnly(2015 + i, 12 - i, 15 + i), + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("DateOnlyTimeOnlyModels"); + return builder.GetEdmModel(); + } +} - EndDay = new DateOnly(2010 + i + 1, 2 + i, 6 + i), +public class DateOnlyTimeOnlyModelsController : ODataController +{ + // private DateAndOnlyTimeOnlyModelContext _db; + private static IList _dateTimes = Enumerable.Range(1, 5).Select(i => + new DateOnyTimeOnlyModel + { + Id = i, + Birthday = new DateOnly(2010 + i, 1 + i, 5 + i), - CreatedTime = new TimeOnly(0, i, 3, 5), + PublishDay = i % 2 == 0 ? null : new DateOnly(2015 + i, 12 - i, 15 + i), - EndTime = i % 2 == 0 ? null : new TimeOnly(i, 10 + i, 3 + i, 5 + i), + EndDay = new DateOnly(2010 + i + 1, 2 + i, 6 + i), - ResumeTime = new TimeOnly(i, 10 + i, 3 + i, 5 + i) + CreatedTime = new TimeOnly(0, i, 3, 5), - }).ToList(); + EndTime = i % 2 == 0 ? null : new TimeOnly(i, 10 + i, 3 + i, 5 + i), - [EnableQuery] - public IActionResult Get() - { - return Ok(_dateTimes); - } + ResumeTime = new TimeOnly(i, 10 + i, 3 + i, 5 + i) - [EnableQuery] - public IActionResult Get(int key) - { - DateOnyTimeOnlyModel dtm = _dateTimes.FirstOrDefault(e => e.Id == key); - if (dtm == null) - { - return NotFound(); - } + }).ToList(); - return Ok(dtm); - } + [EnableQuery] + public IActionResult Get() + { + return Ok(_dateTimes); + } - public IActionResult Post([FromBody]DateOnyTimeOnlyModel dt) + [EnableQuery] + public IActionResult Get(int key) + { + DateOnyTimeOnlyModel dtm = _dateTimes.FirstOrDefault(e => e.Id == key); + if (dtm == null) { - Assert.NotNull(dt); - - Assert.Equal(99, dt.Id); - Assert.Equal(new DateOnly(2099, 1, 1), dt.Birthday); - Assert.Equal(new TimeOnly(14, 13, 15, 179), dt.CreatedTime); - Assert.Equal(new DateOnly(1990, 12, 22), dt.EndDay); - - return Created(dt); + return NotFound(); } - public IActionResult Put(int key, [FromBody]Delta dt) - { - Assert.Equal(new[] { "Birthday", "CreatedTime" }, dt.GetChangedPropertyNames()); - - // Birthday - object value; - bool success = dt.TryGetPropertyValue("Birthday", out value); - Assert.True(success); - DateOnly dateOnly = Assert.IsType(value); - Assert.Equal(new DateOnly(2199, 1, 2), dateOnly); - - // CreatedTime - success = dt.TryGetPropertyValue("CreatedTime", out value); - Assert.True(success); - TimeOnly timeOnly = Assert.IsType(value); - Assert.Equal(new TimeOnly(14, 13, 15, 179), timeOnly); - return Updated(dt); - } + return Ok(dtm); } - // EF Core 6 doesn't support DateOnly and TimeOnly yet - public class DateAndOnlyTimeOnlyModelContext : DbContext + public IActionResult Post([FromBody]DateOnyTimeOnlyModel dt) { - public DateAndOnlyTimeOnlyModelContext(DbContextOptions options) - : base(options) - { - } + Assert.NotNull(dt); - public DbSet DateTimes { get; set; } + Assert.Equal(99, dt.Id); + Assert.Equal(new DateOnly(2099, 1, 1), dt.Birthday); + Assert.Equal(new TimeOnly(14, 13, 15, 179), dt.CreatedTime); + Assert.Equal(new DateOnly(1990, 12, 22), dt.EndDay); - //protected override void OnModelCreating(ModelBuilder modelBuilder) - //{ - // modelBuilder.Entity().Property(c => c.EndDay).HasColumnType("date"); - // modelBuilder.Entity().Property(c => c.DeliverDay).HasColumnType("date"); - //} + return Created(dt); } - public class DateOnyTimeOnlyModel + public IActionResult Put(int key, [FromBody]Delta dt) { - public int Id { get; set; } + Assert.Equal(new[] { "Birthday", "CreatedTime" }, dt.GetChangedPropertyNames()); + + // Birthday + object value; + bool success = dt.TryGetPropertyValue("Birthday", out value); + Assert.True(success); + DateOnly dateOnly = Assert.IsType(value); + Assert.Equal(new DateOnly(2199, 1, 2), dateOnly); + + // CreatedTime + success = dt.TryGetPropertyValue("CreatedTime", out value); + Assert.True(success); + TimeOnly timeOnly = Assert.IsType(value); + Assert.Equal(new TimeOnly(14, 13, 15, 179), timeOnly); + return Updated(dt); + } +} - public DateOnly Birthday { get; set; } +// EF Core 6 doesn't support DateOnly and TimeOnly yet +public class DateAndOnlyTimeOnlyModelContext : DbContext +{ + public DateAndOnlyTimeOnlyModelContext(DbContextOptions options) + : base(options) + { + } - public DateOnly? PublishDay { get; set; } + public DbSet DateTimes { get; set; } - public DateOnly EndDay { get; set; } + //protected override void OnModelCreating(ModelBuilder modelBuilder) + //{ + // modelBuilder.Entity().Property(c => c.EndDay).HasColumnType("date"); + // modelBuilder.Entity().Property(c => c.DeliverDay).HasColumnType("date"); + //} +} - public TimeOnly CreatedTime { get; set; } +public class DateOnyTimeOnlyModel +{ + public int Id { get; set; } - public TimeOnly? EndTime { get; set; } + public DateOnly Birthday { get; set; } - public TimeOnly ResumeTime { get; set; } - } -} \ No newline at end of file + public DateOnly? PublishDay { get; set; } + + public DateOnly EndDay { get; set; } + + public TimeOnly CreatedTime { get; set; } + + public TimeOnly? EndTime { get; set; } + + public TimeOnly ResumeTime { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetController.cs index 5a428090f..8c9741048 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetController.cs @@ -15,139 +15,138 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DateTimeOffsetSupport +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateTimeOffsetSupport; + +public class FilesController : ODataController { - public class FilesController : ODataController - { - private readonly FilesContext _db; + private readonly FilesContext _db; - public FilesController(FilesContext context) + public FilesController(FilesContext context) + { + context.Database.EnsureCreated(); + if (!context.Files.Any()) { - context.Database.EnsureCreated(); - if (!context.Files.Any()) + foreach (var file in CreateFiles()) { - foreach (var file in CreateFiles()) - { - context.Files.Add(file); - } - - context.SaveChanges(); + context.Files.Add(file); } - _db = context; + context.SaveChanges(); } - [EnableQuery] - public IQueryable Get() - { - return _db.Files; - } + _db = context; + } - [EnableQuery] - public IActionResult Get(int key) - { - File file = _db.Files.FirstOrDefault(c => c.FileId == key); - if (file == null) - { - return NotFound(); - } + [EnableQuery] + public IQueryable Get() + { + return _db.Files; + } - return Ok(file); + [EnableQuery] + public IActionResult Get(int key) + { + File file = _db.Files.FirstOrDefault(c => c.FileId == key); + if (file == null) + { + return NotFound(); } - [HttpPost] - public IActionResult Post([FromBody]File file) + return Ok(file); + } + + [HttpPost] + public IActionResult Post([FromBody]File file) + { + if (file.FileId == 99) { - if (file.FileId == 99) - { - // Special test case for property name case-insensitive - Assert.Equal("abc", file.Name); - Assert.Equal(new DateTimeOffset(2021, 10, 28, 21, 33, 26, TimeSpan.FromHours(8)), file.CreatedDate); - Assert.Equal(new DateTimeOffset(2021, 11, 01, 10, 48, 12, TimeSpan.FromHours(8)), file.DeleteDate); + // Special test case for property name case-insensitive + Assert.Equal("abc", file.Name); + Assert.Equal(new DateTimeOffset(2021, 10, 28, 21, 33, 26, TimeSpan.FromHours(8)), file.CreatedDate); + Assert.Equal(new DateTimeOffset(2021, 11, 01, 10, 48, 12, TimeSpan.FromHours(8)), file.DeleteDate); - // special string used to verify at test case. - return Ok("PropertyCaseInsensitive"); - } + // special string used to verify at test case. + return Ok("PropertyCaseInsensitive"); + } - _db.Files.Add(file); - _db.SaveChanges(); + _db.Files.Add(file); + _db.SaveChanges(); - return Created(file); - } + return Created(file); + } - public IActionResult Patch(int key, Delta patch) + public IActionResult Patch(int key, Delta patch) + { + var file = _db.Files.SingleOrDefault(c => c.FileId == key); + if (file == null) { - var file = _db.Files.SingleOrDefault(c => c.FileId == key); - if (file == null) - { - return NotFound(); - } + return NotFound(); + } - patch.Patch(file); - _db.SaveChanges(); + patch.Patch(file); + _db.SaveChanges(); - return Updated(file); - } + return Updated(file); + } - public IActionResult Delete(int key) + public IActionResult Delete(int key) + { + File original = _db.Files.FirstOrDefault(c => c.FileId == key); + if (original == null) { - File original = _db.Files.FirstOrDefault(c => c.FileId == key); - if (original == null) - { - return NotFound(); - } + return NotFound(); + } - _db.Files.Remove(original); - _db.SaveChanges(); + _db.Files.Remove(original); + _db.SaveChanges(); - return StatusCode(StatusCodes.Status204NoContent); - } + return StatusCode(StatusCodes.Status204NoContent); + } - public IActionResult GetCreatedDate(int key) + public IActionResult GetCreatedDate(int key) + { + File file = _db.Files.FirstOrDefault(c => c.FileId == key); + if (file == null) { - File file = _db.Files.FirstOrDefault(c => c.FileId == key); - if (file == null) - { - return NotFound(); - } - - return Ok(file.CreatedDate); + return NotFound(); } - [HttpPost("/ResetDataSource")] - public IActionResult ResetDataSource() - { - _db.Files.RemoveRange(_db.Files); - _db.SaveChanges(); + return Ok(file.CreatedDate); + } - _db.Database.EnsureDeleted(); - _db.Database.EnsureCreated(); + [HttpPost("/ResetDataSource")] + public IActionResult ResetDataSource() + { + _db.Files.RemoveRange(_db.Files); + _db.SaveChanges(); - var files = CreateFiles(); - _db.Files.AddRange(files); - _db.SaveChanges(); + _db.Database.EnsureDeleted(); + _db.Database.EnsureCreated(); - return StatusCode(StatusCodes.Status204NoContent); - } + var files = CreateFiles(); + _db.Files.AddRange(files); + _db.SaveChanges(); - public static IEnumerable CreateFiles() - { - DateTimeOffset dateTime = new DateTimeOffset(2021, 4, 15, 16, 24, 8, TimeSpan.FromHours(-8)); - - // #2 is used for update/get round trip, its value will be changed every test running. - // #3 is used for get, will never change its value. - // #4 is used to select date time property, will never change its value. - // #6 is used for create/get round trip, it will create, get, delete - return Enumerable.Range(1, 5).Select(e => - new File - { - // FileId = e, - Name = "File #" + e, - CreatedDate = dateTime.AddMonths(3 - e).AddYears(e % 2 == 0 ? e : -e), - DeleteDate = dateTime.AddMonths(e % 2 == 0 ? e : -e), - }); - } + return StatusCode(StatusCodes.Status204NoContent); + } + + public static IEnumerable CreateFiles() + { + DateTimeOffset dateTime = new DateTimeOffset(2021, 4, 15, 16, 24, 8, TimeSpan.FromHours(-8)); + + // #2 is used for update/get round trip, its value will be changed every test running. + // #3 is used for get, will never change its value. + // #4 is used to select date time property, will never change its value. + // #6 is used for create/get round trip, it will create, get, delete + return Enumerable.Range(1, 5).Select(e => + new File + { + // FileId = e, + Name = "File #" + e, + CreatedDate = dateTime.AddMonths(3 - e).AddYears(e % 2 == 0 ? e : -e), + DeleteDate = dateTime.AddMonths(e % 2 == 0 ? e : -e), + }); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetDataModel.cs index 239282320..2326ac43c 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetDataModel.cs @@ -9,46 +9,45 @@ using System.ComponentModel.DataAnnotations; using Microsoft.EntityFrameworkCore; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DateTimeOffsetSupport +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateTimeOffsetSupport; + +public class FilesContext : DbContext { - public class FilesContext : DbContext - { - public FilesContext(DbContextOptions options) - : base(options) - { } + public FilesContext(DbContextOptions options) + : base(options) + { } - public DbSet Files { get; set; } - } + public DbSet Files { get; set; } +} - public class File - { - [Key] - public int FileId { get; set; } +public class File +{ + [Key] + public int FileId { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public DateTimeOffset CreatedDate { get; set; } + public DateTimeOffset CreatedDate { get; set; } - public DateTimeOffset? DeleteDate { get; set; } + public DateTimeOffset? DeleteDate { get; set; } - public override bool Equals(Object o) + public override bool Equals(Object o) + { + var f = o as File; + if (f == null) { - var f = o as File; - if (f == null) - { - return false; - } - - var v1 = FileId.Equals(f.FileId); - var v2 = Name.Equals(f.Name); - var v3 = CreatedDate.Equals(f.CreatedDate); - var v4 = Object.Equals(DeleteDate, f.DeleteDate); - return v1 && v2 && v3 && v4; + return false; } - public override int GetHashCode() - { - return base.GetHashCode(); - } + var v1 = FileId.Equals(f.FileId); + var v2 = Name.Equals(f.Name); + var v3 = CreatedDate.Equals(f.CreatedDate); + var v4 = Object.Equals(DeleteDate, f.DeleteDate); + return v1 && v2 && v3 && v4; + } + + public override int GetHashCode() + { + return base.GetHashCode(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetEdmModel.cs index eb6bfef85..abf635c20 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetEdmModel.cs @@ -8,27 +8,26 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DateTimeOffsetSupport +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateTimeOffsetSupport; + +public class DateTimeOffsetEdmModel { - public class DateTimeOffsetEdmModel + public static IEdmModel GetExplicitModel() { - public static IEdmModel GetExplicitModel() - { - ODataModelBuilder builder = new ODataModelBuilder(); - var fileType = builder.EntityType().HasKey(f => f.FileId); - fileType.Property(f => f.Name); - fileType.Property(f => f.CreatedDate); - fileType.Property(f => f.DeleteDate); + ODataModelBuilder builder = new ODataModelBuilder(); + var fileType = builder.EntityType().HasKey(f => f.FileId); + fileType.Property(f => f.Name); + fileType.Property(f => f.CreatedDate); + fileType.Property(f => f.DeleteDate); - var files = builder.EntitySet("Files"); - return builder.GetEdmModel(); - } + var files = builder.EntitySet("Files"); + return builder.GetEdmModel(); + } - public static IEdmModel GetConventionModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Files"); - return builder.GetEdmModel(); - } + public static IEdmModel GetConventionModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Files"); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetTest.cs index 23049ca29..7b4038008 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateTimeOffsetSupport/DateTimeOffsetTest.cs @@ -19,363 +19,362 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DateTimeOffsetSupport +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateTimeOffsetSupport; + +public class DateTimeOffsetTest : WebApiTestBase { - public class DateTimeOffsetTest : WebApiTestBase + #region Configuration and Static Members + public DateTimeOffsetTest(WebApiTestFixture fixture) + :base(fixture) { - #region Configuration and Static Members - public DateTimeOffsetTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=DateTimeOffsetSupport8"; - services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); + protected static void UpdateConfigureServices(IServiceCollection services) + { + string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=DateTimeOffsetSupport8"; + services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); - services.ConfigureControllers(typeof(FilesController)); + services.ConfigureControllers(typeof(FilesController)); - services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select() - .AddRouteComponents("convention", DateTimeOffsetEdmModel.GetConventionModel()) - .AddRouteComponents("explicit", DateTimeOffsetEdmModel.GetExplicitModel())); - } - #endregion + services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select() + .AddRouteComponents("convention", DateTimeOffsetEdmModel.GetConventionModel()) + .AddRouteComponents("explicit", DateTimeOffsetEdmModel.GetExplicitModel())); + } + #endregion - #region Helper methods - private string Serialize(Object obj) - { - return JsonConvert.SerializeObject(obj); - } + #region Helper methods + private string Serialize(Object obj) + { + return JsonConvert.SerializeObject(obj); + } + + private async Task DeserializeAsync(HttpResponseMessage response) + { + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + } + + private async Task> DeserializeListAsync(HttpResponseMessage response) + { + JObject json = JObject.Parse(await response.Content.ReadAsStringAsync()); + IList value = json["value"].Children().ToList(); + IList files = new List(); - private async Task DeserializeAsync(HttpResponseMessage response) + foreach (JToken token in value) { - return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var file = JsonConvert.DeserializeObject(token.ToString()); + files.Add(file); } - private async Task> DeserializeListAsync(HttpResponseMessage response) - { - JObject json = JObject.Parse(await response.Content.ReadAsStringAsync()); - IList value = json["value"].Children().ToList(); - IList files = new List(); + return files; + } + #endregion - foreach (JToken token in value) - { - var file = JsonConvert.DeserializeObject(token.ToString()); - files.Add(file); - } + #region CRUD on DateTimeOffset related entity + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task QueryFileEntityTest(string mode) + { + // Arrange + string requestUri = $"{mode}/Files(3)"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); - return files; - } - #endregion + // Act + HttpResponseMessage response = await client.SendAsync(request); - #region CRUD on DateTimeOffset related entity - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task QueryFileEntityTest(string mode) - { - // Arrange - string requestUri = $"{mode}/Files(3)"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - File file = await DeserializeAsync(response); - - Assert.Equal(new File() - { - FileId = 3, - Name = "File #3", - CreatedDate = new DateTimeOffset(2018, 4, 15, 16, 24, 08, TimeSpan.FromHours(-8)), - DeleteDate = new DateTimeOffset(2021, 1, 15, 16, 24, 8, TimeSpan.FromHours(-8)) - }, file); - } + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + File file = await DeserializeAsync(response); - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task UpdateFileEntityTestRoundTrip(string mode) + Assert.Equal(new File() { - HttpClient client = CreateClient(); - string requestUri = $"{mode}/Files(2)"; - - // GET ~/Files(2) - HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - File file2 = await DeserializeAsync(response); - - // set - var now = DateTimeOffset.Now; - var contentObject = new { CreatedDate = now }; - string content = Serialize(contentObject); - - // Patch ~/Files(2) - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - request.Content = new StringContent(content); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = content.Length; - - response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - - // GET ~/Files(2) - response = await client.GetAsync(requestUri); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - file2.CreatedDate = now; - File newFile2 = await DeserializeAsync(response); - Assert.Equal(file2, newFile2); - } + FileId = 3, + Name = "File #3", + CreatedDate = new DateTimeOffset(2018, 4, 15, 16, 24, 08, TimeSpan.FromHours(-8)), + DeleteDate = new DateTimeOffset(2021, 1, 15, 16, 24, 8, TimeSpan.FromHours(-8)) + }, file); + } - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task CreateDeleteFileEntityRoundTrip(string mode) - { - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.PostAsync("/ResetDataSource", null); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - - // Arrange - string filesUri = $"{mode}/Files"; - - File newFile = new File() - { - Name = "FileName", - CreatedDate = DateTimeOffset.Now - }; - string content = Serialize(newFile); - - // POST ~/Files - HttpRequestMessage postRequest = new HttpRequestMessage(HttpMethod.Post, filesUri); - postRequest.Content = new StringContent(content); - postRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - postRequest.Content.Headers.ContentLength = content.Length; - - var postResponse = await client.SendAsync(postRequest); - Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode); - - var postResult = await DeserializeAsync(postResponse); - newFile.FileId = postResult.FileId; - Assert.Equal(newFile, postResult); - - string fileUri = filesUri + $"({postResult.FileId})"; - - // GET ~/Files(?) - HttpResponseMessage getResponse = await client.GetAsync(fileUri); - Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); - var getResult = JsonConvert.DeserializeObject(await getResponse.Content.ReadAsStringAsync()); - Assert.Equal(getResult, postResult); - - // Delete ~/Files(?) - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, fileUri); - var deleteResponse = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode); - - // GET ~/Files(?) - getResponse = await client.GetAsync(fileUri); - Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode); - } + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task UpdateFileEntityTestRoundTrip(string mode) + { + HttpClient client = CreateClient(); + string requestUri = $"{mode}/Files(2)"; + + // GET ~/Files(2) + HttpResponseMessage response = await client.GetAsync(requestUri); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + File file2 = await DeserializeAsync(response); + + // set + var now = DateTimeOffset.Now; + var contentObject = new { CreatedDate = now }; + string content = Serialize(contentObject); + + // Patch ~/Files(2) + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + request.Content = new StringContent(content); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = content.Length; + + response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // GET ~/Files(2) + response = await client.GetAsync(requestUri); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + file2.CreatedDate = now; + File newFile2 = await DeserializeAsync(response); + Assert.Equal(file2, newFile2); + } - [Fact] - public async Task CreateFileEntity_Works_UsingDifferentPropertyNameCase() - { - // Arrange - HttpClient client = CreateClient(); - string filesUri = $"convention/Files"; - string content = - "{" + - "\"fileid\":99," + // use a special ID to test - "\"naMe\":\"abc\"," + - "\"creaTeddate\":\"2021-10-28T21:33:26+08:00\"," + - "\"deLEteDate\":\"2021-11-01T10:48:12+08:00\"" + - "}"; - - // Act: POST ~/Files - HttpRequestMessage postRequest = new HttpRequestMessage(HttpMethod.Post, filesUri); - postRequest.Content = new StringContent(content); - postRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - - var postResponse = await client.SendAsync(postRequest); - - // Assert - Assert.Equal(HttpStatusCode.OK, postResponse.StatusCode); - - string payload = await postResponse.Content.ReadAsStringAsync(); - Assert.Contains("PropertyCaseInsensitive", payload); - } - #endregion + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task CreateDeleteFileEntityRoundTrip(string mode) + { + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.PostAsync("/ResetDataSource", null); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - #region Query option on DateTime + // Arrange + string filesUri = $"{mode}/Files"; - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task CanSelectDateTimeProperty(string mode) + File newFile = new File() { - // Arrange - HttpClient client = CreateClient(); + Name = "FileName", + CreatedDate = DateTimeOffset.Now + }; + string content = Serialize(newFile); + + // POST ~/Files + HttpRequestMessage postRequest = new HttpRequestMessage(HttpMethod.Post, filesUri); + postRequest.Content = new StringContent(content); + postRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + postRequest.Content.Headers.ContentLength = content.Length; + + var postResponse = await client.SendAsync(postRequest); + Assert.Equal(HttpStatusCode.Created, postResponse.StatusCode); + + var postResult = await DeserializeAsync(postResponse); + newFile.FileId = postResult.FileId; + Assert.Equal(newFile, postResult); + + string fileUri = filesUri + $"({postResult.FileId})"; + + // GET ~/Files(?) + HttpResponseMessage getResponse = await client.GetAsync(fileUri); + Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); + var getResult = JsonConvert.DeserializeObject(await getResponse.Content.ReadAsStringAsync()); + Assert.Equal(getResult, postResult); + + // Delete ~/Files(?) + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, fileUri); + var deleteResponse = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode); + + // GET ~/Files(?) + getResponse = await client.GetAsync(fileUri); + Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode); + } + + [Fact] + public async Task CreateFileEntity_Works_UsingDifferentPropertyNameCase() + { + // Arrange + HttpClient client = CreateClient(); + string filesUri = $"convention/Files"; + string content = + "{" + + "\"fileid\":99," + // use a special ID to test + "\"naMe\":\"abc\"," + + "\"creaTeddate\":\"2021-10-28T21:33:26+08:00\"," + + "\"deLEteDate\":\"2021-11-01T10:48:12+08:00\"" + + "}"; + + // Act: POST ~/Files + HttpRequestMessage postRequest = new HttpRequestMessage(HttpMethod.Post, filesUri); + postRequest.Content = new StringContent(content); + postRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + var postResponse = await client.SendAsync(postRequest); + + // Assert + Assert.Equal(HttpStatusCode.OK, postResponse.StatusCode); + + string payload = await postResponse.Content.ReadAsStringAsync(); + Assert.Contains("PropertyCaseInsensitive", payload); + } + #endregion - string requestUri = $"{mode}/Files(4)?$select=CreatedDate"; + #region Query option on DateTime - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task CanSelectDateTimeProperty(string mode) + { + // Arrange + HttpClient client = CreateClient(); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string payload = await response.Content.ReadAsStringAsync(); + string requestUri = $"{mode}/Files(4)?$select=CreatedDate"; - Assert.Contains("$metadata#Files(CreatedDate)/$entity\",\"CreatedDate\":\"2025-03-15T16:24:08-08:00\"}", payload); - } + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task CanFilterDateTimeProperty(string mode) - { - // Arrange - HttpClient client = CreateClient(); - string requestUri = $"{mode}/Files?$filter=year(CreatedDate) eq 2020"; + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string payload = await response.Content.ReadAsStringAsync(); - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("$metadata#Files(CreatedDate)/$entity\",\"CreatedDate\":\"2025-03-15T16:24:08-08:00\"}", payload); + } - var responseFileList = await DeserializeListAsync(response); - File actual = Assert.Single(responseFileList); - Assert.Equal("File #1", actual.Name); - } + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task CanFilterDateTimeProperty(string mode) + { + // Arrange + HttpClient client = CreateClient(); + string requestUri = $"{mode}/Files?$filter=year(CreatedDate) eq 2020"; - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task CanOrderByDateTimeProperty(string mode) - { - // Arrange - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.PostAsync("/ResetDataSource", null); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string requestUri = $"{mode}/Files?$orderby=CreatedDate"; + var responseFileList = await DeserializeListAsync(response); + File actual = Assert.Single(responseFileList); + Assert.Equal("File #1", actual.Name); + } - // Act - response = await client.GetAsync(requestUri + " desc"); + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task CanOrderByDateTimeProperty(string mode) + { + // Arrange + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.PostAsync("/ResetDataSource", null); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string requestUri = $"{mode}/Files?$orderby=CreatedDate"; - var responseFileList = await DeserializeListAsync(response); + // Act + response = await client.GetAsync(requestUri + " desc"); - Assert.Equal(new[] { 4, 2, 1, 3, 5 }, responseFileList.Select(f => f.FileId)); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - // Act - response = await client.GetAsync(requestUri + " asc"); + var responseFileList = await DeserializeListAsync(response); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var responseFileList2 = await DeserializeListAsync(response); + Assert.Equal(new[] { 4, 2, 1, 3, 5 }, responseFileList.Select(f => f.FileId)); - Assert.Equal(new[] { 5, 3, 1, 2, 4 }, responseFileList2.Select(f => f.FileId)); - } - #endregion + // Act + response = await client.GetAsync(requestUri + " asc"); - #region now() Function Tests - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task Now_FilterDateTimePropertyWithGt(string mode) - { - // Arrange - HttpClient client = CreateClient(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseFileList2 = await DeserializeListAsync(response); - string requestUri = $"{mode}/Files?$filter=CreatedDate gt now()"; + Assert.Equal(new[] { 5, 3, 1, 2, 4 }, responseFileList2.Select(f => f.FileId)); + } + #endregion - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + #region now() Function Tests + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task Now_FilterDateTimePropertyWithGt(string mode) + { + // Arrange + HttpClient client = CreateClient(); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string requestUri = $"{mode}/Files?$filter=CreatedDate gt now()"; - await DeserializeListAsync(response); - } + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task NowFilterDateTimePropertyWithLt(string mode) - { - // Arrange - HttpClient client = CreateClient(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string requestUri = $"{mode}/Files?$filter=CreatedDate lt now()"; + await DeserializeListAsync(response); + } - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task NowFilterDateTimePropertyWithLt(string mode) + { + // Arrange + HttpClient client = CreateClient(); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - await DeserializeListAsync(response); - } + string requestUri = $"{mode}/Files?$filter=CreatedDate lt now()"; - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task NowFilterDateTimePropertyWithDayFunction(string mode) - { - // Arrange - HttpClient client = CreateClient(); - string requestUri = $"{mode}/Files?$filter=day(CreatedDate) eq day(now())"; + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + await DeserializeListAsync(response); + } - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task NowFilterDateTimePropertyWithDayFunction(string mode) + { + // Arrange + HttpClient client = CreateClient(); + string requestUri = $"{mode}/Files?$filter=day(CreatedDate) eq day(now())"; - await DeserializeListAsync(response); - } + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task NowFilterDateTimePropertyWithMonthFunction(string mode) - { - // Arrange - HttpClient client = CreateClient(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string requestUri = $"{mode}/Files?$filter=month(CreatedDate) eq month(now())"; + await DeserializeListAsync(response); + } - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task NowFilterDateTimePropertyWithMonthFunction(string mode) + { + // Arrange + HttpClient client = CreateClient(); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - await DeserializeListAsync(response); - } + string requestUri = $"{mode}/Files?$filter=month(CreatedDate) eq month(now())"; - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task NowFilterDateTimePropertyWithYearFunction(string mode) - { - // Arrange - HttpClient client = CreateClient(); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + await DeserializeListAsync(response); + } + + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task NowFilterDateTimePropertyWithYearFunction(string mode) + { + // Arrange + HttpClient client = CreateClient(); - string requestUri = $"{mode}/Files?$filter=year(CreatedDate) ge year(now())"; + string requestUri = $"{mode}/Files?$filter=year(CreatedDate) ge year(now())"; - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - await DeserializeListAsync(response); - } - #endregion + await DeserializeListAsync(response); } + #endregion } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenControllers.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenControllers.cs index dfc61306c..8736d41df 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenControllers.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenControllers.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -14,110 +14,109 @@ using Microsoft.OData; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken +namespace Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken; + +public class TestCustomersController : ODataController { - public class TestCustomersController : ODataController + public IActionResult Get() { - public IActionResult Get() - { - IEdmModel model = Request.GetModel(); - - IEdmEntityType customerType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestCustomer") as IEdmEntityType; - IEdmEntityType customerWithAddressType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestCustomerWithAddress") as IEdmEntityType; - IEdmComplexType addressType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestAddress") as IEdmComplexType; - IEdmEntityType orderType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestOrder") as IEdmEntityType; - IEdmEntitySet ordersSet = model.FindDeclaredEntitySet("TestOrders") as IEdmEntitySet; - EdmChangedObjectCollection changedObjects = new EdmChangedObjectCollection(customerType); - - EdmDeltaComplexObject a = new EdmDeltaComplexObject(addressType); - a.TrySetPropertyValue("State", "State"); - a.TrySetPropertyValue("ZipCode", null); - - EdmDeltaResourceObject changedEntity = new EdmDeltaResourceObject(customerWithAddressType); - changedEntity.TrySetPropertyValue("Id", 1); - changedEntity.TrySetPropertyValue("Name", "Name"); - changedEntity.TrySetPropertyValue("Address", a); - changedEntity.TrySetPropertyValue("PhoneNumbers", new List { "123-4567", "765-4321" }); - changedEntity.TrySetPropertyValue("OpenProperty", 10); - changedEntity.TrySetPropertyValue("NullOpenProperty", null); - changedObjects.Add(changedEntity); - - EdmComplexObjectCollection places = new EdmComplexObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(addressType, true)))); - EdmDeltaComplexObject b = new EdmDeltaComplexObject(addressType); - b.TrySetPropertyValue("City", "City2"); - b.TrySetPropertyValue("State", "State2"); - b.TrySetPropertyValue("ZipCode", 12345); - b.TrySetPropertyValue("OpenProperty", 10); - b.TrySetPropertyValue("NullOpenProperty", null); - places.Add(a); - places.Add(b); - - var newCustomer = new EdmDeltaResourceObject(customerType); - newCustomer.TrySetPropertyValue("Id", 10); - newCustomer.TrySetPropertyValue("Name", "NewCustomer"); - newCustomer.TrySetPropertyValue("FavoritePlaces", places); - changedObjects.Add(newCustomer); - - var newOrder = new EdmDeltaResourceObject(orderType); - newOrder.NavigationSource = ordersSet; - newOrder.TrySetPropertyValue("Id", 27); - newOrder.TrySetPropertyValue("Amount", 100); - changedObjects.Add(newOrder); - - var deletedCustomer = new EdmDeltaDeletedResourceObject(customerType); - deletedCustomer.Id = new Uri("7", UriKind.RelativeOrAbsolute); - deletedCustomer.Reason = DeltaDeletedEntryReason.Changed; - changedObjects.Add(deletedCustomer); - - var deletedOrder = new EdmDeltaDeletedResourceObject(orderType); - deletedOrder.NavigationSource = ordersSet; - deletedOrder.Id = new Uri("12", UriKind.RelativeOrAbsolute); - deletedOrder.Reason = DeltaDeletedEntryReason.Deleted; - changedObjects.Add(deletedOrder); - - var deletedLink = new EdmDeltaDeletedLink(customerType); - deletedLink.Source = new Uri("http://localhost/odata/TestCustomers(1)"); - deletedLink.Target = new Uri("http://localhost/odata/TestOrders(12)"); - deletedLink.Relationship = "Orders"; - changedObjects.Add(deletedLink); - - var addedLink = new EdmDeltaLink(customerType); - addedLink.Source = new Uri("http://localhost/odata/TestCustomers(10)"); - addedLink.Target = new Uri("http://localhost/odata/TestOrders(27)"); - addedLink.Relationship = "Orders"; - changedObjects.Add(addedLink); - - return Ok(changedObjects); - } + IEdmModel model = Request.GetModel(); + + IEdmEntityType customerType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestCustomer") as IEdmEntityType; + IEdmEntityType customerWithAddressType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestCustomerWithAddress") as IEdmEntityType; + IEdmComplexType addressType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestAddress") as IEdmComplexType; + IEdmEntityType orderType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestOrder") as IEdmEntityType; + IEdmEntitySet ordersSet = model.FindDeclaredEntitySet("TestOrders") as IEdmEntitySet; + EdmChangedObjectCollection changedObjects = new EdmChangedObjectCollection(customerType); + + EdmDeltaComplexObject a = new EdmDeltaComplexObject(addressType); + a.TrySetPropertyValue("State", "State"); + a.TrySetPropertyValue("ZipCode", null); + + EdmDeltaResourceObject changedEntity = new EdmDeltaResourceObject(customerWithAddressType); + changedEntity.TrySetPropertyValue("Id", 1); + changedEntity.TrySetPropertyValue("Name", "Name"); + changedEntity.TrySetPropertyValue("Address", a); + changedEntity.TrySetPropertyValue("PhoneNumbers", new List { "123-4567", "765-4321" }); + changedEntity.TrySetPropertyValue("OpenProperty", 10); + changedEntity.TrySetPropertyValue("NullOpenProperty", null); + changedObjects.Add(changedEntity); + + EdmComplexObjectCollection places = new EdmComplexObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(addressType, true)))); + EdmDeltaComplexObject b = new EdmDeltaComplexObject(addressType); + b.TrySetPropertyValue("City", "City2"); + b.TrySetPropertyValue("State", "State2"); + b.TrySetPropertyValue("ZipCode", 12345); + b.TrySetPropertyValue("OpenProperty", 10); + b.TrySetPropertyValue("NullOpenProperty", null); + places.Add(a); + places.Add(b); + + var newCustomer = new EdmDeltaResourceObject(customerType); + newCustomer.TrySetPropertyValue("Id", 10); + newCustomer.TrySetPropertyValue("Name", "NewCustomer"); + newCustomer.TrySetPropertyValue("FavoritePlaces", places); + changedObjects.Add(newCustomer); + + var newOrder = new EdmDeltaResourceObject(orderType); + newOrder.NavigationSource = ordersSet; + newOrder.TrySetPropertyValue("Id", 27); + newOrder.TrySetPropertyValue("Amount", 100); + changedObjects.Add(newOrder); + + var deletedCustomer = new EdmDeltaDeletedResourceObject(customerType); + deletedCustomer.Id = new Uri("7", UriKind.RelativeOrAbsolute); + deletedCustomer.Reason = DeltaDeletedEntryReason.Changed; + changedObjects.Add(deletedCustomer); + + var deletedOrder = new EdmDeltaDeletedResourceObject(orderType); + deletedOrder.NavigationSource = ordersSet; + deletedOrder.Id = new Uri("12", UriKind.RelativeOrAbsolute); + deletedOrder.Reason = DeltaDeletedEntryReason.Deleted; + changedObjects.Add(deletedOrder); + + var deletedLink = new EdmDeltaDeletedLink(customerType); + deletedLink.Source = new Uri("http://localhost/odata/TestCustomers(1)"); + deletedLink.Target = new Uri("http://localhost/odata/TestOrders(12)"); + deletedLink.Relationship = "Orders"; + changedObjects.Add(deletedLink); + + var addedLink = new EdmDeltaLink(customerType); + addedLink.Source = new Uri("http://localhost/odata/TestCustomers(10)"); + addedLink.Target = new Uri("http://localhost/odata/TestOrders(27)"); + addedLink.Relationship = "Orders"; + changedObjects.Add(addedLink); + + return Ok(changedObjects); } +} - public class TestOrdersController : ODataController +public class TestOrdersController : ODataController +{ + public IActionResult Get() { - public IActionResult Get() - { - IEdmModel model = Request.GetModel(); - IEdmComplexType addressType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestAddress") as IEdmComplexType; - IEdmEntityType orderType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestOrder") as IEdmEntityType; - EdmChangedObjectCollection changedObjects = new EdmChangedObjectCollection(orderType); - - EdmDeltaComplexObject sampleList = new EdmDeltaComplexObject(addressType); - sampleList.TrySetPropertyValue("State", "sample state"); - sampleList.TrySetPropertyValue("ZipCode", 9); - sampleList.TrySetPropertyValue("title", "sample title"); // primitive dynamic - - EdmDeltaComplexObject location = new EdmDeltaComplexObject(addressType); - location.TrySetPropertyValue("State", "State"); - location.TrySetPropertyValue("ZipCode", null); - location.TrySetPropertyValue("OpenProperty", 10); // primitive dynamic - location.TrySetPropertyValue("key-samplelist", sampleList); // complex dynamic - - EdmDeltaResourceObject changedOrder = new EdmDeltaResourceObject(orderType); - changedOrder.TrySetPropertyValue("Id", 1); - changedOrder.TrySetPropertyValue("Amount", 42); - changedOrder.TrySetPropertyValue("Location", location); - changedObjects.Add(changedOrder); - - return Ok(changedObjects); - } + IEdmModel model = Request.GetModel(); + IEdmComplexType addressType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestAddress") as IEdmComplexType; + IEdmEntityType orderType = model.FindDeclaredType("Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestOrder") as IEdmEntityType; + EdmChangedObjectCollection changedObjects = new EdmChangedObjectCollection(orderType); + + EdmDeltaComplexObject sampleList = new EdmDeltaComplexObject(addressType); + sampleList.TrySetPropertyValue("State", "sample state"); + sampleList.TrySetPropertyValue("ZipCode", 9); + sampleList.TrySetPropertyValue("title", "sample title"); // primitive dynamic + + EdmDeltaComplexObject location = new EdmDeltaComplexObject(addressType); + location.TrySetPropertyValue("State", "State"); + location.TrySetPropertyValue("ZipCode", null); + location.TrySetPropertyValue("OpenProperty", 10); // primitive dynamic + location.TrySetPropertyValue("key-samplelist", sampleList); // complex dynamic + + EdmDeltaResourceObject changedOrder = new EdmDeltaResourceObject(orderType); + changedOrder.TrySetPropertyValue("Id", 1); + changedOrder.TrySetPropertyValue("Amount", 42); + changedOrder.TrySetPropertyValue("Location", location); + changedObjects.Add(changedOrder); + + return Ok(changedObjects); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenDataModel.cs index 89b18aa26..ec0f4a9f8 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenDataModel.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -7,37 +7,36 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken +namespace Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken; + +public class TestCustomer { - public class TestCustomer - { - public int Id { get; set; } - public string Name { get; set; } - public int Age { get; set; } - public virtual IList PhoneNumbers { get; set; } - public virtual IList Orders { get; set; } - public virtual IList FavoritePlaces { get; set; } - public IDictionary DynamicProperties { get; set; } - } + public int Id { get; set; } + public string Name { get; set; } + public int Age { get; set; } + public virtual IList PhoneNumbers { get; set; } + public virtual IList Orders { get; set; } + public virtual IList FavoritePlaces { get; set; } + public IDictionary DynamicProperties { get; set; } +} - public class TestCustomerWithAddress : TestCustomer - { - public virtual TestAddress Address { get; set; } - } +public class TestCustomerWithAddress : TestCustomer +{ + public virtual TestAddress Address { get; set; } +} - public class TestOrder - { - public int Id { get; set; } - public int Amount { get; set; } +public class TestOrder +{ + public int Id { get; set; } + public int Amount { get; set; } - public TestAddress Location { get; set; } - } + public TestAddress Location { get; set; } +} - public class TestAddress - { - public string State { get; set; } - public string City { get; set; } - public int? ZipCode { get; set; } - public IDictionary DynamicProperties { get; set; } - } +public class TestAddress +{ + public string State { get; set; } + public string City { get; set; } + public int? ZipCode { get; set; } + public IDictionary DynamicProperties { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs index 0eff7d9bd..f04856b46 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DeltaToken/DeltaTokenQueryTests.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -16,144 +16,143 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken +namespace Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken; + +public class DeltaTokenQueryTests : WebApiTestBase { - public class DeltaTokenQueryTests : WebApiTestBase + public DeltaTokenQueryTests(WebApiTestFixture fixture) + : base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(TestCustomersController), typeof(TestOrdersController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel()).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); + } + + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("TestCustomers"); + builder.EntitySet("TestOrders"); + return builder.GetEdmModel(); + } + + [Fact] + public async Task DeltaVerifyReslt() + { + HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, "odata/TestCustomers?$deltaToken=abc"); + get.Headers.Add("Accept", "application/json;odata.metadata=minimal"); + get.Headers.Add("OData-Version", "4.01"); + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.SendAsync(get); + Assert.True(response.IsSuccessStatusCode); + dynamic results = await response.Content.ReadAsObject(); + + Assert.True(results.value.Count == 7, "There should be 7 entries in the response"); + + var changeEntity = results.value[0]; + Assert.True(((JToken)changeEntity).Count() == 9, "The changed customer should have 6 properties plus type written. But now it contains non-changed properties, it's regression bug?"); + string changeEntityType = changeEntity["@type"].Value as string; + Assert.True(changeEntityType != null, "The changed customer should have type written"); + Assert.True(changeEntityType.Contains("#Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestCustomerWithAddress"), "The changed order should be a TestCustomerWithAddress"); + Assert.True(changeEntity.Id.Value == 1, "The ID Of changed customer should be 1."); + Assert.True(changeEntity.OpenProperty.Value == 10, "The OpenProperty property of changed customer should be 10."); + Assert.True(changeEntity.NullOpenProperty.Value == null, "The NullOpenProperty property of changed customer should be null."); + Assert.True(changeEntity.Name.Value == "Name", "The Name of changed customer should be 'Name'"); + Assert.True(((JToken)changeEntity.Address).Count() == 3, "The changed entity's Address should have 2 properties written. But now it contains non-changed properties, it's regression bug?"); + Assert.True(changeEntity.Address.State.Value == "State", "The changed customer's Address.State should be 'State'."); + Assert.True(changeEntity.Address.ZipCode.Value == (int?)null, "The changed customer's Address.ZipCode should be null."); + + var phoneNumbers = changeEntity.PhoneNumbers; + Assert.True(((JToken)phoneNumbers).Count() == 2, "The changed customer should have 2 phone numbers"); + Assert.True(phoneNumbers[0].Value == "123-4567", "The first phone number should be '123-4567'"); + Assert.True(phoneNumbers[1].Value == "765-4321", "The second phone number should be '765-4321'"); + + var newCustomer = results.value[1]; + Assert.True(((JToken)newCustomer).Count() == 5, "The new customer should have 3 properties written, But now it contains 2 non-changed properties, it's regression bug?"); + Assert.True(newCustomer.Id.Value == 10, "The ID of the new customer should be 10"); + Assert.True(newCustomer.Name.Value == "NewCustomer", "The name of the new customer should be 'NewCustomer'"); + + var places = newCustomer.FavoritePlaces; + Assert.True(((JToken)places).Count() == 2, "The new customer should have 2 favorite places"); + + var place1 = places[0]; + Assert.True(((JToken)place1).Count() == 3, "The first favorite place should have 2 properties written.But now it contains non-changed properties, it's regression bug?"); + Assert.True(place1.State.Value == "State", "The first favorite place's state should be 'State'."); + Assert.True(place1.ZipCode.Value == (int?)null, "The first favorite place's Address.ZipCode should be null."); + + var place2 = places[1]; + Assert.True(((JToken)place2).Count() == 5, "The second favorite place should have 5 properties written."); + Assert.True(place2.City.Value == "City2", "The second favorite place's Address.City should be 'City2'."); + Assert.True(place2.State.Value == "State2", "The second favorite place's Address.State should be 'State2'."); + Assert.True(place2.ZipCode.Value == 12345, "The second favorite place's Address.ZipCode should be 12345."); + Assert.True(place2.OpenProperty.Value == 10, "The second favorite place's Address.OpenProperty should be 10."); + Assert.True(place2.NullOpenProperty.Value == null, "The second favorite place's Address.NullOpenProperty should be null."); + + var newOrder = results.value[2]; + Assert.True(((JToken)newOrder).Count() == 4, "The new order should have 2 properties plus context written, , But now it contains one non-changed properties, it's regression bug?"); + string newOrderContext = newOrder["@context"].Value as string; + Assert.True(newOrderContext != null, "The new order should have a context written"); + Assert.True(newOrderContext.Contains("$metadata#TestOrders"), "The new order should come from the TestOrders entity set"); + Assert.True(newOrder.Id.Value == 27, "The ID of the new order should be 27"); + Assert.True(newOrder.Amount.Value == 100, "The amount of the new order should be 100"); + + var deletedEntity = results.value[3]; + Assert.True(deletedEntity["@id"].Value == "7", "The ID of the deleted customer should be 7"); + Assert.True(deletedEntity["@removed"].reason.Value == "changed", "The reason for the deleted customer should be 'changed'"); + + var deletedOrder = results.value[4]; + string deletedOrderContext = deletedOrder["@context"].Value as string; + Assert.True(deletedOrderContext != null, "The deleted order should have a context written"); + Assert.True(deletedOrderContext.Contains("$metadata#TestOrders"), "The deleted order should come from the TestOrders entity set"); + Assert.True(deletedOrder["@id"].Value == "12", "The ID of the deleted order should be 12"); + Assert.True(deletedOrder["@removed"].reason.Value == "deleted", "The reason for the deleted order should be 'deleted'"); + + var deletedLink = results.value[5]; + Assert.True(deletedLink.source.Value == "http://localhost/odata/TestCustomers(1)", "The source of the deleted link should be 'http://localhost/odata/TestCustomers(1)'"); + Assert.True(deletedLink.target.Value == "http://localhost/odata/TestOrders(12)", "The target of the deleted link should be 'http://localhost/odata/TestOrders(12)'"); + Assert.True(deletedLink.relationship.Value == "Orders", "The relationship of the deleted link should be 'Orders'"); + + var addedLink = results.value[6]; + Assert.True(addedLink.source.Value == "http://localhost/odata/TestCustomers(10)", "The source of the added link should be 'http://localhost/odata/TestCustomers(10)'"); + Assert.True(addedLink.target.Value == "http://localhost/odata/TestOrders(27)", "The target of the added link should be 'http://localhost/odata/TestOrders(27)'"); + Assert.True(addedLink.relationship.Value == "Orders", "The relationship of the added link should be 'Orders'"); + } + + [Fact] + public async Task DeltaVerifyReslt_ContainsDynamicComplexProperties() { - public DeltaTokenQueryTests(WebApiTestFixture fixture) - : base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(TestCustomersController), typeof(TestOrdersController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel()).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); - } - - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("TestCustomers"); - builder.EntitySet("TestOrders"); - return builder.GetEdmModel(); - } - - [Fact] - public async Task DeltaVerifyReslt() - { - HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, "odata/TestCustomers?$deltaToken=abc"); - get.Headers.Add("Accept", "application/json;odata.metadata=minimal"); - get.Headers.Add("OData-Version", "4.01"); - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.SendAsync(get); - Assert.True(response.IsSuccessStatusCode); - dynamic results = await response.Content.ReadAsObject(); - - Assert.True(results.value.Count == 7, "There should be 7 entries in the response"); - - var changeEntity = results.value[0]; - Assert.True(((JToken)changeEntity).Count() == 9, "The changed customer should have 6 properties plus type written. But now it contains non-changed properties, it's regression bug?"); - string changeEntityType = changeEntity["@type"].Value as string; - Assert.True(changeEntityType != null, "The changed customer should have type written"); - Assert.True(changeEntityType.Contains("#Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestCustomerWithAddress"), "The changed order should be a TestCustomerWithAddress"); - Assert.True(changeEntity.Id.Value == 1, "The ID Of changed customer should be 1."); - Assert.True(changeEntity.OpenProperty.Value == 10, "The OpenProperty property of changed customer should be 10."); - Assert.True(changeEntity.NullOpenProperty.Value == null, "The NullOpenProperty property of changed customer should be null."); - Assert.True(changeEntity.Name.Value == "Name", "The Name of changed customer should be 'Name'"); - Assert.True(((JToken)changeEntity.Address).Count() == 3, "The changed entity's Address should have 2 properties written. But now it contains non-changed properties, it's regression bug?"); - Assert.True(changeEntity.Address.State.Value == "State", "The changed customer's Address.State should be 'State'."); - Assert.True(changeEntity.Address.ZipCode.Value == (int?)null, "The changed customer's Address.ZipCode should be null."); - - var phoneNumbers = changeEntity.PhoneNumbers; - Assert.True(((JToken)phoneNumbers).Count() == 2, "The changed customer should have 2 phone numbers"); - Assert.True(phoneNumbers[0].Value == "123-4567", "The first phone number should be '123-4567'"); - Assert.True(phoneNumbers[1].Value == "765-4321", "The second phone number should be '765-4321'"); - - var newCustomer = results.value[1]; - Assert.True(((JToken)newCustomer).Count() == 5, "The new customer should have 3 properties written, But now it contains 2 non-changed properties, it's regression bug?"); - Assert.True(newCustomer.Id.Value == 10, "The ID of the new customer should be 10"); - Assert.True(newCustomer.Name.Value == "NewCustomer", "The name of the new customer should be 'NewCustomer'"); - - var places = newCustomer.FavoritePlaces; - Assert.True(((JToken)places).Count() == 2, "The new customer should have 2 favorite places"); - - var place1 = places[0]; - Assert.True(((JToken)place1).Count() == 3, "The first favorite place should have 2 properties written.But now it contains non-changed properties, it's regression bug?"); - Assert.True(place1.State.Value == "State", "The first favorite place's state should be 'State'."); - Assert.True(place1.ZipCode.Value == (int?)null, "The first favorite place's Address.ZipCode should be null."); - - var place2 = places[1]; - Assert.True(((JToken)place2).Count() == 5, "The second favorite place should have 5 properties written."); - Assert.True(place2.City.Value == "City2", "The second favorite place's Address.City should be 'City2'."); - Assert.True(place2.State.Value == "State2", "The second favorite place's Address.State should be 'State2'."); - Assert.True(place2.ZipCode.Value == 12345, "The second favorite place's Address.ZipCode should be 12345."); - Assert.True(place2.OpenProperty.Value == 10, "The second favorite place's Address.OpenProperty should be 10."); - Assert.True(place2.NullOpenProperty.Value == null, "The second favorite place's Address.NullOpenProperty should be null."); - - var newOrder = results.value[2]; - Assert.True(((JToken)newOrder).Count() == 4, "The new order should have 2 properties plus context written, , But now it contains one non-changed properties, it's regression bug?"); - string newOrderContext = newOrder["@context"].Value as string; - Assert.True(newOrderContext != null, "The new order should have a context written"); - Assert.True(newOrderContext.Contains("$metadata#TestOrders"), "The new order should come from the TestOrders entity set"); - Assert.True(newOrder.Id.Value == 27, "The ID of the new order should be 27"); - Assert.True(newOrder.Amount.Value == 100, "The amount of the new order should be 100"); - - var deletedEntity = results.value[3]; - Assert.True(deletedEntity["@id"].Value == "7", "The ID of the deleted customer should be 7"); - Assert.True(deletedEntity["@removed"].reason.Value == "changed", "The reason for the deleted customer should be 'changed'"); - - var deletedOrder = results.value[4]; - string deletedOrderContext = deletedOrder["@context"].Value as string; - Assert.True(deletedOrderContext != null, "The deleted order should have a context written"); - Assert.True(deletedOrderContext.Contains("$metadata#TestOrders"), "The deleted order should come from the TestOrders entity set"); - Assert.True(deletedOrder["@id"].Value == "12", "The ID of the deleted order should be 12"); - Assert.True(deletedOrder["@removed"].reason.Value == "deleted", "The reason for the deleted order should be 'deleted'"); - - var deletedLink = results.value[5]; - Assert.True(deletedLink.source.Value == "http://localhost/odata/TestCustomers(1)", "The source of the deleted link should be 'http://localhost/odata/TestCustomers(1)'"); - Assert.True(deletedLink.target.Value == "http://localhost/odata/TestOrders(12)", "The target of the deleted link should be 'http://localhost/odata/TestOrders(12)'"); - Assert.True(deletedLink.relationship.Value == "Orders", "The relationship of the deleted link should be 'Orders'"); - - var addedLink = results.value[6]; - Assert.True(addedLink.source.Value == "http://localhost/odata/TestCustomers(10)", "The source of the added link should be 'http://localhost/odata/TestCustomers(10)'"); - Assert.True(addedLink.target.Value == "http://localhost/odata/TestOrders(27)", "The target of the added link should be 'http://localhost/odata/TestOrders(27)'"); - Assert.True(addedLink.relationship.Value == "Orders", "The relationship of the added link should be 'Orders'"); - } - - [Fact] - public async Task DeltaVerifyReslt_ContainsDynamicComplexProperties() - { - HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, "odata/TestOrders?$deltaToken=abc"); - get.Headers.Add("Accept", "application/json;odata.metadata=minimal"); - get.Headers.Add("OData-Version", "4.01"); - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.SendAsync(get); - Assert.True(response.IsSuccessStatusCode); - - string result = await response.Content.ReadAsStringAsync(); - Assert.Equal("{\"@context\":\"http://localhost/odata/$metadata#TestOrders/$delta\"," + - "\"value\":[" + - "{" + - "\"Id\":1," + - "\"Amount\":42," + - "\"Location\":{" + - "\"State\":\"State\"," + - "\"City\":null," + - "\"ZipCode\":null," + - "\"OpenProperty\":10," + - "\"key-samplelist\":{" + - "\"@type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestAddress\"," + - "\"State\":\"sample state\"," + - "\"City\":null," + - "\"ZipCode\":9," + - "\"title\":\"sample title\"" + - "}" + - "}" + + HttpRequestMessage get = new HttpRequestMessage(HttpMethod.Get, "odata/TestOrders?$deltaToken=abc"); + get.Headers.Add("Accept", "application/json;odata.metadata=minimal"); + get.Headers.Add("OData-Version", "4.01"); + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.SendAsync(get); + Assert.True(response.IsSuccessStatusCode); + + string result = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"@context\":\"http://localhost/odata/$metadata#TestOrders/$delta\"," + + "\"value\":[" + + "{" + + "\"Id\":1," + + "\"Amount\":42," + + "\"Location\":{" + + "\"State\":\"State\"," + + "\"City\":null," + + "\"ZipCode\":null," + + "\"OpenProperty\":10," + + "\"key-samplelist\":{" + + "\"@type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.DeltaToken.TestAddress\"," + + "\"State\":\"sample state\"," + + "\"City\":null," + + "\"ZipCode\":9," + + "\"title\":\"sample title\"" + "}" + - "]" + - "}", - result); - } + "}" + + "}" + + "]" + + "}", + result); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DerivedTypes/DerivedTypesControllers.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DerivedTypes/DerivedTypesControllers.cs index dbeba1008..dbc8dbdfd 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DerivedTypes/DerivedTypesControllers.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DerivedTypes/DerivedTypesControllers.cs @@ -12,107 +12,106 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes +namespace Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes; + +public class CustomersController : ODataController { - public class CustomersController : ODataController - { - static List Customers { get; set; } + static List Customers { get; set; } - static CustomersController() + static CustomersController() + { + Customers = new List { - Customers = new List - { - new Customer { - Id = 1, - Name = "Customer 1", - Orders = new List - { - new Order { Id = 1, Amount = 100M } - } - }, - new VipCustomer { - Id = 2, - Name = "Customer 2", - LoyaltyCardNo = "9876543210", - Orders = new List - { - new Order { Id = 2, Amount = 230M }, - new Order { Id = 3, Amount = 150M } - } - }, - new Customer { - Id = 3, - Name = "Customer 3", - Orders = new List - { - new Order { Id = 4, Amount = 170M } - } - }, - new EnterpriseCustomer + new Customer { + Id = 1, + Name = "Customer 1", + Orders = new List { - Id = 4, - Name = "Customer 4", - Orders = new List - { - new Order { Id = 5, Amount = 190M} - }, - RelationshipManager = new Employee { Id = 1, Name = "Employee 1" } + new Order { Id = 1, Amount = 100M } } - }; - } - - [EnableQuery] - public IActionResult Get() - { - return Ok(Customers); - } - - [EnableQuery] - [HttpGet("odata/Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer({key})")] // convention routing doesn't create this template. - public IActionResult Get(int key) - { - var customer = Customers.FirstOrDefault(c => c.Id == key); - - if (customer == null) + }, + new VipCustomer { + Id = 2, + Name = "Customer 2", + LoyaltyCardNo = "9876543210", + Orders = new List + { + new Order { Id = 2, Amount = 230M }, + new Order { Id = 3, Amount = 150M } + } + }, + new Customer { + Id = 3, + Name = "Customer 3", + Orders = new List + { + new Order { Id = 4, Amount = 170M } + } + }, + new EnterpriseCustomer { - return NotFound(); + Id = 4, + Name = "Customer 4", + Orders = new List + { + new Order { Id = 5, Amount = 190M} + }, + RelationshipManager = new Employee { Id = 1, Name = "Employee 1" } } + }; + } - return Ok(customer); - } + [EnableQuery] + public IActionResult Get() + { + return Ok(Customers); + } - // Handles /entityset/cast path template - [EnableQuery] - public IActionResult GetFromVipCustomer() + [EnableQuery] + [HttpGet("odata/Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer({key})")] // convention routing doesn't create this template. + public IActionResult Get(int key) + { + var customer = Customers.FirstOrDefault(c => c.Id == key); + + if (customer == null) { - return Ok(Customers.OfType()); + return NotFound(); } - // Handles /entityset/key/cast and /entityset/cast/key path templates - [EnableQuery] - public IActionResult GetVipCustomer([FromODataUri] int key) - { - var vipCustomer = Customers.OfType().SingleOrDefault(d => d.Id.Equals(key)); + return Ok(customer); + } - if (vipCustomer == null) - { - return NotFound(); - } + // Handles /entityset/cast path template + [EnableQuery] + public IActionResult GetFromVipCustomer() + { + return Ok(Customers.OfType()); + } - return Ok(vipCustomer); - } + // Handles /entityset/key/cast and /entityset/cast/key path templates + [EnableQuery] + public IActionResult GetVipCustomer([FromODataUri] int key) + { + var vipCustomer = Customers.OfType().SingleOrDefault(d => d.Id.Equals(key)); - [EnableQuery] - public IActionResult GetEnterpriseCustomer([FromRoute] int key) + if (vipCustomer == null) { - var enterpriseCustomer = Customers.OfType().SingleOrDefault(d => d.Id.Equals(key)); + return NotFound(); + } - if (enterpriseCustomer == null) - { - return NotFound(); - } + return Ok(vipCustomer); + } + + [EnableQuery] + public IActionResult GetEnterpriseCustomer([FromRoute] int key) + { + var enterpriseCustomer = Customers.OfType().SingleOrDefault(d => d.Id.Equals(key)); - return Ok(enterpriseCustomer); + if (enterpriseCustomer == null) + { + return NotFound(); } + + return Ok(enterpriseCustomer); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DerivedTypes/DerivedTypesDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DerivedTypes/DerivedTypesDataModel.cs index a9155ff1c..44a6bd28f 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DerivedTypes/DerivedTypesDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DerivedTypes/DerivedTypesDataModel.cs @@ -7,34 +7,33 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes +namespace Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes; + +public class Customer { - public class Customer - { - public int Id { get; set; } - public string Name { get; set; } - public List Orders { get; set; } - } + public int Id { get; set; } + public string Name { get; set; } + public List Orders { get; set; } +} - public class VipCustomer : Customer - { - public string LoyaltyCardNo { get; set; } - } +public class VipCustomer : Customer +{ + public string LoyaltyCardNo { get; set; } +} - public class Order - { - public int Id { get; set; } - public decimal Amount { get; set; } - } +public class Order +{ + public int Id { get; set; } + public decimal Amount { get; set; } +} - public class EnterpriseCustomer : Customer - { - public Employee RelationshipManager { get; set; } - } +public class EnterpriseCustomer : Customer +{ + public Employee RelationshipManager { get; set; } +} - public class Employee - { - public int Id { get; set; } - public string Name { get; set; } - } +public class Employee +{ + public int Id { get; set; } + public string Name { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DerivedTypes/DerivedTypesTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DerivedTypes/DerivedTypesTests.cs index 89d827d20..0af05bc37 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DerivedTypes/DerivedTypesTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DerivedTypes/DerivedTypesTests.cs @@ -17,243 +17,242 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes +namespace Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes; + +public class DerivedTypeTests : WebApiTestBase { - public class DerivedTypeTests : WebApiTestBase + public DerivedTypeTests(WebApiTestFixture fixture) + : base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(CustomersController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel()).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); + } + + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Employees"); + builder.EntityType(); + builder.EntityType().DerivesFrom(); + builder.EntityType().DerivesFrom(); + return builder.GetEdmModel(); + } + + [Fact] + public async Task RestrictEntitySetToDerivedTypeInstances() + { + // Arrange + string requestUri = "/odata/Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + string py = await response.Content.ReadAsStringAsync(); + // Assert + Assert.True(response.IsSuccessStatusCode); + + string expectedContent = "\"value\":[{\"Id\":2,\"Name\":\"Customer 2\",\"LoyaltyCardNo\":\"9876543210\"}]"; + Assert.Contains(expectedContent, await response.Content.ReadAsStringAsync()); + } + + [Theory] + [InlineData("Customers(2)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer")] + [InlineData("Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer(2)")] // So far, we don't support key after the type cast. + public async Task RestrictEntityToDerivedTypeInstance(string path) { - public DerivedTypeTests(WebApiTestFixture fixture) - : base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(CustomersController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel()).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); - } - - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Employees"); - builder.EntityType(); - builder.EntityType().DerivesFrom(); - builder.EntityType().DerivesFrom(); - return builder.GetEdmModel(); - } - - [Fact] - public async Task RestrictEntitySetToDerivedTypeInstances() - { - // Arrange - string requestUri = "/odata/Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - string py = await response.Content.ReadAsStringAsync(); - // Assert - Assert.True(response.IsSuccessStatusCode); - - string expectedContent = "\"value\":[{\"Id\":2,\"Name\":\"Customer 2\",\"LoyaltyCardNo\":\"9876543210\"}]"; - Assert.Contains(expectedContent, await response.Content.ReadAsStringAsync()); - } - - [Theory] - [InlineData("Customers(2)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer")] - [InlineData("Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer(2)")] // So far, we don't support key after the type cast. - public async Task RestrictEntityToDerivedTypeInstance(string path) - { - // Arrange: Key preceeds name of the derived type - string requestUri = $"odata/{path}"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - - string expectedContent = "\"Id\":2,\"Name\":\"Customer 2\",\"LoyaltyCardNo\":\"9876543210\""; - Assert.Contains(expectedContent, await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task ReturnNotFound_ForKeyNotAssociatedWithDerivedType() - { - // Arrange: Customer with Id 1 is not a VipCustomer - string requestUri = "/odata/Customers(1)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.False(response.IsSuccessStatusCode); - Assert.Equal(System.Net.HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - public async Task RestrictEntitySetToDerivedTypeInstances_ThenExpandNavProperty() - { - // Arrange - string requestUri = "/odata/Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer?$expand=Orders"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - - string expectedContent = "\"value\":[{\"Id\":2,\"Name\":\"Customer 2\",\"LoyaltyCardNo\":\"9876543210\"," + - "\"Orders\":[{\"Id\":2,\"Amount\":230},{\"Id\":3,\"Amount\":150}]}]"; - Assert.Contains(expectedContent, await response.Content.ReadAsStringAsync()); - } - - [Theory] - [InlineData("Customers(2)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer?$expand=Orders")] - [InlineData("Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer(2)?$expand=Orders")] - public async Task RestrictEntityToDerivedTypeInstance_ThenExpandNavProperty(string pathAndQuery) - { - // Arrange: Key preceeds name of the derived type - string requestUri = $"/odata/{pathAndQuery}"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - - string expectedContent = "\"Id\":2,\"Name\":\"Customer 2\",\"LoyaltyCardNo\":\"9876543210\"," + - "\"Orders\":[{\"Id\":2,\"Amount\":230},{\"Id\":3,\"Amount\":150}]"; - Assert.Contains(expectedContent, await response.Content.ReadAsStringAsync()); - } - - [Theory] - [InlineData("Customers(4)")] - [InlineData("Customers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer")] - public async Task RequestFullMetadataForDerivedTypeInstance(string odataPath) - { - // Arrange - var requestUri = $"/odata/{odataPath}"; - - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - var client = CreateClient(); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - var enterpriseCustomer = await response.Content.ReadAsObject(); - - Assert.Equal(10, enterpriseCustomer.Count); - - var odataContext = enterpriseCustomer.Value("@odata.context"); - var odataType = enterpriseCustomer.Value("@odata.type"); - var odataId = enterpriseCustomer.Value("@odata.id"); - var odataEditLink = enterpriseCustomer.Value("@odata.editLink"); - var id = enterpriseCustomer.Value("Id"); - var name = enterpriseCustomer.Value("Name"); - var relationshipManagerAssociationLink = enterpriseCustomer.Value("RelationshipManager@odata.associationLink"); - var relationshipManagerNavigationLink = enterpriseCustomer.Value("RelationshipManager@odata.navigationLink"); - - Assert.NotNull(odataContext); - Assert.NotNull(odataType); - Assert.NotNull(odataId); - Assert.NotNull(odataEditLink); - Assert.NotNull(id); - Assert.NotNull(name); - Assert.NotNull(relationshipManagerAssociationLink); - Assert.NotNull(relationshipManagerNavigationLink); - - Assert.EndsWith("$metadata#Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer/$entity", odataContext); - Assert.Equal("#Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer", odataType); - Assert.EndsWith("Customers(4)", odataId); - Assert.EndsWith("Customers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer", odataEditLink); - Assert.Equal(4, id); - Assert.Equal("Customer 4", name); - Assert.EndsWith("Customers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer/RelationshipManager/$ref", relationshipManagerAssociationLink); - Assert.EndsWith("Customers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer/RelationshipManager", relationshipManagerNavigationLink); - } - - [Theory] - [InlineData("Customers(4)", 4)] - [InlineData("Customers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer", 3)] - public async Task RequestMinimalMetadataForDerivedTypeInstance(string odataPath, int propertyCount) - { - // Arrange - var requestUri = $"/odata/{odataPath}"; - - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); - var client = CreateClient(); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - var enterpriseCustomer = await response.Content.ReadAsObject(); - - Assert.Equal(propertyCount, enterpriseCustomer.Count); - - var odataContext = enterpriseCustomer.Value("@odata.context"); - var id = enterpriseCustomer.Value("Id"); - var name = enterpriseCustomer.Value("Name"); - - Assert.NotNull(odataContext); - Assert.NotNull(id); - Assert.NotNull(name); - - Assert.EndsWith("$metadata#Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer/$entity", odataContext); - Assert.Equal(4, id); - Assert.Equal("Customer 4", name); - } - - [Theory] - [InlineData("Customers(4)")] - [InlineData("Customers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer")] - public async Task RequestNoMetadataForDerivedTypeInstance(string odataPath) - { - // Arrange - var requestUri = $"/odata/{odataPath}"; - - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - var client = CreateClient(); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - var enterpriseCustomer = await response.Content.ReadAsObject(); - - Assert.Equal(2, enterpriseCustomer.Count); - - var id = enterpriseCustomer.Value("Id"); - var name = enterpriseCustomer.Value("Name"); - - Assert.NotNull(id); - Assert.NotNull(name); - - Assert.Equal(4, id); - Assert.Equal("Customer 4", name); - } + // Arrange: Key preceeds name of the derived type + string requestUri = $"odata/{path}"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + + string expectedContent = "\"Id\":2,\"Name\":\"Customer 2\",\"LoyaltyCardNo\":\"9876543210\""; + Assert.Contains(expectedContent, await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task ReturnNotFound_ForKeyNotAssociatedWithDerivedType() + { + // Arrange: Customer with Id 1 is not a VipCustomer + string requestUri = "/odata/Customers(1)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.False(response.IsSuccessStatusCode); + Assert.Equal(System.Net.HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task RestrictEntitySetToDerivedTypeInstances_ThenExpandNavProperty() + { + // Arrange + string requestUri = "/odata/Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer?$expand=Orders"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + + string expectedContent = "\"value\":[{\"Id\":2,\"Name\":\"Customer 2\",\"LoyaltyCardNo\":\"9876543210\"," + + "\"Orders\":[{\"Id\":2,\"Amount\":230},{\"Id\":3,\"Amount\":150}]}]"; + Assert.Contains(expectedContent, await response.Content.ReadAsStringAsync()); + } + + [Theory] + [InlineData("Customers(2)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer?$expand=Orders")] + [InlineData("Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.VipCustomer(2)?$expand=Orders")] + public async Task RestrictEntityToDerivedTypeInstance_ThenExpandNavProperty(string pathAndQuery) + { + // Arrange: Key preceeds name of the derived type + string requestUri = $"/odata/{pathAndQuery}"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + + string expectedContent = "\"Id\":2,\"Name\":\"Customer 2\",\"LoyaltyCardNo\":\"9876543210\"," + + "\"Orders\":[{\"Id\":2,\"Amount\":230},{\"Id\":3,\"Amount\":150}]"; + Assert.Contains(expectedContent, await response.Content.ReadAsStringAsync()); + } + + [Theory] + [InlineData("Customers(4)")] + [InlineData("Customers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer")] + public async Task RequestFullMetadataForDerivedTypeInstance(string odataPath) + { + // Arrange + var requestUri = $"/odata/{odataPath}"; + + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var enterpriseCustomer = await response.Content.ReadAsObject(); + + Assert.Equal(10, enterpriseCustomer.Count); + + var odataContext = enterpriseCustomer.Value("@odata.context"); + var odataType = enterpriseCustomer.Value("@odata.type"); + var odataId = enterpriseCustomer.Value("@odata.id"); + var odataEditLink = enterpriseCustomer.Value("@odata.editLink"); + var id = enterpriseCustomer.Value("Id"); + var name = enterpriseCustomer.Value("Name"); + var relationshipManagerAssociationLink = enterpriseCustomer.Value("RelationshipManager@odata.associationLink"); + var relationshipManagerNavigationLink = enterpriseCustomer.Value("RelationshipManager@odata.navigationLink"); + + Assert.NotNull(odataContext); + Assert.NotNull(odataType); + Assert.NotNull(odataId); + Assert.NotNull(odataEditLink); + Assert.NotNull(id); + Assert.NotNull(name); + Assert.NotNull(relationshipManagerAssociationLink); + Assert.NotNull(relationshipManagerNavigationLink); + + Assert.EndsWith("$metadata#Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer/$entity", odataContext); + Assert.Equal("#Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer", odataType); + Assert.EndsWith("Customers(4)", odataId); + Assert.EndsWith("Customers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer", odataEditLink); + Assert.Equal(4, id); + Assert.Equal("Customer 4", name); + Assert.EndsWith("Customers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer/RelationshipManager/$ref", relationshipManagerAssociationLink); + Assert.EndsWith("Customers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer/RelationshipManager", relationshipManagerNavigationLink); + } + + [Theory] + [InlineData("Customers(4)", 4)] + [InlineData("Customers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer", 3)] + public async Task RequestMinimalMetadataForDerivedTypeInstance(string odataPath, int propertyCount) + { + // Arrange + var requestUri = $"/odata/{odataPath}"; + + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var enterpriseCustomer = await response.Content.ReadAsObject(); + + Assert.Equal(propertyCount, enterpriseCustomer.Count); + + var odataContext = enterpriseCustomer.Value("@odata.context"); + var id = enterpriseCustomer.Value("Id"); + var name = enterpriseCustomer.Value("Name"); + + Assert.NotNull(odataContext); + Assert.NotNull(id); + Assert.NotNull(name); + + Assert.EndsWith("$metadata#Customers/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer/$entity", odataContext); + Assert.Equal(4, id); + Assert.Equal("Customer 4", name); + } + + [Theory] + [InlineData("Customers(4)")] + [InlineData("Customers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DerivedTypes.EnterpriseCustomer")] + public async Task RequestNoMetadataForDerivedTypeInstance(string odataPath) + { + // Arrange + var requestUri = $"/odata/{odataPath}"; + + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var enterpriseCustomer = await response.Content.ReadAsObject(); + + Assert.Equal(2, enterpriseCustomer.Count); + + var id = enterpriseCustomer.Value("Id"); + var name = enterpriseCustomer.Value("Name"); + + Assert.NotNull(id); + Assert.NotNull(name); + + Assert.Equal(4, id); + Assert.Equal("Customer 4", name); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeController.cs index af56b6171..8e02a4072 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeController.cs @@ -10,45 +10,44 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarCompute +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarCompute; + +public class CustomersController : ODataController { - public class CustomersController : ODataController + [EnableQuery] + public IActionResult Get() { - [EnableQuery] - public IActionResult Get() - { - return Ok(DollarComputeDataSource.Customers); - } + return Ok(DollarComputeDataSource.Customers); + } - [EnableQuery] - public IActionResult Get(int key) + [EnableQuery] + public IActionResult Get(int key) + { + ComputeCustomer c = DollarComputeDataSource.Customers.FirstOrDefault(c => c.Id == key); + if (c == null) { - ComputeCustomer c = DollarComputeDataSource.Customers.FirstOrDefault(c => c.Id == key); - if (c == null) - { - return NotFound($"Cannot find customer with key = {key}"); - } - - return Ok(c); + return NotFound($"Cannot find customer with key = {key}"); } - [EnableQuery] - public IActionResult GetLocation(int key) - { - ComputeCustomer c = DollarComputeDataSource.Customers.FirstOrDefault(c => c.Id == key); - if (c == null) - { - return NotFound($"Cannot find customer with key = {key}"); - } - - return Ok(c.Location); - } + return Ok(c); + } - [HttpGet("odata/sales")] - [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.None)] - public IActionResult GetSales() + [EnableQuery] + public IActionResult GetLocation(int key) + { + ComputeCustomer c = DollarComputeDataSource.Customers.FirstOrDefault(c => c.Id == key); + if (c == null) { - return Ok(); + return NotFound($"Cannot find customer with key = {key}"); } + + return Ok(c.Location); + } + + [HttpGet("odata/sales")] + [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.None)] + public IActionResult GetSales() + { + return Ok(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeDataModel.cs index f2f0e81e3..49f396732 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeDataModel.cs @@ -7,46 +7,45 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarCompute +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarCompute; + +public class ComputeCustomer { - public class ComputeCustomer - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public int Age { get; set; } + public int Age { get; set; } - public double Price { get; set; } + public double Price { get; set; } - public int Qty { get; set; } + public int Qty { get; set; } - public IList Candys { get; set; } + public IList Candys { get; set; } - public ComputeAddress Location { get; set; } + public ComputeAddress Location { get; set; } - public IList Sales { get; set; } + public IList Sales { get; set; } - public IDictionary Dynamics { get; set; } = new Dictionary(); - } + public IDictionary Dynamics { get; set; } = new Dictionary(); +} - public class ComputeAddress - { - public string Street { get; set; } +public class ComputeAddress +{ + public string Street { get; set; } - public int ZipCode { get; set; } - } + public int ZipCode { get; set; } +} - public class ComputeSale - { - public int Id { get; set; } +public class ComputeSale +{ + public int Id { get; set; } - public int Amount { get; set; } + public int Amount { get; set; } - public double TaxRate { get; set; } + public double TaxRate { get; set; } - public double Price { get; set; } + public double Price { get; set; } - public IDictionary Dynamics { get; set; } = new Dictionary(); - } + public IDictionary Dynamics { get; set; } = new Dictionary(); } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeDataSource.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeDataSource.cs index 72d2ed56e..aea5ab4e1 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeDataSource.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeDataSource.cs @@ -8,42 +8,41 @@ using System.Collections.Generic; using System.Linq; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarCompute +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarCompute; + +public class DollarComputeDataSource { - public class DollarComputeDataSource + private static IList _customers; + + static DollarComputeDataSource() { - private static IList _customers; + GenerateCustomers(); + } - static DollarComputeDataSource() - { - GenerateCustomers(); - } + public static IList Customers => _customers; - public static IList Customers => _customers; + private static void GenerateCustomers() + { + _customers = new List + { + new ComputeCustomer { Id = 1, Name = "Peter", Age = 19, Price = 1.99, Qty = 10, Candys = new List(){ "kit kat"} }, + new ComputeCustomer { Id = 2, Name = "Sam", Age = 40, Price = 2.99, Qty = 15, Candys = new List(){ "BasicBerry"} }, + new ComputeCustomer { Id = 3, Name = "John", Age = 34, Price = 6.99, Qty = 4, Candys = new List(){ "Snickers", "Hershey"} }, + new ComputeCustomer { Id = 4, Name = "Kerry", Age = 29, Price = 3.99, Qty = 15, Candys = new List(){ "Snickers", "M&M"} }, + new ComputeCustomer { Id = 5, Name = "Alex", Age = 08, Price = 9.01, Qty = 20, Candys = new List(){ "M&M", "Twix"} }, + }; + int[] zipcode = { 98029, 32509, 98052, 88309, 12304 }; - private static void GenerateCustomers() + for (int i = 1; i <= 5; i++) { - _customers = new List + _customers[i - 1].Location = new ComputeAddress { - new ComputeCustomer { Id = 1, Name = "Peter", Age = 19, Price = 1.99, Qty = 10, Candys = new List(){ "kit kat"} }, - new ComputeCustomer { Id = 2, Name = "Sam", Age = 40, Price = 2.99, Qty = 15, Candys = new List(){ "BasicBerry"} }, - new ComputeCustomer { Id = 3, Name = "John", Age = 34, Price = 6.99, Qty = 4, Candys = new List(){ "Snickers", "Hershey"} }, - new ComputeCustomer { Id = 4, Name = "Kerry", Age = 29, Price = 3.99, Qty = 15, Candys = new List(){ "Snickers", "M&M"} }, - new ComputeCustomer { Id = 5, Name = "Alex", Age = 08, Price = 9.01, Qty = 20, Candys = new List(){ "M&M", "Twix"} }, + Street = "Street " + i, + ZipCode = zipcode[i - 1] }; - int[] zipcode = { 98029, 32509, 98052, 88309, 12304 }; - for (int i = 1; i <= 5; i++) - { - _customers[i - 1].Location = new ComputeAddress - { - Street = "Street " + i, - ZipCode = zipcode[i - 1] - }; - - _customers[i - 1].Sales = Enumerable.Range(0, i + 3) - .Select(idx => new ComputeSale { Id = 100 * i + idx, Amount = idx + i, Price = (3.1 + idx) * i, TaxRate = (0.1 + idx + i) / 10.0 }).ToList(); - } + _customers[i - 1].Sales = Enumerable.Range(0, i + 3) + .Select(idx => new ComputeSale { Id = 100 * i + idx, Amount = idx + i, Price = (3.1 + idx) * i, TaxRate = (0.1 + idx + i) / 10.0 }).ToList(); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeEdmModel.cs index 1b096563f..041b79147 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeEdmModel.cs @@ -8,17 +8,16 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarCompute +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarCompute; + +public class DollarComputeEdmModel { - public class DollarComputeEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Sales"); - IEdmModel model = builder.GetEdmModel(); - return model; - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Sales"); + IEdmModel model = builder.GetEdmModel(); + return model; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeTests.cs index 8aad9b49a..34571529b 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarCompute/DollarComputeTests.cs @@ -18,352 +18,351 @@ using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarCompute +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarCompute; + +public class DollarComputeTests : WebApiTestBase { - public class DollarComputeTests : WebApiTestBase + private readonly ITestOutputHelper output; + + public DollarComputeTests(WebApiTestFixture fixture, ITestOutputHelper output) + : base(fixture) { - private readonly ITestOutputHelper output; + this.output = output; + } - public DollarComputeTests(WebApiTestFixture fixture, ITestOutputHelper output) - : base(fixture) - { - this.output = output; - } + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = DollarComputeEdmModel.GetEdmModel(); - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = DollarComputeEdmModel.GetEdmModel(); + services.ConfigureControllers(typeof(CustomersController)); - services.ConfigureControllers(typeof(CustomersController)); + services.AddControllers().AddOData(opt => + opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select().AddRouteComponents("odata", edmModel)); + } - services.AddControllers().AddOData(opt => - opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select().AddRouteComponents("odata", edmModel)); - } + [Theory] + [InlineData("$filter=Total lt 30&$compute=Price mul Qty as Total", new [] { 1, 3 })] + [InlineData("$filter=Total gt 30&$compute=Price mul Qty as Total", new [] { 2, 4, 5})] + [InlineData("$filter=MainZipCode lt 90&$compute=Location/ZipCode div 1000 as MainZipCode", new[] { 2, 4, 5 })] + [InlineData("$filter=FirstChar eq 'S'&$compute=substring(Name, 0, 1) as FirstChar", new[] { 2 })] + public async Task QueryForResourceSet_IncludesDollarCompute_UsedInDollarFilter(string query, int[] ids) + { + // Arrange + string queryUrl = $"odata/Customers?{query}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; - [Theory] - [InlineData("$filter=Total lt 30&$compute=Price mul Qty as Total", new [] { 1, 3 })] - [InlineData("$filter=Total gt 30&$compute=Price mul Qty as Total", new [] { 2, 4, 5})] - [InlineData("$filter=MainZipCode lt 90&$compute=Location/ZipCode div 1000 as MainZipCode", new[] { 2, 4, 5 })] - [InlineData("$filter=FirstChar eq 'S'&$compute=substring(Name, 0, 1) as FirstChar", new[] { 2 })] - public async Task QueryForResourceSet_IncludesDollarCompute_UsedInDollarFilter(string query, int[] ids) - { - // Arrange - string queryUrl = $"odata/Customers?{query}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; + // Act + response = await client.SendAsync(request); - // Act - response = await client.SendAsync(request); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + JObject payloadBody = await response.Content.ReadAsObject(); - JObject payloadBody = await response.Content.ReadAsObject(); + int[] actualIds = GetIds(payloadBody); + Assert.True(ids.SequenceEqual(actualIds)); + } - int[] actualIds = GetIds(payloadBody); - Assert.True(ids.SequenceEqual(actualIds)); - } + [Theory] + [InlineData("$orderby=Total&$compute=Price mul Qty as Total", new[] { 1, 3, 2, 4, 5 })] + [InlineData("$orderby=Total desc&$compute=Price mul Qty as Total", new[] { 5, 4, 2, 3, 1 })] + [InlineData("$orderby=MainZipCode&$compute=Location/ZipCode div 1000 as MainZipCode", new[] { 5, 2, 4, 1, 3 })] + [InlineData("$orderby=FirstChar&$compute=substring(Name, 0, 1) as FirstChar", new[] { 5, 3, 4, 1, 2 })] + public async Task QueryForResourceSet_IncludesDollarCompute_UsedInDollarOrder(string query, int[] ids) + { + // Arrange + string queryUrl = $"odata/Customers?{query}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; - [Theory] - [InlineData("$orderby=Total&$compute=Price mul Qty as Total", new[] { 1, 3, 2, 4, 5 })] - [InlineData("$orderby=Total desc&$compute=Price mul Qty as Total", new[] { 5, 4, 2, 3, 1 })] - [InlineData("$orderby=MainZipCode&$compute=Location/ZipCode div 1000 as MainZipCode", new[] { 5, 2, 4, 1, 3 })] - [InlineData("$orderby=FirstChar&$compute=substring(Name, 0, 1) as FirstChar", new[] { 5, 3, 4, 1, 2 })] - public async Task QueryForResourceSet_IncludesDollarCompute_UsedInDollarOrder(string query, int[] ids) - { - // Arrange - string queryUrl = $"odata/Customers?{query}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; + // Act + response = await client.SendAsync(request); - // Act - response = await client.SendAsync(request); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + JObject payloadBody = await response.Content.ReadAsObject(); - JObject payloadBody = await response.Content.ReadAsObject(); + int[] actualIds = GetIds(payloadBody); + Assert.True(ids.SequenceEqual(actualIds)); + } - int[] actualIds = GetIds(payloadBody); - Assert.True(ids.SequenceEqual(actualIds)); - } + private static int[] GetIds(JObject payload) + { + JArray value = payload["value"] as JArray; + Assert.NotNull(value); - private static int[] GetIds(JObject payload) + int[] ids = new int[value.Count()]; + for (int i = 0; i < value.Count(); i++) { - JArray value = payload["value"] as JArray; - Assert.NotNull(value); - - int[] ids = new int[value.Count()]; - for (int i = 0; i < value.Count(); i++) - { - JObject item = value[i] as JObject; - ids[i] = (int)item["Id"]; - } - - return ids; + JObject item = value[i] as JObject; + ids[i] = (int)item["Id"]; } - [Fact] - public async Task QueryForResourceSet_IncludesDollarCompute_UsedInDollarSelect() - { - // Arrange - string queryUrl = "odata/Customers?$select=Name,Total,MainZipCode,FirstChar&$compute=Price mul Qty as Total,Location/ZipCode div 1000 as MainZipCode,substring(Name, 0, 1) as FirstChar"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - string payload = await response.Content.ReadAsStringAsync(); - Assert.Equal("{" + - "\"value\":[" + - "{\"Name\":\"Peter\",\"Total\":19.9,\"MainZipCode\":98,\"FirstChar\":\"P\"}," + - "{\"Name\":\"Sam\",\"Total\":44.85,\"MainZipCode\":32,\"FirstChar\":\"S\"}," + - "{\"Name\":\"John\",\"Total\":27.96,\"MainZipCode\":98,\"FirstChar\":\"J\"}," + - "{\"Name\":\"Kerry\",\"Total\":59.85,\"MainZipCode\":88,\"FirstChar\":\"K\"}," + - "{\"Name\":\"Alex\",\"Total\":180.2,\"MainZipCode\":12,\"FirstChar\":\"A\"}" + - "]" + - "}", payload); - } + return ids; + } - [Fact] - public async Task QueryForResource_IncludesDollarCompute_UsedSelectAllInDollarSelect() - { - // Arrange - string queryUrl = "odata/Customers(2)?$select=*&$compute=Age sub 20 as Age20Ago,toupper(Name) as UpperChar"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - string payload = await response.Content.ReadAsStringAsync(); - Assert.Equal("{\"Id\":2," + - "\"Name\":\"Sam\"," + - "\"Age\":40," + - "\"Price\":2.99," + - "\"Qty\":15," + - "\"Candys\":[\"BasicBerry\"]," + - "\"Age20Ago\":20," + - "\"UpperChar\":\"SAM\"," + - "\"Location\":{\"Street\":\"Street 2\",\"ZipCode\":32509}}", payload); - } + [Fact] + public async Task QueryForResourceSet_IncludesDollarCompute_UsedInDollarSelect() + { + // Arrange + string queryUrl = "odata/Customers?$select=Name,Total,MainZipCode,FirstChar&$compute=Price mul Qty as Total,Location/ZipCode div 1000 as MainZipCode,substring(Name, 0, 1) as FirstChar"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payload = await response.Content.ReadAsStringAsync(); + Assert.Equal("{" + + "\"value\":[" + + "{\"Name\":\"Peter\",\"Total\":19.9,\"MainZipCode\":98,\"FirstChar\":\"P\"}," + + "{\"Name\":\"Sam\",\"Total\":44.85,\"MainZipCode\":32,\"FirstChar\":\"S\"}," + + "{\"Name\":\"John\",\"Total\":27.96,\"MainZipCode\":98,\"FirstChar\":\"J\"}," + + "{\"Name\":\"Kerry\",\"Total\":59.85,\"MainZipCode\":88,\"FirstChar\":\"K\"}," + + "{\"Name\":\"Alex\",\"Total\":180.2,\"MainZipCode\":12,\"FirstChar\":\"A\"}" + + "]" + + "}", payload); + } - [Fact] - public async Task QueryForAnResource_IncludesDollarCompute_InNestedDollarSelect() - { - // Arrange - string queryUrl = "odata/Customers(4)?$select=Location($select=StateCode;$compute=ZipCode div 1000 as StateCode)"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; + [Fact] + public async Task QueryForResource_IncludesDollarCompute_UsedSelectAllInDollarSelect() + { + // Arrange + string queryUrl = "odata/Customers(2)?$select=*&$compute=Age sub 20 as Age20Ago,toupper(Name) as UpperChar"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payload = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"Id\":2," + + "\"Name\":\"Sam\"," + + "\"Age\":40," + + "\"Price\":2.99," + + "\"Qty\":15," + + "\"Candys\":[\"BasicBerry\"]," + + "\"Age20Ago\":20," + + "\"UpperChar\":\"SAM\"," + + "\"Location\":{\"Street\":\"Street 2\",\"ZipCode\":32509}}", payload); + } - // Act - response = await client.SendAsync(request); + [Fact] + public async Task QueryForAnResource_IncludesDollarCompute_InNestedDollarSelect() + { + // Arrange + string queryUrl = "odata/Customers(4)?$select=Location($select=StateCode;$compute=ZipCode div 1000 as StateCode)"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; - // Assert + // Act + response = await client.SendAsync(request); - Assert.NotNull(response); - string payload = await response.Content.ReadAsStringAsync(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + // Assert - Assert.Equal("{\"Location\":{\"StateCode\":88}}", payload); - } + Assert.NotNull(response); + string payload = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - [Fact] - public async Task QueryForAnResource_IncludesDollarCompute_InTopLevelAndNestedDollarSelect() - { - // Arrange - string queryUrl = "odata/Customers(4)?$select=Total,Location($select=StateCode;$compute=ZipCode div 1000 as StateCode)&$compute=Price mul Qty as Total"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - string payload = await response.Content.ReadAsStringAsync(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - Assert.Equal("{\"Total\":59.85,\"Location\":{\"StateCode\":88}}", payload); - } + Assert.Equal("{\"Location\":{\"StateCode\":88}}", payload); + } - [Fact] - public async Task QueryForAnResource_IncludesDollarCompute_InNestedDollarExpand_WithDollarSelectDollarFilter() - { - // Arrange - string queryUrl = "odata/Customers(3)?$expand=Sales($select=Amount,TaxRate,Tax;$filter=Tax lt 4.0;$compute=Amount mul TaxRate as Tax)"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - string payload = await response.Content.ReadAsStringAsync(); - - Assert.Equal("{\"Id\":3," + - "\"Name\":\"John\"," + - "\"Age\":34," + - "\"Price\":6.99," + - "\"Qty\":4," + - "\"Candys\":[\"Snickers\",\"Hershey\"]," + - "\"Location\":{\"Street\":\"Street 3\",\"ZipCode\":98052}," + - "\"Sales\":[" + - "{\"Amount\":3,\"TaxRate\":0.31,\"Tax\":0.9299999999999999}," + - "{\"Amount\":4,\"TaxRate\":0.41,\"Tax\":1.64}," + - "{\"Amount\":5,\"TaxRate\":0.51,\"Tax\":2.55}," + - "{\"Amount\":6,\"TaxRate\":0.61,\"Tax\":3.66}" + - "]" + - "}", payload); - } + [Fact] + public async Task QueryForAnResource_IncludesDollarCompute_InTopLevelAndNestedDollarSelect() + { + // Arrange + string queryUrl = "odata/Customers(4)?$select=Total,Location($select=StateCode;$compute=ZipCode div 1000 as StateCode)&$compute=Price mul Qty as Total"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + string payload = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + Assert.Equal("{\"Total\":59.85,\"Location\":{\"StateCode\":88}}", payload); + } - [Fact] - public async Task QueryForAnResource_IncludesDollarCompute_InNestedDollarExpand_WithDollarSelectDollarFilterDollarOrderBy() - { - // Arrange - string queryUrl = "odata/Customers(4)?$expand=Sales($select=Amount,TaxRate,Tax;$orderby=Tax;$filter=Tax gt 4.0;$compute=Amount mul TaxRate as Tax)"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - string payload = await response.Content.ReadAsStringAsync(); - - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - Assert.Equal("{\"Id\":4," + - "\"Name\":\"Kerry\"," + - "\"Age\":29," + - "\"Price\":3.99," + - "\"Qty\":15," + - "\"Candys\":[\"Snickers\",\"M&M\"]," + - "\"Location\":{\"Street\":\"Street 4\",\"ZipCode\":88309}," + - "\"Sales\":[" + - "{\"Amount\":7,\"TaxRate\":0.71,\"Tax\":4.97}," + - "{\"Amount\":8,\"TaxRate\":0.8099999999999999,\"Tax\":6.4799999999999995}," + - "{\"Amount\":9,\"TaxRate\":0.9099999999999999,\"Tax\":8.19}," + - "{\"Amount\":10,\"TaxRate\":1.01,\"Tax\":10.1}" + - "]" + - "}", payload); - } + [Fact] + public async Task QueryForAnResource_IncludesDollarCompute_InNestedDollarExpand_WithDollarSelectDollarFilter() + { + // Arrange + string queryUrl = "odata/Customers(3)?$expand=Sales($select=Amount,TaxRate,Tax;$filter=Tax lt 4.0;$compute=Amount mul TaxRate as Tax)"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + string payload = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"Id\":3," + + "\"Name\":\"John\"," + + "\"Age\":34," + + "\"Price\":6.99," + + "\"Qty\":4," + + "\"Candys\":[\"Snickers\",\"Hershey\"]," + + "\"Location\":{\"Street\":\"Street 3\",\"ZipCode\":98052}," + + "\"Sales\":[" + + "{\"Amount\":3,\"TaxRate\":0.31,\"Tax\":0.9299999999999999}," + + "{\"Amount\":4,\"TaxRate\":0.41,\"Tax\":1.64}," + + "{\"Amount\":5,\"TaxRate\":0.51,\"Tax\":2.55}," + + "{\"Amount\":6,\"TaxRate\":0.61,\"Tax\":3.66}" + + "]" + + "}", payload); + } - [Fact] - public async Task QueryForAnResource_IncludesDollarComputeWithAnyClause_WithDollarSelect() - { - // Arrange - string queryUrl = "odata/Customers?$select=Id,HasSnickers&$compute=Candys/any(r:r eq 'Snickers') as HasSnickers"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - string payload = await response.Content.ReadAsStringAsync(); - - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - Assert.Equal("{\"value\":[" + - "{\"Id\":1,\"HasSnickers\":false}," + - "{\"Id\":2,\"HasSnickers\":false}," + - "{\"Id\":3,\"HasSnickers\":true}," + - "{\"Id\":4,\"HasSnickers\":true}," + - "{\"Id\":5,\"HasSnickers\":false}" + - "]}", payload); - } + [Fact] + public async Task QueryForAnResource_IncludesDollarCompute_InNestedDollarExpand_WithDollarSelectDollarFilterDollarOrderBy() + { + // Arrange + string queryUrl = "odata/Customers(4)?$expand=Sales($select=Amount,TaxRate,Tax;$orderby=Tax;$filter=Tax gt 4.0;$compute=Amount mul TaxRate as Tax)"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + string payload = await response.Content.ReadAsStringAsync(); + + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + Assert.Equal("{\"Id\":4," + + "\"Name\":\"Kerry\"," + + "\"Age\":29," + + "\"Price\":3.99," + + "\"Qty\":15," + + "\"Candys\":[\"Snickers\",\"M&M\"]," + + "\"Location\":{\"Street\":\"Street 4\",\"ZipCode\":88309}," + + "\"Sales\":[" + + "{\"Amount\":7,\"TaxRate\":0.71,\"Tax\":4.97}," + + "{\"Amount\":8,\"TaxRate\":0.8099999999999999,\"Tax\":6.4799999999999995}," + + "{\"Amount\":9,\"TaxRate\":0.9099999999999999,\"Tax\":8.19}," + + "{\"Amount\":10,\"TaxRate\":1.01,\"Tax\":10.1}" + + "]" + + "}", payload); + } - [Fact] - public async Task QueryForAnResource_IncludesDollarComputeWithAllClause_WithDollarSelect() - { - // Arrange - string queryUrl = "odata/Customers?$select=Id,HasNoSnickers&$compute=Candys/all(r:r ne 'Snickers') as HasNoSnickers"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - string payload = await response.Content.ReadAsStringAsync(); - - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - Assert.Equal("{\"value\":[" + - "{\"Id\":1,\"HasNoSnickers\":true}," + - "{\"Id\":2,\"HasNoSnickers\":true}," + - "{\"Id\":3,\"HasNoSnickers\":false}," + - "{\"Id\":4,\"HasNoSnickers\":false}," + - "{\"Id\":5,\"HasNoSnickers\":true}" + - "]}", payload); - } + [Fact] + public async Task QueryForAnResource_IncludesDollarComputeWithAnyClause_WithDollarSelect() + { + // Arrange + string queryUrl = "odata/Customers?$select=Id,HasSnickers&$compute=Candys/any(r:r eq 'Snickers') as HasSnickers"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + string payload = await response.Content.ReadAsStringAsync(); + + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + Assert.Equal("{\"value\":[" + + "{\"Id\":1,\"HasSnickers\":false}," + + "{\"Id\":2,\"HasSnickers\":false}," + + "{\"Id\":3,\"HasSnickers\":true}," + + "{\"Id\":4,\"HasSnickers\":true}," + + "{\"Id\":5,\"HasSnickers\":false}" + + "]}", payload); + } - [Fact] - public async Task QuerySales_ThrowsNotAllowed_IncludesDollarCompute_WithAllowedQueryOptionsNone() - { - // Arrange - string queryUrl = "odata/sales?$compute=Amount mul TaxRate as Tax"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - HttpClient client = CreateClient(); - HttpResponseMessage response; + [Fact] + public async Task QueryForAnResource_IncludesDollarComputeWithAllClause_WithDollarSelect() + { + // Arrange + string queryUrl = "odata/Customers?$select=Id,HasNoSnickers&$compute=Candys/all(r:r ne 'Snickers') as HasNoSnickers"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + string payload = await response.Content.ReadAsStringAsync(); + + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + Assert.Equal("{\"value\":[" + + "{\"Id\":1,\"HasNoSnickers\":true}," + + "{\"Id\":2,\"HasNoSnickers\":true}," + + "{\"Id\":3,\"HasNoSnickers\":false}," + + "{\"Id\":4,\"HasNoSnickers\":false}," + + "{\"Id\":5,\"HasNoSnickers\":true}" + + "]}", payload); + } - // Act - response = await client.SendAsync(request); + [Fact] + public async Task QuerySales_ThrowsNotAllowed_IncludesDollarCompute_WithAllowedQueryOptionsNone() + { + // Arrange + string queryUrl = "odata/sales?$compute=Amount mul TaxRate as Tax"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + HttpClient client = CreateClient(); + HttpResponseMessage response; - // Assert - string payload = await response.Content.ReadAsStringAsync(); + // Act + response = await client.SendAsync(request); - Assert.Contains("The query specified in the URI is not valid. " + - "Query option 'Compute' is not allowed. To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings", payload); - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } + // Assert + string payload = await response.Content.ReadAsStringAsync(); + + Assert.Contains("The query specified in the URI is not valid. " + + "Query option 'Compute' is not allowed. To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings", payload); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterController.cs index f0070c87d..529c9f079 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterController.cs @@ -10,14 +10,13 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter; + +public class PeopleController : ODataController { - public class PeopleController : ODataController + [EnableQuery] + public ActionResult> Get() { - [EnableQuery] - public ActionResult> Get() - { - return Ok(DollarFilterDataSource.People); - } + return Ok(DollarFilterDataSource.People); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataModel.cs index fbb9ad80b..3e1a5a2b8 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataModel.cs @@ -5,11 +5,10 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter; + +public class Person { - public class Person - { - public int Id { get; set; } - public string SSN { get; set; } - } + public int Id { get; set; } + public string SSN { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataSource.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataSource.cs index a839cc4d1..2b138d6ae 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataSource.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterDataSource.cs @@ -7,23 +7,22 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter; + +public class DollarFilterDataSource { - public class DollarFilterDataSource - { - private static IList people; + private static IList people; - static DollarFilterDataSource() + static DollarFilterDataSource() + { + people = new List { - people = new List - { - new Person { Id = 1, SSN = "a'bc" }, - new Person { Id = 2, SSN = "'def" }, - new Person { Id = 3, SSN = "xyz'" }, - new Person { Id = 4, SSN = "'pqr'" } - }; - } - - public static IList People => people; + new Person { Id = 1, SSN = "a'bc" }, + new Person { Id = 2, SSN = "'def" }, + new Person { Id = 3, SSN = "xyz'" }, + new Person { Id = 4, SSN = "'pqr'" } + }; } + + public static IList People => people; } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterEdmModel.cs index 6882766ae..b161ba3dd 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterEdmModel.cs @@ -8,16 +8,15 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter; + +public class DollarFilterEdmModel { - public class DollarFilterEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("People"); + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("People"); - return builder.GetEdmModel(); - } + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterTests.cs index 0b76cf5bc..0db808e9d 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFilter/DollarFilterTests.cs @@ -14,85 +14,84 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFilter; + +public class DollarFilterTests : WebApiTestBase { - public class DollarFilterTests : WebApiTestBase + public DollarFilterTests(WebApiTestFixture fixture) + : base(fixture) { - public DollarFilterTests(WebApiTestFixture fixture) - : base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel model = DollarFilterEdmModel.GetEdmModel(); + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel model = DollarFilterEdmModel.GetEdmModel(); - services.ConfigureControllers(typeof(PeopleController)); + services.ConfigureControllers(typeof(PeopleController)); - services.AddControllers().AddOData(opt => - opt.Filter().Select().AddRouteComponents("odata", model)); - } + services.AddControllers().AddOData(opt => + opt.Filter().Select().AddRouteComponents("odata", model)); + } - [Theory] - [InlineData("('a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"}]")] - [InlineData("('''def')", "[{\"Id\":2,\"SSN\":\"'def\"}]")] - [InlineData("('xyz''')", "[{\"Id\":3,\"SSN\":\"xyz'\"}]")] - [InlineData("('''pqr''')", "[{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('a''bc','''def')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"}]")] - [InlineData("('a''bc','xyz''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] - [InlineData("('a''bc','''pqr''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''def','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"}]")] - [InlineData("('''def','xyz''')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] - [InlineData("('''def','''pqr''')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('xyz''','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] - [InlineData("('xyz''','''def')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] - [InlineData("('xyz''','''pqr''')", "[{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''pqr''','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''pqr''','''def')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''pqr''','xyz''')", "[{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('a''bc','''def','xyz''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] - [InlineData("('a''bc','''def','''pqr''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('a''bc','xyz''','''def')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] - [InlineData("('a''bc','xyz''','''pqr''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('a''bc','''pqr''','''def')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('a''bc','''pqr''','xyz''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''def','a''bc','xyz''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] - [InlineData("('''def','a''bc','''pqr''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''def','xyz''','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] - [InlineData("('''def','xyz''','''pqr''')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''def','''pqr''','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''def','''pqr''','xyz''')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('xyz''','a''bc','''def')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] - [InlineData("('xyz''','a''bc','''pqr''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('xyz''','''def','''pqr''')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('xyz''','''def','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] - [InlineData("('xyz''','''pqr''','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('xyz''','''pqr''','''def')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''pqr''','a''bc','''def')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''pqr''','a''bc','xyz''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''pqr''','''def','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''pqr''','''def','xyz''')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''pqr''','xyz''','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - [InlineData("('''pqr''','xyz''','''def')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] - public async Task TestSingleQuotesOnInExpression(string inExpr, string partialResult) - { - // Arrange - var queryUrl = $"odata/People?$filter=SSN in {inExpr}"; - var request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); - var client = CreateClient(); + [Theory] + [InlineData("('a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"}]")] + [InlineData("('''def')", "[{\"Id\":2,\"SSN\":\"'def\"}]")] + [InlineData("('xyz''')", "[{\"Id\":3,\"SSN\":\"xyz'\"}]")] + [InlineData("('''pqr''')", "[{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('a''bc','''def')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"}]")] + [InlineData("('a''bc','xyz''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] + [InlineData("('a''bc','''pqr''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''def','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"}]")] + [InlineData("('''def','xyz''')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] + [InlineData("('''def','''pqr''')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('xyz''','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] + [InlineData("('xyz''','''def')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] + [InlineData("('xyz''','''pqr''')", "[{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''pqr''','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''pqr''','''def')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''pqr''','xyz''')", "[{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('a''bc','''def','xyz''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] + [InlineData("('a''bc','''def','''pqr''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('a''bc','xyz''','''def')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] + [InlineData("('a''bc','xyz''','''pqr''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('a''bc','''pqr''','''def')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('a''bc','''pqr''','xyz''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''def','a''bc','xyz''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] + [InlineData("('''def','a''bc','''pqr''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''def','xyz''','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] + [InlineData("('''def','xyz''','''pqr''')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''def','''pqr''','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''def','''pqr''','xyz''')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('xyz''','a''bc','''def')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] + [InlineData("('xyz''','a''bc','''pqr''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('xyz''','''def','''pqr''')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('xyz''','''def','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"}]")] + [InlineData("('xyz''','''pqr''','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('xyz''','''pqr''','''def')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''pqr''','a''bc','''def')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''pqr''','a''bc','xyz''')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''pqr''','''def','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''pqr''','''def','xyz''')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''pqr''','xyz''','a''bc')", "[{\"Id\":1,\"SSN\":\"a'bc\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + [InlineData("('''pqr''','xyz''','''def')", "[{\"Id\":2,\"SSN\":\"'def\"},{\"Id\":3,\"SSN\":\"xyz'\"},{\"Id\":4,\"SSN\":\"'pqr'\"}]")] + public async Task TestSingleQuotesOnInExpression(string inExpr, string partialResult) + { + // Arrange + var queryUrl = $"odata/People?$filter=SSN in {inExpr}"; + var request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); + var client = CreateClient(); - // Act - var response = await client.SendAsync(request); + // Act + var response = await client.SendAsync(request); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - var result = await response.Content.ReadAsStringAsync(); + var result = await response.Content.ReadAsStringAsync(); - Assert.EndsWith($"$metadata#People\",\"value\":{partialResult}}}", result); - } + Assert.EndsWith($"$metadata#People\",\"value\":{partialResult}}}", result); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatController.cs index ccd894427..7c33d72ff 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatController.cs @@ -12,47 +12,46 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFormat +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFormat; + +public class DollarFormatCustomersController : ODataController { - public class DollarFormatCustomersController : ODataController - { - private IList customers = Enumerable.Range(0, 10).Select(i => - new DollarFormatCustomer - { - Id = i, - Name = "Customer Name " + i, - Orders = Enumerable.Range(0, i).Select(j => - new DollarFormatOrder - { - Id = i * 10 + j, - PurchaseDate = DateTime.Today.Subtract(TimeSpan.FromDays(i * 10 + j)), - Detail = "This is Order " + i * 10 + j - }).ToList(), - SpecialOrder = new DollarFormatOrder + private IList customers = Enumerable.Range(0, 10).Select(i => + new DollarFormatCustomer + { + Id = i, + Name = "Customer Name " + i, + Orders = Enumerable.Range(0, i).Select(j => + new DollarFormatOrder { - Id = i * 10, - PurchaseDate = DateTime.Today.Subtract(TimeSpan.FromDays(i * 10)), - Detail = "This is Order " + i * 10 - } - }).ToList(); - - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - public IActionResult Get() - { - return Ok(customers); - } + Id = i * 10 + j, + PurchaseDate = DateTime.Today.Subtract(TimeSpan.FromDays(i * 10 + j)), + Detail = "This is Order " + i * 10 + j + }).ToList(), + SpecialOrder = new DollarFormatOrder + { + Id = i * 10, + PurchaseDate = DateTime.Today.Subtract(TimeSpan.FromDays(i * 10)), + Detail = "This is Order " + i * 10 + } + }).ToList(); - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - public IActionResult Get(int key) - { - var customer = customers.FirstOrDefault(c => c.Id == key); + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public IActionResult Get() + { + return Ok(customers); + } - if (customer == null) - { - throw new ArgumentOutOfRangeException("key"); - } + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public IActionResult Get(int key) + { + var customer = customers.FirstOrDefault(c => c.Id == key); - return Ok(customer); + if (customer == null) + { + throw new ArgumentOutOfRangeException("key"); } + + return Ok(customer); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatModel.cs index 75f7e83e4..63fecbc07 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatModel.cs @@ -8,25 +8,24 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFormat +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFormat; + +public class DollarFormatCustomer { - public class DollarFormatCustomer - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public IList Orders { get; set; } + public IList Orders { get; set; } - public DollarFormatOrder SpecialOrder { get; set; } - } + public DollarFormatOrder SpecialOrder { get; set; } +} - public class DollarFormatOrder - { - public int Id { get; set; } +public class DollarFormatOrder +{ + public int Id { get; set; } - public DateTimeOffset PurchaseDate { get; set; } + public DateTimeOffset PurchaseDate { get; set; } - public string Detail { get; set; } - } + public string Detail { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatOverrideAcceptMediaTypeTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatOverrideAcceptMediaTypeTests.cs index 5b1979f29..f6475d2ae 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatOverrideAcceptMediaTypeTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatOverrideAcceptMediaTypeTests.cs @@ -18,299 +18,298 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFormat +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFormat; + +public class DollarFormatOverrideAcceptMediaTypeTests : WebApiTestBase { - public class DollarFormatOverrideAcceptMediaTypeTests : WebApiTestBase + public DollarFormatOverrideAcceptMediaTypeTests(WebApiTestFixture fixture) + :base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) { - public DollarFormatOverrideAcceptMediaTypeTests(WebApiTestFixture fixture) - :base(fixture) + IEdmModel model = GetEdmModel(); + services.ConfigureControllers(typeof(DollarFormatCustomersController), typeof(MetadataController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); + } + + public static TheoryDataSet BasicMediaTypes + { + get { + return DollarFormatWithoutAcceptMediaTypeTests.BasicMediaTypes; } + } - protected static void UpdateConfigureServices(IServiceCollection services) + [Theory] + [MemberData(nameof(BasicMediaTypes))] + public async Task QueryEntitySetWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string query = $"?$expand=SpecialOrder($select=Detail)&$filter=Id le 5&$orderby=Id desc&$select=Id&$format={dollarFormat}"; + string requestUri = $"odata/DollarFormatCustomers{query}"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + + if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) { - IEdmModel model = GetEdmModel(); - services.ConfigureControllers(typeof(DollarFormatCustomersController), typeof(MetadataController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - public static TheoryDataSet BasicMediaTypes + if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) { - get - { - return DollarFormatWithoutAcceptMediaTypeTests.BasicMediaTypes; - } + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - [Theory] - [MemberData(nameof(BasicMediaTypes))] - public async Task QueryEntitySetWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) - { - // Arrange - string query = $"?$expand=SpecialOrder($select=Detail)&$filter=Id le 5&$orderby=Id desc&$select=Id&$format={dollarFormat}"; - string requestUri = $"odata/DollarFormatCustomers{query}"; + JObject jObj = await response.Content.ReadAsObject(); + JArray value = jObj["value"] as JArray; + Assert.Equal(6, value.Count); + } - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - HttpClient client = CreateClient(); + [Theory] + [MemberData(nameof(BasicMediaTypes))] + public async Task QueryEntityWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string requestUri = $"/odata/DollarFormatCustomers(1)?$format={dollarFormat}"; - // Act - HttpResponseMessage response = await client.SendAsync(request); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + HttpClient client = CreateClient(); - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + // Act + HttpResponseMessage response = await client.SendAsync(request); - if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - JObject jObj = await response.Content.ReadAsObject(); - JArray value = jObj["value"] as JArray; - Assert.Equal(6, value.Count); + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + + if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) + { + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - [Theory] - [MemberData(nameof(BasicMediaTypes))] - public async Task QueryEntityWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) + if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) { - // Arrange - string requestUri = $"/odata/DollarFormatCustomers(1)?$format={dollarFormat}"; + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); + } + + JObject jObj = await response.Content.ReadAsObject(); + Assert.Equal("Customer Name 1", jObj["Name"]); + } - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - HttpClient client = CreateClient(); + [Theory] + [MemberData(nameof(BasicMediaTypes))] + public async Task QueryPropertyWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string requestUri = $"odata/DollarFormatCustomers(1)?$select=Name&$format={dollarFormat}"; - // Act - HttpResponseMessage response = await client.SendAsync(request); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + HttpClient client = CreateClient(); - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + // Act + HttpResponseMessage response = await client.SendAsync(request); - if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - JObject jObj = await response.Content.ReadAsObject(); - Assert.Equal("Customer Name 1", jObj["Name"]); + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + + if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) + { + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - [Theory] - [MemberData(nameof(BasicMediaTypes))] - public async Task QueryPropertyWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) + if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) { - // Arrange - string requestUri = $"odata/DollarFormatCustomers(1)?$select=Name&$format={dollarFormat}"; + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); + } + + JObject jObj = await response.Content.ReadAsObject(); + Assert.Equal("Customer Name 1", jObj["Name"]); + } - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - HttpClient client = CreateClient(); + [Theory] + [MemberData(nameof(BasicMediaTypes))] + public async Task QueryNavigationPropertyWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string requestUri = $"odata/DollarFormatCustomers(1)?$select=SpecialOrder&$expand=SpecialOrder&$format={dollarFormat}"; - // Act - HttpResponseMessage response = await client.SendAsync(request); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + HttpClient client = CreateClient(); - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + // Act + HttpResponseMessage response = await client.SendAsync(request); - if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - JObject jObj = await response.Content.ReadAsObject(); - Assert.Equal("Customer Name 1", jObj["Name"]); + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + + if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) + { + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - [Theory] - [MemberData(nameof(BasicMediaTypes))] - public async Task QueryNavigationPropertyWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) + if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) { - // Arrange - string requestUri = $"odata/DollarFormatCustomers(1)?$select=SpecialOrder&$expand=SpecialOrder&$format={dollarFormat}"; + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); + } - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - HttpClient client = CreateClient(); + JObject jObj = await response.Content.ReadAsObject(); + Assert.Equal(10, jObj["SpecialOrder"]["Id"]); + } - // Act - HttpResponseMessage response = await client.SendAsync(request); + [Theory] + [MemberData(nameof(BasicMediaTypes))] + public async Task QueryCollectionWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string requestUri = $"odata/DollarFormatCustomers(2)?$select=Orders&$expand=Orders&$format={dollarFormat}"; - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + HttpClient client = CreateClient(); - if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - JObject jObj = await response.Content.ReadAsObject(); - Assert.Equal(10, jObj["SpecialOrder"]["Id"]); + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + + if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) + { + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - [Theory] - [MemberData(nameof(BasicMediaTypes))] - public async Task QueryCollectionWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) + if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) { - // Arrange - string requestUri = $"odata/DollarFormatCustomers(2)?$select=Orders&$expand=Orders&$format={dollarFormat}"; + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); + } - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - HttpClient client = CreateClient(); + JObject jObj = await response.Content.ReadAsObject(); + JArray orders = jObj["Orders"] as JArray; + Assert.Equal(2, orders.Count); + } - // Act - HttpResponseMessage response = await client.SendAsync(request); + [Theory] + [InlineData("json")] + [InlineData("application/json")] + [InlineData("application/json;odata.metadata=none")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=full")] + [InlineData("application/json;odata.metadata=none;odata.streaming=true")] + [InlineData("application/json;odata.metadata=none;odata.streaming=false")] + [InlineData("application/json;odata.metadata=minimal;odata.streaming=true")] + [InlineData("application/json;odata.metadata=minimal;odata.streaming=false")] + [InlineData("application/json;odata.metadata=full;odata.streaming=true")] + [InlineData("application/json;odata.metadata=full;odata.streaming=false")] + public async Task QueryServiceDocumentWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string requestUri = $"odata?$format={dollarFormat}"; - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + HttpClient client = CreateClient(); - if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - JObject jObj = await response.Content.ReadAsObject(); - JArray orders = jObj["Orders"] as JArray; - Assert.Equal(2, orders.Count); + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + + if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) + { + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - [Theory] - [InlineData("json")] - [InlineData("application/json")] - [InlineData("application/json;odata.metadata=none")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=full")] - [InlineData("application/json;odata.metadata=none;odata.streaming=true")] - [InlineData("application/json;odata.metadata=none;odata.streaming=false")] - [InlineData("application/json;odata.metadata=minimal;odata.streaming=true")] - [InlineData("application/json;odata.metadata=minimal;odata.streaming=false")] - [InlineData("application/json;odata.metadata=full;odata.streaming=true")] - [InlineData("application/json;odata.metadata=full;odata.streaming=false")] - public async Task QueryServiceDocumentWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) + if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) { - // Arrange - string requestUri = $"odata?$format={dollarFormat}"; + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); + } - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - HttpClient client = CreateClient(); + JObject jObj = await response.Content.ReadAsObject(); + JArray value = jObj["value"] as JArray; + Assert.Equal(2, value.Count); + } - // Act - HttpResponseMessage response = await client.SendAsync(request); + [Theory] + [InlineData("xml")] + [InlineData("application/xml")] + [InlineData("json")] + [InlineData("application/json")] + public async Task QueryMetadataDocumentWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string requestUri = $"odata/$metadata?$format={dollarFormat}"; - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml")); // accept for xml + HttpClient client = CreateClient(); - if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - JObject jObj = await response.Content.ReadAsObject(); - JArray value = jObj["value"] as JArray; - Assert.Equal(2, value.Count); - } + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + string payload = await response.Content.ReadAsStringAsync(); - [Theory] - [InlineData("xml")] - [InlineData("application/xml")] - [InlineData("json")] - [InlineData("application/json")] - public async Task QueryMetadataDocumentWithDollarFormatOverrideAcceptMediaTypeTests(string dollarFormat) + if (dollarFormat.Contains("xml")) { - // Arrange - string requestUri = $"odata/$metadata?$format={dollarFormat}"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml")); // accept for xml - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - string payload = await response.Content.ReadAsStringAsync(); - - if (dollarFormat.Contains("xml")) - { - Assert.Equal("application/xml", response.Content.Headers.ContentType.MediaType); - Assert.Contains("("DollarFormatCustomers"); - builder.EntitySet("DollarFormatOrders"); - return builder.GetEdmModel(); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + Assert.Contains("\"$Version\": \"4.0\",", payload); } } + + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("DollarFormatCustomers"); + builder.EntitySet("DollarFormatOrders"); + return builder.GetEdmModel(); + } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatWithoutAcceptMediaTypeTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatWithoutAcceptMediaTypeTests.cs index a1425390c..bc54a0aa2 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatWithoutAcceptMediaTypeTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarFormat/DollarFormatWithoutAcceptMediaTypeTests.cs @@ -18,303 +18,302 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFormat +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarFormat; + +public class DollarFormatWithoutAcceptMediaTypeTests : WebApiTestBase { - public class DollarFormatWithoutAcceptMediaTypeTests : WebApiTestBase + public DollarFormatWithoutAcceptMediaTypeTests(WebApiTestFixture fixture) + :base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) { - public DollarFormatWithoutAcceptMediaTypeTests(WebApiTestFixture fixture) - :base(fixture) + IEdmModel model = GetEdmModel(); + services.ConfigureControllers(typeof(DollarFormatCustomersController), typeof(MetadataController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); + } + + public static TheoryDataSet BasicMediaTypes + { + get { + var data = new TheoryDataSet(); + + data.Add(Uri.EscapeDataString("json")); + data.Add(Uri.EscapeDataString("application/json")); + data.Add(Uri.EscapeDataString("application/json;odata.metadata=none")); + data.Add(Uri.EscapeDataString("application/json;odata.metadata=minimal")); + data.Add(Uri.EscapeDataString("application/json;odata.metadata=full")); + data.Add(Uri.EscapeDataString("application/json;odata.metadata=none;odata.streaming=true")); + data.Add(Uri.EscapeDataString("application/json;odata.metadata=none;odata.streaming=false")); + data.Add(Uri.EscapeDataString("application/json;odata.metadata=minimal;odata.streaming=true")); + data.Add(Uri.EscapeDataString("application/json;odata.metadata=minimal;odata.streaming=false")); + data.Add(Uri.EscapeDataString("application/json;odata.metadata=full;odata.streaming=true")); + data.Add(Uri.EscapeDataString("application/json;odata.metadata=full;odata.streaming=false")); + + data.Add(Uri.EscapeDataString("application/json;odata.streaming=true;odata.metadata=none")); + data.Add(Uri.EscapeDataString("application/json;odata.streaming=false;odata.metadata=none")); + data.Add(Uri.EscapeDataString("application/json;odata.streaming=true;odata.metadata=minimal")); + data.Add(Uri.EscapeDataString("application/json;odata.streaming=false;odata.metadata=minimal")); + data.Add(Uri.EscapeDataString("application/json;odata.streaming=true;odata.metadata=full")); + data.Add(Uri.EscapeDataString("application/json;odata.streaming=false;odata.metadata=full")); + + data.Add(Uri.EscapeDataString("Json")); + data.Add(Uri.EscapeDataString("jSoN")); + data.Add(Uri.EscapeDataString("APPLICATION/JSON;ODATA.METADATA=NONE;odata.streaming=TRUE")); + data.Add(Uri.EscapeDataString("aPpLiCaTiOn/JsOn;odata.streaming=tRuE;oDaTa.MeTaDaTa=NoNe")); + + return data; } + } - protected static void UpdateConfigureServices(IServiceCollection services) + [Theory] + [MemberData(nameof(BasicMediaTypes))] + public async Task QueryEntitySetWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string requestUri = $"odata/DollarFormatCustomers?$expand=SpecialOrder($select=Detail)&$filter=Id le 5&$orderby=Id desc&$select=Id&$format={dollarFormat}"; + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + + if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) { - IEdmModel model = GetEdmModel(); - services.ConfigureControllers(typeof(DollarFormatCustomersController), typeof(MetadataController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - public static TheoryDataSet BasicMediaTypes + if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) { - get - { - var data = new TheoryDataSet(); - - data.Add(Uri.EscapeDataString("json")); - data.Add(Uri.EscapeDataString("application/json")); - data.Add(Uri.EscapeDataString("application/json;odata.metadata=none")); - data.Add(Uri.EscapeDataString("application/json;odata.metadata=minimal")); - data.Add(Uri.EscapeDataString("application/json;odata.metadata=full")); - data.Add(Uri.EscapeDataString("application/json;odata.metadata=none;odata.streaming=true")); - data.Add(Uri.EscapeDataString("application/json;odata.metadata=none;odata.streaming=false")); - data.Add(Uri.EscapeDataString("application/json;odata.metadata=minimal;odata.streaming=true")); - data.Add(Uri.EscapeDataString("application/json;odata.metadata=minimal;odata.streaming=false")); - data.Add(Uri.EscapeDataString("application/json;odata.metadata=full;odata.streaming=true")); - data.Add(Uri.EscapeDataString("application/json;odata.metadata=full;odata.streaming=false")); - - data.Add(Uri.EscapeDataString("application/json;odata.streaming=true;odata.metadata=none")); - data.Add(Uri.EscapeDataString("application/json;odata.streaming=false;odata.metadata=none")); - data.Add(Uri.EscapeDataString("application/json;odata.streaming=true;odata.metadata=minimal")); - data.Add(Uri.EscapeDataString("application/json;odata.streaming=false;odata.metadata=minimal")); - data.Add(Uri.EscapeDataString("application/json;odata.streaming=true;odata.metadata=full")); - data.Add(Uri.EscapeDataString("application/json;odata.streaming=false;odata.metadata=full")); - - data.Add(Uri.EscapeDataString("Json")); - data.Add(Uri.EscapeDataString("jSoN")); - data.Add(Uri.EscapeDataString("APPLICATION/JSON;ODATA.METADATA=NONE;odata.streaming=TRUE")); - data.Add(Uri.EscapeDataString("aPpLiCaTiOn/JsOn;odata.streaming=tRuE;oDaTa.MeTaDaTa=NoNe")); - - return data; - } + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - [Theory] - [MemberData(nameof(BasicMediaTypes))] - public async Task QueryEntitySetWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) - { - // Arrange - string requestUri = $"odata/DollarFormatCustomers?$expand=SpecialOrder($select=Detail)&$filter=Id le 5&$orderby=Id desc&$select=Id&$format={dollarFormat}"; - HttpClient client = CreateClient(); + JObject jObj = await response.Content.ReadAsObject(); + JArray value = jObj["value"] as JArray; + Assert.Equal(6, value.Count); + } - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + [Theory] + [MemberData(nameof(BasicMediaTypes))] + public async Task QueryEntityWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string requestUri = $"odata/DollarFormatCustomers(1)?$format={dollarFormat}"; + HttpClient client = CreateClient(); - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); - if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - JObject jObj = await response.Content.ReadAsObject(); - JArray value = jObj["value"] as JArray; - Assert.Equal(6, value.Count); + if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) + { + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - [Theory] - [MemberData(nameof(BasicMediaTypes))] - public async Task QueryEntityWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) + if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) { - // Arrange - string requestUri = $"odata/DollarFormatCustomers(1)?$format={dollarFormat}"; - HttpClient client = CreateClient(); + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); + } - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + JObject jObj = await response.Content.ReadAsObject(); + Assert.Equal("Customer Name 1", jObj["Name"]); + } - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + [Theory] + [MemberData(nameof(BasicMediaTypes))] + public async Task QueryPropertyWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string requestUri = $"odata/DollarFormatCustomers(1)?$select=Name&$format={dollarFormat}"; + HttpClient client = CreateClient(); - if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - JObject jObj = await response.Content.ReadAsObject(); - Assert.Equal("Customer Name 1", jObj["Name"]); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) + { + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - [Theory] - [MemberData(nameof(BasicMediaTypes))] - public async Task QueryPropertyWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) + if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) { - // Arrange - string requestUri = $"odata/DollarFormatCustomers(1)?$select=Name&$format={dollarFormat}"; - HttpClient client = CreateClient(); + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); + } - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + JObject jObj = await response.Content.ReadAsObject(); + Assert.Equal("Customer Name 1", jObj["Name"]); + } - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); - if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - JObject jObj = await response.Content.ReadAsObject(); - Assert.Equal("Customer Name 1", jObj["Name"]); + [Theory] + [MemberData(nameof(BasicMediaTypes))] + public async Task QueryNavigationPropertyWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string requestUri = $"odata/DollarFormatCustomers(1)?$select=SpecialOrder&$expand=SpecialOrder&$format={dollarFormat}"; + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + + if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) + { + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - [Theory] - [MemberData(nameof(BasicMediaTypes))] - public async Task QueryNavigationPropertyWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) + if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) { - // Arrange - string requestUri = $"odata/DollarFormatCustomers(1)?$select=SpecialOrder&$expand=SpecialOrder&$format={dollarFormat}"; - HttpClient client = CreateClient(); + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); + } - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + JObject jObj = await response.Content.ReadAsObject(); + Assert.Equal(10, jObj["SpecialOrder"]["Id"]); + } - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + [Theory] + [MemberData(nameof(BasicMediaTypes))] + public async Task QueryCollectionWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string requestUri = $"odata/DollarFormatCustomers(1)?$select=Orders&$expand=Orders&$format={dollarFormat}"; + HttpClient client = CreateClient(); - if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - JObject jObj = await response.Content.ReadAsObject(); - Assert.Equal(10, jObj["SpecialOrder"]["Id"]); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + + if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) + { + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - [Theory] - [MemberData(nameof(BasicMediaTypes))] - public async Task QueryCollectionWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) + if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) { - // Arrange - string requestUri = $"odata/DollarFormatCustomers(1)?$select=Orders&$expand=Orders&$format={dollarFormat}"; - HttpClient client = CreateClient(); + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); + } - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + JObject jObj = await response.Content.ReadAsObject(); + JArray orders = jObj["Orders"] as JArray; + Assert.Single(orders); + } - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + [Theory] + [InlineData("json")] + [InlineData("application/json")] + [InlineData("application/json;odata.metadata=none")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=full")] + [InlineData("application/json;odata.metadata=none;odata.streaming=true")] + [InlineData("application/json;odata.metadata=none;odata.streaming=false")] + [InlineData("application/json;odata.metadata=minimal;odata.streaming=true")] + [InlineData("application/json;odata.metadata=minimal;odata.streaming=false")] + [InlineData("application/json;odata.metadata=full;odata.streaming=true")] + [InlineData("application/json;odata.metadata=full;odata.streaming=false")] + public async Task QueryServiceDocumentWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string requestUri = $"odata?$format={dollarFormat}"; + HttpClient client = CreateClient(); - if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - JObject jObj = await response.Content.ReadAsObject(); - JArray orders = jObj["Orders"] as JArray; - Assert.Single(orders); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + + if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) + { + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); } - [Theory] - [InlineData("json")] - [InlineData("application/json")] - [InlineData("application/json;odata.metadata=none")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=full")] - [InlineData("application/json;odata.metadata=none;odata.streaming=true")] - [InlineData("application/json;odata.metadata=none;odata.streaming=false")] - [InlineData("application/json;odata.metadata=minimal;odata.streaming=true")] - [InlineData("application/json;odata.metadata=minimal;odata.streaming=false")] - [InlineData("application/json;odata.metadata=full;odata.streaming=true")] - [InlineData("application/json;odata.metadata=full;odata.streaming=false")] - public async Task QueryServiceDocumentWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) + if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) { - // Arrange - string requestUri = $"odata?$format={dollarFormat}"; - HttpClient client = CreateClient(); + var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); + Assert.NotNull(param); + Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); + } - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + JObject jObj = await response.Content.ReadAsObject(); + JArray value = jObj["value"] as JArray; + Assert.Equal(2, value.Count); + } - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + [Theory] + [InlineData("xml")] + [InlineData("application/xml")] + [InlineData("json")] + [InlineData("application/json")] + public async Task QueryMetadataDocumentWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) + { + // Arrange + string lowerDollarFormat = dollarFormat.ToLowerInvariant(); + string requestUri = $"odata/$metadata?$format={dollarFormat}"; + HttpClient client = CreateClient(); - if (dollarFormat.ToLowerInvariant().Contains("odata.metadata")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.metadata")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - if (dollarFormat.ToLowerInvariant().Contains("odata.streaming")) - { - var param = response.Content.Headers.ContentType.Parameters.FirstOrDefault(e => e.Name.Equals("odata.streaming")); - Assert.NotNull(param); - Assert.Contains(param.Value, dollarFormat.ToLowerInvariant()); - } - - JObject jObj = await response.Content.ReadAsObject(); - JArray value = jObj["value"] as JArray; - Assert.Equal(2, value.Count); - } + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + + // Assert + Assert.True(response.IsSuccessStatusCode); - [Theory] - [InlineData("xml")] - [InlineData("application/xml")] - [InlineData("json")] - [InlineData("application/json")] - public async Task QueryMetadataDocumentWithDollarFormatWithoutAcceptMediaTypeTests(string dollarFormat) + string payload = await response.Content.ReadAsStringAsync(); + if (lowerDollarFormat.Contains("xml")) { - // Arrange - string lowerDollarFormat = dollarFormat.ToLowerInvariant(); - string requestUri = $"odata/$metadata?$format={dollarFormat}"; - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); - - // Assert - Assert.True(response.IsSuccessStatusCode); - - string payload = await response.Content.ReadAsStringAsync(); - if (lowerDollarFormat.Contains("xml")) - { - Assert.Equal("application/xml", response.Content.Headers.ContentType.MediaType); - Assert.Contains("("DollarFormatCustomers"); - builder.EntitySet("DollarFormatOrders"); - return builder.GetEdmModel(); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + Assert.Contains("\"$Version\": \"4.0\",", payload); } } + + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("DollarFormatCustomers"); + builder.EntitySet("DollarFormatOrders"); + return builder.GetEdmModel(); + } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchBinder.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchBinder.cs index 1f689946c..e88e34627 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchBinder.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchBinder.cs @@ -12,100 +12,99 @@ using Microsoft.AspNetCore.OData.Query.Expressions; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarSearch +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarSearch; + +/// +/// A simple search binder +/// +public class DollarSearchBinder : QueryBinder, ISearchBinder { - /// - /// A simple search binder - /// - public class DollarSearchBinder : QueryBinder, ISearchBinder - { - internal static readonly MethodInfo StringEqualsMethodInfo = typeof(string).GetMethod("Equals", - new[] - { - typeof(string), - typeof(string), - typeof(StringComparison) - }); - - private static ISet _categories = new HashSet + internal static readonly MethodInfo StringEqualsMethodInfo = typeof(string).GetMethod("Equals", + new[] { - "food", "office", "device" - }; + typeof(string), + typeof(string), + typeof(StringComparison) + }); - private static ISet _colors = new HashSet - { - "white", "red", "green", "blue", "brown" - }; + private static ISet _categories = new HashSet + { + "food", "office", "device" + }; + + private static ISet _colors = new HashSet + { + "white", "red", "green", "blue", "brown" + }; + + public Expression BindSearch(SearchClause searchClause, QueryBinderContext context) + { + Expression exp = BindSingleValueNode(searchClause.Expression, context); + + LambdaExpression lambdaExp = Expression.Lambda(exp, context.CurrentParameter); + + return lambdaExp; + } - public Expression BindSearch(SearchClause searchClause, QueryBinderContext context) + public override Expression BindSingleValueNode(SingleValueNode node, QueryBinderContext context) + { + switch (node.Kind) { - Expression exp = BindSingleValueNode(searchClause.Expression, context); + case QueryNodeKind.BinaryOperator: + return BindBinaryOperatorNode(node as BinaryOperatorNode, context); - LambdaExpression lambdaExp = Expression.Lambda(exp, context.CurrentParameter); + case QueryNodeKind.SearchTerm: + return BindSearchTerm(node as SearchTermNode, context); - return lambdaExp; + case QueryNodeKind.UnaryOperator: + return BindUnaryOperatorNode(node as UnaryOperatorNode, context); } - public override Expression BindSingleValueNode(SingleValueNode node, QueryBinderContext context) - { - switch (node.Kind) - { - case QueryNodeKind.BinaryOperator: - return BindBinaryOperatorNode(node as BinaryOperatorNode, context); + return null; + } - case QueryNodeKind.SearchTerm: - return BindSearchTerm(node as SearchTermNode, context); + public override Expression BindBinaryOperatorNode(BinaryOperatorNode binaryOperatorNode, QueryBinderContext context) + { + Expression left = Bind(binaryOperatorNode.Left, context); - case QueryNodeKind.UnaryOperator: - return BindUnaryOperatorNode(node as UnaryOperatorNode, context); - } + Expression right = Bind(binaryOperatorNode.Right, context); - return null; - } + return ExpressionBinderHelper.CreateBinaryExpression(binaryOperatorNode.OperatorKind, left, right, liftToNull: false, context.QuerySettings); + } - public override Expression BindBinaryOperatorNode(BinaryOperatorNode binaryOperatorNode, QueryBinderContext context) + public Expression BindSearchTerm(SearchTermNode node, QueryBinderContext context) + { + // Source is from context; + Expression source = context.CurrentParameter; + + string text = node.Text.ToLowerInvariant(); + if (_categories.Contains(text)) { - Expression left = Bind(binaryOperatorNode.Left, context); + // $it.Category + Expression categoryProperty = Expression.Property(source, "Category"); - Expression right = Bind(binaryOperatorNode.Right, context); + // $it.Category.Name + Expression categoryName = Expression.Property(categoryProperty, "Name"); - return ExpressionBinderHelper.CreateBinaryExpression(binaryOperatorNode.OperatorKind, left, right, liftToNull: false, context.QuerySettings); + // string.Equals($it.Category.Name, text, StringComparison.OrdinalIgnoreCase); + return Expression.Call(null, StringEqualsMethodInfo, + categoryName, Expression.Constant(text, typeof(string)), Expression.Constant(StringComparison.OrdinalIgnoreCase, typeof(StringComparison))); } + else if (_colors.Contains(text)) + { + // $it.Color + Expression colorProperty = Expression.Property(source, "Color"); - public Expression BindSearchTerm(SearchTermNode node, QueryBinderContext context) + // $it.Color.ToString() + Expression colorPropertyString = Expression.Call(colorProperty, "ToString", typeArguments: null, arguments: null); + + // string.Equals($it.Color.ToString(), text, StringComparison.OrdinalIgnoreCase); + return Expression.Call(null, StringEqualsMethodInfo, + colorPropertyString, Expression.Constant(text, typeof(string)), Expression.Constant(StringComparison.OrdinalIgnoreCase, typeof(StringComparison))); + } + else { - // Source is from context; - Expression source = context.CurrentParameter; - - string text = node.Text.ToLowerInvariant(); - if (_categories.Contains(text)) - { - // $it.Category - Expression categoryProperty = Expression.Property(source, "Category"); - - // $it.Category.Name - Expression categoryName = Expression.Property(categoryProperty, "Name"); - - // string.Equals($it.Category.Name, text, StringComparison.OrdinalIgnoreCase); - return Expression.Call(null, StringEqualsMethodInfo, - categoryName, Expression.Constant(text, typeof(string)), Expression.Constant(StringComparison.OrdinalIgnoreCase, typeof(StringComparison))); - } - else if (_colors.Contains(text)) - { - // $it.Color - Expression colorProperty = Expression.Property(source, "Color"); - - // $it.Color.ToString() - Expression colorPropertyString = Expression.Call(colorProperty, "ToString", typeArguments: null, arguments: null); - - // string.Equals($it.Color.ToString(), text, StringComparison.OrdinalIgnoreCase); - return Expression.Call(null, StringEqualsMethodInfo, - colorPropertyString, Expression.Constant(text, typeof(string)), Expression.Constant(StringComparison.OrdinalIgnoreCase, typeof(StringComparison))); - } - else - { - return Expression.Constant(false, typeof(bool)); - } + return Expression.Constant(false, typeof(bool)); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchController.cs index 18bb7c56e..cdf98900b 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchController.cs @@ -10,26 +10,25 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarSearch +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarSearch; + +public class ProductsController : ODataController { - public class ProductsController : ODataController + [EnableQuery] + public IActionResult Get() { - [EnableQuery] - public IActionResult Get() - { - return Ok(DollarSearchDataSource.Products); - } + return Ok(DollarSearchDataSource.Products); + } - [EnableQuery] - public IActionResult Get(int key) + [EnableQuery] + public IActionResult Get(int key) + { + SearchProduct c = DollarSearchDataSource.Products.FirstOrDefault(c => c.Id == key); + if (c == null) { - SearchProduct c = DollarSearchDataSource.Products.FirstOrDefault(c => c.Id == key); - if (c == null) - { - return NotFound($"Cannot find product with key = {key}"); - } - - return Ok(c); + return NotFound($"Cannot find product with key = {key}"); } + + return Ok(c); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchDataModel.cs index c4d6f1520..2d3eefa79 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchDataModel.cs @@ -5,40 +5,39 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarSearch +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarSearch; + +public class SearchProduct { - public class SearchProduct - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public SearchColor Color { get; set; } + public SearchColor Color { get; set; } - public double Price { get; set; } + public double Price { get; set; } - public int Qty { get; set; } + public int Qty { get; set; } - public SearchCategory Category { get; set; } - } + public SearchCategory Category { get; set; } +} - public class SearchCategory - { - public int Id { get; set; } +public class SearchCategory +{ + public int Id { get; set; } - public string Name { get; set; } - } + public string Name { get; set; } +} - public enum SearchColor - { - White, +public enum SearchColor +{ + White, - Red, + Red, - Green, + Green, - Blue, + Blue, - Brown - } + Brown } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchDataSource.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchDataSource.cs index c0ae5ee19..e4c4ffbe4 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchDataSource.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchDataSource.cs @@ -7,49 +7,48 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarSearch +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarSearch; + +public class DollarSearchDataSource { - public class DollarSearchDataSource + private static IList _products; + private static IList _categories; + + static DollarSearchDataSource() { - private static IList _products; - private static IList _categories; + GenerateProducts(); + } + + public static IList Products => _products; - static DollarSearchDataSource() + public static IList Category => _categories; + + private static void GenerateProducts() + { + _products = new List { - GenerateProducts(); - } + new SearchProduct { Id = 1, Name = "Sugar", Color = SearchColor.White, Price = 1.99, Qty = 10 }, // food + new SearchProduct { Id = 2, Name = "Pencil", Color = SearchColor.Blue, Price = 3.99, Qty = 15 }, // office + new SearchProduct { Id = 3, Name = "Coffee", Color = SearchColor.Brown, Price = 2.99, Qty = 15 }, // food + new SearchProduct { Id = 4, Name = "Phone", Color = SearchColor.Red, Price = 9.01, Qty = 20 }, // device + new SearchProduct { Id = 5, Name = "Paper", Color = SearchColor.White, Price = 6.99, Qty = 4 }, // office + new SearchProduct { Id = 6, Name = "TV", Color = SearchColor.Red, Price = 9.01, Qty = 20 }, // device + }; + + _categories = new List + { + new SearchCategory { Id = 1, Name = "Food" }, + new SearchCategory { Id = 2, Name = "Office" }, + new SearchCategory { Id = 3, Name = "Device" }, + }; - public static IList Products => _products; + _products[0].Category = _categories[0]; + _products[2].Category = _categories[0]; - public static IList Category => _categories; + _products[1].Category = _categories[1]; + _products[4].Category = _categories[1]; - private static void GenerateProducts() - { - _products = new List - { - new SearchProduct { Id = 1, Name = "Sugar", Color = SearchColor.White, Price = 1.99, Qty = 10 }, // food - new SearchProduct { Id = 2, Name = "Pencil", Color = SearchColor.Blue, Price = 3.99, Qty = 15 }, // office - new SearchProduct { Id = 3, Name = "Coffee", Color = SearchColor.Brown, Price = 2.99, Qty = 15 }, // food - new SearchProduct { Id = 4, Name = "Phone", Color = SearchColor.Red, Price = 9.01, Qty = 20 }, // device - new SearchProduct { Id = 5, Name = "Paper", Color = SearchColor.White, Price = 6.99, Qty = 4 }, // office - new SearchProduct { Id = 6, Name = "TV", Color = SearchColor.Red, Price = 9.01, Qty = 20 }, // device - }; - - _categories = new List - { - new SearchCategory { Id = 1, Name = "Food" }, - new SearchCategory { Id = 2, Name = "Office" }, - new SearchCategory { Id = 3, Name = "Device" }, - }; - - _products[0].Category = _categories[0]; - _products[2].Category = _categories[0]; - - _products[1].Category = _categories[1]; - _products[4].Category = _categories[1]; - - _products[3].Category = _categories[2]; - _products[5].Category = _categories[2]; - } + _products[3].Category = _categories[2]; + _products[5].Category = _categories[2]; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchEdmModel.cs index 151e0d12e..a316ae029 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchEdmModel.cs @@ -8,17 +8,16 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarSearch +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarSearch; + +public class DollarSearchEdmModel { - public class DollarSearchEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Products"); - builder.EntitySet("Categories"); - IEdmModel model = builder.GetEdmModel(); - return model; - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Products"); + builder.EntitySet("Categories"); + IEdmModel model = builder.GetEdmModel(); + return model; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchTests.cs index 8881cd746..cb24c9e59 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DollarSearch/DollarSearchTests.cs @@ -19,191 +19,190 @@ using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarSearch +namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarSearch; + +public class DollarSearchTests : WebApiTestBase { - public class DollarSearchTests : WebApiTestBase + private readonly ITestOutputHelper output; + + public DollarSearchTests(WebApiTestFixture fixture, ITestOutputHelper output) + : base(fixture) { - private readonly ITestOutputHelper output; + this.output = output; + } - public DollarSearchTests(WebApiTestFixture fixture, ITestOutputHelper output) - : base(fixture) - { - this.output = output; - } + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = DollarSearchEdmModel.GetEdmModel(); - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = DollarSearchEdmModel.GetEdmModel(); + services.ConfigureControllers(typeof(ProductsController)); - services.ConfigureControllers(typeof(ProductsController)); + services.AddControllers().AddOData(opt => + opt.Count() + .Filter() + .OrderBy() + .Expand() + .SetMaxTop(null) + .Select() - services.AddControllers().AddOData(opt => - opt.Count() - .Filter() - .OrderBy() - .Expand() - .SetMaxTop(null) - .Select() + // route with ISearchBinder registered + .AddRouteComponents("odata", edmModel, services => services.AddSingleton()) - // route with ISearchBinder registered - .AddRouteComponents("odata", edmModel, services => services.AddSingleton()) + // route without ISearchBinder + .AddRouteComponents("nonsearch", edmModel)); + } - // route without ISearchBinder - .AddRouteComponents("nonsearch", edmModel)); - } + [Fact] + public async Task QueryForProducts_IncludesDollarSearch_OnRouteWithoutISeachBinder() + { + // Arrange + string queryUrl = $"nonsearch/Products?$search=office"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; - [Fact] - public async Task QueryForProducts_IncludesDollarSearch_OnRouteWithoutISeachBinder() - { - // Arrange - string queryUrl = $"nonsearch/Products?$search=office"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; + // Act + response = await client.SendAsync(request); - // Act - response = await client.SendAsync(request); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + JObject payloadBody = await response.Content.ReadAsObject(); - JObject payloadBody = await response.Content.ReadAsObject(); + int[] actualIds = GetIds(payloadBody); + Assert.True(new[] { 1, 2, 3, 4, 5, 6 }.SequenceEqual(actualIds)); + } - int[] actualIds = GetIds(payloadBody); - Assert.True(new[] { 1, 2, 3, 4, 5, 6 }.SequenceEqual(actualIds)); - } + [Theory] + [InlineData("$search=office", new[] { 2, 5 })] + [InlineData("$search=food", new[] { 1, 3 })] + [InlineData("$search=device", new[] { 4, 6 })] + [InlineData("$search=NOT device", new[] { 1, 2, 3, 5 })] + [InlineData("$search=food OR device", new[] { 1,3, 4, 6 })] + [InlineData("$search=food AND device", new int[] { })] + [InlineData("$search=unknown", new int[] { })] + public async Task QueryForProducts_IncludesDollarSearch_OnCategory(string query, int[] ids) + { + // Arrange + string queryUrl = $"odata/Products?{query}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; - [Theory] - [InlineData("$search=office", new[] { 2, 5 })] - [InlineData("$search=food", new[] { 1, 3 })] - [InlineData("$search=device", new[] { 4, 6 })] - [InlineData("$search=NOT device", new[] { 1, 2, 3, 5 })] - [InlineData("$search=food OR device", new[] { 1,3, 4, 6 })] - [InlineData("$search=food AND device", new int[] { })] - [InlineData("$search=unknown", new int[] { })] - public async Task QueryForProducts_IncludesDollarSearch_OnCategory(string query, int[] ids) - { - // Arrange - string queryUrl = $"odata/Products?{query}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; + // Act + response = await client.SendAsync(request); - // Act - response = await client.SendAsync(request); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + JObject payloadBody = await response.Content.ReadAsObject(); - JObject payloadBody = await response.Content.ReadAsObject(); + int[] actualIds = GetIds(payloadBody); + Assert.True(ids.SequenceEqual(actualIds)); + } - int[] actualIds = GetIds(payloadBody); - Assert.True(ids.SequenceEqual(actualIds)); - } + [Theory] + [InlineData("$search=white", new[] { 1, 5 })] + [InlineData("$search=Blue", new[] { 2 })] + [InlineData("$search=rED", new[] { 4, 6 })] + [InlineData("$search=Brown", new int[] { 3 })] + [InlineData("$search=green", new int[] { })] + [InlineData("$search=NOT blue", new int[] { 1, 3, 4, 5, 6 })] + public async Task QueryForProducts_IncludesDollarSearch_OnEnumColor(string query, int[] ids) + { + // Arrange + string queryUrl = $"odata/Products?{query}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; - [Theory] - [InlineData("$search=white", new[] { 1, 5 })] - [InlineData("$search=Blue", new[] { 2 })] - [InlineData("$search=rED", new[] { 4, 6 })] - [InlineData("$search=Brown", new int[] { 3 })] - [InlineData("$search=green", new int[] { })] - [InlineData("$search=NOT blue", new int[] { 1, 3, 4, 5, 6 })] - public async Task QueryForProducts_IncludesDollarSearch_OnEnumColor(string query, int[] ids) - { - // Arrange - string queryUrl = $"odata/Products?{query}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; + // Act + response = await client.SendAsync(request); - // Act - response = await client.SendAsync(request); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + JObject payloadBody = await response.Content.ReadAsObject(); - JObject payloadBody = await response.Content.ReadAsObject(); + int[] actualIds = GetIds(payloadBody); + Assert.True(ids.SequenceEqual(actualIds)); + } - int[] actualIds = GetIds(payloadBody); - Assert.True(ids.SequenceEqual(actualIds)); - } + [Theory] + [InlineData("$search=food AND White", new[] { 1 })] + [InlineData("$search=(office OR food) AND White", new[] { 1, 5 })] + [InlineData("$search=(office OR food) AND NOT white", new[] { 2, 3 })] + public async Task QueryForProducts_IncludesDollarSearch_OnCategoryAndEnumColor(string query, int[] ids) + { + // Arrange + string queryUrl = $"odata/Products?{query}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; - [Theory] - [InlineData("$search=food AND White", new[] { 1 })] - [InlineData("$search=(office OR food) AND White", new[] { 1, 5 })] - [InlineData("$search=(office OR food) AND NOT white", new[] { 2, 3 })] - public async Task QueryForProducts_IncludesDollarSearch_OnCategoryAndEnumColor(string query, int[] ids) - { - // Arrange - string queryUrl = $"odata/Products?{query}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; + // Act + response = await client.SendAsync(request); - // Act - response = await client.SendAsync(request); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + JObject payloadBody = await response.Content.ReadAsObject(); - JObject payloadBody = await response.Content.ReadAsObject(); + int[] actualIds = GetIds(payloadBody); + Assert.True(ids.SequenceEqual(actualIds)); + } - int[] actualIds = GetIds(payloadBody); - Assert.True(ids.SequenceEqual(actualIds)); - } + [Fact] + public async Task QueryForProducts_IncludesDollarSearch_WithOtherQueryOptions() + { + // Arrange + string queryUrl = $"odata/Products?$search=food OR office&$filter=Id lt 4&$select=Name&$expand=Category"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"value\":[" + + "{\"Name\":\"Sugar\",\"Category\":{\"Id\":1,\"Name\":\"Food\"}}," + + "{\"Name\":\"Pencil\",\"Category\":{\"Id\":2,\"Name\":\"Office\"}}," + + "{\"Name\":\"Coffee\",\"Category\":{\"Id\":1,\"Name\":\"Food\"}}" + + "]}", payloadBody); + } - [Fact] - public async Task QueryForProducts_IncludesDollarSearch_WithOtherQueryOptions() - { - // Arrange - string queryUrl = $"odata/Products?$search=food OR office&$filter=Id lt 4&$select=Name&$expand=Category"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - string payloadBody = await response.Content.ReadAsStringAsync(); - - Assert.Equal("{\"value\":[" + - "{\"Name\":\"Sugar\",\"Category\":{\"Id\":1,\"Name\":\"Food\"}}," + - "{\"Name\":\"Pencil\",\"Category\":{\"Id\":2,\"Name\":\"Office\"}}," + - "{\"Name\":\"Coffee\",\"Category\":{\"Id\":1,\"Name\":\"Food\"}}" + - "]}", payloadBody); - } + private static int[] GetIds(JObject payload) + { + JArray value = payload["value"] as JArray; + Assert.NotNull(value); - private static int[] GetIds(JObject payload) + int[] ids = new int[value.Count()]; + for (int i = 0; i < value.Count(); i++) { - JArray value = payload["value"] as JArray; - Assert.NotNull(value); - - int[] ids = new int[value.Count()]; - for (int i = 0; i < value.Count(); i++) - { - JObject item = value[i] as JObject; - ids[i] = (int)item["Id"]; - } - - return ids; + JObject item = value[i] as JObject; + ids[i] = (int)item["Id"]; } + + return ids; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DynamicProperties/DynamicPropertiesController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DynamicProperties/DynamicPropertiesController.cs index 9af6471d1..090297a1e 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DynamicProperties/DynamicPropertiesController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DynamicProperties/DynamicPropertiesController.cs @@ -9,66 +9,65 @@ using Microsoft.AspNetCore.OData.Formatter; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties +namespace Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties; + +[Route("odata")] +public class DynamicCustomersController : ODataController { - [Route("odata")] - public class DynamicCustomersController : ODataController + // Convention routing + public IActionResult GetId(int key) { - // Convention routing - public IActionResult GetId(int key) - { - return Ok(string.Format("{0}_{1}", "Id", key)); - } + return Ok(string.Format("{0}_{1}", "Id", key)); + } - [HttpGet("DynamicCustomers({key})/{property}")] // this is for generic property, but for "Id", we have the more specific route - public IActionResult GetProperty(int key, string property) - { - return Ok($"GetProperty_{property}_{key}"); - } + [HttpGet("DynamicCustomers({key})/{property}")] // this is for generic property, but for "Id", we have the more specific route + public IActionResult GetProperty(int key, string property) + { + return Ok($"GetProperty_{property}_{key}"); + } - [Route("DynamicCustomers({key})/{dynamicproperty}")] // combined with [HttpGet, HttpPatch, HttpDelete] - [HttpGet] - [HttpPatch] - [HttpDelete] - [HttpGet("DynamicCustomers({key})/Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties.DynamicVipCustomer/{dynamicproperty}")] - public IActionResult GetDynamicProperty(int key, string dynamicProperty) - { - return Ok(string.Format("{0}_{1}_{2}", dynamicProperty, "GetDynamicProperty", key)); - } + [Route("DynamicCustomers({key})/{dynamicproperty}")] // combined with [HttpGet, HttpPatch, HttpDelete] + [HttpGet] + [HttpPatch] + [HttpDelete] + [HttpGet("DynamicCustomers({key})/Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties.DynamicVipCustomer/{dynamicproperty}")] + public IActionResult GetDynamicProperty(int key, string dynamicProperty) + { + return Ok(string.Format("{0}_{1}_{2}", dynamicProperty, "GetDynamicProperty", key)); + } - [HttpGet("DynamicCustomers({key})/Account/{dynamicproperty}")] - [HttpGet("DynamicCustomers({key})/Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties.DynamicVipCustomer/Account/{dynamicproperty}")] - public IActionResult GetDynamicPropertyFromAccount([FromODataUri] int key, [FromODataUri] string dynamicProperty) - { - return Ok(string.Format("{0}_{1}_{2}", dynamicProperty, "GetDynamicPropertyFromAccount", key)); - } + [HttpGet("DynamicCustomers({key})/Account/{dynamicproperty}")] + [HttpGet("DynamicCustomers({key})/Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties.DynamicVipCustomer/Account/{dynamicproperty}")] + public IActionResult GetDynamicPropertyFromAccount([FromODataUri] int key, [FromODataUri] string dynamicProperty) + { + return Ok(string.Format("{0}_{1}_{2}", dynamicProperty, "GetDynamicPropertyFromAccount", key)); + } - [HttpGet("DynamicCustomers({id})/Order/{dynamicproperty}")] - public IActionResult GetDynamicPropertyFromOrder([FromODataUri] int id, [FromODataUri] string dynamicproperty) - { - return Ok(string.Format("{0}_{1}_{2}", dynamicproperty, "GetDynamicPropertyFromOrder", id)); - } + [HttpGet("DynamicCustomers({id})/Order/{dynamicproperty}")] + public IActionResult GetDynamicPropertyFromOrder([FromODataUri] int id, [FromODataUri] string dynamicproperty) + { + return Ok(string.Format("{0}_{1}_{2}", dynamicproperty, "GetDynamicPropertyFromOrder", id)); } +} - [Route("odata")] - public class DynamicSingleCustomerController : ODataController +[Route("odata")] +public class DynamicSingleCustomerController : ODataController +{ + [HttpGet("DynamicSingleCustomer/{dynamicproperty}")] + public IActionResult GetDynamicProperty(string dynamicProperty) { - [HttpGet("DynamicSingleCustomer/{dynamicproperty}")] - public IActionResult GetDynamicProperty(string dynamicProperty) - { - return Ok(string.Format("{0}_{1}", dynamicProperty, "GetDynamicProperty")); - } + return Ok(string.Format("{0}_{1}", dynamicProperty, "GetDynamicProperty")); + } - [HttpGet("DynamicSingleCustomer/Account/{dynamicproperty}")] - public IActionResult GetDynamicPropertyFromAccount([FromODataUri] string dynamicProperty) - { - return Ok(string.Format("{0}_{1}", dynamicProperty, "GetDynamicPropertyFromAccount")); - } + [HttpGet("DynamicSingleCustomer/Account/{dynamicproperty}")] + public IActionResult GetDynamicPropertyFromAccount([FromODataUri] string dynamicProperty) + { + return Ok(string.Format("{0}_{1}", dynamicProperty, "GetDynamicPropertyFromAccount")); + } - [HttpGet("DynamicSingleCustomer/Order/{dynamicproperty}")] - public IActionResult GetDynamicPropertyFromOrder(string dynamicproperty) - { - return Ok(string.Format("{0}_{1}", dynamicproperty, "GetDynamicPropertyFromOrder")); - } + [HttpGet("DynamicSingleCustomer/Order/{dynamicproperty}")] + public IActionResult GetDynamicPropertyFromOrder(string dynamicproperty) + { + return Ok(string.Format("{0}_{1}", dynamicproperty, "GetDynamicPropertyFromOrder")); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DynamicProperties/DynamicPropertiesModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DynamicProperties/DynamicPropertiesModel.cs index 9f0f10bb9..571cc22b8 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DynamicProperties/DynamicPropertiesModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DynamicProperties/DynamicPropertiesModel.cs @@ -7,58 +7,57 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties +namespace Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties; + +public class DynamicCustomer { - public class DynamicCustomer - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public int Age { get; set; } + public int Age { get; set; } - public Account Account { get; set; } + public Account Account { get; set; } - public Order Order { get; set; } + public Order Order { get; set; } - public Account SecondAccount { get; set; } + public Account SecondAccount { get; set; } - public Dictionary DynamicProperties { get; set; } - } + public Dictionary DynamicProperties { get; set; } +} - public class Order - { - public string Name { get; set; } +public class Order +{ + public string Name { get; set; } - public Dictionary DynamicProperties { get; set; } - } + public Dictionary DynamicProperties { get; set; } +} - public class Account - { - public string Name { get; set; } +public class Account +{ + public string Name { get; set; } - public string Number { get; set; } + public string Number { get; set; } - public Dictionary DynamicProperties { get; set; } - } + public Dictionary DynamicProperties { get; set; } +} - public class DynamicVipCustomer : DynamicCustomer - { - public string VipCode { get; set; } - } +public class DynamicVipCustomer : DynamicCustomer +{ + public string VipCode { get; set; } +} - public class DynamicSingleCustomer - { - public int Id { get; set; } +public class DynamicSingleCustomer +{ + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public Account Account { get; set; } + public Account Account { get; set; } - public Order Order { get; set; } + public Order Order { get; set; } - public Account SecondAccount { get; set; } + public Account SecondAccount { get; set; } - public Dictionary DynamicProperties { get; set; } - } + public Dictionary DynamicProperties { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DynamicProperties/DynamicPropertiesTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DynamicProperties/DynamicPropertiesTest.cs index 961c1af93..448057dfb 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DynamicProperties/DynamicPropertiesTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DynamicProperties/DynamicPropertiesTest.cs @@ -14,144 +14,143 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties +namespace Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties; + +public class DynamicPropertiesTest : WebApiTestBase { - public class DynamicPropertiesTest : WebApiTestBase + public DynamicPropertiesTest(WebApiTestFixture fixture) + :base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(DynamicCustomersController), typeof(DynamicSingleCustomerController)/*, typeof(ODataEndpointController)*/); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel())); + } + + //[Fact] + // If test the routes, enable this test and see the payload. Remember to include the ODataEndpointController. + //public async Task TestRoutes() + //{ + // // Arrange + // string requestUri = "$odata"; + // HttpClient client = CreateClient(); + + // // Act + // var response = await client.GetAsync(requestUri); + + // // Assert + // response.EnsureSuccessStatusCode(); + // string payload = await response.Content.ReadAsStringAsync(); + //} + + [Theory] + [InlineData("DynamicCustomers(1)/DynamicPropertyName", "DynamicPropertyName_GetDynamicProperty_1")] + [InlineData("DynamicCustomers(2)/Account/DynamicPropertyName", "DynamicPropertyName_GetDynamicPropertyFromAccount_2")] + [InlineData("DynamicCustomers(3)/Order/DynamicPropertyName", "DynamicPropertyName_GetDynamicPropertyFromOrder_3")] + [InlineData("DynamicCustomers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties.DynamicVipCustomer/DynamicPropertyName", "DynamicPropertyName_GetDynamicProperty_4")] + [InlineData("DynamicCustomers(5)/Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties.DynamicVipCustomer/Account/DynamicPropertyName", "DynamicPropertyName_GetDynamicPropertyFromAccount_5")] + [InlineData("DynamicSingleCustomer/DynamicPropertyName", "DynamicPropertyName_GetDynamicProperty")] + [InlineData("DynamicSingleCustomer/Account/DynamicPropertyName", "DynamicPropertyName_GetDynamicPropertyFromAccount")] + [InlineData("DynamicSingleCustomer/Order/DynamicPropertyName", "DynamicPropertyName_GetDynamicPropertyFromOrder")] + [InlineData("DynamicCustomers(1)/Id", "Id_1")] + public async Task AccessPropertyTest(string uri, string expected) + { + string requestUri = $"odata/{uri}"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); + + HttpResponseMessage response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(expected, await response.Content.ReadAsStringAsync()); + } + + [Theory] + [InlineData("Name")] + [InlineData("Age")] + [InlineData("Order")] + [InlineData("Account")] + [InlineData("SecondAccount")] + public async Task AccessNormalPropertyWithGenericRoute(string property) + { + // Arrange + string requestUri = $"odata/DynamicCustomers(9)/{property}"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains($"GetProperty_{property}_9", await response.Content.ReadAsStringAsync()); + } + + [Theory] + [InlineData("Get")] + [InlineData("Patch")] + [InlineData("Delete")] + public async Task AccessDynamicPropertyWithOtherMethodsTest(string method) + { + // Arrange + string requestUri = "odata/DynamicCustomers(9)/DynamicPropertyName"; + + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), requestUri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("DynamicPropertyName_GetDynamicProperty_9", await response.Content.ReadAsStringAsync()); + } + + [Theory] + [InlineData("Put")] + [InlineData("Post")] + public async Task AccessDynamicPropertyWithWrongMethodReturnsMethodNotAllowed(string method) + { + // Arranget + string requestUri = "odata/DynamicCustomers(1)/DynamicPropertyName"; + + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), requestUri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); + } + + [Theory] + [InlineData("DynamicCustomers(2)/SecondAccount/DynamicPropertyName")] + [InlineData("DynamicSingleCustomer/SecondAccount/DynamicPropertyName")] + public async Task AccessDynamicPropertyWithoutImplementMethod(string uri) + { + // Arrange + string requestUri = $"odata/{uri}"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + private static IEdmModel GetEdmModel() { - public DynamicPropertiesTest(WebApiTestFixture fixture) - :base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(DynamicCustomersController), typeof(DynamicSingleCustomerController)/*, typeof(ODataEndpointController)*/); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel())); - } - - //[Fact] - // If test the routes, enable this test and see the payload. Remember to include the ODataEndpointController. - //public async Task TestRoutes() - //{ - // // Arrange - // string requestUri = "$odata"; - // HttpClient client = CreateClient(); - - // // Act - // var response = await client.GetAsync(requestUri); - - // // Assert - // response.EnsureSuccessStatusCode(); - // string payload = await response.Content.ReadAsStringAsync(); - //} - - [Theory] - [InlineData("DynamicCustomers(1)/DynamicPropertyName", "DynamicPropertyName_GetDynamicProperty_1")] - [InlineData("DynamicCustomers(2)/Account/DynamicPropertyName", "DynamicPropertyName_GetDynamicPropertyFromAccount_2")] - [InlineData("DynamicCustomers(3)/Order/DynamicPropertyName", "DynamicPropertyName_GetDynamicPropertyFromOrder_3")] - [InlineData("DynamicCustomers(4)/Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties.DynamicVipCustomer/DynamicPropertyName", "DynamicPropertyName_GetDynamicProperty_4")] - [InlineData("DynamicCustomers(5)/Microsoft.AspNetCore.OData.E2E.Tests.DynamicProperties.DynamicVipCustomer/Account/DynamicPropertyName", "DynamicPropertyName_GetDynamicPropertyFromAccount_5")] - [InlineData("DynamicSingleCustomer/DynamicPropertyName", "DynamicPropertyName_GetDynamicProperty")] - [InlineData("DynamicSingleCustomer/Account/DynamicPropertyName", "DynamicPropertyName_GetDynamicPropertyFromAccount")] - [InlineData("DynamicSingleCustomer/Order/DynamicPropertyName", "DynamicPropertyName_GetDynamicPropertyFromOrder")] - [InlineData("DynamicCustomers(1)/Id", "Id_1")] - public async Task AccessPropertyTest(string uri, string expected) - { - string requestUri = $"odata/{uri}"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); - - HttpResponseMessage response = await client.SendAsync(request); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Contains(expected, await response.Content.ReadAsStringAsync()); - } - - [Theory] - [InlineData("Name")] - [InlineData("Age")] - [InlineData("Order")] - [InlineData("Account")] - [InlineData("SecondAccount")] - public async Task AccessNormalPropertyWithGenericRoute(string property) - { - // Arrange - string requestUri = $"odata/DynamicCustomers(9)/{property}"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Contains($"GetProperty_{property}_9", await response.Content.ReadAsStringAsync()); - } - - [Theory] - [InlineData("Get")] - [InlineData("Patch")] - [InlineData("Delete")] - public async Task AccessDynamicPropertyWithOtherMethodsTest(string method) - { - // Arrange - string requestUri = "odata/DynamicCustomers(9)/DynamicPropertyName"; - - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), requestUri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Contains("DynamicPropertyName_GetDynamicProperty_9", await response.Content.ReadAsStringAsync()); - } - - [Theory] - [InlineData("Put")] - [InlineData("Post")] - public async Task AccessDynamicPropertyWithWrongMethodReturnsMethodNotAllowed(string method) - { - // Arranget - string requestUri = "odata/DynamicCustomers(1)/DynamicPropertyName"; - - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), requestUri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); - } - - [Theory] - [InlineData("DynamicCustomers(2)/SecondAccount/DynamicPropertyName")] - [InlineData("DynamicSingleCustomer/SecondAccount/DynamicPropertyName")] - public async Task AccessDynamicPropertyWithoutImplementMethod(string uri) - { - // Arrange - string requestUri = $"odata/{uri}"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("DynamicCustomers"); - builder.Singleton("DynamicSingleCustomer"); - return builder.GetEdmModel(); - } + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("DynamicCustomers"); + builder.Singleton("DynamicSingleCustomer"); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/CRUDWithIfMatchETagsTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/CRUDWithIfMatchETagsTest.cs index e6e555746..f78a32f9f 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/CRUDWithIfMatchETagsTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/CRUDWithIfMatchETagsTest.cs @@ -18,207 +18,206 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags +namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags; + +public class CRUDWithIfMatchETagsTest : WebApiTestBase { - public class CRUDWithIfMatchETagsTest : WebApiTestBase + public CRUDWithIfMatchETagsTest(WebApiTestFixture fixture) + :base(fixture) { - public CRUDWithIfMatchETagsTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = GetEdmModel(); - services.ConfigureControllers(typeof(ETagsCustomersController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel)); - services.AddControllers(opt => opt.Filters.Add(new ETagActionFilterAttribute())); - } + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = GetEdmModel(); + services.ConfigureControllers(typeof(ETagsCustomersController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel)); + services.AddControllers(opt => opt.Filters.Add(new ETagActionFilterAttribute())); + } - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - EntitySetConfiguration eTagsCustomersSet = builder.EntitySet("ETagsCustomers"); - EntityTypeConfiguration eTagsCustomers = eTagsCustomersSet.EntityType; - eTagsCustomers.Property(c => c.Id).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.Name).IsConcurrencyToken(); - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + EntitySetConfiguration eTagsCustomersSet = builder.EntitySet("ETagsCustomers"); + EntityTypeConfiguration eTagsCustomers = eTagsCustomersSet.EntityType; + eTagsCustomers.Property(c => c.Id).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.Name).IsConcurrencyToken(); + return builder.GetEdmModel(); + } - // Be noted, the id=0 is for "DeletedUpdated", don't change it otherwise there will be a conflict with other tests. - [Fact] - public async Task DeleteUpdatedEntityWithIfMatchShouldReturnPreconditionFailed() - { - // Arrange - string eTag; - HttpClient client = CreateClient(); - - // Act - 1 - var getUri = "odata/ETagsCustomers?$format=json"; - using (HttpResponseMessage getResponse = await client.GetAsync(getUri)) - { - // Assert - 1 - Assert.True(getResponse.IsSuccessStatusCode); - var json = await getResponse.Content.ReadAsObject(); - var result = json.GetValue("value") as JArray; - - eTag = result[0]["@odata.etag"].ToString(); - Assert.False(string.IsNullOrEmpty(eTag)); - } - - // Act - 2 - var putUri = "odata/ETagsCustomers(0)"; - var putContent = JObject.Parse(string.Format(@"{{""@odata.type"":""#{0}"",""Id"":{1},""Name"":""{2}"",""Notes"":[""{3}""]}}", typeof(ETagsCustomer), 0, "Customer Name 0 updated", "This is note 0 updated")); - using (HttpResponseMessage response = await client.PutAsJsonAsync(putUri, putContent)) - { - // Assert - 2 - response.EnsureSuccessStatusCode(); - } - - // Act - 3 - var deleteUri = "odata/ETagsCustomers(0)"; - var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, deleteUri); - deleteRequest.Headers.IfMatch.ParseAdd(eTag); - using (HttpResponseMessage response = await client.SendAsync(deleteRequest)) - { - // Assert -3 - Assert.Equal(HttpStatusCode.PreconditionFailed, response.StatusCode); - } - } + // Be noted, the id=0 is for "DeletedUpdated", don't change it otherwise there will be a conflict with other tests. + [Fact] + public async Task DeleteUpdatedEntityWithIfMatchShouldReturnPreconditionFailed() + { + // Arrange + string eTag; + HttpClient client = CreateClient(); - // Be noted, the id=1 is for "PutUpdated", don't change it otherwise there will be a conflict with other tests. - [Fact] - public async Task PutUpdatedEntityWithIfMatchShouldReturnPreconditionFailed() + // Act - 1 + var getUri = "odata/ETagsCustomers?$format=json"; + using (HttpResponseMessage getResponse = await client.GetAsync(getUri)) { - // Arrange - 1 - string requestUri = "odata/ETagsCustomers(1)?$format=json"; - HttpClient client = CreateClient(); - - // Act - 1 - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpResponseMessage response = await client.SendAsync(request); - // Assert - 1 - Assert.True(response.IsSuccessStatusCode); - - JObject result = await response.Content.ReadAsObject(); - var etagInHeader = response.Headers.ETag.ToString(); - var etagInPayload = (string)result["@odata.etag"]; - Assert.True(etagInPayload == etagInHeader, - string.Format("The etag value in payload is not the same as the one in Header, in payload it is: {0}, but in header, {1}.", etagInPayload, etagInHeader)); + Assert.True(getResponse.IsSuccessStatusCode); + var json = await getResponse.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; - // Arrange - 2 - requestUri = "odata/ETagsCustomers(1)"; - request = new HttpRequestMessage(HttpMethod.Put, requestUri); - string payload = string.Format(@"{{""@odata.type"":""#{0}"",""Id"":{1},""Name"":""{2}"",""Notes"":[""{3}""]}}", typeof(ETagsCustomer), 1, "Customer Name 1 updated", "This is note 1 updated"); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - - // Act - 2 - response = await client.SendAsync(request); + eTag = result[0]["@odata.etag"].ToString(); + Assert.False(string.IsNullOrEmpty(eTag)); + } + // Act - 2 + var putUri = "odata/ETagsCustomers(0)"; + var putContent = JObject.Parse(string.Format(@"{{""@odata.type"":""#{0}"",""Id"":{1},""Name"":""{2}"",""Notes"":[""{3}""]}}", typeof(ETagsCustomer), 0, "Customer Name 0 updated", "This is note 0 updated")); + using (HttpResponseMessage response = await client.PutAsJsonAsync(putUri, putContent)) + { // Assert - 2 - Assert.True(response.IsSuccessStatusCode); - - // Arrange - 3 - requestUri = "odata/ETagsCustomers(1)"; - request = new HttpRequestMessage(HttpMethod.Put, requestUri); - payload = string.Format(@"{{""@odata.type"":""#{0}"",""Id"":{1},""Name"":""{2}"",""Notes"":[""{3}""]}}", typeof(ETagsCustomer), 1, "Customer Name 1 updated again", "This is note 1 updated again"); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Headers.IfMatch.ParseAdd(etagInPayload); - - // Act - 3 - response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + } - // Assert - 3 + // Act - 3 + var deleteUri = "odata/ETagsCustomers(0)"; + var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, deleteUri); + deleteRequest.Headers.IfMatch.ParseAdd(eTag); + using (HttpResponseMessage response = await client.SendAsync(deleteRequest)) + { + // Assert -3 Assert.Equal(HttpStatusCode.PreconditionFailed, response.StatusCode); } + } - // Be noted, the id=2 is for "PatchUpdated", don't change it otherwise there will be a conflict with other tests. - [Fact] - public async Task PatchUpdatedEntityWithIfMatchShouldReturnPreconditionFailed() - { - // Arrange (1) - string requestUri = "odata/ETagsCustomers(2)?$format=json"; - HttpClient client = CreateClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + // Be noted, the id=1 is for "PutUpdated", don't change it otherwise there will be a conflict with other tests. + [Fact] + public async Task PutUpdatedEntityWithIfMatchShouldReturnPreconditionFailed() + { + // Arrange - 1 + string requestUri = "odata/ETagsCustomers(1)?$format=json"; + HttpClient client = CreateClient(); + + // Act - 1 + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert - 1 + Assert.True(response.IsSuccessStatusCode); + + JObject result = await response.Content.ReadAsObject(); + var etagInHeader = response.Headers.ETag.ToString(); + var etagInPayload = (string)result["@odata.etag"]; + Assert.True(etagInPayload == etagInHeader, + string.Format("The etag value in payload is not the same as the one in Header, in payload it is: {0}, but in header, {1}.", etagInPayload, etagInHeader)); + + // Arrange - 2 + requestUri = "odata/ETagsCustomers(1)"; + request = new HttpRequestMessage(HttpMethod.Put, requestUri); + string payload = string.Format(@"{{""@odata.type"":""#{0}"",""Id"":{1},""Name"":""{2}"",""Notes"":[""{3}""]}}", typeof(ETagsCustomer), 1, "Customer Name 1 updated", "This is note 1 updated"); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + // Act - 2 + response = await client.SendAsync(request); + + // Assert - 2 + Assert.True(response.IsSuccessStatusCode); + + // Arrange - 3 + requestUri = "odata/ETagsCustomers(1)"; + request = new HttpRequestMessage(HttpMethod.Put, requestUri); + payload = string.Format(@"{{""@odata.type"":""#{0}"",""Id"":{1},""Name"":""{2}"",""Notes"":[""{3}""]}}", typeof(ETagsCustomer), 1, "Customer Name 1 updated again", "This is note 1 updated again"); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Headers.IfMatch.ParseAdd(etagInPayload); + + // Act - 3 + response = await client.SendAsync(request); + + // Assert - 3 + Assert.Equal(HttpStatusCode.PreconditionFailed, response.StatusCode); + } - // Act (1) - HttpResponseMessage response = await client.SendAsync(request); + // Be noted, the id=2 is for "PatchUpdated", don't change it otherwise there will be a conflict with other tests. + [Fact] + public async Task PatchUpdatedEntityWithIfMatchShouldReturnPreconditionFailed() + { + // Arrange (1) + string requestUri = "odata/ETagsCustomers(2)?$format=json"; + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act (1) + HttpResponseMessage response = await client.SendAsync(request); + + // Assert (1) + Assert.True(response.IsSuccessStatusCode); + var etagInHeader = response.Headers.ETag.ToString(); + JObject result = await response.Content.ReadAsObject(); + var etagInPayload = (string)result["@odata.etag"]; + Assert.True(etagInPayload == etagInHeader, + string.Format("The etag value in payload is not the same as the one in Header, in payload it is: {0}, but in header, {1}.", etagInPayload, etagInHeader)); + + // Arrange (2) + requestUri = "odata/ETagsCustomers(2)"; + request = new HttpRequestMessage(HttpMethod.Put, requestUri); + string payload = string.Format(@"{{""@odata.type"":""#{0}"",""Id"":{1},""Name"":""{2}"",""Notes"":[""{3}""]}}", typeof(ETagsCustomer), 2, "Customer Name 2 updated", "This is note 2 updated"); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + // Act (2) + response = await client.SendAsync(request); + + // Assert (2) + Assert.True(response.IsSuccessStatusCode); + + // Arrange (3) + requestUri = "odata/ETagsCustomers(2)"; + request = new HttpRequestMessage(new HttpMethod("Patch"), requestUri); + payload = string.Format(@"{{""@odata.type"":""#{0}"",""Id"":{1},""Name"":""{2}""}}", typeof(ETagsCustomer), 2, "Customer Name 2 updated again"); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Headers.IfMatch.ParseAdd(etagInPayload); + + // Act (3) + response = await client.SendAsync(request); + + // Assert (3) + Assert.Equal(HttpStatusCode.PreconditionFailed, response.StatusCode); + } - // Assert (1) - Assert.True(response.IsSuccessStatusCode); - var etagInHeader = response.Headers.ETag.ToString(); - JObject result = await response.Content.ReadAsObject(); - var etagInPayload = (string)result["@odata.etag"]; - Assert.True(etagInPayload == etagInHeader, - string.Format("The etag value in payload is not the same as the one in Header, in payload it is: {0}, but in header, {1}.", etagInPayload, etagInHeader)); - - // Arrange (2) - requestUri = "odata/ETagsCustomers(2)"; - request = new HttpRequestMessage(HttpMethod.Put, requestUri); - string payload = string.Format(@"{{""@odata.type"":""#{0}"",""Id"":{1},""Name"":""{2}"",""Notes"":[""{3}""]}}", typeof(ETagsCustomer), 2, "Customer Name 2 updated", "This is note 2 updated"); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - - // Act (2) - response = await client.SendAsync(request); - - // Assert (2) + [Fact] + public async Task GetEntityWithIfNoneMatchShouldReturnNotModifiedETagsTest() + { + // Arrange + HttpClient client = CreateClient(); + string eTag; + + // DeleteUpdatedEntryWithIfMatchETagsTests will change #"0" customer + // PutUpdatedEntryWithIfMatchETagsTests will change #"1"customer + // PatchUpdatedEntryWithIfMatchETagsTest will change #"2" customer + // So, this case uses "4" + int customerId = 4; + var getUri = "odata/ETagsCustomers?$format=json"; + + // Act + using (var response = await client.GetAsync(getUri)) + { + // Assert Assert.True(response.IsSuccessStatusCode); - // Arrange (3) - requestUri = "odata/ETagsCustomers(2)"; - request = new HttpRequestMessage(new HttpMethod("Patch"), requestUri); - payload = string.Format(@"{{""@odata.type"":""#{0}"",""Id"":{1},""Name"":""{2}""}}", typeof(ETagsCustomer), 2, "Customer Name 2 updated again"); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Headers.IfMatch.ParseAdd(etagInPayload); + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + Assert.NotNull(result); - // Act (3) - response = await client.SendAsync(request); - - // Assert (3) - Assert.Equal(HttpStatusCode.PreconditionFailed, response.StatusCode); + eTag = result[customerId]["@odata.etag"].ToString(); + Assert.False(string.IsNullOrEmpty(eTag)); } - [Fact] - public async Task GetEntityWithIfNoneMatchShouldReturnNotModifiedETagsTest() + // Arrange & Act + var getRequestWithEtag = new HttpRequestMessage(HttpMethod.Get, $"odata/ETagsCustomers({customerId})"); + getRequestWithEtag.Headers.IfNoneMatch.ParseAdd(eTag); + using (var response = await client.SendAsync(getRequestWithEtag)) { - // Arrange - HttpClient client = CreateClient(); - string eTag; - - // DeleteUpdatedEntryWithIfMatchETagsTests will change #"0" customer - // PutUpdatedEntryWithIfMatchETagsTests will change #"1"customer - // PatchUpdatedEntryWithIfMatchETagsTest will change #"2" customer - // So, this case uses "4" - int customerId = 4; - var getUri = "odata/ETagsCustomers?$format=json"; - - // Act - using (var response = await client.GetAsync(getUri)) - { - // Assert - Assert.True(response.IsSuccessStatusCode); - - var json = await response.Content.ReadAsObject(); - var result = json.GetValue("value") as JArray; - Assert.NotNull(result); - - eTag = result[customerId]["@odata.etag"].ToString(); - Assert.False(string.IsNullOrEmpty(eTag)); - } - - // Arrange & Act - var getRequestWithEtag = new HttpRequestMessage(HttpMethod.Get, $"odata/ETagsCustomers({customerId})"); - getRequestWithEtag.Headers.IfNoneMatch.ParseAdd(eTag); - using (var response = await client.SendAsync(getRequestWithEtag)) - { - // Assert - Assert.Equal(HttpStatusCode.NotModified, response.StatusCode); - } + // Assert + Assert.Equal(HttpStatusCode.NotModified, response.StatusCode); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/DerivedETagTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/DerivedETagTests.cs index 83a11a9a2..a6f1991c8 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/DerivedETagTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/DerivedETagTests.cs @@ -18,136 +18,135 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags +namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags; + +public class DerivedETagTests : WebApiTestBase { - public class DerivedETagTests : WebApiTestBase + public DerivedETagTests(WebApiTestFixture fixture) + :base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel1 = GetEdmModel(); + IEdmModel edmModel2 = GetDerivedEdmModel(); + services.ConfigureControllers(typeof(ETagsCustomersController), typeof(ETagsDerivedCustomersSingletonController), typeof(ETagsDerivedCustomersController)); + services.AddControllers().AddOData(opt => opt.Select().AddRouteComponents("odata", edmModel1).AddRouteComponents("derivedEtag", edmModel2)); + services.AddControllers(opt => opt.Filters.Add(new ETagActionFilterAttribute())); + } + + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + EntitySetConfiguration eTagsCustomersSet = builder.EntitySet("ETagsCustomers"); + eTagsCustomersSet.HasRequiredBinding(c => c.RelatedCustomer, eTagsCustomersSet); + eTagsCustomersSet.HasRequiredBinding(c => c.ContainedCustomer, eTagsCustomersSet); + EntitySetConfiguration eTagsDerivedCustomersSet = builder.EntitySet("ETagsDerivedCustomers"); + eTagsDerivedCustomersSet.HasRequiredBinding(c => c.RelatedCustomer, eTagsCustomersSet); + eTagsDerivedCustomersSet.HasRequiredBinding(c => c.ContainedCustomer, eTagsCustomersSet); + SingletonConfiguration eTagsCustomerSingleton = builder.Singleton("ETagsDerivedCustomersSingleton"); + eTagsCustomerSingleton.HasRequiredBinding(c => c.RelatedCustomer, eTagsCustomersSet); + eTagsCustomerSingleton.HasRequiredBinding(c => c.ContainedCustomer, eTagsCustomersSet); + return builder.GetEdmModel(); + } + + private static IEdmModel GetDerivedEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + EntitySetConfiguration eTagsCustomersSet = builder.EntitySet("ETagsCustomers"); + eTagsCustomersSet.HasRequiredBinding(c => c.RelatedCustomer, eTagsCustomersSet); + eTagsCustomersSet.HasRequiredBinding(c => c.ContainedCustomer, eTagsCustomersSet); + EntitySetConfiguration eTagsDerivedCustomersSet = builder.EntitySet("ETagsDerivedCustomers"); + eTagsDerivedCustomersSet.HasRequiredBinding(c => c.RelatedCustomer, eTagsCustomersSet); + eTagsDerivedCustomersSet.HasRequiredBinding(c => c.ContainedCustomer, eTagsCustomersSet); + return builder.GetEdmModel(); + } + + [Fact] + public async Task DerivedTypesHaveSameETagsTest() { - public DerivedETagTests(WebApiTestFixture fixture) - :base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel1 = GetEdmModel(); - IEdmModel edmModel2 = GetDerivedEdmModel(); - services.ConfigureControllers(typeof(ETagsCustomersController), typeof(ETagsDerivedCustomersSingletonController), typeof(ETagsDerivedCustomersController)); - services.AddControllers().AddOData(opt => opt.Select().AddRouteComponents("odata", edmModel1).AddRouteComponents("derivedEtag", edmModel2)); - services.AddControllers(opt => opt.Filters.Add(new ETagActionFilterAttribute())); - } - - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - EntitySetConfiguration eTagsCustomersSet = builder.EntitySet("ETagsCustomers"); - eTagsCustomersSet.HasRequiredBinding(c => c.RelatedCustomer, eTagsCustomersSet); - eTagsCustomersSet.HasRequiredBinding(c => c.ContainedCustomer, eTagsCustomersSet); - EntitySetConfiguration eTagsDerivedCustomersSet = builder.EntitySet("ETagsDerivedCustomers"); - eTagsDerivedCustomersSet.HasRequiredBinding(c => c.RelatedCustomer, eTagsCustomersSet); - eTagsDerivedCustomersSet.HasRequiredBinding(c => c.ContainedCustomer, eTagsCustomersSet); - SingletonConfiguration eTagsCustomerSingleton = builder.Singleton("ETagsDerivedCustomersSingleton"); - eTagsCustomerSingleton.HasRequiredBinding(c => c.RelatedCustomer, eTagsCustomersSet); - eTagsCustomerSingleton.HasRequiredBinding(c => c.ContainedCustomer, eTagsCustomersSet); - return builder.GetEdmModel(); - } - - private static IEdmModel GetDerivedEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - EntitySetConfiguration eTagsCustomersSet = builder.EntitySet("ETagsCustomers"); - eTagsCustomersSet.HasRequiredBinding(c => c.RelatedCustomer, eTagsCustomersSet); - eTagsCustomersSet.HasRequiredBinding(c => c.ContainedCustomer, eTagsCustomersSet); - EntitySetConfiguration eTagsDerivedCustomersSet = builder.EntitySet("ETagsDerivedCustomers"); - eTagsDerivedCustomersSet.HasRequiredBinding(c => c.RelatedCustomer, eTagsCustomersSet); - eTagsDerivedCustomersSet.HasRequiredBinding(c => c.ContainedCustomer, eTagsCustomersSet); - return builder.GetEdmModel(); - } - - [Fact] - public async Task DerivedTypesHaveSameETagsTest() - { - // Arrange - Base - HttpClient client = CreateClient(); - string requestUri = "odata/ETagsCustomers?$select=Id"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.ParseAdd("application/json"); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - var jsonResult = await response.Content.ReadAsObject(); - var jsonETags = jsonResult.GetValue("value").Select(e => e["@odata.etag"].ToString()); - - // Arrange - Derived - requestUri = "odata/ETagsDerivedCustomers?$select=Id"; - request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.ParseAdd("application/json"); - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - jsonResult = await response.Content.ReadAsObject(); - var derivedEtags = jsonResult.GetValue("value").Select(e => e["@odata.etag"].ToString()); + // Arrange - Base + HttpClient client = CreateClient(); + string requestUri = "odata/ETagsCustomers?$select=Id"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.ParseAdd("application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var jsonResult = await response.Content.ReadAsObject(); + var jsonETags = jsonResult.GetValue("value").Select(e => e["@odata.etag"].ToString()); + + // Arrange - Derived + requestUri = "odata/ETagsDerivedCustomers?$select=Id"; + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.ParseAdd("application/json"); + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + jsonResult = await response.Content.ReadAsObject(); + var derivedEtags = jsonResult.GetValue("value").Select(e => e["@odata.etag"].ToString()); - Assert.True(String.Concat(jsonETags) == String.Concat(derivedEtags), "Derived Types has different etags than base type"); - } - - [Fact] - public async Task SingletonsHaveSameETagsTest() - { - // Arrange - Base - HttpClient client = CreateClient(); - - string requestUri = "odata/ETagsCustomers?$select=Id"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.ParseAdd("application/json"); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - var jsonResult = await response.Content.ReadAsObject(); - var jsonETags = jsonResult.GetValue("value").Select(e => e["@odata.etag"].ToString()); - - // Arrange - Derived - requestUri = "odata/ETagsDerivedCustomersSingleton?$select=Id"; - request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.ParseAdd("application/json"); - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - jsonResult = await response.Content.ReadAsObject(); - var singletonEtag = jsonResult.GetValue("@odata.etag").ToString(); - - Assert.True(jsonETags.FirstOrDefault() == singletonEtag, "Singleton has different etags than Set"); - } - - [Fact] - public async Task DerivedEntitySetsHaveETagsTest() - { - // Arrange - HttpClient client = CreateClient(); - string requestUri = "derivedEtag/ETagsDerivedCustomers?$select=Id"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.ParseAdd("application/json"); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - var jsonResult = await response.Content.ReadAsObject(); - var derivedEtags = jsonResult.GetValue("value").Select(e => e["@odata.etag"].ToString()); - Assert.Equal(10, derivedEtags.Count()); - Assert.Equal("W/\"bnVsbA==\"", derivedEtags.First()); - } + Assert.True(String.Concat(jsonETags) == String.Concat(derivedEtags), "Derived Types has different etags than base type"); + } + + [Fact] + public async Task SingletonsHaveSameETagsTest() + { + // Arrange - Base + HttpClient client = CreateClient(); + + string requestUri = "odata/ETagsCustomers?$select=Id"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.ParseAdd("application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var jsonResult = await response.Content.ReadAsObject(); + var jsonETags = jsonResult.GetValue("value").Select(e => e["@odata.etag"].ToString()); + + // Arrange - Derived + requestUri = "odata/ETagsDerivedCustomersSingleton?$select=Id"; + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.ParseAdd("application/json"); + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + jsonResult = await response.Content.ReadAsObject(); + var singletonEtag = jsonResult.GetValue("@odata.etag").ToString(); + + Assert.True(jsonETags.FirstOrDefault() == singletonEtag, "Singleton has different etags than Set"); + } + + [Fact] + public async Task DerivedEntitySetsHaveETagsTest() + { + // Arrange + HttpClient client = CreateClient(); + string requestUri = "derivedEtag/ETagsDerivedCustomers?$select=Id"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.ParseAdd("application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var jsonResult = await response.Content.ReadAsObject(); + var derivedEtags = jsonResult.GetValue("value").Select(e => e["@odata.etag"].ToString()); + Assert.Equal(10, derivedEtags.Count()); + Assert.Equal("W/\"bnVsbA==\"", derivedEtags.First()); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/DominiosController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/DominiosController.cs index 5f1680327..a566fc770 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/DominiosController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/DominiosController.cs @@ -9,22 +9,21 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags.EFCore +namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags.EFCore; + +public class DominiosController : ODataController { - public class DominiosController : ODataController - { - private ETagCurrencyTokenEfContext _db; + private ETagCurrencyTokenEfContext _db; - public DominiosController(ETagCurrencyTokenEfContext context) - { - _db = context; - ETagCurrencyTokenEfContextInitializer.Seed(_db); - } + public DominiosController(ETagCurrencyTokenEfContext context) + { + _db = context; + ETagCurrencyTokenEfContextInitializer.Seed(_db); + } - [EnableQuery] - public IActionResult Get() - { - return Ok(_db.Dominios); - } + [EnableQuery] + public IActionResult Get() + { + return Ok(_db.Dominios); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/ETagCurrencyTokenEfContext.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/ETagCurrencyTokenEfContext.cs index cf2d3aa35..d14dda7bf 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/ETagCurrencyTokenEfContext.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/ETagCurrencyTokenEfContext.cs @@ -9,53 +9,52 @@ using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags.EFCore +namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags.EFCore; + +public class ETagCurrencyTokenEfContext : DbContext { - public class ETagCurrencyTokenEfContext : DbContext + public ETagCurrencyTokenEfContext(DbContextOptions options) + : base(options) { - public ETagCurrencyTokenEfContext(DbContextOptions options) - : base(options) - { - } + } - public DbSet Dominios { get; set; } + public DbSet Dominios { get; set; } - public DbSet Servers { get; set; } - } + public DbSet Servers { get; set; } +} - [Table("Domini")] - public class Dominio - { - [Key] - [StringLength(50)] - public string Id { get; set; } +[Table("Domini")] +public class Dominio +{ + [Key] + [StringLength(50)] + public string Id { get; set; } - [StringLength(200)] - public string Descrizione { get; set; } + [StringLength(200)] + public string Descrizione { get; set; } - public string ServerAutenticazioneId { get; set; } + public string ServerAutenticazioneId { get; set; } - [ForeignKey("ServerAutenticazioneId")] - public virtual Server ServerAutenticazione { get; set; } + [ForeignKey("ServerAutenticazioneId")] + public virtual Server ServerAutenticazione { get; set; } - [ConcurrencyCheck] - public int? RECVER { get; set; } - } + [ConcurrencyCheck] + public int? RECVER { get; set; } +} - [Table("Servers")] - public class Server - { - [Key] - [StringLength(50)] - public string Id { get; set; } +[Table("Servers")] +public class Server +{ + [Key] + [StringLength(50)] + public string Id { get; set; } - [StringLength(200)] - public string Descrizione { get; set; } + [StringLength(200)] + public string Descrizione { get; set; } - [StringLength(2000)] - public string Url { get; set; } + [StringLength(2000)] + public string Url { get; set; } - [ConcurrencyCheck] - public int? RECVER { get; set; } - } + [ConcurrencyCheck] + public int? RECVER { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/ETagCurrencyTokenEfContextInitializer.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/ETagCurrencyTokenEfContextInitializer.cs index 699ad792a..c6c435016 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/ETagCurrencyTokenEfContextInitializer.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/ETagCurrencyTokenEfContextInitializer.cs @@ -9,112 +9,111 @@ using System.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags.EFCore +namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags.EFCore; + +public class ETagCurrencyTokenEfContextInitializer { - public class ETagCurrencyTokenEfContextInitializer - { - private static IEnumerable _dominios; - private static IEnumerable _servers; + private static IEnumerable _dominios; + private static IEnumerable _servers; - private static IEnumerable Dominios + private static IEnumerable Dominios + { + get { - get + if (_dominios == null) { - if (_dominios == null) - { - Generate(); - } - - Assert.NotNull(_dominios); - return _dominios; + Generate(); } + + Assert.NotNull(_dominios); + return _dominios; } + } - private static IEnumerable Servers + private static IEnumerable Servers + { + get { - get + if (_servers == null) { - if (_servers == null) - { - Generate(); - } - - Assert.NotNull(_servers); - return _servers; + Generate(); } + + Assert.NotNull(_servers); + return _servers; } + } - private static void Generate() + private static void Generate() + { + if (_dominios != null || _servers != null) { - if (_dominios != null || _servers != null) - { - return; - } - - List servers = new List(2); - Server server1 = new Server - { - Id = "1", - Descrizione = "Server 1", - Url = "http://server1", - RECVER = null - }; - servers.Add(server1); - - Server server2 = new Server - { - Id = "2", - Descrizione = "Server 2", - Url = "http://server2", - RECVER = 5 - }; - servers.Add(server2); - _servers = servers; - - List dominios = new List(2); - Dominio do1 = new Dominio - { - Id = "1", - ServerAutenticazione = server1, - Descrizione = "Test1", - RECVER = null, - ServerAutenticazioneId = "1", - }; - dominios.Add(do1); - - Dominio do2 = new Dominio - { - Id = "2", - ServerAutenticazione = server2, - Descrizione = "Test2", - RECVER = 10, - ServerAutenticazioneId = "2", - }; - - dominios.Add(do2); - _dominios = dominios; + return; } - public static void Seed(ETagCurrencyTokenEfContext context) + List servers = new List(2); + Server server1 = new Server { - context.Database.EnsureCreated(); + Id = "1", + Descrizione = "Server 1", + Url = "http://server1", + RECVER = null + }; + servers.Add(server1); + + Server server2 = new Server + { + Id = "2", + Descrizione = "Server 2", + Url = "http://server2", + RECVER = 5 + }; + servers.Add(server2); + _servers = servers; + + List dominios = new List(2); + Dominio do1 = new Dominio + { + Id = "1", + ServerAutenticazione = server1, + Descrizione = "Test1", + RECVER = null, + ServerAutenticazioneId = "1", + }; + dominios.Add(do1); + + Dominio do2 = new Dominio + { + Id = "2", + ServerAutenticazione = server2, + Descrizione = "Test2", + RECVER = 10, + ServerAutenticazioneId = "2", + }; + + dominios.Add(do2); + _dominios = dominios; + } - if (!context.Dominios.Any()) - { - Generate(); + public static void Seed(ETagCurrencyTokenEfContext context) + { + context.Database.EnsureCreated(); - foreach (var d in Dominios) - { - context.Dominios.Add(d); - } + if (!context.Dominios.Any()) + { + Generate(); - foreach (var s in Servers) - { - context.Servers.Add(s); - } + foreach (var d in Dominios) + { + context.Dominios.Add(d); + } - context.SaveChanges(); + foreach (var s in Servers) + { + context.Servers.Add(s); } + + context.SaveChanges(); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/ETagCurrencyTokenEfContextTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/ETagCurrencyTokenEfContextTest.cs index e148cc767..72a60421f 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/ETagCurrencyTokenEfContextTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/EFCore/ETagCurrencyTokenEfContextTest.cs @@ -15,37 +15,37 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags.EFCore +namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags.EFCore; + +public class ETagCurrencyTokenEfContextTest : WebApiTestBase { - public class ETagCurrencyTokenEfContextTest : WebApiTestBase + public ETagCurrencyTokenEfContextTest(WebApiTestFixture fixture) + :base(fixture) { - public ETagCurrencyTokenEfContextTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=ETagCurrencyTokenEfContext8"; - services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); + protected static void UpdateConfigureServices(IServiceCollection services) + { + string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=ETagCurrencyTokenEfContext8"; + services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); - IEdmModel edmModel = GetEdmModel(); - services.ConfigureControllers(typeof(DominiosController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel).Expand().Select()); - } + IEdmModel edmModel = GetEdmModel(); + services.ConfigureControllers(typeof(DominiosController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel).Expand().Select()); + } - private static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Dominios"); - builder.EntitySet("Servers"); - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Dominios"); + builder.EntitySet("Servers"); + return builder.GetEdmModel(); + } - [Fact] - public async Task NestedDollarSelectWorksOnCurrencyTokenProperty() - { - string expect = "{\r\n" + + [Fact] + public async Task NestedDollarSelectWorksOnCurrencyTokenProperty() + { + string expect = "{\r\n" + " \"@odata.context\":\"http://localhost/odata/$metadata#Dominios(ServerAutenticazione(Id,RECVER))\",\"value\":[\r\n" + " {\r\n" + " \"@odata.etag\":\"W/\\\"bnVsbA==\\\"\",\"Id\":\"1\",\"Descrizione\":\"Test1\",\"ServerAutenticazioneId\":\"1\",\"RECVER\":null,\"ServerAutenticazione\":{\r\n" + @@ -58,21 +58,20 @@ public async Task NestedDollarSelectWorksOnCurrencyTokenProperty() " }\r\n" + " ]\r\n" + "}"; - // Remove indentation - expect = Regex.Replace(expect, @"\r\n\s*([""{}\]])", "$1"); + // Remove indentation + expect = Regex.Replace(expect, @"\r\n\s*([""{}\]])", "$1"); - var getUri = "odata/Dominios?$expand=ServerAutenticazione($select=Id,RECVER)"; - HttpClient client = CreateClient(); + var getUri = "odata/Dominios?$expand=ServerAutenticazione($select=Id,RECVER)"; + HttpClient client = CreateClient(); - var response = await client.GetAsync(getUri); + var response = await client.GetAsync(getUri); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - Assert.NotNull(response.Content); + Assert.NotNull(response.Content); - var payload = await response.Content.ReadAsStringAsync(); + var payload = await response.Content.ReadAsStringAsync(); - Assert.Equal(expect, payload); - } + Assert.Equal(expect, payload); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsController.cs index 6c70b6252..77b62489e 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsController.cs @@ -15,240 +15,239 @@ using Microsoft.AspNetCore.OData.Results; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags +namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags; + +// Be noted, some test cases updates the static "customers". +// So, there's some test cases using certain "id" to make sure all test cases can run in parallel. +public class ETagsDerivedCustomersController : ODataController { - // Be noted, some test cases updates the static "customers". - // So, there's some test cases using certain "id" to make sure all test cases can run in parallel. - public class ETagsDerivedCustomersController : ODataController - { - internal static IList customers = Enumerable.Range(0, 10).Select(i => Helpers.CreateCustomer(i)).ToList(); + internal static IList customers = Enumerable.Range(0, 10).Select(i => Helpers.CreateCustomer(i)).ToList(); - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - public IActionResult Get() - { - return Ok(customers.Select(c => Helpers.CreateDerivedCustomer(c))); - } + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public IActionResult Get() + { + return Ok(customers.Select(c => Helpers.CreateDerivedCustomer(c))); } +} + +public class ETagsDerivedCustomersSingletonController : ODataController +{ + internal static IList customers = Enumerable.Range(0, 10).Select(i => Helpers.CreateCustomer(i)).ToList(); - public class ETagsDerivedCustomersSingletonController : ODataController + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public IActionResult Get() { - internal static IList customers = Enumerable.Range(0, 10).Select(i => Helpers.CreateCustomer(i)).ToList(); + return Ok(customers.Select(c => Helpers.CreateDerivedCustomer(c)).FirstOrDefault()); + } +} - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - public IActionResult Get() - { - return Ok(customers.Select(c => Helpers.CreateDerivedCustomer(c)).FirstOrDefault()); - } +public class ETagsCustomersController : ODataController +{ + internal static IList customers = Enumerable.Range(0, 10).Select(i => Helpers.CreateCustomer(i)).ToList(); + + [HttpGet] + [EnableQuery] + public IActionResult Get() + { + return Ok(customers); } - public class ETagsCustomersController : ODataController + [HttpGet] + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public IActionResult Get(int key, ODataQueryOptions queryOptions) { - internal static IList customers = Enumerable.Range(0, 10).Select(i => Helpers.CreateCustomer(i)).ToList(); + IEnumerable appliedCustomers = customers.Where(c => c.Id == key); - [HttpGet] - [EnableQuery] - public IActionResult Get() + if (appliedCustomers.Count() == 0) { - return Ok(customers); + return BadRequest("The key is not valid"); } - [HttpGet] - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - public IActionResult Get(int key, ODataQueryOptions queryOptions) + if (queryOptions.IfNoneMatch != null) { - IEnumerable appliedCustomers = customers.Where(c => c.Id == key); - - if (appliedCustomers.Count() == 0) - { - return BadRequest("The key is not valid"); - } + appliedCustomers = queryOptions.IfNoneMatch.ApplyTo(appliedCustomers.AsQueryable()).Cast(); + } - if (queryOptions.IfNoneMatch != null) - { - appliedCustomers = queryOptions.IfNoneMatch.ApplyTo(appliedCustomers.AsQueryable()).Cast(); - } + if (appliedCustomers.Count() == 0) + { + return StatusCode(StatusCodes.Status304NotModified); + } + else + { + return Ok(new SingleResult(appliedCustomers.AsQueryable())); + } + } - if (appliedCustomers.Count() == 0) - { - return StatusCode(StatusCodes.Status304NotModified); - } - else - { - return Ok(new SingleResult(appliedCustomers.AsQueryable())); - } + [HttpPut] + public IActionResult Put(int key, [FromBody]ETagsCustomer eTagsCustomer, ODataQueryOptions queryOptions) + { + if (key != eTagsCustomer.Id) + { + return BadRequest("The Id of customer is not matched with the key"); } - [HttpPut] - public IActionResult Put(int key, [FromBody]ETagsCustomer eTagsCustomer, ODataQueryOptions queryOptions) + IEnumerable appliedCustomers = customers.Where(c => c.Id == eTagsCustomer.Id); + + if (appliedCustomers.Count() == 0) { - if (key != eTagsCustomer.Id) - { - return BadRequest("The Id of customer is not matched with the key"); - } + customers.Add(eTagsCustomer); + return Ok(eTagsCustomer); + } - IEnumerable appliedCustomers = customers.Where(c => c.Id == eTagsCustomer.Id); + if (queryOptions.IfMatch != null) + { + IQueryable ifMatchCustomers = queryOptions.IfMatch.ApplyTo(appliedCustomers.AsQueryable()).Cast(); - if (appliedCustomers.Count() == 0) + if (ifMatchCustomers.Count() == 0) { - customers.Add(eTagsCustomer); - return Ok(eTagsCustomer); + return StatusCode(StatusCodes.Status412PreconditionFailed); } + } - if (queryOptions.IfMatch != null) - { - IQueryable ifMatchCustomers = queryOptions.IfMatch.ApplyTo(appliedCustomers.AsQueryable()).Cast(); + ETagsCustomer customer = appliedCustomers.Single(); + customer.Name = eTagsCustomer.Name; + customer.Notes = eTagsCustomer.Notes; + customer.BoolProperty = eTagsCustomer.BoolProperty; + customer.ByteProperty = eTagsCustomer.ByteProperty; + customer.CharProperty = eTagsCustomer.CharProperty; + customer.DecimalProperty = eTagsCustomer.DecimalProperty; + customer.DoubleProperty = eTagsCustomer.DoubleProperty; + customer.ShortProperty = eTagsCustomer.ShortProperty; + customer.LongProperty = eTagsCustomer.LongProperty; + customer.SbyteProperty = eTagsCustomer.SbyteProperty; + customer.FloatProperty = eTagsCustomer.FloatProperty; + customer.UshortProperty = eTagsCustomer.UshortProperty; + customer.UintProperty = eTagsCustomer.UintProperty; + customer.UlongProperty = eTagsCustomer.UlongProperty; + customer.GuidProperty = eTagsCustomer.GuidProperty; + customer.DateTimeOffsetProperty = eTagsCustomer.DateTimeOffsetProperty; + + return Ok(customer); + } - if (ifMatchCustomers.Count() == 0) - { - return StatusCode(StatusCodes.Status412PreconditionFailed); - } - } + [HttpDelete] + public IActionResult Delete(int key, ODataQueryOptions queryOptions) + { + IEnumerable appliedCustomers = customers.Where(c => c.Id == key); - ETagsCustomer customer = appliedCustomers.Single(); - customer.Name = eTagsCustomer.Name; - customer.Notes = eTagsCustomer.Notes; - customer.BoolProperty = eTagsCustomer.BoolProperty; - customer.ByteProperty = eTagsCustomer.ByteProperty; - customer.CharProperty = eTagsCustomer.CharProperty; - customer.DecimalProperty = eTagsCustomer.DecimalProperty; - customer.DoubleProperty = eTagsCustomer.DoubleProperty; - customer.ShortProperty = eTagsCustomer.ShortProperty; - customer.LongProperty = eTagsCustomer.LongProperty; - customer.SbyteProperty = eTagsCustomer.SbyteProperty; - customer.FloatProperty = eTagsCustomer.FloatProperty; - customer.UshortProperty = eTagsCustomer.UshortProperty; - customer.UintProperty = eTagsCustomer.UintProperty; - customer.UlongProperty = eTagsCustomer.UlongProperty; - customer.GuidProperty = eTagsCustomer.GuidProperty; - customer.DateTimeOffsetProperty = eTagsCustomer.DateTimeOffsetProperty; - - return Ok(customer); + if (appliedCustomers.Count() == 0) + { + return BadRequest(string.Format("The entry with Id {0} doesn't exist", key)); } - [HttpDelete] - public IActionResult Delete(int key, ODataQueryOptions queryOptions) + if (queryOptions.IfMatch != null) { - IEnumerable appliedCustomers = customers.Where(c => c.Id == key); + IQueryable ifMatchCustomers = queryOptions.IfMatch.ApplyTo(appliedCustomers.AsQueryable()).Cast(); - if (appliedCustomers.Count() == 0) + if (ifMatchCustomers.Count() == 0) { - return BadRequest(string.Format("The entry with Id {0} doesn't exist", key)); + return StatusCode(StatusCodes.Status412PreconditionFailed); } + } - if (queryOptions.IfMatch != null) - { - IQueryable ifMatchCustomers = queryOptions.IfMatch.ApplyTo(appliedCustomers.AsQueryable()).Cast(); + ETagsCustomer customer = appliedCustomers.Single(); + customers.Remove(customer); + return Ok(customer); + } - if (ifMatchCustomers.Count() == 0) - { - return StatusCode(StatusCodes.Status412PreconditionFailed); - } - } + [HttpPatch] + public IActionResult Patch(int key, [FromBody]Delta patch, ODataQueryOptions queryOptions) + { + IEnumerable appliedCustomers = customers.Where(c => c.Id == key); - ETagsCustomer customer = appliedCustomers.Single(); - customers.Remove(customer); - return Ok(customer); + if (appliedCustomers.Count() == 0) + { + return BadRequest(string.Format("The entry with Id {0} doesn't exist", key)); } - [HttpPatch] - public IActionResult Patch(int key, [FromBody]Delta patch, ODataQueryOptions queryOptions) + if (queryOptions.IfMatch != null) { - IEnumerable appliedCustomers = customers.Where(c => c.Id == key); + IQueryable ifMatchCustomers = queryOptions.IfMatch.ApplyTo(appliedCustomers.AsQueryable()).Cast(); - if (appliedCustomers.Count() == 0) + if (ifMatchCustomers.Count() == 0) { - return BadRequest(string.Format("The entry with Id {0} doesn't exist", key)); - } - - if (queryOptions.IfMatch != null) - { - IQueryable ifMatchCustomers = queryOptions.IfMatch.ApplyTo(appliedCustomers.AsQueryable()).Cast(); - - if (ifMatchCustomers.Count() == 0) - { - return StatusCode(StatusCodes.Status412PreconditionFailed); - } + return StatusCode(StatusCodes.Status412PreconditionFailed); } + } - ETagsCustomer customer = appliedCustomers.Single(); - patch.Patch(customer); + ETagsCustomer customer = appliedCustomers.Single(); + patch.Patch(customer); - return Ok(customer); - } + return Ok(customer); } +} - internal class Helpers +internal class Helpers +{ + internal static ETagsCustomer CreateCustomer(int i) { - internal static ETagsCustomer CreateCustomer(int i) + return new ETagsDerivedCustomer { - return new ETagsDerivedCustomer + Id = i, + Name = "Customer Name " + i, + ShortProperty = (short)(Int16.MaxValue - i), + DoubleProperty = 2.0 * (i + 1), + Notes = Enumerable.Range(0, i + 1).Select(j => "This is note " + (i * 10 + j)).ToList(), + RelatedCustomer = new ETagsCustomer { - Id = i, - Name = "Customer Name " + i, - ShortProperty = (short)(Int16.MaxValue - i), - DoubleProperty = 2.0 * (i + 1), - Notes = Enumerable.Range(0, i + 1).Select(j => "This is note " + (i * 10 + j)).ToList(), - RelatedCustomer = new ETagsCustomer - { - Id = i + 1, - Name = "Customer Name " + i + 1, - ShortProperty = (short)(Int16.MaxValue - (i + 1) * 10), - DoubleProperty = 2.0 * (i + 1) * 10, - Notes = Enumerable.Range(0, (i + 1) * 10).Select(j => "This is note " + ((i + 1) * 10 + j)).ToList() - }, - ContainedCustomer = new ETagsCustomer - { - Id = (i + 1) * 100, - Name = "Customer Name " + i * 10, - ShortProperty = (short)(Int16.MaxValue - i * 10), - DoubleProperty = 2.0 * (i * 10 + 1), - Notes = Enumerable.Range(0, i * 10 + 1).Select(j => "This is note " + (i * 100 + j)).ToList() - } - }; - } - - internal static bool ValidateEtag(ETagsCustomer customer, ODataQueryOptions options) - { - if (options.IfMatch != null) + Id = i + 1, + Name = "Customer Name " + i + 1, + ShortProperty = (short)(Int16.MaxValue - (i + 1) * 10), + DoubleProperty = 2.0 * (i + 1) * 10, + Notes = Enumerable.Range(0, (i + 1) * 10).Select(j => "This is note " + ((i + 1) * 10 + j)).ToList() + }, + ContainedCustomer = new ETagsCustomer { - IQueryable ifMatchCustomers = options.IfMatch.ApplyTo((new ETagsCustomer[] { customer }).AsQueryable()).Cast(); - - if (ifMatchCustomers.Count() == 0) - { - return false; - } + Id = (i + 1) * 100, + Name = "Customer Name " + i * 10, + ShortProperty = (short)(Int16.MaxValue - i * 10), + DoubleProperty = 2.0 * (i * 10 + 1), + Notes = Enumerable.Range(0, i * 10 + 1).Select(j => "This is note " + (i * 100 + j)).ToList() } - return true; - } + }; + } - internal static ETagsDerivedCustomer CreateDerivedCustomer(ETagsCustomer customer) + internal static bool ValidateEtag(ETagsCustomer customer, ODataQueryOptions options) + { + if (options.IfMatch != null) { - ETagsDerivedCustomer newCustomer = new ETagsDerivedCustomer(); - newCustomer.Id = customer.Id; - newCustomer.Role = customer.Name + customer.Id; - ReplaceCustomer(newCustomer, customer); - return newCustomer; - } + IQueryable ifMatchCustomers = options.IfMatch.ApplyTo((new ETagsCustomer[] { customer }).AsQueryable()).Cast(); - internal static void ReplaceCustomer(ETagsCustomer newCustomer, ETagsCustomer customer) - { - newCustomer.Name = customer.Name; - newCustomer.Notes = customer.Notes; - newCustomer.BoolProperty = customer.BoolProperty; - newCustomer.ByteProperty = customer.ByteProperty; - newCustomer.CharProperty = customer.CharProperty; - newCustomer.DecimalProperty = customer.DecimalProperty; - newCustomer.DoubleProperty = customer.DoubleProperty; - newCustomer.ShortProperty = customer.ShortProperty; - newCustomer.LongProperty = customer.LongProperty; - newCustomer.SbyteProperty = customer.SbyteProperty; - newCustomer.FloatProperty = customer.FloatProperty; - newCustomer.UshortProperty = customer.UshortProperty; - newCustomer.UintProperty = customer.UintProperty; - newCustomer.UlongProperty = customer.UlongProperty; - newCustomer.GuidProperty = customer.GuidProperty; - newCustomer.DateTimeOffsetProperty = customer.DateTimeOffsetProperty; + if (ifMatchCustomers.Count() == 0) + { + return false; + } } + return true; } + internal static ETagsDerivedCustomer CreateDerivedCustomer(ETagsCustomer customer) + { + ETagsDerivedCustomer newCustomer = new ETagsDerivedCustomer(); + newCustomer.Id = customer.Id; + newCustomer.Role = customer.Name + customer.Id; + ReplaceCustomer(newCustomer, customer); + return newCustomer; + } + + internal static void ReplaceCustomer(ETagsCustomer newCustomer, ETagsCustomer customer) + { + newCustomer.Name = customer.Name; + newCustomer.Notes = customer.Notes; + newCustomer.BoolProperty = customer.BoolProperty; + newCustomer.ByteProperty = customer.ByteProperty; + newCustomer.CharProperty = customer.CharProperty; + newCustomer.DecimalProperty = customer.DecimalProperty; + newCustomer.DoubleProperty = customer.DoubleProperty; + newCustomer.ShortProperty = customer.ShortProperty; + newCustomer.LongProperty = customer.LongProperty; + newCustomer.SbyteProperty = customer.SbyteProperty; + newCustomer.FloatProperty = customer.FloatProperty; + newCustomer.UshortProperty = customer.UshortProperty; + newCustomer.UintProperty = customer.UintProperty; + newCustomer.UlongProperty = customer.UlongProperty; + newCustomer.GuidProperty = customer.GuidProperty; + newCustomer.DateTimeOffsetProperty = customer.DateTimeOffsetProperty; + } } + diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsModel.cs index 682dc51b2..7952cdc0b 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsModel.cs @@ -10,36 +10,35 @@ using System.ComponentModel.DataAnnotations; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags +namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags; + +public class ETagsDerivedCustomer : ETagsCustomer { - public class ETagsDerivedCustomer : ETagsCustomer - { - public string Role { get; set; } - } + public string Role { get; set; } +} - public class ETagsCustomer - { - public int Id { get; set; } - public string Name { get; set; } - public IList Notes { get; set; } - public bool BoolProperty { get; set; } - public byte ByteProperty { get; set; } - public char CharProperty { get; set; } - public decimal DecimalProperty { get; set; } - public double DoubleProperty { get; set; } - public short ShortProperty { get; set; } - public long LongProperty { get; set; } - public sbyte SbyteProperty { get; set; } - public float FloatProperty { get; set; } - public ushort UshortProperty { get; set; } - public uint UintProperty { get; set; } - public ulong UlongProperty { get; set; } - public Guid GuidProperty { get; set; } - public DateTimeOffset DateTimeOffsetProperty { get; set; } - [ConcurrencyCheck] - public string StringWithConcurrencyCheckAttributeProperty { get; set; } - public ETagsCustomer RelatedCustomer { get; set; } - [Contained] - public ETagsCustomer ContainedCustomer { get; set; } - } +public class ETagsCustomer +{ + public int Id { get; set; } + public string Name { get; set; } + public IList Notes { get; set; } + public bool BoolProperty { get; set; } + public byte ByteProperty { get; set; } + public char CharProperty { get; set; } + public decimal DecimalProperty { get; set; } + public double DoubleProperty { get; set; } + public short ShortProperty { get; set; } + public long LongProperty { get; set; } + public sbyte SbyteProperty { get; set; } + public float FloatProperty { get; set; } + public ushort UshortProperty { get; set; } + public uint UintProperty { get; set; } + public ulong UlongProperty { get; set; } + public Guid GuidProperty { get; set; } + public DateTimeOffset DateTimeOffsetProperty { get; set; } + [ConcurrencyCheck] + public string StringWithConcurrencyCheckAttributeProperty { get; set; } + public ETagsCustomer RelatedCustomer { get; set; } + [Contained] + public ETagsCustomer ContainedCustomer { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsOtherTypesTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsOtherTypesTest.cs index 2d0cc2ec2..9b237d5d1 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsOtherTypesTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsOtherTypesTest.cs @@ -22,154 +22,153 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags +namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags; + +public class ETagsOtherTypesTest : WebApiTestBase { - public class ETagsOtherTypesTest : WebApiTestBase + public ETagsOtherTypesTest(WebApiTestFixture fixture) + :base(fixture) { - public ETagsOtherTypesTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel1 = GetDoubleETagEdmModel(); - IEdmModel edmModel2 = GetShortETagEdmModel(); - services.ConfigureControllers(typeof(ETagsCustomersController)); - services.AddControllers().AddOData(opt => opt.Select().AddRouteComponents("double", edmModel1).AddRouteComponents("short", edmModel2)); - } + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel1 = GetDoubleETagEdmModel(); + IEdmModel edmModel2 = GetShortETagEdmModel(); + services.ConfigureControllers(typeof(ETagsCustomersController)); + services.AddControllers().AddOData(opt => opt.Select().AddRouteComponents("double", edmModel1).AddRouteComponents("short", edmModel2)); + } - private static IEdmModel GetDoubleETagEdmModel() - { - var builder = new ODataConventionModelBuilder(); - var customer = builder.EntitySet("ETagsCustomers").EntityType; - customer.Property(c => c.DoubleProperty).IsConcurrencyToken(); - customer.Ignore(c => c.StringWithConcurrencyCheckAttributeProperty); - return builder.GetEdmModel(); - } + private static IEdmModel GetDoubleETagEdmModel() + { + var builder = new ODataConventionModelBuilder(); + var customer = builder.EntitySet("ETagsCustomers").EntityType; + customer.Property(c => c.DoubleProperty).IsConcurrencyToken(); + customer.Ignore(c => c.StringWithConcurrencyCheckAttributeProperty); + return builder.GetEdmModel(); + } - private static IEdmModel GetShortETagEdmModel() - { - var builder = new ODataConventionModelBuilder(); - var customer = builder.EntitySet("ETagsCustomers").EntityType; - customer.Ignore(c => c.StringWithConcurrencyCheckAttributeProperty); - customer.Property(c => c.ShortProperty).IsConcurrencyToken(); - return builder.GetEdmModel(); - } + private static IEdmModel GetShortETagEdmModel() + { + var builder = new ODataConventionModelBuilder(); + var customer = builder.EntitySet("ETagsCustomers").EntityType; + customer.Ignore(c => c.StringWithConcurrencyCheckAttributeProperty); + customer.Property(c => c.ShortProperty).IsConcurrencyToken(); + return builder.GetEdmModel(); + } - [Fact] - public async Task GetEntityWithIfNoneMatchShouldReturnNotModifiedETagsTest_ForDouble() - { - // Arrange - HttpClient client = CreateClient(); - string eTag; + [Fact] + public async Task GetEntityWithIfNoneMatchShouldReturnNotModifiedETagsTest_ForDouble() + { + // Arrange + HttpClient client = CreateClient(); + string eTag; - var getUri = "double/ETagsCustomers?$format=json"; + var getUri = "double/ETagsCustomers?$format=json"; - // Act - using (var response = await client.GetAsync(getUri)) - { - // Assert - Assert.True(response.IsSuccessStatusCode); - - var json = await response.Content.ReadAsObject(); - var result = json.GetValue("value") as JArray; - Assert.NotNull(result); - - // check the first unchanged, the first unchanged could be "3" - // because #0, #1, #2 will change potentially running in parallel by other tests. - eTag = result[3]["@odata.etag"].ToString(); - Assert.False(String.IsNullOrEmpty(eTag)); - Assert.Equal("W/\"OC4w\"", eTag); - - EntityTagHeaderValue parsedValue; - Assert.True(EntityTagHeaderValue.TryParse(eTag, out parsedValue)); - IDictionary tags = this.ParseETag(parsedValue); - KeyValuePair pair = Assert.Single(tags); - Single value = Assert.IsType(pair.Value); - Assert.Equal((Single)8.0, value); - } + // Act + using (var response = await client.GetAsync(getUri)) + { + // Assert + Assert.True(response.IsSuccessStatusCode); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + Assert.NotNull(result); + + // check the first unchanged, the first unchanged could be "3" + // because #0, #1, #2 will change potentially running in parallel by other tests. + eTag = result[3]["@odata.etag"].ToString(); + Assert.False(String.IsNullOrEmpty(eTag)); + Assert.Equal("W/\"OC4w\"", eTag); + + EntityTagHeaderValue parsedValue; + Assert.True(EntityTagHeaderValue.TryParse(eTag, out parsedValue)); + IDictionary tags = this.ParseETag(parsedValue); + KeyValuePair pair = Assert.Single(tags); + Single value = Assert.IsType(pair.Value); + Assert.Equal((Single)8.0, value); + } - // Arrange - var getRequestWithEtag = new HttpRequestMessage(HttpMethod.Get, "double/ETagsCustomers(3)"); - getRequestWithEtag.Headers.IfNoneMatch.ParseAdd(eTag); + // Arrange + var getRequestWithEtag = new HttpRequestMessage(HttpMethod.Get, "double/ETagsCustomers(3)"); + getRequestWithEtag.Headers.IfNoneMatch.ParseAdd(eTag); - // Act - using (var response = await client.SendAsync(getRequestWithEtag)) - { - // Assert - Assert.Equal(HttpStatusCode.NotModified, response.StatusCode); - } + // Act + using (var response = await client.SendAsync(getRequestWithEtag)) + { + // Assert + Assert.Equal(HttpStatusCode.NotModified, response.StatusCode); } + } - [Fact] - public async Task GetEntityWithIfNoneMatchShouldReturnNotModifiedETagsTest_ForShort() - { - // Arrange - HttpClient client = CreateClient(); - string eTag; + [Fact] + public async Task GetEntityWithIfNoneMatchShouldReturnNotModifiedETagsTest_ForShort() + { + // Arrange + HttpClient client = CreateClient(); + string eTag; - var getUri = "short/ETagsCustomers?$format=json"; + var getUri = "short/ETagsCustomers?$format=json"; - // Act - using (var response = await client.GetAsync(getUri)) - { - // Assert - Assert.True(response.IsSuccessStatusCode); - - var json = await response.Content.ReadAsObject(); - var result = json.GetValue("value") as JArray; - Assert.NotNull(result); - - // check the first unchanged, the first unchanged could be "3", - // because #0, #1, #2 will change potentially running in parallel by other tests. - eTag = result[3]["@odata.etag"].ToString(); - Assert.False(String.IsNullOrEmpty(eTag)); - Assert.Equal("W/\"MzI3NjQ=\"", eTag); - - EntityTagHeaderValue parsedValue; - Assert.True(EntityTagHeaderValue.TryParse(eTag, out parsedValue)); - IDictionary tags = this.ParseETag(parsedValue); - KeyValuePair pair = Assert.Single(tags); - int value = Assert.IsType(pair.Value); - Assert.Equal(32764, value); - } + // Act + using (var response = await client.GetAsync(getUri)) + { + // Assert + Assert.True(response.IsSuccessStatusCode); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + Assert.NotNull(result); + + // check the first unchanged, the first unchanged could be "3", + // because #0, #1, #2 will change potentially running in parallel by other tests. + eTag = result[3]["@odata.etag"].ToString(); + Assert.False(String.IsNullOrEmpty(eTag)); + Assert.Equal("W/\"MzI3NjQ=\"", eTag); + + EntityTagHeaderValue parsedValue; + Assert.True(EntityTagHeaderValue.TryParse(eTag, out parsedValue)); + IDictionary tags = this.ParseETag(parsedValue); + KeyValuePair pair = Assert.Single(tags); + int value = Assert.IsType(pair.Value); + Assert.Equal(32764, value); + } - // Arrange - var getRequestWithEtag = new HttpRequestMessage(HttpMethod.Get, "short/ETagsCustomers(3)"); - getRequestWithEtag.Headers.IfNoneMatch.ParseAdd(eTag); + // Arrange + var getRequestWithEtag = new HttpRequestMessage(HttpMethod.Get, "short/ETagsCustomers(3)"); + getRequestWithEtag.Headers.IfNoneMatch.ParseAdd(eTag); - // Act - using (var response = await client.SendAsync(getRequestWithEtag)) - { - // Assert - Assert.Equal(HttpStatusCode.NotModified, response.StatusCode); - } + // Act + using (var response = await client.SendAsync(getRequestWithEtag)) + { + // Assert + Assert.Equal(HttpStatusCode.NotModified, response.StatusCode); } + } + + private IDictionary ParseETag(EntityTagHeaderValue etagHeaderValue) + { + string tag = etagHeaderValue.Tag.Trim('\"'); - private IDictionary ParseETag(EntityTagHeaderValue etagHeaderValue) + // split etag + string[] rawValues = tag.Split(','); + IDictionary properties = new Dictionary(); + for (int index = 0; index < rawValues.Length; index++) { - string tag = etagHeaderValue.Tag.Trim('\"'); + string rawValue = rawValues[index]; - // split etag - string[] rawValues = tag.Split(','); - IDictionary properties = new Dictionary(); - for (int index = 0; index < rawValues.Length; index++) + // base64 decode + byte[] bytes = Convert.FromBase64String(rawValue); + string valueString = Encoding.UTF8.GetString(bytes); + object obj = ODataUriUtils.ConvertFromUriLiteral(valueString, ODataVersion.V4); + if (obj is ODataNullValue) { - string rawValue = rawValues[index]; - - // base64 decode - byte[] bytes = Convert.FromBase64String(rawValue); - string valueString = Encoding.UTF8.GetString(bytes); - object obj = ODataUriUtils.ConvertFromUriLiteral(valueString, ODataVersion.V4); - if (obj is ODataNullValue) - { - obj = null; - } - properties.Add(index.ToString(CultureInfo.InvariantCulture), obj); + obj = null; } - - return properties; + properties.Add(index.ToString(CultureInfo.InvariantCulture), obj); } + + return properties; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsUntypedTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsUntypedTests.cs index 5cb4c0e58..a30d82197 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsUntypedTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/ETagsUntypedTests.cs @@ -26,128 +26,127 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags +namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags; + +public class ETagsUntypedTests : WebApiTestBase { - public class ETagsUntypedTests : WebApiTestBase + public ETagsUntypedTests(WebApiTestFixture fixture) + :base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = GetEdmModel(); + services.ConfigureControllers(typeof(ETagUntypedCustomersController), typeof(MetadataController)); + services.AddControllers().AddOData(opt => opt.Select().AddRouteComponents("odata", edmModel)); + + services.AddControllers(opt => opt.Filters.Add(new ETagActionFilterAttribute())); + } + + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + + // entity type customer + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + IEdmStructuralProperty customerName = customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + model.AddElement(customer); + + // entity sets + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + model.AddElement(container); + EdmEntitySet customers = container.AddEntitySet("ETagUntypedCustomers", customer); + + model.SetOptimisticConcurrencyAnnotation(customers, new[] { customerName }); + + return model; + } + + [Fact] + public async Task ModelBuilderTest() + { + // Arrange + string expectMetadata = + "\r\n" + + " \r\n" + + " \r\n" + + " Name\r\n" + + " \r\n" + + " \r\n" + + " "; + + // Remove indentation + expectMetadata = Regex.Replace(expectMetadata, @"\r\n\s*<", @"<"); + HttpClient client = CreateClient(); + string requestUri = "odata/$metadata"; + + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(expectMetadata, content); + + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); + var reader = new ODataMessageReader(message); + var edmModel = reader.ReadMetadataDocument(); + Assert.NotNull(edmModel); + + var etagCustomers = edmModel.FindDeclaredEntitySet("ETagUntypedCustomers"); + Assert.NotNull(etagCustomers); + + var annotations = edmModel.FindDeclaredVocabularyAnnotations(etagCustomers); + IEdmVocabularyAnnotation annotation = Assert.Single(annotations); + Assert.NotNull(annotation); + + Assert.Same(CoreVocabularyModel.ConcurrencyTerm, annotation.Term); + Assert.Same(etagCustomers, annotation.Target); + + IEdmVocabularyAnnotation valueAnnotation = annotation as IEdmVocabularyAnnotation; + Assert.NotNull(valueAnnotation); + Assert.NotNull(valueAnnotation.Value); + + IEdmCollectionExpression collection = valueAnnotation.Value as IEdmCollectionExpression; + Assert.NotNull(collection); + Assert.Equal(new[] { "Name" }, collection.Elements.Select(e => ((IEdmPathExpression)e).PathSegments.Single())); + } + + [Fact] + public async Task PatchUpdatedEntryWithIfMatchShouldReturnPreconditionFailed() { - public ETagsUntypedTests(WebApiTestFixture fixture) - :base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = GetEdmModel(); - services.ConfigureControllers(typeof(ETagUntypedCustomersController), typeof(MetadataController)); - services.AddControllers().AddOData(opt => opt.Select().AddRouteComponents("odata", edmModel)); - - services.AddControllers(opt => opt.Filters.Add(new ETagActionFilterAttribute())); - } - - private static IEdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); - - // entity type customer - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - IEdmStructuralProperty customerName = customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - model.AddElement(customer); - - // entity sets - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - model.AddElement(container); - EdmEntitySet customers = container.AddEntitySet("ETagUntypedCustomers", customer); - - model.SetOptimisticConcurrencyAnnotation(customers, new[] { customerName }); - - return model; - } - - [Fact] - public async Task ModelBuilderTest() - { - // Arrange - string expectMetadata = - "\r\n" + - " \r\n" + - " \r\n" + - " Name\r\n" + - " \r\n" + - " \r\n" + - " "; - - // Remove indentation - expectMetadata = Regex.Replace(expectMetadata, @"\r\n\s*<", @"<"); - HttpClient client = CreateClient(); - string requestUri = "odata/$metadata"; - - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); - - // Assert - var content = await response.Content.ReadAsStringAsync(); - Assert.Contains(expectMetadata, content); - - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); - var reader = new ODataMessageReader(message); - var edmModel = reader.ReadMetadataDocument(); - Assert.NotNull(edmModel); - - var etagCustomers = edmModel.FindDeclaredEntitySet("ETagUntypedCustomers"); - Assert.NotNull(etagCustomers); - - var annotations = edmModel.FindDeclaredVocabularyAnnotations(etagCustomers); - IEdmVocabularyAnnotation annotation = Assert.Single(annotations); - Assert.NotNull(annotation); - - Assert.Same(CoreVocabularyModel.ConcurrencyTerm, annotation.Term); - Assert.Same(etagCustomers, annotation.Target); - - IEdmVocabularyAnnotation valueAnnotation = annotation as IEdmVocabularyAnnotation; - Assert.NotNull(valueAnnotation); - Assert.NotNull(valueAnnotation.Value); - - IEdmCollectionExpression collection = valueAnnotation.Value as IEdmCollectionExpression; - Assert.NotNull(collection); - Assert.Equal(new[] { "Name" }, collection.Elements.Select(e => ((IEdmPathExpression)e).PathSegments.Single())); - } - - [Fact] - public async Task PatchUpdatedEntryWithIfMatchShouldReturnPreconditionFailed() - { - // Arrange - HttpClient client = CreateClient(); - string requestUri = "odata/ETagUntypedCustomers(1)?$format=json"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - var etagInHeader = response.Headers.ETag.ToString(); - JObject result = await response.Content.ReadAsObject(); - var etagInPayload = (string)result["@odata.etag"]; - - Assert.True(etagInPayload == etagInHeader); - Assert.Equal("W/\"J1NhbSc=\"", etagInPayload); - } + // Arrange + HttpClient client = CreateClient(); + string requestUri = "odata/ETagUntypedCustomers(1)?$format=json"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var etagInHeader = response.Headers.ETag.ToString(); + JObject result = await response.Content.ReadAsObject(); + var etagInPayload = (string)result["@odata.etag"]; + + Assert.True(etagInPayload == etagInHeader); + Assert.Equal("W/\"J1NhbSc=\"", etagInPayload); } +} - public class ETagUntypedCustomersController : ODataController +public class ETagUntypedCustomersController : ODataController +{ + [EnableQuery] + public IActionResult Get(int key) { - [EnableQuery] - public IActionResult Get(int key) - { - IEdmModel model = Request.GetModel(); - IEdmEntityType entityType = model.SchemaElements.OfType().First(c => c.Name == "Customer"); - EdmEntityObject customer = new EdmEntityObject(entityType); - customer.TrySetPropertyValue("ID", key); - customer.TrySetPropertyValue("Name", "Sam"); - return Ok(customer); - } + IEdmModel model = Request.GetModel(); + IEdmEntityType entityType = model.SchemaElements.OfType().First(c => c.Name == "Customer"); + EdmEntityObject customer = new EdmEntityObject(entityType); + customer.TrySetPropertyValue("ID", key); + customer.TrySetPropertyValue("Name", "Sam"); + return Ok(customer); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/JsonETagsTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/JsonETagsTests.cs index a2a0d4ba8..4336b53f3 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/JsonETagsTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ETags/JsonETagsTests.cs @@ -24,193 +24,192 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags +namespace Microsoft.AspNetCore.OData.E2E.Tests.ETags; + +public class JsonETagsTests : WebApiTestBase { - public class JsonETagsTests : WebApiTestBase + public JsonETagsTests(WebApiTestFixture fixture) + :base(fixture) { - public JsonETagsTests(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = GetEdmModel(); - services.ConfigureControllers(typeof(ETagsCustomersController), typeof(MetadataController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel)); - services.AddControllers(opt => opt.Filters.Add(new ETagActionFilterAttribute())); - } + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = GetEdmModel(); + services.ConfigureControllers(typeof(ETagsCustomersController), typeof(MetadataController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel)); + services.AddControllers(opt => opt.Filters.Add(new ETagActionFilterAttribute())); + } - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - EntitySetConfiguration eTagsCustomersSet = builder.EntitySet("ETagsCustomers"); - EntityTypeConfiguration eTagsCustomers = eTagsCustomersSet.EntityType; - eTagsCustomers.Property(c => c.Id).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.Name).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.BoolProperty).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.ByteProperty).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.CharProperty).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.DecimalProperty).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.DoubleProperty).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.ShortProperty).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.LongProperty).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.SbyteProperty).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.FloatProperty).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.UshortProperty).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.UintProperty).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.UlongProperty).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.GuidProperty).IsConcurrencyToken(); - eTagsCustomers.Property(c => c.DateTimeOffsetProperty).IsConcurrencyToken(); - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + EntitySetConfiguration eTagsCustomersSet = builder.EntitySet("ETagsCustomers"); + EntityTypeConfiguration eTagsCustomers = eTagsCustomersSet.EntityType; + eTagsCustomers.Property(c => c.Id).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.Name).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.BoolProperty).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.ByteProperty).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.CharProperty).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.DecimalProperty).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.DoubleProperty).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.ShortProperty).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.LongProperty).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.SbyteProperty).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.FloatProperty).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.UshortProperty).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.UintProperty).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.UlongProperty).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.GuidProperty).IsConcurrencyToken(); + eTagsCustomers.Property(c => c.DateTimeOffsetProperty).IsConcurrencyToken(); + return builder.GetEdmModel(); + } - [Fact] - public async Task ModelBuilderTest() + [Fact] + public async Task ModelBuilderTest() + { + // Arrange + string expectMetadata = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " Id\r\n" + + " Name\r\n" + + " BoolProperty\r\n" + + " ByteProperty\r\n" + + " CharProperty\r\n" + + " DecimalProperty\r\n" + + " DoubleProperty\r\n" + + " ShortProperty\r\n" + + " LongProperty\r\n" + + " SbyteProperty\r\n" + + " FloatProperty\r\n" + + " UshortProperty\r\n" + + " UintProperty\r\n" + + " UlongProperty\r\n" + + " GuidProperty\r\n" + + " DateTimeOffsetProperty\r\n" + + " StringWithConcurrencyCheckAttributeProperty\r\n" + + " \r\n" + + " \r\n" + + " "; + + // Remove indentation + expectMetadata = Regex.Replace(expectMetadata, @"\r\n\s*<", @"<"); + + string requestUri = "odata/$metadata"; + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(expectMetadata, content); + + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); + var reader = new ODataMessageReader(message); + var edmModel = reader.ReadMetadataDocument(); + Assert.NotNull(edmModel); + + var etagCustomers = edmModel.FindDeclaredEntitySet("ETagsCustomers"); + Assert.NotNull(etagCustomers); + + var annotations = edmModel.FindDeclaredVocabularyAnnotations(etagCustomers); + IEdmVocabularyAnnotation annotation = Assert.Single(annotations); + Assert.NotNull(annotation); + + Assert.Same(CoreVocabularyModel.ConcurrencyTerm, annotation.Term); + Assert.Same(etagCustomers, annotation.Target); + + IEdmVocabularyAnnotation valueAnnotation = annotation as IEdmVocabularyAnnotation; + Assert.NotNull(valueAnnotation); + Assert.NotNull(valueAnnotation.Value); + + IEdmCollectionExpression collection = valueAnnotation.Value as IEdmCollectionExpression; + Assert.NotNull(collection); + Assert.Equal(new[] { - // Arrange - string expectMetadata = - "\r\n" + - " \r\n" + - " \r\n" + - " \r\n" + - " Id\r\n" + - " Name\r\n" + - " BoolProperty\r\n" + - " ByteProperty\r\n" + - " CharProperty\r\n" + - " DecimalProperty\r\n" + - " DoubleProperty\r\n" + - " ShortProperty\r\n" + - " LongProperty\r\n" + - " SbyteProperty\r\n" + - " FloatProperty\r\n" + - " UshortProperty\r\n" + - " UintProperty\r\n" + - " UlongProperty\r\n" + - " GuidProperty\r\n" + - " DateTimeOffsetProperty\r\n" + - " StringWithConcurrencyCheckAttributeProperty\r\n" + - " \r\n" + - " \r\n" + - " "; - - // Remove indentation - expectMetadata = Regex.Replace(expectMetadata, @"\r\n\s*<", @"<"); - - string requestUri = "odata/$metadata"; - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); - - // Assert - var content = await response.Content.ReadAsStringAsync(); - Assert.Contains(expectMetadata, content); - - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); - var reader = new ODataMessageReader(message); - var edmModel = reader.ReadMetadataDocument(); - Assert.NotNull(edmModel); - - var etagCustomers = edmModel.FindDeclaredEntitySet("ETagsCustomers"); - Assert.NotNull(etagCustomers); - - var annotations = edmModel.FindDeclaredVocabularyAnnotations(etagCustomers); - IEdmVocabularyAnnotation annotation = Assert.Single(annotations); - Assert.NotNull(annotation); - - Assert.Same(CoreVocabularyModel.ConcurrencyTerm, annotation.Term); - Assert.Same(etagCustomers, annotation.Target); - - IEdmVocabularyAnnotation valueAnnotation = annotation as IEdmVocabularyAnnotation; - Assert.NotNull(valueAnnotation); - Assert.NotNull(valueAnnotation.Value); - - IEdmCollectionExpression collection = valueAnnotation.Value as IEdmCollectionExpression; - Assert.NotNull(collection); - Assert.Equal(new[] - { - "Id", "Name", "BoolProperty", "ByteProperty", "CharProperty", "DecimalProperty", - "DoubleProperty", "ShortProperty", "LongProperty", "SbyteProperty", - "FloatProperty", "UshortProperty", "UintProperty", "UlongProperty", - "GuidProperty", "DateTimeOffsetProperty", - "StringWithConcurrencyCheckAttributeProperty" - }, - collection.Elements.Select(e => ((IEdmPathExpression) e).PathSegments.Single())); - } + "Id", "Name", "BoolProperty", "ByteProperty", "CharProperty", "DecimalProperty", + "DoubleProperty", "ShortProperty", "LongProperty", "SbyteProperty", + "FloatProperty", "UshortProperty", "UintProperty", "UlongProperty", + "GuidProperty", "DateTimeOffsetProperty", + "StringWithConcurrencyCheckAttributeProperty" + }, + collection.Elements.Select(e => ((IEdmPathExpression) e).PathSegments.Single())); + } - [Theory] - [InlineData("application/json")] // default metadata level - [InlineData("application/json;odata.metadata=full")] - [InlineData("application/json;odata.metadata=minimal")] - public async Task JsonWithDifferentMetadataLevelsHaveSameETagsTest(string metadataLevel) + [Theory] + [InlineData("application/json")] // default metadata level + [InlineData("application/json;odata.metadata=full")] + [InlineData("application/json;odata.metadata=minimal")] + public async Task JsonWithDifferentMetadataLevelsHaveSameETagsTest(string metadataLevel) + { + // Arrange + HttpClient client = CreateClient(); + string requestUri = "odata/ETagsCustomers"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.ParseAdd(metadataLevel); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var jsonResult = await response.Content.ReadAsObject(); + var jsonValue = jsonResult.GetValue("value") as JArray; + Assert.Equal(10, jsonValue.Count); + + var expectedEtags = new Dictionary { - // Arrange - HttpClient client = CreateClient(); - string requestUri = "odata/ETagsCustomers"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.ParseAdd(metadataLevel); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - var jsonResult = await response.Content.ReadAsObject(); - var jsonValue = jsonResult.GetValue("value") as JArray; - Assert.Equal(10, jsonValue.Count); - - var expectedEtags = new Dictionary - { - // { "0", ",MA==,Mi4w," }, // DeleteUpdatedEntryWithIfMatchETagsTests will change #"0" customer - // { "1", ",MA==,NC4w," }, // PutUpdatedEntryWithIfMatchETagsTests will change #"1"customer - // { "2", ",MA==,Ni4w," }, // PatchUpdatedEntryWithIfMatchETagsTest will change #"2" cusotmer - { "3", ",MA==,OC4w," }, - { "4", ",MA==,MTAuMA==," }, - { "5", ",MA==,MTIuMA==," }, - { "6", ",MA==,MTQuMA==," }, - { "7", ",MA==,MTYuMA==," }, - { "8", ",MA==,MTguMA==," }, - { "9", ",MA==,MjAuMA==," }, - }; - - var jsonETags = jsonValue.Select(e => e["@odata.etag"]); - foreach (var etag in jsonValue) + // { "0", ",MA==,Mi4w," }, // DeleteUpdatedEntryWithIfMatchETagsTests will change #"0" customer + // { "1", ",MA==,NC4w," }, // PutUpdatedEntryWithIfMatchETagsTests will change #"1"customer + // { "2", ",MA==,Ni4w," }, // PatchUpdatedEntryWithIfMatchETagsTest will change #"2" cusotmer + { "3", ",MA==,OC4w," }, + { "4", ",MA==,MTAuMA==," }, + { "5", ",MA==,MTIuMA==," }, + { "6", ",MA==,MTQuMA==," }, + { "7", ",MA==,MTYuMA==," }, + { "8", ",MA==,MTguMA==," }, + { "9", ",MA==,MjAuMA==," }, + }; + + var jsonETags = jsonValue.Select(e => e["@odata.etag"]); + foreach (var etag in jsonValue) + { + string key = etag["Id"].ToString(); + if (expectedEtags.TryGetValue(key, out string etagValue)) { - string key = etag["Id"].ToString(); - if (expectedEtags.TryGetValue(key, out string etagValue)) - { - Assert.Contains(etagValue, etag["@odata.etag"].ToString()); - } + Assert.Contains(etagValue, etag["@odata.etag"].ToString()); } } + } - [Fact] - public async Task JsonWithNoneMetadataLevelsNotIncludeETags() + [Fact] + public async Task JsonWithNoneMetadataLevelsNotIncludeETags() + { + // Arrange + HttpClient client = CreateClient(); + string requestUri = "odata/ETagsCustomers"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.ParseAdd("application/json;odata.metadata=none"); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var jsonResult = await response.Content.ReadAsObject(); + var jsonValue = jsonResult.GetValue("value") as JArray; + Assert.Equal(10, jsonValue.Count()); + + foreach (var item in jsonValue) { - // Arrange - HttpClient client = CreateClient(); - string requestUri = "odata/ETagsCustomers"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.ParseAdd("application/json;odata.metadata=none"); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - var jsonResult = await response.Content.ReadAsObject(); - var jsonValue = jsonResult.GetValue("value") as JArray; - Assert.Equal(10, jsonValue.Count()); - - foreach (var item in jsonValue) - { - JObject itemObject = item as JObject; - Assert.NotNull(itemObject); - Assert.DoesNotContain("@odata.etag", itemObject.Properties().Select(p => p.Name)); - } + JObject itemObject = item as JObject; + Assert.NotNull(itemObject); + Assert.DoesNotContain("@odata.etag", itemObject.Properties().Select(p => p.Name)); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationController.cs index 47e9282d8..64b3ebad7 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationController.cs @@ -11,73 +11,72 @@ using Microsoft.AspNetCore.OData.Results; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.EntitySetAggregation +namespace Microsoft.AspNetCore.OData.E2E.Tests.EntitySetAggregation; + +public class CustomersController : ODataController { - public class CustomersController : ODataController - { - private readonly EntitySetAggregationContext _context; + private readonly EntitySetAggregationContext _context; - public CustomersController(EntitySetAggregationContext context) - { - EntitySetAggregationContext.EnsureDatabaseCreated(context); - _context = context; - } + public CustomersController(EntitySetAggregationContext context) + { + EntitySetAggregationContext.EnsureDatabaseCreated(context); + _context = context; + } - [EnableQuery] - public IQueryable Get() - { - return _context.Customers; - } + [EnableQuery] + public IQueryable Get() + { + return _context.Customers; + } - [EnableQuery] - public SingleResult Get(int key) - { - return SingleResult.Create(_context.Customers.Where(c => c.Id == key)); - } + [EnableQuery] + public SingleResult Get(int key) + { + return SingleResult.Create(_context.Customers.Where(c => c.Id == key)); } +} - public class EmployeesController : ODataController +public class EmployeesController : ODataController +{ + private static readonly List employees = new List { - private static readonly List employees = new List + new Employee { - new Employee - { - Id = 1, - NextOfKin = new NextOfKin { Name = "NoK 1", PhysicalAddress = new Location { City = "Redmond" } } - }, - new Employee - { - Id = 2, - NextOfKin = new NextOfKin { Name = "NoK 2", PhysicalAddress = new Location { City = "Nairobi" } } - }, - new Employee - { - Id = 3, - NextOfKin = new NextOfKin { Name = "NoK 3", PhysicalAddress = new Location { City = "Redmond" } } - } - }; - - [EnableQuery] - public IQueryable Get() + Id = 1, + NextOfKin = new NextOfKin { Name = "NoK 1", PhysicalAddress = new Location { City = "Redmond" } } + }, + new Employee + { + Id = 2, + NextOfKin = new NextOfKin { Name = "NoK 2", PhysicalAddress = new Location { City = "Nairobi" } } + }, + new Employee { - return employees.AsQueryable(); + Id = 3, + NextOfKin = new NextOfKin { Name = "NoK 3", PhysicalAddress = new Location { City = "Redmond" } } } - } + }; - public class OrdersController : ODataController + [EnableQuery] + public IQueryable Get() { - private readonly EntitySetAggregationContext _context; + return employees.AsQueryable(); + } +} - public OrdersController(EntitySetAggregationContext context) - { - EntitySetAggregationContext.EnsureDatabaseCreated(context); - _context = context; - } +public class OrdersController : ODataController +{ + private readonly EntitySetAggregationContext _context; - [EnableQuery] - public IQueryable Get() - { - return _context.Orders; - } + public OrdersController(EntitySetAggregationContext context) + { + EntitySetAggregationContext.EnsureDatabaseCreated(context); + _context = context; + } + + [EnableQuery] + public IQueryable Get() + { + return _context.Orders; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationDataModel.cs index 2f263ebc7..be904663f 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationDataModel.cs @@ -9,119 +9,118 @@ using System.Linq; using Microsoft.EntityFrameworkCore; -namespace Microsoft.AspNetCore.OData.E2E.Tests.EntitySetAggregation +namespace Microsoft.AspNetCore.OData.E2E.Tests.EntitySetAggregation; + +public class EntitySetAggregationContext : DbContext { - public class EntitySetAggregationContext : DbContext + //public static string ConnectionString = + // @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=EntitySetAggregationTest1"; + + public EntitySetAggregationContext(DbContextOptions options) + : base(options) { - //public static string ConnectionString = - // @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=EntitySetAggregationTest1"; + } - public EntitySetAggregationContext(DbContextOptions options) - : base(options) - { - } + public DbSet Customers { get; set; } - public DbSet Customers { get; set; } + public DbSet Orders { get; set; } - public DbSet Orders { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().OwnsOne(c => c.Address).WithOwner(); + modelBuilder.Entity().OwnsOne(c => c.SaleInfo).WithOwner(); + } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().OwnsOne(c => c.Address).WithOwner(); - modelBuilder.Entity().OwnsOne(c => c.SaleInfo).WithOwner(); - } + public static void EnsureDatabaseCreated(EntitySetAggregationContext context) + { + context.Database.EnsureCreated(); - public static void EnsureDatabaseCreated(EntitySetAggregationContext context) + if (!context.Customers.Any()) { - context.Database.EnsureCreated(); - - if (!context.Customers.Any()) + for (int i = 1; i <= 3; i++) { - for (int i = 1; i <= 3; i++) + var customer = new Customer { - var customer = new Customer - { - // Id = i, - Name = "Customer" + (i + 1) % 2, - Orders = - new List { - new Order { - Name = "Order" + 2*i, - Price = i * 25, - SaleInfo = new SaleInfo { Quantity = i, UnitPrice = 25 } - }, - new Order { - Name = "Order" + 2*i+1, - Price = i * 75, - SaleInfo = new SaleInfo { Quantity = i, UnitPrice = 75 } - } - }, - Address = new Address - { - Name = "City" + i % 2, - Street = "Street" + i % 2, + // Id = i, + Name = "Customer" + (i + 1) % 2, + Orders = + new List { + new Order { + Name = "Order" + 2*i, + Price = i * 25, + SaleInfo = new SaleInfo { Quantity = i, UnitPrice = 25 } + }, + new Order { + Name = "Order" + 2*i+1, + Price = i * 75, + SaleInfo = new SaleInfo { Quantity = i, UnitPrice = 75 } } - }; - - context.Customers.Add(customer); - } + }, + Address = new Address + { + Name = "City" + i % 2, + Street = "Street" + i % 2, + } + }; - context.SaveChanges(); + context.Customers.Add(customer); } + + context.SaveChanges(); } } +} - public class Customer - { - public int Id { get; set; } +public class Customer +{ + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public Address Address { get; set; } + public Address Address { get; set; } - public IList Orders { get; set; } - } + public IList Orders { get; set; } +} - public class Order - { - public int Id { get; set; } +public class Order +{ + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public int Price { get; set; } + public int Price { get; set; } - public SaleInfo SaleInfo { get; set; } - } + public SaleInfo SaleInfo { get; set; } +} - public class SaleInfo - { - public int Quantity { get; set; } +public class SaleInfo +{ + public int Quantity { get; set; } - public int UnitPrice { get; set; } - } + public int UnitPrice { get; set; } +} - //[Owned, ComplexType] - public class Address - { - public string Name { get; set; } +//[Owned, ComplexType] +public class Address +{ + public string Name { get; set; } - public string Street { get; set; } - } + public string Street { get; set; } +} - public class Employee - { - public int Id { get; set; } - public NextOfKin NextOfKin { get; set; } - } +public class Employee +{ + public int Id { get; set; } + public NextOfKin NextOfKin { get; set; } +} - public class NextOfKin - { - public string Name { get; set; } - public Location PhysicalAddress { get; set; } - } +public class NextOfKin +{ + public string Name { get; set; } + public Location PhysicalAddress { get; set; } +} - public class Location - { - public string City { get; set; } - } +public class Location +{ + public string City { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationEdmModel.cs index 43a6c4ae5..5a96c1d8d 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationEdmModel.cs @@ -8,17 +8,16 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.EntitySetAggregation +namespace Microsoft.AspNetCore.OData.E2E.Tests.EntitySetAggregation; + +public class EntitySetAggregationEdmModel { - public class EntitySetAggregationEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder =new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Orders"); - IEdmModel model = builder.GetEdmModel(); - return model; - } + var builder =new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); + IEdmModel model = builder.GetEdmModel(); + return model; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationTests.cs index 0ead0293c..777471caa 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/EntitySetAggregation/EntitySetAggregationTests.cs @@ -18,358 +18,357 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.EntitySetAggregation +namespace Microsoft.AspNetCore.OData.E2E.Tests.EntitySetAggregation; + +// The test can't work in EFCore, because it's not supported with Groupby and select many on collection. +// Later, we'd switch it to EF6. +public class EntitySetAggregationTests : WebODataTestBase { - // The test can't work in EFCore, because it's not supported with Groupby and select many on collection. - // Later, we'd switch it to EF6. - public class EntitySetAggregationTests : WebODataTestBase + public class TestsStartup : TestStartupBase { - public class TestsStartup : TestStartupBase + public override void ConfigureServices(IServiceCollection services) { - public override void ConfigureServices(IServiceCollection services) - { - // Use the sql server got the access error. - // services.AddDbContext(opt => opt.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EntitySetAggregationContext;Trusted_Connection=True;")); - services.AddDbContext(opt => opt.UseInMemoryDatabase("EntitySetAggregationTest")); - - services.ConfigureControllers(typeof(CustomersController), typeof(OrdersController)); - - IEdmModel edmModel = EntitySetAggregationEdmModel.GetEdmModel(); - services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(null) - .AddRouteComponents("aggregation", edmModel)); - } - } + // Use the sql server got the access error. + // services.AddDbContext(opt => opt.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EntitySetAggregationContext;Trusted_Connection=True;")); + services.AddDbContext(opt => opt.UseInMemoryDatabase("EntitySetAggregationTest")); - private const string AggregationTestBaseUrl = "aggregation/Customers"; + services.ConfigureControllers(typeof(CustomersController), typeof(OrdersController)); - public EntitySetAggregationTests(WebODataTestFixture factory) - : base(factory) - { + IEdmModel edmModel = EntitySetAggregationEdmModel.GetEdmModel(); + services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(null) + .AddRouteComponents("aggregation", edmModel)); } + } - [Theory(Skip = "See the comments above")] - [InlineData("sum",600)] - [InlineData("min", 25)] - [InlineData("max", 225)] - [InlineData("average", 100)] - public async Task AggregationOnEntitySetWorks(string method, int expected) - { - // Arrange - string queryUrl = AggregationTestBaseUrl + "?$apply=aggregate(Orders(Price with " + method + " as TotalPrice))"; + private const string AggregationTestBaseUrl = "aggregation/Customers"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + public EntitySetAggregationTests(WebODataTestFixture factory) + : base(factory) + { + } + + [Theory(Skip = "See the comments above")] + [InlineData("sum",600)] + [InlineData("min", 25)] + [InlineData("max", 225)] + [InlineData("average", 100)] + public async Task AggregationOnEntitySetWorks(string method, int expected) + { + // Arrange + string queryUrl = AggregationTestBaseUrl + "?$apply=aggregate(Orders(Price with " + method + " as TotalPrice))"; - // Act - HttpResponseMessage response = await Client.SendAsync(request); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - // Assert - var result = await response.Content.ReadAsObject(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Act + HttpResponseMessage response = await Client.SendAsync(request); + + // Assert + var result = await response.Content.ReadAsObject(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var value = result["value"]; - var orders = value.First["Orders"]; - var TotalPrice = orders.First["TotalPrice"].ToObject(); + var value = result["value"]; + var orders = value.First["Orders"]; + var TotalPrice = orders.First["TotalPrice"].ToObject(); - Assert.Equal(expected, TotalPrice); - } + Assert.Equal(expected, TotalPrice); + } - [Theory(Skip = "See the comments above")] - [InlineData("?$apply=aggregate(Orders(Price with sum as TotalPrice, Id with sum as TotalId))")] - [InlineData("?$apply=aggregate(Orders(Price with sum as TotalPrice), Orders(Id with sum as TotalId))")] - public async Task MultipleAggregationOnEntitySetWorks(string queryString) - { - // Arrange - string queryUrl = AggregationTestBaseUrl + queryString; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - - // Act - HttpResponseMessage response = Client.SendAsync(request).Result; - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = await response.Content.ReadAsObject(); - var value = result["value"]; - var orders = value.First["Orders"]; - var totalPrice = orders.First["TotalPrice"].ToObject(); - var totalId = orders.First["TotalId"].ToObject(); - - // OBS: DB uses sequential ID - // Each Customer has 2 orders that cost 25*CustomerId and 75*CustomerId - Assert.Equal(1 * (25 + 75) + 2 * (25 + 75) + 3 * (25 + 75), totalPrice); - // Sum of the 6 Orders IDs - Assert.Equal(1 + 2 + 3 + 4 + 5 + 6, totalId); - } + [Theory(Skip = "See the comments above")] + [InlineData("?$apply=aggregate(Orders(Price with sum as TotalPrice, Id with sum as TotalId))")] + [InlineData("?$apply=aggregate(Orders(Price with sum as TotalPrice), Orders(Id with sum as TotalId))")] + public async Task MultipleAggregationOnEntitySetWorks(string queryString) + { + // Arrange + string queryUrl = AggregationTestBaseUrl + queryString; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + + // Act + HttpResponseMessage response = Client.SendAsync(request).Result; + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadAsObject(); + var value = result["value"]; + var orders = value.First["Orders"]; + var totalPrice = orders.First["TotalPrice"].ToObject(); + var totalId = orders.First["TotalId"].ToObject(); + + // OBS: DB uses sequential ID + // Each Customer has 2 orders that cost 25*CustomerId and 75*CustomerId + Assert.Equal(1 * (25 + 75) + 2 * (25 + 75) + 3 * (25 + 75), totalPrice); + // Sum of the 6 Orders IDs + Assert.Equal(1 + 2 + 3 + 4 + 5 + 6, totalId); + } - [Fact] - public async Task GroupByWithAggregationAndOrderByWorks() - { - // Arrange - string queryUrl = AggregationTestBaseUrl + "?$apply=groupby((Name),aggregate(Orders(Price with sum as TotalPrice)))&$orderby=Name desc"; - string expectedResult = "{\"value\":[{\"Name\":\"Customer1\",\"Orders\":[{\"TotalPrice\":200}]},{\"Name\":\"Customer0\",\"Orders\":[{\"TotalPrice\":400}]}]}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - - // Act - HttpResponseMessage response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var stringObject = await response.Content.ReadAsStringAsync(); + [Fact] + public async Task GroupByWithAggregationAndOrderByWorks() + { + // Arrange + string queryUrl = AggregationTestBaseUrl + "?$apply=groupby((Name),aggregate(Orders(Price with sum as TotalPrice)))&$orderby=Name desc"; + string expectedResult = "{\"value\":[{\"Name\":\"Customer1\",\"Orders\":[{\"TotalPrice\":200}]},{\"Name\":\"Customer0\",\"Orders\":[{\"TotalPrice\":400}]}]}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + + // Act + HttpResponseMessage response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var stringObject = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResult, stringObject.ToString()); + Assert.Equal(expectedResult, stringObject.ToString()); - var result = await response.Content.ReadAsObject(); - var value = result["value"]; - var orders = value.First["Orders"]; - var totalPrice = orders.First["TotalPrice"].ToObject(); + var result = await response.Content.ReadAsObject(); + var value = result["value"]; + var orders = value.First["Orders"]; + var totalPrice = orders.First["TotalPrice"].ToObject(); - Assert.Equal(200, totalPrice); - } + Assert.Equal(200, totalPrice); + } - [Fact] - public async Task GroupByWithAggregationAndOrderByDynamicPropsWorks() - { - // Arrange - string queryUrl = "aggregation/Orders?$apply=groupby((Name),aggregate(Price with sum as TotalPrice))&$orderby=TotalPrice desc"; - string expectedResult = "{\"value\":[{\"Name\":\"Order61\",\"TotalPrice\":225},{\"Name\":\"Order41\",\"TotalPrice\":150},{\"Name\":\"Order21\",\"TotalPrice\":75},{\"Name\":\"Order6\",\"TotalPrice\":75},{\"Name\":\"Order4\",\"TotalPrice\":50},{\"Name\":\"Order2\",\"TotalPrice\":25}]}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + [Fact] + public async Task GroupByWithAggregationAndOrderByDynamicPropsWorks() + { + // Arrange + string queryUrl = "aggregation/Orders?$apply=groupby((Name),aggregate(Price with sum as TotalPrice))&$orderby=TotalPrice desc"; + string expectedResult = "{\"value\":[{\"Name\":\"Order61\",\"TotalPrice\":225},{\"Name\":\"Order41\",\"TotalPrice\":150},{\"Name\":\"Order21\",\"TotalPrice\":75},{\"Name\":\"Order6\",\"TotalPrice\":75},{\"Name\":\"Order4\",\"TotalPrice\":50},{\"Name\":\"Order2\",\"TotalPrice\":25}]}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - // Act - HttpResponseMessage response = await Client.SendAsync(request); + // Act + HttpResponseMessage response = await Client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var stringObject = await response.Content.ReadAsStringAsync(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var stringObject = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResult, stringObject.ToString()); - } + Assert.Equal(expectedResult, stringObject.ToString()); + } - [Fact] - public async Task GroupByMoreThanOnePropertyWithDollarTopWorks() - { - // Arrange - string queryUrl = "aggregation/Orders?$apply=groupby((Id, Name),aggregate(Price with sum as TotalPrice))&$top=1"; - string expectedResult = "{\"value\":[{\"Name\":\"Order2\",\"Id\":1,\"TotalPrice\":25}]}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + [Fact] + public async Task GroupByMoreThanOnePropertyWithDollarTopWorks() + { + // Arrange + string queryUrl = "aggregation/Orders?$apply=groupby((Id, Name),aggregate(Price with sum as TotalPrice))&$top=1"; + string expectedResult = "{\"value\":[{\"Name\":\"Order2\",\"Id\":1,\"TotalPrice\":25}]}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - // Act - HttpResponseMessage response = await Client.SendAsync(request); + // Act + HttpResponseMessage response = await Client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var stringObject = await response.Content.ReadAsStringAsync(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var stringObject = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResult, stringObject.ToString()); - } + Assert.Equal(expectedResult, stringObject.ToString()); + } - [Fact] - public async Task AggregationWithFilterWorks() - { - // Arrange - string queryUrl = AggregationTestBaseUrl + "?$apply=filter((contains(Name,'Customer1')))/aggregate(Orders(Price with sum as TotalPrice))"; - string expectedResult = "{\"value\":[{\"Orders\":[{\"TotalPrice\":200}]}]}"; + [Fact] + public async Task AggregationWithFilterWorks() + { + // Arrange + string queryUrl = AggregationTestBaseUrl + "?$apply=filter((contains(Name,'Customer1')))/aggregate(Orders(Price with sum as TotalPrice))"; + string expectedResult = "{\"value\":[{\"Orders\":[{\"TotalPrice\":200}]}]}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - // Act - HttpResponseMessage response = await Client.SendAsync(request); + // Act + HttpResponseMessage response = await Client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var stringObject = await response.Content.ReadAsStringAsync(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var stringObject = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResult, stringObject.ToString()); - } + Assert.Equal(expectedResult, stringObject.ToString()); + } - [Fact] - public async Task GroupByWithAggregationAndFilterByWorks() - { - // Arrange - string queryUrl = AggregationTestBaseUrl + "?$apply=groupby((Name),aggregate(Orders(Price with sum as TotalPrice)))&$filter=Name eq 'Customer1'"; - string expectedResult = "{\"value\":[{\"Name\":\"Customer1\",\"Orders\":[{\"TotalPrice\":200}]}]}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + [Fact] + public async Task GroupByWithAggregationAndFilterByWorks() + { + // Arrange + string queryUrl = AggregationTestBaseUrl + "?$apply=groupby((Name),aggregate(Orders(Price with sum as TotalPrice)))&$filter=Name eq 'Customer1'"; + string expectedResult = "{\"value\":[{\"Name\":\"Customer1\",\"Orders\":[{\"TotalPrice\":200}]}]}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - // Act - HttpResponseMessage response = await Client.SendAsync(request); + // Act + HttpResponseMessage response = await Client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var stringObject = await response.Content.ReadAsStringAsync(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var stringObject = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResult, stringObject.ToString()); - } + Assert.Equal(expectedResult, stringObject.ToString()); + } - [Fact(Skip = "See the comments above")] - public async Task AggregationOnEntitySetWorksWithPropertyAggregation() - { - // Arrange - string queryUrl = AggregationTestBaseUrl + "?$apply=aggregate(Id with sum as TotalId, Orders(Price with sum as TotalPrice))"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - - // Act - HttpResponseMessage response = Client.SendAsync(request).Result; - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = await response.Content.ReadAsObject(); - var value = result["value"]; - var totalId = value.First["TotalId"].ToObject(); - var orders = value.First["Orders"]; - var totalPrice = orders.First["TotalPrice"].ToObject(); - - // OBS: DB uses sequential ID - // Each Customer has 2 orders that cost 25*CustomerId and 75*CustomerId - Assert.Equal(1 * (25 + 75) + 2 * (25 + 75) + 3 * (25 + 75), totalPrice); - // Sum of the first 3 Customers IDs - Assert.Equal(1 + 2 + 3, totalId); - } + [Fact(Skip = "See the comments above")] + public async Task AggregationOnEntitySetWorksWithPropertyAggregation() + { + // Arrange + string queryUrl = AggregationTestBaseUrl + "?$apply=aggregate(Id with sum as TotalId, Orders(Price with sum as TotalPrice))"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + + // Act + HttpResponseMessage response = Client.SendAsync(request).Result; + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadAsObject(); + var value = result["value"]; + var totalId = value.First["TotalId"].ToObject(); + var orders = value.First["Orders"]; + var totalPrice = orders.First["TotalPrice"].ToObject(); + + // OBS: DB uses sequential ID + // Each Customer has 2 orders that cost 25*CustomerId and 75*CustomerId + Assert.Equal(1 * (25 + 75) + 2 * (25 + 75) + 3 * (25 + 75), totalPrice); + // Sum of the first 3 Customers IDs + Assert.Equal(1 + 2 + 3, totalId); + } - [Fact(Skip = "See the comments above")] - public async Task TestAggregationOnEntitySetsWithArithmeticOperators() - { - // Arrange - string queryUrl = AggregationTestBaseUrl + "?$apply=aggregate(Orders(Price mul Price with sum as TotalPrice))"; + [Fact(Skip = "See the comments above")] + public async Task TestAggregationOnEntitySetsWithArithmeticOperators() + { + // Arrange + string queryUrl = AggregationTestBaseUrl + "?$apply=aggregate(Orders(Price mul Price with sum as TotalPrice))"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - // Act - HttpResponseMessage response = Client.SendAsync(request).Result; + // Act + HttpResponseMessage response = Client.SendAsync(request).Result; - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = await response.Content.ReadAsObject(); - var value = result["value"]; - var orders = value.First["Orders"]; - var TotalPrice = orders.First["TotalPrice"].ToObject(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadAsObject(); + var value = result["value"]; + var orders = value.First["Orders"]; + var TotalPrice = orders.First["TotalPrice"].ToObject(); - Assert.Equal((1 + 4 + 9) * (25 * 25 + 75 * 75), TotalPrice); - } + Assert.Equal((1 + 4 + 9) * (25 * 25 + 75 * 75), TotalPrice); + } - [Fact(Skip = "See the comments above")] - public async Task TestAggregationOnEntitySetsWithArithmeticOperatorsAndPropertyNavigation() - { - // Arrange - string queryUrl = AggregationTestBaseUrl + "?$apply=aggregate(Orders(SaleInfo/Quantity mul SaleInfo/UnitPrice with sum as TotalPrice))"; + [Fact(Skip = "See the comments above")] + public async Task TestAggregationOnEntitySetsWithArithmeticOperatorsAndPropertyNavigation() + { + // Arrange + string queryUrl = AggregationTestBaseUrl + "?$apply=aggregate(Orders(SaleInfo/Quantity mul SaleInfo/UnitPrice with sum as TotalPrice))"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - // Act - HttpResponseMessage response = Client.SendAsync(request).Result; + // Act + HttpResponseMessage response = Client.SendAsync(request).Result; - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = await response.Content.ReadAsObject(); - var value = result["value"]; - var orders = value.First["Orders"]; - var TotalPrice = orders.First["TotalPrice"].ToObject(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadAsObject(); + var value = result["value"]; + var orders = value.First["Orders"]; + var TotalPrice = orders.First["TotalPrice"].ToObject(); - Assert.Equal(1 * (25 + 75) + 2 * (25 + 75) + 3 * (25 + 75), TotalPrice); - } + Assert.Equal(1 * (25 + 75) + 2 * (25 + 75) + 3 * (25 + 75), TotalPrice); + } - [Fact(Skip = "See the comments above")] - public async Task AggregationOnEntitySetWorksWithGroupby() - { - // Arrange - string queryUrl = AggregationTestBaseUrl + "?$apply=groupby((Name), aggregate(Orders(Price with sum as TotalPrice)))"; + [Fact(Skip = "See the comments above")] + public async Task AggregationOnEntitySetWorksWithGroupby() + { + // Arrange + string queryUrl = AggregationTestBaseUrl + "?$apply=groupby((Name), aggregate(Orders(Price with sum as TotalPrice)))"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - // Act - HttpResponseMessage response = Client.SendAsync(request).Result; + // Act + HttpResponseMessage response = Client.SendAsync(request).Result; - // Assert - var result = await response.Content.ReadAsObject(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var value = result["value"]; + // Assert + var result = await response.Content.ReadAsObject(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var value = result["value"]; - Assert.Equal("Customer0", value[0]["Name"].ToObject()); - Assert.Equal("Customer1", value[1]["Name"].ToObject()); + Assert.Equal("Customer0", value[0]["Name"].ToObject()); + Assert.Equal("Customer1", value[1]["Name"].ToObject()); - var customerZeroOrders = value[0]["Orders"]; - var customerZeroPrice = customerZeroOrders.First["TotalPrice"].ToObject(); - Assert.Equal(1 * (25 + 75) + 3 * (25 + 75), customerZeroPrice); + var customerZeroOrders = value[0]["Orders"]; + var customerZeroPrice = customerZeroOrders.First["TotalPrice"].ToObject(); + Assert.Equal(1 * (25 + 75) + 3 * (25 + 75), customerZeroPrice); - var customerOneOrders = value[1]["Orders"]; - var customerOnePrice = customerOneOrders.First["TotalPrice"].ToObject(); - Assert.Equal(2 * (25 + 75), customerOnePrice); - } + var customerOneOrders = value[1]["Orders"]; + var customerOnePrice = customerOneOrders.First["TotalPrice"].ToObject(); + Assert.Equal(2 * (25 + 75), customerOnePrice); } +} - public class NestedComplexPropertyAggregationTests : WebApiTestBase +public class NestedComplexPropertyAggregationTests : WebApiTestBase +{ + public NestedComplexPropertyAggregationTests(WebApiTestFixture fixture) + : base(fixture) { - public NestedComplexPropertyAggregationTests(WebApiTestFixture fixture) - : base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Employees"); + protected static void UpdateConfigureServices(IServiceCollection services) + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Employees"); - services.ConfigureControllers(typeof(EmployeesController)); + services.ConfigureControllers(typeof(EmployeesController)); - services.AddControllers().AddOData(options => options.Select().Filter().OrderBy().Expand().Count().SkipToken().SetMaxTop(null) - .AddRouteComponents("aggregation", builder.GetEdmModel())); - } + services.AddControllers().AddOData(options => options.Select().Filter().OrderBy().Expand().Count().SkipToken().SetMaxTop(null) + .AddRouteComponents("aggregation", builder.GetEdmModel())); + } - private const string AggregationTestBaseUrl = "aggregation/Employees"; + private const string AggregationTestBaseUrl = "aggregation/Employees"; - [Fact] - public async Task GroupByComplexProperty() - { - // Arrange - string queryUrl = AggregationTestBaseUrl + "?$apply=groupby((NextOfKin/Name))"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = this.CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - var result = await response.Content.ReadAsObject(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var results = result["value"] as JArray; - Assert.Equal(3, results.Count); - Assert.Equal("NoK 1", (results[0]["NextOfKin"] as JObject)["Name"].ToString()); - Assert.Equal("NoK 2", (results[1]["NextOfKin"] as JObject)["Name"].ToString()); - Assert.Equal("NoK 3", (results[2]["NextOfKin"] as JObject)["Name"].ToString()); - } + [Fact] + public async Task GroupByComplexProperty() + { + // Arrange + string queryUrl = AggregationTestBaseUrl + "?$apply=groupby((NextOfKin/Name))"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = this.CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + var result = await response.Content.ReadAsObject(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var results = result["value"] as JArray; + Assert.Equal(3, results.Count); + Assert.Equal("NoK 1", (results[0]["NextOfKin"] as JObject)["Name"].ToString()); + Assert.Equal("NoK 2", (results[1]["NextOfKin"] as JObject)["Name"].ToString()); + Assert.Equal("NoK 3", (results[2]["NextOfKin"] as JObject)["Name"].ToString()); + } - [Fact] - public async Task GroupByNestedComplexProperty() - { - // Arrange - string queryUrl = AggregationTestBaseUrl + "?$apply=groupby((NextOfKin/PhysicalAddress/City))"; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = this.CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - var result = await response.Content.ReadAsObject(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var results = result["value"] as JArray; - Assert.Equal(2, results.Count); - Assert.Equal("Redmond", ((results[0]["NextOfKin"] as JObject)["PhysicalAddress"] as JObject)["City"].ToString()); - Assert.Equal("Nairobi", ((results[1]["NextOfKin"] as JObject)["PhysicalAddress"] as JObject)["City"].ToString()); - } + [Fact] + public async Task GroupByNestedComplexProperty() + { + // Arrange + string queryUrl = AggregationTestBaseUrl + "?$apply=groupby((NextOfKin/PhysicalAddress/City))"; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = this.CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + var result = await response.Content.ReadAsObject(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var results = result["value"] as JArray; + Assert.Equal(2, results.Count); + Assert.Equal("Redmond", ((results[0]["NextOfKin"] as JObject)["PhysicalAddress"] as JObject)["City"].ToString()); + Assert.Equal("Nairobi", ((results[1]["NextOfKin"] as JObject)["PhysicalAddress"] as JObject)["City"].ToString()); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs index 09447a549..abc4f6247 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsController.cs @@ -16,291 +16,290 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Enums +namespace Microsoft.AspNetCore.OData.E2E.Tests.Enums; + +[Route("convention")] +[Route("explicit")] +public class EmployeesController : ODataController { - [Route("convention")] - [Route("explicit")] - public class EmployeesController : ODataController + public EmployeesController() { - public EmployeesController() + if (null == Employees) { - if (null == Employees) - { - InitEmployees(); - } + InitEmployees(); } + } - /// - /// static so that the data is shared among requests. - /// - private static IList Employees = null; + /// + /// static so that the data is shared among requests. + /// + private static IList Employees = null; - private void InitEmployees() + private void InitEmployees() + { + Employees = new List { - Employees = new List + new Employee() { - new Employee() + ID=1, + Name="Name1", + SkillSet=new List { Skill.CSharp, Skill.Sql }, + Gender=Gender.Female, + AccessLevel=AccessLevel.Execute, + FavoriteSports=new FavoriteSports() { - ID=1, - Name="Name1", - SkillSet=new List { Skill.CSharp, Skill.Sql }, - Gender=Gender.Female, - AccessLevel=AccessLevel.Execute, - FavoriteSports=new FavoriteSports() - { - LikeMost=Sport.Pingpong, - Like=new List{Sport.Pingpong,Sport.Basketball} - } - }, - new Employee() + LikeMost=Sport.Pingpong, + Like=new List{Sport.Pingpong,Sport.Basketball} + } + }, + new Employee() + { + ID=2,Name="Name2", + SkillSet=new List(), + Gender=Gender.Female, + AccessLevel=AccessLevel.Read, + FavoriteSports=new FavoriteSports() { - ID=2,Name="Name2", - SkillSet=new List(), - Gender=Gender.Female, - AccessLevel=AccessLevel.Read, - FavoriteSports=new FavoriteSports() - { - LikeMost=Sport.Pingpong, - Like=new List { Sport.Pingpong, Sport.Basketball } - } - }, - new Employee() + LikeMost=Sport.Pingpong, + Like=new List { Sport.Pingpong, Sport.Basketball } + } + }, + new Employee() + { + ID=3,Name="Name3", + SkillSet=new List { Skill.Web, Skill.Sql }, + Gender=Gender.Female, + AccessLevel=AccessLevel.Read|AccessLevel.Write, + FavoriteSports=new FavoriteSports() { - ID=3,Name="Name3", - SkillSet=new List { Skill.Web, Skill.Sql }, - Gender=Gender.Female, - AccessLevel=AccessLevel.Read|AccessLevel.Write, - FavoriteSports=new FavoriteSports() - { - LikeMost=Sport.Pingpong|Sport.Basketball, - Like=new List { Sport.Pingpong, Sport.Basketball } - } - }, - }; - } + LikeMost=Sport.Pingpong|Sport.Basketball, + Like=new List { Sport.Pingpong, Sport.Basketball } + } + }, + }; + } - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - public IActionResult Get() - { - return Ok(Employees.AsQueryable()); - } + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public IActionResult Get() + { + return Ok(Employees.AsQueryable()); + } - public IActionResult Get(int key) + public IActionResult Get(int key) + { + return Ok(Employees.SingleOrDefault(e => e.ID == key)); + } + + [HttpGet] + // public IActionResult FindAccessLevelFromEmployee(int key) + public IActionResult FindAccessLevel(int key) + { + if (key == 9) { - return Ok(Employees.SingleOrDefault(e => e.ID == key)); + // Special key to verify the function call + return Ok(AccessLevel.Execute); } - [HttpGet] - // public IActionResult FindAccessLevelFromEmployee(int key) - public IActionResult FindAccessLevel(int key) - { - if (key == 9) - { - // Special key to verify the function call - return Ok(AccessLevel.Execute); - } + return Ok(Employees.SingleOrDefault(e => e.ID == key).AccessLevel); + } - return Ok(Employees.SingleOrDefault(e => e.ID == key).AccessLevel); - } + public IActionResult GetNameFromEmployee(int key) + { + return Ok(Employees.SingleOrDefault(e => e.ID == key).Name); + } - public IActionResult GetNameFromEmployee(int key) - { - return Ok(Employees.SingleOrDefault(e => e.ID == key).Name); - } + [EnableQuery] + public IActionResult GetSkillSetFromEmployee(int key) + { + return Ok(Employees.SingleOrDefault(e => e.ID == key).SkillSet); + } - [EnableQuery] - public IActionResult GetSkillSetFromEmployee(int key) - { - return Ok(Employees.SingleOrDefault(e => e.ID == key).SkillSet); - } + [EnableQuery] + public IActionResult GetFavoriteSportsFromEmployee(int key) + { + var employee = Employees.SingleOrDefault(e => e.ID == key); + return Ok(employee.FavoriteSports); + } - [EnableQuery] - public IActionResult GetFavoriteSportsFromEmployee(int key) - { - var employee = Employees.SingleOrDefault(e => e.ID == key); - return Ok(employee.FavoriteSports); - } + [HttpGet("Employees({key})/FavoriteSports/LikeMost")] + public IActionResult GetFavoriteSportLikeMost(int key) + { + var firstOrDefault = Employees.FirstOrDefault(e => e.ID == key); + return Ok(firstOrDefault.FavoriteSports.LikeMost); + } - [HttpGet("Employees({key})/FavoriteSports/LikeMost")] - public IActionResult GetFavoriteSportLikeMost(int key) + public IActionResult Post([FromBody]Employee employee) + { + employee.ID = Employees.Count + 1; + Employees.Add(employee); + + return Created(employee); + } + + [HttpPost("Employees({key})/FavoriteSports/LikeMost")] + public IActionResult PostToSkillSet(int key, [FromBody]Skill newSkill) + { + Employee employee = Employees.FirstOrDefault(e => e.ID == key); + if (employee == null) { - var firstOrDefault = Employees.FirstOrDefault(e => e.ID == key); - return Ok(firstOrDefault.FavoriteSports.LikeMost); + return NotFound(); } + employee.SkillSet.Add(newSkill); + return Updated(employee.SkillSet); + } - public IActionResult Post([FromBody]Employee employee) + public IActionResult Put(int key, [FromBody]Employee employee) + { + employee.ID = key; + Employee originalEmployee = Employees.SingleOrDefault(c => c.ID == key); + + if (originalEmployee == null) { - employee.ID = Employees.Count + 1; Employees.Add(employee); return Created(employee); } - [HttpPost("Employees({key})/FavoriteSports/LikeMost")] - public IActionResult PostToSkillSet(int key, [FromBody]Skill newSkill) - { - Employee employee = Employees.FirstOrDefault(e => e.ID == key); - if (employee == null) - { - return NotFound(); - } - employee.SkillSet.Add(newSkill); - return Updated(employee.SkillSet); - } + Employees.Remove(originalEmployee); + Employees.Add(employee); + return Ok(employee); + } - public IActionResult Put(int key, [FromBody]Employee employee) - { - employee.ID = key; - Employee originalEmployee = Employees.SingleOrDefault(c => c.ID == key); + public IActionResult Patch(int key, [FromBody]Delta delta) + { + Employee originalEmployee = Employees.SingleOrDefault(c => c.ID == key); - if (originalEmployee == null) - { - Employees.Add(employee); + if (originalEmployee == null) + { + Employee temp = new Employee(); + delta.Patch(temp); + Employees.Add(temp); + return Created(temp); + } - return Created(employee); - } + delta.Patch(originalEmployee); + return Ok(delta); + } - Employees.Remove(originalEmployee); - Employees.Add(employee); - return Ok(employee); - } + public IActionResult Delete(int key) + { + IEnumerable appliedEmployees = Employees.Where(c => c.ID == key); - public IActionResult Patch(int key, [FromBody]Delta delta) + if (appliedEmployees.Count() == 0) { - Employee originalEmployee = Employees.SingleOrDefault(c => c.ID == key); - - if (originalEmployee == null) - { - Employee temp = new Employee(); - delta.Patch(temp); - Employees.Add(temp); - return Created(temp); - } - - delta.Patch(originalEmployee); - return Ok(delta); + return BadRequest(string.Format("The entry with ID {0} doesn't exist", key)); } - public IActionResult Delete(int key) + Employee employee = appliedEmployees.Single(); + Employees.Remove(employee); + return this.StatusCode(StatusCodes.Status204NoContent); + } + + [HttpPost] + public IActionResult AddSkill([FromODataUri] int key, [FromBody]ODataActionParameters parameters) + { + if (!ModelState.IsValid) { - IEnumerable appliedEmployees = Employees.Where(c => c.ID == key); + return BadRequest(); + } - if (appliedEmployees.Count() == 0) - { - return BadRequest(string.Format("The entry with ID {0} doesn't exist", key)); - } + Skill skill = (Skill)parameters["skill"]; - Employee employee = appliedEmployees.Single(); - Employees.Remove(employee); - return this.StatusCode(StatusCodes.Status204NoContent); + if (key == 6) + { + Assert.Equal(Skill.Sql, skill); + return Ok(); } - [HttpPost] - public IActionResult AddSkill([FromODataUri] int key, [FromBody]ODataActionParameters parameters) + Employee employee = Employees.FirstOrDefault(e => e.ID == key); + if (!employee.SkillSet.Contains(skill)) { - if (!ModelState.IsValid) - { - return BadRequest(); - } - - Skill skill = (Skill)parameters["skill"]; + employee.SkillSet.Add(skill); + } - if (key == 6) - { - Assert.Equal(Skill.Sql, skill); - return Ok(); - } + return Ok(employee.SkillSet); + } - Employee employee = Employees.FirstOrDefault(e => e.ID == key); - if (!employee.SkillSet.Contains(skill)) - { - employee.SkillSet.Add(skill); - } + [HttpPost("ResetDataSource")] + public IActionResult ResetDataSource() + { + this.InitEmployees(); + return this.StatusCode(StatusCodes.Status204NoContent); + } - return Ok(employee.SkillSet); + [HttpPost("SetAccessLevel")] + public IActionResult SetAccessLevel([FromBody]ODataActionParameters parameters) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); } - [HttpPost("ResetDataSource")] - public IActionResult ResetDataSource() + int ID = (int)parameters["ID"]; + AccessLevel accessLevel = (AccessLevel)parameters["accessLevel"]; + if (accessLevel.HasFlag(AccessLevel.Read) && + accessLevel.HasFlag(AccessLevel.Execute) && + ID == 7) // special { - this.InitEmployees(); - return this.StatusCode(StatusCodes.Status204NoContent); + return Ok(AccessLevel.Read | AccessLevel.Write); } - [HttpPost("SetAccessLevel")] - public IActionResult SetAccessLevel([FromBody]ODataActionParameters parameters) - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - int ID = (int)parameters["ID"]; - AccessLevel accessLevel = (AccessLevel)parameters["accessLevel"]; - if (accessLevel.HasFlag(AccessLevel.Read) && - accessLevel.HasFlag(AccessLevel.Execute) && - ID == 7) // special - { - return Ok(AccessLevel.Read | AccessLevel.Write); - } + Employee employee = Employees.FirstOrDefault(e => e.ID == ID); + employee.AccessLevel = accessLevel; + return Ok(employee.AccessLevel); + } - Employee employee = Employees.FirstOrDefault(e => e.ID == ID); - employee.AccessLevel = accessLevel; - return Ok(employee.AccessLevel); + [HttpGet] + public IActionResult GetAccessLevel([FromODataUri] int key) + { + if (!ModelState.IsValid) + { + return BadRequest(); } - [HttpGet] - public IActionResult GetAccessLevel([FromODataUri] int key) - { - if (!ModelState.IsValid) - { - return BadRequest(); - } + Employee employee = Employees.FirstOrDefault(e => e.ID == key); - Employee employee = Employees.FirstOrDefault(e => e.ID == key); + return Ok(employee.AccessLevel); + } - return Ok(employee.AccessLevel); + [HttpGet("HasAccessLevel(ID={id},AccessLevel={accessLevel})")] + public IActionResult HasAccessLevel([FromODataUri] int id, [FromODataUri] AccessLevel accessLevel) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); } - [HttpGet("HasAccessLevel(ID={id},AccessLevel={accessLevel})")] - public IActionResult HasAccessLevel([FromODataUri] int id, [FromODataUri] AccessLevel accessLevel) + if (id == 1 && accessLevel == AccessLevel.Read) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - if (id == 1 && accessLevel == AccessLevel.Read) - { - return Ok(false); - } - - if (id == 2 && accessLevel == AccessLevel.Read) - { - return Ok(true); - } + return Ok(false); + } - return BadRequest("Bad request!"); + if (id == 2 && accessLevel == AccessLevel.Read) + { + return Ok(true); } + + return BadRequest("Bad request!"); } +} - public class WeatherForecastController : ODataController +public class WeatherForecastController : ODataController +{ + private static readonly string[] Summaries = new[] { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; - [EnableQuery] - public IEnumerable Get() + [EnableQuery] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Id = index, - Status = index % 2 == 0 ? Status.SoldOut : Status.InStore, - Skill = index % 2 == 0 ? Skill.CSharp : Skill.Sql - }) - .ToArray(); - } + Id = index, + Status = index % 2 == 0 ? Status.SoldOut : Status.InStore, + Skill = index % 2 == 0 ? Skill.CSharp : Skill.Sql + }) + .ToArray(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs index deb5cdca7..795999463 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsDataModel.cs @@ -9,78 +9,77 @@ using System.Collections.Generic; using System.Runtime.Serialization; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Enums +namespace Microsoft.AspNetCore.OData.E2E.Tests.Enums; + +public class Employee { - public class Employee - { - public int ID { get; set; } + public int ID { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public List SkillSet { get; set; } + public List SkillSet { get; set; } - public Gender Gender { get; set; } + public Gender Gender { get; set; } - public AccessLevel AccessLevel { get; set; } + public AccessLevel AccessLevel { get; set; } - public FavoriteSports FavoriteSports { get; set; } - } + public FavoriteSports FavoriteSports { get; set; } +} - [Flags] - public enum AccessLevel - { - Read = 1, +[Flags] +public enum AccessLevel +{ + Read = 1, - Write = 2, + Write = 2, - Execute = 4 - } + Execute = 4 +} - public enum Gender - { - Male = 1, +public enum Gender +{ + Male = 1, - Female = 2 - } + Female = 2 +} - public enum Skill - { - CSharp, +public enum Skill +{ + CSharp, - Sql, + Sql, - Web, - } + Web, +} - public enum Sport - { - Pingpong, +public enum Sport +{ + Pingpong, - Basketball - } + Basketball +} - public class FavoriteSports - { - public Sport LikeMost { get; set; } - public List Like { get; set; } - } +public class FavoriteSports +{ + public Sport LikeMost { get; set; } + public List Like { get; set; } +} - [DataContract] - public enum Status - { - [EnumMember(Value = "Sold out")] - SoldOut, +[DataContract] +public enum Status +{ + [EnumMember(Value = "Sold out")] + SoldOut, - [EnumMember(Value = "In store")] - InStore - } + [EnumMember(Value = "In store")] + InStore +} - public class WeatherForecast - { - public int Id { get; set; } +public class WeatherForecast +{ + public int Id { get; set; } - public Status Status { get; set; } + public Status Status { get; set; } - public Skill Skill { get; set; } - } + public Skill Skill { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsEdmModel.cs index fce0706ae..3b9fe2b24 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsEdmModel.cs @@ -8,98 +8,97 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Enums +namespace Microsoft.AspNetCore.OData.E2E.Tests.Enums; + +internal class EnumsEdmModel { - internal class EnumsEdmModel + public static IEdmModel GetExplicitModel() + { + ODataModelBuilder builder = new ODataModelBuilder(); + var employee = builder.EntityType(); + employee.HasKey(c => c.ID); + employee.Property(c => c.Name); + employee.CollectionProperty(c => c.SkillSet); + employee.EnumProperty(c => c.Gender); + employee.EnumProperty(c => c.AccessLevel); + employee.ComplexProperty(c => c.FavoriteSports); + + var skill = builder.EnumType(); + skill.Member(Skill.CSharp); + skill.Member(Skill.Sql); + skill.Member(Skill.Web); + + var gender = builder.EnumType(); + gender.Member(Gender.Female); + gender.Member(Gender.Male); + + var accessLevel = builder.EnumType(); + accessLevel.Member(AccessLevel.Execute); + accessLevel.Member(AccessLevel.Read); + accessLevel.Member(AccessLevel.Write); + + var favoriteSports = builder.ComplexType(); + favoriteSports.EnumProperty(f => f.LikeMost); + favoriteSports.CollectionProperty(f => f.Like); + + var sport = builder.EnumType(); + sport.Member(Sport.Basketball); + sport.Member(Sport.Pingpong); + + AddBoundActionsAndFunctions(employee); + AddUnboundActionsAndFunctions(builder); + + EntitySetConfiguration employees = builder.EntitySet("Employees"); + builder.Namespace = typeof(Employee).Namespace; + return builder.GetEdmModel(); + } + + public static IEdmModel GetConventionModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + EntitySetConfiguration employees = builder.EntitySet("Employees"); + EntityTypeConfiguration employee = employees.EntityType; + + // maybe following lines are not required once bug #1587 is fixed. + // 1587: It's better to support automatically adding actions and functions in ODataConventionModelBuilder. + AddBoundActionsAndFunctions(employee); + AddUnboundActionsAndFunctions(builder); + + builder.Namespace = typeof(Employee).Namespace; + + var edmModel = builder.GetEdmModel(); + return edmModel; + } + + public static IEdmModel GetEnumAliasModel() { - public static IEdmModel GetExplicitModel() - { - ODataModelBuilder builder = new ODataModelBuilder(); - var employee = builder.EntityType(); - employee.HasKey(c => c.ID); - employee.Property(c => c.Name); - employee.CollectionProperty(c => c.SkillSet); - employee.EnumProperty(c => c.Gender); - employee.EnumProperty(c => c.AccessLevel); - employee.ComplexProperty(c => c.FavoriteSports); - - var skill = builder.EnumType(); - skill.Member(Skill.CSharp); - skill.Member(Skill.Sql); - skill.Member(Skill.Web); - - var gender = builder.EnumType(); - gender.Member(Gender.Female); - gender.Member(Gender.Male); - - var accessLevel = builder.EnumType(); - accessLevel.Member(AccessLevel.Execute); - accessLevel.Member(AccessLevel.Read); - accessLevel.Member(AccessLevel.Write); - - var favoriteSports = builder.ComplexType(); - favoriteSports.EnumProperty(f => f.LikeMost); - favoriteSports.CollectionProperty(f => f.Like); - - var sport = builder.EnumType(); - sport.Member(Sport.Basketball); - sport.Member(Sport.Pingpong); - - AddBoundActionsAndFunctions(employee); - AddUnboundActionsAndFunctions(builder); - - EntitySetConfiguration employees = builder.EntitySet("Employees"); - builder.Namespace = typeof(Employee).Namespace; - return builder.GetEdmModel(); - } - - public static IEdmModel GetConventionModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - EntitySetConfiguration employees = builder.EntitySet("Employees"); - EntityTypeConfiguration employee = employees.EntityType; - - // maybe following lines are not required once bug #1587 is fixed. - // 1587: It's better to support automatically adding actions and functions in ODataConventionModelBuilder. - AddBoundActionsAndFunctions(employee); - AddUnboundActionsAndFunctions(builder); - - builder.Namespace = typeof(Employee).Namespace; - - var edmModel = builder.GetEdmModel(); - return edmModel; - } - - public static IEdmModel GetEnumAliasModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("WeatherForecast"); - return builder.GetEdmModel(); - } - - private static void AddBoundActionsAndFunctions(EntityTypeConfiguration employee) - { - var actionConfiguration = employee.Action("AddSkill"); - actionConfiguration.Parameter("skill"); - actionConfiguration.ReturnsCollection(); - - var functionConfiguration = employee.Function("FindAccessLevel"); - functionConfiguration.Returns(); - } - - private static void AddUnboundActionsAndFunctions(ODataModelBuilder odataModelBuilder) - { - var actionConfiguration = odataModelBuilder.Action("SetAccessLevel"); - actionConfiguration.Parameter("ID"); - actionConfiguration.Parameter("accessLevel"); - actionConfiguration.Returns(); - - var functionConfiguration = odataModelBuilder.Function("HasAccessLevel"); - functionConfiguration.Parameter("ID"); - functionConfiguration.Parameter("AccessLevel"); - functionConfiguration.Returns(); - - var actionConfiguration2 = odataModelBuilder.Action("ResetDataSource"); - } + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("WeatherForecast"); + return builder.GetEdmModel(); + } + + private static void AddBoundActionsAndFunctions(EntityTypeConfiguration employee) + { + var actionConfiguration = employee.Action("AddSkill"); + actionConfiguration.Parameter("skill"); + actionConfiguration.ReturnsCollection(); + + var functionConfiguration = employee.Function("FindAccessLevel"); + functionConfiguration.Returns(); + } + + private static void AddUnboundActionsAndFunctions(ODataModelBuilder odataModelBuilder) + { + var actionConfiguration = odataModelBuilder.Action("SetAccessLevel"); + actionConfiguration.Parameter("ID"); + actionConfiguration.Parameter("accessLevel"); + actionConfiguration.Returns(); + + var functionConfiguration = odataModelBuilder.Function("HasAccessLevel"); + functionConfiguration.Parameter("ID"); + functionConfiguration.Parameter("AccessLevel"); + functionConfiguration.Returns(); + + var actionConfiguration2 = odataModelBuilder.Action("ResetDataSource"); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs index 01055c8b3..9fa8baf87 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Enums/EnumsTest.cs @@ -22,705 +22,704 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Enums +namespace Microsoft.AspNetCore.OData.E2E.Tests.Enums; + +public class EnumsTest : WebApiTestBase { - public class EnumsTest : WebApiTestBase + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) { - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(EmployeesController), typeof(MetadataController)); + services.ConfigureControllers(typeof(EmployeesController), typeof(MetadataController)); - IEdmModel model1 = EnumsEdmModel.GetConventionModel(); - IEdmModel model2 = EnumsEdmModel.GetExplicitModel(); + IEdmModel model1 = EnumsEdmModel.GetConventionModel(); + IEdmModel model2 = EnumsEdmModel.GetExplicitModel(); - services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5) - .AddRouteComponents("convention", model1) - .AddRouteComponents("explicit", model2)); - } + services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5) + .AddRouteComponents("convention", model1) + .AddRouteComponents("explicit", model2)); + } - public EnumsTest(WebApiTestFixture fixture) - : base(fixture) - { - } + public EnumsTest(WebApiTestFixture fixture) + : base(fixture) + { + } #region ModelBuilder - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task ModelBuilderTest(string modelMode) - { - // Arrange - string requestUri = string.Format("{0}/$metadata", modelMode); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); - - // Assert - var stream = await response.Content.ReadAsStreamAsync(); - - IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); - var reader = new ODataMessageReader(message); - var edmModel = reader.ReadMetadataDocument(); - - var container = edmModel.EntityContainer; - Assert.Equal("Container", container.Name); - - var favoriteSports = edmModel.SchemaElements.OfType().First(); - Assert.Equal("FavoriteSports", favoriteSports.Name); - Assert.Equal(2, favoriteSports.Properties().Count()); - - //ComplexType Enum Property - var likeMost = favoriteSports.Properties().SingleOrDefault(p => p.Name == "LikeMost"); - Assert.True(likeMost.Type.IsEnum()); - - //ComplexType Enum Property - var like = favoriteSports.Properties().SingleOrDefault(p => p.Name == "Like"); - Assert.True(like.Type.IsCollection()); - - var employee = edmModel.SchemaElements.SingleOrDefault(e => e.Name == "Employee") as IEdmEntityType; - Assert.Single(employee.Key()); - Assert.Equal("ID", employee.Key().First().Name); - Assert.Equal(6, employee.Properties().Count()); - - //Entity Enum Collection Property - var skillSet = employee.Properties().SingleOrDefault(p => p.Name == "SkillSet"); - Assert.True(skillSet.Type.IsCollection()); - - //Entity Enum Property - var gender = employee.Properties().SingleOrDefault(p => p.Name == "Gender"); - Assert.True(gender.Type.IsEnum()); - var edmEnumType = gender.Type.Definition as IEdmEnumType; - Assert.False(edmEnumType.IsFlags); - - var accessLevel = employee.Properties().SingleOrDefault(p => p.Name == "AccessLevel") as IEdmStructuralProperty; - edmEnumType = accessLevel.Type.Definition as IEdmEnumType; - Assert.Equal(3, edmEnumType.Members.Count()); - Assert.True(edmEnumType.IsFlags); - - //Action AddSkill - var iEdmOperation = edmModel.FindOperations(typeof(Employee).Namespace + ".AddSkill").FirstOrDefault(); - var iEdmOperationParameter = iEdmOperation.Parameters.SingleOrDefault(p => p.Name == "skill"); - var definition = iEdmOperationParameter.Type.Definition; - Assert.Equal(EdmTypeKind.Enum, definition.TypeKind); - - var iEdmCollectionTpeReference = iEdmOperation.ReturnType as IEdmCollectionTypeReference; - var iEdmTypeReference = iEdmCollectionTpeReference.ElementType(); - Assert.Equal(EdmTypeKind.Enum, iEdmTypeReference.Definition.TypeKind); - - // Action SetAccessLevel - var iEdmOperationOfSetAccessLevel = edmModel.FindOperations(typeof(Employee).Namespace + ".SetAccessLevel").FirstOrDefault(); - var iEdmOperationParameterOfAccessLevel = iEdmOperationOfSetAccessLevel.Parameters.SingleOrDefault(p => p.Name == "accessLevel"); - var definitionOfAccessLevel = iEdmOperationParameterOfAccessLevel.Type.Definition; - Assert.Equal(EdmTypeKind.Enum, definitionOfAccessLevel.TypeKind); - - var iEdmTpeReferenceOfAccessLevel = iEdmOperationOfSetAccessLevel.ReturnType; - Assert.Equal(EdmTypeKind.Enum, iEdmTpeReferenceOfAccessLevel.Definition.TypeKind); - - // Function GetAccessLevel - var iEdmOperationOfGetAccessLevel = edmModel.FindDeclaredOperations(typeof(Employee).Namespace + ".FindAccessLevel").FirstOrDefault(); - var iEdmTypeReferenceOfGetAccessLevel = iEdmOperationOfGetAccessLevel.ReturnType; - Assert.Equal(EdmTypeKind.Enum, iEdmTypeReferenceOfGetAccessLevel.Definition.TypeKind); - - // Function HasAccessLevel - var iEdmOperationOfHasAccessLevel = edmModel.FindDeclaredOperations(typeof(Employee).Namespace + ".HasAccessLevel").FirstOrDefault(); - var iEdmOperationParameterOfHasAccessLevel = iEdmOperationOfHasAccessLevel.Parameters.SingleOrDefault(p => p.Name == "AccessLevel"); - Assert.Equal(EdmTypeKind.Enum, iEdmOperationParameterOfHasAccessLevel.Type.Definition.TypeKind); - } + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task ModelBuilderTest(string modelMode) + { + // Arrange + string requestUri = string.Format("{0}/$metadata", modelMode); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + + // Assert + var stream = await response.Content.ReadAsStreamAsync(); + + IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); + var reader = new ODataMessageReader(message); + var edmModel = reader.ReadMetadataDocument(); + + var container = edmModel.EntityContainer; + Assert.Equal("Container", container.Name); + + var favoriteSports = edmModel.SchemaElements.OfType().First(); + Assert.Equal("FavoriteSports", favoriteSports.Name); + Assert.Equal(2, favoriteSports.Properties().Count()); + + //ComplexType Enum Property + var likeMost = favoriteSports.Properties().SingleOrDefault(p => p.Name == "LikeMost"); + Assert.True(likeMost.Type.IsEnum()); + + //ComplexType Enum Property + var like = favoriteSports.Properties().SingleOrDefault(p => p.Name == "Like"); + Assert.True(like.Type.IsCollection()); + + var employee = edmModel.SchemaElements.SingleOrDefault(e => e.Name == "Employee") as IEdmEntityType; + Assert.Single(employee.Key()); + Assert.Equal("ID", employee.Key().First().Name); + Assert.Equal(6, employee.Properties().Count()); + + //Entity Enum Collection Property + var skillSet = employee.Properties().SingleOrDefault(p => p.Name == "SkillSet"); + Assert.True(skillSet.Type.IsCollection()); + + //Entity Enum Property + var gender = employee.Properties().SingleOrDefault(p => p.Name == "Gender"); + Assert.True(gender.Type.IsEnum()); + var edmEnumType = gender.Type.Definition as IEdmEnumType; + Assert.False(edmEnumType.IsFlags); + + var accessLevel = employee.Properties().SingleOrDefault(p => p.Name == "AccessLevel") as IEdmStructuralProperty; + edmEnumType = accessLevel.Type.Definition as IEdmEnumType; + Assert.Equal(3, edmEnumType.Members.Count()); + Assert.True(edmEnumType.IsFlags); + + //Action AddSkill + var iEdmOperation = edmModel.FindOperations(typeof(Employee).Namespace + ".AddSkill").FirstOrDefault(); + var iEdmOperationParameter = iEdmOperation.Parameters.SingleOrDefault(p => p.Name == "skill"); + var definition = iEdmOperationParameter.Type.Definition; + Assert.Equal(EdmTypeKind.Enum, definition.TypeKind); + + var iEdmCollectionTpeReference = iEdmOperation.ReturnType as IEdmCollectionTypeReference; + var iEdmTypeReference = iEdmCollectionTpeReference.ElementType(); + Assert.Equal(EdmTypeKind.Enum, iEdmTypeReference.Definition.TypeKind); + + // Action SetAccessLevel + var iEdmOperationOfSetAccessLevel = edmModel.FindOperations(typeof(Employee).Namespace + ".SetAccessLevel").FirstOrDefault(); + var iEdmOperationParameterOfAccessLevel = iEdmOperationOfSetAccessLevel.Parameters.SingleOrDefault(p => p.Name == "accessLevel"); + var definitionOfAccessLevel = iEdmOperationParameterOfAccessLevel.Type.Definition; + Assert.Equal(EdmTypeKind.Enum, definitionOfAccessLevel.TypeKind); + + var iEdmTpeReferenceOfAccessLevel = iEdmOperationOfSetAccessLevel.ReturnType; + Assert.Equal(EdmTypeKind.Enum, iEdmTpeReferenceOfAccessLevel.Definition.TypeKind); + + // Function GetAccessLevel + var iEdmOperationOfGetAccessLevel = edmModel.FindDeclaredOperations(typeof(Employee).Namespace + ".FindAccessLevel").FirstOrDefault(); + var iEdmTypeReferenceOfGetAccessLevel = iEdmOperationOfGetAccessLevel.ReturnType; + Assert.Equal(EdmTypeKind.Enum, iEdmTypeReferenceOfGetAccessLevel.Definition.TypeKind); + + // Function HasAccessLevel + var iEdmOperationOfHasAccessLevel = edmModel.FindDeclaredOperations(typeof(Employee).Namespace + ".HasAccessLevel").FirstOrDefault(); + var iEdmOperationParameterOfHasAccessLevel = iEdmOperationOfHasAccessLevel.Parameters.SingleOrDefault(p => p.Name == "AccessLevel"); + Assert.Equal(EdmTypeKind.Enum, iEdmOperationParameterOfHasAccessLevel.Type.Definition.TypeKind); + } #endregion - [Theory] - [InlineData("application/json;odata.metadata=full")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=none")] - public async Task QueryEntitySet(string format) - { - // Arrange - await ResetDatasource(); - string requestUri = "/convention/Employees?$format=" + format; - HttpClient client = CreateClient(); + [Theory] + [InlineData("application/json;odata.metadata=full")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=none")] + public async Task QueryEntitySet(string format) + { + // Arrange + await ResetDatasource(); + string requestUri = "/convention/Employees?$format=" + format; + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - // Assert - Assert.True(response.IsSuccessStatusCode); + // Assert + Assert.True(response.IsSuccessStatusCode); - var json = await response.Content.ReadAsObject(); - var results = json.GetValue("value") as JArray; - Assert.Equal(3, results.Count); - if (format == "application/json;odata.metadata=full") - { - var typeOfAccessLevel = results[0]["AccessLevel@odata.type"].ToString(); - Assert.Equal("#Microsoft.AspNetCore.OData.E2E.Tests.Enums.AccessLevel", typeOfAccessLevel); - - var typeOfSkillSet = results[0]["SkillSet@odata.type"].ToString(); - Assert.Equal("#Collection(Microsoft.AspNetCore.OData.E2E.Tests.Enums.Skill)", typeOfSkillSet); - } + var json = await response.Content.ReadAsObject(); + var results = json.GetValue("value") as JArray; + Assert.Equal(3, results.Count); + if (format == "application/json;odata.metadata=full") + { + var typeOfAccessLevel = results[0]["AccessLevel@odata.type"].ToString(); + Assert.Equal("#Microsoft.AspNetCore.OData.E2E.Tests.Enums.AccessLevel", typeOfAccessLevel); + + var typeOfSkillSet = results[0]["SkillSet@odata.type"].ToString(); + Assert.Equal("#Collection(Microsoft.AspNetCore.OData.E2E.Tests.Enums.Skill)", typeOfSkillSet); } + } - [Theory] - [InlineData("/convention/Employees/$count", 3)] - [InlineData("/convention/Employees/$count?$filter=Name eq 'Name1'", 1)] - public async Task QueryEntitySetCount(string requestUri, int expectedCount) - { - // Arrange - await ResetDatasource(); - HttpClient client = CreateClient(); + [Theory] + [InlineData("/convention/Employees/$count", 3)] + [InlineData("/convention/Employees/$count?$filter=Name eq 'Name1'", 1)] + public async Task QueryEntitySetCount(string requestUri, int expectedCount) + { + // Arrange + await ResetDatasource(); + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - // Assert - Assert.True(response.IsSuccessStatusCode); - string count = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedCount, int.Parse(count)); - } + // Assert + Assert.True(response.IsSuccessStatusCode); + string count = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedCount, int.Parse(count)); + } - [Theory] - [InlineData("/convention/Employees/$count", true)] - [InlineData("/convention/Employees/$count", false)] - public async Task QueryEntitySetCountEncoding(string requestUri, bool sendAcceptCharset) + [Theory] + [InlineData("/convention/Employees/$count", true)] + [InlineData("/convention/Employees/$count", false)] + public async Task QueryEntitySetCountEncoding(string requestUri, bool sendAcceptCharset) + { + // Arrange + await ResetDatasource(); + HttpClient client = CreateClient(); + client.DefaultRequestHeaders.AcceptCharset.Clear(); + if (sendAcceptCharset) { - // Arrange - await ResetDatasource(); - HttpClient client = CreateClient(); - client.DefaultRequestHeaders.AcceptCharset.Clear(); - if (sendAcceptCharset) - { - client.DefaultRequestHeaders.AcceptCharset.ParseAdd("utf-8"); - } - - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); - - // Assert - Assert.True(response.IsSuccessStatusCode); - var blob = await response.Content.ReadAsByteArrayAsync(); - Assert.Equal("utf-8", response.Content.Headers.ContentType.CharSet, StringComparer.OrdinalIgnoreCase); - var count = Encoding.UTF8.GetString(blob); - Assert.Equal(3, int.Parse(count)); - Assert.False((blob[0] == 0xEF) && (blob[1] == 0xBB) && (blob[2] == 0xBF)); + client.DefaultRequestHeaders.AcceptCharset.ParseAdd("utf-8"); } - [Theory] - [InlineData("/convention/Employees(1)/SkillSet/$count", 2)] - [InlineData("/convention/Employees(1)/SkillSet/$count?$filter=$it eq Microsoft.AspNetCore.OData.E2E.Tests.Enums.Skill'Sql'", 1)] - public async Task QuerySkillSetCount(string requestUri, int expectedCount) - { - // Arrange - await ResetDatasource(); - HttpClient client = CreateClient(); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + // Assert + Assert.True(response.IsSuccessStatusCode); + var blob = await response.Content.ReadAsByteArrayAsync(); + Assert.Equal("utf-8", response.Content.Headers.ContentType.CharSet, StringComparer.OrdinalIgnoreCase); + var count = Encoding.UTF8.GetString(blob); + Assert.Equal(3, int.Parse(count)); + Assert.False((blob[0] == 0xEF) && (blob[1] == 0xBB) && (blob[2] == 0xBF)); + } - // Assert - Assert.True(response.IsSuccessStatusCode); - string count = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedCount, int.Parse(count)); - } + [Theory] + [InlineData("/convention/Employees(1)/SkillSet/$count", 2)] + [InlineData("/convention/Employees(1)/SkillSet/$count?$filter=$it eq Microsoft.AspNetCore.OData.E2E.Tests.Enums.Skill'Sql'", 1)] + public async Task QuerySkillSetCount(string requestUri, int expectedCount) + { + // Arrange + await ResetDatasource(); + HttpClient client = CreateClient(); - [Theory] - [InlineData("application/json;odata.metadata=full")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=none")] - public async Task QueryEnumPropertyInEntityType(string format) - { - await ResetDatasource(); - string requestUri = "/convention/Employees(1)/AccessLevel?$format=" + format; - HttpClient client = CreateClient(); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.True(response.IsSuccessStatusCode); + // Assert + Assert.True(response.IsSuccessStatusCode); + string count = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedCount, int.Parse(count)); + } - var json = await response.Content.ReadAsObject(); - var value = json.GetValue("value").ToString(); - Assert.Equal("Execute", value); - if (format != "application/json;odata.metadata=none") - { - var context = json.GetValue("@odata.context").ToString(); - Assert.True(context.IndexOf("/$metadata#Employees(1)/AccessLevel") > 0); - } - - requestUri = "/convention/Employees(1)/SkillSet?$format=" + format; - response = await client.GetAsync(requestUri); - json = await response.Content.ReadAsObject(); - JArray skillSet = json["value"] as JArray; - Assert.Equal(2, skillSet.Count); - - Assert.Equal("CSharp", (string)skillSet[0]); - Assert.Equal("Sql", (string)skillSet[1]); - if (format != "application/json;odata.metadata=none") - { - var context = json["@odata.context"].ToString(); - Assert.True(context.IndexOf("/$metadata#Collection(Microsoft.AspNetCore.OData.E2E.Tests.Enums.Skill)") >= 0); - } + [Theory] + [InlineData("application/json;odata.metadata=full")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=none")] + public async Task QueryEnumPropertyInEntityType(string format) + { + await ResetDatasource(); + string requestUri = "/convention/Employees(1)/AccessLevel?$format=" + format; + HttpClient client = CreateClient(); + + HttpResponseMessage response = await client.GetAsync(requestUri); + Assert.True(response.IsSuccessStatusCode); + + var json = await response.Content.ReadAsObject(); + var value = json.GetValue("value").ToString(); + Assert.Equal("Execute", value); + if (format != "application/json;odata.metadata=none") + { + var context = json.GetValue("@odata.context").ToString(); + Assert.True(context.IndexOf("/$metadata#Employees(1)/AccessLevel") > 0); } - [Theory] - [InlineData("application/json;odata.metadata=full")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=none")] - public async Task QueryEnumPropertyValueInEntityType(string format) + requestUri = "/convention/Employees(1)/SkillSet?$format=" + format; + response = await client.GetAsync(requestUri); + json = await response.Content.ReadAsObject(); + JArray skillSet = json["value"] as JArray; + Assert.Equal(2, skillSet.Count); + + Assert.Equal("CSharp", (string)skillSet[0]); + Assert.Equal("Sql", (string)skillSet[1]); + if (format != "application/json;odata.metadata=none") { - await ResetDatasource(); - HttpClient client = CreateClient(); + var context = json["@odata.context"].ToString(); + Assert.True(context.IndexOf("/$metadata#Collection(Microsoft.AspNetCore.OData.E2E.Tests.Enums.Skill)") >= 0); + } + } - var requestUri = "/convention/Employees(1)/AccessLevel/$value?$format=" + format; - var response = await client.GetAsync(requestUri); + [Theory] + [InlineData("application/json;odata.metadata=full")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=none")] + public async Task QueryEnumPropertyValueInEntityType(string format) + { + await ResetDatasource(); + HttpClient client = CreateClient(); - response.EnsureSuccessStatusCode(); + var requestUri = "/convention/Employees(1)/AccessLevel/$value?$format=" + format; + var response = await client.GetAsync(requestUri); - var content = await response.Content.ReadAsStringAsync(); - AccessLevel actual; - Assert.True(Enum.TryParse(content, out actual)); - Assert.Equal(AccessLevel.Execute, actual); - } + response.EnsureSuccessStatusCode(); - [Theory] - [InlineData("application/json;odata.metadata=full")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=none")] - public async Task QueryEnumPropertyInComplexType(string format) - { - await ResetDatasource(); - HttpClient client = CreateClient(); + var content = await response.Content.ReadAsStringAsync(); + AccessLevel actual; + Assert.True(Enum.TryParse(content, out actual)); + Assert.Equal(AccessLevel.Execute, actual); + } - string requestUri = "/convention/Employees(1)/FavoriteSports?$format=" + format; + [Theory] + [InlineData("application/json;odata.metadata=full")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=none")] + public async Task QueryEnumPropertyInComplexType(string format) + { + await ResetDatasource(); + HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.True(response.IsSuccessStatusCode); + string requestUri = "/convention/Employees(1)/FavoriteSports?$format=" + format; - var result = await response.Content.ReadAsObject(); - var value = result.GetValue("LikeMost").ToString(); - Assert.Equal("Pingpong", value); - value = result.GetValue("Like").ToString(); - Assert.Equal(@"[""Pingpong"",""Basketball""]", value.Replace("\r\n", "").Replace(" ", "")); - if (format == "application/json;odata.metadata=full") - { - var context = result.GetValue("@odata.context").ToString(); - Assert.True(context.IndexOf("/$metadata#Employees(1)/FavoriteSports") > 0); - } - - requestUri = "/convention/Employees(1)/FavoriteSports/LikeMost?$format=" + format; - response = await client.GetAsync(requestUri); - result = await response.Content.ReadAsObject(); - value = result.GetValue("value").ToString(); - Assert.Equal("Pingpong", value); - } + HttpResponseMessage response = await client.GetAsync(requestUri); + Assert.True(response.IsSuccessStatusCode); - [Theory] - [InlineData("application/json;odata.metadata=full")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=none")] - public async Task QueryEntity(string format) + var result = await response.Content.ReadAsObject(); + var value = result.GetValue("LikeMost").ToString(); + Assert.Equal("Pingpong", value); + value = result.GetValue("Like").ToString(); + Assert.Equal(@"[""Pingpong"",""Basketball""]", value.Replace("\r\n", "").Replace(" ", "")); + if (format == "application/json;odata.metadata=full") { - await ResetDatasource(); - HttpClient client = CreateClient(); - string requestUri = "/convention/Employees(1)?$format=" + format; + var context = result.GetValue("@odata.context").ToString(); + Assert.True(context.IndexOf("/$metadata#Employees(1)/FavoriteSports") > 0); + } - HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.True(response.IsSuccessStatusCode); + requestUri = "/convention/Employees(1)/FavoriteSports/LikeMost?$format=" + format; + response = await client.GetAsync(requestUri); + result = await response.Content.ReadAsObject(); + value = result.GetValue("value").ToString(); + Assert.Equal("Pingpong", value); + } - var result = await response.Content.ReadAsObject(); - if (format == "application/json;odata.metadata=full") - { - var typeOfAccessLevel = result["AccessLevel@odata.type"].ToString(); - Assert.Equal("#Microsoft.AspNetCore.OData.E2E.Tests.Enums.AccessLevel", typeOfAccessLevel); - - var typeOfSkillSet = result["SkillSet@odata.type"].ToString(); - Assert.Equal("#Collection(Microsoft.AspNetCore.OData.E2E.Tests.Enums.Skill)", typeOfSkillSet); - } - } + [Theory] + [InlineData("application/json;odata.metadata=full")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=none")] + public async Task QueryEntity(string format) + { + await ResetDatasource(); + HttpClient client = CreateClient(); + string requestUri = "/convention/Employees(1)?$format=" + format; - [Theory] - [InlineData("application/json;odata.metadata=full")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=none")] - public async Task QueryEntitiesFilterByEnum(string format) + HttpResponseMessage response = await client.GetAsync(requestUri); + Assert.True(response.IsSuccessStatusCode); + + var result = await response.Content.ReadAsObject(); + if (format == "application/json;odata.metadata=full") { - await ResetDatasource(); - HttpClient client = CreateClient(); - - // in the template {0}: operation, {1}: typename, {2}: enum value, {3}: format - string uriTemplate = "/convention/Employees?$filter=AccessLevel {0} {1}'{2}'&$format={3}"; - string uriEq = string.Format(uriTemplate, "eq", typeof(AccessLevel).FullName, AccessLevel.Read.ToString(), format); - string uriHas = string.Format(uriTemplate, "has", typeof(AccessLevel).FullName, AccessLevel.Read.ToString(), format); - - using (var response = await client.GetAsync(uriEq)) - { - // http:///convention/Employees?$filter=AccessLevel eq Microsoft.Test.E2E.AspNet.OData.Enums.AccessLevel'Read'&$format= - Assert.True(response.IsSuccessStatusCode); - - var result = await response.Content.ReadAsObject(); - var value = result.GetValue("value") as JArray; - Assert.NotNull(value); - Assert.Single(value); - } - - using (var response = await client.GetAsync(uriHas)) - { - // http:///convention/Employees?$filter=AccessLevel has Microsoft.Test.E2E.AspNet.OData.Enums.AccessLevel'Read'&$format= - Assert.True(response.IsSuccessStatusCode); - - var result = await response.Content.ReadAsObject(); - var value = result.GetValue("value") as JArray; - Assert.NotNull(value); - Assert.Equal(2, value.Count); - } + var typeOfAccessLevel = result["AccessLevel@odata.type"].ToString(); + Assert.Equal("#Microsoft.AspNetCore.OData.E2E.Tests.Enums.AccessLevel", typeOfAccessLevel); + + var typeOfSkillSet = result["SkillSet@odata.type"].ToString(); + Assert.Equal("#Collection(Microsoft.AspNetCore.OData.E2E.Tests.Enums.Skill)", typeOfSkillSet); } + } - [Theory] - [InlineData("application/json;odata.metadata=full")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=none")] - public async Task EnumInSelect(string format) - { - await ResetDatasource(); - HttpClient client = CreateClient(); + [Theory] + [InlineData("application/json;odata.metadata=full")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=none")] + public async Task QueryEntitiesFilterByEnum(string format) + { + await ResetDatasource(); + HttpClient client = CreateClient(); - string requestUri = "/convention/Employees?$select=AccessLevel,SkillSet,FavoriteSports&$format=" + format; + // in the template {0}: operation, {1}: typename, {2}: enum value, {3}: format + string uriTemplate = "/convention/Employees?$filter=AccessLevel {0} {1}'{2}'&$format={3}"; + string uriEq = string.Format(uriTemplate, "eq", typeof(AccessLevel).FullName, AccessLevel.Read.ToString(), format); + string uriHas = string.Format(uriTemplate, "has", typeof(AccessLevel).FullName, AccessLevel.Read.ToString(), format); - HttpResponseMessage response = await client.GetAsync(requestUri); + using (var response = await client.GetAsync(uriEq)) + { + // http:///convention/Employees?$filter=AccessLevel eq Microsoft.Test.E2E.AspNet.OData.Enums.AccessLevel'Read'&$format= Assert.True(response.IsSuccessStatusCode); var result = await response.Content.ReadAsObject(); - var value = result.GetValue("value") as JArray; - Assert.Equal(3, value.Count); + Assert.NotNull(value); + Assert.Single(value); } - [Theory] - [InlineData("application/json;odata.metadata=full")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=none")] - public async Task EnumInOrderBy(string format) + using (var response = await client.GetAsync(uriHas)) { - await ResetDatasource(); - HttpClient client = CreateClient(); - string requestUri = "/convention/Employees?$orderby=AccessLevel,FavoriteSports/LikeMost&$format=" + format; - - HttpResponseMessage response = await client.GetAsync(requestUri); + // http:///convention/Employees?$filter=AccessLevel has Microsoft.Test.E2E.AspNet.OData.Enums.AccessLevel'Read'&$format= Assert.True(response.IsSuccessStatusCode); var result = await response.Content.ReadAsObject(); - var value = result.GetValue("value") as JArray; - Assert.Equal(3, value.Count); + Assert.NotNull(value); + Assert.Equal(2, value.Count); + } + } - var firstEmployee = value[0]; - Assert.Equal(2, firstEmployee["ID"]); + [Theory] + [InlineData("application/json;odata.metadata=full")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=none")] + public async Task EnumInSelect(string format) + { + await ResetDatasource(); + HttpClient client = CreateClient(); - var secondEmployee = value[1]; - Assert.Equal(3, secondEmployee["ID"]); - } + string requestUri = "/convention/Employees?$select=AccessLevel,SkillSet,FavoriteSports&$format=" + format; + + HttpResponseMessage response = await client.GetAsync(requestUri); + Assert.True(response.IsSuccessStatusCode); + + var result = await response.Content.ReadAsObject(); + + var value = result.GetValue("value") as JArray; + Assert.Equal(3, value.Count); + } + + [Theory] + [InlineData("application/json;odata.metadata=full")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=none")] + public async Task EnumInOrderBy(string format) + { + await ResetDatasource(); + HttpClient client = CreateClient(); + string requestUri = "/convention/Employees?$orderby=AccessLevel,FavoriteSports/LikeMost&$format=" + format; + + HttpResponseMessage response = await client.GetAsync(requestUri); + Assert.True(response.IsSuccessStatusCode); + + var result = await response.Content.ReadAsObject(); + + var value = result.GetValue("value") as JArray; + Assert.Equal(3, value.Count); + + var firstEmployee = value[0]; + Assert.Equal(2, firstEmployee["ID"]); + + var secondEmployee = value[1]; + Assert.Equal(3, secondEmployee["ID"]); + } #region Update - //[Fact] - //public async Task AddEntity() - //{ - // await ResetDatasource(); - // string requestUri = "/convention/Employees?$format=application/json;odata.metadata=none"; - - // using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) - // { - // response.EnsureSuccessStatusCode(); - - // var json = await response.Content.ReadAsObject(); - // var result = json.GetValue("value") as JArray; - // Assert.Equal(3, result.Count); - // } - - // var postUri = "/convention/Employees"; - - // var postContent = JObject.Parse(@"{""ID"":1, - // ""Name"":""Name2"", - // ""SkillSet"":[""Sql""], - // ""Gender"":""Female"", - // ""AccessLevel"":""Read,Write"", - // ""FavoriteSports"":{ - // ""LikeMost"":""Pingpong"", - // ""Like"":[""Pingpong"",""Basketball""] - // }}"); - // using (HttpResponseMessage response = await this.Client.PostAsJsonAsync(postUri, postContent)) - // { - // Assert.Equal(HttpStatusCode.Created, response.StatusCode); - // } - - // using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) - // { - // response.EnsureSuccessStatusCode(); - - // var json = await response.Content.ReadAsObject(); - // var result = json.GetValue("value") as JArray; - // Assert.Equal(4, result.Count); - // } - //} - - [Fact] - public async Task PostToEnumCollection() + //[Fact] + //public async Task AddEntity() + //{ + // await ResetDatasource(); + // string requestUri = "/convention/Employees?$format=application/json;odata.metadata=none"; + + // using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + // { + // response.EnsureSuccessStatusCode(); + + // var json = await response.Content.ReadAsObject(); + // var result = json.GetValue("value") as JArray; + // Assert.Equal(3, result.Count); + // } + + // var postUri = "/convention/Employees"; + + // var postContent = JObject.Parse(@"{""ID"":1, + // ""Name"":""Name2"", + // ""SkillSet"":[""Sql""], + // ""Gender"":""Female"", + // ""AccessLevel"":""Read,Write"", + // ""FavoriteSports"":{ + // ""LikeMost"":""Pingpong"", + // ""Like"":[""Pingpong"",""Basketball""] + // }}"); + // using (HttpResponseMessage response = await this.Client.PostAsJsonAsync(postUri, postContent)) + // { + // Assert.Equal(HttpStatusCode.Created, response.StatusCode); + // } + + // using (HttpResponseMessage response = await this.Client.GetAsync(requestUri)) + // { + // response.EnsureSuccessStatusCode(); + + // var json = await response.Content.ReadAsObject(); + // var result = json.GetValue("value") as JArray; + // Assert.Equal(4, result.Count); + // } + //} + + [Fact] + public async Task PostToEnumCollection() + { + //Arrange + await ResetDatasource(); + HttpClient client = CreateClient(); + string requestUri = "/convention/Employees/2/SkillSet?$format=application/json;odata.metadata=none"; + //Get the count before the post + int count = 0; + using (HttpResponseMessage response = await client.GetAsync(requestUri)) { - //Arrange - await ResetDatasource(); - HttpClient client = CreateClient(); - string requestUri = "/convention/Employees/2/SkillSet?$format=application/json;odata.metadata=none"; - //Get the count before the post - int count = 0; - using (HttpResponseMessage response = await client.GetAsync(requestUri)) - { - response.EnsureSuccessStatusCode(); - - var json = await response.Content.ReadAsObject(); - var result = json.GetValue("value") as JArray; - count = result.Count; - } - - //Set up the post request - var requestForPost = new HttpRequestMessage(HttpMethod.Post, requestUri); - requestForPost.Content = new StringContent(content: @"{ - 'value':'Sql' - }", encoding: Encoding.UTF8, mediaType: "application/json"); - - //Act - using (HttpResponseMessage response = await client.SendAsync(requestForPost)) - { - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } - - //Assert - using (HttpResponseMessage response = await client.GetAsync(requestUri)) - { - response.EnsureSuccessStatusCode(); - - var json = await response.Content.ReadAsObject(); - var result = json.GetValue("value") as JArray; - Assert.True(count + 1 == result.Count, - String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2}", - count + 1, - result.Count, - requestUri)); - } + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + count = result.Count; } - [Fact] - public async Task UpdateEntity() + //Set up the post request + var requestForPost = new HttpRequestMessage(HttpMethod.Post, requestUri); + requestForPost.Content = new StringContent(content: @"{ + 'value':'Sql' + }", encoding: Encoding.UTF8, mediaType: "application/json"); + + //Act + using (HttpResponseMessage response = await client.SendAsync(requestForPost)) { - await ResetDatasource(); - HttpClient client = CreateClient(); - string getUri = "/convention/Employees(2)?$format=application/json;odata.metadata=none"; - - using (HttpResponseMessage response = await client.GetAsync(getUri)) - { - response.EnsureSuccessStatusCode(); - - var json = await response.Content.ReadAsObject(); - var accessLevel = json.GetValue("AccessLevel").ToString(); - Assert.Equal("Read", accessLevel); - - var skillSet = json.GetValue("SkillSet").ToString(); - Assert.Equal("[]", skillSet); - - var favoriteSport = json["FavoriteSports"]["LikeMost"].ToString(); - Assert.Equal("Pingpong", favoriteSport); - - var sports = json["FavoriteSports"]["Like"].ToString(); - Assert.Equal(@"[""Pingpong"",""Basketball""]", sports.Replace("\r\n", "").Replace(" ", "")); - } - - var putUri = "/convention/Employees(2)"; - var putContent = JObject.Parse(@"{""ID"":2, - ""Name"":""Name2"", - ""SkillSet"":[""Sql""], - ""Gender"":""Female"", - ""AccessLevel"":""Execute,Write"", - ""FavoriteSports"":{ - ""LikeMost"":""Basketball"", - ""Like"":[""Pingpong"",""Basketball""] - }}"); - using (HttpResponseMessage response = await client.PutAsJsonAsync(putUri, putContent)) - { - response.EnsureSuccessStatusCode(); - } - - using (HttpResponseMessage response = await client.GetAsync(getUri)) - { - response.EnsureSuccessStatusCode(); - - var json = await response.Content.ReadAsObject(); - - var accessLevel = json.GetValue("AccessLevel"); - Assert.Equal("Write, Execute", accessLevel); - - var skillSet = json.GetValue("SkillSet").ToString(); - Assert.Equal(@"[""Sql""]", skillSet.Replace("\r\n", "").Replace(" ", "")); - - var favoriteSport = json["FavoriteSports"]["LikeMost"].ToString(); - Assert.Equal("Basketball", favoriteSport); - - var sports = json["FavoriteSports"]["Like"].ToString(); - Assert.Equal(@"[""Pingpong"",""Basketball""]", sports.Replace("\r\n", "").Replace(" ", "")); - } + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } - [Theory] - [InlineData("PUT")] - [InlineData("PATCH")] - public async Task UpsertEntity(string method) + //Assert + using (HttpResponseMessage response = await client.GetAsync(requestUri)) { - await ResetDatasource(); - HttpClient client = CreateClient(); - - var requestUri = "/convention/Employees(20)"; - var requestContent = @"{""ID"":20, - ""Name"":""Name2"", - ""SkillSet"":[""Sql""], - ""Gender"":""Female"", - ""AccessLevel"":""Execute,Write"", - ""FavoriteSports"":{ - ""LikeMost"":""Basketball"", - ""Like"":[""Pingpong"",""Basketball""] - }}"; - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), requestUri); - request.Content = new StringContent(requestContent); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = requestContent.Length; - request.Headers.Add("Prefer", "return=minimal"); - using (HttpResponseMessage response = await client.SendAsync(request)) - { - Assert.True(HttpStatusCode.NoContent == response.StatusCode, - string.Format("Response code is not right, expected: {0}, actual: {1}", HttpStatusCode.NoContent, response.StatusCode)); - Assert.True(response.Headers.Contains("OData-EntityId"), "The response should contain Header 'OData-EntityId'"); - Assert.True(response.Headers.Contains("Location"), "The response should contain Header 'Location'"); - Assert.True(response.Headers.Contains("OData-Version"), "The response should contain Header 'OData-Version'"); - } - } + response.EnsureSuccessStatusCode(); -#endregion + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + Assert.True(count + 1 == result.Count, + String.Format("\nExpected count: {0},\n actual: {1},\n request uri: {2}", + count + 1, + result.Count, + requestUri)); + } + } -#region Delete + [Fact] + public async Task UpdateEntity() + { + await ResetDatasource(); + HttpClient client = CreateClient(); + string getUri = "/convention/Employees(2)?$format=application/json;odata.metadata=none"; - [Fact] - public async Task DeleteEntity() + using (HttpResponseMessage response = await client.GetAsync(getUri)) { - await ResetDatasource(); - HttpClient client = CreateClient(); - string uriGet = "/convention/Employees?$format=application/json;odata.metadata=none"; - - using (HttpResponseMessage response = await client.GetAsync(uriGet)) - { - response.EnsureSuccessStatusCode(); - - var json = await response.Content.ReadAsObject(); - var values = json.GetValue("value") as JArray; - Assert.Equal(3, values.Count); - } - - var uriDelete = "/convention/Employees(1)"; - using (HttpResponseMessage response = await client.DeleteAsync(uriDelete)) - { - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } - - using (HttpResponseMessage response = await client.GetAsync(uriGet)) - { - response.EnsureSuccessStatusCode(); - - var json = await response.Content.ReadAsObject(); - var values = json.GetValue("value") as JArray; - Assert.Equal(2, values.Count); - } - } + response.EnsureSuccessStatusCode(); -#endregion + var json = await response.Content.ReadAsObject(); + var accessLevel = json.GetValue("AccessLevel").ToString(); + Assert.Equal("Read", accessLevel); -#region Enum with action + var skillSet = json.GetValue("SkillSet").ToString(); + Assert.Equal("[]", skillSet); + + var favoriteSport = json["FavoriteSports"]["LikeMost"].ToString(); + Assert.Equal("Pingpong", favoriteSport); + + var sports = json["FavoriteSports"]["Like"].ToString(); + Assert.Equal(@"[""Pingpong"",""Basketball""]", sports.Replace("\r\n", "").Replace(" ", "")); + } - [Fact] - public async Task EnumInActionParameter() + var putUri = "/convention/Employees(2)"; + var putContent = JObject.Parse(@"{""ID"":2, + ""Name"":""Name2"", + ""SkillSet"":[""Sql""], + ""Gender"":""Female"", + ""AccessLevel"":""Execute,Write"", + ""FavoriteSports"":{ + ""LikeMost"":""Basketball"", + ""Like"":[""Pingpong"",""Basketball""] + }}"); + using (HttpResponseMessage response = await client.PutAsJsonAsync(putUri, putContent)) { - // Arrange - string postUri = "/convention/Employees(6)/Microsoft.AspNetCore.OData.E2E.Tests.Enums.AddSkill"; - string payload = @"{""skill"":""Sql""}"; - var postContent = new StringContent(payload); - postContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - postContent.Headers.ContentLength = payload.Length; - HttpClient client = CreateClient(); - - // Act - var response = await client.PostAsync(postUri, postContent); - - // Assert response.EnsureSuccessStatusCode(); } - [Fact] - public async Task EnumInActionOutput() + using (HttpResponseMessage response = await client.GetAsync(getUri)) { - // Arrange - HttpClient client = CreateClient(); - var postUri = client.BaseAddress + "convention/SetAccessLevel"; - var postContent = JObject.Parse(@"{""accessLevel"":""Read,Execute"",""ID"":7}"); - - // Act - var response = await client.PostAsJsonAsync(postUri, postContent); - - // Assert response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsObject(); - var value = json["value"].ToString(); - Assert.Equal("Read, Write", value); + var accessLevel = json.GetValue("AccessLevel"); + Assert.Equal("Write, Execute", accessLevel); + + var skillSet = json.GetValue("SkillSet").ToString(); + Assert.Equal(@"[""Sql""]", skillSet.Replace("\r\n", "").Replace(" ", "")); + + var favoriteSport = json["FavoriteSports"]["LikeMost"].ToString(); + Assert.Equal("Basketball", favoriteSport); + + var sports = json["FavoriteSports"]["Like"].ToString(); + Assert.Equal(@"[""Pingpong"",""Basketball""]", sports.Replace("\r\n", "").Replace(" ", "")); } + } + + [Theory] + [InlineData("PUT")] + [InlineData("PATCH")] + public async Task UpsertEntity(string method) + { + await ResetDatasource(); + HttpClient client = CreateClient(); + + var requestUri = "/convention/Employees(20)"; + var requestContent = @"{""ID"":20, + ""Name"":""Name2"", + ""SkillSet"":[""Sql""], + ""Gender"":""Female"", + ""AccessLevel"":""Execute,Write"", + ""FavoriteSports"":{ + ""LikeMost"":""Basketball"", + ""Like"":[""Pingpong"",""Basketball""] + }}"; + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), requestUri); + request.Content = new StringContent(requestContent); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = requestContent.Length; + request.Headers.Add("Prefer", "return=minimal"); + using (HttpResponseMessage response = await client.SendAsync(request)) + { + Assert.True(HttpStatusCode.NoContent == response.StatusCode, + string.Format("Response code is not right, expected: {0}, actual: {1}", HttpStatusCode.NoContent, response.StatusCode)); + Assert.True(response.Headers.Contains("OData-EntityId"), "The response should contain Header 'OData-EntityId'"); + Assert.True(response.Headers.Contains("Location"), "The response should contain Header 'Location'"); + Assert.True(response.Headers.Contains("OData-Version"), "The response should contain Header 'OData-Version'"); + } + } #endregion -#region Enum with function +#region Delete - [Fact] - public async Task EnumInFunctionOutput() - { - // Arrange - HttpClient client = CreateClient(); - var getUri = "/convention/Employees(9)/Microsoft.AspNetCore.OData.E2E.Tests.Enums.FindAccessLevel()"; - var response = await client.GetAsync(getUri); + [Fact] + public async Task DeleteEntity() + { + await ResetDatasource(); + HttpClient client = CreateClient(); + string uriGet = "/convention/Employees?$format=application/json;odata.metadata=none"; - // Act & Assert + using (HttpResponseMessage response = await client.GetAsync(uriGet)) + { response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsObject(); - var value = json["value"].ToString(); - Assert.Equal("Execute", value); + var values = json.GetValue("value") as JArray; + Assert.Equal(3, values.Count); } - [Theory] - [InlineData("/convention/HasAccessLevel(ID=1,AccessLevel=Microsoft.AspNetCore.OData.E2E.Tests.Enums.AccessLevel'Read')", false)] - [InlineData("/convention/HasAccessLevel(ID=2,AccessLevel=Microsoft.AspNetCore.OData.E2E.Tests.Enums.AccessLevel'1')", true)] - public async Task EnumInFunctionParameter(string requestUri, bool expectedValue) + var uriDelete = "/convention/Employees(1)"; + using (HttpResponseMessage response = await client.DeleteAsync(uriDelete)) { - // Arrange - HttpClient client = CreateClient(); - var response = await client.GetAsync(requestUri); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } - // Act & Assert + using (HttpResponseMessage response = await client.GetAsync(uriGet)) + { response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsObject(); - var actualValue = json["value"].Value(); - Assert.Equal(expectedValue, actualValue); + var values = json.GetValue("value") as JArray; + Assert.Equal(2, values.Count); } + } #endregion - private async Task ResetDatasource() - { - HttpClient client = CreateClient(); - var response = await client.PostAsync("convention/ResetDataSource", null); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - return response; - } +#region Enum with action + + [Fact] + public async Task EnumInActionParameter() + { + // Arrange + string postUri = "/convention/Employees(6)/Microsoft.AspNetCore.OData.E2E.Tests.Enums.AddSkill"; + string payload = @"{""skill"":""Sql""}"; + var postContent = new StringContent(payload); + postContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + postContent.Headers.ContentLength = payload.Length; + HttpClient client = CreateClient(); + + // Act + var response = await client.PostAsync(postUri, postContent); + + // Assert + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task EnumInActionOutput() + { + // Arrange + HttpClient client = CreateClient(); + var postUri = client.BaseAddress + "convention/SetAccessLevel"; + var postContent = JObject.Parse(@"{""accessLevel"":""Read,Execute"",""ID"":7}"); + + // Act + var response = await client.PostAsJsonAsync(postUri, postContent); + + // Assert + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var value = json["value"].ToString(); + + Assert.Equal("Read, Write", value); + } + +#endregion + +#region Enum with function + + [Fact] + public async Task EnumInFunctionOutput() + { + // Arrange + HttpClient client = CreateClient(); + var getUri = "/convention/Employees(9)/Microsoft.AspNetCore.OData.E2E.Tests.Enums.FindAccessLevel()"; + var response = await client.GetAsync(getUri); + + // Act & Assert + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var value = json["value"].ToString(); + Assert.Equal("Execute", value); + } + + [Theory] + [InlineData("/convention/HasAccessLevel(ID=1,AccessLevel=Microsoft.AspNetCore.OData.E2E.Tests.Enums.AccessLevel'Read')", false)] + [InlineData("/convention/HasAccessLevel(ID=2,AccessLevel=Microsoft.AspNetCore.OData.E2E.Tests.Enums.AccessLevel'1')", true)] + public async Task EnumInFunctionParameter(string requestUri, bool expectedValue) + { + // Arrange + HttpClient client = CreateClient(); + var response = await client.GetAsync(requestUri); + + // Act & Assert + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsObject(); + var actualValue = json["value"].Value(); + Assert.Equal(expectedValue, actualValue); + } + +#endregion + + private async Task ResetDatasource() + { + HttpClient client = CreateClient(); + var response = await client.PostAsync("convention/ResetDataSource", null); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + return response; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Extensions/HttpContentExtensions.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Extensions/HttpContentExtensions.cs index f14745a2c..8465fa579 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Extensions/HttpContentExtensions.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Extensions/HttpContentExtensions.cs @@ -10,58 +10,57 @@ using System.Text.Json; using Newtonsoft.Json; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Extensions +namespace Microsoft.AspNetCore.OData.E2E.Tests.Extensions; + +/// +/// Extensions for HttpContent. +/// +public static class HttpContentExtensions { /// - /// Extensions for HttpContent. + /// Get the content as the value of ObjectContent. /// - public static class HttpContentExtensions + /// The content value. + public static string AsObjectContentValue(this HttpContent content) { - /// - /// Get the content as the value of ObjectContent. - /// - /// The content value. - public static string AsObjectContentValue(this HttpContent content) + string json = content.ReadAsStringAsync().Result; + try { - string json = content.ReadAsStringAsync().Result; - try - { - using JsonDocument document = JsonDocument.Parse(json); + using JsonDocument document = JsonDocument.Parse(json); - JsonElement root = document.RootElement; - - return root.GetProperty("value").GetString(); - } - catch (System.Text.Json.JsonException) - { - } + JsonElement root = document.RootElement; - return json; + return root.GetProperty("value").GetString(); } - - /// - /// A custom extension for AspNetCore to deserialize JSON content as an object. - /// AspNet provides this in System.Net.Http.Formatting. - /// - /// The content value. - public static async Task ReadAsObject(this HttpContent content) + catch (System.Text.Json.JsonException) { - string json = await content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json); } - /// - /// Get the content as the value of JsonElement. - /// - /// The content. - /// The content as Json element. - public static async Task ReadAsElement(this HttpContent content) - { - string json = await content.ReadAsStringAsync(); + return json; + } - using JsonDocument document = JsonDocument.Parse(json); + /// + /// A custom extension for AspNetCore to deserialize JSON content as an object. + /// AspNet provides this in System.Net.Http.Formatting. + /// + /// The content value. + public static async Task ReadAsObject(this HttpContent content) + { + string json = await content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } - return document.RootElement; - } + /// + /// Get the content as the value of JsonElement. + /// + /// The content. + /// The content as Json element. + public static async Task ReadAsElement(this HttpContent content) + { + string json = await content.ReadAsStringAsync(); + + using JsonDocument document = JsonDocument.Parse(json); + + return document.RootElement; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableController.cs index c902ffafc..a69c1fe4d 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableController.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -12,124 +12,123 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.IAsyncEnumerableTests +namespace Microsoft.AspNetCore.OData.E2E.Tests.IAsyncEnumerableTests; + +public class CustomersController : ODataController { - public class CustomersController : ODataController + private readonly IAsyncEnumerableContext _context; + + public CustomersController(IAsyncEnumerableContext context) { - private readonly IAsyncEnumerableContext _context; + context.Database.EnsureCreated(); + _context = context; - public CustomersController(IAsyncEnumerableContext context) + if (!_context.Customers.Any()) { - context.Database.EnsureCreated(); - _context = context; - - if (!_context.Customers.Any()) - { - Generate(); - } + Generate(); } + } - [EnableQuery] - [HttpGet("v1/Customers")] - public IAsyncEnumerable CustomersData() - { - IAsyncEnumerable customers = CreateCollectionAsync(); + [EnableQuery] + [HttpGet("v1/Customers")] + public IAsyncEnumerable CustomersData() + { + IAsyncEnumerable customers = CreateCollectionAsync(); - return customers; - } + return customers; + } - [EnableQuery] - [HttpGet("odata/Customers")] - public IAsyncEnumerable Get() - { - return _context.Customers.AsAsyncEnumerable(); - } + [EnableQuery] + [HttpGet("odata/Customers")] + public IAsyncEnumerable Get() + { + return _context.Customers.AsAsyncEnumerable(); + } - [EnableQuery] - [HttpGet("v2/Customers")] - public ActionResult> CustomersDataNew() - { - return Ok(_context.Customers.AsAsyncEnumerable()); - } + [EnableQuery] + [HttpGet("v2/Customers")] + public ActionResult> CustomersDataNew() + { + return Ok(_context.Customers.AsAsyncEnumerable()); + } - public async IAsyncEnumerable CreateCollectionAsync() + public async IAsyncEnumerable CreateCollectionAsync() + { + await Task.Delay(5); + // Yield the items one by one asynchronously + yield return new Customer { - await Task.Delay(5); - // Yield the items one by one asynchronously - yield return new Customer - { - Id = 1, - Name = "Customer1", - Orders = new List { - new Order { - Name = "Order1", - Price = 25 - }, - new Order { - Name = "Order2", - Price = 75 - } + Id = 1, + Name = "Customer1", + Orders = new List { + new Order { + Name = "Order1", + Price = 25 }, - Address = new Address - { - Name = "City1", - Street = "Street1" + new Order { + Name = "Order2", + Price = 75 } - }; + }, + Address = new Address + { + Name = "City1", + Street = "Street1" + } + }; - await Task.Delay(5); + await Task.Delay(5); - yield return new Customer - { - Id = 2, - Name = "Customer2", - Orders = new List { - new Order { - Name = "Order1", - Price = 35 - }, - new Order { - Name = "Order2", - Price = 65 - } + yield return new Customer + { + Id = 2, + Name = "Customer2", + Orders = new List { + new Order { + Name = "Order1", + Price = 35 }, - Address = new Address - { - Name = "City2", - Street = "Street2" + new Order { + Name = "Order2", + Price = 65 } - }; - } + }, + Address = new Address + { + Name = "City2", + Street = "Street2" + } + }; + } - public void Generate() + public void Generate() + { + for (int i = 1; i <= 3; i++) { - for (int i = 1; i <= 3; i++) + var customer = new Customer { - var customer = new Customer - { - Name = "Customer" + (i + 1) % 2, - Orders = - new List { - new Order { - Name = "Order" + 2*i, - Price = i * 25 - }, - new Order { - Name = "Order" + 2*i+1, - Price = i * 75 - } + Name = "Customer" + (i + 1) % 2, + Orders = + new List { + new Order { + Name = "Order" + 2*i, + Price = i * 25 }, - Address = new Address - { - Name = "City" + i % 2, - Street = "Street" + i % 2, - } - }; - - _context.Customers.Add(customer); - } + new Order { + Name = "Order" + 2*i+1, + Price = i * 75 + } + }, + Address = new Address + { + Name = "City" + i % 2, + Street = "Street" + i % 2, + } + }; - _context.SaveChanges(); + _context.Customers.Add(customer); } + + _context.SaveChanges(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableDataModel.cs index 8aff1a864..c4e892bb1 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableDataModel.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -8,47 +8,46 @@ using System.Collections.Generic; using Microsoft.EntityFrameworkCore; -namespace Microsoft.AspNetCore.OData.E2E.Tests.IAsyncEnumerableTests +namespace Microsoft.AspNetCore.OData.E2E.Tests.IAsyncEnumerableTests; + +public class IAsyncEnumerableContext : DbContext { - public class IAsyncEnumerableContext : DbContext + public IAsyncEnumerableContext(DbContextOptions options) + : base(options) { - public IAsyncEnumerableContext(DbContextOptions options) - : base(options) - { - } + } - public DbSet Customers { get; set; } + public DbSet Customers { get; set; } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().OwnsOne(c => c.Address).WithOwner(); - } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().OwnsOne(c => c.Address).WithOwner(); } +} - public class Customer - { - public int Id { get; set; } +public class Customer +{ + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public Address Address { get; set; } + public Address Address { get; set; } - public IList Orders { get; set; } - } + public IList Orders { get; set; } +} - public class Order - { - public int Id { get; set; } +public class Order +{ + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public int Price { get; set; } - } + public int Price { get; set; } +} - public class Address - { - public string Name { get; set; } +public class Address +{ + public string Name { get; set; } - public string Street { get; set; } - } + public string Street { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableEdmModel.cs index 3e76c333a..8c4d4ae8e 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableEdmModel.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -8,17 +8,16 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.IAsyncEnumerableTests +namespace Microsoft.AspNetCore.OData.E2E.Tests.IAsyncEnumerableTests; + +public class IAsyncEnumerableEdmModel { - public class IAsyncEnumerableEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Orders"); - IEdmModel model = builder.GetEdmModel(); - return model; - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); + IEdmModel model = builder.GetEdmModel(); + return model; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableTests.cs index 0f9f143ee..3afab2610 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableTests.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -17,97 +17,96 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.IAsyncEnumerableTests -{ - public class IAsyncEnumerableTests : WebODataTestBase - { - public class TestsStartup : TestStartupBase - { - public override void ConfigureServices(IServiceCollection services) - { - services.AddDbContext(opt => opt.UseInMemoryDatabase("IAsyncEnumerableTest")); +namespace Microsoft.AspNetCore.OData.E2E.Tests.IAsyncEnumerableTests; - services.ConfigureControllers(typeof(CustomersController)); +public class IAsyncEnumerableTests : WebODataTestBase +{ + public class TestsStartup : TestStartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddDbContext(opt => opt.UseInMemoryDatabase("IAsyncEnumerableTest")); - IEdmModel edmModel = IAsyncEnumerableEdmModel.GetEdmModel(); - services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(null) - .AddRouteComponents("odata", edmModel)); + services.ConfigureControllers(typeof(CustomersController)); - services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(null) - .AddRouteComponents("v1", edmModel)); + IEdmModel edmModel = IAsyncEnumerableEdmModel.GetEdmModel(); + services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(null) + .AddRouteComponents("odata", edmModel)); - services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(null) - .AddRouteComponents("v2", edmModel)); - } - } + services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(null) + .AddRouteComponents("v1", edmModel)); - public IAsyncEnumerableTests(WebODataTestFixture factory) - : base(factory) - { + services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(null) + .AddRouteComponents("v2", edmModel)); } + } - [Fact] - public async Task UsingAsAsyncEnumerableWorks() - { - // Arrange - string queryUrl = "odata/Customers"; - var expectedResult = "{\"@odata.context\":\"http://localhost/odata/$metadata#Customers\",\"value\":[{\"Id\":1,\"Name\":\"Customer0\",\"Address\":{\"Name\":\"City1\",\"Street\":\"Street1\"}},{\"Id\":2,\"Name\":\"Customer1\",\"Address\":{\"Name\":\"City0\",\"Street\":\"Street0\"}},{\"Id\":3,\"Name\":\"Customer0\",\"Address\":{\"Name\":\"City1\",\"Street\":\"Street1\"}}]}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - - // Act - HttpResponseMessage response = await Client.SendAsync(request); + public IAsyncEnumerableTests(WebODataTestFixture factory) + : base(factory) + { + } + + [Fact] + public async Task UsingAsAsyncEnumerableWorks() + { + // Arrange + string queryUrl = "odata/Customers"; + var expectedResult = "{\"@odata.context\":\"http://localhost/odata/$metadata#Customers\",\"value\":[{\"Id\":1,\"Name\":\"Customer0\",\"Address\":{\"Name\":\"City1\",\"Street\":\"Street1\"}},{\"Id\":2,\"Name\":\"Customer1\",\"Address\":{\"Name\":\"City0\",\"Street\":\"Street0\"}},{\"Id\":3,\"Name\":\"Customer0\",\"Address\":{\"Name\":\"City1\",\"Street\":\"Street1\"}}]}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + HttpResponseMessage response = await Client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var resultObject = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResult, resultObject); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var resultObject = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResult, resultObject); - List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); - Assert.Equal(3, customers.Count); - } + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + Assert.Equal(3, customers.Count); + } - [Fact] - public async Task UsingAsAsyncEnumerableWorksWithoutEFCore() - { - // Arrange - string queryUrl = "v1/Customers"; - var expectedResult = "{\"@odata.context\":\"http://localhost/v1/$metadata#Customers\",\"value\":[{\"Id\":1,\"Name\":\"Customer1\",\"Address\":{\"Name\":\"City1\",\"Street\":\"Street1\"}},{\"Id\":2,\"Name\":\"Customer2\",\"Address\":{\"Name\":\"City2\",\"Street\":\"Street2\"}}]}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - - // Act - HttpResponseMessage response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var resultObject = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResult, resultObject); - - List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); - Assert.Equal(2, customers.Count); - } + [Fact] + public async Task UsingAsAsyncEnumerableWorksWithoutEFCore() + { + // Arrange + string queryUrl = "v1/Customers"; + var expectedResult = "{\"@odata.context\":\"http://localhost/v1/$metadata#Customers\",\"value\":[{\"Id\":1,\"Name\":\"Customer1\",\"Address\":{\"Name\":\"City1\",\"Street\":\"Street1\"}},{\"Id\":2,\"Name\":\"Customer2\",\"Address\":{\"Name\":\"City2\",\"Street\":\"Street2\"}}]}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + HttpResponseMessage response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var resultObject = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResult, resultObject); + + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + Assert.Equal(2, customers.Count); + } - [Fact] - public async Task UsingAsAsyncEnumerableWithActionResultWorks() - { - // Arrange - string queryUrl = "v2/Customers?$expand=Orders"; - var expectedResult = "{\"@odata.context\":\"http://localhost/v2/$metadata#Customers(Orders())\",\"value\":[{\"Id\":1,\"Name\":\"Customer0\",\"Address\":{\"Name\":\"City1\",\"Street\":\"Street1\"},\"Orders\":[{\"Id\":1,\"Name\":\"Order2\",\"Price\":25},{\"Id\":2,\"Name\":\"Order21\",\"Price\":75}]},{\"Id\":2,\"Name\":\"Customer1\",\"Address\":{\"Name\":\"City0\",\"Street\":\"Street0\"},\"Orders\":[{\"Id\":3,\"Name\":\"Order4\",\"Price\":50},{\"Id\":4,\"Name\":\"Order41\",\"Price\":150}]},{\"Id\":3,\"Name\":\"Customer0\",\"Address\":{\"Name\":\"City1\",\"Street\":\"Street1\"},\"Orders\":[{\"Id\":5,\"Name\":\"Order6\",\"Price\":75},{\"Id\":6,\"Name\":\"Order61\",\"Price\":225}]}]}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - - // Act - HttpResponseMessage response = await Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var resultObject = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResult, resultObject); - - List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); - Assert.Equal(3, customers.Count); - Assert.Equal(2, customers[0].Orders.Count); - } + [Fact] + public async Task UsingAsAsyncEnumerableWithActionResultWorks() + { + // Arrange + string queryUrl = "v2/Customers?$expand=Orders"; + var expectedResult = "{\"@odata.context\":\"http://localhost/v2/$metadata#Customers(Orders())\",\"value\":[{\"Id\":1,\"Name\":\"Customer0\",\"Address\":{\"Name\":\"City1\",\"Street\":\"Street1\"},\"Orders\":[{\"Id\":1,\"Name\":\"Order2\",\"Price\":25},{\"Id\":2,\"Name\":\"Order21\",\"Price\":75}]},{\"Id\":2,\"Name\":\"Customer1\",\"Address\":{\"Name\":\"City0\",\"Street\":\"Street0\"},\"Orders\":[{\"Id\":3,\"Name\":\"Order4\",\"Price\":50},{\"Id\":4,\"Name\":\"Order41\",\"Price\":150}]},{\"Id\":3,\"Name\":\"Customer0\",\"Address\":{\"Name\":\"City1\",\"Street\":\"Street1\"},\"Orders\":[{\"Id\":5,\"Name\":\"Order6\",\"Price\":75},{\"Id\":6,\"Name\":\"Order61\",\"Price\":225}]}]}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + HttpResponseMessage response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var resultObject = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResult, resultObject); + + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + Assert.Equal(3, customers.Count); + Assert.Equal(2, customers[0].Orders.Count); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesController.cs index 2e90d4cf6..72ee46c21 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesController.cs @@ -11,27 +11,26 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.MediaTypes +namespace Microsoft.AspNetCore.OData.E2E.Tests.MediaTypes; + +public class OrdersController : ODataController { - public class OrdersController : ODataController + [EnableQuery] + public ActionResult> Get() { - [EnableQuery] - public ActionResult> Get() - { - return MediaTypesDataSource.Orders; - } - - [EnableQuery] - public ActionResult Get(int key) - { - var order = MediaTypesDataSource.Orders.SingleOrDefault(d => d.Id == key); + return MediaTypesDataSource.Orders; + } - if (order == null) - { - return NotFound(); - } + [EnableQuery] + public ActionResult Get(int key) + { + var order = MediaTypesDataSource.Orders.SingleOrDefault(d => d.Id == key); - return order; + if (order == null) + { + return NotFound(); } + + return order; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesDataModel.cs index 4eb0eb270..c1200ac93 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesDataModel.cs @@ -5,12 +5,11 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.E2E.Tests.MediaTypes +namespace Microsoft.AspNetCore.OData.E2E.Tests.MediaTypes; + +public class Order { - public class Order - { - public int Id { get; set; } - public decimal Amount { get; set; } - public long TrackingNumber { get; set; } - } + public int Id { get; set; } + public decimal Amount { get; set; } + public long TrackingNumber { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesDataSource.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesDataSource.cs index 235232443..8c6e91525 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesDataSource.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesDataSource.cs @@ -7,20 +7,19 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.MediaTypes +namespace Microsoft.AspNetCore.OData.E2E.Tests.MediaTypes; + +internal static class MediaTypesDataSource { - internal static class MediaTypesDataSource - { - private readonly static List orders; + private readonly static List orders; - static MediaTypesDataSource() + static MediaTypesDataSource() + { + orders = new List { - orders = new List - { - new Order { Id = 1, Amount = 130, TrackingNumber = 9223372036854775807L } - }; - } - - public static List Orders => orders; + new Order { Id = 1, Amount = 130, TrackingNumber = 9223372036854775807L } + }; } + + public static List Orders => orders; } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesEdmModel.cs index 672e352c6..8e5fcbcbc 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesEdmModel.cs @@ -8,16 +8,15 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.MediaTypes +namespace Microsoft.AspNetCore.OData.E2E.Tests.MediaTypes; + +public class MediaTypesEdmModel { - public class MediaTypesEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var modelBuilder = new ODataConventionModelBuilder(); - modelBuilder.EntitySet("Orders"); + var modelBuilder = new ODataConventionModelBuilder(); + modelBuilder.EntitySet("Orders"); - return modelBuilder.GetEdmModel(); - } + return modelBuilder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesTests.cs index 989150e84..7b43aa2ae 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/MediaTypes/MediaTypesTests.cs @@ -14,218 +14,217 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.MediaTypes +namespace Microsoft.AspNetCore.OData.E2E.Tests.MediaTypes; + +public class MediaTypesTests : WebApiTestBase { - public class MediaTypesTests : WebApiTestBase + public MediaTypesTests(WebApiTestFixture fixture) + : base(fixture) { - public MediaTypesTests(WebApiTestFixture fixture) - : base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - var model = MediaTypesEdmModel.GetEdmModel(); + protected static void UpdateConfigureServices(IServiceCollection services) + { + var model = MediaTypesEdmModel.GetEdmModel(); - services.ConfigureControllers(typeof(OrdersController)); + services.ConfigureControllers(typeof(OrdersController)); - services.AddControllers().AddOData( - options => options.EnableQueryFeatures() - .AddRouteComponents(model)); - } + services.AddControllers().AddOData( + options => options.EnableQueryFeatures() + .AddRouteComponents(model)); + } - public static IEnumerable GetMediaTypeTestData() - { - return new List - { - new object[] - { - "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false", - "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" - }, - new object[] - { - "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=true", - "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" - }, - new object[] - { - "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=false", - "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" - }, - new object[] - { - "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=true", - "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" - }, - new object[] - { - "application/json;odata.metadata=minimal;IEEE754Compatible=false", - "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" - }, - new object[] - { - "application/json;odata.metadata=minimal;IEEE754Compatible=true", - "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" - }, - new object[] - { - "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=false", - $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"@odata.type\":\"#{typeof(Order).FullName}\",\"@odata.id\":\"http://localhost/Orders(1)\",\"@odata.editLink\":\"Orders(1)\",\"Id\":1,\"Amount@odata.type\":\"#Decimal\",\"Amount\":130,\"TrackingNumber@odata.type\":\"#Int64\",\"TrackingNumber\":9223372036854775807}}" - }, - new object[] - { - "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=true", - $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"@odata.type\":\"#{typeof(Order).FullName}\",\"@odata.id\":\"http://localhost/Orders(1)\",\"@odata.editLink\":\"Orders(1)\",\"Id\":1,\"Amount@odata.type\":\"#Decimal\",\"Amount\":\"130\",\"TrackingNumber@odata.type\":\"#Int64\",\"TrackingNumber\":\"9223372036854775807\"}}" - }, - new object[] - { - "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=false", - $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"@odata.type\":\"#{typeof(Order).FullName}\",\"@odata.id\":\"http://localhost/Orders(1)\",\"@odata.editLink\":\"Orders(1)\",\"Id\":1,\"Amount@odata.type\":\"#Decimal\",\"Amount\":130,\"TrackingNumber@odata.type\":\"#Int64\",\"TrackingNumber\":9223372036854775807}}" - }, - new object[] - { - "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=true", - $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"@odata.type\":\"#{typeof(Order).FullName}\",\"@odata.id\":\"http://localhost/Orders(1)\",\"@odata.editLink\":\"Orders(1)\",\"Id\":1,\"Amount@odata.type\":\"#Decimal\",\"Amount\":\"130\",\"TrackingNumber@odata.type\":\"#Int64\",\"TrackingNumber\":\"9223372036854775807\"}}" - }, - new object[] - { - "application/json;odata.metadata=full;IEEE754Compatible=false", - $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"@odata.type\":\"#{typeof(Order).FullName}\",\"@odata.id\":\"http://localhost/Orders(1)\",\"@odata.editLink\":\"Orders(1)\",\"Id\":1,\"Amount@odata.type\":\"#Decimal\",\"Amount\":130,\"TrackingNumber@odata.type\":\"#Int64\",\"TrackingNumber\":9223372036854775807}}" - }, - new object[] - { - "application/json;odata.metadata=full;IEEE754Compatible=true", - $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"@odata.type\":\"#{typeof(Order).FullName}\",\"@odata.id\":\"http://localhost/Orders(1)\",\"@odata.editLink\":\"Orders(1)\",\"Id\":1,\"Amount@odata.type\":\"#Decimal\",\"Amount\":\"130\",\"TrackingNumber@odata.type\":\"#Int64\",\"TrackingNumber\":\"9223372036854775807\"}}" - }, - new object[] - { - "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=false", - "{\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" - }, - new object[] - { - "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=true", - "{\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" - }, - new object[] - { - "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=false", - "{\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" - }, - new object[] - { - "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=true", - "{\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" - }, - new object[] - { - "application/json;odata.metadata=none;IEEE754Compatible=false", - "{\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" - }, - new object[] - { - "application/json;odata.metadata=none;IEEE754Compatible=true", - "{\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" - }, - new object[] - { - "application/json;odata.streaming=false;IEEE754Compatible=false", - $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}}" - }, - new object[] - { - "application/json;odata.streaming=false;IEEE754Compatible=true", - $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}}" - }, - new object[] - { - "application/json;odata.streaming=true;IEEE754Compatible=false", - $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}}" - }, - new object[] - { - "application/json;odata.streaming=true;IEEE754Compatible=true", - $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}}" - }, - new object[] - { - "application/json;IEEE754Compatible=false", - "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" - }, - new object[] - { - "application/json;IEEE754Compatible=true", - "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" - }, - }; - } - - [Theory] - [MemberData(nameof(GetMediaTypeTestData))] - public async Task VerifyResultForMediaTypeInAcceptHeader(string mediaType, string expected) + public static IEnumerable GetMediaTypeTestData() + { + return new List { - // Arrange - var requestUri = "Orders(1)"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(mediaType)); - var client = CreateClient(); + new object[] + { + "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false", + "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" + }, + new object[] + { + "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=true", + "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" + }, + new object[] + { + "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=false", + "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" + }, + new object[] + { + "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=true", + "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" + }, + new object[] + { + "application/json;odata.metadata=minimal;IEEE754Compatible=false", + "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" + }, + new object[] + { + "application/json;odata.metadata=minimal;IEEE754Compatible=true", + "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" + }, + new object[] + { + "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=false", + $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"@odata.type\":\"#{typeof(Order).FullName}\",\"@odata.id\":\"http://localhost/Orders(1)\",\"@odata.editLink\":\"Orders(1)\",\"Id\":1,\"Amount@odata.type\":\"#Decimal\",\"Amount\":130,\"TrackingNumber@odata.type\":\"#Int64\",\"TrackingNumber\":9223372036854775807}}" + }, + new object[] + { + "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=true", + $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"@odata.type\":\"#{typeof(Order).FullName}\",\"@odata.id\":\"http://localhost/Orders(1)\",\"@odata.editLink\":\"Orders(1)\",\"Id\":1,\"Amount@odata.type\":\"#Decimal\",\"Amount\":\"130\",\"TrackingNumber@odata.type\":\"#Int64\",\"TrackingNumber\":\"9223372036854775807\"}}" + }, + new object[] + { + "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=false", + $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"@odata.type\":\"#{typeof(Order).FullName}\",\"@odata.id\":\"http://localhost/Orders(1)\",\"@odata.editLink\":\"Orders(1)\",\"Id\":1,\"Amount@odata.type\":\"#Decimal\",\"Amount\":130,\"TrackingNumber@odata.type\":\"#Int64\",\"TrackingNumber\":9223372036854775807}}" + }, + new object[] + { + "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=true", + $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"@odata.type\":\"#{typeof(Order).FullName}\",\"@odata.id\":\"http://localhost/Orders(1)\",\"@odata.editLink\":\"Orders(1)\",\"Id\":1,\"Amount@odata.type\":\"#Decimal\",\"Amount\":\"130\",\"TrackingNumber@odata.type\":\"#Int64\",\"TrackingNumber\":\"9223372036854775807\"}}" + }, + new object[] + { + "application/json;odata.metadata=full;IEEE754Compatible=false", + $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"@odata.type\":\"#{typeof(Order).FullName}\",\"@odata.id\":\"http://localhost/Orders(1)\",\"@odata.editLink\":\"Orders(1)\",\"Id\":1,\"Amount@odata.type\":\"#Decimal\",\"Amount\":130,\"TrackingNumber@odata.type\":\"#Int64\",\"TrackingNumber\":9223372036854775807}}" + }, + new object[] + { + "application/json;odata.metadata=full;IEEE754Compatible=true", + $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"@odata.type\":\"#{typeof(Order).FullName}\",\"@odata.id\":\"http://localhost/Orders(1)\",\"@odata.editLink\":\"Orders(1)\",\"Id\":1,\"Amount@odata.type\":\"#Decimal\",\"Amount\":\"130\",\"TrackingNumber@odata.type\":\"#Int64\",\"TrackingNumber\":\"9223372036854775807\"}}" + }, + new object[] + { + "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=false", + "{\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" + }, + new object[] + { + "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=true", + "{\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" + }, + new object[] + { + "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=false", + "{\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" + }, + new object[] + { + "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=true", + "{\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" + }, + new object[] + { + "application/json;odata.metadata=none;IEEE754Compatible=false", + "{\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" + }, + new object[] + { + "application/json;odata.metadata=none;IEEE754Compatible=true", + "{\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" + }, + new object[] + { + "application/json;odata.streaming=false;IEEE754Compatible=false", + $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}}" + }, + new object[] + { + "application/json;odata.streaming=false;IEEE754Compatible=true", + $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}}" + }, + new object[] + { + "application/json;odata.streaming=true;IEEE754Compatible=false", + $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}}" + }, + new object[] + { + "application/json;odata.streaming=true;IEEE754Compatible=true", + $"{{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}}" + }, + new object[] + { + "application/json;IEEE754Compatible=false", + "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":130,\"TrackingNumber\":9223372036854775807}" + }, + new object[] + { + "application/json;IEEE754Compatible=true", + "{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}" + }, + }; + } + + [Theory] + [MemberData(nameof(GetMediaTypeTestData))] + public async Task VerifyResultForMediaTypeInAcceptHeader(string mediaType, string expected) + { + // Arrange + var requestUri = "Orders(1)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(mediaType)); + var client = CreateClient(); - // Act - var response = await client.SendAsync(request); + // Act + var response = await client.SendAsync(request); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = await response.Content.ReadAsStringAsync(); + var result = await response.Content.ReadAsStringAsync(); - Assert.Equal(expected, result); - } + Assert.Equal(expected, result); + } - [Theory] - [MemberData(nameof(GetMediaTypeTestData))] - public async Task VerifyResultForMediaTypeInFormatQueryOption(string mediaType, string expected) - { - // Arrange - var requestUri = $"Orders(1)?$format={mediaType}"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); + [Theory] + [MemberData(nameof(GetMediaTypeTestData))] + public async Task VerifyResultForMediaTypeInFormatQueryOption(string mediaType, string expected) + { + // Arrange + var requestUri = $"Orders(1)?$format={mediaType}"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); - // Act - var response = await client.SendAsync(request); + // Act + var response = await client.SendAsync(request); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = await response.Content.ReadAsStringAsync(); + var result = await response.Content.ReadAsStringAsync(); - Assert.Equal(expected, result); - } + Assert.Equal(expected, result); + } - [Theory] - [InlineData("application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=true")] - [InlineData("application/json;odata.metadata=minimal;IEEE754Compatible=true;odata.streaming=true")] - [InlineData("application/json;IEEE754Compatible=true;odata.metadata=minimal;odata.streaming=true")] - public async Task VerifyPositionOfIEEE754CompatibleParameterInMediaTypeShouldNotMatter(string mediaType) - { - // Arrange - var requestUri = "Orders(1)"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(mediaType)); - var client = CreateClient(); + [Theory] + [InlineData("application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=true")] + [InlineData("application/json;odata.metadata=minimal;IEEE754Compatible=true;odata.streaming=true")] + [InlineData("application/json;IEEE754Compatible=true;odata.metadata=minimal;odata.streaming=true")] + public async Task VerifyPositionOfIEEE754CompatibleParameterInMediaTypeShouldNotMatter(string mediaType) + { + // Arrange + var requestUri = "Orders(1)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(mediaType)); + var client = CreateClient(); - // Act - var response = await client.SendAsync(request); + // Act + var response = await client.SendAsync(request); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = await response.Content.ReadAsStringAsync(); + var result = await response.Content.ReadAsStringAsync(); - Assert.Equal("{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}", result); - } + Assert.Equal("{\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\",\"Id\":1,\"Amount\":\"130\",\"TrackingNumber\":\"9223372036854775807\"}", result); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesController.cs index 31097a9fe..5c64bad5b 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesController.cs @@ -12,87 +12,86 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties +namespace Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties; + +[Route("NonContainedNavPropInContainedNavSource")] +[Route("ContainedNavPropInContainedNavSource")] +public class SitesController : ODataController { - [Route("NonContainedNavPropInContainedNavSource")] - [Route("ContainedNavPropInContainedNavSource")] - public class SitesController : ODataController + [EnableQuery] + [HttpGet("Sites")] + public ActionResult> Get() { - [EnableQuery] - [HttpGet("Sites")] - public ActionResult> Get() - { - return MetadataPropertiesDataSource.Sites; - } - - [EnableQuery] - [HttpGet("Sites({key})")] - public ActionResult Get(int key) - { - var site = MetadataPropertiesDataSource.Sites.SingleOrDefault(d => d.Id == key); + return MetadataPropertiesDataSource.Sites; + } - if (site == null) - { - return NotFound(); - } + [EnableQuery] + [HttpGet("Sites({key})")] + public ActionResult Get(int key) + { + var site = MetadataPropertiesDataSource.Sites.SingleOrDefault(d => d.Id == key); - return site; + if (site == null) + { + return NotFound(); } - [EnableQuery] - [HttpGet("Sites({key})/Plants")] - public ActionResult> GetPlants(int key) - { - var site = MetadataPropertiesDataSource.Sites.SingleOrDefault(d => d.Id == key); + return site; + } - if (site == null || site.Plants == null) - { - return Enumerable.Empty().ToList(); - } + [EnableQuery] + [HttpGet("Sites({key})/Plants")] + public ActionResult> GetPlants(int key) + { + var site = MetadataPropertiesDataSource.Sites.SingleOrDefault(d => d.Id == key); - return site.Plants.ToList(); + if (site == null || site.Plants == null) + { + return Enumerable.Empty().ToList(); } - [EnableQuery] - [HttpGet("Sites({siteKey})/Plants({plantKey})")] - public ActionResult GetPlant(int siteKey, int plantKey) - { - var plant = MetadataPropertiesDataSource.Sites.SingleOrDefault(d => d.Id == siteKey)?.Plants?.SingleOrDefault(d => d.Id == plantKey); + return site.Plants.ToList(); + } - if (plant == null) - { - return NotFound(); - } + [EnableQuery] + [HttpGet("Sites({siteKey})/Plants({plantKey})")] + public ActionResult GetPlant(int siteKey, int plantKey) + { + var plant = MetadataPropertiesDataSource.Sites.SingleOrDefault(d => d.Id == siteKey)?.Plants?.SingleOrDefault(d => d.Id == plantKey); - return plant; + if (plant == null) + { + return NotFound(); } - [EnableQuery] - [HttpGet("Sites({siteKey})/Plants({plantKey})/Pipelines")] - public ActionResult> GetPipelines(int siteKey, int plantKey) - { - var plant = MetadataPropertiesDataSource.Sites.SingleOrDefault(d => d.Id == siteKey)?.Plants?.SingleOrDefault(d => d.Id == plantKey); + return plant; + } - if (plant == null || plant.Pipelines == null) - { - return Enumerable.Empty().ToList(); - } + [EnableQuery] + [HttpGet("Sites({siteKey})/Plants({plantKey})/Pipelines")] + public ActionResult> GetPipelines(int siteKey, int plantKey) + { + var plant = MetadataPropertiesDataSource.Sites.SingleOrDefault(d => d.Id == siteKey)?.Plants?.SingleOrDefault(d => d.Id == plantKey); - return plant.Pipelines.ToList(); + if (plant == null || plant.Pipelines == null) + { + return Enumerable.Empty().ToList(); } - [EnableQuery] - [HttpGet("Sites({siteKey})/Plants({plantKey})/Pipelines({pipelineKey})")] - public ActionResult GetPlantPipeline(int siteKey, int plantKey, int pipelineKey) - { - var pipeline = MetadataPropertiesDataSource.Sites.SingleOrDefault(d => d.Id == siteKey)?.Plants?.SingleOrDefault(d => d.Id == plantKey)?.Pipelines?.SingleOrDefault(d => d.Id == pipelineKey); + return plant.Pipelines.ToList(); + } - if (pipeline == null) - { - return NotFound(); - } + [EnableQuery] + [HttpGet("Sites({siteKey})/Plants({plantKey})/Pipelines({pipelineKey})")] + public ActionResult GetPlantPipeline(int siteKey, int plantKey, int pipelineKey) + { + var pipeline = MetadataPropertiesDataSource.Sites.SingleOrDefault(d => d.Id == siteKey)?.Plants?.SingleOrDefault(d => d.Id == plantKey)?.Pipelines?.SingleOrDefault(d => d.Id == pipelineKey); - return pipeline; + if (pipeline == null) + { + return NotFound(); } + + return pipeline; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesDataModel.Plant1.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesDataModel.Plant1.cs new file mode 100644 index 000000000..99ac7c8f8 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesDataModel.Plant1.cs @@ -0,0 +1,16 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties.Core; + +// NOTE: Pipeline class defined in a different namespace to repro a reported scenario +namespace Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties.Plant1; + +public class Pipeline : PipelineBase +{ + public int? Length { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesDataModel.cs index d0fd98ecc..b39bb370e 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesDataModel.cs @@ -5,46 +5,34 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties.Core -{ - using System.Collections.Generic; - using System.ComponentModel.DataAnnotations; - using Microsoft.OData.ModelBuilder; - - public abstract class EntityBase - { - [Key] - public int Id { get; set; } - public string Name { get; set; } - public Dictionary Attributes { get; set; } - } +namespace Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties.Core; - public class Site : EntityBase - { - [Contained] - public IEnumerable Plants { get; set; } - } +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.OData.ModelBuilder; - public class Plant : EntityBase - { - public Site Site { get; set; } - [Contained] - public IEnumerable Pipelines { get; set; } - } +public abstract class EntityBase +{ + [Key] + public int Id { get; set; } + public string Name { get; set; } + public Dictionary Attributes { get; set; } +} - public abstract class PipelineBase : EntityBase - { - public Plant Plant { get; set; } - } +public class Site : EntityBase +{ + [Contained] + public IEnumerable Plants { get; set; } } -// NOTE: Pipeline class defined in a different namespace to repro a reported scenario -namespace Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties.Plant1 +public class Plant : EntityBase { - using Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties.Core; + public Site Site { get; set; } + [Contained] + public IEnumerable Pipelines { get; set; } +} - public class Pipeline : PipelineBase - { - public int? Length { get; set; } - } +public abstract class PipelineBase : EntityBase +{ + public Plant Plant { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesDataSource.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesDataSource.cs index ffd90a5d3..ebacc7f59 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesDataSource.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesDataSource.cs @@ -10,48 +10,47 @@ using Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties.Core; using Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties.Plant1; -namespace Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties +namespace Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties; + +internal static class MetadataPropertiesDataSource { - internal static class MetadataPropertiesDataSource + private readonly static List sites; + private readonly static List plants; + private readonly static List pipelines; + + static MetadataPropertiesDataSource() { - private readonly static List sites; - private readonly static List plants; - private readonly static List pipelines; + pipelines = new List(Enumerable.Range(1, 8).Select(idx => new Pipeline + { + Id = idx, + Name = $"Pipeline {idx}", + Length = idx * 100 + })); + + plants = new List(Enumerable.Range(1, 4).Select(idx => new Plant + { + Id = idx, + Name = $"Plant {idx}", + Pipelines = pipelines.Skip((idx - 1) * 2).Take(2) + })); - static MetadataPropertiesDataSource() + sites = new List(Enumerable.Range(1, 2).Select(idx => new Site { - pipelines = new List(Enumerable.Range(1, 8).Select(idx => new Pipeline - { - Id = idx, - Name = $"Pipeline {idx}", - Length = idx * 100 - })); - - plants = new List(Enumerable.Range(1, 4).Select(idx => new Plant - { - Id = idx, - Name = $"Plant {idx}", - Pipelines = pipelines.Skip((idx - 1) * 2).Take(2) - })); - - sites = new List(Enumerable.Range(1, 2).Select(idx => new Site - { - Id = idx, - Name = $"Site {idx}", - Plants = plants.Skip((idx - 1) * 2).Take(2) - })); - - for (var i = 0; i < plants.Count; i++) - { - plants[i].Site = sites[i / 2]; - } - - for (var i = 0; i < pipelines.Count; i++) - { - pipelines[i].Plant = plants[i / 2]; - } + Id = idx, + Name = $"Site {idx}", + Plants = plants.Skip((idx - 1) * 2).Take(2) + })); + + for (var i = 0; i < plants.Count; i++) + { + plants[i].Site = sites[i / 2]; } - public static List Sites => sites; + for (var i = 0; i < pipelines.Count; i++) + { + pipelines[i].Plant = plants[i / 2]; + } } + + public static List Sites => sites; } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesEdmModel.cs index a61038ea1..e67b65604 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesEdmModel.cs @@ -11,58 +11,57 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties +namespace Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties; + +public class MetadataPropertiesEdmModel { - public class MetadataPropertiesEdmModel + /// + /// Returns model where Site and Plant navigation properties are non-contained and navigation source is contained. + /// + /// Returns Edm model. + public static IEdmModel GetEdmModelWithNonContainedNavPropInContainedNavSource() { - /// - /// Returns model where Site and Plant navigation properties are non-contained and navigation source is contained. - /// - /// Returns Edm model. - public static IEdmModel GetEdmModelWithNonContainedNavPropInContainedNavSource() - { - var modelBuilder = new ODataConventionModelBuilder(); - modelBuilder.EntityType(); - modelBuilder.EntityType(); - modelBuilder.EntityType(); - modelBuilder.EntityType(); - modelBuilder.EntityType(); - modelBuilder.EntitySet("Sites"); + var modelBuilder = new ODataConventionModelBuilder(); + modelBuilder.EntityType(); + modelBuilder.EntityType(); + modelBuilder.EntityType(); + modelBuilder.EntityType(); + modelBuilder.EntityType(); + modelBuilder.EntitySet("Sites"); - var model = modelBuilder.GetEdmModel(); + var model = modelBuilder.GetEdmModel(); - var sitesEntitySet = (EdmEntitySet)model.FindDeclaredEntitySet("Default.Container.Sites"); - var plantsNavigationProperty = sitesEntitySet.EntityType.DeclaredNavigationProperties().Single(d => d.Name.Equals("Plants")); - var plantsContainedEntitySet = sitesEntitySet.FindNavigationTarget(plantsNavigationProperty); - var siteNavigationProperty = plantsContainedEntitySet.EntityType.DeclaredNavigationProperties().Single(d => d.Name.Equals("Site")); - var pipelinesNavigationProperty = plantsContainedEntitySet.EntityType.DeclaredNavigationProperties().Single(d => d.Name.Equals("Pipelines")); - var pipelineContainedEntitySet = plantsContainedEntitySet.FindNavigationTarget(pipelinesNavigationProperty, new EdmPathExpression("Plants", "Pipelines")); - var plantNavigationProperty = pipelineContainedEntitySet.EntityType.DeclaredNavigationProperties().Single(d => d.Name.Equals("Plant")); + var sitesEntitySet = (EdmEntitySet)model.FindDeclaredEntitySet("Default.Container.Sites"); + var plantsNavigationProperty = sitesEntitySet.EntityType.DeclaredNavigationProperties().Single(d => d.Name.Equals("Plants")); + var plantsContainedEntitySet = sitesEntitySet.FindNavigationTarget(plantsNavigationProperty); + var siteNavigationProperty = plantsContainedEntitySet.EntityType.DeclaredNavigationProperties().Single(d => d.Name.Equals("Site")); + var pipelinesNavigationProperty = plantsContainedEntitySet.EntityType.DeclaredNavigationProperties().Single(d => d.Name.Equals("Pipelines")); + var pipelineContainedEntitySet = plantsContainedEntitySet.FindNavigationTarget(pipelinesNavigationProperty, new EdmPathExpression("Plants", "Pipelines")); + var plantNavigationProperty = pipelineContainedEntitySet.EntityType.DeclaredNavigationProperties().Single(d => d.Name.Equals("Plant")); - sitesEntitySet.AddNavigationTarget(siteNavigationProperty, sitesEntitySet, new EdmPathExpression("Plants", "Site")); - sitesEntitySet.AddNavigationTarget(plantNavigationProperty, plantsContainedEntitySet, new EdmPathExpression("Plants", "Pipelines", "Plant")); + sitesEntitySet.AddNavigationTarget(siteNavigationProperty, sitesEntitySet, new EdmPathExpression("Plants", "Site")); + sitesEntitySet.AddNavigationTarget(plantNavigationProperty, plantsContainedEntitySet, new EdmPathExpression("Plants", "Pipelines", "Plant")); - return model; - } + return model; + } - /// - /// Returns model where Site and Plant navigation properties are contained and navigation source is contained. - /// - /// Returns Edm model. - public static IEdmModel GetEdmModelWithContainedNavPropInContainedNavSource() - { - var modelBuilder = new ODataConventionModelBuilder(); - modelBuilder.EntityType(); - modelBuilder.EntityType(); - // Make Site and Plant navigation properties contained - modelBuilder.EntityType().ContainsRequired(d => d.Site).Contained(); - modelBuilder.EntityType().ContainsRequired(d => d.Plant).Contained(); - modelBuilder.EntityType(); - modelBuilder.EntitySet("Sites"); + /// + /// Returns model where Site and Plant navigation properties are contained and navigation source is contained. + /// + /// Returns Edm model. + public static IEdmModel GetEdmModelWithContainedNavPropInContainedNavSource() + { + var modelBuilder = new ODataConventionModelBuilder(); + modelBuilder.EntityType(); + modelBuilder.EntityType(); + // Make Site and Plant navigation properties contained + modelBuilder.EntityType().ContainsRequired(d => d.Site).Contained(); + modelBuilder.EntityType().ContainsRequired(d => d.Plant).Contained(); + modelBuilder.EntityType(); + modelBuilder.EntitySet("Sites"); - var model = modelBuilder.GetEdmModel(); + var model = modelBuilder.GetEdmModel(); - return model; - } + return model; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesTests.cs index 49d205924..0b4b61971 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/MetadataProperties/MetadataPropertiesTests.cs @@ -18,245 +18,244 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties +namespace Microsoft.AspNetCore.OData.E2E.Tests.MetadataProperties; + +public class MetadataPropertiesTests : WebApiTestBase { - public class MetadataPropertiesTests : WebApiTestBase + public MetadataPropertiesTests(WebApiTestFixture fixture) + : base(fixture) { - public MetadataPropertiesTests(WebApiTestFixture fixture) - : base(fixture) - { - } + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + var nonContainedNavPropInContainedNavSourceModel = MetadataPropertiesEdmModel.GetEdmModelWithNonContainedNavPropInContainedNavSource(); + var containedNavPropertyInContainedNavSourceModel = MetadataPropertiesEdmModel.GetEdmModelWithContainedNavPropInContainedNavSource(); + + services.ConfigureControllers(typeof(SitesController)); + + services.AddControllers().AddOData( + options => options.EnableQueryFeatures() + .AddRouteComponents( + routePrefix: "NonContainedNavPropInContainedNavSource", + model: nonContainedNavPropInContainedNavSourceModel) + .AddRouteComponents( + routePrefix: "ContainedNavPropInContainedNavSource", + model: containedNavPropertyInContainedNavSourceModel)); + } + + [Theory] + [InlineData("NonContainedNavPropInContainedNavSource", "Sites(1)/Plants(1)")] + [InlineData("ContainedNavPropInContainedNavSource", "Sites(1)/Plants(1)/Pipelines(1)/Plant")] + public async Task TestExpandPlantNavigationPropertyOnContainedNavigationSource(string routePrefix, string plantResourceBase) + { + // Arrange + var requestUri = $"{routePrefix}/Sites(1)/Plants(1)/Pipelines(1)?$expand=Plant"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + var client = CreateClient(); + var typeofPipeline = typeof(Pipeline); + var typeofPlant = typeof(Plant); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var result = await response.Content.ReadAsObject(); + + Assert.EndsWith($"{routePrefix}/$metadata#Sites(1)/Plants(1)/Pipelines/{typeofPipeline}(Plant())/$entity", result.Value("@odata.context")); + Assert.Equal($"#{typeofPipeline}", result.Value("@odata.type")); + Assert.EndsWith("Sites(1)/Plants(1)/Pipelines(1)", result.Value("@odata.id")); + Assert.EndsWith($"Sites(1)/Plants(1)/Pipelines(1)/{typeofPipeline}", result.Value("@odata.editLink")); + Assert.Equal(1, result.Value("Id")); + Assert.Equal("Pipeline 1", result.Value("Name")); + Assert.Equal(100, result.Value("Length")); + Assert.EndsWith($"/Sites(1)/Plants(1)/Pipelines(1)/{typeofPipeline}/Plant/$ref", result.Value("Plant@odata.associationLink")); + Assert.EndsWith($"/Sites(1)/Plants(1)/Pipelines(1)/{typeofPipeline}/Plant", result.Value("Plant@odata.navigationLink")); + + var plant = result.GetValue("Plant") as JObject; + Assert.NotNull(plant); + Assert.Equal($"#{typeofPlant}", plant.Value("@odata.type")); + Assert.EndsWith($"{plantResourceBase}", plant.Value("@odata.id")); + Assert.EndsWith($"{plantResourceBase}", plant.Value("@odata.editLink")); + Assert.Equal(1, plant.Value("Id")); + Assert.Equal("Plant 1", plant.Value("Name")); + Assert.EndsWith($"{routePrefix}/{plantResourceBase}/Site/$ref", plant.Value("Site@odata.associationLink")); + Assert.EndsWith($"{routePrefix}/{plantResourceBase}/Site", plant.Value("Site@odata.navigationLink")); + Assert.EndsWith($"{routePrefix}/{plantResourceBase}/Pipelines/$ref", plant.Value("Pipelines@odata.associationLink")); + Assert.EndsWith($"{routePrefix}/{plantResourceBase}/Pipelines", plant.Value("Pipelines@odata.navigationLink")); + } + + [Theory] + [InlineData("NonContainedNavPropInContainedNavSource", "Sites(1)")] + [InlineData("ContainedNavPropInContainedNavSource", "Sites(1)/Plants(1)/Site")] + public async Task TestExpandSiteNavigationPropertyOnContainedNavigationSource(string routePrefix, string siteResourceBase) + { + // Arrange + var requestUri = $"{routePrefix}/Sites(1)/Plants(1)?$expand=Site"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + var client = CreateClient(); + var typeofPlant = typeof(Plant); + var typeofSite = typeof(Site); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var result = await response.Content.ReadAsObject(); + + Assert.EndsWith($"{routePrefix}/$metadata#Sites(1)/Plants(Site())/$entity", result.Value("@odata.context")); + Assert.Equal($"#{typeofPlant}", result.Value("@odata.type")); + Assert.EndsWith("Sites(1)/Plants(1)", result.Value("@odata.id")); + Assert.EndsWith("Sites(1)/Plants(1)", result.Value("@odata.editLink")); + Assert.Equal(1, result.Value("Id")); + Assert.Equal("Plant 1", result.Value("Name")); + Assert.EndsWith($"{routePrefix}/Sites(1)/Plants(1)/Pipelines/$ref", result.Value("Pipelines@odata.associationLink")); + Assert.EndsWith($"{routePrefix}/Sites(1)/Plants(1)/Pipelines", result.Value("Pipelines@odata.navigationLink")); + Assert.EndsWith($"{routePrefix}/Sites(1)/Plants(1)/Site/$ref", result.Value("Site@odata.associationLink")); + Assert.EndsWith($"{routePrefix}/Sites(1)/Plants(1)/Site", result.Value("Site@odata.navigationLink")); + + var site = result.GetValue("Site") as JObject; + Assert.NotNull(site); + Assert.Equal($"#{typeofSite}", site.Value("@odata.type")); + Assert.EndsWith($"{siteResourceBase}", site.Value("@odata.id")); + Assert.EndsWith($"{siteResourceBase}", site.Value("@odata.editLink")); + Assert.Equal(1, site.Value("Id")); + Assert.Equal("Site 1", site.Value("Name")); + Assert.EndsWith($"{routePrefix}/{siteResourceBase}/Plants/$ref", site.Value("Plants@odata.associationLink")); + Assert.EndsWith($"{routePrefix}/{siteResourceBase}/Plants", site.Value("Plants@odata.navigationLink")); + } + + [Theory] + [InlineData("NonContainedNavPropInContainedNavSource", "Sites(1)/Plants(2)")] + [InlineData("ContainedNavPropInContainedNavSource", "Sites(1)/Plants(2)/Pipelines({0})/Plant")] + public async Task TestExpandPipelinesNavigationPropertyOnContainedNavigationSource(string routePrefix, string plantResourceBase) + { + // Arrange + var requestUri = $"{routePrefix}/Sites(1)/Plants(2)?$expand=Pipelines($expand=Plant)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + var client = CreateClient(); - protected static void UpdateConfigureServices(IServiceCollection services) + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var result = await response.Content.ReadAsObject(); + + Action verifyPlantAction = (plant, localRoutePrefix, localPlantResourceBase) => { - var nonContainedNavPropInContainedNavSourceModel = MetadataPropertiesEdmModel.GetEdmModelWithNonContainedNavPropInContainedNavSource(); - var containedNavPropertyInContainedNavSourceModel = MetadataPropertiesEdmModel.GetEdmModelWithContainedNavPropInContainedNavSource(); - - services.ConfigureControllers(typeof(SitesController)); - - services.AddControllers().AddOData( - options => options.EnableQueryFeatures() - .AddRouteComponents( - routePrefix: "NonContainedNavPropInContainedNavSource", - model: nonContainedNavPropInContainedNavSourceModel) - .AddRouteComponents( - routePrefix: "ContainedNavPropInContainedNavSource", - model: containedNavPropertyInContainedNavSourceModel)); - } - - [Theory] - [InlineData("NonContainedNavPropInContainedNavSource", "Sites(1)/Plants(1)")] - [InlineData("ContainedNavPropInContainedNavSource", "Sites(1)/Plants(1)/Pipelines(1)/Plant")] - public async Task TestExpandPlantNavigationPropertyOnContainedNavigationSource(string routePrefix, string plantResourceBase) + Assert.NotNull(plant); + Assert.Equal($"#{typeof(Plant)}", plant.Value("@odata.type")); + Assert.EndsWith($"{localPlantResourceBase}", plant.Value("@odata.id")); + Assert.EndsWith($"{localPlantResourceBase}", plant.Value("@odata.editLink")); + Assert.Equal(2, plant.Value("Id")); + Assert.Equal($"Plant 2", plant.Value("Name")); + Assert.EndsWith($"{localRoutePrefix}/{localPlantResourceBase}/Pipelines/$ref", plant.Value("Pipelines@odata.associationLink")); + Assert.EndsWith($"{localRoutePrefix}/{localPlantResourceBase}/Pipelines", plant.Value("Pipelines@odata.navigationLink")); + Assert.EndsWith($"{localRoutePrefix}/{localPlantResourceBase}/Site/$ref", plant.Value("Site@odata.associationLink")); + Assert.EndsWith($"{localRoutePrefix}/{localPlantResourceBase}/Site", plant.Value("Site@odata.navigationLink")); + }; + + verifyPlantAction(result, routePrefix, "Sites(1)/Plants(2)"); + + var pipelines = result.GetValue("Pipelines") as JArray; + Assert.NotNull(pipelines); + Assert.Equal(2, pipelines.Count); + + var pipelineAt0 = pipelines[0] as JObject; + var pipelineAt1 = pipelines[1] as JObject; + + Action verifyPipelineAction = (pipeline, localRoutePrefix, pipelineId) => { - // Arrange - var requestUri = $"{routePrefix}/Sites(1)/Plants(1)/Pipelines(1)?$expand=Plant"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - var client = CreateClient(); var typeofPipeline = typeof(Pipeline); - var typeofPlant = typeof(Plant); - // Act - var response = await client.SendAsync(request); + Assert.NotNull(pipeline); + Assert.Equal($"#{typeofPipeline}", pipeline.Value("@odata.type")); + Assert.EndsWith($"Sites(1)/Plants(2)/Pipelines({pipelineId})", pipeline.Value("@odata.id")); + Assert.EndsWith($"Sites(1)/Plants(2)/Pipelines({pipelineId})/{typeofPipeline}", pipeline.Value("@odata.editLink")); + Assert.Equal(pipelineId, pipeline.Value("Id")); + Assert.Equal($"Pipeline {pipelineId}", pipeline.Value("Name")); + Assert.Equal(pipelineId * 100, pipeline.Value("Length")); + Assert.EndsWith($"/Sites(1)/Plants(2)/Pipelines({pipelineId})/{typeofPipeline}/Plant/$ref", pipeline.Value("Plant@odata.associationLink")); + Assert.EndsWith($"/Sites(1)/Plants(2)/Pipelines({pipelineId})/{typeofPipeline}/Plant", pipeline.Value("Plant@odata.navigationLink")); + + var plant = pipeline.GetValue("Plant") as JObject; + verifyPlantAction(plant, localRoutePrefix, string.Format(plantResourceBase, pipelineId)); + }; + + verifyPipelineAction(pipelineAt0, routePrefix, 3); // Pipeline Id = 3 + verifyPipelineAction(pipelineAt1, routePrefix, 4); // Pipeline Id = 4 + } - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + [Theory] + [InlineData("NonContainedNavPropInContainedNavSource", "Sites(2)")] + [InlineData("ContainedNavPropInContainedNavSource", "Sites(2)/Plants({0})/Site")] + public async Task TestExpandPlantsNavigationPropertyOnNonContainedNavigationSource(string routePrefix, string siteResourceBase) + { + // Arrange + var requestUri = $"{routePrefix}/Sites(2)?$expand=Plants($expand=Site)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + var client = CreateClient(); - var result = await response.Content.ReadAsObject(); + // Act + var response = await client.SendAsync(request); - Assert.EndsWith($"{routePrefix}/$metadata#Sites(1)/Plants(1)/Pipelines/{typeofPipeline}(Plant())/$entity", result.Value("@odata.context")); - Assert.Equal($"#{typeofPipeline}", result.Value("@odata.type")); - Assert.EndsWith("Sites(1)/Plants(1)/Pipelines(1)", result.Value("@odata.id")); - Assert.EndsWith($"Sites(1)/Plants(1)/Pipelines(1)/{typeofPipeline}", result.Value("@odata.editLink")); - Assert.Equal(1, result.Value("Id")); - Assert.Equal("Pipeline 1", result.Value("Name")); - Assert.Equal(100, result.Value("Length")); - Assert.EndsWith($"/Sites(1)/Plants(1)/Pipelines(1)/{typeofPipeline}/Plant/$ref", result.Value("Plant@odata.associationLink")); - Assert.EndsWith($"/Sites(1)/Plants(1)/Pipelines(1)/{typeofPipeline}/Plant", result.Value("Plant@odata.navigationLink")); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var plant = result.GetValue("Plant") as JObject; - Assert.NotNull(plant); - Assert.Equal($"#{typeofPlant}", plant.Value("@odata.type")); - Assert.EndsWith($"{plantResourceBase}", plant.Value("@odata.id")); - Assert.EndsWith($"{plantResourceBase}", plant.Value("@odata.editLink")); - Assert.Equal(1, plant.Value("Id")); - Assert.Equal("Plant 1", plant.Value("Name")); - Assert.EndsWith($"{routePrefix}/{plantResourceBase}/Site/$ref", plant.Value("Site@odata.associationLink")); - Assert.EndsWith($"{routePrefix}/{plantResourceBase}/Site", plant.Value("Site@odata.navigationLink")); - Assert.EndsWith($"{routePrefix}/{plantResourceBase}/Pipelines/$ref", plant.Value("Pipelines@odata.associationLink")); - Assert.EndsWith($"{routePrefix}/{plantResourceBase}/Pipelines", plant.Value("Pipelines@odata.navigationLink")); - } - - [Theory] - [InlineData("NonContainedNavPropInContainedNavSource", "Sites(1)")] - [InlineData("ContainedNavPropInContainedNavSource", "Sites(1)/Plants(1)/Site")] - public async Task TestExpandSiteNavigationPropertyOnContainedNavigationSource(string routePrefix, string siteResourceBase) - { - // Arrange - var requestUri = $"{routePrefix}/Sites(1)/Plants(1)?$expand=Site"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - var client = CreateClient(); - var typeofPlant = typeof(Plant); - var typeofSite = typeof(Site); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var result = await response.Content.ReadAsObject(); - - Assert.EndsWith($"{routePrefix}/$metadata#Sites(1)/Plants(Site())/$entity", result.Value("@odata.context")); - Assert.Equal($"#{typeofPlant}", result.Value("@odata.type")); - Assert.EndsWith("Sites(1)/Plants(1)", result.Value("@odata.id")); - Assert.EndsWith("Sites(1)/Plants(1)", result.Value("@odata.editLink")); - Assert.Equal(1, result.Value("Id")); - Assert.Equal("Plant 1", result.Value("Name")); - Assert.EndsWith($"{routePrefix}/Sites(1)/Plants(1)/Pipelines/$ref", result.Value("Pipelines@odata.associationLink")); - Assert.EndsWith($"{routePrefix}/Sites(1)/Plants(1)/Pipelines", result.Value("Pipelines@odata.navigationLink")); - Assert.EndsWith($"{routePrefix}/Sites(1)/Plants(1)/Site/$ref", result.Value("Site@odata.associationLink")); - Assert.EndsWith($"{routePrefix}/Sites(1)/Plants(1)/Site", result.Value("Site@odata.navigationLink")); - - var site = result.GetValue("Site") as JObject; - Assert.NotNull(site); - Assert.Equal($"#{typeofSite}", site.Value("@odata.type")); - Assert.EndsWith($"{siteResourceBase}", site.Value("@odata.id")); - Assert.EndsWith($"{siteResourceBase}", site.Value("@odata.editLink")); - Assert.Equal(1, site.Value("Id")); - Assert.Equal("Site 1", site.Value("Name")); - Assert.EndsWith($"{routePrefix}/{siteResourceBase}/Plants/$ref", site.Value("Plants@odata.associationLink")); - Assert.EndsWith($"{routePrefix}/{siteResourceBase}/Plants", site.Value("Plants@odata.navigationLink")); - } - - [Theory] - [InlineData("NonContainedNavPropInContainedNavSource", "Sites(1)/Plants(2)")] - [InlineData("ContainedNavPropInContainedNavSource", "Sites(1)/Plants(2)/Pipelines({0})/Plant")] - public async Task TestExpandPipelinesNavigationPropertyOnContainedNavigationSource(string routePrefix, string plantResourceBase) + var result = await response.Content.ReadAsObject(); + + Action verifySiteAction = (site, localRoutePrefix, localSiteResourceBase) => { - // Arrange - var requestUri = $"{routePrefix}/Sites(1)/Plants(2)?$expand=Pipelines($expand=Plant)"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - var client = CreateClient(); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var result = await response.Content.ReadAsObject(); - - Action verifyPlantAction = (plant, localRoutePrefix, localPlantResourceBase) => - { - Assert.NotNull(plant); - Assert.Equal($"#{typeof(Plant)}", plant.Value("@odata.type")); - Assert.EndsWith($"{localPlantResourceBase}", plant.Value("@odata.id")); - Assert.EndsWith($"{localPlantResourceBase}", plant.Value("@odata.editLink")); - Assert.Equal(2, plant.Value("Id")); - Assert.Equal($"Plant 2", plant.Value("Name")); - Assert.EndsWith($"{localRoutePrefix}/{localPlantResourceBase}/Pipelines/$ref", plant.Value("Pipelines@odata.associationLink")); - Assert.EndsWith($"{localRoutePrefix}/{localPlantResourceBase}/Pipelines", plant.Value("Pipelines@odata.navigationLink")); - Assert.EndsWith($"{localRoutePrefix}/{localPlantResourceBase}/Site/$ref", plant.Value("Site@odata.associationLink")); - Assert.EndsWith($"{localRoutePrefix}/{localPlantResourceBase}/Site", plant.Value("Site@odata.navigationLink")); - }; - - verifyPlantAction(result, routePrefix, "Sites(1)/Plants(2)"); - - var pipelines = result.GetValue("Pipelines") as JArray; - Assert.NotNull(pipelines); - Assert.Equal(2, pipelines.Count); - - var pipelineAt0 = pipelines[0] as JObject; - var pipelineAt1 = pipelines[1] as JObject; - - Action verifyPipelineAction = (pipeline, localRoutePrefix, pipelineId) => - { - var typeofPipeline = typeof(Pipeline); - - Assert.NotNull(pipeline); - Assert.Equal($"#{typeofPipeline}", pipeline.Value("@odata.type")); - Assert.EndsWith($"Sites(1)/Plants(2)/Pipelines({pipelineId})", pipeline.Value("@odata.id")); - Assert.EndsWith($"Sites(1)/Plants(2)/Pipelines({pipelineId})/{typeofPipeline}", pipeline.Value("@odata.editLink")); - Assert.Equal(pipelineId, pipeline.Value("Id")); - Assert.Equal($"Pipeline {pipelineId}", pipeline.Value("Name")); - Assert.Equal(pipelineId * 100, pipeline.Value("Length")); - Assert.EndsWith($"/Sites(1)/Plants(2)/Pipelines({pipelineId})/{typeofPipeline}/Plant/$ref", pipeline.Value("Plant@odata.associationLink")); - Assert.EndsWith($"/Sites(1)/Plants(2)/Pipelines({pipelineId})/{typeofPipeline}/Plant", pipeline.Value("Plant@odata.navigationLink")); - - var plant = pipeline.GetValue("Plant") as JObject; - verifyPlantAction(plant, localRoutePrefix, string.Format(plantResourceBase, pipelineId)); - }; - - verifyPipelineAction(pipelineAt0, routePrefix, 3); // Pipeline Id = 3 - verifyPipelineAction(pipelineAt1, routePrefix, 4); // Pipeline Id = 4 - } - - [Theory] - [InlineData("NonContainedNavPropInContainedNavSource", "Sites(2)")] - [InlineData("ContainedNavPropInContainedNavSource", "Sites(2)/Plants({0})/Site")] - public async Task TestExpandPlantsNavigationPropertyOnNonContainedNavigationSource(string routePrefix, string siteResourceBase) + Assert.Equal($"#{typeof(Site)}", site.Value("@odata.type")); + Assert.EndsWith($"{localSiteResourceBase}", site.Value("@odata.id")); + Assert.EndsWith($"{localSiteResourceBase}", site.Value("@odata.editLink")); + Assert.Equal(2, site.Value("Id")); + Assert.Equal("Site 2", site.Value("Name")); + Assert.EndsWith($"{localRoutePrefix}/{localSiteResourceBase}/Plants/$ref", site.Value("Plants@odata.associationLink")); + Assert.EndsWith($"{localRoutePrefix}/{localSiteResourceBase}/Plants", site.Value("Plants@odata.navigationLink")); + }; + + verifySiteAction(result, routePrefix, "Sites(2)"); + + var plants = result.GetValue("Plants") as JArray; + Assert.NotNull(plants); + Assert.Equal(2, plants.Count); + + var plantAt0 = plants[0] as JObject; + var plantAt1 = plants[1] as JObject; + + Action verifyPlantAction = (plant, localRoutePrefix, plantId) => { - // Arrange - var requestUri = $"{routePrefix}/Sites(2)?$expand=Plants($expand=Site)"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - var client = CreateClient(); - - // Act - var response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var result = await response.Content.ReadAsObject(); - - Action verifySiteAction = (site, localRoutePrefix, localSiteResourceBase) => - { - Assert.Equal($"#{typeof(Site)}", site.Value("@odata.type")); - Assert.EndsWith($"{localSiteResourceBase}", site.Value("@odata.id")); - Assert.EndsWith($"{localSiteResourceBase}", site.Value("@odata.editLink")); - Assert.Equal(2, site.Value("Id")); - Assert.Equal("Site 2", site.Value("Name")); - Assert.EndsWith($"{localRoutePrefix}/{localSiteResourceBase}/Plants/$ref", site.Value("Plants@odata.associationLink")); - Assert.EndsWith($"{localRoutePrefix}/{localSiteResourceBase}/Plants", site.Value("Plants@odata.navigationLink")); - }; - - verifySiteAction(result, routePrefix, "Sites(2)"); - - var plants = result.GetValue("Plants") as JArray; - Assert.NotNull(plants); - Assert.Equal(2, plants.Count); - - var plantAt0 = plants[0] as JObject; - var plantAt1 = plants[1] as JObject; - - Action verifyPlantAction = (plant, localRoutePrefix, plantId) => - { - Assert.NotNull(plant); - Assert.Equal($"#{typeof(Plant)}", plant.Value("@odata.type")); - Assert.EndsWith($"Sites(2)/Plants({plantId})", plant.Value("@odata.id")); - Assert.EndsWith($"Sites(2)/Plants({plantId})", plant.Value("@odata.editLink")); - Assert.Equal(plantId, plant.Value("Id")); - Assert.Equal($"Plant {plantId}", plant.Value("Name")); - Assert.EndsWith($"{localRoutePrefix}/Sites(2)/Plants({plantId})/Pipelines/$ref", plant.Value("Pipelines@odata.associationLink")); - Assert.EndsWith($"{localRoutePrefix}/Sites(2)/Plants({plantId})/Pipelines", plant.Value("Pipelines@odata.navigationLink")); - Assert.EndsWith($"{localRoutePrefix}/Sites(2)/Plants({plantId})/Site/$ref", plant.Value("Site@odata.associationLink")); - Assert.EndsWith($"{localRoutePrefix}/Sites(2)/Plants({plantId})/Site", plant.Value("Site@odata.navigationLink")); - - var site = plant.GetValue("Site") as JObject; - verifySiteAction(site, localRoutePrefix, string.Format(siteResourceBase, plantId)); - }; - - verifyPlantAction(plantAt0, routePrefix, 3); // Plant Id = 3 - verifyPlantAction(plantAt1, routePrefix, 4); // Plant Id = 4 - } + Assert.NotNull(plant); + Assert.Equal($"#{typeof(Plant)}", plant.Value("@odata.type")); + Assert.EndsWith($"Sites(2)/Plants({plantId})", plant.Value("@odata.id")); + Assert.EndsWith($"Sites(2)/Plants({plantId})", plant.Value("@odata.editLink")); + Assert.Equal(plantId, plant.Value("Id")); + Assert.Equal($"Plant {plantId}", plant.Value("Name")); + Assert.EndsWith($"{localRoutePrefix}/Sites(2)/Plants({plantId})/Pipelines/$ref", plant.Value("Pipelines@odata.associationLink")); + Assert.EndsWith($"{localRoutePrefix}/Sites(2)/Plants({plantId})/Pipelines", plant.Value("Pipelines@odata.navigationLink")); + Assert.EndsWith($"{localRoutePrefix}/Sites(2)/Plants({plantId})/Site/$ref", plant.Value("Site@odata.associationLink")); + Assert.EndsWith($"{localRoutePrefix}/Sites(2)/Plants({plantId})/Site", plant.Value("Site@odata.navigationLink")); + + var site = plant.GetValue("Site") as JObject; + verifySiteAction(site, localRoutePrefix, string.Format(siteResourceBase, plantId)); + }; + + verifyPlantAction(plantAt0, routePrefix, 3); // Plant Id = 3 + verifyPlantAction(plantAt1, routePrefix, 4); // Plant Id = 4 } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsController.cs index 0325270d3..01839ebe9 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsController.cs @@ -10,14 +10,13 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ModelBoundQuerySettings +namespace Microsoft.AspNetCore.OData.E2E.Tests.ModelBoundQuerySettings; + +public class AuthorsController : ODataController { - public class AuthorsController : ODataController + [EnableQuery] + public ActionResult> Get() { - [EnableQuery] - public ActionResult> Get() - { - return ModelBoundQuerySettingsDataSource.Authors; - } + return ModelBoundQuerySettingsDataSource.Authors; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsDataModel.cs index be1921e0c..73eaf8352 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsDataModel.cs @@ -8,18 +8,17 @@ using System.Collections.Generic; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ModelBoundQuerySettings +namespace Microsoft.AspNetCore.OData.E2E.Tests.ModelBoundQuerySettings; + +[Filter("Books")] +public class Author { - [Filter("Books")] - public class Author - { - public long AuthorId { get; set; } - public List Books { get; set; } - } + public long AuthorId { get; set; } + public List Books { get; set; } +} - [Filter("BookId")] - public class Book - { - public long BookId { get; set; } - } +[Filter("BookId")] +public class Book +{ + public long BookId { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsDataSource.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsDataSource.cs index b5bede1bf..736b14756 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsDataSource.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsDataSource.cs @@ -8,22 +8,21 @@ using System.Collections.Generic; using System.Linq; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ModelBoundQuerySettings +namespace Microsoft.AspNetCore.OData.E2E.Tests.ModelBoundQuerySettings; + +public static class ModelBoundQuerySettingsDataSource { - public static class ModelBoundQuerySettingsDataSource - { - private const int TargetSize = 3; - private static readonly List authors = new List( - Enumerable.Range(1, TargetSize).Select(idx => new Author - { - AuthorId = idx, - Books = new List( - Enumerable.Range(1, 3).Select(dx => new Book - { - BookId = (idx - 1) * TargetSize + dx - })) - })); + private const int TargetSize = 3; + private static readonly List authors = new List( + Enumerable.Range(1, TargetSize).Select(idx => new Author + { + AuthorId = idx, + Books = new List( + Enumerable.Range(1, 3).Select(dx => new Book + { + BookId = (idx - 1) * TargetSize + dx + })) + })); - public static List Authors => authors; - } + public static List Authors => authors; } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsEdmModel.cs index 186bb4f4f..d4cd1e08c 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsEdmModel.cs @@ -8,26 +8,25 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ModelBoundQuerySettings +namespace Microsoft.AspNetCore.OData.E2E.Tests.ModelBoundQuerySettings; + +public class ModelBoundQuerySettingsEdmModel { - public class ModelBoundQuerySettingsEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Authors"); - builder.EntitySet("Books"); + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Authors"); + builder.EntitySet("Books"); - return builder.GetEdmModel(); - } + return builder.GetEdmModel(); + } - public static IEdmModel GetEdmModelByModelBoundAPI() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Authors").EntityType.Filter("Books"); - builder.EntitySet("Books").EntityType.Filter("BookId"); + public static IEdmModel GetEdmModelByModelBoundAPI() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Authors").EntityType.Filter("Books"); + builder.EntitySet("Books").EntityType.Filter("BookId"); - return builder.GetEdmModel(); - } + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsTests.cs index 50ee03a9c..3025b149c 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ModelBoundQuerySettings/ModelBoundQuerySettingsTests.cs @@ -15,46 +15,45 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ModelBoundQuerySettings +namespace Microsoft.AspNetCore.OData.E2E.Tests.ModelBoundQuerySettings; + +public class ModelBoundQuerySettingsTests : WebApiTestBase { - public class ModelBoundQuerySettingsTests : WebApiTestBase + public ModelBoundQuerySettingsTests(WebApiTestFixture fixture) + : base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) { - public ModelBoundQuerySettingsTests(WebApiTestFixture fixture) - : base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(AuthorsController)); - - services.AddControllers().AddOData( - options => - { - options.AddRouteComponents("enablequery", ModelBoundQuerySettingsEdmModel.GetEdmModel()); - options.AddRouteComponents("modelboundapi", ModelBoundQuerySettingsEdmModel.GetEdmModelByModelBoundAPI()); - }); - } - - [Theory] - [InlineData("enablequery/Authors?$filter=Books/any(d: d/BookId eq 7)")] - [InlineData("modelboundapi/Authors?$filter=Books/any(d: d/BookId eq 7)")] - public async Task FilterOnNestedCollection(string requestUri) - { - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); - HttpClient client = CreateClient(); - - var response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var content = await response.Content.ReadAsObject(); - - var authors = content.GetValue("value") as JArray; - Assert.NotNull(authors); - - var author = Assert.Single(authors) as JObject; - Assert.Equal(3, author.GetValue("AuthorId")); - } + services.ConfigureControllers(typeof(AuthorsController)); + + services.AddControllers().AddOData( + options => + { + options.AddRouteComponents("enablequery", ModelBoundQuerySettingsEdmModel.GetEdmModel()); + options.AddRouteComponents("modelboundapi", ModelBoundQuerySettingsEdmModel.GetEdmModelByModelBoundAPI()); + }); + } + + [Theory] + [InlineData("enablequery/Authors?$filter=Books/any(d: d/BookId eq 7)")] + [InlineData("modelboundapi/Authors?$filter=Books/any(d: d/BookId eq 7)")] + public async Task FilterOnNestedCollection(string requestUri) + { + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); + HttpClient client = CreateClient(); + + var response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var content = await response.Content.ReadAsObject(); + + var authors = content.GetValue("value") as JArray; + Assert.NotNull(authors); + + var author = Assert.Single(authors) as JObject; + Assert.Equal(3, author.GetValue("AuthorId")); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/NavigationPropertyOnComplexTypeModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/NavigationPropertyOnComplexTypeModel.cs index 794eda1b9..ca83ccb44 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/NavigationPropertyOnComplexTypeModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/NavigationPropertyOnComplexTypeModel.cs @@ -10,103 +10,102 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType +namespace Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType; + +public class Person { - public class Person - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public int Age { get; set; } + public int Age { get; set; } - public IList Taxes { get; set; } + public IList Taxes { get; set; } - public Address HomeLocation { get; set; } + public Address HomeLocation { get; set; } - public IList
RepoLocations { get; set; } + public IList
RepoLocations { get; set; } - public GeoLocation PreciseLocation { get; set; } + public GeoLocation PreciseLocation { get; set; } - public OrderInfo OrderInfo { get; set; } - } + public OrderInfo OrderInfo { get; set; } +} - public class VipPerson : Person - { - public int Bonus { get; set; } - } +public class VipPerson : Person +{ + public int Bonus { get; set; } +} - public class Address - { - public string Street { get; set; } +public class Address +{ + public string Street { get; set; } - public int TaxNo { get; set; } + public int TaxNo { get; set; } - public IList Emails { get; set; } + public IList Emails { get; set; } - public AddressInfo RelatedInfo { get; set; } + public AddressInfo RelatedInfo { get; set; } - public IList AdditionInfos { get; set; } + public IList AdditionInfos { get; set; } - public ZipCode ZipCode { get; set; } + public ZipCode ZipCode { get; set; } - public IList DetailCodes { get; set; } - } + public IList DetailCodes { get; set; } +} - public class AddressInfo - { - public int AreaSize { get; set; } +public class AddressInfo +{ + public int AreaSize { get; set; } - public string CountyName { get; set; } - } + public string CountyName { get; set; } +} - public class OrderInfo - { - public Address BillLocation { get; set; } +public class OrderInfo +{ + public Address BillLocation { get; set; } - public OrderInfo SubInfo { get; set; } + public OrderInfo SubInfo { get; set; } - public IDictionary propertybag { get; set; } - } + public IDictionary propertybag { get; set; } +} - public class ZipCode - { - [Key] - public int Zip { get; set; } +public class ZipCode +{ + [Key] + public int Zip { get; set; } - public string City { get; set; } + public string City { get; set; } - public string State { get; set; } - } + public string State { get; set; } +} - public class GeoLocation : Address - { - public string Latitude { get; set; } +public class GeoLocation : Address +{ + public string Latitude { get; set; } - public string Longitude { get; set; } + public string Longitude { get; set; } - public ZipCode Area { get; set; } - } + public ZipCode Area { get; set; } +} - // with the same propert name in different derived type. - public class GeometryLocation : Address - { - public string Latitude { get; set; } +// with the same propert name in different derived type. +public class GeometryLocation : Address +{ + public string Latitude { get; set; } - public string Longitude { get; set; } - } + public string Longitude { get; set; } +} - public class ModelGenerator +public class ModelGenerator +{ + // Builds the EDM model for the OData service. + public static IEdmModel GetConventionalEdmModel() { - // Builds the EDM model for the OData service. - public static IEdmModel GetConventionalEdmModel() - { - var modelBuilder = new ODataConventionModelBuilder(); - modelBuilder.EntitySet("People"); - modelBuilder.EntitySet("ZipCodes"); - - modelBuilder.Namespace = typeof(Person).Namespace; - return modelBuilder.GetEdmModel(); - } + var modelBuilder = new ODataConventionModelBuilder(); + modelBuilder.EntitySet("People"); + modelBuilder.EntitySet("ZipCodes"); + + modelBuilder.Namespace = typeof(Person).Namespace; + return modelBuilder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/NavigationPropertyOnComplexTypeTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/NavigationPropertyOnComplexTypeTests.cs index e7f3719d2..98762c37c 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/NavigationPropertyOnComplexTypeTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/NavigationPropertyOnComplexTypeTests.cs @@ -18,375 +18,374 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType -{ - // TODO: the test cases in this class hangs on the Azure Build pipeline, don't know the root cause yet. +namespace Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType; + +// TODO: the test cases in this class hangs on the Azure Build pipeline, don't know the root cause yet. #if false - public class NavigationPropertyOnComplexTypeTests : WebODataTestBase +public class NavigationPropertyOnComplexTypeTests : WebODataTestBase +{ + public class Startup : TestStartupBase { - public class Startup : TestStartupBase + public override void ConfigureServices(IServiceCollection services) { - public override void ConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(PeopleController)); + services.ConfigureControllers(typeof(PeopleController)); - IEdmModel model = ModelGenerator.GetConventionalEdmModel(); - services.AddOData(options => options.AddModel("odata", model).SetMaxTop(2).Expand().Select().OrderBy().Filter()); - } + IEdmModel model = ModelGenerator.GetConventionalEdmModel(); + services.AddOData(options => options.AddModel("odata", model).SetMaxTop(2).Expand().Select().OrderBy().Filter()); } + } - private const string PeopleBaseUrl = "odata/People"; + private const string PeopleBaseUrl = "odata/People"; - public NavigationPropertyOnComplexTypeTests(WebODataTestFixture factory) - : base(factory) - { - } + public NavigationPropertyOnComplexTypeTests(WebODataTestFixture factory) + : base(factory) + { + } - [Fact] - public void QueryNavigationPropertyOnComplexProperty() - { - // Arrange : GET ~/People(1)/HomeLocation/ZipCode - string requestUri = PeopleBaseUrl + "(1)/HomeLocation/ZipCode"; + [Fact] + public void QueryNavigationPropertyOnComplexProperty() + { + // Arrange : GET ~/People(1)/HomeLocation/ZipCode + string requestUri = PeopleBaseUrl + "(1)/HomeLocation/ZipCode"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#ZipCodes/$entity\"," + - "\"Zip\":98052,\"City\":\"Redmond\",\"State\":\"Washington\"}"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#ZipCodes/$entity\"," + + "\"Zip\":98052,\"City\":\"Redmond\",\"State\":\"Washington\"}"; - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, contains: null, equals: equals); - } + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, contains: null, equals: equals); + } - [Fact] - public void QueryComplexTypePropertyWithSelectAndExpand() - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)/HomeLocation?$select=Street&$expand=ZipCode"; + [Fact] + public void QueryComplexTypePropertyWithSelectAndExpand() + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)/HomeLocation?$select=Street&$expand=ZipCode"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(1)/HomeLocation(Street,ZipCode())\"," + - "\"Street\":\"110th\"," + - "\"ZipCode\":{\"Zip\":98052,\"City\":\"Redmond\",\"State\":\"Washington\"}}"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(1)/HomeLocation(Street,ZipCode())\"," + + "\"Street\":\"110th\"," + + "\"ZipCode\":{\"Zip\":98052,\"City\":\"Redmond\",\"State\":\"Washington\"}}"; - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, contains: null, equals: equals); - } + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, contains: null, equals: equals); + } + + [Fact] + public void QueryCollectionComplexTypePropertyWithSelectAndExpand() + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)/RepoLocations?$select=Street&$expand=ZipCode"; - [Fact] - public void QueryCollectionComplexTypePropertyWithSelectAndExpand() + // Act + string result = ExecuteAndVerifyQueryRequest(requestUri); + + // Assert + JObject jObj = JObject.Parse(result); + Assert.Equal("BASE_ADDRESS/odata/$metadata#People(1)/RepoLocations(Street,ZipCode())", jObj["@odata.context"]); + + var array = jObj["value"] as JArray; + Assert.Equal(3, array.Count); + + for (int i = 0; i < 3; i++) { - // Arrange - string requestUri = PeopleBaseUrl + "(1)/RepoLocations?$select=Street&$expand=ZipCode"; - - // Act - string result = ExecuteAndVerifyQueryRequest(requestUri); - - // Assert - JObject jObj = JObject.Parse(result); - Assert.Equal("BASE_ADDRESS/odata/$metadata#People(1)/RepoLocations(Street,ZipCode())", jObj["@odata.context"]); - - var array = jObj["value"] as JArray; - Assert.Equal(3, array.Count); - - for (int i = 0; i < 3; i++) - { - JObject item = array[i] as JObject; - Assert.Equal(new[] { "Street", "ZipCode" }, - item.Properties().Where(p => !p.Name.StartsWith("@")).Select(p => p.Name)); - string street = "1" + (1 + i) + "0th"; // 110th, 120th, 130th - Assert.Equal(street, array[i]["Street"].ToString()); - } + JObject item = array[i] as JObject; + Assert.Equal(new[] { "Street", "ZipCode" }, + item.Properties().Where(p => !p.Name.StartsWith("@")).Select(p => p.Name)); + string street = "1" + (1 + i) + "0th"; // 110th, 120th, 130th + Assert.Equal(street, array[i]["Street"].ToString()); } + } - [Fact] - public void QueryEntityWithExpandOnNavigationPropertyOfComplexProperty() - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$expand=HomeLocation/ZipCode"; + [Fact] + public void QueryEntityWithExpandOnNavigationPropertyOfComplexProperty() + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$expand=HomeLocation/ZipCode"; - // Includes all properties of People(1) and expand the ZipCode on HomeLocation - string contains = ",\"Id\":1," + - "\"Name\":\"Kate\"," + - "\"Age\":5," + - "\"Taxes\":[7,5,9]," + - "\"HomeLocation\":{\"Street\":\"110th\",\"TaxNo\":19,\"Emails\":[\"E1\",\"E3\",\"E2\"],"; + // Includes all properties of People(1) and expand the ZipCode on HomeLocation + string contains = ",\"Id\":1," + + "\"Name\":\"Kate\"," + + "\"Age\":5," + + "\"Taxes\":[7,5,9]," + + "\"HomeLocation\":{\"Street\":\"110th\",\"TaxNo\":19,\"Emails\":[\"E1\",\"E3\",\"E2\"],"; - // Act & Assert - string result = ExecuteAndVerifyQueryRequest(requestUri, contains); + // Act & Assert + string result = ExecuteAndVerifyQueryRequest(requestUri, contains); - Assert.Contains("}],\"ZipCode\":{\"Zip\":98052,\"City\":\"Redmond\",\"State\":\"Washington\"}},\"RepoLoc", result); - } + Assert.Contains("}],\"ZipCode\":{\"Zip\":98052,\"City\":\"Redmond\",\"State\":\"Washington\"}},\"RepoLoc", result); + } - [Fact] - public void QueryEntityWithExpandOnNavigationPropertyOfComplexTypePropertyAndSelectOnOtherProperty() - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$expand=HomeLocation/ZipCode&$select=Name"; - - // Only includes Name, HomeLocation and expand ZipCode on HomeLocation - // Be noted: The output should not include "Primitive properties" in "HomeLocation". - // The issue is form ODL, it includes an "PathSelectItem (HomeLocation)" in the SelectExpandClause. - // See detail at: https://github.com/OData/odata.net/issues/1574 - string contains = "\"Name\":\"Kate\"," + - "\"HomeLocation\":{\"Street\":\"110th\",\"TaxNo\":19,\"Emails\":[\"E1\",\"E3\",\"E2\"]," + - "\"RelatedInfo\":{\"AreaSize\":101,\"CountyName\":\"King\"}," + - "\"AdditionInfos\":[{\"AreaSize\":102,\"CountyName\":\"King1\"},{\"AreaSize\":103,\"CountyName\":\"King2\"},{\"AreaSize\":104,\"CountyName\":\"King3\"}]," + - "\"ZipCode\":{\"Zip\":98052,\"City\":\"Redmond\",\"State\":\"Washington\"}}"; - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, contains); - } + [Fact] + public void QueryEntityWithExpandOnNavigationPropertyOfComplexTypePropertyAndSelectOnOtherProperty() + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$expand=HomeLocation/ZipCode&$select=Name"; + + // Only includes Name, HomeLocation and expand ZipCode on HomeLocation + // Be noted: The output should not include "Primitive properties" in "HomeLocation". + // The issue is form ODL, it includes an "PathSelectItem (HomeLocation)" in the SelectExpandClause. + // See detail at: https://github.com/OData/odata.net/issues/1574 + string contains = "\"Name\":\"Kate\"," + + "\"HomeLocation\":{\"Street\":\"110th\",\"TaxNo\":19,\"Emails\":[\"E1\",\"E3\",\"E2\"]," + + "\"RelatedInfo\":{\"AreaSize\":101,\"CountyName\":\"King\"}," + + "\"AdditionInfos\":[{\"AreaSize\":102,\"CountyName\":\"King1\"},{\"AreaSize\":103,\"CountyName\":\"King2\"},{\"AreaSize\":104,\"CountyName\":\"King3\"}]," + + "\"ZipCode\":{\"Zip\":98052,\"City\":\"Redmond\",\"State\":\"Washington\"}}"; + + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, contains); + } - [Fact] - public void QueryEntityWithExpandOnNavigationPropertyOfComplexTypePropertyAndSelectOnComplexProperty() - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$expand=HomeLocation/ZipCode&$select=HomeLocation/Street"; + [Fact] + public void QueryEntityWithExpandOnNavigationPropertyOfComplexTypePropertyAndSelectOnComplexProperty() + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$expand=HomeLocation/ZipCode&$select=HomeLocation/Street"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Street,HomeLocation/ZipCode())/$entity\"," + - "\"HomeLocation\":{\"Street\":\"110th\",\"ZipCode\":{\"Zip\":98052,\"City\":\"Redmond\",\"State\":\"Washington\"}}}"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Street,HomeLocation/ZipCode())/$entity\"," + + "\"HomeLocation\":{\"Street\":\"110th\",\"ZipCode\":{\"Zip\":98052,\"City\":\"Redmond\",\"State\":\"Washington\"}}}"; - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, contains: null, equals: equals); - } + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, contains: null, equals: equals); + } - [Theory] - [InlineData(1)] - [InlineData(3)] - public void QueryEntityWithExpandOnMultipleNavigationPropertiesOfComplexTypeProperty(int key) + [Theory] + [InlineData(1)] + [InlineData(3)] + public void QueryEntityWithExpandOnMultipleNavigationPropertiesOfComplexTypeProperty(int key) + { + // Arrange + string requestUri = PeopleBaseUrl + "(" + key + ")?$expand=HomeLocation/ZipCode,PreciseLocation/ZipCode&$select=Name"; + + // only includes Name, HomeLocation, PreciseLocation and expand ZipCode on HomeLocation + string equals; + if (key == 1) { - // Arrange - string requestUri = PeopleBaseUrl + "(" + key + ")?$expand=HomeLocation/ZipCode,PreciseLocation/ZipCode&$select=Name"; - - // only includes Name, HomeLocation, PreciseLocation and expand ZipCode on HomeLocation - string equals; - if (key == 1) - { - equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Name,HomeLocation/ZipCode(),PreciseLocation/ZipCode())/$entity\"," + - "\"Name\":\"Kate\"," + - "\"HomeLocation\":{\"Street\":\"110th\",\"TaxNo\":19,\"Emails\":[\"E1\",\"E3\",\"E2\"],\"RelatedInfo\":{\"AreaSize\":101,\"CountyName\":\"King\"},\"AdditionInfos\":[{\"AreaSize\":102,\"CountyName\":\"King1\"},{\"AreaSize\":103,\"CountyName\":\"King2\"},{\"AreaSize\":104,\"CountyName\":\"King3\"}],\"ZipCode\":{\"Zip\":98052,\"City\":\"Redmond\",\"State\":\"Washington\"}}," + - "\"PreciseLocation\":null}"; - } - else - { - - equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Name,HomeLocation/ZipCode(),PreciseLocation/ZipCode())/$entity\"," + - "\"Name\":\"Carlos\"," + - "\"HomeLocation\":null," + - "\"PreciseLocation\":{" + - "\"Street\":\"50th\"," + - "\"TaxNo\":0,\"Emails\":[],\"Latitude\":\"12\",\"Longitude\":\"22\"," + - "\"RelatedInfo\":null," + - "\"AdditionInfos\":[]," + - "\"ZipCode\":{\"Zip\":35816,\"City\":\"Huntsville\",\"State\":\"Alabama\"}}}"; - } - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, null, equals); + equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Name,HomeLocation/ZipCode(),PreciseLocation/ZipCode())/$entity\"," + + "\"Name\":\"Kate\"," + + "\"HomeLocation\":{\"Street\":\"110th\",\"TaxNo\":19,\"Emails\":[\"E1\",\"E3\",\"E2\"],\"RelatedInfo\":{\"AreaSize\":101,\"CountyName\":\"King\"},\"AdditionInfos\":[{\"AreaSize\":102,\"CountyName\":\"King1\"},{\"AreaSize\":103,\"CountyName\":\"King2\"},{\"AreaSize\":104,\"CountyName\":\"King3\"}],\"ZipCode\":{\"Zip\":98052,\"City\":\"Redmond\",\"State\":\"Washington\"}}," + + "\"PreciseLocation\":null}"; } - - [Fact] - public void QueryEntityWithExpandOnNavigationPropertiesOnDeepComplexTypeProperty() + else { - // Arrange - string requestUri = PeopleBaseUrl + "(2)?$expand=OrderInfo/BillLocation/ZipCode&$select=OrderInfo"; - - string contains = "odata/$metadata#People(OrderInfo,OrderInfo/BillLocation/ZipCode())/$entity\"," + - "\"OrderInfo\":{" + - "\"BillLocation\":{" + - "\"Street\":\"110th\"," + - "\"TaxNo\":0," + - "\"Emails\":[]," + - "\"RelatedInfo\":null,\"AdditionInfos\":[]," + - "\"ZipCode\":{" + - "\"Zip\":98052," + - "\"City\":\"Redmond\"," + - "\"State\":\"Washington\"" + - "}" + - "}," + - "\"SubInfo\":null" + - "}"; - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, contains); + equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Name,HomeLocation/ZipCode(),PreciseLocation/ZipCode())/$entity\"," + + "\"Name\":\"Carlos\"," + + "\"HomeLocation\":null," + + "\"PreciseLocation\":{" + + "\"Street\":\"50th\"," + + "\"TaxNo\":0,\"Emails\":[],\"Latitude\":\"12\",\"Longitude\":\"22\"," + + "\"RelatedInfo\":null," + + "\"AdditionInfos\":[]," + + "\"ZipCode\":{\"Zip\":35816,\"City\":\"Huntsville\",\"State\":\"Alabama\"}}}"; } - [Fact] - public void QueryEntityWithReferenceOnNavigationPropertiesOfComplexProperty() - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$expand=HomeLocation/ZipCode/$ref&$select=HomeLocation/Street"; - - string contains = "odata/$metadata#People(HomeLocation/Street,HomeLocation/ZipCode,HomeLocation/ZipCode/$ref())/$entity\"," + - "\"HomeLocation\":{" + - "\"Street\":\"110th\"," + - "\"ZipCode\":{" + - "\"@odata.id\":\"ZipCodes(98052)\"" + - "}" + - "}" + - "}"; + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, null, equals); + } - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, contains); - } + [Fact] + public void QueryEntityWithExpandOnNavigationPropertiesOnDeepComplexTypeProperty() + { + // Arrange + string requestUri = PeopleBaseUrl + "(2)?$expand=OrderInfo/BillLocation/ZipCode&$select=OrderInfo"; + + string contains = "odata/$metadata#People(OrderInfo,OrderInfo/BillLocation/ZipCode())/$entity\"," + + "\"OrderInfo\":{" + + "\"BillLocation\":{" + + "\"Street\":\"110th\"," + + "\"TaxNo\":0," + + "\"Emails\":[]," + + "\"RelatedInfo\":null,\"AdditionInfos\":[]," + + "\"ZipCode\":{" + + "\"Zip\":98052," + + "\"City\":\"Redmond\"," + + "\"State\":\"Washington\"" + + "}" + + "}," + + "\"SubInfo\":null" + + "}"; - [Fact] - public void QueryEntityWithReferenceOnCollectionNavigationPropertiesOfComplexProperty() - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$expand=HomeLocation/DetailCodes/$ref&$select=HomeLocation/Street"; - - string contains = "odata/$metadata#People(HomeLocation/Street,HomeLocation/DetailCodes,HomeLocation/DetailCodes/$ref())/$entity\"," + - "\"HomeLocation\":{" + - "\"Street\":\"110th\"," + - "\"DetailCodes\":[" + - "{" + - "\"@odata.id\":\"ZipCodes(98052)\"" + - "}," + - "{" + - "\"@odata.id\":\"ZipCodes(35816)\"" + - "}," + - "{" + - "\"@odata.id\":\"ZipCodes(10048)\"" + - "}" + - "]" + - "}" + - "}"; + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, contains); + } - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, contains); - } + [Fact] + public void QueryEntityWithReferenceOnNavigationPropertiesOfComplexProperty() + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$expand=HomeLocation/ZipCode/$ref&$select=HomeLocation/Street"; + + string contains = "odata/$metadata#People(HomeLocation/Street,HomeLocation/ZipCode,HomeLocation/ZipCode/$ref())/$entity\"," + + "\"HomeLocation\":{" + + "\"Street\":\"110th\"," + + "\"ZipCode\":{" + + "\"@odata.id\":\"ZipCodes(98052)\"" + + "}" + + "}" + + "}"; - [Fact] - public void QueryEntityWithReferenceOnNavigationPropertiesOfDeepComplexProperty() - { - // Arrange - string requestUri = PeopleBaseUrl + "(2)?$expand=OrderInfo/BillLocation/ZipCode/$ref&$select=OrderInfo/BillLocation/Street"; - - string contains = "odata/$metadata#People(OrderInfo/BillLocation/Street,OrderInfo/BillLocation/ZipCode,OrderInfo/BillLocation/ZipCode/$ref())/$entity\"," + - "\"OrderInfo\":{" + - "\"BillLocation\":{" + - "\"Street\":\"110th\"," + - "\"ZipCode\":{" + - "\"@odata.id\":\"ZipCodes(98052)\"" + - "}" + + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, contains); + } + + [Fact] + public void QueryEntityWithReferenceOnCollectionNavigationPropertiesOfComplexProperty() + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$expand=HomeLocation/DetailCodes/$ref&$select=HomeLocation/Street"; + + string contains = "odata/$metadata#People(HomeLocation/Street,HomeLocation/DetailCodes,HomeLocation/DetailCodes/$ref())/$entity\"," + + "\"HomeLocation\":{" + + "\"Street\":\"110th\"," + + "\"DetailCodes\":[" + + "{" + + "\"@odata.id\":\"ZipCodes(98052)\"" + + "}," + + "{" + + "\"@odata.id\":\"ZipCodes(35816)\"" + + "}," + + "{" + + "\"@odata.id\":\"ZipCodes(10048)\"" + "}" + - "}" + - "}"; + "]" + + "}" + + "}"; - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, contains); - } + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, contains); + } - [Fact] - public async Task DeserializingNavigationPropertyOnComplexType() - { - // Arrange - string url = PeopleBaseUrl + "(1)/HomeLocation/ZipCode/$ref"; - string payload = "{\"Zip\":98038,\"City\":\"Redmond\",\"State\":\"Washington\"}"; - HttpContent content = new StringContent(payload, Encoding.UTF8, mediaType: "application/json"); - content.Headers.ContentLength = payload.Length; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url); - request.Content = content; - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); - - // Act - HttpResponseMessage response = await this.Client.SendAsync(request); - string result = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Contains(payload.TrimStart('{'), result); - } + [Fact] + public void QueryEntityWithReferenceOnNavigationPropertiesOfDeepComplexProperty() + { + // Arrange + string requestUri = PeopleBaseUrl + "(2)?$expand=OrderInfo/BillLocation/ZipCode/$ref&$select=OrderInfo/BillLocation/Street"; + + string contains = "odata/$metadata#People(OrderInfo/BillLocation/Street,OrderInfo/BillLocation/ZipCode,OrderInfo/BillLocation/ZipCode/$ref())/$entity\"," + + "\"OrderInfo\":{" + + "\"BillLocation\":{" + + "\"Street\":\"110th\"," + + "\"ZipCode\":{" + + "\"@odata.id\":\"ZipCodes(98052)\"" + + "}" + + "}" + + "}" + + "}"; - [Fact] - public void QueryComplexWithExpandOnDerivedNavigationProperty() - { - // Arrange - string requestUri = PeopleBaseUrl + "(4)/HomeLocation?$expand=Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Area"; + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, contains); + } - string contains = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(4)/HomeLocation(Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Area())\"," + - "\"Street\":\"120th\",\"TaxNo\":17,\"Emails\":[\"E7\",\"E4\",\"E5\"],\"Latitude\":\"12.8\",\"Longitude\":\"22.9\",\"Rela"; + [Fact] + public async Task DeserializingNavigationPropertyOnComplexType() + { + // Arrange + string url = PeopleBaseUrl + "(1)/HomeLocation/ZipCode/$ref"; + string payload = "{\"Zip\":98038,\"City\":\"Redmond\",\"State\":\"Washington\"}"; + HttpContent content = new StringContent(payload, Encoding.UTF8, mediaType: "application/json"); + content.Headers.ContentLength = payload.Length; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url); + request.Content = content; + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); + + // Act + HttpResponseMessage response = await this.Client.SendAsync(request); + string result = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains(payload.TrimStart('{'), result); + } - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, contains: contains, equals: null); - } + [Fact] + public void QueryComplexWithExpandOnDerivedNavigationProperty() + { + // Arrange + string requestUri = PeopleBaseUrl + "(4)/HomeLocation?$expand=Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Area"; - [Fact] - public void QueryComplexWithTypeCastAndExpandOnDerivedNavigationProperty() - { - // Arrange - string requestUri = PeopleBaseUrl + "(4)/HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation?$expand=Area"; + string contains = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(4)/HomeLocation(Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Area())\"," + + "\"Street\":\"120th\",\"TaxNo\":17,\"Emails\":[\"E7\",\"E4\",\"E5\"],\"Latitude\":\"12.8\",\"Longitude\":\"22.9\",\"Rela"; - string contains = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(4)/HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation(Area())\"," + - "\"Street\":\"120th\",\"TaxNo\":17,\"Emails\":[\"E7\",\"E4\",\"E5\"],\"Latitude\":\"12.8\",\"Longitude\":\"22.9\",\"Rela"; + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, contains: contains, equals: null); + } - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, contains: contains, equals: null); - } + [Fact] + public void QueryComplexWithTypeCastAndExpandOnDerivedNavigationProperty() + { + // Arrange + string requestUri = PeopleBaseUrl + "(4)/HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation?$expand=Area"; - [Fact] - public void QueryEntityWithExpandOnComplexWithTypeCastAndDerivedNavigationProperty() - { - // Arrange - string requestUri = PeopleBaseUrl + "(4)?$expand=HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Area"; + string contains = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(4)/HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation(Area())\"," + + "\"Street\":\"120th\",\"TaxNo\":17,\"Emails\":[\"E7\",\"E4\",\"E5\"],\"Latitude\":\"12.8\",\"Longitude\":\"22.9\",\"Rela"; - string contains = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Area())/$entity\"," + - "\"Id\":4,\"Name\":\"Jones\",\"Age\":9,"; + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, contains: contains, equals: null); + } - // Act & Assert - var result = ExecuteAndVerifyQueryRequest(requestUri, contains: contains, equals: null); + [Fact] + public void QueryEntityWithExpandOnComplexWithTypeCastAndDerivedNavigationProperty() + { + // Arrange + string requestUri = PeopleBaseUrl + "(4)?$expand=HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Area"; - Assert.Contains("\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Street\":\"12", result); - } + string contains = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Area())/$entity\"," + + "\"Id\":4,\"Name\":\"Jones\",\"Age\":9,"; - [Fact] - public void QueryEntityWithExpandNavigationPropertyOnRecursiveComplexProperty() - { - // Arrange - string requestUri = PeopleBaseUrl + "(4)?$expand=OrderInfo/SubInfo/BillLocation/ZipCode&$select=OrderInfo/SubInfo/BillLocation/Street"; - - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(OrderInfo/SubInfo/BillLocation/Street,OrderInfo/SubInfo/BillLocation/ZipCode())/$entity\"," + - "\"OrderInfo\":{" + - "\"SubInfo\":{" + - "\"BillLocation\":{" + - "\"Street\":\"110th\"," + - "\"ZipCode\":{\"Zip\":35816,\"City\":\"Huntsville\",\"State\":\"Alabama\"}}}}}"; - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, contains: null, equals: equals); - } + // Act & Assert + var result = ExecuteAndVerifyQueryRequest(requestUri, contains: contains, equals: null); - private string ExecuteAndVerifyQueryRequest(string requestUri, string contains = null, string equals = null) - { - // Arrange - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + Assert.Contains("\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Street\":\"12", result); + } - // Act - HttpResponseMessage response = Client.SendAsync(request).Result; + [Fact] + public void QueryEntityWithExpandNavigationPropertyOnRecursiveComplexProperty() + { + // Arrange + string requestUri = PeopleBaseUrl + "(4)?$expand=OrderInfo/SubInfo/BillLocation/ZipCode&$select=OrderInfo/SubInfo/BillLocation/Street"; + + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(OrderInfo/SubInfo/BillLocation/Street,OrderInfo/SubInfo/BillLocation/ZipCode())/$entity\"," + + "\"OrderInfo\":{" + + "\"SubInfo\":{" + + "\"BillLocation\":{" + + "\"Street\":\"110th\"," + + "\"ZipCode\":{\"Zip\":35816,\"City\":\"Huntsville\",\"State\":\"Alabama\"}}}}}"; + + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, contains: null, equals: equals); + } - // Assert - string result = response.Content.ReadAsStringAsync().Result; + private string ExecuteAndVerifyQueryRequest(string requestUri, string contains = null, string equals = null) + { + // Arrange + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Act + HttpResponseMessage response = Client.SendAsync(request).Result; - // replace the real address using "BASE_ADDRESS" - string odataContext = "\"@odata.context\":\""; - int start = result.IndexOf(odataContext) + odataContext.Length; - int end = result.IndexOf("/odata/$metadata"); - string uri = result.Substring(start, end - start); - result = result.Replace(uri, "BASE_ADDRESS"); + // Assert + string result = response.Content.ReadAsStringAsync().Result; - if (contains != null) - { - Assert.Contains(contains, result); - } + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - if (equals != null) - { - Assert.Equal(equals, result); - } + // replace the real address using "BASE_ADDRESS" + string odataContext = "\"@odata.context\":\""; + int start = result.IndexOf(odataContext) + odataContext.Length; + int end = result.IndexOf("/odata/$metadata"); + string uri = result.Substring(start, end - start); + result = result.Replace(uri, "BASE_ADDRESS"); - return result; + if (contains != null) + { + Assert.Contains(contains, result); } + + if (equals != null) + { + Assert.Equal(equals, result); + } + + return result; } -#endif } +#endif diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/PeopleController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/PeopleController.cs index 0d353c80e..5b8fbfa7e 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/PeopleController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/PeopleController.cs @@ -11,101 +11,100 @@ using Microsoft.AspNetCore.OData.Formatter; using Microsoft.AspNetCore.OData.Query; -namespace Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType +namespace Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType; + +public class PeopleController : ControllerBase { - public class PeopleController : ControllerBase + private PeopleRepository _repo = new PeopleRepository(); + + [HttpGet] + [EnableQuery] + public IEnumerable Get() { - private PeopleRepository _repo = new PeopleRepository(); + return _repo.People; + } - [HttpGet] - [EnableQuery] - public IEnumerable Get() + [HttpGet] + [EnableQuery] + public IActionResult Get([FromODataUri]int key) + { + Person person = _repo.People.FirstOrDefault(p => p.Id == key); + if (person == null) { - return _repo.People; + return NotFound(); } - [HttpGet] - [EnableQuery] - public IActionResult Get([FromODataUri]int key) - { - Person person = _repo.People.FirstOrDefault(p => p.Id == key); - if (person == null) - { - return NotFound(); - } - - return Ok(person); - } + return Ok(person); + } - [EnableQuery] - public IActionResult GetHomeLocationFromPerson([FromODataUri]int key) + [EnableQuery] + public IActionResult GetHomeLocationFromPerson([FromODataUri]int key) + { + Person person = _repo.People.FirstOrDefault(p => p.Id == key); + if (person == null) { - Person person = _repo.People.FirstOrDefault(p => p.Id == key); - if (person == null) - { - return NotFound(); - } - return Ok(person.HomeLocation); + return NotFound(); } + return Ok(person.HomeLocation); + } - [EnableQuery] - public IActionResult GetRepoLocationsFromPerson([FromODataUri]int key) + [EnableQuery] + public IActionResult GetRepoLocationsFromPerson([FromODataUri]int key) + { + Person person = _repo.People.FirstOrDefault(p => p.Id == key); + if (person == null) { - Person person = _repo.People.FirstOrDefault(p => p.Id == key); - if (person == null) - { - return NotFound(); - } - return Ok(person.RepoLocations); + return NotFound(); } + return Ok(person.RepoLocations); + } - /* - [EnableQuery] - public ITestActionResult GetLocationOfAddress([FromODataUri]int key) - { - Person person = _repo.people.FirstOrDefault(p => p.Id == key); - if (person == null) - { - return NotFound(); - } - return Ok(person.Location as Address); - }*/ - - [EnableQuery] - public IActionResult GetHomeLocationOfGeoLocation([FromODataUri]int key) + /* + [EnableQuery] + public ITestActionResult GetLocationOfAddress([FromODataUri]int key) + { + Person person = _repo.people.FirstOrDefault(p => p.Id == key); + if (person == null) { - Person person = _repo.People.FirstOrDefault(p => p.Id == key); - if (person == null) - { - return NotFound(); - } - - return Ok(person.HomeLocation as GeoLocation); + return NotFound(); } + return Ok(person.Location as Address); + }*/ - [EnableQuery] - [HttpGet("People({id})/OrderInfo")] - public IActionResult GetOrdeInfoFromPerson([FromODataUri]int id) + [EnableQuery] + public IActionResult GetHomeLocationOfGeoLocation([FromODataUri]int key) + { + Person person = _repo.People.FirstOrDefault(p => p.Id == key); + if (person == null) { - return Ok(_repo.People.FirstOrDefault(p => p.Id == id).OrderInfo); + return NotFound(); } - [HttpGet("People({id})/HomeLocation/ZipCode")] - public IActionResult GetZipCode([FromODataUri]int id) - { - Person person = _repo.People.FirstOrDefault(p => p.Id == id); - if (person == null) - { - return NotFound(); - } + return Ok(person.HomeLocation as GeoLocation); + } - return Ok(person.HomeLocation.ZipCode); - } + [EnableQuery] + [HttpGet("People({id})/OrderInfo")] + public IActionResult GetOrdeInfoFromPerson([FromODataUri]int id) + { + return Ok(_repo.People.FirstOrDefault(p => p.Id == id).OrderInfo); + } - [HttpPost("People({id})/HomeLocation/ZipCode/$ref")] - public IActionResult CreateRefToZipCode([FromODataUri] int id, [FromBody] ZipCode zip) + [HttpGet("People({id})/HomeLocation/ZipCode")] + public IActionResult GetZipCode([FromODataUri]int id) + { + Person person = _repo.People.FirstOrDefault(p => p.Id == id); + if (person == null) { - return Ok(zip); + return NotFound(); } + + return Ok(person.HomeLocation.ZipCode); + } + + [HttpPost("People({id})/HomeLocation/ZipCode/$ref")] + public IActionResult CreateRefToZipCode([FromODataUri] int id, [FromBody] ZipCode zip) + { + return Ok(zip); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/PeopleRepository.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/PeopleRepository.cs index 498797870..9d56e4401 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/PeopleRepository.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/PeopleRepository.cs @@ -8,178 +8,177 @@ using System.Collections.Generic; using System.Linq; -namespace Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType +namespace Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType; + +public class PeopleRepository { - public class PeopleRepository - { - public List People { get; private set; } + public List People { get; private set; } - public PeopleRepository() + public PeopleRepository() + { + var zipCodes = new List { - var zipCodes = new List - { - new ZipCode { Zip = 98052, City = "Redmond", State="Washington"}, - new ZipCode { Zip = 35816, City = "Huntsville", State = "Alabama"}, - new ZipCode { Zip = 10048, City = "New York", State = "New York"} - }; + new ZipCode { Zip = 98052, City = "Redmond", State="Washington"}, + new ZipCode { Zip = 35816, City = "Huntsville", State = "Alabama"}, + new ZipCode { Zip = 10048, City = "New York", State = "New York"} + }; - IDictionary propertyBag = new Dictionary + IDictionary propertyBag = new Dictionary + { + { "DynamicInt", 9 }, { - { "DynamicInt", 9 }, + "DynamicAddress", + new Address { - "DynamicAddress", - new Address + Street = "", + Emails = new List { - Street = "", - Emails = new List - { - "abc@1.com", - "xyz@2.com" - } + "abc@1.com", + "xyz@2.com" } } - }; + } + }; - var repoLocations = new Address[] + var repoLocations = new Address[] + { + new Address { - new Address - { - Street = "110th", - TaxNo = 19, - Emails = new [] { "E1", "E3", "E2" }, - RelatedInfo = new AddressInfo { AreaSize = 101, CountyName = "King" }, - AdditionInfos = Enumerable.Range(1, 3).Select(e => new AddressInfo - { - AreaSize = 101 + e, - CountyName = "King" + e - }).ToList(), - ZipCode = zipCodes[0], - DetailCodes = zipCodes - }, - new GeoLocation + Street = "110th", + TaxNo = 19, + Emails = new [] { "E1", "E3", "E2" }, + RelatedInfo = new AddressInfo { AreaSize = 101, CountyName = "King" }, + AdditionInfos = Enumerable.Range(1, 3).Select(e => new AddressInfo { - Street = "120th", - TaxNo = 17, - Emails = new [] { "E7", "E4", "E5" }, - RelatedInfo = null, - AdditionInfos = Enumerable.Range(1, 3).Select(e => new AddressInfo - { - AreaSize = 101 + e, - CountyName = "King" + e - }).ToList(), - Latitude = "12.8", - Longitude = "22.9", - ZipCode = zipCodes[1], - DetailCodes = zipCodes, - Area = zipCodes[2] - }, - new Address + AreaSize = 101 + e, + CountyName = "King" + e + }).ToList(), + ZipCode = zipCodes[0], + DetailCodes = zipCodes + }, + new GeoLocation + { + Street = "120th", + TaxNo = 17, + Emails = new [] { "E7", "E4", "E5" }, + RelatedInfo = null, + AdditionInfos = Enumerable.Range(1, 3).Select(e => new AddressInfo { - Street = "130th", - TaxNo = 18, - Emails = new [] { "E9", "E6", "E8" }, - RelatedInfo = new AddressInfo { AreaSize = 201, CountyName = "Queue" }, - AdditionInfos = new AddressInfo[0], - ZipCode = zipCodes[2], - DetailCodes = zipCodes - }, + AreaSize = 101 + e, + CountyName = "King" + e + }).ToList(), + Latitude = "12.8", + Longitude = "22.9", + ZipCode = zipCodes[1], + DetailCodes = zipCodes, + Area = zipCodes[2] + }, + new Address + { + Street = "130th", + TaxNo = 18, + Emails = new [] { "E9", "E6", "E8" }, + RelatedInfo = new AddressInfo { AreaSize = 201, CountyName = "Queue" }, + AdditionInfos = new AddressInfo[0], + ZipCode = zipCodes[2], + DetailCodes = zipCodes + }, - }; + }; - People = new List + People = new List + { + new Person { - new Person + Id = 1, + Name = "Kate", + Age = 5, + Taxes = new [] { 7, 5, 9 }, + HomeLocation = repoLocations[0], + RepoLocations = repoLocations, + PreciseLocation = null, // by design + OrderInfo = new OrderInfo { - Id = 1, - Name = "Kate", - Age = 5, - Taxes = new [] { 7, 5, 9 }, - HomeLocation = repoLocations[0], - RepoLocations = repoLocations, - PreciseLocation = null, // by design - OrderInfo = new OrderInfo - { - BillLocation = repoLocations[0], - SubInfo = null - } - }, - new Person + BillLocation = repoLocations[0], + SubInfo = null + } + }, + new Person + { + Id = 2, + Name = "Lewis", + Age = 6 , + Taxes = new [] { 1, 5, 2 }, + HomeLocation = new GeoLocation{ ZipCode = zipCodes[1], Street = "110th", Latitude = "12.211", Longitude ="231.131" }, + RepoLocations = repoLocations, + PreciseLocation = null, // by design + OrderInfo = new OrderInfo { - Id = 2, - Name = "Lewis", - Age = 6 , - Taxes = new [] { 1, 5, 2 }, - HomeLocation = new GeoLocation{ ZipCode = zipCodes[1], Street = "110th", Latitude = "12.211", Longitude ="231.131" }, - RepoLocations = repoLocations, - PreciseLocation = null, // by design - OrderInfo = new OrderInfo - { - BillLocation = new Address{ ZipCode = zipCodes[0], Street = "110th" } - } - }, - new Person + BillLocation = new Address{ ZipCode = zipCodes[0], Street = "110th" } + } + }, + new Person + { + Id = 3, + Name = "Carlos", + Age = 7, + HomeLocation = null, // by design + RepoLocations = repoLocations, + OrderInfo = new OrderInfo { - Id = 3, - Name = "Carlos", - Age = 7, - HomeLocation = null, // by design - RepoLocations = repoLocations, - OrderInfo = new OrderInfo - { - BillLocation = new Address{ ZipCode = zipCodes[0], Street = "110th" } - }, - PreciseLocation = new GeoLocation{Area = zipCodes[2], Latitude = "12", Longitude = "22", Street = "50th", ZipCode = zipCodes[1]} + BillLocation = new Address{ ZipCode = zipCodes[0], Street = "110th" } }, - new Person + PreciseLocation = new GeoLocation{Area = zipCodes[2], Latitude = "12", Longitude = "22", Street = "50th", ZipCode = zipCodes[1]} + }, + new Person + { + Id = 4, + Name = "Jones", + Age = 9, + HomeLocation = repoLocations[1], + RepoLocations = repoLocations.Take(2).ToList(), + PreciseLocation = new GeoLocation{Area = zipCodes[2], Latitude = "12", Longitude = "22", Street = "50th", ZipCode = zipCodes[1]}, + OrderInfo = new OrderInfo { - Id = 4, - Name = "Jones", - Age = 9, - HomeLocation = repoLocations[1], - RepoLocations = repoLocations.Take(2).ToList(), - PreciseLocation = new GeoLocation{Area = zipCodes[2], Latitude = "12", Longitude = "22", Street = "50th", ZipCode = zipCodes[1]}, - OrderInfo = new OrderInfo - { - BillLocation = new Address{ ZipCode = zipCodes[0], Street = "110th" }, - SubInfo = new OrderInfo{ BillLocation = new Address{ ZipCode = zipCodes[1], Street = "110th" }} - } - }, - new Person + BillLocation = new Address{ ZipCode = zipCodes[0], Street = "110th" }, + SubInfo = new OrderInfo{ BillLocation = new Address{ ZipCode = zipCodes[1], Street = "110th" }} + } + }, + new Person + { + Id = 5, + Name = "Park", + Age = 17, + HomeLocation = repoLocations[2], + RepoLocations = repoLocations.Take(1).ToList(), + OrderInfo = new OrderInfo() { - Id = 5, - Name = "Park", - Age = 17, - HomeLocation = repoLocations[2], - RepoLocations = repoLocations.Take(1).ToList(), - OrderInfo = new OrderInfo() - { - propertybag = propertyBag - } - }, - new VipPerson + propertybag = propertyBag + } + }, + new VipPerson + { + Id = 6, + Name = "Sam", + Age = 40, + HomeLocation = new GeometryLocation { - Id = 6, - Name = "Sam", - Age = 40, - HomeLocation = new GeometryLocation + Street = "130th", + TaxNo = 18, + Emails = new [] { "A9", "A6", "A8" }, + RelatedInfo = new AddressInfo { AreaSize = 101, CountyName = "King" }, + AdditionInfos = Enumerable.Range(1, 3).Select(e => new AddressInfo { - Street = "130th", - TaxNo = 18, - Emails = new [] { "A9", "A6", "A8" }, - RelatedInfo = new AddressInfo { AreaSize = 101, CountyName = "King" }, - AdditionInfos = Enumerable.Range(1, 3).Select(e => new AddressInfo - { - AreaSize = 101 + e, - CountyName = "King" + e - }).ToList(), - ZipCode = zipCodes[2], - DetailCodes = zipCodes, - Latitude = "101.1", - Longitude = "202.2" - }, - Bonus = 99 - } - }; - } + AreaSize = 101 + e, + CountyName = "King" + e + }).ToList(), + ZipCode = zipCodes[2], + DetailCodes = zipCodes, + Latitude = "101.1", + Longitude = "202.2" + }, + Bonus = 99 + } + }; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/SelectImprovementOnComplexTypeTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/SelectImprovementOnComplexTypeTests.cs index 640420cf9..f2d31a969 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/SelectImprovementOnComplexTypeTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/NavigationPropertyOnComplexType/SelectImprovementOnComplexTypeTests.cs @@ -13,527 +13,526 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType +namespace Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType; + +public class SelectImprovementOnComplexTypeTests : WebODataTestBase { - public class SelectImprovementOnComplexTypeTests : WebODataTestBase + public class Startup : TestStartupBase { - public class Startup : TestStartupBase + public override void ConfigureServices(IServiceCollection services) { - public override void ConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(PeopleController)); + services.ConfigureControllers(typeof(PeopleController)); - IEdmModel model = ModelGenerator.GetConventionalEdmModel(); - services.AddControllers().AddOData(options => options.AddRouteComponents("odata", model).SetMaxTop(2).Expand().Select().OrderBy().Filter()); - } + IEdmModel model = ModelGenerator.GetConventionalEdmModel(); + services.AddControllers().AddOData(options => options.AddRouteComponents("odata", model).SetMaxTop(2).Expand().Select().OrderBy().Filter()); } + } - private const string PeopleBaseUrl = "odata/People"; + private const string PeopleBaseUrl = "odata/People"; - public SelectImprovementOnComplexTypeTests(WebODataTestFixture factory) - : base(factory) - { - } + public SelectImprovementOnComplexTypeTests(WebODataTestFixture factory) + : base(factory) + { + } - #region SubProperty on Single ComplexProperty - [Theory] - [InlineData("HomeLocation/Street,HomeLocation/TaxNo")] - [InlineData("HomeLocation($select=Street,TaxNo)")] - public void QueryEntityWithSelectOnSubPrimitivePropertyOfComplexProperty(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$select=" + select; + #region SubProperty on Single ComplexProperty + [Theory] + [InlineData("HomeLocation/Street,HomeLocation/TaxNo")] + [InlineData("HomeLocation($select=Street,TaxNo)")] + public void QueryEntityWithSelectOnSubPrimitivePropertyOfComplexProperty(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$select=" + select; - string value = "\"HomeLocation\":{\"Street\":\"110th\",\"TaxNo\":19}"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Street,HomeLocation/TaxNo)/$entity\"," + value + "}"; + string value = "\"HomeLocation\":{\"Street\":\"110th\",\"TaxNo\":19}"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Street,HomeLocation/TaxNo)/$entity\"," + value + "}"; - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - [Theory] - [InlineData("HomeLocation/Emails")] - [InlineData("HomeLocation($select=Emails)")] - public void QueryEntityWithSelectOnSubCollectionPrimitivePropertyOfComplexTypeProperty(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$select=" + select; + [Theory] + [InlineData("HomeLocation/Emails")] + [InlineData("HomeLocation($select=Emails)")] + public void QueryEntityWithSelectOnSubCollectionPrimitivePropertyOfComplexTypeProperty(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$select=" + select; - string value = "\"HomeLocation\":{\"Emails\":[\"E1\",\"E3\",\"E2\"]}"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Emails)/$entity\"," + value + "}"; + string value = "\"HomeLocation\":{\"Emails\":[\"E1\",\"E3\",\"E2\"]}"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Emails)/$entity\"," + value + "}"; - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - [Theory] - [InlineData("HomeLocation/RelatedInfo,HomeLocation/AdditionInfos")] - [InlineData("HomeLocation($select=RelatedInfo,AdditionInfos)")] - public void QueryEntityWithSelectOnSubComplexPropertyOfComplexProperty(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$select=" + select; + [Theory] + [InlineData("HomeLocation/RelatedInfo,HomeLocation/AdditionInfos")] + [InlineData("HomeLocation($select=RelatedInfo,AdditionInfos)")] + public void QueryEntityWithSelectOnSubComplexPropertyOfComplexProperty(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$select=" + select; + + string value = "\"HomeLocation\":{\"RelatedInfo\":{\"AreaSize\":101,\"CountyName\":\"King\"}," + + "\"AdditionInfos\":[" + + "{\"AreaSize\":102,\"CountyName\":\"King1\"}," + + "{\"AreaSize\":103,\"CountyName\":\"King2\"}," + + "{\"AreaSize\":104,\"CountyName\":\"King3\"}" + + "]}"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/RelatedInfo,HomeLocation/AdditionInfos)/$entity\"," + value + "}"; + + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - string value = "\"HomeLocation\":{\"RelatedInfo\":{\"AreaSize\":101,\"CountyName\":\"King\"}," + - "\"AdditionInfos\":[" + - "{\"AreaSize\":102,\"CountyName\":\"King1\"}," + - "{\"AreaSize\":103,\"CountyName\":\"King2\"}," + - "{\"AreaSize\":104,\"CountyName\":\"King3\"}" + - "]}"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/RelatedInfo,HomeLocation/AdditionInfos)/$entity\"," + value + "}"; - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + [Theory] + [InlineData("HomeLocation/ZipCode,HomeLocation/Street")] + [InlineData("HomeLocation($select=ZipCode,Street)")] + public void QueryEntityWithSelectOnSubNavigationPropertyOfComplexTypeProperty(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$select=" + select + "&$format=application/json;odata.metadata=full"; + + string value = "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Person\"," + + "\"@odata.id\":\"BASE_ADDRESS/odata/People(1)\"," + + "\"@odata.editLink\":\"People(1)\"," + + "\"HomeLocation\":{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Address\"," + + "\"Street\":\"110th\"," + + "\"ZipCode@odata.associationLink\":\"BASE_ADDRESS/odata/People(1)/HomeLocation/ZipCode/$ref\"," + + "\"ZipCode@odata.navigationLink\":\"BASE_ADDRESS/odata/People(1)/HomeLocation/ZipCode\"" + + "}"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/ZipCode,HomeLocation/Street)/$entity\"," + value + "}"; + + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - [Theory] - [InlineData("HomeLocation/ZipCode,HomeLocation/Street")] - [InlineData("HomeLocation($select=ZipCode,Street)")] - public void QueryEntityWithSelectOnSubNavigationPropertyOfComplexTypeProperty(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$select=" + select + "&$format=application/json;odata.metadata=full"; - - string value = "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Person\"," + - "\"@odata.id\":\"BASE_ADDRESS/odata/People(1)\"," + - "\"@odata.editLink\":\"People(1)\"," + - "\"HomeLocation\":{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Address\"," + - "\"Street\":\"110th\"," + - "\"ZipCode@odata.associationLink\":\"BASE_ADDRESS/odata/People(1)/HomeLocation/ZipCode/$ref\"," + - "\"ZipCode@odata.navigationLink\":\"BASE_ADDRESS/odata/People(1)/HomeLocation/ZipCode\"" + - "}"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/ZipCode,HomeLocation/Street)/$entity\"," + value + "}"; - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + [Theory] + [InlineData("HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude,HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Longitude")] + [InlineData("HomeLocation($select=Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude,Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Longitude)")] + public void QueryEntityWithSelectOnDerivedSubPropertyOfComplexTypeProperty(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(2)?$select=" + select; - [Theory] - [InlineData("HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude,HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Longitude")] - [InlineData("HomeLocation($select=Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude,Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Longitude)")] - public void QueryEntityWithSelectOnDerivedSubPropertyOfComplexTypeProperty(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(2)?$select=" + select; + string value = "\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Latitude\":\"12.211\",\"Longitude\":\"231.131\"}"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude,HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Longitude)/$entity\"," + value + "}"; - string value = "\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Latitude\":\"12.211\",\"Longitude\":\"231.131\"}"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude,HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Longitude)/$entity\"," + value + "}"; + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + [Theory] + [InlineData("OrderInfo/DynamicAddress,OrderInfo/DynamicInt")] + [InlineData("OrderInfo($select=DynamicAddress,DynamicInt)")] + public void QueryEntityWithSelectOnSubDynamicPropertyOfComplexTypeProperty(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(5)?$select=" + select; + + string value = "\"OrderInfo\":{" + + "\"DynamicInt\":9," + + "\"DynamicAddress\":{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Address\"," + + "\"Street\":\"\"," + + "\"TaxNo\":0," + + "\"Emails\":[\"abc@1.com\",\"xyz@2.com\"]," + + "\"RelatedInfo\":null," + + "\"AdditionInfos\":[]" + + "}}"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(OrderInfo/DynamicAddress,OrderInfo/DynamicInt)/$entity\"," + value + "}"; + + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } + #endregion - [Theory] - [InlineData("OrderInfo/DynamicAddress,OrderInfo/DynamicInt")] - [InlineData("OrderInfo($select=DynamicAddress,DynamicInt)")] - public void QueryEntityWithSelectOnSubDynamicPropertyOfComplexTypeProperty(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(5)?$select=" + select; - - string value = "\"OrderInfo\":{" + - "\"DynamicInt\":9," + - "\"DynamicAddress\":{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Address\"," + - "\"Street\":\"\"," + - "\"TaxNo\":0," + - "\"Emails\":[\"abc@1.com\",\"xyz@2.com\"]," + - "\"RelatedInfo\":null," + - "\"AdditionInfos\":[]" + - "}}"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(OrderInfo/DynamicAddress,OrderInfo/DynamicInt)/$entity\"," + value + "}"; - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } - #endregion + #region SubProperty On Collection ComplexProperty + [Theory] + [InlineData("RepoLocations/Street,RepoLocations/TaxNo,RepoLocations/RelatedInfo")] + [InlineData("RepoLocations($select=Street,TaxNo,RelatedInfo)")] + public void QueryEntityWithSelectOnSubPropertyOfCollectionComplexProperty(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$select=" + select; + + string value = "\"RepoLocations\":[" + + "{\"Street\":\"110th\",\"TaxNo\":19,\"RelatedInfo\":{\"AreaSize\":101,\"CountyName\":\"King\"}}," + + "{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Street\":\"120th\",\"TaxNo\":17,\"RelatedInfo\":null}," + + "{\"Street\":\"130th\",\"TaxNo\":18,\"RelatedInfo\":{\"AreaSize\":201,\"CountyName\":\"Queue\"}}" + + "]"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/Street,RepoLocations/TaxNo,RepoLocations/RelatedInfo)/$entity\"," + value + "}"; + + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - #region SubProperty On Collection ComplexProperty - [Theory] - [InlineData("RepoLocations/Street,RepoLocations/TaxNo,RepoLocations/RelatedInfo")] - [InlineData("RepoLocations($select=Street,TaxNo,RelatedInfo)")] - public void QueryEntityWithSelectOnSubPropertyOfCollectionComplexProperty(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$select=" + select; - - string value = "\"RepoLocations\":[" + - "{\"Street\":\"110th\",\"TaxNo\":19,\"RelatedInfo\":{\"AreaSize\":101,\"CountyName\":\"King\"}}," + - "{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Street\":\"120th\",\"TaxNo\":17,\"RelatedInfo\":null}," + - "{\"Street\":\"130th\",\"TaxNo\":18,\"RelatedInfo\":{\"AreaSize\":201,\"CountyName\":\"Queue\"}}" + - "]"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/Street,RepoLocations/TaxNo,RepoLocations/RelatedInfo)/$entity\"," + value + "}"; - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + [Theory] + [InlineData("RepoLocations/Emails")] + [InlineData("RepoLocations($select=Emails)")] + public void QueryEntityWithSelectOnSubCollectionPrimitivePropertyOfCollectionComplexProperty(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(4)?$select=" + select; - [Theory] - [InlineData("RepoLocations/Emails")] - [InlineData("RepoLocations($select=Emails)")] - public void QueryEntityWithSelectOnSubCollectionPrimitivePropertyOfCollectionComplexProperty(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(4)?$select=" + select; + string value = "\"RepoLocations\":[" + + "{\"Emails\":[\"E1\",\"E3\",\"E2\"]}," + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\"," + + "\"Emails\":[\"E7\",\"E4\",\"E5\"]" + + "}" + + "]"; - string value = "\"RepoLocations\":[" + - "{\"Emails\":[\"E1\",\"E3\",\"E2\"]}," + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\"," + - "\"Emails\":[\"E7\",\"E4\",\"E5\"]" + - "}" + - "]"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/Emails)/$entity\"," + value + "}"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/Emails)/$entity\"," + value + "}"; + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + [Theory] + [InlineData("RepoLocations/RelatedInfo,RepoLocations/AdditionInfos")] + [InlineData("RepoLocations($select=RelatedInfo,AdditionInfos)")] + public void QueryEntityWithSelectOnSubComplexPropertyOfCollectionComplexProperty(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(5)?$select=" + select; - [Theory] - [InlineData("RepoLocations/RelatedInfo,RepoLocations/AdditionInfos")] - [InlineData("RepoLocations($select=RelatedInfo,AdditionInfos)")] - public void QueryEntityWithSelectOnSubComplexPropertyOfCollectionComplexProperty(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(5)?$select=" + select; + string value = "\"RepoLocations\":[{\"RelatedInfo\":{\"AreaSize\":101,\"CountyName\":\"King\"}," + + "\"AdditionInfos\":[" + + "{\"AreaSize\":102,\"CountyName\":\"King1\"}," + + "{\"AreaSize\":103,\"CountyName\":\"King2\"}," + + "{\"AreaSize\":104,\"CountyName\":\"King3\"}" + + "]}]"; - string value = "\"RepoLocations\":[{\"RelatedInfo\":{\"AreaSize\":101,\"CountyName\":\"King\"}," + - "\"AdditionInfos\":[" + - "{\"AreaSize\":102,\"CountyName\":\"King1\"}," + - "{\"AreaSize\":103,\"CountyName\":\"King2\"}," + - "{\"AreaSize\":104,\"CountyName\":\"King3\"}" + - "]}]"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/RelatedInfo,RepoLocations/AdditionInfos)/$entity\"," + value + "}"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/RelatedInfo,RepoLocations/AdditionInfos)/$entity\"," + value + "}"; + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + [Theory] + [InlineData("RepoLocations/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude")] + [InlineData("RepoLocations($select=Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude)")] + public void QueryEntityWithSelectOnDerivedSubPropertyOfCollectionComplexTypeProperty(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(4)?$select=" + select; - [Theory] - [InlineData("RepoLocations/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude")] - [InlineData("RepoLocations($select=Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude)")] - public void QueryEntityWithSelectOnDerivedSubPropertyOfCollectionComplexTypeProperty(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(4)?$select=" + select; + string value = "\"RepoLocations\":[" + + "{}," + // Be noted, this is correct. + "{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Latitude\":\"12.8\"}" + + "]"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude)/$entity\"," + value + "}"; - string value = "\"RepoLocations\":[" + - "{}," + // Be noted, this is correct. - "{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Latitude\":\"12.8\"}" + - "]"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude)/$entity\"," + value + "}"; + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } + #endregion + + #region Nested query option on select + [Theory] + [InlineData("Taxes", "\"Taxes\":[7,5,9]")] + // [InlineData("Taxes($filter=$this eq 5)", "\"Taxes\":[5]")] "TODO: enable these three cases when ODL supports $this + // [InlineData("Taxes($filter=$this le 8)", "\"Taxes\":[7,5]")] + public void QueryEntityWithSelectOnCollectionPrimitivePropertyWithNestedFilter(string select, string value) + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$select=" + select; + string equals = string.Format("{{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Taxes)/$entity\",{0}}}", value); - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } - #endregion - - #region Nested query option on select - [Theory] - [InlineData("Taxes", "\"Taxes\":[7,5,9]")] - // [InlineData("Taxes($filter=$this eq 5)", "\"Taxes\":[5]")] "TODO: enable these three cases when ODL supports $this - // [InlineData("Taxes($filter=$this le 8)", "\"Taxes\":[7,5]")] - public void QueryEntityWithSelectOnCollectionPrimitivePropertyWithNestedFilter(string select, string value) - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$select=" + select; - string equals = string.Format("{{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Taxes)/$entity\",{0}}}", value); + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + [Theory(Skip = "TODO: enable these three cases when ODL supports $this")] + [InlineData("Taxes($orderby=$this)", "\"Taxes\":[5,7,9]")] + [InlineData("Taxes($orderby =$this desc)", "\"Taxes\":[9,7,5]")] + public void QueryEntityWithSelectOnCollectionPrimitivePropertyWithNestedOrderby(string select, string value) + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$select=" + select; + string equals = string.Format("{{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Taxes)/$entity\",{0}}}", value); - [Theory(Skip = "TODO: enable these three cases when ODL supports $this")] - [InlineData("Taxes($orderby=$this)", "\"Taxes\":[5,7,9]")] - [InlineData("Taxes($orderby =$this desc)", "\"Taxes\":[9,7,5]")] - public void QueryEntityWithSelectOnCollectionPrimitivePropertyWithNestedOrderby(string select, string value) - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$select=" + select; - string equals = string.Format("{{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Taxes)/$entity\",{0}}}", value); + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + [Theory] + [InlineData("Taxes($top=1;$skip=1)", "\"Taxes\":[5]")] + [InlineData("Taxes($top=2)", "\"Taxes\":[7,5]")] + [InlineData("Taxes($top=2;$skip=1)", "\"Taxes\":[5,9]")] + public void QueryEntityWithSelectOnCollectionPrimitivePropertyWithNestedTopAndSkip(string select, string value) + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$select=" + select; + string equals = string.Format("{{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Taxes)/$entity\",{0}}}", value); - [Theory] - [InlineData("Taxes($top=1;$skip=1)", "\"Taxes\":[5]")] - [InlineData("Taxes($top=2)", "\"Taxes\":[7,5]")] - [InlineData("Taxes($top=2;$skip=1)", "\"Taxes\":[5,9]")] - public void QueryEntityWithSelectOnCollectionPrimitivePropertyWithNestedTopAndSkip(string select, string value) - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$select=" + select; - string equals = string.Format("{{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Taxes)/$entity\",{0}}}", value); + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + [Theory(Skip = "TODO: enable these three cases when ODL supports $this")] + [InlineData("HomeLocation/Emails($filter=$this eq 'E3')")] + [InlineData("HomeLocation($select=Emails($filter=$this eq 'E3'))")] + public void QueryEntityWithSelectOnSubCollectionPrimitivePropertyOfComplexTypePropertyWithNestedFilter(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$select=" + select; - [Theory(Skip = "TODO: enable these three cases when ODL supports $this")] - [InlineData("HomeLocation/Emails($filter=$this eq 'E3')")] - [InlineData("HomeLocation($select=Emails($filter=$this eq 'E3'))")] - public void QueryEntityWithSelectOnSubCollectionPrimitivePropertyOfComplexTypePropertyWithNestedFilter(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$select=" + select; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Emails)/$entity\"," + + "\"HomeLocation\":{\"Emails\":[\"E3\"]}}"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Emails)/$entity\"," + - "\"HomeLocation\":{\"Emails\":[\"E3\"]}}"; + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + [Theory(Skip = "TODO: enable these three cases when ODL supports $this")] + [InlineData("HomeLocation/Emails($orderby=$this)")] + [InlineData("HomeLocation($select=Emails($orderby=$this desc))")] + public void QueryEntityWithSelectOnCollectionPrimitivePropertyOfComplexPropertyWithNestedOrderby(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(5)?$select=" + select; - [Theory(Skip = "TODO: enable these three cases when ODL supports $this")] - [InlineData("HomeLocation/Emails($orderby=$this)")] - [InlineData("HomeLocation($select=Emails($orderby=$this desc))")] - public void QueryEntityWithSelectOnCollectionPrimitivePropertyOfComplexPropertyWithNestedOrderby(string select) + string equals; + if (select.Contains("$select=")) { - // Arrange - string requestUri = PeopleBaseUrl + "(5)?$select=" + select; - - string equals; - if (select.Contains("$select=")) - { - equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Emails)/$entity\"," + - "\"HomeLocation\":{\"Emails\":[\"E9\",\"E8\",\"E6\"]}}"; - } - else - { - equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Emails)/$entity\"," + - "\"HomeLocation\":{\"Emails\":[\"E6\",\"E8\",\"E9\"]}}"; - } - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); + equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Emails)/$entity\"," + + "\"HomeLocation\":{\"Emails\":[\"E9\",\"E8\",\"E6\"]}}"; } - - [Theory] - [InlineData("HomeLocation/Emails($top=1;$skip=1)", "\"Emails\":[\"E6\"]")] - [InlineData("HomeLocation/Emails($top=2)", "\"Emails\":[\"E9\",\"E6\"]")] - [InlineData("HomeLocation/Emails($top=2;$skip=1)", "\"Emails\":[\"E6\",\"E8\"]")] - public void QueryEntityWithSelectOnCollectionPrimitivePropertyOfComplexPropertyWithNestedTopAndSkip(string select, string value) + else { - // Arrange - string requestUri = PeopleBaseUrl + "(5)?$select=" + select; - string equals = string.Format("{{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Emails)/$entity\",\"HomeLocation\":{{{0}}}}}", value); - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); + equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Emails)/$entity\"," + + "\"HomeLocation\":{\"Emails\":[\"E6\",\"E8\",\"E9\"]}}"; } - [Theory(Skip = "TODO: enable these three cases when ODL supports $this")] - [InlineData("RepoLocations/Emails($filter=$this eq 'E3')")] - [InlineData("RepoLocations($select=Emails($filter=$this eq 'E3'))")] - public void QueryEntityWithSelectOnSubCollectionPrimitivePropertyOfCollectionComplexTypePropertyWithFilter(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$select=" + select; - - string value = "\"RepoLocations\":[" + - "{\"Emails\":[\"E3\"]}," + - "{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Emails\":[]}," + - "{\"Emails\":[]}" + - "]"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/Emails)/$entity\"," + value + "}"; - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - [Theory(Skip = "TODO: enable these three cases when ODL supports $this")] - [InlineData("RepoLocations/Emails($orderby=$this;$top=1;$skip=2)")] - [InlineData("RepoLocations($select=Emails($orderby=$this;$top=1;$skip=2))")] - public void QueryEntityWithSelectOnSubCollectionPrimitivePropertyOfCollectionComplexTypePropertyWithOrderByTopSkip(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$select=" + select; - - string value = "\"RepoLocations\":[" + - "{\"Emails\":[\"E3\"]}," + - "{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Emails\":[\"E7\"]}," + - "{\"Emails\":[\"E9\"]}" + - "]"; - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/Emails)/$entity\"," + value + "}"; - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + [Theory] + [InlineData("HomeLocation/Emails($top=1;$skip=1)", "\"Emails\":[\"E6\"]")] + [InlineData("HomeLocation/Emails($top=2)", "\"Emails\":[\"E9\",\"E6\"]")] + [InlineData("HomeLocation/Emails($top=2;$skip=1)", "\"Emails\":[\"E6\",\"E8\"]")] + public void QueryEntityWithSelectOnCollectionPrimitivePropertyOfComplexPropertyWithNestedTopAndSkip(string select, string value) + { + // Arrange + string requestUri = PeopleBaseUrl + "(5)?$select=" + select; + string equals = string.Format("{{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Emails)/$entity\",\"HomeLocation\":{{{0}}}}}", value); - [Theory] - [InlineData("RepoLocations($filter=TaxNo eq 17;$select=AdditionInfos($filter=AreaSize eq 102))")] - public void QueryEntityWithSelectOnSubCollectionComplexPropertyOfCollectionComplexTypePropertyWithNestedFilter(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "(1)?$select=" + select; - - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/AdditionInfos)/$entity\"," + - "\"RepoLocations\":[" + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\"," + - "\"AdditionInfos\":[" + - "{\"AreaSize\":102,\"CountyName\":\"King1\"}" + - "]" + - "}" + - "]}"; - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } - #endregion + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - [Theory] - [InlineData("HomeLocation/Street")] - [InlineData("HomeLocation($select=Street)")] - public void QueryEntitySetWithSelectOnSubPropertyOfComplexTypeProperty(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "?$select=" + select; + [Theory(Skip = "TODO: enable these three cases when ODL supports $this")] + [InlineData("RepoLocations/Emails($filter=$this eq 'E3')")] + [InlineData("RepoLocations($select=Emails($filter=$this eq 'E3'))")] + public void QueryEntityWithSelectOnSubCollectionPrimitivePropertyOfCollectionComplexTypePropertyWithFilter(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$select=" + select; + + string value = "\"RepoLocations\":[" + + "{\"Emails\":[\"E3\"]}," + + "{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Emails\":[]}," + + "{\"Emails\":[]}" + + "]"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/Emails)/$entity\"," + value + "}"; + + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - string value = "\"value\":[" + - "{\"HomeLocation\":{\"Street\":\"110th\"}}," + - "{\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Street\":\"110th\"}}," + - "{\"HomeLocation\":{\"Street\":null}}," + - "{\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Street\":\"120th\"}}," + - "{\"HomeLocation\":{\"Street\":\"130th\"}}," + - "{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson\",\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeometryLocation\",\"Street\":\"130th\"}}]"; + [Theory(Skip = "TODO: enable these three cases when ODL supports $this")] + [InlineData("RepoLocations/Emails($orderby=$this;$top=1;$skip=2)")] + [InlineData("RepoLocations($select=Emails($orderby=$this;$top=1;$skip=2))")] + public void QueryEntityWithSelectOnSubCollectionPrimitivePropertyOfCollectionComplexTypePropertyWithOrderByTopSkip(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$select=" + select; + + string value = "\"RepoLocations\":[" + + "{\"Emails\":[\"E3\"]}," + + "{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Emails\":[\"E7\"]}," + + "{\"Emails\":[\"E9\"]}" + + "]"; + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/Emails)/$entity\"," + value + "}"; + + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Street)\"," + value + "}"; + [Theory] + [InlineData("RepoLocations($filter=TaxNo eq 17;$select=AdditionInfos($filter=AreaSize eq 102))")] + public void QueryEntityWithSelectOnSubCollectionComplexPropertyOfCollectionComplexTypePropertyWithNestedFilter(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "(1)?$select=" + select; - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(RepoLocations/AdditionInfos)/$entity\"," + + "\"RepoLocations\":[" + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\"," + + "\"AdditionInfos\":[" + + "{\"AreaSize\":102,\"CountyName\":\"King1\"}" + + "]" + + "}" + + "]}"; - [Fact] - public void QueryEntitySetWithSelectOnDerivedProperty() - { - // Arrange - string requestUri = PeopleBaseUrl + - $"?$select=Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson/Bonus"; - - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson/Bonus)\"," + - "\"value\":[" + - "{}," + - "{}," + - "{}," + - "{}," + - "{}," + - "{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson\",\"Bonus\":99}" + - "]}"; - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } + #endregion - [Fact] - public void QueryEntitySetWithSelectOnDerivedPropertyWithFullMetadata() - { - // Arrange - string requestUri = PeopleBaseUrl + - "?$select=Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson/Bonus&$format=application/json;odata.metadata=full"; - - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson/Bonus)\"," + - "\"value\":[" + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Person\"," + - "\"@odata.id\":\"BASE_ADDRESS/odata/People(1)\"," + - "\"@odata.editLink\":\"People(1)\"" + - "}," + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Person\"," + - "\"@odata.id\":\"BASE_ADDRESS/odata/People(2)\"," + - "\"@odata.editLink\":\"People(2)\"" + - "}," + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Person\"," + - "\"@odata.id\":\"BASE_ADDRESS/odata/People(3)\"," + - "\"@odata.editLink\":\"People(3)\"" + - "}," + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Person\"," + - "\"@odata.id\":\"BASE_ADDRESS/odata/People(4)\"," + - "\"@odata.editLink\":\"People(4)\"" + - "}," + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Person\"," + - "\"@odata.id\":\"BASE_ADDRESS/odata/People(5)\"," + - "\"@odata.editLink\":\"People(5)\"" + - "}," + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson\"," + - "\"@odata.id\":\"BASE_ADDRESS/odata/People(6)\"," + - "\"@odata.editLink\":\"People(6)/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson\"," + - "\"Bonus\":99" + - "}" + - "]}"; - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + [Theory] + [InlineData("HomeLocation/Street")] + [InlineData("HomeLocation($select=Street)")] + public void QueryEntitySetWithSelectOnSubPropertyOfComplexTypeProperty(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "?$select=" + select; - [Theory] - [InlineData("HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude,HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeometryLocation/Latitude")] - [InlineData("HomeLocation($select=Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude,Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeometryLocation/Latitude)")] - public void QueryEntitySetWithSelectOnSameNameDerivedPropertyOfComplexPropertyWithTypeCast(string select) - { - // Arrange - string requestUri = PeopleBaseUrl + "?$select=" + select; - - string value = "\"value\":[" + - "{" + - "\"HomeLocation\":{}" + - "}," + - "{" + - "\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Latitude\":\"12.211\"}" + - "}," + - "{" + - "\"HomeLocation\":{}" + - "}," + - "{" + - "\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Latitude\":\"12.8\"}" + - "}," + - "{" + - "\"HomeLocation\":{}" + - "}," + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson\"," + - "\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeometryLocation\",\"Latitude\":\"101.1\"}" + - "}" + - "]"; - - string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude,HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeometryLocation/Latitude)\"," + value + "}"; - - // Act & Assert - ExecuteAndVerifyQueryRequest(requestUri, equals); - } + string value = "\"value\":[" + + "{\"HomeLocation\":{\"Street\":\"110th\"}}," + + "{\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Street\":\"110th\"}}," + + "{\"HomeLocation\":{\"Street\":null}}," + + "{\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Street\":\"120th\"}}," + + "{\"HomeLocation\":{\"Street\":\"130th\"}}," + + "{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson\",\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeometryLocation\",\"Street\":\"130th\"}}]"; - private string ExecuteAndVerifyQueryRequest(string requestUri, string equals) - { - // Arrange - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Street)\"," + value + "}"; + + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - // Act - HttpResponseMessage response = this.Client.SendAsync(request).Result; + [Fact] + public void QueryEntitySetWithSelectOnDerivedProperty() + { + // Arrange + string requestUri = PeopleBaseUrl + + $"?$select=Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson/Bonus"; + + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson/Bonus)\"," + + "\"value\":[" + + "{}," + + "{}," + + "{}," + + "{}," + + "{}," + + "{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson\",\"Bonus\":99}" + + "]}"; + + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - // Assert - string result = response.Content.ReadAsStringAsync().Result; + [Fact] + public void QueryEntitySetWithSelectOnDerivedPropertyWithFullMetadata() + { + // Arrange + string requestUri = PeopleBaseUrl + + "?$select=Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson/Bonus&$format=application/json;odata.metadata=full"; + + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson/Bonus)\"," + + "\"value\":[" + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Person\"," + + "\"@odata.id\":\"BASE_ADDRESS/odata/People(1)\"," + + "\"@odata.editLink\":\"People(1)\"" + + "}," + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Person\"," + + "\"@odata.id\":\"BASE_ADDRESS/odata/People(2)\"," + + "\"@odata.editLink\":\"People(2)\"" + + "}," + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Person\"," + + "\"@odata.id\":\"BASE_ADDRESS/odata/People(3)\"," + + "\"@odata.editLink\":\"People(3)\"" + + "}," + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Person\"," + + "\"@odata.id\":\"BASE_ADDRESS/odata/People(4)\"," + + "\"@odata.editLink\":\"People(4)\"" + + "}," + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.Person\"," + + "\"@odata.id\":\"BASE_ADDRESS/odata/People(5)\"," + + "\"@odata.editLink\":\"People(5)\"" + + "}," + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson\"," + + "\"@odata.id\":\"BASE_ADDRESS/odata/People(6)\"," + + "\"@odata.editLink\":\"People(6)/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson\"," + + "\"Bonus\":99" + + "}" + + "]}"; + + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - // replace the real address using "BASE_ADDRESS" - string odataContext = "\"@odata.context\":\""; - int start = result.IndexOf(odataContext) + odataContext.Length; - int end = result.IndexOf("/odata/$metadata"); - string uri = result.Substring(start, end - start); - result = result.Replace(uri, "BASE_ADDRESS"); + [Theory] + [InlineData("HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude,HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeometryLocation/Latitude")] + [InlineData("HomeLocation($select=Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude,Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeometryLocation/Latitude)")] + public void QueryEntitySetWithSelectOnSameNameDerivedPropertyOfComplexPropertyWithTypeCast(string select) + { + // Arrange + string requestUri = PeopleBaseUrl + "?$select=" + select; + + string value = "\"value\":[" + + "{" + + "\"HomeLocation\":{}" + + "}," + + "{" + + "\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Latitude\":\"12.211\"}" + + "}," + + "{" + + "\"HomeLocation\":{}" + + "}," + + "{" + + "\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation\",\"Latitude\":\"12.8\"}" + + "}," + + "{" + + "\"HomeLocation\":{}" + + "}," + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.VipPerson\"," + + "\"HomeLocation\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeometryLocation\",\"Latitude\":\"101.1\"}" + + "}" + + "]"; + + string equals = "{\"@odata.context\":\"BASE_ADDRESS/odata/$metadata#People(HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeoLocation/Latitude,HomeLocation/Microsoft.AspNetCore.OData.E2E.Tests.NavigationPropertyOnComplexType.GeometryLocation/Latitude)\"," + value + "}"; + + // Act & Assert + ExecuteAndVerifyQueryRequest(requestUri, equals); + } - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(equals, result); + private string ExecuteAndVerifyQueryRequest(string requestUri, string equals) + { + // Arrange + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - return result; - } + // Act + HttpResponseMessage response = this.Client.SendAsync(request).Result; + + // Assert + string result = response.Content.ReadAsStringAsync().Result; + + // replace the real address using "BASE_ADDRESS" + string odataContext = "\"@odata.context\":\""; + int start = result.IndexOf(odataContext) + odataContext.Length; + int end = result.IndexOf("/odata/$metadata"); + string uri = result.Substring(start, end - start); + result = result.Replace(uri, "BASE_ADDRESS"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(equals, result); + + return result; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsController.cs index 69d8b4ee7..3750848c4 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsController.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -11,105 +11,104 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors +namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors; + +public class CustomersController : ODataController { - public class CustomersController : ODataController + [EnableQuery] + public IActionResult Get() { - [EnableQuery] - public IActionResult Get() - { - return Unauthorized("Not authorized to access this resource."); - } + return Unauthorized("Not authorized to access this resource."); + } - [EnableQuery] - public IActionResult Get(int key) - { - return NotFound($"Customer with key: {key} not found."); - } + [EnableQuery] + public IActionResult Get(int key) + { + return NotFound($"Customer with key: {key} not found."); + } - public IActionResult Post([FromBody] Customer customer) - { - return UnprocessableEntity("Unprocessable customer object."); - } + public IActionResult Post([FromBody] Customer customer) + { + return UnprocessableEntity("Unprocessable customer object."); + } - public IActionResult Patch([FromODataUri] int key,[FromBody] Customer customer) - { - return Conflict("Conflict during update."); - } + public IActionResult Patch([FromODataUri] int key,[FromBody] Customer customer) + { + return Conflict("Conflict during update."); + } - public IActionResult Put([FromODataUri] int key, [FromBody] Customer customer) - { - return ODataErrorResult("400", "Bad request during PUT."); - } + public IActionResult Put([FromODataUri] int key, [FromBody] Customer customer) + { + return ODataErrorResult("400", "Bad request during PUT."); + } - public IActionResult Delete(int key) - { - return BadRequest("Bad request on delete."); - } + public IActionResult Delete(int key) + { + return BadRequest("Bad request on delete."); } +} - public class OrdersController : ODataController +public class OrdersController : ODataController +{ + [EnableQuery] + public IActionResult Get() { - [EnableQuery] - public IActionResult Get() + ODataError odataError = new ODataError() { - ODataError odataError = new ODataError() - { - Code = "401", - Message = "Not authorized to access this resource." - }; - return Unauthorized(odataError); - } + Code = "401", + Message = "Not authorized to access this resource." + }; + return Unauthorized(odataError); + } - [EnableQuery] - public IActionResult Get(int key) + [EnableQuery] + public IActionResult Get(int key) + { + ODataError odataError = new ODataError() { - ODataError odataError = new ODataError() - { - Code = "404", - Message = $"Order with key: {key} not found." - }; - return NotFound(odataError); - } + Code = "404", + Message = $"Order with key: {key} not found." + }; + return NotFound(odataError); + } - public IActionResult Post([FromBody] Order order) + public IActionResult Post([FromBody] Order order) + { + ODataError odataError = new ODataError() { - ODataError odataError = new ODataError() - { - Code = "422", - Message = "Unprocessable order object." - }; - return UnprocessableEntity(odataError); - } + Code = "422", + Message = "Unprocessable order object." + }; + return UnprocessableEntity(odataError); + } - public IActionResult Patch([FromODataUri] int key, [FromBody] Order order) + public IActionResult Patch([FromODataUri] int key, [FromBody] Order order) + { + ODataError odataError = new ODataError() { - ODataError odataError = new ODataError() - { - Code = "409", - Message = "Conflict during update." - }; - return Conflict(odataError); - } + Code = "409", + Message = "Conflict during update." + }; + return Conflict(odataError); + } - public IActionResult Put([FromODataUri] int key, [FromBody] Order order) + public IActionResult Put([FromODataUri] int key, [FromBody] Order order) + { + ODataError odataError = new ODataError() { - ODataError odataError = new ODataError() - { - Code = "400", - Message = "Bad request during PUT." - }; - return ODataErrorResult(odataError); - } + Code = "400", + Message = "Bad request during PUT." + }; + return ODataErrorResult(odataError); + } - public IActionResult Delete(int key) + public IActionResult Delete(int key) + { + ODataError odataError = new ODataError() { - ODataError odataError = new ODataError() - { - Code = "400", - Message = "Bad request on delete." - }; - return BadRequest(odataError); - } + Code = "400", + Message = "Bad request on delete." + }; + return BadRequest(odataError); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsDataModel.cs index 505db26f1..9ffda8429 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsDataModel.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -7,21 +7,20 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors +namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors; + +public class Customer { - public class Customer - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public List Orders { get; set; } - } + public List Orders { get; set; } +} - public class Order - { - public int Id { get; set; } +public class Order +{ + public int Id { get; set; } - public string Name { get; set; } - } + public string Name { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsEdmModel.cs index 1eeceafdd..6c2dc772d 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsEdmModel.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -8,17 +8,16 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors +namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors; + +public class ODataErrorsEdmModel { - public class ODataErrorsEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Orders"); - IEdmModel model = builder.GetEdmModel(); - return model; - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); + IEdmModel model = builder.GetEdmModel(); + return model; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsTests.cs index df278e8bf..6b3e3b839 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataErrors/ODataErrorsTests.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -15,184 +15,183 @@ using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors +namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors; + +public class ODataErrorsTests : WebApiTestBase { - public class ODataErrorsTests : WebApiTestBase + private readonly ITestOutputHelper output; + + public ODataErrorsTests(WebApiTestFixture fixture, ITestOutputHelper output) + : base(fixture) + { + this.output = output; + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = ODataErrorsEdmModel.GetEdmModel(); + + services.ConfigureControllers(typeof(CustomersController), + typeof(OrdersController)); + + services.AddControllers().AddOData(opt => + opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select().AddRouteComponents("odataerrors", edmModel)); + } + + [Theory] + [InlineData("odataerrors/Customers(1)", "{\"error\":{\"code\":\"404\",\"message\":\"Customer with key: 1 not found.\"}}")] + [InlineData("odataerrors/Orders(1)", "{\"error\":{\"code\":\"404\",\"message\":\"Order with key: 1 not found.\"}}")] + public async Task NotFoundResponseFromODataControllerIsSerializedAsODataError(string queryUrl, string expectedResponse) + { + // Arrange + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + using HttpClient client = CreateClient(); + + // Act + using HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + Assert.NotNull(response.Content); + + string payload = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResponse, payload); + } + + [Theory] + [InlineData("odataerrors/Customers")] + [InlineData("odataerrors/Orders")] + public async Task UnauthorizedResponseFromODataControllerIsSerializedAsODataError(string queryUrl) + { + // Arrange + const string expectedResponse = "{\"error\":{\"code\":\"401\",\"message\":\"Not authorized to access this resource.\"}}"; + + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + using HttpClient client = CreateClient(); + + // Act + using HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.NotNull(response.Content); + + string payload = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResponse, payload); + } + + [Theory] + [InlineData("odataerrors/Customers(1000)", @"{{'@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors.Customer', + 'ID':1000,'Name':'Customer 1000,}}")] + [InlineData("odataerrors/Orders(1000)", @"{{'@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors.Order', + 'ID':1000,'Name':'Order 1000,}}")] + public async Task ConflictResponseFromODataControllerIsSerializedAsODataError(string queryUrl, string requestContent) + { + // Arrange + const string expectedResponse = "{\"error\":{\"code\":\"409\",\"message\":\"Conflict during update.\"}}"; + + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Patch, queryUrl); + using var content = new StringContent(requestContent); + request.Content = content; + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + using HttpClient client = CreateClient(); + + // Act + using HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.Conflict, response.StatusCode); + Assert.NotNull(response.Content); + + string payload = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResponse, payload); + } + + [Theory] + [InlineData("odataerrors/Customers(1)")] + [InlineData("odataerrors/Orders(1)")] + public async Task BadRequestResponseFromODataControllerIsSerializedAsODataError(string queryUrl) + { + // Arrange + const string expectedResponse = "{\"error\":{\"code\":\"400\",\"message\":\"Bad request on delete.\"}}"; + + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + using HttpClient client = CreateClient(); + + // Act + using HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.NotNull(response.Content); + + string payload = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResponse, payload); + } + + [Theory] + [InlineData( + "odataerrors/Customers", + @"{{'@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors.Customer', 'ID':1000,'Name':'Customer 1000}}", + "{\"error\":{\"code\":\"422\",\"message\":\"Unprocessable customer object.\"}}")] + [InlineData( + "odataerrors/Orders", + @"{{'@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors.Order', 'ID':1000,'Name':'Order 1000}}", + "{\"error\":{\"code\":\"422\",\"message\":\"Unprocessable order object.\"}}")] + public async Task UnprocessableEntityResponseFromODataControllerIsSerializedAsODataError(string queryUrl, string requestContent, string expectedResponse) + { + // Arrange + + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, queryUrl); + using var content = new StringContent(requestContent); + request.Content = content; + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + using HttpClient client = CreateClient(); + + // Act + using HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode); + Assert.NotNull(response.Content); + + string payload = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResponse, payload); + } + + [Theory] + [InlineData("odataerrors/Customers(1000)", @"{{'@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors.Customer', + 'ID':1000,'Name':'Customer 1000,}}")] + [InlineData("odataerrors/Orders(1000)", @"{{'@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors.Order', + 'ID':1000,'Name':'Order 1000,}}")] + public async Task ODataErrorResultResponseFromODataControllerIsSerializedAsODataError(string queryUrl, string requestContent) { - private readonly ITestOutputHelper output; - - public ODataErrorsTests(WebApiTestFixture fixture, ITestOutputHelper output) - : base(fixture) - { - this.output = output; - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = ODataErrorsEdmModel.GetEdmModel(); - - services.ConfigureControllers(typeof(CustomersController), - typeof(OrdersController)); - - services.AddControllers().AddOData(opt => - opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select().AddRouteComponents("odataerrors", edmModel)); - } - - [Theory] - [InlineData("odataerrors/Customers(1)", "{\"error\":{\"code\":\"404\",\"message\":\"Customer with key: 1 not found.\"}}")] - [InlineData("odataerrors/Orders(1)", "{\"error\":{\"code\":\"404\",\"message\":\"Order with key: 1 not found.\"}}")] - public async Task NotFoundResponseFromODataControllerIsSerializedAsODataError(string queryUrl, string expectedResponse) - { - // Arrange - using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - using HttpClient client = CreateClient(); - - // Act - using HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - Assert.NotNull(response.Content); - - string payload = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResponse, payload); - } - - [Theory] - [InlineData("odataerrors/Customers")] - [InlineData("odataerrors/Orders")] - public async Task UnauthorizedResponseFromODataControllerIsSerializedAsODataError(string queryUrl) - { - // Arrange - const string expectedResponse = "{\"error\":{\"code\":\"401\",\"message\":\"Not authorized to access this resource.\"}}"; - - using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - using HttpClient client = CreateClient(); - - // Act - using HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - Assert.NotNull(response.Content); - - string payload = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResponse, payload); - } - - [Theory] - [InlineData("odataerrors/Customers(1000)", @"{{'@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors.Customer', - 'ID':1000,'Name':'Customer 1000,}}")] - [InlineData("odataerrors/Orders(1000)", @"{{'@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors.Order', - 'ID':1000,'Name':'Order 1000,}}")] - public async Task ConflictResponseFromODataControllerIsSerializedAsODataError(string queryUrl, string requestContent) - { - // Arrange - const string expectedResponse = "{\"error\":{\"code\":\"409\",\"message\":\"Conflict during update.\"}}"; - - using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Patch, queryUrl); - using var content = new StringContent(requestContent); - request.Content = content; - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - using HttpClient client = CreateClient(); - - // Act - using HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.Conflict, response.StatusCode); - Assert.NotNull(response.Content); - - string payload = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResponse, payload); - } - - [Theory] - [InlineData("odataerrors/Customers(1)")] - [InlineData("odataerrors/Orders(1)")] - public async Task BadRequestResponseFromODataControllerIsSerializedAsODataError(string queryUrl) - { - // Arrange - const string expectedResponse = "{\"error\":{\"code\":\"400\",\"message\":\"Bad request on delete.\"}}"; - - using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - using HttpClient client = CreateClient(); - - // Act - using HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - Assert.NotNull(response.Content); - - string payload = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResponse, payload); - } - - [Theory] - [InlineData( - "odataerrors/Customers", - @"{{'@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors.Customer', 'ID':1000,'Name':'Customer 1000}}", - "{\"error\":{\"code\":\"422\",\"message\":\"Unprocessable customer object.\"}}")] - [InlineData( - "odataerrors/Orders", - @"{{'@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors.Order', 'ID':1000,'Name':'Order 1000}}", - "{\"error\":{\"code\":\"422\",\"message\":\"Unprocessable order object.\"}}")] - public async Task UnprocessableEntityResponseFromODataControllerIsSerializedAsODataError(string queryUrl, string requestContent, string expectedResponse) - { - // Arrange - - using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, queryUrl); - using var content = new StringContent(requestContent); - request.Content = content; - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - using HttpClient client = CreateClient(); - - // Act - using HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode); - Assert.NotNull(response.Content); - - string payload = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResponse, payload); - } - - [Theory] - [InlineData("odataerrors/Customers(1000)", @"{{'@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors.Customer', - 'ID':1000,'Name':'Customer 1000,}}")] - [InlineData("odataerrors/Orders(1000)", @"{{'@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.ODataErrors.Order', - 'ID':1000,'Name':'Order 1000,}}")] - public async Task ODataErrorResultResponseFromODataControllerIsSerializedAsODataError(string queryUrl, string requestContent) - { - // Arrange - const string expectedResponse = "{\"error\":{\"code\":\"400\",\"message\":\"Bad request during PUT.\"}}"; - - using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, queryUrl); - using var content = new StringContent(requestContent); - request.Content = content; - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - using HttpClient client = CreateClient(); - - // Act - using HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - Assert.NotNull(response.Content); - - string payload = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResponse, payload); - } + // Arrange + const string expectedResponse = "{\"error\":{\"code\":\"400\",\"message\":\"Bad request during PUT.\"}}"; + + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, queryUrl); + using var content = new StringContent(requestContent); + request.Content = content; + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + using HttpClient client = CreateClient(); + + // Act + using HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.NotNull(response.Content); + + string payload = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResponse, payload); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByAdvancedTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByAdvancedTest.cs index f9afdda5e..f61578035 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByAdvancedTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByAdvancedTest.cs @@ -18,235 +18,234 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataOrderByTest +namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataOrderByTest; + +public class OrderByAdvancedTest : WebApiTestBase { - public class OrderByAdvancedTest : WebApiTestBase + public OrderByAdvancedTest(WebApiTestFixture fixture) + :base(fixture) { - public OrderByAdvancedTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = GetEdmModel(); + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = GetEdmModel(); - services.ConfigureControllers(typeof(StudentsController)); + services.ConfigureControllers(typeof(StudentsController)); - services.AddControllers().AddOData(opt => - opt.Count().Filter().OrderBy().Expand().Select().AddRouteComponents("odata", edmModel)); - } + services.AddControllers().AddOData(opt => + opt.Count().Filter().OrderBy().Expand().Select().AddRouteComponents("odata", edmModel)); + } - public static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Students"); - return builder.GetEdmModel(); - } + public static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Students"); + return builder.GetEdmModel(); + } - [Theory] - [InlineData("$orderby=name", new[] { 2, 1, 6, 5, 3, 4 }, new[] { "a1", "A1", "Ab", "AB", "bb", "Bc" })] - [InlineData("$orderby=tolower(name)", new[] { 1, 2, 5, 6, 3, 4 }, new[] { "A1", "a1", "AB", "Ab", "bb", "Bc" })] - [InlineData("$orderby=toupper(name)", new[] { 1, 2, 5, 6, 3, 4 }, new[] { "A1", "a1", "AB", "Ab", "bb", "Bc" })] - public async Task DollarOrderBy_UsingAdvanced_BuiltInStringFunctions(string orderBy, int[] ids, string[] names) - { - // Arrange - string queryUrl = $"odata/students?{orderBy}&$select=id,name"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - JObject payloadBody = await response.Content.ReadAsObject(); - (int[] actualIds, string[] actualNames) = GetIds(payloadBody, "Name"); - - Assert.True(ids.SequenceEqual(actualIds)); - Assert.True(names.SequenceEqual(actualNames)); - } + [Theory] + [InlineData("$orderby=name", new[] { 2, 1, 6, 5, 3, 4 }, new[] { "a1", "A1", "Ab", "AB", "bb", "Bc" })] + [InlineData("$orderby=tolower(name)", new[] { 1, 2, 5, 6, 3, 4 }, new[] { "A1", "a1", "AB", "Ab", "bb", "Bc" })] + [InlineData("$orderby=toupper(name)", new[] { 1, 2, 5, 6, 3, 4 }, new[] { "A1", "a1", "AB", "Ab", "bb", "Bc" })] + public async Task DollarOrderBy_UsingAdvanced_BuiltInStringFunctions(string orderBy, int[] ids, string[] names) + { + // Arrange + string queryUrl = $"odata/students?{orderBy}&$select=id,name"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + JObject payloadBody = await response.Content.ReadAsObject(); + (int[] actualIds, string[] actualNames) = GetIds(payloadBody, "Name"); + + Assert.True(ids.SequenceEqual(actualIds)); + Assert.True(names.SequenceEqual(actualNames)); + } - [Theory] - [InlineData("$orderby=birthday", new[] { 2, 3, 1, 6, 5, 4 }, new[] { "1908", "1987", "2011", "2019", "2019", "2023" })] - [InlineData("$orderby=year(birthday)", new[] { 2, 3, 1, 5, 6, 4 },new[] { "1908", "1987", "2011", "2019", "2019", "2023" })] // Be noted: #5 goes before #6 - [InlineData("$orderby=month(birthday)", new[] { 1, 6, 4, 5, 3, 2 }, new[] { "2011", "2019", "2023", "2019", "1987", "1908" })] - public async Task DollarOrderBy_UsingAdvanced_BuiltInDateFunctions(string orderBy, int[] ids, string[] birthdays) - { - // Arrange - string queryUrl = $"odata/students?{orderBy}&$select=id,year&$compute=year(birthday) as year"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - JObject payloadBody = await response.Content.ReadAsObject(); - (int[] actualIds, string[] actualBirthdays) = GetIds(payloadBody, "year"); - - Assert.True(ids.SequenceEqual(actualIds)); - Assert.True(birthdays.SequenceEqual(actualBirthdays)); - } + [Theory] + [InlineData("$orderby=birthday", new[] { 2, 3, 1, 6, 5, 4 }, new[] { "1908", "1987", "2011", "2019", "2019", "2023" })] + [InlineData("$orderby=year(birthday)", new[] { 2, 3, 1, 5, 6, 4 },new[] { "1908", "1987", "2011", "2019", "2019", "2023" })] // Be noted: #5 goes before #6 + [InlineData("$orderby=month(birthday)", new[] { 1, 6, 4, 5, 3, 2 }, new[] { "2011", "2019", "2023", "2019", "1987", "1908" })] + public async Task DollarOrderBy_UsingAdvanced_BuiltInDateFunctions(string orderBy, int[] ids, string[] birthdays) + { + // Arrange + string queryUrl = $"odata/students?{orderBy}&$select=id,year&$compute=year(birthday) as year"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + JObject payloadBody = await response.Content.ReadAsObject(); + (int[] actualIds, string[] actualBirthdays) = GetIds(payloadBody, "year"); + + Assert.True(ids.SequenceEqual(actualIds)); + Assert.True(birthdays.SequenceEqual(actualBirthdays)); + } - [Fact] - public async Task DollarOrderBy_UsingDollarCount() - { - // Arrange - string queryUrl = $"odata/students?$orderby=Grades/$count&$select=id,number&$compute=Grades/$count as number"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - JObject payloadBody = await response.Content.ReadAsObject(); - (int[] actualIds, string[] actualNumbers) = GetIds(payloadBody, "number"); - - Assert.True(new int[] { 2, 5, 1, 4, 3, 6 }.SequenceEqual(actualIds)); - Assert.True(new int[] { 1, 1, 3, 3, 4, 5 }.SequenceEqual(actualNumbers.Select(a => int.Parse(a)))); - } + [Fact] + public async Task DollarOrderBy_UsingDollarCount() + { + // Arrange + string queryUrl = $"odata/students?$orderby=Grades/$count&$select=id,number&$compute=Grades/$count as number"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + JObject payloadBody = await response.Content.ReadAsObject(); + (int[] actualIds, string[] actualNumbers) = GetIds(payloadBody, "number"); + + Assert.True(new int[] { 2, 5, 1, 4, 3, 6 }.SequenceEqual(actualIds)); + Assert.True(new int[] { 1, 1, 3, 3, 4, 5 }.SequenceEqual(actualNumbers.Select(a => int.Parse(a)))); + } - private static (int[], string[]) GetIds(JObject payload, string propertyName) - { - JArray value = payload["value"] as JArray; - Assert.NotNull(value); - - int[] ids = new int[value.Count()]; - string[] properties = new string[value.Count()]; - for (int i = 0; i < value.Count(); i++) - { - JObject item = value[i] as JObject; - ids[i] = (int)item["Id"]; - properties[i] = (string)item[propertyName]; - } - - return (ids, properties); - } + private static (int[], string[]) GetIds(JObject payload, string propertyName) + { + JArray value = payload["value"] as JArray; + Assert.NotNull(value); - [Fact] - public async Task DollarOrderBy_UsingPropertyPath() + int[] ids = new int[value.Count()]; + string[] properties = new string[value.Count()]; + for (int i = 0; i < value.Count(); i++) { - // Arrange - string queryUrl = $"odata/students?$orderby=location/city&$select=id,location/city"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - string payloadBody = await response.Content.ReadAsStringAsync(); - Assert.Equal("{\"value\":[" + - "{\"Id\":3,\"Location\":{\"City\":\"Avat\"}}," + - "{\"Id\":4,\"Location\":{\"City\":\"Aveneve\"}}," + - "{\"Id\":5,\"Location\":{\"City\":\"Ces\"}}," + - "{\"Id\":1,\"Location\":{\"City\":\"Cesar\"}}," + - "{\"Id\":6,\"Location\":{\"City\":\"Claire\"}}," + - "{\"Id\":2,\"Location\":{\"City\":\"Debra\"}}" + - "]}", payloadBody); + JObject item = value[i] as JObject; + ids[i] = (int)item["Id"]; + properties[i] = (string)item[propertyName]; } - [Fact] - public async Task DollarOrderBy_UsingPropertyPath_WithBuiltInStringFunction() - { - // Arrange - string queryUrl = $"odata/students?$orderby=tolower(substring(location/city,1,2))&$select=id,location/city,t&$compute=tolower(substring(location/city,1,2)) as t"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - string payloadBody = await response.Content.ReadAsStringAsync(); - Assert.Equal("{\"value\":[" + - "{\"Id\":2,\"t\":\"eb\",\"Location\":{\"City\":\"Debra\"}}," + - "{\"Id\":1,\"t\":\"es\",\"Location\":{\"City\":\"Cesar\"}}," + - "{\"Id\":5,\"t\":\"es\",\"Location\":{\"City\":\"Ces\"}}," + - "{\"Id\":6,\"t\":\"la\",\"Location\":{\"City\":\"Claire\"}}," + - "{\"Id\":3,\"t\":\"va\",\"Location\":{\"City\":\"Avat\"}}," + - "{\"Id\":4,\"t\":\"ve\",\"Location\":{\"City\":\"Aveneve\"}}" + - "]}", payloadBody); - } + return (ids, properties); + } - [Fact] - public async Task DollarOrderBy_UsingDollarThisWithinDollarSelect_OnCollection() - { - // Arrange - string queryUrl = "odata/students/6?$select=Grades($orderby=$this)"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; + [Fact] + public async Task DollarOrderBy_UsingPropertyPath() + { + // Arrange + string queryUrl = $"odata/students?$orderby=location/city&$select=id,location/city"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"value\":[" + + "{\"Id\":3,\"Location\":{\"City\":\"Avat\"}}," + + "{\"Id\":4,\"Location\":{\"City\":\"Aveneve\"}}," + + "{\"Id\":5,\"Location\":{\"City\":\"Ces\"}}," + + "{\"Id\":1,\"Location\":{\"City\":\"Cesar\"}}," + + "{\"Id\":6,\"Location\":{\"City\":\"Claire\"}}," + + "{\"Id\":2,\"Location\":{\"City\":\"Debra\"}}" + + "]}", payloadBody); + } - // Act - response = await client.SendAsync(request); + [Fact] + public async Task DollarOrderBy_UsingPropertyPath_WithBuiltInStringFunction() + { + // Arrange + string queryUrl = $"odata/students?$orderby=tolower(substring(location/city,1,2))&$select=id,location/city,t&$compute=tolower(substring(location/city,1,2)) as t"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"value\":[" + + "{\"Id\":2,\"t\":\"eb\",\"Location\":{\"City\":\"Debra\"}}," + + "{\"Id\":1,\"t\":\"es\",\"Location\":{\"City\":\"Cesar\"}}," + + "{\"Id\":5,\"t\":\"es\",\"Location\":{\"City\":\"Ces\"}}," + + "{\"Id\":6,\"t\":\"la\",\"Location\":{\"City\":\"Claire\"}}," + + "{\"Id\":3,\"t\":\"va\",\"Location\":{\"City\":\"Avat\"}}," + + "{\"Id\":4,\"t\":\"ve\",\"Location\":{\"City\":\"Aveneve\"}}" + + "]}", payloadBody); + } - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + [Fact] + public async Task DollarOrderBy_UsingDollarThisWithinDollarSelect_OnCollection() + { + // Arrange + string queryUrl = "odata/students/6?$select=Grades($orderby=$this)"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; - string payloadBody = await response.Content.ReadAsStringAsync(); + // Act + response = await client.SendAsync(request); - // Origin is : 9, 8, 7, 1, 2 - Assert.Equal("{\"Grades\":[1,2,7,8,9]}", payloadBody); - } + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + string payloadBody = await response.Content.ReadAsStringAsync(); - [Fact] - public async Task DollarOrderBy_UsingDollarIt_OnCollection() - { - // Arrange - string queryUrl = "odata/students/6/Grades?$orderby=$it"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; + // Origin is : 9, 8, 7, 1, 2 + Assert.Equal("{\"Grades\":[1,2,7,8,9]}", payloadBody); + } - // Act - response = await client.SendAsync(request); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + [Fact] + public async Task DollarOrderBy_UsingDollarIt_OnCollection() + { + // Arrange + string queryUrl = "odata/students/6/Grades?$orderby=$it"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; - string payloadBody = await response.Content.ReadAsStringAsync(); + // Act + response = await client.SendAsync(request); - // Origin is : 9, 8, 7, 1, 2 - Assert.Equal("{\"value\":[1,2,7,8,9]}", payloadBody); - } + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + // Origin is : 9, 8, 7, 1, 2 + Assert.Equal("{\"value\":[1,2,7,8,9]}", payloadBody); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByContext.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByContext.cs index a98b367bc..2b6e148af 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByContext.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByContext.cs @@ -7,31 +7,30 @@ using Microsoft.EntityFrameworkCore; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataOrderByTest +namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataOrderByTest; + +public class OrderByContext : DbContext { - public class OrderByContext : DbContext + public OrderByContext(DbContextOptions options) + : base(options) { - public OrderByContext(DbContextOptions options) - : base(options) - { - } + } - public DbSet Items { get; set; } + public DbSet Items { get; set; } - public DbSet Items2 { get; set; } + public DbSet Items2 { get; set; } - public DbSet ItemsWithEnum { get; set; } + public DbSet ItemsWithEnum { get; set; } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity() - .HasKey(c => new { c.A, c.B, c.C }); + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasKey(c => new { c.A, c.B, c.C }); - modelBuilder.Entity() - .HasKey(c => new { c.A, c.B, c.C }); + modelBuilder.Entity() + .HasKey(c => new { c.A, c.B, c.C }); - modelBuilder.Entity() - .HasKey(c => new { c.A, c.B, c.C }); - } + modelBuilder.Entity() + .HasKey(c => new { c.A, c.B, c.C }); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByController.cs index ad25df67d..f117476e9 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByController.cs @@ -13,136 +13,135 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using Microsoft.EntityFrameworkCore; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataOrderByTest +namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataOrderByTest; + +[Route("odata")] +public class OrderByItemsController : ODataController { - [Route("odata")] - public class OrderByItemsController : ODataController - { - private readonly OrderByContext _db; - private static readonly IQueryable _itemWithoutColumns; + private readonly OrderByContext _db; + private static readonly IQueryable _itemWithoutColumns; - static OrderByItemsController() + static OrderByItemsController() + { + _itemWithoutColumns = new List() { - _itemWithoutColumns = new List() - { - // The key is A, B, C - new ItemWithoutColumn() { A = 2, B = 1, C = 1, ExpectedOrder = 4 }, - new ItemWithoutColumn() { A = 1, B = 2, C = 1, ExpectedOrder = 3 }, - new ItemWithoutColumn() { A = 1, B = 1, C = 2, ExpectedOrder = 2 }, - new ItemWithoutColumn() { A = 1, B = 1, C = 1, ExpectedOrder = 1 } - }.AsQueryable(); - } + // The key is A, B, C + new ItemWithoutColumn() { A = 2, B = 1, C = 1, ExpectedOrder = 4 }, + new ItemWithoutColumn() { A = 1, B = 2, C = 1, ExpectedOrder = 3 }, + new ItemWithoutColumn() { A = 1, B = 1, C = 2, ExpectedOrder = 2 }, + new ItemWithoutColumn() { A = 1, B = 1, C = 1, ExpectedOrder = 1 } + }.AsQueryable(); + } - public OrderByItemsController(OrderByContext context) + public OrderByItemsController(OrderByContext context) + { + context.Database.EnsureCreated(); + if (!context.Items.Any()) { - context.Database.EnsureCreated(); - if (!context.Items.Any()) - { - AddInSet(context.Items, - // The key is C, A, B - new Item() { A = 1, B = 99, C = 2, ExpectedOrder = 4 }, - new Item() { A = 2, B = 2, C = 1, ExpectedOrder = 3 }, - new Item() { A = 2, B = 1, C = 1, ExpectedOrder = 2 }, - new Item() { A = 1, B = 96, C = 1, ExpectedOrder = 1 } - ); - - AddInSet(context.Items2, - // The key is C, B, A - new Item2() { A = "AA", C = "BB", B = 99, ExpectedOrder = 2 }, - new Item2() { A = "BB", C = "AA", B = 98, ExpectedOrder = 1 }, - new Item2() { A = "01", C = "XX", B = 1, ExpectedOrder = 3 }, - new Item2() { A = "00", C = "ZZ", B = 96, ExpectedOrder = 4 } - ); - - AddInSet(context.ItemsWithEnum, - // The key is C, B, A - new ItemWithEnum() { A = SmallNumber.One, B = "A", C = SmallNumber.One, ExpectedOrder = 1 }, - new ItemWithEnum() { A = SmallNumber.One, B = "B", C = SmallNumber.One, ExpectedOrder = 3 }, - new ItemWithEnum() { A = SmallNumber.One, B = "B", C = SmallNumber.Two, ExpectedOrder = 4 }, - new ItemWithEnum() { A = SmallNumber.Two, B = "A", C = SmallNumber.One, ExpectedOrder = 2 } - ); - context.SaveChanges(); - } - - _db = context; + AddInSet(context.Items, + // The key is C, A, B + new Item() { A = 1, B = 99, C = 2, ExpectedOrder = 4 }, + new Item() { A = 2, B = 2, C = 1, ExpectedOrder = 3 }, + new Item() { A = 2, B = 1, C = 1, ExpectedOrder = 2 }, + new Item() { A = 1, B = 96, C = 1, ExpectedOrder = 1 } + ); + + AddInSet(context.Items2, + // The key is C, B, A + new Item2() { A = "AA", C = "BB", B = 99, ExpectedOrder = 2 }, + new Item2() { A = "BB", C = "AA", B = 98, ExpectedOrder = 1 }, + new Item2() { A = "01", C = "XX", B = 1, ExpectedOrder = 3 }, + new Item2() { A = "00", C = "ZZ", B = 96, ExpectedOrder = 4 } + ); + + AddInSet(context.ItemsWithEnum, + // The key is C, B, A + new ItemWithEnum() { A = SmallNumber.One, B = "A", C = SmallNumber.One, ExpectedOrder = 1 }, + new ItemWithEnum() { A = SmallNumber.One, B = "B", C = SmallNumber.One, ExpectedOrder = 3 }, + new ItemWithEnum() { A = SmallNumber.One, B = "B", C = SmallNumber.Two, ExpectedOrder = 4 }, + new ItemWithEnum() { A = SmallNumber.Two, B = "A", C = SmallNumber.One, ExpectedOrder = 2 } + ); + context.SaveChanges(); } - private static void AddInSet(DbSet set, params T[] items) where T : class - { - foreach (var item in items) - { - set.Add(item); - } - } + _db = context; + } - [EnableQuery] - [HttpGet("Items")] - public IActionResult GetItems() + private static void AddInSet(DbSet set, params T[] items) where T : class + { + foreach (var item in items) { - return Ok(_db.Items); + set.Add(item); } + } - [EnableQuery] - [HttpGet("Items2")] - public IActionResult GetItems2() - { - return Ok(_db.Items2); - } + [EnableQuery] + [HttpGet("Items")] + public IActionResult GetItems() + { + return Ok(_db.Items); + } - [EnableQuery] - [HttpGet("ItemsWithEnum")] - public IActionResult GetItemsWithEnum() - { - return Ok(_db.ItemsWithEnum); - } + [EnableQuery] + [HttpGet("Items2")] + public IActionResult GetItems2() + { + return Ok(_db.Items2); + } - [EnableQuery] - [HttpGet("ItemsWithoutColumn")] - public IActionResult GetItemsWithoutColumn() - { - return Ok(_itemWithoutColumns); - } + [EnableQuery] + [HttpGet("ItemsWithEnum")] + public IActionResult GetItemsWithEnum() + { + return Ok(_db.ItemsWithEnum); } - public class StudentsController : ODataController + [EnableQuery] + [HttpGet("ItemsWithoutColumn")] + public IActionResult GetItemsWithoutColumn() { - private static IList _students = new List - { - new OrderByStudent { Id = 1, Name = "A1", Birthday = new DateTimeOffset(2011, 1, 1, 1, 1, 1, TimeSpan.Zero), - Grades = new List{ 1, 2 ,3 }, Location = new OrderByAddress{ City = "Cesar", ZipCode = "98" } }, + return Ok(_itemWithoutColumns); + } +} - new OrderByStudent { Id = 2, Name = "a1", Birthday = new DateTimeOffset(1908, 11, 21, 5, 1, 1, TimeSpan.Zero), - Grades = new List{ 8 }, Location = new OrderByAddress{ City = "Debra", ZipCode = "1807" } }, +public class StudentsController : ODataController +{ + private static IList _students = new List + { + new OrderByStudent { Id = 1, Name = "A1", Birthday = new DateTimeOffset(2011, 1, 1, 1, 1, 1, TimeSpan.Zero), + Grades = new List{ 1, 2 ,3 }, Location = new OrderByAddress{ City = "Cesar", ZipCode = "98" } }, - new OrderByStudent { Id = 3, Name = "bb", Birthday = new DateTimeOffset(1987, 9, 11, 1, 1, 1, TimeSpan.Zero), - Grades = new List{ 5, 6, 8, 9 }, Location = new OrderByAddress{ City = "Avat", ZipCode = "98087" } }, + new OrderByStudent { Id = 2, Name = "a1", Birthday = new DateTimeOffset(1908, 11, 21, 5, 1, 1, TimeSpan.Zero), + Grades = new List{ 8 }, Location = new OrderByAddress{ City = "Debra", ZipCode = "1807" } }, - new OrderByStudent { Id = 4, Name = "Bc", Birthday = new DateTimeOffset(2023, 3, 4, 1, 1, 1, TimeSpan.Zero), - Grades = new List{ 4, 5, 6 }, Location = new OrderByAddress{ City = "Aveneve", ZipCode = "89" } }, + new OrderByStudent { Id = 3, Name = "bb", Birthday = new DateTimeOffset(1987, 9, 11, 1, 1, 1, TimeSpan.Zero), + Grades = new List{ 5, 6, 8, 9 }, Location = new OrderByAddress{ City = "Avat", ZipCode = "98087" } }, - new OrderByStudent { Id = 5, Name = "AB", Birthday = new DateTimeOffset(2019, 6, 7, 1, 1, 1, TimeSpan.Zero), - Grades = new List{ 0 }, Location = new OrderByAddress{ City = "Ces", ZipCode = "11" } }, + new OrderByStudent { Id = 4, Name = "Bc", Birthday = new DateTimeOffset(2023, 3, 4, 1, 1, 1, TimeSpan.Zero), + Grades = new List{ 4, 5, 6 }, Location = new OrderByAddress{ City = "Aveneve", ZipCode = "89" } }, - new OrderByStudent { Id = 6, Name = "Ab", Birthday = new DateTimeOffset(2019, 2, 18, 1, 1, 1, TimeSpan.Zero), - Grades = new List{ 9, 8, 7, 1, 2 }, Location = new OrderByAddress{ City = "Claire", ZipCode = "56" } } - }; + new OrderByStudent { Id = 5, Name = "AB", Birthday = new DateTimeOffset(2019, 6, 7, 1, 1, 1, TimeSpan.Zero), + Grades = new List{ 0 }, Location = new OrderByAddress{ City = "Ces", ZipCode = "11" } }, - [EnableQuery] - public IActionResult Get() - { - return Ok(_students); - } + new OrderByStudent { Id = 6, Name = "Ab", Birthday = new DateTimeOffset(2019, 2, 18, 1, 1, 1, TimeSpan.Zero), + Grades = new List{ 9, 8, 7, 1, 2 }, Location = new OrderByAddress{ City = "Claire", ZipCode = "56" } } + }; - [EnableQuery] - public IActionResult Get(int key) - { - return Ok(_students.FirstOrDefault(c => c.Id == key)); - } + [EnableQuery] + public IActionResult Get() + { + return Ok(_students); + } - [EnableQuery] - public IActionResult GetGrades(int key) - { - return Ok(_students.FirstOrDefault(c => c.Id == key).Grades); - } + [EnableQuery] + public IActionResult Get(int key) + { + return Ok(_students.FirstOrDefault(c => c.Id == key)); + } + + [EnableQuery] + public IActionResult GetGrades(int key) + { + return Ok(_students.FirstOrDefault(c => c.Id == key).Grades); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByDataModel.cs index ab2c8f3a3..f32f426de 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByDataModel.cs @@ -11,98 +11,97 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataOrderByTest +namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataOrderByTest; + +public abstract class OrderedItem +{ + public int ExpectedOrder { get; set; } +} + +public class Item : OrderedItem +{ + [Key] + [Column(Order = 2)] + public int A { get; set; } + + [Key] + [Column(Order = 1)] + public int C { get; set; } + + [Key] + [Column(Order = 3)] + public int B { get; set; } +} + +public class Item2 : OrderedItem +{ + [Key] + [Column(Order = 3)] + public string A { get; set; } + + [Key] + [Column(Order = 1)] + public string C { get; set; } + + [Key] + [Column(Order = 2)] + public int B { get; set; } +} + +public class ItemWithEnum : OrderedItem +{ + [Key] + [Column(Order = 3)] + public SmallNumber A { get; set; } + + [Key] + [Column(Order = 2)] + public string B { get; set; } + + [Key] + [Column(Order = 1)] + public SmallNumber C { get; set; } +} + +public class ItemWithoutColumn : OrderedItem +{ + [Key] + public int C { get; set; } + + [Key] + public int B { get; set; } + + [Key] + public int A { get; set; } +} + +public enum SmallNumber +{ + One, + Two, + Three, + Four +} + +#region Advanced $orderby + +public class OrderByStudent { - public abstract class OrderedItem - { - public int ExpectedOrder { get; set; } - } - - public class Item : OrderedItem - { - [Key] - [Column(Order = 2)] - public int A { get; set; } - - [Key] - [Column(Order = 1)] - public int C { get; set; } - - [Key] - [Column(Order = 3)] - public int B { get; set; } - } - - public class Item2 : OrderedItem - { - [Key] - [Column(Order = 3)] - public string A { get; set; } - - [Key] - [Column(Order = 1)] - public string C { get; set; } - - [Key] - [Column(Order = 2)] - public int B { get; set; } - } - - public class ItemWithEnum : OrderedItem - { - [Key] - [Column(Order = 3)] - public SmallNumber A { get; set; } - - [Key] - [Column(Order = 2)] - public string B { get; set; } - - [Key] - [Column(Order = 1)] - public SmallNumber C { get; set; } - } - - public class ItemWithoutColumn : OrderedItem - { - [Key] - public int C { get; set; } - - [Key] - public int B { get; set; } - - [Key] - public int A { get; set; } - } - - public enum SmallNumber - { - One, - Two, - Three, - Four - } - - #region Advanced $orderby - - public class OrderByStudent - { - public int Id { get; set; } - - public string Name { get; set; } - - public DateTimeOffset Birthday { get; set; } - - public IList Grades { get; set; } - - public OrderByAddress Location { get; set; } - } - - public class OrderByAddress - { - public string City { get; set; } - - public string ZipCode { get; set; } - } - #endregion + public int Id { get; set; } + + public string Name { get; set; } + + public DateTimeOffset Birthday { get; set; } + + public IList Grades { get; set; } + + public OrderByAddress Location { get; set; } +} + +public class OrderByAddress +{ + public string City { get; set; } + + public string ZipCode { get; set; } } +#endregion diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByMoreTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByMoreTest.cs index 244ec36bb..947720f8e 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByMoreTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByMoreTest.cs @@ -18,76 +18,75 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataOrderByTest +namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataOrderByTest; + +public class ODataOrderByMoreTest : WebApiTestBase { - public class ODataOrderByMoreTest : WebApiTestBase + public ODataOrderByMoreTest(WebApiTestFixture fixture) + :base(fixture) { - public ODataOrderByMoreTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(BooksController)); - services.AddControllers() - .AddOData(); // without any configuration - } + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(BooksController)); + services.AddControllers() + .AddOData(); // without any configuration + } - [Theory] - [InlineData("/books?orderby=ISBN&top=1")] - [InlineData("/books?orderby=isbn&top=1")] - [InlineData("/books?$orderby=ISBN&top=1")] - [InlineData("/books?$orderby=isbn&top=1")] - public async Task TestOrderBy_WithDifferentPropertyCase(string requestUri) - { - // Arrange - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); + [Theory] + [InlineData("/books?orderby=ISBN&top=1")] + [InlineData("/books?orderby=isbn&top=1")] + [InlineData("/books?$orderby=ISBN&top=1")] + [InlineData("/books?$orderby=isbn&top=1")] + public async Task TestOrderBy_WithDifferentPropertyCase(string requestUri) + { + // Arrange + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var resultArray = await response.Content.ReadAsObject(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var resultArray = await response.Content.ReadAsObject(); - JObject objectItem = Assert.IsType(Assert.Single(resultArray)); - Assert.Equal(2, objectItem.Properties().Count()); - Assert.Equal("063-6-920-02371-5", objectItem["isbn"]); - Assert.Equal(2, objectItem["id"]); - } + JObject objectItem = Assert.IsType(Assert.Single(resultArray)); + Assert.Equal(2, objectItem.Properties().Count()); + Assert.Equal("063-6-920-02371-5", objectItem["isbn"]); + Assert.Equal(2, objectItem["id"]); } +} - [ApiController] - [Route("[controller]")] - public class BooksController : ControllerBase +[ApiController] +[Route("[controller]")] +public class BooksController : ControllerBase +{ + private static IList _books = new List { - private static IList _books = new List + new Book { - new Book - { - Id = 1, - ISBN = "978-0-321-87758-1" - }, - new Book - { - Id = 2, - ISBN = "063-6-920-02371-5", - } - }; - - [HttpGet] - public IEnumerable Get(ODataQueryOptions queryOptions) + Id = 1, + ISBN = "978-0-321-87758-1" + }, + new Book { - var queryable = (IQueryable)queryOptions.ApplyTo(_books.AsQueryable()); - return queryable.ToList(); + Id = 2, + ISBN = "063-6-920-02371-5", } - } + }; - public class Book + [HttpGet] + public IEnumerable Get(ODataQueryOptions queryOptions) { - public int Id { get; set; } - public string ISBN { get; set; } + var queryable = (IQueryable)queryOptions.ApplyTo(_books.AsQueryable()); + return queryable.ToList(); } } + +public class Book +{ + public int Id { get; set; } + public string ISBN { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByTest.cs index a83f06549..8eaa9d6a9 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ODataOrderByTest/OrderByTest.cs @@ -18,93 +18,92 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataOrderByTest +namespace Microsoft.AspNetCore.OData.E2E.Tests.ODataOrderByTest; + +public class ODataOrderByTest : WebApiTestBase { - public class ODataOrderByTest : WebApiTestBase + public ODataOrderByTest(WebApiTestFixture fixture) + :base(fixture) { - public ODataOrderByTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=OrderByColumnTest8"; - services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); + protected static void UpdateConfigureServices(IServiceCollection services) + { + string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=OrderByColumnTest8"; + services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); - IEdmModel edmModel = GetEdmModel(); - services.ConfigureControllers(typeof(OrderByItemsController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel).OrderBy().Expand().SetMaxTop(null)); - } + IEdmModel edmModel = GetEdmModel(); + services.ConfigureControllers(typeof(OrderByItemsController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel).OrderBy().Expand().SetMaxTop(null)); + } - public static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Items"); - builder.EntitySet("Items2"); - builder.EntitySet("ItemsWithEnum"); - builder.EntitySet("ItemsWithoutColumn"); - builder.EnumType(); - return builder.GetEdmModel(); - } + public static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Items"); + builder.EntitySet("Items2"); + builder.EntitySet("ItemsWithEnum"); + builder.EntitySet("ItemsWithoutColumn"); + builder.EnumType(); + return builder.GetEdmModel(); + } - [Fact] - public async Task TestStableOrder_WithCompositeKeyOrderedByColumnAttribute_UseColumnAttributeToDetermineTheKeyOrder() - { // Arrange - await TestOrderedQuery("Items"); - } + [Fact] + public async Task TestStableOrder_WithCompositeKeyOrderedByColumnAttribute_UseColumnAttributeToDetermineTheKeyOrder() + { // Arrange + await TestOrderedQuery("Items"); + } - [Fact] - public async Task TestStableOrder_WithCompositeKeyOrderedByColumnAttribute_UseColumnAttributeToDetermineTheKeyOrder2() - { - await TestOrderedQuery("Items2"); - } + [Fact] + public async Task TestStableOrder_WithCompositeKeyOrderedByColumnAttribute_UseColumnAttributeToDetermineTheKeyOrder2() + { + await TestOrderedQuery("Items2"); + } - [Fact] - public async Task TestStableOrder_WithCompositeKeyOrderedByColumnAttribute_AndContainingEnums_UseColumnAttributeToDetermineTheKeyOrder() - { - await TestOrderedQuery("ItemsWithEnum"); - } + [Fact] + public async Task TestStableOrder_WithCompositeKeyOrderedByColumnAttribute_AndContainingEnums_UseColumnAttributeToDetermineTheKeyOrder() + { + await TestOrderedQuery("ItemsWithEnum"); + } - [Fact] - public async Task TestStableOrder_WithCompositeKeyNotOrdered_OrderTheKeyByPropertyName() - { - await TestOrderedQuery("ItemsWithoutColumn"); - } + [Fact] + public async Task TestStableOrder_WithCompositeKeyNotOrdered_OrderTheKeyByPropertyName() + { + await TestOrderedQuery("ItemsWithoutColumn"); + } - private async Task TestOrderedQuery(string entitySet) where T : OrderedItem, new() - { - // Arrange - var requestUri = $"odata/{entitySet}?$top=10"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); + private async Task TestOrderedQuery(string entitySet) where T : OrderedItem, new() + { + // Arrange + var requestUri = $"odata/{entitySet}?$top=10"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var rawResult = await response.Content.ReadAsStringAsync(); - var jsonResult = JObject.Parse(rawResult); - var jsonValue = jsonResult.SelectToken("value"); - Assert.NotNull(jsonValue); - var concreteResult = jsonValue.ToObject>(); - Assert.NotEmpty(concreteResult); - var expected = Enumerable.Range(1, 4).Select(i => new T() { ExpectedOrder = i }).ToList(); - Assert.Equal(expected, concreteResult, new OrderedItemComparer()); - } + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var rawResult = await response.Content.ReadAsStringAsync(); + var jsonResult = JObject.Parse(rawResult); + var jsonValue = jsonResult.SelectToken("value"); + Assert.NotNull(jsonValue); + var concreteResult = jsonValue.ToObject>(); + Assert.NotEmpty(concreteResult); + var expected = Enumerable.Range(1, 4).Select(i => new T() { ExpectedOrder = i }).ToList(); + Assert.Equal(expected, concreteResult, new OrderedItemComparer()); + } - private sealed class OrderedItemComparer : IEqualityComparer where T : OrderedItem + private sealed class OrderedItemComparer : IEqualityComparer where T : OrderedItem + { + public bool Equals(T x, T y) { - public bool Equals(T x, T y) - { - return x.ExpectedOrder == y.ExpectedOrder; - } + return x.ExpectedOrder == y.ExpectedOrder; + } - public int GetHashCode(T obj) - { - return obj.ExpectedOrder.GetHashCode(); - } + public int GetHashCode(T obj) + { + return obj.ExpectedOrder.GetHashCode(); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/AccountsController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/AccountsController.cs index 8e5add088..eb33f4122 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/AccountsController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/AccountsController.cs @@ -15,448 +15,447 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.OpenType +namespace Microsoft.AspNetCore.OData.E2E.Tests.OpenType; + +public class AccountsController : ODataController { - public class AccountsController : ODataController + static AccountsController() { - static AccountsController() - { - InitAccounts(); - } + InitAccounts(); + } - /// - /// static so that the data is shared among requests. - /// - public static IList Accounts = null; + /// + /// static so that the data is shared among requests. + /// + public static IList Accounts = null; - private static void InitAccounts() + private static void InitAccounts() + { + Accounts = new List { - Accounts = new List + new PremiumAccount() { - new PremiumAccount() + Id = 1, // #1 is used for query + Name = "Name1", + AccountInfo = new AccountInfo() { - Id = 1, // #1 is used for query - Name = "Name1", - AccountInfo = new AccountInfo() - { - NickName = "NickName1" - }, - Address = new GlobalAddress() - { - City = "Redmond", - Street = "1 Microsoft Way", - CountryCode="US" - }, - Tags = new Tags(), - Since=new DateTimeOffset(new DateTime(2014,5,22),TimeSpan.FromHours(8)), + NickName = "NickName1" }, - new Account() + Address = new GlobalAddress() { - Id = 2, // #2 is used for patch & Put - Name = "Name2", - AccountInfo = new AccountInfo() - { - NickName = "NickName2" - }, - Address = new Address() - { - City = "Shanghai", - Street = "Zixing Road" - }, - Tags = new Tags() + City = "Redmond", + Street = "1 Microsoft Way", + CountryCode="US" }, - new Account() + Tags = new Tags(), + Since=new DateTimeOffset(new DateTime(2014,5,22),TimeSpan.FromHours(8)), + }, + new Account() + { + Id = 2, // #2 is used for patch & Put + Name = "Name2", + AccountInfo = new AccountInfo() + { + NickName = "NickName2" + }, + Address = new Address() + { + City = "Shanghai", + Street = "Zixing Road" + }, + Tags = new Tags() + }, + new Account() + { + Id = 3, + Name = "Name3", + AccountInfo = new AccountInfo() { - Id = 3, - Name = "Name3", - AccountInfo = new AccountInfo() - { - NickName = "NickName3" + NickName = "NickName3" - }, - Address = new Address() - { - City = "Beijing", - Street = "Danling Street" - }, - Tags = new Tags() - } - }; - - Account account = Accounts.Single(a => a.Id == 1); - account.DynamicProperties["OwnerAlias"] = "jinfutan"; - account.DynamicProperties["OwnerGender"] = Gender.Female; - account.DynamicProperties["IsValid"] = true; - account.DynamicProperties["ShipAddresses"] = new List
(){ - new Address + }, + Address = new Address() { City = "Beijing", Street = "Danling Street" }, - new Address - { - City="Shanghai", - Street="Zixing", - } - }; + Tags = new Tags() + } + }; + + Account account = Accounts.Single(a => a.Id == 1); + account.DynamicProperties["OwnerAlias"] = "jinfutan"; + account.DynamicProperties["OwnerGender"] = Gender.Female; + account.DynamicProperties["IsValid"] = true; + account.DynamicProperties["ShipAddresses"] = new List
(){ + new Address + { + City = "Beijing", + Street = "Danling Street" + }, + new Address + { + City="Shanghai", + Street="Zixing", + } + }; - Accounts[0].AccountInfo.DynamicProperties["Age"] = 10; + Accounts[0].AccountInfo.DynamicProperties["Age"] = 10; - Accounts[0].AccountInfo.DynamicProperties["Gender"] = Gender.Male; + Accounts[0].AccountInfo.DynamicProperties["Gender"] = Gender.Male; - Accounts[0].AccountInfo.DynamicProperties["Subs"] = new string[] { "Xbox", "Windows", "Office" }; + Accounts[0].AccountInfo.DynamicProperties["Subs"] = new string[] { "Xbox", "Windows", "Office" }; - Accounts[0].Address.DynamicProperties["CountryOrRegion"] = "US"; - Accounts[0].Tags.DynamicProperties["Tag1"] = "Value 1"; - Accounts[0].Tags.DynamicProperties["Tag2"] = "Value 2"; + Accounts[0].Address.DynamicProperties["CountryOrRegion"] = "US"; + Accounts[0].Tags.DynamicProperties["Tag1"] = "Value 1"; + Accounts[0].Tags.DynamicProperties["Tag2"] = "Value 2"; - Accounts[1].AccountInfo.DynamicProperties["Age"] = 20; + Accounts[1].AccountInfo.DynamicProperties["Age"] = 20; - Accounts[1].AccountInfo.DynamicProperties["Gender"] = Gender.Female; + Accounts[1].AccountInfo.DynamicProperties["Gender"] = Gender.Female; - Accounts[1].AccountInfo.DynamicProperties["Subs"] = new string[] { "Xbox", "Windows" }; + Accounts[1].AccountInfo.DynamicProperties["Subs"] = new string[] { "Xbox", "Windows" }; - Accounts[1].Address.DynamicProperties["CountryOrRegion"] = "AnyCountry"; - Accounts[1].Tags.DynamicProperties["Tag1"] = "abc"; + Accounts[1].Address.DynamicProperties["CountryOrRegion"] = "AnyCountry"; + Accounts[1].Tags.DynamicProperties["Tag1"] = "abc"; - Accounts[2].AccountInfo.DynamicProperties["Age"] = 30; + Accounts[2].AccountInfo.DynamicProperties["Age"] = 30; - Accounts[2].AccountInfo.DynamicProperties["Gender"] = Gender.Female; + Accounts[2].AccountInfo.DynamicProperties["Gender"] = Gender.Female; - Accounts[2].AccountInfo.DynamicProperties["Subs"] = new string[] { "Windows", "Office" }; + Accounts[2].AccountInfo.DynamicProperties["Subs"] = new string[] { "Windows", "Office" }; - Accounts[2].Address.DynamicProperties["CountryOrRegion"] = "AnyCountry"; - } + Accounts[2].Address.DynamicProperties["CountryOrRegion"] = "AnyCountry"; + } - #region Get ~/.../Accounts - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - public IActionResult Get() - { - return Ok(Accounts.AsQueryable()); - } + #region Get ~/.../Accounts + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public IActionResult Get() + { + return Ok(Accounts.AsQueryable()); + } - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - [HttpGet("/attributeRouting/Accounts")] - public IActionResult GetAttributeRouting() - { - return Ok(Accounts.AsQueryable()); - } - #endregion + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + [HttpGet("/attributeRouting/Accounts")] + public IActionResult GetAttributeRouting() + { + return Ok(Accounts.AsQueryable()); + } + #endregion - #region Get ~/.../Accounts({key}) - [HttpGet] - public IActionResult Get(int key) - { - return Ok(Accounts.SingleOrDefault(e => e.Id == key)); - } + #region Get ~/.../Accounts({key}) + [HttpGet] + public IActionResult Get(int key) + { + return Ok(Accounts.SingleOrDefault(e => e.Id == key)); + } - [HttpGet("/attributeRouting/Accounts({key})")] - public IActionResult GetAttributeRouting(int key) - { - return Ok(Accounts.SingleOrDefault(e => e.Id == key)); - } - #endregion + [HttpGet("/attributeRouting/Accounts({key})")] + public IActionResult GetAttributeRouting(int key) + { + return Ok(Accounts.SingleOrDefault(e => e.Id == key)); + } + #endregion - [EnableQuery] - public IActionResult GetAccountsFromPremiumAccount() - { - return Ok(Accounts.OfType().AsQueryable()); - } + [EnableQuery] + public IActionResult GetAccountsFromPremiumAccount() + { + return Ok(Accounts.OfType().AsQueryable()); + } - // This action has 9 endpoints - // ~/attributeRouting/Accounts({key})/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since - // ~/convention|explicit/Accounts({key})/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since - // ~/convention|explicit/Accounts/{key}/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since - // ~/convention|explicit/Accounts({key})/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since/$value - // ~/convention|explicit/Accounts/{key}/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since/$value - [HttpGet("/attributeRouting/Accounts({key})/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since")] - public IActionResult GetSinceFromPremiumAccount(int key) - { - return Ok(Accounts.OfType().SingleOrDefault(e => e.Id == key).Since); - } + // This action has 9 endpoints + // ~/attributeRouting/Accounts({key})/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since + // ~/convention|explicit/Accounts({key})/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since + // ~/convention|explicit/Accounts/{key}/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since + // ~/convention|explicit/Accounts({key})/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since/$value + // ~/convention|explicit/Accounts/{key}/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since/$value + [HttpGet("/attributeRouting/Accounts({key})/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since")] + public IActionResult GetSinceFromPremiumAccount(int key) + { + return Ok(Accounts.OfType().SingleOrDefault(e => e.Id == key).Since); + } + + // This action has 4 endpoints + // ~/convention|explicit/Accounts({key})/AccountInfo + // ~/convention|explicit/Accounts/{key}/AccountInfo + public IActionResult GetAccountInfoFromAccount(int key) + { + return Ok(Accounts.SingleOrDefault(e => e.Id == key).AccountInfo); + } - // This action has 4 endpoints - // ~/convention|explicit/Accounts({key})/AccountInfo - // ~/convention|explicit/Accounts/{key}/AccountInfo - public IActionResult GetAccountInfoFromAccount(int key) + #region ~/.../Accounts({key})/Address + [HttpGet("/attributeRouting/Accounts({key})/Address")] + public IActionResult GetAddressAttributeRouting(int key) + { + return GetAddress(key); + } + + // convention routing + public IActionResult GetAddress(int key) + { + Account account = Accounts.SingleOrDefault(e => e.Id == key); + if (account == null) { - return Ok(Accounts.SingleOrDefault(e => e.Id == key).AccountInfo); + return NotFound(); } - #region ~/.../Accounts({key})/Address - [HttpGet("/attributeRouting/Accounts({key})/Address")] - public IActionResult GetAddressAttributeRouting(int key) + if (account.Address == null) { - return GetAddress(key); + return this.StatusCode(StatusCodes.Status204NoContent); } - // convention routing - public IActionResult GetAddress(int key) - { - Account account = Accounts.SingleOrDefault(e => e.Id == key); - if (account == null) - { - return NotFound(); - } + return Ok(account.Address); + } + #endregion - if (account.Address == null) - { - return this.StatusCode(StatusCodes.Status204NoContent); - } + // This action only has the attribute routing. + [HttpGet("/attributeRouting/Accounts({key})/Address/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress")] + public IActionResult GetGlobalAddress(int key) + { + Address address = Accounts.SingleOrDefault(e => e.Id == key).Address; + return Ok(address as GlobalAddress); + } - return Ok(account.Address); - } - #endregion + [HttpGet] + public IActionResult GetAddressOfGlobalAddressFromAccount(int key) + { + Address address = Accounts.SingleOrDefault(e => e.Id == key).Address; + return Ok(address as GlobalAddress); + } - // This action only has the attribute routing. - [HttpGet("/attributeRouting/Accounts({key})/Address/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress")] - public IActionResult GetGlobalAddress(int key) - { - Address address = Accounts.SingleOrDefault(e => e.Id == key).Address; - return Ok(address as GlobalAddress); - } + [HttpGet("/attributeRouting/Accounts({key})/Address/City")] + public IActionResult GetCityAttributeRouting(int key) + { + return Ok(Accounts.SingleOrDefault(e => e.Id == key).Address.City); + } - [HttpGet] - public IActionResult GetAddressOfGlobalAddressFromAccount(int key) - { - Address address = Accounts.SingleOrDefault(e => e.Id == key).Address; - return Ok(address as GlobalAddress); - } + public IActionResult GetTagsFromAccount(int key) + { + return Ok(Accounts.SingleOrDefault(e => e.Id == key).Tags); + } - [HttpGet("/attributeRouting/Accounts({key})/Address/City")] - public IActionResult GetCityAttributeRouting(int key) - { - return Ok(Accounts.SingleOrDefault(e => e.Id == key).Address.City); - } + [HttpGet("/attributeRouting/Accounts({key})/Tags")] + public IActionResult GetTagsAttributeRouting(int key) + { + return Ok(Accounts.SingleOrDefault(e => e.Id == key).Tags); + } - public IActionResult GetTagsFromAccount(int key) - { - return Ok(Accounts.SingleOrDefault(e => e.Id == key).Tags); - } + [HttpPatch] + public IActionResult Patch(int key, [FromBody]Delta patch, ODataQueryOptions queryOptions) + { + return PatchAttributeRouting(key, patch, queryOptions); + } - [HttpGet("/attributeRouting/Accounts({key})/Tags")] - public IActionResult GetTagsAttributeRouting(int key) - { - return Ok(Accounts.SingleOrDefault(e => e.Id == key).Tags); - } + [HttpPatch("/attributeRouting/Accounts({key})")] + public IActionResult PatchAttributeRouting(int key, [FromBody]Delta patch, ODataQueryOptions queryOptions) + { + IEnumerable appliedAccounts = Accounts.Where(a => a.Id == key); - [HttpPatch] - public IActionResult Patch(int key, [FromBody]Delta patch, ODataQueryOptions queryOptions) + if (appliedAccounts.Count() == 0) { - return PatchAttributeRouting(key, patch, queryOptions); + return BadRequest(string.Format("The entry with Id {0} doesn't exist", key)); } - [HttpPatch("/attributeRouting/Accounts({key})")] - public IActionResult PatchAttributeRouting(int key, [FromBody]Delta patch, ODataQueryOptions queryOptions) + if (queryOptions.IfMatch != null) { - IEnumerable appliedAccounts = Accounts.Where(a => a.Id == key); + IQueryable ifMatchAccounts = queryOptions.IfMatch.ApplyTo(appliedAccounts.AsQueryable()).Cast(); - if (appliedAccounts.Count() == 0) + if (ifMatchAccounts.Count() == 0) { - return BadRequest(string.Format("The entry with Id {0} doesn't exist", key)); + return BadRequest(string.Format("The entry with Id {0} has been updated", key)); } + } - if (queryOptions.IfMatch != null) - { - IQueryable ifMatchAccounts = queryOptions.IfMatch.ApplyTo(appliedAccounts.AsQueryable()).Cast(); - - if (ifMatchAccounts.Count() == 0) - { - return BadRequest(string.Format("The entry with Id {0} has been updated", key)); - } - } + Account account = appliedAccounts.Single(); + patch.Patch(account); - Account account = appliedAccounts.Single(); - patch.Patch(account); + return Ok(account); + } - return Ok(account); - } + [HttpPut] + public IActionResult Put(int key, [FromBody]Account account) + { + return PutAttributeRouting(key, account); + } - [HttpPut] - public IActionResult Put(int key, [FromBody]Account account) + [HttpPut("/attributeRouting/Accounts({key})")] + public IActionResult PutAttributeRouting(int key, [FromBody]Account account) + { + if (key != account.Id) { - return PutAttributeRouting(key, account); + return BadRequest("The ID of customer is not matched with the key"); } - [HttpPut("/attributeRouting/Accounts({key})")] - public IActionResult PutAttributeRouting(int key, [FromBody]Account account) - { - if (key != account.Id) - { - return BadRequest("The ID of customer is not matched with the key"); - } + Account originalAccount = Accounts.Where(a => a.Id == account.Id).Single(); + Accounts.Remove(originalAccount); + Accounts.Add(account); + return Ok(account); + } - Account originalAccount = Accounts.Where(a => a.Id == account.Id).Single(); - Accounts.Remove(originalAccount); - Accounts.Add(account); - return Ok(account); - } + [HttpPost("/attributeRouting/Accounts")] + public IActionResult Post([FromBody]Account account) + { + account.Id = Accounts.Count + 1; + Accounts.Add(account); - [HttpPost("/attributeRouting/Accounts")] - public IActionResult Post([FromBody]Account account) - { - account.Id = Accounts.Count + 1; - Accounts.Add(account); + return Created(account); + } - return Created(account); - } + [HttpDelete("/attributeRouting/Accounts({key})")] + public IActionResult Delete(int key) + { + IEnumerable appliedAccounts = Accounts.Where(c => c.Id == key); - [HttpDelete("/attributeRouting/Accounts({key})")] - public IActionResult Delete(int key) + if (appliedAccounts.Count() == 0) { - IEnumerable appliedAccounts = Accounts.Where(c => c.Id == key); + return BadRequest(string.Format("The entry with ID {0} doesn't exist", key)); + } - if (appliedAccounts.Count() == 0) - { - return BadRequest(string.Format("The entry with ID {0} doesn't exist", key)); - } + Account account = appliedAccounts.Single(); + Accounts.Remove(account); + return this.StatusCode(StatusCodes.Status204NoContent); + } - Account account = appliedAccounts.Single(); - Accounts.Remove(account); - return this.StatusCode(StatusCodes.Status204NoContent); + // This action has two endpoints: + // One is from Convention routing + // the other is from attribute routing. + [HttpPatch("/attributeRouting/Accounts({key})/Address")] + public IActionResult PatchToAddress(int key, [FromBody]Delta
address) + { + Account account = Accounts.FirstOrDefault(a => a.Id == key); + if (account == null) + { + return NotFound(); } - // This action has two endpoints: - // One is from Convention routing - // the other is from attribute routing. - [HttpPatch("/attributeRouting/Accounts({key})/Address")] - public IActionResult PatchToAddress(int key, [FromBody]Delta
address) + if (account.Address == null) { - Account account = Accounts.FirstOrDefault(a => a.Id == key); - if (account == null) - { - return NotFound(); - } + account.Address = new Address(); + } - if (account.Address == null) - { - account.Address = new Address(); - } + account.Address = address.Patch(account.Address); - account.Address = address.Patch(account.Address); + return Updated(account); + } - return Updated(account); + // This action has two endpoints: + // One is from Convention routing + // the other is from attribute routing. + [HttpPatch("/attributeRouting/Accounts({key})/Address/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress")] + public IActionResult PatchToAddressOfGlobalAddress(int key, [FromBody]Delta address) + { + Account account = Accounts.FirstOrDefault(a => a.Id == key); + if (account == null) + { + return NotFound(); } - // This action has two endpoints: - // One is from Convention routing - // the other is from attribute routing. - [HttpPatch("/attributeRouting/Accounts({key})/Address/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress")] - public IActionResult PatchToAddressOfGlobalAddress(int key, [FromBody]Delta address) + if (account.Address == null) { - Account account = Accounts.FirstOrDefault(a => a.Id == key); - if (account == null) - { - return NotFound(); - } + account.Address = new GlobalAddress(); + } - if (account.Address == null) - { - account.Address = new GlobalAddress(); - } + GlobalAddress globalAddress = account.Address as GlobalAddress; + account.Address = address.Patch(globalAddress); + return Updated(account); + } - GlobalAddress globalAddress = account.Address as GlobalAddress; - account.Address = address.Patch(globalAddress); - return Updated(account); + // This action has two endpoints: + // One is from Convention routing + // the other is from attribute routing. + [HttpPut("/attributeRouting/Accounts({key})/Address")] + public IActionResult PutToAddress(int key, [FromBody]Delta
address) + { + Account account = Accounts.FirstOrDefault(a => a.Id == key); + if (account == null) + { + return NotFound(); } - // This action has two endpoints: - // One is from Convention routing - // the other is from attribute routing. - [HttpPut("/attributeRouting/Accounts({key})/Address")] - public IActionResult PutToAddress(int key, [FromBody]Delta
address) + if (account.Address == null) { - Account account = Accounts.FirstOrDefault(a => a.Id == key); - if (account == null) - { - return NotFound(); - } - - if (account.Address == null) - { - account.Address = new Address(); - } + account.Address = new Address(); + } - address.Put(account.Address); + address.Put(account.Address); - return Updated(account); - } + return Updated(account); + } - [HttpDelete("/attributeRouting/Accounts({key})/Address")] - public IActionResult DeleteToAddress(int key) + [HttpDelete("/attributeRouting/Accounts({key})/Address")] + public IActionResult DeleteToAddress(int key) + { + Account account = Accounts.FirstOrDefault(a => a.Id == key); + if (account == null) { - Account account = Accounts.FirstOrDefault(a => a.Id == key); - if (account == null) - { - return NotFound(); - } - - account.Address = null; - return Updated(account); + return NotFound(); } - #region Function & Action + account.Address = null; + return Updated(account); + } - [HttpGet("/attributeRouting/Accounts({key})/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GetAddressFunction()")] - public Address GetAddressFunctionOnAccount(int key) - { - return Accounts.SingleOrDefault(e => e.Id == key).Address; - } + #region Function & Action - [HttpPost("/attributeRouting/Accounts({key})/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.IncreaseAgeAction")] - public AccountInfo IncreaseAgeActionOnAccount(int key) - { - AccountInfo accountInfo = Accounts.SingleOrDefault(e => e.Id == key).AccountInfo; - accountInfo.DynamicProperties["Age"] = (int)accountInfo.DynamicProperties["Age"] + 1; - return accountInfo; - } + [HttpGet("/attributeRouting/Accounts({key})/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GetAddressFunction()")] + public Address GetAddressFunctionOnAccount(int key) + { + return Accounts.SingleOrDefault(e => e.Id == key).Address; + } - [HttpPost("/attributeRouting/UpdateAddressAction")] - public Address UpdateAddressActionAttributeRouting([FromBody]ODataActionParameters parameters) - { - var id = (int)parameters["ID"]; - var address = parameters["Address"] as Address; + [HttpPost("/attributeRouting/Accounts({key})/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.IncreaseAgeAction")] + public AccountInfo IncreaseAgeActionOnAccount(int key) + { + AccountInfo accountInfo = Accounts.SingleOrDefault(e => e.Id == key).AccountInfo; + accountInfo.DynamicProperties["Age"] = (int)accountInfo.DynamicProperties["Age"] + 1; + return accountInfo; + } - Account account = Accounts.Single(a => a.Id == id); - account.Address = address; - return address; - } + [HttpPost("/attributeRouting/UpdateAddressAction")] + public Address UpdateAddressActionAttributeRouting([FromBody]ODataActionParameters parameters) + { + var id = (int)parameters["ID"]; + var address = parameters["Address"] as Address; - [HttpPost("Accounts({key})/Microsoft.Test.E2E.AspNet.OData.OpenType.AddShipAddress")] - public IActionResult AddShipAddress(int key, [FromBody]ODataActionParameters parameters) - { - Account account = Accounts.Single(c => c.Id == key); - if (account.DynamicProperties["ShipAddresses"] == null) - { - account.DynamicProperties["ShipAddresses"] = new List
(); - } + Account account = Accounts.Single(a => a.Id == id); + account.Address = address; + return address; + } - IList
addresses = (IList
)account.DynamicProperties["ShipAddresses"]; - addresses.Add(parameters["address"] as Address); - return Ok(addresses.Count); + [HttpPost("Accounts({key})/Microsoft.Test.E2E.AspNet.OData.OpenType.AddShipAddress")] + public IActionResult AddShipAddress(int key, [FromBody]ODataActionParameters parameters) + { + Account account = Accounts.Single(c => c.Id == key); + if (account.DynamicProperties["ShipAddresses"] == null) + { + account.DynamicProperties["ShipAddresses"] = new List
(); } - [HttpGet("Accounts({key})/Microsoft.Test.E2E.AspNet.OData.OpenType.GetShipAddresses")] - public IActionResult GetShipAddresses(int key) + IList
addresses = (IList
)account.DynamicProperties["ShipAddresses"]; + addresses.Add(parameters["address"] as Address); + return Ok(addresses.Count); + } + + [HttpGet("Accounts({key})/Microsoft.Test.E2E.AspNet.OData.OpenType.GetShipAddresses")] + public IActionResult GetShipAddresses(int key) + { + Account account = Accounts.Single(c => c.Id == key); + if (account.DynamicProperties["ShipAddresses"] == null) { - Account account = Accounts.Single(c => c.Id == key); - if (account.DynamicProperties["ShipAddresses"] == null) - { - return Ok(new List
()); - } - else - { - IList
addresses = (IList
)account.DynamicProperties["ShipAddresses"]; - return Ok(addresses); - } + return Ok(new List
()); } - #endregion - - [HttpPost("ResetDataSource")] - public IActionResult ResetDataSource() + else { - InitAccounts(); - return this.StatusCode(StatusCodes.Status204NoContent); + IList
addresses = (IList
)account.DynamicProperties["ShipAddresses"]; + return Ok(addresses); } } + #endregion + + [HttpPost("ResetDataSource")] + public IActionResult ResetDataSource() + { + InitAccounts(); + return this.StatusCode(StatusCodes.Status204NoContent); + } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/OpenTypeDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/OpenTypeDataModel.cs index 9447afc04..dcab106bb 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/OpenTypeDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/OpenTypeDataModel.cs @@ -8,97 +8,96 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.OpenType +namespace Microsoft.AspNetCore.OData.E2E.Tests.OpenType; + +// This is an open entity type. +public class Account { - // This is an open entity type. - public class Account + public Account() { - public Account() - { - DynamicProperties = new Dictionary(); - } - - public int Id { get; set; } - public String Name { get; set; } - public AccountInfo AccountInfo { get; set; } - public Address Address { get; set; } - public Tags Tags { get; set; } - public IDictionary DynamicProperties { get; set; } + DynamicProperties = new Dictionary(); } - // The base type is open, so it is open too. - public class PremiumAccount : Account - { - public DateTimeOffset Since { get; set; } - } + public int Id { get; set; } + public String Name { get; set; } + public AccountInfo AccountInfo { get; set; } + public Address Address { get; set; } + public Tags Tags { get; set; } + public IDictionary DynamicProperties { get; set; } +} + +// The base type is open, so it is open too. +public class PremiumAccount : Account +{ + public DateTimeOffset Since { get; set; } +} + +// This is not an open entity type. +public class Employee +{ + public int Id { get; set; } + public string Name { get; set; } + public Account Account { get; set; } +} - // This is not an open entity type. - public class Employee +// This is an open entity type. +public class Manager : Employee +{ + public Manager() { - public int Id { get; set; } - public string Name { get; set; } - public Account Account { get; set; } + DynamicProperties = new Dictionary(); } - // This is an open entity type. - public class Manager : Employee - { - public Manager() - { - DynamicProperties = new Dictionary(); - } + public int Heads { get; set; } + public IDictionary DynamicProperties { get; set; } +} - public int Heads { get; set; } - public IDictionary DynamicProperties { get; set; } +// This is an open complex type, which may contain dynamic properties: age(int), gender(enum), etc. +public class AccountInfo +{ + public AccountInfo() + { + DynamicProperties = new Dictionary(); } - // This is an open complex type, which may contain dynamic properties: age(int), gender(enum), etc. - public class AccountInfo - { - public AccountInfo() - { - DynamicProperties = new Dictionary(); - } + public string NickName { get; set; } - public string NickName { get; set; } + public Dictionary DynamicProperties { get; set; } +} - public Dictionary DynamicProperties { get; set; } +// This is an open complex type,which may contain dynamic properties: countryOrRegion(string) +public class Address +{ + public Address() + { + DynamicProperties = new Dictionary(); } - // This is an open complex type,which may contain dynamic properties: countryOrRegion(string) - public class Address - { - public Address() - { - DynamicProperties = new Dictionary(); - } + public string City { get; set; } + public string Street { get; set; } - public string City { get; set; } - public string Street { get; set; } + public Dictionary DynamicProperties { get; set; } +} - public Dictionary DynamicProperties { get; set; } - } +public class GlobalAddress : Address +{ + public string CountryCode { get; set; } +} - public class GlobalAddress : Address +// This is an open complex type, which does not contain any declared properties. +public class Tags +{ + public Tags() { - public string CountryCode { get; set; } + DynamicProperties = new Dictionary(); } - // This is an open complex type, which does not contain any declared properties. - public class Tags - { - public Tags() - { - DynamicProperties = new Dictionary(); - } - - public Dictionary DynamicProperties { get; set; } - } + public Dictionary DynamicProperties { get; set; } +} - public enum Gender - { - Male = 1, +public enum Gender +{ + Male = 1, - Female = 2 - } + Female = 2 } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/OpenTypeEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/OpenTypeEdmModel.cs index 944a9409b..49d159f1d 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/OpenTypeEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/OpenTypeEdmModel.cs @@ -8,101 +8,100 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.OpenType +namespace Microsoft.AspNetCore.OData.E2E.Tests.OpenType; + +internal class OpenComplexTypeEdmModel { - internal class OpenComplexTypeEdmModel + public static IEdmModel GetTypedExplicitModel() + { + ODataModelBuilder builder = new ODataModelBuilder(); + var accountType = builder.EntityType(); + accountType.HasKey(c => c.Id); + accountType.Property(c => c.Name); + accountType.HasDynamicProperties(c => c.DynamicProperties); + + accountType.ComplexProperty(c => c.AccountInfo); + accountType.ComplexProperty
(a => a.Address); + accountType.ComplexProperty(a => a.Tags); + + var premiumAccountType = builder.EntityType(); + premiumAccountType.Property(p => p.Since); + premiumAccountType.DerivesFrom(); + + var accountInfoType = builder.ComplexType(); + accountInfoType.Property(i => i.NickName); + accountInfoType.HasDynamicProperties(i => i.DynamicProperties); + + var addressType = builder.ComplexType
(); + addressType.Property(a => a.City); + addressType.Property(a => a.Street); + addressType.HasDynamicProperties(a => a.DynamicProperties); + + var globalAddressType = builder.ComplexType(); + globalAddressType.Property(a => a.CountryCode); + globalAddressType.DerivesFrom
(); + + var tagsType = builder.ComplexType(); + tagsType.HasDynamicProperties(t => t.DynamicProperties); + + var gender = builder.EnumType(); + gender.Member(Gender.Female); + gender.Member(Gender.Male); + + var employeeType = builder.EntityType(); + employeeType.HasKey(e => e.Id); + employeeType.HasOptional(e => e.Account); + builder.EntitySet("Employees"); + + var managerType = builder.EntityType(); + managerType.Property(m => m.Heads); + managerType.HasDynamicProperties(m => m.DynamicProperties); + managerType.DerivesFrom(); + + AddBoundActionsAndFunctions(accountType); + AddUnboundActionsAndFunctions(builder); + + EntitySetConfiguration accounts = builder.EntitySet("Accounts"); + builder.Namespace = typeof(Account).Namespace; + return builder.GetEdmModel(); + } + + public static IEdmModel GetTypedConventionModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + + builder.EntitySet("Employees"); + builder.EntitySet("Accounts"); + + builder.EnumType(); + + AddBoundActionsAndFunctions(builder.EntityType()); + AddUnboundActionsAndFunctions(builder); + + builder.Namespace = typeof(Account).Namespace; + + return builder.GetEdmModel(); + } + + private static void AddBoundActionsAndFunctions(EntityTypeConfiguration account) { - public static IEdmModel GetTypedExplicitModel() - { - ODataModelBuilder builder = new ODataModelBuilder(); - var accountType = builder.EntityType(); - accountType.HasKey(c => c.Id); - accountType.Property(c => c.Name); - accountType.HasDynamicProperties(c => c.DynamicProperties); - - accountType.ComplexProperty(c => c.AccountInfo); - accountType.ComplexProperty
(a => a.Address); - accountType.ComplexProperty(a => a.Tags); - - var premiumAccountType = builder.EntityType(); - premiumAccountType.Property(p => p.Since); - premiumAccountType.DerivesFrom(); - - var accountInfoType = builder.ComplexType(); - accountInfoType.Property(i => i.NickName); - accountInfoType.HasDynamicProperties(i => i.DynamicProperties); - - var addressType = builder.ComplexType
(); - addressType.Property(a => a.City); - addressType.Property(a => a.Street); - addressType.HasDynamicProperties(a => a.DynamicProperties); - - var globalAddressType = builder.ComplexType(); - globalAddressType.Property(a => a.CountryCode); - globalAddressType.DerivesFrom
(); - - var tagsType = builder.ComplexType(); - tagsType.HasDynamicProperties(t => t.DynamicProperties); - - var gender = builder.EnumType(); - gender.Member(Gender.Female); - gender.Member(Gender.Male); - - var employeeType = builder.EntityType(); - employeeType.HasKey(e => e.Id); - employeeType.HasOptional(e => e.Account); - builder.EntitySet("Employees"); - - var managerType = builder.EntityType(); - managerType.Property(m => m.Heads); - managerType.HasDynamicProperties(m => m.DynamicProperties); - managerType.DerivesFrom(); - - AddBoundActionsAndFunctions(accountType); - AddUnboundActionsAndFunctions(builder); - - EntitySetConfiguration accounts = builder.EntitySet("Accounts"); - builder.Namespace = typeof(Account).Namespace; - return builder.GetEdmModel(); - } - - public static IEdmModel GetTypedConventionModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - - builder.EntitySet("Employees"); - builder.EntitySet("Accounts"); - - builder.EnumType(); - - AddBoundActionsAndFunctions(builder.EntityType()); - AddUnboundActionsAndFunctions(builder); - - builder.Namespace = typeof(Account).Namespace; - - return builder.GetEdmModel(); - } - - private static void AddBoundActionsAndFunctions(EntityTypeConfiguration account) - { - account.Function("GetAddressFunction").Returns
(); - - account.Function("GetShipAddresses").ReturnsCollection
(); - account.Action("IncreaseAgeAction").Returns(); - - ActionConfiguration addShipAddress = account.Action("AddShipAddress"); - addShipAddress.Parameter
("address"); - addShipAddress.Returns();// Total ship addresses count. - } - - private static void AddUnboundActionsAndFunctions(ODataModelBuilder odataModelBuilder) - { - // odataModelBuilder.Action("ResetDataSource"); - - ActionConfiguration udpateAddress = odataModelBuilder.Action("UpdateAddressAction"); - udpateAddress.Parameter
("Address"); - udpateAddress.Parameter("ID"); - udpateAddress.Returns
(); - } + account.Function("GetAddressFunction").Returns
(); + + account.Function("GetShipAddresses").ReturnsCollection
(); + account.Action("IncreaseAgeAction").Returns(); + + ActionConfiguration addShipAddress = account.Action("AddShipAddress"); + addShipAddress.Parameter
("address"); + addShipAddress.Returns();// Total ship addresses count. + } + + private static void AddUnboundActionsAndFunctions(ODataModelBuilder odataModelBuilder) + { + // odataModelBuilder.Action("ResetDataSource"); + + ActionConfiguration udpateAddress = odataModelBuilder.Action("UpdateAddressAction"); + udpateAddress.Parameter
("Address"); + udpateAddress.Parameter("ID"); + udpateAddress.Returns
(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/TypedOpenTypeTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/TypedOpenTypeTest.cs index 61cb06c3d..260bda649 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/TypedOpenTypeTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/OpenType/TypedOpenTypeTest.cs @@ -17,865 +17,864 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.OpenType -{ - // this convention is used to stop the conventions for a certain route prefix. - // Here, we stop to apply routing conventions for "attributeRouting" route prefix. - public class StopODataRoutingConvention : IODataControllerActionConvention - { - public int Order => int.MinValue; - - public bool AppliesToAction(ODataControllerActionContext context) - { - if (context.Prefix == "attributeRouting") - { - return true; - } +namespace Microsoft.AspNetCore.OData.E2E.Tests.OpenType; - return false; - } +// this convention is used to stop the conventions for a certain route prefix. +// Here, we stop to apply routing conventions for "attributeRouting" route prefix. +public class StopODataRoutingConvention : IODataControllerActionConvention +{ + public int Order => int.MinValue; - public bool AppliesToController(ODataControllerActionContext context) + public bool AppliesToAction(ODataControllerActionContext context) + { + if (context.Prefix == "attributeRouting") { return true; } + + return false; } - public class TypedOpenTypeTest : WebApiTestBase + public bool AppliesToController(ODataControllerActionContext context) { - public TypedOpenTypeTest(WebApiTestFixture fixture) - :base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(AccountsController), typeof(ODataEndpointController)); - - IEdmModel model1 = OpenComplexTypeEdmModel.GetTypedConventionModel(); - - services.AddControllers().AddOData(opt => - { - opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select() - .AddRouteComponents("convention", model1) - .AddRouteComponents("attributeRouting", model1) - .AddRouteComponents("explicit", OpenComplexTypeEdmModel.GetTypedExplicitModel()) - .Conventions.Add(new StopODataRoutingConvention()); - - // simply suppress the route number from conventional routing - opt.RouteOptions.EnableUnqualifiedOperationCall = false; - opt.RouteOptions.EnableKeyAsSegment = false; - }); - } - - [Theory] - [InlineData("convention", "application/json;odata.metadata=full")] - [InlineData("convention", "application/json;odata.metadata=minimal")] - [InlineData("convention", "application/json;odata.metadata=none")] - [InlineData("attributeRouting", "application/json;odata.metadata=full")] - [InlineData("attributeRouting", "application/json;odata.metadata=minimal")] - [InlineData("attributeRouting", "application/json;odata.metadata=none")] - public async Task QueryEntitySet(string mode, string format) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); - - string requestUri = $"{mode}/Accounts?$format={format}"; - - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); - - // Assert - Assert.True(response.IsSuccessStatusCode); - - var json = await response.Content.ReadAsObject(); - - var results = json.GetValue("value") as JArray; - Assert.Equal(3, results.Count); - - var age = results[1]["AccountInfo"]["Age"]; - Assert.Equal(20, age); + return true; + } +} - var gender = (string)results[2]["AccountInfo"]["Gender"]; - Assert.Equal("Female", gender); +public class TypedOpenTypeTest : WebApiTestBase +{ + public TypedOpenTypeTest(WebApiTestFixture fixture) + :base(fixture) + { + } - var countryOrRegion = results[1]["Address"]["CountryOrRegion"].ToString(); - Assert.Equal("AnyCountry", countryOrRegion); + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(AccountsController), typeof(ODataEndpointController)); - var tag1 = results[0]["Tags"]["Tag1"]; - Assert.Equal("Value 1", tag1); - var tag2 = results[0]["Tags"]["Tag2"]; - Assert.Equal("Value 2", tag2); - } + IEdmModel model1 = OpenComplexTypeEdmModel.GetTypedConventionModel(); - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task QueryEntity(string mode) + services.AddControllers().AddOData(opt => { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); - string requestUri = $"{mode}/Accounts(1)"; - - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select() + .AddRouteComponents("convention", model1) + .AddRouteComponents("attributeRouting", model1) + .AddRouteComponents("explicit", OpenComplexTypeEdmModel.GetTypedExplicitModel()) + .Conventions.Add(new StopODataRoutingConvention()); + + // simply suppress the route number from conventional routing + opt.RouteOptions.EnableUnqualifiedOperationCall = false; + opt.RouteOptions.EnableKeyAsSegment = false; + }); + } - // Assert - Assert.True(response.IsSuccessStatusCode); + [Theory] + [InlineData("convention", "application/json;odata.metadata=full")] + [InlineData("convention", "application/json;odata.metadata=minimal")] + [InlineData("convention", "application/json;odata.metadata=none")] + [InlineData("attributeRouting", "application/json;odata.metadata=full")] + [InlineData("attributeRouting", "application/json;odata.metadata=minimal")] + [InlineData("attributeRouting", "application/json;odata.metadata=none")] + public async Task QueryEntitySet(string mode, string format) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); - var result = await response.Content.ReadAsObject(); + string requestUri = $"{mode}/Accounts?$format={format}"; - var age = result["AccountInfo"]["Age"]; - Assert.Equal(10, age); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - var gender = (string)result["AccountInfo"]["Gender"]; - Assert.Equal("Male", gender); + // Assert + Assert.True(response.IsSuccessStatusCode); - var countryOrRegion = result["Address"]["CountryOrRegion"].ToString(); - Assert.Equal("US", countryOrRegion); + var json = await response.Content.ReadAsObject(); - var tag1 = result["Tags"]["Tag1"]; - Assert.Equal("Value 1", tag1); - var tag2 = result["Tags"]["Tag2"]; - Assert.Equal("Value 2", tag2); - } - - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task QueryPropertyFromDerivedOpenEntity(string mode) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); - string requestUri = $"{mode}/Accounts(1)/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since"; + var results = json.GetValue("value") as JArray; + Assert.Equal(3, results.Count); - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); - - // Assert - Assert.True(response.IsSuccessStatusCode); + var age = results[1]["AccountInfo"]["Age"]; + Assert.Equal(20, age); - var content = await response.Content.ReadAsStringAsync(); + var gender = (string)results[2]["AccountInfo"]["Gender"]; + Assert.Equal("Female", gender); - Assert.Contains("2014-05-22T00:00:00+08:00", content); - } + var countryOrRegion = results[1]["Address"]["CountryOrRegion"].ToString(); + Assert.Equal("AnyCountry", countryOrRegion); - [Theory] - [InlineData("convention")] - [InlineData("explicit")] - public async Task QueryOpenComplexTypePropertyAccountInfo(string mode) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); - string requestUri = $"{mode}/Accounts(1)/AccountInfo"; + var tag1 = results[0]["Tags"]["Tag1"]; + Assert.Equal("Value 1", tag1); + var tag2 = results[0]["Tags"]["Tag2"]; + Assert.Equal("Value 2", tag2); + } - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task QueryEntity(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); + string requestUri = $"{mode}/Accounts(1)"; - // Assert - Assert.True(response.IsSuccessStatusCode); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - var json = await response.Content.ReadAsObject(); + // Assert + Assert.True(response.IsSuccessStatusCode); - var nickName = json.GetValue("NickName").ToString(); - Assert.Equal("NickName1", nickName); + var result = await response.Content.ReadAsObject(); - var age = json.GetValue("Age"); - Assert.Equal(10, age); + var age = result["AccountInfo"]["Age"]; + Assert.Equal(10, age); - var gender = (string)json.GetValue("Gender"); - Assert.Equal("Male", gender); - } + var gender = (string)result["AccountInfo"]["Gender"]; + Assert.Equal("Male", gender); - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task QueryOpenComplexTypePropertyAddress(string mode) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); - string requestUri = $"{mode}/Accounts(1)/Address"; + var countryOrRegion = result["Address"]["CountryOrRegion"].ToString(); + Assert.Equal("US", countryOrRegion); - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + var tag1 = result["Tags"]["Tag1"]; + Assert.Equal("Value 1", tag1); + var tag2 = result["Tags"]["Tag2"]; + Assert.Equal("Value 2", tag2); + } - // Assert - Assert.True(response.IsSuccessStatusCode); + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task QueryPropertyFromDerivedOpenEntity(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); + string requestUri = $"{mode}/Accounts(1)/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.PremiumAccount/Since"; - var json = await response.Content.ReadAsObject(); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - var city = json.GetValue("City").ToString(); - Assert.Equal("Redmond", city); + // Assert + Assert.True(response.IsSuccessStatusCode); - var countryOrRegion = json.GetValue("CountryOrRegion").ToString(); - Assert.Equal("US", countryOrRegion); + var content = await response.Content.ReadAsStringAsync(); - // Property defined in the derived type. - var countryCode = json.GetValue("CountryCode").ToString(); - Assert.Equal("US", countryCode); - } + Assert.Contains("2014-05-22T00:00:00+08:00", content); + } - [Theory] - [InlineData("application/json;odata.metadata=full")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=none")] - public async Task QueryDerivedOpenComplexType(string format) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); - string requestUri = "attributeRouting/Accounts(1)/Address/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress?$format=" + format; + [Theory] + [InlineData("convention")] + [InlineData("explicit")] + public async Task QueryOpenComplexTypePropertyAccountInfo(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); + string requestUri = $"{mode}/Accounts(1)/AccountInfo"; - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - // Assert - Assert.True(response.IsSuccessStatusCode); + // Assert + Assert.True(response.IsSuccessStatusCode); - var json = await response.Content.ReadAsObject(); + var json = await response.Content.ReadAsObject(); - var city = json.GetValue("City").ToString(); - Assert.Equal("Redmond", city); + var nickName = json.GetValue("NickName").ToString(); + Assert.Equal("NickName1", nickName); - var countryOrRegion = json.GetValue("CountryOrRegion").ToString(); - Assert.Equal("US", countryOrRegion); - } + var age = json.GetValue("Age"); + Assert.Equal(10, age); - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task QueryOpenComplexTypePropertyTags(string mode) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); - string requestUri = $"{mode}/Accounts(1)/Tags"; + var gender = (string)json.GetValue("Gender"); + Assert.Equal("Male", gender); + } - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task QueryOpenComplexTypePropertyAddress(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); + string requestUri = $"{mode}/Accounts(1)/Address"; - // Assert - Assert.True(response.IsSuccessStatusCode); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - var json = await response.Content.ReadAsObject(); + // Assert + Assert.True(response.IsSuccessStatusCode); - var tag1 = json.GetValue("Tag1").ToString(); - Assert.Equal("Value 1", tag1); + var json = await response.Content.ReadAsObject(); - var tag2 = json.GetValue("Tag2").ToString(); - Assert.Equal("Value 2", tag2); - } + var city = json.GetValue("City").ToString(); + Assert.Equal("Redmond", city); - [Theory] - [InlineData("application/json;odata.metadata=full")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=none")] - public async Task QueryNonDynamicProperty(string format) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); - string requestUri = $"attributeRouting/Accounts(1)/Address/City?$format=" + format; + var countryOrRegion = json.GetValue("CountryOrRegion").ToString(); + Assert.Equal("US", countryOrRegion); - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + // Property defined in the derived type. + var countryCode = json.GetValue("CountryCode").ToString(); + Assert.Equal("US", countryCode); + } - // Assert - Assert.True(response.IsSuccessStatusCode); + [Theory] + [InlineData("application/json;odata.metadata=full")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=none")] + public async Task QueryDerivedOpenComplexType(string format) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); + string requestUri = "attributeRouting/Accounts(1)/Address/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress?$format=" + format; - var json = await response.Content.ReadAsObject(); - var city = json.GetValue("value").ToString(); - Assert.Equal("Redmond", city); - } + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - #region Update + // Assert + Assert.True(response.IsSuccessStatusCode); - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task PatchEntityWithOpenComplexType(string mode) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); - - string patchUri = $"{mode}/Accounts(2)"; - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), patchUri); - string payload = @"{ - '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Account', - 'AccountInfo':{'NickName':'NewNickName1','Age':40,'Gender': 'Male'}, - 'Address':{'CountryOrRegion':'United States'}, - 'Tags':{'Tag1':'New Value'}, - 'ShipAddresses@odata.type':'#Collection(Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Address)', - 'ShipAddresses':[], - 'OwnerGender@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Gender', - 'OwnerGender':null - }"; - request.Content = new StringContent(payload); - - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - - // Act - using (var patchResponse = await client.SendAsync(request)) - { - // Assert - Assert.Equal(HttpStatusCode.OK, patchResponse.StatusCode); - - var content = await patchResponse.Content.ReadAsObject(); - - var accountInfo = content["AccountInfo"]; - Assert.Equal("NewNickName1", accountInfo["NickName"]); - Assert.Equal(40, accountInfo["Age"]); - - Assert.Equal("Male", (string)accountInfo["Gender"]); - - var address = content["Address"]; - Assert.Equal("United States", address["CountryOrRegion"]); - - var tags = content["Tags"]; - Assert.Equal("New Value", tags["Tag1"]); - JsonAssert.DoesNotContainProperty("OwnerGender", content); - Assert.Empty(((JArray)content["ShipAddresses"])); - } - - // Arrange - string requestUri = $"/{mode}/Accounts(2)"; - - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + var json = await response.Content.ReadAsObject(); - // Assert - Assert.True(response.IsSuccessStatusCode); + var city = json.GetValue("City").ToString(); + Assert.Equal("Redmond", city); - var result = await response.Content.ReadAsObject(); + var countryOrRegion = json.GetValue("CountryOrRegion").ToString(); + Assert.Equal("US", countryOrRegion); + } - var updatedAccountinfo = result["AccountInfo"]; - Assert.Equal("NewNickName1", updatedAccountinfo["NickName"]); - Assert.Equal(40, updatedAccountinfo["Age"]); - Assert.Equal("Male", updatedAccountinfo["Gender"]); + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task QueryOpenComplexTypePropertyTags(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); + string requestUri = $"{mode}/Accounts(1)/Tags"; - var updatedAddress = result["Address"]; - Assert.Equal("United States", updatedAddress["CountryOrRegion"]); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - var updatedTags = result["Tags"]; - Assert.Equal("New Value", updatedTags["Tag1"]); - } + // Assert + Assert.True(response.IsSuccessStatusCode); - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task PutEntityWithOpenComplexType(string mode) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); + var json = await response.Content.ReadAsObject(); - string putUri = $"{mode}/Accounts(2)"; - var putContent = JObject.Parse(@"{'Id':2,'Name':'NewName2', - 'AccountInfo':{'NickName':'NewNickName1','Age':11,'Gender@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Gender','Gender':'Male'}, - 'Address':{'City':'Redmond','Street':'1 Microsoft Way','CountryOrRegion':'United States'}, - 'Tags':{'Tag1':'New Value'}}"); + var tag1 = json.GetValue("Tag1").ToString(); + Assert.Equal("Value 1", tag1); - // Act - using (HttpResponseMessage putResponse = await client.PutAsJsonAsync(putUri, putContent)) - { - // Assert - Assert.Equal(HttpStatusCode.OK, putResponse.StatusCode); + var tag2 = json.GetValue("Tag2").ToString(); + Assert.Equal("Value 2", tag2); + } - var content = await putResponse.Content.ReadAsObject(); + [Theory] + [InlineData("application/json;odata.metadata=full")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=none")] + public async Task QueryNonDynamicProperty(string format) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); + string requestUri = $"attributeRouting/Accounts(1)/Address/City?$format=" + format; - var accountInfo = content["AccountInfo"]; - Assert.Equal("NewNickName1", accountInfo["NickName"]); - Assert.Equal(11, accountInfo["Age"]); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.Equal("Male", accountInfo["Gender"]); + // Assert + Assert.True(response.IsSuccessStatusCode); - var address = content["Address"]; - Assert.Equal("United States", address["CountryOrRegion"]); + var json = await response.Content.ReadAsObject(); + var city = json.GetValue("value").ToString(); + Assert.Equal("Redmond", city); + } - var tags = content["Tags"]; - Assert.Equal("New Value", tags["Tag1"]); - } + #region Update - // Arrange - string requestUri = putUri; + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task PatchEntityWithOpenComplexType(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); + + string patchUri = $"{mode}/Accounts(2)"; + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), patchUri); + string payload = @"{ + '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Account', + 'AccountInfo':{'NickName':'NewNickName1','Age':40,'Gender': 'Male'}, + 'Address':{'CountryOrRegion':'United States'}, + 'Tags':{'Tag1':'New Value'}, + 'ShipAddresses@odata.type':'#Collection(Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Address)', + 'ShipAddresses':[], + 'OwnerGender@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Gender', + 'OwnerGender':null + }"; + request.Content = new StringContent(payload); + + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + // Act + using (var patchResponse = await client.SendAsync(request)) + { + // Assert + Assert.Equal(HttpStatusCode.OK, patchResponse.StatusCode); - HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.True(response.IsSuccessStatusCode); + var content = await patchResponse.Content.ReadAsObject(); - // Act - var result = await response.Content.ReadAsObject(); + var accountInfo = content["AccountInfo"]; + Assert.Equal("NewNickName1", accountInfo["NickName"]); + Assert.Equal(40, accountInfo["Age"]); - // Assert - var updatedAccountinfo = result["AccountInfo"]; - Assert.Equal("NewNickName1", updatedAccountinfo["NickName"]); - Assert.Equal(11, updatedAccountinfo["Age"]); + Assert.Equal("Male", (string)accountInfo["Gender"]); - var updatedAddress = result["Address"]; - Assert.Equal("United States", updatedAddress["CountryOrRegion"]); + var address = content["Address"]; + Assert.Equal("United States", address["CountryOrRegion"]); - var updatedTags = result["Tags"]; - Assert.Equal("New Value", updatedTags["Tag1"]); + var tags = content["Tags"]; + Assert.Equal("New Value", tags["Tag1"]); + JsonAssert.DoesNotContainProperty("OwnerGender", content); + Assert.Empty(((JArray)content["ShipAddresses"])); } - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task PatchOpenComplexTypeProperty(string mode) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); + // Arrange + string requestUri = $"/{mode}/Accounts(2)"; - // Get ~/Accounts(1)/Address - var requestUri = $"{mode}/Accounts(1)/Address"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - // Act - var response = await client.SendAsync(request); + // Assert + Assert.True(response.IsSuccessStatusCode); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var content = await response.Content.ReadAsObject(); - Assert.Equal(5, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties - Assert.Equal("Redmond", content["City"]); - Assert.Equal("1 Microsoft Way", content["Street"]); - Assert.Equal("US", content["CountryCode"]); - Assert.Equal("US", content["CountryOrRegion"]); // dynamic property - - // Patch ~/Accounts(1)/Address - request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - request.Content = new StringContent( - @"{ - '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress', - 'City':'NewCity', - 'OtherProperty@odata.type':'#Date', - 'OtherProperty':'2016-02-01' - }"); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - - response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + var result = await response.Content.ReadAsObject(); - // Get ~/Accounts(1)/Address - request = new HttpRequestMessage(HttpMethod.Get, requestUri); - response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - content = await response.Content.ReadAsObject(); - Assert.Equal(7, content.Count); // @odata.context + 3 declared properties + 2 dynamic properties + 1 @odata.type - Assert.Equal("NewCity", content["City"]); // updated - Assert.Equal("1 Microsoft Way", content["Street"]); - Assert.Equal("US", content["CountryCode"]); - Assert.Equal("US", content["CountryOrRegion"]); - Assert.Equal("2016-02-01", content["OtherProperty"]); - } + var updatedAccountinfo = result["AccountInfo"]; + Assert.Equal("NewNickName1", updatedAccountinfo["NickName"]); + Assert.Equal(40, updatedAccountinfo["Age"]); + Assert.Equal("Male", updatedAccountinfo["Gender"]); - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task PatchOpenComplexTypeProperty_WithDifferentType(string mode) - { - HttpClient client = CreateClient(); - await ResetDatasource(client); - - // Get ~/Accounts(1)/Address - var requestUri = $"{mode}/Accounts(1)/Address"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var content = await response.Content.ReadAsObject(); - Assert.Equal(5, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties - Assert.Equal("Redmond", content["City"]); - Assert.Equal("1 Microsoft Way", content["Street"]); - Assert.Equal("US", content["CountryCode"]); - Assert.Equal("US", content["CountryOrRegion"]); // dynamic property - - // Patch ~/Accounts(1)/Address - request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - request.Content = new StringContent( - @"{ - '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Address', - 'City':'NewCity', - 'OtherProperty@odata.type':'#Date', - 'OtherProperty':'2016-02-01' - }"); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - - response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + var updatedAddress = result["Address"]; + Assert.Equal("United States", updatedAddress["CountryOrRegion"]); - // Get ~/Accounts(1)/Address - request = new HttpRequestMessage(HttpMethod.Get, requestUri); - response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - content = await response.Content.ReadAsObject(); - Assert.Equal(6, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties + 1 @odata.type - Assert.Equal("NewCity", content["City"]); // updated - Assert.Equal("1 Microsoft Way", content["Street"]); - - Assert.Equal("US", content["CountryOrRegion"]); - Assert.Equal("2016-02-01", content["OtherProperty"]); - } + var updatedTags = result["Tags"]; + Assert.Equal("New Value", updatedTags["Tag1"]); + } - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task PatchOpenDerivedComplexTypeProperty(string mode) + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task PutEntityWithOpenComplexType(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); + + string putUri = $"{mode}/Accounts(2)"; + var putContent = JObject.Parse(@"{'Id':2,'Name':'NewName2', + 'AccountInfo':{'NickName':'NewNickName1','Age':11,'Gender@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Gender','Gender':'Male'}, + 'Address':{'City':'Redmond','Street':'1 Microsoft Way','CountryOrRegion':'United States'}, + 'Tags':{'Tag1':'New Value'}}"); + + // Act + using (HttpResponseMessage putResponse = await client.PutAsJsonAsync(putUri, putContent)) { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); + // Assert + Assert.Equal(HttpStatusCode.OK, putResponse.StatusCode); - // Get ~/Accounts(1)/Address - var requestUri = $"{mode}/Accounts(1)/Address/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var content = await putResponse.Content.ReadAsObject(); - // Act - var response = await client.SendAsync(request); + var accountInfo = content["AccountInfo"]; + Assert.Equal("NewNickName1", accountInfo["NickName"]); + Assert.Equal(11, accountInfo["Age"]); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var content = await response.Content.ReadAsObject(); - Assert.Equal(5, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties - Assert.Equal("Redmond", content["City"]); - Assert.Equal("1 Microsoft Way", content["Street"]); - Assert.Equal("US", content["CountryCode"]); - Assert.Equal("US", content["CountryOrRegion"]); - - // Arrange - // Patch ~/Accounts(1)/Address/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress - request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - request.Content = new StringContent( - @"{ - '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress', - 'CountryCode':'NewCountryCode', - 'CountryOrRegion':'NewCountry' - }"); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - - // Act - response = await client.SendAsync(request); + Assert.Equal("Male", accountInfo["Gender"]); - // Assert - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + var address = content["Address"]; + Assert.Equal("United States", address["CountryOrRegion"]); - // Arrange - // Get ~/Accounts(1)/Address/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress - request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var tags = content["Tags"]; + Assert.Equal("New Value", tags["Tag1"]); + } - // Act - response = await client.SendAsync(request); + // Arrange + string requestUri = putUri; - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - content = await response.Content.ReadAsObject(); - Assert.Equal(5, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties - Assert.Equal("Redmond", content["City"]); - Assert.Equal("1 Microsoft Way", content["Street"]); - Assert.Equal("NewCountryCode", content["CountryCode"]); // updated - Assert.Equal("NewCountry", content["CountryOrRegion"]); // updated - } + HttpResponseMessage response = await client.GetAsync(requestUri); + Assert.True(response.IsSuccessStatusCode); - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task PutOpenComplexTypeProperty(string mode) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); + // Act + var result = await response.Content.ReadAsObject(); - // Get ~/Accounts(1)/Address - var requestUri = $"{mode}/Accounts(1)/Address"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + // Assert + var updatedAccountinfo = result["AccountInfo"]; + Assert.Equal("NewNickName1", updatedAccountinfo["NickName"]); + Assert.Equal(11, updatedAccountinfo["Age"]); - // Act - var response = await client.SendAsync(request); + var updatedAddress = result["Address"]; + Assert.Equal("United States", updatedAddress["CountryOrRegion"]); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var content = await response.Content.ReadAsObject(); - Assert.Equal(5, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties - Assert.Equal("Redmond", content["City"]); - Assert.Equal("1 Microsoft Way", content["Street"]); - Assert.Equal("US", content["CountryCode"]); - Assert.Equal("US", content["CountryOrRegion"]); // dynamic property - - // Put ~/Accounts(1)/Address - request = new HttpRequestMessage(HttpMethod.Put, requestUri); - request.Content = new StringContent( - @"{ - '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Address', - 'City':'NewCity', - 'Street':'NewStreet', - 'OtherProperty@odata.type':'#Date', - 'OtherProperty':'2016-02-01' - }"); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - - // Act - response = await client.SendAsync(request); + var updatedTags = result["Tags"]; + Assert.Equal("New Value", updatedTags["Tag1"]); + } - // Assert - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task PatchOpenComplexTypeProperty(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); + + // Get ~/Accounts(1)/Address + var requestUri = $"{mode}/Accounts(1)/Address"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsObject(); + Assert.Equal(5, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties + Assert.Equal("Redmond", content["City"]); + Assert.Equal("1 Microsoft Way", content["Street"]); + Assert.Equal("US", content["CountryCode"]); + Assert.Equal("US", content["CountryOrRegion"]); // dynamic property + + // Patch ~/Accounts(1)/Address + request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + request.Content = new StringContent( + @"{ + '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress', + 'City':'NewCity', + 'OtherProperty@odata.type':'#Date', + 'OtherProperty':'2016-02-01' + }"); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // Get ~/Accounts(1)/Address + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + content = await response.Content.ReadAsObject(); + Assert.Equal(7, content.Count); // @odata.context + 3 declared properties + 2 dynamic properties + 1 @odata.type + Assert.Equal("NewCity", content["City"]); // updated + Assert.Equal("1 Microsoft Way", content["Street"]); + Assert.Equal("US", content["CountryCode"]); + Assert.Equal("US", content["CountryOrRegion"]); + Assert.Equal("2016-02-01", content["OtherProperty"]); + } - // Arrange - // Get ~/Accounts(1)/Address - request = new HttpRequestMessage(HttpMethod.Get, requestUri); + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task PatchOpenComplexTypeProperty_WithDifferentType(string mode) + { + HttpClient client = CreateClient(); + await ResetDatasource(client); + + // Get ~/Accounts(1)/Address + var requestUri = $"{mode}/Accounts(1)/Address"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsObject(); + Assert.Equal(5, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties + Assert.Equal("Redmond", content["City"]); + Assert.Equal("1 Microsoft Way", content["Street"]); + Assert.Equal("US", content["CountryCode"]); + Assert.Equal("US", content["CountryOrRegion"]); // dynamic property + + // Patch ~/Accounts(1)/Address + request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + request.Content = new StringContent( + @"{ + '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Address', + 'City':'NewCity', + 'OtherProperty@odata.type':'#Date', + 'OtherProperty':'2016-02-01' + }"); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // Get ~/Accounts(1)/Address + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + content = await response.Content.ReadAsObject(); + Assert.Equal(6, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties + 1 @odata.type + Assert.Equal("NewCity", content["City"]); // updated + Assert.Equal("1 Microsoft Way", content["Street"]); + + Assert.Equal("US", content["CountryOrRegion"]); + Assert.Equal("2016-02-01", content["OtherProperty"]); + } - // Act - response = await client.SendAsync(request); + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task PatchOpenDerivedComplexTypeProperty(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); + + // Get ~/Accounts(1)/Address + var requestUri = $"{mode}/Accounts(1)/Address/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsObject(); + Assert.Equal(5, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties + Assert.Equal("Redmond", content["City"]); + Assert.Equal("1 Microsoft Way", content["Street"]); + Assert.Equal("US", content["CountryCode"]); + Assert.Equal("US", content["CountryOrRegion"]); + + // Arrange + // Patch ~/Accounts(1)/Address/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress + request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + request.Content = new StringContent( + @"{ + '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress', + 'CountryCode':'NewCountryCode', + 'CountryOrRegion':'NewCountry' + }"); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // Arrange + // Get ~/Accounts(1)/Address/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + content = await response.Content.ReadAsObject(); + Assert.Equal(5, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties + Assert.Equal("Redmond", content["City"]); + Assert.Equal("1 Microsoft Way", content["Street"]); + Assert.Equal("NewCountryCode", content["CountryCode"]); // updated + Assert.Equal("NewCountry", content["CountryOrRegion"]); // updated + } - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - content = await response.Content.ReadAsObject(); - Assert.Equal(6, content.Count); // @odata.context + 3 declared properties + 2 new dynamic properties - Assert.Equal("NewCity", content["City"]); // updated - Assert.Equal("NewStreet", content["Street"]); // updated - Assert.Equal("US", content["CountryCode"]); - Assert.Null(content["CountryOrRegion"]); - Assert.Equal("2016-02-01", content["OtherProperty"]); - } - #endregion + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task PutOpenComplexTypeProperty(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); + + // Get ~/Accounts(1)/Address + var requestUri = $"{mode}/Accounts(1)/Address"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsObject(); + Assert.Equal(5, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties + Assert.Equal("Redmond", content["City"]); + Assert.Equal("1 Microsoft Way", content["Street"]); + Assert.Equal("US", content["CountryCode"]); + Assert.Equal("US", content["CountryOrRegion"]); // dynamic property + + // Put ~/Accounts(1)/Address + request = new HttpRequestMessage(HttpMethod.Put, requestUri); + request.Content = new StringContent( + @"{ + '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Address', + 'City':'NewCity', + 'Street':'NewStreet', + 'OtherProperty@odata.type':'#Date', + 'OtherProperty':'2016-02-01' + }"); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // Arrange + // Get ~/Accounts(1)/Address + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + content = await response.Content.ReadAsObject(); + Assert.Equal(6, content.Count); // @odata.context + 3 declared properties + 2 new dynamic properties + Assert.Equal("NewCity", content["City"]); // updated + Assert.Equal("NewStreet", content["Street"]); // updated + Assert.Equal("US", content["CountryCode"]); + Assert.Null(content["CountryOrRegion"]); + Assert.Equal("2016-02-01", content["OtherProperty"]); + } + #endregion - #region Insert + #region Insert - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task InsertEntityWithOpenComplexTypeProperty(string mode) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task InsertEntityWithOpenComplexTypeProperty(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); - var postUri = $"{mode}/Accounts"; + var postUri = $"{mode}/Accounts"; - var postContent = JObject.Parse( + var postContent = JObject.Parse( @"{ - 'Id':4, - 'Name':'Name4', - 'AccountInfo': - { - 'NickName':'NickName4','Age':40,'Gender@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Gender','Gender':'Male' - }, - 'Address': - { - '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress', - 'City':'London','Street':'Baker street','CountryOrRegion':'UnitedKindom','CountryCode':'Code' - }, - 'Tags':{'Tag1':'Value 1','Tag2':'Value 2'}, - 'AnotherGender@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Gender', - 'AnotherGender':'Female' +'Id':4, +'Name':'Name4', +'AccountInfo': +{ + 'NickName':'NickName4','Age':40,'Gender@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Gender','Gender':'Male' +}, +'Address': +{ + '@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GlobalAddress', + 'City':'London','Street':'Baker street','CountryOrRegion':'UnitedKindom','CountryCode':'Code' +}, +'Tags':{'Tag1':'Value 1','Tag2':'Value 2'}, +'AnotherGender@odata.type':'#Microsoft.AspNetCore.OData.E2E.Tests.OpenType.Gender', +'AnotherGender':'Female' }"); - using (HttpResponseMessage response = await client.PostAsJsonAsync(postUri, postContent)) - { - Assert.Equal(HttpStatusCode.Created, response.StatusCode); + using (HttpResponseMessage response = await client.PostAsJsonAsync(postUri, postContent)) + { + Assert.Equal(HttpStatusCode.Created, response.StatusCode); - var json = await response.Content.ReadAsObject(); + var json = await response.Content.ReadAsObject(); - var age = json["AccountInfo"]["Age"]; - Assert.Equal(40, age); + var age = json["AccountInfo"]["Age"]; + Assert.Equal(40, age); - var gender = (string)json["AccountInfo"]["Gender"]; - Assert.Equal("Male", gender); + var gender = (string)json["AccountInfo"]["Gender"]; + Assert.Equal("Male", gender); - var countryOrRegion = json["Address"]["CountryOrRegion"]; - Assert.Equal("UnitedKindom", countryOrRegion); + var countryOrRegion = json["Address"]["CountryOrRegion"]; + Assert.Equal("UnitedKindom", countryOrRegion); - var countryCode = json["Address"]["CountryCode"]; - Assert.Equal("Code", countryCode); + var countryCode = json["Address"]["CountryCode"]; + Assert.Equal("Code", countryCode); - var tag1 = json["Tags"]["Tag1"]; - Assert.Equal("Value 1", tag1); - var tag2 = json["Tags"]["Tag2"]; - Assert.Equal("Value 2", tag2); - var anotherGender = (string)json["AnotherGender"]; - Assert.Equal("Female", anotherGender); - } + var tag1 = json["Tags"]["Tag1"]; + Assert.Equal("Value 1", tag1); + var tag2 = json["Tags"]["Tag2"]; + Assert.Equal("Value 2", tag2); + var anotherGender = (string)json["AnotherGender"]; + Assert.Equal("Female", anotherGender); } + } - #endregion - - #region Delete - - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task DeleteEntityWithOpenComplexTypeProperty(string mode) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); - - var deleteUri = $"/{mode}/Accounts(1)"; - - // Act & Assert - using (HttpResponseMessage response = await client.DeleteAsync(deleteUri)) - { - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } - - // Arrange - var requestUri = $"/{mode}/Accounts?$format={1}"; + #endregion - // Act & Assert - using (HttpResponseMessage response = await client.GetAsync(requestUri)) - { - response.EnsureSuccessStatusCode(); + #region Delete - var json = await response.Content.ReadAsObject(); + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task DeleteEntityWithOpenComplexTypeProperty(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); - var results = json.GetValue("value") as JArray; - Assert.Equal(2, results.Count); - } - } + var deleteUri = $"/{mode}/Accounts(1)"; - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task DeleteOpenComplexTypeProperty(string mode) + // Act & Assert + using (HttpResponseMessage response = await client.DeleteAsync(deleteUri)) { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } - // Get ~/Accounts(1)/Address - var requestUri = $"/{mode}/Accounts(1)/Address"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + // Arrange + var requestUri = $"/{mode}/Accounts?$format={1}"; - // Act - var response = await client.SendAsync(request); + // Act & Assert + using (HttpResponseMessage response = await client.GetAsync(requestUri)) + { + response.EnsureSuccessStatusCode(); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var content = await response.Content.ReadAsObject(); - Assert.Equal(5, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties - Assert.Equal("Redmond", content["City"]); - Assert.Equal("1 Microsoft Way", content["Street"]); - Assert.Equal("US", content["CountryCode"]); - Assert.Equal("US", content["CountryOrRegion"]); // dynamic property - - // Arrange & Act & Assert - // Delete ~/Accounts(1)/Address - request = new HttpRequestMessage(HttpMethod.Delete, requestUri); - response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + var json = await response.Content.ReadAsObject(); - // Arrange & Act & Assert - // Get ~/Accounts(1)/Address - request = new HttpRequestMessage(HttpMethod.Get, requestUri); - response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + var results = json.GetValue("value") as JArray; + Assert.Equal(2, results.Count); } - #endregion + } - #region Function & Action + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task DeleteOpenComplexTypeProperty(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); + + // Get ~/Accounts(1)/Address + var requestUri = $"/{mode}/Accounts(1)/Address"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsObject(); + Assert.Equal(5, content.Count); // @odata.context + 3 declared properties + 1 dynamic properties + Assert.Equal("Redmond", content["City"]); + Assert.Equal("1 Microsoft Way", content["Street"]); + Assert.Equal("US", content["CountryCode"]); + Assert.Equal("US", content["CountryOrRegion"]); // dynamic property + + // Arrange & Act & Assert + // Delete ~/Accounts(1)/Address + request = new HttpRequestMessage(HttpMethod.Delete, requestUri); + response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // Arrange & Act & Assert + // Get ~/Accounts(1)/Address + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + #endregion - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task GetAddressFunction(string mode) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); + #region Function & Action - string requestUri = $"/{mode}/Accounts(1)/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GetAddressFunction()"; + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task GetAddressFunction(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); - HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.True(response.IsSuccessStatusCode); + string requestUri = $"/{mode}/Accounts(1)/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.GetAddressFunction()"; - var json = await response.Content.ReadAsObject(); + HttpResponseMessage response = await client.GetAsync(requestUri); + Assert.True(response.IsSuccessStatusCode); - var city = json.GetValue("City").ToString(); - Assert.Equal("Redmond", city); + var json = await response.Content.ReadAsObject(); - var countryOrRegion = json.GetValue("CountryOrRegion"); - Assert.Equal("US", countryOrRegion); + var city = json.GetValue("City").ToString(); + Assert.Equal("Redmond", city); - var countryCode = json.GetValue("CountryCode"); - Assert.Equal("US", countryCode); - } + var countryOrRegion = json.GetValue("CountryOrRegion"); + Assert.Equal("US", countryOrRegion); - [Theory] - [InlineData("convention")] - [InlineData("attributeRouting")] - public async Task IncreaseAgeAction(string mode) - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); + var countryCode = json.GetValue("CountryCode"); + Assert.Equal("US", countryCode); + } - string requestUri = $"/{mode}/Accounts(1)/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.IncreaseAgeAction"; - var requestForPost = new HttpRequestMessage(HttpMethod.Post, requestUri); - requestForPost.Content = new StringContent(string.Empty); - requestForPost.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + [Theory] + [InlineData("convention")] + [InlineData("attributeRouting")] + public async Task IncreaseAgeAction(string mode) + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); - HttpResponseMessage response = await client.SendAsync(requestForPost); - Assert.True(response.IsSuccessStatusCode); + string requestUri = $"/{mode}/Accounts(1)/Microsoft.AspNetCore.OData.E2E.Tests.OpenType.IncreaseAgeAction"; + var requestForPost = new HttpRequestMessage(HttpMethod.Post, requestUri); + requestForPost.Content = new StringContent(string.Empty); + requestForPost.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - var json = await response.Content.ReadAsObject(); + HttpResponseMessage response = await client.SendAsync(requestForPost); + Assert.True(response.IsSuccessStatusCode); - var nickName = json.GetValue("NickName").ToString(); - Assert.Equal("NickName1", nickName); + var json = await response.Content.ReadAsObject(); - var age = json.GetValue("Age"); - Assert.Equal(11, age); - } + var nickName = json.GetValue("NickName").ToString(); + Assert.Equal("NickName1", nickName); - [Fact] - public async Task TestRoutes() - { - // Arrange - string requestUri = "$odata"; - HttpClient client = CreateClient(); + var age = json.GetValue("Age"); + Assert.Equal(11, age); + } - // Act - var response = await client.GetAsync(requestUri); + [Fact] + public async Task TestRoutes() + { + // Arrange + string requestUri = "$odata"; + HttpClient client = CreateClient(); - // Assert - response.EnsureSuccessStatusCode(); - string payload = await response.Content.ReadAsStringAsync(); - } + // Act + var response = await client.GetAsync(requestUri); - [Fact] - public async Task UpdateAddressAction() - { - // Arrange - HttpClient client = CreateClient(); - await ResetDatasource(client); + // Assert + response.EnsureSuccessStatusCode(); + string payload = await response.Content.ReadAsStringAsync(); + } - string uri = "/attributeRouting/UpdateAddressAction"; - var content = new { Address = new { Street = "Street 11", City = "City 11", CountryOrRegion = "CountryOrRegion 11" }, ID = 1 }; + [Fact] + public async Task UpdateAddressAction() + { + // Arrange + HttpClient client = CreateClient(); + await ResetDatasource(client); - var response = await client.PostAsJsonAsync(uri, content); - Assert.True(response.IsSuccessStatusCode); + string uri = "/attributeRouting/UpdateAddressAction"; + var content = new { Address = new { Street = "Street 11", City = "City 11", CountryOrRegion = "CountryOrRegion 11" }, ID = 1 }; - string getUri = "/attributeRouting/Accounts(1)"; + var response = await client.PostAsJsonAsync(uri, content); + Assert.True(response.IsSuccessStatusCode); - HttpResponseMessage getResponse = await client.GetAsync(getUri); - Assert.True(getResponse.IsSuccessStatusCode); + string getUri = "/attributeRouting/Accounts(1)"; - var result = await getResponse.Content.ReadAsObject(); + HttpResponseMessage getResponse = await client.GetAsync(getUri); + Assert.True(getResponse.IsSuccessStatusCode); - var city = result["Address"]["City"].ToString(); - Assert.Equal("City 11", city); - var country = result["Address"]["CountryOrRegion"].ToString(); - Assert.Equal("CountryOrRegion 11", country); - } - #endregion - private async Task ResetDatasource(HttpClient client) - { - var uriReset = "ResetDataSource"; - var response = await client.PostAsync(uriReset, null); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - return response; - } + var result = await getResponse.Content.ReadAsObject(); + + var city = result["Address"]["City"].ToString(); + Assert.Equal("City 11", city); + var country = result["Address"]["CountryOrRegion"].ToString(); + Assert.Equal("CountryOrRegion 11", country); + } + #endregion + private async Task ResetDatasource(HttpClient client) + { + var uriReset = "ResetDataSource"; + var response = await client.PostAsync(uriReset, null); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + return response; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ParameterAlias/ParameterAliasDataSource.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ParameterAlias/ParameterAliasDataSource.cs index 3b8291042..edf42d737 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ParameterAlias/ParameterAliasDataSource.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ParameterAlias/ParameterAliasDataSource.cs @@ -7,48 +7,47 @@ using System.ComponentModel.DataAnnotations; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias +namespace Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias; + +/// +/// entity type +/// +public class Trade { - /// - /// entity type - /// - public class Trade - { - [Key] - public int TradeID { get; set; } + [Key] + public int TradeID { get; set; } - public string ProductName { get; set; } + public string ProductName { get; set; } - public string Description { get; set; } + public string Description { get; set; } - public long? TradingVolume { get; set; } + public long? TradingVolume { get; set; } - public CountryOrRegion PortingCountryOrRegion { get; set; } + public CountryOrRegion PortingCountryOrRegion { get; set; } - public TradeLocation TradeLocation { get; set; } - } + public TradeLocation TradeLocation { get; set; } +} - /// - /// enum type - /// - public enum CountryOrRegion - { - Australia, +/// +/// enum type +/// +public enum CountryOrRegion +{ + Australia, - USA, + USA, - Canada, + Canada, - Italy - } + Italy +} - /// - /// complex type - /// - public class TradeLocation - { - public string City { get; set; } +/// +/// complex type +/// +public class TradeLocation +{ + public string City { get; set; } - public int ZipCode { get; set; } - } + public int ZipCode { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ParameterAlias/ParameterAliasTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ParameterAlias/ParameterAliasTest.cs index b10a7425e..f9781f049 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ParameterAlias/ParameterAliasTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ParameterAlias/ParameterAliasTest.cs @@ -16,132 +16,131 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias +namespace Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias; + +public class ParameterAliasTest : WebApiTestBase { - public class ParameterAliasTest : WebApiTestBase + public ParameterAliasTest(WebApiTestFixture fixture) + :base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(TradesController)); + + services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null) + .AddRouteComponents(GetModel())); + } + + private static IEdmModel GetModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + EntitySetConfiguration tradesConfiguration = builder.EntitySet("Trades"); + + //Add bound function + var boundFunction = tradesConfiguration.EntityType.Collection.Function("GetTradingVolume"); + boundFunction.Parameter("productName"); + boundFunction.Parameter("PortingCountryOrRegion"); + boundFunction.Returns(); + + //Add bound function + boundFunction = tradesConfiguration.EntityType.Collection.Function("GetTopTrading"); + boundFunction.Parameter("productName"); + boundFunction.ReturnsFromEntitySet("Trades"); + boundFunction.IsComposable = true; + + //Add unbound function + var unboundFunction = builder.Function("GetTradeByCountry"); + unboundFunction.Parameter("PortingCountryOrRegion"); + unboundFunction.ReturnsCollectionFromEntitySet("Trades"); + + builder.Namespace = typeof(CountryOrRegion).Namespace; + + return builder.GetEdmModel(); + } + + #region Test + [Fact] + public async Task ParameterAliasInFunctionCall() + { + //Unbound function + string query = "/GetTradeByCountry(PortingCountryOrRegion=@p1)?@p1=Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias.CountryOrRegion'USA'"; + HttpClient client = CreateClient(); + + HttpResponseMessage response = await client.GetAsync(query); + var json = await response.Content.ReadAsObject(); + var result = json["value"] as JArray; + Assert.Equal(3, result.Count); + + //Bound function + string requestUri = "/Trades/Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias.GetTradingVolume(productName=@p1,PortingCountryOrRegion=@p2)?@p1='Rice'&@p2=Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias.CountryOrRegion'USA'"; + response = await client.GetAsync(requestUri); + json = await response.Content.ReadAsObject(); + Assert.Equal(1000, (long)json["value"]); + } + + [Theory] + [InlineData("?$filter=contains(@p1, @p2)&@p1=Description&@p2='Export'", 3)] //Reference property and primitive type + [InlineData("?@p1=startswith(Description,'Import')&$filter=@p1", 3)] //Reference expression + [InlineData("?$filter=TradingVolume eq @p1", 1)] //Reference nullable value + public async Task ParameterAliasInFilter(string queryOption, int expectedResult) + { + string requestBaseUri = "/Trades"; + HttpClient client = CreateClient(); + + HttpResponseMessage response = await client.GetAsync(requestBaseUri + queryOption); + + var json = await response.Content.ReadAsObject(); + var result = json["value"] as JArray; + Assert.Equal(expectedResult, result.Count); + } + + [Theory] + [InlineData("?$orderby=@p1&@p1=PortingCountryOrRegion", "Australia")] + [InlineData("?$orderby=ProductName,@p2 desc,PortingCountryOrRegion desc&@p2=TradingVolume", "USA")] + public async Task ParameterAliasInOrderby(string queryOption, string expectedPortingCountry) + { + string requestBaseUri = "/Trades"; + HttpClient client = CreateClient(); + + HttpResponseMessage response = await client.GetAsync(requestBaseUri + queryOption); + + var json = await response.Content.ReadAsObject(); + var result = json["value"] as JArray; + Assert.Equal(expectedPortingCountry, result.First["PortingCountryOrRegion"]); + Assert.Equal("Corn", result.First["ProductName"]); + Assert.Equal(8000, result.First["TradingVolume"]); + } + + [Theory] + //Use multi times in different place + [InlineData("/GetTradeByCountry(PortingCountryOrRegion=@p1)?@p1=Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias.CountryOrRegion'USA'&$filter=PortingCountryOrRegion eq @p1 and @p2 gt 1000&$orderby=@p2&@p2=TradingVolume", 1, 0)] + //Reference property under complex type + [InlineData("/Trades?$filter=@p1 gt 0&$orderby=@p1&@p1=TradeLocation/ZipCode", 3, 1)] + public async Task MiscParameterAlias(string queryUri, int expectedEntryCount, int expectedZipCode) { - public ParameterAliasTest(WebApiTestFixture fixture) - :base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(TradesController)); - - services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null) - .AddRouteComponents(GetModel())); - } - - private static IEdmModel GetModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - EntitySetConfiguration tradesConfiguration = builder.EntitySet("Trades"); - - //Add bound function - var boundFunction = tradesConfiguration.EntityType.Collection.Function("GetTradingVolume"); - boundFunction.Parameter("productName"); - boundFunction.Parameter("PortingCountryOrRegion"); - boundFunction.Returns(); - - //Add bound function - boundFunction = tradesConfiguration.EntityType.Collection.Function("GetTopTrading"); - boundFunction.Parameter("productName"); - boundFunction.ReturnsFromEntitySet("Trades"); - boundFunction.IsComposable = true; - - //Add unbound function - var unboundFunction = builder.Function("GetTradeByCountry"); - unboundFunction.Parameter("PortingCountryOrRegion"); - unboundFunction.ReturnsCollectionFromEntitySet("Trades"); - - builder.Namespace = typeof(CountryOrRegion).Namespace; - - return builder.GetEdmModel(); - } - - #region Test - [Fact] - public async Task ParameterAliasInFunctionCall() - { - //Unbound function - string query = "/GetTradeByCountry(PortingCountryOrRegion=@p1)?@p1=Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias.CountryOrRegion'USA'"; - HttpClient client = CreateClient(); - - HttpResponseMessage response = await client.GetAsync(query); - var json = await response.Content.ReadAsObject(); - var result = json["value"] as JArray; - Assert.Equal(3, result.Count); - - //Bound function - string requestUri = "/Trades/Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias.GetTradingVolume(productName=@p1,PortingCountryOrRegion=@p2)?@p1='Rice'&@p2=Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias.CountryOrRegion'USA'"; - response = await client.GetAsync(requestUri); - json = await response.Content.ReadAsObject(); - Assert.Equal(1000, (long)json["value"]); - } - - [Theory] - [InlineData("?$filter=contains(@p1, @p2)&@p1=Description&@p2='Export'", 3)] //Reference property and primitive type - [InlineData("?@p1=startswith(Description,'Import')&$filter=@p1", 3)] //Reference expression - [InlineData("?$filter=TradingVolume eq @p1", 1)] //Reference nullable value - public async Task ParameterAliasInFilter(string queryOption, int expectedResult) - { - string requestBaseUri = "/Trades"; - HttpClient client = CreateClient(); - - HttpResponseMessage response = await client.GetAsync(requestBaseUri + queryOption); - - var json = await response.Content.ReadAsObject(); - var result = json["value"] as JArray; - Assert.Equal(expectedResult, result.Count); - } - - [Theory] - [InlineData("?$orderby=@p1&@p1=PortingCountryOrRegion", "Australia")] - [InlineData("?$orderby=ProductName,@p2 desc,PortingCountryOrRegion desc&@p2=TradingVolume", "USA")] - public async Task ParameterAliasInOrderby(string queryOption, string expectedPortingCountry) - { - string requestBaseUri = "/Trades"; - HttpClient client = CreateClient(); - - HttpResponseMessage response = await client.GetAsync(requestBaseUri + queryOption); - - var json = await response.Content.ReadAsObject(); - var result = json["value"] as JArray; - Assert.Equal(expectedPortingCountry, result.First["PortingCountryOrRegion"]); - Assert.Equal("Corn", result.First["ProductName"]); - Assert.Equal(8000, result.First["TradingVolume"]); - } - - [Theory] - //Use multi times in different place - [InlineData("/GetTradeByCountry(PortingCountryOrRegion=@p1)?@p1=Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias.CountryOrRegion'USA'&$filter=PortingCountryOrRegion eq @p1 and @p2 gt 1000&$orderby=@p2&@p2=TradingVolume", 1, 0)] - //Reference property under complex type - [InlineData("/Trades?$filter=@p1 gt 0&$orderby=@p1&@p1=TradeLocation/ZipCode", 3, 1)] - public async Task MiscParameterAlias(string queryUri, int expectedEntryCount, int expectedZipCode) - { - HttpClient client = CreateClient(); - - HttpResponseMessage response = await client.GetAsync(queryUri); - - var json = await response.Content.ReadAsObject(); - var result = json["value"] as JArray; - Assert.Equal(expectedEntryCount, result.Count); - Assert.Equal(expectedZipCode, result.First["TradeLocation"]["ZipCode"]); - } - - [Fact] - public async Task ParameterAliasWithUnresolvedPathSegment() - { - HttpClient client = CreateClient(); - - var queryUri = "/Trades/Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias.GetTopTrading(productName=@p1)/unknown?@p1='Corn'"; - var response = await client.GetAsync(queryUri); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - - //var json = await response.Content.ReadAsObject(); - //Assert.Equal("Corn", (string)json["value"]); - } - #endregion + HttpClient client = CreateClient(); + + HttpResponseMessage response = await client.GetAsync(queryUri); + + var json = await response.Content.ReadAsObject(); + var result = json["value"] as JArray; + Assert.Equal(expectedEntryCount, result.Count); + Assert.Equal(expectedZipCode, result.First["TradeLocation"]["ZipCode"]); + } + + [Fact] + public async Task ParameterAliasWithUnresolvedPathSegment() + { + HttpClient client = CreateClient(); + + var queryUri = "/Trades/Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias.GetTopTrading(productName=@p1)/unknown?@p1='Corn'"; + var response = await client.GetAsync(queryUri); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + //var json = await response.Content.ReadAsObject(); + //Assert.Equal("Corn", (string)json["value"]); } + #endregion } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ParameterAlias/TradesController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ParameterAlias/TradesController.cs index 568757f06..097da2451 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ParameterAlias/TradesController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ParameterAlias/TradesController.cs @@ -16,202 +16,201 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias +namespace Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias; + +public class TradesController : ODataController { - public class TradesController : ODataController + public TradesController() { - public TradesController() + if (null == Trades) { - if (null == Trades) - { - InitCustomers(); - } + InitCustomers(); } + } + + private static List Trades = null; + + private void InitCustomers() + { + Trades = new List() + { + new Trade() + { + TradeID = 1, + ProductName = "Rice", + Description = "Export Rice to USA", + PortingCountryOrRegion = CountryOrRegion.USA, + TradingVolume = 1000, + TradeLocation = new TradeLocation() + { + City = "Guangzhou", + ZipCode = 010 + } + }, + new Trade() + { + TradeID = 2, + ProductName = "Wheat", + Description = "Export Wheat to USA", + PortingCountryOrRegion = CountryOrRegion.USA, + TradingVolume = null, + TradeLocation = new TradeLocation() + { + City = "Shenzhen", + ZipCode = 100 + } + }, + new Trade() + { + TradeID = 3, + ProductName = "Wheat", + Description = "Export Wheat to Italy", + PortingCountryOrRegion = CountryOrRegion.Italy, + TradingVolume = 2000, + TradeLocation = new TradeLocation() + { + City = "Shanghai", + ZipCode = 001 + } + }, + new Trade() + { + TradeID = 4, + ProductName = "Corn", + Description = "Import Corn from USA", + PortingCountryOrRegion = CountryOrRegion.USA, + TradingVolume = 8000, + TradeLocation = new TradeLocation() + { + City = "Beijing", + ZipCode = 000 + } + }, + new Trade() + { + TradeID = 5, + ProductName = "Corn", + Description = "Import Corn from Australia", + PortingCountryOrRegion = CountryOrRegion.Australia, + TradingVolume = 8000, + TradeLocation = new TradeLocation() + { + City = "Beijing", + ZipCode = 000 + } + }, + new Trade() + { + TradeID = 6, + ProductName = "Corn", + Description = "Import Corn from Canada", + PortingCountryOrRegion = CountryOrRegion.Canada, + TradingVolume = 6000, + TradeLocation = new TradeLocation() + { + City = "Beijing", + ZipCode = 000 + } + } + }; + } - private static List Trades = null; + [EnableQuery] + public IActionResult Get() + { + return Ok(Trades.AsQueryable()); + } - private void InitCustomers() + [HttpGet] + public IActionResult HandleUnmappedRequest(ODataPath odataPath) + { + var functionSegment = odataPath.ElementAt(1) as OperationSegment; + if (functionSegment != null) { - Trades = new List() - { - new Trade() - { - TradeID = 1, - ProductName = "Rice", - Description = "Export Rice to USA", - PortingCountryOrRegion = CountryOrRegion.USA, - TradingVolume = 1000, - TradeLocation = new TradeLocation() - { - City = "Guangzhou", - ZipCode = 010 - } - }, - new Trade() - { - TradeID = 2, - ProductName = "Wheat", - Description = "Export Wheat to USA", - PortingCountryOrRegion = CountryOrRegion.USA, - TradingVolume = null, - TradeLocation = new TradeLocation() - { - City = "Shenzhen", - ZipCode = 100 - } - }, - new Trade() - { - TradeID = 3, - ProductName = "Wheat", - Description = "Export Wheat to Italy", - PortingCountryOrRegion = CountryOrRegion.Italy, - TradingVolume = 2000, - TradeLocation = new TradeLocation() - { - City = "Shanghai", - ZipCode = 001 - } - }, - new Trade() - { - TradeID = 4, - ProductName = "Corn", - Description = "Import Corn from USA", - PortingCountryOrRegion = CountryOrRegion.USA, - TradingVolume = 8000, - TradeLocation = new TradeLocation() - { - City = "Beijing", - ZipCode = 000 - } - }, - new Trade() - { - TradeID = 5, - ProductName = "Corn", - Description = "Import Corn from Australia", - PortingCountryOrRegion = CountryOrRegion.Australia, - TradingVolume = 8000, - TradeLocation = new TradeLocation() - { - City = "Beijing", - ZipCode = 000 - } - }, - new Trade() - { - TradeID = 6, - ProductName = "Corn", - Description = "Import Corn from Canada", - PortingCountryOrRegion = CountryOrRegion.Canada, - TradingVolume = 6000, - TradeLocation = new TradeLocation() - { - City = "Beijing", - ZipCode = 000 - } - } - }; + return Ok(GetParameterValue(functionSegment, "productName") as string); } - - [EnableQuery] - public IActionResult Get() + else { - return Ok(Trades.AsQueryable()); + return BadRequest(); } + } + + [HttpGet("Trades/Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias.GetTradingVolume(productName={productName},PortingCountryOrRegion={PortingCountryOrRegion})")] + public IActionResult GetTradingVolume([FromODataUri]string productName, [FromODataUri]CountryOrRegion portingCountryOrRegion) + { + var trades = Trades.Where(t => t.ProductName == productName && t.PortingCountryOrRegion == portingCountryOrRegion).ToArray(); + long? tradingVolume = 0; - [HttpGet] - public IActionResult HandleUnmappedRequest(ODataPath odataPath) + foreach (var trade in trades) { - var functionSegment = odataPath.ElementAt(1) as OperationSegment; - if (functionSegment != null) - { - return Ok(GetParameterValue(functionSegment, "productName") as string); - } - else - { - return BadRequest(); - } + tradingVolume += trade.TradingVolume; } + return Ok(tradingVolume); + } - [HttpGet("Trades/Microsoft.AspNetCore.OData.E2E.Tests.ParameterAlias.GetTradingVolume(productName={productName},PortingCountryOrRegion={PortingCountryOrRegion})")] - public IActionResult GetTradingVolume([FromODataUri]string productName, [FromODataUri]CountryOrRegion portingCountryOrRegion) - { - var trades = Trades.Where(t => t.ProductName == productName && t.PortingCountryOrRegion == portingCountryOrRegion).ToArray(); - long? tradingVolume = 0; + [EnableQuery] + [HttpGet("GetTradeByCountry(PortingCountryOrRegion={CountryOrRegion})")] + public IActionResult GetTradeByCountry([FromODataUri] CountryOrRegion countryOrRegion) + { + var trades = Trades.Where(t => t.PortingCountryOrRegion == countryOrRegion).ToList(); + return Ok(trades); + } - foreach (var trade in trades) - { - tradingVolume += trade.TradingVolume; - } - return Ok(tradingVolume); + private static object GetParameterValue(OperationSegment segment, string paramName) + { + if (segment == null) + { + throw Error.ArgumentNull("segment"); } - [EnableQuery] - [HttpGet("GetTradeByCountry(PortingCountryOrRegion={CountryOrRegion})")] - public IActionResult GetTradeByCountry([FromODataUri] CountryOrRegion countryOrRegion) + if (string.IsNullOrEmpty(paramName)) { - var trades = Trades.Where(t => t.PortingCountryOrRegion == countryOrRegion).ToList(); - return Ok(trades); + throw Error.ArgumentNullOrEmpty("parameterName"); } - private static object GetParameterValue(OperationSegment segment, string paramName) + if (!segment.Operations.Any() || !segment.Operations.First().IsFunction()) { - if (segment == null) - { - throw Error.ArgumentNull("segment"); - } - - if (string.IsNullOrEmpty(paramName)) - { - throw Error.ArgumentNullOrEmpty("parameterName"); - } + throw Error.Argument("segment"); + } - if (!segment.Operations.Any() || !segment.Operations.First().IsFunction()) - { - throw Error.Argument("segment"); - } + OperationSegmentParameter parameter = segment.Parameters.FirstOrDefault(p => p.Name == paramName); + Assert.NotNull(parameter); - OperationSegmentParameter parameter = segment.Parameters.FirstOrDefault(p => p.Name == paramName); - Assert.NotNull(parameter); + ConstantNode node = parameter.Value as ConstantNode; + if (node != null) + { + return node.Value; + } - ConstantNode node = parameter.Value as ConstantNode; - if (node != null) - { - return node.Value; - } + return TranslateNode(parameter.Value); + } - return TranslateNode(parameter.Value); + internal static object TranslateNode(object value) + { + if (value == null) + { + return null; } - internal static object TranslateNode(object value) + ConstantNode node = value as ConstantNode; + if (node != null) { - if (value == null) - { - return null; - } - - ConstantNode node = value as ConstantNode; - if (node != null) - { - return node.Value; - } - - ConvertNode convertNode = value as ConvertNode; - if (convertNode != null) - { - object source = TranslateNode(convertNode.Source); - return source; - } + return node.Value; + } - ParameterAliasNode parameterAliasNode = value as ParameterAliasNode; - if (parameterAliasNode != null) - { - return parameterAliasNode.Alias; - } + ConvertNode convertNode = value as ConvertNode; + if (convertNode != null) + { + object source = TranslateNode(convertNode.Source); + return source; + } - throw new NotSupportedException(); + ParameterAliasNode parameterAliasNode = value as ParameterAliasNode; + if (parameterAliasNode != null) + { + return parameterAliasNode.Alias; } + + throw new NotSupportedException(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/PropertyNameCaseSensitive/PropertyNameCaseSensitiveControllers.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/PropertyNameCaseSensitive/PropertyNameCaseSensitiveControllers.cs index ea4147ba9..7958ed4aa 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/PropertyNameCaseSensitive/PropertyNameCaseSensitiveControllers.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/PropertyNameCaseSensitive/PropertyNameCaseSensitiveControllers.cs @@ -13,75 +13,74 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.PropertyNameCaseSensitive +namespace Microsoft.AspNetCore.OData.E2E.Tests.PropertyNameCaseSensitive; + +public class BillsController : ODataController { - public class BillsController : ODataController + [EnableQuery] + public IActionResult Post([FromBody]Bill bill) + { + Assert.NotNull(bill); + + // Let's verify the 'bill' from the "Post" test case + Assert.Equal(921, bill.ID); + Assert.Equal("Fan", bill.Name); + Assert.Equal(Frequency.BiWeekly, bill.Frequency); + Assert.Equal(3.14, bill.Weight); + + Assert.Equal(new Guid("21EC2020-3AEA-1069-A2DD-08002B30309D"), bill.ContactGuid); + + Assert.NotNull(bill.HomeAddress); + Assert.Equal("MyStreet", bill.HomeAddress.Street); + Assert.Equal("MyCity", bill.HomeAddress.City); + + Assert.NotNull(bill.Addresses); + Assert.Equal(2, bill.Addresses.Count); + + Assert.Equal("Street-1", bill.Addresses[0].Street); + Assert.Equal("City-1", bill.Addresses[0].City); + + Assert.Equal("Street-2", bill.Addresses[1].Street); + Assert.Equal("City-2", bill.Addresses[1].City); + + return Ok(true); + } + + [EnableQuery] + public IActionResult Patch(int key, Delta delta) { - [EnableQuery] - public IActionResult Post([FromBody]Bill bill) - { - Assert.NotNull(bill); - - // Let's verify the 'bill' from the "Post" test case - Assert.Equal(921, bill.ID); - Assert.Equal("Fan", bill.Name); - Assert.Equal(Frequency.BiWeekly, bill.Frequency); - Assert.Equal(3.14, bill.Weight); - - Assert.Equal(new Guid("21EC2020-3AEA-1069-A2DD-08002B30309D"), bill.ContactGuid); - - Assert.NotNull(bill.HomeAddress); - Assert.Equal("MyStreet", bill.HomeAddress.Street); - Assert.Equal("MyCity", bill.HomeAddress.City); - - Assert.NotNull(bill.Addresses); - Assert.Equal(2, bill.Addresses.Count); - - Assert.Equal("Street-1", bill.Addresses[0].Street); - Assert.Equal("City-1", bill.Addresses[0].City); - - Assert.Equal("Street-2", bill.Addresses[1].Street); - Assert.Equal("City-2", bill.Addresses[1].City); - - return Ok(true); - } - - [EnableQuery] - public IActionResult Patch(int key, Delta delta) - { - Assert.Equal(2, key); - - // Let's verify the 'delta' from the "Patch" test case - Assert.True(delta.TryGetPropertyValue("Frequency", out object frequency)); - Assert.Equal(Frequency.BiWeekly, frequency); - - Assert.True(delta.TryGetPropertyValue("ContactGuid", out object contractGuid)); - Assert.Equal(new Guid("21EC2020-3AEA-1069-A2DD-08002B30309D"), contractGuid); - - Assert.True(delta.TryGetPropertyValue("Weight", out object weight)); - Assert.Equal(6.24, weight); - - Assert.True(delta.TryGetPropertyValue("HomeAddress", out object homeAddressObj)); - Delta
homeAddress = Assert.IsType>(homeAddressObj); - Address originalHomeAddress = new Address(); - homeAddress.Patch(originalHomeAddress); - Assert.Equal("YouStreet", originalHomeAddress.Street); - Assert.Equal("YouCity", originalHomeAddress.City); - - Assert.True(delta.TryGetPropertyValue("Addresses", out object addressesObj)); - Assert.Collection((IList
)addressesObj, - e => - { - Assert.Equal("Street-3", e.Street); - Assert.Equal("City-3", e.City); - }, - e => - { - Assert.Equal("Street-4", e.Street); - Assert.Equal("City-4", e.City); - }); - - return Ok(false); - } + Assert.Equal(2, key); + + // Let's verify the 'delta' from the "Patch" test case + Assert.True(delta.TryGetPropertyValue("Frequency", out object frequency)); + Assert.Equal(Frequency.BiWeekly, frequency); + + Assert.True(delta.TryGetPropertyValue("ContactGuid", out object contractGuid)); + Assert.Equal(new Guid("21EC2020-3AEA-1069-A2DD-08002B30309D"), contractGuid); + + Assert.True(delta.TryGetPropertyValue("Weight", out object weight)); + Assert.Equal(6.24, weight); + + Assert.True(delta.TryGetPropertyValue("HomeAddress", out object homeAddressObj)); + Delta
homeAddress = Assert.IsType>(homeAddressObj); + Address originalHomeAddress = new Address(); + homeAddress.Patch(originalHomeAddress); + Assert.Equal("YouStreet", originalHomeAddress.Street); + Assert.Equal("YouCity", originalHomeAddress.City); + + Assert.True(delta.TryGetPropertyValue("Addresses", out object addressesObj)); + Assert.Collection((IList
)addressesObj, + e => + { + Assert.Equal("Street-3", e.Street); + Assert.Equal("City-3", e.City); + }, + e => + { + Assert.Equal("Street-4", e.Street); + Assert.Equal("City-4", e.City); + }); + + return Ok(false); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/PropertyNameCaseSensitive/PropertyNameCaseSensitiveDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/PropertyNameCaseSensitive/PropertyNameCaseSensitiveDataModel.cs index 03e698328..e19b3386f 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/PropertyNameCaseSensitive/PropertyNameCaseSensitiveDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/PropertyNameCaseSensitive/PropertyNameCaseSensitiveDataModel.cs @@ -8,53 +8,52 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.PropertyNameCaseSensitive +namespace Microsoft.AspNetCore.OData.E2E.Tests.PropertyNameCaseSensitive; + +public class Bill { - public class Bill - { - public int ID { get; set; } + public int ID { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public Frequency Frequency { get; set; } + public Frequency Frequency { get; set; } - public Guid ContactGuid { get; set; } + public Guid ContactGuid { get; set; } - public Double Weight { get; set; } + public Double Weight { get; set; } - public Address HomeAddress { get; set; } + public Address HomeAddress { get; set; } - public IList
Addresses { get; set; } + public IList
Addresses { get; set; } - public BillDetail BillDetail { get; set; } + public BillDetail BillDetail { get; set; } - public IList Details { get; set; } - } + public IList Details { get; set; } +} - public enum Frequency - { - Once, +public enum Frequency +{ + Once, - BiWeekly, + BiWeekly, - Monthly, + Monthly, - Yealy - } + Yealy +} - public class Address - { - public string Street { get; set; } +public class Address +{ + public string Street { get; set; } - public string City { get; set; } - } + public string City { get; set; } +} - public class BillDetail - { - public int Id { get; set; } +public class BillDetail +{ + public int Id { get; set; } - public string Title { get; set; } + public string Title { get; set; } - public IList DimensionInCentimeter { get; set; } - } + public IList DimensionInCentimeter { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/PropertyNameCaseSensitive/PropertyNameCaseSensitiveTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/PropertyNameCaseSensitive/PropertyNameCaseSensitiveTest.cs index b6bc6455b..39a99ed3d 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/PropertyNameCaseSensitive/PropertyNameCaseSensitiveTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/PropertyNameCaseSensitive/PropertyNameCaseSensitiveTest.cs @@ -16,150 +16,149 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.PropertyNameCaseSensitive +namespace Microsoft.AspNetCore.OData.E2E.Tests.PropertyNameCaseSensitive; + +/// +/// This test class contains cases about property name case-senstive or case-insensitive. +/// The library supports property name case-insensitive by default. +/// +public class PropertyNameCaseSensitiveTest : WebApiTestBase { - /// - /// This test class contains cases about property name case-senstive or case-insensitive. - /// The library supports property name case-insensitive by default. - /// - public class PropertyNameCaseSensitiveTest : WebApiTestBase + public PropertyNameCaseSensitiveTest(WebApiTestFixture fixture) + : base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = GetEdmModel(); + services.ConfigureControllers(typeof(MetadataController), typeof(BillsController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel)); + } + + public static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Bills"); + var edmModel = builder.GetEdmModel(); + return edmModel; + } + + [Fact] + public async Task PropertyNameCaseSensitive_QueryMetadata_WorksAsExpected() + { + // Arrange + var requestUri = "odata/$metadata"; + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(HttpStatusCode.OK == response.StatusCode, + string.Format("Response status code, expected: {0}, actual: {1}, request url: {2}", + HttpStatusCode.OK, response.StatusCode, requestUri)); + + Assert.Contains("", responseString); + Assert.Contains("", responseString); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task PropertyNameCaseSensitive_Post_UsingDifferentCase_WorksAsExpected(bool caseSensitive) { - public PropertyNameCaseSensitiveTest(WebApiTestFixture fixture) - : base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = GetEdmModel(); - services.ConfigureControllers(typeof(MetadataController), typeof(BillsController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel)); - } - - public static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Bills"); - var edmModel = builder.GetEdmModel(); - return edmModel; - } - - [Fact] - public async Task PropertyNameCaseSensitive_QueryMetadata_WorksAsExpected() - { - // Arrange - var requestUri = "odata/$metadata"; - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(HttpStatusCode.OK == response.StatusCode, - string.Format("Response status code, expected: {0}, actual: {1}, request url: {2}", - HttpStatusCode.OK, response.StatusCode, requestUri)); - - Assert.Contains("", responseString); - Assert.Contains("", responseString); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task PropertyNameCaseSensitive_Post_UsingDifferentCase_WorksAsExpected(bool caseSensitive) - { - // Arrange - string id = caseSensitive ? "ID" : "id"; - string name = caseSensitive ? "Name" : "nAme"; - string frequency = caseSensitive ? "Frequency" : "frequency"; - string contactGuid = caseSensitive ? "ContactGuid" : "contactguid"; - string weight = caseSensitive ? "Weight" : "weight"; - string homeAddress = caseSensitive ? "HomeAddress" : "homeADDRESS"; - string addresses = caseSensitive ? "Addresses" : "addresses"; - - // in the controller, we will verify the following content to make sure it works fine. - // if you change the content, please change the verification codes in the controller as well. - string content = $@" + // Arrange + string id = caseSensitive ? "ID" : "id"; + string name = caseSensitive ? "Name" : "nAme"; + string frequency = caseSensitive ? "Frequency" : "frequency"; + string contactGuid = caseSensitive ? "ContactGuid" : "contactguid"; + string weight = caseSensitive ? "Weight" : "weight"; + string homeAddress = caseSensitive ? "HomeAddress" : "homeADDRESS"; + string addresses = caseSensitive ? "Addresses" : "addresses"; + + // in the controller, we will verify the following content to make sure it works fine. + // if you change the content, please change the verification codes in the controller as well. + string content = $@" {{ - '{id}':921, - '{name}':'Fan', - '{frequency}': 'BiWeekly', - '{contactGuid}': '21EC2020-3AEA-1069-A2DD-08002B30309D', - '{weight}': 3.14, - '{homeAddress}': {{ 'Street':'MyStreet','City':'MyCity'}}, - '{addresses}':[ - {{ 'Street':'Street-1','City':'City-1'}}, - {{ 'Street':'Street-2','City':'City-2'}} - ] +'{id}':921, +'{name}':'Fan', +'{frequency}': 'BiWeekly', +'{contactGuid}': '21EC2020-3AEA-1069-A2DD-08002B30309D', +'{weight}': 3.14, +'{homeAddress}': {{ 'Street':'MyStreet','City':'MyCity'}}, +'{addresses}':[ + {{ 'Street':'Street-1','City':'City-1'}}, + {{ 'Street':'Street-2','City':'City-2'}} +] }}"; - var requestUri = "odata/Bills"; - HttpClient client = CreateClient(); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Content = new StringContent(content); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = content.Length; - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(HttpStatusCode.OK == response.StatusCode, - string.Format("Response status code, expected: {0}, actual: {1}, request url: {2}", - HttpStatusCode.OK, response.StatusCode, requestUri)); - - string responseString = await response.Content.ReadAsStringAsync(); - - Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Edm.Boolean\",\"value\":true}", responseString); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task PropertyNameCaseSensitive_Patch_UsingDifferentCase_WorksAsExpected(bool caseSensitive) - { - // Arrange - string frequency = caseSensitive ? "Frequency" : "frequency"; - string contactGuid = caseSensitive ? "ContactGuid" : "contactguid"; - string weight = caseSensitive ? "Weight" : "weight"; - string homeAddress = caseSensitive ? "HomeAddress" : "homeADDRESS"; - string addresses = caseSensitive ? "Addresses" : "addresses"; - - // in the controller, we will verify the following content to make sure it works fine. - // if you change the content, please change the verification codes in the controller as well. - string content = $@" + var requestUri = "odata/Bills"; + HttpClient client = CreateClient(); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Content = new StringContent(content); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = content.Length; + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(HttpStatusCode.OK == response.StatusCode, + string.Format("Response status code, expected: {0}, actual: {1}, request url: {2}", + HttpStatusCode.OK, response.StatusCode, requestUri)); + + string responseString = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Edm.Boolean\",\"value\":true}", responseString); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task PropertyNameCaseSensitive_Patch_UsingDifferentCase_WorksAsExpected(bool caseSensitive) + { + // Arrange + string frequency = caseSensitive ? "Frequency" : "frequency"; + string contactGuid = caseSensitive ? "ContactGuid" : "contactguid"; + string weight = caseSensitive ? "Weight" : "weight"; + string homeAddress = caseSensitive ? "HomeAddress" : "homeADDRESS"; + string addresses = caseSensitive ? "Addresses" : "addresses"; + + // in the controller, we will verify the following content to make sure it works fine. + // if you change the content, please change the verification codes in the controller as well. + string content = $@" {{ - '{frequency}': 'BiWeekly', - '{contactGuid}': '21EC2020-3AEA-1069-A2DD-08002B30309D', - '{weight}': 6.24, - '{homeAddress}': {{ 'Street':'YouStreet','City':'YouCity'}}, - '{addresses}':[ - {{ 'Street':'Street-3','City':'City-3'}}, - {{ 'Street':'Street-4','City':'City-4'}} - ] +'{frequency}': 'BiWeekly', +'{contactGuid}': '21EC2020-3AEA-1069-A2DD-08002B30309D', +'{weight}': 6.24, +'{homeAddress}': {{ 'Street':'YouStreet','City':'YouCity'}}, +'{addresses}':[ + {{ 'Street':'Street-3','City':'City-3'}}, + {{ 'Street':'Street-4','City':'City-4'}} +] }}"; - var requestUri = "odata/Bills/2"; - HttpClient client = CreateClient(); + var requestUri = "odata/Bills/2"; + HttpClient client = CreateClient(); - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Patch, requestUri); - request.Content = new StringContent(content); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = content.Length; + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Patch, requestUri); + request.Content = new StringContent(content); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = content.Length; - // Act - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.True(HttpStatusCode.OK == response.StatusCode, - string.Format("Response status code, expected: {0}, actual: {1}, request url: {2}", - HttpStatusCode.OK, response.StatusCode, requestUri)); + // Assert + Assert.True(HttpStatusCode.OK == response.StatusCode, + string.Format("Response status code, expected: {0}, actual: {1}, request url: {2}", + HttpStatusCode.OK, response.StatusCode, requestUri)); - string responseString = await response.Content.ReadAsStringAsync(); + string responseString = await response.Content.ReadAsStringAsync(); - Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Edm.Boolean\",\"value\":false}", responseString); - } + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Edm.Boolean\",\"value\":false}", responseString); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultController.cs index 991eaee61..d143149c5 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultController.cs @@ -10,28 +10,27 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Query; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ActionResult +namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ActionResult; + +public class CustomersController : ControllerBase { - public class CustomersController : ControllerBase + [HttpGet] + [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Expand | AllowedQueryOptions.Filter)] + public async Task>> Get() { - [HttpGet] - [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Expand | AllowedQueryOptions.Filter)] - public async Task>> Get() - { - return await Task.FromResult(new List - { - new Customer + return await Task.FromResult(new List + { + new Customer + { + Id = "CustId", + Books = new List { - Id = "CustId", - Books = new List + new Book { - new Book - { - Id = "BookId", - }, + Id = "BookId", }, }, - }); - } + }, + }); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultEdmModel.cs index 17cc7584c..dc1599980 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultEdmModel.cs @@ -8,15 +8,14 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ActionResult +namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ActionResult; + +public class ActionResultEdmModel { - public class ActionResultEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultODataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultODataModel.cs index 67086c9fb..0daa83f20 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultODataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultODataModel.cs @@ -7,17 +7,16 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ActionResult +namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ActionResult; + +public class Customer { - public class Customer - { - public string Id { get; set; } + public string Id { get; set; } - public IEnumerable Books { get; set; } - } + public IEnumerable Books { get; set; } +} - public class Book - { - public string Id { get; set; } - } +public class Book +{ + public string Id { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultTests.cs index f50909cb7..010840563 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ActionResult/ActionResultTests.cs @@ -17,106 +17,105 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ActionResult +namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ActionResult; + +/// +/// EnableQuery attribute works correctly when controller returns ActionResult. +/// +public class ActionResultTests : WebODataTestBase { /// - /// EnableQuery attribute works correctly when controller returns ActionResult. + /// Startup class. /// - public class ActionResultTests : WebODataTestBase + public class Startup : TestStartupBase { - /// - /// Startup class. - /// - public class Startup : TestStartupBase + public override void ConfigureServices(IServiceCollection services) { - public override void ConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(CustomersController)); + services.ConfigureControllers(typeof(CustomersController)); - IEdmModel model = ActionResultEdmModel.GetEdmModel(); - services.AddControllers().AddOData(options => options.AddRouteComponents("odata", model).SetMaxTop(2).Expand().Select().OrderBy().Filter()); - } + IEdmModel model = ActionResultEdmModel.GetEdmModel(); + services.AddControllers().AddOData(options => options.AddRouteComponents("odata", model).SetMaxTop(2).Expand().Select().OrderBy().Filter()); } + } - public ActionResultTests(WebODataTestFixture fixture) - : base(fixture) - { - } + public ActionResultTests(WebODataTestFixture fixture) + : base(fixture) + { + } - /// - /// For OData paths enable query should work with expansion. - /// - /// Task tracking operation. - [Fact] - public async Task ActionResultODataPathReturnsExpansion() - { - // Arrange - string queryUrl = "odata/Customers?$expand=books"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + /// + /// For OData paths enable query should work with expansion. + /// + /// Task tracking operation. + [Fact] + public async Task ActionResultODataPathReturnsExpansion() + { + // Arrange + string queryUrl = "odata/Customers?$expand=books"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - // Act - HttpResponseMessage response = await this.Client.SendAsync(request); + // Act + HttpResponseMessage response = await this.Client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); - Assert.Single(customers); - Assert.Equal("CustId", customers.First().Id); - Assert.Single(customers.First().Books); - Assert.Equal("BookId", customers.First().Books.First().Id); - } + Assert.Single(customers); + Assert.Equal("CustId", customers.First().Id); + Assert.Single(customers.First().Books); + Assert.Equal("BookId", customers.First().Books.First().Id); + } - /// - /// For OData paths enable query should work without expansion. - /// - /// - [Fact] - public async Task ActionResultODataPathReturnsBaseWithoutExpansion() - { - // Arrange - string queryUrl = "odata/Customers"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + /// + /// For OData paths enable query should work without expansion. + /// + /// + [Fact] + public async Task ActionResultODataPathReturnsBaseWithoutExpansion() + { + // Arrange + string queryUrl = "odata/Customers"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - // Act - HttpResponseMessage response = await this.Client.SendAsync(request); + // Act + HttpResponseMessage response = await this.Client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); - Assert.Single(customers); - Assert.Equal("CustId", customers.First().Id); - Assert.Null(customers.First().Books); - } + Assert.Single(customers); + Assert.Equal("CustId", customers.First().Id); + Assert.Null(customers.First().Books); + } - /// - /// $filter should work with $count. - /// - [Theory] - [InlineData(1, 1)] - [InlineData(0, 0)] - public async Task ActionResultShouldAllowCountInFilter(int filterParam, int expectedItemsCount) - { - // Arrange - string queryUrl = $"odata/Customers?$filter=Books/$count eq {filterParam}"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + /// + /// $filter should work with $count. + /// + [Theory] + [InlineData(1, 1)] + [InlineData(0, 0)] + public async Task ActionResultShouldAllowCountInFilter(int filterParam, int expectedItemsCount) + { + // Arrange + string queryUrl = $"odata/Customers?$filter=Books/$count eq {filterParam}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - // Act - HttpResponseMessage response = await this.Client.SendAsync(request); + // Act + HttpResponseMessage response = await this.Client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); - Assert.Equal(expectedItemsCount, customers.Count()); + Assert.Equal(expectedItemsCount, customers.Count()); - } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryController.cs index 603573912..b9c1c800f 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryController.cs @@ -9,19 +9,18 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Query; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ConcurrentQuery +namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ConcurrentQuery; + +public class CustomersController : ControllerBase { - public class CustomersController : ControllerBase + [HttpGet] + [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Count | AllowedQueryOptions.Filter)] + public IQueryable GetCustomers() { - [HttpGet] - [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Count | AllowedQueryOptions.Filter)] - public IQueryable GetCustomers() - { - return Enumerable.Range(1, 100) - .Select(i => new Customer - { - Id = i, - }).AsQueryable(); - } + return Enumerable.Range(1, 100) + .Select(i => new Customer + { + Id = i, + }).AsQueryable(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryEdmModel.cs index 27a4bf3a6..02d42859e 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryEdmModel.cs @@ -8,15 +8,14 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ConcurrentQuery +namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ConcurrentQuery; + +public class ConcurrentQueryEdmModel { - public class ConcurrentQueryEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryODataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryODataModel.cs index 4a53d7fde..e43057458 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryODataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryODataModel.cs @@ -5,10 +5,9 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ConcurrentQuery +namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ConcurrentQuery; + +public class Customer { - public class Customer - { - public int Id { get; set; } - } + public int Id { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryTests.cs index 4e5df4598..328a1ef2a 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/ConcurrentQuery/ConcurrentQueryTests.cs @@ -18,66 +18,65 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ConcurrentQuery +namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.ConcurrentQuery; + +/// +/// EnableQuery attribute works correctly when controller returns ActionResult. +/// +public class ConcurrentQueryTests : WebODataTestBase { /// - /// EnableQuery attribute works correctly when controller returns ActionResult. + /// Startup class. /// - public class ConcurrentQueryTests : WebODataTestBase + public class Startup : TestStartupBase { - /// - /// Startup class. - /// - public class Startup : TestStartupBase + public override void ConfigureServices(IServiceCollection services) { - public override void ConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(CustomersController)); + services.ConfigureControllers(typeof(CustomersController)); - IEdmModel model = ConcurrentQueryEdmModel.GetEdmModel(); - services.AddControllers().AddOData(options => options.AddRouteComponents("odata", model).SetMaxTop(2).Expand().Select().OrderBy().Filter()); - } + IEdmModel model = ConcurrentQueryEdmModel.GetEdmModel(); + services.AddControllers().AddOData(options => options.AddRouteComponents("odata", model).SetMaxTop(2).Expand().Select().OrderBy().Filter()); } + } - public ConcurrentQueryTests(WebODataTestFixture fixture) - : base(fixture) - { - } + public ConcurrentQueryTests(WebODataTestFixture fixture) + : base(fixture) + { + } - /// - /// For OData paths enable query should work with expansion. - /// - /// Task tracking operation. - ////[Fact] - Commented out as running this test right now throws 'Concurrent reads or writes are not supported' exception - public async Task ConcurrentQueryExecutionIsThreadSafe() - { - // Arrange - // Bumping thread count to allow higher parallelization. - ThreadPool.SetMinThreads(100, 100); + /// + /// For OData paths enable query should work with expansion. + /// + /// Task tracking operation. + ////[Fact] - Commented out as running this test right now throws 'Concurrent reads or writes are not supported' exception + public async Task ConcurrentQueryExecutionIsThreadSafe() + { + // Arrange + // Bumping thread count to allow higher parallelization. + ThreadPool.SetMinThreads(100, 100); - // Act - var results = await Task.WhenAll( - Enumerable.Range(1, 100) - .Select(async i => - { - string queryUrl = string.Format("odata/Customers?$filter=Id gt {0}", i); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + // Act + var results = await Task.WhenAll( + Enumerable.Range(1, 100) + .Select(async i => + { + string queryUrl = string.Format("odata/Customers?$filter=Id gt {0}", i); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpResponseMessage response = await this.Client.SendAsync(request); + HttpResponseMessage response = await this.Client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); - return (i: i, length: customers.Count); - })); + return (i: i, length: customers.Count); + })); - // Assert - foreach (var result in results) - { - Assert.Equal(100 - result.i, result.length); - } + // Assert + foreach (var result in results) + { + Assert.Equal(100 - result.i, result.length); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionController.cs index b68860d09..b2b836844 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionController.cs @@ -10,15 +10,14 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Query; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.QueryValidationBeforeAction +namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.QueryValidationBeforeAction; + +public class CustomersController : ControllerBase { - public class CustomersController : ControllerBase + [HttpGet] + [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Top, MaxTop = 10)] + public IEnumerable GetCustomers() { - [HttpGet] - [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Top, MaxTop = 10)] - public IEnumerable GetCustomers() - { - throw new Exception("Controller should never be invoked as query validation should fail"); - } + throw new Exception("Controller should never be invoked as query validation should fail"); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionEdmModel.cs index e0264fdc5..456e85103 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionEdmModel.cs @@ -8,15 +8,14 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.QueryValidationBeforeAction +namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.QueryValidationBeforeAction; + +public class QueryValidationBeforeActionEdmModel { - public class QueryValidationBeforeActionEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionODataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionODataModel.cs index 5e4e428e4..4f374eef9 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionODataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionODataModel.cs @@ -7,17 +7,16 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.QueryValidationBeforeAction +namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.QueryValidationBeforeAction; + +public class Customer { - public class Customer - { - public string Id { get; set; } + public string Id { get; set; } - public IEnumerable Books { get; set; } - } + public IEnumerable Books { get; set; } +} - public class Book - { - public string Id { get; set; } - } +public class Book +{ + public string Id { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionTests.cs index bfd01a8ba..39e11167d 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/QueryValidationBeforeAction/QueryValidationBeforeActionTests.cs @@ -17,48 +17,47 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.QueryValidationBeforeAction +namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.QueryValidationBeforeAction; + +/// +/// EnableQuery attribute works correctly when controller returns ActionResult. +/// +public class QueryValidationBeforeActionTests : WebODataTestBase { /// - /// EnableQuery attribute works correctly when controller returns ActionResult. + /// Startup class. /// - public class QueryValidationBeforeActionTests : WebODataTestBase + public class Startup : TestStartupBase { - /// - /// Startup class. - /// - public class Startup : TestStartupBase + public override void ConfigureServices(IServiceCollection services) { - public override void ConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(CustomersController)); + services.ConfigureControllers(typeof(CustomersController)); - IEdmModel model = QueryValidationBeforeActionEdmModel.GetEdmModel(); - services.AddControllers().AddOData(options => options.AddRouteComponents("odata", model).SetMaxTop(2).Expand().Select().OrderBy().Filter()); - } + IEdmModel model = QueryValidationBeforeActionEdmModel.GetEdmModel(); + services.AddControllers().AddOData(options => options.AddRouteComponents("odata", model).SetMaxTop(2).Expand().Select().OrderBy().Filter()); } + } - public QueryValidationBeforeActionTests(WebODataTestFixture fixture) - : base(fixture) - { - } + public QueryValidationBeforeActionTests(WebODataTestFixture fixture) + : base(fixture) + { + } - /// - /// For bad queries query execution should happen (and fail) before action being called. - /// - /// Task tracking operation. - [Fact] - public async Task QueryExecutionBeforeActionBadQuery() - { - // Arrange (Allowed top is 10, we are sending 100) - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/Customers?$top=100"); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + /// + /// For bad queries query execution should happen (and fail) before action being called. + /// + /// Task tracking operation. + [Fact] + public async Task QueryExecutionBeforeActionBadQuery() + { + // Arrange (Allowed top is 10, we are sending 100) + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/Customers?$top=100"); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - // Act - HttpResponseMessage response = await this.Client.SendAsync(request); + // Act + HttpResponseMessage response = await this.Client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/SelectWilCardOnFunction/SelectWildCardOnFunctionTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/SelectWilCardOnFunction/SelectWildCardOnFunctionTests.cs index be96323ca..28cb8f761 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/SelectWilCardOnFunction/SelectWildCardOnFunctionTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Query/SelectWilCardOnFunction/SelectWildCardOnFunctionTests.cs @@ -20,94 +20,93 @@ using System.Threading.Tasks; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.SelectWilCardOnFunction +namespace Microsoft.AspNetCore.OData.E2E.Tests.Query.SelectWilCardOnFunction; + +public sealed class SelectWildCardOnFunctionTests : WebODataTestBase { - public sealed class SelectWildCardOnFunctionTests : WebODataTestBase + public class Startup : TestStartupBase { - public class Startup : TestStartupBase + public override void ConfigureServices(IServiceCollection services) { - public override void ConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(CustomersController)); + services.ConfigureControllers(typeof(CustomersController)); - IEdmModel model = GetEdmModel(); - services.AddControllers().AddOData(opt => - { - opt.Select(); - opt.RouteOptions.EnableNonParenthesisForEmptyParameterFunction = true; - opt.AddRouteComponents("odata", model); - }); - } - - public static IEdmModel GetEdmModel() + IEdmModel model = GetEdmModel(); + services.AddControllers().AddOData(opt => { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers").EntityType - .Collection.Function("GetAllCustomers") - .ReturnsCollectionFromEntitySet("Customers"); - - return builder.GetEdmModel(); - } + opt.Select(); + opt.RouteOptions.EnableNonParenthesisForEmptyParameterFunction = true; + opt.AddRouteComponents("odata", model); + }); } - public SelectWildCardOnFunctionTests(WebODataTestFixture fixture) - : base(fixture) + public static IEdmModel GetEdmModel() { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers").EntityType + .Collection.Function("GetAllCustomers") + .ReturnsCollectionFromEntitySet("Customers"); + + return builder.GetEdmModel(); } + } - /// - /// For Select query with wildcard on Function - /// - /// - [Fact] - public async Task SelectWildCardOnFunction_Success() - { - //Arrange - string queryUrl = "odata/Customers/GetAllCustomers?$select=*"; + public SelectWildCardOnFunctionTests(WebODataTestFixture fixture) + : base(fixture) + { + } + + /// + /// For Select query with wildcard on Function + /// + /// + [Fact] + public async Task SelectWildCardOnFunction_Success() + { + //Arrange + string queryUrl = "odata/Customers/GetAllCustomers?$select=*"; - using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl)) + using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl)) + { + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + + //Act + using (HttpResponseMessage response = await this.Client.SendAsync(request)) { - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - //Act - using (HttpResponseMessage response = await this.Client.SendAsync(request)) + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + foreach(Customer c in customers) { - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); - foreach(Customer c in customers) - { - Assert.Equal("custId1", c.Id); - Assert.Equal("John", c.Name); - Assert.Equal("Active", c.Status); - } + Assert.Equal("custId1", c.Id); + Assert.Equal("John", c.Name); + Assert.Equal("Active", c.Status); } } } } +} - public class Customer - { - public string Id { get; set; } - public string Name { get; set; } - public string Status { get; set; } - } +public class Customer +{ + public string Id { get; set; } + public string Name { get; set; } + public string Status { get; set; } +} - public class CustomersController : ODataController +public class CustomersController : ODataController +{ + [HttpGet] + public IEnumerable GetAllCustomers() { - [HttpGet] - public IEnumerable GetAllCustomers() + return new List() { - return new List() + new Customer { - new Customer - { - Id = "custId1", - Name = "John", - Status = "Active" - } - }; - } + Id = "custId1", + Name = "John", + Status = "Active" + } + }; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/CustomODataQueryOptionsParser.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/CustomODataQueryOptionsParser.cs index 57a0980b4..646d1c80a 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/CustomODataQueryOptionsParser.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/CustomODataQueryOptionsParser.cs @@ -15,37 +15,36 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OData.Query; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing.QueryRequest +namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing.QueryRequest; + +public class CustomODataQueryOptionsParser : IODataQueryRequestParser { - public class CustomODataQueryOptionsParser : IODataQueryRequestParser - { - private static MediaTypeHeaderValue SupportedMediaType = MediaTypeHeaderValue.Parse("text/xml"); + private static MediaTypeHeaderValue SupportedMediaType = MediaTypeHeaderValue.Parse("text/xml"); - public bool CanParse(HttpRequest request) - { - return request.ContentType?.StartsWith(SupportedMediaType.MediaType, StringComparison.Ordinal) == true ? true : false; - } + public bool CanParse(HttpRequest request) + { + return request.ContentType?.StartsWith(SupportedMediaType.MediaType, StringComparison.Ordinal) == true ? true : false; + } - public async Task ParseAsync(HttpRequest request) + public async Task ParseAsync(HttpRequest request) + { + using (var reader = new StreamReader( + request.Body, + encoding: Encoding.UTF8, + detectEncodingFromByteOrderMarks: false, + bufferSize: 1024, + leaveOpen: true)) { - using (var reader = new StreamReader( - request.Body, - encoding: Encoding.UTF8, - detectEncodingFromByteOrderMarks: false, - bufferSize: 1024, - leaveOpen: true)) + var content = await reader.ReadToEndAsync(); + var document = XDocument.Parse(content); + var queryOptions = document.Descendants("QueryOption").Select(d => + new { - var content = await reader.ReadToEndAsync(); - var document = XDocument.Parse(content); - var queryOptions = document.Descendants("QueryOption").Select(d => - new - { - Option = d.Attribute("Option").Value, - d.Attribute("Value").Value - }); + Option = d.Attribute("Option").Value, + d.Attribute("Value").Value + }); - return string.Join("&", queryOptions.Select(d => d.Option + "=" + d.Value)); - } + return string.Join("&", queryOptions.Select(d => d.Option + "=" + d.Value)); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/CustomQueryOptionsParserTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/CustomQueryOptionsParserTests.cs index 10da099df..7234cc37c 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/CustomQueryOptionsParserTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/CustomQueryOptionsParserTests.cs @@ -17,70 +17,69 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing.QueryRequest +namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing.QueryRequest; + +public class CustomQueryOptionsParserTests : WebApiTestBase { - public class CustomQueryOptionsParserTests : WebApiTestBase + public CustomQueryOptionsParserTests(WebApiTestFixture fixture) + : base(fixture) { - public CustomQueryOptionsParserTests(WebApiTestFixture fixture) - : base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(DollarQueryCustomersController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel()).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); - services.TryAddEnumerable( - ServiceDescriptor.Singleton()); - // NOTE: The following statement also does what is expected - // services.AddSingleton(); - } + } - protected static void UpdateConfigure(IApplicationBuilder app) - { - // Add OData /$query middleware - app.UseODataQueryRequest(); + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(DollarQueryCustomersController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel()).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); + services.TryAddEnumerable( + ServiceDescriptor.Singleton()); + // NOTE: The following statement also does what is expected + // services.AddSingleton(); + } - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + protected static void UpdateConfigure(IApplicationBuilder app) + { + // Add OData /$query middleware + app.UseODataQueryRequest(); - private static IEdmModel GetEdmModel() + app.UseRouting(); + app.UseEndpoints(endpoints => { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + endpoints.MapControllers(); + }); + } - builder.EntitySet("DollarQueryCustomers"); - builder.EntitySet("DollarQueryOrders"); + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - return builder.GetEdmModel(); - } + builder.EntitySet("DollarQueryCustomers"); + builder.EntitySet("DollarQueryOrders"); - [Fact] - public async Task ODataQueryOptionsInRequestBody_ForSupportedMediaType() - { - // Arrange - string requestUri = "odata/DollarQueryCustomers/$query"; - var contentType = "text/xml"; - var queryOptionsPayload = ""; + return builder.GetEdmModel(); + } + + [Fact] + public async Task ODataQueryOptionsInRequestBody_ForSupportedMediaType() + { + // Arrange + string requestUri = "odata/DollarQueryCustomers/$query"; + var contentType = "text/xml"; + var queryOptionsPayload = ""; - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Content = new StringContent(queryOptionsPayload); - request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); - request.Content.Headers.ContentLength = queryOptionsPayload.Length; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Content = new StringContent(queryOptionsPayload); + request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); + request.Content.Headers.ContentLength = queryOptionsPayload.Length; - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); - HttpClient client = CreateClient(); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=minimal")); + HttpClient client = CreateClient(); - // Act - var response = await client.SendAsync(request); + // Act + var response = await client.SendAsync(request); - // Arrange - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("\"value\":[{\"Id\":1,\"Name\":\"Customer Name 1\"}]", - await response.Content.ReadAsStringAsync()); - } + // Arrange + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("\"value\":[{\"Id\":1,\"Name\":\"Customer Name 1\"}]", + await response.Content.ReadAsStringAsync()); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/DollarQueryCustomersController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/DollarQueryCustomersController.cs index 503391d9f..374b703c5 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/DollarQueryCustomersController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/DollarQueryCustomersController.cs @@ -12,47 +12,46 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing.QueryRequest +namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing.QueryRequest; + +public class DollarQueryCustomersController : ODataController { - public class DollarQueryCustomersController : ODataController - { - private IList customers = Enumerable.Range(0, 10).Select(i => - new DollarQueryCustomer - { - Id = i, - Name = "Customer Name " + i, - Orders = Enumerable.Range(0, i).Select(j => - new DollarQueryOrder - { - Id = i * 10 + j, - PurchaseDate = DateTime.Today.Subtract(TimeSpan.FromDays(i * 10 + j)), - Detail = "This is Order " + i * 10 + j - }).ToList(), - SpecialOrder = new DollarQueryOrder + private IList customers = Enumerable.Range(0, 10).Select(i => + new DollarQueryCustomer + { + Id = i, + Name = "Customer Name " + i, + Orders = Enumerable.Range(0, i).Select(j => + new DollarQueryOrder { - Id = i * 10, - PurchaseDate = DateTime.Today.Subtract(TimeSpan.FromDays(i * 10)), - Detail = "This is Order " + i * 10 - } - }).ToList(); - - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - public IActionResult Get() - { - return Ok(customers); - } + Id = i * 10 + j, + PurchaseDate = DateTime.Today.Subtract(TimeSpan.FromDays(i * 10 + j)), + Detail = "This is Order " + i * 10 + j + }).ToList(), + SpecialOrder = new DollarQueryOrder + { + Id = i * 10, + PurchaseDate = DateTime.Today.Subtract(TimeSpan.FromDays(i * 10)), + Detail = "This is Order " + i * 10 + } + }).ToList(); - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - public IActionResult Get(int key) - { - var customer = customers.FirstOrDefault(c => c.Id == key); + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public IActionResult Get() + { + return Ok(customers); + } - if (customer == null) - { - throw new ArgumentOutOfRangeException("key"); - } + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public IActionResult Get(int key) + { + var customer = customers.FirstOrDefault(c => c.Id == key); - return Ok(customer); + if (customer == null) + { + throw new ArgumentOutOfRangeException("key"); } + + return Ok(customer); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/DollarQueryModels.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/DollarQueryModels.cs index c03401e50..41963fb6a 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/DollarQueryModels.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/DollarQueryModels.cs @@ -8,20 +8,19 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing.QueryRequest +namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing.QueryRequest; + +public class DollarQueryCustomer { - public class DollarQueryCustomer - { - public int Id { get; set; } - public string Name { get; set; } - public IList Orders { get; set; } - public DollarQueryOrder SpecialOrder { get; set; } - } + public int Id { get; set; } + public string Name { get; set; } + public IList Orders { get; set; } + public DollarQueryOrder SpecialOrder { get; set; } +} - public class DollarQueryOrder - { - public int Id { get; set; } - public DateTimeOffset PurchaseDate { get; set; } - public string Detail { get; set; } - } +public class DollarQueryOrder +{ + public int Id { get; set; } + public DateTimeOffset PurchaseDate { get; set; } + public string Detail { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/DollarQueryTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/DollarQueryTests.cs index 5efd7c52c..cb5b07de1 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/DollarQueryTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/QueryRequest/DollarQueryTests.cs @@ -18,218 +18,217 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing.QueryRequest +namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing.QueryRequest; + +public class DollarQueryTests : WebApiTestBase { - public class DollarQueryTests : WebApiTestBase + private const string CustomersResourcePath = "odata/DollarQueryCustomers"; + private const string SingleCustomerResourcePath = "odata/DollarQueryCustomers(1)"; + private const string ApplicationJsonODataMinimalMetadataStreamingTrue = "application/json;odata.metadata=minimal;odata.streaming=true"; + private const string ApplicationJsonODataMinimalMetadataStreamingFalse = "application/json;odata.metadata=minimal;odata.streaming=false"; + + public DollarQueryTests(WebApiTestFixture fixture) + : base(fixture) { - private const string CustomersResourcePath = "odata/DollarQueryCustomers"; - private const string SingleCustomerResourcePath = "odata/DollarQueryCustomers(1)"; - private const string ApplicationJsonODataMinimalMetadataStreamingTrue = "application/json;odata.metadata=minimal;odata.streaming=true"; - private const string ApplicationJsonODataMinimalMetadataStreamingFalse = "application/json;odata.metadata=minimal;odata.streaming=false"; + } - public DollarQueryTests(WebApiTestFixture fixture) - : base(fixture) - { - } + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(DollarQueryCustomersController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel()).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(DollarQueryCustomersController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel()).Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); - } + protected static void UpdateConfigure(IApplicationBuilder app) + { + // Add OData /$query middleware + app.UseODataQueryRequest(); - protected static void UpdateConfigure(IApplicationBuilder app) + app.UseRouting(); + app.UseEndpoints(endpoints => { - // Add OData /$query middleware - app.UseODataQueryRequest(); - - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + endpoints.MapControllers(); + }); + } - public static TheoryDataSet ODataQueryOptionsData + public static TheoryDataSet ODataQueryOptionsData + { + get { - get + var odataQueryOptionsData = new TheoryDataSet(); + + foreach (var tuple in + new[]{ + new Tuple(CustomersResourcePath, "$filter=Id le 5"), + new Tuple(CustomersResourcePath, "$filter=contains(Name, '3')"), + new Tuple(CustomersResourcePath, "$orderby=Id desc"), + new Tuple(CustomersResourcePath, "$top=1"), + new Tuple(CustomersResourcePath, "$top=1&$skip=3"), + new Tuple(CustomersResourcePath, "$orderby=Id desc&top=2&skip=3"), + new Tuple(CustomersResourcePath, "$select=Id,Name"), + new Tuple(CustomersResourcePath, "$expand=Orders"), + new Tuple(CustomersResourcePath, "$select=Orders&$expand=Orders"), + new Tuple(CustomersResourcePath, "$format=" + Uri.EscapeDataString(ApplicationJsonODataMinimalMetadataStreamingFalse)), + new Tuple(CustomersResourcePath, "$expand=SpecialOrder($select=Detail)&$filter=Id le 5&$orderby=Id desc&$select=Id&$format=" + Uri.EscapeDataString(ApplicationJsonODataMinimalMetadataStreamingFalse)), + new Tuple(SingleCustomerResourcePath, "$select=Id,Name"), + new Tuple(SingleCustomerResourcePath, "$expand=Orders"), + new Tuple(SingleCustomerResourcePath, "$format=" + Uri.EscapeDataString(ApplicationJsonODataMinimalMetadataStreamingFalse)), + new Tuple(SingleCustomerResourcePath, "$expand=SpecialOrder($select=Detail)&$select=Id&$format=" + Uri.EscapeDataString(ApplicationJsonODataMinimalMetadataStreamingFalse)) + }) { - var odataQueryOptionsData = new TheoryDataSet(); - - foreach (var tuple in - new[]{ - new Tuple(CustomersResourcePath, "$filter=Id le 5"), - new Tuple(CustomersResourcePath, "$filter=contains(Name, '3')"), - new Tuple(CustomersResourcePath, "$orderby=Id desc"), - new Tuple(CustomersResourcePath, "$top=1"), - new Tuple(CustomersResourcePath, "$top=1&$skip=3"), - new Tuple(CustomersResourcePath, "$orderby=Id desc&top=2&skip=3"), - new Tuple(CustomersResourcePath, "$select=Id,Name"), - new Tuple(CustomersResourcePath, "$expand=Orders"), - new Tuple(CustomersResourcePath, "$select=Orders&$expand=Orders"), - new Tuple(CustomersResourcePath, "$format=" + Uri.EscapeDataString(ApplicationJsonODataMinimalMetadataStreamingFalse)), - new Tuple(CustomersResourcePath, "$expand=SpecialOrder($select=Detail)&$filter=Id le 5&$orderby=Id desc&$select=Id&$format=" + Uri.EscapeDataString(ApplicationJsonODataMinimalMetadataStreamingFalse)), - new Tuple(SingleCustomerResourcePath, "$select=Id,Name"), - new Tuple(SingleCustomerResourcePath, "$expand=Orders"), - new Tuple(SingleCustomerResourcePath, "$format=" + Uri.EscapeDataString(ApplicationJsonODataMinimalMetadataStreamingFalse)), - new Tuple(SingleCustomerResourcePath, "$expand=SpecialOrder($select=Detail)&$select=Id&$format=" + Uri.EscapeDataString(ApplicationJsonODataMinimalMetadataStreamingFalse)) - }) - { - odataQueryOptionsData.Add(tuple.Item1, tuple.Item2); - } - - return odataQueryOptionsData; + odataQueryOptionsData.Add(tuple.Item1, tuple.Item2); } - } - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("DollarQueryCustomers"); - builder.EntitySet("DollarQueryOrders"); - return builder.GetEdmModel(); + return odataQueryOptionsData; } + } - [Theory] - [MemberData(nameof(ODataQueryOptionsData))] - public async Task ODataQueryOptionsInRequestBody_ForSupportedMediaType(string resourcePath, string queryOptionsPayload) - { - // Arrange - string requestUri = resourcePath + "/$query"; - var contentType = "text/plain"; + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("DollarQueryCustomers"); + builder.EntitySet("DollarQueryOrders"); + return builder.GetEdmModel(); + } + + [Theory] + [MemberData(nameof(ODataQueryOptionsData))] + public async Task ODataQueryOptionsInRequestBody_ForSupportedMediaType(string resourcePath, string queryOptionsPayload) + { + // Arrange + string requestUri = resourcePath + "/$query"; + var contentType = "text/plain"; - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Content = new StringContent(queryOptionsPayload); - request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); - request.Content.Headers.ContentLength = queryOptionsPayload.Length; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Content = new StringContent(queryOptionsPayload); + request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); + request.Content.Headers.ContentLength = queryOptionsPayload.Length; - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(ApplicationJsonODataMinimalMetadataStreamingTrue)); - HttpClient client = CreateClient(); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(ApplicationJsonODataMinimalMetadataStreamingTrue)); + HttpClient client = CreateClient(); - // Act - var response = await client.SendAsync(request); + // Act + var response = await client.SendAsync(request); - // Arrange - Assert.True(response.IsSuccessStatusCode); - } + // Arrange + Assert.True(response.IsSuccessStatusCode); + } - [Fact] - public async Task ODataQueryOptionsInRequestBody_ReturnsExpectedResult() - { - // Arrange - string requestUri = CustomersResourcePath + "/$query"; - var contentType = "text/plain"; - var queryOptionsPayload = "$filter=Id eq 1"; + [Fact] + public async Task ODataQueryOptionsInRequestBody_ReturnsExpectedResult() + { + // Arrange + string requestUri = CustomersResourcePath + "/$query"; + var contentType = "text/plain"; + var queryOptionsPayload = "$filter=Id eq 1"; - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Content = new StringContent(queryOptionsPayload); - request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); - request.Content.Headers.ContentLength = queryOptionsPayload.Length; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Content = new StringContent(queryOptionsPayload); + request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); + request.Content.Headers.ContentLength = queryOptionsPayload.Length; - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(ApplicationJsonODataMinimalMetadataStreamingTrue)); - HttpClient client = CreateClient(); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(ApplicationJsonODataMinimalMetadataStreamingTrue)); + HttpClient client = CreateClient(); - // Act - var response = await client.SendAsync(request); + // Act + var response = await client.SendAsync(request); - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("\"value\":[{\"Id\":1,\"Name\":\"Customer Name 1\"}]", await response.Content.ReadAsStringAsync()); - } + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("\"value\":[{\"Id\":1,\"Name\":\"Customer Name 1\"}]", await response.Content.ReadAsStringAsync()); + } - [Fact] - public async Task ODataQueryOptionsInRequestBody_PlusQueryOptionsOnRequestUrl() - { - // Arrange - string requestUri = CustomersResourcePath + "/$query?$orderby=Id desc"; - var contentType = "text/plain"; - string payload = "$filter=Id eq 1 or Id eq 9"; + [Fact] + public async Task ODataQueryOptionsInRequestBody_PlusQueryOptionsOnRequestUrl() + { + // Arrange + string requestUri = CustomersResourcePath + "/$query?$orderby=Id desc"; + var contentType = "text/plain"; + string payload = "$filter=Id eq 1 or Id eq 9"; - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); - request.Content.Headers.ContentLength = payload.Length; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); + request.Content.Headers.ContentLength = payload.Length; - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(ApplicationJsonODataMinimalMetadataStreamingTrue)); - HttpClient client = CreateClient(); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(ApplicationJsonODataMinimalMetadataStreamingTrue)); + HttpClient client = CreateClient(); - // Act - var response = await client.SendAsync(request); + // Act + var response = await client.SendAsync(request); - // Assert - Assert.True(response.IsSuccessStatusCode); + // Assert + Assert.True(response.IsSuccessStatusCode); - Assert.Contains("\"value\":[{\"Id\":9,\"Name\":\"Customer Name 9\"},{\"Id\":1,\"Name\":\"Customer Name 1\"}]", - await response.Content.ReadAsStringAsync()); - } + Assert.Contains("\"value\":[{\"Id\":9,\"Name\":\"Customer Name 9\"},{\"Id\":1,\"Name\":\"Customer Name 1\"}]", + await response.Content.ReadAsStringAsync()); + } - [Fact] - public async Task ODataQueryOptionsInRequestBody_RepeatedOnRequestUrl() - { - // Arrange - string requestUri = CustomersResourcePath + "/$query?$filter=Id eq 1"; - var contentType = "text/plain"; - string payload = "$filter=Id eq 1"; + [Fact] + public async Task ODataQueryOptionsInRequestBody_RepeatedOnRequestUrl() + { + // Arrange + string requestUri = CustomersResourcePath + "/$query?$filter=Id eq 1"; + var contentType = "text/plain"; + string payload = "$filter=Id eq 1"; - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); - request.Content.Headers.ContentLength = payload.Length; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); + request.Content.Headers.ContentLength = payload.Length; - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(ApplicationJsonODataMinimalMetadataStreamingTrue)); - HttpClient client = CreateClient(); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(ApplicationJsonODataMinimalMetadataStreamingTrue)); + HttpClient client = CreateClient(); - // Act - var response = await client.SendAsync(request); + // Act + var response = await client.SendAsync(request); - // Assert - Assert.False(response.IsSuccessStatusCode); - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } + // Assert + Assert.False(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } - [Fact] - public async Task ODataQueryOptionsInRequestBody_ForUnsupportedMediaType() - { - // Arrange - string requestUri = CustomersResourcePath + "/$query"; - var contentType = "application/xml"; - var queryOptionsPayload = "Id le 5"; + [Fact] + public async Task ODataQueryOptionsInRequestBody_ForUnsupportedMediaType() + { + // Arrange + string requestUri = CustomersResourcePath + "/$query"; + var contentType = "application/xml"; + var queryOptionsPayload = "Id le 5"; - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Content = new StringContent(queryOptionsPayload); - request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); - request.Content.Headers.ContentLength = queryOptionsPayload.Length; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Content = new StringContent(queryOptionsPayload); + request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); + request.Content.Headers.ContentLength = queryOptionsPayload.Length; - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(ApplicationJsonODataMinimalMetadataStreamingTrue)); - HttpClient client = CreateClient(); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(ApplicationJsonODataMinimalMetadataStreamingTrue)); + HttpClient client = CreateClient(); - // Act - var response = await client.SendAsync(request); + // Act + var response = await client.SendAsync(request); - // Assert - Assert.False(response.IsSuccessStatusCode); - } + // Assert + Assert.False(response.IsSuccessStatusCode); + } - [Fact] - public async Task ODataQueryOptionsInRequestBody_Empty() - { - // Arrange - string requestUri = CustomersResourcePath + "/$query"; - var contentType = "text/plain"; + [Fact] + public async Task ODataQueryOptionsInRequestBody_Empty() + { + // Arrange + string requestUri = CustomersResourcePath + "/$query"; + var contentType = "text/plain"; - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Content = new StringContent(""); - request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); - request.Content.Headers.ContentLength = 0; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Content = new StringContent(""); + request.Content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(contentType); + request.Content.Headers.ContentLength = 0; - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(ApplicationJsonODataMinimalMetadataStreamingTrue)); - HttpClient client = CreateClient(); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(ApplicationJsonODataMinimalMetadataStreamingTrue)); + HttpClient client = CreateClient(); - // Act - var response = await client.SendAsync(request); + // Act + var response = await client.SendAsync(request); - // Assert - Assert.True(response.IsSuccessStatusCode); - } + // Assert + Assert.True(response.IsSuccessStatusCode); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/LocationHeaderTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/LocationHeaderTests.cs index 8a27de466..0f18c6d8a 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/LocationHeaderTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/LocationHeaderTests.cs @@ -19,116 +19,115 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Regressions +namespace Microsoft.AspNetCore.OData.E2E.Tests.Regressions; + +public class LocationHeaderTests : WebApiTestBase { - public class LocationHeaderTests : WebApiTestBase + public LocationHeaderTests(WebApiTestFixture fixture) + : base(fixture) { - public LocationHeaderTests(WebApiTestFixture fixture) - : base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = GetEdmModel(); - services.ConfigureControllers(typeof(HandleController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("location", edmModel)); - } - - [Fact] - public async Task CreateCustomerWithSingleKey_ReturnsCorrectLocationHeaderEscapedUri() - { - // Arrange - string payload = @"{""Name"":""abc""}"; - var postContent = new StringContent(payload); - postContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - postContent.Headers.ContentLength = payload.Length; - - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.PostAsync("location/customers", postContent); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.NotNull(response.Content); - - Assert.True(response.Headers.Contains("Location"), "The response should contain Header 'Location'"); - - string locationHeader = response.Headers.GetValues("Location").Single(); - - Assert.Equal("http://localhost/location/Customers('abc%2F$+%2F-8')", locationHeader); - } - - [Fact] - public async Task CreateOrderWithCompositeKey_ReturnsCorrectLocationHeaderEscapedUri() - { - // Arrange - string payload = @"{""Title"":""xzy""}"; - var postContent = new StringContent(payload); - postContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - postContent.Headers.ContentLength = payload.Length; - - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.PostAsync("location/orders", postContent); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.NotNull(response.Content); - - Assert.True(response.Headers.Contains("Location"), "The response should contain Header 'Location'"); - - string locationHeader = response.Headers.GetValues("Location").Single(); - - Assert.Equal("http://localhost/location/Orders(Id1='xzy%2F',Id2='%2Fxzy')", locationHeader); - } - - public static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Orders"); - return builder.GetEdmModel(); - } } - public class HandleController : ODataController + protected static void UpdateConfigureServices(IServiceCollection services) { - [HttpPost("location/customers")] - public IActionResult CreateCustomer([FromBody]LocCustomer customer) - { - customer.Id = $"{customer.Name}/$+/-8"; // insert slash middle - return Created(customer); - } - - [HttpPost("location/orders")] - public IActionResult CreateOrder([FromBody] LocOrder order) - { - order.Id1 = $"{order.Title}/"; // append slash at end - order.Id2 = $"/{order.Title}"; // insert slash at beginning - return Created(order); - } + IEdmModel edmModel = GetEdmModel(); + services.ConfigureControllers(typeof(HandleController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("location", edmModel)); } - public class LocCustomer + [Fact] + public async Task CreateCustomerWithSingleKey_ReturnsCorrectLocationHeaderEscapedUri() { - public string Id { get; set; } + // Arrange + string payload = @"{""Name"":""abc""}"; + var postContent = new StringContent(payload); + postContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + postContent.Headers.ContentLength = payload.Length; + + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.PostAsync("location/customers", postContent); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.NotNull(response.Content); + + Assert.True(response.Headers.Contains("Location"), "The response should contain Header 'Location'"); + + string locationHeader = response.Headers.GetValues("Location").Single(); + + Assert.Equal("http://localhost/location/Customers('abc%2F$+%2F-8')", locationHeader); + } + + [Fact] + public async Task CreateOrderWithCompositeKey_ReturnsCorrectLocationHeaderEscapedUri() + { + // Arrange + string payload = @"{""Title"":""xzy""}"; + var postContent = new StringContent(payload); + postContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + postContent.Headers.ContentLength = payload.Length; + + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.PostAsync("location/orders", postContent); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.NotNull(response.Content); + + Assert.True(response.Headers.Contains("Location"), "The response should contain Header 'Location'"); + + string locationHeader = response.Headers.GetValues("Location").Single(); - public string Name { get; set; } + Assert.Equal("http://localhost/location/Orders(Id1='xzy%2F',Id2='%2Fxzy')", locationHeader); } - public class LocOrder + public static IEdmModel GetEdmModel() { - [Key] - public string Id1 { get; set; } + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); + return builder.GetEdmModel(); + } +} - [Key] - public string Id2 { get; set; } +public class HandleController : ODataController +{ + [HttpPost("location/customers")] + public IActionResult CreateCustomer([FromBody]LocCustomer customer) + { + customer.Id = $"{customer.Name}/$+/-8"; // insert slash middle + return Created(customer); + } - public string Title { get; set; } + [HttpPost("location/orders")] + public IActionResult CreateOrder([FromBody] LocOrder order) + { + order.Id1 = $"{order.Title}/"; // append slash at end + order.Id2 = $"/{order.Title}"; // insert slash at beginning + return Created(order); } } + +public class LocCustomer +{ + public string Id { get; set; } + + public string Name { get; set; } +} + +public class LocOrder +{ + [Key] + public string Id1 { get; set; } + + [Key] + public string Id2 { get; set; } + + public string Title { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsController.cs index 4f3adfc9a..32e112361 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsController.cs @@ -10,87 +10,86 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.EntityFrameworkCore; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Regressions +namespace Microsoft.AspNetCore.OData.E2E.Tests.Regressions; + +public class UsersController : Controller { - public class UsersController : Controller - { - private readonly RegressionsDbContext _dbContext; + private readonly RegressionsDbContext _dbContext; - public UsersController(RegressionsDbContext dbContext) + public UsersController(RegressionsDbContext dbContext) + { + dbContext.Database.EnsureCreated(); + _dbContext = dbContext; + if (!_dbContext.Users.Any()) { - dbContext.Database.EnsureCreated(); - _dbContext = dbContext; - if (!_dbContext.Users.Any()) + _dbContext.DataFiles.Add(new DataFile { - _dbContext.DataFiles.Add(new DataFile - { - FileId = 1, - FileName = "76x473626.pdf" - }); + FileId = 1, + FileName = "76x473626.pdf" + }); - DataFile dataFile2 = new DataFile - { - FileId = 2, - FileName = "uyr65euit5.pdf" - }; - _dbContext.DataFiles.Add(dataFile2); + DataFile dataFile2 = new DataFile + { + FileId = 2, + FileName = "uyr65euit5.pdf" + }; + _dbContext.DataFiles.Add(dataFile2); - _dbContext.DataFiles.Add(new DataFile - { - FileId = 3, - FileName = "hj7x87643.pdf" - }); + _dbContext.DataFiles.Add(new DataFile + { + FileId = 3, + FileName = "hj7x87643.pdf" + }); - _dbContext.Users.Add(new User - { - UserId = 1, - Name = "Alex", - Age = 35, - DataFileRef = null, - Files = null - }); + _dbContext.Users.Add(new User + { + UserId = 1, + Name = "Alex", + Age = 35, + DataFileRef = null, + Files = null + }); - _dbContext.Users.Add(new User - { - UserId = 2, - Name = "Amanda", - Age = 29, - DataFileRef = 2, - Files = dataFile2 - }); + _dbContext.Users.Add(new User + { + UserId = 2, + Name = "Amanda", + Age = 29, + DataFileRef = 2, + Files = dataFile2 + }); - _dbContext.Users.Add(new User - { - UserId = 3, - Name = "Lara", - Age = 25, - DataFileRef = null, - Files = null - }); + _dbContext.Users.Add(new User + { + UserId = 3, + Name = "Lara", + Age = 25, + DataFileRef = null, + Files = null + }); - _dbContext.SaveChanges(); - } + _dbContext.SaveChanges(); } + } - [HttpGet] - [EnableQuery] - public IQueryable Get() - { - IQueryable data = _dbContext.Users.AsQueryable(); - return data; - } + [HttpGet] + [EnableQuery] + public IQueryable Get() + { + IQueryable data = _dbContext.Users.AsQueryable(); + return data; + } - [HttpGet] - [EnableQuery] - public IActionResult Get(int key) + [HttpGet] + [EnableQuery] + public IActionResult Get(int key) + { + User data = _dbContext.Users.Include(c => c.Files).FirstOrDefault(c => c.UserId == key); + if (data == null) { - User data = _dbContext.Users.Include(c => c.Files).FirstOrDefault(c => c.UserId == key); - if (data == null) - { - return NotFound(); - } - - return Ok(data); + return NotFound(); } + + return Ok(data); } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsDataModel.cs index 69e26dd97..e0ed0ce47 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsDataModel.cs @@ -8,32 +8,31 @@ using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Regressions +namespace Microsoft.AspNetCore.OData.E2E.Tests.Regressions; + +public class User { - public class User - { - [Key] - public int UserId { get; set; } + [Key] + public int UserId { get; set; } - [Required] - public string Name { get; set; } + [Required] + public string Name { get; set; } - [Required] - public int Age { get; set; } + [Required] + public int Age { get; set; } - //Navigations - [ForeignKey("Files")] - public int? DataFileRef { get; set; } + //Navigations + [ForeignKey("Files")] + public int? DataFileRef { get; set; } - public virtual DataFile Files { get; set; } - } + public virtual DataFile Files { get; set; } +} - public class DataFile - { - [Key] - public int FileId { get; set; } +public class DataFile +{ + [Key] + public int FileId { get; set; } - [Required] - public string FileName { get; set; } - } -} \ No newline at end of file + [Required] + public string FileName { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsDbContext.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsDbContext.cs index 902d91608..94793de30 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsDbContext.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsDbContext.cs @@ -7,17 +7,16 @@ using Microsoft.EntityFrameworkCore; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Regressions +namespace Microsoft.AspNetCore.OData.E2E.Tests.Regressions; + +public class RegressionsDbContext : DbContext { - public class RegressionsDbContext : DbContext + public RegressionsDbContext(DbContextOptions options) + : base(options) { - public RegressionsDbContext(DbContextOptions options) - : base(options) - { - } + } - public DbSet Users { get; set; } + public DbSet Users { get; set; } - public DbSet DataFiles { get; set; } - } -} \ No newline at end of file + public DbSet DataFiles { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsTests.cs index da5aa94d2..b7861d237 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Regressions/RegressionsTests.cs @@ -15,133 +15,132 @@ using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Regressions +namespace Microsoft.AspNetCore.OData.E2E.Tests.Regressions; + +public class RegressionsTests : WebApiTestBase { - public class RegressionsTests : WebApiTestBase - { - private readonly ITestOutputHelper _output; + private readonly ITestOutputHelper _output; - public RegressionsTests(WebApiTestFixture fixture, ITestOutputHelper output) - : base(fixture) - { - _output = output; - } + public RegressionsTests(WebApiTestFixture fixture, ITestOutputHelper output) + : base(fixture) + { + _output = output; + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.AddSqlite($"Data Source=UsersWithNullContext.db"); + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.AddSqlite($"Data Source=UsersWithNullContext.db"); - IEdmModel edmModel = GetEdmModel(); + IEdmModel edmModel = GetEdmModel(); - services.ConfigureControllers(typeof(UsersController)); + services.ConfigureControllers(typeof(UsersController)); - services.AddControllers().AddOData(opt => - opt.EnableQueryFeatures() - .AddRouteComponents("odata", edmModel)); - } + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures() + .AddRouteComponents("odata", edmModel)); + } - [Fact] - public async Task QueryUsersWithNullReferenceKey_Works() - { - // Arrange - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync("odata/users"); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - string payloadBody = await response.Content.ReadAsStringAsync(); - - Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Users\"," + - "\"value\":[" + - "{\"UserId\":1,\"Name\":\"Alex\",\"Age\":35,\"DataFileRef\":null}," + - "{\"UserId\":2,\"Name\":\"Amanda\",\"Age\":29,\"DataFileRef\":2}," + - "{\"UserId\":3,\"Name\":\"Lara\",\"Age\":25,\"DataFileRef\":null}" + - "]" + - "}", payloadBody); - } + [Fact] + public async Task QueryUsersWithNullReferenceKey_Works() + { + // Arrange + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync("odata/users"); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Users\"," + + "\"value\":[" + + "{\"UserId\":1,\"Name\":\"Alex\",\"Age\":35,\"DataFileRef\":null}," + + "{\"UserId\":2,\"Name\":\"Amanda\",\"Age\":29,\"DataFileRef\":2}," + + "{\"UserId\":3,\"Name\":\"Lara\",\"Age\":25,\"DataFileRef\":null}" + + "]" + + "}", payloadBody); + } - // This is a failing regression test, see https://github.com/OData/AspNetCoreOData/issues/1035 - // It was fail with: System.InvalidOperationException : Nullable object must have a value. - [Fact] - public async Task QueryUsersWithNullReferenceKeyUsingDollarExpand_Works() - { - // Arrange - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync($"odata/users?$expand=Files"); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - string payloadBody = await response.Content.ReadAsStringAsync(); - - Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Users(Files())\"," + - "\"value\":[" + - "{\"UserId\":1,\"Name\":\"Alex\",\"Age\":35,\"DataFileRef\":null,\"Files\":null}," + - "{\"UserId\":2,\"Name\":\"Amanda\",\"Age\":29,\"DataFileRef\":2,\"Files\":{\"FileId\":2,\"FileName\":\"uyr65euit5.pdf\"}}," + - "{\"UserId\":3,\"Name\":\"Lara\",\"Age\":25,\"DataFileRef\":null,\"Files\":null}" + - "]" + - "}", payloadBody); - } + // This is a failing regression test, see https://github.com/OData/AspNetCoreOData/issues/1035 + // It was fail with: System.InvalidOperationException : Nullable object must have a value. + [Fact] + public async Task QueryUsersWithNullReferenceKeyUsingDollarExpand_Works() + { + // Arrange + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync($"odata/users?$expand=Files"); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Users(Files())\"," + + "\"value\":[" + + "{\"UserId\":1,\"Name\":\"Alex\",\"Age\":35,\"DataFileRef\":null,\"Files\":null}," + + "{\"UserId\":2,\"Name\":\"Amanda\",\"Age\":29,\"DataFileRef\":2,\"Files\":{\"FileId\":2,\"FileName\":\"uyr65euit5.pdf\"}}," + + "{\"UserId\":3,\"Name\":\"Lara\",\"Age\":25,\"DataFileRef\":null,\"Files\":null}" + + "]" + + "}", payloadBody); + } - public static TheoryDataSet SingleUserQueryCases + public static TheoryDataSet SingleUserQueryCases + { + get { - get + var data = new TheoryDataSet { - var data = new TheoryDataSet { - { - "odata/users/1", - "{CONTEXTURI,\"UserId\":1,\"Name\":\"Alex\",\"Age\":35,\"DataFileRef\":null,\"Files\":null}" - }, - { - "odata/users/2", - "{CONTEXTURI,\"UserId\":2,\"Name\":\"Amanda\",\"Age\":29,\"DataFileRef\":2,\"Files\":{\"FileId\":2,\"FileName\":\"uyr65euit5.pdf\"}}" - }, - { - "odata/users/3", - "{CONTEXTURI,\"UserId\":3,\"Name\":\"Lara\",\"Age\":25,\"DataFileRef\":null,\"Files\":null}" - } - }; - - return data; - } + "odata/users/1", + "{CONTEXTURI,\"UserId\":1,\"Name\":\"Alex\",\"Age\":35,\"DataFileRef\":null,\"Files\":null}" + }, + { + "odata/users/2", + "{CONTEXTURI,\"UserId\":2,\"Name\":\"Amanda\",\"Age\":29,\"DataFileRef\":2,\"Files\":{\"FileId\":2,\"FileName\":\"uyr65euit5.pdf\"}}" + }, + { + "odata/users/3", + "{CONTEXTURI,\"UserId\":3,\"Name\":\"Lara\",\"Age\":25,\"DataFileRef\":null,\"Files\":null}" + } + }; + + return data; } + } - [Theory] - [MemberData(nameof(SingleUserQueryCases))] - public async Task QuerySingleUserWithNullReferenceKeyUsingDollarExpand_Works(string request, string expected) - { - // Arrange - HttpClient client = CreateClient(); - expected = expected.Replace("CONTEXTURI", "\"@odata.context\":\"http://localhost/odata/$metadata#Users(Files())/$entity\""); + [Theory] + [MemberData(nameof(SingleUserQueryCases))] + public async Task QuerySingleUserWithNullReferenceKeyUsingDollarExpand_Works(string request, string expected) + { + // Arrange + HttpClient client = CreateClient(); + expected = expected.Replace("CONTEXTURI", "\"@odata.context\":\"http://localhost/odata/$metadata#Users(Files())/$entity\""); - // Act - HttpResponseMessage response = await client.GetAsync($"{request}?$expand=Files"); + // Act + HttpResponseMessage response = await client.GetAsync($"{request}?$expand=Files"); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - string payloadBody = await response.Content.ReadAsStringAsync(); + string payloadBody = await response.Content.ReadAsStringAsync(); - Assert.Equal(expected, payloadBody); - } + Assert.Equal(expected, payloadBody); + } - public static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Users"); - return builder.GetEdmModel(); - } + public static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Users"); + return builder.GetEdmModel(); } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/AttributeRoutingTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/AttributeRoutingTests.cs index 5c8e9f46c..b8345a6b1 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/AttributeRoutingTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/AttributeRoutingTests.cs @@ -18,164 +18,163 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing +namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing; + +public class AttributeRoutingTests : WebApiTestBase { - public class AttributeRoutingTests : WebApiTestBase + public AttributeRoutingTests(WebApiTestFixture fixture) + : base(fixture) { - public AttributeRoutingTests(WebApiTestFixture fixture) - : base(fixture) - { - } + } - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(DogsController), typeof(CatsController), typeof(OwnersController)); + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(DogsController), typeof(CatsController), typeof(OwnersController)); - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Dogs").EntityType.Collection.Function("BestDog").Returns(); - builder.EntitySet("Owners").EntityType.Collection.Function("BestOwner").Returns(); - var model1 = builder.GetEdmModel(); + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Dogs").EntityType.Collection.Function("BestDog").Returns(); + builder.EntitySet("Owners").EntityType.Collection.Function("BestOwner").Returns(); + var model1 = builder.GetEdmModel(); - builder = new ODataConventionModelBuilder(); - builder.EntitySet("Cats").EntityType.Collection.Function("BestCat").Returns(); - builder.EntitySet("Owners").EntityType.Collection.Function("BestOwner").Returns(); - var model2 = builder.GetEdmModel(); + builder = new ODataConventionModelBuilder(); + builder.EntitySet("Cats").EntityType.Collection.Function("BestCat").Returns(); + builder.EntitySet("Owners").EntityType.Collection.Function("BestOwner").Returns(); + var model2 = builder.GetEdmModel(); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("dog", model1).AddRouteComponents("cat", model2)); - } + services.AddControllers().AddOData(opt => opt.AddRouteComponents("dog", model1).AddRouteComponents("cat", model2)); + } - [Theory] - [InlineData("/dog/Dogs")] - [InlineData("/cat/Cats")] - [InlineData("/dog/Owners")] - [InlineData("/cat/Owners")] - public async Task CanCallTheRoutingWithoutAttributeRoutingDecorated(string requestUri) - { - // Arrange - HttpClient client = CreateClient(); - - // Act - var response = await client.GetAsync(requestUri); - - // Assert - response.EnsureSuccessStatusCode(); - - string payload = await response.Content.ReadAsStringAsync(); - Assert.NotNull(payload); - using (JsonDocument document = JsonDocument.Parse(payload)) - { - bool found = document.RootElement.TryGetProperty("value", out JsonElement value); - Assert.True(found); - Assert.Equal(5, value.EnumerateArray().Count()); - } - } + [Theory] + [InlineData("/dog/Dogs")] + [InlineData("/cat/Cats")] + [InlineData("/dog/Owners")] + [InlineData("/cat/Owners")] + public async Task CanCallTheRoutingWithoutAttributeRoutingDecorated(string requestUri) + { + // Arrange + HttpClient client = CreateClient(); - [Theory] - [InlineData("/dog/Dogs/Default.BestDog", "Dog 1")] - [InlineData("/dog/Owners/Default.BestOwner", "Owner 1")] - [InlineData("/dog/Owners/BestOwner", "Owner 1")] - [InlineData("/cat/Cats/Default.BestCat", "Cat 1")] - [InlineData("/cat/Owners/Default.BestOwner", "Owner 1")] - public async Task CanCallTheRoutingWithAttributeRoutingDecorated(string url, string expected) - { - // Arrange - HttpClient client = CreateClient(); + // Act + var response = await client.GetAsync(requestUri); - // Act - HttpResponseMessage response = await client.GetAsync(url); + // Assert + response.EnsureSuccessStatusCode(); - // Assert - Assert.NotNull(response); - Assert.True(response.IsSuccessStatusCode); - string payload = await response.Content.ReadAsStringAsync(); - Assert.Contains(expected, payload); + string payload = await response.Content.ReadAsStringAsync(); + Assert.NotNull(payload); + using (JsonDocument document = JsonDocument.Parse(payload)) + { + bool found = document.RootElement.TryGetProperty("value", out JsonElement value); + Assert.True(found); + Assert.Equal(5, value.EnumerateArray().Count()); } } - public class DogsController : ODataController + [Theory] + [InlineData("/dog/Dogs/Default.BestDog", "Dog 1")] + [InlineData("/dog/Owners/Default.BestOwner", "Owner 1")] + [InlineData("/dog/Owners/BestOwner", "Owner 1")] + [InlineData("/cat/Cats/Default.BestCat", "Cat 1")] + [InlineData("/cat/Owners/Default.BestOwner", "Owner 1")] + public async Task CanCallTheRoutingWithAttributeRoutingDecorated(string url, string expected) { - private static IList dogs = Enumerable.Range(1, 5) - .Select(e => { return new Dog() { Id = e, Name = "Dog " + e.ToString() }; }) - .ToList(); - - // This method will have the following routing: - // ~/dog/Dogs - // ~/dog/Dogs/$count - public IActionResult Get() - { - return Ok(dogs); - } + // Arrange + HttpClient client = CreateClient(); - [HttpGet("~/dog/Dogs/Default.BestDog")] - public IActionResult WhoIsTheBestDog() - { - return Ok(dogs.First().Name); - } + // Act + HttpResponseMessage response = await client.GetAsync(url); + + // Assert + Assert.NotNull(response); + Assert.True(response.IsSuccessStatusCode); + string payload = await response.Content.ReadAsStringAsync(); + Assert.Contains(expected, payload); } +} - [ODataAttributeRouting] - public class CatsController : ControllerBase +public class DogsController : ODataController +{ + private static IList dogs = Enumerable.Range(1, 5) + .Select(e => { return new Dog() { Id = e, Name = "Dog " + e.ToString() }; }) + .ToList(); + + // This method will have the following routing: + // ~/dog/Dogs + // ~/dog/Dogs/$count + public IActionResult Get() { - private static IList cats = Enumerable.Range(1, 5) - .Select(e => { return new Cat() { Id = e, Name = "Cat " + e.ToString() }; }) - .ToList(); - - // This method will have the following routing: - // ~/cat/Cats - // ~/cat/Cats/$count - public IActionResult Get() - { - return Ok(cats); - } - - [HttpGet("cat/Cats/Default.BestCat")] - public IActionResult WhoIsTheBestCat() - { - return Ok(cats.First().Name); - } + return Ok(dogs); } - public class OwnersController : ODataController + [HttpGet("~/dog/Dogs/Default.BestDog")] + public IActionResult WhoIsTheBestDog() { - private static IList owners = Enumerable.Range(1, 5) - .Select(e => { return new Owner() { Id = e, Name = "Owner " + e.ToString() }; }) - .ToList(); - - // This method will have the following routing: - // ~/dog/Owners - // ~/cat/Owners - // ~/dog/Owners/$count - // ~/cat/Owners/$count - public IActionResult Get() - { - return Ok(owners); - } + return Ok(dogs.First().Name); + } +} - [HttpGet("dog/Owners/Default.BestOwner")] - [HttpGet("dog/Owners/BestOwner")] - [HttpGet("cat/Owners/Default.BestOwner")] - public IActionResult WhoIsTheBestOwner() - { - return Ok(owners.First().Name); - } +[ODataAttributeRouting] +public class CatsController : ControllerBase +{ + private static IList cats = Enumerable.Range(1, 5) + .Select(e => { return new Cat() { Id = e, Name = "Cat " + e.ToString() }; }) + .ToList(); + + // This method will have the following routing: + // ~/cat/Cats + // ~/cat/Cats/$count + public IActionResult Get() + { + return Ok(cats); } - public class Dog + [HttpGet("cat/Cats/Default.BestCat")] + public IActionResult WhoIsTheBestCat() { - public int Id { get; set; } - public string Name { get; set; } + return Ok(cats.First().Name); } +} - public class Cat +public class OwnersController : ODataController +{ + private static IList owners = Enumerable.Range(1, 5) + .Select(e => { return new Owner() { Id = e, Name = "Owner " + e.ToString() }; }) + .ToList(); + + // This method will have the following routing: + // ~/dog/Owners + // ~/cat/Owners + // ~/dog/Owners/$count + // ~/cat/Owners/$count + public IActionResult Get() { - public int Id { get; set; } - public string Name { get; set; } + return Ok(owners); } - public class Owner + [HttpGet("dog/Owners/Default.BestOwner")] + [HttpGet("dog/Owners/BestOwner")] + [HttpGet("cat/Owners/Default.BestOwner")] + public IActionResult WhoIsTheBestOwner() { - public int Id { get; set; } - public string Name { get; set; } + return Ok(owners.First().Name); } } + +public class Dog +{ + public int Id { get; set; } + public string Name { get; set; } +} + +public class Cat +{ + public int Id { get; set; } + public string Name { get; set; } +} + +public class Owner +{ + public int Id { get; set; } + public string Name { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataActionTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataActionTests.cs index 28528ee55..99c3c1bb0 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataActionTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataActionTests.cs @@ -21,428 +21,427 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing +namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing; + +public class ODataActionTests : WebApiTestBase { - public class ODataActionTests : WebApiTestBase + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) { - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = GetEdmModel(); - IEdmModel typelessEdmModel = GetTypelessModel(); + IEdmModel edmModel = GetEdmModel(); + IEdmModel typelessEdmModel = GetTypelessModel(); - services.ConfigureControllers(typeof(CustomersController), typeof(UntypedCustomersController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel).AddRouteComponents("typeless", typelessEdmModel)); - } + services.ConfigureControllers(typeof(CustomersController), typeof(UntypedCustomersController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel).AddRouteComponents("typeless", typelessEdmModel)); + } - public ODataActionTests(WebApiTestFixture fixture) - : base(fixture) - { } + public ODataActionTests(WebApiTestFixture fixture) + : base(fixture) + { } - [Fact] - public async Task CanDispatch_ActionPayload_ToBoundAction() - { - // Arrange - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/Customers(1)/org.odata.DoSomething"); - request.Headers.Add("accept", "application/json"); - string payload = @"{ - ""p1"": 1, - ""p2"": { ""StreetAddress"": ""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 }, - ""p3"": [ ""one"", ""two"" ], - ""p4"": [ { ""StreetAddress"": ""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 } ], - ""color"": ""Red"", - ""colors"": [""Red"", null, ""Green""] - }"; - - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("\"@odata.context\":\"http://localhost/odata/$metadata#Edm.Boolean\",\"value\":true", responseString); - } + [Fact] + public async Task CanDispatch_ActionPayload_ToBoundAction() + { + // Arrange + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/Customers(1)/org.odata.DoSomething"); + request.Headers.Add("accept", "application/json"); + string payload = @"{ + ""p1"": 1, + ""p2"": { ""StreetAddress"": ""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 }, + ""p3"": [ ""one"", ""two"" ], + ""p4"": [ { ""StreetAddress"": ""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 } ], + ""color"": ""Red"", + ""colors"": [""Red"", null, ""Green""] + }"; + + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("\"@odata.context\":\"http://localhost/odata/$metadata#Edm.Boolean\",\"value\":true", responseString); + } - private const string EntityPayload = @"{ - ""Customer"": {""@odata.type"":""#org.odata.Customer"", ""ID"":101,""Name"":""Avatar"" } , - ""Customers"": [ - {""@odata.type"":""#org.odata.Customer"", ""ID"":901,""Name"":""John"" } , - {""@odata.type"":""#org.odata.SubCustomer"", ""ID"":902,""Name"":""Mike"", ""Price"": 9.9 } - ] - }"; + private const string EntityPayload = @"{ + ""Customer"": {""@odata.type"":""#org.odata.Customer"", ""ID"":101,""Name"":""Avatar"" } , + ""Customers"": [ + {""@odata.type"":""#org.odata.Customer"", ""ID"":901,""Name"":""John"" } , + {""@odata.type"":""#org.odata.SubCustomer"", ""ID"":902,""Name"":""Mike"", ""Price"": 9.9 } + ] + }"; - [Fact] - public async Task CanDispatch_ActionPayloadWithEntity_ToBoundAction() - { - // Arrange - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/Customers/org.odata.MyAction"); - request.Headers.Add("accept", "application/json"); - - request.Content = new StringContent(EntityPayload); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = EntityPayload.Length; - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("\"@odata.context\":\"http://localhost/odata/$metadata#Edm.Boolean\",\"value\":true", responseString); - } + [Fact] + public async Task CanDispatch_ActionPayloadWithEntity_ToBoundAction() + { + // Arrange + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/Customers/org.odata.MyAction"); + request.Headers.Add("accept", "application/json"); + + request.Content = new StringContent(EntityPayload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = EntityPayload.Length; + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("\"@odata.context\":\"http://localhost/odata/$metadata#Edm.Boolean\",\"value\":true", responseString); + } - [Fact] - public async Task CanDispatch_ActionPayloadWithEntity_ToBoundAction_Typeless() - { - // Arrange - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "typeless/UntypedCustomers/NS.MyAction"); - request.Headers.Add("accept", "application/json"); - HttpClient client = CreateClient(); - - request.Content = new StringContent(EntityPayload); - request.Content.Headers.ContentLength = EntityPayload.Length; - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("\"@odata.context\":\"http://localhost/typeless/$metadata#Edm.Boolean\",\"value\":true", responseString); - } + [Fact] + public async Task CanDispatch_ActionPayloadWithEntity_ToBoundAction_Typeless() + { + // Arrange + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "typeless/UntypedCustomers/NS.MyAction"); + request.Headers.Add("accept", "application/json"); + HttpClient client = CreateClient(); + + request.Content = new StringContent(EntityPayload); + request.Content.Headers.ContentLength = EntityPayload.Length; + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("\"@odata.context\":\"http://localhost/typeless/$metadata#Edm.Boolean\",\"value\":true", responseString); + } - [Theory] - [InlineData("org.odata.DoSomething", "DoSomething")] - [InlineData("customize.CNSAction", "CNSAction")] - public async Task Response_Includes_ActionLink_WithAcceptHeader(string fullname, string name) - { - // Arrange - string editLink = "http://localhost/odata/Customers(1)"; - string expectedTarget = editLink + "/" + fullname; + [Theory] + [InlineData("org.odata.DoSomething", "DoSomething")] + [InlineData("customize.CNSAction", "CNSAction")] + public async Task Response_Includes_ActionLink_WithAcceptHeader(string fullname, string name) + { + // Arrange + string editLink = "http://localhost/odata/Customers(1)"; + string expectedTarget = editLink + "/" + fullname; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/Customers(1)"); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/Customers(1)"); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.SendAsync(request); - string responseString = await response.Content.ReadAsStringAsync(); + // Act + HttpResponseMessage response = await client.SendAsync(request); + string responseString = await response.Content.ReadAsStringAsync(); - using (JsonDocument document = JsonDocument.Parse(responseString)) - { - bool found = document.RootElement.TryGetProperty($"#{fullname}", out JsonElement action); - Assert.True(found); + using (JsonDocument document = JsonDocument.Parse(responseString)) + { + bool found = document.RootElement.TryGetProperty($"#{fullname}", out JsonElement action); + Assert.True(found); - bool titleFound = action.TryGetProperty("title", out JsonElement title); - Assert.True(titleFound); - Assert.Equal(name, title.GetString()); + bool titleFound = action.TryGetProperty("title", out JsonElement title); + Assert.True(titleFound); + Assert.Equal(name, title.GetString()); - bool targetFound = action.TryGetProperty("target", out JsonElement target); - Assert.True(titleFound); - Assert.Equal(expectedTarget, target.GetString()); - } + bool targetFound = action.TryGetProperty("target", out JsonElement target); + Assert.True(titleFound); + Assert.Equal(expectedTarget, target.GetString()); } + } - [Fact] - public async Task Response_Includes_ActionLink_WithDollarFormat() - { - // Arrange - string requestUri = "odata/Customers?$format=application/json;odata.metadata=full"; - HttpClient client = CreateClient(); + [Fact] + public async Task Response_Includes_ActionLink_WithDollarFormat() + { + // Arrange + string requestUri = "odata/Customers?$format=application/json;odata.metadata=full"; + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); - string responseString = await response.Content.ReadAsStringAsync(); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + string responseString = await response.Content.ReadAsStringAsync(); - // Assert - using (JsonDocument document = JsonDocument.Parse(responseString)) + // Assert + using (JsonDocument document = JsonDocument.Parse(responseString)) + { + JsonElement valueElement = document.RootElement.GetProperty("value"); + foreach (var item in valueElement.EnumerateArray()) { - JsonElement valueElement = document.RootElement.GetProperty("value"); - foreach (var item in valueElement.EnumerateArray()) - { - bool idFound = item.TryGetProperty($"ID", out JsonElement idNode); - Assert.True(idFound); - int id = idNode.GetInt32(); + bool idFound = item.TryGetProperty($"ID", out JsonElement idNode); + Assert.True(idFound); + int id = idNode.GetInt32(); - bool actionfound = item.TryGetProperty($"#org.odata.DoSomething", out JsonElement action); - Assert.True(actionfound); + bool actionfound = item.TryGetProperty($"#org.odata.DoSomething", out JsonElement action); + Assert.True(actionfound); - bool targetFound = action.TryGetProperty("target", out JsonElement target); - Assert.True(targetFound); + bool targetFound = action.TryGetProperty("target", out JsonElement target); + Assert.True(targetFound); - Assert.Equal($"http://localhost/odata/Customers({id})/org.odata.DoSomething", target.GetString()); - } + Assert.Equal($"http://localhost/odata/Customers({id})/org.odata.DoSomething", target.GetString()); } } + } - [Fact] - public async Task Response_Includes_ActionLinkForFeed_WithAcceptHeader() - { - // Arrange - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/Customers"); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - HttpClient client = CreateClient(); + [Fact] + public async Task Response_Includes_ActionLinkForFeed_WithAcceptHeader() + { + // Arrange + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/Customers"); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.SendAsync(request); - string responseString = await response.Content.ReadAsStringAsync(); + // Act + HttpResponseMessage response = await client.SendAsync(request); + string responseString = await response.Content.ReadAsStringAsync(); - // Assert - using (JsonDocument document = JsonDocument.Parse(responseString)) - { - bool found = document.RootElement.TryGetProperty($"#org.odata.MyAction", out JsonElement action); - Assert.True(found); + // Assert + using (JsonDocument document = JsonDocument.Parse(responseString)) + { + bool found = document.RootElement.TryGetProperty($"#org.odata.MyAction", out JsonElement action); + Assert.True(found); - bool titleFound = action.TryGetProperty("title", out JsonElement title); - Assert.True(titleFound); - Assert.Equal("MyAction", title.GetString()); + bool titleFound = action.TryGetProperty("title", out JsonElement title); + Assert.True(titleFound); + Assert.Equal("MyAction", title.GetString()); - bool targetFound = action.TryGetProperty("target", out JsonElement target); - Assert.True(titleFound); - Assert.Equal("http://localhost/odata/Customers/org.odata.MyAction", target.GetString()); - } + bool targetFound = action.TryGetProperty("target", out JsonElement target); + Assert.True(titleFound); + Assert.Equal("http://localhost/odata/Customers/org.odata.MyAction", target.GetString()); } + } - [Fact] - public async Task Response_Includes_ActionLinkForFeed_WithDollarFormat() - { - // Arrange - HttpClient client = CreateClient(); + [Fact] + public async Task Response_Includes_ActionLinkForFeed_WithDollarFormat() + { + // Arrange + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.GetAsync("odata/Customers?$format=application/json;odata.metadata=full"); - string responseString = await response.Content.ReadAsStringAsync(); + // Act + HttpResponseMessage response = await client.GetAsync("odata/Customers?$format=application/json;odata.metadata=full"); + string responseString = await response.Content.ReadAsStringAsync(); - // Assert - Assert.Contains("\"target\":\"http://localhost/odata/Customers/org.odata.MyAction\"", responseString); - } + // Assert + Assert.Contains("\"target\":\"http://localhost/odata/Customers/org.odata.MyAction\"", responseString); + } - private static IEdmModel GetEdmModel() - { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - builder.ContainerName = "Container"; - builder.Namespace = "org.odata"; - EntityTypeConfiguration customer = builder.EntitySet("Customers").EntityType; - ActionConfiguration action = customer.Action("DoSomething"); - action.Parameter("p1"); - action.Parameter
("p2"); - action.Parameter("color"); - action.CollectionParameter("p3"); - action.CollectionParameter
("p4"); - action.CollectionParameter("colors"); - - action = customer.Collection.Action("MyAction"); - action.EntityParameter("Customer"); - action.CollectionEntityParameter("Customers"); - - action = customer.Action("CNSAction"); - action.Namespace = "customize"; - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.ContainerName = "Container"; + builder.Namespace = "org.odata"; + EntityTypeConfiguration customer = builder.EntitySet("Customers").EntityType; + ActionConfiguration action = customer.Action("DoSomething"); + action.Parameter("p1"); + action.Parameter
("p2"); + action.Parameter("color"); + action.CollectionParameter("p3"); + action.CollectionParameter
("p4"); + action.CollectionParameter("colors"); + + action = customer.Collection.Action("MyAction"); + action.EntityParameter("Customer"); + action.CollectionEntityParameter("Customers"); + + action = customer.Action("CNSAction"); + action.Namespace = "customize"; + return builder.GetEdmModel(); + } - private static IEdmModel GetTypelessModel() - { - var model = new EdmModel(); - - // entity type customer - EdmEntityType customer = new EdmEntityType("org.odata", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - model.AddElement(customer); - - EdmEntityType subCustomer = new EdmEntityType("org.odata", "SubCustomer", customer); - customer.AddKeys(subCustomer.AddStructuralProperty("Price", EdmPrimitiveTypeKind.Double)); - model.AddElement(subCustomer); - - EdmAction action = new EdmAction("NS", "MyAction", null, isBound: true, entitySetPathExpression: null); - action.AddParameter("bindingParameter", - new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, isNullable: false)))); - action.AddParameter("Customer", new EdmEntityTypeReference(customer, isNullable: true)); - action.AddParameter("Customers", - new EdmCollectionTypeReference( - new EdmCollectionType(new EdmEntityTypeReference(customer, isNullable: true)))); - model.AddElement(action); - - EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); - container.AddEntitySet("UntypedCustomers", customer); - - model.AddElement(container); - return model; - } + private static IEdmModel GetTypelessModel() + { + var model = new EdmModel(); + + // entity type customer + EdmEntityType customer = new EdmEntityType("org.odata", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + model.AddElement(customer); + + EdmEntityType subCustomer = new EdmEntityType("org.odata", "SubCustomer", customer); + customer.AddKeys(subCustomer.AddStructuralProperty("Price", EdmPrimitiveTypeKind.Double)); + model.AddElement(subCustomer); + + EdmAction action = new EdmAction("NS", "MyAction", null, isBound: true, entitySetPathExpression: null); + action.AddParameter("bindingParameter", + new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, isNullable: false)))); + action.AddParameter("Customer", new EdmEntityTypeReference(customer, isNullable: true)); + action.AddParameter("Customers", + new EdmCollectionTypeReference( + new EdmCollectionType(new EdmEntityTypeReference(customer, isNullable: true)))); + model.AddElement(action); + + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + container.AddEntitySet("UntypedCustomers", customer); + + model.AddElement(container); + return model; + } - public class Customer - { - public int ID { get; set; } - public string Name { get; set; } - } + public class Customer + { + public int ID { get; set; } + public string Name { get; set; } + } - public class SubCustomer : Customer - { - public double Price { get; set; } - } + public class SubCustomer : Customer + { + public double Price { get; set; } + } - public class Address - { - public string StreetAddress { get; set; } - public string City { get; set; } - public string State { get; set; } - public int ZipCode { get; set; } - } + public class Address + { + public string StreetAddress { get; set; } + public string City { get; set; } + public string State { get; set; } + public int ZipCode { get; set; } + } - public enum Color - { - Red, - Blue, - Green - } + public enum Color + { + Red, + Blue, + Green } +} - // Controllers - public class CustomersController : ODataController +// Controllers +public class CustomersController : ODataController +{ + [HttpGet] + public IActionResult Get() { - [HttpGet] - public IActionResult Get() + var customers = Enumerable.Range(1, 6).Select(i => new ODataActionTests.Customer { - var customers = Enumerable.Range(1, 6).Select(i => new ODataActionTests.Customer - { - ID = i, - Name = "Name " + i - }).ToList(); + ID = i, + Name = "Name " + i + }).ToList(); - return Ok(customers); - } + return Ok(customers); + } - [HttpGet] - public ODataActionTests.Customer Get(int key) - { - return new ODataActionTests.Customer { ID = key, Name = "Name" + key.ToString() }; - } + [HttpGet] + public ODataActionTests.Customer Get(int key) + { + return new ODataActionTests.Customer { ID = key, Name = "Name" + key.ToString() }; + } - [HttpPost] - public bool CNSAction(int key, ODataActionParameters parameters) - { - return true; - } + [HttpPost] + public bool CNSAction(int key, ODataActionParameters parameters) + { + return true; + } - [HttpPost] - public bool DoSomething(int key, ODataActionParameters parameters) - { - Assert.Equal(1, key); - Assert.Equal(1, parameters["p1"]); - ValidateAddress(parameters["p2"] as ODataActionTests.Address); - ValidateNumbers((parameters["p3"] as IEnumerable).ToList()); - ValidateAddresses((parameters["p4"] as IEnumerable).ToList()); - Assert.Equal(ODataActionTests.Color.Red, parameters["color"]); - - Assert.NotNull(parameters["colors"]); - IList colors = (parameters["colors"] as IEnumerable).ToList(); - Assert.NotNull(colors); - - Assert.Equal(ODataActionTests.Color.Red, colors[0]); - Assert.Null(colors[1]); - Assert.Equal(ODataActionTests.Color.Green, colors[2]); - - return true; - } + [HttpPost] + public bool DoSomething(int key, ODataActionParameters parameters) + { + Assert.Equal(1, key); + Assert.Equal(1, parameters["p1"]); + ValidateAddress(parameters["p2"] as ODataActionTests.Address); + ValidateNumbers((parameters["p3"] as IEnumerable).ToList()); + ValidateAddresses((parameters["p4"] as IEnumerable).ToList()); + Assert.Equal(ODataActionTests.Color.Red, parameters["color"]); + + Assert.NotNull(parameters["colors"]); + IList colors = (parameters["colors"] as IEnumerable).ToList(); + Assert.NotNull(colors); + + Assert.Equal(ODataActionTests.Color.Red, colors[0]); + Assert.Null(colors[1]); + Assert.Equal(ODataActionTests.Color.Green, colors[2]); + + return true; + } - [HttpPost] - public bool MyAction(ODataActionParameters parameters) - { - Assert.True(parameters.ContainsKey("Customer")); - ODataActionTests.Customer customer = parameters["Customer"] as ODataActionTests.Customer; - Assert.NotNull(customer); - Assert.Equal(101, customer.ID); - Assert.Equal("Avatar", customer.Name); + [HttpPost] + public bool MyAction(ODataActionParameters parameters) + { + Assert.True(parameters.ContainsKey("Customer")); + ODataActionTests.Customer customer = parameters["Customer"] as ODataActionTests.Customer; + Assert.NotNull(customer); + Assert.Equal(101, customer.ID); + Assert.Equal("Avatar", customer.Name); - Assert.True(parameters.ContainsKey("Customers")); - ValidateCustomers((parameters["Customers"] as IEnumerable).ToList()); + Assert.True(parameters.ContainsKey("Customers")); + ValidateCustomers((parameters["Customers"] as IEnumerable).ToList()); - return true; - } + return true; + } - private void ValidateAddress(ODataActionTests.Address address) - { - Assert.NotNull(address); - Assert.Equal("1 Microsoft Way", address.StreetAddress); - Assert.Equal("Redmond", address.City); - Assert.Equal("WA", address.State); - Assert.Equal(98052, address.ZipCode); - } + private void ValidateAddress(ODataActionTests.Address address) + { + Assert.NotNull(address); + Assert.Equal("1 Microsoft Way", address.StreetAddress); + Assert.Equal("Redmond", address.City); + Assert.Equal("WA", address.State); + Assert.Equal(98052, address.ZipCode); + } - private void ValidateNumbers(IList numbers) - { - Assert.NotNull(numbers); - Assert.Equal(2, numbers.Count); - Assert.Equal("one", numbers[0]); - Assert.Equal("two", numbers[1]); - } + private void ValidateNumbers(IList numbers) + { + Assert.NotNull(numbers); + Assert.Equal(2, numbers.Count); + Assert.Equal("one", numbers[0]); + Assert.Equal("two", numbers[1]); + } - private void ValidateAddresses(IList addresses) - { - Assert.NotNull(addresses); - Assert.Single(addresses); - ValidateAddress(addresses[0]); - } + private void ValidateAddresses(IList addresses) + { + Assert.NotNull(addresses); + Assert.Single(addresses); + ValidateAddress(addresses[0]); + } - private void ValidateCustomers(IList customers) - { - Assert.NotNull(customers); - Assert.Equal(2, customers.Count); - - ODataActionTests.Customer customer = Assert.IsType(customers[0]); - Assert.NotNull(customer); - Assert.Equal(901, customer.ID); - Assert.Equal("John", customer.Name); - - ODataActionTests.SubCustomer subCustomer = Assert.IsType(customers[1]); - Assert.NotNull(subCustomer); - Assert.Equal(902, subCustomer.ID); - Assert.Equal("Mike", subCustomer.Name); - Assert.Equal(9.9, subCustomer.Price); - } + private void ValidateCustomers(IList customers) + { + Assert.NotNull(customers); + Assert.Equal(2, customers.Count); + + ODataActionTests.Customer customer = Assert.IsType(customers[0]); + Assert.NotNull(customer); + Assert.Equal(901, customer.ID); + Assert.Equal("John", customer.Name); + + ODataActionTests.SubCustomer subCustomer = Assert.IsType(customers[1]); + Assert.NotNull(subCustomer); + Assert.Equal(902, subCustomer.ID); + Assert.Equal("Mike", subCustomer.Name); + Assert.Equal(9.9, subCustomer.Price); } +} - public class UntypedCustomersController : ODataController +public class UntypedCustomersController : ODataController +{ + [HttpPost] + public bool MyAction(ODataUntypedActionParameters parameters) { - [HttpPost] - public bool MyAction(ODataUntypedActionParameters parameters) - { - Assert.True(parameters.ContainsKey("Customer")); - dynamic customer = parameters["Customer"] as EdmEntityObject; - Assert.NotNull(customer); - Assert.Equal(101, customer.ID); - Assert.Equal("Avatar", customer.Name); - - Assert.True(parameters.ContainsKey("Customers")); - IEnumerable customers = parameters["Customers"] as EdmEntityObjectCollection; - - Assert.Equal(2, customers.Count()); - EdmEntityObject entity = customers.First() as EdmEntityObject; - IEdmTypeReference typeReference = entity.GetEdmType(); - Assert.Equal("org.odata.Customer", typeReference.FullName()); - - customer = customers.First(); - Assert.NotNull(customer); - Assert.Equal(901, customer.ID); - Assert.Equal("John", customer.Name); - - entity = customers.Last() as EdmEntityObject; - typeReference = entity.GetEdmType(); - Assert.Equal("org.odata.SubCustomer", typeReference.FullName()); - customer = customers.Last(); - Assert.NotNull(customer); - Assert.Equal(902, customer.ID); - Assert.Equal("Mike", customer.Name); - Assert.Equal(9.9, customer.Price); - - return true; - } + Assert.True(parameters.ContainsKey("Customer")); + dynamic customer = parameters["Customer"] as EdmEntityObject; + Assert.NotNull(customer); + Assert.Equal(101, customer.ID); + Assert.Equal("Avatar", customer.Name); + + Assert.True(parameters.ContainsKey("Customers")); + IEnumerable customers = parameters["Customers"] as EdmEntityObjectCollection; + + Assert.Equal(2, customers.Count()); + EdmEntityObject entity = customers.First() as EdmEntityObject; + IEdmTypeReference typeReference = entity.GetEdmType(); + Assert.Equal("org.odata.Customer", typeReference.FullName()); + + customer = customers.First(); + Assert.NotNull(customer); + Assert.Equal(901, customer.ID); + Assert.Equal("John", customer.Name); + + entity = customers.Last() as EdmEntityObject; + typeReference = entity.GetEdmType(); + Assert.Equal("org.odata.SubCustomer", typeReference.FullName()); + customer = customers.Last(); + Assert.NotNull(customer); + Assert.Equal(902, customer.ID); + Assert.Equal("Mike", customer.Name); + Assert.Equal(9.9, customer.Price); + + return true; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataCountTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataCountTests.cs index 0c7212810..f97df98ca 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataCountTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataCountTests.cs @@ -23,425 +23,424 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing +namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing; + +public class ODataCountTests : WebApiTestBase { - public class ODataCountTests : WebApiTestBase + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) { - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = GetEdmModel(); - services.ConfigureControllers(typeof(DollarCountEntitiesController), typeof(ODataEndpointController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents(edmModel).Count().OrderBy().Filter().Expand().SetMaxTop(null).Select()); - } + IEdmModel edmModel = GetEdmModel(); + services.ConfigureControllers(typeof(DollarCountEntitiesController), typeof(ODataEndpointController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents(edmModel).Count().OrderBy().Filter().Expand().SetMaxTop(null).Select()); + } - public ODataCountTests(WebApiTestFixture fixture) - : base(fixture) - { - } + public ODataCountTests(WebApiTestFixture fixture) + : base(fixture) + { + } - public static TheoryDataSet DollarCountData + public static TheoryDataSet DollarCountData + { + get { - get - { - var data = new TheoryDataSet(); - - // $count follows entity set, structural collection property or navigation collection property. - data.Add("DollarCountEntities/$count", 10); - data.Add("DollarCountEntities/$count?$filter=Id gt 5", 5); - data.Add("DollarCountEntities/Microsoft.AspNetCore.OData.E2E.Tests.Routing.DerivedDollarCountEntity/$count", 5); - data.Add("DollarCountEntities(5)/StringCollectionProp/$count", 2); - data.Add("DollarCountEntities(5)/StringCollectionProp/$count?$filter=$it eq '2'", 1); - data.Add("DollarCountEntities(5)/EnumCollectionProp/$count", 3); - data.Add("DollarCountEntities(5)/EnumCollectionProp/$count?$filter=$it has Microsoft.AspNetCore.OData.E2E.Tests.Routing.DollarColor'Green'", 2); - data.Add("DollarCountEntities(5)/TimeSpanCollectionProp/$count", 4); - data.Add("DollarCountEntities(5)/ComplexCollectionProp/$count", 5); - data.Add("DollarCountEntities(5)/EntityCollectionProp/$count", 4); - - // $count follows unbound function that returns collection. - data.Add("UnboundFunctionReturnsPrimitveCollection()/$count", 6); - data.Add("UnboundFunctionReturnsEnumCollection()/$count", 7); - data.Add("UnboundFunctionReturnsDateTimeOffsetCollection()/$count", 8); - data.Add("UnboundFunctionReturnsDateCollection()/$count", 18); - data.Add("UnboundFunctionReturnsComplexCollection()/$count", 9); - data.Add("UnboundFunctionReturnsEntityCollection()/$count", 10); - data.Add("UnboundFunctionReturnsEntityCollection()/Microsoft.AspNetCore.OData.E2E.Tests.Routing.DerivedDollarCountEntity/$count", 11); - - // $count follows bound function that returns collection. - data.Add("DollarCountEntities/Default.BoundFunctionReturnsPrimitveCollection()/$count", 12); - data.Add("DollarCountEntities/Default.BoundFunctionReturnsEnumCollection()/$count", 13); - data.Add("DollarCountEntities/Default.BoundFunctionReturnsDateTimeOffsetCollection()/$count", 14); - data.Add("DollarCountEntities/Default.BoundFunctionReturnsComplexCollection()/$count", 15); - data.Add("DollarCountEntities/Default.BoundFunctionReturnsComplexCollection()/$count?$filter=contains(StringProp,'1')", 7); - data.Add("DollarCountEntities/Default.BoundFunctionReturnsEntityCollection()/$count", 10); - data.Add("DollarCountEntities/Default.BoundFunctionReturnsEntityCollection()/Microsoft.AspNetCore.OData.E2E.Tests.Routing.DerivedDollarCountEntity/$count", 5); - - return data; - } + var data = new TheoryDataSet(); + + // $count follows entity set, structural collection property or navigation collection property. + data.Add("DollarCountEntities/$count", 10); + data.Add("DollarCountEntities/$count?$filter=Id gt 5", 5); + data.Add("DollarCountEntities/Microsoft.AspNetCore.OData.E2E.Tests.Routing.DerivedDollarCountEntity/$count", 5); + data.Add("DollarCountEntities(5)/StringCollectionProp/$count", 2); + data.Add("DollarCountEntities(5)/StringCollectionProp/$count?$filter=$it eq '2'", 1); + data.Add("DollarCountEntities(5)/EnumCollectionProp/$count", 3); + data.Add("DollarCountEntities(5)/EnumCollectionProp/$count?$filter=$it has Microsoft.AspNetCore.OData.E2E.Tests.Routing.DollarColor'Green'", 2); + data.Add("DollarCountEntities(5)/TimeSpanCollectionProp/$count", 4); + data.Add("DollarCountEntities(5)/ComplexCollectionProp/$count", 5); + data.Add("DollarCountEntities(5)/EntityCollectionProp/$count", 4); + + // $count follows unbound function that returns collection. + data.Add("UnboundFunctionReturnsPrimitveCollection()/$count", 6); + data.Add("UnboundFunctionReturnsEnumCollection()/$count", 7); + data.Add("UnboundFunctionReturnsDateTimeOffsetCollection()/$count", 8); + data.Add("UnboundFunctionReturnsDateCollection()/$count", 18); + data.Add("UnboundFunctionReturnsComplexCollection()/$count", 9); + data.Add("UnboundFunctionReturnsEntityCollection()/$count", 10); + data.Add("UnboundFunctionReturnsEntityCollection()/Microsoft.AspNetCore.OData.E2E.Tests.Routing.DerivedDollarCountEntity/$count", 11); + + // $count follows bound function that returns collection. + data.Add("DollarCountEntities/Default.BoundFunctionReturnsPrimitveCollection()/$count", 12); + data.Add("DollarCountEntities/Default.BoundFunctionReturnsEnumCollection()/$count", 13); + data.Add("DollarCountEntities/Default.BoundFunctionReturnsDateTimeOffsetCollection()/$count", 14); + data.Add("DollarCountEntities/Default.BoundFunctionReturnsComplexCollection()/$count", 15); + data.Add("DollarCountEntities/Default.BoundFunctionReturnsComplexCollection()/$count?$filter=contains(StringProp,'1')", 7); + data.Add("DollarCountEntities/Default.BoundFunctionReturnsEntityCollection()/$count", 10); + data.Add("DollarCountEntities/Default.BoundFunctionReturnsEntityCollection()/Microsoft.AspNetCore.OData.E2E.Tests.Routing.DerivedDollarCountEntity/$count", 5); + + return data; } + } - [Theory] - [MemberData(nameof(DollarCountData))] - public async Task DollarCount_Works(string uri, int expectedCount) - { - // Arrange & Act - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - int actualCount = int.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal(expectedCount, actualCount); - } + [Theory] + [MemberData(nameof(DollarCountData))] + public async Task DollarCount_Works(string uri, int expectedCount) + { + // Arrange & Act + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + int actualCount = int.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal(expectedCount, actualCount); + } - [Fact] - public async Task GetCollection_Works_WithoutDollarCount() - { - // Arrange - string uri = "DollarCountEntities(5)/StringCollectionProp"; - HttpClient client = CreateClient(); + [Fact] + public async Task GetCollection_Works_WithoutDollarCount() + { + // Arrange + string uri = "DollarCountEntities(5)/StringCollectionProp"; + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.GetAsync(uri); + // Act + HttpResponseMessage response = await client.GetAsync(uri); - // Assert - response.EnsureSuccessStatusCode(); - string responseStr = await response.Content.ReadAsStringAsync(); + // Assert + response.EnsureSuccessStatusCode(); + string responseStr = await response.Content.ReadAsStringAsync(); - using (JsonDocument jsonDoc = JsonDocument.Parse(responseStr)) - { - JsonElement jsonValue = jsonDoc.RootElement.GetProperty("value"); - Assert.Collection(jsonValue.EnumerateArray(), - e => - { - Assert.Equal("1", e.GetString()); - }, - e => - { - Assert.Equal("2", e.GetString()); - }); - } + using (JsonDocument jsonDoc = JsonDocument.Parse(responseStr)) + { + JsonElement jsonValue = jsonDoc.RootElement.GetProperty("value"); + Assert.Collection(jsonValue.EnumerateArray(), + e => + { + Assert.Equal("1", e.GetString()); + }, + e => + { + Assert.Equal("2", e.GetString()); + }); } + } - [Fact] - public async Task Function_Works_WithDollarCountInQueryOption() - { - // Arrange - string uri = "DollarCountEntities/Default.BoundFunctionReturnsComplexCollection()?$count=true"; - HttpClient client = CreateClient(); + [Fact] + public async Task Function_Works_WithDollarCountInQueryOption() + { + // Arrange + string uri = "DollarCountEntities/Default.BoundFunctionReturnsComplexCollection()?$count=true"; + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.GetAsync(uri); + // Act + HttpResponseMessage response = await client.GetAsync(uri); - // Assert - response.EnsureSuccessStatusCode(); - string responseStr = await response.Content.ReadAsStringAsync(); + // Assert + response.EnsureSuccessStatusCode(); + string responseStr = await response.Content.ReadAsStringAsync(); - Assert.Contains("\"@odata.count\":15,", responseStr); - } + Assert.Contains("\"@odata.count\":15,", responseStr); + } - [Fact] - public async Task GetCount_Throws_DollarCountNotAllowed() - { - // Arrange - string uri = "DollarCountEntities(5)/DollarCountNotAllowedCollectionProp/$count"; - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - var result = await response.Content.ReadAsStringAsync(); - Assert.Contains( - "The query specified in the URI is not valid. Query option 'Count' is not allowed. " + - "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", - result); - } + [Fact] + public async Task GetCount_Throws_DollarCountNotAllowed() + { + // Arrange + string uri = "DollarCountEntities(5)/DollarCountNotAllowedCollectionProp/$count"; + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var result = await response.Content.ReadAsStringAsync(); + Assert.Contains( + "The query specified in the URI is not valid. Query option 'Count' is not allowed. " + + "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", + result); + } - public static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - var entityCollection = builder.EntitySet("DollarCountEntities").EntityType.Collection; + public static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + var entityCollection = builder.EntitySet("DollarCountEntities").EntityType.Collection; - // Add unbound functions that return collection. - FunctionConfiguration function = builder.Function("UnboundFunctionReturnsPrimitveCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + // Add unbound functions that return collection. + FunctionConfiguration function = builder.Function("UnboundFunctionReturnsPrimitveCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = builder.Function("UnboundFunctionReturnsEnumCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = builder.Function("UnboundFunctionReturnsEnumCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = builder.Function("UnboundFunctionReturnsDateTimeOffsetCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = builder.Function("UnboundFunctionReturnsDateTimeOffsetCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = builder.Function("UnboundFunctionReturnsDateCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = builder.Function("UnboundFunctionReturnsDateCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = builder.Function("UnboundFunctionReturnsComplexCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = builder.Function("UnboundFunctionReturnsComplexCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = builder.Function("UnboundFunctionReturnsEntityCollection"); - function.IsComposable = true; - function.ReturnsCollectionFromEntitySet("DollarCountEntities"); + function = builder.Function("UnboundFunctionReturnsEntityCollection"); + function.IsComposable = true; + function.ReturnsCollectionFromEntitySet("DollarCountEntities"); - // Add bound functions that return collection. - function = entityCollection.Function("BoundFunctionReturnsPrimitveCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + // Add bound functions that return collection. + function = entityCollection.Function("BoundFunctionReturnsPrimitveCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = entityCollection.Function("BoundFunctionReturnsEnumCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = entityCollection.Function("BoundFunctionReturnsEnumCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = entityCollection.Function("BoundFunctionReturnsDateTimeOffsetCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = entityCollection.Function("BoundFunctionReturnsDateTimeOffsetCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = entityCollection.Function("BoundFunctionReturnsComplexCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = entityCollection.Function("BoundFunctionReturnsComplexCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = entityCollection.Function("BoundFunctionReturnsEntityCollection"); - function.IsComposable = true; - function.ReturnsCollectionFromEntitySet("DollarCountEntities"); + function = entityCollection.Function("BoundFunctionReturnsEntityCollection"); + function.IsComposable = true; + function.ReturnsCollectionFromEntitySet("DollarCountEntities"); - return builder.GetEdmModel(); - } + return builder.GetEdmModel(); } +} - public class DollarCountEntitiesController : ODataController - { - public IList Entities; +public class DollarCountEntitiesController : ODataController +{ + public IList Entities; - public DollarCountEntitiesController() + public DollarCountEntitiesController() + { + Entities = new List(); + for (int i = 1; i <= 10; i++) { - Entities = new List(); - for (int i = 1; i <= 10; i++) + if (i % 2 == 0) { - if (i % 2 == 0) + var newEntity = new DollarCountEntity { - var newEntity = new DollarCountEntity - { - Id = i, - StringCollectionProp = Enumerable.Range(1, 2).Select(index => index.ToString()).ToArray(), - EnumCollectionProp = new[] { DollarColor.Red, DollarColor.Blue | DollarColor.Green, DollarColor.Green }, - TimeSpanCollectionProp = Enumerable.Range(1, 4).Select(_ => TimeSpan.Zero).ToArray(), - ComplexCollectionProp = - Enumerable.Range(1, 5).Select(_ => new DollarCountComplex()).ToArray(), - EntityCollectionProp = Entities.ToArray(), - DollarCountNotAllowedCollectionProp = new[] { 1, 2, 3, 4 } - }; - Entities.Add(newEntity); - } - else + Id = i, + StringCollectionProp = Enumerable.Range(1, 2).Select(index => index.ToString()).ToArray(), + EnumCollectionProp = new[] { DollarColor.Red, DollarColor.Blue | DollarColor.Green, DollarColor.Green }, + TimeSpanCollectionProp = Enumerable.Range(1, 4).Select(_ => TimeSpan.Zero).ToArray(), + ComplexCollectionProp = + Enumerable.Range(1, 5).Select(_ => new DollarCountComplex()).ToArray(), + EntityCollectionProp = Entities.ToArray(), + DollarCountNotAllowedCollectionProp = new[] { 1, 2, 3, 4 } + }; + Entities.Add(newEntity); + } + else + { + var newEntity = new DerivedDollarCountEntity { - var newEntity = new DerivedDollarCountEntity - { - Id = i, - StringCollectionProp = Enumerable.Range(1, 2).Select(index => index.ToString()).ToArray(), - EnumCollectionProp = new[] { DollarColor.Red, DollarColor.Blue | DollarColor.Green, DollarColor.Green }, - TimeSpanCollectionProp = Enumerable.Range(1, 4).Select(_ => TimeSpan.Zero).ToArray(), - ComplexCollectionProp = - Enumerable.Range(1, 5).Select(_ => new DollarCountComplex()).ToArray(), - EntityCollectionProp = Entities.ToArray(), - DollarCountNotAllowedCollectionProp = new[] { 1, 2, 3, 4 }, - DerivedProp = "DerivedProp" - }; - Entities.Add(newEntity); - } + Id = i, + StringCollectionProp = Enumerable.Range(1, 2).Select(index => index.ToString()).ToArray(), + EnumCollectionProp = new[] { DollarColor.Red, DollarColor.Blue | DollarColor.Green, DollarColor.Green }, + TimeSpanCollectionProp = Enumerable.Range(1, 4).Select(_ => TimeSpan.Zero).ToArray(), + ComplexCollectionProp = + Enumerable.Range(1, 5).Select(_ => new DollarCountComplex()).ToArray(), + EntityCollectionProp = Entities.ToArray(), + DollarCountNotAllowedCollectionProp = new[] { 1, 2, 3, 4 }, + DerivedProp = "DerivedProp" + }; + Entities.Add(newEntity); } } + } - [EnableQuery(PageSize = 3)] - public IActionResult Get() - { - return Ok(Entities); - } - - [EnableQuery] - public IActionResult GetDollarCountEntitiesFromDerivedDollarCountEntity() - { - return Ok(Entities.OfType()); - } - - [EnableQuery] - public IActionResult Get(int key) - { - return Ok(Entities.Single(e => e.Id == key)); - } + [EnableQuery(PageSize = 3)] + public IActionResult Get() + { + return Ok(Entities); + } - [HttpGet] - public IActionResult GetStringCollectionProp(int key, ODataQueryOptions options) - { - IQueryable result = Entities.Single(e => e.Id == key).StringCollectionProp.AsQueryable(); + [EnableQuery] + public IActionResult GetDollarCountEntitiesFromDerivedDollarCountEntity() + { + return Ok(Entities.OfType()); + } - if (options.Filter != null) - { - result = options.Filter.ApplyTo(result, new ODataQuerySettings()).Cast(); - } + [EnableQuery] + public IActionResult Get(int key) + { + return Ok(Entities.Single(e => e.Id == key)); + } - ODataPath odataPath = Request.ODataFeature().Path; - if (odataPath.OfType().Any()) - { - return Ok(result.Count()); - } + [HttpGet] + public IActionResult GetStringCollectionProp(int key, ODataQueryOptions options) + { + IQueryable result = Entities.Single(e => e.Id == key).StringCollectionProp.AsQueryable(); - return Ok(result); + if (options.Filter != null) + { + result = options.Filter.ApplyTo(result, new ODataQuerySettings()).Cast(); } - [HttpGet("DollarCountEntities({key})/EnumCollectionProp/$count")] - public IActionResult GetCountForEnumCollectionProp(int key, ODataQueryOptions options) + ODataPath odataPath = Request.ODataFeature().Path; + if (odataPath.OfType().Any()) { - IQueryable result = Entities.Single(e => e.Id == key).EnumCollectionProp.AsQueryable(); - - if (options.Filter != null) - { - result = options.Filter.ApplyTo(result, new ODataQuerySettings()).Cast(); - } - return Ok(result.Count()); } - [EnableQuery] - public IActionResult GetTimeSpanCollectionProp(int key) - { - return Ok(Entities.Single(e => e.Id == key).TimeSpanCollectionProp); - } + return Ok(result); + } - [EnableQuery] - public IActionResult GetComplexCollectionProp(int key) - { - return Ok(Entities.Single(e => e.Id == key).ComplexCollectionProp); - } + [HttpGet("DollarCountEntities({key})/EnumCollectionProp/$count")] + public IActionResult GetCountForEnumCollectionProp(int key, ODataQueryOptions options) + { + IQueryable result = Entities.Single(e => e.Id == key).EnumCollectionProp.AsQueryable(); - [EnableQuery] - public IActionResult GetEntityCollectionProp(int key) + if (options.Filter != null) { - return Ok(Entities.Single(e => e.Id == key).EntityCollectionProp); + result = options.Filter.ApplyTo(result, new ODataQuerySettings()).Cast(); } - [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All ^ AllowedQueryOptions.Count)] - public IActionResult GetDollarCountNotAllowedCollectionProp(int key) - { - return Ok(Entities.Single(e => e.Id == key).EntityCollectionProp); - } + return Ok(result.Count()); + } - [HttpGet("UnboundFunctionReturnsPrimitveCollection()/$count")] - public IActionResult UnboundFunctionReturnsPrimitveCollectionWithDollarCount() - { - return Ok(6); - } + [EnableQuery] + public IActionResult GetTimeSpanCollectionProp(int key) + { + return Ok(Entities.Single(e => e.Id == key).TimeSpanCollectionProp); + } - [HttpGet("UnboundFunctionReturnsEnumCollection()/$count")] - public IActionResult UnboundFunctionReturnsEnumCollectionWithDollarCount() - { - return Ok(7); - } + [EnableQuery] + public IActionResult GetComplexCollectionProp(int key) + { + return Ok(Entities.Single(e => e.Id == key).ComplexCollectionProp); + } - [HttpGet("UnboundFunctionReturnsDateTimeOffsetCollection()/$count")] - public IActionResult UnboundFunctionReturnsDateTimeOffsetCollectionWithDollarCount() - { - return Ok(8); - } + [EnableQuery] + public IActionResult GetEntityCollectionProp(int key) + { + return Ok(Entities.Single(e => e.Id == key).EntityCollectionProp); + } - [HttpGet("UnboundFunctionReturnsDateCollection()/$count")] - public IActionResult UnboundFunctionReturnsDateCollectionWithDollarCount() - { - return Ok(18); - } + [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All ^ AllowedQueryOptions.Count)] + public IActionResult GetDollarCountNotAllowedCollectionProp(int key) + { + return Ok(Entities.Single(e => e.Id == key).EntityCollectionProp); + } - [HttpGet("UnboundFunctionReturnsComplexCollection()/$count")] - public IActionResult UnboundFunctionReturnsComplexCollectionWithDollarCount() - { - return Ok(9); - } + [HttpGet("UnboundFunctionReturnsPrimitveCollection()/$count")] + public IActionResult UnboundFunctionReturnsPrimitveCollectionWithDollarCount() + { + return Ok(6); + } - [HttpGet("UnboundFunctionReturnsEntityCollection()/$count")] - public IActionResult UnboundFunctionReturnsEntityCollectionWithDollarCount() - { - return Ok(10); - } + [HttpGet("UnboundFunctionReturnsEnumCollection()/$count")] + public IActionResult UnboundFunctionReturnsEnumCollectionWithDollarCount() + { + return Ok(7); + } - [HttpGet("UnboundFunctionReturnsEntityCollection()/Microsoft.AspNetCore.OData.E2E.Tests.Routing.DerivedDollarCountEntity/$count")] - public IActionResult UnboundFunctionReturnsDerivedEntityCollectionWithDollarCount() - { - return Ok(11); - } + [HttpGet("UnboundFunctionReturnsDateTimeOffsetCollection()/$count")] + public IActionResult UnboundFunctionReturnsDateTimeOffsetCollectionWithDollarCount() + { + return Ok(8); + } - [EnableQuery] - [HttpGet("DollarCountEntities/Default.BoundFunctionReturnsPrimitveCollection()/$count")] - public IActionResult BoundFunctionReturnsPrimitveCollectionOnCollectionOfDollarCountEntity() - { - return Ok(Enumerable.Range(1, 12).Select(_ => DateTimeOffset.Now)); - } - - [EnableQuery] - [HttpGet("DollarCountEntities/Default.BoundFunctionReturnsEnumCollection()/$count")] - public IActionResult BoundFunctionReturnsEnumCollectionOnCollectionOfDollarCountEntity() - { - return Ok(Enumerable.Range(1, 13).Select(_ => DollarColor.Green)); - } + [HttpGet("UnboundFunctionReturnsDateCollection()/$count")] + public IActionResult UnboundFunctionReturnsDateCollectionWithDollarCount() + { + return Ok(18); + } - [EnableQuery] - [HttpGet("DollarCountEntities/Default.BoundFunctionReturnsDateTimeOffsetCollection()/$count")] - public IActionResult BoundFunctionReturnsDateTimeOffsetCollectionOnCollectionOfDollarCountEntity() - { - return Ok(Enumerable.Range(1, 14).Select(_ => DateTimeOffset.Now)); - } + [HttpGet("UnboundFunctionReturnsComplexCollection()/$count")] + public IActionResult UnboundFunctionReturnsComplexCollectionWithDollarCount() + { + return Ok(9); + } - [EnableQuery] - [HttpGet("DollarCountEntities/Default.BoundFunctionReturnsComplexCollection()/$count")] - public IActionResult BoundFunctionReturnsComplexCollectionOnCollectionOfDollarCountEntity() - { - return Ok(Enumerable.Range(1, 15).Select(i => new DollarCountComplex { StringProp = i.ToString() })); - } + [HttpGet("UnboundFunctionReturnsEntityCollection()/$count")] + public IActionResult UnboundFunctionReturnsEntityCollectionWithDollarCount() + { + return Ok(10); + } - [EnableQuery] - [HttpGet("DollarCountEntities/Default.BoundFunctionReturnsEntityCollection()/$count")] - public IActionResult BoundFunctionReturnsEntityCollectionOnCollectionOfDollarCountEntity() - { - return Ok(Entities); - } + [HttpGet("UnboundFunctionReturnsEntityCollection()/Microsoft.AspNetCore.OData.E2E.Tests.Routing.DerivedDollarCountEntity/$count")] + public IActionResult UnboundFunctionReturnsDerivedEntityCollectionWithDollarCount() + { + return Ok(11); + } - [EnableQuery] - [HttpGet("DollarCountEntities/Default.BoundFunctionReturnsEntityCollection()/Microsoft.AspNetCore.OData.E2E.Tests.Routing.DerivedDollarCountEntity/$count")] - public IActionResult BoundFunctionReturnsDerivedEntityCollectionOnCollectionOfDollarCountEntity() - { - return Ok(Entities.OfType()); - } + [EnableQuery] + [HttpGet("DollarCountEntities/Default.BoundFunctionReturnsPrimitveCollection()/$count")] + public IActionResult BoundFunctionReturnsPrimitveCollectionOnCollectionOfDollarCountEntity() + { + return Ok(Enumerable.Range(1, 12).Select(_ => DateTimeOffset.Now)); + } + + [EnableQuery] + [HttpGet("DollarCountEntities/Default.BoundFunctionReturnsEnumCollection()/$count")] + public IActionResult BoundFunctionReturnsEnumCollectionOnCollectionOfDollarCountEntity() + { + return Ok(Enumerable.Range(1, 13).Select(_ => DollarColor.Green)); } - [Flags] - public enum DollarColor + [EnableQuery] + [HttpGet("DollarCountEntities/Default.BoundFunctionReturnsDateTimeOffsetCollection()/$count")] + public IActionResult BoundFunctionReturnsDateTimeOffsetCollectionOnCollectionOfDollarCountEntity() { - Red = 1, - Green = 2, - Blue = 4 + return Ok(Enumerable.Range(1, 14).Select(_ => DateTimeOffset.Now)); } - public class DollarCountEntity + [EnableQuery] + [HttpGet("DollarCountEntities/Default.BoundFunctionReturnsComplexCollection()/$count")] + public IActionResult BoundFunctionReturnsComplexCollectionOnCollectionOfDollarCountEntity() { - public int Id { get; set; } - public string[] StringCollectionProp { get; set; } - public DollarColor[] EnumCollectionProp { get; set; } - public TimeSpan[] TimeSpanCollectionProp { get; set; } - public DollarCountComplex[] ComplexCollectionProp { get; set; } - public DollarCountEntity[] EntityCollectionProp { get; set; } - public int[] DollarCountNotAllowedCollectionProp { get; set; } + return Ok(Enumerable.Range(1, 15).Select(i => new DollarCountComplex { StringProp = i.ToString() })); } - public class DerivedDollarCountEntity : DollarCountEntity + [EnableQuery] + [HttpGet("DollarCountEntities/Default.BoundFunctionReturnsEntityCollection()/$count")] + public IActionResult BoundFunctionReturnsEntityCollectionOnCollectionOfDollarCountEntity() { - public string DerivedProp { get; set; } + return Ok(Entities); } - public class DollarCountComplex + [EnableQuery] + [HttpGet("DollarCountEntities/Default.BoundFunctionReturnsEntityCollection()/Microsoft.AspNetCore.OData.E2E.Tests.Routing.DerivedDollarCountEntity/$count")] + public IActionResult BoundFunctionReturnsDerivedEntityCollectionOnCollectionOfDollarCountEntity() { - public string StringProp { get; set; } + return Ok(Entities.OfType()); } } + +[Flags] +public enum DollarColor +{ + Red = 1, + Green = 2, + Blue = 4 +} + +public class DollarCountEntity +{ + public int Id { get; set; } + public string[] StringCollectionProp { get; set; } + public DollarColor[] EnumCollectionProp { get; set; } + public TimeSpan[] TimeSpanCollectionProp { get; set; } + public DollarCountComplex[] ComplexCollectionProp { get; set; } + public DollarCountEntity[] EntityCollectionProp { get; set; } + public int[] DollarCountNotAllowedCollectionProp { get; set; } +} + +public class DerivedDollarCountEntity : DollarCountEntity +{ + public string DerivedProp { get; set; } +} + +public class DollarCountComplex +{ + public string StringProp { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataFunctionTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataFunctionTests.cs index 1138a8bee..6bad74ae4 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataFunctionTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataFunctionTests.cs @@ -24,564 +24,563 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing +namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing; + +public class ODataFunctionTests : WebApiTestBase { - public class ODataFunctionTests : WebApiTestBase - { - private const string PrimitiveValues = "(intValues=@p)?@p=[1, 2, null, 7, 8]"; + private const string PrimitiveValues = "(intValues=@p)?@p=[1, 2, null, 7, 8]"; - private const string ComplexValue1 = "{\"@odata.type\":\"%23NS.Address\",\"Street\":\"NE 24th St.\",\"City\":\"Redmond\"}"; - private const string ComplexValue2 = "{\"@odata.type\":\"%23NS.SubAddress\",\"Street\":\"LianHua Rd.\",\"City\":\"Shanghai\", \"Code\":9.9}"; + private const string ComplexValue1 = "{\"@odata.type\":\"%23NS.Address\",\"Street\":\"NE 24th St.\",\"City\":\"Redmond\"}"; + private const string ComplexValue2 = "{\"@odata.type\":\"%23NS.SubAddress\",\"Street\":\"LianHua Rd.\",\"City\":\"Shanghai\", \"Code\":9.9}"; - private const string ComplexValue = "(address=@p)?@p=" + ComplexValue1; - private const string CollectionComplex = "(addresses=@p)?@p=[" + ComplexValue1 + "," + ComplexValue2 + "]"; + private const string ComplexValue = "(address=@p)?@p=" + ComplexValue1; + private const string CollectionComplex = "(addresses=@p)?@p=[" + ComplexValue1 + "," + ComplexValue2 + "]"; - private const string EnumValue = "(color=NS.Color'Red')"; - private const string CollectionEnum = "(colors=@p)?@p=['Red', 'Green']"; + private const string EnumValue = "(color=NS.Color'Red')"; + private const string CollectionEnum = "(colors=@p)?@p=['Red', 'Green']"; - private const string EntityValue1 = "{\"@odata.type\":\"%23NS.Customer\",\"Id\":91,\"Name\":\"John\",\"Location\":" + ComplexValue1 + "}"; - private const string EntityValue2 = "{\"@odata.type\":\"%23NS.SpecialCustomer\",\"Id\":92,\"Name\":\"Mike\",\"Location\":" + ComplexValue2 + ",\"Title\":\"883F50C5-F554-4C49-98EA-F7CACB41658C\"}"; + private const string EntityValue1 = "{\"@odata.type\":\"%23NS.Customer\",\"Id\":91,\"Name\":\"John\",\"Location\":" + ComplexValue1 + "}"; + private const string EntityValue2 = "{\"@odata.type\":\"%23NS.SpecialCustomer\",\"Id\":92,\"Name\":\"Mike\",\"Location\":" + ComplexValue2 + ",\"Title\":\"883F50C5-F554-4C49-98EA-F7CACB41658C\"}"; - private const string EntityValue = "(customer=@p)?@p=" + EntityValue1; - private const string CollectionEntity = "(customers=@p)?@p=[" + EntityValue1 + "," + EntityValue2 + "]"; + private const string EntityValue = "(customer=@p)?@p=" + EntityValue1; + private const string CollectionEntity = "(customers=@p)?@p=[" + EntityValue1 + "," + EntityValue2 + "]"; - private const string EntityReference = "(customer=@p)?@p={\"@odata.id\":\"http://localhost/odata/FCustomers(8)\"}"; + private const string EntityReference = "(customer=@p)?@p={\"@odata.id\":\"http://localhost/odata/FCustomers(8)\"}"; - private const string EntityReferences = - "(customers=@p)?@p=[{\"@odata.id\":\"http://localhost/odata/FCustomers(81)\"},{\"@odata.id\":\"http://localhost/odata/FCustomers(82)/NS.SpecialCustomer\"}]"; + private const string EntityReferences = + "(customers=@p)?@p=[{\"@odata.id\":\"http://localhost/odata/FCustomers(81)\"},{\"@odata.id\":\"http://localhost/odata/FCustomers(82)/NS.SpecialCustomer\"}]"; - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = GetTypelessEdmModel(); - services.ConfigureControllers(typeof(FCustomersController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel).AddRouteComponents("attribute", edmModel)); - } + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = GetTypelessEdmModel(); + services.ConfigureControllers(typeof(FCustomersController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", edmModel).AddRouteComponents("attribute", edmModel)); + } - public ODataFunctionTests(WebApiTestFixture fixture) - : base(fixture) - { - } + public ODataFunctionTests(WebApiTestFixture fixture) + : base(fixture) + { + } #region Bound Function - public static TheoryDataSet BoundFunctionRouteData + public static TheoryDataSet BoundFunctionRouteData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { GetBoundFunction("IntCollectionFunction", PrimitiveValues) }, + { GetBoundFunction("IntCollectionFunction", PrimitiveValues) }, - { GetBoundFunction("ComplexFunction", ComplexValue) }, + { GetBoundFunction("ComplexFunction", ComplexValue) }, - { GetBoundFunction("ComplexCollectionFunction", CollectionComplex) }, + { GetBoundFunction("ComplexCollectionFunction", CollectionComplex) }, - { GetBoundFunction("EnumFunction", EnumValue) }, + { GetBoundFunction("EnumFunction", EnumValue) }, - { GetBoundFunction("EnumCollectionFunction", CollectionEnum) }, + { GetBoundFunction("EnumCollectionFunction", CollectionEnum) }, - { GetBoundFunction("EntityFunction", EntityValue) }, - { GetBoundFunction("EntityFunction", EntityReference) },// reference + { GetBoundFunction("EntityFunction", EntityValue) }, + { GetBoundFunction("EntityFunction", EntityReference) },// reference - { GetBoundFunction("CollectionEntityFunction", CollectionEntity) }, - { GetBoundFunction("CollectionEntityFunction", EntityReferences) },// references - }; - } + { GetBoundFunction("CollectionEntityFunction", CollectionEntity) }, + { GetBoundFunction("CollectionEntityFunction", EntityReferences) },// references + }; } + } - private static string GetBoundFunction(string functionName, string parameter) + private static string GetBoundFunction(string functionName, string parameter) + { + int key = 9; + if (parameter.Contains("@odata.id")) { - int key = 9; - if (parameter.Contains("@odata.id")) - { - key = 8; // used to check the result - } - - return "FCustomers(" + key + ")/NS." + functionName + parameter; + key = 8; // used to check the result } - [Theory] - [MemberData(nameof(BoundFunctionRouteData))] - public async Task BoundFunctionWorks_WithParameters_ForTypeless(string odataPath) - { - // Arrange - string requestUri = "odata/" + odataPath; - HttpClient client = CreateClient(); + return "FCustomers(" + key + ")/NS." + functionName + parameter; + } - // Act - var response = await client.GetAsync(requestUri); + [Theory] + [MemberData(nameof(BoundFunctionRouteData))] + public async Task BoundFunctionWorks_WithParameters_ForTypeless(string odataPath) + { + // Arrange + string requestUri = "odata/" + odataPath; + HttpClient client = CreateClient(); - // Assert - response.EnsureSuccessStatusCode(); - string responseString = await response.Content.ReadAsStringAsync(); - Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Edm.Boolean\",\"value\":true}", responseString); - } + // Act + var response = await client.GetAsync(requestUri); - [Theory] - [MemberData(nameof(BoundFunctionRouteData))] - public async Task BoundFunctionWorks_UsingAttributeRouting_WithParameters_ForTypeless(string odataPath) + // Assert + response.EnsureSuccessStatusCode(); + string responseString = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Edm.Boolean\",\"value\":true}", responseString); + } + + [Theory] + [MemberData(nameof(BoundFunctionRouteData))] + public async Task BoundFunctionWorks_UsingAttributeRouting_WithParameters_ForTypeless(string odataPath) + { + // Arrange + string requestUri = "attribute/" + odataPath; + if (requestUri.Contains("@odata.id")) { - // Arrange - string requestUri = "attribute/" + odataPath; - if (requestUri.Contains("@odata.id")) - { - requestUri = requestUri.Replace("http://localhost/odata", "http://localhost/attribute"); - } + requestUri = requestUri.Replace("http://localhost/odata", "http://localhost/attribute"); + } - HttpClient client = CreateClient(); + HttpClient client = CreateClient(); - // Act - var response = await client.GetAsync(requestUri); + // Act + var response = await client.GetAsync(requestUri); - // Assert - response.EnsureSuccessStatusCode(); - string responseString = await response.Content.ReadAsStringAsync(); - Assert.Equal("{\"@odata.context\":\"http://localhost/attribute/$metadata#Edm.Boolean\",\"value\":true}", responseString); - } + // Assert + response.EnsureSuccessStatusCode(); + string responseString = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"@odata.context\":\"http://localhost/attribute/$metadata#Edm.Boolean\",\"value\":true}", responseString); + } #endregion #region Unbound Function - public static TheoryDataSet UnboundFunctionRouteData + public static TheoryDataSet UnboundFunctionRouteData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { GetUnboundFunction("UnboundIntCollectionFunction", PrimitiveValues) }, + { GetUnboundFunction("UnboundIntCollectionFunction", PrimitiveValues) }, - { GetUnboundFunction("UnboundComplexFunction", ComplexValue) }, + { GetUnboundFunction("UnboundComplexFunction", ComplexValue) }, - { GetUnboundFunction("UnboundComplexCollectionFunction", CollectionComplex) }, + { GetUnboundFunction("UnboundComplexCollectionFunction", CollectionComplex) }, - { GetUnboundFunction("UnboundEnumFunction", EnumValue) }, + { GetUnboundFunction("UnboundEnumFunction", EnumValue) }, - { GetUnboundFunction("UnboundEnumCollectionFunction", CollectionEnum) }, + { GetUnboundFunction("UnboundEnumCollectionFunction", CollectionEnum) }, - { GetUnboundFunction("UnboundEntityFunction", EntityValue) }, - { GetUnboundFunction("UnboundEntityFunction", EntityReference) },// reference + { GetUnboundFunction("UnboundEntityFunction", EntityValue) }, + { GetUnboundFunction("UnboundEntityFunction", EntityReference) },// reference - { GetUnboundFunction("UnboundCollectionEntityFunction", CollectionEntity) }, - { GetUnboundFunction("UnboundCollectionEntityFunction", EntityReferences) }, // references - }; - } + { GetUnboundFunction("UnboundCollectionEntityFunction", CollectionEntity) }, + { GetUnboundFunction("UnboundCollectionEntityFunction", EntityReferences) }, // references + }; } + } - private static string GetUnboundFunction(string functionName, string parameter) + private static string GetUnboundFunction(string functionName, string parameter) + { + int key = 9; + if (parameter.Contains("@odata.id")) { - int key = 9; - if (parameter.Contains("@odata.id")) - { - key = 8; // used to check the result - } - - parameter = parameter.Insert(1, "key=" + key + ","); - return functionName + parameter; + key = 8; // used to check the result } - [Theory] - [MemberData(nameof(UnboundFunctionRouteData))] - public async Task UnboundFunctionWorks_WithParameters_WithAttributeRouting_ForTypeless(string odataPath) - { - // Arrange - string requestUri = "odata/" + odataPath; - HttpClient client = CreateClient(); + parameter = parameter.Insert(1, "key=" + key + ","); + return functionName + parameter; + } - // Act - var response = await client.GetAsync(requestUri); + [Theory] + [MemberData(nameof(UnboundFunctionRouteData))] + public async Task UnboundFunctionWorks_WithParameters_WithAttributeRouting_ForTypeless(string odataPath) + { + // Arrange + string requestUri = "odata/" + odataPath; + HttpClient client = CreateClient(); - // Assert - response.EnsureSuccessStatusCode(); - string responseString = await response.Content.ReadAsStringAsync(); - Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Edm.Boolean\",\"value\":true}", responseString); - } + // Act + var response = await client.GetAsync(requestUri); + + // Assert + response.EnsureSuccessStatusCode(); + string responseString = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#Edm.Boolean\",\"value\":true}", responseString); + } #endregion - private static IEdmModel GetTypelessEdmModel() - { - EdmModel model = new EdmModel(); + private static IEdmModel GetTypelessEdmModel() + { + EdmModel model = new EdmModel(); - // Enum type "Color" - EdmEnumType colorEnum = new EdmEnumType("NS", "Color"); - colorEnum.AddMember(new EdmEnumMember(colorEnum, "Red", new EdmEnumMemberValue(0))); - colorEnum.AddMember(new EdmEnumMember(colorEnum, "Blue", new EdmEnumMemberValue(1))); - colorEnum.AddMember(new EdmEnumMember(colorEnum, "Green", new EdmEnumMemberValue(2))); - model.AddElement(colorEnum); + // Enum type "Color" + EdmEnumType colorEnum = new EdmEnumType("NS", "Color"); + colorEnum.AddMember(new EdmEnumMember(colorEnum, "Red", new EdmEnumMemberValue(0))); + colorEnum.AddMember(new EdmEnumMember(colorEnum, "Blue", new EdmEnumMemberValue(1))); + colorEnum.AddMember(new EdmEnumMember(colorEnum, "Green", new EdmEnumMemberValue(2))); + model.AddElement(colorEnum); - // complex type "Address" - EdmComplexType address = new EdmComplexType("NS", "Address"); - address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); - address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); - model.AddElement(address); + // complex type "Address" + EdmComplexType address = new EdmComplexType("NS", "Address"); + address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + model.AddElement(address); - // derived complex type "SubAddress" - EdmComplexType subAddress = new EdmComplexType("NS", "SubAddress", address); - subAddress.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Double); - model.AddElement(subAddress); + // derived complex type "SubAddress" + EdmComplexType subAddress = new EdmComplexType("NS", "SubAddress", address); + subAddress.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Double); + model.AddElement(subAddress); - // entity type "Customer" - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - customer.AddStructuralProperty("Location", new EdmComplexTypeReference(address, isNullable: true)); - model.AddElement(customer); + // entity type "Customer" + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + customer.AddStructuralProperty("Location", new EdmComplexTypeReference(address, isNullable: true)); + model.AddElement(customer); - // derived entity type special customer - EdmEntityType specialCustomer = new EdmEntityType("NS", "SpecialCustomer", customer); - specialCustomer.AddStructuralProperty("Title", EdmPrimitiveTypeKind.Guid); - model.AddElement(specialCustomer); + // derived entity type special customer + EdmEntityType specialCustomer = new EdmEntityType("NS", "SpecialCustomer", customer); + specialCustomer.AddStructuralProperty("Title", EdmPrimitiveTypeKind.Guid); + model.AddElement(specialCustomer); - // entity sets - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - model.AddElement(container); - container.AddEntitySet("FCustomers", customer); + // entity sets + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + model.AddElement(container); + container.AddEntitySet("FCustomers", customer); - EdmComplexTypeReference complexType = new EdmComplexTypeReference(address, isNullable: true); - EdmCollectionTypeReference complexCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(complexType)); + EdmComplexTypeReference complexType = new EdmComplexTypeReference(address, isNullable: true); + EdmCollectionTypeReference complexCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(complexType)); - EdmEnumTypeReference enumType = new EdmEnumTypeReference(colorEnum, isNullable: false); - EdmCollectionTypeReference enumCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(enumType)); + EdmEnumTypeReference enumType = new EdmEnumTypeReference(colorEnum, isNullable: false); + EdmCollectionTypeReference enumCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(enumType)); - EdmEntityTypeReference entityType = new EdmEntityTypeReference(customer, isNullable: false); - EdmCollectionTypeReference entityCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(entityType)); + EdmEntityTypeReference entityType = new EdmEntityTypeReference(customer, isNullable: false); + EdmCollectionTypeReference entityCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(entityType)); - IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: true); - EdmCollectionTypeReference primitiveCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(intType)); + IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: true); + EdmCollectionTypeReference primitiveCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(intType)); - // bound functions - BoundFunction(model, "IntCollectionFunction", "intValues", primitiveCollectionType, entityType); + // bound functions + BoundFunction(model, "IntCollectionFunction", "intValues", primitiveCollectionType, entityType); - BoundFunction(model, "ComplexFunction", "address", complexType, entityType); + BoundFunction(model, "ComplexFunction", "address", complexType, entityType); - BoundFunction(model, "ComplexCollectionFunction", "addresses", complexCollectionType, entityType); + BoundFunction(model, "ComplexCollectionFunction", "addresses", complexCollectionType, entityType); - BoundFunction(model, "EnumFunction", "color", enumType, entityType); + BoundFunction(model, "EnumFunction", "color", enumType, entityType); - BoundFunction(model, "EnumCollectionFunction", "colors", enumCollectionType, entityType); + BoundFunction(model, "EnumCollectionFunction", "colors", enumCollectionType, entityType); - BoundFunction(model, "EntityFunction", "customer", entityType, entityType); + BoundFunction(model, "EntityFunction", "customer", entityType, entityType); - BoundFunction(model, "CollectionEntityFunction", "customers", entityCollectionType, entityType); + BoundFunction(model, "CollectionEntityFunction", "customers", entityCollectionType, entityType); - // unbound functions - UnboundFunction(container, "UnboundIntCollectionFunction", "intValues", primitiveCollectionType); + // unbound functions + UnboundFunction(container, "UnboundIntCollectionFunction", "intValues", primitiveCollectionType); - UnboundFunction(container, "UnboundComplexFunction", "address", complexType); + UnboundFunction(container, "UnboundComplexFunction", "address", complexType); - UnboundFunction(container, "UnboundComplexCollectionFunction", "addresses", complexCollectionType); + UnboundFunction(container, "UnboundComplexCollectionFunction", "addresses", complexCollectionType); - UnboundFunction(container, "UnboundEnumFunction", "color", enumType); + UnboundFunction(container, "UnboundEnumFunction", "color", enumType); - UnboundFunction(container, "UnboundEnumCollectionFunction", "colors", enumCollectionType); + UnboundFunction(container, "UnboundEnumCollectionFunction", "colors", enumCollectionType); - UnboundFunction(container, "UnboundEntityFunction", "customer", entityType); + UnboundFunction(container, "UnboundEntityFunction", "customer", entityType); - UnboundFunction(container, "UnboundCollectionEntityFunction", "customers", entityCollectionType); + UnboundFunction(container, "UnboundCollectionEntityFunction", "customers", entityCollectionType); - // bound to collection - BoundToCollectionFunction(model, "BoundToCollectionFunction", "p", intType, entityType); + // bound to collection + BoundToCollectionFunction(model, "BoundToCollectionFunction", "p", intType, entityType); - model.SetAnnotationValue(model, new BindableOperationFinder(model)); - return model; - } + model.SetAnnotationValue(model, new BindableOperationFinder(model)); + return model; + } - private static void BoundFunction(EdmModel model, string funcName, string paramName, IEdmTypeReference edmType, IEdmEntityTypeReference bindingType) - { - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + private static void BoundFunction(EdmModel model, string funcName, string paramName, IEdmTypeReference edmType, IEdmEntityTypeReference bindingType) + { + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - EdmFunction boundFunction = new EdmFunction("NS", funcName, returnType, isBound: true, entitySetPathExpression: null, isComposable: false); - boundFunction.AddParameter("entity", bindingType); - boundFunction.AddParameter(paramName, edmType); - model.AddElement(boundFunction); - } + EdmFunction boundFunction = new EdmFunction("NS", funcName, returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + boundFunction.AddParameter("entity", bindingType); + boundFunction.AddParameter(paramName, edmType); + model.AddElement(boundFunction); + } - private static void BoundToCollectionFunction(EdmModel model, string funcName, string paramName, IEdmTypeReference edmType, IEdmEntityTypeReference bindingType) - { - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmCollectionTypeReference collectonType = new EdmCollectionTypeReference(new EdmCollectionType(bindingType)); - EdmFunction boundFunction = new EdmFunction("NS", funcName, returnType, isBound: true, entitySetPathExpression: null, isComposable: false); - boundFunction.AddParameter("entityset", collectonType); - boundFunction.AddParameter(paramName, edmType); - model.AddElement(boundFunction); - } + private static void BoundToCollectionFunction(EdmModel model, string funcName, string paramName, IEdmTypeReference edmType, IEdmEntityTypeReference bindingType) + { + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmCollectionTypeReference collectonType = new EdmCollectionTypeReference(new EdmCollectionType(bindingType)); + EdmFunction boundFunction = new EdmFunction("NS", funcName, returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + boundFunction.AddParameter("entityset", collectonType); + boundFunction.AddParameter(paramName, edmType); + model.AddElement(boundFunction); + } - private static void UnboundFunction(EdmEntityContainer container, string funcName, string paramName, IEdmTypeReference edmType) - { - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + private static void UnboundFunction(EdmEntityContainer container, string funcName, string paramName, IEdmTypeReference edmType) + { + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - var unboundFunction = new EdmFunction("NS", funcName, returnType, isBound: false, entitySetPathExpression: null, isComposable: true); - unboundFunction.AddParameter("key", EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false)); - unboundFunction.AddParameter(paramName, edmType); - container.AddFunctionImport(funcName, unboundFunction, entitySet: null); - } + var unboundFunction = new EdmFunction("NS", funcName, returnType, isBound: false, entitySetPathExpression: null, isComposable: true); + unboundFunction.AddParameter("key", EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false)); + unboundFunction.AddParameter(paramName, edmType); + container.AddFunctionImport(funcName, unboundFunction, entitySet: null); } +} - public class FCustomersController : ODataController +public class FCustomersController : ODataController +{ + [EnableQuery] + public IActionResult Get() { - [EnableQuery] - public IActionResult Get() - { - IEdmModel model = Request.GetModel(); - IEdmEntityType customerType = model.SchemaElements.OfType().First(e => e.Name == "Customer"); + IEdmModel model = Request.GetModel(); + IEdmEntityType customerType = model.SchemaElements.OfType().First(e => e.Name == "Customer"); - EdmEntityObject customer = new EdmEntityObject(customerType); + EdmEntityObject customer = new EdmEntityObject(customerType); - customer.TrySetPropertyValue("Id", 1); - customer.TrySetPropertyValue("Tony", 1); + customer.TrySetPropertyValue("Id", 1); + customer.TrySetPropertyValue("Tony", 1); - EdmEntityObjectCollection customers = - new EdmEntityObjectCollection( - new EdmCollectionTypeReference(new EdmCollectionType(customerType.ToEdmTypeReference(false)))); - customers.Add(customer); - return Ok(customers); - } + EdmEntityObjectCollection customers = + new EdmEntityObjectCollection( + new EdmCollectionTypeReference(new EdmCollectionType(customerType.ToEdmTypeReference(false)))); + customers.Add(customer); + return Ok(customers); + } #region Bound Function using attribute routing - [HttpGet("attribute/FCustomers({key})/NS.IntCollectionFunction(intValues={intValues})")] - public bool IntCollectionFunctionOnAttriubte(int key, [FromODataUri] IEnumerable intValues) - { - return IntCollectionFunction(key, intValues); - } + [HttpGet("attribute/FCustomers({key})/NS.IntCollectionFunction(intValues={intValues})")] + public bool IntCollectionFunctionOnAttriubte(int key, [FromODataUri] IEnumerable intValues) + { + return IntCollectionFunction(key, intValues); + } - [HttpGet("attribute/FCustomers({key})/NS.EnumFunction(color={color})")] - public bool EnumFunctionOnAttribute(int key, [FromODataUri] EdmEnumObject color) - { - return EnumFunction(key, color); - } + [HttpGet("attribute/FCustomers({key})/NS.EnumFunction(color={color})")] + public bool EnumFunctionOnAttribute(int key, [FromODataUri] EdmEnumObject color) + { + return EnumFunction(key, color); + } - [HttpGet("attribute/FCustomers({key})/NS.EnumCollectionFunction(colors={colors})")] - public bool EnumCollectionFunctionOnAttribute(int key, [FromODataUri] EdmEnumObjectCollection colors) - { - return EnumCollectionFunction(key, colors); - } + [HttpGet("attribute/FCustomers({key})/NS.EnumCollectionFunction(colors={colors})")] + public bool EnumCollectionFunctionOnAttribute(int key, [FromODataUri] EdmEnumObjectCollection colors) + { + return EnumCollectionFunction(key, colors); + } - [HttpGet("attribute/FCustomers({key})/NS.ComplexFunction(address={address})")] - public bool ComplexFunctionOnAttribute(int key, [FromODataUri] EdmComplexObject address) - { - return ComplexFunction(key, address); - } + [HttpGet("attribute/FCustomers({key})/NS.ComplexFunction(address={address})")] + public bool ComplexFunctionOnAttribute(int key, [FromODataUri] EdmComplexObject address) + { + return ComplexFunction(key, address); + } - [HttpGet("attribute/FCustomers({key})/NS.ComplexCollectionFunction(addresses={addresses})")] - public bool ComplexCollectionFunctionOnAttribute(int key, [FromODataUri] EdmComplexObjectCollection addresses) - { - return ComplexCollectionFunction(key, addresses); - } + [HttpGet("attribute/FCustomers({key})/NS.ComplexCollectionFunction(addresses={addresses})")] + public bool ComplexCollectionFunctionOnAttribute(int key, [FromODataUri] EdmComplexObjectCollection addresses) + { + return ComplexCollectionFunction(key, addresses); + } - [HttpGet("attribute/FCustomers({key})/NS.EntityFunction(customer={customer})")] - public bool EntityFunctionOnAttribute(int key, [FromODataUri] EdmEntityObject customer) - { - return EntityFunction(key, customer); - } + [HttpGet("attribute/FCustomers({key})/NS.EntityFunction(customer={customer})")] + public bool EntityFunctionOnAttribute(int key, [FromODataUri] EdmEntityObject customer) + { + return EntityFunction(key, customer); + } - [HttpGet("attribute/FCustomers({key})/NS.CollectionEntityFunction(customers={customers})")] - public bool CollectionEntityFunctionOnAttribute(int key, [FromODataUri] EdmEntityObjectCollection customers) - { - return CollectionEntityFunction(key, customers); - } + [HttpGet("attribute/FCustomers({key})/NS.CollectionEntityFunction(customers={customers})")] + public bool CollectionEntityFunctionOnAttribute(int key, [FromODataUri] EdmEntityObjectCollection customers) + { + return CollectionEntityFunction(key, customers); + } #endregion #region Bound function using convention routing & Unbound function using Attribute routing - // Here's the note: - // [HttpGet] & [ODataModel] will create an odata convention routing for this method. - // [HttpGet("odata/....")] will create an attribute routing. - [HttpGet] - [ODataRouteComponent("odata")] - [HttpGet("odata/UnboundIntCollectionFunction(key={key},intValues={intValues})")] - public bool IntCollectionFunction(int key, [FromODataUri] IEnumerable intValues) - { - Assert.NotNull(intValues); + // Here's the note: + // [HttpGet] & [ODataModel] will create an odata convention routing for this method. + // [HttpGet("odata/....")] will create an attribute routing. + [HttpGet] + [ODataRouteComponent("odata")] + [HttpGet("odata/UnboundIntCollectionFunction(key={key},intValues={intValues})")] + public bool IntCollectionFunction(int key, [FromODataUri] IEnumerable intValues) + { + Assert.NotNull(intValues); - IList values = intValues.ToList(); - Assert.Equal(1, values[0]); - Assert.Equal(2, values[1]); - Assert.Null(values[2]); - Assert.Equal(7, values[3]); - Assert.Equal(8, values[4]); + IList values = intValues.ToList(); + Assert.Equal(1, values[0]); + Assert.Equal(2, values[1]); + Assert.Null(values[2]); + Assert.Equal(7, values[3]); + Assert.Equal(8, values[4]); - return true; - } + return true; + } - [HttpGet] - [ODataRouteComponent("odata")] - [HttpGet("odata/UnboundEnumFunction(key={key},color={color})")] - public bool EnumFunction(int key, [FromODataUri] EdmEnumObject color) + [HttpGet] + [ODataRouteComponent("odata")] + [HttpGet("odata/UnboundEnumFunction(key={key},color={color})")] + public bool EnumFunction(int key, [FromODataUri] EdmEnumObject color) + { + Assert.NotNull(color); + Assert.Equal("NS.Color", color.GetEdmType().FullName()); + Assert.Equal("0", color.Value); + return true; + } + + [HttpGet] + [ODataRouteComponent("odata")] + [HttpGet("odata/UnboundEnumCollectionFunction(key={key},colors={colors})")] + public bool EnumCollectionFunction(int key, [FromODataUri] EdmEnumObjectCollection colors) + { + Assert.NotNull(colors); + IList results = colors.ToList(); + + Assert.Equal(2, results.Count); + + // #1 + EdmEnumObject color = results[0] as EdmEnumObject; + Assert.NotNull(color); + Assert.Equal("NS.Color", color.GetEdmType().FullName()); + Assert.Equal("Red", color.Value); + + // #2 + EdmEnumObject color2 = results[1] as EdmEnumObject; + Assert.NotNull(color2); + Assert.Equal("NS.Color", color2.GetEdmType().FullName()); + Assert.Equal("Green", color2.Value); + return true; + } + + [HttpGet] + [ODataRouteComponent("odata")] + [HttpGet("odata/UnboundComplexFunction(key={key},address={address})")] + public bool ComplexFunction(int key, [FromODataUri] EdmComplexObject address) + { + if (key == 99) { - Assert.NotNull(color); - Assert.Equal("NS.Color", color.GetEdmType().FullName()); - Assert.Equal("0", color.Value); - return true; + Assert.Null(address); + return false; } - [HttpGet] - [ODataRouteComponent("odata")] - [HttpGet("odata/UnboundEnumCollectionFunction(key={key},colors={colors})")] - public bool EnumCollectionFunction(int key, [FromODataUri] EdmEnumObjectCollection colors) - { - Assert.NotNull(colors); - IList results = colors.ToList(); + Assert.NotNull(address); + dynamic result = address; + Assert.Equal("NS.Address", address.GetEdmType().FullName()); + Assert.Equal("NE 24th St.", result.Street); + Assert.Equal("Redmond", result.City); + return true; + } - Assert.Equal(2, results.Count); + [HttpGet] + [ODataRouteComponent("odata")] + [HttpGet("odata/UnboundComplexCollectionFunction(key={key},addresses={addresses})")] + public bool ComplexCollectionFunction(int key, [FromODataUri] EdmComplexObjectCollection addresses) + { + Assert.NotNull(addresses); + IList results = addresses.ToList(); + + Assert.Equal(2, results.Count); + + // #1 + EdmComplexObject complex = results[0] as EdmComplexObject; + Assert.Equal("NS.Address", complex.GetEdmType().FullName()); + + dynamic address = results[0]; + Assert.NotNull(address); + Assert.Equal("NE 24th St.", address.Street); + Assert.Equal("Redmond", address.City); + + // #2 + complex = results[1] as EdmComplexObject; + Assert.Equal("NS.SubAddress", complex.GetEdmType().FullName()); + + address = results[1]; + Assert.NotNull(address); + Assert.Equal("LianHua Rd.", address.Street); + Assert.Equal("Shanghai", address.City); + Assert.Equal(9.9, address.Code); + return true; + } - // #1 - EdmEnumObject color = results[0] as EdmEnumObject; - Assert.NotNull(color); - Assert.Equal("NS.Color", color.GetEdmType().FullName()); - Assert.Equal("Red", color.Value); + [HttpGet] + [ODataRouteComponent("odata")] + [HttpGet("odata/UnboundEntityFunction(key={key},customer={customer})")] + public bool EntityFunction(int key, [FromODataUri] EdmEntityObject customer) + { + Assert.NotNull(customer); + dynamic result = customer; + Assert.Equal("NS.Customer", customer.GetEdmType().FullName()); - // #2 - EdmEnumObject color2 = results[1] as EdmEnumObject; - Assert.NotNull(color2); - Assert.Equal("NS.Color", color2.GetEdmType().FullName()); - Assert.Equal("Green", color2.Value); - return true; - } + // entity call + if (key == 9) + { + Assert.Equal(91, result.Id); + Assert.Equal("John", result.Name); - [HttpGet] - [ODataRouteComponent("odata")] - [HttpGet("odata/UnboundComplexFunction(key={key},address={address})")] - public bool ComplexFunction(int key, [FromODataUri] EdmComplexObject address) + dynamic address = result.Location; + EdmComplexObject addressObj = Assert.IsType(address); + Assert.Equal("NS.Address", addressObj.GetEdmType().FullName()); + Assert.Equal("NE 24th St.", address.Street); + Assert.Equal("Redmond", address.City); + } + else { - if (key == 99) - { - Assert.Null(address); - return false; - } + // entity reference call + Assert.Equal(8, result.Id); + Assert.Equal("Id", String.Join(",", customer.GetChangedPropertyNames())); - Assert.NotNull(address); - dynamic result = address; - Assert.Equal("NS.Address", address.GetEdmType().FullName()); - Assert.Equal("NE 24th St.", result.Street); - Assert.Equal("Redmond", result.City); - return true; + Assert.Equal("Name,Location", String.Join(",", customer.GetUnchangedPropertyNames())); } - [HttpGet] - [ODataRouteComponent("odata")] - [HttpGet("odata/UnboundComplexCollectionFunction(key={key},addresses={addresses})")] - public bool ComplexCollectionFunction(int key, [FromODataUri] EdmComplexObjectCollection addresses) - { - Assert.NotNull(addresses); - IList results = addresses.ToList(); + return true; + } - Assert.Equal(2, results.Count); + [HttpGet] + [ODataRouteComponent("odata")] + [HttpGet("odata/UnboundCollectionEntityFunction(key={key},customers={customers})")] + public bool CollectionEntityFunction(int key, [FromODataUri] EdmEntityObjectCollection customers) + { + Assert.NotNull(customers); + IList results = customers.ToList(); + Assert.Equal(2, results.Count); + // entities call + if (key == 9) + { // #1 - EdmComplexObject complex = results[0] as EdmComplexObject; - Assert.Equal("NS.Address", complex.GetEdmType().FullName()); + EdmEntityObject entity = results[0] as EdmEntityObject; + Assert.NotNull(entity); + Assert.Equal("NS.Customer", entity.GetEdmType().FullName()); + + dynamic customer = results[0]; + Assert.Equal(91, customer.Id); + Assert.Equal("John", customer.Name); - dynamic address = results[0]; - Assert.NotNull(address); + dynamic address = customer.Location; + EdmComplexObject addressObj = Assert.IsType(address); + Assert.Equal("NS.Address", addressObj.GetEdmType().FullName()); Assert.Equal("NE 24th St.", address.Street); Assert.Equal("Redmond", address.City); // #2 - complex = results[1] as EdmComplexObject; - Assert.Equal("NS.SubAddress", complex.GetEdmType().FullName()); + entity = results[1] as EdmEntityObject; + Assert.Equal("NS.SpecialCustomer", entity.GetEdmType().FullName()); - address = results[1]; - Assert.NotNull(address); + customer = results[1]; + Assert.Equal(92, customer.Id); + Assert.Equal("Mike", customer.Name); + + address = customer.Location; + addressObj = Assert.IsType(address); + Assert.Equal("NS.SubAddress", addressObj.GetEdmType().FullName()); Assert.Equal("LianHua Rd.", address.Street); Assert.Equal("Shanghai", address.City); Assert.Equal(9.9, address.Code); - return true; - } - - [HttpGet] - [ODataRouteComponent("odata")] - [HttpGet("odata/UnboundEntityFunction(key={key},customer={customer})")] - public bool EntityFunction(int key, [FromODataUri] EdmEntityObject customer) - { - Assert.NotNull(customer); - dynamic result = customer; - Assert.Equal("NS.Customer", customer.GetEdmType().FullName()); - - // entity call - if (key == 9) - { - Assert.Equal(91, result.Id); - Assert.Equal("John", result.Name); - - dynamic address = result.Location; - EdmComplexObject addressObj = Assert.IsType(address); - Assert.Equal("NS.Address", addressObj.GetEdmType().FullName()); - Assert.Equal("NE 24th St.", address.Street); - Assert.Equal("Redmond", address.City); - } - else - { - // entity reference call - Assert.Equal(8, result.Id); - Assert.Equal("Id", String.Join(",", customer.GetChangedPropertyNames())); - Assert.Equal("Name,Location", String.Join(",", customer.GetUnchangedPropertyNames())); - } - - return true; + Assert.Equal(new Guid("883F50C5-F554-4C49-98EA-F7CACB41658C"), customer.Title); } - - [HttpGet] - [ODataRouteComponent("odata")] - [HttpGet("odata/UnboundCollectionEntityFunction(key={key},customers={customers})")] - public bool CollectionEntityFunction(int key, [FromODataUri] EdmEntityObjectCollection customers) + else { - Assert.NotNull(customers); - IList results = customers.ToList(); - Assert.Equal(2, results.Count); - - // entities call - if (key == 9) + // entity references call + int id = 81; + foreach (IEdmEntityObject edmObj in results) { - // #1 - EdmEntityObject entity = results[0] as EdmEntityObject; + EdmEntityObject entity = edmObj as EdmEntityObject; Assert.NotNull(entity); Assert.Equal("NS.Customer", entity.GetEdmType().FullName()); - dynamic customer = results[0]; - Assert.Equal(91, customer.Id); - Assert.Equal("John", customer.Name); - - dynamic address = customer.Location; - EdmComplexObject addressObj = Assert.IsType(address); - Assert.Equal("NS.Address", addressObj.GetEdmType().FullName()); - Assert.Equal("NE 24th St.", address.Street); - Assert.Equal("Redmond", address.City); - - // #2 - entity = results[1] as EdmEntityObject; - Assert.Equal("NS.SpecialCustomer", entity.GetEdmType().FullName()); - - customer = results[1]; - Assert.Equal(92, customer.Id); - Assert.Equal("Mike", customer.Name); - - address = customer.Location; - addressObj = Assert.IsType(address); - Assert.Equal("NS.SubAddress", addressObj.GetEdmType().FullName()); - Assert.Equal("LianHua Rd.", address.Street); - Assert.Equal("Shanghai", address.City); - Assert.Equal(9.9, address.Code); - - Assert.Equal(new Guid("883F50C5-F554-4C49-98EA-F7CACB41658C"), customer.Title); - } - else - { - // entity references call - int id = 81; - foreach (IEdmEntityObject edmObj in results) - { - EdmEntityObject entity = edmObj as EdmEntityObject; - Assert.NotNull(entity); - Assert.Equal("NS.Customer", entity.GetEdmType().FullName()); - - dynamic customer = entity; - Assert.Equal(id++, customer.Id); - Assert.Equal("Id", String.Join(",", customer.GetChangedPropertyNames())); - Assert.Equal("Name,Location", String.Join(",", customer.GetUnchangedPropertyNames())); - } + dynamic customer = entity; + Assert.Equal(id++, customer.Id); + Assert.Equal("Id", String.Join(",", customer.GetChangedPropertyNames())); + Assert.Equal("Name,Location", String.Join(",", customer.GetUnchangedPropertyNames())); } - - return true; } -#endregion + + return true; } +#endregion } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataRoutingTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataRoutingTests.cs index 7c604cfe9..07fbb992f 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataRoutingTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Routing/ODataRoutingTests.cs @@ -15,145 +15,144 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing +namespace Microsoft.AspNetCore.OData.E2E.Tests.Routing; + +public class ODataRoutingTests : WebApiTestBase { - public class ODataRoutingTests : WebApiTestBase + public ODataRoutingTests(WebApiTestFixture fixture) + : base(fixture) { - public ODataRoutingTests(WebApiTestFixture fixture) - : base(fixture) - { - } - - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(ProductsController), typeof(CategoriesController)); - - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Products"); - builder.EntitySet("Categories"); - var model = builder.GetEdmModel(); + } - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model)); - } + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(ProductsController), typeof(CategoriesController)); - [Theory] - [InlineData("1", true, "\"Id\":1,\"Name\":\"OData Product\"}")] - [InlineData("11", true, "\"Id\":11,\"Name\":\"OData Product\"}")] - [InlineData("42", true, "\"Id\":42,\"Name\":\"OData Product\"}")] - [InlineData("other", false, "{\"id\":11,\"name\":\"Non-OData Product\"}")] - public async Task RequestContainsValidValues_CanRouteToCorrectEndpoint(string key, bool isODataRouting, string expect) - { - // Arrange - HttpClient client = CreateClient(); + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Products"); + builder.EntitySet("Categories"); + var model = builder.GetEdmModel(); - // Act - var response = await client.GetAsync($"/odata/products/{key}"); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model)); + } - // Assert - response.EnsureSuccessStatusCode(); + [Theory] + [InlineData("1", true, "\"Id\":1,\"Name\":\"OData Product\"}")] + [InlineData("11", true, "\"Id\":11,\"Name\":\"OData Product\"}")] + [InlineData("42", true, "\"Id\":42,\"Name\":\"OData Product\"}")] + [InlineData("other", false, "{\"id\":11,\"name\":\"Non-OData Product\"}")] + public async Task RequestContainsValidValues_CanRouteToCorrectEndpoint(string key, bool isODataRouting, string expect) + { + // Arrange + HttpClient client = CreateClient(); - string payload = await response.Content.ReadAsStringAsync(); - if (isODataRouting) - { - expect = $"{{\"@odata.context\":\"http://localhost/odata/$metadata#Products/$entity\",{expect}"; - } + // Act + var response = await client.GetAsync($"/odata/products/{key}"); - Assert.Equal(expect, payload); - } + // Assert + response.EnsureSuccessStatusCode(); - [Theory] - [InlineData("1", true, "\"Id\":1,\"Name\":\"OData Category\"}")] - [InlineData("11", true, "\"Id\":11,\"Name\":\"OData Category\"}")] - [InlineData("42", true, "\"Id\":42,\"Name\":\"OData Category\"}")] - [InlineData("other", false, "{\"id\":11,\"name\":\"Non-OData Category\"}")] - public async Task RequestContainsValidValues_CanRouteToCorrectEndpoint_UsingDifferentOrder(string key, bool isODataRouting, string expect) + string payload = await response.Content.ReadAsStringAsync(); + if (isODataRouting) { - // Arrange - HttpClient client = CreateClient(); + expect = $"{{\"@odata.context\":\"http://localhost/odata/$metadata#Products/$entity\",{expect}"; + } - // Act - var response = await client.GetAsync($"/odata/categories/{key}"); + Assert.Equal(expect, payload); + } - // Assert - response.EnsureSuccessStatusCode(); + [Theory] + [InlineData("1", true, "\"Id\":1,\"Name\":\"OData Category\"}")] + [InlineData("11", true, "\"Id\":11,\"Name\":\"OData Category\"}")] + [InlineData("42", true, "\"Id\":42,\"Name\":\"OData Category\"}")] + [InlineData("other", false, "{\"id\":11,\"name\":\"Non-OData Category\"}")] + public async Task RequestContainsValidValues_CanRouteToCorrectEndpoint_UsingDifferentOrder(string key, bool isODataRouting, string expect) + { + // Arrange + HttpClient client = CreateClient(); - string payload = await response.Content.ReadAsStringAsync(); - if (isODataRouting) - { - expect = $"{{\"@odata.context\":\"http://localhost/odata/$metadata#Categories/$entity\",{expect}"; - } + // Act + var response = await client.GetAsync($"/odata/categories/{key}"); - Assert.Equal(expect, payload); - } + // Assert + response.EnsureSuccessStatusCode(); - [Theory] - [InlineData("odata/products/\"1\"")] - [InlineData("odata/products/\"other\"")] - [InlineData("odata/products/other1")] - [InlineData("odata/products/a1other")] - [InlineData("odata/categories/\"1\"")] - [InlineData("odata/categories/\"other\"")] - [InlineData("odata/categories/other1")] - [InlineData("odata/categories/a1other")] - public async Task RequestContainsInValidValues_CanRouteToCorrectEndpoint(string request) + string payload = await response.Content.ReadAsStringAsync(); + if (isODataRouting) { - // Arrange - HttpClient client = CreateClient(); - - // Act - var response = await client.GetAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + expect = $"{{\"@odata.context\":\"http://localhost/odata/$metadata#Categories/$entity\",{expect}"; } + + Assert.Equal(expect, payload); } - public class ProductsController : ODataController + [Theory] + [InlineData("odata/products/\"1\"")] + [InlineData("odata/products/\"other\"")] + [InlineData("odata/products/other1")] + [InlineData("odata/products/a1other")] + [InlineData("odata/categories/\"1\"")] + [InlineData("odata/categories/\"other\"")] + [InlineData("odata/categories/other1")] + [InlineData("odata/categories/a1other")] + public async Task RequestContainsInValidValues_CanRouteToCorrectEndpoint(string request) { - // This method will have the following routing: - // ~/odata/products({key}) - // ~/odata/products/{key} - public IActionResult Get(int key) - { - return Ok(new Product { Id = key, Name = "OData Product" }); - } + // Arrange + HttpClient client = CreateClient(); - // Be NOTED: "GetOther" is added after Get() by test, don't change the order - [HttpGet("/odata/products/other")] - public IActionResult GetOther() - { - return Ok(new Product { Id = 11, Name = "Non-OData Product" }); - } + // Act + var response = await client.GetAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } +} - public class CategoriesController : ODataController +public class ProductsController : ODataController +{ + // This method will have the following routing: + // ~/odata/products({key}) + // ~/odata/products/{key} + public IActionResult Get(int key) { - // Be NOTED: "GetOther" is added before Get() by test, don't change the order - [HttpGet("/odata/categories/other")] - public IActionResult GetOther() - { - return Ok(new Category { Id = 11, Name = "Non-OData Category" }); - } + return Ok(new Product { Id = key, Name = "OData Product" }); + } - // This method will have the following routing: - // ~/odata/categories({key}) - // ~/odata/categories/{key} - public IActionResult Get(int key) - { - return Ok(new Category { Id = key, Name = "OData Category"}); - } + // Be NOTED: "GetOther" is added after Get() by test, don't change the order + [HttpGet("/odata/products/other")] + public IActionResult GetOther() + { + return Ok(new Product { Id = 11, Name = "Non-OData Product" }); } +} - public class Product +public class CategoriesController : ODataController +{ + // Be NOTED: "GetOther" is added before Get() by test, don't change the order + [HttpGet("/odata/categories/other")] + public IActionResult GetOther() { - public int Id { get; set; } - public string Name { get; set; } + return Ok(new Category { Id = 11, Name = "Non-OData Category" }); } - public class Category + // This method will have the following routing: + // ~/odata/categories({key}) + // ~/odata/categories/{key} + public IActionResult Get(int key) { - public int Id { get; set; } - public string Name { get; set; } + return Ok(new Category { Id = key, Name = "OData Category"}); } } + +public class Product +{ + public int Id { get; set; } + public string Name { get; set; } +} + +public class Category +{ + public int Id { get; set; } + public string Name { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingControllers.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingControllers.cs index f3c8e1582..1b9f970c7 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingControllers.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingControllers.cs @@ -12,309 +12,308 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging +namespace Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging; + +public class ServerSidePagingCustomersController : ODataController { - public class ServerSidePagingCustomersController : ODataController + private readonly IList _serverSidePagingCustomers; + + public ServerSidePagingCustomersController() { - private readonly IList _serverSidePagingCustomers; + _serverSidePagingCustomers = Enumerable.Range(1, 7) + .Select(i => new ServerSidePagingCustomer + { + Id = i, + Name = "Customer Name " + i + }).ToList(); - public ServerSidePagingCustomersController() + for (int i = 0; i < _serverSidePagingCustomers.Count; i++) { - _serverSidePagingCustomers = Enumerable.Range(1, 7) - .Select(i => new ServerSidePagingCustomer + // Customer 1 => 6 Orders, Customer 2 => 5 Orders, Customer 3 => 4 Orders, ... + // NextPageLink will be expected on the Customers collection as well as + // the Orders child collection on Customer 1 + _serverSidePagingCustomers[i].ServerSidePagingOrders = Enumerable.Range(1, 6 - i) + .Select(j => new ServerSidePagingOrder { - Id = i, - Name = "Customer Name " + i + Id = j, + Amount = (i + j) * 10, + ServerSidePagingCustomer = _serverSidePagingCustomers[i] }).ToList(); - - for (int i = 0; i < _serverSidePagingCustomers.Count; i++) - { - // Customer 1 => 6 Orders, Customer 2 => 5 Orders, Customer 3 => 4 Orders, ... - // NextPageLink will be expected on the Customers collection as well as - // the Orders child collection on Customer 1 - _serverSidePagingCustomers[i].ServerSidePagingOrders = Enumerable.Range(1, 6 - i) - .Select(j => new ServerSidePagingOrder - { - Id = j, - Amount = (i + j) * 10, - ServerSidePagingCustomer = _serverSidePagingCustomers[i] - }).ToList(); - } - } - - [EnableQuery(PageSize = 5)] - public IActionResult Get() - { - return Ok(_serverSidePagingCustomers); } } - public class ServerSidePagingEmployeesController : ODataController + [EnableQuery(PageSize = 5)] + public IActionResult Get() { - private static List employees = new List( - Enumerable.Range(1, 13).Select(idx => new ServerSidePagingEmployee - { - Id = idx, - HireDate = TimeZoneInfo.ConvertTimeFromUtc(new DateTime(2022, 11, 07).AddMonths(idx), TimeZoneInfo.Local) - })); + return Ok(_serverSidePagingCustomers); + } +} - [HttpGet] - [EnableQuery(PageSize = 3)] - public IActionResult GetEmployeesHiredInPeriod([FromRoute] DateTime fromDate, [FromRoute] DateTime toDate) +public class ServerSidePagingEmployeesController : ODataController +{ + private static List employees = new List( + Enumerable.Range(1, 13).Select(idx => new ServerSidePagingEmployee { - var hiredInPeriod = employees.Where(d => d.HireDate >= fromDate && d.HireDate <= toDate); + Id = idx, + HireDate = TimeZoneInfo.ConvertTimeFromUtc(new DateTime(2022, 11, 07).AddMonths(idx), TimeZoneInfo.Local) + })); - return Ok(hiredInPeriod); - } + [HttpGet] + [EnableQuery(PageSize = 3)] + public IActionResult GetEmployeesHiredInPeriod([FromRoute] DateTime fromDate, [FromRoute] DateTime toDate) + { + var hiredInPeriod = employees.Where(d => d.HireDate >= fromDate && d.HireDate <= toDate); + + return Ok(hiredInPeriod); } +} - public class SkipTokenPagingS1CustomersController : ODataController +public class SkipTokenPagingS1CustomersController : ODataController +{ + private static readonly List customers = new List { - private static readonly List customers = new List - { - new SkipTokenPagingCustomer { Id = 1, CreditLimit = null }, - new SkipTokenPagingCustomer { Id = 2, CreditLimit = 2 }, - new SkipTokenPagingCustomer { Id = 3, CreditLimit = null }, - new SkipTokenPagingCustomer { Id = 4, CreditLimit = 30 }, - new SkipTokenPagingCustomer { Id = 5, CreditLimit = null }, - new SkipTokenPagingCustomer { Id = 6, CreditLimit = 35 }, - new SkipTokenPagingCustomer { Id = 7, CreditLimit = 5 }, - new SkipTokenPagingCustomer { Id = 8, CreditLimit = 50 }, - new SkipTokenPagingCustomer { Id = 9, CreditLimit = 25 }, - }; - - [EnableQuery(PageSize = 2)] - public ActionResult> Get() - { - return customers; - } + new SkipTokenPagingCustomer { Id = 1, CreditLimit = null }, + new SkipTokenPagingCustomer { Id = 2, CreditLimit = 2 }, + new SkipTokenPagingCustomer { Id = 3, CreditLimit = null }, + new SkipTokenPagingCustomer { Id = 4, CreditLimit = 30 }, + new SkipTokenPagingCustomer { Id = 5, CreditLimit = null }, + new SkipTokenPagingCustomer { Id = 6, CreditLimit = 35 }, + new SkipTokenPagingCustomer { Id = 7, CreditLimit = 5 }, + new SkipTokenPagingCustomer { Id = 8, CreditLimit = 50 }, + new SkipTokenPagingCustomer { Id = 9, CreditLimit = 25 }, + }; + + [EnableQuery(PageSize = 2)] + public ActionResult> Get() + { + return customers; } +} - public class SkipTokenPagingS2CustomersController : ODataController +public class SkipTokenPagingS2CustomersController : ODataController +{ + private readonly List customers = new List { - private readonly List customers = new List - { - new SkipTokenPagingCustomer { Id = 1, Grade = "A", CreditLimit = null }, - new SkipTokenPagingCustomer { Id = 2, Grade = "B", CreditLimit = null }, - new SkipTokenPagingCustomer { Id = 3, Grade = "A", CreditLimit = 10 }, - new SkipTokenPagingCustomer { Id = 4, Grade = "C", CreditLimit = null }, - new SkipTokenPagingCustomer { Id = 5, Grade = "A", CreditLimit = 30 }, - new SkipTokenPagingCustomer { Id = 6, Grade = "C", CreditLimit = null }, - new SkipTokenPagingCustomer { Id = 7, Grade = "B", CreditLimit = 5 }, - new SkipTokenPagingCustomer { Id = 8, Grade = "C", CreditLimit = 25 }, - new SkipTokenPagingCustomer { Id = 9, Grade = "B", CreditLimit = 50 }, - new SkipTokenPagingCustomer { Id = 10, Grade = "D", CreditLimit = 50 }, - new SkipTokenPagingCustomer { Id = 11, Grade = "F", CreditLimit = 35 }, - new SkipTokenPagingCustomer { Id = 12, Grade = "F", CreditLimit = 30 }, - new SkipTokenPagingCustomer { Id = 13, Grade = "F", CreditLimit = 55 } - }; - - [EnableQuery(PageSize = 4)] - public ActionResult> Get() - { - return customers; - } + new SkipTokenPagingCustomer { Id = 1, Grade = "A", CreditLimit = null }, + new SkipTokenPagingCustomer { Id = 2, Grade = "B", CreditLimit = null }, + new SkipTokenPagingCustomer { Id = 3, Grade = "A", CreditLimit = 10 }, + new SkipTokenPagingCustomer { Id = 4, Grade = "C", CreditLimit = null }, + new SkipTokenPagingCustomer { Id = 5, Grade = "A", CreditLimit = 30 }, + new SkipTokenPagingCustomer { Id = 6, Grade = "C", CreditLimit = null }, + new SkipTokenPagingCustomer { Id = 7, Grade = "B", CreditLimit = 5 }, + new SkipTokenPagingCustomer { Id = 8, Grade = "C", CreditLimit = 25 }, + new SkipTokenPagingCustomer { Id = 9, Grade = "B", CreditLimit = 50 }, + new SkipTokenPagingCustomer { Id = 10, Grade = "D", CreditLimit = 50 }, + new SkipTokenPagingCustomer { Id = 11, Grade = "F", CreditLimit = 35 }, + new SkipTokenPagingCustomer { Id = 12, Grade = "F", CreditLimit = 30 }, + new SkipTokenPagingCustomer { Id = 13, Grade = "F", CreditLimit = 55 } + }; + + [EnableQuery(PageSize = 4)] + public ActionResult> Get() + { + return customers; } +} - public class SkipTokenPagingS3CustomersController : ODataController +public class SkipTokenPagingS3CustomersController : ODataController +{ + private static readonly List customers = new List { - private static readonly List customers = new List - { - new SkipTokenPagingCustomer { Id = 1, CustomerSince = null }, - new SkipTokenPagingCustomer { Id = 2, CustomerSince = new DateTime(2023, 1, 2) }, - new SkipTokenPagingCustomer { Id = 3, CustomerSince = null }, - new SkipTokenPagingCustomer { Id = 4, CustomerSince = new DateTime(2023, 1, 30) }, - new SkipTokenPagingCustomer { Id = 5, CustomerSince = null }, - new SkipTokenPagingCustomer { Id = 6, CustomerSince = new DateTime(2023, 2, 4) }, - new SkipTokenPagingCustomer { Id = 7, CustomerSince = new DateTime(2023, 1, 5) }, - new SkipTokenPagingCustomer { Id = 8, CustomerSince = new DateTime(2023, 2, 19) }, - new SkipTokenPagingCustomer { Id = 9, CustomerSince = new DateTime(2023, 1, 25) }, - }; - - [EnableQuery(PageSize = 2)] - public ActionResult> Get() - { - return customers; - } + new SkipTokenPagingCustomer { Id = 1, CustomerSince = null }, + new SkipTokenPagingCustomer { Id = 2, CustomerSince = new DateTime(2023, 1, 2) }, + new SkipTokenPagingCustomer { Id = 3, CustomerSince = null }, + new SkipTokenPagingCustomer { Id = 4, CustomerSince = new DateTime(2023, 1, 30) }, + new SkipTokenPagingCustomer { Id = 5, CustomerSince = null }, + new SkipTokenPagingCustomer { Id = 6, CustomerSince = new DateTime(2023, 2, 4) }, + new SkipTokenPagingCustomer { Id = 7, CustomerSince = new DateTime(2023, 1, 5) }, + new SkipTokenPagingCustomer { Id = 8, CustomerSince = new DateTime(2023, 2, 19) }, + new SkipTokenPagingCustomer { Id = 9, CustomerSince = new DateTime(2023, 1, 25) }, + }; + + [EnableQuery(PageSize = 2)] + public ActionResult> Get() + { + return customers; } +} - public class SkipTokenPagingEdgeCase1CustomersController : ODataController +public class SkipTokenPagingEdgeCase1CustomersController : ODataController +{ + private static readonly List customers = new List { - private static readonly List customers = new List - { - new SkipTokenPagingEdgeCase1Customer { Id = 2, CreditLimit = 2 }, - new SkipTokenPagingEdgeCase1Customer { Id = 4, CreditLimit = 30 }, - new SkipTokenPagingEdgeCase1Customer { Id = 6, CreditLimit = 35 }, - new SkipTokenPagingEdgeCase1Customer { Id = 7, CreditLimit = 5 }, - new SkipTokenPagingEdgeCase1Customer { Id = 9, CreditLimit = 25 }, - }; - - [EnableQuery(PageSize = 2)] - public ActionResult> Get() - { - return customers; - } + new SkipTokenPagingEdgeCase1Customer { Id = 2, CreditLimit = 2 }, + new SkipTokenPagingEdgeCase1Customer { Id = 4, CreditLimit = 30 }, + new SkipTokenPagingEdgeCase1Customer { Id = 6, CreditLimit = 35 }, + new SkipTokenPagingEdgeCase1Customer { Id = 7, CreditLimit = 5 }, + new SkipTokenPagingEdgeCase1Customer { Id = 9, CreditLimit = 25 }, + }; + + [EnableQuery(PageSize = 2)] + public ActionResult> Get() + { + return customers; } +} - public class ContainmentPagingCustomersController : ODataController +public class ContainmentPagingCustomersController : ODataController +{ + [EnableQuery(PageSize = 2)] + public ActionResult Get() { - [EnableQuery(PageSize = 2)] - public ActionResult Get() - { - return Ok(ContainmentPagingDataSource.Customers); - } - - [EnableQuery(PageSize = 2)] - public ActionResult GetOrders(int key) - { - var customer = ContainmentPagingDataSource.Customers.SingleOrDefault(d => d.Id == key); - - if (customer == null) - { - return BadRequest(); - } - - return Ok(customer.Orders); - } + return Ok(ContainmentPagingDataSource.Customers); } - public class ContainmentPagingCompanyController : ODataController + [EnableQuery(PageSize = 2)] + public ActionResult GetOrders(int key) { - private static readonly ContainmentPagingCustomer company = new ContainmentPagingCustomer - { - Id = 1, - Orders = ContainmentPagingDataSource.Orders.Take(ContainmentPagingDataSource.TargetSize).ToList() - }; + var customer = ContainmentPagingDataSource.Customers.SingleOrDefault(d => d.Id == key); - [EnableQuery(PageSize = 2)] - public ActionResult Get() + if (customer == null) { - return Ok(company); + return BadRequest(); } - [EnableQuery(PageSize = 2)] - public ActionResult GetOrders() - { - return Ok(company.Orders); - } + return Ok(customer.Orders); } +} - public class NoContainmentPagingCustomersController : ODataController +public class ContainmentPagingCompanyController : ODataController +{ + private static readonly ContainmentPagingCustomer company = new ContainmentPagingCustomer { - [EnableQuery(PageSize = 2)] - public ActionResult Get() - { - return Ok(NoContainmentPagingDataSource.Customers); - } + Id = 1, + Orders = ContainmentPagingDataSource.Orders.Take(ContainmentPagingDataSource.TargetSize).ToList() + }; - [EnableQuery(PageSize = 2)] - public ActionResult GetOrders(int key) - { - var customer = NoContainmentPagingDataSource.Customers.SingleOrDefault(d => d.Id == key); + [EnableQuery(PageSize = 2)] + public ActionResult Get() + { + return Ok(company); + } - if (customer == null) - { - return BadRequest(); - } + [EnableQuery(PageSize = 2)] + public ActionResult GetOrders() + { + return Ok(company.Orders); + } +} - return Ok(customer.Orders); - } +public class NoContainmentPagingCustomersController : ODataController +{ + [EnableQuery(PageSize = 2)] + public ActionResult Get() + { + return Ok(NoContainmentPagingDataSource.Customers); } - public class ContainmentPagingMenusController : ODataController + [EnableQuery(PageSize = 2)] + public ActionResult GetOrders(int key) { - [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] - public ActionResult Get() - { - return Ok(ContainmentPagingDataSource.Menus); - } + var customer = NoContainmentPagingDataSource.Customers.SingleOrDefault(d => d.Id == key); - [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] - public ActionResult GetFromContainmentPagingExtendedMenu() + if (customer == null) { - return Ok(ContainmentPagingDataSource.Menus.OfType()); + return BadRequest(); } - [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] - public ActionResult GetTabsFromContainmentPagingExtendedMenu(int key) - { - var menu = ContainmentPagingDataSource.Menus.OfType().SingleOrDefault(d => d.Id == key); - - if (menu == null) - { - return BadRequest(); - } + return Ok(customer.Orders); + } +} - return Ok(menu.Tabs); - } +public class ContainmentPagingMenusController : ODataController +{ + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + public ActionResult Get() + { + return Ok(ContainmentPagingDataSource.Menus); + } - [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] - public ActionResult GetPanelsFromContainmentPagingExtendedMenu(int key) - { - var menu = ContainmentPagingDataSource.Menus.OfType().SingleOrDefault(d => d.Id == key); + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + public ActionResult GetFromContainmentPagingExtendedMenu() + { + return Ok(ContainmentPagingDataSource.Menus.OfType()); + } - if (menu == null) - { - return BadRequest(); - } + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + public ActionResult GetTabsFromContainmentPagingExtendedMenu(int key) + { + var menu = ContainmentPagingDataSource.Menus.OfType().SingleOrDefault(d => d.Id == key); - return Ok(menu.Panels); + if (menu == null) + { + return BadRequest(); } + + return Ok(menu.Tabs); } - public class ContainmentPagingRibbonController : ODataController + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + public ActionResult GetPanelsFromContainmentPagingExtendedMenu(int key) { - private static readonly ContainmentPagingMenu ribbon = new ContainmentPagingExtendedMenu - { - Id = 1, - Tabs = ContainmentPagingDataSource.Tabs.Take(ContainmentPagingDataSource.TargetSize).ToList() - }; + var menu = ContainmentPagingDataSource.Menus.OfType().SingleOrDefault(d => d.Id == key); - [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] - public ActionResult Get() + if (menu == null) { - return Ok(ribbon); + return BadRequest(); } - [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] - public ActionResult GetFromContainmentPagingExtendedMenu() - { - return Ok(ribbon as ContainmentPagingExtendedMenu); - } + return Ok(menu.Panels); + } +} - [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] - [HttpGet("ContainmentPagingRibbon/Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging.ContainmentPagingExtendedMenu/Tabs")] - public ActionResult GetTabsFromContainmentPagingExtendedMenu() - { - return Ok((ribbon as ContainmentPagingExtendedMenu).Tabs); - } +public class ContainmentPagingRibbonController : ODataController +{ + private static readonly ContainmentPagingMenu ribbon = new ContainmentPagingExtendedMenu + { + Id = 1, + Tabs = ContainmentPagingDataSource.Tabs.Take(ContainmentPagingDataSource.TargetSize).ToList() + }; + + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + public ActionResult Get() + { + return Ok(ribbon); + } + + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + public ActionResult GetFromContainmentPagingExtendedMenu() + { + return Ok(ribbon as ContainmentPagingExtendedMenu); } - public class CollectionPagingCustomersController : ODataController + [EnableQuery(PageSize = 2, MaxExpansionDepth = 4)] + [HttpGet("ContainmentPagingRibbon/Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging.ContainmentPagingExtendedMenu/Tabs")] + public ActionResult GetTabsFromContainmentPagingExtendedMenu() { - private const int TargetSize = 3; - private static readonly List customers = new List( - Enumerable.Range(1, TargetSize).Select(idx => new CollectionPagingCustomer + return Ok((ribbon as ContainmentPagingExtendedMenu).Tabs); + } +} + +public class CollectionPagingCustomersController : ODataController +{ + private const int TargetSize = 3; + private static readonly List customers = new List( + Enumerable.Range(1, TargetSize).Select(idx => new CollectionPagingCustomer + { + Id = idx, + Tags = new List { "Tier 1", "Gen-Z", "HNW" }, + Categories = new List { - Id = idx, - Tags = new List { "Tier 1", "Gen-Z", "HNW" }, - Categories = new List + CollectionPagingCategory.Retailer, + CollectionPagingCategory.Wholesaler, + CollectionPagingCategory.Distributor + }, + Locations = new List( + Enumerable.Range(1, TargetSize).Select(dx => new CollectionPagingLocation { - CollectionPagingCategory.Retailer, - CollectionPagingCategory.Wholesaler, - CollectionPagingCategory.Distributor - }, - Locations = new List( - Enumerable.Range(1, TargetSize).Select(dx => new CollectionPagingLocation - { - Street = $"Street {idx}{dx}" - })) - })); - - [EnableQuery(PageSize = 2)] - public ActionResult Get() - { - return Ok(customers); - } + Street = $"Street {idx}{dx}" + })) + })); + + [EnableQuery(PageSize = 2)] + public ActionResult Get() + { + return Ok(customers); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingDataModel.cs index e09cdd6b2..2b06d42d5 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingDataModel.cs @@ -10,148 +10,147 @@ using System.ComponentModel.DataAnnotations; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging -{ - public class ServerSidePagingCustomer - { - [Key] - public int Id { get; set; } - public string Name { get; set; } - public IList ServerSidePagingOrders { get; set; } - } - - public class ServerSidePagingOrder - { - [Key] - public int Id { get; set; } - public decimal Amount { get; set; } - public ServerSidePagingCustomer ServerSidePagingCustomer { get; set; } - } - - public class ServerSidePagingEmployee - { - public int Id { get; set; } - public DateTime HireDate { get; set; } - } - - public class SkipTokenPagingCustomer - { - public int Id { get; set; } - public string Grade { get; set; } - public decimal? CreditLimit { get; set; } - public DateTime? CustomerSince { get; set; } - } - - public class SkipTokenPagingEdgeCase1Customer - { - public int Id { get; set; } - public decimal? CreditLimit { get; set; } - } - - public class ContainmentPagingCustomer - { - public int Id { get; set; } - [Contained] - public List Orders { get; set; } - } - - public class ContainedPagingOrder - { - public int Id { get; set; } - [Contained] - public List Items { get; set; } - } - - public class ContainedPagingOrderItem - { - public int Id { get; set; } - } - - public class NoContainmentPagingCustomer - { - public int Id { get; set; } - public List Orders { get; set; } - } - - public class NoContainmentPagingOrder - { - public int Id { get; set; } - public List Items { get; set; } - } - - public class NoContainmentPagingOrderItem - { - public int Id { get; set; } - } - - public class ContainmentPagingMenu - { - public int Id { get; set; } - } - - public class ContainmentPagingExtendedMenu : ContainmentPagingMenu - { - [Contained] - public List Tabs { get; set; } - // Non-contained - public List Panels { get; set; } - } - - public class ContainedPagingTab - { - public int Id { get; set; } - } - - public class ContainedPagingExtendedTab : ContainedPagingTab - { - [Contained] - public List Items { get; set; } - } - - public class ContainedPagingItem - { - public int Id { get; set; } - } - - public class ContainedPagingExtendedItem : ContainedPagingItem - { - [Contained] - public List Notes { get; set; } - } - - public class ContainedPagingNote - { - public int Id { get; set; } - } - - public class ContainmentPagingPanel - { - public int Id { get; set; } - } - - public class ContainmentPagingExtendedPanel : ContainmentPagingPanel - { - [Contained] - public List Items { get; set; } - } - - public enum CollectionPagingCategory - { - Retailer, - Wholesaler, - Distributor - } - - public class CollectionPagingLocation - { - public string Street { get; set; } - } - - public class CollectionPagingCustomer - { - public int Id { get; set; } - public List Tags { get; set; } - public List Categories { get; set; } - public List Locations { get; set; } - } +namespace Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging; + +public class ServerSidePagingCustomer +{ + [Key] + public int Id { get; set; } + public string Name { get; set; } + public IList ServerSidePagingOrders { get; set; } +} + +public class ServerSidePagingOrder +{ + [Key] + public int Id { get; set; } + public decimal Amount { get; set; } + public ServerSidePagingCustomer ServerSidePagingCustomer { get; set; } +} + +public class ServerSidePagingEmployee +{ + public int Id { get; set; } + public DateTime HireDate { get; set; } +} + +public class SkipTokenPagingCustomer +{ + public int Id { get; set; } + public string Grade { get; set; } + public decimal? CreditLimit { get; set; } + public DateTime? CustomerSince { get; set; } +} + +public class SkipTokenPagingEdgeCase1Customer +{ + public int Id { get; set; } + public decimal? CreditLimit { get; set; } +} + +public class ContainmentPagingCustomer +{ + public int Id { get; set; } + [Contained] + public List Orders { get; set; } +} + +public class ContainedPagingOrder +{ + public int Id { get; set; } + [Contained] + public List Items { get; set; } +} + +public class ContainedPagingOrderItem +{ + public int Id { get; set; } +} + +public class NoContainmentPagingCustomer +{ + public int Id { get; set; } + public List Orders { get; set; } +} + +public class NoContainmentPagingOrder +{ + public int Id { get; set; } + public List Items { get; set; } +} + +public class NoContainmentPagingOrderItem +{ + public int Id { get; set; } +} + +public class ContainmentPagingMenu +{ + public int Id { get; set; } +} + +public class ContainmentPagingExtendedMenu : ContainmentPagingMenu +{ + [Contained] + public List Tabs { get; set; } + // Non-contained + public List Panels { get; set; } +} + +public class ContainedPagingTab +{ + public int Id { get; set; } +} + +public class ContainedPagingExtendedTab : ContainedPagingTab +{ + [Contained] + public List Items { get; set; } +} + +public class ContainedPagingItem +{ + public int Id { get; set; } +} + +public class ContainedPagingExtendedItem : ContainedPagingItem +{ + [Contained] + public List Notes { get; set; } +} + +public class ContainedPagingNote +{ + public int Id { get; set; } +} + +public class ContainmentPagingPanel +{ + public int Id { get; set; } +} + +public class ContainmentPagingExtendedPanel : ContainmentPagingPanel +{ + [Contained] + public List Items { get; set; } +} + +public enum CollectionPagingCategory +{ + Retailer, + Wholesaler, + Distributor +} + +public class CollectionPagingLocation +{ + public string Street { get; set; } +} + +public class CollectionPagingCustomer +{ + public int Id { get; set; } + public List Tags { get; set; } + public List Categories { get; set; } + public List Locations { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingDataSource.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingDataSource.cs index 43d76d6cb..fdbd86819 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingDataSource.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingDataSource.cs @@ -8,102 +8,101 @@ using System.Collections.Generic; using System.Linq; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging +namespace Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging; + +public static class ContainmentPagingDataSource +{ + internal const int TargetSize = 3; + + private static readonly List orderItems = new List( + Enumerable.Range(1, TargetSize * TargetSize * TargetSize).Select(idx => new ContainedPagingOrderItem + { + Id = idx + })); + + private static readonly List orders = new List( + Enumerable.Range(1, TargetSize * TargetSize).Select(idx => new ContainedPagingOrder + { + Id = idx, + Items = orderItems.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + private static readonly List customers = new List( + Enumerable.Range(1, TargetSize).Select(idx => new ContainmentPagingCustomer + { + Id = idx, + Orders = orders.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + private static readonly List notes = new List( + Enumerable.Range(1, TargetSize * TargetSize * TargetSize * TargetSize).Select(idx => new ContainedPagingNote + { + Id = idx + })); + + private static readonly List items = new List( + Enumerable.Range(1, TargetSize * TargetSize * TargetSize).Select(idx => new ContainedPagingExtendedItem + { + Id = idx, + Notes = notes.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + private static readonly List tabs = new List( + Enumerable.Range(1, TargetSize * TargetSize).Select(idx => new ContainedPagingExtendedTab + { + Id = idx, + Items = items.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + private static readonly List panels = new List( + Enumerable.Range(1, TargetSize * TargetSize).Select(idx => new ContainmentPagingExtendedPanel + { + Id = idx, + Items = items.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + private static readonly List menus = new List( + Enumerable.Range(1, TargetSize).Select(idx => new ContainmentPagingExtendedMenu + { + Id = idx, + Tabs = tabs.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList(), + Panels = panels.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + public static List Customers => customers; + + public static List Orders => orders; + + public static List Menus => menus; + + public static List Tabs => tabs; +} + +public static class NoContainmentPagingDataSource { - public static class ContainmentPagingDataSource - { - internal const int TargetSize = 3; - - private static readonly List orderItems = new List( - Enumerable.Range(1, TargetSize * TargetSize * TargetSize).Select(idx => new ContainedPagingOrderItem - { - Id = idx - })); - - private static readonly List orders = new List( - Enumerable.Range(1, TargetSize * TargetSize).Select(idx => new ContainedPagingOrder - { - Id = idx, - Items = orderItems.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() - })); - - private static readonly List customers = new List( - Enumerable.Range(1, TargetSize).Select(idx => new ContainmentPagingCustomer - { - Id = idx, - Orders = orders.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() - })); - - private static readonly List notes = new List( - Enumerable.Range(1, TargetSize * TargetSize * TargetSize * TargetSize).Select(idx => new ContainedPagingNote - { - Id = idx - })); - - private static readonly List items = new List( - Enumerable.Range(1, TargetSize * TargetSize * TargetSize).Select(idx => new ContainedPagingExtendedItem - { - Id = idx, - Notes = notes.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() - })); - - private static readonly List tabs = new List( - Enumerable.Range(1, TargetSize * TargetSize).Select(idx => new ContainedPagingExtendedTab - { - Id = idx, - Items = items.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() - })); - - private static readonly List panels = new List( - Enumerable.Range(1, TargetSize * TargetSize).Select(idx => new ContainmentPagingExtendedPanel - { - Id = idx, - Items = items.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() - })); - - private static readonly List menus = new List( - Enumerable.Range(1, TargetSize).Select(idx => new ContainmentPagingExtendedMenu - { - Id = idx, - Tabs = tabs.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList(), - Panels = panels.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() - })); - - public static List Customers => customers; - - public static List Orders => orders; - - public static List Menus => menus; - - public static List Tabs => tabs; - } - - public static class NoContainmentPagingDataSource - { - private const int TargetSize = 3; - - private static readonly List orderItems = new List( - Enumerable.Range(1, TargetSize * TargetSize * TargetSize).Select(idx => new NoContainmentPagingOrderItem - { - Id = idx - })); - - private static readonly List orders = new List( - Enumerable.Range(1, TargetSize * TargetSize).Select(idx => new NoContainmentPagingOrder - { - Id = idx, - Items = orderItems.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() - })); - - private static readonly List customers = new List( - Enumerable.Range(1, TargetSize).Select(idx => new NoContainmentPagingCustomer - { - Id = idx, - Orders = orders.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() - })); - - public static List Customers => customers; - - public static List Orders => orders; - } + private const int TargetSize = 3; + + private static readonly List orderItems = new List( + Enumerable.Range(1, TargetSize * TargetSize * TargetSize).Select(idx => new NoContainmentPagingOrderItem + { + Id = idx + })); + + private static readonly List orders = new List( + Enumerable.Range(1, TargetSize * TargetSize).Select(idx => new NoContainmentPagingOrder + { + Id = idx, + Items = orderItems.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + private static readonly List customers = new List( + Enumerable.Range(1, TargetSize).Select(idx => new NoContainmentPagingCustomer + { + Id = idx, + Orders = orders.Skip((idx - 1) * TargetSize).Take(TargetSize).ToList() + })); + + public static List Customers => customers; + + public static List Orders => orders; } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingTests.cs index 323f13267..91fe3b22a 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/ServerSidePaging/ServerSidePagingTests.cs @@ -23,590 +23,636 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging +namespace Microsoft.AspNetCore.OData.E2E.Tests.ServerSidePaging; + +public class ServerSidePagingTests : WebApiTestBase { - public class ServerSidePagingTests : WebApiTestBase + public ServerSidePagingTests(WebApiTestFixture fixture) + : base(fixture) { - public ServerSidePagingTests(WebApiTestFixture fixture) - : base(fixture) - { - } + } - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = GetEdmModel(); - services.ConfigureControllers( - typeof(ServerSidePagingCustomersController), - typeof(ServerSidePagingEmployeesController), - typeof(ContainmentPagingCustomersController), - typeof(ContainmentPagingCompanyController), - typeof(NoContainmentPagingCustomersController), - typeof(ContainmentPagingMenusController), - typeof(ContainmentPagingRibbonController), - typeof(CollectionPagingCustomersController)); - services.AddControllers().AddOData(opt => opt.Expand().OrderBy().Select().SetMaxTop(null).AddRouteComponents("{a}", edmModel)); - } + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = GetEdmModel(); + services.ConfigureControllers( + typeof(ServerSidePagingCustomersController), + typeof(ServerSidePagingEmployeesController), + typeof(ContainmentPagingCustomersController), + typeof(ContainmentPagingCompanyController), + typeof(NoContainmentPagingCustomersController), + typeof(ContainmentPagingMenusController), + typeof(ContainmentPagingRibbonController), + typeof(CollectionPagingCustomersController)); + services.AddControllers().AddOData(opt => opt.Expand().OrderBy().Select().SetMaxTop(null).AddRouteComponents("{a}", edmModel)); + } - protected static IEdmModel GetEdmModel() - { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("ServerSidePagingOrders").EntityType.HasRequired(d => d.ServerSidePagingCustomer); - builder.EntitySet("ServerSidePagingCustomers").EntityType.HasMany(d => d.ServerSidePagingOrders); - builder.EntitySet("ContainmentPagingCustomers"); - builder.Singleton("ContainmentPagingCompany"); - builder.EntitySet("NoContainmentPagingCustomers"); - builder.EntitySet("NoContainmentPagingOrders"); - builder.EntitySet("NoContainmentPagingOrderItems"); - builder.EntitySet("ContainmentPagingMenus"); - builder.EntitySet("ContainmentPagingPanels"); - builder.Singleton("ContainmentPagingRibbon"); - builder.EntitySet("CollectionPagingCustomers"); - - var getEmployeesHiredInPeriodFunction = builder.EntitySet( - "ServerSidePagingEmployees").EntityType.Collection.Function("GetEmployeesHiredInPeriod"); - getEmployeesHiredInPeriodFunction.Parameter(typeof(DateTime), "fromDate"); - getEmployeesHiredInPeriodFunction.Parameter(typeof(DateTime), "toDate"); - getEmployeesHiredInPeriodFunction.ReturnsCollectionFromEntitySet("ServerSidePagingEmployees"); - - return builder.GetEdmModel(); - } + protected static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("ServerSidePagingOrders").EntityType.HasRequired(d => d.ServerSidePagingCustomer); + builder.EntitySet("ServerSidePagingCustomers").EntityType.HasMany(d => d.ServerSidePagingOrders); + builder.EntitySet("ContainmentPagingCustomers"); + builder.Singleton("ContainmentPagingCompany"); + builder.EntitySet("NoContainmentPagingCustomers"); + builder.EntitySet("NoContainmentPagingOrders"); + builder.EntitySet("NoContainmentPagingOrderItems"); + builder.EntitySet("ContainmentPagingMenus"); + builder.EntitySet("ContainmentPagingPanels"); + builder.Singleton("ContainmentPagingRibbon"); + builder.EntitySet("CollectionPagingCustomers"); + + var getEmployeesHiredInPeriodFunction = builder.EntitySet( + "ServerSidePagingEmployees").EntityType.Collection.Function("GetEmployeesHiredInPeriod"); + getEmployeesHiredInPeriodFunction.Parameter(typeof(DateTime), "fromDate"); + getEmployeesHiredInPeriodFunction.Parameter(typeof(DateTime), "toDate"); + getEmployeesHiredInPeriodFunction.ReturnsCollectionFromEntitySet("ServerSidePagingEmployees"); + + return builder.GetEdmModel(); + } - [Fact] - public async Task ValidNextLinksGenerated() + [Fact] + public async Task ValidNextLinksGenerated() + { + // Arrange + string requestUri = "/prefix/ServerSidePagingCustomers?$expand=ServerSidePagingOrders"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + string content = await response.Content.ReadAsStringAsync(); + + // Assert + // Customer 1 => 6 Orders, Customer 2 => 5 Orders, Customer 3 => 4 Orders, ... + // NextPageLink will be expected on the Customers collection as well as + // the Orders child collection on Customer 1 + using (JsonDocument document = JsonDocument.Parse(content)) { - // Arrange - string requestUri = "/prefix/ServerSidePagingCustomers?$expand=ServerSidePagingOrders"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - string content = await response.Content.ReadAsStringAsync(); + bool found = document.RootElement.TryGetProperty("value", out JsonElement value); + Assert.True(found); - // Assert - // Customer 1 => 6 Orders, Customer 2 => 5 Orders, Customer 3 => 4 Orders, ... - // NextPageLink will be expected on the Customers collection as well as - // the Orders child collection on Customer 1 - using (JsonDocument document = JsonDocument.Parse(content)) + foreach (JsonElement item in value.EnumerateArray()) { - bool found = document.RootElement.TryGetProperty("value", out JsonElement value); + found = item.TryGetProperty("Id", out JsonElement id); Assert.True(found); - foreach (JsonElement item in value.EnumerateArray()) + // only the Orders child collection on Customer 1 + bool odersNextLink = item.TryGetProperty("ServerSidePagingOrders@odata.nextLink", out JsonElement ordersNextLink); + int idValue = id.GetInt32(); + if (idValue == 1) { - found = item.TryGetProperty("Id", out JsonElement id); - Assert.True(found); - - // only the Orders child collection on Customer 1 - bool odersNextLink = item.TryGetProperty("ServerSidePagingOrders@odata.nextLink", out JsonElement ordersNextLink); - int idValue = id.GetInt32(); - if (idValue == 1) - { - Assert.True(odersNextLink); - Assert.Equal("http://localhost/prefix/ServerSidePagingCustomers/1/ServerSidePagingOrders?$skip=5", ordersNextLink.GetString()); - } - else - { - Assert.False(odersNextLink); - } + Assert.True(odersNextLink); + Assert.Equal("http://localhost/prefix/ServerSidePagingCustomers/1/ServerSidePagingOrders?$skip=5", ordersNextLink.GetString()); + } + else + { + Assert.False(odersNextLink); } - - bool nextLinkFound = document.RootElement.TryGetProperty("@odata.nextLink", out JsonElement nextLink); - Assert.True(nextLinkFound); - Assert.Equal("http://localhost/prefix/ServerSidePagingCustomers?$expand=ServerSidePagingOrders&$skip=5", nextLink.GetString()); } - } - - [Fact] - public async Task VerifyParametersInNextPageLinkInEdmFunctionResponseBodyAreInSameCaseAsInRequestUrl() - { - // Arrange - var requestUri = "/prefix/ServerSidePagingEmployees/" + - "GetEmployeesHiredInPeriod(fromDate=@fromDate,toDate=@toDate)" + - "?@fromDate=2023-01-07T00:00:00%2B00:00&@toDate=2023-05-07T00:00:00%2B00:00"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); - - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); - // Assert - Assert.Contains("\"@odata.nextLink\":", content); - Assert.Contains( - "/prefix/ServerSidePagingEmployees/GetEmployeesHiredInPeriod(fromDate=@fromDate,toDate=@toDate)" + - "?%40fromDate=2023-01-07T00%3A00%3A00%2B00%3A00&%40toDate=2023-05-07T00%3A00%3A00%2B00%3A00&$skip=3", - content); + bool nextLinkFound = document.RootElement.TryGetProperty("@odata.nextLink", out JsonElement nextLink); + Assert.True(nextLinkFound); + Assert.Equal("http://localhost/prefix/ServerSidePagingCustomers?$expand=ServerSidePagingOrders&$skip=5", nextLink.GetString()); } + } - [Fact] - public async Task VerifyExpectedNextLinksGeneratedForNestedExpandInContainmentScenario() - { - // Arrange - var requestUri = "/prefix/ContainmentPagingCustomers?$expand=Orders($expand=Items)"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); - - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Contains("/prefix/ContainmentPagingCustomers/1/Orders/1/Items?$skip=2", content); - Assert.Contains("/prefix/ContainmentPagingCustomers/1/Orders/2/Items?$skip=2", content); - Assert.Contains("/prefix/ContainmentPagingCustomers/1/Orders?$expand=Items&$skip=2", content); + [Fact] + public async Task VerifyParametersInNextPageLinkInEdmFunctionResponseBodyAreInSameCaseAsInRequestUrl() + { + // Arrange + var requestUri = "/prefix/ServerSidePagingEmployees/" + + "GetEmployeesHiredInPeriod(fromDate=@fromDate,toDate=@toDate)" + + "?@fromDate=2023-01-07T00:00:00%2B00:00&@toDate=2023-05-07T00:00:00%2B00:00"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains("\"@odata.nextLink\":", content); + Assert.Contains( + "/prefix/ServerSidePagingEmployees/GetEmployeesHiredInPeriod(fromDate=@fromDate,toDate=@toDate)" + + "?%40fromDate=2023-01-07T00%3A00%3A00%2B00%3A00&%40toDate=2023-05-07T00%3A00%3A00%2B00%3A00&$skip=3", + content); + } - Assert.Contains("/prefix/ContainmentPagingCustomers/2/Orders/4/Items?$skip=2", content); - Assert.Contains("/prefix/ContainmentPagingCustomers/2/Orders/5/Items?$skip=2", content); - Assert.Contains("/prefix/ContainmentPagingCustomers/2/Orders?$expand=Items&$skip=2", content); + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForNestedExpandInContainmentScenario() + { + // Arrange + var requestUri = "/prefix/ContainmentPagingCustomers?$expand=Orders($expand=Items)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); - Assert.Contains("/prefix/ContainmentPagingCustomers?$expand=Orders", content); - } + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); - [Fact] - public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyAsODataPathSegment() - { - // Arrange - var requestUri = "/prefix/ContainmentPagingCustomers/2/Orders?$expand=Items"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); + // Assert + Assert.Contains("/prefix/ContainmentPagingCustomers/1/Orders/1/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCustomers/1/Orders/2/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCustomers/1/Orders?$expand=Items&$skip=2", content); - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("/prefix/ContainmentPagingCustomers/2/Orders/4/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCustomers/2/Orders/5/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCustomers/2/Orders?$expand=Items&$skip=2", content); - // Assert - Assert.Contains("/prefix/ContainmentPagingCustomers/2/Orders/4/Items?$skip=2", content); - Assert.Contains("/prefix/ContainmentPagingCustomers/2/Orders/5/Items?$skip=2", content); - Assert.Contains("/prefix/ContainmentPagingCustomers/2/Orders?$expand=Items&$skip=2", content); - } + Assert.Contains("/prefix/ContainmentPagingCustomers?$expand=Orders", content); + } - [Fact] - public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyInSingletonScenario() - { - // Arrange - var requestUri = "/prefix/ContainmentPagingCompany?$expand=Orders($expand=Items)"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyAsODataPathSegment() + { + // Arrange + var requestUri = "/prefix/ContainmentPagingCustomers/2/Orders?$expand=Items"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains("/prefix/ContainmentPagingCustomers/2/Orders/4/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCustomers/2/Orders/5/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCustomers/2/Orders?$expand=Items&$skip=2", content); + } - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyInSingletonScenario() + { + // Arrange + var requestUri = "/prefix/ContainmentPagingCompany?$expand=Orders($expand=Items)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains("/prefix/ContainmentPagingCompany/Orders/1/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCompany/Orders/2/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCompany/Orders?$expand=Items&$skip=2", content); + } - // Assert - Assert.Contains("/prefix/ContainmentPagingCompany/Orders/1/Items?$skip=2", content); - Assert.Contains("/prefix/ContainmentPagingCompany/Orders/2/Items?$skip=2", content); - Assert.Contains("/prefix/ContainmentPagingCompany/Orders?$expand=Items&$skip=2", content); - } + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyAsODataPathSegmentInSingletonScenario() + { + // Arrange + var requestUri = "/prefix/ContainmentPagingCompany/Orders?$expand=Items"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains("/prefix/ContainmentPagingCompany/Orders/1/Items?$skip=2", content); + Assert.Contains("/prefix/ContainmentPagingCompany/Orders/2/Items?$skip=2", content); + } - [Fact] - public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyAsODataPathSegmentInSingletonScenario() - { - // Arrange - var requestUri = "/prefix/ContainmentPagingCompany/Orders?$expand=Items"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForNestedExpandInNoContainmentScenario() + { + // Arrange + var requestUri = "/prefix/NoContainmentPagingCustomers?$expand=Orders($expand=Items)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); - // Assert - Assert.Contains("/prefix/ContainmentPagingCompany/Orders/1/Items?$skip=2", content); - Assert.Contains("/prefix/ContainmentPagingCompany/Orders/2/Items?$skip=2", content); - } + // Assert + Assert.Contains("/prefix/NoContainmentPagingOrders/1/Items?$skip=2", content); + Assert.Contains("/prefix/NoContainmentPagingOrders/2/Items?$skip=2", content); + Assert.Contains("/prefix/NoContainmentPagingCustomers/1/Orders?$expand=Items&$skip=2", content); - [Fact] - public async Task VerifyExpectedNextLinksGeneratedForNestedExpandInNoContainmentScenario() - { - // Arrange - var requestUri = "/prefix/NoContainmentPagingCustomers?$expand=Orders($expand=Items)"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); + Assert.Contains("/prefix/NoContainmentPagingOrders/4/Items?$skip=2", content); + Assert.Contains("/prefix/NoContainmentPagingOrders/5/Items?$skip=2", content); + Assert.Contains("/prefix/NoContainmentPagingCustomers/2/Orders?$expand=Items&$skip=2", content); - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("/prefix/NoContainmentPagingCustomers?$expand=Orders", content); + } - // Assert - Assert.Contains("/prefix/NoContainmentPagingOrders/1/Items?$skip=2", content); - Assert.Contains("/prefix/NoContainmentPagingOrders/2/Items?$skip=2", content); - Assert.Contains("/prefix/NoContainmentPagingCustomers/1/Orders?$expand=Items&$skip=2", content); + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForNonContainedNavigationPropertyAsODataPathSegment() + { + // Arrange + var requestUri = "/prefix/NoContainmentPagingCustomers/2/Orders?$expand=Items"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains("/prefix/NoContainmentPagingOrders/4/Items?$skip=2", content); + Assert.Contains("/prefix/NoContainmentPagingOrders/5/Items?$skip=2", content); + Assert.Contains("/prefix/NoContainmentPagingCustomers/2/Orders?$expand=Items&$skip=2", content); + } - Assert.Contains("/prefix/NoContainmentPagingOrders/4/Items?$skip=2", content); - Assert.Contains("/prefix/NoContainmentPagingOrders/5/Items?$skip=2", content); - Assert.Contains("/prefix/NoContainmentPagingCustomers/2/Orders?$expand=Items&$skip=2", content); + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyDeclaredOnDerivedType() + { + // Arrange + var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; + var extendedTabTypeName = typeof(ContainedPagingExtendedTab).FullName; + var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; + var menusResourcePath = "/prefix/ContainmentPagingMenus"; + var menu1ResourcePath = $"/prefix/ContainmentPagingMenus/1/{extendedMenuTypeName}"; + var menu2ResourcePath = $"/prefix/ContainmentPagingMenus/2/{extendedMenuTypeName}"; + + var requestUri = $"{menusResourcePath}?$expand={extendedMenuTypeName}/Tabs($expand={extendedTabTypeName}/Items($expand={extendedItemTypeName}/Notes))"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains($"{menu1ResourcePath}/Tabs/1/{extendedTabTypeName}/Items/1/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs/1/{extendedTabTypeName}/Items/2/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs/1/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs/2/{extendedTabTypeName}/Items/4/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs/2/{extendedTabTypeName}/Items/5/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs/2/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + + Assert.Contains($"{menu2ResourcePath}/Tabs/4/{extendedTabTypeName}/Items/10/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Tabs/4/{extendedTabTypeName}/Items/11/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Tabs/4/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Tabs/5/{extendedTabTypeName}/Items/13/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Tabs/5/{extendedTabTypeName}/Items/14/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Tabs/5/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + } - Assert.Contains("/prefix/NoContainmentPagingCustomers?$expand=Orders", content); - } + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyDeclaredOnDerivedTypeAsODataPathSegment() + { + // Arrange + var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; + var extendedTabTypeName = typeof(ContainedPagingExtendedTab).FullName; + var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; + var menu1ResourcePath = $"/prefix/ContainmentPagingMenus/1/{extendedMenuTypeName}"; + + var requestUri = $"{menu1ResourcePath}/Tabs?$expand={extendedTabTypeName}/Items($expand={extendedItemTypeName}/Notes)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains($"{menu1ResourcePath}/Tabs/1/{extendedTabTypeName}/Items/1/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs/1/{extendedTabTypeName}/Items/2/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs/1/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs/2/{extendedTabTypeName}/Items/4/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs/2/{extendedTabTypeName}/Items/5/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs/2/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + } - [Fact] - public async Task VerifyExpectedNextLinksGeneratedForNonContainedNavigationPropertyAsODataPathSegment() - { - // Arrange - var requestUri = "/prefix/NoContainmentPagingCustomers/2/Orders?$expand=Items"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyDeclaredOnDerivedTypeInSingletonScenario() + { + // Arrange + var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; + var extendedTabTypeName = typeof(ContainedPagingExtendedTab).FullName; + var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; + var ribbonResourcePath = $"/prefix/ContainmentPagingRibbon"; + + var requestUri = $"{ribbonResourcePath}?$expand={extendedMenuTypeName}/Tabs($expand={extendedTabTypeName}/Items($expand={extendedItemTypeName}/Notes))"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs/1/{extendedTabTypeName}/Items/1/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs/1/{extendedTabTypeName}/Items/2/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs/1/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs/2/{extendedTabTypeName}/Items/4/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs/2/{extendedTabTypeName}/Items/5/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs/2/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + } - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyDeclaredOnDerivedTypeAsODataPathSegmentInSingletonScenario() + { + // Arrange + var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; + var extendedTabTypeName = typeof(ContainedPagingExtendedTab).FullName; + var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; + var ribbonResourcePath = $"/prefix/ContainmentPagingRibbon/{extendedMenuTypeName}"; + + var requestUri = $"{ribbonResourcePath}/Tabs?$expand={extendedTabTypeName}/Items($expand={extendedItemTypeName}/Notes)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains($"{ribbonResourcePath}/Tabs/1/{extendedTabTypeName}/Items/1/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/Tabs/1/{extendedTabTypeName}/Items/2/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/Tabs/1/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/Tabs/2/{extendedTabTypeName}/Items/4/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/Tabs/2/{extendedTabTypeName}/Items/5/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/Tabs/2/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{ribbonResourcePath}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + } - // Assert - Assert.Contains("/prefix/NoContainmentPagingOrders/4/Items?$skip=2", content); - Assert.Contains("/prefix/NoContainmentPagingOrders/5/Items?$skip=2", content); - Assert.Contains("/prefix/NoContainmentPagingCustomers/2/Orders?$expand=Items&$skip=2", content); - } + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForNonContainedNavigationPropertyDeclaredOnDerivedType() + { + // Arrange + var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; + var extendedPanelTypeName = typeof(ContainmentPagingExtendedPanel).FullName; + var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; + var menusResourcePath = $"/prefix/ContainmentPagingMenus"; + var menu1ResourcePath = $"/prefix/ContainmentPagingMenus/1/{extendedMenuTypeName}"; + var menu2ResourcePath = $"/prefix/ContainmentPagingMenus/2/{extendedMenuTypeName}"; + + var requestUri = $"{menusResourcePath}?$expand={extendedMenuTypeName}/Panels($expand={extendedPanelTypeName}/Items($expand={extendedItemTypeName}/Notes))"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains($"/prefix/ContainmentPagingPanels/1/{extendedPanelTypeName}/Items/1/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/1/{extendedPanelTypeName}/Items/2/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/1/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/2/{extendedPanelTypeName}/Items/4/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/2/{extendedPanelTypeName}/Items/5/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/2/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Panels?$expand={extendedPanelTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + + Assert.Contains($"/prefix/ContainmentPagingPanels/4/{extendedPanelTypeName}/Items/10/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/4/{extendedPanelTypeName}/Items/11/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/4/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/5/{extendedPanelTypeName}/Items/13/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/5/{extendedPanelTypeName}/Items/14/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/5/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu2ResourcePath}/Panels?$expand={extendedPanelTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + } - [Fact] - public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyDeclaredOnDerivedType() - { - // Arrange - var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; - var extendedTabTypeName = typeof(ContainedPagingExtendedTab).FullName; - var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; - var menusResourcePath = "/prefix/ContainmentPagingMenus"; - var menu1ResourcePath = $"/prefix/ContainmentPagingMenus/1/{extendedMenuTypeName}"; - var menu2ResourcePath = $"/prefix/ContainmentPagingMenus/2/{extendedMenuTypeName}"; + [Fact] + public async Task VerifyExpectedNextLinksGeneratedForNonContainedNavigationPropertyDeclaredOnDerivedTypeAsODataPathSegment() + { + // Arrange + var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; + var extendedPanelTypeName = typeof(ContainmentPagingExtendedPanel).FullName; + var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; + var menu1ResourcePath = $"/prefix/ContainmentPagingMenus/1/{extendedMenuTypeName}"; + + var requestUri = $"{menu1ResourcePath}/Panels?$expand={extendedPanelTypeName}/Items($expand={extendedItemTypeName}/Notes)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Contains($"/prefix/ContainmentPagingPanels/1/{extendedPanelTypeName}/Items/1/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/1/{extendedPanelTypeName}/Items/2/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/1/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/2/{extendedPanelTypeName}/Items/4/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/2/{extendedPanelTypeName}/Items/5/{extendedItemTypeName}/Notes?$skip=2", content); + Assert.Contains($"/prefix/ContainmentPagingPanels/2/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); + Assert.Contains($"{menu1ResourcePath}/Panels?$expand={extendedPanelTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + } - var requestUri = $"{menusResourcePath}?$expand={extendedMenuTypeName}/Tabs($expand={extendedTabTypeName}/Items($expand={extendedItemTypeName}/Notes))"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); + [Theory] + [InlineData("")] + [InlineData("?$select=Tags,Categories,Locations")] + public async Task VerifyServerSidePagingNotAppliedToNonEntityCollections(string url) + { + // Arrange + var requestUri = $"/prefix/CollectionPagingCustomers{url}"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsObject(); - // Assert - Assert.Contains($"{menu1ResourcePath}/Tabs/1/{extendedTabTypeName}/Items/1/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Tabs/1/{extendedTabTypeName}/Items/2/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Tabs/1/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Tabs/2/{extendedTabTypeName}/Items/4/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Tabs/2/{extendedTabTypeName}/Items/5/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Tabs/2/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); - - Assert.Contains($"{menu2ResourcePath}/Tabs/4/{extendedTabTypeName}/Items/10/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{menu2ResourcePath}/Tabs/4/{extendedTabTypeName}/Items/11/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{menu2ResourcePath}/Tabs/4/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"{menu2ResourcePath}/Tabs/5/{extendedTabTypeName}/Items/13/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{menu2ResourcePath}/Tabs/5/{extendedTabTypeName}/Items/14/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{menu2ResourcePath}/Tabs/5/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"{menu2ResourcePath}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); - } + // Assert + var pageResult = content.GetValue("value") as JArray; + Assert.NotNull(pageResult); + Assert.Equal(2, pageResult.Count); - [Fact] - public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyDeclaredOnDerivedTypeAsODataPathSegment() + foreach (JObject item in pageResult) { - // Arrange - var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; - var extendedTabTypeName = typeof(ContainedPagingExtendedTab).FullName; - var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; - var menu1ResourcePath = $"/prefix/ContainmentPagingMenus/1/{extendedMenuTypeName}"; + var tags = item.GetValue("Tags") as JArray; + Assert.NotNull(tags); + Assert.Equal(3, tags.Count); - var requestUri = $"{menu1ResourcePath}/Tabs?$expand={extendedTabTypeName}/Items($expand={extendedItemTypeName}/Notes)"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); + var categories = item.GetValue("Categories") as JArray; + Assert.NotNull(categories); + Assert.Equal(3, categories.Count); - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Contains($"{menu1ResourcePath}/Tabs/1/{extendedTabTypeName}/Items/1/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Tabs/1/{extendedTabTypeName}/Items/2/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Tabs/1/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Tabs/2/{extendedTabTypeName}/Items/4/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Tabs/2/{extendedTabTypeName}/Items/5/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Tabs/2/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); + var locations = item.GetValue("Locations") as JArray; + Assert.NotNull(locations); + Assert.Equal(3, locations.Count); } + } - [Fact] - public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyDeclaredOnDerivedTypeInSingletonScenario() - { - // Arrange - var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; - var extendedTabTypeName = typeof(ContainedPagingExtendedTab).FullName; - var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; - var ribbonResourcePath = $"/prefix/ContainmentPagingRibbon"; - - var requestUri = $"{ribbonResourcePath}?$expand={extendedMenuTypeName}/Tabs($expand={extendedTabTypeName}/Items($expand={extendedItemTypeName}/Notes))"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); - - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs/1/{extendedTabTypeName}/Items/1/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs/1/{extendedTabTypeName}/Items/2/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs/1/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs/2/{extendedTabTypeName}/Items/4/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs/2/{extendedTabTypeName}/Items/5/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs/2/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"{ribbonResourcePath}/{extendedMenuTypeName}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); - } + [Fact] + public async Task VerifyClientSidePagingAppliedToNonEntityCollections() + { + // Arrange + var requestUri = "/prefix/CollectionPagingCustomers?$select=Tags($skip=1;$top=1),Categories($skip=1;$top=1),Locations($skip=1;$top=1)"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + var client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + var content = await response.Content.ReadAsObject(); + + // Assert + var pageResult = content.GetValue("value") as JArray; + Assert.NotNull(pageResult); + Assert.Equal(2, pageResult.Count); + + JObject page; + string tag; + CollectionPagingCategory? category; + CollectionPagingLocation location; + + page = pageResult[0] as JObject; + tag = Assert.Single(page.GetValue("Tags")).ToObject(); + category = Assert.Single(page.GetValue("Categories")).ToObject(); + location = Assert.Single(page.GetValue("Locations")).ToObject(); + + Assert.Equal("Gen-Z", tag); + Assert.Equal(CollectionPagingCategory.Wholesaler, category); + Assert.NotNull(location); + Assert.Equal("Street 12", location.Street); + + page = pageResult[1] as JObject; + tag = Assert.Single(page.GetValue("Tags")).ToObject(); + category = Assert.Single(page.GetValue("Categories")).ToObject(); + location = Assert.Single(page.GetValue("Locations")).ToObject(); + + Assert.Equal("Gen-Z", tag); + Assert.Equal(CollectionPagingCategory.Wholesaler, category); + Assert.NotNull(location); + Assert.Equal("Street 22", location.Street); + } +} - [Fact] - public async Task VerifyExpectedNextLinksGeneratedForContainedNavigationPropertyDeclaredOnDerivedTypeAsODataPathSegmentInSingletonScenario() - { - // Arrange - var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; - var extendedTabTypeName = typeof(ContainedPagingExtendedTab).FullName; - var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; - var ribbonResourcePath = $"/prefix/ContainmentPagingRibbon/{extendedMenuTypeName}"; +public class SkipTokenPagingTests : WebApiTestBase +{ + public SkipTokenPagingTests(WebApiTestFixture fixture) + : base(fixture) + { + } - var requestUri = $"{ribbonResourcePath}/Tabs?$expand={extendedTabTypeName}/Items($expand={extendedItemTypeName}/Notes)"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel model = GetEdmModel(); + services.ConfigureControllers( + typeof(SkipTokenPagingS1CustomersController), + typeof(SkipTokenPagingS2CustomersController), + typeof(SkipTokenPagingS3CustomersController)); + services.AddControllers().AddOData(opt => opt.Expand().OrderBy().SkipToken().AddRouteComponents("{a}", model)); + } - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); + protected static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("SkipTokenPagingS1Customers"); + builder.EntitySet("SkipTokenPagingS2Customers"); + builder.EntitySet("SkipTokenPagingS3Customers"); - // Assert - Assert.Contains($"{ribbonResourcePath}/Tabs/1/{extendedTabTypeName}/Items/1/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{ribbonResourcePath}/Tabs/1/{extendedTabTypeName}/Items/2/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{ribbonResourcePath}/Tabs/1/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"{ribbonResourcePath}/Tabs/2/{extendedTabTypeName}/Items/4/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{ribbonResourcePath}/Tabs/2/{extendedTabTypeName}/Items/5/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"{ribbonResourcePath}/Tabs/2/{extendedTabTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"{ribbonResourcePath}/Tabs?$expand={extendedTabTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); - } + return builder.GetEdmModel(); + } - [Fact] - public async Task VerifyExpectedNextLinksGeneratedForNonContainedNavigationPropertyDeclaredOnDerivedType() + [Fact] + public async Task VerifySkipTokenPagingOrderedByNullableProperty() + { + HttpClient client = CreateClient(); + HttpRequestMessage request; + HttpResponseMessage response; + JObject content; + JArray pageResult; + + // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) + // is intentional. The next-link in one response is used in the next request + // so we need to control the execution order (unlike Theory attribute where order is random) + var skipTokenTestData = new List<(int, decimal?, int, decimal?)> { - // Arrange - var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; - var extendedPanelTypeName = typeof(ContainmentPagingExtendedPanel).FullName; - var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; - var menusResourcePath = $"/prefix/ContainmentPagingMenus"; - var menu1ResourcePath = $"/prefix/ContainmentPagingMenus/1/{extendedMenuTypeName}"; - var menu2ResourcePath = $"/prefix/ContainmentPagingMenus/2/{extendedMenuTypeName}"; + (1, null, 3, null), + (5, null, 2, 2), + (7, 5, 9, 25), + (4, 30, 6, 35) + }; - var requestUri = $"{menusResourcePath}?$expand={extendedMenuTypeName}/Panels($expand={extendedPanelTypeName}/Items($expand={extendedItemTypeName}/Notes))"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); - - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); + string requestUri = "/prefix/SkipTokenPagingS1Customers?$orderby=CreditLimit"; - // Assert - Assert.Contains($"/prefix/ContainmentPagingPanels/1/{extendedPanelTypeName}/Items/1/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/1/{extendedPanelTypeName}/Items/2/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/1/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/2/{extendedPanelTypeName}/Items/4/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/2/{extendedPanelTypeName}/Items/5/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/2/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Panels?$expand={extendedPanelTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); - - Assert.Contains($"/prefix/ContainmentPagingPanels/4/{extendedPanelTypeName}/Items/10/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/4/{extendedPanelTypeName}/Items/11/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/4/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/5/{extendedPanelTypeName}/Items/13/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/5/{extendedPanelTypeName}/Items/14/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/5/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"{menu2ResourcePath}/Panels?$expand={extendedPanelTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); - } - - [Fact] - public async Task VerifyExpectedNextLinksGeneratedForNonContainedNavigationPropertyDeclaredOnDerivedTypeAsODataPathSegment() + foreach (var testData in skipTokenTestData) { - // Arrange - var extendedMenuTypeName = typeof(ContainmentPagingExtendedMenu).FullName; - var extendedPanelTypeName = typeof(ContainmentPagingExtendedPanel).FullName; - var extendedItemTypeName = typeof(ContainedPagingExtendedItem).FullName; - var menu1ResourcePath = $"/prefix/ContainmentPagingMenus/1/{extendedMenuTypeName}"; + int idAt0 = testData.Item1; + decimal? creditLimitAt0 = testData.Item2; + int idAt1 = testData.Item3; + decimal? creditLimitAt1 = testData.Item4; - var requestUri = $"{menu1ResourcePath}/Panels?$expand={extendedPanelTypeName}/Items($expand={extendedItemTypeName}/Notes)"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); - - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Contains($"/prefix/ContainmentPagingPanels/1/{extendedPanelTypeName}/Items/1/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/1/{extendedPanelTypeName}/Items/2/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/1/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/2/{extendedPanelTypeName}/Items/4/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/2/{extendedPanelTypeName}/Items/5/{extendedItemTypeName}/Notes?$skip=2", content); - Assert.Contains($"/prefix/ContainmentPagingPanels/2/{extendedPanelTypeName}/Items?$expand={extendedItemTypeName}%2FNotes&$skip=2", content); - Assert.Contains($"{menu1ResourcePath}/Panels?$expand={extendedPanelTypeName}%2FItems%28%24expand%3D{extendedItemTypeName}%2FNotes%29&$skip=2", content); - } - - [Theory] - [InlineData("")] - [InlineData("?$select=Tags,Categories,Locations")] - public async Task VerifyServerSidePagingNotAppliedToNonEntityCollections(string url) - { // Arrange - var requestUri = $"/prefix/CollectionPagingCustomers{url}"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + string skipToken = string.Concat("$skiptoken=CreditLimit-", creditLimitAt1 != null ? creditLimitAt1.ToString() : "null", ",Id-", idAt1); // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsObject(); + response = await client.SendAsync(request); + content = await response.Content.ReadAsObject(); // Assert - var pageResult = content.GetValue("value") as JArray; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + pageResult = content["value"] as JArray; Assert.NotNull(pageResult); Assert.Equal(2, pageResult.Count); + Assert.Equal(idAt0, (pageResult[0] as JObject)["Id"].ToObject()); + Assert.Equal(creditLimitAt0, (pageResult[0] as JObject)["CreditLimit"].ToObject()); + Assert.Equal(idAt1, (pageResult[1] as JObject)["Id"].ToObject()); + Assert.Equal(creditLimitAt1, (pageResult[1] as JObject)["CreditLimit"].ToObject()); - foreach (JObject item in pageResult) - { - var tags = item.GetValue("Tags") as JArray; - Assert.NotNull(tags); - Assert.Equal(3, tags.Count); - - var categories = item.GetValue("Categories") as JArray; - Assert.NotNull(categories); - Assert.Equal(3, categories.Count); + string nextPageLink = content["@odata.nextLink"].ToObject(); + Assert.NotNull(nextPageLink); + Assert.EndsWith("/prefix/SkipTokenPagingS1Customers?$orderby=CreditLimit&" + skipToken, nextPageLink); - var locations = item.GetValue("Locations") as JArray; - Assert.NotNull(locations); - Assert.Equal(3, locations.Count); - } + requestUri = nextPageLink; } - [Fact] - public async Task VerifyClientSidePagingAppliedToNonEntityCollections() - { - // Arrange - var requestUri = "/prefix/CollectionPagingCustomers?$select=Tags($skip=1;$top=1),Categories($skip=1;$top=1),Locations($skip=1;$top=1)"; - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var client = CreateClient(); + // Fetch last page + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + response = await client.SendAsync(request); - // Act - var response = await client.SendAsync(request); - var content = await response.Content.ReadAsObject(); + content = await response.Content.ReadAsObject(); - // Assert - var pageResult = content.GetValue("value") as JArray; - Assert.NotNull(pageResult); - Assert.Equal(2, pageResult.Count); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject page; - string tag; - CollectionPagingCategory? category; - CollectionPagingLocation location; - - page = pageResult[0] as JObject; - tag = Assert.Single(page.GetValue("Tags")).ToObject(); - category = Assert.Single(page.GetValue("Categories")).ToObject(); - location = Assert.Single(page.GetValue("Locations")).ToObject(); - - Assert.Equal("Gen-Z", tag); - Assert.Equal(CollectionPagingCategory.Wholesaler, category); - Assert.NotNull(location); - Assert.Equal("Street 12", location.Street); - - page = pageResult[1] as JObject; - tag = Assert.Single(page.GetValue("Tags")).ToObject(); - category = Assert.Single(page.GetValue("Categories")).ToObject(); - location = Assert.Single(page.GetValue("Locations")).ToObject(); - - Assert.Equal("Gen-Z", tag); - Assert.Equal(CollectionPagingCategory.Wholesaler, category); - Assert.NotNull(location); - Assert.Equal("Street 22", location.Street); - } + pageResult = content["value"] as JArray; + Assert.NotNull(pageResult); + Assert.Single(pageResult); + Assert.Equal(8, (pageResult[0] as JObject)["Id"].ToObject()); + Assert.Equal(50, (pageResult[0] as JObject)["CreditLimit"].ToObject()); + Assert.Null(content.GetValue("@odata.nextLink")); } - public class SkipTokenPagingTests : WebApiTestBase + [Fact] + public async Task VerifySkipTokenPagingOrderedByNullablePropertyDescending() { - public SkipTokenPagingTests(WebApiTestFixture fixture) - : base(fixture) - { - } - - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) + HttpClient client = CreateClient(); + HttpRequestMessage request; + HttpResponseMessage response; + JObject content; + JArray pageResult; + + // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) + // is intentional. The next-link in one response is used in the next request + // so we need to control the execution order (unlike Theory attribute where order is random) + var skipTokenTestData = new List<(int, decimal?)> { - IEdmModel model = GetEdmModel(); - services.ConfigureControllers( - typeof(SkipTokenPagingS1CustomersController), - typeof(SkipTokenPagingS2CustomersController), - typeof(SkipTokenPagingS3CustomersController)); - services.AddControllers().AddOData(opt => opt.Expand().OrderBy().SkipToken().AddRouteComponents("{a}", model)); - } + (6, 35), + (9, 25), + (2, 2), + (3, null) + }; - protected static IEdmModel GetEdmModel() - { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("SkipTokenPagingS1Customers"); - builder.EntitySet("SkipTokenPagingS2Customers"); - builder.EntitySet("SkipTokenPagingS3Customers"); - - return builder.GetEdmModel(); - } + string requestUri = "/prefix/SkipTokenPagingS1Customers?$orderby=CreditLimit desc"; - [Fact] - public async Task VerifySkipTokenPagingOrderedByNullableProperty() + foreach (var testData in skipTokenTestData) { - HttpClient client = CreateClient(); - HttpRequestMessage request; - HttpResponseMessage response; - JObject content; - JArray pageResult; - - // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) - // is intentional. The next-link in one response is used in the next request - // so we need to control the execution order (unlike Theory attribute where order is random) - var skipTokenTestData = new List<(int, decimal?, int, decimal?)> - { - (1, null, 3, null), - (5, null, 2, 2), - (7, 5, 9, 25), - (4, 30, 6, 35) - }; - - string requestUri = "/prefix/SkipTokenPagingS1Customers?$orderby=CreditLimit"; + int idAt1 = testData.Item1; + decimal? creditLimitAt1 = testData.Item2; - foreach (var testData in skipTokenTestData) - { - int idAt0 = testData.Item1; - decimal? creditLimitAt0 = testData.Item2; - int idAt1 = testData.Item3; - decimal? creditLimitAt1 = testData.Item4; - - // Arrange - request = new HttpRequestMessage(HttpMethod.Get, requestUri); - string skipToken = string.Concat("$skiptoken=CreditLimit-", creditLimitAt1 != null ? creditLimitAt1.ToString() : "null", ",Id-", idAt1); - - // Act - response = await client.SendAsync(request); - content = await response.Content.ReadAsObject(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - pageResult = content["value"] as JArray; - Assert.NotNull(pageResult); - Assert.Equal(2, pageResult.Count); - Assert.Equal(idAt0, (pageResult[0] as JObject)["Id"].ToObject()); - Assert.Equal(creditLimitAt0, (pageResult[0] as JObject)["CreditLimit"].ToObject()); - Assert.Equal(idAt1, (pageResult[1] as JObject)["Id"].ToObject()); - Assert.Equal(creditLimitAt1, (pageResult[1] as JObject)["CreditLimit"].ToObject()); - - string nextPageLink = content["@odata.nextLink"].ToObject(); - Assert.NotNull(nextPageLink); - Assert.EndsWith("/prefix/SkipTokenPagingS1Customers?$orderby=CreditLimit&" + skipToken, nextPageLink); - - requestUri = nextPageLink; - } - - // Fetch last page + // Arrange request = new HttpRequestMessage(HttpMethod.Get, requestUri); - response = await client.SendAsync(request); + string skipToken = string.Concat("$skiptoken=CreditLimit-", creditLimitAt1 != null ? creditLimitAt1.ToString() : "null", ",Id-", idAt1); + // Act + response = await client.SendAsync(request); content = await response.Content.ReadAsObject(); // Assert @@ -614,67 +660,67 @@ public async Task VerifySkipTokenPagingOrderedByNullableProperty() pageResult = content["value"] as JArray; Assert.NotNull(pageResult); - Assert.Single(pageResult); - Assert.Equal(8, (pageResult[0] as JObject)["Id"].ToObject()); - Assert.Equal(50, (pageResult[0] as JObject)["CreditLimit"].ToObject()); - Assert.Null(content.GetValue("@odata.nextLink")); - } + Assert.Equal(2, pageResult.Count); + Assert.Equal(idAt1, (pageResult[1] as JObject)["Id"].ToObject()); + Assert.Equal(creditLimitAt1, (pageResult[1] as JObject)["CreditLimit"].ToObject()); - [Fact] - public async Task VerifySkipTokenPagingOrderedByNullablePropertyDescending() - { - HttpClient client = CreateClient(); - HttpRequestMessage request; - HttpResponseMessage response; - JObject content; - JArray pageResult; - - // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) - // is intentional. The next-link in one response is used in the next request - // so we need to control the execution order (unlike Theory attribute where order is random) - var skipTokenTestData = new List<(int, decimal?)> - { - (6, 35), - (9, 25), - (2, 2), - (3, null) - }; + string nextPageLink = content["@odata.nextLink"].ToObject(); + Assert.NotNull(nextPageLink); + Assert.EndsWith("/prefix/SkipTokenPagingS1Customers?$orderby=CreditLimit%20desc&" + skipToken, nextPageLink); - string requestUri = "/prefix/SkipTokenPagingS1Customers?$orderby=CreditLimit desc"; + requestUri = nextPageLink; + } - foreach (var testData in skipTokenTestData) - { - int idAt1 = testData.Item1; - decimal? creditLimitAt1 = testData.Item2; + // Fetch last page + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + response = await client.SendAsync(request); - // Arrange - request = new HttpRequestMessage(HttpMethod.Get, requestUri); - string skipToken = string.Concat("$skiptoken=CreditLimit-", creditLimitAt1 != null ? creditLimitAt1.ToString() : "null", ",Id-", idAt1); + content = await response.Content.ReadAsObject(); - // Act - response = await client.SendAsync(request); - content = await response.Content.ReadAsObject(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + pageResult = content["value"] as JArray; + Assert.NotNull(pageResult); + Assert.Single(pageResult); + Assert.Equal(5, (pageResult[0] as JObject)["Id"].ToObject()); + Assert.Null((pageResult[0] as JObject)["CreditLimit"].ToObject()); + Assert.Null(content.GetValue("@odata.nextLink")); + } - pageResult = content["value"] as JArray; - Assert.NotNull(pageResult); - Assert.Equal(2, pageResult.Count); - Assert.Equal(idAt1, (pageResult[1] as JObject)["Id"].ToObject()); - Assert.Equal(creditLimitAt1, (pageResult[1] as JObject)["CreditLimit"].ToObject()); + [Fact] + public async Task VerifySkipTokenPagingOrderedByNonNullablePropertyThenByNullableProperty() + { + HttpClient client = CreateClient(); + HttpRequestMessage request; + HttpResponseMessage response; + JObject content; + JArray pageResult; + + // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) + // is intentional. The next-link in one response is used in the next request + // so we need to control the execution order (unlike Theory attribute where order is random) + var skipTokenTestData = new List<(int, string, decimal?)> + { + (2, "B", null), + (6, "C", null), + (11, "F", 35), + }; - string nextPageLink = content["@odata.nextLink"].ToObject(); - Assert.NotNull(nextPageLink); - Assert.EndsWith("/prefix/SkipTokenPagingS1Customers?$orderby=CreditLimit%20desc&" + skipToken, nextPageLink); + string requestUri = "/prefix/SkipTokenPagingS2Customers?$orderby=Grade,CreditLimit"; - requestUri = nextPageLink; - } + foreach (var testData in skipTokenTestData) + { + int idAt3 = testData.Item1; + string gradeAt3 = testData.Item2; + decimal? creditLimitAt3 = testData.Item3; - // Fetch last page + // Arrange request = new HttpRequestMessage(HttpMethod.Get, requestUri); - response = await client.SendAsync(request); + string skipToken = string.Concat("$skiptoken=Grade-%27", gradeAt3, "%27,CreditLimit-", creditLimitAt3 != null ? creditLimitAt3.ToString() : "null", ",Id-", idAt3); + // Act + response = await client.SendAsync(request); content = await response.Content.ReadAsObject(); // Assert @@ -682,68 +728,69 @@ public async Task VerifySkipTokenPagingOrderedByNullablePropertyDescending() pageResult = content["value"] as JArray; Assert.NotNull(pageResult); - Assert.Single(pageResult); - Assert.Equal(5, (pageResult[0] as JObject)["Id"].ToObject()); - Assert.Null((pageResult[0] as JObject)["CreditLimit"].ToObject()); - Assert.Null(content.GetValue("@odata.nextLink")); - } + Assert.Equal(4, pageResult.Count); + Assert.Equal(idAt3, (pageResult[3] as JObject)["Id"].ToObject()); + Assert.Equal(gradeAt3, (pageResult[3] as JObject)["Grade"].ToObject()); + Assert.Equal(creditLimitAt3, (pageResult[3] as JObject)["CreditLimit"].ToObject()); - [Fact] - public async Task VerifySkipTokenPagingOrderedByNonNullablePropertyThenByNullableProperty() - { - HttpClient client = CreateClient(); - HttpRequestMessage request; - HttpResponseMessage response; - JObject content; - JArray pageResult; - - // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) - // is intentional. The next-link in one response is used in the next request - // so we need to control the execution order (unlike Theory attribute where order is random) - var skipTokenTestData = new List<(int, string, decimal?)> - { - (2, "B", null), - (6, "C", null), - (11, "F", 35), - }; + string nextPageLink = content["@odata.nextLink"].ToObject(); + Assert.NotNull(nextPageLink); + Assert.EndsWith("/prefix/SkipTokenPagingS2Customers?$orderby=Grade%2CCreditLimit&" + skipToken, nextPageLink); - string requestUri = "/prefix/SkipTokenPagingS2Customers?$orderby=Grade,CreditLimit"; + requestUri = nextPageLink; + } - foreach (var testData in skipTokenTestData) - { - int idAt3 = testData.Item1; - string gradeAt3 = testData.Item2; - decimal? creditLimitAt3 = testData.Item3; + // Fetch last page + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + response = await client.SendAsync(request); - // Arrange - request = new HttpRequestMessage(HttpMethod.Get, requestUri); - string skipToken = string.Concat("$skiptoken=Grade-%27", gradeAt3, "%27,CreditLimit-", creditLimitAt3 != null ? creditLimitAt3.ToString() : "null", ",Id-", idAt3); + content = await response.Content.ReadAsObject(); - // Act - response = await client.SendAsync(request); - content = await response.Content.ReadAsObject(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + pageResult = content["value"] as JArray; + Assert.NotNull(pageResult); + Assert.Single(pageResult); + Assert.Equal(13, (pageResult[0] as JObject)["Id"].ToObject()); + Assert.Equal("F", (pageResult[0] as JObject)["Grade"].ToObject()); + Assert.Equal(55, (pageResult[0] as JObject)["CreditLimit"].ToObject()); + Assert.Null(content.GetValue("@odata.nextLink")); + } - pageResult = content["value"] as JArray; - Assert.NotNull(pageResult); - Assert.Equal(4, pageResult.Count); - Assert.Equal(idAt3, (pageResult[3] as JObject)["Id"].ToObject()); - Assert.Equal(gradeAt3, (pageResult[3] as JObject)["Grade"].ToObject()); - Assert.Equal(creditLimitAt3, (pageResult[3] as JObject)["CreditLimit"].ToObject()); + [Fact] + public async Task VerifySkipTokenPagingOrderedByNullablePropertyThenByNonNullableProperty() + { + HttpClient client = CreateClient(); + HttpRequestMessage request; + HttpResponseMessage response; + JObject content; + JArray pageResult; + + // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) + // is intentional. The next-link in one response is used in the next request + // so we need to control the execution order (unlike Theory attribute where order is random) + var skipTokenTestData = new List<(int, string, decimal?)> + { + (6, "C", null), + (5, "A", 30), + (10, "D", 50), + }; - string nextPageLink = content["@odata.nextLink"].ToObject(); - Assert.NotNull(nextPageLink); - Assert.EndsWith("/prefix/SkipTokenPagingS2Customers?$orderby=Grade%2CCreditLimit&" + skipToken, nextPageLink); + string requestUri = "/prefix/SkipTokenPagingS2Customers?$orderby=CreditLimit,Grade"; - requestUri = nextPageLink; - } + foreach (var testData in skipTokenTestData) + { + int idAt3 = testData.Item1; + string gradeAt3 = testData.Item2; + decimal? creditLimitAt3 = testData.Item3; - // Fetch last page + // Arrange request = new HttpRequestMessage(HttpMethod.Get, requestUri); - response = await client.SendAsync(request); + string skipToken = string.Concat("$skiptoken=CreditLimit-", creditLimitAt3 != null ? creditLimitAt3.ToString() : "null", ",Grade-%27", gradeAt3, "%27", ",Id-", idAt3); + // Act + response = await client.SendAsync(request); content = await response.Content.ReadAsObject(); // Assert @@ -751,69 +798,74 @@ public async Task VerifySkipTokenPagingOrderedByNonNullablePropertyThenByNullabl pageResult = content["value"] as JArray; Assert.NotNull(pageResult); - Assert.Single(pageResult); - Assert.Equal(13, (pageResult[0] as JObject)["Id"].ToObject()); - Assert.Equal("F", (pageResult[0] as JObject)["Grade"].ToObject()); - Assert.Equal(55, (pageResult[0] as JObject)["CreditLimit"].ToObject()); - Assert.Null(content.GetValue("@odata.nextLink")); - } + Assert.Equal(4, pageResult.Count); + Assert.Equal(idAt3, (pageResult[3] as JObject)["Id"].ToObject()); + Assert.Equal(gradeAt3, (pageResult[3] as JObject)["Grade"].ToObject()); + Assert.Equal(creditLimitAt3, (pageResult[3] as JObject)["CreditLimit"].ToObject()); - [Fact] - public async Task VerifySkipTokenPagingOrderedByNullablePropertyThenByNonNullableProperty() - { - HttpClient client = CreateClient(); - HttpRequestMessage request; - HttpResponseMessage response; - JObject content; - JArray pageResult; - - // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) - // is intentional. The next-link in one response is used in the next request - // so we need to control the execution order (unlike Theory attribute where order is random) - var skipTokenTestData = new List<(int, string, decimal?)> - { - (6, "C", null), - (5, "A", 30), - (10, "D", 50), - }; + string nextPageLink = content["@odata.nextLink"].ToObject(); + Assert.NotNull(nextPageLink); + Assert.EndsWith("/prefix/SkipTokenPagingS2Customers?$orderby=CreditLimit%2CGrade&" + skipToken, nextPageLink); - string requestUri = "/prefix/SkipTokenPagingS2Customers?$orderby=CreditLimit,Grade"; + requestUri = nextPageLink; + } - foreach (var testData in skipTokenTestData) - { - int idAt3 = testData.Item1; - string gradeAt3 = testData.Item2; - decimal? creditLimitAt3 = testData.Item3; + // Fetch last page + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + response = await client.SendAsync(request); - // Arrange - request = new HttpRequestMessage(HttpMethod.Get, requestUri); - string skipToken = string.Concat("$skiptoken=CreditLimit-", creditLimitAt3 != null ? creditLimitAt3.ToString() : "null", ",Grade-%27", gradeAt3, "%27", ",Id-", idAt3); + content = await response.Content.ReadAsObject(); - // Act - response = await client.SendAsync(request); - content = await response.Content.ReadAsObject(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + pageResult = content["value"] as JArray; + Assert.NotNull(pageResult); + Assert.Single(pageResult); + Assert.Equal(13, (pageResult[0] as JObject)["Id"].ToObject()); + Assert.Equal("F", (pageResult[0] as JObject)["Grade"].ToObject()); + Assert.Equal(55, (pageResult[0] as JObject)["CreditLimit"].ToObject()); + Assert.Null(content.GetValue("@odata.nextLink")); + } - pageResult = content["value"] as JArray; - Assert.NotNull(pageResult); - Assert.Equal(4, pageResult.Count); - Assert.Equal(idAt3, (pageResult[3] as JObject)["Id"].ToObject()); - Assert.Equal(gradeAt3, (pageResult[3] as JObject)["Grade"].ToObject()); - Assert.Equal(creditLimitAt3, (pageResult[3] as JObject)["CreditLimit"].ToObject()); + [Fact] + public async Task VerifySkipTokenPagingOrderedByNullableDateTimeProperty() + { + HttpClient client = CreateClient(); + HttpRequestMessage request; + HttpResponseMessage response; + JObject content; + JArray pageResult; + + // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) + // is intentional. The next-link in one response is used in the next request + // so we need to control the execution order (unlike Theory attribute where order is random) + var skipTokenTestData = new List<(int, DateTime?, int, DateTime?)> + { + (1, null, 3, null), + (5, null, 2, new DateTime(2023, 1, 2)), + (7, new DateTime(2023, 1, 5), 9, new DateTime(2023, 1, 25)), + (4, new DateTime(2023, 1, 30), 6, new DateTime(2023, 2, 4)) + }; - string nextPageLink = content["@odata.nextLink"].ToObject(); - Assert.NotNull(nextPageLink); - Assert.EndsWith("/prefix/SkipTokenPagingS2Customers?$orderby=CreditLimit%2CGrade&" + skipToken, nextPageLink); + string requestUri = "/prefix/SkipTokenPagingS3Customers?$orderby=CustomerSince"; - requestUri = nextPageLink; - } + foreach (var testData in skipTokenTestData) + { + int idAt0 = testData.Item1; + DateTime? customerSinceAt0 = testData.Item2; + int idAt1 = testData.Item3; + DateTime? customerSinceAt1 = testData.Item4; - // Fetch last page + // Arrange request = new HttpRequestMessage(HttpMethod.Get, requestUri); - response = await client.SendAsync(request); + string skipTokenStart = string.Concat( + "$skiptoken=CustomerSince-", + customerSinceAt1 != null ? customerSinceAt1.Value.ToString("yyyy-MM-dd") : "null"); + string skipTokenEnd = string.Concat(",Id-", idAt1); + // Act + response = await client.SendAsync(request); content = await response.Content.ReadAsObject(); // Assert @@ -821,76 +873,73 @@ public async Task VerifySkipTokenPagingOrderedByNullablePropertyThenByNonNullabl pageResult = content["value"] as JArray; Assert.NotNull(pageResult); - Assert.Single(pageResult); - Assert.Equal(13, (pageResult[0] as JObject)["Id"].ToObject()); - Assert.Equal("F", (pageResult[0] as JObject)["Grade"].ToObject()); - Assert.Equal(55, (pageResult[0] as JObject)["CreditLimit"].ToObject()); - Assert.Null(content.GetValue("@odata.nextLink")); + Assert.Equal(2, pageResult.Count); + Assert.Equal(idAt0, (pageResult[0] as JObject)["Id"].ToObject()); + Assert.Equal(customerSinceAt0, (pageResult[0] as JObject)["CustomerSince"].ToObject()); + Assert.Equal(idAt1, (pageResult[1] as JObject)["Id"].ToObject()); + Assert.Equal(customerSinceAt1, (pageResult[1] as JObject)["CustomerSince"].ToObject()); + + string nextPageLink = content["@odata.nextLink"].ToObject(); + Assert.NotNull(nextPageLink); + Assert.Contains("/prefix/SkipTokenPagingS3Customers?$orderby=CustomerSince&" + skipTokenStart, nextPageLink); + Assert.EndsWith(skipTokenEnd, nextPageLink); + + requestUri = nextPageLink; } - [Fact] - public async Task VerifySkipTokenPagingOrderedByNullableDateTimeProperty() + // Fetch last page + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + response = await client.SendAsync(request); + + content = await response.Content.ReadAsObject(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + pageResult = content["value"] as JArray; + Assert.NotNull(pageResult); + Assert.Single(pageResult); + Assert.Equal(8, (pageResult[0] as JObject)["Id"].ToObject()); + Assert.Equal(new DateTime(2023, 2, 19), (pageResult[0] as JObject)["CustomerSince"].ToObject()); + Assert.Null(content.GetValue("@odata.nextLink")); + } + + [Fact] + public async Task VerifySkipTokenPagingOrderedByNullableDateTimePropertyDescending() + { + HttpClient client = CreateClient(); + HttpRequestMessage request; + HttpResponseMessage response; + JObject content; + JArray pageResult; + + // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) + // is intentional. The next-link in one response is used in the next request + // so we need to control the execution order (unlike Theory attribute where order is random) + var skipTokenTestData = new List<(int, DateTime?)> { - HttpClient client = CreateClient(); - HttpRequestMessage request; - HttpResponseMessage response; - JObject content; - JArray pageResult; - - // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) - // is intentional. The next-link in one response is used in the next request - // so we need to control the execution order (unlike Theory attribute where order is random) - var skipTokenTestData = new List<(int, DateTime?, int, DateTime?)> - { - (1, null, 3, null), - (5, null, 2, new DateTime(2023, 1, 2)), - (7, new DateTime(2023, 1, 5), 9, new DateTime(2023, 1, 25)), - (4, new DateTime(2023, 1, 30), 6, new DateTime(2023, 2, 4)) - }; + (6, new DateTime(2023, 2, 4)), + (9, new DateTime(2023, 1, 25)), + (2, new DateTime(2023, 1, 2)), + (3, null) + }; - string requestUri = "/prefix/SkipTokenPagingS3Customers?$orderby=CustomerSince"; + string requestUri = "/prefix/SkipTokenPagingS3Customers?$orderby=CustomerSince desc"; - foreach (var testData in skipTokenTestData) - { - int idAt0 = testData.Item1; - DateTime? customerSinceAt0 = testData.Item2; - int idAt1 = testData.Item3; - DateTime? customerSinceAt1 = testData.Item4; - - // Arrange - request = new HttpRequestMessage(HttpMethod.Get, requestUri); - string skipTokenStart = string.Concat( - "$skiptoken=CustomerSince-", - customerSinceAt1 != null ? customerSinceAt1.Value.ToString("yyyy-MM-dd") : "null"); - string skipTokenEnd = string.Concat(",Id-", idAt1); - - // Act - response = await client.SendAsync(request); - content = await response.Content.ReadAsObject(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - pageResult = content["value"] as JArray; - Assert.NotNull(pageResult); - Assert.Equal(2, pageResult.Count); - Assert.Equal(idAt0, (pageResult[0] as JObject)["Id"].ToObject()); - Assert.Equal(customerSinceAt0, (pageResult[0] as JObject)["CustomerSince"].ToObject()); - Assert.Equal(idAt1, (pageResult[1] as JObject)["Id"].ToObject()); - Assert.Equal(customerSinceAt1, (pageResult[1] as JObject)["CustomerSince"].ToObject()); - - string nextPageLink = content["@odata.nextLink"].ToObject(); - Assert.NotNull(nextPageLink); - Assert.Contains("/prefix/SkipTokenPagingS3Customers?$orderby=CustomerSince&" + skipTokenStart, nextPageLink); - Assert.EndsWith(skipTokenEnd, nextPageLink); - - requestUri = nextPageLink; - } + foreach (var testData in skipTokenTestData) + { + int idAt1 = testData.Item1; + DateTime? customerSinceAt1 = testData.Item2; - // Fetch last page + // Arrange request = new HttpRequestMessage(HttpMethod.Get, requestUri); - response = await client.SendAsync(request); + string skipTokenStart = string.Concat( + "$skiptoken=CustomerSince-", + customerSinceAt1 != null ? customerSinceAt1.Value.ToString("yyyy-MM-dd") : "null"); + string skipTokenEnd = string.Concat(",Id-", idAt1); + // Act + response = await client.SendAsync(request); content = await response.Content.ReadAsObject(); // Assert @@ -898,191 +947,118 @@ public async Task VerifySkipTokenPagingOrderedByNullableDateTimeProperty() pageResult = content["value"] as JArray; Assert.NotNull(pageResult); - Assert.Single(pageResult); - Assert.Equal(8, (pageResult[0] as JObject)["Id"].ToObject()); - Assert.Equal(new DateTime(2023, 2, 19), (pageResult[0] as JObject)["CustomerSince"].ToObject()); - Assert.Null(content.GetValue("@odata.nextLink")); - } + Assert.Equal(2, pageResult.Count); + Assert.Equal(idAt1, (pageResult[1] as JObject)["Id"].ToObject()); + Assert.Equal(customerSinceAt1, (pageResult[1] as JObject)["CustomerSince"].ToObject()); - [Fact] - public async Task VerifySkipTokenPagingOrderedByNullableDateTimePropertyDescending() - { - HttpClient client = CreateClient(); - HttpRequestMessage request; - HttpResponseMessage response; - JObject content; - JArray pageResult; - - // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) - // is intentional. The next-link in one response is used in the next request - // so we need to control the execution order (unlike Theory attribute where order is random) - var skipTokenTestData = new List<(int, DateTime?)> - { - (6, new DateTime(2023, 2, 4)), - (9, new DateTime(2023, 1, 25)), - (2, new DateTime(2023, 1, 2)), - (3, null) - }; + string nextPageLink = content["@odata.nextLink"].ToObject(); + Assert.NotNull(nextPageLink); + Assert.Contains("/prefix/SkipTokenPagingS3Customers?$orderby=CustomerSince%20desc&" + skipTokenStart, nextPageLink); + Assert.EndsWith(skipTokenEnd, nextPageLink); - string requestUri = "/prefix/SkipTokenPagingS3Customers?$orderby=CustomerSince desc"; + requestUri = nextPageLink; + } - foreach (var testData in skipTokenTestData) - { - int idAt1 = testData.Item1; - DateTime? customerSinceAt1 = testData.Item2; - - // Arrange - request = new HttpRequestMessage(HttpMethod.Get, requestUri); - string skipTokenStart = string.Concat( - "$skiptoken=CustomerSince-", - customerSinceAt1 != null ? customerSinceAt1.Value.ToString("yyyy-MM-dd") : "null"); - string skipTokenEnd = string.Concat(",Id-", idAt1); - - // Act - response = await client.SendAsync(request); - content = await response.Content.ReadAsObject(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - pageResult = content["value"] as JArray; - Assert.NotNull(pageResult); - Assert.Equal(2, pageResult.Count); - Assert.Equal(idAt1, (pageResult[1] as JObject)["Id"].ToObject()); - Assert.Equal(customerSinceAt1, (pageResult[1] as JObject)["CustomerSince"].ToObject()); - - string nextPageLink = content["@odata.nextLink"].ToObject(); - Assert.NotNull(nextPageLink); - Assert.Contains("/prefix/SkipTokenPagingS3Customers?$orderby=CustomerSince%20desc&" + skipTokenStart, nextPageLink); - Assert.EndsWith(skipTokenEnd, nextPageLink); - - requestUri = nextPageLink; - } + // Fetch last page + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + response = await client.SendAsync(request); - // Fetch last page - request = new HttpRequestMessage(HttpMethod.Get, requestUri); - response = await client.SendAsync(request); + content = await response.Content.ReadAsObject(); - content = await response.Content.ReadAsObject(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + pageResult = content["value"] as JArray; + Assert.NotNull(pageResult); + Assert.Single(pageResult); + Assert.Equal(5, (pageResult[0] as JObject)["Id"].ToObject()); + Assert.Null((pageResult[0] as JObject)["CustomerSince"].ToObject()); + Assert.Null(content.GetValue("@odata.nextLink")); + } +} - pageResult = content["value"] as JArray; - Assert.NotNull(pageResult); - Assert.Single(pageResult); - Assert.Equal(5, (pageResult[0] as JObject)["Id"].ToObject()); - Assert.Null((pageResult[0] as JObject)["CustomerSince"].ToObject()); - Assert.Null(content.GetValue("@odata.nextLink")); - } +public class SkipTokenPagingEdgeCaseTests : WebApiTestBase +{ + public SkipTokenPagingEdgeCaseTests(WebApiTestFixture fixture) + : base(fixture) + { } - public class SkipTokenPagingEdgeCaseTests : WebApiTestBase + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) { - public SkipTokenPagingEdgeCaseTests(WebApiTestFixture fixture) - : base(fixture) - { - } + IEdmModel model = GetEdmModel(); + services.ConfigureControllers( + typeof(SkipTokenPagingEdgeCase1CustomersController)); + services.AddControllers().AddOData(opt => opt.Expand().OrderBy().SkipToken().AddRouteComponents("{a}", model)); + } - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) + protected static IEdmModel GetEdmModel() + { + var csdl = "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + // Property is nullable on CLR type + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + + IEdmModel model; + + using (var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(csdl))) + using (var reader = XmlReader.Create(memoryStream)) { - IEdmModel model = GetEdmModel(); - services.ConfigureControllers( - typeof(SkipTokenPagingEdgeCase1CustomersController)); - services.AddControllers().AddOData(opt => opt.Expand().OrderBy().SkipToken().AddRouteComponents("{a}", model)); + model = CsdlReader.Parse(reader); } - protected static IEdmModel GetEdmModel() - { - var csdl = "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + // Property is nullable on CLR type - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - ""; - - IEdmModel model; - - using (var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(csdl))) - using (var reader = XmlReader.Create(memoryStream)) - { - model = CsdlReader.Parse(reader); - } - - return model; - } + return model; + } - [Fact] - public async Task VerifySkipTokenPagingForPropertyNullableOnClrTypeButNotNullableOnEdmType() + [Fact] + public async Task VerifySkipTokenPagingForPropertyNullableOnClrTypeButNotNullableOnEdmType() + { + HttpClient client = CreateClient(); + HttpRequestMessage request; + HttpResponseMessage response; + JObject content; + JArray pageResult; + + // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) + // is intentional. The next-link in one response is used in the next request + // so we need to control the execution order (unlike Theory attribute where order is random) + var skipTokenTestData = new List<(int, decimal?, int, decimal?)> { - HttpClient client = CreateClient(); - HttpRequestMessage request; - HttpResponseMessage response; - JObject content; - JArray pageResult; - - // NOTE: Using a loop in this test (as opposed to parameterized tests using xunit Theory attribute) - // is intentional. The next-link in one response is used in the next request - // so we need to control the execution order (unlike Theory attribute where order is random) - var skipTokenTestData = new List<(int, decimal?, int, decimal?)> - { - (2, 2, 7, 5), - (9, 25, 4, 30), - }; + (2, 2, 7, 5), + (9, 25, 4, 30), + }; - string requestUri = "/prefix/SkipTokenPagingEdgeCase1Customers?$orderby=CreditLimit"; + string requestUri = "/prefix/SkipTokenPagingEdgeCase1Customers?$orderby=CreditLimit"; - foreach (var testData in skipTokenTestData) - { - int idAt0 = testData.Item1; - decimal? creditLimitAt0 = testData.Item2; - int idAt1 = testData.Item3; - decimal? creditLimitAt1 = testData.Item4; - - // Arrange - request = new HttpRequestMessage(HttpMethod.Get, requestUri); - string skipToken = string.Concat("$skiptoken=CreditLimit-", creditLimitAt1 != null ? creditLimitAt1.ToString() : "null", ",Id-", idAt1); - - // Act - response = await client.SendAsync(request); - content = await response.Content.ReadAsObject(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - pageResult = content["value"] as JArray; - Assert.NotNull(pageResult); - Assert.Equal(2, pageResult.Count); - Assert.Equal(idAt0, (pageResult[0] as JObject)["Id"].ToObject()); - Assert.Equal(creditLimitAt0, (pageResult[0] as JObject)["CreditLimit"].ToObject()); - Assert.Equal(idAt1, (pageResult[1] as JObject)["Id"].ToObject()); - Assert.Equal(creditLimitAt1, (pageResult[1] as JObject)["CreditLimit"].ToObject()); - - string nextPageLink = content["@odata.nextLink"].ToObject(); - Assert.NotNull(nextPageLink); - Assert.EndsWith("/prefix/SkipTokenPagingEdgeCase1Customers?$orderby=CreditLimit&" + skipToken, nextPageLink); - - requestUri = nextPageLink; - } + foreach (var testData in skipTokenTestData) + { + int idAt0 = testData.Item1; + decimal? creditLimitAt0 = testData.Item2; + int idAt1 = testData.Item3; + decimal? creditLimitAt1 = testData.Item4; - // Fetch last page + // Arrange request = new HttpRequestMessage(HttpMethod.Get, requestUri); - response = await client.SendAsync(request); + string skipToken = string.Concat("$skiptoken=CreditLimit-", creditLimitAt1 != null ? creditLimitAt1.ToString() : "null", ",Id-", idAt1); + // Act + response = await client.SendAsync(request); content = await response.Content.ReadAsObject(); // Assert @@ -1090,10 +1066,33 @@ public async Task VerifySkipTokenPagingForPropertyNullableOnClrTypeButNotNullabl pageResult = content["value"] as JArray; Assert.NotNull(pageResult); - Assert.Single(pageResult); - Assert.Equal(6, (pageResult[0] as JObject)["Id"].ToObject()); - Assert.Equal(35, (pageResult[0] as JObject)["CreditLimit"].ToObject()); - Assert.Null(content.GetValue("@odata.nextLink")); + Assert.Equal(2, pageResult.Count); + Assert.Equal(idAt0, (pageResult[0] as JObject)["Id"].ToObject()); + Assert.Equal(creditLimitAt0, (pageResult[0] as JObject)["CreditLimit"].ToObject()); + Assert.Equal(idAt1, (pageResult[1] as JObject)["Id"].ToObject()); + Assert.Equal(creditLimitAt1, (pageResult[1] as JObject)["CreditLimit"].ToObject()); + + string nextPageLink = content["@odata.nextLink"].ToObject(); + Assert.NotNull(nextPageLink); + Assert.EndsWith("/prefix/SkipTokenPagingEdgeCase1Customers?$orderby=CreditLimit&" + skipToken, nextPageLink); + + requestUri = nextPageLink; } + + // Fetch last page + request = new HttpRequestMessage(HttpMethod.Get, requestUri); + response = await client.SendAsync(request); + + content = await response.Content.ReadAsObject(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + pageResult = content["value"] as JArray; + Assert.NotNull(pageResult); + Assert.Single(pageResult); + Assert.Equal(6, (pageResult[0] as JObject)["Id"].ToObject()); + Assert.Equal(35, (pageResult[0] as JObject)["CreditLimit"].ToObject()); + Assert.Null(content.GetValue("@odata.nextLink")); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultController.cs index 31545ede5..b664b89c9 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultController.cs @@ -11,49 +11,48 @@ using Microsoft.AspNetCore.OData.Results; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.SingleResultTest +namespace Microsoft.AspNetCore.OData.E2E.Tests.SingleResultTest; + +public class CustomersController : ODataController { - public class CustomersController : ODataController - { - private readonly SingleResultContext _db = new SingleResultContext(); + private readonly SingleResultContext _db = new SingleResultContext(); - [EnableQuery] - public SingleResult Get(int key) - { - ResetDataSource(); - var db = new SingleResultContext(); - return SingleResult.Create(db.Customers.Where(c => c.Id == key)); - } + [EnableQuery] + public SingleResult Get(int key) + { + ResetDataSource(); + var db = new SingleResultContext(); + return SingleResult.Create(db.Customers.Where(c => c.Id == key)); + } - public void Generate() + public void Generate() + { + for (int i = 1; i < 10; i++) { - for (int i = 1; i < 10; i++) + var customer = new Customer { - var customer = new Customer + Name = $"name_{i}", + Orders = new List { - Name = $"name_{i}", - Orders = new List + new Order { - new Order - { - Title = $"title_{i}", - } + Title = $"title_{i}", } - }; - - _db.Customers.Add(customer); - } + } + }; - _db.SaveChanges(); + _db.Customers.Add(customer); } - private void ResetDataSource() + _db.SaveChanges(); + } + + private void ResetDataSource() + { + _db.Database.EnsureCreated(); + if (!_db.Customers.Any()) { - _db.Database.EnsureCreated(); - if (!_db.Customers.Any()) - { - Generate(); - } + Generate(); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultDataModel.cs index 22ad91c42..02945f2e7 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultDataModel.cs @@ -8,32 +8,31 @@ using System.Collections.Generic; using Microsoft.EntityFrameworkCore; -namespace Microsoft.AspNetCore.OData.E2E.Tests.SingleResultTest +namespace Microsoft.AspNetCore.OData.E2E.Tests.SingleResultTest; + +public class SingleResultContext : DbContext { - public class SingleResultContext : DbContext - { - public DbSet Customers { get; set; } + public DbSet Customers { get; set; } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseSqlServer(@"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=SingleResultTest2"); - base.OnConfiguring(optionsBuilder); - } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(@"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=SingleResultTest2"); + base.OnConfiguring(optionsBuilder); } +} - public class Customer - { - public int Id { get; set; } +public class Customer +{ + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public List Orders { get; set; } - } + public List Orders { get; set; } +} - public class Order - { - public int Id { get; set; } +public class Order +{ + public int Id { get; set; } - public string Title { get; set; } - } + public string Title { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultEdmModel.cs index 283c1ff34..5508669d0 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultEdmModel.cs @@ -8,16 +8,15 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.SingleResultTest +namespace Microsoft.AspNetCore.OData.E2E.Tests.SingleResultTest; + +public class SingleResultEdmModel { - public class SingleResultEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Orders"); - return builder.GetEdmModel(); - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultTests.cs index 1f1ab5051..05b1cac2e 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/SingleResult/SingleResultTests.cs @@ -13,55 +13,54 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.SingleResultTest +namespace Microsoft.AspNetCore.OData.E2E.Tests.SingleResultTest; + +public class SingleResultTests : WebApiTestBase { - public class SingleResultTests : WebApiTestBase + public SingleResultTests(WebApiTestFixture fixture) + :base(fixture) { - public SingleResultTests(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(CustomersController)); + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(CustomersController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("singleresult", SingleResultEdmModel.GetEdmModel()) - .Count().Filter().OrderBy().Expand().SetMaxTop(null)); - } + services.AddControllers().AddOData(opt => opt.AddRouteComponents("singleresult", SingleResultEdmModel.GetEdmModel()) + .Count().Filter().OrderBy().Expand().SetMaxTop(null)); + } - [Fact] - public async Task SingleResultReturnsCorrentResult() - { - // Arrange - string queryUrl = "singleresult/Customers(8)"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); + [Fact] + public async Task SingleResultReturnsCorrentResult() + { + // Arrange + string queryUrl = "singleresult/Customers(8)"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpResponseMessage response = await client.SendAsync(request); - // Assert - response.EnsureSuccessStatusCode(); - string payload = await response.Content.ReadAsStringAsync(); - Assert.Equal("{\"Id\":8,\"Name\":\"name_8\"}", payload); - } + // Assert + response.EnsureSuccessStatusCode(); + string payload = await response.Content.ReadAsStringAsync(); + Assert.Equal("{\"Id\":8,\"Name\":\"name_8\"}", payload); + } - [Fact] - public async Task EmptySingleResultReturnsNotFound() - { - // Arrange - string queryUrl = "singleresult/Customers(100)"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); + [Fact] + public async Task EmptySingleResultReturnsNotFound() + { + // Arrange + string queryUrl = "singleresult/Customers(100)"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/MonstersIncController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/MonstersIncController.cs index e033540cd..0cfbcb491 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/MonstersIncController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/MonstersIncController.cs @@ -16,230 +16,229 @@ using Microsoft.AspNetCore.OData.Routing.Attributes; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Singleton +namespace Microsoft.AspNetCore.OData.E2E.Tests.Singleton; + +/// +/// Present a singleton named "MonstersInc" +/// Use attribute routing +/// +[Route("odata/MonstersInc")] +public class MonstersIncController : ODataController { - /// - /// Present a singleton named "MonstersInc" - /// Use attribute routing - /// - [Route("odata/MonstersInc")] - public class MonstersIncController : ODataController - { - public static Company MonstersInc; + public static Company MonstersInc; - static MonstersIncController() - { - InitData(); - } + static MonstersIncController() + { + InitData(); + } - private static void InitData() - { - MonstersInc = new Company() + private static void InitData() + { + MonstersInc = new Company() + { + ID = 1, + Name = "MonstersInc", + Revenue = 1000, + Category = CompanyCategory.Electronics, + Partners = new List(), + Branches = new List() { new Office { City = "Shanghai", Address = "Minhang" }, new Office { City = "Xi'an", Address = "Dayanta" } }, + Projects = new List() { - ID = 1, - Name = "MonstersInc", - Revenue = 1000, - Category = CompanyCategory.Electronics, - Partners = new List(), - Branches = new List() { new Office { City = "Shanghai", Address = "Minhang" }, new Office { City = "Xi'an", Address = "Dayanta" } }, - Projects = new List() + new Project { - new Project - { - Id = 1, - Title = "In Closet Scare", - ProjectDetails = new List() - { - new ProjectDetail { Id = 1, Comment = "The original scare" }, - new ProjectDetail { Id = 2, Comment = "Leaving the door open is the worst mistake any employee can make" }, - new ProjectDetail { Id = 3, Comment = "Leaving the door open could let it not only a draft, but a child" }, - new ProjectDetail { Id = 4, Comment = "Has led to the intrusion of a young girl, Boo" } - }, - }, - new Project + Id = 1, + Title = "In Closet Scare", + ProjectDetails = new List() { - Id = 2, - Title = "Under Bed Scare", - ProjectDetails = new List() { - new ProjectDetail { Id = 5, Comment = "Tried and true" }, - new ProjectDetail { Id = 6, Comment = "Tip: grab a foot"} - }, + new ProjectDetail { Id = 1, Comment = "The original scare" }, + new ProjectDetail { Id = 2, Comment = "Leaving the door open is the worst mistake any employee can make" }, + new ProjectDetail { Id = 3, Comment = "Leaving the door open could let it not only a draft, but a child" }, + new ProjectDetail { Id = 4, Comment = "Has led to the intrusion of a young girl, Boo" } }, - new Project - { - Id = 3, - Title = "Midnight Snack in Kitchen Scare", - ProjectDetails= new List(), + }, + new Project + { + Id = 2, + Title = "Under Bed Scare", + ProjectDetails = new List() { + new ProjectDetail { Id = 5, Comment = "Tried and true" }, + new ProjectDetail { Id = 6, Comment = "Tip: grab a foot"} }, }, - }; - } - - #region Query - [EnableQuery] - [HttpGet("")] - public IActionResult QueryCompany() - { - return Ok(MonstersInc); - } - - [HttpGet("Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany")] - public IActionResult QueryCompanyFromDerivedType() - { - var subCompany = MonstersInc as SubCompany; - if (subCompany != null) - { - return Ok(subCompany); - } - return BadRequest("The target cannot be casted"); - } + new Project + { + Id = 3, + Title = "Midnight Snack in Kitchen Scare", + ProjectDetails= new List(), + }, + }, + }; + } - [HttpGet("Revenue")] - public IActionResult GetCompanyRevenue() - { - return Ok(MonstersInc.Revenue); - } + #region Query + [EnableQuery] + [HttpGet("")] + public IActionResult QueryCompany() + { + return Ok(MonstersInc); + } - [HttpGet("Projects")] - [EnableQuery(PageSize = 2)] - public IActionResult GetProjects() + [HttpGet("Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany")] + public IActionResult QueryCompanyFromDerivedType() + { + var subCompany = MonstersInc as SubCompany; + if (subCompany != null) { - return Ok(MonstersInc.Projects); + return Ok(subCompany); } + return BadRequest("The target cannot be casted"); + } - [HttpGet("Projects/{key}/ProjectDetails")] - [EnableQuery] - public IActionResult GetProjectDetails(int key) - { - var project = MonstersInc.Projects.FirstOrDefault(a => a.Id == key); - if (project == null) - { - return NotFound($"Project with given key {key} does not exist."); - } + [HttpGet("Revenue")] + public IActionResult GetCompanyRevenue() + { + return Ok(MonstersInc.Revenue); + } - return Ok(project.ProjectDetails); - } + [HttpGet("Projects")] + [EnableQuery(PageSize = 2)] + public IActionResult GetProjects() + { + return Ok(MonstersInc.Projects); + } - [HttpGet("Branches/$count")] - public IActionResult GetBranchesCount(ODataQueryOptions options) + [HttpGet("Projects/{key}/ProjectDetails")] + [EnableQuery] + public IActionResult GetProjectDetails(int key) + { + var project = MonstersInc.Projects.FirstOrDefault(a => a.Id == key); + if (project == null) { - IQueryable eligibleBranches = MonstersInc.Branches.AsQueryable(); - if (options.Filter != null) - { - eligibleBranches = options.Filter.ApplyTo(eligibleBranches, new ODataQuerySettings()).Cast(); - } - return Ok(eligibleBranches.Count()); + return NotFound($"Project with given key {key} does not exist."); } - [HttpGet("Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany/Location")] - public IActionResult GetDerivedTypeProperty() - { - var subCompany = MonstersInc as SubCompany; - if (subCompany != null) - { - return Ok(subCompany.Location); - } - return BadRequest("The target cannot be casted"); - } + return Ok(project.ProjectDetails); + } - [HttpGet("Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany/Office")] - public IActionResult QueryDerivedTypeComplexProperty() + [HttpGet("Branches/$count")] + public IActionResult GetBranchesCount(ODataQueryOptions options) + { + IQueryable eligibleBranches = MonstersInc.Branches.AsQueryable(); + if (options.Filter != null) { - var subCompany = MonstersInc as SubCompany; - if (subCompany != null) - { - return Ok(subCompany.Office); - } - return BadRequest("The target cannot be casted"); + eligibleBranches = options.Filter.ApplyTo(eligibleBranches, new ODataQuerySettings()).Cast(); } + return Ok(eligibleBranches.Count()); + } - [HttpGet("Partners")] - public IActionResult QueryNavigationProperty() + [HttpGet("Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany/Location")] + public IActionResult GetDerivedTypeProperty() + { + var subCompany = MonstersInc as SubCompany; + if (subCompany != null) { - return Ok(MonstersInc.Partners); + return Ok(subCompany.Location); } - #endregion + return BadRequest("The target cannot be casted"); + } - #region Update - [HttpPut("")] - public IActionResult UpdateCompanyByPut([FromBody] Company newCompany) + [HttpGet("Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany/Office")] + public IActionResult QueryDerivedTypeComplexProperty() + { + var subCompany = MonstersInc as SubCompany; + if (subCompany != null) { - MonstersInc = newCompany; - return StatusCode(StatusCodes.Status204NoContent); + return Ok(subCompany.Office); } + return BadRequest("The target cannot be casted"); + } - [HttpPut("Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany")] - public IActionResult UpdateCompanyByPutWithDerivedTypeObject([FromBody] SubCompany newCompany) - { - MonstersInc = newCompany; - return StatusCode(StatusCodes.Status204NoContent); - } + [HttpGet("Partners")] + public IActionResult QueryNavigationProperty() + { + return Ok(MonstersInc.Partners); + } + #endregion - [HttpPatch("")] - public IActionResult UpdateCompanyByPatch([FromBody]Delta item) - { - item.Patch(MonstersInc); - return StatusCode(StatusCodes.Status204NoContent); - } - #endregion + #region Update + [HttpPut("")] + public IActionResult UpdateCompanyByPut([FromBody] Company newCompany) + { + MonstersInc = newCompany; + return StatusCode(StatusCodes.Status204NoContent); + } - #region Navigation link - [HttpPost("Partners/$ref")] - public IActionResult AddOrUpdateNavigationLink([FromBody] Uri link) - { - int relatedKey = Request.GetKeyValue(link); - Partner partner = PartnersController.Partners.First(x => x.ID == relatedKey); - MonstersInc.Partners.Add(partner); + [HttpPut("Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany")] + public IActionResult UpdateCompanyByPutWithDerivedTypeObject([FromBody] SubCompany newCompany) + { + MonstersInc = newCompany; + return StatusCode(StatusCodes.Status204NoContent); + } - return StatusCode(StatusCodes.Status204NoContent); - } + [HttpPatch("")] + public IActionResult UpdateCompanyByPatch([FromBody]Delta item) + { + item.Patch(MonstersInc); + return StatusCode(StatusCodes.Status204NoContent); + } + #endregion - [HttpDelete("Partners({relatedKey})/$ref")] - public IActionResult DeleteNavigationLink(string relatedKey) - { - int key = int.Parse(relatedKey); - Partner partner = MonstersInc.Partners.First(x => x.ID == key); + #region Navigation link + [HttpPost("Partners/$ref")] + public IActionResult AddOrUpdateNavigationLink([FromBody] Uri link) + { + int relatedKey = Request.GetKeyValue(link); + Partner partner = PartnersController.Partners.First(x => x.ID == relatedKey); + MonstersInc.Partners.Add(partner); - MonstersInc.Partners.Remove(partner); - return StatusCode(StatusCodes.Status204NoContent); - } + return StatusCode(StatusCodes.Status204NoContent); + } - [HttpGet("Partners/$ref")] - public IActionResult GetNavigationLink() - { - return Ok(); - } + [HttpDelete("Partners({relatedKey})/$ref")] + public IActionResult DeleteNavigationLink(string relatedKey) + { + int key = int.Parse(relatedKey); + Partner partner = MonstersInc.Partners.First(x => x.ID == key); - [HttpPost("Partners")] - public IActionResult AddPartnersToCompany([FromBody] Partner partner) - { - PartnersController.Partners.Add(partner); - if (MonstersInc.Partners == null) - { - MonstersInc.Partners = new List() { partner }; - } - else - { - MonstersInc.Partners.Add(partner); - } + MonstersInc.Partners.Remove(partner); + return StatusCode(StatusCodes.Status204NoContent); + } - return Created(partner); - } - #endregion + [HttpGet("Partners/$ref")] + public IActionResult GetNavigationLink() + { + return Ok(); + } - #region Action and function - [HttpPost("Microsoft.Test.E2E.AspNet.OData.Singleton.ResetDataSource")] - public IActionResult CallActionResetDataSource() + [HttpPost("Partners")] + public IActionResult AddPartnersToCompany([FromBody] Partner partner) + { + PartnersController.Partners.Add(partner); + if (MonstersInc.Partners == null) { - InitData(); - return StatusCode(StatusCodes.Status204NoContent); + MonstersInc.Partners = new List() { partner }; } - - [HttpGet("Microsoft.Test.E2E.AspNet.OData.Singleton.GetPartnersCount()")] - public IActionResult CallFunctionGetPartnersCount() + else { - return Ok(MonstersInc.Partners.Count); + MonstersInc.Partners.Add(partner); } - #endregion + + return Created(partner); + } + #endregion + + #region Action and function + [HttpPost("Microsoft.Test.E2E.AspNet.OData.Singleton.ResetDataSource")] + public IActionResult CallActionResetDataSource() + { + InitData(); + return StatusCode(StatusCodes.Status204NoContent); + } + + [HttpGet("Microsoft.Test.E2E.AspNet.OData.Singleton.GetPartnersCount()")] + public IActionResult CallFunctionGetPartnersCount() + { + return Ok(MonstersInc.Partners.Count); } + #endregion } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/PartnersController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/PartnersController.cs index 024d294a4..b754ab74d 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/PartnersController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/PartnersController.cs @@ -16,149 +16,148 @@ using Microsoft.AspNetCore.OData.Routing.Attributes; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Singleton +namespace Microsoft.AspNetCore.OData.E2E.Tests.Singleton; + +public class PartnersController : ODataController { - public class PartnersController : ODataController + public static List Partners; + + static PartnersController() { - public static List Partners; + InitData(); + } - static PartnersController() - { - InitData(); - } + private static void InitData() + { + Partners = Enumerable.Range(0, 10).Select(i => + new Partner() + { + ID = i, + Name = string.Format("Name {0}", i) + }).ToList(); + } - private static void InitData() - { - Partners = Enumerable.Range(0, 10).Select(i => - new Partner() - { - ID = i, - Name = string.Format("Name {0}", i) - }).ToList(); - } + #region Query + [EnableQuery] + public IActionResult Get() + { + return Ok(Partners.AsQueryable()); + } - #region Query - [EnableQuery] - public IActionResult Get() - { - return Ok(Partners.AsQueryable()); - } + [EnableQuery] + public IActionResult GetPartners() + { + return Ok(Partners.AsQueryable()); + } - [EnableQuery] - public IActionResult GetPartners() + [EnableQuery] + public IActionResult Get(int key) + { + return Ok(Partners.SingleOrDefault(p=>p.ID == key)); + } + + public IActionResult GetCompanyFromPartner([FromODataUri] int key) + { + var company = Partners.First(e => e.ID == key).Company; + if (company == null) { - return Ok(Partners.AsQueryable()); + return StatusCode(StatusCodes.Status204NoContent); } + return Ok(company); + } + #endregion + + #region Update + public IActionResult POST([FromBody] Partner partner) + { + Partners.Add(partner); + return Created(partner); + } + #endregion - [EnableQuery] - public IActionResult Get(int key) + #region Navigation link + [AcceptVerbs("PUT")] + public IActionResult CreateRef([FromODataUri] int key, string navigationProperty, [FromBody] Uri link) + { + if (navigationProperty != "Company") { - return Ok(Partners.SingleOrDefault(p=>p.ID == key)); + return BadRequest(); } - public IActionResult GetCompanyFromPartner([FromODataUri] int key) + var strArray = link.AbsoluteUri.Split('/'); + var company = strArray[strArray.Length - 1]; + + if (company == "Umbrella") { - var company = Partners.First(e => e.ID == key).Company; - if (company == null) - { - return StatusCode(StatusCodes.Status204NoContent); - } - return Ok(company); + Partners.First(e => e.ID == key).Company = UmbrellaController.Umbrella; } - #endregion - - #region Update - public IActionResult POST([FromBody] Partner partner) + else if (company == "MonstersInc") { - Partners.Add(partner); - return Created(partner); + Partners.First(e => e.ID == key).Company = MonstersIncController.MonstersInc; } - #endregion + else + return BadRequest(); - #region Navigation link - [AcceptVerbs("PUT")] - public IActionResult CreateRef([FromODataUri] int key, string navigationProperty, [FromBody] Uri link) - { - if (navigationProperty != "Company") - { - return BadRequest(); - } - - var strArray = link.AbsoluteUri.Split('/'); - var company = strArray[strArray.Length - 1]; - - if (company == "Umbrella") - { - Partners.First(e => e.ID == key).Company = UmbrellaController.Umbrella; - } - else if (company == "MonstersInc") - { - Partners.First(e => e.ID == key).Company = MonstersIncController.MonstersInc; - } - else - return BadRequest(); + return StatusCode(StatusCodes.Status204NoContent); + } - return StatusCode(StatusCodes.Status204NoContent); + public IActionResult DeleteRef([FromODataUri] int key, string navigationProperty) + { + if (navigationProperty != "Company") + { + return BadRequest(); } - public IActionResult DeleteRef([FromODataUri] int key, string navigationProperty) - { - if (navigationProperty != "Company") - { - return BadRequest(); - } + Partners.First(e => e.ID == key).Company = null; + return StatusCode(StatusCodes.Status204NoContent); + } - Partners.First(e => e.ID == key).Company = null; - return StatusCode(StatusCodes.Status204NoContent); + [HttpPut("Partners({key})/Company")] + public IActionResult PutToCompany(int key, [FromBody]Company company) + { + var navigateCompany = Partners.First(e => e.ID == key).Company; + Partners.First(e => e.ID == key).Company = company; + if (navigateCompany.Name == "Umbrella") + { + UmbrellaController.Umbrella = navigateCompany; } - - [HttpPut("Partners({key})/Company")] - public IActionResult PutToCompany(int key, [FromBody]Company company) + else { - var navigateCompany = Partners.First(e => e.ID == key).Company; - Partners.First(e => e.ID == key).Company = company; - if (navigateCompany.Name == "Umbrella") - { - UmbrellaController.Umbrella = navigateCompany; - } - else - { - MonstersIncController.MonstersInc = navigateCompany; - } - return StatusCode(StatusCodes.Status204NoContent); + MonstersIncController.MonstersInc = navigateCompany; } + return StatusCode(StatusCodes.Status204NoContent); + } - [HttpPatch("Partners({key})/Company")] - public IActionResult PatchToCompany(int key, Delta company) + [HttpPatch("Partners({key})/Company")] + public IActionResult PatchToCompany(int key, Delta company) + { + var navigateCompany = Partners.First(e => e.ID == key).Company; + company.Patch(Partners.First(e => e.ID == key).Company); + if (navigateCompany.Name == "Umbrella") { - var navigateCompany = Partners.First(e => e.ID == key).Company; - company.Patch(Partners.First(e => e.ID == key).Company); - if (navigateCompany.Name == "Umbrella") - { - company.Patch(UmbrellaController.Umbrella); - } - else - { - company.Patch(MonstersIncController.MonstersInc); - } - return StatusCode(StatusCodes.Status204NoContent); + company.Patch(UmbrellaController.Umbrella); } - - [HttpPost] - public IActionResult PostToCompany(int key, [FromBody] Company company) + else { - return Ok(); + company.Patch(MonstersIncController.MonstersInc); } + return StatusCode(StatusCodes.Status204NoContent); + } + + [HttpPost] + public IActionResult PostToCompany(int key, [FromBody] Company company) + { + return Ok(); + } - #endregion + #endregion - #region Action - [HttpPost] - public IActionResult ResetDataSourceOnCollectionOfPartner() - { - InitData(); - return StatusCode(StatusCodes.Status204NoContent); - } - #endregion + #region Action + [HttpPost] + public IActionResult ResetDataSourceOnCollectionOfPartner() + { + InitData(); + return StatusCode(StatusCodes.Status204NoContent); } + #endregion } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/SingletonDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/SingletonDataModel.cs index 05ab7e1d9..7e14b9296 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/SingletonDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/SingletonDataModel.cs @@ -11,90 +11,89 @@ using System.Linq; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Singleton +namespace Microsoft.AspNetCore.OData.E2E.Tests.Singleton; + +/// +/// Present the EntityType "Partner" +/// +public class Partner { - /// - /// Present the EntityType "Partner" - /// - public class Partner - { - public int ID { get; set; } - public string Name { get; set; } + public int ID { get; set; } + public string Name { get; set; } - [Singleton] - public Company Company { get; set;} - } + [Singleton] + public Company Company { get; set;} +} - /// - /// Present company category, which is an enum type - /// - public enum CompanyCategory - { - IT = 0, - Communication = 1, - Electronics = 2, - Others = 3 - } +/// +/// Present company category, which is an enum type +/// +public enum CompanyCategory +{ + IT = 0, + Communication = 1, + Electronics = 2, + Others = 3 +} - /// - /// Present the EntityType "Company" - /// - public class Company - { - public int ID { get; set; } +/// +/// Present the EntityType "Company" +/// +public class Company +{ + public int ID { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public long Revenue { get; set; } - public CompanyCategory Category { get; set; } + public long Revenue { get; set; } + public CompanyCategory Category { get; set; } - [NotCountable] - public IList Partners { get; set; } - public IList Branches { get; set; } + [NotCountable] + public IList Partners { get; set; } + public IList Branches { get; set; } - [Contained] - [AutoExpand] - public IList Projects { get; set; } - } + [Contained] + [AutoExpand] + public IList Projects { get; set; } +} - /// - /// Present a complex type - /// - public class Office - { - public string City { get; set; } - public string Address { get; set; } - } +/// +/// Present a complex type +/// +public class Office +{ + public string City { get; set; } + public string Address { get; set; } +} - /// - /// Present a contained navigation property - /// - public class Project - { - public int Id { get; set; } - public string Title { get; set; } +/// +/// Present a contained navigation property +/// +public class Project +{ + public int Id { get; set; } + public string Title { get; set; } - [AutoExpand] - [Contained] - public IList ProjectDetails { get; set; } - } + [AutoExpand] + [Contained] + public IList ProjectDetails { get; set; } +} - /// - /// Present a nested contained navigation property - /// - public class ProjectDetail - { - public int Id { get; set; } - public string Comment { get; set; } - } +/// +/// Present a nested contained navigation property +/// +public class ProjectDetail +{ + public int Id { get; set; } + public string Comment { get; set; } +} - /// - /// EntityType derives from "Company" - /// - public class SubCompany : Company - { - public string Location { get; set; } - public string Description { get; set; } - public Office Office { get; set; } - } -} \ No newline at end of file +/// +/// EntityType derives from "Company" +/// +public class SubCompany : Company +{ + public string Location { get; set; } + public string Description { get; set; } + public Office Office { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/SingletonEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/SingletonEdmModel.cs index 83859e030..fa2363527 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/SingletonEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/SingletonEdmModel.cs @@ -8,109 +8,108 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Singleton +namespace Microsoft.AspNetCore.OData.E2E.Tests.Singleton; + +internal class SingletonEdmModel { - internal class SingletonEdmModel + public static IEdmModel GetExplicitModel(string singletonName) + { + ODataModelBuilder builder = new ODataModelBuilder(); + + // Define EntityType of Partner + var partner = builder.EntityType(); + partner.HasKey(p => p.ID); + partner.Property(p => p.Name); + var partnerCompany = partner.HasRequired(p => p.Company); + + // Define Enum Type + var category = builder.EnumType(); + category.Member(CompanyCategory.IT); + category.Member(CompanyCategory.Communication); + category.Member(CompanyCategory.Electronics); + category.Member(CompanyCategory.Others); + + // Define EntityType of Company + var company = builder.EntityType(); + company.HasKey(p => p.ID); + company.Property(p => p.Name); + company.Property(p => p.Revenue); + company.EnumProperty(p => p.Category); + var companyPartners = company.HasMany(p => p.Partners); + companyPartners.IsNotCountable(); + + var companyBranches = company.CollectionProperty(p => p.Branches); + + // Define Complex Type: Office + var office = builder.ComplexType(); + office.Property(p => p.City); + office.Property(p => p.Address); + + // Define Derived Type: SubCompany + var subCompany = builder.EntityType(); + subCompany.DerivesFrom(); + subCompany.Property(p => p.Location); + subCompany.Property(p => p.Description); + subCompany.ComplexProperty(p => p.Office); + + builder.Namespace = typeof(Partner).Namespace; + + // Define PartnerSet and Company(singleton) + EntitySetConfiguration partnersConfiguration = builder.EntitySet("Partners"); + //partnersConfiguration.HasIdLink(c=>c.GenerateSelfLink(false), true); + //partnersConfiguration.HasSingletonBinding(c => c.Company, singletonName); + //Func, IEdmNavigationProperty, Uri> link = (eic, np) => eic.GenerateNavigationPropertyLink(np, false); + //partnersConfiguration.HasNavigationPropertyLink(partnerCompany, link, true); + partnersConfiguration.EntityType.Collection.Action("ResetDataSource"); + + SingletonConfiguration companyConfiguration = builder.Singleton("Umbrella"); + //companyConfiguration.HasIdLink(c => c.GenerateSelfLink(false), true); + //companyConfiguration.HasManyBinding(c => c.Partners, "Partners"); + //Func, IEdmNavigationProperty, Uri> linkFactory = (eic, np) => eic.GenerateNavigationPropertyLink(np, false); + //companyConfiguration.HasNavigationPropertyLink(companyPartners, linkFactory, true); + companyConfiguration.EntityType.Action("ResetDataSource"); + companyConfiguration.EntityType.Function("GetPartnersCount").Returns(); + + SingletonConfiguration monstersIncConfiguration = builder.Singleton("MonstersInc"); + monstersIncConfiguration.EntityType.Function("GetPartnersCount").Returns(); + + return builder.GetEdmModel(); + } + + public static IEdmModel GetConventionModel(string singletonName) + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + EntitySetConfiguration partnersConfiguration = builder.EntitySet("Partners"); + EntityTypeConfiguration partnerConfiguration = partnersConfiguration.EntityType; + partnerConfiguration.Collection.Action("ResetDataSource"); + + SingletonConfiguration companyConfiguration = builder.Singleton(singletonName); + companyConfiguration.EntityType.Action("ResetDataSource"); + companyConfiguration.EntityType.Function("GetPartnersCount").Returns(); + + builder.Namespace = typeof(Company).Namespace; + + return builder.GetEdmModel(); + } + + public static IEdmModel GetEdmModel() { - public static IEdmModel GetExplicitModel(string singletonName) - { - ODataModelBuilder builder = new ODataModelBuilder(); - - // Define EntityType of Partner - var partner = builder.EntityType(); - partner.HasKey(p => p.ID); - partner.Property(p => p.Name); - var partnerCompany = partner.HasRequired(p => p.Company); - - // Define Enum Type - var category = builder.EnumType(); - category.Member(CompanyCategory.IT); - category.Member(CompanyCategory.Communication); - category.Member(CompanyCategory.Electronics); - category.Member(CompanyCategory.Others); - - // Define EntityType of Company - var company = builder.EntityType(); - company.HasKey(p => p.ID); - company.Property(p => p.Name); - company.Property(p => p.Revenue); - company.EnumProperty(p => p.Category); - var companyPartners = company.HasMany(p => p.Partners); - companyPartners.IsNotCountable(); - - var companyBranches = company.CollectionProperty(p => p.Branches); - - // Define Complex Type: Office - var office = builder.ComplexType(); - office.Property(p => p.City); - office.Property(p => p.Address); - - // Define Derived Type: SubCompany - var subCompany = builder.EntityType(); - subCompany.DerivesFrom(); - subCompany.Property(p => p.Location); - subCompany.Property(p => p.Description); - subCompany.ComplexProperty(p => p.Office); - - builder.Namespace = typeof(Partner).Namespace; - - // Define PartnerSet and Company(singleton) - EntitySetConfiguration partnersConfiguration = builder.EntitySet("Partners"); - //partnersConfiguration.HasIdLink(c=>c.GenerateSelfLink(false), true); - //partnersConfiguration.HasSingletonBinding(c => c.Company, singletonName); - //Func, IEdmNavigationProperty, Uri> link = (eic, np) => eic.GenerateNavigationPropertyLink(np, false); - //partnersConfiguration.HasNavigationPropertyLink(partnerCompany, link, true); - partnersConfiguration.EntityType.Collection.Action("ResetDataSource"); - - SingletonConfiguration companyConfiguration = builder.Singleton("Umbrella"); - //companyConfiguration.HasIdLink(c => c.GenerateSelfLink(false), true); - //companyConfiguration.HasManyBinding(c => c.Partners, "Partners"); - //Func, IEdmNavigationProperty, Uri> linkFactory = (eic, np) => eic.GenerateNavigationPropertyLink(np, false); - //companyConfiguration.HasNavigationPropertyLink(companyPartners, linkFactory, true); - companyConfiguration.EntityType.Action("ResetDataSource"); - companyConfiguration.EntityType.Function("GetPartnersCount").Returns(); - - SingletonConfiguration monstersIncConfiguration = builder.Singleton("MonstersInc"); - monstersIncConfiguration.EntityType.Function("GetPartnersCount").Returns(); - - return builder.GetEdmModel(); - } - - public static IEdmModel GetConventionModel(string singletonName) - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - EntitySetConfiguration partnersConfiguration = builder.EntitySet("Partners"); - EntityTypeConfiguration partnerConfiguration = partnersConfiguration.EntityType; - partnerConfiguration.Collection.Action("ResetDataSource"); - - SingletonConfiguration companyConfiguration = builder.Singleton(singletonName); - companyConfiguration.EntityType.Action("ResetDataSource"); - companyConfiguration.EntityType.Function("GetPartnersCount").Returns(); - - builder.Namespace = typeof(Company).Namespace; - - return builder.GetEdmModel(); - } - - public static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - EntitySetConfiguration partnersConfiguration = builder.EntitySet("Partners"); - EntityTypeConfiguration partnerConfiguration = partnersConfiguration.EntityType; - partnerConfiguration.Collection.Action("ResetDataSource"); - - // Singleton "Umbrella" - SingletonConfiguration umbrellaConfiguration = builder.Singleton("Umbrella"); - //umbrellaConfiguration.EntityType.Action("ResetDataSource"); - umbrellaConfiguration.EntityType.Function("GetPartnersCount").Returns(); - - // Singleton "MonstersInc" - SingletonConfiguration monstersIncConfiguration = builder.Singleton("MonstersInc"); - //monstersIncConfiguration.EntityType.Action("ResetDataSource"); - monstersIncConfiguration.EntityType.Function("GetPartnersCount").Returns(); - - builder.Namespace = typeof(Company).Namespace; - return builder.GetEdmModel(); - } + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + EntitySetConfiguration partnersConfiguration = builder.EntitySet("Partners"); + EntityTypeConfiguration partnerConfiguration = partnersConfiguration.EntityType; + partnerConfiguration.Collection.Action("ResetDataSource"); + + // Singleton "Umbrella" + SingletonConfiguration umbrellaConfiguration = builder.Singleton("Umbrella"); + //umbrellaConfiguration.EntityType.Action("ResetDataSource"); + umbrellaConfiguration.EntityType.Function("GetPartnersCount").Returns(); + + // Singleton "MonstersInc" + SingletonConfiguration monstersIncConfiguration = builder.Singleton("MonstersInc"); + //monstersIncConfiguration.EntityType.Action("ResetDataSource"); + monstersIncConfiguration.EntityType.Function("GetPartnersCount").Returns(); + + builder.Namespace = typeof(Company).Namespace; + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/SingletonTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/SingletonTest.cs index 341f7f405..f641fd94a 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/SingletonTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/SingletonTest.cs @@ -13,442 +13,441 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Singleton +namespace Microsoft.AspNetCore.OData.E2E.Tests.Singleton; + +public class SingletonTest : WebApiTestBase { - public class SingletonTest : WebApiTestBase + public SingletonTest(WebApiTestFixture fixture) + : base(fixture) { - public SingletonTest(WebApiTestFixture fixture) - : base(fixture) - { - } - - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) - { - var controllers = new[] - { - typeof(MetadataController), - typeof(UmbrellaController), - typeof(MonstersIncController), - typeof(PartnersController), - typeof(ODataEndpointController) - }; - - services.ConfigureControllers(controllers); - services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select() - .AddRouteComponents("odata", SingletonEdmModel.GetEdmModel())); - } + } - [Fact] - public async Task SingletonShouldShowInServiceDocument() + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) + { + var controllers = new[] { - // Arrange - string requestUri = $"odata"; - HttpClient client = CreateClient(); + typeof(MetadataController), + typeof(UmbrellaController), + typeof(MonstersIncController), + typeof(PartnersController), + typeof(ODataEndpointController) + }; + + services.ConfigureControllers(controllers); + services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select() + .AddRouteComponents("odata", SingletonEdmModel.GetEdmModel())); + } - // Act - HttpResponseMessage response = await client.GetAsync(requestUri); + [Fact] + public async Task SingletonShouldShowInServiceDocument() + { + // Arrange + string requestUri = $"odata"; + HttpClient client = CreateClient(); - // Assert - response.EnsureSuccessStatusCode(); - string result = await response.Content.ReadAsStringAsync(); + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); - Assert.Contains("{\"name\":\"Partners\",\"kind\":\"EntitySet\",\"url\":\"Partners\"}", result); - Assert.Contains("{\"name\":\"Umbrella\",\"kind\":\"Singleton\",\"url\":\"Umbrella\"}", result); - Assert.Contains("{\"name\":\"MonstersInc\",\"kind\":\"Singleton\",\"url\":\"MonstersInc\"}", result); - } + // Assert + response.EnsureSuccessStatusCode(); + string result = await response.Content.ReadAsStringAsync(); - [Fact] - public async Task TestRoutes() - { - // Arrange - string requestUri = "$odata"; - HttpClient client = CreateClient(); + Assert.Contains("{\"name\":\"Partners\",\"kind\":\"EntitySet\",\"url\":\"Partners\"}", result); + Assert.Contains("{\"name\":\"Umbrella\",\"kind\":\"Singleton\",\"url\":\"Umbrella\"}", result); + Assert.Contains("{\"name\":\"MonstersInc\",\"kind\":\"Singleton\",\"url\":\"MonstersInc\"}", result); + } - // Act - var response = await client.GetAsync(requestUri); + [Fact] + public async Task TestRoutes() + { + // Arrange + string requestUri = "$odata"; + HttpClient client = CreateClient(); - // Assert - response.EnsureSuccessStatusCode(); - string contentOfString = await response.Content.ReadAsStringAsync(); - } + // Act + var response = await client.GetAsync(requestUri); - [Fact] - public async Task SingletonContainerGeneratesCorrectNextLinks() - { - // Arrange - string requestUri = "odata/MonstersInc/Projects"; - string nextLinkUri = "odata/MonstersInc/Projects?$skip=2"; - string nestedNextLinkUri = "odata/MonstersInc/Projects/1/ProjectDetails?$skip=2"; - - using (HttpClient client = CreateClient()) - { - // Act & Assert - string expectedOriginalResult = - "{\"@odata.context\":\"http://localhost/odata/$metadata#MonstersInc/Projects(ProjectDetails())\"," + - "\"value\":[" + - "{\"Id\":1,\"Title\":\"In Closet Scare\",\"ProjectDetails\":[" + - "{\"Id\":1,\"Comment\":\"The original scare\"}," + - "{\"Id\":2,\"Comment\":\"Leaving the door open is the worst mistake any employee can make\"}]," + - "\"ProjectDetails@odata.nextLink\":\"http://localhost/odata/MonstersInc/Projects/1/ProjectDetails?$skip=2\"}," + - "{\"Id\":2,\"Title\":\"Under Bed Scare\",\"ProjectDetails\":[" + - "{\"Id\":5,\"Comment\":\"Tried and true\"}," + - "{\"Id\":6,\"Comment\":\"Tip: grab a foot\"}]}]," + - "\"@odata.nextLink\":\"http://localhost/odata/MonstersInc/Projects?$skip=2\"" + - "}"; - await RequestYieldsExpectedResult(client, requestUri, expectedOriginalResult); - - string expectedNextResult = - "{\"@odata.context\":\"http://localhost/odata/$metadata#MonstersInc/Projects(ProjectDetails())\"," + - "\"value\":[{\"Id\":3,\"Title\":\"Midnight Snack in Kitchen Scare\",\"ProjectDetails\":[]}]}"; - await RequestYieldsExpectedResult(client, nextLinkUri, expectedNextResult); - - string expectedNestedNextResult = - "{\"@odata.context\":\"http://localhost/odata/$metadata#MonstersInc/Projects(1)/ProjectDetails\"," + - "\"value\":[" + - "{\"Id\":3,\"Comment\":\"Leaving the door open could let it not only a draft, but a child\"}," + - "{\"Id\":4,\"Comment\":\"Has led to the intrusion of a young girl, Boo\"}]}"; - await RequestYieldsExpectedResult(client, nestedNextLinkUri, expectedNestedNextResult); - } - } + // Assert + response.EnsureSuccessStatusCode(); + string contentOfString = await response.Content.ReadAsStringAsync(); + } - private async Task RequestYieldsExpectedResult(HttpClient client, string requestUri, string expectedResult) - { - // Act - using (HttpResponseMessage response = await client.GetAsync(requestUri)) - { - // Assert - string result = await response.Content.ReadAsStringAsync(); - response.EnsureSuccessStatusCode(); - Assert.Equal(expectedResult, result); - } - } + [Fact] + public async Task SingletonContainerGeneratesCorrectNextLinks() + { + // Arrange + string requestUri = "odata/MonstersInc/Projects"; + string nextLinkUri = "odata/MonstersInc/Projects?$skip=2"; + string nestedNextLinkUri = "odata/MonstersInc/Projects/1/ProjectDetails?$skip=2"; - [Fact] - public async Task NotCountable() + using (HttpClient client = CreateClient()) { - // Arrange - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync("odata/Umbrella/Partners/$count"); - - // Assert - string contentOfString = await response.Content.ReadAsStringAsync(); - Assert.True(HttpStatusCode.BadRequest == response.StatusCode, string.Format( - @"The response code is incorrect, expanded: 400, but actually: {0}, response: {1}.", - response.StatusCode, - contentOfString)); - - Assert.Contains("\"message\":\"The query specified in the URI is not valid. The property 'Partners' cannot be used for $count.\"", - contentOfString); + // Act & Assert + string expectedOriginalResult = + "{\"@odata.context\":\"http://localhost/odata/$metadata#MonstersInc/Projects(ProjectDetails())\"," + + "\"value\":[" + + "{\"Id\":1,\"Title\":\"In Closet Scare\",\"ProjectDetails\":[" + + "{\"Id\":1,\"Comment\":\"The original scare\"}," + + "{\"Id\":2,\"Comment\":\"Leaving the door open is the worst mistake any employee can make\"}]," + + "\"ProjectDetails@odata.nextLink\":\"http://localhost/odata/MonstersInc/Projects/1/ProjectDetails?$skip=2\"}," + + "{\"Id\":2,\"Title\":\"Under Bed Scare\",\"ProjectDetails\":[" + + "{\"Id\":5,\"Comment\":\"Tried and true\"}," + + "{\"Id\":6,\"Comment\":\"Tip: grab a foot\"}]}]," + + "\"@odata.nextLink\":\"http://localhost/odata/MonstersInc/Projects?$skip=2\"" + + "}"; + await RequestYieldsExpectedResult(client, requestUri, expectedOriginalResult); + + string expectedNextResult = + "{\"@odata.context\":\"http://localhost/odata/$metadata#MonstersInc/Projects(ProjectDetails())\"," + + "\"value\":[{\"Id\":3,\"Title\":\"Midnight Snack in Kitchen Scare\",\"ProjectDetails\":[]}]}"; + await RequestYieldsExpectedResult(client, nextLinkUri, expectedNextResult); + + string expectedNestedNextResult = + "{\"@odata.context\":\"http://localhost/odata/$metadata#MonstersInc/Projects(1)/ProjectDetails\"," + + "\"value\":[" + + "{\"Id\":3,\"Comment\":\"Leaving the door open could let it not only a draft, but a child\"}," + + "{\"Id\":4,\"Comment\":\"Has led to the intrusion of a young girl, Boo\"}]}"; + await RequestYieldsExpectedResult(client, nestedNextLinkUri, expectedNestedNextResult); } + } - [Theory] - [InlineData("odata/MonstersInc/Branches/$count", 2)] - [InlineData("odata/MonstersInc/Branches/$count?$filter=City eq 'Shanghai'", 1)] - public async Task QueryBranchesCount(string url, int expectedCount) + private async Task RequestYieldsExpectedResult(HttpClient client, string requestUri, string expectedResult) + { + // Act + using (HttpResponseMessage response = await client.GetAsync(requestUri)) { - // Arrange - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync(url); - string responseString = await response.Content.ReadAsStringAsync(); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.True(expectedCount == int.Parse(responseString), - string.Format("Expected: {0}; Actual: {1}; Request URL: {2}", expectedCount, responseString, url)); - } - -#if false -#region Singleton - [Theory] - [InlineData("expCon", "Umbrella")] - [InlineData("expAttr", "MonstersInc")] - [InlineData("conCon", "Umbrella")] - [InlineData("conAttr", "MonstersInc")] - public async Task SingletonCRUD(string model, string singletonName) - { - string requestUri = string.Format(this.BaseAddress + "/{0}/{1}", model, singletonName); - - // Reset data source - await ResetDataSource(model, singletonName); - await ResetDataSource(model, "Partners"); - - // GET singleton - HttpResponseMessage response = await this.Client.GetAsync(requestUri); - dynamic result = JObject.Parse(await response.Content.ReadAsStringAsync()); - - // PUT singleton with {"ID":1,"Name":"singletonName","Revenue":2000,"Category":"IT"} - result["Revenue"] = 2000; - response = await HttpClientExtensions.PutAsJsonAsync(this.Client, requestUri, result); - response.EnsureSuccessStatusCode(); - - // GET singleton/Revenue - response = await this.Client.GetAsync(requestUri + "/Revenue"); - result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal(2000, (int)result["value"]); - - // PATCH singleton with {"@odata.type":"#Microsoft.Test.E2E.AspNet.OData.Singleton.Company","Revenue":3000} - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - request.Content = new StringContent(string.Format(@"{{""@odata.type"":""#{0}"",""Revenue"":3000}}", typeof(Company))); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - response = await Client.SendAsync(request); + string result = await response.Content.ReadAsStringAsync(); response.EnsureSuccessStatusCode(); - - // GET singleton - response = await this.Client.GetAsync(requestUri); - result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal(3000, (int)result["Revenue"]); - - // Negative: Add singleton - // POST singleton - var company = new Company(); - response = await this.Client.PostAsJsonAsync(requestUri, company); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - - // Negative: Delete singleton - // DELETE singleton - response = await this.Client.DeleteAsync(requestUri); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + Assert.Equal(expectedResult, result); } + } - // Navigation link CRUD, singleton is navigation source and entityset is navigation target - [Theory] - [InlineData("expCon", "Umbrella")] - [InlineData("expAttr", "MonstersInc")] - [InlineData("conCon", "Umbrella")] - [InlineData("conAttr", "MonstersInc")] - public async Task SingletonNavigationLinkCRUD(string model, string singletonName) - { - string requestUri = string.Format(this.BaseAddress + "/{0}/{1}/Partners", model, singletonName); - - // Reset data source - await ResetDataSource(model, singletonName); - await ResetDataSource(model, "Partners"); - - // GET singleton/Partners - HttpResponseMessage response = await this.Client.GetAsync(requestUri); - var json = await response.Content.ReadAsObject(); - var result = json.GetValue("value") as JArray; - Assert.Empty(result); - - string navigationLinkUri = string.Format(requestUri + "/$ref"); - - // POST singleton/Partners/$ref - string idLinkBase = string.Format(this.BaseAddress + "/{0}/Partners", model); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, navigationLinkUri); - request.Content = new StringContent("{ \"@odata.id\" : \"" + idLinkBase + "(1)\"}"); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - response = await Client.SendAsync(request); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - - // POST singleton/Partners/$ref - request = new HttpRequestMessage(HttpMethod.Post, navigationLinkUri); - request.Content = new StringContent("{ \"@odata.id\" : \"" + idLinkBase + "(2)\"}"); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - response = await Client.SendAsync(request); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - - // POST singleton/Partners/$ref - request = new HttpRequestMessage(HttpMethod.Post, navigationLinkUri); - request.Content = new StringContent("{ \"@odata.id\" : \"" + idLinkBase + "(3)\"}"); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - response = await Client.SendAsync(request); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - - // GET singleton/Partners - response = await this.Client.GetAsync(requestUri); - json = await response.Content.ReadAsObject(); - result = json.GetValue("value") as JArray; - Assert.Equal(3, result.Count); - - // Add Partner to Company by "Deep Insert" - // POST singleton/Partners - Partner partner = new Partner() { ID = 100, Name = "NewHire" }; - response = await this.Client.PostAsJsonAsync(requestUri, partner); - response.EnsureSuccessStatusCode(); + [Fact] + public async Task NotCountable() + { + // Arrange + HttpClient client = CreateClient(); - // GET singleton/Partners - response = await this.Client.GetAsync(requestUri); - json = await response.Content.ReadAsObject(); - result = json.GetValue("value") as JArray; - Assert.Equal(4, result.Count); - - // Unrelate Partners(3) from Company - // DELETE singleton/Partners(3)/$ref - request = new HttpRequestMessage(HttpMethod.Delete, requestUri + "(3)/$ref"); - response = await Client.SendAsync(request); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - - // GET singleton/Partners - response = await this.Client.GetAsync(requestUri); - json = await response.Content.ReadAsObject(); - result = json.GetValue("value") as JArray; - Assert.Equal(3, result.Count); - - // GET singleton/Microsoft.Test.E2E.AspNet.OData.Singleton.GetPartnersCount() - requestUri = string.Format(BaseAddress + "/{0}/{1}/{2}.GetPartnersCount()", model, singletonName, NameSpace); - response = await this.Client.GetAsync(requestUri); - json = await response.Content.ReadAsObject(); - Assert.Equal(3, (int)json["value"]); - } + // Act + HttpResponseMessage response = await client.GetAsync("odata/Umbrella/Partners/$count"); - // Navigation link CRUD, where entityset is navigation source and singleton is navigation target - [Theory] - [InlineData("expAttr", "application/json;odata.metadata=full")] - [InlineData("conAttr", "application/json;odata.metadata=minimal")] - public async Task EntitySetNavigationLinkCRUD(string model, string format) - { - string requestUri = string.Format(this.BaseAddress + "/{0}/Partners(1)/Company", model); - string navigationUri = requestUri + "/$ref"; - string formatQuery = string.Format("?$format={0}", format); - - //Reset data source - await ResetDataSource(model, "MonstersInc"); - await ResetDataSource(model, "Partners"); - - // PUT Partners(1)/Company/$ref - string idLinkBase = string.Format(this.BaseAddress + "/{0}/MonstersInc", model); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, navigationUri); - request.Content = new StringContent("{ \"@odata.id\" : \"" + idLinkBase + "\"}"); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - var response = await Client.SendAsync(request); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - - // GET Partners(1)/Company - response = await this.Client.GetAsync(requestUri); - var result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal("MonstersInc", (string)result["Name"]); - - // PUT Partners(1)/Company - result["Revenue"] = 2000; - response = await HttpClientExtensions.PutAsJsonAsync(this.Client, requestUri, result); - response.EnsureSuccessStatusCode(); + // Assert + string contentOfString = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.BadRequest == response.StatusCode, string.Format( + @"The response code is incorrect, expanded: 400, but actually: {0}, response: {1}.", + response.StatusCode, + contentOfString)); - // GET Partners(1)/Company/Revenue - response = await this.Client.GetAsync(requestUri + formatQuery); - result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal(2000, (int)result["Revenue"]); + Assert.Contains("\"message\":\"The query specified in the URI is not valid. The property 'Partners' cannot be used for $count.\"", + contentOfString); + } - // PATCH Partners(1)/Company - request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - request.Content = new StringContent(string.Format(@"{{""@odata.type"":""#{0}"",""Revenue"":3000}}", typeof(Company))); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - response = await Client.SendAsync(request); - response.EnsureSuccessStatusCode(); + [Theory] + [InlineData("odata/MonstersInc/Branches/$count", 2)] + [InlineData("odata/MonstersInc/Branches/$count?$filter=City eq 'Shanghai'", 1)] + public async Task QueryBranchesCount(string url, int expectedCount) + { + // Arrange + HttpClient client = CreateClient(); - // GET Partners(1)/Company/Revenue - response = await this.Client.GetAsync(requestUri + formatQuery); - result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal(3000, (int)result["Revenue"]); + // Act + HttpResponseMessage response = await client.GetAsync(url); + string responseString = await response.Content.ReadAsStringAsync(); - // DELETE Partners(1)/Company/$ref - response = await Client.DeleteAsync(navigationUri); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(expectedCount == int.Parse(responseString), + string.Format("Expected: {0}; Actual: {1}; Request URL: {2}", expectedCount, responseString, url)); + } - // GET Partners(1)/Company - response = await this.Client.GetAsync(requestUri + formatQuery); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); +#if false +#region Singleton + [Theory] + [InlineData("expCon", "Umbrella")] + [InlineData("expAttr", "MonstersInc")] + [InlineData("conCon", "Umbrella")] + [InlineData("conAttr", "MonstersInc")] + public async Task SingletonCRUD(string model, string singletonName) + { + string requestUri = string.Format(this.BaseAddress + "/{0}/{1}", model, singletonName); + + // Reset data source + await ResetDataSource(model, singletonName); + await ResetDataSource(model, "Partners"); + + // GET singleton + HttpResponseMessage response = await this.Client.GetAsync(requestUri); + dynamic result = JObject.Parse(await response.Content.ReadAsStringAsync()); + + // PUT singleton with {"ID":1,"Name":"singletonName","Revenue":2000,"Category":"IT"} + result["Revenue"] = 2000; + response = await HttpClientExtensions.PutAsJsonAsync(this.Client, requestUri, result); + response.EnsureSuccessStatusCode(); + + // GET singleton/Revenue + response = await this.Client.GetAsync(requestUri + "/Revenue"); + result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal(2000, (int)result["value"]); + + // PATCH singleton with {"@odata.type":"#Microsoft.Test.E2E.AspNet.OData.Singleton.Company","Revenue":3000} + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + request.Content = new StringContent(string.Format(@"{{""@odata.type"":""#{0}"",""Revenue"":3000}}", typeof(Company))); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + response = await Client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + // GET singleton + response = await this.Client.GetAsync(requestUri); + result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal(3000, (int)result["Revenue"]); + + // Negative: Add singleton + // POST singleton + var company = new Company(); + response = await this.Client.PostAsJsonAsync(requestUri, company); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + // Negative: Delete singleton + // DELETE singleton + response = await this.Client.DeleteAsync(requestUri); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } - // Negative: POST Partners(1)/Company - var company = new Company(); - response = await this.Client.PostAsJsonAsync(requestUri, company); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } + // Navigation link CRUD, singleton is navigation source and entityset is navigation target + [Theory] + [InlineData("expCon", "Umbrella")] + [InlineData("expAttr", "MonstersInc")] + [InlineData("conCon", "Umbrella")] + [InlineData("conAttr", "MonstersInc")] + public async Task SingletonNavigationLinkCRUD(string model, string singletonName) + { + string requestUri = string.Format(this.BaseAddress + "/{0}/{1}/Partners", model, singletonName); + + // Reset data source + await ResetDataSource(model, singletonName); + await ResetDataSource(model, "Partners"); + + // GET singleton/Partners + HttpResponseMessage response = await this.Client.GetAsync(requestUri); + var json = await response.Content.ReadAsObject(); + var result = json.GetValue("value") as JArray; + Assert.Empty(result); + + string navigationLinkUri = string.Format(requestUri + "/$ref"); + + // POST singleton/Partners/$ref + string idLinkBase = string.Format(this.BaseAddress + "/{0}/Partners", model); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, navigationLinkUri); + request.Content = new StringContent("{ \"@odata.id\" : \"" + idLinkBase + "(1)\"}"); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // POST singleton/Partners/$ref + request = new HttpRequestMessage(HttpMethod.Post, navigationLinkUri); + request.Content = new StringContent("{ \"@odata.id\" : \"" + idLinkBase + "(2)\"}"); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // POST singleton/Partners/$ref + request = new HttpRequestMessage(HttpMethod.Post, navigationLinkUri); + request.Content = new StringContent("{ \"@odata.id\" : \"" + idLinkBase + "(3)\"}"); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // GET singleton/Partners + response = await this.Client.GetAsync(requestUri); + json = await response.Content.ReadAsObject(); + result = json.GetValue("value") as JArray; + Assert.Equal(3, result.Count); + + // Add Partner to Company by "Deep Insert" + // POST singleton/Partners + Partner partner = new Partner() { ID = 100, Name = "NewHire" }; + response = await this.Client.PostAsJsonAsync(requestUri, partner); + response.EnsureSuccessStatusCode(); + + // GET singleton/Partners + response = await this.Client.GetAsync(requestUri); + json = await response.Content.ReadAsObject(); + result = json.GetValue("value") as JArray; + Assert.Equal(4, result.Count); + + // Unrelate Partners(3) from Company + // DELETE singleton/Partners(3)/$ref + request = new HttpRequestMessage(HttpMethod.Delete, requestUri + "(3)/$ref"); + response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // GET singleton/Partners + response = await this.Client.GetAsync(requestUri); + json = await response.Content.ReadAsObject(); + result = json.GetValue("value") as JArray; + Assert.Equal(3, result.Count); + + // GET singleton/Microsoft.Test.E2E.AspNet.OData.Singleton.GetPartnersCount() + requestUri = string.Format(BaseAddress + "/{0}/{1}/{2}.GetPartnersCount()", model, singletonName, NameSpace); + response = await this.Client.GetAsync(requestUri); + json = await response.Content.ReadAsObject(); + Assert.Equal(3, (int)json["value"]); + } - [Theory] - [InlineData("expCon", "Umbrella", "application/json;odata.metadata=full")] - [InlineData("expAttr", "MonstersInc", "application/json;odata.metadata=minimal")] - [InlineData("conCon", "Umbrella", "application/json;odata.metadata=full")] - [InlineData("conAttr", "MonstersInc", "application/json;odata.metadata=minimal")] - public async Task SingletonDerivedTypeTest(string model, string singletonName, string format) - { - string requestUri = string.Format(this.BaseAddress + "/{0}/{1}/{2}.SubCompany", model, singletonName, NameSpace); - string formatQuery = string.Format("$format={0}", format); + // Navigation link CRUD, where entityset is navigation source and singleton is navigation target + [Theory] + [InlineData("expAttr", "application/json;odata.metadata=full")] + [InlineData("conAttr", "application/json;odata.metadata=minimal")] + public async Task EntitySetNavigationLinkCRUD(string model, string format) + { + string requestUri = string.Format(this.BaseAddress + "/{0}/Partners(1)/Company", model); + string navigationUri = requestUri + "/$ref"; + string formatQuery = string.Format("?$format={0}", format); + + //Reset data source + await ResetDataSource(model, "MonstersInc"); + await ResetDataSource(model, "Partners"); + + // PUT Partners(1)/Company/$ref + string idLinkBase = string.Format(this.BaseAddress + "/{0}/MonstersInc", model); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, navigationUri); + request.Content = new StringContent("{ \"@odata.id\" : \"" + idLinkBase + "\"}"); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + var response = await Client.SendAsync(request); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // GET Partners(1)/Company + response = await this.Client.GetAsync(requestUri); + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("MonstersInc", (string)result["Name"]); + + // PUT Partners(1)/Company + result["Revenue"] = 2000; + response = await HttpClientExtensions.PutAsJsonAsync(this.Client, requestUri, result); + response.EnsureSuccessStatusCode(); + + // GET Partners(1)/Company/Revenue + response = await this.Client.GetAsync(requestUri + formatQuery); + result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal(2000, (int)result["Revenue"]); + + // PATCH Partners(1)/Company + request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + request.Content = new StringContent(string.Format(@"{{""@odata.type"":""#{0}"",""Revenue"":3000}}", typeof(Company))); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + response = await Client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + // GET Partners(1)/Company/Revenue + response = await this.Client.GetAsync(requestUri + formatQuery); + result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal(3000, (int)result["Revenue"]); + + // DELETE Partners(1)/Company/$ref + response = await Client.DeleteAsync(navigationUri); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // GET Partners(1)/Company + response = await this.Client.GetAsync(requestUri + formatQuery); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + // Negative: POST Partners(1)/Company + var company = new Company(); + response = await this.Client.PostAsJsonAsync(requestUri, company); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } - await ResetDataSource(model, singletonName); + [Theory] + [InlineData("expCon", "Umbrella", "application/json;odata.metadata=full")] + [InlineData("expAttr", "MonstersInc", "application/json;odata.metadata=minimal")] + [InlineData("conCon", "Umbrella", "application/json;odata.metadata=full")] + [InlineData("conAttr", "MonstersInc", "application/json;odata.metadata=minimal")] + public async Task SingletonDerivedTypeTest(string model, string singletonName, string format) + { + string requestUri = string.Format(this.BaseAddress + "/{0}/{1}/{2}.SubCompany", model, singletonName, NameSpace); + string formatQuery = string.Format("$format={0}", format); + + await ResetDataSource(model, singletonName); + + // PUT singleton/Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany + var company = new { ID = 100, Name = "UmbrellaInSouthPole", Category = CompanyCategory.Communication.ToString(), Revenue = 1000, Location = "South Pole", Description = "The Umbrella In South Pole", Partners = new List(), Branches = new List(), Office = new Office() { City = "South", Address = "999" } }; + var request = new HttpRequestMessage(HttpMethod.Put, requestUri); + request.Content = new StringContent(JsonConvert.SerializeObject(company)); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + var response = await Client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + // GET singleton/Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany/Location + response = await this.Client.GetAsync(requestUri + "/Location?" + formatQuery); + var result = await response.Content.ReadAsObject(); + Assert.Equal(company.Location, (string)result["value"]); + + // Query complex type + // GET GET singleton/Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany/Office + response = await this.Client.GetAsync(requestUri + "/Office?" + formatQuery); + result = await response.Content.ReadAsObject(); + Assert.Equal(company.Office.City, (string)result["City"]); + + // GET singleton/Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany?$select=Location + response = await this.Client.GetAsync(requestUri + "?$select=Location&" + formatQuery); + result = await response.Content.ReadAsObject(); + Assert.Equal(company.Location, (string)result["Location"]); + } - // PUT singleton/Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany - var company = new { ID = 100, Name = "UmbrellaInSouthPole", Category = CompanyCategory.Communication.ToString(), Revenue = 1000, Location = "South Pole", Description = "The Umbrella In South Pole", Partners = new List(), Branches = new List(), Office = new Office() { City = "South", Address = "999" } }; - var request = new HttpRequestMessage(HttpMethod.Put, requestUri); - request.Content = new StringContent(JsonConvert.SerializeObject(company)); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - var response = await Client.SendAsync(request); - response.EnsureSuccessStatusCode(); + [Theory] + [InlineData("expCon", "Umbrella")] + [InlineData("expAttr", "MonstersInc")] + [InlineData("conCon", "Umbrella")] + [InlineData("conAttr", "MonstersInc")] + public async Task SingletonQueryOptionsTest(string model, string singletonName) + { + string requestUri = string.Format(this.BaseAddress + "/{0}/{1}", model, singletonName); - // GET singleton/Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany/Location - response = await this.Client.GetAsync(requestUri + "/Location?" + formatQuery); - var result = await response.Content.ReadAsObject(); - Assert.Equal(company.Location, (string)result["value"]); - - // Query complex type - // GET GET singleton/Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany/Office - response = await this.Client.GetAsync(requestUri + "/Office?" + formatQuery); - result = await response.Content.ReadAsObject(); - Assert.Equal(company.Office.City, (string)result["City"]); - - // GET singleton/Microsoft.Test.E2E.AspNet.OData.Singleton.SubCompany?$select=Location - response = await this.Client.GetAsync(requestUri + "?$select=Location&" + formatQuery); - result = await response.Content.ReadAsObject(); - Assert.Equal(company.Location, (string)result["Location"]); - } + await ResetDataSource(model, singletonName); - [Theory] - [InlineData("expCon", "Umbrella")] - [InlineData("expAttr", "MonstersInc")] - [InlineData("conCon", "Umbrella")] - [InlineData("conAttr", "MonstersInc")] - public async Task SingletonQueryOptionsTest(string model, string singletonName) + // GET /singleton?$select=Name + var response = await this.Client.GetAsync(requestUri + "?$select=Name"); + var result = await response.Content.ReadAsObject(); + int i = 0; + foreach (var pro in result.Properties()) { - string requestUri = string.Format(this.BaseAddress + "/{0}/{1}", model, singletonName); - - await ResetDataSource(model, singletonName); - - // GET /singleton?$select=Name - var response = await this.Client.GetAsync(requestUri + "?$select=Name"); - var result = await response.Content.ReadAsObject(); - int i = 0; - foreach (var pro in result.Properties()) - { - i++; - } - Assert.Equal(2, i); - - // POST /singleton/Partners - Partner partner = new Partner() { ID = 100, Name = "NewHire" }; - response = await this.Client.PostAsJsonAsync(requestUri + "/Partners", partner); - response.EnsureSuccessStatusCode(); - - // POST /singleton/Partners - partner = new Partner() { ID = 101, Name = "NewHire2" }; - response = await this.Client.PostAsJsonAsync(requestUri + "/Partners", partner); - response.EnsureSuccessStatusCode(); - - // GET /singleton?$expand=Partners($select=Name) - response = await this.Client.GetAsync(requestUri + "?$expand=Partners($select=Name)"); - result = await response.Content.ReadAsObject(); - var json = result.GetValue("Partners") as JArray; - Assert.Equal(2, json.Count); - - // PUT Partners(1)/Company/$ref - var navigationUri = string.Format(this.BaseAddress + "/{0}/Partners(1)/Company/$ref", model); - string idLinkBase = string.Format(this.BaseAddress + "/{0}/{1}", model, singletonName); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, navigationUri); - request.Content = new StringContent("{ \"@odata.id\" : \"" + idLinkBase + "\"}"); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - response = await Client.SendAsync(request); - - // GET /Partners(1)?$expand=Company($select=Name) - requestUri = string.Format(this.BaseAddress + "/{0}/Partners(1)", model); - response = await this.Client.GetAsync(requestUri + "?$expand=Company($select=Name)"); - result = await response.Content.ReadAsObject(); - var company = result.GetValue("Company") as JObject; - Assert.Equal(singletonName, company.GetValue("Name")); + i++; } + Assert.Equal(2, i); + + // POST /singleton/Partners + Partner partner = new Partner() { ID = 100, Name = "NewHire" }; + response = await this.Client.PostAsJsonAsync(requestUri + "/Partners", partner); + response.EnsureSuccessStatusCode(); + + // POST /singleton/Partners + partner = new Partner() { ID = 101, Name = "NewHire2" }; + response = await this.Client.PostAsJsonAsync(requestUri + "/Partners", partner); + response.EnsureSuccessStatusCode(); + + // GET /singleton?$expand=Partners($select=Name) + response = await this.Client.GetAsync(requestUri + "?$expand=Partners($select=Name)"); + result = await response.Content.ReadAsObject(); + var json = result.GetValue("Partners") as JArray; + Assert.Equal(2, json.Count); + + // PUT Partners(1)/Company/$ref + var navigationUri = string.Format(this.BaseAddress + "/{0}/Partners(1)/Company/$ref", model); + string idLinkBase = string.Format(this.BaseAddress + "/{0}/{1}", model, singletonName); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, navigationUri); + request.Content = new StringContent("{ \"@odata.id\" : \"" + idLinkBase + "\"}"); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + response = await Client.SendAsync(request); + + // GET /Partners(1)?$expand=Company($select=Name) + requestUri = string.Format(this.BaseAddress + "/{0}/Partners(1)", model); + response = await this.Client.GetAsync(requestUri + "?$expand=Company($select=Name)"); + result = await response.Content.ReadAsObject(); + var company = result.GetValue("Company") as JObject; + Assert.Equal(singletonName, company.GetValue("Name")); + } #endregion #endif - } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/UmbrellaController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/UmbrellaController.cs index d596f04b4..88c1080f5 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/UmbrellaController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Singleton/UmbrellaController.cs @@ -16,174 +16,173 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Singleton +namespace Microsoft.AspNetCore.OData.E2E.Tests.Singleton; + +/// +/// Present a singleton named "Umbrella" +/// Use convention routing +/// +public class UmbrellaController : ODataController { - /// - /// Present a singleton named "Umbrella" - /// Use convention routing - /// - public class UmbrellaController : ODataController + public static Company Umbrella; + + static UmbrellaController() { - public static Company Umbrella; + InitData(); + } - static UmbrellaController() - { - InitData(); - } + private static void InitData() + { + Umbrella = new Company() + { + ID = 1, + Name = "Umbrella", + Revenue = 1000, + Category = CompanyCategory.Communication, + Partners = new List(), + Branches = new List(), + Projects = new List(), + }; + } - private static void InitData() - { - Umbrella = new Company() - { - ID = 1, - Name = "Umbrella", - Revenue = 1000, - Category = CompanyCategory.Communication, - Partners = new List(), - Branches = new List(), - Projects = new List(), - }; - } + #region Query + [EnableQuery] + public IActionResult Get() + { + return Ok(Umbrella); + } - #region Query - [EnableQuery] - public IActionResult Get() + public IActionResult GetFromSubCompany() + { + var subCompany = Umbrella as SubCompany; + if (subCompany != null) { - return Ok(Umbrella); + return Ok(subCompany); } + return BadRequest(); + } - public IActionResult GetFromSubCompany() - { - var subCompany = Umbrella as SubCompany; - if (subCompany != null) - { - return Ok(subCompany); - } - return BadRequest(); - } + public IActionResult GetRevenueFromCompany() + { + return Ok(Umbrella.Revenue); + } - public IActionResult GetRevenueFromCompany() - { - return Ok(Umbrella.Revenue); - } + public IActionResult GetNameFromCompany() + { + return Ok(Umbrella.Name); + } - public IActionResult GetNameFromCompany() - { - return Ok(Umbrella.Name); - } + public IActionResult GetCategoryFromCompany() + { + return Ok(Umbrella.Category); + } - public IActionResult GetCategoryFromCompany() + public IActionResult GetLocationFromSubCompany() + { + var subCompany = Umbrella as SubCompany; + if (subCompany != null) { - return Ok(Umbrella.Category); + return Ok(subCompany.Location); } + return BadRequest(); + } - public IActionResult GetLocationFromSubCompany() + public IActionResult GetOffice() + { + var subCompany = Umbrella as SubCompany; + if (subCompany != null) { - var subCompany = Umbrella as SubCompany; - if (subCompany != null) - { - return Ok(subCompany.Location); - } - return BadRequest(); + return Ok(subCompany.Office); } + return BadRequest(); + } - public IActionResult GetOffice() - { - var subCompany = Umbrella as SubCompany; - if (subCompany != null) - { - return Ok(subCompany.Office); - } - return BadRequest(); - } + [EnableQuery] + public IActionResult GetPartnersFromCompany() + { + return Ok(Umbrella.Partners); + } + #endregion - [EnableQuery] - public IActionResult GetPartnersFromCompany() - { - return Ok(Umbrella.Partners); - } - #endregion + #region Update + public IActionResult Put([FromBody]Company newCompany) + { + Umbrella = newCompany; + return StatusCode(StatusCodes.Status204NoContent); + } - #region Update - public IActionResult Put([FromBody]Company newCompany) - { - Umbrella = newCompany; - return StatusCode(StatusCodes.Status204NoContent); - } + public IActionResult PutUmbrellaFromSubCompany([FromBody]SubCompany newCompany) + { + Umbrella = newCompany; + return StatusCode(StatusCodes.Status204NoContent); + } - public IActionResult PutUmbrellaFromSubCompany([FromBody]SubCompany newCompany) - { - Umbrella = newCompany; - return StatusCode(StatusCodes.Status204NoContent); - } + public IActionResult Patch([FromBody]Delta item) + { + item.Patch(Umbrella); + return StatusCode(StatusCodes.Status204NoContent); + } + #endregion - public IActionResult Patch([FromBody]Delta item) - { - item.Patch(Umbrella); - return StatusCode(StatusCodes.Status204NoContent); - } - #endregion + #region Navigation link + [AcceptVerbs("POST")] + public IActionResult CreateRef(string navigationProperty, [FromBody] Uri link) + { + int relatedKey = Request.GetKeyValue(link); + Partner partner = PartnersController.Partners.First(x => x.ID == relatedKey); - #region Navigation link - [AcceptVerbs("POST")] - public IActionResult CreateRef(string navigationProperty, [FromBody] Uri link) + if (navigationProperty != "Partners" || partner == null) { - int relatedKey = Request.GetKeyValue(link); - Partner partner = PartnersController.Partners.First(x => x.ID == relatedKey); - - if (navigationProperty != "Partners" || partner == null) - { - return BadRequest(); - } - - Umbrella.Partners.Add(partner); - return StatusCode(StatusCodes.Status204NoContent); + return BadRequest(); } - [AcceptVerbs("DELETE")] - public IActionResult DeleteRef(string relatedKey, string navigationProperty) - { - int key = int.Parse(relatedKey); - Partner partner = Umbrella.Partners.First(x => x.ID == key); - - if (navigationProperty != "Partners") - { - return BadRequest(); - } + Umbrella.Partners.Add(partner); + return StatusCode(StatusCodes.Status204NoContent); + } - Umbrella.Partners.Remove(partner); - return StatusCode(StatusCodes.Status204NoContent); - } + [AcceptVerbs("DELETE")] + public IActionResult DeleteRef(string relatedKey, string navigationProperty) + { + int key = int.Parse(relatedKey); + Partner partner = Umbrella.Partners.First(x => x.ID == key); - [HttpPost] - public IActionResult PostToPartners([FromBody] Partner partner) + if (navigationProperty != "Partners") { - PartnersController.Partners.Add(partner); - if (Umbrella.Partners == null) - { - Umbrella.Partners = new List() { partner }; - } - else - { - Umbrella.Partners.Add(partner); - } - - return Created(partner); + return BadRequest(); } - #endregion - #region Action and Function - [HttpPost] - public IActionResult ResetDataSourceOnCompany() + Umbrella.Partners.Remove(partner); + return StatusCode(StatusCodes.Status204NoContent); + } + + [HttpPost] + public IActionResult PostToPartners([FromBody] Partner partner) + { + PartnersController.Partners.Add(partner); + if (Umbrella.Partners == null) { - InitData(); - return StatusCode(StatusCodes.Status204NoContent); + Umbrella.Partners = new List() { partner }; } - - public IActionResult GetPartnersCount() + else { - return Ok(Umbrella.Partners.Count); + Umbrella.Partners.Add(partner); } - #endregion + + return Created(partner); + } + #endregion + + #region Action and Function + [HttpPost] + public IActionResult ResetDataSourceOnCompany() + { + InitData(); + return StatusCode(StatusCodes.Status204NoContent); + } + + public IActionResult GetPartnersCount() + { + return Ok(Umbrella.Partners.Count); } + #endregion } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/SkipToken/SkipTokenControllers.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/SkipToken/SkipTokenControllers.cs index 420d0554d..fbfe4e4ae 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/SkipToken/SkipTokenControllers.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/SkipToken/SkipTokenControllers.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -12,199 +12,198 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.SkipToken +namespace Microsoft.AspNetCore.OData.E2E.Tests.SkipToken; + +public class SkipTokenControllers : ODataController { - public class SkipTokenControllers : ODataController - { - private static IList _stCustomers; - private static IList _stOrders; + private static IList _stCustomers; + private static IList _stOrders; - #region Data - static SkipTokenControllers() + #region Data + static SkipTokenControllers() + { + IList stAddresses = new List() { - IList stAddresses = new List() + new StAddress { - new StAddress + State = "WA", + City = "Settle", + ZipCode = 9811, + DynamicProperties = new Dictionary { - State = "WA", - City = "Settle", - ZipCode = 9811, - DynamicProperties = new Dictionary - { - { "Region", "Bb" } - } - }, + { "Region", "Bb" } + } + }, - new StAddress + new StAddress + { + State = "OT", + City = "Perry", + ZipCode = 1831, + DynamicProperties = new Dictionary { - State = "OT", - City = "Perry", - ZipCode = 1831, - DynamicProperties = new Dictionary - { - { "Region", "Aa" } - } - }, + { "Region", "Aa" } + } + }, - new StAddress + new StAddress + { + State = "AJ", + City = "Reedy", + ZipCode = 7817, + DynamicProperties = new Dictionary { - State = "AJ", - City = "Reedy", - ZipCode = 7817, - DynamicProperties = new Dictionary - { - { "Region", "Cc" } - } + { "Region", "Cc" } } - }; + } + }; - _stCustomers = new List + _stCustomers = new List + { + new StCustomer { - new StCustomer + Id = 1, + Name = "Mars", + Age = 871, + Birthday = new DateTimeOffset(1999, 1, 2, 1, 1, 2, 3, TimeSpan.Zero), + MagicNumber = -88, + PhoneNumbers = new [] { "M-110", "M-019", "M-712"}, + FavoritePlace = stAddresses[1], + Orders = new List { - Id = 1, - Name = "Mars", - Age = 871, - Birthday = new DateTimeOffset(1999, 1, 2, 1, 1, 2, 3, TimeSpan.Zero), - MagicNumber = -88, - PhoneNumbers = new [] { "M-110", "M-019", "M-712"}, - FavoritePlace = stAddresses[1], - Orders = new List - { - new StOrder{ RegId = "A9", Id = 11, Amount = 66, Location = stAddresses[0] }, - new StOrder{ RegId = "A9", Id = 12, Amount = 42, Location = stAddresses[2] }, - new StOrder{ RegId = "A9", Id = 13, Amount = 17, Location = stAddresses[1] } - }, - DynamicProperties = new Dictionary - { - { "Detail", "Vip" } - } + new StOrder{ RegId = "A9", Id = 11, Amount = 66, Location = stAddresses[0] }, + new StOrder{ RegId = "A9", Id = 12, Amount = 42, Location = stAddresses[2] }, + new StOrder{ RegId = "A9", Id = 13, Amount = 17, Location = stAddresses[1] } }, - new StCustomer + DynamicProperties = new Dictionary + { + { "Detail", "Vip" } + } + }, + new StCustomer + { + Id = 2, + Name = "Earth", + Age = 79, + Birthday = new DateTimeOffset(2001, 4, 6, 6, 1, 4, 3, TimeSpan.Zero), + MagicNumber = -86, + PhoneNumbers = new [] { "E-819" }, + FavoritePlace = stAddresses[2], + Orders = new List { - Id = 2, - Name = "Earth", - Age = 79, - Birthday = new DateTimeOffset(2001, 4, 6, 6, 1, 4, 3, TimeSpan.Zero), - MagicNumber = -86, - PhoneNumbers = new [] { "E-819" }, - FavoritePlace = stAddresses[2], - Orders = new List - { - new StOrder{ RegId = "A8", Id = 2, Amount = 18, Location = stAddresses[1] }, - new StOrder{ RegId = "A9", Id = 5, Amount = 87, Location = stAddresses[0] } - }, - DynamicProperties = new Dictionary - { - { "Detail", "Regular" } - } + new StOrder{ RegId = "A8", Id = 2, Amount = 18, Location = stAddresses[1] }, + new StOrder{ RegId = "A9", Id = 5, Amount = 87, Location = stAddresses[0] } }, - new StCustomer + DynamicProperties = new Dictionary + { + { "Detail", "Regular" } + } + }, + new StCustomer + { + Id = 3, + Name = "Apply", + Age = 1001, + Birthday = new DateTimeOffset(1948, 12, 6, 9, 11, 14, 5, TimeSpan.Zero), + MagicNumber = -90, + PhoneNumbers = new [] { "A-123", "A-819" }, + FavoritePlace = stAddresses[0], + Orders = new List { - Id = 3, - Name = "Apply", - Age = 1001, - Birthday = new DateTimeOffset(1948, 12, 6, 9, 11, 14, 5, TimeSpan.Zero), - MagicNumber = -90, - PhoneNumbers = new [] { "A-123", "A-819" }, - FavoritePlace = stAddresses[0], - Orders = new List - { - new StOrder{ RegId = "A8", Id = 3, Amount = 78, Location = stAddresses[2] } - }, - DynamicProperties = new Dictionary - { - { "Detail", "Regular" } - } + new StOrder{ RegId = "A8", Id = 3, Amount = 78, Location = stAddresses[2] } }, - new StCustomer + DynamicProperties = new Dictionary + { + { "Detail", "Regular" } + } + }, + new StCustomer + { + Id = 4, + Name = "Apple", + Age = 11, + Birthday = new DateTimeOffset(2024, 2, 12, 3, 5, 8, 13, TimeSpan.Zero), + MagicNumber = -84, + PhoneNumbers = new [] { "A-444", "A-888", "A-111", "A-446" }, + FavoritePlace = stAddresses[2], + Orders = new List { - Id = 4, - Name = "Apple", - Age = 11, - Birthday = new DateTimeOffset(2024, 2, 12, 3, 5, 8, 13, TimeSpan.Zero), - MagicNumber = -84, - PhoneNumbers = new [] { "A-444", "A-888", "A-111", "A-446" }, - FavoritePlace = stAddresses[2], - Orders = new List - { - new StOrder{ RegId = "A8", Id = 31, Amount = 78, Location = stAddresses[2] } - }, - DynamicProperties = new Dictionary - { - { "Detail", "Vip" } - } + new StOrder{ RegId = "A8", Id = 31, Amount = 78, Location = stAddresses[2] } }, - new StCustomer + DynamicProperties = new Dictionary { - Id = 5, - Name = "Kare", - Age = 101, - Birthday = new DateTimeOffset(1978, 11, 15, 13, 15, 18, 23, TimeSpan.Zero), - MagicNumber = -85, - PhoneNumbers = new [] { "K-023", "K-919", "K-119", "K-745" }, - FavoritePlace = stAddresses[1], - Orders = new List - { - new StOrder{ RegId = "A9", Id = 83, Amount = 14, Location = stAddresses[1] }, - new StOrder{ RegId = "A9", Id = 65, Amount = 42, Location = stAddresses[0] } - }, - DynamicProperties = new Dictionary - { - { "Detail", "Vip" } - } + { "Detail", "Vip" } } - }; - - List orders = new List(); - foreach (var c in _stCustomers) + }, + new StCustomer { - orders.AddRange(c.Orders); + Id = 5, + Name = "Kare", + Age = 101, + Birthday = new DateTimeOffset(1978, 11, 15, 13, 15, 18, 23, TimeSpan.Zero), + MagicNumber = -85, + PhoneNumbers = new [] { "K-023", "K-919", "K-119", "K-745" }, + FavoritePlace = stAddresses[1], + Orders = new List + { + new StOrder{ RegId = "A9", Id = 83, Amount = 14, Location = stAddresses[1] }, + new StOrder{ RegId = "A9", Id = 65, Amount = 42, Location = stAddresses[0] } + }, + DynamicProperties = new Dictionary + { + { "Detail", "Vip" } + } } + }; - _stOrders = orders; - } - #endregion - - [EnableQuery(PageSize = 2)] - [HttpGet("/odata/customers")] - public IActionResult Get() + List orders = new List(); + foreach (var c in _stCustomers) { - return Ok(_stCustomers); + orders.AddRange(c.Orders); } - [EnableQuery(PageSize = 2)] - [HttpGet("/odata/customers/{id}")] - public IActionResult Get(int id) - { - StCustomer customer = _stCustomers.FirstOrDefault(c => c.Id == id); - if (customer == null) - { - return NotFound(); - } + _stOrders = orders; + } + #endregion - return Ok(customer); - } + [EnableQuery(PageSize = 2)] + [HttpGet("/odata/customers")] + public IActionResult Get() + { + return Ok(_stCustomers); + } - [EnableQuery(PageSize = 2)] - [HttpGet("/odata/orders")] - public IActionResult GetOrders() + [EnableQuery(PageSize = 2)] + [HttpGet("/odata/customers/{id}")] + public IActionResult Get(int id) + { + StCustomer customer = _stCustomers.FirstOrDefault(c => c.Id == id); + if (customer == null) { - return Ok(_stOrders); + return NotFound(); } - [EnableQuery] - [HttpGet("all/customers")] // no page size, used to get all customers for reference - public IActionResult GetAllCustomers() - { - return Ok(_stCustomers); - } + return Ok(customer); + } - [EnableQuery] - [HttpGet("all/orders")] // no page size, used to get all orders for reference - public IActionResult GetAllOrders() - { - return Ok(_stOrders); - } + [EnableQuery(PageSize = 2)] + [HttpGet("/odata/orders")] + public IActionResult GetOrders() + { + return Ok(_stOrders); + } + + [EnableQuery] + [HttpGet("all/customers")] // no page size, used to get all customers for reference + public IActionResult GetAllCustomers() + { + return Ok(_stCustomers); + } + + [EnableQuery] + [HttpGet("all/orders")] // no page size, used to get all orders for reference + public IActionResult GetAllOrders() + { + return Ok(_stOrders); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/SkipToken/SkipTokenDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/SkipToken/SkipTokenDataModel.cs index 5f1bc642e..26ce62241 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/SkipToken/SkipTokenDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/SkipToken/SkipTokenDataModel.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -8,50 +8,49 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.SkipToken +namespace Microsoft.AspNetCore.OData.E2E.Tests.SkipToken; + +public class StCustomer { - public class StCustomer - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public int Age { get; set; } + public int Age { get; set; } - public DateTimeOffset Birthday { get; set; } + public DateTimeOffset Birthday { get; set; } - public int MagicNumber { get; set; } // for negative value test + public int MagicNumber { get; set; } // for negative value test - public StAddress FavoritePlace { get; set; } + public StAddress FavoritePlace { get; set; } - public IList PhoneNumbers { get; set; } + public IList PhoneNumbers { get; set; } - public IList Orders { get; set; } + public IList Orders { get; set; } - public IDictionary DynamicProperties { get; set; } - } + public IDictionary DynamicProperties { get; set; } +} - public class StOrder - { - public string RegId { get; set; } +public class StOrder +{ + public string RegId { get; set; } - public int Id { get; set; } + public int Id { get; set; } - public int Amount { get; set; } + public int Amount { get; set; } - public StAddress Location { get; set; } + public StAddress Location { get; set; } - public IDictionary DynamicProperties { get; set; } - } + public IDictionary DynamicProperties { get; set; } +} - public class StAddress - { - public string State { get; set; } +public class StAddress +{ + public string State { get; set; } - public string City { get; set; } + public string City { get; set; } - public int ZipCode { get; set; } + public int ZipCode { get; set; } - public IDictionary DynamicProperties { get; set; } - } + public IDictionary DynamicProperties { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/SkipToken/SkipTokenQueryTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/SkipToken/SkipTokenQueryTests.cs index 9755a8d86..e9542bb60 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/SkipToken/SkipTokenQueryTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/SkipToken/SkipTokenQueryTests.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -20,187 +20,237 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.SkipToken +namespace Microsoft.AspNetCore.OData.E2E.Tests.SkipToken; + +public class SkipTokenQueryTests : WebApiTestBase { - public class SkipTokenQueryTests : WebApiTestBase + public SkipTokenQueryTests(WebApiTestFixture fixture) + : base(fixture) { - public SkipTokenQueryTests(WebApiTestFixture fixture) - : base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(SkipTokenControllers), typeof(MetadataController)); - services.AddControllers() - .AddOData( - opt => opt.AddRouteComponents("all", GetEdmModel()) - .AddRouteComponents("odata", GetEdmModel()) - .Count().Filter().OrderBy().Expand().SetMaxTop(null).Select().SkipToken()); - } + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(SkipTokenControllers), typeof(MetadataController)); + services.AddControllers() + .AddOData( + opt => opt.AddRouteComponents("all", GetEdmModel()) + .AddRouteComponents("odata", GetEdmModel()) + .Count().Filter().OrderBy().Expand().SetMaxTop(null).Select().SkipToken()); + } - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Orders").EntityType.HasKey(c => new { c.RegId, c.Id}); - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Orders").EntityType.HasKey(c => new { c.RegId, c.Id}); + return builder.GetEdmModel(); + } - public static TheoryDataSet Customers_SkipTokenGeneratorCases - => new TheoryDataSet - { - { - "odata/customers", - new int[] { 1, 2 }, - "http://localhost/odata/customers?$skiptoken=Id-2" - }, - { - "odata/customers?$orderby=id", - new int[] { 1, 2 }, - "http://localhost/odata/customers?$orderby=id&$skiptoken=Id-2" - }, - { - "odata/customers?$orderby=id asc", - new int[] { 1, 2 }, - "http://localhost/odata/customers?$orderby=id%20asc&$skiptoken=Id-2" - }, - { - "odata/customers?$orderby=id desc", - new int[] { 5, 4 }, - "http://localhost/odata/customers?$orderby=id%20desc&$skiptoken=Id-4" - }, - { - "odata/customers?$orderby=favoritePlace/state", - new int[] { 2, 4 }, - "http://localhost/odata/customers?$orderby=favoritePlace%2Fstate&$skiptoken=%27AJ%27,Id-4" - }, - { - "odata/customers?$orderby=substring(favoritePlace/state,1,1)", - new int[] { 3, 2 }, - "http://localhost/odata/customers?$orderby=substring%28favoritePlace%2Fstate%2C1%2C1%29&$skiptoken=%27J%27,Id-2" - }, - { - "odata/customers?$orderby=substring(favoritePlace/state,1,1),substring(name,1,2)", - new int[] { 3, 2 }, - "http://localhost/odata/customers?$orderby=substring%28favoritePlace%2Fstate%2C1%2C1%29%2Csubstring%28name%2C1%2C2%29&$skiptoken=%27J%27,%27ar%27,Id-2" - }, - { - "odata/customers?$orderby=phoneNumbers/$count", - new int[] { 2, 3 }, - "http://localhost/odata/customers?$orderby=phoneNumbers%2F%24count&$skiptoken=2,Id-3" - }, - { - "odata/customers?$orderby=tolower(name),magic&$compute=age div id as magic", - new int[] { 4, 3 }, - "http://localhost/odata/customers?$orderby=tolower%28name%29%2Cmagic&$compute=age%20div%20id%20as%20magic&$skiptoken=%27apply%27,magic-333,Id-3" - }, - { - "odata/customers?$orderby=tolower(name)&$select=id,age", - new int[] { 4, 3 }, - "http://localhost/odata/customers?$orderby=tolower%28name%29&$select=id%2Cage&$skiptoken=%27apply%27,Id-3" - }, - { - "odata/customers?$orderby=tolower(Detail)&$select=id,age", - new int[] { 2, 3 }, - "http://localhost/odata/customers?$orderby=tolower%28Detail%29&$select=id%2Cage&$skiptoken=%27regular%27,Id-3" - }, - { - "odata/customers?$orderby=birthday&$select=id,birthday", - new int[] { 3, 5 }, - "http://localhost/odata/customers?$orderby=birthday&$select=id%2Cbirthday&$skiptoken=Birthday-1978-11-15T13%3A15%3A18.023Z,Id-5" - }, - { - "odata/customers?$orderby=year(birthday) desc&$select=id,birthday", - new int[] { 4, 2 }, - "http://localhost/odata/customers?$orderby=year%28birthday%29%20desc&$select=id%2Cbirthday&$skiptoken=2001,Id-2" - }, - { - "odata/customers?$orderby=magicNumber desc&$select=id,magicNumber", - new int[] { 4, 5 }, - "http://localhost/odata/customers?$orderby=magicNumber%20desc&$select=id%2CmagicNumber&$skiptoken=MagicNumber--85,Id-5" - } - }; - - [Theory] - [MemberData(nameof(Customers_SkipTokenGeneratorCases))] - public async Task EnableSkipToken_GenerateNextLinkWithSkipToken_ForSingleKey(string requestUri, int[] ids, string nextLink) + public static TheoryDataSet Customers_SkipTokenGeneratorCases + => new TheoryDataSet { - // Arrange - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; + { + "odata/customers", + new int[] { 1, 2 }, + "http://localhost/odata/customers?$skiptoken=Id-2" + }, + { + "odata/customers?$orderby=id", + new int[] { 1, 2 }, + "http://localhost/odata/customers?$orderby=id&$skiptoken=Id-2" + }, + { + "odata/customers?$orderby=id asc", + new int[] { 1, 2 }, + "http://localhost/odata/customers?$orderby=id%20asc&$skiptoken=Id-2" + }, + { + "odata/customers?$orderby=id desc", + new int[] { 5, 4 }, + "http://localhost/odata/customers?$orderby=id%20desc&$skiptoken=Id-4" + }, + { + "odata/customers?$orderby=favoritePlace/state", + new int[] { 2, 4 }, + "http://localhost/odata/customers?$orderby=favoritePlace%2Fstate&$skiptoken=%27AJ%27,Id-4" + }, + { + "odata/customers?$orderby=substring(favoritePlace/state,1,1)", + new int[] { 3, 2 }, + "http://localhost/odata/customers?$orderby=substring%28favoritePlace%2Fstate%2C1%2C1%29&$skiptoken=%27J%27,Id-2" + }, + { + "odata/customers?$orderby=substring(favoritePlace/state,1,1),substring(name,1,2)", + new int[] { 3, 2 }, + "http://localhost/odata/customers?$orderby=substring%28favoritePlace%2Fstate%2C1%2C1%29%2Csubstring%28name%2C1%2C2%29&$skiptoken=%27J%27,%27ar%27,Id-2" + }, + { + "odata/customers?$orderby=phoneNumbers/$count", + new int[] { 2, 3 }, + "http://localhost/odata/customers?$orderby=phoneNumbers%2F%24count&$skiptoken=2,Id-3" + }, + { + "odata/customers?$orderby=tolower(name),magic&$compute=age div id as magic", + new int[] { 4, 3 }, + "http://localhost/odata/customers?$orderby=tolower%28name%29%2Cmagic&$compute=age%20div%20id%20as%20magic&$skiptoken=%27apply%27,magic-333,Id-3" + }, + { + "odata/customers?$orderby=tolower(name)&$select=id,age", + new int[] { 4, 3 }, + "http://localhost/odata/customers?$orderby=tolower%28name%29&$select=id%2Cage&$skiptoken=%27apply%27,Id-3" + }, + { + "odata/customers?$orderby=tolower(Detail)&$select=id,age", + new int[] { 2, 3 }, + "http://localhost/odata/customers?$orderby=tolower%28Detail%29&$select=id%2Cage&$skiptoken=%27regular%27,Id-3" + }, + { + "odata/customers?$orderby=birthday&$select=id,birthday", + new int[] { 3, 5 }, + "http://localhost/odata/customers?$orderby=birthday&$select=id%2Cbirthday&$skiptoken=Birthday-1978-11-15T13%3A15%3A18.023Z,Id-5" + }, + { + "odata/customers?$orderby=year(birthday) desc&$select=id,birthday", + new int[] { 4, 2 }, + "http://localhost/odata/customers?$orderby=year%28birthday%29%20desc&$select=id%2Cbirthday&$skiptoken=2001,Id-2" + }, + { + "odata/customers?$orderby=magicNumber desc&$select=id,magicNumber", + new int[] { 4, 5 }, + "http://localhost/odata/customers?$orderby=magicNumber%20desc&$select=id%2CmagicNumber&$skiptoken=MagicNumber--85,Id-5" + } + }; - // Act - response = await client.SendAsync(request); + [Theory] + [MemberData(nameof(Customers_SkipTokenGeneratorCases))] + public async Task EnableSkipToken_GenerateNextLinkWithSkipToken_ForSingleKey(string requestUri, int[] ids, string nextLink) + { + // Arrange + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + // Act + response = await client.SendAsync(request); - JObject payloadBody = await response.Content.ReadAsObject(); - (int[] actualIds, string actualNextLink) = GetActual(payloadBody); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - Assert.True(ids.SequenceEqual(actualIds)); - Assert.Equal(nextLink, actualNextLink); - } + JObject payloadBody = await response.Content.ReadAsObject(); + (int[] actualIds, string actualNextLink) = GetActual(payloadBody); - private static (int[], string nextLink) GetActual(JObject payload) + Assert.True(ids.SequenceEqual(actualIds)); + Assert.Equal(nextLink, actualNextLink); + } + + private static (int[], string nextLink) GetActual(JObject payload) + { + JArray value = payload["value"] as JArray; + Assert.NotNull(value); + + int[] ids = new int[value.Count()]; + for (int i = 0; i < value.Count(); i++) { - JArray value = payload["value"] as JArray; - Assert.NotNull(value); + JObject item = value[i] as JObject; + ids[i] = (int)item["Id"]; + } + + return (ids, (string)payload["@odata.nextLink"]); + } - int[] ids = new int[value.Count()]; - for (int i = 0; i < value.Count(); i++) + public static TheoryDataSet Orders_SkipTokenGeneratorCases + => new TheoryDataSet + { { - JObject item = value[i] as JObject; - ids[i] = (int)item["Id"]; + "odata/orders", + new string[] { "A8|2", "A8|3"}, + "http://localhost/odata/orders?$skiptoken=Id-3,RegId-%27A8%27" + }, + { + "odata/orders?$orderby=id", + new string[] { "A8|2", "A8|3" }, + "http://localhost/odata/orders?$orderby=id&$skiptoken=Id-3,RegId-%27A8%27" + }, + { + "odata/orders?$orderby=RegId desc", + new string[] { "A9|5", "A9|11" }, + "http://localhost/odata/orders?$orderby=RegId%20desc&$skiptoken=RegId-%27A9%27,Id-11" + }, + { + "odata/orders?$orderby=Location/city desc", + new string[] { "A9|5", "A9|11"}, + "http://localhost/odata/orders?$orderby=Location%2Fcity%20desc&$skiptoken=%27Settle%27,Id-11,RegId-%27A9%27" + }, + { + "odata/orders?$orderby=toupper(Location/Region)", + new string[] { "A8|2", "A9|13"}, + "http://localhost/odata/orders?$orderby=toupper%28Location%2FRegion%29&$skiptoken=%27AA%27,Id-13,RegId-%27A9%27" } + }; + + [Theory] + [MemberData(nameof(Orders_SkipTokenGeneratorCases))] + public async Task EnableSkipToken_GenerateNextLinkWithSkipToken_ForCompositeKey(string requestUri, string[] keys, string nextLink) + { + // Arrange + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + HttpClient client = CreateClient(); + HttpResponseMessage response; + + // Act + response = await client.SendAsync(request); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - return (ids, (string)payload["@odata.nextLink"]); + JObject payloadBody = await response.Content.ReadAsObject(); + (string[] actualIds, string actualNextLink) = GetOrderActual(payloadBody); + + Assert.True(keys.SequenceEqual(actualIds)); + Assert.Equal(nextLink, actualNextLink); + } + + private static (string[], string nextLink) GetOrderActual(JObject payload) + { + JArray value = payload["value"] as JArray; + Assert.NotNull(value); + + string[] ids = new string[value.Count()]; + for (int i = 0; i < value.Count(); i++) + { + JObject item = value[i] as JObject; + ids[i] = $"{(string)item["RegId"]}|{(int)item["Id"]}"; } - public static TheoryDataSet Orders_SkipTokenGeneratorCases - => new TheoryDataSet - { - { - "odata/orders", - new string[] { "A8|2", "A8|3"}, - "http://localhost/odata/orders?$skiptoken=Id-3,RegId-%27A8%27" - }, - { - "odata/orders?$orderby=id", - new string[] { "A8|2", "A8|3" }, - "http://localhost/odata/orders?$orderby=id&$skiptoken=Id-3,RegId-%27A8%27" - }, - { - "odata/orders?$orderby=RegId desc", - new string[] { "A9|5", "A9|11" }, - "http://localhost/odata/orders?$orderby=RegId%20desc&$skiptoken=RegId-%27A9%27,Id-11" - }, - { - "odata/orders?$orderby=Location/city desc", - new string[] { "A9|5", "A9|11"}, - "http://localhost/odata/orders?$orderby=Location%2Fcity%20desc&$skiptoken=%27Settle%27,Id-11,RegId-%27A9%27" - }, - { - "odata/orders?$orderby=toupper(Location/Region)", - new string[] { "A8|2", "A9|13"}, - "http://localhost/odata/orders?$orderby=toupper%28Location%2FRegion%29&$skiptoken=%27AA%27,Id-13,RegId-%27A9%27" - } - }; - - [Theory] - [MemberData(nameof(Orders_SkipTokenGeneratorCases))] - public async Task EnableSkipToken_GenerateNextLinkWithSkipToken_ForCompositeKey(string requestUri, string[] keys, string nextLink) + return (ids, (string)payload["@odata.nextLink"]); + } + + [Fact] + public async Task EnableSkipToken_WithSkipToken_CallNextLinkRecursively_ForSingleKey() + { + // Arrange + string[][] expected = new string[3][]; + expected[0] = new string[] { "2|Earth", "3|Apply" }; + expected[1] = new string[] { "1|Mars", "4|Apple" }; + expected[2] = new string[] { "5|Kare" }; + + string requestUri = "odata/customers?$orderby=phoneNumbers/$count&$select=id,name"; + + HttpClient client = CreateClient(); + HttpResponseMessage response; + + int index = 0; + while (requestUri != null) { - // Arrange HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; // Act response = await client.SendAsync(request); @@ -211,153 +261,102 @@ public async Task EnableSkipToken_GenerateNextLinkWithSkipToken_ForCompositeKey( Assert.NotNull(response.Content); JObject payloadBody = await response.Content.ReadAsObject(); - (string[] actualIds, string actualNextLink) = GetOrderActual(payloadBody); + (string[] actualIds, string actualNextLink) = GetActualCustomerInfo(payloadBody); - Assert.True(keys.SequenceEqual(actualIds)); - Assert.Equal(nextLink, actualNextLink); + Assert.True(expected[index].SequenceEqual(actualIds)); + + requestUri = actualNextLink; + index++; } - private static (string[], string nextLink) GetOrderActual(JObject payload) - { - JArray value = payload["value"] as JArray; - Assert.NotNull(value); + Assert.Equal(3, index); + } - string[] ids = new string[value.Count()]; - for (int i = 0; i < value.Count(); i++) - { - JObject item = value[i] as JObject; - ids[i] = $"{(string)item["RegId"]}|{(int)item["Id"]}"; - } + private static (string[], string nextLink) GetActualCustomerInfo(JObject payload) + { + JArray value = payload["value"] as JArray; + Assert.NotNull(value); - return (ids, (string)payload["@odata.nextLink"]); + string[] ids = new string[value.Count()]; + for (int i = 0; i < value.Count(); i++) + { + JObject item = value[i] as JObject; + ids[i] = $"{(int)item["Id"]}|{(string)item["Name"]}"; } - [Fact] - public async Task EnableSkipToken_WithSkipToken_CallNextLinkRecursively_ForSingleKey() + JProperty nextLinkProperty = payload.Property("@odata.nextLink"); + if (nextLinkProperty != null) { - // Arrange - string[][] expected = new string[3][]; - expected[0] = new string[] { "2|Earth", "3|Apply" }; - expected[1] = new string[] { "1|Mars", "4|Apple" }; - expected[2] = new string[] { "5|Kare" }; - - string requestUri = "odata/customers?$orderby=phoneNumbers/$count&$select=id,name"; - - HttpClient client = CreateClient(); - HttpResponseMessage response; - - int index = 0; - while (requestUri != null) - { - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - - // Act - response = await client.SendAsync(request); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - JObject payloadBody = await response.Content.ReadAsObject(); - (string[] actualIds, string actualNextLink) = GetActualCustomerInfo(payloadBody); - - Assert.True(expected[index].SequenceEqual(actualIds)); - - requestUri = actualNextLink; - index++; - } - - Assert.Equal(3, index); + return (ids, (string)nextLinkProperty.Value); } - - private static (string[], string nextLink) GetActualCustomerInfo(JObject payload) + else { - JArray value = payload["value"] as JArray; - Assert.NotNull(value); + return (ids, null); + } + } - string[] ids = new string[value.Count()]; - for (int i = 0; i < value.Count(); i++) - { - JObject item = value[i] as JObject; - ids[i] = $"{(int)item["Id"]}|{(string)item["Name"]}"; - } + [Fact] + public async Task EnableSkipToken_WithSkipToken_CallNextLinkRecursively_ForCompositeKey() + { + // Arrange + string[][] expected = new string[5][]; + expected[0] = new string[] { "A9|5|87|435|Settle", "A9|11|66|726|Settle" }; + expected[1] = new string[] { "A9|65|42|2730|Settle", "A8|3|78|234|Reedy" }; + expected[2] = new string[] { "A9|12|42|504|Reedy", "A8|31|78|2418|Reedy" }; + expected[3] = new string[] { "A8|2|18|36|Perry", "A9|13|17|221|Perry" }; + expected[4] = new string[] { "A9|83|14|1162|Perry" }; - JProperty nextLinkProperty = payload.Property("@odata.nextLink"); - if (nextLinkProperty != null) - { - return (ids, (string)nextLinkProperty.Value); - } - else - { - return (ids, null); - } - } + string requestUri = "odata/orders?$orderby=Location/city desc,magic&$compute=id mul amount as magic,location/city as city&$select=regid,id,amount,magic,city"; - [Fact] - public async Task EnableSkipToken_WithSkipToken_CallNextLinkRecursively_ForCompositeKey() - { - // Arrange - string[][] expected = new string[5][]; - expected[0] = new string[] { "A9|5|87|435|Settle", "A9|11|66|726|Settle" }; - expected[1] = new string[] { "A9|65|42|2730|Settle", "A8|3|78|234|Reedy" }; - expected[2] = new string[] { "A9|12|42|504|Reedy", "A8|31|78|2418|Reedy" }; - expected[3] = new string[] { "A8|2|18|36|Perry", "A9|13|17|221|Perry" }; - expected[4] = new string[] { "A9|83|14|1162|Perry" }; + HttpClient client = CreateClient(); + HttpResponseMessage response; - string requestUri = "odata/orders?$orderby=Location/city desc,magic&$compute=id mul amount as magic,location/city as city&$select=regid,id,amount,magic,city"; + int index = 0; + while (requestUri != null) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - HttpClient client = CreateClient(); - HttpResponseMessage response; + // Act + response = await client.SendAsync(request); - int index = 0; - while (requestUri != null) - { - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - // Act - response = await client.SendAsync(request); + JObject payloadBody = await response.Content.ReadAsObject(); + (string[] actualIds, string actualNextLink) = GetActualOrderInfo(payloadBody); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + Assert.True(expected[index].SequenceEqual(actualIds)); - JObject payloadBody = await response.Content.ReadAsObject(); - (string[] actualIds, string actualNextLink) = GetActualOrderInfo(payloadBody); + requestUri = actualNextLink; + index++; + } - Assert.True(expected[index].SequenceEqual(actualIds)); + Assert.Equal(5, index); + } - requestUri = actualNextLink; - index++; - } + private static (string[], string nextLink) GetActualOrderInfo(JObject payload) + { + JArray value = payload["value"] as JArray; + Assert.NotNull(value); - Assert.Equal(5, index); + string[] ids = new string[value.Count()]; + for (int i = 0; i < value.Count(); i++) + { + JObject item = value[i] as JObject; + ids[i] = $"{(string)item["RegId"]}|{(int)item["Id"]}|{(int)item["Amount"]}|{(int)item["magic"]}|{(string)item["city"]}"; } - private static (string[], string nextLink) GetActualOrderInfo(JObject payload) + JProperty nextLinkProperty = payload.Property("@odata.nextLink"); + if (nextLinkProperty != null) { - JArray value = payload["value"] as JArray; - Assert.NotNull(value); - - string[] ids = new string[value.Count()]; - for (int i = 0; i < value.Count(); i++) - { - JObject item = value[i] as JObject; - ids[i] = $"{(string)item["RegId"]}|{(int)item["Id"]}|{(int)item["Amount"]}|{(int)item["magic"]}|{(string)item["city"]}"; - } - - JProperty nextLinkProperty = payload.Property("@odata.nextLink"); - if (nextLinkProperty != null) - { - return (ids, (string)nextLinkProperty.Value); - } - else - { - return (ids, null); - } + return (ids, (string)nextLinkProperty.Value); + } + else + { + return (ids, null); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialController.cs index 6d8ede04b..be93d7c24 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialController.cs @@ -14,82 +14,81 @@ using Microsoft.Spatial; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Spatial +namespace Microsoft.AspNetCore.OData.E2E.Tests.Spatial; + +public class SpatialCustomersController : TestODataController { - public class SpatialCustomersController : TestODataController - { - private static readonly IEnumerable Customers; + private static readonly IEnumerable Customers; - static SpatialCustomersController() + static SpatialCustomersController() + { + if (Customers == null) { - if (Customers == null) + string[] names = { "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune" }; + + Customers = Enumerable.Range(1, 7).Select(e => new SpatialCustomer { - string[] names = { "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune" }; - - Customers = Enumerable.Range(1, 7).Select(e => new SpatialCustomer - { - CustomerId = e, - Name = names[e - 1], - Location = GeographyFactory.Point(e, e, e, e), - Region = (e != 2 - ? GeographyFactory.LineString(1 + e, 2 + e).LineTo(3 * e, 4 * e) - : GeographyFactory.LineString(55, 66, null, 0).LineTo(33, 44, null, 12.3)), - HomePoint = GeometryFactory.Point(e * 2, e * 5) - }); - } + CustomerId = e, + Name = names[e - 1], + Location = GeographyFactory.Point(e, e, e, e), + Region = (e != 2 + ? GeographyFactory.LineString(1 + e, 2 + e).LineTo(3 * e, 4 * e) + : GeographyFactory.LineString(55, 66, null, 0).LineTo(33, 44, null, 12.3)), + HomePoint = GeometryFactory.Point(e * 2, e * 5) + }); } + } - [EnableQuery] - public IActionResult Get() - { - return Ok(Customers); - } + [EnableQuery] + public IActionResult Get() + { + return Ok(Customers); + } - [EnableQuery] - public IActionResult Get(int key) + [EnableQuery] + public IActionResult Get(int key) + { + SpatialCustomer customer = Customers.FirstOrDefault(c => c.CustomerId == key); + if (customer == null) { - SpatialCustomer customer = Customers.FirstOrDefault(c => c.CustomerId == key); - if (customer == null) - { - return NotFound(); - } - - return Ok(customer); + return NotFound(); } - [EnableQuery] - public IActionResult Post([FromBody]SpatialCustomer customer) - { - // Assert part - Assert.Equal("Sam", customer.Name); + return Ok(customer); + } - Assert.Equal(7, customer.Location.Longitude); - Assert.Equal(8, customer.Location.Latitude); - Assert.Equal(9, customer.Location.Z); - Assert.Equal(10, customer.Location.M); + [EnableQuery] + public IActionResult Post([FromBody]SpatialCustomer customer) + { + // Assert part + Assert.Equal("Sam", customer.Name); - return Created(customer); - } + Assert.Equal(7, customer.Location.Longitude); + Assert.Equal(8, customer.Location.Latitude); + Assert.Equal(9, customer.Location.Z); + Assert.Equal(10, customer.Location.M); - [EnableQuery] - public IActionResult Patch(int key, Delta customer) - { - // Assert part - Assert.Equal(3, key); + return Created(customer); + } - Assert.Equal(new[] {"Location"}, customer.GetChangedPropertyNames()); + [EnableQuery] + public IActionResult Patch(int key, Delta customer) + { + // Assert part + Assert.Equal(3, key); - object value; - customer.TryGetPropertyValue("Location", out value); + Assert.Equal(new[] {"Location"}, customer.GetChangedPropertyNames()); - GeographyPoint point = value as GeographyPoint; - Assert.NotNull(point); - Assert.Equal(7, point.Longitude); - Assert.Equal(8, point.Latitude); - Assert.Equal(9, point.Z); - Assert.Equal(10, point.M); + object value; + customer.TryGetPropertyValue("Location", out value); - return Ok(); - } + GeographyPoint point = value as GeographyPoint; + Assert.NotNull(point); + Assert.Equal(7, point.Longitude); + Assert.Equal(8, point.Latitude); + Assert.Equal(9, point.Z); + Assert.Equal(10, point.M); + + return Ok(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialEdmModel.cs index a1102baee..a56fce61c 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialEdmModel.cs @@ -8,22 +8,21 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Spatial +namespace Microsoft.AspNetCore.OData.E2E.Tests.Spatial; + +public static class IsofEdmModel { - public static class IsofEdmModel - { - private static IEdmModel _model; + private static IEdmModel _model; - public static IEdmModel GetEdmModel() + public static IEdmModel GetEdmModel() + { + if (_model != null) { - if (_model != null) - { - return _model; - } - - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("SpatialCustomers"); - return _model = builder.GetEdmModel(); + return _model; } + + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("SpatialCustomers"); + return _model = builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialFactory.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialFactory.cs index a7df09193..69eff8e31 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialFactory.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialFactory.cs @@ -10,1352 +10,1351 @@ using System.Diagnostics; using Microsoft.Spatial; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Spatial +namespace Microsoft.AspNetCore.OData.E2E.Tests.Spatial; + +/// +/// Geography Factory +/// +public static class GeographyFactory { + #region Point and MultiPoint + /// - /// Geography Factory + /// Create a Geography Point /// - public static class GeographyFactory + /// The CoordinateSystem + /// The latitude value + /// The longitude value + /// The Z value + /// The M value + /// A Geography Point Factory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public static GeographyFactory Point(CoordinateSystem coordinateSystem, double latitude, double longitude, double? z, double? m) { - #region Point and MultiPoint - - /// - /// Create a Geography Point - /// - /// The CoordinateSystem - /// The latitude value - /// The longitude value - /// The Z value - /// The M value - /// A Geography Point Factory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public static GeographyFactory Point(CoordinateSystem coordinateSystem, double latitude, double longitude, double? z, double? m) - { - return new GeographyFactory(coordinateSystem).Point(latitude, longitude, z, m); - } + return new GeographyFactory(coordinateSystem).Point(latitude, longitude, z, m); + } - /// - /// Create a Geography Point - /// - /// The latitude value - /// The longitude value - /// The Z value - /// The M value - /// A Geography Point Factory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public static GeographyFactory Point(double latitude, double longitude, double? z, double? m) - { - return Point(CoordinateSystem.DefaultGeography, latitude, longitude, z, m); - } + /// + /// Create a Geography Point + /// + /// The latitude value + /// The longitude value + /// The Z value + /// The M value + /// A Geography Point Factory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public static GeographyFactory Point(double latitude, double longitude, double? z, double? m) + { + return Point(CoordinateSystem.DefaultGeography, latitude, longitude, z, m); + } - /// - /// Create a Geography Point - /// - /// The CoordinateSystem - /// The latitude value - /// The longitude value - /// A Geography Point Factory - public static GeographyFactory Point(CoordinateSystem coordinateSystem, double latitude, double longitude) - { - return Point(coordinateSystem, latitude, longitude, null, null); - } + /// + /// Create a Geography Point + /// + /// The CoordinateSystem + /// The latitude value + /// The longitude value + /// A Geography Point Factory + public static GeographyFactory Point(CoordinateSystem coordinateSystem, double latitude, double longitude) + { + return Point(coordinateSystem, latitude, longitude, null, null); + } - /// - /// Create a Geography Point - /// - /// The latitude value - /// The longitude value - /// A Geography Point Factory - public static GeographyFactory Point(double latitude, double longitude) - { - return Point(CoordinateSystem.DefaultGeography, latitude, longitude, null, null); - } + /// + /// Create a Geography Point + /// + /// The latitude value + /// The longitude value + /// A Geography Point Factory + public static GeographyFactory Point(double latitude, double longitude) + { + return Point(CoordinateSystem.DefaultGeography, latitude, longitude, null, null); + } - /// - /// Create a factory with an empty Geography Point - /// - /// The CoordinateSystem - /// A Geography Point Factory - public static GeographyFactory Point(CoordinateSystem coordinateSystem) - { - return new GeographyFactory(coordinateSystem).Point(); - } + /// + /// Create a factory with an empty Geography Point + /// + /// The CoordinateSystem + /// A Geography Point Factory + public static GeographyFactory Point(CoordinateSystem coordinateSystem) + { + return new GeographyFactory(coordinateSystem).Point(); + } - /// - /// Create a factory with an empty Geography Point - /// - /// A Geography Point Factory - public static GeographyFactory Point() - { - return Point(CoordinateSystem.DefaultGeography); - } + /// + /// Create a factory with an empty Geography Point + /// + /// A Geography Point Factory + public static GeographyFactory Point() + { + return Point(CoordinateSystem.DefaultGeography); + } - /// - /// Create a Geography MultiPoint - /// - /// The CoordinateSystem - /// A Geography MultiPoint Factory - public static GeographyFactory MultiPoint(CoordinateSystem coordinateSystem) - { - return new GeographyFactory(coordinateSystem).MultiPoint(); - } + /// + /// Create a Geography MultiPoint + /// + /// The CoordinateSystem + /// A Geography MultiPoint Factory + public static GeographyFactory MultiPoint(CoordinateSystem coordinateSystem) + { + return new GeographyFactory(coordinateSystem).MultiPoint(); + } - /// - /// Create a Geography MultiPoint - /// - /// A Geography MultiPoint Factory - public static GeographyFactory MultiPoint() - { - return MultiPoint(CoordinateSystem.DefaultGeography); - } + /// + /// Create a Geography MultiPoint + /// + /// A Geography MultiPoint Factory + public static GeographyFactory MultiPoint() + { + return MultiPoint(CoordinateSystem.DefaultGeography); + } - #endregion - - #region LineString and MultiLineString - - /// - /// Create a Geography LineString with a starting position - /// - /// The CoordinateSystem - /// The latitude value - /// The longitude value - /// The Z value - /// The M value - /// A Geography LineString Factory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public static GeographyFactory LineString(CoordinateSystem coordinateSystem, double latitude, double longitude, double? z, double? m) - { - return new GeographyFactory(coordinateSystem).LineString(latitude, longitude, z, m); - } + #endregion - /// - /// Create a Geography LineString with a starting position - /// - /// The latitude value - /// The longitude value - /// The Z value - /// The M value - /// A Geography LineString Factory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public static GeographyFactory LineString(double latitude, double longitude, double? z, double? m) - { - return LineString(CoordinateSystem.DefaultGeography, latitude, longitude, z, m); - } + #region LineString and MultiLineString - /// - /// Create a Geography LineString with a starting position - /// - /// The CoordinateSystem - /// The latitude value - /// The longitude value - /// A Geography LineString Factory - public static GeographyFactory LineString(CoordinateSystem coordinateSystem, double latitude, double longitude) - { - return LineString(coordinateSystem, latitude, longitude, null, null); - } + /// + /// Create a Geography LineString with a starting position + /// + /// The CoordinateSystem + /// The latitude value + /// The longitude value + /// The Z value + /// The M value + /// A Geography LineString Factory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public static GeographyFactory LineString(CoordinateSystem coordinateSystem, double latitude, double longitude, double? z, double? m) + { + return new GeographyFactory(coordinateSystem).LineString(latitude, longitude, z, m); + } - /// - /// Create a Geography LineString with a starting position - /// - /// The latitude value - /// The longitude value - /// A Geography LineString Factory - public static GeographyFactory LineString(double latitude, double longitude) - { - return LineString(CoordinateSystem.DefaultGeography, latitude, longitude, null, null); - } + /// + /// Create a Geography LineString with a starting position + /// + /// The latitude value + /// The longitude value + /// The Z value + /// The M value + /// A Geography LineString Factory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public static GeographyFactory LineString(double latitude, double longitude, double? z, double? m) + { + return LineString(CoordinateSystem.DefaultGeography, latitude, longitude, z, m); + } - /// - /// Create an empty Geography LineString - /// - /// The CoordinateSystem - /// A Geography LineString Factory - public static GeographyFactory LineString(CoordinateSystem coordinateSystem) - { - return new GeographyFactory(coordinateSystem).LineString(); - } + /// + /// Create a Geography LineString with a starting position + /// + /// The CoordinateSystem + /// The latitude value + /// The longitude value + /// A Geography LineString Factory + public static GeographyFactory LineString(CoordinateSystem coordinateSystem, double latitude, double longitude) + { + return LineString(coordinateSystem, latitude, longitude, null, null); + } - /// - /// Create an empty Geography LineString - /// - /// A Geography LineString Factory - public static GeographyFactory LineString() - { - return LineString(CoordinateSystem.DefaultGeography); - } + /// + /// Create a Geography LineString with a starting position + /// + /// The latitude value + /// The longitude value + /// A Geography LineString Factory + public static GeographyFactory LineString(double latitude, double longitude) + { + return LineString(CoordinateSystem.DefaultGeography, latitude, longitude, null, null); + } - /// - /// Create a Geography MultiLineString - /// - /// The CoordinateSystem - /// A Geography MultiLineString Factory - public static GeographyFactory MultiLineString(CoordinateSystem coordinateSystem) - { - return new GeographyFactory(coordinateSystem).MultiLineString(); - } + /// + /// Create an empty Geography LineString + /// + /// The CoordinateSystem + /// A Geography LineString Factory + public static GeographyFactory LineString(CoordinateSystem coordinateSystem) + { + return new GeographyFactory(coordinateSystem).LineString(); + } - /// - /// Create a Geography MultiLineString - /// - /// A Geography MultiLineString Factory - public static GeographyFactory MultiLineString() - { - return MultiLineString(CoordinateSystem.DefaultGeography); - } + /// + /// Create an empty Geography LineString + /// + /// A Geography LineString Factory + public static GeographyFactory LineString() + { + return LineString(CoordinateSystem.DefaultGeography); + } - #endregion + /// + /// Create a Geography MultiLineString + /// + /// The CoordinateSystem + /// A Geography MultiLineString Factory + public static GeographyFactory MultiLineString(CoordinateSystem coordinateSystem) + { + return new GeographyFactory(coordinateSystem).MultiLineString(); + } - #region Polygon + /// + /// Create a Geography MultiLineString + /// + /// A Geography MultiLineString Factory + public static GeographyFactory MultiLineString() + { + return MultiLineString(CoordinateSystem.DefaultGeography); + } - /// - /// Create a Geography Polygon - /// - /// The CoordinateSystem - /// A Geography Polygon Factory - public static GeographyFactory Polygon(CoordinateSystem coordinateSystem) - { - return new GeographyFactory(coordinateSystem).Polygon(); - } + #endregion - /// - /// Create a Geography Polygon - /// - /// A Geography Polygon Factory - public static GeographyFactory Polygon() - { - return Polygon(CoordinateSystem.DefaultGeography); - } + #region Polygon - /// - /// Create a Geography MultiPolygon - /// - /// The CoordinateSystem - /// A Geography MultiPolygon Factory - public static GeographyFactory MultiPolygon(CoordinateSystem coordinateSystem) - { - return new GeographyFactory(coordinateSystem).MultiPolygon(); - } + /// + /// Create a Geography Polygon + /// + /// The CoordinateSystem + /// A Geography Polygon Factory + public static GeographyFactory Polygon(CoordinateSystem coordinateSystem) + { + return new GeographyFactory(coordinateSystem).Polygon(); + } - /// - /// Create a Geography MultiPolygon - /// - /// A Geography MultiPolygon Factory - public static GeographyFactory MultiPolygon() - { - return MultiPolygon(CoordinateSystem.DefaultGeography); - } + /// + /// Create a Geography Polygon + /// + /// A Geography Polygon Factory + public static GeographyFactory Polygon() + { + return Polygon(CoordinateSystem.DefaultGeography); + } - #endregion + /// + /// Create a Geography MultiPolygon + /// + /// The CoordinateSystem + /// A Geography MultiPolygon Factory + public static GeographyFactory MultiPolygon(CoordinateSystem coordinateSystem) + { + return new GeographyFactory(coordinateSystem).MultiPolygon(); + } - #region Collection + /// + /// Create a Geography MultiPolygon + /// + /// A Geography MultiPolygon Factory + public static GeographyFactory MultiPolygon() + { + return MultiPolygon(CoordinateSystem.DefaultGeography); + } - /// - /// Create a Geography Collection - /// - /// The CoordinateSystem - /// A Geography Collection Factory - public static GeographyFactory Collection(CoordinateSystem coordinateSystem) - { - return new GeographyFactory(coordinateSystem).Collection(); - } + #endregion - /// - /// Create a Geography Collection - /// - /// A Geography Collection Factory - public static GeographyFactory Collection() - { - return Collection(CoordinateSystem.DefaultGeography); - } + #region Collection - #endregion + /// + /// Create a Geography Collection + /// + /// The CoordinateSystem + /// A Geography Collection Factory + public static GeographyFactory Collection(CoordinateSystem coordinateSystem) + { + return new GeographyFactory(coordinateSystem).Collection(); } /// - /// Geometry Factory + /// Create a Geography Collection /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleClass", Justification = "Required for this file.")] - public static class GeometryFactory + /// A Geography Collection Factory + public static GeographyFactory Collection() { - #region Point and MultiPoint - - /// - /// Create a Geometry Point - /// - /// The CoordinateSystem - /// The X value - /// The Y value - /// The Z value - /// The M value - /// A Geometry Point Factory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public static GeometryFactory Point(CoordinateSystem coordinateSystem, double x, double y, double? z, double? m) - { - return new GeometryFactory(coordinateSystem).Point(x, y, z, m); - } - - /// - /// Create a Geometry Point - /// - /// The X value - /// The Y value - /// The Z value - /// The M value - /// A Geometry Point Factory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public static GeometryFactory Point(double x, double y, double? z, double? m) - { - return Point(CoordinateSystem.DefaultGeometry, x, y, z, m); - } + return Collection(CoordinateSystem.DefaultGeography); + } - /// - /// Create a Geometry Point - /// - /// The CoordinateSystem - /// The X value - /// The Y value - /// A Geometry Point Factory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public static GeometryFactory Point(CoordinateSystem coordinateSystem, double x, double y) - { - return Point(coordinateSystem, x, y, null, null); - } + #endregion +} - /// - /// Create a Geometry Point - /// - /// The X value - /// The Y value - /// A Geometry Point Factory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public static GeometryFactory Point(double x, double y) - { - return Point(CoordinateSystem.DefaultGeometry, x, y, null, null); - } +/// +/// Geometry Factory +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleClass", Justification = "Required for this file.")] +public static class GeometryFactory +{ + #region Point and MultiPoint - /// - /// Create a factory with an empty Geometry Point - /// - /// The CoordinateSystem - /// A Geometry Point Factory - public static GeometryFactory Point(CoordinateSystem coordinateSystem) - { - return new GeometryFactory(coordinateSystem).Point(); - } + /// + /// Create a Geometry Point + /// + /// The CoordinateSystem + /// The X value + /// The Y value + /// The Z value + /// The M value + /// A Geometry Point Factory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public static GeometryFactory Point(CoordinateSystem coordinateSystem, double x, double y, double? z, double? m) + { + return new GeometryFactory(coordinateSystem).Point(x, y, z, m); + } - /// - /// Create a factory with an empty Geometry Point - /// - /// A Geometry Point Factory - public static GeometryFactory Point() - { - return Point(CoordinateSystem.DefaultGeometry); - } + /// + /// Create a Geometry Point + /// + /// The X value + /// The Y value + /// The Z value + /// The M value + /// A Geometry Point Factory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public static GeometryFactory Point(double x, double y, double? z, double? m) + { + return Point(CoordinateSystem.DefaultGeometry, x, y, z, m); + } - /// - /// Create a Geometry MultiPoint - /// - /// The CoordinateSystem - /// A Geometry MultiPoint Factory - public static GeometryFactory MultiPoint(CoordinateSystem coordinateSystem) - { - return new GeometryFactory(coordinateSystem).MultiPoint(); - } + /// + /// Create a Geometry Point + /// + /// The CoordinateSystem + /// The X value + /// The Y value + /// A Geometry Point Factory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public static GeometryFactory Point(CoordinateSystem coordinateSystem, double x, double y) + { + return Point(coordinateSystem, x, y, null, null); + } - /// - /// Create a Geometry MultiPoint - /// - /// A Geometry MultiPoint Factory - public static GeometryFactory MultiPoint() - { - return MultiPoint(CoordinateSystem.DefaultGeometry); - } + /// + /// Create a Geometry Point + /// + /// The X value + /// The Y value + /// A Geometry Point Factory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public static GeometryFactory Point(double x, double y) + { + return Point(CoordinateSystem.DefaultGeometry, x, y, null, null); + } - #endregion - - #region LineString and MultiLineString - - /// - /// Create a Geometry LineString with a starting position - /// - /// The CoordinateSystem - /// The X value - /// The Y value - /// The Z value - /// The M value - /// A Geometry LineString Factory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public static GeometryFactory LineString(CoordinateSystem coordinateSystem, double x, double y, double? z, double? m) - { - return new GeometryFactory(coordinateSystem).LineString(x, y, z, m); - } + /// + /// Create a factory with an empty Geometry Point + /// + /// The CoordinateSystem + /// A Geometry Point Factory + public static GeometryFactory Point(CoordinateSystem coordinateSystem) + { + return new GeometryFactory(coordinateSystem).Point(); + } - /// - /// Create a Geometry LineString with a starting position - /// - /// The X value - /// The Y value - /// The Z value - /// The M value - /// A Geometry LineString Factory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public static GeometryFactory LineString(double x, double y, double? z, double? m) - { - return LineString(CoordinateSystem.DefaultGeometry, x, y, z, m); - } + /// + /// Create a factory with an empty Geometry Point + /// + /// A Geometry Point Factory + public static GeometryFactory Point() + { + return Point(CoordinateSystem.DefaultGeometry); + } - /// - /// Create a Geometry LineString with a starting position - /// - /// The coordinate system - /// The X value - /// The Y value - /// A Geometry LineString Factory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public static GeometryFactory LineString(CoordinateSystem coordinateSystem, double x, double y) - { - return LineString(coordinateSystem, x, y, null, null); - } + /// + /// Create a Geometry MultiPoint + /// + /// The CoordinateSystem + /// A Geometry MultiPoint Factory + public static GeometryFactory MultiPoint(CoordinateSystem coordinateSystem) + { + return new GeometryFactory(coordinateSystem).MultiPoint(); + } - /// - /// Create a Geometry LineString with a starting position - /// - /// The X value - /// The Y value - /// A Geometry LineString Factory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public static GeometryFactory LineString(double x, double y) - { - return LineString(CoordinateSystem.DefaultGeometry, x, y, null, null); - } + /// + /// Create a Geometry MultiPoint + /// + /// A Geometry MultiPoint Factory + public static GeometryFactory MultiPoint() + { + return MultiPoint(CoordinateSystem.DefaultGeometry); + } - /// - /// Create an empty Geometry LineString - /// - /// The CoordinateSystem - /// A Geometry LineString Factory - public static GeometryFactory LineString(CoordinateSystem coordinateSystem) - { - return new GeometryFactory(coordinateSystem).LineString(); - } + #endregion - /// - /// Create an empty Geometry LineString - /// - /// A Geometry LineString Factory - public static GeometryFactory LineString() - { - return LineString(CoordinateSystem.DefaultGeometry); - } + #region LineString and MultiLineString - /// - /// Create a Geometry MultiLineString - /// - /// The CoordinateSystem - /// A Geometry MultiLineString Factory - public static GeometryFactory MultiLineString(CoordinateSystem coordinateSystem) - { - return new GeometryFactory(coordinateSystem).MultiLineString(); - } + /// + /// Create a Geometry LineString with a starting position + /// + /// The CoordinateSystem + /// The X value + /// The Y value + /// The Z value + /// The M value + /// A Geometry LineString Factory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public static GeometryFactory LineString(CoordinateSystem coordinateSystem, double x, double y, double? z, double? m) + { + return new GeometryFactory(coordinateSystem).LineString(x, y, z, m); + } - /// - /// Create a Geometry MultiLineString - /// - /// A Geometry MultiLineString Factory - public static GeometryFactory MultiLineString() - { - return MultiLineString(CoordinateSystem.DefaultGeometry); - } + /// + /// Create a Geometry LineString with a starting position + /// + /// The X value + /// The Y value + /// The Z value + /// The M value + /// A Geometry LineString Factory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public static GeometryFactory LineString(double x, double y, double? z, double? m) + { + return LineString(CoordinateSystem.DefaultGeometry, x, y, z, m); + } - #endregion + /// + /// Create a Geometry LineString with a starting position + /// + /// The coordinate system + /// The X value + /// The Y value + /// A Geometry LineString Factory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public static GeometryFactory LineString(CoordinateSystem coordinateSystem, double x, double y) + { + return LineString(coordinateSystem, x, y, null, null); + } - #region Polygon + /// + /// Create a Geometry LineString with a starting position + /// + /// The X value + /// The Y value + /// A Geometry LineString Factory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public static GeometryFactory LineString(double x, double y) + { + return LineString(CoordinateSystem.DefaultGeometry, x, y, null, null); + } - /// - /// Create a Geometry Polygon - /// - /// The CoordinateSystem - /// A Geometry Polygon Factory - public static GeometryFactory Polygon(CoordinateSystem coordinateSystem) - { - return new GeometryFactory(coordinateSystem).Polygon(); - } + /// + /// Create an empty Geometry LineString + /// + /// The CoordinateSystem + /// A Geometry LineString Factory + public static GeometryFactory LineString(CoordinateSystem coordinateSystem) + { + return new GeometryFactory(coordinateSystem).LineString(); + } - /// - /// Create a Geometry Polygon - /// - /// A Geometry Polygon Factory - public static GeometryFactory Polygon() - { - return Polygon(CoordinateSystem.DefaultGeometry); - } + /// + /// Create an empty Geometry LineString + /// + /// A Geometry LineString Factory + public static GeometryFactory LineString() + { + return LineString(CoordinateSystem.DefaultGeometry); + } - /// - /// Create a Geometry MultiPolygon - /// - /// The CoordinateSystem - /// A Geometry MultiPolygon Factory - public static GeometryFactory MultiPolygon(CoordinateSystem coordinateSystem) - { - return new GeometryFactory(coordinateSystem).MultiPolygon(); - } + /// + /// Create a Geometry MultiLineString + /// + /// The CoordinateSystem + /// A Geometry MultiLineString Factory + public static GeometryFactory MultiLineString(CoordinateSystem coordinateSystem) + { + return new GeometryFactory(coordinateSystem).MultiLineString(); + } - /// - /// Create a Geometry MultiPolygon - /// - /// A Geometry MultiPolygon Factory - public static GeometryFactory MultiPolygon() - { - return MultiPolygon(CoordinateSystem.DefaultGeometry); - } + /// + /// Create a Geometry MultiLineString + /// + /// A Geometry MultiLineString Factory + public static GeometryFactory MultiLineString() + { + return MultiLineString(CoordinateSystem.DefaultGeometry); + } - #endregion + #endregion - #region Collection + #region Polygon - /// - /// Create a Geometry Collection - /// - /// The CoordinateSystem - /// A Geometry Collection Factory - public static GeometryFactory Collection(CoordinateSystem coordinateSystem) - { - return new GeometryFactory(coordinateSystem).Collection(); - } + /// + /// Create a Geometry Polygon + /// + /// The CoordinateSystem + /// A Geometry Polygon Factory + public static GeometryFactory Polygon(CoordinateSystem coordinateSystem) + { + return new GeometryFactory(coordinateSystem).Polygon(); + } - /// - /// Create a Geometry Collection - /// - /// A Geometry Collection Factory - public static GeometryFactory Collection() - { - return Collection(CoordinateSystem.DefaultGeometry); - } + /// + /// Create a Geometry Polygon + /// + /// A Geometry Polygon Factory + public static GeometryFactory Polygon() + { + return Polygon(CoordinateSystem.DefaultGeometry); + } - #endregion + /// + /// Create a Geometry MultiPolygon + /// + /// The CoordinateSystem + /// A Geometry MultiPolygon Factory + public static GeometryFactory MultiPolygon(CoordinateSystem coordinateSystem) + { + return new GeometryFactory(coordinateSystem).MultiPolygon(); } /// - /// Base Spatial Factory + /// Create a Geometry MultiPolygon /// - public abstract class BaseSpatialFactory + /// A Geometry MultiPolygon Factory + public static GeometryFactory MultiPolygon() { - /// - /// Stack of Containers - /// - private Stack containers; + return MultiPolygon(CoordinateSystem.DefaultGeometry); + } - /// - /// Whether a figure has been started - /// - private bool figureDrawn; + #endregion - /// - /// Inside a Polygon Ring - /// - private bool inRing; + #region Collection - /// - /// Current polygon ring has been closed - /// - private bool ringClosed; + /// + /// Create a Geometry Collection + /// + /// The CoordinateSystem + /// A Geometry Collection Factory + public static GeometryFactory Collection(CoordinateSystem coordinateSystem) + { + return new GeometryFactory(coordinateSystem).Collection(); + } - /// - /// X coordinate of the current ring's starting position - /// - private double ringStartX; - - /// - /// Y coordinate of the current ring's starting position - /// - private double ringStartY; - - /// - /// Z coordinate of the current ring's starting position - /// - private double? ringStartZ; - - /// - /// M coordinate of the current ring's starting position - /// - private double? ringStartM; - - /// - /// Initializes a new instance of the BaseSpatialFactory class - /// - internal BaseSpatialFactory() - { - this.containers = new Stack(); - } - - /// - /// Gets the current container Definition - /// - private SpatialType CurrentType - { - get - { - if (this.containers.Count == 0) - { - return SpatialType.Unknown; - } - else - { - return this.containers.Peek(); - } - } - } - - /// - /// Begin Geo - /// - /// The spatial type - protected virtual void BeginGeo(SpatialType type) - { - // close on nesting types until we find a container suitable for the current type - while (!this.CanContain(type)) - { - this.EndGeo(); - } + /// + /// Create a Geometry Collection + /// + /// A Geometry Collection Factory + public static GeometryFactory Collection() + { + return Collection(CoordinateSystem.DefaultGeometry); + } - this.containers.Push(type); - } + #endregion +} - /// - /// Begin drawing a figure - /// - /// X or Latitude Coordinate - /// Y or Longitude Coordinate - /// Z Coordinate - /// M Coordinate - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - protected virtual void BeginFigure(double x, double y, double? z, double? m) - { - Debug.Assert(!this.figureDrawn, "Figure already started"); - this.figureDrawn = true; - } +/// +/// Base Spatial Factory +/// +public abstract class BaseSpatialFactory +{ + /// + /// Stack of Containers + /// + private Stack containers; - /// - /// Draw a point in the specified coordinate - /// - /// X or Latitude Coordinate - /// Y or Longitude Coordinate - /// Z Coordinate - /// M Coordinate - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - protected virtual void AddLine(double x, double y, double? z, double? m) - { - Debug.Assert(this.figureDrawn, "Figure not yet started"); + /// + /// Whether a figure has been started + /// + private bool figureDrawn; - if (this.inRing) - { - this.ringClosed = x == this.ringStartX && y == this.ringStartY; - } - } + /// + /// Inside a Polygon Ring + /// + private bool inRing; - /// - /// Ends the figure set on the current node - /// - protected virtual void EndFigure() - { - Debug.Assert(this.figureDrawn, "Figure not yet started"); - if (this.inRing) - { - if (!this.ringClosed) - { - this.AddLine(this.ringStartX, this.ringStartY, this.ringStartZ, this.ringStartM); - } + /// + /// Current polygon ring has been closed + /// + private bool ringClosed; - this.inRing = false; - this.ringClosed = true; - } + /// + /// X coordinate of the current ring's starting position + /// + private double ringStartX; - this.figureDrawn = false; - } + /// + /// Y coordinate of the current ring's starting position + /// + private double ringStartY; - /// - /// Ends the current spatial object - /// - protected virtual void EndGeo() - { - if (this.figureDrawn) - { - this.EndFigure(); - } + /// + /// Z coordinate of the current ring's starting position + /// + private double? ringStartZ; - this.containers.Pop(); - } + /// + /// M coordinate of the current ring's starting position + /// + private double? ringStartM; - /// - /// Finish the current instance - /// - protected virtual void Finish() - { - while (this.containers.Count > 0) - { - this.EndGeo(); - } - } + /// + /// Initializes a new instance of the BaseSpatialFactory class + /// + internal BaseSpatialFactory() + { + this.containers = new Stack(); + } - /// - /// Add a new position to the current line figure - /// - /// The X value - /// The Y value - /// The Z value - /// The M value - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - protected virtual void AddPos(double x, double y, double? z, double? m) + /// + /// Gets the current container Definition + /// + private SpatialType CurrentType + { + get { - if (!this.figureDrawn) + if (this.containers.Count == 0) { - this.BeginFigure(x, y, z, m); + return SpatialType.Unknown; } else { - this.AddLine(x, y, z, m); + return this.containers.Peek(); } } + } - /// - /// Start a new polygon ring - /// - /// The X value - /// The Y value - /// The Z value - /// The M value - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - protected virtual void StartRing(double x, double y, double? z, double? m) + /// + /// Begin Geo + /// + /// The spatial type + protected virtual void BeginGeo(SpatialType type) + { + // close on nesting types until we find a container suitable for the current type + while (!this.CanContain(type)) { - if (this.figureDrawn) - { - this.EndFigure(); - } - - this.BeginFigure(x, y, z, m); - - this.ringStartX = x; - this.ringStartY = y; - this.ringStartM = m; - this.ringStartZ = z; - this.inRing = true; - this.ringClosed = false; + this.EndGeo(); } - /// - /// Can the current container contain the spatial type - /// - /// The spatial type to test - /// A boolean value indicating whether the current container can contain the spatial type - private bool CanContain(SpatialType type) - { - switch (this.CurrentType) - { - case SpatialType.Unknown: - case SpatialType.Collection: - // top level or collection - return true; - case SpatialType.MultiPoint: - return type == SpatialType.Point; - case SpatialType.MultiLineString: - return type == SpatialType.LineString; - case SpatialType.MultiPolygon: - return type == SpatialType.Polygon; - default: - return false; - } - } + this.containers.Push(type); } /// - /// Spatial Factory + /// Begin drawing a figure /// - /// The target type - public class GeographyFactory : BaseSpatialFactory where T : Geography + /// X or Latitude Coordinate + /// Y or Longitude Coordinate + /// Z Coordinate + /// M Coordinate + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + protected virtual void BeginFigure(double x, double y, double? z, double? m) { - /// - /// The provider of the built type - /// - private IGeographyProvider provider; + Debug.Assert(!this.figureDrawn, "Figure already started"); + this.figureDrawn = true; + } - /// - /// The chain to build through - /// - private GeographyPipeline buildChain; + /// + /// Draw a point in the specified coordinate + /// + /// X or Latitude Coordinate + /// Y or Longitude Coordinate + /// Z Coordinate + /// M Coordinate + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + protected virtual void AddLine(double x, double y, double? z, double? m) + { + Debug.Assert(this.figureDrawn, "Figure not yet started"); - /// - /// Initializes a new instance of the GeographyFactory class - /// - /// The coordinate system - internal GeographyFactory(CoordinateSystem coordinateSystem) + if (this.inRing) { - var builder = SpatialBuilder.Create(); - this.provider = builder; - this.buildChain = SpatialValidator.Create().ChainTo(builder).StartingLink; - this.buildChain.SetCoordinateSystem(coordinateSystem); + this.ringClosed = x == this.ringStartX && y == this.ringStartY; } + } - /// - /// Using implicit cast to trigger the Finalize call - /// - /// The factory - /// The built instance of the target type - public static implicit operator T(GeographyFactory factory) + /// + /// Ends the figure set on the current node + /// + protected virtual void EndFigure() + { + Debug.Assert(this.figureDrawn, "Figure not yet started"); + if (this.inRing) { - if (factory == null) + if (!this.ringClosed) { - throw new ArgumentNullException("factory"); + this.AddLine(this.ringStartX, this.ringStartY, this.ringStartZ, this.ringStartM); } - return factory.Build(); + this.inRing = false; + this.ringClosed = true; } - #region Public Construction Calls - - /// - /// Start a new Point - /// - /// The latitude value - /// The longitude value - /// The Z value - /// The M Value - /// The current instance of GeographyFactory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public GeographyFactory Point(double latitude, double longitude, double? z, double? m) - { - this.BeginGeo(SpatialType.Point); - this.LineTo(latitude, longitude, z, m); - return this; - } + this.figureDrawn = false; + } - /// - /// Start a new Point - /// - /// The latitude value - /// The longitude value - /// The current instance of GeographyFactory - public GeographyFactory Point(double latitude, double longitude) + /// + /// Ends the current spatial object + /// + protected virtual void EndGeo() + { + if (this.figureDrawn) { - return this.Point(latitude, longitude, null, null); + this.EndFigure(); } - /// - /// Start a new empty Point - /// - /// The current instance of GeographyFactory - public GeographyFactory Point() - { - this.BeginGeo(SpatialType.Point); - return this; - } + this.containers.Pop(); + } - /// - /// Start a new LineString - /// - /// The latitude value - /// The longitude value - /// The Z value - /// The M Value - /// The current instance of GeographyFactory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public GeographyFactory LineString(double latitude, double longitude, double? z, double? m) + /// + /// Finish the current instance + /// + protected virtual void Finish() + { + while (this.containers.Count > 0) { - this.BeginGeo(SpatialType.LineString); - this.LineTo(latitude, longitude, z, m); - return this; + this.EndGeo(); } + } - /// - /// Start a new LineString - /// - /// The latitude value - /// The longitude value - /// The current instance of GeographyFactory - public GeographyFactory LineString(double latitude, double longitude) + /// + /// Add a new position to the current line figure + /// + /// The X value + /// The Y value + /// The Z value + /// The M value + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + protected virtual void AddPos(double x, double y, double? z, double? m) + { + if (!this.figureDrawn) { - return this.LineString(latitude, longitude, null, null); + this.BeginFigure(x, y, z, m); } - - /// - /// Start a new empty LineString - /// - /// The current instance of GeographyFactory - public GeographyFactory LineString() + else { - this.BeginGeo(SpatialType.LineString); - return this; + this.AddLine(x, y, z, m); } + } - /// - /// Start a new Polygon - /// - /// The current instance of GeographyFactory - public GeographyFactory Polygon() + /// + /// Start a new polygon ring + /// + /// The X value + /// The Y value + /// The Z value + /// The M value + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + protected virtual void StartRing(double x, double y, double? z, double? m) + { + if (this.figureDrawn) { - this.BeginGeo(SpatialType.Polygon); - return this; + this.EndFigure(); } - /// - /// Start a new MultiPoint - /// - /// The current instance of GeographyFactory - public GeographyFactory MultiPoint() - { - this.BeginGeo(SpatialType.MultiPoint); - return this; - } + this.BeginFigure(x, y, z, m); - /// - /// Start a new MultiLineString - /// - /// The current instance of GeographyFactory - public GeographyFactory MultiLineString() - { - this.BeginGeo(SpatialType.MultiLineString); - return this; - } + this.ringStartX = x; + this.ringStartY = y; + this.ringStartM = m; + this.ringStartZ = z; + this.inRing = true; + this.ringClosed = false; + } - /// - /// Start a new MultiPolygon - /// - /// The current instance of GeographyFactory - public GeographyFactory MultiPolygon() - { - this.BeginGeo(SpatialType.MultiPolygon); - return this; + /// + /// Can the current container contain the spatial type + /// + /// The spatial type to test + /// A boolean value indicating whether the current container can contain the spatial type + private bool CanContain(SpatialType type) + { + switch (this.CurrentType) + { + case SpatialType.Unknown: + case SpatialType.Collection: + // top level or collection + return true; + case SpatialType.MultiPoint: + return type == SpatialType.Point; + case SpatialType.MultiLineString: + return type == SpatialType.LineString; + case SpatialType.MultiPolygon: + return type == SpatialType.Polygon; + default: + return false; } + } +} - /// - /// Start a new Collection - /// - /// The current instance of GeographyFactory - public GeographyFactory Collection() - { - this.BeginGeo(SpatialType.Collection); - return this; - } +/// +/// Spatial Factory +/// +/// The target type +public class GeographyFactory : BaseSpatialFactory where T : Geography +{ + /// + /// The provider of the built type + /// + private IGeographyProvider provider; - /// - /// Start a new Polygon Ring - /// - /// The latitude value - /// The longitude value - /// The Z value - /// The M Value - /// The current instance of GeographyFactory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public GeographyFactory Ring(double latitude, double longitude, double? z, double? m) - { - this.StartRing(latitude, longitude, z, m); - return this; - } + /// + /// The chain to build through + /// + private GeographyPipeline buildChain; - /// - /// Start a new Polygon Ring - /// - /// The latitude value - /// The longitude value - /// The current instance of GeographyFactory - public GeographyFactory Ring(double latitude, double longitude) - { - return this.Ring(latitude, longitude, null, null); - } + /// + /// Initializes a new instance of the GeographyFactory class + /// + /// The coordinate system + internal GeographyFactory(CoordinateSystem coordinateSystem) + { + var builder = SpatialBuilder.Create(); + this.provider = builder; + this.buildChain = SpatialValidator.Create().ChainTo(builder).StartingLink; + this.buildChain.SetCoordinateSystem(coordinateSystem); + } - /// - /// Add a new point in the current line figure - /// - /// The latitude value - /// The longitude value - /// The Z value - /// The M Value - /// The current instance of GeographyFactory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public GeographyFactory LineTo(double latitude, double longitude, double? z, double? m) + /// + /// Using implicit cast to trigger the Finalize call + /// + /// The factory + /// The built instance of the target type + public static implicit operator T(GeographyFactory factory) + { + if (factory == null) { - this.AddPos(latitude, longitude, z, m); - return this; + throw new ArgumentNullException("factory"); } - /// - /// Add a new point in the current line figure - /// - /// The latitude value - /// The longitude value - /// The current instance of GeographyFactory - public GeographyFactory LineTo(double latitude, double longitude) - { - return this.LineTo(latitude, longitude, null, null); - } + return factory.Build(); + } - #endregion + #region Public Construction Calls - /// - /// Finish the current geography - /// - /// The constructed instance - public T Build() - { - this.Finish(); - return (T)this.provider.ConstructedGeography; - } + /// + /// Start a new Point + /// + /// The latitude value + /// The longitude value + /// The Z value + /// The M Value + /// The current instance of GeographyFactory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public GeographyFactory Point(double latitude, double longitude, double? z, double? m) + { + this.BeginGeo(SpatialType.Point); + this.LineTo(latitude, longitude, z, m); + return this; + } - #region GeoDataPipeline overrides + /// + /// Start a new Point + /// + /// The latitude value + /// The longitude value + /// The current instance of GeographyFactory + public GeographyFactory Point(double latitude, double longitude) + { + return this.Point(latitude, longitude, null, null); + } - /// - /// Begin a new geography - /// - /// The spatial type - protected override void BeginGeo(SpatialType type) - { - base.BeginGeo(type); - this.buildChain.BeginGeography(type); - } + /// + /// Start a new empty Point + /// + /// The current instance of GeographyFactory + public GeographyFactory Point() + { + this.BeginGeo(SpatialType.Point); + return this; + } - /// - /// Begin drawing a figure - /// TODO: longitude and latitude should be swapped !!! per ABNF. - /// - /// X or Latitude Coordinate - /// Y or Longitude Coordinate - /// Z Coordinate - /// M Coordinate - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - protected override void BeginFigure(double latitude, double longitude, double? z, double? m) - { - base.BeginFigure(latitude, longitude, z, m); - this.buildChain.BeginFigure(new GeographyPosition(latitude, longitude, z, m)); - } + /// + /// Start a new LineString + /// + /// The latitude value + /// The longitude value + /// The Z value + /// The M Value + /// The current instance of GeographyFactory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public GeographyFactory LineString(double latitude, double longitude, double? z, double? m) + { + this.BeginGeo(SpatialType.LineString); + this.LineTo(latitude, longitude, z, m); + return this; + } - /// - /// Draw a point in the specified coordinate - /// - /// X or Latitude Coordinate - /// Y or Longitude Coordinate - /// Z Coordinate - /// M Coordinate - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - protected override void AddLine(double latitude, double longitude, double? z, double? m) - { - base.AddLine(latitude, longitude, z, m); - this.buildChain.LineTo(new GeographyPosition(latitude, longitude, z, m)); - } + /// + /// Start a new LineString + /// + /// The latitude value + /// The longitude value + /// The current instance of GeographyFactory + public GeographyFactory LineString(double latitude, double longitude) + { + return this.LineString(latitude, longitude, null, null); + } - /// - /// Ends the figure set on the current node - /// - protected override void EndFigure() - { - base.EndFigure(); - this.buildChain.EndFigure(); - } + /// + /// Start a new empty LineString + /// + /// The current instance of GeographyFactory + public GeographyFactory LineString() + { + this.BeginGeo(SpatialType.LineString); + return this; + } - /// - /// Ends the current spatial object - /// - protected override void EndGeo() - { - base.EndGeo(); - this.buildChain.EndGeography(); - } + /// + /// Start a new Polygon + /// + /// The current instance of GeographyFactory + public GeographyFactory Polygon() + { + this.BeginGeo(SpatialType.Polygon); + return this; + } - #endregion + /// + /// Start a new MultiPoint + /// + /// The current instance of GeographyFactory + public GeographyFactory MultiPoint() + { + this.BeginGeo(SpatialType.MultiPoint); + return this; } /// - /// Spatial Factory + /// Start a new MultiLineString /// - /// The target type - public class GeometryFactory : BaseSpatialFactory where T : Geometry + /// The current instance of GeographyFactory + public GeographyFactory MultiLineString() { - /// - /// The provider of the built type - /// - private IGeometryProvider provider; + this.BeginGeo(SpatialType.MultiLineString); + return this; + } - /// - /// The chain to build through - /// - private GeometryPipeline buildChain; + /// + /// Start a new MultiPolygon + /// + /// The current instance of GeographyFactory + public GeographyFactory MultiPolygon() + { + this.BeginGeo(SpatialType.MultiPolygon); + return this; + } - /// - /// Initializes a new instance of the GeometryFactory class - /// - /// The coordinate system - internal GeometryFactory(CoordinateSystem coordinateSystem) - { - var builder = SpatialBuilder.Create(); - this.provider = builder; - this.buildChain = SpatialValidator.Create().ChainTo(builder).StartingLink; - this.buildChain.SetCoordinateSystem(coordinateSystem); - } + /// + /// Start a new Collection + /// + /// The current instance of GeographyFactory + public GeographyFactory Collection() + { + this.BeginGeo(SpatialType.Collection); + return this; + } - /// - /// Cast a factory to the target type - /// - /// The factory - /// The built instance of the target type - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates", Justification = "Operator used to build")] - public static implicit operator T(GeometryFactory factory) - { - if (factory != null) - { - return factory.Build(); - } + /// + /// Start a new Polygon Ring + /// + /// The latitude value + /// The longitude value + /// The Z value + /// The M Value + /// The current instance of GeographyFactory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public GeographyFactory Ring(double latitude, double longitude, double? z, double? m) + { + this.StartRing(latitude, longitude, z, m); + return this; + } - return null; - } + /// + /// Start a new Polygon Ring + /// + /// The latitude value + /// The longitude value + /// The current instance of GeographyFactory + public GeographyFactory Ring(double latitude, double longitude) + { + return this.Ring(latitude, longitude, null, null); + } - #region Public Construction Calls - - /// - /// Start a new Point - /// - /// The X value - /// The Y value - /// The Z value - /// The M value - /// The current instance of GeometryFactory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public GeometryFactory Point(double x, double y, double? z, double? m) - { - this.BeginGeo(SpatialType.Point); - this.LineTo(x, y, z, m); - return this; - } + /// + /// Add a new point in the current line figure + /// + /// The latitude value + /// The longitude value + /// The Z value + /// The M Value + /// The current instance of GeographyFactory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public GeographyFactory LineTo(double latitude, double longitude, double? z, double? m) + { + this.AddPos(latitude, longitude, z, m); + return this; + } - /// - /// Start a new Point - /// - /// The X value - /// The Y value - /// The current instance of GeometryFactory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public GeometryFactory Point(double x, double y) - { - return this.Point(x, y, null, null); - } + /// + /// Add a new point in the current line figure + /// + /// The latitude value + /// The longitude value + /// The current instance of GeographyFactory + public GeographyFactory LineTo(double latitude, double longitude) + { + return this.LineTo(latitude, longitude, null, null); + } - /// - /// Start a new empty Point - /// - /// The current instance of GeometryFactory - public GeometryFactory Point() - { - this.BeginGeo(SpatialType.Point); - return this; - } + #endregion - /// - /// Start a new LineString - /// - /// The X value - /// The Y value - /// The Z value - /// The M value - /// The current instance of GeometryFactory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public GeometryFactory LineString(double x, double y, double? z, double? m) - { - this.BeginGeo(SpatialType.LineString); - this.LineTo(x, y, z, m); - return this; - } + /// + /// Finish the current geography + /// + /// The constructed instance + public T Build() + { + this.Finish(); + return (T)this.provider.ConstructedGeography; + } - /// - /// Start a new LineString - /// - /// The X value - /// The Y value - /// The current instance of GeometryFactory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public GeometryFactory LineString(double x, double y) - { - return this.LineString(x, y, null, null); - } + #region GeoDataPipeline overrides - /// - /// Start a new empty LineString - /// - /// The current instance of GeometryFactory - public GeometryFactory LineString() - { - this.BeginGeo(SpatialType.LineString); - return this; - } + /// + /// Begin a new geography + /// + /// The spatial type + protected override void BeginGeo(SpatialType type) + { + base.BeginGeo(type); + this.buildChain.BeginGeography(type); + } - /// - /// Start a new Polygon - /// - /// The current instance of GeometryFactory - public GeometryFactory Polygon() - { - this.BeginGeo(SpatialType.Polygon); - return this; - } + /// + /// Begin drawing a figure + /// TODO: longitude and latitude should be swapped !!! per ABNF. + /// + /// X or Latitude Coordinate + /// Y or Longitude Coordinate + /// Z Coordinate + /// M Coordinate + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + protected override void BeginFigure(double latitude, double longitude, double? z, double? m) + { + base.BeginFigure(latitude, longitude, z, m); + this.buildChain.BeginFigure(new GeographyPosition(latitude, longitude, z, m)); + } - /// - /// Start a new MultiPoint - /// - /// The current instance of GeometryFactory - public GeometryFactory MultiPoint() - { - this.BeginGeo(SpatialType.MultiPoint); - return this; - } + /// + /// Draw a point in the specified coordinate + /// + /// X or Latitude Coordinate + /// Y or Longitude Coordinate + /// Z Coordinate + /// M Coordinate + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + protected override void AddLine(double latitude, double longitude, double? z, double? m) + { + base.AddLine(latitude, longitude, z, m); + this.buildChain.LineTo(new GeographyPosition(latitude, longitude, z, m)); + } - /// - /// Start a new MultiLineString - /// - /// The current instance of GeometryFactory - public GeometryFactory MultiLineString() - { - this.BeginGeo(SpatialType.MultiLineString); - return this; - } + /// + /// Ends the figure set on the current node + /// + protected override void EndFigure() + { + base.EndFigure(); + this.buildChain.EndFigure(); + } - /// - /// Start a new MultiPolygon - /// - /// The current instance of GeometryFactory - public GeometryFactory MultiPolygon() - { - this.BeginGeo(SpatialType.MultiPolygon); - return this; - } + /// + /// Ends the current spatial object + /// + protected override void EndGeo() + { + base.EndGeo(); + this.buildChain.EndGeography(); + } - /// - /// Start a new Collection - /// - /// The current instance of GeometryFactory - public GeometryFactory Collection() - { - this.BeginGeo(SpatialType.Collection); - return this; - } + #endregion +} - /// - /// Start a new Polygon Ring - /// - /// The X value - /// The Y value - /// The Z value - /// The M value - /// The current instance of GeometryFactory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public GeometryFactory Ring(double x, double y, double? z, double? m) - { - this.StartRing(x, y, z, m); - return this; - } +/// +/// Spatial Factory +/// +/// The target type +public class GeometryFactory : BaseSpatialFactory where T : Geometry +{ + /// + /// The provider of the built type + /// + private IGeometryProvider provider; - /// - /// Start a new Polygon Ring - /// - /// The X value - /// The Y value - /// The current instance of GeometryFactory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public GeometryFactory Ring(double x, double y) - { - return this.Ring(x, y, null, null); - } + /// + /// The chain to build through + /// + private GeometryPipeline buildChain; - /// - /// Add a new point in the current line figure - /// - /// The X value - /// The Y value - /// The Z value - /// The M value - /// The current instance of GeometryFactory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public GeometryFactory LineTo(double x, double y, double? z, double? m) - { - this.AddPos(x, y, z, m); - return this; - } + /// + /// Initializes a new instance of the GeometryFactory class + /// + /// The coordinate system + internal GeometryFactory(CoordinateSystem coordinateSystem) + { + var builder = SpatialBuilder.Create(); + this.provider = builder; + this.buildChain = SpatialValidator.Create().ChainTo(builder).StartingLink; + this.buildChain.SetCoordinateSystem(coordinateSystem); + } - /// - /// Add a new point in the current line figure - /// - /// The X value - /// The Y value - /// The current instance of GeometryFactory - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] - public GeometryFactory LineTo(double x, double y) + /// + /// Cast a factory to the target type + /// + /// The factory + /// The built instance of the target type + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates", Justification = "Operator used to build")] + public static implicit operator T(GeometryFactory factory) + { + if (factory != null) { - return this.LineTo(x, y, null, null); + return factory.Build(); } - #endregion + return null; + } - /// - /// Finish the current Geometry - /// - /// The constructed instance - public T Build() - { - this.Finish(); - return (T)this.provider.ConstructedGeometry; - } + #region Public Construction Calls - #region GeoDataPipeline overrides + /// + /// Start a new Point + /// + /// The X value + /// The Y value + /// The Z value + /// The M value + /// The current instance of GeometryFactory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public GeometryFactory Point(double x, double y, double? z, double? m) + { + this.BeginGeo(SpatialType.Point); + this.LineTo(x, y, z, m); + return this; + } - /// - /// Begin a new geometry - /// - /// The spatial type - protected override void BeginGeo(SpatialType type) - { - base.BeginGeo(type); - this.buildChain.BeginGeometry(type); - } + /// + /// Start a new Point + /// + /// The X value + /// The Y value + /// The current instance of GeometryFactory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public GeometryFactory Point(double x, double y) + { + return this.Point(x, y, null, null); + } - /// - /// Begin drawing a figure - /// - /// X or Latitude Coordinate - /// Y or Longitude Coordinate - /// Z Coordinate - /// M Coordinate - protected override void BeginFigure(double x, double y, double? z, double? m) - { - base.BeginFigure(x, y, z, m); - this.buildChain.BeginFigure(new GeometryPosition(x, y, z, m)); - } + /// + /// Start a new empty Point + /// + /// The current instance of GeometryFactory + public GeometryFactory Point() + { + this.BeginGeo(SpatialType.Point); + return this; + } - /// - /// Draw a point in the specified coordinate - /// - /// X or Latitude Coordinate - /// Y or Longitude Coordinate - /// Z Coordinate - /// M Coordinate - protected override void AddLine(double x, double y, double? z, double? m) - { - base.AddLine(x, y, z, m); - this.buildChain.LineTo(new GeometryPosition(x, y, z, m)); - } + /// + /// Start a new LineString + /// + /// The X value + /// The Y value + /// The Z value + /// The M value + /// The current instance of GeometryFactory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public GeometryFactory LineString(double x, double y, double? z, double? m) + { + this.BeginGeo(SpatialType.LineString); + this.LineTo(x, y, z, m); + return this; + } - /// - /// Ends the figure set on the current node - /// - protected override void EndFigure() - { - base.EndFigure(); - this.buildChain.EndFigure(); - } + /// + /// Start a new LineString + /// + /// The X value + /// The Y value + /// The current instance of GeometryFactory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public GeometryFactory LineString(double x, double y) + { + return this.LineString(x, y, null, null); + } - /// - /// Ends the current spatial object - /// - protected override void EndGeo() - { - base.EndGeo(); - this.buildChain.EndGeometry(); - } + /// + /// Start a new empty LineString + /// + /// The current instance of GeometryFactory + public GeometryFactory LineString() + { + this.BeginGeo(SpatialType.LineString); + return this; + } - #endregion + /// + /// Start a new Polygon + /// + /// The current instance of GeometryFactory + public GeometryFactory Polygon() + { + this.BeginGeo(SpatialType.Polygon); + return this; + } + + /// + /// Start a new MultiPoint + /// + /// The current instance of GeometryFactory + public GeometryFactory MultiPoint() + { + this.BeginGeo(SpatialType.MultiPoint); + return this; + } + + /// + /// Start a new MultiLineString + /// + /// The current instance of GeometryFactory + public GeometryFactory MultiLineString() + { + this.BeginGeo(SpatialType.MultiLineString); + return this; + } + + /// + /// Start a new MultiPolygon + /// + /// The current instance of GeometryFactory + public GeometryFactory MultiPolygon() + { + this.BeginGeo(SpatialType.MultiPolygon); + return this; + } + + /// + /// Start a new Collection + /// + /// The current instance of GeometryFactory + public GeometryFactory Collection() + { + this.BeginGeo(SpatialType.Collection); + return this; + } + + /// + /// Start a new Polygon Ring + /// + /// The X value + /// The Y value + /// The Z value + /// The M value + /// The current instance of GeometryFactory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public GeometryFactory Ring(double x, double y, double? z, double? m) + { + this.StartRing(x, y, z, m); + return this; } + + /// + /// Start a new Polygon Ring + /// + /// The X value + /// The Y value + /// The current instance of GeometryFactory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public GeometryFactory Ring(double x, double y) + { + return this.Ring(x, y, null, null); + } + + /// + /// Add a new point in the current line figure + /// + /// The X value + /// The Y value + /// The Z value + /// The M value + /// The current instance of GeometryFactory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public GeometryFactory LineTo(double x, double y, double? z, double? m) + { + this.AddPos(x, y, z, m); + return this; + } + + /// + /// Add a new point in the current line figure + /// + /// The X value + /// The Y value + /// The current instance of GeometryFactory + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", Justification = "x, y, z and m are meaningful")] + public GeometryFactory LineTo(double x, double y) + { + return this.LineTo(x, y, null, null); + } + + #endregion + + /// + /// Finish the current Geometry + /// + /// The constructed instance + public T Build() + { + this.Finish(); + return (T)this.provider.ConstructedGeometry; + } + + #region GeoDataPipeline overrides + + /// + /// Begin a new geometry + /// + /// The spatial type + protected override void BeginGeo(SpatialType type) + { + base.BeginGeo(type); + this.buildChain.BeginGeometry(type); + } + + /// + /// Begin drawing a figure + /// + /// X or Latitude Coordinate + /// Y or Longitude Coordinate + /// Z Coordinate + /// M Coordinate + protected override void BeginFigure(double x, double y, double? z, double? m) + { + base.BeginFigure(x, y, z, m); + this.buildChain.BeginFigure(new GeometryPosition(x, y, z, m)); + } + + /// + /// Draw a point in the specified coordinate + /// + /// X or Latitude Coordinate + /// Y or Longitude Coordinate + /// Z Coordinate + /// M Coordinate + protected override void AddLine(double x, double y, double? z, double? m) + { + base.AddLine(x, y, z, m); + this.buildChain.LineTo(new GeometryPosition(x, y, z, m)); + } + + /// + /// Ends the figure set on the current node + /// + protected override void EndFigure() + { + base.EndFigure(); + this.buildChain.EndFigure(); + } + + /// + /// Ends the current spatial object + /// + protected override void EndGeo() + { + base.EndGeo(); + this.buildChain.EndGeometry(); + } + + #endregion } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialModels.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialModels.cs index b5addb68d..f929761ab 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialModels.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialModels.cs @@ -8,19 +8,18 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Spatial; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Spatial +namespace Microsoft.AspNetCore.OData.E2E.Tests.Spatial; + +public class SpatialCustomer { - public class SpatialCustomer - { - [Key] - public int CustomerId { get; set; } + [Key] + public int CustomerId { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public GeographyPoint Location { get; set; } + public GeographyPoint Location { get; set; } - public GeographyLineString Region { get; set; } + public GeographyLineString Region { get; set; } - public GeometryPoint HomePoint { get; set; } - } + public GeometryPoint HomePoint { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialTests.cs index bfafda9eb..e66e25ee8 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Spatial/SpatialTests.cs @@ -20,158 +20,157 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Spatial +namespace Microsoft.AspNetCore.OData.E2E.Tests.Spatial; + +public class SpatialTests : WebODataTestBase { - public class SpatialTests : WebODataTestBase + public class SpatialTestsStartup : TestStartupBase { - public class SpatialTestsStartup : TestStartupBase + public override void ConfigureServices(IServiceCollection services) { - public override void ConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(SpatialCustomersController), typeof(MetadataController)); + services.ConfigureControllers(typeof(SpatialCustomersController), typeof(MetadataController)); - IEdmModel model = IsofEdmModel.GetEdmModel(); - services.AddControllers().AddOData(options => options.AddRouteComponents("odata", model)); - } + IEdmModel model = IsofEdmModel.GetEdmModel(); + services.AddControllers().AddOData(options => options.AddRouteComponents("odata", model)); } + } - public SpatialTests(WebODataTestFixture factory) - : base(factory) - { - } + public SpatialTests(WebODataTestFixture factory) + : base(factory) + { + } - [Fact] - public async Task SpatialModelMetadataTest() - { - // Arrange & Act - HttpResponseMessage response = await this.Client.GetAsync("odata/$metadata"); + [Fact] + public async Task SpatialModelMetadataTest() + { + // Arrange & Act + HttpResponseMessage response = await this.Client.GetAsync("odata/$metadata"); - // Assert - var stream = await response.Content.ReadAsStreamAsync(); - IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); - var reader = new ODataMessageReader(message); - var edmModel = reader.ReadMetadataDocument(); + // Assert + var stream = await response.Content.ReadAsStreamAsync(); + IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); + var reader = new ODataMessageReader(message); + var edmModel = reader.ReadMetadataDocument(); - var customer = edmModel.SchemaElements.OfType().Single(et => et.Name == "SpatialCustomer"); - Assert.Equal(5, customer.Properties().Count()); + var customer = edmModel.SchemaElements.OfType().Single(et => et.Name == "SpatialCustomer"); + Assert.Equal(5, customer.Properties().Count()); - var locationProperty = customer.DeclaredProperties.Single(p => p.Name == "Location"); - Assert.Equal("Edm.GeographyPoint", locationProperty.Type.FullName()); + var locationProperty = customer.DeclaredProperties.Single(p => p.Name == "Location"); + Assert.Equal("Edm.GeographyPoint", locationProperty.Type.FullName()); - var regionProperty = customer.DeclaredProperties.Single(p => p.Name == "Region"); - Assert.Equal("Edm.GeographyLineString", regionProperty.Type.FullName()); + var regionProperty = customer.DeclaredProperties.Single(p => p.Name == "Region"); + Assert.Equal("Edm.GeographyLineString", regionProperty.Type.FullName()); - var homePointProperty = customer.DeclaredProperties.Single(p => p.Name == "HomePoint"); - Assert.Equal("Edm.GeometryPoint", homePointProperty.Type.FullName()); - } + var homePointProperty = customer.DeclaredProperties.Single(p => p.Name == "HomePoint"); + Assert.Equal("Edm.GeometryPoint", homePointProperty.Type.FullName()); + } - [Fact] - public async Task QuerySpatialEntity() - { - // Arrange & Act - HttpResponseMessage response = await Client.GetAsync("odata/SpatialCustomers(2)"); - JObject responseString = await response.Content.ReadAsObject(); - - // Assert - Assert.True(HttpStatusCode.OK == response.StatusCode); - - JArray regionData = responseString["Region"]["coordinates"] as JArray; - Assert.NotNull(regionData); - Assert.Equal(2, regionData.Count); - - Assert.Equal(66.0, regionData[0][0]); - Assert.Equal(55.0, regionData[0][1]); - Assert.Equal(JValue.CreateNull(), regionData[0][2]); - Assert.Equal(0.0, regionData[0][3]); - - Assert.Equal(44.0, regionData[1][0]); - Assert.Equal(33, regionData[1][1]); - Assert.Equal(JValue.CreateNull(), regionData[1][2]); - Assert.Equal(12.3, regionData[1][3]); - } + [Fact] + public async Task QuerySpatialEntity() + { + // Arrange & Act + HttpResponseMessage response = await Client.GetAsync("odata/SpatialCustomers(2)"); + JObject responseString = await response.Content.ReadAsObject(); + + // Assert + Assert.True(HttpStatusCode.OK == response.StatusCode); + + JArray regionData = responseString["Region"]["coordinates"] as JArray; + Assert.NotNull(regionData); + Assert.Equal(2, regionData.Count); + + Assert.Equal(66.0, regionData[0][0]); + Assert.Equal(55.0, regionData[0][1]); + Assert.Equal(JValue.CreateNull(), regionData[0][2]); + Assert.Equal(0.0, regionData[0][3]); + + Assert.Equal(44.0, regionData[1][0]); + Assert.Equal(33, regionData[1][1]); + Assert.Equal(JValue.CreateNull(), regionData[1][2]); + Assert.Equal(12.3, regionData[1][3]); + } - [Fact] - public async Task PostSpatialEntity() - { - // Arrange - const string payload = @"{ + [Fact] + public async Task PostSpatialEntity() + { + // Arrange + const string payload = @"{ ""Location"":{ - ""type"":""Point"",""coordinates"":[ - 7,8,9,10 - ],""crs"":{ - ""type"":""name"",""properties"":{ - ""name"":""EPSG:4326"" - } - } +""type"":""Point"",""coordinates"":[ + 7,8,9,10 +],""crs"":{ + ""type"":""name"",""properties"":{ + ""name"":""EPSG:4326"" + } +} },""Region"":{ - ""type"":""LineString"",""coordinates"":[ - [ - 1.0,1.0 - ],[ - 3.0,3.0 - ],[ - 4.0,4.0 - ],[ - 0.0,0.0 - ] - ],""crs"":{ - ""type"":""name"",""properties"":{ - ""name"":""EPSG:4326"" - } - } +""type"":""LineString"",""coordinates"":[ + [ + 1.0,1.0 + ],[ + 3.0,3.0 + ],[ + 4.0,4.0 + ],[ + 0.0,0.0 + ] +],""crs"":{ + ""type"":""name"",""properties"":{ + ""name"":""EPSG:4326"" + } +} }, ""Name"":""Sam"", ""HomePoint"": { - ""type"": ""Point"", - ""coordinates"": [ - 4.0, - 10.0 - ], - ""crs"": { - ""type"": ""name"", - ""properties"": { - ""name"": ""EPSG:0"" - } - } +""type"": ""Point"", +""coordinates"": [ + 4.0, + 10.0 +], +""crs"": { + ""type"": ""name"", + ""properties"": { + ""name"": ""EPSG:0"" + } +} } }"; - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/SpatialCustomers"); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; - HttpResponseMessage response = await Client.SendAsync(request); + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/SpatialCustomers"); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + HttpResponseMessage response = await Client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - } + // Assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } - [Fact] - public async Task UpdateSpatialEntity() - { - // Arrange - const string payload = @"{ + [Fact] + public async Task UpdateSpatialEntity() + { + // Arrange + const string payload = @"{ ""Location"":{ - ""type"":""Point"",""coordinates"":[ - 7,8,9,10 - ],""crs"":{ - ""type"":""name"",""properties"":{ - ""name"":""EPSG:4326"" - } - } +""type"":""Point"",""coordinates"":[ + 7,8,9,10 +],""crs"":{ + ""type"":""name"",""properties"":{ + ""name"":""EPSG:4326"" + } +} } }"; - // Act - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("Patch"), "odata/SpatialCustomers(3)"); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; - HttpResponseMessage response = await Client.SendAsync(request); + // Act + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("Patch"), "odata/SpatialCustomers(3)"); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + HttpResponseMessage response = await Client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Stream/StreamPropertyController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Stream/StreamPropertyController.cs index 59cf72dd9..69b72f6cc 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Stream/StreamPropertyController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Stream/StreamPropertyController.cs @@ -12,74 +12,73 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.StreamProperty +namespace Microsoft.AspNetCore.OData.E2E.Tests.StreamProperty; + +public class StreamCustomersController : ODataController { - public class StreamCustomersController : ODataController + [HttpGet] + [EnableQuery] + public IQueryable Get() { - [HttpGet] - [EnableQuery] - public IQueryable Get() - { - return CreateCustomers().AsQueryable(); - } + return CreateCustomers().AsQueryable(); + } - [HttpGet] - public IActionResult Get(int key) + [HttpGet] + public IActionResult Get(int key) + { + IList customers = CreateCustomers(); + StreamCustomer customer = customers.FirstOrDefault(c => c.Id == key); + if (customer == null) { - IList customers = CreateCustomers(); - StreamCustomer customer = customers.FirstOrDefault(c => c.Id == key); - if (customer == null) - { - return NotFound(); - } - - return Ok(customer); + return NotFound(); } - [HttpGet] - public IActionResult GetName(int key) - { - IList customers = CreateCustomers(); - StreamCustomer customer = customers.FirstOrDefault(c => c.Id == key); - if (customer == null) - { - return NotFound(); - } + return Ok(customer); + } - return Ok(customer.Name); + [HttpGet] + public IActionResult GetName(int key) + { + IList customers = CreateCustomers(); + StreamCustomer customer = customers.FirstOrDefault(c => c.Id == key); + if (customer == null) + { + return NotFound(); } - [HttpGet] - public IActionResult GetPhoto(int key) - { - IList customers = CreateCustomers(); - StreamCustomer customer = customers.FirstOrDefault(c => c.Id == key); - if (customer == null) - { - return NotFound(); - } + return Ok(customer.Name); + } - return Ok(customer.Photo); + [HttpGet] + public IActionResult GetPhoto(int key) + { + IList customers = CreateCustomers(); + StreamCustomer customer = customers.FirstOrDefault(c => c.Id == key); + if (customer == null) + { + return NotFound(); } - private static IList CreateCustomers() - { - byte[] byteArray = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; - IList customers = Enumerable.Range(0, 5).Select(i => - new StreamCustomer - { - Id = i, - Name = "FirstName " + i, - Photo = new MemoryStream(byteArray, i, 4) - }).ToList(); + return Ok(customer.Photo); + } - foreach (var c in customers) + private static IList CreateCustomers() + { + byte[] byteArray = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + IList customers = Enumerable.Range(0, 5).Select(i => + new StreamCustomer { - c.PhotoText = new StreamReader(c.Photo).ReadToEnd(); - c.Photo.Seek(0, SeekOrigin.Begin); - } + Id = i, + Name = "FirstName " + i, + Photo = new MemoryStream(byteArray, i, 4) + }).ToList(); - return customers; + foreach (var c in customers) + { + c.PhotoText = new StreamReader(c.Photo).ReadToEnd(); + c.Photo.Seek(0, SeekOrigin.Begin); } + + return customers; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Stream/StreamPropertyTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Stream/StreamPropertyTests.cs index d35faa5c5..f9857e2e3 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Stream/StreamPropertyTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Stream/StreamPropertyTests.cs @@ -21,138 +21,137 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.StreamProperty +namespace Microsoft.AspNetCore.OData.E2E.Tests.StreamProperty; + +public class StreamPropertyTests : WebApiTestBase { - public class StreamPropertyTests : WebApiTestBase - { - private static IEdmModel EdmModel; + private static IEdmModel EdmModel; - public StreamPropertyTests(WebApiTestFixture fixture) - : base(fixture) - { - } + public StreamPropertyTests(WebApiTestFixture fixture) + : base(fixture) + { + } - [Fact] - public async Task GetMetadata_WithStreamProperty() - { - // Arrange - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync("odata/$metadata"); - - // Assert - var payload = await response.Content.ReadAsStringAsync(); - Assert.Contains("" + - "" + - "" + - "application/javascript" + - "image/png" + - "" + - "" + - "", payload); - } + [Fact] + public async Task GetMetadata_WithStreamProperty() + { + // Arrange + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync("odata/$metadata"); + + // Assert + var payload = await response.Content.ReadAsStringAsync(); + Assert.Contains("" + + "" + + "" + + "application/javascript" + + "image/png" + + "" + + "" + + "", payload); + } - [Fact] - public async Task Get_EntityWithStreamProperty() - { - // Arrange - HttpClient client = CreateClient(); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/StreamCustomers(1)"); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); - - Assert.Equal("http://localhost/odata/$metadata#StreamCustomers/$entity", result["@odata.context"]); - Assert.Equal("#Microsoft.AspNetCore.OData.E2E.Tests.StreamProperty.StreamCustomer", result["@odata.type"]); - Assert.Equal("\u0002\u0003\u0004\u0005", result["PhotoText"]); - Assert.Equal("http://localhost/odata/StreamCustomers(1)/Photo", result["Photo@odata.mediaEditLink"]); - Assert.Equal("http://localhost/odata/StreamCustomers(1)/Photo", result["Photo@odata.mediaReadLink"]); - } + [Fact] + public async Task Get_EntityWithStreamProperty() + { + // Arrange + HttpClient client = CreateClient(); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/StreamCustomers(1)"); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); + + Assert.Equal("http://localhost/odata/$metadata#StreamCustomers/$entity", result["@odata.context"]); + Assert.Equal("#Microsoft.AspNetCore.OData.E2E.Tests.StreamProperty.StreamCustomer", result["@odata.type"]); + Assert.Equal("\u0002\u0003\u0004\u0005", result["PhotoText"]); + Assert.Equal("http://localhost/odata/StreamCustomers(1)/Photo", result["Photo@odata.mediaEditLink"]); + Assert.Equal("http://localhost/odata/StreamCustomers(1)/Photo", result["Photo@odata.mediaReadLink"]); + } - [Fact] - public async Task Get_SingleStreamProperty() - { - // Arrange - HttpClient client = CreateClient(); + [Fact] + public async Task Get_SingleStreamProperty() + { + // Arrange + HttpClient client = CreateClient(); - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/StreamCustomers(2)/Photo"); - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "odata/StreamCustomers(2)/Photo"); + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.True(response.IsSuccessStatusCode); + // Assert + Assert.True(response.IsSuccessStatusCode); - Assert.Equal("application/octet-stream", response.Content.Headers.ContentType.MediaType); - var stream = await response.Content.ReadAsStreamAsync(); + Assert.Equal("application/octet-stream", response.Content.Headers.ContentType.MediaType); + var stream = await response.Content.ReadAsStreamAsync(); - StreamReader reader = new StreamReader(stream); - string text = reader.ReadToEnd(); - Assert.Equal("\u0003\u0004\u0005\u0006", text); + StreamReader reader = new StreamReader(stream); + string text = reader.ReadToEnd(); + Assert.Equal("\u0003\u0004\u0005\u0006", text); - byte[] byteArray = ReadAllBytes(stream); - Assert.Equal(new byte[] { 3, 4, 5, 6 }, byteArray); - } + byte[] byteArray = ReadAllBytes(stream); + Assert.Equal(new byte[] { 3, 4, 5, 6 }, byteArray); + } - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) - { - EdmModel = GetEdmModel(); + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) + { + EdmModel = GetEdmModel(); - services.ConfigureControllers(typeof(MetadataController), typeof(StreamCustomersController)); + services.ConfigureControllers(typeof(MetadataController), typeof(StreamCustomersController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", EdmModel)); - } + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", EdmModel)); + } - public static byte[] ReadAllBytes(Stream instream) + public static byte[] ReadAllBytes(Stream instream) + { + if (instream is MemoryStream) { - if (instream is MemoryStream) - { - return ((MemoryStream)instream).ToArray(); - } - - using (var memoryStream = new MemoryStream()) - { - instream.CopyTo(memoryStream); - return memoryStream.ToArray(); - } + return ((MemoryStream)instream).ToArray(); } - public static IEdmModel GetEdmModel() + using (var memoryStream = new MemoryStream()) { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("StreamCustomers"); - EdmModel model = builder.GetEdmModel() as EdmModel; - - IEdmEntityType streamCustomerType = model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "StreamCustomer"); - Assert.NotNull(streamCustomerType); - IEdmProperty photoProperty = streamCustomerType.FindProperty("Photo"); - - EdmStringConstant strConstant1 = new EdmStringConstant("application/javascript"); - EdmStringConstant strConstant2 = new EdmStringConstant("image/png"); - EdmCollectionExpression collectionExpression = new EdmCollectionExpression(strConstant1, strConstant2); - EdmVocabularyAnnotation annotation = new EdmVocabularyAnnotation(photoProperty, CoreVocabularyModel.AcceptableMediaTypesTerm, collectionExpression); - annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); - model.AddVocabularyAnnotation(annotation); - - return model; + instream.CopyTo(memoryStream); + return memoryStream.ToArray(); } } - public class StreamCustomer + public static IEdmModel GetEdmModel() { - public int Id { get; set; } + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("StreamCustomers"); + EdmModel model = builder.GetEdmModel() as EdmModel; + + IEdmEntityType streamCustomerType = model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "StreamCustomer"); + Assert.NotNull(streamCustomerType); + IEdmProperty photoProperty = streamCustomerType.FindProperty("Photo"); + + EdmStringConstant strConstant1 = new EdmStringConstant("application/javascript"); + EdmStringConstant strConstant2 = new EdmStringConstant("image/png"); + EdmCollectionExpression collectionExpression = new EdmCollectionExpression(strConstant1, strConstant2); + EdmVocabularyAnnotation annotation = new EdmVocabularyAnnotation(photoProperty, CoreVocabularyModel.AcceptableMediaTypesTerm, collectionExpression); + annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); + model.AddVocabularyAnnotation(annotation); + + return model; + } +} - public string Name { get; set; } +public class StreamCustomer +{ + public int Id { get; set; } - // this property saves the string of the Photo - public string PhotoText { get; set; } + public string Name { get; set; } - public Stream Photo { get; set; } - } + // this property saves the string of the Photo + public string PhotoText { get; set; } + + public Stream Photo { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessDeltaSerializationTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessDeltaSerializationTests.cs index ba048205d..b5572fd93 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessDeltaSerializationTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessDeltaSerializationTests.cs @@ -23,115 +23,114 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Typeless +namespace Microsoft.AspNetCore.OData.E2E.Tests.Typeless; + +public class TypelessDeltaSerializationTests : WebApiTestBase { - public class TypelessDeltaSerializationTests : WebApiTestBase + public TypelessDeltaSerializationTests(WebApiTestFixture fixture) + :base(fixture) { - public TypelessDeltaSerializationTests(WebApiTestFixture fixture) - :base(fixture) - { - } + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = GetEdmModel(); + services.ConfigureControllers(typeof(TypelessDeltaCustomersController)); + services.AddControllers().AddOData(opt => opt.Expand().AddRouteComponents("odata", edmModel)); + } + + private static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + var customers = builder.EntitySet("TypelessDeltaCustomers"); + customers.EntityType.Property(c => c.Name).IsRequired(); + var orders = builder.EntitySet("TypelessDeltaOrders"); + return builder.GetEdmModel(); + } + + [Theory] + [InlineData("application/json")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=full")] + public async Task TypelessDeltaWorksInAllFormats(string acceptHeader) + { + // Arrange + string url = "odata/TypelessDeltaCustomers?$deltatoken=abc"; + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptHeader)); - protected static void UpdateConfigureServices(IServiceCollection services) + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.NotNull(response.Content); + JObject returnedObject = await response.Content.ReadAsObject(); + Assert.True(((dynamic)returnedObject).value.Count == 15); + + //Verification of content to validate Payload + for (int i = 0 ; i < 10 ; i++) { - IEdmModel edmModel = GetEdmModel(); - services.ConfigureControllers(typeof(TypelessDeltaCustomersController)); - services.AddControllers().AddOData(opt => opt.Expand().AddRouteComponents("odata", edmModel)); + string name = string.Format("Name {0}", i); + Assert.True(name.Equals(((dynamic)returnedObject).value[i]["Name"].Value)); } - private static IEdmModel GetEdmModel() + for (int i=10 ; i < 15 ; i++) { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - var customers = builder.EntitySet("TypelessDeltaCustomers"); - customers.EntityType.Property(c => c.Name).IsRequired(); - var orders = builder.EntitySet("TypelessDeltaOrders"); - return builder.GetEdmModel(); + Assert.True(i.ToString().Equals(((dynamic)returnedObject).value[i]["@id"].Value)); } + } +} - [Theory] - [InlineData("application/json")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=full")] - public async Task TypelessDeltaWorksInAllFormats(string acceptHeader) +public class TypelessDeltaCustomersController : ODataController +{ + public IEdmEntityType DeltaCustomerType + { + get { - // Arrange - string url = "odata/TypelessDeltaCustomers?$deltatoken=abc"; - HttpClient client = CreateClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptHeader)); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.NotNull(response.Content); - JObject returnedObject = await response.Content.ReadAsObject(); - Assert.True(((dynamic)returnedObject).value.Count == 15); - - //Verification of content to validate Payload - for (int i = 0 ; i < 10 ; i++) - { - string name = string.Format("Name {0}", i); - Assert.True(name.Equals(((dynamic)returnedObject).value[i]["Name"].Value)); - } - - for (int i=10 ; i < 15 ; i++) - { - Assert.True(i.ToString().Equals(((dynamic)returnedObject).value[i]["@id"].Value)); - } + return Request.GetModel().FindType("Microsoft.AspNetCore.OData.E2E.Tests.Typeless.TypelessCustomer") as IEdmEntityType; } } - public class TypelessDeltaCustomersController : ODataController + public IEdmEntityType DeltaOrderType { - public IEdmEntityType DeltaCustomerType + get { - get - { - return Request.GetModel().FindType("Microsoft.AspNetCore.OData.E2E.Tests.Typeless.TypelessCustomer") as IEdmEntityType; - } + return Request.GetModel().FindType("Microsoft.AspNetCore.OData.E2E.Tests.Typeless.TypelessOrder") as IEdmEntityType; } + } - public IEdmEntityType DeltaOrderType + public IEdmComplexType DeltaAddressType + { + get { - get - { - return Request.GetModel().FindType("Microsoft.AspNetCore.OData.E2E.Tests.Typeless.TypelessOrder") as IEdmEntityType; - } + return Request.GetModel().FindType("Microsoft.AspNetCore.OData.E2E.Tests.Typeless.TypelessAddress") as IEdmComplexType; } + } - public IEdmComplexType DeltaAddressType + public IActionResult Get() + { + EdmChangedObjectCollection changedCollection = new EdmChangedObjectCollection(DeltaCustomerType); + //Changed or Modified objects are represented as EdmDeltaResourceObjects + for (int i = 0; i < 10; i++) { - get - { - return Request.GetModel().FindType("Microsoft.AspNetCore.OData.E2E.Tests.Typeless.TypelessAddress") as IEdmComplexType; - } + dynamic typelessCustomer = new EdmDeltaResourceObject(DeltaCustomerType); + typelessCustomer.Id = i; + typelessCustomer.Name = string.Format("Name {0}", i); + typelessCustomer.FavoriteNumbers = Enumerable.Range(0, i).ToArray(); + changedCollection.Add(typelessCustomer); } - public IActionResult Get() + //Deleted objects are represented as EdmDeltaDeletedObjects + for (int i = 10; i < 15; i++) { - EdmChangedObjectCollection changedCollection = new EdmChangedObjectCollection(DeltaCustomerType); - //Changed or Modified objects are represented as EdmDeltaResourceObjects - for (int i = 0; i < 10; i++) - { - dynamic typelessCustomer = new EdmDeltaResourceObject(DeltaCustomerType); - typelessCustomer.Id = i; - typelessCustomer.Name = string.Format("Name {0}", i); - typelessCustomer.FavoriteNumbers = Enumerable.Range(0, i).ToArray(); - changedCollection.Add(typelessCustomer); - } - - //Deleted objects are represented as EdmDeltaDeletedObjects - for (int i = 10; i < 15; i++) - { - dynamic typelessCustomer = new EdmDeltaDeletedResourceObject(DeltaCustomerType); - typelessCustomer.Id = new Uri(i.ToString(), UriKind.RelativeOrAbsolute); - typelessCustomer.Reason = DeltaDeletedEntryReason.Deleted; - changedCollection.Add(typelessCustomer); - } - - return Ok(changedCollection); + dynamic typelessCustomer = new EdmDeltaDeletedResourceObject(DeltaCustomerType); + typelessCustomer.Id = new Uri(i.ToString(), UriKind.RelativeOrAbsolute); + typelessCustomer.Reason = DeltaDeletedEntryReason.Deleted; + changedCollection.Add(typelessCustomer); } + + return Ok(changedCollection); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessSerializationTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessSerializationTests.cs index 3bc8eb309..fa8e0e0c2 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessSerializationTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Typeless/TypelessSerializationTests.cs @@ -27,411 +27,410 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Typeless +namespace Microsoft.AspNetCore.OData.E2E.Tests.Typeless; + +public class TypelessSerializationTests : WebApiTestBase { - public class TypelessSerializationTests : WebApiTestBase + public TypelessSerializationTests(WebApiTestFixture fixture) + :base(fixture) { - public TypelessSerializationTests(WebApiTestFixture fixture) - :base(fixture) - { - } - - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = GetEdmModel(); - services.ConfigureControllers(typeof(TypelessCustomersController)); - services.AddControllers().AddOData(opt => opt.Expand().AddRouteComponents("odata", edmModel)); - } + } - private static IEdmModel GetEdmModel() - { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - var customers = builder.EntitySet("TypelessCustomers"); - customers.EntityType.Property(c => c.Name).IsRequired(); - var orders = builder.EntitySet("TypelessOrders"); - customers.EntityType.Collection.Action("PrimitiveCollection").ReturnsCollection(); - customers.EntityType.Collection.Action("ComplexObjectCollection").ReturnsCollection(); - customers.EntityType.Collection.Action("EntityCollection").ReturnsCollectionFromEntitySet("TypelessOrders"); - customers.EntityType.Collection.Action("SinglePrimitive").Returns(); - customers.EntityType.Collection.Action("SingleComplexObject").Returns(); - customers.EntityType.Collection.Action("SingleEntity").ReturnsFromEntitySet("TypelessOrders"); - customers.EntityType.Collection.Action("EnumerableOfIEdmObject").ReturnsFromEntitySet("TypelessOrders"); - - var typelessAction = customers.EntityType.Collection.Action("TypelessParameters"); - typelessAction.Parameter("address"); - typelessAction.Parameter("value"); - typelessAction.CollectionParameter("addresses"); - typelessAction.CollectionParameter("values"); - typelessAction.Returns(); - return builder.GetEdmModel(); - } + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = GetEdmModel(); + services.ConfigureControllers(typeof(TypelessCustomersController)); + services.AddControllers().AddOData(opt => opt.Expand().AddRouteComponents("odata", edmModel)); + } - [Theory] - [InlineData("application/json")] - [InlineData("application/json;odata.metadata=none")] - [InlineData("application/json;odata.metadata=minimal")] - [InlineData("application/json;odata.metadata=full")] - public async Task TypelessWorksInAllFormats(string acceptHeader) - { - // Arrange - string url = "odata/TypelessCustomers"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptHeader)); - HttpClient client = CreateClient(); - - // Act & Assert - HttpResponseMessage response = await client.SendAsync(request); - response.EnsureSuccessStatusCode(); - } + private static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + var customers = builder.EntitySet("TypelessCustomers"); + customers.EntityType.Property(c => c.Name).IsRequired(); + var orders = builder.EntitySet("TypelessOrders"); + customers.EntityType.Collection.Action("PrimitiveCollection").ReturnsCollection(); + customers.EntityType.Collection.Action("ComplexObjectCollection").ReturnsCollection(); + customers.EntityType.Collection.Action("EntityCollection").ReturnsCollectionFromEntitySet("TypelessOrders"); + customers.EntityType.Collection.Action("SinglePrimitive").Returns(); + customers.EntityType.Collection.Action("SingleComplexObject").Returns(); + customers.EntityType.Collection.Action("SingleEntity").ReturnsFromEntitySet("TypelessOrders"); + customers.EntityType.Collection.Action("EnumerableOfIEdmObject").ReturnsFromEntitySet("TypelessOrders"); + + var typelessAction = customers.EntityType.Collection.Action("TypelessParameters"); + typelessAction.Parameter("address"); + typelessAction.Parameter("value"); + typelessAction.CollectionParameter("addresses"); + typelessAction.CollectionParameter("values"); + typelessAction.Returns(); + return builder.GetEdmModel(); + } - [Theory] - [InlineData("PrimitiveCollection")] - [InlineData("ComplexObjectCollection")] - [InlineData("EntityCollection")] - [InlineData("SinglePrimitive")] - [InlineData("SingleComplexObject")] - [InlineData("SingleEntity")] - public async Task TypelessWorksForAllKindsOfDataTypes(string actionName) - { - // Arrange - object expectedPayload = null; - expectedPayload = (actionName == "PrimitiveCollection") ? new { value = Enumerable.Range(1, 10) } : expectedPayload; - expectedPayload = (actionName == "ComplexObjectCollection") ? new { value = CreateAddresses(10) } : expectedPayload; - expectedPayload = (actionName == "EntityCollection") ? new { value = CreateOrders(10) } : expectedPayload; - expectedPayload = (actionName == "SinglePrimitive") ? new { value = 10 } : expectedPayload; - expectedPayload = (actionName == "SingleComplexObject") ? CreateAddress(10) : expectedPayload; - expectedPayload = (actionName == "SingleEntity") ? CreateOrder(10) : expectedPayload; - - HttpClient client = CreateClient(); - string url = "odata/TypelessCustomers/Default." + actionName; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.NotNull(response.Content); - JToken result = await response.Content.ReadAsObject(); - Assert.Equal(JToken.FromObject(expectedPayload), result, JToken.EqualityComparer); - } + [Theory] + [InlineData("application/json")] + [InlineData("application/json;odata.metadata=none")] + [InlineData("application/json;odata.metadata=minimal")] + [InlineData("application/json;odata.metadata=full")] + public async Task TypelessWorksInAllFormats(string acceptHeader) + { + // Arrange + string url = "odata/TypelessCustomers"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptHeader)); + HttpClient client = CreateClient(); + + // Act & Assert + HttpResponseMessage response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + } - [Fact] - public async Task RoundTripEntityWorks() - { - // Arrange - int i = 10; - JObject typelessCustomer = new JObject(); - typelessCustomer["Id"] = i; - typelessCustomer["Name"] = string.Format("Name {0}", i); - typelessCustomer["Orders"] = CreateOrders(i); - typelessCustomer["Addresses"] = CreateAddresses(i); - typelessCustomer["FavoriteNumbers"] = new JArray(Enumerable.Range(0, i).ToArray()); - HttpClient client = CreateClient(); - - string url = "odata/TypelessCustomers"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url); - string payload = typelessCustomer.ToString(); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - - // Arrange - HttpRequestMessage getRequest = new HttpRequestMessage(HttpMethod.Get, string.Format("{0}({1})?$expand=Orders", url, i)); - getRequest.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); - - // Act - HttpResponseMessage getResponse = await client.SendAsync(getRequest); - - // Assert - Assert.True(getResponse.IsSuccessStatusCode); - Assert.NotNull(getResponse.Content); - JObject returnedObject = await getResponse.Content.ReadAsObject(); - Assert.Equal(typelessCustomer, returnedObject, JToken.EqualityComparer); - } + [Theory] + [InlineData("PrimitiveCollection")] + [InlineData("ComplexObjectCollection")] + [InlineData("EntityCollection")] + [InlineData("SinglePrimitive")] + [InlineData("SingleComplexObject")] + [InlineData("SingleEntity")] + public async Task TypelessWorksForAllKindsOfDataTypes(string actionName) + { + // Arrange + object expectedPayload = null; + expectedPayload = (actionName == "PrimitiveCollection") ? new { value = Enumerable.Range(1, 10) } : expectedPayload; + expectedPayload = (actionName == "ComplexObjectCollection") ? new { value = CreateAddresses(10) } : expectedPayload; + expectedPayload = (actionName == "EntityCollection") ? new { value = CreateOrders(10) } : expectedPayload; + expectedPayload = (actionName == "SinglePrimitive") ? new { value = 10 } : expectedPayload; + expectedPayload = (actionName == "SingleComplexObject") ? CreateAddress(10) : expectedPayload; + expectedPayload = (actionName == "SingleEntity") ? CreateOrder(10) : expectedPayload; + + HttpClient client = CreateClient(); + string url = "odata/TypelessCustomers/Default." + actionName; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.NotNull(response.Content); + JToken result = await response.Content.ReadAsObject(); + Assert.Equal(JToken.FromObject(expectedPayload), result, JToken.EqualityComparer); + } - [Fact] - public async Task TypelessActionParametersRoundtrip() - { - // Arrange - HttpClient client = CreateClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/TypelessCustomers/Default.TypelessParameters"); - object obj = new { address = CreateAddress(5), value = 5, addresses = CreateAddresses(10), values = Enumerable.Range(0, 5) }; - string payload = (JToken.FromObject(obj) as JObject).ToString(); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - } + [Fact] + public async Task RoundTripEntityWorks() + { + // Arrange + int i = 10; + JObject typelessCustomer = new JObject(); + typelessCustomer["Id"] = i; + typelessCustomer["Name"] = string.Format("Name {0}", i); + typelessCustomer["Orders"] = CreateOrders(i); + typelessCustomer["Addresses"] = CreateAddresses(i); + typelessCustomer["FavoriteNumbers"] = new JArray(Enumerable.Range(0, i).ToArray()); + HttpClient client = CreateClient(); + + string url = "odata/TypelessCustomers"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url); + string payload = typelessCustomer.ToString(); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + + // Arrange + HttpRequestMessage getRequest = new HttpRequestMessage(HttpMethod.Get, string.Format("{0}({1})?$expand=Orders", url, i)); + getRequest.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none")); + + // Act + HttpResponseMessage getResponse = await client.SendAsync(getRequest); + + // Assert + Assert.True(getResponse.IsSuccessStatusCode); + Assert.NotNull(getResponse.Content); + JObject returnedObject = await getResponse.Content.ReadAsObject(); + Assert.Equal(typelessCustomer, returnedObject, JToken.EqualityComparer); + } - private static JArray CreateAddresses(int i) - { - JArray addresses = new JArray(); - for (int j = 0; j < i; j++) - { - JObject complexObject = CreateAddress(j); - addresses.Add(complexObject); - } - return addresses; - } + [Fact] + public async Task TypelessActionParametersRoundtrip() + { + // Arrange + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/TypelessCustomers/Default.TypelessParameters"); + object obj = new { address = CreateAddress(5), value = 5, addresses = CreateAddresses(10), values = Enumerable.Range(0, 5) }; + string payload = (JToken.FromObject(obj) as JObject).ToString(); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + } - private static JArray CreateOrders(int i) + private static JArray CreateAddresses(int i) + { + JArray addresses = new JArray(); + for (int j = 0; j < i; j++) { - JArray orders = new JArray(); - for (int j = 0; j < i; j++) - { - JObject order = new JObject(); - order["Id"] = j; - order["ShippingAddress"] = CreateAddress(j); - orders.Add(order); - } - return orders; + JObject complexObject = CreateAddress(j); + addresses.Add(complexObject); } + return addresses; + } - private static JObject CreateOrder(int j) + private static JArray CreateOrders(int i) + { + JArray orders = new JArray(); + for (int j = 0; j < i; j++) { JObject order = new JObject(); order["Id"] = j; order["ShippingAddress"] = CreateAddress(j); - return order; + orders.Add(order); } + return orders; + } - private static JObject CreateAddress(int j) - { - JObject address = new JObject(); - address["FirstLine"] = "First line " + j; - address["SecondLine"] = "Second line " + j; - address["ZipCode"] = j; - address["City"] = "City " + j; - address["State"] = "State " + j; - return address; - } + private static JObject CreateOrder(int j) + { + JObject order = new JObject(); + order["Id"] = j; + order["ShippingAddress"] = CreateAddress(j); + return order; } - public class TypelessCustomersController : ODataController + private static JObject CreateAddress(int j) { - private static IEdmEntityObject postedCustomer = null; + JObject address = new JObject(); + address["FirstLine"] = "First line " + j; + address["SecondLine"] = "Second line " + j; + address["ZipCode"] = j; + address["City"] = "City " + j; + address["State"] = "State " + j; + return address; + } +} - public IEdmEntityType CustomerType - { - get - { - return Request.GetModel().FindType("Microsoft.AspNetCore.OData.E2E.Tests.Typeless.TypelessCustomer") as IEdmEntityType; - } - } +public class TypelessCustomersController : ODataController +{ + private static IEdmEntityObject postedCustomer = null; - public IEdmEntityType OrderType + public IEdmEntityType CustomerType + { + get { - get - { - return Request.GetModel().FindType("Microsoft.AspNetCore.OData.E2E.Tests.Typeless.TypelessOrder") as IEdmEntityType; - } + return Request.GetModel().FindType("Microsoft.AspNetCore.OData.E2E.Tests.Typeless.TypelessCustomer") as IEdmEntityType; } + } - public IEdmComplexType AddressType + public IEdmEntityType OrderType + { + get { - get - { - return Request.GetModel().FindType("Microsoft.AspNetCore.OData.E2E.Tests.Typeless.TypelessAddress") as IEdmComplexType; - } + return Request.GetModel().FindType("Microsoft.AspNetCore.OData.E2E.Tests.Typeless.TypelessOrder") as IEdmEntityType; } + } - public IActionResult Get() + public IEdmComplexType AddressType + { + get { - IEdmEntityObject[] typelessCustomers = new EdmEntityObject[20]; - for (int i = 0; i < 20; i++) - { - dynamic typelessCustomer = new EdmEntityObject(CustomerType); - typelessCustomer.Id = i; - typelessCustomer.Name = string.Format("Name {0}", i); - typelessCustomer.Orders = CreateOrders(i); - typelessCustomer.Addresses = CreateAddresses(i); - typelessCustomer.FavoriteNumbers = Enumerable.Range(0, i).ToArray(); - typelessCustomers[i] = typelessCustomer; - } - - IEdmCollectionTypeReference entityCollectionType = - new EdmCollectionTypeReference( - new EdmCollectionType( - new EdmEntityTypeReference(CustomerType, isNullable: false))); - - return Ok(new EdmEntityObjectCollection(entityCollectionType, typelessCustomers.ToList())); + return Request.GetModel().FindType("Microsoft.AspNetCore.OData.E2E.Tests.Typeless.TypelessAddress") as IEdmComplexType; } + } - public IActionResult Get([FromODataUri] int key) + public IActionResult Get() + { + IEdmEntityObject[] typelessCustomers = new EdmEntityObject[20]; + for (int i = 0; i < 20; i++) { - object id; - if (postedCustomer == null || !postedCustomer.TryGetPropertyValue("Id", out id) || key != (int)id) - { - return BadRequest("The key isn't the one posted to the customer"); - } - - ODataQueryContext context = new ODataQueryContext(Request.GetModel(), CustomerType, path: null); - ODataQueryOptions query = new ODataQueryOptions(context, Request); - if (query.SelectExpand != null) - { - Request.ODataFeature().SelectExpandClause = query.SelectExpand.SelectExpandClause; - } - - return Ok(postedCustomer); + dynamic typelessCustomer = new EdmEntityObject(CustomerType); + typelessCustomer.Id = i; + typelessCustomer.Name = string.Format("Name {0}", i); + typelessCustomer.Orders = CreateOrders(i); + typelessCustomer.Addresses = CreateAddresses(i); + typelessCustomer.FavoriteNumbers = Enumerable.Range(0, i).ToArray(); + typelessCustomers[i] = typelessCustomer; } - public IActionResult Post([FromBody]IEdmEntityObject customer) - { - if (!ModelState.IsValid) - { - return BadRequest("customer is null"); - } - postedCustomer = customer; - object id; - customer.TryGetPropertyValue("Id", out id); - - IEdmEntitySet entitySet = Request.GetModel().EntityContainer.FindEntitySet("TypelessCustomers"); - return Created(Request.CreateODataLink(new EntitySetSegment(entitySet), - new KeySegment(new[] { new KeyValuePair("Id", id) }, entitySet.EntityType, null)), customer); - } + IEdmCollectionTypeReference entityCollectionType = + new EdmCollectionTypeReference( + new EdmCollectionType( + new EdmEntityTypeReference(CustomerType, isNullable: false))); - [HttpPost] - public IActionResult PrimitiveCollection() - { - return Ok(Enumerable.Range(1, 10)); - } + return Ok(new EdmEntityObjectCollection(entityCollectionType, typelessCustomers.ToList())); + } - [HttpPost] - public IActionResult ComplexObjectCollection() + public IActionResult Get([FromODataUri] int key) + { + object id; + if (postedCustomer == null || !postedCustomer.TryGetPropertyValue("Id", out id) || key != (int)id) { - return Ok(CreateAddresses(10)); + return BadRequest("The key isn't the one posted to the customer"); } - [HttpPost] - public IActionResult EntityCollection() + ODataQueryContext context = new ODataQueryContext(Request.GetModel(), CustomerType, path: null); + ODataQueryOptions query = new ODataQueryOptions(context, Request); + if (query.SelectExpand != null) { - return Ok(CreateOrders(10)); + Request.ODataFeature().SelectExpandClause = query.SelectExpand.SelectExpandClause; } - [HttpPost] - public IActionResult SinglePrimitive() - { - return Ok(10); - } + return Ok(postedCustomer); + } - [HttpPost] - public IActionResult SingleComplexObject() + public IActionResult Post([FromBody]IEdmEntityObject customer) + { + if (!ModelState.IsValid) { - return Ok(CreateAddress(10)); + return BadRequest("customer is null"); } + postedCustomer = customer; + object id; + customer.TryGetPropertyValue("Id", out id); - [HttpPost] - public IActionResult SingleEntity() - { - return Ok(CreateOrder(10)); - } + IEdmEntitySet entitySet = Request.GetModel().EntityContainer.FindEntitySet("TypelessCustomers"); + return Created(Request.CreateODataLink(new EntitySetSegment(entitySet), + new KeySegment(new[] { new KeyValuePair("Id", id) }, entitySet.EntityType, null)), customer); + } - public IActionResult EnumerableOfIEdmObject() - { - IList result = Enumerable.Range(0, 10).Select(i => (IEdmEntityObject)CreateOrder(i)).ToList(); - return Ok(result); - } + [HttpPost] + public IActionResult PrimitiveCollection() + { + return Ok(Enumerable.Range(1, 10)); + } + + [HttpPost] + public IActionResult ComplexObjectCollection() + { + return Ok(CreateAddresses(10)); + } + + [HttpPost] + public IActionResult EntityCollection() + { + return Ok(CreateOrders(10)); + } + + [HttpPost] + public IActionResult SinglePrimitive() + { + return Ok(10); + } + + [HttpPost] + public IActionResult SingleComplexObject() + { + return Ok(CreateAddress(10)); + } + + [HttpPost] + public IActionResult SingleEntity() + { + return Ok(CreateOrder(10)); + } + + public IActionResult EnumerableOfIEdmObject() + { + IList result = Enumerable.Range(0, 10).Select(i => (IEdmEntityObject)CreateOrder(i)).ToList(); + return Ok(result); + } - [HttpPost] - public IActionResult TypelessParameters(ODataUntypedActionParameters parameters) + [HttpPost] + public IActionResult TypelessParameters(ODataUntypedActionParameters parameters) + { + if (!ModelState.IsValid) { - if (!ModelState.IsValid) - { - return BadRequest("parameters is null"); - } - object address; - object addresses; - object value; - object values; - if (!parameters.TryGetValue("address", out address) || address as IEdmComplexObject == null || - !parameters.TryGetValue("addresses", out addresses) || addresses as IEnumerable == null || - !parameters.TryGetValue("value", out value) || (int)value != 5 || - !parameters.TryGetValue("values", out values) || values as IEnumerable == null || - !(values as IEnumerable).Cast().SequenceEqual(Enumerable.Range(0, 5))) - { - return BadRequest("Address is not present or is not a complex object"); - } - return Ok(address as IEdmComplexObject); + return BadRequest("parameters is null"); } - - private dynamic CreateAddresses(int i) + object address; + object addresses; + object value; + object values; + if (!parameters.TryGetValue("address", out address) || address as IEdmComplexObject == null || + !parameters.TryGetValue("addresses", out addresses) || addresses as IEnumerable == null || + !parameters.TryGetValue("value", out value) || (int)value != 5 || + !parameters.TryGetValue("values", out values) || values as IEnumerable == null || + !(values as IEnumerable).Cast().SequenceEqual(Enumerable.Range(0, 5))) { - EdmComplexObject[] addresses = new EdmComplexObject[i]; - for (int j = 0; j < i; j++) - { - dynamic complexObject = CreateAddress(j); - addresses[j] = complexObject; - } - var collection = new EdmComplexObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(AddressType, false))), addresses); - return collection; + return BadRequest("Address is not present or is not a complex object"); } + return Ok(address as IEdmComplexObject); + } - private dynamic CreateOrders(int i) + private dynamic CreateAddresses(int i) + { + EdmComplexObject[] addresses = new EdmComplexObject[i]; + for (int j = 0; j < i; j++) { - EdmEntityObject[] orders = new EdmEntityObject[i]; - for (int j = 0; j < i; j++) - { - dynamic order = new EdmEntityObject(OrderType); - order.Id = j; - order.ShippingAddress = CreateAddress(j); - orders[j] = order; - } - var collection = new EdmEntityObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(OrderType, false))), orders); - return collection; + dynamic complexObject = CreateAddress(j); + addresses[j] = complexObject; } + var collection = new EdmComplexObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(AddressType, false))), addresses); + return collection; + } - private dynamic CreateOrder(int j) + private dynamic CreateOrders(int i) + { + EdmEntityObject[] orders = new EdmEntityObject[i]; + for (int j = 0; j < i; j++) { dynamic order = new EdmEntityObject(OrderType); order.Id = j; order.ShippingAddress = CreateAddress(j); - return order; - } - - private dynamic CreateAddress(int j) - { - dynamic address = new EdmComplexObject(AddressType); - address.FirstLine = "First line " + j; - address.SecondLine = "Second line " + j; - address.ZipCode = j; - address.City = "City " + j; - address.State = "State " + j; - return address; + orders[j] = order; } + var collection = new EdmEntityObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(OrderType, false))), orders); + return collection; } - public class TypelessCustomer + private dynamic CreateOrder(int j) { - public int Id { get; set; } - public string Name { get; set; } - public virtual IList Orders { get; set; } - public virtual IList Addresses { get; set; } - public virtual IList FavoriteNumbers { get; set; } + dynamic order = new EdmEntityObject(OrderType); + order.Id = j; + order.ShippingAddress = CreateAddress(j); + return order; } - public class TypelessOrder + private dynamic CreateAddress(int j) { - public int Id { get; set; } - public TypelessAddress ShippingAddress { get; set; } + dynamic address = new EdmComplexObject(AddressType); + address.FirstLine = "First line " + j; + address.SecondLine = "Second line " + j; + address.ZipCode = j; + address.City = "City " + j; + address.State = "State " + j; + return address; } +} - public class TypelessAddress - { - public string FirstLine { get; set; } - public string SecondLine { get; set; } - public int ZipCode { get; set; } - public string City { get; set; } - public string State { get; set; } - } +public class TypelessCustomer +{ + public int Id { get; set; } + public string Name { get; set; } + public virtual IList Orders { get; set; } + public virtual IList Addresses { get; set; } + public virtual IList FavoriteNumbers { get; set; } +} + +public class TypelessOrder +{ + public int Id { get; set; } + public TypelessAddress ShippingAddress { get; set; } +} + +public class TypelessAddress +{ + public string FirstLine { get; set; } + public string SecondLine { get; set; } + public int ZipCode { get; set; } + public string City { get; set; } + public string State { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationController.cs index 971797b71..176bd9fad 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationController.cs @@ -15,213 +15,212 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.UnboundOperation +namespace Microsoft.AspNetCore.OData.E2E.Tests.UnboundOperation; + +public class ConventionCustomersController : ODataController { - public class ConventionCustomersController : ODataController - { - private static IList _customers = null; + private static IList _customers = null; - private IList InitCustomers() + private IList InitCustomers() + { + IList customers = Enumerable.Range(1, 10).Select(i => + new ConventionCustomer { - IList customers = Enumerable.Range(1, 10).Select(i => - new ConventionCustomer + ID = 400 + i, + Name = "Name " + i, + Address = new ConventionAddress() { - ID = 400 + i, - Name = "Name " + i, - Address = new ConventionAddress() - { - Street = "Street " + i, - City = "City " + i, - ZipCode = (201100 + i).ToString() - }, - Orders = Enumerable.Range(1, i).Select(j => - new ConventionOrder - { - ID = j, - OrderName = "OrderName " + j, - Price = j, - OrderGuid = Guid.Empty - }).ToList() - }).ToList(); - - return customers; - } + Street = "Street " + i, + City = "City " + i, + ZipCode = (201100 + i).ToString() + }, + Orders = Enumerable.Range(1, i).Select(j => + new ConventionOrder + { + ID = j, + OrderName = "OrderName " + j, + Price = j, + OrderGuid = Guid.Empty + }).ToList() + }).ToList(); + + return customers; + } - public ConventionCustomersController() + public ConventionCustomersController() + { + if (_customers == null) { - if (_customers == null) - { - _customers = InitCustomers(); - } + _customers = InitCustomers(); } + } - public IList Customers => _customers; + public IList Customers => _customers; - [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] - public IActionResult Get() - { - return Ok(_customers.AsQueryable()); - } + [EnableQuery(PageSize = 10, MaxExpansionDepth = 5)] + public IActionResult Get() + { + return Ok(_customers.AsQueryable()); + } - // It's a top level function without parameters - [EnableQuery] - [HttpGet("odata/GetAllConventionCustomers()")] - [HttpGet("odata/GetAllConventionCustomersImport()")] - public IEnumerable GetAllConventionCustomers() - { - return _customers; - } + // It's a top level function without parameters + [EnableQuery] + [HttpGet("odata/GetAllConventionCustomers()")] + [HttpGet("odata/GetAllConventionCustomersImport()")] + public IEnumerable GetAllConventionCustomers() + { + return _customers; + } + + [EnableQuery] + [HttpGet("odata/GetAllConventionCustomersImport(CustomerName={customerName})")] + [HttpGet("odata/GetAllConventionCustomersImport(CustomerName={customerName})/$count")] + // [FromODataUri] can not be deleted within below line, or the value of OrderName will be enclosed by single quote mark('). + public IEnumerable GetAllConventionCustomers([FromODataUri]String CustomerName) + { + IEnumerable customers = _customers.Where(c => c.Name.Contains(CustomerName)); + return customers; + } + + // It's a top level function with one parameter] + [HttpGet("odata/GetConventionCustomerById(CustomerId={CustomerId})")] + [HttpGet("odata/GetConventionCustomerByIdImport(CustomerId={CustomerId})")] + public ConventionCustomer GetConventionCustomerById(int CustomerId) + { + return _customers.Where(c => c.ID == CustomerId).FirstOrDefault(); + } + + [HttpGet("odata/GetConventionCustomerNameByIdImport(CustomerId={CustomerId})")] + [HttpGet("odata/GetConventionCustomerByIdImport(CustomerId={CustomerId})/Name")] + public String GetConventionCustomerNameById([FromODataUri]int CustomerId) + { + return _customers.Where(c => c.ID == CustomerId).FirstOrDefault().Name; + } + + [HttpGet("odata/GetConventionOrderByCustomerIdAndOrderName(CustomerId={CustomerId},OrderName={OrderName})")] + [HttpGet("odata/GetConventionOrderByCustomerIdAndOrderNameImport(CustomerId={CustomerId},OrderName={OrderName})")] + public ConventionOrder GetConventionOrderByCustomerIdAndOrderName(int CustomerId, [FromODataUri]string OrderName) + { + ConventionCustomer customer = _customers.Where(c => c.ID == CustomerId).FirstOrDefault(); + return customer.Orders.Where(o => o.OrderName == OrderName).FirstOrDefault(); + } + + [HttpGet("odata/AdvancedFunction(nums={numbers},genders={genders},location={address},addresses={addresses},customer={customer},customers={customers})")] + public bool AdvancedFunction([FromODataUri]IEnumerable numbers, + [FromODataUri]IEnumerable genders, + [FromODataUri]ConventionAddress address, [FromODataUri]IEnumerable addresses, + [FromODataUri]ConventionCustomer customer, [FromODataUri]IEnumerable customers) + { + Assert.Equal(new[] {1, 2, 3}, numbers); + Assert.Equal(new[] {ConventionGender.Male, ConventionGender.Female}, genders); - [EnableQuery] - [HttpGet("odata/GetAllConventionCustomersImport(CustomerName={customerName})")] - [HttpGet("odata/GetAllConventionCustomersImport(CustomerName={customerName})/$count")] - // [FromODataUri] can not be deleted within below line, or the value of OrderName will be enclosed by single quote mark('). - public IEnumerable GetAllConventionCustomers([FromODataUri]String CustomerName) + IEnumerable newAddress = addresses.Concat(new[] {address}); + Assert.Equal(2, newAddress.Count()); + foreach (ConventionAddress addr in newAddress) { - IEnumerable customers = _customers.Where(c => c.Name.Contains(CustomerName)); - return customers; + Assert.Equal("Zi Xin Rd.", addr.Street); + Assert.Equal("Shanghai", addr.City); + Assert.Equal("2001100", addr.ZipCode); } - // It's a top level function with one parameter] - [HttpGet("odata/GetConventionCustomerById(CustomerId={CustomerId})")] - [HttpGet("odata/GetConventionCustomerByIdImport(CustomerId={CustomerId})")] - public ConventionCustomer GetConventionCustomerById(int CustomerId) + IEnumerable newCustomers = customers.Concat(new[] { customer}); + Assert.Equal(2, newCustomers.Count()); + foreach (ConventionCustomer cust in newCustomers) { - return _customers.Where(c => c.ID == CustomerId).FirstOrDefault(); + Assert.Equal(7, cust.ID); + Assert.Equal("Tony", cust.Name); + Assert.Null(cust.Address); } - [HttpGet("odata/GetConventionCustomerNameByIdImport(CustomerId={CustomerId})")] - [HttpGet("odata/GetConventionCustomerByIdImport(CustomerId={CustomerId})/Name")] - public String GetConventionCustomerNameById([FromODataUri]int CustomerId) + return true; + } + + [EnableQuery] + [HttpGet("odata/GetDefinedGenders()")] + [HttpGet("odata/GetDefinedGenders()/$count")] + public IActionResult GetDefinedGenders() + { + IList genders = new List(); + genders.Add(ConventionGender.Male); + genders.Add(ConventionGender.Female); + return Ok(genders); + } + + [EnableQuery] + [HttpPost("odata/UpdateAddress")] + [HttpPost("odata/UpdateAddressImport")] + public IActionResult UpdateAddress([FromBody]ODataUntypedActionParameters parameters) + { + if (!ModelState.IsValid) { - return _customers.Where(c => c.ID == CustomerId).FirstOrDefault().Name; + return BadRequest(ModelState); } - - [HttpGet("odata/GetConventionOrderByCustomerIdAndOrderName(CustomerId={CustomerId},OrderName={OrderName})")] - [HttpGet("odata/GetConventionOrderByCustomerIdAndOrderNameImport(CustomerId={CustomerId},OrderName={OrderName})")] - public ConventionOrder GetConventionOrderByCustomerIdAndOrderName(int CustomerId, [FromODataUri]string OrderName) + var id = (int)parameters["ID"]; + var address = parameters["Address"] as EdmComplexObject; + var conventionAddress = new ConventionAddress(); + object temp = null; + if (address.TryGetPropertyValue("Street", out temp)) { - ConventionCustomer customer = _customers.Where(c => c.ID == CustomerId).FirstOrDefault(); - return customer.Orders.Where(o => o.OrderName == OrderName).FirstOrDefault(); + conventionAddress.Street = temp.ToString(); + Assert.Equal("Street 11", conventionAddress.Street); } - - [HttpGet("odata/AdvancedFunction(nums={numbers},genders={genders},location={address},addresses={addresses},customer={customer},customers={customers})")] - public bool AdvancedFunction([FromODataUri]IEnumerable numbers, - [FromODataUri]IEnumerable genders, - [FromODataUri]ConventionAddress address, [FromODataUri]IEnumerable addresses, - [FromODataUri]ConventionCustomer customer, [FromODataUri]IEnumerable customers) + if (address.TryGetPropertyValue("City", out temp)) { - Assert.Equal(new[] {1, 2, 3}, numbers); - Assert.Equal(new[] {ConventionGender.Male, ConventionGender.Female}, genders); - - IEnumerable newAddress = addresses.Concat(new[] {address}); - Assert.Equal(2, newAddress.Count()); - foreach (ConventionAddress addr in newAddress) - { - Assert.Equal("Zi Xin Rd.", addr.Street); - Assert.Equal("Shanghai", addr.City); - Assert.Equal("2001100", addr.ZipCode); - } - - IEnumerable newCustomers = customers.Concat(new[] { customer}); - Assert.Equal(2, newCustomers.Count()); - foreach (ConventionCustomer cust in newCustomers) - { - Assert.Equal(7, cust.ID); - Assert.Equal("Tony", cust.Name); - Assert.Null(cust.Address); - } - - return true; + conventionAddress.City = temp.ToString(); } - - [EnableQuery] - [HttpGet("odata/GetDefinedGenders()")] - [HttpGet("odata/GetDefinedGenders()/$count")] - public IActionResult GetDefinedGenders() + if (address.TryGetPropertyValue("ZipCode", out temp)) { - IList genders = new List(); - genders.Add(ConventionGender.Male); - genders.Add(ConventionGender.Female); - return Ok(genders); + conventionAddress.ZipCode = temp.ToString(); } - [EnableQuery] - [HttpPost("odata/UpdateAddress")] - [HttpPost("odata/UpdateAddressImport")] - public IActionResult UpdateAddress([FromBody]ODataUntypedActionParameters parameters) + // In real scenario, we should update the original data, but for test, let's create a new Database + var customers = InitCustomers(); + ConventionCustomer customer = customers.Where(c => c.ID == id).FirstOrDefault(); + customer.Address = conventionAddress; + return Ok(customers); + } + + /* + [HttpPost("odata/CreateCustomer")] + [HttpPost("odata/CreateCustomerImport")] + public IActionResult CreateCustomer(ODataActionParameters parameters) + { + if (!ModelState.IsValid) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - var id = (int)parameters["ID"]; - var address = parameters["Address"] as EdmComplexObject; - var conventionAddress = new ConventionAddress(); - object temp = null; - if (address.TryGetPropertyValue("Street", out temp)) - { - conventionAddress.Street = temp.ToString(); - Assert.Equal("Street 11", conventionAddress.Street); - } - if (address.TryGetPropertyValue("City", out temp)) - { - conventionAddress.City = temp.ToString(); - } - if (address.TryGetPropertyValue("ZipCode", out temp)) - { - conventionAddress.ZipCode = temp.ToString(); - } - - // In real scenario, we should update the original data, but for test, let's create a new Database - var customers = InitCustomers(); - ConventionCustomer customer = customers.Where(c => c.ID == id).FirstOrDefault(); - customer.Address = conventionAddress; - return Ok(customers); + return BadRequest(ModelState); } + var conventionCustomer =(ConventionCustomer) parameters["value"]; + conventionCustomer.ID = _customers.Count() + 1; + _customers.Add(conventionCustomer); + return Ok(_customers); + } + * */ - /* - [HttpPost("odata/CreateCustomer")] - [HttpPost("odata/CreateCustomerImport")] - public IActionResult CreateCustomer(ODataActionParameters parameters) + [HttpPost("odata/AdvancedAction")] + public IActionResult AdvancedAction([FromBody]ODataActionParameters parameters) + { + Assert.NotNull(parameters); + Assert.Equal(new[] { 4, 5, 6 }, parameters["nums"] as IEnumerable); + Assert.Equal(new[] { ConventionGender.Male, ConventionGender.Female }, parameters["genders"] as IEnumerable); + + IList newAddress = (parameters["addresses"] as IEnumerable).ToList(); + Assert.Single(newAddress); + foreach (ConventionAddress addr in newAddress.Concat(new[] {parameters["location"]})) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - var conventionCustomer =(ConventionCustomer) parameters["value"]; - conventionCustomer.ID = _customers.Count() + 1; - _customers.Add(conventionCustomer); - return Ok(_customers); + Assert.Equal("NY Rd.", addr.Street); + Assert.Equal("Redmond", addr.City); + Assert.Equal("9011", addr.ZipCode); } - * */ - [HttpPost("odata/AdvancedAction")] - public IActionResult AdvancedAction([FromBody]ODataActionParameters parameters) + IList newCustomers = (parameters["customers"] as IEnumerable).ToList(); + Assert.Single(newAddress); + foreach (ConventionCustomer cust in newCustomers.Concat(new[] { parameters["customer"] })) { - Assert.NotNull(parameters); - Assert.Equal(new[] { 4, 5, 6 }, parameters["nums"] as IEnumerable); - Assert.Equal(new[] { ConventionGender.Male, ConventionGender.Female }, parameters["genders"] as IEnumerable); - - IList newAddress = (parameters["addresses"] as IEnumerable).ToList(); - Assert.Single(newAddress); - foreach (ConventionAddress addr in newAddress.Concat(new[] {parameters["location"]})) - { - Assert.Equal("NY Rd.", addr.Street); - Assert.Equal("Redmond", addr.City); - Assert.Equal("9011", addr.ZipCode); - } - - IList newCustomers = (parameters["customers"] as IEnumerable).ToList(); - Assert.Single(newAddress); - foreach (ConventionCustomer cust in newCustomers.Concat(new[] { parameters["customer"] })) - { - Assert.Equal(8, cust.ID); - Assert.Equal("Mike", cust.Name); - Assert.Null(cust.Address); - } - - return Ok(); + Assert.Equal(8, cust.ID); + Assert.Equal("Mike", cust.Name); + Assert.Null(cust.Address); } + + return Ok(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationDataModel.cs index 46eba8ea4..28109be64 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationDataModel.cs @@ -8,43 +8,42 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.UnboundOperation +namespace Microsoft.AspNetCore.OData.E2E.Tests.UnboundOperation; + +public class ConventionCustomer { - public class ConventionCustomer - { - public int ID { get; set; } + public int ID { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public ConventionAddress Address { get; set; } + public ConventionAddress Address { get; set; } - public List Orders { get; set; } - } + public List Orders { get; set; } +} - public class ConventionAddress - { - public string Street { get; set; } +public class ConventionAddress +{ + public string Street { get; set; } - public string City { get; set; } + public string City { get; set; } - public string ZipCode { get; set; } - } + public string ZipCode { get; set; } +} - public class ConventionOrder - { - public int ID { get; set; } +public class ConventionOrder +{ + public int ID { get; set; } - public string OrderName { get; set; } + public string OrderName { get; set; } - public decimal Price { get; set; } + public decimal Price { get; set; } - public Guid OrderGuid { get; set; } - } + public Guid OrderGuid { get; set; } +} - public enum ConventionGender - { - Male, +public enum ConventionGender +{ + Male, - Female, - } + Female, } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationEdmModel.cs index 658f0dede..c42996edc 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationEdmModel.cs @@ -9,123 +9,122 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.UnboundOperation +namespace Microsoft.AspNetCore.OData.E2E.Tests.UnboundOperation; + +internal class UnboundFunctionEdmModel { - internal class UnboundFunctionEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("ConventionCustomers"); - builder.EntitySet("ConventionOrders"); - - EnumTypeConfiguration enumType = builder.EnumType(); - enumType.Member(ConventionGender.Female); - enumType.Member(ConventionGender.Male); - - #region functions - FunctionConfiguration getAllCustomers = builder.Function("GetAllConventionCustomers"); - getAllCustomers.ReturnsCollectionFromEntitySet("ConventionCustomers"); - getAllCustomers.IsComposable = true; - - // Return all the customers whose name contains CustomerName - FunctionConfiguration getAllCustomersOverload = builder.Function("GetAllConventionCustomers"); - getAllCustomersOverload.ReturnsCollectionFromEntitySet("ConventionCustomers"); - getAllCustomersOverload.Parameter("CustomerName"); - getAllCustomersOverload.IsComposable = true; - - FunctionConfiguration getCustomersById = builder.Function("GetConventionCustomerById"); - getCustomersById.Parameter("CustomerId"); - getCustomersById.ReturnsFromEntitySet("ConventionCustomers"); - getCustomersById.IsComposable = true; - - FunctionConfiguration getOrder = builder.Function("GetConventionOrderByCustomerIdAndOrderName"); - getOrder.Parameter("CustomerId"); - getOrder.Parameter("OrderName"); - getOrder.ReturnsFromEntitySet("ConventionOrders"); - - FunctionConfiguration getCustomerNameById = builder.Function("GetConventionCustomerNameById"); - getCustomerNameById.Parameter("CustomerId"); - getCustomerNameById.Returns(); - - FunctionConfiguration getDefinedGenders = builder.Function("GetDefinedGenders"); - getDefinedGenders.ReturnsCollection() - .IsComposable = true; - - FunctionConfiguration function = builder.Function("AdvancedFunction").Returns(); - function.CollectionParameter("nums"); - function.CollectionParameter("genders"); - function.Parameter("location"); - function.CollectionParameter("addresses"); - function.EntityParameter("customer"); - function.CollectionEntityParameter("customers"); - - #endregion - - #region actions - // bug: error message: non-binding parameter type must be either Primitive, Complex, Collection of Primitive or a Collection of Complex. - /* - ActionConfiguration createCustomer = builder.Action("CreateCustomer"); - createCustomer.Parameter("Customer"); - createCustomer.ReturnsFromEntitySet("ConventionCustomers"); - */ - - ActionConfiguration udpateAddress = builder.Action("UpdateAddress"); - udpateAddress.Parameter("Address"); - udpateAddress.Parameter("ID"); - udpateAddress.ReturnsCollectionFromEntitySet("ConventionCustomers"); - - ActionConfiguration action = builder.Action("AdvancedAction"); - action.CollectionParameter("nums"); - action.CollectionParameter("genders"); - action.Parameter("location"); - action.CollectionParameter("addresses"); - action.EntityParameter("customer"); - action.CollectionEntityParameter("customers"); - #endregion - - var schemaNamespace = typeof(ConventionCustomer).Namespace; - - builder.Namespace = schemaNamespace; - - var edmModel = builder.GetEdmModel(); - var container = edmModel.EntityContainer as EdmEntityContainer; - - #region function imports - - var entitySet = container.FindEntitySet("ConventionCustomers"); - var getCustomersByIdOfEdmFunction = edmModel.FindDeclaredOperations(schemaNamespace + ".GetConventionCustomerById").First() as EdmFunction; - container.AddFunctionImport("GetConventionCustomerByIdImport", getCustomersByIdOfEdmFunction, new EdmPathExpression(entitySet.Name)); - - var functionsOfGetAllConventionCustomers = edmModel.FindDeclaredOperations(schemaNamespace + ".GetAllConventionCustomers"); - var getAllConventionCustomersOfEdmFunction = functionsOfGetAllConventionCustomers.FirstOrDefault(f => f.Parameters.Count() == 0) as EdmFunction; - container.AddFunctionImport("GetAllConventionCustomersImport", getAllConventionCustomersOfEdmFunction, new EdmPathExpression(entitySet.Name)); - - // TODO delete this overload after bug 1640 is fixed: It can not find the correct overload function if the the function is exposed as a function import. - var getAllConventionCustomersOverloadOfEdmFunction = functionsOfGetAllConventionCustomers.FirstOrDefault(f => f.Parameters.Count() > 0) as EdmFunction; - container.AddFunctionImport("GetAllConventionCustomersImport", getAllConventionCustomersOverloadOfEdmFunction, new EdmPathExpression(entitySet.Name)); - - var entitySet1 = container.FindEntitySet("ConventionOrders"); - var GetConventionOrderByCustomerIdAndOrderNameOfEdmFunction = edmModel.FindDeclaredOperations(schemaNamespace + ".GetConventionOrderByCustomerIdAndOrderName").First() as EdmFunction; - container.AddFunctionImport("GetConventionOrderByCustomerIdAndOrderNameImport", GetConventionOrderByCustomerIdAndOrderNameOfEdmFunction, new EdmPathExpression(entitySet1.Name)); - - var getConventionCustomerNameByIdOfEdmFunction = edmModel.FindDeclaredOperations(schemaNamespace + ".GetConventionCustomerNameById").First() as EdmFunction; - container.AddFunctionImport("GetConventionCustomerNameByIdImport", getConventionCustomerNameByIdOfEdmFunction, null); - - #endregion - - #region action imports - // TODO: it is a potential issue that entity can not be used as an un-bound parameter. - /* - var createCustomerOfEdmAction = edmModel.FindDeclaredOperations(schemaNamespace + ".CreateCustomer").FirstOrDefault() as EdmAction; - container.AddActionImport("CreateCustomerImport", createCustomerOfEdmAction); - */ - var updateAddressOfEdmAction = edmModel.FindDeclaredOperations(schemaNamespace + ".UpdateAddress").FirstOrDefault() as EdmAction; - container.AddActionImport("UpdateAddressImport", updateAddressOfEdmAction, new EdmPathExpression(entitySet.Name)); - - #endregion - - return edmModel; - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("ConventionCustomers"); + builder.EntitySet("ConventionOrders"); + + EnumTypeConfiguration enumType = builder.EnumType(); + enumType.Member(ConventionGender.Female); + enumType.Member(ConventionGender.Male); + + #region functions + FunctionConfiguration getAllCustomers = builder.Function("GetAllConventionCustomers"); + getAllCustomers.ReturnsCollectionFromEntitySet("ConventionCustomers"); + getAllCustomers.IsComposable = true; + + // Return all the customers whose name contains CustomerName + FunctionConfiguration getAllCustomersOverload = builder.Function("GetAllConventionCustomers"); + getAllCustomersOverload.ReturnsCollectionFromEntitySet("ConventionCustomers"); + getAllCustomersOverload.Parameter("CustomerName"); + getAllCustomersOverload.IsComposable = true; + + FunctionConfiguration getCustomersById = builder.Function("GetConventionCustomerById"); + getCustomersById.Parameter("CustomerId"); + getCustomersById.ReturnsFromEntitySet("ConventionCustomers"); + getCustomersById.IsComposable = true; + + FunctionConfiguration getOrder = builder.Function("GetConventionOrderByCustomerIdAndOrderName"); + getOrder.Parameter("CustomerId"); + getOrder.Parameter("OrderName"); + getOrder.ReturnsFromEntitySet("ConventionOrders"); + + FunctionConfiguration getCustomerNameById = builder.Function("GetConventionCustomerNameById"); + getCustomerNameById.Parameter("CustomerId"); + getCustomerNameById.Returns(); + + FunctionConfiguration getDefinedGenders = builder.Function("GetDefinedGenders"); + getDefinedGenders.ReturnsCollection() + .IsComposable = true; + + FunctionConfiguration function = builder.Function("AdvancedFunction").Returns(); + function.CollectionParameter("nums"); + function.CollectionParameter("genders"); + function.Parameter("location"); + function.CollectionParameter("addresses"); + function.EntityParameter("customer"); + function.CollectionEntityParameter("customers"); + + #endregion + + #region actions + // bug: error message: non-binding parameter type must be either Primitive, Complex, Collection of Primitive or a Collection of Complex. + /* + ActionConfiguration createCustomer = builder.Action("CreateCustomer"); + createCustomer.Parameter("Customer"); + createCustomer.ReturnsFromEntitySet("ConventionCustomers"); + */ + + ActionConfiguration udpateAddress = builder.Action("UpdateAddress"); + udpateAddress.Parameter("Address"); + udpateAddress.Parameter("ID"); + udpateAddress.ReturnsCollectionFromEntitySet("ConventionCustomers"); + + ActionConfiguration action = builder.Action("AdvancedAction"); + action.CollectionParameter("nums"); + action.CollectionParameter("genders"); + action.Parameter("location"); + action.CollectionParameter("addresses"); + action.EntityParameter("customer"); + action.CollectionEntityParameter("customers"); + #endregion + + var schemaNamespace = typeof(ConventionCustomer).Namespace; + + builder.Namespace = schemaNamespace; + + var edmModel = builder.GetEdmModel(); + var container = edmModel.EntityContainer as EdmEntityContainer; + + #region function imports + + var entitySet = container.FindEntitySet("ConventionCustomers"); + var getCustomersByIdOfEdmFunction = edmModel.FindDeclaredOperations(schemaNamespace + ".GetConventionCustomerById").First() as EdmFunction; + container.AddFunctionImport("GetConventionCustomerByIdImport", getCustomersByIdOfEdmFunction, new EdmPathExpression(entitySet.Name)); + + var functionsOfGetAllConventionCustomers = edmModel.FindDeclaredOperations(schemaNamespace + ".GetAllConventionCustomers"); + var getAllConventionCustomersOfEdmFunction = functionsOfGetAllConventionCustomers.FirstOrDefault(f => f.Parameters.Count() == 0) as EdmFunction; + container.AddFunctionImport("GetAllConventionCustomersImport", getAllConventionCustomersOfEdmFunction, new EdmPathExpression(entitySet.Name)); + + // TODO delete this overload after bug 1640 is fixed: It can not find the correct overload function if the the function is exposed as a function import. + var getAllConventionCustomersOverloadOfEdmFunction = functionsOfGetAllConventionCustomers.FirstOrDefault(f => f.Parameters.Count() > 0) as EdmFunction; + container.AddFunctionImport("GetAllConventionCustomersImport", getAllConventionCustomersOverloadOfEdmFunction, new EdmPathExpression(entitySet.Name)); + + var entitySet1 = container.FindEntitySet("ConventionOrders"); + var GetConventionOrderByCustomerIdAndOrderNameOfEdmFunction = edmModel.FindDeclaredOperations(schemaNamespace + ".GetConventionOrderByCustomerIdAndOrderName").First() as EdmFunction; + container.AddFunctionImport("GetConventionOrderByCustomerIdAndOrderNameImport", GetConventionOrderByCustomerIdAndOrderNameOfEdmFunction, new EdmPathExpression(entitySet1.Name)); + + var getConventionCustomerNameByIdOfEdmFunction = edmModel.FindDeclaredOperations(schemaNamespace + ".GetConventionCustomerNameById").First() as EdmFunction; + container.AddFunctionImport("GetConventionCustomerNameByIdImport", getConventionCustomerNameByIdOfEdmFunction, null); + + #endregion + + #region action imports + // TODO: it is a potential issue that entity can not be used as an un-bound parameter. + /* + var createCustomerOfEdmAction = edmModel.FindDeclaredOperations(schemaNamespace + ".CreateCustomer").FirstOrDefault() as EdmAction; + container.AddActionImport("CreateCustomerImport", createCustomerOfEdmAction); + */ + var updateAddressOfEdmAction = edmModel.FindDeclaredOperations(schemaNamespace + ".UpdateAddress").FirstOrDefault() as EdmAction; + container.AddActionImport("UpdateAddressImport", updateAddressOfEdmAction, new EdmPathExpression(entitySet.Name)); + + #endregion + + return edmModel; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationTest.cs index 1c9bae11a..2e4f882eb 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/UnboundOperation/UnboundOperationTest.cs @@ -19,313 +19,313 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.UnboundOperation +namespace Microsoft.AspNetCore.OData.E2E.Tests.UnboundOperation; + +public class UnboundOperationTest : WebApiTestBase { - public class UnboundOperationTest : WebApiTestBase + public UnboundOperationTest(WebApiTestFixture fixture) + : base(fixture) { - public UnboundOperationTest(WebApiTestFixture fixture) - : base(fixture) - { - } + } #region Set up - private readonly string EdmSchemaNamespace = typeof(ConventionCustomer).Namespace; + private readonly string EdmSchemaNamespace = typeof(ConventionCustomer).Namespace; - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel model = UnboundFunctionEdmModel.GetEdmModel(); + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel model = UnboundFunctionEdmModel.GetEdmModel(); - services.ConfigureControllers(typeof(ConventionCustomersController), typeof(MetadataController)); + services.ConfigureControllers(typeof(ConventionCustomersController), typeof(MetadataController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model) - .Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); - } + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model) + .Count().Filter().OrderBy().Expand().SetMaxTop(null).Select()); + } - private async Task ResetDatasource() - { - var requestUriForPost = "odata/ResetDataSource"; - var requestForPost = new HttpRequestMessage(HttpMethod.Post, requestUriForPost); + private async Task ResetDatasource() + { + var requestUriForPost = "odata/ResetDataSource"; + var requestForPost = new HttpRequestMessage(HttpMethod.Post, requestUriForPost); - HttpClient client = CreateClient(); - var responseForPost = await client.SendAsync(requestForPost); - Assert.True(responseForPost.IsSuccessStatusCode); + HttpClient client = CreateClient(); + var responseForPost = await client.SendAsync(requestForPost); + Assert.True(responseForPost.IsSuccessStatusCode); - return responseForPost; - } + return responseForPost; + } #endregion #region Model Builder - [Fact] - public async Task MetaDataTest() - { - // Arrange - var requestUri = "odata/$metadata"; - HttpClient client = CreateClient(); + [Fact] + public async Task MetaDataTest() + { + // Arrange + var requestUri = "odata/$metadata"; + HttpClient client = CreateClient(); - // Act - var response = await client.GetAsync(requestUri); - var stream = await response.Content.ReadAsStreamAsync(); + // Act + var response = await client.GetAsync(requestUri); + var stream = await response.Content.ReadAsStreamAsync(); - // Assert - IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); - var reader = new ODataMessageReader(message); - var edmModel = reader.ReadMetadataDocument(); + // Assert + IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); + var reader = new ODataMessageReader(message); + var edmModel = reader.ReadMetadataDocument(); #region functions - // Function GetAllConventionCustomers - var typeOfConventionCustomer = typeof(ConventionCustomer); - var function1 = edmModel.FindDeclaredOperations(typeOfConventionCustomer.Namespace + ".GetAllConventionCustomers").FirstOrDefault(); - Assert.Equal(string.Format("Collection({0})", typeOfConventionCustomer.FullName), function1.ReturnType.Definition.FullTypeName()); - Assert.Empty(function1.Parameters); - - // Function GetConventionCustomerById - var function2 = edmModel.FindDeclaredOperations(typeof(ConventionCustomer).Namespace + ".GetConventionCustomerById").FirstOrDefault(); - Assert.Equal(typeOfConventionCustomer.FullName, function2.ReturnType.Definition.FullTypeName()); - Assert.Single(function2.Parameters); - - // Function GetConventionOrderByCustomerIdAndOrderName - var typeOfConventionOrder = typeof(ConventionOrder); - var function3 = edmModel.FindDeclaredOperations(typeOfConventionOrder.Namespace + ".GetConventionOrderByCustomerIdAndOrderName").FirstOrDefault(); - Assert.Equal(typeOfConventionOrder.FullName, function3.ReturnType.Definition.FullTypeName()); - Assert.Equal(2, function3.Parameters.Count()); + // Function GetAllConventionCustomers + var typeOfConventionCustomer = typeof(ConventionCustomer); + var function1 = edmModel.FindDeclaredOperations(typeOfConventionCustomer.Namespace + ".GetAllConventionCustomers").FirstOrDefault(); + Assert.Equal(string.Format("Collection({0})", typeOfConventionCustomer.FullName), function1.ReturnType.Definition.FullTypeName()); + Assert.Empty(function1.Parameters); + + // Function GetConventionCustomerById + var function2 = edmModel.FindDeclaredOperations(typeof(ConventionCustomer).Namespace + ".GetConventionCustomerById").FirstOrDefault(); + Assert.Equal(typeOfConventionCustomer.FullName, function2.ReturnType.Definition.FullTypeName()); + Assert.Single(function2.Parameters); + + // Function GetConventionOrderByCustomerIdAndOrderName + var typeOfConventionOrder = typeof(ConventionOrder); + var function3 = edmModel.FindDeclaredOperations(typeOfConventionOrder.Namespace + ".GetConventionOrderByCustomerIdAndOrderName").FirstOrDefault(); + Assert.Equal(typeOfConventionOrder.FullName, function3.ReturnType.Definition.FullTypeName()); + Assert.Equal(2, function3.Parameters.Count()); #endregion #region function imports - var container = edmModel.EntityContainer; - Assert.Equal("Container", container.Name); + var container = edmModel.EntityContainer; + Assert.Equal("Container", container.Name); - var functionImport1 = container.FindOperationImports("GetAllConventionCustomers"); - Assert.Equal(2, functionImport1.Count()); + var functionImport1 = container.FindOperationImports("GetAllConventionCustomers"); + Assert.Equal(2, functionImport1.Count()); - var functionImport2 = container.FindOperationImports("GetConventionCustomerById"); - Assert.Single(functionImport2); + var functionImport2 = container.FindOperationImports("GetConventionCustomerById"); + Assert.Single(functionImport2); - var functionImport3 = container.FindOperationImports("GetConventionOrderByCustomerIdAndOrderName"); - Assert.Single(functionImport3); + var functionImport3 = container.FindOperationImports("GetConventionOrderByCustomerIdAndOrderName"); + Assert.Single(functionImport3); #endregion #region actions - var action2 = edmModel.FindDeclaredOperations(typeOfConventionCustomer.Namespace + ".UpdateAddress").FirstOrDefault(); - Assert.Equal(string.Format("Collection({0})", typeOfConventionCustomer.FullName), action2.ReturnType.Definition.FullTypeName()); - Assert.Equal(2, action2.Parameters.Count()); + var action2 = edmModel.FindDeclaredOperations(typeOfConventionCustomer.Namespace + ".UpdateAddress").FirstOrDefault(); + Assert.Equal(string.Format("Collection({0})", typeOfConventionCustomer.FullName), action2.ReturnType.Definition.FullTypeName()); + Assert.Equal(2, action2.Parameters.Count()); #endregion #region action imports - var actionImport2 = container.FindOperationImports("UpdateAddress"); - Assert.Single(actionImport2); + var actionImport2 = container.FindOperationImports("UpdateAddress"); + Assert.Single(actionImport2); #endregion - } + } - [Fact] - public async Task ServiceDocumentTest() - { - // Arrange - var requestUri = "odata"; - HttpClient client = CreateClient(); - - // Act - var response = await client.GetAsync(requestUri); - var stream = await response.Content.ReadAsStreamAsync(); - - //Assert - var oDataMessageReaderSettings = new ODataMessageReaderSettings(); - IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); - var reader = new ODataMessageReader(message, oDataMessageReaderSettings, UnboundFunctionEdmModel.GetEdmModel()); - var oDataWorkSpace = reader.ReadServiceDocument(); - - var function1 = oDataWorkSpace.FunctionImports.Where(odataResourceCollectionInfo => odataResourceCollectionInfo.Name == "GetAllConventionCustomers"); - Assert.Single(function1); - var function2 = oDataWorkSpace.FunctionImports.Where(odataResourceCollectionInfo => odataResourceCollectionInfo.Name == "GetConventionOrderByCustomerIdAndOrderName"); - // ODL spec says: - // The edm:FunctionImport for a parameterless function MAY include the IncludeInServiceDocument attribute - // whose Boolean value indicates whether the function import is advertised in the service document. - // So the below 2 FunctionImports are not displayed in ServiceDocument. - Assert.Empty(function2); - var function3 = oDataWorkSpace.FunctionImports.Where(odataResourceCollectionInfo => odataResourceCollectionInfo.Name == "GetConventionCustomerById"); - Assert.Empty(function3); - } + [Fact] + public async Task ServiceDocumentTest() + { + // Arrange + var requestUri = "odata"; + HttpClient client = CreateClient(); + + // Act + var response = await client.GetAsync(requestUri); + var stream = await response.Content.ReadAsStreamAsync(); + + //Assert + var oDataMessageReaderSettings = new ODataMessageReaderSettings(); + IODataResponseMessage message = new ODataMessageWrapper(stream, response.Content.Headers); + var reader = new ODataMessageReader(message, oDataMessageReaderSettings, UnboundFunctionEdmModel.GetEdmModel()); + var oDataWorkSpace = reader.ReadServiceDocument(); + + var function1 = oDataWorkSpace.FunctionImports.Where(odataResourceCollectionInfo => odataResourceCollectionInfo.Name == "GetAllConventionCustomers"); + Assert.Single(function1); + var function2 = oDataWorkSpace.FunctionImports.Where(odataResourceCollectionInfo => odataResourceCollectionInfo.Name == "GetConventionOrderByCustomerIdAndOrderName"); + // ODL spec says: + // The edm:FunctionImport for a parameterless function MAY include the IncludeInServiceDocument attribute + // whose Boolean value indicates whether the function import is advertised in the service document. + // So the below 2 FunctionImports are not displayed in ServiceDocument. + Assert.Empty(function2); + var function3 = oDataWorkSpace.FunctionImports.Where(odataResourceCollectionInfo => odataResourceCollectionInfo.Name == "GetConventionCustomerById"); + Assert.Empty(function3); + } #endregion #region functions and function imports - [Fact] - public async Task FunctionImportWithoutParameters() + [Fact] + public async Task FunctionImportWithoutParameters() + { + // Arrange + var requestUri = "odata/GetAllConventionCustomersImport()"; + HttpClient client = CreateClient(); + + // Act + var response = await client.GetAsync(requestUri); + var responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("/$metadata#ConventionCustomers", responseString); + foreach (ConventionCustomer customer in new ConventionCustomersController().Customers) { - // Arrange - var requestUri = "odata/GetAllConventionCustomersImport()"; - HttpClient client = CreateClient(); - - // Act - var response = await client.GetAsync(requestUri); - var responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("/$metadata#ConventionCustomers", responseString); - foreach (ConventionCustomer customer in new ConventionCustomersController().Customers) - { - string expect = "\"ID\":" + customer.ID; - Assert.Contains(expect, responseString); - expect = "\"Name\":\"" + customer.Name + "\""; - Assert.Contains(expect, responseString); - } + string expect = "\"ID\":" + customer.ID; + Assert.Contains(expect, responseString); + expect = "\"Name\":\"" + customer.Name + "\""; + Assert.Contains(expect, responseString); } + } - [Fact] - public async Task FunctionImportOverload() - { - // Arrange - var requestUri = "odata/GetAllConventionCustomersImport(CustomerName='Name 1')"; - HttpClient client = CreateClient(); - - // Act - var response = await client.GetAsync(requestUri); - var responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("/$metadata#ConventionCustomers", responseString); - // only customer 401 and 410 are returned. - Assert.Contains("\"ID\":401", responseString); - Assert.Contains("\"ID\":410", responseString); - Assert.DoesNotContain("\"ID\":402", responseString); - Assert.DoesNotContain("\"ID\":409", responseString); - } + [Fact] + public async Task FunctionImportOverload() + { + // Arrange + var requestUri = "odata/GetAllConventionCustomersImport(CustomerName='Name 1')"; + HttpClient client = CreateClient(); + + // Act + var response = await client.GetAsync(requestUri); + var responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("/$metadata#ConventionCustomers", responseString); + // only customer 401 and 410 are returned. + Assert.Contains("\"ID\":401", responseString); + Assert.Contains("\"ID\":410", responseString); + Assert.DoesNotContain("\"ID\":402", responseString); + Assert.DoesNotContain("\"ID\":409", responseString); + } - [Theory] - [InlineData("odata/GetAllConventionCustomersImport(CustomerName='Name 1')/$count", "2")] // returns collection of entity. - [InlineData("odata/GetDefinedGenders()/$count", "2")] // returns collection of enum - public async Task DollarCountFollowingFunctionImport(string url, string expectedCount) - { - // Arrange & Act - var requestUri = url; - HttpClient client = CreateClient(); - var response = await client.GetAsync(requestUri); - var responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.NotNull(expectedCount); - Assert.True(response.IsSuccessStatusCode); - Assert.Equal("2", responseString); - } + [Theory] + [InlineData("odata/GetAllConventionCustomersImport(CustomerName='Name 1')/$count", "2")] // returns collection of entity. + [InlineData("odata/GetDefinedGenders()/$count", "2")] // returns collection of enum + public async Task DollarCountFollowingFunctionImport(string url, string expectedCount) + { + // Arrange & Act + var requestUri = url; + HttpClient client = CreateClient(); + var response = await client.GetAsync(requestUri); + var responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.NotNull(expectedCount); + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("2", responseString); + } - [Fact] - public async Task FunctionImportWithOneParameters() - { - // Arrange - const int CustomerId = 407; - ConventionCustomer expectCustomer = new ConventionCustomersController().GetConventionCustomerById(CustomerId); - - // Act - var requestUri = "odata/GetConventionCustomerByIdImport(CustomerId=" + CustomerId + ")"; - HttpClient client = CreateClient(); - var response = await client.GetAsync(requestUri); - var responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.NotNull(expectCustomer); - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("/$metadata#ConventionCustomers/$entity", responseString); - string expect = "\"ID\":" + expectCustomer.ID; - Assert.Contains(expect, responseString); - expect = "\"Name\":\"" + expectCustomer.Name + "\""; - Assert.Contains(expect, responseString); - } + [Fact] + public async Task FunctionImportWithOneParameters() + { + // Arrange + const int CustomerId = 407; + ConventionCustomer expectCustomer = new ConventionCustomersController().GetConventionCustomerById(CustomerId); + + // Act + var requestUri = "odata/GetConventionCustomerByIdImport(CustomerId=" + CustomerId + ")"; + HttpClient client = CreateClient(); + var response = await client.GetAsync(requestUri); + var responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.NotNull(expectCustomer); + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("/$metadata#ConventionCustomers/$entity", responseString); + string expect = "\"ID\":" + expectCustomer.ID; + Assert.Contains(expect, responseString); + expect = "\"Name\":\"" + expectCustomer.Name + "\""; + Assert.Contains(expect, responseString); + } - [Fact] - public async Task FunctionImportWithMoreThanOneParameters() - { - // Arrange - const int CustomerId = 408; - const string OrderName = "OrderName 5"; - - var requestUri = "odata/GetConventionOrderByCustomerIdAndOrderNameImport(CustomerId=" + CustomerId + ",OrderName='" + OrderName + "')"; - HttpClient client = CreateClient(); - - // Act - var response = await client.GetWithAcceptAsync(requestUri, "application/json;odata.metadata=full"); - string responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains("/$metadata#ConventionOrders/$entity", responseString); - Assert.Contains(string.Format("\"@odata.type\":\"#{0}.ConventionOrder", EdmSchemaNamespace), responseString); - Assert.Contains("\"OrderName\":\"OrderName 5\"", responseString); - Assert.Contains("\"Price@odata.type\":\"#Decimal\",\"Price\":5", responseString); - } + [Fact] + public async Task FunctionImportWithMoreThanOneParameters() + { + // Arrange + const int CustomerId = 408; + const string OrderName = "OrderName 5"; + + var requestUri = "odata/GetConventionOrderByCustomerIdAndOrderNameImport(CustomerId=" + CustomerId + ",OrderName='" + OrderName + "')"; + HttpClient client = CreateClient(); + + // Act + var response = await client.GetWithAcceptAsync(requestUri, "application/json;odata.metadata=full"); + string responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains("/$metadata#ConventionOrders/$entity", responseString); + Assert.Contains(string.Format("\"@odata.type\":\"#{0}.ConventionOrder", EdmSchemaNamespace), responseString); + Assert.Contains("\"OrderName\":\"OrderName 5\"", responseString); + Assert.Contains("\"Price@odata.type\":\"#Decimal\",\"Price\":5", responseString); + } - [Fact] - public async Task FunctionImportFollowedByProperty() - { - // Arrange - const int CustomerId = 407; - ConventionCustomer expectCustomer = new ConventionCustomersController().GetConventionCustomerById(CustomerId); // expect customer instance - Assert.NotNull(expectCustomer); - - // Act - var requestUri = "odata/GetConventionCustomerByIdImport(CustomerId=" + CustomerId + ")/Name"; - HttpClient client = CreateClient(); - var response = await client.GetAsync(requestUri); - var responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - string expect = "\"value\":\"" + expectCustomer.Name + "\""; - Assert.Contains(expect, responseString); - } + [Fact] + public async Task FunctionImportFollowedByProperty() + { + // Arrange + const int CustomerId = 407; + ConventionCustomer expectCustomer = new ConventionCustomersController().GetConventionCustomerById(CustomerId); // expect customer instance + Assert.NotNull(expectCustomer); + + // Act + var requestUri = "odata/GetConventionCustomerByIdImport(CustomerId=" + CustomerId + ")/Name"; + HttpClient client = CreateClient(); + var response = await client.GetAsync(requestUri); + var responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(response.IsSuccessStatusCode); + string expect = "\"value\":\"" + expectCustomer.Name + "\""; + Assert.Contains(expect, responseString); + } - [Theory] - [InlineData("GetAllConventionCustomersImport()")] - [InlineData("GetAllConventionCustomersImport(CustomerName='Name%201')")] - public async Task FunctionImportFollowedByQueryOption(string functionImport) - { - // Arrange - const int CustomerId = 401; - ConventionCustomer expectCustomer = (new ConventionCustomersController()).GetConventionCustomerById(CustomerId); // expect customer instance - Assert.NotNull(expectCustomer); - - // Act - var requestUri = String.Format("odata/{0}?$filter=ID eq {1}", functionImport, CustomerId); - HttpClient client = CreateClient(); - var response = await client.GetAsync(requestUri); - var responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.True(response.IsSuccessStatusCode); - string expect = "\"Name\":\"" + expectCustomer.Name + "\""; - Assert.Contains(expect, responseString); - Assert.DoesNotContain("402", responseString); - } + [Theory] + [InlineData("GetAllConventionCustomersImport()")] + [InlineData("GetAllConventionCustomersImport(CustomerName='Name%201')")] + public async Task FunctionImportFollowedByQueryOption(string functionImport) + { + // Arrange + const int CustomerId = 401; + ConventionCustomer expectCustomer = (new ConventionCustomersController()).GetConventionCustomerById(CustomerId); // expect customer instance + Assert.NotNull(expectCustomer); + + // Act + var requestUri = String.Format("odata/{0}?$filter=ID eq {1}", functionImport, CustomerId); + HttpClient client = CreateClient(); + var response = await client.GetAsync(requestUri); + var responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.True(response.IsSuccessStatusCode); + string expect = "\"Name\":\"" + expectCustomer.Name + "\""; + Assert.Contains(expect, responseString); + Assert.DoesNotContain("402", responseString); + } - // Negative: Unbound function in query option is not supported - [Fact] - public async Task UnboundFunctionInFilter() - { - // Arrange - const int CustomerId = 407; - ConventionCustomer expectCustomer = new ConventionCustomersController().GetConventionCustomerById(CustomerId); // expect customer instance - Assert.NotNull(expectCustomer); + // Negative: Unbound function in query option is not supported + [Fact] + public async Task UnboundFunctionInFilter() + { + // Arrange + const int CustomerId = 407; + ConventionCustomer expectCustomer = new ConventionCustomersController().GetConventionCustomerById(CustomerId); // expect customer instance + Assert.NotNull(expectCustomer); - var requestUri = "odata/ConventionCustomers?$filter=Microsoft.AspNetCore.OData.E2E.Tests.UnboundOperation.GetConventionCustomerNameById(CustomerId%3D" + CustomerId + ") eq 'Name 7'"; - HttpClient client = CreateClient(); + var requestUri = "odata/ConventionCustomers?$filter=Microsoft.AspNetCore.OData.E2E.Tests.UnboundOperation.GetConventionCustomerNameById(CustomerId%3D" + CustomerId + ") eq 'Name 7'"; + HttpClient client = CreateClient(); - // Act - var response = await client.GetAsync(requestUri); + // Act + var response = await client.GetAsync(requestUri); - // Assert - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } - // Re-enable this test after ODL issue is fixed. - // See ODL issue: https://github.com/OData/odata.net/issues/1155 - // In ODL, e2e test ExceptionSholdThrowForFunctionImport_EnableCaseInsensitive has been added to cover similar - // scenario on ODL layer. - //[Fact] + // Re-enable this test after ODL issue is fixed. + // See ODL issue: https://github.com/OData/odata.net/issues/1155 + // In ODL, e2e test ExceptionSholdThrowForFunctionImport_EnableCaseInsensitive has been added to cover similar + // scenario on ODL layer. + //[Fact] // public async Task FunctionImportInFilter() // { // // Arrange @@ -353,109 +353,108 @@ public async Task UnboundFunctionInFilter() // } // } - [Fact] - public async Task UnboundFunction_WithPrimitiveEnumComplexEntity_AndCollectionOfThemParameters() - { - // Arrange - var requestUri = string.Format("odata/AdvancedFunction(nums=@a,genders=@b,location=@c,addresses=@d,customer=@e,customers=@f)?@a={0}&@b={1}&@c={2}&@d={3}&@e={4}&@f={5}", - "[1,2,3]", "['Male','Female']", - "{\"Street\":\"Zi Xin Rd.\",\"City\":\"Shanghai\",\"ZipCode\":\"2001100\"}", - "[{\"Street\":\"Zi Xin Rd.\",\"City\":\"Shanghai\",\"ZipCode\":\"2001100\"}]", - "{\"@odata.type\":\"%23{NAMESPACE}.ConventionCustomer\",\"ID\":7,\"Name\":\"Tony\"}", - "[{\"@odata.type\":\"%23{NAMESPACE}.ConventionCustomer\",\"ID\":7,\"Name\":\"Tony\"}]" - ); - requestUri = requestUri.Replace("{NAMESPACE}", EdmSchemaNamespace); - HttpClient client = CreateClient(); - - // Act - var response = await client.GetAsync(requestUri); - - // Assert - response.EnsureSuccessStatusCode(); - } + [Fact] + public async Task UnboundFunction_WithPrimitiveEnumComplexEntity_AndCollectionOfThemParameters() + { + // Arrange + var requestUri = string.Format("odata/AdvancedFunction(nums=@a,genders=@b,location=@c,addresses=@d,customer=@e,customers=@f)?@a={0}&@b={1}&@c={2}&@d={3}&@e={4}&@f={5}", + "[1,2,3]", "['Male','Female']", + "{\"Street\":\"Zi Xin Rd.\",\"City\":\"Shanghai\",\"ZipCode\":\"2001100\"}", + "[{\"Street\":\"Zi Xin Rd.\",\"City\":\"Shanghai\",\"ZipCode\":\"2001100\"}]", + "{\"@odata.type\":\"%23{NAMESPACE}.ConventionCustomer\",\"ID\":7,\"Name\":\"Tony\"}", + "[{\"@odata.type\":\"%23{NAMESPACE}.ConventionCustomer\",\"ID\":7,\"Name\":\"Tony\"}]" + ); + requestUri = requestUri.Replace("{NAMESPACE}", EdmSchemaNamespace); + HttpClient client = CreateClient(); + + // Act + var response = await client.GetAsync(requestUri); + + // Assert + response.EnsureSuccessStatusCode(); + } - [Fact] - public async Task UnboundAction_WithPrimitiveEnumComplexEntity_AndCollectionOfThemParameters() - { - // Arrange - var requestUri = "odata/AdvancedAction"; - - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - string payload = @"{ - ""nums"": [4,5,6], - ""genders"": ['Male', 'Female'], - ""location"": {""Street"":""NY Rd."",""City"":""Redmond"",""ZipCode"":""9011""}, - ""addresses"": [{""Street"":""NY Rd."",""City"":""Redmond"",""ZipCode"":""9011""}], - ""customer"": {""@odata.type"":""#{NAMESPACE}.ConventionCustomer"",""ID"":8,""Name"":""Mike""}, - ""customers"": [{""@odata.type"":""#{NAMESPACE}.ConventionCustomer"",""ID"":8,""Name"":""Mike""}] - }"; - payload = payload.Replace("{NAMESPACE}", EdmSchemaNamespace); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; - HttpClient client = CreateClient(); - - // Act - var response = await client.SendAsync(request); - - // Assert - response.EnsureSuccessStatusCode(); - } + [Fact] + public async Task UnboundAction_WithPrimitiveEnumComplexEntity_AndCollectionOfThemParameters() + { + // Arrange + var requestUri = "odata/AdvancedAction"; + + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + string payload = @"{ + ""nums"": [4,5,6], + ""genders"": ['Male', 'Female'], + ""location"": {""Street"":""NY Rd."",""City"":""Redmond"",""ZipCode"":""9011""}, + ""addresses"": [{""Street"":""NY Rd."",""City"":""Redmond"",""ZipCode"":""9011""}], + ""customer"": {""@odata.type"":""#{NAMESPACE}.ConventionCustomer"",""ID"":8,""Name"":""Mike""}, + ""customers"": [{""@odata.type"":""#{NAMESPACE}.ConventionCustomer"",""ID"":8,""Name"":""Mike""}] + }"; + payload = payload.Replace("{NAMESPACE}", EdmSchemaNamespace); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + HttpClient client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + + // Assert + response.EnsureSuccessStatusCode(); + } #endregion #region action imports - [Fact] - public async Task ActionImportWithParameters() - { - // Arrange - var uri = "odata/UpdateAddress"; - var content = new { Address = new { Street = "Street 11", City = "City 11", ZipCode = "201101" }, ID = 401 }; - HttpClient client = CreateClient(); - - // Act - var response = await client.PostAsJsonAsync(uri, content); + [Fact] + public async Task ActionImportWithParameters() + { + // Arrange + var uri = "odata/UpdateAddress"; + var content = new { Address = new { Street = "Street 11", City = "City 11", ZipCode = "201101" }, ID = 401 }; + HttpClient client = CreateClient(); - // Assert - response.EnsureSuccessStatusCode(); + // Act + var response = await client.PostAsJsonAsync(uri, content); - var responseString = await response.Content.ReadAsStringAsync(); - Assert.Contains("Street 11", responseString); - } + // Assert + response.EnsureSuccessStatusCode(); - [Fact] - public async Task ActionImportFollowedByQueryOption() - { - // Arrange - var uri = "odata/UpdateAddress?$filter=ID%20eq%20402"; - var content = new { Address = new { Street = "Street 11", City = "City 11", ZipCode = "201101" }, ID = 401 }; - HttpClient client = CreateClient(); - - // Act - var response = await client.PostAsJsonAsync(uri, content); - - // Assert - response.EnsureSuccessStatusCode(); - var responseString = await response.Content.ReadAsStringAsync(); - Assert.DoesNotContain("Street 11", responseString); - } + var responseString = await response.Content.ReadAsStringAsync(); + Assert.Contains("Street 11", responseString); + } - [Fact] - public async Task AnEmptyFilterQueryOptionShouldReturnA400() - { - // Arrange - var uri = "odata/UpdateAddress?$filter="; - var content = new { Address = new { Street = "Street 11", City = "City 11", ZipCode = "201101" }, ID = 401 }; - HttpClient client = CreateClient(); + [Fact] + public async Task ActionImportFollowedByQueryOption() + { + // Arrange + var uri = "odata/UpdateAddress?$filter=ID%20eq%20402"; + var content = new { Address = new { Street = "Street 11", City = "City 11", ZipCode = "201101" }, ID = 401 }; + HttpClient client = CreateClient(); + + // Act + var response = await client.PostAsJsonAsync(uri, content); + + // Assert + response.EnsureSuccessStatusCode(); + var responseString = await response.Content.ReadAsStringAsync(); + Assert.DoesNotContain("Street 11", responseString); + } - // Act - var response = await client.PostAsJsonAsync(uri, content); + [Fact] + public async Task AnEmptyFilterQueryOptionShouldReturnA400() + { + // Arrange + var uri = "odata/UpdateAddress?$filter="; + var content = new { Address = new { Street = "Street 11", City = "City 11", ZipCode = "201101" }, ID = 401 }; + HttpClient client = CreateClient(); - // Assert - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } + // Act + var response = await client.PostAsJsonAsync(uri, content); - #endregion + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } + + #endregion } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedControllers.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedControllers.cs index 5e3ab1953..5a213afa5 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedControllers.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedControllers.cs @@ -17,127 +17,126 @@ using Microsoft.AspNetCore.OData.Routing.Controllers; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Untyped +namespace Microsoft.AspNetCore.OData.E2E.Tests.Untyped; + +public class UntypedController : ODataController { - public class UntypedController : ODataController + [EnableQuery] + [HttpGet("/odata/managers")] + public IActionResult GetManagers() { - [EnableQuery] - [HttpGet("/odata/managers")] - public IActionResult GetManagers() - { - return Ok(UntypedDataSource.Managers); - } + return Ok(UntypedDataSource.Managers); + } + + [EnableQuery] + [HttpGet("odata/people")] + public IActionResult GetPeople() + { + return Ok(UntypedDataSource.GetAllPeople()); + } - [EnableQuery] - [HttpGet("odata/people")] - public IActionResult GetPeople() + [EnableQuery] + [HttpGet("odata/people/{id}")] + public IActionResult Get(int id) + { + InModelPerson person = UntypedDataSource.GetAllPeople().FirstOrDefault(p => p.Id == id); + return Ok(person); + } + + [EnableQuery] + [HttpGet("odata/people/{id}/data")] + public IActionResult GetData(int id) + { + InModelPerson person = UntypedDataSource.GetAllPeople().FirstOrDefault(p => p.Id == id); + return Ok(person.Data); + } + + [EnableQuery] + [HttpGet("odata/people/{id}/infos")] + public IActionResult GetInfos(int id) + { + InModelPerson person = UntypedDataSource.GetAllPeople().FirstOrDefault(p => p.Id == id); + return Ok(person.Infos); + } + + [EnableQuery] + [HttpPost("odata/people")] + public IActionResult Post([FromBody] InModelPerson person) + { + Assert.NotNull(person); + + if (person.Id == 90) // primitive { - return Ok(UntypedDataSource.GetAllPeople()); + Assert.IsType(person.Data); + Assert.Equal(new Guid("40EE4E85-C443-41B2-9611-C55F97D80E84"), person.Data); + return Created(person); } - [EnableQuery] - [HttpGet("odata/people/{id}")] - public IActionResult Get(int id) + if (person.Id == 91) // enum { - InModelPerson person = UntypedDataSource.GetAllPeople().FirstOrDefault(p => p.Id == id); - return Ok(person); + Assert.Equal(InModelColor.Blue, person.Data); + return Created(person); } - [EnableQuery] - [HttpGet("odata/people/{id}/data")] - public IActionResult GetData(int id) + if (person.Id == 92) // resource { - InModelPerson person = UntypedDataSource.GetAllPeople().FirstOrDefault(p => p.Id == id); - return Ok(person.Data); + InModelAddress address = Assert.IsType(person.Data); + Assert.Equal("Redmond", address.City); + Assert.Equal("156TH AVE", address.Street); + return Created(person); } - [EnableQuery] - [HttpGet("odata/people/{id}/infos")] - public IActionResult GetInfos(int id) + if (person.Id == 93) // collection of primitive { - InModelPerson person = UntypedDataSource.GetAllPeople().FirstOrDefault(p => p.Id == id); - return Ok(person.Infos); + IEnumerable enumerable = person.Data as IEnumerable; + Assert.Collection(enumerable, + e => Assert.Equal(4, e), + e => Assert.Equal(5, e)); + return Created(person); } - [EnableQuery] - [HttpPost("odata/people")] - public IActionResult Post([FromBody] InModelPerson person) + if (person.Id == 94) // collection of mix { - Assert.NotNull(person); - - if (person.Id == 90) // primitive - { - Assert.IsType(person.Data); - Assert.Equal(new Guid("40EE4E85-C443-41B2-9611-C55F97D80E84"), person.Data); - return Created(person); - } - - if (person.Id == 91) // enum - { - Assert.Equal(InModelColor.Blue, person.Data); - return Created(person); - } - - if (person.Id == 92) // resource - { - InModelAddress address = Assert.IsType(person.Data); - Assert.Equal("Redmond", address.City); - Assert.Equal("156TH AVE", address.Street); - return Created(person); - } - - if (person.Id == 93) // collection of primitive - { - IEnumerable enumerable = person.Data as IEnumerable; - Assert.Collection(enumerable, - e => Assert.Equal(4, e), - e => Assert.Equal(5, e)); - return Created(person); - } - - if (person.Id == 94) // collection of mix - { - EdmUntypedCollection untypedcoll = Assert.IsType(person.Data); - Assert.Equal(2, untypedcoll.Count); - Assert.Equal(4, untypedcoll.ElementAt(0)); - InModelAddress address = Assert.IsType(untypedcoll.ElementAt(1)); - Assert.Equal("Earth", address.City); - Assert.Equal("Min AVE", address.Street); - return Created(person); - } - - if (person.Id == 98) - { - // 98 is special created - Assert.Equal("Sam", person.Name); - - // Data - EdmUntypedObject data = Assert.IsType(person.Data); - Assert.Equal(3, data.Count); // three properties - Assert.Equal("LineString", data["type"]); - EdmUntypedCollection coordinates = Assert.IsType(data["coordinates"]); - Assert.Equal(4, coordinates.Count); - - EdmUntypedObject crs = Assert.IsType(data["crs"]); - - // Infos - Assert.Equal(2, person.Infos.Count); - - string personJson = JsonSerializer.Serialize(person); - - Assert.Equal("{\"Id\":98,\"Name\":\"Sam\",\"Data\":{\"type\":\"LineString\",\"coordinates\":[[1.0,1.0],[3.0,3.0],[4.0,4.0],[0.0,0.0]],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}}},\"Infos\":[[42],{\"k1\":\"abc\",\"k2\":42,\"k:3\":{\"a1\":2,\"b2\":null},\"k/4\":[null,42]}],\"Containers\":{\"dynamic_p\":[null,{\"X1\":\"Red\",\"Data\":{\"D1\":42}},\"finance\",\"hr\",\"legal\",43]}}", personJson); - return Created(person); - } - - return Ok(true); + EdmUntypedCollection untypedcoll = Assert.IsType(person.Data); + Assert.Equal(2, untypedcoll.Count); + Assert.Equal(4, untypedcoll.ElementAt(0)); + InModelAddress address = Assert.IsType(untypedcoll.ElementAt(1)); + Assert.Equal("Earth", address.City); + Assert.Equal("Min AVE", address.Street); + return Created(person); } - [EnableQuery] - public IActionResult Patch(int key, Delta delta) + if (person.Id == 98) { - Assert.Equal(2, key); + // 98 is special created + Assert.Equal("Sam", person.Name); + + // Data + EdmUntypedObject data = Assert.IsType(person.Data); + Assert.Equal(3, data.Count); // three properties + Assert.Equal("LineString", data["type"]); + EdmUntypedCollection coordinates = Assert.IsType(data["coordinates"]); + Assert.Equal(4, coordinates.Count); + + EdmUntypedObject crs = Assert.IsType(data["crs"]); + + // Infos + Assert.Equal(2, person.Infos.Count); - return Ok(false); + string personJson = JsonSerializer.Serialize(person); + + Assert.Equal("{\"Id\":98,\"Name\":\"Sam\",\"Data\":{\"type\":\"LineString\",\"coordinates\":[[1.0,1.0],[3.0,3.0],[4.0,4.0],[0.0,0.0]],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}}},\"Infos\":[[42],{\"k1\":\"abc\",\"k2\":42,\"k:3\":{\"a1\":2,\"b2\":null},\"k/4\":[null,42]}],\"Containers\":{\"dynamic_p\":[null,{\"X1\":\"Red\",\"Data\":{\"D1\":42}},\"finance\",\"hr\",\"legal\",43]}}", personJson); + return Created(person); } + + return Ok(true); + } + + [EnableQuery] + public IActionResult Patch(int key, Delta delta) + { + Assert.Equal(2, key); + + return Ok(false); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedDataModel.cs index 015cd267a..bb1cd80cf 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedDataModel.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -7,44 +7,43 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Untyped +namespace Microsoft.AspNetCore.OData.E2E.Tests.Untyped; + +public class InModelPerson +{ + public int Id { get; set; } + + public string Name { get; set; } + + public object Data { get; set; } // ==> Declared Edm.Untyped + + public IList Infos { get; set; } = new List();// ==> Declared Collection(Edm.Untyped) + + public IDictionary Containers { get; set; } +} + +public class InModelAddress +{ + public string City { get; set; } + public string Street { get; set; } +} + +public enum InModelColor +{ + Red, + Green, + Blue, +} + +// These classes are not built into Edm model, just for normal type +public class NotInModelAddress +{ + public string ZipCode { get; set; } + public string Location { get; set; } +} + +public enum NotInModelEnum { - public class InModelPerson - { - public int Id { get; set; } - - public string Name { get; set; } - - public object Data { get; set; } // ==> Declared Edm.Untyped - - public IList Infos { get; set; } = new List();// ==> Declared Collection(Edm.Untyped) - - public IDictionary Containers { get; set; } - } - - public class InModelAddress - { - public string City { get; set; } - public string Street { get; set; } - } - - public enum InModelColor - { - Red, - Green, - Blue, - } - - // These classes are not built into Edm model, just for normal type - public class NotInModelAddress - { - public string ZipCode { get; set; } - public string Location { get; set; } - } - - public enum NotInModelEnum - { - Apple, - Peach - } + Apple, + Peach } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedDataSource.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedDataSource.cs index 94f48697a..4154133ae 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedDataSource.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedDataSource.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -8,199 +8,198 @@ using System.Collections.Generic; using Microsoft.AspNetCore.OData.Formatter.Value; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Untyped +namespace Microsoft.AspNetCore.OData.E2E.Tests.Untyped; + +public class UntypedDataSource { - public class UntypedDataSource + public static IList Managers => new List { - public static IList Managers => new List + new InModelPerson { - new InModelPerson + Id = 1, + Name = "Sun", + Data = new InModelAddress { - Id = 1, - Name = "Sun", - Data = new InModelAddress - { - City = "Shanghai", Street = "Fengjin RD" - }, - Infos = new object[] { 1, "abc", 3}, - Containers = new Dictionary - { - { "D_Data", new InModelAddress { City = "Shanghai", Street = "Fengjin RD" } }, - { "D_Infos", new object[] { 1, "abc", 3} }, - } + City = "Shanghai", Street = "Fengjin RD" }, - new InModelPerson + Infos = new object[] { 1, "abc", 3}, + Containers = new Dictionary { - Id = 2, - Name = "Sun", - Data = new object[] + { "D_Data", new InModelAddress { City = "Shanghai", Street = "Fengjin RD" } }, + { "D_Infos", new object[] { 1, "abc", 3} }, + } + }, + new InModelPerson + { + Id = 2, + Name = "Sun", + Data = new object[] + { + 42, + null, + "abc", + new EdmUntypedObject { - 42, - null, - "abc", - new EdmUntypedObject - { - { "ACity", "Shanghai" }, - { "AData", new EdmUntypedCollection - { - 42, - InModelColor.Red - } + { "ACity", "Shanghai" }, + { "AData", new EdmUntypedCollection + { + 42, + InModelColor.Red } } - }, - Infos = new EdmUntypedCollection - { - 42, - InModelColor.Red - }, - Containers = new Dictionary - { - { "D_Data", new EdmUntypedObject { { "D_City", new EdmUntypedCollection() } } }, - { "D_Infos", new EdmUntypedCollection { new Dictionary { {"k", "v"} } } } } + }, + Infos = new EdmUntypedCollection + { + 42, + InModelColor.Red + }, + Containers = new Dictionary + { + { "D_Data", new EdmUntypedObject { { "D_City", new EdmUntypedCollection() } } }, + { "D_Infos", new EdmUntypedCollection { new Dictionary { {"k", "v"} } } } } - }; + } + }; - private static IList _people; + private static IList _people; - public static IList GetAllPeople() + public static IList GetAllPeople() + { + if (_people == null) { - if (_people == null) + _people = new List { - _people = new List + // Basic primitive value + new InModelPerson { - // Basic primitive value - new InModelPerson + Id = 1, + Name = "Kerry", + Data = 13, + Infos = new object[] { 1, 2, 3}, + Containers = new Dictionary { - Id = 1, - Name = "Kerry", - Data = 13, - Infos = new object[] { 1, 2, 3}, - Containers = new Dictionary - { - { "Dynamic1", 13 }, - { "Dynamic2", true }, - } - }, + { "Dynamic1", 13 }, + { "Dynamic2", true }, + } + }, - // In model and not in model enum - new InModelPerson + // In model and not in model enum + new InModelPerson + { + Id = 2, + Name = "Xu", + Data = InModelColor.Red, + Infos = new object[] { InModelColor.Blue, InModelColor.Green, NotInModelEnum.Apple }, + Containers = new Dictionary { - Id = 2, - Name = "Xu", - Data = InModelColor.Red, - Infos = new object[] { InModelColor.Blue, InModelColor.Green, NotInModelEnum.Apple }, - Containers = new Dictionary - { - { "EnumDynamic1", InModelColor.Blue }, - { "EnumDynamic2", NotInModelEnum.Apple }, - } - }, + { "EnumDynamic1", InModelColor.Blue }, + { "EnumDynamic2", NotInModelEnum.Apple }, + } + }, - // not in model enum - new InModelPerson + // not in model enum + new InModelPerson + { + Id = 22, // special Id number + Name = "Yin", + Data = NotInModelEnum.Apple, + Infos = new object[] { InModelColor.Blue, InModelColor.Green, NotInModelEnum.Apple }, + Containers = new Dictionary { - Id = 22, // special Id number - Name = "Yin", - Data = NotInModelEnum.Apple, - Infos = new object[] { InModelColor.Blue, InModelColor.Green, NotInModelEnum.Apple }, - Containers = new Dictionary - { - { "EnumDynamic1", InModelColor.Blue }, - { "EnumDynamic2", NotInModelEnum.Apple }, - } - }, + { "EnumDynamic1", InModelColor.Blue }, + { "EnumDynamic2", NotInModelEnum.Apple }, + } + }, - // In model complex - new InModelPerson + // In model complex + new InModelPerson + { + Id = 3, + Name = "Mars", + Data = new InModelAddress{ City = "Redmond", Street = "134TH AVE" }, // declared + Infos = new object[] { - Id = 3, - Name = "Mars", - Data = new InModelAddress{ City = "Redmond", Street = "134TH AVE" }, // declared - Infos = new object[] - { - new InModelAddress{ City = "Issaq", Street = "Klahanie Way" } - }, - Containers = new Dictionary - { - { "ComplexDynamic1",new InModelAddress{ City = "RedCity", Street = "Mos Rd" } } - } + new InModelAddress{ City = "Issaq", Street = "Klahanie Way" } }, - - // In and Not in model complex - new InModelPerson + Containers = new Dictionary { - Id = 4, - Name = "Wu", - Data = new NotInModelAddress { ZipCode = "<--->", Location = "******"}, // un-declared in the model - Infos = new object[] { new NotInModelAddress { ZipCode = "<===>", Location = "Info-Locations" } }, - Containers = new Dictionary - { - { "ComplexDynamic1",new InModelAddress{ City = "BlackCity", Street = "Shang Rd" } }, - { "ComplexDynamic2", new NotInModelAddress { ZipCode = "AnyDynanicValue", Location = "In Dy location." } }, - } - }, + { "ComplexDynamic1",new InModelAddress{ City = "RedCity", Street = "Mos Rd" } } + } + }, - // Collection using in and not in model types - new InModelPerson + // In and Not in model complex + new InModelPerson + { + Id = 4, + Name = "Wu", + Data = new NotInModelAddress { ZipCode = "<--->", Location = "******"}, // un-declared in the model + Infos = new object[] { new NotInModelAddress { ZipCode = "<===>", Location = "Info-Locations" } }, + Containers = new Dictionary { - Id = 5, - Name = "Wen", - Data = new object[] - { - null, - 42, - new InModelAddress{ City = "Redmond", Street = "134TH AVE" } - }, - Infos = new object[] { new NotInModelAddress { ZipCode = "<===>", Location = "!@#$" } }, - Containers = new Dictionary - { - { "AnyDynamic1",new InModelAddress{ City = "RedCity", Street = "Mos Rd" } }, - { "AnyDynamic2", new NotInModelAddress { ZipCode = "AnyDynanicValue", Location = "Duck Location" } }, - } - }, - }; + { "ComplexDynamic1",new InModelAddress{ City = "BlackCity", Street = "Shang Rd" } }, + { "ComplexDynamic2", new NotInModelAddress { ZipCode = "AnyDynanicValue", Location = "In Dy location." } }, + } + }, - // Collection in collection - InModelPerson p = new InModelPerson + // Collection using in and not in model types + new InModelPerson { - Id = 99, // special Id to test collection in collection - Name = "Chuan", + Id = 5, + Name = "Wen", Data = new object[] { null, - new object[] { 42, new InModelAddress { City = "Redmond", Street = "134TH AVE" } } + 42, + new InModelAddress{ City = "Redmond", Street = "134TH AVE" } }, - Infos = new object[] + Infos = new object[] { new NotInModelAddress { ZipCode = "<===>", Location = "!@#$" } }, + Containers = new Dictionary { + { "AnyDynamic1",new InModelAddress{ City = "RedCity", Street = "Mos Rd" } }, + { "AnyDynamic2", new NotInModelAddress { ZipCode = "AnyDynanicValue", Location = "Duck Location" } }, + } + }, + }; + + // Collection in collection + InModelPerson p = new InModelPerson + { + Id = 99, // special Id to test collection in collection + Name = "Chuan", + Data = new object[] + { + null, + new object[] { 42, new InModelAddress { City = "Redmond", Street = "134TH AVE" } } + }, + Infos = new object[] + { + new EdmUntypedCollection + { + new NotInModelAddress { ZipCode = "NoAValidZip", Location = "OnEarth" }, + null, new EdmUntypedCollection { - new NotInModelAddress { ZipCode = "NoAValidZip", Location = "OnEarth" }, - null, new EdmUntypedCollection { - new EdmUntypedCollection + new object[] { - new object[] - { - new InModelAddress { City = "Issaquah", Street = "80TH ST" } - } + new InModelAddress { City = "Issaquah", Street = "80TH ST" } } } - }, - 42 + } }, - Containers = new Dictionary - { - { "Dp", new object[] { new InModelAddress{ City = "BlackCastle", Street = "To Castle Rd" } } } - } - }; - - _people.Add(p); - } + 42 + }, + Containers = new Dictionary + { + { "Dp", new object[] { new InModelAddress{ City = "BlackCastle", Street = "To Castle Rd" } } } + } + }; - return _people; + _people.Add(p); } + + return _people; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedEdmModel.cs index a7b29701c..ef8d65360 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedEdmModel.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -8,19 +8,18 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Untyped +namespace Microsoft.AspNetCore.OData.E2E.Tests.Untyped; + +public class UntypedEdmModel { - public class UntypedEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("People"); - builder.EntitySet("Managers"); - builder.ComplexType(); - builder.EnumType(); - IEdmModel model = builder.GetEdmModel(); - return model; - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("People"); + builder.EntitySet("Managers"); + builder.ComplexType(); + builder.EnumType(); + IEdmModel model = builder.GetEdmModel(); + return model; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedTests.cs index a36dcfd34..bbf42fbcf 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Untyped/UntypedTests.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -20,629 +20,628 @@ using System.Net.Http.Headers; using System.Xml.Linq; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Untyped +namespace Microsoft.AspNetCore.OData.E2E.Tests.Untyped; + +public class UntypedTests : WebApiTestBase { - public class UntypedTests : WebApiTestBase - { - private readonly ITestOutputHelper output; + private readonly ITestOutputHelper output; - public UntypedTests(WebApiTestFixture fixture, ITestOutputHelper output) - : base(fixture) - { - this.output = output; - } + public UntypedTests(WebApiTestFixture fixture, ITestOutputHelper output) + : base(fixture) + { + this.output = output; + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel edmModel = UntypedEdmModel.GetEdmModel(); + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel edmModel = UntypedEdmModel.GetEdmModel(); - services.ConfigureControllers(typeof(UntypedController), typeof(MetadataController)); + services.ConfigureControllers(typeof(UntypedController), typeof(MetadataController)); - services.AddControllers().AddOData(opt => - opt.EnableQueryFeatures() - .AddRouteComponents("odata", edmModel)); - } + services.AddControllers().AddOData(opt => + opt.EnableQueryFeatures() + .AddRouteComponents("odata", edmModel)); + } - [Fact] - public async Task Untyped_Metadata() - { - string expect = "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - ""; - - // Remove indentation - expect = Regex.Replace(expect, @"\r\n\s*<", @"<"); - - var requestUri = "odata/$metadata"; - HttpClient client = CreateClient(); - HttpResponseMessage response = await client.GetAsync(requestUri); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string responseContent = await response.Content.ReadAsStringAsync(); - - Assert.Equal(expect, responseContent); - } + [Fact] + public async Task Untyped_Metadata() + { + string expect = "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + + // Remove indentation + expect = Regex.Replace(expect, @"\r\n\s*<", @"<"); + + var requestUri = "odata/$metadata"; + HttpClient client = CreateClient(); + HttpResponseMessage response = await client.GetAsync(requestUri); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string responseContent = await response.Content.ReadAsStringAsync(); + + Assert.Equal(expect, responseContent); + } - public static TheoryDataSet ManagersQueryCases + public static TheoryDataSet ManagersQueryCases + { + get { - get + var data = new TheoryDataSet { - var data = new TheoryDataSet { - { - "application/json;odata.metadata=full", + "application/json;odata.metadata=full", + "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#Managers\"," + + "\"value\":[" + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelPerson\"," + + "\"@odata.id\":\"http://localhost/odata/Managers(1)\"," + + "\"@odata.editLink\":\"Managers(1)\"," + + "\"Id\":1," + + "\"Name\":\"Sun\"," + + "\"Data\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\",\"City\":\"Shanghai\",\"Street\":\"Fengjin RD\"}," + + "\"Infos\":[1,\"abc\",3]," + + "\"D_Data\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\",\"City\":\"Shanghai\",\"Street\":\"Fengjin RD\"}," + + "\"D_Infos\":[1,\"abc\",3]" + + "}," + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelPerson\"," + + "\"@odata.id\":\"http://localhost/odata/Managers(2)\"," + + "\"@odata.editLink\":\"Managers(2)\"," + + "\"Id\":2," + + "\"Name\":\"Sun\"," + + "\"Data\":[" + + "42," + + "null," + + "\"abc\"," + + "{\"ACity\":\"Shanghai\",\"AData\":[42,\"Red\"]}" + + "]," + + "\"Infos\":[42,\"Red\"]," + + "\"D_Data\":{\"D_City\":[]}," + + "\"D_Infos\":[{\"k\":\"v\"}]" + + "}" + + "]" + + "}" + }, + { + "application/json;odata.metadata=minimal", + "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#Managers\"," + + "\"value\":[" + "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#Managers\"," + - "\"value\":[" + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelPerson\"," + - "\"@odata.id\":\"http://localhost/odata/Managers(1)\"," + - "\"@odata.editLink\":\"Managers(1)\"," + - "\"Id\":1," + - "\"Name\":\"Sun\"," + - "\"Data\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\",\"City\":\"Shanghai\",\"Street\":\"Fengjin RD\"}," + - "\"Infos\":[1,\"abc\",3]," + - "\"D_Data\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\",\"City\":\"Shanghai\",\"Street\":\"Fengjin RD\"}," + - "\"D_Infos\":[1,\"abc\",3]" + - "}," + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelPerson\"," + - "\"@odata.id\":\"http://localhost/odata/Managers(2)\"," + - "\"@odata.editLink\":\"Managers(2)\"," + - "\"Id\":2," + - "\"Name\":\"Sun\"," + - "\"Data\":[" + - "42," + - "null," + - "\"abc\"," + - "{\"ACity\":\"Shanghai\",\"AData\":[42,\"Red\"]}" + - "]," + - "\"Infos\":[42,\"Red\"]," + - "\"D_Data\":{\"D_City\":[]}," + - "\"D_Infos\":[{\"k\":\"v\"}]" + - "}" + - "]" + - "}" - }, - { - "application/json;odata.metadata=minimal", + "\"Id\":1," + + "\"Name\":\"Sun\"," + + "\"Data\":{\"City\":\"Shanghai\",\"Street\":\"Fengjin RD\"}," + + "\"Infos\":[1,\"abc\",3]," + + "\"D_Data\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\",\"City\":\"Shanghai\",\"Street\":\"Fengjin RD\"}," + + "\"D_Infos\":[1,\"abc\",3]" + + "}," + "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#Managers\"," + - "\"value\":[" + - "{" + - "\"Id\":1," + - "\"Name\":\"Sun\"," + - "\"Data\":{\"City\":\"Shanghai\",\"Street\":\"Fengjin RD\"}," + - "\"Infos\":[1,\"abc\",3]," + - "\"D_Data\":{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\",\"City\":\"Shanghai\",\"Street\":\"Fengjin RD\"}," + - "\"D_Infos\":[1,\"abc\",3]" + - "}," + - "{" + - "\"Id\":2," + - "\"Name\":\"Sun\"," + - "\"Data\":[" + - "42," + - "null," + - "\"abc\"," + - "{\"ACity\":\"Shanghai\",\"AData\":[42,\"Red\"]}" + - "]," + - "\"Infos\":[42,\"Red\"]," + - "\"D_Data\":{\"D_City\":[]}," + - "\"D_Infos\":[{\"k\":\"v\"}]" + - "}" + - "]" + - "}" - }, - { - "application/json;odata.metadata=none", + "\"Id\":2," + + "\"Name\":\"Sun\"," + + "\"Data\":[" + + "42," + + "null," + + "\"abc\"," + + "{\"ACity\":\"Shanghai\",\"AData\":[42,\"Red\"]}" + + "]," + + "\"Infos\":[42,\"Red\"]," + + "\"D_Data\":{\"D_City\":[]}," + + "\"D_Infos\":[{\"k\":\"v\"}]" + + "}" + + "]" + + "}" + }, + { + "application/json;odata.metadata=none", + "{" + + "\"value\":[" + + "{" + + "\"Id\":1," + + "\"Name\":\"Sun\"," + + "\"Data\":{\"City\":\"Shanghai\",\"Street\":\"Fengjin RD\"}," + + "\"Infos\":[1,\"abc\",3]," + + "\"D_Data\":{\"City\":\"Shanghai\",\"Street\":\"Fengjin RD\"}," + + "\"D_Infos\":[1,\"abc\",3]" + + "}," + "{" + - "\"value\":[" + - "{" + - "\"Id\":1," + - "\"Name\":\"Sun\"," + - "\"Data\":{\"City\":\"Shanghai\",\"Street\":\"Fengjin RD\"}," + - "\"Infos\":[1,\"abc\",3]," + - "\"D_Data\":{\"City\":\"Shanghai\",\"Street\":\"Fengjin RD\"}," + - "\"D_Infos\":[1,\"abc\",3]" + - "}," + - "{" + - "\"Id\":2," + - "\"Name\":\"Sun\"," + - "\"Data\":[" + - "42," + - "null," + - "\"abc\"," + - "{\"ACity\":\"Shanghai\",\"AData\":[42,\"Red\"]}" + - "]," + - "\"Infos\":[42,\"Red\"]," + - "\"D_Data\":{\"D_City\":[]}," + - "\"D_Infos\":[{\"k\":\"v\"}]" + - "}" + - "]" + - "}" - } - }; - - return data; - } + "\"Id\":2," + + "\"Name\":\"Sun\"," + + "\"Data\":[" + + "42," + + "null," + + "\"abc\"," + + "{\"ACity\":\"Shanghai\",\"AData\":[42,\"Red\"]}" + + "]," + + "\"Infos\":[42,\"Red\"]," + + "\"D_Data\":{\"D_City\":[]}," + + "\"D_Infos\":[{\"k\":\"v\"}]" + + "}" + + "]" + + "}" + } + }; + + return data; } + } - [Theory] - [MemberData(nameof(ManagersQueryCases))] - public async Task QueryUntypedEntitySet_OnDifferentMetadataLevel(string format, string expected) - { - // Arrange - HttpClient client = CreateClient(); + [Theory] + [MemberData(nameof(ManagersQueryCases))] + public async Task QueryUntypedEntitySet_OnDifferentMetadataLevel(string format, string expected) + { + // Arrange + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.GetAsync($"odata/managers?$format={format}"); + // Act + HttpResponseMessage response = await client.GetAsync($"odata/managers?$format={format}"); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - string payloadBody = await response.Content.ReadAsStringAsync(); + string payloadBody = await response.Content.ReadAsStringAsync(); - Assert.Equal(expected, payloadBody); - } + Assert.Equal(expected, payloadBody); + } - public static TheoryDataSet DeclaredPropertyQueryCases + public static TheoryDataSet DeclaredPropertyQueryCases + { + get { - get + var data = new TheoryDataSet { - var data = new TheoryDataSet + // for Edm.Untyped { - // for Edm.Untyped - { - "odata/people/1/data", - "{\"@odata.context\":\"http://localhost/odata/$metadata#People(1)/Data\"," + - "\"value\":13" + - "}" - }, - { - "odata/people/2/data", - "{\"@odata.context\":\"http://localhost/odata/$metadata#People(2)/Data\"," + - "\"value\":\"Red\"" + - "}" - }, - { - "odata/people/3/data", - "{\"@odata.context\":\"http://localhost/odata/$metadata#People(3)/Data\"," + - "\"City\":\"Redmond\",\"Street\":\"134TH AVE\"" + - "}" - }, - { - "odata/people/4/data", - "{\"@odata.context\":\"http://localhost/odata/$metadata#People(4)/Data/Edm.Untyped\"," + - "\"ZipCode\":\"<--->\",\"Location\":\"******\"" + - "}" - }, - { - "odata/people/5/data", - "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#People(5)/Data/Edm.Untyped\"," + - "\"value\":[" + - "null," + - "42," + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\"," + - "\"City\":\"Redmond\"," + - "\"Street\":\"134TH AVE\"" + - "}" + - "]" + - "}" - }, - - // for Collection(Edm.Untyped) - { - "odata/people/1/infos", - "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#People(1)/Infos/Edm.Untyped\"," + - "\"value\":[1,2,3]" + - "}" - }, - { - "odata/people/2/infos", - "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#People(2)/Infos/Edm.Untyped\"," + - "\"value\":[\"Blue\",\"Green\",\"Apple\"]" + - "}" - }, - { - "odata/people/3/infos", - "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#People(3)/Infos/Edm.Untyped\"," + - "\"value\":[{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\",\"City\":\"Issaq\",\"Street\":\"Klahanie Way\"}]" + - "}" - }, - { - "odata/people/4/infos", - "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#People(4)/Infos/Edm.Untyped\"," + - "\"value\":[{\"ZipCode\":\"<===>\",\"Location\":\"Info-Locations\"}]" + - "}" - }, - { - "odata/people/5/infos", + "odata/people/1/data", + "{\"@odata.context\":\"http://localhost/odata/$metadata#People(1)/Data\"," + + "\"value\":13" + + "}" + }, + { + "odata/people/2/data", + "{\"@odata.context\":\"http://localhost/odata/$metadata#People(2)/Data\"," + + "\"value\":\"Red\"" + + "}" + }, + { + "odata/people/3/data", + "{\"@odata.context\":\"http://localhost/odata/$metadata#People(3)/Data\"," + + "\"City\":\"Redmond\",\"Street\":\"134TH AVE\"" + + "}" + }, + { + "odata/people/4/data", + "{\"@odata.context\":\"http://localhost/odata/$metadata#People(4)/Data/Edm.Untyped\"," + + "\"ZipCode\":\"<--->\",\"Location\":\"******\"" + + "}" + }, + { + "odata/people/5/data", + "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#People(5)/Data/Edm.Untyped\"," + + "\"value\":[" + + "null," + + "42," + "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#People(5)/Infos/Edm.Untyped\"," + - "\"value\":[{\"ZipCode\":\"<===>\",\"Location\":\"!@#$\"}]" + - "}" - } - }; - - return data; - } + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\"," + + "\"City\":\"Redmond\"," + + "\"Street\":\"134TH AVE\"" + + "}" + + "]" + + "}" + }, + + // for Collection(Edm.Untyped) + { + "odata/people/1/infos", + "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#People(1)/Infos/Edm.Untyped\"," + + "\"value\":[1,2,3]" + + "}" + }, + { + "odata/people/2/infos", + "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#People(2)/Infos/Edm.Untyped\"," + + "\"value\":[\"Blue\",\"Green\",\"Apple\"]" + + "}" + }, + { + "odata/people/3/infos", + "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#People(3)/Infos/Edm.Untyped\"," + + "\"value\":[{\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\",\"City\":\"Issaq\",\"Street\":\"Klahanie Way\"}]" + + "}" + }, + { + "odata/people/4/infos", + "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#People(4)/Infos/Edm.Untyped\"," + + "\"value\":[{\"ZipCode\":\"<===>\",\"Location\":\"Info-Locations\"}]" + + "}" + }, + { + "odata/people/5/infos", + "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#People(5)/Infos/Edm.Untyped\"," + + "\"value\":[{\"ZipCode\":\"<===>\",\"Location\":\"!@#$\"}]" + + "}" + } + }; + + return data; } + } - [Theory] - [MemberData(nameof(DeclaredPropertyQueryCases))] - public async Task QuerySinglePeople_OnDeclaredUntypedProperty(string request, string expected) - { - // Arrange - HttpClient client = CreateClient(); + [Theory] + [MemberData(nameof(DeclaredPropertyQueryCases))] + public async Task QuerySinglePeople_OnDeclaredUntypedProperty(string request, string expected) + { + // Arrange + HttpClient client = CreateClient(); - // Act - HttpResponseMessage response = await client.GetAsync(request); + // Act + HttpResponseMessage response = await client.GetAsync(request); - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); - string payloadBody = await response.Content.ReadAsStringAsync(); + string payloadBody = await response.Content.ReadAsStringAsync(); - Assert.Equal(expected, payloadBody); - } + Assert.Equal(expected, payloadBody); + } - [Fact] - public async Task QuerySinglePeople_WithDeclaredOrUndeclaredEnum_OnUntypedAndDynamicProperty() - { - // Arrange - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync("odata/people/22"); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - string payloadBody = await response.Content.ReadAsStringAsync(); - - Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + - "\"Id\":22," + - "\"Name\":\"Yin\"," + - "\"EnumDynamic1@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelColor\"," + - "\"EnumDynamic1\":\"Blue\"," + - "\"EnumDynamic2\":\"Apple\"," + - "\"Data@odata.type\":\"#String\"," + - "\"Data\":\"Apple\"," + - "\"Infos\":[\"Blue\",\"Green\",\"Apple\"]" + - "}", payloadBody); - } + [Fact] + public async Task QuerySinglePeople_WithDeclaredOrUndeclaredEnum_OnUntypedAndDynamicProperty() + { + // Arrange + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync("odata/people/22"); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + + "\"Id\":22," + + "\"Name\":\"Yin\"," + + "\"EnumDynamic1@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelColor\"," + + "\"EnumDynamic1\":\"Blue\"," + + "\"EnumDynamic2\":\"Apple\"," + + "\"Data@odata.type\":\"#String\"," + + "\"Data\":\"Apple\"," + + "\"Infos\":[\"Blue\",\"Green\",\"Apple\"]" + + "}", payloadBody); + } - [Fact] - public async Task QuerySinglePeople_WithCollectionInCollection_OnDeclaredUntypedProperty() - { - // Arrange - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync("odata/people/99"); - - // Assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - - string payloadBody = await response.Content.ReadAsStringAsync(); - - Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + - "\"Id\":99," + - "\"Name\":\"Chuan\"," + - "\"Data\":[" + - "null," + - "[" + - "42," + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\"," + - "\"City\":\"Redmond\"," + - "\"Street\":\"134TH AVE\"" + - "}" + - "]" + - "]," + - "\"Infos\":[" + + [Fact] + public async Task QuerySinglePeople_WithCollectionInCollection_OnDeclaredUntypedProperty() + { + // Arrange + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync("odata/people/99"); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + + "\"Id\":99," + + "\"Name\":\"Chuan\"," + + "\"Data\":[" + + "null," + + "[" + + "42," + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\"," + + "\"City\":\"Redmond\"," + + "\"Street\":\"134TH AVE\"" + + "}" + + "]" + + "]," + + "\"Infos\":[" + + "[" + + "{\"ZipCode\":\"NoAValidZip\",\"Location\":\"OnEarth\"}," + // a resource whose type is not defined in the Edm model, so there's no @odata.type + "null," + + "[" + "[" + - "{\"ZipCode\":\"NoAValidZip\",\"Location\":\"OnEarth\"}," + // a resource whose type is not defined in the Edm model, so there's no @odata.type - "null," + "[" + - "[" + - "[" + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\"," + - "\"City\":\"Issaquah\"," + - "\"Street\":\"80TH ST\"" + - "}" + - "]" + - "]" + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\"," + + "\"City\":\"Issaquah\"," + + "\"Street\":\"80TH ST\"" + + "}" + "]" + - "]," + - "42" + - "]," + - "\"Dp\":[" + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\"," + - "\"City\":\"BlackCastle\"," + - "\"Street\":\"To Castle Rd\"" + - "}" + + "]" + "]" + - "}", payloadBody); - } + "]," + + "42" + + "]," + + "\"Dp\":[" + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\"," + + "\"City\":\"BlackCastle\"," + + "\"Street\":\"To Castle Rd\"" + + "}" + + "]" + + "}", payloadBody); + } - [Fact] - public async Task CreatePerson_WithPrimitiveUntypedValueODataTyped_Works_RoundTrip() - { - // Arrange - const string payload = @"{ + [Fact] + public async Task CreatePerson_WithPrimitiveUntypedValueODataTyped_Works_RoundTrip() + { + // Arrange + const string payload = @"{ ""data@odata.type"": ""#Edm.Guid"", ""data"":""40EE4E85-C443-41B2-9611-C55F97D80E84"", ""id"": 90 }"; - HttpClient client = CreateClient(); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/people"); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - - string payloadBody = await response.Content.ReadAsStringAsync(); - - Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + - "\"Id\":90," + - "\"Name\":null," + - "\"Data@odata.type\":\"#Guid\"," + - "\"Data\":\"40ee4e85-c443-41b2-9611-c55f97d80e84\"," + - "\"Infos\":[]" + - "}", payloadBody); - } + HttpClient client = CreateClient(); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/people"); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + + "\"Id\":90," + + "\"Name\":null," + + "\"Data@odata.type\":\"#Guid\"," + + "\"Data\":\"40ee4e85-c443-41b2-9611-c55f97d80e84\"," + + "\"Infos\":[]" + + "}", payloadBody); + } - [Fact] - public async Task CreatePerson_WithEnumUntypedValueODataTyped_Works_RoundTrip() - { - // Arrange - const string payload = @"{ + [Fact] + public async Task CreatePerson_WithEnumUntypedValueODataTyped_Works_RoundTrip() + { + // Arrange + const string payload = @"{ ""data@odata.type"": ""#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelColor"", ""data"":""Blue"", ""id"": 91 }"; - HttpClient client = CreateClient(); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/people"); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - - string payloadBody = await response.Content.ReadAsStringAsync(); - - Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + - "\"Id\":91," + - "\"Name\":null," + - "\"Data\":\"Blue\"," + - "\"Infos\":[]" + - "}", payloadBody); - } + HttpClient client = CreateClient(); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/people"); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + + "\"Id\":91," + + "\"Name\":null," + + "\"Data\":\"Blue\"," + + "\"Infos\":[]" + + "}", payloadBody); + } - [Fact] - public async Task CreatePerson_WithResourceUntypedValueODataTyped_Works_RoundTrip() - { - // Arrange - const string payload = @"{ + [Fact] + public async Task CreatePerson_WithResourceUntypedValueODataTyped_Works_RoundTrip() + { + // Arrange + const string payload = @"{ ""data"":{ - ""@odata.type"":""#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress"", - ""City"":""Redmond"", - ""Street"":""156TH AVE"" +""@odata.type"":""#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress"", +""City"":""Redmond"", +""Street"":""156TH AVE"" }, ""id"": 92 }"; - HttpClient client = CreateClient(); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/people"); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - - string payloadBody = await response.Content.ReadAsStringAsync(); - - Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + - "\"Id\":92," + - "\"Name\":null," + - "\"Data\":{\"City\":\"Redmond\",\"Street\":\"156TH AVE\"}," + - "\"Infos\":[]" + - "}", payloadBody); - } + HttpClient client = CreateClient(); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/people"); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + + "\"Id\":92," + + "\"Name\":null," + + "\"Data\":{\"City\":\"Redmond\",\"Street\":\"156TH AVE\"}," + + "\"Infos\":[]" + + "}", payloadBody); + } - [Fact] - public async Task CreatePerson_WithPrimitiveCollectionUntypedValueODataTyped_Works_RoundTrip() - { - // Arrange - const string payload = @"{ - ""data"":[ - 4, + [Fact] + public async Task CreatePerson_WithPrimitiveCollectionUntypedValueODataTyped_Works_RoundTrip() { - ""@odata.type"":""#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress"", - ""City"":""Earth"", - ""Street"":""Min AVE"" - } + // Arrange + const string payload = @"{ + ""data"":[ +4, +{ + ""@odata.type"":""#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress"", + ""City"":""Earth"", + ""Street"":""Min AVE"" +} ], ""id"": 94 }"; - HttpClient client = CreateClient(); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/people"); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - - string payloadBody = await response.Content.ReadAsStringAsync(); - - Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + - "\"Id\":94," + - "\"Name\":null," + - "\"Data\":[" + - "4," + - "{" + - "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\"," + - "\"City\":\"Earth\"," + - "\"Street\":\"Min AVE\"" + - "}" + - "]," + - "\"Infos\":[]" + - "}", payloadBody); - } + HttpClient client = CreateClient(); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/people"); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + + "\"Id\":94," + + "\"Name\":null," + + "\"Data\":[" + + "4," + + "{" + + "\"@odata.type\":\"#Microsoft.AspNetCore.OData.E2E.Tests.Untyped.InModelAddress\"," + + "\"City\":\"Earth\"," + + "\"Street\":\"Min AVE\"" + + "}" + + "]," + + "\"Infos\":[]" + + "}", payloadBody); + } - [Fact] - public async Task CreatePerson_WithCollectionItemUntypedValueODataTyped_Works_RoundTrip() - { - // Arrange - const string payload = @"{ + [Fact] + public async Task CreatePerson_WithCollectionItemUntypedValueODataTyped_Works_RoundTrip() + { + // Arrange + const string payload = @"{ ""data@odata.type"":""#Collection(Edm.Int32)"", ""data"":[ - 4, - 5 +4, +5 ], ""id"": 93 }"; - HttpClient client = CreateClient(); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/people"); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - - string payloadBody = await response.Content.ReadAsStringAsync(); - - Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + - "\"Id\":93," + - "\"Name\":null," + - "\"Data\":[4,5]," + - "\"Infos\":[]" + - "}", payloadBody); - } + HttpClient client = CreateClient(); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/people"); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + string payloadBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal("{\"@odata.context\":\"http://localhost/odata/$metadata#People/$entity\"," + + "\"Id\":93," + + "\"Name\":null," + + "\"Data\":[4,5]," + + "\"Infos\":[]" + + "}", payloadBody); + } - [Fact] - public async Task CreatePerson_Works_RoundTrip() - { - // Arrange - const string payload = @"{ + [Fact] + public async Task CreatePerson_Works_RoundTrip() + { + // Arrange + const string payload = @"{ ""infos"":[ - [42], - {""k1"": ""abc"", ""k2"": 42, ""k:3"": { ""a1"": 2, ""b2"": null}, ""k/4"": [null, 42]} + [42], + {""k1"": ""abc"", ""k2"": 42, ""k:3"": { ""a1"": 2, ""b2"": null}, ""k/4"": [null, 42]} ], ""data"":{ - ""type"":""LineString"",""coordinates"":[ - [ - 1.0,1.0 - ],[ - 3.0,3.0 - ],[ - 4.0,4.0 - ],[ - 0.0,0.0 - ] - ],""crs"":{ - ""type"":""name"",""properties"":{ - ""name"":""EPSG:4326"" - } - } +""type"":""LineString"",""coordinates"":[ + [ + 1.0,1.0 + ],[ + 3.0,3.0 + ],[ + 4.0,4.0 + ],[ + 0.0,0.0 + ] +],""crs"":{ + ""type"":""name"",""properties"":{ + ""name"":""EPSG:4326"" + } +} }, ""id"": 98, ""name"":""Sam"", ""dynamic_p"": [ - null, - { - ""X1"": ""Red"", - ""Data"": { - ""D1"": 42 - } - }, - ""finance"", - ""hr"", - ""legal"", - 43 - ] + null, + { + ""X1"": ""Red"", + ""Data"": { + ""D1"": 42 + } + }, + ""finance"", + ""hr"", + ""legal"", + 43 +] }"; - HttpClient client = CreateClient(); + HttpClient client = CreateClient(); - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/people"); - request.Content = new StringContent(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payload.Length; - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "odata/people"); + request.Content = new StringContent(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payload.Length; + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); + // Assert + Assert.Equal(HttpStatusCode.Created, response.StatusCode); - string payloadBody = await response.Content.ReadAsStringAsync(); + string payloadBody = await response.Content.ReadAsStringAsync(); - Assert.Contains("\"coordinates\":[[1.0,1.0],[3.0,3.0],[4.0,4.0],[0.0,0.0]],\"cr", payloadBody); - Assert.Contains("\"k:3\":{\"a1@odata.type\":\"#Decimal\",\"a1\":2,\"b2\":null},\"k/4\":[null,42]}],\"dyna", payloadBody); - } + Assert.Contains("\"coordinates\":[[1.0,1.0],[3.0,3.0],[4.0,4.0],[0.0,0.0]],\"cr", payloadBody); + Assert.Contains("\"k:3\":{\"a1@odata.type\":\"#Decimal\",\"a1\":2,\"b2\":null},\"k/4\":[null,42]}],\"dyna", payloadBody); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/CaseInsensitiveResolver.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/CaseInsensitiveResolver.cs index 56c5f3a9e..6687f6dfa 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/CaseInsensitiveResolver.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/CaseInsensitiveResolver.cs @@ -7,19 +7,18 @@ using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension +namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension; + +/// +/// Add this class for ODL's issue #695, should remove it and use ODataUriResolver after the issue fix. +/// +public class CaseInsensitiveResolver : ODataUriResolver { - /// - /// Add this class for ODL's issue #695, should remove it and use ODataUriResolver after the issue fix. - /// - public class CaseInsensitiveResolver : ODataUriResolver - { - private bool _enableCaseInsensitive; + private bool _enableCaseInsensitive; - public override bool EnableCaseInsensitive - { - get { return true; } - set { _enableCaseInsensitive = value; } - } + public override bool EnableCaseInsensitive + { + get { return true; } + set { _enableCaseInsensitive = value; } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/CaseInsensitiveTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/CaseInsensitiveTest.cs index 4e069ced7..cd0299746 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/CaseInsensitiveTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/CaseInsensitiveTest.cs @@ -14,71 +14,70 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension +namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension; + +public class CaseInsensitiveTest : WebApiTestBase { - public class CaseInsensitiveTest : WebApiTestBase + public CaseInsensitiveTest(WebApiTestFixture fixture) + :base(fixture) { - public CaseInsensitiveTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(CustomersController), typeof(OrdersController), typeof(MetadataController)); + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(CustomersController), typeof(OrdersController), typeof(MetadataController)); - IEdmModel model = UriParserExtenstionEdmModel.GetEdmModel(); + IEdmModel model = UriParserExtenstionEdmModel.GetEdmModel(); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model).Count().Filter().OrderBy().Expand().SetMaxTop(null)); - } + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model).Count().Filter().OrderBy().Expand().SetMaxTop(null)); + } - public static TheoryDataSet CaseInsensitiveCases + public static TheoryDataSet CaseInsensitiveCases + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() - { - // $metadata, $count, $ref, $value - { "Get", "$metadata", "$meTadata"}, - { "Get", "Customers(1)/Name/$value", "Customers(1)/Name/$vAlue"}, - // { "Get", "Customers(1)/Orders/$ref", "Customers(1)/Orders/$rEf" }, - { "Get", "Customers/$count", "Customers/$coUNt" }, + // $metadata, $count, $ref, $value + { "Get", "$metadata", "$meTadata"}, + { "Get", "Customers(1)/Name/$value", "Customers(1)/Name/$vAlue"}, + // { "Get", "Customers(1)/Orders/$ref", "Customers(1)/Orders/$rEf" }, + { "Get", "Customers/$count", "Customers/$coUNt" }, - // Metadata value - { "Get", "Customers", "CusTomeRs"}, - { "Get", "Customers(2)", "CusTomeRs(2)"}, - { "Get", "Customers(2)/Name", "CusTomeRs(2)/nAMe"}, + // Metadata value + { "Get", "Customers", "CusTomeRs"}, + { "Get", "Customers(2)", "CusTomeRs(2)"}, + { "Get", "Customers(2)/Name", "CusTomeRs(2)/nAMe"}, - // { "Get", "Customers(6)/Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.VipCustomer/VipProperty", "Customers(6)/Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.VipCustomer/vipproPERty"}, - { "Get", "Customers(1)/Default.CalculateSalary(month=2)", "Customers(1)/deFault.calCULateSalary(month=2)" }, - { "Post", "Customers(1)/Default.UpdateAddress", "Customers(1)/deFault.updateaDDress" }, - }; - } + // { "Get", "Customers(6)/Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.VipCustomer/VipProperty", "Customers(6)/Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.VipCustomer/vipproPERty"}, + { "Get", "Customers(1)/Default.CalculateSalary(month=2)", "Customers(1)/deFault.calCULateSalary(month=2)" }, + { "Post", "Customers(1)/Default.UpdateAddress", "Customers(1)/deFault.updateaDDress" }, + }; } + } - [Theory] - [MemberData(nameof(CaseInsensitiveCases))] - public async Task EnableCaseInsensitiveTest(string method, string caseSensitive, string caseInsensitive) - { - // Case sensitive - HttpClient client = CreateClient(); + [Theory] + [MemberData(nameof(CaseInsensitiveCases))] + public async Task EnableCaseInsensitiveTest(string method, string caseSensitive, string caseInsensitive) + { + // Case sensitive + HttpClient client = CreateClient(); - var caseSensitiveUri = $"odata/{caseSensitive}"; - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), caseSensitiveUri); - HttpResponseMessage response = await client.SendAsync(request); + var caseSensitiveUri = $"odata/{caseSensitive}"; + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), caseSensitiveUri); + HttpResponseMessage response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string caseSensitiveResponse = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string caseSensitiveResponse = await response.Content.ReadAsStringAsync(); - // Case Insensitive - var caseInsensitiveUri = $"odata/{caseInsensitive}"; - request = new HttpRequestMessage(new HttpMethod(method), caseInsensitiveUri); - response = await client.SendAsync(request); + // Case Insensitive + var caseInsensitiveUri = $"odata/{caseInsensitive}"; + request = new HttpRequestMessage(new HttpMethod(method), caseInsensitiveUri); + response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string caseInsensitiveResponse = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string caseInsensitiveResponse = await response.Content.ReadAsStringAsync(); - Assert.Equal(caseSensitiveResponse, caseInsensitiveResponse); - } + Assert.Equal(caseSensitiveResponse, caseInsensitiveResponse); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/EnumPrefixFreeTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/EnumPrefixFreeTest.cs index 1e825a770..a1fe44c85 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/EnumPrefixFreeTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/EnumPrefixFreeTest.cs @@ -16,98 +16,97 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension +namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension; + +public class EnumPrefixFreeTest : WebApiTestBase { - public class EnumPrefixFreeTest : WebApiTestBase + public EnumPrefixFreeTest(WebApiTestFixture fixture) + :base(fixture) { - public EnumPrefixFreeTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(CustomersController), typeof(OrdersController), typeof(MetadataController)); - - IEdmModel model = UriParserExtenstionEdmModel.GetEdmModel(); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model, - services => - { - services.AddSingleton(sp => new StringAsEnumResolver() { EnableCaseInsensitive = true }); - } - )); - } + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(CustomersController), typeof(OrdersController), typeof(MetadataController)); - public static TheoryDataSet EnumPrefixFreeCases - { - get + IEdmModel model = UriParserExtenstionEdmModel.GetEdmModel(); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model, + services => { - // Create data with case insensitive parameter name and case insensitive enum value. - // Enum type prefix, if present, is still required to be case sensitive since it is type-related. - return new TheoryDataSet() - { - { "gEnDeR=Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'mAlE'", "GeNdEr='MaLe'", (int)HttpStatusCode.OK }, - { "GeNdEr=Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'UnknownValue'", "gEnDeR='UnknownValue'", (int)HttpStatusCode.BadRequest }, - }; + services.AddSingleton(sp => new StringAsEnumResolver() { EnableCaseInsensitive = true }); } - } + )); + } - [Fact] - public async Task EnableEnumPrefixFreeTest() + public static TheoryDataSet EnumPrefixFreeCases + { + get { - // Enum with prefix - HttpClient client = CreateClient(); + // Create data with case insensitive parameter name and case insensitive enum value. + // Enum type prefix, if present, is still required to be case sensitive since it is type-related. + return new TheoryDataSet() + { + { "gEnDeR=Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'mAlE'", "GeNdEr='MaLe'", (int)HttpStatusCode.OK }, + { "GeNdEr=Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'UnknownValue'", "gEnDeR='UnknownValue'", (int)HttpStatusCode.BadRequest }, + }; + } + } - var prefixUri = $"odata/Customers/Default.GetCustomerByGender(gEnDeR=Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'mAlE')"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, prefixUri); - HttpResponseMessage response = await client.SendAsync(request); + [Fact] + public async Task EnableEnumPrefixFreeTest() + { + // Enum with prefix + HttpClient client = CreateClient(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string prefixResponse = await response.Content.ReadAsStringAsync(); + var prefixUri = $"odata/Customers/Default.GetCustomerByGender(gEnDeR=Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'mAlE')"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, prefixUri); + HttpResponseMessage response = await client.SendAsync(request); - // Enum prefix free - var prefixFreeUri = $"odata/Customers/Default.GetCustomerByGender(GeNdEr='MaLe')"; - request = new HttpRequestMessage(HttpMethod.Get, prefixFreeUri); - response = await client.SendAsync(request); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string prefixResponse = await response.Content.ReadAsStringAsync(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string prefixFreeResponse = await response.Content.ReadAsStringAsync(); + // Enum prefix free + var prefixFreeUri = $"odata/Customers/Default.GetCustomerByGender(GeNdEr='MaLe')"; + request = new HttpRequestMessage(HttpMethod.Get, prefixFreeUri); + response = await client.SendAsync(request); - Assert.Equal(prefixResponse, prefixFreeResponse); - } + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string prefixFreeResponse = await response.Content.ReadAsStringAsync(); - [Fact] - public async Task EnableEnumPrefixFreeTestThrows() - { - // Enum with prefix - HttpClient client = CreateClient(); + Assert.Equal(prefixResponse, prefixFreeResponse); + } - var prefixUri = $"odata/Customers/Default.GetCustomerByGender(GeNdEr=Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'UnknownValue')"; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, prefixUri); + [Fact] + public async Task EnableEnumPrefixFreeTestThrows() + { + // Enum with prefix + HttpClient client = CreateClient(); - try - { - await client.SendAsync(request); - } - catch(ODataException ex) - { - Assert.Equal("The parameter value (Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'UnknownValue') from request is not valid. " + - "The parameter value should be format of type 'Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'.", ex.Message); - } + var prefixUri = $"odata/Customers/Default.GetCustomerByGender(GeNdEr=Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'UnknownValue')"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, prefixUri); - // Enum prefix free - var prefixFreeUri = $"odata/Customers/Default.GetCustomerByGender(gEnDeR='UnknownValue')"; - request = new HttpRequestMessage(HttpMethod.Get, prefixFreeUri); + try + { + await client.SendAsync(request); + } + catch(ODataException ex) + { + Assert.Equal("The parameter value (Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'UnknownValue') from request is not valid. " + + "The parameter value should be format of type 'Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'.", ex.Message); + } - try - { - await client.SendAsync(request); - } - catch(ODataException ex) - { - Assert.Equal("The parameter value ('UnknownValue') from request is not valid. " + - "The parameter value should be format of type 'Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'.", ex.Message); - } + // Enum prefix free + var prefixFreeUri = $"odata/Customers/Default.GetCustomerByGender(gEnDeR='UnknownValue')"; + request = new HttpRequestMessage(HttpMethod.Get, prefixFreeUri); + + try + { + await client.SendAsync(request); + } + catch(ODataException ex) + { + Assert.Equal("The parameter value ('UnknownValue') from request is not valid. " + + "The parameter value should be format of type 'Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension.Gender'.", ex.Message); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UnqualifiedCallTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UnqualifiedCallTest.cs index 65471a200..5f9bbe8ca 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UnqualifiedCallTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UnqualifiedCallTest.cs @@ -14,95 +14,94 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension +namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension; + +public class UnqualifiedCallTest : WebApiTestBase { - public class UnqualifiedCallTest : WebApiTestBase + public UnqualifiedCallTest(WebApiTestFixture fixture) + :base(fixture) { - public UnqualifiedCallTest(WebApiTestFixture fixture) - :base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(CustomersController), typeof(OrdersController), typeof(MetadataController)); + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(CustomersController), typeof(OrdersController), typeof(MetadataController)); - IEdmModel model = UriParserExtenstionEdmModel.GetEdmModel(); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model)); - } + IEdmModel model = UriParserExtenstionEdmModel.GetEdmModel(); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model)); + } - public static TheoryDataSet UnqualifiedCallCases + public static TheoryDataSet UnqualifiedCallCases + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() - { - { "Get", "Customers(1)/Default.CalculateSalary(month=2)", "Customers(1)/CalculateSalary(month=2)" }, - { "Post", "Customers(1)/Default.UpdateAddress", "Customers(1)/UpdateAddress" }, - }; - } + { "Get", "Customers(1)/Default.CalculateSalary(month=2)", "Customers(1)/CalculateSalary(month=2)" }, + { "Post", "Customers(1)/Default.UpdateAddress", "Customers(1)/UpdateAddress" }, + }; } + } - public static TheoryDataSet UnqualifiedCallAndCaseInsensitiveCases + public static TheoryDataSet UnqualifiedCallAndCaseInsensitiveCases + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() - { - { "Get", "Customers(1)/Default.CalculateSalary(month=2)", "CuStOmErS(1)/CaLcUlAtESaLaRy(MONTH=2)" }, - { "Post", "Customers(1)/Default.UpdateAddress", "cUsToMeRs(1)/upDaTeAdDrEsS" }, - }; - } + { "Get", "Customers(1)/Default.CalculateSalary(month=2)", "CuStOmErS(1)/CaLcUlAtESaLaRy(MONTH=2)" }, + { "Post", "Customers(1)/Default.UpdateAddress", "cUsToMeRs(1)/upDaTeAdDrEsS" }, + }; } + } - [Theory] - [MemberData(nameof(UnqualifiedCallCases))] - public async Task EnableUnqualifiedCallTest(string method, string qualifiedFunction, string unqualifiedFunction) - { - // Case sensitive - HttpClient client = CreateClient(); + [Theory] + [MemberData(nameof(UnqualifiedCallCases))] + public async Task EnableUnqualifiedCallTest(string method, string qualifiedFunction, string unqualifiedFunction) + { + // Case sensitive + HttpClient client = CreateClient(); - var qualifiedFunctionUri = $"odata/{qualifiedFunction}"; - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), qualifiedFunctionUri); - HttpResponseMessage response = await client.SendAsync(request); + var qualifiedFunctionUri = $"odata/{qualifiedFunction}"; + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), qualifiedFunctionUri); + HttpResponseMessage response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string qualifiedFunctionResponse = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string qualifiedFunctionResponse = await response.Content.ReadAsStringAsync(); - // Case Insensitive - var unqualifiedFunctionUri = $"odata/{unqualifiedFunction}"; - request = new HttpRequestMessage(new HttpMethod(method), unqualifiedFunctionUri); - response = await client.SendAsync(request); + // Case Insensitive + var unqualifiedFunctionUri = $"odata/{unqualifiedFunction}"; + request = new HttpRequestMessage(new HttpMethod(method), unqualifiedFunctionUri); + response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string unqualifiedFunctionResponse = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string unqualifiedFunctionResponse = await response.Content.ReadAsStringAsync(); - Assert.Equal(qualifiedFunctionResponse, unqualifiedFunctionResponse); - } + Assert.Equal(qualifiedFunctionResponse, unqualifiedFunctionResponse); + } - [Theory] - [MemberData(nameof(UnqualifiedCallAndCaseInsensitiveCases))] - public async Task EnableUnqualifiedCallAndCaseInsensitiveTest(string method, string qualifiedSensitiveFunction, - string unqualifiedInsensitiveFunction) - { - // Case sensitive - HttpClient client = CreateClient(); - var qualifiedSensitiveFunctionUri = $"odata/{qualifiedSensitiveFunction}"; - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), qualifiedSensitiveFunctionUri); - HttpResponseMessage response = await client.SendAsync(request); + [Theory] + [MemberData(nameof(UnqualifiedCallAndCaseInsensitiveCases))] + public async Task EnableUnqualifiedCallAndCaseInsensitiveTest(string method, string qualifiedSensitiveFunction, + string unqualifiedInsensitiveFunction) + { + // Case sensitive + HttpClient client = CreateClient(); + var qualifiedSensitiveFunctionUri = $"odata/{qualifiedSensitiveFunction}"; + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), qualifiedSensitiveFunctionUri); + HttpResponseMessage response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string qualifiedSensitiveFunctionResponse = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string qualifiedSensitiveFunctionResponse = await response.Content.ReadAsStringAsync(); - // Case Insensitive - var unqualifiedInsensitiveFunctionUri = $"odata/{unqualifiedInsensitiveFunction}"; - request = new HttpRequestMessage(new HttpMethod(method), unqualifiedInsensitiveFunctionUri); - response = await client.SendAsync(request); + // Case Insensitive + var unqualifiedInsensitiveFunctionUri = $"odata/{unqualifiedInsensitiveFunction}"; + request = new HttpRequestMessage(new HttpMethod(method), unqualifiedInsensitiveFunctionUri); + response = await client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string unqualifiedInsensitiveFunctionResponse = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string unqualifiedInsensitiveFunctionResponse = await response.Content.ReadAsStringAsync(); - Assert.Equal(qualifiedSensitiveFunctionResponse, unqualifiedInsensitiveFunctionResponse); - } + Assert.Equal(qualifiedSensitiveFunctionResponse, unqualifiedInsensitiveFunctionResponse); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriParserExtensionDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriParserExtensionDataModel.cs index 2a8b19b0f..a8aee2d69 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriParserExtensionDataModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriParserExtensionDataModel.cs @@ -7,30 +7,29 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension +namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension; + +public class Customer { - public class Customer - { - public int Id { get; set; } - public string Name { get; set; } - public Gender Gender { get; set; } - public IList Orders { get; set; } - } + public int Id { get; set; } + public string Name { get; set; } + public Gender Gender { get; set; } + public IList Orders { get; set; } +} - public class VipCustomer : Customer - { - public string VipProperty { get; set; } - } +public class VipCustomer : Customer +{ + public string VipProperty { get; set; } +} - public enum Gender - { - Male = 1, - Female = 2 - } +public enum Gender +{ + Male = 1, + Female = 2 +} - public class Order - { - public int Id { get; set; } - public string Title { get; set; } - } +public class Order +{ + public int Id { get; set; } + public string Title { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriParserExtenstionController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriParserExtenstionController.cs index 6a4613119..56348a879 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriParserExtenstionController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriParserExtenstionController.cs @@ -15,174 +15,173 @@ using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension +namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension; + +public class CustomersController : ODataController { - public class CustomersController : ODataController + [EnableQuery] + public IActionResult Get() { - [EnableQuery] - public IActionResult Get() - { - return Ok(UriParseExtenstionDbContext.GetCustomers()); - } + return Ok(UriParseExtenstionDbContext.GetCustomers()); + } - public IActionResult Get(int key) - { - return Ok(UriParseExtenstionDbContext.GetCustomers().FirstOrDefault(c => c.Id == key)); - } + public IActionResult Get(int key) + { + return Ok(UriParseExtenstionDbContext.GetCustomers().FirstOrDefault(c => c.Id == key)); + } - public IActionResult GetName(int key) + public IActionResult GetName(int key) + { + var customer = UriParseExtenstionDbContext.GetCustomers().FirstOrDefault(c => c.Id == key); + if (customer == null) { - var customer = UriParseExtenstionDbContext.GetCustomers().FirstOrDefault(c => c.Id == key); - if (customer == null) - { - return NotFound(); - } - - return Ok(customer.Name); + return NotFound(); } - public IActionResult GetVipProperty(int key) - { - var customer = UriParseExtenstionDbContext.GetCustomers().FirstOrDefault(c => c.Id == key); - if (customer == null) - { - return NotFound(); - } - - VipCustomer vipCusomter = customer as VipCustomer; - if (vipCusomter == null) - { - return NotFound(); - } - - return Ok(vipCusomter.VipProperty); - } + return Ok(customer.Name); + } - [EnableQuery] - public IActionResult GetOrders(int key) + public IActionResult GetVipProperty(int key) + { + var customer = UriParseExtenstionDbContext.GetCustomers().FirstOrDefault(c => c.Id == key); + if (customer == null) { - var customer = UriParseExtenstionDbContext.GetCustomers().FirstOrDefault(c => c.Id == key); - if (customer == null) - { - return NotFound(); - } - - return Ok(customer.Orders); + return NotFound(); } - public IActionResult GetRef(int key, string navigationProperty) + VipCustomer vipCusomter = customer as VipCustomer; + if (vipCusomter == null) { - var serviceRootUri = GetServiceRootUri(); - var entityId = string.Format("{0}/Customers({1})/{2}", serviceRootUri, key, navigationProperty); - return Ok(new Uri(entityId)); + return NotFound(); } - [HttpGet] - public IActionResult CalculateSalary(int key, int month) - { - return Ok("CalculateSalary: Key(" + key + ")(" + month + ")"); - } + return Ok(vipCusomter.VipProperty); + } - [HttpPost] - public IActionResult UpdateAddress(int key) + [EnableQuery] + public IActionResult GetOrders(int key) + { + var customer = UriParseExtenstionDbContext.GetCustomers().FirstOrDefault(c => c.Id == key); + if (customer == null) { - return Ok("UpdateAddress: Key(" + key + ")"); + return NotFound(); } - [HttpGet] - public IActionResult GetCustomerByGender([FromODataUri]Gender gender) - { - if (!this.ModelState.IsValid) - { - return BadRequest(); - } + return Ok(customer.Orders); + } - var customers = UriParseExtenstionDbContext.GetCustomers().Where(c => c.Gender == gender); - return Ok(customers); - } + public IActionResult GetRef(int key, string navigationProperty) + { + var serviceRootUri = GetServiceRootUri(); + var entityId = string.Format("{0}/Customers({1})/{2}", serviceRootUri, key, navigationProperty); + return Ok(new Uri(entityId)); + } - private string GetServiceRootUri() - { - return Request.CreateODataLink(); - } + [HttpGet] + public IActionResult CalculateSalary(int key, int month) + { + return Ok("CalculateSalary: Key(" + key + ")(" + month + ")"); } - public class OrdersController : ODataController + [HttpPost] + public IActionResult UpdateAddress(int key) { - [EnableQuery] - public IActionResult Get() - { - return Ok(UriParseExtenstionDbContext.GetOrders()); - } + return Ok("UpdateAddress: Key(" + key + ")"); + } - public IActionResult Get(int key) + [HttpGet] + public IActionResult GetCustomerByGender([FromODataUri]Gender gender) + { + if (!this.ModelState.IsValid) { - return Ok(UriParseExtenstionDbContext.GetOrders().FirstOrDefault(c => c.Id == key)); + return BadRequest(); } + + var customers = UriParseExtenstionDbContext.GetCustomers().Where(c => c.Gender == gender); + return Ok(customers); } - public class UriParseExtenstionDbContext + private string GetServiceRootUri() { - private static IList _customers; - private static IList _orders; + return Request.CreateODataLink(); + } +} - public static IList GetCustomers() - { - if (_customers == null) - { - Generate(); - } +public class OrdersController : ODataController +{ + [EnableQuery] + public IActionResult Get() + { + return Ok(UriParseExtenstionDbContext.GetOrders()); + } - return _customers; - } + public IActionResult Get(int key) + { + return Ok(UriParseExtenstionDbContext.GetOrders().FirstOrDefault(c => c.Id == key)); + } +} - public static IList GetOrders() - { - if (_orders == null) - { - Generate(); - } +public class UriParseExtenstionDbContext +{ + private static IList _customers; + private static IList _orders; - return _orders; + public static IList GetCustomers() + { + if (_customers == null) + { + Generate(); } - private static void Generate() + return _customers; + } + + public static IList GetOrders() + { + if (_orders == null) { - _customers = Enumerable.Range(1, 5).Select(e => - new Customer - { - Id = e, - Name = "Customer #" + e, - Gender = e%2 == 0 ? Gender.Female : Gender.Male, - Orders = Enumerable.Range(1, e + 1).Select(f => - new Order - { - Id = f, - Title = "Order #" + f - }).ToList() - }).ToList(); - - _customers.Add(new VipCustomer + Generate(); + } + + return _orders; + } + + private static void Generate() + { + _customers = Enumerable.Range(1, 5).Select(e => + new Customer { - Id = 6, - Name = "VipCustomer #6", - Gender = Gender.Female, - Orders = Enumerable.Range(1, 3).Select(f => + Id = e, + Name = "Customer #" + e, + Gender = e%2 == 0 ? Gender.Female : Gender.Male, + Orders = Enumerable.Range(1, e + 1).Select(f => new Order { Id = f, Title = "Order #" + f - }).ToList(), - VipProperty = "VipProperty " - }); + }).ToList() + }).ToList(); - _orders = new List(); - foreach (var customer in _customers) - { - foreach (var order in customer.Orders) + _customers.Add(new VipCustomer + { + Id = 6, + Name = "VipCustomer #6", + Gender = Gender.Female, + Orders = Enumerable.Range(1, 3).Select(f => + new Order { - _orders.Add(order); - } + Id = f, + Title = "Order #" + f + }).ToList(), + VipProperty = "VipProperty " + }); + + _orders = new List(); + foreach (var customer in _customers) + { + foreach (var order in customer.Orders) + { + _orders.Add(order); } } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriParserExtenstionEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriParserExtenstionEdmModel.cs index b4e86ad0b..7945d21ed 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriParserExtenstionEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriParserExtenstionEdmModel.cs @@ -8,24 +8,23 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension +namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension; + +public class UriParserExtenstionEdmModel { - public class UriParserExtenstionEdmModel + public static IEdmModel GetEdmModel() { - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Orders"); + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); - builder.EntityType().Function("CalculateSalary").Returns().Parameter("month"); - builder.EntityType().Action("UpdateAddress"); - builder.EntityType() - .Collection.Function("GetCustomerByGender") - .ReturnsCollectionFromEntitySet("Customers") - .Parameter("gender"); + builder.EntityType().Function("CalculateSalary").Returns().Parameter("month"); + builder.EntityType().Action("UpdateAddress"); + builder.EntityType() + .Collection.Function("GetCustomerByGender") + .ReturnsCollectionFromEntitySet("Customers") + .Parameter("gender"); - return builder.GetEdmModel(); - } + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriResolverDependencyTestWithOldDefaultConfig.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriResolverDependencyTestWithOldDefaultConfig.cs index 71622b09e..8a40d8a48 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriResolverDependencyTestWithOldDefaultConfig.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/UriParserExtension/UriResolverDependencyTestWithOldDefaultConfig.cs @@ -17,55 +17,54 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension +namespace Microsoft.AspNetCore.OData.E2E.Tests.UriParserExtension; + +public class UnqualifiedCallTestWithOldDefaultConfig : WebApiTestBase { - public class UnqualifiedCallTestWithOldDefaultConfig : WebApiTestBase + public UnqualifiedCallTestWithOldDefaultConfig(WebApiTestFixture fixture) + : base(fixture) { - public UnqualifiedCallTestWithOldDefaultConfig(WebApiTestFixture fixture) - : base(fixture) - { - } + } - protected static void UpdateConfigureServices(IServiceCollection services) - { - IEdmModel model = UriParserExtenstionEdmModel.GetEdmModel(); + protected static void UpdateConfigureServices(IServiceCollection services) + { + IEdmModel model = UriParserExtenstionEdmModel.GetEdmModel(); - services.ConfigureControllers(typeof(CustomersController), typeof(OrdersController), typeof(MetadataController)); + services.ConfigureControllers(typeof(CustomersController), typeof(OrdersController), typeof(MetadataController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model).RouteOptions.EnableUnqualifiedOperationCall = false); - } + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model).RouteOptions.EnableUnqualifiedOperationCall = false); + } - public static TheoryDataSet urisForOldDefaultConfig + public static TheoryDataSet urisForOldDefaultConfig + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() - { - // bad cases - { "Get", "Customers(1)/CalculateSalary(month=2)", HttpStatusCode.NotFound }, - // { "Post", "Customers(1)/UpdateAddress", HttpStatusCode.NotFound }, - // { "Get", "CuStOmRrS(1)/Default.CaLcUlAtESaLaRy(MoNtH=2)", HttpStatusCode.NotFound }, - // { "Post", "CuUtOmRrS(1)/Default.UpDaTeAdDrEsS", HttpStatusCode.NotFound }, + // bad cases + { "Get", "Customers(1)/CalculateSalary(month=2)", HttpStatusCode.NotFound }, + // { "Post", "Customers(1)/UpdateAddress", HttpStatusCode.NotFound }, + // { "Get", "CuStOmRrS(1)/Default.CaLcUlAtESaLaRy(MoNtH=2)", HttpStatusCode.NotFound }, + // { "Post", "CuUtOmRrS(1)/Default.UpDaTeAdDrEsS", HttpStatusCode.NotFound }, - // good cases - { "Get", "Customers(1)/Default.CalculateSalary(month=2)", HttpStatusCode.OK }, - { "Post", "Customers(1)/Default.UpdateAddress", HttpStatusCode.OK }, - }; - } + // good cases + { "Get", "Customers(1)/Default.CalculateSalary(month=2)", HttpStatusCode.OK }, + { "Post", "Customers(1)/Default.UpdateAddress", HttpStatusCode.OK }, + }; } + } - [Theory] - [MemberData(nameof(urisForOldDefaultConfig))] - public async Task ParseUriWithOldDefaultRestored(string method, string uri, HttpStatusCode expectedStatusCode) - { - // Case Insensitive - HttpClient client = CreateClient(); + [Theory] + [MemberData(nameof(urisForOldDefaultConfig))] + public async Task ParseUriWithOldDefaultRestored(string method, string uri, HttpStatusCode expectedStatusCode) + { + // Case Insensitive + HttpClient client = CreateClient(); - var caseInsensitiveUri = $"odata/{uri}"; - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), caseInsensitiveUri); - HttpResponseMessage response = await client.SendAsync(request); + var caseInsensitiveUri = $"odata/{uri}"; + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), caseInsensitiveUri); + HttpResponseMessage response = await client.SendAsync(request); - Assert.Equal(expectedStatusCode, response.StatusCode); - } + Assert.Equal(expectedStatusCode, response.StatusCode); } } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Validation/DeltaOfTValidationTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/Validation/DeltaOfTValidationTests.cs index cb582af21..8357ab2ac 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Validation/DeltaOfTValidationTests.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Validation/DeltaOfTValidationTests.cs @@ -19,95 +19,94 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.E2E.Tests.Validation +namespace Microsoft.AspNetCore.OData.E2E.Tests.Validation; + +public class DeltaOfTValidationTests : WebApiTestBase { - public class DeltaOfTValidationTests : WebApiTestBase + public DeltaOfTValidationTests(WebApiTestFixture fixture) + :base(fixture) { - public DeltaOfTValidationTests(WebApiTestFixture fixture) - :base(fixture) - { - } + } - // following the Fixture convention. - protected static void UpdateConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(PatchCustomersController)); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", GetModel())); - } + // following the Fixture convention. + protected static void UpdateConfigureServices(IServiceCollection services) + { + services.ConfigureControllers(typeof(PatchCustomersController)); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", GetModel())); + } - private static IEdmModel GetModel() - { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - EntitySetConfiguration patchCustomer = builder.EntitySet("PatchCustomers"); - patchCustomer.EntityType.Property(p => p.ExtraProperty).IsRequired(); - return builder.GetEdmModel(); - } + private static IEdmModel GetModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + EntitySetConfiguration patchCustomer = builder.EntitySet("PatchCustomers"); + patchCustomer.EntityType.Property(p => p.ExtraProperty).IsRequired(); + return builder.GetEdmModel(); + } - [Theory] - [InlineData(HttpStatusCode.BadRequest, "The field ExtraProperty must match the regular expression 'Some value'")] - [InlineData(HttpStatusCode.OK, "")] - public async Task CanValidatePatches(HttpStatusCode statusCode, string message) + [Theory] + [InlineData(HttpStatusCode.BadRequest, "The field ExtraProperty must match the regular expression 'Some value'")] + [InlineData(HttpStatusCode.OK, "")] + public async Task CanValidatePatches(HttpStatusCode statusCode, string message) + { + // Arrange + object payload = null; + switch (statusCode) { - // Arrange - object payload = null; - switch (statusCode) - { - case HttpStatusCode.BadRequest: - payload = new { Id = 5, Name = "Some name", ExtraProperty = "Another value" }; - break; + case HttpStatusCode.BadRequest: + payload = new { Id = 5, Name = "Some name", ExtraProperty = "Another value" }; + break; - case HttpStatusCode.OK: - payload = new { }; - break; - } - string payloadStr = JsonSerializer.Serialize(payload); + case HttpStatusCode.OK: + payload = new { }; + break; + } + string payloadStr = JsonSerializer.Serialize(payload); - HttpClient client = CreateClient(); - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), "odata/PatchCustomers(5)"); - request.Content = new StringContent(payloadStr); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = payloadStr.Length; + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), "odata/PatchCustomers(5)"); + request.Content = new StringContent(payloadStr); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = payloadStr.Length; - // Act - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.Equal(statusCode, response.StatusCode); - if (response.StatusCode == HttpStatusCode.BadRequest) - { - var result = await response.Content.ReadAsStringAsync(); - Assert.Contains(message, result); - } + // Assert + Assert.Equal(statusCode, response.StatusCode); + if (response.StatusCode == HttpStatusCode.BadRequest) + { + var result = await response.Content.ReadAsStringAsync(); + Assert.Contains(message, result); } } +} - public class PatchCustomer - { - public int Id { get; set; } +public class PatchCustomer +{ + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - [RegularExpression("Some value")] - public string ExtraProperty { get; set; } - } + [RegularExpression("Some value")] + public string ExtraProperty { get; set; } +} - public class PatchCustomersController : Controller +public class PatchCustomersController : Controller +{ + [HttpPatch] + public IActionResult Patch(int key, [FromBody]Delta patch) { - [HttpPatch] - public IActionResult Patch(int key, [FromBody]Delta patch) - { - PatchCustomer c = new PatchCustomer() { Id = key, ExtraProperty = "Some value" }; - patch.Patch(c); - TryValidateModel(c); + PatchCustomer c = new PatchCustomer() { Id = key, ExtraProperty = "Some value" }; + patch.Patch(c); + TryValidateModel(c); - if (ModelState.IsValid) - { - return Ok(c); - } - else - { - return BadRequest(ModelState); - } + if (ModelState.IsValid) + { + return Ok(c); + } + else + { + return BadRequest(ModelState); } } } diff --git a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JDynamicTypeWrapperConverterTests.cs b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JDynamicTypeWrapperConverterTests.cs index db3a96be6..1c1ca8f90 100644 --- a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JDynamicTypeWrapperConverterTests.cs +++ b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JDynamicTypeWrapperConverterTests.cs @@ -10,143 +10,142 @@ using Microsoft.AspNetCore.OData.Query.Wrapper; using Xunit; -namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests +namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests; + +public class JDynamicTypeWrapperConverterTests { - public class JDynamicTypeWrapperConverterTests + [Theory] + [InlineData(typeof(AggregationWrapper), true)] + [InlineData(typeof(ComputeWrapper), true)] + [InlineData(typeof(EntitySetAggregationWrapper), true)] + [InlineData(typeof(FlatteningWrapper), true)] + [InlineData(typeof(GroupByWrapper), true)] + [InlineData(typeof(NoGroupByAggregationWrapper), true)] + [InlineData(typeof(NoGroupByWrapper), true)] + [InlineData(typeof(object), false)] + [InlineData(typeof(SelectExpandWrapper), false)] + public void CanConvertWorksForDynamicTypeWrapper(Type type, bool expected) { - [Theory] - [InlineData(typeof(AggregationWrapper), true)] - [InlineData(typeof(ComputeWrapper), true)] - [InlineData(typeof(EntitySetAggregationWrapper), true)] - [InlineData(typeof(FlatteningWrapper), true)] - [InlineData(typeof(GroupByWrapper), true)] - [InlineData(typeof(NoGroupByAggregationWrapper), true)] - [InlineData(typeof(NoGroupByWrapper), true)] - [InlineData(typeof(object), false)] - [InlineData(typeof(SelectExpandWrapper), false)] - public void CanConvertWorksForDynamicTypeWrapper(Type type, bool expected) - { - // Arrange - DynamicTypeWrapperConverter converter = new DynamicTypeWrapperConverter(); + // Arrange + DynamicTypeWrapperConverter converter = new DynamicTypeWrapperConverter(); - // Act & Assert - Assert.Equal(expected, converter.CanConvert(type)); - } + // Act & Assert + Assert.Equal(expected, converter.CanConvert(type)); + } - [Fact] - public void ReadJsonForDynamicTypeWrapperThrowsNotImplementedException() - { - // Arrange - JDynamicTypeWrapperConverter converter = new JDynamicTypeWrapperConverter(); + [Fact] + public void ReadJsonForDynamicTypeWrapperThrowsNotImplementedException() + { + // Arrange + JDynamicTypeWrapperConverter converter = new JDynamicTypeWrapperConverter(); - // Act - Action test = () => converter.ReadJson(null, typeof(object), null, null); + // Act + Action test = () => converter.ReadJson(null, typeof(object), null, null); - // Assert - NotImplementedException exception = Assert.Throws(test); - Assert.Equal(SRResources.ReadDynamicTypeWrapperNotImplemented, exception.Message); - } + // Assert + NotImplementedException exception = Assert.Throws(test); + Assert.Equal(SRResources.ReadDynamicTypeWrapperNotImplemented, exception.Message); + } - [Fact] - public void AggregationWrapperConverterCanSerializeAggregationWrapper() - { - // Arrange & Act & Assert - TestDynamicTypeWrapperConverter(); - } + [Fact] + public void AggregationWrapperConverterCanSerializeAggregationWrapper() + { + // Arrange & Act & Assert + TestDynamicTypeWrapperConverter(); + } - [Fact] - public void EntitySetAggregationWrapperConverterCanSerializeEntitySetAggregationWrapper() - { - // Arrange & Act & Assert - TestDynamicTypeWrapperConverter(); - } + [Fact] + public void EntitySetAggregationWrapperConverterCanSerializeEntitySetAggregationWrapper() + { + // Arrange & Act & Assert + TestDynamicTypeWrapperConverter(); + } - [Fact] - public void GroupByWrapperWrapperConverterCanSerializeGroupByWrapper() - { - // Arrange & Act & Assert - TestDynamicTypeWrapperConverter(); - } + [Fact] + public void GroupByWrapperWrapperConverterCanSerializeGroupByWrapper() + { + // Arrange & Act & Assert + TestDynamicTypeWrapperConverter(); + } - [Fact] - public void NoGroupByAggregationWrapperConverterCanSerializeNoGroupByAggregationWrapper() - { - // Arrange & Act & Assert - TestDynamicTypeWrapperConverter(); - } + [Fact] + public void NoGroupByAggregationWrapperConverterCanSerializeNoGroupByAggregationWrapper() + { + // Arrange & Act & Assert + TestDynamicTypeWrapperConverter(); + } - [Fact] - public void NoGroupByWrapperWrapperConverterCanSerializeNoGroupByWrapper() - { - // Arrange & Act & Assert - TestDynamicTypeWrapperConverter(); - } + [Fact] + public void NoGroupByWrapperWrapperConverterCanSerializeNoGroupByWrapper() + { + // Arrange & Act & Assert + TestDynamicTypeWrapperConverter(); + } - internal static void TestDynamicTypeWrapperConverter() where T : GroupByWrapper - { - // Arrange - T wrapper = (T)Activator.CreateInstance(typeof(T)); + internal static void TestDynamicTypeWrapperConverter() where T : GroupByWrapper + { + // Arrange + T wrapper = (T)Activator.CreateInstance(typeof(T)); - wrapper.GroupByContainer = new AggregationPropertyContainer() - { - Name = "TestProp", - Value = "TestValue" - }; + wrapper.GroupByContainer = new AggregationPropertyContainer() + { + Name = "TestProp", + Value = "TestValue" + }; - JDynamicTypeWrapperConverter converter = new JDynamicTypeWrapperConverter(); + JDynamicTypeWrapperConverter converter = new JDynamicTypeWrapperConverter(); - // Act - string json = SerializeUtils.WriteJson(converter, wrapper); + // Act + string json = SerializeUtils.WriteJson(converter, wrapper); - // Assert - Assert.Equal("{\"TestProp\":\"TestValue\"}", json); - } + // Assert + Assert.Equal("{\"TestProp\":\"TestValue\"}", json); + } - [Fact] - public void ComputeWrapperOfTypeConverterCanSerializeGroupByWrapper() + [Fact] + public void ComputeWrapperOfTypeConverterCanSerializeGroupByWrapper() + { + // Arrange + GroupByWrapper wrapper = new GroupByWrapper(); + wrapper.GroupByContainer = new AggregationPropertyContainer() { - // Arrange - GroupByWrapper wrapper = new GroupByWrapper(); - wrapper.GroupByContainer = new AggregationPropertyContainer() - { - Name = "TestProp", - Value = "TestValue" - }; + Name = "TestProp", + Value = "TestValue" + }; - ComputeWrapper computeWrapper = new ComputeWrapper - { - Instance = wrapper - }; + ComputeWrapper computeWrapper = new ComputeWrapper + { + Instance = wrapper + }; - JDynamicTypeWrapperConverter converter = new JDynamicTypeWrapperConverter(); + JDynamicTypeWrapperConverter converter = new JDynamicTypeWrapperConverter(); - // Act - string json = SerializeUtils.WriteJson(converter, computeWrapper); + // Act + string json = SerializeUtils.WriteJson(converter, computeWrapper); - // Assert - Assert.Equal("{\"TestProp\":\"TestValue\"}", json); - } + // Assert + Assert.Equal("{\"TestProp\":\"TestValue\"}", json); + } - [Fact] - public void FlatteningWrapperOfTypeConverterCanSerializeGroupByWrapper() + [Fact] + public void FlatteningWrapperOfTypeConverterCanSerializeGroupByWrapper() + { + // Arrange + FlatteningWrapper flatteningWrapper = new FlatteningWrapper { - // Arrange - FlatteningWrapper flatteningWrapper = new FlatteningWrapper + GroupByContainer = new AggregationPropertyContainer() { - GroupByContainer = new AggregationPropertyContainer() - { - Name = "TestProp", - Value = "TestValue" - } - }; + Name = "TestProp", + Value = "TestValue" + } + }; - JDynamicTypeWrapperConverter converter = new JDynamicTypeWrapperConverter(); + JDynamicTypeWrapperConverter converter = new JDynamicTypeWrapperConverter(); - // Act - string json = SerializeUtils.WriteJson(converter, flatteningWrapper); + // Act + string json = SerializeUtils.WriteJson(converter, flatteningWrapper); - // Assert - Assert.Equal("{\"TestProp\":\"TestValue\"}", json); - } + // Assert + Assert.Equal("{\"TestProp\":\"TestValue\"}", json); } } diff --git a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JPageResultValueConverterTests.cs b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JPageResultValueConverterTests.cs index 8bf1b1a60..c3b0cb333 100644 --- a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JPageResultValueConverterTests.cs +++ b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JPageResultValueConverterTests.cs @@ -11,79 +11,79 @@ using Microsoft.AspNetCore.OData.Results; using Xunit; -namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests +namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests; + +public class JPageResultValueConverterTests { - public class JPageResultValueConverterTests + private static IList _customers = new List { - private static IList _customers = new List + new PageCustomer { - new PageCustomer - { - Id = 1, - Name = "XU" - }, - new PageCustomer - { - Id = 2, - Name = "WU" - }, - }; - - [Theory] - [InlineData(typeof(PageResult), true)] - [InlineData(typeof(PageResult), true)] - [InlineData(typeof(SelectExpandWrapper), false)] - [InlineData(typeof(object), false)] - public void CanConvertWorksForPageResult(Type type, bool expected) + Id = 1, + Name = "XU" + }, + new PageCustomer { - // Arrange - JPageResultValueConverter converter = new JPageResultValueConverter(); + Id = 2, + Name = "WU" + }, + }; - // Act & Assert - Assert.Equal(expected, converter.CanConvert(type)); - } + [Theory] + [InlineData(typeof(PageResult), true)] + [InlineData(typeof(PageResult), true)] + [InlineData(typeof(SelectExpandWrapper), false)] + [InlineData(typeof(object), false)] + public void CanConvertWorksForPageResult(Type type, bool expected) + { + // Arrange + JPageResultValueConverter converter = new JPageResultValueConverter(); - [Fact] - public void ReadJsonForPageResultThrowsNotImplementedException() - { - // Arrange - JPageResultValueConverter converter = new JPageResultValueConverter(); + // Act & Assert + Assert.Equal(expected, converter.CanConvert(type)); + } - // Act - Action test = () => converter.ReadJson(null, typeof(object), null, null); + [Fact] + public void ReadJsonForPageResultThrowsNotImplementedException() + { + // Arrange + JPageResultValueConverter converter = new JPageResultValueConverter(); - // Assert - NotImplementedException exception = Assert.Throws(test); - Assert.Equal(SRResources.ReadPageResultNotImplemented, exception.Message); - } + // Act + Action test = () => converter.ReadJson(null, typeof(object), null, null); - [Fact] - public void CanWritePageResultOnlyWithEnumerableToJsonUsingNewtonsoftJsonConverter() - { - // Arrange - PageResult pageResult = new PageResult(_customers, null, null); - JPageResultValueConverter converter = new JPageResultValueConverter(); + // Assert + NotImplementedException exception = Assert.Throws(test); + Assert.Equal(SRResources.ReadPageResultNotImplemented, exception.Message); + } + + [Fact] + public void CanWritePageResultOnlyWithEnumerableToJsonUsingNewtonsoftJsonConverter() + { + // Arrange + PageResult pageResult = new PageResult(_customers, null, null); + JPageResultValueConverter converter = new JPageResultValueConverter(); - // Act - string json = SerializeUtils.WriteJson(converter, pageResult); + // Act + string json = SerializeUtils.WriteJson(converter, pageResult); - // Assert - Assert.Equal("{\"items\":[{\"Id\":1,\"Name\":\"XU\"},{\"Id\":2,\"Name\":\"WU\"}]}", json); - } + // Assert + Assert.Equal("{\"items\":[{\"Id\":1,\"Name\":\"XU\"},{\"Id\":2,\"Name\":\"WU\"}]}", json); + } - [Fact] - public void CanWritePageResultAllToJsonUsingNewtonsoftJsonConverter() - { - // Arrange - Uri uri = new Uri("http://any"); - PageResult pageResult = new PageResult(_customers, uri, 4); - JPageResultValueConverter converter = new JPageResultValueConverter(); + [Fact] + public void CanWritePageResultAllToJsonUsingNewtonsoftJsonConverter() + { + // Arrange + Uri uri = new Uri("http://any"); + PageResult pageResult = new PageResult(_customers, uri, 4); + JPageResultValueConverter converter = new JPageResultValueConverter(); - // Act - string json = SerializeUtils.WriteJson(converter, pageResult, true); + // Act + string json = SerializeUtils.WriteJson(converter, pageResult, true); - // Assert - Assert.Equal(@"{ + // Assert + Assert.Equal(@"{ ""items"": [ { ""Id"": 1, @@ -97,13 +97,12 @@ public void CanWritePageResultAllToJsonUsingNewtonsoftJsonConverter() ""nextpagelink"": ""http://any"", ""count"": 4 }", json); - } } +} - public class PageCustomer - { - public int Id { get; set; } +public class PageCustomer +{ + public int Id { get; set; } - public string Name { get; set; } - } + public string Name { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JSelectExpandWrapperConverterTests.cs b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JSelectExpandWrapperConverterTests.cs index f88291806..4566223dd 100644 --- a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JSelectExpandWrapperConverterTests.cs +++ b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JSelectExpandWrapperConverterTests.cs @@ -12,110 +12,110 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests +namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests; + +public class JSelectExpandWrapperConverterTests { - public class JSelectExpandWrapperConverterTests + private static IEdmModel _edmModel = GetEdmModel(); + + [Theory] + [InlineData(typeof(SelectAllAndExpand), true)] + [InlineData(typeof(SelectAll), true)] + [InlineData(typeof(SelectExpandWrapper), true)] + [InlineData(typeof(SelectSomeAndInheritance), true)] + [InlineData(typeof(SelectSome), true)] + [InlineData(typeof(SelectExpandWrapper), true)] + [InlineData(typeof(FlatteningWrapper), false)] + [InlineData(typeof(NoGroupByWrapper), false)] + [InlineData(typeof(object), false)] + public void CanConvertWorksForSelectExpandWrapper(Type type, bool expected) { - private static IEdmModel _edmModel = GetEdmModel(); - - [Theory] - [InlineData(typeof(SelectAllAndExpand), true)] - [InlineData(typeof(SelectAll), true)] - [InlineData(typeof(SelectExpandWrapper), true)] - [InlineData(typeof(SelectSomeAndInheritance), true)] - [InlineData(typeof(SelectSome), true)] - [InlineData(typeof(SelectExpandWrapper), true)] - [InlineData(typeof(FlatteningWrapper), false)] - [InlineData(typeof(NoGroupByWrapper), false)] - [InlineData(typeof(object), false)] - public void CanConvertWorksForSelectExpandWrapper(Type type, bool expected) - { - // Arrange - JSelectExpandWrapperConverter converter = new JSelectExpandWrapperConverter(); + // Arrange + JSelectExpandWrapperConverter converter = new JSelectExpandWrapperConverter(); - // Act & Assert - Assert.Equal(expected, converter.CanConvert(type)); - } + // Act & Assert + Assert.Equal(expected, converter.CanConvert(type)); + } - [Fact] - public void ReadJsonThrowsNotImplementedException() - { - // Arrange - JSelectExpandWrapperConverter converter = new JSelectExpandWrapperConverter(); + [Fact] + public void ReadJsonThrowsNotImplementedException() + { + // Arrange + JSelectExpandWrapperConverter converter = new JSelectExpandWrapperConverter(); - // Act - Action test = () => converter.ReadJson(null, typeof(object), null, null); + // Act + Action test = () => converter.ReadJson(null, typeof(object), null, null); - // Assert - NotImplementedException exception = Assert.Throws(test); - Assert.Equal(SRResources.ReadSelectExpandWrapperNotImplemented, exception.Message); - } + // Assert + NotImplementedException exception = Assert.Throws(test); + Assert.Equal(SRResources.ReadSelectExpandWrapperNotImplemented, exception.Message); + } - [Fact] - public void CanWriteSelectAllToJsonUsingNewtonsoftJsonConverter() + [Fact] + public void CanWriteSelectAllToJsonUsingNewtonsoftJsonConverter() + { + // Arrange + SelectAll selectAll = new SelectAll(); + selectAll.Container = null; + selectAll.Model = _edmModel; + selectAll.UseInstanceForProperties = true; + selectAll.Instance = new Customer { - // Arrange - SelectAll selectAll = new SelectAll(); - selectAll.Container = null; - selectAll.Model = _edmModel; - selectAll.UseInstanceForProperties = true; - selectAll.Instance = new Customer + Id = 2, + Name = "abc", + Location = new Address { - Id = 2, - Name = "abc", - Location = new Address - { - Street = "37TH PL", - City = "Reond" - } - }; - - JSelectExpandWrapperConverter converter = new JSelectExpandWrapperConverter(); - - // Act - string json = SerializeUtils.WriteJson(converter, selectAll); - - // Assert - Assert.Equal("{\"Id\":2,\"Name\":\"abc\",\"Location\":{\"Street\":\"37TH PL\",\"City\":\"Reond\"}}", json); - } - - [Fact] - public void CanWriteSelectAllAndExpandToJsonUsingNewtonsoftJsonConverter() + Street = "37TH PL", + City = "Reond" + } + }; + + JSelectExpandWrapperConverter converter = new JSelectExpandWrapperConverter(); + + // Act + string json = SerializeUtils.WriteJson(converter, selectAll); + + // Assert + Assert.Equal("{\"Id\":2,\"Name\":\"abc\",\"Location\":{\"Street\":\"37TH PL\",\"City\":\"Reond\"}}", json); + } + + [Fact] + public void CanWriteSelectAllAndExpandToJsonUsingNewtonsoftJsonConverter() + { + // Arrange + SelectAll expandOrder = new SelectAll(); + expandOrder.Model = _edmModel; + expandOrder.UseInstanceForProperties = true; + expandOrder.Instance = new Order { - // Arrange - SelectAll expandOrder = new SelectAll(); - expandOrder.Model = _edmModel; - expandOrder.UseInstanceForProperties = true; - expandOrder.Instance = new Order - { - Id = 21, - Title = "new Order21" - }; - - SelectAllAndExpand selectExpand = new SelectAllAndExpand(); - MockPropertyContainer container = new MockPropertyContainer(); - container.Properties["Orders"] = expandOrder; // expanded - selectExpand.Container = container; - selectExpand.Model = _edmModel; - selectExpand.UseInstanceForProperties = true; - selectExpand.Instance = new Customer + Id = 21, + Title = "new Order21" + }; + + SelectAllAndExpand selectExpand = new SelectAllAndExpand(); + MockPropertyContainer container = new MockPropertyContainer(); + container.Properties["Orders"] = expandOrder; // expanded + selectExpand.Container = container; + selectExpand.Model = _edmModel; + selectExpand.UseInstanceForProperties = true; + selectExpand.Instance = new Customer + { + Id = 2, + Name = "abc", + Location = new Address { - Id = 2, - Name = "abc", - Location = new Address - { - Street = "37TH PL", - City = "Reond" - } - }; - - JSelectExpandWrapperConverter converter = new JSelectExpandWrapperConverter(); - - // Act - string json = SerializeUtils.WriteJson(converter, selectExpand, true); - - // Assert - Assert.Equal(@"{ + Street = "37TH PL", + City = "Reond" + } + }; + + JSelectExpandWrapperConverter converter = new JSelectExpandWrapperConverter(); + + // Act + string json = SerializeUtils.WriteJson(converter, selectExpand, true); + + // Assert + Assert.Equal(@"{ ""Orders"": { ""Id"": 21, ""Title"": ""new Order21"" @@ -127,32 +127,31 @@ public void CanWriteSelectAllAndExpandToJsonUsingNewtonsoftJsonConverter() ""City"": ""Reond"" } }", json); - } + } - [Fact] - public void CanWriteSelectSomeWrapperToJsonUsingNewtonsoftJsonConverter() - { - // Arrange - SelectSome selectSome = new SelectSome(); - MockPropertyContainer container = new MockPropertyContainer(); - container.Properties["Name"] = "sam"; - selectSome.Container = container; - selectSome.Model = _edmModel; + [Fact] + public void CanWriteSelectSomeWrapperToJsonUsingNewtonsoftJsonConverter() + { + // Arrange + SelectSome selectSome = new SelectSome(); + MockPropertyContainer container = new MockPropertyContainer(); + container.Properties["Name"] = "sam"; + selectSome.Container = container; + selectSome.Model = _edmModel; - JSelectExpandWrapperConverter converter = new JSelectExpandWrapperConverter(); + JSelectExpandWrapperConverter converter = new JSelectExpandWrapperConverter(); - // Act - string json = SerializeUtils.WriteJson(converter, selectSome); + // Act + string json = SerializeUtils.WriteJson(converter, selectSome); - // Assert - Assert.Equal("{\"Name\":\"sam\"}", json); - } + // Assert + Assert.Equal("{\"Name\":\"sam\"}", json); + } - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntityType(); - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntityType(); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JSingleResultValueConverterTests.cs b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JSingleResultValueConverterTests.cs index ea9ce1180..892198af8 100644 --- a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JSingleResultValueConverterTests.cs +++ b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JSingleResultValueConverterTests.cs @@ -12,71 +12,70 @@ using Microsoft.AspNetCore.OData.Results; using Xunit; -namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests +namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests; + +public class JSingleResultValueConverterTests { - public class JSingleResultValueConverterTests + private static IQueryable _customers = new List { - private static IQueryable _customers = new List + new SingleCustomer { - new SingleCustomer - { - Id = 1, - Name = "XU" - }, - new SingleCustomer - { - Id = 2, - Name = "WU" - }, - }.AsQueryable(); - - [Theory] - [InlineData(typeof(SingleResult), true)] - [InlineData(typeof(SingleResult), true)] - [InlineData(typeof(SelectExpandWrapper), false)] - [InlineData(typeof(object), false)] - public void CanConvertWorksForSingleResult(Type type, bool expected) + Id = 1, + Name = "XU" + }, + new SingleCustomer { - // Arrange - JSingleResultValueConverter converter = new JSingleResultValueConverter(); + Id = 2, + Name = "WU" + }, + }.AsQueryable(); - // Act & Assert - Assert.Equal(expected, converter.CanConvert(type)); - } - - [Fact] - public void ReadJsonForSingleResultThrowsNotImplementedException() - { - // Arrange - JSingleResultValueConverter converter = new JSingleResultValueConverter(); - - // Act - Action test = () => converter.ReadJson(null, typeof(object), null, null); + [Theory] + [InlineData(typeof(SingleResult), true)] + [InlineData(typeof(SingleResult), true)] + [InlineData(typeof(SelectExpandWrapper), false)] + [InlineData(typeof(object), false)] + public void CanConvertWorksForSingleResult(Type type, bool expected) + { + // Arrange + JSingleResultValueConverter converter = new JSingleResultValueConverter(); - // Assert - NotImplementedException exception = Assert.Throws(test); - Assert.Equal(SRResources.ReadSingleResultNotImplemented, exception.Message); - } + // Act & Assert + Assert.Equal(expected, converter.CanConvert(type)); + } - [Fact] - public void CanWriteSingleResultToJsonUsingNewtonsoftJsonConverter() - { - // Arrange - SingleResult pageResult = new SingleResult(_customers); - JSingleResultValueConverter converter = new JSingleResultValueConverter(); + [Fact] + public void ReadJsonForSingleResultThrowsNotImplementedException() + { + // Arrange + JSingleResultValueConverter converter = new JSingleResultValueConverter(); - // Act - string json = SerializeUtils.WriteJson(converter, pageResult); + // Act + Action test = () => converter.ReadJson(null, typeof(object), null, null); - // Assert - Assert.Equal("{\"Id\":1,\"Name\":\"XU\"}", json); - } + // Assert + NotImplementedException exception = Assert.Throws(test); + Assert.Equal(SRResources.ReadSingleResultNotImplemented, exception.Message); } - public class SingleCustomer + [Fact] + public void CanWriteSingleResultToJsonUsingNewtonsoftJsonConverter() { - public int Id { get; set; } + // Arrange + SingleResult pageResult = new SingleResult(_customers); + JSingleResultValueConverter converter = new JSingleResultValueConverter(); - public string Name { get; set; } + // Act + string json = SerializeUtils.WriteJson(converter, pageResult); + + // Assert + Assert.Equal("{\"Id\":1,\"Name\":\"XU\"}", json); } } + +public class SingleCustomer +{ + public int Id { get; set; } + + public string Name { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JsonPropertyNameMapperTest.cs b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JsonPropertyNameMapperTest.cs index 0168411ca..3da164885 100644 --- a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JsonPropertyNameMapperTest.cs +++ b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/JsonPropertyNameMapperTest.cs @@ -11,56 +11,55 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Container +namespace Microsoft.AspNetCore.OData.Tests.Query.Container; + +public class JsonPropertyNameMapperTests { - public class JsonPropertyNameMapperTests + [Fact] + public void MapProperty_Maps_PropertyName() { - [Fact] - public void MapProperty_Maps_PropertyName() - { - // Arrange - (IEdmModel model, IEdmStructuredType address) = GetOData(); - JsonPropertyNameMapper mapper = new JsonPropertyNameMapper(model, address); + // Arrange + (IEdmModel model, IEdmStructuredType address) = GetOData(); + JsonPropertyNameMapper mapper = new JsonPropertyNameMapper(model, address); - // Act & Assert - Assert.Equal("Road", mapper.MapProperty("Street")); + // Act & Assert + Assert.Equal("Road", mapper.MapProperty("Street")); - // Act & Assert - Assert.Equal("City", mapper.MapProperty("City")); + // Act & Assert + Assert.Equal("City", mapper.MapProperty("City")); - // Act & Assert - Assert.Null(mapper.MapProperty("IgnoreThis")); - } + // Act & Assert + Assert.Null(mapper.MapProperty("IgnoreThis")); + } - private static (IEdmModel, IEdmStructuredType) GetOData() - { - EdmModel model = new EdmModel(); - EdmComplexType address = new EdmComplexType("NS", "Address"); - address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); - address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); - address.AddStructuralProperty("IgnoreThis", EdmPrimitiveTypeKind.String); - model.AddElement(address); + private static (IEdmModel, IEdmStructuredType) GetOData() + { + EdmModel model = new EdmModel(); + EdmComplexType address = new EdmComplexType("NS", "Address"); + address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("IgnoreThis", EdmPrimitiveTypeKind.String); + model.AddElement(address); - model.SetAnnotationValue(address, new ClrTypeAnnotation(typeof(JAddress))); + model.SetAnnotationValue(address, new ClrTypeAnnotation(typeof(JAddress))); - model.SetAnnotationValue(address.FindProperty("Street"), - new ClrPropertyInfoAnnotation(typeof(JAddress).GetProperty("Street"))); + model.SetAnnotationValue(address.FindProperty("Street"), + new ClrPropertyInfoAnnotation(typeof(JAddress).GetProperty("Street"))); - model.SetAnnotationValue(address.FindProperty("IgnoreThis"), - new ClrPropertyInfoAnnotation(typeof(JAddress).GetProperty("IgnoreThis"))); + model.SetAnnotationValue(address.FindProperty("IgnoreThis"), + new ClrPropertyInfoAnnotation(typeof(JAddress).GetProperty("IgnoreThis"))); - return (model, address); - } + return (model, address); + } - private class JAddress - { - public string City { get; set; } + private class JAddress + { + public string City { get; set; } - [JsonProperty("Road")] - public string Street { get; set; } + [JsonProperty("Road")] + public string Street { get; set; } - [JsonIgnore] - public string IgnoreThis { get; set; } - } + [JsonIgnore] + public string IgnoreThis { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/MockPropertyContainer.cs b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/MockPropertyContainer.cs index 2241e14c1..f20bbc9d3 100644 --- a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/MockPropertyContainer.cs +++ b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/MockPropertyContainer.cs @@ -8,24 +8,23 @@ using System.Collections.Generic; using Microsoft.AspNetCore.OData.Query.Container; -namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests +namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests; + +internal class MockPropertyContainer : PropertyContainer { - internal class MockPropertyContainer : PropertyContainer + public MockPropertyContainer() { - public MockPropertyContainer() - { - Properties = new Dictionary(); - } + Properties = new Dictionary(); + } - public Dictionary Properties { get; private set; } + public Dictionary Properties { get; private set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + foreach (var kvp in Properties) { - foreach (var kvp in Properties) - { - dictionary.Add(kvp.Key, kvp.Value); - } + dictionary.Add(kvp.Key, kvp.Value); } } } diff --git a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/Models/Address.cs b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/Models/Address.cs index b4db96c7c..5705d3660 100644 --- a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/Models/Address.cs +++ b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/Models/Address.cs @@ -5,12 +5,11 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests.Models +namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests.Models; + +public class Address { - public class Address - { - public string Street { get; set; } + public string Street { get; set; } - public string City { get; set; } - } + public string City { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/Models/Customer.cs b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/Models/Customer.cs index 3fa74e8ca..283de631b 100644 --- a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/Models/Customer.cs +++ b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/Models/Customer.cs @@ -5,16 +5,15 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests.Models +namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests.Models; + +public class Customer { - public class Customer - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public Address Location { get; set; } + public Address Location { get; set; } - public Order[] Orders { get; set; } - } + public Order[] Orders { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/Models/Order.cs b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/Models/Order.cs index 1bc006370..261acd50a 100644 --- a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/Models/Order.cs +++ b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/Models/Order.cs @@ -5,12 +5,11 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests.Models +namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests.Models; + +public class Order { - public class Order - { - public int Id { get; set; } + public int Id { get; set; } - public string Title { get; set; } - } + public string Title { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/ODataNewtonsoftJsonMvcBuilderExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/ODataNewtonsoftJsonMvcBuilderExtensionsTests.cs index e9342c505..e94dd327d 100644 --- a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/ODataNewtonsoftJsonMvcBuilderExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/ODataNewtonsoftJsonMvcBuilderExtensionsTests.cs @@ -12,63 +12,62 @@ using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests +namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests; + +public class ODataNewtonsoftJsonMvcBuilderExtensionsTests { - public class ODataNewtonsoftJsonMvcBuilderExtensionsTests + [Fact] + public void AddODataNewtonsoftJson_OnIMvcBuilder_Throws_NullBuilder() { - [Fact] - public void AddODataNewtonsoftJson_OnIMvcBuilder_Throws_NullBuilder() - { - // Arrange - IMvcBuilder builder = null; + // Arrange + IMvcBuilder builder = null; - // Act & Assert - Assert.Throws("builder", () => builder.AddODataNewtonsoftJson()); - } + // Act & Assert + Assert.Throws("builder", () => builder.AddODataNewtonsoftJson()); + } - [Fact] - public void AddODataNewtonsoftJson_OnIMvcCoreBuilder_Throws_NullBuilder() - { - // Arrange - IMvcCoreBuilder builder = null; + [Fact] + public void AddODataNewtonsoftJson_OnIMvcCoreBuilder_Throws_NullBuilder() + { + // Arrange + IMvcCoreBuilder builder = null; - // Act & Assert - Assert.Throws("builder", () => builder.AddODataNewtonsoftJson()); - } + // Act & Assert + Assert.Throws("builder", () => builder.AddODataNewtonsoftJson()); + } - [Fact] - public void AddODataNewtonsoftJson_OnMvcCoreBuilder_RegistersODataJsonConverts() + [Fact] + public void AddODataNewtonsoftJson_OnMvcCoreBuilder_RegistersODataJsonConverts() + { + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); + IMvcCoreBuilder builder = new MyMvcCoreBuilder { - // Arrange - var services = new ServiceCollection(); - services.AddLogging(); - IMvcCoreBuilder builder = new MyMvcCoreBuilder - { - Services = services - }; + Services = services + }; - // Act - builder.AddODataNewtonsoftJson(); - IServiceProvider provider = services.BuildServiceProvider(); + // Act + builder.AddODataNewtonsoftJson(); + IServiceProvider provider = services.BuildServiceProvider(); - // Assert - IOptions options = provider.GetService>(); - Assert.NotNull(options); + // Assert + IOptions options = provider.GetService>(); + Assert.NotNull(options); - MvcNewtonsoftJsonOptions jsonOptions = options.Value; - Assert.Contains(jsonOptions.SerializerSettings.Converters, c => c is JSelectExpandWrapperConverter); - Assert.Contains(jsonOptions.SerializerSettings.Converters, c => c is JDynamicTypeWrapperConverter); - Assert.Contains(jsonOptions.SerializerSettings.Converters, c => c is JPageResultValueConverter); - Assert.Contains(jsonOptions.SerializerSettings.Converters, c => c is JSingleResultValueConverter); - } + MvcNewtonsoftJsonOptions jsonOptions = options.Value; + Assert.Contains(jsonOptions.SerializerSettings.Converters, c => c is JSelectExpandWrapperConverter); + Assert.Contains(jsonOptions.SerializerSettings.Converters, c => c is JDynamicTypeWrapperConverter); + Assert.Contains(jsonOptions.SerializerSettings.Converters, c => c is JPageResultValueConverter); + Assert.Contains(jsonOptions.SerializerSettings.Converters, c => c is JSingleResultValueConverter); } +} - internal class MyMvcCoreBuilder : IMvcCoreBuilder - { - /// - public ApplicationPartManager PartManager { get; set; } +internal class MyMvcCoreBuilder : IMvcCoreBuilder +{ + /// + public ApplicationPartManager PartManager { get; set; } - /// - public IServiceCollection Services { get; set; } - } + /// + public IServiceCollection Services { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/SerializeUtils.cs b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/SerializeUtils.cs index a8f620075..a1c904ee0 100644 --- a/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/SerializeUtils.cs +++ b/test/Microsoft.AspNetCore.OData.NewtonsoftJson.Tests/SerializeUtils.cs @@ -9,32 +9,31 @@ using System.Text; using Newtonsoft.Json; -namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests +namespace Microsoft.AspNetCore.OData.NewtonsoftJson.Tests; + +/// +/// Serialize Utils +/// +public static class SerializeUtils { - /// - /// Serialize Utils - /// - public static class SerializeUtils + public static string WriteJson(JsonConverter converter, object value, bool indented = false) { - public static string WriteJson(JsonConverter converter, object value, bool indented = false) - { - StringBuilder sb = new StringBuilder(); - StringWriter sw = new StringWriter(sb); + StringBuilder sb = new StringBuilder(); + StringWriter sw = new StringWriter(sb); - using (JsonWriter writer = new JsonTextWriter(sw)) - { - JsonSerializerSettings settings = new JsonSerializerSettings(); - settings.Formatting = indented ? Formatting.Indented : Formatting.None; - settings.Converters.Add(converter); + using (JsonWriter writer = new JsonTextWriter(sw)) + { + JsonSerializerSettings settings = new JsonSerializerSettings(); + settings.Formatting = indented ? Formatting.Indented : Formatting.None; + settings.Converters.Add(converter); - JsonSerializer serializer = JsonSerializer.Create(settings); + JsonSerializer serializer = JsonSerializer.Create(settings); - converter.WriteJson(writer, value, serializer); + converter.WriteJson(writer, value, serializer); - writer.Flush(); + writer.Flush(); - return sb.ToString(); - } + return sb.ToString(); } } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/Controllers/EndpointRouteInfo.cs b/test/Microsoft.AspNetCore.OData.TestCommon/Controllers/EndpointRouteInfo.cs index 8ba7ef32a..07308f0c3 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/Controllers/EndpointRouteInfo.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/Controllers/EndpointRouteInfo.cs @@ -7,18 +7,17 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +public class EndpointRouteInfo { - public class EndpointRouteInfo - { - public string ControllerFullName { get; set; } + public string ControllerFullName { get; set; } - public string ActionFullName { get; set; } + public string ActionFullName { get; set; } - public string HttpMethods { get; set; } + public string HttpMethods { get; set; } - public string Template { get; set; } + public string Template { get; set; } - public bool IsODataRoute { get; set; } - } + public bool IsODataRoute { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/Controllers/ODataEndpointController.cs b/test/Microsoft.AspNetCore.OData.TestCommon/Controllers/ODataEndpointController.cs index 374222730..cfa50d57e 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/Controllers/ODataEndpointController.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/Controllers/ODataEndpointController.cs @@ -14,107 +14,106 @@ using Microsoft.AspNetCore.OData.Routing; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// If want to see the templates, add this controller into controller feature: +/// services.ConfigureControllers(typeof(ODataEndpointController)); +/// +public class ODataEndpointController : ControllerBase { - /// - /// If want to see the templates, add this controller into controller feature: - /// services.ConfigureControllers(typeof(ODataEndpointController)); - /// - public class ODataEndpointController : ControllerBase + /* + [Fact] + public async Task TestRoutes() { - /* - [Fact] - public async Task TestRoutes() - { - // Arrange - string requestUri = "$odata"; - HttpClient client = CreateClient(); + // Arrange + string requestUri = "$odata"; + HttpClient client = CreateClient(); - // Act - var response = await client.GetAsync(requestUri); + // Act + var response = await client.GetAsync(requestUri); - // Assert - response.EnsureSuccessStatusCode(); - string payload = await response.Content.ReadAsStringAsync(); - } - */ - private EndpointDataSource _dataSource; + // Assert + response.EnsureSuccessStatusCode(); + string payload = await response.Content.ReadAsStringAsync(); + } + */ + private EndpointDataSource _dataSource; + + public ODataEndpointController(EndpointDataSource dataSource) + { + _dataSource = dataSource; + } - public ODataEndpointController(EndpointDataSource dataSource) + [HttpGet("$odata")] + public IEnumerable GetAllRoutes() + { + if (_dataSource == null) { - _dataSource = dataSource; + return Enumerable.Empty(); } - [HttpGet("$odata")] - public IEnumerable GetAllRoutes() + IList routeInfos = new List(); + foreach (var endpoint in _dataSource.Endpoints) { - if (_dataSource == null) + RouteEndpoint routeEndpoint = endpoint as RouteEndpoint; + if (routeEndpoint == null) { - return Enumerable.Empty(); + continue; } - IList routeInfos = new List(); - foreach (var endpoint in _dataSource.Endpoints) + ControllerActionDescriptor controllerActionDescriptor = endpoint.Metadata.GetMetadata(); + if (controllerActionDescriptor == null) { - RouteEndpoint routeEndpoint = endpoint as RouteEndpoint; - if (routeEndpoint == null) - { - continue; - } - - ControllerActionDescriptor controllerActionDescriptor = endpoint.Metadata.GetMetadata(); - if (controllerActionDescriptor == null) - { - continue; - } - - EndpointRouteInfo routeInfo = new EndpointRouteInfo(); - - StringBuilder action = new StringBuilder(); - if (controllerActionDescriptor.MethodInfo.ReturnType != null) - { - action.Append(controllerActionDescriptor.MethodInfo.ReturnType.Name + " "); - } - else - { - action.Append("void "); - } - action.Append(controllerActionDescriptor.MethodInfo.Name + "("); - action.Append(string.Join(",", controllerActionDescriptor.MethodInfo.GetParameters().Select(p => p.ParameterType.Name))); - action.Append(")"); - - routeInfo.ControllerFullName = controllerActionDescriptor.ControllerTypeInfo.FullName; - routeInfo.ActionFullName = action.ToString(); - routeInfo.Template = routeEndpoint.RoutePattern.RawText; - - var httpMethods = GetHttpMethods(endpoint); - routeInfo.HttpMethods = string.Join(",", httpMethods); - - IODataRoutingMetadata metadata = endpoint.Metadata.GetMetadata(); - if (metadata == null) - { - routeInfo.IsODataRoute = false; - } - else - { - routeInfo.IsODataRoute = true; - } - - routeInfos.Add(routeInfo); + continue; } - return routeInfos; - } + EndpointRouteInfo routeInfo = new EndpointRouteInfo(); - private static IEnumerable GetHttpMethods(Endpoint endpoint) - { - HttpMethodMetadata metadata = endpoint.Metadata.GetMetadata(); - if (metadata != null) + StringBuilder action = new StringBuilder(); + if (controllerActionDescriptor.MethodInfo.ReturnType != null) + { + action.Append(controllerActionDescriptor.MethodInfo.ReturnType.Name + " "); + } + else + { + action.Append("void "); + } + action.Append(controllerActionDescriptor.MethodInfo.Name + "("); + action.Append(string.Join(",", controllerActionDescriptor.MethodInfo.GetParameters().Select(p => p.ParameterType.Name))); + action.Append(")"); + + routeInfo.ControllerFullName = controllerActionDescriptor.ControllerTypeInfo.FullName; + routeInfo.ActionFullName = action.ToString(); + routeInfo.Template = routeEndpoint.RoutePattern.RawText; + + var httpMethods = GetHttpMethods(endpoint); + routeInfo.HttpMethods = string.Join(",", httpMethods); + + IODataRoutingMetadata metadata = endpoint.Metadata.GetMetadata(); + if (metadata == null) { - return metadata.HttpMethods; + routeInfo.IsODataRoute = false; + } + else + { + routeInfo.IsODataRoute = true; } - return new[] { "No HttpMethod" }; + routeInfos.Add(routeInfo); } + + return routeInfos; + } + + private static IEnumerable GetHttpMethods(Endpoint endpoint) + { + HttpMethodMetadata metadata = endpoint.Metadata.GetMetadata(); + if (metadata != null) + { + return metadata.HttpMethods; + } + + return new[] { "No HttpMethod" }; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/HttpClientExtensions.cs b/test/Microsoft.AspNetCore.OData.TestCommon/HttpClientExtensions.cs index 4c0f7dae8..fc47366c9 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/HttpClientExtensions.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/HttpClientExtensions.cs @@ -13,201 +13,200 @@ using System.Threading.Tasks; using Newtonsoft.Json; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +public static class HttpClientExtensions { - public static class HttpClientExtensions + /// + /// Sends a Patch request as an asynchronous operation to the specified Uri with the given serialized + /// as JSON. + /// + /// + /// This method uses a default instance of . + /// + /// The client used to make the request. + /// The Uri the request is sent to. + /// The value that will be placed in the request's entity body. + /// A task object representing the asynchronous operation. + public static Task PatchAsync(this HttpClient client, Uri requestUri, string content) + { + return client.PatchAsync(requestUri, content, CancellationToken.None); + } + + /// + /// Sends a Patch request as an asynchronous operation to the specified Uri with the given serialized + /// as JSON. + /// + /// + /// This method uses a default instance of . + /// + /// The client used to make the request. + /// The Uri the request is sent to. + /// The value that will be placed in the request's entity body. + /// A task object representing the asynchronous operation. + public static Task PatchAsync(this HttpClient client, string requestUri, string content) + { + return client.PatchAsync(new Uri(requestUri, UriKind.RelativeOrAbsolute), content, CancellationToken.None); + } + + /// + /// Sends a Patch request as an asynchronous operation to the specified Uri with the given serialized + /// as JSON. + /// + /// + /// This method uses a default instance of . + /// + /// The type of . + /// The client used to make the request. + /// The Uri the request is sent to. + /// The value that will be placed in the request's entity body. + /// A task object representing the asynchronous operation. + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")] + public static Task PatchAsJsonAsync(this HttpClient client, string requestUri, T value) + { + return client.PatchAsJsonAsync(new Uri(requestUri, UriKind.RelativeOrAbsolute), value, CancellationToken.None); + } + + /// + /// Sends a Patch request as an asynchronous operation to the specified Uri with the given serialized + /// as JSON. + /// + /// + /// This method uses a default instance of . + /// + /// The type of . + /// The client used to make the request. + /// The Uri the request is sent to. + /// The value that will be placed in the request's entity body. + /// A task object representing the asynchronous operation. + public static Task PatchAsJsonAsync(this HttpClient client, Uri requestUri, T value) + { + return client.PatchAsJsonAsync(requestUri, value, CancellationToken.None); + } + + /// + /// Sends a Patch request as an asynchronous operation to the specified Uri with the given serialized + /// as JSON. + /// + /// + /// This method uses a default instance of . + /// + /// The type of . + /// The client used to make the request. + /// The Uri the request is sent to. + /// The value that will be placed in the request's entity body. + /// The token to monitor for cancellation requests. + /// A task object representing the asynchronous operation. + public static Task PatchAsJsonAsync(this HttpClient client, Uri requestUri, T value, CancellationToken cancellationToken) + { + string content = JsonConvert.SerializeObject(value); + return client.PatchAsync(requestUri, content, cancellationToken); + } + + /// + /// Sends a Patch request as an asynchronous operation to the specified Uri with the given serialized + /// as JSON. + /// + /// + /// This method uses a default instance of . + /// + /// The type of . + /// The client used to make the request. + /// The Uri the request is sent to. + /// The value that will be placed in the request's entity body. + /// The token to monitor for cancellation requests. + /// A task object representing the asynchronous operation. + public static Task PatchAsync(this HttpClient client, Uri requestUri, string content, CancellationToken cancellationToken) + { + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); + request.Content = new StringContent(content); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + return client.SendAsync(request, cancellationToken); + } + + /// + /// Sends a POST request as an asynchronous operation to the specified Uri with the given serialized + /// as JSON. + /// + /// The type of . + /// The client used to make the request. + /// The Uri the request is sent to. + /// The value that will be placed in the request's entity body. + /// A task object representing the asynchronous operation. + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")] + public static Task PostAsJsonAsync(this HttpClient client, string requestUri, T value) + { + return client.PostAsJsonAsync(new Uri(requestUri, UriKind.RelativeOrAbsolute), value); + } + + /// + /// Sends a POST request as an asynchronous operation to the specified Uri with the given serialized + /// as JSON. + /// + /// The type of . + /// The client used to make the request. + /// The Uri the request is sent to. + /// The value that will be placed in the request's entity body. + /// A task object representing the asynchronous operation. + public static Task PostAsJsonAsync(this HttpClient client, Uri requestUri, T value) + { + string content = JsonConvert.SerializeObject(value); + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("POST"), requestUri); + request.Content = new StringContent(content); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = content.Length; + return client.SendAsync(request, CancellationToken.None); + } + + /// + /// Sends a PUT request as an asynchronous operation to the specified Uri with the given serialized + /// as JSON. + /// + /// The type of . + /// The client used to make the request. + /// The Uri the request is sent to. + /// The value that will be placed in the request's entity body. + /// A task object representing the asynchronous operation. + [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")] + public static Task PutAsJsonAsync(this HttpClient client, string requestUri, T value) + { + return client.PutAsJsonAsync(new Uri(requestUri, UriKind.RelativeOrAbsolute), value); + } + + /// + /// Sends a PUT request as an asynchronous operation to the specified Uri with the given serialized + /// as JSON. + /// + /// The type of . + /// The client used to make the request. + /// The Uri the request is sent to. + /// The value that will be placed in the request's entity body. + /// A task object representing the asynchronous operation. + public static Task PutAsJsonAsync(this HttpClient client, Uri requestUri, T value) { - /// - /// Sends a Patch request as an asynchronous operation to the specified Uri with the given serialized - /// as JSON. - /// - /// - /// This method uses a default instance of . - /// - /// The client used to make the request. - /// The Uri the request is sent to. - /// The value that will be placed in the request's entity body. - /// A task object representing the asynchronous operation. - public static Task PatchAsync(this HttpClient client, Uri requestUri, string content) - { - return client.PatchAsync(requestUri, content, CancellationToken.None); - } - - /// - /// Sends a Patch request as an asynchronous operation to the specified Uri with the given serialized - /// as JSON. - /// - /// - /// This method uses a default instance of . - /// - /// The client used to make the request. - /// The Uri the request is sent to. - /// The value that will be placed in the request's entity body. - /// A task object representing the asynchronous operation. - public static Task PatchAsync(this HttpClient client, string requestUri, string content) - { - return client.PatchAsync(new Uri(requestUri, UriKind.RelativeOrAbsolute), content, CancellationToken.None); - } - - /// - /// Sends a Patch request as an asynchronous operation to the specified Uri with the given serialized - /// as JSON. - /// - /// - /// This method uses a default instance of . - /// - /// The type of . - /// The client used to make the request. - /// The Uri the request is sent to. - /// The value that will be placed in the request's entity body. - /// A task object representing the asynchronous operation. - [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")] - public static Task PatchAsJsonAsync(this HttpClient client, string requestUri, T value) - { - return client.PatchAsJsonAsync(new Uri(requestUri, UriKind.RelativeOrAbsolute), value, CancellationToken.None); - } - - /// - /// Sends a Patch request as an asynchronous operation to the specified Uri with the given serialized - /// as JSON. - /// - /// - /// This method uses a default instance of . - /// - /// The type of . - /// The client used to make the request. - /// The Uri the request is sent to. - /// The value that will be placed in the request's entity body. - /// A task object representing the asynchronous operation. - public static Task PatchAsJsonAsync(this HttpClient client, Uri requestUri, T value) - { - return client.PatchAsJsonAsync(requestUri, value, CancellationToken.None); - } - - /// - /// Sends a Patch request as an asynchronous operation to the specified Uri with the given serialized - /// as JSON. - /// - /// - /// This method uses a default instance of . - /// - /// The type of . - /// The client used to make the request. - /// The Uri the request is sent to. - /// The value that will be placed in the request's entity body. - /// The token to monitor for cancellation requests. - /// A task object representing the asynchronous operation. - public static Task PatchAsJsonAsync(this HttpClient client, Uri requestUri, T value, CancellationToken cancellationToken) - { - string content = JsonConvert.SerializeObject(value); - return client.PatchAsync(requestUri, content, cancellationToken); - } - - /// - /// Sends a Patch request as an asynchronous operation to the specified Uri with the given serialized - /// as JSON. - /// - /// - /// This method uses a default instance of . - /// - /// The type of . - /// The client used to make the request. - /// The Uri the request is sent to. - /// The value that will be placed in the request's entity body. - /// The token to monitor for cancellation requests. - /// A task object representing the asynchronous operation. - public static Task PatchAsync(this HttpClient client, Uri requestUri, string content, CancellationToken cancellationToken) - { - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUri); - request.Content = new StringContent(content); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - return client.SendAsync(request, cancellationToken); - } - - /// - /// Sends a POST request as an asynchronous operation to the specified Uri with the given serialized - /// as JSON. - /// - /// The type of . - /// The client used to make the request. - /// The Uri the request is sent to. - /// The value that will be placed in the request's entity body. - /// A task object representing the asynchronous operation. - [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")] - public static Task PostAsJsonAsync(this HttpClient client, string requestUri, T value) - { - return client.PostAsJsonAsync(new Uri(requestUri, UriKind.RelativeOrAbsolute), value); - } - - /// - /// Sends a POST request as an asynchronous operation to the specified Uri with the given serialized - /// as JSON. - /// - /// The type of . - /// The client used to make the request. - /// The Uri the request is sent to. - /// The value that will be placed in the request's entity body. - /// A task object representing the asynchronous operation. - public static Task PostAsJsonAsync(this HttpClient client, Uri requestUri, T value) - { - string content = JsonConvert.SerializeObject(value); - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("POST"), requestUri); - request.Content = new StringContent(content); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = content.Length; - return client.SendAsync(request, CancellationToken.None); - } - - /// - /// Sends a PUT request as an asynchronous operation to the specified Uri with the given serialized - /// as JSON. - /// - /// The type of . - /// The client used to make the request. - /// The Uri the request is sent to. - /// The value that will be placed in the request's entity body. - /// A task object representing the asynchronous operation. - [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "We want to support URIs as strings")] - public static Task PutAsJsonAsync(this HttpClient client, string requestUri, T value) - { - return client.PutAsJsonAsync(new Uri(requestUri, UriKind.RelativeOrAbsolute), value); - } - - /// - /// Sends a PUT request as an asynchronous operation to the specified Uri with the given serialized - /// as JSON. - /// - /// The type of . - /// The client used to make the request. - /// The Uri the request is sent to. - /// The value that will be placed in the request's entity body. - /// A task object representing the asynchronous operation. - public static Task PutAsJsonAsync(this HttpClient client, Uri requestUri, T value) - { - string content = JsonConvert.SerializeObject(value); - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PUT"), requestUri); - request.Content = new StringContent(content); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - request.Content.Headers.ContentLength = content.Length; - return client.SendAsync(request, CancellationToken.None); - } - - public static async Task GetWithAcceptAsync(this HttpClient client, string uri, string acceptHeader) - { - var request = new HttpRequestMessage(HttpMethod.Get, uri); - request.Headers.Clear(); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptHeader)); - - return await client.SendAsync(request); - } - - public static async Task GetWithAcceptAsync(this HttpClient client, Uri uri, string acceptHeader) - { - var request = new HttpRequestMessage(HttpMethod.Get, uri); - request.Headers.Clear(); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptHeader)); - - return await client.SendAsync(request); - } + string content = JsonConvert.SerializeObject(value); + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PUT"), requestUri); + request.Content = new StringContent(content); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = content.Length; + return client.SendAsync(request, CancellationToken.None); + } + + public static async Task GetWithAcceptAsync(this HttpClient client, string uri, string acceptHeader) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + request.Headers.Clear(); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptHeader)); + + return await client.SendAsync(request); + } + + public static async Task GetWithAcceptAsync(this HttpClient client, Uri uri, string acceptHeader) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + request.Headers.Clear(); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptHeader)); + + return await client.SendAsync(request); } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/HttpContentExtensions.cs b/test/Microsoft.AspNetCore.OData.TestCommon/HttpContentExtensions.cs index 4404f8647..b1c96452d 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/HttpContentExtensions.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/HttpContentExtensions.cs @@ -14,275 +14,274 @@ using Microsoft.AspNetCore.OData.TestCommon.Values; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// Extension methods for +/// +public static class HttpContentExtensions { /// - /// Extension methods for + /// Read the as OData resource set. /// - public static class HttpContentExtensions + /// The http content. + /// The OData array. + public static async Task ReadAsODataArrayAsync(this HttpContent content) { - /// - /// Read the as OData resource set. - /// - /// The http content. - /// The OData array. - public static async Task ReadAsODataArrayAsync(this HttpContent content) + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + Stream stream = await content.ReadAsStreamAsync(); + using (JsonDocument doc = await JsonDocument.ParseAsync(stream)) { - if (content == null) + JsonElement root = doc.RootElement; + + // OData array is an object as: + // { + // "@odata.context":... + // "value" [ ... ] + // } + if (root.ValueKind != JsonValueKind.Object) { - throw new ArgumentNullException(nameof(content)); + return null; } - Stream stream = await content.ReadAsStreamAsync(); - using (JsonDocument doc = await JsonDocument.ParseAsync(stream)) + if (!root.TryGetProperty("value", out JsonElement value) && + value.ValueKind != JsonValueKind.Array) { - JsonElement root = doc.RootElement; - - // OData array is an object as: - // { - // "@odata.context":... - // "value" [ ... ] - // } - if (root.ValueKind != JsonValueKind.Object) - { - return null; - } + return null; + } - if (!root.TryGetProperty("value", out JsonElement value) && - value.ValueKind != JsonValueKind.Array) - { - return null; - } + ODataArray odataArray = new ODataArray(); - ODataArray odataArray = new ODataArray(); + // value array + foreach (var item in value.EnumerateArray()) + { + IODataValue itemValue = item.ParseAsODataValue(); + odataArray.Add(itemValue); + } - // value array - foreach (var item in value.EnumerateArray()) - { - IODataValue itemValue = item.ParseAsODataValue(); - odataArray.Add(itemValue); - } + // context uri + odataArray.ContextUri = ReadStringPropertyValue(root, "@odata.context", "@context"); - // context uri - odataArray.ContextUri = ReadStringPropertyValue(root, "@odata.context", "@context"); + // next link + odataArray.NextLink = ReadStringPropertyValue(root, "@odata.nextlink", "@nextlink"); - // next link - odataArray.NextLink = ReadStringPropertyValue(root, "@odata.nextlink", "@nextlink"); + // odata.count + odataArray.TotalCount = ReadIntPropertyValue(root, "@odata.count", "@count"); - // odata.count - odataArray.TotalCount = ReadIntPropertyValue(root, "@odata.count", "@count"); + return odataArray; + } + } - return odataArray; - } + /// + /// Read the as OData resource. + /// + /// The http content. + /// The OData object. + public static async Task ReadAsODataObjectAsync(this HttpContent content) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); } - /// - /// Read the as OData resource. - /// - /// The http content. - /// The OData object. - public static async Task ReadAsODataObjectAsync(this HttpContent content) + Stream stream = await content.ReadAsStreamAsync(); + using (JsonDocument doc = await JsonDocument.ParseAsync(stream)) { - if (content == null) + JsonElement root = doc.RootElement; + + // OData object is an object as: + // { + // "@odata.context":... + // "Id": 2, + // "Name: "Sam" + // } + if (root.ValueKind != JsonValueKind.Object) { - throw new ArgumentNullException(nameof(content)); + return null; } - Stream stream = await content.ReadAsStreamAsync(); - using (JsonDocument doc = await JsonDocument.ParseAsync(stream)) + ODataObject odataObject = new ODataObject(); + + // value array + foreach (JsonProperty property in root.EnumerateObject()) { - JsonElement root = doc.RootElement; - - // OData object is an object as: - // { - // "@odata.context":... - // "Id": 2, - // "Name: "Sam" - // } - if (root.ValueKind != JsonValueKind.Object) + if (property.Name == "@odata.context" || property.Name == "@context") { - return null; + // context uri + odataObject.ContextUri = property.Value.GetString(); } - - ODataObject odataObject = new ODataObject(); - - // value array - foreach (JsonProperty property in root.EnumerateObject()) + else { - if (property.Name == "@odata.context" || property.Name == "@context") - { - // context uri - odataObject.ContextUri = property.Value.GetString(); - } - else - { - IODataValue itemValue = property.Value.ParseAsODataValue(); - odataObject[property.Name] = itemValue; - } + IODataValue itemValue = property.Value.ParseAsODataValue(); + odataObject[property.Name] = itemValue; } - - return odataObject; } + + return odataObject; } + } - /// - /// Read the as OData value. - /// - /// The http content. - /// The OData value. - public static IODataValue ReadAsOData(this HttpContent content) + /// + /// Read the as OData value. + /// + /// The http content. + /// The OData value. + public static IODataValue ReadAsOData(this HttpContent content) + { + if (content == null) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } + throw new ArgumentNullException(nameof(content)); + } - Stream stream = content.ReadAsStreamAsync().Result; - using (JsonDocument doc = JsonDocument.Parse(stream)) - { - JsonElement root = doc.RootElement; - return ParseAsODataValue(root); - } + Stream stream = content.ReadAsStreamAsync().Result; + using (JsonDocument doc = JsonDocument.Parse(stream)) + { + JsonElement root = doc.RootElement; + return ParseAsODataValue(root); } + } - /// - /// Read the as OData value. - /// - /// The http content. - /// The OData value. - public static async Task ReadAsODataAsync(this HttpContent content) + /// + /// Read the as OData value. + /// + /// The http content. + /// The OData value. + public static async Task ReadAsODataAsync(this HttpContent content) + { + if (content == null) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } + throw new ArgumentNullException(nameof(content)); + } - Stream stream = await content.ReadAsStreamAsync(); - using (JsonDocument doc = await JsonDocument.ParseAsync(stream)) - { - JsonElement root = doc.RootElement; - return ParseAsODataValue(root); - } + Stream stream = await content.ReadAsStreamAsync(); + using (JsonDocument doc = await JsonDocument.ParseAsync(stream)) + { + JsonElement root = doc.RootElement; + return ParseAsODataValue(root); } + } - /// - /// Read as OData value. - /// - /// The JSON element. - /// The OData value. - public static IODataValue ParseAsODataValue(this JsonElement node) + /// + /// Read as OData value. + /// + /// The JSON element. + /// The OData value. + public static IODataValue ParseAsODataValue(this JsonElement node) + { + switch (node.ValueKind) { - switch (node.ValueKind) - { - case JsonValueKind.Object: // A JSON object. - ODataObject odataObject = new ODataObject(); - foreach (JsonProperty property in node.EnumerateObject()) - { - odataObject[property.Name] = ParseAsODataValue(property.Value); - } + case JsonValueKind.Object: // A JSON object. + ODataObject odataObject = new ODataObject(); + foreach (JsonProperty property in node.EnumerateObject()) + { + odataObject[property.Name] = ParseAsODataValue(property.Value); + } - return odataObject; + return odataObject; - case JsonValueKind.Array: // A JSON array. - ODataArray odataArray = new ODataArray(); - foreach (JsonElement element in node.EnumerateArray()) - { - odataArray.Add(ParseAsODataValue(element)); - } + case JsonValueKind.Array: // A JSON array. + ODataArray odataArray = new ODataArray(); + foreach (JsonElement element in node.EnumerateArray()) + { + odataArray.Add(ParseAsODataValue(element)); + } - return odataArray; + return odataArray; - case JsonValueKind.String: // A JSON string. - return new ODataString(node.GetString()); + case JsonValueKind.String: // A JSON string. + return new ODataString(node.GetString()); - case JsonValueKind.Number: // A JSON number. - return ReadNumber(node); + case JsonValueKind.Number: // A JSON number. + return ReadNumber(node); - case JsonValueKind.True: // A JSON true. - return ODataBoolean.True; + case JsonValueKind.True: // A JSON true. + return ODataBoolean.True; - case JsonValueKind.False: // A JSON false. - return ODataBoolean.False; + case JsonValueKind.False: // A JSON false. + return ODataBoolean.False; - case JsonValueKind.Null: // A JSON null. - return ODataNull.Null; + case JsonValueKind.Null: // A JSON null. + return ODataNull.Null; - case JsonValueKind.Undefined: - default: - throw new ODataException($"Found an Undefined JSON element '{node.GetRawText()}'!"); - } + case JsonValueKind.Undefined: + default: + throw new ODataException($"Found an Undefined JSON element '{node.GetRawText()}'!"); } + } + + /// + /// Read the JSON node as a number + /// + /// The JSON element. + /// OData number. + private static ODataNumber ReadNumber(JsonElement node) + { + Contract.Assert(JsonValueKind.Number == node.ValueKind); - /// - /// Read the JSON node as a number - /// - /// The JSON element. - /// OData number. - private static ODataNumber ReadNumber(JsonElement node) + if (node.TryGetInt32(out int int32Value)) { - Contract.Assert(JsonValueKind.Number == node.ValueKind); + return new ODataInt(int32Value); + } - if (node.TryGetInt32(out int int32Value)) - { - return new ODataInt(int32Value); - } + if (node.TryGetInt64(out long int64Value)) + { + return new ODataLong(int64Value); + } - if (node.TryGetInt64(out long int64Value)) - { - return new ODataLong(int64Value); - } + if (node.TryGetDecimal(out decimal decimalValue)) + { + return new ODataDecimal(decimalValue); + } - if (node.TryGetDecimal(out decimal decimalValue)) - { - return new ODataDecimal(decimalValue); - } + if (node.TryGetDouble(out double doubleValue)) + { + return new ODataDouble(doubleValue); + } - if (node.TryGetDouble(out double doubleValue)) - { - return new ODataDouble(doubleValue); - } + throw new ODataException($"Can not read a JSON element '{node.GetRawText()}' as Number!"); + } - throw new ODataException($"Can not read a JSON element '{node.GetRawText()}' as Number!"); - } + private static string ReadStringPropertyValue(JsonElement node, params string[] propertyNames) + { + Contract.Assert(node.ValueKind == JsonValueKind.Object); - private static string ReadStringPropertyValue(JsonElement node, params string[] propertyNames) + foreach (string propertyName in propertyNames) { - Contract.Assert(node.ValueKind == JsonValueKind.Object); - - foreach (string propertyName in propertyNames) + if (node.TryGetProperty(propertyName, out JsonElement propertyValue)) { - if (node.TryGetProperty(propertyName, out JsonElement propertyValue)) + if (propertyValue.ValueKind != JsonValueKind.String) { - if (propertyValue.ValueKind != JsonValueKind.String) - { - throw new ODataException($"Found a non-string JSON element '{node.GetRawText()}'!"); - } - - return propertyValue.GetString(); + throw new ODataException($"Found a non-string JSON element '{node.GetRawText()}'!"); } - } - return null; + return propertyValue.GetString(); + } } - private static int? ReadIntPropertyValue(JsonElement node, params string[] propertyNames) - { - Contract.Assert(node.ValueKind == JsonValueKind.Object); + return null; + } + + private static int? ReadIntPropertyValue(JsonElement node, params string[] propertyNames) + { + Contract.Assert(node.ValueKind == JsonValueKind.Object); - foreach (string propertyName in propertyNames) + foreach (string propertyName in propertyNames) + { + if (node.TryGetProperty(propertyName, out JsonElement propertyValue)) { - if (node.TryGetProperty(propertyName, out JsonElement propertyValue)) + if (propertyValue.ValueKind != JsonValueKind.Number) { - if (propertyValue.ValueKind != JsonValueKind.Number) - { - throw new ODataException($"Found a non-number JSON element '{node.GetRawText()}'!"); - } - - return propertyValue.GetInt32(); + throw new ODataException($"Found a non-number JSON element '{node.GetRawText()}'!"); } - } - return null; + return propertyValue.GetInt32(); + } } + + return null; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/JsonAssert.cs b/test/Microsoft.AspNetCore.OData.TestCommon/JsonAssert.cs index 5119a3e3f..14add9bf4 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/JsonAssert.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/JsonAssert.cs @@ -12,60 +12,59 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +public class JsonAssert { - public class JsonAssert + public static void PropertyEquals(T expectedValue, string propertyName, TJobject jsonObject) where TJobject : JObject { - public static void PropertyEquals(T expectedValue, string propertyName, TJobject jsonObject) where TJobject : JObject - { - Func, string> selector = x => x.Key; - IEnumerable keys = Enumerable.Select, string>(jsonObject, selector); - Assert.Contains(propertyName, keys); - Assert.Equal(expectedValue, (T)(dynamic)jsonObject[propertyName]); - } + Func, string> selector = x => x.Key; + IEnumerable keys = Enumerable.Select, string>(jsonObject, selector); + Assert.Contains(propertyName, keys); + Assert.Equal(expectedValue, (T)(dynamic)jsonObject[propertyName]); + } - public static void ArrayLength(int expected, string jsonProperty, JObject actual) - { - Func, string> selector = x => x.Key; - IEnumerable keys = Enumerable.Select, string>(actual, selector); - Assert.Contains(jsonProperty, keys); - Assert.Equal(expected, ((JArray)actual[jsonProperty]).Count); - } + public static void ArrayLength(int expected, string jsonProperty, JObject actual) + { + Func, string> selector = x => x.Key; + IEnumerable keys = Enumerable.Select, string>(actual, selector); + Assert.Contains(jsonProperty, keys); + Assert.Equal(expected, ((JArray)actual[jsonProperty]).Count); + } - public static void DoesNotContainProperty(string expected, JObject actual) - { - Func, string> selector = x => x.Key; - IEnumerable keys = Enumerable.Select, string>(actual, selector); - Assert.DoesNotContain(expected, keys, new ContainsEqualityComparer()); - } + public static void DoesNotContainProperty(string expected, JObject actual) + { + Func, string> selector = x => x.Key; + IEnumerable keys = Enumerable.Select, string>(actual, selector); + Assert.DoesNotContain(expected, keys, new ContainsEqualityComparer()); + } - public static void ContainsProperty(string expected, JObject actual) - { - Func, string> selector = x => x.Key; - IEnumerable keys = Enumerable.Select, string>(actual, selector); - Assert.Contains(expected, keys, new ContainsEqualityComparer()); - } + public static void ContainsProperty(string expected, JObject actual) + { + Func, string> selector = x => x.Key; + IEnumerable keys = Enumerable.Select, string>(actual, selector); + Assert.Contains(expected, keys, new ContainsEqualityComparer()); + } - private class ContainsEqualityComparer : IEqualityComparer + private class ContainsEqualityComparer : IEqualityComparer + { + public bool Equals(string x, string y) { - public bool Equals(string x, string y) + if (x == null) { - if (x == null) - { - throw new ArgumentNullException("x"); - } - if (y == null) - { - throw new ArgumentNullException("y"); - } - return Regex.IsMatch(y, x); + throw new ArgumentNullException("x"); } - - public int GetHashCode(string obj) + if (y == null) { - throw new InvalidOperationException("Not supported on this comparer"); + throw new ArgumentNullException("y"); } + return Regex.IsMatch(y, x); + } + + public int GetHashCode(string obj) + { + throw new InvalidOperationException("Not supported on this comparer"); } } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/MockAssembliesResolverFactory.cs b/test/Microsoft.AspNetCore.OData.TestCommon/MockAssembliesResolverFactory.cs index fb107c201..285662b5f 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/MockAssembliesResolverFactory.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/MockAssembliesResolverFactory.cs @@ -9,35 +9,34 @@ using Microsoft.OData.ModelBuilder; using Moq; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// A mock to represent an assembly resolver +/// +public class MockAssembliesResolverFactory { /// - /// A mock to represent an assembly resolver + /// Initializes a new instance of the routing configuration class. /// - public class MockAssembliesResolverFactory + /// A new instance of the routing configuration class. + public static IAssemblyResolver Create(MockAssembly assembly = null) { - /// - /// Initializes a new instance of the routing configuration class. - /// - /// A new instance of the routing configuration class. - public static IAssemblyResolver Create(MockAssembly assembly = null) + IAssemblyResolver resolver = null; + if (assembly != null) { - IAssemblyResolver resolver = null; - if (assembly != null) - { - resolver = new TestAssemblyResolver(assembly); - } - else - { - Mock mockAssembliesResolver = new Mock(); - mockAssembliesResolver - .Setup(r => r.Assemblies) - .Returns(new Assembly[0]); - - resolver = mockAssembliesResolver.Object; - } + resolver = new TestAssemblyResolver(assembly); + } + else + { + Mock mockAssembliesResolver = new Mock(); + mockAssembliesResolver + .Setup(r => r.Assemblies) + .Returns(new Assembly[0]); - return resolver; + resolver = mockAssembliesResolver.Object; } + + return resolver; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/MockAssembly.cs b/test/Microsoft.AspNetCore.OData.TestCommon/MockAssembly.cs index ed1e62149..b8197c705 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/MockAssembly.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/MockAssembly.cs @@ -10,52 +10,51 @@ using System.Linq; using System.Reflection; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// A mock to represent an assembly +/// +public sealed class MockAssembly : Assembly { + private Type[] _types; + /// - /// A mock to represent an assembly + /// Initializes a new instance of the class. /// - public sealed class MockAssembly : Assembly + /// The types in this assembly. + public MockAssembly(params Type[] types) { - private Type[] _types; + _types = types; + } - /// - /// Initializes a new instance of the class. - /// - /// The types in this assembly. - public MockAssembly(params Type[] types) + /// + /// Initializes a new instance of the class. + /// + /// The mock types in this assembly. + public MockAssembly(params MockType[] types) + { + foreach (var type in types) { - _types = types; + type.SetupGet(t => t.Assembly).Returns(this); } - /// - /// Initializes a new instance of the class. - /// - /// The mock types in this assembly. - public MockAssembly(params MockType[] types) - { - foreach (var type in types) - { - type.SetupGet(t => t.Assembly).Returns(this); - } - - _types = types.Select(t => t.Object).ToArray(); - } + _types = types.Select(t => t.Object).ToArray(); + } - /// - /// AspNet uses GetTypes as opposed to DefinedTypes() - /// - public override Type[] GetTypes() - { - return _types; - } + /// + /// AspNet uses GetTypes as opposed to DefinedTypes() + /// + public override Type[] GetTypes() + { + return _types; + } - /// - /// AspNetCore uses DefinedTypes as opposed to GetTypes() - /// - public override IEnumerable DefinedTypes - { - get { return _types.AsEnumerable().Select(a => a.GetTypeInfo()); } - } + /// + /// AspNetCore uses DefinedTypes as opposed to GetTypes() + /// + public override IEnumerable DefinedTypes + { + get { return _types.AsEnumerable().Select(a => a.GetTypeInfo()); } } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/MockPropertyInfo.cs b/test/Microsoft.AspNetCore.OData.TestCommon/MockPropertyInfo.cs index 450aa9995..3d323bf18 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/MockPropertyInfo.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/MockPropertyInfo.cs @@ -9,63 +9,62 @@ using System.Reflection; using Moq; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// A mock to represent a property info. +/// +public sealed class MockPropertyInfo : Mock { + private readonly Mock _mockGetMethod = new Mock(); + private readonly Mock _mockSetMethod = new Mock(); + /// - /// A mock to represent a property info. + /// Initializes a new instance of the class. /// - public sealed class MockPropertyInfo : Mock + public MockPropertyInfo() + : this(typeof(object), "P") { - private readonly Mock _mockGetMethod = new Mock(); - private readonly Mock _mockSetMethod = new Mock(); - - /// - /// Initializes a new instance of the class. - /// - public MockPropertyInfo() - : this(typeof(object), "P") - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The property type. - /// The property name. - public MockPropertyInfo(Type propertyType, string propertyName) - { - SetupGet(p => p.DeclaringType).Returns(typeof(object)); - SetupGet(p => p.ReflectedType).Returns(typeof(object)); - SetupGet(p => p.Name).Returns(propertyName); - SetupGet(p => p.PropertyType).Returns(propertyType); - SetupGet(p => p.CanRead).Returns(true); - SetupGet(p => p.CanWrite).Returns(true); - Setup(p => p.GetGetMethod(It.IsAny())).Returns(_mockGetMethod.Object); - Setup(p => p.GetSetMethod(It.IsAny())).Returns(_mockSetMethod.Object); - Setup(p => p.Equals(It.IsAny())).Returns(p => ReferenceEquals(Object, p)); + /// + /// Initializes a new instance of the class. + /// + /// The property type. + /// The property name. + public MockPropertyInfo(Type propertyType, string propertyName) + { + SetupGet(p => p.DeclaringType).Returns(typeof(object)); + SetupGet(p => p.ReflectedType).Returns(typeof(object)); + SetupGet(p => p.Name).Returns(propertyName); + SetupGet(p => p.PropertyType).Returns(propertyType); + SetupGet(p => p.CanRead).Returns(true); + SetupGet(p => p.CanWrite).Returns(true); + Setup(p => p.GetGetMethod(It.IsAny())).Returns(_mockGetMethod.Object); + Setup(p => p.GetSetMethod(It.IsAny())).Returns(_mockSetMethod.Object); + Setup(p => p.Equals(It.IsAny())).Returns(p => ReferenceEquals(Object, p)); - _mockGetMethod.SetupGet(m => m.Attributes).Returns(MethodAttributes.Public); - } + _mockGetMethod.SetupGet(m => m.Attributes).Returns(MethodAttributes.Public); + } - /// - /// Implicit operator to convert Mock property info to property info. - /// - /// The left mock property info. - public static implicit operator PropertyInfo(MockPropertyInfo mockPropertyInfo) - { - return mockPropertyInfo.Object; - } + /// + /// Implicit operator to convert Mock property info to property info. + /// + /// The left mock property info. + public static implicit operator PropertyInfo(MockPropertyInfo mockPropertyInfo) + { + return mockPropertyInfo.Object; + } - /// - /// Set up the property as abstract. - /// - /// - public MockPropertyInfo Abstract() - { - _mockGetMethod.SetupGet(m => m.Attributes) - .Returns(_mockGetMethod.Object.Attributes | MethodAttributes.Abstract); + /// + /// Set up the property as abstract. + /// + /// + public MockPropertyInfo Abstract() + { + _mockGetMethod.SetupGet(m => m.Attributes) + .Returns(_mockGetMethod.Object.Attributes | MethodAttributes.Abstract); - return this; - } + return this; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/MockServiceProvider.cs b/test/Microsoft.AspNetCore.OData.TestCommon/MockServiceProvider.cs index e22caa6c2..8e21e4b4e 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/MockServiceProvider.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/MockServiceProvider.cs @@ -13,73 +13,72 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// A mock to represent a service provider. +/// +public class MockServiceProvider : IServiceProvider { + private IServiceProvider _serviceProvider; + /// - /// A mock to represent a service provider. + /// Initializes a new instance of the class. /// - public class MockServiceProvider : IServiceProvider + public MockServiceProvider() { - private IServiceProvider _serviceProvider; - - /// - /// Initializes a new instance of the class. - /// - public MockServiceProvider() - { - _serviceProvider = BuilderDefaultServiceProvider(null); - } + _serviceProvider = BuilderDefaultServiceProvider(null); + } - /// - /// Initializes a new instance of the class. - /// - /// The input service provider. - public MockServiceProvider(IServiceProvider sp) - { - _serviceProvider = sp; - } + /// + /// Initializes a new instance of the class. + /// + /// The input service provider. + public MockServiceProvider(IServiceProvider sp) + { + _serviceProvider = sp; + } - /// - /// Initializes a new instance of the class. - /// - /// The Edm model. - public MockServiceProvider(IEdmModel model) - { - _serviceProvider = BuilderDefaultServiceProvider(b => b.AddSingleton(sp => model)); - } + /// + /// Initializes a new instance of the class. + /// + /// The Edm model. + public MockServiceProvider(IEdmModel model) + { + _serviceProvider = BuilderDefaultServiceProvider(b => b.AddSingleton(sp => model)); + } - /// - /// Initializes a new instance of the class. - /// - /// The setup action. - public MockServiceProvider(Action setupAction) - { - _serviceProvider = BuilderDefaultServiceProvider(setupAction); - } + /// + /// Initializes a new instance of the class. + /// + /// The setup action. + public MockServiceProvider(Action setupAction) + { + _serviceProvider = BuilderDefaultServiceProvider(setupAction); + } - public object GetService(Type serviceType) - { - return _serviceProvider?.GetService(serviceType); - } + public object GetService(Type serviceType) + { + return _serviceProvider?.GetService(serviceType); + } - private static IServiceProvider BuilderDefaultServiceProvider(Action setupAction) - { - IServiceCollection odataContainerBuilder = new ServiceCollection(); + private static IServiceProvider BuilderDefaultServiceProvider(Action setupAction) + { + IServiceCollection odataContainerBuilder = new ServiceCollection(); - odataContainerBuilder.AddDefaultODataServices(); + odataContainerBuilder.AddDefaultODataServices(); - odataContainerBuilder.AddSingleton(sp => new DefaultQueryConfigurations()); + odataContainerBuilder.AddSingleton(sp => new DefaultQueryConfigurations()); - odataContainerBuilder.AddSingleton(typeof(ODataUriResolver), - sp => new UnqualifiedODataUriResolver { EnableCaseInsensitive = true }); + odataContainerBuilder.AddSingleton(typeof(ODataUriResolver), + sp => new UnqualifiedODataUriResolver { EnableCaseInsensitive = true }); - // Inject the default Web API OData services. - odataContainerBuilder.AddDefaultWebApiServices(); + // Inject the default Web API OData services. + odataContainerBuilder.AddDefaultWebApiServices(); - // Inject the customized services. - setupAction?.Invoke(odataContainerBuilder); + // Inject the customized services. + setupAction?.Invoke(odataContainerBuilder); - return odataContainerBuilder.BuildServiceProvider(); - } + return odataContainerBuilder.BuildServiceProvider(); } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/MockType.cs b/test/Microsoft.AspNetCore.OData.TestCommon/MockType.cs index 88897b60f..9e131b9aa 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/MockType.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/MockType.cs @@ -12,117 +12,116 @@ using Moq; using Moq.Protected; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// A mock to represent a CLR type. +/// +public sealed class MockType : Mock { + private readonly List _propertyInfos = new List(); + private MockType _baseType; + + /// + /// Initializes a new instance of the class. + /// + public MockType() + : this("T") + { } + /// - /// A mock to represent a CLR type. + /// Initializes a new instance of the class. /// - public sealed class MockType : Mock + /// The type name. + /// Has default constructor. + /// The namespace. + public MockType(string typeName, bool hasDefaultCtor = true, string @namespace = "DefaultNamespace") { - private readonly List _propertyInfos = new List(); - private MockType _baseType; - - /// - /// Initializes a new instance of the class. - /// - public MockType() - : this("T") - { } - - /// - /// Initializes a new instance of the class. - /// - /// The type name. - /// Has default constructor. - /// The namespace. - public MockType(string typeName, bool hasDefaultCtor = true, string @namespace = "DefaultNamespace") - { - SetupGet(t => t.Name).Returns(typeName); - SetupGet(t => t.BaseType).Returns(typeof(Object)); - SetupGet(t => t.Assembly).Returns(typeof(object).Assembly); - Setup(t => t.GetProperties(It.IsAny())) - .Returns(() => _propertyInfos.Union(_baseType != null ? _baseType._propertyInfos : Enumerable.Empty()).Select(p => p.Object).ToArray()); - Setup(t => t.Equals(It.IsAny())).Returns(t => ReferenceEquals(Object, t)); - Setup(t => t.ToString()).Returns(typeName); - Setup(t => t.Namespace).Returns(@namespace); - Setup(t => t.IsAssignableFrom(It.IsAny())).Returns(true); - Setup(t => t.FullName).Returns(@namespace + "." + typeName); - - TypeAttributes(System.Reflection.TypeAttributes.Class | System.Reflection.TypeAttributes.Public); - - - if (hasDefaultCtor) - { - this.Protected() - .Setup( - "GetConstructorImpl", - BindingFlags.Instance | BindingFlags.Public, - ItExpr.IsNull(), - CallingConventions.Standard | CallingConventions.VarArgs, - Type.EmptyTypes, - ItExpr.IsNull()) - .Returns(new Mock().Object); - } - } + SetupGet(t => t.Name).Returns(typeName); + SetupGet(t => t.BaseType).Returns(typeof(Object)); + SetupGet(t => t.Assembly).Returns(typeof(object).Assembly); + Setup(t => t.GetProperties(It.IsAny())) + .Returns(() => _propertyInfos.Union(_baseType != null ? _baseType._propertyInfos : Enumerable.Empty()).Select(p => p.Object).ToArray()); + Setup(t => t.Equals(It.IsAny())).Returns(t => ReferenceEquals(Object, t)); + Setup(t => t.ToString()).Returns(typeName); + Setup(t => t.Namespace).Returns(@namespace); + Setup(t => t.IsAssignableFrom(It.IsAny())).Returns(true); + Setup(t => t.FullName).Returns(@namespace + "." + typeName); + + TypeAttributes(System.Reflection.TypeAttributes.Class | System.Reflection.TypeAttributes.Public); - /// - /// Implicit operator to convert Mock type info to type. - /// - /// The mock type. - public static implicit operator Type(MockType mockType) - { - return mockType.Object; - } - public MockType TypeAttributes(TypeAttributes typeAttributes) + if (hasDefaultCtor) { this.Protected() - .Setup("GetAttributeFlagsImpl") - .Returns(typeAttributes); - - return this; + .Setup( + "GetConstructorImpl", + BindingFlags.Instance | BindingFlags.Public, + ItExpr.IsNull(), + CallingConventions.Standard | CallingConventions.VarArgs, + Type.EmptyTypes, + ItExpr.IsNull()) + .Returns(new Mock().Object); } + } - public MockType BaseType(MockType mockBaseType) - { - _baseType = mockBaseType; - SetupGet(t => t.BaseType).Returns(mockBaseType); - Setup(t => t.IsSubclassOf(mockBaseType)).Returns(true); + /// + /// Implicit operator to convert Mock type info to type. + /// + /// The mock type. + public static implicit operator Type(MockType mockType) + { + return mockType.Object; + } - return this; - } + public MockType TypeAttributes(TypeAttributes typeAttributes) + { + this.Protected() + .Setup("GetAttributeFlagsImpl") + .Returns(typeAttributes); - public MockType Property(string propertyName) - { - Property(typeof(T), propertyName); + return this; + } - return this; - } + public MockType BaseType(MockType mockBaseType) + { + _baseType = mockBaseType; + SetupGet(t => t.BaseType).Returns(mockBaseType); + Setup(t => t.IsSubclassOf(mockBaseType)).Returns(true); - public MockType Property(Type propertyType, string propertyName, params Attribute[] attributes) - { - var mockPropertyInfo = new MockPropertyInfo(propertyType, propertyName); - mockPropertyInfo.SetupGet(p => p.DeclaringType).Returns(this); - mockPropertyInfo.SetupGet(p => p.ReflectedType).Returns(this); - mockPropertyInfo.Setup(p => p.GetCustomAttributes(It.IsAny())).Returns(attributes); + return this; + } - _propertyInfos.Add(mockPropertyInfo); + public MockType Property(string propertyName) + { + Property(typeof(T), propertyName); - return this; - } + return this; + } - public MockPropertyInfo GetProperty(string name) - { - return _propertyInfos.Single(p => p.Object.Name == name); - } + public MockType Property(Type propertyType, string propertyName, params Attribute[] attributes) + { + var mockPropertyInfo = new MockPropertyInfo(propertyType, propertyName); + mockPropertyInfo.SetupGet(p => p.DeclaringType).Returns(this); + mockPropertyInfo.SetupGet(p => p.ReflectedType).Returns(this); + mockPropertyInfo.Setup(p => p.GetCustomAttributes(It.IsAny())).Returns(attributes); - public MockType AsCollection() - { - var mockCollectionType = new MockType(); + _propertyInfos.Add(mockPropertyInfo); - mockCollectionType.Setup(t => t.GetInterfaces()).Returns(new Type[] { typeof(IEnumerable<>).MakeGenericType(this) }); + return this; + } - return mockCollectionType; - } + public MockPropertyInfo GetProperty(string name) + { + return _propertyInfos.Single(p => p.Object.Name == name); + } + + public MockType AsCollection() + { + var mockCollectionType = new MockType(); + + mockCollectionType.Setup(t => t.GetInterfaces()).Returns(new Type[] { typeof(IEnumerable<>).MakeGenericType(this) }); + + return mockCollectionType; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/ODataModelBuilderMocks.cs b/test/Microsoft.AspNetCore.OData.TestCommon/ODataModelBuilderMocks.cs index 9a47fd597..002606a8d 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/ODataModelBuilderMocks.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/ODataModelBuilderMocks.cs @@ -9,27 +9,26 @@ using Microsoft.OData.ModelBuilder; using Moq; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +public static class ODataModelBuilderMocks { - public static class ODataModelBuilderMocks + // Creates a mock of an ODataModelBuilder or any subclass of it that disables model validation + // in order to reduce verbosity on tests. + public static T GetModelBuilderMock() where T : ODataModelBuilder { - // Creates a mock of an ODataModelBuilder or any subclass of it that disables model validation - // in order to reduce verbosity on tests. - public static T GetModelBuilderMock() where T : ODataModelBuilder + Mock mock; + if (typeof(T) == typeof(ODataConventionModelBuilder)) { - Mock mock; - if (typeof(T) == typeof(ODataConventionModelBuilder)) - { - mock = new Mock(); - } - else - { - mock = new Mock(); - } - - mock.Setup(b => b.ValidateModel(It.IsAny())).Callback(() => { }); - mock.CallBase = true; - return mock.Object; + mock = new Mock(); } + else + { + mock = new Mock(); + } + + mock.Setup(b => b.ValidateModel(It.IsAny())).Callback(() => { }); + mock.CallBase = true; + return mock.Object; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/ServiceCollectionExtensions.cs b/test/Microsoft.AspNetCore.OData.TestCommon/ServiceCollectionExtensions.cs index 091f5722b..8936acdc9 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/ServiceCollectionExtensions.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/ServiceCollectionExtensions.cs @@ -8,33 +8,32 @@ using System; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// Extension for . +/// +public static class ServiceCollectionExtensions { /// - /// Extension for . + /// Config the controller provider. /// - public static class ServiceCollectionExtensions + /// The service collection. + /// The configured controllers. + /// The caller. + public static IServiceCollection ConfigureControllers(this IServiceCollection services, params Type[] controllers) { - /// - /// Config the controller provider. - /// - /// The service collection. - /// The configured controllers. - /// The caller. - public static IServiceCollection ConfigureControllers(this IServiceCollection services, params Type[] controllers) + if (services == null) { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } + throw new ArgumentNullException(nameof(services)); + } - services.AddControllers() - .ConfigureApplicationPartManager(pm => - { - pm.FeatureProviders.Add(new WebODataControllerFeatureProvider(controllers)); - }); + services.AddControllers() + .ConfigureApplicationPartManager(pm => + { + pm.FeatureProviders.Add(new WebODataControllerFeatureProvider(controllers)); + }); - return services; - } + return services; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/StringContentWithLength.cs b/test/Microsoft.AspNetCore.OData.TestCommon/StringContentWithLength.cs index 0df0cedd8..b270779a7 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/StringContentWithLength.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/StringContentWithLength.cs @@ -8,39 +8,38 @@ using System.Net.Http; using System.Text; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +public class StringContentWithLength : StringContent { - public class StringContentWithLength : StringContent + public StringContentWithLength(string content) + : base(content) { - public StringContentWithLength(string content) - : base(content) - { - EnsureContentLength(); - } + EnsureContentLength(); + } - public StringContentWithLength(string content, Encoding encoding) - : base(content, encoding) - { - EnsureContentLength(); - } + public StringContentWithLength(string content, Encoding encoding) + : base(content, encoding) + { + EnsureContentLength(); + } - public StringContentWithLength(string content, Encoding encoding, string mediaType) - : base(content, encoding, mediaType) - { - EnsureContentLength(); - } + public StringContentWithLength(string content, Encoding encoding, string mediaType) + : base(content, encoding, mediaType) + { + EnsureContentLength(); + } - public StringContentWithLength(string content, string unvalidatedContentType) - : base(content) - { - Headers.TryAddWithoutValidation("Content-Type", unvalidatedContentType); - EnsureContentLength(); - } + public StringContentWithLength(string content, string unvalidatedContentType) + : base(content) + { + Headers.TryAddWithoutValidation("Content-Type", unvalidatedContentType); + EnsureContentLength(); + } - private void EnsureContentLength() - { - // See: https://github.com/dotnet/aspnetcore/issues/18463 - _ = Headers.ContentLength; - } + private void EnsureContentLength() + { + // See: https://github.com/dotnet/aspnetcore/issues/18463 + _ = Headers.ContentLength; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/TestAssemblyResolver.cs b/test/Microsoft.AspNetCore.OData.TestCommon/TestAssemblyResolver.cs index a260f62e1..a92f171c6 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/TestAssemblyResolver.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/TestAssemblyResolver.cs @@ -10,23 +10,22 @@ using System.Reflection; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.TestCommon -{ - internal class TestAssemblyResolver : IAssemblyResolver - { - List _assemblies; +namespace Microsoft.AspNetCore.OData.TestCommon; - public TestAssemblyResolver(MockAssembly assembly) - { - _assemblies = new List(); - _assemblies.Add(assembly); - } +internal class TestAssemblyResolver : IAssemblyResolver +{ + List _assemblies; - public TestAssemblyResolver(params Type[] types) - : this(new MockAssembly(types)) - { - } + public TestAssemblyResolver(MockAssembly assembly) + { + _assemblies = new List(); + _assemblies.Add(assembly); + } - public IEnumerable Assemblies => _assemblies; + public TestAssemblyResolver(params Type[] types) + : this(new MockAssembly(types)) + { } + + public IEnumerable Assemblies => _assemblies; } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/TestServerUtils.cs b/test/Microsoft.AspNetCore.OData.TestCommon/TestServerUtils.cs index af6947b21..0e8aee6d4 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/TestServerUtils.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/TestServerUtils.cs @@ -11,74 +11,73 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// Utils for the . +/// +public static class TestServerUtils { /// - /// Utils for the . + /// Creates the default TestServer /// - public static class TestServerUtils + /// The created test server. + public static TestServer Create() { - /// - /// Creates the default TestServer - /// - /// The created test server. - public static TestServer Create() - { - var builder = new WebHostBuilder() - .ConfigureServices(services => - { - services.AddControllers(); - }) - .Configure(app => + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddControllers(); + }) + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => { - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); + endpoints.MapControllers(); }); + }); - return new TestServer(builder); - } + return new TestServer(builder); + } - /// - /// Creates the Test server using the config and controller types. - /// - /// The config action. - /// The controller types. - /// The created test server. - public static TestServer Create(Action setupConfig, params Type[] controllers) - { - var builder = new WebHostBuilder() - .ConfigureServices(services => - { - services.ConfigureControllers(controllers); - services.AddControllers() - .AddOData(setupConfig); - }) - .Configure(app => + /// + /// Creates the Test server using the config and controller types. + /// + /// The config action. + /// The controller types. + /// The created test server. + public static TestServer Create(Action setupConfig, params Type[] controllers) + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.ConfigureControllers(controllers); + services.AddControllers() + .AddOData(setupConfig); + }) + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => { - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); + endpoints.MapControllers(); }); + }); - return new TestServer(builder); - } + return new TestServer(builder); + } - /// - /// Creates the Test server using the startup class. - /// - /// The start up class type. - /// The created test server. - public static TestServer Create() where TStartup : class - { - var builder = new WebHostBuilder() - .UseStartup(); + /// + /// Creates the Test server using the startup class. + /// + /// The start up class type. + /// The created test server. + public static TestServer Create() where TStartup : class + { + var builder = new WebHostBuilder() + .UseStartup(); - return new TestServer(builder); - } + return new TestServer(builder); } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/TestStartupBase.cs b/test/Microsoft.AspNetCore.OData.TestCommon/TestStartupBase.cs index 9d002a793..f948230a9 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/TestStartupBase.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/TestStartupBase.cs @@ -9,37 +9,36 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// The startup base class +/// +public class TestStartupBase { - /// - /// The startup base class - /// - public class TestStartupBase + public virtual void ConfigureServices(IServiceCollection services) { - public virtual void ConfigureServices(IServiceCollection services) - { - } - - public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - ConfigureBeforeRouting(app, env); + } - app.UseRouting(); + public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + ConfigureBeforeRouting(app, env); - ConfigureInRouting(app, env); + app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } + ConfigureInRouting(app, env); - protected virtual void ConfigureBeforeRouting(IApplicationBuilder app, IWebHostEnvironment env) + app.UseEndpoints(endpoints => { - } + endpoints.MapControllers(); + }); + } - protected virtual void ConfigureInRouting(IApplicationBuilder app, IWebHostEnvironment env) - { - } + protected virtual void ConfigureBeforeRouting(IApplicationBuilder app, IWebHostEnvironment env) + { + } + + protected virtual void ConfigureInRouting(IApplicationBuilder app, IWebHostEnvironment env) + { } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/TheoryDataSet.cs b/test/Microsoft.AspNetCore.OData.TestCommon/TheoryDataSet.cs index 2a843558f..887eb94ad 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/TheoryDataSet.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/TheoryDataSet.cs @@ -8,28 +8,27 @@ using System.Collections; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// Base class for TheoryDataSet classes. +/// +public abstract class TheoryDataSet : IEnumerable { - /// - /// Base class for TheoryDataSet classes. - /// - public abstract class TheoryDataSet : IEnumerable - { - private readonly List data = new List(); + private readonly List data = new List(); - protected void AddItem(params object[] values) - { - data.Add(values); - } + protected void AddItem(params object[] values) + { + data.Add(values); + } - public IEnumerator GetEnumerator() - { - return data.GetEnumerator(); - } + public IEnumerator GetEnumerator() + { + return data.GetEnumerator(); + } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/TheoryDataSetOfT.cs b/test/Microsoft.AspNetCore.OData.TestCommon/TheoryDataSetOfT.cs index 38b806c51..9acc71a54 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/TheoryDataSetOfT.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/TheoryDataSetOfT.cs @@ -5,75 +5,74 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// Helper class for generating test data for XUnit's -based tests. +/// +/// First parameter type +public class TheoryDataSet : TheoryDataSet { - /// - /// Helper class for generating test data for XUnit's -based tests. - /// - /// First parameter type - public class TheoryDataSet : TheoryDataSet + public void Add(TParam p) { - public void Add(TParam p) - { - AddItem(p); - } + AddItem(p); } +} - /// - /// Helper class for generating test data for XUnit's -based tests. - /// - /// First parameter type - /// Second parameter type - public class TheoryDataSet : TheoryDataSet +/// +/// Helper class for generating test data for XUnit's -based tests. +/// +/// First parameter type +/// Second parameter type +public class TheoryDataSet : TheoryDataSet +{ + public void Add(TParam1 p1, TParam2 p2) { - public void Add(TParam1 p1, TParam2 p2) - { - AddItem(p1, p2); - } + AddItem(p1, p2); } +} - /// - /// Helper class for generating test data for XUnit's -based tests. - /// - /// First parameter type - /// Second parameter type - /// Third parameter type - public class TheoryDataSet : TheoryDataSet +/// +/// Helper class for generating test data for XUnit's -based tests. +/// +/// First parameter type +/// Second parameter type +/// Third parameter type +public class TheoryDataSet : TheoryDataSet +{ + public void Add(TParam1 p1, TParam2 p2, TParam3 p3) { - public void Add(TParam1 p1, TParam2 p2, TParam3 p3) - { - AddItem(p1, p2, p3); - } + AddItem(p1, p2, p3); } +} - /// - /// Helper class for generating test data for XUnit's -based tests. - /// - /// First parameter type - /// Second parameter type - /// Third parameter type - /// Fourth parameter type - public class TheoryDataSet : TheoryDataSet +/// +/// Helper class for generating test data for XUnit's -based tests. +/// +/// First parameter type +/// Second parameter type +/// Third parameter type +/// Fourth parameter type +public class TheoryDataSet : TheoryDataSet +{ + public void Add(TParam1 p1, TParam2 p2, TParam3 p3, TParam4 p4) { - public void Add(TParam1 p1, TParam2 p2, TParam3 p3, TParam4 p4) - { - AddItem(p1, p2, p3, p4); - } + AddItem(p1, p2, p3, p4); } +} - /// - /// Helper class for generating test data for XUnit's -based tests. - /// - /// First parameter type - /// Second parameter type - /// Third parameter type - /// Fourth parameter type - /// Fifth parameter type - public class TheoryDataSet : TheoryDataSet +/// +/// Helper class for generating test data for XUnit's -based tests. +/// +/// First parameter type +/// Second parameter type +/// Third parameter type +/// Fourth parameter type +/// Fifth parameter type +public class TheoryDataSet : TheoryDataSet +{ + public void Add(TParam1 p1, TParam2 p2, TParam3 p3, TParam4 p4, TParam5 p5) { - public void Add(TParam1 p1, TParam2 p2, TParam3 p3, TParam4 p4, TParam5 p5) - { - AddItem(p1, p2, p3, p4, p5); - } + AddItem(p1, p2, p3, p4, p5); } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/Utils/WebApiTestBaseOfT.cs b/test/Microsoft.AspNetCore.OData.TestCommon/Utils/WebApiTestBaseOfT.cs index 05a8e4846..0009258ea 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/Utils/WebApiTestBaseOfT.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/Utils/WebApiTestBaseOfT.cs @@ -8,28 +8,27 @@ using System.Net.Http; using Xunit; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// The WebApiTestBase used for the test base class. +/// +public abstract class WebApiTestBase : IClassFixture> where TTest : class { + private WebApiTestFixture _fixture; + /// - /// The WebApiTestBase used for the test base class. + /// Initializes a new instance of the class. /// - public abstract class WebApiTestBase : IClassFixture> where TTest : class + /// The factory used to initialize the web service client. + protected WebApiTestBase(WebApiTestFixture fixture) { - private WebApiTestFixture _fixture; - - /// - /// Initializes a new instance of the class. - /// - /// The factory used to initialize the web service client. - protected WebApiTestBase(WebApiTestFixture fixture) - { - _fixture = fixture; - } - - /// - /// Create the httpClient - /// - /// - public virtual HttpClient CreateClient() => _fixture.CreateClient(); + _fixture = fixture; } + + /// + /// Create the httpClient + /// + /// + public virtual HttpClient CreateClient() => _fixture.CreateClient(); } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/Utils/WebApiTestFixtureOfT.cs b/test/Microsoft.AspNetCore.OData.TestCommon/Utils/WebApiTestFixtureOfT.cs index 28d2f8c14..22c82a0c6 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/Utils/WebApiTestFixtureOfT.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/Utils/WebApiTestFixtureOfT.cs @@ -12,94 +12,93 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +public class WebApiTestFixture : IDisposable where T : class { - public class WebApiTestFixture : IDisposable where T : class - { - /// - /// The test server. - /// - private TestServer _server; + /// + /// The test server. + /// + private TestServer _server; - /// - /// Initializes a new instance of the class - /// - public WebApiTestFixture() - { - Initialize(); - } + /// + /// Initializes a new instance of the class + /// + public WebApiTestFixture() + { + Initialize(); + } - /// - /// Create the . - /// - /// The created HttpClient. - public HttpClient CreateClient() - { - return _server.CreateClient(); - } + /// + /// Create the . + /// + /// The created HttpClient. + public HttpClient CreateClient() + { + return _server.CreateClient(); + } - /// - /// Cleanup the server. - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - } + /// + /// Cleanup the server. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } - /// - /// Cleanup the server. - /// - protected virtual void Dispose(bool disposing) + /// + /// Cleanup the server. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) { - if (disposing) + if (_server != null) { - if (_server != null) - { - _server.Dispose(); - _server = null; - } + _server.Dispose(); + _server = null; } } + } - /// - /// Initialize the fixture. - /// - private void Initialize() - { - Type testType = typeof(T); - // Be noted: - // We use the convention as follows - // 1) if you want to configure the service, add "protected static void UpdateConfigureServices(IServiceCollection)" method into your test class. - MethodInfo configureServicesMethod = testType.GetMethod("UpdateConfigureServices", BindingFlags.NonPublic | BindingFlags.Static); + /// + /// Initialize the fixture. + /// + private void Initialize() + { + Type testType = typeof(T); + // Be noted: + // We use the convention as follows + // 1) if you want to configure the service, add "protected static void UpdateConfigureServices(IServiceCollection)" method into your test class. + MethodInfo configureServicesMethod = testType.GetMethod("UpdateConfigureServices", BindingFlags.NonPublic | BindingFlags.Static); - // 2) if you want to configure the routing, add "protected static void updateConfigure(IApplicationBuilder)" method into your test class. - MethodInfo configureMethod = testType.GetMethod("UpdateConfigure", BindingFlags.NonPublic | BindingFlags.Static); - // Owing that this is used in Test only, I assume every developer can following the convention. - // So I skip the method parameter checking. + // 2) if you want to configure the routing, add "protected static void updateConfigure(IApplicationBuilder)" method into your test class. + MethodInfo configureMethod = testType.GetMethod("UpdateConfigure", BindingFlags.NonPublic | BindingFlags.Static); + // Owing that this is used in Test only, I assume every developer can following the convention. + // So I skip the method parameter checking. - var builder = new WebHostBuilder() - .ConfigureServices(services => - { - configureServicesMethod?.Invoke(null, new object[] { services }); - }) - .Configure(app => + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + configureServicesMethod?.Invoke(null, new object[] { services }); + }) + .Configure(app => + { + if (configureMethod == null) { - if (configureMethod == null) - { - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - else + app.UseRouting(); + app.UseEndpoints(endpoints => { - configureMethod.Invoke(null, new object[] { app }); - } - }); + endpoints.MapControllers(); + }); + } + else + { + configureMethod.Invoke(null, new object[] { app }); + } + }); - _server = new TestServer(builder); - } + _server = new TestServer(builder); } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/Values/IODataValue.cs b/test/Microsoft.AspNetCore.OData.TestCommon/Values/IODataValue.cs index 5c53a83c4..033233a9f 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/Values/IODataValue.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/Values/IODataValue.cs @@ -5,12 +5,11 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.TestCommon.Values +namespace Microsoft.AspNetCore.OData.TestCommon.Values; + +/// +/// Interface of OData value. +/// +public interface IODataValue { - /// - /// Interface of OData value. - /// - public interface IODataValue - { - } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataArray.cs b/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataArray.cs index 88ac4816c..ad1549707 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataArray.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataArray.cs @@ -7,17 +7,16 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.TestCommon.Values +namespace Microsoft.AspNetCore.OData.TestCommon.Values; + +/// +/// A array of OData value. +/// +public class ODataArray : List, IODataValue { - /// - /// A array of OData value. - /// - public class ODataArray : List, IODataValue - { - public string ContextUri { get; set; } + public string ContextUri { get; set; } - public string NextLink { get; set; } + public string NextLink { get; set; } - public int? TotalCount { get; set; } - } + public int? TotalCount { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataBoolean.cs b/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataBoolean.cs index 7855e73d9..800c5c980 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataBoolean.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataBoolean.cs @@ -5,22 +5,21 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.TestCommon.Values -{ - /// - /// A OData boolean value. - /// - public class ODataBoolean : IODataValue - { - public static ODataBoolean True = new ODataBoolean(true); +namespace Microsoft.AspNetCore.OData.TestCommon.Values; - public static ODataBoolean False = new ODataBoolean(false); +/// +/// A OData boolean value. +/// +public class ODataBoolean : IODataValue +{ + public static ODataBoolean True = new ODataBoolean(true); - private ODataBoolean(bool value) - { - Value = value; - } + public static ODataBoolean False = new ODataBoolean(false); - public bool Value { get; } + private ODataBoolean(bool value) + { + Value = value; } + + public bool Value { get; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataNull.cs b/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataNull.cs index 6e23bfcc5..4c0a410aa 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataNull.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataNull.cs @@ -5,13 +5,12 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.TestCommon.Values +namespace Microsoft.AspNetCore.OData.TestCommon.Values; + +/// +/// A OData null value. +/// +public class ODataNull : IODataValue { - /// - /// A OData null value. - /// - public class ODataNull : IODataValue - { - public static ODataNull Null = new ODataNull(); - } + public static ODataNull Null = new ODataNull(); } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataNumber.cs b/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataNumber.cs index cb38ffb97..6b2e1beeb 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataNumber.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataNumber.cs @@ -5,74 +5,73 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.TestCommon.Values -{ - //public class ODataNumber : IODataValue where T : struct - //{ - // public ODataNumber(T value) - // { - // Value = value; - // } +namespace Microsoft.AspNetCore.OData.TestCommon.Values; - // public T Value { get; } - //} +//public class ODataNumber : IODataValue where T : struct +//{ +// public ODataNumber(T value) +// { +// Value = value; +// } - /// - /// A OData number. - /// - public abstract class ODataNumber : IODataValue - { - } +// public T Value { get; } +//} - /// - /// OData int32 - /// - public class ODataInt : ODataNumber - { - public ODataInt(int value) - { - Value = value; - } +/// +/// A OData number. +/// +public abstract class ODataNumber : IODataValue +{ +} - public int Value { get; } +/// +/// OData int32 +/// +public class ODataInt : ODataNumber +{ + public ODataInt(int value) + { + Value = value; } - /// - /// OData int64 - /// - public class ODataLong : ODataNumber - { - public ODataLong(long value) - { - Value = value; - } + public int Value { get; } +} - public long Value { get; } +/// +/// OData int64 +/// +public class ODataLong : ODataNumber +{ + public ODataLong(long value) + { + Value = value; } - /// - /// OData decimal - /// - public class ODataDecimal : ODataNumber - { - public ODataDecimal(decimal value) - { - Value = value; - } + public long Value { get; } +} - public decimal Value { get; } +/// +/// OData decimal +/// +public class ODataDecimal : ODataNumber +{ + public ODataDecimal(decimal value) + { + Value = value; } - /// - /// OData double - /// - public class ODataDouble : ODataNumber - { - public ODataDouble(double value) - { - Value = value; - } + public decimal Value { get; } +} - public double Value { get; } +/// +/// OData double +/// +public class ODataDouble : ODataNumber +{ + public ODataDouble(double value) + { + Value = value; } + + public double Value { get; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataObject.cs b/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataObject.cs index a310cfa83..aee257266 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataObject.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataObject.cs @@ -7,13 +7,12 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.TestCommon.Values +namespace Microsoft.AspNetCore.OData.TestCommon.Values; + +/// +/// An OData object +/// +public class ODataObject : Dictionary, IODataValue { - /// - /// An OData object - /// - public class ODataObject : Dictionary, IODataValue - { - public string ContextUri { get; set; } - } + public string ContextUri { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataString.cs b/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataString.cs index 52985cb01..98b1f8770 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataString.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/Values/ODataString.cs @@ -5,18 +5,17 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.TestCommon.Values +namespace Microsoft.AspNetCore.OData.TestCommon.Values; + +/// +/// A OData string value. +/// +public class ODataString : IODataValue { - /// - /// A OData string value. - /// - public class ODataString : IODataValue + public ODataString(string value) { - public ODataString(string value) - { - Value = value; - } - - public string Value { get; } + Value = value; } + + public string Value { get; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/WebODataControllerFeatureProvider.cs b/test/Microsoft.AspNetCore.OData.TestCommon/WebODataControllerFeatureProvider.cs index b4f1f431f..f0bee4937 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/WebODataControllerFeatureProvider.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/WebODataControllerFeatureProvider.cs @@ -11,41 +11,40 @@ using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Controllers; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// Controller feature provider +/// +public class WebODataControllerFeatureProvider : IApplicationFeatureProvider, IApplicationFeatureProvider { + private Type[] _controllers; + /// - /// Controller feature provider + /// Initializes a new instance of the class. /// - public class WebODataControllerFeatureProvider : IApplicationFeatureProvider, IApplicationFeatureProvider + /// The controllers + public WebODataControllerFeatureProvider(params Type[] controllers) { - private Type[] _controllers; + _controllers = controllers; + } - /// - /// Initializes a new instance of the class. - /// - /// The controllers - public WebODataControllerFeatureProvider(params Type[] controllers) + /// + /// Updates the feature instance. + /// + /// The list of instances in the application. + /// The controller feature. + public void PopulateFeature(IEnumerable parts, ControllerFeature feature) + { + if (_controllers == null) { - _controllers = controllers; + return; } - /// - /// Updates the feature instance. - /// - /// The list of instances in the application. - /// The controller feature. - public void PopulateFeature(IEnumerable parts, ControllerFeature feature) + feature.Controllers.Clear(); + foreach (var type in _controllers) { - if (_controllers == null) - { - return; - } - - feature.Controllers.Clear(); - foreach (var type in _controllers) - { - feature.Controllers.Add(type.GetTypeInfo()); - } + feature.Controllers.Add(type.GetTypeInfo()); } } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/WebODataTestBaseOfT.cs b/test/Microsoft.AspNetCore.OData.TestCommon/WebODataTestBaseOfT.cs index 967de0768..c8869bfee 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/WebODataTestBaseOfT.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/WebODataTestBaseOfT.cs @@ -9,44 +9,43 @@ using System.Net.Http; using Xunit; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +/// +/// The WebODataTestBase used for the test base class. +/// +public abstract class WebODataTestBase : IClassFixture> where TStartup : class { + private HttpClient _client; + /// - /// The WebODataTestBase used for the test base class. + /// Initializes a new instance of the class. /// - public abstract class WebODataTestBase : IClassFixture> where TStartup : class + /// The factory used to initialize the web service client. + protected WebODataTestBase(WebODataTestFixture factory) { - private HttpClient _client; - - /// - /// Initializes a new instance of the class. - /// - /// The factory used to initialize the web service client. - protected WebODataTestBase(WebODataTestFixture factory) - { - Factory = factory ?? throw new ArgumentNullException(nameof(factory)); - } + Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + } - /// - /// An HttpClient to use with the server. - /// - public virtual HttpClient Client + /// + /// An HttpClient to use with the server. + /// + public virtual HttpClient Client + { + get { - get + if (_client == null) { - if (_client == null) - { - _client = Factory.CreateClient(); - _client.Timeout = TimeSpan.FromSeconds(3600); - } - - return _client; + _client = Factory.CreateClient(); + _client.Timeout = TimeSpan.FromSeconds(3600); } - } - /// - /// Gets the factory. - /// - public WebODataTestFixture Factory { get; } + return _client; + } } + + /// + /// Gets the factory. + /// + public WebODataTestFixture Factory { get; } } diff --git a/test/Microsoft.AspNetCore.OData.TestCommon/WebODataTestFixtureOfT.cs b/test/Microsoft.AspNetCore.OData.TestCommon/WebODataTestFixtureOfT.cs index 44f2f00a2..45d3c3b41 100644 --- a/test/Microsoft.AspNetCore.OData.TestCommon/WebODataTestFixtureOfT.cs +++ b/test/Microsoft.AspNetCore.OData.TestCommon/WebODataTestFixtureOfT.cs @@ -9,22 +9,21 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Hosting; -namespace Microsoft.AspNetCore.OData.TestCommon +namespace Microsoft.AspNetCore.OData.TestCommon; + +public class WebODataTestFixture : WebApplicationFactory where TStartup : class { - public class WebODataTestFixture : WebApplicationFactory where TStartup : class + protected override void ConfigureWebHost(IWebHostBuilder builder) { - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - builder - .UseStartup() + builder + .UseStartup() - // we have to set the root otherwise we get the System.IO.DirectoryNotFoundException - .UseContentRoot(""); - } + // we have to set the root otherwise we get the System.IO.DirectoryNotFoundException + .UseContentRoot(""); + } - protected override IHostBuilder CreateHostBuilder() - { - return Host.CreateDefaultBuilder(); - } + protected override IHostBuilder CreateHostBuilder() + { + return Host.CreateDefaultBuilder(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Abstracts/ContainerBuilderExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Abstracts/ContainerBuilderExtensionsTests.cs index edfc3d601..9266c0c45 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Abstracts/ContainerBuilderExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Abstracts/ContainerBuilderExtensionsTests.cs @@ -11,16 +11,15 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Abstracts +namespace Microsoft.AspNetCore.OData.Tests.Abstracts; + +public class ContainerBuilderExtensionsTests { - public class ContainerBuilderExtensionsTests + [Fact] + public void AddDefaultWebApiServices_ThrowsArgumentNull_Builder() { - [Fact] - public void AddDefaultWebApiServices_ThrowsArgumentNull_Builder() - { - // Arrange & Act & Assert - IServiceCollection services = null; - ExceptionAssert.ThrowsArgumentNull(() => services.AddDefaultWebApiServices(), "services"); - } + // Arrange & Act & Assert + IServiceCollection services = null; + ExceptionAssert.ThrowsArgumentNull(() => services.AddDefaultWebApiServices(), "services"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Abstracts/DefaultContainerBuilderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Abstracts/DefaultContainerBuilderTests.cs index 813ccb65d..8a7d16561 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Abstracts/DefaultContainerBuilderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Abstracts/DefaultContainerBuilderTests.cs @@ -14,130 +14,129 @@ using Xunit; using ServiceLifetime = Microsoft.OData.ServiceLifetime; -namespace Microsoft.AspNetCore.OData.Tests.Abstracts +namespace Microsoft.AspNetCore.OData.Tests.Abstracts; + +public class DefaultContainerBuilderTests { - public class DefaultContainerBuilderTests + [Fact] + public void AddServiceWithImplementationType_ThrowsArgumentNull_ForInputParameters() + { + // Arrange & Act & Assert + IServiceCollection services = new ServiceCollection(); + ExceptionAssert.ThrowsArgumentNull( + () => services.AddSingleton(serviceType: null, implementationType: null), "serviceType"); + + ExceptionAssert.ThrowsArgumentNull( + () => services.AddSingleton(serviceType: typeof(int), implementationType: null), "implementationType"); + } + + [Fact] + public void AddServiceWithImplementationFactory_ThrowsArgumentNull_ForInputParameters() + { + // Arrange & Act & Assert + IServiceCollection services = new ServiceCollection(); + ExceptionAssert.ThrowsArgumentNull( + () => services.AddSingleton(serviceType: null, implementationFactory: null), "serviceType"); + + ExceptionAssert.ThrowsArgumentNull( + () => services.AddSingleton(serviceType: typeof(int), implementationFactory: null), "implementationFactory"); + } + + [Fact] + public void AddService_WithImplementationType() { - [Fact] - public void AddServiceWithImplementationType_ThrowsArgumentNull_ForInputParameters() - { - // Arrange & Act & Assert - IServiceCollection services = new ServiceCollection(); - ExceptionAssert.ThrowsArgumentNull( - () => services.AddSingleton(serviceType: null, implementationType: null), "serviceType"); - - ExceptionAssert.ThrowsArgumentNull( - () => services.AddSingleton(serviceType: typeof(int), implementationType: null), "implementationType"); - } - - [Fact] - public void AddServiceWithImplementationFactory_ThrowsArgumentNull_ForInputParameters() - { - // Arrange & Act & Assert - IServiceCollection services = new ServiceCollection(); - ExceptionAssert.ThrowsArgumentNull( - () => services.AddSingleton(serviceType: null, implementationFactory: null), "serviceType"); - - ExceptionAssert.ThrowsArgumentNull( - () => services.AddSingleton(serviceType: typeof(int), implementationFactory: null), "implementationFactory"); - } - - [Fact] - public void AddService_WithImplementationType() - { - // Arrange - IServiceCollection services = new ServiceCollection(); - services.AddTransient(); - - // Act - IServiceProvider container = services.BuildServiceProvider(); - - // Assert - Assert.NotNull(container.GetService()); - } - - [Fact] - public void AddService_WithImplementationFactory() - { - // Arrange - IServiceCollection services = new ServiceCollection(); - services.AddTransient(sp => new TestService()); - - // Act - IServiceProvider container = services.BuildServiceProvider(); - - // Assert - Assert.NotNull(container.GetService()); - } - - [Fact] - public void AddSingletonService_Works() - { - // Arrange - IServiceCollection services = new ServiceCollection(); - services.AddSingleton(); - IServiceProvider container = services.BuildServiceProvider(); - - // Act - ITestService o1 = container.GetService(); - ITestService o2 = container.GetService(); - - // Assert - Assert.NotNull(o1); - Assert.Equal(o1, o2); - } - - [Fact] - public void AddTransientService_Works() - { - // Arrange - IServiceCollection services = new ServiceCollection(); - services.AddTransient(); - IServiceProvider container = services.BuildServiceProvider(); - - // Act - ITestService o1 = container.GetService(); - ITestService o2 = container.GetService(); - - // Assert - Assert.NotNull(o1); - Assert.NotNull(o2); - Assert.NotEqual(o1, o2); - } - - [Fact] - public void AddScopedService_Works() - { - // Arrange - IServiceCollection services = new ServiceCollection(); - services.AddScoped(); - IServiceProvider container = services.BuildServiceProvider(); - - // Act - IServiceProvider scopedContainer1 = container.GetRequiredService() - .CreateScope().ServiceProvider; - ITestService o11 = scopedContainer1.GetService(); - ITestService o12 = scopedContainer1.GetService(); - - // Assert - Assert.NotNull(o11); - Assert.NotNull(o12); - Assert.Equal(o11, o12); - - IServiceProvider scopedContainer2 = container.GetRequiredService() - .CreateScope().ServiceProvider; - ITestService o21 = scopedContainer2.GetService(); - ITestService o22 = scopedContainer2.GetService(); - - Assert.NotNull(o21); - Assert.NotNull(o22); - Assert.Equal(o21, o22); - - Assert.NotEqual(o11, o21); - } - - private interface ITestService { } - - private class TestService : ITestService { } + // Arrange + IServiceCollection services = new ServiceCollection(); + services.AddTransient(); + + // Act + IServiceProvider container = services.BuildServiceProvider(); + + // Assert + Assert.NotNull(container.GetService()); } + + [Fact] + public void AddService_WithImplementationFactory() + { + // Arrange + IServiceCollection services = new ServiceCollection(); + services.AddTransient(sp => new TestService()); + + // Act + IServiceProvider container = services.BuildServiceProvider(); + + // Assert + Assert.NotNull(container.GetService()); + } + + [Fact] + public void AddSingletonService_Works() + { + // Arrange + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(); + IServiceProvider container = services.BuildServiceProvider(); + + // Act + ITestService o1 = container.GetService(); + ITestService o2 = container.GetService(); + + // Assert + Assert.NotNull(o1); + Assert.Equal(o1, o2); + } + + [Fact] + public void AddTransientService_Works() + { + // Arrange + IServiceCollection services = new ServiceCollection(); + services.AddTransient(); + IServiceProvider container = services.BuildServiceProvider(); + + // Act + ITestService o1 = container.GetService(); + ITestService o2 = container.GetService(); + + // Assert + Assert.NotNull(o1); + Assert.NotNull(o2); + Assert.NotEqual(o1, o2); + } + + [Fact] + public void AddScopedService_Works() + { + // Arrange + IServiceCollection services = new ServiceCollection(); + services.AddScoped(); + IServiceProvider container = services.BuildServiceProvider(); + + // Act + IServiceProvider scopedContainer1 = container.GetRequiredService() + .CreateScope().ServiceProvider; + ITestService o11 = scopedContainer1.GetService(); + ITestService o12 = scopedContainer1.GetService(); + + // Assert + Assert.NotNull(o11); + Assert.NotNull(o12); + Assert.Equal(o11, o12); + + IServiceProvider scopedContainer2 = container.GetRequiredService() + .CreateScope().ServiceProvider; + ITestService o21 = scopedContainer2.GetService(); + ITestService o22 = scopedContainer2.GetService(); + + Assert.NotNull(o21); + Assert.NotNull(o22); + Assert.Equal(o21, o22); + + Assert.NotEqual(o11, o21); + } + + private interface ITestService { } + + private class TestService : ITestService { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Abstracts/ETagActionFilterAttributeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Abstracts/ETagActionFilterAttributeTests.cs index ab2ed66fe..364c3d20b 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Abstracts/ETagActionFilterAttributeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Abstracts/ETagActionFilterAttributeTests.cs @@ -19,87 +19,86 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Abstracts +namespace Microsoft.AspNetCore.OData.Tests.Abstracts; + +public class ETagActionFilterAttributeTests { - public class ETagActionFilterAttributeTests + [Fact] + public void OnActionExecuted_ThrowsArgumentNull_ActionExecutedContext() { - [Fact] - public void OnActionExecuted_ThrowsArgumentNull_ActionExecutedContext() - { - // Arrange - ETagActionFilterAttribute filter = new ETagActionFilterAttribute(); + // Arrange + ETagActionFilterAttribute filter = new ETagActionFilterAttribute(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => filter.OnActionExecuted(null), "actionExecutedContext"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => filter.OnActionExecuted(null), "actionExecutedContext"); + } - [Fact] - public void OnActionExecuted_ThrowsArgumentNull_HttpContextOnActionExecutedContext() - { - // Arrange - ETagActionFilterAttribute filter = new ETagActionFilterAttribute(); - ActionContext actionContext = new ActionContext - ( - new DefaultHttpContext(), - new RouteData(), - new ActionDescriptor() - ); - ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), null); - context.HttpContext = null; + [Fact] + public void OnActionExecuted_ThrowsArgumentNull_HttpContextOnActionExecutedContext() + { + // Arrange + ETagActionFilterAttribute filter = new ETagActionFilterAttribute(); + ActionContext actionContext = new ActionContext + ( + new DefaultHttpContext(), + new RouteData(), + new ActionDescriptor() + ); + ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), null); + context.HttpContext = null; - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => filter.OnActionExecuted(context), "httpContext"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => filter.OnActionExecuted(context), "httpContext"); + } - [Fact] - public void OnActionExecuted_ThrowsArgumentNull_PathOnActionExecutedContext() - { - // Arrange - ETagActionFilterAttribute filter = new ETagActionFilterAttribute(); - HttpContext httpContext = new DefaultHttpContext(); - ActionContext actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), null); + [Fact] + public void OnActionExecuted_ThrowsArgumentNull_PathOnActionExecutedContext() + { + // Arrange + ETagActionFilterAttribute filter = new ETagActionFilterAttribute(); + HttpContext httpContext = new DefaultHttpContext(); + ActionContext actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), null); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => filter.OnActionExecuted(context), "path"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => filter.OnActionExecuted(context), "path"); + } - [Fact] - public void OnActionExecuted_ThrowsArgumentNull_ModelOnActionExecutedContext() - { - // Arrange - ETagActionFilterAttribute filter = new ETagActionFilterAttribute(); - HttpContext httpContext = new DefaultHttpContext(); - ActionContext actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), null); - httpContext.ODataFeature().Path = new ODataPath(); + [Fact] + public void OnActionExecuted_ThrowsArgumentNull_ModelOnActionExecutedContext() + { + // Arrange + ETagActionFilterAttribute filter = new ETagActionFilterAttribute(); + HttpContext httpContext = new DefaultHttpContext(); + ActionContext actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), null); + httpContext.ODataFeature().Path = new ODataPath(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => filter.OnActionExecuted(context), "model"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => filter.OnActionExecuted(context), "model"); + } - [Fact] - public void OnActionExecuted_ThrowsArgumentNull_ETagHandlerOnActionExecutedContext() - { - // Arrange - ETagActionFilterAttribute filter = new ETagActionFilterAttribute(); - HttpContext httpContext = new DefaultHttpContext(); - ActionContext actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), null); - httpContext.ODataFeature().Path = new ODataPath(); - httpContext.ODataFeature().Model = new EdmModel(); - httpContext.ODataFeature().Services = new ServiceCollection().BuildServiceProvider(); + [Fact] + public void OnActionExecuted_ThrowsArgumentNull_ETagHandlerOnActionExecutedContext() + { + // Arrange + ETagActionFilterAttribute filter = new ETagActionFilterAttribute(); + HttpContext httpContext = new DefaultHttpContext(); + ActionContext actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), null); + httpContext.ODataFeature().Path = new ODataPath(); + httpContext.ODataFeature().Model = new EdmModel(); + httpContext.ODataFeature().Services = new ServiceCollection().BuildServiceProvider(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => filter.OnActionExecuted(context), "etagHandler"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => filter.OnActionExecuted(context), "etagHandler"); + } - [Fact] - public void GetSingleEntityEntityType_Returns_EntityType() - { - // Arrange & Act & Assert - Assert.Null(ETagActionFilterAttribute.GetSingleEntityEntityType(null)); - Assert.Null(ETagActionFilterAttribute.GetSingleEntityEntityType(new ODataPath())); - } + [Fact] + public void GetSingleEntityEntityType_Returns_EntityType() + { + // Arrange & Act & Assert + Assert.Null(ETagActionFilterAttribute.GetSingleEntityEntityType(null)); + Assert.Null(ETagActionFilterAttribute.GetSingleEntityEntityType(new ODataPath())); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Abstracts/ODataFeatureTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Abstracts/ODataFeatureTests.cs index 9e7699c03..95df3e277 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Abstracts/ODataFeatureTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Abstracts/ODataFeatureTests.cs @@ -9,10 +9,9 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Abstracts +namespace Microsoft.AspNetCore.OData.Tests.Abstracts; + +public class ODataFeatureTests { - public class ODataFeatureTests - { - } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Abstracts/PerRouteContainerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Abstracts/PerRouteContainerTests.cs index 945c88b2d..245be1f7d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Abstracts/PerRouteContainerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Abstracts/PerRouteContainerTests.cs @@ -13,61 +13,60 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Abstracts -{ - //public class PerRouteContainerTests - //{ - // [Fact] - // public void Ctor_BuildServiceContainer_WithOneModel() - // { - // // Arrange - // IEdmModel model = EdmCoreModel.Instance; - // var options = new ODataOptions(); - // options.AddModel("odata", model); +namespace Microsoft.AspNetCore.OData.Tests.Abstracts; - // // Act - // PerRouteContainer container = CreateContainer(options); +//public class PerRouteContainerTests +//{ +// [Fact] +// public void Ctor_BuildServiceContainer_WithOneModel() +// { +// // Arrange +// IEdmModel model = EdmCoreModel.Instance; +// var options = new ODataOptions(); +// options.AddModel("odata", model); - // // Assert - // KeyValuePair service = Assert.Single(container.Services); - // Assert.Equal("odata", service.Key); - // Assert.NotNull(service.Value); +// // Act +// PerRouteContainer container = CreateContainer(options); - // IEdmModel actualModel =service.Value.GetService(); - // Assert.Same(model, actualModel); - // } +// // Assert +// KeyValuePair service = Assert.Single(container.Services); +// Assert.Equal("odata", service.Key); +// Assert.NotNull(service.Value); - // [Fact] - // public void Ctor_BuildServiceContainer_WithTwoModels() - // { - // // Arrange - // IEdmModel model = new EdmModel(); - // var options = new ODataOptions(); - // options.AddModel("odata", EdmCoreModel.Instance); - // options.AddModel("my{data}", model); +// IEdmModel actualModel =service.Value.GetService(); +// Assert.Same(model, actualModel); +// } - // // Act - // PerRouteContainer container = CreateContainer(options); +// [Fact] +// public void Ctor_BuildServiceContainer_WithTwoModels() +// { +// // Arrange +// IEdmModel model = new EdmModel(); +// var options = new ODataOptions(); +// options.AddModel("odata", EdmCoreModel.Instance); +// options.AddModel("my{data}", model); - // // Assert - // Assert.Equal(2, container.Services.Count); - // IServiceProvider sp1 = container.GetServiceProvider("odata"); - // Assert.NotNull(sp1); - // IEdmModel actualModel = sp1.GetService(); - // Assert.Same(EdmCoreModel.Instance, actualModel); +// // Act +// PerRouteContainer container = CreateContainer(options); - // IServiceProvider sp2 = container.GetServiceProvider("my{data}"); - // Assert.NotNull(sp2); - // actualModel = sp2.GetService(); - // Assert.Same(model, actualModel); +// // Assert +// Assert.Equal(2, container.Services.Count); +// IServiceProvider sp1 = container.GetServiceProvider("odata"); +// Assert.NotNull(sp1); +// IEdmModel actualModel = sp1.GetService(); +// Assert.Same(EdmCoreModel.Instance, actualModel); - // Assert.Null(container.GetServiceProvider("any")); - // } +// IServiceProvider sp2 = container.GetServiceProvider("my{data}"); +// Assert.NotNull(sp2); +// actualModel = sp2.GetService(); +// Assert.Same(model, actualModel); - // private static PerRouteContainer CreateContainer(ODataOptions options) - // { - // IOptions odataOptions = Options.Create(options); - // return new PerRouteContainer(odataOptions); - // } - //} -} +// Assert.Null(container.GetServiceProvider("any")); +// } + +// private static PerRouteContainer CreateContainer(ODataOptions options) +// { +// IOptions odataOptions = Options.Create(options); +// return new PerRouteContainer(odataOptions); +// } +//} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/ChangeSetRequestItemTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/ChangeSetRequestItemTest.cs index ec06e05d1..13bbdbb7f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/ChangeSetRequestItemTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/ChangeSetRequestItemTest.cs @@ -15,124 +15,123 @@ using Microsoft.AspNetCore.OData.Tests.Extensions; using Xunit; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +public class ChangeSetRequestItemTest { - public class ChangeSetRequestItemTest + [Fact] + public void Parameter_Constructor() { - [Fact] - public void Parameter_Constructor() - { - // Arrange & Act - HttpContext[] contexts = Array.Empty(); - ChangeSetRequestItem requestItem = new ChangeSetRequestItem(contexts); + // Arrange & Act + HttpContext[] contexts = Array.Empty(); + ChangeSetRequestItem requestItem = new ChangeSetRequestItem(contexts); - // Assert - Assert.Same(contexts, requestItem.Contexts); - } + // Assert + Assert.Same(contexts, requestItem.Contexts); + } - [Fact] - public void Constructor_NullRequests_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ChangeSetRequestItem(null), "contexts"); - } + [Fact] + public void Constructor_NullRequests_Throws() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ChangeSetRequestItem(null), "contexts"); + } - [Fact] - public async Task SendRequestAsync_NullHandler_Throws() - { - // Arrange - HttpContext[] contexts = Array.Empty(); - ChangeSetRequestItem requestItem = new ChangeSetRequestItem(contexts); + [Fact] + public async Task SendRequestAsync_NullHandler_Throws() + { + // Arrange + HttpContext[] contexts = Array.Empty(); + ChangeSetRequestItem requestItem = new ChangeSetRequestItem(contexts); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => requestItem.SendRequestAsync(null), "handler"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => requestItem.SendRequestAsync(null), "handler"); + } - [Fact] - public async Task SendRequestAsync_ReturnsChangeSetResponse() + [Fact] + public async Task SendRequestAsync_ReturnsChangeSetResponse() + { + // Arrange + HttpContext[] contexts = new HttpContext[] { - // Arrange - HttpContext[] contexts = new HttpContext[] - { - HttpContextHelper.Create("Get", "http://example.com"), - HttpContextHelper.Create("Post", "http://example.com") - }; - ChangeSetRequestItem requestItem = new ChangeSetRequestItem(contexts); + HttpContextHelper.Create("Get", "http://example.com"), + HttpContextHelper.Create("Post", "http://example.com") + }; + ChangeSetRequestItem requestItem = new ChangeSetRequestItem(contexts); - RequestDelegate handler = context => Task.CompletedTask; + RequestDelegate handler = context => Task.CompletedTask; - // Act - ODataBatchResponseItem response = await requestItem.SendRequestAsync(handler); + // Act + ODataBatchResponseItem response = await requestItem.SendRequestAsync(handler); - // Assert - ChangeSetResponseItem changesetResponse = Assert.IsType(response); - Assert.Equal(2, changesetResponse.Contexts.Count()); - } + // Assert + ChangeSetResponseItem changesetResponse = Assert.IsType(response); + Assert.Equal(2, changesetResponse.Contexts.Count()); + } - [Fact] - public async Task SendRequestAsync_ReturnsSingleErrorResponse() + [Fact] + public async Task SendRequestAsync_ReturnsSingleErrorResponse() + { + // Arrange + HttpContext[] contexts = new HttpContext[] + { + HttpContextHelper.Create("Get", "http://example.com"), + HttpContextHelper.Create("Post", "http://example.com"), + HttpContextHelper.Create("Put", "http://example.com") + }; + ChangeSetRequestItem requestItem = new ChangeSetRequestItem(contexts); + + RequestDelegate handler = context => { - // Arrange - HttpContext[] contexts = new HttpContext[] - { - HttpContextHelper.Create("Get", "http://example.com"), - HttpContextHelper.Create("Post", "http://example.com"), - HttpContextHelper.Create("Put", "http://example.com") - }; - ChangeSetRequestItem requestItem = new ChangeSetRequestItem(contexts); - - RequestDelegate handler = context => + if (context.Request.Method == "Post") { - if (context.Request.Method == "Post") - { - context.Response.StatusCode = StatusCodes.Status400BadRequest; - } + context.Response.StatusCode = StatusCodes.Status400BadRequest; + } - return Task.CompletedTask; - }; + return Task.CompletedTask; + }; - // Act - ODataBatchResponseItem response = await requestItem.SendRequestAsync(handler); + // Act + ODataBatchResponseItem response = await requestItem.SendRequestAsync(handler); - // Assert - ChangeSetResponseItem changesetResponse = Assert.IsType(response); - HttpContext responseContext = Assert.Single(changesetResponse.Contexts); - Assert.Equal(StatusCodes.Status400BadRequest, responseContext.Response.StatusCode); - } + // Assert + ChangeSetResponseItem changesetResponse = Assert.IsType(response); + HttpContext responseContext = Assert.Single(changesetResponse.Contexts); + Assert.Equal(StatusCodes.Status400BadRequest, responseContext.Response.StatusCode); + } - [Fact] - public async Task SendRequestAsync_DisposesResponseInCaseOfException() + [Fact] + public async Task SendRequestAsync_DisposesResponseInCaseOfException() + { + // Arrange + HttpContext[] contexts = new HttpContext[] + { + HttpContextHelper.Create("Get", "http://example.com"), + HttpContextHelper.Create("Post", "http://example.com"), + HttpContextHelper.Create("Put", "http://example.com") + }; + ChangeSetRequestItem requestItem = new ChangeSetRequestItem(contexts); + + List responses = new List(); + RequestDelegate handler = context => { - // Arrange - HttpContext[] contexts = new HttpContext[] - { - HttpContextHelper.Create("Get", "http://example.com"), - HttpContextHelper.Create("Post", "http://example.com"), - HttpContextHelper.Create("Put", "http://example.com") - }; - ChangeSetRequestItem requestItem = new ChangeSetRequestItem(contexts); - - List responses = new List(); - RequestDelegate handler = context => + if (context.Request.Method == "Put") { - if (context.Request.Method == "Put") - { - throw new InvalidOperationException(); - } + throw new InvalidOperationException(); + } - responses.Add(context.Response); - return Task.CompletedTask; - }; + responses.Add(context.Response); + return Task.CompletedTask; + }; - // Act - ODataBatchResponseItem response = await requestItem.SendRequestAsync(handler); + // Act + ODataBatchResponseItem response = await requestItem.SendRequestAsync(handler); - // Assert - ChangeSetResponseItem changesetResponse = Assert.IsType(response); - HttpContext responseContext = Assert.Single(changesetResponse.Contexts); - Assert.Equal(StatusCodes.Status500InternalServerError, responseContext.Response.StatusCode); + // Assert + ChangeSetResponseItem changesetResponse = Assert.IsType(response); + HttpContext responseContext = Assert.Single(changesetResponse.Contexts); + Assert.Equal(StatusCodes.Status500InternalServerError, responseContext.Response.StatusCode); - Assert.Equal(2, responses.Count); - } + Assert.Equal(2, responses.Count); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/ChangeSetResponseItemTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/ChangeSetResponseItemTest.cs index bbe93dc16..1d8a22b25 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/ChangeSetResponseItemTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/ChangeSetResponseItemTest.cs @@ -16,112 +16,111 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +public class ChangeSetResponseItemTest { - public class ChangeSetResponseItemTest + [Fact] + public void Parameter_Constructor() { - [Fact] - public void Parameter_Constructor() - { - // Arrange & Act - HttpContext[] contexts = Array.Empty(); - ChangeSetResponseItem responseItem = new ChangeSetResponseItem(contexts); + // Arrange & Act + HttpContext[] contexts = Array.Empty(); + ChangeSetResponseItem responseItem = new ChangeSetResponseItem(contexts); - // Assert - Assert.Same(contexts, responseItem.Contexts); - } + // Assert + Assert.Same(contexts, responseItem.Contexts); + } - [Fact] - public void Constructor_NullContexts_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ChangeSetResponseItem(null), "contexts"); - } + [Fact] + public void Constructor_NullContexts_Throws() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ChangeSetResponseItem(null), "contexts"); + } - [Fact] - public async Task WriteResponseAsync_NullWriter_Throws() - { - // Arrange & Act - HttpContext[] contexts = Array.Empty(); - ChangeSetResponseItem responseItem = new ChangeSetResponseItem(contexts); + [Fact] + public async Task WriteResponseAsync_NullWriter_Throws() + { + // Arrange & Act + HttpContext[] contexts = Array.Empty(); + ChangeSetResponseItem responseItem = new ChangeSetResponseItem(contexts); - await ExceptionAssert.ThrowsArgumentNullAsync(() => responseItem.WriteResponseAsync(null), "writer"); - } + await ExceptionAssert.ThrowsArgumentNullAsync(() => responseItem.WriteResponseAsync(null), "writer"); + } - [Fact] - public void WriteResponse_SynchronouslyWritesChangeSet_Throws() - { - // Arrange - HttpContext context1 = HttpContextHelper.Create(StatusCodes.Status202Accepted); - HttpContext context2 = HttpContextHelper.Create(StatusCodes.Status204NoContent); - - ChangeSetResponseItem responseItem = new ChangeSetResponseItem(new[] { context1, context2 }); - MemoryStream memoryStream = new MemoryStream(); - IODataResponseMessage responseMessage = new ODataMessageWrapper(memoryStream); - ODataMessageWriter writer = new ODataMessageWriter(responseMessage); - - // Act - ODataBatchWriter batchWriter = writer.CreateODataBatchWriter(); - batchWriter.WriteStartBatch(); - - // Assert - Action test = () => responseItem.WriteResponseAsync(batchWriter).Wait(); - ODataException exception = ExceptionAssert.Throws(test); - Assert.Equal("An asynchronous operation was called on a synchronous batch writer. Calls on a batch writer instance must be either all synchronous or all asynchronous.", - exception.Message); - } - - [Fact] - public async Task WriteResponseAsync_WritesChangeSet() + [Fact] + public void WriteResponse_SynchronouslyWritesChangeSet_Throws() + { + // Arrange + HttpContext context1 = HttpContextHelper.Create(StatusCodes.Status202Accepted); + HttpContext context2 = HttpContextHelper.Create(StatusCodes.Status204NoContent); + + ChangeSetResponseItem responseItem = new ChangeSetResponseItem(new[] { context1, context2 }); + MemoryStream memoryStream = new MemoryStream(); + IODataResponseMessage responseMessage = new ODataMessageWrapper(memoryStream); + ODataMessageWriter writer = new ODataMessageWriter(responseMessage); + + // Act + ODataBatchWriter batchWriter = writer.CreateODataBatchWriter(); + batchWriter.WriteStartBatch(); + + // Assert + Action test = () => responseItem.WriteResponseAsync(batchWriter).Wait(); + ODataException exception = ExceptionAssert.Throws(test); + Assert.Equal("An asynchronous operation was called on a synchronous batch writer. Calls on a batch writer instance must be either all synchronous or all asynchronous.", + exception.Message); + } + + [Fact] + public async Task WriteResponseAsync_WritesChangeSet() + { + // Arrange + HttpContext context1 = HttpContextHelper.Create(StatusCodes.Status202Accepted); + HttpContext context2 = HttpContextHelper.Create(StatusCodes.Status204NoContent); + + ChangeSetResponseItem responseItem = new ChangeSetResponseItem(new[] { context1, context2 }); + MemoryStream memoryStream = new MemoryStream(); + IODataResponseMessage responseMessage = new ODataMessageWrapper(memoryStream); + ODataMessageWriter writer = new ODataMessageWriter(responseMessage); + + // Act + ODataBatchWriter batchWriter = await writer.CreateODataBatchWriterAsync(); + await batchWriter.WriteStartBatchAsync(); + await responseItem.WriteResponseAsync(batchWriter); + await batchWriter.WriteEndBatchAsync(); + + memoryStream.Position = 0; + string responseString = new StreamReader(memoryStream).ReadToEnd(); + + // Assert + Assert.Contains("changesetresponse", responseString); + Assert.Contains("Accepted", responseString); + Assert.Contains("No Content", responseString); + } + + [Fact] + public void IsResponseSuccessful_TestResponse() + { + // Arrange + HttpContext[] successResponses = new HttpContext[] { - // Arrange - HttpContext context1 = HttpContextHelper.Create(StatusCodes.Status202Accepted); - HttpContext context2 = HttpContextHelper.Create(StatusCodes.Status204NoContent); - - ChangeSetResponseItem responseItem = new ChangeSetResponseItem(new[] { context1, context2 }); - MemoryStream memoryStream = new MemoryStream(); - IODataResponseMessage responseMessage = new ODataMessageWrapper(memoryStream); - ODataMessageWriter writer = new ODataMessageWriter(responseMessage); - - // Act - ODataBatchWriter batchWriter = await writer.CreateODataBatchWriterAsync(); - await batchWriter.WriteStartBatchAsync(); - await responseItem.WriteResponseAsync(batchWriter); - await batchWriter.WriteEndBatchAsync(); - - memoryStream.Position = 0; - string responseString = new StreamReader(memoryStream).ReadToEnd(); - - // Assert - Assert.Contains("changesetresponse", responseString); - Assert.Contains("Accepted", responseString); - Assert.Contains("No Content", responseString); - } - - [Fact] - public void IsResponseSuccessful_TestResponse() + HttpContextHelper.Create(StatusCodes.Status202Accepted), + HttpContextHelper.Create(StatusCodes.Status201Created), + HttpContextHelper.Create(StatusCodes.Status200OK) + }; + + HttpContext[] errorResponses = new HttpContext[] { - // Arrange - HttpContext[] successResponses = new HttpContext[] - { - HttpContextHelper.Create(StatusCodes.Status202Accepted), - HttpContextHelper.Create(StatusCodes.Status201Created), - HttpContextHelper.Create(StatusCodes.Status200OK) - }; - - HttpContext[] errorResponses = new HttpContext[] - { - HttpContextHelper.Create(StatusCodes.Status201Created), - HttpContextHelper.Create(StatusCodes.Status502BadGateway), - HttpContextHelper.Create(StatusCodes.Status300MultipleChoices) - }; - - ChangeSetResponseItem successResponseItem = new ChangeSetResponseItem(successResponses); - ChangeSetResponseItem errorResponseItem = new ChangeSetResponseItem(errorResponses); - - // Act & Assert - Assert.True(successResponseItem.IsResponseSuccessful()); - Assert.False(errorResponseItem.IsResponseSuccessful()); - } + HttpContextHelper.Create(StatusCodes.Status201Created), + HttpContextHelper.Create(StatusCodes.Status502BadGateway), + HttpContextHelper.Create(StatusCodes.Status300MultipleChoices) + }; + + ChangeSetResponseItem successResponseItem = new ChangeSetResponseItem(successResponses); + ChangeSetResponseItem errorResponseItem = new ChangeSetResponseItem(errorResponses); + + // Act & Assert + Assert.True(successResponseItem.IsResponseSuccessful()); + Assert.False(errorResponseItem.IsResponseSuccessful()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/DefaultODataBatchHandlerTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/DefaultODataBatchHandlerTest.cs index 3c0760567..92ba5e74a 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/DefaultODataBatchHandlerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/DefaultODataBatchHandlerTest.cs @@ -36,123 +36,123 @@ using Newtonsoft.Json; using Xunit; -namespace Microsoft.AspNetCore.OData.Test.Batch -{ - public class DefaultODataBatchHandlerTest - { - private static IServiceProvider _serviceProvider = BuildServiceProvider(); +namespace Microsoft.AspNetCore.OData.Test.Batch; - [Fact] - public void Parameter_Constructor() - { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); +public class DefaultODataBatchHandlerTest +{ + private static IServiceProvider _serviceProvider = BuildServiceProvider(); - // Act & Assert - Assert.False(batchHandler.ContinueOnError); - Assert.NotNull(batchHandler.MessageQuotas); - Assert.Null(batchHandler.PrefixName); - } + [Fact] + public void Parameter_Constructor() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - [Fact] - public async Task CreateResponseMessageAsync_Throws_IfResponsesAreNull() - { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - HttpContext context = new DefaultHttpContext(); - context.ODataFeature().Services = _serviceProvider; - HttpRequest request = context.Request; + // Act & Assert + Assert.False(batchHandler.ContinueOnError); + Assert.NotNull(batchHandler.MessageQuotas); + Assert.Null(batchHandler.PrefixName); + } - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.CreateResponseMessageAsync(null, request), "responses"); - } + [Fact] + public async Task CreateResponseMessageAsync_Throws_IfResponsesAreNull() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + HttpContext context = new DefaultHttpContext(); + context.ODataFeature().Services = _serviceProvider; + HttpRequest request = context.Request; + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.CreateResponseMessageAsync(null, request), "responses"); + } - [Fact] - public async Task CreateResponseMessageAsync_Throws_IfRequestIsNull() - { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + [Fact] + public async Task CreateResponseMessageAsync_Throws_IfRequestIsNull() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.CreateResponseMessageAsync(new ODataBatchResponseItem[0], null), "request"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.CreateResponseMessageAsync(new ODataBatchResponseItem[0], null), "request"); + } - [Fact] - public async Task CreateResponseMessageAsync_ReturnsODataBatchContent() + [Fact] + public async Task CreateResponseMessageAsync_ReturnsODataBatchContent() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance)); + request.ODataFeature().RoutePrefix = "odata"; + HttpContext httpContext = request.HttpContext; + httpContext.Response.StatusCode = StatusCodes.Status200OK; + httpContext.Response.Body = new MemoryStream(); + ODataBatchResponseItem[] responses = new ODataBatchResponseItem[] { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance)); - request.ODataFeature().RoutePrefix = "odata"; - HttpContext httpContext = request.HttpContext; - httpContext.Response.StatusCode = StatusCodes.Status200OK; - httpContext.Response.Body = new MemoryStream(); - ODataBatchResponseItem[] responses = new ODataBatchResponseItem[] - { - new OperationResponseItem(httpContext) - }; + new OperationResponseItem(httpContext) + }; - // Act - await batchHandler.CreateResponseMessageAsync(responses, request); + // Act + await batchHandler.CreateResponseMessageAsync(responses, request); - // Assert - string responseString = httpContext.Response.ReadBody(); - Assert.Contains("200 OK", responseString); - } + // Assert + string responseString = httpContext.Response.ReadBody(); + Assert.Contains("200 OK", responseString); + } - [Fact] - public async Task ProcessBatchAsync_Throws_IfContextIsNull() - { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + [Fact] + public async Task ProcessBatchAsync_Throws_IfContextIsNull() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.ProcessBatchAsync(null, null), "context"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.ProcessBatchAsync(null, null), "context"); + } - [Theory] - [InlineData(true, null, false)] - [InlineData(true, "odata.continue-on-error", true)] - [InlineData(true, "odata.continue-on-error=true", true)] - [InlineData(true, "odata.continue-on-error=false", false)] - [InlineData(true, "continue-on-error", true)] - [InlineData(true, "continue-on-error=true", true)] - [InlineData(true, "continue-on-error=false", false)] - [InlineData(false, null, true)] - [InlineData(false, "odata.continue-on-error", true)] - [InlineData(false, "odata.continue-on-error=true", true)] - [InlineData(false, "odata.continue-on-error=false", true)] - [InlineData(false, "continue-on-error", true)] - [InlineData(false, "continue-on-error=true", true)] - [InlineData(false, "continue-on-error=false", true)] - public async Task ProcessBatchAsync_ContinueOnError(bool enableContinueOnError, string preferenceHeader, bool hasThirdResponse) + [Theory] + [InlineData(true, null, false)] + [InlineData(true, "odata.continue-on-error", true)] + [InlineData(true, "odata.continue-on-error=true", true)] + [InlineData(true, "odata.continue-on-error=false", false)] + [InlineData(true, "continue-on-error", true)] + [InlineData(true, "continue-on-error=true", true)] + [InlineData(true, "continue-on-error=false", false)] + [InlineData(false, null, true)] + [InlineData(false, "odata.continue-on-error", true)] + [InlineData(false, "odata.continue-on-error=true", true)] + [InlineData(false, "odata.continue-on-error=false", true)] + [InlineData(false, "continue-on-error", true)] + [InlineData(false, "continue-on-error=true", true)] + [InlineData(false, "continue-on-error=false", true)] + public async Task ProcessBatchAsync_ContinueOnError(bool enableContinueOnError, string preferenceHeader, bool hasThirdResponse) + { + // Arrange + RequestDelegate handler = async context => { - // Arrange - RequestDelegate handler = async context => + HttpRequest request = context.Request; + string responseContent = request.GetDisplayUrl(); + string content = request.ReadBody(); + if (!string.IsNullOrEmpty(content)) { - HttpRequest request = context.Request; - string responseContent = request.GetDisplayUrl(); - string content = request.ReadBody(); - if (!string.IsNullOrEmpty(content)) - { - responseContent += "," + content; - } + responseContent += "," + content; + } - HttpResponse response = context.Response; - if (content.Equals("foo")) - { - response.StatusCode = StatusCodes.Status400BadRequest; - } + HttpResponse response = context.Response; + if (content.Equals("foo")) + { + response.StatusCode = StatusCodes.Status400BadRequest; + } - await response.WriteAsync(responseContent); - }; + await response.WriteAsync(responseContent); + }; - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - HttpContext httpContext = HttpContextHelper.Create("Post", "http://example.com/$batch"); + HttpContext httpContext = HttpContextHelper.Create("Post", "http://example.com/$batch"); - string batchRequest = @"--d3df74a8-8212-4c2a-b4fb-d713a4ba383e + string batchRequest = @"--d3df74a8-8212-4c2a-b4fb-d713a4ba383e Content-Type: application/http Content-Transfer-Encoding: binary Content-ID: 1948857409 @@ -189,54 +189,54 @@ public async Task ProcessBatchAsync_ContinueOnError(bool enableContinueOnError, --d3df74a8-8212-4c2a-b4fb-d713a4ba383e-- "; - byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); - httpContext.Request.Body = new MemoryStream(requestBytes); - httpContext.Request.ContentType = "multipart/mixed;boundary=\"d3df74a8-8212-4c2a-b4fb-d713a4ba383e\""; - httpContext.Request.ContentLength = 827; - httpContext.Request.Method = "Post"; + byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); + httpContext.Request.Body = new MemoryStream(requestBytes); + httpContext.Request.ContentType = "multipart/mixed;boundary=\"d3df74a8-8212-4c2a-b4fb-d713a4ba383e\""; + httpContext.Request.ContentLength = 827; + httpContext.Request.Method = "Post"; - IEdmModel model = new EdmModel(); - httpContext.ODataFeature().RoutePrefix = "odata"; - httpContext.RequestServices = BuildServiceProvider(opt => opt.AddRouteComponents("odata", model).EnableContinueOnErrorHeader = enableContinueOnError); + IEdmModel model = new EdmModel(); + httpContext.ODataFeature().RoutePrefix = "odata"; + httpContext.RequestServices = BuildServiceProvider(opt => opt.AddRouteComponents("odata", model).EnableContinueOnErrorHeader = enableContinueOnError); - if (preferenceHeader != null) - { - httpContext.Request.Headers.Append("prefer", preferenceHeader); - } + if (preferenceHeader != null) + { + httpContext.Request.Headers.Append("prefer", preferenceHeader); + } - httpContext.Response.Body = new MemoryStream(); - batchHandler.PrefixName = "odata"; + httpContext.Response.Body = new MemoryStream(); + batchHandler.PrefixName = "odata"; - // Act - await batchHandler.ProcessBatchAsync(httpContext, handler); - string responseBody = httpContext.Response.ReadBody(); + // Act + await batchHandler.ProcessBatchAsync(httpContext, handler); + string responseBody = httpContext.Response.ReadBody(); - // Assert - Assert.NotNull(responseBody); + // Assert + Assert.NotNull(responseBody); - // #1 response - Assert.Contains("http://example.com/", responseBody); + // #1 response + Assert.Contains("http://example.com/", responseBody); - // #2 bad response - Assert.Contains("Bad Request", responseBody); - Assert.Contains("http://example.com/values,foo", responseBody); + // #2 bad response + Assert.Contains("Bad Request", responseBody); + Assert.Contains("http://example.com/values,foo", responseBody); - // #3 response - if (hasThirdResponse) - { - Assert.Contains("http://example.com/values,bar", responseBody); - } - else - { - Assert.DoesNotContain("http://example.com/values,bar", responseBody); - } + // #3 response + if (hasThirdResponse) + { + Assert.Contains("http://example.com/values,bar", responseBody); } - - [Fact] - public async Task ProcessBatchAsync_DoesNotCopyContentHeadersToGetAndDelete() + else { - // Arrange - string batchRequest = @" + Assert.DoesNotContain("http://example.com/values,bar", responseBody); + } + } + + [Fact] + public async Task ProcessBatchAsync_DoesNotCopyContentHeadersToGetAndDelete() + { + // Arrange + string batchRequest = @" --40e2c6b6-e6ce-43aa-9985-ddc12dc4bb9b Content-Type: application/http Content-Transfer-Encoding: binary @@ -268,115 +268,115 @@ public async Task ProcessBatchAsync_DoesNotCopyContentHeadersToGetAndDelete() --40e2c6b6-e6ce-43aa-9985-ddc12dc4bb9b-- "; - RequestDelegate handler = async context => - { - HttpRequest request = context.Request; - string responseContent = $"{request.Method},{request.ContentLength},{request.ContentType}"; - await context.Response.WriteAsync(responseContent); - }; - - HttpContext httpContext = HttpContextHelper.Create("Post", "http://example.com/$batch"); - byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); - httpContext.Request.Body = new MemoryStream(requestBytes); - httpContext.Request.ContentType = "multipart/mixed;boundary=40e2c6b6-e6ce-43aa-9985-ddc12dc4bb9b"; - httpContext.Request.ContentLength = requestBytes.Length; - - IEdmModel model = new EdmModel(); - httpContext.ODataFeature().RoutePrefix = "odata"; - httpContext.RequestServices = BuildServiceProvider(opt => opt.AddRouteComponents("odata", model)); - - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - httpContext.Response.Body = new MemoryStream(); - batchHandler.PrefixName = "odata"; - - // Act - await batchHandler.ProcessBatchAsync(httpContext, handler); - string responseBody = httpContext.Response.ReadBody(); - - // Assert - Assert.NotNull(responseBody); - Assert.Contains("GET,,", responseBody); - Assert.Contains("DELETE,,", responseBody); - Assert.Contains("POST,3,text/plain; charset=utf-8", responseBody); - } + RequestDelegate handler = async context => + { + HttpRequest request = context.Request; + string responseContent = $"{request.Method},{request.ContentLength},{request.ContentType}"; + await context.Response.WriteAsync(responseContent); + }; + + HttpContext httpContext = HttpContextHelper.Create("Post", "http://example.com/$batch"); + byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); + httpContext.Request.Body = new MemoryStream(requestBytes); + httpContext.Request.ContentType = "multipart/mixed;boundary=40e2c6b6-e6ce-43aa-9985-ddc12dc4bb9b"; + httpContext.Request.ContentLength = requestBytes.Length; + + IEdmModel model = new EdmModel(); + httpContext.ODataFeature().RoutePrefix = "odata"; + httpContext.RequestServices = BuildServiceProvider(opt => opt.AddRouteComponents("odata", model)); + + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + httpContext.Response.Body = new MemoryStream(); + batchHandler.PrefixName = "odata"; + + // Act + await batchHandler.ProcessBatchAsync(httpContext, handler); + string responseBody = httpContext.Response.ReadBody(); + + // Assert + Assert.NotNull(responseBody); + Assert.Contains("GET,,", responseBody); + Assert.Contains("DELETE,,", responseBody); + Assert.Contains("POST,3,text/plain; charset=utf-8", responseBody); + } - [Fact] - public async Task ExecuteRequestMessagesAsync_CallsInvokerForEachRequest() + [Fact] + public async Task ExecuteRequestMessagesAsync_CallsInvokerForEachRequest() + { + // Arrange + RequestDelegate handler = async context => { - // Arrange - RequestDelegate handler = async context => + HttpRequest request = context.Request; + string responseContent = request.GetDisplayUrl(); + string content = request.ReadBody(); + if (!string.IsNullOrEmpty(content)) { - HttpRequest request = context.Request; - string responseContent = request.GetDisplayUrl(); - string content = request.ReadBody(); - if (!string.IsNullOrEmpty(content)) - { - responseContent += "," + content; - } + responseContent += "," + content; + } - context.Response.Body = new MemoryStream(); - await context.Response.WriteAsync(responseContent); - }; + context.Response.Body = new MemoryStream(); + await context.Response.WriteAsync(responseContent); + }; - ODataBatchRequestItem[] requests = new ODataBatchRequestItem[] + ODataBatchRequestItem[] requests = new ODataBatchRequestItem[] + { + new OperationRequestItem(HttpContextHelper.Create("Get", "http://example.com/")), + new ChangeSetRequestItem(new HttpContext[] { - new OperationRequestItem(HttpContextHelper.Create("Get", "http://example.com/")), - new ChangeSetRequestItem(new HttpContext[] - { - HttpContextHelper.Create("Post", "http://example.com/values", "foo", "text/plan") - }) - }; - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + HttpContextHelper.Create("Post", "http://example.com/values", "foo", "text/plan") + }) + }; + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - // Act - IList responses = await batchHandler.ExecuteRequestMessagesAsync(requests, handler); + // Act + IList responses = await batchHandler.ExecuteRequestMessagesAsync(requests, handler); - // Assert - Assert.Equal(2, responses.Count); + // Assert + Assert.Equal(2, responses.Count); - // #1 - OperationResponseItem response0 = Assert.IsType(responses[0]); - Assert.Equal("http://example.com/", response0.Context.Response.ReadBody()); + // #1 + OperationResponseItem response0 = Assert.IsType(responses[0]); + Assert.Equal("http://example.com/", response0.Context.Response.ReadBody()); - // #2 - ChangeSetResponseItem response1 = Assert.IsType(responses[1]); - HttpContext subContext = Assert.Single(response1.Contexts); - Assert.Equal("http://example.com/values,foo", response1.Contexts.First().Response.ReadBody()); - } + // #2 + ChangeSetResponseItem response1 = Assert.IsType(responses[1]); + HttpContext subContext = Assert.Single(response1.Contexts); + Assert.Equal("http://example.com/values,foo", response1.Contexts.First().Response.ReadBody()); + } - private static IServiceProvider BuildServiceProvider(Action setupAction) - { - IServiceCollection services = new ServiceCollection(); - services.Configure(setupAction); - return services.BuildServiceProvider(); - } + private static IServiceProvider BuildServiceProvider(Action setupAction) + { + IServiceCollection services = new ServiceCollection(); + services.Configure(setupAction); + return services.BuildServiceProvider(); + } - [Fact] - public async Task ExecuteRequestMessagesAsync_Throws_IfRequestsIsNull() - { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + [Fact] + public async Task ExecuteRequestMessagesAsync_Throws_IfRequestsIsNull() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.ExecuteRequestMessagesAsync(null, null), "requests"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.ExecuteRequestMessagesAsync(null, null), "requests"); + } - [Fact] - public async Task ParseBatchRequestsAsync_Throws_IfRequestIsNull() - { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + [Fact] + public async Task ParseBatchRequestsAsync_Throws_IfRequestIsNull() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.ParseBatchRequestsAsync(null), "context"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.ParseBatchRequestsAsync(null), "context"); + } - [Fact] - public async Task ParseBatchRequestsAsync_Returns_RequestsFromMultipartContent() - { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - string batchRequest = @" + [Fact] + public async Task ParseBatchRequestsAsync_Returns_RequestsFromMultipartContent() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + string batchRequest = @" --7289e6c2-adbd-4dd8-bfb1-f36099442947 Content-Type: application/http Content-Transfer-Encoding: binary @@ -403,40 +403,40 @@ public async Task ParseBatchRequestsAsync_Returns_RequestsFromMultipartContent() --7289e6c2-adbd-4dd8-bfb1-f36099442947-- "; - HttpContext httpContext = HttpContextHelper.Create("Post", "http://example.com/$batch"); - byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); - httpContext.Request.Body = new MemoryStream(requestBytes); - httpContext.Request.ContentType = "multipart/mixed;boundary=7289e6c2-adbd-4dd8-bfb1-f36099442947"; - httpContext.Request.ContentLength = requestBytes.Length; + HttpContext httpContext = HttpContextHelper.Create("Post", "http://example.com/$batch"); + byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); + httpContext.Request.Body = new MemoryStream(requestBytes); + httpContext.Request.ContentType = "multipart/mixed;boundary=7289e6c2-adbd-4dd8-bfb1-f36099442947"; + httpContext.Request.ContentLength = requestBytes.Length; - IEdmModel model = new EdmModel(); - httpContext.ODataFeature().RoutePrefix = "odata"; - httpContext.RequestServices = BuildServiceProvider(opt => opt.AddRouteComponents("odata", model)); - httpContext.Response.Body = new MemoryStream(); - batchHandler.PrefixName = "odata"; + IEdmModel model = new EdmModel(); + httpContext.ODataFeature().RoutePrefix = "odata"; + httpContext.RequestServices = BuildServiceProvider(opt => opt.AddRouteComponents("odata", model)); + httpContext.Response.Body = new MemoryStream(); + batchHandler.PrefixName = "odata"; - // Act - IList requests = await batchHandler.ParseBatchRequestsAsync(httpContext); + // Act + IList requests = await batchHandler.ParseBatchRequestsAsync(httpContext); - // Assert - Assert.Equal(2, requests.Count); + // Assert + Assert.Equal(2, requests.Count); - var operationContext = Assert.IsType(requests[0]).Context; - Assert.Equal("GET", operationContext.Request.Method); - Assert.Equal("http://example.com/", operationContext.Request.GetDisplayUrl()); + var operationContext = Assert.IsType(requests[0]).Context; + Assert.Equal("GET", operationContext.Request.Method); + Assert.Equal("http://example.com/", operationContext.Request.GetDisplayUrl()); - var changeSetContexts = Assert.IsType(requests[1]).Contexts; - var changeSetContext = Assert.Single(changeSetContexts); - Assert.Equal("POST", changeSetContext.Request.Method); - Assert.Equal("http://example.com/values", changeSetContext.Request.GetDisplayUrl()); - } + var changeSetContexts = Assert.IsType(requests[1]).Contexts; + var changeSetContext = Assert.Single(changeSetContexts); + Assert.Equal("POST", changeSetContext.Request.Method); + Assert.Equal("http://example.com/values", changeSetContext.Request.GetDisplayUrl()); + } - [Fact] - public async Task ParseBatchRequestsAsync_CopiesPropertiesFromRequest_WithoutExcludedProperties() - { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - string batchRequest = @" + [Fact] + public async Task ParseBatchRequestsAsync_CopiesPropertiesFromRequest_WithoutExcludedProperties() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + string batchRequest = @" --7289e6c2-adbd-4dd8-bfb1-f36099442947 Content-Type: application/http Content-Transfer-Encoding: binary @@ -462,163 +462,163 @@ public async Task ParseBatchRequestsAsync_CopiesPropertiesFromRequest_WithoutExc --7289e6c2-adbd-4dd8-bfb1-f36099442947-- "; - HttpContext httpContext = HttpContextHelper.Create("Post", "http://example.com/$batch"); - byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); - httpContext.Request.Body = new MemoryStream(requestBytes); - httpContext.Request.ContentType = "multipart/mixed;boundary=7289e6c2-adbd-4dd8-bfb1-f36099442947"; - httpContext.Request.ContentLength = requestBytes.Length; - - IEdmModel model = new EdmModel(); - httpContext.ODataFeature().RoutePrefix = "odata"; - httpContext.RequestServices = BuildServiceProvider(opt => opt.AddRouteComponents("odata", model)); - httpContext.Response.Body = new MemoryStream(); - batchHandler.PrefixName = "odata"; - - httpContext.Features[typeof(DefaultODataBatchHandlerTest)] = "bar"; - - // Act - IList requests = await batchHandler.ParseBatchRequestsAsync(httpContext); - - // Assert - Assert.Equal(2, requests.Count); - - var operationContext = ((OperationRequestItem)requests[0]).Context; - Assert.Equal("GET", operationContext.Request.Method); - Assert.Equal("http://example.com/", operationContext.Request.GetDisplayUrl()); - Assert.Equal("bar", operationContext.Features[typeof(DefaultODataBatchHandlerTest)]); - - var changeSetContext = ((ChangeSetRequestItem)requests[1]).Contexts.First(); - Assert.Equal("POST", changeSetContext.Request.Method); - Assert.Equal("http://example.com/values", changeSetContext.Request.GetDisplayUrl()); - Assert.Equal("bar", operationContext.Features[typeof(DefaultODataBatchHandlerTest)]); - } - - [Fact] - public void ValidateRequest_Throws_IfRequestIsNull() - { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => batchHandler.ValidateRequest(null), "request"); - } - - [Fact] - public async Task ValidateRequest_GetBadResponse_IfRequestContentIsNull() - { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - request.Body = null; - context.Response.Body = new MemoryStream(); - - // Act - await batchHandler.ValidateRequest(request); - - // Assert - Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); - Assert.Equal("The 'Body' property on the batch request cannot be null.", context.Response.ReadBody()); - } - - [Fact] - public async Task ValidateRequest_GetBadResponse_IfRequestContentTypeIsNull() - { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - request.Body = new MemoryStream(); - request.ContentType = null; - context.Response.Body = new MemoryStream(); - - // Act - await batchHandler.ValidateRequest(request); - - // Assert - Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); - Assert.Equal("The batch request must have a \"Content-Type\" header.", context.Response.ReadBody()); - } + HttpContext httpContext = HttpContextHelper.Create("Post", "http://example.com/$batch"); + byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); + httpContext.Request.Body = new MemoryStream(requestBytes); + httpContext.Request.ContentType = "multipart/mixed;boundary=7289e6c2-adbd-4dd8-bfb1-f36099442947"; + httpContext.Request.ContentLength = requestBytes.Length; + + IEdmModel model = new EdmModel(); + httpContext.ODataFeature().RoutePrefix = "odata"; + httpContext.RequestServices = BuildServiceProvider(opt => opt.AddRouteComponents("odata", model)); + httpContext.Response.Body = new MemoryStream(); + batchHandler.PrefixName = "odata"; + + httpContext.Features[typeof(DefaultODataBatchHandlerTest)] = "bar"; + + // Act + IList requests = await batchHandler.ParseBatchRequestsAsync(httpContext); + + // Assert + Assert.Equal(2, requests.Count); + + var operationContext = ((OperationRequestItem)requests[0]).Context; + Assert.Equal("GET", operationContext.Request.Method); + Assert.Equal("http://example.com/", operationContext.Request.GetDisplayUrl()); + Assert.Equal("bar", operationContext.Features[typeof(DefaultODataBatchHandlerTest)]); + + var changeSetContext = ((ChangeSetRequestItem)requests[1]).Contexts.First(); + Assert.Equal("POST", changeSetContext.Request.Method); + Assert.Equal("http://example.com/values", changeSetContext.Request.GetDisplayUrl()); + Assert.Equal("bar", operationContext.Features[typeof(DefaultODataBatchHandlerTest)]); + } - [Fact] - public async Task ValidateRequest_Throws_IfRequestMediaTypeIsWrong() - { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - HttpContext context = new DefaultHttpContext(); - context.Request.Body = new MemoryStream(); - context.Request.ContentType = "text/json"; - context.Response.Body = new MemoryStream(); + [Fact] + public void ValidateRequest_Throws_IfRequestIsNull() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - // Act - await batchHandler.ValidateRequest(context.Request); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => batchHandler.ValidateRequest(null), "request"); + } - // Assert - Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); - Assert.Equal("The batch request must have 'multipart/mixed' or 'application/json' as the media type.", context.Response.ReadBody()); - } + [Fact] + public async Task ValidateRequest_GetBadResponse_IfRequestContentIsNull() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; + request.Body = null; + context.Response.Body = new MemoryStream(); + + // Act + await batchHandler.ValidateRequest(request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + Assert.Equal("The 'Body' property on the batch request cannot be null.", context.Response.ReadBody()); + } - [Fact] - public async Task ValidateRequest_Throws_IfRequestContentTypeDoesNotHaveBoundary() - { - // Arrange - DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); - HttpContext context = new DefaultHttpContext(); - context.Request.Body = new MemoryStream(); - context.Request.ContentType = "multipart/mixed"; - context.Response.Body = new MemoryStream(); + [Fact] + public async Task ValidateRequest_GetBadResponse_IfRequestContentTypeIsNull() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; + request.Body = new MemoryStream(); + request.ContentType = null; + context.Response.Body = new MemoryStream(); + + // Act + await batchHandler.ValidateRequest(request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + Assert.Equal("The batch request must have a \"Content-Type\" header.", context.Response.ReadBody()); + } - // Act - await batchHandler.ValidateRequest(context.Request); + [Fact] + public async Task ValidateRequest_Throws_IfRequestMediaTypeIsWrong() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + HttpContext context = new DefaultHttpContext(); + context.Request.Body = new MemoryStream(); + context.Request.ContentType = "text/json"; + context.Response.Body = new MemoryStream(); + + // Act + await batchHandler.ValidateRequest(context.Request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + Assert.Equal("The batch request must have 'multipart/mixed' or 'application/json' as the media type.", context.Response.ReadBody()); + } - // Assert - Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); - Assert.Equal("The batch request must have a boundary specification in the \"Content-Type\" header.", context.Response.ReadBody()); - } + [Fact] + public async Task ValidateRequest_Throws_IfRequestContentTypeDoesNotHaveBoundary() + { + // Arrange + DefaultODataBatchHandler batchHandler = new DefaultODataBatchHandler(); + HttpContext context = new DefaultHttpContext(); + context.Request.Body = new MemoryStream(); + context.Request.ContentType = "multipart/mixed"; + context.Response.Body = new MemoryStream(); + + // Act + await batchHandler.ValidateRequest(context.Request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + Assert.Equal("The batch request must have a boundary specification in the \"Content-Type\" header.", context.Response.ReadBody()); + } - [Fact] - public async Task SendAsync_Works_ForBatchRequestWithInsertedEntityReferencedInAnotherRequest() - { - // Arrange - var builder = new WebHostBuilder() - .ConfigureServices(services => - { - services.ConfigureControllers(typeof(BatchTestCustomersController), typeof(BatchTestOrdersController)); - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("BatchTestCustomers"); - builder.EntitySet("BatchTestOrders"); - IEdmModel model = builder.GetEdmModel(); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model, new DefaultODataBatchHandler()).Expand()); - }) - .Configure(app => + [Fact] + public async Task SendAsync_Works_ForBatchRequestWithInsertedEntityReferencedInAnotherRequest() + { + // Arrange + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.ConfigureControllers(typeof(BatchTestCustomersController), typeof(BatchTestOrdersController)); + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("BatchTestCustomers"); + builder.EntitySet("BatchTestOrders"); + IEdmModel model = builder.GetEdmModel(); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model, new DefaultODataBatchHandler()).Expand()); + }) + .Configure(app => + { + app.UseODataBatching(); + app.UseRouting(); + app.UseEndpoints(endpoints => { - app.UseODataBatching(); - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); + endpoints.MapControllers(); }); + }); - var server = new TestServer(builder); + var server = new TestServer(builder); - const string acceptJsonFullMetadata = "application/json;odata.metadata=minimal"; - const string acceptJson = "application/json"; + const string acceptJsonFullMetadata = "application/json;odata.metadata=minimal"; + const string acceptJson = "application/json"; - var client = server.CreateClient(); + var client = server.CreateClient(); - var endpoint = "http://localhost/odata"; + var endpoint = "http://localhost/odata"; - var batchRef = $"batch_{Guid.NewGuid()}"; - var changesetRef = $"changeset_{Guid.NewGuid()}"; + var batchRef = $"batch_{Guid.NewGuid()}"; + var changesetRef = $"changeset_{Guid.NewGuid()}"; - var orderId = 2; - var createOrderPayload = $@"{{""@odata.type"":""Microsoft.AspNetCore.OData.Test.Batch.BatchTestOrder"",""Id"":{orderId},""Amount"":50}}"; - var createRefPayload = @"{""@odata.id"":""$3""}"; + var orderId = 2; + var createOrderPayload = $@"{{""@odata.type"":""Microsoft.AspNetCore.OData.Test.Batch.BatchTestOrder"",""Id"":{orderId},""Amount"":50}}"; + var createRefPayload = @"{""@odata.id"":""$3""}"; - var batchRequest = new HttpRequestMessage(HttpMethod.Post, $"{endpoint}/$batch"); - batchRequest.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); - StringContent httpContent = new StringContent($@" + var batchRequest = new HttpRequestMessage(HttpMethod.Post, $"{endpoint}/$batch"); + batchRequest.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); + StringContent httpContent = new StringContent($@" --{batchRef} Content-Type: multipart/mixed; boundary={changesetRef} @@ -652,169 +652,169 @@ public async Task SendAsync_Works_ForBatchRequestWithInsertedEntityReferencedInA --{batchRef}-- "); - httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse($"multipart/mixed; boundary={batchRef}"); - batchRequest.Content = httpContent; + httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse($"multipart/mixed; boundary={batchRef}"); + batchRequest.Content = httpContent; - // Act - var response = await client.SendAsync(batchRequest); + // Act + var response = await client.SendAsync(batchRequest); - // Assert - ExceptionAssert.DoesNotThrow(() => response.EnsureSuccessStatusCode()); + // Assert + ExceptionAssert.DoesNotThrow(() => response.EnsureSuccessStatusCode()); - HttpRequestMessage customerRequest = new HttpRequestMessage(HttpMethod.Get, $"{endpoint}/BatchTestCustomers(2)?$expand=Orders"); - customerRequest.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptJson)); + HttpRequestMessage customerRequest = new HttpRequestMessage(HttpMethod.Get, $"{endpoint}/BatchTestCustomers(2)?$expand=Orders"); + customerRequest.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(acceptJson)); - var customerResponse = client.SendAsync(customerRequest).Result; - var objAsJsonString = await customerResponse.Content.ReadAsStringAsync(); - var customer = JsonConvert.DeserializeObject(objAsJsonString); + var customerResponse = client.SendAsync(customerRequest).Result; + var objAsJsonString = await customerResponse.Content.ReadAsStringAsync(); + var customer = JsonConvert.DeserializeObject(objAsJsonString); - Assert.NotNull(customer.Orders?.SingleOrDefault(d => d.Id.Equals(orderId))); - } + Assert.NotNull(customer.Orders?.SingleOrDefault(d => d.Id.Equals(orderId))); + } - public static readonly TheoryDataSet, string, string, string> _batchHeadersTestData = new TheoryDataSet, string, string, string>() + public static readonly TheoryDataSet, string, string, string> _batchHeadersTestData = new TheoryDataSet, string, string, string>() + { { + // should not copy over content type and content length headers to individual request + Enumerable.Empty(), + "GET,ContentType=,ContentLength=,Prefer=", + "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient", + "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=" + }, + { + // should not copy over preferences that should not be inherited + new [] { - // should not copy over content type and content length headers to individual request - Enumerable.Empty(), - "GET,ContentType=,ContentLength=,Prefer=", - "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient", - "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=" + "respond-async, odata.continue-on-error" }, + "GET,ContentType=,ContentLength=,Prefer=", + "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient", + "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=" + }, + { + // should not concatenate preferences that should not be inherited + new [] { - // should not copy over preferences that should not be inherited - new [] - { - "respond-async, odata.continue-on-error" - }, - "GET,ContentType=,ContentLength=,Prefer=", - "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient", - "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=" + "wait=100,handling=lenient" }, + "GET,ContentType=,ContentLength=,Prefer=", + "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient", + "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=" + }, + { + // inheritable preferences should be copied over + // and combined with the individual request's own preferences if any + new [] { - // should not concatenate preferences that should not be inherited - new [] - { - "wait=100,handling=lenient" - }, - "GET,ContentType=,ContentLength=,Prefer=", - "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient", - "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=" + "allow-entityreferences, include-annotations=\"display.*\"" }, + "GET,ContentType=,ContentLength=,Prefer=allow-entityreferences,include-annotations=\\\"display.*\\\"", + "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient,allow-entityreferences,include-annotations=\\\"display.*\\\"", + "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=allow-entityreferences,include-annotations=\\\"display.*\\\"" + }, + { + // if batch Prefer header contains both inheritable and non-inheritable preferences, + // the non-inheritable ones should be removed before merging with individual request's own preferences + new [] { - // inheritable preferences should be copied over - // and combined with the individual request's own preferences if any - new [] - { - "allow-entityreferences, include-annotations=\"display.*\"" - }, - "GET,ContentType=,ContentLength=,Prefer=allow-entityreferences,include-annotations=\\\"display.*\\\"", - "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient,allow-entityreferences,include-annotations=\\\"display.*\\\"", - "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=allow-entityreferences,include-annotations=\\\"display.*\\\"" + "allow-entityreferences, respond-async, include-annotations=\"display.*\", continue-on-error" }, + "GET,ContentType=,ContentLength=,Prefer=allow-entityreferences,include-annotations=\\\"display.*\\\"", + "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient,allow-entityreferences,include-annotations=\\\"display.*\\\"", + "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=allow-entityreferences,include-annotations=\\\"display.*\\\"" + }, + { + // if batch and individual request define the same preference, then the one from the individual request should be retained + new [] { - // if batch Prefer header contains both inheritable and non-inheritable preferences, - // the non-inheritable ones should be removed before merging with individual request's own preferences - new [] - { - "allow-entityreferences, respond-async, include-annotations=\"display.*\", continue-on-error" - }, - "GET,ContentType=,ContentLength=,Prefer=allow-entityreferences,include-annotations=\\\"display.*\\\"", - "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient,allow-entityreferences,include-annotations=\\\"display.*\\\"", - "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=allow-entityreferences,include-annotations=\\\"display.*\\\"" + "allow-entityreferences, respond-async, include-annotations=\"display.*\", continue-on-error, wait=200" }, + "GET,ContentType=,ContentLength=,Prefer=allow-entityreferences,include-annotations=\\\"display.*\\\",wait=200", + "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient,allow-entityreferences,include-annotations=\\\"display.*\\\"", + "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=allow-entityreferences,include-annotations=\\\"display.*\\\",wait=200" + }, + { + // should correctly handle preferences that contain parameters + new [] { - // if batch and individual request define the same preference, then the one from the individual request should be retained - new [] - { - "allow-entityreferences, respond-async, include-annotations=\"display.*\", continue-on-error, wait=200" - }, - "GET,ContentType=,ContentLength=,Prefer=allow-entityreferences,include-annotations=\\\"display.*\\\",wait=200", - "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient,allow-entityreferences,include-annotations=\\\"display.*\\\"", - "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=allow-entityreferences,include-annotations=\\\"display.*\\\",wait=200" + "allow-entityreferences, respond-async, foo; param=paramValue,include-annotations=\"display.*\", continue-on-error, wait=200" }, + "GET,ContentType=,ContentLength=,Prefer=allow-entityreferences,foo; param=paramValue,include-annotations=\\\"display.*\\\",wait=200", + "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient,allow-entityreferences,foo; param=paramValue,include-annotations=\\\"display.*\\\"", + "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=allow-entityreferences,foo; param=paramValue,include-annotations=\\\"display.*\\\",wait=200" + }, + { + // should correctly parse preferences with commas in their quoted values + new [] { - // should correctly handle preferences that contain parameters - new [] - { - "allow-entityreferences, respond-async, foo; param=paramValue,include-annotations=\"display.*\", continue-on-error, wait=200" - }, - "GET,ContentType=,ContentLength=,Prefer=allow-entityreferences,foo; param=paramValue,include-annotations=\\\"display.*\\\",wait=200", - "DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient,allow-entityreferences,foo; param=paramValue,include-annotations=\\\"display.*\\\"", - "POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=allow-entityreferences,foo; param=paramValue,include-annotations=\\\"display.*\\\",wait=200" + @"allow-entityreferences, respond-async, include-annotations=""display.*,foo"", continue-on-error, wait=""200,\""300""" }, + @"GET,ContentType=,ContentLength=,Prefer=allow-entityreferences,include-annotations=\""display.*,foo\"",wait=\""200,\\\""300\""", + @"DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient,allow-entityreferences,include-annotations=\""display.*,foo\""", + @"POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=allow-entityreferences,include-annotations=\""display.*,foo\"",wait=\""200,\\\""300\""" + }, + { + // should correctly handle batch request with multiple Prefer headers and should not copy duplicate references + new [] { - // should correctly parse preferences with commas in their quoted values - new [] - { - @"allow-entityreferences, respond-async, include-annotations=""display.*,foo"", continue-on-error, wait=""200,\""300""" - }, - @"GET,ContentType=,ContentLength=,Prefer=allow-entityreferences,include-annotations=\""display.*,foo\"",wait=\""200,\\\""300\""", - @"DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient,allow-entityreferences,include-annotations=\""display.*,foo\""", - @"POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=allow-entityreferences,include-annotations=\""display.*,foo\"",wait=\""200,\\\""300\""" + @"allow-entityreferences, respond-async, wait=300", + @"continue-on-error, wait=250, include-annotations=display" }, + @"GET,ContentType=,ContentLength=,Prefer=allow-entityreferences,wait=300,include-annotations=display", + @"DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient,allow-entityreferences,include-annotations=display", + @"POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=allow-entityreferences,wait=300,include-annotations=display" + } + }; + + [Theory] + [MemberData(nameof(_batchHeadersTestData))] + public async Task SendAsync_CorrectlyCopiesHeadersToIndividualRequests( + IEnumerable batchPreferHeaderValues, + string getRequest, + string deleteRequest, + string postRequest) + { + var batchRef = $"batch_{Guid.NewGuid()}"; + var changesetRef = $"changeset_{Guid.NewGuid()}"; + var endpoint = "http://localhost/odata"; + var acceptJsonFullMetadata = "application/json;odata.metadata=minimal"; + var postPayload = "Bar"; + + Type[] controllers = new[] { typeof(BatchTestHeadersCustomersController) }; + var builder = new WebHostBuilder() + .ConfigureServices(services => { - // should correctly handle batch request with multiple Prefer headers and should not copy duplicate references - new [] - { - @"allow-entityreferences, respond-async, wait=300", - @"continue-on-error, wait=250, include-annotations=display" - }, - @"GET,ContentType=,ContentLength=,Prefer=allow-entityreferences,wait=300,include-annotations=display", - @"DELETE,ContentType=,ContentLength=,Prefer=wait=100,handling=lenient,allow-entityreferences,include-annotations=display", - @"POST,ContentType=text/plain; charset=utf-8,ContentLength=3,Prefer=allow-entityreferences,wait=300,include-annotations=display" - } - }; + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("BatchTestHeadersCustomers"); + IEdmModel model = builder.GetEdmModel(); + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model, new DefaultODataBatchHandler()).Expand()); + }) + .Configure(app => + { + ApplicationPartManager applicationPartManager = app.ApplicationServices.GetRequiredService(); + applicationPartManager.ApplicationParts.Clear(); - [Theory] - [MemberData(nameof(_batchHeadersTestData))] - public async Task SendAsync_CorrectlyCopiesHeadersToIndividualRequests( - IEnumerable batchPreferHeaderValues, - string getRequest, - string deleteRequest, - string postRequest) - { - var batchRef = $"batch_{Guid.NewGuid()}"; - var changesetRef = $"changeset_{Guid.NewGuid()}"; - var endpoint = "http://localhost/odata"; - var acceptJsonFullMetadata = "application/json;odata.metadata=minimal"; - var postPayload = "Bar"; - - Type[] controllers = new[] { typeof(BatchTestHeadersCustomersController) }; - var builder = new WebHostBuilder() - .ConfigureServices(services => + if (controllers != null) { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("BatchTestHeadersCustomers"); - IEdmModel model = builder.GetEdmModel(); - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", model, new DefaultODataBatchHandler()).Expand()); - }) - .Configure(app => + AssemblyPart part = new AssemblyPart(new MockAssembly(controllers)); + applicationPartManager.ApplicationParts.Add(part); + } + + app.UseODataBatching(); + app.UseRouting(); + app.UseEndpoints(endpoints => { - ApplicationPartManager applicationPartManager = app.ApplicationServices.GetRequiredService(); - applicationPartManager.ApplicationParts.Clear(); - - if (controllers != null) - { - AssemblyPart part = new AssemblyPart(new MockAssembly(controllers)); - applicationPartManager.ApplicationParts.Add(part); - } - - app.UseODataBatching(); - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); + endpoints.MapControllers(); }); + }); - var server = new TestServer(builder); - var client = server.CreateClient(); + var server = new TestServer(builder); + var client = server.CreateClient(); - var batchRequest = new HttpRequestMessage(HttpMethod.Post, $"{endpoint}/$batch"); - batchRequest.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); - batchRequest.Headers.Add("Prefer", batchPreferHeaderValues); + var batchRequest = new HttpRequestMessage(HttpMethod.Post, $"{endpoint}/$batch"); + batchRequest.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("multipart/mixed")); + batchRequest.Headers.Add("Prefer", batchPreferHeaderValues); - var batchContent = $@" + var batchContent = $@" --{batchRef} Content-Type: application/http Content-Transfer-Encoding: binary @@ -854,68 +854,68 @@ public async Task SendAsync_CorrectlyCopiesHeadersToIndividualRequests( --{batchRef}-- "; - var httpContent = new StringContent(batchContent); - httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse($"multipart/mixed; boundary={batchRef}"); - httpContent.Headers.ContentLength = batchContent.Length; - batchRequest.Content = httpContent; - var response = await client.SendAsync(batchRequest); - - ExceptionAssert.DoesNotThrow(() => response.EnsureSuccessStatusCode()); - var responseContent = await response.Content.ReadAsStringAsync(); - Assert.Contains(getRequest, responseContent); - Assert.Contains(deleteRequest, responseContent); - Assert.Contains(postRequest, responseContent); - } + var httpContent = new StringContent(batchContent); + httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse($"multipart/mixed; boundary={batchRef}"); + httpContent.Headers.ContentLength = batchContent.Length; + batchRequest.Content = httpContent; + var response = await client.SendAsync(batchRequest); + + ExceptionAssert.DoesNotThrow(() => response.EnsureSuccessStatusCode()); + var responseContent = await response.Content.ReadAsStringAsync(); + Assert.Contains(getRequest, responseContent); + Assert.Contains(deleteRequest, responseContent); + Assert.Contains(postRequest, responseContent); + } - [Fact] - public async Task SendAsync_CorrectlyHandlesCookieHeader() - { - var batchRef = $"batch_{Guid.NewGuid()}"; - var changesetRef = $"changeset_{Guid.NewGuid()}"; - var endpoint = "http://localhost"; + [Fact] + public async Task SendAsync_CorrectlyHandlesCookieHeader() + { + var batchRef = $"batch_{Guid.NewGuid()}"; + var changesetRef = $"changeset_{Guid.NewGuid()}"; + var endpoint = "http://localhost"; - Type[] controllers = new[] { typeof(BatchTestCustomersController), typeof(BatchTestOrdersController) }; + Type[] controllers = new[] { typeof(BatchTestCustomersController), typeof(BatchTestOrdersController) }; - var builder = new WebHostBuilder() - .ConfigureServices(services => + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("BatchTestOrders"); + IEdmModel model = builder.GetEdmModel(); + services.AddControllers().AddOData(opt => opt.AddRouteComponents(model, new DefaultODataBatchHandler()).Expand()); + }) + .Configure(app => + { + ApplicationPartManager applicationPartManager = app.ApplicationServices.GetRequiredService(); + applicationPartManager.ApplicationParts.Clear(); + + if (controllers != null) { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("BatchTestOrders"); - IEdmModel model = builder.GetEdmModel(); - services.AddControllers().AddOData(opt => opt.AddRouteComponents(model, new DefaultODataBatchHandler()).Expand()); - }) - .Configure(app => + AssemblyPart part = new AssemblyPart(new MockAssembly(controllers)); + applicationPartManager.ApplicationParts.Add(part); + } + + app.UseODataBatching(); + app.UseRouting(); + app.UseEndpoints(endpoints => { - ApplicationPartManager applicationPartManager = app.ApplicationServices.GetRequiredService(); - applicationPartManager.ApplicationParts.Clear(); - - if (controllers != null) - { - AssemblyPart part = new AssemblyPart(new MockAssembly(controllers)); - applicationPartManager.ApplicationParts.Add(part); - } - - app.UseODataBatching(); - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); + endpoints.MapControllers(); }); + }); - var server = new TestServer(builder); - var client = server.CreateClient(); + var server = new TestServer(builder); + var client = server.CreateClient(); - var orderId = 2; - var createOrderPayload = $@"{{""@odata.type"":""Microsoft.AspNetCore.OData.Test.Batch.BatchTestOrder"",""Id"":{orderId},""Amount"":50}}"; + var orderId = 2; + var createOrderPayload = $@"{{""@odata.type"":""Microsoft.AspNetCore.OData.Test.Batch.BatchTestOrder"",""Id"":{orderId},""Amount"":50}}"; - var batchRequest = new HttpRequestMessage(HttpMethod.Post, $"{endpoint}/$batch"); - batchRequest.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/plain")); + var batchRequest = new HttpRequestMessage(HttpMethod.Post, $"{endpoint}/$batch"); + batchRequest.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/plain")); - // Add cookie (for example IdentityServer adds antiforgery after login) - batchRequest.Headers.TryAddWithoutValidation("Cookie", ".AspNetCore.Antiforgery.9TtSrW0hzOs=" + Guid.NewGuid()); + // Add cookie (for example IdentityServer adds antiforgery after login) + batchRequest.Headers.TryAddWithoutValidation("Cookie", ".AspNetCore.Antiforgery.9TtSrW0hzOs=" + Guid.NewGuid()); - var batchContent = $@" + var batchContent = $@" --{batchRef} Content-Type: multipart/mixed;boundary={changesetRef} @@ -941,171 +941,170 @@ public async Task SendAsync_CorrectlyHandlesCookieHeader() --{batchRef}-- "; - var httpContent = new StringContent(batchContent); - httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse($"multipart/mixed;boundary={batchRef}"); - httpContent.Headers.ContentLength = batchContent.Length; - batchRequest.Content = httpContent; - var response = await client.SendAsync(batchRequest); + var httpContent = new StringContent(batchContent); + httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse($"multipart/mixed;boundary={batchRef}"); + httpContent.Headers.ContentLength = batchContent.Length; + batchRequest.Content = httpContent; + var response = await client.SendAsync(batchRequest); - ExceptionAssert.DoesNotThrow(() => response.EnsureSuccessStatusCode()); + ExceptionAssert.DoesNotThrow(() => response.EnsureSuccessStatusCode()); - // TODO: assert somehow? - } + // TODO: assert somehow? + } - private static IServiceProvider BuildServiceProvider() - { - IServiceCollection services = new ServiceCollection(); + private static IServiceProvider BuildServiceProvider() + { + IServiceCollection services = new ServiceCollection(); - services.AddSingleton(new ODataMessageWriterSettings - { - EnableMessageStreamDisposal = false, - MessageQuotas = new ODataMessageQuotas { MaxReceivedMessageSize = Int64.MaxValue }, - }); + services.AddSingleton(new ODataMessageWriterSettings + { + EnableMessageStreamDisposal = false, + MessageQuotas = new ODataMessageQuotas { MaxReceivedMessageSize = Int64.MaxValue }, + }); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - return services.BuildServiceProvider(); - } + return services.BuildServiceProvider(); } +} - public class BatchTestCustomer - { - private static Lazy> _customers = - new Lazy>(() => - { - BatchTestCustomer customer01 = new BatchTestCustomer { Id = 1, Name = "Customer 01" }; - customer01.Orders = new List { BatchTestOrder.Orders.SingleOrDefault(d => d.Id.Equals(1)) }; +public class BatchTestCustomer +{ + private static Lazy> _customers = + new Lazy>(() => + { + BatchTestCustomer customer01 = new BatchTestCustomer { Id = 1, Name = "Customer 01" }; + customer01.Orders = new List { BatchTestOrder.Orders.SingleOrDefault(d => d.Id.Equals(1)) }; - BatchTestCustomer customer02 = new BatchTestCustomer { Id = 2, Name = "Customer 02" }; + BatchTestCustomer customer02 = new BatchTestCustomer { Id = 2, Name = "Customer 02" }; - return new List { customer01, customer02 }; - }); + return new List { customer01, customer02 }; + }); - public static IList Customers + public static IList Customers + { + get { - get - { - return _customers.Value; - } + return _customers.Value; } - - public int Id { get; set; } - public string Name { get; set; } - public virtual IList Orders { get; set; } } - public class BatchTestOrder - { - private static Lazy> _orders = - new Lazy>(() => - { - BatchTestOrder order01 = new BatchTestOrder { Id = 1, Amount = 100 }; - - return new List { order01 }; - }); + public int Id { get; set; } + public string Name { get; set; } + public virtual IList Orders { get; set; } +} - public static IList Orders +public class BatchTestOrder +{ + private static Lazy> _orders = + new Lazy>(() => { - get - { - return _orders.Value; - } - } + BatchTestOrder order01 = new BatchTestOrder { Id = 1, Amount = 100 }; - public int Id { get; set; } - public decimal Amount { get; set; } - } + return new List { order01 }; + }); - public class BatchTestCustomersController : ODataController + public static IList Orders { - [EnableQuery] - public IEnumerable Get() + get { - return BatchTestCustomer.Customers; - } - - [EnableQuery] - public SingleResult Get([FromODataUri] int key) - { - return SingleResult.Create(BatchTestCustomer.Customers.Where(d => d.Id.Equals(key)).AsQueryable()); + return _orders.Value; } + } - public IActionResult CreateRef([FromODataUri] int key, [FromODataUri] string navigationProperty, [FromBody] Uri link) - { - var customer = BatchTestCustomer.Customers.FirstOrDefault(d => d.Id.Equals(key)); - if (customer == null) - return NotFound(); + public int Id { get; set; } + public decimal Amount { get; set; } +} - switch (navigationProperty) - { - case "Orders": - var orderId = Request.GetKeyFromLinkUri(link); - var order = BatchTestOrder.Orders.FirstOrDefault(d => d.Id.Equals(orderId)); - - if (order == null) - return NotFound(); - - if (customer.Orders == null) - customer.Orders = new List(); - if (customer.Orders.FirstOrDefault(d => d.Id.Equals(orderId)) == null) - customer.Orders.Add(order); - break; - default: - return BadRequest(); - } +public class BatchTestCustomersController : ODataController +{ + [EnableQuery] + public IEnumerable Get() + { + return BatchTestCustomer.Customers; + } - return NoContent(); - } + [EnableQuery] + public SingleResult Get([FromODataUri] int key) + { + return SingleResult.Create(BatchTestCustomer.Customers.Where(d => d.Id.Equals(key)).AsQueryable()); } - public class BatchTestOrdersController : ODataController + public IActionResult CreateRef([FromODataUri] int key, [FromODataUri] string navigationProperty, [FromBody] Uri link) { - [EnableQuery] - public IEnumerable Get() - { - return BatchTestOrder.Orders; - } + var customer = BatchTestCustomer.Customers.FirstOrDefault(d => d.Id.Equals(key)); + if (customer == null) + return NotFound(); - [EnableQuery] - public SingleResult Get([FromODataUri] int key) + switch (navigationProperty) { - return SingleResult.Create(BatchTestOrder.Orders.Where(d => d.Id.Equals(key)).AsQueryable()); + case "Orders": + var orderId = Request.GetKeyFromLinkUri(link); + var order = BatchTestOrder.Orders.FirstOrDefault(d => d.Id.Equals(orderId)); + + if (order == null) + return NotFound(); + + if (customer.Orders == null) + customer.Orders = new List(); + if (customer.Orders.FirstOrDefault(d => d.Id.Equals(orderId)) == null) + customer.Orders.Add(order); + break; + default: + return BadRequest(); } - public IActionResult Post([FromBody] BatchTestOrder order) - { - BatchTestOrder.Orders.Add(order); + return NoContent(); + } +} - return Created(order); - } +public class BatchTestOrdersController : ODataController +{ + [EnableQuery] + public IEnumerable Get() + { + return BatchTestOrder.Orders; } - public class BatchTestHeadersCustomersController : ODataController + [EnableQuery] + public SingleResult Get([FromODataUri] int key) { - private string GetResponseString(string method) - { - return $"{method},ContentType={HttpContext.Request.ContentType},ContentLength={HttpContext.Request.ContentLength}," - + $"Prefer={HttpContext.Request.Headers["Prefer"]}"; - } - public string Get() - { - return GetResponseString("GET"); - } + return SingleResult.Create(BatchTestOrder.Orders.Where(d => d.Id.Equals(key)).AsQueryable()); + } - public string Delete(int key) - { - return GetResponseString("DELETE"); - } + public IActionResult Post([FromBody] BatchTestOrder order) + { + BatchTestOrder.Orders.Add(order); - public string Post() - { - return GetResponseString("POST"); - } + return Created(order); + } +} + +public class BatchTestHeadersCustomersController : ODataController +{ + private string GetResponseString(string method) + { + return $"{method},ContentType={HttpContext.Request.ContentType},ContentLength={HttpContext.Request.ContentLength}," + + $"Prefer={HttpContext.Request.Headers["Prefer"]}"; + } + public string Get() + { + return GetResponseString("GET"); + } + + public string Delete(int key) + { + return GetResponseString("DELETE"); } - public class BatchTestHeadersCustomer + public string Post() { - public int Id { get; set; } + return GetResponseString("POST"); } } + +public class BatchTestHeadersCustomer +{ + public int Id { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchContentTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchContentTest.cs index a917eae7c..236d52da4 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchContentTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchContentTest.cs @@ -19,112 +19,111 @@ using Xunit; using Microsoft.AspNetCore.OData.TestCommon; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +public class ODataBatchContentTest { - public class ODataBatchContentTest + [Fact] + public void Parameter_Constructor() + { + // Arrange & Act + const string boundaryHeader = "boundary"; + ODataBatchContent batchContent = CreateBatchContent(Array.Empty()); + string contentTypeHeader = batchContent.Headers["Content-Type"].FirstOrDefault(); + string mediaType = contentTypeHeader.Substring(0, contentTypeHeader.IndexOf(';')); + int boundaryParamStart = contentTypeHeader.IndexOf(boundaryHeader); + string boundary = contentTypeHeader.Substring(boundaryParamStart + boundaryHeader.Length); + var odataVersion = batchContent.Headers.FirstOrDefault(h => string.Equals(h.Key, ODataVersionConstraint.ODataServiceVersionHeader, StringComparison.OrdinalIgnoreCase)); + + // Assert + Assert.NotEmpty(boundary); + Assert.NotEmpty(odataVersion.Value); + Assert.Equal("multipart/mixed", mediaType); + } + + [Fact] + public void Constructor_Throws_WhenResponsesAreNull() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => CreateBatchContent(null), "responses"); + } + + [Fact] + public void NoODataVersionSettingInWriterSetting_SetDefaultVersionInTheHeader() + { + // Arrange & Act + ODataBatchContent batchContent = CreateBatchContent(Array.Empty()); + + var odataVersion = batchContent.Headers + .FirstOrDefault(h => string.Equals(h.Key, ODataVersionConstraint.ODataServiceVersionHeader, StringComparison.OrdinalIgnoreCase)); + + // Assert + Assert.Equal("4.0", odataVersion.Value.FirstOrDefault()); + } + + [Theory] + [InlineData(ODataVersion.V4, "4.0")] + [InlineData(ODataVersion.V401, "4.01")] + public void ODataVersionInWriterSetting_IsPropagatedToTheHeader(ODataVersion version, string expect) + { + // Arrange & Act + ODataBatchContent batchContent = CreateBatchContent(Array.Empty(), + s => s.AddSingleton(new ODataMessageWriterSettings { Version = version })); + + var odataVersion = batchContent.Headers + .FirstOrDefault(h => string.Equals(h.Key, ODataVersionConstraint.ODataServiceVersionHeader, StringComparison.OrdinalIgnoreCase)); + + // Assert + Assert.Equal(expect, odataVersion.Value.FirstOrDefault()); + } + + [Fact] + public async Task SerializeToStreamAsync_WritesODataBatchResponseItems() { - [Fact] - public void Parameter_Constructor() - { - // Arrange & Act - const string boundaryHeader = "boundary"; - ODataBatchContent batchContent = CreateBatchContent(Array.Empty()); - string contentTypeHeader = batchContent.Headers["Content-Type"].FirstOrDefault(); - string mediaType = contentTypeHeader.Substring(0, contentTypeHeader.IndexOf(';')); - int boundaryParamStart = contentTypeHeader.IndexOf(boundaryHeader); - string boundary = contentTypeHeader.Substring(boundaryParamStart + boundaryHeader.Length); - var odataVersion = batchContent.Headers.FirstOrDefault(h => string.Equals(h.Key, ODataVersionConstraint.ODataServiceVersionHeader, StringComparison.OrdinalIgnoreCase)); - - // Assert - Assert.NotEmpty(boundary); - Assert.NotEmpty(odataVersion.Value); - Assert.Equal("multipart/mixed", mediaType); - } - - [Fact] - public void Constructor_Throws_WhenResponsesAreNull() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => CreateBatchContent(null), "responses"); - } - - [Fact] - public void NoODataVersionSettingInWriterSetting_SetDefaultVersionInTheHeader() - { - // Arrange & Act - ODataBatchContent batchContent = CreateBatchContent(Array.Empty()); - - var odataVersion = batchContent.Headers - .FirstOrDefault(h => string.Equals(h.Key, ODataVersionConstraint.ODataServiceVersionHeader, StringComparison.OrdinalIgnoreCase)); - - // Assert - Assert.Equal("4.0", odataVersion.Value.FirstOrDefault()); - } - - [Theory] - [InlineData(ODataVersion.V4, "4.0")] - [InlineData(ODataVersion.V401, "4.01")] - public void ODataVersionInWriterSetting_IsPropagatedToTheHeader(ODataVersion version, string expect) - { - // Arrange & Act - ODataBatchContent batchContent = CreateBatchContent(Array.Empty(), - s => s.AddSingleton(new ODataMessageWriterSettings { Version = version })); - - var odataVersion = batchContent.Headers - .FirstOrDefault(h => string.Equals(h.Key, ODataVersionConstraint.ODataServiceVersionHeader, StringComparison.OrdinalIgnoreCase)); - - // Assert - Assert.Equal(expect, odataVersion.Value.FirstOrDefault()); - } - - [Fact] - public async Task SerializeToStreamAsync_WritesODataBatchResponseItems() - { - // Arrange - HttpContext okContext = new DefaultHttpContext(); - okContext.Response.StatusCode = StatusCodes.Status200OK; - - HttpContext acceptedContext = new DefaultHttpContext(); - acceptedContext.Response.StatusCode = StatusCodes.Status202Accepted; - - HttpContext badRequestContext = new DefaultHttpContext(); - badRequestContext.Response.StatusCode = StatusCodes.Status400BadRequest; - - // Act - ODataBatchContent batchContent = new ODataBatchContent(new ODataBatchResponseItem[] + // Arrange + HttpContext okContext = new DefaultHttpContext(); + okContext.Response.StatusCode = StatusCodes.Status200OK; + + HttpContext acceptedContext = new DefaultHttpContext(); + acceptedContext.Response.StatusCode = StatusCodes.Status202Accepted; + + HttpContext badRequestContext = new DefaultHttpContext(); + badRequestContext.Response.StatusCode = StatusCodes.Status400BadRequest; + + // Act + ODataBatchContent batchContent = new ODataBatchContent(new ODataBatchResponseItem[] + { + new OperationResponseItem(okContext), + new ChangeSetResponseItem(new HttpContext[] { - new OperationResponseItem(okContext), - new ChangeSetResponseItem(new HttpContext[] - { - acceptedContext, - badRequestContext - }) - }, - new MockServiceProvider()); - - MemoryStream stream = new MemoryStream(); - await batchContent.SerializeToStreamAsync(stream); - stream.Position = 0; - string responseString = await new StreamReader(stream).ReadToEndAsync(); - - // Assert - Assert.Contains("changesetresponse", responseString); - Assert.Contains("OK", responseString); - Assert.Contains("Accepted", responseString); - Assert.Contains("Bad Request", responseString); - } - - private static ODataBatchContent CreateBatchContent(IEnumerable responses) - { - return CreateBatchContent(responses, s => s.AddSingleton()); - } - - private static ODataBatchContent CreateBatchContent(IEnumerable responses, Action setupConfig) - { - IServiceCollection services = new ServiceCollection(); - setupConfig?.Invoke(services); - IServiceProvider sp = services.BuildServiceProvider(); - return new ODataBatchContent(responses, sp); - } + acceptedContext, + badRequestContext + }) + }, + new MockServiceProvider()); + + MemoryStream stream = new MemoryStream(); + await batchContent.SerializeToStreamAsync(stream); + stream.Position = 0; + string responseString = await new StreamReader(stream).ReadToEndAsync(); + + // Assert + Assert.Contains("changesetresponse", responseString); + Assert.Contains("OK", responseString); + Assert.Contains("Accepted", responseString); + Assert.Contains("Bad Request", responseString); + } + + private static ODataBatchContent CreateBatchContent(IEnumerable responses) + { + return CreateBatchContent(responses, s => s.AddSingleton()); + } + + private static ODataBatchContent CreateBatchContent(IEnumerable responses, Action setupConfig) + { + IServiceCollection services = new ServiceCollection(); + setupConfig?.Invoke(services); + IServiceProvider sp = services.BuildServiceProvider(); + return new ODataBatchContent(responses, sp); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchHttpRequestExtensionsTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchHttpRequestExtensionsTest.cs index ddbb34a69..0f2888c7f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchHttpRequestExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchHttpRequestExtensionsTest.cs @@ -19,224 +19,223 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +public class ODataBatchHttpRequestExtensionsTest { - public class ODataBatchHttpRequestExtensionsTest + [Fact] + public void GetODataBatchId_NullRequest_Throws() { - [Fact] - public void GetODataBatchId_NullRequest_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.GetODataBatchId(null), "request"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.GetODataBatchId(null), "request"); + } - [Fact] - public void SetODataBatchId_NullRequest_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.SetODataBatchId(null, Guid.NewGuid()), "request"); - } + [Fact] + public void SetODataBatchId_NullRequest_Throws() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.SetODataBatchId(null, Guid.NewGuid()), "request"); + } - [Fact] - public void SetODataBatchId_SetsTheBatchIdOnTheRequest() - { - // Arrange - HttpRequest request = new DefaultHttpContext().Request; - Guid id = Guid.NewGuid(); + [Fact] + public void SetODataBatchId_SetsTheBatchIdOnTheRequest() + { + // Arrange + HttpRequest request = new DefaultHttpContext().Request; + Guid id = Guid.NewGuid(); - // Act - request.SetODataBatchId(id); + // Act + request.SetODataBatchId(id); - // Assert - Assert.Equal(id, request.GetODataBatchId()); - } + // Assert + Assert.Equal(id, request.GetODataBatchId()); + } - [Fact] - public void GetODataChangeSetId_NullRequest_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.GetODataChangeSetId(null), "request"); - } + [Fact] + public void GetODataChangeSetId_NullRequest_Throws() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.GetODataChangeSetId(null), "request"); + } - [Fact] - public void SetODataChangeSetId_NullRequest_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.SetODataChangeSetId(null, Guid.NewGuid()), "request"); - } + [Fact] + public void SetODataChangeSetId_NullRequest_Throws() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.SetODataChangeSetId(null, Guid.NewGuid()), "request"); + } - [Fact] - public void SetODataChangeSetId_SetsTheChangeSetIdOnTheRequest() - { - // Arrange - HttpRequest request = new DefaultHttpContext().Request; - Guid id = Guid.NewGuid(); + [Fact] + public void SetODataChangeSetId_SetsTheChangeSetIdOnTheRequest() + { + // Arrange + HttpRequest request = new DefaultHttpContext().Request; + Guid id = Guid.NewGuid(); - // Act - request.SetODataChangeSetId(id); + // Act + request.SetODataChangeSetId(id); - // Assert - Assert.Equal(id, request.GetODataChangeSetId()); - } + // Assert + Assert.Equal(id, request.GetODataChangeSetId()); + } - [Fact] - public void GetODataContentIdMapping_NullRequest_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.GetODataContentIdMapping(null), "request"); - } + [Fact] + public void GetODataContentIdMapping_NullRequest_Throws() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.GetODataContentIdMapping(null), "request"); + } - [Fact] - public void SetODataContentIdMapping_NullRequest_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => ODataBatchHttpRequestExtensions.SetODataContentIdMapping(null, new Dictionary()), - "request"); - } + [Fact] + public void SetODataContentIdMapping_NullRequest_Throws() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => ODataBatchHttpRequestExtensions.SetODataContentIdMapping(null, new Dictionary()), + "request"); + } - [Fact] - public void SetODataContentIdMapping_SetsTheContentIdMappingOnTheRequest() - { - // Arrange - HttpRequest request = new DefaultHttpContext().Request; - IDictionary mapping = new Dictionary(); + [Fact] + public void SetODataContentIdMapping_SetsTheContentIdMappingOnTheRequest() + { + // Arrange + HttpRequest request = new DefaultHttpContext().Request; + IDictionary mapping = new Dictionary(); - // Act - request.SetODataContentIdMapping(mapping); + // Act + request.SetODataContentIdMapping(mapping); - // Assert - Assert.Equal(mapping, ODataBatchHttpRequestExtensions.GetODataContentIdMapping(request)); - } + // Assert + Assert.Equal(mapping, ODataBatchHttpRequestExtensions.GetODataContentIdMapping(request)); + } - [Fact] - public async Task CreateODataBatchResponseAsync_ReturnsHttpStatusCodeOK() - { - // Arrange - IEdmModel model = new EdmModel(); - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - context.ODataFeature().RoutePrefix = "odata"; - context.RequestServices = BuildServiceProvider(opt => opt.AddRouteComponents("odata", model)); + [Fact] + public async Task CreateODataBatchResponseAsync_ReturnsHttpStatusCodeOK() + { + // Arrange + IEdmModel model = new EdmModel(); + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; + context.ODataFeature().RoutePrefix = "odata"; + context.RequestServices = BuildServiceProvider(opt => opt.AddRouteComponents("odata", model)); - ODataBatchResponseItem[] responses = new ODataBatchResponseItem[] { }; - ODataMessageQuotas quotas = new ODataMessageQuotas(); + ODataBatchResponseItem[] responses = new ODataBatchResponseItem[] { }; + ODataMessageQuotas quotas = new ODataMessageQuotas(); - // Act - await request.CreateODataBatchResponseAsync(responses, quotas); + // Act + await request.CreateODataBatchResponseAsync(responses, quotas); - // Assert - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - } + // Assert + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + } - [Fact] - public void GetODataContentId_NullRequest_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.GetODataContentId(null), "request"); - } + [Fact] + public void GetODataContentId_NullRequest_Throws() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.GetODataContentId(null), "request"); + } - [Fact] - public void SetODataContentId_NullRequest_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.SetODataContentId(null, Guid.NewGuid().ToString()), "request"); - } + [Fact] + public void SetODataContentId_NullRequest_Throws() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => ODataBatchHttpRequestExtensions.SetODataContentId(null, Guid.NewGuid().ToString()), "request"); + } - [Fact] - public void SetODataContentId_SetsTheContentIdOnTheRequest() - { - // Arrange - HttpRequest request = new DefaultHttpContext().Request; - string id = Guid.NewGuid().ToString(); + [Fact] + public void SetODataContentId_SetsTheContentIdOnTheRequest() + { + // Arrange + HttpRequest request = new DefaultHttpContext().Request; + string id = Guid.NewGuid().ToString(); - // Act - request.SetODataContentId(id); + // Act + request.SetODataContentId(id); - // Assert - Assert.Equal(id, request.GetODataContentId()); - } + // Assert + Assert.Equal(id, request.GetODataContentId()); + } + + private static ODataMessageQuotas _odataMessageQuotas = new ODataMessageQuotas { MaxReceivedMessageSize = Int64.MaxValue }; - private static ODataMessageQuotas _odataMessageQuotas = new ODataMessageQuotas { MaxReceivedMessageSize = Int64.MaxValue }; + [Theory] + // if no accept header, return multipart/mixed + [InlineData(null, "multipart/mixed")] - [Theory] - // if no accept header, return multipart/mixed - [InlineData(null, "multipart/mixed")] + // if accept is multipart/mixed, return multipart/mixed + [InlineData(new[] { "multipart/mixed" }, "multipart/mixed")] - // if accept is multipart/mixed, return multipart/mixed - [InlineData(new[] { "multipart/mixed" }, "multipart/mixed")] + // if accept is application/json, return application/json + [InlineData(new[] { "application/json" }, "application/json")] - // if accept is application/json, return application/json - [InlineData(new[] { "application/json" }, "application/json")] + // if accept is application/json with charset, return application/json + [InlineData(new[] { "application/json; charset=utf-8" }, "application/json")] - // if accept is application/json with charset, return application/json - [InlineData(new[] { "application/json; charset=utf-8" }, "application/json")] + // if multipart/mixed is high proprity, return multipart/mixed + [InlineData(new[] { "multipart/mixed;q=0.9", "application/json;q=0.5" }, "multipart/mixed")] + [InlineData(new[] { "application/json;q=0.5", "multipart/mixed;q=0.9" }, "multipart/mixed")] - // if multipart/mixed is high proprity, return multipart/mixed - [InlineData(new[] { "multipart/mixed;q=0.9", "application/json;q=0.5" }, "multipart/mixed")] - [InlineData(new[] { "application/json;q=0.5", "multipart/mixed;q=0.9" }, "multipart/mixed")] + // if application/json is high proprity, return application/json + [InlineData(new[] { "application/json;q=0.9", "multipart/mixed;q=0.5" }, "application/json")] + [InlineData(new[] { "multipart/mixed;q=0.5", "application/json;q=0.9" }, "application/json")] - // if application/json is high proprity, return application/json - [InlineData(new[] { "application/json;q=0.9", "multipart/mixed;q=0.5" }, "application/json")] - [InlineData(new[] { "multipart/mixed;q=0.5", "application/json;q=0.9" }, "application/json")] + // if priorities are same, return first + [InlineData(new[] { "multipart/mixed;q=0.9", "application/json;q=0.9" }, "multipart/mixed")] + [InlineData(new[] { "multipart/mixed", "application/json" }, "multipart/mixed")] - // if priorities are same, return first - [InlineData(new[] { "multipart/mixed;q=0.9", "application/json;q=0.9" }, "multipart/mixed")] - [InlineData(new[] { "multipart/mixed", "application/json" }, "multipart/mixed")] + // if priorities are same, return first + [InlineData(new[] { "application/json;q=0.9", "multipart/mixed;q=0.9" }, "application/json")] + [InlineData(new[] { "application/json", "multipart/mixed" }, "application/json")] - // if priorities are same, return first - [InlineData(new[] { "application/json;q=0.9", "multipart/mixed;q=0.9" }, "application/json")] - [InlineData(new[] { "application/json", "multipart/mixed" }, "application/json")] + // no priority has q=1.0 + [InlineData(new[] { "application/json", "multipart/mixed;q=0.9" }, "application/json")] + [InlineData(new[] { "application/json;q=0.9", "multipart/mixed" }, "multipart/mixed")] + public async Task CreateODataBatchResponseAsync(string[] accept, string expected) + { + var request = RequestFactory.Create("Get", "http://localhost/$batch", opt => opt.AddRouteComponents(EdmCoreModel.Instance)); + request.ODataFeature().RoutePrefix = string.Empty; + var responses = new[] { new ChangeSetResponseItem(Enumerable.Empty()) }; - // no priority has q=1.0 - [InlineData(new[] { "application/json", "multipart/mixed;q=0.9" }, "application/json")] - [InlineData(new[] { "application/json;q=0.9", "multipart/mixed" }, "multipart/mixed")] - public async Task CreateODataBatchResponseAsync(string[] accept, string expected) + if (accept != null) { - var request = RequestFactory.Create("Get", "http://localhost/$batch", opt => opt.AddRouteComponents(EdmCoreModel.Instance)); - request.ODataFeature().RoutePrefix = string.Empty; - var responses = new[] { new ChangeSetResponseItem(Enumerable.Empty()) }; + request.Headers.Append("Accept", accept); + } - if (accept != null) - { - request.Headers.Append("Accept", accept); - } + await request.CreateODataBatchResponseAsync(responses, _odataMessageQuotas); - await request.CreateODataBatchResponseAsync(responses, _odataMessageQuotas); + Assert.StartsWith(expected, request.HttpContext.Response.ContentType); + } - Assert.StartsWith(expected, request.HttpContext.Response.ContentType); - } + [Theory] + // if no contentType, return multipart/mixed + [InlineData(null, "multipart/mixed")] + // if contentType is application/json, return application/json + [InlineData("application/json", "application/json")] + [InlineData("application/json; charset=utf-8", "application/json")] + // if contentType is multipart/mixed, return multipart/mixed + [InlineData("multipart/mixed", "multipart/mixed")] + public async Task CreateODataBatchResponseAsyncWhenNoAcceptHeader(string contentType, string expected) + { + var request = RequestFactory.Create("Get", "http://localhost/$batch", opt => opt.AddRouteComponents(EdmCoreModel.Instance)); + request.ODataFeature().RoutePrefix = string.Empty; + var responses = new[] { new ChangeSetResponseItem(Enumerable.Empty()) }; - [Theory] - // if no contentType, return multipart/mixed - [InlineData(null, "multipart/mixed")] - // if contentType is application/json, return application/json - [InlineData("application/json", "application/json")] - [InlineData("application/json; charset=utf-8", "application/json")] - // if contentType is multipart/mixed, return multipart/mixed - [InlineData("multipart/mixed", "multipart/mixed")] - public async Task CreateODataBatchResponseAsyncWhenNoAcceptHeader(string contentType, string expected) + if (contentType != null) { - var request = RequestFactory.Create("Get", "http://localhost/$batch", opt => opt.AddRouteComponents(EdmCoreModel.Instance)); - request.ODataFeature().RoutePrefix = string.Empty; - var responses = new[] { new ChangeSetResponseItem(Enumerable.Empty()) }; - - if (contentType != null) - { - request.ContentType = contentType; - } + request.ContentType = contentType; + } - await request.CreateODataBatchResponseAsync(responses, _odataMessageQuotas); + await request.CreateODataBatchResponseAsync(responses, _odataMessageQuotas); - Assert.False(request.Headers.ContainsKey("Accept")); // check no accept header - Assert.StartsWith(expected, request.HttpContext.Response.ContentType); - } + Assert.False(request.Headers.ContainsKey("Accept")); // check no accept header + Assert.StartsWith(expected, request.HttpContext.Response.ContentType); + } - private static IServiceProvider BuildServiceProvider(Action setupAction) - { - IServiceCollection services = new ServiceCollection(); - services.Configure(setupAction); - return services.BuildServiceProvider(); - } + private static IServiceProvider BuildServiceProvider(Action setupAction) + { + IServiceCollection services = new ServiceCollection(); + services.Configure(setupAction); + return services.BuildServiceProvider(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchMiddlewareTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchMiddlewareTest.cs index 8d56d9a84..c7ae56122 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchMiddlewareTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchMiddlewareTest.cs @@ -15,141 +15,140 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +public class ODataBatchMiddlewareTest { - public class ODataBatchMiddlewareTest + [Fact] + public void Ctor_NullBatchMapping_WithoutServiceProvider() { - [Fact] - public void Ctor_NullBatchMapping_WithoutServiceProvider() - { - // Arrange - ODataBatchMiddleware middleware = new ODataBatchMiddleware(null, null); + // Arrange + ODataBatchMiddleware middleware = new ODataBatchMiddleware(null, null); - // Act & Assert - Assert.Null(middleware.BatchMapping); - } + // Act & Assert + Assert.Null(middleware.BatchMapping); + } - [Fact] - public void Ctor_NullBatchMapping_WithServiceProvider_WithoutBatchHandler() - { - // Arrange - IServiceProvider sp = BuildServiceProvider(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance)); - ODataBatchMiddleware middleware = new ODataBatchMiddleware(sp, null); + [Fact] + public void Ctor_NullBatchMapping_WithServiceProvider_WithoutBatchHandler() + { + // Arrange + IServiceProvider sp = BuildServiceProvider(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance)); + ODataBatchMiddleware middleware = new ODataBatchMiddleware(sp, null); - // Act & Assert - Assert.Null(middleware.BatchMapping); - } + // Act & Assert + Assert.Null(middleware.BatchMapping); + } - [Fact] - public void Ctor_NullBatchMapping_WithServiceProvider_WithBatchHandler() - { - // Arrange - IServiceProvider sp = BuildServiceProvider(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance, new DefaultODataBatchHandler())); - ODataBatchMiddleware middleware = new ODataBatchMiddleware(sp, null); + [Fact] + public void Ctor_NullBatchMapping_WithServiceProvider_WithBatchHandler() + { + // Arrange + IServiceProvider sp = BuildServiceProvider(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance, new DefaultODataBatchHandler())); + ODataBatchMiddleware middleware = new ODataBatchMiddleware(sp, null); - // Act & Assert - Assert.NotNull(middleware.BatchMapping); - } + // Act & Assert + Assert.NotNull(middleware.BatchMapping); + } - [Fact] - public async Task Invoke_CallNextDelegate_WithoutBatchHandler() + [Fact] + public async Task Invoke_CallNextDelegate_WithoutBatchHandler() + { + // Arrange + bool called = false; + RequestDelegate next = context => { - // Arrange - bool called = false; - RequestDelegate next = context => - { - called = true; - return Task.CompletedTask; - }; + called = true; + return Task.CompletedTask; + }; - ODataBatchMiddleware middleware = new ODataBatchMiddleware(null, next.Invoke); - HttpContext context = new DefaultHttpContext(); + ODataBatchMiddleware middleware = new ODataBatchMiddleware(null, next.Invoke); + HttpContext context = new DefaultHttpContext(); - // Act - Assert.False(called); - await middleware.Invoke(context); + // Act + Assert.False(called); + await middleware.Invoke(context); - // Assert - Assert.True(called); - } + // Assert + Assert.True(called); + } - [Fact] - public async Task Invoke_CallProcessBatchAsync_WithBatchHandler() + [Fact] + public async Task Invoke_CallProcessBatchAsync_WithBatchHandler() + { + // Arrange + bool called = false; + RequestDelegate next = context => { - // Arrange - bool called = false; - RequestDelegate next = context => + called = true; + return Task.CompletedTask; + }; + Mock batchHandlerMock = new Mock(); + + bool processed = false; + batchHandlerMock.Setup(b => b.ProcessBatchAsync(It.IsAny(), It.IsAny())) + .Returns(() => { - called = true; + processed = true; return Task.CompletedTask; - }; - Mock batchHandlerMock = new Mock(); - - bool processed = false; - batchHandlerMock.Setup(b => b.ProcessBatchAsync(It.IsAny(), It.IsAny())) - .Returns(() => - { - processed = true; - return Task.CompletedTask; - }); - - IServiceProvider sp = BuildServiceProvider(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance, batchHandlerMock.Object)); - ODataBatchMiddleware middleware = new ODataBatchMiddleware(sp, next.Invoke); - HttpContext context = new DefaultHttpContext(); - context.Request.Path = new PathString("/odata/$batch"); - context.Request.Method = "Post"; - - // Act - Assert.False(called); - Assert.False(processed); - await middleware.Invoke(context); - - // Assert - Assert.False(called); - Assert.True(processed); - } - - [Fact] - public async Task Invoke_CorsCallNextDelegateWithBatchHandler() + }); + + IServiceProvider sp = BuildServiceProvider(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance, batchHandlerMock.Object)); + ODataBatchMiddleware middleware = new ODataBatchMiddleware(sp, next.Invoke); + HttpContext context = new DefaultHttpContext(); + context.Request.Path = new PathString("/odata/$batch"); + context.Request.Method = "Post"; + + // Act + Assert.False(called); + Assert.False(processed); + await middleware.Invoke(context); + + // Assert + Assert.False(called); + Assert.True(processed); + } + + [Fact] + public async Task Invoke_CorsCallNextDelegateWithBatchHandler() + { + // Arrange + bool called = false; + RequestDelegate next = context => { - // Arrange - bool called = false; - RequestDelegate next = context => + called = true; + return Task.CompletedTask; + }; + Mock batchHandlerMock = new Mock(); + + bool processed = false; + batchHandlerMock.Setup(b => b.ProcessBatchAsync(It.IsAny(), It.IsAny())) + .Returns(() => { - called = true; + processed = true; return Task.CompletedTask; - }; - Mock batchHandlerMock = new Mock(); - - bool processed = false; - batchHandlerMock.Setup(b => b.ProcessBatchAsync(It.IsAny(), It.IsAny())) - .Returns(() => - { - processed = true; - return Task.CompletedTask; - }); - - IServiceProvider sp = BuildServiceProvider(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance, batchHandlerMock.Object)); - ODataBatchMiddleware middleware = new ODataBatchMiddleware(sp, next.Invoke); - HttpContext context = new DefaultHttpContext(); - context.Request.Path = new PathString("/odata/$batch"); - context.Request.Method = "options"; - - // Act - Assert.False(called); - Assert.False(processed); - await middleware.Invoke(context); - - // Assert - Assert.True(called); - Assert.False(processed); - } - - private static IServiceProvider BuildServiceProvider(Action setupAction) - { - IServiceCollection services = new ServiceCollection(); - services.Configure(setupAction); - return services.BuildServiceProvider(); - } + }); + + IServiceProvider sp = BuildServiceProvider(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance, batchHandlerMock.Object)); + ODataBatchMiddleware middleware = new ODataBatchMiddleware(sp, next.Invoke); + HttpContext context = new DefaultHttpContext(); + context.Request.Path = new PathString("/odata/$batch"); + context.Request.Method = "options"; + + // Act + Assert.False(called); + Assert.False(processed); + await middleware.Invoke(context); + + // Assert + Assert.True(called); + Assert.False(processed); + } + + private static IServiceProvider BuildServiceProvider(Action setupAction) + { + IServiceCollection services = new ServiceCollection(); + services.Configure(setupAction); + return services.BuildServiceProvider(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchPathMappingTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchPathMappingTest.cs index 2e952d1a8..11083cb07 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchPathMappingTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchPathMappingTest.cs @@ -13,80 +13,79 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +public class ODataBatchPathMappingTest { - public class ODataBatchPathMappingTest + [Fact] + public void ODataBatchPathMappingWorksForNormalTemplate() { - [Fact] - public void ODataBatchPathMappingWorksForNormalTemplate() - { - // Arrange - ODataBatchHandler handler = new Mock().Object; - var mapping = new ODataBatchPathMapping(); - var request = RequestFactory.Create(HttpMethods.Get, "http://localhost/$batch"); - mapping.AddRoute("odata", "$batch", handler); + // Arrange + ODataBatchHandler handler = new Mock().Object; + var mapping = new ODataBatchPathMapping(); + var request = RequestFactory.Create(HttpMethods.Get, "http://localhost/$batch"); + mapping.AddRoute("odata", "$batch", handler); - // Act & Assert - Assert.True(mapping.TryGetPrefixName(request.HttpContext, out string outputName, out ODataBatchHandler outHandler)); - Assert.Equal("odata", outputName); - Assert.Same(outHandler, handler); - } + // Act & Assert + Assert.True(mapping.TryGetPrefixName(request.HttpContext, out string outputName, out ODataBatchHandler outHandler)); + Assert.Equal("odata", outputName); + Assert.Same(outHandler, handler); + } - [Theory] - [InlineData("world")] - [InlineData("kit")] - public void ODataBatchPathMappingWorksForSimpleTemplate(string name) - { - // Arrange - string routeName = "odata"; - string routeTemplate = "hello/{name}/$batch"; - string uri = "http://localhost/hello/" + name + "/$batch"; - var request = RequestFactory.Create(HttpMethods.Get, uri); - var mapping = new ODataBatchPathMapping(); - ODataBatchHandler handler = new Mock().Object; + [Theory] + [InlineData("world")] + [InlineData("kit")] + public void ODataBatchPathMappingWorksForSimpleTemplate(string name) + { + // Arrange + string routeName = "odata"; + string routeTemplate = "hello/{name}/$batch"; + string uri = "http://localhost/hello/" + name + "/$batch"; + var request = RequestFactory.Create(HttpMethods.Get, uri); + var mapping = new ODataBatchPathMapping(); + ODataBatchHandler handler = new Mock().Object; - // Act - mapping.AddRoute(routeName, routeTemplate, handler); + // Act + mapping.AddRoute(routeName, routeTemplate, handler); - bool result = mapping.TryGetPrefixName(request.HttpContext, out string outputName, out ODataBatchHandler outHandler); + bool result = mapping.TryGetPrefixName(request.HttpContext, out string outputName, out ODataBatchHandler outHandler); - // Assert - Assert.True(result); - Assert.Equal(outputName, routeName); - var routeData = request.ODataFeature().BatchRouteData; - Assert.NotNull(routeData); - var actual = Assert.Single(routeData); - Assert.Equal("name", actual.Key); - Assert.Equal(name, actual.Value); - } + // Assert + Assert.True(result); + Assert.Equal(outputName, routeName); + var routeData = request.ODataFeature().BatchRouteData; + Assert.NotNull(routeData); + var actual = Assert.Single(routeData); + Assert.Equal("name", actual.Key); + Assert.Equal(name, actual.Value); + } - [Theory] - [InlineData("1", "4")] - [InlineData("2", "3")] - [InlineData("latest", "unknown")] - public void ODataBatchPathMappingWorksForComplexTemplate(string version, string spec) - { - // Arrange - string routeName = "odata"; - string routeTemplate = "/v{api-version:apiVersion}/odata{spec}/$batch"; - string uri = "http://localhost/v" + version + "/odata" + spec + "/$batch"; - var request = RequestFactory.Create(HttpMethods.Get, uri); - var mapping = new ODataBatchPathMapping(); - ODataBatchHandler handler = new Mock().Object; + [Theory] + [InlineData("1", "4")] + [InlineData("2", "3")] + [InlineData("latest", "unknown")] + public void ODataBatchPathMappingWorksForComplexTemplate(string version, string spec) + { + // Arrange + string routeName = "odata"; + string routeTemplate = "/v{api-version:apiVersion}/odata{spec}/$batch"; + string uri = "http://localhost/v" + version + "/odata" + spec + "/$batch"; + var request = RequestFactory.Create(HttpMethods.Get, uri); + var mapping = new ODataBatchPathMapping(); + ODataBatchHandler handler = new Mock().Object; - // Act - mapping.AddRoute(routeName, routeTemplate, handler); + // Act + mapping.AddRoute(routeName, routeTemplate, handler); - bool result = mapping.TryGetPrefixName(request.HttpContext, out string outputName, out ODataBatchHandler outHandler); + bool result = mapping.TryGetPrefixName(request.HttpContext, out string outputName, out ODataBatchHandler outHandler); - // Assert - Assert.True(result); - Assert.Equal(outputName, routeName); - var routeData = request.ODataFeature().BatchRouteData; - Assert.NotNull(routeData); - Assert.Equal(new[] { "api-version", "spec" }, routeData.Keys); - Assert.Equal(version, routeData["api-version"]); - Assert.Equal(spec, routeData["spec"]); - } + // Assert + Assert.True(result); + Assert.Equal(outputName, routeName); + var routeData = request.ODataFeature().BatchRouteData; + Assert.NotNull(routeData); + Assert.Equal(new[] { "api-version", "spec" }, routeData.Keys); + Assert.Equal(version, routeData["api-version"]); + Assert.Equal(spec, routeData["spec"]); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchReaderExtensionsTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchReaderExtensionsTest.cs index 88a176a43..c1cebcb54 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchReaderExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchReaderExtensionsTest.cs @@ -18,90 +18,89 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +public class ODataBatchReaderExtensionsTest { - public class ODataBatchReaderExtensionsTest + [Fact] + public async Task ReadChangeSetRequest_NullReader_Throws() { - [Fact] - public async Task ReadChangeSetRequest_NullReader_Throws() - { - // Arrange - Mock mockContext = new Mock(); + // Arrange + Mock mockContext = new Mock(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => ODataBatchReaderExtensions.ReadChangeSetRequestAsync(null, mockContext.Object, Guid.NewGuid(), CancellationToken.None), - "reader"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => ODataBatchReaderExtensions.ReadChangeSetRequestAsync(null, mockContext.Object, Guid.NewGuid(), CancellationToken.None), + "reader"); + } - [Fact] - public async Task ReadChangeSetRequest_InvalidState_Throws() - { - // Arrange - StringContent httpContent = new StringContent(String.Empty, Encoding.UTF8, "multipart/mixed"); - httpContent.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("boundary", Guid.NewGuid().ToString())); - ODataMessageReader reader = await httpContent.GetODataMessageReaderAsync(new ODataMessageReaderSettings(), CancellationToken.None); - Mock mockContext = new Mock(); + [Fact] + public async Task ReadChangeSetRequest_InvalidState_Throws() + { + // Arrange + StringContent httpContent = new StringContent(String.Empty, Encoding.UTF8, "multipart/mixed"); + httpContent.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("boundary", Guid.NewGuid().ToString())); + ODataMessageReader reader = await httpContent.GetODataMessageReaderAsync(new ODataMessageReaderSettings(), CancellationToken.None); + Mock mockContext = new Mock(); - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => ODataBatchReaderExtensions.ReadChangeSetRequestAsync(reader.CreateODataBatchReader(), mockContext.Object, Guid.NewGuid(), CancellationToken.None), - "The current batch reader state 'Initial' is invalid. The expected state is 'ChangesetStart'."); - } + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => ODataBatchReaderExtensions.ReadChangeSetRequestAsync(reader.CreateODataBatchReader(), mockContext.Object, Guid.NewGuid(), CancellationToken.None), + "The current batch reader state 'Initial' is invalid. The expected state is 'ChangesetStart'."); + } - [Fact] - public async Task ReadOperationRequest_NullReader_Throws() - { - // Arrange - Mock mockContext = new Mock(); + [Fact] + public async Task ReadOperationRequest_NullReader_Throws() + { + // Arrange + Mock mockContext = new Mock(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => ODataBatchReaderExtensions.ReadOperationRequestAsync(null, mockContext.Object, Guid.NewGuid(), CancellationToken.None), - "reader"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => ODataBatchReaderExtensions.ReadOperationRequestAsync(null, mockContext.Object, Guid.NewGuid(), CancellationToken.None), + "reader"); + } - [Fact] - public async Task ReadOperationRequest_InvalidState_Throws() - { - // Arrange - StringContent httpContent = new StringContent(String.Empty, Encoding.UTF8, "multipart/mixed"); + [Fact] + public async Task ReadOperationRequest_InvalidState_Throws() + { + // Arrange + StringContent httpContent = new StringContent(String.Empty, Encoding.UTF8, "multipart/mixed"); - httpContent.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("boundary", Guid.NewGuid().ToString())); - ODataMessageReader reader = await httpContent.GetODataMessageReaderAsync(new ODataMessageReaderSettings(), CancellationToken.None); - Mock mockContext = new Mock(); + httpContent.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("boundary", Guid.NewGuid().ToString())); + ODataMessageReader reader = await httpContent.GetODataMessageReaderAsync(new ODataMessageReaderSettings(), CancellationToken.None); + Mock mockContext = new Mock(); - // Act & Assert - ExceptionAssert.Throws( - () => ODataBatchReaderExtensions.ReadOperationRequestAsync(reader.CreateODataBatchReader(), mockContext.Object, Guid.NewGuid(), CancellationToken.None), - "The current batch reader state 'Initial' is invalid. The expected state is 'Operation'."); - } + // Act & Assert + ExceptionAssert.Throws( + () => ODataBatchReaderExtensions.ReadOperationRequestAsync(reader.CreateODataBatchReader(), mockContext.Object, Guid.NewGuid(), CancellationToken.None), + "The current batch reader state 'Initial' is invalid. The expected state is 'Operation'."); + } - [Fact] - public async Task ReadChangeSetOperationRequest_NullReader_Throws() - { - // Arrange - Mock mockContext = new Mock(); + [Fact] + public async Task ReadChangeSetOperationRequest_NullReader_Throws() + { + // Arrange + Mock mockContext = new Mock(); - await ExceptionAssert.ThrowsArgumentNullAsync( - () => ODataBatchReaderExtensions.ReadChangeSetOperationRequestAsync(null, mockContext.Object, Guid.NewGuid(), Guid.NewGuid(), CancellationToken.None), - "reader"); - } + await ExceptionAssert.ThrowsArgumentNullAsync( + () => ODataBatchReaderExtensions.ReadChangeSetOperationRequestAsync(null, mockContext.Object, Guid.NewGuid(), Guid.NewGuid(), CancellationToken.None), + "reader"); + } - [Fact] - public async Task ReadChangeSetOperationRequest_InvalidState_Throws() - { - // Arrange - StringContent httpContent = new StringContent(String.Empty, Encoding.UTF8, "multipart/mixed"); - httpContent.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("boundary", Guid.NewGuid().ToString())); - ODataMessageReader reader = await httpContent.GetODataMessageReaderAsync(new ODataMessageReaderSettings(), CancellationToken.None); - Mock mockContext = new Mock(); + [Fact] + public async Task ReadChangeSetOperationRequest_InvalidState_Throws() + { + // Arrange + StringContent httpContent = new StringContent(String.Empty, Encoding.UTF8, "multipart/mixed"); + httpContent.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("boundary", Guid.NewGuid().ToString())); + ODataMessageReader reader = await httpContent.GetODataMessageReaderAsync(new ODataMessageReaderSettings(), CancellationToken.None); + Mock mockContext = new Mock(); - // Act & Assert - ExceptionAssert.Throws( - () => ODataBatchReaderExtensions.ReadChangeSetOperationRequestAsync(reader.CreateODataBatchReader(), mockContext.Object, - Guid.NewGuid(), Guid.NewGuid(), CancellationToken.None), - "The current batch reader state 'Initial' is invalid. The expected state is 'Operation'."); - } + // Act & Assert + ExceptionAssert.Throws( + () => ODataBatchReaderExtensions.ReadChangeSetOperationRequestAsync(reader.CreateODataBatchReader(), mockContext.Object, + Guid.NewGuid(), Guid.NewGuid(), CancellationToken.None), + "The current batch reader state 'Initial' is invalid. The expected state is 'Operation'."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchRequestHelper.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchRequestHelper.cs index cffe31d20..dfd27795e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchRequestHelper.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchRequestHelper.cs @@ -9,30 +9,29 @@ using System.Globalization; using System.Net.Http; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +internal static class ODataBatchRequestHelper { - internal static class ODataBatchRequestHelper - { - //public static HttpMessageContent CreateODataRequestContent(HttpRequestMessage request) - //{ - // var changeSetMessageContent = new HttpMessageContent(request); - // changeSetMessageContent.Headers.ContentType.Parameters.Clear(); - // changeSetMessageContent.Headers.TryAddWithoutValidation("Content-Transfer-Encoding", "binary"); - // changeSetMessageContent.Headers.TryAddWithoutValidation( - // "Content-ID", - // Guid.NewGuid().GetHashCode().ToString(CultureInfo.InvariantCulture)); - // return changeSetMessageContent; - //} + //public static HttpMessageContent CreateODataRequestContent(HttpRequestMessage request) + //{ + // var changeSetMessageContent = new HttpMessageContent(request); + // changeSetMessageContent.Headers.ContentType.Parameters.Clear(); + // changeSetMessageContent.Headers.TryAddWithoutValidation("Content-Transfer-Encoding", "binary"); + // changeSetMessageContent.Headers.TryAddWithoutValidation( + // "Content-ID", + // Guid.NewGuid().GetHashCode().ToString(CultureInfo.InvariantCulture)); + // return changeSetMessageContent; + //} - //public static HttpContent CreateODataRequestContent(this HttpRequestMessage request) - //{ - // var changeSetMessageContent = new StringContent(); - // changeSetMessageContent.Headers.ContentType.Parameters.Clear(); - // changeSetMessageContent.Headers.TryAddWithoutValidation("Content-Transfer-Encoding", "binary"); - // changeSetMessageContent.Headers.TryAddWithoutValidation( - // "Content-ID", - // Guid.NewGuid().GetHashCode().ToString(CultureInfo.InvariantCulture)); - // return changeSetMessageContent; - //} - } + //public static HttpContent CreateODataRequestContent(this HttpRequestMessage request) + //{ + // var changeSetMessageContent = new StringContent(); + // changeSetMessageContent.Headers.ContentType.Parameters.Clear(); + // changeSetMessageContent.Headers.TryAddWithoutValidation("Content-Transfer-Encoding", "binary"); + // changeSetMessageContent.Headers.TryAddWithoutValidation( + // "Content-ID", + // Guid.NewGuid().GetHashCode().ToString(CultureInfo.InvariantCulture)); + // return changeSetMessageContent; + //} } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchRequestItemTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchRequestItemTest.cs index 0edbfdb1d..5403cbc51 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchRequestItemTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchRequestItemTest.cs @@ -16,70 +16,69 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +public class ODataBatchRequestItemTest { - public class ODataBatchRequestItemTest + [Fact] + public async Task SendRequestAsync_Throws_WhenInvokerIsNull() { - [Fact] - public async Task SendRequestAsync_Throws_WhenInvokerIsNull() - { - // Arrange - Mock httpContext = new Mock(); + // Arrange + Mock httpContext = new Mock(); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => ODataBatchRequestItem.SendRequestAsync(null, httpContext.Object, null), + "handler"); + } - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => ODataBatchRequestItem.SendRequestAsync(null, httpContext.Object, null), - "handler"); - } + [Fact] + public async Task SendRequestAsync_Throws_WhenRequestIsNull() + { + // Arrange + Mock handler = new Mock(); - [Fact] - public async Task SendRequestAsync_Throws_WhenRequestIsNull() - { - // Arrange - Mock handler = new Mock(); + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => ODataBatchRequestItem.SendRequestAsync(handler.Object, null, null), + "context"); + } - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => ODataBatchRequestItem.SendRequestAsync(handler.Object, null, null), - "context"); - } + [Fact] + public async Task SendRequestAsync_CallsHandler() + { + // Arrange + HttpContext context = HttpContextHelper.Create("Get", "http://example.com"); - [Fact] - public async Task SendRequestAsync_CallsHandler() + RequestDelegate handler = context => { - // Arrange - HttpContext context = HttpContextHelper.Create("Get", "http://example.com"); - - RequestDelegate handler = context => - { - context.Response.StatusCode = StatusCodes.Status201Created; - return Task.CompletedTask; - }; + context.Response.StatusCode = StatusCodes.Status201Created; + return Task.CompletedTask; + }; - // Act - await ODataBatchRequestItem.SendRequestAsync(handler, context, new Dictionary()); + // Act + await ODataBatchRequestItem.SendRequestAsync(handler, context, new Dictionary()); - // Assert - Assert.Equal(StatusCodes.Status201Created, context.Response.StatusCode); - } + // Assert + Assert.Equal(StatusCodes.Status201Created, context.Response.StatusCode); + } - [Fact] - public async Task SendMessageAsync_Resolves_Uri_From_ContentId() - { - // Arrange - DefaultHttpContext context = new DefaultHttpContext(); - HttpResponseMessage response = new HttpResponseMessage(); - RequestDelegate handler = (c) => Task.CompletedTask; - Dictionary contentIdLocationMappings = new Dictionary(); - contentIdLocationMappings.Add("1", "http://localhost:12345/odata/Customers(42)"); - Uri unresolvedUri = new Uri("http://localhost:12345/odata/$1/Orders"); - context.Request.CopyAbsoluteUrl(unresolvedUri); + [Fact] + public async Task SendMessageAsync_Resolves_Uri_From_ContentId() + { + // Arrange + DefaultHttpContext context = new DefaultHttpContext(); + HttpResponseMessage response = new HttpResponseMessage(); + RequestDelegate handler = (c) => Task.CompletedTask; + Dictionary contentIdLocationMappings = new Dictionary(); + contentIdLocationMappings.Add("1", "http://localhost:12345/odata/Customers(42)"); + Uri unresolvedUri = new Uri("http://localhost:12345/odata/$1/Orders"); + context.Request.CopyAbsoluteUrl(unresolvedUri); - // Act - await ODataBatchRequestItem.SendRequestAsync(handler, context, contentIdLocationMappings); + // Act + await ODataBatchRequestItem.SendRequestAsync(handler, context, contentIdLocationMappings); - // Assert - Assert.Equal("/odata/Customers(42)/Orders", context.Request.Path.ToString()); - } + // Assert + Assert.Equal("/odata/Customers(42)/Orders", context.Request.Path.ToString()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchResponseItemTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchResponseItemTest.cs index 66e0542bf..0acc751e0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchResponseItemTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataBatchResponseItemTest.cs @@ -18,167 +18,166 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +public class ODataBatchResponseItemTest { - public class ODataBatchResponseItemTest + [Fact] + public async Task WriteMessageAsync_NullWriter_Throws() { - [Fact] - public async Task WriteMessageAsync_NullWriter_Throws() - { - // Arrange - HttpContext context = new Mock().Object; + // Arrange + HttpContext context = new Mock().Object; - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => ODataBatchResponseItem.WriteMessageAsync(null, context), "writer"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => ODataBatchResponseItem.WriteMessageAsync(null, context), "writer"); + } - [Fact] - public async Task WriteMessageAsync_NullContext_Throws() + [Fact] + public async Task WriteMessageAsync_NullContext_Throws() + { + // Arrange + HeaderDictionary headers = new HeaderDictionary { - // Arrange - HeaderDictionary headers = new HeaderDictionary - { - { "Content-Type", $"multipart/mixed;charset=utf-8;boundary={Guid.NewGuid()}" }, - }; - IODataResponseMessage odataResponse = ODataMessageWrapperHelper.Create(new MemoryStream(), headers); - ODataMessageWriter messageWriter = new ODataMessageWriter(odataResponse); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => ODataBatchResponseItem.WriteMessageAsync(messageWriter.CreateODataBatchWriter(), null), "context"); - } - - [Fact] - public void WriteMessage_SynchronouslyWritesResponseMessage_Throws() + { "Content-Type", $"multipart/mixed;charset=utf-8;boundary={Guid.NewGuid()}" }, + }; + IODataResponseMessage odataResponse = ODataMessageWrapperHelper.Create(new MemoryStream(), headers); + ODataMessageWriter messageWriter = new ODataMessageWriter(odataResponse); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => ODataBatchResponseItem.WriteMessageAsync(messageWriter.CreateODataBatchWriter(), null), "context"); + } + + [Fact] + public void WriteMessage_SynchronouslyWritesResponseMessage_Throws() + { + HeaderDictionary headers = new HeaderDictionary { - HeaderDictionary headers = new HeaderDictionary - { - { "Content-Type", $"multipart/mixed;charset=utf-8;boundary={Guid.NewGuid()}" }, - }; - - MemoryStream ms = new MemoryStream(); - IODataResponseMessage odataResponse = ODataMessageWrapperHelper.Create(ms, headers); - - HeaderDictionary responseHeaders = new HeaderDictionary - { - { "customHeader", "bar" } - }; - HttpResponse response = CreateResponse("example content", responseHeaders, "text/example"); - - // Act - ODataBatchWriter batchWriter = new ODataMessageWriter(odataResponse).CreateODataBatchWriter(); - batchWriter.WriteStartBatch(); - - // Assert - Action test = () => ODataBatchResponseItem.WriteMessageAsync(batchWriter, response.HttpContext).Wait(); - ODataException exception = ExceptionAssert.Throws(test); - Assert.Equal("An asynchronous operation was called on a synchronous batch writer. Calls on a batch writer instance must be either all synchronous or all asynchronous.", - exception.Message); - } - - [Fact] - public async Task WriteMessageAsync_AsynchronouslyWritesResponseMessage() + { "Content-Type", $"multipart/mixed;charset=utf-8;boundary={Guid.NewGuid()}" }, + }; + + MemoryStream ms = new MemoryStream(); + IODataResponseMessage odataResponse = ODataMessageWrapperHelper.Create(ms, headers); + + HeaderDictionary responseHeaders = new HeaderDictionary { - // Arrange - HeaderDictionary headers = new HeaderDictionary - { - { "Content-Type", $"multipart/mixed;charset=utf-8;boundary={Guid.NewGuid()}" }, - }; - MemoryStream ms = new MemoryStream(); - IODataResponseMessage odataResponse = ODataMessageWrapperHelper.Create(ms, headers); - - HeaderDictionary responseHeaders = new HeaderDictionary - { - { "customHeader", "bar" } - }; - HttpResponse response = CreateResponse("example content", responseHeaders, "text/example"); - - // Act - ODataBatchWriter batchWriter = await new ODataMessageWriter(odataResponse).CreateODataBatchWriterAsync(); - await batchWriter.WriteStartBatchAsync(); - await ODataBatchResponseItem.WriteMessageAsync(batchWriter, response.HttpContext); - await batchWriter.WriteEndBatchAsync(); - - ms.Position = 0; - string result = new StreamReader(ms).ReadToEnd(); - - // Assert - Assert.Contains("example content", result); - Assert.Contains("text/example", result); - Assert.Contains("customHeader", result); - Assert.Contains("bar", result); - } - - [Fact] - public void WriteMessageAsync_SynchronousResponseContainsContentId_IfHasContentIdInRequestChangeSet() + { "customHeader", "bar" } + }; + HttpResponse response = CreateResponse("example content", responseHeaders, "text/example"); + + // Act + ODataBatchWriter batchWriter = new ODataMessageWriter(odataResponse).CreateODataBatchWriter(); + batchWriter.WriteStartBatch(); + + // Assert + Action test = () => ODataBatchResponseItem.WriteMessageAsync(batchWriter, response.HttpContext).Wait(); + ODataException exception = ExceptionAssert.Throws(test); + Assert.Equal("An asynchronous operation was called on a synchronous batch writer. Calls on a batch writer instance must be either all synchronous or all asynchronous.", + exception.Message); + } + + [Fact] + public async Task WriteMessageAsync_AsynchronouslyWritesResponseMessage() + { + // Arrange + HeaderDictionary headers = new HeaderDictionary { - // Arrange - HeaderDictionary headers = new HeaderDictionary - { - { "Content-Type", $"multipart/mixed;charset=utf-8;boundary={Guid.NewGuid()}" }, - }; - - MemoryStream ms = new MemoryStream(); - IODataResponseMessage odataResponse = ODataMessageWrapperHelper.Create(ms, headers); - - string contentId = Guid.NewGuid().ToString(); - HttpResponse httpResponse = CreateResponse("any", new HeaderDictionary(), "text/example;charset=utf-8"); - httpResponse.HttpContext.Request.SetODataContentId(contentId); - - // Act - ODataBatchWriter batchWriter = new ODataMessageWriter(odataResponse).CreateODataBatchWriter(); - batchWriter.WriteStartBatch(); - batchWriter.WriteStartChangeset(); - - // Assert - Action test = () => ODataBatchResponseItem.WriteMessageAsync(batchWriter, httpResponse.HttpContext).Wait(); - ODataException exception = ExceptionAssert.Throws(test); - Assert.Equal("An asynchronous operation was called on a synchronous batch writer. Calls on a batch writer instance must be either all synchronous or all asynchronous.", - exception.Message); - } - - [Fact] - public async Task WriteMessageAsync_ResponseContainsContentId_IfHasContentIdInRequestChangeSet() + { "Content-Type", $"multipart/mixed;charset=utf-8;boundary={Guid.NewGuid()}" }, + }; + MemoryStream ms = new MemoryStream(); + IODataResponseMessage odataResponse = ODataMessageWrapperHelper.Create(ms, headers); + + HeaderDictionary responseHeaders = new HeaderDictionary { - // Arrange - HeaderDictionary headers = new HeaderDictionary - { - { "Content-Type", $"multipart/mixed;charset=utf-8;boundary={Guid.NewGuid()}" }, - }; - - MemoryStream ms = new MemoryStream(); - IODataResponseMessage odataResponse = ODataMessageWrapperHelper.Create(ms, headers); - - string contentId = Guid.NewGuid().ToString(); - HttpResponse httpResponse = CreateResponse("any", new HeaderDictionary(), "text/example;charset=utf-8"); - httpResponse.HttpContext.Request.SetODataContentId(contentId); - - // Act - ODataBatchWriter batchWriter = await new ODataMessageWriter(odataResponse).CreateODataBatchWriterAsync(); - await batchWriter.WriteStartBatchAsync(); - await batchWriter.WriteStartChangesetAsync(); - await ODataBatchResponseItem.WriteMessageAsync(batchWriter, httpResponse.HttpContext); - await batchWriter.WriteEndChangesetAsync(); - await batchWriter.WriteEndBatchAsync(); - - ms.Position = 0; - string result = new StreamReader(ms).ReadToEnd(); - - // Assert - Assert.Contains("any", result); - Assert.Contains("text/example", result); - Assert.Contains("Content-ID", result); - Assert.Contains(contentId, result); - } - - private static HttpResponse CreateResponse(string body, IHeaderDictionary headers, string contextType) + { "customHeader", "bar" } + }; + HttpResponse response = CreateResponse("example content", responseHeaders, "text/example"); + + // Act + ODataBatchWriter batchWriter = await new ODataMessageWriter(odataResponse).CreateODataBatchWriterAsync(); + await batchWriter.WriteStartBatchAsync(); + await ODataBatchResponseItem.WriteMessageAsync(batchWriter, response.HttpContext); + await batchWriter.WriteEndBatchAsync(); + + ms.Position = 0; + string result = new StreamReader(ms).ReadToEnd(); + + // Assert + Assert.Contains("example content", result); + Assert.Contains("text/example", result); + Assert.Contains("customHeader", result); + Assert.Contains("bar", result); + } + + [Fact] + public void WriteMessageAsync_SynchronousResponseContainsContentId_IfHasContentIdInRequestChangeSet() + { + // Arrange + HeaderDictionary headers = new HeaderDictionary { - HttpContext httpContext = new DefaultHttpContext(); - httpContext.Features.Get().Headers = headers; - httpContext.Response.ContentType = contextType; - byte[] contentBytes = Encoding.UTF8.GetBytes(body); - httpContext.Response.Body = new MemoryStream(contentBytes); - return httpContext.Response; - } + { "Content-Type", $"multipart/mixed;charset=utf-8;boundary={Guid.NewGuid()}" }, + }; + + MemoryStream ms = new MemoryStream(); + IODataResponseMessage odataResponse = ODataMessageWrapperHelper.Create(ms, headers); + + string contentId = Guid.NewGuid().ToString(); + HttpResponse httpResponse = CreateResponse("any", new HeaderDictionary(), "text/example;charset=utf-8"); + httpResponse.HttpContext.Request.SetODataContentId(contentId); + + // Act + ODataBatchWriter batchWriter = new ODataMessageWriter(odataResponse).CreateODataBatchWriter(); + batchWriter.WriteStartBatch(); + batchWriter.WriteStartChangeset(); + + // Assert + Action test = () => ODataBatchResponseItem.WriteMessageAsync(batchWriter, httpResponse.HttpContext).Wait(); + ODataException exception = ExceptionAssert.Throws(test); + Assert.Equal("An asynchronous operation was called on a synchronous batch writer. Calls on a batch writer instance must be either all synchronous or all asynchronous.", + exception.Message); + } + + [Fact] + public async Task WriteMessageAsync_ResponseContainsContentId_IfHasContentIdInRequestChangeSet() + { + // Arrange + HeaderDictionary headers = new HeaderDictionary + { + { "Content-Type", $"multipart/mixed;charset=utf-8;boundary={Guid.NewGuid()}" }, + }; + + MemoryStream ms = new MemoryStream(); + IODataResponseMessage odataResponse = ODataMessageWrapperHelper.Create(ms, headers); + + string contentId = Guid.NewGuid().ToString(); + HttpResponse httpResponse = CreateResponse("any", new HeaderDictionary(), "text/example;charset=utf-8"); + httpResponse.HttpContext.Request.SetODataContentId(contentId); + + // Act + ODataBatchWriter batchWriter = await new ODataMessageWriter(odataResponse).CreateODataBatchWriterAsync(); + await batchWriter.WriteStartBatchAsync(); + await batchWriter.WriteStartChangesetAsync(); + await ODataBatchResponseItem.WriteMessageAsync(batchWriter, httpResponse.HttpContext); + await batchWriter.WriteEndChangesetAsync(); + await batchWriter.WriteEndBatchAsync(); + + ms.Position = 0; + string result = new StreamReader(ms).ReadToEnd(); + + // Assert + Assert.Contains("any", result); + Assert.Contains("text/example", result); + Assert.Contains("Content-ID", result); + Assert.Contains(contentId, result); + } + + private static HttpResponse CreateResponse(string body, IHeaderDictionary headers, string contextType) + { + HttpContext httpContext = new DefaultHttpContext(); + httpContext.Features.Get().Headers = headers; + httpContext.Response.ContentType = contextType; + byte[] contentBytes = Encoding.UTF8.GetBytes(body); + httpContext.Response.Body = new MemoryStream(contentBytes); + return httpContext.Response; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataHttpContentExtensions.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataHttpContentExtensions.cs index 90866ed17..81159dfa6 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataHttpContentExtensions.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataHttpContentExtensions.cs @@ -17,52 +17,51 @@ using Microsoft.Extensions.Primitives; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +/// +/// Provides extension methods for the class. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class ODataHttpContentExtensions { /// - /// Provides extension methods for the class. + /// Gets the for the stream. /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static class ODataHttpContentExtensions + /// The . + /// The . + /// A task object that produces an when completed. + public static Task GetODataMessageReaderAsync(this HttpContent content, ODataMessageReaderSettings settings) { - /// - /// Gets the for the stream. - /// - /// The . - /// The . - /// A task object that produces an when completed. - public static Task GetODataMessageReaderAsync(this HttpContent content, ODataMessageReaderSettings settings) - { - return GetODataMessageReaderAsync(content, settings, CancellationToken.None); - } + return GetODataMessageReaderAsync(content, settings, CancellationToken.None); + } - /// - /// Gets the for the stream. - /// - /// The . - /// The . - /// The token to monitor for cancellation requests. - /// A task object that produces an when completed. - public static async Task GetODataMessageReaderAsync(this HttpContent content, - ODataMessageReaderSettings settings, CancellationToken cancellationToken) + /// + /// Gets the for the stream. + /// + /// The . + /// The . + /// The token to monitor for cancellation requests. + /// A task object that produces an when completed. + public static async Task GetODataMessageReaderAsync(this HttpContent content, + ODataMessageReaderSettings settings, CancellationToken cancellationToken) + { + if (content == null) { - if (content == null) - { - throw Error.ArgumentNull("content"); - } - - cancellationToken.ThrowIfCancellationRequested(); - Stream contentStream = await content.ReadAsStreamAsync(); + throw Error.ArgumentNull("content"); + } - HeaderDictionary headerDict = new HeaderDictionary(); - foreach (var head in content.Headers) - { - headerDict[head.Key] = new StringValues(head.Value.ToArray()); - } + cancellationToken.ThrowIfCancellationRequested(); + Stream contentStream = await content.ReadAsStreamAsync(); - IODataRequestMessage oDataRequestMessage = ODataMessageWrapperHelper.Create(contentStream, headerDict/*, new MockServiceProvider()*/); - ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, settings); - return oDataMessageReader; + HeaderDictionary headerDict = new HeaderDictionary(); + foreach (var head in content.Headers) + { + headerDict[head.Key] = new StringValues(head.Value.ToArray()); } + + IODataRequestMessage oDataRequestMessage = ODataMessageWrapperHelper.Create(contentStream, headerDict/*, new MockServiceProvider()*/); + ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, settings); + return oDataMessageReader; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataHttpContentExtensionsTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataHttpContentExtensionsTest.cs index d3b5e097b..48ba16ec7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataHttpContentExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/ODataHttpContentExtensionsTest.cs @@ -13,27 +13,26 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +public class ODataHttpContentExtensionsTest { - public class ODataHttpContentExtensionsTest + [Fact] + public async Task GetODataMessageReaderAsync_NullContent_Throws() { - [Fact] - public async Task GetODataMessageReaderAsync_NullContent_Throws() - { - // Arrange & Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => ODataHttpContentExtensions.GetODataMessageReaderAsync(null, new ODataMessageReaderSettings(), CancellationToken.None), - "content"); - } + // Arrange & Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => ODataHttpContentExtensions.GetODataMessageReaderAsync(null, new ODataMessageReaderSettings(), CancellationToken.None), + "content"); + } - [Fact] - public async Task GetODataMessageReaderAsync_ReturnsMessageReader() - { - // Arrange - StringContent content = new StringContent("foo", Encoding.UTF8, "multipart/mixed"); + [Fact] + public async Task GetODataMessageReaderAsync_ReturnsMessageReader() + { + // Arrange + StringContent content = new StringContent("foo", Encoding.UTF8, "multipart/mixed"); - // Act & Assert - Assert.NotNull(await content.GetODataMessageReaderAsync(new ODataMessageReaderSettings(), CancellationToken.None)); - } + // Act & Assert + Assert.NotNull(await content.GetODataMessageReaderAsync(new ODataMessageReaderSettings(), CancellationToken.None)); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/OperationRequestItemTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/OperationRequestItemTest.cs index ccc9e02ce..035c9e887 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/OperationRequestItemTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/OperationRequestItemTest.cs @@ -13,57 +13,56 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +public class OperationRequestItemTest { - public class OperationRequestItemTest + [Fact] + public void Parameter_Constructor() { - [Fact] - public void Parameter_Constructor() - { - // Arrange & Act - Mock context = new Mock(); - OperationRequestItem requestItem = new OperationRequestItem(context.Object); + // Arrange & Act + Mock context = new Mock(); + OperationRequestItem requestItem = new OperationRequestItem(context.Object); - // Assert - Assert.Same(context.Object, requestItem.Context); - } + // Assert + Assert.Same(context.Object, requestItem.Context); + } - [Fact] - public void Constructor_NullContext_Throws() - { - ExceptionAssert.ThrowsArgumentNull(() => new OperationRequestItem(null), "context"); - } + [Fact] + public void Constructor_NullContext_Throws() + { + ExceptionAssert.ThrowsArgumentNull(() => new OperationRequestItem(null), "context"); + } - [Fact] - public async Task SendRequestAsync_NullHandler_Throws() - { - // Arrange - Mock context = new Mock(); - OperationRequestItem requestItem = new OperationRequestItem(context.Object); + [Fact] + public async Task SendRequestAsync_NullHandler_Throws() + { + // Arrange + Mock context = new Mock(); + OperationRequestItem requestItem = new OperationRequestItem(context.Object); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => requestItem.SendRequestAsync(null), "handler"); + } - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => requestItem.SendRequestAsync(null), "handler"); - } + [Fact] + public async Task SendRequestAsync_ReturnsOperationResponse() + { + // Arrange + HttpContext context = HttpContextHelper.Create("Get", "http://example.com"); + OperationRequestItem requestItem = new OperationRequestItem(context); - [Fact] - public async Task SendRequestAsync_ReturnsOperationResponse() + RequestDelegate handler = context => { - // Arrange - HttpContext context = HttpContextHelper.Create("Get", "http://example.com"); - OperationRequestItem requestItem = new OperationRequestItem(context); - - RequestDelegate handler = context => - { - context.Response.StatusCode = StatusCodes.Status304NotModified; - return Task.CompletedTask; - }; + context.Response.StatusCode = StatusCodes.Status304NotModified; + return Task.CompletedTask; + }; - // Act - ODataBatchResponseItem response = await requestItem.SendRequestAsync(handler); + // Act + ODataBatchResponseItem response = await requestItem.SendRequestAsync(handler); - // Assert - OperationResponseItem operationResponse = Assert.IsType(response); - Assert.Equal(StatusCodes.Status304NotModified, operationResponse.Context.Response.StatusCode); - } + // Assert + OperationResponseItem operationResponse = Assert.IsType(response); + Assert.Equal(StatusCodes.Status304NotModified, operationResponse.Context.Response.StatusCode); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/OperationResponseItemTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/OperationResponseItemTest.cs index 7502f6edd..ba9a18234 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/OperationResponseItemTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/OperationResponseItemTest.cs @@ -17,95 +17,94 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +public class OperationResponseItemTest { - public class OperationResponseItemTest + [Fact] + public void Parameter_Constructor() + { + // Arrange & Act + Mock context = new Mock(); + OperationResponseItem responseItem = new OperationResponseItem(context.Object); + + // Assert + Assert.Same(context.Object, responseItem.Context); + } + + [Fact] + public void Constructor_NullContext_Throws() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new OperationResponseItem(null), "context"); + } + + [Fact] + public async Task WriteResponseAsync_NullWriter_Throws() + { + // Arrange & Act + Mock context = new Mock(); + OperationResponseItem responseItem = new OperationResponseItem(context.Object); + + // Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => responseItem.WriteResponseAsync(null), "writer"); + } + + [Fact] + public void WriteResponseAsync_SynchronouslyWritesOperation_Throws() { - [Fact] - public void Parameter_Constructor() - { - // Arrange & Act - Mock context = new Mock(); - OperationResponseItem responseItem = new OperationResponseItem(context.Object); - - // Assert - Assert.Same(context.Object, responseItem.Context); - } - - [Fact] - public void Constructor_NullContext_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new OperationResponseItem(null), "context"); - } - - [Fact] - public async Task WriteResponseAsync_NullWriter_Throws() - { - // Arrange & Act - Mock context = new Mock(); - OperationResponseItem responseItem = new OperationResponseItem(context.Object); - - // Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => responseItem.WriteResponseAsync(null), "writer"); - } - - [Fact] - public void WriteResponseAsync_SynchronouslyWritesOperation_Throws() - { - // Arrange - HttpContext context = HttpContextHelper.Create(StatusCodes.Status202Accepted); - - OperationResponseItem responseItem = new OperationResponseItem(context); - MemoryStream memoryStream = new MemoryStream(); - IODataResponseMessage responseMessage = new ODataMessageWrapper(memoryStream); - ODataMessageWriter writer = new ODataMessageWriter(responseMessage); - - // Act - ODataBatchWriter batchWriter = writer.CreateODataBatchWriter(); - batchWriter.WriteStartBatch(); - - // Assert - Action test = () => responseItem.WriteResponseAsync(batchWriter).Wait(); - ODataException exception = ExceptionAssert.Throws(test); - Assert.Equal("An asynchronous operation was called on a synchronous batch writer. Calls on a batch writer instance must be either all synchronous or all asynchronous.", - exception.Message); - } - - [Fact] - public async Task WriteResponseAsync_AsynchronouslyWritesOperation() - { - // Arrange - HttpContext context = HttpContextHelper.Create(StatusCodes.Status202Accepted); - - OperationResponseItem responseItem = new OperationResponseItem(context); - MemoryStream memoryStream = new MemoryStream(); - IODataResponseMessage responseMessage = new ODataMessageWrapper(memoryStream); - ODataMessageWriter writer = new ODataMessageWriter(responseMessage); - - // Act - ODataBatchWriter batchWriter = await writer.CreateODataBatchWriterAsync(); - await batchWriter.WriteStartBatchAsync(); - await responseItem.WriteResponseAsync(batchWriter); - - await batchWriter.WriteEndBatchAsync(); - memoryStream.Position = 0; - string responseString = new StreamReader(memoryStream).ReadToEnd(); - - // Assert - Assert.Contains("Accepted", responseString); - } - - [Fact] - public void IsResponseSucess_TestResponse() - { - // Arrange - OperationResponseItem successResponseItem = new OperationResponseItem(HttpContextHelper.Create(StatusCodes.Status200OK)); - OperationResponseItem errorResponseItem = new OperationResponseItem(HttpContextHelper.Create(StatusCodes.Status300MultipleChoices)); - - // Act & Assert - Assert.True(successResponseItem.IsResponseSuccessful()); - Assert.False(errorResponseItem.IsResponseSuccessful()); - } + // Arrange + HttpContext context = HttpContextHelper.Create(StatusCodes.Status202Accepted); + + OperationResponseItem responseItem = new OperationResponseItem(context); + MemoryStream memoryStream = new MemoryStream(); + IODataResponseMessage responseMessage = new ODataMessageWrapper(memoryStream); + ODataMessageWriter writer = new ODataMessageWriter(responseMessage); + + // Act + ODataBatchWriter batchWriter = writer.CreateODataBatchWriter(); + batchWriter.WriteStartBatch(); + + // Assert + Action test = () => responseItem.WriteResponseAsync(batchWriter).Wait(); + ODataException exception = ExceptionAssert.Throws(test); + Assert.Equal("An asynchronous operation was called on a synchronous batch writer. Calls on a batch writer instance must be either all synchronous or all asynchronous.", + exception.Message); + } + + [Fact] + public async Task WriteResponseAsync_AsynchronouslyWritesOperation() + { + // Arrange + HttpContext context = HttpContextHelper.Create(StatusCodes.Status202Accepted); + + OperationResponseItem responseItem = new OperationResponseItem(context); + MemoryStream memoryStream = new MemoryStream(); + IODataResponseMessage responseMessage = new ODataMessageWrapper(memoryStream); + ODataMessageWriter writer = new ODataMessageWriter(responseMessage); + + // Act + ODataBatchWriter batchWriter = await writer.CreateODataBatchWriterAsync(); + await batchWriter.WriteStartBatchAsync(); + await responseItem.WriteResponseAsync(batchWriter); + + await batchWriter.WriteEndBatchAsync(); + memoryStream.Position = 0; + string responseString = new StreamReader(memoryStream).ReadToEnd(); + + // Assert + Assert.Contains("Accepted", responseString); + } + + [Fact] + public void IsResponseSucess_TestResponse() + { + // Arrange + OperationResponseItem successResponseItem = new OperationResponseItem(HttpContextHelper.Create(StatusCodes.Status200OK)); + OperationResponseItem errorResponseItem = new OperationResponseItem(HttpContextHelper.Create(StatusCodes.Status300MultipleChoices)); + + // Act & Assert + Assert.True(successResponseItem.IsResponseSuccessful()); + Assert.False(errorResponseItem.IsResponseSuccessful()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Batch/UnbufferedODataBatchHandlerTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Batch/UnbufferedODataBatchHandlerTest.cs index e21cbcf4b..c34315585 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Batch/UnbufferedODataBatchHandlerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Batch/UnbufferedODataBatchHandlerTest.cs @@ -27,66 +27,66 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Test.Batch +namespace Microsoft.AspNetCore.OData.Test.Batch; + +public class UnbufferedODataBatchHandlerTest { - public class UnbufferedODataBatchHandlerTest + [Fact] + public void Parameter_Constructor() { - [Fact] - public void Parameter_Constructor() - { - // Arrange - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); + // Arrange + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - // Act & Assert - Assert.False(batchHandler.ContinueOnError); - Assert.NotNull(batchHandler.MessageQuotas); - Assert.Null(batchHandler.PrefixName); - } + // Act & Assert + Assert.False(batchHandler.ContinueOnError); + Assert.NotNull(batchHandler.MessageQuotas); + Assert.Null(batchHandler.PrefixName); + } - [Fact] - public async Task CreateResponseMessageAsync_Throws_IfResponsesAreNull() - { - // Arrange - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - HttpContext context = new DefaultHttpContext(); + [Fact] + public async Task CreateResponseMessageAsync_Throws_IfResponsesAreNull() + { + // Arrange + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); + HttpContext context = new DefaultHttpContext(); - context.ODataFeature().RoutePrefix = "odata"; - context.RequestServices = BuildServiceProvider(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance)); + context.ODataFeature().RoutePrefix = "odata"; + context.RequestServices = BuildServiceProvider(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance)); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.CreateResponseMessageAsync(null, context.Request), - "responses"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.CreateResponseMessageAsync(null, context.Request), + "responses"); + } - [Fact] - public async Task ProcessBatchAsync_Throws_IfContextIsNull() - { - // Arrange - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - RequestDelegate next = null; + [Fact] + public async Task ProcessBatchAsync_Throws_IfContextIsNull() + { + // Arrange + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); + RequestDelegate next = null; - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.ProcessBatchAsync(null, next), "context"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.ProcessBatchAsync(null, next), "context"); + } - [Fact] - public async Task ProcessBatchAsync_CallsInvokerForEachRequest() + [Fact] + public async Task ProcessBatchAsync_CallsInvokerForEachRequest() + { + // Arrange + RequestDelegate next = async context => { - // Arrange - RequestDelegate next = async context => + HttpRequest request = context.Request; + string responseContent = request.GetDisplayUrl(); + string content = request.ReadBody(); + if (!string.IsNullOrEmpty(content)) { - HttpRequest request = context.Request; - string responseContent = request.GetDisplayUrl(); - string content = request.ReadBody(); - if (!string.IsNullOrEmpty(content)) - { - responseContent += "," + content; - } - - await context.Response.WriteAsync(responseContent); - }; - - string batchRequest = @"--2d958200-beb1-4437-97c5-9d19f7a1d538 + responseContent += "," + content; + } + + await context.Response.WriteAsync(responseContent); + }; + + string batchRequest = @"--2d958200-beb1-4437-97c5-9d19f7a1d538 Content-Type: application/http Content-Transfer-Encoding: binary Content-ID: 1301336816 @@ -114,52 +114,52 @@ public async Task ProcessBatchAsync_CallsInvokerForEachRequest() "; - HttpRequest request = RequestFactory.Create("Post", "http://example.com/$batch", opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance)); - HttpContext httpContext = request.HttpContext; - byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); - request.Body = new MemoryStream(requestBytes); - request.ContentType = "multipart/mixed; boundary=\"2d958200-beb1-4437-97c5-9d19f7a1d538\""; - request.ContentLength = 603; - httpContext.ODataFeature().RoutePrefix = "odata"; - httpContext.Response.Body = new MemoryStream(); + HttpRequest request = RequestFactory.Create("Post", "http://example.com/$batch", opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance)); + HttpContext httpContext = request.HttpContext; + byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); + request.Body = new MemoryStream(requestBytes); + request.ContentType = "multipart/mixed; boundary=\"2d958200-beb1-4437-97c5-9d19f7a1d538\""; + request.ContentLength = 603; + httpContext.ODataFeature().RoutePrefix = "odata"; + httpContext.Response.Body = new MemoryStream(); - // Act - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - batchHandler.PrefixName = "odata"; - await batchHandler.ProcessBatchAsync(httpContext, next); + // Act + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); + batchHandler.PrefixName = "odata"; + await batchHandler.ProcessBatchAsync(httpContext, next); - // Assert - string responseBody = httpContext.Response.ReadBody(); + // Assert + string responseBody = httpContext.Response.ReadBody(); - Assert.Contains("http://example.com/", responseBody); - Assert.Contains("http://example.com/values,foo", responseBody); - } + Assert.Contains("http://example.com/", responseBody); + Assert.Contains("http://example.com/values,foo", responseBody); + } - [Fact] - public async Task ProcessBatchAsync_DisposesResponseInCaseOfException() + [Fact] + public async Task ProcessBatchAsync_DisposesResponseInCaseOfException() + { + // Arrange + RequestDelegate handler = context => { - // Arrange - RequestDelegate handler = context => + HttpRequest request = context.Request; + if (request.Method.ToLowerInvariant() == "put") + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + } + else if (request.Method.ToLowerInvariant() == "post") { - HttpRequest request = context.Request; - if (request.Method.ToLowerInvariant() == "put") - { - context.Response.StatusCode = StatusCodes.Status400BadRequest; - } - else if (request.Method.ToLowerInvariant() == "post") - { - context.Response.WriteAsync("POSTED VALUE"); - } - else - { - context.Response.WriteAsync("OTHER VALUE"); - } - - return Task.CompletedTask; - }; - - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - string batchRequest = @" + context.Response.WriteAsync("POSTED VALUE"); + } + else + { + context.Response.WriteAsync("OTHER VALUE"); + } + + return Task.CompletedTask; + }; + + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); + string batchRequest = @" --97a4482b-26c6-4551-aed3-85f8b500bbbf Content-Type: application/http Content-Transfer-Encoding: binary @@ -194,64 +194,64 @@ public async Task ProcessBatchAsync_DisposesResponseInCaseOfException() --97a4482b-26c6-4551-aed3-85f8b500bbbf-- "; - HttpRequest request = RequestFactory.Create("Post", "http://example.com/$batch", opt => opt.AddRouteComponents(EdmCoreModel.Instance)); - HttpContext httpContext = request.HttpContext; - - byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); - request.Body = new MemoryStream(requestBytes); - request.ContentType = "multipart/mixed;boundary=\"be13321d-3c7b-4126-aa20-958b2c7a87e0\""; - request.ContentLength = 736; - httpContext.ODataFeature().RoutePrefix = ""; - httpContext.Response.Body = new MemoryStream(); - batchHandler.PrefixName = ""; - - // Act - await batchHandler.ProcessBatchAsync(httpContext, handler); - - // Assert - string responseBody = httpContext.Response.ReadBody(); - Assert.Contains("POSTED VALUE", responseBody); - Assert.Contains("400 Bad Request", responseBody); - Assert.DoesNotContain("OTHER VALUE", responseBody); - } + HttpRequest request = RequestFactory.Create("Post", "http://example.com/$batch", opt => opt.AddRouteComponents(EdmCoreModel.Instance)); + HttpContext httpContext = request.HttpContext; + + byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); + request.Body = new MemoryStream(requestBytes); + request.ContentType = "multipart/mixed;boundary=\"be13321d-3c7b-4126-aa20-958b2c7a87e0\""; + request.ContentLength = 736; + httpContext.ODataFeature().RoutePrefix = ""; + httpContext.Response.Body = new MemoryStream(); + batchHandler.PrefixName = ""; + + // Act + await batchHandler.ProcessBatchAsync(httpContext, handler); + + // Assert + string responseBody = httpContext.Response.ReadBody(); + Assert.Contains("POSTED VALUE", responseBody); + Assert.Contains("400 Bad Request", responseBody); + Assert.DoesNotContain("OTHER VALUE", responseBody); + } - [Theory] - [InlineData(true, null, false)] - [InlineData(true, "odata.continue-on-error", true)] - [InlineData(true, "odata.continue-on-error=true", true)] - [InlineData(true, "odata.continue-on-error=false", false)] - [InlineData(true, "continue-on-error", true)] - [InlineData(true, "continue-on-error=true", true)] - [InlineData(true, "continue-on-error=false", false)] - [InlineData(false, null, true)] - [InlineData(false, "odata.continue-on-error", true)] - [InlineData(false, "odata.continue-on-error=true", true)] - [InlineData(false, "odata.continue-on-error=false", true)] - [InlineData(false, "continue-on-error", true)] - [InlineData(false, "continue-on-error=true", true)] - [InlineData(false, "continue-on-error=false", true)] - public async Task ProcessBatchAsync_ContinueOnError(bool enableContinueOnError, string preferenceHeader, bool hasThirdResponse) + [Theory] + [InlineData(true, null, false)] + [InlineData(true, "odata.continue-on-error", true)] + [InlineData(true, "odata.continue-on-error=true", true)] + [InlineData(true, "odata.continue-on-error=false", false)] + [InlineData(true, "continue-on-error", true)] + [InlineData(true, "continue-on-error=true", true)] + [InlineData(true, "continue-on-error=false", false)] + [InlineData(false, null, true)] + [InlineData(false, "odata.continue-on-error", true)] + [InlineData(false, "odata.continue-on-error=true", true)] + [InlineData(false, "odata.continue-on-error=false", true)] + [InlineData(false, "continue-on-error", true)] + [InlineData(false, "continue-on-error=true", true)] + [InlineData(false, "continue-on-error=false", true)] + public async Task ProcessBatchAsync_ContinueOnError(bool enableContinueOnError, string preferenceHeader, bool hasThirdResponse) + { + // Arrange + RequestDelegate handler = async context => { - // Arrange - RequestDelegate handler = async context => + HttpRequest request = context.Request; + string responseContent = request.GetDisplayUrl(); + string content = request.ReadBody(); + if (!string.IsNullOrEmpty(content)) { - HttpRequest request = context.Request; - string responseContent = request.GetDisplayUrl(); - string content = request.ReadBody(); - if (!string.IsNullOrEmpty(content)) - { - responseContent += "," + content; - } - - HttpResponse response = context.Response; - if (content.Equals("foo")) - { - response.StatusCode = StatusCodes.Status400BadRequest; - } - await response.WriteAsync(responseContent); - }; - - string batchRequest = @"--d3df74a8-8212-4c2a-b4fb-d713a4ba383e + responseContent += "," + content; + } + + HttpResponse response = context.Response; + if (content.Equals("foo")) + { + response.StatusCode = StatusCodes.Status400BadRequest; + } + await response.WriteAsync(responseContent); + }; + + string batchRequest = @"--d3df74a8-8212-4c2a-b4fb-d713a4ba383e Content-Type: application/http Content-Transfer-Encoding: binary Content-ID: 1948857409 @@ -288,100 +288,100 @@ public async Task ProcessBatchAsync_ContinueOnError(bool enableContinueOnError, --d3df74a8-8212-4c2a-b4fb-d713a4ba383e-- "; - HttpRequest request = RequestFactory.Create("Post", "http://example.com/$batch", - opt => opt.AddRouteComponents(EdmCoreModel.Instance).EnableContinueOnErrorHeader = enableContinueOnError); - HttpContext httpContext = request.HttpContext; - byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); - request.Body = new MemoryStream(requestBytes); - request.ContentType = "multipart/mixed;boundary=\"d3df74a8-8212-4c2a-b4fb-d713a4ba383e\""; - request.ContentLength = 827; - httpContext.ODataFeature().RoutePrefix = ""; + HttpRequest request = RequestFactory.Create("Post", "http://example.com/$batch", + opt => opt.AddRouteComponents(EdmCoreModel.Instance).EnableContinueOnErrorHeader = enableContinueOnError); + HttpContext httpContext = request.HttpContext; + byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); + request.Body = new MemoryStream(requestBytes); + request.ContentType = "multipart/mixed;boundary=\"d3df74a8-8212-4c2a-b4fb-d713a4ba383e\""; + request.ContentLength = 827; + httpContext.ODataFeature().RoutePrefix = ""; - if (preferenceHeader != null) - { - httpContext.Request.Headers.Append("prefer", preferenceHeader); - } + if (preferenceHeader != null) + { + httpContext.Request.Headers.Append("prefer", preferenceHeader); + } - httpContext.Response.Body = new MemoryStream(); - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - batchHandler.PrefixName = ""; + httpContext.Response.Body = new MemoryStream(); + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); + batchHandler.PrefixName = ""; - // Act - await batchHandler.ProcessBatchAsync(httpContext, handler); - string responseBody = httpContext.Response.ReadBody(); + // Act + await batchHandler.ProcessBatchAsync(httpContext, handler); + string responseBody = httpContext.Response.ReadBody(); - // Assert - Assert.NotNull(responseBody); + // Assert + Assert.NotNull(responseBody); - // #1 response - Assert.Contains("http://example.com/", responseBody); + // #1 response + Assert.Contains("http://example.com/", responseBody); - // #2 bad response - Assert.Contains("Bad Request", responseBody); - Assert.Contains("http://example.com/values,foo", responseBody); + // #2 bad response + Assert.Contains("Bad Request", responseBody); + Assert.Contains("http://example.com/values,foo", responseBody); - // #3 response - if (hasThirdResponse) - { - Assert.Contains("http://example.com/values,bar", responseBody); - } - else - { - Assert.DoesNotContain("http://example.com/values,bar", responseBody); - } + // #3 response + if (hasThirdResponse) + { + Assert.Contains("http://example.com/values,bar", responseBody); } - - [Fact] - public void ValidateRequest_Throws_IfResponsesIsNull() + else { - // Arrange - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => batchHandler.ValidateRequest(null), "request"); + Assert.DoesNotContain("http://example.com/values,bar", responseBody); } + } - [Fact] - public async Task ExecuteChangeSet_Throws_IfReaderIsNull() - { - // Arrange - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - HttpContext httpContext = new DefaultHttpContext(); + [Fact] + public void ValidateRequest_Throws_IfResponsesIsNull() + { + // Arrange + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.ExecuteChangeSetAsync(null, Guid.NewGuid(), httpContext.Request, null), - "batchReader"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => batchHandler.ValidateRequest(null), "request"); + } - [Fact] - public async Task ExecuteChangeSet_Throws_IfRequestIsNull() - { - // Arrange - ODataMessageInfo messageInfo = new ODataMessageInfo(); - ODataMessageReaderSettings settings = new ODataMessageReaderSettings(); - Mock inputContext = new Mock(ODataFormat.Batch, messageInfo, settings); - Mock batchReader = new Mock(inputContext.Object, false); - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => batchHandler.ExecuteChangeSetAsync(batchReader.Object, Guid.NewGuid(), null, null), - "originalRequest"); - } + [Fact] + public async Task ExecuteChangeSet_Throws_IfReaderIsNull() + { + // Arrange + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); + HttpContext httpContext = new DefaultHttpContext(); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => batchHandler.ExecuteChangeSetAsync(null, Guid.NewGuid(), httpContext.Request, null), + "batchReader"); + } + + [Fact] + public async Task ExecuteChangeSet_Throws_IfRequestIsNull() + { + // Arrange + ODataMessageInfo messageInfo = new ODataMessageInfo(); + ODataMessageReaderSettings settings = new ODataMessageReaderSettings(); + Mock inputContext = new Mock(ODataFormat.Batch, messageInfo, settings); + Mock batchReader = new Mock(inputContext.Object, false); + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => batchHandler.ExecuteChangeSetAsync(batchReader.Object, Guid.NewGuid(), null, null), + "originalRequest"); + } - [Fact] - public async Task ExecuteChangeSetAsync_CopiesPropertiesFromRequest_WithoutExcludedProperties() + [Fact] + public async Task ExecuteChangeSetAsync_CopiesPropertiesFromRequest_WithoutExcludedProperties() + { + // Arrange + RequestDelegate handler = context => { - // Arrange - RequestDelegate handler = context => - { - context.Features[typeof(UnbufferedODataBatchHandlerTest)] = "bar"; - return Task.CompletedTask; - }; + context.Features[typeof(UnbufferedODataBatchHandlerTest)] = "bar"; + return Task.CompletedTask; + }; - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - string batchRequest = @"--cb7bb9ee-dce2-4c94-bf11-b742b2bc6072 + string batchRequest = @"--cb7bb9ee-dce2-4c94-bf11-b742b2bc6072 Content-Type: multipart/mixed; boundary=""09f27d33-41ea-4334-8ace-1738bd2793a9"" --09f27d33-41ea-4334-8ace-1738bd2793a9 @@ -398,46 +398,46 @@ public async Task ExecuteChangeSetAsync_CopiesPropertiesFromRequest_WithoutExclu --cb7bb9ee-dce2-4c94-bf11-b742b2bc6072-- "; - HttpRequest request = RequestFactory.Create("Post", "http://example.com/$batch", opt => opt.AddRouteComponents(EdmCoreModel.Instance)); - HttpContext httpContext = request.HttpContext; - byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); - request.Body = new MemoryStream(requestBytes); - request.ContentType = "multipart/mixed;boundary=\"09f27d33-41ea-4334-8ace-1738bd2793a9\""; - request.ContentLength = 431; - httpContext.ODataFeature().RoutePrefix = ""; - IServiceProvider sp = request.GetRouteServices(); - - IODataRequestMessage oDataRequestMessage = ODataMessageWrapperHelper.Create(request.Body, request.Headers, sp); - ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, new ODataMessageReaderSettings { BaseUri = new Uri("http://example.com") }); - - ODataBatchReader batchReader = await oDataMessageReader.CreateODataBatchReaderAsync(); - List responses = new List(); - - // Act - var response = await batchHandler.ExecuteChangeSetAsync(batchReader, Guid.NewGuid(), request, handler); - - // Arrange - ChangeSetResponseItem changeSetResponseItem = Assert.IsType(response); - HttpContext changeSetContext = Assert.Single(changeSetResponseItem.Contexts); - Assert.Equal("bar", changeSetContext.Features[typeof(UnbufferedODataBatchHandlerTest)]); - } + HttpRequest request = RequestFactory.Create("Post", "http://example.com/$batch", opt => opt.AddRouteComponents(EdmCoreModel.Instance)); + HttpContext httpContext = request.HttpContext; + byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); + request.Body = new MemoryStream(requestBytes); + request.ContentType = "multipart/mixed;boundary=\"09f27d33-41ea-4334-8ace-1738bd2793a9\""; + request.ContentLength = 431; + httpContext.ODataFeature().RoutePrefix = ""; + IServiceProvider sp = request.GetRouteServices(); + + IODataRequestMessage oDataRequestMessage = ODataMessageWrapperHelper.Create(request.Body, request.Headers, sp); + ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, new ODataMessageReaderSettings { BaseUri = new Uri("http://example.com") }); + + ODataBatchReader batchReader = await oDataMessageReader.CreateODataBatchReaderAsync(); + List responses = new List(); + + // Act + var response = await batchHandler.ExecuteChangeSetAsync(batchReader, Guid.NewGuid(), request, handler); + + // Arrange + ChangeSetResponseItem changeSetResponseItem = Assert.IsType(response); + HttpContext changeSetContext = Assert.Single(changeSetResponseItem.Contexts); + Assert.Equal("bar", changeSetContext.Features[typeof(UnbufferedODataBatchHandlerTest)]); + } - [Fact] - public async Task ExecuteChangeSetAsync_ReturnsSingleErrorResponse() + [Fact] + public async Task ExecuteChangeSetAsync_ReturnsSingleErrorResponse() + { + // Arrange + RequestDelegate handler = context => { - // Arrange - RequestDelegate handler = context => + if (context.Request.Method.ToUpperInvariant() == "POST") { - if (context.Request.Method.ToUpperInvariant() == "POST") - { - context.Response.StatusCode = StatusCodes.Status400BadRequest; - } + context.Response.StatusCode = StatusCodes.Status400BadRequest; + } - return Task.CompletedTask; - }; + return Task.CompletedTask; + }; - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - string batchRequest = @"--86aef3eb-4af6-4750-a66d-df65e3f31ab0 + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); + string batchRequest = @"--86aef3eb-4af6-4750-a66d-df65e3f31ab0 Content-Type: multipart/mixed; boundary=""7a61b8c1-a80e-4e6b-bac7-2f65564e3fd6"" --7a61b8c1-a80e-4e6b-bac7-2f65564e3fd6 @@ -471,42 +471,42 @@ public async Task ExecuteChangeSetAsync_ReturnsSingleErrorResponse() --86aef3eb-4af6-4750-a66d-df65e3f31ab0--"; - IEdmModel model = new EdmModel(); - HttpRequest request = RequestFactory.Create("Post", "http://example.com/$batch", opt => opt.AddRouteComponents(model)); - HttpContext httpContext = request.HttpContext; - byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); - httpContext.Request.Body = new MemoryStream(requestBytes); - httpContext.Request.ContentType = "multipart/mixed;boundary=\"86aef3eb-4af6-4750-a66d-df65e3f31ab0\""; - httpContext.Request.ContentLength = 431; - httpContext.ODataFeature().RoutePrefix = ""; - IServiceProvider sp = request.GetRouteServices(); + IEdmModel model = new EdmModel(); + HttpRequest request = RequestFactory.Create("Post", "http://example.com/$batch", opt => opt.AddRouteComponents(model)); + HttpContext httpContext = request.HttpContext; + byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); + httpContext.Request.Body = new MemoryStream(requestBytes); + httpContext.Request.ContentType = "multipart/mixed;boundary=\"86aef3eb-4af6-4750-a66d-df65e3f31ab0\""; + httpContext.Request.ContentLength = 431; + httpContext.ODataFeature().RoutePrefix = ""; + IServiceProvider sp = request.GetRouteServices(); - IODataRequestMessage oDataRequestMessage = ODataMessageWrapperHelper.Create(httpContext.Request.Body, request.Headers, sp); - ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, new ODataMessageReaderSettings { BaseUri = new Uri("http://example.com") }); + IODataRequestMessage oDataRequestMessage = ODataMessageWrapperHelper.Create(httpContext.Request.Body, request.Headers, sp); + ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, new ODataMessageReaderSettings { BaseUri = new Uri("http://example.com") }); - ODataBatchReader batchReader = await oDataMessageReader.CreateODataBatchReaderAsync(); + ODataBatchReader batchReader = await oDataMessageReader.CreateODataBatchReaderAsync(); - // Act - var response = await batchHandler.ExecuteChangeSetAsync(batchReader, Guid.NewGuid(), httpContext.Request, handler); + // Act + var response = await batchHandler.ExecuteChangeSetAsync(batchReader, Guid.NewGuid(), httpContext.Request, handler); - // Assert - var changesetResponse = Assert.IsType(response); - var returnContext = Assert.Single(changesetResponse.Contexts); - Assert.Equal(StatusCodes.Status400BadRequest, returnContext.Response.StatusCode); - } + // Assert + var changesetResponse = Assert.IsType(response); + var returnContext = Assert.Single(changesetResponse.Contexts); + Assert.Equal(StatusCodes.Status400BadRequest, returnContext.Response.StatusCode); + } - [Fact] - public async Task ExecuteOperationAsync_CopiesPropertiesFromRequest_WithoutExcludedProperties() + [Fact] + public async Task ExecuteOperationAsync_CopiesPropertiesFromRequest_WithoutExcludedProperties() + { + // Arrange + RequestDelegate handler = context => { - // Arrange - RequestDelegate handler = context => - { - context.Features[typeof(UnbufferedODataBatchHandlerTest)] = "foo"; - return Task.CompletedTask; - }; + context.Features[typeof(UnbufferedODataBatchHandlerTest)] = "foo"; + return Task.CompletedTask; + }; - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - string batchRequest = @"--75148e61-e67a-4bb7-ac0f-78fa30d0da30 + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); + string batchRequest = @"--75148e61-e67a-4bb7-ac0f-78fa30d0da30 Content-Type: application/http Content-Transfer-Encoding: binary Content-ID: 318941389 @@ -518,64 +518,63 @@ public async Task ExecuteOperationAsync_CopiesPropertiesFromRequest_WithoutExclu --75148e61-e67a-4bb7-ac0f-78fa30d0da30-- "; - HttpRequest request = RequestFactory.Create("Post", "http://example.com/$batch", opt => opt.AddRouteComponents(EdmCoreModel.Instance)); - HttpContext httpContext = request.HttpContext; - byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); - request.Body = new MemoryStream(requestBytes); - request.ContentType = "multipart/mixed;boundary=\"75148e61-e67a-4bb7-ac0f-78fa30d0da30\""; - request.ContentLength = 431; - httpContext.ODataFeature().RoutePrefix = ""; - IServiceProvider sp = request.GetRouteServices(); + HttpRequest request = RequestFactory.Create("Post", "http://example.com/$batch", opt => opt.AddRouteComponents(EdmCoreModel.Instance)); + HttpContext httpContext = request.HttpContext; + byte[] requestBytes = Encoding.UTF8.GetBytes(batchRequest); + request.Body = new MemoryStream(requestBytes); + request.ContentType = "multipart/mixed;boundary=\"75148e61-e67a-4bb7-ac0f-78fa30d0da30\""; + request.ContentLength = 431; + httpContext.ODataFeature().RoutePrefix = ""; + IServiceProvider sp = request.GetRouteServices(); - IODataRequestMessage oDataRequestMessage = ODataMessageWrapperHelper.Create(httpContext.Request.Body, request.Headers, sp); - ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, new ODataMessageReaderSettings { BaseUri = new Uri("http://example.com") }); + IODataRequestMessage oDataRequestMessage = ODataMessageWrapperHelper.Create(httpContext.Request.Body, request.Headers, sp); + ODataMessageReader oDataMessageReader = new ODataMessageReader(oDataRequestMessage, new ODataMessageReaderSettings { BaseUri = new Uri("http://example.com") }); - ODataBatchReader batchReader = await oDataMessageReader.CreateODataBatchReaderAsync(); - List responses = new List(); - await batchReader.ReadAsync(); + ODataBatchReader batchReader = await oDataMessageReader.CreateODataBatchReaderAsync(); + List responses = new List(); + await batchReader.ReadAsync(); - // Act - var response = await batchHandler.ExecuteOperationAsync(batchReader, Guid.NewGuid(), httpContext.Request, handler); + // Act + var response = await batchHandler.ExecuteOperationAsync(batchReader, Guid.NewGuid(), httpContext.Request, handler); - // Assert - OperationResponseItem operationResponseItem = Assert.IsType(response); - Assert.Equal("foo", operationResponseItem.Context.Features[typeof(UnbufferedODataBatchHandlerTest)]); - } + // Assert + OperationResponseItem operationResponseItem = Assert.IsType(response); + Assert.Equal("foo", operationResponseItem.Context.Features[typeof(UnbufferedODataBatchHandlerTest)]); + } - [Fact] - public async Task ExecuteOperation_Throws_IfReaderIsNull() - { - // Arrange - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - Mock request = new Mock(); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => batchHandler.ExecuteOperationAsync(null, Guid.NewGuid(), request.Object, null), - "batchReader"); - } + [Fact] + public async Task ExecuteOperation_Throws_IfReaderIsNull() + { + // Arrange + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); + Mock request = new Mock(); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => batchHandler.ExecuteOperationAsync(null, Guid.NewGuid(), request.Object, null), + "batchReader"); + } - [Fact] - public async Task ExecuteOperation_Throws_IfRequestIsNull() - { - // Arrange - StringContent httpContent = new StringContent(string.Empty, Encoding.UTF8, "multipart/mixed"); - httpContent.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("boundary", Guid.NewGuid().ToString())); + [Fact] + public async Task ExecuteOperation_Throws_IfRequestIsNull() + { + // Arrange + StringContent httpContent = new StringContent(string.Empty, Encoding.UTF8, "multipart/mixed"); + httpContent.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("boundary", Guid.NewGuid().ToString())); - ODataMessageReader reader = await httpContent.GetODataMessageReaderAsync(new ODataMessageReaderSettings(), CancellationToken.None); - UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); + ODataMessageReader reader = await httpContent.GetODataMessageReaderAsync(new ODataMessageReaderSettings(), CancellationToken.None); + UnbufferedODataBatchHandler batchHandler = new UnbufferedODataBatchHandler(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => batchHandler.ExecuteOperationAsync(reader.CreateODataBatchReader(), Guid.NewGuid(), null, null), - "originalRequest"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => batchHandler.ExecuteOperationAsync(reader.CreateODataBatchReader(), Guid.NewGuid(), null, null), + "originalRequest"); + } - private static IServiceProvider BuildServiceProvider(Action setupAction) - { - IServiceCollection services = new ServiceCollection(); - services.Configure(setupAction); - return services.BuildServiceProvider(); - } + private static IServiceProvider BuildServiceProvider(Action setupAction) + { + IServiceCollection services = new ServiceCollection(); + services.Configure(setupAction); + return services.BuildServiceProvider(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Commons/CultureReplacer.cs b/test/Microsoft.AspNetCore.OData.Tests/Commons/CultureReplacer.cs index 6b17c8f85..a4183d714 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Commons/CultureReplacer.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Commons/CultureReplacer.cs @@ -10,69 +10,68 @@ using System.Threading; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Commons +namespace Microsoft.AspNetCore.OData.Tests.Commons; + +public class CultureReplacer : IDisposable { - public class CultureReplacer : IDisposable - { - private const string _defaultCultureName = "en-GB"; - private const string _defaultUICultureName = "en-US"; - private static readonly CultureInfo _defaultCulture = CultureInfo.GetCultureInfo(_defaultCultureName); - private readonly CultureInfo _originalCulture; - private readonly CultureInfo _originalUICulture; - private readonly long _threadId; + private const string _defaultCultureName = "en-GB"; + private const string _defaultUICultureName = "en-US"; + private static readonly CultureInfo _defaultCulture = CultureInfo.GetCultureInfo(_defaultCultureName); + private readonly CultureInfo _originalCulture; + private readonly CultureInfo _originalUICulture; + private readonly long _threadId; - // Culture => Formatting of dates/times/money/etc, defaults to en-GB because en-US is the same as InvariantCulture - // We want to be able to find issues where the InvariantCulture is used, but a specific culture should be. - // - // UICulture => Language - public CultureReplacer(string culture = _defaultCultureName, string uiCulture = _defaultUICultureName) - { - _originalCulture = Thread.CurrentThread.CurrentCulture; - _originalUICulture = Thread.CurrentThread.CurrentUICulture; - _threadId = Thread.CurrentThread.ManagedThreadId; + // Culture => Formatting of dates/times/money/etc, defaults to en-GB because en-US is the same as InvariantCulture + // We want to be able to find issues where the InvariantCulture is used, but a specific culture should be. + // + // UICulture => Language + public CultureReplacer(string culture = _defaultCultureName, string uiCulture = _defaultUICultureName) + { + _originalCulture = Thread.CurrentThread.CurrentCulture; + _originalUICulture = Thread.CurrentThread.CurrentUICulture; + _threadId = Thread.CurrentThread.ManagedThreadId; - Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture); - Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(uiCulture); - } + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture); + Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(uiCulture); + } - /// - /// The name of the culture that is used as the default value for Thread.CurrentCulture when CultureReplacer is used. - /// - public static string DefaultCultureName - { - get { return _defaultCultureName; } - } + /// + /// The name of the culture that is used as the default value for Thread.CurrentCulture when CultureReplacer is used. + /// + public static string DefaultCultureName + { + get { return _defaultCultureName; } + } - /// - /// The name of the culture that is used as the default value for Thread.UICurrentCulture when CultureReplacer is used. - /// - public static string DefaultUICultureName - { - get { return _defaultUICultureName; } - } + /// + /// The name of the culture that is used as the default value for Thread.UICurrentCulture when CultureReplacer is used. + /// + public static string DefaultUICultureName + { + get { return _defaultUICultureName; } + } - /// - /// The culture that is used as the default value for Thread.CurrentCulture when CultureReplacer is used. - /// - public static CultureInfo DefaultCulture - { - get { return _defaultCulture; } - } + /// + /// The culture that is used as the default value for Thread.CurrentCulture when CultureReplacer is used. + /// + public static CultureInfo DefaultCulture + { + get { return _defaultCulture; } + } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - private void Dispose(bool disposing) + private void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - Assert.True(Thread.CurrentThread.ManagedThreadId == _threadId, "The current thread is not the same as the thread invoking the constructor. This should never happen."); - Thread.CurrentThread.CurrentCulture = _originalCulture; - Thread.CurrentThread.CurrentUICulture = _originalUICulture; - } + Assert.True(Thread.CurrentThread.ManagedThreadId == _threadId, "The current thread is not the same as the thread invoking the constructor. This should never happen."); + Thread.CurrentThread.CurrentCulture = _originalCulture; + Thread.CurrentThread.CurrentUICulture = _originalUICulture; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Commons/ExceptionAssert.cs b/test/Microsoft.AspNetCore.OData.Tests/Commons/ExceptionAssert.cs index 2aa476e3d..911e75896 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Commons/ExceptionAssert.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Commons/ExceptionAssert.cs @@ -12,555 +12,554 @@ using System.Threading.Tasks; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Commons +namespace Microsoft.AspNetCore.OData.Tests.Commons; + +public class ExceptionAssert { - public class ExceptionAssert + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static void DoesNotThrow(Action testCode) { - /// - /// Verifies that the exact exception is thrown (and not a derived exception type). - /// - /// The type of the exception expected to be thrown - /// A delegate to the code to be tested - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static void DoesNotThrow(Action testCode) + try { - try - { - testCode(); - } - catch (Exception ex) - { - Assert.Null(ex); - } + testCode(); } - - /// - /// Verifies that the exact exception is thrown (and not a derived exception type). - /// - /// The type of the exception expected to be thrown - /// A delegate to the code to be tested - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static T Throws(Action testCode) - where T : Exception + catch (Exception ex) { - return (T)Throws(typeof(T), testCode); + Assert.Null(ex); } + } + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static T Throws(Action testCode) + where T : Exception + { + return (T)Throws(typeof(T), testCode); + } + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// Generally used to test property accessors. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static T Throws(Func testCode) + where T : Exception + { + return (T)Throws(typeof(T), testCode); + } + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static Exception Throws(Type exceptionType, Action testCode) + { + Exception exception = RecordException(testCode); + return VerifyException(exceptionType, exception); + } + + /// + /// Verifies that the exact exception is thrown (and not a derived exception type). + /// Generally used to test property accessors. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static Exception Throws(Type exceptionType, Func testCode) + { + return Throws(exceptionType, () => { testCode(); }); + } + + /// + /// Verifies that an exception of the given type (or optionally a derived type) is thrown. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// Pass true to allow exceptions which derive from TException; pass false, otherwise + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static TException Throws(Action testCode, bool allowDerivedExceptions) + where TException : Exception + { + Type exceptionType = typeof(TException); + Exception exception = RecordException(testCode); - /// - /// Verifies that the exact exception is thrown (and not a derived exception type). - /// Generally used to test property accessors. - /// - /// The type of the exception expected to be thrown - /// A delegate to the code to be tested - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static T Throws(Func testCode) - where T : Exception + TargetInvocationException tie = exception as TargetInvocationException; + if (tie != null) { - return (T)Throws(typeof(T), testCode); + exception = tie.InnerException; } - /// - /// Verifies that the exact exception is thrown (and not a derived exception type). - /// - /// The type of the exception expected to be thrown - /// A delegate to the code to be tested - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static Exception Throws(Type exceptionType, Action testCode) + if (exception == null) { - Exception exception = RecordException(testCode); - return VerifyException(exceptionType, exception); + throw new ThrowsException(exceptionType); } - /// - /// Verifies that the exact exception is thrown (and not a derived exception type). - /// Generally used to test property accessors. - /// - /// The type of the exception expected to be thrown - /// A delegate to the code to be tested - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static Exception Throws(Type exceptionType, Func testCode) + var typedException = exception as TException; + if (typedException == null || (!allowDerivedExceptions && typedException.GetType() != typeof(TException))) { - return Throws(exceptionType, () => { testCode(); }); + throw new ThrowsException(exceptionType, exception); } - /// - /// Verifies that an exception of the given type (or optionally a derived type) is thrown. - /// - /// The type of the exception expected to be thrown - /// A delegate to the code to be tested - /// Pass true to allow exceptions which derive from TException; pass false, otherwise - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static TException Throws(Action testCode, bool allowDerivedExceptions) - where TException : Exception - { - Type exceptionType = typeof(TException); - Exception exception = RecordException(testCode); + return typedException; + } - TargetInvocationException tie = exception as TargetInvocationException; - if (tie != null) - { - exception = tie.InnerException; - } + /// + /// Verifies that an exception of the given type (or optionally a derived type) is thrown. + /// Generally used to test property accessors. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// Pass true to allow exceptions which derive from TException; pass false, otherwise + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static TException Throws(Func testCode, bool allowDerivedExceptions) + where TException : Exception + { + return Throws(() => { testCode(); }, allowDerivedExceptions); + } - if (exception == null) - { - throw new ThrowsException(exceptionType); - } + /// + /// Verifies that an exception of the given type (or optionally a derived type) is thrown. + /// Also verifies that the exception message matches. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception message to verify + /// Pass true to allow exceptions which derive from TException; pass false, otherwise + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static TException Throws(Action testCode, string exceptionMessage, bool allowDerivedExceptions = false, bool partialMatch = false) + where TException : Exception + { + var ex = Throws(testCode, allowDerivedExceptions); + VerifyExceptionMessage(ex, exceptionMessage, partialMatch); + return ex; + } - var typedException = exception as TException; - if (typedException == null || (!allowDerivedExceptions && typedException.GetType() != typeof(TException))) - { - throw new ThrowsException(exceptionType, exception); - } + /// + /// Verifies that an exception of the given type (or optionally a derived type) is thrown. + /// Also verified that the exception message matches. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception message to verify + /// Pass true to allow exceptions which derive from TException; pass false, otherwise + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static TException Throws(Func testCode, string exceptionMessage, bool allowDerivedExceptions = false) + where TException : Exception + { + return Throws(() => { testCode(); }, exceptionMessage, allowDerivedExceptions); + } - return typedException; - } + /// + /// Verifies that the code throws an (or optionally any exception which derives from it). + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// Pass true to allow exceptions which derive from TException; pass false, otherwise + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static ArgumentException ThrowsArgument(Action testCode, string paramName, bool allowDerivedExceptions = false) + { + var ex = Throws(testCode, allowDerivedExceptions); - /// - /// Verifies that an exception of the given type (or optionally a derived type) is thrown. - /// Generally used to test property accessors. - /// - /// The type of the exception expected to be thrown - /// A delegate to the code to be tested - /// Pass true to allow exceptions which derive from TException; pass false, otherwise - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static TException Throws(Func testCode, bool allowDerivedExceptions) - where TException : Exception + if (paramName != null) { - return Throws(() => { testCode(); }, allowDerivedExceptions); + Assert.Equal(paramName, ex.ParamName); } - /// - /// Verifies that an exception of the given type (or optionally a derived type) is thrown. - /// Also verifies that the exception message matches. - /// - /// The type of the exception expected to be thrown - /// A delegate to the code to be tested - /// The exception message to verify - /// Pass true to allow exceptions which derive from TException; pass false, otherwise - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static TException Throws(Action testCode, string exceptionMessage, bool allowDerivedExceptions = false, bool partialMatch = false) - where TException : Exception - { - var ex = Throws(testCode, allowDerivedExceptions); - VerifyExceptionMessage(ex, exceptionMessage, partialMatch); - return ex; - } + return ex; + } - /// - /// Verifies that an exception of the given type (or optionally a derived type) is thrown. - /// Also verified that the exception message matches. - /// - /// The type of the exception expected to be thrown - /// A delegate to the code to be tested - /// The exception message to verify - /// Pass true to allow exceptions which derive from TException; pass false, otherwise - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static TException Throws(Func testCode, string exceptionMessage, bool allowDerivedExceptions = false) - where TException : Exception + /// + /// Verifies that the code throws an (or optionally any exception which derives from it). + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception message to verify + /// Pass true to allow exceptions which derive from TException; pass false, otherwise + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static ArgumentException ThrowsArgument(Action testCode, string paramName, string exceptionMessage, bool allowDerivedExceptions = false) + { + var ex = Throws(testCode, allowDerivedExceptions); + + if (paramName != null) { - return Throws(() => { testCode(); }, exceptionMessage, allowDerivedExceptions); + Assert.Equal(paramName, ex.ParamName); } - /// - /// Verifies that the code throws an (or optionally any exception which derives from it). - /// - /// A delegate to the code to be tested - /// The name of the parameter that should throw the exception - /// Pass true to allow exceptions which derive from TException; pass false, otherwise - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static ArgumentException ThrowsArgument(Action testCode, string paramName, bool allowDerivedExceptions = false) - { - var ex = Throws(testCode, allowDerivedExceptions); + VerifyExceptionMessage(ex, exceptionMessage, partialMatch: true); - if (paramName != null) - { - Assert.Equal(paramName, ex.ParamName); - } + return ex; + } - return ex; - } + /// + /// Verifies that the code throws an ArgumentException (or optionally any exception which derives from it). + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// Pass true to allow exceptions which derive from TException; pass false, otherwise + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static ArgumentException ThrowsArgument(Func testCode, string paramName, bool allowDerivedExceptions = false) + { + var ex = Throws(testCode, allowDerivedExceptions); - /// - /// Verifies that the code throws an (or optionally any exception which derives from it). - /// - /// A delegate to the code to be tested - /// The name of the parameter that should throw the exception - /// The exception message to verify - /// Pass true to allow exceptions which derive from TException; pass false, otherwise - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static ArgumentException ThrowsArgument(Action testCode, string paramName, string exceptionMessage, bool allowDerivedExceptions = false) + if (paramName != null) { - var ex = Throws(testCode, allowDerivedExceptions); + Assert.Equal(paramName, ex.ParamName); + } - if (paramName != null) - { - Assert.Equal(paramName, ex.ParamName); - } + return ex; + } - VerifyExceptionMessage(ex, exceptionMessage, partialMatch: true); + /// + /// Verifies that the code throws an ArgumentNullException (or optionally any exception which derives from it). + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static ArgumentNullException ThrowsArgumentNull(Action testCode, string paramName) + { + var ex = Throws(testCode, allowDerivedExceptions: false); - return ex; + if (paramName != null) + { + Assert.Equal(paramName, ex.ParamName); } - /// - /// Verifies that the code throws an ArgumentException (or optionally any exception which derives from it). - /// - /// A delegate to the code to be tested - /// The name of the parameter that should throw the exception - /// Pass true to allow exceptions which derive from TException; pass false, otherwise - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static ArgumentException ThrowsArgument(Func testCode, string paramName, bool allowDerivedExceptions = false) - { - var ex = Throws(testCode, allowDerivedExceptions); + return ex; + } - if (paramName != null) - { - Assert.Equal(paramName, ex.ParamName); - } + /// + /// Verifies that the code throws an ArgumentNullException with the expected message that indicates that the value cannot + /// be null or empty. + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static ArgumentException ThrowsArgumentNullOrEmpty(Action testCode, string paramName) + { + return Throws(testCode, $"The argument '{paramName}' is null or empty. (Parameter '{paramName}')", allowDerivedExceptions: false); + } - return ex; - } + /// + /// Verifies that the code throws an ArgumentNullException with the expected message that indicates that the value cannot + /// be null or empty string. + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static ArgumentException ThrowsArgumentNullOrEmptyString(Action testCode, string paramName) + { + return ThrowsArgument(testCode, paramName, "Value cannot be null or an empty string.", allowDerivedExceptions: true); + } - /// - /// Verifies that the code throws an ArgumentNullException (or optionally any exception which derives from it). - /// - /// A delegate to the code to be tested - /// The name of the parameter that should throw the exception - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static ArgumentNullException ThrowsArgumentNull(Action testCode, string paramName) + /// + /// Verifies that the code throws an ArgumentOutOfRangeException (or optionally any exception which derives from it). + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception message to verify + /// Pass true to allow exceptions which derive from TException; pass false, otherwise + /// The actual value provided + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static ArgumentOutOfRangeException ThrowsArgumentOutOfRange(Action testCode, string paramName, string exceptionMessage, bool allowDerivedExceptions = false, object actualValue = null) + { + if (exceptionMessage != null) { - var ex = Throws(testCode, allowDerivedExceptions: false); - - if (paramName != null) + exceptionMessage = exceptionMessage + " (Parameter '" + paramName + "')"; + if (actualValue != null) { - Assert.Equal(paramName, ex.ParamName); + exceptionMessage += string.Format(CultureInfo.CurrentCulture, "\r\nActual value was {0}.", actualValue); } - - return ex; } - /// - /// Verifies that the code throws an ArgumentNullException with the expected message that indicates that the value cannot - /// be null or empty. - /// - /// A delegate to the code to be tested - /// The name of the parameter that should throw the exception - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static ArgumentException ThrowsArgumentNullOrEmpty(Action testCode, string paramName) - { - return Throws(testCode, $"The argument '{paramName}' is null or empty. (Parameter '{paramName}')", allowDerivedExceptions: false); - } + var ex = Throws(testCode, exceptionMessage, allowDerivedExceptions); - /// - /// Verifies that the code throws an ArgumentNullException with the expected message that indicates that the value cannot - /// be null or empty string. - /// - /// A delegate to the code to be tested - /// The name of the parameter that should throw the exception - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static ArgumentException ThrowsArgumentNullOrEmptyString(Action testCode, string paramName) + if (paramName != null) { - return ThrowsArgument(testCode, paramName, "Value cannot be null or an empty string.", allowDerivedExceptions: true); + Assert.Equal(paramName, ex.ParamName); } - /// - /// Verifies that the code throws an ArgumentOutOfRangeException (or optionally any exception which derives from it). - /// - /// A delegate to the code to be tested - /// The name of the parameter that should throw the exception - /// The exception message to verify - /// Pass true to allow exceptions which derive from TException; pass false, otherwise - /// The actual value provided - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static ArgumentOutOfRangeException ThrowsArgumentOutOfRange(Action testCode, string paramName, string exceptionMessage, bool allowDerivedExceptions = false, object actualValue = null) - { - if (exceptionMessage != null) - { - exceptionMessage = exceptionMessage + " (Parameter '" + paramName + "')"; - if (actualValue != null) - { - exceptionMessage += string.Format(CultureInfo.CurrentCulture, "\r\nActual value was {0}.", actualValue); - } - } + return ex; + } - var ex = Throws(testCode, exceptionMessage, allowDerivedExceptions); + /// + /// Verifies that the code throws an with the expected message that indicates that + /// the value must be greater than the given . + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The actual value provided. + /// The expected limit value. + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static ArgumentOutOfRangeException ThrowsArgumentGreaterThan(Action testCode, string paramName, string value, object actualValue = null) + { + return ThrowsArgumentOutOfRange( + testCode, + paramName, + String.Format(CultureInfo.CurrentCulture, "Value must be greater than {0}.", value), false, actualValue); + } - if (paramName != null) - { - Assert.Equal(paramName, ex.ParamName); - } + /// + /// Verifies that the code throws an with the expected message that indicates that + /// the value must be greater than or equal to the given value. + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The expected limit value. + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static ArgumentOutOfRangeException ThrowsArgumentGreaterThanOrEqualTo(Action testCode, string paramName, string value, object actualValue = null) + { + return ThrowsArgumentOutOfRange( + testCode, + paramName, + String.Format(CultureInfo.CurrentCulture, "Value must be greater than or equal to {0}.", value), false, actualValue); + } - return ex; - } + /// + /// Verifies that the code throws an with the expected message that indicates that + /// the value must be less than the given . + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The actual value provided. + /// The expected limit value. + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static ArgumentOutOfRangeException ThrowsArgumentLessThan(Action testCode, string paramName, string maxValue, object actualValue = null) + { + return ThrowsArgumentOutOfRange( + testCode, + paramName, + String.Format(CultureInfo.CurrentCulture, "Value must be less than {0}.", maxValue), false, actualValue); + } - /// - /// Verifies that the code throws an with the expected message that indicates that - /// the value must be greater than the given . - /// - /// A delegate to the code to be tested - /// The name of the parameter that should throw the exception - /// The actual value provided. - /// The expected limit value. - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static ArgumentOutOfRangeException ThrowsArgumentGreaterThan(Action testCode, string paramName, string value, object actualValue = null) + /// + /// Verifies that the code throws an with the expected message that indicates that + /// the value must be less than or equal to the given . + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The actual value provided. + /// The expected limit value. + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static ArgumentOutOfRangeException ThrowsArgumentLessThanOrEqualTo(Action testCode, string paramName, string maxValue, object actualValue = null) + { + return ThrowsArgumentOutOfRange( + testCode, + paramName, + String.Format(CultureInfo.CurrentCulture, "Value must be less than or equal to {0}.", maxValue), false, actualValue); + } + + /// + /// Verifies that the code throws an InvalidEnumArgumentException (or optionally any exception which derives from it). + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The expected invalid value that should appear in the message + /// The type of the enumeration + /// Pass true to allow exceptions which derive from TException; pass false, otherwise + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static InvalidEnumArgumentException ThrowsInvalidEnumArgument(Action testCode, string paramName, int invalidValue, Type enumType, bool allowDerivedExceptions = false) + { + string message = String.Format(CultureInfo.CurrentCulture, + "The value of argument '{0}' ({1}) is invalid for Enum type '{2}'. (Parameter '{0}')", + paramName, invalidValue, enumType.Name); + return Throws(testCode, message, allowDerivedExceptions); + } + + /// + /// Verifies that the code throws an HttpException (or optionally any exception which derives from it). + /// + /// A delegate to the code to be tested + /// The name of the object that was dispose + /// Pass true to allow exceptions which derive from TException; pass false, otherwise + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static ObjectDisposedException ThrowsObjectDisposed(Action testCode, string objectName, bool allowDerivedExceptions = false) + { + var ex = Throws(testCode, allowDerivedExceptions); + + if (objectName != null) { - return ThrowsArgumentOutOfRange( - testCode, - paramName, - String.Format(CultureInfo.CurrentCulture, "Value must be greater than {0}.", value), false, actualValue); + Assert.Equal(objectName, ex.ObjectName); } - /// - /// Verifies that the code throws an with the expected message that indicates that - /// the value must be greater than or equal to the given value. - /// - /// A delegate to the code to be tested - /// The name of the parameter that should throw the exception - /// The expected limit value. - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static ArgumentOutOfRangeException ThrowsArgumentGreaterThanOrEqualTo(Action testCode, string paramName, string value, object actualValue = null) + return ex; + } + + /// + /// Verifies that an exception of the given type is thrown. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + /// + /// Unlike other Throws* methods, this method does not enforce running the exception delegate with a known Thread Culture. + /// + public static async Task ThrowsAsync(Func testCode, bool allowDerivedExceptions = false) + where TException : Exception + { + Type exceptionType = typeof(TException); + Exception exception = null; + try { - return ThrowsArgumentOutOfRange( - testCode, - paramName, - String.Format(CultureInfo.CurrentCulture, "Value must be greater than or equal to {0}.", value), false, actualValue); + // The 'testCode' Task might execute asynchronously in a different thread making it hard to enforce the thread culture. + // The correct way to verify exception messages in such a scenario would be to run the task synchronously inside of a + // culture enforced block. + await testCode(); } - - /// - /// Verifies that the code throws an with the expected message that indicates that - /// the value must be less than the given . - /// - /// A delegate to the code to be tested - /// The name of the parameter that should throw the exception - /// The actual value provided. - /// The expected limit value. - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static ArgumentOutOfRangeException ThrowsArgumentLessThan(Action testCode, string paramName, string maxValue, object actualValue = null) + catch (Exception ex) { - return ThrowsArgumentOutOfRange( - testCode, - paramName, - String.Format(CultureInfo.CurrentCulture, "Value must be less than {0}.", maxValue), false, actualValue); + exception = ex; } - /// - /// Verifies that the code throws an with the expected message that indicates that - /// the value must be less than or equal to the given . - /// - /// A delegate to the code to be tested - /// The name of the parameter that should throw the exception - /// The actual value provided. - /// The expected limit value. - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static ArgumentOutOfRangeException ThrowsArgumentLessThanOrEqualTo(Action testCode, string paramName, string maxValue, object actualValue = null) + if (exception == null) { - return ThrowsArgumentOutOfRange( - testCode, - paramName, - String.Format(CultureInfo.CurrentCulture, "Value must be less than or equal to {0}.", maxValue), false, actualValue); + throw new ThrowsException(exceptionType); } - /// - /// Verifies that the code throws an InvalidEnumArgumentException (or optionally any exception which derives from it). - /// - /// A delegate to the code to be tested - /// The name of the parameter that should throw the exception - /// The expected invalid value that should appear in the message - /// The type of the enumeration - /// Pass true to allow exceptions which derive from TException; pass false, otherwise - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static InvalidEnumArgumentException ThrowsInvalidEnumArgument(Action testCode, string paramName, int invalidValue, Type enumType, bool allowDerivedExceptions = false) + var typedException = exception as TException; + if (typedException == null || (!allowDerivedExceptions && typedException.GetType() != typeof(TException))) { - string message = String.Format(CultureInfo.CurrentCulture, - "The value of argument '{0}' ({1}) is invalid for Enum type '{2}'. (Parameter '{0}')", - paramName, invalidValue, enumType.Name); - return Throws(testCode, message, allowDerivedExceptions); + throw new ThrowsException(exceptionType, exception); } - /// - /// Verifies that the code throws an HttpException (or optionally any exception which derives from it). - /// - /// A delegate to the code to be tested - /// The name of the object that was dispose - /// Pass true to allow exceptions which derive from TException; pass false, otherwise - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static ObjectDisposedException ThrowsObjectDisposed(Action testCode, string objectName, bool allowDerivedExceptions = false) - { - var ex = Throws(testCode, allowDerivedExceptions); + return (TException)exception; + } - if (objectName != null) - { - Assert.Equal(objectName, ex.ObjectName); - } + /// + /// Verifies that an exception of the given type (or optionally a derived type) is thrown. + /// Also verifies that the exception message matches. + /// + /// The type of the exception expected to be thrown + /// A delegate to the code to be tested + /// The exception message to verify + /// Pass true to allow exceptions which derive from TException; pass false, otherwise + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static async Task ThrowsAsync(Func testCode, string exceptionMessage, bool allowDerivedExceptions = false, bool partialMatch = false) + where TException : Exception + { + var ex = await ThrowsAsync(testCode, allowDerivedExceptions); + VerifyExceptionMessage(ex, exceptionMessage, partialMatch); + return ex; + } - return ex; - } + /// + /// Verifies that the code throws an ArgumentNullException (or optionally any exception which derives from it). + /// + /// A delegate to the code to be tested + /// The name of the parameter that should throw the exception + /// The exception that was thrown, when successful + /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown + public static async Task ThrowsArgumentNullAsync(Func testCode, string paramName) + { + var ex = await ThrowsAsync(testCode); - /// - /// Verifies that an exception of the given type is thrown. - /// - /// The type of the exception expected to be thrown - /// A delegate to the code to be tested - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - /// - /// Unlike other Throws* methods, this method does not enforce running the exception delegate with a known Thread Culture. - /// - public static async Task ThrowsAsync(Func testCode, bool allowDerivedExceptions = false) - where TException : Exception + if (paramName != null) { - Type exceptionType = typeof(TException); - Exception exception = null; - try - { - // The 'testCode' Task might execute asynchronously in a different thread making it hard to enforce the thread culture. - // The correct way to verify exception messages in such a scenario would be to run the task synchronously inside of a - // culture enforced block. - await testCode(); - } - catch (Exception ex) - { - exception = ex; - } - - if (exception == null) - { - throw new ThrowsException(exceptionType); - } + Assert.Equal(paramName, ex.ParamName); + } - var typedException = exception as TException; - if (typedException == null || (!allowDerivedExceptions && typedException.GetType() != typeof(TException))) - { - throw new ThrowsException(exceptionType, exception); - } + return ex; + } - return (TException)exception; + // We've re-implemented all the xUnit.net Throws code so that we can get this + // updated implementation of RecordException which silently unwraps any instances + // of AggregateException. In addition to unwrapping exceptions, this method ensures + // that tests are executed in with a known set of Culture and UICulture. This prevents + // tests from failing when executed on a non-English machine. + public static Exception RecordException(Action testCode) + { + try + { + testCode(); + return null; } - - /// - /// Verifies that an exception of the given type (or optionally a derived type) is thrown. - /// Also verifies that the exception message matches. - /// - /// The type of the exception expected to be thrown - /// A delegate to the code to be tested - /// The exception message to verify - /// Pass true to allow exceptions which derive from TException; pass false, otherwise - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static async Task ThrowsAsync(Func testCode, string exceptionMessage, bool allowDerivedExceptions = false, bool partialMatch = false) - where TException : Exception + catch (Exception exception) { - var ex = await ThrowsAsync(testCode, allowDerivedExceptions); - VerifyExceptionMessage(ex, exceptionMessage, partialMatch); - return ex; + return UnwrapException(exception); } + } - /// - /// Verifies that the code throws an ArgumentNullException (or optionally any exception which derives from it). - /// - /// A delegate to the code to be tested - /// The name of the parameter that should throw the exception - /// The exception that was thrown, when successful - /// Thrown when an exception was not thrown, or when an exception of the incorrect type is thrown - public static async Task ThrowsArgumentNullAsync(Func testCode, string paramName) + private static Exception UnwrapException(Exception exception) + { + AggregateException aggEx = exception as AggregateException; + if (aggEx != null) { - var ex = await ThrowsAsync(testCode); - - if (paramName != null) - { - Assert.Equal(paramName, ex.ParamName); - } - - return ex; + return aggEx.GetBaseException(); } + return exception; + } - // We've re-implemented all the xUnit.net Throws code so that we can get this - // updated implementation of RecordException which silently unwraps any instances - // of AggregateException. In addition to unwrapping exceptions, this method ensures - // that tests are executed in with a known set of Culture and UICulture. This prevents - // tests from failing when executed on a non-English machine. - public static Exception RecordException(Action testCode) + private static Exception VerifyException(Type exceptionType, Exception exception) + { + if (exception == null) { - try - { - testCode(); - return null; - } - catch (Exception exception) - { - return UnwrapException(exception); - } + throw new ThrowsException(exceptionType); } - - private static Exception UnwrapException(Exception exception) + else if (exceptionType != exception.GetType()) { - AggregateException aggEx = exception as AggregateException; - if (aggEx != null) - { - return aggEx.GetBaseException(); - } - return exception; + throw new ThrowsException(exceptionType, exception); } - private static Exception VerifyException(Type exceptionType, Exception exception) + return exception; + } + + private static void VerifyExceptionMessage(Exception exception, string expectedMessage, bool partialMatch = false) + { + if (expectedMessage != null) { - if (exception == null) + if (!partialMatch) { - throw new ThrowsException(exceptionType); + Assert.Equal(expectedMessage, exception.Message); } - else if (exceptionType != exception.GetType()) + else { - throw new ThrowsException(exceptionType, exception); - } - - return exception; - } - - private static void VerifyExceptionMessage(Exception exception, string expectedMessage, bool partialMatch = false) - { - if (expectedMessage != null) - { - if (!partialMatch) - { - Assert.Equal(expectedMessage, exception.Message); - } - else - { - Assert.Contains(expectedMessage, exception.Message); - } + Assert.Contains(expectedMessage, exception.Message); } } + } - // Custom ThrowsException so we can filter the stack trace. - [Serializable] - private class ThrowsException : Xunit.Sdk.ThrowsException - { - public ThrowsException(Type type) : base(type) { } + // Custom ThrowsException so we can filter the stack trace. + [Serializable] + private class ThrowsException : Xunit.Sdk.ThrowsException + { + public ThrowsException(Type type) : base(type) { } - public ThrowsException(Type type, Exception ex) : base(type, ex) { } - } + public ThrowsException(Type type, Exception ex) : base(type, ex) { } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Commons/ExpressionLexerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Commons/ExpressionLexerTests.cs index a198b256c..5480b88bc 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Commons/ExpressionLexerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Commons/ExpressionLexerTests.cs @@ -10,163 +10,162 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Commons +namespace Microsoft.AspNetCore.OData.Tests.Commons; + +public class ExpressionLexerTests { - public class ExpressionLexerTests + [Fact] + public void CtorExpressionLexer_ThrowsArgumentNull_Expression() { - [Fact] - public void CtorExpressionLexer_ThrowsArgumentNull_Expression() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ExpressionLexer(null), "expression"); - } - - [Theory] - [InlineData("42")] - [InlineData("123.001")] - [InlineData("72BB05FC-38CE-44DB-A13F-974B3E623908")] - [InlineData("' abc''=,'")] - [InlineData("Pròjè_x00A2_tÎð瑞갂థ్క_x0020_Iiلإَّ")] - public void ExpressionLexerInitialShouldReturnLiteral(string expression) - { - // Arrange - ExpressionLexer lexer = new ExpressionLexer(expression); - - // Act - ExpressionToken token = lexer.CurrentToken; - Assert.Equal(ExpressionTokenKind.Literal, token.Kind); - Assert.Equal(expression, token.Text); - Assert.Equal(0, token.Position); - } - - [Fact] - public void ExpressionLexerCanParseEqualStartingExpression() - { - // Arrange - ExpressionLexer lexer = new ExpressionLexer(" = abc"); - - // Act - Assert.Equal(ExpressionTokenKind.Equal, lexer.CurrentToken.Kind); + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ExpressionLexer(null), "expression"); + } - lexer.NextToken(); - Assert.Equal(ExpressionTokenKind.Literal, lexer.CurrentToken.Kind); - Assert.Equal("abc", lexer.CurrentToken.Text); - Assert.Equal(3, lexer.CurrentToken.Position); + [Theory] + [InlineData("42")] + [InlineData("123.001")] + [InlineData("72BB05FC-38CE-44DB-A13F-974B3E623908")] + [InlineData("' abc''=,'")] + [InlineData("Pròjè_x00A2_tÎð瑞갂థ్క_x0020_Iiلإَّ")] + public void ExpressionLexerInitialShouldReturnLiteral(string expression) + { + // Arrange + ExpressionLexer lexer = new ExpressionLexer(expression); + + // Act + ExpressionToken token = lexer.CurrentToken; + Assert.Equal(ExpressionTokenKind.Literal, token.Kind); + Assert.Equal(expression, token.Text); + Assert.Equal(0, token.Position); + } - lexer.NextToken(); - Assert.Equal(ExpressionTokenKind.TextEnd, lexer.CurrentToken.Kind); - } + [Fact] + public void ExpressionLexerCanParseEqualStartingExpression() + { + // Arrange + ExpressionLexer lexer = new ExpressionLexer(" = abc"); - [Fact] - public void ExpressionLexerCanParseCommaStartingExpression() - { - // Arrange - ExpressionLexer lexer = new ExpressionLexer(" , ,abc"); + // Act + Assert.Equal(ExpressionTokenKind.Equal, lexer.CurrentToken.Kind); - // Act - Assert.Equal(ExpressionTokenKind.Comma, lexer.CurrentToken.Kind); // first , + lexer.NextToken(); + Assert.Equal(ExpressionTokenKind.Literal, lexer.CurrentToken.Kind); + Assert.Equal("abc", lexer.CurrentToken.Text); + Assert.Equal(3, lexer.CurrentToken.Position); - lexer.NextToken(); - Assert.Equal(ExpressionTokenKind.Comma, lexer.CurrentToken.Kind); // second , + lexer.NextToken(); + Assert.Equal(ExpressionTokenKind.TextEnd, lexer.CurrentToken.Kind); + } - lexer.NextToken(); - Assert.Equal(ExpressionTokenKind.Literal, lexer.CurrentToken.Kind); - Assert.Equal("abc", lexer.CurrentToken.Text); - Assert.Equal(4, lexer.CurrentToken.Position); + [Fact] + public void ExpressionLexerCanParseCommaStartingExpression() + { + // Arrange + ExpressionLexer lexer = new ExpressionLexer(" , ,abc"); - lexer.NextToken(); - Assert.Equal(ExpressionTokenKind.TextEnd, lexer.CurrentToken.Kind); - } + // Act + Assert.Equal(ExpressionTokenKind.Comma, lexer.CurrentToken.Kind); // first , - [Fact] - public void ExpressionLexerShouldThrows() - { - // Arrange - ExpressionLexer lexer = new ExpressionLexer("name='xzg"); + lexer.NextToken(); + Assert.Equal(ExpressionTokenKind.Comma, lexer.CurrentToken.Kind); // second , - // Act - lexer.NextToken(); // move to '=' - Action test = () => lexer.NextToken(); // test 'xzg + lexer.NextToken(); + Assert.Equal(ExpressionTokenKind.Literal, lexer.CurrentToken.Kind); + Assert.Equal("abc", lexer.CurrentToken.Text); + Assert.Equal(4, lexer.CurrentToken.Position); - ExceptionAssert.Throws(test, "There is an unterminated string literal at position 9 in 'name='xzg'."); - } + lexer.NextToken(); + Assert.Equal(ExpressionTokenKind.TextEnd, lexer.CurrentToken.Kind); + } - [Fact] - public void ExpressionLexerShouldParseComplexKeyValuePairs() - { - // Arrange - string expression = " 123 = true, inOffice = 12345, name = 'abc,''efg' , 瑞갂థ్క= @p"; - ExpressionLexer lexer = new ExpressionLexer(expression); + [Fact] + public void ExpressionLexerShouldThrows() + { + // Arrange + ExpressionLexer lexer = new ExpressionLexer("name='xzg"); - // Act - Action verifyAction = (key, value, hasComma) => - { - Assert.Equal(ExpressionTokenKind.Literal, lexer.CurrentToken.Kind); - Assert.Equal(key, lexer.CurrentToken.Text); + // Act + lexer.NextToken(); // move to '=' + Action test = () => lexer.NextToken(); // test 'xzg - lexer.NextToken(); - Assert.Equal(ExpressionTokenKind.Equal, lexer.CurrentToken.Kind); + ExceptionAssert.Throws(test, "There is an unterminated string literal at position 9 in 'name='xzg'."); + } - lexer.NextToken(); - Assert.Equal(ExpressionTokenKind.Literal, lexer.CurrentToken.Kind); - Assert.Equal(value, lexer.CurrentToken.Text); + [Fact] + public void ExpressionLexerShouldParseComplexKeyValuePairs() + { + // Arrange + string expression = " 123 = true, inOffice = 12345, name = 'abc,''efg' , 瑞갂థ్క= @p"; + ExpressionLexer lexer = new ExpressionLexer(expression); - lexer.NextToken(); - if (hasComma) - { - Assert.Equal(ExpressionTokenKind.Comma, lexer.CurrentToken.Kind); - lexer.NextToken(); - } - else - { - Assert.Equal(ExpressionTokenKind.TextEnd, lexer.CurrentToken.Kind); - } - }; - - verifyAction("123", "true", true); - verifyAction("inOffice", "12345", true); - verifyAction("name", "'abc,''efg'", true); - verifyAction("瑞갂థ్క", "@p", false); - } - - [Fact] - public void ExpressionLexerCanParseCurlyBraceExpression() + // Act + Action verifyAction = (key, value, hasComma) => { - // Arrange - ExpressionLexer lexer = new ExpressionLexer(" { abc }"); - - // Act Assert.Equal(ExpressionTokenKind.Literal, lexer.CurrentToken.Kind); - Assert.Equal("{ abc }", lexer.CurrentToken.Text); - Assert.Equal(1, lexer.CurrentToken.Position); + Assert.Equal(key, lexer.CurrentToken.Text); lexer.NextToken(); - Assert.Equal(ExpressionTokenKind.TextEnd, lexer.CurrentToken.Kind); - } - - [Fact] - public void ExpressionLexerCanParseBracketsExpression() - { - // Arrange - ExpressionLexer lexer = new ExpressionLexer(" [ abc ] "); + Assert.Equal(ExpressionTokenKind.Equal, lexer.CurrentToken.Kind); - // Act + lexer.NextToken(); Assert.Equal(ExpressionTokenKind.Literal, lexer.CurrentToken.Kind); - Assert.Equal("[ abc ]", lexer.CurrentToken.Text); - Assert.Equal(2, lexer.CurrentToken.Position); + Assert.Equal(value, lexer.CurrentToken.Text); lexer.NextToken(); - Assert.Equal(ExpressionTokenKind.TextEnd, lexer.CurrentToken.Kind); - } + if (hasComma) + { + Assert.Equal(ExpressionTokenKind.Comma, lexer.CurrentToken.Kind); + lexer.NextToken(); + } + else + { + Assert.Equal(ExpressionTokenKind.TextEnd, lexer.CurrentToken.Kind); + } + }; + + verifyAction("123", "true", true); + verifyAction("inOffice", "12345", true); + verifyAction("name", "'abc,''efg'", true); + verifyAction("瑞갂థ్క", "@p", false); + } - [Fact] - public void ExpressionLexerThrowsUnbalancedBracketsExpression() - { - // Arrange & Act - Action test = () => new ExpressionLexer(" [ abc ) "); + [Fact] + public void ExpressionLexerCanParseCurlyBraceExpression() + { + // Arrange + ExpressionLexer lexer = new ExpressionLexer(" { abc }"); + + // Act + Assert.Equal(ExpressionTokenKind.Literal, lexer.CurrentToken.Kind); + Assert.Equal("{ abc }", lexer.CurrentToken.Text); + Assert.Equal(1, lexer.CurrentToken.Position); + + lexer.NextToken(); + Assert.Equal(ExpressionTokenKind.TextEnd, lexer.CurrentToken.Kind); + } + + [Fact] + public void ExpressionLexerCanParseBracketsExpression() + { + // Arrange + ExpressionLexer lexer = new ExpressionLexer(" [ abc ] "); + + // Act + Assert.Equal(ExpressionTokenKind.Literal, lexer.CurrentToken.Kind); + Assert.Equal("[ abc ]", lexer.CurrentToken.Text); + Assert.Equal(2, lexer.CurrentToken.Position); + + lexer.NextToken(); + Assert.Equal(ExpressionTokenKind.TextEnd, lexer.CurrentToken.Kind); + } + + [Fact] + public void ExpressionLexerThrowsUnbalancedBracketsExpression() + { + // Arrange & Act + Action test = () => new ExpressionLexer(" [ abc ) "); - // Assert - ExceptionAssert.Throws(test, "Found an unbalanced bracket '[' and ']' expression."); - } + // Assert + ExceptionAssert.Throws(test, "Found an unbalanced bracket '[' and ']' expression."); } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Commons/FastPropertyAccessorTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Commons/FastPropertyAccessorTest.cs index ec6c76fdd..2ce335e21 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Commons/FastPropertyAccessorTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Commons/FastPropertyAccessorTest.cs @@ -8,120 +8,119 @@ using Microsoft.AspNetCore.OData.Common; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Commons +namespace Microsoft.AspNetCore.OData.Tests.Commons; + +public class FastPropertyAccessorTest { - public class FastPropertyAccessorTest + [Fact] + public void Ctor_ThrowsArgumentNull_Property() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new FastPropertyAccessor(null), "property"); + } + + [Fact] + public void Ctor_ThrowsArgument_Property() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgument( + () => new FastPropertyAccessor(typeof(MyProps).GetProperty("DoubleProp")), + "property", "The PropertyInfo provided must have public 'get' and 'set' accessor methods."); + } + + [Fact] + public void GetterWorksForValueType() + { + // Arrange + var mine = new MyProps(); + var accessor = new FastPropertyAccessor(mine.GetType().GetProperty("IntProp")); + mine.IntProp = 4; + + // Act & Assert + Assert.Equal(4, accessor.GetValue(mine)); + } + + [Fact] + public void SetterWorksForValueType() + { + // Arrange + var mine = new MyProps(); + var accessor = new FastPropertyAccessor(mine.GetType().GetProperty("IntProp")); + mine.IntProp = 4; + + // Act + accessor.SetValue(mine, 3); + + // Assert + Assert.Equal(3, accessor.GetValue(mine)); + Assert.Equal(3, mine.IntProp); + } + + [Fact] + public void Getter_ThrowsArgumentNull_Instance() + { + // Arrange & Act & Assert + var mine = new MyProps(); + var accessor = new FastPropertyAccessor(mine.GetType().GetProperty("StringProp")); + ExceptionAssert.ThrowsArgumentNull(() => accessor.GetValue(null), "instance"); + } + + [Fact] + public void GetterWorksForReferenceType() { - [Fact] - public void Ctor_ThrowsArgumentNull_Property() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new FastPropertyAccessor(null), "property"); - } - - [Fact] - public void Ctor_ThrowsArgument_Property() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgument( - () => new FastPropertyAccessor(typeof(MyProps).GetProperty("DoubleProp")), - "property", "The PropertyInfo provided must have public 'get' and 'set' accessor methods."); - } - - [Fact] - public void GetterWorksForValueType() - { - // Arrange - var mine = new MyProps(); - var accessor = new FastPropertyAccessor(mine.GetType().GetProperty("IntProp")); - mine.IntProp = 4; - - // Act & Assert - Assert.Equal(4, accessor.GetValue(mine)); - } - - [Fact] - public void SetterWorksForValueType() - { - // Arrange - var mine = new MyProps(); - var accessor = new FastPropertyAccessor(mine.GetType().GetProperty("IntProp")); - mine.IntProp = 4; - - // Act - accessor.SetValue(mine, 3); - - // Assert - Assert.Equal(3, accessor.GetValue(mine)); - Assert.Equal(3, mine.IntProp); - } - - [Fact] - public void Getter_ThrowsArgumentNull_Instance() - { - // Arrange & Act & Assert - var mine = new MyProps(); - var accessor = new FastPropertyAccessor(mine.GetType().GetProperty("StringProp")); - ExceptionAssert.ThrowsArgumentNull(() => accessor.GetValue(null), "instance"); - } - - [Fact] - public void GetterWorksForReferenceType() - { - // Arrange - var mine = new MyProps(); - var accessor = new FastPropertyAccessor(mine.GetType().GetProperty("StringProp")); - mine.StringProp = "*4"; - - // Act & Assert - Assert.Equal("*4", accessor.GetValue(mine)); - } - - [Fact] - public void Setter_ThrowsArgumentNull_Instance() - { - // Arrange & Act & Assert - var mine = new MyProps(); - var accessor = new FastPropertyAccessor(mine.GetType().GetProperty("StringProp")); - ExceptionAssert.ThrowsArgumentNull(() => accessor.SetValue(null, 5), "instance"); - } - - [Fact] - public void SetterWorksForReferenceType() - { - // Arrange - var mine = new MyProps(); - var accessor = new FastPropertyAccessor(mine.GetType().GetProperty("StringProp")); - mine.StringProp = "*4"; - - // Act - accessor.SetValue(mine, "#3"); - - // Assert - Assert.Equal("#3", accessor.GetValue(mine)); - Assert.Equal("#3", mine.StringProp); - } - - [Fact] - public void Copy_ThrowsArgumentNull_FromAndTo() - { - // Arrange - var accessor = new FastPropertyAccessor(typeof(MyProps).GetProperty("StringProp")); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => accessor.Copy(null, null), "from"); - - // Act & Assert - MyProps mine = new MyProps(); - ExceptionAssert.ThrowsArgumentNull(() => accessor.Copy(mine, null), "to"); - } - - public class MyProps - { - public int IntProp { get; set; } - public string StringProp { get; set; } - - public double DoubleProp { get; } - } + // Arrange + var mine = new MyProps(); + var accessor = new FastPropertyAccessor(mine.GetType().GetProperty("StringProp")); + mine.StringProp = "*4"; + + // Act & Assert + Assert.Equal("*4", accessor.GetValue(mine)); + } + + [Fact] + public void Setter_ThrowsArgumentNull_Instance() + { + // Arrange & Act & Assert + var mine = new MyProps(); + var accessor = new FastPropertyAccessor(mine.GetType().GetProperty("StringProp")); + ExceptionAssert.ThrowsArgumentNull(() => accessor.SetValue(null, 5), "instance"); + } + + [Fact] + public void SetterWorksForReferenceType() + { + // Arrange + var mine = new MyProps(); + var accessor = new FastPropertyAccessor(mine.GetType().GetProperty("StringProp")); + mine.StringProp = "*4"; + + // Act + accessor.SetValue(mine, "#3"); + + // Assert + Assert.Equal("#3", accessor.GetValue(mine)); + Assert.Equal("#3", mine.StringProp); + } + + [Fact] + public void Copy_ThrowsArgumentNull_FromAndTo() + { + // Arrange + var accessor = new FastPropertyAccessor(typeof(MyProps).GetProperty("StringProp")); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => accessor.Copy(null, null), "from"); + + // Act & Assert + MyProps mine = new MyProps(); + ExceptionAssert.ThrowsArgumentNull(() => accessor.Copy(mine, null), "to"); + } + + public class MyProps + { + public int IntProp { get; set; } + public string StringProp { get; set; } + + public double DoubleProp { get; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Commons/InMemoryMessage.cs b/test/Microsoft.AspNetCore.OData.Tests/Commons/InMemoryMessage.cs index c2f0c03da..baa215d93 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Commons/InMemoryMessage.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Commons/InMemoryMessage.cs @@ -11,61 +11,60 @@ using System.Threading.Tasks; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Tests.Commons +namespace Microsoft.AspNetCore.OData.Tests.Commons; + +public class InMemoryMessage : IODataRequestMessageAsync, IODataResponseMessageAsync, IDisposable { - public class InMemoryMessage : IODataRequestMessageAsync, IODataResponseMessageAsync, IDisposable - { - private readonly Dictionary headers; + private readonly Dictionary headers; - public InMemoryMessage() - { - headers = new Dictionary(); - } + public InMemoryMessage() + { + headers = new Dictionary(); + } - public IEnumerable> Headers - { - get { return this.headers; } - } + public IEnumerable> Headers + { + get { return this.headers; } + } - public int StatusCode { get; set; } + public int StatusCode { get; set; } - public Uri Url { get; set; } + public Uri Url { get; set; } - public string Method { get; set; } + public string Method { get; set; } - public Stream Stream { get; set; } + public Stream Stream { get; set; } - public IServiceProvider Container { get; set; } + public IServiceProvider Container { get; set; } - public string GetHeader(string headerName) - { - string headerValue; - return this.headers.TryGetValue(headerName, out headerValue) ? headerValue : null; - } + public string GetHeader(string headerName) + { + string headerValue; + return this.headers.TryGetValue(headerName, out headerValue) ? headerValue : null; + } - public void SetHeader(string headerName, string headerValue) - { - headers[headerName] = headerValue; - } + public void SetHeader(string headerName, string headerValue) + { + headers[headerName] = headerValue; + } - public Stream GetStream() - { - return this.Stream; - } + public Stream GetStream() + { + return this.Stream; + } - public Action DisposeAction { get; set; } + public Action DisposeAction { get; set; } - void IDisposable.Dispose() + void IDisposable.Dispose() + { + if (this.DisposeAction != null) { - if (this.DisposeAction != null) - { - this.DisposeAction(); - } + this.DisposeAction(); } + } - public Task GetStreamAsync() - { - return Task.FromResult(this.Stream); - } + public Task GetStreamAsync() + { + return Task.FromResult(this.Stream); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Commons/KeyValuePairParserTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Commons/KeyValuePairParserTests.cs index 7608e3cef..7b98151a0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Commons/KeyValuePairParserTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Commons/KeyValuePairParserTests.cs @@ -11,123 +11,122 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Commons +namespace Microsoft.AspNetCore.OData.Tests.Commons; + +public class KeyValuePairParseTests { - public class KeyValuePairParseTests + [Fact] + public void ParseThrowsUnterminatedStringLiteral() + { + // Arrange & Act & Assert + Action test = () => KeyValuePairParser.Parse(" inOffice='xzg "); + + ExceptionAssert.Throws(test, "There is an unterminated string literal at position 16 in ' inOffice='xzg '."); + } + + [Theory] + [InlineData(" =key", 2)] + [InlineData(" ,", 2)] + [InlineData(" =key value", 2)] + [InlineData(" key value", 10)] + [InlineData(" key value=", 10)] + public void ParseThrowsSyntaxError(string expression, int pos) + { + // Arrange & Act & Assert + Action test = () => KeyValuePairParser.Parse(expression); + + ExceptionAssert.Throws(test, string.Format("Syntax error at position {0} in '{1}'.", pos, expression)); + } + + [Fact] + public void ParseThrowsMultipleLiteralsNotAllowed() + { + // Arrange & Act & Assert + Action test = () => KeyValuePairParser.Parse(" inOffice , xzg "); + + ExceptionAssert.Throws(test, "' inOffice , xzg ' is not a valid expression.Single literal is only for single key. Multiple keys should use key=value."); + } + + [Theory] + [InlineData("inOffice = 1, xzg")] + [InlineData("xzg, inOffice = 1 ")] + public void ParseRetunsCorrectlyForSingleLiteralAndKeyValue(string expression) + { + // Arrange & Act + IDictionary pairs = KeyValuePairParser.Parse(expression); + + // Assert + Assert.NotNull(pairs); + Assert.Equal(2, pairs.Count); + Assert.Equal("xzg", pairs[string.Empty]); + Assert.Equal("1", pairs["inOffice"]); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void ParseReturnsNullForNullEmptyOrWhitespaceString(string expression) + { + // Arrange & Act + IDictionary pairs = KeyValuePairParser.Parse(expression); + + // Assert + Assert.Null(pairs); + + // Arrange & Act & Assert + Assert.False(KeyValuePairParser.TryParse(expression, out _)); + } + + [Theory] + [InlineData("123=true,inOffice=12345,name='abc,''efg'")] + [InlineData("123 = true , inOffice = 12345 , name= 'abc,''efg' ")] + [InlineData(" 123 = true , inOffice = 12345 , name= 'abc,''efg'")] + public void ParseComplexFunctionParametersWorks(string expression) { - [Fact] - public void ParseThrowsUnterminatedStringLiteral() - { - // Arrange & Act & Assert - Action test = () => KeyValuePairParser.Parse(" inOffice='xzg "); - - ExceptionAssert.Throws(test, "There is an unterminated string literal at position 16 in ' inOffice='xzg '."); - } - - [Theory] - [InlineData(" =key", 2)] - [InlineData(" ,", 2)] - [InlineData(" =key value", 2)] - [InlineData(" key value", 10)] - [InlineData(" key value=", 10)] - public void ParseThrowsSyntaxError(string expression, int pos) - { - // Arrange & Act & Assert - Action test = () => KeyValuePairParser.Parse(expression); - - ExceptionAssert.Throws(test, string.Format("Syntax error at position {0} in '{1}'.", pos, expression)); - } - - [Fact] - public void ParseThrowsMultipleLiteralsNotAllowed() - { - // Arrange & Act & Assert - Action test = () => KeyValuePairParser.Parse(" inOffice , xzg "); - - ExceptionAssert.Throws(test, "' inOffice , xzg ' is not a valid expression.Single literal is only for single key. Multiple keys should use key=value."); - } - - [Theory] - [InlineData("inOffice = 1, xzg")] - [InlineData("xzg, inOffice = 1 ")] - public void ParseRetunsCorrectlyForSingleLiteralAndKeyValue(string expression) - { - // Arrange & Act - IDictionary pairs = KeyValuePairParser.Parse(expression); - - // Assert - Assert.NotNull(pairs); - Assert.Equal(2, pairs.Count); - Assert.Equal("xzg", pairs[string.Empty]); - Assert.Equal("1", pairs["inOffice"]); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public void ParseReturnsNullForNullEmptyOrWhitespaceString(string expression) - { - // Arrange & Act - IDictionary pairs = KeyValuePairParser.Parse(expression); - - // Assert - Assert.Null(pairs); - - // Arrange & Act & Assert - Assert.False(KeyValuePairParser.TryParse(expression, out _)); - } - - [Theory] - [InlineData("123=true,inOffice=12345,name='abc,''efg'")] - [InlineData("123 = true , inOffice = 12345 , name= 'abc,''efg' ")] - [InlineData(" 123 = true , inOffice = 12345 , name= 'abc,''efg'")] - public void ParseComplexFunctionParametersWorks(string expression) - { - // Arrange & Act - IDictionary pairs = KeyValuePairParser.Parse(expression); - - Assert.NotNull(pairs); - Assert.Equal(3, pairs.Count); - Assert.Equal("true", pairs["123"]); - Assert.Equal("12345", pairs["inOffice"]); - Assert.Equal("'abc,''efg'", pairs["name"]); - } - - [Theory] - [InlineData("123=@p1,myname=@p2")] - [InlineData(" 123=@p1,myname=@p2")] - [InlineData("123=@p1,myname=@p2 ")] - [InlineData(" 123= @p1, myname= @p2 ")] - public void ParseForParameterAliasWorks(string expression) - { - // Arrange & Act - IDictionary pairs = KeyValuePairParser.Parse(expression); - - Assert.NotNull(pairs); - Assert.Equal(2, pairs.Count); - Assert.Equal("@p1", pairs["123"]); - Assert.Equal("@p2", pairs["myname"]); - } - - [Theory] - [InlineData("'abc'")] - [InlineData("'abc' ")] - [InlineData(" 'abc'")] - [InlineData(" 'abc' ")] - public void TryParseWorksForSingleKeyString(string expression) - { - // Arrange & Act - IDictionary pairs1 = KeyValuePairParser.Parse(expression); - bool result = KeyValuePairParser.TryParse(expression, out IDictionary pairs2); - - // Assert - Assert.True(result); - Assert.Equal(pairs1, pairs2); - - KeyValuePair keyValue = Assert.Single(pairs1); - Assert.Equal(string.Empty, keyValue.Key); - Assert.Equal("'abc'", keyValue.Value); - } + // Arrange & Act + IDictionary pairs = KeyValuePairParser.Parse(expression); + + Assert.NotNull(pairs); + Assert.Equal(3, pairs.Count); + Assert.Equal("true", pairs["123"]); + Assert.Equal("12345", pairs["inOffice"]); + Assert.Equal("'abc,''efg'", pairs["name"]); + } + + [Theory] + [InlineData("123=@p1,myname=@p2")] + [InlineData(" 123=@p1,myname=@p2")] + [InlineData("123=@p1,myname=@p2 ")] + [InlineData(" 123= @p1, myname= @p2 ")] + public void ParseForParameterAliasWorks(string expression) + { + // Arrange & Act + IDictionary pairs = KeyValuePairParser.Parse(expression); + + Assert.NotNull(pairs); + Assert.Equal(2, pairs.Count); + Assert.Equal("@p1", pairs["123"]); + Assert.Equal("@p2", pairs["myname"]); + } + + [Theory] + [InlineData("'abc'")] + [InlineData("'abc' ")] + [InlineData(" 'abc'")] + [InlineData(" 'abc' ")] + public void TryParseWorksForSingleKeyString(string expression) + { + // Arrange & Act + IDictionary pairs1 = KeyValuePairParser.Parse(expression); + bool result = KeyValuePairParser.TryParse(expression, out IDictionary pairs2); + + // Assert + Assert.True(result); + Assert.Equal(pairs1, pairs2); + + KeyValuePair keyValue = Assert.Single(pairs1); + Assert.Equal(string.Empty, keyValue.Key); + Assert.Equal("'abc'", keyValue.Value); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Commons/PropertyHelperTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Commons/PropertyHelperTests.cs index b5751b989..c6dec9525 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Commons/PropertyHelperTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Commons/PropertyHelperTests.cs @@ -8,42 +8,41 @@ using Microsoft.AspNetCore.OData.Common; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Commons +namespace Microsoft.AspNetCore.OData.Tests.Commons; + +public class PropertyHelperTests { - public class PropertyHelperTests + [Fact] + public void GetProperties_Returns_PropertyHelpers() { - [Fact] - public void GetProperties_Returns_PropertyHelpers() + // Arrange + MyProps props = new MyProps { - // Arrange - MyProps props = new MyProps - { - IntProp = 42, - StringProp = "abc" - }; + IntProp = 42, + StringProp = "abc" + }; - // Act - PropertyHelper[] properties = PropertyHelper.GetProperties(props); + // Act + PropertyHelper[] properties = PropertyHelper.GetProperties(props); - // Assert - Assert.Equal(2, properties.Length); - Assert.Collection(properties, - e => - { - Assert.Equal("IntProp", e.Name); - Assert.Equal(42, e.GetValue(props)); - }, - e => - { - Assert.Equal("StringProp", e.Name); - Assert.Equal("abc", e.GetValue(props)); - }); - } + // Assert + Assert.Equal(2, properties.Length); + Assert.Collection(properties, + e => + { + Assert.Equal("IntProp", e.Name); + Assert.Equal(42, e.GetValue(props)); + }, + e => + { + Assert.Equal("StringProp", e.Name); + Assert.Equal("abc", e.GetValue(props)); + }); + } - private class MyProps - { - public int IntProp { get; set; } - public string StringProp { get; set; } - } + private class MyProps + { + public int IntProp { get; set; } + public string StringProp { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Commons/ReflectionAssert.cs b/test/Microsoft.AspNetCore.OData.Tests/Commons/ReflectionAssert.cs index 6e04e823a..4ea0ee6ef 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Commons/ReflectionAssert.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Commons/ReflectionAssert.cs @@ -10,218 +10,217 @@ using System.Reflection; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Commons +namespace Microsoft.AspNetCore.OData.Tests.Commons; + +public class ReflectionAssert { - public class ReflectionAssert + private static PropertyInfo GetPropertyInfo(Expression> property) { - private static PropertyInfo GetPropertyInfo(Expression> property) + if (property.Body is MemberExpression) { - if (property.Body is MemberExpression) - { - return (PropertyInfo)((MemberExpression)property.Body).Member; - } - else if (property.Body is UnaryExpression && property.Body.NodeType == ExpressionType.Convert) - { - return (PropertyInfo)((MemberExpression)((UnaryExpression)property.Body).Operand).Member; - } - else - { - throw new InvalidOperationException("Could not determine property from lambda expression."); - } + return (PropertyInfo)((MemberExpression)property.Body).Member; } - - private static void TestPropertyValue(TInstance instance, Func getFunc, Action setFunc, TValue valueToSet, TValue valueToCheck) + else if (property.Body is UnaryExpression && property.Body.NodeType == ExpressionType.Convert) { - setFunc(instance, valueToSet); - TValue newValue = getFunc(instance); - Assert.Equal(valueToCheck, newValue); + return (PropertyInfo)((MemberExpression)((UnaryExpression)property.Body).Operand).Member; } - - private static void TestPropertyValue(TInstance instance, Func getFunc, Action setFunc, TValue value) + else { - TestPropertyValue(instance, getFunc, setFunc, value, value); + throw new InvalidOperationException("Could not determine property from lambda expression."); } + } - public static void Property(T instance, Expression> propertyGetter, TResult expectedDefaultValue, bool allowNull = false, TResult roundTripTestValue = null) where TResult : class - { - PropertyInfo property = GetPropertyInfo(propertyGetter); - Func getFunc = (obj) => (TResult)property.GetValue(obj, index: null); - Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); + private static void TestPropertyValue(TInstance instance, Func getFunc, Action setFunc, TValue valueToSet, TValue valueToCheck) + { + setFunc(instance, valueToSet); + TValue newValue = getFunc(instance); + Assert.Equal(valueToCheck, newValue); + } - Assert.Equal(expectedDefaultValue, getFunc(instance)); + private static void TestPropertyValue(TInstance instance, Func getFunc, Action setFunc, TValue value) + { + TestPropertyValue(instance, getFunc, setFunc, value, value); + } - if (allowNull) - { - TestPropertyValue(instance, getFunc, setFunc, null); - } - else - { - ExceptionAssert.ThrowsArgumentNull(() => - { - setFunc(instance, null); - }, "value"); - } + public static void Property(T instance, Expression> propertyGetter, TResult expectedDefaultValue, bool allowNull = false, TResult roundTripTestValue = null) where TResult : class + { + PropertyInfo property = GetPropertyInfo(propertyGetter); + Func getFunc = (obj) => (TResult)property.GetValue(obj, index: null); + Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); + + Assert.Equal(expectedDefaultValue, getFunc(instance)); - if (roundTripTestValue != null) + if (allowNull) + { + TestPropertyValue(instance, getFunc, setFunc, null); + } + else + { + ExceptionAssert.ThrowsArgumentNull(() => { - TestPropertyValue(instance, getFunc, setFunc, roundTripTestValue); - } + setFunc(instance, null); + }, "value"); } - public static void IntegerProperty(T instance, Expression> propertyGetter, TResult expectedDefaultValue, - TResult? minLegalValue, TResult? illegalLowerValue, - TResult? maxLegalValue, TResult? illegalUpperValue, - TResult roundTripTestValue) where TResult : struct + if (roundTripTestValue != null) { - PropertyInfo property = GetPropertyInfo(propertyGetter); - Func getFunc = (obj) => (TResult)property.GetValue(obj, index: null); - Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); - - Assert.Equal(expectedDefaultValue, getFunc(instance)); + TestPropertyValue(instance, getFunc, setFunc, roundTripTestValue); + } + } - if (minLegalValue.HasValue) - { - TestPropertyValue(instance, getFunc, setFunc, minLegalValue.Value); - } + public static void IntegerProperty(T instance, Expression> propertyGetter, TResult expectedDefaultValue, + TResult? minLegalValue, TResult? illegalLowerValue, + TResult? maxLegalValue, TResult? illegalUpperValue, + TResult roundTripTestValue) where TResult : struct + { + PropertyInfo property = GetPropertyInfo(propertyGetter); + Func getFunc = (obj) => (TResult)property.GetValue(obj, index: null); + Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); - if (maxLegalValue.HasValue) - { - TestPropertyValue(instance, getFunc, setFunc, maxLegalValue.Value); - } + Assert.Equal(expectedDefaultValue, getFunc(instance)); - if (illegalLowerValue.HasValue) - { - ExceptionAssert.ThrowsArgumentGreaterThanOrEqualTo(() => { setFunc(instance, illegalLowerValue.Value); }, "value", minLegalValue.Value.ToString(), illegalLowerValue.Value); - } + if (minLegalValue.HasValue) + { + TestPropertyValue(instance, getFunc, setFunc, minLegalValue.Value); + } - if (illegalUpperValue.HasValue) - { - ExceptionAssert.ThrowsArgumentLessThanOrEqualTo(() => { setFunc(instance, illegalLowerValue.Value); }, "value", maxLegalValue.Value.ToString(), illegalUpperValue.Value); - } + if (maxLegalValue.HasValue) + { + TestPropertyValue(instance, getFunc, setFunc, maxLegalValue.Value); + } - TestPropertyValue(instance, getFunc, setFunc, roundTripTestValue); + if (illegalLowerValue.HasValue) + { + ExceptionAssert.ThrowsArgumentGreaterThanOrEqualTo(() => { setFunc(instance, illegalLowerValue.Value); }, "value", minLegalValue.Value.ToString(), illegalLowerValue.Value); } - public static void NullableIntegerProperty(T instance, Expression> propertyGetter, TResult? expectedDefaultValue, - TResult? minLegalValue, TResult? illegalLowerValue, - TResult? maxLegalValue, TResult? illegalUpperValue, - TResult roundTripTestValue) where TResult : struct + if (illegalUpperValue.HasValue) { - PropertyInfo property = GetPropertyInfo(propertyGetter); - Func getFunc = (obj) => (TResult?)property.GetValue(obj, index: null); - Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); + ExceptionAssert.ThrowsArgumentLessThanOrEqualTo(() => { setFunc(instance, illegalLowerValue.Value); }, "value", maxLegalValue.Value.ToString(), illegalUpperValue.Value); + } - Assert.Equal(expectedDefaultValue, getFunc(instance)); + TestPropertyValue(instance, getFunc, setFunc, roundTripTestValue); + } - TestPropertyValue(instance, getFunc, setFunc, null); + public static void NullableIntegerProperty(T instance, Expression> propertyGetter, TResult? expectedDefaultValue, + TResult? minLegalValue, TResult? illegalLowerValue, + TResult? maxLegalValue, TResult? illegalUpperValue, + TResult roundTripTestValue) where TResult : struct + { + PropertyInfo property = GetPropertyInfo(propertyGetter); + Func getFunc = (obj) => (TResult?)property.GetValue(obj, index: null); + Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); - if (minLegalValue.HasValue) - { - TestPropertyValue(instance, getFunc, setFunc, minLegalValue.Value); - } + Assert.Equal(expectedDefaultValue, getFunc(instance)); - if (maxLegalValue.HasValue) - { - TestPropertyValue(instance, getFunc, setFunc, maxLegalValue.Value); - } + TestPropertyValue(instance, getFunc, setFunc, null); - if (illegalLowerValue.HasValue) - { - ExceptionAssert.ThrowsArgumentGreaterThanOrEqualTo(() => { setFunc(instance, illegalLowerValue.Value); }, "value", minLegalValue.Value.ToString(), illegalLowerValue.Value); - } + if (minLegalValue.HasValue) + { + TestPropertyValue(instance, getFunc, setFunc, minLegalValue.Value); + } - if (illegalUpperValue.HasValue) - { - ExceptionAssert.ThrowsArgumentLessThanOrEqualTo(() => { setFunc(instance, illegalLowerValue.Value); }, "value", maxLegalValue.Value.ToString(), illegalUpperValue.Value); - } + if (maxLegalValue.HasValue) + { + TestPropertyValue(instance, getFunc, setFunc, maxLegalValue.Value); + } - TestPropertyValue(instance, getFunc, setFunc, roundTripTestValue); + if (illegalLowerValue.HasValue) + { + ExceptionAssert.ThrowsArgumentGreaterThanOrEqualTo(() => { setFunc(instance, illegalLowerValue.Value); }, "value", minLegalValue.Value.ToString(), illegalLowerValue.Value); } - public static void BooleanProperty(T instance, Expression> propertyGetter, bool expectedDefaultValue) + if (illegalUpperValue.HasValue) { - PropertyInfo property = GetPropertyInfo(propertyGetter); - Func getFunc = (obj) => (bool)property.GetValue(obj, index: null); - Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); + ExceptionAssert.ThrowsArgumentLessThanOrEqualTo(() => { setFunc(instance, illegalLowerValue.Value); }, "value", maxLegalValue.Value.ToString(), illegalUpperValue.Value); + } - Assert.Equal(expectedDefaultValue, getFunc(instance)); + TestPropertyValue(instance, getFunc, setFunc, roundTripTestValue); + } - TestPropertyValue(instance, getFunc, setFunc, !expectedDefaultValue); - } + public static void BooleanProperty(T instance, Expression> propertyGetter, bool expectedDefaultValue) + { + PropertyInfo property = GetPropertyInfo(propertyGetter); + Func getFunc = (obj) => (bool)property.GetValue(obj, index: null); + Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); - public static void EnumProperty(T instance, Expression> propertyGetter, TResult expectedDefaultValue, TResult illegalValue, TResult roundTripTestValue) where TResult : struct - { - PropertyInfo property = GetPropertyInfo(propertyGetter); - Func getFunc = (obj) => (TResult)property.GetValue(obj, index: null); - Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); + Assert.Equal(expectedDefaultValue, getFunc(instance)); - Assert.Equal(expectedDefaultValue, getFunc(instance)); + TestPropertyValue(instance, getFunc, setFunc, !expectedDefaultValue); + } - ExceptionAssert.ThrowsInvalidEnumArgument(() => { setFunc(instance, illegalValue); }, "value", Convert.ToInt32(illegalValue), typeof(TResult)); + public static void EnumProperty(T instance, Expression> propertyGetter, TResult expectedDefaultValue, TResult illegalValue, TResult roundTripTestValue) where TResult : struct + { + PropertyInfo property = GetPropertyInfo(propertyGetter); + Func getFunc = (obj) => (TResult)property.GetValue(obj, index: null); + Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); - TestPropertyValue(instance, getFunc, setFunc, roundTripTestValue); - } + Assert.Equal(expectedDefaultValue, getFunc(instance)); - public static void EnumPropertyWithoutIllegalValueCheck(T instance, Expression> propertyGetter, TResult expectedDefaultValue, TResult roundTripTestValue) where TResult : struct - { - PropertyInfo property = GetPropertyInfo(propertyGetter); - Func getFunc = (obj) => (TResult)property.GetValue(obj, index: null); - Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); + ExceptionAssert.ThrowsInvalidEnumArgument(() => { setFunc(instance, illegalValue); }, "value", Convert.ToInt32(illegalValue), typeof(TResult)); - Assert.Equal(expectedDefaultValue, getFunc(instance)); + TestPropertyValue(instance, getFunc, setFunc, roundTripTestValue); + } - TestPropertyValue(instance, getFunc, setFunc, roundTripTestValue); - } + public static void EnumPropertyWithoutIllegalValueCheck(T instance, Expression> propertyGetter, TResult expectedDefaultValue, TResult roundTripTestValue) where TResult : struct + { + PropertyInfo property = GetPropertyInfo(propertyGetter); + Func getFunc = (obj) => (TResult)property.GetValue(obj, index: null); + Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); - public static void StringProperty(T instance, Expression> propertyGetter, string expectedDefaultValue, - bool allowNullAndEmpty = true, bool treatNullAsEmpty = true) - { - PropertyInfo property = GetPropertyInfo(propertyGetter); - Func getFunc = (obj) => (string)property.GetValue(obj, index: null); - Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); + Assert.Equal(expectedDefaultValue, getFunc(instance)); - Assert.Equal(expectedDefaultValue, getFunc(instance)); + TestPropertyValue(instance, getFunc, setFunc, roundTripTestValue); + } - if (allowNullAndEmpty) - { - // Assert get/set works for null - TestPropertyValue(instance, getFunc, setFunc, null, treatNullAsEmpty ? String.Empty : null); + public static void StringProperty(T instance, Expression> propertyGetter, string expectedDefaultValue, + bool allowNullAndEmpty = true, bool treatNullAsEmpty = true) + { + PropertyInfo property = GetPropertyInfo(propertyGetter); + Func getFunc = (obj) => (string)property.GetValue(obj, index: null); + Action setFunc = (obj, value) => property.SetValue(obj, value, index: null); - // Assert get/set works for String.Empty - TestPropertyValue(instance, getFunc, setFunc, String.Empty, String.Empty); - } - else - { - ExceptionAssert.ThrowsArgumentNullOrEmpty( - delegate () + Assert.Equal(expectedDefaultValue, getFunc(instance)); + + if (allowNullAndEmpty) + { + // Assert get/set works for null + TestPropertyValue(instance, getFunc, setFunc, null, treatNullAsEmpty ? String.Empty : null); + + // Assert get/set works for String.Empty + TestPropertyValue(instance, getFunc, setFunc, String.Empty, String.Empty); + } + else + { + ExceptionAssert.ThrowsArgumentNullOrEmpty( + delegate () + { + try { - try - { - TestPropertyValue(instance, getFunc, setFunc, null); - } - catch (TargetInvocationException e) - { - throw e.InnerException; - } - }, - "value"); - ExceptionAssert.ThrowsArgumentNullOrEmpty( - delegate () + TestPropertyValue(instance, getFunc, setFunc, null); + } + catch (TargetInvocationException e) { - try - { - TestPropertyValue(instance, getFunc, setFunc, String.Empty); - } - catch (TargetInvocationException e) - { - throw e.InnerException; - } - }, - "value"); - } - - // Assert get/set works for arbitrary value - TestPropertyValue(instance, getFunc, setFunc, "TestValue"); + throw e.InnerException; + } + }, + "value"); + ExceptionAssert.ThrowsArgumentNullOrEmpty( + delegate () + { + try + { + TestPropertyValue(instance, getFunc, setFunc, String.Empty); + } + catch (TargetInvocationException e) + { + throw e.InnerException; + } + }, + "value"); } + + // Assert get/set works for arbitrary value + TestPropertyValue(instance, getFunc, setFunc, "TestValue"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Commons/ReplaceCultureAttribute.cs b/test/Microsoft.AspNetCore.OData.Tests/Commons/ReplaceCultureAttribute.cs index 1519f783c..028afbfec 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Commons/ReplaceCultureAttribute.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Commons/ReplaceCultureAttribute.cs @@ -10,51 +10,50 @@ using System.Reflection; using System.Threading; -namespace Microsoft.AspNetCore.OData.Tests.Commons +namespace Microsoft.AspNetCore.OData.Tests.Commons; + +/// +/// Replaces the current culture and UI culture for the test. +/// +[AttributeUsage(AttributeTargets.Method)] +public class ReplaceCultureAttribute : Xunit.Sdk.BeforeAfterTestAttribute { + private CultureInfo _originalCulture; + private CultureInfo _originalUICulture; + + public ReplaceCultureAttribute() + { + Culture = CultureReplacer.DefaultCultureName; + UICulture = CultureReplacer.DefaultUICultureName; + } + /// - /// Replaces the current culture and UI culture for the test. + /// Sets for the test. Defaults to en-GB. /// - [AttributeUsage(AttributeTargets.Method)] - public class ReplaceCultureAttribute : Xunit.Sdk.BeforeAfterTestAttribute + /// + /// en-GB is used here as the default because en-US is equivalent to the InvariantCulture. We + /// want to be able to find bugs where we're accidentally relying on the Invariant instead of the + /// user's culture. + /// + public string Culture { get; set; } + + /// + /// Sets for the test. Defaults to en-US. + /// + public string UICulture { get; set; } + + public override void Before(MethodInfo methodUnderTest) + { + _originalCulture = Thread.CurrentThread.CurrentCulture; + _originalUICulture = Thread.CurrentThread.CurrentUICulture; + + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(Culture); + Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(UICulture); + } + + public override void After(MethodInfo methodUnderTest) { - private CultureInfo _originalCulture; - private CultureInfo _originalUICulture; - - public ReplaceCultureAttribute() - { - Culture = CultureReplacer.DefaultCultureName; - UICulture = CultureReplacer.DefaultUICultureName; - } - - /// - /// Sets for the test. Defaults to en-GB. - /// - /// - /// en-GB is used here as the default because en-US is equivalent to the InvariantCulture. We - /// want to be able to find bugs where we're accidentally relying on the Invariant instead of the - /// user's culture. - /// - public string Culture { get; set; } - - /// - /// Sets for the test. Defaults to en-US. - /// - public string UICulture { get; set; } - - public override void Before(MethodInfo methodUnderTest) - { - _originalCulture = Thread.CurrentThread.CurrentCulture; - _originalUICulture = Thread.CurrentThread.CurrentUICulture; - - Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(Culture); - Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(UICulture); - } - - public override void After(MethodInfo methodUnderTest) - { - Thread.CurrentThread.CurrentCulture = _originalCulture; - Thread.CurrentThread.CurrentUICulture = _originalUICulture; - } + Thread.CurrentThread.CurrentCulture = _originalCulture; + Thread.CurrentThread.CurrentUICulture = _originalUICulture; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Commons/StringExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Commons/StringExtensionsTests.cs index 4e634f660..b749c6e64 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Commons/StringExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Commons/StringExtensionsTests.cs @@ -8,38 +8,37 @@ using Microsoft.AspNetCore.OData.Common; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Commons +namespace Microsoft.AspNetCore.OData.Tests.Commons; + +public class StringExtensionsTests { - public class StringExtensionsTests + [Theory] + [InlineData("PostTo", "Post")] + [InlineData("PUTTO", "Put")] + [InlineData("PATCHTO", "Patch")] + [InlineData("DELETETO", "Delete")] + [InlineData("Get", "Get")] + public void NormalizeHttpMethod_Returns_MethodExpected(string method, string expected) { - [Theory] - [InlineData("PostTo", "Post")] - [InlineData("PUTTO", "Put")] - [InlineData("PATCHTO", "Patch")] - [InlineData("DELETETO", "Delete")] - [InlineData("Get", "Get")] - public void NormalizeHttpMethod_Returns_MethodExpected(string method, string expected) - { - // Arrange - string actual = method.NormalizeHttpMethod(); + // Arrange + string actual = method.NormalizeHttpMethod(); - // Act & Assert - Assert.Equal(expected, actual); - } + // Act & Assert + Assert.Equal(expected, actual); + } - [Theory] - [InlineData("{any}", true)] - [InlineData(null, false)] - [InlineData("", false)] - [InlineData("{any", false)] - [InlineData("any}", false)] - public void IsValidTemplateLiteral_Returns_BooleanExpected(string literal, bool expected) - { - // Arrange - bool actual = literal.IsValidTemplateLiteral(); + [Theory] + [InlineData("{any}", true)] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("{any", false)] + [InlineData("any}", false)] + public void IsValidTemplateLiteral_Returns_BooleanExpected(string literal, bool expected) + { + // Arrange + bool actual = literal.IsValidTemplateLiteral(); - // Act & Assert - Assert.Equal(expected, actual); - } + // Act & Assert + Assert.Equal(expected, actual); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Commons/TestAssemblyResolver.cs b/test/Microsoft.AspNetCore.OData.Tests/Commons/TestAssemblyResolver.cs index 90b037e67..a3f56795c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Commons/TestAssemblyResolver.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Commons/TestAssemblyResolver.cs @@ -9,12 +9,11 @@ using System.Reflection; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Tests.Commons +namespace Microsoft.AspNetCore.OData.Tests.Commons; + +public class TestAssemblyResolver : IAssemblyResolver { - public class TestAssemblyResolver : IAssemblyResolver - { - public static TestAssemblyResolver Instance = new TestAssemblyResolver(); + public static TestAssemblyResolver Instance = new TestAssemblyResolver(); - public IEnumerable Assemblies => new[] { typeof(TestAssemblyResolver).Assembly }; - } + public IEnumerable Assemblies => new[] { typeof(TestAssemblyResolver).Assembly }; } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Commons/TypeHelperTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Commons/TypeHelperTest.cs index 245fbb193..ad6859416 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Commons/TypeHelperTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Commons/TypeHelperTest.cs @@ -18,255 +18,254 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Commons +namespace Microsoft.AspNetCore.OData.Tests.Commons; + +public class TypeHelperTest { - public class TypeHelperTest + [Theory] + [InlineData(typeof(AggregationWrapper), true)] + [InlineData(typeof(ComputeWrapper), true)] + [InlineData(typeof(EntitySetAggregationWrapper), true)] + [InlineData(typeof(FlatteningWrapper), true)] + [InlineData(typeof(GroupByWrapper), true)] + [InlineData(typeof(NoGroupByAggregationWrapper), true)] + [InlineData(typeof(NoGroupByWrapper), true)] + [InlineData(typeof(object), false)] + [InlineData(typeof(SelectExpandWrapper), false)] + [InlineData(null, false)] + public void IsDynamicTypeWrapper_with_NonCollections(Type type, bool expected) { - [Theory] - [InlineData(typeof(AggregationWrapper), true)] - [InlineData(typeof(ComputeWrapper), true)] - [InlineData(typeof(EntitySetAggregationWrapper), true)] - [InlineData(typeof(FlatteningWrapper), true)] - [InlineData(typeof(GroupByWrapper), true)] - [InlineData(typeof(NoGroupByAggregationWrapper), true)] - [InlineData(typeof(NoGroupByWrapper), true)] - [InlineData(typeof(object), false)] - [InlineData(typeof(SelectExpandWrapper), false)] - [InlineData(null, false)] - public void IsDynamicTypeWrapper_with_NonCollections(Type type, bool expected) - { - // Arrange & Act & Assert - Assert.Equal(expected, TypeHelper.IsDynamicTypeWrapper(type)); - } + // Arrange & Act & Assert + Assert.Equal(expected, TypeHelper.IsDynamicTypeWrapper(type)); + } - [Fact] - public void IsCollection_ThrowsArgumentNull_ClrType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => TypeHelper.IsCollection(null), "clrType"); - } + [Fact] + public void IsCollection_ThrowsArgumentNull_ClrType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => TypeHelper.IsCollection(null), "clrType"); + } - /// - /// Collection types to test. - /// - public static TheoryDataSet CollectionTypesData + /// + /// Collection types to test. + /// + public static TheoryDataSet CollectionTypesData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { typeof(ICollection), typeof(string) }, - { typeof(IList), typeof(string) }, - { typeof(List), typeof(int) }, - { typeof(CustomBoolCollection), typeof(bool) }, - { typeof(IEnumerable), typeof(int) }, - { typeof(int[]), typeof(int) }, - { typeof(CustomIntCollection), typeof(int) }, - }; - } - } - - [Theory] - [MemberData(nameof(CollectionTypesData))] - public void IsCollection_with_Collections(Type collectionType, Type elementType) - { - // Arrange & Act & Assert - Type type; - Assert.True(TypeHelper.IsCollection(collectionType, out type)); - Assert.Equal(elementType, type); - Assert.True(TypeHelper.IsCollection(collectionType)); - } - - [Theory] - [InlineData(typeof(IDictionary), true)] - [InlineData(typeof(EdmUntypedObject), true)] - [InlineData(typeof(IDictionary), true)] - [InlineData(typeof(Dictionary), true)] - [InlineData(typeof(IList), false)] - public void IsDictionary_with_Dictionary(Type clrType, bool expected) - { - // Arrange & Act & Assert - Assert.Equal(expected, TypeHelper.IsDictionary(clrType)); + { typeof(ICollection), typeof(string) }, + { typeof(IList), typeof(string) }, + { typeof(List), typeof(int) }, + { typeof(CustomBoolCollection), typeof(bool) }, + { typeof(IEnumerable), typeof(int) }, + { typeof(int[]), typeof(int) }, + { typeof(CustomIntCollection), typeof(int) }, + }; } + } - [Theory] - [MemberData(nameof(CollectionTypesData))] - public void GetInnerElementType(Type collectionType, Type elementType) - { - // Arrange & Act & Assert - Assert.Equal(elementType, TypeHelper.GetInnerElementType(collectionType)); - } + [Theory] + [MemberData(nameof(CollectionTypesData))] + public void IsCollection_with_Collections(Type collectionType, Type elementType) + { + // Arrange & Act & Assert + Type type; + Assert.True(TypeHelper.IsCollection(collectionType, out type)); + Assert.Equal(elementType, type); + Assert.True(TypeHelper.IsCollection(collectionType)); + } - [Theory] - [InlineData(typeof(object))] - [InlineData(typeof(ICollection))] - [InlineData(typeof(IEnumerable))] - [InlineData(typeof(string))] - public void IsCollection_with_NonCollections(Type type) - { - // Arrange & Act & Assert - Assert.False(TypeHelper.IsCollection(type)); - } + [Theory] + [InlineData(typeof(IDictionary), true)] + [InlineData(typeof(EdmUntypedObject), true)] + [InlineData(typeof(IDictionary), true)] + [InlineData(typeof(Dictionary), true)] + [InlineData(typeof(IList), false)] + public void IsDictionary_with_Dictionary(Type clrType, bool expected) + { + // Arrange & Act & Assert + Assert.Equal(expected, TypeHelper.IsDictionary(clrType)); + } - [Theory] - [InlineData(typeof(int), typeof(int?))] - [InlineData(typeof(string), typeof(string))] - [InlineData(typeof(DateTime), typeof(DateTime?))] - [InlineData(typeof(int?), typeof(int?))] - [InlineData(typeof(IEnumerable), typeof(IEnumerable))] - [InlineData(typeof(int[]), typeof(int[]))] - [InlineData(typeof(string[]), typeof(string[]))] - public void ToNullable_Returns_ExpectedValue(Type type, Type expectedResult) - { - // Arrange & Act & Assert - Assert.Equal(expectedResult, TypeHelper.ToNullable(type)); - } + [Theory] + [MemberData(nameof(CollectionTypesData))] + public void GetInnerElementType(Type collectionType, Type elementType) + { + // Arrange & Act & Assert + Assert.Equal(elementType, TypeHelper.GetInnerElementType(collectionType)); + } - [Theory] - [InlineData(null, false)] - [InlineData(typeof(int), false)] - [InlineData(typeof(int?), true)] - [InlineData(typeof(bool), false)] - [InlineData(typeof(bool?), true)] - [InlineData(typeof(string), true)] - [InlineData(typeof(CustomBoolCollection), true)] - public void IsNullable(Type type, bool expected) - { - // Arrange & Act & Assert - Assert.Equal(expected, TypeHelper.IsNullable(type)); - } + [Theory] + [InlineData(typeof(object))] + [InlineData(typeof(ICollection))] + [InlineData(typeof(IEnumerable))] + [InlineData(typeof(string))] + public void IsCollection_with_NonCollections(Type type) + { + // Arrange & Act & Assert + Assert.False(TypeHelper.IsCollection(type)); + } - [Theory] - [InlineData(typeof(object), typeof(object), true)] - [InlineData(typeof(int), typeof(object), false)] - [InlineData(typeof(long), typeof(int), false)] - [InlineData(typeof(string), typeof(object), false)] - [InlineData(typeof(object), typeof(string), true)] - [InlineData(typeof(CustomBoolCollection), typeof(List), false)] - [InlineData(typeof(CustomBoolCollection), typeof(List), false)] - [InlineData(typeof(CustomAbstractClass), typeof(CustomConcreteClass), true)] - public void IsTypeAssignableFrom(Type type,Type fromType, bool expected) - { - // Arrange & Act & Assert - Assert.Equal(expected, TypeHelper.IsTypeAssignableFrom(type, fromType)); - } + [Theory] + [InlineData(typeof(int), typeof(int?))] + [InlineData(typeof(string), typeof(string))] + [InlineData(typeof(DateTime), typeof(DateTime?))] + [InlineData(typeof(int?), typeof(int?))] + [InlineData(typeof(IEnumerable), typeof(IEnumerable))] + [InlineData(typeof(int[]), typeof(int[]))] + [InlineData(typeof(string[]), typeof(string[]))] + public void ToNullable_Returns_ExpectedValue(Type type, Type expectedResult) + { + // Arrange & Act & Assert + Assert.Equal(expectedResult, TypeHelper.ToNullable(type)); + } - [Theory] - [InlineData(typeof(object), false)] - [InlineData(typeof(ICollection), false)] - [InlineData(typeof(MemberTypes), true)] - [InlineData(typeof(string), false)] - public void IsEnum(Type type, bool expected) - { - // Arrange & Act & Assert - Assert.Equal(expected, TypeHelper.IsEnum(type)); - } + [Theory] + [InlineData(null, false)] + [InlineData(typeof(int), false)] + [InlineData(typeof(int?), true)] + [InlineData(typeof(bool), false)] + [InlineData(typeof(bool?), true)] + [InlineData(typeof(string), true)] + [InlineData(typeof(CustomBoolCollection), true)] + public void IsNullable(Type type, bool expected) + { + // Arrange & Act & Assert + Assert.Equal(expected, TypeHelper.IsNullable(type)); + } - [Theory] - [InlineData(typeof(object), false)] - [InlineData(typeof(ICollection), false)] - [InlineData(typeof(DateTime), true)] - [InlineData(typeof(string), false)] - public void IsDateTime(Type type, bool expected) - { - // Arrange & Act & Assert - Assert.Equal(expected, TypeHelper.IsDateTime(type)); - } + [Theory] + [InlineData(typeof(object), typeof(object), true)] + [InlineData(typeof(int), typeof(object), false)] + [InlineData(typeof(long), typeof(int), false)] + [InlineData(typeof(string), typeof(object), false)] + [InlineData(typeof(object), typeof(string), true)] + [InlineData(typeof(CustomBoolCollection), typeof(List), false)] + [InlineData(typeof(CustomBoolCollection), typeof(List), false)] + [InlineData(typeof(CustomAbstractClass), typeof(CustomConcreteClass), true)] + public void IsTypeAssignableFrom(Type type,Type fromType, bool expected) + { + // Arrange & Act & Assert + Assert.Equal(expected, TypeHelper.IsTypeAssignableFrom(type, fromType)); + } - [Theory] - [InlineData(typeof(object), false)] - [InlineData(typeof(ICollection), false)] - [InlineData(typeof(TimeSpan), true)] - [InlineData(typeof(string), false)] - public void IsTimeSpan(Type type, bool expected) - { - // Arrange & Act & Assert - Assert.Equal(expected, TypeHelper.IsTimeSpan(type)); - } + [Theory] + [InlineData(typeof(object), false)] + [InlineData(typeof(ICollection), false)] + [InlineData(typeof(MemberTypes), true)] + [InlineData(typeof(string), false)] + public void IsEnum(Type type, bool expected) + { + // Arrange & Act & Assert + Assert.Equal(expected, TypeHelper.IsEnum(type)); + } - [Theory] - [InlineData(typeof(object), null)] - [InlineData(typeof(int), null)] - [InlineData(typeof(int[]), typeof(int))] - [InlineData(typeof(CustomBoolCollection), typeof(bool))] - [InlineData(typeof(IQueryable), typeof(int))] - [InlineData(typeof(IEnumerable), typeof(bool))] - [InlineData(typeof(string), typeof(char))] - [InlineData(typeof(Task), null)] - [InlineData(typeof(Task), typeof(char))] - [InlineData(typeof(Task>), typeof(bool))] - [InlineData(typeof(IEnumerable>), typeof(IEnumerable))] - public void GetImplementedIEnumerableType(Type collectionType, Type elementType) - { - // Arrange & Act & Assert - Assert.Equal(elementType, TypeHelper.GetImplementedIEnumerableType(collectionType)); - } + [Theory] + [InlineData(typeof(object), false)] + [InlineData(typeof(ICollection), false)] + [InlineData(typeof(DateTime), true)] + [InlineData(typeof(string), false)] + public void IsDateTime(Type type, bool expected) + { + // Arrange & Act & Assert + Assert.Equal(expected, TypeHelper.IsDateTime(type)); + } - [Fact] - public void GetLoadedTypes_ReturnsEmpty_IfNullAssemblyResolver() - { - // Arrange & Act & Assert - Assert.Empty(TypeHelper.GetLoadedTypes(null)); - } + [Theory] + [InlineData(typeof(object), false)] + [InlineData(typeof(ICollection), false)] + [InlineData(typeof(TimeSpan), true)] + [InlineData(typeof(string), false)] + public void IsTimeSpan(Type type, bool expected) + { + // Arrange & Act & Assert + Assert.Equal(expected, TypeHelper.IsTimeSpan(type)); + } - [Fact] - public void GetLoadedTypes_ReturnsAsExpected() - { - // Arrange - MockType baseType = new MockType("BaseType").Property(typeof(int), "ID"); + [Theory] + [InlineData(typeof(object), null)] + [InlineData(typeof(int), null)] + [InlineData(typeof(int[]), typeof(int))] + [InlineData(typeof(CustomBoolCollection), typeof(bool))] + [InlineData(typeof(IQueryable), typeof(int))] + [InlineData(typeof(IEnumerable), typeof(bool))] + [InlineData(typeof(string), typeof(char))] + [InlineData(typeof(Task), null)] + [InlineData(typeof(Task), typeof(char))] + [InlineData(typeof(Task>), typeof(bool))] + [InlineData(typeof(IEnumerable>), typeof(IEnumerable))] + public void GetImplementedIEnumerableType(Type collectionType, Type elementType) + { + // Arrange & Act & Assert + Assert.Equal(elementType, TypeHelper.GetImplementedIEnumerableType(collectionType)); + } - MockType derivedType = new MockType("DerivedType").Property(typeof(int), "DerivedTypeId").BaseType(baseType); + [Fact] + public void GetLoadedTypes_ReturnsEmpty_IfNullAssemblyResolver() + { + // Arrange & Act & Assert + Assert.Empty(TypeHelper.GetLoadedTypes(null)); + } - MockAssembly assembly = new MockAssembly(baseType, derivedType); - IAssemblyResolver resolver = MockAssembliesResolverFactory.Create(assembly); - IEnumerable foundTypes = TypeHelper.GetLoadedTypes(resolver); + [Fact] + public void GetLoadedTypes_ReturnsAsExpected() + { + // Arrange + MockType baseType = new MockType("BaseType").Property(typeof(int), "ID"); - IEnumerable definedNames = assembly.GetTypes().Select(t => t.FullName); - IEnumerable foundNames = foundTypes.Select(t => t.FullName); + MockType derivedType = new MockType("DerivedType").Property(typeof(int), "DerivedTypeId").BaseType(baseType); - foreach (string name in definedNames) - { - Assert.Contains(name, foundNames); - } + MockAssembly assembly = new MockAssembly(baseType, derivedType); + IAssemblyResolver resolver = MockAssembliesResolverFactory.Create(assembly); + IEnumerable foundTypes = TypeHelper.GetLoadedTypes(resolver); - Assert.DoesNotContain(typeof(TypeHelperTest), foundTypes); - } + IEnumerable definedNames = assembly.GetTypes().Select(t => t.FullName); + IEnumerable foundNames = foundTypes.Select(t => t.FullName); - /// - /// Custom internal class - /// - internal class CustomInternalClass + foreach (string name in definedNames) { + Assert.Contains(name, foundNames); } - /// - /// Custom collection of bool - /// - private sealed class CustomBoolCollection : List - { - } + Assert.DoesNotContain(typeof(TypeHelperTest), foundTypes); + } - /// - /// Custom collection of int - /// - private class CustomIntCollection : List - { - } + /// + /// Custom internal class + /// + internal class CustomInternalClass + { + } - /// - /// Custom abstract class - /// - private abstract class CustomAbstractClass - { - public abstract int Area(); - } + /// + /// Custom collection of bool + /// + private sealed class CustomBoolCollection : List + { + } - /// - /// Custom abstract class - /// - private class CustomConcreteClass : CustomAbstractClass - { - public override int Area() { return 42; } - } + /// + /// Custom collection of int + /// + private class CustomIntCollection : List + { + } + + /// + /// Custom abstract class + /// + private abstract class CustomAbstractClass + { + public abstract int Area(); + } + + /// + /// Custom abstract class + /// + private class CustomConcreteClass : CustomAbstractClass + { + public override int Area() { return 42; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaDeletedLinkOfTTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaDeletedLinkOfTTests.cs index 2a26a39ff..eb62564d5 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaDeletedLinkOfTTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaDeletedLinkOfTTests.cs @@ -10,47 +10,46 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Deltas +namespace Microsoft.AspNetCore.OData.Tests.Deltas; + +public class DeltaDeletedLinkOfTTests { - public class DeltaDeletedLinkOfTTests + [Fact] + public void CtorDeltaDeletedLinkOfT_ThrowsArgumentNull_StructuralType() { - [Fact] - public void CtorDeltaDeletedLinkOfT_ThrowsArgumentNull_StructuralType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new DeltaDeletedLink(null), "structuralType"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new DeltaDeletedLink(null), "structuralType"); + } - [Fact] - public void CtorDeltaDeletedLinkOfT_ThrowsInvalidOperation_NotAssignableFrom() - { - // Arrange & Act & Assert - ExceptionAssert.Throws( - () => new DeltaDeletedLink(typeof(A)), - "The actual entity type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaDeletedLinkOfTTests+A' is not assignable to the expected type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaDeletedLinkOfTTests+B'."); - } + [Fact] + public void CtorDeltaDeletedLinkOfT_ThrowsInvalidOperation_NotAssignableFrom() + { + // Arrange & Act & Assert + ExceptionAssert.Throws( + () => new DeltaDeletedLink(typeof(A)), + "The actual entity type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaDeletedLinkOfTTests+A' is not assignable to the expected type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaDeletedLinkOfTTests+B'."); + } - [Fact] - public void CtorDeltaDeletedLinkOfT_Sets_PropertyValue() + [Fact] + public void CtorDeltaDeletedLinkOfT_Sets_PropertyValue() + { + // Arrange & Act + DeltaDeletedLink deletedLink = new DeltaDeletedLink(typeof(B)) { - // Arrange & Act - DeltaDeletedLink deletedLink = new DeltaDeletedLink(typeof(B)) - { - Source = new Uri("http://source"), - Target = new Uri("http://target"), - Relationship = "any" - }; + Source = new Uri("http://source"), + Target = new Uri("http://target"), + Relationship = "any" + }; - // Assert - Assert.Equal("http://source", deletedLink.Source.OriginalString); - Assert.Equal("http://target", deletedLink.Target.OriginalString); - Assert.Equal("any", deletedLink.Relationship); - Assert.Equal(DeltaItemKind.DeltaDeletedLink, deletedLink.Kind); - Assert.Equal(typeof(B), deletedLink.StructuredType); - Assert.Equal(typeof(A), deletedLink.ExpectedClrType); - } - - private class A { } - private class B : A { } + // Assert + Assert.Equal("http://source", deletedLink.Source.OriginalString); + Assert.Equal("http://target", deletedLink.Target.OriginalString); + Assert.Equal("any", deletedLink.Relationship); + Assert.Equal(DeltaItemKind.DeltaDeletedLink, deletedLink.Kind); + Assert.Equal(typeof(B), deletedLink.StructuredType); + Assert.Equal(typeof(A), deletedLink.ExpectedClrType); } + + private class A { } + private class B : A { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaHelperTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaHelperTests.cs index 1d9bc96d6..a1ffaff26 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaHelperTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaHelperTests.cs @@ -11,29 +11,28 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Deltas +namespace Microsoft.AspNetCore.OData.Tests.Deltas; + +public class DeltaHelperTests { - public class DeltaHelperTests + [Fact] + public void IsDeltaOfT_Returns_BooleanAsExpected() { - [Fact] - public void IsDeltaOfT_Returns_BooleanAsExpected() - { - // Arrange & Act & Assert - Assert.False(DeltaHelper.IsDeltaOfT(null)); - Assert.False(DeltaHelper.IsDeltaOfT(typeof(int))); - Assert.False(DeltaHelper.IsDeltaOfT(typeof(DeltaSet<>))); - } + // Arrange & Act & Assert + Assert.False(DeltaHelper.IsDeltaOfT(null)); + Assert.False(DeltaHelper.IsDeltaOfT(typeof(int))); + Assert.False(DeltaHelper.IsDeltaOfT(typeof(DeltaSet<>))); + } - [Fact] - public void IsDeltaResourceSet_Returns_BooleanAsExpected() - { - // Arrange & Act & Assert - Assert.False(DeltaHelper.IsDeltaResourceSet(null)); - Assert.False(DeltaHelper.IsDeltaResourceSet(42)); - Assert.True(DeltaHelper.IsDeltaResourceSet(new DeltaSet())); + [Fact] + public void IsDeltaResourceSet_Returns_BooleanAsExpected() + { + // Arrange & Act & Assert + Assert.False(DeltaHelper.IsDeltaResourceSet(null)); + Assert.False(DeltaHelper.IsDeltaResourceSet(42)); + Assert.True(DeltaHelper.IsDeltaResourceSet(new DeltaSet())); - IEdmEntityType entityType = new Mock().Object; - Assert.True(DeltaHelper.IsDeltaResourceSet(new EdmChangedObjectCollection(entityType))); - } + IEdmEntityType entityType = new Mock().Object; + Assert.True(DeltaHelper.IsDeltaResourceSet(new EdmChangedObjectCollection(entityType))); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaLinkOfTTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaLinkOfTTests.cs index c68d1e9b9..a24083dc0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaLinkOfTTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaLinkOfTTests.cs @@ -10,47 +10,46 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Deltas +namespace Microsoft.AspNetCore.OData.Tests.Deltas; + +public class DeltaLinkOfTTests { - public class DeltaLinkOfTTests + [Fact] + public void CtorDeltaLinkOfT_ThrowsArgumentNull_StructuralType() { - [Fact] - public void CtorDeltaLinkOfT_ThrowsArgumentNull_StructuralType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new DeltaLink(null), "structuralType"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new DeltaLink(null), "structuralType"); + } - [Fact] - public void CtorDeltaLinkOfT_ThrowsInvalidOperation_NotAssignableFrom() - { - // Arrange & Act & Assert - ExceptionAssert.Throws( - () => new DeltaLink(typeof(A)), - "The actual entity type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaLinkOfTTests+A' is not assignable to the expected type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaLinkOfTTests+B'."); - } + [Fact] + public void CtorDeltaLinkOfT_ThrowsInvalidOperation_NotAssignableFrom() + { + // Arrange & Act & Assert + ExceptionAssert.Throws( + () => new DeltaLink(typeof(A)), + "The actual entity type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaLinkOfTTests+A' is not assignable to the expected type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaLinkOfTTests+B'."); + } - [Fact] - public void CtorDeltaLinkOfT_Sets_PropertyValue() + [Fact] + public void CtorDeltaLinkOfT_Sets_PropertyValue() + { + // Arrange & Act + DeltaLink link = new DeltaLink(typeof(B)) { - // Arrange & Act - DeltaLink link = new DeltaLink(typeof(B)) - { - Source = new Uri("http://source"), - Target = new Uri("http://target"), - Relationship = "any" - }; + Source = new Uri("http://source"), + Target = new Uri("http://target"), + Relationship = "any" + }; - // Assert - Assert.Equal("http://source", link.Source.OriginalString); - Assert.Equal("http://target", link.Target.OriginalString); - Assert.Equal("any", link.Relationship); - Assert.Equal(DeltaItemKind.DeltaLink, link.Kind); - Assert.Equal(typeof(B), link.StructuredType); - Assert.Equal(typeof(A), link.ExpectedClrType); - } - - private class A { } - private class B : A { } + // Assert + Assert.Equal("http://source", link.Source.OriginalString); + Assert.Equal("http://target", link.Target.OriginalString); + Assert.Equal("any", link.Relationship); + Assert.Equal(DeltaItemKind.DeltaLink, link.Kind); + Assert.Equal(typeof(B), link.StructuredType); + Assert.Equal(typeof(A), link.ExpectedClrType); } + + private class A { } + private class B : A { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaSetOfTTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaSetOfTTests.cs index 348e5274d..2364edeb9 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaSetOfTTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaSetOfTTests.cs @@ -9,38 +9,37 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Deltas +namespace Microsoft.AspNetCore.OData.Tests.Deltas; + +public class DeltaSetOfTTests { - public class DeltaSetOfTTests + [Fact] + public void DeltaSet_Returns_StructuredType() + { + // Arrange + DeltaSet set = new DeltaSet(); + + // Act & Assert + Assert.Equal(typeof(DeltaSetOfTTests), set.StructuredType); + } + + [Fact] + public void DeltaSet_Returns_ExpectedClrType() { - [Fact] - public void DeltaSet_Returns_StructuredType() - { - // Arrange - DeltaSet set = new DeltaSet(); - - // Act & Assert - Assert.Equal(typeof(DeltaSetOfTTests), set.StructuredType); - } - - [Fact] - public void DeltaSet_Returns_ExpectedClrType() - { - // Arrange - DeltaSet set = new DeltaSet(); - - // Act & Assert - Assert.Equal(typeof(DeltaSetOfTTests), set.ExpectedClrType); - } - - //[Fact] - //public void PatchOnDeltaSet_ThrowsArgumentNull_OriginalSet() - //{ - // // Arrange - // DeltaSet set = new DeltaSet(); - - // // Act & Assert - // ExceptionAssert.ThrowsArgumentNull(() => set.Patch(null), "originalSet"); - //} + // Arrange + DeltaSet set = new DeltaSet(); + + // Act & Assert + Assert.Equal(typeof(DeltaSetOfTTests), set.ExpectedClrType); } + + //[Fact] + //public void PatchOnDeltaSet_ThrowsArgumentNull_OriginalSet() + //{ + // // Arrange + // DeltaSet set = new DeltaSet(); + + // // Act & Assert + // ExceptionAssert.ThrowsArgumentNull(() => set.Patch(null), "originalSet"); + //} } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaTests.cs index b29f1f50c..61e24f414 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Deltas/DeltaTests.cs @@ -19,1126 +19,1125 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Deltas +namespace Microsoft.AspNetCore.OData.Tests.Deltas; + +public class DeltaTest { - public class DeltaTest + [Fact] + public void Ctor_ThrowsArgumentNull_StructuralType() { - [Fact] - public void Ctor_ThrowsArgumentNull_StructuralType() - { - ExceptionAssert.ThrowsArgumentNull(() => new Delta(structuralType: null), "structuralType"); - } + ExceptionAssert.ThrowsArgumentNull(() => new Delta(structuralType: null), "structuralType"); + } - [Fact] - public void Ctor_ThrowsInvalidOperation_If_EntityType_IsNotAssignable_To_TEntityType() - { - ExceptionAssert.Throws( - () => new Delta(typeof(AnotherDerived)), - "The actual entity type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+AnotherDerived' is not assignable to the expected type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+Derived'."); - } + [Fact] + public void Ctor_ThrowsInvalidOperation_If_EntityType_IsNotAssignable_To_TEntityType() + { + ExceptionAssert.Throws( + () => new Delta(typeof(AnotherDerived)), + "The actual entity type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+AnotherDerived' is not assignable to the expected type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+Derived'."); + } - [Fact] - public void Can_Declare_A_Delta_Of_An_AbstractClass() - { - Delta abstractDelta = null; - Assert.Null(abstractDelta); - } + [Fact] + public void Can_Declare_A_Delta_Of_An_AbstractClass() + { + Delta abstractDelta = null; + Assert.Null(abstractDelta); + } - public static IEnumerable DeltaModelPropertyNamesData + public static IEnumerable DeltaModelPropertyNamesData + { + get { - get + MethodInfo getDefaultValue = typeof(DeltaTest).GetMethod("GetDefaultValue"); + + IEnumerable defaultValues = typeof(DeltaModel).GetProperties().Select(p => new[] { p.Name, getDefaultValue.MakeGenericMethod(p.PropertyType).Invoke(obj: null, parameters: null) }); + return defaultValues.Concat(new object[][] { - MethodInfo getDefaultValue = typeof(DeltaTest).GetMethod("GetDefaultValue"); - - IEnumerable defaultValues = typeof(DeltaModel).GetProperties().Select(p => new[] { p.Name, getDefaultValue.MakeGenericMethod(p.PropertyType).Invoke(obj: null, parameters: null) }); - return defaultValues.Concat(new object[][] - { - new object[] { "StringProperty" , "42" }, - new object[] { "ComplexModelProperty", new ComplexModel { ComplexIntProperty = 42, ComplexNullableIntProperty = null } }, - new object[] { "CollectionProperty", new Collection { 1, 2, 3 }} - }); - } + new object[] { "StringProperty" , "42" }, + new object[] { "ComplexModelProperty", new ComplexModel { ComplexIntProperty = 42, ComplexNullableIntProperty = null } }, + new object[] { "CollectionProperty", new Collection { 1, 2, 3 }} + }); } + } - [Fact] - public void TryGetPropertyValue_ThrowsArgumentNull_original() - { - // Arrange & Act - Delta delta = new Delta(); - ExceptionAssert.ThrowsArgumentNull(() => delta.TryGetPropertyValue(null, out _), "name"); - } + [Fact] + public void TryGetPropertyValue_ThrowsArgumentNull_original() + { + // Arrange & Act + Delta delta = new Delta(); + ExceptionAssert.ThrowsArgumentNull(() => delta.TryGetPropertyValue(null, out _), "name"); + } - [Fact] - public void TryGetPropertyType_ThrowsArgumentNull_original() - { - // Arrange & Act - Delta delta = new Delta(); - ExceptionAssert.ThrowsArgumentNull(() => delta.TryGetPropertyType(null, out _), "name"); - } + [Fact] + public void TryGetPropertyType_ThrowsArgumentNull_original() + { + // Arrange & Act + Delta delta = new Delta(); + ExceptionAssert.ThrowsArgumentNull(() => delta.TryGetPropertyType(null, out _), "name"); + } - [Fact] - public void TryGetNestedPropertyValue_ThrowsArgumentNull_original() - { - // Arrange & Act - Delta delta = new Delta(); - ExceptionAssert.ThrowsArgumentNull(() => delta.TryGetNestedPropertyValue(null, out _), "name"); - } + [Fact] + public void TryGetNestedPropertyValue_ThrowsArgumentNull_original() + { + // Arrange & Act + Delta delta = new Delta(); + ExceptionAssert.ThrowsArgumentNull(() => delta.TryGetNestedPropertyValue(null, out _), "name"); + } - [Theory] - [MemberData(nameof(DeltaModelPropertyNamesData))] - public void RoundTrip_Properties(string propertyName, object value) - { - Delta delta = new Delta(); + [Theory] + [MemberData(nameof(DeltaModelPropertyNamesData))] + public void RoundTrip_Properties(string propertyName, object value) + { + Delta delta = new Delta(); - Assert.True(delta.TryGetPropertyType(propertyName, out _)); + Assert.True(delta.TryGetPropertyType(propertyName, out _)); - Assert.True(delta.TrySetPropertyValue(propertyName, value)); + Assert.True(delta.TrySetPropertyValue(propertyName, value)); - object retrievedValue; - delta.TryGetPropertyValue(propertyName, out retrievedValue); - Assert.Equal(value, retrievedValue); - } + object retrievedValue; + delta.TryGetPropertyValue(propertyName, out retrievedValue); + Assert.Equal(value, retrievedValue); + } - [Fact] - public void RoundTrip_Properties_InDynamicContainer() - { - // Arrange - Type dynamicType = typeof(AddressWithDynamicContainer); - PropertyInfo dynamicDictionaryPropertyinfo = dynamicType.GetProperty("Dynamics"); - Delta delta = new Delta( - dynamicType, null, dynamicDictionaryPropertyinfo); - - // Act & Assert - string propertyName = "DynamicPropertyName"; - Assert.False(delta.TryGetPropertyType(propertyName, out _)); - - // Act & Assert - object value = 42; - Assert.True(delta.TrySetPropertyValue(propertyName, value)); - - // Act & Assert - object retrievedValue; - delta.TryGetPropertyValue(propertyName, out retrievedValue); - Assert.Equal(value, retrievedValue); - } + [Fact] + public void RoundTrip_Properties_InDynamicContainer() + { + // Arrange + Type dynamicType = typeof(AddressWithDynamicContainer); + PropertyInfo dynamicDictionaryPropertyinfo = dynamicType.GetProperty("Dynamics"); + Delta delta = new Delta( + dynamicType, null, dynamicDictionaryPropertyinfo); + + // Act & Assert + string propertyName = "DynamicPropertyName"; + Assert.False(delta.TryGetPropertyType(propertyName, out _)); + + // Act & Assert + object value = 42; + Assert.True(delta.TrySetPropertyValue(propertyName, value)); + + // Act & Assert + object retrievedValue; + delta.TryGetPropertyValue(propertyName, out retrievedValue); + Assert.Equal(value, retrievedValue); + } - [Fact] - public void TrySetPropertyValue_ThrowsArgumentNull_name() - { - // Arrange & Act - Delta delta = new Delta(); - ExceptionAssert.ThrowsArgumentNull(() => delta.TrySetPropertyValue(null, "Invalid"), "name"); - } + [Fact] + public void TrySetPropertyValue_ThrowsArgumentNull_name() + { + // Arrange & Act + Delta delta = new Delta(); + ExceptionAssert.ThrowsArgumentNull(() => delta.TrySetPropertyValue(null, "Invalid"), "name"); + } - [Fact] - public void TrySetPropertyValue_ThrowsInvalidOperation_IfDynamicContainerWithoutSetter() - { - // Arrange - Type dynamicType = typeof(AddressWithDynamicContainer); - PropertyInfo dynamicDictionaryPropertyinfo = dynamicType.GetProperty("NonSetDynamics"); - Delta delta = new Delta( - dynamicType, null, dynamicDictionaryPropertyinfo); - - // Act - Action test = () => delta.TrySetPropertyValue("AnyDynamicName", 42); - - // Assert - ExceptionAssert.Throws(test, - "The dynamic dictionary property 'NonSetDynamics' of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+AddressWithDynamicContainer' cannot be set. The dynamic property dictionary must have a setter."); - } + [Fact] + public void TrySetPropertyValue_ThrowsInvalidOperation_IfDynamicContainerWithoutSetter() + { + // Arrange + Type dynamicType = typeof(AddressWithDynamicContainer); + PropertyInfo dynamicDictionaryPropertyinfo = dynamicType.GetProperty("NonSetDynamics"); + Delta delta = new Delta( + dynamicType, null, dynamicDictionaryPropertyinfo); + + // Act + Action test = () => delta.TrySetPropertyValue("AnyDynamicName", 42); + + // Assert + ExceptionAssert.Throws(test, + "The dynamic dictionary property 'NonSetDynamics' of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+AddressWithDynamicContainer' cannot be set. The dynamic property dictionary must have a setter."); + } - [Fact] - public void CanGetChangedPropertyNames() - { - dynamic delta = new Delta(); - IDelta idelta = delta as IDelta; - // modify in the way we expect the formatter too. - idelta.TrySetPropertyValue("City", "Sammamish"); - Assert.Single(idelta.GetChangedPropertyNames()); - Assert.Equal("City", idelta.GetChangedPropertyNames().Single()); - Assert.Equal(4, idelta.GetUnchangedPropertyNames().Count()); - - // read the property back - object city; - Assert.True(idelta.TryGetPropertyValue("City", out city)); - Assert.Equal("Sammamish", city); - - // modify the way people will through custom code - delta.StreetAddress = "23213 NE 15th Ct"; - string[] mods = idelta.GetChangedPropertyNames().ToArray(); - Assert.Equal(2, mods.Length); - Assert.Contains("StreetAddress", mods); - Assert.Contains("City", mods); - Assert.Equal("23213 NE 15th Ct", delta.StreetAddress); - Assert.Equal(3, idelta.GetUnchangedPropertyNames().Count()); - } + [Fact] + public void CanGetChangedPropertyNames() + { + dynamic delta = new Delta(); + IDelta idelta = delta as IDelta; + // modify in the way we expect the formatter too. + idelta.TrySetPropertyValue("City", "Sammamish"); + Assert.Single(idelta.GetChangedPropertyNames()); + Assert.Equal("City", idelta.GetChangedPropertyNames().Single()); + Assert.Equal(4, idelta.GetUnchangedPropertyNames().Count()); + + // read the property back + object city; + Assert.True(idelta.TryGetPropertyValue("City", out city)); + Assert.Equal("Sammamish", city); + + // modify the way people will through custom code + delta.StreetAddress = "23213 NE 15th Ct"; + string[] mods = idelta.GetChangedPropertyNames().ToArray(); + Assert.Equal(2, mods.Length); + Assert.Contains("StreetAddress", mods); + Assert.Contains("City", mods); + Assert.Equal("23213 NE 15th Ct", delta.StreetAddress); + Assert.Equal(3, idelta.GetUnchangedPropertyNames().Count()); + } - [Fact] - public void CanGetChangedNestedPropertyNames() - { - dynamic deltaCustomer = new Delta(); - IDelta ideltaCustomer = deltaCustomer as IDelta; - - AddressEntity Address = new AddressEntity(); - Address.ID = 42; - Address.StreetAddress = "23213 NE 15th Ct"; - Address.City = "Sammamish"; - Address.State = "WA"; - Address.ZipCode = 98074; - - // modify in the way we expect the formatter too. - ideltaCustomer.TrySetPropertyValue("Address", Address); - Assert.Single(ideltaCustomer.GetChangedPropertyNames()); - Assert.Equal("Address", ideltaCustomer.GetChangedPropertyNames().Single()); - Assert.Equal(3, ideltaCustomer.GetUnchangedPropertyNames().Count()); - - // read the property back - Assert.True(ideltaCustomer.TryGetPropertyValue("Address", out object address)); - Assert.Equal(Address, address); - - // read the instance - CustomerEntity instance = deltaCustomer.GetInstance(); - Assert.Equal(Address, instance.Address); - } + [Fact] + public void CanGetChangedNestedPropertyNames() + { + dynamic deltaCustomer = new Delta(); + IDelta ideltaCustomer = deltaCustomer as IDelta; + + AddressEntity Address = new AddressEntity(); + Address.ID = 42; + Address.StreetAddress = "23213 NE 15th Ct"; + Address.City = "Sammamish"; + Address.State = "WA"; + Address.ZipCode = 98074; + + // modify in the way we expect the formatter too. + ideltaCustomer.TrySetPropertyValue("Address", Address); + Assert.Single(ideltaCustomer.GetChangedPropertyNames()); + Assert.Equal("Address", ideltaCustomer.GetChangedPropertyNames().Single()); + Assert.Equal(3, ideltaCustomer.GetUnchangedPropertyNames().Count()); + + // read the property back + Assert.True(ideltaCustomer.TryGetPropertyValue("Address", out object address)); + Assert.Equal(Address, address); + + // read the instance + CustomerEntity instance = deltaCustomer.GetInstance(); + Assert.Equal(Address, instance.Address); + } - [Fact] - public void CanGetChangedNestedDeltaPropertyNames() - { - dynamic deltaCustomer = new Delta(); - IDelta ideltaCustomer = deltaCustomer as IDelta; - - dynamic deltaAddress = new Delta(); - IDelta ideltaAddress = deltaAddress as IDelta; - - // modify - ideltaAddress.TrySetPropertyValue("City", "Sammamish"); - ideltaAddress.TrySetPropertyValue("StreetAddress", "23213 NE 15th Ct"); - Assert.Equal(3, ideltaAddress.GetUnchangedPropertyNames().Count()); - string[] mods = ideltaAddress.GetChangedPropertyNames().ToArray(); - Assert.Equal(2, mods.Length); - Assert.Contains("StreetAddress", mods); - Assert.Contains("City", mods); - Assert.Equal("23213 NE 15th Ct", deltaAddress.StreetAddress); - Assert.Equal("Sammamish", deltaAddress.City); - - // read the property back - Assert.True(ideltaAddress.TryGetPropertyValue("City", out var city)); - Assert.Equal("Sammamish", city); - Assert.True(ideltaAddress.TryGetPropertyValue("StreetAddress", out var streetAddress)); - Assert.Equal("23213 NE 15th Ct", streetAddress); - - // modify the nested property - ideltaCustomer.TrySetPropertyValue("Address", ideltaAddress); - Assert.Single(ideltaCustomer.GetChangedPropertyNames()); - Assert.Equal("Address", ideltaCustomer.GetChangedPropertyNames().Single()); - Assert.Equal(3, ideltaCustomer.GetUnchangedPropertyNames().Count()); - - // read the nested property back - Assert.True(ideltaCustomer.TryGetPropertyValue("Address", out dynamic nestedAddress)); - Assert.IsAssignableFrom>(nestedAddress); - Assert.Equal("Sammamish", nestedAddress.City); - Assert.Equal("23213 NE 15th Ct", nestedAddress.StreetAddress); - } + [Fact] + public void CanGetChangedNestedDeltaPropertyNames() + { + dynamic deltaCustomer = new Delta(); + IDelta ideltaCustomer = deltaCustomer as IDelta; + + dynamic deltaAddress = new Delta(); + IDelta ideltaAddress = deltaAddress as IDelta; + + // modify + ideltaAddress.TrySetPropertyValue("City", "Sammamish"); + ideltaAddress.TrySetPropertyValue("StreetAddress", "23213 NE 15th Ct"); + Assert.Equal(3, ideltaAddress.GetUnchangedPropertyNames().Count()); + string[] mods = ideltaAddress.GetChangedPropertyNames().ToArray(); + Assert.Equal(2, mods.Length); + Assert.Contains("StreetAddress", mods); + Assert.Contains("City", mods); + Assert.Equal("23213 NE 15th Ct", deltaAddress.StreetAddress); + Assert.Equal("Sammamish", deltaAddress.City); + + // read the property back + Assert.True(ideltaAddress.TryGetPropertyValue("City", out var city)); + Assert.Equal("Sammamish", city); + Assert.True(ideltaAddress.TryGetPropertyValue("StreetAddress", out var streetAddress)); + Assert.Equal("23213 NE 15th Ct", streetAddress); + + // modify the nested property + ideltaCustomer.TrySetPropertyValue("Address", ideltaAddress); + Assert.Single(ideltaCustomer.GetChangedPropertyNames()); + Assert.Equal("Address", ideltaCustomer.GetChangedPropertyNames().Single()); + Assert.Equal(3, ideltaCustomer.GetUnchangedPropertyNames().Count()); + + // read the nested property back + Assert.True(ideltaCustomer.TryGetPropertyValue("Address", out dynamic nestedAddress)); + Assert.IsAssignableFrom>(nestedAddress); + Assert.Equal("Sammamish", nestedAddress.City); + Assert.Equal("23213 NE 15th Ct", nestedAddress.StreetAddress); + } - [Fact] - public void CannotGetChangedNestedDeltaPropertyNames() - { - dynamic deltaCustomer = new Delta(); - IDelta ideltaCustomer = deltaCustomer as IDelta; - - AddressEntity address = new AddressEntity(); - - // modify - address.City = "Sammamish"; - address.StreetAddress = "23213 NE 15th Ct"; - - // modify the nested property - ideltaCustomer.TrySetPropertyValue("Address", address); - Assert.Single(ideltaCustomer.GetChangedPropertyNames()); - Assert.Equal("Address", ideltaCustomer.GetChangedPropertyNames().Single()); - Assert.Equal(3, ideltaCustomer.GetUnchangedPropertyNames().Count()); - - // read the not nested property back using legacy API - Assert.True(ideltaCustomer.TryGetPropertyValue("Address", out dynamic deltaAddressEntity)); - Assert.IsAssignableFrom(deltaAddressEntity); - AddressEntity addressEntity = deltaAddressEntity as AddressEntity; - Assert.Equal("23213 NE 15th Ct", addressEntity.StreetAddress); - Assert.Equal("Sammamish", addressEntity.City); - - // read the not nested property back using nested API - Assert.False(deltaCustomer.TryGetNestedPropertyValue("Address", out dynamic deltaNestedAddress)); - } + [Fact] + public void CannotGetChangedNestedDeltaPropertyNames() + { + dynamic deltaCustomer = new Delta(); + IDelta ideltaCustomer = deltaCustomer as IDelta; + + AddressEntity address = new AddressEntity(); + + // modify + address.City = "Sammamish"; + address.StreetAddress = "23213 NE 15th Ct"; + + // modify the nested property + ideltaCustomer.TrySetPropertyValue("Address", address); + Assert.Single(ideltaCustomer.GetChangedPropertyNames()); + Assert.Equal("Address", ideltaCustomer.GetChangedPropertyNames().Single()); + Assert.Equal(3, ideltaCustomer.GetUnchangedPropertyNames().Count()); + + // read the not nested property back using legacy API + Assert.True(ideltaCustomer.TryGetPropertyValue("Address", out dynamic deltaAddressEntity)); + Assert.IsAssignableFrom(deltaAddressEntity); + AddressEntity addressEntity = deltaAddressEntity as AddressEntity; + Assert.Equal("23213 NE 15th Ct", addressEntity.StreetAddress); + Assert.Equal("Sammamish", addressEntity.City); + + // read the not nested property back using nested API + Assert.False(deltaCustomer.TryGetNestedPropertyValue("Address", out dynamic deltaNestedAddress)); + } - [Fact] - public void CanGetChangedPropertyNamesButOnlyUpdatable() - { - dynamic delta = new Delta(); - IDelta idelta = delta as IDelta; - // modify in the way we expect the formatter too. - idelta.TrySetPropertyValue("City", "Sammamish"); - Assert.Single(idelta.GetChangedPropertyNames()); - Assert.Equal("City", idelta.GetChangedPropertyNames().Single()); - Assert.Equal(4, idelta.GetUnchangedPropertyNames().Count()); - - // read the property back - object city; - Assert.True(idelta.TryGetPropertyValue("City", out city)); - Assert.Equal("Sammamish", city); - - // limit updatable properties - delta.UpdatableProperties.Clear(); - delta.UpdatableProperties.Add("City"); - delta.UpdatableProperties.Add("StreetAddress"); - - // modify the way people will through custom code - delta.StreetAddress = "23213 NE 15th Ct"; - string[] mods = idelta.GetChangedPropertyNames().ToArray(); - Assert.Equal(2, mods.Length); - Assert.Contains("StreetAddress", mods); - Assert.Contains("City", mods); - Assert.Equal("23213 NE 15th Ct", delta.StreetAddress); - Assert.Empty(idelta.GetUnchangedPropertyNames()); - - // try to modify an un-updatable property - idelta.TrySetPropertyValue("State", "IA"); - mods = idelta.GetChangedPropertyNames().ToArray(); - Assert.Equal(2, mods.Length); - Assert.Contains("StreetAddress", mods); - Assert.Contains("City", mods); - Assert.Null(delta.State); - Assert.Empty(idelta.GetUnchangedPropertyNames()); - - // limit a property that has been updated - delta.UpdatableProperties.Remove("StreetAddress"); - mods = idelta.GetChangedPropertyNames().ToArray(); - Assert.Single(mods); - Assert.Contains("City", mods); - Assert.Null(delta.State); - Assert.Empty(idelta.GetUnchangedPropertyNames()); - - // enable a property that has not been updated - delta.UpdatableProperties.Add("State"); - mods = idelta.GetChangedPropertyNames().ToArray(); - Assert.Single(mods); - Assert.Contains("City", mods); - Assert.Null(delta.State); - Assert.Single(idelta.GetUnchangedPropertyNames()); - Assert.Equal("State", idelta.GetUnchangedPropertyNames().Single()); - - // enable a property that doesn't exist - delta.UpdatableProperties.Add("Bogus"); - mods = idelta.GetChangedPropertyNames().ToArray(); - Assert.Single(mods); - Assert.Contains("City", mods); - Assert.Null(delta.State); - Assert.Single(idelta.GetUnchangedPropertyNames()); - Assert.Equal("State", idelta.GetUnchangedPropertyNames().Single()); - - // set a property that doesn't exist - Assert.False(delta.TrySetPropertyValue("Bogus", "Bad")); - mods = idelta.GetChangedPropertyNames().ToArray(); - Assert.Single(mods); - Assert.Contains("City", mods); - Assert.Null(delta.State); - Assert.Single(idelta.GetUnchangedPropertyNames()); - Assert.Equal("State", idelta.GetUnchangedPropertyNames().Single()); - } + [Fact] + public void CanGetChangedPropertyNamesButOnlyUpdatable() + { + dynamic delta = new Delta(); + IDelta idelta = delta as IDelta; + // modify in the way we expect the formatter too. + idelta.TrySetPropertyValue("City", "Sammamish"); + Assert.Single(idelta.GetChangedPropertyNames()); + Assert.Equal("City", idelta.GetChangedPropertyNames().Single()); + Assert.Equal(4, idelta.GetUnchangedPropertyNames().Count()); + + // read the property back + object city; + Assert.True(idelta.TryGetPropertyValue("City", out city)); + Assert.Equal("Sammamish", city); + + // limit updatable properties + delta.UpdatableProperties.Clear(); + delta.UpdatableProperties.Add("City"); + delta.UpdatableProperties.Add("StreetAddress"); + + // modify the way people will through custom code + delta.StreetAddress = "23213 NE 15th Ct"; + string[] mods = idelta.GetChangedPropertyNames().ToArray(); + Assert.Equal(2, mods.Length); + Assert.Contains("StreetAddress", mods); + Assert.Contains("City", mods); + Assert.Equal("23213 NE 15th Ct", delta.StreetAddress); + Assert.Empty(idelta.GetUnchangedPropertyNames()); + + // try to modify an un-updatable property + idelta.TrySetPropertyValue("State", "IA"); + mods = idelta.GetChangedPropertyNames().ToArray(); + Assert.Equal(2, mods.Length); + Assert.Contains("StreetAddress", mods); + Assert.Contains("City", mods); + Assert.Null(delta.State); + Assert.Empty(idelta.GetUnchangedPropertyNames()); + + // limit a property that has been updated + delta.UpdatableProperties.Remove("StreetAddress"); + mods = idelta.GetChangedPropertyNames().ToArray(); + Assert.Single(mods); + Assert.Contains("City", mods); + Assert.Null(delta.State); + Assert.Empty(idelta.GetUnchangedPropertyNames()); + + // enable a property that has not been updated + delta.UpdatableProperties.Add("State"); + mods = idelta.GetChangedPropertyNames().ToArray(); + Assert.Single(mods); + Assert.Contains("City", mods); + Assert.Null(delta.State); + Assert.Single(idelta.GetUnchangedPropertyNames()); + Assert.Equal("State", idelta.GetUnchangedPropertyNames().Single()); + + // enable a property that doesn't exist + delta.UpdatableProperties.Add("Bogus"); + mods = idelta.GetChangedPropertyNames().ToArray(); + Assert.Single(mods); + Assert.Contains("City", mods); + Assert.Null(delta.State); + Assert.Single(idelta.GetUnchangedPropertyNames()); + Assert.Equal("State", idelta.GetUnchangedPropertyNames().Single()); + + // set a property that doesn't exist + Assert.False(delta.TrySetPropertyValue("Bogus", "Bad")); + mods = idelta.GetChangedPropertyNames().ToArray(); + Assert.Single(mods); + Assert.Contains("City", mods); + Assert.Null(delta.State); + Assert.Single(idelta.GetUnchangedPropertyNames()); + Assert.Equal("State", idelta.GetUnchangedPropertyNames().Single()); + } - [Fact] - public void CanReadUnmodifiedDefaultValuesFromDelta() - { - dynamic patch = new Delta(); - Assert.Equal(0, patch.ID); - Assert.Null(patch.City); - Assert.Null(patch.State); - Assert.Null(patch.StreetAddress); - Assert.Equal(0, patch.ZipCode); - } + [Fact] + public void CanReadUnmodifiedDefaultValuesFromDelta() + { + dynamic patch = new Delta(); + Assert.Equal(0, patch.ID); + Assert.Null(patch.City); + Assert.Null(patch.State); + Assert.Null(patch.StreetAddress); + Assert.Equal(0, patch.ZipCode); + } - [Fact] - public void CannotSetNestedDeltaPropertyBadName() - { - dynamic deltaCustomer = new Delta(); - IDelta ideltaCustomer = deltaCustomer as IDelta; + [Fact] + public void CannotSetNestedDeltaPropertyBadName() + { + dynamic deltaCustomer = new Delta(); + IDelta ideltaCustomer = deltaCustomer as IDelta; - dynamic deltaAddress = new Delta(); - IDelta ideltaAddress = deltaAddress as IDelta; + dynamic deltaAddress = new Delta(); + IDelta ideltaAddress = deltaAddress as IDelta; - // Nested Delta with bad name - Assert.False(ideltaCustomer.TrySetPropertyValue("Bogus", ideltaAddress)); + // Nested Delta with bad name + Assert.False(ideltaCustomer.TrySetPropertyValue("Bogus", ideltaAddress)); - // Nested Delta with good name, but not updatable - deltaCustomer.UpdatableProperties.Clear(); - Assert.False(ideltaCustomer.TrySetPropertyValue("Address", ideltaAddress)); - } + // Nested Delta with good name, but not updatable + deltaCustomer.UpdatableProperties.Clear(); + Assert.False(ideltaCustomer.TrySetPropertyValue("Address", ideltaAddress)); + } - [Fact] - public void CannotSetNestedDeltaPropertyNameTwice() - { - dynamic deltaCustomer = new Delta(); - IDelta ideltaCustomer = deltaCustomer as IDelta; + [Fact] + public void CannotSetNestedDeltaPropertyNameTwice() + { + dynamic deltaCustomer = new Delta(); + IDelta ideltaCustomer = deltaCustomer as IDelta; - dynamic deltaAddress = new Delta(); - IDelta ideltaAddress = deltaAddress as IDelta; + dynamic deltaAddress = new Delta(); + IDelta ideltaAddress = deltaAddress as IDelta; - // modify the nested property - ideltaCustomer.TrySetPropertyValue("Address", ideltaAddress); - Assert.Single(ideltaCustomer.GetChangedPropertyNames()); - Assert.Equal("Address", ideltaCustomer.GetChangedPropertyNames().Single()); - Assert.Equal(3, ideltaCustomer.GetUnchangedPropertyNames().Count()); + // modify the nested property + ideltaCustomer.TrySetPropertyValue("Address", ideltaAddress); + Assert.Single(ideltaCustomer.GetChangedPropertyNames()); + Assert.Equal("Address", ideltaCustomer.GetChangedPropertyNames().Single()); + Assert.Equal(3, ideltaCustomer.GetUnchangedPropertyNames().Count()); - // modify again - Assert.False(ideltaCustomer.TrySetPropertyValue("Address", ideltaAddress)); - } + // modify again + Assert.False(ideltaCustomer.TrySetPropertyValue("Address", ideltaAddress)); + } - [Fact] - public void CanPatch() - { - AddressEntity original = new AddressEntity { ID = 1, City = "Redmond", State = "WA", StreetAddress = "21110 NE 44th St", ZipCode = 98074 }; - - dynamic delta = new Delta(); - delta.City = "Sammamish"; - delta.StreetAddress = "23213 NE 15th Ct"; - - delta.Patch(original); - // unchanged - Assert.Equal(1, original.ID); - Assert.Equal(98074, original.ZipCode); - Assert.Equal("WA", original.State); - // changed - Assert.Equal("Sammamish", original.City); - Assert.Equal("23213 NE 15th Ct", original.StreetAddress); - } + [Fact] + public void CanPatch() + { + AddressEntity original = new AddressEntity { ID = 1, City = "Redmond", State = "WA", StreetAddress = "21110 NE 44th St", ZipCode = 98074 }; + + dynamic delta = new Delta(); + delta.City = "Sammamish"; + delta.StreetAddress = "23213 NE 15th Ct"; + + delta.Patch(original); + // unchanged + Assert.Equal(1, original.ID); + Assert.Equal(98074, original.ZipCode); + Assert.Equal("WA", original.State); + // changed + Assert.Equal("Sammamish", original.City); + Assert.Equal("23213 NE 15th Ct", original.StreetAddress); + } - [Fact] - public void CanPatch_OpenType() + [Fact] + public void CanPatch_OpenType() + { + // Arrange + SimpleOpenAddress address = new SimpleOpenAddress { - // Arrange - SimpleOpenAddress address = new SimpleOpenAddress + City = "City", + Street = "Street", + Properties = new Dictionary { - City = "City", - Street = "Street", - Properties = new Dictionary - { - { "IntProp", 9 }, - { "ListProp", new List {1, 2, 3} } - } - }; + { "IntProp", 9 }, + { "ListProp", new List {1, 2, 3} } + } + }; - PropertyInfo propertyInfo = typeof(SimpleOpenAddress).GetProperty("Properties"); - Delta delta = new Delta(typeof(SimpleOpenAddress), null, propertyInfo); - delta.TrySetPropertyValue("City", "ChangedCity"); - delta.TrySetPropertyValue("IntProp", 1); + PropertyInfo propertyInfo = typeof(SimpleOpenAddress).GetProperty("Properties"); + Delta delta = new Delta(typeof(SimpleOpenAddress), null, propertyInfo); + delta.TrySetPropertyValue("City", "ChangedCity"); + delta.TrySetPropertyValue("IntProp", 1); - // Act - delta.Patch(address); + // Act + delta.Patch(address); - // Assert - // unchanged - Assert.Equal("Street", address.Street); - Assert.Equal(new List { 1, 2, 3 }, address.Properties["ListProp"]); + // Assert + // unchanged + Assert.Equal("Street", address.Street); + Assert.Equal(new List { 1, 2, 3 }, address.Properties["ListProp"]); - // changed - Assert.Equal("ChangedCity", address.City); - Assert.Equal(1, address.Properties["IntProp"]); - } + // changed + Assert.Equal("ChangedCity", address.City); + Assert.Equal(1, address.Properties["IntProp"]); + } - [Fact] - public void CanPatchNestedProperty() - { - AddressEntity originalAddress = new AddressEntity { ID = 1, City = "Redmond", State = "WA", StreetAddress = "21110 NE 44th St", ZipCode = 98074 }; - CustomerEntity originalCustomer = new CustomerEntity { ID = 7, FirstName = "Bob", LastName = "Smith", Address = originalAddress }; - - dynamic deltaCustomer = new Delta(); - - dynamic deltaAddress = new Delta(); - deltaAddress.City = "Sammamish"; - deltaAddress.StreetAddress = "23213 NE 15th Ct"; - - deltaCustomer.Address = deltaAddress; - - deltaCustomer.Patch(originalCustomer); - // unchanged - Assert.Equal(7, originalCustomer.ID); - Assert.Equal("Bob", originalCustomer.FirstName); - Assert.Equal("Smith", originalCustomer.LastName); - Assert.Equal(1, originalCustomer.Address.ID); - Assert.Equal(98074, originalCustomer.Address.ZipCode); - Assert.Equal("WA", originalCustomer.Address.State); - // changed - Assert.Equal("Sammamish", originalCustomer.Address.City); - Assert.Equal("23213 NE 15th Ct", originalCustomer.Address.StreetAddress); - } + [Fact] + public void CanPatchNestedProperty() + { + AddressEntity originalAddress = new AddressEntity { ID = 1, City = "Redmond", State = "WA", StreetAddress = "21110 NE 44th St", ZipCode = 98074 }; + CustomerEntity originalCustomer = new CustomerEntity { ID = 7, FirstName = "Bob", LastName = "Smith", Address = originalAddress }; + + dynamic deltaCustomer = new Delta(); + + dynamic deltaAddress = new Delta(); + deltaAddress.City = "Sammamish"; + deltaAddress.StreetAddress = "23213 NE 15th Ct"; + + deltaCustomer.Address = deltaAddress; + + deltaCustomer.Patch(originalCustomer); + // unchanged + Assert.Equal(7, originalCustomer.ID); + Assert.Equal("Bob", originalCustomer.FirstName); + Assert.Equal("Smith", originalCustomer.LastName); + Assert.Equal(1, originalCustomer.Address.ID); + Assert.Equal(98074, originalCustomer.Address.ZipCode); + Assert.Equal("WA", originalCustomer.Address.State); + // changed + Assert.Equal("Sammamish", originalCustomer.Address.City); + Assert.Equal("23213 NE 15th Ct", originalCustomer.Address.StreetAddress); + } - [Fact] - public void CanPatchNestedPropertyNullOriginal() - { - AddressEntity originalAddress = null; - CustomerEntity originalCustomer = new CustomerEntity { ID = 7, FirstName = "Bob", LastName = "Smith", Address = originalAddress }; - - dynamic deltaCustomer = new Delta(); - - dynamic deltaAddress = new Delta(); - deltaAddress.City = "Sammamish"; - deltaAddress.StreetAddress = "23213 NE 15th Ct"; - - deltaCustomer.Address = deltaAddress; - - deltaCustomer.Patch(originalCustomer); - // unchanged - Assert.Equal(7, originalCustomer.ID); - Assert.Equal("Bob", originalCustomer.FirstName); - Assert.Equal("Smith", originalCustomer.LastName); - Assert.Equal(0, originalCustomer.Address.ID); - Assert.Equal(0, originalCustomer.Address.ZipCode); - Assert.Null(originalCustomer.Address.State); - // changed - Assert.Equal("Sammamish", originalCustomer.Address.City); - Assert.Equal("23213 NE 15th Ct", originalCustomer.Address.StreetAddress); - } + [Fact] + public void CanPatchNestedPropertyNullOriginal() + { + AddressEntity originalAddress = null; + CustomerEntity originalCustomer = new CustomerEntity { ID = 7, FirstName = "Bob", LastName = "Smith", Address = originalAddress }; + + dynamic deltaCustomer = new Delta(); + + dynamic deltaAddress = new Delta(); + deltaAddress.City = "Sammamish"; + deltaAddress.StreetAddress = "23213 NE 15th Ct"; + + deltaCustomer.Address = deltaAddress; + + deltaCustomer.Patch(originalCustomer); + // unchanged + Assert.Equal(7, originalCustomer.ID); + Assert.Equal("Bob", originalCustomer.FirstName); + Assert.Equal("Smith", originalCustomer.LastName); + Assert.Equal(0, originalCustomer.Address.ID); + Assert.Equal(0, originalCustomer.Address.ZipCode); + Assert.Null(originalCustomer.Address.State); + // changed + Assert.Equal("Sammamish", originalCustomer.Address.City); + Assert.Equal("23213 NE 15th Ct", originalCustomer.Address.StreetAddress); + } - [Fact] - public void TestDelta_IgnoresUnmapped() - { - //Arrange - var delta = new Delta(); + [Fact] + public void TestDelta_IgnoresUnmapped() + { + //Arrange + var delta = new Delta(); - //Act - var properties = delta.GetUnchangedPropertyNames().ToList(); + //Act + var properties = delta.GetUnchangedPropertyNames().ToList(); - //Assert - Assert.Equal(3, properties.Count); - Assert.Equal("Id", properties.First()); - Assert.Equal("City", properties[1]); - Assert.Equal("State", properties[2]); - } + //Assert + Assert.Equal(3, properties.Count); + Assert.Equal("Id", properties.First()); + Assert.Equal("City", properties[1]); + Assert.Equal("State", properties[2]); + } - [Fact] - public void TestDelta_IgnoredMember() - { - //Arrange - var delta = new Delta(); + [Fact] + public void TestDelta_IgnoredMember() + { + //Arrange + var delta = new Delta(); - //Act - var properties = delta.GetUnchangedPropertyNames().ToList(); + //Act + var properties = delta.GetUnchangedPropertyNames().ToList(); - //Assert - Assert.Equal(2, properties.Count); - Assert.Equal("Name", properties[0]); - Assert.Equal("Street", properties[1]); - } + //Assert + Assert.Equal(2, properties.Count); + Assert.Equal("Name", properties[0]); + Assert.Equal("Street", properties[1]); + } - [Fact] - public void CanPut_OpenType() + [Fact] + public void CanPut_OpenType() + { + // Arrange + SimpleOpenAddress address = new SimpleOpenAddress { - // Arrange - SimpleOpenAddress address = new SimpleOpenAddress + City = "City", + Street = "Street", + Properties = new Dictionary { - City = "City", - Street = "Street", - Properties = new Dictionary - { - { "IntProp", 9 }, - { "ListProp", new List {1, 2, 3} } - } - }; + { "IntProp", 9 }, + { "ListProp", new List {1, 2, 3} } + } + }; - PropertyInfo propertyInfo = typeof(SimpleOpenAddress).GetProperty("Properties"); - Delta delta = new Delta(typeof(SimpleOpenAddress), null, propertyInfo); - delta.TrySetPropertyValue("City", "ChangedCity"); - delta.TrySetPropertyValue("IntProp", 1); + PropertyInfo propertyInfo = typeof(SimpleOpenAddress).GetProperty("Properties"); + Delta delta = new Delta(typeof(SimpleOpenAddress), null, propertyInfo); + delta.TrySetPropertyValue("City", "ChangedCity"); + delta.TrySetPropertyValue("IntProp", 1); - // Act - delta.Put(address); + // Act + delta.Put(address); - // Assert - Assert.Equal("ChangedCity", address.City); - Assert.Null(address.Street); - Assert.Equal(1, address.Properties["IntProp"]); - Assert.False(address.Properties.ContainsKey("ListProp")); - } + // Assert + Assert.Equal("ChangedCity", address.City); + Assert.Null(address.Street); + Assert.Equal(1, address.Properties["IntProp"]); + Assert.False(address.Properties.ContainsKey("ListProp")); + } - [Fact] - public void CopyUnchangedValues_ThrowsArgumentNull_original() - { - // Arrange & Act - Delta delta = new Delta(); - ExceptionAssert.ThrowsArgumentNull(() => delta.CopyUnchangedValues(null), "original"); - } + [Fact] + public void CopyUnchangedValues_ThrowsArgumentNull_original() + { + // Arrange & Act + Delta delta = new Delta(); + ExceptionAssert.ThrowsArgumentNull(() => delta.CopyUnchangedValues(null), "original"); + } - [Fact] - public void CopyChangedValues_ThrowsArgumentNull_original() - { - // Arrange & Act - Delta delta = new Delta(); - ExceptionAssert.ThrowsArgumentNull(() => delta.CopyChangedValues(null), "original"); - } + [Fact] + public void CopyChangedValues_ThrowsArgumentNull_original() + { + // Arrange & Act + Delta delta = new Delta(); + ExceptionAssert.ThrowsArgumentNull(() => delta.CopyChangedValues(null), "original"); + } - [Fact] - public void CanCopyUnchangedValues() - { - AddressEntity original = new AddressEntity { ID = 1, City = "Redmond", State = "WA", StreetAddress = "21110 NE 44th St", ZipCode = 98074 }; - - dynamic delta = new Delta(); - delta.City = "Sammamish"; - delta.StreetAddress = "23213 NE 15th Ct"; - Delta idelta = delta as Delta; - idelta.CopyUnchangedValues(original); - // unchanged values have been reset to defaults - Assert.Equal(0, original.ID); - Assert.Equal(0, original.ZipCode); - Assert.Null(original.State); - // changed values have been left unmodified - Assert.Equal("Redmond", original.City); - Assert.Equal("21110 NE 44th St", original.StreetAddress); - } + [Fact] + public void CanCopyUnchangedValues() + { + AddressEntity original = new AddressEntity { ID = 1, City = "Redmond", State = "WA", StreetAddress = "21110 NE 44th St", ZipCode = 98074 }; + + dynamic delta = new Delta(); + delta.City = "Sammamish"; + delta.StreetAddress = "23213 NE 15th Ct"; + Delta idelta = delta as Delta; + idelta.CopyUnchangedValues(original); + // unchanged values have been reset to defaults + Assert.Equal(0, original.ID); + Assert.Equal(0, original.ZipCode); + Assert.Null(original.State); + // changed values have been left unmodified + Assert.Equal("Redmond", original.City); + Assert.Equal("21110 NE 44th St", original.StreetAddress); + } - [Fact] - public void CanPut() - { - AddressEntity original = new AddressEntity { ID = 1, City = "Redmond", State = "WA", StreetAddress = "21110 NE 44th St", ZipCode = 98074 }; - - dynamic delta = new Delta(); - delta.City = "Sammamish"; - delta.StreetAddress = "23213 NE 15th Ct"; - Delta idelta = delta as Delta; - idelta.Put(original); - - // unchanged values have been reset to defaults - Assert.Equal(0, original.ID); - Assert.Equal(0, original.ZipCode); - Assert.Null(original.State); - // changed values have been updated to values in delta - Assert.Equal("Sammamish", original.City); - Assert.Equal("23213 NE 15th Ct", original.StreetAddress); - } + [Fact] + public void CanPut() + { + AddressEntity original = new AddressEntity { ID = 1, City = "Redmond", State = "WA", StreetAddress = "21110 NE 44th St", ZipCode = 98074 }; + + dynamic delta = new Delta(); + delta.City = "Sammamish"; + delta.StreetAddress = "23213 NE 15th Ct"; + Delta idelta = delta as Delta; + idelta.Put(original); + + // unchanged values have been reset to defaults + Assert.Equal(0, original.ID); + Assert.Equal(0, original.ZipCode); + Assert.Null(original.State); + // changed values have been updated to values in delta + Assert.Equal("Sammamish", original.City); + Assert.Equal("23213 NE 15th Ct", original.StreetAddress); + } - [Fact] - public void CanClear() - { - dynamic delta = new Delta(); - delta.StreetAddress = "Test"; - IDelta idelta = delta as IDelta; - Assert.Single(idelta.GetChangedPropertyNames()); - idelta.Clear(); - Assert.Empty(idelta.GetChangedPropertyNames()); - } + [Fact] + public void CanClear() + { + dynamic delta = new Delta(); + delta.StreetAddress = "Test"; + IDelta idelta = delta as IDelta; + Assert.Single(idelta.GetChangedPropertyNames()); + idelta.Clear(); + Assert.Empty(idelta.GetChangedPropertyNames()); + } - [Fact] - public void CanCreateDeltaOfDerivedTypes() - { - Delta delta = new Delta(typeof(Derived)); - Assert.IsType(delta.GetInstance()); - } + [Fact] + public void CanCreateDeltaOfDerivedTypes() + { + Delta delta = new Delta(typeof(Derived)); + Assert.IsType(delta.GetInstance()); + } - [Fact] - public void CanChangeDerivedClassProperties() - { - // Arrange - dynamic delta = new Delta(typeof(Derived)); + [Fact] + public void CanChangeDerivedClassProperties() + { + // Arrange + dynamic delta = new Delta(typeof(Derived)); - // Act - delta.DerivedInt = 10; + // Act + delta.DerivedInt = 10; - // Assert - Assert.Equal(delta.GetChangedPropertyNames(), new[] { "DerivedInt" }); - } + // Assert + Assert.Equal(delta.GetChangedPropertyNames(), new[] { "DerivedInt" }); + } - [Fact] - public void Patch_Patches_DerivedTypeProperties() - { - // Arrange - dynamic delta = new Delta(typeof(Derived)); - delta.DerivedInt = 42; - Derived derived = new Derived(); - - // Act - delta.Patch(derived); - - // Assert - Assert.Equal(42, derived.DerivedInt); - Assert.Equal(0, derived.BaseInt); - Assert.Null(derived.BaseString); - Assert.Null(derived.DerivedString); - } + [Fact] + public void Patch_Patches_DerivedTypeProperties() + { + // Arrange + dynamic delta = new Delta(typeof(Derived)); + delta.DerivedInt = 42; + Derived derived = new Derived(); + + // Act + delta.Patch(derived); + + // Assert + Assert.Equal(42, derived.DerivedInt); + Assert.Equal(0, derived.BaseInt); + Assert.Null(derived.BaseString); + Assert.Null(derived.DerivedString); + } - [Fact] - public void Put_Clears_DerivedTypeProperties() - { - // Arrange - dynamic delta = new Delta(typeof(Derived)); - delta.DerivedInt = 24; - Derived derived = new Derived { BaseInt = 42, DerivedInt = 0, BaseString = "42", DerivedString = "42" }; - - // Act - delta.Put(derived); - - // Assert - Assert.Equal(24, derived.DerivedInt); - Assert.Equal(0, derived.BaseInt); - Assert.Null(derived.BaseString); - Assert.Null(derived.DerivedString); - } + [Fact] + public void Put_Clears_DerivedTypeProperties() + { + // Arrange + dynamic delta = new Delta(typeof(Derived)); + delta.DerivedInt = 24; + Derived derived = new Derived { BaseInt = 42, DerivedInt = 0, BaseString = "42", DerivedString = "42" }; + + // Act + delta.Put(derived); + + // Assert + Assert.Equal(24, derived.DerivedInt); + Assert.Equal(0, derived.BaseInt); + Assert.Null(derived.BaseString); + Assert.Null(derived.DerivedString); + } - [Fact] - public void Put_DoesNotClear_PropertiesNotOnEntityType() - { - // Arrange - dynamic delta = new Delta(typeof(Derived)); - delta.DerivedInt = 24; - DerivedDerived derived = new DerivedDerived { BaseInt = 42, DerivedInt = 0, BaseString = "42", DerivedString = "42", DerivedDerivedInt = 42, DerivedDerivedString = "42" }; + [Fact] + public void Put_DoesNotClear_PropertiesNotOnEntityType() + { + // Arrange + dynamic delta = new Delta(typeof(Derived)); + delta.DerivedInt = 24; + DerivedDerived derived = new DerivedDerived { BaseInt = 42, DerivedInt = 0, BaseString = "42", DerivedString = "42", DerivedDerivedInt = 42, DerivedDerivedString = "42" }; - // Act - delta.Put(derived); + // Act + delta.Put(derived); - // Assert - Assert.Equal("42", derived.DerivedDerivedString); - Assert.Equal(42, derived.DerivedDerivedInt); - } + // Assert + Assert.Equal("42", derived.DerivedDerivedString); + Assert.Equal(42, derived.DerivedDerivedInt); + } - [Fact] - public void Put_DoesNotClear_NonUpdatableProperties() - { - // Arrange - string expectedString = "hello, world"; - int expectedInt = 24; - Delta delta = new Delta(typeof(Base), new[] { "BaseInt" }); - delta.TrySetPropertyValue("BaseInt", expectedInt); + [Fact] + public void Put_DoesNotClear_NonUpdatableProperties() + { + // Arrange + string expectedString = "hello, world"; + int expectedInt = 24; + Delta delta = new Delta(typeof(Base), new[] { "BaseInt" }); + delta.TrySetPropertyValue("BaseInt", expectedInt); - Base entity = new Base { BaseInt = 42, BaseString = expectedString }; + Base entity = new Base { BaseInt = 42, BaseString = expectedString }; - // Act - delta.Put(entity); + // Act + delta.Put(entity); - // Assert - Assert.Equal(expectedInt, entity.BaseInt); - Assert.Equal(expectedString, entity.BaseString); - } + // Assert + Assert.Equal(expectedInt, entity.BaseInt); + Assert.Equal(expectedString, entity.BaseString); + } - [Fact] - public void Put_DoesNotClear_ChangedNonUpdatableProperties() - { - // Arrange - string expectedString = "hello, world"; - int expectedInt = 24; - Delta delta = new Delta(typeof(Base)); - delta.TrySetPropertyValue("BaseInt", expectedInt); - delta.UpdatableProperties.Clear(); - delta.UpdatableProperties.Add("BaseInt"); - - Base entity = new Base { BaseInt = 42, BaseString = expectedString }; - - // Act - delta.Put(entity); - - // Assert - Assert.Equal(expectedInt, entity.BaseInt); - Assert.Equal(expectedString, entity.BaseString); - } + [Fact] + public void Put_DoesNotClear_ChangedNonUpdatableProperties() + { + // Arrange + string expectedString = "hello, world"; + int expectedInt = 24; + Delta delta = new Delta(typeof(Base)); + delta.TrySetPropertyValue("BaseInt", expectedInt); + delta.UpdatableProperties.Clear(); + delta.UpdatableProperties.Add("BaseInt"); + + Base entity = new Base { BaseInt = 42, BaseString = expectedString }; + + // Act + delta.Put(entity); + + // Assert + Assert.Equal(expectedInt, entity.BaseInt); + Assert.Equal(expectedString, entity.BaseString); + } - [Fact] - public void Patch_DoesNotSet_ChangedUpdatableProperties() - { - AddressEntity original = new AddressEntity { ID = 1, City = "Redmond", State = "WA", StreetAddress = "21110 NE 44th St", ZipCode = 98074 }; - - dynamic delta = new Delta(); - delta.City = "Sammamish"; - delta.StreetAddress = "23213 NE 15th Ct"; - - IDelta idelta = delta as IDelta; - string[] mods = idelta.GetChangedPropertyNames().ToArray(); - Assert.Equal(2, mods.Length); - Assert.Contains("StreetAddress", mods); - Assert.Contains("City", mods); - - delta.UpdatableProperties.Clear(); - delta.UpdatableProperties.Add("City"); - - delta.Patch(original); - // unchanged - Assert.Equal(1, original.ID); - Assert.Equal("WA", original.State); - Assert.Equal("21110 NE 44th St", original.StreetAddress); - Assert.Equal(98074, original.ZipCode); - // changed - Assert.Equal("Sammamish", original.City); - - Assert.Equal(delta.GetChangedPropertyNames(), new[] { "City" }); - } + [Fact] + public void Patch_DoesNotSet_ChangedUpdatableProperties() + { + AddressEntity original = new AddressEntity { ID = 1, City = "Redmond", State = "WA", StreetAddress = "21110 NE 44th St", ZipCode = 98074 }; + + dynamic delta = new Delta(); + delta.City = "Sammamish"; + delta.StreetAddress = "23213 NE 15th Ct"; + + IDelta idelta = delta as IDelta; + string[] mods = idelta.GetChangedPropertyNames().ToArray(); + Assert.Equal(2, mods.Length); + Assert.Contains("StreetAddress", mods); + Assert.Contains("City", mods); + + delta.UpdatableProperties.Clear(); + delta.UpdatableProperties.Add("City"); + + delta.Patch(original); + // unchanged + Assert.Equal(1, original.ID); + Assert.Equal("WA", original.State); + Assert.Equal("21110 NE 44th St", original.StreetAddress); + Assert.Equal(98074, original.ZipCode); + // changed + Assert.Equal("Sammamish", original.City); + + Assert.Equal(delta.GetChangedPropertyNames(), new[] { "City" }); + } - [Fact] - public void Patch_ClearsAndAddsTo_CollectionPropertiesWithNoSetter() - { - // Arrange - dynamic delta = new Delta(); - delta.CollectionPropertyWithoutSet = new[] { 1, 2, 3 }; - DeltaModelWithCollection model = new DeltaModelWithCollection { CollectionPropertyWithoutSet = { 42 } }; + [Fact] + public void Patch_ClearsAndAddsTo_CollectionPropertiesWithNoSetter() + { + // Arrange + dynamic delta = new Delta(); + delta.CollectionPropertyWithoutSet = new[] { 1, 2, 3 }; + DeltaModelWithCollection model = new DeltaModelWithCollection { CollectionPropertyWithoutSet = { 42 } }; - // Act - delta.Patch(model); + // Act + delta.Patch(model); - // Assert - Assert.Equal(new[] { 1, 2, 3 }, model.CollectionPropertyWithoutSet); - } + // Assert + Assert.Equal(new[] { 1, 2, 3 }, model.CollectionPropertyWithoutSet); + } - [Fact] - public void Delta_Fails_IfCollectionPropertyDoesNotHaveSetAndHasNullValue() - { - // Arrange - dynamic delta = new Delta(); - - // Act & Assert - ExceptionAssert.Throws( - () => delta.CollectionPropertyWithoutSetAndNullValue = new[] { "1" }, - "The property 'CollectionPropertyWithoutSetAndNullValue' on type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+InvalidD" + - "eltaModel' returned a null value. The input stream contains collection items which cannot be added if " + - "the instance is null."); - } + [Fact] + public void Delta_Fails_IfCollectionPropertyDoesNotHaveSetAndHasNullValue() + { + // Arrange + dynamic delta = new Delta(); + + // Act & Assert + ExceptionAssert.Throws( + () => delta.CollectionPropertyWithoutSetAndNullValue = new[] { "1" }, + "The property 'CollectionPropertyWithoutSetAndNullValue' on type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+InvalidD" + + "eltaModel' returned a null value. The input stream contains collection items which cannot be added if " + + "the instance is null."); + } - [Fact] - public void Delta_Fails_IfCollectionPropertyDoesNotHaveSetAndClear() - { - // Arrange - dynamic delta = new Delta(); - - // Act & Assert - ExceptionAssert.Throws( - () => delta.CollectionPropertyWithoutSetAndClear = new[] { "1" }, - "The type 'System.Int32[]' of the property 'CollectionPropertyWithoutSetAndClear' on type 'Microsoft." + - "AspNetCore.OData.Tests.Deltas.DeltaTest+InvalidDeltaModel' does not have a Clear method. Consider using a collection type" + - " that does have a Clear method, such as IList or ICollection."); - } + [Fact] + public void Delta_Fails_IfCollectionPropertyDoesNotHaveSetAndClear() + { + // Arrange + dynamic delta = new Delta(); + + // Act & Assert + ExceptionAssert.Throws( + () => delta.CollectionPropertyWithoutSetAndClear = new[] { "1" }, + "The type 'System.Int32[]' of the property 'CollectionPropertyWithoutSetAndClear' on type 'Microsoft." + + "AspNetCore.OData.Tests.Deltas.DeltaTest+InvalidDeltaModel' does not have a Clear method. Consider using a collection type" + + " that does have a Clear method, such as IList or ICollection."); + } - [Fact] - public void Patch_UnRelatedType_Throws_Argument() - { - // Arrange - Delta delta = new Delta(typeof(Derived)); - AnotherDerived unrelatedEntity = new AnotherDerived(); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => delta.Patch(unrelatedEntity), - "original", - "Cannot use Delta of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+Derived' on an entity of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+AnotherDerived'."); - } + [Fact] + public void Patch_UnRelatedType_Throws_Argument() + { + // Arrange + Delta delta = new Delta(typeof(Derived)); + AnotherDerived unrelatedEntity = new AnotherDerived(); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => delta.Patch(unrelatedEntity), + "original", + "Cannot use Delta of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+Derived' on an entity of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+AnotherDerived'."); + } - [Fact] - public void Put_UnRelatedType_Throws_Argument() - { - // Arrange - Delta delta = new Delta(typeof(Derived)); - AnotherDerived unrelatedEntity = new AnotherDerived(); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => delta.Put(unrelatedEntity), - "original", - "Cannot use Delta of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+Derived' on an entity of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+AnotherDerived'."); - } + [Fact] + public void Put_UnRelatedType_Throws_Argument() + { + // Arrange + Delta delta = new Delta(typeof(Derived)); + AnotherDerived unrelatedEntity = new AnotherDerived(); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => delta.Put(unrelatedEntity), + "original", + "Cannot use Delta of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+Derived' on an entity of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+AnotherDerived'."); + } - [Fact] - public void CopyChangedValues_UnRelatedType_Throws_Argument() - { - // Arrange - Delta delta = new Delta(typeof(Derived)); - AnotherDerived unrelatedEntity = new AnotherDerived(); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => delta.CopyChangedValues(unrelatedEntity), - "original", - "Cannot use Delta of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+Derived' on an entity of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+AnotherDerived'."); - } + [Fact] + public void CopyChangedValues_UnRelatedType_Throws_Argument() + { + // Arrange + Delta delta = new Delta(typeof(Derived)); + AnotherDerived unrelatedEntity = new AnotherDerived(); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => delta.CopyChangedValues(unrelatedEntity), + "original", + "Cannot use Delta of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+Derived' on an entity of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+AnotherDerived'."); + } - [Fact] - public void CopyUnchangedValues_UnRelatedType_Throws_Argument() - { - // Arrange - Delta delta = new Delta(typeof(Derived)); - AnotherDerived unrelatedEntity = new AnotherDerived(); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => delta.CopyUnchangedValues(unrelatedEntity), - "original", - "Cannot use Delta of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+Derived' on an entity of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+AnotherDerived'."); - } + [Fact] + public void CopyUnchangedValues_UnRelatedType_Throws_Argument() + { + // Arrange + Delta delta = new Delta(typeof(Derived)); + AnotherDerived unrelatedEntity = new AnotherDerived(); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => delta.CopyUnchangedValues(unrelatedEntity), + "original", + "Cannot use Delta of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+Derived' on an entity of type 'Microsoft.AspNetCore.OData.Tests.Deltas.DeltaTest+AnotherDerived'."); + } - public static TheoryDataSet ODataFormatter_Can_Read_Delta_DataSet - => new TheoryDataSet() - { - { "IntProperty", "23", 23 }, - { "LongProperty", String.Format(CultureInfo.InvariantCulture, "'{0}'", Int64.MaxValue), Int64.MaxValue }, // longs are serialized as strings in odata json - { "LongProperty", String.Format(CultureInfo.InvariantCulture, "'{0}'", Int64.MinValue), Int64.MinValue }, // longs are serialized as strings in odata json - { "NullableIntProperty", "null", null }, - { "BoolProperty", "true", true }, - { "NullableBoolProperty", "null", null }, - // TODO: Investigate how to add support for DataTime in webapi.odata, ODataLib v4 does not support it. - // { "DateTimeProperty", "'1992-01-01'", new DateTime(1992, 1, 1) }, - { "DateTimeOffsetProperty", "'1992-01-01'", new DateTimeOffset(new DateTime(1992, 1, 1)) }, - { "NullableDateTimeOffsetProperty", "'1992-01-01'", new DateTimeOffset(new DateTime(1992, 1, 1)) }, - { "NullableDateTimeOffsetProperty", "null", null }, - { "DateProperty", "'1997-07-01'", new Date(1997, 7, 1) }, - { "NullableDateProperty", "null", null }, - { "TimeOfDayProperty", "'10:11:12.0130000'", new TimeOfDay(10, 11, 12, 13) }, - { "NullableTimeOfDayProperty", "null", null }, - { "StringProperty", "'42'", "42" }, - { "ComplexModelProperty", "{ 'ComplexIntProperty' : 42 }", new ComplexModel { ComplexIntProperty = 42 } }, - { "CollectionProperty", "[ 1, 2, 3 ]", new Collection { 1, 2, 3} }, - { "ComplexModelCollectionProperty", "[ { 'ComplexIntProperty' : 42 } ]", new Collection { new ComplexModel { ComplexIntProperty = 42 } } } - }; - - public static TheoryDataSet TypedDelta_Returns_Correct_ExpectedClrType_And_ActualType_DataSet - => new TheoryDataSet() - { - { typeof(SimpleOpenCustomer) }, - { typeof(SimpleVipCustomer) } - }; - - [Theory] - [MemberData(nameof(TypedDelta_Returns_Correct_ExpectedClrType_And_ActualType_DataSet))] - public void TypedDelta_Returns_Correct_ExpectedClrType_And_ActualType(Type actualType) - { - // Arrange - ITypedDelta delta = new Delta(actualType); + public static TheoryDataSet ODataFormatter_Can_Read_Delta_DataSet + => new TheoryDataSet() + { + { "IntProperty", "23", 23 }, + { "LongProperty", String.Format(CultureInfo.InvariantCulture, "'{0}'", Int64.MaxValue), Int64.MaxValue }, // longs are serialized as strings in odata json + { "LongProperty", String.Format(CultureInfo.InvariantCulture, "'{0}'", Int64.MinValue), Int64.MinValue }, // longs are serialized as strings in odata json + { "NullableIntProperty", "null", null }, + { "BoolProperty", "true", true }, + { "NullableBoolProperty", "null", null }, + // TODO: Investigate how to add support for DataTime in webapi.odata, ODataLib v4 does not support it. + // { "DateTimeProperty", "'1992-01-01'", new DateTime(1992, 1, 1) }, + { "DateTimeOffsetProperty", "'1992-01-01'", new DateTimeOffset(new DateTime(1992, 1, 1)) }, + { "NullableDateTimeOffsetProperty", "'1992-01-01'", new DateTimeOffset(new DateTime(1992, 1, 1)) }, + { "NullableDateTimeOffsetProperty", "null", null }, + { "DateProperty", "'1997-07-01'", new Date(1997, 7, 1) }, + { "NullableDateProperty", "null", null }, + { "TimeOfDayProperty", "'10:11:12.0130000'", new TimeOfDay(10, 11, 12, 13) }, + { "NullableTimeOfDayProperty", "null", null }, + { "StringProperty", "'42'", "42" }, + { "ComplexModelProperty", "{ 'ComplexIntProperty' : 42 }", new ComplexModel { ComplexIntProperty = 42 } }, + { "CollectionProperty", "[ 1, 2, 3 ]", new Collection { 1, 2, 3} }, + { "ComplexModelCollectionProperty", "[ { 'ComplexIntProperty' : 42 } ]", new Collection { new ComplexModel { ComplexIntProperty = 42 } } } + }; - // Act - Type actualActualType = delta.StructuredType; - Type actualExpectedType = delta.ExpectedClrType; + public static TheoryDataSet TypedDelta_Returns_Correct_ExpectedClrType_And_ActualType_DataSet + => new TheoryDataSet() + { + { typeof(SimpleOpenCustomer) }, + { typeof(SimpleVipCustomer) } + }; - // Assert - Assert.Equal(typeof(SimpleOpenCustomer), actualExpectedType); - Assert.Equal(actualType, actualActualType); - } + [Theory] + [MemberData(nameof(TypedDelta_Returns_Correct_ExpectedClrType_And_ActualType_DataSet))] + public void TypedDelta_Returns_Correct_ExpectedClrType_And_ActualType(Type actualType) + { + // Arrange + ITypedDelta delta = new Delta(actualType); - public static T GetDefaultValue() - { - return default; - } + // Act + Type actualActualType = delta.StructuredType; + Type actualExpectedType = delta.ExpectedClrType; - private class DeltaModel - { - public int IntProperty { get; set; } + // Assert + Assert.Equal(typeof(SimpleOpenCustomer), actualExpectedType); + Assert.Equal(actualType, actualActualType); + } - public int? NullableIntProperty { get; set; } + public static T GetDefaultValue() + { + return default; + } - public long LongProperty { get; set; } + private class DeltaModel + { + public int IntProperty { get; set; } - public long? NullableLongProperty { get; set; } + public int? NullableIntProperty { get; set; } - public bool BoolProperty { get; set; } + public long LongProperty { get; set; } - public bool? NullableBoolProperty { get; set; } + public long? NullableLongProperty { get; set; } - public Guid GuidProperty { get; set; } + public bool BoolProperty { get; set; } - public Guid? NullableGuidProperty { get; set; } + public bool? NullableBoolProperty { get; set; } - public DateTimeOffset DateTimeOffsetProperty { get; set; } + public Guid GuidProperty { get; set; } - public DateTimeOffset? NullableDateTimeOffsetProperty { get; set; } + public Guid? NullableGuidProperty { get; set; } - public Date DateProperty { get; set; } + public DateTimeOffset DateTimeOffsetProperty { get; set; } - public Date? NullableDateProperty { get; set; } + public DateTimeOffset? NullableDateTimeOffsetProperty { get; set; } - public TimeOfDay TimeOfDayProperty { get; set; } + public Date DateProperty { get; set; } - public TimeOfDay? NullableTimeOfDayProperty { get; set; } + public Date? NullableDateProperty { get; set; } - public string StringProperty { get; set; } + public TimeOfDay TimeOfDayProperty { get; set; } - public ComplexModel ComplexModelProperty { get; set; } + public TimeOfDay? NullableTimeOfDayProperty { get; set; } - public Collection CollectionProperty { get; set; } + public string StringProperty { get; set; } - public Collection ComplexModelCollectionProperty { get; set; } - } + public ComplexModel ComplexModelProperty { get; set; } - private class DeltaModelWithCollection - { - public DeltaModelWithCollection() - { - CollectionPropertyWithoutSet = new Collection(); - ComplexModelCollectionPropertyWithOutSet = new Collection(); - } + public Collection CollectionProperty { get; set; } - public Collection CollectionPropertyWithoutSet { get; private set; } + public Collection ComplexModelCollectionProperty { get; set; } + } - public Collection ComplexModelCollectionPropertyWithOutSet { get; private set; } + private class DeltaModelWithCollection + { + public DeltaModelWithCollection() + { + CollectionPropertyWithoutSet = new Collection(); + ComplexModelCollectionPropertyWithOutSet = new Collection(); } - private class InvalidDeltaModel - { - public InvalidDeltaModel() - { - CollectionPropertyWithoutSetAndClear = new int[0]; - } + public Collection CollectionPropertyWithoutSet { get; private set; } - public IEnumerable CollectionPropertyWithoutSetAndNullValue { get; private set; } + public Collection ComplexModelCollectionPropertyWithOutSet { get; private set; } + } - public IEnumerable CollectionPropertyWithoutSetAndClear { get; private set; } + private class InvalidDeltaModel + { + public InvalidDeltaModel() + { + CollectionPropertyWithoutSetAndClear = new int[0]; } - private class ComplexModel - { - public int ComplexIntProperty { get; set; } + public IEnumerable CollectionPropertyWithoutSetAndNullValue { get; private set; } - public int? ComplexNullableIntProperty { get; set; } + public IEnumerable CollectionPropertyWithoutSetAndClear { get; private set; } + } - public override bool Equals(object obj) - { - ComplexModel model = obj as ComplexModel; + private class ComplexModel + { + public int ComplexIntProperty { get; set; } - if (model == null) - { - return false; - } + public int? ComplexNullableIntProperty { get; set; } - return ComplexIntProperty == model.ComplexIntProperty && ComplexNullableIntProperty == model.ComplexNullableIntProperty; - } + public override bool Equals(object obj) + { + ComplexModel model = obj as ComplexModel; - public override int GetHashCode() + if (model == null) { - throw new NotImplementedException(); + return false; } - } - private abstract class AbstractBase - { + return ComplexIntProperty == model.ComplexIntProperty && ComplexNullableIntProperty == model.ComplexNullableIntProperty; } - private class Base + public override int GetHashCode() { - public int BaseInt { get; set; } - - public string BaseString { get; set; } + throw new NotImplementedException(); } + } - private class Derived : Base - { - public int DerivedInt { get; set; } + private abstract class AbstractBase + { + } - public string DerivedString { get; set; } - } + private class Base + { + public int BaseInt { get; set; } - private class DerivedDerived : Derived - { - public int DerivedDerivedInt { get; set; } + public string BaseString { get; set; } + } - public string DerivedDerivedString { get; set; } - } + private class Derived : Base + { + public int DerivedInt { get; set; } - private class AnotherDerived : Base - { - } + public string DerivedString { get; set; } + } - [DataContract(Namespace = "com.contoso", Name = "DeltaModelAlias")] - private class DeltaModelWithAlias - { - [DataMember(Name = "StringPropertyAlias")] - public string StringProperty { get; set; } + private class DerivedDerived : Derived + { + public int DerivedDerivedInt { get; set; } - [DataMember(Name = "ComplexModelPropertyAlias")] - public ComplexModelWithAlias ComplexModelProperty { get; set; } + public string DerivedDerivedString { get; set; } + } - [DataMember(Name = "CollectionPropertyAlias")] - public Collection CollectionProperty { get; set; } + private class AnotherDerived : Base + { + } - [DataMember(Name = "ComplexModelCollectionPropertyAlias")] - public Collection ComplexModelCollectionProperty { get; set; } - } + [DataContract(Namespace = "com.contoso", Name = "DeltaModelAlias")] + private class DeltaModelWithAlias + { + [DataMember(Name = "StringPropertyAlias")] + public string StringProperty { get; set; } - [DataContract(Namespace = "com.contoso", Name = "ComplexModelAlias")] - private class ComplexModelWithAlias - { - [DataMember(Name = "ComplexIntPropertyAlias")] - public int ComplexIntProperty { get; set; } + [DataMember(Name = "ComplexModelPropertyAlias")] + public ComplexModelWithAlias ComplexModelProperty { get; set; } - [DataMember(Name = "ComplexNullableIntPropertyAlias")] - public int? ComplexNullableIntProperty { get; set; } + [DataMember(Name = "CollectionPropertyAlias")] + public Collection CollectionProperty { get; set; } - public override bool Equals(object obj) - { - ComplexModelWithAlias model = obj as ComplexModelWithAlias; + [DataMember(Name = "ComplexModelCollectionPropertyAlias")] + public Collection ComplexModelCollectionProperty { get; set; } + } - if (model == null) - { - return false; - } + [DataContract(Namespace = "com.contoso", Name = "ComplexModelAlias")] + private class ComplexModelWithAlias + { + [DataMember(Name = "ComplexIntPropertyAlias")] + public int ComplexIntProperty { get; set; } - return ComplexIntProperty == model.ComplexIntProperty && ComplexNullableIntProperty == model.ComplexNullableIntProperty; - } + [DataMember(Name = "ComplexNullableIntPropertyAlias")] + public int? ComplexNullableIntProperty { get; set; } + + public override bool Equals(object obj) + { + ComplexModelWithAlias model = obj as ComplexModelWithAlias; - public override int GetHashCode() + if (model == null) { - throw new NotImplementedException(); + return false; } + + return ComplexIntProperty == model.ComplexIntProperty && ComplexNullableIntProperty == model.ComplexNullableIntProperty; } - public class CustomerEntity + public override int GetHashCode() { - public int ID { get; set; } + throw new NotImplementedException(); + } + } - public string FirstName { get; set; } + public class CustomerEntity + { + public int ID { get; set; } - public string LastName { get; set; } + public string FirstName { get; set; } - public AddressEntity Address { get; set; } - } + public string LastName { get; set; } - public class AddressEntity : IEquatable - { - public int ID { get; set; } + public AddressEntity Address { get; set; } + } + + public class AddressEntity : IEquatable + { + public int ID { get; set; } - public string StreetAddress { get; set; } + public string StreetAddress { get; set; } - public string City { get; set; } + public string City { get; set; } - public string State { get; set; } + public string State { get; set; } - public int ZipCode { get; set; } + public int ZipCode { get; set; } - public override bool Equals(object obj) - { - return Equals(obj as AddressEntity); - } + public override bool Equals(object obj) + { + return Equals(obj as AddressEntity); + } - public bool Equals(AddressEntity other) + public bool Equals(AddressEntity other) + { + if (other is null) { - if (other is null) - { - return false; - } - - if (Object.ReferenceEquals(other, this)) - { - return true; - } - - return ID == other.ID && - StreetAddress == other.StreetAddress && - City == other.City && - State == other.State && - ZipCode == other.ZipCode; + return false; } - public override int GetHashCode() + if (Object.ReferenceEquals(other, this)) { - return HashCode.Combine(ID, StreetAddress, City, State, ZipCode); + return true; } + + return ID == other.ID && + StreetAddress == other.StreetAddress && + City == other.City && + State == other.State && + ZipCode == other.ZipCode; } - public class AddressWithDynamicContainer + public override int GetHashCode() { - public int ID { get; set; } + return HashCode.Combine(ID, StreetAddress, City, State, ZipCode); + } + } + + public class AddressWithDynamicContainer + { + public int ID { get; set; } - public string City { get; set; } + public string City { get; set; } - public IDictionary Dynamics { get; set; } + public IDictionary Dynamics { get; set; } - public IDictionary NonSetDynamics { get; } - } + public IDictionary NonSetDynamics { get; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/AutoSelectExpandHelperTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/AutoSelectExpandHelperTests.cs index c09b63892..17e5c96d7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/AutoSelectExpandHelperTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/AutoSelectExpandHelperTests.cs @@ -11,48 +11,47 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class AutoSelectExpandHelperTests { - public class AutoSelectExpandHelperTests + [Fact] + public void HasAutoSelectProperty_ThrowsArgumentNull_ForParameters() + { + IEdmModel edmModel = null; + ExceptionAssert.ThrowsArgumentNull(() => edmModel.HasAutoSelectProperty(null, null), "edmModel"); + + edmModel = EdmCoreModel.Instance; + ExceptionAssert.ThrowsArgumentNull(() => edmModel.HasAutoSelectProperty(null, null), "structuredType"); + } + + [Fact] + public void HasAutoExpandProperty_ThrowsArgumentNull_ForParameters() + { + IEdmModel edmModel = null; + ExceptionAssert.ThrowsArgumentNull(() => edmModel.HasAutoExpandProperty(null, null), "edmModel"); + + edmModel = EdmCoreModel.Instance; + ExceptionAssert.ThrowsArgumentNull(() => edmModel.HasAutoExpandProperty(null, null), "structuredType"); + } + + [Fact] + public void GetAutoSelectPaths_ThrowsArgumentNull_ForParameters() { - [Fact] - public void HasAutoSelectProperty_ThrowsArgumentNull_ForParameters() - { - IEdmModel edmModel = null; - ExceptionAssert.ThrowsArgumentNull(() => edmModel.HasAutoSelectProperty(null, null), "edmModel"); - - edmModel = EdmCoreModel.Instance; - ExceptionAssert.ThrowsArgumentNull(() => edmModel.HasAutoSelectProperty(null, null), "structuredType"); - } - - [Fact] - public void HasAutoExpandProperty_ThrowsArgumentNull_ForParameters() - { - IEdmModel edmModel = null; - ExceptionAssert.ThrowsArgumentNull(() => edmModel.HasAutoExpandProperty(null, null), "edmModel"); - - edmModel = EdmCoreModel.Instance; - ExceptionAssert.ThrowsArgumentNull(() => edmModel.HasAutoExpandProperty(null, null), "structuredType"); - } - - [Fact] - public void GetAutoSelectPaths_ThrowsArgumentNull_ForParameters() - { - IEdmModel edmModel = null; - ExceptionAssert.ThrowsArgumentNull(() => edmModel.GetAutoSelectPaths(null, null), "edmModel"); - - edmModel = EdmCoreModel.Instance; - ExceptionAssert.ThrowsArgumentNull(() => edmModel.GetAutoSelectPaths(null, null), "structuredType"); - } - - [Fact] - public void GetAutoExpandPaths_ThrowsArgumentNull_ForParameters() - { - IEdmModel edmModel = null; - ExceptionAssert.ThrowsArgumentNull(() => edmModel.GetAutoExpandPaths(null, null), "edmModel"); - - edmModel = EdmCoreModel.Instance; - ExceptionAssert.ThrowsArgumentNull(() => edmModel.GetAutoExpandPaths(null, null), "structuredType"); - } + IEdmModel edmModel = null; + ExceptionAssert.ThrowsArgumentNull(() => edmModel.GetAutoSelectPaths(null, null), "edmModel"); + + edmModel = EdmCoreModel.Instance; + ExceptionAssert.ThrowsArgumentNull(() => edmModel.GetAutoSelectPaths(null, null), "structuredType"); + } + + [Fact] + public void GetAutoExpandPaths_ThrowsArgumentNull_ForParameters() + { + IEdmModel edmModel = null; + ExceptionAssert.ThrowsArgumentNull(() => edmModel.GetAutoExpandPaths(null, null), "edmModel"); + + edmModel = EdmCoreModel.Instance; + ExceptionAssert.ThrowsArgumentNull(() => edmModel.GetAutoExpandPaths(null, null), "structuredType"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/CustomAggregateMethodAnnotationTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/CustomAggregateMethodAnnotationTests.cs index 97efbabe7..c87f019f1 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/CustomAggregateMethodAnnotationTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/CustomAggregateMethodAnnotationTests.cs @@ -12,30 +12,29 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class CustomAggregateMethodAnnotationTests { - public class CustomAggregateMethodAnnotationTests + [Fact] + public void CustomAggregateMethodAnnotation_Works_RoundTrip() { - [Fact] - public void CustomAggregateMethodAnnotation_Works_RoundTrip() - { - // Arrange - MethodInfo methodInfo = new Mock().Object; + // Arrange + MethodInfo methodInfo = new Mock().Object; - IDictionary methods = new Dictionary - { - { typeof(int), methodInfo } - }; + IDictionary methods = new Dictionary + { + { typeof(int), methodInfo } + }; - CustomAggregateMethodAnnotation annotation = new CustomAggregateMethodAnnotation(); + CustomAggregateMethodAnnotation annotation = new CustomAggregateMethodAnnotation(); - // Act & Assert - annotation.AddMethod("token", methods); + // Act & Assert + annotation.AddMethod("token", methods); - Assert.True(annotation.GetMethodInfo("token", typeof(int), out MethodInfo actual)); - Assert.Same(methodInfo, actual); + Assert.True(annotation.GetMethodInfo("token", typeof(int), out MethodInfo actual)); + Assert.Same(methodInfo, actual); - Assert.False(annotation.GetMethodInfo("unknown", typeof(int), out _)); - } + Assert.False(annotation.GetMethodInfo("unknown", typeof(int), out _)); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/DefaultODataTypeMapperTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/DefaultODataTypeMapperTests.cs index 9781563ca..48d67893d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/DefaultODataTypeMapperTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/DefaultODataTypeMapperTests.cs @@ -18,492 +18,491 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class DefaultODataTypeMapperTests { - public class DefaultODataTypeMapperTests + private static IEdmModel EdmModel = GetEdmModel(); + private DefaultODataTypeMapper _mapper = new DefaultODataTypeMapper(); + + #region PrimitiveType + [Theory] + [InlineData(typeof(string), "Edm.String", true)] + [InlineData(typeof(bool), "Edm.Boolean", false)] + [InlineData(typeof(bool?), "Edm.Boolean", true)] + [InlineData(typeof(byte), "Edm.Byte", false)] + [InlineData(typeof(byte?), "Edm.Byte", true)] + [InlineData(typeof(decimal), "Edm.Decimal", false)] + [InlineData(typeof(decimal?), "Edm.Decimal", true)] + [InlineData(typeof(double), "Edm.Double", false)] + [InlineData(typeof(double?), "Edm.Double", true)] + [InlineData(typeof(Guid), "Edm.Guid", false)] + [InlineData(typeof(Guid?), "Edm.Guid", true)] + [InlineData(typeof(short), "Edm.Int16", false)] + [InlineData(typeof(short?), "Edm.Int16", true)] + [InlineData(typeof(int), "Edm.Int32", false)] + [InlineData(typeof(int?), "Edm.Int32", true)] + [InlineData(typeof(long), "Edm.Int64", false)] + [InlineData(typeof(long?), "Edm.Int64", true)] + [InlineData(typeof(sbyte), "Edm.SByte", false)] + [InlineData(typeof(sbyte?), "Edm.SByte", true)] + [InlineData(typeof(float), "Edm.Single", false)] + [InlineData(typeof(float?), "Edm.Single", true)] + [InlineData(typeof(DateTimeOffset), "Edm.DateTimeOffset", false)] + [InlineData(typeof(DateTimeOffset?), "Edm.DateTimeOffset", true)] + [InlineData(typeof(TimeSpan), "Edm.Duration", false)] + [InlineData(typeof(TimeSpan?), "Edm.Duration", true)] + [InlineData(typeof(Date), "Edm.Date", false)] + [InlineData(typeof(Date?), "Edm.Date", true)] + [InlineData(typeof(TimeOfDay), "Edm.TimeOfDay", false)] + [InlineData(typeof(TimeOfDay?), "Edm.TimeOfDay", true)] + [InlineData(typeof(byte[]), "Edm.Binary", true)] + [InlineData(typeof(Stream), "Edm.Stream", true)] + public void GetEdmPrimitiveType_ForClrType_WorksAsExpected_ForStandardPrimitive(Type clrType, string name, bool nullable) { - private static IEdmModel EdmModel = GetEdmModel(); - private DefaultODataTypeMapper _mapper = new DefaultODataTypeMapper(); - - #region PrimitiveType - [Theory] - [InlineData(typeof(string), "Edm.String", true)] - [InlineData(typeof(bool), "Edm.Boolean", false)] - [InlineData(typeof(bool?), "Edm.Boolean", true)] - [InlineData(typeof(byte), "Edm.Byte", false)] - [InlineData(typeof(byte?), "Edm.Byte", true)] - [InlineData(typeof(decimal), "Edm.Decimal", false)] - [InlineData(typeof(decimal?), "Edm.Decimal", true)] - [InlineData(typeof(double), "Edm.Double", false)] - [InlineData(typeof(double?), "Edm.Double", true)] - [InlineData(typeof(Guid), "Edm.Guid", false)] - [InlineData(typeof(Guid?), "Edm.Guid", true)] - [InlineData(typeof(short), "Edm.Int16", false)] - [InlineData(typeof(short?), "Edm.Int16", true)] - [InlineData(typeof(int), "Edm.Int32", false)] - [InlineData(typeof(int?), "Edm.Int32", true)] - [InlineData(typeof(long), "Edm.Int64", false)] - [InlineData(typeof(long?), "Edm.Int64", true)] - [InlineData(typeof(sbyte), "Edm.SByte", false)] - [InlineData(typeof(sbyte?), "Edm.SByte", true)] - [InlineData(typeof(float), "Edm.Single", false)] - [InlineData(typeof(float?), "Edm.Single", true)] - [InlineData(typeof(DateTimeOffset), "Edm.DateTimeOffset", false)] - [InlineData(typeof(DateTimeOffset?), "Edm.DateTimeOffset", true)] - [InlineData(typeof(TimeSpan), "Edm.Duration", false)] - [InlineData(typeof(TimeSpan?), "Edm.Duration", true)] - [InlineData(typeof(Date), "Edm.Date", false)] - [InlineData(typeof(Date?), "Edm.Date", true)] - [InlineData(typeof(TimeOfDay), "Edm.TimeOfDay", false)] - [InlineData(typeof(TimeOfDay?), "Edm.TimeOfDay", true)] - [InlineData(typeof(byte[]), "Edm.Binary", true)] - [InlineData(typeof(Stream), "Edm.Stream", true)] - public void GetEdmPrimitiveType_ForClrType_WorksAsExpected_ForStandardPrimitive(Type clrType, string name, bool nullable) - { - // Arrange & Act - IEdmPrimitiveTypeReference primitiveTypeReference = _mapper.GetEdmPrimitiveType(clrType); + // Arrange & Act + IEdmPrimitiveTypeReference primitiveTypeReference = _mapper.GetEdmPrimitiveType(clrType); - // Assert - Assert.NotNull(primitiveTypeReference); - Assert.Equal(name, primitiveTypeReference.FullName()); - Assert.Equal(nullable, primitiveTypeReference.IsNullable); - } + // Assert + Assert.NotNull(primitiveTypeReference); + Assert.Equal(name, primitiveTypeReference.FullName()); + Assert.Equal(nullable, primitiveTypeReference.IsNullable); + } - [Theory] - [InlineData(typeof(XElement), "Edm.String", true)] - [InlineData(typeof(ushort), "Edm.Int32", false)] - [InlineData(typeof(ushort?), "Edm.Int32", true)] - [InlineData(typeof(uint), "Edm.Int64", false)] - [InlineData(typeof(uint?), "Edm.Int64", true)] - [InlineData(typeof(ulong), "Edm.Int64", false)] - [InlineData(typeof(ulong?), "Edm.Int64", true)] - [InlineData(typeof(char[]), "Edm.String", true)] - [InlineData(typeof(char), "Edm.String", false)] - [InlineData(typeof(char?), "Edm.String", true)] - [InlineData(typeof(DateTime), "Edm.DateTimeOffset", false)] - [InlineData(typeof(DateTime?), "Edm.DateTimeOffset", true)] - public void GetEdmPrimitiveType_ForClrType_WorksAsExpected_ForNonStandardPrimitive(Type clrType, string name, bool nullable) - { - // Arrange & Act - IEdmPrimitiveTypeReference primitiveTypeReference = _mapper.GetEdmPrimitiveType(clrType); + [Theory] + [InlineData(typeof(XElement), "Edm.String", true)] + [InlineData(typeof(ushort), "Edm.Int32", false)] + [InlineData(typeof(ushort?), "Edm.Int32", true)] + [InlineData(typeof(uint), "Edm.Int64", false)] + [InlineData(typeof(uint?), "Edm.Int64", true)] + [InlineData(typeof(ulong), "Edm.Int64", false)] + [InlineData(typeof(ulong?), "Edm.Int64", true)] + [InlineData(typeof(char[]), "Edm.String", true)] + [InlineData(typeof(char), "Edm.String", false)] + [InlineData(typeof(char?), "Edm.String", true)] + [InlineData(typeof(DateTime), "Edm.DateTimeOffset", false)] + [InlineData(typeof(DateTime?), "Edm.DateTimeOffset", true)] + public void GetEdmPrimitiveType_ForClrType_WorksAsExpected_ForNonStandardPrimitive(Type clrType, string name, bool nullable) + { + // Arrange & Act + IEdmPrimitiveTypeReference primitiveTypeReference = _mapper.GetEdmPrimitiveType(clrType); - // Assert - Assert.NotNull(primitiveTypeReference); - Assert.Equal(name, primitiveTypeReference.FullName()); - Assert.Equal(nullable, primitiveTypeReference.IsNullable); - } + // Assert + Assert.NotNull(primitiveTypeReference); + Assert.Equal(name, primitiveTypeReference.FullName()); + Assert.Equal(nullable, primitiveTypeReference.IsNullable); + } - [Theory] - [InlineData(typeof(Geography), "Edm.Geography")] - [InlineData(typeof(GeographyPoint), "Edm.GeographyPoint")] - [InlineData(typeof(GeographyLineString), "Edm.GeographyLineString")] - [InlineData(typeof(GeographyPolygon), "Edm.GeographyPolygon")] - [InlineData(typeof(GeographyCollection), "Edm.GeographyCollection")] - [InlineData(typeof(GeographyMultiLineString), "Edm.GeographyMultiLineString")] - [InlineData(typeof(GeographyMultiPoint), "Edm.GeographyMultiPoint")] - [InlineData(typeof(GeographyMultiPolygon), "Edm.GeographyMultiPolygon")] - [InlineData(typeof(Geometry), "Edm.Geometry")] - [InlineData(typeof(GeometryPoint), "Edm.GeometryPoint")] - [InlineData(typeof(GeometryLineString), "Edm.GeometryLineString")] - [InlineData(typeof(GeometryPolygon), "Edm.GeometryPolygon")] - [InlineData(typeof(GeometryCollection), "Edm.GeometryCollection")] - [InlineData(typeof(GeometryMultiLineString), "Edm.GeometryMultiLineString")] - [InlineData(typeof(GeometryMultiPoint), "Edm.GeometryMultiPoint")] - [InlineData(typeof(GeometryMultiPolygon), "Edm.GeometryMultiPolygon")] - public void GetEdmPrimitiveType_ForClrType_WorksAsExpected_ForSpatialPrimitive(Type clrType, string name) - { - // Arrange & Act - IEdmPrimitiveTypeReference primitiveTypeReference = _mapper.GetEdmPrimitiveType(clrType); + [Theory] + [InlineData(typeof(Geography), "Edm.Geography")] + [InlineData(typeof(GeographyPoint), "Edm.GeographyPoint")] + [InlineData(typeof(GeographyLineString), "Edm.GeographyLineString")] + [InlineData(typeof(GeographyPolygon), "Edm.GeographyPolygon")] + [InlineData(typeof(GeographyCollection), "Edm.GeographyCollection")] + [InlineData(typeof(GeographyMultiLineString), "Edm.GeographyMultiLineString")] + [InlineData(typeof(GeographyMultiPoint), "Edm.GeographyMultiPoint")] + [InlineData(typeof(GeographyMultiPolygon), "Edm.GeographyMultiPolygon")] + [InlineData(typeof(Geometry), "Edm.Geometry")] + [InlineData(typeof(GeometryPoint), "Edm.GeometryPoint")] + [InlineData(typeof(GeometryLineString), "Edm.GeometryLineString")] + [InlineData(typeof(GeometryPolygon), "Edm.GeometryPolygon")] + [InlineData(typeof(GeometryCollection), "Edm.GeometryCollection")] + [InlineData(typeof(GeometryMultiLineString), "Edm.GeometryMultiLineString")] + [InlineData(typeof(GeometryMultiPoint), "Edm.GeometryMultiPoint")] + [InlineData(typeof(GeometryMultiPolygon), "Edm.GeometryMultiPolygon")] + public void GetEdmPrimitiveType_ForClrType_WorksAsExpected_ForSpatialPrimitive(Type clrType, string name) + { + // Arrange & Act + IEdmPrimitiveTypeReference primitiveTypeReference = _mapper.GetEdmPrimitiveType(clrType); - // Assert - Assert.NotNull(primitiveTypeReference); - Assert.Equal(name, primitiveTypeReference.FullName()); - Assert.True(primitiveTypeReference.IsNullable); - } + // Assert + Assert.NotNull(primitiveTypeReference); + Assert.Equal(name, primitiveTypeReference.FullName()); + Assert.True(primitiveTypeReference.IsNullable); + } - [Theory] - [InlineData(EdmPrimitiveTypeKind.String, typeof(string), typeof(string))] - [InlineData(EdmPrimitiveTypeKind.Boolean, typeof(bool?), typeof(bool))] - [InlineData(EdmPrimitiveTypeKind.Byte, typeof(byte?), typeof(byte))] - [InlineData(EdmPrimitiveTypeKind.Decimal, typeof(decimal?), typeof(decimal))] - [InlineData(EdmPrimitiveTypeKind.Double, typeof(double?), typeof(double))] - [InlineData(EdmPrimitiveTypeKind.Guid, typeof(Guid?), typeof(Guid))] - [InlineData(EdmPrimitiveTypeKind.Int16, typeof(short?), typeof(short))] - [InlineData(EdmPrimitiveTypeKind.Int32, typeof(int?), typeof(int))] - [InlineData(EdmPrimitiveTypeKind.Int64, typeof(long?), typeof(long))] - [InlineData(EdmPrimitiveTypeKind.SByte, typeof(sbyte?), typeof(sbyte))] - [InlineData(EdmPrimitiveTypeKind.Single, typeof(float?), typeof(float))] - [InlineData(EdmPrimitiveTypeKind.DateTimeOffset, typeof(DateTimeOffset?), typeof(DateTimeOffset))] - [InlineData(EdmPrimitiveTypeKind.Duration, typeof(TimeSpan?), typeof(TimeSpan))] - [InlineData(EdmPrimitiveTypeKind.Date, typeof(Date?), typeof(Date))] - [InlineData(EdmPrimitiveTypeKind.TimeOfDay, typeof(TimeOfDay?), typeof(TimeOfDay))] - [InlineData(EdmPrimitiveTypeKind.Binary, typeof(byte[]), typeof(byte[]))] - [InlineData(EdmPrimitiveTypeKind.Stream, typeof(Stream), typeof(Stream))] - public void GetClrPrimitiveType_ForEdmType_WorksAsExpected_ForStandardPrimitive(EdmPrimitiveTypeKind kind, Type nullExpected, Type nonNullExpected) - { - // Arrange & Act & Assert - IEdmPrimitiveType primitiveType = EdmCoreModel.Instance.GetPrimitiveType(kind); - Type clrType = _mapper.GetClrPrimitiveType(primitiveType, true); - Assert.Equal(nullExpected, clrType); - - // Arrange & Act & Assert - clrType = _mapper.GetClrPrimitiveType(primitiveType, false); - Assert.Equal(nonNullExpected, clrType); - } + [Theory] + [InlineData(EdmPrimitiveTypeKind.String, typeof(string), typeof(string))] + [InlineData(EdmPrimitiveTypeKind.Boolean, typeof(bool?), typeof(bool))] + [InlineData(EdmPrimitiveTypeKind.Byte, typeof(byte?), typeof(byte))] + [InlineData(EdmPrimitiveTypeKind.Decimal, typeof(decimal?), typeof(decimal))] + [InlineData(EdmPrimitiveTypeKind.Double, typeof(double?), typeof(double))] + [InlineData(EdmPrimitiveTypeKind.Guid, typeof(Guid?), typeof(Guid))] + [InlineData(EdmPrimitiveTypeKind.Int16, typeof(short?), typeof(short))] + [InlineData(EdmPrimitiveTypeKind.Int32, typeof(int?), typeof(int))] + [InlineData(EdmPrimitiveTypeKind.Int64, typeof(long?), typeof(long))] + [InlineData(EdmPrimitiveTypeKind.SByte, typeof(sbyte?), typeof(sbyte))] + [InlineData(EdmPrimitiveTypeKind.Single, typeof(float?), typeof(float))] + [InlineData(EdmPrimitiveTypeKind.DateTimeOffset, typeof(DateTimeOffset?), typeof(DateTimeOffset))] + [InlineData(EdmPrimitiveTypeKind.Duration, typeof(TimeSpan?), typeof(TimeSpan))] + [InlineData(EdmPrimitiveTypeKind.Date, typeof(Date?), typeof(Date))] + [InlineData(EdmPrimitiveTypeKind.TimeOfDay, typeof(TimeOfDay?), typeof(TimeOfDay))] + [InlineData(EdmPrimitiveTypeKind.Binary, typeof(byte[]), typeof(byte[]))] + [InlineData(EdmPrimitiveTypeKind.Stream, typeof(Stream), typeof(Stream))] + public void GetClrPrimitiveType_ForEdmType_WorksAsExpected_ForStandardPrimitive(EdmPrimitiveTypeKind kind, Type nullExpected, Type nonNullExpected) + { + // Arrange & Act & Assert + IEdmPrimitiveType primitiveType = EdmCoreModel.Instance.GetPrimitiveType(kind); + Type clrType = _mapper.GetClrPrimitiveType(primitiveType, true); + Assert.Equal(nullExpected, clrType); + + // Arrange & Act & Assert + clrType = _mapper.GetClrPrimitiveType(primitiveType, false); + Assert.Equal(nonNullExpected, clrType); + } - [Theory] - [InlineData(EdmPrimitiveTypeKind.Geography, typeof(Geography))] - [InlineData(EdmPrimitiveTypeKind.GeographyPoint, typeof(GeographyPoint))] - [InlineData(EdmPrimitiveTypeKind.GeographyLineString, typeof(GeographyLineString))] - [InlineData(EdmPrimitiveTypeKind.GeographyPolygon, typeof(GeographyPolygon))] - [InlineData(EdmPrimitiveTypeKind.GeographyCollection, typeof(GeographyCollection))] - [InlineData(EdmPrimitiveTypeKind.GeographyMultiLineString, typeof(GeographyMultiLineString))] - [InlineData(EdmPrimitiveTypeKind.GeographyMultiPoint, typeof(GeographyMultiPoint))] - [InlineData(EdmPrimitiveTypeKind.GeographyMultiPolygon, typeof(GeographyMultiPolygon))] - [InlineData(EdmPrimitiveTypeKind.Geometry, typeof(Geometry))] - [InlineData(EdmPrimitiveTypeKind.GeometryPoint, typeof(GeometryPoint))] - [InlineData(EdmPrimitiveTypeKind.GeometryLineString, typeof(GeometryLineString))] - [InlineData(EdmPrimitiveTypeKind.GeometryPolygon, typeof(GeometryPolygon))] - [InlineData(EdmPrimitiveTypeKind.GeometryCollection, typeof(GeometryCollection))] - [InlineData(EdmPrimitiveTypeKind.GeometryMultiLineString, typeof(GeometryMultiLineString))] - [InlineData(EdmPrimitiveTypeKind.GeometryMultiPoint, typeof(GeometryMultiPoint))] - [InlineData(EdmPrimitiveTypeKind.GeometryMultiPolygon, typeof(GeometryMultiPolygon))] - public void GetClrPrimitiveType_ForEdmType_WorksAsExpected_ForSpatialPrimitive(EdmPrimitiveTypeKind kind, Type expected) - { - // Arrange & Act & Assert - IEdmPrimitiveType primitiveType = EdmCoreModel.Instance.GetPrimitiveType(kind); - Type clrType = _mapper.GetClrPrimitiveType(primitiveType, true); - Assert.Equal(expected, clrType); - - // Arrange & Act & Assert - clrType = _mapper.GetClrPrimitiveType(primitiveType, false); - Assert.Equal(expected, clrType); - } + [Theory] + [InlineData(EdmPrimitiveTypeKind.Geography, typeof(Geography))] + [InlineData(EdmPrimitiveTypeKind.GeographyPoint, typeof(GeographyPoint))] + [InlineData(EdmPrimitiveTypeKind.GeographyLineString, typeof(GeographyLineString))] + [InlineData(EdmPrimitiveTypeKind.GeographyPolygon, typeof(GeographyPolygon))] + [InlineData(EdmPrimitiveTypeKind.GeographyCollection, typeof(GeographyCollection))] + [InlineData(EdmPrimitiveTypeKind.GeographyMultiLineString, typeof(GeographyMultiLineString))] + [InlineData(EdmPrimitiveTypeKind.GeographyMultiPoint, typeof(GeographyMultiPoint))] + [InlineData(EdmPrimitiveTypeKind.GeographyMultiPolygon, typeof(GeographyMultiPolygon))] + [InlineData(EdmPrimitiveTypeKind.Geometry, typeof(Geometry))] + [InlineData(EdmPrimitiveTypeKind.GeometryPoint, typeof(GeometryPoint))] + [InlineData(EdmPrimitiveTypeKind.GeometryLineString, typeof(GeometryLineString))] + [InlineData(EdmPrimitiveTypeKind.GeometryPolygon, typeof(GeometryPolygon))] + [InlineData(EdmPrimitiveTypeKind.GeometryCollection, typeof(GeometryCollection))] + [InlineData(EdmPrimitiveTypeKind.GeometryMultiLineString, typeof(GeometryMultiLineString))] + [InlineData(EdmPrimitiveTypeKind.GeometryMultiPoint, typeof(GeometryMultiPoint))] + [InlineData(EdmPrimitiveTypeKind.GeometryMultiPolygon, typeof(GeometryMultiPolygon))] + public void GetClrPrimitiveType_ForEdmType_WorksAsExpected_ForSpatialPrimitive(EdmPrimitiveTypeKind kind, Type expected) + { + // Arrange & Act & Assert + IEdmPrimitiveType primitiveType = EdmCoreModel.Instance.GetPrimitiveType(kind); + Type clrType = _mapper.GetClrPrimitiveType(primitiveType, true); + Assert.Equal(expected, clrType); + + // Arrange & Act & Assert + clrType = _mapper.GetClrPrimitiveType(primitiveType, false); + Assert.Equal(expected, clrType); + } - [Theory] - [InlineData(EdmPrimitiveTypeKind.None)] - [InlineData(EdmPrimitiveTypeKind.PrimitiveType)] - public void GetPrimitiveType_ForEdmType_WorksAsExpected_ForNotUsedKind(EdmPrimitiveTypeKind kind) - { - // Arrange & Act & Assert - IEdmPrimitiveType primitiveType = EdmCoreModel.Instance.GetPrimitiveType(kind); - Type clrType = _mapper.GetClrPrimitiveType(primitiveType, true); - Assert.Null(clrType); - - // Arrange & Act & Assert - clrType = _mapper.GetClrPrimitiveType(primitiveType, false); - Assert.Null(clrType); - } - #endregion + [Theory] + [InlineData(EdmPrimitiveTypeKind.None)] + [InlineData(EdmPrimitiveTypeKind.PrimitiveType)] + public void GetPrimitiveType_ForEdmType_WorksAsExpected_ForNotUsedKind(EdmPrimitiveTypeKind kind) + { + // Arrange & Act & Assert + IEdmPrimitiveType primitiveType = EdmCoreModel.Instance.GetPrimitiveType(kind); + Type clrType = _mapper.GetClrPrimitiveType(primitiveType, true); + Assert.Null(clrType); + + // Arrange & Act & Assert + clrType = _mapper.GetClrPrimitiveType(primitiveType, false); + Assert.Null(clrType); + } + #endregion - #region GetClrType - [Fact] - public void GetClrType_ThrowsArgumentNull_ForInputParameters() - { - // Arrange & Act & Assert - Mock edmType = new Mock(); - edmType.Setup(x => x.TypeKind).Returns(EdmTypeKind.Entity); - ExceptionAssert.ThrowsArgumentNull(() => _mapper.GetClrType(null, edmType.Object, true, null), "edmModel"); - - IEdmModel model = new Mock().Object; - IAssemblyResolver resolver = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => _mapper.GetClrType(model, null, true, resolver), "edmType"); - } + #region GetClrType + [Fact] + public void GetClrType_ThrowsArgumentNull_ForInputParameters() + { + // Arrange & Act & Assert + Mock edmType = new Mock(); + edmType.Setup(x => x.TypeKind).Returns(EdmTypeKind.Entity); + ExceptionAssert.ThrowsArgumentNull(() => _mapper.GetClrType(null, edmType.Object, true, null), "edmModel"); + + IEdmModel model = new Mock().Object; + IAssemblyResolver resolver = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => _mapper.GetClrType(model, null, true, resolver), "edmType"); + } - [Fact] - public void FindClrType_ThrowsArgumentNull_ForInputParameters() - { - // Arrange & Act & Assert - Mock edmType = new Mock(); - edmType.Setup(x => x.TypeKind).Returns(EdmTypeKind.Entity); - ExceptionAssert.ThrowsArgumentNull(() => DefaultODataTypeMapper.FindClrType(null, edmType.Object, null), "edmModel"); + [Fact] + public void FindClrType_ThrowsArgumentNull_ForInputParameters() + { + // Arrange & Act & Assert + Mock edmType = new Mock(); + edmType.Setup(x => x.TypeKind).Returns(EdmTypeKind.Entity); + ExceptionAssert.ThrowsArgumentNull(() => DefaultODataTypeMapper.FindClrType(null, edmType.Object, null), "edmModel"); - IEdmModel model = new Mock().Object; - IAssemblyResolver resolver = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => DefaultODataTypeMapper.FindClrType(model, null, resolver), "edmType"); + IEdmModel model = new Mock().Object; + IAssemblyResolver resolver = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => DefaultODataTypeMapper.FindClrType(model, null, resolver), "edmType"); - ExceptionAssert.ThrowsArgumentNull(() => DefaultODataTypeMapper.FindClrType(model, edmType.Object, null), "assembliesResolver"); - } + ExceptionAssert.ThrowsArgumentNull(() => DefaultODataTypeMapper.FindClrType(model, edmType.Object, null), "assembliesResolver"); + } - [Theory] - [InlineData(EdmPrimitiveTypeKind.String, typeof(string))] - [InlineData(EdmPrimitiveTypeKind.Boolean, typeof(bool))] - [InlineData(EdmPrimitiveTypeKind.Byte, typeof(byte))] - [InlineData(EdmPrimitiveTypeKind.Decimal, typeof(decimal))] - [InlineData(EdmPrimitiveTypeKind.Double, typeof(double))] - [InlineData(EdmPrimitiveTypeKind.Guid, typeof(Guid))] - [InlineData(EdmPrimitiveTypeKind.Int16, typeof(short))] - [InlineData(EdmPrimitiveTypeKind.Int32, typeof(int))] - [InlineData(EdmPrimitiveTypeKind.Int64, typeof(long))] - [InlineData(EdmPrimitiveTypeKind.SByte, typeof(sbyte))] - [InlineData(EdmPrimitiveTypeKind.Single, typeof(float))] - [InlineData(EdmPrimitiveTypeKind.Binary, typeof(byte[]))] - [InlineData(EdmPrimitiveTypeKind.Stream, typeof(Stream))] - [InlineData(EdmPrimitiveTypeKind.DateTimeOffset, typeof(DateTimeOffset))] - [InlineData(EdmPrimitiveTypeKind.Duration, typeof(TimeSpan))] - [InlineData(EdmPrimitiveTypeKind.Date, typeof(Date))] - [InlineData(EdmPrimitiveTypeKind.TimeOfDay, typeof(TimeOfDay))] - public void GetClrType_WorksAsExpected_ForStandardPrimitive(EdmPrimitiveTypeKind kind, Type expected) + [Theory] + [InlineData(EdmPrimitiveTypeKind.String, typeof(string))] + [InlineData(EdmPrimitiveTypeKind.Boolean, typeof(bool))] + [InlineData(EdmPrimitiveTypeKind.Byte, typeof(byte))] + [InlineData(EdmPrimitiveTypeKind.Decimal, typeof(decimal))] + [InlineData(EdmPrimitiveTypeKind.Double, typeof(double))] + [InlineData(EdmPrimitiveTypeKind.Guid, typeof(Guid))] + [InlineData(EdmPrimitiveTypeKind.Int16, typeof(short))] + [InlineData(EdmPrimitiveTypeKind.Int32, typeof(int))] + [InlineData(EdmPrimitiveTypeKind.Int64, typeof(long))] + [InlineData(EdmPrimitiveTypeKind.SByte, typeof(sbyte))] + [InlineData(EdmPrimitiveTypeKind.Single, typeof(float))] + [InlineData(EdmPrimitiveTypeKind.Binary, typeof(byte[]))] + [InlineData(EdmPrimitiveTypeKind.Stream, typeof(Stream))] + [InlineData(EdmPrimitiveTypeKind.DateTimeOffset, typeof(DateTimeOffset))] + [InlineData(EdmPrimitiveTypeKind.Duration, typeof(TimeSpan))] + [InlineData(EdmPrimitiveTypeKind.Date, typeof(Date))] + [InlineData(EdmPrimitiveTypeKind.TimeOfDay, typeof(TimeOfDay))] + public void GetClrType_WorksAsExpected_ForStandardPrimitive(EdmPrimitiveTypeKind kind, Type expected) + { + // #1 Arrange & Act & Assert for nullable equals to false + IEdmPrimitiveType primitiveType = EdmCoreModel.Instance.GetPrimitiveType(kind); + Type clrType = _mapper.GetClrType(EdmModel, primitiveType, false, assembliesResolver: null); + Assert.Equal(expected, clrType); + + // #2 Arrange & Act & Assert for nullable equals to true + clrType = _mapper.GetClrType(EdmModel, primitiveType, true, assembliesResolver: null); + if (expected.IsValueType) { - // #1 Arrange & Act & Assert for nullable equals to false - IEdmPrimitiveType primitiveType = EdmCoreModel.Instance.GetPrimitiveType(kind); - Type clrType = _mapper.GetClrType(EdmModel, primitiveType, false, assembliesResolver: null); - Assert.Equal(expected, clrType); - - // #2 Arrange & Act & Assert for nullable equals to true - clrType = _mapper.GetClrType(EdmModel, primitiveType, true, assembliesResolver: null); - if (expected.IsValueType) - { - Type generic = typeof(Nullable<>); - expected = generic.MakeGenericType(expected); - Assert.Same(expected, clrType); - } - else - { - Assert.Same(expected, clrType); - } + Type generic = typeof(Nullable<>); + expected = generic.MakeGenericType(expected); + Assert.Same(expected, clrType); } - - [Theory] - [InlineData(EdmPrimitiveTypeKind.Geography, typeof(Geography))] - [InlineData(EdmPrimitiveTypeKind.GeographyPoint, typeof(GeographyPoint))] - [InlineData(EdmPrimitiveTypeKind.GeographyLineString, typeof(GeographyLineString))] - [InlineData(EdmPrimitiveTypeKind.GeographyPolygon, typeof(GeographyPolygon))] - [InlineData(EdmPrimitiveTypeKind.GeographyCollection, typeof(GeographyCollection))] - [InlineData(EdmPrimitiveTypeKind.GeographyMultiLineString, typeof(GeographyMultiLineString))] - [InlineData(EdmPrimitiveTypeKind.GeographyMultiPoint, typeof(GeographyMultiPoint))] - [InlineData(EdmPrimitiveTypeKind.GeographyMultiPolygon, typeof(GeographyMultiPolygon))] - [InlineData(EdmPrimitiveTypeKind.Geometry, typeof(Geometry))] - [InlineData(EdmPrimitiveTypeKind.GeometryPoint, typeof(GeometryPoint))] - [InlineData(EdmPrimitiveTypeKind.GeometryLineString, typeof(GeometryLineString))] - [InlineData(EdmPrimitiveTypeKind.GeometryPolygon, typeof(GeometryPolygon))] - [InlineData(EdmPrimitiveTypeKind.GeometryCollection, typeof(GeometryCollection))] - [InlineData(EdmPrimitiveTypeKind.GeometryMultiLineString, typeof(GeometryMultiLineString))] - [InlineData(EdmPrimitiveTypeKind.GeometryMultiPoint, typeof(GeometryMultiPoint))] - [InlineData(EdmPrimitiveTypeKind.GeometryMultiPolygon, typeof(GeometryMultiPolygon))] - public void GetClrType_WorksAsExpected_ForSpatialPrimitive(EdmPrimitiveTypeKind kind, Type type) + else { - // Arrange - IEdmPrimitiveType primitiveType = EdmCoreModel.Instance.GetPrimitiveType(kind); + Assert.Same(expected, clrType); + } + } - // Act - Type clrType1 = _mapper.GetClrType(EdmModel, primitiveType, false, assembliesResolver: null); - Type clrType2 = _mapper.GetClrType(EdmModel, primitiveType, true, assembliesResolver: null); + [Theory] + [InlineData(EdmPrimitiveTypeKind.Geography, typeof(Geography))] + [InlineData(EdmPrimitiveTypeKind.GeographyPoint, typeof(GeographyPoint))] + [InlineData(EdmPrimitiveTypeKind.GeographyLineString, typeof(GeographyLineString))] + [InlineData(EdmPrimitiveTypeKind.GeographyPolygon, typeof(GeographyPolygon))] + [InlineData(EdmPrimitiveTypeKind.GeographyCollection, typeof(GeographyCollection))] + [InlineData(EdmPrimitiveTypeKind.GeographyMultiLineString, typeof(GeographyMultiLineString))] + [InlineData(EdmPrimitiveTypeKind.GeographyMultiPoint, typeof(GeographyMultiPoint))] + [InlineData(EdmPrimitiveTypeKind.GeographyMultiPolygon, typeof(GeographyMultiPolygon))] + [InlineData(EdmPrimitiveTypeKind.Geometry, typeof(Geometry))] + [InlineData(EdmPrimitiveTypeKind.GeometryPoint, typeof(GeometryPoint))] + [InlineData(EdmPrimitiveTypeKind.GeometryLineString, typeof(GeometryLineString))] + [InlineData(EdmPrimitiveTypeKind.GeometryPolygon, typeof(GeometryPolygon))] + [InlineData(EdmPrimitiveTypeKind.GeometryCollection, typeof(GeometryCollection))] + [InlineData(EdmPrimitiveTypeKind.GeometryMultiLineString, typeof(GeometryMultiLineString))] + [InlineData(EdmPrimitiveTypeKind.GeometryMultiPoint, typeof(GeometryMultiPoint))] + [InlineData(EdmPrimitiveTypeKind.GeometryMultiPolygon, typeof(GeometryMultiPolygon))] + public void GetClrType_WorksAsExpected_ForSpatialPrimitive(EdmPrimitiveTypeKind kind, Type type) + { + // Arrange + IEdmPrimitiveType primitiveType = EdmCoreModel.Instance.GetPrimitiveType(kind); - // Assert - Assert.Same(clrType1, clrType2); - Assert.Same(type, clrType1); - } + // Act + Type clrType1 = _mapper.GetClrType(EdmModel, primitiveType, false, assembliesResolver: null); + Type clrType2 = _mapper.GetClrType(EdmModel, primitiveType, true, assembliesResolver: null); - [Theory] - [InlineData("NS.Address", typeof(MyAddress))] // use ClrTypeAnnotation - [InlineData("NS.CnAddress", typeof(CnMyAddress))] - [InlineData("Microsoft.AspNetCore.OData.Tests.Edm.MyCustomer", typeof(MyCustomer))] // use the full name match - public void GetClrType_WorksAsExpected_ForSchemaStrucutralType(string typeName, Type expected) - { - // Arrange - IEdmType edmType = EdmModel.FindType(typeName); - Assert.NotNull(edmType); // Guard + // Assert + Assert.Same(clrType1, clrType2); + Assert.Same(type, clrType1); + } - // #1. Act & Assert - Type clrType = _mapper.GetClrType(EdmModel, edmType, true, new AssemblyResolver()); - Assert.Same(expected, clrType); + [Theory] + [InlineData("NS.Address", typeof(MyAddress))] // use ClrTypeAnnotation + [InlineData("NS.CnAddress", typeof(CnMyAddress))] + [InlineData("Microsoft.AspNetCore.OData.Tests.Edm.MyCustomer", typeof(MyCustomer))] // use the full name match + public void GetClrType_WorksAsExpected_ForSchemaStrucutralType(string typeName, Type expected) + { + // Arrange + IEdmType edmType = EdmModel.FindType(typeName); + Assert.NotNull(edmType); // Guard - // #2. Act & Assert - clrType = _mapper.GetClrType(EdmModel, edmType, false, new AssemblyResolver()); - Assert.Same(expected, clrType); - } + // #1. Act & Assert + Type clrType = _mapper.GetClrType(EdmModel, edmType, true, new AssemblyResolver()); + Assert.Same(expected, clrType); - [Fact] - public void GetClrType_WorksAsExpected_ForSchemaEnumType() - { - // Arrange - IEdmType edmType = EdmModel.FindType("NS.Color"); - Assert.NotNull(edmType); // Guard + // #2. Act & Assert + clrType = _mapper.GetClrType(EdmModel, edmType, false, new AssemblyResolver()); + Assert.Same(expected, clrType); + } - // #1. Act & Assert - Type clrType = _mapper.GetClrType(EdmModel, edmType, true, null); - Assert.Same(typeof(MyColor?), clrType); + [Fact] + public void GetClrType_WorksAsExpected_ForSchemaEnumType() + { + // Arrange + IEdmType edmType = EdmModel.FindType("NS.Color"); + Assert.NotNull(edmType); // Guard - // #2. Act & Assert - clrType = _mapper.GetClrType(EdmModel, edmType, false, null); - Assert.Same(typeof(MyColor), clrType); - } + // #1. Act & Assert + Type clrType = _mapper.GetClrType(EdmModel, edmType, true, null); + Assert.Same(typeof(MyColor?), clrType); - #endregion + // #2. Act & Assert + clrType = _mapper.GetClrType(EdmModel, edmType, false, null); + Assert.Same(typeof(MyColor), clrType); + } - #region GetEdmType - [Fact] - public void GetEdmType_ThrowsArgumentNull_ModelAndClrType() - { - // Arrange & Act - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => _mapper.GetEdmTypeReference(model, typeof(TypeNotInModel)), "edmModel"); + #endregion - model = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => _mapper.GetEdmTypeReference(model, null), "clrType"); - } + #region GetEdmType + [Fact] + public void GetEdmType_ThrowsArgumentNull_ModelAndClrType() + { + // Arrange & Act + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => _mapper.GetEdmTypeReference(model, typeof(TypeNotInModel)), "edmModel"); - [Fact] - public void GetEdmTypeReference_ReturnsNull_ForUnknownType() - { - // Arrange & Act & Assert - Assert.Null(_mapper.GetEdmTypeReference(EdmModel, typeof(TypeNotInModel))); - } + model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => _mapper.GetEdmTypeReference(model, null), "clrType"); + } - [Theory] - [InlineData(typeof(IEnumerable), "NS.BaseType")] - [InlineData(typeof(IEnumerable), "NS.Derived1Type")] - [InlineData(typeof(Derived2Type[]), "NS.Derived2Type")] - public void GetEdmTypeReference_ReturnsCollection_ForIEnumerableOfT(Type clrType, string typeName) - { - // Arrange & Act - IEdmType edmType = _mapper.GetEdmType(EdmModel, clrType); + [Fact] + public void GetEdmTypeReference_ReturnsNull_ForUnknownType() + { + // Arrange & Act & Assert + Assert.Null(_mapper.GetEdmTypeReference(EdmModel, typeof(TypeNotInModel))); + } - // Assert - Assert.Equal(EdmTypeKind.Collection, edmType.TypeKind); - Assert.Equal(typeName, (edmType as IEdmCollectionType).ElementType.FullName()); - } + [Theory] + [InlineData(typeof(IEnumerable), "NS.BaseType")] + [InlineData(typeof(IEnumerable), "NS.Derived1Type")] + [InlineData(typeof(Derived2Type[]), "NS.Derived2Type")] + public void GetEdmTypeReference_ReturnsCollection_ForIEnumerableOfT(Type clrType, string typeName) + { + // Arrange & Act + IEdmType edmType = _mapper.GetEdmType(EdmModel, clrType); - [Theory] - [InlineData(typeof(string), "Edm.String")] - [InlineData(typeof(int?), "Edm.Int32")] - [InlineData(typeof(MyAddress), "NS.Address")] - [InlineData(typeof(CnMyAddress), "NS.CnAddress")] - [InlineData(typeof(MyCustomer), "Microsoft.AspNetCore.OData.Tests.Edm.MyCustomer")] - [InlineData(typeof(BaseType), "NS.BaseType")] - [InlineData(typeof(Derived1Type), "NS.Derived1Type")] - [InlineData(typeof(Derived2Type), "NS.Derived2Type")] - [InlineData(typeof(SubDerivedType), "NS.SubDerivedType")] - public void GetEdmTypeReference_WorksAsExpected_ForEdmType(Type clrType, string typeName) - { - // Arrange - IEdmType expectedEdmType = EdmModel.FindType(typeName); - Assert.NotNull(expectedEdmType); // Guard - - // Arrange & Act - IEdmTypeReference edmTypeRef = _mapper.GetEdmTypeReference(EdmModel, clrType); - IEdmType edmType = _mapper.GetEdmType(EdmModel, clrType); - - // Assert - Assert.NotNull(edmTypeRef); - Assert.Same(expectedEdmType, edmTypeRef.Definition); - Assert.Same(expectedEdmType, edmType); - Assert.True(edmTypeRef.IsNullable); - } + // Assert + Assert.Equal(EdmTypeKind.Collection, edmType.TypeKind); + Assert.Equal(typeName, (edmType as IEdmCollectionType).ElementType.FullName()); + } - [Fact] - public void GetEdmTypeReference_WorksAsExpected_ForSchemaEnumType() - { - // Arrange - IEdmType expectedType = EdmModel.FindType("NS.Color"); - Assert.NotNull(expectedType); // Guard - - // #1. Act & Assert - IEdmTypeReference colorType = _mapper.GetEdmTypeReference(EdmModel, typeof(MyColor)); - Assert.Same(expectedType, colorType.Definition); - Assert.False(colorType.IsNullable); - - // #2. Act & Assert - colorType = _mapper.GetEdmTypeReference(EdmModel, typeof(MyColor?)); - Assert.Same(expectedType, colorType.Definition); - Assert.True(colorType.IsNullable); - } - #endregion - - [Theory] - [InlineData(typeof(MyCustomer), "MyCustomer")] - [InlineData(typeof(int), "Int32")] - [InlineData(typeof(IEnumerable), "IEnumerable_1OfInt32")] - [InlineData(typeof(IEnumerable>), "IEnumerable_1OfFunc_2OfInt32_String")] - [InlineData(typeof(List>), "List_1OfFunc_2OfInt32_String")] - public void EdmFullName(Type clrType, string expectedName) - { - // Arrange & Act & Assert - Assert.Equal(expectedName, clrType.EdmName()); - } + [Theory] + [InlineData(typeof(string), "Edm.String")] + [InlineData(typeof(int?), "Edm.Int32")] + [InlineData(typeof(MyAddress), "NS.Address")] + [InlineData(typeof(CnMyAddress), "NS.CnAddress")] + [InlineData(typeof(MyCustomer), "Microsoft.AspNetCore.OData.Tests.Edm.MyCustomer")] + [InlineData(typeof(BaseType), "NS.BaseType")] + [InlineData(typeof(Derived1Type), "NS.Derived1Type")] + [InlineData(typeof(Derived2Type), "NS.Derived2Type")] + [InlineData(typeof(SubDerivedType), "NS.SubDerivedType")] + public void GetEdmTypeReference_WorksAsExpected_ForEdmType(Type clrType, string typeName) + { + // Arrange + IEdmType expectedEdmType = EdmModel.FindType(typeName); + Assert.NotNull(expectedEdmType); // Guard + + // Arrange & Act + IEdmTypeReference edmTypeRef = _mapper.GetEdmTypeReference(EdmModel, clrType); + IEdmType edmType = _mapper.GetEdmType(EdmModel, clrType); + + // Assert + Assert.NotNull(edmTypeRef); + Assert.Same(expectedEdmType, edmTypeRef.Definition); + Assert.Same(expectedEdmType, edmType); + Assert.True(edmTypeRef.IsNullable); + } - private static IEdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); - - // ComplexType: Address - EdmComplexType address = new EdmComplexType("NS", "Address"); - address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); - model.AddElement(address); - model.SetAnnotationValue(address, new ClrTypeAnnotation(typeof(MyAddress))); - - // ComplexType: CnAddress - var cnAddress = new EdmComplexType("NS", "CnAddress", address); - cnAddress.AddStructuralProperty("Zipcode", EdmPrimitiveTypeKind.String); - model.AddElement(cnAddress); - model.SetAnnotationValue(cnAddress, new ClrTypeAnnotation(typeof(CnMyAddress))); - - // EnumType: Color - var color = new EdmEnumType("NS", "Color"); - model.AddElement(color); - model.SetAnnotationValue(color, new ClrTypeAnnotation(typeof(MyColor))); - - // EntityType: MyCustomer - var customer = new EdmEntityType("Microsoft.AspNetCore.OData.Tests.Edm", "MyCustomer"); - model.AddElement(customer); - - // Inheritance EntityType - var baseEntity = new EdmEntityType("NS", "BaseType"); - var derived1Entity = new EdmEntityType("NS", "Derived1Type", baseEntity); - var derived2Entity = new EdmEntityType("NS", "Derived2Type", baseEntity); - var subDerivedEntity = new EdmEntityType("NS", "SubDerivedType", derived1Entity); - model.AddElements(new[] { baseEntity, derived1Entity, derived2Entity, subDerivedEntity }); - model.SetAnnotationValue(baseEntity, new ClrTypeAnnotation(typeof(BaseType))); - model.SetAnnotationValue(derived1Entity, new ClrTypeAnnotation(typeof(Derived1Type))); - model.SetAnnotationValue(derived2Entity, new ClrTypeAnnotation(typeof(Derived2Type))); - model.SetAnnotationValue(subDerivedEntity, new ClrTypeAnnotation(typeof(SubDerivedType))); - - return model; - } + [Fact] + public void GetEdmTypeReference_WorksAsExpected_ForSchemaEnumType() + { + // Arrange + IEdmType expectedType = EdmModel.FindType("NS.Color"); + Assert.NotNull(expectedType); // Guard + + // #1. Act & Assert + IEdmTypeReference colorType = _mapper.GetEdmTypeReference(EdmModel, typeof(MyColor)); + Assert.Same(expectedType, colorType.Definition); + Assert.False(colorType.IsNullable); + + // #2. Act & Assert + colorType = _mapper.GetEdmTypeReference(EdmModel, typeof(MyColor?)); + Assert.Same(expectedType, colorType.Definition); + Assert.True(colorType.IsNullable); + } + #endregion + + [Theory] + [InlineData(typeof(MyCustomer), "MyCustomer")] + [InlineData(typeof(int), "Int32")] + [InlineData(typeof(IEnumerable), "IEnumerable_1OfInt32")] + [InlineData(typeof(IEnumerable>), "IEnumerable_1OfFunc_2OfInt32_String")] + [InlineData(typeof(List>), "List_1OfFunc_2OfInt32_String")] + public void EdmFullName(Type clrType, string expectedName) + { + // Arrange & Act & Assert + Assert.Equal(expectedName, clrType.EdmName()); + } - public class MyAddress - { - public string City { get; set; } - } + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + + // ComplexType: Address + EdmComplexType address = new EdmComplexType("NS", "Address"); + address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + model.AddElement(address); + model.SetAnnotationValue(address, new ClrTypeAnnotation(typeof(MyAddress))); + + // ComplexType: CnAddress + var cnAddress = new EdmComplexType("NS", "CnAddress", address); + cnAddress.AddStructuralProperty("Zipcode", EdmPrimitiveTypeKind.String); + model.AddElement(cnAddress); + model.SetAnnotationValue(cnAddress, new ClrTypeAnnotation(typeof(CnMyAddress))); + + // EnumType: Color + var color = new EdmEnumType("NS", "Color"); + model.AddElement(color); + model.SetAnnotationValue(color, new ClrTypeAnnotation(typeof(MyColor))); + + // EntityType: MyCustomer + var customer = new EdmEntityType("Microsoft.AspNetCore.OData.Tests.Edm", "MyCustomer"); + model.AddElement(customer); + + // Inheritance EntityType + var baseEntity = new EdmEntityType("NS", "BaseType"); + var derived1Entity = new EdmEntityType("NS", "Derived1Type", baseEntity); + var derived2Entity = new EdmEntityType("NS", "Derived2Type", baseEntity); + var subDerivedEntity = new EdmEntityType("NS", "SubDerivedType", derived1Entity); + model.AddElements(new[] { baseEntity, derived1Entity, derived2Entity, subDerivedEntity }); + model.SetAnnotationValue(baseEntity, new ClrTypeAnnotation(typeof(BaseType))); + model.SetAnnotationValue(derived1Entity, new ClrTypeAnnotation(typeof(Derived1Type))); + model.SetAnnotationValue(derived2Entity, new ClrTypeAnnotation(typeof(Derived2Type))); + model.SetAnnotationValue(subDerivedEntity, new ClrTypeAnnotation(typeof(SubDerivedType))); + + return model; + } - public class CnMyAddress : MyAddress - { - public string Zipcode { get; set; } - } + public class MyAddress + { + public string City { get; set; } + } - public enum MyColor - { - Red - } + public class CnMyAddress : MyAddress + { + public string Zipcode { get; set; } + } - public class BaseType - { } + public enum MyColor + { + Red + } - public class Derived1Type : BaseType - { } + public class BaseType + { } - public class Derived2Type : BaseType - { } + public class Derived1Type : BaseType + { } - public class SubDerivedType : Derived1Type - { } + public class Derived2Type : BaseType + { } - public class TypeNotInModel - { } + public class SubDerivedType : Derived1Type + { } - public class AssemblyResolver : IAssemblyResolver + public class TypeNotInModel + { } + + public class AssemblyResolver : IAssemblyResolver + { + public IEnumerable Assemblies { - public IEnumerable Assemblies + get { - get - { - yield return typeof(AssemblyResolver).Assembly; - } + yield return typeof(AssemblyResolver).Assembly; } } } - - public class MyCustomer - { } } + +public class MyCustomer +{ } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmClrTypeMapExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmClrTypeMapExtensionsTests.cs index 77d289cd9..7270e4a54 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmClrTypeMapExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmClrTypeMapExtensionsTests.cs @@ -15,211 +15,210 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class EdmClrTypeMapExtensionsTests { - public class EdmClrTypeMapExtensionsTests + [Fact] + public void GetEdmPrimitiveTypeReference_Calls_GetPrimitiveTypeOnMapper() + { + // Arrange + Type type = typeof(int); + Mock mapper = new Mock(); + mapper.Setup(x => x.GetEdmPrimitiveType(type)).Verifiable(); + + EdmModel model = new EdmModel(); + model.SetTypeMapper(mapper.Object); + + // Act + model.GetEdmPrimitiveTypeReference(type); + + // Assert + mapper.Verify(); + } + + [Fact] + public void GetClrPrimitiveType_Calls_GetPrimitiveTypeOnMapper() + { + // Arrange + Mock primitiveType = new Mock(); + Mock edmType = new Mock(); + edmType.Setup(x => x.Definition).Returns(primitiveType.Object); + edmType.Setup(x => x.IsNullable).Returns(true); + + Mock mapper = new Mock(); + mapper.Setup(x => x.GetClrPrimitiveType(edmType.Object.PrimitiveDefinition(), edmType.Object.IsNullable)).Verifiable(); + + EdmModel model = new EdmModel(); + model.SetTypeMapper(mapper.Object); + + // Act + model.GetClrPrimitiveType(edmType.Object); + + // Assert + mapper.Verify(); + } + + [Theory] + [InlineData(null, null, false)] + [InlineData(typeof(int), typeof(int), false)] + [InlineData(typeof(int?), typeof(int?), false)] + [InlineData(typeof(object), typeof(object), false)] + // non-standard primitive types + [InlineData(typeof(XElement), typeof(string), true)] + [InlineData(typeof(ushort), typeof(int), true)] + [InlineData(typeof(ushort?), typeof(int?), true)] + [InlineData(typeof(uint), typeof(long), true)] + [InlineData(typeof(uint?), typeof(long?), true)] + [InlineData(typeof(ulong), typeof(long), true)] + [InlineData(typeof(ulong?), typeof(long?), true)] + [InlineData(typeof(char[]), typeof(string), true)] + [InlineData(typeof(char), typeof(string), true)] + [InlineData(typeof(char?), typeof(string), true)] + [InlineData(typeof(DateTime), typeof(DateTimeOffset), true)] + [InlineData(typeof(DateTime?), typeof(DateTimeOffset?), true)] + public void IsNonstandardEdmPrimitive_WorksAsExpected_ForNonstandardType(Type clrType, Type expectType, bool isNonstandard) + { + // Arrange + EdmModel model = new EdmModel(); + model.SetTypeMapper(DefaultODataTypeMapper.Default); + + // Act + Type actual = model.IsNonstandardEdmPrimitive(clrType, out bool isNonstandardEdmPrimtive); + + // Assert + Assert.Equal(expectType, actual); + Assert.Equal(isNonstandard, isNonstandardEdmPrimtive); + } + + [Fact] + public void GetEdmTypeReference_Calls_GetEdmTypeReferenceOnMapper() + { + // Arrange + Type type = typeof(int); + EdmModel model = new EdmModel(); + + Mock mapper = new Mock(); + mapper.Setup(x => x.GetEdmTypeReference(model, type)).Verifiable(); + model.SetTypeMapper(mapper.Object); + + // Act + model.GetEdmTypeReference(type); + + // Assert + mapper.Verify(); + } + + [Fact] + public void GetEdmType_Calls_GetEdmTypeReferenceOnMapper() + { + // Arrange + Type type = typeof(int); + EdmModel model = new EdmModel(); + + Mock mapper = new Mock(); + mapper.Setup(x => x.GetEdmTypeReference(model, type)).Verifiable(); + model.SetTypeMapper(mapper.Object); + + // Act + model.GetEdmType(type); + + // Assert + mapper.Verify(); + } + + [Fact] + public void GetClrType_Calls_GetClrTypeOnMapper() + { + // Arrange + Mock edmType = new Mock(); + Mock edmTypeRef = new Mock(); + edmTypeRef.Setup(x => x.Definition).Returns(edmType.Object); + edmTypeRef.Setup(x => x.IsNullable).Returns(true); + + EdmModel model = new EdmModel(); + + Mock mapper = new Mock(); + mapper.Setup(x => x.GetClrType(model, edmType.Object, true, AssemblyResolverHelper.Default)).Verifiable(); + model.SetTypeMapper(mapper.Object); + + // Act + model.GetClrType(edmTypeRef.Object); + + // Assert + mapper.Verify(); + } + + [Fact] + public void GetClrTypeWithResolver_Calls_GetClrTypeOnMapper() + { + // Arrange + Mock resolver = new Mock(); + Mock edmType = new Mock(); + Mock edmTypeRef = new Mock(); + edmTypeRef.Setup(x => x.Definition).Returns(edmType.Object); + edmTypeRef.Setup(x => x.IsNullable).Returns(true); + + EdmModel model = new EdmModel(); + + Mock mapper = new Mock(); + mapper.Setup(x => x.GetClrType(model, edmType.Object, true, resolver.Object)).Verifiable(); + model.SetTypeMapper(mapper.Object); + + // Act + model.GetClrType(edmTypeRef.Object, resolver.Object); + + // Assert + mapper.Verify(); + } + + [Fact] + public void GetClrTypeUsingEdmType_Calls_GetClrTypeOnMapper() + { + // Arrange + Mock edmType = new Mock(); + + EdmModel model = new EdmModel(); + Mock mapper = new Mock(); + mapper.Setup(x => x.GetClrType(model, edmType.Object, true, AssemblyResolverHelper.Default)).Verifiable(); + model.SetTypeMapper(mapper.Object); + + // Act + model.GetClrType(edmType.Object); + + // Assert + mapper.Verify(); + } + + [Fact] + public void GetClrTypeUsingEdmTypeWithResolver_Calls_GetClrTypeOnMapper() + { + // Arrange + Mock resolver = new Mock(); + Mock edmType = new Mock(); + + EdmModel model = new EdmModel(); + + Mock mapper = new Mock(); + mapper.Setup(x => x.GetClrType(model, edmType.Object, true, resolver.Object)).Verifiable(); + model.SetTypeMapper(mapper.Object); + + // Act + model.GetClrType(edmType.Object, resolver.Object); + + // Assert + mapper.Verify(); + } + + [Theory] + [InlineData(typeof(MyCustomer), "MyCustomer")] + [InlineData(typeof(int), "Int32")] + [InlineData(typeof(IEnumerable), "IEnumerable_1OfInt32")] + [InlineData(typeof(IEnumerable>), "IEnumerable_1OfFunc_2OfInt32_String")] + [InlineData(typeof(List>), "List_1OfFunc_2OfInt32_String")] + public void EdmFullName(Type clrType, string expectedName) { - [Fact] - public void GetEdmPrimitiveTypeReference_Calls_GetPrimitiveTypeOnMapper() - { - // Arrange - Type type = typeof(int); - Mock mapper = new Mock(); - mapper.Setup(x => x.GetEdmPrimitiveType(type)).Verifiable(); - - EdmModel model = new EdmModel(); - model.SetTypeMapper(mapper.Object); - - // Act - model.GetEdmPrimitiveTypeReference(type); - - // Assert - mapper.Verify(); - } - - [Fact] - public void GetClrPrimitiveType_Calls_GetPrimitiveTypeOnMapper() - { - // Arrange - Mock primitiveType = new Mock(); - Mock edmType = new Mock(); - edmType.Setup(x => x.Definition).Returns(primitiveType.Object); - edmType.Setup(x => x.IsNullable).Returns(true); - - Mock mapper = new Mock(); - mapper.Setup(x => x.GetClrPrimitiveType(edmType.Object.PrimitiveDefinition(), edmType.Object.IsNullable)).Verifiable(); - - EdmModel model = new EdmModel(); - model.SetTypeMapper(mapper.Object); - - // Act - model.GetClrPrimitiveType(edmType.Object); - - // Assert - mapper.Verify(); - } - - [Theory] - [InlineData(null, null, false)] - [InlineData(typeof(int), typeof(int), false)] - [InlineData(typeof(int?), typeof(int?), false)] - [InlineData(typeof(object), typeof(object), false)] - // non-standard primitive types - [InlineData(typeof(XElement), typeof(string), true)] - [InlineData(typeof(ushort), typeof(int), true)] - [InlineData(typeof(ushort?), typeof(int?), true)] - [InlineData(typeof(uint), typeof(long), true)] - [InlineData(typeof(uint?), typeof(long?), true)] - [InlineData(typeof(ulong), typeof(long), true)] - [InlineData(typeof(ulong?), typeof(long?), true)] - [InlineData(typeof(char[]), typeof(string), true)] - [InlineData(typeof(char), typeof(string), true)] - [InlineData(typeof(char?), typeof(string), true)] - [InlineData(typeof(DateTime), typeof(DateTimeOffset), true)] - [InlineData(typeof(DateTime?), typeof(DateTimeOffset?), true)] - public void IsNonstandardEdmPrimitive_WorksAsExpected_ForNonstandardType(Type clrType, Type expectType, bool isNonstandard) - { - // Arrange - EdmModel model = new EdmModel(); - model.SetTypeMapper(DefaultODataTypeMapper.Default); - - // Act - Type actual = model.IsNonstandardEdmPrimitive(clrType, out bool isNonstandardEdmPrimtive); - - // Assert - Assert.Equal(expectType, actual); - Assert.Equal(isNonstandard, isNonstandardEdmPrimtive); - } - - [Fact] - public void GetEdmTypeReference_Calls_GetEdmTypeReferenceOnMapper() - { - // Arrange - Type type = typeof(int); - EdmModel model = new EdmModel(); - - Mock mapper = new Mock(); - mapper.Setup(x => x.GetEdmTypeReference(model, type)).Verifiable(); - model.SetTypeMapper(mapper.Object); - - // Act - model.GetEdmTypeReference(type); - - // Assert - mapper.Verify(); - } - - [Fact] - public void GetEdmType_Calls_GetEdmTypeReferenceOnMapper() - { - // Arrange - Type type = typeof(int); - EdmModel model = new EdmModel(); - - Mock mapper = new Mock(); - mapper.Setup(x => x.GetEdmTypeReference(model, type)).Verifiable(); - model.SetTypeMapper(mapper.Object); - - // Act - model.GetEdmType(type); - - // Assert - mapper.Verify(); - } - - [Fact] - public void GetClrType_Calls_GetClrTypeOnMapper() - { - // Arrange - Mock edmType = new Mock(); - Mock edmTypeRef = new Mock(); - edmTypeRef.Setup(x => x.Definition).Returns(edmType.Object); - edmTypeRef.Setup(x => x.IsNullable).Returns(true); - - EdmModel model = new EdmModel(); - - Mock mapper = new Mock(); - mapper.Setup(x => x.GetClrType(model, edmType.Object, true, AssemblyResolverHelper.Default)).Verifiable(); - model.SetTypeMapper(mapper.Object); - - // Act - model.GetClrType(edmTypeRef.Object); - - // Assert - mapper.Verify(); - } - - [Fact] - public void GetClrTypeWithResolver_Calls_GetClrTypeOnMapper() - { - // Arrange - Mock resolver = new Mock(); - Mock edmType = new Mock(); - Mock edmTypeRef = new Mock(); - edmTypeRef.Setup(x => x.Definition).Returns(edmType.Object); - edmTypeRef.Setup(x => x.IsNullable).Returns(true); - - EdmModel model = new EdmModel(); - - Mock mapper = new Mock(); - mapper.Setup(x => x.GetClrType(model, edmType.Object, true, resolver.Object)).Verifiable(); - model.SetTypeMapper(mapper.Object); - - // Act - model.GetClrType(edmTypeRef.Object, resolver.Object); - - // Assert - mapper.Verify(); - } - - [Fact] - public void GetClrTypeUsingEdmType_Calls_GetClrTypeOnMapper() - { - // Arrange - Mock edmType = new Mock(); - - EdmModel model = new EdmModel(); - Mock mapper = new Mock(); - mapper.Setup(x => x.GetClrType(model, edmType.Object, true, AssemblyResolverHelper.Default)).Verifiable(); - model.SetTypeMapper(mapper.Object); - - // Act - model.GetClrType(edmType.Object); - - // Assert - mapper.Verify(); - } - - [Fact] - public void GetClrTypeUsingEdmTypeWithResolver_Calls_GetClrTypeOnMapper() - { - // Arrange - Mock resolver = new Mock(); - Mock edmType = new Mock(); - - EdmModel model = new EdmModel(); - - Mock mapper = new Mock(); - mapper.Setup(x => x.GetClrType(model, edmType.Object, true, resolver.Object)).Verifiable(); - model.SetTypeMapper(mapper.Object); - - // Act - model.GetClrType(edmType.Object, resolver.Object); - - // Assert - mapper.Verify(); - } - - [Theory] - [InlineData(typeof(MyCustomer), "MyCustomer")] - [InlineData(typeof(int), "Int32")] - [InlineData(typeof(IEnumerable), "IEnumerable_1OfInt32")] - [InlineData(typeof(IEnumerable>), "IEnumerable_1OfFunc_2OfInt32_String")] - [InlineData(typeof(List>), "List_1OfFunc_2OfInt32_String")] - public void EdmFullName(Type clrType, string expectedName) - { - // Arrange & Act & Assert - Assert.Equal(expectedName, clrType.EdmName()); - } + // Arrange & Act & Assert + Assert.Equal(expectedName, clrType.EdmName()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmHelpersTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmHelpersTests.cs index 8cda7f1a4..bf5706d4f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmHelpersTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmHelpersTests.cs @@ -14,218 +14,217 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class EdmHelpersTests { - public class EdmHelpersTests + [Fact] + public void ToStructuredTypeReference_ReturnsCorrectly() { - [Fact] - public void ToStructuredTypeReference_ReturnsCorrectly() - { - // 1) null - IEdmTypeReference typeReference = null; - Assert.Null(typeReference.ToStructuredTypeReference()); + // 1) null + IEdmTypeReference typeReference = null; + Assert.Null(typeReference.ToStructuredTypeReference()); - // 2) Edm.ComplexType - typeReference = EdmCoreModel.Instance.GetComplexType(false); - Assert.Same(typeReference, typeReference.ToStructuredTypeReference()); + // 2) Edm.ComplexType + typeReference = EdmCoreModel.Instance.GetComplexType(false); + Assert.Same(typeReference, typeReference.ToStructuredTypeReference()); - // 3) Edm.Untyped - typeReference = EdmCoreModel.Instance.GetUntyped(); - Assert.Same(EdmUntypedStructuredTypeReference.NullableTypeReference, typeReference.ToStructuredTypeReference()); - } + // 3) Edm.Untyped + typeReference = EdmCoreModel.Instance.GetUntyped(); + Assert.Same(EdmUntypedStructuredTypeReference.NullableTypeReference, typeReference.ToStructuredTypeReference()); + } - [Fact] - public void IsStructuredOrUntypedStructuredCollection_ReturnsCorrectly() - { - // 1) null - IEdmTypeReference typeReference = null; - Assert.False(typeReference.IsStructuredOrUntypedStructuredCollection()); + [Fact] + public void IsStructuredOrUntypedStructuredCollection_ReturnsCorrectly() + { + // 1) null + IEdmTypeReference typeReference = null; + Assert.False(typeReference.IsStructuredOrUntypedStructuredCollection()); - // 2) non-collection - typeReference = EdmCoreModel.Instance.GetInt32(false); - Assert.False(typeReference.IsStructuredOrUntypedStructuredCollection()); + // 2) non-collection + typeReference = EdmCoreModel.Instance.GetInt32(false); + Assert.False(typeReference.IsStructuredOrUntypedStructuredCollection()); - // 3) Collection(Edm.ComplexType) - typeReference = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetComplexType(false))); - Assert.True(typeReference.IsStructuredOrUntypedStructuredCollection()); - } + // 3) Collection(Edm.ComplexType) + typeReference = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetComplexType(false))); + Assert.True(typeReference.IsStructuredOrUntypedStructuredCollection()); + } - [Fact] - public void IsStructuredOrUntypedStructured_ReturnsCorrectly() - { - // 1) null - IEdmTypeReference typeReference = null; - Assert.False(typeReference.IsStructuredOrUntypedStructured()); + [Fact] + public void IsStructuredOrUntypedStructured_ReturnsCorrectly() + { + // 1) null + IEdmTypeReference typeReference = null; + Assert.False(typeReference.IsStructuredOrUntypedStructured()); - // 2) non-structured - typeReference = EdmCoreModel.Instance.GetInt32(false); - Assert.False(typeReference.IsStructuredOrUntypedStructured()); + // 2) non-structured + typeReference = EdmCoreModel.Instance.GetInt32(false); + Assert.False(typeReference.IsStructuredOrUntypedStructured()); - // 3) Edm.ComplexType - typeReference = EdmCoreModel.Instance.GetComplexType(false); - Assert.True(typeReference.IsStructuredOrUntypedStructured()); + // 3) Edm.ComplexType + typeReference = EdmCoreModel.Instance.GetComplexType(false); + Assert.True(typeReference.IsStructuredOrUntypedStructured()); - // 4) Edm.Untyped (structured) - typeReference = EdmUntypedStructuredTypeReference.NullableTypeReference; - Assert.True(typeReference.IsStructuredOrUntypedStructured()); - } + // 4) Edm.Untyped (structured) + typeReference = EdmUntypedStructuredTypeReference.NullableTypeReference; + Assert.True(typeReference.IsStructuredOrUntypedStructured()); + } - [Fact] - public void IsUntypedOrCollectionUntyped_ReturnsCorrectly() - { - // 1) null - IEdmTypeReference typeReference = null; - Assert.False(typeReference.IsUntypedOrCollectionUntyped()); + [Fact] + public void IsUntypedOrCollectionUntyped_ReturnsCorrectly() + { + // 1) null + IEdmTypeReference typeReference = null; + Assert.False(typeReference.IsUntypedOrCollectionUntyped()); - // 2) Primitive - typeReference = EdmCoreModel.Instance.GetInt16(false); - Assert.False(typeReference.IsUntypedOrCollectionUntyped()); + // 2) Primitive + typeReference = EdmCoreModel.Instance.GetInt16(false); + Assert.False(typeReference.IsUntypedOrCollectionUntyped()); - // 3) Edm.Untyped - typeReference = EdmCoreModel.Instance.GetUntyped(); - Assert.True(typeReference.IsUntypedOrCollectionUntyped()); + // 3) Edm.Untyped + typeReference = EdmCoreModel.Instance.GetUntyped(); + Assert.True(typeReference.IsUntypedOrCollectionUntyped()); - // 4) Collection(Edm.Untyped) - typeReference = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetUntyped())); - Assert.True(typeReference.IsUntypedOrCollectionUntyped()); - } + // 4) Collection(Edm.Untyped) + typeReference = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetUntyped())); + Assert.True(typeReference.IsUntypedOrCollectionUntyped()); + } - [Fact] - public void GetElementTypeOrSelf_ReturnsCorrectly() - { - // 1) null - IEdmTypeReference typeReference = null; - IEdmTypeReference actualTypeRef = typeReference.GetElementTypeOrSelf(); - Assert.Null(actualTypeRef); - - // 2) non-collection - typeReference = EdmCoreModel.Instance.GetString(false); - actualTypeRef = typeReference.GetElementTypeOrSelf(); - Assert.Same(actualTypeRef, typeReference); - - // 3) Collection - typeReference = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetString(false))); - actualTypeRef = typeReference.GetElementTypeOrSelf(); - Assert.Same(actualTypeRef, typeReference.AsCollection().ElementType()); - } + [Fact] + public void GetElementTypeOrSelf_ReturnsCorrectly() + { + // 1) null + IEdmTypeReference typeReference = null; + IEdmTypeReference actualTypeRef = typeReference.GetElementTypeOrSelf(); + Assert.Null(actualTypeRef); + + // 2) non-collection + typeReference = EdmCoreModel.Instance.GetString(false); + actualTypeRef = typeReference.GetElementTypeOrSelf(); + Assert.Same(actualTypeRef, typeReference); + + // 3) Collection + typeReference = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetString(false))); + actualTypeRef = typeReference.GetElementTypeOrSelf(); + Assert.Same(actualTypeRef, typeReference.AsCollection().ElementType()); + } - [Fact] - public void GetElementType_ReturnsCorrectly() - { - // 1) null - IEdmTypeReference typeReference = null; - IEdmType actualTypeRef = typeReference.GetElementType(); - Assert.Null(actualTypeRef); - - // 2) non-collection - typeReference = EdmCoreModel.Instance.GetString(false); - actualTypeRef = typeReference.GetElementType(); - Assert.Same(actualTypeRef, typeReference.Definition); - - // 3) Collection - typeReference = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetString(false))); - actualTypeRef = typeReference.GetElementType(); - Assert.Same(actualTypeRef, typeReference.AsCollection().ElementType().Definition); - } + [Fact] + public void GetElementType_ReturnsCorrectly() + { + // 1) null + IEdmTypeReference typeReference = null; + IEdmType actualTypeRef = typeReference.GetElementType(); + Assert.Null(actualTypeRef); + + // 2) non-collection + typeReference = EdmCoreModel.Instance.GetString(false); + actualTypeRef = typeReference.GetElementType(); + Assert.Same(actualTypeRef, typeReference.Definition); + + // 3) Collection + typeReference = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetString(false))); + actualTypeRef = typeReference.GetElementType(); + Assert.Same(actualTypeRef, typeReference.AsCollection().ElementType().Definition); + } - public static TheoryDataSet ToEdmTypeReferenceTestData + public static TheoryDataSet ToEdmTypeReferenceTestData + { + get { - get + IEdmEntityType entity = new EdmEntityType("NS", "Entity"); + IEdmComplexType complex = new EdmComplexType("NS", "Complex"); + IEdmEnumType enumType = new EdmEnumType("NS", "Enum"); + IEdmPrimitiveType primitive = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); + IEdmPathType path = EdmCoreModel.Instance.GetPathType(EdmPathTypeKind.AnnotationPath); + IEdmTypeDefinition typeDefinition = new EdmTypeDefinition("NS", "TypeDef", primitive); + IEdmCollectionType collection = new EdmCollectionType(new EdmEntityTypeReference(entity, isNullable: false)); + IEdmCollectionType collectionNullable = new EdmCollectionType(new EdmEntityTypeReference(entity, isNullable: true)); + IEdmEntityReferenceType entityReferenceType = new EdmEntityReferenceType(entity); + + return new TheoryDataSet { - IEdmEntityType entity = new EdmEntityType("NS", "Entity"); - IEdmComplexType complex = new EdmComplexType("NS", "Complex"); - IEdmEnumType enumType = new EdmEnumType("NS", "Enum"); - IEdmPrimitiveType primitive = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); - IEdmPathType path = EdmCoreModel.Instance.GetPathType(EdmPathTypeKind.AnnotationPath); - IEdmTypeDefinition typeDefinition = new EdmTypeDefinition("NS", "TypeDef", primitive); - IEdmCollectionType collection = new EdmCollectionType(new EdmEntityTypeReference(entity, isNullable: false)); - IEdmCollectionType collectionNullable = new EdmCollectionType(new EdmEntityTypeReference(entity, isNullable: true)); - IEdmEntityReferenceType entityReferenceType = new EdmEntityReferenceType(entity); - - return new TheoryDataSet - { - { primitive, true, typeof(IEdmPrimitiveTypeReference) }, - { primitive, false, typeof(IEdmPrimitiveTypeReference) }, - { enumType, true, typeof(IEdmEnumTypeReference) }, - { enumType, false, typeof(IEdmEnumTypeReference) }, - { entity, true, typeof(IEdmEntityTypeReference) }, - { entity, false, typeof(IEdmEntityTypeReference) }, - { complex, true, typeof(IEdmComplexTypeReference) }, - { complex, false, typeof(IEdmComplexTypeReference) }, - { collectionNullable, true, typeof(IEdmCollectionTypeReference) }, - { collection, false, typeof(IEdmCollectionTypeReference) }, - { path, true, typeof(IEdmPathTypeReference) }, - { path, false, typeof(IEdmPathTypeReference) }, - { typeDefinition, true, typeof(IEdmTypeDefinitionReference) }, - { typeDefinition, false, typeof(IEdmTypeDefinitionReference) }, - { entityReferenceType, true, typeof(IEdmEntityReferenceTypeReference) }, - { entityReferenceType, true, typeof(IEdmEntityReferenceTypeReference) }, - }; - } + { primitive, true, typeof(IEdmPrimitiveTypeReference) }, + { primitive, false, typeof(IEdmPrimitiveTypeReference) }, + { enumType, true, typeof(IEdmEnumTypeReference) }, + { enumType, false, typeof(IEdmEnumTypeReference) }, + { entity, true, typeof(IEdmEntityTypeReference) }, + { entity, false, typeof(IEdmEntityTypeReference) }, + { complex, true, typeof(IEdmComplexTypeReference) }, + { complex, false, typeof(IEdmComplexTypeReference) }, + { collectionNullable, true, typeof(IEdmCollectionTypeReference) }, + { collection, false, typeof(IEdmCollectionTypeReference) }, + { path, true, typeof(IEdmPathTypeReference) }, + { path, false, typeof(IEdmPathTypeReference) }, + { typeDefinition, true, typeof(IEdmTypeDefinitionReference) }, + { typeDefinition, false, typeof(IEdmTypeDefinitionReference) }, + { entityReferenceType, true, typeof(IEdmEntityReferenceTypeReference) }, + { entityReferenceType, true, typeof(IEdmEntityReferenceTypeReference) }, + }; } + } - [Theory] - [MemberData(nameof(ToEdmTypeReferenceTestData))] - public void ToEdmTypeReference_InstantiatesRightEdmTypeReference(IEdmType edmType, bool isNullable, Type expectedType) - { - // Arrange & Act - IEdmTypeReference result = edmType.ToEdmTypeReference(isNullable); - - // Assert - IEdmCollectionTypeReference collection = result as IEdmCollectionTypeReference; - if (collection != null) - { - Assert.Equal(isNullable, collection.ElementType().IsNullable); - } - else - { - Assert.Equal(isNullable, result.IsNullable); - } - - Assert.Equal(edmType, result.Definition); - Assert.IsAssignableFrom(expectedType, result); - } + [Theory] + [MemberData(nameof(ToEdmTypeReferenceTestData))] + public void ToEdmTypeReference_InstantiatesRightEdmTypeReference(IEdmType edmType, bool isNullable, Type expectedType) + { + // Arrange & Act + IEdmTypeReference result = edmType.ToEdmTypeReference(isNullable); - [Fact] - public void ToEdmTypeReference_ThrowsNotSupportedException_UnknownTypeKind() + // Assert + IEdmCollectionTypeReference collection = result as IEdmCollectionTypeReference; + if (collection != null) { - // Arrange & Act - Mock mock = new Mock(); - mock.Setup(s => s.TypeKind).Returns(EdmTypeKind.None); - ExceptionAssert.Throws(() => mock.Object.ToEdmTypeReference(false), - "UnknownType is not a supported EDM type."); + Assert.Equal(isNullable, collection.ElementType().IsNullable); } - - [Fact] - public void ToEdmTypeReference_ThrowsArgumentNull_ForNullInput() + else { - // Arrange & Act - IEdmType edmType = null; - ExceptionAssert.ThrowsArgumentNull(() => edmType.ToEdmTypeReference(false), "edmType"); + Assert.Equal(isNullable, result.IsNullable); } - [Fact] - public void ToCollection_ThrowsArgumentNull_EdmType() - { - // Arrange & Act - IEdmType edmType = null; - ExceptionAssert.ThrowsArgumentNull(() => edmType.ToCollection(false), "edmType"); - } + Assert.Equal(edmType, result.Definition); + Assert.IsAssignableFrom(expectedType, result); + } - [Fact] - public void GetModelBoundQuerySettingsOrNull_ThrowsArgumentNull_Model() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.GetModelBoundQuerySettingsOrNull(null, null), "edmModel"); - } + [Fact] + public void ToEdmTypeReference_ThrowsNotSupportedException_UnknownTypeKind() + { + // Arrange & Act + Mock mock = new Mock(); + mock.Setup(s => s.TypeKind).Returns(EdmTypeKind.None); + ExceptionAssert.Throws(() => mock.Object.ToEdmTypeReference(false), + "UnknownType is not a supported EDM type."); + } - [Fact] - public void GetModelBoundQuerySettings_ThrowsArgumentNull_Model() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.GetModelBoundQuerySettings(null, null), "edmModel"); - } + [Fact] + public void ToEdmTypeReference_ThrowsArgumentNull_ForNullInput() + { + // Arrange & Act + IEdmType edmType = null; + ExceptionAssert.ThrowsArgumentNull(() => edmType.ToEdmTypeReference(false), "edmType"); + } + + [Fact] + public void ToCollection_ThrowsArgumentNull_EdmType() + { + // Arrange & Act + IEdmType edmType = null; + ExceptionAssert.ThrowsArgumentNull(() => edmType.ToCollection(false), "edmType"); + } + + [Fact] + public void GetModelBoundQuerySettingsOrNull_ThrowsArgumentNull_Model() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.GetModelBoundQuerySettingsOrNull(null, null), "edmModel"); + } + + [Fact] + public void GetModelBoundQuerySettings_ThrowsArgumentNull_Model() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.GetModelBoundQuerySettings(null, null), "edmModel"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelAnnotationExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelAnnotationExtensionsTests.cs index 7fec55a8a..0f10c1e1c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelAnnotationExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelAnnotationExtensionsTests.cs @@ -18,326 +18,325 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class EdmModelAnnotationExtensionsTests { - public class EdmModelAnnotationExtensionsTests + private static IEdmModel _model = GetEdmModel(); + + [Fact] + public void GetAcceptableMediaTypes_ThrowsArgumentNull_Model() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.GetAcceptableMediaTypes(null), "model"); + } + + [Fact] + public void GetAcceptableMediaTypes_ThrowsArgumentNull_Target() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => EdmCoreModel.Instance.GetAcceptableMediaTypes(null), "target"); + } + + [Fact] + public void GetAcceptableMediaTypes_Works_Target() + { + // Arrange + EdmModel model = new EdmModel(); + EdmEntityType entity = new EdmEntityType("NS", "entity"); + model.AddElement(entity); + + // Act + IList mediaTypes = model.GetAcceptableMediaTypes(entity); + + // Assert + Assert.Null(mediaTypes); + + // Act + EdmCollectionExpression collectionExp = new EdmCollectionExpression( + new EdmStringConstant("application/octet-stream"), + new EdmStringConstant("text/plain")); + + var annotation = new EdmVocabularyAnnotation(entity, CoreVocabularyModel.AcceptableMediaTypesTerm, collectionExp); + annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); + model.SetVocabularyAnnotation(annotation); + + // Act + mediaTypes = model.GetAcceptableMediaTypes(entity); + + // Assert + Assert.Collection(mediaTypes, + e => + { + Assert.Equal("application/octet-stream", e); + }, + e => + { + Assert.Equal("text/plain", e); + }); + } + + [Fact] + public void GetAlternateKeysTest_WorksForCoreAlternateKeys() + { + // Arrange + IEdmEntityType customer = _model.SchemaElements.OfType().First(c => c.Name == "Customer"); + + // 1) Act & Assert: Cannot find the property + IEnumerable> alternateKeys = _model.GetAlternateKeys(customer); + + // Assert + Assert.NotNull(alternateKeys); + IDictionary alternateKeyDict = Assert.Single(alternateKeys); + KeyValuePair alternateKey = Assert.Single(alternateKeyDict); + Assert.Equal("Title", alternateKey.Key); + Assert.Equal("Title", alternateKey.Value.Path); + } + + [Fact] + public void GetAlternateKeysTest_WorksForCommunityAlternateKeys() + { + // Arrange + IEdmEntityType customer = _model.SchemaElements.OfType().First(c => c.Name == "Company"); + + // Act + IDictionary[] alternateKeys = _model.GetAlternateKeys(customer).ToArray(); + + // Assert + Assert.NotNull(alternateKeys); + Assert.Equal(2, alternateKeys.Length); + + // 1) + IDictionary alternateKeyDict1 = alternateKeys[0]; + KeyValuePair alternateKey1 = Assert.Single(alternateKeyDict1); + Assert.Equal("Code", alternateKey1.Key); + Assert.Equal("Code", alternateKey1.Value.Path); + + // 2) + IDictionary alternateKeyDict2 = alternateKeys[1]; + Assert.Equal(2, alternateKeyDict2.Count); + KeyValuePair alternateKey2 = alternateKeyDict2.First(a => a.Key == "City"); + Assert.Equal("Location/City", alternateKey2.Value.Path); + + alternateKey2 = alternateKeyDict2.First(a => a.Key == "Street"); + Assert.Equal("Location/Street", alternateKey2.Value.Path); + } + + [Fact] + public void GetClrEnumMemberAnnotation_ThrowsArugmentNull_ForInputParameters() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.GetClrEnumMemberAnnotation(null), "edmModel"); + + model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => model.GetClrEnumMemberAnnotation(null), "enumType"); + } + + [Fact] + public void GetClrPropertyName_ThrowsArugmentNull_ForInputParameters() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.GetClrPropertyName(null), "edmModel"); + + model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => model.GetClrPropertyName(null), "edmProperty"); + } + + [Fact] + public void GetDynamicPropertyDictionary_ThrowsArugmentNull_ForInputParameters() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.GetDynamicPropertyDictionary(null), "edmModel"); + + model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => model.GetDynamicPropertyDictionary(null), "edmType"); + } + + [Fact] + public void GetModelName_ThrowsArugmentNull_Model() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.GetModelName(), "model"); + } + + [Fact] + public void SetModelName_ThrowsArugmentNull_Model() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.SetModelName(null), "model"); + + model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => model.SetModelName(null), "name"); + } + + [Fact] + public void GetModelName_CanCallSetModelName_UsingDefaultGuid() + { + // Arrange + IEdmModel model = new EdmModel(); + + // Act + string modelName = model.GetModelName(); + + // Assert + Assert.NotNull(modelName); + Assert.True(Guid.TryParse(modelName, out _)); + } + + [Fact] + public void GetAndSetModelName_RoundTrip() { - private static IEdmModel _model = GetEdmModel(); - - [Fact] - public void GetAcceptableMediaTypes_ThrowsArgumentNull_Model() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.GetAcceptableMediaTypes(null), "model"); - } - - [Fact] - public void GetAcceptableMediaTypes_ThrowsArgumentNull_Target() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => EdmCoreModel.Instance.GetAcceptableMediaTypes(null), "target"); - } - - [Fact] - public void GetAcceptableMediaTypes_Works_Target() - { - // Arrange - EdmModel model = new EdmModel(); - EdmEntityType entity = new EdmEntityType("NS", "entity"); - model.AddElement(entity); - - // Act - IList mediaTypes = model.GetAcceptableMediaTypes(entity); - - // Assert - Assert.Null(mediaTypes); - - // Act - EdmCollectionExpression collectionExp = new EdmCollectionExpression( - new EdmStringConstant("application/octet-stream"), - new EdmStringConstant("text/plain")); - - var annotation = new EdmVocabularyAnnotation(entity, CoreVocabularyModel.AcceptableMediaTypesTerm, collectionExp); - annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); - model.SetVocabularyAnnotation(annotation); - - // Act - mediaTypes = model.GetAcceptableMediaTypes(entity); - - // Assert - Assert.Collection(mediaTypes, - e => - { - Assert.Equal("application/octet-stream", e); - }, - e => - { - Assert.Equal("text/plain", e); - }); - } - - [Fact] - public void GetAlternateKeysTest_WorksForCoreAlternateKeys() - { - // Arrange - IEdmEntityType customer = _model.SchemaElements.OfType().First(c => c.Name == "Customer"); - - // 1) Act & Assert: Cannot find the property - IEnumerable> alternateKeys = _model.GetAlternateKeys(customer); - - // Assert - Assert.NotNull(alternateKeys); - IDictionary alternateKeyDict = Assert.Single(alternateKeys); - KeyValuePair alternateKey = Assert.Single(alternateKeyDict); - Assert.Equal("Title", alternateKey.Key); - Assert.Equal("Title", alternateKey.Value.Path); - } - - [Fact] - public void GetAlternateKeysTest_WorksForCommunityAlternateKeys() - { - // Arrange - IEdmEntityType customer = _model.SchemaElements.OfType().First(c => c.Name == "Company"); - - // Act - IDictionary[] alternateKeys = _model.GetAlternateKeys(customer).ToArray(); - - // Assert - Assert.NotNull(alternateKeys); - Assert.Equal(2, alternateKeys.Length); - - // 1) - IDictionary alternateKeyDict1 = alternateKeys[0]; - KeyValuePair alternateKey1 = Assert.Single(alternateKeyDict1); - Assert.Equal("Code", alternateKey1.Key); - Assert.Equal("Code", alternateKey1.Value.Path); - - // 2) - IDictionary alternateKeyDict2 = alternateKeys[1]; - Assert.Equal(2, alternateKeyDict2.Count); - KeyValuePair alternateKey2 = alternateKeyDict2.First(a => a.Key == "City"); - Assert.Equal("Location/City", alternateKey2.Value.Path); - - alternateKey2 = alternateKeyDict2.First(a => a.Key == "Street"); - Assert.Equal("Location/Street", alternateKey2.Value.Path); - } - - [Fact] - public void GetClrEnumMemberAnnotation_ThrowsArugmentNull_ForInputParameters() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.GetClrEnumMemberAnnotation(null), "edmModel"); - - model = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => model.GetClrEnumMemberAnnotation(null), "enumType"); - } - - [Fact] - public void GetClrPropertyName_ThrowsArugmentNull_ForInputParameters() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.GetClrPropertyName(null), "edmModel"); - - model = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => model.GetClrPropertyName(null), "edmProperty"); - } - - [Fact] - public void GetDynamicPropertyDictionary_ThrowsArugmentNull_ForInputParameters() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.GetDynamicPropertyDictionary(null), "edmModel"); - - model = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => model.GetDynamicPropertyDictionary(null), "edmType"); - } - - [Fact] - public void GetModelName_ThrowsArugmentNull_Model() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.GetModelName(), "model"); - } - - [Fact] - public void SetModelName_ThrowsArugmentNull_Model() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.SetModelName(null), "model"); - - model = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => model.SetModelName(null), "name"); - } - - [Fact] - public void GetModelName_CanCallSetModelName_UsingDefaultGuid() - { - // Arrange - IEdmModel model = new EdmModel(); - - // Act - string modelName = model.GetModelName(); - - // Assert - Assert.NotNull(modelName); - Assert.True(Guid.TryParse(modelName, out _)); - } - - [Fact] - public void GetAndSetModelName_RoundTrip() - { - // Arrange - string testName = "myName"; - IEdmModel model = new EdmModel(); - - // Act - model.SetModelName(testName); - string name = model.GetModelName(); - - // Assert - Assert.Equal(testName, name); - } - - [Fact] - public void GetTypeMapper_ReturnsDefaultTypeMapper_IfNullModelOrWithoutTypeMapper() - { - // Arrange & Act & Assert - IEdmModel model = null; - Assert.IsType(model.GetTypeMapper()); - - // Arrange & Act & Assert - model = EdmCoreModel.Instance; - Assert.IsType(model.GetTypeMapper()); - - // Arrange & Act & Assert - model = new EdmModel(); - Assert.IsType(model.GetTypeMapper()); - } - - [Fact] - public void SetTypeMapper_ThrowsArugmentNull_Model() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.SetTypeMapper(null), "model"); - - model = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => model.SetTypeMapper(null), "mapper"); - } - - [Fact] - public void GetAndSetTypeMapper_RoundTrip() - { - // Arrange - IODataTypeMapper mapper = new Mock().Object; - IEdmModel model = new EdmModel(); - - // Act - model.SetTypeMapper(mapper); - IODataTypeMapper actual = model.GetTypeMapper(); - - // Assert - Assert.Same(mapper, actual); - } - - [Fact] - public void GetAlternateKeys_ThrowsArugmentNull_ForInputParameters() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.GetAlternateKeys(null), "model"); - - model = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => model.GetAlternateKeys(null), "entityType"); - } - - private static IEdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); - - // complex type address - EdmComplexType address = new EdmComplexType("NS", "Address"); - address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); - address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); - model.AddElement(address); - - EdmComplexType vipAddress = new EdmComplexType("NS", "VipAddress"); - vipAddress.AddStructuralProperty("ZipCode", EdmPrimitiveTypeKind.String); - model.AddElement(vipAddress); - - EdmEntityType company = new EdmEntityType("NS", "Company"); - company.AddKeys(company.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - company.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Int32); - company.AddStructuralProperty("Location", new EdmComplexTypeReference(address, isNullable: true)); - model.AddElement(company); - AddComplexPropertyCommunityAlternateKey(model, company); - - // entity type 'Customer' with single alternate keys - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - customer.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); - model.AddElement(customer); - AddComplexPropertyCoreAlternateKey(model, customer); - return model; - } - - private static void AddComplexPropertyCommunityAlternateKey(EdmModel model, EdmEntityType entity) - { - // Alternate key 1 -> Code - List propertyRefs = new List(); - IEdmRecordExpression propertyRef = new EdmRecordExpression( - new EdmPropertyConstructor("Alias", new EdmStringConstant("Code")), - new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Code"))); - propertyRefs.Add(propertyRef); - - EdmRecordExpression alternateKey1 = new EdmRecordExpression(new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); - - // Alternate key 2 -> City & Street - propertyRefs = new List(); - propertyRef = new EdmRecordExpression( - new EdmPropertyConstructor("Alias", new EdmStringConstant("City")), - new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Location/City"))); - propertyRefs.Add(propertyRef); - - propertyRef = new EdmRecordExpression( - new EdmPropertyConstructor("Alias", new EdmStringConstant("Street")), - new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Location/Street"))); - propertyRefs.Add(propertyRef); - - EdmRecordExpression alternateKey2 = new EdmRecordExpression(new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); - - IEdmTerm coreAlternateTerm = AlternateKeysVocabularyModel.Instance.FindDeclaredTerm("OData.Community.Keys.V1.AlternateKeys"); - Assert.NotNull(coreAlternateTerm); - - var annotation = new EdmVocabularyAnnotation(entity, coreAlternateTerm, new EdmCollectionExpression(alternateKey1, alternateKey2)); - - annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); - model.SetVocabularyAnnotation(annotation); - } - - private static void AddComplexPropertyCoreAlternateKey(EdmModel model, EdmEntityType entity) - { - List propertyRefs = new List(); - IEdmRecordExpression propertyRef = new EdmRecordExpression( - new EdmPropertyConstructor("Alias", new EdmStringConstant("Title")), - new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Title"))); - propertyRefs.Add(propertyRef); - - EdmRecordExpression alternateKeyRecord = new EdmRecordExpression(new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); - - IEdmTerm coreAlternateTerm = CoreVocabularyModel.Instance.FindDeclaredTerm("Org.OData.Core.V1.AlternateKeys"); - Assert.NotNull(coreAlternateTerm); - - var annotation = new EdmVocabularyAnnotation(entity, coreAlternateTerm, new EdmCollectionExpression(alternateKeyRecord)); - - annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); - model.SetVocabularyAnnotation(annotation); - } + // Arrange + string testName = "myName"; + IEdmModel model = new EdmModel(); + + // Act + model.SetModelName(testName); + string name = model.GetModelName(); + + // Assert + Assert.Equal(testName, name); + } + + [Fact] + public void GetTypeMapper_ReturnsDefaultTypeMapper_IfNullModelOrWithoutTypeMapper() + { + // Arrange & Act & Assert + IEdmModel model = null; + Assert.IsType(model.GetTypeMapper()); + + // Arrange & Act & Assert + model = EdmCoreModel.Instance; + Assert.IsType(model.GetTypeMapper()); + + // Arrange & Act & Assert + model = new EdmModel(); + Assert.IsType(model.GetTypeMapper()); + } + + [Fact] + public void SetTypeMapper_ThrowsArugmentNull_Model() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.SetTypeMapper(null), "model"); + + model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => model.SetTypeMapper(null), "mapper"); + } + + [Fact] + public void GetAndSetTypeMapper_RoundTrip() + { + // Arrange + IODataTypeMapper mapper = new Mock().Object; + IEdmModel model = new EdmModel(); + + // Act + model.SetTypeMapper(mapper); + IODataTypeMapper actual = model.GetTypeMapper(); + + // Assert + Assert.Same(mapper, actual); + } + + [Fact] + public void GetAlternateKeys_ThrowsArugmentNull_ForInputParameters() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.GetAlternateKeys(null), "model"); + + model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => model.GetAlternateKeys(null), "entityType"); + } + + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + + // complex type address + EdmComplexType address = new EdmComplexType("NS", "Address"); + address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + model.AddElement(address); + + EdmComplexType vipAddress = new EdmComplexType("NS", "VipAddress"); + vipAddress.AddStructuralProperty("ZipCode", EdmPrimitiveTypeKind.String); + model.AddElement(vipAddress); + + EdmEntityType company = new EdmEntityType("NS", "Company"); + company.AddKeys(company.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + company.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Int32); + company.AddStructuralProperty("Location", new EdmComplexTypeReference(address, isNullable: true)); + model.AddElement(company); + AddComplexPropertyCommunityAlternateKey(model, company); + + // entity type 'Customer' with single alternate keys + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + customer.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); + model.AddElement(customer); + AddComplexPropertyCoreAlternateKey(model, customer); + return model; + } + + private static void AddComplexPropertyCommunityAlternateKey(EdmModel model, EdmEntityType entity) + { + // Alternate key 1 -> Code + List propertyRefs = new List(); + IEdmRecordExpression propertyRef = new EdmRecordExpression( + new EdmPropertyConstructor("Alias", new EdmStringConstant("Code")), + new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Code"))); + propertyRefs.Add(propertyRef); + + EdmRecordExpression alternateKey1 = new EdmRecordExpression(new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); + + // Alternate key 2 -> City & Street + propertyRefs = new List(); + propertyRef = new EdmRecordExpression( + new EdmPropertyConstructor("Alias", new EdmStringConstant("City")), + new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Location/City"))); + propertyRefs.Add(propertyRef); + + propertyRef = new EdmRecordExpression( + new EdmPropertyConstructor("Alias", new EdmStringConstant("Street")), + new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Location/Street"))); + propertyRefs.Add(propertyRef); + + EdmRecordExpression alternateKey2 = new EdmRecordExpression(new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); + + IEdmTerm coreAlternateTerm = AlternateKeysVocabularyModel.Instance.FindDeclaredTerm("OData.Community.Keys.V1.AlternateKeys"); + Assert.NotNull(coreAlternateTerm); + + var annotation = new EdmVocabularyAnnotation(entity, coreAlternateTerm, new EdmCollectionExpression(alternateKey1, alternateKey2)); + + annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); + model.SetVocabularyAnnotation(annotation); + } + + private static void AddComplexPropertyCoreAlternateKey(EdmModel model, EdmEntityType entity) + { + List propertyRefs = new List(); + IEdmRecordExpression propertyRef = new EdmRecordExpression( + new EdmPropertyConstructor("Alias", new EdmStringConstant("Title")), + new EdmPropertyConstructor("Name", new EdmPropertyPathExpression("Title"))); + propertyRefs.Add(propertyRef); + + EdmRecordExpression alternateKeyRecord = new EdmRecordExpression(new EdmPropertyConstructor("Key", new EdmCollectionExpression(propertyRefs))); + + IEdmTerm coreAlternateTerm = CoreVocabularyModel.Instance.FindDeclaredTerm("Org.OData.Core.V1.AlternateKeys"); + Assert.NotNull(coreAlternateTerm); + + var annotation = new EdmVocabularyAnnotation(entity, coreAlternateTerm, new EdmCollectionExpression(alternateKeyRecord)); + + annotation.SetSerializationLocation(model, EdmVocabularyAnnotationSerializationLocation.Inline); + model.SetVocabularyAnnotation(annotation); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelExtensionsTests.cs index a58e51ea3..b6fe0ce3a 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelExtensionsTests.cs @@ -15,241 +15,240 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class EdmModelExtensionsTests { - public class EdmModelExtensionsTests - { - private static IEdmModel _model = GetEdmModel(); + private static IEdmModel _model = GetEdmModel(); - [Fact] - public void ResolveAlternateKeyProperties_ThrowsArgumentNull() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.ResolveAlternateKeyProperties(null), "model"); + [Fact] + public void ResolveAlternateKeyProperties_ThrowsArgumentNull() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.ResolveAlternateKeyProperties(null), "model"); - // Arrange & Act & Assert - model = EdmCoreModel.Instance; - ExceptionAssert.ThrowsArgumentNull(() => model.ResolveAlternateKeyProperties(null), "keySegment"); - } + // Arrange & Act & Assert + model = EdmCoreModel.Instance; + ExceptionAssert.ThrowsArgumentNull(() => model.ResolveAlternateKeyProperties(null), "keySegment"); + } - [Fact] - public void ResolveProperty_ThrowsArgumentNull() - { - // Arrange & Act & Assert - IEdmStructuredType structuredType = null; - ExceptionAssert.ThrowsArgumentNull(() => structuredType.ResolveProperty(null), "structuredType"); - } + [Fact] + public void ResolveProperty_ThrowsArgumentNull() + { + // Arrange & Act & Assert + IEdmStructuredType structuredType = null; + ExceptionAssert.ThrowsArgumentNull(() => structuredType.ResolveProperty(null), "structuredType"); + } - [Fact] - public void ResolveResourceSetType_ThrowsArgumentNull() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.ResolveResourceSetType(null), "model"); + [Fact] + public void ResolveResourceSetType_ThrowsArgumentNull() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.ResolveResourceSetType(null), "model"); - // Arrange & Act & Assert - model = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => model.ResolveResourceSetType(null), "resourceSet"); - } + // Arrange & Act & Assert + model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => model.ResolveResourceSetType(null), "resourceSet"); + } - [Fact] - public void GetAllProperties_ThrowsArgumentNull() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.GetAllProperties(null), "model"); + [Fact] + public void GetAllProperties_ThrowsArgumentNull() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.GetAllProperties(null), "model"); - // Arrange & Act & Assert - model = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => model.GetAllProperties(null), "structuredType"); - } + // Arrange & Act & Assert + model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => model.GetAllProperties(null), "structuredType"); + } - [Fact] - public void ResolvePropertyTest_WorksForCaseSensitiveAndInsensitive() + [Fact] + public void ResolvePropertyTest_WorksForCaseSensitiveAndInsensitive() + { + // Arrange + EdmComplexType structuredType = new EdmComplexType("NS", "Complex"); + structuredType.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); + structuredType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + structuredType.AddStructuralProperty("nAme", EdmPrimitiveTypeKind.Int32); + structuredType.AddStructuralProperty("naMe", EdmPrimitiveTypeKind.Double); + + // 1) Act & Assert: Cannot find the property + IEdmProperty property = structuredType.ResolveProperty("Unknown"); + Assert.Null(property); + + // 2) Act & Assert : Can find one "Title" property + foreach (var name in new[] { "Title", "title", "tiTle", "TITLE" }) { - // Arrange - EdmComplexType structuredType = new EdmComplexType("NS", "Complex"); - structuredType.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); - structuredType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - structuredType.AddStructuralProperty("nAme", EdmPrimitiveTypeKind.Int32); - structuredType.AddStructuralProperty("naMe", EdmPrimitiveTypeKind.Double); - - // 1) Act & Assert: Cannot find the property - IEdmProperty property = structuredType.ResolveProperty("Unknown"); - Assert.Null(property); - - // 2) Act & Assert : Can find one "Title" property - foreach (var name in new[] { "Title", "title", "tiTle", "TITLE" }) - { - VerifyResolvedProperty(structuredType, name, "Title", "Edm.String"); - } - - // 3) Act & Assert: Can find the correct overload version - VerifyResolvedProperty(structuredType, "Name", "Name", "Edm.String"); - VerifyResolvedProperty(structuredType, "nAme", "nAme", "Edm.Int32"); - VerifyResolvedProperty(structuredType, "naMe", "naMe", "Edm.Double"); + VerifyResolvedProperty(structuredType, name, "Title", "Edm.String"); } - private static void VerifyResolvedProperty(IEdmStructuredType structuredType, string propertyName, string expectedName, string expectedTypeName) - { - IEdmProperty property = structuredType.ResolveProperty(propertyName); - Assert.NotNull(property); + // 3) Act & Assert: Can find the correct overload version + VerifyResolvedProperty(structuredType, "Name", "Name", "Edm.String"); + VerifyResolvedProperty(structuredType, "nAme", "nAme", "Edm.Int32"); + VerifyResolvedProperty(structuredType, "naMe", "naMe", "Edm.Double"); + } - Assert.Equal(expectedName, property.Name); - Assert.Equal(expectedTypeName, property.Type.FullName()); - } + private static void VerifyResolvedProperty(IEdmStructuredType structuredType, string propertyName, string expectedName, string expectedTypeName) + { + IEdmProperty property = structuredType.ResolveProperty(propertyName); + Assert.NotNull(property); - [Fact] - public void ResolvePropertyTest_ThrowsForAmbiguousPropertyName() - { - // Arrange - EdmComplexType structuredType = new EdmComplexType("NS", "Complex"); - structuredType.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); - structuredType.AddStructuralProperty("tiTle", EdmPrimitiveTypeKind.Int32); - structuredType.AddStructuralProperty("tiTlE", EdmPrimitiveTypeKind.Double); - - // Act & Assert - Positive case - IEdmProperty edmProperty = structuredType.ResolveProperty("tiTlE"); - Assert.NotNull(edmProperty); - Assert.Equal("Edm.Double", edmProperty.Type.FullName()); - - // Act & Assert - Negative case - Action test = () => structuredType.ResolveProperty("title"); - ExceptionAssert.Throws(test, "Ambiguous property name 'title' found. Please use correct property name case."); - } + Assert.Equal(expectedName, property.Name); + Assert.Equal(expectedTypeName, property.Type.FullName()); + } - [Fact] - public void FindProperty_ThrowsArugmentNull_ForInputParameters() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.FindProperty(null, null), "model"); + [Fact] + public void ResolvePropertyTest_ThrowsForAmbiguousPropertyName() + { + // Arrange + EdmComplexType structuredType = new EdmComplexType("NS", "Complex"); + structuredType.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); + structuredType.AddStructuralProperty("tiTle", EdmPrimitiveTypeKind.Int32); + structuredType.AddStructuralProperty("tiTlE", EdmPrimitiveTypeKind.Double); + + // Act & Assert - Positive case + IEdmProperty edmProperty = structuredType.ResolveProperty("tiTlE"); + Assert.NotNull(edmProperty); + Assert.Equal("Edm.Double", edmProperty.Type.FullName()); + + // Act & Assert - Negative case + Action test = () => structuredType.ResolveProperty("title"); + ExceptionAssert.Throws(test, "Ambiguous property name 'title' found. Please use correct property name case."); + } + + [Fact] + public void FindProperty_ThrowsArugmentNull_ForInputParameters() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.FindProperty(null, null), "model"); - model = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => model.FindProperty(null, null), "structuredType"); + model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => model.FindProperty(null, null), "structuredType"); - IEdmStructuredType structuredType = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => model.FindProperty(structuredType, null), "path"); - } + IEdmStructuredType structuredType = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => model.FindProperty(structuredType, null), "path"); + } - [Theory] - [InlineData("Code", "Code")] - [InlineData("Location", "Location")] - [InlineData("Location/Street", "Street")] - [InlineData("Location/City", "City")] - [InlineData("Location/NS.VipAddress/ZipCode", "ZipCode")] - public void FindPropertyTest_WorksForDirectPropertyOnType(string pathStr, string name) - { - // Arrange - EdmPropertyPathExpression path = new EdmPropertyPathExpression(pathStr); - IEdmEntityType company = _model.SchemaElements.OfType().First(c => c.Name == "Company"); - Assert.NotNull(company); + [Theory] + [InlineData("Code", "Code")] + [InlineData("Location", "Location")] + [InlineData("Location/Street", "Street")] + [InlineData("Location/City", "City")] + [InlineData("Location/NS.VipAddress/ZipCode", "ZipCode")] + public void FindPropertyTest_WorksForDirectPropertyOnType(string pathStr, string name) + { + // Arrange + EdmPropertyPathExpression path = new EdmPropertyPathExpression(pathStr); + IEdmEntityType company = _model.SchemaElements.OfType().First(c => c.Name == "Company"); + Assert.NotNull(company); - // Act - IEdmProperty property = _model.FindProperty(company, path); + // Act + IEdmProperty property = _model.FindProperty(company, path); - // Assert - Assert.NotNull(property); - Assert.Equal(name, property.Name); - } + // Assert + Assert.NotNull(property); + Assert.Equal(name, property.Name); + } - [Fact] - public void FindPropertyTest_ThrowsForPropertyNotFoundOnPathExpression() - { - // Arrange - EdmPropertyPathExpression path = new EdmPropertyPathExpression("OtherPath"); - IEdmEntityType company = _model.SchemaElements.OfType().First(c => c.Name == "Company"); - Assert.NotNull(company); - - // Act & Assert - Action test = () => _model.FindProperty(company, path); - ExceptionAssert.Throws(test, "Can not resolve the property using property path 'OtherPath' from type 'NS.Company'."); - } + [Fact] + public void FindPropertyTest_ThrowsForPropertyNotFoundOnPathExpression() + { + // Arrange + EdmPropertyPathExpression path = new EdmPropertyPathExpression("OtherPath"); + IEdmEntityType company = _model.SchemaElements.OfType().First(c => c.Name == "Company"); + Assert.NotNull(company); + + // Act & Assert + Action test = () => _model.FindProperty(company, path); + ExceptionAssert.Throws(test, "Can not resolve the property using property path 'OtherPath' from type 'NS.Company'."); + } - [Fact] - public void FindPropertyTest_ThrowsForResourceTypeNotInModel() - { - // Arrange - EdmPropertyPathExpression path = new EdmPropertyPathExpression("Location/NS.AnotherType"); - IEdmEntityType company = _model.SchemaElements.OfType().First(c => c.Name == "Company"); - Assert.NotNull(company); - - // Act & Assert - Action test = () => _model.FindProperty(company, path); - ExceptionAssert.Throws(test, "Cannot find the resource type 'NS.AnotherType' in the model."); - } + [Fact] + public void FindPropertyTest_ThrowsForResourceTypeNotInModel() + { + // Arrange + EdmPropertyPathExpression path = new EdmPropertyPathExpression("Location/NS.AnotherType"); + IEdmEntityType company = _model.SchemaElements.OfType().First(c => c.Name == "Company"); + Assert.NotNull(company); + + // Act & Assert + Action test = () => _model.FindProperty(company, path); + ExceptionAssert.Throws(test, "Cannot find the resource type 'NS.AnotherType' in the model."); + } - [Fact] - public void ResolveNavigationSource_ThrowsArugmentNull() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.ResolveNavigationSource(null), "model"); - } + [Fact] + public void ResolveNavigationSource_ThrowsArugmentNull() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.ResolveNavigationSource(null), "model"); + } - [Fact] - public void ResolveNavigationSource_ThrowsODataException_AmbiguousIdentifier() - { - // Arrange - EdmModel model = new EdmModel(); - EdmEntityType entityType = new EdmEntityType("NS", "Entity"); - EdmEntityContainer containter = new EdmEntityContainer("NS", "Default"); - model.AddElement(entityType); - model.AddElement(containter); - containter.AddEntitySet("entities", entityType); - containter.AddEntitySet("enTIties", entityType); - - // Act & Assert - Assert.NotNull(model.ResolveNavigationSource("enTIties")); - Assert.NotNull(model.ResolveNavigationSource("enTIties", true)); - - // Act & Assert - Assert.Null(model.ResolveNavigationSource("Entities")); - - Action test = () => model.ResolveNavigationSource("Entities", true); - ExceptionAssert.Throws(test, - "Ambiguous navigation source (entity set or singleton) name 'Entities' found. Please use correct navigation source name case."); - } + [Fact] + public void ResolveNavigationSource_ThrowsODataException_AmbiguousIdentifier() + { + // Arrange + EdmModel model = new EdmModel(); + EdmEntityType entityType = new EdmEntityType("NS", "Entity"); + EdmEntityContainer containter = new EdmEntityContainer("NS", "Default"); + model.AddElement(entityType); + model.AddElement(containter); + containter.AddEntitySet("entities", entityType); + containter.AddEntitySet("enTIties", entityType); + + // Act & Assert + Assert.NotNull(model.ResolveNavigationSource("enTIties")); + Assert.NotNull(model.ResolveNavigationSource("enTIties", true)); + + // Act & Assert + Assert.Null(model.ResolveNavigationSource("Entities")); + + Action test = () => model.ResolveNavigationSource("Entities", true); + ExceptionAssert.Throws(test, + "Ambiguous navigation source (entity set or singleton) name 'Entities' found. Please use correct navigation source name case."); + } - [Fact] - public void IsEnumOrCollectionEnum_Works_EdmType() - { - // Arrange & Act & Assert - IEdmTypeReference typeReference = EdmCoreModel.Instance.GetString(false); - Assert.False(typeReference.IsEnumOrCollectionEnum()); - - // Arrange & Act & Assert - EdmEnumType enumType = new EdmEnumType("NS", "Enum"); - typeReference = new EdmEnumTypeReference(enumType, true); - Assert.True(typeReference.IsEnumOrCollectionEnum()); - - // Arrange & Act & Assert - typeReference = new EdmCollectionTypeReference(new EdmCollectionType(new EdmEnumTypeReference(enumType, true))); - Assert.True(typeReference.IsEnumOrCollectionEnum()); - } + [Fact] + public void IsEnumOrCollectionEnum_Works_EdmType() + { + // Arrange & Act & Assert + IEdmTypeReference typeReference = EdmCoreModel.Instance.GetString(false); + Assert.False(typeReference.IsEnumOrCollectionEnum()); + + // Arrange & Act & Assert + EdmEnumType enumType = new EdmEnumType("NS", "Enum"); + typeReference = new EdmEnumTypeReference(enumType, true); + Assert.True(typeReference.IsEnumOrCollectionEnum()); + + // Arrange & Act & Assert + typeReference = new EdmCollectionTypeReference(new EdmCollectionType(new EdmEnumTypeReference(enumType, true))); + Assert.True(typeReference.IsEnumOrCollectionEnum()); + } - private static IEdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); - - // complex type address - EdmComplexType address = new EdmComplexType("NS", "Address"); - address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); - address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); - model.AddElement(address); - - EdmComplexType vipAddress = new EdmComplexType("NS", "VipAddress"); - vipAddress.AddStructuralProperty("ZipCode", EdmPrimitiveTypeKind.String); - model.AddElement(vipAddress); - - EdmEntityType company = new EdmEntityType("NS", "Company"); - company.AddKeys(company.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - company.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Int32); - company.AddStructuralProperty("Location", new EdmComplexTypeReference(address, isNullable: true)); - model.AddElement(company); - return model; - } + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + + // complex type address + EdmComplexType address = new EdmComplexType("NS", "Address"); + address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + model.AddElement(address); + + EdmComplexType vipAddress = new EdmComplexType("NS", "VipAddress"); + vipAddress.AddStructuralProperty("ZipCode", EdmPrimitiveTypeKind.String); + model.AddElement(vipAddress); + + EdmEntityType company = new EdmEntityType("NS", "Company"); + company.AddKeys(company.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + company.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Int32); + company.AddStructuralProperty("Location", new EdmComplexTypeReference(address, isNullable: true)); + model.AddElement(company); + return model; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelLinkBuilderExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelLinkBuilderExtensionsTests.cs index fb56f86ec..cf0c4d58d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelLinkBuilderExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmModelLinkBuilderExtensionsTests.cs @@ -13,209 +13,208 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class EdmModelLinkBuilderExtensionsTests { - public class EdmModelLinkBuilderExtensionsTests + private static IEdmModel Model; + private static IEdmEntitySet Customers; + private static IEdmNavigationProperty OrdersNavigationProperty; + + static EdmModelLinkBuilderExtensionsTests() + { + EdmModel model = new EdmModel(); + + // Order + EdmEntityType order = new EdmEntityType("NS", "Order", null, false, true); + model.AddElement(order); + + // Customer + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + model.AddElement(customer); + + OrdersNavigationProperty = customer.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "Orders", + TargetMultiplicity = EdmMultiplicity.Many, + Target = order + }); + + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + model.AddElement(container); + Customers = container.AddEntitySet("Customers", customer); + + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + EdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + function.AddParameter("entity", new EdmEntityTypeReference(customer, true)); + model.AddElement(function); + + Model = model; + } + + [Fact] + public void HasIdLink_SetsIdLinkBuilder() + { + // Arrange + EdmModel model = new EdmModel(); + SelfLinkBuilder linkBuilder = new SelfLinkBuilder((ResourceContext a) => new Uri("http://id"), followsConventions: false); + + // Act + model.HasIdLink(Customers, linkBuilder); + + // Assert + SelfLinkBuilder actualLinkBuilder = model.GetNavigationSourceLinkBuilder(Customers).IdLinkBuilder; + Assert.Same(linkBuilder, actualLinkBuilder); + } + + [Fact] + public void HasEditLink_SetsIdLinkBuilder() + { + // Arrange + EdmModel model = new EdmModel(); + + SelfLinkBuilder linkBuilder = new SelfLinkBuilder((ResourceContext a) => new Uri("http://id"), followsConventions: false); + + // Act + model.HasEditLink(Customers, linkBuilder); + + // Assert + SelfLinkBuilder actualLinkBuilder = model.GetNavigationSourceLinkBuilder(Customers).EditLinkBuilder; + Assert.Same(linkBuilder, actualLinkBuilder); + } + + [Fact] + public void HasReadLink_SetsIdLinkBuilder() + { + // Arrange + EdmModel model = new EdmModel(); + + SelfLinkBuilder linkBuilder = new SelfLinkBuilder((ResourceContext a) => new Uri("http://id"), followsConventions: false); + + // Act + model.HasReadLink(Customers, linkBuilder); + + // Assert + SelfLinkBuilder actualLinkBuilder = model.GetNavigationSourceLinkBuilder(Customers).ReadLinkBuilder; + Assert.Same(linkBuilder, actualLinkBuilder); + } + + [Fact] + public void HasNavigationPropertyLink_SetsIdLinkBuilder() + { + // Arrange + EdmModel model = new EdmModel(); + + NavigationLinkBuilder linkBuilder = new NavigationLinkBuilder( + (ResourceContext a, IEdmNavigationProperty b) => new Uri("http://orders"), followsConventions: false); + + // Act + model.HasNavigationPropertyLink(Customers, OrdersNavigationProperty, linkBuilder); + + // Assert + NavigationSourceLinkBuilderAnnotation annotation = model.GetNavigationSourceLinkBuilder(Customers); + Uri uri = annotation.BuildNavigationLink(new ResourceContext(), OrdersNavigationProperty); + Assert.Equal(uri, new Uri("http://orders")); + } + + [Fact] + public void SetOperationLinkBuilder_SetsOperationLinkBuilder() + { + // Arrange + IEdmModel model = new EdmModel(); + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("http://localhost"), followsConventions: false); + + // Act + model.SetOperationLinkBuilder(function, linkBuilder); + + // Assert + OperationLinkBuilder actualLinkBuilder = model.GetAnnotationValue(function); + + Assert.Same(linkBuilder, actualLinkBuilder); + Assert.Equal(new Uri("http://localhost"), actualLinkBuilder.BuildLink((ResourceContext)null)); + } + + [Fact] + public void SetOperationLinkBuilder_ResetOperationLinkBuilder() + { + // Arrange + IEdmModel model = new EdmModel(); + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + + // Act + OperationLinkBuilder linkBuilder1 = new OperationLinkBuilder((ResourceContext a) => new Uri("http://localhost1"), followsConventions: false); + model.SetOperationLinkBuilder(function, linkBuilder1); + + OperationLinkBuilder linkBuilder2 = new OperationLinkBuilder((ResourceContext a) => new Uri("http://localhost2"), followsConventions: false); + model.SetOperationLinkBuilder(function, linkBuilder2); + + // Assert + OperationLinkBuilder actualLinkBuilder = model.GetAnnotationValue(function); + + Assert.Same(linkBuilder2, actualLinkBuilder); + Assert.Equal(new Uri("http://localhost2"), actualLinkBuilder.BuildLink((ResourceContext)null)); + } + + [Fact] + public void SetOperationLinkBuilder_ResetOperationLinkBuilder_AfterCallGetOperationLinkBuilder() + { + // Arrange + IEdmModel model = new EdmModel(); + IEdmEntityType entity = new EdmEntityType("NS", "entity"); + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + EdmFunction function = new EdmFunction("NS", "Function", returnType); + function.AddParameter("entity", new EdmEntityTypeReference(entity, true)); + + // Act + OperationLinkBuilder linkBuilder = model.GetOperationLinkBuilder(function); + Assert.NotNull(linkBuilder); + + OperationLinkBuilder linkBuilder2 = new OperationLinkBuilder((ResourceContext a) => new Uri("http://localhost2"), followsConventions: false); + model.SetOperationLinkBuilder(function, linkBuilder2); + + // Assert + OperationLinkBuilder actualLinkBuilder = model.GetAnnotationValue(function); + + Assert.Same(linkBuilder2, actualLinkBuilder); + Assert.Equal(new Uri("http://localhost2"), actualLinkBuilder.BuildLink((ResourceContext)null)); + } + + [Fact] + public void GetNavigationSourceLinkBuilder_ThrowsArgumentNull_Model() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.GetNavigationSourceLinkBuilder(null), "model"); + } + + [Fact] + public void SetNavigationSourceLinkBuilder_ThrowsArgumentNull_Model() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.SetNavigationSourceLinkBuilder(null, null), "model"); + } + + [Fact] + public void GetOperationLinkBuilder_ThrowsArgumentNull_ForInputs() + { + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.GetOperationLinkBuilder(null), "model"); + model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => model.GetOperationLinkBuilder(null), "operation"); + } + + [Fact] + public void SetOperationLinkBuilder_ThrowsArgumentNull_Model() { - private static IEdmModel Model; - private static IEdmEntitySet Customers; - private static IEdmNavigationProperty OrdersNavigationProperty; - - static EdmModelLinkBuilderExtensionsTests() - { - EdmModel model = new EdmModel(); - - // Order - EdmEntityType order = new EdmEntityType("NS", "Order", null, false, true); - model.AddElement(order); - - // Customer - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - model.AddElement(customer); - - OrdersNavigationProperty = customer.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "Orders", - TargetMultiplicity = EdmMultiplicity.Many, - Target = order - }); - - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - model.AddElement(container); - Customers = container.AddEntitySet("Customers", customer); - - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - EdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); - function.AddParameter("entity", new EdmEntityTypeReference(customer, true)); - model.AddElement(function); - - Model = model; - } - - [Fact] - public void HasIdLink_SetsIdLinkBuilder() - { - // Arrange - EdmModel model = new EdmModel(); - SelfLinkBuilder linkBuilder = new SelfLinkBuilder((ResourceContext a) => new Uri("http://id"), followsConventions: false); - - // Act - model.HasIdLink(Customers, linkBuilder); - - // Assert - SelfLinkBuilder actualLinkBuilder = model.GetNavigationSourceLinkBuilder(Customers).IdLinkBuilder; - Assert.Same(linkBuilder, actualLinkBuilder); - } - - [Fact] - public void HasEditLink_SetsIdLinkBuilder() - { - // Arrange - EdmModel model = new EdmModel(); - - SelfLinkBuilder linkBuilder = new SelfLinkBuilder((ResourceContext a) => new Uri("http://id"), followsConventions: false); - - // Act - model.HasEditLink(Customers, linkBuilder); - - // Assert - SelfLinkBuilder actualLinkBuilder = model.GetNavigationSourceLinkBuilder(Customers).EditLinkBuilder; - Assert.Same(linkBuilder, actualLinkBuilder); - } - - [Fact] - public void HasReadLink_SetsIdLinkBuilder() - { - // Arrange - EdmModel model = new EdmModel(); - - SelfLinkBuilder linkBuilder = new SelfLinkBuilder((ResourceContext a) => new Uri("http://id"), followsConventions: false); - - // Act - model.HasReadLink(Customers, linkBuilder); - - // Assert - SelfLinkBuilder actualLinkBuilder = model.GetNavigationSourceLinkBuilder(Customers).ReadLinkBuilder; - Assert.Same(linkBuilder, actualLinkBuilder); - } - - [Fact] - public void HasNavigationPropertyLink_SetsIdLinkBuilder() - { - // Arrange - EdmModel model = new EdmModel(); - - NavigationLinkBuilder linkBuilder = new NavigationLinkBuilder( - (ResourceContext a, IEdmNavigationProperty b) => new Uri("http://orders"), followsConventions: false); - - // Act - model.HasNavigationPropertyLink(Customers, OrdersNavigationProperty, linkBuilder); - - // Assert - NavigationSourceLinkBuilderAnnotation annotation = model.GetNavigationSourceLinkBuilder(Customers); - Uri uri = annotation.BuildNavigationLink(new ResourceContext(), OrdersNavigationProperty); - Assert.Equal(uri, new Uri("http://orders")); - } - - [Fact] - public void SetOperationLinkBuilder_SetsOperationLinkBuilder() - { - // Arrange - IEdmModel model = new EdmModel(); - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); - - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("http://localhost"), followsConventions: false); - - // Act - model.SetOperationLinkBuilder(function, linkBuilder); - - // Assert - OperationLinkBuilder actualLinkBuilder = model.GetAnnotationValue(function); - - Assert.Same(linkBuilder, actualLinkBuilder); - Assert.Equal(new Uri("http://localhost"), actualLinkBuilder.BuildLink((ResourceContext)null)); - } - - [Fact] - public void SetOperationLinkBuilder_ResetOperationLinkBuilder() - { - // Arrange - IEdmModel model = new EdmModel(); - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); - - // Act - OperationLinkBuilder linkBuilder1 = new OperationLinkBuilder((ResourceContext a) => new Uri("http://localhost1"), followsConventions: false); - model.SetOperationLinkBuilder(function, linkBuilder1); - - OperationLinkBuilder linkBuilder2 = new OperationLinkBuilder((ResourceContext a) => new Uri("http://localhost2"), followsConventions: false); - model.SetOperationLinkBuilder(function, linkBuilder2); - - // Assert - OperationLinkBuilder actualLinkBuilder = model.GetAnnotationValue(function); - - Assert.Same(linkBuilder2, actualLinkBuilder); - Assert.Equal(new Uri("http://localhost2"), actualLinkBuilder.BuildLink((ResourceContext)null)); - } - - [Fact] - public void SetOperationLinkBuilder_ResetOperationLinkBuilder_AfterCallGetOperationLinkBuilder() - { - // Arrange - IEdmModel model = new EdmModel(); - IEdmEntityType entity = new EdmEntityType("NS", "entity"); - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - EdmFunction function = new EdmFunction("NS", "Function", returnType); - function.AddParameter("entity", new EdmEntityTypeReference(entity, true)); - - // Act - OperationLinkBuilder linkBuilder = model.GetOperationLinkBuilder(function); - Assert.NotNull(linkBuilder); - - OperationLinkBuilder linkBuilder2 = new OperationLinkBuilder((ResourceContext a) => new Uri("http://localhost2"), followsConventions: false); - model.SetOperationLinkBuilder(function, linkBuilder2); - - // Assert - OperationLinkBuilder actualLinkBuilder = model.GetAnnotationValue(function); - - Assert.Same(linkBuilder2, actualLinkBuilder); - Assert.Equal(new Uri("http://localhost2"), actualLinkBuilder.BuildLink((ResourceContext)null)); - } - - [Fact] - public void GetNavigationSourceLinkBuilder_ThrowsArgumentNull_Model() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.GetNavigationSourceLinkBuilder(null), "model"); - } - - [Fact] - public void SetNavigationSourceLinkBuilder_ThrowsArgumentNull_Model() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.SetNavigationSourceLinkBuilder(null, null), "model"); - } - - [Fact] - public void GetOperationLinkBuilder_ThrowsArgumentNull_ForInputs() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.GetOperationLinkBuilder(null), "model"); - model = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => model.GetOperationLinkBuilder(null), "operation"); - } - - [Fact] - public void SetOperationLinkBuilder_ThrowsArgumentNull_Model() - { - // Arrange & Act & Assert - IEdmModel model = null; - ExceptionAssert.ThrowsArgumentNull(() => model.SetOperationLinkBuilder(null, null), "model"); - } + // Arrange & Act & Assert + IEdmModel model = null; + ExceptionAssert.ThrowsArgumentNull(() => model.SetOperationLinkBuilder(null, null), "model"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmPrimitiveHelperTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmPrimitiveHelperTests.cs index e894122cd..a3f6c63d6 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmPrimitiveHelperTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmPrimitiveHelperTests.cs @@ -13,138 +13,137 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class EdmPrimitiveHelperTests { - public class EdmPrimitiveHelperTests + private enum TestEnum { - private enum TestEnum - { - Zero = 0, - One = 1, - Two = 2, - Three = 3 - }; + Zero = 0, + One = 1, + Two = 2, + Three = 3 + }; - private struct TestStruct - { - public int X { get; set; } - } + private struct TestStruct + { + public int X { get; set; } + } - public static TheoryDataSet ConvertPrimitiveValue_NonStandardPrimitives_Data - => new TheoryDataSet - { - { 1, 1, typeof(int) }, - { "1", (char)'1', typeof(char) }, - { "1", (char?)'1', typeof(char?) }, - { "123", (char[]) new char[] {'1', '2', '3' }, typeof(char[]) }, - { (int)1 , (ushort)1, typeof(ushort)}, - { (int?)1, (ushort?)1, typeof(ushort?) }, - { (long)1, (uint)1, typeof(uint) }, - { (long?)1, (uint?)1, typeof(uint?) }, - { (long)1 , (ulong)1, typeof(ulong) }, - { (long?)1 ,(ulong?)1, typeof(ulong?) }, - //(Stream) new MemoryStream(new byte[] { 1 }), // TODO: Enable once we have support for streams - { "" ,(XElement)new XElement(XName.Get("element","namespace")), typeof(XElement)} - }; + public static TheoryDataSet ConvertPrimitiveValue_NonStandardPrimitives_Data + => new TheoryDataSet + { + { 1, 1, typeof(int) }, + { "1", (char)'1', typeof(char) }, + { "1", (char?)'1', typeof(char?) }, + { "123", (char[]) new char[] {'1', '2', '3' }, typeof(char[]) }, + { (int)1 , (ushort)1, typeof(ushort)}, + { (int?)1, (ushort?)1, typeof(ushort?) }, + { (long)1, (uint)1, typeof(uint) }, + { (long?)1, (uint?)1, typeof(uint?) }, + { (long)1 , (ulong)1, typeof(ulong) }, + { (long?)1 ,(ulong?)1, typeof(ulong?) }, + //(Stream) new MemoryStream(new byte[] { 1 }), // TODO: Enable once we have support for streams + { "" ,(XElement)new XElement(XName.Get("element","namespace")), typeof(XElement)} + }; - public static TheoryDataSet ConvertPrimitiveValue_NonStandardPrimitives_ExtraData - => new TheoryDataSet - { - { "", (char?)null, typeof(char?) }, - { "Two", TestEnum.Two, typeof(TestEnum) }, - { "True", true, typeof(bool) }, - { "true", true, typeof(bool) }, - { " True ", true, typeof(bool) }, - { "False", false, typeof(bool) }, - { "false", false, typeof(bool) }, - { " False ", false, typeof(bool) }, - { TestEnum.Two, (double)2.0, typeof(double) } - }; + public static TheoryDataSet ConvertPrimitiveValue_NonStandardPrimitives_ExtraData + => new TheoryDataSet + { + { "", (char?)null, typeof(char?) }, + { "Two", TestEnum.Two, typeof(TestEnum) }, + { "True", true, typeof(bool) }, + { "true", true, typeof(bool) }, + { " True ", true, typeof(bool) }, + { "False", false, typeof(bool) }, + { "false", false, typeof(bool) }, + { " False ", false, typeof(bool) }, + { TestEnum.Two, (double)2.0, typeof(double) } + }; - public static TheoryDataSet ConvertThrow_NonStandardPrimitives_Data - => new TheoryDataSet - { - { 9, typeof(char), "The value must be a string with a length of 1." }, - { "", typeof(char), "The value must be a string with a length of 1." }, - { "123", typeof(char), "The value must be a string with a length of 1." }, - { 9, typeof(char?), "The value must be a string with a maximum length of 1." }, - { "123", typeof(char?), "The value must be a string with a maximum length of 1." }, - { 123, typeof(XElement), "The value must be a string." }, - { 9, typeof(char[]), "The value must be a string." }, - { 9, typeof(XElement), "The value must be a string." }, - { 9, typeof(TestEnum), "The value must be a string." }, - { 9, typeof(DateTime), "The value must be a DateTimeOffset or Date." }, - { 9, typeof(TimeSpan), "The value must be a Edm.TimeOfDay." }, - { "", typeof(bool), "The value must be a boolean." }, - { "0", typeof(bool), "The value must be a boolean." }, - { 0, typeof(bool), "The value must be a boolean." }, - { 1024, typeof(byte), "The value has a value that is out of range of type System.Byte." }, - { "data", typeof(long), "The value has a format that is not recognized by type System.Int64." }, - { "data", typeof(TestStruct), "The value cannot be converted to type Microsoft.AspNetCore.OData.Tests.Edm.EdmPrimitiveHelperTests+TestStruct." }, - { new TestStruct(), typeof(int), "The value cannot be converted to type System.Int32." } - }; + public static TheoryDataSet ConvertThrow_NonStandardPrimitives_Data + => new TheoryDataSet + { + { 9, typeof(char), "The value must be a string with a length of 1." }, + { "", typeof(char), "The value must be a string with a length of 1." }, + { "123", typeof(char), "The value must be a string with a length of 1." }, + { 9, typeof(char?), "The value must be a string with a maximum length of 1." }, + { "123", typeof(char?), "The value must be a string with a maximum length of 1." }, + { 123, typeof(XElement), "The value must be a string." }, + { 9, typeof(char[]), "The value must be a string." }, + { 9, typeof(XElement), "The value must be a string." }, + { 9, typeof(TestEnum), "The value must be a string." }, + { 9, typeof(DateTime), "The value must be a DateTimeOffset or Date." }, + { 9, typeof(TimeSpan), "The value must be a Edm.TimeOfDay." }, + { "", typeof(bool), "The value must be a boolean." }, + { "0", typeof(bool), "The value must be a boolean." }, + { 0, typeof(bool), "The value must be a boolean." }, + { 1024, typeof(byte), "The value has a value that is out of range of type System.Byte." }, + { "data", typeof(long), "The value has a format that is not recognized by type System.Int64." }, + { "data", typeof(TestStruct), "The value cannot be converted to type Microsoft.AspNetCore.OData.Tests.Edm.EdmPrimitiveHelperTests+TestStruct." }, + { new TestStruct(), typeof(int), "The value cannot be converted to type System.Int32." } + }; - public static TheoryDataSet ConvertDateTime_NonStandardPrimitives_Data - => new TheoryDataSet - { - DateTimeOffset.Parse("2014-12-12T01:02:03Z"), - DateTimeOffset.Parse("2014-12-12T01:02:03-8:00"), - DateTimeOffset.Parse("2014-12-12T01:02:03+8:00"), - }; + public static TheoryDataSet ConvertDateTime_NonStandardPrimitives_Data + => new TheoryDataSet + { + DateTimeOffset.Parse("2014-12-12T01:02:03Z"), + DateTimeOffset.Parse("2014-12-12T01:02:03-8:00"), + DateTimeOffset.Parse("2014-12-12T01:02:03+8:00"), + }; - [Theory] - [MemberData(nameof(ConvertPrimitiveValue_NonStandardPrimitives_Data))] - [MemberData(nameof(ConvertPrimitiveValue_NonStandardPrimitives_ExtraData))] - public void ConvertPrimitiveValue_NonStandardPrimitives(object valueToConvert, object result, Type conversionType) - { - // Arrange & Act - object actual = EdmPrimitiveHelper.ConvertPrimitiveValue(valueToConvert, conversionType); + [Theory] + [MemberData(nameof(ConvertPrimitiveValue_NonStandardPrimitives_Data))] + [MemberData(nameof(ConvertPrimitiveValue_NonStandardPrimitives_ExtraData))] + public void ConvertPrimitiveValue_NonStandardPrimitives(object valueToConvert, object result, Type conversionType) + { + // Arrange & Act + object actual = EdmPrimitiveHelper.ConvertPrimitiveValue(valueToConvert, conversionType); - // Assert - if (result == null) - { - Assert.Equal(result, actual); - } - else - { - Assert.Equal(result.GetType(), actual.GetType()); - Assert.Equal(result.ToString(), actual.ToString()); - } + // Assert + if (result == null) + { + Assert.Equal(result, actual); } - - [Theory] - [MemberData(nameof(ConvertDateTime_NonStandardPrimitives_Data))] - public void ConvertDateTimeValue_NonStandardPrimitives_DefaultTimeZoneInfo(DateTimeOffset valueToConvert) + else { - // Arrange & Act - object actual = EdmPrimitiveHelper.ConvertPrimitiveValue(valueToConvert, typeof(DateTime)); - - // Assert - DateTime dt = Assert.IsType(actual); - Assert.Equal(valueToConvert.LocalDateTime, dt); + Assert.Equal(result.GetType(), actual.GetType()); + Assert.Equal(result.ToString(), actual.ToString()); } + } - [Theory] - [MemberData(nameof(ConvertDateTime_NonStandardPrimitives_Data))] - public void ConvertDateTimeValue_NonStandardPrimitives_CustomTimeZoneInfo(DateTimeOffset valueToConvert) - { - // Arrange & Act - var timeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); - object actual = EdmPrimitiveHelper.ConvertPrimitiveValue(valueToConvert, typeof(DateTime), timeZone); + [Theory] + [MemberData(nameof(ConvertDateTime_NonStandardPrimitives_Data))] + public void ConvertDateTimeValue_NonStandardPrimitives_DefaultTimeZoneInfo(DateTimeOffset valueToConvert) + { + // Arrange & Act + object actual = EdmPrimitiveHelper.ConvertPrimitiveValue(valueToConvert, typeof(DateTime)); - // Assert - DateTime dt = Assert.IsType(actual); - Assert.Equal(TimeZoneInfo.ConvertTime(valueToConvert, timeZone).DateTime, dt); - } + // Assert + DateTime dt = Assert.IsType(actual); + Assert.Equal(valueToConvert.LocalDateTime, dt); + } - [Theory] - [MemberData(nameof(ConvertThrow_NonStandardPrimitives_Data))] - public void ConvertPrimitiveValue_Throws(object valueToConvert, Type conversionType, string exception) - { - // Arrange & Act & Assert - ExceptionAssert.Throws( - () => EdmPrimitiveHelper.ConvertPrimitiveValue(valueToConvert, conversionType), - exception); - } + [Theory] + [MemberData(nameof(ConvertDateTime_NonStandardPrimitives_Data))] + public void ConvertDateTimeValue_NonStandardPrimitives_CustomTimeZoneInfo(DateTimeOffset valueToConvert) + { + // Arrange & Act + var timeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); + object actual = EdmPrimitiveHelper.ConvertPrimitiveValue(valueToConvert, typeof(DateTime), timeZone); + + // Assert + DateTime dt = Assert.IsType(actual); + Assert.Equal(TimeZoneInfo.ConvertTime(valueToConvert, timeZone).DateTime, dt); + } + + [Theory] + [MemberData(nameof(ConvertThrow_NonStandardPrimitives_Data))] + public void ConvertPrimitiveValue_Throws(object valueToConvert, Type conversionType, string exception) + { + // Arrange & Act & Assert + ExceptionAssert.Throws( + () => EdmPrimitiveHelper.ConvertPrimitiveValue(valueToConvert, conversionType), + exception); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmTestHelpers.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmTestHelpers.cs index c23a06b28..09f8177d2 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmTestHelpers.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/EdmTestHelpers.cs @@ -7,18 +7,17 @@ using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +internal static class EdmTestHelpers { - internal static class EdmTestHelpers + public static IEdmEntityTypeReference AsReference(this IEdmEntityType entity) { - public static IEdmEntityTypeReference AsReference(this IEdmEntityType entity) - { - return new EdmEntityTypeReference(entity, isNullable: false); - } + return new EdmEntityTypeReference(entity, isNullable: false); + } - public static IEdmComplexTypeReference AsReference(this IEdmComplexType complex) - { - return new EdmComplexTypeReference(complex, isNullable: false); - } + public static IEdmComplexTypeReference AsReference(this IEdmComplexType complex) + { + return new EdmComplexTypeReference(complex, isNullable: false); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/ExpandModelPathTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/ExpandModelPathTests.cs index 42babab58..bc9695155 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/ExpandModelPathTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/ExpandModelPathTests.cs @@ -13,174 +13,173 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class ExpandModelPathTests { - public class ExpandModelPathTests - { - private IEdmEntityType _customerType; - private IEdmComplexType _addressType; - private IEdmNavigationProperty _relatedNavProperty; - private IEdmStructuralProperty _homeAddress; + private IEdmEntityType _customerType; + private IEdmComplexType _addressType; + private IEdmNavigationProperty _relatedNavProperty; + private IEdmStructuralProperty _homeAddress; - public ExpandModelPathTests() - { - // Address is an open complex type - var addressType = new EdmComplexType("NS", "Address"); - addressType.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); - addressType.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); - _addressType = addressType; - - EdmEntityType customerType = new EdmEntityType("NS", "Customer"); - customerType.AddKeys(customerType.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - _homeAddress = customerType.AddStructuralProperty("HomeAddress", new EdmComplexTypeReference(addressType, false)); - _customerType = customerType; - - _relatedNavProperty = addressType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "RelatedCustomers", - Target = customerType, - TargetMultiplicity = EdmMultiplicity.Many - }); - } - - [Fact] - public void Ctor_ExpandModelPath_WithBasicElements_SetsProperties() + public ExpandModelPathTests() + { + // Address is an open complex type + var addressType = new EdmComplexType("NS", "Address"); + addressType.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + addressType.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + _addressType = addressType; + + EdmEntityType customerType = new EdmEntityType("NS", "Customer"); + customerType.AddKeys(customerType.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + _homeAddress = customerType.AddStructuralProperty("HomeAddress", new EdmComplexTypeReference(addressType, false)); + _customerType = customerType; + + _relatedNavProperty = addressType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - // Arrange - IList elements = new List - { - _relatedNavProperty - }; - - // Act - ExpandModelPath expandPath = new ExpandModelPath(elements); - - // Assert - Assert.Same(_relatedNavProperty, expandPath.Navigation); - Assert.Equal("RelatedCustomers", expandPath.ExpandPath); - Assert.Equal("", expandPath.NavigationPropertyPath); - } - - [Fact] - public void Ctor_ExpandModelPath_WithOtherElements_SetsProperties() + Name = "RelatedCustomers", + Target = customerType, + TargetMultiplicity = EdmMultiplicity.Many + }); + } + + [Fact] + public void Ctor_ExpandModelPath_WithBasicElements_SetsProperties() + { + // Arrange + IList elements = new List { - // Arrange - IList elements = new List - { - _homeAddress, - _relatedNavProperty - }; - - // Act - ExpandModelPath expandPath = new ExpandModelPath(elements); - - // Assert - Assert.Same(_relatedNavProperty, expandPath.Navigation); - Assert.Equal("HomeAddress/RelatedCustomers", expandPath.ExpandPath); - Assert.Equal("HomeAddress", expandPath.NavigationPropertyPath); - } - - [Fact] - public void Ctor_ExpandModelPath_WithTypeElements_SetsProperties() + _relatedNavProperty + }; + + // Act + ExpandModelPath expandPath = new ExpandModelPath(elements); + + // Assert + Assert.Same(_relatedNavProperty, expandPath.Navigation); + Assert.Equal("RelatedCustomers", expandPath.ExpandPath); + Assert.Equal("", expandPath.NavigationPropertyPath); + } + + [Fact] + public void Ctor_ExpandModelPath_WithOtherElements_SetsProperties() + { + // Arrange + IList elements = new List { - // Arrange - IList elements = new List - { - _homeAddress, - _customerType, // it's not valid from the model, but it's valid for the test. - _relatedNavProperty - }; - - // Act - ExpandModelPath expandPath = new ExpandModelPath(elements); - - // Assert - Assert.Same(_relatedNavProperty, expandPath.Navigation); - Assert.Equal("HomeAddress/NS.Customer/RelatedCustomers", expandPath.ExpandPath); - Assert.Equal("HomeAddress/NS.Customer", expandPath.NavigationPropertyPath); - } - - [Fact] - public void Ctor_ExpandModelPath_Throws_EmptyElement() + _homeAddress, + _relatedNavProperty + }; + + // Act + ExpandModelPath expandPath = new ExpandModelPath(elements); + + // Assert + Assert.Same(_relatedNavProperty, expandPath.Navigation); + Assert.Equal("HomeAddress/RelatedCustomers", expandPath.ExpandPath); + Assert.Equal("HomeAddress", expandPath.NavigationPropertyPath); + } + + [Fact] + public void Ctor_ExpandModelPath_WithTypeElements_SetsProperties() + { + // Arrange + IList elements = new List { - // Arrange - IList elements = new List(); + _homeAddress, + _customerType, // it's not valid from the model, but it's valid for the test. + _relatedNavProperty + }; + + // Act + ExpandModelPath expandPath = new ExpandModelPath(elements); + + // Assert + Assert.Same(_relatedNavProperty, expandPath.Navigation); + Assert.Equal("HomeAddress/NS.Customer/RelatedCustomers", expandPath.ExpandPath); + Assert.Equal("HomeAddress/NS.Customer", expandPath.NavigationPropertyPath); + } - // Act - Action test = () => new ExpandModelPath(elements); + [Fact] + public void Ctor_ExpandModelPath_Throws_EmptyElement() + { + // Arrange + IList elements = new List(); - // Assert - ExceptionAssert.Throws(test, - "A navigation property expand path should have navigation property in the path."); - } + // Act + Action test = () => new ExpandModelPath(elements); - [Fact] - public void Ctor_ExpandModelPath_Throws_WithoutNavigationElementOnLast() + // Assert + ExceptionAssert.Throws(test, + "A navigation property expand path should have navigation property in the path."); + } + + [Fact] + public void Ctor_ExpandModelPath_Throws_WithoutNavigationElementOnLast() + { + // Arrange + IList elements = new List { - // Arrange - IList elements = new List - { - _homeAddress - }; - - // Act - Action test = () => new ExpandModelPath(elements); - - // Assert - ExceptionAssert.Throws(test, - "The last segment 'EdmStructuralProperty' of the select or expand query option is not supported."); - } - - [Fact] - public void Ctor_ExpandModelPath_Throws_OnlyWithTypeCastElementOnLast() + _homeAddress + }; + + // Act + Action test = () => new ExpandModelPath(elements); + + // Assert + ExceptionAssert.Throws(test, + "The last segment 'EdmStructuralProperty' of the select or expand query option is not supported."); + } + + [Fact] + public void Ctor_ExpandModelPath_Throws_OnlyWithTypeCastElementOnLast() + { + // Arrange + IList elements = new List { - // Arrange - IList elements = new List - { - _addressType - }; - - // Act - Action test = () => new ExpandModelPath(elements); - - // Assert - ExceptionAssert.Throws(test, - "The last segment 'EdmComplexType' of the select or expand query option is not supported."); - } - - [Fact] - public void Ctor_ExpandModelPath_Throws_OnlyMultipleNavigationElement() + _addressType + }; + + // Act + Action test = () => new ExpandModelPath(elements); + + // Assert + ExceptionAssert.Throws(test, + "The last segment 'EdmComplexType' of the select or expand query option is not supported."); + } + + [Fact] + public void Ctor_ExpandModelPath_Throws_OnlyMultipleNavigationElement() + { + // Arrange + IList elements = new List { - // Arrange - IList elements = new List - { - _relatedNavProperty, - _relatedNavProperty - }; - - // Act - Action test = () => new ExpandModelPath(elements); - - // Assert - ExceptionAssert.Throws(test, - "A segment 'EdmNavigationProperty' within the select or expand query option is not supported."); - } - - [Fact] - public void Ctor_ExpandModelPath_Throws_ForNonSupportedElement() + _relatedNavProperty, + _relatedNavProperty + }; + + // Act + Action test = () => new ExpandModelPath(elements); + + // Assert + ExceptionAssert.Throws(test, + "A segment 'EdmNavigationProperty' within the select or expand query option is not supported."); + } + + [Fact] + public void Ctor_ExpandModelPath_Throws_ForNonSupportedElement() + { + // Arrange + IList elements = new List { - // Arrange - IList elements = new List - { - new EdmEntityContainer("NS", "Default") - }; - - // Act - Action test = () => new ExpandModelPath(elements); - - // Assert - ExceptionAssert.Throws(test, - "A segment 'EdmEntityContainer' within the select or expand query option is not supported."); - } + new EdmEntityContainer("NS", "Default") + }; + + // Act + Action test = () => new ExpandModelPath(elements); + + // Assert + ExceptionAssert.Throws(test, + "A segment 'EdmEntityContainer' within the select or expand query option is not supported."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/IODataTypeMapperExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/IODataTypeMapperExtensionsTests.cs index 2a81ebb39..4c4f7ef8e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/IODataTypeMapperExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/IODataTypeMapperExtensionsTests.cs @@ -14,117 +14,116 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class IODataTypeMapperExtensionsTests { - public class IODataTypeMapperExtensionsTests + [Fact] + public void GetPrimitiveType_ThrowsArgumentNull_ForInputParameters() + { + // Arrange & Act & Assert + IODataTypeMapper mapper = null; + ExceptionAssert.ThrowsArgumentNull(() => mapper.GetPrimitiveType(primitiveType: null), "mapper"); + + // Arrange & Act & Assert + mapper = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => mapper.GetPrimitiveType(primitiveType: null), "primitiveType"); + } + + [Fact] + public void GetPrimitiveType_Calls_GetPrimitiveTypeOnInterface() + { + // Arrange + Mock primitive = new Mock(); + Mock primitiveRef = new Mock(); + primitiveRef.Setup(x => x.Definition).Returns(primitive.Object); + primitiveRef.SetupGet(x => x.IsNullable).Returns(false); + + Mock mapper = new Mock(); + mapper.Setup(s => s.GetClrPrimitiveType(primitive.Object, false)).Verifiable(); + + // Act + mapper.Object.GetPrimitiveType(primitiveRef.Object); + + // Assert + mapper.Verify(); + } + + [Fact] + public void GetEdmType_ThrowsArgumentNull_ForInputParameters() + { + // Arrange & Act & Assert + IODataTypeMapper mapper = null; + ExceptionAssert.ThrowsArgumentNull(() => mapper.GetEdmType(null, null), "mapper"); + } + + [Fact] + public void GetEdmType_Calls_GetEdmTypeReferenceOnInterface() + { + // Arrange + Mock model = new Mock(); + Type type = typeof(int); + + Mock mapper = new Mock(); + mapper.Setup(s => s.GetEdmTypeReference(model.Object, type)).Verifiable(); + + // Act + mapper.Object.GetEdmType(model.Object, type); + + // Assert + mapper.Verify(); + } + + [Fact] + public void GetClrType_ThrowsArgumentNull_ForInputParameters() + { + // Arrange & Act & Assert + IODataTypeMapper mapper = null; + ExceptionAssert.ThrowsArgumentNull(() => mapper.GetClrType(null, null), "mapper"); + + // Arrange & Act & Assert + mapper = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => mapper.GetClrType(null, null), "edmType"); + } + + [Fact] + public void GetClrType_Calls_GetClrTypeOnInterface() + { + // Arrange + Mock model = new Mock(); + Mock primitive = new Mock(); + Mock primitiveRef = new Mock(); + primitiveRef.Setup(x => x.Definition).Returns(primitive.Object); + primitiveRef.SetupGet(x => x.IsNullable).Returns(false); + + Mock mapper = new Mock(); + mapper.Setup(s => s.GetClrType(model.Object, primitive.Object, false, AssemblyResolverHelper.Default)).Verifiable(); + + // Act + mapper.Object.GetClrType(model.Object, primitiveRef.Object); + + // Assert + mapper.Verify(); + } + + [Fact] + public void GetClrTypeWithAssemblyResolver_Calls_GetClrTypeOnInterface() { - [Fact] - public void GetPrimitiveType_ThrowsArgumentNull_ForInputParameters() - { - // Arrange & Act & Assert - IODataTypeMapper mapper = null; - ExceptionAssert.ThrowsArgumentNull(() => mapper.GetPrimitiveType(primitiveType: null), "mapper"); - - // Arrange & Act & Assert - mapper = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => mapper.GetPrimitiveType(primitiveType: null), "primitiveType"); - } - - [Fact] - public void GetPrimitiveType_Calls_GetPrimitiveTypeOnInterface() - { - // Arrange - Mock primitive = new Mock(); - Mock primitiveRef = new Mock(); - primitiveRef.Setup(x => x.Definition).Returns(primitive.Object); - primitiveRef.SetupGet(x => x.IsNullable).Returns(false); - - Mock mapper = new Mock(); - mapper.Setup(s => s.GetClrPrimitiveType(primitive.Object, false)).Verifiable(); - - // Act - mapper.Object.GetPrimitiveType(primitiveRef.Object); - - // Assert - mapper.Verify(); - } - - [Fact] - public void GetEdmType_ThrowsArgumentNull_ForInputParameters() - { - // Arrange & Act & Assert - IODataTypeMapper mapper = null; - ExceptionAssert.ThrowsArgumentNull(() => mapper.GetEdmType(null, null), "mapper"); - } - - [Fact] - public void GetEdmType_Calls_GetEdmTypeReferenceOnInterface() - { - // Arrange - Mock model = new Mock(); - Type type = typeof(int); - - Mock mapper = new Mock(); - mapper.Setup(s => s.GetEdmTypeReference(model.Object, type)).Verifiable(); - - // Act - mapper.Object.GetEdmType(model.Object, type); - - // Assert - mapper.Verify(); - } - - [Fact] - public void GetClrType_ThrowsArgumentNull_ForInputParameters() - { - // Arrange & Act & Assert - IODataTypeMapper mapper = null; - ExceptionAssert.ThrowsArgumentNull(() => mapper.GetClrType(null, null), "mapper"); - - // Arrange & Act & Assert - mapper = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => mapper.GetClrType(null, null), "edmType"); - } - - [Fact] - public void GetClrType_Calls_GetClrTypeOnInterface() - { - // Arrange - Mock model = new Mock(); - Mock primitive = new Mock(); - Mock primitiveRef = new Mock(); - primitiveRef.Setup(x => x.Definition).Returns(primitive.Object); - primitiveRef.SetupGet(x => x.IsNullable).Returns(false); - - Mock mapper = new Mock(); - mapper.Setup(s => s.GetClrType(model.Object, primitive.Object, false, AssemblyResolverHelper.Default)).Verifiable(); - - // Act - mapper.Object.GetClrType(model.Object, primitiveRef.Object); - - // Assert - mapper.Verify(); - } - - [Fact] - public void GetClrTypeWithAssemblyResolver_Calls_GetClrTypeOnInterface() - { - // Arrange - Mock resolver = new Mock(); - Mock model = new Mock(); - Mock primitive = new Mock(); - Mock primitiveRef = new Mock(); - primitiveRef.Setup(x => x.Definition).Returns(primitive.Object); - primitiveRef.SetupGet(x => x.IsNullable).Returns(false); - - Mock mapper = new Mock(); - mapper.Setup(s => s.GetClrType(model.Object, primitive.Object, false, resolver.Object)).Verifiable(); - - // Act - mapper.Object.GetClrType(model.Object, primitiveRef.Object, resolver.Object); - - // Assert - mapper.Verify(); - } + // Arrange + Mock resolver = new Mock(); + Mock model = new Mock(); + Mock primitive = new Mock(); + Mock primitiveRef = new Mock(); + primitiveRef.Setup(x => x.Definition).Returns(primitive.Object); + primitiveRef.SetupGet(x => x.IsNullable).Returns(false); + + Mock mapper = new Mock(); + mapper.Setup(s => s.GetClrType(model.Object, primitive.Object, false, resolver.Object)).Verifiable(); + + // Act + mapper.Object.GetClrType(model.Object, primitiveRef.Object, resolver.Object); + + // Assert + mapper.Verify(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/MockNavigationSourceLinkBuilderAnnotation.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/MockNavigationSourceLinkBuilderAnnotation.cs index ac7a06046..a89a1a74d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/MockNavigationSourceLinkBuilderAnnotation.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/MockNavigationSourceLinkBuilderAnnotation.cs @@ -10,50 +10,49 @@ using Microsoft.AspNetCore.OData.Formatter; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +internal class MockNavigationSourceLinkBuilderAnnotation : NavigationSourceLinkBuilderAnnotation { - internal class MockNavigationSourceLinkBuilderAnnotation : NavigationSourceLinkBuilderAnnotation - { - public Func NavigationLinkBuilder { get; set; } + public Func NavigationLinkBuilder { get; set; } - public override Uri BuildEditLink(ResourceContext instanceContext, ODataMetadataLevel metadataLevel, Uri idLink) + public override Uri BuildEditLink(ResourceContext instanceContext, ODataMetadataLevel metadataLevel, Uri idLink) + { + if (EditLinkBuilder != null) { - if (EditLinkBuilder != null) - { - return EditLinkBuilder.Factory(instanceContext); - } - - return null; + return EditLinkBuilder.Factory(instanceContext); } - public override Uri BuildIdLink(ResourceContext instanceContext, ODataMetadataLevel metadataLevel) - { - if (IdLinkBuilder != null) - { - return IdLinkBuilder.Factory(instanceContext); - } + return null; + } - return null; + public override Uri BuildIdLink(ResourceContext instanceContext, ODataMetadataLevel metadataLevel) + { + if (IdLinkBuilder != null) + { + return IdLinkBuilder.Factory(instanceContext); } - public override Uri BuildReadLink(ResourceContext instanceContext, ODataMetadataLevel metadataLevel, Uri editLink) - { - if (ReadLinkBuilder != null) - { - return ReadLinkBuilder.Factory(instanceContext); - } + return null; + } - return null; + public override Uri BuildReadLink(ResourceContext instanceContext, ODataMetadataLevel metadataLevel, Uri editLink) + { + if (ReadLinkBuilder != null) + { + return ReadLinkBuilder.Factory(instanceContext); } - public override Uri BuildNavigationLink(ResourceContext context, IEdmNavigationProperty navigationProperty, ODataMetadataLevel metadataLevel) - { - if (NavigationLinkBuilder != null) - { - return NavigationLinkBuilder(context, navigationProperty, metadataLevel); - } + return null; + } - return null; + public override Uri BuildNavigationLink(ResourceContext context, IEdmNavigationProperty navigationProperty, ODataMetadataLevel metadataLevel) + { + if (NavigationLinkBuilder != null) + { + return NavigationLinkBuilder(context, navigationProperty, metadataLevel); } + + return null; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/ModelNameAnnotationTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/ModelNameAnnotationTests.cs index b151ff264..7a616a7df 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/ModelNameAnnotationTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/ModelNameAnnotationTests.cs @@ -10,24 +10,23 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class ModelNameAnnotationTests { - public class ModelNameAnnotationTests + [Fact] + public void Ctor_ThrowsArgumentNull_Name() { - [Fact] - public void Ctor_ThrowsArgumentNull_Name() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ModelNameAnnotation(null), "name"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ModelNameAnnotation(null), "name"); + } - [Fact] - public void Ctor_SetsProperties() - { - // Arrange & Act & Assert - ModelNameAnnotation annotation = new ModelNameAnnotation("any"); + [Fact] + public void Ctor_SetsProperties() + { + // Arrange & Act & Assert + ModelNameAnnotation annotation = new ModelNameAnnotation("any"); - Assert.Equal("any", annotation.ModelName); - } + Assert.Equal("any", annotation.ModelName); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/NavigationLinkBuilderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/NavigationLinkBuilderTests.cs index 72bf94b40..9ed5c258d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/NavigationLinkBuilderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/NavigationLinkBuilderTests.cs @@ -12,27 +12,26 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class NavigationLinkBuilderTests { - public class NavigationLinkBuilderTests + [Fact] + public void CtorNavigationLinkBuilder_ThrowsArgumentNull_LinkFactory() { - [Fact] - public void CtorNavigationLinkBuilder_ThrowsArgumentNull_LinkFactory() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new NavigationLinkBuilder(null, true), "navigationLinkFactory"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new NavigationLinkBuilder(null, true), "navigationLinkFactory"); + } - [Fact] - public void CtorNavigationLinkBuilder_SetsProperties() - { - // Arrange & Act & Assert - Func navigationLinkFactory = (r, p) => null; - NavigationLinkBuilder builder = new NavigationLinkBuilder(navigationLinkFactory, false); + [Fact] + public void CtorNavigationLinkBuilder_SetsProperties() + { + // Arrange & Act & Assert + Func navigationLinkFactory = (r, p) => null; + NavigationLinkBuilder builder = new NavigationLinkBuilder(navigationLinkFactory, false); - // Assert - Assert.Same(navigationLinkFactory, builder.Factory); - Assert.False(builder.FollowsConventions); - } + // Assert + Assert.Same(navigationLinkFactory, builder.Factory); + Assert.False(builder.FollowsConventions); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/NavigationSourceLinkBuilderAnnotationTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/NavigationSourceLinkBuilderAnnotationTests.cs index 3535d041f..ee487d220 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/NavigationSourceLinkBuilderAnnotationTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/NavigationSourceLinkBuilderAnnotationTests.cs @@ -13,104 +13,103 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class NavigationSourceLinkBuilderAnnotationTests { - public class NavigationSourceLinkBuilderAnnotationTests + private static IEdmModel _model; + private static IEdmEntitySet _customers; + + static NavigationSourceLinkBuilderAnnotationTests() + { + _model = GetEdmMode(); + _customers = _model.EntityContainer.FindEntitySet("Customers"); + } + + [Fact] + public void CtorNavigationSourceLinkBuilderAnnotation_ThrowsArgumentNull_ForInputParameters() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new NavigationSourceLinkBuilderAnnotation(null, null), "navigationSource"); + + // Arrange & Act & Assert + IEdmNavigationSource navigationSource = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => new NavigationSourceLinkBuilderAnnotation(navigationSource, null), "model"); + } + + [Fact] + public void BuildIdLink_ThrowsArgumentNull_InstanceContext() + { + // Arrange + NavigationSourceLinkBuilderAnnotation linkBuilder = new NavigationSourceLinkBuilderAnnotation(_customers, _model); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildIdLink(null, ODataMetadataLevel.Full), "instanceContext"); + } + + [Fact] + public void BuildEditLink_ThrowsArgumentNull_InstanceContext() + { + // Arrange + NavigationSourceLinkBuilderAnnotation linkBuilder = new NavigationSourceLinkBuilderAnnotation(_customers, _model); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildEditLink(null, ODataMetadataLevel.Full, null), "instanceContext"); + } + + [Fact] + public void BuildReadLink_ThrowsArgumentNull_InstanceContext() + { + // Arrange + NavigationSourceLinkBuilderAnnotation linkBuilder = new NavigationSourceLinkBuilderAnnotation(_customers, _model); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildReadLink(null, ODataMetadataLevel.Full, null), "instanceContext"); + } + + [Fact] + public void BuildNavigationLink_ThrowsArgumentNull_InstanceContext() + { + // Arrange + NavigationSourceLinkBuilderAnnotation linkBuilder = new NavigationSourceLinkBuilderAnnotation(_customers, _model); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildNavigationLink(null, null, ODataMetadataLevel.Full), "instanceContext"); + ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildNavigationLink(null, null), "instanceContext"); + } + + [Fact] + public void BuildNavigationLink_ThrowsArgumentNull_NavigationProperty() + { + // Arrange + NavigationSourceLinkBuilderAnnotation linkBuilder = new NavigationSourceLinkBuilderAnnotation(_customers, _model); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildNavigationLink(new ResourceContext(), null, ODataMetadataLevel.Full), "navigationProperty"); + ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildNavigationLink(new ResourceContext(), null), "navigationProperty"); + } + + [Fact] + public void CtorNavigationSourceLinkBuilderAnnotation_SetsProperties() + { + // Arrange & Act & Assert + Func navigationLinkFactory = (r, p) => null; + NavigationLinkBuilder builder = new NavigationLinkBuilder(navigationLinkFactory, false); + + // Assert + Assert.Same(navigationLinkFactory, builder.Factory); + Assert.False(builder.FollowsConventions); + } + + private static IEdmModel GetEdmMode() + { + var builder = new Microsoft.OData.ModelBuilder.ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); + } + + private class Customer { - private static IEdmModel _model; - private static IEdmEntitySet _customers; - - static NavigationSourceLinkBuilderAnnotationTests() - { - _model = GetEdmMode(); - _customers = _model.EntityContainer.FindEntitySet("Customers"); - } - - [Fact] - public void CtorNavigationSourceLinkBuilderAnnotation_ThrowsArgumentNull_ForInputParameters() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new NavigationSourceLinkBuilderAnnotation(null, null), "navigationSource"); - - // Arrange & Act & Assert - IEdmNavigationSource navigationSource = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => new NavigationSourceLinkBuilderAnnotation(navigationSource, null), "model"); - } - - [Fact] - public void BuildIdLink_ThrowsArgumentNull_InstanceContext() - { - // Arrange - NavigationSourceLinkBuilderAnnotation linkBuilder = new NavigationSourceLinkBuilderAnnotation(_customers, _model); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildIdLink(null, ODataMetadataLevel.Full), "instanceContext"); - } - - [Fact] - public void BuildEditLink_ThrowsArgumentNull_InstanceContext() - { - // Arrange - NavigationSourceLinkBuilderAnnotation linkBuilder = new NavigationSourceLinkBuilderAnnotation(_customers, _model); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildEditLink(null, ODataMetadataLevel.Full, null), "instanceContext"); - } - - [Fact] - public void BuildReadLink_ThrowsArgumentNull_InstanceContext() - { - // Arrange - NavigationSourceLinkBuilderAnnotation linkBuilder = new NavigationSourceLinkBuilderAnnotation(_customers, _model); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildReadLink(null, ODataMetadataLevel.Full, null), "instanceContext"); - } - - [Fact] - public void BuildNavigationLink_ThrowsArgumentNull_InstanceContext() - { - // Arrange - NavigationSourceLinkBuilderAnnotation linkBuilder = new NavigationSourceLinkBuilderAnnotation(_customers, _model); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildNavigationLink(null, null, ODataMetadataLevel.Full), "instanceContext"); - ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildNavigationLink(null, null), "instanceContext"); - } - - [Fact] - public void BuildNavigationLink_ThrowsArgumentNull_NavigationProperty() - { - // Arrange - NavigationSourceLinkBuilderAnnotation linkBuilder = new NavigationSourceLinkBuilderAnnotation(_customers, _model); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildNavigationLink(new ResourceContext(), null, ODataMetadataLevel.Full), "navigationProperty"); - ExceptionAssert.ThrowsArgumentNull(() => linkBuilder.BuildNavigationLink(new ResourceContext(), null), "navigationProperty"); - } - - [Fact] - public void CtorNavigationSourceLinkBuilderAnnotation_SetsProperties() - { - // Arrange & Act & Assert - Func navigationLinkFactory = (r, p) => null; - NavigationLinkBuilder builder = new NavigationLinkBuilder(navigationLinkFactory, false); - - // Assert - Assert.Same(navigationLinkFactory, builder.Factory); - Assert.False(builder.FollowsConventions); - } - - private static IEdmModel GetEdmMode() - { - var builder = new Microsoft.OData.ModelBuilder.ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } - - private class Customer - { - public int Id { get; set; } - } + public int Id { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/OperationHelperTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/OperationHelperTests.cs index ce50c7997..74fba3526 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/OperationHelperTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/OperationHelperTests.cs @@ -15,259 +15,258 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class OperationHelperTests { - public class OperationHelperTests + [Fact] + public void VerifyAndBuildParameterMappings_ThrowsArgumentNull_InputParameters() { - [Fact] - public void VerifyAndBuildParameterMappings_ThrowsArgumentNull_InputParameters() - { - // Arrange & Act & Assert - IEdmFunction function = null; - ExceptionAssert.ThrowsArgumentNull(() => function.VerifyAndBuildParameterMappings(null), "function"); + // Arrange & Act & Assert + IEdmFunction function = null; + ExceptionAssert.ThrowsArgumentNull(() => function.VerifyAndBuildParameterMappings(null), "function"); - // Arrange & Act & Assert - function = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => function.VerifyAndBuildParameterMappings(null), "parameters"); - } + // Arrange & Act & Assert + function = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => function.VerifyAndBuildParameterMappings(null), "parameters"); + } - [Fact] - public void VerifyAndBuildParameterMappings_ThrowsODataException_MissMatchParameters() + [Fact] + public void VerifyAndBuildParameterMappings_ThrowsODataException_MissMatchParameters() + { + // Arrange + IEdmTypeReference strType = EdmCoreModel.Instance.GetString(false); + EdmFunction function = new EdmFunction("NS", "Function", strType); + function.AddParameter("Name", strType); + + // Act & Assert + IDictionary parameters = new Dictionary { - // Arrange - IEdmTypeReference strType = EdmCoreModel.Instance.GetString(false); - EdmFunction function = new EdmFunction("NS", "Function", strType); - function.AddParameter("Name", strType); + { "Other", "{Other}" } + }; + Action test = () => function.VerifyAndBuildParameterMappings(parameters); + ExceptionAssert.Throws(test, "Missing the required parameter 'Name' is in the operation 'NS.Function' parameter mapping."); + + // Act & Assert + parameters["Name"] = "{Name}"; // Other is still in the dictionary + test = () => function.VerifyAndBuildParameterMappings(parameters); + ExceptionAssert.Throws(test, "Cannot find parameter 'Other' is in the operation 'NS.Function'."); + } - // Act & Assert - IDictionary parameters = new Dictionary - { - { "Other", "{Other}" } - }; - Action test = () => function.VerifyAndBuildParameterMappings(parameters); - ExceptionAssert.Throws(test, "Missing the required parameter 'Name' is in the operation 'NS.Function' parameter mapping."); - - // Act & Assert - parameters["Name"] = "{Name}"; // Other is still in the dictionary - test = () => function.VerifyAndBuildParameterMappings(parameters); - ExceptionAssert.Throws(test, "Cannot find parameter 'Other' is in the operation 'NS.Function'."); - } - - [Theory] - [InlineData(null)] - [InlineData("Name")] - public void VerifyAndBuildParameterMappings_ThrowsODataException_UnwrapperedWithCurlyBraces(string parameterTemplate) + [Theory] + [InlineData(null)] + [InlineData("Name")] + public void VerifyAndBuildParameterMappings_ThrowsODataException_UnwrapperedWithCurlyBraces(string parameterTemplate) + { + // Arrange + IEdmTypeReference strType = EdmCoreModel.Instance.GetString(false); + EdmFunction function = new EdmFunction("NS", "Function", strType); + function.AddParameter("Name", strType); + + IDictionary parameters = new Dictionary { - // Arrange - IEdmTypeReference strType = EdmCoreModel.Instance.GetString(false); - EdmFunction function = new EdmFunction("NS", "Function", strType); - function.AddParameter("Name", strType); + { "Name", parameterTemplate } + }; - IDictionary parameters = new Dictionary - { - { "Name", parameterTemplate } - }; + // Act & Assert + Action test = () => function.VerifyAndBuildParameterMappings(parameters); + ExceptionAssert.Throws(test, $"Parameter template '{parameterTemplate}' in segment 'NS.Function' does not start with '{{' or ends with '}}'."); + } - // Act & Assert - Action test = () => function.VerifyAndBuildParameterMappings(parameters); - ExceptionAssert.Throws(test, $"Parameter template '{parameterTemplate}' in segment 'NS.Function' does not start with '{{' or ends with '}}'."); - } + [Fact] + public void VerifyAndBuildParameterMappings_ThrowsODataException_EmptyTemplate() + { + // Arrange + IEdmTypeReference strType = EdmCoreModel.Instance.GetString(false); + EdmFunction function = new EdmFunction("NS", "Function", strType); + function.AddParameter("Name", strType); - [Fact] - public void VerifyAndBuildParameterMappings_ThrowsODataException_EmptyTemplate() + IDictionary parameters = new Dictionary { - // Arrange - IEdmTypeReference strType = EdmCoreModel.Instance.GetString(false); - EdmFunction function = new EdmFunction("NS", "Function", strType); - function.AddParameter("Name", strType); + { "Name", "{}" } + }; - IDictionary parameters = new Dictionary - { - { "Name", "{}" } - }; + // Act & Assert + Action test = () => function.VerifyAndBuildParameterMappings(parameters); + ExceptionAssert.Throws(test, "Parameter alias 'Name' in segment 'NS.Function' is empty."); + } - // Act & Assert - Action test = () => function.VerifyAndBuildParameterMappings(parameters); - ExceptionAssert.Throws(test, "Parameter alias 'Name' in segment 'NS.Function' is empty."); - } + [Fact] + public void VerifyAndBuildParameterMappings_Works_FunctionParameter() + { + // Arrange + IEdmTypeReference strType = EdmCoreModel.Instance.GetString(false); + EdmFunction function = new EdmFunction("NS", "Function", strType); + function.AddParameter("Name", strType); + function.AddParameter("Title", strType); - [Fact] - public void VerifyAndBuildParameterMappings_Works_FunctionParameter() + IDictionary parameters = new Dictionary { - // Arrange - IEdmTypeReference strType = EdmCoreModel.Instance.GetString(false); - EdmFunction function = new EdmFunction("NS", "Function", strType); - function.AddParameter("Name", strType); - function.AddParameter("Title", strType); + { "Name", "{NameValue}" }, + { "Title", "{TitleValue}" } + }; + + // Act + IDictionary actual = function.VerifyAndBuildParameterMappings(parameters); - IDictionary parameters = new Dictionary + // Assert + Assert.Equal(2, actual.Count); + Assert.Collection(actual, + e => { - { "Name", "{NameValue}" }, - { "Title", "{TitleValue}" } - }; - - // Act - IDictionary actual = function.VerifyAndBuildParameterMappings(parameters); - - // Assert - Assert.Equal(2, actual.Count); - Assert.Collection(actual, - e => - { - Assert.Equal("Name", e.Key); - Assert.Equal("NameValue", e.Value); - }, - e => - { - Assert.Equal("Title", e.Key); - Assert.Equal("TitleValue", e.Value); - }); - } - - [Fact] - public void BuildParameterMappings_ThrowsArgumentNull_Parameters() + Assert.Equal("Name", e.Key); + Assert.Equal("NameValue", e.Value); + }, + e => + { + Assert.Equal("Title", e.Key); + Assert.Equal("TitleValue", e.Value); + }); + } + + [Fact] + public void BuildParameterMappings_ThrowsArgumentNull_Parameters() + { + // Arrange & Act & Assert + IEnumerable parameters = null; + ExceptionAssert.ThrowsArgumentNull(() => parameters.BuildParameterMappings("function"), "parameters"); + } + + [Theory] + [InlineData(null)] + [InlineData("Name")] + public void BuildParameterMappings_ThrowsODataException_UnwrapperedWithCurlyBraces(string parameterTemplate) + { + // Arrange + IEnumerable parameters = new List { - // Arrange & Act & Assert - IEnumerable parameters = null; - ExceptionAssert.ThrowsArgumentNull(() => parameters.BuildParameterMappings("function"), "parameters"); - } - - [Theory] - [InlineData(null)] - [InlineData("Name")] - public void BuildParameterMappings_ThrowsODataException_UnwrapperedWithCurlyBraces(string parameterTemplate) + new OperationSegmentParameter("Name", parameterTemplate) + }; + + // Act & Assert + Action test = () => parameters.BuildParameterMappings("NS.Function"); + ExceptionAssert.Throws(test, $"Parameter template '{parameterTemplate}' in segment 'NS.Function' does not start with '{{' or ends with '}}'."); + } + + [Fact] + public void BuildParameterMappings_ThrowsODataException_EmptyTemplate() + { + // Arrange + IEnumerable parameters = new List { - // Arrange - IEnumerable parameters = new List - { - new OperationSegmentParameter("Name", parameterTemplate) - }; + new OperationSegmentParameter("Name", "{}") + }; - // Act & Assert - Action test = () => parameters.BuildParameterMappings("NS.Function"); - ExceptionAssert.Throws(test, $"Parameter template '{parameterTemplate}' in segment 'NS.Function' does not start with '{{' or ends with '}}'."); - } + // Act & Assert + Action test = () => parameters.BuildParameterMappings("NS.Function"); + ExceptionAssert.Throws(test, "Parameter alias 'Name' in segment 'NS.Function' is empty."); + } - [Fact] - public void BuildParameterMappings_ThrowsODataException_EmptyTemplate() + [Fact] + public void BuildParameterMappings_Works_FunctionParameter() + { + // Arrange + IEnumerable parameters = new List { - // Arrange - IEnumerable parameters = new List + new OperationSegmentParameter("Name", "{NameValue}"), + new OperationSegmentParameter("Title", "{TitleValue}"), + }; + + // Act + IDictionary actual = parameters.BuildParameterMappings("NS.Function"); + + // Assert + Assert.Equal(2, actual.Count); + Assert.Collection(actual, + e => { - new OperationSegmentParameter("Name", "{}") - }; + Assert.Equal("Name", e.Key); + Assert.Equal("NameValue", e.Value); + }, + e => + { + Assert.Equal("Title", e.Key); + Assert.Equal("TitleValue", e.Value); + }); + } + + [Fact] + public void GetFunctionParamterMappings_ThrowsArgumentNull_Parameters() + { + // Arrange & Act & Assert + IEdmFunction function = null; + ExceptionAssert.ThrowsArgumentNull(() => function.GetFunctionParamterMappings(), "function"); - // Act & Assert - Action test = () => parameters.BuildParameterMappings("NS.Function"); - ExceptionAssert.Throws(test, "Parameter alias 'Name' in segment 'NS.Function' is empty."); - } + // Arrange & Act & Assert + IEdmFunctionImport functionImport = null; + ExceptionAssert.ThrowsArgumentNull(() => functionImport.GetFunctionParamterMappings(), "functionImport"); + } - [Fact] - public void BuildParameterMappings_Works_FunctionParameter() - { - // Arrange - IEnumerable parameters = new List + [Fact] + public void GetFunctionParamterMappings_Works_FunctionParameter() + { + // Arrange + IEdmTypeReference strType = EdmCoreModel.Instance.GetString(false); + EdmFunction function = new EdmFunction("NS", "Function", strType); + function.AddParameter("Name", strType); + function.AddParameter("Title", strType); + + // Act + IDictionary parameters = function.GetFunctionParamterMappings(); + + // Assert + Assert.Collection(parameters, + e => { - new OperationSegmentParameter("Name", "{NameValue}"), - new OperationSegmentParameter("Title", "{TitleValue}"), - }; - - // Act - IDictionary actual = parameters.BuildParameterMappings("NS.Function"); - - // Assert - Assert.Equal(2, actual.Count); - Assert.Collection(actual, - e => - { - Assert.Equal("Name", e.Key); - Assert.Equal("NameValue", e.Value); - }, - e => - { - Assert.Equal("Title", e.Key); - Assert.Equal("TitleValue", e.Value); - }); - } - - [Fact] - public void GetFunctionParamterMappings_ThrowsArgumentNull_Parameters() - { - // Arrange & Act & Assert - IEdmFunction function = null; - ExceptionAssert.ThrowsArgumentNull(() => function.GetFunctionParamterMappings(), "function"); + Assert.Equal("Name", e.Key); + Assert.Equal("{Name}", e.Value); + }, + e => + { + Assert.Equal("Title", e.Key); + Assert.Equal("{Title}", e.Value); + }); - // Arrange & Act & Assert - IEdmFunctionImport functionImport = null; - ExceptionAssert.ThrowsArgumentNull(() => functionImport.GetFunctionParamterMappings(), "functionImport"); - } + // Act + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + EdmFunctionImport functionImport = new EdmFunctionImport(container, "FunctionImport", function); - [Fact] - public void GetFunctionParamterMappings_Works_FunctionParameter() - { - // Arrange - IEdmTypeReference strType = EdmCoreModel.Instance.GetString(false); - EdmFunction function = new EdmFunction("NS", "Function", strType); - function.AddParameter("Name", strType); - function.AddParameter("Title", strType); - - // Act - IDictionary parameters = function.GetFunctionParamterMappings(); - - // Assert - Assert.Collection(parameters, - e => - { - Assert.Equal("Name", e.Key); - Assert.Equal("{Name}", e.Value); - }, - e => - { - Assert.Equal("Title", e.Key); - Assert.Equal("{Title}", e.Value); - }); - - // Act - EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); - EdmFunctionImport functionImport = new EdmFunctionImport(container, "FunctionImport", function); - - IDictionary parametersImport = parameters = function.GetFunctionParamterMappings(); - - // Assert - Assert.Equal(parameters, parametersImport); - } - - [Fact] - public void SplitOperationImports_ThrowsArgumentNull_OperationImports() - { - // Arrange & Act & Assert - IEnumerable operationImports = null; - ExceptionAssert.ThrowsArgumentNull(() => operationImports.SplitOperationImports(), "operationImports"); - } + IDictionary parametersImport = parameters = function.GetFunctionParamterMappings(); + + // Assert + Assert.Equal(parameters, parametersImport); + } + + [Fact] + public void SplitOperationImports_ThrowsArgumentNull_OperationImports() + { + // Arrange & Act & Assert + IEnumerable operationImports = null; + ExceptionAssert.ThrowsArgumentNull(() => operationImports.SplitOperationImports(), "operationImports"); + } - [Fact] - public void SplitOperationImports_Works_OperationImports() + [Fact] + public void SplitOperationImports_Works_OperationImports() + { + // Arrange + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + IEdmTypeReference strType = EdmCoreModel.Instance.GetString(false); + EdmFunction function = new EdmFunction("NS", "Function", strType); + EdmFunctionImport functionImport = new EdmFunctionImport(container, "functionImport", function); + EdmAction action = new EdmAction("NS", "Action", strType); + EdmActionImport actionImport = new EdmActionImport(container, "actionImport", action); + + IEnumerable operationImports = new List { - // Arrange - EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); - IEdmTypeReference strType = EdmCoreModel.Instance.GetString(false); - EdmFunction function = new EdmFunction("NS", "Function", strType); - EdmFunctionImport functionImport = new EdmFunctionImport(container, "functionImport", function); - EdmAction action = new EdmAction("NS", "Action", strType); - EdmActionImport actionImport = new EdmActionImport(container, "actionImport", action); - - IEnumerable operationImports = new List - { - functionImport, - actionImport - }; + functionImport, + actionImport + }; - // Act - (var actionImports, var functionImports) = operationImports.SplitOperationImports(); + // Act + (var actionImports, var functionImports) = operationImports.SplitOperationImports(); - // Assert - Assert.Same(actionImport, Assert.Single(actionImports)); - Assert.Same(functionImport, Assert.Single(functionImports)); - } + // Assert + Assert.Same(actionImport, Assert.Single(actionImports)); + Assert.Same(functionImport, Assert.Single(functionImports)); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/OperationLinkBuilderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/OperationLinkBuilderTests.cs index 8159e736a..85fc65ed4 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/OperationLinkBuilderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/OperationLinkBuilderTests.cs @@ -11,76 +11,75 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class OperationLinkBuilderTests { - public class OperationLinkBuilderTests + [Fact] + public void CtorOperationLinkBuilder_ThrowsArgumentNull_LinkFactory() { - [Fact] - public void CtorOperationLinkBuilder_ThrowsArgumentNull_LinkFactory() - { - // Arrange & Act & Assert - Func linkFactory = null; - ExceptionAssert.ThrowsArgumentNull(() => new OperationLinkBuilder(linkFactory, false), "linkFactory"); + // Arrange & Act & Assert + Func linkFactory = null; + ExceptionAssert.ThrowsArgumentNull(() => new OperationLinkBuilder(linkFactory, false), "linkFactory"); - // Arrange & Act & Assert - Func linkSetFactory = null; - ExceptionAssert.ThrowsArgumentNull(() => new OperationLinkBuilder(linkSetFactory, false), "linkFactory"); - } + // Arrange & Act & Assert + Func linkSetFactory = null; + ExceptionAssert.ThrowsArgumentNull(() => new OperationLinkBuilder(linkSetFactory, false), "linkFactory"); + } - [Fact] - public void CtorOperationLinkBuilder_SetsProperties() - { - // Arrange & Act & Assert - Func linkFactory = r => null; - OperationLinkBuilder linkBuilder = new OperationLinkBuilder(linkFactory, false); + [Fact] + public void CtorOperationLinkBuilder_SetsProperties() + { + // Arrange & Act & Assert + Func linkFactory = r => null; + OperationLinkBuilder linkBuilder = new OperationLinkBuilder(linkFactory, false); - // Act & Assert - Assert.Same(linkFactory, linkBuilder.LinkFactory); - Assert.False(linkBuilder.FollowsConventions); - Assert.Null(linkBuilder.FeedLinkFactory); + // Act & Assert + Assert.Same(linkFactory, linkBuilder.LinkFactory); + Assert.False(linkBuilder.FollowsConventions); + Assert.Null(linkBuilder.FeedLinkFactory); - // Arrange - Func linkSetFactory = r => null; - OperationLinkBuilder linkSetBuilder = new OperationLinkBuilder(linkSetFactory, true); + // Arrange + Func linkSetFactory = r => null; + OperationLinkBuilder linkSetBuilder = new OperationLinkBuilder(linkSetFactory, true); - // Act & Assert - Assert.Null(linkSetBuilder.LinkFactory); - Assert.True(linkSetBuilder.FollowsConventions); - Assert.Same(linkSetFactory, linkSetBuilder.FeedLinkFactory); - } + // Act & Assert + Assert.Null(linkSetBuilder.LinkFactory); + Assert.True(linkSetBuilder.FollowsConventions); + Assert.Same(linkSetFactory, linkSetBuilder.FeedLinkFactory); + } - [Fact] - public void BuildLinkOperationLinkBuilder_Works_ResourceContext() - { - // Arrange - Uri uri = new Uri("http://any"); - Func linkFactory = r => uri; - OperationLinkBuilder linkBuilder = new OperationLinkBuilder(linkFactory, false); + [Fact] + public void BuildLinkOperationLinkBuilder_Works_ResourceContext() + { + // Arrange + Uri uri = new Uri("http://any"); + Func linkFactory = r => uri; + OperationLinkBuilder linkBuilder = new OperationLinkBuilder(linkFactory, false); - // Act & Assert - ResourceContext context = new ResourceContext(); - Assert.Same(uri, linkBuilder.BuildLink(context)); + // Act & Assert + ResourceContext context = new ResourceContext(); + Assert.Same(uri, linkBuilder.BuildLink(context)); - // Act & Assert - ResourceSetContext setContext = new ResourceSetContext(); - Assert.Null(linkBuilder.BuildLink(setContext)); - } + // Act & Assert + ResourceSetContext setContext = new ResourceSetContext(); + Assert.Null(linkBuilder.BuildLink(setContext)); + } - [Fact] - public void BuildLinkOperationLinkBuilder_Works_ResourceSetContext() - { - // Arrange - Uri uri = new Uri("http://any"); - Func linkFactory = r => uri; - OperationLinkBuilder linkBuilder = new OperationLinkBuilder(linkFactory, false); + [Fact] + public void BuildLinkOperationLinkBuilder_Works_ResourceSetContext() + { + // Arrange + Uri uri = new Uri("http://any"); + Func linkFactory = r => uri; + OperationLinkBuilder linkBuilder = new OperationLinkBuilder(linkFactory, false); - // Act & Assert - ResourceContext context = new ResourceContext(); - Assert.Null(linkBuilder.BuildLink(context)); + // Act & Assert + ResourceContext context = new ResourceContext(); + Assert.Null(linkBuilder.BuildLink(context)); - // Act & Assert - ResourceSetContext setContext = new ResourceSetContext(); - Assert.Same(uri, linkBuilder.BuildLink(setContext)); - } + // Act & Assert + ResourceSetContext setContext = new ResourceSetContext(); + Assert.Same(uri, linkBuilder.BuildLink(setContext)); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/OrderByClauseHelpersTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/OrderByClauseHelpersTests.cs index 7038583f3..a7879474d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/OrderByClauseHelpersTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/OrderByClauseHelpersTests.cs @@ -12,95 +12,94 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class OrderByClauseHelpersTests { - public class OrderByClauseHelpersTests + [Fact] + public void OrderByClause_ToList_WorksForNull() + { + // Arrange + OrderByClause clause = null; + + // Act + IList list = clause.ToList(); + + // Assert + Assert.Empty(list); + } + + [Fact] + public void OrderByClause_ToList_WorksForMultiple() + { + // Arrange + SingleValueNode valueNode = new Mock().Object; + RangeVariable rangeVariable = new Mock().Object; + + OrderByClause clause1 = new OrderByClause(null, valueNode, OrderByDirection.Ascending, rangeVariable); + OrderByClause clause = new OrderByClause(clause1, valueNode, OrderByDirection.Ascending, rangeVariable); + + // Act + IList list = clause.ToList(); + + // Assert + Assert.NotEmpty(list); + Assert.Equal(2, list.Count); + } + + [Fact] + public void OrderByClause_IsTopLevelSingleProperty_ReturnsFalseForNull() + { + // Arrange + OrderByClause clause = null; + + // Act + bool isTop = clause.IsTopLevelSingleProperty(out _, out _); + + // Assert + Assert.False(isTop); + } + + [Fact] + public void OrderByClause_IsTopLevelSingleProperty_ReturnsFalseForNonTopLevelPropertyNode() + { + // Arrange + SingleValueNode valueNode = new Mock().Object; + RangeVariable rangeVariable = new Mock().Object; + OrderByClause clause = new OrderByClause(null, valueNode, OrderByDirection.Ascending, rangeVariable); + + // Act + bool isTop = clause.IsTopLevelSingleProperty(out _, out _); + + // Assert + Assert.False(isTop); + } + + [Fact] + public void OrderByClause_IsTopLevelSingleProperty_ReturnsFalseForTopLevelPropertyNode() { - [Fact] - public void OrderByClause_ToList_WorksForNull() - { - // Arrange - OrderByClause clause = null; - - // Act - IList list = clause.ToList(); - - // Assert - Assert.Empty(list); - } - - [Fact] - public void OrderByClause_ToList_WorksForMultiple() - { - // Arrange - SingleValueNode valueNode = new Mock().Object; - RangeVariable rangeVariable = new Mock().Object; - - OrderByClause clause1 = new OrderByClause(null, valueNode, OrderByDirection.Ascending, rangeVariable); - OrderByClause clause = new OrderByClause(clause1, valueNode, OrderByDirection.Ascending, rangeVariable); - - // Act - IList list = clause.ToList(); - - // Assert - Assert.NotEmpty(list); - Assert.Equal(2, list.Count); - } - - [Fact] - public void OrderByClause_IsTopLevelSingleProperty_ReturnsFalseForNull() - { - // Arrange - OrderByClause clause = null; - - // Act - bool isTop = clause.IsTopLevelSingleProperty(out _, out _); - - // Assert - Assert.False(isTop); - } - - [Fact] - public void OrderByClause_IsTopLevelSingleProperty_ReturnsFalseForNonTopLevelPropertyNode() - { - // Arrange - SingleValueNode valueNode = new Mock().Object; - RangeVariable rangeVariable = new Mock().Object; - OrderByClause clause = new OrderByClause(null, valueNode, OrderByDirection.Ascending, rangeVariable); - - // Act - bool isTop = clause.IsTopLevelSingleProperty(out _, out _); - - // Assert - Assert.False(isTop); - } - - [Fact] - public void OrderByClause_IsTopLevelSingleProperty_ReturnsFalseForTopLevelPropertyNode() - { - // Arrange - Mock type = new Mock(); - Mock definition = new Mock(); - definition.Setup(t => t.TypeKind).Returns(EdmTypeKind.Entity); - type.Setup(t => t.Definition).Returns(definition.Object); - - ResourceRangeVariable variable = new ResourceRangeVariable("$it", type.Object, navigationSource: null); - SingleValueNode source = new ResourceRangeVariableReferenceNode("$it", variable); - Mock property = new Mock(); - property.Setup(p => p.Name).Returns("Top"); - property.Setup(p => p.Type).Returns(type.Object); - property.Setup(p => p.PropertyKind).Returns(EdmPropertyKind.Structural); - SingleValueNode valueNode = new SingleValuePropertyAccessNode(source, property.Object); - RangeVariable rangeVariable = new Mock().Object; - OrderByClause clause = new OrderByClause(null, valueNode, OrderByDirection.Ascending, rangeVariable); - - // Act - bool isTop = clause.IsTopLevelSingleProperty(out IEdmProperty actualProperty, out string name); - - // Assert - Assert.True(isTop); - Assert.Same(property.Object, actualProperty); - Assert.Equal("Top", name); - } + // Arrange + Mock type = new Mock(); + Mock definition = new Mock(); + definition.Setup(t => t.TypeKind).Returns(EdmTypeKind.Entity); + type.Setup(t => t.Definition).Returns(definition.Object); + + ResourceRangeVariable variable = new ResourceRangeVariable("$it", type.Object, navigationSource: null); + SingleValueNode source = new ResourceRangeVariableReferenceNode("$it", variable); + Mock property = new Mock(); + property.Setup(p => p.Name).Returns("Top"); + property.Setup(p => p.Type).Returns(type.Object); + property.Setup(p => p.PropertyKind).Returns(EdmPropertyKind.Structural); + SingleValueNode valueNode = new SingleValuePropertyAccessNode(source, property.Object); + RangeVariable rangeVariable = new Mock().Object; + OrderByClause clause = new OrderByClause(null, valueNode, OrderByDirection.Ascending, rangeVariable); + + // Act + bool isTop = clause.IsTopLevelSingleProperty(out IEdmProperty actualProperty, out string name); + + // Assert + Assert.True(isTop); + Assert.Same(property.Object, actualProperty); + Assert.Equal("Top", name); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/SelectModelPathTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/SelectModelPathTests.cs index 51e9cd542..9172ab686 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/SelectModelPathTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/SelectModelPathTests.cs @@ -13,81 +13,80 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class SelectModelPathTests { - public class SelectModelPathTests - { - private IEdmStructuralProperty _homeAddress; - private IEdmStructuralProperty _city; - private IEdmNavigationProperty _relatedNavProperty; + private IEdmStructuralProperty _homeAddress; + private IEdmStructuralProperty _city; + private IEdmNavigationProperty _relatedNavProperty; - public SelectModelPathTests() - { - // Address is an open complex type - var addressType = new EdmComplexType("NS", "Address"); - addressType.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); - _city = addressType.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + public SelectModelPathTests() + { + // Address is an open complex type + var addressType = new EdmComplexType("NS", "Address"); + addressType.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + _city = addressType.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); - EdmEntityType customerType = new EdmEntityType("NS", "Customer"); - customerType.AddKeys(customerType.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - _homeAddress = customerType.AddStructuralProperty("HomeAddress", new EdmComplexTypeReference(addressType, false)); + EdmEntityType customerType = new EdmEntityType("NS", "Customer"); + customerType.AddKeys(customerType.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + _homeAddress = customerType.AddStructuralProperty("HomeAddress", new EdmComplexTypeReference(addressType, false)); - _relatedNavProperty = addressType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "RelatedCustomers", - Target = customerType, - TargetMultiplicity = EdmMultiplicity.Many - }); - } + _relatedNavProperty = addressType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo + { + Name = "RelatedCustomers", + Target = customerType, + TargetMultiplicity = EdmMultiplicity.Many + }); + } - [Fact] - public void Ctor_SelectModelPath_WithBasicElements_SetsProperties() + [Fact] + public void Ctor_SelectModelPath_WithBasicElements_SetsProperties() + { + // Arrange + IList elements = new List { - // Arrange - IList elements = new List - { - _homeAddress - }; + _homeAddress + }; - // Act - SelectModelPath selectPath = new SelectModelPath(elements); + // Act + SelectModelPath selectPath = new SelectModelPath(elements); - // Assert - Assert.Equal("HomeAddress", selectPath.SelectPath); - } + // Assert + Assert.Equal("HomeAddress", selectPath.SelectPath); + } - [Fact] - public void Ctor_SelectModelPath_WithOtherElements_SetsProperties() + [Fact] + public void Ctor_SelectModelPath_WithOtherElements_SetsProperties() + { + // Arrange + IList elements = new List { - // Arrange - IList elements = new List - { - _homeAddress, - _city - }; + _homeAddress, + _city + }; - // Act - SelectModelPath selectPath = new SelectModelPath(elements); + // Act + SelectModelPath selectPath = new SelectModelPath(elements); - // Assert - Assert.Equal("HomeAddress/City", selectPath.SelectPath); - } + // Assert + Assert.Equal("HomeAddress/City", selectPath.SelectPath); + } - [Fact] - public void Ctor_SelectModelPath_Throws_EmtpyElement() + [Fact] + public void Ctor_SelectModelPath_Throws_EmtpyElement() + { + // Arrange + IList elements = new List { - // Arrange - IList elements = new List - { - _relatedNavProperty - }; + _relatedNavProperty + }; - // Act - Action test = () => new SelectModelPath(elements); + // Act + Action test = () => new SelectModelPath(elements); - // Assert - ExceptionAssert.Throws(test, - "A segment 'EdmNavigationProperty' within the select or expand query option is not supported."); - } + // Assert + ExceptionAssert.Throws(test, + "A segment 'EdmNavigationProperty' within the select or expand query option is not supported."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/SelfLinkBuilderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/SelfLinkBuilderTests.cs index b9a014d36..55a9c6221 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/SelfLinkBuilderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/SelfLinkBuilderTests.cs @@ -11,16 +11,15 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class SelfLinkBuilderTests { - public class SelfLinkBuilderTests + [Fact] + public void Ctor_ThrowsArgumentNull_LinkFactory() { - [Fact] - public void Ctor_ThrowsArgumentNull_LinkFactory() - { - // Arrange & Act & Assert - Func linkFactory = null; - ExceptionAssert.ThrowsArgumentNull(() => new SelfLinkBuilder(linkFactory, false), "linkFactory"); - } + // Arrange & Act & Assert + Func linkFactory = null; + ExceptionAssert.ThrowsArgumentNull(() => new SelfLinkBuilder(linkFactory, false), "linkFactory"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Edm/TypeCacheItemTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Edm/TypeCacheItemTests.cs index 241330f82..980973a03 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Edm/TypeCacheItemTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Edm/TypeCacheItemTests.cs @@ -12,113 +12,112 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Edm +namespace Microsoft.AspNetCore.OData.Tests.Edm; + +public class TypeCacheItemTests { - public class TypeCacheItemTests + #region TryFindEdmType + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(string))] + [InlineData(typeof(TypeCacheItemTests))] + public void AddAndFindEdmType_Returns_CachedInstance(Type testType) { - #region TryFindEdmType - [Theory] - [InlineData(typeof(int))] - [InlineData(typeof(string))] - [InlineData(typeof(TypeCacheItemTests))] - public void AddAndFindEdmType_Returns_CachedInstance(Type testType) - { - // Arrange - IEdmTypeReference edmType = new Mock().Object; - TypeCacheItem cache = new TypeCacheItem(); + // Arrange + IEdmTypeReference edmType = new Mock().Object; + TypeCacheItem cache = new TypeCacheItem(); - // Act - bool found = cache.TryFindEdmType(testType, out _); - Assert.False(found); // not found + // Act + bool found = cache.TryFindEdmType(testType, out _); + Assert.False(found); // not found - cache.AddClrToEdmMap(testType, edmType); - found = cache.TryFindEdmType(testType, out IEdmTypeReference acutal); + cache.AddClrToEdmMap(testType, edmType); + found = cache.TryFindEdmType(testType, out IEdmTypeReference acutal); - // Assert - Assert.True(found); - Assert.Same(edmType, acutal); - } + // Assert + Assert.True(found); + Assert.Same(edmType, acutal); + } - [Fact] - public void AddClrToEdmMap_Cached_OnlyOneInstance() + [Fact] + public void AddClrToEdmMap_Cached_OnlyOneInstance() + { + // Arrange + TypeCacheItem cache = new TypeCacheItem(); + Action cacheCallAndVerify = () => { - // Arrange - TypeCacheItem cache = new TypeCacheItem(); - Action cacheCallAndVerify = () => - { - IEdmTypeReference edmType = new Mock().Object; - cache.AddClrToEdmMap(typeof(TypeCacheItemTests), edmType); - Assert.Single(cache.ClrToEdmTypeCache); - }; - - // Act & Assert - cacheCallAndVerify(); + IEdmTypeReference edmType = new Mock().Object; + cache.AddClrToEdmMap(typeof(TypeCacheItemTests), edmType); + Assert.Single(cache.ClrToEdmTypeCache); + }; - // 5 is a magic number, it doesn't matter, just want to call it multiple times. - for (int i = 0; i < 5; i++) - { - cacheCallAndVerify(); - } + // Act & Assert + cacheCallAndVerify(); + // 5 is a magic number, it doesn't matter, just want to call it multiple times. + for (int i = 0; i < 5; i++) + { cacheCallAndVerify(); } - #endregion + cacheCallAndVerify(); + } - #region TryFindClrType + #endregion - [Theory] - [InlineData(typeof(int))] - [InlineData(typeof(string))] - [InlineData(typeof(TypeCacheItemTests))] - public void AddAndGetClrType_Returns_CorrectType(Type testType) - { - // Arrange - IEdmType edmType = new Mock().Object; - TypeCacheItem cache = new TypeCacheItem(); + #region TryFindClrType - // Act - bool found = cache.TryFindClrType(edmType, true, out _); - Assert.False(found); + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(string))] + [InlineData(typeof(TypeCacheItemTests))] + public void AddAndGetClrType_Returns_CorrectType(Type testType) + { + // Arrange + IEdmType edmType = new Mock().Object; + TypeCacheItem cache = new TypeCacheItem(); - cache.AddEdmToClrMap(edmType, true, testType); + // Act + bool found = cache.TryFindClrType(edmType, true, out _); + Assert.False(found); - found = cache.TryFindClrType(edmType, true, out Type actualType); + cache.AddEdmToClrMap(edmType, true, testType); - // Act & Assert - Assert.True(found); - Assert.Same(testType, actualType); - } + found = cache.TryFindClrType(edmType, true, out Type actualType); + + // Act & Assert + Assert.True(found); + Assert.Same(testType, actualType); + } + + [Fact] + public void AddEdmToClrMap_Cached_OnlyOneInstance() + { + // Arrange + TypeCacheItem cache = new TypeCacheItem(); + IEdmType edmType = new Mock().Object; - [Fact] - public void AddEdmToClrMap_Cached_OnlyOneInstance() + Action cacheCallAndVerify = () => { - // Arrange - TypeCacheItem cache = new TypeCacheItem(); - IEdmType edmType = new Mock().Object; - - Action cacheCallAndVerify = () => - { - cache.AddEdmToClrMap(edmType, true, typeof(int?)); - cache.AddEdmToClrMap(edmType, false, typeof(int)); - - KeyValuePair item = Assert.Single(cache.EdmToClrTypeCache); - Assert.Same(edmType, item.Key); - Assert.Equal(typeof(int), item.Value.Item1); - Assert.Equal(typeof(int?), item.Value.Item2); - }; - - // Act & Assert - cacheCallAndVerify(); + cache.AddEdmToClrMap(edmType, true, typeof(int?)); + cache.AddEdmToClrMap(edmType, false, typeof(int)); + + KeyValuePair item = Assert.Single(cache.EdmToClrTypeCache); + Assert.Same(edmType, item.Key); + Assert.Equal(typeof(int), item.Value.Item1); + Assert.Equal(typeof(int?), item.Value.Item2); + }; - // 5 is a magic number, it doesn't matter, just want to call it multiple times. - for (int i = 0; i < 5; i++) - { - cacheCallAndVerify(); - } + // Act & Assert + cacheCallAndVerify(); + // 5 is a magic number, it doesn't matter, just want to call it multiple times. + for (int i = 0; i < 5; i++) + { cacheCallAndVerify(); } - #endregion + + cacheCallAndVerify(); } + #endregion } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/ActionDescriptorExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/ActionDescriptorExtensionsTests.cs index f60c9b134..3d6c2aacb 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/ActionDescriptorExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/ActionDescriptorExtensionsTests.cs @@ -14,78 +14,77 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +public class ActionDescriptorExtensionsTests { - public class ActionDescriptorExtensionsTests + [Fact] + public void GetEdmModel_ThrowsArgumentNull_ActionDescriptor() { - [Fact] - public void GetEdmModel_ThrowsArgumentNull_ActionDescriptor() - { - // Arrange & Act & Assert - ActionDescriptor actionDescriptor = null; - ExceptionAssert.ThrowsArgumentNull(() => actionDescriptor.GetEdmModel(null, null), "actionDescriptor"); - } + // Arrange & Act & Assert + ActionDescriptor actionDescriptor = null; + ExceptionAssert.ThrowsArgumentNull(() => actionDescriptor.GetEdmModel(null, null), "actionDescriptor"); + } - [Fact] - public void GetEdmModel_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - ActionDescriptor actionDescriptor = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => actionDescriptor.GetEdmModel(null, null), "request"); - } + [Fact] + public void GetEdmModel_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + ActionDescriptor actionDescriptor = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => actionDescriptor.GetEdmModel(null, null), "request"); + } - [Fact] - public void GetEdmModel_ThrowsArgumentNull_EntityClrType() - { - // Arrange & Act & Assert - ActionDescriptor actionDescriptor = new Mock().Object; - HttpRequest request = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => actionDescriptor.GetEdmModel(request, null), "entityClrType"); - } + [Fact] + public void GetEdmModel_ThrowsArgumentNull_EntityClrType() + { + // Arrange & Act & Assert + ActionDescriptor actionDescriptor = new Mock().Object; + HttpRequest request = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => actionDescriptor.GetEdmModel(request, null), "entityClrType"); + } - [Fact] - public void GetEdmModel_Returns_CachedModel() - { - // Arrange - IEdmModel model = new Mock().Object; - ActionDescriptor actionDescriptor = new ActionDescriptor(); - string key = "Microsoft.AspNetCore.OData.Model+" + typeof(ActionCustomer).FullName; - actionDescriptor.Properties.Add(key, model); - HttpRequest request = new Mock().Object; + [Fact] + public void GetEdmModel_Returns_CachedModel() + { + // Arrange + IEdmModel model = new Mock().Object; + ActionDescriptor actionDescriptor = new ActionDescriptor(); + string key = "Microsoft.AspNetCore.OData.Model+" + typeof(ActionCustomer).FullName; + actionDescriptor.Properties.Add(key, model); + HttpRequest request = new Mock().Object; - // Act - IEdmModel actual = actionDescriptor.GetEdmModel(request, typeof(ActionCustomer)); + // Act + IEdmModel actual = actionDescriptor.GetEdmModel(request, typeof(ActionCustomer)); - // Assert - Assert.Same(model, actual); - } + // Assert + Assert.Same(model, actual); + } - [Fact] - public void GetEdmModel_Returns_BuiltEdmModel() - { - // Arrange - string key = "Microsoft.AspNetCore.OData.Model+" + typeof(ActionCustomer).FullName; - ActionDescriptor actionDescriptor = new ActionDescriptor(); - Assert.False(actionDescriptor.Properties.TryGetValue(key, out _)); // Guard + [Fact] + public void GetEdmModel_Returns_BuiltEdmModel() + { + // Arrange + string key = "Microsoft.AspNetCore.OData.Model+" + typeof(ActionCustomer).FullName; + ActionDescriptor actionDescriptor = new ActionDescriptor(); + Assert.False(actionDescriptor.Properties.TryGetValue(key, out _)); // Guard - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - IServiceCollection services = new ServiceCollection(); - context.RequestServices = services.BuildServiceProvider(); + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; + IServiceCollection services = new ServiceCollection(); + context.RequestServices = services.BuildServiceProvider(); - // Act - IEdmModel actual = actionDescriptor.GetEdmModel(request, typeof(ActionCustomer)); + // Act + IEdmModel actual = actionDescriptor.GetEdmModel(request, typeof(ActionCustomer)); - // Assert - Assert.NotNull(actual); - actionDescriptor.Properties.TryGetValue(key, out object cachedModel); + // Assert + Assert.NotNull(actual); + actionDescriptor.Properties.TryGetValue(key, out object cachedModel); - Assert.Same(cachedModel, actual); - } + Assert.Same(cachedModel, actual); + } - private class ActionCustomer - { - public int Id { get; set; } - } + private class ActionCustomer + { + public int Id { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/ActionModelExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/ActionModelExtensionsTests.cs index 655ead2fb..4148bf005 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/ActionModelExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/ActionModelExtensionsTests.cs @@ -20,148 +20,147 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +public class ActionModelExtensionsTests { - public class ActionModelExtensionsTests + private static MethodInfo _methodInfo = typeof(TestController).GetMethod("Index"); + + [Fact] + public void IsNonODataAction_ThrowsArgumentNull_Action() { - private static MethodInfo _methodInfo = typeof(TestController).GetMethod("Index"); + // Arrange & Act & Assert + ActionModel action = null; + ExceptionAssert.ThrowsArgumentNull(() => action.IsODataIgnored(), "action"); + } - [Fact] - public void IsNonODataAction_ThrowsArgumentNull_Action() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void IsNonODataAction_ReturnsAsExpected(bool expected) + { + // Arrange + List attributes = new List(); + if (expected) { - // Arrange & Act & Assert - ActionModel action = null; - ExceptionAssert.ThrowsArgumentNull(() => action.IsODataIgnored(), "action"); + attributes.Add(new ODataIgnoredAttribute()); } + ActionModel action = new ActionModel(_methodInfo, attributes); - [Theory] - [InlineData(true)] - [InlineData(false)] - public void IsNonODataAction_ReturnsAsExpected(bool expected) - { - // Arrange - List attributes = new List(); - if (expected) - { - attributes.Add(new ODataIgnoredAttribute()); - } - ActionModel action = new ActionModel(_methodInfo, attributes); - - // Act - bool actual = action.IsODataIgnored(); - - // Assert - Assert.Equal(expected, actual); - } + // Act + bool actual = action.IsODataIgnored(); - [Fact] - public void HasParameter_ThrowsArgumentNull_Action() - { - // Arrange & Act & Assert - ActionModel action = null; - ExceptionAssert.ThrowsArgumentNull(() => action.HasParameter("key"), "action"); - } + // Assert + Assert.Equal(expected, actual); + } - [Fact] - public void HasParameter_Returns_BooleanAsExpected() - { - // Arrange - ActionModel action = _methodInfo.BuildActionModel(); + [Fact] + public void HasParameter_ThrowsArgumentNull_Action() + { + // Arrange & Act & Assert + ActionModel action = null; + ExceptionAssert.ThrowsArgumentNull(() => action.HasParameter("key"), "action"); + } - // Act & Assert - Assert.False(action.HasParameter("key")); - Assert.True(action.HasParameter("id")); - Assert.False(action.HasParameter("id")); - } + [Fact] + public void HasParameter_Returns_BooleanAsExpected() + { + // Arrange + ActionModel action = _methodInfo.BuildActionModel(); - [Fact] - public void GetAttribute_ThrowsArgumentNull_Action() - { - // Arrange & Act & Assert - ActionModel action = null; - ExceptionAssert.ThrowsArgumentNull(() => action.GetAttribute(), "action"); - } + // Act & Assert + Assert.False(action.HasParameter("key")); + Assert.True(action.HasParameter("id")); + Assert.False(action.HasParameter("id")); + } - [Fact] - public void HasODataKeyParameter_ThrowsArgumentNull_Action() - { - // Arrange & Act & Assert - ActionModel action = null; - ExceptionAssert.ThrowsArgumentNull(() => action.HasODataKeyParameter(entityType: null), "action"); - } + [Fact] + public void GetAttribute_ThrowsArgumentNull_Action() + { + // Arrange & Act & Assert + ActionModel action = null; + ExceptionAssert.ThrowsArgumentNull(() => action.GetAttribute(), "action"); + } - [Fact] - public void HasODataKeyParameter_ThrowsArgumentNull_EntityType() - { - // Arrange & Act & Assert - ActionModel action = new ActionModel(_methodInfo, new List()); - ExceptionAssert.ThrowsArgumentNull(() => action.HasODataKeyParameter(entityType: null), "entityType"); - } + [Fact] + public void HasODataKeyParameter_ThrowsArgumentNull_Action() + { + // Arrange & Act & Assert + ActionModel action = null; + ExceptionAssert.ThrowsArgumentNull(() => action.HasODataKeyParameter(entityType: null), "action"); + } - [Fact] - public void AddSelector_ThrowsArgumentNull_ForInputParameter() - { - // Arrange & Act & Assert - ActionModel action = null; - ExceptionAssert.ThrowsArgumentNull(() => action.AddSelector(null, null, null, null), "action"); - - // Arrange & Act & Assert - MethodInfo methodInfo = typeof(TestController).GetMethod("Get"); - action = methodInfo.BuildActionModel(); - ExceptionAssert.ThrowsArgumentNullOrEmpty(() => action.AddSelector(null, null, null, null), "httpMethods"); - - // Arrange & Act & Assert - string httpMethods = "get"; - ExceptionAssert.ThrowsArgumentNull(() => action.AddSelector(httpMethods, null, null, null), "model"); - - // Arrange & Act & Assert - IEdmModel model = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => action.AddSelector(httpMethods, null, model, null), "path"); - } + [Fact] + public void HasODataKeyParameter_ThrowsArgumentNull_EntityType() + { + // Arrange & Act & Assert + ActionModel action = new ActionModel(_methodInfo, new List()); + ExceptionAssert.ThrowsArgumentNull(() => action.HasODataKeyParameter(entityType: null), "entityType"); + } - [Theory] - [InlineData(typeof(TestController), "Get", true)] - [InlineData(typeof(TestController), "Create", true)] - [InlineData(typeof(TestController), "Index", false)] - [InlineData(typeof(CorsTestController), "Index", true)] - public void AddSelector_AddsCors_ForActionsWithCorsAttribute(Type controllerType, string actionName, bool expectedCorsSetting) - { - // Arrange - IEdmModel model = new Mock().Object; - MethodInfo methodInfo = controllerType.GetMethod(actionName); - ActionModel action = methodInfo.BuildActionModel(); - action.Controller = ControllerModelHelpers.BuildControllerModel(controllerType); + [Fact] + public void AddSelector_ThrowsArgumentNull_ForInputParameter() + { + // Arrange & Act & Assert + ActionModel action = null; + ExceptionAssert.ThrowsArgumentNull(() => action.AddSelector(null, null, null, null), "action"); + + // Arrange & Act & Assert + MethodInfo methodInfo = typeof(TestController).GetMethod("Get"); + action = methodInfo.BuildActionModel(); + ExceptionAssert.ThrowsArgumentNullOrEmpty(() => action.AddSelector(null, null, null, null), "httpMethods"); + + // Arrange & Act & Assert + string httpMethods = "get"; + ExceptionAssert.ThrowsArgumentNull(() => action.AddSelector(httpMethods, null, null, null), "model"); + + // Arrange & Act & Assert + IEdmModel model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => action.AddSelector(httpMethods, null, model, null), "path"); + } + + [Theory] + [InlineData(typeof(TestController), "Get", true)] + [InlineData(typeof(TestController), "Create", true)] + [InlineData(typeof(TestController), "Index", false)] + [InlineData(typeof(CorsTestController), "Index", true)] + public void AddSelector_AddsCors_ForActionsWithCorsAttribute(Type controllerType, string actionName, bool expectedCorsSetting) + { + // Arrange + IEdmModel model = new Mock().Object; + MethodInfo methodInfo = controllerType.GetMethod(actionName); + ActionModel action = methodInfo.BuildActionModel(); + action.Controller = ControllerModelHelpers.BuildControllerModel(controllerType); - // Act - action.AddSelector("Get", string.Empty, model, new ODataPathTemplate(CountSegmentTemplate.Instance)); + // Act + action.AddSelector("Get", string.Empty, model, new ODataPathTemplate(CountSegmentTemplate.Instance)); - // Assert - SelectorModel newSelector = action.Selectors.FirstOrDefault(); - Assert.NotNull(newSelector); - HttpMethodMetadata httpMethodMetadata = newSelector.EndpointMetadata.OfType().FirstOrDefault(); - Assert.NotNull(httpMethodMetadata); - Assert.Equal(httpMethodMetadata.AcceptCorsPreflight, expectedCorsSetting); - } + // Assert + SelectorModel newSelector = action.Selectors.FirstOrDefault(); + Assert.NotNull(newSelector); + HttpMethodMetadata httpMethodMetadata = newSelector.EndpointMetadata.OfType().FirstOrDefault(); + Assert.NotNull(httpMethodMetadata); + Assert.Equal(httpMethodMetadata.AcceptCorsPreflight, expectedCorsSetting); } +} - internal class TestController - { - public void Index(int id) - { } +internal class TestController +{ + public void Index(int id) + { } - [EnableCors] - public void Get(int key) - { } + [EnableCors] + public void Get(int key) + { } - [DisableCors] - public void Create() - { } - } + [DisableCors] + public void Create() + { } +} - [EnableCors] - internal class CorsTestController - { - public void Index(int id) - { } - } +[EnableCors] +internal class CorsTestController +{ + public void Index(int id) + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/ActionModelHelpers.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/ActionModelHelpers.cs index 2ca45244e..e12b2b753 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/ActionModelHelpers.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/ActionModelHelpers.cs @@ -11,104 +11,103 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +internal static class ActionModelHelpers { - internal static class ActionModelHelpers + /// + /// Build the actio nmodel using the method info. + /// + /// The input method info. + /// The action model. + public static ActionModel BuildActionModel(this MethodInfo methodInfo) { - /// - /// Build the actio nmodel using the method info. - /// - /// The input method info. - /// The action model. - public static ActionModel BuildActionModel(this MethodInfo methodInfo) - { - object[] attributes = methodInfo.GetCustomAttributes(inherit: true); - ActionModel actionModel = new ActionModel(methodInfo, attributes); + object[] attributes = methodInfo.GetCustomAttributes(inherit: true); + ActionModel actionModel = new ActionModel(methodInfo, attributes); - foreach (var parameterInfo in methodInfo.GetParameters()) + foreach (var parameterInfo in methodInfo.GetParameters()) + { + object[] paramAttributes = parameterInfo.GetCustomAttributes(inherit: true); + ParameterModel parameterModel = new ParameterModel(parameterInfo, paramAttributes) { - object[] paramAttributes = parameterInfo.GetCustomAttributes(inherit: true); - ParameterModel parameterModel = new ParameterModel(parameterInfo, paramAttributes) - { - ParameterName = parameterInfo.Name, - }; + ParameterName = parameterInfo.Name, + }; - actionModel.Parameters.Add(parameterModel); - } - - return actionModel; + actionModel.Parameters.Add(parameterModel); } - /// - /// Copied from ASP.NET Core and make some changes. - /// Returns true if the is an action. Otherwise false. - /// - /// The . - /// The . - /// true if the is an action. Otherwise false. - /// - /// Override this method to provide custom logic to determine which methods are considered actions. - /// - public static bool IsAction(this MethodInfo methodInfo) - { - Assert.NotNull(methodInfo); - - // The SpecialName bit is set to flag members that are treated in a special way by some compilers - // (such as property accessors and operator overloading methods). - if (methodInfo.IsSpecialName) - { - return false; - } - - if (methodInfo.IsDefined(typeof(NonActionAttribute))) - { - return false; - } + return actionModel; + } - // Overridden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid. - if (methodInfo.GetBaseDefinition().DeclaringType == typeof(object)) - { - return false; - } + /// + /// Copied from ASP.NET Core and make some changes. + /// Returns true if the is an action. Otherwise false. + /// + /// The . + /// The . + /// true if the is an action. Otherwise false. + /// + /// Override this method to provide custom logic to determine which methods are considered actions. + /// + public static bool IsAction(this MethodInfo methodInfo) + { + Assert.NotNull(methodInfo); - // Dispose method implemented from IDisposable is not valid - if (IsIDisposableMethod(methodInfo)) - { - return false; - } + // The SpecialName bit is set to flag members that are treated in a special way by some compilers + // (such as property accessors and operator overloading methods). + if (methodInfo.IsSpecialName) + { + return false; + } - if (methodInfo.IsStatic) - { - return false; - } + if (methodInfo.IsDefined(typeof(NonActionAttribute))) + { + return false; + } - if (methodInfo.IsAbstract) - { - return false; - } + // Overridden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid. + if (methodInfo.GetBaseDefinition().DeclaringType == typeof(object)) + { + return false; + } - if (methodInfo.IsConstructor) - { - return false; - } + // Dispose method implemented from IDisposable is not valid + if (IsIDisposableMethod(methodInfo)) + { + return false; + } - if (methodInfo.IsGenericMethod) - { - return false; - } + if (methodInfo.IsStatic) + { + return false; + } - return methodInfo.IsPublic; + if (methodInfo.IsAbstract) + { + return false; } - private static bool IsIDisposableMethod(MethodInfo methodInfo) + if (methodInfo.IsConstructor) { - // Find where the method was originally declared - var baseMethodInfo = methodInfo.GetBaseDefinition(); - var declaringTypeInfo = baseMethodInfo.DeclaringType.GetTypeInfo(); + return false; + } - return - (typeof(IDisposable).GetTypeInfo().IsAssignableFrom(declaringTypeInfo) && - declaringTypeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0] == baseMethodInfo); + if (methodInfo.IsGenericMethod) + { + return false; } + + return methodInfo.IsPublic; + } + + private static bool IsIDisposableMethod(MethodInfo methodInfo) + { + // Find where the method was originally declared + var baseMethodInfo = methodInfo.GetBaseDefinition(); + var declaringTypeInfo = baseMethodInfo.DeclaringType.GetTypeInfo(); + + return + (typeof(IDisposable).GetTypeInfo().IsAssignableFrom(declaringTypeInfo) && + declaringTypeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0] == baseMethodInfo); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/ControllerModelExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/ControllerModelExtensionsTests.cs index 8ed43059a..b4fa3ce88 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/ControllerModelExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/ControllerModelExtensionsTests.cs @@ -11,32 +11,31 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +public class ControllerModelExtensionsTests { - public class ControllerModelExtensionsTests + [Fact] + public void IsODataIgnored_ThrowsArgumentNull_Controller() { - [Fact] - public void IsODataIgnored_ThrowsArgumentNull_Controller() - { - // Arrange & Act & Assert - ControllerModel controller = null; - ExceptionAssert.ThrowsArgumentNull(() => controller.IsODataIgnored(), "controller"); - } + // Arrange & Act & Assert + ControllerModel controller = null; + ExceptionAssert.ThrowsArgumentNull(() => controller.IsODataIgnored(), "controller"); + } - [Fact] - public void HasAttribute_ThrowsArgumentNull_Controller() - { - // Arrange & Act & Assert - ControllerModel controller = null; - ExceptionAssert.ThrowsArgumentNull(() => controller.HasAttribute(), "controller"); - } + [Fact] + public void HasAttribute_ThrowsArgumentNull_Controller() + { + // Arrange & Act & Assert + ControllerModel controller = null; + ExceptionAssert.ThrowsArgumentNull(() => controller.HasAttribute(), "controller"); + } - [Fact] - public void GetAttribute_ThrowsArgumentNull_Controller() - { - // Arrange & Act & Assert - ControllerModel controller = null; - ExceptionAssert.ThrowsArgumentNull(() => controller.GetAttribute(), "controller"); - } + [Fact] + public void GetAttribute_ThrowsArgumentNull_Controller() + { + // Arrange & Act & Assert + ControllerModel controller = null; + ExceptionAssert.ThrowsArgumentNull(() => controller.GetAttribute(), "controller"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/ControllerModelHelpers.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/ControllerModelHelpers.cs index ef6af6b16..5caa157fa 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/ControllerModelHelpers.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/ControllerModelHelpers.cs @@ -17,664 +17,663 @@ using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +internal static class ControllerModelHelpers { - internal static class ControllerModelHelpers + /// + /// Build the controller model. + /// + /// The controller type. + /// The controller model. + public static ControllerModel BuildControllerModelWithAllActions() { - /// - /// Build the controller model. - /// - /// The controller type. - /// The controller model. - public static ControllerModel BuildControllerModelWithAllActions() - { - Type controllerType = typeof(T); - return BuildControllerModelByMethodInfo(controllerType, controllerType.GetMethods()); - } + Type controllerType = typeof(T); + return BuildControllerModelByMethodInfo(controllerType, controllerType.GetMethods()); + } - /// - /// Build the controller model. - /// - /// The controller type. - /// The controller model. - public static ControllerModel BuildControllerModelWithAllActions(Type controllerType) - { - return BuildControllerModelByMethodInfo(controllerType, controllerType.GetMethods()); - } + /// + /// Build the controller model. + /// + /// The controller type. + /// The controller model. + public static ControllerModel BuildControllerModelWithAllActions(Type controllerType) + { + return BuildControllerModelByMethodInfo(controllerType, controllerType.GetMethods()); + } - /// - /// Build the controller model. - /// - /// The controller type. - /// The actions. - /// The controller model. - public static ControllerModel BuildControllerModel(params string[] actions) - { - return BuildControllerModel(typeof(T), actions); - } + /// + /// Build the controller model. + /// + /// The controller type. + /// The actions. + /// The controller model. + public static ControllerModel BuildControllerModel(params string[] actions) + { + return BuildControllerModel(typeof(T), actions); + } - /// - /// Build the controller model. - /// - /// The controller type. - /// The actions. - /// The controller model. - public static ControllerModel BuildControllerModel(Type controllerType, params string[] actions) - { - return BuildControllerModelByMethodInfo(controllerType, actions.Select(a => controllerType.GetMethod(a)).ToArray()); - } + /// + /// Build the controller model. + /// + /// The controller type. + /// The actions. + /// The controller model. + public static ControllerModel BuildControllerModel(Type controllerType, params string[] actions) + { + return BuildControllerModelByMethodInfo(controllerType, actions.Select(a => controllerType.GetMethod(a)).ToArray()); + } - /// - /// Build the controller model. - /// - /// The controller type. - /// The method infos. - /// The controller model. - public static ControllerModel BuildControllerModelByMethodInfo(params MethodInfo[] methodInfos) + /// + /// Build the controller model. + /// + /// The controller type. + /// The method infos. + /// The controller model. + public static ControllerModel BuildControllerModelByMethodInfo(params MethodInfo[] methodInfos) + { + return BuildControllerModelByMethodInfo(typeof(T), methodInfos); + } + + /// + /// Build the controller model. + /// + /// The controller type. + /// The method infos. + /// The controller model. + public static ControllerModel BuildControllerModelByMethodInfo(Type controllerType, params MethodInfo[] methodInfos) + { + if (controllerType == null) { - return BuildControllerModelByMethodInfo(typeof(T), methodInfos); + throw Error.ArgumentNull(nameof(controllerType)); } - /// - /// Build the controller model. - /// - /// The controller type. - /// The method infos. - /// The controller model. - public static ControllerModel BuildControllerModelByMethodInfo(Type controllerType, params MethodInfo[] methodInfos) + TypeInfo controllerTypeInfo = controllerType.GetTypeInfo(); + ControllerModel controller = CreateControllerModel(controllerTypeInfo); + + foreach (var methodInfo in methodInfos) { - if (controllerType == null) + if (methodInfo == null) { - throw Error.ArgumentNull(nameof(controllerType)); + continue; } - TypeInfo controllerTypeInfo = controllerType.GetTypeInfo(); - ControllerModel controller = CreateControllerModel(controllerTypeInfo); + ActionModel actionModel = CreateActionModel(controllerTypeInfo, methodInfo); + if (actionModel == null) + { + continue; + } + actionModel.Controller = controller; - foreach (var methodInfo in methodInfos) + foreach (var parameterInfo in actionModel.ActionMethod.GetParameters()) { - if (methodInfo == null) + var parameterModel = CreateParameterModel(parameterInfo); + if (parameterModel != null) { - continue; + parameterModel.Action = actionModel; + actionModel.Parameters.Add(parameterModel); } + } - ActionModel actionModel = CreateActionModel(controllerTypeInfo, methodInfo); - if (actionModel == null) - { - continue; - } - actionModel.Controller = controller; + controller.Actions.Add(actionModel); + } - foreach (var parameterInfo in actionModel.ActionMethod.GetParameters()) - { - var parameterModel = CreateParameterModel(parameterInfo); - if (parameterModel != null) - { - parameterModel.Action = actionModel; - actionModel.Parameters.Add(parameterModel); - } - } + return controller; + } + + /// + /// Creates a for the given . + /// + /// The . + /// A for the given . + internal static ControllerModel CreateControllerModel(TypeInfo typeInfo) + { + if (typeInfo == null) + { + throw Error.ArgumentNull(nameof(typeInfo)); + } + + // For attribute routes on a controller, we want to support 'overriding' routes on a derived + // class. So we need to walk up the hierarchy looking for the first class to define routes. + // + // Then we want to 'filter' the set of attributes, so that only the effective routes apply. + var currentTypeInfo = typeInfo; + var objectTypeInfo = typeof(object).GetTypeInfo(); + + IRouteTemplateProvider[] routeAttributes; + + do + { + routeAttributes = currentTypeInfo + .GetCustomAttributes(inherit: false) + .OfType() + .ToArray(); - controller.Actions.Add(actionModel); + if (routeAttributes.Length > 0) + { + // Found 1 or more route attributes. + break; } - return controller; + currentTypeInfo = currentTypeInfo.BaseType.GetTypeInfo(); } + while (currentTypeInfo != objectTypeInfo); + + // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType + // is needed to so that the result of ToArray() is object + var attributes = typeInfo.GetCustomAttributes(inherit: true); - /// - /// Creates a for the given . - /// - /// The . - /// A for the given . - internal static ControllerModel CreateControllerModel(TypeInfo typeInfo) + // This is fairly complicated so that we maintain referential equality between items in + // ControllerModel.Attributes and ControllerModel.Attributes[*].Attribute. + var filteredAttributes = new List(); + foreach (var attribute in attributes) { - if (typeInfo == null) + if (attribute is IRouteTemplateProvider) { - throw Error.ArgumentNull(nameof(typeInfo)); + // This attribute is a route-attribute, leave it out. } + else + { + filteredAttributes.Add(attribute); + } + } - // For attribute routes on a controller, we want to support 'overriding' routes on a derived - // class. So we need to walk up the hierarchy looking for the first class to define routes. - // - // Then we want to 'filter' the set of attributes, so that only the effective routes apply. - var currentTypeInfo = typeInfo; - var objectTypeInfo = typeof(object).GetTypeInfo(); + filteredAttributes.AddRange(routeAttributes); - IRouteTemplateProvider[] routeAttributes; + attributes = filteredAttributes.ToArray(); - do - { - routeAttributes = currentTypeInfo - .GetCustomAttributes(inherit: false) - .OfType() - .ToArray(); + var controllerModel = new ControllerModel(typeInfo, attributes); - if (routeAttributes.Length > 0) - { - // Found 1 or more route attributes. - break; - } + AddRange(controllerModel.Selectors, CreateSelectors(attributes)); - currentTypeInfo = currentTypeInfo.BaseType.GetTypeInfo(); - } - while (currentTypeInfo != objectTypeInfo); + controllerModel.ControllerName = + typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) ? + typeInfo.Name.Substring(0, typeInfo.Name.Length - "Controller".Length) : + typeInfo.Name; - // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType - // is needed to so that the result of ToArray() is object - var attributes = typeInfo.GetCustomAttributes(inherit: true); + AddRange(controllerModel.Filters, attributes.OfType()); - // This is fairly complicated so that we maintain referential equality between items in - // ControllerModel.Attributes and ControllerModel.Attributes[*].Attribute. - var filteredAttributes = new List(); - foreach (var attribute in attributes) - { - if (attribute is IRouteTemplateProvider) - { - // This attribute is a route-attribute, leave it out. - } - else - { - filteredAttributes.Add(attribute); - } - } + foreach (var routeValueProvider in attributes.OfType()) + { + controllerModel.RouteValues.Add(routeValueProvider.RouteKey, routeValueProvider.RouteValue); + } - filteredAttributes.AddRange(routeAttributes); + var apiVisibility = attributes.OfType().FirstOrDefault(); + if (apiVisibility != null) + { + controllerModel.ApiExplorer.IsVisible = !apiVisibility.IgnoreApi; + } - attributes = filteredAttributes.ToArray(); + var apiGroupName = attributes.OfType().FirstOrDefault(); + if (apiGroupName != null) + { + controllerModel.ApiExplorer.GroupName = apiGroupName.GroupName; + } - var controllerModel = new ControllerModel(typeInfo, attributes); + // Controllers can implement action filter and result filter interfaces. We add + // a special delegating filter implementation to the pipeline to handle it. + // + // This is needed because filters are instantiated before the controller. + //if (typeof(IAsyncActionFilter).GetTypeInfo().IsAssignableFrom(typeInfo) || + // typeof(IActionFilter).GetTypeInfo().IsAssignableFrom(typeInfo)) + //{ + // controllerModel.Filters.Add(new ControllerActionFilter()); + //} + //if (typeof(IAsyncResultFilter).GetTypeInfo().IsAssignableFrom(typeInfo) || + // typeof(IResultFilter).GetTypeInfo().IsAssignableFrom(typeInfo)) + //{ + // controllerModel.Filters.Add(new ControllerResultFilter()); + //} + + return controllerModel; + } - AddRange(controllerModel.Selectors, CreateSelectors(attributes)); + /// + /// Creates the instance for the given action . + /// + /// The controller . + /// The action . + /// + /// An instance for the given action or + /// null if the does not represent an action. + /// + internal static ActionModel CreateActionModel(TypeInfo typeInfo, MethodInfo methodInfo) + { + if (typeInfo == null) + { + throw new ArgumentNullException(nameof(typeInfo)); + } - controllerModel.ControllerName = - typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) ? - typeInfo.Name.Substring(0, typeInfo.Name.Length - "Controller".Length) : - typeInfo.Name; + if (methodInfo == null) + { + throw new ArgumentNullException(nameof(methodInfo)); + } - AddRange(controllerModel.Filters, attributes.OfType()); + if (!IsAction(typeInfo, methodInfo)) + { + return null; + } - foreach (var routeValueProvider in attributes.OfType()) - { - controllerModel.RouteValues.Add(routeValueProvider.RouteKey, routeValueProvider.RouteValue); - } + // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType + // is needed to so that the result of ToArray() is object + var attributes = methodInfo.GetCustomAttributes(inherit: true); - var apiVisibility = attributes.OfType().FirstOrDefault(); - if (apiVisibility != null) - { - controllerModel.ApiExplorer.IsVisible = !apiVisibility.IgnoreApi; - } + var actionModel = new ActionModel(methodInfo, attributes); - var apiGroupName = attributes.OfType().FirstOrDefault(); - if (apiGroupName != null) - { - controllerModel.ApiExplorer.GroupName = apiGroupName.GroupName; - } + AddRange(actionModel.Filters, attributes.OfType()); - // Controllers can implement action filter and result filter interfaces. We add - // a special delegating filter implementation to the pipeline to handle it. - // - // This is needed because filters are instantiated before the controller. - //if (typeof(IAsyncActionFilter).GetTypeInfo().IsAssignableFrom(typeInfo) || - // typeof(IActionFilter).GetTypeInfo().IsAssignableFrom(typeInfo)) - //{ - // controllerModel.Filters.Add(new ControllerActionFilter()); - //} - //if (typeof(IAsyncResultFilter).GetTypeInfo().IsAssignableFrom(typeInfo) || - // typeof(IResultFilter).GetTypeInfo().IsAssignableFrom(typeInfo)) - //{ - // controllerModel.Filters.Add(new ControllerResultFilter()); - //} - - return controllerModel; - } - - /// - /// Creates the instance for the given action . - /// - /// The controller . - /// The action . - /// - /// An instance for the given action or - /// null if the does not represent an action. - /// - internal static ActionModel CreateActionModel(TypeInfo typeInfo, MethodInfo methodInfo) - { - if (typeInfo == null) - { - throw new ArgumentNullException(nameof(typeInfo)); - } + var actionName = attributes.OfType().FirstOrDefault(); + if (actionName?.Name != null) + { + actionModel.ActionName = actionName.Name; + } + else + { + actionModel.ActionName = CanonicalizeActionName(methodInfo.Name); + } - if (methodInfo == null) - { - throw new ArgumentNullException(nameof(methodInfo)); - } + var apiVisibility = attributes.OfType().FirstOrDefault(); + if (apiVisibility != null) + { + actionModel.ApiExplorer.IsVisible = !apiVisibility.IgnoreApi; + } - if (!IsAction(typeInfo, methodInfo)) - { - return null; - } + var apiGroupName = attributes.OfType().FirstOrDefault(); + if (apiGroupName != null) + { + actionModel.ApiExplorer.GroupName = apiGroupName.GroupName; + } - // CoreCLR returns IEnumerable from GetCustomAttributes - the OfType - // is needed to so that the result of ToArray() is object - var attributes = methodInfo.GetCustomAttributes(inherit: true); + foreach (var routeValueProvider in attributes.OfType()) + { + actionModel.RouteValues.Add(routeValueProvider.RouteKey, routeValueProvider.RouteValue); + } - var actionModel = new ActionModel(methodInfo, attributes); + // Now we need to determine the action selection info (cross-section of routes and constraints) + // + // For attribute routes on a action, we want to support 'overriding' routes on a + // virtual method, but allow 'overriding'. So we need to walk up the hierarchy looking + // for the first definition to define routes. + // + // Then we want to 'filter' the set of attributes, so that only the effective routes apply. + var currentMethodInfo = methodInfo; - AddRange(actionModel.Filters, attributes.OfType()); + IRouteTemplateProvider[] routeAttributes; - var actionName = attributes.OfType().FirstOrDefault(); - if (actionName?.Name != null) - { - actionModel.ActionName = actionName.Name; - } - else - { - actionModel.ActionName = CanonicalizeActionName(methodInfo.Name); - } + while (true) + { + routeAttributes = currentMethodInfo + .GetCustomAttributes(inherit: false) + .OfType() + .ToArray(); - var apiVisibility = attributes.OfType().FirstOrDefault(); - if (apiVisibility != null) + if (routeAttributes.Length > 0) { - actionModel.ApiExplorer.IsVisible = !apiVisibility.IgnoreApi; + // Found 1 or more route attributes. + break; } - var apiGroupName = attributes.OfType().FirstOrDefault(); - if (apiGroupName != null) + // GetBaseDefinition returns 'this' when it gets to the bottom of the chain. + var nextMethodInfo = currentMethodInfo.GetBaseDefinition(); + if (currentMethodInfo == nextMethodInfo) { - actionModel.ApiExplorer.GroupName = apiGroupName.GroupName; + break; } - foreach (var routeValueProvider in attributes.OfType()) + currentMethodInfo = nextMethodInfo; + } + + // This is fairly complicated so that we maintain referential equality between items in + // ActionModel.Attributes and ActionModel.Attributes[*].Attribute. + var applicableAttributes = new List(routeAttributes.Length); + foreach (var attribute in attributes) + { + if (attribute is IRouteTemplateProvider) { - actionModel.RouteValues.Add(routeValueProvider.RouteKey, routeValueProvider.RouteValue); + // This attribute is a route-attribute, leave it out. } - - // Now we need to determine the action selection info (cross-section of routes and constraints) - // - // For attribute routes on a action, we want to support 'overriding' routes on a - // virtual method, but allow 'overriding'. So we need to walk up the hierarchy looking - // for the first definition to define routes. - // - // Then we want to 'filter' the set of attributes, so that only the effective routes apply. - var currentMethodInfo = methodInfo; - - IRouteTemplateProvider[] routeAttributes; - - while (true) + else { - routeAttributes = currentMethodInfo - .GetCustomAttributes(inherit: false) - .OfType() - .ToArray(); - - if (routeAttributes.Length > 0) - { - // Found 1 or more route attributes. - break; - } + applicableAttributes.Add(attribute); + } + } - // GetBaseDefinition returns 'this' when it gets to the bottom of the chain. - var nextMethodInfo = currentMethodInfo.GetBaseDefinition(); - if (currentMethodInfo == nextMethodInfo) - { - break; - } + applicableAttributes.AddRange(routeAttributes); + AddRange(actionModel.Selectors, CreateSelectors(applicableAttributes)); - currentMethodInfo = nextMethodInfo; - } + return actionModel; + } - // This is fairly complicated so that we maintain referential equality between items in - // ActionModel.Attributes and ActionModel.Attributes[*].Attribute. - var applicableAttributes = new List(routeAttributes.Length); - foreach (var attribute in attributes) + private static IList CreateSelectors(IList attributes) + { + // Route attributes create multiple selector models, we want to split the set of + // attributes based on these so each selector only has the attributes that affect it. + // + // The set of route attributes are split into those that 'define' a route versus those that are + // 'silent'. + // + // We need to define a selector for each attribute that 'defines' a route, and a single selector + // for all of the ones that don't (if any exist). + // + // If the attribute that 'defines' a route is NOT an IActionHttpMethodProvider, then we'll include with + // it, any IActionHttpMethodProvider that are 'silent' IRouteTemplateProviders. In this case the 'extra' + // action for silent route providers isn't needed. + // + // Ex: + // [HttpGet] + // [AcceptVerbs("POST", "PUT")] + // [HttpPost("Api/Things")] + // public void DoThing() + // + // This will generate 2 selectors: + // 1. [HttpPost("Api/Things")] + // 2. [HttpGet], [AcceptVerbs("POST", "PUT")] + // + // Another example of this situation is: + // + // [Route("api/Products")] + // [AcceptVerbs("GET", "HEAD")] + // [HttpPost("api/Products/new")] + // + // This will generate 2 selectors: + // 1. [AcceptVerbs("GET", "HEAD")] + // 2. [HttpPost] + // + // Note that having a route attribute that doesn't define a route template _might_ be an error. We + // don't have enough context to really know at this point so we just pass it on. + var routeProviders = new List(); + + var createSelectorForSilentRouteProviders = false; + foreach (var attribute in attributes) + { + if (attribute is IRouteTemplateProvider routeTemplateProvider) { - if (attribute is IRouteTemplateProvider) + if (IsSilentRouteAttribute(routeTemplateProvider)) { - // This attribute is a route-attribute, leave it out. + createSelectorForSilentRouteProviders = true; } else { - applicableAttributes.Add(attribute); + routeProviders.Add(routeTemplateProvider); } } - - applicableAttributes.AddRange(routeAttributes); - AddRange(actionModel.Selectors, CreateSelectors(applicableAttributes)); - - return actionModel; } - private static IList CreateSelectors(IList attributes) + foreach (var routeProvider in routeProviders) { - // Route attributes create multiple selector models, we want to split the set of - // attributes based on these so each selector only has the attributes that affect it. + // If we see an attribute like + // [Route(...)] // - // The set of route attributes are split into those that 'define' a route versus those that are - // 'silent'. + // Then we want to group any attributes like [HttpGet] with it. // - // We need to define a selector for each attribute that 'defines' a route, and a single selector - // for all of the ones that don't (if any exist). + // Basically... // - // If the attribute that 'defines' a route is NOT an IActionHttpMethodProvider, then we'll include with - // it, any IActionHttpMethodProvider that are 'silent' IRouteTemplateProviders. In this case the 'extra' - // action for silent route providers isn't needed. - // - // Ex: // [HttpGet] - // [AcceptVerbs("POST", "PUT")] - // [HttpPost("Api/Things")] - // public void DoThing() - // - // This will generate 2 selectors: - // 1. [HttpPost("Api/Things")] - // 2. [HttpGet], [AcceptVerbs("POST", "PUT")] + // [HttpPost("Products")] + // public void Foo() { } // - // Another example of this situation is: + // Is two selectors. And... // - // [Route("api/Products")] - // [AcceptVerbs("GET", "HEAD")] - // [HttpPost("api/Products/new")] - // - // This will generate 2 selectors: - // 1. [AcceptVerbs("GET", "HEAD")] - // 2. [HttpPost] + // [HttpGet] + // [Route("Products")] + // public void Foo() { } // - // Note that having a route attribute that doesn't define a route template _might_ be an error. We - // don't have enough context to really know at this point so we just pass it on. - var routeProviders = new List(); + // Is one selector. + if (!(routeProvider is IActionHttpMethodProvider)) + { + createSelectorForSilentRouteProviders = false; + break; + } + } - var createSelectorForSilentRouteProviders = false; - foreach (var attribute in attributes) + var selectorModels = new List(); + if (routeProviders.Count == 0 && !createSelectorForSilentRouteProviders) + { + // Simple case, all attributes apply + selectorModels.Add(CreateSelectorModel(route: null, attributes: attributes)); + } + else + { + // Each of these routeProviders are the ones that actually have routing information on them + // something like [HttpGet] won't show up here, but [HttpGet("Products")] will. + foreach (var routeProvider in routeProviders) { - if (attribute is IRouteTemplateProvider routeTemplateProvider) + var filteredAttributes = new List(); + foreach (var attribute in attributes) { - if (IsSilentRouteAttribute(routeTemplateProvider)) + if (ReferenceEquals(attribute, routeProvider)) + { + filteredAttributes.Add(attribute); + } + else if (InRouteProviders(routeProviders, attribute)) { - createSelectorForSilentRouteProviders = true; + // Exclude other route template providers + // Example: + // [HttpGet("template")] + // [Route("template/{id}")] + } + else if ( + routeProvider is IActionHttpMethodProvider && + attribute is IActionHttpMethodProvider) + { + // Example: + // [HttpGet("template")] + // [AcceptVerbs("GET", "POST")] + // + // Exclude other http method providers if this route is an + // http method provider. } else { - routeProviders.Add(routeTemplateProvider); + filteredAttributes.Add(attribute); } } - } - foreach (var routeProvider in routeProviders) - { - // If we see an attribute like - // [Route(...)] - // - // Then we want to group any attributes like [HttpGet] with it. - // - // Basically... - // - // [HttpGet] - // [HttpPost("Products")] - // public void Foo() { } - // - // Is two selectors. And... - // - // [HttpGet] - // [Route("Products")] - // public void Foo() { } - // - // Is one selector. - if (!(routeProvider is IActionHttpMethodProvider)) - { - createSelectorForSilentRouteProviders = false; - break; - } + selectorModels.Add(CreateSelectorModel(routeProvider, filteredAttributes)); } - var selectorModels = new List(); - if (routeProviders.Count == 0 && !createSelectorForSilentRouteProviders) + if (createSelectorForSilentRouteProviders) { - // Simple case, all attributes apply - selectorModels.Add(CreateSelectorModel(route: null, attributes: attributes)); - } - else - { - // Each of these routeProviders are the ones that actually have routing information on them - // something like [HttpGet] won't show up here, but [HttpGet("Products")] will. - foreach (var routeProvider in routeProviders) + var filteredAttributes = new List(); + foreach (var attribute in attributes) { - var filteredAttributes = new List(); - foreach (var attribute in attributes) + if (!InRouteProviders(routeProviders, attribute)) { - if (ReferenceEquals(attribute, routeProvider)) - { - filteredAttributes.Add(attribute); - } - else if (InRouteProviders(routeProviders, attribute)) - { - // Exclude other route template providers - // Example: - // [HttpGet("template")] - // [Route("template/{id}")] - } - else if ( - routeProvider is IActionHttpMethodProvider && - attribute is IActionHttpMethodProvider) - { - // Example: - // [HttpGet("template")] - // [AcceptVerbs("GET", "POST")] - // - // Exclude other http method providers if this route is an - // http method provider. - } - else - { - filteredAttributes.Add(attribute); - } + filteredAttributes.Add(attribute); } - - selectorModels.Add(CreateSelectorModel(routeProvider, filteredAttributes)); } - if (createSelectorForSilentRouteProviders) - { - var filteredAttributes = new List(); - foreach (var attribute in attributes) - { - if (!InRouteProviders(routeProviders, attribute)) - { - filteredAttributes.Add(attribute); - } - } - - selectorModels.Add(CreateSelectorModel(route: null, attributes: filteredAttributes)); - } + selectorModels.Add(CreateSelectorModel(route: null, attributes: filteredAttributes)); } - - return selectorModels; } - private static bool InRouteProviders(List routeProviders, object attribute) - { - foreach (var rp in routeProviders) - { - if (ReferenceEquals(rp, attribute)) - { - return true; - } - } - - return false; - } + return selectorModels; + } - /// - /// Returns true if the is an action. Otherwise false. - /// - /// The . - /// The . - /// true if the is an action. Otherwise false. - /// - /// Override this method to provide custom logic to determine which methods are considered actions. - /// - internal static bool IsAction(TypeInfo typeInfo, MethodInfo methodInfo) + private static bool InRouteProviders(List routeProviders, object attribute) + { + foreach (var rp in routeProviders) { - if (typeInfo == null) - { - throw new ArgumentNullException(nameof(typeInfo)); - } - - if (methodInfo == null) + if (ReferenceEquals(rp, attribute)) { - throw new ArgumentNullException(nameof(methodInfo)); - } - - // The SpecialName bit is set to flag members that are treated in a special way by some compilers - // (such as property accessors and operator overloading methods). - if (methodInfo.IsSpecialName) - { - return false; + return true; } + } - if (methodInfo.IsDefined(typeof(NonActionAttribute))) - { - return false; - } + return false; + } - // Overridden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid. - if (methodInfo.GetBaseDefinition().DeclaringType == typeof(object)) - { - return false; - } + /// + /// Returns true if the is an action. Otherwise false. + /// + /// The . + /// The . + /// true if the is an action. Otherwise false. + /// + /// Override this method to provide custom logic to determine which methods are considered actions. + /// + internal static bool IsAction(TypeInfo typeInfo, MethodInfo methodInfo) + { + if (typeInfo == null) + { + throw new ArgumentNullException(nameof(typeInfo)); + } - // Dispose method implemented from IDisposable is not valid - if (IsIDisposableMethod(methodInfo)) - { - return false; - } + if (methodInfo == null) + { + throw new ArgumentNullException(nameof(methodInfo)); + } - if (methodInfo.IsStatic) - { - return false; - } + // The SpecialName bit is set to flag members that are treated in a special way by some compilers + // (such as property accessors and operator overloading methods). + if (methodInfo.IsSpecialName) + { + return false; + } - if (methodInfo.IsAbstract) - { - return false; - } + if (methodInfo.IsDefined(typeof(NonActionAttribute))) + { + return false; + } - if (methodInfo.IsConstructor) - { - return false; - } + // Overridden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid. + if (methodInfo.GetBaseDefinition().DeclaringType == typeof(object)) + { + return false; + } - if (methodInfo.IsGenericMethod) - { - return false; - } + // Dispose method implemented from IDisposable is not valid + if (IsIDisposableMethod(methodInfo)) + { + return false; + } - return methodInfo.IsPublic; + if (methodInfo.IsStatic) + { + return false; } - private static SelectorModel CreateSelectorModel(IRouteTemplateProvider route, IList attributes) + if (methodInfo.IsAbstract) { - var selectorModel = new SelectorModel(); - if (route != null) - { - selectorModel.AttributeRouteModel = new AttributeRouteModel(route); - } + return false; + } - AddRange(selectorModel.ActionConstraints, attributes.OfType()); - AddRange(selectorModel.EndpointMetadata, attributes); + if (methodInfo.IsConstructor) + { + return false; + } - // Simple case, all HTTP method attributes apply - var httpMethods = attributes - .OfType() - .SelectMany(a => a.HttpMethods) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); + if (methodInfo.IsGenericMethod) + { + return false; + } - if (httpMethods.Length > 0) - { - selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(httpMethods)); - selectorModel.EndpointMetadata.Add(new HttpMethodMetadata(httpMethods)); - } + return methodInfo.IsPublic; + } - return selectorModel; + private static SelectorModel CreateSelectorModel(IRouteTemplateProvider route, IList attributes) + { + var selectorModel = new SelectorModel(); + if (route != null) + { + selectorModel.AttributeRouteModel = new AttributeRouteModel(route); } - private static bool IsIDisposableMethod(MethodInfo methodInfo) - { - // Ideally we do not want Dispose method to be exposed as an action. However there are some scenarios where a user - // might want to expose a method with name "Dispose" (even though they might not be really disposing resources) - // Example: A controller deriving from MVC's Controller type might wish to have a method with name Dispose, - // in which case they can use the "new" keyword to hide the base controller's declaration. + AddRange(selectorModel.ActionConstraints, attributes.OfType()); + AddRange(selectorModel.EndpointMetadata, attributes); - // Find where the method was originally declared - var baseMethodInfo = methodInfo.GetBaseDefinition(); - var declaringTypeInfo = baseMethodInfo.DeclaringType.GetTypeInfo(); + // Simple case, all HTTP method attributes apply + var httpMethods = attributes + .OfType() + .SelectMany(a => a.HttpMethods) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); - return - (typeof(IDisposable).GetTypeInfo().IsAssignableFrom(declaringTypeInfo) && - declaringTypeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0] == baseMethodInfo); + if (httpMethods.Length > 0) + { + selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(httpMethods)); + selectorModel.EndpointMetadata.Add(new HttpMethodMetadata(httpMethods)); } - /// - /// Creates a for the given . - /// - /// The . - /// A for the given . - internal static ParameterModel CreateParameterModel(ParameterInfo parameterInfo) - { - if (parameterInfo == null) - { - throw new ArgumentNullException(nameof(parameterInfo)); - } + return selectorModel; + } - var attributes = parameterInfo.GetCustomAttributes(inherit: true); - - //BindingInfo bindingInfo; - //if (_modelMetadataProvider is ModelMetadataProvider modelMetadataProviderBase) - //{ - // var modelMetadata = modelMetadataProviderBase.GetMetadataForParameter(parameterInfo); - // bindingInfo = BindingInfo.GetBindingInfo(attributes, modelMetadata); - //} - //else - //{ - // // GetMetadataForParameter should only be used if the user has opted in to the 2.1 behavior. - // bindingInfo = BindingInfo.GetBindingInfo(attributes); - //} - - var parameterModel = new ParameterModel(parameterInfo, attributes) - { - ParameterName = parameterInfo.Name, - // BindingInfo = bindingInfo, - }; + private static bool IsIDisposableMethod(MethodInfo methodInfo) + { + // Ideally we do not want Dispose method to be exposed as an action. However there are some scenarios where a user + // might want to expose a method with name "Dispose" (even though they might not be really disposing resources) + // Example: A controller deriving from MVC's Controller type might wish to have a method with name Dispose, + // in which case they can use the "new" keyword to hide the base controller's declaration. + + // Find where the method was originally declared + var baseMethodInfo = methodInfo.GetBaseDefinition(); + var declaringTypeInfo = baseMethodInfo.DeclaringType.GetTypeInfo(); + + return + (typeof(IDisposable).GetTypeInfo().IsAssignableFrom(declaringTypeInfo) && + declaringTypeInfo.GetRuntimeInterfaceMap(typeof(IDisposable)).TargetMethods[0] == baseMethodInfo); + } - return parameterModel; + /// + /// Creates a for the given . + /// + /// The . + /// A for the given . + internal static ParameterModel CreateParameterModel(ParameterInfo parameterInfo) + { + if (parameterInfo == null) + { + throw new ArgumentNullException(nameof(parameterInfo)); } - private static string CanonicalizeActionName(string actionName) + var attributes = parameterInfo.GetCustomAttributes(inherit: true); + + //BindingInfo bindingInfo; + //if (_modelMetadataProvider is ModelMetadataProvider modelMetadataProviderBase) + //{ + // var modelMetadata = modelMetadataProviderBase.GetMetadataForParameter(parameterInfo); + // bindingInfo = BindingInfo.GetBindingInfo(attributes, modelMetadata); + //} + //else + //{ + // // GetMetadataForParameter should only be used if the user has opted in to the 2.1 behavior. + // bindingInfo = BindingInfo.GetBindingInfo(attributes); + //} + + var parameterModel = new ParameterModel(parameterInfo, attributes) { - const string Suffix = "Async"; + ParameterName = parameterInfo.Name, + // BindingInfo = bindingInfo, + }; - if (actionName.EndsWith(Suffix, StringComparison.Ordinal)) - { - actionName = actionName.Substring(0, actionName.Length - Suffix.Length); - } + return parameterModel; + } - return actionName; - } + private static string CanonicalizeActionName(string actionName) + { + const string Suffix = "Async"; - private static bool IsSilentRouteAttribute(IRouteTemplateProvider routeTemplateProvider) + if (actionName.EndsWith(Suffix, StringComparison.Ordinal)) { - return - routeTemplateProvider.Template == null && - routeTemplateProvider.Order == null && - routeTemplateProvider.Name == null; + actionName = actionName.Substring(0, actionName.Length - Suffix.Length); } - private static void AddRange(IList list, IEnumerable items) + return actionName; + } + + private static bool IsSilentRouteAttribute(IRouteTemplateProvider routeTemplateProvider) + { + return + routeTemplateProvider.Template == null && + routeTemplateProvider.Order == null && + routeTemplateProvider.Name == null; + } + + private static void AddRange(IList list, IEnumerable items) + { + foreach (var item in items) { - foreach (var item in items) - { - list.Add(item); - } + list.Add(item); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContentExtensions.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContentExtensions.cs index 6d5bc6714..9124f9f3c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContentExtensions.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContentExtensions.cs @@ -10,58 +10,57 @@ using System.Text.Json; using Newtonsoft.Json; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +/// +/// Extensions for HttpContent. +/// +public static class HttpContentExtensions { /// - /// Extensions for HttpContent. + /// Get the content as the value of ObjectContent. /// - public static class HttpContentExtensions + /// The content value. + public static string AsObjectContentValue(this HttpContent content) { - /// - /// Get the content as the value of ObjectContent. - /// - /// The content value. - public static string AsObjectContentValue(this HttpContent content) + string json = content.ReadAsStringAsync().Result; + try { - string json = content.ReadAsStringAsync().Result; - try - { - using JsonDocument document = JsonDocument.Parse(json); - - JsonElement root = document.RootElement; + using JsonDocument document = JsonDocument.Parse(json); - return root.GetProperty("value").GetString(); - } - catch (System.Text.Json.JsonException) - { - } + JsonElement root = document.RootElement; - return json; + return root.GetProperty("value").GetString(); } - - /// - /// A custom extension for AspNetCore to deserialize JSON content as an object. - /// AspNet provides this in System.Net.Http.Formatting. - /// - /// The content value. - public static async Task ReadAsObject(this HttpContent content) + catch (System.Text.Json.JsonException) { - string json = await content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(json); } - /// - /// - /// - /// - /// - public static async Task ReadAsElement(this HttpContent content) - { - string json = await content.ReadAsStringAsync(); + return json; + } - using JsonDocument document = JsonDocument.Parse(json); + /// + /// A custom extension for AspNetCore to deserialize JSON content as an object. + /// AspNet provides this in System.Net.Http.Formatting. + /// + /// The content value. + public static async Task ReadAsObject(this HttpContent content) + { + string json = await content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(json); + } - return document.RootElement; - } + /// + /// + /// + /// + /// + public static async Task ReadAsElement(this HttpContent content) + { + string json = await content.ReadAsStringAsync(); + + using JsonDocument document = JsonDocument.Parse(json); + + return document.RootElement; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContextExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContextExtensionsTests.cs index 85b2a7562..2b47e40ec 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContextExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContextExtensionsTests.cs @@ -10,24 +10,23 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +public class HttpContextExtensionsTests { - public class HttpContextExtensionsTests + [Fact] + public void ODataFeature_ThrowsArgumentNull_HttpContext() { - [Fact] - public void ODataFeature_ThrowsArgumentNull_HttpContext() - { - // Arrange & Act & Assert - HttpContext httpContext = null; - ExceptionAssert.ThrowsArgumentNull(() => httpContext.ODataFeature(), "httpContext"); - } + // Arrange & Act & Assert + HttpContext httpContext = null; + ExceptionAssert.ThrowsArgumentNull(() => httpContext.ODataFeature(), "httpContext"); + } - [Fact] - public void ODataBatchFeature_ThrowsArgumentNull_HttpContext() - { - // Arrange & Act & Assert - HttpContext httpContext = null; - ExceptionAssert.ThrowsArgumentNull(() => httpContext.ODataBatchFeature(), "httpContext"); - } + [Fact] + public void ODataBatchFeature_ThrowsArgumentNull_HttpContext() + { + // Arrange & Act & Assert + HttpContext httpContext = null; + ExceptionAssert.ThrowsArgumentNull(() => httpContext.ODataBatchFeature(), "httpContext"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContextHelper.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContextHelper.cs index c4c43d2a0..be195a8f0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContextHelper.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpContextHelper.cs @@ -10,65 +10,64 @@ using System.Text; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +/// +/// A class to create HttpContext. +/// +public class HttpContextHelper { /// - /// A class to create HttpContext. + /// /// - public class HttpContextHelper + /// + public static HttpContext Create() { - /// - /// - /// - /// - public static HttpContext Create() - { - return new DefaultHttpContext(); - } + return new DefaultHttpContext(); + } - /// - /// - /// - /// - /// - /// - public static HttpContext Create(string requestMethod, string uri) - { - DefaultHttpContext httpContext = new DefaultHttpContext(); - HttpRequest request = httpContext.Request; + /// + /// + /// + /// + /// + /// + public static HttpContext Create(string requestMethod, string uri) + { + DefaultHttpContext httpContext = new DefaultHttpContext(); + HttpRequest request = httpContext.Request; - request.Method = requestMethod; - Uri requestUri = new Uri(uri); - request.Scheme = requestUri.Scheme; - request.Host = requestUri.IsDefaultPort ? - new HostString(requestUri.Host) : - new HostString(requestUri.Host, requestUri.Port); - request.QueryString = new QueryString(requestUri.Query); - request.Path = new PathString(requestUri.AbsolutePath); - return httpContext; - } + request.Method = requestMethod; + Uri requestUri = new Uri(uri); + request.Scheme = requestUri.Scheme; + request.Host = requestUri.IsDefaultPort ? + new HostString(requestUri.Host) : + new HostString(requestUri.Host, requestUri.Port); + request.QueryString = new QueryString(requestUri.Query); + request.Path = new PathString(requestUri.AbsolutePath); + return httpContext; + } - /// - /// - /// - /// - /// - /// - public static HttpContext Create(string requestMethod, string uri, string requestBody, string contentType = "application/json") - { - HttpContext httpContext = Create(requestMethod, uri); - byte[] body = Encoding.UTF8.GetBytes(requestBody); - httpContext.Request.Body = new MemoryStream(body); - httpContext.Request.ContentType = contentType; - httpContext.Request.ContentLength = body.Length; - return httpContext; - } + /// + /// + /// + /// + /// + /// + public static HttpContext Create(string requestMethod, string uri, string requestBody, string contentType = "application/json") + { + HttpContext httpContext = Create(requestMethod, uri); + byte[] body = Encoding.UTF8.GetBytes(requestBody); + httpContext.Request.Body = new MemoryStream(body); + httpContext.Request.ContentType = contentType; + httpContext.Request.ContentLength = body.Length; + return httpContext; + } - public static HttpContext Create(int statusCode) - { - HttpContext httpContext = new DefaultHttpContext(); - httpContext.Response.StatusCode = statusCode; - return httpContext; - } + public static HttpContext Create(int statusCode) + { + HttpContext httpContext = new DefaultHttpContext(); + httpContext.Response.StatusCode = statusCode; + return httpContext; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpRequestExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpRequestExtensionsTests.cs index af9292141..6736634b8 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpRequestExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpRequestExtensionsTests.cs @@ -14,247 +14,246 @@ using System; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +public class HttpRequestExtensionsTests { - public class HttpRequestExtensionsTests + [Fact] + public void ODataFeature_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.ODataFeature(), "request"); + } + + [Fact] + public void ODataBatchFeature_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.ODataBatchFeature(), "request"); + } + + [Fact] + public void GetModel_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.GetModel(), "request"); + } + + [Fact] + public void GetTimeZoneInfo_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.GetTimeZoneInfo(), "request"); + } + + [Fact] + public void IsNoDollarQueryEnable_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.IsNoDollarQueryEnable(), "request"); + } + + [Fact] + public void IsCountRequest_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.IsCountRequest(), "request"); + } + + [Fact] + public void GetReaderSettings_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.GetReaderSettings(), "request"); + } + + [Fact] + public void GetWriterSettings_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.GetWriterSettings(), "request"); + } + + [Fact] + public void GetDeserializerProvider_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.GetDeserializerProvider(), "request"); + } + + [Fact] + public void GetNextPageLink_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.GetNextPageLink(4, 4, null), "request"); + } + + [Fact] + public void GetNextPageLink_Returns_Uri() + { + // Arrange & Act & Assert + HttpRequest request = RequestFactory.Create("get", "http://localhost"); + + // Act + Uri uri = request.GetNextPageLink(4, 4, null); + + // Assert + Assert.Equal(new Uri("http://localhost/?$skip=4"), uri); + } + + [Fact] + public void CreateETag_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.CreateETag(null, null), "request"); + } + + [Fact] + public void GetETagHandler_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.GetETagHandler(), "request"); + } + + [Fact] + public void IsODataQueryRequest_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.IsODataQueryRequest(), "request"); + } + + [Fact] + public void IsODataQueryRequest_ReturnsCorrectly() + { + // 'GET' + HttpRequest request = RequestFactory.Create(); + request.Method = "GET"; + Assert.False(request.IsODataQueryRequest()); + + // 'POST' but not /$query + request.Method = "POST"; + request.Path = new PathString("/any"); + Assert.False(request.IsODataQueryRequest()); + + // 'POST' but /$query + request.Method = "POST"; + request.Path = new PathString("/any/$query"); + Assert.True(request.IsODataQueryRequest()); + } + + [Fact] + public void GetRouteServices_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.GetRouteServices(), "request"); + } + + [Fact] + public void CreateRouteServices_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.CreateRouteServices(""), "request"); + } + + [Fact] + public void CreateRouteServices_ThrowsInvalidOperation_RouteServices() + { + // Arrange + HttpRequest request = RequestFactory.Create(); + request.ODataFeature().Services = new Mock().Object; + + // Act + Action test = () => request.CreateRouteServices("odata"); + + // Assert + ExceptionAssert.Throws(test, "A dependency injection container for this request already exists."); + } + + [Fact] + public void ClearRouteServices_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.ClearRouteServices(), "request"); + } + + [Fact] + public void ClearRouteServices_ClearsServiceFromRequest() + { + // Arrange + HttpRequest request = RequestFactory.Create(); + IODataFeature oDataFeature = request.ODataFeature(); + oDataFeature.RequestScope = new Mock().Object; + oDataFeature.Services = new Mock().Object; + + // Act + request.ClearRouteServices(false); + + // Assert + Assert.Null(oDataFeature.RequestScope); + Assert.Null(oDataFeature.Services); + } + + [Fact] + public void ODataOptions_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.ODataOptions(), "request"); + } + + [Fact] + public void GetODataVersion_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.GetODataVersion(), "request"); + } + + [Fact] + public void GetQueryOptions_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.GetQueryOptions(), "request"); + } + + [Fact] + public void ODataServiceVersion_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.ODataServiceVersion(), "request"); + } + + [Fact] + public void ODataMaxServiceVersion_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.ODataMaxServiceVersion(), "request"); + } + + [Fact] + public void ODataMinServiceVersion_ThrowsArgumentNull_Request() { - [Fact] - public void ODataFeature_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.ODataFeature(), "request"); - } - - [Fact] - public void ODataBatchFeature_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.ODataBatchFeature(), "request"); - } - - [Fact] - public void GetModel_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.GetModel(), "request"); - } - - [Fact] - public void GetTimeZoneInfo_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.GetTimeZoneInfo(), "request"); - } - - [Fact] - public void IsNoDollarQueryEnable_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.IsNoDollarQueryEnable(), "request"); - } - - [Fact] - public void IsCountRequest_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.IsCountRequest(), "request"); - } - - [Fact] - public void GetReaderSettings_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.GetReaderSettings(), "request"); - } - - [Fact] - public void GetWriterSettings_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.GetWriterSettings(), "request"); - } - - [Fact] - public void GetDeserializerProvider_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.GetDeserializerProvider(), "request"); - } - - [Fact] - public void GetNextPageLink_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.GetNextPageLink(4, 4, null), "request"); - } - - [Fact] - public void GetNextPageLink_Returns_Uri() - { - // Arrange & Act & Assert - HttpRequest request = RequestFactory.Create("get", "http://localhost"); - - // Act - Uri uri = request.GetNextPageLink(4, 4, null); - - // Assert - Assert.Equal(new Uri("http://localhost/?$skip=4"), uri); - } - - [Fact] - public void CreateETag_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.CreateETag(null, null), "request"); - } - - [Fact] - public void GetETagHandler_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.GetETagHandler(), "request"); - } - - [Fact] - public void IsODataQueryRequest_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.IsODataQueryRequest(), "request"); - } - - [Fact] - public void IsODataQueryRequest_ReturnsCorrectly() - { - // 'GET' - HttpRequest request = RequestFactory.Create(); - request.Method = "GET"; - Assert.False(request.IsODataQueryRequest()); - - // 'POST' but not /$query - request.Method = "POST"; - request.Path = new PathString("/any"); - Assert.False(request.IsODataQueryRequest()); - - // 'POST' but /$query - request.Method = "POST"; - request.Path = new PathString("/any/$query"); - Assert.True(request.IsODataQueryRequest()); - } - - [Fact] - public void GetRouteServices_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.GetRouteServices(), "request"); - } - - [Fact] - public void CreateRouteServices_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.CreateRouteServices(""), "request"); - } - - [Fact] - public void CreateRouteServices_ThrowsInvalidOperation_RouteServices() - { - // Arrange - HttpRequest request = RequestFactory.Create(); - request.ODataFeature().Services = new Mock().Object; - - // Act - Action test = () => request.CreateRouteServices("odata"); - - // Assert - ExceptionAssert.Throws(test, "A dependency injection container for this request already exists."); - } - - [Fact] - public void ClearRouteServices_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.ClearRouteServices(), "request"); - } - - [Fact] - public void ClearRouteServices_ClearsServiceFromRequest() - { - // Arrange - HttpRequest request = RequestFactory.Create(); - IODataFeature oDataFeature = request.ODataFeature(); - oDataFeature.RequestScope = new Mock().Object; - oDataFeature.Services = new Mock().Object; - - // Act - request.ClearRouteServices(false); - - // Assert - Assert.Null(oDataFeature.RequestScope); - Assert.Null(oDataFeature.Services); - } - - [Fact] - public void ODataOptions_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.ODataOptions(), "request"); - } - - [Fact] - public void GetODataVersion_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.GetODataVersion(), "request"); - } - - [Fact] - public void GetQueryOptions_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.GetQueryOptions(), "request"); - } - - [Fact] - public void ODataServiceVersion_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.ODataServiceVersion(), "request"); - } - - [Fact] - public void ODataMaxServiceVersion_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.ODataMaxServiceVersion(), "request"); - } - - [Fact] - public void ODataMinServiceVersion_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.ODataMinServiceVersion(), "request"); - } + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.ODataMinServiceVersion(), "request"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpResponseExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpResponseExtensionsTests.cs index 100e1acb1..6371758b0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpResponseExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/HttpResponseExtensionsTests.cs @@ -9,29 +9,28 @@ using Microsoft.AspNetCore.OData.Extensions; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +public class HttpResponseExtensionsTests { - public class HttpResponseExtensionsTests + [Fact] + public void IsSuccessStatusCode_ReturnsCorrectly() { - [Fact] - public void IsSuccessStatusCode_ReturnsCorrectly() - { - // null - HttpResponse response = null; - Assert.False(response.IsSuccessStatusCode()); + // null + HttpResponse response = null; + Assert.False(response.IsSuccessStatusCode()); - // 500 - response = new DefaultHttpContext().Response; - response.StatusCode = 500; - Assert.False(response.IsSuccessStatusCode()); + // 500 + response = new DefaultHttpContext().Response; + response.StatusCode = 500; + Assert.False(response.IsSuccessStatusCode()); - // 100 - response.StatusCode = 100; - Assert.False(response.IsSuccessStatusCode()); + // 100 + response.StatusCode = 100; + Assert.False(response.IsSuccessStatusCode()); - // Success - response.StatusCode = 202; - Assert.True(response.IsSuccessStatusCode()); - } + // Success + response.StatusCode = 202; + Assert.True(response.IsSuccessStatusCode()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/IHeaderDictionaryExtensions.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/IHeaderDictionaryExtensions.cs index 8e8941633..ce9d0bc05 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/IHeaderDictionaryExtensions.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/IHeaderDictionaryExtensions.cs @@ -9,31 +9,30 @@ using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +/// +/// Extensions for IHeaderDictionaryExtensions. +/// +public static class IHeaderDictionaryExtensions { /// - /// Extensions for IHeaderDictionaryExtensions. + /// Add to IfMatch values; /// - public static class IHeaderDictionaryExtensions + /// The IfMatch values. + public static void AddIfMatch(this IHeaderDictionary headers, EntityTagHeaderValue value) { - /// - /// Add to IfMatch values; - /// - /// The IfMatch values. - public static void AddIfMatch(this IHeaderDictionary headers, EntityTagHeaderValue value) - { - StringValues newValue = StringValues.Concat(headers["If-Match"], new StringValues(value.ToString())); - headers["If-Match"] = newValue; - } + StringValues newValue = StringValues.Concat(headers["If-Match"], new StringValues(value.ToString())); + headers["If-Match"] = newValue; + } - /// - /// Add to IfNoneMatch values. - /// - /// The IfNoneMatch values. - public static void AddIfNoneMatch(this IHeaderDictionary headers, EntityTagHeaderValue value) - { - StringValues newValue = StringValues.Concat(headers["If-None-Match"], new StringValues(value.ToString())); - headers["If-None-Match"] = newValue; - } + /// + /// Add to IfNoneMatch values. + /// + /// The IfNoneMatch values. + public static void AddIfNoneMatch(this IHeaderDictionary headers, EntityTagHeaderValue value) + { + StringValues newValue = StringValues.Concat(headers["If-None-Match"], new StringValues(value.ToString())); + headers["If-None-Match"] = newValue; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/LinkGeneratorHelpersTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/LinkGeneratorHelpersTests.cs index 144d6aa35..f390d938c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/LinkGeneratorHelpersTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/LinkGeneratorHelpersTests.cs @@ -18,145 +18,144 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +public class LinkGeneratorHelpersTests { - public class LinkGeneratorHelpersTests + [Fact] + public void CreateODataLink_ThrowsArgumentNull_Request() { - [Fact] - public void CreateODataLink_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.CreateODataLink(), "request"); - } + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.CreateODataLink(), "request"); + } - [Theory] - [InlineData("")] - [InlineData("odata")] - public void CreateODataLinkReturnsODataLinksAsExpected(string prefix) - { - // Arrange - string baseAddress = "http://localhost:8080/"; - HttpRequest request = RequestFactory.Create("Get", baseAddress, opt => opt.AddRouteComponents(prefix, EdmCoreModel.Instance)); - request.ODataFeature().RoutePrefix = prefix; + [Theory] + [InlineData("")] + [InlineData("odata")] + public void CreateODataLinkReturnsODataLinksAsExpected(string prefix) + { + // Arrange + string baseAddress = "http://localhost:8080/"; + HttpRequest request = RequestFactory.Create("Get", baseAddress, opt => opt.AddRouteComponents(prefix, EdmCoreModel.Instance)); + request.ODataFeature().RoutePrefix = prefix; - // Act - string odataLink = request.CreateODataLink(); + // Act + string odataLink = request.CreateODataLink(); - // Assert - Assert.Equal(baseAddress + prefix, odataLink); - } + // Assert + Assert.Equal(baseAddress + prefix, odataLink); + } - [Theory] - [InlineData("")] - [InlineData("odata")] - public void CreateODataLinkWithODataSegmentsReturnsODataLinksAsExpected(string prefix) - { - // Arrange - string baseAddress = "http://localhost:8080/"; - HttpRequest request = RequestFactory.Create("Get", baseAddress, opt => opt.AddRouteComponents(prefix, EdmCoreModel.Instance)); - request.ODataFeature().RoutePrefix = prefix; - - // Act - string odataLink = request.CreateODataLink(MetadataSegment.Instance); - - // Assert - if (prefix == "") - { - Assert.Equal($"{baseAddress}$metadata", odataLink); - } - else - { - Assert.Equal($"{baseAddress}{prefix}/$metadata", odataLink); - } - } + [Theory] + [InlineData("")] + [InlineData("odata")] + public void CreateODataLinkWithODataSegmentsReturnsODataLinksAsExpected(string prefix) + { + // Arrange + string baseAddress = "http://localhost:8080/"; + HttpRequest request = RequestFactory.Create("Get", baseAddress, opt => opt.AddRouteComponents(prefix, EdmCoreModel.Instance)); + request.ODataFeature().RoutePrefix = prefix; - [Theory] - [InlineData("v1")] - [InlineData("v2")] - [InlineData("anything")] - public void CreateODataLinkReturnsODataLinksWithTemplateAsExpected(string value) + // Act + string odataLink = request.CreateODataLink(MetadataSegment.Instance); + + // Assert + if (prefix == "") { - // Arrange - IServiceCollection services = new ServiceCollection(); - services.AddOptions(); - services.AddRouting(); // because want to use "TemplateBinderFactory" - IServiceProvider sp = services.BuildServiceProvider(); - - string baseAddress = "http://localhost:8080/"; - string prefix = "odata{version}"; - HttpRequest request = RequestFactory.Create("Get", baseAddress, opt => opt.AddRouteComponents(prefix, EdmCoreModel.Instance)); - request.ODataFeature().RoutePrefix = prefix; - request.RouteValues = new RouteValueDictionary(new { version = value }); - request.HttpContext.RequestServices = sp; // global level SP - - // Act - string odataLink = request.CreateODataLink(); - - // Assert - Assert.Equal(baseAddress + $"odata{value}", odataLink); + Assert.Equal($"{baseAddress}$metadata", odataLink); } - - [Theory] - [InlineData("v1")] - [InlineData("v2")] - [InlineData("anything")] - public void CreateODataLinkWithODataSegmentsReturnsODataLinksWithTemplateAsExpected(string value) + else { - // Arrange - IServiceCollection services = new ServiceCollection(); - services.AddOptions(); - services.AddRouting(); // because want to use "TemplateBinderFactory" - IServiceProvider sp = services.BuildServiceProvider(); - - string baseAddress = "http://localhost:8080/"; - string prefix = "odata{version}"; - HttpRequest request = RequestFactory.Create("Get", baseAddress, opt => opt.AddRouteComponents(prefix, EdmCoreModel.Instance)); - request.ODataFeature().RoutePrefix = prefix; - request.RouteValues = new RouteValueDictionary(new { version = value }); - request.HttpContext.RequestServices = sp; // global level SP - - // Act - string odataLink = request.CreateODataLink(MetadataSegment.Instance); - - // Assert - Assert.Equal($"{baseAddress}odata{value}/$metadata", odataLink); + Assert.Equal($"{baseAddress}{prefix}/$metadata", odataLink); } } - internal static class EndpointFactory + [Theory] + [InlineData("v1")] + [InlineData("v2")] + [InlineData("anything")] + public void CreateODataLinkReturnsODataLinksWithTemplateAsExpected(string value) { - public static RouteEndpoint CreateRouteEndpoint( - string template, - object defaults = null, - object policies = null, - object requiredValues = null, - int order = 0, - string displayName = null, - params object[] metadata) - { - var routePattern = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); + // Arrange + IServiceCollection services = new ServiceCollection(); + services.AddOptions(); + services.AddRouting(); // because want to use "TemplateBinderFactory" + IServiceProvider sp = services.BuildServiceProvider(); + + string baseAddress = "http://localhost:8080/"; + string prefix = "odata{version}"; + HttpRequest request = RequestFactory.Create("Get", baseAddress, opt => opt.AddRouteComponents(prefix, EdmCoreModel.Instance)); + request.ODataFeature().RoutePrefix = prefix; + request.RouteValues = new RouteValueDictionary(new { version = value }); + request.HttpContext.RequestServices = sp; // global level SP + + // Act + string odataLink = request.CreateODataLink(); + + // Assert + Assert.Equal(baseAddress + $"odata{value}", odataLink); + } - return CreateRouteEndpoint(routePattern, order, displayName, metadata); - } + [Theory] + [InlineData("v1")] + [InlineData("v2")] + [InlineData("anything")] + public void CreateODataLinkWithODataSegmentsReturnsODataLinksWithTemplateAsExpected(string value) + { + // Arrange + IServiceCollection services = new ServiceCollection(); + services.AddOptions(); + services.AddRouting(); // because want to use "TemplateBinderFactory" + IServiceProvider sp = services.BuildServiceProvider(); + + string baseAddress = "http://localhost:8080/"; + string prefix = "odata{version}"; + HttpRequest request = RequestFactory.Create("Get", baseAddress, opt => opt.AddRouteComponents(prefix, EdmCoreModel.Instance)); + request.ODataFeature().RoutePrefix = prefix; + request.RouteValues = new RouteValueDictionary(new { version = value }); + request.HttpContext.RequestServices = sp; // global level SP + + // Act + string odataLink = request.CreateODataLink(MetadataSegment.Instance); + + // Assert + Assert.Equal($"{baseAddress}odata{value}/$metadata", odataLink); + } +} - public static RouteEndpoint CreateRouteEndpoint( - RoutePattern routePattern = null, - int order = 0, - string displayName = null, - IList metadata = null) - { - return new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - routePattern, - order, - new EndpointMetadataCollection(metadata ?? Array.Empty()), - displayName); - } +internal static class EndpointFactory +{ + public static RouteEndpoint CreateRouteEndpoint( + string template, + object defaults = null, + object policies = null, + object requiredValues = null, + int order = 0, + string displayName = null, + params object[] metadata) + { + var routePattern = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); + + return CreateRouteEndpoint(routePattern, order, displayName, metadata); } - public static class TestConstants + public static RouteEndpoint CreateRouteEndpoint( + RoutePattern routePattern = null, + int order = 0, + string displayName = null, + IList metadata = null) { - internal static readonly RequestDelegate EmptyRequestDelegate = (context) => Task.CompletedTask; + return new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + routePattern, + order, + new EndpointMetadataCollection(metadata ?? Array.Empty()), + displayName); } } + +public static class TestConstants +{ + internal static readonly RequestDelegate EmptyRequestDelegate = (context) => Task.CompletedTask; +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/RequestFactory.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/RequestFactory.cs index 19cdbd45e..81e696c9e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/RequestFactory.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/RequestFactory.cs @@ -17,224 +17,223 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +/// +/// A class to create HttpRequest for tests. +/// +public static class RequestFactory { /// - /// A class to create HttpRequest for tests. + /// Reads the request body as string. /// - public static class RequestFactory + /// The Http request. + /// true/false for multiple read. + /// The request body or empty string. + public static string ReadBody(this HttpRequest request, bool multipleRead = false) { - /// - /// Reads the request body as string. - /// - /// The Http request. - /// true/false for multiple read. - /// The request body or empty string. - public static string ReadBody(this HttpRequest request, bool multipleRead = false) + if (request == null || request.Body == null) { - if (request == null || request.Body == null) - { - return ""; - } - - // Allows using several time the stream in ASP.Net Core - if (multipleRead) - { - request.EnableBuffering(); - } - - string requestBody = ""; - using (StreamReader reader = new StreamReader(request.Body, Encoding.UTF8, true, 1024, true)) - { - requestBody = reader.ReadToEnd(); - } - - // Rewind, so the core is not lost when it looks the body for the request - if (multipleRead) - { - request.Body.Position = 0; - } - - return requestBody; + return ""; } - /// - /// Creates the with OData configuration. - /// - /// The OData options configuration. - /// The Http Request. - public static HttpRequest Create(Action setupAction) + // Allows using several time the stream in ASP.Net Core + if (multipleRead) { - return Create("Get", "http://localhost", setupAction); + request.EnableBuffering(); } - /// - /// Creates the with OData configuration. - /// - /// The http method. - /// The http request uri. - /// The OData configuration. - /// The HttpRequest. - public static HttpRequest Create(string method, string uri, Action setupAction = null) + string requestBody = ""; + using (StreamReader reader = new StreamReader(request.Body, Encoding.UTF8, true, 1024, true)) { - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - - IServiceCollection services = new ServiceCollection(); - if (setupAction != null) - { - services.Configure(setupAction); - } - - context.RequestServices = services.BuildServiceProvider(); - - request.Method = method; - Uri requestUri = new Uri(uri); - request.Scheme = requestUri.Scheme; - request.Host = requestUri.IsDefaultPort ? new HostString(requestUri.Host) : new HostString(requestUri.Host, requestUri.Port); - request.QueryString = new QueryString(requestUri.Query); - request.Path = new PathString(requestUri.AbsolutePath); - - //request.Host = HostString.FromUriComponent(BaseAddress); - //if (BaseAddress.IsDefaultPort) - //{ - // request.Host = new HostString(request.Host.Host); - //} - //var pathBase = PathString.FromUriComponent(BaseAddress); - //if (pathBase.HasValue && pathBase.Value.EndsWith("/")) - //{ - // pathBase = new PathString(pathBase.Value[..^1]); // All but the last character. - //} - //request.PathBase = pathBase; - - return request; + requestBody = reader.ReadToEnd(); } - /// - /// Create the default HttpRequest. - /// - /// The built default HttpRequest. - public static HttpRequest Create() + // Rewind, so the core is not lost when it looks the body for the request + if (multipleRead) { - HttpContext context = new DefaultHttpContext(); - return context.Request; + request.Body.Position = 0; } - /// - /// Create the HttpRequest with IEdmModel. - /// - /// The given Edm model. - /// The created HttpRequest. - public static HttpRequest Create(IEdmModel model) => Create("Get", "http://localhost/", model); - - /// - /// Create the HttpRequest with IEdmModel. - /// - /// The given Edm model. - /// The OData configuration. - /// The created HttpRequest. - public static HttpRequest Create(IEdmModel model, Action setupAction) - { - HttpRequest request = Create("Get", "http://localhost/", setupAction); - IODataFeature feature = request.ODataFeature(); - feature.RoutePrefix = ""; - feature.Model = model; - return request; - } + return requestBody; + } - /// - /// - /// - /// - /// - /// - public static HttpRequest Create(IEdmModel model, ODataPath path) - { - HttpContext context = new DefaultHttpContext(); - context.ODataFeature().Model = model; - context.ODataFeature().Path = path; - return context.Request; - } + /// + /// Creates the with OData configuration. + /// + /// The OData options configuration. + /// The Http Request. + public static HttpRequest Create(Action setupAction) + { + return Create("Get", "http://localhost", setupAction); + } - /// - /// - /// - /// - /// - public static HttpRequest Create(Action setupAction) - { - HttpContext context = new DefaultHttpContext(); - IODataFeature odataFeature = context.ODataFeature(); - setupAction?.Invoke(odataFeature); - return context.Request; - } + /// + /// Creates the with OData configuration. + /// + /// The http method. + /// The http request uri. + /// The OData configuration. + /// The HttpRequest. + public static HttpRequest Create(string method, string uri, Action setupAction = null) + { + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; - public static HttpRequest Create(string method, string uri, IEdmModel model) + IServiceCollection services = new ServiceCollection(); + if (setupAction != null) { - // HttpRequest request = Create(method, uri, opt => opt.AddModel("odata", model)); - HttpRequest request = Create(method, uri, setupAction: null); - IODataFeature feature = request.ODataFeature(); - feature.RoutePrefix = ""; - feature.Model = model; - return request; + services.Configure(setupAction); } - /// - /// Configures the http request with OData values. - /// - /// The http request. - /// The prefix. - /// The Edm model. - /// The OData path. - /// - public static HttpRequest Configure(this HttpRequest request, string prefix, IEdmModel model, ODataPath path) + context.RequestServices = services.BuildServiceProvider(); + + request.Method = method; + Uri requestUri = new Uri(uri); + request.Scheme = requestUri.Scheme; + request.Host = requestUri.IsDefaultPort ? new HostString(requestUri.Host) : new HostString(requestUri.Host, requestUri.Port); + request.QueryString = new QueryString(requestUri.Query); + request.Path = new PathString(requestUri.AbsolutePath); + + //request.Host = HostString.FromUriComponent(BaseAddress); + //if (BaseAddress.IsDefaultPort) + //{ + // request.Host = new HostString(request.Host.Host); + //} + //var pathBase = PathString.FromUriComponent(BaseAddress); + //if (pathBase.HasValue && pathBase.Value.EndsWith("/")) + //{ + // pathBase = new PathString(pathBase.Value[..^1]); // All but the last character. + //} + //request.PathBase = pathBase; + + return request; + } + + /// + /// Create the default HttpRequest. + /// + /// The built default HttpRequest. + public static HttpRequest Create() + { + HttpContext context = new DefaultHttpContext(); + return context.Request; + } + + /// + /// Create the HttpRequest with IEdmModel. + /// + /// The given Edm model. + /// The created HttpRequest. + public static HttpRequest Create(IEdmModel model) => Create("Get", "http://localhost/", model); + + /// + /// Create the HttpRequest with IEdmModel. + /// + /// The given Edm model. + /// The OData configuration. + /// The created HttpRequest. + public static HttpRequest Create(IEdmModel model, Action setupAction) + { + HttpRequest request = Create("Get", "http://localhost/", setupAction); + IODataFeature feature = request.ODataFeature(); + feature.RoutePrefix = ""; + feature.Model = model; + return request; + } + + /// + /// + /// + /// + /// + /// + public static HttpRequest Create(IEdmModel model, ODataPath path) + { + HttpContext context = new DefaultHttpContext(); + context.ODataFeature().Model = model; + context.ODataFeature().Path = path; + return context.Request; + } + + /// + /// + /// + /// + /// + public static HttpRequest Create(Action setupAction) + { + HttpContext context = new DefaultHttpContext(); + IODataFeature odataFeature = context.ODataFeature(); + setupAction?.Invoke(odataFeature); + return context.Request; + } + + public static HttpRequest Create(string method, string uri, IEdmModel model) + { + // HttpRequest request = Create(method, uri, opt => opt.AddModel("odata", model)); + HttpRequest request = Create(method, uri, setupAction: null); + IODataFeature feature = request.ODataFeature(); + feature.RoutePrefix = ""; + feature.Model = model; + return request; + } + + /// + /// Configures the http request with OData values. + /// + /// The http request. + /// The prefix. + /// The Edm model. + /// The OData path. + /// + public static HttpRequest Configure(this HttpRequest request, string prefix, IEdmModel model, ODataPath path) + { + if (request == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - IODataFeature feature = request.ODataFeature(); - feature.RoutePrefix = prefix; - feature.Model = model; - feature.Path = path; - return request; + throw new ArgumentNullException(nameof(request)); } - private static HttpRequest CreateRequest(IHeaderDictionary headers) + IODataFeature feature = request.ODataFeature(); + feature.RoutePrefix = prefix; + feature.Model = model; + feature.Path = path; + return request; + } + + private static HttpRequest CreateRequest(IHeaderDictionary headers) + { + var context = new DefaultHttpContext(); + context.Features.Get().Headers = headers; + return context.Request; + } + + public static TKey GetKeyFromLinkUri(this HttpRequest request, Uri link) + { + if (request == null) { - var context = new DefaultHttpContext(); - context.Features.Get().Headers = headers; - return context.Request; + throw new ArgumentNullException(nameof(request)); } - public static TKey GetKeyFromLinkUri(this HttpRequest request, Uri link) + if (link == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - if (link == null) - { - throw new ArgumentNullException(nameof(link)); - } + throw new ArgumentNullException(nameof(link)); + } - var serviceRoot = request.CreateODataLink(); - IEdmModel model = request.GetModel(); + var serviceRoot = request.CreateODataLink(); + IEdmModel model = request.GetModel(); - ODataUriParser uriParser = new ODataUriParser(model, new Uri(serviceRoot), new Uri(link.LocalPath, UriKind.Relative), - request.GetRouteServices()); + ODataUriParser uriParser = new ODataUriParser(model, new Uri(serviceRoot), new Uri(link.LocalPath, UriKind.Relative), + request.GetRouteServices()); - var odataPath = uriParser.ParsePath(); + var odataPath = uriParser.ParsePath(); - var keySegment = odataPath.Where(p => p is KeySegment).FirstOrDefault() as KeySegment; + var keySegment = odataPath.Where(p => p is KeySegment).FirstOrDefault() as KeySegment; - if (keySegment == null || !keySegment.Keys.Any()) - throw new InvalidOperationException("This link does not contain a key."); + if (keySegment == null || !keySegment.Keys.Any()) + throw new InvalidOperationException("This link does not contain a key."); - // Return the key value of the first segment - return (TKey)keySegment.Keys.First().Value; - } + // Return the key value of the first segment + return (TKey)keySegment.Keys.First().Value; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/RequestPreferenceHelpersTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/RequestPreferenceHelpersTests.cs index 676d1f666..a40622198 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/RequestPreferenceHelpersTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/RequestPreferenceHelpersTests.cs @@ -12,35 +12,34 @@ using Microsoft.Extensions.Primitives; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +public class RequestPreferenceHelpersTests { - public class RequestPreferenceHelpersTests + [Fact] + public void RequestPrefersMaxPageSize_ReturnsFalse_WithPreferHeader() { - [Fact] - public void RequestPrefersMaxPageSize_ReturnsFalse_WithPreferHeader() - { - // Arrange - HeaderDictionary headers = new HeaderDictionary(); + // Arrange + HeaderDictionary headers = new HeaderDictionary(); - // Act & Assert - Assert.False(RequestPreferenceHelpers.RequestPrefersMaxPageSize(headers, out _)); - } + // Act & Assert + Assert.False(RequestPreferenceHelpers.RequestPrefersMaxPageSize(headers, out _)); + } - [Theory] - [InlineData("maxpagesize=5")] - [InlineData("odata.maxpagesize=5")] - public void RequestPrefersMaxPageSize_ReturnsPageSize_WithPreferValue(string preferValue) - { - // Arrange - HeaderDictionary headers = new HeaderDictionary( - new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "Prefer", preferValue } - }); + [Theory] + [InlineData("maxpagesize=5")] + [InlineData("odata.maxpagesize=5")] + public void RequestPrefersMaxPageSize_ReturnsPageSize_WithPreferValue(string preferValue) + { + // Arrange + HeaderDictionary headers = new HeaderDictionary( + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Prefer", preferValue } + }); - // Act & Assert - Assert.True(RequestPreferenceHelpers.RequestPrefersMaxPageSize(headers, out int pageSize)); - Assert.Equal(5, pageSize); - } + // Act & Assert + Assert.True(RequestPreferenceHelpers.RequestPrefersMaxPageSize(headers, out int pageSize)); + Assert.Equal(5, pageSize); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/ResponseFactory.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/ResponseFactory.cs index 7119a8075..056fcccc3 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/ResponseFactory.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/ResponseFactory.cs @@ -9,49 +9,48 @@ using System.Text; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +/// +/// A class to create HttpRequest. +/// +public static class ResponseFactory { - /// - /// A class to create HttpRequest. - /// - public static class ResponseFactory + public static string ReadBody(this HttpResponse response) { - public static string ReadBody(this HttpResponse response) + if (response.Body == null) { - if (response.Body == null) - { - return ""; - } - - response.Body.Position = 0; - string requestBody = ""; - using (StreamReader reader = new StreamReader(response.Body, Encoding.UTF8, true, 1024, true)) - { - requestBody = reader.ReadToEnd(); - } - - return requestBody; + return ""; } - /// - /// - /// - /// - public static HttpResponse Create() + response.Body.Position = 0; + string requestBody = ""; + using (StreamReader reader = new StreamReader(response.Body, Encoding.UTF8, true, 1024, true)) { - HttpContext context = new DefaultHttpContext(); - return context.Response; + requestBody = reader.ReadToEnd(); } - /// - /// - /// - /// - public static HttpResponse Create(int statusCode) - { - HttpContext context = new DefaultHttpContext(); - context.Response.StatusCode = statusCode; - return context.Response; - } + return requestBody; + } + + /// + /// + /// + /// + public static HttpResponse Create() + { + HttpContext context = new DefaultHttpContext(); + return context.Response; + } + + /// + /// + /// + /// + public static HttpResponse Create(int statusCode) + { + HttpContext context = new DefaultHttpContext(); + context.Response.StatusCode = statusCode; + return context.Response; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/SerializableErrorExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/SerializableErrorExtensionsTests.cs index 069f56a55..75aca9d64 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/SerializableErrorExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/SerializableErrorExtensionsTests.cs @@ -12,87 +12,86 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +public class SerializableErrorExtensionsTests { - public class SerializableErrorExtensionsTests + [Fact] + public void CreateODataError_ThrowsArgumentNull_SerializableError() { - [Fact] - public void CreateODataError_ThrowsArgumentNull_SerializableError() - { - // Arrange & Act & Assert - SerializableError serializableError = null; - ExceptionAssert.ThrowsArgumentNull(() => SerializableErrorExtensions.CreateODataError(serializableError), "serializableError"); - } + // Arrange & Act & Assert + SerializableError serializableError = null; + ExceptionAssert.ThrowsArgumentNull(() => SerializableErrorExtensions.CreateODataError(serializableError), "serializableError"); + } - [Fact] - public void CreateODataError_Creates_ODataError_UsingModelStateDictionary() - { - // Arrange & Act & Assert - ModelStateDictionary modelState = new ModelStateDictionary(); - modelState.AddModelError("key1", "Test Error 1"); - modelState.AddModelError("key1", "Test Error 2"); - modelState.AddModelError("key3", "Test Error 3"); - SerializableError serializableError = new SerializableError(modelState); + [Fact] + public void CreateODataError_Creates_ODataError_UsingModelStateDictionary() + { + // Arrange & Act & Assert + ModelStateDictionary modelState = new ModelStateDictionary(); + modelState.AddModelError("key1", "Test Error 1"); + modelState.AddModelError("key1", "Test Error 2"); + modelState.AddModelError("key3", "Test Error 3"); + SerializableError serializableError = new SerializableError(modelState); - // Act - ODataError error = SerializableErrorExtensions.CreateODataError(serializableError); + // Act + ODataError error = SerializableErrorExtensions.CreateODataError(serializableError); - // Assert - Assert.NotNull(error); - Assert.Equal("key1:\r\nTest Error 1\r\nTest Error 2\r\n\r\nkey3:\r\nTest Error 3", error.Message); - Assert.Null(error.Code); - Assert.Null(error.InnerError); - Assert.Equal(3, error.Details.Count); - } + // Assert + Assert.NotNull(error); + Assert.Equal("key1:\r\nTest Error 1\r\nTest Error 2\r\n\r\nkey3:\r\nTest Error 3", error.Message); + Assert.Null(error.Code); + Assert.Null(error.InnerError); + Assert.Equal(3, error.Details.Count); + } - [Fact] - public void CreateODataError_Creates_BasicODataError_WithoutModelStateDictionary() - { - // Arrange & Act & Assert - ModelStateDictionary modelState = new ModelStateDictionary(); - modelState.AddModelError("key3", "Test Error 3"); - SerializableError innerSerializableError = new SerializableError(modelState); + [Fact] + public void CreateODataError_Creates_BasicODataError_WithoutModelStateDictionary() + { + // Arrange & Act & Assert + ModelStateDictionary modelState = new ModelStateDictionary(); + modelState.AddModelError("key3", "Test Error 3"); + SerializableError innerSerializableError = new SerializableError(modelState); - SerializableError serializableError = new SerializableError(); - serializableError["key1"] = "Test Error 1"; - serializableError["key2"] = "Test Error 2"; - serializableError["ModelState"] = innerSerializableError; + SerializableError serializableError = new SerializableError(); + serializableError["key1"] = "Test Error 1"; + serializableError["key2"] = "Test Error 2"; + serializableError["ModelState"] = innerSerializableError; - // Act - ODataError error = SerializableErrorExtensions.CreateODataError(serializableError); + // Act + ODataError error = SerializableErrorExtensions.CreateODataError(serializableError); - // Assert - Assert.NotNull(error); - Assert.Equal("key1:\r\nTest Error 1\r\n\r\nkey2:\r\nTest Error 2", error.Message); - Assert.Null(error.Code); - Assert.True(error.InnerError.Properties.TryGetValue(SerializableErrorKeys.MessageKey, out ODataValue odataValue)); - var exceptionMessage = Assert.IsType(odataValue).Value as string; - Assert.Equal("key3:\r\nTest Error 3", exceptionMessage); - Assert.Equal(2, error.Details.Count); - } + // Assert + Assert.NotNull(error); + Assert.Equal("key1:\r\nTest Error 1\r\n\r\nkey2:\r\nTest Error 2", error.Message); + Assert.Null(error.Code); + Assert.True(error.InnerError.Properties.TryGetValue(SerializableErrorKeys.MessageKey, out ODataValue odataValue)); + var exceptionMessage = Assert.IsType(odataValue).Value as string; + Assert.Equal("key3:\r\nTest Error 3", exceptionMessage); + Assert.Equal(2, error.Details.Count); + } - [Fact] - public void CreateODataError_Creates_AdvancedODataError() - { - // Arrange & Act & Assert - ModelStateDictionary modelState = new ModelStateDictionary(); - modelState.AddModelError("key1", "Test Error 1"); - SerializableError serializableError = new SerializableError(modelState); - serializableError["ErrorCode"] = "Error Code 1"; - serializableError["message"] = "Error Message 1"; - serializableError["ExceptionMessage"] = "Error ExceptionMessage 1"; + [Fact] + public void CreateODataError_Creates_AdvancedODataError() + { + // Arrange & Act & Assert + ModelStateDictionary modelState = new ModelStateDictionary(); + modelState.AddModelError("key1", "Test Error 1"); + SerializableError serializableError = new SerializableError(modelState); + serializableError["ErrorCode"] = "Error Code 1"; + serializableError["message"] = "Error Message 1"; + serializableError["ExceptionMessage"] = "Error ExceptionMessage 1"; - // Act - ODataError error = SerializableErrorExtensions.CreateODataError(serializableError); + // Act + ODataError error = SerializableErrorExtensions.CreateODataError(serializableError); - // Assert - Assert.NotNull(error); - Assert.Equal("Error Message 1", error.Message); - Assert.Equal("Error Code 1", error.Code); - Assert.True(error.InnerError.Properties.TryGetValue(SerializableErrorKeys.MessageKey, out ODataValue odataValue)); - var exceptionMessage = Assert.IsType(odataValue).Value as string; - Assert.Equal("Error ExceptionMessage 1", exceptionMessage); - Assert.Single(error.Details); - } + // Assert + Assert.NotNull(error); + Assert.Equal("Error Message 1", error.Message); + Assert.Equal("Error Code 1", error.Code); + Assert.True(error.InnerError.Properties.TryGetValue(SerializableErrorKeys.MessageKey, out ODataValue odataValue)); + var exceptionMessage = Assert.IsType(odataValue).Value as string; + Assert.Equal("Error ExceptionMessage 1", exceptionMessage); + Assert.Single(error.Details); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Extensions/SerializeUtils.cs b/test/Microsoft.AspNetCore.OData.Tests/Extensions/SerializeUtils.cs index 34419cf7c..655c04ce7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Extensions/SerializeUtils.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Extensions/SerializeUtils.cs @@ -10,51 +10,50 @@ using System.Text.Json; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.OData.Tests.Extensions +namespace Microsoft.AspNetCore.OData.Tests.Extensions; + +/// +/// Serialize Utils +/// +public static class SerializeUtils { - /// - /// Serialize Utils - /// - public static class SerializeUtils + public static string SerializeAsJson(Action action) { - public static string SerializeAsJson(Action action) + using (MemoryStream ms = new MemoryStream()) { - using (MemoryStream ms = new MemoryStream()) + JsonWriterOptions options = new JsonWriterOptions + { + Indented = true + }; + + using (Utf8JsonWriter jsonWriter = new Utf8JsonWriter(ms)) { - JsonWriterOptions options = new JsonWriterOptions - { - Indented = true - }; - - using (Utf8JsonWriter jsonWriter = new Utf8JsonWriter(ms)) - { - action(jsonWriter); - jsonWriter.Flush(); - } - - ms.Seek(0, SeekOrigin.Begin); - return new StreamReader(ms).ReadToEnd(); + action(jsonWriter); + jsonWriter.Flush(); } + + ms.Seek(0, SeekOrigin.Begin); + return new StreamReader(ms).ReadToEnd(); } + } - public static async Task SerializeAsJsonAsync(Action action) + public static async Task SerializeAsJsonAsync(Action action) + { + using (MemoryStream ms = new MemoryStream()) { - using (MemoryStream ms = new MemoryStream()) + JsonWriterOptions options = new JsonWriterOptions + { + Indented = true + }; + + using (Utf8JsonWriter jsonWriter = new Utf8JsonWriter(ms)) { - JsonWriterOptions options = new JsonWriterOptions - { - Indented = true - }; - - using (Utf8JsonWriter jsonWriter = new Utf8JsonWriter(ms)) - { - action(jsonWriter); - await jsonWriter.FlushAsync(); - } - - ms.Seek(0, SeekOrigin.Begin); - return await new StreamReader(ms).ReadToEndAsync(); + action(jsonWriter); + await jsonWriter.FlushAsync(); } + + ms.Seek(0, SeekOrigin.Begin); + return await new StreamReader(ms).ReadToEndAsync(); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ConventionsHelpersTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ConventionsHelpersTests.cs index 185caaa23..f8b122f2e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ConventionsHelpersTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ConventionsHelpersTests.cs @@ -16,369 +16,368 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ConventionsHelpersTests { - public class ConventionsHelpersTests - { - private ODataSerializerContext _writeContext = new ODataSerializerContext { Model = EdmCoreModel.Instance }; + private ODataSerializerContext _writeContext = new ODataSerializerContext { Model = EdmCoreModel.Instance }; - public static TheoryDataSet GetEntityKeyValue_SingleKey_DifferentDataTypes_Data + public static TheoryDataSet GetEntityKeyValue_SingleKey_DifferentDataTypes_Data + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { 1, "1" }, - { "1", "'1'" }, - { new DateTimeOffset(new DateTime(2012,12,31,0,0,0,DateTimeKind.Utc)), "2012-12-31T00:00:00Z" }, - { new byte[] { 1,2 }, "binary'AQI='" }, - { false, "false" }, - { true, "true" }, - { new Guid("dddddddd-dddd-dddd-dddd-dddddddddddd"), "dddddddd-dddd-dddd-dddd-dddddddddddd" }, - { new Date(2014, 10, 14), "2014-10-14"}, - { new TimeOfDay(15, 38, 25, 109), "15:38:25.1090000"}, - }; - } + { 1, "1" }, + { "1", "'1'" }, + { new DateTimeOffset(new DateTime(2012,12,31,0,0,0,DateTimeKind.Utc)), "2012-12-31T00:00:00Z" }, + { new byte[] { 1,2 }, "binary'AQI='" }, + { false, "false" }, + { true, "true" }, + { new Guid("dddddddd-dddd-dddd-dddd-dddddddddddd"), "dddddddd-dddd-dddd-dddd-dddddddddddd" }, + { new Date(2014, 10, 14), "2014-10-14"}, + { new TimeOfDay(15, 38, 25, 109), "15:38:25.1090000"}, + }; } + } - [Fact] - public void GetEntityKey_ForNonEntityType() - { - // Arrange - EdmComplexType complexType = new EdmComplexType("NS", "Name"); - var entityInstance = new { Property = "key" }; + [Fact] + public void GetEntityKey_ForNonEntityType() + { + // Arrange + EdmComplexType complexType = new EdmComplexType("NS", "Name"); + var entityInstance = new { Property = "key" }; - ResourceContext entityContext = new ResourceContext(_writeContext, complexType.AsReference(), entityInstance); + ResourceContext entityContext = new ResourceContext(_writeContext, complexType.AsReference(), entityInstance); - // Act - var keyValue = ConventionsHelpers.GetEntityKey(entityContext); + // Act + var keyValue = ConventionsHelpers.GetEntityKey(entityContext); - // Assert - Assert.Empty(keyValue); - } + // Assert + Assert.Empty(keyValue); + } - [Fact] - public void GetEntityKeyValue_ForNonEntityType() - { - // Arrange - EdmComplexType complexType = new EdmComplexType("NS", "Name"); - var entityInstance = new { Property = "key" }; + [Fact] + public void GetEntityKeyValue_ForNonEntityType() + { + // Arrange + EdmComplexType complexType = new EdmComplexType("NS", "Name"); + var entityInstance = new { Property = "key" }; - ResourceContext entityContext = new ResourceContext(_writeContext, complexType.AsReference(), entityInstance); + ResourceContext entityContext = new ResourceContext(_writeContext, complexType.AsReference(), entityInstance); - // Act - var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); + // Act + var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); - // Assert - Assert.Equal(string.Empty, keyValue); - } + // Assert + Assert.Equal(string.Empty, keyValue); + } - [Fact] - public void GetEntityKeyValue_SingleKey() - { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "Name"); - entityType.AddKeys(entityType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.String)); - var entityInstance = new { Property = "key" }; + [Fact] + public void GetEntityKeyValue_SingleKey() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "Name"); + entityType.AddKeys(entityType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.String)); + var entityInstance = new { Property = "key" }; - ResourceContext entityContext = new ResourceContext(_writeContext, entityType.AsReference(), entityInstance); + ResourceContext entityContext = new ResourceContext(_writeContext, entityType.AsReference(), entityInstance); - // Act - var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); + // Act + var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); - // Assert - Assert.Equal("'key'", keyValue); - } + // Assert + Assert.Equal("'key'", keyValue); + } - [Theory] - [MemberData(nameof(GetEntityKeyValue_SingleKey_DifferentDataTypes_Data))] - public void GetEntityKeyValue_SingleKey_DifferentDataTypes(object value, object expectedValue) - { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "Name"); - entityType.AddKeys(entityType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.String)); - var entityInstance = new { Property = value }; + [Theory] + [MemberData(nameof(GetEntityKeyValue_SingleKey_DifferentDataTypes_Data))] + public void GetEntityKeyValue_SingleKey_DifferentDataTypes(object value, object expectedValue) + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "Name"); + entityType.AddKeys(entityType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.String)); + var entityInstance = new { Property = value }; - ResourceContext entityContext = new ResourceContext(_writeContext, entityType.AsReference(), entityInstance); + ResourceContext entityContext = new ResourceContext(_writeContext, entityType.AsReference(), entityInstance); - // Act - var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); + // Act + var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); - // Assert - Assert.Equal(expectedValue, keyValue); - } + // Assert + Assert.Equal(expectedValue, keyValue); + } - [Fact] - public void GetEntityKeyValue_MultipleKeys() - { - // Arrange - var entityInstance = new { Key1 = "key1", Key2 = 2, Key3 = true }; - EdmEntityType entityType = new EdmEntityType("NS", "Name"); - entityType.AddKeys(entityType.AddStructuralProperty("Key1", EdmPrimitiveTypeKind.String)); - entityType.AddKeys(entityType.AddStructuralProperty("Key2", EdmPrimitiveTypeKind.Int32)); - entityType.AddKeys(entityType.AddStructuralProperty("Key3", EdmPrimitiveTypeKind.Boolean)); + [Fact] + public void GetEntityKeyValue_MultipleKeys() + { + // Arrange + var entityInstance = new { Key1 = "key1", Key2 = 2, Key3 = true }; + EdmEntityType entityType = new EdmEntityType("NS", "Name"); + entityType.AddKeys(entityType.AddStructuralProperty("Key1", EdmPrimitiveTypeKind.String)); + entityType.AddKeys(entityType.AddStructuralProperty("Key2", EdmPrimitiveTypeKind.Int32)); + entityType.AddKeys(entityType.AddStructuralProperty("Key3", EdmPrimitiveTypeKind.Boolean)); - ResourceContext entityContext = new ResourceContext(_writeContext, entityType.AsReference(), entityInstance); + ResourceContext entityContext = new ResourceContext(_writeContext, entityType.AsReference(), entityInstance); - // Act - var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); + // Act + var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); - // Assert - Assert.Equal("Key1='key1',Key2=2,Key3=true", keyValue); - } + // Assert + Assert.Equal("Key1='key1',Key2=2,Key3=true", keyValue); + } - [Fact] - public void GetEntityKeyValue_MultipleKeys_ForDateAndTimeOfDay() - { - // Arrange - var entityInstance = new { Key1 = new Date(2015, 12, 2), Key2 = new TimeOfDay(4, 3, 2, 1) }; - EdmEntityType entityType = new EdmEntityType("NS", "Name"); - entityType.AddKeys(entityType.AddStructuralProperty("Key1", EdmPrimitiveTypeKind.Date)); - entityType.AddKeys(entityType.AddStructuralProperty("Key2", EdmPrimitiveTypeKind.TimeOfDay)); + [Fact] + public void GetEntityKeyValue_MultipleKeys_ForDateAndTimeOfDay() + { + // Arrange + var entityInstance = new { Key1 = new Date(2015, 12, 2), Key2 = new TimeOfDay(4, 3, 2, 1) }; + EdmEntityType entityType = new EdmEntityType("NS", "Name"); + entityType.AddKeys(entityType.AddStructuralProperty("Key1", EdmPrimitiveTypeKind.Date)); + entityType.AddKeys(entityType.AddStructuralProperty("Key2", EdmPrimitiveTypeKind.TimeOfDay)); - ResourceContext entityContext = new ResourceContext(_writeContext, - entityType.AsReference(), entityInstance); + ResourceContext entityContext = new ResourceContext(_writeContext, + entityType.AsReference(), entityInstance); - // Act - var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); + // Act + var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); - // Assert - Assert.Equal("Key1=2015-12-02,Key2=04:03:02.0010000", keyValue); - } + // Assert + Assert.Equal("Key1=2015-12-02,Key2=04:03:02.0010000", keyValue); + } - [Fact] - public void GetEntityKeyValue_ThrowsForNullKeys() - { - // Arrange - var entityInstance = new { Key = (string)null }; - EdmEntityType entityType = new EdmEntityType("NS", "Name"); - entityType.AddKeys(entityType.AddStructuralProperty("Key", EdmPrimitiveTypeKind.String)); + [Fact] + public void GetEntityKeyValue_ThrowsForNullKeys() + { + // Arrange + var entityInstance = new { Key = (string)null }; + EdmEntityType entityType = new EdmEntityType("NS", "Name"); + entityType.AddKeys(entityType.AddStructuralProperty("Key", EdmPrimitiveTypeKind.String)); - ResourceContext entityContext = new ResourceContext(_writeContext, entityType.AsReference(), entityInstance); + ResourceContext entityContext = new ResourceContext(_writeContext, entityType.AsReference(), entityInstance); - // Act & Assert - ExceptionAssert.Throws( - () => ConventionsHelpers.GetEntityKeyValue(entityContext), - "Key property 'Key' of type 'NS.Name' is null. Key properties cannot have null values."); - } + // Act & Assert + ExceptionAssert.Throws( + () => ConventionsHelpers.GetEntityKeyValue(entityContext), + "Key property 'Key' of type 'NS.Name' is null. Key properties cannot have null values."); + } - [Fact] - public void GetEntityKeyValue_ThrowsForNullKeys_WithMultipleKeys() - { - // Arrange - var entityInstance = new { Key1 = "abc", Key2 = "def", Key3 = (string)null }; - EdmEntityType entityType = new EdmEntityType("NS", "Name"); - entityType.AddKeys(entityType.AddStructuralProperty("Key1", EdmPrimitiveTypeKind.String)); - entityType.AddKeys(entityType.AddStructuralProperty("Key2", EdmPrimitiveTypeKind.Int32)); - entityType.AddKeys(entityType.AddStructuralProperty("Key3", EdmPrimitiveTypeKind.Boolean)); - - ResourceContext entityContext = new ResourceContext(_writeContext, entityType.AsReference(), entityInstance); - - // Act & Assert - ExceptionAssert.Throws( - () => ConventionsHelpers.GetEntityKeyValue(entityContext), - "Key property 'Key3' of type 'NS.Name' is null. Key properties cannot have null values."); - } + [Fact] + public void GetEntityKeyValue_ThrowsForNullKeys_WithMultipleKeys() + { + // Arrange + var entityInstance = new { Key1 = "abc", Key2 = "def", Key3 = (string)null }; + EdmEntityType entityType = new EdmEntityType("NS", "Name"); + entityType.AddKeys(entityType.AddStructuralProperty("Key1", EdmPrimitiveTypeKind.String)); + entityType.AddKeys(entityType.AddStructuralProperty("Key2", EdmPrimitiveTypeKind.Int32)); + entityType.AddKeys(entityType.AddStructuralProperty("Key3", EdmPrimitiveTypeKind.Boolean)); + + ResourceContext entityContext = new ResourceContext(_writeContext, entityType.AsReference(), entityInstance); + + // Act & Assert + ExceptionAssert.Throws( + () => ConventionsHelpers.GetEntityKeyValue(entityContext), + "Key property 'Key3' of type 'NS.Name' is null. Key properties cannot have null values."); + } - [Fact] - public void GetEntityKeyValue_DerivedType() - { - // Arrange - var entityInstance = new { Key = "key" }; - EdmEntityType baseEntityType = new EdmEntityType("NS", "Name"); - baseEntityType.AddKeys(baseEntityType.AddStructuralProperty("Key", EdmPrimitiveTypeKind.String)); - EdmEntityType derivedEntityType = new EdmEntityType("NS", "Derived", baseEntityType); + [Fact] + public void GetEntityKeyValue_DerivedType() + { + // Arrange + var entityInstance = new { Key = "key" }; + EdmEntityType baseEntityType = new EdmEntityType("NS", "Name"); + baseEntityType.AddKeys(baseEntityType.AddStructuralProperty("Key", EdmPrimitiveTypeKind.String)); + EdmEntityType derivedEntityType = new EdmEntityType("NS", "Derived", baseEntityType); - ResourceContext entityContext = new ResourceContext(_writeContext, derivedEntityType.AsReference(), entityInstance); + ResourceContext entityContext = new ResourceContext(_writeContext, derivedEntityType.AsReference(), entityInstance); - // Act - var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); + // Act + var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); - // Assert - Assert.Equal("'key'", keyValue); - } + // Assert + Assert.Equal("'key'", keyValue); + } - [Fact] - public void GetEntityKeyValue_MultipleKeys_DerivedType() - { - // Arrange - var entityInstance = new { Key1 = "key1", Key2 = 2, Key3 = true }; - EdmEntityType baseEntityType = new EdmEntityType("NS", "Name"); - baseEntityType.AddKeys(baseEntityType.AddStructuralProperty("Key1", EdmPrimitiveTypeKind.String)); - baseEntityType.AddKeys(baseEntityType.AddStructuralProperty("Key2", EdmPrimitiveTypeKind.Int32)); - baseEntityType.AddKeys(baseEntityType.AddStructuralProperty("Key3", EdmPrimitiveTypeKind.Boolean)); - EdmEntityType derivedEntityType = new EdmEntityType("NS", "Derived", baseEntityType); + [Fact] + public void GetEntityKeyValue_MultipleKeys_DerivedType() + { + // Arrange + var entityInstance = new { Key1 = "key1", Key2 = 2, Key3 = true }; + EdmEntityType baseEntityType = new EdmEntityType("NS", "Name"); + baseEntityType.AddKeys(baseEntityType.AddStructuralProperty("Key1", EdmPrimitiveTypeKind.String)); + baseEntityType.AddKeys(baseEntityType.AddStructuralProperty("Key2", EdmPrimitiveTypeKind.Int32)); + baseEntityType.AddKeys(baseEntityType.AddStructuralProperty("Key3", EdmPrimitiveTypeKind.Boolean)); + EdmEntityType derivedEntityType = new EdmEntityType("NS", "Derived", baseEntityType); - ResourceContext entityContext = new ResourceContext(_writeContext, derivedEntityType.AsReference(), entityInstance); + ResourceContext entityContext = new ResourceContext(_writeContext, derivedEntityType.AsReference(), entityInstance); - // Act - var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); + // Act + var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); - // Assert - Assert.Equal("Key1='key1',Key2=2,Key3=true", keyValue); - } + // Assert + Assert.Equal("Key1='key1',Key2=2,Key3=true", keyValue); + } - [Fact] - public void GetEntityKeyValue_MultipleKeys_DerivedType_ForDateAndTimeOfDay() - { - // Arrange - var entityInstance = new { Key1 = new Date(2015, 2, 26), Key2 = new TimeOfDay(1, 2, 3, 4) }; - EdmEntityType baseEntityType = new EdmEntityType("NS", "Name"); - baseEntityType.AddKeys(baseEntityType.AddStructuralProperty("Key1", EdmPrimitiveTypeKind.Date)); - baseEntityType.AddKeys(baseEntityType.AddStructuralProperty("Key2", EdmPrimitiveTypeKind.TimeOfDay)); - EdmEntityType derivedEntityType = new EdmEntityType("NS", "Derived", baseEntityType); + [Fact] + public void GetEntityKeyValue_MultipleKeys_DerivedType_ForDateAndTimeOfDay() + { + // Arrange + var entityInstance = new { Key1 = new Date(2015, 2, 26), Key2 = new TimeOfDay(1, 2, 3, 4) }; + EdmEntityType baseEntityType = new EdmEntityType("NS", "Name"); + baseEntityType.AddKeys(baseEntityType.AddStructuralProperty("Key1", EdmPrimitiveTypeKind.Date)); + baseEntityType.AddKeys(baseEntityType.AddStructuralProperty("Key2", EdmPrimitiveTypeKind.TimeOfDay)); + EdmEntityType derivedEntityType = new EdmEntityType("NS", "Derived", baseEntityType); - ResourceContext entityContext = new ResourceContext(_writeContext, - derivedEntityType.AsReference(), entityInstance); + ResourceContext entityContext = new ResourceContext(_writeContext, + derivedEntityType.AsReference(), entityInstance); - // Act - var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); + // Act + var keyValue = ConventionsHelpers.GetEntityKeyValue(entityContext); - // Assert - Assert.Equal("Key1=2015-02-26,Key2=01:02:03.0040000", keyValue); - } + // Assert + Assert.Equal("Key1=2015-02-26,Key2=01:02:03.0040000", keyValue); + } - [Fact] - public void ConvertValue_ForEnumValue() - { - // Arrange & Act - var value = ConventionsHelpers.ConvertValue(SimpleEnum.First, null, null); + [Fact] + public void ConvertValue_ForEnumValue() + { + // Arrange & Act + var value = ConventionsHelpers.ConvertValue(SimpleEnum.First, null, null); - // Assert - ODataEnumValue enumValue = Assert.IsType(value); - Assert.Equal("First", enumValue.Value); - Assert.Equal("Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum", enumValue.TypeName); - } + // Assert + ODataEnumValue enumValue = Assert.IsType(value); + Assert.Equal("First", enumValue.Value); + Assert.Equal("Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum", enumValue.TypeName); + } - public static TheoryDataSet GetUriRepresentationForValue_DataSet + public static TheoryDataSet GetUriRepresentationForValue_DataSet + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() - { - { (bool)true, "true" }, - { (char)'1', "'1'" }, - { (char?)'1', "'1'" }, - { (string)"123", "'123'" }, - { (char[])new char[] { '1', '2', '3' }, "'123'" }, - { (int)123, "123" }, - { (short)123, "123" }, - { (long)123, "123" }, - { (ushort)123, "123" }, - { (uint)123, "123" }, - { (ulong)123, "123" }, - { (int?)123, "123" }, - { (short?)123, "123" }, - { (long?)123, "123" }, - { (ushort?)123, "123" }, - { (uint?)123, "123" }, - { (ulong?)123, "123" }, - { (float)123.123, "123.123" }, - { (double)123.123, "123.123" }, - { (decimal)123.123, "123.123" }, - { Guid.Empty, "00000000-0000-0000-0000-000000000000" }, - { new DateTimeOffset(1,1,1,0,0,0,TimeSpan.Zero), "0001-01-01T00:00:00Z" }, - { TimeSpan.FromSeconds(86456), "duration'P1DT56S'" }, - { DateTimeOffset.FromFileTime(0).ToUniversalTime(), "1601-01-01T00:00:00Z" }, - { SimpleEnum.First, "Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'" }, - { FlagsEnum.One | FlagsEnum.Two, "Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Two'" }, - { (SimpleEnum)123, "Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'123'" }, - { (FlagsEnum)123, "Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'123'" }, - { new Date(2014, 10, 14), "2014-10-14"}, - { new TimeOfDay(15, 38, 25, 109), "15:38:25.1090000"}, - }; - } + { (bool)true, "true" }, + { (char)'1', "'1'" }, + { (char?)'1', "'1'" }, + { (string)"123", "'123'" }, + { (char[])new char[] { '1', '2', '3' }, "'123'" }, + { (int)123, "123" }, + { (short)123, "123" }, + { (long)123, "123" }, + { (ushort)123, "123" }, + { (uint)123, "123" }, + { (ulong)123, "123" }, + { (int?)123, "123" }, + { (short?)123, "123" }, + { (long?)123, "123" }, + { (ushort?)123, "123" }, + { (uint?)123, "123" }, + { (ulong?)123, "123" }, + { (float)123.123, "123.123" }, + { (double)123.123, "123.123" }, + { (decimal)123.123, "123.123" }, + { Guid.Empty, "00000000-0000-0000-0000-000000000000" }, + { new DateTimeOffset(1,1,1,0,0,0,TimeSpan.Zero), "0001-01-01T00:00:00Z" }, + { TimeSpan.FromSeconds(86456), "duration'P1DT56S'" }, + { DateTimeOffset.FromFileTime(0).ToUniversalTime(), "1601-01-01T00:00:00Z" }, + { SimpleEnum.First, "Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'" }, + { FlagsEnum.One | FlagsEnum.Two, "Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Two'" }, + { (SimpleEnum)123, "Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'123'" }, + { (FlagsEnum)123, "Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'123'" }, + { new Date(2014, 10, 14), "2014-10-14"}, + { new TimeOfDay(15, 38, 25, 109), "15:38:25.1090000"}, + }; } + } - [Theory] - [MemberData(nameof(GetUriRepresentationForValue_DataSet))] - public void GetUriRepresentationForValue_Works(object value, string result) - { - Assert.Equal( - result, - ConventionsHelpers.GetUriRepresentationForValue(value)); - } + [Theory] + [MemberData(nameof(GetUriRepresentationForValue_DataSet))] + public void GetUriRepresentationForValue_Works(object value, string result) + { + Assert.Equal( + result, + ConventionsHelpers.GetUriRepresentationForValue(value)); + } - private class GetKeyProperty_validEntityType_TestClass_Id - { - public int Id { get; set; } - } + private class GetKeyProperty_validEntityType_TestClass_Id + { + public int Id { get; set; } + } - private class GetKeyProperty_validEntityType_TestClass2_ClassName - { - public string GetKeyProperty_validEntityType_TestClass2_ClassNameId { get; set; } - } + private class GetKeyProperty_validEntityType_TestClass2_ClassName + { + public string GetKeyProperty_validEntityType_TestClass2_ClassNameId { get; set; } + } - private class GetKeyProperty_InValidEntityType_NoId - { - public int IDD { get; set; } - } + private class GetKeyProperty_InValidEntityType_NoId + { + public int IDD { get; set; } + } - private class GetKeyProperty_InValidEntityType_ComplexId - { - public GetProperties_Complex Id { get; set; } - } + private class GetKeyProperty_InValidEntityType_ComplexId + { + public GetProperties_Complex Id { get; set; } + } - class GetProperties_Base - { - public int Base_I { get; set; } + class GetProperties_Base + { + public int Base_I { get; set; } - private int Pri { get; set; } + private int Pri { get; set; } - public int InternalGet { internal get; set; } + public int InternalGet { internal get; set; } - public GetProperties_Complex Base_Complex { get; set; } + public GetProperties_Complex Base_Complex { get; set; } - public virtual string Base_Str { get; set; } - } + public virtual string Base_Str { get; set; } + } - class GetProperties_Derived : GetProperties_Base - { - public static int SomeStaticProperty1 { get; set; } + class GetProperties_Derived : GetProperties_Base + { + public static int SomeStaticProperty1 { get; set; } - internal static int SomeStaticProperty2 { get; set; } + internal static int SomeStaticProperty2 { get; set; } - private static int SomeStaticProperty3 { get; set; } + private static int SomeStaticProperty3 { get; set; } - public string Derived_I { get; set; } + public string Derived_I { get; set; } - public string PrivateSetPublicGet { get; private set; } + public string PrivateSetPublicGet { get; private set; } - public string PrivateGetPublicSet { private get; set; } + public string PrivateGetPublicSet { private get; set; } - public GetProperties_Complex Derived_Complex { get; set; } + public GetProperties_Complex Derived_Complex { get; set; } - public int[] Collection { get; private set; } + public int[] Collection { get; private set; } + + public string this[string str] + { + get + { + throw new NotImplementedException(); + } - public string this[string str] + set { - get - { - throw new NotImplementedException(); - } - - set - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } + } + + class GetProperties_Complex + { + public int A { get; set; } + } - class GetProperties_Complex + public class GetProperties_NestParent + { + public class Nest { - public int A { get; set; } + public NestPropertyType NestProperty { get; set; } } - public class GetProperties_NestParent + public class NestPropertyType { - public class Nest - { - public NestPropertyType NestProperty { get; set; } - } - - public class NestPropertyType - { - } } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/DefaultODataETagHandlerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/DefaultODataETagHandlerTests.cs index 657217104..ff55e454b 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/DefaultODataETagHandlerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/DefaultODataETagHandlerTests.cs @@ -15,117 +15,116 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Formatter +namespace Microsoft.AspNetCore.OData.Formatter; + +public class DefaultODataETagHandlerTests { - public class DefaultODataETagHandlerTests + [Fact] + public void CreateETagDefaultODataETagHandler_ThrowsArgumentNull_Segment() { - [Fact] - public void CreateETagDefaultODataETagHandler_ThrowsArgumentNull_Segment() - { - // Arrange & Act & Assert - DefaultODataETagHandler handler = new DefaultODataETagHandler(); - ExceptionAssert.ThrowsArgumentNull(() => handler.CreateETag(null), "properties"); - } + // Arrange & Act & Assert + DefaultODataETagHandler handler = new DefaultODataETagHandler(); + ExceptionAssert.ThrowsArgumentNull(() => handler.CreateETag(null), "properties"); + } - [Fact] - public void ParseETagDefaultODataETagHandler_ThrowsArgumentNull_EtagHeaderValue() - { - // Arrange & Act & Assert - DefaultODataETagHandler handler = new DefaultODataETagHandler(); - ExceptionAssert.ThrowsArgumentNull(() => handler.ParseETag(null), "etagHeaderValue"); - } + [Fact] + public void ParseETagDefaultODataETagHandler_ThrowsArgumentNull_EtagHeaderValue() + { + // Arrange & Act & Assert + DefaultODataETagHandler handler = new DefaultODataETagHandler(); + ExceptionAssert.ThrowsArgumentNull(() => handler.ParseETag(null), "etagHeaderValue"); + } - public static TheoryDataSet CreateAndParseETagForValue_DataSet + public static TheoryDataSet CreateAndParseETagForValue_DataSet + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - (bool)true, - (string)"123", - (int)123, - (long)123123123123, - (float)123.123, - (double)123123123123.123, - Guid.Empty, - new DateTimeOffset(DateTime.FromBinary(0), TimeSpan.Zero), - TimeSpan.FromSeconds(86456), - DateTimeOffset.FromFileTime(0).ToUniversalTime(), - - // ODL has bug in ConvertFromUriLiteral, please uncomment it after fix https://github.com/OData/odata.net/issues/77. - // new Date(1997, 7, 1), - new TimeOfDay(10, 11, 12, 13), - }; - } + (bool)true, + (string)"123", + (int)123, + (long)123123123123, + (float)123.123, + (double)123123123123.123, + Guid.Empty, + new DateTimeOffset(DateTime.FromBinary(0), TimeSpan.Zero), + TimeSpan.FromSeconds(86456), + DateTimeOffset.FromFileTime(0).ToUniversalTime(), + + // ODL has bug in ConvertFromUriLiteral, please uncomment it after fix https://github.com/OData/odata.net/issues/77. + // new Date(1997, 7, 1), + new TimeOfDay(10, 11, 12, 13), + }; } + } - [Theory] - [MemberData(nameof(CreateAndParseETagForValue_DataSet))] - public void DefaultODataETagHandler_RoundTrips(object value) - { - // Arrange - DefaultODataETagHandler handler = new DefaultODataETagHandler(); - Dictionary properties = new Dictionary { { "Any", value } }; - - // Act - EntityTagHeaderValue etagHeaderValue = handler.CreateETag(properties); - IList values = handler.ParseETag(etagHeaderValue).Select(p => p.Value).ToList(); - - // Assert - Assert.True(etagHeaderValue.IsWeak); - Assert.Single(values); - Assert.Equal(value, values[0]); - } + [Theory] + [MemberData(nameof(CreateAndParseETagForValue_DataSet))] + public void DefaultODataETagHandler_RoundTrips(object value) + { + // Arrange + DefaultODataETagHandler handler = new DefaultODataETagHandler(); + Dictionary properties = new Dictionary { { "Any", value } }; + + // Act + EntityTagHeaderValue etagHeaderValue = handler.CreateETag(properties); + IList values = handler.ParseETag(etagHeaderValue).Select(p => p.Value).ToList(); + + // Assert + Assert.True(etagHeaderValue.IsWeak); + Assert.Single(values); + Assert.Equal(value, values[0]); + } - [Theory] - [InlineData("UTC")] // +0:00 - [InlineData("Pacific Standard Time")] // -8:00 - [InlineData("China Standard Time")] // +8:00 - public void DefaultODataETagHandler_DateTime_RoundTrips(string timeZoneId) - { - // Arrange - TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); - DateTime value = new DateTime(2015, 2, 17, 1, 2, 3, DateTimeKind.Utc); + [Theory] + [InlineData("UTC")] // +0:00 + [InlineData("Pacific Standard Time")] // -8:00 + [InlineData("China Standard Time")] // +8:00 + public void DefaultODataETagHandler_DateTime_RoundTrips(string timeZoneId) + { + // Arrange + TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); + DateTime value = new DateTime(2015, 2, 17, 1, 2, 3, DateTimeKind.Utc); + + DefaultODataETagHandler handler = new DefaultODataETagHandler(); + Dictionary properties = new Dictionary { { "Any", value } }; - DefaultODataETagHandler handler = new DefaultODataETagHandler(); - Dictionary properties = new Dictionary { { "Any", value } }; + // Act + EntityTagHeaderValue etagHeaderValue = handler.CreateETag(properties, timeZone); + IList values = handler.ParseETag(etagHeaderValue).Select(p => p.Value).ToList(); - // Act - EntityTagHeaderValue etagHeaderValue = handler.CreateETag(properties, timeZone); - IList values = handler.ParseETag(etagHeaderValue).Select(p => p.Value).ToList(); + // Assert + Assert.True(etagHeaderValue.IsWeak); + Assert.Single(values); + DateTimeOffset result = Assert.IsType(values[0]); - // Assert - Assert.True(etagHeaderValue.IsWeak); - Assert.Single(values); - DateTimeOffset result = Assert.IsType(values[0]); + Assert.Equal(TimeZoneInfo.ConvertTime(new DateTimeOffset(value), timeZone), result); + } - Assert.Equal(TimeZoneInfo.ConvertTime(new DateTimeOffset(value), timeZone), result); + [Theory] + [InlineData("1", new object[] { "any", 1 })] + public void CreateETag_ETagCreatedAndParsed_GivenValues(string notUsed, object[] values) + { + // Arrange + DefaultODataETagHandler handler = new DefaultODataETagHandler(); + Dictionary properties = new Dictionary(); + for (int i = 0; i < values.Length; i++) + { + properties.Add("Prop" + i, values[i]); } - [Theory] - [InlineData("1", new object[] { "any", 1 })] - public void CreateETag_ETagCreatedAndParsed_GivenValues(string notUsed, object[] values) + // Act + EntityTagHeaderValue etagHeaderValue = handler.CreateETag(properties); + IList results = handler.ParseETag(etagHeaderValue).Select(p => p.Value).ToList(); + + // Assert + Assert.NotNull(notUsed); + Assert.True(etagHeaderValue.IsWeak); + Assert.Equal(values.Length, results.Count); + for (int i = 0; i < values.Length; i++) { - // Arrange - DefaultODataETagHandler handler = new DefaultODataETagHandler(); - Dictionary properties = new Dictionary(); - for (int i = 0; i < values.Length; i++) - { - properties.Add("Prop" + i, values[i]); - } - - // Act - EntityTagHeaderValue etagHeaderValue = handler.CreateETag(properties); - IList results = handler.ParseETag(etagHeaderValue).Select(p => p.Value).ToList(); - - // Assert - Assert.NotNull(notUsed); - Assert.True(etagHeaderValue.IsWeak); - Assert.Equal(values.Length, results.Count); - for (int i = 0; i < values.Length; i++) - { - Assert.Equal(values[i], results[i]); - } + Assert.Equal(values[i], results[i]); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/CollectionDeserializationHelpersTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/CollectionDeserializationHelpersTest.cs index a10c08b09..213a9b666 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/CollectionDeserializationHelpersTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/CollectionDeserializationHelpersTest.cs @@ -21,224 +21,223 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class CollectionDeserializationHelpersTest { - public class CollectionDeserializationHelpersTest + public static TheoryDataSet CopyItemsToCollectionData { - public static TheoryDataSet CopyItemsToCollectionData + get { - get + IList source = new List { 1, 2, 3 }; + return new TheoryDataSet { - IList source = new List { 1, 2, 3 }; - return new TheoryDataSet - { - { source, new List() }, - { source, new Collection() }, - { source, new CustomCollectionWithAdd() }, - }; - } + { source, new List() }, + { source, new Collection() }, + { source, new CustomCollectionWithAdd() }, + }; } + } - [Theory] - [MemberData(nameof(CopyItemsToCollectionData))] - public void CopyItemsToCollection(IList oldCollection, IEnumerable newCollection) - { - oldCollection.AddToCollection(newCollection, typeof(int), typeof(CollectionDeserializationHelpersTest), "PropertyName", newCollection.GetType()); + [Theory] + [MemberData(nameof(CopyItemsToCollectionData))] + public void CopyItemsToCollection(IList oldCollection, IEnumerable newCollection) + { + oldCollection.AddToCollection(newCollection, typeof(int), typeof(CollectionDeserializationHelpersTest), "PropertyName", newCollection.GetType()); - Assert.Equal( - new[] { 1, 2, 3 }, - newCollection as IEnumerable); - } + Assert.Equal( + new[] { 1, 2, 3 }, + newCollection as IEnumerable); + } - [Fact] - public void CopyItemsToCollection_CanConvertToNonStandardEdm() - { - IList source = new List { SimpleEnum.First, SimpleEnum.Second, SimpleEnum.Third }; - IEnumerable newCollection = new CustomCollectionWithAdd(); + [Fact] + public void CopyItemsToCollection_CanConvertToNonStandardEdm() + { + IList source = new List { SimpleEnum.First, SimpleEnum.Second, SimpleEnum.Third }; + IEnumerable newCollection = new CustomCollectionWithAdd(); - source.AddToCollection(newCollection, typeof(SimpleEnum), typeof(CollectionDeserializationHelpersTest), "PropertyName", newCollection.GetType()); + source.AddToCollection(newCollection, typeof(SimpleEnum), typeof(CollectionDeserializationHelpersTest), "PropertyName", newCollection.GetType()); - Assert.Equal(new[] { SimpleEnum.First, SimpleEnum.Second, SimpleEnum.Third }, newCollection as IEnumerable); - } + Assert.Equal(new[] { SimpleEnum.First, SimpleEnum.Second, SimpleEnum.Third }, newCollection as IEnumerable); + } - [Fact] - public void AddToCollection_ThrowsSerializationException_IsEnumerableWithoutAddMethod() - { - // Arrange & Act & Assert - IList source = new List(); - IEnumerable newCollection = Enumerable.Empty(); - Action test = () => source.AddToCollection(newCollection, typeof(int), typeof(CollectionDeserializationHelpersTest), "PropertyName", newCollection.GetType()); - SerializationException exception = Assert.Throws(() => test()); - Assert.Contains("of the property 'PropertyName' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.CollectionDeserializationHelpersTest' does not have an Add method. Consider using a collection type that does have an Add method - for example IList or ICollection.", - exception.Message); - } + [Fact] + public void AddToCollection_ThrowsSerializationException_IsEnumerableWithoutAddMethod() + { + // Arrange & Act & Assert + IList source = new List(); + IEnumerable newCollection = Enumerable.Empty(); + Action test = () => source.AddToCollection(newCollection, typeof(int), typeof(CollectionDeserializationHelpersTest), "PropertyName", newCollection.GetType()); + SerializationException exception = Assert.Throws(() => test()); + Assert.Contains("of the property 'PropertyName' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.CollectionDeserializationHelpersTest' does not have an Add method. Consider using a collection type that does have an Add method - for example IList or ICollection.", + exception.Message); + } - [Fact] - public void AddToCollection_ThrowsSerializationException_IsAnArray() - { - // Arrange - IList source = new List { SimpleEnum.First, SimpleEnum.Second, SimpleEnum.Third }; - IEnumerable newCollection = new string[] { }; + [Fact] + public void AddToCollection_ThrowsSerializationException_IsAnArray() + { + // Arrange + IList source = new List { SimpleEnum.First, SimpleEnum.Second, SimpleEnum.Third }; + IEnumerable newCollection = new string[] { }; - // Act - Action test = () => source.AddToCollection(newCollection, typeof(SimpleEnum), typeof(CollectionDeserializationHelpersTest), "PropertyName", newCollection.GetType()); + // Act + Action test = () => source.AddToCollection(newCollection, typeof(SimpleEnum), typeof(CollectionDeserializationHelpersTest), "PropertyName", newCollection.GetType()); - // Assert - ExceptionAssert.Throws(test, - "The value of the property 'PropertyName' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.CollectionDeserializationHelpersTest' is an array." + - " Consider adding a setter for the property."); - } + // Assert + ExceptionAssert.Throws(test, + "The value of the property 'PropertyName' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.CollectionDeserializationHelpersTest' is an array." + + " Consider adding a setter for the property."); + } - [Fact] - public void CopyItemsToCollection_CanConvertUtcDateTime() - { - // Arrange - DateTime dt1 = new DateTime(1978, 11, 15, 0, 0, 0, DateTimeKind.Utc); - DateTime dt2 = new DateTime(2014, 10, 27, 0, 0, 0, DateTimeKind.Utc); - IList source = new List { new DateTimeOffset(dt1), new DateTimeOffset(dt2) }; + [Fact] + public void CopyItemsToCollection_CanConvertUtcDateTime() + { + // Arrange + DateTime dt1 = new DateTime(1978, 11, 15, 0, 0, 0, DateTimeKind.Utc); + DateTime dt2 = new DateTime(2014, 10, 27, 0, 0, 0, DateTimeKind.Utc); + IList source = new List { new DateTimeOffset(dt1), new DateTimeOffset(dt2) }; - IEnumerable expect = source.Select(e => e.LocalDateTime); - IEnumerable newCollection = new CustomCollectionWithAdd(); + IEnumerable expect = source.Select(e => e.LocalDateTime); + IEnumerable newCollection = new CustomCollectionWithAdd(); - // Act - source.AddToCollection(newCollection, typeof(DateTime), typeof(CollectionDeserializationHelpersTest), - "PropertyName", newCollection.GetType(), context: null); + // Act + source.AddToCollection(newCollection, typeof(DateTime), typeof(CollectionDeserializationHelpersTest), + "PropertyName", newCollection.GetType(), context: null); - // Assert - Assert.Equal(expect, newCollection as IEnumerable); - } + // Assert + Assert.Equal(expect, newCollection as IEnumerable); + } - [Fact] - public void CopyItemsToCollection_CanConvertUtcDateTime_ToDestinationTimeZone() + [Fact] + public void CopyItemsToCollection_CanConvertUtcDateTime_ToDestinationTimeZone() + { + // Arrange + DateTime dt1 = new DateTime(1978, 11, 15, 10, 20, 30, DateTimeKind.Utc); + DateTime dt2 = new DateTime(2014, 10, 27, 10, 20, 30, DateTimeKind.Utc); + IList source = new List { new DateTimeOffset(dt1), new DateTimeOffset(dt2) }; + IEnumerable newCollection = new CustomCollectionWithAdd(); + TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); // -8:00 / -7:00 + ODataDeserializerContext context = new ODataDeserializerContext { - // Arrange - DateTime dt1 = new DateTime(1978, 11, 15, 10, 20, 30, DateTimeKind.Utc); - DateTime dt2 = new DateTime(2014, 10, 27, 10, 20, 30, DateTimeKind.Utc); - IList source = new List { new DateTimeOffset(dt1), new DateTimeOffset(dt2) }; - IEnumerable newCollection = new CustomCollectionWithAdd(); - TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); // -8:00 / -7:00 - ODataDeserializerContext context = new ODataDeserializerContext - { - TimeZone = timeZoneInfo - }; + TimeZone = timeZoneInfo + }; - // Act - source.AddToCollection(newCollection, typeof(DateTime), typeof(CollectionDeserializationHelpersTest), - "PropertyName", newCollection.GetType(), context); + // Act + source.AddToCollection(newCollection, typeof(DateTime), typeof(CollectionDeserializationHelpersTest), + "PropertyName", newCollection.GetType(), context); - // Assert - Assert.Equal(new[] { dt1.AddHours(-8), dt2.AddHours(-7) }, newCollection as IEnumerable); - } + // Assert + Assert.Equal(new[] { dt1.AddHours(-8), dt2.AddHours(-7) }, newCollection as IEnumerable); + } - [Fact] - public void CopyItemsToCollection_CanConvertLocalDateTime_ToDestinationTimeZone() + [Fact] + public void CopyItemsToCollection_CanConvertLocalDateTime_ToDestinationTimeZone() + { + // Arrange + DateTimeOffset dto1 = DateTimeOffset.Parse("2014-12-16T01:02:03+8:00"); + DateTimeOffset dto2 = DateTimeOffset.Parse("2014-12-16T01:02:03-2:00"); + IList source = new List { dto1, dto2 }; + IEnumerable expect = new List { - // Arrange - DateTimeOffset dto1 = DateTimeOffset.Parse("2014-12-16T01:02:03+8:00"); - DateTimeOffset dto2 = DateTimeOffset.Parse("2014-12-16T01:02:03-2:00"); - IList source = new List { dto1, dto2 }; - IEnumerable expect = new List - { - dto1.LocalDateTime, - dto2.LocalDateTime - }; - IEnumerable newCollection = new CustomCollectionWithAdd(); + dto1.LocalDateTime, + dto2.LocalDateTime + }; + IEnumerable newCollection = new CustomCollectionWithAdd(); - // Act - source.AddToCollection(newCollection, typeof(DateTime), typeof(CollectionDeserializationHelpersTest), - "PropertyName", newCollection.GetType(), context: null); + // Act + source.AddToCollection(newCollection, typeof(DateTime), typeof(CollectionDeserializationHelpersTest), + "PropertyName", newCollection.GetType(), context: null); - // Assert - Assert.Equal(expect, newCollection as IEnumerable); - } + // Assert + Assert.Equal(expect, newCollection as IEnumerable); + } - [Fact] - public void CopyItemsToCollection_CanConvertLocalDateTime() + [Fact] + public void CopyItemsToCollection_CanConvertLocalDateTime() + { + // Arrange + DateTimeOffset dto1 = DateTimeOffset.Parse("2014-12-16T01:02:03+8:00"); + DateTimeOffset dto2 = DateTimeOffset.Parse("2014-12-16T01:02:03-2:00"); + IList source = new List { dto1, dto2 }; + IEnumerable newCollection = new CustomCollectionWithAdd(); + TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); // -8:00 / -7:00 + ODataDeserializerContext context = new ODataDeserializerContext { - // Arrange - DateTimeOffset dto1 = DateTimeOffset.Parse("2014-12-16T01:02:03+8:00"); - DateTimeOffset dto2 = DateTimeOffset.Parse("2014-12-16T01:02:03-2:00"); - IList source = new List { dto1, dto2 }; - IEnumerable newCollection = new CustomCollectionWithAdd(); - TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); // -8:00 / -7:00 - ODataDeserializerContext context = new ODataDeserializerContext - { - TimeZone = timeZone - }; + TimeZone = timeZone + }; - // Act - source.AddToCollection(newCollection, typeof(DateTime), typeof(CollectionDeserializationHelpersTest), - "PropertyName", newCollection.GetType(), context); + // Act + source.AddToCollection(newCollection, typeof(DateTime), typeof(CollectionDeserializationHelpersTest), + "PropertyName", newCollection.GetType(), context); - // Assert - Assert.Equal(new[] { new DateTime(2014, 12, 15, 9, 2, 3), new DateTime(2014, 12, 15, 19, 2, 3) }, - newCollection as IEnumerable); - } + // Assert + Assert.Equal(new[] { new DateTime(2014, 12, 15, 9, 2, 3), new DateTime(2014, 12, 15, 19, 2, 3) }, + newCollection as IEnumerable); + } - [Theory] - [InlineData(typeof(IEnumerable), typeof(int))] - [InlineData(typeof(ICollection), typeof(int))] - [InlineData(typeof(IList), typeof(int))] - [InlineData(typeof(Collection), typeof(int))] - [InlineData(typeof(List), typeof(int))] - [InlineData(typeof(LinkedList), typeof(int))] - public void TryCreateInstance_Creates_AppropriateCollectionObject(Type collectionType, Type elementType) - { - IEnumerable result; - bool created = CollectionDeserializationHelpers.TryCreateInstance(collectionType, null, elementType, out result); + [Theory] + [InlineData(typeof(IEnumerable), typeof(int))] + [InlineData(typeof(ICollection), typeof(int))] + [InlineData(typeof(IList), typeof(int))] + [InlineData(typeof(Collection), typeof(int))] + [InlineData(typeof(List), typeof(int))] + [InlineData(typeof(LinkedList), typeof(int))] + public void TryCreateInstance_Creates_AppropriateCollectionObject(Type collectionType, Type elementType) + { + IEnumerable result; + bool created = CollectionDeserializationHelpers.TryCreateInstance(collectionType, null, elementType, out result); - Assert.True(created); - Assert.IsAssignableFrom(collectionType, result); - } + Assert.True(created); + Assert.IsAssignableFrom(collectionType, result); + } - [Fact] - public void TryCreateInstance_EdmComplexObjectCollection_SetsEdmType() - { - EdmComplexType complexType = new EdmComplexType("NS", "ComplexType"); - IEdmCollectionTypeReference complexCollectionType = - new EdmCollectionType(complexType.ToEdmTypeReference(true)) - .ToEdmTypeReference(true).AsCollection(); + [Fact] + public void TryCreateInstance_EdmComplexObjectCollection_SetsEdmType() + { + EdmComplexType complexType = new EdmComplexType("NS", "ComplexType"); + IEdmCollectionTypeReference complexCollectionType = + new EdmCollectionType(complexType.ToEdmTypeReference(true)) + .ToEdmTypeReference(true).AsCollection(); - IEnumerable result; - CollectionDeserializationHelpers.TryCreateInstance(typeof(EdmComplexObjectCollection), complexCollectionType, typeof(EdmComplexObject), out result); + IEnumerable result; + CollectionDeserializationHelpers.TryCreateInstance(typeof(EdmComplexObjectCollection), complexCollectionType, typeof(EdmComplexObject), out result); - var edmObject = Assert.IsType(result); - Assert.Equal(edmObject.GetEdmType(), complexCollectionType, new EdmTypeReferenceEqualityComparer()); - } + var edmObject = Assert.IsType(result); + Assert.Equal(edmObject.GetEdmType(), complexCollectionType, new EdmTypeReferenceEqualityComparer()); + } - [Fact] - public void TryCreateInstance_EdmEntityObjectCollection_SetsEdmType() - { - EdmEntityType entityType = new EdmEntityType("NS", "EntityType"); - IEdmCollectionTypeReference entityCollectionType = - new EdmCollectionType(entityType.ToEdmTypeReference(true)) - .ToEdmTypeReference(true).AsCollection(); + [Fact] + public void TryCreateInstance_EdmEntityObjectCollection_SetsEdmType() + { + EdmEntityType entityType = new EdmEntityType("NS", "EntityType"); + IEdmCollectionTypeReference entityCollectionType = + new EdmCollectionType(entityType.ToEdmTypeReference(true)) + .ToEdmTypeReference(true).AsCollection(); - IEnumerable result; - CollectionDeserializationHelpers.TryCreateInstance(typeof(EdmEntityObjectCollection), entityCollectionType, typeof(EdmComplexObject), out result); + IEnumerable result; + CollectionDeserializationHelpers.TryCreateInstance(typeof(EdmEntityObjectCollection), entityCollectionType, typeof(EdmComplexObject), out result); - var edmObject = Assert.IsType(result); - Assert.Equal(edmObject.GetEdmType(), entityCollectionType, new EdmTypeReferenceEqualityComparer()); - } + var edmObject = Assert.IsType(result); + Assert.Equal(edmObject.GetEdmType(), entityCollectionType, new EdmTypeReferenceEqualityComparer()); + } - private class CustomCollectionWithAdd : IEnumerable - { - List _list = new List(); + private class CustomCollectionWithAdd : IEnumerable + { + List _list = new List(); - public void Add(T item) - { - _list.Add(item); - } + public void Add(T item) + { + _list.Add(item); + } - public IEnumerator GetEnumerator() - { - return _list.GetEnumerator(); - } + public IEnumerator GetEnumerator() + { + return _list.GetEnumerator(); + } - IEnumerator IEnumerable.GetEnumerator() - { - return _list.GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() + { + return _list.GetEnumerator(); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/DeserializationHelpersTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/DeserializationHelpersTest.cs index 132fd92a3..984ea6448 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/DeserializationHelpersTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/DeserializationHelpersTest.cs @@ -20,501 +20,500 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class DeserializationHelpersTest { - public class DeserializationHelpersTest + [Theory] + [InlineData("Property", true, typeof(int))] + [InlineData("Property", false, typeof(int))] + [InlineData("PropertyNotPresent", true, null)] + [InlineData("PropertyNotPresent", false, null)] + public void GetPropertyType_NonDelta(string propertyName, bool isDelta, Type expectedPropertyType) { - [Theory] - [InlineData("Property", true, typeof(int))] - [InlineData("Property", false, typeof(int))] - [InlineData("PropertyNotPresent", true, null)] - [InlineData("PropertyNotPresent", false, null)] - public void GetPropertyType_NonDelta(string propertyName, bool isDelta, Type expectedPropertyType) - { - object resource = isDelta ? (object)new Delta() : new GetPropertyType_TestClass(); - Assert.Equal( - expectedPropertyType, - DeserializationHelpers.GetPropertyType(resource, propertyName)); - } + object resource = isDelta ? (object)new Delta() : new GetPropertyType_TestClass(); + Assert.Equal( + expectedPropertyType, + DeserializationHelpers.GetPropertyType(resource, propertyName)); + } - [Theory] - [InlineData("Array")] - [InlineData("IEnumerable")] - [InlineData("ICollection")] - [InlineData("IList")] - [InlineData("Collection")] - [InlineData("List")] - [InlineData("CustomCollection")] - public void SetCollectionProperty_CollectionTypeCanBeInstantiated_And_SettableProperty(string propertyName) - { - object value = new SampleClassWithSettableCollectionProperties(); - IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); + [Theory] + [InlineData("Array")] + [InlineData("IEnumerable")] + [InlineData("ICollection")] + [InlineData("IList")] + [InlineData("Collection")] + [InlineData("List")] + [InlineData("CustomCollection")] + public void SetCollectionProperty_CollectionTypeCanBeInstantiated_And_SettableProperty(string propertyName) + { + object value = new SampleClassWithSettableCollectionProperties(); + IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); - DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name); + DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name); - Assert.Equal( - new[] { 1, 2, 3 }, - value.GetType().GetProperty(propertyName).GetValue(value, index: null) as IEnumerable); - } + Assert.Equal( + new[] { 1, 2, 3 }, + value.GetType().GetProperty(propertyName).GetValue(value, index: null) as IEnumerable); + } - [Theory] - [InlineData("CustomCollectionWithNoEmptyCtor")] - [InlineData("ICustomCollectionInterface")] - public void SetCollectionProperty_CollectionTypeCannotBeInstantiated_And_SettableProperty_Throws(string propertyName) - { - object value = new SampleClassWithSettableCollectionProperties(); - IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); + [Theory] + [InlineData("CustomCollectionWithNoEmptyCtor")] + [InlineData("ICustomCollectionInterface")] + public void SetCollectionProperty_CollectionTypeCannotBeInstantiated_And_SettableProperty_Throws(string propertyName) + { + object value = new SampleClassWithSettableCollectionProperties(); + IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); - ExceptionAssert.Throws( - () => DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name), - string.Format("The property '{0}' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.DeserializationHelpersTest+SampleClassWithSettableCollectionProperties' returned a null value. " + - "The input stream contains collection items which cannot be added if the instance is null.", propertyName)); - } + ExceptionAssert.Throws( + () => DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name), + string.Format("The property '{0}' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.DeserializationHelpersTest+SampleClassWithSettableCollectionProperties' returned a null value. " + + "The input stream contains collection items which cannot be added if the instance is null.", propertyName)); + } - [Theory] - [InlineData("ICollection")] - [InlineData("IList")] - [InlineData("Collection")] - [InlineData("List")] - [InlineData("CustomCollectionWithNoEmptyCtor")] - [InlineData("CustomCollection")] - public void SetCollectionProperty_NonSettableProperty_NonNullValue_WithAddMethod(string propertyName) - { - object value = new SampleClassWithNonSettableCollectionProperties(); - IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); + [Theory] + [InlineData("ICollection")] + [InlineData("IList")] + [InlineData("Collection")] + [InlineData("List")] + [InlineData("CustomCollectionWithNoEmptyCtor")] + [InlineData("CustomCollection")] + public void SetCollectionProperty_NonSettableProperty_NonNullValue_WithAddMethod(string propertyName) + { + object value = new SampleClassWithNonSettableCollectionProperties(); + IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); - DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name); + DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name); - Assert.Equal( - new[] { 1, 2, 3 }, - value.GetType().GetProperty(propertyName).GetValue(value, index: null) as IEnumerable); - } + Assert.Equal( + new[] { 1, 2, 3 }, + value.GetType().GetProperty(propertyName).GetValue(value, index: null) as IEnumerable); + } - [Theory] - [InlineData("Array")] - [InlineData("IEnumerable")] - public void SetCollectionProperty_NonSettableProperty_ArrayValue_FixedSize_Throws(string propertyName) - { - object value = new SampleClassWithNonSettableCollectionProperties(); - Type propertyType = typeof(SampleClassWithNonSettableCollectionProperties).GetProperty(propertyName).PropertyType; - IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); - - ExceptionAssert.Throws( - () => DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name), - string.Format("The value of the property '{0}' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.DeserializationHelpersTest+SampleClassWithNonSettableCollectionProperties' is an array. " + - "Consider adding a setter for the property.", propertyName)); - } + [Theory] + [InlineData("Array")] + [InlineData("IEnumerable")] + public void SetCollectionProperty_NonSettableProperty_ArrayValue_FixedSize_Throws(string propertyName) + { + object value = new SampleClassWithNonSettableCollectionProperties(); + Type propertyType = typeof(SampleClassWithNonSettableCollectionProperties).GetProperty(propertyName).PropertyType; + IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); + + ExceptionAssert.Throws( + () => DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name), + string.Format("The value of the property '{0}' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.DeserializationHelpersTest+SampleClassWithNonSettableCollectionProperties' is an array. " + + "Consider adding a setter for the property.", propertyName)); + } - [Theory] - [InlineData("CustomCollectionWithoutAdd")] - public void SetCollectionProperty_NonSettableProperty_NonNullValue_NoAdd_Throws(string propertyName) - { - object value = new SampleClassWithNonSettableCollectionProperties(); - Type propertyType = typeof(SampleClassWithNonSettableCollectionProperties).GetProperty(propertyName).PropertyType; - IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); - - ExceptionAssert.Throws( - () => DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name), - string.Format("The type '{0}' of the property '{1}' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.DeserializationHelpersTest+SampleClassWithNonSettableCollectionProperties' does not have an Add method. " + - "Consider using a collection type that does have an Add method - for example IList or ICollection.", propertyType.FullName, propertyName)); - } + [Theory] + [InlineData("CustomCollectionWithoutAdd")] + public void SetCollectionProperty_NonSettableProperty_NonNullValue_NoAdd_Throws(string propertyName) + { + object value = new SampleClassWithNonSettableCollectionProperties(); + Type propertyType = typeof(SampleClassWithNonSettableCollectionProperties).GetProperty(propertyName).PropertyType; + IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); + + ExceptionAssert.Throws( + () => DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name), + string.Format("The type '{0}' of the property '{1}' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.DeserializationHelpersTest+SampleClassWithNonSettableCollectionProperties' does not have an Add method. " + + "Consider using a collection type that does have an Add method - for example IList or ICollection.", propertyType.FullName, propertyName)); + } - [Theory] - [InlineData("Array")] - [InlineData("IEnumerable")] - [InlineData("ICollection")] - [InlineData("IList")] - [InlineData("Collection")] - [InlineData("List")] - [InlineData("CustomCollectionWithNoEmptyCtor")] - [InlineData("CustomCollection")] - public void SetCollectionProperty_NonSettableProperty_NullValue_Throws(string propertyName) - { - object value = new SampleClassWithNonSettableCollectionProperties(); - value.GetType().GetProperty(propertyName).SetValue(value, null, null); - IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); - - ExceptionAssert.Throws( - () => DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name), - string.Format("The property '{0}' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.DeserializationHelpersTest+SampleClassWithNonSettableCollectionProperties' returned a null value. " + - "The input stream contains collection items which cannot be added if the instance is null.", propertyName)); - } + [Theory] + [InlineData("Array")] + [InlineData("IEnumerable")] + [InlineData("ICollection")] + [InlineData("IList")] + [InlineData("Collection")] + [InlineData("List")] + [InlineData("CustomCollectionWithNoEmptyCtor")] + [InlineData("CustomCollection")] + public void SetCollectionProperty_NonSettableProperty_NullValue_Throws(string propertyName) + { + object value = new SampleClassWithNonSettableCollectionProperties(); + value.GetType().GetProperty(propertyName).SetValue(value, null, null); + IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); + + ExceptionAssert.Throws( + () => DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name), + string.Format("The property '{0}' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.DeserializationHelpersTest+SampleClassWithNonSettableCollectionProperties' returned a null value. " + + "The input stream contains collection items which cannot be added if the instance is null.", propertyName)); + } - [Fact] - public void SetCollectionProperty_CanConvertNonStandardEdmTypes() - { - SampleClassWithDifferentCollectionProperties value = new SampleClassWithDifferentCollectionProperties(); - IEdmProperty edmProperty = GetMockEdmProperty("UnsignedArray", EdmPrimitiveTypeKind.Int32); + [Fact] + public void SetCollectionProperty_CanConvertNonStandardEdmTypes() + { + SampleClassWithDifferentCollectionProperties value = new SampleClassWithDifferentCollectionProperties(); + IEdmProperty edmProperty = GetMockEdmProperty("UnsignedArray", EdmPrimitiveTypeKind.Int32); - DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name); + DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name); - Assert.Equal( - new uint[] { 1, 2, 3 }, - value.UnsignedArray); - } + Assert.Equal( + new uint[] { 1, 2, 3 }, + value.UnsignedArray); + } - [Fact] - public void SetCollectionProperty_CanConvertDataTime_ByDefault() + [Fact] + public void SetCollectionProperty_CanConvertDataTime_ByDefault() + { + // Arrange + SampleClassWithDifferentCollectionProperties source = new SampleClassWithDifferentCollectionProperties(); + IEdmProperty edmProperty = GetMockEdmProperty("DateTimeList", EdmPrimitiveTypeKind.DateTimeOffset); + DateTime dt = new DateTime(2014, 11, 15, 1, 2, 3); + IList dtos = new List { - // Arrange - SampleClassWithDifferentCollectionProperties source = new SampleClassWithDifferentCollectionProperties(); - IEdmProperty edmProperty = GetMockEdmProperty("DateTimeList", EdmPrimitiveTypeKind.DateTimeOffset); - DateTime dt = new DateTime(2014, 11, 15, 1, 2, 3); - IList dtos = new List - { - new DateTimeOffset(dt, TimeSpan.Zero), - new DateTimeOffset(dt, new TimeSpan(+7, 0, 0)), - new DateTimeOffset(dt, new TimeSpan(-8, 0, 0)) - }; + new DateTimeOffset(dt, TimeSpan.Zero), + new DateTimeOffset(dt, new TimeSpan(+7, 0, 0)), + new DateTimeOffset(dt, new TimeSpan(-8, 0, 0)) + }; - IEnumerable expects = dtos.Select(e => e.LocalDateTime); + IEnumerable expects = dtos.Select(e => e.LocalDateTime); - // Act - DeserializationHelpers.SetCollectionProperty(source, edmProperty, dtos, edmProperty.Name, context: null); + // Act + DeserializationHelpers.SetCollectionProperty(source, edmProperty, dtos, edmProperty.Name, context: null); - // Assert - Assert.Equal(expects, source.DateTimeList); - } - - [Fact] - public void SetCollectionProperty_CanConvertDataTime_ByTimeZoneInfo() - { - // Arrange - SampleClassWithDifferentCollectionProperties source = new SampleClassWithDifferentCollectionProperties(); - IEdmProperty edmProperty = GetMockEdmProperty("DateTimeList", EdmPrimitiveTypeKind.DateTimeOffset); + // Assert + Assert.Equal(expects, source.DateTimeList); + } - TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); - DateTime dt = new DateTime(2014, 11, 15, 1, 2, 3); - IList dtos = new List - { - new DateTimeOffset(dt, TimeSpan.Zero), - new DateTimeOffset(dt, new TimeSpan(+7, 0, 0)), - new DateTimeOffset(dt, new TimeSpan(-8, 0, 0)) - }; - ODataDeserializerContext context = new ODataDeserializerContext - { - TimeZone = tzi - }; + [Fact] + public void SetCollectionProperty_CanConvertDataTime_ByTimeZoneInfo() + { + // Arrange + SampleClassWithDifferentCollectionProperties source = new SampleClassWithDifferentCollectionProperties(); + IEdmProperty edmProperty = GetMockEdmProperty("DateTimeList", EdmPrimitiveTypeKind.DateTimeOffset); - // Act - DeserializationHelpers.SetCollectionProperty(source, edmProperty, dtos, edmProperty.Name, context: context); + TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); + DateTime dt = new DateTime(2014, 11, 15, 1, 2, 3); + IList dtos = new List + { + new DateTimeOffset(dt, TimeSpan.Zero), + new DateTimeOffset(dt, new TimeSpan(+7, 0, 0)), + new DateTimeOffset(dt, new TimeSpan(-8, 0, 0)) + }; + ODataDeserializerContext context = new ODataDeserializerContext + { + TimeZone = tzi + }; - // Assert - Assert.Equal(new List { dt.AddHours(-8), dt.AddHours(-15), dt }, source.DateTimeList); - } + // Act + DeserializationHelpers.SetCollectionProperty(source, edmProperty, dtos, edmProperty.Name, context: context); - [Fact] - public void SetCollectionProperty_CanConvertEnumCollection() - { - SampleClassWithDifferentCollectionProperties value = new SampleClassWithDifferentCollectionProperties(); - IEdmProperty edmProperty = GetMockEdmProperty("FlagsEnum", EdmPrimitiveTypeKind.String); - - DeserializationHelpers.SetCollectionProperty( - value, - edmProperty, - value: new List { FlagsEnum.One, FlagsEnum.Four | FlagsEnum.Two | (FlagsEnum)123 }, - propertyName: edmProperty.Name); - - Assert.Equal( - new FlagsEnum[] { FlagsEnum.One, FlagsEnum.Four | FlagsEnum.Two | (FlagsEnum)123 }, - value.FlagsEnum); - } + // Assert + Assert.Equal(new List { dt.AddHours(-8), dt.AddHours(-15), dt }, source.DateTimeList); + } - [Theory] - [InlineData("NonCollectionString")] - [InlineData("NonCollectionInt")] - public void SetCollectionProperty_OnNonCollection_ThrowsSerialization(string propertyName) - { - object value = new SampleClassWithDifferentCollectionProperties(); - Type propertyType = typeof(SampleClassWithDifferentCollectionProperties).GetProperty(propertyName).PropertyType; - IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); - - ExceptionAssert.Throws( - () => DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name), - Error.Format( - "The type '{0}' of the property '{1}' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.DeserializationHelpersTest+SampleClassWithDifferentCollectionProperties' must be a collection.", - propertyType.FullName, - propertyName)); - } + [Fact] + public void SetCollectionProperty_CanConvertEnumCollection() + { + SampleClassWithDifferentCollectionProperties value = new SampleClassWithDifferentCollectionProperties(); + IEdmProperty edmProperty = GetMockEdmProperty("FlagsEnum", EdmPrimitiveTypeKind.String); + + DeserializationHelpers.SetCollectionProperty( + value, + edmProperty, + value: new List { FlagsEnum.One, FlagsEnum.Four | FlagsEnum.Two | (FlagsEnum)123 }, + propertyName: edmProperty.Name); + + Assert.Equal( + new FlagsEnum[] { FlagsEnum.One, FlagsEnum.Four | FlagsEnum.Two | (FlagsEnum)123 }, + value.FlagsEnum); + } - [Theory] - [InlineData("ICollection")] - [InlineData("IList")] - [InlineData("Collection")] - [InlineData("List")] - [InlineData("CustomCollectionWithNoEmptyCtor")] - [InlineData("CustomCollection")] - public void SetCollectionProperty_ClearsCollection_IfClearCollectionIsTrue(string propertyName) - { - // Arrange - IEnumerable value = new int[] { 1, 2, 3 }; - object resource = new SampleClassWithNonSettableCollectionProperties - { - ICollection = { 42 }, - IList = { 42 }, - Collection = { 42 }, - List = { 42 }, - CustomCollectionWithNoEmptyCtor = { 42 }, - CustomCollection = { 42 } - }; - - // Act - DeserializationHelpers.SetCollectionProperty(resource, propertyName, null, value, clearCollection: true); - - // Assert - Assert.Equal( - value, - resource.GetType().GetProperty(propertyName).GetValue(resource, index: null) as IEnumerable); - } + [Theory] + [InlineData("NonCollectionString")] + [InlineData("NonCollectionInt")] + public void SetCollectionProperty_OnNonCollection_ThrowsSerialization(string propertyName) + { + object value = new SampleClassWithDifferentCollectionProperties(); + Type propertyType = typeof(SampleClassWithDifferentCollectionProperties).GetProperty(propertyName).PropertyType; + IEdmProperty edmProperty = GetMockEdmProperty(propertyName, EdmPrimitiveTypeKind.Int32); + + ExceptionAssert.Throws( + () => DeserializationHelpers.SetCollectionProperty(value, edmProperty, value: new List { 1, 2, 3 }, propertyName: edmProperty.Name), + Error.Format( + "The type '{0}' of the property '{1}' on type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.DeserializationHelpersTest+SampleClassWithDifferentCollectionProperties' must be a collection.", + propertyType.FullName, + propertyName)); + } - //[Fact] - //public void ApplyProperty_DoesNotIgnoreKeyProperty() - //{ - // // Arrange - // ODataProperty property = new ODataProperty { Name = "Key1", Value = "Value1" }; - // EdmEntityType entityType = new EdmEntityType("namespace", "name"); - // entityType.AddKeys(entityType.AddStructuralProperty("Key1", typeof(string).GetEdmPrimitiveTypeReference()); - - // EdmEntityTypeReference entityTypeReference = new EdmEntityTypeReference(entityType, isNullable: false); - // ODataDeserializerProvider provider = ODataDeserializerProviderFactory.Create(); - - // var resource = new Mock(MockBehavior.Strict); - // Type propertyType = typeof(string); - // resource.Setup(r => r.TryGetPropertyType("Key1", out propertyType)).Returns(true).Verifiable(); - // resource.Setup(r => r.TrySetPropertyValue("Key1", "Value1")).Returns(true).Verifiable(); - - // // Act - // DeserializationHelpers.ApplyProperty(property, entityTypeReference, resource.Object, provider, - // new ODataDeserializerContext{ Model = new EdmModel() }); - - // // Assert - // resource.Verify(); - //} - - [Theory] - [InlineData("null", null)] - [InlineData("42", 42)] - [InlineData("\"abc\"", "abc")] - public void ConvertValue_Works_WithODataUntypedValue(string rawValue, object expected) - { - // Arrange - object oDataValue = new ODataUntypedValue + [Theory] + [InlineData("ICollection")] + [InlineData("IList")] + [InlineData("Collection")] + [InlineData("List")] + [InlineData("CustomCollectionWithNoEmptyCtor")] + [InlineData("CustomCollection")] + public void SetCollectionProperty_ClearsCollection_IfClearCollectionIsTrue(string propertyName) + { + // Arrange + IEnumerable value = new int[] { 1, 2, 3 }; + object resource = new SampleClassWithNonSettableCollectionProperties { - RawValue = rawValue + ICollection = { 42 }, + IList = { 42 }, + Collection = { 42 }, + List = { 42 }, + CustomCollectionWithNoEmptyCtor = { 42 }, + CustomCollection = { 42 } }; - // Act - IEdmTypeReference typeRef = null; - object value = DeserializationHelpers.ConvertValue(oDataValue, ref typeRef, null, null, out EdmTypeKind typeKind); + // Act + DeserializationHelpers.SetCollectionProperty(resource, propertyName, null, value, clearCollection: true); - // Assert - Assert.Equal(EdmTypeKind.Primitive, typeKind); - Assert.Equal(expected, value); - } + // Assert + Assert.Equal( + value, + resource.GetType().GetProperty(propertyName).GetValue(resource, index: null) as IEnumerable); + } - [Fact] - public void ConvertValue_Works_WithODataUntypedValue_Decimal() + //[Fact] + //public void ApplyProperty_DoesNotIgnoreKeyProperty() + //{ + // // Arrange + // ODataProperty property = new ODataProperty { Name = "Key1", Value = "Value1" }; + // EdmEntityType entityType = new EdmEntityType("namespace", "name"); + // entityType.AddKeys(entityType.AddStructuralProperty("Key1", typeof(string).GetEdmPrimitiveTypeReference()); + + // EdmEntityTypeReference entityTypeReference = new EdmEntityTypeReference(entityType, isNullable: false); + // ODataDeserializerProvider provider = ODataDeserializerProviderFactory.Create(); + + // var resource = new Mock(MockBehavior.Strict); + // Type propertyType = typeof(string); + // resource.Setup(r => r.TryGetPropertyType("Key1", out propertyType)).Returns(true).Verifiable(); + // resource.Setup(r => r.TrySetPropertyValue("Key1", "Value1")).Returns(true).Verifiable(); + + // // Act + // DeserializationHelpers.ApplyProperty(property, entityTypeReference, resource.Object, provider, + // new ODataDeserializerContext{ Model = new EdmModel() }); + + // // Assert + // resource.Verify(); + //} + + [Theory] + [InlineData("null", null)] + [InlineData("42", 42)] + [InlineData("\"abc\"", "abc")] + public void ConvertValue_Works_WithODataUntypedValue(string rawValue, object expected) + { + // Arrange + object oDataValue = new ODataUntypedValue { - // Arrange - object oDataValue = new ODataUntypedValue - { - RawValue = "42.6" - }; + RawValue = rawValue + }; - // Act - IEdmTypeReference typeRef = null; - object value = DeserializationHelpers.ConvertValue(oDataValue, ref typeRef, null, null, out EdmTypeKind typeKind); + // Act + IEdmTypeReference typeRef = null; + object value = DeserializationHelpers.ConvertValue(oDataValue, ref typeRef, null, null, out EdmTypeKind typeKind); - // Assert - Assert.Equal(EdmTypeKind.Primitive, typeKind); - Assert.Equal((decimal)42.6, value); - } + // Assert + Assert.Equal(EdmTypeKind.Primitive, typeKind); + Assert.Equal(expected, value); + } - [Fact] - public void ConvertValue_Works_WithODataUntypedValue_Double() + [Fact] + public void ConvertValue_Works_WithODataUntypedValue_Decimal() + { + // Arrange + object oDataValue = new ODataUntypedValue { - // Arrange - object oDataValue = new ODataUntypedValue - { - RawValue = "-1.643e6" - }; + RawValue = "42.6" + }; - // Act - IEdmTypeReference typeRef = null; - object value = DeserializationHelpers.ConvertValue(oDataValue, ref typeRef, null, null, out EdmTypeKind typeKind); + // Act + IEdmTypeReference typeRef = null; + object value = DeserializationHelpers.ConvertValue(oDataValue, ref typeRef, null, null, out EdmTypeKind typeKind); - // Assert - Assert.Equal(EdmTypeKind.Primitive, typeKind); - Assert.Equal((double)-1643000, value); - } + // Assert + Assert.Equal(EdmTypeKind.Primitive, typeKind); + Assert.Equal((decimal)42.6, value); + } - [Theory] - [InlineData("[abc1.643e6]")] - [InlineData("{abc1.643e6}")] - [InlineData("abc1.643e6")] - public void ConvertValue_ThrowsODataException_WithInvalidValue(string rawValue) + [Fact] + public void ConvertValue_Works_WithODataUntypedValue_Double() + { + // Arrange + object oDataValue = new ODataUntypedValue { - // Arrange - object oDataValue = new ODataUntypedValue - { - RawValue = rawValue - }; + RawValue = "-1.643e6" + }; - // Act - IEdmTypeReference typeRef = null; - Action test = () => DeserializationHelpers.ConvertValue(oDataValue, ref typeRef, null, null, out EdmTypeKind typeKind); + // Act + IEdmTypeReference typeRef = null; + object value = DeserializationHelpers.ConvertValue(oDataValue, ref typeRef, null, null, out EdmTypeKind typeKind); - // Assert - ExceptionAssert.Throws(test, - $"The given untyped value '{rawValue}' in payload is invalid. Consider using a OData type annotation explicitly."); - } + // Assert + Assert.Equal(EdmTypeKind.Primitive, typeKind); + Assert.Equal((double)-1643000, value); + } - [Fact] - public void ApplyProperty_FailsWithUsefulErrorMessageOnUnknownProperty() + [Theory] + [InlineData("[abc1.643e6]")] + [InlineData("{abc1.643e6}")] + [InlineData("abc1.643e6")] + public void ConvertValue_ThrowsODataException_WithInvalidValue(string rawValue) + { + // Arrange + object oDataValue = new ODataUntypedValue { - // Arrange - const string HelpfulErrorMessage = - "The property 'Unknown' does not exist on type 'namespace.name'. Make sure to only use property names " + - "that are defined by the type."; - - var property = new ODataProperty { Name = "Unknown", Value = "Value" }; - var entityType = new EdmComplexType("namespace", "name"); - entityType.AddStructuralProperty("Known", EdmCoreModel.Instance.GetString(true)); - - var entityTypeReference = new EdmComplexTypeReference(entityType, isNullable: false); - - // Act - var exception = Assert.Throws(() => - DeserializationHelpers.ApplyProperty( - property, - entityTypeReference, - resource: null, - deserializerProvider: null, - readContext: null)); - - // Assert - Assert.Equal(HelpfulErrorMessage, exception.Message); - } + RawValue = rawValue + }; - [Fact] - public void GetCollectionElementTypeName_ThrowsODataException_NestedCollection() - { - // Arrange & Act & Assert - ExceptionAssert.Throws( - () => DeserializationHelpers.GetCollectionElementTypeName("Collection(Edm.Int32)", true), - "The type 'Collection(Edm.Int32)' is a nested collection type. Nested collection types are not allowed."); - } + // Act + IEdmTypeReference typeRef = null; + Action test = () => DeserializationHelpers.ConvertValue(oDataValue, ref typeRef, null, null, out EdmTypeKind typeKind); - private static IEdmProperty GetMockEdmProperty(string name, EdmPrimitiveTypeKind elementType) - { - Mock property = new Mock(); - property.Setup(p => p.Name).Returns(name); - IEdmTypeReference elementTypeReference = - EdmCoreModel.Instance.GetPrimitiveType(elementType).ToEdmTypeReference(isNullable: false); - property.Setup(p => p.Type) - .Returns(new EdmCollectionTypeReference(new EdmCollectionType(elementTypeReference))); - return property.Object; - } + // Assert + ExceptionAssert.Throws(test, + $"The given untyped value '{rawValue}' in payload is invalid. Consider using a OData type annotation explicitly."); + } - private class GetPropertyType_TestClass - { - public int Property { get; set; } - } + [Fact] + public void ApplyProperty_FailsWithUsefulErrorMessageOnUnknownProperty() + { + // Arrange + const string HelpfulErrorMessage = + "The property 'Unknown' does not exist on type 'namespace.name'. Make sure to only use property names " + + "that are defined by the type."; + + var property = new ODataProperty { Name = "Unknown", Value = "Value" }; + var entityType = new EdmComplexType("namespace", "name"); + entityType.AddStructuralProperty("Known", EdmCoreModel.Instance.GetString(true)); + + var entityTypeReference = new EdmComplexTypeReference(entityType, isNullable: false); + + // Act + var exception = Assert.Throws(() => + DeserializationHelpers.ApplyProperty( + property, + entityTypeReference, + resource: null, + deserializerProvider: null, + readContext: null)); + + // Assert + Assert.Equal(HelpfulErrorMessage, exception.Message); + } - private class SampleClassWithSettableCollectionProperties - { - public int[] Array { get; set; } + [Fact] + public void GetCollectionElementTypeName_ThrowsODataException_NestedCollection() + { + // Arrange & Act & Assert + ExceptionAssert.Throws( + () => DeserializationHelpers.GetCollectionElementTypeName("Collection(Edm.Int32)", true), + "The type 'Collection(Edm.Int32)' is a nested collection type. Nested collection types are not allowed."); + } - public IEnumerable IEnumerable { get; set; } + private static IEdmProperty GetMockEdmProperty(string name, EdmPrimitiveTypeKind elementType) + { + Mock property = new Mock(); + property.Setup(p => p.Name).Returns(name); + IEdmTypeReference elementTypeReference = + EdmCoreModel.Instance.GetPrimitiveType(elementType).ToEdmTypeReference(isNullable: false); + property.Setup(p => p.Type) + .Returns(new EdmCollectionTypeReference(new EdmCollectionType(elementTypeReference))); + return property.Object; + } + + private class GetPropertyType_TestClass + { + public int Property { get; set; } + } - public ICollection ICollection { get; set; } + private class SampleClassWithSettableCollectionProperties + { + public int[] Array { get; set; } - public IList IList { get; set; } + public IEnumerable IEnumerable { get; set; } - public Collection Collection { get; set; } + public ICollection ICollection { get; set; } - public List List { get; set; } + public IList IList { get; set; } - public CustomCollection CustomCollection { get; set; } + public Collection Collection { get; set; } - public CustomCollectionWithNoEmptyCtor CustomCollectionWithNoEmptyCtor { get; set; } + public List List { get; set; } - public ICustomCollectionInterface ICustomCollectionInterface { get; set; } - } + public CustomCollection CustomCollection { get; set; } - private class SampleClassWithNonSettableCollectionProperties + public CustomCollectionWithNoEmptyCtor CustomCollectionWithNoEmptyCtor { get; set; } + + public ICustomCollectionInterface ICustomCollectionInterface { get; set; } + } + + private class SampleClassWithNonSettableCollectionProperties + { + public SampleClassWithNonSettableCollectionProperties() { - public SampleClassWithNonSettableCollectionProperties() - { - Array = new int[0]; - IEnumerable = new int[0]; - ICollection = new Collection(); - IList = new List(); - Collection = new Collection(); - List = new List(); - CustomCollection = new CustomCollection(); - CustomCollectionWithNoEmptyCtor = new CustomCollectionWithNoEmptyCtor(100); - CustomCollectionWithoutAdd = new CustomCollectionWithoutAdd(); - } + Array = new int[0]; + IEnumerable = new int[0]; + ICollection = new Collection(); + IList = new List(); + Collection = new Collection(); + List = new List(); + CustomCollection = new CustomCollection(); + CustomCollectionWithNoEmptyCtor = new CustomCollectionWithNoEmptyCtor(100); + CustomCollectionWithoutAdd = new CustomCollectionWithoutAdd(); + } - public int[] Array { get; internal set; } + public int[] Array { get; internal set; } - public IEnumerable IEnumerable { get; internal set; } + public IEnumerable IEnumerable { get; internal set; } - public ICollection ICollection { get; internal set; } + public ICollection ICollection { get; internal set; } - public IList IList { get; internal set; } + public IList IList { get; internal set; } - public Collection Collection { get; internal set; } + public Collection Collection { get; internal set; } - public List List { get; internal set; } + public List List { get; internal set; } - public CustomCollection CustomCollection { get; internal set; } + public CustomCollection CustomCollection { get; internal set; } - public CustomCollectionWithNoEmptyCtor CustomCollectionWithNoEmptyCtor { get; internal set; } + public CustomCollectionWithNoEmptyCtor CustomCollectionWithNoEmptyCtor { get; internal set; } - public CustomCollectionWithoutAdd CustomCollectionWithoutAdd { get; internal set; } - } + public CustomCollectionWithoutAdd CustomCollectionWithoutAdd { get; internal set; } + } - private class SampleClassWithDifferentCollectionProperties - { - public string NonCollectionString { get; set; } + private class SampleClassWithDifferentCollectionProperties + { + public string NonCollectionString { get; set; } - public int NonCollectionInt { get; set; } + public int NonCollectionInt { get; set; } - public uint[] UnsignedArray { get; set; } + public uint[] UnsignedArray { get; set; } - public FlagsEnum[] FlagsEnum { get; set; } + public FlagsEnum[] FlagsEnum { get; set; } - public IList DateTimeList { get; set; } - } + public IList DateTimeList { get; set; } + } - private class CustomCollection : List { } + private class CustomCollection : List { } - private class CustomCollectionWithNoEmptyCtor : List + private class CustomCollectionWithNoEmptyCtor : List + { + public CustomCollectionWithNoEmptyCtor(int i) { - public CustomCollectionWithNoEmptyCtor(int i) - { - } } + } - private interface ICustomCollectionInterface : IEnumerable + private interface ICustomCollectionInterface : IEnumerable + { + } + + private class CustomCollectionWithoutAdd : IEnumerable + { + private List _list = new List(); + + public IEnumerator GetEnumerator() { + throw new NotImplementedException(); } - private class CustomCollectionWithoutAdd : IEnumerable + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { - private List _list = new List(); - - public IEnumerator GetEnumerator() - { - throw new NotImplementedException(); - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/DeserializationServiceProviderHelper.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/DeserializationServiceProviderHelper.cs index 7d785c873..907b41b49 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/DeserializationServiceProviderHelper.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/DeserializationServiceProviderHelper.cs @@ -11,28 +11,27 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +internal static class DeserializationServiceProviderHelper { - internal static class DeserializationServiceProviderHelper + public static IServiceProvider GetServiceProvider() { - public static IServiceProvider GetServiceProvider() - { - IServiceCollection services = new ServiceCollection(); + IServiceCollection services = new ServiceCollection(); - services.AddSingleton(); + services.AddSingleton(); - // Deserializers. - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + // Deserializers. + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); - return services.BuildServiceProvider(); - } + return services.BuildServiceProvider(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/EnumDeserializationHelpersTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/EnumDeserializationHelpersTests.cs index aa83c7c19..97c7d70b6 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/EnumDeserializationHelpersTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/EnumDeserializationHelpersTests.cs @@ -12,63 +12,62 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class EnumDeserializationHelpersTests { - public class EnumDeserializationHelpersTests + [Fact] + public void ConvertEnumValue_ThrowsArgumentNull_ForInputParameters() { - [Fact] - public void ConvertEnumValue_ThrowsArgumentNull_ForInputParameters() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => EnumDeserializationHelpers.ConvertEnumValue(null, null), "value"); - ExceptionAssert.ThrowsArgumentNull(() => EnumDeserializationHelpers.ConvertEnumValue(42, null), "type"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => EnumDeserializationHelpers.ConvertEnumValue(null, null), "value"); + ExceptionAssert.ThrowsArgumentNull(() => EnumDeserializationHelpers.ConvertEnumValue(42, null), "type"); + } - [Fact] - public void ConvertEnumValue_Returns_ForPocoEnum() - { - // Arrange & Act & Assert - Assert.Equal(EnumColor.Blue, EnumDeserializationHelpers.ConvertEnumValue(EnumColor.Blue, typeof(EnumColor))); - Assert.Equal(EnumColor.Blue, EnumDeserializationHelpers.ConvertEnumValue(EnumColor.Blue, typeof(EnumColor?))); - } + [Fact] + public void ConvertEnumValue_Returns_ForPocoEnum() + { + // Arrange & Act & Assert + Assert.Equal(EnumColor.Blue, EnumDeserializationHelpers.ConvertEnumValue(EnumColor.Blue, typeof(EnumColor))); + Assert.Equal(EnumColor.Blue, EnumDeserializationHelpers.ConvertEnumValue(EnumColor.Blue, typeof(EnumColor?))); + } - [Fact] - public void ConvertEnumValue_ThrowsValidationException_NonODataEnumValue() - { - // Arrange & Act & Assert - ExceptionAssert.Throws( - () => EnumDeserializationHelpers.ConvertEnumValue(42, typeof(EnumColor)), - "The value with type 'Int32' must have type 'ODataEnumValue'."); - } + [Fact] + public void ConvertEnumValue_ThrowsValidationException_NonODataEnumValue() + { + // Arrange & Act & Assert + ExceptionAssert.Throws( + () => EnumDeserializationHelpers.ConvertEnumValue(42, typeof(EnumColor)), + "The value with type 'Int32' must have type 'ODataEnumValue'."); + } - [Fact] - public void ConvertEnumValue_ThrowsValidationException_NonEnumType() - { - // Arrange & Act & Assert - ODataEnumValue enumValue = new ODataEnumValue("Red"); + [Fact] + public void ConvertEnumValue_ThrowsValidationException_NonEnumType() + { + // Arrange & Act & Assert + ODataEnumValue enumValue = new ODataEnumValue("Red"); - ExceptionAssert.Throws( - () => EnumDeserializationHelpers.ConvertEnumValue(enumValue, typeof(int)), - "The type 'Int32' must be an enum or Nullable where T is an enum type."); - } + ExceptionAssert.Throws( + () => EnumDeserializationHelpers.ConvertEnumValue(enumValue, typeof(int)), + "The type 'Int32' must be an enum or Nullable where T is an enum type."); + } - [Fact] - public void ConvertEnumValue_Returns_ForODataEnumValue() - { - // Arrange & Act & Assert - ODataEnumValue enumValue = new ODataEnumValue("Red"); - Assert.Equal(EnumColor.Red, EnumDeserializationHelpers.ConvertEnumValue(enumValue, typeof(EnumColor))); + [Fact] + public void ConvertEnumValue_Returns_ForODataEnumValue() + { + // Arrange & Act & Assert + ODataEnumValue enumValue = new ODataEnumValue("Red"); + Assert.Equal(EnumColor.Red, EnumDeserializationHelpers.ConvertEnumValue(enumValue, typeof(EnumColor))); - // Arrange & Act & Assert - enumValue = new ODataEnumValue("Green"); - Assert.Equal(EnumColor.Green, EnumDeserializationHelpers.ConvertEnumValue(enumValue, typeof(EnumColor))); - } + // Arrange & Act & Assert + enumValue = new ODataEnumValue("Green"); + Assert.Equal(EnumColor.Green, EnumDeserializationHelpers.ConvertEnumValue(enumValue, typeof(EnumColor))); + } - public enum EnumColor - { - Red, - Blue, - Green - } + public enum EnumColor + { + Red, + Blue, + Green } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/HttpRequestODataMessage.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/HttpRequestODataMessage.cs index 584c24f3d..31c0275dc 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/HttpRequestODataMessage.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/HttpRequestODataMessage.cs @@ -15,79 +15,78 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +class HttpRequestODataMessage : IODataRequestMessageAsync { - class HttpRequestODataMessage : IODataRequestMessageAsync - { - public HttpRequest _httpRequest; + public HttpRequest _httpRequest; - public Dictionary _headers; + public Dictionary _headers; - //public HttpRequestODataMessage(HttpRequestMessage request) - //{ - // _request = request; - // _headers = Enumerable - // .Concat>>(request.Headers, request.Content.Headers) - // .Select(kvp => new KeyValuePair(kvp.Key, string.Join(";", kvp.Value))) - // .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - //} + //public HttpRequestODataMessage(HttpRequestMessage request) + //{ + // _request = request; + // _headers = Enumerable + // .Concat>>(request.Headers, request.Content.Headers) + // .Select(kvp => new KeyValuePair(kvp.Key, string.Join(";", kvp.Value))) + // .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + //} - public HttpRequestODataMessage(HttpRequest request) - { - _httpRequest = request; - _headers = request.Headers. - Select(kvp => new KeyValuePair(kvp.Key, string.Join(";", kvp.Value))) - .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - } + public HttpRequestODataMessage(HttpRequest request) + { + _httpRequest = request; + _headers = request.Headers. + Select(kvp => new KeyValuePair(kvp.Key, string.Join(";", kvp.Value))) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + } - public string GetHeader(string headerName) - { - return _headers[headerName]; - } + public string GetHeader(string headerName) + { + return _headers[headerName]; + } - public Stream GetStream() - { - // Can't make this async as the interface requires a return stream, not Task - return _httpRequest.Body; - } + public Stream GetStream() + { + // Can't make this async as the interface requires a return stream, not Task + return _httpRequest.Body; + } - public IEnumerable> Headers - { - get { return _headers; } - } + public IEnumerable> Headers + { + get { return _headers; } + } - public string Method + public string Method + { + get { - get - { - return _httpRequest.Method; - } - set - { - throw new NotImplementedException(); - } + return _httpRequest.Method; } - - public void SetHeader(string headerName, string headerValue) + set { throw new NotImplementedException(); } + } - public Task GetStreamAsync() + public void SetHeader(string headerName, string headerValue) + { + throw new NotImplementedException(); + } + + public Task GetStreamAsync() + { + return Task.FromResult(_httpRequest.Body); + } + + public Uri Url + { + get { - return Task.FromResult(_httpRequest.Body); + return new Uri(UriHelper.GetEncodedUrl(_httpRequest)); } - - public Uri Url + set { - get - { - return new Uri(UriHelper.GetEncodedUrl(_httpRequest)); - } - set - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/MockODataRequestMessage.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/MockODataRequestMessage.cs index 74a2832e8..3b4c081cb 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/MockODataRequestMessage.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/MockODataRequestMessage.cs @@ -11,74 +11,73 @@ using System.Threading.Tasks; using Microsoft.OData; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +internal class MockODataRequestMessage : IODataRequestMessageAsync { - internal class MockODataRequestMessage : IODataRequestMessageAsync + Dictionary _headers; + MemoryStream _body; + + public MockODataRequestMessage() { - Dictionary _headers; - MemoryStream _body; + _headers = new Dictionary(); + _body = new MemoryStream(); + } - public MockODataRequestMessage() - { - _headers = new Dictionary(); - _body = new MemoryStream(); - } + public MockODataRequestMessage(MockODataRequestMessage requestMessage) + { + _headers = new Dictionary(requestMessage._headers); + _body = new MemoryStream(requestMessage._body.ToArray()); + } - public MockODataRequestMessage(MockODataRequestMessage requestMessage) - { - _headers = new Dictionary(requestMessage._headers); - _body = new MemoryStream(requestMessage._body.ToArray()); - } + public string GetHeader(string headerName) + { + string value; + _headers.TryGetValue(headerName, out value); + return value; + } - public string GetHeader(string headerName) - { - string value; - _headers.TryGetValue(headerName, out value); - return value; - } + public Stream GetStream() + { + return _body; + } - public Stream GetStream() - { - return _body; - } + public IEnumerable> Headers + { + get { return _headers; } + } - public IEnumerable> Headers + public string Method + { + get { - get { return _headers; } + return "Get"; } - - public string Method + set { - get - { - return "Get"; - } - set - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } + } - public void SetHeader(string headerName, string headerValue) - { - _headers[headerName] = headerValue; - } + public void SetHeader(string headerName, string headerValue) + { + _headers[headerName] = headerValue; + } - public Task GetStreamAsync() + public Task GetStreamAsync() + { + return Task.FromResult(_body); + } + + public Uri Url + { + get { - return Task.FromResult(_body); + throw new NotImplementedException(); } - - public Uri Url + set { - get - { - throw new NotImplementedException(); - } - set - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataActionPayloadDeserializerTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataActionPayloadDeserializerTest.cs index fb9e9616f..28cd1a1a6 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataActionPayloadDeserializerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataActionPayloadDeserializerTest.cs @@ -25,802 +25,801 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class ODataActionPayloadDeserializerTest { - public class ODataActionPayloadDeserializerTest - { - private static IEdmModel _model; - private static IEdmEntityContainer _container; - private static IODataDeserializerProvider _deserializerProvider; - private static ODataActionPayloadDeserializer _deserializer; - private const string _serviceRoot = "http://any/"; + private static IEdmModel _model; + private static IEdmEntityContainer _container; + private static IODataDeserializerProvider _deserializerProvider; + private static ODataActionPayloadDeserializer _deserializer; + private const string _serviceRoot = "http://any/"; - static ODataActionPayloadDeserializerTest() - { - _model = GetModel(); - _container = _model.EntityContainer; + static ODataActionPayloadDeserializerTest() + { + _model = GetModel(); + _container = _model.EntityContainer; - _deserializerProvider = DeserializationServiceProviderHelper.GetServiceProvider().GetRequiredService(); - _deserializer = new ODataActionPayloadDeserializer(_deserializerProvider); - } + _deserializerProvider = DeserializationServiceProviderHelper.GetServiceProvider().GetRequiredService(); + _deserializer = new ODataActionPayloadDeserializer(_deserializerProvider); + } - [Fact] - public void Ctor_ThrowsArgumentNull_DeserializerProvider() - { - ExceptionAssert.ThrowsArgumentNull( - () => new ODataActionPayloadDeserializer(deserializerProvider: null), - "deserializerProvider"); - } + [Fact] + public void Ctor_ThrowsArgumentNull_DeserializerProvider() + { + ExceptionAssert.ThrowsArgumentNull( + () => new ODataActionPayloadDeserializer(deserializerProvider: null), + "deserializerProvider"); + } - [Fact] - public void Ctor_SetsProperty_DeserializerProvider() - { - // Arrange - IODataDeserializerProvider deserializerProvider = new Mock().Object; + [Fact] + public void Ctor_SetsProperty_DeserializerProvider() + { + // Arrange + IODataDeserializerProvider deserializerProvider = new Mock().Object; - // Act - var deserializer = new ODataActionPayloadDeserializer(deserializerProvider); + // Act + var deserializer = new ODataActionPayloadDeserializer(deserializerProvider); - // Assert - Assert.Same(deserializerProvider, deserializer.DeserializerProvider); - } + // Assert + Assert.Same(deserializerProvider, deserializer.DeserializerProvider); + } - [Fact] - public void Ctor_SetsProperty_ODataPayloadKind() - { - // Arrange - IODataDeserializerProvider deserializerProvider = new Mock().Object; + [Fact] + public void Ctor_SetsProperty_ODataPayloadKind() + { + // Arrange + IODataDeserializerProvider deserializerProvider = new Mock().Object; - // Act - var deserializer = new ODataActionPayloadDeserializer(deserializerProvider); + // Act + var deserializer = new ODataActionPayloadDeserializer(deserializerProvider); - // Assert - Assert.Equal(ODataPayloadKind.Parameter, deserializer.ODataPayloadKind); - } + // Assert + Assert.Equal(ODataPayloadKind.Parameter, deserializer.ODataPayloadKind); + } - [Fact] - public async Task ReadAsync_ThrowsArgumentNull_MessageReader() - { - // Arrange - ODataActionPayloadDeserializer deserializer = new ODataActionPayloadDeserializer(_deserializerProvider); + [Fact] + public async Task ReadAsync_ThrowsArgumentNull_MessageReader() + { + // Arrange + ODataActionPayloadDeserializer deserializer = new ODataActionPayloadDeserializer(_deserializerProvider); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => deserializer.ReadAsync(messageReader: null, type: typeof(ODataActionParameters), readContext: new ODataDeserializerContext()), - "messageReader"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => deserializer.ReadAsync(messageReader: null, type: typeof(ODataActionParameters), readContext: new ODataDeserializerContext()), + "messageReader"); + } - [Fact] - public async Task ReadAsync_ThrowsArgumentNull_ReadContext() - { - // Arrange - ODataActionPayloadDeserializer deserializer = new ODataActionPayloadDeserializer(_deserializerProvider); - ODataMessageReader messageReader = ODataTestUtil.GetMockODataMessageReader(); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => deserializer.ReadAsync(messageReader, typeof(ODataActionParameters), readContext: null), - "readContext"); - } + [Fact] + public async Task ReadAsync_ThrowsArgumentNull_ReadContext() + { + // Arrange + ODataActionPayloadDeserializer deserializer = new ODataActionPayloadDeserializer(_deserializerProvider); + ODataMessageReader messageReader = ODataTestUtil.GetMockODataMessageReader(); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => deserializer.ReadAsync(messageReader, typeof(ODataActionParameters), readContext: null), + "readContext"); + } - [Fact] - public async Task ReadAsync_Throws_SerializationException_ODataPathMissing() - { - // Arrange - ODataActionPayloadDeserializer deserializer = new ODataActionPayloadDeserializer(_deserializerProvider); - ODataMessageReader messageReader = ODataTestUtil.GetMockODataMessageReader(); - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => deserializer.ReadAsync(messageReader, typeof(ODataActionParameters), readContext: new ODataDeserializerContext()), - "The operation cannot be completed because no ODataPath is available for the request."); - } + [Fact] + public async Task ReadAsync_Throws_SerializationException_ODataPathMissing() + { + // Arrange + ODataActionPayloadDeserializer deserializer = new ODataActionPayloadDeserializer(_deserializerProvider); + ODataMessageReader messageReader = ODataTestUtil.GetMockODataMessageReader(); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => deserializer.ReadAsync(messageReader, typeof(ODataActionParameters), readContext: new ODataDeserializerContext()), + "The operation cannot be completed because no ODataPath is available for the request."); + } - public static TheoryDataSet DeserializeWithPrimitiveParametersTest + public static TheoryDataSet DeserializeWithPrimitiveParametersTest + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - {"Primitive", GetBoundAction("Primitive"), CreateBoundPath("Primitive") }, - {"UnboundPrimitive", GetUnboundAction("UnboundPrimitive"), CreateUnboundPath("UnboundPrimitive")} - }; - } + {"Primitive", GetBoundAction("Primitive"), CreateBoundPath("Primitive") }, + {"UnboundPrimitive", GetUnboundAction("UnboundPrimitive"), CreateUnboundPath("UnboundPrimitive")} + }; } + } - public static TheoryDataSet DeserializeWithComplexParametersTest + public static TheoryDataSet DeserializeWithComplexParametersTest + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - {"Complex", GetBoundAction("Complex"), CreateBoundPath("Complex") }, - {"UnboundComplex", GetUnboundAction("UnboundComplex"), CreateUnboundPath("UnboundComplex")} - }; - } + {"Complex", GetBoundAction("Complex"), CreateBoundPath("Complex") }, + {"UnboundComplex", GetUnboundAction("UnboundComplex"), CreateUnboundPath("UnboundComplex")} + }; } - public static TheoryDataSet DeserializeWithEnumParametersTest + } + public static TheoryDataSet DeserializeWithEnumParametersTest + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - {"Enum", GetBoundAction("Enum"), CreateBoundPath("Enum") }, - {"UnboundEnum", GetUnboundAction("UnboundEnum"), CreateUnboundPath("UnboundEnum")} - }; - } + {"Enum", GetBoundAction("Enum"), CreateBoundPath("Enum") }, + {"UnboundEnum", GetUnboundAction("UnboundEnum"), CreateUnboundPath("UnboundEnum")} + }; } + } - public static TheoryDataSet DeserializeWithEntityParametersTest + public static TheoryDataSet DeserializeWithEntityParametersTest + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { GetBoundAction("Entity"), CreateBoundPath("Entity") }, - { GetUnboundAction("UnboundEntity"), CreateUnboundPath("UnboundEntity")} - }; - } + { GetBoundAction("Entity"), CreateBoundPath("Entity") }, + { GetUnboundAction("UnboundEntity"), CreateUnboundPath("UnboundEntity")} + }; } + } - public static TheoryDataSet DeserializeWithPrimitiveCollectionsTest + public static TheoryDataSet DeserializeWithPrimitiveCollectionsTest + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - {"PrimitiveCollection", GetBoundAction("PrimitiveCollection"), CreateBoundPath("PrimitiveCollection") }, - {"UnboundPrimitiveCollection", GetUnboundAction("UnboundPrimitiveCollection"), CreateUnboundPath("UnboundPrimitiveCollection")} - }; - } + {"PrimitiveCollection", GetBoundAction("PrimitiveCollection"), CreateBoundPath("PrimitiveCollection") }, + {"UnboundPrimitiveCollection", GetUnboundAction("UnboundPrimitiveCollection"), CreateUnboundPath("UnboundPrimitiveCollection")} + }; } + } - public static TheoryDataSet DeserializeWithComplexCollectionsTest + public static TheoryDataSet DeserializeWithComplexCollectionsTest + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - {"ComplexCollection", GetBoundAction("ComplexCollection"), CreateBoundPath("ComplexCollection") }, - {"UnboundComplexCollection", GetUnboundAction("UnboundComplexCollection"), CreateUnboundPath("UnboundComplexCollection")} - }; - } + {"ComplexCollection", GetBoundAction("ComplexCollection"), CreateBoundPath("ComplexCollection") }, + {"UnboundComplexCollection", GetUnboundAction("UnboundComplexCollection"), CreateUnboundPath("UnboundComplexCollection")} + }; } - public static TheoryDataSet DeserializeWithEnumCollectionsTest + } + public static TheoryDataSet DeserializeWithEnumCollectionsTest + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - {"EnumCollection", GetBoundAction("EnumCollection"), CreateBoundPath("EnumCollection") }, - {"UnboundEnumCollection", GetUnboundAction("UnboundEnumCollection"), CreateUnboundPath("UnboundEnumCollection")} - }; - } + {"EnumCollection", GetBoundAction("EnumCollection"), CreateBoundPath("EnumCollection") }, + {"UnboundEnumCollection", GetUnboundAction("UnboundEnumCollection"), CreateUnboundPath("UnboundEnumCollection")} + }; } + } - public static TheoryDataSet DeserializeWithEntityCollectionsTest + public static TheoryDataSet DeserializeWithEntityCollectionsTest + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { GetBoundAction("EntityCollection"), CreateBoundPath("EntityCollection") }, - { GetUnboundAction("UnboundEntityCollection"), CreateUnboundPath("UnboundEntityCollection")} - }; - } + { GetBoundAction("EntityCollection"), CreateBoundPath("EntityCollection") }, + { GetUnboundAction("UnboundEntityCollection"), CreateUnboundPath("UnboundEntityCollection")} + }; } + } - [Theory] - [MemberData(nameof(DeserializeWithPrimitiveParametersTest))] - public async Task Can_DeserializePayload_WithPrimitiveParameters(string actionName, IEdmAction expectedAction, ODataPath path) - { - // Arrange - const int Quantity = 1; - const string ProductCode = "PCode"; - string body = "{" + - string.Format(@" ""Quantity"": {0} , ""ProductCode"": ""{1}"" , ""Birthday"": ""2015-02-27"", ""BkgColor"": ""Red"", ""InnerColor"": null", Quantity, ProductCode) + - "}"; - - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(body)); - message.SetHeader("Content-Type", "application/json"); - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); - ODataDeserializerContext context = new ODataDeserializerContext() { Path = path, Model = _model }; - - // Act - ODataActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context) as ODataActionParameters; - IEdmAction action = ODataActionPayloadDeserializer.GetAction(context); - - // Assert - Assert.NotNull(actionName); - Assert.Same(expectedAction, action); - Assert.NotNull(payload); - Assert.True(payload.ContainsKey("Quantity")); - Assert.Equal(Quantity, payload["Quantity"]); - Assert.True(payload.ContainsKey("ProductCode")); - Assert.Equal(ProductCode, payload["ProductCode"]); - - Assert.True(payload.ContainsKey("Birthday")); - Assert.Equal(new Date(2015, 2, 27), payload["Birthday"]); - - Assert.True(payload.ContainsKey("BkgColor")); - AColor bkgColor = Assert.IsType(payload["BkgColor"]); - Assert.Equal(AColor.Red, bkgColor); - - Assert.True(payload.ContainsKey("InnerColor")); - Assert.Null(payload["InnerColor"]); - } + [Theory] + [MemberData(nameof(DeserializeWithPrimitiveParametersTest))] + public async Task Can_DeserializePayload_WithPrimitiveParameters(string actionName, IEdmAction expectedAction, ODataPath path) + { + // Arrange + const int Quantity = 1; + const string ProductCode = "PCode"; + string body = "{" + + string.Format(@" ""Quantity"": {0} , ""ProductCode"": ""{1}"" , ""Birthday"": ""2015-02-27"", ""BkgColor"": ""Red"", ""InnerColor"": null", Quantity, ProductCode) + + "}"; + + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(body)); + message.SetHeader("Content-Type", "application/json"); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); + ODataDeserializerContext context = new ODataDeserializerContext() { Path = path, Model = _model }; + + // Act + ODataActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context) as ODataActionParameters; + IEdmAction action = ODataActionPayloadDeserializer.GetAction(context); + + // Assert + Assert.NotNull(actionName); + Assert.Same(expectedAction, action); + Assert.NotNull(payload); + Assert.True(payload.ContainsKey("Quantity")); + Assert.Equal(Quantity, payload["Quantity"]); + Assert.True(payload.ContainsKey("ProductCode")); + Assert.Equal(ProductCode, payload["ProductCode"]); + + Assert.True(payload.ContainsKey("Birthday")); + Assert.Equal(new Date(2015, 2, 27), payload["Birthday"]); + + Assert.True(payload.ContainsKey("BkgColor")); + AColor bkgColor = Assert.IsType(payload["BkgColor"]); + Assert.Equal(AColor.Red, bkgColor); + + Assert.True(payload.ContainsKey("InnerColor")); + Assert.Null(payload["InnerColor"]); + } - [Theory] - [MemberData(nameof(DeserializeWithComplexParametersTest))] - public async Task Can_DeserializePayload_WithComplexParameters(string actionName, IEdmAction expectedAction, ODataPath path) - { - // Arrange - const string Body = @"{ ""Quantity"": 1 , ""Address"": { ""StreetAddress"":""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 } }"; - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); - message.SetHeader("Content-Type", "application/json"); - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); - ODataDeserializerContext context = new ODataDeserializerContext() { Path = path, Model = _model }; - - // Act - ODataActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context) as ODataActionParameters; - IEdmAction action = ODataActionPayloadDeserializer.GetAction(context); - - // Assert - Assert.NotNull(actionName); - Assert.Same(expectedAction, action); - Assert.NotNull(payload); - Assert.True(payload.ContainsKey("Quantity")); - Assert.Equal(1, payload["Quantity"]); - Assert.True(payload.ContainsKey("Address")); - MyAddress address = payload["Address"] as MyAddress; - Assert.NotNull(address); - Assert.Equal("1 Microsoft Way", address.StreetAddress); - Assert.Equal("Redmond", address.City); - Assert.Equal("WA", address.State); - Assert.Equal(98052, address.ZipCode); - } + [Theory] + [MemberData(nameof(DeserializeWithComplexParametersTest))] + public async Task Can_DeserializePayload_WithComplexParameters(string actionName, IEdmAction expectedAction, ODataPath path) + { + // Arrange + const string Body = @"{ ""Quantity"": 1 , ""Address"": { ""StreetAddress"":""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 } }"; + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); + message.SetHeader("Content-Type", "application/json"); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); + ODataDeserializerContext context = new ODataDeserializerContext() { Path = path, Model = _model }; + + // Act + ODataActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context) as ODataActionParameters; + IEdmAction action = ODataActionPayloadDeserializer.GetAction(context); + + // Assert + Assert.NotNull(actionName); + Assert.Same(expectedAction, action); + Assert.NotNull(payload); + Assert.True(payload.ContainsKey("Quantity")); + Assert.Equal(1, payload["Quantity"]); + Assert.True(payload.ContainsKey("Address")); + MyAddress address = payload["Address"] as MyAddress; + Assert.NotNull(address); + Assert.Equal("1 Microsoft Way", address.StreetAddress); + Assert.Equal("Redmond", address.City); + Assert.Equal("WA", address.State); + Assert.Equal(98052, address.ZipCode); + } - [Theory] - [MemberData(nameof(DeserializeWithPrimitiveCollectionsTest))] - public async Task Can_DeserializePayload_WithPrimitiveCollections_InUntypedMode(string actionName, IEdmAction expectedAction, ODataPath path) - { - // Arrange - const string Body = - @"{ ""Name"": ""Avatar"", ""Ratings"": [ 5, 5, 3, 4, 5, 5, 4, 5, 5, 4 ], ""Time"": [""01:02:03.0040000"", ""12:13:14.1150000""]}"; - int[] expectedRatings = new int[] { 5, 5, 3, 4, 5, 5, 4, 5, 5, 4 }; - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); - message.SetHeader("Content-Type", "application/json"); - - ODataMessageReaderSettings settings = new ODataMessageReaderSettings(); - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, settings, _model); - ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; - - // Act - ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; - IEdmAction action = ODataActionPayloadDeserializer.GetAction(context); - - //Assert - Assert.NotNull(actionName); - Assert.Same(expectedAction, action); - Assert.NotNull(payload); - Assert.True(payload.ContainsKey("Name")); - Assert.Equal("Avatar", payload["Name"]); - Assert.True(payload.ContainsKey("Ratings")); - IEnumerable ratings = payload["Ratings"] as IEnumerable; - Assert.Equal(10, ratings.Count()); - Assert.True(expectedRatings.Zip(ratings, (expected, actual) => expected - actual).All(diff => diff == 0)); - - Assert.True(payload.ContainsKey("Time")); - IEnumerable times = payload["Time"] as IEnumerable; - Assert.Equal(2, times.Count()); - Assert.Equal(new[] { new TimeOfDay(1, 2, 3, 4), new TimeOfDay(12, 13, 14, 115) }, times.ToList()); - } + [Theory] + [MemberData(nameof(DeserializeWithPrimitiveCollectionsTest))] + public async Task Can_DeserializePayload_WithPrimitiveCollections_InUntypedMode(string actionName, IEdmAction expectedAction, ODataPath path) + { + // Arrange + const string Body = + @"{ ""Name"": ""Avatar"", ""Ratings"": [ 5, 5, 3, 4, 5, 5, 4, 5, 5, 4 ], ""Time"": [""01:02:03.0040000"", ""12:13:14.1150000""]}"; + int[] expectedRatings = new int[] { 5, 5, 3, 4, 5, 5, 4, 5, 5, 4 }; + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); + message.SetHeader("Content-Type", "application/json"); + + ODataMessageReaderSettings settings = new ODataMessageReaderSettings(); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, settings, _model); + ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; + + // Act + ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; + IEdmAction action = ODataActionPayloadDeserializer.GetAction(context); + + //Assert + Assert.NotNull(actionName); + Assert.Same(expectedAction, action); + Assert.NotNull(payload); + Assert.True(payload.ContainsKey("Name")); + Assert.Equal("Avatar", payload["Name"]); + Assert.True(payload.ContainsKey("Ratings")); + IEnumerable ratings = payload["Ratings"] as IEnumerable; + Assert.Equal(10, ratings.Count()); + Assert.True(expectedRatings.Zip(ratings, (expected, actual) => expected - actual).All(diff => diff == 0)); + + Assert.True(payload.ContainsKey("Time")); + IEnumerable times = payload["Time"] as IEnumerable; + Assert.Equal(2, times.Count()); + Assert.Equal(new[] { new TimeOfDay(1, 2, 3, 4), new TimeOfDay(12, 13, 14, 115) }, times.ToList()); + } - [Theory] - [MemberData(nameof(DeserializeWithComplexCollectionsTest))] - public async Task Can_DeserializePayload_WithComplexCollections_InUntypedMode(string actionName, IEdmAction expectedAction, ODataPath path) - { - // Arrange - const string Body = @"{ ""Name"": ""Avatar"" , ""Addresses"": [{ ""StreetAddress"":""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 }] }"; - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); - message.SetHeader("Content-Type", "application/json"); - - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); - ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; - - // Act - ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; - - // Assert - Assert.NotNull(actionName); - Assert.Same(expectedAction, payload.Action); - Assert.NotNull(payload); - Assert.True(payload.ContainsKey("Name")); - Assert.Equal("Avatar", payload["Name"]); - Assert.True(payload.ContainsKey("Addresses")); - IEnumerable addresses = payload["Addresses"] as EdmComplexObjectCollection; - dynamic address = addresses.SingleOrDefault(); - Assert.NotNull(address); - Assert.Equal("1 Microsoft Way", address.StreetAddress); - Assert.Equal("Redmond", address.City); - Assert.Equal("WA", address.State); - Assert.Equal(98052, address.ZipCode); - } + [Theory] + [MemberData(nameof(DeserializeWithComplexCollectionsTest))] + public async Task Can_DeserializePayload_WithComplexCollections_InUntypedMode(string actionName, IEdmAction expectedAction, ODataPath path) + { + // Arrange + const string Body = @"{ ""Name"": ""Avatar"" , ""Addresses"": [{ ""StreetAddress"":""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 }] }"; + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); + message.SetHeader("Content-Type", "application/json"); + + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); + ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; + + // Act + ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; + + // Assert + Assert.NotNull(actionName); + Assert.Same(expectedAction, payload.Action); + Assert.NotNull(payload); + Assert.True(payload.ContainsKey("Name")); + Assert.Equal("Avatar", payload["Name"]); + Assert.True(payload.ContainsKey("Addresses")); + IEnumerable addresses = payload["Addresses"] as EdmComplexObjectCollection; + dynamic address = addresses.SingleOrDefault(); + Assert.NotNull(address); + Assert.Equal("1 Microsoft Way", address.StreetAddress); + Assert.Equal("Redmond", address.City); + Assert.Equal("WA", address.State); + Assert.Equal(98052, address.ZipCode); + } - [Theory] - [MemberData(nameof(DeserializeWithEnumCollectionsTest))] - public async Task Can_DeserializePayload_WithEnumCollections_InUntypedMode(string actionName, IEdmAction expectedAction, ODataPath path) - { - // Arrange - const string Body = @"{ ""Colors"": [ ""Red"", ""Green""] }"; - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); - message.SetHeader("Content-Type", "application/json"); - - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); - ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; - - // Act - ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; - - // Assert - Assert.NotNull(actionName); - Assert.Same(expectedAction, payload.Action); - Assert.NotNull(payload); - Assert.True(payload.ContainsKey("Colors")); - EdmEnumObjectCollection colors = payload["Colors"] as EdmEnumObjectCollection; - EdmEnumObject color = colors[0] as EdmEnumObject; - Assert.NotNull(color); - Assert.Equal("Red", color.Value); - } + [Theory] + [MemberData(nameof(DeserializeWithEnumCollectionsTest))] + public async Task Can_DeserializePayload_WithEnumCollections_InUntypedMode(string actionName, IEdmAction expectedAction, ODataPath path) + { + // Arrange + const string Body = @"{ ""Colors"": [ ""Red"", ""Green""] }"; + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); + message.SetHeader("Content-Type", "application/json"); + + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); + ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; + + // Act + ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; + + // Assert + Assert.NotNull(actionName); + Assert.Same(expectedAction, payload.Action); + Assert.NotNull(payload); + Assert.True(payload.ContainsKey("Colors")); + EdmEnumObjectCollection colors = payload["Colors"] as EdmEnumObjectCollection; + EdmEnumObject color = colors[0] as EdmEnumObject; + Assert.NotNull(color); + Assert.Equal("Red", color.Value); + } - [Theory] - [MemberData(nameof(DeserializeWithComplexParametersTest))] - public async Task Can_DeserializePayload_WithComplexParameters_InUntypedMode(string actionName, IEdmAction expectedAction, ODataPath path) - { - // Arrange - const string Body = @"{ ""Quantity"": 1 , ""Address"": { ""StreetAddress"":""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 } }"; - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); - message.SetHeader("Content-Type", "application/json"); - - ODataMessageReaderSettings settings = new ODataMessageReaderSettings(); - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, settings, _model); - - ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; - - // Act - ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; - - // Assert - Assert.NotNull(actionName); - Assert.NotNull(payload); - Assert.Same(expectedAction, payload.Action); - Assert.True(payload.ContainsKey("Quantity")); - Assert.Equal(1, payload["Quantity"]); - Assert.True(payload.ContainsKey("Address")); - dynamic address = payload["Address"] as EdmComplexObject; - Assert.IsType(address); - Assert.Equal("1 Microsoft Way", address.StreetAddress); - Assert.Equal("Redmond", address.City); - Assert.Equal("WA", address.State); - Assert.Equal(98052, address.ZipCode); - } + [Theory] + [MemberData(nameof(DeserializeWithComplexParametersTest))] + public async Task Can_DeserializePayload_WithComplexParameters_InUntypedMode(string actionName, IEdmAction expectedAction, ODataPath path) + { + // Arrange + const string Body = @"{ ""Quantity"": 1 , ""Address"": { ""StreetAddress"":""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 } }"; + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); + message.SetHeader("Content-Type", "application/json"); + + ODataMessageReaderSettings settings = new ODataMessageReaderSettings(); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, settings, _model); + + ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; + + // Act + ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; + + // Assert + Assert.NotNull(actionName); + Assert.NotNull(payload); + Assert.Same(expectedAction, payload.Action); + Assert.True(payload.ContainsKey("Quantity")); + Assert.Equal(1, payload["Quantity"]); + Assert.True(payload.ContainsKey("Address")); + dynamic address = payload["Address"] as EdmComplexObject; + Assert.IsType(address); + Assert.Equal("1 Microsoft Way", address.StreetAddress); + Assert.Equal("Redmond", address.City); + Assert.Equal("WA", address.State); + Assert.Equal(98052, address.ZipCode); + } - [Theory] - [MemberData(nameof(DeserializeWithEnumParametersTest))] - public async Task Can_DeserializePayload_WithEnumParameters_InUntypedMode(string actionName, IEdmAction expectedAction, ODataPath path) - { - // Arrange - const string Body = @"{ ""Color"": ""Red""}"; - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); - message.SetHeader("Content-Type", "application/json"); - - ODataMessageReaderSettings settings = new ODataMessageReaderSettings(); - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, settings, _model); - - ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; - - // Act - ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; - - // Assert - Assert.NotNull(actionName); - Assert.NotNull(payload); - Assert.Same(expectedAction, payload.Action); - Assert.True(payload.ContainsKey("Color")); - EdmEnumObject color = payload["Color"] as EdmEnumObject; - Assert.IsType(color); - Assert.Equal("Red", color.Value); - } + [Theory] + [MemberData(nameof(DeserializeWithEnumParametersTest))] + public async Task Can_DeserializePayload_WithEnumParameters_InUntypedMode(string actionName, IEdmAction expectedAction, ODataPath path) + { + // Arrange + const string Body = @"{ ""Color"": ""Red""}"; + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); + message.SetHeader("Content-Type", "application/json"); + + ODataMessageReaderSettings settings = new ODataMessageReaderSettings(); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, settings, _model); + + ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; + + // Act + ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; + + // Assert + Assert.NotNull(actionName); + Assert.NotNull(payload); + Assert.Same(expectedAction, payload.Action); + Assert.True(payload.ContainsKey("Color")); + EdmEnumObject color = payload["Color"] as EdmEnumObject; + Assert.IsType(color); + Assert.Equal("Red", color.Value); + } - [Theory] - [MemberData(nameof(DeserializeWithPrimitiveCollectionsTest))] - public async Task Can_DeserializePayload_WithPrimitiveCollectionParameters(string actionName, IEdmAction expectedAction, ODataPath path) - { - // Arrange - const string Body = - @"{ ""Name"": ""Avatar"", ""Ratings"": [ 5, 5, 3, 4, 5, 5, 4, 5, 5, 4 ], ""Time"": [""01:02:03.0040000"", ""12:13:14.1150000""], ""Colors"": [ ""Red"", null, ""Green""] }"; - int[] expectedRatings = new int[] { 5, 5, 3, 4, 5, 5, 4, 5, 5, 4 }; - - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); - message.SetHeader("Content-Type", "application/json"); - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); - - ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model }; - - // Act - ODataActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context) as ODataActionParameters; - IEdmAction action = ODataActionPayloadDeserializer.GetAction(context); - - // Assert - Assert.NotNull(actionName); - Assert.NotNull(payload); - Assert.Same(expectedAction, action); - Assert.True(payload.ContainsKey("Name")); - Assert.Equal("Avatar", payload["Name"]); - Assert.True(payload.ContainsKey("Ratings")); - IEnumerable ratings = payload["Ratings"] as IEnumerable; - Assert.Equal(10, ratings.Count()); - Assert.True(expectedRatings.Zip(ratings, (expected, actual) => expected - actual).All(diff => diff == 0)); - - Assert.True(payload.ContainsKey("Time")); - IEnumerable times = payload["Time"] as IEnumerable; - Assert.Equal(2, times.Count()); - Assert.Equal(new[] { new TimeOfDay(1, 2, 3, 4), new TimeOfDay(12, 13, 14, 115) }, times.ToList()); - - Assert.True(payload.ContainsKey("Colors")); - IEnumerable colors = payload["Colors"] as IEnumerable; - Assert.Equal("Red|null|Green", String.Join("|", colors.Select(e => e == null ? "null" : e.ToString()))); - } + [Theory] + [MemberData(nameof(DeserializeWithPrimitiveCollectionsTest))] + public async Task Can_DeserializePayload_WithPrimitiveCollectionParameters(string actionName, IEdmAction expectedAction, ODataPath path) + { + // Arrange + const string Body = + @"{ ""Name"": ""Avatar"", ""Ratings"": [ 5, 5, 3, 4, 5, 5, 4, 5, 5, 4 ], ""Time"": [""01:02:03.0040000"", ""12:13:14.1150000""], ""Colors"": [ ""Red"", null, ""Green""] }"; + int[] expectedRatings = new int[] { 5, 5, 3, 4, 5, 5, 4, 5, 5, 4 }; + + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); + message.SetHeader("Content-Type", "application/json"); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); + + ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model }; + + // Act + ODataActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context) as ODataActionParameters; + IEdmAction action = ODataActionPayloadDeserializer.GetAction(context); + + // Assert + Assert.NotNull(actionName); + Assert.NotNull(payload); + Assert.Same(expectedAction, action); + Assert.True(payload.ContainsKey("Name")); + Assert.Equal("Avatar", payload["Name"]); + Assert.True(payload.ContainsKey("Ratings")); + IEnumerable ratings = payload["Ratings"] as IEnumerable; + Assert.Equal(10, ratings.Count()); + Assert.True(expectedRatings.Zip(ratings, (expected, actual) => expected - actual).All(diff => diff == 0)); + + Assert.True(payload.ContainsKey("Time")); + IEnumerable times = payload["Time"] as IEnumerable; + Assert.Equal(2, times.Count()); + Assert.Equal(new[] { new TimeOfDay(1, 2, 3, 4), new TimeOfDay(12, 13, 14, 115) }, times.ToList()); + + Assert.True(payload.ContainsKey("Colors")); + IEnumerable colors = payload["Colors"] as IEnumerable; + Assert.Equal("Red|null|Green", String.Join("|", colors.Select(e => e == null ? "null" : e.ToString()))); + } - [Theory] - [MemberData(nameof(DeserializeWithComplexCollectionsTest))] - public async Task Can_DeserializePayload_WithComplexCollectionParameters(string actionName, IEdmAction expectedAction, ODataPath path) - { - // Arrange - const string Body = @"{ ""Name"": ""Microsoft"", ""Addresses"": [ { ""StreetAddress"":""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 } ] }"; - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); - message.SetHeader("Content-Type", "application/json"); - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); - ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model }; - - // Act - ODataActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context) as ODataActionParameters; - - // Assert - Assert.NotNull(actionName); - Assert.NotNull(expectedAction); - Assert.NotNull(payload); - Assert.True(payload.ContainsKey("Name")); - Assert.Equal("Microsoft", payload["Name"]); - Assert.True(payload.ContainsKey("Addresses")); - IList addresses = (payload["Addresses"] as IEnumerable).ToList(); - Assert.NotNull(addresses); - Assert.Single(addresses); - MyAddress address = addresses[0]; - Assert.NotNull(address); - Assert.Equal("1 Microsoft Way", address.StreetAddress); - Assert.Equal("Redmond", address.City); - Assert.Equal("WA", address.State); - Assert.Equal(98052, address.ZipCode); - } + [Theory] + [MemberData(nameof(DeserializeWithComplexCollectionsTest))] + public async Task Can_DeserializePayload_WithComplexCollectionParameters(string actionName, IEdmAction expectedAction, ODataPath path) + { + // Arrange + const string Body = @"{ ""Name"": ""Microsoft"", ""Addresses"": [ { ""StreetAddress"":""1 Microsoft Way"", ""City"": ""Redmond"", ""State"": ""WA"", ""ZipCode"": 98052 } ] }"; + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(Body)); + message.SetHeader("Content-Type", "application/json"); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); + ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model }; + + // Act + ODataActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context) as ODataActionParameters; + + // Assert + Assert.NotNull(actionName); + Assert.NotNull(expectedAction); + Assert.NotNull(payload); + Assert.True(payload.ContainsKey("Name")); + Assert.Equal("Microsoft", payload["Name"]); + Assert.True(payload.ContainsKey("Addresses")); + IList addresses = (payload["Addresses"] as IEnumerable).ToList(); + Assert.NotNull(addresses); + Assert.Single(addresses); + MyAddress address = addresses[0]; + Assert.NotNull(address); + Assert.Equal("1 Microsoft Way", address.StreetAddress); + Assert.Equal("Redmond", address.City); + Assert.Equal("WA", address.State); + Assert.Equal(98052, address.ZipCode); + } - private const string EntityPayload = + private const string EntityPayload = "{" + - "\"Id\": 1, " + - "\"Customer\": {\"@odata.type\":\"#A.B.Customer\", \"Id\":109,\"Name\":\"Avatar\" } " + +"\"Id\": 1, " + +"\"Customer\": {\"@odata.type\":\"#A.B.Customer\", \"Id\":109,\"Name\":\"Avatar\" } " + // null can't work here, see: https://github.com/OData/odata.net/issues/99 // ",\"NullableCustomer\" : null " + // "}"; - [Theory] - [MemberData(nameof(DeserializeWithEntityParametersTest))] - public async Task Can_DeserializePayload_WithEntityParameters(IEdmAction expectedAction, ODataPath path) - { - // Arrange - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(EntityPayload)); - message.SetHeader("Content-Type", "application/json"); - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); - ODataDeserializerContext context = new ODataDeserializerContext() { Path = path, Model = _model }; - - // Act - ODataActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context) as ODataActionParameters; - IEdmAction action = ODataActionPayloadDeserializer.GetAction(context); - - // Assert - Assert.Same(expectedAction, action); - Assert.NotNull(payload); - Assert.True(payload.ContainsKey("Id")); - Assert.Equal(1, payload["Id"]); - - Assert.True(payload.ContainsKey("Customer")); - Customer customer = payload["Customer"] as Customer; - Assert.NotNull(customer); - Assert.Equal(109, customer.Id); - Assert.Equal("Avatar", customer.Name); - - Assert.False(payload.ContainsKey("NullableCustomer")); - } + [Theory] + [MemberData(nameof(DeserializeWithEntityParametersTest))] + public async Task Can_DeserializePayload_WithEntityParameters(IEdmAction expectedAction, ODataPath path) + { + // Arrange + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(EntityPayload)); + message.SetHeader("Content-Type", "application/json"); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); + ODataDeserializerContext context = new ODataDeserializerContext() { Path = path, Model = _model }; + + // Act + ODataActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context) as ODataActionParameters; + IEdmAction action = ODataActionPayloadDeserializer.GetAction(context); + + // Assert + Assert.Same(expectedAction, action); + Assert.NotNull(payload); + Assert.True(payload.ContainsKey("Id")); + Assert.Equal(1, payload["Id"]); + + Assert.True(payload.ContainsKey("Customer")); + Customer customer = payload["Customer"] as Customer; + Assert.NotNull(customer); + Assert.Equal(109, customer.Id); + Assert.Equal("Avatar", customer.Name); + + Assert.False(payload.ContainsKey("NullableCustomer")); + } - [Theory] - [MemberData(nameof(DeserializeWithEntityParametersTest))] - public async Task Can_DeserializePayload_WithEntityParameters_InUntypedMode(IEdmAction expectedAction, ODataPath path) - { - // Arrange - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(EntityPayload)); - message.SetHeader("Content-Type", "application/json"); - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); - ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; + [Theory] + [MemberData(nameof(DeserializeWithEntityParametersTest))] + public async Task Can_DeserializePayload_WithEntityParameters_InUntypedMode(IEdmAction expectedAction, ODataPath path) + { + // Arrange + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(EntityPayload)); + message.SetHeader("Content-Type", "application/json"); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); + ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; - // Act - ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; + // Act + ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; - // Assert - Assert.NotNull(payload); - Assert.Same(expectedAction, payload.Action); + // Assert + Assert.NotNull(payload); + Assert.Same(expectedAction, payload.Action); - Assert.True(payload.ContainsKey("Id")); - Assert.Equal(1, payload["Id"]); + Assert.True(payload.ContainsKey("Id")); + Assert.Equal(1, payload["Id"]); - Assert.True(payload.ContainsKey("Customer")); - dynamic customer = payload["Customer"] as EdmEntityObject; - Assert.IsType(customer); + Assert.True(payload.ContainsKey("Customer")); + dynamic customer = payload["Customer"] as EdmEntityObject; + Assert.IsType(customer); - Assert.Equal(109, customer.Id); - Assert.Equal("Avatar", customer.Name); + Assert.Equal(109, customer.Id); + Assert.Equal("Avatar", customer.Name); - Assert.False(payload.ContainsKey("NullableCustomer")); - } + Assert.False(payload.ContainsKey("NullableCustomer")); + } - private const string EntityCollectionPayload = + private const string EntityCollectionPayload = "{" + - "\"Id\": 1, " + - "\"Customers\": [" + - "{\"@odata.type\":\"#A.B.Customer\", \"Id\":109,\"Name\":\"Avatar\" }, " + - // null can't work. see: https://github.com/OData/odata.net/issues/100 - // "null," + - "{\"@odata.type\":\"#A.B.Customer\", \"Id\":901,\"Name\":\"Robot\" } " + - "]" + +"\"Id\": 1, " + +"\"Customers\": [" + + "{\"@odata.type\":\"#A.B.Customer\", \"Id\":109,\"Name\":\"Avatar\" }, " + + // null can't work. see: https://github.com/OData/odata.net/issues/100 + // "null," + + "{\"@odata.type\":\"#A.B.Customer\", \"Id\":901,\"Name\":\"Robot\" } " + + "]" + "}"; - [Theory] - [MemberData(nameof(DeserializeWithEntityCollectionsTest))] - public async Task Can_DeserializePayload_WithEntityCollectionParameters(IEdmAction expectedAction, ODataPath path) - { - // Arrange - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(EntityCollectionPayload)); - message.SetHeader("Content-Type", "application/json"); - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); - ODataDeserializerContext context = new ODataDeserializerContext() { Path = path, Model = _model }; - - // Act - ODataActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context) as ODataActionParameters; - IEdmAction action = ODataActionPayloadDeserializer.GetAction(context); - - // Assert - Assert.Same(expectedAction, action); - Assert.NotNull(payload); - Assert.True(payload.ContainsKey("Id")); - Assert.Equal(1, payload["Id"]); - - IList customers = (payload["Customers"] as IEnumerable).ToList(); - Assert.NotNull(customers); - Assert.Equal(2, customers.Count); - Customer customer = customers[0]; - Assert.NotNull(customer); - Assert.Equal(109, customer.Id); - Assert.Equal("Avatar", customer.Name); - - customer = customers[1]; - Assert.NotNull(customer); - Assert.Equal(901, customer.Id); - Assert.Equal("Robot", customer.Name); - } - - [Theory] - [MemberData(nameof(DeserializeWithEntityCollectionsTest))] - public async Task Can_DeserializePayload_WithEntityCollectionParameters_InUntypedMode(IEdmAction expectedAction, ODataPath path) - { - // Arrange - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(EntityCollectionPayload)); - message.SetHeader("Content-Type", "application/json"); - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); - ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; - - // Act - ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; - - // Assert - Assert.Same(expectedAction, payload.Action); - Assert.NotNull(payload); - Assert.True(payload.ContainsKey("Id")); - Assert.Equal(1, payload["Id"]); - - IEnumerable customers = payload["Customers"] as EdmEntityObjectCollection; - Assert.Equal(2, customers.Count()); - dynamic customer = customers.First(); - Assert.NotNull(customer); - Assert.Equal(109, customer.Id); - Assert.Equal("Avatar", customer.Name); - - customer = customers.Last(); - Assert.NotNull(customer); - Assert.Equal(901, customer.Id); - Assert.Equal("Robot", customer.Name); - } - - [Fact] - public void GetAction_ThrowsSerializationException_ForNonAction() - { - // Arrange - ODataPath path = new ODataPath(MetadataSegment.Instance); - ODataDeserializerContext context = new ODataDeserializerContext() { Path = path }; + [Theory] + [MemberData(nameof(DeserializeWithEntityCollectionsTest))] + public async Task Can_DeserializePayload_WithEntityCollectionParameters(IEdmAction expectedAction, ODataPath path) + { + // Arrange + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(EntityCollectionPayload)); + message.SetHeader("Content-Type", "application/json"); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); + ODataDeserializerContext context = new ODataDeserializerContext() { Path = path, Model = _model }; + + // Act + ODataActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context) as ODataActionParameters; + IEdmAction action = ODataActionPayloadDeserializer.GetAction(context); + + // Assert + Assert.Same(expectedAction, action); + Assert.NotNull(payload); + Assert.True(payload.ContainsKey("Id")); + Assert.Equal(1, payload["Id"]); + + IList customers = (payload["Customers"] as IEnumerable).ToList(); + Assert.NotNull(customers); + Assert.Equal(2, customers.Count); + Customer customer = customers[0]; + Assert.NotNull(customer); + Assert.Equal(109, customer.Id); + Assert.Equal("Avatar", customer.Name); + + customer = customers[1]; + Assert.NotNull(customer); + Assert.Equal(901, customer.Id); + Assert.Equal("Robot", customer.Name); + } - // Act & Assert - ExceptionAssert.Throws(() => ODataActionPayloadDeserializer.GetAction(context), - "The last segment of the request URI '$metadata' was not recognized as an OData action."); - } + [Theory] + [MemberData(nameof(DeserializeWithEntityCollectionsTest))] + public async Task Can_DeserializePayload_WithEntityCollectionParameters_InUntypedMode(IEdmAction expectedAction, ODataPath path) + { + // Arrange + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(EntityCollectionPayload)); + message.SetHeader("Content-Type", "application/json"); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); + ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model, ResourceType = typeof(ODataUntypedActionParameters) }; + + // Act + ODataUntypedActionParameters payload = await _deserializer.ReadAsync(reader, typeof(ODataUntypedActionParameters), context) as ODataUntypedActionParameters; + + // Assert + Assert.Same(expectedAction, payload.Action); + Assert.NotNull(payload); + Assert.True(payload.ContainsKey("Id")); + Assert.Equal(1, payload["Id"]); + + IEnumerable customers = payload["Customers"] as EdmEntityObjectCollection; + Assert.Equal(2, customers.Count()); + dynamic customer = customers.First(); + Assert.NotNull(customer); + Assert.Equal(109, customer.Id); + Assert.Equal("Avatar", customer.Name); + + customer = customers.Last(); + Assert.NotNull(customer); + Assert.Equal(901, customer.Id); + Assert.Equal("Robot", customer.Name); + } - [Theory] - [MemberData(nameof(DeserializeWithPrimitiveParametersTest))] - public void ThrowsAsync_ODataException_When_Parameter_Notfound(string actionName, IEdmAction expectedAction, ODataPath path) - { - // Arrange - const string Body = @"{ ""Quantity"": 1 , ""ProductCode"": ""PCode"", ""MissingParameter"": 1 }"; - IODataRequestMessageAsync message = new ODataMessageWrapper(GetStringAsStreamAsync(Body).Result); - message.SetHeader("Content-Type", "application/json"); - ODataMessageReader reader = new ODataMessageReader(message, new ODataMessageReaderSettings(), _model); - ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model }; - - // Act & Assert - Assert.NotNull(expectedAction); - ExceptionAssert.Throws(() => - { - ODataActionParameters payload = _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context).Result as ODataActionParameters; - }, "The parameter 'MissingParameter' in the request payload is not a valid parameter for the operation '" + actionName + "'."); - } + [Fact] + public void GetAction_ThrowsSerializationException_ForNonAction() + { + // Arrange + ODataPath path = new ODataPath(MetadataSegment.Instance); + ODataDeserializerContext context = new ODataDeserializerContext() { Path = path }; - private static ODataPath CreateBoundPath(string actionName) - { - string path = String.Format("Customers(1)/A.B.{0}", actionName); - ODataPath odataPath = new ODataUriParser(_model, new Uri(_serviceRoot), new Uri(path, UriKind.Relative)).ParsePath(); - Assert.NotNull(odataPath); // Guard - return odataPath; - } + // Act & Assert + ExceptionAssert.Throws(() => ODataActionPayloadDeserializer.GetAction(context), + "The last segment of the request URI '$metadata' was not recognized as an OData action."); + } - private static ODataPath CreateUnboundPath(string actionName) - { - string path = string.Format("{0}", actionName); - ODataPath odataPath = new ODataUriParser(_model, new Uri(_serviceRoot), new Uri(path, UriKind.Relative)).ParsePath(); - Assert.NotNull(odataPath); // Guard - return odataPath; - } + [Theory] + [MemberData(nameof(DeserializeWithPrimitiveParametersTest))] + public void ThrowsAsync_ODataException_When_Parameter_Notfound(string actionName, IEdmAction expectedAction, ODataPath path) + { + // Arrange + const string Body = @"{ ""Quantity"": 1 , ""ProductCode"": ""PCode"", ""MissingParameter"": 1 }"; + IODataRequestMessageAsync message = new ODataMessageWrapper(GetStringAsStreamAsync(Body).Result); + message.SetHeader("Content-Type", "application/json"); + ODataMessageReader reader = new ODataMessageReader(message, new ODataMessageReaderSettings(), _model); + ODataDeserializerContext context = new ODataDeserializerContext { Path = path, Model = _model }; + + // Act & Assert + Assert.NotNull(expectedAction); + ExceptionAssert.Throws(() => + { + ODataActionParameters payload = _deserializer.ReadAsync(reader, typeof(ODataActionParameters), context).Result as ODataActionParameters; + }, "The parameter 'MissingParameter' in the request payload is not a valid parameter for the operation '" + actionName + "'."); + } - public static IEdmAction GetBoundAction(string actionName) - { - IEdmAction action = _model.SchemaElements.OfType().SingleOrDefault(a => a.Name == actionName); - Assert.NotNull(action); // Guard - return action; - } + private static ODataPath CreateBoundPath(string actionName) + { + string path = String.Format("Customers(1)/A.B.{0}", actionName); + ODataPath odataPath = new ODataUriParser(_model, new Uri(_serviceRoot), new Uri(path, UriKind.Relative)).ParsePath(); + Assert.NotNull(odataPath); // Guard + return odataPath; + } - public static IEdmAction GetUnboundAction(string actionName) - { - IEdmActionImport actionImport = _container.OperationImports().SingleOrDefault(a => a.Name == actionName) as IEdmActionImport; - Assert.NotNull(actionImport); // Guard - return actionImport.Action; - } + private static ODataPath CreateUnboundPath(string actionName) + { + string path = string.Format("{0}", actionName); + ODataPath odataPath = new ODataUriParser(_model, new Uri(_serviceRoot), new Uri(path, UriKind.Relative)).ParsePath(); + Assert.NotNull(odataPath); // Guard + return odataPath; + } - private static IEdmModel GetModel() - { - //var config = RoutingConfigurationFactory.CreateWithTypes(typeof(Customer)); - ODataModelBuilder builder = new ODataConventionModelBuilder(); - builder.ContainerName = "C"; - builder.Namespace = "A.B"; - EntityTypeConfiguration customer = builder.EntitySet("Customers").EntityType; - - // bound actions - ActionConfiguration primitive = customer.Action("Primitive"); - primitive.Parameter("Quantity"); - primitive.Parameter("ProductCode"); - primitive.Parameter("Birthday"); - primitive.Parameter("BkgColor"); - primitive.Parameter("InnerColor"); - - ActionConfiguration complex = customer.Action("Complex"); - complex.Parameter("Quantity"); - complex.Parameter("Address"); - - ActionConfiguration enumType = customer.Action("Enum"); - enumType.Parameter("Color"); - - ActionConfiguration primitiveCollection = customer.Action("PrimitiveCollection"); - primitiveCollection.Parameter("Name"); - primitiveCollection.CollectionParameter("Ratings"); - primitiveCollection.CollectionParameter("Time"); - primitiveCollection.CollectionParameter("Colors"); - - ActionConfiguration complexCollection = customer.Action("ComplexCollection"); - complexCollection.Parameter("Name"); - complexCollection.CollectionParameter("Addresses"); - - ActionConfiguration enumCollection = customer.Action("EnumCollection"); - enumCollection.CollectionParameter("Colors"); - - ActionConfiguration entity = customer.Action("Entity"); - entity.Parameter("Id"); - entity.EntityParameter("Customer"); - entity.EntityParameter("NullableCustomer"); - - ActionConfiguration entityCollection = customer.Action("EntityCollection"); - entityCollection.Parameter("Id"); - entityCollection.CollectionEntityParameter("Customers"); - - // unbound actions - ActionConfiguration unboundPrimitive = builder.Action("UnboundPrimitive"); - unboundPrimitive.Parameter("Quantity"); - unboundPrimitive.Parameter("ProductCode"); - unboundPrimitive.Parameter("Birthday"); - unboundPrimitive.Parameter("BkgColor"); - unboundPrimitive.Parameter("InnerColor"); - - ActionConfiguration unboundComplex = builder.Action("UnboundComplex"); - unboundComplex.Parameter("Quantity"); - unboundComplex.Parameter("Address"); - - ActionConfiguration unboundEnum = builder.Action("UnboundEnum"); - unboundEnum.Parameter("Color"); - - ActionConfiguration unboundPrimitiveCollection = builder.Action("UnboundPrimitiveCollection"); - unboundPrimitiveCollection.Parameter("Name"); - unboundPrimitiveCollection.CollectionParameter("Ratings"); - unboundPrimitiveCollection.CollectionParameter("Time"); - unboundPrimitiveCollection.CollectionParameter("Colors"); - - ActionConfiguration unboundComplexCollection = builder.Action("UnboundComplexCollection"); - unboundComplexCollection.Parameter("Name"); - unboundComplexCollection.CollectionParameter("Addresses"); - - ActionConfiguration unboundEnumCollection = builder.Action("UnboundEnumCollection"); - unboundEnumCollection.CollectionParameter("Colors"); - - ActionConfiguration unboundEntity = builder.Action("UnboundEntity"); - unboundEntity.Parameter("Id"); - unboundEntity.EntityParameter("Customer").Nullable = false; - unboundEntity.EntityParameter("NullableCustomer"); - - ActionConfiguration unboundEntityCollection = builder.Action("UnboundEntityCollection"); - unboundEntityCollection.Parameter("Id"); - unboundEntityCollection.CollectionEntityParameter("Customers"); - - return builder.GetEdmModel(); - } + public static IEdmAction GetBoundAction(string actionName) + { + IEdmAction action = _model.SchemaElements.OfType().SingleOrDefault(a => a.Name == actionName); + Assert.NotNull(action); // Guard + return action; + } - private static async Task GetStringAsStreamAsync(string body) - { - Stream stream = new MemoryStream(); - StreamWriter writer = new StreamWriter(stream); - await writer.WriteAsync(body); - await writer.FlushAsync(); - stream.Seek(0, SeekOrigin.Begin); - return stream; - } + public static IEdmAction GetUnboundAction(string actionName) + { + IEdmActionImport actionImport = _container.OperationImports().SingleOrDefault(a => a.Name == actionName) as IEdmActionImport; + Assert.NotNull(actionImport); // Guard + return actionImport.Action; + } - private class DerivedODataActionParameters : ODataActionParameters - { - } + private static IEdmModel GetModel() + { + //var config = RoutingConfigurationFactory.CreateWithTypes(typeof(Customer)); + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.ContainerName = "C"; + builder.Namespace = "A.B"; + EntityTypeConfiguration customer = builder.EntitySet("Customers").EntityType; + + // bound actions + ActionConfiguration primitive = customer.Action("Primitive"); + primitive.Parameter("Quantity"); + primitive.Parameter("ProductCode"); + primitive.Parameter("Birthday"); + primitive.Parameter("BkgColor"); + primitive.Parameter("InnerColor"); + + ActionConfiguration complex = customer.Action("Complex"); + complex.Parameter("Quantity"); + complex.Parameter("Address"); + + ActionConfiguration enumType = customer.Action("Enum"); + enumType.Parameter("Color"); + + ActionConfiguration primitiveCollection = customer.Action("PrimitiveCollection"); + primitiveCollection.Parameter("Name"); + primitiveCollection.CollectionParameter("Ratings"); + primitiveCollection.CollectionParameter("Time"); + primitiveCollection.CollectionParameter("Colors"); + + ActionConfiguration complexCollection = customer.Action("ComplexCollection"); + complexCollection.Parameter("Name"); + complexCollection.CollectionParameter("Addresses"); + + ActionConfiguration enumCollection = customer.Action("EnumCollection"); + enumCollection.CollectionParameter("Colors"); + + ActionConfiguration entity = customer.Action("Entity"); + entity.Parameter("Id"); + entity.EntityParameter("Customer"); + entity.EntityParameter("NullableCustomer"); + + ActionConfiguration entityCollection = customer.Action("EntityCollection"); + entityCollection.Parameter("Id"); + entityCollection.CollectionEntityParameter("Customers"); + + // unbound actions + ActionConfiguration unboundPrimitive = builder.Action("UnboundPrimitive"); + unboundPrimitive.Parameter("Quantity"); + unboundPrimitive.Parameter("ProductCode"); + unboundPrimitive.Parameter("Birthday"); + unboundPrimitive.Parameter("BkgColor"); + unboundPrimitive.Parameter("InnerColor"); + + ActionConfiguration unboundComplex = builder.Action("UnboundComplex"); + unboundComplex.Parameter("Quantity"); + unboundComplex.Parameter("Address"); + + ActionConfiguration unboundEnum = builder.Action("UnboundEnum"); + unboundEnum.Parameter("Color"); + + ActionConfiguration unboundPrimitiveCollection = builder.Action("UnboundPrimitiveCollection"); + unboundPrimitiveCollection.Parameter("Name"); + unboundPrimitiveCollection.CollectionParameter("Ratings"); + unboundPrimitiveCollection.CollectionParameter("Time"); + unboundPrimitiveCollection.CollectionParameter("Colors"); + + ActionConfiguration unboundComplexCollection = builder.Action("UnboundComplexCollection"); + unboundComplexCollection.Parameter("Name"); + unboundComplexCollection.CollectionParameter("Addresses"); + + ActionConfiguration unboundEnumCollection = builder.Action("UnboundEnumCollection"); + unboundEnumCollection.CollectionParameter("Colors"); + + ActionConfiguration unboundEntity = builder.Action("UnboundEntity"); + unboundEntity.Parameter("Id"); + unboundEntity.EntityParameter("Customer").Nullable = false; + unboundEntity.EntityParameter("NullableCustomer"); + + ActionConfiguration unboundEntityCollection = builder.Action("UnboundEntityCollection"); + unboundEntityCollection.Parameter("Id"); + unboundEntityCollection.CollectionEntityParameter("Customers"); + + return builder.GetEdmModel(); } - public enum AColor + private static async Task GetStringAsStreamAsync(string body) { - Red, - Blue, - Green + Stream stream = new MemoryStream(); + StreamWriter writer = new StreamWriter(stream); + await writer.WriteAsync(body); + await writer.FlushAsync(); + stream.Seek(0, SeekOrigin.Begin); + return stream; } - public class MyAddress + private class DerivedODataActionParameters : ODataActionParameters { - public string StreetAddress { get; set; } - public string City { get; set; } - public string State { get; set; } - public int ZipCode { get; set; } } } + +public enum AColor +{ + Red, + Blue, + Green +} + +public class MyAddress +{ + public string StreetAddress { get; set; } + public string City { get; set; } + public string State { get; set; } + public int ZipCode { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataCollectionDeserializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataCollectionDeserializerTests.cs index 3a6c11c84..05c65ca12 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataCollectionDeserializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataCollectionDeserializerTests.cs @@ -22,263 +22,262 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class ODataCollectionDeserializerTests { - public class ODataCollectionDeserializerTests + private static readonly IEdmModel Model = GetEdmModel(); + + // private static readonly ODataSerializerProvider SerializerProvider = ODataSerializerProviderFactory.Create(); + private static readonly IODataSerializerProvider SerializerProvider = ODataFormatterHelpers.GetSerializerProvider(); // TODO: + + // private static readonly ODataDeserializerProvider DeserializerProvider = ODataDeserializerProviderFactory.Create(); + private static readonly IODataDeserializerProvider DeserializerProvider = ODataFormatterHelpers.GetDeserializerProvider(); // TODO: + + private static readonly IEdmEnumTypeReference ColorType = + new EdmEnumTypeReference(Model.SchemaElements.OfType().First(c => c.Name == "Color"), + isNullable: true); + + private static readonly IEdmCollectionTypeReference ColorCollectionType = new EdmCollectionTypeReference(new EdmCollectionType((ColorType))); + + private static readonly IEdmCollectionTypeReference IntCollectionType = + new EdmCollectionTypeReference(new EdmCollectionType(Model.GetEdmTypeReference(typeof(int)))); + + [Fact] + public void Ctor_ThrowsArgumentNull_DeserializerProvider() { - private static readonly IEdmModel Model = GetEdmModel(); + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataCollectionDeserializer(deserializerProvider: null), + "deserializerProvider"); + } - // private static readonly ODataSerializerProvider SerializerProvider = ODataSerializerProviderFactory.Create(); - private static readonly IODataSerializerProvider SerializerProvider = ODataFormatterHelpers.GetSerializerProvider(); // TODO: + [Fact] + public async Task Read_ThrowsArgumentNull_MessageReader() + { + // Arrange + var deserializer = new ODataCollectionDeserializer(DeserializerProvider); - // private static readonly ODataDeserializerProvider DeserializerProvider = ODataDeserializerProviderFactory.Create(); - private static readonly IODataDeserializerProvider DeserializerProvider = ODataFormatterHelpers.GetDeserializerProvider(); // TODO: + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => deserializer.ReadAsync(messageReader: null, type: typeof(int[]), readContext: new ODataDeserializerContext()), + "messageReader"); + } - private static readonly IEdmEnumTypeReference ColorType = - new EdmEnumTypeReference(Model.SchemaElements.OfType().First(c => c.Name == "Color"), - isNullable: true); + [Fact] + public void ReadAsync_ThrowsArgumentMustBeOfType_Type() + { + // Arrange + var deserializer = new ODataCollectionDeserializer(DeserializerProvider); - private static readonly IEdmCollectionTypeReference ColorCollectionType = new EdmCollectionTypeReference(new EdmCollectionType((ColorType))); + // Act & Assert + ExceptionAssert.ThrowsArgument(() => deserializer.ReadAsync(messageReader: ODataTestUtil.GetMockODataMessageReader(), + type: typeof(int), readContext: new ODataDeserializerContext { Model = Model }).Wait(), + "type", "The argument must be of type 'Collection'."); + } - private static readonly IEdmCollectionTypeReference IntCollectionType = - new EdmCollectionTypeReference(new EdmCollectionType(Model.GetEdmTypeReference(typeof(int)))); + [Fact] + public void ReadInline_ThrowsArgument_ArgumentMustBeOfType() + { + // Arrange + var deserializer = new ODataCollectionDeserializer(DeserializerProvider); - [Fact] - public void Ctor_ThrowsArgumentNull_DeserializerProvider() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataCollectionDeserializer(deserializerProvider: null), - "deserializerProvider"); - } + // Act & Assert + ExceptionAssert.Throws( + () => deserializer.ReadInline(42, IntCollectionType, new ODataDeserializerContext()), + "The argument must be of type 'ODataCollectionValue'. (Parameter 'item')"); + } - [Fact] - public async Task Read_ThrowsArgumentNull_MessageReader() - { - // Arrange - var deserializer = new ODataCollectionDeserializer(DeserializerProvider); + [Fact] + public void ReadInline_ThrowsArgumentNull_EdmType() + { + // Arrange + var deserializer = new ODataCollectionDeserializer(DeserializerProvider); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => deserializer.ReadAsync(messageReader: null, type: typeof(int[]), readContext: new ODataDeserializerContext()), - "messageReader"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ReadInline(42, null, new ODataDeserializerContext()), + "edmType"); + } - [Fact] - public void ReadAsync_ThrowsArgumentMustBeOfType_Type() - { - // Arrange - var deserializer = new ODataCollectionDeserializer(DeserializerProvider); + [Fact] + public void ReadInline_ThrowsArgumentNull_ReadContext() + { + // Arrange + var deserializer = new ODataCollectionDeserializer(DeserializerProvider); - // Act & Assert - ExceptionAssert.ThrowsArgument(() => deserializer.ReadAsync(messageReader: ODataTestUtil.GetMockODataMessageReader(), - type: typeof(int), readContext: new ODataDeserializerContext { Model = Model }).Wait(), - "type", "The argument must be of type 'Collection'."); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadInline(42, IntCollectionType, null), "readContext"); + } - [Fact] - public void ReadInline_ThrowsArgument_ArgumentMustBeOfType() - { - // Arrange - var deserializer = new ODataCollectionDeserializer(DeserializerProvider); + [Fact] + public void ReadInline_ThrowsSerializationException_IfNonCollectionType() + { + // Arrange + var deserializer = new ODataCollectionDeserializer(DeserializerProvider); + IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); - // Act & Assert - ExceptionAssert.Throws( - () => deserializer.ReadInline(42, IntCollectionType, new ODataDeserializerContext()), - "The argument must be of type 'ODataCollectionValue'. (Parameter 'item')"); - } + // Act & Assert + ExceptionAssert.Throws(() => deserializer.ReadInline(42, intType, new ODataDeserializerContext()), + "'[Edm.Int32 Nullable=False]' cannot be deserialized using the OData input formatter."); + } - [Fact] - public void ReadInline_ThrowsArgumentNull_EdmType() - { - // Arrange - var deserializer = new ODataCollectionDeserializer(DeserializerProvider); + [Fact] + public void ReadInline_ReturnsNull_IfItemIsNull() + { + // Arrange + var deserializer = new ODataCollectionDeserializer(DeserializerProvider); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.ReadInline(42, null, new ODataDeserializerContext()), - "edmType"); - } + // Act & Assert + Assert.Null(deserializer.ReadInline(item: null, edmType: IntCollectionType, readContext: new ODataDeserializerContext())); + } - [Fact] - public void ReadInline_ThrowsArgumentNull_ReadContext() - { - // Arrange - var deserializer = new ODataCollectionDeserializer(DeserializerProvider); + [Fact] + public void ReadInline_Calls_ReadCollectionValue() + { + // Arrange + Mock deserializer = new Mock(DeserializerProvider); + ODataCollectionValue collectionValue = new ODataCollectionValue(); + ODataDeserializerContext readContext = new ODataDeserializerContext(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadInline(42, IntCollectionType, null), "readContext"); - } + deserializer.CallBase = true; + deserializer.Setup(s => s.ReadCollectionValue(collectionValue, IntCollectionType.ElementType(), readContext)).Verifiable(); - [Fact] - public void ReadInline_ThrowsSerializationException_IfNonCollectionType() - { - // Arrange - var deserializer = new ODataCollectionDeserializer(DeserializerProvider); - IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); + // Act + deserializer.Object.ReadInline(collectionValue, IntCollectionType, readContext); - // Act & Assert - ExceptionAssert.Throws(() => deserializer.ReadInline(42, intType, new ODataDeserializerContext()), - "'[Edm.Int32 Nullable=False]' cannot be deserialized using the OData input formatter."); - } + // Assert + deserializer.Verify(); + } - [Fact] - public void ReadInline_ReturnsNull_IfItemIsNull() - { - // Arrange - var deserializer = new ODataCollectionDeserializer(DeserializerProvider); + [Fact] + public void ReadInline_ReturnsNull_ReadCollectionValueReturnsNull() + { + // Arrange + Mock deserializer = new Mock(DeserializerProvider); + ODataCollectionValue collectionValue = new ODataCollectionValue(); + ODataDeserializerContext readContext = new ODataDeserializerContext(); - // Act & Assert - Assert.Null(deserializer.ReadInline(item: null, edmType: IntCollectionType, readContext: new ODataDeserializerContext())); - } + deserializer.CallBase = true; + deserializer.Setup(s => s.ReadCollectionValue(collectionValue, IntCollectionType.ElementType(), readContext)).Returns((IEnumerable)null); - [Fact] - public void ReadInline_Calls_ReadCollectionValue() - { - // Arrange - Mock deserializer = new Mock(DeserializerProvider); - ODataCollectionValue collectionValue = new ODataCollectionValue(); - ODataDeserializerContext readContext = new ODataDeserializerContext(); + // Act + object actual = deserializer.Object.ReadInline(collectionValue, IntCollectionType, readContext); - deserializer.CallBase = true; - deserializer.Setup(s => s.ReadCollectionValue(collectionValue, IntCollectionType.ElementType(), readContext)).Verifiable(); + // Assert + Assert.Null(actual); + } - // Act - deserializer.Object.ReadInline(collectionValue, IntCollectionType, readContext); + [Fact] + public void ReadCollectionValue_ThrowsArgumentNull_CollectionValue() + { + // Arrange + var deserializer = new ODataCollectionDeserializer(DeserializerProvider); - // Assert - deserializer.Verify(); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadCollectionValue(collectionValue: null, + elementType: IntCollectionType.ElementType(), readContext: new ODataDeserializerContext()).GetEnumerator().MoveNext(), + "collectionValue"); + } - [Fact] - public void ReadInline_ReturnsNull_ReadCollectionValueReturnsNull() - { - // Arrange - Mock deserializer = new Mock(DeserializerProvider); - ODataCollectionValue collectionValue = new ODataCollectionValue(); - ODataDeserializerContext readContext = new ODataDeserializerContext(); + [Fact] + public void ReadCollectionValue_ThrowsArgumentNull_ElementType() + { + // Arrange + var deserializer = new ODataCollectionDeserializer(DeserializerProvider); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ReadCollectionValue(new ODataCollectionValue(), elementType: null, + readContext: new ODataDeserializerContext()).GetEnumerator().MoveNext(), + "elementType"); + } - deserializer.CallBase = true; - deserializer.Setup(s => s.ReadCollectionValue(collectionValue, IntCollectionType.ElementType(), readContext)).Returns((IEnumerable)null); + [Fact] + public void ReadCollectionValue_Throws_IfElementTypeCannotBeDeserialized() + { + // Arrange + Mock deserializerProvider = new Mock(); + deserializerProvider.Setup(p => p.GetEdmTypeDeserializer(ColorType, false)).Returns(null); + var deserializer = new ODataCollectionDeserializer(deserializerProvider.Object); + + // Act & Assert + ExceptionAssert.Throws( + () => deserializer.ReadCollectionValue(new ODataCollectionValue() { Items = new[] { 1, 2, 3 }.Cast() }, + ColorCollectionType.ElementType(), new ODataDeserializerContext()) + .GetEnumerator() + .MoveNext(), + "'NS.Color' cannot be deserialized using the OData input formatter."); + } - // Act - object actual = deserializer.Object.ReadInline(collectionValue, IntCollectionType, readContext); + [Fact] + public async Task Read_Roundtrip_PrimitiveCollection() + { + // Arrange + int[] numbers = Enumerable.Range(0, 100).ToArray(); - // Assert - Assert.Null(actual); - } + ODataCollectionSerializer serializer = new ODataCollectionSerializer(SerializerProvider); + ODataCollectionDeserializer deserializer = new ODataCollectionDeserializer(DeserializerProvider); - [Fact] - public void ReadCollectionValue_ThrowsArgumentNull_CollectionValue() + MemoryStream stream = new MemoryStream(); + ODataMessageWrapper message = new ODataMessageWrapper(stream); + ODataMessageWriterSettings settings = new ODataMessageWriterSettings { - // Arrange - var deserializer = new ODataCollectionDeserializer(DeserializerProvider); + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } + }; + ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, Model); + ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), Model); + ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = Model }; + ODataDeserializerContext readContext = new ODataDeserializerContext() { Model = Model }; + + // Act + await serializer.WriteObjectAsync(numbers, numbers.GetType(), messageWriter, writeContext); + stream.Seek(0, SeekOrigin.Begin); + IEnumerable readnumbers = await deserializer.ReadAsync(messageReader, typeof(int[]), readContext) as IEnumerable; + + // Assert + Assert.Equal(numbers, readnumbers.Cast()); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadCollectionValue(collectionValue: null, - elementType: IntCollectionType.ElementType(), readContext: new ODataDeserializerContext()).GetEnumerator().MoveNext(), - "collectionValue"); - } + [Fact] + public async Task Read_Roundtrip_EnumCollection() + { + // Arrange + Color[] colors = { Color.Blue, Color.Green }; - [Fact] - public void ReadCollectionValue_ThrowsArgumentNull_ElementType() - { - // Arrange - var deserializer = new ODataCollectionDeserializer(DeserializerProvider); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.ReadCollectionValue(new ODataCollectionValue(), elementType: null, - readContext: new ODataDeserializerContext()).GetEnumerator().MoveNext(), - "elementType"); - } - - [Fact] - public void ReadCollectionValue_Throws_IfElementTypeCannotBeDeserialized() - { - // Arrange - Mock deserializerProvider = new Mock(); - deserializerProvider.Setup(p => p.GetEdmTypeDeserializer(ColorType, false)).Returns(null); - var deserializer = new ODataCollectionDeserializer(deserializerProvider.Object); - - // Act & Assert - ExceptionAssert.Throws( - () => deserializer.ReadCollectionValue(new ODataCollectionValue() { Items = new[] { 1, 2, 3 }.Cast() }, - ColorCollectionType.ElementType(), new ODataDeserializerContext()) - .GetEnumerator() - .MoveNext(), - "'NS.Color' cannot be deserialized using the OData input formatter."); - } - - [Fact] - public async Task Read_Roundtrip_PrimitiveCollection() - { - // Arrange - int[] numbers = Enumerable.Range(0, 100).ToArray(); - - ODataCollectionSerializer serializer = new ODataCollectionSerializer(SerializerProvider); - ODataCollectionDeserializer deserializer = new ODataCollectionDeserializer(DeserializerProvider); - - MemoryStream stream = new MemoryStream(); - ODataMessageWrapper message = new ODataMessageWrapper(stream); - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } - }; - ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, Model); - ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), Model); - ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = Model }; - ODataDeserializerContext readContext = new ODataDeserializerContext() { Model = Model }; - - // Act - await serializer.WriteObjectAsync(numbers, numbers.GetType(), messageWriter, writeContext); - stream.Seek(0, SeekOrigin.Begin); - IEnumerable readnumbers = await deserializer.ReadAsync(messageReader, typeof(int[]), readContext) as IEnumerable; - - // Assert - Assert.Equal(numbers, readnumbers.Cast()); - } - - [Fact] - public async Task Read_Roundtrip_EnumCollection() - { - // Arrange - Color[] colors = { Color.Blue, Color.Green }; - - ODataCollectionSerializer serializer = new ODataCollectionSerializer(SerializerProvider); - ODataCollectionDeserializer deserializer = new ODataCollectionDeserializer(DeserializerProvider); - - MemoryStream stream = new MemoryStream(); - ODataMessageWrapper message = new ODataMessageWrapper(stream); - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } - }; - ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, Model); - ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), Model); - ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = Model }; - ODataDeserializerContext readContext = new ODataDeserializerContext() { Model = Model }; - - // Act - await serializer.WriteObjectAsync(colors, colors.GetType(), messageWriter, writeContext); - stream.Seek(0, SeekOrigin.Begin); - IEnumerable readAddresses = await deserializer.ReadAsync(messageReader, typeof(Color[]), readContext) as IEnumerable; - - // Assert - Assert.Equal(colors, readAddresses.Cast()); - } - - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EnumType().Namespace = "NS"; - return builder.GetEdmModel(); - } + ODataCollectionSerializer serializer = new ODataCollectionSerializer(SerializerProvider); + ODataCollectionDeserializer deserializer = new ODataCollectionDeserializer(DeserializerProvider); - public enum Color + MemoryStream stream = new MemoryStream(); + ODataMessageWrapper message = new ODataMessageWrapper(stream); + ODataMessageWriterSettings settings = new ODataMessageWriterSettings { - Red, - Blue, - Green - } + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } + }; + ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, Model); + ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), Model); + ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = Model }; + ODataDeserializerContext readContext = new ODataDeserializerContext() { Model = Model }; + + // Act + await serializer.WriteObjectAsync(colors, colors.GetType(), messageWriter, writeContext); + stream.Seek(0, SeekOrigin.Begin); + IEnumerable readAddresses = await deserializer.ReadAsync(messageReader, typeof(Color[]), readContext) as IEnumerable; + + // Assert + Assert.Equal(colors, readAddresses.Cast()); + } + + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EnumType().Namespace = "NS"; + return builder.GetEdmModel(); + } + + public enum Color + { + Red, + Blue, + Green } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeltaResourceSetDeserializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeltaResourceSetDeserializerTests.cs index 0ac59815c..2df73f09c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeltaResourceSetDeserializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeltaResourceSetDeserializerTests.cs @@ -26,499 +26,498 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class ODataDeltaResourceSetDeserializerTests { - public class ODataDeltaResourceSetDeserializerTests + private static IEdmModel _model = GetEdmModel(); + + [Fact] + public async Task ReadAsync_ThrowsArgumentNull_MessageReader() { - private static IEdmModel _model = GetEdmModel(); + // Arrange & Act & Assert + Mock deserializerProvider = new Mock(); + ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); - [Fact] - public async Task ReadAsync_ThrowsArgumentNull_MessageReader() - { - // Arrange & Act & Assert - Mock deserializerProvider = new Mock(); - ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); + await ExceptionAssert.ThrowsArgumentNullAsync(() => deserializer.ReadAsync(null, null, null), "messageReader"); + } - await ExceptionAssert.ThrowsArgumentNullAsync(() => deserializer.ReadAsync(null, null, null), "messageReader"); - } + [Fact] + public async Task ReadAsync_ThrowsArgumentNull_ReadContext() + { + // Arrange & Act & Assert + Mock deserializerProvider = new Mock(); + ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); + ODataMessageReader reader = new ODataMessageReader((IODataResponseMessage)new InMemoryMessage(), new ODataMessageReaderSettings()); - [Fact] - public async Task ReadAsync_ThrowsArgumentNull_ReadContext() - { - // Arrange & Act & Assert - Mock deserializerProvider = new Mock(); - ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); - ODataMessageReader reader = new ODataMessageReader((IODataResponseMessage)new InMemoryMessage(), new ODataMessageReaderSettings()); + await ExceptionAssert.ThrowsArgumentNullAsync(() => deserializer.ReadAsync(reader, typeof(DeltaSet<>), null), "readContext"); + } - await ExceptionAssert.ThrowsArgumentNullAsync(() => deserializer.ReadAsync(reader, typeof(DeltaSet<>), null), "readContext"); - } + [Fact] + public async Task ReadAsync_Calls_ReadInline() + { + // Arrange + string body = "{\"@context\":\"http://example.com/$metadata#Customers/$delta\"," + + "\"value\":[" + + "{" + + "\"@removed\":{\"reason\":\"changed\"}," + + "\"ID\":1" + + "}" + + "]" + + "}"; + + ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(body)); + message.SetHeader("Content-Type", "application/json"); + ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); + IEdmEntitySet entitySet = _model.EntityContainer.FindEntitySet("Customers"); + ODataPath path = new ODataPath(new EntitySetSegment(entitySet)); + ODataDeserializerContext readerContext = new ODataDeserializerContext() { Path = path, Model = _model }; + + Mock deserializerProvider = new Mock(); + Mock deserializer = new Mock(deserializerProvider.Object); + + // Arrange & Act & Assert + deserializer.CallBase = true; + deserializer + .Setup(s => s.ReadInline(It.IsAny(), It.IsAny(), readerContext)) + .Returns((object)null) + .Verifiable(); + + object actual = await deserializer.Object.ReadAsync(reader, typeof(DeltaSet), readerContext); + + // Arrange & Act & Assert + deserializer.Verify(); + } - [Fact] - public async Task ReadAsync_Calls_ReadInline() - { - // Arrange - string body = "{\"@context\":\"http://example.com/$metadata#Customers/$delta\"," + - "\"value\":[" + - "{" + - "\"@removed\":{\"reason\":\"changed\"}," + - "\"ID\":1" + - "}" + - "]" + - "}"; - - ODataMessageWrapper message = new ODataMessageWrapper(await GetStringAsStreamAsync(body)); - message.SetHeader("Content-Type", "application/json"); - ODataMessageReader reader = new ODataMessageReader(message as IODataRequestMessage, new ODataMessageReaderSettings(), _model); - IEdmEntitySet entitySet = _model.EntityContainer.FindEntitySet("Customers"); - ODataPath path = new ODataPath(new EntitySetSegment(entitySet)); - ODataDeserializerContext readerContext = new ODataDeserializerContext() { Path = path, Model = _model }; - - Mock deserializerProvider = new Mock(); - Mock deserializer = new Mock(deserializerProvider.Object); - - // Arrange & Act & Assert - deserializer.CallBase = true; - deserializer - .Setup(s => s.ReadInline(It.IsAny(), It.IsAny(), readerContext)) - .Returns((object)null) - .Verifiable(); - - object actual = await deserializer.Object.ReadAsync(reader, typeof(DeltaSet), readerContext); - - // Arrange & Act & Assert - deserializer.Verify(); - } + private static async Task GetStringAsStreamAsync(string body) + { + Stream stream = new MemoryStream(); + StreamWriter writer = new StreamWriter(stream); + await writer.WriteAsync(body); + await writer.FlushAsync(); + stream.Seek(0, SeekOrigin.Begin); + return stream; + } - private static async Task GetStringAsStreamAsync(string body) - { - Stream stream = new MemoryStream(); - StreamWriter writer = new StreamWriter(stream); - await writer.WriteAsync(body); - await writer.FlushAsync(); - stream.Seek(0, SeekOrigin.Begin); - return stream; - } + [Fact] + public void ReadInline_ThrowsArgumentNull_ForInputParameters() + { + // Arrange + Mock deserializerProvider = new Mock(); + ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); + + // Arrange & Act & Assert + Assert.Null(deserializer.ReadInline(null, null, null)); + + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadInline(5, null, null), "edmType"); + + // Arrange & Act & Assert + IEdmTypeReference typeReference = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadInline(5, typeReference, null), "readContext"); + + // Arrange & Act & Assert + ODataDeserializerContext context = new ODataDeserializerContext(); + IEdmPrimitiveTypeReference intType = EdmCoreModel.Instance.GetString(false); + IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(intType)); + ExceptionAssert.ThrowsArgument(() => deserializer.ReadInline(4, collectionType, context), + "edmType", + "'Collection(Edm.String)' is not a resource set type. Only resource set are supported."); + + // Arrange & Act & Assert + EdmEntityType entityType = new EdmEntityType("NS", "Customer"); + collectionType = new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(entityType, false))); + ExceptionAssert.ThrowsArgument(() => deserializer.ReadInline(4, collectionType, context), + "item", + "The argument must be of type 'ODataDeltaResourceSetWrapper'."); + } - [Fact] - public void ReadInline_ThrowsArgumentNull_ForInputParameters() - { - // Arrange - Mock deserializerProvider = new Mock(); - ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); - - // Arrange & Act & Assert - Assert.Null(deserializer.ReadInline(null, null, null)); - - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadInline(5, null, null), "edmType"); - - // Arrange & Act & Assert - IEdmTypeReference typeReference = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadInline(5, typeReference, null), "readContext"); - - // Arrange & Act & Assert - ODataDeserializerContext context = new ODataDeserializerContext(); - IEdmPrimitiveTypeReference intType = EdmCoreModel.Instance.GetString(false); - IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(intType)); - ExceptionAssert.ThrowsArgument(() => deserializer.ReadInline(4, collectionType, context), - "edmType", - "'Collection(Edm.String)' is not a resource set type. Only resource set are supported."); - - // Arrange & Act & Assert - EdmEntityType entityType = new EdmEntityType("NS", "Customer"); - collectionType = new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(entityType, false))); - ExceptionAssert.ThrowsArgument(() => deserializer.ReadInline(4, collectionType, context), - "item", - "The argument must be of type 'ODataDeltaResourceSetWrapper'."); - } + [Fact] + public void ReadInline_Calls_ReadDeltaResourceSet() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "Customer"); + IEdmEntityTypeReference entityTypeRef = new EdmEntityTypeReference(entityType, false); + IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(entityTypeRef)); - [Fact] - public void ReadInline_Calls_ReadDeltaResourceSet() - { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "Customer"); - IEdmEntityTypeReference entityTypeRef = new EdmEntityTypeReference(entityType, false); - IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(entityTypeRef)); + Mock deserializerProvider = new Mock(); + Mock deserializer = new Mock(deserializerProvider.Object); - Mock deserializerProvider = new Mock(); - Mock deserializer = new Mock(deserializerProvider.Object); + ODataDeltaResourceSetWrapper wrapper = new ODataDeltaResourceSetWrapper(new ODataDeltaResourceSet()); + ODataDeserializerContext readContext = new ODataDeserializerContext(); - ODataDeltaResourceSetWrapper wrapper = new ODataDeltaResourceSetWrapper(new ODataDeltaResourceSet()); - ODataDeserializerContext readContext = new ODataDeserializerContext(); + deserializer.CallBase = true; + deserializer.Setup(d => d.ReadDeltaResourceSet(wrapper, It.IsAny(), readContext)).Returns((IEnumerable)null).Verifiable(); - deserializer.CallBase = true; - deserializer.Setup(d => d.ReadDeltaResourceSet(wrapper, It.IsAny(), readContext)).Returns((IEnumerable)null).Verifiable(); + // Act + var result = deserializer.Object.ReadInline(wrapper, collectionType, readContext); - // Act - var result = deserializer.Object.ReadInline(wrapper, collectionType, readContext); + // Assert + deserializer.Verify(); + Assert.Null(result); + } - // Assert - deserializer.Verify(); - Assert.Null(result); - } + [Fact] + public void ReadDeltaResourceSet_ThrowsArgumentNull_Inputs() + { + // Arrange & Act & Assert + Mock deserializerProvider = new Mock(); + ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); - [Fact] - public void ReadDeltaResourceSet_ThrowsArgumentNull_Inputs() + ExceptionAssert.ThrowsArgumentNull(() => { - // Arrange & Act & Assert - Mock deserializerProvider = new Mock(); - ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); - - ExceptionAssert.ThrowsArgumentNull(() => - { - foreach (var item in deserializer.ReadDeltaResourceSet(null, null, null)) - ; - }, "deltaResourceSet"); - - // Arrange & Act & Assert - ODataDeltaResourceSetWrapper wrapper = new ODataDeltaResourceSetWrapper(null); - Mock elementType = new Mock(); - ExceptionAssert.ThrowsArgumentNull(() => - { - foreach (var item in deserializer.ReadDeltaResourceSet(wrapper, elementType.Object, null)) - ; - }, "readContext"); - } - - [Fact] - public void ReadDeltaResourceSet_Calls_ReadDeltaLinkItems() + foreach (var item in deserializer.ReadDeltaResourceSet(null, null, null)) + ; + }, "deltaResourceSet"); + + // Arrange & Act & Assert + ODataDeltaResourceSetWrapper wrapper = new ODataDeltaResourceSetWrapper(null); + Mock elementType = new Mock(); + ExceptionAssert.ThrowsArgumentNull(() => { - // Arrange - IEdmModel model = GetEdmModel(); - IEdmEntityType customer = model.SchemaElements.OfType().First(c => c.Name == "Customer"); - IEdmStructuredTypeReference elementType = new EdmEntityTypeReference(customer, true); + foreach (var item in deserializer.ReadDeltaResourceSet(wrapper, elementType.Object, null)) + ; + }, "readContext"); + } - ODataDeltaDeletedLinkWrapper deletedLinkWrapper = new ODataDeltaDeletedLinkWrapper(new ODataDeltaDeletedLink(new Uri("http://localhost"), new Uri("http://delete"), "delete")); - ODataDeltaLinkWrapper linkWrapper = new ODataDeltaLinkWrapper(new ODataDeltaLink(new Uri("http://localhost"), new Uri("http://link"), "delete")); + [Fact] + public void ReadDeltaResourceSet_Calls_ReadDeltaLinkItems() + { + // Arrange + IEdmModel model = GetEdmModel(); + IEdmEntityType customer = model.SchemaElements.OfType().First(c => c.Name == "Customer"); + IEdmStructuredTypeReference elementType = new EdmEntityTypeReference(customer, true); - ODataDeltaResourceSetWrapper deltaResourceSetWrapper = new ODataDeltaResourceSetWrapper(new ODataDeltaResourceSet()); - deltaResourceSetWrapper.DeltaItems.Add(deletedLinkWrapper); - deltaResourceSetWrapper.DeltaItems.Add(linkWrapper); + ODataDeltaDeletedLinkWrapper deletedLinkWrapper = new ODataDeltaDeletedLinkWrapper(new ODataDeltaDeletedLink(new Uri("http://localhost"), new Uri("http://delete"), "delete")); + ODataDeltaLinkWrapper linkWrapper = new ODataDeltaLinkWrapper(new ODataDeltaLink(new Uri("http://localhost"), new Uri("http://link"), "delete")); - Mock deserializerProvider = new Mock(); - ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); - ODataDeserializerContext readContext = new ODataDeserializerContext() - { - Model = model, - ResourceType = typeof(DeltaSet<>) - }; - - // Act - var result = deserializer.ReadDeltaResourceSet(deltaResourceSetWrapper, elementType, readContext) as IEnumerable; - - // Assert - Assert.Collection(result, - e => - { - DeltaDeletedLink ddl = Assert.IsType>(e); - Assert.Equal(new Uri("http://delete"), ddl.Target); - }, - e => - { - DeltaLink dl = Assert.IsType>(e); - Assert.Equal(new Uri("http://link"), dl.Target); - }); - } + ODataDeltaResourceSetWrapper deltaResourceSetWrapper = new ODataDeltaResourceSetWrapper(new ODataDeltaResourceSet()); + deltaResourceSetWrapper.DeltaItems.Add(deletedLinkWrapper); + deltaResourceSetWrapper.DeltaItems.Add(linkWrapper); - [Fact] - public void ReadDeltaResourceSet_Calls_ReadInlineForEachDeltaItem() + Mock deserializerProvider = new Mock(); + ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); + ODataDeserializerContext readContext = new ODataDeserializerContext() { - // Arrange - IEdmModel model = GetEdmModel(); - IEdmEntityType customer = model.SchemaElements.OfType().First(c => c.Name == "Customer"); - IEdmStructuredTypeReference elementType = new EdmEntityTypeReference(customer, true); - - Mock deserializerProvider = new Mock(); - Mock resourceDeserializer = new Mock(ODataPayloadKind.Resource); + Model = model, + ResourceType = typeof(DeltaSet<>) + }; - ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); + // Act + var result = deserializer.ReadDeltaResourceSet(deltaResourceSetWrapper, elementType, readContext) as IEnumerable; - ODataDeltaResourceSetWrapper deltaResourceSetWrapper = new ODataDeltaResourceSetWrapper(new ODataDeltaResourceSet()); - - Uri source = new Uri("Customers(8)", UriKind.RelativeOrAbsolute); - Uri target = new Uri("Orders(10645)", UriKind.RelativeOrAbsolute); - deltaResourceSetWrapper.DeltaItems.Add(new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a1/") })); - deltaResourceSetWrapper.DeltaItems.Add(new ODataResourceWrapper(new ODataDeletedResource { Id = new Uri("http://a2/") })); - ODataDeserializerContext readContext = new ODataDeserializerContext() + // Assert + Assert.Collection(result, + e => + { + DeltaDeletedLink ddl = Assert.IsType>(e); + Assert.Equal(new Uri("http://delete"), ddl.Target); + }, + e => { - Model = model, - ResourceType = typeof(DeltaSet<>) - }; + DeltaLink dl = Assert.IsType>(e); + Assert.Equal(new Uri("http://link"), dl.Target); + }); + } - deserializerProvider.Setup(p => p.GetEdmTypeDeserializer(elementType, false)).Returns(resourceDeserializer.Object); - resourceDeserializer.Setup(d => d.ReadInline(deltaResourceSetWrapper.DeltaItems[0], elementType, It.IsAny())).Returns("entry1").Verifiable(); - resourceDeserializer.Setup(d => d.ReadInline(deltaResourceSetWrapper.DeltaItems[1], elementType, It.IsAny())).Returns("entry2").Verifiable(); + [Fact] + public void ReadDeltaResourceSet_Calls_ReadInlineForEachDeltaItem() + { + // Arrange + IEdmModel model = GetEdmModel(); + IEdmEntityType customer = model.SchemaElements.OfType().First(c => c.Name == "Customer"); + IEdmStructuredTypeReference elementType = new EdmEntityTypeReference(customer, true); - // Act - var result = deserializer.ReadDeltaResourceSet(deltaResourceSetWrapper, elementType, readContext); + Mock deserializerProvider = new Mock(); + Mock resourceDeserializer = new Mock(ODataPayloadKind.Resource); - // Assert - Assert.Equal(new[] { "entry1", "entry2" }, result.OfType()); - resourceDeserializer.Verify(); - } + ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); - [Fact] - public void ReadDeltaResource_ThrowsArgumentNull_ForInputParameters() - { - // Arrange & Act & Assert - Mock deserializerProvider = new Mock(); - ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadDeltaResource(null, null, null), "resource"); - - // Arrange & Act & Assert - ODataResourceWrapper wrapper = new ODataResourceWrapper(new ODataResource()); - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadDeltaResource(wrapper, null, null), "readContext"); - } + ODataDeltaResourceSetWrapper deltaResourceSetWrapper = new ODataDeltaResourceSetWrapper(new ODataDeltaResourceSet()); - [Fact] - public void ReadDeltaResource_ThrowsSerializationException_NullDeserializer() + Uri source = new Uri("Customers(8)", UriKind.RelativeOrAbsolute); + Uri target = new Uri("Orders(10645)", UriKind.RelativeOrAbsolute); + deltaResourceSetWrapper.DeltaItems.Add(new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a1/") })); + deltaResourceSetWrapper.DeltaItems.Add(new ODataResourceWrapper(new ODataDeletedResource { Id = new Uri("http://a2/") })); + ODataDeserializerContext readContext = new ODataDeserializerContext() { - // Arrange & Act & Assert - IEdmComplexType complex = new EdmComplexType("NS", "Complex"); - IEdmStructuredTypeReference typeRef = new EdmComplexTypeReference(complex, false); - Mock deserializerProvider = new Mock(); - deserializerProvider.Setup(s => s.GetEdmTypeDeserializer(typeRef, false)).Returns((ODataEdmTypeDeserializer)null); + Model = model, + ResourceType = typeof(DeltaSet<>) + }; - ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); + deserializerProvider.Setup(p => p.GetEdmTypeDeserializer(elementType, false)).Returns(resourceDeserializer.Object); + resourceDeserializer.Setup(d => d.ReadInline(deltaResourceSetWrapper.DeltaItems[0], elementType, It.IsAny())).Returns("entry1").Verifiable(); + resourceDeserializer.Setup(d => d.ReadInline(deltaResourceSetWrapper.DeltaItems[1], elementType, It.IsAny())).Returns("entry2").Verifiable(); - // Arrange & Act & Assert - ODataResourceWrapper wrapper = new ODataResourceWrapper(new ODataResource()); - ODataDeserializerContext readContext = new ODataDeserializerContext(); + // Act + var result = deserializer.ReadDeltaResourceSet(deltaResourceSetWrapper, elementType, readContext); - ExceptionAssert.Throws( - () => deserializer.ReadDeltaResource(wrapper, typeRef, readContext), - "'NS.Complex' cannot be deserialized using the OData input formatter."); - } + // Assert + Assert.Equal(new[] { "entry1", "entry2" }, result.OfType()); + resourceDeserializer.Verify(); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ReadDeltaResource_Returns_DeletedResource(bool typed) - { - // Arrange - IEdmModel model = GetEdmModel(); - IEdmEntityType customer = model.SchemaElements.OfType().First(c => c.Name == "Customer"); - IEdmStructuredTypeReference elementType = new EdmEntityTypeReference(customer, true); + [Fact] + public void ReadDeltaResource_ThrowsArgumentNull_ForInputParameters() + { + // Arrange & Act & Assert + Mock deserializerProvider = new Mock(); + ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadDeltaResource(null, null, null), "resource"); + + // Arrange & Act & Assert + ODataResourceWrapper wrapper = new ODataResourceWrapper(new ODataResource()); + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadDeltaResource(wrapper, null, null), "readContext"); + } - Mock deserializerProvider = new Mock(); - ODataResourceDeserializer resourceDeserializer = new ODataResourceDeserializer(deserializerProvider.Object); + [Fact] + public void ReadDeltaResource_ThrowsSerializationException_NullDeserializer() + { + // Arrange & Act & Assert + IEdmComplexType complex = new EdmComplexType("NS", "Complex"); + IEdmStructuredTypeReference typeRef = new EdmComplexTypeReference(complex, false); + Mock deserializerProvider = new Mock(); + deserializerProvider.Setup(s => s.GetEdmTypeDeserializer(typeRef, false)).Returns((ODataEdmTypeDeserializer)null); - Uri id = new Uri("Customers(8)", UriKind.RelativeOrAbsolute); - ODataDeletedResource customerDeleted = new ODataDeletedResource(id, DeltaDeletedEntryReason.Deleted) - { - Properties = new List - { - new ODataProperty { Name = "FirstName", Value = "Peter" }, - new ODataProperty { Name = "LastName", Value = "John" } - } - }; - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(customerDeleted); - ODataDeserializerContext context = new ODataDeserializerContext - { - Model = model, - }; + ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); - if (typed) - { - context.ResourceType = typeof(DeltaSet<>); - } - else - { - context.ResourceType = typeof(EdmChangedObjectCollection); - } + // Arrange & Act & Assert + ODataResourceWrapper wrapper = new ODataResourceWrapper(new ODataResource()); + ODataDeserializerContext readContext = new ODataDeserializerContext(); - deserializerProvider.Setup(d => d.GetEdmTypeDeserializer(It.IsAny(), false)).Returns(resourceDeserializer); - ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); + ExceptionAssert.Throws( + () => deserializer.ReadDeltaResource(wrapper, typeRef, readContext), + "'NS.Complex' cannot be deserialized using the OData input formatter."); + } - // Act - object result = deserializer.ReadDeltaResource(resourceWrapper, elementType, context); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadDeltaResource_Returns_DeletedResource(bool typed) + { + // Arrange + IEdmModel model = GetEdmModel(); + IEdmEntityType customer = model.SchemaElements.OfType().First(c => c.Name == "Customer"); + IEdmStructuredTypeReference elementType = new EdmEntityTypeReference(customer, true); - // Assert - Action testPropertyAction = d => - { - d.TryGetPropertyValue("FirstName", out object firstName); - Assert.Equal("Peter", firstName); - d.TryGetPropertyValue("LastName", out object lastName); - Assert.Equal("John", lastName); - }; + Mock deserializerProvider = new Mock(); + ODataResourceDeserializer resourceDeserializer = new ODataResourceDeserializer(deserializerProvider.Object); - if (typed) - { - DeltaDeletedResource deltaDeletedResource = Assert.IsType>(result); - Assert.Equal(id, deltaDeletedResource.Id); - Assert.Equal(DeltaDeletedEntryReason.Deleted, deltaDeletedResource.Reason); - testPropertyAction(deltaDeletedResource); - } - else + Uri id = new Uri("Customers(8)", UriKind.RelativeOrAbsolute); + ODataDeletedResource customerDeleted = new ODataDeletedResource(id, DeltaDeletedEntryReason.Deleted) + { + Properties = new List { - EdmDeltaDeletedResourceObject deltaDeletedResource = Assert.IsType(result); - Assert.Equal(id, deltaDeletedResource.Id); - Assert.Equal(DeltaDeletedEntryReason.Deleted, deltaDeletedResource.Reason); - testPropertyAction(deltaDeletedResource); + new ODataProperty { Name = "FirstName", Value = "Peter" }, + new ODataProperty { Name = "LastName", Value = "John" } } - } + }; + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(customerDeleted); + ODataDeserializerContext context = new ODataDeserializerContext + { + Model = model, + }; - [Fact] - public void ReadDeltaDeletedLink_ThrowsArgumentNull_ForInputParameters() + if (typed) { - // Arrange & Act & Assert - Mock deserializerProvider = new Mock(); - ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadDeltaDeletedLink(null, null, null), "deletedLink"); - - // Arrange & Act & Assert - ODataDeltaDeletedLinkWrapper wrapper = new ODataDeltaDeletedLinkWrapper( - new ODataDeltaDeletedLink(new Uri("http://localhost"), new Uri("http://localhost"), "delete")); - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadDeltaDeletedLink(wrapper, null, null), "readContext"); + context.ResourceType = typeof(DeltaSet<>); } + else + { + context.ResourceType = typeof(EdmChangedObjectCollection); + } + + deserializerProvider.Setup(d => d.GetEdmTypeDeserializer(It.IsAny(), false)).Returns(resourceDeserializer); + ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); + + // Act + object result = deserializer.ReadDeltaResource(resourceWrapper, elementType, context); - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ReadDeltaDeletedLink_Returns_DeletedDeltaLink(bool typed) + // Assert + Action testPropertyAction = d => { - // Arrange - Mock deserializerProvider = new Mock(); - ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); + d.TryGetPropertyValue("FirstName", out object firstName); + Assert.Equal("Peter", firstName); + d.TryGetPropertyValue("LastName", out object lastName); + Assert.Equal("John", lastName); + }; - Uri source = new Uri("Customers(8)", UriKind.RelativeOrAbsolute); - Uri target = new Uri("Orders(10645)", UriKind.RelativeOrAbsolute); - ODataDeltaDeletedLink deletedLink = new ODataDeltaDeletedLink(source, target, "Orders"); - ODataDeltaDeletedLinkWrapper wrapper = new ODataDeltaDeletedLinkWrapper(deletedLink); + if (typed) + { + DeltaDeletedResource deltaDeletedResource = Assert.IsType>(result); + Assert.Equal(id, deltaDeletedResource.Id); + Assert.Equal(DeltaDeletedEntryReason.Deleted, deltaDeletedResource.Reason); + testPropertyAction(deltaDeletedResource); + } + else + { + EdmDeltaDeletedResourceObject deltaDeletedResource = Assert.IsType(result); + Assert.Equal(id, deltaDeletedResource.Id); + Assert.Equal(DeltaDeletedEntryReason.Deleted, deltaDeletedResource.Reason); + testPropertyAction(deltaDeletedResource); + } + } - IEdmModel model = GetEdmModel(); - IEdmEntityType customer = model.SchemaElements.OfType().First(c => c.Name == "Customer"); - IEdmStructuredTypeReference elementType = new EdmEntityTypeReference(customer, true); + [Fact] + public void ReadDeltaDeletedLink_ThrowsArgumentNull_ForInputParameters() + { + // Arrange & Act & Assert + Mock deserializerProvider = new Mock(); + ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadDeltaDeletedLink(null, null, null), "deletedLink"); + + // Arrange & Act & Assert + ODataDeltaDeletedLinkWrapper wrapper = new ODataDeltaDeletedLinkWrapper( + new ODataDeltaDeletedLink(new Uri("http://localhost"), new Uri("http://localhost"), "delete")); + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadDeltaDeletedLink(wrapper, null, null), "readContext"); + } - ODataDeserializerContext context = new ODataDeserializerContext - { - Model = model, - ResourceType = typeof(DeltaSet<>) - }; + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadDeltaDeletedLink_Returns_DeletedDeltaLink(bool typed) + { + // Arrange + Mock deserializerProvider = new Mock(); + ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); - if (typed) - { - context.ResourceType = typeof(DeltaSet<>); - } - else - { - context.ResourceType = typeof(EdmChangedObjectCollection); - } + Uri source = new Uri("Customers(8)", UriKind.RelativeOrAbsolute); + Uri target = new Uri("Orders(10645)", UriKind.RelativeOrAbsolute); + ODataDeltaDeletedLink deletedLink = new ODataDeltaDeletedLink(source, target, "Orders"); + ODataDeltaDeletedLinkWrapper wrapper = new ODataDeltaDeletedLinkWrapper(deletedLink); - // Act - object deltaLinkObject = deserializer.ReadDeltaDeletedLink(wrapper, elementType, context); + IEdmModel model = GetEdmModel(); + IEdmEntityType customer = model.SchemaElements.OfType().First(c => c.Name == "Customer"); + IEdmStructuredTypeReference elementType = new EdmEntityTypeReference(customer, true); - // Assert - if (typed) - { - DeltaDeletedLink deltaDeletedLink = Assert.IsType>(deltaLinkObject); - Assert.Equal(source, deltaDeletedLink.Source); - Assert.Equal(target, deltaDeletedLink.Target); - Assert.Equal("Orders", deltaDeletedLink.Relationship); - } - else - { - EdmDeltaDeletedLink deltaDeletedLink = Assert.IsType(deltaLinkObject); - Assert.Equal(source, deltaDeletedLink.Source); - Assert.Equal(target, deltaDeletedLink.Target); - Assert.Equal("Orders", deltaDeletedLink.Relationship); - } - } + ODataDeserializerContext context = new ODataDeserializerContext + { + Model = model, + ResourceType = typeof(DeltaSet<>) + }; - [Fact] - public void ReadDeltaLink_ThrowsArgumentNull_ForInputParameters() + if (typed) { - // Arrange & Act & Assert - Mock deserializerProvider = new Mock(); - ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadDeltaLink(null, null, null), "link"); - - // Arrange & Act & Assert - ODataDeltaLinkWrapper wrapper = new ODataDeltaLinkWrapper( - new ODataDeltaLink(new Uri("http://localhost"), new Uri("http://localhost"), "delete")); - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadDeltaLink(wrapper, null, null), "readContext"); + context.ResourceType = typeof(DeltaSet<>); } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ReadDeltaLink_Returns_DeltaLink(bool typed) + else { - // Arrange - Mock deserializerProvider = new Mock(); - ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); + context.ResourceType = typeof(EdmChangedObjectCollection); + } - Uri source = new Uri("Customers(8)", UriKind.RelativeOrAbsolute); - Uri target = new Uri("Orders(10645)", UriKind.RelativeOrAbsolute); - ODataDeltaLink link = new ODataDeltaLink(source, target, "Orders"); - ODataDeltaLinkWrapper wrapper = new ODataDeltaLinkWrapper(link); + // Act + object deltaLinkObject = deserializer.ReadDeltaDeletedLink(wrapper, elementType, context); - IEdmModel model = GetEdmModel(); - IEdmEntityType customer = model.SchemaElements.OfType().First(c => c.Name == "Customer"); - IEdmStructuredTypeReference elementType = new EdmEntityTypeReference(customer, true); + // Assert + if (typed) + { + DeltaDeletedLink deltaDeletedLink = Assert.IsType>(deltaLinkObject); + Assert.Equal(source, deltaDeletedLink.Source); + Assert.Equal(target, deltaDeletedLink.Target); + Assert.Equal("Orders", deltaDeletedLink.Relationship); + } + else + { + EdmDeltaDeletedLink deltaDeletedLink = Assert.IsType(deltaLinkObject); + Assert.Equal(source, deltaDeletedLink.Source); + Assert.Equal(target, deltaDeletedLink.Target); + Assert.Equal("Orders", deltaDeletedLink.Relationship); + } + } - ODataDeserializerContext context = new ODataDeserializerContext - { - Model = model, - ResourceType = typeof(DeltaSet<>) - }; + [Fact] + public void ReadDeltaLink_ThrowsArgumentNull_ForInputParameters() + { + // Arrange & Act & Assert + Mock deserializerProvider = new Mock(); + ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadDeltaLink(null, null, null), "link"); + + // Arrange & Act & Assert + ODataDeltaLinkWrapper wrapper = new ODataDeltaLinkWrapper( + new ODataDeltaLink(new Uri("http://localhost"), new Uri("http://localhost"), "delete")); + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadDeltaLink(wrapper, null, null), "readContext"); + } - if (typed) - { - context.ResourceType = typeof(DeltaSet<>); - } - else - { - context.ResourceType = typeof(EdmChangedObjectCollection); - } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadDeltaLink_Returns_DeltaLink(bool typed) + { + // Arrange + Mock deserializerProvider = new Mock(); + ODataDeltaResourceSetDeserializer deserializer = new ODataDeltaResourceSetDeserializer(deserializerProvider.Object); - // Act - object deltaLinkObject = deserializer.ReadDeltaLink(wrapper, elementType, context); + Uri source = new Uri("Customers(8)", UriKind.RelativeOrAbsolute); + Uri target = new Uri("Orders(10645)", UriKind.RelativeOrAbsolute); + ODataDeltaLink link = new ODataDeltaLink(source, target, "Orders"); + ODataDeltaLinkWrapper wrapper = new ODataDeltaLinkWrapper(link); - // Assert - if (typed) - { - DeltaLink deltaLink = Assert.IsType>(deltaLinkObject); - Assert.Equal(source, deltaLink.Source); - Assert.Equal(target, deltaLink.Target); - Assert.Equal("Orders", deltaLink.Relationship); - } - else - { - EdmDeltaLink deltaLink = Assert.IsType(deltaLinkObject); - Assert.Equal(source, deltaLink.Source); - Assert.Equal(target, deltaLink.Target); - Assert.Equal("Orders", deltaLink.Relationship); - } - } + IEdmModel model = GetEdmModel(); + IEdmEntityType customer = model.SchemaElements.OfType().First(c => c.Name == "Customer"); + IEdmStructuredTypeReference elementType = new EdmEntityTypeReference(customer, true); - private static IEdmModel GetEdmModel() + ODataDeserializerContext context = new ODataDeserializerContext { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Orders"); - return builder.GetEdmModel(); - } + Model = model, + ResourceType = typeof(DeltaSet<>) + }; - public class Customer + if (typed) { - public int ID { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public Address HomeAddress { get; set; } - - public IList Orders { get; set; } + context.ResourceType = typeof(DeltaSet<>); } - - public class Address + else { - public string Street { get; set; } - public string ZipCode { get; set; } + context.ResourceType = typeof(EdmChangedObjectCollection); } - public class Order + // Act + object deltaLinkObject = deserializer.ReadDeltaLink(wrapper, elementType, context); + + // Assert + if (typed) { - public int Id { get; set; } + DeltaLink deltaLink = Assert.IsType>(deltaLinkObject); + Assert.Equal(source, deltaLink.Source); + Assert.Equal(target, deltaLink.Target); + Assert.Equal("Orders", deltaLink.Relationship); } + else + { + EdmDeltaLink deltaLink = Assert.IsType(deltaLinkObject); + Assert.Equal(source, deltaLink.Source); + Assert.Equal(target, deltaLink.Target); + Assert.Equal("Orders", deltaLink.Relationship); + } + } + + private static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); + return builder.GetEdmModel(); + } + + public class Customer + { + public int ID { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public Address HomeAddress { get; set; } + + public IList Orders { get; set; } + } + + public class Address + { + public string Street { get; set; } + public string ZipCode { get; set; } + } + + public class Order + { + public int Id { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerContextTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerContextTest.cs index 52f255805..67ab69992 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerContextTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerContextTest.cs @@ -13,42 +13,41 @@ using Microsoft.AspNetCore.OData.Tests.Models; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class ODataDeserializerContextTest { - public class ODataDeserializerContextTest + [Theory] + [InlineData(typeof(Delta), false)] + [InlineData(typeof(int), false)] + [InlineData(typeof(string), false)] + [InlineData(typeof(Delta), true)] + public void Property_IsDeltaOfT_HasRightValue(Type resourceType, bool expectedResult) { - [Theory] - [InlineData(typeof(Delta), false)] - [InlineData(typeof(int), false)] - [InlineData(typeof(string), false)] - [InlineData(typeof(Delta), true)] - public void Property_IsDeltaOfT_HasRightValue(Type resourceType, bool expectedResult) - { - // Arrange & Act - ODataDeserializerContext context = new ODataDeserializerContext { ResourceType = resourceType }; + // Arrange & Act + ODataDeserializerContext context = new ODataDeserializerContext { ResourceType = resourceType }; - // Act - Assert.Equal(expectedResult, context.IsDeltaOfT); - } + // Act + Assert.Equal(expectedResult, context.IsDeltaOfT); + } - [Theory] - [InlineData(typeof(Delta), false)] - [InlineData(typeof(int), false)] - [InlineData(typeof(string), false)] - [InlineData(typeof(Delta), false)] - [InlineData(typeof(IEdmObject), true)] - [InlineData(typeof(IEdmComplexObject), true)] - [InlineData(typeof(IEdmEntityObject), true)] - [InlineData(typeof(EdmComplexObject), true)] - [InlineData(typeof(EdmEntityObject), true)] - [InlineData(typeof(ODataUntypedActionParameters), true)] - public void Property_IsNoClrType_HasRightValue(Type resourceType, bool expectedResult) - { - // Arrange & Act - ODataDeserializerContext context = new ODataDeserializerContext { ResourceType = resourceType }; + [Theory] + [InlineData(typeof(Delta), false)] + [InlineData(typeof(int), false)] + [InlineData(typeof(string), false)] + [InlineData(typeof(Delta), false)] + [InlineData(typeof(IEdmObject), true)] + [InlineData(typeof(IEdmComplexObject), true)] + [InlineData(typeof(IEdmEntityObject), true)] + [InlineData(typeof(EdmComplexObject), true)] + [InlineData(typeof(EdmEntityObject), true)] + [InlineData(typeof(ODataUntypedActionParameters), true)] + public void Property_IsNoClrType_HasRightValue(Type resourceType, bool expectedResult) + { + // Arrange & Act + ODataDeserializerContext context = new ODataDeserializerContext { ResourceType = resourceType }; - // Assert - Assert.Equal(expectedResult, context.IsNoClrType); - } + // Assert + Assert.Equal(expectedResult, context.IsNoClrType); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerProviderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerProviderTests.cs index 67fc834cd..e337f8540 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerProviderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerProviderTests.cs @@ -22,301 +22,300 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class ODataDeserializerProviderTests { - public class ODataDeserializerProviderTests + private static IODataDeserializerProvider _deserializerProvider = GetServiceProvider().GetRequiredService(); + private static IEdmModel _edmModel = GetEdmModel(); + + [Fact] + public void ODataDeserializerProvider_Ctor_ThrowsArgumentNull_ServiceProvider() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataDeserializerProvider(null), "serviceProvider"); + } + + [Fact] + public void GetODataDeserializer_Uri() + { + // Arrange + HttpRequest request = GetRequest(model: null); + + // Act + IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(typeof(Uri), request); + + // Assert + Assert.NotNull(deserializer); + var referenceLinkDeserializer = Assert.IsType(deserializer); + Assert.Equal(ODataPayloadKind.EntityReferenceLink, referenceLinkDeserializer.ODataPayloadKind); + } + + [Theory] + [InlineData(typeof(Int16))] + [InlineData(typeof(int))] + [InlineData(typeof(Decimal))] + [InlineData(typeof(DateTimeOffset))] + [InlineData(typeof(DateTime))] + [InlineData(typeof(Date))] + [InlineData(typeof(TimeOfDay))] + [InlineData(typeof(double))] + [InlineData(typeof(byte[]))] + [InlineData(typeof(bool))] + [InlineData(typeof(int?))] + public void GetODataDeserializer_Primitive(Type type) + { + // Arrange + HttpRequest request = GetRequest(EdmCoreModel.Instance); + + // Act + IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(type, request); + + // Assert + Assert.NotNull(deserializer); + ODataPrimitiveDeserializer rawValueDeserializer = Assert.IsType(deserializer); + Assert.Equal(ODataPayloadKind.Property, rawValueDeserializer.ODataPayloadKind); + } + + [Fact] + public void GetODataDeserializer_Resource_ForEntity() + { + // Arrange + HttpRequest request = GetRequest(_edmModel); + + // Act + IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(typeof(Product), request); + + // Assert + Assert.NotNull(deserializer); + ODataResourceDeserializer entityDeserializer = Assert.IsType(deserializer); + Assert.Equal(ODataPayloadKind.Resource, deserializer.ODataPayloadKind); + Assert.Equal(entityDeserializer.DeserializerProvider, _deserializerProvider); + } + + [Fact] + public void GetODataDeserializer_Resource_ForComplex() + { + // Arrange + HttpRequest request = GetRequest(_edmModel); + + // Act + IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(typeof(Address), request); + + // Assert + Assert.NotNull(deserializer); + ODataResourceDeserializer complexDeserializer = Assert.IsType(deserializer); + Assert.Equal(ODataPayloadKind.Resource, deserializer.ODataPayloadKind); + Assert.Equal(complexDeserializer.DeserializerProvider, _deserializerProvider); + } + + [Theory] + [InlineData(typeof(Product[]))] + [InlineData(typeof(IEnumerable))] + [InlineData(typeof(ICollection))] + [InlineData(typeof(IList))] + [InlineData(typeof(List))] + // [InlineData(typeof(PageResult))] + public void GetODataDeserializer_ResourceSet_ForEntityCollection(Type collectionType) + { + // Arrange + HttpRequest request = GetRequest(_edmModel); + + // Act + IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(collectionType, request); + + // Assert + Assert.NotNull(deserializer); + ODataResourceSetDeserializer resourceSetDeserializer = Assert.IsType(deserializer); + Assert.Equal(ODataPayloadKind.ResourceSet, deserializer.ODataPayloadKind); + Assert.Equal(resourceSetDeserializer.DeserializerProvider, _deserializerProvider); + } + + [Theory] + [InlineData(typeof(Address[]))] + [InlineData(typeof(IEnumerable
))] + [InlineData(typeof(ICollection
))] + [InlineData(typeof(IList
))] + [InlineData(typeof(List
))] + // [InlineData(typeof(PageResult
))] + public void GetODataDeserializer_ResourceSet_ForComplexCollection(Type collectionType) + { + // Arrange + HttpRequest request = GetRequest(_edmModel); + + // Act + IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(collectionType, request); + + // Assert + Assert.NotNull(deserializer); + ODataResourceSetDeserializer resourceSetDeserializer = Assert.IsType(deserializer); + Assert.Equal(ODataPayloadKind.ResourceSet, deserializer.ODataPayloadKind); + Assert.Equal(resourceSetDeserializer.DeserializerProvider, _deserializerProvider); + } + + [Theory] + [InlineData(typeof(DeltaSet<>))] + [InlineData(typeof(EdmChangedObjectCollection))] + public void GetODataDeserializer_DeltaResourceSet_ForDeltaSet(Type deltaType) + { + // Arrange + HttpRequest request = GetRequest(_edmModel); + + // Act + IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(deltaType, request); + + // Assert + Assert.NotNull(deserializer); + ODataDeltaResourceSetDeserializer setDeserializer = Assert.IsType(deserializer); + Assert.Equal(ODataPayloadKind.Delta, setDeserializer.ODataPayloadKind); + Assert.Equal(setDeserializer.DeserializerProvider, _deserializerProvider); + } + + [Fact] + public void GetODataDeserializer_ReturnsSameDeserializer_ForSameType() + { + // Arrange + HttpRequest request = GetRequest(_edmModel); + + // Act + IODataDeserializer firstCallDeserializer = _deserializerProvider.GetODataDeserializer(typeof(Product), request); + IODataDeserializer secondCallDeserializer = _deserializerProvider.GetODataDeserializer(typeof(Product), request); + + // Assert + Assert.Same(firstCallDeserializer, secondCallDeserializer); + } + + [Theory] + [InlineData(typeof(ODataActionParameters))] + [InlineData(typeof(ODataUntypedActionParameters))] + public void GetODataDeserializer_ActionPayload(Type resourceType) + { + // Arrange + HttpRequest request = GetRequest(model: null); + + // Act + ODataActionPayloadDeserializer basicActionPayload + = _deserializerProvider.GetODataDeserializer(resourceType, request) as ODataActionPayloadDeserializer; + + // Assert + Assert.NotNull(basicActionPayload); + } + + [Fact] + public void GetODataDeserializer_ThrowsArgumentNull_ForType() + { + // Arrange + HttpRequest request = GetRequest(model: null); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => _deserializerProvider.GetODataDeserializer(type: null, request: request), + "type"); + } + + [Fact] + public void GetODataDeserializer_ThrowsArgumentNull_ForRequest() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => _deserializerProvider.GetODataDeserializer(typeof(int), request: null), + "request"); + } + + [Fact] + public void GetEdmTypeDeserializer_ThrowsArgument_EdmType() + { + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => _deserializerProvider.GetEdmTypeDeserializer(edmType: null), + "edmType"); + } + + [Fact] + public void GetEdmTypeDeserializer_Caches_CreateDeserializerOutput() + { + // Arrange + IEdmTypeReference edmType = new Mock().Object; + + // Act + var deserializer1 = _deserializerProvider.GetEdmTypeDeserializer(edmType); + var deserializer2 = _deserializerProvider.GetEdmTypeDeserializer(edmType); + + // Assert + Assert.Same(deserializer1, deserializer2); + } + + [Fact] + public void GetEdmTypeDeserializer_ReturnsCorrectDeserializer_ForEdmUntyped() + { + // Arrange + IEdmTypeReference edmType = EdmUntypedStructuredTypeReference.NullableTypeReference; + + // Act + var deserializer = _deserializerProvider.GetEdmTypeDeserializer(edmType); + + // Assert + ODataResourceDeserializer resourceSerializer = Assert.IsType(deserializer); + Assert.Equal(ODataPayloadKind.Resource, resourceSerializer.ODataPayloadKind); + } + + [Fact] + public void GetEdmTypeDeserializer_ReturnsCorrectDeserializer_ForCollectionOfEdmUntyped() + { + // Arrange + IEdmTypeReference edmType = EdmUntypedHelpers.NullableUntypedCollectionReference; + + // Act + var deserializer = _deserializerProvider.GetEdmTypeDeserializer(edmType); + + // Assert + ODataResourceSetDeserializer setSerializer = Assert.IsType(deserializer); + Assert.Equal(ODataPayloadKind.ResourceSet, setSerializer.ODataPayloadKind); + } + + private static IServiceProvider GetServiceProvider() + { + IServiceCollection services = new ServiceCollection(); + + services.AddSingleton(); + + // Deserializers. + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + + return services.BuildServiceProvider(); + } + + private HttpRequest GetRequest(IEdmModel model) + { + HttpContext context = new DefaultHttpContext(); + context.ODataFeature().Model = model; + return context.Request; + } + + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Products"); + return builder.GetEdmModel(); + } + + private class Product + { + public int Id { get; set; } + public Address Location { get; set; } + } + + private class Address { - private static IODataDeserializerProvider _deserializerProvider = GetServiceProvider().GetRequiredService(); - private static IEdmModel _edmModel = GetEdmModel(); - - [Fact] - public void ODataDeserializerProvider_Ctor_ThrowsArgumentNull_ServiceProvider() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataDeserializerProvider(null), "serviceProvider"); - } - - [Fact] - public void GetODataDeserializer_Uri() - { - // Arrange - HttpRequest request = GetRequest(model: null); - - // Act - IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(typeof(Uri), request); - - // Assert - Assert.NotNull(deserializer); - var referenceLinkDeserializer = Assert.IsType(deserializer); - Assert.Equal(ODataPayloadKind.EntityReferenceLink, referenceLinkDeserializer.ODataPayloadKind); - } - - [Theory] - [InlineData(typeof(Int16))] - [InlineData(typeof(int))] - [InlineData(typeof(Decimal))] - [InlineData(typeof(DateTimeOffset))] - [InlineData(typeof(DateTime))] - [InlineData(typeof(Date))] - [InlineData(typeof(TimeOfDay))] - [InlineData(typeof(double))] - [InlineData(typeof(byte[]))] - [InlineData(typeof(bool))] - [InlineData(typeof(int?))] - public void GetODataDeserializer_Primitive(Type type) - { - // Arrange - HttpRequest request = GetRequest(EdmCoreModel.Instance); - - // Act - IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(type, request); - - // Assert - Assert.NotNull(deserializer); - ODataPrimitiveDeserializer rawValueDeserializer = Assert.IsType(deserializer); - Assert.Equal(ODataPayloadKind.Property, rawValueDeserializer.ODataPayloadKind); - } - - [Fact] - public void GetODataDeserializer_Resource_ForEntity() - { - // Arrange - HttpRequest request = GetRequest(_edmModel); - - // Act - IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(typeof(Product), request); - - // Assert - Assert.NotNull(deserializer); - ODataResourceDeserializer entityDeserializer = Assert.IsType(deserializer); - Assert.Equal(ODataPayloadKind.Resource, deserializer.ODataPayloadKind); - Assert.Equal(entityDeserializer.DeserializerProvider, _deserializerProvider); - } - - [Fact] - public void GetODataDeserializer_Resource_ForComplex() - { - // Arrange - HttpRequest request = GetRequest(_edmModel); - - // Act - IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(typeof(Address), request); - - // Assert - Assert.NotNull(deserializer); - ODataResourceDeserializer complexDeserializer = Assert.IsType(deserializer); - Assert.Equal(ODataPayloadKind.Resource, deserializer.ODataPayloadKind); - Assert.Equal(complexDeserializer.DeserializerProvider, _deserializerProvider); - } - - [Theory] - [InlineData(typeof(Product[]))] - [InlineData(typeof(IEnumerable))] - [InlineData(typeof(ICollection))] - [InlineData(typeof(IList))] - [InlineData(typeof(List))] - // [InlineData(typeof(PageResult))] - public void GetODataDeserializer_ResourceSet_ForEntityCollection(Type collectionType) - { - // Arrange - HttpRequest request = GetRequest(_edmModel); - - // Act - IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(collectionType, request); - - // Assert - Assert.NotNull(deserializer); - ODataResourceSetDeserializer resourceSetDeserializer = Assert.IsType(deserializer); - Assert.Equal(ODataPayloadKind.ResourceSet, deserializer.ODataPayloadKind); - Assert.Equal(resourceSetDeserializer.DeserializerProvider, _deserializerProvider); - } - - [Theory] - [InlineData(typeof(Address[]))] - [InlineData(typeof(IEnumerable
))] - [InlineData(typeof(ICollection
))] - [InlineData(typeof(IList
))] - [InlineData(typeof(List
))] - // [InlineData(typeof(PageResult
))] - public void GetODataDeserializer_ResourceSet_ForComplexCollection(Type collectionType) - { - // Arrange - HttpRequest request = GetRequest(_edmModel); - - // Act - IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(collectionType, request); - - // Assert - Assert.NotNull(deserializer); - ODataResourceSetDeserializer resourceSetDeserializer = Assert.IsType(deserializer); - Assert.Equal(ODataPayloadKind.ResourceSet, deserializer.ODataPayloadKind); - Assert.Equal(resourceSetDeserializer.DeserializerProvider, _deserializerProvider); - } - - [Theory] - [InlineData(typeof(DeltaSet<>))] - [InlineData(typeof(EdmChangedObjectCollection))] - public void GetODataDeserializer_DeltaResourceSet_ForDeltaSet(Type deltaType) - { - // Arrange - HttpRequest request = GetRequest(_edmModel); - - // Act - IODataDeserializer deserializer = _deserializerProvider.GetODataDeserializer(deltaType, request); - - // Assert - Assert.NotNull(deserializer); - ODataDeltaResourceSetDeserializer setDeserializer = Assert.IsType(deserializer); - Assert.Equal(ODataPayloadKind.Delta, setDeserializer.ODataPayloadKind); - Assert.Equal(setDeserializer.DeserializerProvider, _deserializerProvider); - } - - [Fact] - public void GetODataDeserializer_ReturnsSameDeserializer_ForSameType() - { - // Arrange - HttpRequest request = GetRequest(_edmModel); - - // Act - IODataDeserializer firstCallDeserializer = _deserializerProvider.GetODataDeserializer(typeof(Product), request); - IODataDeserializer secondCallDeserializer = _deserializerProvider.GetODataDeserializer(typeof(Product), request); - - // Assert - Assert.Same(firstCallDeserializer, secondCallDeserializer); - } - - [Theory] - [InlineData(typeof(ODataActionParameters))] - [InlineData(typeof(ODataUntypedActionParameters))] - public void GetODataDeserializer_ActionPayload(Type resourceType) - { - // Arrange - HttpRequest request = GetRequest(model: null); - - // Act - ODataActionPayloadDeserializer basicActionPayload - = _deserializerProvider.GetODataDeserializer(resourceType, request) as ODataActionPayloadDeserializer; - - // Assert - Assert.NotNull(basicActionPayload); - } - - [Fact] - public void GetODataDeserializer_ThrowsArgumentNull_ForType() - { - // Arrange - HttpRequest request = GetRequest(model: null); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => _deserializerProvider.GetODataDeserializer(type: null, request: request), - "type"); - } - - [Fact] - public void GetODataDeserializer_ThrowsArgumentNull_ForRequest() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => _deserializerProvider.GetODataDeserializer(typeof(int), request: null), - "request"); - } - - [Fact] - public void GetEdmTypeDeserializer_ThrowsArgument_EdmType() - { - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => _deserializerProvider.GetEdmTypeDeserializer(edmType: null), - "edmType"); - } - - [Fact] - public void GetEdmTypeDeserializer_Caches_CreateDeserializerOutput() - { - // Arrange - IEdmTypeReference edmType = new Mock().Object; - - // Act - var deserializer1 = _deserializerProvider.GetEdmTypeDeserializer(edmType); - var deserializer2 = _deserializerProvider.GetEdmTypeDeserializer(edmType); - - // Assert - Assert.Same(deserializer1, deserializer2); - } - - [Fact] - public void GetEdmTypeDeserializer_ReturnsCorrectDeserializer_ForEdmUntyped() - { - // Arrange - IEdmTypeReference edmType = EdmUntypedStructuredTypeReference.NullableTypeReference; - - // Act - var deserializer = _deserializerProvider.GetEdmTypeDeserializer(edmType); - - // Assert - ODataResourceDeserializer resourceSerializer = Assert.IsType(deserializer); - Assert.Equal(ODataPayloadKind.Resource, resourceSerializer.ODataPayloadKind); - } - - [Fact] - public void GetEdmTypeDeserializer_ReturnsCorrectDeserializer_ForCollectionOfEdmUntyped() - { - // Arrange - IEdmTypeReference edmType = EdmUntypedHelpers.NullableUntypedCollectionReference; - - // Act - var deserializer = _deserializerProvider.GetEdmTypeDeserializer(edmType); - - // Assert - ODataResourceSetDeserializer setSerializer = Assert.IsType(deserializer); - Assert.Equal(ODataPayloadKind.ResourceSet, setSerializer.ODataPayloadKind); - } - - private static IServiceProvider GetServiceProvider() - { - IServiceCollection services = new ServiceCollection(); - - services.AddSingleton(); - - // Deserializers. - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - - return services.BuildServiceProvider(); - } - - private HttpRequest GetRequest(IEdmModel model) - { - HttpContext context = new DefaultHttpContext(); - context.ODataFeature().Model = model; - return context.Request; - } - - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Products"); - return builder.GetEdmModel(); - } - - private class Product - { - public int Id { get; set; } - public Address Location { get; set; } - } - - private class Address - { - public string Street { get; set; } - } + public string Street { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerTest.cs index 3b012cf37..6be00cf30 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataDeserializerTest.cs @@ -13,30 +13,29 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class ODataDeserializerTest { - public class ODataDeserializerTest + [Fact] + public void Ctor_SetsProperty_ODataPayloadKind() { - [Fact] - public void Ctor_SetsProperty_ODataPayloadKind() - { - // Arrange - Mock deserializer = new Mock(ODataPayloadKind.Unsupported); + // Arrange + Mock deserializer = new Mock(ODataPayloadKind.Unsupported); - // Act & Assert - Assert.Equal(ODataPayloadKind.Unsupported, deserializer.Object.ODataPayloadKind); - } + // Act & Assert + Assert.Equal(ODataPayloadKind.Unsupported, deserializer.Object.ODataPayloadKind); + } - [Fact] - public async Task ReadAsync_Throws_NotSupported() - { - // Arrange - Mock deserializer = new Mock(ODataPayloadKind.Resource) { CallBase = true }; + [Fact] + public async Task ReadAsync_Throws_NotSupported() + { + // Arrange + Mock deserializer = new Mock(ODataPayloadKind.Resource) { CallBase = true }; - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => deserializer.Object.ReadAsync(messageReader: null, type: null, readContext: null), - "'ODataDeserializerProxy' does not support Read."); - } + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => deserializer.Object.ReadAsync(messageReader: null, type: null, readContext: null), + "'ODataDeserializerProxy' does not support Read."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEdmTypeDeserializerTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEdmTypeDeserializerTest.cs index b2ced79de..e590ef262 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEdmTypeDeserializerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEdmTypeDeserializerTest.cs @@ -12,35 +12,34 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class ODataEdmTypeDeserializerTest { - public class ODataEdmTypeDeserializerTest + [Fact] + public void Ctor_SetsProperty_ODataPayloadKind() + { + var deserializer = new Mock(ODataPayloadKind.Unsupported); + + Assert.Equal(ODataPayloadKind.Unsupported, deserializer.Object.ODataPayloadKind); + } + + [Fact] + public void Ctor_SetsProperty_DeserializerProvider() { - [Fact] - public void Ctor_SetsProperty_ODataPayloadKind() - { - var deserializer = new Mock(ODataPayloadKind.Unsupported); - - Assert.Equal(ODataPayloadKind.Unsupported, deserializer.Object.ODataPayloadKind); - } - - [Fact] - public void Ctor_SetsProperty_DeserializerProvider() - { - Mock deserializerProvider = new Mock(); - var deserializer = new Mock(ODataPayloadKind.Unsupported, deserializerProvider.Object); - - Assert.Same(deserializerProvider.Object, deserializer.Object.DeserializerProvider); - } - - [Fact] - public void ReadInline_Throws_NotSupported() - { - var deserializer = new Mock(ODataPayloadKind.Unsupported) { CallBase = true }; - - ExceptionAssert.Throws( - () => deserializer.Object.ReadInline(item: null, edmType: null, readContext: null), - "Type 'ODataEdmTypeDeserializerProxy' does not support ReadInline."); - } + Mock deserializerProvider = new Mock(); + var deserializer = new Mock(ODataPayloadKind.Unsupported, deserializerProvider.Object); + + Assert.Same(deserializerProvider.Object, deserializer.Object.DeserializerProvider); + } + + [Fact] + public void ReadInline_Throws_NotSupported() + { + var deserializer = new Mock(ODataPayloadKind.Unsupported) { CallBase = true }; + + ExceptionAssert.Throws( + () => deserializer.Object.ReadInline(item: null, edmType: null, readContext: null), + "Type 'ODataEdmTypeDeserializerProxy' does not support ReadInline."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEntityReferenceLinkDeserializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEntityReferenceLinkDeserializerTests.cs index 999e23e9f..f41eaccff 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEntityReferenceLinkDeserializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEntityReferenceLinkDeserializerTests.cs @@ -19,140 +19,139 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class ODataEntityReferenceLinkDeserializerTests { - public class ODataEntityReferenceLinkDeserializerTests + [Fact] + public void Ctor_DoesnotThrow() { - [Fact] - public void Ctor_DoesnotThrow() - { - // Arrange - var deserializer = new ODataEntityReferenceLinkDeserializer(); + // Arrange + var deserializer = new ODataEntityReferenceLinkDeserializer(); - // Act & Assert - Assert.Equal(ODataPayloadKind.EntityReferenceLink, deserializer.ODataPayloadKind); - } + // Act & Assert + Assert.Equal(ODataPayloadKind.EntityReferenceLink, deserializer.ODataPayloadKind); + } - [Fact] - public async Task ReadAsync_ThrowsArgumentNull_MessageReader() - { - // Arrange - var deserializer = new ODataEntityReferenceLinkDeserializer(); + [Fact] + public async Task ReadAsync_ThrowsArgumentNull_MessageReader() + { + // Arrange + var deserializer = new ODataEntityReferenceLinkDeserializer(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => deserializer.ReadAsync(messageReader: null, type: null, readContext: new ODataDeserializerContext()), - "messageReader"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => deserializer.ReadAsync(messageReader: null, type: null, readContext: new ODataDeserializerContext()), + "messageReader"); + } - [Fact] - public async Task ReadAsync_ThrowsArgumentNull_ReadContext() - { - // Arrange - var deserializer = new ODataEntityReferenceLinkDeserializer(); - ODataMessageReader messageReader = ODataTestUtil.GetMockODataMessageReader(); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => deserializer.ReadAsync(messageReader, type: null, readContext: null), - "readContext"); - } - - [Fact] - public async Task ReadAsync_RoundTrips() - { - // Arrange - IEdmModel model = CreateModel(); - var deserializer = new ODataEntityReferenceLinkDeserializer(); - MockODataRequestMessage requestMessage = new MockODataRequestMessage(); - ODataMessageWriterSettings settings = new ODataMessageWriterSettings() - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } - }; - settings.SetContentType(ODataFormat.Json); - - ODataMessageWriter messageWriter = new ODataMessageWriter(requestMessage, settings); - await messageWriter.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink { Url = new Uri("http://localhost/samplelink") }); - - var request = RequestFactory.Create("Get", "http://localhost", opt => opt.AddRouteComponents("odata", model)); - request.ODataFeature().RoutePrefix = "odata"; - ODataMessageReaderSettings readSettings = new ODataMessageReaderSettings(); - ODataMessageReader messageReader = new ODataMessageReader(new MockODataRequestMessage(requestMessage), readSettings, model); - ODataDeserializerContext context = new ODataDeserializerContext - { - Request = request, - Path = new ODataPath(new NavigationPropertySegment(GetNavigationProperty(model), navigationSource: null)) - }; - - // Act - Uri uri = await deserializer.ReadAsync(messageReader, typeof(Uri), context) as Uri; - - // Assert - Assert.NotNull(uri); - Assert.Equal("http://localhost/samplelink", uri.AbsoluteUri); - } - - [Fact] - public async Task ReadJsonLightAsync() - { - // Arrange - var deserializer = new ODataEntityReferenceLinkDeserializer(); - MockODataRequestMessage requestMessage = new MockODataRequestMessage(); - ODataMessageWriterSettings writerSettings = new ODataMessageWriterSettings(); - writerSettings.SetContentType(ODataFormat.Json); - IEdmModel model = CreateModel(); - ODataMessageWriter messageWriter = new ODataMessageWriter(requestMessage, writerSettings, model); - await messageWriter.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink { Url = new Uri("http://localhost/samplelink") }); - ODataMessageReader messageReader = new ODataMessageReader(new MockODataRequestMessage(requestMessage), - new ODataMessageReaderSettings(), model); - - IEdmNavigationProperty navigationProperty = GetNavigationProperty(model); - - var request = RequestFactory.Create("Get", "http://localhost", opt => opt.AddRouteComponents("odata", model)); - request.ODataFeature().RoutePrefix = "odata"; - ODataDeserializerContext context = new ODataDeserializerContext - { - Request = request, - Path = new ODataPath(new NavigationPropertySegment(navigationProperty, navigationSource: null)) - }; - - // Act - Uri uri = await deserializer.ReadAsync(messageReader, typeof(Uri), context) as Uri; - - // Assert - Assert.NotNull(uri); - Assert.Equal("http://localhost/samplelink", uri.AbsoluteUri); - } - - private static IEdmModel CreateModel() + [Fact] + public async Task ReadAsync_ThrowsArgumentNull_ReadContext() + { + // Arrange + var deserializer = new ODataEntityReferenceLinkDeserializer(); + ODataMessageReader messageReader = ODataTestUtil.GetMockODataMessageReader(); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => deserializer.ReadAsync(messageReader, type: null, readContext: null), + "readContext"); + } + + [Fact] + public async Task ReadAsync_RoundTrips() + { + // Arrange + IEdmModel model = CreateModel(); + var deserializer = new ODataEntityReferenceLinkDeserializer(); + MockODataRequestMessage requestMessage = new MockODataRequestMessage(); + ODataMessageWriterSettings settings = new ODataMessageWriterSettings() { - Mock mock = new Mock(); - mock.Setup(b => b.ValidateModel(It.IsAny())).Callback(() => { }); - mock.CallBase = true; - ODataModelBuilder builder = mock.Object; - EntitySetConfiguration entities = builder.EntitySet("entities"); - builder.EntitySet("related"); - NavigationPropertyConfiguration entityToRelated = - entities.EntityType.HasOptional((e) => e.Related); - // entities.HasNavigationPropertyLink(entityToRelated, (a, b) => new Uri("aa:b"), false); - entities.HasOptionalBinding((e) => e.Related, "related"); - - return builder.GetEdmModel(); - } - - private static IEdmNavigationProperty GetNavigationProperty(IEdmModel model) + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } + }; + settings.SetContentType(ODataFormat.Json); + + ODataMessageWriter messageWriter = new ODataMessageWriter(requestMessage, settings); + await messageWriter.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink { Url = new Uri("http://localhost/samplelink") }); + + var request = RequestFactory.Create("Get", "http://localhost", opt => opt.AddRouteComponents("odata", model)); + request.ODataFeature().RoutePrefix = "odata"; + ODataMessageReaderSettings readSettings = new ODataMessageReaderSettings(); + ODataMessageReader messageReader = new ODataMessageReader(new MockODataRequestMessage(requestMessage), readSettings, model); + ODataDeserializerContext context = new ODataDeserializerContext { - return - model.EntityContainer.EntitySets().First().NavigationPropertyBindings.Single().NavigationProperty; - } + Request = request, + Path = new ODataPath(new NavigationPropertySegment(GetNavigationProperty(model), navigationSource: null)) + }; - private class Entity - { - public RelatedEntity Related { get; set; } - } + // Act + Uri uri = await deserializer.ReadAsync(messageReader, typeof(Uri), context) as Uri; + + // Assert + Assert.NotNull(uri); + Assert.Equal("http://localhost/samplelink", uri.AbsoluteUri); + } - private class RelatedEntity + [Fact] + public async Task ReadJsonLightAsync() + { + // Arrange + var deserializer = new ODataEntityReferenceLinkDeserializer(); + MockODataRequestMessage requestMessage = new MockODataRequestMessage(); + ODataMessageWriterSettings writerSettings = new ODataMessageWriterSettings(); + writerSettings.SetContentType(ODataFormat.Json); + IEdmModel model = CreateModel(); + ODataMessageWriter messageWriter = new ODataMessageWriter(requestMessage, writerSettings, model); + await messageWriter.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink { Url = new Uri("http://localhost/samplelink") }); + ODataMessageReader messageReader = new ODataMessageReader(new MockODataRequestMessage(requestMessage), + new ODataMessageReaderSettings(), model); + + IEdmNavigationProperty navigationProperty = GetNavigationProperty(model); + + var request = RequestFactory.Create("Get", "http://localhost", opt => opt.AddRouteComponents("odata", model)); + request.ODataFeature().RoutePrefix = "odata"; + ODataDeserializerContext context = new ODataDeserializerContext { - } + Request = request, + Path = new ODataPath(new NavigationPropertySegment(navigationProperty, navigationSource: null)) + }; + + // Act + Uri uri = await deserializer.ReadAsync(messageReader, typeof(Uri), context) as Uri; + + // Assert + Assert.NotNull(uri); + Assert.Equal("http://localhost/samplelink", uri.AbsoluteUri); + } + + private static IEdmModel CreateModel() + { + Mock mock = new Mock(); + mock.Setup(b => b.ValidateModel(It.IsAny())).Callback(() => { }); + mock.CallBase = true; + ODataModelBuilder builder = mock.Object; + EntitySetConfiguration entities = builder.EntitySet("entities"); + builder.EntitySet("related"); + NavigationPropertyConfiguration entityToRelated = + entities.EntityType.HasOptional((e) => e.Related); + // entities.HasNavigationPropertyLink(entityToRelated, (a, b) => new Uri("aa:b"), false); + entities.HasOptionalBinding((e) => e.Related, "related"); + + return builder.GetEdmModel(); + } + + private static IEdmNavigationProperty GetNavigationProperty(IEdmModel model) + { + return + model.EntityContainer.EntitySets().First().NavigationPropertyBindings.Single().NavigationProperty; + } + + private class Entity + { + public RelatedEntity Related { get; set; } + } + + private class RelatedEntity + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEnumDeserializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEnumDeserializerTests.cs index 730632e5c..4f665f7b4 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEnumDeserializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataEnumDeserializerTests.cs @@ -18,163 +18,162 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class ODataEnumDeserializerTests { - public class ODataEnumDeserializerTests + private static IEdmModel _edmModel = GetEdmModel(); + + [Fact] + public async Task ReadAsync_ThrowsArgumentNull_ForInputs() { - private static IEdmModel _edmModel = GetEdmModel(); + // Arrange & Act & Assert + var deserializer = new ODataEnumDeserializer(); + await ExceptionAssert.ThrowsArgumentNullAsync( + () => deserializer.ReadAsync(messageReader: null, type: null, readContext: null), "messageReader"); + + // Arrange & Act & Assert + ODataMessageReader messageReader = ODataFormatterHelpers.GetMockODataMessageReader(); + await ExceptionAssert.ThrowsArgumentNullAsync( + () => deserializer.ReadAsync(messageReader, type: null, readContext: null), "type"); + + // Arrange & Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => deserializer.ReadAsync(messageReader, typeof(Color), readContext: null), "readContext"); + } - [Fact] - public async Task ReadAsync_ThrowsArgumentNull_ForInputs() - { - // Arrange & Act & Assert - var deserializer = new ODataEnumDeserializer(); - await ExceptionAssert.ThrowsArgumentNullAsync( - () => deserializer.ReadAsync(messageReader: null, type: null, readContext: null), "messageReader"); - - // Arrange & Act & Assert - ODataMessageReader messageReader = ODataFormatterHelpers.GetMockODataMessageReader(); - await ExceptionAssert.ThrowsArgumentNullAsync( - () => deserializer.ReadAsync(messageReader, type: null, readContext: null), "type"); - - // Arrange & Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => deserializer.ReadAsync(messageReader, typeof(Color), readContext: null), "readContext"); - } - - [Fact] - public async Task ReadAsync_Works_ForEnumValue() + [Fact] + public async Task ReadAsync_Works_ForEnumValue() + { + // Arrange + string content = "{\"@odata.type\":\"#NS.Color\",\"value\":\"Blue\"}"; + + ODataEnumDeserializer deserializer = new ODataEnumDeserializer(); + ODataDeserializerContext readContext = new ODataDeserializerContext { - // Arrange - string content = "{\"@odata.type\":\"#NS.Color\",\"value\":\"Blue\"}"; + Model = _edmModel, + ResourceType = typeof(Color) + }; - ODataEnumDeserializer deserializer = new ODataEnumDeserializer(); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _edmModel, - ResourceType = typeof(Color) - }; + HttpRequest request = RequestFactory.Create("Post", "http://localhost/TestUri", opt => opt.AddRouteComponents("odata", _edmModel)); - HttpRequest request = RequestFactory.Create("Post", "http://localhost/TestUri", opt => opt.AddRouteComponents("odata", _edmModel)); + // Act + object value = await deserializer.ReadAsync(ODataTestUtil.GetODataMessageReader(request.GetODataMessage(content), _edmModel), + typeof(Color), readContext); - // Act - object value = await deserializer.ReadAsync(ODataTestUtil.GetODataMessageReader(request.GetODataMessage(content), _edmModel), - typeof(Color), readContext); + // Assert + Color color = Assert.IsType(value); + Assert.Equal(Color.Blue, color); + } - // Assert - Color color = Assert.IsType(value); - Assert.Equal(Color.Blue, color); - } + [Fact] + public async Task ReadAsync_Works_ForRawValue() + { + // Arrange + string content = "{\"value\":\"Blue\"}"; - [Fact] - public async Task ReadAsync_Works_ForRawValue() + ODataEnumDeserializer deserializer = new ODataEnumDeserializer(); + ODataDeserializerContext readContext = new ODataDeserializerContext { - // Arrange - string content = "{\"value\":\"Blue\"}"; - - ODataEnumDeserializer deserializer = new ODataEnumDeserializer(); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _edmModel, - ResourceType = typeof(Color) - }; - HttpRequest request = RequestFactory.Create("Post", "http://localhost/", _edmModel); - - // Act - object value = await deserializer.ReadAsync(ODataTestUtil.GetODataMessageReader(request.GetODataMessage(content), _edmModel), - typeof(Color), readContext); - - // Assert - Color color = Assert.IsType(value); - Assert.Equal(Color.Blue, color); - } - - [Fact] - public async Task ReadAsync_Works_ForUnType() - { - // Arrange - string content = "{\"@odata.type\":\"#NS.Color\",\"value\":\"Blue\"}"; - - ODataEnumDeserializer deserializer = new ODataEnumDeserializer(); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _edmModel, - ResourceType = typeof(IEdmEnumObject) - }; - HttpRequest request = RequestFactory.Create("Post", "http://localhost/", _edmModel); - - // Act - object value = await deserializer.ReadAsync(ODataTestUtil.GetODataMessageReader(request.GetODataMessage(content), _edmModel), - typeof(Color), readContext); - - // Assert - EdmEnumObject color = Assert.IsType(value); - Assert.NotNull(color); - - Assert.Equal("Blue", color.Value); - } - - [Fact] - public async Task ReadAsync_Works_ForModelAlias() + Model = _edmModel, + ResourceType = typeof(Color) + }; + HttpRequest request = RequestFactory.Create("Post", "http://localhost/", _edmModel); + + // Act + object value = await deserializer.ReadAsync(ODataTestUtil.GetODataMessageReader(request.GetODataMessage(content), _edmModel), + typeof(Color), readContext); + + // Assert + Color color = Assert.IsType(value); + Assert.Equal(Color.Blue, color); + } + + [Fact] + public async Task ReadAsync_Works_ForUnType() + { + // Arrange + string content = "{\"@odata.type\":\"#NS.Color\",\"value\":\"Blue\"}"; + + ODataEnumDeserializer deserializer = new ODataEnumDeserializer(); + ODataDeserializerContext readContext = new ODataDeserializerContext { - // Arrange - string content = "{\"@odata.type\":\"#NS.level\",\"value\":\"veryhigh\"}"; + Model = _edmModel, + ResourceType = typeof(IEdmEnumObject) + }; + HttpRequest request = RequestFactory.Create("Post", "http://localhost/", _edmModel); - var builder = new ODataConventionModelBuilder(); - builder.EnumType().Namespace = "NS"; - IEdmModel model = builder.GetEdmModel(); + // Act + object value = await deserializer.ReadAsync(ODataTestUtil.GetODataMessageReader(request.GetODataMessage(content), _edmModel), + typeof(Color), readContext); - ODataEnumDeserializer deserializer = new ODataEnumDeserializer(); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = model, - ResourceType = typeof(Level) - }; + // Assert + EdmEnumObject color = Assert.IsType(value); + Assert.NotNull(color); - HttpRequest request = RequestFactory.Create("Post", "http://localhost/", _edmModel); + Assert.Equal("Blue", color.Value); + } - // Act - object value = await deserializer.ReadAsync(ODataTestUtil.GetODataMessageReader(request.GetODataMessage(content), model), - typeof(Level), readContext); + [Fact] + public async Task ReadAsync_Works_ForModelAlias() + { + // Arrange + string content = "{\"@odata.type\":\"#NS.level\",\"value\":\"veryhigh\"}"; - // Assert - Level level = Assert.IsType(value); - Assert.Equal(Level.High, level); - } + var builder = new ODataConventionModelBuilder(); + builder.EnumType().Namespace = "NS"; + IEdmModel model = builder.GetEdmModel(); - [Fact] - public void ReadInline_ThrowsArgumentNull_ForInputs() + ODataEnumDeserializer deserializer = new ODataEnumDeserializer(); + ODataDeserializerContext readContext = new ODataDeserializerContext { - // Arrange - ODataEnumDeserializer deserializer = new ODataEnumDeserializer(); - Mock edmType = new Mock(); + Model = model, + ResourceType = typeof(Level) + }; - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadInline(new object(), edmType.Object, readContext: null), "readContext"); - } + HttpRequest request = RequestFactory.Create("Post", "http://localhost/", _edmModel); - private static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EnumType().Namespace = "NS"; - return builder.GetEdmModel(); - } + // Act + object value = await deserializer.ReadAsync(ODataTestUtil.GetODataMessageReader(request.GetODataMessage(content), model), + typeof(Level), readContext); - public enum Color - { - Red, - Blue, - Green - } + // Assert + Level level = Assert.IsType(value); + Assert.Equal(Level.High, level); + } - [DataContract(Name = "level")] - public enum Level - { - [EnumMember(Value = "low")] - Low, + [Fact] + public void ReadInline_ThrowsArgumentNull_ForInputs() + { + // Arrange + ODataEnumDeserializer deserializer = new ODataEnumDeserializer(); + Mock edmType = new Mock(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadInline(new object(), edmType.Object, readContext: null), "readContext"); + } + + private static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EnumType().Namespace = "NS"; + return builder.GetEdmModel(); + } + + public enum Color + { + Red, + Blue, + Green + } + + [DataContract(Name = "level")] + public enum Level + { + [EnumMember(Value = "low")] + Low, - [EnumMember(Value = "veryhigh")] - High - } + [EnumMember(Value = "veryhigh")] + High } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataPrimitiveDeserializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataPrimitiveDeserializerTests.cs index 6dcacd61a..649c0e183 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataPrimitiveDeserializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataPrimitiveDeserializerTests.cs @@ -22,389 +22,388 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class ODataPrimitiveDeserializerTests { - public class ODataPrimitiveDeserializerTests + private IEdmPrimitiveTypeReference _edmIntType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); + public static TheoryDataSet NonEdmPrimitiveData { - private IEdmPrimitiveTypeReference _edmIntType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); - public static TheoryDataSet NonEdmPrimitiveData + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { (char)'1', "1" }, - { (char[]) new char[] {'1'}, "1" }, - { (UInt16)1, (int)1 }, - { (UInt32)1, (long)1 }, - { (UInt64)1, (long)1 }, - //(Stream) new MemoryStream(new byte[] { 1 }), // TODO: Enable once we have support for streams - }; - } + { (char)'1', "1" }, + { (char[]) new char[] {'1'}, "1" }, + { (UInt16)1, (int)1 }, + { (UInt32)1, (long)1 }, + { (UInt64)1, (long)1 }, + //(Stream) new MemoryStream(new byte[] { 1 }), // TODO: Enable once we have support for streams + }; } + } - public static TheoryDataSet EdmPrimitiveData + public static TheoryDataSet EdmPrimitiveData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { "1", "Edm.String", "\"1\"" }, - { true, "Edm.Boolean", "true" }, - { (Byte)1, "Edm.Byte", "1" }, - { (Decimal)1, "Edm.Decimal", "1" }, - { (Double)1, "Edm.Double", "1.0" }, - { (Guid)Guid.Empty, "Edm.Guid", "\"00000000-0000-0000-0000-000000000000\"" }, - { (Int16)1, "Edm.Int16", "1" }, - { (Int32)1, "Edm.Int32", "1" }, - { (Int64)1, "Edm.Int64", "1" }, - { (SByte)1, "Edm.SByte", "1" }, - { (Single)1, "Edm.Single", "1" }, - { new byte[] { 1 }, "Edm.Binary", "\"AQ==\"" }, - { new TimeSpan(), "Edm.Duration", "\"PT0S\"" }, - { new DateTimeOffset(), "Edm.DateTimeOffset", "\"0001-01-01T00:00:00Z\"" }, - { new Date(2014, 10, 13), "Edm.Date", "\"2014-10-13\"" }, - { new TimeOfDay(15, 38, 25, 109), "Edm.TimeOfDay", "\"15:38:25.1090000\"" }, - }; - } + { "1", "Edm.String", "\"1\"" }, + { true, "Edm.Boolean", "true" }, + { (Byte)1, "Edm.Byte", "1" }, + { (Decimal)1, "Edm.Decimal", "1" }, + { (Double)1, "Edm.Double", "1.0" }, + { (Guid)Guid.Empty, "Edm.Guid", "\"00000000-0000-0000-0000-000000000000\"" }, + { (Int16)1, "Edm.Int16", "1" }, + { (Int32)1, "Edm.Int32", "1" }, + { (Int64)1, "Edm.Int64", "1" }, + { (SByte)1, "Edm.SByte", "1" }, + { (Single)1, "Edm.Single", "1" }, + { new byte[] { 1 }, "Edm.Binary", "\"AQ==\"" }, + { new TimeSpan(), "Edm.Duration", "\"PT0S\"" }, + { new DateTimeOffset(), "Edm.DateTimeOffset", "\"0001-01-01T00:00:00Z\"" }, + { new Date(2014, 10, 13), "Edm.Date", "\"2014-10-13\"" }, + { new TimeOfDay(15, 38, 25, 109), "Edm.TimeOfDay", "\"15:38:25.1090000\"" }, + }; } + } - public static TheoryDataSet DateTimePrimitiveData + public static TheoryDataSet DateTimePrimitiveData + { + get { - get + DateTime dtUtc = new DateTime(2014, 10, 16, 1, 2, 3, DateTimeKind.Utc); + DateTime dtLocal = new DateTime(2014, 10, 16, 1, 2, 3, DateTimeKind.Local); + DateTime dtUnspecified = new DateTime(2014, 10, 16, 1, 2, 3, DateTimeKind.Unspecified); + TimeZoneInfo pacificStandard = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); + TimeZoneInfo chinaStandard = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"); + return new TheoryDataSet { - DateTime dtUtc = new DateTime(2014, 10, 16, 1, 2, 3, DateTimeKind.Utc); - DateTime dtLocal = new DateTime(2014, 10, 16, 1, 2, 3, DateTimeKind.Local); - DateTime dtUnspecified = new DateTime(2014, 10, 16, 1, 2, 3, DateTimeKind.Unspecified); - TimeZoneInfo pacificStandard = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); - TimeZoneInfo chinaStandard = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"); - return new TheoryDataSet - { - { DateTimeOffset.Parse("2014-10-16T01:02:03Z"), dtUtc, null }, - { new DateTimeOffset(dtLocal), dtLocal, null }, - { new DateTimeOffset(new DateTime(2014, 10, 16, 1, 2, 3, DateTimeKind.Unspecified)), dtUnspecified, null }, - { DateTimeOffset.Parse("2014-10-16T09:02:03+8:00"), dtUtc, chinaStandard }, - { new DateTimeOffset(dtLocal).ToOffset(new TimeSpan(+8,0,0)), dtLocal, chinaStandard }, - { DateTimeOffset.Parse("2014-10-16T01:02:03-7:00"), dtUnspecified, pacificStandard }, - }; - } + { DateTimeOffset.Parse("2014-10-16T01:02:03Z"), dtUtc, null }, + { new DateTimeOffset(dtLocal), dtLocal, null }, + { new DateTimeOffset(new DateTime(2014, 10, 16, 1, 2, 3, DateTimeKind.Unspecified)), dtUnspecified, null }, + { DateTimeOffset.Parse("2014-10-16T09:02:03+8:00"), dtUtc, chinaStandard }, + { new DateTimeOffset(dtLocal).ToOffset(new TimeSpan(+8,0,0)), dtLocal, chinaStandard }, + { DateTimeOffset.Parse("2014-10-16T01:02:03-7:00"), dtUnspecified, pacificStandard }, + }; } + } - [Fact] - public void ReadInline_ReturnsNull_IfItemIsNull() - { - IEdmPrimitiveTypeReference primitiveType = EdmCoreModel.Instance.GetInt32(isNullable: true); - var deserializer = new ODataPrimitiveDeserializer(); + [Fact] + public void ReadInline_ReturnsNull_IfItemIsNull() + { + IEdmPrimitiveTypeReference primitiveType = EdmCoreModel.Instance.GetInt32(isNullable: true); + var deserializer = new ODataPrimitiveDeserializer(); - Assert.Null(deserializer.ReadInline(item: null, edmType: _edmIntType, readContext: new ODataDeserializerContext())); - } + Assert.Null(deserializer.ReadInline(item: null, edmType: _edmIntType, readContext: new ODataDeserializerContext())); + } - [Fact] - public void ReadInline_Throws_ArgumentMustBeOfType() - { - IEdmPrimitiveTypeReference primitiveType = EdmCoreModel.Instance.GetInt32(isNullable: true); - var deserializer = new ODataPrimitiveDeserializer(); + [Fact] + public void ReadInline_Throws_ArgumentMustBeOfType() + { + IEdmPrimitiveTypeReference primitiveType = EdmCoreModel.Instance.GetInt32(isNullable: true); + var deserializer = new ODataPrimitiveDeserializer(); - ExceptionAssert.ThrowsArgument( - () => deserializer.ReadInline(42, _edmIntType, new ODataDeserializerContext()), - "item", - "The argument must be of type 'ODataProperty'"); - } + ExceptionAssert.ThrowsArgument( + () => deserializer.ReadInline(42, _edmIntType, new ODataDeserializerContext()), + "item", + "The argument must be of type 'ODataProperty'"); + } - [Fact] - public void ReadInline_Calls_ReadPrimitive() - { - // Arrange - IEdmPrimitiveTypeReference primitiveType = EdmCoreModel.Instance.GetInt32(isNullable: true); - Mock deserializer = new Mock(); - ODataProperty property = new ODataProperty(); - ODataDeserializerContext readContext = new ODataDeserializerContext(); + [Fact] + public void ReadInline_Calls_ReadPrimitive() + { + // Arrange + IEdmPrimitiveTypeReference primitiveType = EdmCoreModel.Instance.GetInt32(isNullable: true); + Mock deserializer = new Mock(); + ODataProperty property = new ODataProperty(); + ODataDeserializerContext readContext = new ODataDeserializerContext(); - deserializer.Setup(d => d.ReadPrimitive(property, readContext)).Returns(42).Verifiable(); + deserializer.Setup(d => d.ReadPrimitive(property, readContext)).Returns(42).Verifiable(); - // Act - var result = deserializer.Object.ReadInline(property, primitiveType, readContext); + // Act + var result = deserializer.Object.ReadInline(property, primitiveType, readContext); - // Assert - deserializer.Verify(); - Assert.Equal(42, result); - } + // Assert + deserializer.Verify(); + Assert.Equal(42, result); + } - [Fact] - public async Task ReadAsync_ThrowsArgumentNull_MessageReader() - { - var deserializer = new ODataPrimitiveDeserializer(); + [Fact] + public async Task ReadAsync_ThrowsArgumentNull_MessageReader() + { + var deserializer = new ODataPrimitiveDeserializer(); - await ExceptionAssert.ThrowsArgumentNullAsync( - () => deserializer.ReadAsync(messageReader: null, type: typeof(int), readContext: new ODataDeserializerContext()), - "messageReader"); - } + await ExceptionAssert.ThrowsArgumentNullAsync( + () => deserializer.ReadAsync(messageReader: null, type: typeof(int), readContext: new ODataDeserializerContext()), + "messageReader"); + } - [Fact] - public void ReadPrimitive_ThrowsArgumentNull_PrimitiveProperty() - { - var deserializer = new ODataPrimitiveDeserializer(); + [Fact] + public void ReadPrimitive_ThrowsArgumentNull_PrimitiveProperty() + { + var deserializer = new ODataPrimitiveDeserializer(); - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.ReadPrimitive(primitiveProperty: null, readContext: new ODataDeserializerContext()), - "primitiveProperty"); - } + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ReadPrimitive(primitiveProperty: null, readContext: new ODataDeserializerContext()), + "primitiveProperty"); + } - [Fact] - public void ReadPrimitive_ThrowsArgumentNull_ReadContext() - { - var deserializer = new ODataPrimitiveDeserializer(); - ODataProperty property = new ODataProperty(); + [Fact] + public void ReadPrimitive_ThrowsArgumentNull_ReadContext() + { + var deserializer = new ODataPrimitiveDeserializer(); + ODataProperty property = new ODataProperty(); - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadPrimitive(property, null), "readContext"); - } + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadPrimitive(property, null), "readContext"); + } - [Fact] - public async Task ReadAsync_ThrowsArgumentNull_ReadContext() - { - var deserializer = new ODataPrimitiveDeserializer(); - await ExceptionAssert.ThrowsArgumentNullAsync(() => deserializer.ReadAsync(null, typeof(object), null), "messageReader"); + [Fact] + public async Task ReadAsync_ThrowsArgumentNull_ReadContext() + { + var deserializer = new ODataPrimitiveDeserializer(); + await ExceptionAssert.ThrowsArgumentNullAsync(() => deserializer.ReadAsync(null, typeof(object), null), "messageReader"); - ODataMessageReader messageReader = ODataFormatterHelpers.GetMockODataMessageReader(); - await ExceptionAssert.ThrowsArgumentNullAsync(() => deserializer.ReadAsync(messageReader, typeof(object), null), "readContext"); - } + ODataMessageReader messageReader = ODataFormatterHelpers.GetMockODataMessageReader(); + await ExceptionAssert.ThrowsArgumentNullAsync(() => deserializer.ReadAsync(messageReader, typeof(object), null), "readContext"); + } - [Theory] - [MemberData(nameof(EdmPrimitiveData))] - public async Task ReadAsync_PrimitiveWithTypeInContext(object obj, string edmType, string value) + [Theory] + [MemberData(nameof(EdmPrimitiveData))] + public async Task ReadAsync_PrimitiveWithTypeInContext(object obj, string edmType, string value) + { + // Arrange + IEdmModel model = CreateModel(); + ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); + ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); + + MemoryStream stream = new MemoryStream(); + ODataMessageWrapper message = new ODataMessageWrapper(stream); + + ODataMessageWriterSettings settings = new ODataMessageWriterSettings { - // Arrange - IEdmModel model = CreateModel(); - ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); - ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } + }; + settings.SetContentType(ODataFormat.Json); - MemoryStream stream = new MemoryStream(); - ODataMessageWrapper message = new ODataMessageWrapper(stream); + Type type = obj == null ? typeof(int) : obj.GetType(); - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } - }; - settings.SetContentType(ODataFormat.Json); + ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, model); + ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), model); + ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = model }; + ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model, ResourceType = type }; - Type type = obj == null ? typeof(int) : obj.GetType(); + await serializer.WriteObjectAsync(obj, type, messageWriter, writeContext); + stream.Seek(0, SeekOrigin.Begin); - ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, model); - ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), model); - ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = model }; - ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model, ResourceType = type }; + // Act & Assert + Assert.NotNull(edmType); + Assert.NotNull(value); + Assert.Equal(obj, await deserializer.ReadAsync(messageReader, type, readContext)); + } - await serializer.WriteObjectAsync(obj, type, messageWriter, writeContext); - stream.Seek(0, SeekOrigin.Begin); + [Theory] + [MemberData(nameof(EdmPrimitiveData))] + public async Task ReadAsync_Primitive(object obj, string edmType, string value) + { + // Arrange + IEdmModel model = CreateModel(); + ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); + ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); - // Act & Assert - Assert.NotNull(edmType); - Assert.NotNull(value); - Assert.Equal(obj, await deserializer.ReadAsync(messageReader, type, readContext)); - } + MemoryStream stream = new MemoryStream(); + ODataMessageWrapper message = new ODataMessageWrapper(stream); - [Theory] - [MemberData(nameof(EdmPrimitiveData))] - public async Task ReadAsync_Primitive(object obj, string edmType, string value) + ODataMessageWriterSettings settings = new ODataMessageWriterSettings { - // Arrange - IEdmModel model = CreateModel(); - ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); - ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } + }; + settings.SetContentType(ODataFormat.Json); - MemoryStream stream = new MemoryStream(); - ODataMessageWrapper message = new ODataMessageWrapper(stream); + ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, model); + ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), model); + ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = model }; + ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model }; - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } - }; - settings.SetContentType(ODataFormat.Json); + Type type = obj == null ? typeof(int) : obj.GetType(); - ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, model); - ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), model); - ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = model }; - ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model }; + await serializer.WriteObjectAsync(obj, type, messageWriter, writeContext); + stream.Seek(0, SeekOrigin.Begin); - Type type = obj == null ? typeof(int) : obj.GetType(); + // Act & Assert + Assert.NotNull(edmType); + Assert.NotNull(value); + Assert.Equal(obj, await deserializer.ReadAsync(messageReader, type, readContext)); + } - await serializer.WriteObjectAsync(obj, type, messageWriter, writeContext); - stream.Seek(0, SeekOrigin.Begin); + [Theory] + [InlineData("{\"value\":\"FgQF\"}", typeof(byte[]), new byte[] { 22, 4, 5 })] + [InlineData("{\"value\":1}", typeof(Int16), (Int16)1)] + [InlineData("{\"value\":1}", typeof(Int32), 1)] + [InlineData("{\"value\":1}", typeof(Int64), (Int64)1)] + [InlineData("{\"value\":\"true\"}", typeof(Boolean), true)] + [InlineData("{\"value\":5}", typeof(SByte), (SByte)5)] + [InlineData("{\"value\":201}", typeof(Byte), (Byte)201)] + [InlineData("{\"value\":1.1}", typeof(Double), 1.1)] + [InlineData("{\"value\":1.1}", typeof(Single), (Single)1.1)] + public async Task ReadFromStreamAsync_RawPrimitive(string content, Type type, object expected) + { + // Arrange + IEdmModel model = CreateModel(); - // Act & Assert - Assert.NotNull(edmType); - Assert.NotNull(value); - Assert.Equal(obj, await deserializer.ReadAsync(messageReader, type, readContext)); - } + HttpRequest request = RequestFactory.Create("Patch", "http://localhost/OData/Suppliers(1)/Address", opt => opt.AddRouteComponents("odata", model)); - [Theory] - [InlineData("{\"value\":\"FgQF\"}", typeof(byte[]), new byte[] { 22, 4, 5 })] - [InlineData("{\"value\":1}", typeof(Int16), (Int16)1)] - [InlineData("{\"value\":1}", typeof(Int32), 1)] - [InlineData("{\"value\":1}", typeof(Int64), (Int64)1)] - [InlineData("{\"value\":\"true\"}", typeof(Boolean), true)] - [InlineData("{\"value\":5}", typeof(SByte), (SByte)5)] - [InlineData("{\"value\":201}", typeof(Byte), (Byte)201)] - [InlineData("{\"value\":1.1}", typeof(Double), 1.1)] - [InlineData("{\"value\":1.1}", typeof(Single), (Single)1.1)] - public async Task ReadFromStreamAsync_RawPrimitive(string content, Type type, object expected) + ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); + ODataDeserializerContext readContext = new ODataDeserializerContext { - // Arrange - IEdmModel model = CreateModel(); + Model = model, + ResourceType = type + }; - HttpRequest request = RequestFactory.Create("Patch", "http://localhost/OData/Suppliers(1)/Address", opt => opt.AddRouteComponents("odata", model)); + // Act + object value = await deserializer.ReadAsync(ODataTestUtil.GetODataMessageReader(request.GetODataMessage(content), model), type, readContext); - ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = model, - ResourceType = type - }; + // Assert + Assert.Equal(expected, value); + } - // Act - object value = await deserializer.ReadAsync(ODataTestUtil.GetODataMessageReader(request.GetODataMessage(content), model), type, readContext); + [Fact] + public async Task ReadFromStreamAsync_RawGuid() + { + // Arrange + string content = "{\"value\":\"f4b787c7-920d-4993-a584-ceb68968058c\"}"; + Type type = typeof(Guid); + object expected = new Guid("f4b787c7-920d-4993-a584-ceb68968058c"); - // Assert - Assert.Equal(expected, value); - } + IEdmModel model = CreateModel(); + HttpRequest request = RequestFactory.Create("Patch", "http://localhost/OData/Suppliers(1)/Address", opt => opt.AddRouteComponents("odata", model)); - [Fact] - public async Task ReadFromStreamAsync_RawGuid() + ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); + ODataDeserializerContext readContext = new ODataDeserializerContext { - // Arrange - string content = "{\"value\":\"f4b787c7-920d-4993-a584-ceb68968058c\"}"; - Type type = typeof(Guid); - object expected = new Guid("f4b787c7-920d-4993-a584-ceb68968058c"); + Model = model, + ResourceType = type + }; - IEdmModel model = CreateModel(); - HttpRequest request = RequestFactory.Create("Patch", "http://localhost/OData/Suppliers(1)/Address", opt => opt.AddRouteComponents("odata", model)); + // Act + object value = await deserializer.ReadAsync(ODataTestUtil.GetODataMessageReader(request.GetODataMessage(content), model), type, readContext); - ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = model, - ResourceType = type - }; + // Assert + Assert.Equal(expected, value); + } - // Act - object value = await deserializer.ReadAsync(ODataTestUtil.GetODataMessageReader(request.GetODataMessage(content), model), type, readContext); + [Theory] + [MemberData(nameof(NonEdmPrimitiveData))] + public async Task ReadAsync_MappedPrimitiveWithTypeinContext(object obj, object expected) + { + // Arrange + IEdmModel model = CreateModel(); + ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); + ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); - // Assert - Assert.Equal(expected, value); - } + MemoryStream stream = new MemoryStream(); + ODataMessageWrapper message = new ODataMessageWrapper(stream); - [Theory] - [MemberData(nameof(NonEdmPrimitiveData))] - public async Task ReadAsync_MappedPrimitiveWithTypeinContext(object obj, object expected) + ODataMessageWriterSettings settings = new ODataMessageWriterSettings { - // Arrange - IEdmModel model = CreateModel(); - ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); - ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } + }; + settings.SetContentType(ODataFormat.Json); - MemoryStream stream = new MemoryStream(); - ODataMessageWrapper message = new ODataMessageWrapper(stream); + Type type = obj == null ? typeof(int) : expected.GetType(); - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } - }; - settings.SetContentType(ODataFormat.Json); + ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, model); + ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), model); + ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = model }; + ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model, ResourceType = type }; - Type type = obj == null ? typeof(int) : expected.GetType(); + await serializer.WriteObjectAsync(obj, type, messageWriter, writeContext); + stream.Seek(0, SeekOrigin.Begin); - ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, model); - ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), model); - ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = model }; - ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model, ResourceType = type }; + // Act && Assert + Assert.Equal(expected, await deserializer.ReadAsync(messageReader, type, readContext)); + } - await serializer.WriteObjectAsync(obj, type, messageWriter, writeContext); - stream.Seek(0, SeekOrigin.Begin); + [Theory] + [MemberData(nameof(NonEdmPrimitiveData))] + public async Task ReadAsync_MappedPrimitive(object obj, object expected) + { + // Arrange + IEdmModel model = CreateModel(); + ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); + ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); - // Act && Assert - Assert.Equal(expected, await deserializer.ReadAsync(messageReader, type, readContext)); - } + MemoryStream stream = new MemoryStream(); + ODataMessageWrapper message = new ODataMessageWrapper(stream); - [Theory] - [MemberData(nameof(NonEdmPrimitiveData))] - public async Task ReadAsync_MappedPrimitive(object obj, object expected) + ODataMessageWriterSettings settings = new ODataMessageWriterSettings { - // Arrange - IEdmModel model = CreateModel(); - ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); - ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } + }; + settings.SetContentType(ODataFormat.Json); - MemoryStream stream = new MemoryStream(); - ODataMessageWrapper message = new ODataMessageWrapper(stream); + ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, model); + ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), model); + ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = model }; + ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model }; - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } - }; - settings.SetContentType(ODataFormat.Json); + Type type = obj == null ? typeof(int) : expected.GetType(); - ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, model); - ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), model); - ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = model }; - ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model }; + await serializer.WriteObjectAsync(obj, type, messageWriter, writeContext); + stream.Seek(0, SeekOrigin.Begin); - Type type = obj == null ? typeof(int) : expected.GetType(); + // Act && Assert + Assert.Equal(expected, await deserializer.ReadAsync(messageReader, type, readContext)); + } - await serializer.WriteObjectAsync(obj, type, messageWriter, writeContext); - stream.Seek(0, SeekOrigin.Begin); + /* + [Theory] + [MemberData(nameof(DateTimePrimitiveData))] + public void Read_DateTimePrimitive(DateTimeOffset expected, DateTime value, TimeZoneInfo timeZoneInfo) + { + // Arrange + IEdmModel model = CreateModel(); - // Act && Assert - Assert.Equal(expected, await deserializer.ReadAsync(messageReader, type, readContext)); + var config = RoutingConfigurationFactory.CreateWithRootContainer("OData"); + var request = RequestFactory.Create(config, "OData"); + if (timeZoneInfo != null) + { + config.SetTimeZoneInfo(timeZoneInfo); } - - /* - [Theory] - [MemberData(nameof(DateTimePrimitiveData))] - public void Read_DateTimePrimitive(DateTimeOffset expected, DateTime value, TimeZoneInfo timeZoneInfo) + else { - // Arrange - IEdmModel model = CreateModel(); - - var config = RoutingConfigurationFactory.CreateWithRootContainer("OData"); - var request = RequestFactory.Create(config, "OData"); - if (timeZoneInfo != null) - { - config.SetTimeZoneInfo(timeZoneInfo); - } - else - { - config.SetTimeZoneInfo(TimeZoneInfo.Local); - } + config.SetTimeZoneInfo(TimeZoneInfo.Local); + } - ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); - ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); + ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); + ODataPrimitiveDeserializer deserializer = new ODataPrimitiveDeserializer(); - MemoryStream stream = new MemoryStream(); - ODataMessageWrapper message = new ODataMessageWrapper(stream); + MemoryStream stream = new MemoryStream(); + ODataMessageWrapper message = new ODataMessageWrapper(stream); - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } - }; - settings.SetContentType(ODataFormat.Json); + ODataMessageWriterSettings settings = new ODataMessageWriterSettings + { + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } + }; + settings.SetContentType(ODataFormat.Json); - ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, model); - ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), model); - ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = model, Request = request }; - ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model, Request = request }; + ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, model); + ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), model); + ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = model, Request = request }; + ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model, Request = request }; - serializer.WriteObject(value, typeof(DateTimeOffset), messageWriter, writeContext); - stream.Seek(0, SeekOrigin.Begin); + serializer.WriteObject(value, typeof(DateTimeOffset), messageWriter, writeContext); + stream.Seek(0, SeekOrigin.Begin); - // Act && Assert - Assert.Equal(expected, deserializer.Read(messageReader, typeof(DateTimeOffset), readContext)); - } - */ - private static IEdmModel CreateModel() - { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } + // Act && Assert + Assert.Equal(expected, deserializer.Read(messageReader, typeof(DateTimeOffset), readContext)); + } + */ + private static IEdmModel CreateModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataResourceDeserializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataResourceDeserializerTests.cs index 652e086c3..8edab1348 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataResourceDeserializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataResourceDeserializerTests.cs @@ -34,1732 +34,1731 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class ODataResourceDeserializerTests { - public class ODataResourceDeserializerTests - { - private static IEdmModel _edmModel = GetEdmModel(); + private static IEdmModel _edmModel = GetEdmModel(); - private readonly ODataDeserializerContext _readContext; - private readonly IEdmEntityTypeReference _productEdmType; - private readonly IEdmEntityTypeReference _supplierEdmType; - private readonly IEdmComplexTypeReference _addressEdmType; - private readonly IODataDeserializerProvider _deserializerProvider; - private readonly string _supplyRequestResource; + private readonly ODataDeserializerContext _readContext; + private readonly IEdmEntityTypeReference _productEdmType; + private readonly IEdmEntityTypeReference _supplierEdmType; + private readonly IEdmComplexTypeReference _addressEdmType; + private readonly IODataDeserializerProvider _deserializerProvider; + private readonly string _supplyRequestResource; - public ODataResourceDeserializerTests() - { - IEdmEntitySet entitySet = _edmModel.EntityContainer.FindEntitySet("Products"); - _readContext = new ODataDeserializerContext + public ODataResourceDeserializerTests() + { + IEdmEntitySet entitySet = _edmModel.EntityContainer.FindEntitySet("Products"); + _readContext = new ODataDeserializerContext + { + Path = new ODataPath(new EntitySetSegment(entitySet)), + Model = _edmModel, + ResourceType = typeof(Product) + }; + _productEdmType = _edmModel.GetEdmTypeReference(typeof(Product)).AsEntity(); + _supplierEdmType = _edmModel.GetEdmTypeReference(typeof(Supplier)).AsEntity(); + _addressEdmType = _edmModel.GetEdmTypeReference(typeof(Address)).AsComplex(); + _deserializerProvider = ODataFormatterHelpers.GetDeserializerProvider(); + + _supplyRequestResource = @"{ + ""ID"":0, + ""Name"":""Supplier Name"", + ""Concurrency"":0, + ""Address"": { - Path = new ODataPath(new EntitySetSegment(entitySet)), - Model = _edmModel, - ResourceType = typeof(Product) - }; - _productEdmType = _edmModel.GetEdmTypeReference(typeof(Product)).AsEntity(); - _supplierEdmType = _edmModel.GetEdmTypeReference(typeof(Supplier)).AsEntity(); - _addressEdmType = _edmModel.GetEdmTypeReference(typeof(Address)).AsComplex(); - _deserializerProvider = ODataFormatterHelpers.GetDeserializerProvider(); - - _supplyRequestResource = @"{ - ""ID"":0, - ""Name"":""Supplier Name"", - ""Concurrency"":0, - ""Address"": + ""Street"":""Supplier Street"", + ""City"":""Supplier City"", + ""State"":""WA"", + ""ZipCode"":""123456"", + ""CountryOrRegion"":""USA"" + }, + Products: + [ { - ""Street"":""Supplier Street"", - ""City"":""Supplier City"", - ""State"":""WA"", - ""ZipCode"":""123456"", - ""CountryOrRegion"":""USA"" + ""ID"":1, + ""Name"":""Milk"", + ""Description"":""Low fat milk"", + ""ReleaseDate"":""1995-10-01T00:00:00z"", + ""DiscontinuedDate"":null, + ""Rating"":3, + ""Price"":3.5 }, - Products: - [ - { - ""ID"":1, - ""Name"":""Milk"", - ""Description"":""Low fat milk"", - ""ReleaseDate"":""1995-10-01T00:00:00z"", - ""DiscontinuedDate"":null, - ""Rating"":3, - ""Price"":3.5 - }, - { - ""ID"":2, - ""Name"":""soda"", - ""Description"":""sample summary"", - ""ReleaseDate"":""1995-10-01T00:00:00z"", - ""DiscontinuedDate"":null, - ""Rating"":3, - ""Price"":20.9 - }, - { - ""ID"":3, - ""Name"":""Product3"", - ""Description"":""Product3 Summary"", - ""ReleaseDate"":""1995-10-01T00:00:00z"", - ""DiscontinuedDate"":null, - ""Rating"":3, - ""Price"":19.9 - }, - { - ""ID"":4, - ""Name"":""Product4"", - ""Description"":""Product4 Summary"", - ""ReleaseDate"":""1995-10-01T00:00:00z"", - ""DiscontinuedDate"":null, - ""Rating"":3, - ""Price"":22.9 - }, - { - ""ID"":5, - ""Name"":""Product5"", - ""Description"":""Product5 Summary"", - ""ReleaseDate"":""1995-10-01T00:00:00z"", - ""DiscontinuedDate"":null, - ""Rating"":3, - ""Price"":22.8 - }, - { - ""ID"":6, - ""Name"":""Product6"", - ""Description"":""Product6 Summary"", - ""ReleaseDate"":""1995-10-01T00:00:00z"", - ""DiscontinuedDate"":null, - ""Rating"":3, - ""Price"":18.8 - } - ] - }"; - } + { + ""ID"":2, + ""Name"":""soda"", + ""Description"":""sample summary"", + ""ReleaseDate"":""1995-10-01T00:00:00z"", + ""DiscontinuedDate"":null, + ""Rating"":3, + ""Price"":20.9 + }, + { + ""ID"":3, + ""Name"":""Product3"", + ""Description"":""Product3 Summary"", + ""ReleaseDate"":""1995-10-01T00:00:00z"", + ""DiscontinuedDate"":null, + ""Rating"":3, + ""Price"":19.9 + }, + { + ""ID"":4, + ""Name"":""Product4"", + ""Description"":""Product4 Summary"", + ""ReleaseDate"":""1995-10-01T00:00:00z"", + ""DiscontinuedDate"":null, + ""Rating"":3, + ""Price"":22.9 + }, + { + ""ID"":5, + ""Name"":""Product5"", + ""Description"":""Product5 Summary"", + ""ReleaseDate"":""1995-10-01T00:00:00z"", + ""DiscontinuedDate"":null, + ""Rating"":3, + ""Price"":22.8 + }, + { + ""ID"":6, + ""Name"":""Product6"", + ""Description"":""Product6 Summary"", + ""ReleaseDate"":""1995-10-01T00:00:00z"", + ""DiscontinuedDate"":null, + ""Rating"":3, + ""Price"":18.8 + } + ] + }"; + } - [Fact] - public void Ctor_ThrowsArgumentNull_DeserializerProvider() - { - ExceptionAssert.ThrowsArgumentNull(() => new ODataResourceDeserializer(deserializerProvider: null), "deserializerProvider"); - } + [Fact] + public void Ctor_ThrowsArgumentNull_DeserializerProvider() + { + ExceptionAssert.ThrowsArgumentNull(() => new ODataResourceDeserializer(deserializerProvider: null), "deserializerProvider"); + } - [Fact] - public async Task ReadAsync_ThrowsArgumentNull_MessageReader() - { - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - await ExceptionAssert.ThrowsArgumentNullAsync( - () => deserializer.ReadAsync(messageReader: null, type: typeof(Product), readContext: _readContext), - "messageReader"); - } + [Fact] + public async Task ReadAsync_ThrowsArgumentNull_MessageReader() + { + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + await ExceptionAssert.ThrowsArgumentNullAsync( + () => deserializer.ReadAsync(messageReader: null, type: typeof(Product), readContext: _readContext), + "messageReader"); + } - [Fact] - public async Task ReadAsync_ThrowsArgumentNull_ReadContext() - { - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - await ExceptionAssert.ThrowsArgumentNullAsync( - () => deserializer.ReadAsync(messageReader: ODataFormatterHelpers.GetMockODataMessageReader(), type: typeof(Product), readContext: null), - "readContext"); - } + [Fact] + public async Task ReadAsync_ThrowsArgumentNull_ReadContext() + { + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + await ExceptionAssert.ThrowsArgumentNullAsync( + () => deserializer.ReadAsync(messageReader: ODataFormatterHelpers.GetMockODataMessageReader(), type: typeof(Product), readContext: null), + "readContext"); + } - [Fact] - public void ReadAsync_ThrowsArgument_ODataPathMissing_ForEntity() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _edmModel, - ResourceType = typeof(Product) - }; - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => deserializer.ReadAsync(ODataFormatterHelpers.GetMockODataMessageReader(), typeof(Product), readContext).Wait(), - "readContext", - "The operation cannot be completed because no ODataPath is available for the request."); - } + [Fact] + public void ReadAsync_ThrowsArgument_ODataPathMissing_ForEntity() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataDeserializerContext readContext = new ODataDeserializerContext + { + Model = _edmModel, + ResourceType = typeof(Product) + }; + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => deserializer.ReadAsync(ODataFormatterHelpers.GetMockODataMessageReader(), typeof(Product), readContext).Wait(), + "readContext", + "The operation cannot be completed because no ODataPath is available for the request."); + } - [Fact] - public async Task ReadAsync_ThrowsArgument_EntitysetMissing() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Path = new ODataPath(), - Model = _edmModel, - ResourceType = typeof(Product) - }; - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => deserializer.ReadAsync(ODataFormatterHelpers.GetMockODataMessageReader(), typeof(Product), readContext), - "The related entity set or singleton cannot be found from the OData path. The related entity set or singleton is required to deserialize the payload."); - } + [Fact] + public async Task ReadAsync_ThrowsArgument_EntitysetMissing() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataDeserializerContext readContext = new ODataDeserializerContext + { + Path = new ODataPath(), + Model = _edmModel, + ResourceType = typeof(Product) + }; + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => deserializer.ReadAsync(ODataFormatterHelpers.GetMockODataMessageReader(), typeof(Product), readContext), + "The related entity set or singleton cannot be found from the OData path. The related entity set or singleton is required to deserialize the payload."); + } - [Fact] - public void ReadInline_ThrowsArgumentNull_Item() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); + [Fact] + public void ReadInline_ThrowsArgumentNull_Item() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.ReadInline(item: null, edmType: _productEdmType, readContext: new ODataDeserializerContext()), - "item"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ReadInline(item: null, edmType: _productEdmType, readContext: new ODataDeserializerContext()), + "item"); + } - [Fact] - public void ReadInline_ThrowsArgumentNull_EdmType() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); + [Fact] + public void ReadInline_ThrowsArgumentNull_EdmType() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.ReadInline(item: new object(), edmType: null, readContext: new ODataDeserializerContext()), - "edmType"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ReadInline(item: new object(), edmType: null, readContext: new ODataDeserializerContext()), + "edmType"); + } - [Fact] - public void ReadInline_Throws_ArgumentMustBeOfType() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => deserializer.ReadInline(item: 42, edmType: _productEdmType, readContext: new ODataDeserializerContext()), - "item", - "The argument must be of type 'ODataResource'"); - } + [Fact] + public void ReadInline_Throws_ArgumentMustBeOfType() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => deserializer.ReadInline(item: 42, edmType: _productEdmType, readContext: new ODataDeserializerContext()), + "item", + "The argument must be of type 'ODataResource'"); + } - [Fact] - public void ReadInline_Calls_ReadResource() - { - // Arrange - var deserializer = new Mock(_deserializerProvider); - ODataResourceWrapper entry = new ODataResourceWrapper(new ODataResource()); - ODataDeserializerContext readContext = new ODataDeserializerContext(); + [Fact] + public void ReadInline_Calls_ReadResource() + { + // Arrange + var deserializer = new Mock(_deserializerProvider); + ODataResourceWrapper entry = new ODataResourceWrapper(new ODataResource()); + ODataDeserializerContext readContext = new ODataDeserializerContext(); - deserializer.CallBase = true; - deserializer.Setup(d => d.ReadResource(entry, _productEdmType, readContext)).Returns(42).Verifiable(); + deserializer.CallBase = true; + deserializer.Setup(d => d.ReadResource(entry, _productEdmType, readContext)).Returns(42).Verifiable(); - // Act - var result = deserializer.Object.ReadInline(entry, _productEdmType, readContext); + // Act + var result = deserializer.Object.ReadInline(entry, _productEdmType, readContext); - // Assert - deserializer.Verify(); - Assert.Equal(42, result); - } + // Assert + deserializer.Verify(); + Assert.Equal(42, result); + } - [Fact] - public void ReadResource_ThrowsArgumentNull_ResourceWrapper() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); + [Fact] + public void ReadResource_ThrowsArgumentNull_ResourceWrapper() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.ReadResource(resourceWrapper: null, structuredType: _productEdmType, readContext: _readContext), - "resourceWrapper"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ReadResource(resourceWrapper: null, structuredType: _productEdmType, readContext: _readContext), + "resourceWrapper"); + } - [Fact] - public void ReadResource_ThrowsArgumentNull_ReadContext() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource()); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.ReadResource(resourceWrapper, structuredType: _productEdmType, readContext: null), - "readContext"); - } + [Fact] + public void ReadResource_ThrowsArgumentNull_ReadContext() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource()); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ReadResource(resourceWrapper, structuredType: _productEdmType, readContext: null), + "readContext"); + } - [Fact] - public void ReadResource_ThrowsArgument_ModelMissingFromReadContext() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { TypeName = _supplierEdmType.FullName() }); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => deserializer.ReadResource(resourceWrapper, _productEdmType, new ODataDeserializerContext()), - "readContext", - "The EDM model is missing on the read context. The model is required on the read context to deserialize the payload."); - } + [Fact] + public void ReadResource_ThrowsArgument_ModelMissingFromReadContext() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { TypeName = _supplierEdmType.FullName() }); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => deserializer.ReadResource(resourceWrapper, _productEdmType, new ODataDeserializerContext()), + "readContext", + "The EDM model is missing on the read context. The model is required on the read context to deserialize the payload."); + } - [Fact] - public void ReadResource_ThrowsODataException_EntityTypeNotInModel() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataResourceWrapper entry = new ODataResourceWrapper(new ODataResource { TypeName = "MissingType" }); - - // Act & Assert - ExceptionAssert.Throws( - () => deserializer.ReadResource(entry, _productEdmType, _readContext), - "Cannot find the resource type 'MissingType' in the model."); - } + [Fact] + public void ReadResource_ThrowsODataException_EntityTypeNotInModel() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataResourceWrapper entry = new ODataResourceWrapper(new ODataResource { TypeName = "MissingType" }); + + // Act & Assert + ExceptionAssert.Throws( + () => deserializer.ReadResource(entry, _productEdmType, _readContext), + "Cannot find the resource type 'MissingType' in the model."); + } - [Fact] - public void ReadResource_ThrowsODataException_CannotInstantiateAbstractResourceType() - { - // Arrange - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntityType().Abstract(); - IEdmModel model = builder.GetEdmModel(); - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataResourceWrapper resourceWrapper = - new ODataResourceWrapper(new ODataResource - { - TypeName = "Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.BaseType" - }); + [Fact] + public void ReadResource_ThrowsODataException_CannotInstantiateAbstractResourceType() + { + // Arrange + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntityType().Abstract(); + IEdmModel model = builder.GetEdmModel(); + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataResourceWrapper resourceWrapper = + new ODataResourceWrapper(new ODataResource + { + TypeName = "Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.BaseType" + }); - // Act & Assert - ExceptionAssert.Throws( - () => deserializer.ReadResource(resourceWrapper, _productEdmType, new ODataDeserializerContext { Model = model }), - "An instance of the abstract resource type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.BaseType' was found. " + - "Abstract resource types cannot be instantiated."); - } + // Act & Assert + ExceptionAssert.Throws( + () => deserializer.ReadResource(resourceWrapper, _productEdmType, new ODataDeserializerContext { Model = model }), + "An instance of the abstract resource type 'Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.BaseType' was found. " + + "Abstract resource types cannot be instantiated."); + } - [Fact] - public void ReadResource_ThrowsSerializationException_TypeCannotBeDeserialized() - { - // Arrange - Mock deserializerProvider = new Mock(); - deserializerProvider.Setup(d => d.GetEdmTypeDeserializer(It.IsAny(), false)).Returns(null); - var deserializer = new ODataResourceDeserializer(deserializerProvider.Object); - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { TypeName = _supplierEdmType.FullName() }); - - // Act & Assert - ExceptionAssert.Throws( - () => deserializer.ReadResource(resourceWrapper, _productEdmType, _readContext), - "'ODataDemo.Supplier' cannot be deserialized using the OData input formatter."); - } + [Fact] + public void ReadResource_ThrowsSerializationException_TypeCannotBeDeserialized() + { + // Arrange + Mock deserializerProvider = new Mock(); + deserializerProvider.Setup(d => d.GetEdmTypeDeserializer(It.IsAny(), false)).Returns(null); + var deserializer = new ODataResourceDeserializer(deserializerProvider.Object); + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { TypeName = _supplierEdmType.FullName() }); + + // Act & Assert + ExceptionAssert.Throws( + () => deserializer.ReadResource(resourceWrapper, _productEdmType, _readContext), + "'ODataDemo.Supplier' cannot be deserialized using the OData input formatter."); + } - [Fact] - public void ReadResource_DispatchesToRightDeserializer_IfEntityTypeNameIsDifferent() - { - // Arrange - Mock supplierDeserializer = new Mock(ODataPayloadKind.Resource); - Mock deserializerProvider = new Mock(); - var deserializer = new ODataResourceDeserializer(deserializerProvider.Object); - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { TypeName = _supplierEdmType.FullName() }); - - deserializerProvider.Setup(d => d.GetEdmTypeDeserializer(It.IsAny(), false)).Returns(supplierDeserializer.Object); - supplierDeserializer - .Setup(d => d.ReadInline(resourceWrapper, It.Is(e => _supplierEdmType.Definition == e.Definition), _readContext)) - .Returns(42).Verifiable(); - - // Act - object result = deserializer.ReadResource(resourceWrapper, _productEdmType, _readContext); - - // Assert - supplierDeserializer.Verify(); - Assert.Equal(42, result); - } + [Fact] + public void ReadResource_DispatchesToRightDeserializer_IfEntityTypeNameIsDifferent() + { + // Arrange + Mock supplierDeserializer = new Mock(ODataPayloadKind.Resource); + Mock deserializerProvider = new Mock(); + var deserializer = new ODataResourceDeserializer(deserializerProvider.Object); + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { TypeName = _supplierEdmType.FullName() }); + + deserializerProvider.Setup(d => d.GetEdmTypeDeserializer(It.IsAny(), false)).Returns(supplierDeserializer.Object); + supplierDeserializer + .Setup(d => d.ReadInline(resourceWrapper, It.Is(e => _supplierEdmType.Definition == e.Definition), _readContext)) + .Returns(42).Verifiable(); + + // Act + object result = deserializer.ReadResource(resourceWrapper, _productEdmType, _readContext); + + // Assert + supplierDeserializer.Verify(); + Assert.Equal(42, result); + } - /* - [Fact] - public void ReadResource_SetsExpectedAndActualEdmType_OnCreatedEdmObject_TypelessMode() + /* + [Fact] + public void ReadResource_SetsExpectedAndActualEdmType_OnCreatedEdmObject_TypelessMode() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + IEdmEntityTypeReference customerType = EdmLibHelpers.ToEdmTypeReference(model.Customer, isNullable: false).AsEntity(); + ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model.Model, ResourceType = typeof(IEdmObject) }; + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - IEdmEntityTypeReference customerType = EdmLibHelpers.ToEdmTypeReference(model.Customer, isNullable: false).AsEntity(); - ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model.Model, ResourceType = typeof(IEdmObject) }; - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource - { - TypeName = model.SpecialCustomer.FullName(), - Properties = new ODataProperty[0] - }); + TypeName = model.SpecialCustomer.FullName(), + Properties = new ODataProperty[0] + }); - ODataResourceDeserializer deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataResourceDeserializer deserializer = new ODataResourceDeserializer(_deserializerProvider); - // Act - var result = deserializer.ReadResource(resourceWrapper, customerType, readContext); + // Act + var result = deserializer.ReadResource(resourceWrapper, customerType, readContext); - // Assert - EdmEntityObject resource = Assert.IsType(result); - Assert.Equal(model.SpecialCustomer, resource.ActualEdmType); - Assert.Equal(model.Customer, resource.ExpectedEdmType); - }*/ + // Assert + EdmEntityObject resource = Assert.IsType(result); + Assert.Equal(model.SpecialCustomer, resource.ActualEdmType); + Assert.Equal(model.Customer, resource.ExpectedEdmType); + }*/ - [Fact] - public void ReadResource_Calls_CreateResourceInstance() - { - // Arrange - Mock deserializer = new Mock(_deserializerProvider); - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { Properties = Enumerable.Empty() }); - deserializer.CallBase = true; - deserializer.Setup(d => d.CreateResourceInstance(_productEdmType, _readContext)).Returns(42).Verifiable(); - - // Act - var result = deserializer.Object.ReadResource(resourceWrapper, _productEdmType, _readContext); - - // Assert - Assert.Equal(42, result); - deserializer.Verify(); - } + [Fact] + public void ReadResource_Calls_CreateResourceInstance() + { + // Arrange + Mock deserializer = new Mock(_deserializerProvider); + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { Properties = Enumerable.Empty() }); + deserializer.CallBase = true; + deserializer.Setup(d => d.CreateResourceInstance(_productEdmType, _readContext)).Returns(42).Verifiable(); + + // Act + var result = deserializer.Object.ReadResource(resourceWrapper, _productEdmType, _readContext); + + // Assert + Assert.Equal(42, result); + deserializer.Verify(); + } - [Fact] - public void ReadResource_Calls_ApplyStructuralProperties() - { - // Arrange - Mock deserializer = new Mock(_deserializerProvider); - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { Properties = Enumerable.Empty() }); - deserializer.CallBase = true; - deserializer.Setup(d => d.CreateResourceInstance(_productEdmType, _readContext)).Returns(42); - deserializer.Setup(d => d.ApplyStructuralProperties(42, resourceWrapper, _productEdmType, _readContext)).Verifiable(); - - // Act - deserializer.Object.ReadResource(resourceWrapper, _productEdmType, _readContext); - - // Assert - deserializer.Verify(); - } + [Fact] + public void ReadResource_Calls_ApplyStructuralProperties() + { + // Arrange + Mock deserializer = new Mock(_deserializerProvider); + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { Properties = Enumerable.Empty() }); + deserializer.CallBase = true; + deserializer.Setup(d => d.CreateResourceInstance(_productEdmType, _readContext)).Returns(42); + deserializer.Setup(d => d.ApplyStructuralProperties(42, resourceWrapper, _productEdmType, _readContext)).Verifiable(); + + // Act + deserializer.Object.ReadResource(resourceWrapper, _productEdmType, _readContext); + + // Assert + deserializer.Verify(); + } - [Fact] - public void ReadResource_Calls_ApplyNestedProperties() - { - // Arrange - Mock deserializer = new Mock(_deserializerProvider); - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { Properties = Enumerable.Empty() }); - deserializer.CallBase = true; - deserializer.Setup(d => d.CreateResourceInstance(_productEdmType, _readContext)).Returns(42); - deserializer.Setup(d => d.ApplyNestedProperties(42, resourceWrapper, _productEdmType, _readContext)).Verifiable(); - - // Act - deserializer.Object.ReadResource(resourceWrapper, _productEdmType, _readContext); - - // Assert - deserializer.Verify(); - } + [Fact] + public void ReadResource_Calls_ApplyNestedProperties() + { + // Arrange + Mock deserializer = new Mock(_deserializerProvider); + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { Properties = Enumerable.Empty() }); + deserializer.CallBase = true; + deserializer.Setup(d => d.CreateResourceInstance(_productEdmType, _readContext)).Returns(42); + deserializer.Setup(d => d.ApplyNestedProperties(42, resourceWrapper, _productEdmType, _readContext)).Verifiable(); + + // Act + deserializer.Object.ReadResource(resourceWrapper, _productEdmType, _readContext); + + // Assert + deserializer.Verify(); + } - [Fact] - public void ReadResource_CanReadDynamicPropertiesForOpenEntityType() - { - // Arrange - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntityType(); - builder.EnumType(); - IEdmModel model = builder.GetEdmModel(); + [Fact] + public void ReadResource_CanReadDynamicPropertiesForOpenEntityType() + { + // Arrange + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntityType(); + builder.EnumType(); + IEdmModel model = builder.GetEdmModel(); - IEdmEntityTypeReference customerTypeReference = model.GetEdmTypeReference(typeof(SimpleOpenCustomer)).AsEntity(); + IEdmEntityTypeReference customerTypeReference = model.GetEdmTypeReference(typeof(SimpleOpenCustomer)).AsEntity(); - var deserializer = new ODataResourceDeserializer(_deserializerProvider); + var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataEnumValue enumValue = new ODataEnumValue("Third", typeof(SimpleEnum).FullName); + ODataEnumValue enumValue = new ODataEnumValue("Third", typeof(SimpleEnum).FullName); - ODataResource[] complexResources = + ODataResource[] complexResources = + { + new ODataResource { - new ODataResource - { - TypeName = typeof(SimpleOpenAddress).FullName, - Properties = new[] - { - // declared properties - new ODataProperty {Name = "Street", Value = "Street 1"}, - new ODataProperty {Name = "City", Value = "City 1"}, - - // dynamic properties - new ODataProperty - { - Name = "DateTimeProperty", - Value = new DateTimeOffset(new DateTime(2014, 5, 6)) - } - } - }, - new ODataResource + TypeName = typeof(SimpleOpenAddress).FullName, + Properties = new[] { - TypeName = typeof(SimpleOpenAddress).FullName, - Properties = new[] + // declared properties + new ODataProperty {Name = "Street", Value = "Street 1"}, + new ODataProperty {Name = "City", Value = "City 1"}, + + // dynamic properties + new ODataProperty { - // declared properties - new ODataProperty { Name = "Street", Value = "Street 2" }, - new ODataProperty { Name = "City", Value = "City 2" }, - - // dynamic properties - new ODataProperty - { - Name = "ArrayProperty", - Value = new ODataCollectionValue { TypeName = "Collection(Edm.Int32)", Items = new[] {1, 2, 3, 4}.Cast() } - } + Name = "DateTimeProperty", + Value = new DateTimeOffset(new DateTime(2014, 5, 6)) } } - }; - - ODataResource odataResource = new ODataResource + }, + new ODataResource { + TypeName = typeof(SimpleOpenAddress).FullName, Properties = new[] { // declared properties - new ODataProperty { Name = "CustomerId", Value = 991 }, - new ODataProperty { Name = "Name", Value = "Name #991" }, + new ODataProperty { Name = "Street", Value = "Street 2" }, + new ODataProperty { Name = "City", Value = "City 2" }, // dynamic properties - new ODataProperty { Name = "GuidProperty", Value = new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA") }, - new ODataProperty { Name = "EnumValue", Value = enumValue }, - }, - TypeName = typeof(SimpleOpenCustomer).FullName - }; + new ODataProperty + { + Name = "ArrayProperty", + Value = new ODataCollectionValue { TypeName = "Collection(Edm.Int32)", Items = new[] {1, 2, 3, 4}.Cast() } + } + } + } + }; - ODataDeserializerContext readContext = new ODataDeserializerContext() + ODataResource odataResource = new ODataResource + { + Properties = new[] { - Model = model - }; + // declared properties + new ODataProperty { Name = "CustomerId", Value = 991 }, + new ODataProperty { Name = "Name", Value = "Name #991" }, - ODataResourceWrapper topLevelResourceWrapper = new ODataResourceWrapper(odataResource); + // dynamic properties + new ODataProperty { Name = "GuidProperty", Value = new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA") }, + new ODataProperty { Name = "EnumValue", Value = enumValue }, + }, + TypeName = typeof(SimpleOpenCustomer).FullName + }; - ODataNestedResourceInfo resourceInfo = new ODataNestedResourceInfo - { - IsCollection = true, - Name = "CollectionProperty" - }; - ODataNestedResourceInfoWrapper resourceInfoWrapper = new ODataNestedResourceInfoWrapper(resourceInfo); - ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet - { - TypeName = String.Format("Collection({0})", typeof(SimpleOpenAddress).FullName) - }); - foreach (var complexResource in complexResources) - { - resourceSetWrapper.Resources.Add(new ODataResourceWrapper(complexResource)); - } - resourceInfoWrapper.NestedItems.Add(resourceSetWrapper); - topLevelResourceWrapper.NestedResourceInfos.Add(resourceInfoWrapper); + ODataDeserializerContext readContext = new ODataDeserializerContext() + { + Model = model + }; - // Act - SimpleOpenCustomer customer = deserializer.ReadResource(topLevelResourceWrapper, customerTypeReference, readContext) - as SimpleOpenCustomer; + ODataResourceWrapper topLevelResourceWrapper = new ODataResourceWrapper(odataResource); - // Assert - Assert.NotNull(customer); + ODataNestedResourceInfo resourceInfo = new ODataNestedResourceInfo + { + IsCollection = true, + Name = "CollectionProperty" + }; + ODataNestedResourceInfoWrapper resourceInfoWrapper = new ODataNestedResourceInfoWrapper(resourceInfo); + ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet + { + TypeName = String.Format("Collection({0})", typeof(SimpleOpenAddress).FullName) + }); + foreach (var complexResource in complexResources) + { + resourceSetWrapper.Resources.Add(new ODataResourceWrapper(complexResource)); + } + resourceInfoWrapper.NestedItems.Add(resourceSetWrapper); + topLevelResourceWrapper.NestedResourceInfos.Add(resourceInfoWrapper); - // Verify the declared properties - Assert.Equal(991, customer.CustomerId); - Assert.Equal("Name #991", customer.Name); + // Act + SimpleOpenCustomer customer = deserializer.ReadResource(topLevelResourceWrapper, customerTypeReference, readContext) + as SimpleOpenCustomer; - // Verify the dynamic properties - Assert.NotNull(customer.CustomerProperties); - Assert.Equal(3, customer.CustomerProperties.Count()); - Assert.Equal(new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA"), customer.CustomerProperties["GuidProperty"]); - Assert.Equal(SimpleEnum.Third, customer.CustomerProperties["EnumValue"]); + // Assert + Assert.NotNull(customer); - // Verify the dynamic collection property - var collectionValues = Assert.IsType>(customer.CustomerProperties["CollectionProperty"]); - Assert.NotNull(collectionValues); - Assert.Equal(2, collectionValues.Count()); + // Verify the declared properties + Assert.Equal(991, customer.CustomerId); + Assert.Equal("Name #991", customer.Name); - Assert.Equal(new DateTimeOffset(new DateTime(2014, 5, 6)), collectionValues[0].Properties["DateTimeProperty"]); - Assert.Equal(new List { 1, 2, 3, 4 }, collectionValues[1].Properties["ArrayProperty"]); - } + // Verify the dynamic properties + Assert.NotNull(customer.CustomerProperties); + Assert.Equal(3, customer.CustomerProperties.Count()); + Assert.Equal(new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA"), customer.CustomerProperties["GuidProperty"]); + Assert.Equal(SimpleEnum.Third, customer.CustomerProperties["EnumValue"]); - [Fact] - public void ReadSource_CanReadDynamicPropertiesForInheritanceOpenEntityType() - { - // Arrange - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntityType(); - builder.EnumType(); - IEdmModel model = builder.GetEdmModel(); + // Verify the dynamic collection property + var collectionValues = Assert.IsType>(customer.CustomerProperties["CollectionProperty"]); + Assert.NotNull(collectionValues); + Assert.Equal(2, collectionValues.Count()); - IEdmEntityTypeReference vipCustomerTypeReference = model.GetEdmTypeReference(typeof(SimpleVipCustomer)).AsEntity(); + Assert.Equal(new DateTimeOffset(new DateTime(2014, 5, 6)), collectionValues[0].Properties["DateTimeProperty"]); + Assert.Equal(new List { 1, 2, 3, 4 }, collectionValues[1].Properties["ArrayProperty"]); + } - var deserializer = new ODataResourceDeserializer(_deserializerProvider); + [Fact] + public void ReadSource_CanReadDynamicPropertiesForInheritanceOpenEntityType() + { + // Arrange + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntityType(); + builder.EnumType(); + IEdmModel model = builder.GetEdmModel(); - ODataResource resource = new ODataResource - { - Properties = new[] - { - // declared properties - new ODataProperty { Name = "CustomerId", Value = 121 }, - new ODataProperty { Name = "Name", Value = "VipName #121" }, - new ODataProperty { Name = "VipNum", Value = "Vip Num 001" }, + IEdmEntityTypeReference vipCustomerTypeReference = model.GetEdmTypeReference(typeof(SimpleVipCustomer)).AsEntity(); - // dynamic properties - new ODataProperty { Name = "GuidProperty", Value = new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA") }, - }, - TypeName = typeof(SimpleVipCustomer).FullName - }; + var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataDeserializerContext readContext = new ODataDeserializerContext() + ODataResource resource = new ODataResource + { + Properties = new[] { - Model = model - }; + // declared properties + new ODataProperty { Name = "CustomerId", Value = 121 }, + new ODataProperty { Name = "Name", Value = "VipName #121" }, + new ODataProperty { Name = "VipNum", Value = "Vip Num 001" }, - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(resource); + // dynamic properties + new ODataProperty { Name = "GuidProperty", Value = new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA") }, + }, + TypeName = typeof(SimpleVipCustomer).FullName + }; - // Act - SimpleVipCustomer customer = deserializer.ReadResource(resourceWrapper, vipCustomerTypeReference, readContext) - as SimpleVipCustomer; + ODataDeserializerContext readContext = new ODataDeserializerContext() + { + Model = model + }; - // Assert - Assert.NotNull(customer); + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(resource); - // Verify the declared properties - Assert.Equal(121, customer.CustomerId); - Assert.Equal("VipName #121", customer.Name); - Assert.Equal("Vip Num 001", customer.VipNum); + // Act + SimpleVipCustomer customer = deserializer.ReadResource(resourceWrapper, vipCustomerTypeReference, readContext) + as SimpleVipCustomer; - // Verify the dynamic properties - Assert.NotNull(customer.CustomerProperties); - Assert.Single(customer.CustomerProperties); - Assert.Equal(new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA"), customer.CustomerProperties["GuidProperty"]); - } + // Assert + Assert.NotNull(customer); - public class MyCustomer - { - public int Id { get; set; } + // Verify the declared properties + Assert.Equal(121, customer.CustomerId); + Assert.Equal("VipName #121", customer.Name); + Assert.Equal("Vip Num 001", customer.VipNum); + + // Verify the dynamic properties + Assert.NotNull(customer.CustomerProperties); + Assert.Single(customer.CustomerProperties); + Assert.Equal(new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA"), customer.CustomerProperties["GuidProperty"]); + } - [Column(TypeName = "date")] - public DateTime Birthday { get; set; } + public class MyCustomer + { + public int Id { get; set; } - [Column(TypeName = "time")] - public TimeSpan ReleaseTime { get; set; } - } + [Column(TypeName = "date")] + public DateTime Birthday { get; set; } - [Fact] - public void ReadResource_CanReadDatTimeRelatedProperties() - { - // Arrange - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntityType().Namespace = "NS"; - IEdmModel model = builder.GetEdmModel(); + [Column(TypeName = "time")] + public TimeSpan ReleaseTime { get; set; } + } + + [Fact] + public void ReadResource_CanReadDatTimeRelatedProperties() + { + // Arrange + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntityType().Namespace = "NS"; + IEdmModel model = builder.GetEdmModel(); - IEdmEntityTypeReference vipCustomerTypeReference = model.GetEdmTypeReference(typeof(MyCustomer)).AsEntity(); + IEdmEntityTypeReference vipCustomerTypeReference = model.GetEdmTypeReference(typeof(MyCustomer)).AsEntity(); - var deserializer = new ODataResourceDeserializer(_deserializerProvider); + var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataResource resource = new ODataResource + ODataResource resource = new ODataResource + { + Properties = new[] { - Properties = new[] - { - new ODataProperty { Name = "Id", Value = 121 }, - new ODataProperty { Name = "Birthday", Value = new Date(2015, 12, 12) }, - new ODataProperty { Name = "ReleaseTime", Value = new TimeOfDay(1, 2, 3, 4) }, - }, - TypeName = "NS.MyCustomer" - }; + new ODataProperty { Name = "Id", Value = 121 }, + new ODataProperty { Name = "Birthday", Value = new Date(2015, 12, 12) }, + new ODataProperty { Name = "ReleaseTime", Value = new TimeOfDay(1, 2, 3, 4) }, + }, + TypeName = "NS.MyCustomer" + }; + + ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model }; + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(resource); + + // Act + var customer = deserializer.ReadResource(resourceWrapper, vipCustomerTypeReference, readContext) as MyCustomer; + + // Assert + Assert.NotNull(customer); + Assert.Equal(121, customer.Id); + Assert.Equal(new DateTime(2015, 12, 12), customer.Birthday); + Assert.Equal(new TimeSpan(0, 1, 2, 3, 4), customer.ReleaseTime); + } - ODataDeserializerContext readContext = new ODataDeserializerContext { Model = model }; - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(resource); + [Fact] + public void CreateResourceInstance_ThrowsArgumentNull_ReadContext() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); - // Act - var customer = deserializer.ReadResource(resourceWrapper, vipCustomerTypeReference, readContext) as MyCustomer; + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.CreateResourceInstance(_productEdmType, readContext: null), + "readContext"); + } - // Assert - Assert.NotNull(customer); - Assert.Equal(121, customer.Id); - Assert.Equal(new DateTime(2015, 12, 12), customer.Birthday); - Assert.Equal(new TimeSpan(0, 1, 2, 3, 4), customer.ReleaseTime); - } + [Fact] + public void CreateResourceInstance_ThrowsArgument_ModelMissingFromReadContext() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => deserializer.CreateResourceInstance(_productEdmType, new ODataDeserializerContext()), + "readContext", + "The EDM model is missing on the read context. The model is required on the read context to deserialize the payload."); + } - [Fact] - public void CreateResourceInstance_ThrowsArgumentNull_ReadContext() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); + [Fact] + public void CreateResourceInstance_ThrowsODataException_MappingDoesNotContainEntityType() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.CreateResourceInstance(_productEdmType, readContext: null), - "readContext"); - } + // Act & Assert + ExceptionAssert.Throws( + () => deserializer.CreateResourceInstance(_productEdmType, new ODataDeserializerContext { Model = EdmCoreModel.Instance }), + "The provided mapping does not contain a resource for the resource type 'ODataDemo.Product'."); + } - [Fact] - public void CreateResourceInstance_ThrowsArgument_ModelMissingFromReadContext() + [Fact] + public void CreateResourceInstance_CreatesEdmUntypedObject_IfUntypedStructured() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + IEdmStructuredTypeReference unTypedStructuredTypeRef + = EdmUntypedStructuredTypeReference.NullableTypeReference; + ODataDeserializerContext readContext = new ODataDeserializerContext { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => deserializer.CreateResourceInstance(_productEdmType, new ODataDeserializerContext()), - "readContext", - "The EDM model is missing on the read context. The model is required on the read context to deserialize the payload."); - } + Model = _readContext.Model, + }; + + // Act & Assert + Assert.IsType(deserializer.CreateResourceInstance(unTypedStructuredTypeRef, readContext)); + } - [Fact] - public void CreateResourceInstance_ThrowsODataException_MappingDoesNotContainEntityType() + [Fact] + public void CreateResourceInstance_CreatesDeltaOfT_IfPatchMode() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataDeserializerContext readContext = new ODataDeserializerContext { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); + Model = _readContext.Model, + ResourceType = typeof(Delta) + }; - // Act & Assert - ExceptionAssert.Throws( - () => deserializer.CreateResourceInstance(_productEdmType, new ODataDeserializerContext { Model = EdmCoreModel.Instance }), - "The provided mapping does not contain a resource for the resource type 'ODataDemo.Product'."); - } + // Act & Assert + Assert.IsType>(deserializer.CreateResourceInstance(_productEdmType, readContext)); + } - [Fact] - public void CreateResourceInstance_CreatesEdmUntypedObject_IfUntypedStructured() + [Fact] + public void CreateResourceInstance_CreatesDeltaWith_ExpectedUpdatableProperties() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataDeserializerContext readContext = new ODataDeserializerContext { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - IEdmStructuredTypeReference unTypedStructuredTypeRef - = EdmUntypedStructuredTypeReference.NullableTypeReference; - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _readContext.Model, - }; + Model = _readContext.Model, + ResourceType = typeof(Delta) + }; + var structuralProperties = _readContext.Model.GetAllProperties(_productEdmType.StructuredDefinition()); - // Act & Assert - Assert.IsType(deserializer.CreateResourceInstance(unTypedStructuredTypeRef, readContext)); - } + // Act + Delta resource = deserializer.CreateResourceInstance(_productEdmType, readContext) as Delta; - [Fact] - public void CreateResourceInstance_CreatesDeltaOfT_IfPatchMode() + // Assert + Assert.NotNull(resource); + Assert.Equal(structuralProperties, resource.GetUnchangedPropertyNames()); + } + + [Fact] + public void CreateResourceInstance_CreatesEdmEntityObject_IfTypeLessMode() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataDeserializerContext readContext = new ODataDeserializerContext { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _readContext.Model, - ResourceType = typeof(Delta) - }; + Model = _readContext.Model, + ResourceType = typeof(IEdmObject) + }; - // Act & Assert - Assert.IsType>(deserializer.CreateResourceInstance(_productEdmType, readContext)); - } + // Act + var result = deserializer.CreateResourceInstance(_productEdmType, readContext); + + // Assert + EdmEntityObject resource = Assert.IsType(result); + Assert.Equal(_productEdmType, resource.GetEdmType(), new EdmTypeReferenceEqualityComparer()); + } - [Fact] - public void CreateResourceInstance_CreatesDeltaWith_ExpectedUpdatableProperties() + [Fact] + public void CreateResourceInstance_CreatesEdmComplexObject_IfTypeLessMode() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataDeserializerContext readContext = new ODataDeserializerContext { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _readContext.Model, - ResourceType = typeof(Delta) - }; - var structuralProperties = _readContext.Model.GetAllProperties(_productEdmType.StructuredDefinition()); + Model = _readContext.Model, + ResourceType = typeof(IEdmObject) + }; - // Act - Delta resource = deserializer.CreateResourceInstance(_productEdmType, readContext) as Delta; + // Act + var result = deserializer.CreateResourceInstance(_addressEdmType, readContext); - // Assert - Assert.NotNull(resource); - Assert.Equal(structuralProperties, resource.GetUnchangedPropertyNames()); - } + // Assert + EdmComplexObject resource = Assert.IsType(result); + Assert.Equal(_addressEdmType, resource.GetEdmType(), new EdmTypeReferenceEqualityComparer()); + } - [Fact] - public void CreateResourceInstance_CreatesEdmEntityObject_IfTypeLessMode() + [Fact] + public void CreateResourceInstance_CreatesT_IfNotPatchMode() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataDeserializerContext readContext = new ODataDeserializerContext { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _readContext.Model, - ResourceType = typeof(IEdmObject) - }; + Model = _readContext.Model, + ResourceType = typeof(Product) + }; - // Act - var result = deserializer.CreateResourceInstance(_productEdmType, readContext); + // Act & Assert + Assert.IsType(deserializer.CreateResourceInstance(_productEdmType, readContext)); + } - // Assert - EdmEntityObject resource = Assert.IsType(result); - Assert.Equal(_productEdmType, resource.GetEdmType(), new EdmTypeReferenceEqualityComparer()); - } + [Fact] + public void ApplyDeletedResource_ThrowsArgumentNull_ResourceWrapper() + { + // Arrange & Act & Assert + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ApplyDeletedResource(null, null, null), "resourceWrapper"); + } - [Fact] - public void CreateResourceInstance_CreatesEdmComplexObject_IfTypeLessMode() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _readContext.Model, - ResourceType = typeof(IEdmObject) - }; + [Fact] + public void ApplyDeletedResource_ThrowsArgumentNull_ReadContext() + { + // Arrange & Act & Assert + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataResourceWrapper wrapper = new ODataResourceWrapper(new ODataResource()); + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ApplyDeletedResource(null, wrapper, null), "readContext"); + } - // Act - var result = deserializer.CreateResourceInstance(_addressEdmType, readContext); + [Fact] + public void ApplyNestedProperties_ThrowsArgumentNull_EntryWrapper() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); - // Assert - EdmComplexObject resource = Assert.IsType(result); - Assert.Equal(_addressEdmType, resource.GetEdmType(), new EdmTypeReferenceEqualityComparer()); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ApplyNestedProperties(42, resourceWrapper: null, structuredType: _productEdmType, readContext: _readContext), + "resourceWrapper"); + } - [Fact] - public void CreateResourceInstance_CreatesT_IfNotPatchMode() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _readContext.Model, - ResourceType = typeof(Product) - }; + [Fact] + public void ApplyNestedProperties_Calls_ApplyNavigationPropertyForEachNavigationLink() + { + // Arrange + ODataResourceWrapper resource = new ODataResourceWrapper(new ODataResource()); + resource.NestedResourceInfos.Add(new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo())); + resource.NestedResourceInfos.Add(new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo())); - // Act & Assert - Assert.IsType(deserializer.CreateResourceInstance(_productEdmType, readContext)); - } + Mock deserializer = new Mock(_deserializerProvider); + deserializer.CallBase = true; + deserializer.Setup(d => d.ApplyNestedProperty(42, resource.NestedResourceInfos[0], _productEdmType, _readContext)).Verifiable(); + deserializer.Setup(d => d.ApplyNestedProperty(42, resource.NestedResourceInfos[1], _productEdmType, _readContext)).Verifiable(); - [Fact] - public void ApplyDeletedResource_ThrowsArgumentNull_ResourceWrapper() - { - // Arrange & Act & Assert - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ApplyDeletedResource(null, null, null), "resourceWrapper"); - } + // Act + deserializer.Object.ApplyNestedProperties(42, resource, _productEdmType, _readContext); - [Fact] - public void ApplyDeletedResource_ThrowsArgumentNull_ReadContext() - { - // Arrange & Act & Assert - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataResourceWrapper wrapper = new ODataResourceWrapper(new ODataResource()); - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ApplyDeletedResource(null, wrapper, null), "readContext"); - } + // Assert + deserializer.Verify(); + } - [Fact] - public void ApplyNestedProperties_ThrowsArgumentNull_EntryWrapper() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); + [Fact] + public void ApplyNestedProperty_ThrowsArgumentNull_ResourceInfoWrapper() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ApplyNestedProperty(42, resourceInfoWrapper: null, structuredType: _productEdmType, + readContext: _readContext), + "resourceInfoWrapper"); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.ApplyNestedProperties(42, resourceWrapper: null, structuredType: _productEdmType, readContext: _readContext), - "resourceWrapper"); - } + [Fact] + public void ApplyNestedProperty_ThrowsArgumentNull_EntityResource() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataNestedResourceInfoWrapper resourceInfoWrapper = new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo()); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ApplyNestedProperty(resource: null, resourceInfoWrapper: resourceInfoWrapper, + structuredType: _productEdmType, readContext: _readContext), + "resource"); + } - [Fact] - public void ApplyNestedProperties_Calls_ApplyNavigationPropertyForEachNavigationLink() - { - // Arrange - ODataResourceWrapper resource = new ODataResourceWrapper(new ODataResource()); - resource.NestedResourceInfos.Add(new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo())); - resource.NestedResourceInfos.Add(new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo())); + [Fact] + public void ApplyNestedProperty_ThrowsODataException_NavigationPropertyNotfound() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ODataNestedResourceInfoWrapper resourceInfoWrapper = new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "SomeProperty" }); + + // Act & Assert + ExceptionAssert.Throws( + () => deserializer.ApplyNestedProperty(42, resourceInfoWrapper, _productEdmType, _readContext), + "Cannot find nested property 'SomeProperty' on the resource type 'ODataDemo.Product'."); + } - Mock deserializer = new Mock(_deserializerProvider); - deserializer.CallBase = true; - deserializer.Setup(d => d.ApplyNestedProperty(42, resource.NestedResourceInfos[0], _productEdmType, _readContext)).Verifiable(); - deserializer.Setup(d => d.ApplyNestedProperty(42, resource.NestedResourceInfos[1], _productEdmType, _readContext)).Verifiable(); + [Fact] + public void ApplyNestedProperty_UsesThePropertyAlias_ForResourceSet() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); + model.Model.SetAnnotationValue(model.Order, new ClrTypeAnnotation(typeof(Order))); + model.Model.SetAnnotationValue( + model.Customer.FindProperty("Orders"), + new ClrPropertyInfoAnnotation(typeof(Customer).GetProperty("AliasedOrders"))); + ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); + resourceSetWrapper.Resources.Add(new ODataResourceWrapper( + new ODataResource { Properties = new[] { new ODataProperty { Name = "ID", Value = 42 } } })); + + Customer customer = new Customer(); + ODataNestedResourceInfoWrapper resourceInfoWrapper = + new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Orders" }); + resourceInfoWrapper.NestedItems.Add(resourceSetWrapper); + + ODataDeserializerContext context = new ODataDeserializerContext { Model = model.Model }; + + // Act + new ODataResourceDeserializer(_deserializerProvider) + .ApplyNestedProperty(customer, resourceInfoWrapper, model.Customer.AsReference(), context); + + // Assert + Assert.Single(customer.AliasedOrders); + Assert.Equal(42, customer.AliasedOrders[0].ID); + } - // Act - deserializer.Object.ApplyNestedProperties(42, resource, _productEdmType, _readContext); + [Fact] + public void ApplyNestedProperty_UsesThePropertyAlias_ForResourceWrapper() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); + model.Model.SetAnnotationValue(model.Order, new ClrTypeAnnotation(typeof(Order))); + model.Model.SetAnnotationValue( + model.Order.FindProperty("Customer"), + new ClrPropertyInfoAnnotation(typeof(Order).GetProperty("AliasedCustomer"))); + ODataResource resource = new ODataResource { Properties = new[] { new ODataProperty { Name = "ID", Value = 42 } } }; + + Order order = new Order(); + ODataNestedResourceInfoWrapper resourceInfoWrapper = + new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Customer" }); + resourceInfoWrapper.NestedItems.Add(new ODataResourceWrapper(resource)); + + ODataDeserializerContext context = new ODataDeserializerContext { Model = model.Model }; + + // Act + new ODataResourceDeserializer(_deserializerProvider) + .ApplyNestedProperty(order, resourceInfoWrapper, model.Order.AsReference(), context); + + // Assert + Assert.Equal(42, order.AliasedCustomer.ID); + } - // Assert - deserializer.Verify(); - } + [Fact] + public void ApplyNestedProperty_Works_ForEntityReferenceLinkWrapper() + { + // Arrange + Product product = new Product(); + ODataNestedResourceInfoWrapper resourceInfoWrapper = + new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Supplier" }); + resourceInfoWrapper.NestedItems.Add( + new ODataEntityReferenceLinkWrapper( + new ODataEntityReferenceLink + { + Url = new Uri("http://localhost/Suppliers(42)", UriKind.RelativeOrAbsolute) + }) + ); - [Fact] - public void ApplyNestedProperty_ThrowsArgumentNull_ResourceInfoWrapper() + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); + ODataDeserializerContext context = new ODataDeserializerContext { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.ApplyNestedProperty(42, resourceInfoWrapper: null, structuredType: _productEdmType, - readContext: _readContext), - "resourceInfoWrapper"); - } + Model = _edmModel, + Request = request + }; - [Fact] - public void ApplyNestedProperty_ThrowsArgumentNull_EntityResource() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataNestedResourceInfoWrapper resourceInfoWrapper = new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo()); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.ApplyNestedProperty(resource: null, resourceInfoWrapper: resourceInfoWrapper, - structuredType: _productEdmType, readContext: _readContext), - "resource"); - } + // Act + new ODataResourceDeserializer(_deserializerProvider) + .ApplyNestedProperty(product, resourceInfoWrapper, _productEdmType, context); - [Fact] - public void ApplyNestedProperty_ThrowsODataException_NavigationPropertyNotfound() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ODataNestedResourceInfoWrapper resourceInfoWrapper = new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "SomeProperty" }); - - // Act & Assert - ExceptionAssert.Throws( - () => deserializer.ApplyNestedProperty(42, resourceInfoWrapper, _productEdmType, _readContext), - "Cannot find nested property 'SomeProperty' on the resource type 'ODataDemo.Product'."); - } + // Assert + Assert.Equal(42, product.Supplier.ID); + } - [Fact] - public void ApplyNestedProperty_UsesThePropertyAlias_ForResourceSet() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); - model.Model.SetAnnotationValue(model.Order, new ClrTypeAnnotation(typeof(Order))); - model.Model.SetAnnotationValue( - model.Customer.FindProperty("Orders"), - new ClrPropertyInfoAnnotation(typeof(Customer).GetProperty("AliasedOrders"))); - ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); - resourceSetWrapper.Resources.Add(new ODataResourceWrapper( - new ODataResource { Properties = new[] { new ODataProperty { Name = "ID", Value = 42 } } })); - - Customer customer = new Customer(); - ODataNestedResourceInfoWrapper resourceInfoWrapper = - new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Orders" }); - resourceInfoWrapper.NestedItems.Add(resourceSetWrapper); - - ODataDeserializerContext context = new ODataDeserializerContext { Model = model.Model }; - - // Act - new ODataResourceDeserializer(_deserializerProvider) - .ApplyNestedProperty(customer, resourceInfoWrapper, model.Customer.AsReference(), context); - - // Assert - Assert.Single(customer.AliasedOrders); - Assert.Equal(42, customer.AliasedOrders[0].ID); - } + [Fact] + public void ApplyNestedProperty_Works_ForEntityReferenceLinksWrapper() + { + // Arrange + Supplier supplier = new Supplier(); + ODataNestedResourceInfoWrapper resourceInfoWrapper = + new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Products" }); + resourceInfoWrapper.NestedItems.Add( + new ODataEntityReferenceLinkWrapper( + new ODataEntityReferenceLink + { + Url = new Uri("http://localhost/Products(5)", UriKind.RelativeOrAbsolute) + }) + ); + resourceInfoWrapper.NestedItems.Add( + new ODataEntityReferenceLinkWrapper( + new ODataEntityReferenceLink + { + Url = new Uri("http://localhost/Products(6)", UriKind.RelativeOrAbsolute) + }) + ); + + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); + ODataDeserializerContext context = new ODataDeserializerContext + { + Model = _edmModel, + Request = request + }; + + // Act + new ODataResourceDeserializer(_deserializerProvider) + .ApplyNestedProperty(supplier, resourceInfoWrapper, _supplierEdmType, context); + + // Assert + Assert.Equal(2, supplier.Products.Count); + Assert.Collection(supplier.Products, + e => Assert.Equal(5, e.ID), + e => Assert.Equal(6, e.ID)); + } - [Fact] - public void ApplyNestedProperty_UsesThePropertyAlias_ForResourceWrapper() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); - model.Model.SetAnnotationValue(model.Order, new ClrTypeAnnotation(typeof(Order))); - model.Model.SetAnnotationValue( - model.Order.FindProperty("Customer"), - new ClrPropertyInfoAnnotation(typeof(Order).GetProperty("AliasedCustomer"))); - ODataResource resource = new ODataResource { Properties = new[] { new ODataProperty { Name = "ID", Value = 42 } } }; - - Order order = new Order(); - ODataNestedResourceInfoWrapper resourceInfoWrapper = - new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Customer" }); - resourceInfoWrapper.NestedItems.Add(new ODataResourceWrapper(resource)); - - ODataDeserializerContext context = new ODataDeserializerContext { Model = model.Model }; - - // Act - new ODataResourceDeserializer(_deserializerProvider) - .ApplyNestedProperty(order, resourceInfoWrapper, model.Order.AsReference(), context); - - // Assert - Assert.Equal(42, order.AliasedCustomer.ID); - } + #region Untyped_NestedResourceInfo + public class Person + { + public int Id { get; set; } - [Fact] - public void ApplyNestedProperty_Works_ForEntityReferenceLinkWrapper() - { - // Arrange - Product product = new Product(); - ODataNestedResourceInfoWrapper resourceInfoWrapper = - new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Supplier" }); - resourceInfoWrapper.NestedItems.Add( - new ODataEntityReferenceLinkWrapper( - new ODataEntityReferenceLink - { - Url = new Uri("http://localhost/Suppliers(42)", UriKind.RelativeOrAbsolute) - }) - ); + public object Data { get; set; } - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); - ODataDeserializerContext context = new ODataDeserializerContext - { - Model = _edmModel, - Request = request - }; + public IList Sources { get; set; } - // Act - new ODataResourceDeserializer(_deserializerProvider) - .ApplyNestedProperty(product, resourceInfoWrapper, _productEdmType, context); + public IDictionary Dynamics { get; set; } + } - // Assert - Assert.Equal(42, product.Supplier.ID); - } + private static IEdmModel GetUntypedModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("People"); + return builder.GetEdmModel(); + } - [Fact] - public void ApplyNestedProperty_Works_ForEntityReferenceLinksWrapper() - { - // Arrange - Supplier supplier = new Supplier(); - ODataNestedResourceInfoWrapper resourceInfoWrapper = - new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Products" }); - resourceInfoWrapper.NestedItems.Add( - new ODataEntityReferenceLinkWrapper( - new ODataEntityReferenceLink - { - Url = new Uri("http://localhost/Products(5)", UriKind.RelativeOrAbsolute) - }) - ); - resourceInfoWrapper.NestedItems.Add( - new ODataEntityReferenceLinkWrapper( - new ODataEntityReferenceLink - { - Url = new Uri("http://localhost/Products(6)", UriKind.RelativeOrAbsolute) - }) - ); + private static IEdmModel EdmUntypedModel = GetUntypedModel(); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); - ODataDeserializerContext context = new ODataDeserializerContext + [Fact] + public void ReadResource_Works_ForDeclaredUntypedProperty_Primitive() + { + // Arrange + Func arranges = () => + { + return new ODataResourceWrapper(new ODataResource { - Model = _edmModel, - Request = request - }; - - // Act - new ODataResourceDeserializer(_deserializerProvider) - .ApplyNestedProperty(supplier, resourceInfoWrapper, _supplierEdmType, context); - - // Assert - Assert.Equal(2, supplier.Products.Count); - Assert.Collection(supplier.Products, - e => Assert.Equal(5, e.ID), - e => Assert.Equal(6, e.ID)); - } + TypeName = "Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.Person", + Properties = new ODataProperty[] + { + new ODataProperty { Name = "Data", Value = 42 }, + new ODataProperty { Name = "DynamicProperty", Value = 12 } + } + }); + }; - #region Untyped_NestedResourceInfo - public class Person + // Assert + Action asserts = (s) => { - public int Id { get; set; } + Assert.NotNull(s); + Person p = Assert.IsType(s); + Assert.Equal(42, p.Data); + Assert.Equal(12, p.Dynamics["DynamicProperty"]); + }; - public object Data { get; set; } + RunReadResourceUntypedTestAndVerify(arranges, asserts); + } + + private void RunReadResourceUntypedTestAndVerify(Func arranges, Action asserts) + { + // Arrange + IEdmModel model = EdmUntypedModel; + var personEdmType = model.GetEdmTypeReference(typeof(Person)).AsEntity(); - public IList Sources { get; set; } + ODataResourceWrapper resourceWrapper = arranges(); - public IDictionary Dynamics { get; set; } - } + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); + ODataDeserializerContext context = new ODataDeserializerContext + { + Model = model, + Request = request + }; + + // Act + object source = new ODataResourceDeserializer(_deserializerProvider) + .ReadResource(resourceWrapper, personEdmType, context); + + // Assert + asserts(source); + } - private static IEdmModel GetUntypedModel() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ApplyNestedProperty_Works_ForDeclaredOrUnDeclaredUntypedProperty_NestedResource(bool isDeclared) + { + // Arrange + /* { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("People"); - return builder.GetEdmModel(); + "Data": { + ...... + } } + */ + Person aPerson = new Person(); + Assert.Null(aPerson.Data); - private static IEdmModel EdmUntypedModel = GetUntypedModel(); + Func arranges = () => + { + string propertyName = isDeclared ? "Data" : "AnyDynamicPropertyName"; + ODataNestedResourceInfoWrapper nestedResourceInfo = + new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = propertyName }); + nestedResourceInfo.NestedItems.Add( + new ODataResourceWrapper( + new ODataResource { Properties = new[] { new ODataProperty { Name = "NestedID", Value = 42 } } })); + return nestedResourceInfo; + }; - [Fact] - public void ReadResource_Works_ForDeclaredUntypedProperty_Primitive() + // Assert + Action asserts = () => { - // Arrange - Func arranges = () => + EdmUntypedObject unTypedData; + if (isDeclared) { - return new ODataResourceWrapper(new ODataResource - { - TypeName = "Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization.Person", - Properties = new ODataProperty[] - { - new ODataProperty { Name = "Data", Value = 42 }, - new ODataProperty { Name = "DynamicProperty", Value = 12 } - } - }); - }; - - // Assert - Action asserts = (s) => + unTypedData = Assert.IsType(aPerson.Data); + } + else { - Assert.NotNull(s); - Person p = Assert.IsType(s); - Assert.Equal(42, p.Data); - Assert.Equal(12, p.Dynamics["DynamicProperty"]); - }; + unTypedData = Assert.IsType(aPerson.Dynamics["AnyDynamicPropertyName"]); + } - RunReadResourceUntypedTestAndVerify(arranges, asserts); - } + KeyValuePair property = Assert.Single(unTypedData); + Assert.Equal("NestedID", property.Key); + Assert.Equal(42, property.Value); + }; + + RunUntypedTestAndVerify(aPerson, arranges, asserts); + } - private void RunReadResourceUntypedTestAndVerify(Func arranges, Action asserts) + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ApplyNestedProperty_Works_ForDeclaredOrUndelcaredUntypedProperty_NestedCollection(bool isDeclared) + { + // Arrange + /* { - // Arrange - IEdmModel model = EdmUntypedModel; - var personEdmType = model.GetEdmTypeReference(typeof(Person)).AsEntity(); + "Data": [ + 13, + { ... }, + null, + { ... } + ] + } + */ + Person aPerson = new Person(); + Assert.Null(aPerson.Data); - ODataResourceWrapper resourceWrapper = arranges(); + Func arranges = () => + { + ODataResourceSetWrapper setWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); + setWrapper.Items.Add(new ODataPrimitiveWrapper(new ODataPrimitiveValue(13))); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); - ODataDeserializerContext context = new ODataDeserializerContext - { - Model = model, - Request = request - }; + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper( + new ODataResource { Properties = new[] { new ODataProperty { Name = "Data_ID", Value = 42 } } }); + setWrapper.Items.Add(resourceWrapper); + setWrapper.Items.Add(null); - // Act - object source = new ODataResourceDeserializer(_deserializerProvider) - .ReadResource(resourceWrapper, personEdmType, context); + // Explicitly added into 'Resources' to test. + setWrapper.Resources.Add(resourceWrapper); + setWrapper.Resources.Add(new ODataResourceWrapper( + new ODataResource { Properties = new[] { new ODataProperty { Name = "Extra_ID", Value = "42" } } })); - // Assert - asserts(source); - } + string propertyName = isDeclared ? "Data" : "AnyDynamicPropertyName"; + ODataNestedResourceInfoWrapper nestedResourceInfo = + new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = propertyName, IsCollection = true }); + nestedResourceInfo.NestedItems.Add(setWrapper); + return nestedResourceInfo; + }; - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ApplyNestedProperty_Works_ForDeclaredOrUnDeclaredUntypedProperty_NestedResource(bool isDeclared) + // Assert + Action asserts = () => { - // Arrange - /* + EdmUntypedCollection unTypedData; + if (isDeclared) { - "Data": { - ...... - } + unTypedData = Assert.IsType(aPerson.Data); } - */ - Person aPerson = new Person(); - Assert.Null(aPerson.Data); - - Func arranges = () => + else { - string propertyName = isDeclared ? "Data" : "AnyDynamicPropertyName"; - ODataNestedResourceInfoWrapper nestedResourceInfo = - new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = propertyName }); - nestedResourceInfo.NestedItems.Add( - new ODataResourceWrapper( - new ODataResource { Properties = new[] { new ODataProperty { Name = "NestedID", Value = 42 } } })); - return nestedResourceInfo; - }; - - // Assert - Action asserts = () => - { - EdmUntypedObject unTypedData; - if (isDeclared) + unTypedData = Assert.IsType(aPerson.Dynamics["AnyDynamicPropertyName"]); + } + + Assert.Equal(4, unTypedData.Count); + Assert.Collection(unTypedData, + e => { - unTypedData = Assert.IsType(aPerson.Data); - } - else + Assert.Equal(13, e); + }, + e => { - unTypedData = Assert.IsType(aPerson.Dynamics["AnyDynamicPropertyName"]); - } - - KeyValuePair property = Assert.Single(unTypedData); - Assert.Equal("NestedID", property.Key); - Assert.Equal(42, property.Value); - }; + EdmUntypedObject unTypedObj = Assert.IsType(e); + KeyValuePair singleProperty = Assert.Single(unTypedObj); + Assert.Equal("Data_ID", singleProperty.Key); + Assert.Equal(42, singleProperty.Value); + }, + e => Assert.Null(e), + e => + { + EdmUntypedObject unTypedObj = Assert.IsType(e); + KeyValuePair singleProperty = Assert.Single(unTypedObj); + Assert.Equal("Extra_ID", singleProperty.Key); + Assert.Equal("42", singleProperty.Value); + }); + }; - RunUntypedTestAndVerify(aPerson, arranges, asserts); - } + RunUntypedTestAndVerify(aPerson, arranges, asserts); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ApplyNestedProperty_Works_ForDeclaredOrUndelcaredUntypedProperty_NestedCollection(bool isDeclared) + [Theory] + [InlineData("Data")] // ==> declared Edm.Untyped property + [InlineData("Sources")] // ==> declared Collection(Edm.Untyped) property + [InlineData("AnyDynamicPropertyName")] // ==> un-declared (or dynamic) property + public void ApplyNestedProperty_Works_ForDeclaredOrUndelcaredUntypedProperty_NestedCollectionofCollection(string propertyName) + { + // Arrange + /* { - // Arrange - /* + "Data": [ + [ + { + "Aws/Name": [ + [ true, 15] + ] + } + ], + { + "Aws/Name": [ + [ true, 15] + ] + } + ] + } + */ + Person aPerson = new Person(); + Assert.Null(aPerson.Data); + + Func arranges = () => + { + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { - "Data": [ - 13, - { ... }, - null, - { ... } - ] - } - */ - Person aPerson = new Person(); - Assert.Null(aPerson.Data); + Properties = Enumerable.Empty() + }); - Func arranges = () => + ODataNestedResourceInfoWrapper awsNameNestedResourceInfo = new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { - ODataResourceSetWrapper setWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); - setWrapper.Items.Add(new ODataPrimitiveWrapper(new ODataPrimitiveValue(13))); - - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper( - new ODataResource { Properties = new[] { new ODataProperty { Name = "Data_ID", Value = 42 } } }); - setWrapper.Items.Add(resourceWrapper); - setWrapper.Items.Add(null); - - // Explicitly added into 'Resources' to test. - setWrapper.Resources.Add(resourceWrapper); - setWrapper.Resources.Add(new ODataResourceWrapper( - new ODataResource { Properties = new[] { new ODataProperty { Name = "Extra_ID", Value = "42" } } })); - - string propertyName = isDeclared ? "Data" : "AnyDynamicPropertyName"; - ODataNestedResourceInfoWrapper nestedResourceInfo = - new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = propertyName, IsCollection = true }); - nestedResourceInfo.NestedItems.Add(setWrapper); - return nestedResourceInfo; - }; - - // Assert - Action asserts = () => + Name = "Aws/Name" // explicitly use '/' in the name. + }); + resourceWrapper.NestedResourceInfos.Add(awsNameNestedResourceInfo); + + ODataResourceSetWrapper setOfAwsNameWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); + awsNameNestedResourceInfo.NestedItems.Add(setOfAwsNameWrapper); + + ODataResourceSetWrapper setOfSetAwsNameWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); + setOfSetAwsNameWrapper.Items.Add(new ODataPrimitiveWrapper(new ODataPrimitiveValue(true))); + setOfSetAwsNameWrapper.Items.Add(new ODataPrimitiveWrapper(new ODataPrimitiveValue(15))); + setOfAwsNameWrapper.Items.Add(setOfSetAwsNameWrapper); + + // first item in 'Data' collection + ODataResourceSetWrapper setWrapperOfDataSet = new ODataResourceSetWrapper(new ODataResourceSet()); + setWrapperOfDataSet.Items.Add(resourceWrapper); + + // Data collection + ODataResourceSetWrapper setWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); + setWrapper.Items.Add(setWrapperOfDataSet); + setWrapper.Items.Add(resourceWrapper); + + ODataNestedResourceInfoWrapper nestedResourceInfo = + new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = propertyName, IsCollection = true }); + nestedResourceInfo.NestedItems.Add(setWrapper); + return nestedResourceInfo; + }; + + // Assert + Action verifyResource = o => + { + KeyValuePair singleProperty = Assert.Single(o); + Assert.Equal("Aws/Name", singleProperty.Key); + EdmUntypedCollection aswNameValuecol = Assert.IsType(singleProperty.Value); + EdmUntypedCollection colInAwsNameCol = Assert.IsType(Assert.Single(aswNameValuecol)); + Assert.Equal(2, colInAwsNameCol.Count); + Assert.Collection(colInAwsNameCol, + e => Assert.True((bool)e), + e => Assert.Equal(15, e)); + }; + + Action asserts = () => + { + IList unTypedData; + if (propertyName == "Data") { - EdmUntypedCollection unTypedData; - if (isDeclared) - { - unTypedData = Assert.IsType(aPerson.Data); - } - else - { - unTypedData = Assert.IsType(aPerson.Dynamics["AnyDynamicPropertyName"]); - } - - Assert.Equal(4, unTypedData.Count); - Assert.Collection(unTypedData, - e => - { - Assert.Equal(13, e); - }, - e => - { - EdmUntypedObject unTypedObj = Assert.IsType(e); - KeyValuePair singleProperty = Assert.Single(unTypedObj); - Assert.Equal("Data_ID", singleProperty.Key); - Assert.Equal(42, singleProperty.Value); - }, - e => Assert.Null(e), - e => - { - EdmUntypedObject unTypedObj = Assert.IsType(e); - KeyValuePair singleProperty = Assert.Single(unTypedObj); - Assert.Equal("Extra_ID", singleProperty.Key); - Assert.Equal("42", singleProperty.Value); - }); - }; - - RunUntypedTestAndVerify(aPerson, arranges, asserts); - } - - [Theory] - [InlineData("Data")] // ==> declared Edm.Untyped property - [InlineData("Sources")] // ==> declared Collection(Edm.Untyped) property - [InlineData("AnyDynamicPropertyName")] // ==> un-declared (or dynamic) property - public void ApplyNestedProperty_Works_ForDeclaredOrUndelcaredUntypedProperty_NestedCollectionofCollection(string propertyName) - { - // Arrange - /* + Assert.IsType(aPerson.Data); + unTypedData = Assert.IsAssignableFrom>(aPerson.Data); + } + else if (propertyName == "Sources") { - "Data": [ - [ - { - "Aws/Name": [ - [ true, 15] - ] - } - ], - { - "Aws/Name": [ - [ true, 15] - ] - } - ] + unTypedData = aPerson.Sources; } - */ - Person aPerson = new Person(); - Assert.Null(aPerson.Data); - - Func arranges = () => + else { - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource + EdmUntypedCollection data = Assert.IsType(aPerson.Dynamics["AnyDynamicPropertyName"]); + unTypedData = Assert.IsAssignableFrom>(data); + } + + Assert.Equal(2, unTypedData.Count); + Assert.Collection(unTypedData, + e => { - Properties = Enumerable.Empty() - }); + EdmUntypedCollection nestedCollection = Assert.IsType(e); + EdmUntypedObject nestedObjInNestedCollection = Assert.IsType(Assert.Single(nestedCollection)); - ODataNestedResourceInfoWrapper awsNameNestedResourceInfo = new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo + verifyResource(nestedObjInNestedCollection); + }, + e => { - Name = "Aws/Name" // explicitly use '/' in the name. + EdmUntypedObject unTypedObj = Assert.IsType(e); + verifyResource(unTypedObj); }); - resourceWrapper.NestedResourceInfos.Add(awsNameNestedResourceInfo); + }; - ODataResourceSetWrapper setOfAwsNameWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); - awsNameNestedResourceInfo.NestedItems.Add(setOfAwsNameWrapper); - - ODataResourceSetWrapper setOfSetAwsNameWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); - setOfSetAwsNameWrapper.Items.Add(new ODataPrimitiveWrapper(new ODataPrimitiveValue(true))); - setOfSetAwsNameWrapper.Items.Add(new ODataPrimitiveWrapper(new ODataPrimitiveValue(15))); - setOfAwsNameWrapper.Items.Add(setOfSetAwsNameWrapper); + RunUntypedTestAndVerify(aPerson, arranges, asserts); + } - // first item in 'Data' collection - ODataResourceSetWrapper setWrapperOfDataSet = new ODataResourceSetWrapper(new ODataResourceSet()); - setWrapperOfDataSet.Items.Add(resourceWrapper); + private void RunUntypedTestAndVerify(object source, Func arranges, Action asserts) + { + // Arrange + IEdmModel model = EdmUntypedModel; + var personEdmType = model.GetEdmTypeReference(typeof(Person)).AsEntity(); - // Data collection - ODataResourceSetWrapper setWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); - setWrapper.Items.Add(setWrapperOfDataSet); - setWrapper.Items.Add(resourceWrapper); + ODataNestedResourceInfoWrapper resourceInfoWrapper = arranges(); - ODataNestedResourceInfoWrapper nestedResourceInfo = - new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = propertyName, IsCollection = true }); - nestedResourceInfo.NestedItems.Add(setWrapper); - return nestedResourceInfo; - }; + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); + ODataDeserializerContext context = new ODataDeserializerContext + { + Model = model, + Request = request + }; - // Assert - Action verifyResource = o => - { - KeyValuePair singleProperty = Assert.Single(o); - Assert.Equal("Aws/Name", singleProperty.Key); - EdmUntypedCollection aswNameValuecol = Assert.IsType(singleProperty.Value); - EdmUntypedCollection colInAwsNameCol = Assert.IsType(Assert.Single(aswNameValuecol)); - Assert.Equal(2, colInAwsNameCol.Count); - Assert.Collection(colInAwsNameCol, - e => Assert.True((bool)e), - e => Assert.Equal(15, e)); - }; - - Action asserts = () => - { - IList unTypedData; - if (propertyName == "Data") - { - Assert.IsType(aPerson.Data); - unTypedData = Assert.IsAssignableFrom>(aPerson.Data); - } - else if (propertyName == "Sources") - { - unTypedData = aPerson.Sources; - } - else - { - EdmUntypedCollection data = Assert.IsType(aPerson.Dynamics["AnyDynamicPropertyName"]); - unTypedData = Assert.IsAssignableFrom>(data); - } + // Act + new ODataResourceDeserializer(_deserializerProvider) + .ApplyNestedProperty(source, resourceInfoWrapper, personEdmType, context); - Assert.Equal(2, unTypedData.Count); - Assert.Collection(unTypedData, - e => - { - EdmUntypedCollection nestedCollection = Assert.IsType(e); - EdmUntypedObject nestedObjInNestedCollection = Assert.IsType(Assert.Single(nestedCollection)); + // Assert + asserts(); + } + #endregion - verifyResource(nestedObjInNestedCollection); - }, - e => - { - EdmUntypedObject unTypedObj = Assert.IsType(e); - verifyResource(unTypedObj); - }); - }; + [Fact(Skip = "This test need to refactor and the ODataResourceDeserializer has the problem for nested resource set.")] + public void ApplyNestedProperty_Works_ForDeltaResourceSetWrapper() + { + // Arrange + Delta supplier = new Delta(); + ODataNestedResourceInfoWrapper resourceInfoWrapper = + new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Products" }); - RunUntypedTestAndVerify(aPerson, arranges, asserts); - } + ODataDeltaResourceSetWrapper wrapper = new ODataDeltaResourceSetWrapper(new ODataDeltaResourceSet()); + ODataResource resource = new ODataResource { Properties = new[] { new ODataProperty { Name = "ID", Value = 42 } } }; + wrapper.DeltaItems.Add(new ODataResourceWrapper(resource)); - private void RunUntypedTestAndVerify(object source, Func arranges, Action asserts) + resourceInfoWrapper.NestedItems.Add(wrapper); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); + ODataDeserializerContext context = new ODataDeserializerContext { - // Arrange - IEdmModel model = EdmUntypedModel; - var personEdmType = model.GetEdmTypeReference(typeof(Person)).AsEntity(); + Model = _edmModel, + Request = request + }; - ODataNestedResourceInfoWrapper resourceInfoWrapper = arranges(); + // Act + new ODataResourceDeserializer(_deserializerProvider) + .ApplyNestedProperty(supplier, resourceInfoWrapper, _supplierEdmType, context); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); - ODataDeserializerContext context = new ODataDeserializerContext - { - Model = model, - Request = request - }; + // Assert + Assert.True(supplier.TryGetPropertyValue("Products", out _)); + } - // Act - new ODataResourceDeserializer(_deserializerProvider) - .ApplyNestedProperty(source, resourceInfoWrapper, personEdmType, context); + [Fact] + public void ApplyStructuralProperties_ThrowsArgumentNull_resourceWrapper() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); - // Assert - asserts(); - } - #endregion + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ApplyStructuralProperties(42, resourceWrapper: null, structuredType: _productEdmType, readContext: _readContext), + "resourceWrapper"); + } - [Fact(Skip = "This test need to refactor and the ODataResourceDeserializer has the problem for nested resource set.")] - public void ApplyNestedProperty_Works_ForDeltaResourceSetWrapper() - { - // Arrange - Delta supplier = new Delta(); - ODataNestedResourceInfoWrapper resourceInfoWrapper = - new ODataNestedResourceInfoWrapper(new ODataNestedResourceInfo { Name = "Products" }); - - ODataDeltaResourceSetWrapper wrapper = new ODataDeltaResourceSetWrapper(new ODataDeltaResourceSet()); - ODataResource resource = new ODataResource { Properties = new[] { new ODataProperty { Name = "ID", Value = 42 } } }; - wrapper.DeltaItems.Add(new ODataResourceWrapper(resource)); - - resourceInfoWrapper.NestedItems.Add(wrapper); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); - ODataDeserializerContext context = new ODataDeserializerContext - { - Model = _edmModel, - Request = request - }; + [Fact] + public void ApplyStructuralProperties_Calls_ApplyStructuralPropertyOnEachPropertyInResource() + { + // Arrange + var deserializer = new Mock(_deserializerProvider); + ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { Properties = properties }); - // Act - new ODataResourceDeserializer(_deserializerProvider) - .ApplyNestedProperty(supplier, resourceInfoWrapper, _supplierEdmType, context); + deserializer.CallBase = true; + deserializer.Setup(d => d.ApplyStructuralProperty(42, properties[0], _productEdmType, _readContext)).Verifiable(); + deserializer.Setup(d => d.ApplyStructuralProperty(42, properties[1], _productEdmType, _readContext)).Verifiable(); - // Assert - Assert.True(supplier.TryGetPropertyValue("Products", out _)); - } + // Act + deserializer.Object.ApplyStructuralProperties(42, resourceWrapper, _productEdmType, _readContext); - [Fact] - public void ApplyStructuralProperties_ThrowsArgumentNull_resourceWrapper() - { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); + // Assert + deserializer.Verify(); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.ApplyStructuralProperties(42, resourceWrapper: null, structuredType: _productEdmType, readContext: _readContext), - "resourceWrapper"); - } + [Fact] + public void ApplyStructuralProperty_ThrowsArgumentNull_Resource() + { + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ApplyStructuralProperty(resource: null, structuralProperty: new ODataProperty(), + structuredType: _productEdmType, readContext: _readContext), + "resource"); + } - [Fact] - public void ApplyStructuralProperties_Calls_ApplyStructuralPropertyOnEachPropertyInResource() - { - // Arrange - var deserializer = new Mock(_deserializerProvider); - ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource { Properties = properties }); + [Fact] + public void ApplyStructuralProperty_ThrowsArgumentNull_StructuralProperty() + { + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + ExceptionAssert.ThrowsArgumentNull( + () => deserializer.ApplyStructuralProperty(42, structuralProperty: null, structuredType: _productEdmType, readContext: _readContext), + "structuralProperty"); + } - deserializer.CallBase = true; - deserializer.Setup(d => d.ApplyStructuralProperty(42, properties[0], _productEdmType, _readContext)).Verifiable(); - deserializer.Setup(d => d.ApplyStructuralProperty(42, properties[1], _productEdmType, _readContext)).Verifiable(); + [Fact] + public void ApplyStructuralProperty_SetsProperty() + { + // Arrange + var deserializer = new ODataResourceDeserializer(_deserializerProvider); + Product product = new Product(); + ODataProperty property = new ODataProperty { Name = "ID", Value = 42 }; - // Act - deserializer.Object.ApplyStructuralProperties(42, resourceWrapper, _productEdmType, _readContext); + // Act + deserializer.ApplyStructuralProperty(product, property, _productEdmType, _readContext); - // Assert - deserializer.Verify(); - } + // Assert + Assert.Equal(42, product.ID); + } - [Fact] - public void ApplyStructuralProperty_ThrowsArgumentNull_Resource() - { - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.ApplyStructuralProperty(resource: null, structuralProperty: new ODataProperty(), - structuredType: _productEdmType, readContext: _readContext), - "resource"); - } + [Fact] + public async Task ReadFromStreamAsync() + { + // Arrange + string content = @"{ + ""ID"":0, + ""Name"":""Bread"", + ""Description"":""Whole grain bread"", + ""ReleaseDate"":""1992-01-01T00:00:00Z"", + ""PublishDate"":""1997-07-01"", + ""DiscontinuedDate"":null, + ""Rating"":4, + ""Price"":2.5 + }"; + ODataResourceDeserializer deserializer = new ODataResourceDeserializer(_deserializerProvider); + + // Act + Product product = await deserializer.ReadAsync(GetODataMessageReader(content, _edmModel), + typeof(Product), _readContext) as Product; + + // Assert + Assert.Equal(0, product.ID); + Assert.Equal(4, product.Rating); + Assert.Equal(2.5m, product.Price); + Assert.Equal(product.ReleaseDate, new DateTimeOffset(new DateTime(1992, 1, 1, 0, 0, 0), TimeSpan.Zero)); + Assert.Equal(product.PublishDate, new Date(1997, 7, 1)); + Assert.Null(product.DiscontinuedDate); + } - [Fact] - public void ApplyStructuralProperty_ThrowsArgumentNull_StructuralProperty() - { - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - ExceptionAssert.ThrowsArgumentNull( - () => deserializer.ApplyStructuralProperty(42, structuralProperty: null, structuredType: _productEdmType, readContext: _readContext), - "structuralProperty"); - } + [Fact] + public async Task ReadFromStreamAsync_ComplexTypeAndInlineData() + { + // Arrange + string content = _supplyRequestResource; + ODataResourceDeserializer deserializer = new ODataResourceDeserializer(_deserializerProvider); - [Fact] - public void ApplyStructuralProperty_SetsProperty() + var readContext = new ODataDeserializerContext { - // Arrange - var deserializer = new ODataResourceDeserializer(_deserializerProvider); - Product product = new Product(); - ODataProperty property = new ODataProperty { Name = "ID", Value = 42 }; + Path = new ODataPath(new EntitySetSegment(_edmModel.EntityContainer.FindEntitySet("Suppliers"))), + Model = _edmModel, + ResourceType = typeof(Supplier) + }; - // Act - deserializer.ApplyStructuralProperty(product, property, _productEdmType, _readContext); + // Act + Supplier supplier = await deserializer.ReadAsync(GetODataMessageReader(content, _edmModel), + typeof(Supplier), readContext) as Supplier; - // Assert - Assert.Equal(42, product.ID); - } + // Assert + Assert.Equal("Supplier Name", supplier.Name); - [Fact] - public async Task ReadFromStreamAsync() - { - // Arrange - string content = @"{ - ""ID"":0, - ""Name"":""Bread"", - ""Description"":""Whole grain bread"", - ""ReleaseDate"":""1992-01-01T00:00:00Z"", - ""PublishDate"":""1997-07-01"", - ""DiscontinuedDate"":null, - ""Rating"":4, - ""Price"":2.5 - }"; - ODataResourceDeserializer deserializer = new ODataResourceDeserializer(_deserializerProvider); - - // Act - Product product = await deserializer.ReadAsync(GetODataMessageReader(content, _edmModel), - typeof(Product), _readContext) as Product; - - // Assert - Assert.Equal(0, product.ID); - Assert.Equal(4, product.Rating); - Assert.Equal(2.5m, product.Price); - Assert.Equal(product.ReleaseDate, new DateTimeOffset(new DateTime(1992, 1, 1, 0, 0, 0), TimeSpan.Zero)); - Assert.Equal(product.PublishDate, new Date(1997, 7, 1)); - Assert.Null(product.DiscontinuedDate); - } + Assert.NotNull(supplier.Products); + Assert.Equal(6, supplier.Products.Count); + Assert.Equal("soda", supplier.Products.ToList()[1].Name); - [Fact] - public async Task ReadFromStreamAsync_ComplexTypeAndInlineData() - { - // Arrange - string content = _supplyRequestResource; - ODataResourceDeserializer deserializer = new ODataResourceDeserializer(_deserializerProvider); + Assert.NotNull(supplier.Address); + Assert.Equal("Supplier City", supplier.Address.City); + Assert.Equal("123456", supplier.Address.ZipCode); + } - var readContext = new ODataDeserializerContext + [Fact] + public async Task ReadAsync_PatchMode() + { + // Arrange + string content = @"{ + ""ID"":123, + ""Name"":""Supplier Name"", + ""Address"": { - Path = new ODataPath(new EntitySetSegment(_edmModel.EntityContainer.FindEntitySet("Suppliers"))), - Model = _edmModel, - ResourceType = typeof(Supplier) - }; - - // Act - Supplier supplier = await deserializer.ReadAsync(GetODataMessageReader(content, _edmModel), - typeof(Supplier), readContext) as Supplier; - - // Assert - Assert.Equal("Supplier Name", supplier.Name); - - Assert.NotNull(supplier.Products); - Assert.Equal(6, supplier.Products.Count); - Assert.Equal("soda", supplier.Products.ToList()[1].Name); - - Assert.NotNull(supplier.Address); - Assert.Equal("Supplier City", supplier.Address.City); - Assert.Equal("123456", supplier.Address.ZipCode); - } + ""Street"":""Supplier Street"", + ""City"":""Supplier City"", + ""State"":""WA"", + ""ZipCode"":""123456"", + ""CountryOrRegion"":""USA"" + } + }"; - [Fact] - public async Task ReadAsync_PatchMode() + var readContext = new ODataDeserializerContext { - // Arrange - string content = @"{ - ""ID"":123, - ""Name"":""Supplier Name"", - ""Address"": - { - ""Street"":""Supplier Street"", - ""City"":""Supplier City"", - ""State"":""WA"", - ""ZipCode"":""123456"", - ""CountryOrRegion"":""USA"" - } - }"; + Path = new ODataPath(new EntitySetSegment(_edmModel.EntityContainer.FindEntitySet("Suppliers"))), + Model = _edmModel, + ResourceType = typeof(Delta) + }; - var readContext = new ODataDeserializerContext - { - Path = new ODataPath(new EntitySetSegment(_edmModel.EntityContainer.FindEntitySet("Suppliers"))), - Model = _edmModel, - ResourceType = typeof(Delta) - }; - - ODataResourceDeserializer deserializer = - new ODataResourceDeserializer(_deserializerProvider); + ODataResourceDeserializer deserializer = + new ODataResourceDeserializer(_deserializerProvider); - // Act - Delta supplier = await deserializer.ReadAsync(GetODataMessageReader(content, _edmModel), - typeof(Delta), readContext) as Delta; + // Act + Delta supplier = await deserializer.ReadAsync(GetODataMessageReader(content, _edmModel), + typeof(Delta), readContext) as Delta; - // Assert - Assert.NotNull(supplier); - Assert.Equal(supplier.GetChangedPropertyNames(), new string[] { "ID", "Name", "Address" }); + // Assert + Assert.NotNull(supplier); + Assert.Equal(supplier.GetChangedPropertyNames(), new string[] { "ID", "Name", "Address" }); - Assert.Equal("Supplier Name", (supplier as dynamic).Name); - Assert.Equal("Supplier City", (supplier as dynamic).Address.City); - Assert.Equal("123456", (supplier as dynamic).Address.ZipCode); - } + Assert.Equal("Supplier Name", (supplier as dynamic).Name); + Assert.Equal("Supplier City", (supplier as dynamic).Address.City); + Assert.Equal("123456", (supplier as dynamic).Address.ZipCode); + } - [Fact] - public void ReadAsync_ThrowsOnUnknownEntityType() - { - // Arrange - string content = _supplyRequestResource; - ODataResourceDeserializer deserializer = new ODataResourceDeserializer(_deserializerProvider); + [Fact] + public void ReadAsync_ThrowsOnUnknownEntityType() + { + // Arrange + string content = _supplyRequestResource; + ODataResourceDeserializer deserializer = new ODataResourceDeserializer(_deserializerProvider); - // Act & Assert - ExceptionAssert.Throws(() => deserializer.ReadAsync(GetODataMessageReader(content, _edmModel), - typeof(Product), _readContext).Wait(), "The property 'Concurrency' does not exist on type 'ODataDemo.Product'. Make sure to only use property names that are defined by the type or mark the type as open type."); - } + // Act & Assert + ExceptionAssert.Throws(() => deserializer.ReadAsync(GetODataMessageReader(content, _edmModel), + typeof(Product), _readContext).Wait(), "The property 'Concurrency' does not exist on type 'ODataDemo.Product'. Make sure to only use property names that are defined by the type or mark the type as open type."); + } - private static ODataMessageReader GetODataMessageReader(IODataRequestMessage oDataRequestMessage, IEdmModel edmModel) - { - return new ODataMessageReader(oDataRequestMessage, new ODataMessageReaderSettings(), edmModel); - } + private static ODataMessageReader GetODataMessageReader(IODataRequestMessage oDataRequestMessage, IEdmModel edmModel) + { + return new ODataMessageReader(oDataRequestMessage, new ODataMessageReaderSettings(), edmModel); + } - private static ODataMessageReader GetODataMessageReader(string content, IEdmModel edmModel) - { - IODataRequestMessage oDataRequestMessage = GetODataMessage(content, edmModel); - return new ODataMessageReader(oDataRequestMessage, new ODataMessageReaderSettings(), edmModel); - } + private static ODataMessageReader GetODataMessageReader(string content, IEdmModel edmModel) + { + IODataRequestMessage oDataRequestMessage = GetODataMessage(content, edmModel); + return new ODataMessageReader(oDataRequestMessage, new ODataMessageReaderSettings(), edmModel); + } - private static IODataRequestMessage GetODataMessage(string content, IEdmModel model) - { - // HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/OData/OData.svc/Products"); + private static IODataRequestMessage GetODataMessage(string content, IEdmModel model) + { + // HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/OData/OData.svc/Products"); - HttpRequest request = RequestFactory.Create("Post", "http://localhost/odata/Products", opt => opt.AddRouteComponents("odata", model)); + HttpRequest request = RequestFactory.Create("Post", "http://localhost/odata/Products", opt => opt.AddRouteComponents("odata", model)); - //request.Content = new StringContent(content); - //request.Headers.Add("OData-Version", "4.0"); + //request.Content = new StringContent(content); + //request.Headers.Add("OData-Version", "4.0"); - //MediaTypeWithQualityHeaderValue mediaType = new MediaTypeWithQualityHeaderValue("application/json"); - //mediaType.Parameters.Add(new NameValueHeaderValue("odata.metadata", "full")); - //request.Headers.Accept.Add(mediaType); - //request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + //MediaTypeWithQualityHeaderValue mediaType = new MediaTypeWithQualityHeaderValue("application/json"); + //mediaType.Parameters.Add(new NameValueHeaderValue("odata.metadata", "full")); + //request.Headers.Accept.Add(mediaType); + //request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - byte[] contentBytes = Encoding.UTF8.GetBytes(content); - request.Body = new MemoryStream(contentBytes); - request.ContentType = "application/json"; - request.ContentLength = contentBytes.Length; - request.Headers.Append("OData-Version", "4.0"); - request.Headers.Append("Accept", "application/json;odata.metadata=full"); + byte[] contentBytes = Encoding.UTF8.GetBytes(content); + request.Body = new MemoryStream(contentBytes); + request.ContentType = "application/json"; + request.ContentLength = contentBytes.Length; + request.Headers.Append("OData-Version", "4.0"); + request.Headers.Append("Accept", "application/json;odata.metadata=full"); - return new HttpRequestODataMessage(request); - } + return new HttpRequestODataMessage(request); + } - private static IEdmModel GetEdmModel() - { - string csdl = @" + private static IEdmModel GetEdmModel() + { + string csdl = @" +xmlns:edmx=""http://docs.oasis-open.org/odata/ns/edmx""> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Concurrency - - - - - - + xmlns:m=""http://docs.oasis-open.org/odata/ns/metadata""> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Concurrency + + + + + + "; - IEdmModel edmModel; - IEnumerable edmErrors; - Assert.True(CsdlReader.TryParse(XmlReader.Create(new StringReader(csdl)), out edmModel, out edmErrors)); + IEdmModel edmModel; + IEnumerable edmErrors; + Assert.True(CsdlReader.TryParse(XmlReader.Create(new StringReader(csdl)), out edmModel, out edmErrors)); - edmModel.SetAnnotationValue(edmModel.FindDeclaredType("ODataDemo.Product"), new ClrTypeAnnotation(typeof(Product))); - edmModel.SetAnnotationValue(edmModel.FindDeclaredType("ODataDemo.Supplier"), new ClrTypeAnnotation(typeof(Supplier))); - edmModel.SetAnnotationValue(edmModel.FindDeclaredType("ODataDemo.Address"), new ClrTypeAnnotation(typeof(Address))); - edmModel.SetAnnotationValue(edmModel.FindDeclaredType("ODataDemo.Category"), new ClrTypeAnnotation(typeof(Category))); + edmModel.SetAnnotationValue(edmModel.FindDeclaredType("ODataDemo.Product"), new ClrTypeAnnotation(typeof(Product))); + edmModel.SetAnnotationValue(edmModel.FindDeclaredType("ODataDemo.Supplier"), new ClrTypeAnnotation(typeof(Supplier))); + edmModel.SetAnnotationValue(edmModel.FindDeclaredType("ODataDemo.Address"), new ClrTypeAnnotation(typeof(Address))); + edmModel.SetAnnotationValue(edmModel.FindDeclaredType("ODataDemo.Category"), new ClrTypeAnnotation(typeof(Category))); - return edmModel; - } + return edmModel; + } - public abstract class BaseType - { - public int ID { get; set; } - } + public abstract class BaseType + { + public int ID { get; set; } + } - public class Product - { - public int ID { get; set; } + public class Product + { + public int ID { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public string Description { get; set; } + public string Description { get; set; } - public DateTimeOffset? ReleaseDate { get; set; } + public DateTimeOffset? ReleaseDate { get; set; } - public DateTimeOffset? DiscontinuedDate { get; set; } + public DateTimeOffset? DiscontinuedDate { get; set; } - public Date PublishDate { get; set; } + public Date PublishDate { get; set; } - public int Rating { get; set; } + public int Rating { get; set; } - public decimal Price { get; set; } + public decimal Price { get; set; } - public virtual Category Category { get; set; } + public virtual Category Category { get; set; } - public virtual Supplier Supplier { get; set; } - } + public virtual Supplier Supplier { get; set; } + } - public class Category - { - public int ID { get; set; } + public class Category + { + public int ID { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public virtual IList Products { get; set; } - } + public virtual IList Products { get; set; } + } - public class Supplier - { - public int ID { get; set; } + public class Supplier + { + public int ID { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public Address Address { get; set; } + public Address Address { get; set; } - public int Concurrency { get; set; } + public int Concurrency { get; set; } - public SupplierRating SupplierRating { get; set; } + public SupplierRating SupplierRating { get; set; } - public virtual IList Products { get; set; } - } + public virtual IList Products { get; set; } + } - public class Address - { - public string Street { get; set; } + public class Address + { + public string Street { get; set; } - public string City { get; set; } + public string City { get; set; } - public string State { get; set; } + public string State { get; set; } - public string ZipCode { get; set; } + public string ZipCode { get; set; } - public string CountryOrRegion { get; set; } - } + public string CountryOrRegion { get; set; } + } - public enum SupplierRating - { - Gold, - Silver, - Bronze - } + public enum SupplierRating + { + Gold, + Silver, + Bronze + } - private class Customer - { - public int ID { get; set; } + private class Customer + { + public int ID { get; set; } - public Order[] AliasedOrders { get; set; } - } + public Order[] AliasedOrders { get; set; } + } - private class Order - { - public int ID { get; set; } + private class Order + { + public int ID { get; set; } - public Customer AliasedCustomer { get; set; } - } + public Customer AliasedCustomer { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataResourceSetDeserializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataResourceSetDeserializerTests.cs index 9c1109ab5..3b10e20e0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataResourceSetDeserializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataResourceSetDeserializerTests.cs @@ -28,536 +28,535 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class ODataResourceSetDeserializerTests { - public class ODataResourceSetDeserializerTests - { - private readonly IEdmModel _model; - private readonly IEdmCollectionTypeReference _customersType; - private readonly IEdmEntityTypeReference _customerType; - private readonly IODataSerializerProvider _serializerProvider; - private readonly IODataDeserializerProvider _deserializerProvider; + private readonly IEdmModel _model; + private readonly IEdmCollectionTypeReference _customersType; + private readonly IEdmEntityTypeReference _customerType; + private readonly IODataSerializerProvider _serializerProvider; + private readonly IODataDeserializerProvider _deserializerProvider; - public ODataResourceSetDeserializerTests() - { - _model = GetEdmModel(); - _customerType = _model.GetEdmTypeReference(typeof(Customer)).AsEntity(); - _customersType = new EdmCollectionTypeReference(new EdmCollectionType(_customerType)); - _serializerProvider = ODataFormatterHelpers.GetSerializerProvider(); - _deserializerProvider = ODataFormatterHelpers.GetDeserializerProvider(); - } + public ODataResourceSetDeserializerTests() + { + _model = GetEdmModel(); + _customerType = _model.GetEdmTypeReference(typeof(Customer)).AsEntity(); + _customersType = new EdmCollectionTypeReference(new EdmCollectionType(_customerType)); + _serializerProvider = ODataFormatterHelpers.GetSerializerProvider(); + _deserializerProvider = ODataFormatterHelpers.GetDeserializerProvider(); + } - [Fact] - public void Ctor_ThrowsArgumentNull_DeserializerProvider() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataResourceSetDeserializer(deserializerProvider: null), "deserializerProvider"); - } + [Fact] + public void Ctor_ThrowsArgumentNull_DeserializerProvider() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataResourceSetDeserializer(deserializerProvider: null), "deserializerProvider"); + } - [Fact] - public async Task ReadAsync_ThrowsArgumentNull_MessageReader() - { - // Arrange - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + [Fact] + public async Task ReadAsync_ThrowsArgumentNull_MessageReader() + { + // Arrange + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => deserializer.ReadAsync(null, null, null), "messageReader"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => deserializer.ReadAsync(null, null, null), "messageReader"); + } - [Fact] - public async Task ReadAsync_ThrowsArgumentNull_ReadContext() - { - // Arrange - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - ODataMessageReader reader = ODataFormatterHelpers.GetMockODataMessageReader(); + [Fact] + public async Task ReadAsync_ThrowsArgumentNull_ReadContext() + { + // Arrange + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + ODataMessageReader reader = ODataFormatterHelpers.GetMockODataMessageReader(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => deserializer.ReadAsync(reader, null, null), "readContext"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => deserializer.ReadAsync(reader, null, null), "readContext"); + } - [Fact] - public async Task ReadAsync_Throws_ArgumentMustBeOfType() + [Fact] + public async Task ReadAsync_Throws_ArgumentMustBeOfType() + { + // Arrange + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + ODataMessageReader reader = ODataFormatterHelpers.GetMockODataMessageReader(); + ODataDeserializerContext context = new ODataDeserializerContext { - // Arrange - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - ODataMessageReader reader = ODataFormatterHelpers.GetMockODataMessageReader(); - ODataDeserializerContext context = new ODataDeserializerContext - { - Model = EdmCoreModel.Instance - }; - IEdmTypeReference edmType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetString(false))); - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => deserializer.ReadAsync(reader, typeof(IList), context), - "The argument must be of type 'Collection of complex, entity or untyped'. (Parameter 'edmType')"); - } + Model = EdmCoreModel.Instance + }; + IEdmTypeReference edmType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetString(false))); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => deserializer.ReadAsync(reader, typeof(IList), context), + "The argument must be of type 'Collection of complex, entity or untyped'. (Parameter 'edmType')"); + } - [Fact] - public void ReadInline_ReturnsNull_IfItemIsNull() - { - // Arrange - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + [Fact] + public void ReadInline_ReturnsNull_IfItemIsNull() + { + // Arrange + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - // Act & Assert - Assert.Null(deserializer.ReadInline(item: null, edmType: _customersType, readContext: new ODataDeserializerContext())); - } + // Act & Assert + Assert.Null(deserializer.ReadInline(item: null, edmType: _customersType, readContext: new ODataDeserializerContext())); + } - [Fact] - public void ReadInline_ThrowsArgumentNull_ReadContext() - { - // Arrange - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - IEdmTypeReference typeReference = new Mock().Object; + [Fact] + public void ReadInline_ThrowsArgumentNull_ReadContext() + { + // Arrange + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + IEdmTypeReference typeReference = new Mock().Object; - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadInline(42, typeReference, null), "readContext"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadInline(42, typeReference, null), "readContext"); + } - [Fact] - public void ReadInline_Throws_ArgumentTypeMustBeResourceSet() - { - // Arrange - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - IEdmTypeReference edmType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetString(false))); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => deserializer.ReadInline(42, edmType, new ODataDeserializerContext()), - "edmType", - "'[Collection([Edm.String Nullable=False Unicode=True]) Nullable=False]' is not a resource set type. Only resource set are supported."); - } + [Fact] + public void ReadInline_Throws_ArgumentTypeMustBeResourceSet() + { + // Arrange + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + IEdmTypeReference edmType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetString(false))); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => deserializer.ReadInline(42, edmType, new ODataDeserializerContext()), + "edmType", + "'[Collection([Edm.String Nullable=False Unicode=True]) Nullable=False]' is not a resource set type. Only resource set are supported."); + } - [Fact] - public void ReadInline_Throws_ArgumentMustBeOfType() - { - // Arrange - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => deserializer.ReadInline(item: 42, edmType: _customersType, readContext: new ODataDeserializerContext()), - "item", - "The argument must be of type 'ODataResourceSetWrapper'."); - } + [Fact] + public void ReadInline_Throws_ArgumentMustBeOfType() + { + // Arrange + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => deserializer.ReadInline(item: 42, edmType: _customersType, readContext: new ODataDeserializerContext()), + "item", + "The argument must be of type 'ODataResourceSetWrapper'."); + } - [Fact] - public void ReadInline_Calls_ReadResourceSet() - { - // Arrange - IODataDeserializerProvider deserializerProvider = _deserializerProvider; - Mock deserializer = new Mock(deserializerProvider); - ODataResourceSetWrapper feedWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); - ODataDeserializerContext readContext = new ODataDeserializerContext(); - IEnumerable expectedResult = new object[0]; - - deserializer.CallBase = true; - deserializer.Setup(f => f.ReadResourceSet(feedWrapper, _customerType, readContext)).Returns(expectedResult).Verifiable(); - - // Act - var result = deserializer.Object.ReadInline(feedWrapper, _customersType, readContext); - - // Assert - deserializer.Verify(); - Assert.Same(expectedResult, result); - } + [Fact] + public void ReadInline_Calls_ReadResourceSet() + { + // Arrange + IODataDeserializerProvider deserializerProvider = _deserializerProvider; + Mock deserializer = new Mock(deserializerProvider); + ODataResourceSetWrapper feedWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); + ODataDeserializerContext readContext = new ODataDeserializerContext(); + IEnumerable expectedResult = new object[0]; + + deserializer.CallBase = true; + deserializer.Setup(f => f.ReadResourceSet(feedWrapper, _customerType, readContext)).Returns(expectedResult).Verifiable(); + + // Act + var result = deserializer.Object.ReadInline(feedWrapper, _customersType, readContext); + + // Assert + deserializer.Verify(); + Assert.Same(expectedResult, result); + } - [Fact] - public void ReadResourceSet_ThrowsArgumentNull_ResourceSet() - { - // Arrange & Act & Assert - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadResourceSet(null, null, null).GetEnumerator().MoveNext(), "resourceSet"); - } + [Fact] + public void ReadResourceSet_ThrowsArgumentNull_ResourceSet() + { + // Arrange & Act & Assert + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadResourceSet(null, null, null).GetEnumerator().MoveNext(), "resourceSet"); + } - [Fact] - public void ReadResourceSet_Calls_ReadPrimitiveItem() - { - // Arrange - Mock deserializerProvider = new Mock(); - Mock structuredTypeRef = new Mock(); - ODataDeserializerContext readContext = new ODataDeserializerContext(); - - int value = 3; - ODataPrimitiveWrapper wrapper = new ODataPrimitiveWrapper(new ODataPrimitiveValue(value)); - ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); - resourceSetWrapper.Items.Add(wrapper); - - Mock deserializer = new Mock(deserializerProvider.Object); - deserializer.CallBase = true; - deserializer.Setup(d => d.ReadPrimitiveItem(wrapper, structuredTypeRef.Object, readContext)).Returns(value).Verifiable(); - - // Act - var result = deserializer.Object.ReadResourceSet(resourceSetWrapper, structuredTypeRef.Object, readContext); - - // Assert - Assert.Equal(new int[] { value }, result.OfType()); - deserializer.Verify(); - } + [Fact] + public void ReadResourceSet_Calls_ReadPrimitiveItem() + { + // Arrange + Mock deserializerProvider = new Mock(); + Mock structuredTypeRef = new Mock(); + ODataDeserializerContext readContext = new ODataDeserializerContext(); + + int value = 3; + ODataPrimitiveWrapper wrapper = new ODataPrimitiveWrapper(new ODataPrimitiveValue(value)); + ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); + resourceSetWrapper.Items.Add(wrapper); + + Mock deserializer = new Mock(deserializerProvider.Object); + deserializer.CallBase = true; + deserializer.Setup(d => d.ReadPrimitiveItem(wrapper, structuredTypeRef.Object, readContext)).Returns(value).Verifiable(); + + // Act + var result = deserializer.Object.ReadResourceSet(resourceSetWrapper, structuredTypeRef.Object, readContext); + + // Assert + Assert.Equal(new int[] { value }, result.OfType()); + deserializer.Verify(); + } - [Fact] - public void ReadResourceSet_Calls_ReadResourceItem_ForEachResource() - { - // Arrange - Mock deserializerProvider = new Mock(); - Mock structuredTypeRef = new Mock(); - ODataDeserializerContext readContext = new ODataDeserializerContext(); - - ODataResourceWrapper resourceWrapper1 = new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a1/") }); - ODataResourceWrapper resourceWrapper2 = new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a1/") }); - ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); - resourceSetWrapper.Items.Add(resourceWrapper1); - resourceSetWrapper.Items.Add(resourceWrapper2); - - Mock deserializer = new Mock(deserializerProvider.Object); - deserializer.CallBase = true; - deserializer.Setup(d => d.ReadResourceItem(resourceWrapper1, structuredTypeRef.Object, readContext)).Returns("a1").Verifiable(); - deserializer.Setup(d => d.ReadResourceItem(resourceWrapper2, structuredTypeRef.Object, readContext)).Returns("a2").Verifiable(); - - // Act - var result = deserializer.Object.ReadResourceSet(resourceSetWrapper, structuredTypeRef.Object, readContext); - - // Assert - Assert.Equal(new[] { "a1", "a2" }, result.OfType()); - deserializer.Verify(); - } + [Fact] + public void ReadResourceSet_Calls_ReadResourceItem_ForEachResource() + { + // Arrange + Mock deserializerProvider = new Mock(); + Mock structuredTypeRef = new Mock(); + ODataDeserializerContext readContext = new ODataDeserializerContext(); + + ODataResourceWrapper resourceWrapper1 = new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a1/") }); + ODataResourceWrapper resourceWrapper2 = new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a1/") }); + ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); + resourceSetWrapper.Items.Add(resourceWrapper1); + resourceSetWrapper.Items.Add(resourceWrapper2); + + Mock deserializer = new Mock(deserializerProvider.Object); + deserializer.CallBase = true; + deserializer.Setup(d => d.ReadResourceItem(resourceWrapper1, structuredTypeRef.Object, readContext)).Returns("a1").Verifiable(); + deserializer.Setup(d => d.ReadResourceItem(resourceWrapper2, structuredTypeRef.Object, readContext)).Returns("a2").Verifiable(); + + // Act + var result = deserializer.Object.ReadResourceSet(resourceSetWrapper, structuredTypeRef.Object, readContext); + + // Assert + Assert.Equal(new[] { "a1", "a2" }, result.OfType()); + deserializer.Verify(); + } - [Fact] - public void ReadResourceSet_Calls_ReadInlineForEachResource() + [Fact] + public void ReadResourceSet_Calls_ReadInlineForEachResource() + { + // Arrange + Mock deserializerProvider = new Mock(); + Mock entityDeserializer = new Mock(ODataPayloadKind.Resource); + ODataResourceSetDeserializer deserializer = new ODataResourceSetDeserializer(deserializerProvider.Object); + ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); + resourceSetWrapper.Resources.Add(new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a1/") })); + resourceSetWrapper.Resources.Add(new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a2/") })); + ODataDeserializerContext readContext = new ODataDeserializerContext { - // Arrange - Mock deserializerProvider = new Mock(); - Mock entityDeserializer = new Mock(ODataPayloadKind.Resource); - ODataResourceSetDeserializer deserializer = new ODataResourceSetDeserializer(deserializerProvider.Object); - ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); - resourceSetWrapper.Resources.Add(new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a1/") })); - resourceSetWrapper.Resources.Add(new ODataResourceWrapper(new ODataResource { Id = new Uri("http://a2/") })); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _model - }; + Model = _model + }; - deserializerProvider.Setup(p => p.GetEdmTypeDeserializer(_customerType, false)).Returns(entityDeserializer.Object); - entityDeserializer.Setup(d => d.ReadInline(resourceSetWrapper.Resources[0], _customerType, It.IsAny())).Returns("a1").Verifiable(); - entityDeserializer.Setup(d => d.ReadInline(resourceSetWrapper.Resources[1], _customerType, It.IsAny())).Returns("a2").Verifiable(); + deserializerProvider.Setup(p => p.GetEdmTypeDeserializer(_customerType, false)).Returns(entityDeserializer.Object); + entityDeserializer.Setup(d => d.ReadInline(resourceSetWrapper.Resources[0], _customerType, It.IsAny())).Returns("a1").Verifiable(); + entityDeserializer.Setup(d => d.ReadInline(resourceSetWrapper.Resources[1], _customerType, It.IsAny())).Returns("a2").Verifiable(); - // Act - var result = deserializer.ReadResourceSet(resourceSetWrapper, _customerType, readContext); + // Act + var result = deserializer.ReadResourceSet(resourceSetWrapper, _customerType, readContext); - // Assert - Assert.Equal(new[] { "a1", "a2" }, result.OfType()); - entityDeserializer.Verify(); - } + // Assert + Assert.Equal(new[] { "a1", "a2" }, result.OfType()); + entityDeserializer.Verify(); + } - [Fact] - public void ReadResourceSet_Calls_ReadResourceSetItem_ForEachResourceSet() - { - // Arrange - Mock deserializerProvider = new Mock(); - Mock structuredTypeRef = new Mock(); - ODataDeserializerContext readContext = new ODataDeserializerContext(); - - ODataResourceSetWrapper setWrapper1 = new ODataResourceSetWrapper(new ODataResourceSet()); - ODataResourceSetWrapper setWrapper2 = new ODataResourceSetWrapper(new ODataResourceSet()); - ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); - resourceSetWrapper.Items.Add(setWrapper1); - resourceSetWrapper.Items.Add(setWrapper2); - - Mock deserializer = new Mock(deserializerProvider.Object); - deserializer.CallBase = true; - deserializer.Setup(d => d.ReadResourceSetItem(setWrapper1, structuredTypeRef.Object, readContext)).Returns("a1").Verifiable(); - deserializer.Setup(d => d.ReadResourceSetItem(setWrapper2, structuredTypeRef.Object, readContext)).Returns("a2").Verifiable(); - - // Act - var result = deserializer.Object.ReadResourceSet(resourceSetWrapper, structuredTypeRef.Object, readContext); - - // Assert - Assert.Equal(new[] { "a1", "a2" }, result.OfType()); - deserializer.Verify(); - } + [Fact] + public void ReadResourceSet_Calls_ReadResourceSetItem_ForEachResourceSet() + { + // Arrange + Mock deserializerProvider = new Mock(); + Mock structuredTypeRef = new Mock(); + ODataDeserializerContext readContext = new ODataDeserializerContext(); + + ODataResourceSetWrapper setWrapper1 = new ODataResourceSetWrapper(new ODataResourceSet()); + ODataResourceSetWrapper setWrapper2 = new ODataResourceSetWrapper(new ODataResourceSet()); + ODataResourceSetWrapper resourceSetWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); + resourceSetWrapper.Items.Add(setWrapper1); + resourceSetWrapper.Items.Add(setWrapper2); + + Mock deserializer = new Mock(deserializerProvider.Object); + deserializer.CallBase = true; + deserializer.Setup(d => d.ReadResourceSetItem(setWrapper1, structuredTypeRef.Object, readContext)).Returns("a1").Verifiable(); + deserializer.Setup(d => d.ReadResourceSetItem(setWrapper2, structuredTypeRef.Object, readContext)).Returns("a2").Verifiable(); + + // Act + var result = deserializer.Object.ReadResourceSet(resourceSetWrapper, structuredTypeRef.Object, readContext); + + // Assert + Assert.Equal(new[] { "a1", "a2" }, result.OfType()); + deserializer.Verify(); + } - [Fact] - public void ReadPrimitiveItem_ThrowsArgumentNull_ResourceWrapper() - { - // Arrange & Act & Assert - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadPrimitiveItem(null, null, null), "primitiveWrapper"); - } + [Fact] + public void ReadPrimitiveItem_ThrowsArgumentNull_ResourceWrapper() + { + // Arrange & Act & Assert + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadPrimitiveItem(null, null, null), "primitiveWrapper"); + } - [Fact] - public void ReadResourceItem_ThrowsArgumentNull_ResourceWrapper() - { - // Arrange & Act & Assert - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadResourceItem(null, null, null), "resourceWrapper"); - } + [Fact] + public void ReadResourceItem_ThrowsArgumentNull_ResourceWrapper() + { + // Arrange & Act & Assert + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadResourceItem(null, null, null), "resourceWrapper"); + } - [Fact] - public void ReadResourceSetItem_ThrowsArgumentNull_ResourceSetWrapper() - { - // Arrange & Act & Assert - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadResourceSetItem(null, null, null), "resourceSetWrapper"); - } + [Fact] + public void ReadResourceSetItem_ThrowsArgumentNull_ResourceSetWrapper() + { + // Arrange & Act & Assert + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + ExceptionAssert.ThrowsArgumentNull(() => deserializer.ReadResourceSetItem(null, null, null), "resourceSetWrapper"); + } - [Fact] - public void ReadResourceItem_Throws_TypeCannotBeDeserialized() - { - Mock deserializerProvider = new Mock(); - ODataResourceSetDeserializer deserializer = new ODataResourceSetDeserializer(deserializerProvider.Object); - ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource()); - ODataDeserializerContext readContext = new ODataDeserializerContext(); + [Fact] + public void ReadResourceItem_Throws_TypeCannotBeDeserialized() + { + Mock deserializerProvider = new Mock(); + ODataResourceSetDeserializer deserializer = new ODataResourceSetDeserializer(deserializerProvider.Object); + ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(new ODataResource()); + ODataDeserializerContext readContext = new ODataDeserializerContext(); - deserializerProvider.Setup(p => p.GetEdmTypeDeserializer(_customerType, false)).Returns(null); + deserializerProvider.Setup(p => p.GetEdmTypeDeserializer(_customerType, false)).Returns(null); - ExceptionAssert.Throws( - () => deserializer.ReadResourceItem(resourceWrapper, _customerType, readContext), - "'Microsoft.AspNetCore.OData.Tests.Models.Customer' cannot be deserialized using the OData input formatter."); - } + ExceptionAssert.Throws( + () => deserializer.ReadResourceItem(resourceWrapper, _customerType, readContext), + "'Microsoft.AspNetCore.OData.Tests.Models.Customer' cannot be deserialized using the OData input formatter."); + } - [Fact] - public async Task ReadAsync_ReturnTypedCollection_WithTypeMode() + [Fact] + public async Task ReadAsync_ReturnTypedCollection_WithTypeMode() + { + // Arrange + HttpContent content = new StringContent("{ 'value': [ {'City' : 'Redmond' }, {'City' : 'Issaquah' } ] }"); + HeaderDictionary headerDict = new HeaderDictionary { - // Arrange - HttpContent content = new StringContent("{ 'value': [ {'City' : 'Redmond' }, {'City' : 'Issaquah' } ] }"); - HeaderDictionary headerDict = new HeaderDictionary - { - { "Content-Type", "application/json" } - }; - - IODataRequestMessage request = ODataMessageWrapperHelper.Create(await content.ReadAsStreamAsync(), headerDict); - ODataMessageReader reader = new ODataMessageReader(request, new ODataMessageReaderSettings(), _model); - IEdmComplexType addressType = _model.SchemaElements.OfType().First(c => c.Name == "Address"); - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _model, - ResourceType = typeof(IList
), - ResourceEdmType = new EdmCollectionTypeReference(new EdmCollectionType(addressType.ToEdmTypeReference(true))), - }; + { "Content-Type", "application/json" } + }; + + IODataRequestMessage request = ODataMessageWrapperHelper.Create(await content.ReadAsStreamAsync(), headerDict); + ODataMessageReader reader = new ODataMessageReader(request, new ODataMessageReaderSettings(), _model); + IEdmComplexType addressType = _model.SchemaElements.OfType().First(c => c.Name == "Address"); + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + ODataDeserializerContext readContext = new ODataDeserializerContext + { + Model = _model, + ResourceType = typeof(IList
), + ResourceEdmType = new EdmCollectionTypeReference(new EdmCollectionType(addressType.ToEdmTypeReference(true))), + }; - // Act - var result = await deserializer.ReadAsync(reader, typeof(IList
), readContext); + // Act + var result = await deserializer.ReadAsync(reader, typeof(IList
), readContext); - // Assert - IEnumerable
addresses = (result as IEnumerable).Cast
(); + // Assert + IEnumerable
addresses = (result as IEnumerable).Cast
(); - Assert.Equal(2, addresses.Count()); - Assert.Collection(addresses, - e => Assert.Equal("Redmond", e.City), - e => Assert.Equal("Issaquah", e.City)); - } + Assert.Equal(2, addresses.Count()); + Assert.Collection(addresses, + e => Assert.Equal("Redmond", e.City), + e => Assert.Equal("Issaquah", e.City)); + } - [Fact] - public async Task ReadAsync_ReturnsEdmUntypedCollection_WithTypeMode() + [Fact] + public async Task ReadAsync_ReturnsEdmUntypedCollection_WithTypeMode() + { + // Arrange + HttpContent content = new StringContent("{ 'value': [ 4, {'@odata.type':'Microsoft.AspNetCore.OData.Tests.Models.Address', 'City' : 'Redmond' } ] }"); + HeaderDictionary headerDict = new HeaderDictionary { - // Arrange - HttpContent content = new StringContent("{ 'value': [ 4, {'@odata.type':'Microsoft.AspNetCore.OData.Tests.Models.Address', 'City' : 'Redmond' } ] }"); - HeaderDictionary headerDict = new HeaderDictionary - { - { "Content-Type", "application/json" } - }; + { "Content-Type", "application/json" } + }; - IODataRequestMessage request = ODataMessageWrapperHelper.Create(await content.ReadAsStreamAsync(), headerDict); - ODataMessageReader reader = new ODataMessageReader(request, new ODataMessageReaderSettings(), _model); - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _model, - ResourceType = typeof(EdmUntypedCollection), - ResourceEdmType = EdmUntypedHelpers.NullableUntypedCollectionReference - }; + IODataRequestMessage request = ODataMessageWrapperHelper.Create(await content.ReadAsStreamAsync(), headerDict); + ODataMessageReader reader = new ODataMessageReader(request, new ODataMessageReaderSettings(), _model); + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + ODataDeserializerContext readContext = new ODataDeserializerContext + { + Model = _model, + ResourceType = typeof(EdmUntypedCollection), + ResourceEdmType = EdmUntypedHelpers.NullableUntypedCollectionReference + }; - // Act - var result = await deserializer.ReadAsync(reader, typeof(EdmUntypedCollection), readContext) as EdmUntypedCollection; + // Act + var result = await deserializer.ReadAsync(reader, typeof(EdmUntypedCollection), readContext) as EdmUntypedCollection; - // Assert - Assert.Equal(2, result.Count); - Assert.Equal(4, result[0]); - Address address = Assert.IsType
(result[1]); + // Assert + Assert.Equal(2, result.Count); + Assert.Equal(4, result[0]); + Address address = Assert.IsType
(result[1]); - Assert.Null(address.Street); - Assert.Equal("Redmond", address.City); - } + Assert.Null(address.Street); + Assert.Equal("Redmond", address.City); + } - [Fact] - public async Task ReadAsync_ReturnsEdmComplexObjectCollection_TypelessMode() + [Fact] + public async Task ReadAsync_ReturnsEdmComplexObjectCollection_TypelessMode() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + IEdmTypeReference addressType = new EdmComplexTypeReference(model.Address, true); + IEdmCollectionTypeReference addressCollectionType = + new EdmCollectionTypeReference(new EdmCollectionType(addressType)); + + HttpContent content = new StringContent("{ 'value': [ {'@odata.type':'NS.Address', 'City' : 'Redmond' } ] }"); + HeaderDictionary headerDict = new HeaderDictionary { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - IEdmTypeReference addressType = new EdmComplexTypeReference(model.Address, true); - IEdmCollectionTypeReference addressCollectionType = - new EdmCollectionTypeReference(new EdmCollectionType(addressType)); - - HttpContent content = new StringContent("{ 'value': [ {'@odata.type':'NS.Address', 'City' : 'Redmond' } ] }"); - HeaderDictionary headerDict = new HeaderDictionary - { - { "Content-Type", "application/json" } - }; + { "Content-Type", "application/json" } + }; - IODataRequestMessage request = ODataMessageWrapperHelper.Create(await content.ReadAsStreamAsync(), headerDict); - ODataMessageReader reader = new ODataMessageReader(request, new ODataMessageReaderSettings(), model.Model); - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = model.Model, - ResourceType = typeof(IEdmObject), - ResourceEdmType = addressCollectionType - }; + IODataRequestMessage request = ODataMessageWrapperHelper.Create(await content.ReadAsStreamAsync(), headerDict); + ODataMessageReader reader = new ODataMessageReader(request, new ODataMessageReaderSettings(), model.Model); + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + ODataDeserializerContext readContext = new ODataDeserializerContext + { + Model = model.Model, + ResourceType = typeof(IEdmObject), + ResourceEdmType = addressCollectionType + }; - // Act - IEnumerable result = await deserializer.ReadAsync(reader, typeof(IEdmObject), readContext) as IEnumerable; + // Act + IEnumerable result = await deserializer.ReadAsync(reader, typeof(IEdmObject), readContext) as IEnumerable; - // Assert - var addresses = result.Cast(); - Assert.NotNull(addresses); + // Assert + var addresses = result.Cast(); + Assert.NotNull(addresses); - EdmComplexObject address = Assert.Single(addresses); - Assert.Equal(new[] { "City" }, address.GetChangedPropertyNames()); + EdmComplexObject address = Assert.Single(addresses); + Assert.Equal(new[] { "City" }, address.GetChangedPropertyNames()); - object city; - Assert.True(address.TryGetPropertyValue("City", out city)); - Assert.Equal("Redmond", city); - } + object city; + Assert.True(address.TryGetPropertyValue("City", out city)); + Assert.Equal("Redmond", city); + } - [Fact(Skip = "See: https://github.com/OData/odata.net/issues/2662")] - public async Task ReadAsync_ReturnsEdmUntypedObjectCollection_UntypedMode() + [Fact(Skip = "See: https://github.com/OData/odata.net/issues/2662")] + public async Task ReadAsync_ReturnsEdmUntypedObjectCollection_UntypedMode() + { + // Arrange + HttpContent content = new StringContent("{ 'value': [42, null, { 'Any/name' : 'Redmond' } ] }"); + HeaderDictionary headerDict = new HeaderDictionary { - // Arrange - HttpContent content = new StringContent("{ 'value': [42, null, { 'Any/name' : 'Redmond' } ] }"); - HeaderDictionary headerDict = new HeaderDictionary - { - { "Content-Type", "application/json" } - }; + { "Content-Type", "application/json" } + }; - IODataRequestMessage request = ODataMessageWrapperHelper.Create(await content.ReadAsStreamAsync(), headerDict); - ODataMessageReader reader = new ODataMessageReader(request, new ODataMessageReaderSettings(), _model); - var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - ODataDeserializerContext readContext = new ODataDeserializerContext - { - Model = _model, - ResourceType = typeof(IEdmObject), - ResourceEdmType = EdmUntypedHelpers.NullableUntypedCollectionReference - }; + IODataRequestMessage request = ODataMessageWrapperHelper.Create(await content.ReadAsStreamAsync(), headerDict); + ODataMessageReader reader = new ODataMessageReader(request, new ODataMessageReaderSettings(), _model); + var deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + ODataDeserializerContext readContext = new ODataDeserializerContext + { + Model = _model, + ResourceType = typeof(IEdmObject), + ResourceEdmType = EdmUntypedHelpers.NullableUntypedCollectionReference + }; - // Act - EdmUntypedCollection result = await deserializer.ReadAsync(reader, typeof(IEdmObject), readContext) as EdmUntypedCollection; + // Act + EdmUntypedCollection result = await deserializer.ReadAsync(reader, typeof(IEdmObject), readContext) as EdmUntypedCollection; - // Assert - Assert.NotNull(result); + // Assert + Assert.NotNull(result); - Assert.Equal(3, result.Count); - } + Assert.Equal(3, result.Count); + } - [Fact] - public async Task ReadAsync_Roundtrip_ComplexCollection() - { - // Arrange - Address[] addresses = new[] - { - new Address { City ="Redmond", StreetAddress ="A", State ="123"}, - new Address { City ="Seattle", StreetAddress ="S", State ="321"} - }; - - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(_serializerProvider); - ODataResourceSetDeserializer deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - - MemoryStream stream = new MemoryStream(); - ODataMessageWrapper message = new ODataMessageWrapper(stream); - ODataMessageWriterSettings settings = new ODataMessageWriterSettings + [Fact] + public async Task ReadAsync_Roundtrip_ComplexCollection() + { + // Arrange + Address[] addresses = new[] { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } + new Address { City ="Redmond", StreetAddress ="A", State ="123"}, + new Address { City ="Seattle", StreetAddress ="S", State ="321"} }; - ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, _model); - ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), _model); - ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = _model }; - ODataDeserializerContext readContext = new ODataDeserializerContext() { Model = _model }; - - // Act - await serializer.WriteObjectAsync(addresses, addresses.GetType(), messageWriter, writeContext); - stream.Seek(0, SeekOrigin.Begin); - IEnumerable readAddresses = await deserializer.ReadAsync(messageReader, typeof(Address[]), readContext) as IEnumerable; - - // Assert - Assert.Equal(addresses, readAddresses.Cast
(), new AddressComparer()); - } - [Fact] - public void ReadResourceSet_Returns_UntypedCollection() + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(_serializerProvider); + ODataResourceSetDeserializer deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + + MemoryStream stream = new MemoryStream(); + ODataMessageWrapper message = new ODataMessageWrapper(stream); + ODataMessageWriterSettings settings = new ODataMessageWriterSettings { - // Arrange - ODataResourceSetWrapper setWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); - setWrapper.Items.Add(new ODataPrimitiveWrapper(new ODataPrimitiveValue(1115))); - setWrapper.Items.Add(null); - setWrapper.Items.Add(new ODataResourceWrapper(new ODataResource - { - Properties = new[] - { - new ODataProperty { Name = "City", Value = "Redmond" } - } - })); - setWrapper.Items.Add(new ODataResourceWrapper(new ODataResource - { - TypeName = "Microsoft.AspNetCore.OData.Tests.Models.Address", - Properties = new[] - { - new ODataProperty { Name = "City", Value = "Redmond" } - } - })); - setWrapper.Items.Add(new ODataResourceSetWrapper(new ODataResourceSet())); - - ODataResourceSetDeserializer deserializer = new ODataResourceSetDeserializer(_deserializerProvider); - - ODataDeserializerContext readContext = new ODataDeserializerContext() { Model = _model }; - - // Act - IEdmTypeReference edmType = EdmUntypedHelpers.NullableUntypedCollectionReference; - EdmUntypedCollection untypedCollection = deserializer.ReadInline(setWrapper, edmType, readContext) as EdmUntypedCollection; - - // Assert - Assert.Equal(5, untypedCollection.Count); - Assert.Collection(untypedCollection, - e => Assert.Equal(1115, e), - e => Assert.Null(e), - e => - { - // Without type name, goes to 'EdmUntypedObject' - KeyValuePair property = Assert.Single(Assert.IsType(e)); - Assert.Equal("City", property.Key); - Assert.Equal("Redmond", property.Value); - }, - e => - { - // Without type name, goes to corresponding CLR type - Address address = Assert.IsType
(e); - Assert.Equal("Redmond", address.City); - }, - e => - { - Assert.Empty(Assert.IsType(e)); - } - ); - } + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } + }; + ODataMessageWriter messageWriter = new ODataMessageWriter(message as IODataResponseMessage, settings, _model); + ODataMessageReader messageReader = new ODataMessageReader(message as IODataResponseMessage, new ODataMessageReaderSettings(), _model); + ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = _model }; + ODataDeserializerContext readContext = new ODataDeserializerContext() { Model = _model }; + + // Act + await serializer.WriteObjectAsync(addresses, addresses.GetType(), messageWriter, writeContext); + stream.Seek(0, SeekOrigin.Begin); + IEnumerable readAddresses = await deserializer.ReadAsync(messageReader, typeof(Address[]), readContext) as IEnumerable; + + // Assert + Assert.Equal(addresses, readAddresses.Cast
(), new AddressComparer()); + } - private class AddressComparer : IEqualityComparer
+ [Fact] + public void ReadResourceSet_Returns_UntypedCollection() + { + // Arrange + ODataResourceSetWrapper setWrapper = new ODataResourceSetWrapper(new ODataResourceSet()); + setWrapper.Items.Add(new ODataPrimitiveWrapper(new ODataPrimitiveValue(1115))); + setWrapper.Items.Add(null); + setWrapper.Items.Add(new ODataResourceWrapper(new ODataResource + { + Properties = new[] + { + new ODataProperty { Name = "City", Value = "Redmond" } + } + })); + setWrapper.Items.Add(new ODataResourceWrapper(new ODataResource { - public bool Equals(Address x, Address y) + TypeName = "Microsoft.AspNetCore.OData.Tests.Models.Address", + Properties = new[] { - return x.City == y.City && x.State == y.State && x.StreetAddress == y.StreetAddress && x.ZipCode == y.ZipCode; + new ODataProperty { Name = "City", Value = "Redmond" } } + })); + setWrapper.Items.Add(new ODataResourceSetWrapper(new ODataResourceSet())); - public int GetHashCode(Address obj) + ODataResourceSetDeserializer deserializer = new ODataResourceSetDeserializer(_deserializerProvider); + + ODataDeserializerContext readContext = new ODataDeserializerContext() { Model = _model }; + + // Act + IEdmTypeReference edmType = EdmUntypedHelpers.NullableUntypedCollectionReference; + EdmUntypedCollection untypedCollection = deserializer.ReadInline(setWrapper, edmType, readContext) as EdmUntypedCollection; + + // Assert + Assert.Equal(5, untypedCollection.Count); + Assert.Collection(untypedCollection, + e => Assert.Equal(1115, e), + e => Assert.Null(e), + e => + { + // Without type name, goes to 'EdmUntypedObject' + KeyValuePair property = Assert.Single(Assert.IsType(e)); + Assert.Equal("City", property.Key); + Assert.Equal("Redmond", property.Value); + }, + e => { - throw new NotImplementedException(); + // Without type name, goes to corresponding CLR type + Address address = Assert.IsType
(e); + Assert.Equal("Redmond", address.City); + }, + e => + { + Assert.Empty(Assert.IsType(e)); } + ); + } + + private class AddressComparer : IEqualityComparer
+ { + public bool Equals(Address x, Address y) + { + return x.City == y.City && x.State == y.State && x.StreetAddress == y.StreetAddress && x.ZipCode == y.ZipCode; } - private IEdmModel GetEdmModel() + public int GetHashCode(Address obj) { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("customers"); - builder.ComplexType
(); - return builder.GetEdmModel(); + throw new NotImplementedException(); } } + + private IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("customers"); + builder.ComplexType
(); + return builder.GetEdmModel(); + } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataSingletonDeserializerTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataSingletonDeserializerTest.cs index ae2b0881f..e5f214d90 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataSingletonDeserializerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Deserialization/ODataSingletonDeserializerTest.cs @@ -18,92 +18,91 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Deserialization; + +public class ODataSingletonDeserializerTest { - public class ODataSingletonDeserializerTest + private IEdmModel _edmModel; + private IEdmSingleton _singleton; + private readonly ODataDeserializerContext _readContext; + private readonly IODataDeserializerProvider _deserializerProvider; + + private sealed class EmployeeModel { - private IEdmModel _edmModel; - private IEdmSingleton _singleton; - private readonly ODataDeserializerContext _readContext; - private readonly IODataDeserializerProvider _deserializerProvider; + public int EmployeeId { get; set; } + public string EmployeeName { get; set; } + } - private sealed class EmployeeModel - { - public int EmployeeId { get; set; } - public string EmployeeName { get; set; } - } + public ODataSingletonDeserializerTest() + { + EdmModel model = new EdmModel(); + var employeeType = new EdmEntityType("NS", "Employee"); + employeeType.AddStructuralProperty("EmployeeId", EdmPrimitiveTypeKind.Int32); + employeeType.AddStructuralProperty("EmployeeName", EdmPrimitiveTypeKind.String); + model.AddElement(employeeType); - public ODataSingletonDeserializerTest() - { - EdmModel model = new EdmModel(); - var employeeType = new EdmEntityType("NS", "Employee"); - employeeType.AddStructuralProperty("EmployeeId", EdmPrimitiveTypeKind.Int32); - employeeType.AddStructuralProperty("EmployeeName", EdmPrimitiveTypeKind.String); - model.AddElement(employeeType); + EdmEntityContainer defaultContainer = new EdmEntityContainer("NS", "Default"); + model.AddElement(defaultContainer); - EdmEntityContainer defaultContainer = new EdmEntityContainer("NS", "Default"); - model.AddElement(defaultContainer); + _singleton = new EdmSingleton(defaultContainer, "CEO", employeeType); + defaultContainer.AddElement(_singleton); - _singleton = new EdmSingleton(defaultContainer, "CEO", employeeType); - defaultContainer.AddElement(_singleton); + model.SetAnnotationValue(employeeType, new ClrTypeAnnotation(typeof(EmployeeModel))); - model.SetAnnotationValue(employeeType, new ClrTypeAnnotation(typeof(EmployeeModel))); + _edmModel = model; - _edmModel = model; + _readContext = new ODataDeserializerContext + { + Path = new ODataPath(new SingletonSegment(_singleton)), + Model = _edmModel, + ResourceType = typeof(EmployeeModel) + }; - _readContext = new ODataDeserializerContext - { - Path = new ODataPath(new SingletonSegment(_singleton)), - Model = _edmModel, - ResourceType = typeof(EmployeeModel) - }; + _deserializerProvider = DeserializationServiceProviderHelper.GetServiceProvider().GetRequiredService(); + } - _deserializerProvider = DeserializationServiceProviderHelper.GetServiceProvider().GetRequiredService(); - } + [Fact] + public async Task CanDeserializerSingletonPayloadFromStream() + { + // Arrange + const string payload = "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#CEO\"," + + "\"EmployeeId\":789," + + "\"EmployeeName\":\"John Hark\"}"; + + ODataResourceDeserializer deserializer = new ODataResourceDeserializer(_deserializerProvider); + + // Act + EmployeeModel employee = await deserializer.ReadAsync( + GetODataMessageReader(payload), + typeof(EmployeeModel), + _readContext) as EmployeeModel; + + // Assert + Assert.NotNull(employee); + Assert.Equal(789, employee.EmployeeId); + Assert.Equal("John Hark", employee.EmployeeName); + } - [Fact] - public async Task CanDeserializerSingletonPayloadFromStream() - { - // Arrange - const string payload = "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#CEO\"," + - "\"EmployeeId\":789," + - "\"EmployeeName\":\"John Hark\"}"; - - ODataResourceDeserializer deserializer = new ODataResourceDeserializer(_deserializerProvider); - - // Act - EmployeeModel employee = await deserializer.ReadAsync( - GetODataMessageReader(payload), - typeof(EmployeeModel), - _readContext) as EmployeeModel; - - // Assert - Assert.NotNull(employee); - Assert.Equal(789, employee.EmployeeId); - Assert.Equal("John Hark", employee.EmployeeName); - } - - private ODataMessageReader GetODataMessageReader(string content) - { - HttpRequest request = RequestFactory.Create("Post", "http://localhost/odata/CEO", opt => opt.AddRouteComponents("odata", _edmModel)); + private ODataMessageReader GetODataMessageReader(string content) + { + HttpRequest request = RequestFactory.Create("Post", "http://localhost/odata/CEO", opt => opt.AddRouteComponents("odata", _edmModel)); - //request.Content = new StringContent(content); - //request.Headers.Add("OData-Version", "4.0"); + //request.Content = new StringContent(content); + //request.Headers.Add("OData-Version", "4.0"); - //MediaTypeWithQualityHeaderValue mediaType = new MediaTypeWithQualityHeaderValue("application/json"); - //mediaType.Parameters.Add(new NameValueHeaderValue("odata.metadata", "full")); - //request.Headers.Accept.Add(mediaType); - //request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + //MediaTypeWithQualityHeaderValue mediaType = new MediaTypeWithQualityHeaderValue("application/json"); + //mediaType.Parameters.Add(new NameValueHeaderValue("odata.metadata", "full")); + //request.Headers.Accept.Add(mediaType); + //request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - byte[] contentBytes = Encoding.UTF8.GetBytes(content); - request.Body = new MemoryStream(contentBytes); - request.ContentType = "application/json"; - request.ContentLength = contentBytes.Length; - request.Headers.Append("OData-Version", "4.0"); - request.Headers.Append("Accept", "application/json;odata.metadata=full"); + byte[] contentBytes = Encoding.UTF8.GetBytes(content); + request.Body = new MemoryStream(contentBytes); + request.ContentType = "application/json"; + request.ContentLength = contentBytes.Length; + request.Headers.Append("OData-Version", "4.0"); + request.Headers.Append("Accept", "application/json;odata.metadata=full"); - return new ODataMessageReader(new HttpRequestODataMessage(request), new ODataMessageReaderSettings(), _edmModel); - } + return new ODataMessageReader(new HttpRequestODataMessage(request), new ODataMessageReaderSettings(), _edmModel); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/FromODataBodyAttributeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/FromODataBodyAttributeTests.cs index 31d0b7a7d..8a2af2124 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/FromODataBodyAttributeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/FromODataBodyAttributeTests.cs @@ -8,18 +8,17 @@ using Microsoft.AspNetCore.OData.Formatter; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class FromODataBodyAttributeTests { - public class FromODataBodyAttributeTests + [Fact] + public void Ctor_ConfigureODataBodyModelBinder() { - [Fact] - public void Ctor_ConfigureODataBodyModelBinder() - { - // Arrange - FromODataBodyAttribute attribute = new FromODataBodyAttribute(); + // Arrange + FromODataBodyAttribute attribute = new FromODataBodyAttribute(); - // Act & Assert - Assert.Equal(typeof(ODataBodyModelBinder), attribute.BinderType); - } + // Act & Assert + Assert.Equal(typeof(ODataBodyModelBinder), attribute.BinderType); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/LinkGenerationHelpersTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/LinkGenerationHelpersTest.cs index 38c5ff4b6..6293bc52b 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/LinkGenerationHelpersTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/LinkGenerationHelpersTest.cs @@ -21,753 +21,752 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class LinkGenerationHelpersTest { - public class LinkGenerationHelpersTest + private static IEdmModel _model; + private static IEdmEntitySet _customers; + private static IEdmEntityType _customer; + private static IEdmEntityType _specialCustomer; + private static IEdmModel _myOrderModel = GetEdmModel2(); + + static LinkGenerationHelpersTest() { - private static IEdmModel _model; - private static IEdmEntitySet _customers; - private static IEdmEntityType _customer; - private static IEdmEntityType _specialCustomer; - private static IEdmModel _myOrderModel = GetEdmModel2(); + _model = GetEdmModel(); + _customers = _model.EntityContainer.FindEntitySet("Customers"); + _customer = _model.SchemaElements.OfType().First(c => c.Name == "Customer"); + _specialCustomer = _model.SchemaElements.OfType().First(c => c.Name == "SpecialCustomer"); + } - static LinkGenerationHelpersTest() - { - _model = GetEdmModel(); - _customers = _model.EntityContainer.FindEntitySet("Customers"); - _customer = _model.SchemaElements.OfType().First(c => c.Name == "Customer"); - _specialCustomer = _model.SchemaElements.OfType().First(c => c.Name == "SpecialCustomer"); - } - - [Theory] - [InlineData(false, "http://localhost/Customers(42)")] - [InlineData(true, "http://localhost/Customers(42)/NS.SpecialCustomer")] - public void GenerateSelfLink_WorksToGenerateExpectedSelfLink_ForEntitySet(bool includeCast, string expectedIdLink) - { - // Arrange - var request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, _customers, request); - var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); - - // Act - var idLink = entityContext.GenerateSelfLink(includeCast); - - // Assert - Assert.Equal(expectedIdLink, idLink.ToString()); - } - - [Theory] - [InlineData(false, "http://localhost/Customers(42)/Orders")] - [InlineData(true, "http://localhost/Customers(42)/NS.SpecialCustomer/Orders")] - public void GenerateNavigationLink_WorksToGenerateExpectedNavigationLink_ForEntitySet(bool includeCast, string expectedNavigationLink) - { - // Arrange - var request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, _customers, request); - var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); - IEdmNavigationProperty ordersProperty = _customer.NavigationProperties().Single(); - - // Act - Uri uri = entityContext.GenerateNavigationPropertyLink(ordersProperty, includeCast); - - // Assert - Assert.Equal(expectedNavigationLink, uri.AbsoluteUri); - } - - [Theory] - [InlineData(false, "http://localhost/Mary")] - [InlineData(true, "http://localhost/Mary/NS.SpecialCustomer")] - public void GenerateSelfLink_WorksToGenerateExpectedSelfLink_ForSingleton(bool includeCast, string expectedIdLink) - { - // Arrange - IEdmSingleton mary = new EdmSingleton(_model.EntityContainer, "Mary", _customer); - var request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, mary, request); - var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); - - // Act - var idLink = entityContext.GenerateSelfLink(includeCast); - - // Assert - Assert.Equal(expectedIdLink, idLink.ToString()); - } - - [Theory] - [InlineData(false, "http://localhost/Mary/Orders")] - [InlineData(true, "http://localhost/Mary/NS.SpecialCustomer/Orders")] - public void GenerateNavigationLink_WorksToGenerateExpectedNavigationLink_ForSingleton(bool includeCast, string expectedNavigationLink) - { - // Arrange - IEdmSingleton mary = new EdmSingleton(_model.EntityContainer, "Mary", _customer); - var request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, mary, request); - var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); - IEdmNavigationProperty ordersProperty = _customer.NavigationProperties().Single(); + [Theory] + [InlineData(false, "http://localhost/Customers(42)")] + [InlineData(true, "http://localhost/Customers(42)/NS.SpecialCustomer")] + public void GenerateSelfLink_WorksToGenerateExpectedSelfLink_ForEntitySet(bool includeCast, string expectedIdLink) + { + // Arrange + var request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, _customers, request); + var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); - // Act - Uri uri = entityContext.GenerateNavigationPropertyLink(ordersProperty, includeCast); + // Act + var idLink = entityContext.GenerateSelfLink(includeCast); - // Assert - Assert.Equal(expectedNavigationLink, uri.AbsoluteUri); - } + // Assert + Assert.Equal(expectedIdLink, idLink.ToString()); + } - private ResourceContext GetOrderLineResourceForNewSingletonContainer() - { - // Arrange - IEdmSingleton myVipOrder = _myOrderModel.FindDeclaredSingleton("VipOrder"); - IEdmEntityType vipOrderType = (IEdmEntityType)myVipOrder.Type; - IEdmNavigationProperty orderLinesProperty = vipOrderType.NavigationProperties().Single(x => x.ContainsTarget && x.Name == "OrderLines"); - IEdmContainedEntitySet orderLines = (IEdmContainedEntitySet)myVipOrder.FindNavigationTarget(orderLinesProperty); - IEdmEntityType orderLine = _myOrderModel.SchemaElements.OfType().First(e => e.Name == "OrderLine"); - IEdmNavigationProperty orderLineDetailsNav = orderLine.NavigationProperties().First(); - - HttpRequest request = RequestFactory.Create(_myOrderModel); - - ODataPath path = new ODataPath( - new SingletonSegment(myVipOrder), - new NavigationPropertySegment(orderLinesProperty, orderLines)); - - ODataSerializerContext orderLineSerializerContext = ODataSerializerContextFactory.Create(_myOrderModel, orderLines, path, request); - orderLineSerializerContext.EdmProperty = orderLineDetailsNav; - ResourceContext orderLineResource = new ResourceContext(orderLineSerializerContext, orderLine.AsReference(), new { ID = 21 }); - orderLineSerializerContext.ExpandedResource = orderLineResource; - - return orderLineResource; - } - - [Fact] - public void GenerateBaseODataPathSegments_WorksToGenerateExpectedPath_ForSingletonContainer() - { - // Arrange - ResourceContext orderLineResource = GetOrderLineResourceForNewSingletonContainer(); + [Theory] + [InlineData(false, "http://localhost/Customers(42)/Orders")] + [InlineData(true, "http://localhost/Customers(42)/NS.SpecialCustomer/Orders")] + public void GenerateNavigationLink_WorksToGenerateExpectedNavigationLink_ForEntitySet(bool includeCast, string expectedNavigationLink) + { + // Arrange + var request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, _customers, request); + var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); + IEdmNavigationProperty ordersProperty = _customer.NavigationProperties().Single(); - // Act - IList newPaths = orderLineResource.GenerateBaseODataPathSegments(); + // Act + Uri uri = entityContext.GenerateNavigationPropertyLink(ordersProperty, includeCast); - // Assert - Assert.Equal(3, newPaths.Count); - Assert.IsType(newPaths[0]); // VipOrder - Assert.IsType(newPaths[1]); // OrderLines - Assert.IsType(newPaths[2]); // 21 - } + // Assert + Assert.Equal(expectedNavigationLink, uri.AbsoluteUri); + } - [Fact] - public void GenerateSelfLink_WorksToGenerateExpectedSelfLink_ForSingletonContainer() - { - // Arrange - ResourceContext orderLineResource = GetOrderLineResourceForNewSingletonContainer(); - - // Act - Uri selfLink = orderLineResource.GenerateSelfLink(false); - - // Assert - Assert.Equal("http://localhost/VipOrder/OrderLines(21)", selfLink.AbsoluteUri); - } - - [Theory] - [InlineData(false, "http://localhost/MyOrders(42)/OrderLines(21)/OrderLines")] - [InlineData(true, "http://localhost/MyOrders(42)/OrderLines(21)/NS.OrderLine/OrderLines")] - public void GenerateNavigationLink_WorksToGenerateExpectedNavigationLink_ForContainedNavigation( - bool includeCast, - string expectedNavigationLink) - { - // NOTE: This test is generating a link that does not technically correspond to a valid model (specifically - // the extra OrderLines navigation), but it allows us to validate the nested navigation scenario - // without twisting the model unnecessarily. + [Theory] + [InlineData(false, "http://localhost/Mary")] + [InlineData(true, "http://localhost/Mary/NS.SpecialCustomer")] + public void GenerateSelfLink_WorksToGenerateExpectedSelfLink_ForSingleton(bool includeCast, string expectedIdLink) + { + // Arrange + IEdmSingleton mary = new EdmSingleton(_model.EntityContainer, "Mary", _customer); + var request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, mary, request); + var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); + + // Act + var idLink = entityContext.GenerateSelfLink(includeCast); + + // Assert + Assert.Equal(expectedIdLink, idLink.ToString()); + } + + [Theory] + [InlineData(false, "http://localhost/Mary/Orders")] + [InlineData(true, "http://localhost/Mary/NS.SpecialCustomer/Orders")] + public void GenerateNavigationLink_WorksToGenerateExpectedNavigationLink_ForSingleton(bool includeCast, string expectedNavigationLink) + { + // Arrange + IEdmSingleton mary = new EdmSingleton(_model.EntityContainer, "Mary", _customer); + var request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, mary, request); + var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); + IEdmNavigationProperty ordersProperty = _customer.NavigationProperties().Single(); + + // Act + Uri uri = entityContext.GenerateNavigationPropertyLink(ordersProperty, includeCast); + + // Assert + Assert.Equal(expectedNavigationLink, uri.AbsoluteUri); + } + + private ResourceContext GetOrderLineResourceForNewSingletonContainer() + { + // Arrange + IEdmSingleton myVipOrder = _myOrderModel.FindDeclaredSingleton("VipOrder"); + IEdmEntityType vipOrderType = (IEdmEntityType)myVipOrder.Type; + IEdmNavigationProperty orderLinesProperty = vipOrderType.NavigationProperties().Single(x => x.ContainsTarget && x.Name == "OrderLines"); + IEdmContainedEntitySet orderLines = (IEdmContainedEntitySet)myVipOrder.FindNavigationTarget(orderLinesProperty); + IEdmEntityType orderLine = _myOrderModel.SchemaElements.OfType().First(e => e.Name == "OrderLine"); + IEdmNavigationProperty orderLineDetailsNav = orderLine.NavigationProperties().First(); + + HttpRequest request = RequestFactory.Create(_myOrderModel); + + ODataPath path = new ODataPath( + new SingletonSegment(myVipOrder), + new NavigationPropertySegment(orderLinesProperty, orderLines)); + + ODataSerializerContext orderLineSerializerContext = ODataSerializerContextFactory.Create(_myOrderModel, orderLines, path, request); + orderLineSerializerContext.EdmProperty = orderLineDetailsNav; + ResourceContext orderLineResource = new ResourceContext(orderLineSerializerContext, orderLine.AsReference(), new { ID = 21 }); + orderLineSerializerContext.ExpandedResource = orderLineResource; + + return orderLineResource; + } + + [Fact] + public void GenerateBaseODataPathSegments_WorksToGenerateExpectedPath_ForSingletonContainer() + { + // Arrange + ResourceContext orderLineResource = GetOrderLineResourceForNewSingletonContainer(); + + // Act + IList newPaths = orderLineResource.GenerateBaseODataPathSegments(); + + // Assert + Assert.Equal(3, newPaths.Count); + Assert.IsType(newPaths[0]); // VipOrder + Assert.IsType(newPaths[1]); // OrderLines + Assert.IsType(newPaths[2]); // 21 + } + + [Fact] + public void GenerateSelfLink_WorksToGenerateExpectedSelfLink_ForSingletonContainer() + { + // Arrange + ResourceContext orderLineResource = GetOrderLineResourceForNewSingletonContainer(); - // Arrange - IEdmEntityType myOrder = (IEdmEntityType)_myOrderModel.FindDeclaredType("NS.MyOrder"); - IEdmEntityType orderLine = (IEdmEntityType)_myOrderModel.FindDeclaredType("NS.OrderLine"); + // Act + Uri selfLink = orderLineResource.GenerateSelfLink(false); - IEdmNavigationProperty orderLinesProperty = myOrder.NavigationProperties().Single(x => x.ContainsTarget && x.Name == "OrderLines"); + // Assert + Assert.Equal("http://localhost/VipOrder/OrderLines(21)", selfLink.AbsoluteUri); + } + + [Theory] + [InlineData(false, "http://localhost/MyOrders(42)/OrderLines(21)/OrderLines")] + [InlineData(true, "http://localhost/MyOrders(42)/OrderLines(21)/NS.OrderLine/OrderLines")] + public void GenerateNavigationLink_WorksToGenerateExpectedNavigationLink_ForContainedNavigation( + bool includeCast, + string expectedNavigationLink) + { + // NOTE: This test is generating a link that does not technically correspond to a valid model (specifically + // the extra OrderLines navigation), but it allows us to validate the nested navigation scenario + // without twisting the model unnecessarily. + + // Arrange + IEdmEntityType myOrder = (IEdmEntityType)_myOrderModel.FindDeclaredType("NS.MyOrder"); + IEdmEntityType orderLine = (IEdmEntityType)_myOrderModel.FindDeclaredType("NS.OrderLine"); + + IEdmNavigationProperty orderLinesProperty = myOrder.NavigationProperties().Single(x => x.ContainsTarget && x.Name == "OrderLines"); - IEdmEntitySet entitySet = _myOrderModel.FindDeclaredEntitySet("MyOrders"); - IDictionary parameters = new Dictionary - { - {"ID", 42} - }; - - IDictionary parameters2 = new Dictionary - { - {"ID", 21} - }; - - // containment - IEdmContainedEntitySet orderLines = (IEdmContainedEntitySet)entitySet.FindNavigationTarget(orderLinesProperty); - - ODataPath path = new ODataPath( - new EntitySetSegment(entitySet), - new KeySegment(parameters.ToArray(), myOrder, entitySet), - new NavigationPropertySegment(orderLinesProperty, orderLines), - new KeySegment(parameters2.ToArray(), orderLine, orderLines)); - - var request = RequestFactory.Create(_myOrderModel); - var serializerContext = ODataSerializerContextFactory.Create(_myOrderModel, orderLines, path, request); - var entityContext = new ResourceContext(serializerContext, orderLine.AsReference(), new { ID = 21 }); - - // Act - Uri uri = entityContext.GenerateNavigationPropertyLink(orderLinesProperty, includeCast); - - // Assert - Assert.Equal(expectedNavigationLink, uri.AbsoluteUri); - } - - [Fact] - public void GenerateNavigationLink_WorksToGenerateExpectedNavigationLink_ForNonContainedNavigation() - { - // Arrange - IEdmEntityType myOrder = (IEdmEntityType)_myOrderModel.FindDeclaredType("NS.MyOrder"); - IEdmEntityType orderLine = (IEdmEntityType)_myOrderModel.FindDeclaredType("NS.OrderLine"); - IEdmNavigationProperty nonOrderLinesProperty = myOrder.NavigationProperties().Single(x => x.Name.Equals("NonContainedOrderLines")); - - IEdmEntitySet entitySet = _myOrderModel.FindDeclaredEntitySet("MyOrders"); - IDictionary parameters = new Dictionary - { - {"ID", 42} - }; - - IDictionary parameters2 = new Dictionary - { - {"ID", 21} - }; - - IEdmNavigationSource nonContainedOrderLines = entitySet.FindNavigationTarget(nonOrderLinesProperty); - ODataPath path = new ODataPath( - new EntitySetSegment(entitySet), - new KeySegment(parameters.ToArray(), myOrder, entitySet), - new NavigationPropertySegment(nonOrderLinesProperty, nonContainedOrderLines), - new KeySegment(parameters2.ToArray(), orderLine, nonContainedOrderLines)); - - IEdmNavigationProperty orderLinesProperty = myOrder.NavigationProperties().Single(x => x.ContainsTarget); - IEdmContainedEntitySet orderLines = (IEdmContainedEntitySet)entitySet.FindNavigationTarget(orderLinesProperty); - - var request = RequestFactory.Create(_myOrderModel); - var serializerContext = ODataSerializerContextFactory.Create(_myOrderModel, orderLines, path, request); - var entityContext = new ResourceContext(serializerContext, orderLine.AsReference(), new { ID = 21 }); - - // Act - Uri uri = entityContext.GenerateSelfLink(false); - - // Assert - Assert.Equal("http://localhost/OrderLines(21)", uri.AbsoluteUri); - } - - [Fact] - public void GenerateSelfLink_ThrowsArgumentNull_EntityContext() + IEdmEntitySet entitySet = _myOrderModel.FindDeclaredEntitySet("MyOrders"); + IDictionary parameters = new Dictionary { - ExceptionAssert.ThrowsArgumentNull( - () => LinkGenerationHelpers.GenerateSelfLink(resourceContext: null, includeCast: false), - "resourceContext"); - } - - //[Fact] - //public void GenerateSelfLink_ThrowsArgument_IfUrlHelperIsNull() - //{ - // ResourceContext context = new ResourceContext(); - - // ExceptionAssert.ThrowsArgument( - // () => LinkGenerationHelpers.GenerateSelfLink(context, includeCast: false), - // "resourceContext", - // "The property 'Url' of ResourceContext cannot be null."); - //} - - [Fact] - public void GenerateNavigationPropertyLink_ThrowsArgumentNull_EntityContext() - { - IEdmNavigationProperty navigationProperty = new Mock().Object; - - ExceptionAssert.ThrowsArgumentNull( - () => LinkGenerationHelpers.GenerateNavigationPropertyLink(resourceContext: null, navigationProperty: navigationProperty, includeCast: false), - "resourceContext"); - } - - //[Fact] - //public void GenerateNavigationPropertyLink_ThrowsArgument_IfUrlHelperIsNull() - //{ - // IEdmNavigationProperty navigationProperty = new Mock().Object; - // ResourceContext context = new ResourceContext(); - - // ExceptionAssert.ThrowsArgument( - // () => LinkGenerationHelpers.GenerateNavigationPropertyLink(context, navigationProperty, includeCast: false), - // "resourceContext", - // "The property 'Url' of ResourceContext cannot be null."); - //} - - [Fact] - public void GenerateActionLinkForFeed_ThrowsArgumentNull_FeedContext() + {"ID", 42} + }; + + IDictionary parameters2 = new Dictionary { - // Arrange - ResourceSetContext feedContext = null; - IEdmAction action = new Mock().Object; + {"ID", 21} + }; - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => feedContext.GenerateActionLink(action), "resourceSetContext"); - } + // containment + IEdmContainedEntitySet orderLines = (IEdmContainedEntitySet)entitySet.FindNavigationTarget(orderLinesProperty); - [Fact] - public void GenerateActionLinkForFeed_ThrowsArgumentNull_Action() - { - // Arrange - ResourceSetContext resourceSetContext = new ResourceSetContext(); + ODataPath path = new ODataPath( + new EntitySetSegment(entitySet), + new KeySegment(parameters.ToArray(), myOrder, entitySet), + new NavigationPropertySegment(orderLinesProperty, orderLines), + new KeySegment(parameters2.ToArray(), orderLine, orderLines)); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => resourceSetContext.GenerateActionLink(action: null), "action"); - } + var request = RequestFactory.Create(_myOrderModel); + var serializerContext = ODataSerializerContextFactory.Create(_myOrderModel, orderLines, path, request); + var entityContext = new ResourceContext(serializerContext, orderLine.AsReference(), new { ID = 21 }); - [Fact] - public void GenerateActionLinkForFeed_ThrowsActionNotBoundToCollectionOfEntity_IfActionHasNoParameters() + // Act + Uri uri = entityContext.GenerateNavigationPropertyLink(orderLinesProperty, includeCast); + + // Assert + Assert.Equal(expectedNavigationLink, uri.AbsoluteUri); + } + + [Fact] + public void GenerateNavigationLink_WorksToGenerateExpectedNavigationLink_ForNonContainedNavigation() + { + // Arrange + IEdmEntityType myOrder = (IEdmEntityType)_myOrderModel.FindDeclaredType("NS.MyOrder"); + IEdmEntityType orderLine = (IEdmEntityType)_myOrderModel.FindDeclaredType("NS.OrderLine"); + IEdmNavigationProperty nonOrderLinesProperty = myOrder.NavigationProperties().Single(x => x.Name.Equals("NonContainedOrderLines")); + + IEdmEntitySet entitySet = _myOrderModel.FindDeclaredEntitySet("MyOrders"); + IDictionary parameters = new Dictionary { - // Arrange - ResourceSetContext context = new ResourceSetContext(); - Mock action = new Mock(); - action.Setup(a => a.Parameters).Returns(Enumerable.Empty()); - action.Setup(a => a.Name).Returns("SomeAction"); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => context.GenerateActionLink(action.Object), - "action", - "The action 'SomeAction' is not bound to the collection of entity. Only actions that are bound to entities can have action links."); - } - - [Fact] - public void GenerateActionLinkForFeed_GeneratesLinkWithoutCast_IfEntitySetTypeMatchesActionEntityType() + {"ID", 42} + }; + + IDictionary parameters2 = new Dictionary { - // Arrange - var request = RequestFactory.Create(_model); - IEdmAction action = _model.SchemaElements.OfType().First(a => a.Name == "UpgradeAll"); - Assert.NotNull(action); // Guard + {"ID", 21} + }; - ResourceSetContext context = new ResourceSetContext { EntitySetBase = _customers, Request = request }; + IEdmNavigationSource nonContainedOrderLines = entitySet.FindNavigationTarget(nonOrderLinesProperty); + ODataPath path = new ODataPath( + new EntitySetSegment(entitySet), + new KeySegment(parameters.ToArray(), myOrder, entitySet), + new NavigationPropertySegment(nonOrderLinesProperty, nonContainedOrderLines), + new KeySegment(parameters2.ToArray(), orderLine, nonContainedOrderLines)); - // Act - Uri link = context.GenerateActionLink(action); + IEdmNavigationProperty orderLinesProperty = myOrder.NavigationProperties().Single(x => x.ContainsTarget); + IEdmContainedEntitySet orderLines = (IEdmContainedEntitySet)entitySet.FindNavigationTarget(orderLinesProperty); - Assert.Equal("http://localhost/Customers/NS.UpgradeAll", link.AbsoluteUri); - } + var request = RequestFactory.Create(_myOrderModel); + var serializerContext = ODataSerializerContextFactory.Create(_myOrderModel, orderLines, path, request); + var entityContext = new ResourceContext(serializerContext, orderLine.AsReference(), new { ID = 21 }); - [Fact] - public void GenerateActionLinkForFeed_GeneratesLinkWithCast_IfEntitySetTypeDoesnotMatchActionEntityType() - { - // Arrange - var request = RequestFactory.Create(_model); - IEdmAction action = _model.SchemaElements.OfType().First(a => a.Name == "UpgradeSpecialAll"); - Assert.NotNull(action); // Guard - ResourceSetContext context = new ResourceSetContext { EntitySetBase = _customers, Request = request }; + // Act + Uri uri = entityContext.GenerateSelfLink(false); - // Act - Uri link = context.GenerateActionLink(action); + // Assert + Assert.Equal("http://localhost/OrderLines(21)", uri.AbsoluteUri); + } - // Assert - Assert.Equal("http://localhost/Customers/NS.SpecialCustomer/NS.UpgradeSpecialAll", link.AbsoluteUri); - } + [Fact] + public void GenerateSelfLink_ThrowsArgumentNull_EntityContext() + { + ExceptionAssert.ThrowsArgumentNull( + () => LinkGenerationHelpers.GenerateSelfLink(resourceContext: null, includeCast: false), + "resourceContext"); + } - [Fact] - public void GenerateActionLinkForFeed_GeneratesLinkWithDownCast_IfElementTypeDerivesFromBindingParameterType() - { - // Arrange - var request = RequestFactory.Create(_model); - IEdmAction action = _model.SchemaElements.OfType().First(a => a.Name == "UpgradeAll"); - Assert.NotNull(action); // Guard - IEdmEntitySet specialCustomers = new EdmEntitySet(_model.EntityContainer, "SpecialCustomers", _specialCustomer); + //[Fact] + //public void GenerateSelfLink_ThrowsArgument_IfUrlHelperIsNull() + //{ + // ResourceContext context = new ResourceContext(); - ResourceSetContext context = new ResourceSetContext { EntitySetBase = specialCustomers, Request = request }; + // ExceptionAssert.ThrowsArgument( + // () => LinkGenerationHelpers.GenerateSelfLink(context, includeCast: false), + // "resourceContext", + // "The property 'Url' of ResourceContext cannot be null."); + //} - // Act - Uri link = context.GenerateActionLink(action); + [Fact] + public void GenerateNavigationPropertyLink_ThrowsArgumentNull_EntityContext() + { + IEdmNavigationProperty navigationProperty = new Mock().Object; - // Assert - Assert.Equal("http://localhost/SpecialCustomers/NS.Customer/NS.UpgradeAll", link.AbsoluteUri); - } + ExceptionAssert.ThrowsArgumentNull( + () => LinkGenerationHelpers.GenerateNavigationPropertyLink(resourceContext: null, navigationProperty: navigationProperty, includeCast: false), + "resourceContext"); + } - [Fact] - public void GenerateActionLink_ThrowsArgumentNull_EntityContext() - { - ResourceContext entityContext = null; - IEdmActionImport action = new Mock().Object; + //[Fact] + //public void GenerateNavigationPropertyLink_ThrowsArgument_IfUrlHelperIsNull() + //{ + // IEdmNavigationProperty navigationProperty = new Mock().Object; + // ResourceContext context = new ResourceContext(); - ExceptionAssert.ThrowsArgumentNull(() => entityContext.GenerateActionLink(action.Action), "resourceContext"); - } + // ExceptionAssert.ThrowsArgument( + // () => LinkGenerationHelpers.GenerateNavigationPropertyLink(context, navigationProperty, includeCast: false), + // "resourceContext", + // "The property 'Url' of ResourceContext cannot be null."); + //} - [Fact] - public void GenerateActionLink_ThrowsArgumentNull_Action() - { - ResourceContext entityContext = new ResourceContext(); + [Fact] + public void GenerateActionLinkForFeed_ThrowsArgumentNull_FeedContext() + { + // Arrange + ResourceSetContext feedContext = null; + IEdmAction action = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => entityContext.GenerateActionLink(action: null), "action"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => feedContext.GenerateActionLink(action), "resourceSetContext"); + } - [Fact] - public void GenerateActionLink_ThrowsActionNotBoundToEntity_IfActionHasNoParameters() - { - ResourceContext entityContext = new ResourceContext(); - Mock action = new Mock(); - action.Setup(a => a.Parameters).Returns(Enumerable.Empty()); - action.Setup(a => a.Name).Returns("SomeAction"); - - ExceptionAssert.ThrowsArgument( - () => entityContext.GenerateActionLink(action.Object), - "action", - "The action 'SomeAction' is not bound to an entity. Only actions that are bound to entities can have action links."); - } - - [Fact] - public void GenerateActionLink_GeneratesLinkWithoutCast_IfEntitySetTypeMatchesActionEntityType() - { - // Arrange - var request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, _customers, request); - var entityContext = new ResourceContext(serializerContext, _customer.AsReference(), new { ID = 42 }); - IEdmAction upgradeCustomer = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "upgrade"); - Assert.NotNull(upgradeCustomer); + [Fact] + public void GenerateActionLinkForFeed_ThrowsArgumentNull_Action() + { + // Arrange + ResourceSetContext resourceSetContext = new ResourceSetContext(); - // Act - Uri link = entityContext.GenerateActionLink(upgradeCustomer); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => resourceSetContext.GenerateActionLink(action: null), "action"); + } - Assert.Equal("http://localhost/Customers(42)/NS.upgrade", link.AbsoluteUri); - } + [Fact] + public void GenerateActionLinkForFeed_ThrowsActionNotBoundToCollectionOfEntity_IfActionHasNoParameters() + { + // Arrange + ResourceSetContext context = new ResourceSetContext(); + Mock action = new Mock(); + action.Setup(a => a.Parameters).Returns(Enumerable.Empty()); + action.Setup(a => a.Name).Returns("SomeAction"); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => context.GenerateActionLink(action.Object), + "action", + "The action 'SomeAction' is not bound to the collection of entity. Only actions that are bound to entities can have action links."); + } - [Fact] - public void GenerateActionLink_GeneratesLinkWithCast_IfEntitySetTypeDoesnotMatchActionEntityType() - { - // Arrange - var request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, _customers, request); - var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); - IEdmAction specialUpgrade = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "specialUpgrade"); - Assert.NotNull(specialUpgrade); - - // Act - Uri link = entityContext.GenerateActionLink(specialUpgrade); - - // Assert - Assert.Equal("http://localhost/Customers(42)/NS.SpecialCustomer/NS.specialUpgrade", link.AbsoluteUri); - } - - [Fact] - public void GenerateActionLink_GeneratesLinkWithDownCast_IfElementTypeDerivesFromBindingParameterType() - { - // Arrange - IEdmEntitySet specialCustomers = new EdmEntitySet(_model.EntityContainer, "SpecialCustomers", _specialCustomer); - var request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, specialCustomers, request); - var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); - IEdmAction upgradeCustomer = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "upgrade"); - Assert.NotNull(upgradeCustomer); - - // Act - Uri link = entityContext.GenerateActionLink(upgradeCustomer); - - // Assert - Assert.Equal("http://localhost/SpecialCustomers(42)/NS.Customer/NS.upgrade", link.AbsoluteUri); - } - - [Fact] - public void GenerateActionLink_GeneratesLinkWithCast_IfEntitySetTypeDoesnotMatchActionEntityType_ForSingleton() - { - // Arrange - IEdmSingleton mary = new EdmSingleton(_model.EntityContainer, "Mary", _customer); - var request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, mary, request); - var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); - IEdmAction specialUpgrade = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "specialUpgrade"); - Assert.NotNull(specialUpgrade); - - // Act - Uri link = entityContext.GenerateActionLink(specialUpgrade); - - // Assert - Assert.Equal("http://localhost/Mary/NS.SpecialCustomer/NS.specialUpgrade", link.AbsoluteUri); - } - - [Fact] - public void GenerateActionLink_GeneratesLinkWithDownCast_IfElementTypeDerivesFromBindingParameterType_ForSingleton() - { - // Arrange - IEdmSingleton me = new EdmSingleton(_model.EntityContainer, "Me", _specialCustomer); - var request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, me, request); - var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); - IEdmAction upgradeCustomer = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "upgrade"); - Assert.NotNull(upgradeCustomer); - - // Act - Uri link = entityContext.GenerateActionLink(upgradeCustomer); - - // Assert - Assert.Equal("http://localhost/Me/NS.Customer/NS.upgrade", link.AbsoluteUri); - } - - //[Fact] - //public void GenerateActionLink_ReturnsNull_ForContainment() - //{ - // // Arrange - // var request = RequestFactory.Create(_model); - // var serializerContext = ODataSerializerContextFactory.Create(_model, _model.OrderLines, request); - // var entityContext = new ResourceContext(serializerContext, _model.OrderLine.AsReference(), new { ID = 42 }); - - // // Act - // Uri link = entityContext.GenerateActionLink(_model.Tag); - - // // Assert - // Assert.Null(link); - //} - - [Fact] - public void GenerateFunctionLinkForFeed_ThrowsArgumentNull_ResourceSetContext() - { - // Arrange - ResourceSetContext resourceSetContext = null; - IEdmFunction function = new Mock().Object; + [Fact] + public void GenerateActionLinkForFeed_GeneratesLinkWithoutCast_IfEntitySetTypeMatchesActionEntityType() + { + // Arrange + var request = RequestFactory.Create(_model); + IEdmAction action = _model.SchemaElements.OfType().First(a => a.Name == "UpgradeAll"); + Assert.NotNull(action); // Guard - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => resourceSetContext.GenerateFunctionLink(function), "resourceSetContext"); - } + ResourceSetContext context = new ResourceSetContext { EntitySetBase = _customers, Request = request }; - [Fact] - public void GenerateFunctionLinkForFeed_ThrowsArgumentNull_Function() - { - // Arrange - ResourceSetContext feedContext = new ResourceSetContext(); + // Act + Uri link = context.GenerateActionLink(action); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => feedContext.GenerateFunctionLink(function: null), "function"); - } + Assert.Equal("http://localhost/Customers/NS.UpgradeAll", link.AbsoluteUri); + } - [Fact] - public void GenerateFunctionLinkForFeed_ThrowsFunctionNotBoundToCollectionOfEntity_IfFunctionHasNoParameters() - { - // Arrange - ResourceSetContext context = new ResourceSetContext(); - Mock function = new Mock(); - function.Setup(a => a.Parameters).Returns(Enumerable.Empty()); - function.Setup(a => a.Name).Returns("SomeFunction"); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => context.GenerateFunctionLink(function.Object), - "function", - "The function 'SomeFunction' is not bound to the collection of entity. Only functions that are bound to entities can have function links."); - } - - [Fact] - public void GenerateFunctionLinkForFeed_GeneratesLinkWithoutCast_IfEntitySetTypeMatchesActionEntityType() - { - // Arrange - var request = RequestFactory.Create(_model); - IEdmFunction function = _model.SchemaElements.OfType().First(a => a.Name == "IsAllUpgraded"); - Assert.NotNull(function); // Guard - ResourceSetContext context = new ResourceSetContext { EntitySetBase = _customers, Request = request }; + [Fact] + public void GenerateActionLinkForFeed_GeneratesLinkWithCast_IfEntitySetTypeDoesnotMatchActionEntityType() + { + // Arrange + var request = RequestFactory.Create(_model); + IEdmAction action = _model.SchemaElements.OfType().First(a => a.Name == "UpgradeSpecialAll"); + Assert.NotNull(action); // Guard + ResourceSetContext context = new ResourceSetContext { EntitySetBase = _customers, Request = request }; - // Act - Uri link = context.GenerateFunctionLink(function); + // Act + Uri link = context.GenerateActionLink(action); - Assert.Equal("http://localhost/Customers/NS.IsAllUpgraded(param=@param)", link.AbsoluteUri); - } + // Assert + Assert.Equal("http://localhost/Customers/NS.SpecialCustomer/NS.UpgradeSpecialAll", link.AbsoluteUri); + } - [Fact] - public void GenerateFunctionLinkForFeed_GeneratesLinkWithCast_IfEntitySetTypeDoesnotMatchActionEntityType() - { - // Arrange - var request = RequestFactory.Create(_model); - IEdmFunction function = _model.SchemaElements.OfType().First(a => a.Name == "IsSpecialAllUpgraded"); - Assert.NotNull(function); // Guard + [Fact] + public void GenerateActionLinkForFeed_GeneratesLinkWithDownCast_IfElementTypeDerivesFromBindingParameterType() + { + // Arrange + var request = RequestFactory.Create(_model); + IEdmAction action = _model.SchemaElements.OfType().First(a => a.Name == "UpgradeAll"); + Assert.NotNull(action); // Guard + IEdmEntitySet specialCustomers = new EdmEntitySet(_model.EntityContainer, "SpecialCustomers", _specialCustomer); - ResourceSetContext context = new ResourceSetContext { EntitySetBase = _customers, Request = request }; - // Act - Uri link = context.GenerateFunctionLink(function); + ResourceSetContext context = new ResourceSetContext { EntitySetBase = specialCustomers, Request = request }; - // Assert - Assert.Equal("http://localhost/Customers/NS.SpecialCustomer/NS.IsSpecialAllUpgraded(param=@param)", link.AbsoluteUri); - } + // Act + Uri link = context.GenerateActionLink(action); - [Fact] - public void GenerateFunctionLinkForFeed_GeneratesLinkWithDownCast_IfElementTypeDerivesFromBindingParameterType() - { - // Arrange - var request = RequestFactory.Create(_model); - IEdmFunction function = _model.SchemaElements.OfType().First(a => a.Name == "IsAllUpgraded"); - Assert.NotNull(function); // Guard - IEdmEntitySet specialCustomers = new EdmEntitySet(_model.EntityContainer, "SpecialCustomers", _specialCustomer); + // Assert + Assert.Equal("http://localhost/SpecialCustomers/NS.Customer/NS.UpgradeAll", link.AbsoluteUri); + } - ResourceSetContext context = new ResourceSetContext { EntitySetBase = specialCustomers, Request = request }; + [Fact] + public void GenerateActionLink_ThrowsArgumentNull_EntityContext() + { + ResourceContext entityContext = null; + IEdmActionImport action = new Mock().Object; - // Act - Uri link = context.GenerateFunctionLink(function); + ExceptionAssert.ThrowsArgumentNull(() => entityContext.GenerateActionLink(action.Action), "resourceContext"); + } - // Assert - Assert.Equal("http://localhost/SpecialCustomers/NS.Customer/NS.IsAllUpgraded(param=@param)", link.AbsoluteUri); - } + [Fact] + public void GenerateActionLink_ThrowsArgumentNull_Action() + { + ResourceContext entityContext = new ResourceContext(); - [Fact] - public void GenerateFunctionLink_GeneratesLinkWithoutCast_IfEntitySetTypeMatchesActionEntityType() - { - // Arrange - var request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, _customers, request); - var entityContext = new ResourceContext(serializerContext, _customer.AsReference(), new { ID = 42 }); - IEdmFunction isCustomerUpgraded = _model.SchemaElements.OfType().First(c => c.Name == "IsUpgradedWithParam"); - Assert.NotNull(isCustomerUpgraded); - - // Act - Uri link = entityContext.GenerateFunctionLink(isCustomerUpgraded); - - // Assert - Assert.Equal("http://localhost/Customers(42)/NS.IsUpgradedWithParam(city=@city)", link.AbsoluteUri); - } - - [Fact] - public void GenerateFunctionLink_GeneratesLinkWithCast_IfEntitySetTypeDoesnotMatchActionEntityType() - { - // Arrange - var request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, _customers, request); - var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); - IEdmFunction isSpecialUpgraded = _model.SchemaElements.OfType().First(c => c.Name == "IsSpecialUpgraded"); - Assert.NotNull(isSpecialUpgraded); - - // Act - Uri link = entityContext.GenerateFunctionLink(isSpecialUpgraded); - - // Assert - Assert.Equal("http://localhost/Customers(42)/NS.SpecialCustomer/NS.IsSpecialUpgraded()", link.AbsoluteUri); - } - - [Fact] - public void GenerateFunctionLink_GeneratesLinkWithDownCast_IfElementTypeDerivesFromBindingParameterType() - { - // Arrange - IEdmEntitySet specialCustomers = new EdmEntitySet(_model.EntityContainer, "SpecialCustomers", _specialCustomer); - var request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, specialCustomers, request); - var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); - IEdmFunction isCustomerUpgraded = _model.SchemaElements.OfType().First(c => c.Name == "IsUpgradedWithParam"); - Assert.NotNull(isCustomerUpgraded); - - // Act - Uri link = entityContext.GenerateFunctionLink(isCustomerUpgraded); - - // Assert - Assert.Equal("http://localhost/SpecialCustomers(42)/NS.Customer/NS.IsUpgradedWithParam(city=@city)", - link.AbsoluteUri); - } - - [Fact] - public void GenerateFunctionLink_GeneratesLinkWithCast_IfEntitySetTypeDoesnotMatchActionEntityType_ForSingleton() - { - // Arrange - IEdmSingleton mary = new EdmSingleton(_model.EntityContainer, "Mary", _customer); - var request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, mary, request); - var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); - IEdmFunction isSpecialUpgraded = _model.SchemaElements.OfType().First(c => c.Name == "IsSpecialUpgraded"); - Assert.NotNull(isSpecialUpgraded); - - // Act - Uri link = entityContext.GenerateFunctionLink(isSpecialUpgraded); - - // Assert - Assert.Equal("http://localhost/Mary/NS.SpecialCustomer/NS.IsSpecialUpgraded()", link.AbsoluteUri); - } - - [Fact] - public void GenerateFunctionLink_GeneratesLinkWithDownCast_IfElementTypeDerivesFromBindingParameterType_ForSingleton() - { - // Arrange - IEdmSingleton me = new EdmSingleton(_model.EntityContainer, "Me", _specialCustomer); - HttpRequest request = RequestFactory.Create(_model); - var serializerContext = ODataSerializerContextFactory.Create(_model, me, request); - var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); - IEdmFunction isCustomerUpgraded = _model.SchemaElements.OfType().First(c => c.Name == "IsUpgradedWithParam"); - Assert.NotNull(isCustomerUpgraded); + ExceptionAssert.ThrowsArgumentNull(() => entityContext.GenerateActionLink(action: null), "action"); + } + + [Fact] + public void GenerateActionLink_ThrowsActionNotBoundToEntity_IfActionHasNoParameters() + { + ResourceContext entityContext = new ResourceContext(); + Mock action = new Mock(); + action.Setup(a => a.Parameters).Returns(Enumerable.Empty()); + action.Setup(a => a.Name).Returns("SomeAction"); + + ExceptionAssert.ThrowsArgument( + () => entityContext.GenerateActionLink(action.Object), + "action", + "The action 'SomeAction' is not bound to an entity. Only actions that are bound to entities can have action links."); + } + + [Fact] + public void GenerateActionLink_GeneratesLinkWithoutCast_IfEntitySetTypeMatchesActionEntityType() + { + // Arrange + var request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, _customers, request); + var entityContext = new ResourceContext(serializerContext, _customer.AsReference(), new { ID = 42 }); + IEdmAction upgradeCustomer = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "upgrade"); + Assert.NotNull(upgradeCustomer); + + // Act + Uri link = entityContext.GenerateActionLink(upgradeCustomer); + + Assert.Equal("http://localhost/Customers(42)/NS.upgrade", link.AbsoluteUri); + } + + [Fact] + public void GenerateActionLink_GeneratesLinkWithCast_IfEntitySetTypeDoesnotMatchActionEntityType() + { + // Arrange + var request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, _customers, request); + var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); + IEdmAction specialUpgrade = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "specialUpgrade"); + Assert.NotNull(specialUpgrade); + + // Act + Uri link = entityContext.GenerateActionLink(specialUpgrade); + + // Assert + Assert.Equal("http://localhost/Customers(42)/NS.SpecialCustomer/NS.specialUpgrade", link.AbsoluteUri); + } + + [Fact] + public void GenerateActionLink_GeneratesLinkWithDownCast_IfElementTypeDerivesFromBindingParameterType() + { + // Arrange + IEdmEntitySet specialCustomers = new EdmEntitySet(_model.EntityContainer, "SpecialCustomers", _specialCustomer); + var request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, specialCustomers, request); + var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); + IEdmAction upgradeCustomer = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "upgrade"); + Assert.NotNull(upgradeCustomer); + + // Act + Uri link = entityContext.GenerateActionLink(upgradeCustomer); + + // Assert + Assert.Equal("http://localhost/SpecialCustomers(42)/NS.Customer/NS.upgrade", link.AbsoluteUri); + } + + [Fact] + public void GenerateActionLink_GeneratesLinkWithCast_IfEntitySetTypeDoesnotMatchActionEntityType_ForSingleton() + { + // Arrange + IEdmSingleton mary = new EdmSingleton(_model.EntityContainer, "Mary", _customer); + var request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, mary, request); + var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); + IEdmAction specialUpgrade = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "specialUpgrade"); + Assert.NotNull(specialUpgrade); + + // Act + Uri link = entityContext.GenerateActionLink(specialUpgrade); + + // Assert + Assert.Equal("http://localhost/Mary/NS.SpecialCustomer/NS.specialUpgrade", link.AbsoluteUri); + } + + [Fact] + public void GenerateActionLink_GeneratesLinkWithDownCast_IfElementTypeDerivesFromBindingParameterType_ForSingleton() + { + // Arrange + IEdmSingleton me = new EdmSingleton(_model.EntityContainer, "Me", _specialCustomer); + var request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, me, request); + var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); + IEdmAction upgradeCustomer = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "upgrade"); + Assert.NotNull(upgradeCustomer); + + // Act + Uri link = entityContext.GenerateActionLink(upgradeCustomer); + + // Assert + Assert.Equal("http://localhost/Me/NS.Customer/NS.upgrade", link.AbsoluteUri); + } + + //[Fact] + //public void GenerateActionLink_ReturnsNull_ForContainment() + //{ + // // Arrange + // var request = RequestFactory.Create(_model); + // var serializerContext = ODataSerializerContextFactory.Create(_model, _model.OrderLines, request); + // var entityContext = new ResourceContext(serializerContext, _model.OrderLine.AsReference(), new { ID = 42 }); + + // // Act + // Uri link = entityContext.GenerateActionLink(_model.Tag); + + // // Assert + // Assert.Null(link); + //} + + [Fact] + public void GenerateFunctionLinkForFeed_ThrowsArgumentNull_ResourceSetContext() + { + // Arrange + ResourceSetContext resourceSetContext = null; + IEdmFunction function = new Mock().Object; + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => resourceSetContext.GenerateFunctionLink(function), "resourceSetContext"); + } + + [Fact] + public void GenerateFunctionLinkForFeed_ThrowsArgumentNull_Function() + { + // Arrange + ResourceSetContext feedContext = new ResourceSetContext(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => feedContext.GenerateFunctionLink(function: null), "function"); + } + + [Fact] + public void GenerateFunctionLinkForFeed_ThrowsFunctionNotBoundToCollectionOfEntity_IfFunctionHasNoParameters() + { + // Arrange + ResourceSetContext context = new ResourceSetContext(); + Mock function = new Mock(); + function.Setup(a => a.Parameters).Returns(Enumerable.Empty()); + function.Setup(a => a.Name).Returns("SomeFunction"); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => context.GenerateFunctionLink(function.Object), + "function", + "The function 'SomeFunction' is not bound to the collection of entity. Only functions that are bound to entities can have function links."); + } - // Act - Uri link = entityContext.GenerateFunctionLink(isCustomerUpgraded); + [Fact] + public void GenerateFunctionLinkForFeed_GeneratesLinkWithoutCast_IfEntitySetTypeMatchesActionEntityType() + { + // Arrange + var request = RequestFactory.Create(_model); + IEdmFunction function = _model.SchemaElements.OfType().First(a => a.Name == "IsAllUpgraded"); + Assert.NotNull(function); // Guard + ResourceSetContext context = new ResourceSetContext { EntitySetBase = _customers, Request = request }; - // Assert - Assert.Equal("http://localhost/Me/NS.Customer/NS.IsUpgradedWithParam(city=@city)", link.AbsoluteUri); - } + // Act + Uri link = context.GenerateFunctionLink(function); + + Assert.Equal("http://localhost/Customers/NS.IsAllUpgraded(param=@param)", link.AbsoluteUri); + } + + [Fact] + public void GenerateFunctionLinkForFeed_GeneratesLinkWithCast_IfEntitySetTypeDoesnotMatchActionEntityType() + { + // Arrange + var request = RequestFactory.Create(_model); + IEdmFunction function = _model.SchemaElements.OfType().First(a => a.Name == "IsSpecialAllUpgraded"); + Assert.NotNull(function); // Guard + + ResourceSetContext context = new ResourceSetContext { EntitySetBase = _customers, Request = request }; + // Act + Uri link = context.GenerateFunctionLink(function); + + // Assert + Assert.Equal("http://localhost/Customers/NS.SpecialCustomer/NS.IsSpecialAllUpgraded(param=@param)", link.AbsoluteUri); + } + + [Fact] + public void GenerateFunctionLinkForFeed_GeneratesLinkWithDownCast_IfElementTypeDerivesFromBindingParameterType() + { + // Arrange + var request = RequestFactory.Create(_model); + IEdmFunction function = _model.SchemaElements.OfType().First(a => a.Name == "IsAllUpgraded"); + Assert.NotNull(function); // Guard + IEdmEntitySet specialCustomers = new EdmEntitySet(_model.EntityContainer, "SpecialCustomers", _specialCustomer); + + ResourceSetContext context = new ResourceSetContext { EntitySetBase = specialCustomers, Request = request }; + + // Act + Uri link = context.GenerateFunctionLink(function); + + // Assert + Assert.Equal("http://localhost/SpecialCustomers/NS.Customer/NS.IsAllUpgraded(param=@param)", link.AbsoluteUri); + } + + [Fact] + public void GenerateFunctionLink_GeneratesLinkWithoutCast_IfEntitySetTypeMatchesActionEntityType() + { + // Arrange + var request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, _customers, request); + var entityContext = new ResourceContext(serializerContext, _customer.AsReference(), new { ID = 42 }); + IEdmFunction isCustomerUpgraded = _model.SchemaElements.OfType().First(c => c.Name == "IsUpgradedWithParam"); + Assert.NotNull(isCustomerUpgraded); + + // Act + Uri link = entityContext.GenerateFunctionLink(isCustomerUpgraded); + + // Assert + Assert.Equal("http://localhost/Customers(42)/NS.IsUpgradedWithParam(city=@city)", link.AbsoluteUri); + } + + [Fact] + public void GenerateFunctionLink_GeneratesLinkWithCast_IfEntitySetTypeDoesnotMatchActionEntityType() + { + // Arrange + var request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, _customers, request); + var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); + IEdmFunction isSpecialUpgraded = _model.SchemaElements.OfType().First(c => c.Name == "IsSpecialUpgraded"); + Assert.NotNull(isSpecialUpgraded); + + // Act + Uri link = entityContext.GenerateFunctionLink(isSpecialUpgraded); + + // Assert + Assert.Equal("http://localhost/Customers(42)/NS.SpecialCustomer/NS.IsSpecialUpgraded()", link.AbsoluteUri); + } + + [Fact] + public void GenerateFunctionLink_GeneratesLinkWithDownCast_IfElementTypeDerivesFromBindingParameterType() + { + // Arrange + IEdmEntitySet specialCustomers = new EdmEntitySet(_model.EntityContainer, "SpecialCustomers", _specialCustomer); + var request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, specialCustomers, request); + var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); + IEdmFunction isCustomerUpgraded = _model.SchemaElements.OfType().First(c => c.Name == "IsUpgradedWithParam"); + Assert.NotNull(isCustomerUpgraded); + + // Act + Uri link = entityContext.GenerateFunctionLink(isCustomerUpgraded); + + // Assert + Assert.Equal("http://localhost/SpecialCustomers(42)/NS.Customer/NS.IsUpgradedWithParam(city=@city)", + link.AbsoluteUri); + } + + [Fact] + public void GenerateFunctionLink_GeneratesLinkWithCast_IfEntitySetTypeDoesnotMatchActionEntityType_ForSingleton() + { + // Arrange + IEdmSingleton mary = new EdmSingleton(_model.EntityContainer, "Mary", _customer); + var request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, mary, request); + var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); + IEdmFunction isSpecialUpgraded = _model.SchemaElements.OfType().First(c => c.Name == "IsSpecialUpgraded"); + Assert.NotNull(isSpecialUpgraded); + + // Act + Uri link = entityContext.GenerateFunctionLink(isSpecialUpgraded); + + // Assert + Assert.Equal("http://localhost/Mary/NS.SpecialCustomer/NS.IsSpecialUpgraded()", link.AbsoluteUri); + } + + [Fact] + public void GenerateFunctionLink_GeneratesLinkWithDownCast_IfElementTypeDerivesFromBindingParameterType_ForSingleton() + { + // Arrange + IEdmSingleton me = new EdmSingleton(_model.EntityContainer, "Me", _specialCustomer); + HttpRequest request = RequestFactory.Create(_model); + var serializerContext = ODataSerializerContextFactory.Create(_model, me, request); + var entityContext = new ResourceContext(serializerContext, _specialCustomer.AsReference(), new { ID = 42 }); + IEdmFunction isCustomerUpgraded = _model.SchemaElements.OfType().First(c => c.Name == "IsUpgradedWithParam"); + Assert.NotNull(isCustomerUpgraded); + + // Act + Uri link = entityContext.GenerateFunctionLink(isCustomerUpgraded); + + // Assert + Assert.Equal("http://localhost/Me/NS.Customer/NS.IsUpgradedWithParam(city=@city)", link.AbsoluteUri); + } - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.Namespace = "NS"; - builder.EntitySet("Customers"); - builder.EntitySet("Orders"); - var specialCustomer = builder.EntityType(); - var customer = builder.EntityType(); + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.Namespace = "NS"; + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); + var specialCustomer = builder.EntityType(); + var customer = builder.EntityType(); - FunctionConfiguration function = customer.Function("IsUpgradedWithParam").Returns(); - function.Parameter("city"); + FunctionConfiguration function = customer.Function("IsUpgradedWithParam").Returns(); + function.Parameter("city"); - specialCustomer.Function("IsSpecialUpgraded").Returns(); + specialCustomer.Function("IsSpecialUpgraded").Returns(); - // bound to collection - function = customer.Collection.Function("IsAllUpgraded").Returns(); - function.Parameter("param"); + // bound to collection + function = customer.Collection.Function("IsAllUpgraded").Returns(); + function.Parameter("param"); - function = specialCustomer.Collection.Function("IsSpecialAllUpgraded").Returns(); - function.Parameter("param"); + function = specialCustomer.Collection.Function("IsSpecialAllUpgraded").Returns(); + function.Parameter("param"); - // actions - customer.Action("upgrade"); - specialCustomer.Action("specialUpgrade"); + // actions + customer.Action("upgrade"); + specialCustomer.Action("specialUpgrade"); - // actions bound to collection - customer.Collection.Action("UpgradeAll"); - specialCustomer.Collection.Action("UpgradeSpecialAll"); + // actions bound to collection + customer.Collection.Action("UpgradeAll"); + specialCustomer.Collection.Action("UpgradeSpecialAll"); - return builder.GetEdmModel(); - } + return builder.GetEdmModel(); + } - private class Customer - { - public int ID { get; set; } + private class Customer + { + public int ID { get; set; } - public IList Orders { get; set; } - } + public IList Orders { get; set; } + } - private class SpecialCustomer : Customer - { } + private class SpecialCustomer : Customer + { } - private class Order - { - public int ID { get; set; } - } + private class Order + { + public int ID { get; set; } + } - private static IEdmModel GetEdmModel2() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.Namespace = "NS"; - builder.EntitySet("MyOrders"); - builder.Singleton("VipOrder"); - return builder.GetEdmModel(); - } - - private class MyOrder - { - public int ID { get; set; } + private static IEdmModel GetEdmModel2() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.Namespace = "NS"; + builder.EntitySet("MyOrders"); + builder.Singleton("VipOrder"); + return builder.GetEdmModel(); + } - [Contained] - public IList OrderLines { get; set; } + private class MyOrder + { + public int ID { get; set; } - public IList NonContainedOrderLines { get; set; } - } + [Contained] + public IList OrderLines { get; set; } - private class OrderLine - { - public int ID { get; set; } + public IList NonContainedOrderLines { get; set; } + } - [Contained] - public IList OrderLineDetails { get; set; } - } + private class OrderLine + { + public int ID { get; set; } - private class OrderLineDetail - { - public int ID { get; set; } - } + [Contained] + public IList OrderLineDetails { get; set; } + } + + private class OrderLineDetail + { + public int ID { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataBinaryValueMediaTypeMappingTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataBinaryValueMediaTypeMappingTests.cs index 3d910bc5b..0a30661a2 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataBinaryValueMediaTypeMappingTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataBinaryValueMediaTypeMappingTests.cs @@ -13,56 +13,55 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Tests.Formatter.MediaType; + +public class ODataBinaryValueMediaTypeMappingTests { - public class ODataBinaryValueMediaTypeMappingTests - { - private static ODataBinaryValueMediaTypeMapping Mapping = new ODataBinaryValueMediaTypeMapping(); + private static ODataBinaryValueMediaTypeMapping Mapping = new ODataBinaryValueMediaTypeMapping(); - [Fact] - public void Ctor_SetMediaType() - { - // Arrange & Act & Assert - Assert.Equal("application/octet-stream", Mapping.MediaType.MediaType); - } + [Fact] + public void Ctor_SetMediaType() + { + // Arrange & Act & Assert + Assert.Equal("application/octet-stream", Mapping.MediaType.MediaType); + } - [Fact] - public void TryMatchMediaType_ThrowsArgumentNull_WhenRequestIsNull() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => Mapping.TryMatchMediaType(null), "request"); - } + [Fact] + public void TryMatchMediaType_ThrowsArgumentNull_WhenRequestIsNull() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => Mapping.TryMatchMediaType(null), "request"); + } - [Fact] - public void TryMatchMediaType_DoesnotMatchRequest_WithNonODataRequest() - { - // Arrange - HttpRequest request = new DefaultHttpContext().Request; + [Fact] + public void TryMatchMediaType_DoesnotMatchRequest_WithNonODataRequest() + { + // Arrange + HttpRequest request = new DefaultHttpContext().Request; - // Act - double mapResult = Mapping.TryMatchMediaType(request); + // Act + double mapResult = Mapping.TryMatchMediaType(request); - // Assert - Assert.Equal(0, mapResult); - } + // Assert + Assert.Equal(0, mapResult); + } - [Fact] - public void TryMatchMediaType_MatchRequest_WithBinaryPrimitivePropertyRequest() - { - // Arrange - EdmEntityType entity = new EdmEntityType("NS", "Entity"); - EdmStructuralProperty property = entity.AddStructuralProperty("Data", EdmCoreModel.Instance.GetBinary(true)); - PropertySegment propertySegment = new PropertySegment(property); - ValueSegment valueSegment = new ValueSegment(property.Type.Definition); + [Fact] + public void TryMatchMediaType_MatchRequest_WithBinaryPrimitivePropertyRequest() + { + // Arrange + EdmEntityType entity = new EdmEntityType("NS", "Entity"); + EdmStructuralProperty property = entity.AddStructuralProperty("Data", EdmCoreModel.Instance.GetBinary(true)); + PropertySegment propertySegment = new PropertySegment(property); + ValueSegment valueSegment = new ValueSegment(property.Type.Definition); - HttpRequest request = new DefaultHttpContext().Request; - request.ODataFeature().Path = new ODataPath(propertySegment, valueSegment); + HttpRequest request = new DefaultHttpContext().Request; + request.ODataFeature().Path = new ODataPath(propertySegment, valueSegment); - // Act - double mapResult = Mapping.TryMatchMediaType(request); + // Act + double mapResult = Mapping.TryMatchMediaType(request); - // Assert - Assert.Equal(1.0, mapResult); - } + // Assert + Assert.Equal(1.0, mapResult); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataCountMediaTypeMappingTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataCountMediaTypeMappingTests.cs index c8e69ac7f..1877c0b9b 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataCountMediaTypeMappingTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataCountMediaTypeMappingTests.cs @@ -12,51 +12,50 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Tests.Formatter.MediaType; + +public class ODataCountMediaTypeMappingTests { - public class ODataCountMediaTypeMappingTests + private static ODataCountMediaTypeMapping Mapping = new ODataCountMediaTypeMapping(); + + [Fact] + public void Ctor_SetMediaType() + { + // Arrange & Act & Assert + Assert.Equal("text/plain", Mapping.MediaType.MediaType); + } + + [Fact] + public void TryMatchMediaType_ThrowsArgumentNull_WhenRequestIsNull() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => Mapping.TryMatchMediaType(null), "request"); + } + + [Fact] + public void TryMatchMediaType_DoesnotMatchRequest_WithNonODataRequest() { - private static ODataCountMediaTypeMapping Mapping = new ODataCountMediaTypeMapping(); - - [Fact] - public void Ctor_SetMediaType() - { - // Arrange & Act & Assert - Assert.Equal("text/plain", Mapping.MediaType.MediaType); - } - - [Fact] - public void TryMatchMediaType_ThrowsArgumentNull_WhenRequestIsNull() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => Mapping.TryMatchMediaType(null), "request"); - } - - [Fact] - public void TryMatchMediaType_DoesnotMatchRequest_WithNonODataRequest() - { - // Arrange - HttpRequest request = new DefaultHttpContext().Request; - - // Act - double mapResult = Mapping.TryMatchMediaType(request); - - // Assert - Assert.Equal(0, mapResult); - } - - [Fact] - public void TryMatchMediaType_MatchRequest_WithDollarCountRequest() - { - // Arrange - HttpRequest request = new DefaultHttpContext().Request; - request.ODataFeature().Path = new ODataPath(CountSegment.Instance); - - // Act - double mapResult = Mapping.TryMatchMediaType(request); - - // Assert - Assert.Equal(1.0, mapResult); - } + // Arrange + HttpRequest request = new DefaultHttpContext().Request; + + // Act + double mapResult = Mapping.TryMatchMediaType(request); + + // Assert + Assert.Equal(0, mapResult); + } + + [Fact] + public void TryMatchMediaType_MatchRequest_WithDollarCountRequest() + { + // Arrange + HttpRequest request = new DefaultHttpContext().Request; + request.ODataFeature().Path = new ODataPath(CountSegment.Instance); + + // Act + double mapResult = Mapping.TryMatchMediaType(request); + + // Assert + Assert.Equal(1.0, mapResult); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataEnumValueMediaTypeMappingTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataEnumValueMediaTypeMappingTests.cs index aaf975ba2..a6dafcc37 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataEnumValueMediaTypeMappingTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataEnumValueMediaTypeMappingTests.cs @@ -13,78 +13,77 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Tests.Formatter.MediaType; + +public class ODataEnumValueMediaTypeMappingTests { - public class ODataEnumValueMediaTypeMappingTests + private static ODataEnumValueMediaTypeMapping Mapping = new ODataEnumValueMediaTypeMapping(); + + [Fact] + public void Ctor_SetMediaType() + { + // Arrange & Act & Assert + Assert.Equal("text/plain", Mapping.MediaType.MediaType); + } + + [Fact] + public void TryMatchMediaType_ThrowsArgumentNull_WhenRequestIsNull() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => Mapping.TryMatchMediaType(null), "request"); + } + + [Fact] + public void TryMatchMediaType_DoesnotMatchRequest_WithNonODataRequest() { - private static ODataEnumValueMediaTypeMapping Mapping = new ODataEnumValueMediaTypeMapping(); - - [Fact] - public void Ctor_SetMediaType() - { - // Arrange & Act & Assert - Assert.Equal("text/plain", Mapping.MediaType.MediaType); - } - - [Fact] - public void TryMatchMediaType_ThrowsArgumentNull_WhenRequestIsNull() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => Mapping.TryMatchMediaType(null), "request"); - } - - [Fact] - public void TryMatchMediaType_DoesnotMatchRequest_WithNonODataRequest() - { - // Arrange - HttpRequest request = new DefaultHttpContext().Request; - - // Act - double mapResult = Mapping.TryMatchMediaType(request); - - // Assert - Assert.Equal(0, mapResult); - } - - [Fact] - public void TryMatchMediaType_DoesnotMatchRequest_WithPrimitiveRawValueRequest() - { - // Arrange - EdmEntityType entity = new EdmEntityType("NS", "Entity"); - EdmStructuralProperty property = entity.AddStructuralProperty("Age", EdmCoreModel.Instance.GetInt32(true)); - PropertySegment propertySegment = new PropertySegment(property); - ValueSegment valueSegment = new ValueSegment(property.Type.Definition); - - HttpRequest request = new DefaultHttpContext().Request; - request.ODataFeature().Path = new ODataPath(propertySegment, valueSegment); - - // Act - double mapResult = Mapping.TryMatchMediaType(request); - - // Assert - Assert.Equal(0.0, mapResult); - } - - [Fact] - public void TryMatchMediaType_MatchRequest_WithPrimitiveRawValueRequest() - { - // Arrange - EdmEnumType colorType = new EdmEnumType("NS", "Color"); - EdmEntityType entity = new EdmEntityType("NS", "Entity"); - EdmStructuralProperty property = - entity.AddStructuralProperty("Color", new EdmEnumTypeReference(colorType, false)); - - PropertySegment propertySegment = new PropertySegment(property); - ValueSegment valueSegment = new ValueSegment(property.Type.Definition); - - HttpRequest request = new DefaultHttpContext().Request; - request.ODataFeature().Path = new ODataPath(propertySegment, valueSegment); - - // Act - double mapResult = Mapping.TryMatchMediaType(request); - - // Assert - Assert.Equal(1.0, mapResult); - } + // Arrange + HttpRequest request = new DefaultHttpContext().Request; + + // Act + double mapResult = Mapping.TryMatchMediaType(request); + + // Assert + Assert.Equal(0, mapResult); + } + + [Fact] + public void TryMatchMediaType_DoesnotMatchRequest_WithPrimitiveRawValueRequest() + { + // Arrange + EdmEntityType entity = new EdmEntityType("NS", "Entity"); + EdmStructuralProperty property = entity.AddStructuralProperty("Age", EdmCoreModel.Instance.GetInt32(true)); + PropertySegment propertySegment = new PropertySegment(property); + ValueSegment valueSegment = new ValueSegment(property.Type.Definition); + + HttpRequest request = new DefaultHttpContext().Request; + request.ODataFeature().Path = new ODataPath(propertySegment, valueSegment); + + // Act + double mapResult = Mapping.TryMatchMediaType(request); + + // Assert + Assert.Equal(0.0, mapResult); + } + + [Fact] + public void TryMatchMediaType_MatchRequest_WithPrimitiveRawValueRequest() + { + // Arrange + EdmEnumType colorType = new EdmEnumType("NS", "Color"); + EdmEntityType entity = new EdmEntityType("NS", "Entity"); + EdmStructuralProperty property = + entity.AddStructuralProperty("Color", new EdmEnumTypeReference(colorType, false)); + + PropertySegment propertySegment = new PropertySegment(property); + ValueSegment valueSegment = new ValueSegment(property.Type.Definition); + + HttpRequest request = new DefaultHttpContext().Request; + request.ODataFeature().Path = new ODataPath(propertySegment, valueSegment); + + // Act + double mapResult = Mapping.TryMatchMediaType(request); + + // Assert + Assert.Equal(1.0, mapResult); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataPrimitiveValueMediaTypeMappingTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataPrimitiveValueMediaTypeMappingTests.cs index eab964def..4b5317a75 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataPrimitiveValueMediaTypeMappingTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataPrimitiveValueMediaTypeMappingTests.cs @@ -13,79 +13,78 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Tests.Formatter.MediaType; + +public class ODataPrimitiveValueMediaTypeMappingTests { - public class ODataPrimitiveValueMediaTypeMappingTests + private static ODataPrimitiveValueMediaTypeMapping Mapping = new ODataPrimitiveValueMediaTypeMapping(); + + [Fact] + public void Ctor_SetMediaType() + { + // Arrange & Act & Assert + Assert.Equal("text/plain", Mapping.MediaType.MediaType); + } + + [Fact] + public void TryMatchMediaType_ThrowsArgumentNull_WhenRequestIsNull() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => Mapping.TryMatchMediaType(null), "request"); + } + + [Fact] + public void TryMatchMediaType_DoesnotMatchRequest_WithNonODataRequest() + { + // Arrange + HttpRequest request = new DefaultHttpContext().Request; + + // Act + double mapResult = Mapping.TryMatchMediaType(request); + + // Assert + Assert.Equal(0, mapResult); + } + + [Fact] + public void TryMatchMediaType_DoesnotMatchRequest_WithBinaryPropertyRequest() { - private static ODataPrimitiveValueMediaTypeMapping Mapping = new ODataPrimitiveValueMediaTypeMapping(); - - [Fact] - public void Ctor_SetMediaType() - { - // Arrange & Act & Assert - Assert.Equal("text/plain", Mapping.MediaType.MediaType); - } - - [Fact] - public void TryMatchMediaType_ThrowsArgumentNull_WhenRequestIsNull() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => Mapping.TryMatchMediaType(null), "request"); - } - - [Fact] - public void TryMatchMediaType_DoesnotMatchRequest_WithNonODataRequest() - { - // Arrange - HttpRequest request = new DefaultHttpContext().Request; - - // Act - double mapResult = Mapping.TryMatchMediaType(request); - - // Assert - Assert.Equal(0, mapResult); - } - - [Fact] - public void TryMatchMediaType_DoesnotMatchRequest_WithBinaryPropertyRequest() - { - // Arrange - EdmEntityType entity = new EdmEntityType("NS", "Entity"); - EdmStructuralProperty property = entity.AddStructuralProperty("Photo", EdmCoreModel.Instance.GetStream(true)); - PropertySegment propertySegment = new PropertySegment(property); - - HttpRequest request = new DefaultHttpContext().Request; - request.ODataFeature().Path = new ODataPath(propertySegment); - - // Act - double mapResult = Mapping.TryMatchMediaType(request); - - // Assert - Assert.Equal(0, mapResult); - } - - [Theory] - [InlineData(EdmPrimitiveTypeKind.Int16)] - [InlineData(EdmPrimitiveTypeKind.Int32)] - [InlineData(EdmPrimitiveTypeKind.String)] - [InlineData(EdmPrimitiveTypeKind.Double)] - public void TryMatchMediaType_MatchRequest_WithPrimitiveRawValueRequest(EdmPrimitiveTypeKind kind) - { - // Arrange - EdmEntityType entity = new EdmEntityType("NS", "Entity"); - EdmStructuralProperty property = entity.AddStructuralProperty("Age", - EdmCoreModel.Instance.GetPrimitive(kind, true)); - PropertySegment propertySegment = new PropertySegment(property); - ValueSegment valueSegment = new ValueSegment(property.Type.Definition); - - HttpRequest request = new DefaultHttpContext().Request; - request.ODataFeature().Path = new ODataPath(propertySegment, valueSegment); - - // Act - double mapResult = Mapping.TryMatchMediaType(request); - - // Assert - Assert.Equal(1.0, mapResult); - } + // Arrange + EdmEntityType entity = new EdmEntityType("NS", "Entity"); + EdmStructuralProperty property = entity.AddStructuralProperty("Photo", EdmCoreModel.Instance.GetStream(true)); + PropertySegment propertySegment = new PropertySegment(property); + + HttpRequest request = new DefaultHttpContext().Request; + request.ODataFeature().Path = new ODataPath(propertySegment); + + // Act + double mapResult = Mapping.TryMatchMediaType(request); + + // Assert + Assert.Equal(0, mapResult); + } + + [Theory] + [InlineData(EdmPrimitiveTypeKind.Int16)] + [InlineData(EdmPrimitiveTypeKind.Int32)] + [InlineData(EdmPrimitiveTypeKind.String)] + [InlineData(EdmPrimitiveTypeKind.Double)] + public void TryMatchMediaType_MatchRequest_WithPrimitiveRawValueRequest(EdmPrimitiveTypeKind kind) + { + // Arrange + EdmEntityType entity = new EdmEntityType("NS", "Entity"); + EdmStructuralProperty property = entity.AddStructuralProperty("Age", + EdmCoreModel.Instance.GetPrimitive(kind, true)); + PropertySegment propertySegment = new PropertySegment(property); + ValueSegment valueSegment = new ValueSegment(property.Type.Definition); + + HttpRequest request = new DefaultHttpContext().Request; + request.ODataFeature().Path = new ODataPath(propertySegment, valueSegment); + + // Act + double mapResult = Mapping.TryMatchMediaType(request); + + // Assert + Assert.Equal(1.0, mapResult); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataStreamMediaTypeMappingTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataStreamMediaTypeMappingTests.cs index 537ddefc2..c75768fab 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataStreamMediaTypeMappingTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/ODataStreamMediaTypeMappingTests.cs @@ -13,55 +13,54 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Tests.Formatter.MediaType; + +public class ODataStreamMediaTypeMappingTests { - public class ODataStreamMediaTypeMappingTests - { - private static ODataStreamMediaTypeMapping Mapping = new ODataStreamMediaTypeMapping(); + private static ODataStreamMediaTypeMapping Mapping = new ODataStreamMediaTypeMapping(); - [Fact] - public void Ctor_SetMediaType() - { - // Arrange & Act & Assert - Assert.Equal("application/octet-stream", Mapping.MediaType.MediaType); - } + [Fact] + public void Ctor_SetMediaType() + { + // Arrange & Act & Assert + Assert.Equal("application/octet-stream", Mapping.MediaType.MediaType); + } - [Fact] - public void TryMatchMediaType_ThrowsArgumentNull_WhenRequestIsNull() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => Mapping.TryMatchMediaType(null), "request"); - } + [Fact] + public void TryMatchMediaType_ThrowsArgumentNull_WhenRequestIsNull() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => Mapping.TryMatchMediaType(null), "request"); + } - [Fact] - public void TryMatchMediaType_DoesnotMatchRequest_WithNonODataRequest() - { - // Arrange - HttpRequest request = new DefaultHttpContext().Request; + [Fact] + public void TryMatchMediaType_DoesnotMatchRequest_WithNonODataRequest() + { + // Arrange + HttpRequest request = new DefaultHttpContext().Request; - // Act - double mapResult = Mapping.TryMatchMediaType(request); + // Act + double mapResult = Mapping.TryMatchMediaType(request); - // Assert - Assert.Equal(0, mapResult); - } + // Assert + Assert.Equal(0, mapResult); + } - [Fact] - public void TryMatchMediaType_MatchRequest_WithStreamPropertyRequest() - { - // Arrange - EdmEntityType entity = new EdmEntityType("NS", "Entity"); - EdmStructuralProperty property = entity.AddStructuralProperty("Photo", EdmCoreModel.Instance.GetStream(true)); - PropertySegment propertySegment = new PropertySegment(property); + [Fact] + public void TryMatchMediaType_MatchRequest_WithStreamPropertyRequest() + { + // Arrange + EdmEntityType entity = new EdmEntityType("NS", "Entity"); + EdmStructuralProperty property = entity.AddStructuralProperty("Photo", EdmCoreModel.Instance.GetStream(true)); + PropertySegment propertySegment = new PropertySegment(property); - HttpRequest request = new DefaultHttpContext().Request; - request.ODataFeature().Path = new ODataPath(propertySegment); + HttpRequest request = new DefaultHttpContext().Request; + request.ODataFeature().Path = new ODataPath(propertySegment); - // Act - double mapResult = Mapping.TryMatchMediaType(request); + // Act + double mapResult = Mapping.TryMatchMediaType(request); - // Assert - Assert.Equal(1.0, mapResult); - } + // Assert + Assert.Equal(1.0, mapResult); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/QueryStringMediaTypeMappingTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/QueryStringMediaTypeMappingTests.cs index d3e9061dc..47011e07e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/QueryStringMediaTypeMappingTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/MediaType/QueryStringMediaTypeMappingTests.cs @@ -10,75 +10,74 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.MediaType +namespace Microsoft.AspNetCore.OData.Tests.Formatter.MediaType; + +public class QueryStringMediaTypeMappingTests { - public class QueryStringMediaTypeMappingTests + [Fact] + public void Ctor_ThrowsArgumentNull_WhenParameterNameNull() { - [Fact] - public void Ctor_ThrowsArgumentNull_WhenParameterNameNull() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new QueryStringMediaTypeMapping(null,"application/json"), "queryStringParameterName"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new QueryStringMediaTypeMapping(null,"application/json"), "queryStringParameterName"); + } - [Fact] - public void Ctor_SetMediaType() - { - // Arrange & Act - QueryStringMediaTypeMapping mapping = new QueryStringMediaTypeMapping("$format", "json", "application/json;odata.streaming=true"); + [Fact] + public void Ctor_SetMediaType() + { + // Arrange & Act + QueryStringMediaTypeMapping mapping = new QueryStringMediaTypeMapping("$format", "json", "application/json;odata.streaming=true"); - // Assert - Assert.Equal("$format", mapping.QueryStringParameterName); - Assert.Equal("json", mapping.QueryStringParameterValue); + // Assert + Assert.Equal("$format", mapping.QueryStringParameterName); + Assert.Equal("json", mapping.QueryStringParameterValue); - Assert.Equal("application/json", mapping.MediaType.MediaType); - var parameter = Assert.Single(mapping.MediaType.Parameters); - Assert.Equal("odata.streaming", parameter.Name); - Assert.Equal("true", parameter.Value); - } + Assert.Equal("application/json", mapping.MediaType.MediaType); + var parameter = Assert.Single(mapping.MediaType.Parameters); + Assert.Equal("odata.streaming", parameter.Name); + Assert.Equal("true", parameter.Value); + } - [Fact] - public void TryMatchMediaType_ThrowsArgumentNull_WhenRequestIsNull() - { - // Arrange & Act - QueryStringMediaTypeMapping mapping = new QueryStringMediaTypeMapping("$format", "application/json"); + [Fact] + public void TryMatchMediaType_ThrowsArgumentNull_WhenRequestIsNull() + { + // Arrange & Act + QueryStringMediaTypeMapping mapping = new QueryStringMediaTypeMapping("$format", "application/json"); - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => mapping.TryMatchMediaType(null), "request"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => mapping.TryMatchMediaType(null), "request"); + } - [Fact] - public void TryMatchMediaType_DoesnotMatchRequest_WithNonQueryString() - { - // Arrange - QueryStringMediaTypeMapping mapping = new QueryStringMediaTypeMapping("$format", "application/json"); - HttpRequest request = new DefaultHttpContext().Request; + [Fact] + public void TryMatchMediaType_DoesnotMatchRequest_WithNonQueryString() + { + // Arrange + QueryStringMediaTypeMapping mapping = new QueryStringMediaTypeMapping("$format", "application/json"); + HttpRequest request = new DefaultHttpContext().Request; - // Act - double mapResult = mapping.TryMatchMediaType(request); + // Act + double mapResult = mapping.TryMatchMediaType(request); - // Assert - Assert.Equal(0, mapResult); - } + // Assert + Assert.Equal(0, mapResult); + } - [Theory] - [InlineData("?$format=application/json;odata.streaming=true", 1.0)] - [InlineData("?$format=json", 1.0)] - [InlineData("?$format=application/json", 0.0)] - [InlineData("?$format=application/xml", 0.0)] - [InlineData("?$format=xml", 0.0)] - public void TryMatchMediaType_MatchRequest_WithStreamPropertyRequest(string queryString, double expect) - { - // Arrange - QueryStringMediaTypeMapping mapping = new QueryStringMediaTypeMapping("$format", "json", "application/json;odata.streaming=true"); - HttpRequest request = new DefaultHttpContext().Request; - request.QueryString = new QueryString(queryString); + [Theory] + [InlineData("?$format=application/json;odata.streaming=true", 1.0)] + [InlineData("?$format=json", 1.0)] + [InlineData("?$format=application/json", 0.0)] + [InlineData("?$format=application/xml", 0.0)] + [InlineData("?$format=xml", 0.0)] + public void TryMatchMediaType_MatchRequest_WithStreamPropertyRequest(string queryString, double expect) + { + // Arrange + QueryStringMediaTypeMapping mapping = new QueryStringMediaTypeMapping("$format", "json", "application/json;odata.streaming=true"); + HttpRequest request = new DefaultHttpContext().Request; + request.QueryString = new QueryString(queryString); - // Act - double mapResult = mapping.TryMatchMediaType(request); + // Act + double mapResult = mapping.TryMatchMediaType(request); - // Assert - Assert.Equal(expect, mapResult); - } + // Assert + Assert.Equal(expect, mapResult); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/Address.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/Address.cs index eb61161d4..2e676ee04 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/Address.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/Address.cs @@ -8,42 +8,41 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Models +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Models; + +public class Address { - public class Address - { - public string Street { get; set; } + public string Street { get; set; } - public string City { get; set; } + public string City { get; set; } - public string State { get; set; } + public string State { get; set; } - public string ZipCode { get; set; } + public string ZipCode { get; set; } - public string CountryOrRegion { get; set; } - } + public string CountryOrRegion { get; set; } +} - public class UsAddress : Address - { - public string UsProp { get; set; } - } +public class UsAddress : Address +{ + public string UsProp { get; set; } +} - public class CnAddress : Address - { - public Guid CnProp { get; set; } - } +public class CnAddress : Address +{ + public Guid CnProp { get; set; } +} - public class Location - { - public string Name { get; set; } +public class Location +{ + public string Name { get; set; } - public Address Address { get; set; } - } + public Address Address { get; set; } +} - public class SimpleOpenAddress - { - public string Street { get; set; } - public string City { get; set; } - public IDictionary Properties { get; set; } - } +public class SimpleOpenAddress +{ + public string Street { get; set; } + public string City { get; set; } + public IDictionary Properties { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/Customer.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/Customer.cs index 5262b3a00..5caa2a1aa 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/Customer.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/Customer.cs @@ -8,29 +8,28 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Models +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Models; + +public class Customer { - public class Customer + public Customer() { - public Customer() - { - this.Orders = new List(); - } - - public int ID { get; set; } - public string Name { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public string City { get; set; } - public IList Orders { get; set; } - public SimpleEnum SimpleEnum { get; set; } - public Address HomeAddress { get; set; } + this.Orders = new List(); } - public class SpecialCustomer : Customer - { - public int Level { get; set; } - public DateTimeOffset Birthday { get; set; } - public decimal Bonus { get; set; } - } + public int ID { get; set; } + public string Name { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string City { get; set; } + public IList Orders { get; set; } + public SimpleEnum SimpleEnum { get; set; } + public Address HomeAddress { get; set; } +} + +public class SpecialCustomer : Customer +{ + public int Level { get; set; } + public DateTimeOffset Birthday { get; set; } + public decimal Bonus { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/Order.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/Order.cs index a601ea675..8116f411d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/Order.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/Order.cs @@ -7,14 +7,13 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Models +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Models; + +public class Order { - public class Order - { - public int ID { get; set; } - public string Name { get; set; } - public string City { get; set; } - public Customer Customer { get; set; } - public IDictionary OrderProperties { get; set; } - } + public int ID { get; set; } + public string Name { get; set; } + public string City { get; set; } + public Customer Customer { get; set; } + public IDictionary OrderProperties { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/SimpleEnum.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/SimpleEnum.cs index 29b870184..c2d87e989 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/SimpleEnum.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Models/SimpleEnum.cs @@ -5,16 +5,15 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Models +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Models; + +public enum SimpleEnum { - public enum SimpleEnum - { - First, + First, - Second, + Second, - Third, + Third, - Fourth - } + Fourth } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataBodyModelBinderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataBodyModelBinderTests.cs index 5d3e3b3d5..80063cdc2 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataBodyModelBinderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataBodyModelBinderTests.cs @@ -18,69 +18,68 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ODataBodyModelBinderTests { - public class ODataBodyModelBinderTests + [Fact] + public async Task BindModelAsync_ThrowsArgumentNull_BindingContext() { - [Fact] - public async Task BindModelAsync_ThrowsArgumentNull_BindingContext() - { - // Arrange & Act & Assert - ODataBodyModelBinder binder = new ODataBodyModelBinder(); - await ExceptionAssert.ThrowsArgumentNullAsync(() => binder.BindModelAsync(null), "bindingContext"); - } + // Arrange & Act & Assert + ODataBodyModelBinder binder = new ODataBodyModelBinder(); + await ExceptionAssert.ThrowsArgumentNullAsync(() => binder.BindModelAsync(null), "bindingContext"); + } - [Fact] - public async Task BindModelAsync_ThrowsArgument_ModelMetadata() - { - // Arrange & Act & Assert - ODataBodyModelBinder binder = new ODataBodyModelBinder(); - Mock context = new Mock(); - context.Setup(c => c.ModelMetadata).Returns((ModelMetadata)null); + [Fact] + public async Task BindModelAsync_ThrowsArgument_ModelMetadata() + { + // Arrange & Act & Assert + ODataBodyModelBinder binder = new ODataBodyModelBinder(); + Mock context = new Mock(); + context.Setup(c => c.ModelMetadata).Returns((ModelMetadata)null); - ArgumentException exception = await ExceptionAssert.ThrowsAsync(() => binder.BindModelAsync(context.Object)); - Assert.Equal("The binding context cannot have a null ModelMetadata. (Parameter 'bindingContext')", exception.Message); - } + ArgumentException exception = await ExceptionAssert.ThrowsAsync(() => binder.BindModelAsync(context.Object)); + Assert.Equal("The binding context cannot have a null ModelMetadata. (Parameter 'bindingContext')", exception.Message); + } + + [Fact] + public async Task BindModelAsync_RetrievesResult_FromODataFeature() + { + // Arrange + HttpContext httpContext = new DefaultHttpContext(); + ModelBindingContext context = new MyDefaultModelBindingContext(httpContext); + ModelMetadataIdentity identity = ModelMetadataIdentity.ForProperty(typeof(ATest).GetProperty("Name"), typeof(ATest), typeof(ATest)); + Mock modelMetadata = new Mock(identity); + context.ModelMetadata = modelMetadata.Object; - [Fact] - public async Task BindModelAsync_RetrievesResult_FromODataFeature() + ODataFeature odataFeature = httpContext.ODataFeature() as ODataFeature; + odataFeature.BodyValues = new Dictionary() { - // Arrange - HttpContext httpContext = new DefaultHttpContext(); - ModelBindingContext context = new MyDefaultModelBindingContext(httpContext); - ModelMetadataIdentity identity = ModelMetadataIdentity.ForProperty(typeof(ATest).GetProperty("Name"), typeof(ATest), typeof(ATest)); - Mock modelMetadata = new Mock(identity); - context.ModelMetadata = modelMetadata.Object; + { "Name", "aValue" } + }; - ODataFeature odataFeature = httpContext.ODataFeature() as ODataFeature; - odataFeature.BodyValues = new Dictionary() - { - { "Name", "aValue" } - }; + // Act + ODataBodyModelBinder binder = new ODataBodyModelBinder(); + await binder.BindModelAsync(context); - // Act - ODataBodyModelBinder binder = new ODataBodyModelBinder(); - await binder.BindModelAsync(context); + // Assert + Assert.True(context.Result.IsModelSet); + Assert.Equal("aValue", context.Result.Model); + } - // Assert - Assert.True(context.Result.IsModelSet); - Assert.Equal("aValue", context.Result.Model); - } + private class ATest + { + public string Name { get; set; } + } - private class ATest + private class MyDefaultModelBindingContext : DefaultModelBindingContext + { + private HttpContext _context; + public MyDefaultModelBindingContext(HttpContext context) { - public string Name { get; set; } + _context = context; } - private class MyDefaultModelBindingContext : DefaultModelBindingContext - { - private HttpContext _context; - public MyDefaultModelBindingContext(HttpContext context) - { - _context = context; - } - - public override HttpContext HttpContext => _context; - } + public override HttpContext HttpContext => _context; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataFormatterHelpers.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataFormatterHelpers.cs index 8b0f2d41f..10e04854e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataFormatterHelpers.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataFormatterHelpers.cs @@ -24,151 +24,150 @@ using Microsoft.OData; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +/// +/// A factory for creating . +/// +public static class ODataFormatterHelpers { - /// - /// A factory for creating . - /// - public static class ODataFormatterHelpers + private static IServiceProvider _serviceProvider = BuildServiceProvider(); + + internal static ODataOutputFormatter GetOutputFormatter(ODataPayloadKind[] payload, string mediaType = null) { - private static IServiceProvider _serviceProvider = BuildServiceProvider(); + // request is not needed on AspNetCore. + ODataOutputFormatter formatter; + formatter = new ODataOutputFormatter(payload); + formatter.SupportedEncodings.Add(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)); + formatter.SupportedEncodings.Add(new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true)); - internal static ODataOutputFormatter GetOutputFormatter(ODataPayloadKind[] payload, string mediaType = null) + if (mediaType != null) { - // request is not needed on AspNetCore. - ODataOutputFormatter formatter; - formatter = new ODataOutputFormatter(payload); - formatter.SupportedEncodings.Add(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)); - formatter.SupportedEncodings.Add(new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true)); - - if (mediaType != null) - { - formatter.SupportedMediaTypes.Add(mediaType); - } - else - { - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadata); - formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationXml); - } - - return formatter; + formatter.SupportedMediaTypes.Add(mediaType); } - - internal static ObjectResult GetContent(T content, ODataOutputFormatter formatter, string mediaType) + else { - ObjectResult objectResult = new ObjectResult(content); - objectResult.Formatters.Add(formatter); - objectResult.ContentTypes.Add(mediaType); - - return objectResult; + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationJsonODataMinimalMetadata); + formatter.SupportedMediaTypes.Add(ODataMediaTypes.ApplicationXml); } - internal static async Task GetContentResult(ObjectResult content, HttpRequest request) - { - var objectType = content.DeclaredType; - if (objectType == null || objectType == typeof(object)) - { - objectType = content.Value?.GetType(); - } - - MemoryStream ms = new MemoryStream(); - request.HttpContext.Response.Body = ms; - - var formatterContext = new OutputFormatterWriteContext( - request.HttpContext, - CreateWriter, - objectType, - content.Value); - - await content.Formatters[0].WriteAsync(formatterContext); - - ms.Flush(); - ms.Position = 0; - StreamReader reader = new StreamReader(request.HttpContext.Response.Body); - return reader.ReadToEnd(); - } + return formatter; + } + + internal static ObjectResult GetContent(T content, ODataOutputFormatter formatter, string mediaType) + { + ObjectResult objectResult = new ObjectResult(content); + objectResult.Formatters.Add(formatter); + objectResult.ContentTypes.Add(mediaType); + + return objectResult; + } - private static TextWriter CreateWriter(Stream stream, Encoding encoding) + internal static async Task GetContentResult(ObjectResult content, HttpRequest request) + { + var objectType = content.DeclaredType; + if (objectType == null || objectType == typeof(object)) { - const int DefaultBufferSize = 16 * 1024; - return new HttpResponseStreamWriter(stream, encoding, DefaultBufferSize); + objectType = content.Value?.GetType(); } - internal static IHeaderDictionary GetContentHeaders(string contentType = null) - { - IHeaderDictionary headers = RequestFactory.Create().Headers; - if (!string.IsNullOrEmpty(contentType)) - { - headers["Content-Type"] = contentType; - } + MemoryStream ms = new MemoryStream(); + request.HttpContext.Response.Body = ms; - return headers; - } + var formatterContext = new OutputFormatterWriteContext( + request.HttpContext, + CreateWriter, + objectType, + content.Value); - /// - /// Gets an . - /// - /// An ODataSerializerProvider. - public static IODataSerializerProvider GetSerializerProvider() - { - return _serviceProvider.GetRequiredService(); - } + await content.Formatters[0].WriteAsync(formatterContext); - /// - /// Gets an . - /// - /// An ODataDeserializerProvider. - public static IODataDeserializerProvider GetDeserializerProvider() - { - return _serviceProvider.GetRequiredService(); - } + ms.Flush(); + ms.Position = 0; + StreamReader reader = new StreamReader(request.HttpContext.Response.Body); + return reader.ReadToEnd(); + } - public static ODataMessageWriter GetMockODataMessageWriter() - { - MockODataRequestMessage requestMessage = new MockODataRequestMessage(); - return new ODataMessageWriter(requestMessage); - } + private static TextWriter CreateWriter(Stream stream, Encoding encoding) + { + const int DefaultBufferSize = 16 * 1024; + return new HttpResponseStreamWriter(stream, encoding, DefaultBufferSize); + } - public static ODataMessageReader GetMockODataMessageReader() + internal static IHeaderDictionary GetContentHeaders(string contentType = null) + { + IHeaderDictionary headers = RequestFactory.Create().Headers; + if (!string.IsNullOrEmpty(contentType)) { - MockODataRequestMessage requestMessage = new MockODataRequestMessage(); - return new ODataMessageReader(requestMessage); + headers["Content-Type"] = contentType; } - private static IServiceProvider BuildServiceProvider() - { - IServiceCollection services = new ServiceCollection(); - - services.AddSingleton(); - - // Deserializers. - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - // Serializers. - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - // services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - return services.BuildServiceProvider(); - } + return headers; + } + + /// + /// Gets an . + /// + /// An ODataSerializerProvider. + public static IODataSerializerProvider GetSerializerProvider() + { + return _serviceProvider.GetRequiredService(); + } + + /// + /// Gets an . + /// + /// An ODataDeserializerProvider. + public static IODataDeserializerProvider GetDeserializerProvider() + { + return _serviceProvider.GetRequiredService(); + } + + public static ODataMessageWriter GetMockODataMessageWriter() + { + MockODataRequestMessage requestMessage = new MockODataRequestMessage(); + return new ODataMessageWriter(requestMessage); + } + + public static ODataMessageReader GetMockODataMessageReader() + { + MockODataRequestMessage requestMessage = new MockODataRequestMessage(); + return new ODataMessageReader(requestMessage); + } + + private static IServiceProvider BuildServiceProvider() + { + IServiceCollection services = new ServiceCollection(); + + services.AddSingleton(); + + // Deserializers. + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Serializers. + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + // services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services.BuildServiceProvider(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataInputFormatterFactoryTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataInputFormatterFactoryTests.cs index 4db612179..85ea25b89 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataInputFormatterFactoryTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataInputFormatterFactoryTests.cs @@ -21,25 +21,95 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ODataInputFormatterFactoryTests { - public class ODataInputFormatterFactoryTests + private static IEdmModel _edmModel = GetEdmModel(); + private static IList _formatters = ODataInputFormatterFactory.Create(); + + [Fact] + public void CreateReturnsCorrectInputFormattersCount() { - private static IEdmModel _edmModel = GetEdmModel(); - private static IList _formatters = ODataInputFormatterFactory.Create(); + // Arrange & Act & Assert + Assert.Equal(3, _formatters.Count); + } - [Fact] - public void CreateReturnsCorrectInputFormattersCount() + [Fact] + public void ODataInputFormattersContainsSupportedMediaTypes() + { + // Arrange + string[] expectedMediaTypes = new string[] { - // Arrange & Act & Assert - Assert.Equal(3, _formatters.Count); - } + "application/json;odata.metadata=minimal;odata.streaming=true", + "application/json;odata.metadata=minimal;odata.streaming=false", + "application/json;odata.metadata=minimal", + "application/json;odata.metadata=full;odata.streaming=true", + "application/json;odata.metadata=full;odata.streaming=false", + "application/json;odata.metadata=full", + "application/json;odata.metadata=none;odata.streaming=true", + "application/json;odata.metadata=none;odata.streaming=false", + "application/json;odata.metadata=none", + "application/json;odata.streaming=true", + "application/json;odata.streaming=false", + "application/json", + "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false", + "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=true", + "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=false", + "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=true", + "application/json;odata.metadata=minimal;IEEE754Compatible=false", + "application/json;odata.metadata=minimal;IEEE754Compatible=true", + "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=false", + "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=true", + "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=false", + "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=true", + "application/json;odata.metadata=full;IEEE754Compatible=false", + "application/json;odata.metadata=full;IEEE754Compatible=true", + "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=false", + "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=true", + "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=true", + "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=false", + "application/json;odata.metadata=none;IEEE754Compatible=false", + "application/json;odata.metadata=none;IEEE754Compatible=true", + "application/json;odata.streaming=true;IEEE754Compatible=false", + "application/json;odata.streaming=true;IEEE754Compatible=true", + "application/json;odata.streaming=false;IEEE754Compatible=false", + "application/json;odata.streaming=false;IEEE754Compatible=true", + "application/json;IEEE754Compatible=false", + "application/json;IEEE754Compatible=true", + "application/xml", + "text/plain" + }; + + // Act + IEnumerable supportedMediaTypes = _formatters.SelectMany(f => f.SupportedMediaTypes).Distinct(); + + // Assert + Assert.True(expectedMediaTypes.SequenceEqual(supportedMediaTypes)); + } + + [Fact] + public void ODataInputFormattersContainsSupportedEncodings() + { + // Arrange + string[] expectedEncodings = new string[] + { + "Unicode (UTF-8)", + "Unicode" + }; + + // Act + IEnumerable supportedEncodings = _formatters.SelectMany(f => f.SupportedEncodings).Distinct().Select(c => c.EncodingName); - [Fact] - public void ODataInputFormattersContainsSupportedMediaTypes() + // Assert + Assert.True(expectedEncodings.SequenceEqual(supportedEncodings)); + } + + public static TheoryDataSet InputFormatterSupportedMediaTypesTests + { + get { - // Arrange - string[] expectedMediaTypes = new string[] + string[] applicationJsonMediaTypes = new[] { "application/json;odata.metadata=minimal;odata.streaming=true", "application/json;odata.metadata=minimal;odata.streaming=false", @@ -76,127 +146,56 @@ public void ODataInputFormattersContainsSupportedMediaTypes() "application/json;odata.streaming=false;IEEE754Compatible=false", "application/json;odata.streaming=false;IEEE754Compatible=true", "application/json;IEEE754Compatible=false", - "application/json;IEEE754Compatible=true", - "application/xml", - "text/plain" + "application/json;IEEE754Compatible=true" }; - // Act - IEnumerable supportedMediaTypes = _formatters.SelectMany(f => f.SupportedMediaTypes).Distinct(); - - // Assert - Assert.True(expectedMediaTypes.SequenceEqual(supportedMediaTypes)); - } - - [Fact] - public void ODataInputFormattersContainsSupportedEncodings() - { - // Arrange - string[] expectedEncodings = new string[] + return new TheoryDataSet { - "Unicode (UTF-8)", - "Unicode" + { typeof(SampleType), applicationJsonMediaTypes }, + { typeof(Uri), applicationJsonMediaTypes }, + { typeof(ODataActionParameters), applicationJsonMediaTypes } }; - - // Act - IEnumerable supportedEncodings = _formatters.SelectMany(f => f.SupportedEncodings).Distinct().Select(c => c.EncodingName); - - // Assert - Assert.True(expectedEncodings.SequenceEqual(supportedEncodings)); - } - - public static TheoryDataSet InputFormatterSupportedMediaTypesTests - { - get - { - string[] applicationJsonMediaTypes = new[] - { - "application/json;odata.metadata=minimal;odata.streaming=true", - "application/json;odata.metadata=minimal;odata.streaming=false", - "application/json;odata.metadata=minimal", - "application/json;odata.metadata=full;odata.streaming=true", - "application/json;odata.metadata=full;odata.streaming=false", - "application/json;odata.metadata=full", - "application/json;odata.metadata=none;odata.streaming=true", - "application/json;odata.metadata=none;odata.streaming=false", - "application/json;odata.metadata=none", - "application/json;odata.streaming=true", - "application/json;odata.streaming=false", - "application/json", - "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false", - "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=true", - "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=false", - "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=true", - "application/json;odata.metadata=minimal;IEEE754Compatible=false", - "application/json;odata.metadata=minimal;IEEE754Compatible=true", - "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=false", - "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=true", - "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=false", - "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=true", - "application/json;odata.metadata=full;IEEE754Compatible=false", - "application/json;odata.metadata=full;IEEE754Compatible=true", - "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=false", - "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=true", - "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=true", - "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=false", - "application/json;odata.metadata=none;IEEE754Compatible=false", - "application/json;odata.metadata=none;IEEE754Compatible=true", - "application/json;odata.streaming=true;IEEE754Compatible=false", - "application/json;odata.streaming=true;IEEE754Compatible=true", - "application/json;odata.streaming=false;IEEE754Compatible=false", - "application/json;odata.streaming=false;IEEE754Compatible=true", - "application/json;IEEE754Compatible=false", - "application/json;IEEE754Compatible=true" - }; - - return new TheoryDataSet - { - { typeof(SampleType), applicationJsonMediaTypes }, - { typeof(Uri), applicationJsonMediaTypes }, - { typeof(ODataActionParameters), applicationJsonMediaTypes } - }; - } } + } - [Theory] - [MemberData(nameof(InputFormatterSupportedMediaTypesTests))] - public void ODataInputFormattersForReadTypeReturnsSupportedMediaTypes(Type type, string[] expectedMediaTypes) - { - // Arrange - HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents("odata", _edmModel)); - request.Configure("odata", _edmModel, new ODataPath()); + [Theory] + [MemberData(nameof(InputFormatterSupportedMediaTypesTests))] + public void ODataInputFormattersForReadTypeReturnsSupportedMediaTypes(Type type, string[] expectedMediaTypes) + { + // Arrange + HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents("odata", _edmModel)); + request.Configure("odata", _edmModel, new ODataPath()); - IEnumerable odataFormatters = _formatters.Where(f => CanReadType(f, type, request)); + IEnumerable odataFormatters = _formatters.Where(f => CanReadType(f, type, request)); - // Act - IEnumerable supportedMediaTypes = odataFormatters.SelectMany(f => f.SupportedMediaTypes).Distinct(); + // Act + IEnumerable supportedMediaTypes = odataFormatters.SelectMany(f => f.SupportedMediaTypes).Distinct(); - // Assert - Assert.True(expectedMediaTypes.SequenceEqual(supportedMediaTypes)); - } + // Assert + Assert.True(expectedMediaTypes.SequenceEqual(supportedMediaTypes)); + } - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder model = new ODataConventionModelBuilder(); - model.EntityType(); - return model.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder model = new ODataConventionModelBuilder(); + model.EntityType(); + return model.GetEdmModel(); + } - private static bool CanReadType(ODataInputFormatter formatter, Type type, HttpRequest request) - { - InputFormatterContext context = new InputFormatterContext( - request.HttpContext, - "modelName", - new ModelStateDictionary(), - new EmptyModelMetadataProvider().GetMetadataForType(type), - (stream, encoding) => new StreamReader(stream, encoding)); - - return formatter.CanRead(context); - } + private static bool CanReadType(ODataInputFormatter formatter, Type type, HttpRequest request) + { + InputFormatterContext context = new InputFormatterContext( + request.HttpContext, + "modelName", + new ModelStateDictionary(), + new EmptyModelMetadataProvider().GetMetadataForType(type), + (stream, encoding) => new StreamReader(stream, encoding)); + + return formatter.CanRead(context); + } - private class SampleType - { - public int Id { get; set; } - } + private class SampleType + { + public int Id { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataInputFormatterTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataInputFormatterTests.cs index a520cbf02..8ad63a770 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataInputFormatterTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataInputFormatterTests.cs @@ -28,314 +28,313 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ODataInputFormatterTests { - public class ODataInputFormatterTests - { - private static IEdmModel _edmModel = GetEdmModel(); + private static IEdmModel _edmModel = GetEdmModel(); - [Fact] - public void ConstructorThrowsIfPayloadsIsNull() - { - // Arrange & Act & Assert - Assert.Throws("payloadKinds", () => new ODataInputFormatter(payloadKinds: null)); - } + [Fact] + public void ConstructorThrowsIfPayloadsIsNull() + { + // Arrange & Act & Assert + Assert.Throws("payloadKinds", () => new ODataInputFormatter(payloadKinds: null)); + } - [Fact] - public void GetSupportedContentTypesODataInputFormatter_WorksForContentType() - { - // Arrange - ODataInputFormatter formatter = ODataInputFormatterFactory.Create().First(); - Assert.NotNull(formatter); // guard + [Fact] + public void GetSupportedContentTypesODataInputFormatter_WorksForContentType() + { + // Arrange + ODataInputFormatter formatter = ODataInputFormatterFactory.Create().First(); + Assert.NotNull(formatter); // guard - // Act & Assert - IReadOnlyList contentTypes = formatter.GetSupportedContentTypes("application/json", typeof(string)); - Assert.Equal(36, contentTypes.Count); + // Act & Assert + IReadOnlyList contentTypes = formatter.GetSupportedContentTypes("application/json", typeof(string)); + Assert.Equal(36, contentTypes.Count); - // Act & Assert - formatter.SupportedMediaTypes.Clear(); - ExceptionAssert.DoesNotThrow(() => formatter.GetSupportedContentTypes("application/json", typeof(string))); - } + // Act & Assert + formatter.SupportedMediaTypes.Clear(); + ExceptionAssert.DoesNotThrow(() => formatter.GetSupportedContentTypes("application/json", typeof(string))); + } - [Fact] - public void CanReadThrowsIfContextIsNull() - { - // Arrange & Act - ODataInputFormatter formatter = new ODataInputFormatter(new[] { ODataPayloadKind.Resource }); + [Fact] + public void CanReadThrowsIfContextIsNull() + { + // Arrange & Act + ODataInputFormatter formatter = new ODataInputFormatter(new[] { ODataPayloadKind.Resource }); - // Assert - Assert.Throws("context", () => formatter.CanRead(context: null)); - } + // Assert + Assert.Throws("context", () => formatter.CanRead(context: null)); + } - [Fact] - public void CanReadResultODataOutputFormatter_Throws_IfRequestIsNull() - { - // Arrange & Act - ODataInputFormatter formatter = new ODataInputFormatter(new[] { ODataPayloadKind.Resource }); - Mock httpContext = new Mock(); - httpContext.Setup(c => c.Request).Returns((HttpRequest)null); - InputFormatterContext context = new InputFormatterContext(httpContext.Object, - "any", - new ModelStateDictionary(), - new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)), - (stream, encoding) => null); - - // Assert - ExceptionAssert.Throws( - () => formatter.CanRead(context), - "The OData formatter requires an attached request in order to deserialize."); - } + [Fact] + public void CanReadResultODataOutputFormatter_Throws_IfRequestIsNull() + { + // Arrange & Act + ODataInputFormatter formatter = new ODataInputFormatter(new[] { ODataPayloadKind.Resource }); + Mock httpContext = new Mock(); + httpContext.Setup(c => c.Request).Returns((HttpRequest)null); + InputFormatterContext context = new InputFormatterContext(httpContext.Object, + "any", + new ModelStateDictionary(), + new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)), + (stream, encoding) => null); + + // Assert + ExceptionAssert.Throws( + () => formatter.CanRead(context), + "The OData formatter requires an attached request in order to deserialize."); + } - [Fact] - public void CanReadReturnsFalseIfNoODataPathSet() - { - // Arrange & Act - InputFormatterContext context = new InputFormatterContext( - new DefaultHttpContext(), - "modelName", - new ModelStateDictionary(), - new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)), - (stream, encoding) => new StreamReader(stream, encoding)); - - ODataInputFormatter formatter = new ODataInputFormatter(new[] { ODataPayloadKind.Resource }); - - // Assert - Assert.False(formatter.CanRead(context)); - } + [Fact] + public void CanReadReturnsFalseIfNoODataPathSet() + { + // Arrange & Act + InputFormatterContext context = new InputFormatterContext( + new DefaultHttpContext(), + "modelName", + new ModelStateDictionary(), + new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)), + (stream, encoding) => new StreamReader(stream, encoding)); + + ODataInputFormatter formatter = new ODataInputFormatter(new[] { ODataPayloadKind.Resource }); + + // Assert + Assert.False(formatter.CanRead(context)); + } - [Theory] - [InlineData(typeof(Customer), ODataPayloadKind.Resource, true)] - [InlineData(typeof(Customer), ODataPayloadKind.Property, false)] - [InlineData(typeof(Customer[]), ODataPayloadKind.ResourceSet, true)] - [InlineData(typeof(int), ODataPayloadKind.Property, true)] - [InlineData(typeof(IList), ODataPayloadKind.Collection, true)] - [InlineData(typeof(ODataActionParameters), ODataPayloadKind.Parameter, true)] - [InlineData(typeof(Delta), ODataPayloadKind.Resource, true)] - [InlineData(typeof(IEdmEntityObject), ODataPayloadKind.Resource, true)] - public void CanReadReturnsBooleanValueAsExpected(Type type, ODataPayloadKind payloadKind, bool expected) - { - // Arrange & Act - IEdmEntitySet entitySet = _edmModel.EntityContainer.FindEntitySet("Customers"); - EntitySetSegment entitySetSeg = new EntitySetSegment(entitySet); - HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents("odata", _edmModel)); - request.ODataFeature().RoutePrefix = "odata"; - request.ODataFeature().Model = _edmModel; - request.ODataFeature().Path = new ODataPath(entitySetSeg); + [Theory] + [InlineData(typeof(Customer), ODataPayloadKind.Resource, true)] + [InlineData(typeof(Customer), ODataPayloadKind.Property, false)] + [InlineData(typeof(Customer[]), ODataPayloadKind.ResourceSet, true)] + [InlineData(typeof(int), ODataPayloadKind.Property, true)] + [InlineData(typeof(IList), ODataPayloadKind.Collection, true)] + [InlineData(typeof(ODataActionParameters), ODataPayloadKind.Parameter, true)] + [InlineData(typeof(Delta), ODataPayloadKind.Resource, true)] + [InlineData(typeof(IEdmEntityObject), ODataPayloadKind.Resource, true)] + public void CanReadReturnsBooleanValueAsExpected(Type type, ODataPayloadKind payloadKind, bool expected) + { + // Arrange & Act + IEdmEntitySet entitySet = _edmModel.EntityContainer.FindEntitySet("Customers"); + EntitySetSegment entitySetSeg = new EntitySetSegment(entitySet); + HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents("odata", _edmModel)); + request.ODataFeature().RoutePrefix = "odata"; + request.ODataFeature().Model = _edmModel; + request.ODataFeature().Path = new ODataPath(entitySetSeg); - InputFormatterContext context = CreateInputContext(type, request); + InputFormatterContext context = CreateInputContext(type, request); - ODataInputFormatter formatter = new ODataInputFormatter(new[] { payloadKind }); + ODataInputFormatter formatter = new ODataInputFormatter(new[] { payloadKind }); - // Assert - Assert.Equal(expected, formatter.CanRead(context)); - } + // Assert + Assert.Equal(expected, formatter.CanRead(context)); + } - [Fact] - public Task ReadFromStreamAsyncDoesNotCloseStreamWhenContentLengthIsZero() - { - // Arrange - ODataInputFormatter formatter = GetInputFormatter(); - Mock mockStream = new Mock(); - - byte[] contentBytes = Encoding.UTF8.GetBytes(string.Empty); - HttpContext httpContext = GetHttpContext(contentBytes); - InputFormatterContext formatterContext = CreateInputFormatterContext(typeof(Customer), httpContext); - - // Act - return formatter.ReadRequestBodyAsync(formatterContext, Encoding.UTF8) - .ContinueWith( - readTask => - { - // Assert - Assert.Equal(TaskStatus.RanToCompletion, readTask.Status); - mockStream.Verify(s => s.Close(), Times.Never()); - }); - } + [Fact] + public Task ReadFromStreamAsyncDoesNotCloseStreamWhenContentLengthIsZero() + { + // Arrange + ODataInputFormatter formatter = GetInputFormatter(); + Mock mockStream = new Mock(); - [Fact] - public async Task ReadRequestBodyAsync_ThrowsArgumentNull_ForInputParameter() - { - // Arrange - ODataInputFormatter formatter = GetInputFormatter(); - InputFormatterContext formatterContext = null; - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => formatter.ReadRequestBodyAsync(formatterContext, Encoding.UTF8), "context"); - - //Mock mock = new Mock(); - //mock.Setup(s => s.ModelType).Returns((Type)null); - //formatterContext = new InputFormatterContext(new DefaultHttpContext(), - // modelName: string.Empty, - // modelState: new ModelStateDictionary(), - // metadata: mock.Object, - // readerFactory: (stream, encoding) => new StreamReader(stream, encoding)); - - //await ExceptionAssert.ThrowsArgumentNullAsync(() => formatter.ReadRequestBodyAsync(formatterContext, Encoding.UTF8), "type"); - } + byte[] contentBytes = Encoding.UTF8.GetBytes(string.Empty); + HttpContext httpContext = GetHttpContext(contentBytes); + InputFormatterContext formatterContext = CreateInputFormatterContext(typeof(Customer), httpContext); - [Fact] - public void GetDefaultBaseAddress_ThrowsArgumentNull_Request() - { - ExceptionAssert.ThrowsArgumentNull(() => ODataInputFormatter.GetDefaultBaseAddress(null), "request"); - } + // Act + return formatter.ReadRequestBodyAsync(formatterContext, Encoding.UTF8) + .ContinueWith( + readTask => + { + // Assert + Assert.Equal(TaskStatus.RanToCompletion, readTask.Status); + mockStream.Verify(s => s.Close(), Times.Never()); + }); + } - [Theory] - [InlineData(typeof(bool))] - [InlineData(typeof(int))] - [InlineData(typeof(string))] - public async Task ReadRequestBodyAsyncFailsWhenContentLengthIsZero(Type type) - { - // Arrange - ODataInputFormatter formatter = GetInputFormatter(); - byte[] contentBytes = Encoding.UTF8.GetBytes(""); - HttpContext httpContext = GetHttpContext(contentBytes); + [Fact] + public async Task ReadRequestBodyAsync_ThrowsArgumentNull_ForInputParameter() + { + // Arrange + ODataInputFormatter formatter = GetInputFormatter(); + InputFormatterContext formatterContext = null; + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => formatter.ReadRequestBodyAsync(formatterContext, Encoding.UTF8), "context"); + + //Mock mock = new Mock(); + //mock.Setup(s => s.ModelType).Returns((Type)null); + //formatterContext = new InputFormatterContext(new DefaultHttpContext(), + // modelName: string.Empty, + // modelState: new ModelStateDictionary(), + // metadata: mock.Object, + // readerFactory: (stream, encoding) => new StreamReader(stream, encoding)); + + //await ExceptionAssert.ThrowsArgumentNullAsync(() => formatter.ReadRequestBodyAsync(formatterContext, Encoding.UTF8), "type"); + } - InputFormatterContext formatterContext = CreateInputFormatterContext(type, httpContext); + [Fact] + public void GetDefaultBaseAddress_ThrowsArgumentNull_Request() + { + ExceptionAssert.ThrowsArgumentNull(() => ODataInputFormatter.GetDefaultBaseAddress(null), "request"); + } - // Act - InputFormatterResult result = await formatter.ReadRequestBodyAsync(formatterContext, Encoding.UTF8); + [Theory] + [InlineData(typeof(bool))] + [InlineData(typeof(int))] + [InlineData(typeof(string))] + public async Task ReadRequestBodyAsyncFailsWhenContentLengthIsZero(Type type) + { + // Arrange + ODataInputFormatter formatter = GetInputFormatter(); + byte[] contentBytes = Encoding.UTF8.GetBytes(""); + HttpContext httpContext = GetHttpContext(contentBytes); - // Assert - Assert.True(result.HasError); - } + InputFormatterContext formatterContext = CreateInputFormatterContext(type, httpContext); - [Fact] - public Task ReadRequestBodyAsyncReadsDataButDoesNotCloseStreamWhenContentLengthIsNull() - { - // Arrange - byte[] expectedSampleTypeByte = Encoding.UTF8.GetBytes( - "{" + - "\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," + - "\"Number\":42" + - "}"); + // Act + InputFormatterResult result = await formatter.ReadRequestBodyAsync(formatterContext, Encoding.UTF8); - ODataInputFormatter formatter = GetInputFormatter(); + // Assert + Assert.True(result.HasError); + } - HttpContext httpContext = GetHttpContext(expectedSampleTypeByte); - httpContext.Request.ContentType = "application/json;odata.metadata=minimal"; - httpContext.Request.ContentLength = null; - Stream memStream = httpContext.Request.Body; + [Fact] + public Task ReadRequestBodyAsyncReadsDataButDoesNotCloseStreamWhenContentLengthIsNull() + { + // Arrange + byte[] expectedSampleTypeByte = Encoding.UTF8.GetBytes( + "{" + + "\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," + + "\"Number\":42" + + "}"); - InputFormatterContext formatterContext = CreateInputFormatterContext(typeof(Customer), httpContext); + ODataInputFormatter formatter = GetInputFormatter(); - // Act - return formatter.ReadRequestBodyAsync(formatterContext, Encoding.UTF8).ContinueWith( - readTask => - { - // Assert - Assert.Equal(TaskStatus.RanToCompletion, readTask.Status); - Assert.True(memStream.CanRead); + HttpContext httpContext = GetHttpContext(expectedSampleTypeByte); + httpContext.Request.ContentType = "application/json;odata.metadata=minimal"; + httpContext.Request.ContentLength = null; + Stream memStream = httpContext.Request.Body; - InputFormatterResult result = Assert.IsType(readTask.Result); - Assert.Null(result.Model); - }); - } + InputFormatterContext formatterContext = CreateInputFormatterContext(typeof(Customer), httpContext); - [Fact] - public Task ReadRequestBodyAsyncReadsDataButDoesNotCloseStreamWhenContentLength() - { - // Arrange - byte[] expectedSampleTypeByte = Encoding.UTF8.GetBytes( - "{" + - "\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," + - "\"Number\":42" + - "}"); - - IEdmSingleton singleton = _edmModel.EntityContainer.FindSingleton("Me"); - SingletonSegment singletonSeg = new SingletonSegment(singleton); - - ODataInputFormatter formatter = GetInputFormatter(); - formatter.BaseAddressFactory = (request) => new Uri("http://localhost"); - - HttpContext httpContext = GetHttpContext(expectedSampleTypeByte, opt => opt.AddRouteComponents("odata", _edmModel)); - httpContext.Request.ContentType = "application/json;odata.metadata=minimal"; - httpContext.ODataFeature().Model = _edmModel; - httpContext.ODataFeature().RoutePrefix = "odata"; - httpContext.ODataFeature().Path = new ODataPath(singletonSeg); - Stream memStream = httpContext.Request.Body; - - InputFormatterContext formatterContext = CreateInputFormatterContext(typeof(Customer), httpContext); - - // Act - return formatter.ReadRequestBodyAsync(formatterContext, Encoding.UTF8).ContinueWith( - readTask => - { - // Assert - Assert.Equal(TaskStatus.RanToCompletion, readTask.Status); - Assert.True(memStream.CanRead); + // Act + return formatter.ReadRequestBodyAsync(formatterContext, Encoding.UTF8).ContinueWith( + readTask => + { + // Assert + Assert.Equal(TaskStatus.RanToCompletion, readTask.Status); + Assert.True(memStream.CanRead); - var value = Assert.IsType(readTask.Result.Model); - Assert.Equal(42, value.Number); - }); - } + InputFormatterResult result = Assert.IsType(readTask.Result); + Assert.Null(result.Model); + }); + } - private static ODataInputFormatter GetInputFormatter() - { - return ODataInputFormatterFactory.Create().FirstOrDefault(); - } + [Fact] + public Task ReadRequestBodyAsyncReadsDataButDoesNotCloseStreamWhenContentLength() + { + // Arrange + byte[] expectedSampleTypeByte = Encoding.UTF8.GetBytes( + "{" + + "\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," + + "\"Number\":42" + + "}"); + + IEdmSingleton singleton = _edmModel.EntityContainer.FindSingleton("Me"); + SingletonSegment singletonSeg = new SingletonSegment(singleton); + + ODataInputFormatter formatter = GetInputFormatter(); + formatter.BaseAddressFactory = (request) => new Uri("http://localhost"); + + HttpContext httpContext = GetHttpContext(expectedSampleTypeByte, opt => opt.AddRouteComponents("odata", _edmModel)); + httpContext.Request.ContentType = "application/json;odata.metadata=minimal"; + httpContext.ODataFeature().Model = _edmModel; + httpContext.ODataFeature().RoutePrefix = "odata"; + httpContext.ODataFeature().Path = new ODataPath(singletonSeg); + Stream memStream = httpContext.Request.Body; + + InputFormatterContext formatterContext = CreateInputFormatterContext(typeof(Customer), httpContext); + + // Act + return formatter.ReadRequestBodyAsync(formatterContext, Encoding.UTF8).ContinueWith( + readTask => + { + // Assert + Assert.Equal(TaskStatus.RanToCompletion, readTask.Status); + Assert.True(memStream.CanRead); - protected static HttpContext GetHttpContext(byte[] contentBytes, Action setupAction = null) - { - MemoryStream stream = new MemoryStream(contentBytes); - DefaultHttpContext httpContext = new DefaultHttpContext(); - httpContext.Request.Body = stream; - httpContext.Request.ContentType = "application/json"; + var value = Assert.IsType(readTask.Result.Model); + Assert.Equal(42, value.Number); + }); + } - IServiceCollection services = new ServiceCollection(); - if (setupAction != null) - { - services.Configure(setupAction); - } + private static ODataInputFormatter GetInputFormatter() + { + return ODataInputFormatterFactory.Create().FirstOrDefault(); + } - httpContext.RequestServices = services.BuildServiceProvider(); - return httpContext; - } + protected static HttpContext GetHttpContext(byte[] contentBytes, Action setupAction = null) + { + MemoryStream stream = new MemoryStream(contentBytes); + DefaultHttpContext httpContext = new DefaultHttpContext(); + httpContext.Request.Body = stream; + httpContext.Request.ContentType = "application/json"; - protected static InputFormatterContext CreateInputFormatterContext( - Type modelType, - HttpContext httpContext, - string modelName = null) + IServiceCollection services = new ServiceCollection(); + if (setupAction != null) { - var provider = new EmptyModelMetadataProvider(); - var metadata = provider.GetMetadataForType(modelType); - - return new InputFormatterContext( - httpContext, - modelName: modelName ?? string.Empty, - modelState: new ModelStateDictionary(), - metadata: metadata, - readerFactory: (stream, encoding) => new StreamReader(stream, encoding)); + services.Configure(setupAction); } - private static InputFormatterContext CreateInputContext(Type type, HttpRequest request) - { - return new InputFormatterContext( - request.HttpContext, - "modelName", - new ModelStateDictionary(), - new EmptyModelMetadataProvider().GetMetadataForType(type), - (stream, encoding) => new StreamReader(stream, encoding)); - } + httpContext.RequestServices = services.BuildServiceProvider(); + return httpContext; + } - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder model = new ODataConventionModelBuilder(); - model.EntitySet("Customers"); - model.Singleton("Me"); - model.ComplexType
(); - return model.GetEdmModel(); - } + protected static InputFormatterContext CreateInputFormatterContext( + Type modelType, + HttpContext httpContext, + string modelName = null) + { + var provider = new EmptyModelMetadataProvider(); + var metadata = provider.GetMetadataForType(modelType); + + return new InputFormatterContext( + httpContext, + modelName: modelName ?? string.Empty, + modelState: new ModelStateDictionary(), + metadata: metadata, + readerFactory: (stream, encoding) => new StreamReader(stream, encoding)); + } - private class Customer - { - public int Id { get; set; } + private static InputFormatterContext CreateInputContext(Type type, HttpRequest request) + { + return new InputFormatterContext( + request.HttpContext, + "modelName", + new ModelStateDictionary(), + new EmptyModelMetadataProvider().GetMetadataForType(type), + (stream, encoding) => new StreamReader(stream, encoding)); + } - public int Number { get; set; } - } + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder model = new ODataConventionModelBuilder(); + model.EntitySet("Customers"); + model.Singleton("Me"); + model.ComplexType
(); + return model.GetEdmModel(); + } - private class Address - { - public string Street { get; set; } - } + private class Customer + { + public int Id { get; set; } + + public int Number { get; set; } + } + + private class Address + { + public string Street { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataMessageWrapperTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataMessageWrapperTests.cs index 570922046..ccec33f85 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataMessageWrapperTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataMessageWrapperTests.cs @@ -10,65 +10,64 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ODataMessageWrapperTests { - public class ODataMessageWrapperTests + [Fact] + public void DefaultCtor_SetsProperties() { - [Fact] - public void DefaultCtor_SetsProperties() - { - // Arrange & Act & Assert - ODataMessageWrapper wrapper = new ODataMessageWrapper(); - Assert.Null(wrapper.GetStream()); - Assert.NotNull(wrapper.Headers); - } + // Arrange & Act & Assert + ODataMessageWrapper wrapper = new ODataMessageWrapper(); + Assert.Null(wrapper.GetStream()); + Assert.NotNull(wrapper.Headers); + } - [Fact] - public void Url_ThrowsNotImplementException() - { - // Arrange & Act & Assert - ODataMessageWrapper wrapper = new ODataMessageWrapper(); - Assert.Throws(() => wrapper.Url = new Uri("http://any")); - Assert.Throws(() => wrapper.Url); - } + [Fact] + public void Url_ThrowsNotImplementException() + { + // Arrange & Act & Assert + ODataMessageWrapper wrapper = new ODataMessageWrapper(); + Assert.Throws(() => wrapper.Url = new Uri("http://any")); + Assert.Throws(() => wrapper.Url); + } - [Fact] - public void Method_ThrowsNotImplementException() - { - // Arrange & Act & Assert - ODataMessageWrapper wrapper = new ODataMessageWrapper(); - Assert.Throws(() => wrapper.Method = "GET"); - Assert.Throws(() => wrapper.Method); - } + [Fact] + public void Method_ThrowsNotImplementException() + { + // Arrange & Act & Assert + ODataMessageWrapper wrapper = new ODataMessageWrapper(); + Assert.Throws(() => wrapper.Method = "GET"); + Assert.Throws(() => wrapper.Method); + } - [Fact] - public void StatusCode_ThrowsNotImplementException() - { - // Arrange & Act & Assert - ODataMessageWrapper wrapper = new ODataMessageWrapper(); - Assert.Throws(() => wrapper.StatusCode = 200); - Assert.Throws(() => wrapper.StatusCode); - } + [Fact] + public void StatusCode_ThrowsNotImplementException() + { + // Arrange & Act & Assert + ODataMessageWrapper wrapper = new ODataMessageWrapper(); + Assert.Throws(() => wrapper.StatusCode = 200); + Assert.Throws(() => wrapper.StatusCode); + } - [Fact] - public void GetHeader_ReturnsHead_NoMatterCaseSensitive() - { - // Arrange & Act & Assert - ODataMessageWrapper wrapper = new ODataMessageWrapper(); - wrapper.SetHeader("MyHead", "HeadValue"); + [Fact] + public void GetHeader_ReturnsHead_NoMatterCaseSensitive() + { + // Arrange & Act & Assert + ODataMessageWrapper wrapper = new ODataMessageWrapper(); + wrapper.SetHeader("MyHead", "HeadValue"); - Assert.Equal("HeadValue", wrapper.GetHeader("MyHead")); - Assert.Equal("HeadValue", wrapper.GetHeader("myhead")); - } + Assert.Equal("HeadValue", wrapper.GetHeader("MyHead")); + Assert.Equal("HeadValue", wrapper.GetHeader("myhead")); + } - [Fact] - public void ConvertPayloadUri_ThrowsArgumentNull_PayloadUri() - { - // Arrange - ODataMessageWrapper wrapper = new ODataMessageWrapper(); + [Fact] + public void ConvertPayloadUri_ThrowsArgumentNull_PayloadUri() + { + // Arrange + ODataMessageWrapper wrapper = new ODataMessageWrapper(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => wrapper.ConvertPayloadUri(null, null), "payloadUri"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => wrapper.ConvertPayloadUri(null, null), "payloadUri"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataModelBinderConverterTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataModelBinderConverterTests.cs index 23c76c87a..9ca9c1026 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataModelBinderConverterTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataModelBinderConverterTests.cs @@ -22,274 +22,273 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ODataModelBinderConverterTests { - public class ODataModelBinderConverterTests + [Fact] + public void Convert_ForNullValues() { - [Fact] - public void Convert_ForNullValues() - { - var result = ODataModelBinderConverter.Convert(null, null, null, "IsActive", null, null); - Assert.Null(result); + var result = ODataModelBinderConverter.Convert(null, null, null, "IsActive", null, null); + Assert.Null(result); - result = ODataModelBinderConverter.Convert(new ODataNullValue(), null, null, "IsActive", null, null); - Assert.Null(result); - } + result = ODataModelBinderConverter.Convert(new ODataNullValue(), null, null, "IsActive", null, null); + Assert.Null(result); + } - /// - /// The set of potential values to test against - /// . - /// - public static TheoryDataSet ODataModelBinderConverter_Works_TestData + /// + /// The set of potential values to test against + /// . + /// + public static TheoryDataSet ODataModelBinderConverter_Works_TestData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { "true", EdmPrimitiveTypeKind.Boolean, typeof(bool), true }, - { true, EdmPrimitiveTypeKind.Boolean, typeof(bool), true }, - { 5, EdmPrimitiveTypeKind.Int32, typeof(int), 5 }, - { new Guid("C2AEFDF2-B533-4971-8B6A-A539373BFC32"), EdmPrimitiveTypeKind.Guid, typeof(Guid), new Guid("C2AEFDF2-B533-4971-8B6A-A539373BFC32") } - }; - } + { "true", EdmPrimitiveTypeKind.Boolean, typeof(bool), true }, + { true, EdmPrimitiveTypeKind.Boolean, typeof(bool), true }, + { 5, EdmPrimitiveTypeKind.Int32, typeof(int), 5 }, + { new Guid("C2AEFDF2-B533-4971-8B6A-A539373BFC32"), EdmPrimitiveTypeKind.Guid, typeof(Guid), new Guid("C2AEFDF2-B533-4971-8B6A-A539373BFC32") } + }; } + } - /// - /// Tests the - /// method to ensure proper operation against primitive types. - /// - /// The value as it would come across the wire. - /// The to test for. - /// The CLR type to convert to. - /// The expected value in the correct type to check against. - /// Contributed by Robert McLaws (@robertmclaws). - [Theory] - [MemberData(nameof(ODataModelBinderConverter_Works_TestData))] - public void Convert_CheckPrimitives(object odataValue, EdmPrimitiveTypeKind edmTypeKind, Type clrType, object expectedResult) - { - var edmTypeReference = new EdmPrimitiveTypeReference(EdmCoreModel.Instance.GetPrimitiveType(edmTypeKind), false); - var value = new ConstantNode(odataValue, odataValue.ToString(), edmTypeReference); - var result = ODataModelBinderConverter.Convert(value, edmTypeReference, clrType, "IsActive", null, null); - Assert.NotNull(result); - Assert.IsType(clrType, result); - Assert.Equal(expectedResult, result); - } + /// + /// Tests the + /// method to ensure proper operation against primitive types. + /// + /// The value as it would come across the wire. + /// The to test for. + /// The CLR type to convert to. + /// The expected value in the correct type to check against. + /// Contributed by Robert McLaws (@robertmclaws). + [Theory] + [MemberData(nameof(ODataModelBinderConverter_Works_TestData))] + public void Convert_CheckPrimitives(object odataValue, EdmPrimitiveTypeKind edmTypeKind, Type clrType, object expectedResult) + { + var edmTypeReference = new EdmPrimitiveTypeReference(EdmCoreModel.Instance.GetPrimitiveType(edmTypeKind), false); + var value = new ConstantNode(odataValue, odataValue.ToString(), edmTypeReference); + var result = ODataModelBinderConverter.Convert(value, edmTypeReference, clrType, "IsActive", null, null); + Assert.NotNull(result); + Assert.IsType(clrType, result); + Assert.Equal(expectedResult, result); + } - /// - /// The set of potential type definition values to test against - /// . - /// - public static TheoryDataSet ODataModelBinderConverter_Works_TypeDefinitionTestData + /// + /// The set of potential type definition values to test against + /// . + /// + public static TheoryDataSet ODataModelBinderConverter_Works_TypeDefinitionTestData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { "100500", EdmPrimitiveTypeKind.String, typeof(BigInteger), new BigInteger(100500) }, - { 300, EdmPrimitiveTypeKind.Int32, typeof(BigInteger), new BigInteger(300) }, - { new BigInteger(222), EdmPrimitiveTypeKind.String, typeof(BigInteger), new BigInteger(222) } - }; - } + { "100500", EdmPrimitiveTypeKind.String, typeof(BigInteger), new BigInteger(100500) }, + { 300, EdmPrimitiveTypeKind.Int32, typeof(BigInteger), new BigInteger(300) }, + { new BigInteger(222), EdmPrimitiveTypeKind.String, typeof(BigInteger), new BigInteger(222) } + }; } + } - /// - /// Tests the - /// method to ensure proper operation against type definition, backed by primitive type. - /// - /// The value as it would come across the wire. - /// The to test for. - /// The CLR type to convert to. - /// The expected value in the correct type to check against. - /// Contributed by Jevgenijs Fjodorovics (@jfshark). - [Theory] - [MemberData(nameof(ODataModelBinderConverter_Works_TypeDefinitionTestData))] - public void Convert_CheckTypeDefinitionPrimitives(object odataValue, EdmPrimitiveTypeKind edmTypeKind, Type clrType, object expectedResult) - { - var edmTypeDefinition = new EdmTypeDefinition(clrType.Namespace, clrType.Name, edmTypeKind); - var edmTypeDefReference = new EdmTypeDefinitionReference(edmTypeDefinition, false); + /// + /// Tests the + /// method to ensure proper operation against type definition, backed by primitive type. + /// + /// The value as it would come across the wire. + /// The to test for. + /// The CLR type to convert to. + /// The expected value in the correct type to check against. + /// Contributed by Jevgenijs Fjodorovics (@jfshark). + [Theory] + [MemberData(nameof(ODataModelBinderConverter_Works_TypeDefinitionTestData))] + public void Convert_CheckTypeDefinitionPrimitives(object odataValue, EdmPrimitiveTypeKind edmTypeKind, Type clrType, object expectedResult) + { + var edmTypeDefinition = new EdmTypeDefinition(clrType.Namespace, clrType.Name, edmTypeKind); + var edmTypeDefReference = new EdmTypeDefinitionReference(edmTypeDefinition, false); - var result = ODataModelBinderConverter.Convert(odataValue, edmTypeDefReference, clrType, "IsActive", null, null); - Assert.NotNull(result); - Assert.IsType(clrType, result); - Assert.Equal(expectedResult, result); - } + var result = ODataModelBinderConverter.Convert(odataValue, edmTypeDefReference, clrType, "IsActive", null, null); + Assert.NotNull(result); + Assert.IsType(clrType, result); + Assert.Equal(expectedResult, result); + } - [Fact] - public void ConvertTo_Converts_InputValue() - { - // Arrange & Act & Assert - Assert.Null(ODataModelBinderConverter.ConvertTo(null, null, null)); + [Fact] + public void ConvertTo_Converts_InputValue() + { + // Arrange & Act & Assert + Assert.Null(ODataModelBinderConverter.ConvertTo(null, null, null)); - // Arrange & Act & Assert - Assert.Null(ODataModelBinderConverter.ConvertTo("null", typeof(int?), null)); + // Arrange & Act & Assert + Assert.Null(ODataModelBinderConverter.ConvertTo("null", typeof(int?), null)); - // Arrange & Act & Assert - Assert.Equal(Color.Red, ODataModelBinderConverter.ConvertTo("NS.Color'Red'", typeof(Color), null)); + // Arrange & Act & Assert + Assert.Equal(Color.Red, ODataModelBinderConverter.ConvertTo("NS.Color'Red'", typeof(Color), null)); - ExceptionAssert.Throws( - () => ODataModelBinderConverter.ConvertTo("NS.Color'Unknown'", typeof(Color), null), - "The binding value 'Unknown' cannot be bound to the enum type 'Color'."); + ExceptionAssert.Throws( + () => ODataModelBinderConverter.ConvertTo("NS.Color'Unknown'", typeof(Color), null), + "The binding value 'Unknown' cannot be bound to the enum type 'Color'."); - Assert.Equal(Color.Red, ODataModelBinderConverter.ConvertTo("NS.Color'Red'", typeof(Color), null)); + Assert.Equal(Color.Red, ODataModelBinderConverter.ConvertTo("NS.Color'Red'", typeof(Color), null)); - // Arrange & Act & Assert - Assert.Equal(new Date(2021, 6, 17), ODataModelBinderConverter.ConvertTo("2021-06-17", typeof(Date?), null)); + // Arrange & Act & Assert + Assert.Equal(new Date(2021, 6, 17), ODataModelBinderConverter.ConvertTo("2021-06-17", typeof(Date?), null)); - // Arrange & Act & Assert - Assert.Equal(42, ODataModelBinderConverter.ConvertTo("42", typeof(int), null)); - } + // Arrange & Act & Assert + Assert.Equal(42, ODataModelBinderConverter.ConvertTo("42", typeof(int), null)); + } - [Fact] - public void ConvertResourceOrResourceSetCanConvert_SingleResource() + [Fact] + public void ConvertResourceOrResourceSetCanConvert_SingleResource() + { + // Arrange + string odataValue = "{\"Id\": 9, \"Name\": \"Sam\"}"; + IEdmModel model = GetEdmModel(); + IEdmEntityType customerType = model.SchemaElements.OfType().First(c => c.Name == "Customer"); + IEdmTypeReference edmTypeReference = new EdmEntityTypeReference(customerType, false); + HttpRequest request = RequestFactory.Create("Get", "http://localhost/", opt => opt.AddRouteComponents("odata", model)); + request.ODataFeature().RoutePrefix = "odata"; + ODataDeserializerContext context = new ODataDeserializerContext { - // Arrange - string odataValue = "{\"Id\": 9, \"Name\": \"Sam\"}"; - IEdmModel model = GetEdmModel(); - IEdmEntityType customerType = model.SchemaElements.OfType().First(c => c.Name == "Customer"); - IEdmTypeReference edmTypeReference = new EdmEntityTypeReference(customerType, false); - HttpRequest request = RequestFactory.Create("Get", "http://localhost/", opt => opt.AddRouteComponents("odata", model)); - request.ODataFeature().RoutePrefix = "odata"; - ODataDeserializerContext context = new ODataDeserializerContext - { - Model = model, - Request = request, - ResourceType = typeof(Customer), - ResourceEdmType = edmTypeReference, - }; + Model = model, + Request = request, + ResourceType = typeof(Customer), + ResourceEdmType = edmTypeReference, + }; - // Act - object value = ODataModelBinderConverter.ConvertResourceOrResourceSet(odataValue, edmTypeReference, context); + // Act + object value = ODataModelBinderConverter.ConvertResourceOrResourceSet(odataValue, edmTypeReference, context); - // Assert - Assert.NotNull(value); + // Assert + Assert.NotNull(value); - Customer customer = Assert.IsType(value); - Assert.Equal(9, customer.Id); - Assert.Equal("Sam", customer.Name); - } + Customer customer = Assert.IsType(value); + Assert.Equal(9, customer.Id); + Assert.Equal("Sam", customer.Name); + } - [Fact] - public void ConvertResourceOrResourceSetCanConvert_ResourceSet() + [Fact] + public void ConvertResourceOrResourceSetCanConvert_ResourceSet() + { + // Arrange + string odataValue = "[{\"Id\": 9, \"Name\": \"Sam\"}, {\"Id\": 18, \"Name\": \"Peter\"}]"; + IEdmModel model = GetEdmModel(); + IEdmEntityType customerType = model.SchemaElements.OfType().First(c => c.Name == "Customer"); + IEdmTypeReference edmTypeReference = new EdmEntityTypeReference(customerType, false); + HttpRequest request = RequestFactory.Create("Get", "http://localhost/", opt => opt.AddRouteComponents("odata", model)); + request.ODataFeature().RoutePrefix = "odata"; + ODataDeserializerContext context = new ODataDeserializerContext { - // Arrange - string odataValue = "[{\"Id\": 9, \"Name\": \"Sam\"}, {\"Id\": 18, \"Name\": \"Peter\"}]"; - IEdmModel model = GetEdmModel(); - IEdmEntityType customerType = model.SchemaElements.OfType().First(c => c.Name == "Customer"); - IEdmTypeReference edmTypeReference = new EdmEntityTypeReference(customerType, false); - HttpRequest request = RequestFactory.Create("Get", "http://localhost/", opt => opt.AddRouteComponents("odata", model)); - request.ODataFeature().RoutePrefix = "odata"; - ODataDeserializerContext context = new ODataDeserializerContext - { - Model = model, - Request = request, - ResourceType = typeof(Customer), - ResourceEdmType = edmTypeReference, - }; + Model = model, + Request = request, + ResourceType = typeof(Customer), + ResourceEdmType = edmTypeReference, + }; - IEdmTypeReference setType = new EdmCollectionTypeReference(new EdmCollectionType(edmTypeReference)); - - // Act - object value = ODataModelBinderConverter.ConvertResourceOrResourceSet(odataValue, setType, context); - - // Assert - Assert.NotNull(value); - - IEnumerable customers = Assert.IsAssignableFrom>(value); - Assert.Collection(customers, - e => - { - Assert.Equal(9, e.Id); - Assert.Equal("Sam", e.Name); - }, - e => - { - Assert.Equal(18, e.Id); - Assert.Equal("Peter", e.Name); - }); - } + IEdmTypeReference setType = new EdmCollectionTypeReference(new EdmCollectionType(edmTypeReference)); - [Fact] - public void ConvertResourceOrResourceSetCanConvert_ResourceId() - { - // Arrange - string odataValue = "{\"@odata.id\":\"http://localhost/odata/Customers(81)\"}"; - IEdmModel model = GetEdmModel(); - IEdmEntityType customerType = model.SchemaElements.OfType().First(c => c.Name == "Customer"); - IEdmTypeReference edmTypeReference = new EdmEntityTypeReference(customerType, false); - HttpRequest request = RequestFactory.Create("Get", "http://localhost/", opt => opt.AddRouteComponents("odata", model)); - request.ODataFeature().RoutePrefix = "odata"; - ODataDeserializerContext context = new ODataDeserializerContext + // Act + object value = ODataModelBinderConverter.ConvertResourceOrResourceSet(odataValue, setType, context); + + // Assert + Assert.NotNull(value); + + IEnumerable customers = Assert.IsAssignableFrom>(value); + Assert.Collection(customers, + e => { - Model = model, - Request = request, - ResourceType = typeof(Customer), - ResourceEdmType = edmTypeReference, - }; + Assert.Equal(9, e.Id); + Assert.Equal("Sam", e.Name); + }, + e => + { + Assert.Equal(18, e.Id); + Assert.Equal("Peter", e.Name); + }); + } + + [Fact] + public void ConvertResourceOrResourceSetCanConvert_ResourceId() + { + // Arrange + string odataValue = "{\"@odata.id\":\"http://localhost/odata/Customers(81)\"}"; + IEdmModel model = GetEdmModel(); + IEdmEntityType customerType = model.SchemaElements.OfType().First(c => c.Name == "Customer"); + IEdmTypeReference edmTypeReference = new EdmEntityTypeReference(customerType, false); + HttpRequest request = RequestFactory.Create("Get", "http://localhost/", opt => opt.AddRouteComponents("odata", model)); + request.ODataFeature().RoutePrefix = "odata"; + ODataDeserializerContext context = new ODataDeserializerContext + { + Model = model, + Request = request, + ResourceType = typeof(Customer), + ResourceEdmType = edmTypeReference, + }; - // Act - object value = ODataModelBinderConverter.ConvertResourceOrResourceSet(odataValue, edmTypeReference, context); + // Act + object value = ODataModelBinderConverter.ConvertResourceOrResourceSet(odataValue, edmTypeReference, context); - // Assert - Assert.NotNull(value); + // Assert + Assert.NotNull(value); - Customer customer = Assert.IsType(value); - Assert.Equal(81, customer.Id); - } + Customer customer = Assert.IsType(value); + Assert.Equal(81, customer.Id); + } - [Fact] - public void ConvertResourceOrResourceSetCanConvert_ResourceWithMulitipleKeys() + [Fact] + public void ConvertResourceOrResourceSetCanConvert_ResourceWithMulitipleKeys() + { + // Arrange + string odataValue = "{\"@odata.id\":\"http://localhost/odata/CustomerWithKeys(First='abc',Last='efg')\"}"; + IEdmModel model = GetEdmModel(); + IEdmEntityType customerType = model.SchemaElements.OfType().First(c => c.Name == "CustomerWithKeys"); + IEdmTypeReference edmTypeReference = new EdmEntityTypeReference(customerType, false); + HttpRequest request = RequestFactory.Create("Get", "http://localhost/", opt => opt.AddRouteComponents("odata", model)); + request.ODataFeature().RoutePrefix = "odata"; + ODataDeserializerContext context = new ODataDeserializerContext { - // Arrange - string odataValue = "{\"@odata.id\":\"http://localhost/odata/CustomerWithKeys(First='abc',Last='efg')\"}"; - IEdmModel model = GetEdmModel(); - IEdmEntityType customerType = model.SchemaElements.OfType().First(c => c.Name == "CustomerWithKeys"); - IEdmTypeReference edmTypeReference = new EdmEntityTypeReference(customerType, false); - HttpRequest request = RequestFactory.Create("Get", "http://localhost/", opt => opt.AddRouteComponents("odata", model)); - request.ODataFeature().RoutePrefix = "odata"; - ODataDeserializerContext context = new ODataDeserializerContext - { - Model = model, - Request = request, - ResourceType = typeof(CustomerWithKeys), - ResourceEdmType = edmTypeReference, - }; + Model = model, + Request = request, + ResourceType = typeof(CustomerWithKeys), + ResourceEdmType = edmTypeReference, + }; - // Act - object value = ODataModelBinderConverter.ConvertResourceOrResourceSet(odataValue, edmTypeReference, context); + // Act + object value = ODataModelBinderConverter.ConvertResourceOrResourceSet(odataValue, edmTypeReference, context); - // Assert - Assert.NotNull(value); + // Assert + Assert.NotNull(value); - CustomerWithKeys customer = Assert.IsType(value); - Assert.Equal("abc", customer.First); - Assert.Equal("efg", customer.Last); - } + CustomerWithKeys customer = Assert.IsType(value); + Assert.Equal("abc", customer.First); + Assert.Equal("efg", customer.Last); + } - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("CustomerWithKeys"); - builder.EntityType().HasKey(c => new { c.First, c.Last }); - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("CustomerWithKeys"); + builder.EntityType().HasKey(c => new { c.First, c.Last }); + return builder.GetEdmModel(); + } - private class Customer - { - public int Id { get; set; } + private class Customer + { + public int Id { get; set; } - public string Name { get; set; } - } + public string Name { get; set; } + } - private class CustomerWithKeys - { - public string First { get; set; } - public string Last { get; set; } - } + private class CustomerWithKeys + { + public string First { get; set; } + public string Last { get; set; } + } - private enum Color - { - Red - } + private enum Color + { + Red } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataModelBinderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataModelBinderTests.cs index c8bfe5b1f..596a24318 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataModelBinderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataModelBinderTests.cs @@ -11,29 +11,28 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ODataModelBinderTests { - public class ODataModelBinderTests + [Fact] + public void BindModelAsyncODataModelBinder_ThrowsArgumentNull_BindingContext() { - [Fact] - public void BindModelAsyncODataModelBinder_ThrowsArgumentNull_BindingContext() - { - // Arrange & Act & Assert - ODataModelBinder binder = new ODataModelBinder(); - ExceptionAssert.ThrowsArgumentNull(() => binder.BindModelAsync(null), "bindingContext"); - } + // Arrange & Act & Assert + ODataModelBinder binder = new ODataModelBinder(); + ExceptionAssert.ThrowsArgumentNull(() => binder.BindModelAsync(null), "bindingContext"); + } - [Fact] - public void BindModelAsyncODataModelBinder_ThrowsArgumentNull_ModelMetadata() - { - // Arrange - ODataModelBinder binder = new ODataModelBinder(); - Mock mock = new Mock(); - mock.Setup(m => m.ModelState).Returns((ModelStateDictionary)null); + [Fact] + public void BindModelAsyncODataModelBinder_ThrowsArgumentNull_ModelMetadata() + { + // Arrange + ODataModelBinder binder = new ODataModelBinder(); + Mock mock = new Mock(); + mock.Setup(m => m.ModelState).Returns((ModelStateDictionary)null); - // Act & Assert - ExceptionAssert.ThrowsArgument(() => binder.BindModelAsync(mock.Object), - "bindingContext", "The binding context cannot have a null ModelMetadata. (Parameter 'bindingContext')"); - } + // Act & Assert + ExceptionAssert.ThrowsArgument(() => binder.BindModelAsync(mock.Object), + "bindingContext", "The binding context cannot have a null ModelMetadata. (Parameter 'bindingContext')"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataOutFormatterTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataOutFormatterTests.cs index a54852ec1..77700edd4 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataOutFormatterTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataOutFormatterTests.cs @@ -23,218 +23,217 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ODataOutputFormatterTests { - public class ODataOutputFormatterTests + [Fact] + public void CtorODataOutputFormatter_ThrowsArgumentNull_IfPayloadsIsNull() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataOutputFormatter(payloadKinds: null), "payloadKinds"); + } + + [Fact] + public void GetSupportedContentTypesODataOutputFormatter_WorksForContentType() + { + // Arrange + ODataOutputFormatter formatter = ODataOutputFormatterFactory.Create().First(); + Assert.NotNull(formatter); // guard + + // Act & Assert + IReadOnlyList contentTypes = formatter.GetSupportedContentTypes("application/json", typeof(string)); + Assert.Equal(36, contentTypes.Count); + + // Act & Assert + formatter.SupportedMediaTypes.Clear(); + ExceptionAssert.DoesNotThrow(() => formatter.GetSupportedContentTypes("application/json", typeof(string))); + } + + [Fact] + public void CanWriteResultODataOutputFormatter_ThrowsArgumentNull_IfContextIsNull() + { + // Arrange & Act + ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); + + // Assert + ExceptionAssert.ThrowsArgumentNull(() => formatter.CanWriteResult(context: null), "context"); + } + + [Fact] + public void CanWriteResultODataOutputFormatter_Throws_IfRequestIsNull() + { + // Arrange & Act + ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); + Mock httpContext = new Mock(); + httpContext.Setup(c => c.Request).Returns((HttpRequest)null); + OutputFormatterWriteContext context = new OutputFormatterWriteContext(httpContext.Object, + (s, e) => null, + typeof(int), + 6); + + // Assert + ExceptionAssert.Throws( + () => formatter.CanWriteResult(context), + "The OData formatter requires an attached request in order to serialize."); + } + + [Fact] + public void CanWriteResultODataOutputFormatter_ReturnsFalseIfNoODataPathSet() + { + // Arrange & Act + OutputFormatterWriteContext context = new OutputFormatterWriteContext( + new DefaultHttpContext(), + (s, e) => null, + typeof(int), + 6); + + ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); + + // Assert + Assert.False(formatter.CanWriteResult(context)); + } + + [Theory] + [InlineData(typeof(Customer), ODataPayloadKind.Resource, true)] + [InlineData(typeof(Customer), ODataPayloadKind.Property, false)] + [InlineData(typeof(Customer[]), ODataPayloadKind.ResourceSet, true)] + [InlineData(typeof(int), ODataPayloadKind.Property, true)] + [InlineData(typeof(IList), ODataPayloadKind.Collection, true)] + [InlineData(typeof(IEdmEntityObject), ODataPayloadKind.Resource, true)] + public void CanWriteResultODataOutputFormatter_ReturnsBooleanValueAsExpected(Type type, ODataPayloadKind payloadKind, bool expected) + { + // Arrange & Act + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + IEdmModel model = builder.GetEdmModel(); + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers"); + EntitySetSegment entitySetSeg = new EntitySetSegment(entitySet); + HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents("odata", model)); + request.ODataFeature().RoutePrefix = "odata"; + request.ODataFeature().Model = model; + request.ODataFeature().Path = new ODataPath(entitySetSeg); + + OutputFormatterWriteContext context = new OutputFormatterWriteContext( + request.HttpContext, + (s, e) => null, + objectType: type, + @object: null); + + ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { payloadKind }); + formatter.SupportedMediaTypes.Add("application/json"); + + // Assert + Assert.Equal(expected, formatter.CanWriteResult(context)); + } + + #region WriteResponseHeaders + [Fact] + public void WriteResponseHeadersODataOutputFormatter_ThrowsArgumentNull_Context() + { + // Arrange & Act + ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); + + // Assert + ExceptionAssert.ThrowsArgumentNull(() => formatter.WriteResponseHeaders(context: null), "context"); + } + + [Fact] + public void WriteResponseHeadersODataOutputFormatter_ThrowsArgumentNull_ObjectType() + { + // Arrange & Act + ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); + OutputFormatterWriteContext context = new OutputFormatterWriteContext( + new DefaultHttpContext(), + (s, e) => null, + null, + null); + + // Assert + ExceptionAssert.ThrowsArgumentNull(() => formatter.WriteResponseHeaders(context), "type"); + } + + [Fact] + public void WriteResponseHeadersODataOutputFormatter_Throws_IfRequestIsNull() + { + // Arrange & Act + ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); + Mock httpContext = new Mock(); + httpContext.Setup(c => c.Request).Returns((HttpRequest)null); + OutputFormatterWriteContext context = new OutputFormatterWriteContext(httpContext.Object, + (s, e) => null, + typeof(int), + 6); + + // Assert + ExceptionAssert.Throws( + () => formatter.WriteResponseHeaders(context), + "The OData formatter requires an attached request in order to serialize."); + } + #endregion + + #region WriteResponseBodyAsync + [Fact] + public void WriteResponseBodyAsyncODataOutputFormatter_ThrowsArgumentNull_Context() + { + // Arrange & Act + ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); + Encoding encoding = Encoding.UTF8; + + // Assert + ExceptionAssert.ThrowsArgumentNull(() => formatter.WriteResponseBodyAsync(context: null, encoding), "context"); + } + + [Fact] + public void WriteResponseBodyAsyncODataOutputFormatter_ThrowsArgumentNull_ObjectType() + { + // Arrange & Act + ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); + OutputFormatterWriteContext context = new OutputFormatterWriteContext( + new DefaultHttpContext(), + (s, e) => null, + null, + null); + + // Assert + ExceptionAssert.ThrowsArgumentNull(() => formatter.WriteResponseBodyAsync(context, Encoding.UTF8), "type"); + } + + [Fact] + public void WriteResponseBodyAsyncODataOutputFormatter_Throws_IfRequestIsNull() + { + // Arrange & Act + ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); + Mock httpContext = new Mock(); + httpContext.Setup(c => c.Request).Returns((HttpRequest)null); + OutputFormatterWriteContext context = new OutputFormatterWriteContext(httpContext.Object, + (s, e) => null, + typeof(int), + 6); + + // Assert + ExceptionAssert.Throws( + () => formatter.WriteResponseBodyAsync(context, Encoding.UTF8), + "The OData formatter requires an attached request in order to serialize."); + } + #endregion + + [Fact] + public void GetDefaultBaseAddressODataOutputFormatter_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => ODataOutputFormatter.GetDefaultBaseAddress(null), "request"); + } + + [Fact] + public void TryGetContentHeaderODataOutputFormatter_ThrowsArgumentNull_Type() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => ODataOutputFormatter.TryGetContentHeader(null, null, out _), "type"); + } + + private class Customer { - [Fact] - public void CtorODataOutputFormatter_ThrowsArgumentNull_IfPayloadsIsNull() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataOutputFormatter(payloadKinds: null), "payloadKinds"); - } - - [Fact] - public void GetSupportedContentTypesODataOutputFormatter_WorksForContentType() - { - // Arrange - ODataOutputFormatter formatter = ODataOutputFormatterFactory.Create().First(); - Assert.NotNull(formatter); // guard - - // Act & Assert - IReadOnlyList contentTypes = formatter.GetSupportedContentTypes("application/json", typeof(string)); - Assert.Equal(36, contentTypes.Count); - - // Act & Assert - formatter.SupportedMediaTypes.Clear(); - ExceptionAssert.DoesNotThrow(() => formatter.GetSupportedContentTypes("application/json", typeof(string))); - } - - [Fact] - public void CanWriteResultODataOutputFormatter_ThrowsArgumentNull_IfContextIsNull() - { - // Arrange & Act - ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); - - // Assert - ExceptionAssert.ThrowsArgumentNull(() => formatter.CanWriteResult(context: null), "context"); - } - - [Fact] - public void CanWriteResultODataOutputFormatter_Throws_IfRequestIsNull() - { - // Arrange & Act - ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); - Mock httpContext = new Mock(); - httpContext.Setup(c => c.Request).Returns((HttpRequest)null); - OutputFormatterWriteContext context = new OutputFormatterWriteContext(httpContext.Object, - (s, e) => null, - typeof(int), - 6); - - // Assert - ExceptionAssert.Throws( - () => formatter.CanWriteResult(context), - "The OData formatter requires an attached request in order to serialize."); - } - - [Fact] - public void CanWriteResultODataOutputFormatter_ReturnsFalseIfNoODataPathSet() - { - // Arrange & Act - OutputFormatterWriteContext context = new OutputFormatterWriteContext( - new DefaultHttpContext(), - (s, e) => null, - typeof(int), - 6); - - ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); - - // Assert - Assert.False(formatter.CanWriteResult(context)); - } - - [Theory] - [InlineData(typeof(Customer), ODataPayloadKind.Resource, true)] - [InlineData(typeof(Customer), ODataPayloadKind.Property, false)] - [InlineData(typeof(Customer[]), ODataPayloadKind.ResourceSet, true)] - [InlineData(typeof(int), ODataPayloadKind.Property, true)] - [InlineData(typeof(IList), ODataPayloadKind.Collection, true)] - [InlineData(typeof(IEdmEntityObject), ODataPayloadKind.Resource, true)] - public void CanWriteResultODataOutputFormatter_ReturnsBooleanValueAsExpected(Type type, ODataPayloadKind payloadKind, bool expected) - { - // Arrange & Act - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - IEdmModel model = builder.GetEdmModel(); - IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers"); - EntitySetSegment entitySetSeg = new EntitySetSegment(entitySet); - HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents("odata", model)); - request.ODataFeature().RoutePrefix = "odata"; - request.ODataFeature().Model = model; - request.ODataFeature().Path = new ODataPath(entitySetSeg); - - OutputFormatterWriteContext context = new OutputFormatterWriteContext( - request.HttpContext, - (s, e) => null, - objectType: type, - @object: null); - - ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { payloadKind }); - formatter.SupportedMediaTypes.Add("application/json"); - - // Assert - Assert.Equal(expected, formatter.CanWriteResult(context)); - } - - #region WriteResponseHeaders - [Fact] - public void WriteResponseHeadersODataOutputFormatter_ThrowsArgumentNull_Context() - { - // Arrange & Act - ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); - - // Assert - ExceptionAssert.ThrowsArgumentNull(() => formatter.WriteResponseHeaders(context: null), "context"); - } - - [Fact] - public void WriteResponseHeadersODataOutputFormatter_ThrowsArgumentNull_ObjectType() - { - // Arrange & Act - ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); - OutputFormatterWriteContext context = new OutputFormatterWriteContext( - new DefaultHttpContext(), - (s, e) => null, - null, - null); - - // Assert - ExceptionAssert.ThrowsArgumentNull(() => formatter.WriteResponseHeaders(context), "type"); - } - - [Fact] - public void WriteResponseHeadersODataOutputFormatter_Throws_IfRequestIsNull() - { - // Arrange & Act - ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); - Mock httpContext = new Mock(); - httpContext.Setup(c => c.Request).Returns((HttpRequest)null); - OutputFormatterWriteContext context = new OutputFormatterWriteContext(httpContext.Object, - (s, e) => null, - typeof(int), - 6); - - // Assert - ExceptionAssert.Throws( - () => formatter.WriteResponseHeaders(context), - "The OData formatter requires an attached request in order to serialize."); - } - #endregion - - #region WriteResponseBodyAsync - [Fact] - public void WriteResponseBodyAsyncODataOutputFormatter_ThrowsArgumentNull_Context() - { - // Arrange & Act - ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); - Encoding encoding = Encoding.UTF8; - - // Assert - ExceptionAssert.ThrowsArgumentNull(() => formatter.WriteResponseBodyAsync(context: null, encoding), "context"); - } - - [Fact] - public void WriteResponseBodyAsyncODataOutputFormatter_ThrowsArgumentNull_ObjectType() - { - // Arrange & Act - ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); - OutputFormatterWriteContext context = new OutputFormatterWriteContext( - new DefaultHttpContext(), - (s, e) => null, - null, - null); - - // Assert - ExceptionAssert.ThrowsArgumentNull(() => formatter.WriteResponseBodyAsync(context, Encoding.UTF8), "type"); - } - - [Fact] - public void WriteResponseBodyAsyncODataOutputFormatter_Throws_IfRequestIsNull() - { - // Arrange & Act - ODataOutputFormatter formatter = new ODataOutputFormatter(new[] { ODataPayloadKind.Resource }); - Mock httpContext = new Mock(); - httpContext.Setup(c => c.Request).Returns((HttpRequest)null); - OutputFormatterWriteContext context = new OutputFormatterWriteContext(httpContext.Object, - (s, e) => null, - typeof(int), - 6); - - // Assert - ExceptionAssert.Throws( - () => formatter.WriteResponseBodyAsync(context, Encoding.UTF8), - "The OData formatter requires an attached request in order to serialize."); - } - #endregion - - [Fact] - public void GetDefaultBaseAddressODataOutputFormatter_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => ODataOutputFormatter.GetDefaultBaseAddress(null), "request"); - } - - [Fact] - public void TryGetContentHeaderODataOutputFormatter_ThrowsArgumentNull_Type() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => ODataOutputFormatter.TryGetContentHeader(null, null, out _), "type"); - } - - private class Customer - { - public int Id { get; set; } - } + public int Id { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataOutputFormatterFactoryTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataOutputFormatterFactoryTests.cs index 4415f06e6..c6761397a 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataOutputFormatterFactoryTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataOutputFormatterFactoryTests.cs @@ -27,25 +27,96 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ODataOutputFormatterFactoryTests { - public class ODataOutputFormatterFactoryTests + private static IEdmModel _edmModel = GetEdmModel(); + private static IList _formatters = ODataOutputFormatterFactory.Create(); + + [Fact] + public void CreateReturnsCorrectOutputFormattersCount() { - private static IEdmModel _edmModel = GetEdmModel(); - private static IList _formatters = ODataOutputFormatterFactory.Create(); + // Arrange & Act & Assert + Assert.Equal(3, _formatters.Count); + } - [Fact] - public void CreateReturnsCorrectOutputFormattersCount() + [Fact] + public void ODataOutputFormattersContainsSupportedMediaTypes() + { + // Arrange + string[] expectedMediaTypes = new string[] { - // Arrange & Act & Assert - Assert.Equal(3, _formatters.Count); - } + "application/json;odata.metadata=minimal;odata.streaming=true", + "application/json;odata.metadata=minimal;odata.streaming=false", + "application/json;odata.metadata=minimal", + "application/json;odata.metadata=full;odata.streaming=true", + "application/json;odata.metadata=full;odata.streaming=false", + "application/json;odata.metadata=full", + "application/json;odata.metadata=none;odata.streaming=true", + "application/json;odata.metadata=none;odata.streaming=false", + "application/json;odata.metadata=none", + "application/json;odata.streaming=true", + "application/json;odata.streaming=false", + "application/json", + "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false", + "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=true", + "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=false", + "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=true", + "application/json;odata.metadata=minimal;IEEE754Compatible=false", + "application/json;odata.metadata=minimal;IEEE754Compatible=true", + "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=false", + "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=true", + "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=false", + "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=true", + "application/json;odata.metadata=full;IEEE754Compatible=false", + "application/json;odata.metadata=full;IEEE754Compatible=true", + "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=false", + "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=true", + "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=true", + "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=false", + "application/json;odata.metadata=none;IEEE754Compatible=false", + "application/json;odata.metadata=none;IEEE754Compatible=true", + "application/json;odata.streaming=true;IEEE754Compatible=false", + "application/json;odata.streaming=true;IEEE754Compatible=true", + "application/json;odata.streaming=false;IEEE754Compatible=false", + "application/json;odata.streaming=false;IEEE754Compatible=true", + "application/json;IEEE754Compatible=false", + "application/json;IEEE754Compatible=true", + "application/xml", + "text/plain", + "application/octet-stream" + }; + + // Act + IEnumerable supportedMediaTypes = _formatters.SelectMany(f => f.SupportedMediaTypes).Distinct(); + + // Assert + Assert.True(expectedMediaTypes.SequenceEqual(supportedMediaTypes)); + } + + [Fact] + public void ODataOutputFormattersContainsSupportedEncodings() + { + // Arrange + IEnumerable expectedEncodings = new Encoding[] + { + new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true), + new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true) + }; + + // Act + IEnumerable supportedEncodings = _formatters.SelectMany(f => f.SupportedEncodings).Distinct(); + + // Assert + Assert.True(expectedEncodings.SequenceEqual(supportedEncodings)); + } - [Fact] - public void ODataOutputFormattersContainsSupportedMediaTypes() + public static TheoryDataSet OutputFormatterSupportedMediaTypesTests + { + get { - // Arrange - string[] expectedMediaTypes = new string[] + string[] applicationJsonMediaTypes = new[] { "application/json;odata.metadata=minimal;odata.streaming=true", "application/json;odata.metadata=minimal;odata.streaming=false", @@ -82,280 +153,208 @@ public void ODataOutputFormattersContainsSupportedMediaTypes() "application/json;odata.streaming=false;IEEE754Compatible=false", "application/json;odata.streaming=false;IEEE754Compatible=true", "application/json;IEEE754Compatible=false", - "application/json;IEEE754Compatible=true", - "application/xml", - "text/plain", - "application/octet-stream" + "application/json;IEEE754Compatible=true" }; - // Act - IEnumerable supportedMediaTypes = _formatters.SelectMany(f => f.SupportedMediaTypes).Distinct(); - - // Assert - Assert.True(expectedMediaTypes.SequenceEqual(supportedMediaTypes)); - } - - [Fact] - public void ODataOutputFormattersContainsSupportedEncodings() - { - // Arrange - IEnumerable expectedEncodings = new Encoding[] + return new TheoryDataSet { - new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true), - new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true) + { typeof(IEnumerable), applicationJsonMediaTypes }, + { typeof(SampleType), applicationJsonMediaTypes }, + { typeof(int), applicationJsonMediaTypes }, + { typeof(IEnumerable), applicationJsonMediaTypes }, + { typeof(Uri), applicationJsonMediaTypes }, + { typeof(IEnumerable), applicationJsonMediaTypes }, + { typeof(ODataServiceDocument), applicationJsonMediaTypes }, + { typeof(IEdmModel), new [] { "application/xml", "application/json" } }, + { typeof(ODataError), applicationJsonMediaTypes } }; + } + } - // Act - IEnumerable supportedEncodings = _formatters.SelectMany(f => f.SupportedEncodings).Distinct(); + [Theory] + [MemberData(nameof(OutputFormatterSupportedMediaTypesTests))] + public void ODataOutputFormattersForWriteTypeReturnsSupportedMediaTypes(Type type, string[] expectedMediaTypes) + { + // Arrange + HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents(_edmModel)); + request.Configure("", _edmModel, new ODataPath()); + var metadataDocumentFormatters = _formatters.Where(f => CanWriteType(f, type, request)); - // Assert - Assert.True(expectedEncodings.SequenceEqual(supportedEncodings)); - } + // Act + IEnumerable supportedMediaTypes = metadataDocumentFormatters.SelectMany(f => f.SupportedMediaTypes).Distinct(); - public static TheoryDataSet OutputFormatterSupportedMediaTypesTests - { - get - { - string[] applicationJsonMediaTypes = new[] - { - "application/json;odata.metadata=minimal;odata.streaming=true", - "application/json;odata.metadata=minimal;odata.streaming=false", - "application/json;odata.metadata=minimal", - "application/json;odata.metadata=full;odata.streaming=true", - "application/json;odata.metadata=full;odata.streaming=false", - "application/json;odata.metadata=full", - "application/json;odata.metadata=none;odata.streaming=true", - "application/json;odata.metadata=none;odata.streaming=false", - "application/json;odata.metadata=none", - "application/json;odata.streaming=true", - "application/json;odata.streaming=false", - "application/json", - "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false", - "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=true", - "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=false", - "application/json;odata.metadata=minimal;odata.streaming=false;IEEE754Compatible=true", - "application/json;odata.metadata=minimal;IEEE754Compatible=false", - "application/json;odata.metadata=minimal;IEEE754Compatible=true", - "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=false", - "application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=true", - "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=false", - "application/json;odata.metadata=full;odata.streaming=false;IEEE754Compatible=true", - "application/json;odata.metadata=full;IEEE754Compatible=false", - "application/json;odata.metadata=full;IEEE754Compatible=true", - "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=false", - "application/json;odata.metadata=none;odata.streaming=true;IEEE754Compatible=true", - "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=true", - "application/json;odata.metadata=none;odata.streaming=false;IEEE754Compatible=false", - "application/json;odata.metadata=none;IEEE754Compatible=false", - "application/json;odata.metadata=none;IEEE754Compatible=true", - "application/json;odata.streaming=true;IEEE754Compatible=false", - "application/json;odata.streaming=true;IEEE754Compatible=true", - "application/json;odata.streaming=false;IEEE754Compatible=false", - "application/json;odata.streaming=false;IEEE754Compatible=true", - "application/json;IEEE754Compatible=false", - "application/json;IEEE754Compatible=true" - }; - - return new TheoryDataSet - { - { typeof(IEnumerable), applicationJsonMediaTypes }, - { typeof(SampleType), applicationJsonMediaTypes }, - { typeof(int), applicationJsonMediaTypes }, - { typeof(IEnumerable), applicationJsonMediaTypes }, - { typeof(Uri), applicationJsonMediaTypes }, - { typeof(IEnumerable), applicationJsonMediaTypes }, - { typeof(ODataServiceDocument), applicationJsonMediaTypes }, - { typeof(IEdmModel), new [] { "application/xml", "application/json" } }, - { typeof(ODataError), applicationJsonMediaTypes } - }; - } - } + // Assert + Assert.True(expectedMediaTypes.SequenceEqual(supportedMediaTypes)); + } - [Theory] - [MemberData(nameof(OutputFormatterSupportedMediaTypesTests))] - public void ODataOutputFormattersForWriteTypeReturnsSupportedMediaTypes(Type type, string[] expectedMediaTypes) + public static TheoryDataSet OutputFormatterContentTypeTests + { + get { - // Arrange - HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents(_edmModel)); - request.Configure("", _edmModel, new ODataPath()); - var metadataDocumentFormatters = _formatters.Where(f => CanWriteType(f, type, request)); - - // Act - IEnumerable supportedMediaTypes = metadataDocumentFormatters.SelectMany(f => f.SupportedMediaTypes).Distinct(); + MediaTypeHeaderValue jsonMedia = MediaTypeHeaderValue.Parse("application/json;odata.metadata=minimal;odata.streaming=true"); + MediaTypeHeaderValue xmlMedia = MediaTypeHeaderValue.Parse("application/xml"); - // Assert - Assert.True(expectedMediaTypes.SequenceEqual(supportedMediaTypes)); + return new TheoryDataSet + { + { typeof(IEnumerable), jsonMedia }, + { typeof(SampleType), jsonMedia }, + { typeof(int), jsonMedia }, + { typeof(ODataServiceDocument), jsonMedia }, + { typeof(IEdmModel), xmlMedia } + }; } + } - public static TheoryDataSet OutputFormatterContentTypeTests - { - get - { - MediaTypeHeaderValue jsonMedia = MediaTypeHeaderValue.Parse("application/json;odata.metadata=minimal;odata.streaming=true"); - MediaTypeHeaderValue xmlMedia = MediaTypeHeaderValue.Parse("application/xml"); + [Theory] + [MemberData(nameof(OutputFormatterContentTypeTests))] + public void ODataOutputFormatters_Feed_DefaultContentType(Type type, MediaTypeHeaderValue expect) + { + // Arrange & Act + MediaTypeHeaderValue mediaType = GetDefaultContentType(_edmModel, type); - return new TheoryDataSet - { - { typeof(IEnumerable), jsonMedia }, - { typeof(SampleType), jsonMedia }, - { typeof(int), jsonMedia }, - { typeof(ODataServiceDocument), jsonMedia }, - { typeof(IEdmModel), xmlMedia } - }; - } - } + // Assert + Assert.Equal(expect, mediaType); + } - [Theory] - [MemberData(nameof(OutputFormatterContentTypeTests))] - public void ODataOutputFormatters_Feed_DefaultContentType(Type type, MediaTypeHeaderValue expect) + public static TheoryDataSet OutputFormatterDollarFormatContentTypeTests + { + get { - // Arrange & Act - MediaTypeHeaderValue mediaType = GetDefaultContentType(_edmModel, type); + Type[] testTypes = new[] + { + typeof(IEnumerable), + typeof(SampleType), + typeof(int), + typeof(Uri), + typeof(IEnumerable), + typeof(ODataServiceDocument), + typeof(ODataError) + }; - // Assert - Assert.Equal(expect, mediaType); - } + KeyValuePair[] dollarFormats = new KeyValuePair[] + { + new KeyValuePair("json", "application/json"), + new KeyValuePair("application%2fjson%3bodata.metadata%3dminimal%3bodata.streaming%3dtrue", "application/json;odata.metadata=minimal;odata.streaming=true"), + new KeyValuePair("application%2fjson%3bodata.metadata%3dminimal%3bodata.streaming%3dfalse", "application/json;odata.metadata=minimal;odata.streaming=false"), + new KeyValuePair("application%2fjson%3bodata.metadata%3dminimal", "application/json;odata.metadata=minimal"), + new KeyValuePair("application%2fjson%3bodata.metadata%3dfull%3bodata.streaming%3dtrue", "application/json;odata.metadata=full;odata.streaming=true"), + new KeyValuePair("application%2fjson%3bodata.metadata%3dfull%3bodata.streaming%3dfalse", "application/json;odata.metadata=full;odata.streaming=false"), + new KeyValuePair("application%2fjson%3bodata.metadata%3dfull", "application/json;odata.metadata=full"), + new KeyValuePair("application%2fjson%3bodata.metadata%3dnone%3bodata.streaming%3dtrue", "application/json;odata.metadata=none;odata.streaming=true"), + new KeyValuePair("application%2fjson%3bodata.metadata%3dnone%3bodata.streaming%3dfalse", "application/json;odata.metadata=none;odata.streaming=false"), + new KeyValuePair("application%2fjson%3bodata.metadata%3dnone", "application/json;odata.metadata=none"), + new KeyValuePair("application%2fjson%3bodata.streaming%3dtrue", "application/json;odata.streaming=true"), + new KeyValuePair("application%2fjson%3bodata.streaming%3dfalse", "application/json;odata.streaming=false"), + new KeyValuePair("application%2fjson", "application/json"), + new KeyValuePair("application%2fjson%3bodata.streaming%3dtrue%3bodata.metadata%3dminimal", "application/json;odata.streaming=true;odata.metadata=minimal") + }; - public static TheoryDataSet OutputFormatterDollarFormatContentTypeTests - { - get + TheoryDataSet data = new TheoryDataSet(); + foreach (Type type in testTypes) { - Type[] testTypes = new[] + foreach (KeyValuePair dollarFormat in dollarFormats) { - typeof(IEnumerable), - typeof(SampleType), - typeof(int), - typeof(Uri), - typeof(IEnumerable), - typeof(ODataServiceDocument), - typeof(ODataError) - }; - - KeyValuePair[] dollarFormats = new KeyValuePair[] - { - new KeyValuePair("json", "application/json"), - new KeyValuePair("application%2fjson%3bodata.metadata%3dminimal%3bodata.streaming%3dtrue", "application/json;odata.metadata=minimal;odata.streaming=true"), - new KeyValuePair("application%2fjson%3bodata.metadata%3dminimal%3bodata.streaming%3dfalse", "application/json;odata.metadata=minimal;odata.streaming=false"), - new KeyValuePair("application%2fjson%3bodata.metadata%3dminimal", "application/json;odata.metadata=minimal"), - new KeyValuePair("application%2fjson%3bodata.metadata%3dfull%3bodata.streaming%3dtrue", "application/json;odata.metadata=full;odata.streaming=true"), - new KeyValuePair("application%2fjson%3bodata.metadata%3dfull%3bodata.streaming%3dfalse", "application/json;odata.metadata=full;odata.streaming=false"), - new KeyValuePair("application%2fjson%3bodata.metadata%3dfull", "application/json;odata.metadata=full"), - new KeyValuePair("application%2fjson%3bodata.metadata%3dnone%3bodata.streaming%3dtrue", "application/json;odata.metadata=none;odata.streaming=true"), - new KeyValuePair("application%2fjson%3bodata.metadata%3dnone%3bodata.streaming%3dfalse", "application/json;odata.metadata=none;odata.streaming=false"), - new KeyValuePair("application%2fjson%3bodata.metadata%3dnone", "application/json;odata.metadata=none"), - new KeyValuePair("application%2fjson%3bodata.streaming%3dtrue", "application/json;odata.streaming=true"), - new KeyValuePair("application%2fjson%3bodata.streaming%3dfalse", "application/json;odata.streaming=false"), - new KeyValuePair("application%2fjson", "application/json"), - new KeyValuePair("application%2fjson%3bodata.streaming%3dtrue%3bodata.metadata%3dminimal", "application/json;odata.streaming=true;odata.metadata=minimal") - }; - - TheoryDataSet data = new TheoryDataSet(); - foreach (Type type in testTypes) - { - foreach (KeyValuePair dollarFormat in dollarFormats) - { - data.Add(type, dollarFormat.Key, dollarFormat.Value); - } + data.Add(type, dollarFormat.Key, dollarFormat.Value); } - - return data; } + + return data; } + } - public static TheoryDataSet OutputFormatterDollarFormatForModelContentTypeTests + public static TheoryDataSet OutputFormatterDollarFormatForModelContentTypeTests + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { typeof(IEdmModel), "xml", "application/xml" }, - { typeof(IEdmModel), "application%2fxml", "application/xml" }, - }; + { typeof(IEdmModel), "xml", "application/xml" }, + { typeof(IEdmModel), "application%2fxml", "application/xml" }, + }; - } } + } - [Theory] - [MemberData(nameof(OutputFormatterDollarFormatContentTypeTests))] - [MemberData(nameof(OutputFormatterDollarFormatForModelContentTypeTests))] - public void TestCreate_DollarFormat_Error(Type type, string dollarFormatValue, string expectedMediaType) - { - // Arrange & Act - MediaTypeHeaderValue mediaType = GetContentTypeFromQueryString(_edmModel, type, dollarFormatValue); + [Theory] + [MemberData(nameof(OutputFormatterDollarFormatContentTypeTests))] + [MemberData(nameof(OutputFormatterDollarFormatForModelContentTypeTests))] + public void TestCreate_DollarFormat_Error(Type type, string dollarFormatValue, string expectedMediaType) + { + // Arrange & Act + MediaTypeHeaderValue mediaType = GetContentTypeFromQueryString(_edmModel, type, dollarFormatValue); - // Assert - Assert.Equal(MediaTypeHeaderValue.Parse(expectedMediaType), mediaType); - } + // Assert + Assert.Equal(MediaTypeHeaderValue.Parse(expectedMediaType), mediaType); + } - private MediaTypeHeaderValue GetDefaultContentType(IEdmModel model, Type type) - { - return GetContentTypeFromQueryString(model, type, null); - } + private MediaTypeHeaderValue GetDefaultContentType(IEdmModel model, Type type) + { + return GetContentTypeFromQueryString(model, type, null); + } - private MediaTypeHeaderValue GetContentTypeFromQueryString(IEdmModel model, Type type, string dollarFormat) + private MediaTypeHeaderValue GetContentTypeFromQueryString(IEdmModel model, Type type, string dollarFormat) + { + Action setupAction = opt => opt.AddRouteComponents("odata", model); + ODataPath path = new ODataPath(); + HttpRequest request = string.IsNullOrEmpty(dollarFormat) + ? RequestFactory.Create("Get", "http://any", setupAction) + : RequestFactory.Create("Get", "http://any/?$format=" + dollarFormat, setupAction); + request.Configure("odata", model, path); + + var context = new OutputFormatterWriteContext( + request.HttpContext, + CreateWriter, + type, + new MemoryStream()); + + foreach (var formatter in _formatters) { - Action setupAction = opt => opt.AddRouteComponents("odata", model); - ODataPath path = new ODataPath(); - HttpRequest request = string.IsNullOrEmpty(dollarFormat) - ? RequestFactory.Create("Get", "http://any", setupAction) - : RequestFactory.Create("Get", "http://any/?$format=" + dollarFormat, setupAction); - request.Configure("odata", model, path); - - var context = new OutputFormatterWriteContext( - request.HttpContext, - CreateWriter, - type, - new MemoryStream()); - - foreach (var formatter in _formatters) + context.ContentType = new StringSegment(); + context.ContentTypeIsServerDefined = false; + + if (formatter.CanWriteResult(context)) { - context.ContentType = new StringSegment(); - context.ContentTypeIsServerDefined = false; + MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse(context.ContentType.ToString()); - if (formatter.CanWriteResult(context)) + // We don't care what the charset is for these tests. + if (mediaType.Parameters.Where(p => p.Name == "charset").Any()) { - MediaTypeHeaderValue mediaType = MediaTypeHeaderValue.Parse(context.ContentType.ToString()); - - // We don't care what the charset is for these tests. - if (mediaType.Parameters.Where(p => p.Name == "charset").Any()) - { - mediaType.Parameters.Remove(mediaType.Parameters.Single(p => p.Name == "charset")); - } - - return mediaType; + mediaType.Parameters.Remove(mediaType.Parameters.Single(p => p.Name == "charset")); } - } - return null; + return mediaType; + } } - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder model = new ODataConventionModelBuilder(); - model.EntityType(); - return model.GetEdmModel(); - } + return null; + } - private static bool CanWriteType(ODataOutputFormatter formatter, Type type, HttpRequest request) - { - var context = new OutputFormatterWriteContext( - request.HttpContext, - CreateWriter, - objectType: type, - @object: null); + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder model = new ODataConventionModelBuilder(); + model.EntityType(); + return model.GetEdmModel(); + } - return formatter.CanWriteResult(context); - } + private static bool CanWriteType(ODataOutputFormatter formatter, Type type, HttpRequest request) + { + var context = new OutputFormatterWriteContext( + request.HttpContext, + CreateWriter, + objectType: type, + @object: null); - private static TextWriter CreateWriter(Stream stream, Encoding encoding) - { - const int DefaultBufferSize = 16 * 1024; - return new HttpResponseStreamWriter(stream, encoding, DefaultBufferSize); - } + return formatter.CanWriteResult(context); + } - private class SampleType - { - public int Id { get; set; } - } + private static TextWriter CreateWriter(Stream stream, Encoding encoding) + { + const int DefaultBufferSize = 16 * 1024; + return new HttpResponseStreamWriter(stream, encoding, DefaultBufferSize); + } + + private class SampleType + { + public int Id { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataOutputFormatterHelperTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataOutputFormatterHelperTests.cs index 40851f3ee..31827a429 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataOutputFormatterHelperTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataOutputFormatterHelperTests.cs @@ -18,68 +18,67 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ODataOutputFormatterHelperTests { - public class ODataOutputFormatterHelperTests + [Fact] + public void BuildSerializerContext_ThrowsArgumentNull_Request() { - [Fact] - public void BuildSerializerContext_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => ODataOutputFormatterHelper.BuildSerializerContext(null), "request"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => ODataOutputFormatterHelper.BuildSerializerContext(null), "request"); + } - [Fact] - public async Task WriteToStreamAsync_ThrowsArgumentNull_Model() - { - // Arrange & Act & Assert - await ExceptionAssert.ThrowsAsync( - () => ODataOutputFormatterHelper.WriteToStreamAsync(null, null, null, ODataVersion.V4, null, null, null, null, null), - "The request must have an associated EDM model. Consider registering Edm model calling AddOData()."); - } + [Fact] + public async Task WriteToStreamAsync_ThrowsArgumentNull_Model() + { + // Arrange & Act & Assert + await ExceptionAssert.ThrowsAsync( + () => ODataOutputFormatterHelper.WriteToStreamAsync(null, null, null, ODataVersion.V4, null, null, null, null, null), + "The request must have an associated EDM model. Consider registering Edm model calling AddOData()."); + } - [Fact] - public void GetSerializer_ThrowsSerializationException_NonEdmType() - { - // Arrange - Mock mock = new Mock(); - mock.Setup(s => s.GetEdmType()).Returns((IEdmTypeReference)null); + [Fact] + public void GetSerializer_ThrowsSerializationException_NonEdmType() + { + // Arrange + Mock mock = new Mock(); + mock.Setup(s => s.GetEdmType()).Returns((IEdmTypeReference)null); - // Act & Assert - ExceptionAssert.Throws(() => ODataOutputFormatterHelper.GetSerializer(type: null, mock.Object, null, null), - "The EDM type of the object of type 'Castle.Proxies.IEdmObjectProxy' is null. The EDM type of an 'IEdmObject' cannot be null."); - } + // Act & Assert + ExceptionAssert.Throws(() => ODataOutputFormatterHelper.GetSerializer(type: null, mock.Object, null, null), + "The EDM type of the object of type 'Castle.Proxies.IEdmObjectProxy' is null. The EDM type of an 'IEdmObject' cannot be null."); + } - [Fact] - public void GetSerializer_ThrowsSerializationException_TypeCannotBeSerialized() - { - // Arrange - Type intType = typeof(int); - HttpRequest request = new DefaultHttpContext().Request; - Mock provider = new Mock(); - provider.Setup(s => s.GetODataPayloadSerializer(intType, request)).Returns((IODataSerializer)null); + [Fact] + public void GetSerializer_ThrowsSerializationException_TypeCannotBeSerialized() + { + // Arrange + Type intType = typeof(int); + HttpRequest request = new DefaultHttpContext().Request; + Mock provider = new Mock(); + provider.Setup(s => s.GetODataPayloadSerializer(intType, request)).Returns((IODataSerializer)null); - // Act & Assert - ExceptionAssert.Throws( - () => ODataOutputFormatterHelper.GetSerializer(intType, null, request, provider.Object), - "'Int32' cannot be serialized using the OData output formatter."); - } + // Act & Assert + ExceptionAssert.Throws( + () => ODataOutputFormatterHelper.GetSerializer(intType, null, request, provider.Object), + "'Int32' cannot be serialized using the OData output formatter."); + } - [Fact] - public void GetSerializer_ThrowsSerializationException_TypeCannotBeSerialized_NonClrType() - { - // Arrange - IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); - Mock mock = new Mock(); - mock.Setup(s => s.GetEdmType()).Returns(intType); + [Fact] + public void GetSerializer_ThrowsSerializationException_TypeCannotBeSerialized_NonClrType() + { + // Arrange + IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); + Mock mock = new Mock(); + mock.Setup(s => s.GetEdmType()).Returns(intType); - Mock provider = new Mock(); - provider.Setup(s => s.GetEdmTypeSerializer(intType)).Returns((IODataEdmTypeSerializer)null); + Mock provider = new Mock(); + provider.Setup(s => s.GetEdmTypeSerializer(intType)).Returns((IODataEdmTypeSerializer)null); - // Act & Assert - ExceptionAssert.Throws( - () => ODataOutputFormatterHelper.GetSerializer(null, mock.Object, null, provider.Object), - "'[Edm.Int32 Nullable=False]' cannot be serialized using the OData output formatter."); - } + // Act & Assert + ExceptionAssert.Throws( + () => ODataOutputFormatterHelper.GetSerializer(null, mock.Object, null, provider.Object), + "'[Edm.Int32 Nullable=False]' cannot be serialized using the OData output formatter."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataParameterValueTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataParameterValueTests.cs index f80458cdf..fc7c62920 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataParameterValueTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataParameterValueTests.cs @@ -9,15 +9,14 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ODataParameterValueTests { - public class ODataParameterValueTests + [Fact] + public void Ctor_ThrowsArgumentNull_ParameterType() { - [Fact] - public void Ctor_ThrowsArgumentNull_ParameterType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataParameterValue(null, null), "paramType"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataParameterValue(null, null), "paramType"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataTestUtil.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataTestUtil.cs index 7daee779c..03b0153df 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataTestUtil.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataTestUtil.cs @@ -14,53 +14,52 @@ using Microsoft.OData.Edm; using Moq; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +internal static class ODataTestUtil { - internal static class ODataTestUtil + public static ODataMessageWriter GetMockODataMessageWriter() { - public static ODataMessageWriter GetMockODataMessageWriter() - { - MockODataRequestMessage requestMessage = new MockODataRequestMessage(); - return new ODataMessageWriter(requestMessage); - } + MockODataRequestMessage requestMessage = new MockODataRequestMessage(); + return new ODataMessageWriter(requestMessage); + } - public static ODataMessageReader GetMockODataMessageReader() - { - MockODataRequestMessage requestMessage = new MockODataRequestMessage(); - return new ODataMessageReader(requestMessage); - } + public static ODataMessageReader GetMockODataMessageReader() + { + MockODataRequestMessage requestMessage = new MockODataRequestMessage(); + return new ODataMessageReader(requestMessage); + } - public static IODataSerializerProvider GetMockODataSerializerProvider(ODataEdmTypeSerializer serializer) - { - Mock serializerProvider = new Mock(); - serializerProvider.Setup(sp => sp.GetEdmTypeSerializer(It.IsAny())).Returns(serializer); - return serializerProvider.Object; - } + public static IODataSerializerProvider GetMockODataSerializerProvider(ODataEdmTypeSerializer serializer) + { + Mock serializerProvider = new Mock(); + serializerProvider.Setup(sp => sp.GetEdmTypeSerializer(It.IsAny())).Returns(serializer); + return serializerProvider.Object; + } - internal static ODataMessageReader GetODataMessageReader(IODataRequestMessageAsync oDataRequestMessage, IEdmModel edmModel) - { - return new ODataMessageReader(oDataRequestMessage, new ODataMessageReaderSettings(), edmModel); - } + internal static ODataMessageReader GetODataMessageReader(IODataRequestMessageAsync oDataRequestMessage, IEdmModel edmModel) + { + return new ODataMessageReader(oDataRequestMessage, new ODataMessageReaderSettings(), edmModel); + } - internal static IODataRequestMessageAsync GetODataMessage(this HttpRequest request, string content) - { - // While NetCore does not use this for AspNet, it can be used here to create - // an HttpRequestODataMessage, which is a Test type that implments IODataRequestMessage - // wrapped around an HttpRequestMessage. + internal static IODataRequestMessageAsync GetODataMessage(this HttpRequest request, string content) + { + // While NetCore does not use this for AspNet, it can be used here to create + // an HttpRequestODataMessage, which is a Test type that implments IODataRequestMessage + // wrapped around an HttpRequestMessage. - byte[] contentBytes = Encoding.UTF8.GetBytes(content); - request.Body = new MemoryStream(contentBytes); - request.ContentType = "application/json"; - request.ContentLength = contentBytes.Length; - request.Headers.Append("OData-Version", "4.0"); - request.Headers.Append("Accept", "application/json;odata.metadata=full"); + byte[] contentBytes = Encoding.UTF8.GetBytes(content); + request.Body = new MemoryStream(contentBytes); + request.ContentType = "application/json"; + request.ContentLength = contentBytes.Length; + request.Headers.Append("OData-Version", "4.0"); + request.Headers.Append("Accept", "application/json;odata.metadata=full"); - //MediaTypeWithQualityHeaderValue mediaType = new MediaTypeWithQualityHeaderValue("application/json"); - //mediaType.Parameters.Add(new NameValueHeaderValue("odata.metadata", "full")); - //request.Headers.Accept.Add(mediaType); - //request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + //MediaTypeWithQualityHeaderValue mediaType = new MediaTypeWithQualityHeaderValue("application/json"); + //mediaType.Parameters.Add(new NameValueHeaderValue("odata.metadata", "full")); + //request.Headers.Accept.Add(mediaType); + //request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - return new HttpRequestODataMessage(request); - } + return new HttpRequestODataMessage(request); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataUntypedActionParametersTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataUntypedActionParametersTests.cs index c2bc4886a..baa102c7e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataUntypedActionParametersTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ODataUntypedActionParametersTests.cs @@ -9,15 +9,14 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ODataUntypedActionParametersTests { - public class ODataUntypedActionParametersTests + [Fact] + public void Ctor_ThrowsArgumentNull_Action() { - [Fact] - public void Ctor_ThrowsArgumentNull_Action() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataUntypedActionParameters(null), "action"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataUntypedActionParameters(null), "action"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ResourceContextTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ResourceContextTests.cs index 737332809..e52abf277 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/ResourceContextTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/ResourceContextTests.cs @@ -18,358 +18,357 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter +namespace Microsoft.AspNetCore.OData.Tests.Formatter; + +public class ResourceContextTest { - public class ResourceContextTest + private ODataSerializerContext _serializerContext = new ODataSerializerContext { Model = EdmCoreModel.Instance }; + private IEdmEntityType _entityType = new EdmEntityType("NS", "Name"); + private object _entityInstance = new object(); + private ResourceContext _context = new ResourceContext(); + + [Fact] + public void EmptyCtor_InitializesProperty_SerializerContext() { - private ODataSerializerContext _serializerContext = new ODataSerializerContext { Model = EdmCoreModel.Instance }; - private IEdmEntityType _entityType = new EdmEntityType("NS", "Name"); - private object _entityInstance = new object(); - private ResourceContext _context = new ResourceContext(); + // Arrange + var context = new ResourceContext(); - [Fact] - public void EmptyCtor_InitializesProperty_SerializerContext() - { - // Arrange - var context = new ResourceContext(); + // Act & Assert + Assert.NotNull(context.SerializerContext); + } - // Act & Assert - Assert.NotNull(context.SerializerContext); - } + [Fact] + public void Ctor_ThrowsArgumentNull_SerializerContext() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new ResourceContext(null, null, null), "serializerContext"); + } - [Fact] - public void Ctor_ThrowsArgumentNull_SerializerContext() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new ResourceContext(null, null, null), "serializerContext"); - } + [Fact] + public void Property_ResourceInstance_RoundTrips() + { + ReflectionAssert.Property(_context, (c) => c.ResourceInstance, null, allowNull: true, roundTripTestValue: _entityInstance); + } - [Fact] - public void Property_ResourceInstance_RoundTrips() - { - ReflectionAssert.Property(_context, (c) => c.ResourceInstance, null, allowNull: true, roundTripTestValue: _entityInstance); - } + [Fact] + public void Property_EdmModel_RoundTrips() + { + ReflectionAssert.Property(_context, (c) => c.EdmModel, null, allowNull: true, roundTripTestValue: EdmCoreModel.Instance); + } - [Fact] - public void Property_EdmModel_RoundTrips() - { - ReflectionAssert.Property(_context, (c) => c.EdmModel, null, allowNull: true, roundTripTestValue: EdmCoreModel.Instance); - } + [Fact] + public void Property_NavigationSource_RoundTrips() + { + ReflectionAssert.Property(_context, (c) => c.NavigationSource, null, allowNull: true, roundTripTestValue: new Mock().Object); + } - [Fact] - public void Property_NavigationSource_RoundTrips() - { - ReflectionAssert.Property(_context, (c) => c.NavigationSource, null, allowNull: true, roundTripTestValue: new Mock().Object); - } + [Fact] + public void Property_StructuredType_RoundTrips() + { + ReflectionAssert.Property(_context, (c) => c.StructuredType, null, allowNull: true, roundTripTestValue: _entityType); + } - [Fact] - public void Property_StructuredType_RoundTrips() - { - ReflectionAssert.Property(_context, (c) => c.StructuredType, null, allowNull: true, roundTripTestValue: _entityType); - } + [Fact] + public void Property_Request_RoundTrips() + { + ReflectionAssert.Property(_context, (c) => c.Request, null, allowNull: true, roundTripTestValue: RequestFactory.Create()); + } - [Fact] - public void Property_Request_RoundTrips() - { - ReflectionAssert.Property(_context, (c) => c.Request, null, allowNull: true, roundTripTestValue: RequestFactory.Create()); - } + [Fact] + public void Property_SerializerContext_RoundTrips() + { + ReflectionAssert.Property(_context, (c) => c.SerializerContext, _context.SerializerContext, allowNull: true, roundTripTestValue: new ODataSerializerContext()); + } - [Fact] - public void Property_SerializerContext_RoundTrips() - { - ReflectionAssert.Property(_context, (c) => c.SerializerContext, _context.SerializerContext, allowNull: true, roundTripTestValue: new ODataSerializerContext()); - } + [Fact] + public void Property_SkipExpensiveAvailabilityChecks_RoundTrips() + { + ReflectionAssert.BooleanProperty(_context, (c) => c.SkipExpensiveAvailabilityChecks, false); + } - [Fact] - public void Property_SkipExpensiveAvailabilityChecks_RoundTrips() - { - ReflectionAssert.BooleanProperty(_context, (c) => c.SkipExpensiveAvailabilityChecks, false); - } + [Fact] + public void GetPropertyValue_ThrowsInvalidOperation_IfPropertyIsNotFound() + { + // Arrange + IEdmEntityTypeReference entityType = new EdmEntityTypeReference(new EdmEntityType("NS", "Name"), isNullable: false); + Mock edmObject = new Mock(); + edmObject.Setup(o => o.GetEdmType()).Returns(entityType); + ResourceContext instanceContext = new ResourceContext(_serializerContext, entityType, edmObject.Object); + + // Act & Assert + ExceptionAssert.Throws( + () => instanceContext.GetPropertyValue("NotPresentProperty"), + "The EDM instance of type '[NS.Name Nullable=False]' is missing the property 'NotPresentProperty'."); + } - [Fact] - public void GetPropertyValue_ThrowsInvalidOperation_IfPropertyIsNotFound() - { - // Arrange - IEdmEntityTypeReference entityType = new EdmEntityTypeReference(new EdmEntityType("NS", "Name"), isNullable: false); - Mock edmObject = new Mock(); - edmObject.Setup(o => o.GetEdmType()).Returns(entityType); - ResourceContext instanceContext = new ResourceContext(_serializerContext, entityType, edmObject.Object); - - // Act & Assert - ExceptionAssert.Throws( - () => instanceContext.GetPropertyValue("NotPresentProperty"), - "The EDM instance of type '[NS.Name Nullable=False]' is missing the property 'NotPresentProperty'."); - } + [Fact] + public void GetPropertyValue_ThrowsInvalidOperation_IfEdmObjectIsNull() + { + // Arrange + ResourceContext instanceContext = new ResourceContext(); - [Fact] - public void GetPropertyValue_ThrowsInvalidOperation_IfEdmObjectIsNull() - { - // Arrange - ResourceContext instanceContext = new ResourceContext(); + // Act & Assert + ExceptionAssert.Throws( + () => instanceContext.GetPropertyValue("SomeProperty"), + "The property 'EdmObject' of ResourceContext cannot be null."); + } - // Act & Assert - ExceptionAssert.Throws( - () => instanceContext.GetPropertyValue("SomeProperty"), - "The property 'EdmObject' of ResourceContext cannot be null."); - } + [Fact] + public void GetPropertyValue_ThrowsInvalidOperation_IfEdmObjectGetEdmTypeReturnsNull() + { + // Arrange + object outObject; + Mock mock = new Mock(); + mock.Setup(o => o.TryGetPropertyValue(It.IsAny(), out outObject)).Returns(false).Verifiable(); + mock.Setup(o => o.GetEdmType()).Returns(null).Verifiable(); + ResourceContext context = new ResourceContext(); + context.EdmObject = mock.Object; + + // Act & Assert + ExceptionAssert.Throws(() => context.GetPropertyValue("SomeProperty"), + "The EDM type of an 'IEdmObject' cannot be null.", partialMatch: true); + mock.Verify(); + } - [Fact] - public void GetPropertyValue_ThrowsInvalidOperation_IfEdmObjectGetEdmTypeReturnsNull() - { - // Arrange - object outObject; - Mock mock = new Mock(); - mock.Setup(o => o.TryGetPropertyValue(It.IsAny(), out outObject)).Returns(false).Verifiable(); - mock.Setup(o => o.GetEdmType()).Returns(null).Verifiable(); - ResourceContext context = new ResourceContext(); - context.EdmObject = mock.Object; - - // Act & Assert - ExceptionAssert.Throws(() => context.GetPropertyValue("SomeProperty"), - "The EDM type of an 'IEdmObject' cannot be null.", partialMatch: true); - mock.Verify(); - } + [Fact] + public void Property_ResourceInstance_CanBeBuiltFromIEdmObject_ForEntity() + { + // Arrange + EdmEntityType edmType = new EdmEntityType("NS", "Name"); + edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); + EdmModel model = new EdmModel(); + model.AddElement(edmType); + model.SetAnnotationValue(edmType, new ClrTypeAnnotation(typeof(TestEntity))); + Mock edmObject = new Mock(); + object propertyValue = 42; + edmObject.Setup(e => e.TryGetPropertyValue("Property", out propertyValue)).Returns(true); + edmObject.Setup(e => e.GetEdmType()).Returns(new EdmEntityTypeReference(edmType, isNullable: false)); + + ResourceContext entityContext = new ResourceContext { EdmModel = model, EdmObject = edmObject.Object, StructuredType = edmType }; + + // Act + object resource = entityContext.ResourceInstance; + + // Assert + TestEntity testEntity = Assert.IsType(resource); + Assert.Equal(42, testEntity.Property); + } - [Fact] - public void Property_ResourceInstance_CanBeBuiltFromIEdmObject_ForEntity() - { - // Arrange - EdmEntityType edmType = new EdmEntityType("NS", "Name"); - edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); - EdmModel model = new EdmModel(); - model.AddElement(edmType); - model.SetAnnotationValue(edmType, new ClrTypeAnnotation(typeof(TestEntity))); - Mock edmObject = new Mock(); - object propertyValue = 42; - edmObject.Setup(e => e.TryGetPropertyValue("Property", out propertyValue)).Returns(true); - edmObject.Setup(e => e.GetEdmType()).Returns(new EdmEntityTypeReference(edmType, isNullable: false)); - - ResourceContext entityContext = new ResourceContext { EdmModel = model, EdmObject = edmObject.Object, StructuredType = edmType }; - - // Act - object resource = entityContext.ResourceInstance; - - // Assert - TestEntity testEntity = Assert.IsType(resource); - Assert.Equal(42, testEntity.Property); - } + [Fact] + public void Property_ResourceInstance_CanBeBuiltFromIEdmObject_ForComplex() + { + // Arrange + EdmComplexType edmType = new EdmComplexType("NS", "Name"); + edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); + EdmModel model = new EdmModel(); + model.AddElement(edmType); + model.SetAnnotationValue(edmType, new ClrTypeAnnotation(typeof(TestEntity))); + Mock edmObject = new Mock(); + object propertyValue = 42; + edmObject.Setup(e => e.TryGetPropertyValue("Property", out propertyValue)).Returns(true); + edmObject.Setup(e => e.GetEdmType()).Returns(new EdmComplexTypeReference(edmType, isNullable: false)); + + ResourceContext entityContext = new ResourceContext { EdmModel = model, EdmObject = edmObject.Object, StructuredType = edmType }; + + // Act + object resource = entityContext.ResourceInstance; + + // Assert + TestEntity testEntity = Assert.IsType(resource); + Assert.Equal(42, testEntity.Property); + } - [Fact] - public void Property_ResourceInstance_CanBeBuiltFromIEdmObject_ForComplex() - { - // Arrange - EdmComplexType edmType = new EdmComplexType("NS", "Name"); - edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); - EdmModel model = new EdmModel(); - model.AddElement(edmType); - model.SetAnnotationValue(edmType, new ClrTypeAnnotation(typeof(TestEntity))); - Mock edmObject = new Mock(); - object propertyValue = 42; - edmObject.Setup(e => e.TryGetPropertyValue("Property", out propertyValue)).Returns(true); - edmObject.Setup(e => e.GetEdmType()).Returns(new EdmComplexTypeReference(edmType, isNullable: false)); - - ResourceContext entityContext = new ResourceContext { EdmModel = model, EdmObject = edmObject.Object, StructuredType = edmType }; - - // Act - object resource = entityContext.ResourceInstance; - - // Assert - TestEntity testEntity = Assert.IsType(resource); - Assert.Equal(42, testEntity.Property); - } + [Fact] + public void Property_ResourceInstance_EdmObjectHasCollectionProperty_ForEntityCollection() + { + // Arrange + EdmEntityType edmType = new EdmEntityType("NS", "Name"); + edmType.AddStructuralProperty( + "CollectionProperty", + new EdmCollectionTypeReference( + new EdmCollectionType(EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false)))); + EdmModel model = new EdmModel(); + model.AddElement(edmType); + model.SetAnnotationValue(edmType, new ClrTypeAnnotation(typeof(TestEntity))); + Mock edmObject = new Mock(); + object propertyValue = new List { 42 }; + edmObject.Setup(e => e.TryGetPropertyValue("CollectionProperty", out propertyValue)).Returns(true); + edmObject.Setup(e => e.GetEdmType()).Returns(new EdmEntityTypeReference(edmType, isNullable: false)); + + ResourceContext entityContext = new ResourceContext { EdmModel = model, EdmObject = edmObject.Object, StructuredType = edmType }; + + // Act + object resource = entityContext.ResourceInstance; + + // Assert + TestEntity testEntity = Assert.IsType(resource); + Assert.Equal(new[] { 42 }, testEntity.CollectionProperty); + } - [Fact] - public void Property_ResourceInstance_EdmObjectHasCollectionProperty_ForEntityCollection() - { - // Arrange - EdmEntityType edmType = new EdmEntityType("NS", "Name"); - edmType.AddStructuralProperty( - "CollectionProperty", - new EdmCollectionTypeReference( - new EdmCollectionType(EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false)))); - EdmModel model = new EdmModel(); - model.AddElement(edmType); - model.SetAnnotationValue(edmType, new ClrTypeAnnotation(typeof(TestEntity))); - Mock edmObject = new Mock(); - object propertyValue = new List { 42 }; - edmObject.Setup(e => e.TryGetPropertyValue("CollectionProperty", out propertyValue)).Returns(true); - edmObject.Setup(e => e.GetEdmType()).Returns(new EdmEntityTypeReference(edmType, isNullable: false)); - - ResourceContext entityContext = new ResourceContext { EdmModel = model, EdmObject = edmObject.Object, StructuredType = edmType }; - - // Act - object resource = entityContext.ResourceInstance; - - // Assert - TestEntity testEntity = Assert.IsType(resource); - Assert.Equal(new[] { 42 }, testEntity.CollectionProperty); - } + [Fact] + public void Property_ResourceInstance_EdmObjectHasCollectionProperty_ForComplexCollection() + { + // Arrange + EdmComplexType edmType = new EdmComplexType("NS", "Name"); + edmType.AddStructuralProperty( + "CollectionProperty", + new EdmCollectionTypeReference( + new EdmCollectionType(EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false)))); + EdmModel model = new EdmModel(); + model.AddElement(edmType); + model.SetAnnotationValue(edmType, new ClrTypeAnnotation(typeof(TestEntity))); + Mock edmObject = new Mock(); + object propertyValue = new List { 42 }; + edmObject.Setup(e => e.TryGetPropertyValue("CollectionProperty", out propertyValue)).Returns(true); + edmObject.Setup(e => e.GetEdmType()).Returns(new EdmComplexTypeReference(edmType, isNullable: false)); + + ResourceContext entityContext = new ResourceContext { EdmModel = model, EdmObject = edmObject.Object, StructuredType = edmType }; + + // Act + object resource = entityContext.ResourceInstance; + + // Assert + TestEntity testEntity = Assert.IsType(resource); + Assert.Equal(new[] { 42 }, testEntity.CollectionProperty); + } - [Fact] - public void Property_ResourceInstance_EdmObjectHasCollectionProperty_ForComplexCollection() - { - // Arrange - EdmComplexType edmType = new EdmComplexType("NS", "Name"); - edmType.AddStructuralProperty( - "CollectionProperty", - new EdmCollectionTypeReference( - new EdmCollectionType(EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false)))); - EdmModel model = new EdmModel(); - model.AddElement(edmType); - model.SetAnnotationValue(edmType, new ClrTypeAnnotation(typeof(TestEntity))); - Mock edmObject = new Mock(); - object propertyValue = new List { 42 }; - edmObject.Setup(e => e.TryGetPropertyValue("CollectionProperty", out propertyValue)).Returns(true); - edmObject.Setup(e => e.GetEdmType()).Returns(new EdmComplexTypeReference(edmType, isNullable: false)); - - ResourceContext entityContext = new ResourceContext { EdmModel = model, EdmObject = edmObject.Object, StructuredType = edmType }; - - // Act - object resource = entityContext.ResourceInstance; - - // Assert - TestEntity testEntity = Assert.IsType(resource); - Assert.Equal(new[] { 42 }, testEntity.CollectionProperty); - } + [Fact] + public void Property_ResourceInstance_HandlesModelClrNameDifferences() + { + // Arrange + const string clrPropertyName = "Property"; + const string modelPropertyName = "DifferentProperty"; + + EdmComplexType edmType = new EdmComplexType("NS", "Name"); + EdmStructuralProperty edmProperty = edmType.AddStructuralProperty(modelPropertyName, EdmPrimitiveTypeKind.Int32); + EdmModel model = new EdmModel(); + model.AddElement(edmType); + model.SetAnnotationValue(edmType, new ClrTypeAnnotation(typeof(TestEntity))); + model.SetAnnotationValue(edmProperty, new ClrPropertyInfoAnnotation(typeof(TestEntity).GetProperty(clrPropertyName))); + Mock edmObject = new Mock(); + object propertyValue = 42; + edmObject.Setup(e => e.TryGetPropertyValue(modelPropertyName, out propertyValue)).Returns(true); + edmObject.Setup(e => e.GetEdmType()).Returns(new EdmComplexTypeReference(edmType, isNullable: false)); + + ResourceContext entityContext = new ResourceContext { EdmModel = model, EdmObject = edmObject.Object, StructuredType = edmType }; + + // Act + object resource = entityContext.ResourceInstance; + + // Assert + TestEntity testEntity = Assert.IsType(resource); + Assert.Equal(42, testEntity.Property); + } - [Fact] - public void Property_ResourceInstance_HandlesModelClrNameDifferences() - { - // Arrange - const string clrPropertyName = "Property"; - const string modelPropertyName = "DifferentProperty"; - - EdmComplexType edmType = new EdmComplexType("NS", "Name"); - EdmStructuralProperty edmProperty = edmType.AddStructuralProperty(modelPropertyName, EdmPrimitiveTypeKind.Int32); - EdmModel model = new EdmModel(); - model.AddElement(edmType); - model.SetAnnotationValue(edmType, new ClrTypeAnnotation(typeof(TestEntity))); - model.SetAnnotationValue(edmProperty, new ClrPropertyInfoAnnotation(typeof(TestEntity).GetProperty(clrPropertyName))); - Mock edmObject = new Mock(); - object propertyValue = 42; - edmObject.Setup(e => e.TryGetPropertyValue(modelPropertyName, out propertyValue)).Returns(true); - edmObject.Setup(e => e.GetEdmType()).Returns(new EdmComplexTypeReference(edmType, isNullable: false)); - - ResourceContext entityContext = new ResourceContext { EdmModel = model, EdmObject = edmObject.Object, StructuredType = edmType }; - - // Act - object resource = entityContext.ResourceInstance; - - // Assert - TestEntity testEntity = Assert.IsType(resource); - Assert.Equal(42, testEntity.Property); - } + [Fact] + public void Property_ResourceInstance_ThrowsInvalidOp_ResourceTypeDoesNotHaveAMapping() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "Name"); + EdmModel model = new EdmModel(); + IEdmEntityObject instance = new Mock().Object; + ResourceContext entityContext = new ResourceContext { StructuredType = entityType, EdmModel = model, EdmObject = instance }; + + // Act & Assert + ExceptionAssert.Throws( + () => entityContext.ResourceInstance, "The provided mapping does not contain a resource for the resource type 'NS.Name'."); + } - [Fact] - public void Property_ResourceInstance_ThrowsInvalidOp_ResourceTypeDoesNotHaveAMapping() + [Fact] + public void Property_ResourceInstance_CanBeBuiltWithSelectExpandWrapperProperties() + { + // Arrange + EdmComplexType edmType = new EdmComplexType("NS", "Name"); + var edmTypeRef = new EdmComplexTypeReference(edmType, isNullable: false); + edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); + edmType.AddStructuralProperty("SubEntity1", edmTypeRef); + edmType.AddStructuralProperty("SubEntity2", edmTypeRef); + EdmModel model = new EdmModel(); + model.AddElement(edmType); + model.SetAnnotationValue(edmType, new ClrTypeAnnotation(typeof(TestSubEntity))); + Mock edmObject = new Mock(); + object propertyValue = 42; + object selectExpandWrapper = new SelectExpandWrapper(); + object subEntity2 = new TestSubEntity { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "Name"); - EdmModel model = new EdmModel(); - IEdmEntityObject instance = new Mock().Object; - ResourceContext entityContext = new ResourceContext { StructuredType = entityType, EdmModel = model, EdmObject = instance }; - - // Act & Assert - ExceptionAssert.Throws( - () => entityContext.ResourceInstance, "The provided mapping does not contain a resource for the resource type 'NS.Name'."); - } + Property = 33 + }; + edmObject.Setup(e => e.TryGetPropertyValue("Property", out propertyValue)).Returns(true); + edmObject.Setup(e => e.TryGetPropertyValue("SubEntity1", out selectExpandWrapper)).Returns(true); + edmObject.Setup(e => e.TryGetPropertyValue("SubEntity2", out subEntity2)).Returns(true); + edmObject.Setup(e => e.GetEdmType()).Returns(edmTypeRef); + + ResourceContext entityContext = new ResourceContext { EdmModel = model, EdmObject = edmObject.Object, StructuredType = edmType }; + + // Act + object resource = entityContext.ResourceInstance; + + // Assert + TestSubEntity testEntity = Assert.IsType(resource); + Assert.Equal(42, testEntity.Property); + Assert.Null(testEntity.SubEntity1); + Assert.NotNull(testEntity.SubEntity2); + Assert.Equal(33, testEntity.SubEntity2.Property); + } - [Fact] - public void Property_ResourceInstance_CanBeBuiltWithSelectExpandWrapperProperties() - { - // Arrange - EdmComplexType edmType = new EdmComplexType("NS", "Name"); - var edmTypeRef = new EdmComplexTypeReference(edmType, isNullable: false); - edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); - edmType.AddStructuralProperty("SubEntity1", edmTypeRef); - edmType.AddStructuralProperty("SubEntity2", edmTypeRef); - EdmModel model = new EdmModel(); - model.AddElement(edmType); - model.SetAnnotationValue(edmType, new ClrTypeAnnotation(typeof(TestSubEntity))); - Mock edmObject = new Mock(); - object propertyValue = 42; - object selectExpandWrapper = new SelectExpandWrapper(); - object subEntity2 = new TestSubEntity - { - Property = 33 - }; - edmObject.Setup(e => e.TryGetPropertyValue("Property", out propertyValue)).Returns(true); - edmObject.Setup(e => e.TryGetPropertyValue("SubEntity1", out selectExpandWrapper)).Returns(true); - edmObject.Setup(e => e.TryGetPropertyValue("SubEntity2", out subEntity2)).Returns(true); - edmObject.Setup(e => e.GetEdmType()).Returns(edmTypeRef); - - ResourceContext entityContext = new ResourceContext { EdmModel = model, EdmObject = edmObject.Object, StructuredType = edmType }; - - // Act - object resource = entityContext.ResourceInstance; - - // Assert - TestSubEntity testEntity = Assert.IsType(resource); - Assert.Equal(42, testEntity.Property); - Assert.Null(testEntity.SubEntity1); - Assert.NotNull(testEntity.SubEntity2); - Assert.Equal(33, testEntity.SubEntity2.Property); - } + [Fact] + public void Property_ResourceInstance_ReturnsNullWhenEdmObjectIsNull() + { + // Arrange + ResourceContext entityContext = new ResourceContext { EdmObject = null }; - [Fact] - public void Property_ResourceInstance_ReturnsNullWhenEdmObjectIsNull() - { - // Arrange - ResourceContext entityContext = new ResourceContext { EdmObject = null }; + // Act & Assert + Assert.Null(entityContext.ResourceInstance); + } - // Act & Assert - Assert.Null(entityContext.ResourceInstance); - } + [Fact] + public void Property_ResourceInstance_ReturnsEdmStructuredObjectInstance() + { + // Arrange + object instance = new object(); + IEdmEntityTypeReference entityType = new Mock().Object; + IEdmModel edmModel = new Mock().Object; + ResourceContext entityContext = + new ResourceContext { EdmObject = new TypedEdmEntityObject(instance, entityType, edmModel) }; + + // Act & Assert + Assert.Same(instance, entityContext.ResourceInstance); + } - [Fact] - public void Property_ResourceInstance_ReturnsEdmStructuredObjectInstance() - { - // Arrange - object instance = new object(); - IEdmEntityTypeReference entityType = new Mock().Object; - IEdmModel edmModel = new Mock().Object; - ResourceContext entityContext = - new ResourceContext { EdmObject = new TypedEdmEntityObject(instance, entityType, edmModel) }; - - // Act & Assert - Assert.Same(instance, entityContext.ResourceInstance); - } + /// + /// A simple class with a property and collection property. + /// + private class TestEntity + { + public int Property { get; set; } - /// - /// A simple class with a property and collection property. - /// - private class TestEntity - { - public int Property { get; set; } + public int[] CollectionProperty { get; set; } + } - public int[] CollectionProperty { get; set; } - } + private class TestSubEntity + { + public int Property { get; set; } - private class TestSubEntity - { - public int Property { get; set; } + public TestSubEntity SubEntity1 { get; set; } - public TestSubEntity SubEntity1 { get; set; } + public TestSubEntity SubEntity2 { get; set; } + } - public TestSubEntity SubEntity2 { get; set; } + /// + /// An instance of IEdmEntityObject with no EdmType. + /// + private class NullEdmType : IEdmEntityObject + { + public IEdmTypeReference GetEdmType() + { + return null; } - /// - /// An instance of IEdmEntityObject with no EdmType. - /// - private class NullEdmType : IEdmEntityObject + public bool TryGetPropertyValue(string propertyName, out object value) { - public IEdmTypeReference GetEdmType() - { - return null; - } - - public bool TryGetPropertyValue(string propertyName, out object value) - { - value = null; - return false; - } + value = null; + return false; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ComplexTypeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ComplexTypeTests.cs index 2367b2652..6dbf09608 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ComplexTypeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ComplexTypeTests.cs @@ -17,52 +17,51 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ComplexTypeTest { - public class ComplexTypeTest + [Fact] + public async Task ComplexTypeSerializesAsOData() { - [Fact] - public async Task ComplexTypeSerializesAsOData() - { - // Arrange - string routeName = "OData"; - IEdmModel model = GetSampleModel(); - var request = RequestFactory.Create("Get", "http://localhost/property", opt => opt.AddRouteComponents(routeName, model)); - var addressComplexType = model.SchemaElements.OfType().Single(d => d.Name.Equals("Address")); - request.ODataFeature().Path = new ODataPath(new ValueSegment(addressComplexType)); - request.ODataFeature().Model = model; - request.ODataFeature().RoutePrefix = routeName; + // Arrange + string routeName = "OData"; + IEdmModel model = GetSampleModel(); + var request = RequestFactory.Create("Get", "http://localhost/property", opt => opt.AddRouteComponents(routeName, model)); + var addressComplexType = model.SchemaElements.OfType().Single(d => d.Name.Equals("Address")); + request.ODataFeature().Path = new ODataPath(new ValueSegment(addressComplexType)); + request.ODataFeature().Model = model; + request.ODataFeature().RoutePrefix = routeName; - var payload = new ODataPayloadKind[] { ODataPayloadKind.Resource }; - var formatter = ODataFormatterHelpers.GetOutputFormatter(payload); + var payload = new ODataPayloadKind[] { ODataPayloadKind.Resource }; + var formatter = ODataFormatterHelpers.GetOutputFormatter(payload); - Address address = new Address - { - Street = "abc", - City = "efg", - State = "opq", - ZipCode = "98029", - CountryOrRegion = "Mars" - }; + Address address = new Address + { + Street = "abc", + City = "efg", + State = "opq", + ZipCode = "98029", + CountryOrRegion = "Mars" + }; - var content = ODataFormatterHelpers.GetContent(address, formatter, ODataMediaTypes.ApplicationJsonODataMinimalMetadata); + var content = ODataFormatterHelpers.GetContent(address, formatter, ODataMediaTypes.ApplicationJsonODataMinimalMetadata); - // Act & Assert - string actual = await ODataFormatterHelpers.GetContentResult(content, request); + // Act & Assert + string actual = await ODataFormatterHelpers.GetContentResult(content, request); - Assert.Equal("{\"@odata.context\":\"http://localhost/OData/$metadata#Microsoft.AspNetCore.OData.Tests.Formatter.Models.Address\"," + - "\"Street\":\"abc\"," + - "\"City\":\"efg\"," + - "\"State\":\"opq\"," + - "\"ZipCode\":\"98029\"," + - "\"CountryOrRegion\":\"Mars\"}", actual); - } + Assert.Equal("{\"@odata.context\":\"http://localhost/OData/$metadata#Microsoft.AspNetCore.OData.Tests.Formatter.Models.Address\"," + + "\"Street\":\"abc\"," + + "\"City\":\"efg\"," + + "\"State\":\"opq\"," + + "\"ZipCode\":\"98029\"," + + "\"CountryOrRegion\":\"Mars\"}", actual); + } - private static IEdmModel GetSampleModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.ComplexType
(); - return builder.GetEdmModel(); - } + private static IEdmModel GetSampleModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.ComplexType
(); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/EdmDirectValueAnnotationsManagerExtensions.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/EdmDirectValueAnnotationsManagerExtensions.cs index 27c8d05fe..1b5258440 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/EdmDirectValueAnnotationsManagerExtensions.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/EdmDirectValueAnnotationsManagerExtensions.cs @@ -11,65 +11,64 @@ using Microsoft.OData.Edm; using Microsoft.OData.Edm.Vocabularies; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +internal static class EdmDirectValueAnnotationsManagerExtensions { - internal static class EdmDirectValueAnnotationsManagerExtensions + public static void SetOperationLinkBuilder(this IEdmDirectValueAnnotationsManager manager, IEdmElement element, + OperationLinkBuilder value) { - public static void SetOperationLinkBuilder(this IEdmDirectValueAnnotationsManager manager, IEdmElement element, - OperationLinkBuilder value) - { - SetCoreAnnotation(manager, element, value); - } + SetCoreAnnotation(manager, element, value); + } - public static void SetIsAlwaysBindable(this IEdmDirectValueAnnotationsManager manager, IEdmOperation operation) - { - SetODataAnnotation(manager, operation, "IsAlwaysBindable", "true"); - } + public static void SetIsAlwaysBindable(this IEdmDirectValueAnnotationsManager manager, IEdmOperation operation) + { + SetODataAnnotation(manager, operation, "IsAlwaysBindable", "true"); + } + + private static void SetCoreAnnotation(this IEdmDirectValueAnnotationsManager manager, IEdmElement element, + T value) + { + Contract.Assert(manager != null); + manager.SetAnnotationValue(element, "http://schemas.microsoft.com/ado/2011/04/edm/internal", + GetCoreAnnotationName(typeof(T)), value); + } - private static void SetCoreAnnotation(this IEdmDirectValueAnnotationsManager manager, IEdmElement element, - T value) + private static void SetODataAnnotation(this IEdmDirectValueAnnotationsManager manager, IEdmElement element, + string name, string value) + { + Contract.Assert(manager != null); + manager.SetAnnotationValue(element, "http://docs.oasis-open.org/odata/ns/metadata", name, + new FakeEdmStringValue(value)); + } + + private static string GetCoreAnnotationName(Type t) + { + return t.FullName.Replace('.', '_'); + } + + private class FakeEdmStringValue : IEdmStringValue + { + private readonly string _value; + + public FakeEdmStringValue(string value) { - Contract.Assert(manager != null); - manager.SetAnnotationValue(element, "http://schemas.microsoft.com/ado/2011/04/edm/internal", - GetCoreAnnotationName(typeof(T)), value); + _value = value; } - private static void SetODataAnnotation(this IEdmDirectValueAnnotationsManager manager, IEdmElement element, - string name, string value) + public string Value { - Contract.Assert(manager != null); - manager.SetAnnotationValue(element, "http://docs.oasis-open.org/odata/ns/metadata", name, - new FakeEdmStringValue(value)); + get { return _value; } } - private static string GetCoreAnnotationName(Type t) + public IEdmTypeReference Type { - return t.FullName.Replace('.', '_'); + get { throw new NotImplementedException(); } } - private class FakeEdmStringValue : IEdmStringValue + public EdmValueKind ValueKind { - private readonly string _value; - - public FakeEdmStringValue(string value) - { - _value = value; - } - - public string Value - { - get { return _value; } - } - - public IEdmTypeReference Type - { - get { throw new NotImplementedException(); } - } - - public EdmValueKind ValueKind - { - get { throw new NotImplementedException(); } - } + get { throw new NotImplementedException(); } } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/EntityTypeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/EntityTypeTests.cs index 9147f4b50..e6c0db2b8 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/EntityTypeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/EntityTypeTests.cs @@ -16,55 +16,54 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class EntityTypeTests { - public class EntityTypeTests - { - private IEdmModel _model = GetSampleModel(); + private IEdmModel _model = GetSampleModel(); - [Fact] - public async Task EntityTypeSerializesAsODataEntry() - { - // Arrange - const string routeName = "OData"; - IEdmEntitySet entitySet = _model.EntityContainer.FindEntitySet("employees"); - ODataPath path = new ODataPath(new EntitySetSegment(entitySet)); + [Fact] + public async Task EntityTypeSerializesAsODataEntry() + { + // Arrange + const string routeName = "OData"; + IEdmEntitySet entitySet = _model.EntityContainer.FindEntitySet("employees"); + ODataPath path = new ODataPath(new EntitySetSegment(entitySet)); - var request = RequestFactory.Create("Get", "http://localhost/property", opt => opt.AddRouteComponents(routeName, _model)); - request.ODataFeature().Model = _model; - request.ODataFeature().Path = path; - request.ODataFeature().RoutePrefix = routeName; + var request = RequestFactory.Create("Get", "http://localhost/property", opt => opt.AddRouteComponents(routeName, _model)); + request.ODataFeature().Model = _model; + request.ODataFeature().Path = path; + request.ODataFeature().RoutePrefix = routeName; - var payload = new ODataPayloadKind[] { ODataPayloadKind.Resource }; - var formatter = ODataFormatterHelpers.GetOutputFormatter(payload); - Employee employee = new Employee - { - EmployeeID = 8, - Birthday = new System.DateTimeOffset(2020, 9, 10, 1, 2, 3, System.TimeSpan.Zero), - EmployeeName = "Ssa", - HomeAddress = null - }; - var content = ODataFormatterHelpers.GetContent(employee, formatter, ODataMediaTypes.ApplicationJsonODataMinimalMetadata); + var payload = new ODataPayloadKind[] { ODataPayloadKind.Resource }; + var formatter = ODataFormatterHelpers.GetOutputFormatter(payload); + Employee employee = new Employee + { + EmployeeID = 8, + Birthday = new System.DateTimeOffset(2020, 9, 10, 1, 2, 3, System.TimeSpan.Zero), + EmployeeName = "Ssa", + HomeAddress = null + }; + var content = ODataFormatterHelpers.GetContent(employee, formatter, ODataMediaTypes.ApplicationJsonODataMinimalMetadata); - // Act & Assert - string actual = await ODataFormatterHelpers.GetContentResult(content, request); + // Act & Assert + string actual = await ODataFormatterHelpers.GetContentResult(content, request); - Assert.Equal("{\"@odata.context\":\"http://localhost/OData/$metadata#employees/$entity\"," + - "\"EmployeeID\":8," + - "\"EmployeeName\":\"Ssa\"," + - "\"BaseSalary\":0," + - "\"Birthday\":\"2020-09-10T01:02:03Z\"," + - "\"WorkCompanyId\":0," + - "\"HomeAddress\":null" + - "}", actual); - } + Assert.Equal("{\"@odata.context\":\"http://localhost/OData/$metadata#employees/$entity\"," + + "\"EmployeeID\":8," + + "\"EmployeeName\":\"Ssa\"," + + "\"BaseSalary\":0," + + "\"Birthday\":\"2020-09-10T01:02:03Z\"," + + "\"WorkCompanyId\":0," + + "\"HomeAddress\":null" + + "}", actual); + } - private static IEdmModel GetSampleModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("employees"); - builder.EntitySet("workitems"); - return builder.GetEdmModel(); - } + private static IEdmModel GetSampleModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("employees"); + builder.EntitySet("workitems"); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataCollectionSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataCollectionSerializerTests.cs index 86089e5d2..7aafd4fd9 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataCollectionSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataCollectionSerializerTests.cs @@ -21,346 +21,345 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataCollectionSerializerTests { - public class ODataCollectionSerializerTests + private IEdmPrimitiveTypeReference _edmIntType; + private IEdmCollectionTypeReference _collectionType; + + public ODataCollectionSerializerTests() { - private IEdmPrimitiveTypeReference _edmIntType; - private IEdmCollectionTypeReference _collectionType; + _edmIntType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); + _collectionType = new EdmCollectionTypeReference(new EdmCollectionType(_edmIntType)); + } - public ODataCollectionSerializerTests() - { - _edmIntType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); - _collectionType = new EdmCollectionTypeReference(new EdmCollectionType(_edmIntType)); - } + [Fact] + public void Ctor_ThrowsArgumentNull_SerializerProvider() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataCollectionSerializer(serializerProvider: null), "serializerProvider"); + } - [Fact] - public void Ctor_ThrowsArgumentNull_SerializerProvider() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataCollectionSerializer(serializerProvider: null), "serializerProvider"); - } + [Fact] + public async Task WriteObjectAsync_Throws_ArgumentNull_MessageWriter() + { + // Arrange + IODataSerializerProvider provider = new Mock().Object; + ODataCollectionSerializer serializer = new ODataCollectionSerializer(provider); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: null, type: typeof(int[]), messageWriter: null, writeContext: null), + "messageWriter"); + } - [Fact] - public async Task WriteObjectAsync_Throws_ArgumentNull_MessageWriter() - { - // Arrange - IODataSerializerProvider provider = new Mock().Object; - ODataCollectionSerializer serializer = new ODataCollectionSerializer(provider); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: null, type: typeof(int[]), messageWriter: null, writeContext: null), - "messageWriter"); - } + [Fact] + public async Task WriteObjectAsync_Throws_ArgumentNull_WriteContext() + { + // Arrange + ODataMessageWriter messageWriter = ODataTestUtil.GetMockODataMessageWriter(); + IODataSerializerProvider provider = new Mock().Object; + ODataCollectionSerializer serializer = new ODataCollectionSerializer(provider); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: null, type: typeof(int[]), messageWriter, writeContext: null), + "writeContext"); + } - [Fact] - public async Task WriteObjectAsync_Throws_ArgumentNull_WriteContext() + [Fact] + public async Task WriteObjectAsync_WritesValueReturnedFrom_CreateODataCollectionValue() + { + // Arrange + MemoryStream stream = new MemoryStream(); + IODataResponseMessage message = new ODataMessageWrapper(stream); + ODataMessageWriterSettings settings = new ODataMessageWriterSettings() { - // Arrange - ODataMessageWriter messageWriter = ODataTestUtil.GetMockODataMessageWriter(); - IODataSerializerProvider provider = new Mock().Object; - ODataCollectionSerializer serializer = new ODataCollectionSerializer(provider); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: null, type: typeof(int[]), messageWriter, writeContext: null), - "writeContext"); - } + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } + }; + + settings.SetContentType(ODataFormat.Json); + + ODataMessageWriter messageWriter = new ODataMessageWriter(message, settings); + IODataSerializerProvider provider = new Mock().Object; + Mock serializer = new Mock(provider); + ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "CollectionName", Model = EdmCoreModel.Instance }; + IEnumerable enumerable = new int[0]; + ODataCollectionValue collectionValue = new ODataCollectionValue { TypeName = "NS.Name", Items = new object[] { 0, 1, 2 } }; + + serializer.CallBase = true; + serializer + .Setup(s => s.CreateODataCollectionValue(enumerable, It.Is(e => e.Definition == this._edmIntType.Definition), writeContext)) + .Returns(collectionValue).Verifiable(); + + // Act + await serializer.Object.WriteObjectAsync(enumerable, typeof(int[]), messageWriter, writeContext); + + // Assert + serializer.Verify(); + stream.Seek(0, SeekOrigin.Begin); + string result = new StreamReader(stream).ReadToEnd(); + Assert.Equal("{\"@odata.context\":\"http://any/$metadata#Collection(Edm.Int32)\",\"value\":[0,1,2]}", result); + } - [Fact] - public async Task WriteObjectAsync_WritesValueReturnedFrom_CreateODataCollectionValue() - { - // Arrange - MemoryStream stream = new MemoryStream(); - IODataResponseMessage message = new ODataMessageWrapper(stream); - ODataMessageWriterSettings settings = new ODataMessageWriterSettings() - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } - }; - - settings.SetContentType(ODataFormat.Json); - - ODataMessageWriter messageWriter = new ODataMessageWriter(message, settings); - IODataSerializerProvider provider = new Mock().Object; - Mock serializer = new Mock(provider); - ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "CollectionName", Model = EdmCoreModel.Instance }; - IEnumerable enumerable = new int[0]; - ODataCollectionValue collectionValue = new ODataCollectionValue { TypeName = "NS.Name", Items = new object[] { 0, 1, 2 } }; - - serializer.CallBase = true; - serializer - .Setup(s => s.CreateODataCollectionValue(enumerable, It.Is(e => e.Definition == this._edmIntType.Definition), writeContext)) - .Returns(collectionValue).Verifiable(); - - // Act - await serializer.Object.WriteObjectAsync(enumerable, typeof(int[]), messageWriter, writeContext); - - // Assert - serializer.Verify(); - stream.Seek(0, SeekOrigin.Begin); - string result = new StreamReader(stream).ReadToEnd(); - Assert.Equal("{\"@odata.context\":\"http://any/$metadata#Collection(Edm.Int32)\",\"value\":[0,1,2]}", result); - } + [Fact] + public async Task WriteCollectionAsync_ThrowsArgumentNull_ForInputParameters() + { + // Arrange + IODataSerializerProvider provider = new Mock().Object; + ODataCollectionSerializer serializer = new ODataCollectionSerializer(provider); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => serializer.WriteCollectionAsync(writer: null, graph: null, collectionType: null, writeContext: null), + "writer"); + + // Arrange & Act & Assert + ODataCollectionWriter writer = new Mock().Object; + await ExceptionAssert.ThrowsArgumentNullAsync(() => serializer.WriteCollectionAsync(writer, graph: null, collectionType: null, writeContext: null), + "writeContext"); + } - [Fact] - public async Task WriteCollectionAsync_ThrowsArgumentNull_ForInputParameters() - { - // Arrange - IODataSerializerProvider provider = new Mock().Object; - ODataCollectionSerializer serializer = new ODataCollectionSerializer(provider); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => serializer.WriteCollectionAsync(writer: null, graph: null, collectionType: null, writeContext: null), - "writer"); - - // Arrange & Act & Assert - ODataCollectionWriter writer = new Mock().Object; - await ExceptionAssert.ThrowsArgumentNullAsync(() => serializer.WriteCollectionAsync(writer, graph: null, collectionType: null, writeContext: null), - "writeContext"); - } + [Fact] + public void CreateODataCollectionValue_ThrowsArgumentNull_ForInputParameters() + { + // Arrange + IODataSerializerProvider provider = new Mock().Object; + ODataCollectionSerializer serializer = new ODataCollectionSerializer(provider); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateODataCollectionValue(Enumerable.Empty(), null, writeContext: null), + "elementType"); + + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateODataCollectionValue(Enumerable.Empty(), this._edmIntType, writeContext: null), + "writeContext"); + } - [Fact] - public void CreateODataCollectionValue_ThrowsArgumentNull_ForInputParameters() - { - // Arrange - IODataSerializerProvider provider = new Mock().Object; - ODataCollectionSerializer serializer = new ODataCollectionSerializer(provider); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateODataCollectionValue(Enumerable.Empty(), null, writeContext: null), - "elementType"); - - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateODataCollectionValue(Enumerable.Empty(), this._edmIntType, writeContext: null), - "writeContext"); - } + [Fact] + public void CreateODataValue_ThrowsArgument_IfGraphIsNotEnumerable() + { + // Arrange + object nonEnumerable = new object(); + Mock serializerProvider = new Mock(); + var serializer = new ODataCollectionSerializer(serializerProvider.Object); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(null); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => serializer.CreateODataValue(graph: nonEnumerable, expectedType: null, writeContext: new ODataSerializerContext()), + "graph", + "The argument must be of type 'IEnumerable'."); + } - [Fact] - public void CreateODataValue_ThrowsArgument_IfGraphIsNotEnumerable() - { - // Arrange - object nonEnumerable = new object(); - Mock serializerProvider = new Mock(); - var serializer = new ODataCollectionSerializer(serializerProvider.Object); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(null); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => serializer.CreateODataValue(graph: nonEnumerable, expectedType: null, writeContext: new ODataSerializerContext()), - "graph", - "The argument must be of type 'IEnumerable'."); - } + [Fact] + public void CreateODataCollectionValue_ThrowsSerializationException_TypeCannotBeSerialized() + { + // Arrange + IEnumerable enumerable = new[] { 0 }; + Mock serializerProvider = new Mock(); + var serializer = new ODataCollectionSerializer(serializerProvider.Object); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(null); + + // Act & Assert + ExceptionAssert.Throws( + () => serializer.CreateODataCollectionValue(enumerable, this._edmIntType, new ODataSerializerContext { Model = EdmCoreModel.Instance }), + "'Edm.Int32' cannot be serialized using the OData output formatter."); + } - [Fact] - public void CreateODataCollectionValue_ThrowsSerializationException_TypeCannotBeSerialized() - { - // Arrange - IEnumerable enumerable = new[] { 0 }; - Mock serializerProvider = new Mock(); - var serializer = new ODataCollectionSerializer(serializerProvider.Object); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(null); - - // Act & Assert - ExceptionAssert.Throws( - () => serializer.CreateODataCollectionValue(enumerable, this._edmIntType, new ODataSerializerContext { Model = EdmCoreModel.Instance }), - "'Edm.Int32' cannot be serialized using the OData output formatter."); - } + [Fact] + public void CreateODataValue_Calls_CreateODataCollectionValue() + { + // Arrange + ODataCollectionValue oDataCollectionValue = new ODataCollectionValue(); + var collection = new object[0]; + Mock serializerProvider = new Mock(); + Mock serializer = new Mock(serializerProvider.Object); + ODataSerializerContext writeContext = new ODataSerializerContext(); + serializer.CallBase = true; + serializer + .Setup(s => s.CreateODataCollectionValue(collection, _edmIntType, writeContext)) + .Returns(oDataCollectionValue) + .Verifiable(); + + // Act + ODataValue value = serializer.Object.CreateODataValue(collection, _collectionType, writeContext); + + // Assert + serializer.Verify(); + Assert.Same(oDataCollectionValue, value); + } - [Fact] - public void CreateODataValue_Calls_CreateODataCollectionValue() - { - // Arrange - ODataCollectionValue oDataCollectionValue = new ODataCollectionValue(); - var collection = new object[0]; - Mock serializerProvider = new Mock(); - Mock serializer = new Mock(serializerProvider.Object); - ODataSerializerContext writeContext = new ODataSerializerContext(); - serializer.CallBase = true; - serializer - .Setup(s => s.CreateODataCollectionValue(collection, _edmIntType, writeContext)) - .Returns(oDataCollectionValue) - .Verifiable(); - - // Act - ODataValue value = serializer.Object.CreateODataValue(collection, _collectionType, writeContext); - - // Assert - serializer.Verify(); - Assert.Same(oDataCollectionValue, value); - } + [Fact] + public void CreateODataCollectionValue_Serializes_AllElementsInTheCollection() + { + // Arrange + ODataPrimitiveSerializer primitiveSerializer = new ODataPrimitiveSerializer(); + ODataSerializerContext writeContext = new ODataSerializerContext { Model = EdmCoreModel.Instance }; + Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(primitiveSerializer); - [Fact] - public void CreateODataCollectionValue_Serializes_AllElementsInTheCollection() - { - // Arrange - ODataPrimitiveSerializer primitiveSerializer = new ODataPrimitiveSerializer(); - ODataSerializerContext writeContext = new ODataSerializerContext { Model = EdmCoreModel.Instance }; - Mock serializerProvider = new Mock(); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(primitiveSerializer); + ODataCollectionSerializer serializer = new ODataCollectionSerializer(serializerProvider.Object); - ODataCollectionSerializer serializer = new ODataCollectionSerializer(serializerProvider.Object); + // Act + var oDataValue = serializer.CreateODataCollectionValue(new int[] { 1, 2, 3 }, _edmIntType, writeContext); - // Act - var oDataValue = serializer.CreateODataCollectionValue(new int[] { 1, 2, 3 }, _edmIntType, writeContext); + // Assert + var values = Assert.IsType(oDataValue); - // Assert - var values = Assert.IsType(oDataValue); + List elements = new List(); + foreach (var item in values.Items) + { + elements.Add(Assert.IsType(item)); + } - List elements = new List(); - foreach (var item in values.Items) - { - elements.Add(Assert.IsType(item)); - } + Assert.Equal(elements, new int[] { 1, 2, 3 }); + } - Assert.Equal(elements, new int[] { 1, 2, 3 }); - } + [Fact] + public void CreateODataCollectionValue_CanSerialize_IEdmObjects() + { + // Arrange + Mock edmEnumObject = new Mock(); + IEdmEnumObject[] collection = { edmEnumObject.Object }; + ODataSerializerContext serializerContext = new ODataSerializerContext(); + IEdmEnumTypeReference elementType = new EdmEnumTypeReference(new EdmEnumType("NS", "EnumType"), isNullable: true); + edmEnumObject.Setup(s => s.GetEdmType()).Returns(elementType); - [Fact] - public void CreateODataCollectionValue_CanSerialize_IEdmObjects() - { - // Arrange - Mock edmEnumObject = new Mock(); - IEdmEnumObject[] collection = { edmEnumObject.Object }; - ODataSerializerContext serializerContext = new ODataSerializerContext(); - IEdmEnumTypeReference elementType = new EdmEnumTypeReference(new EdmEnumType("NS", "EnumType"), isNullable: true); - edmEnumObject.Setup(s => s.GetEdmType()).Returns(elementType); + Mock serializerProvider = new Mock(); + Mock elementSerializer = new Mock(MockBehavior.Strict, serializerProvider.Object); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(elementType)).Returns(elementSerializer.Object); + elementSerializer.Setup(s => s.CreateODataEnumValue(collection[0], elementType, serializerContext)).Returns(new ODataEnumValue("1", "NS.EnumType")).Verifiable(); - Mock serializerProvider = new Mock(); - Mock elementSerializer = new Mock(MockBehavior.Strict, serializerProvider.Object); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(elementType)).Returns(elementSerializer.Object); - elementSerializer.Setup(s => s.CreateODataEnumValue(collection[0], elementType, serializerContext)).Returns(new ODataEnumValue("1", "NS.EnumType")).Verifiable(); + ODataCollectionSerializer serializer = new ODataCollectionSerializer(serializerProvider.Object); - ODataCollectionSerializer serializer = new ODataCollectionSerializer(serializerProvider.Object); + // Act + serializer.CreateODataCollectionValue(collection, elementType, serializerContext); - // Act - serializer.CreateODataCollectionValue(collection, elementType, serializerContext); + // Assert + elementSerializer.Verify(); + } - // Assert - elementSerializer.Verify(); - } + [Fact] + public void CreateODataCollectionValue_Returns_EmptyODataCollectionValue_ForNull() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataCollectionSerializer serializer = new ODataCollectionSerializer(serializerProvider.Object); - [Fact] - public void CreateODataCollectionValue_Returns_EmptyODataCollectionValue_ForNull() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataCollectionSerializer serializer = new ODataCollectionSerializer(serializerProvider.Object); + // Act + var oDataValue = serializer.CreateODataCollectionValue(null, _edmIntType, new ODataSerializerContext()); - // Act - var oDataValue = serializer.CreateODataCollectionValue(null, _edmIntType, new ODataSerializerContext()); + // Assert + Assert.NotNull(oDataValue); + ODataCollectionValue collection = Assert.IsType(oDataValue); + Assert.Empty(collection.Items); + } - // Assert - Assert.NotNull(oDataValue); - ODataCollectionValue collection = Assert.IsType(oDataValue); - Assert.Empty(collection.Items); - } + [Fact] + public void CreateODataCollectionValue_SetsTypeName() + { + // Arrange + IEnumerable enumerable = new int[] { 1, 2, 3 }; + ODataSerializerContext context = new ODataSerializerContext { Model = EdmCoreModel.Instance }; + Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + ODataCollectionSerializer serializer = new ODataCollectionSerializer(serializerProvider.Object); + + // Act + ODataValue oDataValue = serializer.CreateODataCollectionValue(enumerable, _edmIntType, context); + + // Assert + ODataCollectionValue collection = Assert.IsType(oDataValue); + Assert.Equal("Collection(Edm.Int32)", collection.TypeName); + } - [Fact] - public void CreateODataCollectionValue_SetsTypeName() - { - // Arrange - IEnumerable enumerable = new int[] { 1, 2, 3 }; - ODataSerializerContext context = new ODataSerializerContext { Model = EdmCoreModel.Instance }; - Mock serializerProvider = new Mock(); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); - ODataCollectionSerializer serializer = new ODataCollectionSerializer(serializerProvider.Object); - - // Act - ODataValue oDataValue = serializer.CreateODataCollectionValue(enumerable, _edmIntType, context); - - // Assert - ODataCollectionValue collection = Assert.IsType(oDataValue); - Assert.Equal("Collection(Edm.Int32)", collection.TypeName); - } + [Fact] + public void AddTypeNameAnnotationAsNeeded_DoesNotAddAnnotation_InDefaultMetadataMode() + { + // Arrange + ODataCollectionValue value = new ODataCollectionValue(); + + // Act + ODataCollectionSerializer.AddTypeNameAnnotationAsNeeded(value, ODataMetadataLevel.Minimal); + + // Assert + Assert.Null(value.TypeAnnotation); + } - [Fact] - public void AddTypeNameAnnotationAsNeeded_DoesNotAddAnnotation_InDefaultMetadataMode() + [Fact] + public void AddTypeNameAnnotationAsNeeded_AddsAnnotation_InJsonLightMetadataMode() + { + // Arrange + string expectedTypeName = "TypeName"; + ODataCollectionValue value = new ODataCollectionValue { - // Arrange - ODataCollectionValue value = new ODataCollectionValue(); + TypeName = expectedTypeName + }; - // Act - ODataCollectionSerializer.AddTypeNameAnnotationAsNeeded(value, ODataMetadataLevel.Minimal); + // Act + ODataCollectionSerializer.AddTypeNameAnnotationAsNeeded(value, ODataMetadataLevel.Full); - // Assert - Assert.Null(value.TypeAnnotation); - } + // Assert + ODataTypeAnnotation annotation = value.TypeAnnotation; + Assert.NotNull(annotation); // Guard + Assert.Equal(expectedTypeName, annotation.TypeName); + } - [Fact] - public void AddTypeNameAnnotationAsNeeded_AddsAnnotation_InJsonLightMetadataMode() + [Fact] + public void AddTypeNameAnnotationAsNeeded_AddsAnnotationWithNull_InJsonLightNoMetadataMode() + { + // Arrange + string expectedTypeName = "TypeName"; + ODataCollectionValue value = new ODataCollectionValue { - // Arrange - string expectedTypeName = "TypeName"; - ODataCollectionValue value = new ODataCollectionValue - { - TypeName = expectedTypeName - }; - - // Act - ODataCollectionSerializer.AddTypeNameAnnotationAsNeeded(value, ODataMetadataLevel.Full); - - // Assert - ODataTypeAnnotation annotation = value.TypeAnnotation; - Assert.NotNull(annotation); // Guard - Assert.Equal(expectedTypeName, annotation.TypeName); - } + TypeName = expectedTypeName + }; - [Fact] - public void AddTypeNameAnnotationAsNeeded_AddsAnnotationWithNull_InJsonLightNoMetadataMode() - { - // Arrange - string expectedTypeName = "TypeName"; - ODataCollectionValue value = new ODataCollectionValue - { - TypeName = expectedTypeName - }; - - // Act - ODataCollectionSerializer.AddTypeNameAnnotationAsNeeded(value, ODataMetadataLevel.None); - - // Assert - ODataTypeAnnotation annotation = value.TypeAnnotation; - Assert.NotNull(annotation); // Guard - Assert.Null(annotation.TypeName); - } + // Act + ODataCollectionSerializer.AddTypeNameAnnotationAsNeeded(value, ODataMetadataLevel.None); - [Theory] - [InlineData(ODataMetadataLevel.Full, true)] - [InlineData(ODataMetadataLevel.Minimal, false)] - [InlineData(ODataMetadataLevel.None, true)] - public void ShouldAddTypeNameAnnotation(ODataMetadataLevel metadataLevel, bool expectedResult) - { - // Act - bool actualResult = ODataCollectionSerializer.ShouldAddTypeNameAnnotation(metadataLevel); + // Assert + ODataTypeAnnotation annotation = value.TypeAnnotation; + Assert.NotNull(annotation); // Guard + Assert.Null(annotation.TypeName); + } - // Assert - Assert.Equal(expectedResult, actualResult); - } + [Theory] + [InlineData(ODataMetadataLevel.Full, true)] + [InlineData(ODataMetadataLevel.Minimal, false)] + [InlineData(ODataMetadataLevel.None, true)] + public void ShouldAddTypeNameAnnotation(ODataMetadataLevel metadataLevel, bool expectedResult) + { + // Act + bool actualResult = ODataCollectionSerializer.ShouldAddTypeNameAnnotation(metadataLevel); - [Theory] - [InlineData(ODataMetadataLevel.Full, false)] - [InlineData(ODataMetadataLevel.None, true)] - public void ShouldSuppressTypeNameSerialization(ODataMetadataLevel metadataLevel, bool expectedResult) - { - // Act - bool actualResult = ODataCollectionSerializer.ShouldSuppressTypeNameSerialization(metadataLevel); + // Assert + Assert.Equal(expectedResult, actualResult); + } - // Assert - Assert.Equal(expectedResult, actualResult); - } + [Theory] + [InlineData(ODataMetadataLevel.Full, false)] + [InlineData(ODataMetadataLevel.None, true)] + public void ShouldSuppressTypeNameSerialization(ODataMetadataLevel metadataLevel, bool expectedResult) + { + // Act + bool actualResult = ODataCollectionSerializer.ShouldSuppressTypeNameSerialization(metadataLevel); - [Fact] - public void GetElementType_ThrowsSerializationException_NonCollectionType() - { - // Arrange - IEdmTypeReference edmType = EdmCoreModel.Instance.GetString(false); + // Assert + Assert.Equal(expectedResult, actualResult); + } - // Act & Assert - ExceptionAssert.Throws( - () => ODataCollectionSerializer.GetElementType(edmType), - "ODataCollectionSerializer cannot write an object of type 'Edm.String'."); - } + [Fact] + public void GetElementType_ThrowsSerializationException_NonCollectionType() + { + // Arrange + IEdmTypeReference edmType = EdmCoreModel.Instance.GetString(false); + + // Act & Assert + ExceptionAssert.Throws( + () => ODataCollectionSerializer.GetElementType(edmType), + "ODataCollectionSerializer cannot write an object of type 'Edm.String'."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeltaResourceSetSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeltaResourceSetSerializerTests.cs index ca6b39ef4..12d5eb378 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeltaResourceSetSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataDeltaResourceSetSerializerTests.cs @@ -24,423 +24,422 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataDeltaResourceSetSerializerTests { - public class ODataDeltaResourceSetSerializerTests + IEdmModel _model; + IEdmEntitySet _customerSet; + Customer[] _customers; + EdmChangedObjectCollection _deltaResourceSetCustomers; + ODataDeltaResourceSetSerializer _serializer; + IEdmCollectionTypeReference _customersType; + private ODataPath _path; + ODataSerializerContext _writeContext; + + public ODataDeltaResourceSetSerializerTests() { - IEdmModel _model; - IEdmEntitySet _customerSet; - Customer[] _customers; - EdmChangedObjectCollection _deltaResourceSetCustomers; - ODataDeltaResourceSetSerializer _serializer; - IEdmCollectionTypeReference _customersType; - private ODataPath _path; - ODataSerializerContext _writeContext; - - public ODataDeltaResourceSetSerializerTests() - { - _model = SerializationTestsHelpers.SimpleCustomerOrderModel(); - _customerSet = _model.EntityContainer.FindEntitySet("Customers"); - _model.SetAnnotationValue(_customerSet.EntityType, new ClrTypeAnnotation(typeof(Customer))); - _path = new ODataPath(new EntitySetSegment(_customerSet)); - _customers = new[] { - new Customer() - { - FirstName = "Foo", - LastName = "Bar", - ID = 10, - HomeAddress = new Address() - { - Street = "Street", - ZipCode = null, - } - }, - new Customer() + _model = SerializationTestsHelpers.SimpleCustomerOrderModel(); + _customerSet = _model.EntityContainer.FindEntitySet("Customers"); + _model.SetAnnotationValue(_customerSet.EntityType, new ClrTypeAnnotation(typeof(Customer))); + _path = new ODataPath(new EntitySetSegment(_customerSet)); + _customers = new[] { + new Customer() + { + FirstName = "Foo", + LastName = "Bar", + ID = 10, + HomeAddress = new Address() { - FirstName = "Foo", - LastName = "Bar", - ID = 42, + Street = "Street", + ZipCode = null, } - }; - - _deltaResourceSetCustomers = new EdmChangedObjectCollection(_customerSet.EntityType); - EdmDeltaResourceObject newCustomer = new EdmDeltaResourceObject(_customerSet.EntityType); - newCustomer.TrySetPropertyValue("ID", 10); - newCustomer.TrySetPropertyValue("FirstName", "Foo"); - EdmComplexObject newCustomerAddress = new EdmComplexObject(_model.FindType("Default.Address") as IEdmComplexType); - newCustomerAddress.TrySetPropertyValue("Street", "Street"); - newCustomerAddress.TrySetPropertyValue("ZipCode", null); - newCustomer.TrySetPropertyValue("HomeAddress", newCustomerAddress); - _deltaResourceSetCustomers.Add(newCustomer); - - _customersType = _model.GetEdmTypeReference(typeof(Customer[])).AsCollection(); - _writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path }; - } - - [Fact] - public void Ctor_ThrowsArgumentNull_SerializerProvider() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataDeltaResourceSetSerializer(serializerProvider: null), "serializerProvider"); - } - - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() - { - // Arrange - Mock provider = new Mock(); - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: null, type: null, messageWriter: null, writeContext: new ODataSerializerContext()), - "messageWriter"); - } - - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_WriteContext() - { - // Arrange - Mock provider = new Mock(); - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: null, type: null, messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: null), - "writeContext"); - } - - [Fact] - public async Task WriteObjectAsync_Throws_EntitySetMissingDuringSerialization() - { - // Arrange - object graph = new object(); - Mock provider = new Mock(); - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectAsync(graph: graph, type: null, messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: new ODataSerializerContext()), - "The related entity set could not be found from the OData path. The related entity set is required to serialize the payload."); - } - - [Fact] - public async Task WriteObjectAsync_Calls_WriteObjectInlineAsync() - { - // Arrange - object graph = new object(); - Mock provider = new Mock(); - Mock serializer = new Mock(provider.Object); - serializer.CallBase = true; - serializer - .Setup(s => s.WriteObjectInlineAsync(graph, It.Is(e => _customersType.IsEquivalentTo(e)), - It.IsAny(), _writeContext)) - .Returns(Task.CompletedTask) - .Verifiable(); - MemoryStream stream = new MemoryStream(); - IODataResponseMessageAsync message = new ODataMessageWrapper(stream); - ODataMessageWriterSettings settings = new ODataMessageWriterSettings() + }, + new Customer() { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } - }; - ODataMessageWriter messageWriter = new ODataMessageWriter(message, settings, _model); + FirstName = "Foo", + LastName = "Bar", + ID = 42, + } + }; + + _deltaResourceSetCustomers = new EdmChangedObjectCollection(_customerSet.EntityType); + EdmDeltaResourceObject newCustomer = new EdmDeltaResourceObject(_customerSet.EntityType); + newCustomer.TrySetPropertyValue("ID", 10); + newCustomer.TrySetPropertyValue("FirstName", "Foo"); + EdmComplexObject newCustomerAddress = new EdmComplexObject(_model.FindType("Default.Address") as IEdmComplexType); + newCustomerAddress.TrySetPropertyValue("Street", "Street"); + newCustomerAddress.TrySetPropertyValue("ZipCode", null); + newCustomer.TrySetPropertyValue("HomeAddress", newCustomerAddress); + _deltaResourceSetCustomers.Add(newCustomer); + + _customersType = _model.GetEdmTypeReference(typeof(Customer[])).AsCollection(); + _writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path }; + } - // Act - await serializer.Object.WriteObjectAsync(graph, typeof(Customer[]), messageWriter, _writeContext); + [Fact] + public void Ctor_ThrowsArgumentNull_SerializerProvider() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataDeltaResourceSetSerializer(serializerProvider: null), "serializerProvider"); + } - // Assert - serializer.Verify(); - } + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() + { + // Arrange + Mock provider = new Mock(); + ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: null, type: null, messageWriter: null, writeContext: new ODataSerializerContext()), + "messageWriter"); + } - [Fact] - public async Task WriteObjectInlineAsync_ThrowsArgumentNull_Writer() - { - // Arrange - Mock provider = new Mock(); - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: null, writeContext: new ODataSerializerContext()), - "writer"); - } - - [Fact] - public async Task WriteObjectInlineAsync_ThrowsArgumentNull_WriteContext() - { - // Arrange - Mock provider = new Mock(); - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: new Mock().Object, writeContext: null), - "writeContext"); - } - - [Fact] - public async Task WriteObjectInlineAsync_ThrowsSerializationException_CannotSerializerNull() - { - // Arrange - Mock provider = new Mock(); - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectInlineAsync(graph: null, expectedType: _customersType, - writer: new Mock().Object, writeContext: _writeContext), - "Cannot serialize a null 'DeltaResourceSet'."); - } - - [Fact] - public async Task WriteObjectInlineAsync_ThrowsSerializationException_IfGraphIsNotEnumerable() - { - // Arrange - Mock provider = new Mock(); - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectInlineAsync(graph: 42, expectedType: _customersType, - writer: new Mock().Object, writeContext: _writeContext), - "ODataDeltaResourceSetSerializer cannot write an object of type 'System.Int32'."); - } - - [Fact] - public async Task WriteObjectInlineAsync_Throws_NullElementInCollection_IfFeedContainsNullElement() - { - // Arrange - IEnumerable instance = new object[] { null }; - Mock provider = new Mock(); - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); - - // Act - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectInlineAsync(instance, _customersType, new Mock().Object, _writeContext), - "Collections cannot contain null elements."); - } - - [Fact] - public async Task WriteObjectInlineAsync_Throws_TypeCannotBeSerialized_IfFeedContainsEntityThatCannotBeSerialized() - { - // Arrange - Mock serializerProvider = new Mock(); - var request = RequestFactory.Create(); - serializerProvider.Setup(s => s.GetODataPayloadSerializer(typeof(int), request)).Returns(null); - IEnumerable instance = new object[] { 42 }; - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(serializerProvider.Object); - - // Act - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectInlineAsync(instance, _customersType, new Mock().Object, _writeContext), - "ODataDeltaResourceSetSerializer cannot write an object of type 'System.Int32'."); - } - - [Fact] - public async Task WriteObjectInlineAsync_Calls_CreateODataDeltaResourceSet() - { - // Arrange - IEnumerable instance = new object[0]; - Mock provider = new Mock(); - Mock serializer = new Mock(provider.Object); - serializer.CallBase = true; - serializer.Setup(s => s.CreateODataDeltaResourceSet(instance, _customersType, _writeContext)).Returns(new ODataDeltaResourceSet()).Verifiable(); - - // Act - await serializer.Object.WriteObjectInlineAsync(instance, _customersType, new Mock().Object, _writeContext); - - // Assert - serializer.Verify(); - } - - [Fact] - public async Task WriteObjectInlineAsync_Throws_CannotSerializerNull_IfCreateODataDeltaResourceSetReturnsNull() - { - // Arrange - IEnumerable instance = new object[0]; - Mock provider = new Mock(); - Mock serializer = new Mock(provider.Object); - serializer.CallBase = true; - serializer.Setup(s => s.CreateODataDeltaResourceSet(instance, _customersType, _writeContext)).Returns(null); - ODataWriter writer = new Mock().Object; - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.Object.WriteObjectInlineAsync(instance, _customersType, writer, _writeContext), - "Cannot serialize a null 'DeltaResourceSet'."); - } - - [Fact] - public async Task WriteObjectInlineAsync_Writes_CreateODataFeedOutput() - { - // Arrange - IEnumerable instance = new object[0]; - ODataDeltaResourceSet deltaResourceSet = new ODataDeltaResourceSet(); - Mock provider = new Mock(); - Mock serializer = new Mock(provider.Object); - serializer.CallBase = true; - serializer.Setup(s => s.CreateODataDeltaResourceSet(instance, _customersType, _writeContext)).Returns(deltaResourceSet); - Mock writer = new Mock(); - writer.Setup(s => s.WriteStartAsync(deltaResourceSet)).Verifiable(); - - // Act - await serializer.Object.WriteObjectInlineAsync(instance, _customersType, writer.Object, _writeContext); - - // Assert - writer.Verify(); - } - - [Fact] - public async Task WriteObjectInlineAsync_Can_WriteCollectionOfIEdmChangedObjects() + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_WriteContext() + { + // Arrange + Mock provider = new Mock(); + ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: null, type: null, messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: null), + "writeContext"); + } + + [Fact] + public async Task WriteObjectAsync_Throws_EntitySetMissingDuringSerialization() + { + // Arrange + object graph = new object(); + Mock provider = new Mock(); + ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectAsync(graph: graph, type: null, messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: new ODataSerializerContext()), + "The related entity set could not be found from the OData path. The related entity set is required to serialize the payload."); + } + + [Fact] + public async Task WriteObjectAsync_Calls_WriteObjectInlineAsync() + { + // Arrange + object graph = new object(); + Mock provider = new Mock(); + Mock serializer = new Mock(provider.Object); + serializer.CallBase = true; + serializer + .Setup(s => s.WriteObjectInlineAsync(graph, It.Is(e => _customersType.IsEquivalentTo(e)), + It.IsAny(), _writeContext)) + .Returns(Task.CompletedTask) + .Verifiable(); + MemoryStream stream = new MemoryStream(); + IODataResponseMessageAsync message = new ODataMessageWrapper(stream); + ODataMessageWriterSettings settings = new ODataMessageWriterSettings() { - // Arrange - IEdmTypeReference edmType = new EdmEntityTypeReference(new EdmEntityType("NS", "Name"), isNullable: false); - IEdmCollectionTypeReference feedType = new EdmCollectionTypeReference(new EdmCollectionType(edmType)); - Mock edmObject = new Mock(); - edmObject.Setup(e => e.GetEdmType()).Returns(edmType); + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } + }; + ODataMessageWriter messageWriter = new ODataMessageWriter(message, settings, _model); - var mockWriter = new Mock(); + // Act + await serializer.Object.WriteObjectAsync(graph, typeof(Customer[]), messageWriter, _writeContext); - Mock provider = new Mock(); - Mock customerSerializer = new Mock(provider.Object); - customerSerializer.Setup(s => s.WriteDeltaObjectInlineAsync(edmObject.Object, edmType, mockWriter.Object, _writeContext)).Verifiable(); + // Assert + serializer.Verify(); + } - Mock serializerProvider = new Mock(); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(edmType)).Returns(customerSerializer.Object); + [Fact] + public async Task WriteObjectInlineAsync_ThrowsArgumentNull_Writer() + { + // Arrange + Mock provider = new Mock(); + ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: null, writeContext: new ODataSerializerContext()), + "writer"); + } - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(serializerProvider.Object); + [Fact] + public async Task WriteObjectInlineAsync_ThrowsArgumentNull_WriteContext() + { + // Arrange + Mock provider = new Mock(); + ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: new Mock().Object, writeContext: null), + "writeContext"); + } - // Act - await serializer.WriteObjectInlineAsync(new[] { edmObject.Object }, feedType, mockWriter.Object, _writeContext); + [Fact] + public async Task WriteObjectInlineAsync_ThrowsSerializationException_CannotSerializerNull() + { + // Arrange + Mock provider = new Mock(); + ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectInlineAsync(graph: null, expectedType: _customersType, + writer: new Mock().Object, writeContext: _writeContext), + "Cannot serialize a null 'DeltaResourceSet'."); + } - // Assert - customerSerializer.Verify(); - } + [Fact] + public async Task WriteObjectInlineAsync_ThrowsSerializationException_IfGraphIsNotEnumerable() + { + // Arrange + Mock provider = new Mock(); + ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectInlineAsync(graph: 42, expectedType: _customersType, + writer: new Mock().Object, writeContext: _writeContext), + "ODataDeltaResourceSetSerializer cannot write an object of type 'System.Int32'."); + } - [Fact] - public async Task WriteObjectInlineAsync_WritesEachEntityInstance() - { - // Arrange - Mock serializeProvider = new Mock(); - Mock customerSerializer = new Mock(serializeProvider.Object); - IODataSerializerProvider provider = ODataTestUtil.GetMockODataSerializerProvider(customerSerializer.Object); - var mockWriter = new Mock(); - customerSerializer.Setup(s => s.WriteDeltaObjectInlineAsync(_deltaResourceSetCustomers[0], _customersType.ElementType(), mockWriter.Object, _writeContext)).Verifiable(); - _serializer = new ODataDeltaResourceSetSerializer(provider); - - // Act - await _serializer.WriteObjectInlineAsync(_deltaResourceSetCustomers, _customersType, mockWriter.Object, _writeContext); - - // Assert - customerSerializer.Verify(); - } - - [Fact] - public async Task WriteObjectInlineAsync_Sets_NextPageLink_OnWriteEndAsync() - { - // Arrange - IEnumerable instance = new object[0]; - ODataDeltaResourceSet deltaResourceSet = new ODataDeltaResourceSet { NextPageLink = new Uri("http://nextlink.com/") }; - Mock serializeProvider = new Mock(); - Mock serializer = new Mock(serializeProvider.Object); - serializer.CallBase = true; - serializer.Setup(s => s.CreateODataDeltaResourceSet(instance, _customersType, _writeContext)).Returns(deltaResourceSet); - var mockWriter = new Mock(); - - mockWriter.Setup(m => m.WriteStartAsync(It.Is(f => f.NextPageLink == null))).Verifiable(); - mockWriter - .Setup(m => m.WriteEndAsync()) - .Callback(() => - { - Assert.Equal("http://nextlink.com/", deltaResourceSet.NextPageLink.AbsoluteUri); - }) - .Returns(Task.CompletedTask) - .Verifiable(); + [Fact] + public async Task WriteObjectInlineAsync_Throws_NullElementInCollection_IfFeedContainsNullElement() + { + // Arrange + IEnumerable instance = new object[] { null }; + Mock provider = new Mock(); + ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(provider.Object); + + // Act + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectInlineAsync(instance, _customersType, new Mock().Object, _writeContext), + "Collections cannot contain null elements."); + } - // Act - await serializer.Object.WriteObjectInlineAsync(instance, _customersType, mockWriter.Object, _writeContext); + [Fact] + public async Task WriteObjectInlineAsync_Throws_TypeCannotBeSerialized_IfFeedContainsEntityThatCannotBeSerialized() + { + // Arrange + Mock serializerProvider = new Mock(); + var request = RequestFactory.Create(); + serializerProvider.Setup(s => s.GetODataPayloadSerializer(typeof(int), request)).Returns(null); + IEnumerable instance = new object[] { 42 }; + ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(serializerProvider.Object); + + // Act + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectInlineAsync(instance, _customersType, new Mock().Object, _writeContext), + "ODataDeltaResourceSetSerializer cannot write an object of type 'System.Int32'."); + } - // Assert - mockWriter.Verify(); - } + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateODataDeltaResourceSet() + { + // Arrange + IEnumerable instance = new object[0]; + Mock provider = new Mock(); + Mock serializer = new Mock(provider.Object); + serializer.CallBase = true; + serializer.Setup(s => s.CreateODataDeltaResourceSet(instance, _customersType, _writeContext)).Returns(new ODataDeltaResourceSet()).Verifiable(); + + // Act + await serializer.Object.WriteObjectInlineAsync(instance, _customersType, new Mock().Object, _writeContext); + + // Assert + serializer.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_Sets_DeltaLink() - { - // Arrange - IEnumerable instance = new object[0]; - ODataDeltaResourceSet deltaResourceSet = new ODataDeltaResourceSet { DeltaLink = new Uri("http://deltalink.com/") }; - Mock serializeProvider = new Mock(); - Mock serializer = new Mock(serializeProvider.Object); - serializer.CallBase = true; - serializer.Setup(s => s.CreateODataDeltaResourceSet(instance, _customersType, _writeContext)).Returns(deltaResourceSet); - var mockWriter = new Mock(); - - mockWriter.Setup(m => m.WriteStartAsync(deltaResourceSet)); - mockWriter - .Setup(m => m.WriteEndAsync()) - .Callback(() => - { - Assert.Equal("http://deltalink.com/", deltaResourceSet.DeltaLink.AbsoluteUri); - }) - .Returns(Task.CompletedTask) - .Verifiable(); + [Fact] + public async Task WriteObjectInlineAsync_Throws_CannotSerializerNull_IfCreateODataDeltaResourceSetReturnsNull() + { + // Arrange + IEnumerable instance = new object[0]; + Mock provider = new Mock(); + Mock serializer = new Mock(provider.Object); + serializer.CallBase = true; + serializer.Setup(s => s.CreateODataDeltaResourceSet(instance, _customersType, _writeContext)).Returns(null); + ODataWriter writer = new Mock().Object; + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.Object.WriteObjectInlineAsync(instance, _customersType, writer, _writeContext), + "Cannot serialize a null 'DeltaResourceSet'."); + } + + [Fact] + public async Task WriteObjectInlineAsync_Writes_CreateODataFeedOutput() + { + // Arrange + IEnumerable instance = new object[0]; + ODataDeltaResourceSet deltaResourceSet = new ODataDeltaResourceSet(); + Mock provider = new Mock(); + Mock serializer = new Mock(provider.Object); + serializer.CallBase = true; + serializer.Setup(s => s.CreateODataDeltaResourceSet(instance, _customersType, _writeContext)).Returns(deltaResourceSet); + Mock writer = new Mock(); + writer.Setup(s => s.WriteStartAsync(deltaResourceSet)).Verifiable(); + + // Act + await serializer.Object.WriteObjectInlineAsync(instance, _customersType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } - // Act - await serializer.Object.WriteObjectInlineAsync(instance, _customersType, mockWriter.Object, _writeContext); + [Fact] + public async Task WriteObjectInlineAsync_Can_WriteCollectionOfIEdmChangedObjects() + { + // Arrange + IEdmTypeReference edmType = new EdmEntityTypeReference(new EdmEntityType("NS", "Name"), isNullable: false); + IEdmCollectionTypeReference feedType = new EdmCollectionTypeReference(new EdmCollectionType(edmType)); + Mock edmObject = new Mock(); + edmObject.Setup(e => e.GetEdmType()).Returns(edmType); - // Assert - mockWriter.Verify(); - } + var mockWriter = new Mock(); - [Fact] - public void CreateODataDeltaResourceSet_Sets_CountValueForPageResult() - { - // Arrange - Mock serializeProvider = new Mock(); - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(serializeProvider.Object); - Uri expectedNextLink = new Uri("http://nextlink.com"); - const long ExpectedCountValue = 1000; + Mock provider = new Mock(); + Mock customerSerializer = new Mock(provider.Object); + customerSerializer.Setup(s => s.WriteDeltaObjectInlineAsync(edmObject.Object, edmType, mockWriter.Object, _writeContext)).Verifiable(); - var result = new PageResult(_customers, expectedNextLink, ExpectedCountValue); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(edmType)).Returns(customerSerializer.Object); - // Act - ODataDeltaResourceSet feed = serializer.CreateODataDeltaResourceSet(result, _customersType, new ODataSerializerContext()); + ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(serializerProvider.Object); - // Assert - Assert.Equal(ExpectedCountValue, feed.Count); - } + // Act + await serializer.WriteObjectInlineAsync(new[] { edmObject.Object }, feedType, mockWriter.Object, _writeContext); - [Fact] - public void CreateODataDeltaResourceSet_Sets_NextPageLinkForPageResult() - { - // Arrange - Mock serializeProvider = new Mock(); - ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(serializeProvider.Object); - Uri expectedNextLink = new Uri("http://nextlink.com"); - const long ExpectedCountValue = 1000; + // Assert + customerSerializer.Verify(); + } - var result = new PageResult(_customers, expectedNextLink, ExpectedCountValue); + [Fact] + public async Task WriteObjectInlineAsync_WritesEachEntityInstance() + { + // Arrange + Mock serializeProvider = new Mock(); + Mock customerSerializer = new Mock(serializeProvider.Object); + IODataSerializerProvider provider = ODataTestUtil.GetMockODataSerializerProvider(customerSerializer.Object); + var mockWriter = new Mock(); + customerSerializer.Setup(s => s.WriteDeltaObjectInlineAsync(_deltaResourceSetCustomers[0], _customersType.ElementType(), mockWriter.Object, _writeContext)).Verifiable(); + _serializer = new ODataDeltaResourceSetSerializer(provider); + + // Act + await _serializer.WriteObjectInlineAsync(_deltaResourceSetCustomers, _customersType, mockWriter.Object, _writeContext); + + // Assert + customerSerializer.Verify(); + } - // Act - ODataDeltaResourceSet feed = serializer.CreateODataDeltaResourceSet(result, _customersType, new ODataSerializerContext()); + [Fact] + public async Task WriteObjectInlineAsync_Sets_NextPageLink_OnWriteEndAsync() + { + // Arrange + IEnumerable instance = new object[0]; + ODataDeltaResourceSet deltaResourceSet = new ODataDeltaResourceSet { NextPageLink = new Uri("http://nextlink.com/") }; + Mock serializeProvider = new Mock(); + Mock serializer = new Mock(serializeProvider.Object); + serializer.CallBase = true; + serializer.Setup(s => s.CreateODataDeltaResourceSet(instance, _customersType, _writeContext)).Returns(deltaResourceSet); + var mockWriter = new Mock(); + + mockWriter.Setup(m => m.WriteStartAsync(It.Is(f => f.NextPageLink == null))).Verifiable(); + mockWriter + .Setup(m => m.WriteEndAsync()) + .Callback(() => + { + Assert.Equal("http://nextlink.com/", deltaResourceSet.NextPageLink.AbsoluteUri); + }) + .Returns(Task.CompletedTask) + .Verifiable(); - // Assert - Assert.Equal(expectedNextLink, feed.NextPageLink); - } + // Act + await serializer.Object.WriteObjectInlineAsync(instance, _customersType, mockWriter.Object, _writeContext); - public class Customer - { - public int ID { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public Address HomeAddress { get; set; } - } + // Assert + mockWriter.Verify(); + } - public class Address - { - public string Street { get; set; } - public string ZipCode { get; set; } - } + [Fact] + public async Task WriteObjectInlineAsync_Sets_DeltaLink() + { + // Arrange + IEnumerable instance = new object[0]; + ODataDeltaResourceSet deltaResourceSet = new ODataDeltaResourceSet { DeltaLink = new Uri("http://deltalink.com/") }; + Mock serializeProvider = new Mock(); + Mock serializer = new Mock(serializeProvider.Object); + serializer.CallBase = true; + serializer.Setup(s => s.CreateODataDeltaResourceSet(instance, _customersType, _writeContext)).Returns(deltaResourceSet); + var mockWriter = new Mock(); + + mockWriter.Setup(m => m.WriteStartAsync(deltaResourceSet)); + mockWriter + .Setup(m => m.WriteEndAsync()) + .Callback(() => + { + Assert.Equal("http://deltalink.com/", deltaResourceSet.DeltaLink.AbsoluteUri); + }) + .Returns(Task.CompletedTask) + .Verifiable(); + + // Act + await serializer.Object.WriteObjectInlineAsync(instance, _customersType, mockWriter.Object, _writeContext); + + // Assert + mockWriter.Verify(); + } + + [Fact] + public void CreateODataDeltaResourceSet_Sets_CountValueForPageResult() + { + // Arrange + Mock serializeProvider = new Mock(); + ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(serializeProvider.Object); + Uri expectedNextLink = new Uri("http://nextlink.com"); + const long ExpectedCountValue = 1000; + + var result = new PageResult(_customers, expectedNextLink, ExpectedCountValue); + + // Act + ODataDeltaResourceSet feed = serializer.CreateODataDeltaResourceSet(result, _customersType, new ODataSerializerContext()); + + // Assert + Assert.Equal(ExpectedCountValue, feed.Count); + } + + [Fact] + public void CreateODataDeltaResourceSet_Sets_NextPageLinkForPageResult() + { + // Arrange + Mock serializeProvider = new Mock(); + ODataDeltaResourceSetSerializer serializer = new ODataDeltaResourceSetSerializer(serializeProvider.Object); + Uri expectedNextLink = new Uri("http://nextlink.com"); + const long ExpectedCountValue = 1000; + + var result = new PageResult(_customers, expectedNextLink, ExpectedCountValue); + + // Act + ODataDeltaResourceSet feed = serializer.CreateODataDeltaResourceSet(result, _customersType, new ODataSerializerContext()); + + // Assert + Assert.Equal(expectedNextLink, feed.NextPageLink); + } + + public class Customer + { + public int ID { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public Address HomeAddress { get; set; } + } + + public class Address + { + public string Street { get; set; } + public string ZipCode { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEdmTypeSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEdmTypeSerializerTests.cs index 7f1932763..d4348e4fb 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEdmTypeSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEdmTypeSerializerTests.cs @@ -14,74 +14,73 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataEdmTypeSerializerTest { - public class ODataEdmTypeSerializerTest + [Fact] + public void Ctor_SetsProperty_ODataPayloadKind() { - [Fact] - public void Ctor_SetsProperty_ODataPayloadKind() - { - // Arrange - var serializer = new Mock(ODataPayloadKind.Unsupported).Object; + // Arrange + var serializer = new Mock(ODataPayloadKind.Unsupported).Object; - // Act & Assert - Assert.Equal(ODataPayloadKind.Unsupported, serializer.ODataPayloadKind); - } + // Act & Assert + Assert.Equal(ODataPayloadKind.Unsupported, serializer.ODataPayloadKind); + } - [Fact] - public void Ctor_SetsProperty_SerializerProvider() - { - // Arrange & Act - IODataSerializerProvider serializerProvider = new Mock().Object; - var serializer = new Mock(ODataPayloadKind.Unsupported, serializerProvider).Object; + [Fact] + public void Ctor_SetsProperty_SerializerProvider() + { + // Arrange & Act + IODataSerializerProvider serializerProvider = new Mock().Object; + var serializer = new Mock(ODataPayloadKind.Unsupported, serializerProvider).Object; - // Assert - Assert.Same(serializerProvider, serializer.SerializerProvider); - } + // Assert + Assert.Same(serializerProvider, serializer.SerializerProvider); + } - [Fact] - public async Task WriteObjectInlineAsync_Throws_NotSupported() - { - // Arrange - var serializer = new Mock(ODataPayloadKind.Unsupported) { CallBase = true }; + [Fact] + public async Task WriteObjectInlineAsync_Throws_NotSupported() + { + // Arrange + var serializer = new Mock(ODataPayloadKind.Unsupported) { CallBase = true }; - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.Object.WriteObjectInlineAsync(graph: null, expectedType: null, writer: null, writeContext: null), - "ODataEdmTypeSerializerProxy does not support WriteObjectInline."); - } + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.Object.WriteObjectInlineAsync(graph: null, expectedType: null, writer: null, writeContext: null), + "ODataEdmTypeSerializerProxy does not support WriteObjectInline."); + } - [Fact] - public void CreateODataValue_Throws_NotSupported() - { - // Arrange - IEdmTypeReference edmType = new Mock().Object; - var serializer = new Mock(ODataPayloadKind.Unsupported) { CallBase = true }; + [Fact] + public void CreateODataValue_Throws_NotSupported() + { + // Arrange + IEdmTypeReference edmType = new Mock().Object; + var serializer = new Mock(ODataPayloadKind.Unsupported) { CallBase = true }; - // Act & Assert - ExceptionAssert.Throws( - () => serializer.Object.CreateODataValue(graph: null, expectedType: edmType, writeContext: null), - "ODataEdmTypeSerializerProxy does not support CreateODataValue."); - } + // Act & Assert + ExceptionAssert.Throws( + () => serializer.Object.CreateODataValue(graph: null, expectedType: edmType, writeContext: null), + "ODataEdmTypeSerializerProxy does not support CreateODataValue."); + } - [Fact] - public void CreateProperty_Returns_ODataProperty() - { - // Arrange - IEdmTypeReference edmType = new Mock().Object; - var serializer = new Mock(ODataPayloadKind.Unsupported); - serializer - .Setup(s => s.CreateODataValue(42, edmType, null)) - .Returns(new ODataPrimitiveValue(42)); + [Fact] + public void CreateProperty_Returns_ODataProperty() + { + // Arrange + IEdmTypeReference edmType = new Mock().Object; + var serializer = new Mock(ODataPayloadKind.Unsupported); + serializer + .Setup(s => s.CreateODataValue(42, edmType, null)) + .Returns(new ODataPrimitiveValue(42)); - // Act - ODataProperty property = serializer.Object.CreateProperty(graph: 42, expectedType: edmType, - elementName: "SomePropertyName", writeContext: null); + // Act + ODataProperty property = serializer.Object.CreateProperty(graph: 42, expectedType: edmType, + elementName: "SomePropertyName", writeContext: null); - // Assert - Assert.NotNull(property); - Assert.Equal("SomePropertyName", property.Name); - Assert.Equal(42, property.Value); - } + // Assert + Assert.NotNull(property); + Assert.Equal("SomePropertyName", property.Name); + Assert.Equal(42, property.Value); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEntityReferenceLinkSerializerTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEntityReferenceLinkSerializerTest.cs index f684d0c18..218447a6c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEntityReferenceLinkSerializerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEntityReferenceLinkSerializerTest.cs @@ -16,87 +16,86 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataEntityReferenceLinkSerializerTest { - public class ODataEntityReferenceLinkSerializerTest + [Fact] + public async Task WriteObject_ThrowsArgumentNull_MessageWriter() { - [Fact] - public async Task WriteObject_ThrowsArgumentNull_MessageWriter() - { - // Arrange - ODataEntityReferenceLinkSerializer serializer = new ODataEntityReferenceLinkSerializer(); + // Arrange + ODataEntityReferenceLinkSerializer serializer = new ODataEntityReferenceLinkSerializer(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: null, type: typeof(ODataEntityReferenceLink), messageWriter: null, - writeContext: new ODataSerializerContext()), - "messageWriter"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: null, type: typeof(ODataEntityReferenceLink), messageWriter: null, + writeContext: new ODataSerializerContext()), + "messageWriter"); + } - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_WriteContext() - { - // Arrange - ODataEntityReferenceLinkSerializer serializer = new ODataEntityReferenceLinkSerializer(); + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_WriteContext() + { + // Arrange + ODataEntityReferenceLinkSerializer serializer = new ODataEntityReferenceLinkSerializer(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: null, type: typeof(ODataEntityReferenceLink), - messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: null), - "writeContext"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: null, type: typeof(ODataEntityReferenceLink), + messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: null), + "writeContext"); + } - [Fact] - public async Task WriteObjectAsync_Throws_ObjectCannotBeWritten_IfGraphIsNotUri() - { - // Arrange - ODataEntityReferenceLinkSerializer serializer = new ODataEntityReferenceLinkSerializer(); + [Fact] + public async Task WriteObjectAsync_Throws_ObjectCannotBeWritten_IfGraphIsNotUri() + { + // Arrange + ODataEntityReferenceLinkSerializer serializer = new ODataEntityReferenceLinkSerializer(); - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectAsync(graph: "not uri", type: typeof(ODataEntityReferenceLink), - messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: new ODataSerializerContext()), - "ODataEntityReferenceLinkSerializer cannot write an object of type 'System.String'."); - } + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectAsync(graph: "not uri", type: typeof(ODataEntityReferenceLink), + messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: new ODataSerializerContext()), + "ODataEntityReferenceLinkSerializer cannot write an object of type 'System.String'."); + } - public static TheoryDataSet SerializationTestData + public static TheoryDataSet SerializationTestData + { + get { - get + Uri uri = new Uri("http://sampleuri/"); + return new TheoryDataSet { - Uri uri = new Uri("http://sampleuri/"); - return new TheoryDataSet - { - uri, - new ODataEntityReferenceLink { Url = uri } - }; - } + uri, + new ODataEntityReferenceLink { Url = uri } + }; } + } - [Theory] - [MemberData(nameof(SerializationTestData))] - public async Task ODataEntityReferenceLinkSerializer_Serializes_Uri(object link) - { - // Arrange - ODataEntityReferenceLinkSerializer serializer = new ODataEntityReferenceLinkSerializer(); - ODataSerializerContext writeContext = new ODataSerializerContext(); - MemoryStream stream = new MemoryStream(); - IODataResponseMessage message = new ODataMessageWrapper(stream); + [Theory] + [MemberData(nameof(SerializationTestData))] + public async Task ODataEntityReferenceLinkSerializer_Serializes_Uri(object link) + { + // Arrange + ODataEntityReferenceLinkSerializer serializer = new ODataEntityReferenceLinkSerializer(); + ODataSerializerContext writeContext = new ODataSerializerContext(); + MemoryStream stream = new MemoryStream(); + IODataResponseMessage message = new ODataMessageWrapper(stream); - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } - }; + ODataMessageWriterSettings settings = new ODataMessageWriterSettings + { + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } + }; - settings.SetContentType(ODataFormat.Json); - ODataMessageWriter writer = new ODataMessageWriter(message, settings); + settings.SetContentType(ODataFormat.Json); + ODataMessageWriter writer = new ODataMessageWriter(message, settings); - // Act - await serializer.WriteObjectAsync(link, typeof(ODataEntityReferenceLink), writer, writeContext); - stream.Seek(0, SeekOrigin.Begin); - string result = new StreamReader(stream).ReadToEnd(); + // Act + await serializer.WriteObjectAsync(link, typeof(ODataEntityReferenceLink), writer, writeContext); + stream.Seek(0, SeekOrigin.Begin); + string result = new StreamReader(stream).ReadToEnd(); - // Assert - Assert.Equal("{\"@odata.context\":\"http://any/$metadata#$ref\",\"@odata.id\":\"http://sampleuri/\"}", result); - } + // Assert + Assert.Equal("{\"@odata.context\":\"http://any/$metadata#$ref\",\"@odata.id\":\"http://sampleuri/\"}", result); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEntityReferenceLinksSerializerTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEntityReferenceLinksSerializerTest.cs index d88a7c5f5..83ff73a0e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEntityReferenceLinksSerializerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEntityReferenceLinksSerializerTest.cs @@ -18,144 +18,143 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataEntityReferenceLinksSerializerTest { - public class ODataEntityReferenceLinksSerializerTest + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() { - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() - { - // Arrange - ODataEntityReferenceLinksSerializer serializer = new ODataEntityReferenceLinksSerializer(); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: null, type: typeof(ODataEntityReferenceLinks), messageWriter: null, - writeContext: new ODataSerializerContext()), - "messageWriter"); - } + // Arrange + ODataEntityReferenceLinksSerializer serializer = new ODataEntityReferenceLinksSerializer(); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: null, type: typeof(ODataEntityReferenceLinks), messageWriter: null, + writeContext: new ODataSerializerContext()), + "messageWriter"); + } - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_WriteContext() - { - // Arrange - ODataEntityReferenceLinksSerializer serializer = new ODataEntityReferenceLinksSerializer(); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: null, type: typeof(ODataEntityReferenceLinks), - messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: null), - "writeContext"); - } + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_WriteContext() + { + // Arrange + ODataEntityReferenceLinksSerializer serializer = new ODataEntityReferenceLinksSerializer(); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: null, type: typeof(ODataEntityReferenceLinks), + messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: null), + "writeContext"); + } - [Fact] - public async Task WriteObjectAsync_Throws_ObjectCannotBeWritten_IfGraphIsNotUri() - { - // Arrange - ODataEntityReferenceLinksSerializer serializer = new ODataEntityReferenceLinksSerializer(); - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectAsync(graph: "not uri", type: typeof(ODataEntityReferenceLinks), - messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: new ODataSerializerContext()), - "ODataEntityReferenceLinksSerializer cannot write an object of type 'System.String'."); - } + [Fact] + public async Task WriteObjectAsync_Throws_ObjectCannotBeWritten_IfGraphIsNotUri() + { + // Arrange + ODataEntityReferenceLinksSerializer serializer = new ODataEntityReferenceLinksSerializer(); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectAsync(graph: "not uri", type: typeof(ODataEntityReferenceLinks), + messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: new ODataSerializerContext()), + "ODataEntityReferenceLinksSerializer cannot write an object of type 'System.String'."); + } - public static TheoryDataSet SerializationTestData + public static TheoryDataSet SerializationTestData + { + get { - get + Uri uri1 = new Uri("http://uri1"); + Uri uri2 = new Uri("http://uri2"); + return new TheoryDataSet { - Uri uri1 = new Uri("http://uri1"); - Uri uri2 = new Uri("http://uri2"); - return new TheoryDataSet - { - new Uri[] { uri1, uri2 }, + new Uri[] { uri1, uri2 }, - new ODataEntityReferenceLinks + new ODataEntityReferenceLinks + { + Links = new ODataEntityReferenceLink[] { - Links = new ODataEntityReferenceLink[] - { - new ODataEntityReferenceLink{ Url = uri1 }, - new ODataEntityReferenceLink{ Url = uri2 } - } + new ODataEntityReferenceLink{ Url = uri1 }, + new ODataEntityReferenceLink{ Url = uri2 } } - }; - } + } + }; } + } - [Theory] - [MemberData(nameof(SerializationTestData))] - public async Task ODataEntityReferenceLinkSerializer_Serializes_UrisAndEntityReferenceLinks(object uris) - { - // Arrange - ODataEntityReferenceLinksSerializer serializer = new ODataEntityReferenceLinksSerializer(); - ODataSerializerContext writeContext = new ODataSerializerContext(); - MemoryStream stream = new MemoryStream(); - IODataResponseMessage message = new ODataMessageWrapper(stream); + [Theory] + [MemberData(nameof(SerializationTestData))] + public async Task ODataEntityReferenceLinkSerializer_Serializes_UrisAndEntityReferenceLinks(object uris) + { + // Arrange + ODataEntityReferenceLinksSerializer serializer = new ODataEntityReferenceLinksSerializer(); + ODataSerializerContext writeContext = new ODataSerializerContext(); + MemoryStream stream = new MemoryStream(); + IODataResponseMessage message = new ODataMessageWrapper(stream); - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } - }; + ODataMessageWriterSettings settings = new ODataMessageWriterSettings + { + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } + }; - settings.SetContentType(ODataFormat.Json); - ODataMessageWriter writer = new ODataMessageWriter(message, settings); + settings.SetContentType(ODataFormat.Json); + ODataMessageWriter writer = new ODataMessageWriter(message, settings); - // Act - await serializer.WriteObjectAsync(uris, typeof(ODataEntityReferenceLinks), writer, writeContext); - stream.Seek(0, SeekOrigin.Begin); - string result = await new StreamReader(stream).ReadToEndAsync(); + // Act + await serializer.WriteObjectAsync(uris, typeof(ODataEntityReferenceLinks), writer, writeContext); + stream.Seek(0, SeekOrigin.Begin); + string result = await new StreamReader(stream).ReadToEndAsync(); - // Assert - Assert.Equal("{\"@odata.context\":\"http://any/$metadata#Collection($ref)\"," + - "\"value\":[{\"@odata.id\":\"http://uri1/\"},{\"@odata.id\":\"http://uri2/\"}]}", - result); - } + // Assert + Assert.Equal("{\"@odata.context\":\"http://any/$metadata#Collection($ref)\"," + + "\"value\":[{\"@odata.id\":\"http://uri1/\"},{\"@odata.id\":\"http://uri2/\"}]}", + result); + } - public static TheoryDataSet SerializationTestData2 + public static TheoryDataSet SerializationTestData2 + { + get { - get + Uri uri1 = new Uri("http://uri1"); + return new TheoryDataSet { - Uri uri1 = new Uri("http://uri1"); - return new TheoryDataSet - { - new Uri[] {uri1} - }; - } + new Uri[] {uri1} + }; } + } - [Theory] - [MemberData(nameof(SerializationTestData2))] - public async Task ODataEntityReferenceLinkSerializer_Serializes_UrisAndEntityReferenceLinks_WithCount(object uris) + [Theory] + [MemberData(nameof(SerializationTestData2))] + public async Task ODataEntityReferenceLinkSerializer_Serializes_UrisAndEntityReferenceLinks_WithCount(object uris) + { + // Arrange + //var config = RoutingConfigurationFactory.CreateWithRootContainer("OData"); + var request = RequestFactory.Create(/*config, "OData"*/); + ODataEntityReferenceLinksSerializer serializer = new ODataEntityReferenceLinksSerializer(); + ODataSerializerContext writeContext = new ODataSerializerContext(); + writeContext.Request = request; + writeContext.Request.ODataFeature().TotalCount = 1; + + MemoryStream stream = new MemoryStream(); + IODataResponseMessage message = new ODataMessageWrapper(stream); + + ODataMessageWriterSettings settings = new ODataMessageWriterSettings { - // Arrange - //var config = RoutingConfigurationFactory.CreateWithRootContainer("OData"); - var request = RequestFactory.Create(/*config, "OData"*/); - ODataEntityReferenceLinksSerializer serializer = new ODataEntityReferenceLinksSerializer(); - ODataSerializerContext writeContext = new ODataSerializerContext(); - writeContext.Request = request; - writeContext.Request.ODataFeature().TotalCount = 1; - - MemoryStream stream = new MemoryStream(); - IODataResponseMessage message = new ODataMessageWrapper(stream); - - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } - }; - - settings.SetContentType(ODataFormat.Json); - ODataMessageWriter writer = new ODataMessageWriter(message, settings); - - // Act - await serializer.WriteObjectAsync(uris, typeof(ODataEntityReferenceLinks), writer, writeContext); - stream.Seek(0, SeekOrigin.Begin); - string result = await new StreamReader(stream).ReadToEndAsync(); - Assert.Equal( - string.Format("{0},{1},{2}", - "{\"@odata.context\":\"http://any/$metadata#Collection($ref)\"", - "\"@odata.count\":1", - "\"value\":[{\"@odata.id\":\"http://uri1/\"}]}"), result); - } + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/") } + }; + + settings.SetContentType(ODataFormat.Json); + ODataMessageWriter writer = new ODataMessageWriter(message, settings); + + // Act + await serializer.WriteObjectAsync(uris, typeof(ODataEntityReferenceLinks), writer, writeContext); + stream.Seek(0, SeekOrigin.Begin); + string result = await new StreamReader(stream).ReadToEndAsync(); + Assert.Equal( + string.Format("{0},{1},{2}", + "{\"@odata.context\":\"http://any/$metadata#Collection($ref)\"", + "\"@odata.count\":1", + "\"value\":[{\"@odata.id\":\"http://uri1/\"}]}"), result); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEnumTypeSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEnumTypeSerializerTests.cs index ffc1fa302..735ef45e6 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEnumTypeSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataEnumTypeSerializerTests.cs @@ -18,167 +18,166 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataEnumTypeSerializerTests { - public class ODataEnumTypeSerializerTests + [Fact] + public void AddTypeNameAnnotationAsNeeded_DoesNotAddAnnotation() { - [Fact] - public void AddTypeNameAnnotationAsNeeded_DoesNotAddAnnotation() - { - // Arrange - ODataEnumValue enumValue = new ODataEnumValue("value"); - IEdmEnumTypeReference enumType = new EdmEnumTypeReference( - new EdmEnumType("TestModel", "EnumType"), isNullable: false); + // Arrange + ODataEnumValue enumValue = new ODataEnumValue("value"); + IEdmEnumTypeReference enumType = new EdmEnumTypeReference( + new EdmEnumType("TestModel", "EnumType"), isNullable: false); - // Act - ODataEnumSerializer.AddTypeNameAnnotationAsNeeded(enumValue, enumType, ODataMetadataLevel.Minimal); + // Act + ODataEnumSerializer.AddTypeNameAnnotationAsNeeded(enumValue, enumType, ODataMetadataLevel.Minimal); - // Assert - ODataTypeAnnotation annotation = enumValue.TypeAnnotation; - Assert.Null(annotation); - } + // Assert + ODataTypeAnnotation annotation = enumValue.TypeAnnotation; + Assert.Null(annotation); + } - [Fact] - public void AddTypeNameAnnotationAsNeeded_AddAnnotation_InFullMetadataMode() - { - // Arrange - ODataEnumValue enumValue = new ODataEnumValue("value"); - IEdmEnumTypeReference enumType = new EdmEnumTypeReference( - new EdmEnumType("TestModel", "EnumType"), isNullable: false); - - // Act - ODataEnumSerializer.AddTypeNameAnnotationAsNeeded(enumValue, enumType, ODataMetadataLevel.Full); - - // Assert - ODataTypeAnnotation annotation = enumValue.TypeAnnotation; - Assert.NotNull(annotation); - Assert.Equal("TestModel.EnumType", annotation.TypeName); - } - - [Fact] - public void AddTypeNameAnnotationAsNeeded_AddsNullAnnotation_InNoMetadataMode() - { - // Arrange - ODataEnumValue enumValue = new ODataEnumValue("value"); - IEdmEnumTypeReference enumType = new EdmEnumTypeReference( - new EdmEnumType("TestModel", "EnumType"), isNullable: false); - - // Act - ODataEnumSerializer.AddTypeNameAnnotationAsNeeded(enumValue, enumType, ODataMetadataLevel.None); - - // Assert - ODataTypeAnnotation annotation = enumValue.TypeAnnotation; - Assert.NotNull(annotation); - Assert.Null(annotation.TypeName); - } - - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_ForInputParameters() - { - // Arrange - IODataSerializerProvider provider = new Mock().Object; - ODataEnumSerializer serializer = new ODataEnumSerializer(provider); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync(() => serializer.WriteObjectAsync(graph: null, type: null, messageWriter: null, writeContext: null), - "messageWriter"); - - // Arrange & Act & Assert - ODataMessageWriter messageWriter = ODataTestUtil.GetMockODataMessageWriter(); - await ExceptionAssert.ThrowsArgumentNullAsync(() => serializer.WriteObjectAsync(graph: null, type: null, messageWriter, null), - "writeContext"); - - // Arrange & Act & Assert - ODataSerializerContext context = new ODataSerializerContext(); - context.RootElementName = null; - await ExceptionAssert.ThrowsAsync(() => serializer.WriteObjectAsync(graph: null, type: null, messageWriter, context), - "The 'RootElementName' property is required on 'ODataSerializerContext'. (Parameter 'writeContext')"); - } - - [Fact] - public void CreateODataEnumValue_ReturnsCorrectEnumMember() - { - // Arrange - var builder = new ODataConventionModelBuilder(); - builder.EnumType().Namespace = "NS"; - IEdmModel model = builder.GetEdmModel(); - IEdmEnumType enumType = model.SchemaElements.OfType().Single(); - - IServiceProvider serviceProvder = new Mock().Object; - var provider = new ODataSerializerProvider(serviceProvder); - ODataEnumSerializer serializer = new ODataEnumSerializer(provider); - ODataSerializerContext writeContext = new ODataSerializerContext - { - Model = model - }; - - // Act - ODataEnumValue value = serializer.CreateODataEnumValue(BookCategory.Newspaper, - new EdmEnumTypeReference(enumType, false), writeContext); - - // Assert - Assert.NotNull(value); - Assert.Equal("news", value.Value); - } - - [Fact] - public void CreateODataValue_ThrowsInvalidOperation_NonEnumType() - { - // Arrange - IODataSerializerProvider provider = new Mock().Object; - ODataEnumSerializer serializer = new ODataEnumSerializer(provider); - IEdmTypeReference expectedType = EdmCoreModel.Instance.GetString(false); - - // Act & Assert - ExceptionAssert.Throws(() => serializer.CreateODataValue(graph: null, expectedType: expectedType, writeContext: null), - "ODataEnumSerializer cannot write an object of type 'Edm.String'."); - } - - [Fact] - public void CreateODataValue_RetrunsNull_IfGraphNull() - { - // Arrange - ODataSerializerContext writeContext = new ODataSerializerContext(); - IODataSerializerProvider provider = new Mock().Object; - ODataEnumSerializer serializer = new ODataEnumSerializer(provider); - IEdmEnumType enumType = new EdmEnumType("NS", "Enum"); - IEdmTypeReference expectedType = new EdmEnumTypeReference(enumType, false); - - // Act - ODataValue actual = serializer.CreateODataValue(graph: null, expectedType: expectedType, writeContext); - - // Assert - Assert.IsType(actual); - } - - [Fact] - public void CreateODataValue_Retruns_CorrectODataValue() + [Fact] + public void AddTypeNameAnnotationAsNeeded_AddAnnotation_InFullMetadataMode() + { + // Arrange + ODataEnumValue enumValue = new ODataEnumValue("value"); + IEdmEnumTypeReference enumType = new EdmEnumTypeReference( + new EdmEnumType("TestModel", "EnumType"), isNullable: false); + + // Act + ODataEnumSerializer.AddTypeNameAnnotationAsNeeded(enumValue, enumType, ODataMetadataLevel.Full); + + // Assert + ODataTypeAnnotation annotation = enumValue.TypeAnnotation; + Assert.NotNull(annotation); + Assert.Equal("TestModel.EnumType", annotation.TypeName); + } + + [Fact] + public void AddTypeNameAnnotationAsNeeded_AddsNullAnnotation_InNoMetadataMode() + { + // Arrange + ODataEnumValue enumValue = new ODataEnumValue("value"); + IEdmEnumTypeReference enumType = new EdmEnumTypeReference( + new EdmEnumType("TestModel", "EnumType"), isNullable: false); + + // Act + ODataEnumSerializer.AddTypeNameAnnotationAsNeeded(enumValue, enumType, ODataMetadataLevel.None); + + // Assert + ODataTypeAnnotation annotation = enumValue.TypeAnnotation; + Assert.NotNull(annotation); + Assert.Null(annotation.TypeName); + } + + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_ForInputParameters() + { + // Arrange + IODataSerializerProvider provider = new Mock().Object; + ODataEnumSerializer serializer = new ODataEnumSerializer(provider); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync(() => serializer.WriteObjectAsync(graph: null, type: null, messageWriter: null, writeContext: null), + "messageWriter"); + + // Arrange & Act & Assert + ODataMessageWriter messageWriter = ODataTestUtil.GetMockODataMessageWriter(); + await ExceptionAssert.ThrowsArgumentNullAsync(() => serializer.WriteObjectAsync(graph: null, type: null, messageWriter, null), + "writeContext"); + + // Arrange & Act & Assert + ODataSerializerContext context = new ODataSerializerContext(); + context.RootElementName = null; + await ExceptionAssert.ThrowsAsync(() => serializer.WriteObjectAsync(graph: null, type: null, messageWriter, context), + "The 'RootElementName' property is required on 'ODataSerializerContext'. (Parameter 'writeContext')"); + } + + [Fact] + public void CreateODataEnumValue_ReturnsCorrectEnumMember() + { + // Arrange + var builder = new ODataConventionModelBuilder(); + builder.EnumType().Namespace = "NS"; + IEdmModel model = builder.GetEdmModel(); + IEdmEnumType enumType = model.SchemaElements.OfType().Single(); + + IServiceProvider serviceProvder = new Mock().Object; + var provider = new ODataSerializerProvider(serviceProvder); + ODataEnumSerializer serializer = new ODataEnumSerializer(provider); + ODataSerializerContext writeContext = new ODataSerializerContext { - // Arrange - ODataSerializerContext writeContext = new ODataSerializerContext(); - IODataSerializerProvider provider = new Mock().Object; - Mock serializer = new Mock(provider); - ODataEnumValue enumValue = new ODataEnumValue("Cartoon"); - serializer.Setup(s => s.CreateODataEnumValue(null, It.IsAny(), writeContext)).Returns(enumValue); - - IEdmEnumType enumType = new EdmEnumType("NS", "Enum"); - IEdmTypeReference expectedType = new EdmEnumTypeReference(enumType, false); - - // Act - ODataValue actual = serializer.Object.CreateODataValue(graph: null, expectedType: expectedType, writeContext); - - // Assert - Assert.Same(enumValue, actual); - } + Model = model + }; + + // Act + ODataEnumValue value = serializer.CreateODataEnumValue(BookCategory.Newspaper, + new EdmEnumTypeReference(enumType, false), writeContext); + + // Assert + Assert.NotNull(value); + Assert.Equal("news", value.Value); + } + + [Fact] + public void CreateODataValue_ThrowsInvalidOperation_NonEnumType() + { + // Arrange + IODataSerializerProvider provider = new Mock().Object; + ODataEnumSerializer serializer = new ODataEnumSerializer(provider); + IEdmTypeReference expectedType = EdmCoreModel.Instance.GetString(false); + + // Act & Assert + ExceptionAssert.Throws(() => serializer.CreateODataValue(graph: null, expectedType: expectedType, writeContext: null), + "ODataEnumSerializer cannot write an object of type 'Edm.String'."); + } + + [Fact] + public void CreateODataValue_RetrunsNull_IfGraphNull() + { + // Arrange + ODataSerializerContext writeContext = new ODataSerializerContext(); + IODataSerializerProvider provider = new Mock().Object; + ODataEnumSerializer serializer = new ODataEnumSerializer(provider); + IEdmEnumType enumType = new EdmEnumType("NS", "Enum"); + IEdmTypeReference expectedType = new EdmEnumTypeReference(enumType, false); + + // Act + ODataValue actual = serializer.CreateODataValue(graph: null, expectedType: expectedType, writeContext); + + // Assert + Assert.IsType(actual); } - [DataContract(Name = "category")] - public enum BookCategory + [Fact] + public void CreateODataValue_Retruns_CorrectODataValue() { - [EnumMember(Value = "cartoon")] - Cartoon, + // Arrange + ODataSerializerContext writeContext = new ODataSerializerContext(); + IODataSerializerProvider provider = new Mock().Object; + Mock serializer = new Mock(provider); + ODataEnumValue enumValue = new ODataEnumValue("Cartoon"); + serializer.Setup(s => s.CreateODataEnumValue(null, It.IsAny(), writeContext)).Returns(enumValue); + + IEdmEnumType enumType = new EdmEnumType("NS", "Enum"); + IEdmTypeReference expectedType = new EdmEnumTypeReference(enumType, false); + + // Act + ODataValue actual = serializer.Object.CreateODataValue(graph: null, expectedType: expectedType, writeContext); - [EnumMember(Value = "news")] - Newspaper + // Assert + Assert.Same(enumValue, actual); } } + +[DataContract(Name = "category")] +public enum BookCategory +{ + [EnumMember(Value = "cartoon")] + Cartoon, + + [EnumMember(Value = "news")] + Newspaper +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataErrorSerializerTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataErrorSerializerTest.cs index dca09ed54..23eafa110 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataErrorSerializerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataErrorSerializerTest.cs @@ -16,80 +16,79 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataErrorSerializerTest { - public class ODataErrorSerializerTest + [Fact] + public void WriteObjectAsync_SupportsHttpError() { - [Fact] - public void WriteObjectAsync_SupportsHttpError() - { - // Arrange - ODataErrorSerializer serializer = new ODataErrorSerializer(); - SerializableError error = new SerializableError(); + // Arrange + ODataErrorSerializer serializer = new ODataErrorSerializer(); + SerializableError error = new SerializableError(); - Mock mockResponseMessage = new Mock(); - mockResponseMessage.Setup(response => response.GetStreamAsync()).ReturnsAsync(new MemoryStream()); + Mock mockResponseMessage = new Mock(); + mockResponseMessage.Setup(response => response.GetStreamAsync()).ReturnsAsync(new MemoryStream()); - // Act & Assert - ExceptionAssert.DoesNotThrow(() => serializer.WriteObjectAsync(error, typeof(ODataError), new ODataMessageWriter(mockResponseMessage.Object), new ODataSerializerContext()) - .Wait()); - } + // Act & Assert + ExceptionAssert.DoesNotThrow(() => serializer.WriteObjectAsync(error, typeof(ODataError), new ODataMessageWriter(mockResponseMessage.Object), new ODataSerializerContext()) + .Wait()); + } - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_Graph() - { - // Arrange - ODataErrorSerializer serializer = new ODataErrorSerializer(); + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_Graph() + { + // Arrange + ODataErrorSerializer serializer = new ODataErrorSerializer(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: null, type: typeof(ODataError), messageWriter: null, writeContext: null), - "graph"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: null, type: typeof(ODataError), messageWriter: null, writeContext: null), + "graph"); + } - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() - { - // Arrange - ODataErrorSerializer serializer = new ODataErrorSerializer(); + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() + { + // Arrange + ODataErrorSerializer serializer = new ODataErrorSerializer(); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: 42, type: typeof(ODataError), messageWriter: null, writeContext: null), - "messageWriter"); - } + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: 42, type: typeof(ODataError), messageWriter: null, writeContext: null), + "messageWriter"); + } - [Fact] - public async Task WriteObject_ThrowsAsync_ErrorTypeMustBeODataErrorOrHttpError() - { - // Arrange - ODataErrorSerializer serializer = new ODataErrorSerializer(); + [Fact] + public async Task WriteObject_ThrowsAsync_ErrorTypeMustBeODataErrorOrHttpError() + { + // Arrange + ODataErrorSerializer serializer = new ODataErrorSerializer(); - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectAsync(42, typeof(ODataError), ODataTestUtil.GetMockODataMessageWriter(), new ODataSerializerContext()), - "The type 'System.Int32' is not supported by the ODataErrorSerializer. The type must be ODataError or HttpError."); - } + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectAsync(42, typeof(ODataError), ODataTestUtil.GetMockODataMessageWriter(), new ODataSerializerContext()), + "The type 'System.Int32' is not supported by the ODataErrorSerializer. The type must be ODataError or HttpError."); + } - [Fact] - public async Task ODataErrorSerializer_Works() - { - // Arrange - ODataErrorSerializer serializer = new ODataErrorSerializer(); - MemoryStream stream = new MemoryStream(); - IODataResponseMessageAsync message = new ODataMessageWrapper(stream); - ODataError error = new ODataError { Message = "Error!!!" }; - ODataMessageWriterSettings settings = new ODataMessageWriterSettings(); - settings.SetContentType(ODataFormat.Json); - ODataMessageWriter writer = new ODataMessageWriter(message, settings); + [Fact] + public async Task ODataErrorSerializer_Works() + { + // Arrange + ODataErrorSerializer serializer = new ODataErrorSerializer(); + MemoryStream stream = new MemoryStream(); + IODataResponseMessageAsync message = new ODataMessageWrapper(stream); + ODataError error = new ODataError { Message = "Error!!!" }; + ODataMessageWriterSettings settings = new ODataMessageWriterSettings(); + settings.SetContentType(ODataFormat.Json); + ODataMessageWriter writer = new ODataMessageWriter(message, settings); - // Act - await serializer.WriteObjectAsync(error, typeof(ODataError), writer, new ODataSerializerContext()); - stream.Seek(0, SeekOrigin.Begin); - string result = new StreamReader(stream).ReadToEnd(); + // Act + await serializer.WriteObjectAsync(error, typeof(ODataError), writer, new ODataSerializerContext()); + stream.Seek(0, SeekOrigin.Begin); + string result = new StreamReader(stream).ReadToEnd(); - // Assert - Assert.Equal("{\"error\":{\"code\":\"\",\"message\":\"Error!!!\"}}", result); - } + // Assert + Assert.Equal("{\"error\":{\"code\":\"\",\"message\":\"Error!!!\"}}", result); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataMetadataSerializerTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataMetadataSerializerTest.cs index 55d3a7b84..306e2c60d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataMetadataSerializerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataMetadataSerializerTest.cs @@ -17,80 +17,80 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataMetadataSerializerTest { - public class ODataMetadataSerializerTest + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() { - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() + // Arrange + ODataMetadataSerializer serializer = new ODataMetadataSerializer(); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(42, typeof(IEdmModel), messageWriter: null, writeContext: null), + "messageWriter"); + } + + [Fact] + public async Task ODataMetadataSerializer_Works() + { + // Arrange + ODataMetadataSerializer serializer = new ODataMetadataSerializer(); + IEdmModel model = new EdmModel(); + + // 1) XML + // Act + string payload = await this.WriteAndGetPayloadAsync(model, "application/xml", async omWriter => { - // Arrange - ODataMetadataSerializer serializer = new ODataMetadataSerializer(); + await serializer.WriteObjectAsync("42" /*useless*/, typeof(IEdmModel), omWriter, new ODataSerializerContext()); + }); - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(42, typeof(IEdmModel), messageWriter: null, writeContext: null), - "messageWriter"); - } + // Assert + Assert.Contains(" { - // Arrange - ODataMetadataSerializer serializer = new ODataMetadataSerializer(); - IEdmModel model = new EdmModel(); - - // 1) XML - // Act - string payload = await this.WriteAndGetPayloadAsync(model, "application/xml", async omWriter => - { - await serializer.WriteObjectAsync("42" /*useless*/, typeof(IEdmModel), omWriter, new ODataSerializerContext()); - }); - - // Assert - Assert.Contains(" - { - await serializer.WriteObjectAsync("42" /*useless*/, typeof(IEdmModel), omWriter, new ODataSerializerContext()); - }); - - // Assert - Assert.Equal(@"{ + await serializer.WriteObjectAsync("42" /*useless*/, typeof(IEdmModel), omWriter, new ODataSerializerContext()); + }); + + // Assert + Assert.Equal(@"{ ""$Version"": ""4.0"" }", payload); - } + } - [Fact] - public async Task ODataMetadataSerializer_Works_ForSingleton() + [Fact] + public async Task ODataMetadataSerializer_Works_ForSingleton() + { + // Arrange + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.Singleton("Me"); + IEdmModel model = builder.GetEdmModel(); + ODataMetadataSerializer serializer = new ODataMetadataSerializer(); + + // XML + // Act + string payload = await this.WriteAndGetPayloadAsync(model, "application/xml", async omWriter => { - // Arrange - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.Singleton("Me"); - IEdmModel model = builder.GetEdmModel(); - ODataMetadataSerializer serializer = new ODataMetadataSerializer(); - - // XML - // Act - string payload = await this.WriteAndGetPayloadAsync(model, "application/xml", async omWriter => - { - await serializer.WriteObjectAsync(model, typeof(IEdmModel), omWriter, new ODataSerializerContext()); - }); - - // Assert - Assert.Contains("", payload); - - // JSON - // Act - payload = await this.WriteAndGetPayloadAsync(model, "application/json", async omWriter => - { - await serializer.WriteObjectAsync(model, typeof(IEdmModel), omWriter, new ODataSerializerContext()); - }); - - // Assert - Assert.Contains(@" ""Default"": { + await serializer.WriteObjectAsync(model, typeof(IEdmModel), omWriter, new ODataSerializerContext()); + }); + + // Assert + Assert.Contains("", payload); + + // JSON + // Act + payload = await this.WriteAndGetPayloadAsync(model, "application/json", async omWriter => + { + await serializer.WriteObjectAsync(model, typeof(IEdmModel), omWriter, new ODataSerializerContext()); + }); + + // Assert + Assert.Contains(@" ""Default"": { ""Container"": { ""$Kind"": ""EntityContainer"", ""Me"": { @@ -98,36 +98,35 @@ public async Task ODataMetadataSerializer_Works_ForSingleton() } } }", payload); - } + } + + private async Task WriteAndGetPayloadAsync(IEdmModel edmModel, string contentType, Func test) + { + MemoryStream stream = new MemoryStream(); + Dictionary headers = new Dictionary + { + // the content type is necessary to write the metadata in async? + { "Content-Type", contentType} + }; + + IODataResponseMessage message = new ODataMessageWrapper(stream, headers); + + ODataMessageWriterSettings writerSettings = new ODataMessageWriterSettings(); + writerSettings.EnableMessageStreamDisposal = false; + writerSettings.BaseUri = new Uri("http://www.example.com/"); - private async Task WriteAndGetPayloadAsync(IEdmModel edmModel, string contentType, Func test) + using (var msgWriter = new ODataMessageWriter((IODataResponseMessageAsync)message, writerSettings, edmModel)) { - MemoryStream stream = new MemoryStream(); - Dictionary headers = new Dictionary - { - // the content type is necessary to write the metadata in async? - { "Content-Type", contentType} - }; - - IODataResponseMessage message = new ODataMessageWrapper(stream, headers); - - ODataMessageWriterSettings writerSettings = new ODataMessageWriterSettings(); - writerSettings.EnableMessageStreamDisposal = false; - writerSettings.BaseUri = new Uri("http://www.example.com/"); - - using (var msgWriter = new ODataMessageWriter((IODataResponseMessageAsync)message, writerSettings, edmModel)) - { - await test(msgWriter); - } - - stream.Seek(0, SeekOrigin.Begin); - using (StreamReader reader = new StreamReader(stream)) - { - return reader.ReadToEnd(); - } + await test(msgWriter); } - private class Customer - { } + stream.Seek(0, SeekOrigin.Begin); + using (StreamReader reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } } + + private class Customer + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataPrimitiveSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataPrimitiveSerializerTests.cs index 491a2258d..d86c3277b 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataPrimitiveSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataPrimitiveSerializerTests.cs @@ -23,437 +23,436 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataPrimitiveSerializerTests { - public class ODataPrimitiveSerializerTests + public static IEnumerable NonEdmPrimitiveConversionData { - public static IEnumerable NonEdmPrimitiveConversionData + get { - get - { - return EdmPrimitiveHelperTests.ConvertPrimitiveValue_NonStandardPrimitives_Data.Select(data => new[] { data[1], data[0] }); - } + return EdmPrimitiveHelperTests.ConvertPrimitiveValue_NonStandardPrimitives_Data.Select(data => new[] { data[1], data[0] }); } + } - public static TheoryDataSet NonEdmPrimitiveConversionDateTime + public static TheoryDataSet NonEdmPrimitiveConversionDateTime + { + get { - get + DateTime dtUtc = new DateTime(2014, 12, 12, 1, 2, 3, DateTimeKind.Utc); + DateTime dtLocal = new DateTime(2014, 12, 12, 1, 2, 3, DateTimeKind.Local); + DateTime unspecified = new DateTime(2014, 12, 12, 1, 2, 3, DateTimeKind.Unspecified); + return new TheoryDataSet { - DateTime dtUtc = new DateTime(2014, 12, 12, 1, 2, 3, DateTimeKind.Utc); - DateTime dtLocal = new DateTime(2014, 12, 12, 1, 2, 3, DateTimeKind.Local); - DateTime unspecified = new DateTime(2014, 12, 12, 1, 2, 3, DateTimeKind.Unspecified); - return new TheoryDataSet - { - { dtUtc }, - { dtLocal }, - { unspecified }, - }; - } + { dtUtc }, + { dtLocal }, + { unspecified }, + }; } + } - public static TheoryDataSet NonEdmPrimitiveData + public static TheoryDataSet NonEdmPrimitiveData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { (char)'1', "Edm.String", "\"1\"" }, - { (char[]) new char[] {'1' }, "Edm.String", "\"1\"" }, - { (UInt16)1, "Edm.Int32", "1" }, - { (UInt32)1, "Edm.Int64", "1" }, - { (UInt64)1, "Edm.Int64", "1" }, - //(Stream) new MemoryStream(new byte[] { 1 }), // TODO: Enable once we have support for streams - { new XElement(XName.Get("element","namespace")), "Edm.String", "\"\"" }, - }; - } + { (char)'1', "Edm.String", "\"1\"" }, + { (char[]) new char[] {'1' }, "Edm.String", "\"1\"" }, + { (UInt16)1, "Edm.Int32", "1" }, + { (UInt32)1, "Edm.Int64", "1" }, + { (UInt64)1, "Edm.Int64", "1" }, + //(Stream) new MemoryStream(new byte[] { 1 }), // TODO: Enable once we have support for streams + { new XElement(XName.Get("element","namespace")), "Edm.String", "\"\"" }, + }; } + } - public static TheoryDataSet EdmPrimitiveData + public static TheoryDataSet EdmPrimitiveData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { "1", "Edm.String", "\"1\"" }, - { true, "Edm.Boolean", "true" }, - { (Byte)1, "Edm.Byte", "1" }, - { (Decimal)1, "Edm.Decimal", "1" }, - { (Double)1, "Edm.Double", "1.0" }, - { (Guid)Guid.Empty, "Edm.Guid", "\"00000000-0000-0000-0000-000000000000\"" }, - { (Int16)1, "Edm.Int16", "1" }, - { (Int32)1, "Edm.Int32", "1" }, - { (Int64)1, "Edm.Int64", "1" }, - { (SByte)1, "Edm.SByte", "1" }, - { (Single)1, "Edm.Single", "1" }, - { new byte[] { 1 }, "Edm.Binary", "\"AQ==\"" }, - { new TimeSpan(), "Edm.Duration", "\"PT0S\"" }, - { new DateTimeOffset(), "Edm.DateTimeOffset", "\"0001-01-01T00:00:00Z\"" }, - { new Date(2014, 10, 13), "Edm.Date", "\"2014-10-13\"" }, - { new TimeOfDay(15, 38, 25, 109), "Edm.TimeOfDay", "\"15:38:25.1090000\"" }, - }; - } + { "1", "Edm.String", "\"1\"" }, + { true, "Edm.Boolean", "true" }, + { (Byte)1, "Edm.Byte", "1" }, + { (Decimal)1, "Edm.Decimal", "1" }, + { (Double)1, "Edm.Double", "1.0" }, + { (Guid)Guid.Empty, "Edm.Guid", "\"00000000-0000-0000-0000-000000000000\"" }, + { (Int16)1, "Edm.Int16", "1" }, + { (Int32)1, "Edm.Int32", "1" }, + { (Int64)1, "Edm.Int64", "1" }, + { (SByte)1, "Edm.SByte", "1" }, + { (Single)1, "Edm.Single", "1" }, + { new byte[] { 1 }, "Edm.Binary", "\"AQ==\"" }, + { new TimeSpan(), "Edm.Duration", "\"PT0S\"" }, + { new DateTimeOffset(), "Edm.DateTimeOffset", "\"0001-01-01T00:00:00Z\"" }, + { new Date(2014, 10, 13), "Edm.Date", "\"2014-10-13\"" }, + { new TimeOfDay(15, 38, 25, 109), "Edm.TimeOfDay", "\"15:38:25.1090000\"" }, + }; } + } - [Fact] - public void Property_ODataPayloadKind() - { - // Arrange - var serializer = new ODataPrimitiveSerializer(); + [Fact] + public void Property_ODataPayloadKind() + { + // Arrange + var serializer = new ODataPrimitiveSerializer(); - // Act & Assert - Assert.Equal(ODataPayloadKind.Property, serializer.ODataPayloadKind); - } + // Act & Assert + Assert.Equal(ODataPayloadKind.Property, serializer.ODataPayloadKind); + } - [Fact] - public async Task WriteObjectAsync_ThrowsAsync_RootElementNameMissing() - { - // Arrange - ODataSerializerContext writeContext = new ODataSerializerContext(); - ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectAsync(42, typeof(int), ODataTestUtil.GetMockODataMessageWriter(), writeContext), - "The 'RootElementName' property is required on 'ODataSerializerContext'. (Parameter 'writeContext')"); - } + [Fact] + public async Task WriteObjectAsync_ThrowsAsync_RootElementNameMissing() + { + // Arrange + ODataSerializerContext writeContext = new ODataSerializerContext(); + ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectAsync(42, typeof(int), ODataTestUtil.GetMockODataMessageWriter(), writeContext), + "The 'RootElementName' property is required on 'ODataSerializerContext'. (Parameter 'writeContext')"); + } - [Fact] - public async Task WriteObjectAsync_Calls_CreateODataPrimitiveValue() - { - // Arrange - ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = EdmCoreModel.Instance }; - Mock serializer = new Mock(); - serializer.CallBase = true; - serializer.Setup(s => s.CreateODataPrimitiveValue( - 42, It.Is(t => t.PrimitiveKind() == EdmPrimitiveTypeKind.Int32), writeContext)) - .Returns(new ODataPrimitiveValue(42)).Verifiable(); - - // Act - await serializer.Object.WriteObjectAsync(42, typeof(int), ODataTestUtil.GetMockODataMessageWriter(), writeContext); - - // Assert - serializer.Verify(); - } + [Fact] + public async Task WriteObjectAsync_Calls_CreateODataPrimitiveValue() + { + // Arrange + ODataSerializerContext writeContext = new ODataSerializerContext { RootElementName = "Property", Model = EdmCoreModel.Instance }; + Mock serializer = new Mock(); + serializer.CallBase = true; + serializer.Setup(s => s.CreateODataPrimitiveValue( + 42, It.Is(t => t.PrimitiveKind() == EdmPrimitiveTypeKind.Int32), writeContext)) + .Returns(new ODataPrimitiveValue(42)).Verifiable(); + + // Act + await serializer.Object.WriteObjectAsync(42, typeof(int), ODataTestUtil.GetMockODataMessageWriter(), writeContext); + + // Assert + serializer.Verify(); + } - [Fact] - public void CreateODataValue_PrimitiveValue() - { - // Arrange - IEdmPrimitiveTypeReference edmPrimitiveType = typeof(int).GetEdmPrimitiveTypeReference(); - var serializer = new ODataPrimitiveSerializer(); + [Fact] + public void CreateODataValue_PrimitiveValue() + { + // Arrange + IEdmPrimitiveTypeReference edmPrimitiveType = typeof(int).GetEdmPrimitiveTypeReference(); + var serializer = new ODataPrimitiveSerializer(); - // Act - var odataValue = serializer.CreateODataValue(20, edmPrimitiveType, writeContext: null); + // Act + var odataValue = serializer.CreateODataValue(20, edmPrimitiveType, writeContext: null); - // Assert - Assert.NotNull(odataValue); - ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); - Assert.Equal(20, primitiveValue.Value); - } + // Assert + Assert.NotNull(odataValue); + ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); + Assert.Equal(20, primitiveValue.Value); + } - [Fact] - public void CreateODataValue_ReturnsODataNullValue_ForNullValue() - { - // Arrange - IEdmPrimitiveTypeReference edmPrimitiveType = typeof(string).GetEdmPrimitiveTypeReference(); - var serializer = new ODataPrimitiveSerializer(); + [Fact] + public void CreateODataValue_ReturnsODataNullValue_ForNullValue() + { + // Arrange + IEdmPrimitiveTypeReference edmPrimitiveType = typeof(string).GetEdmPrimitiveTypeReference(); + var serializer = new ODataPrimitiveSerializer(); - // Act - var odataValue = serializer.CreateODataValue(null, edmPrimitiveType, new ODataSerializerContext()); + // Act + var odataValue = serializer.CreateODataValue(null, edmPrimitiveType, new ODataSerializerContext()); - // Assert - Assert.IsType(odataValue); - } + // Assert + Assert.IsType(odataValue); + } - [Fact] - public void CreateODataValue_ReturnsDateTimeOffset_ForDateTime_ByDefault() - { - // Arrange - IEdmPrimitiveTypeReference edmPrimitiveType = typeof(DateTime).GetEdmPrimitiveTypeReference(); - ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); - DateTime dt = new DateTime(2014, 10, 27); + [Fact] + public void CreateODataValue_ReturnsDateTimeOffset_ForDateTime_ByDefault() + { + // Arrange + IEdmPrimitiveTypeReference edmPrimitiveType = typeof(DateTime).GetEdmPrimitiveTypeReference(); + ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); + DateTime dt = new DateTime(2014, 10, 27); - // Act - ODataValue odataValue = serializer.CreateODataValue(dt, edmPrimitiveType, new ODataSerializerContext()); + // Act + ODataValue odataValue = serializer.CreateODataValue(dt, edmPrimitiveType, new ODataSerializerContext()); - // Assert - ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); - Assert.Equal(new DateTimeOffset(dt), primitiveValue.Value); - } + // Assert + ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); + Assert.Equal(new DateTimeOffset(dt), primitiveValue.Value); + } - [Theory] - [InlineData("UTC")] // +0:00 - [InlineData("Pacific Standard Time")] // -8:00 - [InlineData("China Standard Time")] // +8:00 - public void CreateODataValue_ReturnsDateTimeOffsetMinValue_ForDateTimeMinValue(string timeZoneId) - { - // Arrange - IEdmPrimitiveTypeReference edmPrimitiveType = typeof(DateTime).GetEdmPrimitiveTypeReference(); - ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); - DateTime dt = DateTime.MinValue; - TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); - var request = RequestFactory.Create(); - ODataSerializerContext context = new ODataSerializerContext { Request = request, TimeZone = timeZone }; + [Theory] + [InlineData("UTC")] // +0:00 + [InlineData("Pacific Standard Time")] // -8:00 + [InlineData("China Standard Time")] // +8:00 + public void CreateODataValue_ReturnsDateTimeOffsetMinValue_ForDateTimeMinValue(string timeZoneId) + { + // Arrange + IEdmPrimitiveTypeReference edmPrimitiveType = typeof(DateTime).GetEdmPrimitiveTypeReference(); + ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); + DateTime dt = DateTime.MinValue; + TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); + var request = RequestFactory.Create(); + ODataSerializerContext context = new ODataSerializerContext { Request = request, TimeZone = timeZone }; - // Act - ODataValue odataValue = serializer.CreateODataValue(dt, edmPrimitiveType, context); + // Act + ODataValue odataValue = serializer.CreateODataValue(dt, edmPrimitiveType, context); - // Assert - ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); + // Assert + ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); - if (timeZone.BaseUtcOffset.Hours < 0) - { - Assert.Equal(new DateTimeOffset(dt, timeZone.GetUtcOffset(dt)), primitiveValue.Value); - } - else - { - Assert.Equal(DateTimeOffset.MinValue, primitiveValue.Value); - } + if (timeZone.BaseUtcOffset.Hours < 0) + { + Assert.Equal(new DateTimeOffset(dt, timeZone.GetUtcOffset(dt)), primitiveValue.Value); } - - [Theory] - [InlineData("UTC")] // +0:00 - [InlineData("Pacific Standard Time")] // -8:00 - [InlineData("China Standard Time")] // +8:00 - public void CreateODataValue_ReturnsDateTimeOffsetMaxValue_ForDateTimeMaxValue(string timeZoneId) + else { - // Arrange - IEdmPrimitiveTypeReference edmPrimitiveType = typeof(DateTime).GetEdmPrimitiveTypeReference(); - ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); - DateTime dt = DateTime.MaxValue; - TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); - var request = RequestFactory.Create(); - ODataSerializerContext context = new ODataSerializerContext { Request = request, TimeZone = timeZone }; + Assert.Equal(DateTimeOffset.MinValue, primitiveValue.Value); + } + } - // Act - ODataValue odataValue = serializer.CreateODataValue(dt, edmPrimitiveType, context); + [Theory] + [InlineData("UTC")] // +0:00 + [InlineData("Pacific Standard Time")] // -8:00 + [InlineData("China Standard Time")] // +8:00 + public void CreateODataValue_ReturnsDateTimeOffsetMaxValue_ForDateTimeMaxValue(string timeZoneId) + { + // Arrange + IEdmPrimitiveTypeReference edmPrimitiveType = typeof(DateTime).GetEdmPrimitiveTypeReference(); + ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); + DateTime dt = DateTime.MaxValue; + TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); + var request = RequestFactory.Create(); + ODataSerializerContext context = new ODataSerializerContext { Request = request, TimeZone = timeZone }; - // Assert - ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); + // Act + ODataValue odataValue = serializer.CreateODataValue(dt, edmPrimitiveType, context); - if (timeZone.BaseUtcOffset.Hours > 0) - { - Assert.Equal(new DateTimeOffset(dt, timeZone.GetUtcOffset(dt)), primitiveValue.Value); - } - else - { - Assert.Equal(DateTimeOffset.MaxValue, primitiveValue.Value); - } - } + // Assert + ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); - [Theory] - [MemberData(nameof(NonEdmPrimitiveConversionDateTime))] - public void CreateODataValue_ReturnsDateTimeOffset_ForDateTime_WithDifferentTimeZone(DateTime value) + if (timeZone.BaseUtcOffset.Hours > 0) { - // Arrange - IEdmPrimitiveTypeReference edmPrimitiveType = typeof(DateTime).GetEdmPrimitiveTypeReference(); - ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); + Assert.Equal(new DateTimeOffset(dt, timeZone.GetUtcOffset(dt)), primitiveValue.Value); + } + else + { + Assert.Equal(DateTimeOffset.MaxValue, primitiveValue.Value); + } + } - TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); - var request = RequestFactory.Create(); + [Theory] + [MemberData(nameof(NonEdmPrimitiveConversionDateTime))] + public void CreateODataValue_ReturnsDateTimeOffset_ForDateTime_WithDifferentTimeZone(DateTime value) + { + // Arrange + IEdmPrimitiveTypeReference edmPrimitiveType = typeof(DateTime).GetEdmPrimitiveTypeReference(); + ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); - ODataSerializerContext context = new ODataSerializerContext { Request = request, TimeZone = tzi }; + TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); + var request = RequestFactory.Create(); - DateTimeOffset expected = value.Kind == DateTimeKind.Unspecified - ? new DateTimeOffset(value, tzi.GetUtcOffset(value)) - : TimeZoneInfo.ConvertTime(new DateTimeOffset(value), tzi); + ODataSerializerContext context = new ODataSerializerContext { Request = request, TimeZone = tzi }; - // Act - ODataValue odataValue = serializer.CreateODataValue(value, edmPrimitiveType, context); + DateTimeOffset expected = value.Kind == DateTimeKind.Unspecified + ? new DateTimeOffset(value, tzi.GetUtcOffset(value)) + : TimeZoneInfo.ConvertTime(new DateTimeOffset(value), tzi); - // Assert - ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); - Assert.Equal(expected, primitiveValue.Value); - } + // Act + ODataValue odataValue = serializer.CreateODataValue(value, edmPrimitiveType, context); - [Fact] - public void CreateODataValue_ReturnsDate_ForDateTime() - { - // Arrange - IEdmPrimitiveTypeReference edmPrimitiveType = typeof(Date).GetEdmPrimitiveTypeReference(); - ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); - DateTime dt = new DateTime(2014, 10, 27); - - // Act - ODataValue odataValue = serializer.CreateODataValue(dt, edmPrimitiveType, new ODataSerializerContext()); - - // Assert - ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); - Assert.IsType(primitiveValue.Value); - Assert.Equal(new Date(dt.Year, dt.Month, dt.Day), primitiveValue.Value); - } + // Assert + ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); + Assert.Equal(expected, primitiveValue.Value); + } - [Fact] - public void CreateODataValue_ReturnsTimeOfDay_ForTimeSpan() - { - // Arrange - IEdmPrimitiveTypeReference edmPrimitiveType = typeof(TimeOfDay).GetEdmPrimitiveTypeReference(); - ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); - TimeSpan ts = new TimeSpan(0, 10, 11, 12, 13); - - // Act - ODataValue odataValue = serializer.CreateODataValue(ts, edmPrimitiveType, new ODataSerializerContext()); - - // Assert - ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); - Assert.IsType(primitiveValue.Value); - Assert.Equal(new TimeOfDay(ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds), primitiveValue.Value); - } + [Fact] + public void CreateODataValue_ReturnsDate_ForDateTime() + { + // Arrange + IEdmPrimitiveTypeReference edmPrimitiveType = typeof(Date).GetEdmPrimitiveTypeReference(); + ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); + DateTime dt = new DateTime(2014, 10, 27); + + // Act + ODataValue odataValue = serializer.CreateODataValue(dt, edmPrimitiveType, new ODataSerializerContext()); + + // Assert + ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); + Assert.IsType(primitiveValue.Value); + Assert.Equal(new Date(dt.Year, dt.Month, dt.Day), primitiveValue.Value); + } + + [Fact] + public void CreateODataValue_ReturnsTimeOfDay_ForTimeSpan() + { + // Arrange + IEdmPrimitiveTypeReference edmPrimitiveType = typeof(TimeOfDay).GetEdmPrimitiveTypeReference(); + ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); + TimeSpan ts = new TimeSpan(0, 10, 11, 12, 13); + + // Act + ODataValue odataValue = serializer.CreateODataValue(ts, edmPrimitiveType, new ODataSerializerContext()); + + // Assert + ODataPrimitiveValue primitiveValue = Assert.IsType(odataValue); + Assert.IsType(primitiveValue.Value); + Assert.Equal(new TimeOfDay(ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds), primitiveValue.Value); + } - [Theory] - [MemberData(nameof(EdmPrimitiveData))] - [MemberData(nameof(NonEdmPrimitiveData))] - public async Task WriteObjectAsync_EdmPrimitives(object graph, string type, string value) + [Theory] + [MemberData(nameof(EdmPrimitiveData))] + [MemberData(nameof(NonEdmPrimitiveData))] + public async Task WriteObjectAsync_EdmPrimitives(object graph, string type, string value) + { + // Arrange + ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); + ODataSerializerContext writecontext = new ODataSerializerContext() { - // Arrange - ODataPrimitiveSerializer serializer = new ODataPrimitiveSerializer(); - ODataSerializerContext writecontext = new ODataSerializerContext() - { - RootElementName = "PropertyName", - Model = EdmCoreModel.Instance - }; + RootElementName = "PropertyName", + Model = EdmCoreModel.Instance + }; - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } - }; + ODataMessageWriterSettings settings = new ODataMessageWriterSettings + { + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } + }; - MemoryStream stream = new MemoryStream(); - ODataMessageWriter writer = new ODataMessageWriter( - new ODataMessageWrapper(stream) as IODataResponseMessage, settings); + MemoryStream stream = new MemoryStream(); + ODataMessageWriter writer = new ODataMessageWriter( + new ODataMessageWrapper(stream) as IODataResponseMessage, settings); - string expect = "{\"@odata.context\":\"http://any/$metadata#" + type + "\","; - if (type == "Edm.Null") - { - expect += "\"@odata.null\":" + value + "}"; - } - else - { - expect += "\"value\":" + value + "}"; - } + string expect = "{\"@odata.context\":\"http://any/$metadata#" + type + "\","; + if (type == "Edm.Null") + { + expect += "\"@odata.null\":" + value + "}"; + } + else + { + expect += "\"value\":" + value + "}"; + } - // Act - await serializer.WriteObjectAsync(graph, typeof(int), writer, writecontext); + // Act + await serializer.WriteObjectAsync(graph, typeof(int), writer, writecontext); - // Assert - stream.Seek(0, SeekOrigin.Begin); - string result = new StreamReader(stream).ReadToEnd(); - Assert.Equal(expect, result); - } + // Assert + stream.Seek(0, SeekOrigin.Begin); + string result = new StreamReader(stream).ReadToEnd(); + Assert.Equal(expect, result); + } - [Fact] - public void AddTypeNameAnnotationAsNeeded_AddsAnnotation_InJsonLightMetadataMode() - { - // Arrange - IEdmPrimitiveTypeReference edmPrimitiveType = typeof(short).GetEdmPrimitiveTypeReference(); - ODataPrimitiveValue primitive = new ODataPrimitiveValue((short)1); + [Fact] + public void AddTypeNameAnnotationAsNeeded_AddsAnnotation_InJsonLightMetadataMode() + { + // Arrange + IEdmPrimitiveTypeReference edmPrimitiveType = typeof(short).GetEdmPrimitiveTypeReference(); + ODataPrimitiveValue primitive = new ODataPrimitiveValue((short)1); - // Act - ODataPrimitiveSerializer.AddTypeNameAnnotationAsNeeded(primitive, edmPrimitiveType, ODataMetadataLevel.Full); + // Act + ODataPrimitiveSerializer.AddTypeNameAnnotationAsNeeded(primitive, edmPrimitiveType, ODataMetadataLevel.Full); - // Assert - ODataTypeAnnotation annotation = primitive.TypeAnnotation; - Assert.NotNull(annotation); // Guard - Assert.Equal("Edm.Int16", annotation.TypeName); - } + // Assert + ODataTypeAnnotation annotation = primitive.TypeAnnotation; + Assert.NotNull(annotation); // Guard + Assert.Equal("Edm.Int16", annotation.TypeName); + } - [Theory] - [InlineData(true, true)] - [InlineData(0, true)] - [InlineData("", true)] - [InlineData(0.1D, true)] - [InlineData(double.PositiveInfinity, false)] - [InlineData(double.NegativeInfinity, false)] - [InlineData(double.NaN, false)] - [InlineData((short)1, false)] - public void CanTypeBeInferredInJson(object value, bool expectedResult) - { - // Act - bool actualResult = ODataPrimitiveSerializer.CanTypeBeInferredInJson(value); + [Theory] + [InlineData(true, true)] + [InlineData(0, true)] + [InlineData("", true)] + [InlineData(0.1D, true)] + [InlineData(double.PositiveInfinity, false)] + [InlineData(double.NegativeInfinity, false)] + [InlineData(double.NaN, false)] + [InlineData((short)1, false)] + public void CanTypeBeInferredInJson(object value, bool expectedResult) + { + // Act + bool actualResult = ODataPrimitiveSerializer.CanTypeBeInferredInJson(value); - // Assert - Assert.Equal(expectedResult, actualResult); - } + // Assert + Assert.Equal(expectedResult, actualResult); + } - [Fact] - public void CreatePrimitive_ReturnsNull_ForNullValue() - { - // Act - IEdmPrimitiveTypeReference edmPrimitiveType = typeof(int).GetEdmPrimitiveTypeReference(); - ODataValue value = ODataPrimitiveSerializer.CreatePrimitive(null, edmPrimitiveType, writeContext: null); + [Fact] + public void CreatePrimitive_ReturnsNull_ForNullValue() + { + // Act + IEdmPrimitiveTypeReference edmPrimitiveType = typeof(int).GetEdmPrimitiveTypeReference(); + ODataValue value = ODataPrimitiveSerializer.CreatePrimitive(null, edmPrimitiveType, writeContext: null); - // Assert - Assert.Null(value); - } + // Assert + Assert.Null(value); + } - [Theory] - [MemberData(nameof(EdmPrimitiveData))] - public void ConvertUnsupportedPrimitives_DoesntChangeStandardEdmPrimitives(object graph, string type, string value) - { - // Arrange & Act & Assert - Assert.NotNull(type); - Assert.NotNull(value); - Assert.Equal(graph, ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(graph, timeZoneInfo: null)); - } + [Theory] + [MemberData(nameof(EdmPrimitiveData))] + public void ConvertUnsupportedPrimitives_DoesntChangeStandardEdmPrimitives(object graph, string type, string value) + { + // Arrange & Act & Assert + Assert.NotNull(type); + Assert.NotNull(value); + Assert.Equal(graph, ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(graph, timeZoneInfo: null)); + } - [Theory] - [MemberData(nameof(NonEdmPrimitiveConversionData))] - public void ConvertUnsupportedPrimitives_NonStandardEdmPrimitives(object graph, object result) - { - // Arrange & Act & Assert - Assert.Equal(result, ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(graph, timeZoneInfo: null)); - } + [Theory] + [MemberData(nameof(NonEdmPrimitiveConversionData))] + public void ConvertUnsupportedPrimitives_NonStandardEdmPrimitives(object graph, object result) + { + // Arrange & Act & Assert + Assert.Equal(result, ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(graph, timeZoneInfo: null)); + } - [Theory] - [MemberData(nameof(NonEdmPrimitiveConversionDateTime))] - public void ConvertUnsupportedDateTime_NonStandardEdmPrimitives(DateTime graph) - { - // Arrange & Act - TimeZoneInfo timeZone = TimeZoneInfo.Local; - object value = ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(graph, timeZoneInfo: null); - - DateTimeOffset expected = graph.Kind == DateTimeKind.Unspecified - ? new DateTimeOffset(graph, timeZone.GetUtcOffset(graph)) - : TimeZoneInfo.ConvertTime(new DateTimeOffset(graph), timeZone); - - // Assert - DateTimeOffset actual = Assert.IsType(value); - Assert.Equal(new DateTimeOffset(graph), actual); - Assert.Equal(expected, actual); - } + [Theory] + [MemberData(nameof(NonEdmPrimitiveConversionDateTime))] + public void ConvertUnsupportedDateTime_NonStandardEdmPrimitives(DateTime graph) + { + // Arrange & Act + TimeZoneInfo timeZone = TimeZoneInfo.Local; + object value = ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(graph, timeZoneInfo: null); + + DateTimeOffset expected = graph.Kind == DateTimeKind.Unspecified + ? new DateTimeOffset(graph, timeZone.GetUtcOffset(graph)) + : TimeZoneInfo.ConvertTime(new DateTimeOffset(graph), timeZone); + + // Assert + DateTimeOffset actual = Assert.IsType(value); + Assert.Equal(new DateTimeOffset(graph), actual); + Assert.Equal(expected, actual); + } - [Theory] - [MemberData(nameof(NonEdmPrimitiveConversionDateTime))] - public void ConvertUnsupportedDateTime_NonStandardEdmPrimitives_TimeZone(DateTime graph) - { - // Arrange - TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); + [Theory] + [MemberData(nameof(NonEdmPrimitiveConversionDateTime))] + public void ConvertUnsupportedDateTime_NonStandardEdmPrimitives_TimeZone(DateTime graph) + { + // Arrange + TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); - DateTimeOffset expected = graph.Kind == DateTimeKind.Unspecified - ? new DateTimeOffset(graph, tzi.GetUtcOffset(graph)) - : TimeZoneInfo.ConvertTime(new DateTimeOffset(graph), tzi); + DateTimeOffset expected = graph.Kind == DateTimeKind.Unspecified + ? new DateTimeOffset(graph, tzi.GetUtcOffset(graph)) + : TimeZoneInfo.ConvertTime(new DateTimeOffset(graph), tzi); - // Act - object value = ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(graph, tzi); + // Act + object value = ODataPrimitiveSerializer.ConvertUnsupportedPrimitives(graph, tzi); - // Assert - DateTimeOffset actual = Assert.IsType(value); - Assert.Equal(expected, actual); - } + // Assert + DateTimeOffset actual = Assert.IsType(value); + Assert.Equal(expected, actual); + } - [Theory] - [InlineData(0, ODataMetadataLevel.Full, true)] - [InlineData((short)1, ODataMetadataLevel.Full, false)] - [InlineData((short)1, ODataMetadataLevel.Minimal, true)] - [InlineData((short)1, ODataMetadataLevel.None, true)] - public void ShouldSuppressTypeNameSerialization(object value, ODataMetadataLevel metadataLevel, - bool expectedResult) - { - // Act - bool actualResult = ODataPrimitiveSerializer.ShouldSuppressTypeNameSerialization(value, metadataLevel); + [Theory] + [InlineData(0, ODataMetadataLevel.Full, true)] + [InlineData((short)1, ODataMetadataLevel.Full, false)] + [InlineData((short)1, ODataMetadataLevel.Minimal, true)] + [InlineData((short)1, ODataMetadataLevel.None, true)] + public void ShouldSuppressTypeNameSerialization(object value, ODataMetadataLevel metadataLevel, + bool expectedResult) + { + // Act + bool actualResult = ODataPrimitiveSerializer.ShouldSuppressTypeNameSerialization(value, metadataLevel); - // Assert - Assert.Equal(expectedResult, actualResult); - } + // Assert + Assert.Equal(expectedResult, actualResult); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataRawValueSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataRawValueSerializerTests.cs index 29eb5762d..1972486a4 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataRawValueSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataRawValueSerializerTests.cs @@ -19,147 +19,146 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataRawValueSerializerTests { - public class ODataRawValueSerializerTests + [Theory] + [InlineData(5)] + [InlineData(5u)] + [InlineData(5L)] + [InlineData(5f)] + [InlineData(5d)] + [InlineData("test")] + [InlineData(false)] + [InlineData('t')] + public async Task SerializesPrimitiveTypes(object value) { - [Theory] - [InlineData(5)] - [InlineData(5u)] - [InlineData(5L)] - [InlineData(5f)] - [InlineData(5d)] - [InlineData("test")] - [InlineData(false)] - [InlineData('t')] - public async Task SerializesPrimitiveTypes(object value) - { - // Arrange - ODataRawValueSerializer serializer = new ODataRawValueSerializer(); - Mock mockRequest = new Mock(); - Stream stream = new MemoryStream(); - mockRequest.Setup(r => r.GetStreamAsync()).ReturnsAsync(stream); - ODataMessageWriter messageWriter = new ODataMessageWriter(mockRequest.Object); - - // Act - await serializer.WriteObjectAsync(value, value.GetType(), messageWriter, null); - stream.Seek(0, SeekOrigin.Begin); - TextReader reader = new StreamReader(stream); - string result = await reader.ReadToEndAsync(); - - // Assert - Assert.Equal(value.ToString(), result, ignoreCase: true); - } + // Arrange + ODataRawValueSerializer serializer = new ODataRawValueSerializer(); + Mock mockRequest = new Mock(); + Stream stream = new MemoryStream(); + mockRequest.Setup(r => r.GetStreamAsync()).ReturnsAsync(stream); + ODataMessageWriter messageWriter = new ODataMessageWriter(mockRequest.Object); + + // Act + await serializer.WriteObjectAsync(value, value.GetType(), messageWriter, null); + stream.Seek(0, SeekOrigin.Begin); + TextReader reader = new StreamReader(stream); + string result = await reader.ReadToEndAsync(); + + // Assert + Assert.Equal(value.ToString(), result, ignoreCase: true); + } - [Fact] - public async Task SerializesNullablePrimitiveTypes() - { - // Arrange - int? value = 5; - ODataRawValueSerializer serializer = new ODataRawValueSerializer(); - Mock mockRequest = new Mock(); - Stream stream = new MemoryStream(); - mockRequest.Setup(r => r.GetStreamAsync()).ReturnsAsync(stream); - ODataMessageWriter messageWriter = new ODataMessageWriter(mockRequest.Object); - - // Act - await serializer.WriteObjectAsync(value, value.GetType(), messageWriter, null); - stream.Seek(0, SeekOrigin.Begin); - TextReader reader = new StreamReader(stream); - - // Assert - Assert.Equal(value.ToString(), await reader.ReadToEndAsync()); - } + [Fact] + public async Task SerializesNullablePrimitiveTypes() + { + // Arrange + int? value = 5; + ODataRawValueSerializer serializer = new ODataRawValueSerializer(); + Mock mockRequest = new Mock(); + Stream stream = new MemoryStream(); + mockRequest.Setup(r => r.GetStreamAsync()).ReturnsAsync(stream); + ODataMessageWriter messageWriter = new ODataMessageWriter(mockRequest.Object); + + // Act + await serializer.WriteObjectAsync(value, value.GetType(), messageWriter, null); + stream.Seek(0, SeekOrigin.Begin); + TextReader reader = new StreamReader(stream); + + // Assert + Assert.Equal(value.ToString(), await reader.ReadToEndAsync()); + } - public static TheoryDataSet DateTimeTestData + public static TheoryDataSet DateTimeTestData + { + get { - get + // Because Xunit uses the test data for a unique Id, we'll use a stable time + // derived from UtcNow() to allow a test discovery pass and run work + // for a 24 hour period. + DateTime dt1 = DateTime.Today; + DateTimeOffset dto1 = new DateTimeOffset(dt1).ToLocalTime(); + + DateTime dt2 = DateTime.Today.AddDays(1); + DateTimeOffset dto2 = new DateTimeOffset(dt2).ToLocalTime(); + + return new TheoryDataSet { - // Because Xunit uses the test data for a unique Id, we'll use a stable time - // derived from UtcNow() to allow a test discovery pass and run work - // for a 24 hour period. - DateTime dt1 = DateTime.Today; - DateTimeOffset dto1 = new DateTimeOffset(dt1).ToLocalTime(); - - DateTime dt2 = DateTime.Today.AddDays(1); - DateTimeOffset dto2 = new DateTimeOffset(dt2).ToLocalTime(); - - return new TheoryDataSet - { - { dt1, dto1 }, - { new DateTime?(dt2), dto2} - }; - } + { dt1, dto1 }, + { new DateTime?(dt2), dto2} + }; } + } - [Theory] - [MemberData(nameof(DateTimeTestData))] - public async Task SerializesDateTimeTypes(object value, DateTimeOffset expect) - { - // Arrange - ODataRawValueSerializer serializer = new ODataRawValueSerializer(); - Mock mockRequest = new Mock(); - Stream stream = new MemoryStream(); - mockRequest.Setup(r => r.GetStreamAsync()).ReturnsAsync(stream); - ODataMessageWriter messageWriter = new ODataMessageWriter(mockRequest.Object); - - // Act - await serializer.WriteObjectAsync(value, value.GetType(), messageWriter, null); - stream.Seek(0, SeekOrigin.Begin); - TextReader reader = new StreamReader(stream); - - // Assert - Assert.Equal(expect, DateTimeOffset.Parse(await reader.ReadToEndAsync())); - } + [Theory] + [MemberData(nameof(DateTimeTestData))] + public async Task SerializesDateTimeTypes(object value, DateTimeOffset expect) + { + // Arrange + ODataRawValueSerializer serializer = new ODataRawValueSerializer(); + Mock mockRequest = new Mock(); + Stream stream = new MemoryStream(); + mockRequest.Setup(r => r.GetStreamAsync()).ReturnsAsync(stream); + ODataMessageWriter messageWriter = new ODataMessageWriter(mockRequest.Object); + + // Act + await serializer.WriteObjectAsync(value, value.GetType(), messageWriter, null); + stream.Seek(0, SeekOrigin.Begin); + TextReader reader = new StreamReader(stream); + + // Assert + Assert.Equal(expect, DateTimeOffset.Parse(await reader.ReadToEndAsync())); + } - [Fact] - public async Task SerializesEnumType() - { - // Arrange - ODataRawValueSerializer serializer = new ODataRawValueSerializer(); - Mock mockRequest = new Mock(); - Stream stream = new MemoryStream(); - mockRequest.Setup(r => r.GetStreamAsync()).ReturnsAsync(stream); - ODataMessageWriter messageWriter = new ODataMessageWriter(mockRequest.Object); - object value = Color.Red | Color.Blue; - - // Act - await serializer.WriteObjectAsync(value, value.GetType(), messageWriter, null); - stream.Seek(0, SeekOrigin.Begin); - TextReader reader = new StreamReader(stream); - string result = await reader.ReadToEndAsync(); - - // Assert - Assert.Equal(value.ToString(), result, ignoreCase: true); - } + [Fact] + public async Task SerializesEnumType() + { + // Arrange + ODataRawValueSerializer serializer = new ODataRawValueSerializer(); + Mock mockRequest = new Mock(); + Stream stream = new MemoryStream(); + mockRequest.Setup(r => r.GetStreamAsync()).ReturnsAsync(stream); + ODataMessageWriter messageWriter = new ODataMessageWriter(mockRequest.Object); + object value = Color.Red | Color.Blue; + + // Act + await serializer.WriteObjectAsync(value, value.GetType(), messageWriter, null); + stream.Seek(0, SeekOrigin.Begin); + TextReader reader = new StreamReader(stream); + string result = await reader.ReadToEndAsync(); + + // Assert + Assert.Equal(value.ToString(), result, ignoreCase: true); + } - [Fact] - public async Task SerializesReturnedCountValue() - { - // Arrange - var serializer = new ODataRawValueSerializer(); - var mockRequest = new Mock(); - Stream stream = new MemoryStream(); - mockRequest.Setup(r => r.GetStreamAsync()).ReturnsAsync(stream); - var messageWriter = new ODataMessageWriter(mockRequest.Object); - HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents(EdmCoreModel.Instance)); - request.ODataFeature().Path = new ODataPath(CountSegment.Instance); - var context = new ODataSerializerContext { Request = request }; - - // Act - await serializer.WriteObjectAsync(5, null, messageWriter, context); - stream.Seek(0, SeekOrigin.Begin); - TextReader reader = new StreamReader(stream); - string result = await reader.ReadToEndAsync(); - - // Assert - Assert.Equal("5", result); - } + [Fact] + public async Task SerializesReturnedCountValue() + { + // Arrange + var serializer = new ODataRawValueSerializer(); + var mockRequest = new Mock(); + Stream stream = new MemoryStream(); + mockRequest.Setup(r => r.GetStreamAsync()).ReturnsAsync(stream); + var messageWriter = new ODataMessageWriter(mockRequest.Object); + HttpRequest request = RequestFactory.Create(opt => opt.AddRouteComponents(EdmCoreModel.Instance)); + request.ODataFeature().Path = new ODataPath(CountSegment.Instance); + var context = new ODataSerializerContext { Request = request }; + + // Act + await serializer.WriteObjectAsync(5, null, messageWriter, context); + stream.Seek(0, SeekOrigin.Begin); + TextReader reader = new StreamReader(stream); + string result = await reader.ReadToEndAsync(); + + // Assert + Assert.Equal("5", result); + } - private enum Color - { - Red, - Blue - } + private enum Color + { + Red, + Blue } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs index 9f3c609af..5d9b46cdc 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSerializerTests.cs @@ -35,2622 +35,2621 @@ using Xunit; using ServiceLifetime = Microsoft.OData.ServiceLifetime; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization -{ - public class ODataResourceSerializerTests - { - private IEdmModel _model; - private IEdmEntitySet _customerSet; - private IEdmEntitySet _orderSet; - private Customer _customer; - private Order _order; - private ODataResourceSerializer _serializer; - private ODataSerializerContext _writeContext; - private ResourceContext _entityContext; - private IODataSerializerProvider _serializerProvider; - private IEdmEntityTypeReference _customerType; - private IEdmEntityTypeReference _orderType; - private IEdmEntityTypeReference _specialCustomerType; - private IEdmEntityTypeReference _specialOrderType; - private ODataPath _path; - - public ODataResourceSerializerTests() - { - _model = SerializationTestsHelpers.SimpleCustomerOrderModel(); +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; - _model.SetAnnotationValue(_model.FindType("Default.Customer"), new ClrTypeAnnotation(typeof(Customer))); - _model.SetAnnotationValue(_model.FindType("Default.Order"), new ClrTypeAnnotation(typeof(Order))); - _model.SetAnnotationValue(_model.FindType("Default.SpecialCustomer"), new ClrTypeAnnotation(typeof(SpecialCustomer))); - _model.SetAnnotationValue(_model.FindType("Default.SpecialOrder"), new ClrTypeAnnotation(typeof(SpecialOrder))); +public class ODataResourceSerializerTests +{ + private IEdmModel _model; + private IEdmEntitySet _customerSet; + private IEdmEntitySet _orderSet; + private Customer _customer; + private Order _order; + private ODataResourceSerializer _serializer; + private ODataSerializerContext _writeContext; + private ResourceContext _entityContext; + private IODataSerializerProvider _serializerProvider; + private IEdmEntityTypeReference _customerType; + private IEdmEntityTypeReference _orderType; + private IEdmEntityTypeReference _specialCustomerType; + private IEdmEntityTypeReference _specialOrderType; + private ODataPath _path; + + public ODataResourceSerializerTests() + { + _model = SerializationTestsHelpers.SimpleCustomerOrderModel(); + + _model.SetAnnotationValue(_model.FindType("Default.Customer"), new ClrTypeAnnotation(typeof(Customer))); + _model.SetAnnotationValue(_model.FindType("Default.Order"), new ClrTypeAnnotation(typeof(Order))); + _model.SetAnnotationValue(_model.FindType("Default.SpecialCustomer"), new ClrTypeAnnotation(typeof(SpecialCustomer))); + _model.SetAnnotationValue(_model.FindType("Default.SpecialOrder"), new ClrTypeAnnotation(typeof(SpecialOrder))); + + _customerSet = _model.EntityContainer.FindEntitySet("Customers"); + _customer = new Customer() + { + FirstName = "Foo", + LastName = "Bar", + ID = 10, + }; + + _orderSet = _model.EntityContainer.FindEntitySet("Orders"); + _order = new Order + { + ID = 20, + }; + + _serializerProvider = GetServiceProvider().GetService(); + _customerType = _model.GetEdmTypeReference(typeof(Customer)).AsEntity(); + _orderType = _model.GetEdmTypeReference(typeof(Order)).AsEntity(); + _specialCustomerType = _model.GetEdmTypeReference(typeof(SpecialCustomer)).AsEntity(); + _specialOrderType = _model.GetEdmTypeReference(typeof(SpecialOrder)).AsEntity(); + _serializer = new ODataResourceSerializer(_serializerProvider); + _path = new ODataPath(new EntitySetSegment(_customerSet)); + _writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path }; + _entityContext = new ResourceContext(_writeContext, _customerSet.EntityType.AsReference(), _customer); + } - _customerSet = _model.EntityContainer.FindEntitySet("Customers"); - _customer = new Customer() - { - FirstName = "Foo", - LastName = "Bar", - ID = 10, - }; + [Fact] + public void Ctor_ThrowsArgumentNull_SerializerProvider() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataResourceSerializer(serializerProvider: null), "serializerProvider"); + } - _orderSet = _model.EntityContainer.FindEntitySet("Orders"); - _order = new Order - { - ID = 20, - }; + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: _customer, type: typeof(Customer), messageWriter: null, writeContext: null), + "messageWriter"); + } - _serializerProvider = GetServiceProvider().GetService(); - _customerType = _model.GetEdmTypeReference(typeof(Customer)).AsEntity(); - _orderType = _model.GetEdmTypeReference(typeof(Order)).AsEntity(); - _specialCustomerType = _model.GetEdmTypeReference(typeof(SpecialCustomer)).AsEntity(); - _specialOrderType = _model.GetEdmTypeReference(typeof(SpecialOrder)).AsEntity(); - _serializer = new ODataResourceSerializer(_serializerProvider); - _path = new ODataPath(new EntitySetSegment(_customerSet)); - _writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path }; - _entityContext = new ResourceContext(_writeContext, _customerSet.EntityType.AsReference(), _customer); - } + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_WriteContext() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + ODataMessageWriter messageWriter = new ODataMessageWriter(new Mock().Object); + + // Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: _customer, type: typeof(Customer), messageWriter: messageWriter, writeContext: null), + "writeContext"); + } - [Fact] - public void Ctor_ThrowsArgumentNull_SerializerProvider() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataResourceSerializer(serializerProvider: null), "serializerProvider"); - } + [Fact] + public async Task WriteObjectAsync_Calls_WriteObjectInline_WithRightEntityType() + { + // Arrange + Mock serializerProvider = new Mock(); + Mock serializer = new Mock(serializerProvider.Object); + serializer + .Setup(s => s.WriteObjectInlineAsync(_customer, It.Is(e => _customerType.Definition == e.Definition), + It.IsAny(), _writeContext)) + .Verifiable(); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(new SelectExpandNode()); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectAsync(_customer, typeof(int), ODataTestUtil.GetMockODataMessageWriter(), _writeContext); + + // Assert + serializer.Verify(); + } - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() - { - // Arrange & Act - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - - // Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: _customer, type: typeof(Customer), messageWriter: null, writeContext: null), - "messageWriter"); - } + [Fact] + public async Task WriteObjectInlineAsync_ThrowsArgumentNull_Writer() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: null, writeContext: new ODataSerializerContext()), + "writer"); + } - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_WriteContext() - { - // Arrange & Act - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - ODataMessageWriter messageWriter = new ODataMessageWriter(new Mock().Object); - - // Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: _customer, type: typeof(Customer), messageWriter: messageWriter, writeContext: null), - "writeContext"); - } + [Fact] + public async Task WriteObjectInlineAsync_ThrowsArgumentNull_WriteContext() + { + // Arrange & Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => _serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: new Mock().Object, writeContext: null), + "writeContext"); + } - [Fact] - public async Task WriteObjectAsync_Calls_WriteObjectInline_WithRightEntityType() - { - // Arrange - Mock serializerProvider = new Mock(); - Mock serializer = new Mock(serializerProvider.Object); - serializer - .Setup(s => s.WriteObjectInlineAsync(_customer, It.Is(e => _customerType.Definition == e.Definition), - It.IsAny(), _writeContext)) - .Verifiable(); - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(new SelectExpandNode()); - serializer.CallBase = true; - - // Act - await serializer.Object.WriteObjectAsync(_customer, typeof(int), ODataTestUtil.GetMockODataMessageWriter(), _writeContext); - - // Assert - serializer.Verify(); - } + [Fact] + public async Task WriteObjectInlineAsync_ThrowsSerializationException_WhenGraphIsNull() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + ODataWriter messageWriter = new Mock().Object; + + // Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: messageWriter, writeContext: new ODataSerializerContext()), + "Cannot serialize a null 'Resource'."); + } - [Fact] - public async Task WriteObjectInlineAsync_ThrowsArgumentNull_Writer() - { - // Arrange & Act - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - - // Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: null, writeContext: new ODataSerializerContext()), - "writer"); - } + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateSelectExpandNode() + { + // Arrange + Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); + Mock serializer = new Mock(serializerProvider.Object); + ODataWriter writer = new Mock().Object; - [Fact] - public async Task WriteObjectInlineAsync_ThrowsArgumentNull_WriteContext() - { - // Arrange & Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => _serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: new Mock().Object, writeContext: null), - "writeContext"); - } + serializer.Setup(s => s.CreateSelectExpandNode(It.Is(e => Verify(e, _customer, _writeContext)))).Verifiable(); + serializer.CallBase = true; - [Fact] - public async Task WriteObjectInlineAsync_ThrowsSerializationException_WhenGraphIsNull() - { - // Arrange & Act - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - ODataWriter messageWriter = new Mock().Object; - - // Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: messageWriter, writeContext: new ODataSerializerContext()), - "Cannot serialize a null 'Resource'."); - } + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer, _writeContext); - [Fact] - public async Task WriteObjectInlineAsync_Calls_CreateSelectExpandNode() - { - // Arrange - Mock serializerProvider = new Mock(); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(new ODataPrimitiveSerializer()); - Mock serializer = new Mock(serializerProvider.Object); - ODataWriter writer = new Mock().Object; + // Assert + serializer.Verify(); + } - serializer.Setup(s => s.CreateSelectExpandNode(It.Is(e => Verify(e, _customer, _writeContext)))).Verifiable(); - serializer.CallBase = true; + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateResource() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode(); + Mock serializerProvider = new Mock(); + Mock serializer = new Mock(serializerProvider.Object); + ODataWriter writer = new Mock().Object; - // Act - await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer, _writeContext); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.Setup(s => s.CreateResource(selectExpandNode, It.Is(e => Verify(e, _customer, _writeContext)))).Verifiable(); + serializer.CallBase = true; - // Assert - serializer.Verify(); - } + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer, _writeContext); - [Fact] - public async Task WriteObjectInlineAsync_Calls_CreateResource() - { - // Arrange - SelectExpandNode selectExpandNode = new SelectExpandNode(); - Mock serializerProvider = new Mock(); - Mock serializer = new Mock(serializerProvider.Object); - ODataWriter writer = new Mock().Object; + // Assert + serializer.Verify(); + } - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); - serializer.Setup(s => s.CreateResource(selectExpandNode, It.Is(e => Verify(e, _customer, _writeContext)))).Verifiable(); - serializer.CallBase = true; + [Fact] + public async Task WriteObjectInlineAsync_WritesODataResourceFrom_CreateResource() + { + // Arrange + ODataResource entry = new ODataResource(); + Mock serializerProvider = new Mock(); + Mock serializer = new Mock(serializerProvider.Object); + Mock writer = new Mock(); - // Act - await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer, _writeContext); + serializer.Setup(s => s.CreateResource(It.IsAny(), It.IsAny())).Returns(entry); + serializer.CallBase = true; - // Assert - serializer.Verify(); - } + writer.Setup(s => s.WriteStartAsync(entry)).Verifiable(); - [Fact] - public async Task WriteObjectInlineAsync_WritesODataResourceFrom_CreateResource() - { - // Arrange - ODataResource entry = new ODataResource(); - Mock serializerProvider = new Mock(); - Mock serializer = new Mock(serializerProvider.Object); - Mock writer = new Mock(); + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); - serializer.Setup(s => s.CreateResource(It.IsAny(), It.IsAny())).Returns(entry); - serializer.CallBase = true; + // Assert + writer.Verify(); + } - writer.Setup(s => s.WriteStartAsync(entry)).Verifiable(); + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateUntypedPropertyValue_ForUntypedProperty() + { + // Arrange + Mock untyped = new Mock(); + untyped.Setup(u => u.TypeKind).Returns(EdmTypeKind.Untyped); - // Act - await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + Mock untypedRef = new Mock(); + untypedRef.Setup(p => p.Definition).Returns(untyped.Object); - // Assert - writer.Verify(); - } + Mock property1 = new Mock(); + property1.Setup(p => p.Type).Returns(untypedRef.Object); - [Fact] - public async Task WriteObjectInlineAsync_Calls_CreateUntypedPropertyValue_ForUntypedProperty() + SelectExpandNode selectExpandNode = new SelectExpandNode { - // Arrange - Mock untyped = new Mock(); - untyped.Setup(u => u.TypeKind).Returns(EdmTypeKind.Untyped); - - Mock untypedRef = new Mock(); - untypedRef.Setup(p => p.Definition).Returns(untyped.Object); - - Mock property1 = new Mock(); - property1.Setup(p => p.Type).Returns(untypedRef.Object); - - SelectExpandNode selectExpandNode = new SelectExpandNode + SelectedStructuralProperties = new HashSet { - SelectedStructuralProperties = new HashSet - { - property1.Object - } - }; + property1.Object + } + }; - Mock writer = new Mock(); - Mock serializer = new Mock(_serializerProvider); - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); - serializer.CallBase = true; - IEdmTypeReference a; + Mock writer = new Mock(); + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + IEdmTypeReference a; - serializer.Setup(s => s.CreateUntypedPropertyValue(property1.Object, It.IsAny(), out a)).Returns("a").Verifiable(); + serializer.Setup(s => s.CreateUntypedPropertyValue(property1.Object, It.IsAny(), out a)).Returns("a").Verifiable(); - // Act - await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); - // Assert - serializer.Verify(); - } + // Assert + serializer.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_Calls_CreateComplexNestedResourceInfo_ForEachSelectedComplexProperty() + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateComplexNestedResourceInfo_ForEachSelectedComplexProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode { - // Arrange - SelectExpandNode selectExpandNode = new SelectExpandNode + SelectedComplexProperties = new Dictionary { - SelectedComplexProperties = new Dictionary - { - { new Mock().Object, null }, - { new Mock().Object, null } - } - }; + { new Mock().Object, null }, + { new Mock().Object, null } + } + }; - Mock writer = new Mock(); - Mock serializer = new Mock(_serializerProvider); - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); - serializer.CallBase = true; + Mock writer = new Mock(); + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; - serializer.Setup(s => s.CreateComplexNestedResourceInfo(selectExpandNode.SelectedComplexProperties.ElementAt(0).Key, null, It.IsAny())).Verifiable(); - serializer.Setup(s => s.CreateComplexNestedResourceInfo(selectExpandNode.SelectedComplexProperties.ElementAt(1).Key, null, It.IsAny())).Verifiable(); + serializer.Setup(s => s.CreateComplexNestedResourceInfo(selectExpandNode.SelectedComplexProperties.ElementAt(0).Key, null, It.IsAny())).Verifiable(); + serializer.Setup(s => s.CreateComplexNestedResourceInfo(selectExpandNode.SelectedComplexProperties.ElementAt(1).Key, null, It.IsAny())).Verifiable(); - // Act - await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); - // Assert - serializer.Verify(); - } + // Assert + serializer.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_Calls_CreateNavigationLink_ForEachSelectedNavigationProperty() + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateNavigationLink_ForEachSelectedNavigationProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode { - // Arrange - SelectExpandNode selectExpandNode = new SelectExpandNode + SelectedNavigationProperties = new HashSet { - SelectedNavigationProperties = new HashSet - { - new Mock().Object, - new Mock().Object - } - }; - Mock writer = new Mock(); - Mock serializer = new Mock(_serializerProvider); - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); - serializer.CallBase = true; + new Mock().Object, + new Mock().Object + } + }; + Mock writer = new Mock(); + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; - serializer.Setup(s => s.CreateNavigationLink(selectExpandNode.SelectedNavigationProperties.ElementAt(0), It.IsAny())).Verifiable(); - serializer.Setup(s => s.CreateNavigationLink(selectExpandNode.SelectedNavigationProperties.ElementAt(1), It.IsAny())).Verifiable(); + serializer.Setup(s => s.CreateNavigationLink(selectExpandNode.SelectedNavigationProperties.ElementAt(0), It.IsAny())).Verifiable(); + serializer.Setup(s => s.CreateNavigationLink(selectExpandNode.SelectedNavigationProperties.ElementAt(1), It.IsAny())).Verifiable(); - ODataSerializerContext writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path }; - writeContext.MetadataLevel = ODataMetadataLevel.Full; + ODataSerializerContext writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path }; + writeContext.MetadataLevel = ODataMetadataLevel.Full; - // Act - await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, writeContext); + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, writeContext); - // Assert - serializer.Verify(); - } + // Assert + serializer.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_WritesNavigationLinksReturnedBy_CreateNavigationLink_ForEachSelectedNavigationProperty() + [Fact] + public async Task WriteObjectInlineAsync_WritesNavigationLinksReturnedBy_CreateNavigationLink_ForEachSelectedNavigationProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode { - // Arrange - SelectExpandNode selectExpandNode = new SelectExpandNode - { - SelectedNavigationProperties = new HashSet - { - new Mock().Object, - new Mock().Object - } - }; - ODataNestedResourceInfo[] navigationLinks = new[] + SelectedNavigationProperties = new HashSet { - new ODataNestedResourceInfo(), - new ODataNestedResourceInfo() - }; - Mock serializer = new Mock(_serializerProvider); - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); - serializer - .Setup(s => s.CreateNavigationLink(selectExpandNode.SelectedNavigationProperties.ElementAt(0), It.IsAny())) - .Returns(navigationLinks[0]); - serializer - .Setup(s => s.CreateNavigationLink(selectExpandNode.SelectedNavigationProperties.ElementAt(1), It.IsAny())) - .Returns(navigationLinks[1]); - serializer.CallBase = true; - - Mock writer = new Mock(); - writer.Setup(w => w.WriteStartAsync(navigationLinks[0])).Verifiable(); - writer.Setup(w => w.WriteStartAsync(navigationLinks[1])).Verifiable(); - - ODataSerializerContext writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path }; - writeContext.MetadataLevel = ODataMetadataLevel.Full; - - // Act - await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, writeContext); - - // Assert - writer.Verify(); - } + new Mock().Object, + new Mock().Object + } + }; + ODataNestedResourceInfo[] navigationLinks = new[] + { + new ODataNestedResourceInfo(), + new ODataNestedResourceInfo() + }; + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer + .Setup(s => s.CreateNavigationLink(selectExpandNode.SelectedNavigationProperties.ElementAt(0), It.IsAny())) + .Returns(navigationLinks[0]); + serializer + .Setup(s => s.CreateNavigationLink(selectExpandNode.SelectedNavigationProperties.ElementAt(1), It.IsAny())) + .Returns(navigationLinks[1]); + serializer.CallBase = true; + + Mock writer = new Mock(); + writer.Setup(w => w.WriteStartAsync(navigationLinks[0])).Verifiable(); + writer.Setup(w => w.WriteStartAsync(navigationLinks[1])).Verifiable(); + + ODataSerializerContext writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model, Path = _path }; + writeContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, writeContext); + + // Assert + writer.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_Calls_CreateNavigationLink_ForEachExpandedNavigationProperty() + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateNavigationLink_ForEachExpandedNavigationProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode { - // Arrange - SelectExpandNode selectExpandNode = new SelectExpandNode + ExpandedProperties = new Dictionary { - ExpandedProperties = new Dictionary - { - { new Mock().Object, null }, - { new Mock().Object, null } - } - }; - Mock writer = new Mock(); - Mock serializer = new Mock(_serializerProvider); - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); - var expandedNavigationProperties = selectExpandNode.ExpandedProperties.Keys; + { new Mock().Object, null }, + { new Mock().Object, null } + } + }; + Mock writer = new Mock(); + Mock serializer = new Mock(_serializerProvider); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + var expandedNavigationProperties = selectExpandNode.ExpandedProperties.Keys; - serializer.Setup(s => s.CreateNavigationLink(expandedNavigationProperties.First(), It.IsAny())).Verifiable(); - serializer.Setup(s => s.CreateNavigationLink(expandedNavigationProperties.Last(), It.IsAny())).Verifiable(); - serializer.CallBase = true; + serializer.Setup(s => s.CreateNavigationLink(expandedNavigationProperties.First(), It.IsAny())).Verifiable(); + serializer.Setup(s => s.CreateNavigationLink(expandedNavigationProperties.Last(), It.IsAny())).Verifiable(); + serializer.CallBase = true; - // Act - await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); - // Assert - serializer.Verify(); - } + // Assert + serializer.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_ExpandsUsingInnerSerializerUsingRightContext_ExpandedNavigationProperties() - { - // Arrange - IEdmEntityType customerType = _customerSet.EntityType; - IEdmNavigationProperty ordersProperty = customerType.NavigationProperties().Single(p => p.Name == "Orders"); + [Fact] + public async Task WriteObjectInlineAsync_ExpandsUsingInnerSerializerUsingRightContext_ExpandedNavigationProperties() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + IEdmNavigationProperty ordersProperty = customerType.NavigationProperties().Single(p => p.Name == "Orders"); - ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, customerType, _customerSet, - new Dictionary { { "$select", "Orders" }, { "$expand", "Orders" } }); - SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, customerType, _customerSet, + new Dictionary { { "$select", "Orders" }, { "$expand", "Orders" } }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); - SelectExpandNode selectExpandNode = new SelectExpandNode + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary { - ExpandedProperties = new Dictionary + { ordersProperty, selectExpandClause.SelectedItems.OfType().Single() } + }, + }; + Mock writer = new Mock(); + + Mock innerSerializer = new Mock(ODataPayloadKind.Resource); + innerSerializer + .Setup(s => s.WriteObjectInlineAsync(_customer.Orders, ordersProperty.Type, writer.Object, It.IsAny())) + .Callback((object o, IEdmTypeReference t, ODataWriter w, ODataSerializerContext context) => { - { ordersProperty, selectExpandClause.SelectedItems.OfType().Single() } - }, - }; - Mock writer = new Mock(); - - Mock innerSerializer = new Mock(ODataPayloadKind.Resource); - innerSerializer - .Setup(s => s.WriteObjectInlineAsync(_customer.Orders, ordersProperty.Type, writer.Object, It.IsAny())) - .Callback((object o, IEdmTypeReference t, ODataWriter w, ODataSerializerContext context) => - { - Assert.Same(context.NavigationSource.Name, "Orders"); - Assert.Same(context.SelectExpandClause, selectExpandNode.ExpandedProperties.Single().Value.SelectAndExpand); - }) - .Returns(Task.CompletedTask) - .Verifiable(); - - Mock serializerProvider = new Mock(); - serializerProvider.Setup(p => p.GetEdmTypeSerializer(ordersProperty.Type)) - .Returns(innerSerializer.Object); - Mock serializer = new Mock(serializerProvider.Object); - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); - serializer.CallBase = true; - _writeContext.SelectExpandClause = selectExpandClause; - - // Act - await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); - - // Assert - innerSerializer.Verify(); - // check that the context is rolled back - Assert.Same(_writeContext.NavigationSource.Name, "Customers"); - Assert.Same(_writeContext.SelectExpandClause, selectExpandClause); - } + Assert.Same(context.NavigationSource.Name, "Orders"); + Assert.Same(context.SelectExpandClause, selectExpandNode.ExpandedProperties.Single().Value.SelectAndExpand); + }) + .Returns(Task.CompletedTask) + .Verifiable(); + + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(ordersProperty.Type)) + .Returns(innerSerializer.Object); + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + _writeContext.SelectExpandClause = selectExpandClause; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, writer.Object, _writeContext); + + // Assert + innerSerializer.Verify(); + // check that the context is rolled back + Assert.Same(_writeContext.NavigationSource.Name, "Customers"); + Assert.Same(_writeContext.SelectExpandClause, selectExpandClause); + } - [Fact] - public async Task WriteObjectInlineAsync_CanExpandNavigationProperty_ContainingEdmObject() - { - // Arrange - IEdmEntityType customerType = _customerSet.EntityType; - IEdmNavigationProperty ordersProperty = customerType.NavigationProperties().Single(p => p.Name == "Orders"); + [Fact] + public async Task WriteObjectInlineAsync_CanExpandNavigationProperty_ContainingEdmObject() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + IEdmNavigationProperty ordersProperty = customerType.NavigationProperties().Single(p => p.Name == "Orders"); - Mock orders = new Mock(); - orders.Setup(o => o.GetEdmType()).Returns(ordersProperty.Type); - object ordersValue = orders.Object; + Mock orders = new Mock(); + orders.Setup(o => o.GetEdmType()).Returns(ordersProperty.Type); + object ordersValue = orders.Object; - Mock customer = new Mock(); - customer.Setup(c => c.TryGetPropertyValue("Orders", out ordersValue)).Returns(true); - customer.Setup(c => c.GetEdmType()).Returns(customerType.AsReference()); + Mock customer = new Mock(); + customer.Setup(c => c.TryGetPropertyValue("Orders", out ordersValue)).Returns(true); + customer.Setup(c => c.GetEdmType()).Returns(customerType.AsReference()); - ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, customerType, _customerSet, - new Dictionary { { "$select", "Orders" }, { "$expand", "Orders" } }); - SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, customerType, _customerSet, + new Dictionary { { "$select", "Orders" }, { "$expand", "Orders" } }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); - SelectExpandNode selectExpandNode = new SelectExpandNode - { - ExpandedProperties = new Dictionary() - }; - selectExpandNode.ExpandedProperties[ordersProperty] = selectExpandClause.SelectedItems.OfType().Single(); + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[ordersProperty] = selectExpandClause.SelectedItems.OfType().Single(); - Mock writer = new Mock(); + Mock writer = new Mock(); - Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); - ordersSerializer.Setup(s => s.WriteObjectInlineAsync(ordersValue, ordersProperty.Type, writer.Object, It.IsAny())).Verifiable(); + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + ordersSerializer.Setup(s => s.WriteObjectInlineAsync(ordersValue, ordersProperty.Type, writer.Object, It.IsAny())).Verifiable(); - Mock serializerProvider = new Mock(); - serializerProvider.Setup(p => p.GetEdmTypeSerializer(ordersProperty.Type)).Returns(ordersSerializer.Object); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(ordersProperty.Type)).Returns(ordersSerializer.Object); - Mock serializer = new Mock(serializerProvider.Object); - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); - serializer.CallBase = true; + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; - // Act - await serializer.Object.WriteObjectInlineAsync(customer.Object, _customerType, writer.Object, _writeContext); + // Act + await serializer.Object.WriteObjectInlineAsync(customer.Object, _customerType, writer.Object, _writeContext); - //Assert - ordersSerializer.Verify(); - } + //Assert + ordersSerializer.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_ExpandedCollectionValuedNavigationPropertyIsNull() - { - // Arrange - IEdmEntityType customerType = _customerSet.EntityType; - IEdmNavigationProperty ordersProperty = customerType.NavigationProperties().Single(p => p.Name == "Orders"); + [Fact] + public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_ExpandedCollectionValuedNavigationPropertyIsNull() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + IEdmNavigationProperty ordersProperty = customerType.NavigationProperties().Single(p => p.Name == "Orders"); - Mock customer = new Mock(); - object ordersValue = null; - customer.Setup(c => c.TryGetPropertyValue("Orders", out ordersValue)).Returns(true); - customer.Setup(c => c.GetEdmType()).Returns(customerType.AsReference()); + Mock customer = new Mock(); + object ordersValue = null; + customer.Setup(c => c.TryGetPropertyValue("Orders", out ordersValue)).Returns(true); + customer.Setup(c => c.GetEdmType()).Returns(customerType.AsReference()); - ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, customerType, _customerSet, - new Dictionary { { "$select", "Orders" }, { "$expand", "Orders" } }); - SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, customerType, _customerSet, + new Dictionary { { "$select", "Orders" }, { "$expand", "Orders" } }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[ordersProperty] = + selectExpandClause.SelectedItems.OfType().Single(); - SelectExpandNode selectExpandNode = new SelectExpandNode + Mock writer = new Mock(); + writer.Setup(w => w.WriteStartAsync(It.IsAny())).Callback( + (ODataResourceSet feed) => { - ExpandedProperties = new Dictionary() - }; - selectExpandNode.ExpandedProperties[ordersProperty] = - selectExpandClause.SelectedItems.OfType().Single(); + Assert.Null(feed.Count); + Assert.Null(feed.DeltaLink); + Assert.Null(feed.Id); + Assert.Empty(feed.InstanceAnnotations); + Assert.Null(feed.NextPageLink); + }).Returns(Task.CompletedTask).Verifiable(); + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(ordersProperty.Type)).Returns(ordersSerializer.Object); + + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(customer.Object, _customerType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } - Mock writer = new Mock(); - writer.Setup(w => w.WriteStartAsync(It.IsAny())).Callback( - (ODataResourceSet feed) => - { - Assert.Null(feed.Count); - Assert.Null(feed.DeltaLink); - Assert.Null(feed.Id); - Assert.Empty(feed.InstanceAnnotations); - Assert.Null(feed.NextPageLink); - }).Returns(Task.CompletedTask).Verifiable(); - Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); - Mock serializerProvider = new Mock(); - serializerProvider.Setup(p => p.GetEdmTypeSerializer(ordersProperty.Type)).Returns(ordersSerializer.Object); - - Mock serializer = new Mock(serializerProvider.Object); - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); - serializer.CallBase = true; - - // Act - await serializer.Object.WriteObjectInlineAsync(customer.Object, _customerType, writer.Object, _writeContext); - - // Assert - writer.Verify(); - } + [Fact] + public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_ExpandedSingleValuedNavigationPropertyIsNull() + { + // Arrange + IEdmEntityType orderType = _orderSet.EntityType; + IEdmNavigationProperty customerProperty = orderType.NavigationProperties().Single(p => p.Name == "Customer"); - [Fact] - public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_ExpandedSingleValuedNavigationPropertyIsNull() - { - // Arrange - IEdmEntityType orderType = _orderSet.EntityType; - IEdmNavigationProperty customerProperty = orderType.NavigationProperties().Single(p => p.Name == "Customer"); + Mock order = new Mock(); + object customerValue = null; + order.Setup(c => c.TryGetPropertyValue("Customer", out customerValue)).Returns(true); + order.Setup(c => c.GetEdmType()).Returns(orderType.AsReference()); - Mock order = new Mock(); - object customerValue = null; - order.Setup(c => c.TryGetPropertyValue("Customer", out customerValue)).Returns(true); - order.Setup(c => c.GetEdmType()).Returns(orderType.AsReference()); + ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, orderType, _orderSet, + new Dictionary { { "$select", "Customer" }, { "$expand", "Customer" } }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); - ODataQueryOptionParser parser = new ODataQueryOptionParser(_model, orderType, _orderSet, - new Dictionary { { "$select", "Customer" }, { "$expand", "Customer" } }); - SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + SelectExpandNode selectExpandNode = new SelectExpandNode + { + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[customerProperty] = + selectExpandClause.SelectedItems.OfType().Single(); - SelectExpandNode selectExpandNode = new SelectExpandNode - { - ExpandedProperties = new Dictionary() - }; - selectExpandNode.ExpandedProperties[customerProperty] = - selectExpandClause.SelectedItems.OfType().Single(); + Mock writer = new Mock(); - Mock writer = new Mock(); + writer.Setup(w => w.WriteStartAsync(null as ODataResource)).Verifiable(); + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(customerProperty.Type)) + .Returns(ordersSerializer.Object); - writer.Setup(w => w.WriteStartAsync(null as ODataResource)).Verifiable(); - Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); - Mock serializerProvider = new Mock(); - serializerProvider.Setup(p => p.GetEdmTypeSerializer(customerProperty.Type)) - .Returns(ordersSerializer.Object); + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; - Mock serializer = new Mock(serializerProvider.Object); - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); - serializer.CallBase = true; + // Act + await serializer.Object.WriteObjectInlineAsync(order.Object, _orderType, writer.Object, _writeContext); - // Act - await serializer.Object.WriteObjectInlineAsync(order.Object, _orderType, writer.Object, _writeContext); + // Assert + writer.Verify(); + } - // Assert - writer.Verify(); - } + [Fact] + public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_DerivedExpandedCollectionValuedNavigationPropertyIsNull() + { + // Arrange + IEdmEntityType specialCustomerType = (IEdmEntityType)_specialCustomerType.Definition; + IEdmNavigationProperty specialOrdersProperty = + specialCustomerType.NavigationProperties().Single(p => p.Name == "SpecialOrders"); + + Mock customer = new Mock(); + object specialOrdersValue = null; + customer.Setup(c => c.TryGetPropertyValue("SpecialOrders", out specialOrdersValue)).Returns(true); + customer.Setup(c => c.GetEdmType()).Returns(_specialCustomerType); + + IEdmEntityType customerType = _customerSet.EntityType; + ODataQueryOptionParser parser = new ODataQueryOptionParser( + _model, + customerType, + _customerSet, + new Dictionary + { + { "$select", "Default.SpecialCustomer/SpecialOrders" }, + { "$expand", "Default.SpecialCustomer/SpecialOrders" } + }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); - [Fact] - public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_DerivedExpandedCollectionValuedNavigationPropertyIsNull() + SelectExpandNode selectExpandNode = new SelectExpandNode { - // Arrange - IEdmEntityType specialCustomerType = (IEdmEntityType)_specialCustomerType.Definition; - IEdmNavigationProperty specialOrdersProperty = - specialCustomerType.NavigationProperties().Single(p => p.Name == "SpecialOrders"); - - Mock customer = new Mock(); - object specialOrdersValue = null; - customer.Setup(c => c.TryGetPropertyValue("SpecialOrders", out specialOrdersValue)).Returns(true); - customer.Setup(c => c.GetEdmType()).Returns(_specialCustomerType); - - IEdmEntityType customerType = _customerSet.EntityType; - ODataQueryOptionParser parser = new ODataQueryOptionParser( - _model, - customerType, - _customerSet, - new Dictionary - { - { "$select", "Default.SpecialCustomer/SpecialOrders" }, - { "$expand", "Default.SpecialCustomer/SpecialOrders" } - }); - SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[specialOrdersProperty] = + selectExpandClause.SelectedItems.OfType().Single(); - SelectExpandNode selectExpandNode = new SelectExpandNode + Mock writer = new Mock(); + writer.Setup(w => w.WriteStartAsync(It.IsAny())).Callback( + (ODataResourceSet feed) => { - ExpandedProperties = new Dictionary() - }; - selectExpandNode.ExpandedProperties[specialOrdersProperty] = - selectExpandClause.SelectedItems.OfType().Single(); + Assert.Null(feed.Count); + Assert.Null(feed.DeltaLink); + Assert.Null(feed.Id); + Assert.Empty(feed.InstanceAnnotations); + Assert.Null(feed.NextPageLink); + }).Returns(Task.CompletedTask).Verifiable(); + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(specialOrdersProperty.Type)) + .Returns(ordersSerializer.Object); + + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(customer.Object, _customerType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } - Mock writer = new Mock(); - writer.Setup(w => w.WriteStartAsync(It.IsAny())).Callback( - (ODataResourceSet feed) => - { - Assert.Null(feed.Count); - Assert.Null(feed.DeltaLink); - Assert.Null(feed.Id); - Assert.Empty(feed.InstanceAnnotations); - Assert.Null(feed.NextPageLink); - }).Returns(Task.CompletedTask).Verifiable(); - Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); - Mock serializerProvider = new Mock(); - serializerProvider.Setup(p => p.GetEdmTypeSerializer(specialOrdersProperty.Type)) - .Returns(ordersSerializer.Object); - - Mock serializer = new Mock(serializerProvider.Object); - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); - serializer.CallBase = true; - - // Act - await serializer.Object.WriteObjectInlineAsync(customer.Object, _customerType, writer.Object, _writeContext); - - // Assert - writer.Verify(); - } + [Fact] + public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_DerivedExpandedSingleValuedNavigationPropertyIsNull() + { + // Arrange + IEdmEntityType specialOrderType = (IEdmEntityType)_specialOrderType.Definition; + IEdmNavigationProperty customerProperty = + specialOrderType.NavigationProperties().Single(p => p.Name == "SpecialCustomer"); + + Mock order = new Mock(); + object customerValue = null; + order.Setup(c => c.TryGetPropertyValue("SpecialCustomer", out customerValue)).Returns(true); + order.Setup(c => c.GetEdmType()).Returns(_specialOrderType); + + IEdmEntityType orderType = (IEdmEntityType)_orderType.Definition; + ODataQueryOptionParser parser = new ODataQueryOptionParser( + _model, + orderType, + _orderSet, + new Dictionary + { + { "$select", "Default.SpecialOrder/SpecialCustomer" }, + { "$expand", "Default.SpecialOrder/SpecialCustomer" } + }); + SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); - [Fact] - public async Task WriteObjectInlineAsync_CanWriteExpandedNavigationProperty_DerivedExpandedSingleValuedNavigationPropertyIsNull() + SelectExpandNode selectExpandNode = new SelectExpandNode { - // Arrange - IEdmEntityType specialOrderType = (IEdmEntityType)_specialOrderType.Definition; - IEdmNavigationProperty customerProperty = - specialOrderType.NavigationProperties().Single(p => p.Name == "SpecialCustomer"); - - Mock order = new Mock(); - object customerValue = null; - order.Setup(c => c.TryGetPropertyValue("SpecialCustomer", out customerValue)).Returns(true); - order.Setup(c => c.GetEdmType()).Returns(_specialOrderType); - - IEdmEntityType orderType = (IEdmEntityType)_orderType.Definition; - ODataQueryOptionParser parser = new ODataQueryOptionParser( - _model, - orderType, - _orderSet, - new Dictionary - { - { "$select", "Default.SpecialOrder/SpecialCustomer" }, - { "$expand", "Default.SpecialOrder/SpecialCustomer" } - }); - SelectExpandClause selectExpandClause = parser.ParseSelectAndExpand(); - - SelectExpandNode selectExpandNode = new SelectExpandNode - { - ExpandedProperties = new Dictionary() - }; - selectExpandNode.ExpandedProperties[customerProperty] = - selectExpandClause.SelectedItems.OfType().Single(); + ExpandedProperties = new Dictionary() + }; + selectExpandNode.ExpandedProperties[customerProperty] = + selectExpandClause.SelectedItems.OfType().Single(); - Mock writer = new Mock(); + Mock writer = new Mock(); - writer.Setup(w => w.WriteStartAsync(null as ODataResource)).Verifiable(); - Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); - Mock serializerProvider = new Mock(); - serializerProvider.Setup(p => p.GetEdmTypeSerializer(customerProperty.Type)) - .Returns(ordersSerializer.Object); + writer.Setup(w => w.WriteStartAsync(null as ODataResource)).Verifiable(); + Mock ordersSerializer = new Mock(ODataPayloadKind.Resource); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(customerProperty.Type)) + .Returns(ordersSerializer.Object); - Mock serializer = new Mock(serializerProvider.Object); - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); - serializer.CallBase = true; + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.CallBase = true; - // Act - await serializer.Object.WriteObjectInlineAsync(order.Object, _orderType, writer.Object, _writeContext); + // Act + await serializer.Object.WriteObjectInlineAsync(order.Object, _orderType, writer.Object, _writeContext); - // Assert - writer.Verify(); - } + // Assert + writer.Verify(); + } - [Fact] - public void CreateResource_ThrowsArgumentNull_SelectExpandNode() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateResource(selectExpandNode: null, resourceContext: _entityContext), - "selectExpandNode"); - } + [Fact] + public void CreateResource_ThrowsArgumentNull_SelectExpandNode() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateResource(selectExpandNode: null, resourceContext: _entityContext), + "selectExpandNode"); + } - [Fact] - public void CreateResource_ThrowsArgumentNull_EntityContext() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateResource(new SelectExpandNode(), resourceContext: null), - "resourceContext"); - } + [Fact] + public void CreateResource_ThrowsArgumentNull_EntityContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateResource(new SelectExpandNode(), resourceContext: null), + "resourceContext"); + } - [Fact] - public void CreateComputedProperty_ThrowsArgumentNull_ForInputs() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + [Fact] + public void CreateComputedProperty_ThrowsArgumentNull_ForInputs() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - // Act & Assert - ExceptionAssert.ThrowsArgumentNullOrEmpty(() => serializer.CreateComputedProperty(null, null), "propertyName"); - ExceptionAssert.ThrowsArgumentNull(() => serializer.CreateComputedProperty("any", null), "resourceContext"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNullOrEmpty(() => serializer.CreateComputedProperty(null, null), "propertyName"); + ExceptionAssert.ThrowsArgumentNull(() => serializer.CreateComputedProperty("any", null), "resourceContext"); + } - [Fact] - public void CreateResource_Calls_CreateComputedProperty_ForEachSelectComputedProperty() - { - // Arrange - SelectExpandNode selectExpandNode = new SelectExpandNode(); - selectExpandNode.SelectedComputedProperties.Add("Computed1"); - selectExpandNode.SelectedComputedProperties.Add("Computed2"); + [Fact] + public void CreateResource_Calls_CreateComputedProperty_ForEachSelectComputedProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode(); + selectExpandNode.SelectedComputedProperties.Add("Computed1"); + selectExpandNode.SelectedComputedProperties.Add("Computed2"); - ODataProperty[] properties = new ODataProperty[] { new ODataProperty(), new ODataProperty() }; - Mock serializer = new Mock(_serializerProvider); - serializer.CallBase = true; + ODataProperty[] properties = new ODataProperty[] { new ODataProperty(), new ODataProperty() }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; - serializer.Setup(s => s.CreateComputedProperty("Computed1", _entityContext)).Returns(properties[0]).Verifiable(); - serializer.Setup(s => s.CreateComputedProperty("Computed2", _entityContext)).Returns(properties[1]).Verifiable(); + serializer.Setup(s => s.CreateComputedProperty("Computed1", _entityContext)).Returns(properties[0]).Verifiable(); + serializer.Setup(s => s.CreateComputedProperty("Computed2", _entityContext)).Returns(properties[1]).Verifiable(); - // Act - ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); - // Assert - serializer.Verify(); - Assert.Equal(properties, entry.Properties); - } + // Assert + serializer.Verify(); + Assert.Equal(properties, entry.Properties); + } - [Fact] - public void CreateResource_Calls_CreateStructuralProperty_ForEachSelectedStructuralProperty() + [Fact] + public void CreateResource_Calls_CreateStructuralProperty_ForEachSelectedStructuralProperty() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode { - // Arrange - SelectExpandNode selectExpandNode = new SelectExpandNode + SelectedStructuralProperties = new HashSet { - SelectedStructuralProperties = new HashSet - { - new Mock().Object, new Mock().Object - } - }; - ODataProperty[] properties = new ODataProperty[] { new ODataProperty(), new ODataProperty() }; - Mock serializer = new Mock(_serializerProvider); - serializer.CallBase = true; - - serializer - .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) - .Returns(properties[0]) - .Verifiable(); - serializer - .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) - .Returns(properties[1]) - .Verifiable(); - - // Act - ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); - - // Assert - serializer.Verify(); - Assert.Equal(properties, entry.Properties); - } + new Mock().Object, new Mock().Object + } + }; + ODataProperty[] properties = new ODataProperty[] { new ODataProperty(), new ODataProperty() }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(properties[0]) + .Verifiable(); + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) + .Returns(properties[1]) + .Verifiable(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + serializer.Verify(); + Assert.Equal(properties, entry.Properties); + } - [Fact] - public void CreateResource_SetsETagToNull_IfRequestIsNull() + [Fact] + public void CreateResource_SetsETagToNull_IfRequestIsNull() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode { - // Arrange - SelectExpandNode selectExpandNode = new SelectExpandNode + SelectedStructuralProperties = new HashSet { - SelectedStructuralProperties = new HashSet - { - new Mock().Object, new Mock().Object - } - }; - ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; - Mock serializer = new Mock(_serializerProvider); - serializer.CallBase = true; - - serializer - .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) - .Returns(properties[0]); - serializer - .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) - .Returns(properties[1]); - - // Act - ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); - - // Assert - Assert.Null(entry.ETag); - } + new Mock().Object, new Mock().Object + } + }; + ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(properties[0]); + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) + .Returns(properties[1]); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + Assert.Null(entry.ETag); + } - [Fact] - public void CreateResource_SetsETagToNull_IfModelNotHaveConcurrencyProperty() + [Fact] + public void CreateResource_SetsETagToNull_IfModelNotHaveConcurrencyProperty() + { + // Arrange + IEdmEntitySet orderSet = _model.EntityContainer.FindEntitySet("Orders"); + Order order = new Order() { - // Arrange - IEdmEntitySet orderSet = _model.EntityContainer.FindEntitySet("Orders"); - Order order = new Order() - { - Name = "Foo", - Shipment = "Bar", - ID = 10, - }; - - _writeContext.NavigationSource = orderSet; - _entityContext = new ResourceContext(_writeContext, orderSet.EntityType.AsReference(), order); + Name = "Foo", + Shipment = "Bar", + ID = 10, + }; - SelectExpandNode selectExpandNode = new SelectExpandNode - { - SelectedStructuralProperties = new HashSet - { - new Mock().Object, new Mock().Object - } - }; - ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; - Mock serializer = new Mock(_serializerProvider); - serializer.CallBase = true; - - serializer - .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) - .Returns(properties[0]); - serializer - .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) - .Returns(properties[1]); - - //var config = RoutingConfigurationFactory.CreateWithRootContainer("Route"); - var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", _model)); - request.ODataFeature().RoutePrefix = "route"; - _entityContext.Request = request; - - // Act - ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); - - // Assert - Assert.Null(entry.ETag); - } + _writeContext.NavigationSource = orderSet; + _entityContext = new ResourceContext(_writeContext, orderSet.EntityType.AsReference(), order); - [Fact] - public void CreateResource_SetsEtagToNotNull_IfWithConcurrencyProperty() + SelectExpandNode selectExpandNode = new SelectExpandNode { - // Arrange - Mock mockConcurrencyProperty = new Mock(); - mockConcurrencyProperty.SetupGet(s => s.Name).Returns("City"); - SelectExpandNode selectExpandNode = new SelectExpandNode - { - SelectedStructuralProperties = new HashSet { new Mock().Object, mockConcurrencyProperty.Object } - }; - ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; - Mock serializer = new Mock(_serializerProvider); - serializer.CallBase = true; - serializer - .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) - .Returns(properties[0]); - serializer - .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) - .Returns(properties[1]); - - Mock mockETagHandler = new Mock(); - string tag = "\"'anycity'\""; - EntityTagHeaderValue etagHeaderValue = new EntityTagHeaderValue(tag, isWeak: true); - mockETagHandler.Setup(e => e.CreateETag(It.IsAny>(), It.IsAny())).Returns(etagHeaderValue); - - var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", _model, services => + SelectedStructuralProperties = new HashSet { - services.AddSingleton(sp => mockETagHandler.Object); - })); - request.ODataFeature().RoutePrefix = "route"; - _entityContext.Request = request; - - // Act - ODataResource resource = serializer.Object.CreateResource(selectExpandNode, _entityContext); + new Mock().Object, new Mock().Object + } + }; + ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(properties[0]); + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) + .Returns(properties[1]); + + //var config = RoutingConfigurationFactory.CreateWithRootContainer("Route"); + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", _model)); + request.ODataFeature().RoutePrefix = "route"; + _entityContext.Request = request; + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + Assert.Null(entry.ETag); + } - // Assert - Assert.Equal(etagHeaderValue.ToString(), resource.ETag); - } + [Fact] + public void CreateResource_SetsEtagToNotNull_IfWithConcurrencyProperty() + { + // Arrange + Mock mockConcurrencyProperty = new Mock(); + mockConcurrencyProperty.SetupGet(s => s.Name).Returns("City"); + SelectExpandNode selectExpandNode = new SelectExpandNode + { + SelectedStructuralProperties = new HashSet { new Mock().Object, mockConcurrencyProperty.Object } + }; + ODataProperty[] properties = new[] { new ODataProperty(), new ODataProperty() }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(properties[0]); + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(1), _entityContext)) + .Returns(properties[1]); + + Mock mockETagHandler = new Mock(); + string tag = "\"'anycity'\""; + EntityTagHeaderValue etagHeaderValue = new EntityTagHeaderValue(tag, isWeak: true); + mockETagHandler.Setup(e => e.CreateETag(It.IsAny>(), It.IsAny())).Returns(etagHeaderValue); + + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", _model, services => + { + services.AddSingleton(sp => mockETagHandler.Object); + })); + request.ODataFeature().RoutePrefix = "route"; + _entityContext.Request = request; + + // Act + ODataResource resource = serializer.Object.CreateResource(selectExpandNode, _entityContext); + + // Assert + Assert.Equal(etagHeaderValue.ToString(), resource.ETag); + } - [Fact] - public void CreateResource_IgnoresProperty_IfCreateStructuralPropertyReturnsNull() + [Fact] + public void CreateResource_IgnoresProperty_IfCreateStructuralPropertyReturnsNull() + { + // Arrange + SelectExpandNode selectExpandNode = new SelectExpandNode { - // Arrange - SelectExpandNode selectExpandNode = new SelectExpandNode - { - SelectedStructuralProperties = new HashSet { new Mock().Object } - }; - Mock serializer = new Mock(_serializerProvider); - serializer.CallBase = true; + SelectedStructuralProperties = new HashSet { new Mock().Object } + }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; - serializer - .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) - .Returns(null); + serializer + .Setup(s => s.CreateStructuralProperty(selectExpandNode.SelectedStructuralProperties.ElementAt(0), _entityContext)) + .Returns(null); - // Act - ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); - // Assert - serializer.Verify(); - Assert.Empty(entry.Properties); - } + // Assert + serializer.Verify(); + Assert.Empty(entry.Properties); + } - [Fact] - public void CreateResource_Calls_CreateODataAction_ForEachSelectAction() + [Fact] + public void CreateResource_Calls_CreateODataAction_ForEachSelectAction() + { + // Arrange + ODataAction[] actions = new ODataAction[] { new ODataAction(), new ODataAction() }; + SelectExpandNode selectExpandNode = new SelectExpandNode { - // Arrange - ODataAction[] actions = new ODataAction[] { new ODataAction(), new ODataAction() }; - SelectExpandNode selectExpandNode = new SelectExpandNode - { - SelectedActions = new HashSet { new Mock().Object, new Mock().Object } - }; - Mock serializer = new Mock(_serializerProvider); - serializer.CallBase = true; + SelectedActions = new HashSet { new Mock().Object, new Mock().Object } + }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; - serializer.Setup(s => s.CreateODataAction(selectExpandNode.SelectedActions.ElementAt(0), _entityContext)).Returns(actions[0]).Verifiable(); - serializer.Setup(s => s.CreateODataAction(selectExpandNode.SelectedActions.ElementAt(1), _entityContext)).Returns(actions[1]).Verifiable(); + serializer.Setup(s => s.CreateODataAction(selectExpandNode.SelectedActions.ElementAt(0), _entityContext)).Returns(actions[0]).Verifiable(); + serializer.Setup(s => s.CreateODataAction(selectExpandNode.SelectedActions.ElementAt(1), _entityContext)).Returns(actions[1]).Verifiable(); - // Act - ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, _entityContext); - // Assert - Assert.Equal(actions, entry.Actions); - serializer.Verify(); - } + // Assert + Assert.Equal(actions, entry.Actions); + serializer.Verify(); + } - [Fact] - public void CreateResource_Works_ToAppendDynamicProperties_ForOpenEntityType() - { - // Arrange - IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + [Fact] + public void CreateResource_Works_ToAppendDynamicProperties_ForOpenEntityType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); - IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); - IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; - Type simpleOpenCustomer = typeof(SimpleOpenCustomer); - model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); - IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; - Type simpleOpenAddress = typeof(SimpleOpenAddress); - model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); - IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; - Type simpleEnumType = typeof(SimpleEnum); - model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); - model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( - simpleOpenCustomer.GetProperty("CustomerProperties"))); + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); - model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( - simpleOpenAddress.GetProperty("Properties"))); + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); - ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); - SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); - ODataSerializerContext writeContext = new ODataSerializerContext - { - Model = model, - Path = new ODataPath(new EntitySetSegment(customers)) - }; + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext + { + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)) + }; - SimpleOpenCustomer customer = new SimpleOpenCustomer() + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress { - CustomerId = 991, - Name = "Name #991", - Address = new SimpleOpenAddress - { - City = "a city", - Street = "a street", - Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } - }, - CustomerProperties = new Dictionary() - }; - DateTime dateTime = new DateTime(2014, 10, 24, 0, 0, 0, DateTimeKind.Utc); - customer.CustomerProperties.Add("EnumProperty", SimpleEnum.Fourth); - customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); - customer.CustomerProperties.Add("ListProperty", new List { 5, 4, 3, 2, 1 }); - customer.CustomerProperties.Add("DateTimeProperty", dateTime); - - ResourceContext resourceContext = new ResourceContext(writeContext, - customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); - - // Act - ODataResource resource = serializer.CreateResource(selectExpandNode, resourceContext); + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; + DateTime dateTime = new DateTime(2014, 10, 24, 0, 0, 0, DateTimeKind.Utc); + customer.CustomerProperties.Add("EnumProperty", SimpleEnum.Fourth); + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("ListProperty", new List { 5, 4, 3, 2, 1 }); + customer.CustomerProperties.Add("DateTimeProperty", dateTime); + + ResourceContext resourceContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + + // Act + ODataResource resource = serializer.CreateResource(selectExpandNode, resourceContext); + + // Assert + Assert.Equal("Default.Customer", resource.TypeName); + Assert.Equal(6, resource.Properties.Count()); + + // Verify the declared properties + ODataProperty street = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "CustomerId"))); + Assert.Equal(991, street.Value); + + ODataProperty city = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "Name"))); + Assert.Equal("Name #991", city.Value); + + // Verify the nested open complex property + Assert.Empty(resource.Properties.Where(p => p.Name == "Address")); + + // Verify the dynamic properties + ODataProperty enumProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "EnumProperty"))); + ODataEnumValue enumValue = Assert.IsType(enumProperty.Value); + Assert.Equal("Fourth", enumValue.Value); + Assert.Equal("Default.SimpleEnum", enumValue.TypeName); + + ODataProperty guidProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "GuidProperty"))); + Assert.Equal(new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA"), guidProperty.Value); + + ODataProperty listProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "ListProperty"))); + ODataCollectionValue collectionValue = Assert.IsType(listProperty.Value); + Assert.Equal(new List { 5, 4, 3, 2, 1 }, collectionValue.Items.OfType().ToList()); + Assert.Equal("Collection(Edm.Int32)", collectionValue.TypeName); + + ODataProperty dateTimeProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "DateTimeProperty"))); + Assert.Equal(new DateTimeOffset(dateTime), dateTimeProperty.Value); + } - // Assert - Assert.Equal("Default.Customer", resource.TypeName); - Assert.Equal(6, resource.Properties.Count()); + [Fact] + public void CreateResource_Works_ToAppendNullDynamicProperties_ForOpenEntityType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); - // Verify the declared properties - ODataProperty street = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "CustomerId"))); - Assert.Equal(991, street.Value); + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); - ODataProperty city = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "Name"))); - Assert.Equal("Name #991", city.Value); + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); - // Verify the nested open complex property - Assert.Empty(resource.Properties.Where(p => p.Name == "Address")); + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); - // Verify the dynamic properties - ODataProperty enumProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "EnumProperty"))); - ODataEnumValue enumValue = Assert.IsType(enumProperty.Value); - Assert.Equal("Fourth", enumValue.Value); - Assert.Equal("Default.SimpleEnum", enumValue.TypeName); + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); - ODataProperty guidProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "GuidProperty"))); - Assert.Equal(new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA"), guidProperty.Value); + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); - ODataProperty listProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "ListProperty"))); - ODataCollectionValue collectionValue = Assert.IsType(listProperty.Value); - Assert.Equal(new List { 5, 4, 3, 2, 1 }, collectionValue.Items.OfType().ToList()); - Assert.Equal("Collection(Edm.Int32)", collectionValue.TypeName); + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); - ODataProperty dateTimeProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "DateTimeProperty"))); - Assert.Equal(new DateTimeOffset(dateTime), dateTimeProperty.Value); - } + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); - [Fact] - public void CreateResource_Works_ToAppendNullDynamicProperties_ForOpenEntityType() + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); + request.ODataFeature().RoutePrefix = "route"; + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext { - // Arrange - IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)), + Request = request + }; - IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; - IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; - Type simpleOpenCustomer = typeof(SimpleOpenCustomer); - model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("NullProperty", null); - IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; - Type simpleOpenAddress = typeof(SimpleOpenAddress); - model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + ResourceContext entityContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); - IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; - Type simpleEnumType = typeof(SimpleEnum); - model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + // Act + ODataResource resource = serializer.CreateResource(selectExpandNode, entityContext); - model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( - simpleOpenCustomer.GetProperty("CustomerProperties"))); + // Assert + Assert.Equal("Default.Customer", resource.TypeName); + Assert.Equal(4, resource.Properties.Count()); - model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( - simpleOpenAddress.GetProperty("Properties"))); + // Verify the declared properties + ODataProperty street = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "CustomerId"))); + Assert.Equal(991, street.Value); - ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + ODataProperty city = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "Name"))); + Assert.Equal("Name #991", city.Value); - var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); - request.ODataFeature().RoutePrefix = "route"; - SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); - ODataSerializerContext writeContext = new ODataSerializerContext - { - Model = model, - Path = new ODataPath(new EntitySetSegment(customers)), - Request = request - }; + // Verify the nested open complex property + Assert.Empty(resource.Properties.Where(p => p.Name == "Address")); - SimpleOpenCustomer customer = new SimpleOpenCustomer() - { - CustomerId = 991, - Name = "Name #991", - Address = new SimpleOpenAddress - { - City = "a city", - Street = "a street", - Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } - }, - CustomerProperties = new Dictionary() - }; + // Verify the dynamic properties + ODataProperty guidProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "GuidProperty"))); + Assert.Equal(new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA"), guidProperty.Value); - customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); - customer.CustomerProperties.Add("NullProperty", null); + ODataProperty nullProperty = resource.Properties.OfType().Single(p => p.Name == "NullProperty"); + Assert.Null(nullProperty.Value); + } - ResourceContext entityContext = new ResourceContext(writeContext, - customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + [Fact] + public void CreateResource_Works_ToIgnoreDynamicPropertiesWithEmptyNames_ForOpenEntityType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); - // Act - ODataResource resource = serializer.CreateResource(selectExpandNode, entityContext); + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); - // Assert - Assert.Equal("Default.Customer", resource.TypeName); - Assert.Equal(4, resource.Properties.Count()); + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); - // Verify the declared properties - ODataProperty street = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "CustomerId"))); - Assert.Equal(991, street.Value); + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); - ODataProperty city = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "Name"))); - Assert.Equal("Name #991", city.Value); + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); - // Verify the nested open complex property - Assert.Empty(resource.Properties.Where(p => p.Name == "Address")); + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); - // Verify the dynamic properties - ODataProperty guidProperty = Assert.IsType(Assert.Single(resource.Properties.Where(p => p.Name == "GuidProperty"))); - Assert.Equal(new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA"), guidProperty.Value); + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); - ODataProperty nullProperty = resource.Properties.OfType().Single(p => p.Name == "NullProperty"); - Assert.Null(nullProperty.Value); - } + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); - [Fact] - public void CreateResource_Works_ToIgnoreDynamicPropertiesWithEmptyNames_ForOpenEntityType() + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); + request.ODataFeature().RoutePrefix = "route"; + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext { - // Arrange - IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)), + Request = request + }; - IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; - IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; - Type simpleOpenCustomer = typeof(SimpleOpenCustomer); - model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("", "EmptyProperty"); - IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; - Type simpleOpenAddress = typeof(SimpleOpenAddress); - model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + ResourceContext entityContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); - IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; - Type simpleEnumType = typeof(SimpleEnum); - model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + // Act + ODataResource resource = serializer.CreateResource(selectExpandNode, entityContext); - model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( - simpleOpenCustomer.GetProperty("CustomerProperties"))); + // Assert + Assert.Equal("Default.Customer", resource.TypeName); + Assert.Equal(3, resource.Properties.Count()); - model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( - simpleOpenAddress.GetProperty("Properties"))); + // Verify properties with empty names are ignored + ODataProperty emptyProperty = resource.Properties.OfType().SingleOrDefault(p => p.Name == ""); + Assert.Null(emptyProperty); + } - ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + [Fact] + public void CreateResource_Throws_IfDynamicPropertyUsesExistingName_ForOpenType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); - var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); - request.ODataFeature().RoutePrefix = "route"; - SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); - ODataSerializerContext writeContext = new ODataSerializerContext - { - Model = model, - Path = new ODataPath(new EntitySetSegment(customers)), - Request = request - }; + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); - SimpleOpenCustomer customer = new SimpleOpenCustomer() - { - CustomerId = 991, - Name = "Name #991", - Address = new SimpleOpenAddress - { - City = "a city", - Street = "a street", - Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } - }, - CustomerProperties = new Dictionary() - }; + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); - customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); - customer.CustomerProperties.Add("", "EmptyProperty"); + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); - ResourceContext entityContext = new ResourceContext(writeContext, - customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); - // Act - ODataResource resource = serializer.CreateResource(selectExpandNode, entityContext); + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); - // Assert - Assert.Equal("Default.Customer", resource.TypeName); - Assert.Equal(3, resource.Properties.Count()); + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); - // Verify properties with empty names are ignored - ODataProperty emptyProperty = resource.Properties.OfType().SingleOrDefault(p => p.Name == ""); - Assert.Null(emptyProperty); - } + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); - [Fact] - public void CreateResource_Throws_IfDynamicPropertyUsesExistingName_ForOpenType() + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); + request.ODataFeature().RoutePrefix = "route"; + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext { - // Arrange - IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); - - IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)), + Request = request + }; - IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; - Type simpleOpenCustomer = typeof(SimpleOpenCustomer); - model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); - - IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; - Type simpleOpenAddress = typeof(SimpleOpenAddress); - model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + SimpleOpenCustomer customer = new SimpleOpenCustomer() + { + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; + + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("Name", "DynamicName"); + + ResourceContext entityContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + + // Act & Assert + ExceptionAssert.Throws( + () => serializer.CreateResource(selectExpandNode, entityContext), + "Name", + partialMatch: true); + } - IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; - Type simpleEnumType = typeof(SimpleEnum); - model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + [Fact] + public void CreateResource_Throws_IfNullDynamicPropertyUsesExistingName_ForOpenType() + { + // Arrange + IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); - model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( - simpleOpenCustomer.GetProperty("CustomerProperties"))); + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); - model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( - simpleOpenAddress.GetProperty("Properties"))); + IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; + Type simpleOpenCustomer = typeof(SimpleOpenCustomer); + model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); - ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; + Type simpleOpenAddress = typeof(SimpleOpenAddress); + model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); - var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); - request.ODataFeature().RoutePrefix = "route"; - SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); - ODataSerializerContext writeContext = new ODataSerializerContext - { - Model = model, - Path = new ODataPath(new EntitySetSegment(customers)), - Request = request - }; + IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; + Type simpleEnumType = typeof(SimpleEnum); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); - SimpleOpenCustomer customer = new SimpleOpenCustomer() - { - CustomerId = 991, - Name = "Name #991", - Address = new SimpleOpenAddress - { - City = "a city", - Street = "a street", - Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } - }, - CustomerProperties = new Dictionary() - }; + model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( + simpleOpenCustomer.GetProperty("CustomerProperties"))); - customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); - customer.CustomerProperties.Add("Name", "DynamicName"); + model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( + simpleOpenAddress.GetProperty("Properties"))); - ResourceContext entityContext = new ResourceContext(writeContext, - customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); - // Act & Assert - ExceptionAssert.Throws( - () => serializer.CreateResource(selectExpandNode, entityContext), - "Name", - partialMatch: true); - } + var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); + request.ODataFeature().RoutePrefix = "route"; + SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); + ODataSerializerContext writeContext = new ODataSerializerContext + { + Model = model, + Path = new ODataPath(new EntitySetSegment(customers)), + Request = request + }; - [Fact] - public void CreateResource_Throws_IfNullDynamicPropertyUsesExistingName_ForOpenType() + SimpleOpenCustomer customer = new SimpleOpenCustomer() { - // Arrange - IEdmModel model = SerializationTestsHelpers.SimpleOpenTypeModel(); + CustomerId = 991, + Name = "Name #991", + Address = new SimpleOpenAddress + { + City = "a city", + Street = "a street", + Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } + }, + CustomerProperties = new Dictionary() + }; + + customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); + customer.CustomerProperties.Add("Name", null); + + ResourceContext entityContext = new ResourceContext(writeContext, + customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + + // Act & Assert + ExceptionAssert.Throws( + () => serializer.CreateResource(selectExpandNode, entityContext), + "Name", + partialMatch: true); + } + [Fact] + public void CreateUntypedPropertyValue_ThrowsArgumentNull_StructuralProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateUntypedPropertyValue(structuralProperty: null, resourceContext: null, out _), + "structuralProperty"); + } - IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + [Fact] + public void CreateUntypedPropertyValue_ThrowsArgumentNull_ResourceContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + Mock property = new Mock(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateUntypedPropertyValue(property.Object, resourceContext: null, out _), + "resourceContext"); + } - IEdmEntityType customerType = model.FindDeclaredType("Default.Customer") as IEdmEntityType; - Type simpleOpenCustomer = typeof(SimpleOpenCustomer); - model.SetAnnotationValue(customerType, new ClrTypeAnnotation(simpleOpenCustomer)); + [Fact] + public void CreateUntypedPropertyValue_ReturnsNull_ForNonUntypedProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + Mock property = new Mock(); + property.Setup(p => p.Type).Returns(EdmCoreModel.Instance.GetInt32(true)); - IEdmComplexType addressType = model.FindDeclaredType("Default.Address") as IEdmComplexType; - Type simpleOpenAddress = typeof(SimpleOpenAddress); - model.SetAnnotationValue(addressType, new ClrTypeAnnotation(simpleOpenAddress)); + // Act + object result = serializer.CreateUntypedPropertyValue(property.Object, new ResourceContext(), out _); - IEdmEnumType enumType = model.FindDeclaredType("Default.SimpleEnum") as IEdmEnumType; - Type simpleEnumType = typeof(SimpleEnum); - model.SetAnnotationValue(enumType, new ClrTypeAnnotation(simpleEnumType)); + // Assert + Assert.Null(result); + } - model.SetAnnotationValue(customerType, new DynamicPropertyDictionaryAnnotation( - simpleOpenCustomer.GetProperty("CustomerProperties"))); + [Fact] + public void CreateUntypedPropertyValue_ReturnsRealValue_ForUntypedPropertyWithRealType() + { + // Arrange + Mock property = new Mock(); + property.Setup(p => p.Name).Returns("PropertyName"); + Mock serializerProvider = new Mock(MockBehavior.Strict); + object propertyValue = new List(); + var entity = new { PropertyName = propertyValue }; + Mock innerSerializer = new Mock(ODataPayloadKind.Property); + IEdmTypeReference propertyType = EdmUntypedStructuredTypeReference.NullableTypeReference; + + property.Setup(p => p.Type).Returns(propertyType); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + ResourceContext entityContext = new ResourceContext(_writeContext, _customerType, entity); + + // Act + object result = serializer.CreateUntypedPropertyValue(property.Object, entityContext, out IEdmTypeReference actualType); + + // Assert + innerSerializer.Verify(); + Assert.Same(propertyValue, result); + Assert.Same(EdmUntypedHelpers.NullableUntypedCollectionReference, actualType); + } - model.SetAnnotationValue(addressType, new DynamicPropertyDictionaryAnnotation( - simpleOpenAddress.GetProperty("Properties"))); + [Fact] + public void CreateUntypedPropertyValue_Calls_CreateODataValueOnInnerSerializer() + { + // Arrange + Mock property = new Mock(); + property.Setup(p => p.Name).Returns("PropertyName"); + Mock serializerProvider = new Mock(MockBehavior.Strict); + var entity = new { PropertyName = 42 }; + Mock innerSerializer = new Mock(ODataPayloadKind.Property); + ODataPrimitiveValue propertyValue = new ODataPrimitiveValue(42); + IEdmTypeReference propertyType = EdmUntypedStructuredTypeReference.NullableTypeReference; + IEdmTypeReference actualType = _writeContext.GetEdmType(propertyValue, typeof(int)); + + property.Setup(p => p.Type).Returns(propertyType); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(actualType)).Returns(innerSerializer.Object); + innerSerializer.Setup(s => s.CreateODataValue(42, actualType, _writeContext)).Returns(propertyValue).Verifiable(); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + ResourceContext entityContext = new ResourceContext(_writeContext, _customerType, entity); + + // Act + object createdProperty = serializer.CreateUntypedPropertyValue(property.Object, entityContext, out _); + + // Assert + innerSerializer.Verify(); + ODataProperty odataProperty = Assert.IsType(createdProperty); + Assert.Equal("PropertyName", odataProperty.Name); + Assert.Equal(42, odataProperty.Value); + } - ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + [Fact] + public void CreateStructuralProperty_ThrowsArgumentNull_StructuralProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateStructuralProperty(structuralProperty: null, resourceContext: null), + "structuralProperty"); + } - var request = RequestFactory.Create(opt => opt.AddRouteComponents("route", model)); - request.ODataFeature().RoutePrefix = "route"; - SelectExpandNode selectExpandNode = new SelectExpandNode(null, customerType, model); - ODataSerializerContext writeContext = new ODataSerializerContext - { - Model = model, - Path = new ODataPath(new EntitySetSegment(customers)), - Request = request - }; - - SimpleOpenCustomer customer = new SimpleOpenCustomer() - { - CustomerId = 991, - Name = "Name #991", - Address = new SimpleOpenAddress - { - City = "a city", - Street = "a street", - Properties = new Dictionary { { "ArrayProperty", new[] { "15", "14", "13" } } } - }, - CustomerProperties = new Dictionary() - }; - - customer.CustomerProperties.Add("GuidProperty", new Guid("181D3A20-B41A-489F-9F15-F91F0F6C9ECA")); - customer.CustomerProperties.Add("Name", null); + [Fact] + public void CreateStructuralProperty_ThrowsArgumentNull_ResourceContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + Mock property = new Mock(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateStructuralProperty(property.Object, resourceContext: null), + "resourceContext"); + } - ResourceContext entityContext = new ResourceContext(writeContext, - customerType.ToEdmTypeReference(false) as IEdmEntityTypeReference, customer); + [Fact] + public void CreateStructuralProperty_ThrowsSerializationException_TypeCannotBeSerialized() + { + // Arrange + Mock propertyType = new Mock(); + propertyType.Setup(t => t.Definition).Returns(new EdmEntityType("Namespace", "Name")); + Mock property = new Mock(); + Mock serializerProvider = new Mock(MockBehavior.Strict); + IEdmEntityObject entity = new Mock().Object; + property.Setup(p => p.Type).Returns(propertyType.Object); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(propertyType.Object)).Returns(null); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.Throws( + () => serializer.CreateStructuralProperty(property.Object, new ResourceContext { EdmObject = entity }), + "'Namespace.Name' cannot be serialized using the OData output formatter."); + } - // Act & Assert - ExceptionAssert.Throws( - () => serializer.CreateResource(selectExpandNode, entityContext), - "Name", - partialMatch: true); - } - [Fact] - public void CreateUntypedPropertyValue_ThrowsArgumentNull_StructuralProperty() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateUntypedPropertyValue(structuralProperty: null, resourceContext: null, out _), - "structuralProperty"); - } + [Fact] + public void CreateStructuralProperty_Calls_CreateODataValueOnInnerSerializer() + { + // Arrange + Mock property = new Mock(); + property.Setup(p => p.Name).Returns("PropertyName"); + Mock serializerProvider = new Mock(MockBehavior.Strict); + var entity = new { PropertyName = 42 }; + Mock innerSerializer = new Mock(ODataPayloadKind.Property); + ODataValue propertyValue = new Mock().Object; + IEdmTypeReference propertyType = _writeContext.GetEdmType(propertyValue, typeof(int)); + + property.Setup(p => p.Type).Returns(propertyType); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(propertyType)).Returns(innerSerializer.Object); + innerSerializer.Setup(s => s.CreateODataValue(42, propertyType, _writeContext)).Returns(propertyValue).Verifiable(); + + var serializer = new ODataResourceSerializer(serializerProvider.Object); + ResourceContext entityContext = new ResourceContext(_writeContext, _customerType, entity); + + // Act + ODataProperty createdProperty = serializer.CreateStructuralProperty(property.Object, entityContext); + + // Assert + innerSerializer.Verify(); + Assert.Equal("PropertyName", createdProperty.Name); + Assert.Equal(propertyValue, createdProperty.Value); + } - [Fact] - public void CreateUntypedPropertyValue_ThrowsArgumentNull_ResourceContext() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - Mock property = new Mock(); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateUntypedPropertyValue(property.Object, resourceContext: null, out _), - "resourceContext"); - } + private bool Verify(ResourceContext instanceContext, object instance, ODataSerializerContext writeContext) + { + Assert.Same(instance, (instanceContext.EdmObject as TypedEdmEntityObject).Instance); + Assert.Equal(writeContext.Model, instanceContext.EdmModel); + Assert.Equal(writeContext.NavigationSource, instanceContext.NavigationSource); + Assert.Equal(writeContext.Request, instanceContext.Request); + Assert.Equal(writeContext.SkipExpensiveAvailabilityChecks, instanceContext.SkipExpensiveAvailabilityChecks); + return true; + } - [Fact] - public void CreateUntypedPropertyValue_ReturnsNull_ForNonUntypedProperty() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - Mock property = new Mock(); - property.Setup(p => p.Type).Returns(EdmCoreModel.Instance.GetInt32(true)); + [Fact] + public void CreateUntypedNestedResourceInfo_ThrowsArgumentNull_StructuralProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateUntypedNestedResourceInfo(null, null, null, null, resourceContext: _entityContext), + "structuralProperty"); + } - // Act - object result = serializer.CreateUntypedPropertyValue(property.Object, new ResourceContext(), out _); + [Fact] + public void CreateUntypedNestedResourceInfo_CreatesCorrectNestedResourceInfo() + { + // Arrange + Mock property = new Mock(); + property.Setup(p => p.Name).Returns("AnyUntypedName"); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + + // Act + ODataNestedResourceInfo untypedNestedResourceInfo + = serializer.Object.CreateUntypedNestedResourceInfo(property.Object, + It.IsAny(), EdmUntypedHelpers.NullableUntypedCollectionReference, null, _entityContext); + + // Assert + Assert.NotNull(untypedNestedResourceInfo); + Assert.Equal("AnyUntypedName", untypedNestedResourceInfo.Name); + Assert.True(untypedNestedResourceInfo.IsCollection); + } - // Assert - Assert.Null(result); - } + [Fact] + public void CreateNavigationLink_ThrowsArgumentNull_NavigationProperty() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateNavigationLink(navigationProperty: null, resourceContext: _entityContext), + "navigationProperty"); + } - [Fact] - public void CreateUntypedPropertyValue_ReturnsRealValue_ForUntypedPropertyWithRealType() - { - // Arrange - Mock property = new Mock(); - property.Setup(p => p.Name).Returns("PropertyName"); - Mock serializerProvider = new Mock(MockBehavior.Strict); - object propertyValue = new List(); - var entity = new { PropertyName = propertyValue }; - Mock innerSerializer = new Mock(ODataPayloadKind.Property); - IEdmTypeReference propertyType = EdmUntypedStructuredTypeReference.NullableTypeReference; - - property.Setup(p => p.Type).Returns(propertyType); - - var serializer = new ODataResourceSerializer(serializerProvider.Object); - ResourceContext entityContext = new ResourceContext(_writeContext, _customerType, entity); - - // Act - object result = serializer.CreateUntypedPropertyValue(property.Object, entityContext, out IEdmTypeReference actualType); - - // Assert - innerSerializer.Verify(); - Assert.Same(propertyValue, result); - Assert.Same(EdmUntypedHelpers.NullableUntypedCollectionReference, actualType); - } + [Fact] + public void CreateNavigationLink_ThrowsArgumentNull_EntityContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + IEdmNavigationProperty navigationProperty = new Mock().Object; + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateNavigationLink(navigationProperty, resourceContext: null), + "resourceContext"); + } - [Fact] - public void CreateUntypedPropertyValue_Calls_CreateODataValueOnInnerSerializer() + [Fact] + public void CreateNavigationLink_CreatesCorrectNavigationLink() + { + // Arrange + Uri navigationLinkUri = new Uri("http://navigation_link"); + IEdmNavigationProperty property1 = CreateFakeNavigationProperty("Property1", _customerType); + OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation { - // Arrange - Mock property = new Mock(); - property.Setup(p => p.Name).Returns("PropertyName"); - Mock serializerProvider = new Mock(MockBehavior.Strict); - var entity = new { PropertyName = 42 }; - Mock innerSerializer = new Mock(ODataPayloadKind.Property); - ODataPrimitiveValue propertyValue = new ODataPrimitiveValue(42); - IEdmTypeReference propertyType = EdmUntypedStructuredTypeReference.NullableTypeReference; - IEdmTypeReference actualType = _writeContext.GetEdmType(propertyValue, typeof(int)); - - property.Setup(p => p.Type).Returns(propertyType); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(actualType)).Returns(innerSerializer.Object); - innerSerializer.Setup(s => s.CreateODataValue(42, actualType, _writeContext)).Returns(propertyValue).Verifiable(); - - var serializer = new ODataResourceSerializer(serializerProvider.Object); - ResourceContext entityContext = new ResourceContext(_writeContext, _customerType, entity); - - // Act - object createdProperty = serializer.CreateUntypedPropertyValue(property.Object, entityContext, out _); - - // Assert - innerSerializer.Verify(); - ODataProperty odataProperty = Assert.IsType(createdProperty); - Assert.Equal("PropertyName", odataProperty.Name); - Assert.Equal(42, odataProperty.Value); - } + NavigationLinkBuilder = (ctxt, property, metadataLevel) => navigationLinkUri + }; + _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); - [Fact] - public void CreateStructuralProperty_ThrowsArgumentNull_StructuralProperty() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateStructuralProperty(structuralProperty: null, resourceContext: null), - "structuralProperty"); - } + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; - [Fact] - public void CreateStructuralProperty_ThrowsArgumentNull_ResourceContext() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - Mock property = new Mock(); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateStructuralProperty(property.Object, resourceContext: null), - "resourceContext"); - } + // Act + ODataNestedResourceInfo navigationLink = serializer.Object.CreateNavigationLink(property1, _entityContext); - [Fact] - public void CreateStructuralProperty_ThrowsSerializationException_TypeCannotBeSerialized() - { - // Arrange - Mock propertyType = new Mock(); - propertyType.Setup(t => t.Definition).Returns(new EdmEntityType("Namespace", "Name")); - Mock property = new Mock(); - Mock serializerProvider = new Mock(MockBehavior.Strict); - IEdmEntityObject entity = new Mock().Object; - property.Setup(p => p.Type).Returns(propertyType.Object); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(propertyType.Object)).Returns(null); - - var serializer = new ODataResourceSerializer(serializerProvider.Object); - - // Act & Assert - ExceptionAssert.Throws( - () => serializer.CreateStructuralProperty(property.Object, new ResourceContext { EdmObject = entity }), - "'Namespace.Name' cannot be serialized using the OData output formatter."); - } + // Assert + Assert.Equal("Property1", navigationLink.Name); + Assert.Equal(navigationLinkUri, navigationLink.Url); + } - [Fact] - public void CreateStructuralProperty_Calls_CreateODataValueOnInnerSerializer() - { - // Arrange - Mock property = new Mock(); - property.Setup(p => p.Name).Returns("PropertyName"); - Mock serializerProvider = new Mock(MockBehavior.Strict); - var entity = new { PropertyName = 42 }; - Mock innerSerializer = new Mock(ODataPayloadKind.Property); - ODataValue propertyValue = new Mock().Object; - IEdmTypeReference propertyType = _writeContext.GetEdmType(propertyValue, typeof(int)); - - property.Setup(p => p.Type).Returns(propertyType); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(propertyType)).Returns(innerSerializer.Object); - innerSerializer.Setup(s => s.CreateODataValue(42, propertyType, _writeContext)).Returns(propertyValue).Verifiable(); - - var serializer = new ODataResourceSerializer(serializerProvider.Object); - ResourceContext entityContext = new ResourceContext(_writeContext, _customerType, entity); - - // Act - ODataProperty createdProperty = serializer.CreateStructuralProperty(property.Object, entityContext); - - // Assert - innerSerializer.Verify(); - Assert.Equal("PropertyName", createdProperty.Name); - Assert.Equal(propertyValue, createdProperty.Value); - } + [Fact] + public void CreateResource_UsesCorrectTypeName() + { + ResourceContext instanceContext = + new ResourceContext { StructuredType = _customerType.EntityDefinition(), SerializerContext = _writeContext }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + SelectExpandNode selectExpandNode = new SelectExpandNode(); - private bool Verify(ResourceContext instanceContext, object instance, ODataSerializerContext writeContext) - { - Assert.Same(instance, (instanceContext.EdmObject as TypedEdmEntityObject).Instance); - Assert.Equal(writeContext.Model, instanceContext.EdmModel); - Assert.Equal(writeContext.NavigationSource, instanceContext.NavigationSource); - Assert.Equal(writeContext.Request, instanceContext.Request); - Assert.Equal(writeContext.SkipExpensiveAvailabilityChecks, instanceContext.SkipExpensiveAvailabilityChecks); - return true; - } + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); - [Fact] - public void CreateUntypedNestedResourceInfo_ThrowsArgumentNull_StructuralProperty() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateUntypedNestedResourceInfo(null, null, null, null, resourceContext: _entityContext), - "structuralProperty"); - } + // Assert + Assert.Equal("Default.Customer", entry.TypeName); + } - [Fact] - public void CreateUntypedNestedResourceInfo_CreatesCorrectNestedResourceInfo() - { - // Arrange - Mock property = new Mock(); - property.Setup(p => p.Name).Returns("AnyUntypedName"); - - Mock serializer = new Mock(_serializerProvider); - serializer.CallBase = true; - - // Act - ODataNestedResourceInfo untypedNestedResourceInfo - = serializer.Object.CreateUntypedNestedResourceInfo(property.Object, - It.IsAny(), EdmUntypedHelpers.NullableUntypedCollectionReference, null, _entityContext); - - // Assert - Assert.NotNull(untypedNestedResourceInfo); - Assert.Equal("AnyUntypedName", untypedNestedResourceInfo.Name); - Assert.True(untypedNestedResourceInfo.IsCollection); - } + [Fact] + public void CreateODataAction_ThrowsArgumentNull_Action() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateODataAction(action: null, resourceContext: null), + "action"); + } - [Fact] - public void CreateNavigationLink_ThrowsArgumentNull_NavigationProperty() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateNavigationLink(navigationProperty: null, resourceContext: _entityContext), - "navigationProperty"); - } + [Fact] + public void CreateODataAction_ThrowsArgumentNull_EntityContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + IEdmAction action = new Mock().Object; + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateODataAction(action, resourceContext: null), + "resourceContext"); + } - [Fact] - public void CreateNavigationLink_ThrowsArgumentNull_EntityContext() + [Fact] + public void CreateResource_WritesCorrectIdLink() + { + // Arrange + ResourceContext instanceContext = new ResourceContext { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - IEdmNavigationProperty navigationProperty = new Mock().Object; - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateNavigationLink(navigationProperty, resourceContext: null), - "resourceContext"); - } + SerializerContext = _writeContext, + StructuredType = _customerType.EntityDefinition() + }; - [Fact] - public void CreateNavigationLink_CreatesCorrectNavigationLink() + bool customIdLinkbuilderCalled = false; + OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation { - // Arrange - Uri navigationLinkUri = new Uri("http://navigation_link"); - IEdmNavigationProperty property1 = CreateFakeNavigationProperty("Property1", _customerType); - OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation + IdLinkBuilder = new SelfLinkBuilder((ResourceContext context) => { - NavigationLinkBuilder = (ctxt, property, metadataLevel) => navigationLinkUri - }; - _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); - - Mock serializer = new Mock(_serializerProvider); - serializer.CallBase = true; - - // Act - ODataNestedResourceInfo navigationLink = serializer.Object.CreateNavigationLink(property1, _entityContext); - - // Assert - Assert.Equal("Property1", navigationLink.Name); - Assert.Equal(navigationLinkUri, navigationLink.Url); - } - - [Fact] - public void CreateResource_UsesCorrectTypeName() - { - ResourceContext instanceContext = - new ResourceContext { StructuredType = _customerType.EntityDefinition(), SerializerContext = _writeContext }; - Mock serializer = new Mock(_serializerProvider); - serializer.CallBase = true; - SelectExpandNode selectExpandNode = new SelectExpandNode(); - - // Act - ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); - - // Assert - Assert.Equal("Default.Customer", entry.TypeName); - } + Assert.Same(instanceContext, context); + customIdLinkbuilderCalled = true; + return new Uri("http://sample_id_link"); + }, + followsConventions: false) + }; + _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + SelectExpandNode selectExpandNode = new SelectExpandNode(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); + + // Assert + Assert.True(customIdLinkbuilderCalled); + } - [Fact] - public void CreateODataAction_ThrowsArgumentNull_Action() + [Fact] + public void WriteObjectInline_WritesCorrectEditLink() + { + // Arrange + ResourceContext instanceContext = new ResourceContext { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateODataAction(action: null, resourceContext: null), - "action"); - } - - [Fact] - public void CreateODataAction_ThrowsArgumentNull_EntityContext() + SerializerContext = _writeContext, + StructuredType = _customerType.EntityDefinition() + }; + bool customEditLinkbuilderCalled = false; + OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - IEdmAction action = new Mock().Object; - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateODataAction(action, resourceContext: null), - "resourceContext"); - } - - [Fact] - public void CreateResource_WritesCorrectIdLink() - { - // Arrange - ResourceContext instanceContext = new ResourceContext - { - SerializerContext = _writeContext, - StructuredType = _customerType.EntityDefinition() - }; - - bool customIdLinkbuilderCalled = false; - OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation + EditLinkBuilder = new SelfLinkBuilder((ResourceContext context) => { - IdLinkBuilder = new SelfLinkBuilder((ResourceContext context) => - { - Assert.Same(instanceContext, context); - customIdLinkbuilderCalled = true; - return new Uri("http://sample_id_link"); - }, - followsConventions: false) - }; - _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); - - Mock serializer = new Mock(_serializerProvider); - serializer.CallBase = true; - SelectExpandNode selectExpandNode = new SelectExpandNode(); - - // Act - ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); - - // Assert - Assert.True(customIdLinkbuilderCalled); - } + Assert.Same(instanceContext, context); + customEditLinkbuilderCalled = true; + return new Uri("http://sample_edit_link"); + }, + followsConventions: false) + }; + _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); + + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + SelectExpandNode selectExpandNode = new SelectExpandNode(); + + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); + + // Assert + Assert.True(customEditLinkbuilderCalled); + } - [Fact] - public void WriteObjectInline_WritesCorrectEditLink() + [Fact] + public void WriteObjectInline_WritesCorrectReadLink() + { + // Arrange + ResourceContext instanceContext = new ResourceContext(_writeContext, _customerType, 42); + bool customReadLinkbuilderCalled = false; + OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation { - // Arrange - ResourceContext instanceContext = new ResourceContext - { - SerializerContext = _writeContext, - StructuredType = _customerType.EntityDefinition() - }; - bool customEditLinkbuilderCalled = false; - OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation + ReadLinkBuilder = new SelfLinkBuilder((ResourceContext context) => { - EditLinkBuilder = new SelfLinkBuilder((ResourceContext context) => - { - Assert.Same(instanceContext, context); - customEditLinkbuilderCalled = true; - return new Uri("http://sample_edit_link"); - }, - followsConventions: false) - }; - _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); + Assert.Same(instanceContext, context); + customReadLinkbuilderCalled = true; + return new Uri("http://sample_read_link"); + }, + followsConventions: false) + }; - Mock serializer = new Mock(_serializerProvider); - serializer.CallBase = true; - SelectExpandNode selectExpandNode = new SelectExpandNode(); - - // Act - ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); - - // Assert - Assert.True(customEditLinkbuilderCalled); - } + _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); - [Fact] - public void WriteObjectInline_WritesCorrectReadLink() - { - // Arrange - ResourceContext instanceContext = new ResourceContext(_writeContext, _customerType, 42); - bool customReadLinkbuilderCalled = false; - OData.Edm.NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation - { - ReadLinkBuilder = new SelfLinkBuilder((ResourceContext context) => - { - Assert.Same(instanceContext, context); - customReadLinkbuilderCalled = true; - return new Uri("http://sample_read_link"); - }, - followsConventions: false) - }; + Mock serializer = new Mock(_serializerProvider); + serializer.CallBase = true; + SelectExpandNode selectExpandNode = new SelectExpandNode(); - _model.SetNavigationSourceLinkBuilder(_customerSet, linkAnnotation); + // Act + ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); - Mock serializer = new Mock(_serializerProvider); - serializer.CallBase = true; - SelectExpandNode selectExpandNode = new SelectExpandNode(); + // Assert + Assert.True(customReadLinkbuilderCalled); + } - // Act - ODataResource entry = serializer.Object.CreateResource(selectExpandNode, instanceContext); + [Fact] + public void CreateSelectExpandNode_ThrowsArgumentNull_EntityContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); - // Assert - Assert.True(customReadLinkbuilderCalled); - } + ExceptionAssert.ThrowsArgumentNull( + () => serializer.CreateSelectExpandNode(resourceContext: null), + "resourceContext"); + } - [Fact] - public void CreateSelectExpandNode_ThrowsArgumentNull_EntityContext() + [Fact] + public void AddTypeNameAnnotationAsNeeded_AddsAnnotation_IfTypeOfPathDoesNotMatchEntryType() + { + // Arrange + string expectedTypeName = "TypeName"; + ODataResource entry = new ODataResource { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSerializer serializer = new ODataResourceSerializer(serializerProvider.Object); + TypeName = expectedTypeName + }; - ExceptionAssert.ThrowsArgumentNull( - () => serializer.CreateSelectExpandNode(resourceContext: null), - "resourceContext"); - } + // Act + ODataResourceSerializer.AddTypeNameAnnotationAsNeeded(entry, _customerType.EntityDefinition(), ODataMetadataLevel.Minimal); - [Fact] - public void AddTypeNameAnnotationAsNeeded_AddsAnnotation_IfTypeOfPathDoesNotMatchEntryType() + // Assert + ODataTypeAnnotation annotation = entry.TypeAnnotation; + Assert.NotNull(annotation); // Guard + Assert.Equal(expectedTypeName, annotation.TypeName); + } + + [Fact] // Issue 984: Redundant type name serialization in OData JSON light minimal metadata mode + public void AddTypeNameAnnotationAsNeeded_AddsAnnotationWithNullValue_IfTypeOfPathMatchesEntryType() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataResource entry = new ODataResource { - // Arrange - string expectedTypeName = "TypeName"; - ODataResource entry = new ODataResource - { - TypeName = expectedTypeName - }; + TypeName = model.SpecialCustomer.FullName() + }; - // Act - ODataResourceSerializer.AddTypeNameAnnotationAsNeeded(entry, _customerType.EntityDefinition(), ODataMetadataLevel.Minimal); + // Act + ODataResourceSerializer.AddTypeNameAnnotationAsNeeded(entry, model.SpecialCustomer, ODataMetadataLevel.Minimal); - // Assert - ODataTypeAnnotation annotation = entry.TypeAnnotation; - Assert.NotNull(annotation); // Guard - Assert.Equal(expectedTypeName, annotation.TypeName); - } + // Assert + ODataTypeAnnotation annotation = entry.TypeAnnotation; + Assert.NotNull(annotation); // Guard + Assert.Null(annotation.TypeName); + } - [Fact] // Issue 984: Redundant type name serialization in OData JSON light minimal metadata mode - public void AddTypeNameAnnotationAsNeeded_AddsAnnotationWithNullValue_IfTypeOfPathMatchesEntryType() + [Theory] + [InlineData("MatchingType", "MatchingType", ODataMetadataLevel.Full, false)] + [InlineData("DoesNotMatch1", "DoesNotMatch2", ODataMetadataLevel.Full, false)] + [InlineData("MatchingType", "MatchingType", ODataMetadataLevel.Minimal, true)] + [InlineData("DoesNotMatch1", "DoesNotMatch2", ODataMetadataLevel.Minimal, false)] + [InlineData("MatchingType", "MatchingType", ODataMetadataLevel.None, true)] + [InlineData("DoesNotMatch1", "DoesNotMatch2", ODataMetadataLevel.None, true)] + public void ShouldSuppressTypeNameSerialization(string resourceType, string entitySetType, + ODataMetadataLevel metadataLevel, bool expectedResult) + { + // Arrange + ODataResource resource = new ODataResource { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataResource entry = new ODataResource - { - TypeName = model.SpecialCustomer.FullName() - }; + // The caller uses a namespace-qualified name, which this test leaves empty. + TypeName = "NS." + resourceType + }; + IEdmEntityType edmType = CreateEntityTypeWithName(entitySetType); - // Act - ODataResourceSerializer.AddTypeNameAnnotationAsNeeded(entry, model.SpecialCustomer, ODataMetadataLevel.Minimal); + // Act + bool actualResult = ODataResourceSerializer.ShouldSuppressTypeNameSerialization(resource, edmType, metadataLevel); - // Assert - ODataTypeAnnotation annotation = entry.TypeAnnotation; - Assert.NotNull(annotation); // Guard - Assert.Null(annotation.TypeName); - } + // Assert + Assert.Equal(expectedResult, actualResult); + } - [Theory] - [InlineData("MatchingType", "MatchingType", ODataMetadataLevel.Full, false)] - [InlineData("DoesNotMatch1", "DoesNotMatch2", ODataMetadataLevel.Full, false)] - [InlineData("MatchingType", "MatchingType", ODataMetadataLevel.Minimal, true)] - [InlineData("DoesNotMatch1", "DoesNotMatch2", ODataMetadataLevel.Minimal, false)] - [InlineData("MatchingType", "MatchingType", ODataMetadataLevel.None, true)] - [InlineData("DoesNotMatch1", "DoesNotMatch2", ODataMetadataLevel.None, true)] - public void ShouldSuppressTypeNameSerialization(string resourceType, string entitySetType, - ODataMetadataLevel metadataLevel, bool expectedResult) - { - // Arrange - ODataResource resource = new ODataResource - { - // The caller uses a namespace-qualified name, which this test leaves empty. - TypeName = "NS." + resourceType - }; - IEdmEntityType edmType = CreateEntityTypeWithName(entitySetType); + [Fact] + public void CreateODataAction_IncludesEverything_ForFullMetadata() + { + // Arrange + string expectedContainerName = "Container"; + string expectedNamespace = "NS"; + string expectedActionName = "Action"; + string expectedTarget = "aa://Target"; + string expectedMetadataPrefix = "http://Metadata"; + + IEdmEntityContainer container = CreateFakeContainer(expectedContainerName); + IEdmAction action = CreateFakeAction(expectedNamespace, expectedActionName, isBindable: true); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri(expectedTarget), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + annotationsManager.SetIsAlwaysBindable(action); + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, expectedMetadataPrefix); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + // In ASP.NET Core, it's using the global UriHelper to pick up the first Router to generate the Uri. + // Owing that there's no router created in this case, a default OData router will be used to generate the Uri. + // The default OData router will add '$metadata' after the route prefix. + string expectedMetadata = expectedMetadataPrefix + "/OData/$metadata#" + expectedNamespace + "." + expectedActionName; + ODataAction expectedAction = new ODataAction + { + Metadata = new Uri(expectedMetadata), + Target = new Uri(expectedTarget), + Title = expectedActionName + }; + + AssertEqual(expectedAction, actualAction); + } - // Act - bool actualResult = ODataResourceSerializer.ShouldSuppressTypeNameSerialization(resource, edmType, metadataLevel); + [Fact] + public void CreateODataAction_OmitsAction_WheOperationLinkBuilderReturnsNull() + { + // Arrange + IEdmAction action = CreateFakeAction("IgnoreAction"); - // Assert - Assert.Equal(expectedResult, actualResult); - } + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => null, followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); - [Fact] - public void CreateODataAction_IncludesEverything_ForFullMetadata() - { - // Arrange - string expectedContainerName = "Container"; - string expectedNamespace = "NS"; - string expectedActionName = "Action"; - string expectedTarget = "aa://Target"; - string expectedMetadataPrefix = "http://Metadata"; - - IEdmEntityContainer container = CreateFakeContainer(expectedContainerName); - IEdmAction action = CreateFakeAction(expectedNamespace, expectedActionName, isBindable: true); - - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri(expectedTarget), - followsConventions: false); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - annotationsManager.SetOperationLinkBuilder(action, linkBuilder); - annotationsManager.SetIsAlwaysBindable(action); - IEdmModel model = CreateFakeModel(annotationsManager); - - ResourceContext context = CreateContext(model, expectedMetadataPrefix); - context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; - - // Act - ODataAction actualAction = _serializer.CreateODataAction(action, context); - - // Assert - // In ASP.NET Core, it's using the global UriHelper to pick up the first Router to generate the Uri. - // Owing that there's no router created in this case, a default OData router will be used to generate the Uri. - // The default OData router will add '$metadata' after the route prefix. - string expectedMetadata = expectedMetadataPrefix + "/OData/$metadata#" + expectedNamespace + "." + expectedActionName; - ODataAction expectedAction = new ODataAction - { - Metadata = new Uri(expectedMetadata), - Target = new Uri(expectedTarget), - Title = expectedActionName - }; + IEdmModel model = CreateFakeModel(annotationsManager); - AssertEqual(expectedAction, actualAction); - } + ResourceContext context = CreateContext(model); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Minimal; - [Fact] - public void CreateODataAction_OmitsAction_WheOperationLinkBuilderReturnsNull() - { - // Arrange - IEdmAction action = CreateFakeAction("IgnoreAction"); + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => null, followsConventions: false); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + // Assert + Assert.Null(actualAction); + } - IEdmModel model = CreateFakeModel(annotationsManager); + [Fact] + public void CreateODataAction_ForJsonLight_OmitsContainerName_PerCreateMetadataFragment() + { + // Arrange + string expectedMetadataPrefix = "http://Metadata"; + string expectedNamespace = "NS"; + string expectedActionName = "Action"; + + IEdmEntityContainer container = CreateFakeContainer("ContainerShouldNotAppearInResult"); + IEdmAction action = CreateFakeAction(expectedNamespace, expectedActionName); + + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + + IEdmModel model = CreateFakeModel(annotationsManager); + + ResourceContext context = CreateContext(model, expectedMetadataPrefix); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Minimal; + + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.NotNull(actualAction); + // Assert + // In ASP.NET Core, it's using the global UriHelper to pick up the first Router to generate the Uri. + // Owing that there's no router created in this case, a default OData router will be used to generate the Uri. + // The default OData router will add '$metadata' after the route prefix. + string expectedMetadata = expectedMetadataPrefix + "/OData/$metadata#" + expectedNamespace + "." + expectedActionName; + AssertEqual(new Uri(expectedMetadata), actualAction.Metadata); + } - ResourceContext context = CreateContext(model); - context.SerializerContext.MetadataLevel = ODataMetadataLevel.Minimal; + [Fact] + public void CreateODataAction_SkipsAlwaysAvailableAction_PerShouldOmitAction() + { + // Arrange + IEdmAction action = CreateFakeAction("action"); - // Act - ODataAction actualAction = _serializer.CreateODataAction(action, context); + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), + followsConventions: true); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + annotationsManager.SetIsAlwaysBindable(action); - // Assert - Assert.Null(actualAction); - } + IEdmModel model = CreateFakeModel(annotationsManager); - [Fact] - public void CreateODataAction_ForJsonLight_OmitsContainerName_PerCreateMetadataFragment() - { - // Arrange - string expectedMetadataPrefix = "http://Metadata"; - string expectedNamespace = "NS"; - string expectedActionName = "Action"; - - IEdmEntityContainer container = CreateFakeContainer("ContainerShouldNotAppearInResult"); - IEdmAction action = CreateFakeAction(expectedNamespace, expectedActionName); - - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), - followsConventions: false); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - annotationsManager.SetOperationLinkBuilder(action, linkBuilder); - - IEdmModel model = CreateFakeModel(annotationsManager); - - ResourceContext context = CreateContext(model, expectedMetadataPrefix); - context.SerializerContext.MetadataLevel = ODataMetadataLevel.Minimal; - - // Act - ODataAction actualAction = _serializer.CreateODataAction(action, context); - - // Assert - Assert.NotNull(actualAction); - // Assert - // In ASP.NET Core, it's using the global UriHelper to pick up the first Router to generate the Uri. - // Owing that there's no router created in this case, a default OData router will be used to generate the Uri. - // The default OData router will add '$metadata' after the route prefix. - string expectedMetadata = expectedMetadataPrefix + "/OData/$metadata#" + expectedNamespace + "." + expectedActionName; - AssertEqual(new Uri(expectedMetadata), actualAction.Metadata); - } + ResourceContext context = CreateContext(model); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Minimal; - [Fact] - public void CreateODataAction_SkipsAlwaysAvailableAction_PerShouldOmitAction() - { - // Arrange - IEdmAction action = CreateFakeAction("action"); + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), - followsConventions: true); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - annotationsManager.SetOperationLinkBuilder(action, linkBuilder); - annotationsManager.SetIsAlwaysBindable(action); + // Assert + Assert.Null(actualAction); + } - IEdmModel model = CreateFakeModel(annotationsManager); + [Fact] + public void CreateODataAction_IncludesTitle() + { + // Arrange + string expectedActionName = "Action"; - ResourceContext context = CreateContext(model); - context.SerializerContext.MetadataLevel = ODataMetadataLevel.Minimal; + IEdmAction action = CreateFakeAction(expectedActionName); - // Act - ODataAction actualAction = _serializer.CreateODataAction(action, context); + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); - // Assert - Assert.Null(actualAction); - } + IEdmModel model = CreateFakeModel(annotationsManager); - [Fact] - public void CreateODataAction_IncludesTitle() - { - // Arrange - string expectedActionName = "Action"; + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; - IEdmAction action = CreateFakeAction(expectedActionName); + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), - followsConventions: false); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + // Assert + Assert.NotNull(actualAction); + Assert.Equal(expectedActionName, actualAction.Title); + } - IEdmModel model = CreateFakeModel(annotationsManager); + [Theory] + [InlineData(ODataMetadataLevel.Minimal)] + [InlineData(ODataMetadataLevel.None)] + public void CreateODataAction_OmitsTitle(ODataMetadataLevel metadataLevel) + { + // Arrange + IEdmAction action = CreateFakeAction("IgnoreAction"); - ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); - context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); - // Act - ODataAction actualAction = _serializer.CreateODataAction(action, context); + IEdmModel model = CreateFakeModel(annotationsManager); - // Assert - Assert.NotNull(actualAction); - Assert.Equal(expectedActionName, actualAction.Title); - } + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = (ODataMetadataLevel)metadataLevel; - [Theory] - [InlineData(ODataMetadataLevel.Minimal)] - [InlineData(ODataMetadataLevel.None)] - public void CreateODataAction_OmitsTitle(ODataMetadataLevel metadataLevel) - { - // Arrange - IEdmAction action = CreateFakeAction("IgnoreAction"); + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), - followsConventions: false); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + // Assert + Assert.NotNull(actualAction); + Assert.Null(actualAction.Title); + } - IEdmModel model = CreateFakeModel(annotationsManager); + [Fact] + public void CreateODataAction_IncludesTarget_IfDoesnotFollowODataConvention() + { + // Arrange + Uri expectedTarget = new Uri("aa://Target"); - ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); - context.SerializerContext.MetadataLevel = (ODataMetadataLevel)metadataLevel; + IEdmAction action = CreateFakeAction("IgnoreAction"); - // Act - ODataAction actualAction = _serializer.CreateODataAction(action, context); + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => expectedTarget, followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); - // Assert - Assert.NotNull(actualAction); - Assert.Null(actualAction.Title); - } + IEdmModel model = CreateFakeModel(annotationsManager); - [Fact] - public void CreateODataAction_IncludesTarget_IfDoesnotFollowODataConvention() - { - // Arrange - Uri expectedTarget = new Uri("aa://Target"); + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; - IEdmAction action = CreateFakeAction("IgnoreAction"); + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => expectedTarget, followsConventions: false); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + // Assert + Assert.NotNull(actualAction); + Assert.Equal(expectedTarget, actualAction.Target); + } - IEdmModel model = CreateFakeModel(annotationsManager); + [Theory] + [InlineData(ODataMetadataLevel.Minimal)] + [InlineData(ODataMetadataLevel.None)] + public void CreateODataAction_OmitsAction_WhenFollowingConventions(ODataMetadataLevel metadataLevel) + { + // Arrange + IEdmAction action = CreateFakeAction("IgnoreAction", isBindable: true); - ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); - context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), + followsConventions: true); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(action, linkBuilder); - // Act - ODataAction actualAction = _serializer.CreateODataAction(action, context); + IEdmModel model = CreateFakeModel(annotationsManager); - // Assert - Assert.NotNull(actualAction); - Assert.Equal(expectedTarget, actualAction.Target); - } + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = metadataLevel; - [Theory] - [InlineData(ODataMetadataLevel.Minimal)] - [InlineData(ODataMetadataLevel.None)] - public void CreateODataAction_OmitsAction_WhenFollowingConventions(ODataMetadataLevel metadataLevel) - { - // Arrange - IEdmAction action = CreateFakeAction("IgnoreAction", isBindable: true); + // Act + ODataAction actualAction = _serializer.CreateODataAction(action, context); + + // Assert + Assert.Null(actualAction); + } - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), - followsConventions: true); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - annotationsManager.SetOperationLinkBuilder(action, linkBuilder); + [Fact] + public void CreateODataFunction_IncludesEverything_ForFullMetadata() + { + // Arrange + string expectedTarget = "aa://Target"; + string expectedMetadataPrefix = "http://Metadata"; - IEdmModel model = CreateFakeModel(annotationsManager); + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); - ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); - context.SerializerContext.MetadataLevel = metadataLevel; + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri(expectedTarget), followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + annotationsManager.SetIsAlwaysBindable(function); + IEdmModel model = CreateFakeModel(annotationsManager); - // Act - ODataAction actualAction = _serializer.CreateODataAction(action, context); + ResourceContext context = CreateContext(model, expectedMetadataPrefix); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; - // Assert - Assert.Null(actualAction); - } + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); - [Fact] - public void CreateODataFunction_IncludesEverything_ForFullMetadata() + // Assert + string expectedMetadata = expectedMetadataPrefix + "#NS.Function"; + ODataFunction expectedFunction = new ODataFunction { - // Arrange - string expectedTarget = "aa://Target"; - string expectedMetadataPrefix = "http://Metadata"; + Metadata = new Uri(expectedMetadata), + Target = new Uri(expectedTarget), + Title = "Function" + }; - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + AssertEqual(actualFunction, actualFunction); + } - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri(expectedTarget), followsConventions: false); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - annotationsManager.SetOperationLinkBuilder(function, linkBuilder); - annotationsManager.SetIsAlwaysBindable(function); - IEdmModel model = CreateFakeModel(annotationsManager); + [Fact] + public void CreateODataFunction_IncludesTitle() + { + // Arrange + string expectedActionName = "Function"; - ResourceContext context = CreateContext(model, expectedMetadataPrefix); - context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); - // Act - ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); - // Assert - string expectedMetadata = expectedMetadataPrefix + "#NS.Function"; - ODataFunction expectedFunction = new ODataFunction - { - Metadata = new Uri(expectedMetadata), - Target = new Uri(expectedTarget), - Title = "Function" - }; + IEdmModel model = CreateFakeModel(annotationsManager); - AssertEqual(actualFunction, actualFunction); - } - - [Fact] - public void CreateODataFunction_IncludesTitle() - { - // Arrange - string expectedActionName = "Function"; + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://IgnoreTarget"), - followsConventions: false); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + // Assert + Assert.NotNull(actualFunction); + Assert.Equal(expectedActionName, actualFunction.Title); + } - IEdmModel model = CreateFakeModel(annotationsManager); + [Theory] + [InlineData(ODataMetadataLevel.Minimal)] + [InlineData(ODataMetadataLevel.None)] + public void CreateODataFunction_OmitsTitle(ODataMetadataLevel metadataLevel) + { + // Arrange + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); - ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); - context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), + followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); - // Act - ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + IEdmModel model = CreateFakeModel(annotationsManager); - // Assert - Assert.NotNull(actualFunction); - Assert.Equal(expectedActionName, actualFunction.Title); - } + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = metadataLevel; - [Theory] - [InlineData(ODataMetadataLevel.Minimal)] - [InlineData(ODataMetadataLevel.None)] - public void CreateODataFunction_OmitsTitle(ODataMetadataLevel metadataLevel) - { - // Arrange - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), - followsConventions: false); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + // Assert + Assert.NotNull(actualFunction); + Assert.Null(actualFunction.Title); + } - IEdmModel model = CreateFakeModel(annotationsManager); + [Fact] + public void CreateODataFunction_IncludesTarget_IfDoesnotFollowODataConvention() + { + // Arrange + Uri expectedTarget = new Uri("aa://Target"); + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); - ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); - context.SerializerContext.MetadataLevel = metadataLevel; + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => expectedTarget, followsConventions: false); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); - // Act - ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + IEdmModel model = CreateFakeModel(annotationsManager); - // Assert - Assert.NotNull(actualFunction); - Assert.Null(actualFunction.Title); - } + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; - [Fact] - public void CreateODataFunction_IncludesTarget_IfDoesnotFollowODataConvention() - { - // Arrange - Uri expectedTarget = new Uri("aa://Target"); - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => expectedTarget, followsConventions: false); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + // Assert + Assert.NotNull(actualFunction); + Assert.Equal(expectedTarget, actualFunction.Target); + } - IEdmModel model = CreateFakeModel(annotationsManager); + [Theory] + [InlineData(ODataMetadataLevel.Minimal)] + [InlineData(ODataMetadataLevel.None)] + public void CreateODataFunction_OmitsAction_WhenFollowingConventions(ODataMetadataLevel metadataLevel) + { + // Arrange + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); - ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); - context.SerializerContext.MetadataLevel = ODataMetadataLevel.Full; + OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), + followsConventions: true); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + annotationsManager.SetOperationLinkBuilder(function, linkBuilder); - // Act - ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + IEdmModel model = CreateFakeModel(annotationsManager); - // Assert - Assert.NotNull(actualFunction); - Assert.Equal(expectedTarget, actualFunction.Target); - } + ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); + context.SerializerContext.MetadataLevel = metadataLevel; - [Theory] - [InlineData(ODataMetadataLevel.Minimal)] - [InlineData(ODataMetadataLevel.None)] - public void CreateODataFunction_OmitsAction_WhenFollowingConventions(ODataMetadataLevel metadataLevel) - { - // Arrange - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + // Act + ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); - OperationLinkBuilder linkBuilder = new OperationLinkBuilder((ResourceContext a) => new Uri("aa://Ignore"), - followsConventions: true); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - annotationsManager.SetOperationLinkBuilder(function, linkBuilder); + // Assert + Assert.Null(actualFunction); + } - IEdmModel model = CreateFakeModel(annotationsManager); + [Fact] + public void CreateMetadataFragment_IncludesNamespaceAndName() + { + // Arrange + string expectedActionName = "Action"; + string expectedNamespace = "NS"; - ResourceContext context = CreateContext(model, "http://IgnoreMetadataPath"); - context.SerializerContext.MetadataLevel = metadataLevel; + IEdmEntityContainer container = CreateFakeContainer("ContainerShouldNotAppearInResult"); + IEdmAction action = CreateFakeAction(expectedNamespace, expectedActionName); - // Act - ODataFunction actualFunction = _serializer.CreateODataFunction(function, context); + IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); + IEdmModel model = CreateFakeModel(annotationsManager); - // Assert - Assert.Null(actualFunction); - } + // Act + string actualFragment = ODataResourceSerializer.CreateMetadataFragment(action); - [Fact] - public void CreateMetadataFragment_IncludesNamespaceAndName() - { - // Arrange - string expectedActionName = "Action"; - string expectedNamespace = "NS"; + // Assert + Assert.Equal(expectedNamespace + "." + expectedActionName, actualFragment); + } - IEdmEntityContainer container = CreateFakeContainer("ContainerShouldNotAppearInResult"); - IEdmAction action = CreateFakeAction(expectedNamespace, expectedActionName); + [Theory] + [InlineData(ODataMetadataLevel.Full, false, false)] + [InlineData(ODataMetadataLevel.Full, true, false)] + [InlineData(ODataMetadataLevel.Minimal, false, false)] + [InlineData(ODataMetadataLevel.Minimal, true, true)] + [InlineData(ODataMetadataLevel.None, false, false)] + [InlineData(ODataMetadataLevel.None, true, true)] + public void TestShouldOmitAction(ODataMetadataLevel metadataLevel, + bool followsConventions, bool expectedResult) + { + // Arrange + IEdmActionImport action = CreateFakeActionImport(true); + IEdmDirectValueAnnotationsManager annonationsManager = CreateFakeAnnotationsManager(); - IEdmDirectValueAnnotationsManager annotationsManager = CreateFakeAnnotationsManager(); - IEdmModel model = CreateFakeModel(annotationsManager); + IEdmModel model = CreateFakeModel(annonationsManager); - // Act - string actualFragment = ODataResourceSerializer.CreateMetadataFragment(action); + OperationLinkBuilder builder = new OperationLinkBuilder((ResourceContext a) => { throw new NotImplementedException(); }, + followsConventions); - // Assert - Assert.Equal(expectedNamespace + "." + expectedActionName, actualFragment); - } + // Act + bool actualResult = ODataResourceSerializer.ShouldOmitOperation(action.Action, builder, metadataLevel); - [Theory] - [InlineData(ODataMetadataLevel.Full, false, false)] - [InlineData(ODataMetadataLevel.Full, true, false)] - [InlineData(ODataMetadataLevel.Minimal, false, false)] - [InlineData(ODataMetadataLevel.Minimal, true, true)] - [InlineData(ODataMetadataLevel.None, false, false)] - [InlineData(ODataMetadataLevel.None, true, true)] - public void TestShouldOmitAction(ODataMetadataLevel metadataLevel, - bool followsConventions, bool expectedResult) - { - // Arrange - IEdmActionImport action = CreateFakeActionImport(true); - IEdmDirectValueAnnotationsManager annonationsManager = CreateFakeAnnotationsManager(); + // Assert + Assert.Equal(expectedResult, actualResult); + } - IEdmModel model = CreateFakeModel(annonationsManager); + [Fact] + public void TestSetTitleAnnotation() + { + // Arrange + IEdmActionImport action = CreateFakeActionImport(true); + IEdmDirectValueAnnotationsManager annonationsManager = CreateFakeAnnotationsManager(); + IEdmModel model = CreateFakeModel(annonationsManager); + string expectedTitle = "The title"; + model.SetOperationTitleAnnotation(action.Operation, new OperationTitleAnnotation(expectedTitle)); + ODataAction odataAction = new ODataAction(); + + // Act + ODataResourceSerializer.EmitTitle(model, action.Operation, odataAction); + + // Assert + Assert.Equal(expectedTitle, odataAction.Title); + } - OperationLinkBuilder builder = new OperationLinkBuilder((ResourceContext a) => { throw new NotImplementedException(); }, - followsConventions); + [Fact] + public void TestSetTitleAnnotation_UsesNameIfNoTitleAnnotationIsPresent() + { + // Arrange + IEdmActionImport action = CreateFakeActionImport(CreateFakeContainer("Container"), "Action"); + IEdmDirectValueAnnotationsManager annonationsManager = CreateFakeAnnotationsManager(); + IEdmModel model = CreateFakeModel(annonationsManager); + ODataAction odataAction = new ODataAction(); - // Act - bool actualResult = ODataResourceSerializer.ShouldOmitOperation(action.Action, builder, metadataLevel); + // Act + ODataResourceSerializer.EmitTitle(model, action.Operation, odataAction); - // Assert - Assert.Equal(expectedResult, actualResult); - } + // Assert + Assert.Equal(action.Operation.Name, odataAction.Title); + } - [Fact] - public void TestSetTitleAnnotation() - { - // Arrange - IEdmActionImport action = CreateFakeActionImport(true); - IEdmDirectValueAnnotationsManager annonationsManager = CreateFakeAnnotationsManager(); - IEdmModel model = CreateFakeModel(annonationsManager); - string expectedTitle = "The title"; - model.SetOperationTitleAnnotation(action.Operation, new OperationTitleAnnotation(expectedTitle)); - ODataAction odataAction = new ODataAction(); - - // Act - ODataResourceSerializer.EmitTitle(model, action.Operation, odataAction); - - // Assert - Assert.Equal(expectedTitle, odataAction.Title); - } + [Fact] + public async Task WriteObjectInlineAsync_SetsParentContext_ForExpandedNavigationProperties() + { + // Arrange + ODataWriter mockWriter = new Mock().Object; + IEdmNavigationProperty ordersProperty = _customerSet.EntityType.DeclaredNavigationProperties().Single(); + Mock expandedItemSerializer = new Mock(ODataPayloadKind.ResourceSet); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(p => p.GetEdmTypeSerializer(ordersProperty.Type)) + .Returns(expandedItemSerializer.Object); - [Fact] - public void TestSetTitleAnnotation_UsesNameIfNoTitleAnnotationIsPresent() + SelectExpandNode selectExpandNode = new SelectExpandNode { - // Arrange - IEdmActionImport action = CreateFakeActionImport(CreateFakeContainer("Container"), "Action"); - IEdmDirectValueAnnotationsManager annonationsManager = CreateFakeAnnotationsManager(); - IEdmModel model = CreateFakeModel(annonationsManager); - ODataAction odataAction = new ODataAction(); - - // Act - ODataResourceSerializer.EmitTitle(model, action.Operation, odataAction); + ExpandedProperties = new Dictionary + { + {ordersProperty, null } + } + }; + Mock serializer = new Mock(serializerProvider.Object); + serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); + serializer.Setup(s => s.CreateResource(selectExpandNode, _entityContext)).Returns(new ODataResource()); + serializer.CallBase = true; + + // Act + await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, mockWriter, _writeContext); + + // Assert + expandedItemSerializer.Verify( + s => s.WriteObjectInlineAsync(It.IsAny(), ordersProperty.Type, mockWriter, + It.Is(c => c.ExpandedResource.SerializerContext == _writeContext))); + } - // Assert - Assert.Equal(action.Operation.Name, odataAction.Title); - } + [Fact] + public async Task WriteObjectInlineAsync_Writes_Nested_Entities_Without_NavigationSource() + { + // Arrange + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.Namespace = "Default"; + builder.EntityType(); + builder.ComplexType(); + var model = builder.GetEdmModel(); - [Fact] - public async Task WriteObjectInlineAsync_SetsParentContext_ForExpandedNavigationProperties() + var result = new Result { - // Arrange - ODataWriter mockWriter = new Mock().Object; - IEdmNavigationProperty ordersProperty = _customerSet.EntityType.DeclaredNavigationProperties().Single(); - Mock expandedItemSerializer = new Mock(ODataPayloadKind.ResourceSet); - Mock serializerProvider = new Mock(); - serializerProvider.Setup(p => p.GetEdmTypeSerializer(ordersProperty.Type)) - .Returns(expandedItemSerializer.Object); - - SelectExpandNode selectExpandNode = new SelectExpandNode + Title = "myResult", + Products = new Product[] { - ExpandedProperties = new Dictionary + new Product { - {ordersProperty, null } + ProductID = 1 + }, + new Product + { + ProductID = 2 } - }; - Mock serializer = new Mock(serializerProvider.Object); - serializer.Setup(s => s.CreateSelectExpandNode(It.IsAny())).Returns(selectExpandNode); - serializer.Setup(s => s.CreateResource(selectExpandNode, _entityContext)).Returns(new ODataResource()); - serializer.CallBase = true; - - // Act - await serializer.Object.WriteObjectInlineAsync(_customer, _customerType, mockWriter, _writeContext); - - // Assert - expandedItemSerializer.Verify( - s => s.WriteObjectInlineAsync(It.IsAny(), ordersProperty.Type, mockWriter, - It.Is(c => c.ExpandedResource.SerializerContext == _writeContext))); - } + } + }; - [Fact] - public async Task WriteObjectInlineAsync_Writes_Nested_Entities_Without_NavigationSource() - { - // Arrange - ODataModelBuilder builder = new ODataConventionModelBuilder(); - builder.Namespace = "Default"; - builder.EntityType(); - builder.ComplexType(); - var model = builder.GetEdmModel(); - - var result = new Result + var resultType = model.FindType("Default.Result") as IEdmComplexType; + var resultTypeReference = new EdmComplexTypeReference(resultType as IEdmComplexType, false); + var titleProperty = resultTypeReference.FindProperty("Title") as IEdmStructuralProperty; + var productsProperty = resultTypeReference.FindNavigationProperty("Products"); + var selectExpand = new SelectExpandClause(new SelectItem[] { - Title = "myResult", - Products = new Product[] - { - new Product - { - ProductID = 1 - }, - new Product - { - ProductID = 2 - } - } - }; + new PathSelectItem(new ODataSelectPath(new PropertySegment(titleProperty))), + new ExpandedNavigationSelectItem(new ODataExpandPath(new NavigationPropertySegment(productsProperty, null)),null,null) + }, + false); - var resultType = model.FindType("Default.Result") as IEdmComplexType; - var resultTypeReference = new EdmComplexTypeReference(resultType as IEdmComplexType, false); - var titleProperty = resultTypeReference.FindProperty("Title") as IEdmStructuralProperty; - var productsProperty = resultTypeReference.FindNavigationProperty("Products"); - var selectExpand = new SelectExpandClause(new SelectItem[] - { - new PathSelectItem(new ODataSelectPath(new PropertySegment(titleProperty))), - new ExpandedNavigationSelectItem(new ODataExpandPath(new NavigationPropertySegment(productsProperty, null)),null,null) - }, - false); + var writeContext = new ODataSerializerContext() + { + Model = model, + SelectExpandClause = selectExpand + }; - var writeContext = new ODataSerializerContext() + using (var stream = new MemoryStream()) + { + IODataResponseMessage responseMessage = new ODataMessageWrapper(stream); + ODataUri uri = new ODataUri { ServiceRoot = new Uri("http://myService", UriKind.Absolute) }; + ODataMessageWriterSettings settings = new ODataMessageWriterSettings { - Model = model, - SelectExpandClause = selectExpand + ODataUri = uri }; - using (var stream = new MemoryStream()) + using (ODataMessageWriter messageWriter = new ODataMessageWriter(responseMessage, settings)) { - IODataResponseMessage responseMessage = new ODataMessageWrapper(stream); - ODataUri uri = new ODataUri { ServiceRoot = new Uri("http://myService", UriKind.Absolute) }; - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = uri - }; + ODataWriter writer = await messageWriter.CreateODataResourceWriterAsync(null, resultType as IEdmComplexType); + ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); + + // Act + await serializer.WriteObjectInlineAsync(result, resultTypeReference, writer, writeContext); - using (ODataMessageWriter messageWriter = new ODataMessageWriter(responseMessage, settings)) + // Assert + stream.Position = 0; + using (StreamReader reader = new StreamReader(stream, leaveOpen: true)) { - ODataWriter writer = await messageWriter.CreateODataResourceWriterAsync(null, resultType as IEdmComplexType); - ODataResourceSerializer serializer = new ODataResourceSerializer(_serializerProvider); - - // Act - await serializer.WriteObjectInlineAsync(result, resultTypeReference, writer, writeContext); - - // Assert - stream.Position = 0; - using (StreamReader reader = new StreamReader(stream, leaveOpen: true)) - { - string response = reader.ReadToEnd(); - Assert.Contains(@"""ProductID"":1", response); - Assert.Contains(@"""ProductID"":2", response); - } + string response = reader.ReadToEnd(); + Assert.Contains(@"""ProductID"":1", response); + Assert.Contains(@"""ProductID"":2", response); } } } + } - [Fact] - public void CreateSelectExpandNode_Caches_SelectExpandNode() - { - // Arrange - IEdmEntityTypeReference customerType = _customerSet.EntityType.AsReference(); - ResourceContext entity1 = new ResourceContext(_writeContext, customerType, new Customer()); - ResourceContext entity2 = new ResourceContext(_writeContext, customerType, new Customer()); + [Fact] + public void CreateSelectExpandNode_Caches_SelectExpandNode() + { + // Arrange + IEdmEntityTypeReference customerType = _customerSet.EntityType.AsReference(); + ResourceContext entity1 = new ResourceContext(_writeContext, customerType, new Customer()); + ResourceContext entity2 = new ResourceContext(_writeContext, customerType, new Customer()); - // Act - var selectExpandNode1 = _serializer.CreateSelectExpandNode(entity1); - var selectExpandNode2 = _serializer.CreateSelectExpandNode(entity2); + // Act + var selectExpandNode1 = _serializer.CreateSelectExpandNode(entity1); + var selectExpandNode2 = _serializer.CreateSelectExpandNode(entity2); - // Assert - Assert.Same(selectExpandNode1, selectExpandNode2); - } + // Assert + Assert.Same(selectExpandNode1, selectExpandNode2); + } - [Fact] - public void CreateSelectExpandNode_ReturnsDifferentSelectExpandNode_IfEntityTypeIsDifferent() - { - // Arrange - IEdmEntityType customerType = _customerSet.EntityType; - IEdmEntityType derivedCustomerType = new EdmEntityType("NS", "DerivedCustomer", customerType); + [Fact] + public void CreateSelectExpandNode_ReturnsDifferentSelectExpandNode_IfEntityTypeIsDifferent() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; + IEdmEntityType derivedCustomerType = new EdmEntityType("NS", "DerivedCustomer", customerType); - ResourceContext entity1 = new ResourceContext(_writeContext, customerType.AsReference(), new Customer()); - ResourceContext entity2 = new ResourceContext(_writeContext, derivedCustomerType.AsReference(), new Customer()); + ResourceContext entity1 = new ResourceContext(_writeContext, customerType.AsReference(), new Customer()); + ResourceContext entity2 = new ResourceContext(_writeContext, derivedCustomerType.AsReference(), new Customer()); - // Act - var selectExpandNode1 = _serializer.CreateSelectExpandNode(entity1); - var selectExpandNode2 = _serializer.CreateSelectExpandNode(entity2); + // Act + var selectExpandNode1 = _serializer.CreateSelectExpandNode(entity1); + var selectExpandNode2 = _serializer.CreateSelectExpandNode(entity2); - // Assert - Assert.NotSame(selectExpandNode1, selectExpandNode2); - } + // Assert + Assert.NotSame(selectExpandNode1, selectExpandNode2); + } - [Fact] - public void CreateSelectExpandNode_ReturnsDifferentSelectExpandNode_IfSelectExpandClauseIsDifferent() - { - // Arrange - IEdmEntityType customerType = _customerSet.EntityType; + [Fact] + public void CreateSelectExpandNode_ReturnsDifferentSelectExpandNode_IfSelectExpandClauseIsDifferent() + { + // Arrange + IEdmEntityType customerType = _customerSet.EntityType; - ResourceContext entity1 = new ResourceContext(_writeContext, customerType.AsReference(), new Customer()); - ResourceContext entity2 = new ResourceContext(_writeContext, customerType.AsReference(), new Customer()); + ResourceContext entity1 = new ResourceContext(_writeContext, customerType.AsReference(), new Customer()); + ResourceContext entity2 = new ResourceContext(_writeContext, customerType.AsReference(), new Customer()); - // Act - _writeContext.SelectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: true); - var selectExpandNode1 = _serializer.CreateSelectExpandNode(entity1); - _writeContext.SelectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: false); - var selectExpandNode2 = _serializer.CreateSelectExpandNode(entity2); + // Act + _writeContext.SelectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: true); + var selectExpandNode1 = _serializer.CreateSelectExpandNode(entity1); + _writeContext.SelectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: false); + var selectExpandNode2 = _serializer.CreateSelectExpandNode(entity2); - // Assert - Assert.NotSame(selectExpandNode1, selectExpandNode2); - } + // Assert + Assert.NotSame(selectExpandNode1, selectExpandNode2); + } - private static IEdmNavigationProperty CreateFakeNavigationProperty(string name, IEdmTypeReference type) - { - Mock property = new Mock(); - property.Setup(p => p.Name).Returns(name); - property.Setup(p => p.Type).Returns(type); - return property.Object; - } + private static IEdmNavigationProperty CreateFakeNavigationProperty(string name, IEdmTypeReference type) + { + Mock property = new Mock(); + property.Setup(p => p.Name).Returns(name); + property.Setup(p => p.Type).Returns(type); + return property.Object; + } - private static void AssertEqual(ODataOperation expected, ODataOperation actual) + private static void AssertEqual(ODataOperation expected, ODataOperation actual) + { + if (expected == null) { - if (expected == null) - { - Assert.Null(actual); - return; - } - - Assert.NotNull(actual); - AssertEqual(expected.Metadata, actual.Metadata); - AssertEqual(expected.Target, actual.Target); - Assert.Equal(expected.Title, actual.Title); + Assert.Null(actual); + return; } - private static void AssertEqual(Uri expected, Uri actual) - { - if (expected == null) - { - Assert.Null(actual); - return; - } - - Assert.NotNull(actual); - Assert.Equal(expected.AbsoluteUri, actual.AbsoluteUri); - } + Assert.NotNull(actual); + AssertEqual(expected.Metadata, actual.Metadata); + AssertEqual(expected.Target, actual.Target); + Assert.Equal(expected.Title, actual.Title); + } - private static ResourceContext CreateContext(IEdmModel model) + private static void AssertEqual(Uri expected, Uri actual) + { + if (expected == null) { - return new ResourceContext - { - EdmModel = model - }; + Assert.Null(actual); + return; } - private static ResourceContext CreateContext(IEdmModel model, string expectedMetadataPrefix) - { - var request = RequestFactory.Create("get", expectedMetadataPrefix, opt => opt.AddRouteComponents("OData", model)); - request.ODataFeature().RoutePrefix = "OData"; - request.ODataFeature().Model = model; - return new ResourceContext - { - EdmModel = model, - Request = request - }; - } + Assert.NotNull(actual); + Assert.Equal(expected.AbsoluteUri, actual.AbsoluteUri); + } - private static IEdmEntityType CreateEntityTypeWithName(string typeName) + private static ResourceContext CreateContext(IEdmModel model) + { + return new ResourceContext { - IEdmEntityType entityType = new EdmEntityType("NS", typeName); - return entityType; - } + EdmModel = model + }; + } - private static IEdmDirectValueAnnotationsManager CreateFakeAnnotationsManager() - { - return new FakeAnnotationsManager(); - } + private static ResourceContext CreateContext(IEdmModel model, string expectedMetadataPrefix) + { + var request = RequestFactory.Create("get", expectedMetadataPrefix, opt => opt.AddRouteComponents("OData", model)); + request.ODataFeature().RoutePrefix = "OData"; + request.ODataFeature().Model = model; + return new ResourceContext + { + EdmModel = model, + Request = request + }; + } - private static IEdmEntityContainer CreateFakeContainer(string name) - { - Mock mock = new Mock(); - mock.Setup(o => o.Name).Returns(name); - return mock.Object; - } + private static IEdmEntityType CreateEntityTypeWithName(string typeName) + { + IEdmEntityType entityType = new EdmEntityType("NS", typeName); + return entityType; + } - private static IEdmActionImport CreateFakeActionImport(IEdmEntityContainer container, string name) - { - Mock mockAction = new Mock(); - mockAction.Setup(o => o.IsBound).Returns(true); - mockAction.Setup(o => o.Name).Returns(name); - Mock mock = new Mock(); - mock.Setup(o => o.Container).Returns(container); - mock.Setup(o => o.Name).Returns(name); - mock.Setup(o => o.Action).Returns(mockAction.Object); - mock.Setup(o => o.Operation).Returns(mockAction.Object); - return mock.Object; - } + private static IEdmDirectValueAnnotationsManager CreateFakeAnnotationsManager() + { + return new FakeAnnotationsManager(); + } - private static IEdmActionImport CreateFakeActionImport(IEdmEntityContainer container, string name, bool isBindable) - { - Mock mock = new Mock(); - mock.Setup(o => o.Container).Returns(container); - mock.Setup(o => o.Name).Returns(name); - Mock mockAction = new Mock(); - mockAction.Setup(o => o.IsBound).Returns(isBindable); - mock.Setup(o => o.Action).Returns(mockAction.Object); - return mock.Object; - } + private static IEdmEntityContainer CreateFakeContainer(string name) + { + Mock mock = new Mock(); + mock.Setup(o => o.Name).Returns(name); + return mock.Object; + } - private static IEdmActionImport CreateFakeActionImport(bool isBindable) - { - Mock mock = new Mock(); - Mock mockAction = new Mock(); - mockAction.Setup(o => o.IsBound).Returns(isBindable); - mock.Setup(o => o.Action).Returns(mockAction.Object); - mock.Setup(o => o.Operation).Returns(mockAction.Object); - return mock.Object; - } + private static IEdmActionImport CreateFakeActionImport(IEdmEntityContainer container, string name) + { + Mock mockAction = new Mock(); + mockAction.Setup(o => o.IsBound).Returns(true); + mockAction.Setup(o => o.Name).Returns(name); + Mock mock = new Mock(); + mock.Setup(o => o.Container).Returns(container); + mock.Setup(o => o.Name).Returns(name); + mock.Setup(o => o.Action).Returns(mockAction.Object); + mock.Setup(o => o.Operation).Returns(mockAction.Object); + return mock.Object; + } - private static IEdmAction CreateFakeAction(string name) - { - return CreateFakeAction(nameSpace: null, name: name, isBindable: true); - } + private static IEdmActionImport CreateFakeActionImport(IEdmEntityContainer container, string name, bool isBindable) + { + Mock mock = new Mock(); + mock.Setup(o => o.Container).Returns(container); + mock.Setup(o => o.Name).Returns(name); + Mock mockAction = new Mock(); + mockAction.Setup(o => o.IsBound).Returns(isBindable); + mock.Setup(o => o.Action).Returns(mockAction.Object); + return mock.Object; + } - private static IEdmAction CreateFakeAction(string name, bool isBindable) - { - return CreateFakeAction(nameSpace: null, name: name, isBindable: isBindable); - } + private static IEdmActionImport CreateFakeActionImport(bool isBindable) + { + Mock mock = new Mock(); + Mock mockAction = new Mock(); + mockAction.Setup(o => o.IsBound).Returns(isBindable); + mock.Setup(o => o.Action).Returns(mockAction.Object); + mock.Setup(o => o.Operation).Returns(mockAction.Object); + return mock.Object; + } - private static IEdmAction CreateFakeAction(string nameSpace, string name) - { - return CreateFakeAction(nameSpace, name, isBindable: true); - } + private static IEdmAction CreateFakeAction(string name) + { + return CreateFakeAction(nameSpace: null, name: name, isBindable: true); + } - private static IEdmAction CreateFakeAction(string nameSpace, string name, bool isBindable) - { - Mock mockAction = new Mock(); - mockAction.SetupGet(o => o.Namespace).Returns(nameSpace); - mockAction.SetupGet(o => o.Name).Returns(name); - mockAction.Setup(o => o.IsBound).Returns(isBindable); - Mock mockParameter = new Mock(); - mockParameter.SetupGet(o => o.DeclaringOperation).Returns(mockAction.Object); - Mock mockEntityTyeRef = new Mock(); - mockEntityTyeRef.Setup(o => o.Definition).Returns(new Mock().Object); - mockParameter.SetupGet(o => o.Type).Returns(mockEntityTyeRef.Object); - mockAction.SetupGet(o => o.Parameters).Returns(new[] { mockParameter.Object }); - return mockAction.Object; - } + private static IEdmAction CreateFakeAction(string name, bool isBindable) + { + return CreateFakeAction(nameSpace: null, name: name, isBindable: isBindable); + } - private static IEdmModel CreateFakeModel(IEdmDirectValueAnnotationsManager annotationsManager) - { - Mock model = new Mock(); - model.Setup(m => m.DirectValueAnnotationsManager).Returns(annotationsManager); - return model.Object; - } + private static IEdmAction CreateFakeAction(string nameSpace, string name) + { + return CreateFakeAction(nameSpace, name, isBindable: true); + } - private static IServiceProvider GetServiceProvider() - { - IServiceCollection services = new ServiceCollection(); + private static IEdmAction CreateFakeAction(string nameSpace, string name, bool isBindable) + { + Mock mockAction = new Mock(); + mockAction.SetupGet(o => o.Namespace).Returns(nameSpace); + mockAction.SetupGet(o => o.Name).Returns(name); + mockAction.Setup(o => o.IsBound).Returns(isBindable); + Mock mockParameter = new Mock(); + mockParameter.SetupGet(o => o.DeclaringOperation).Returns(mockAction.Object); + Mock mockEntityTyeRef = new Mock(); + mockEntityTyeRef.Setup(o => o.Definition).Returns(new Mock().Object); + mockParameter.SetupGet(o => o.Type).Returns(mockEntityTyeRef.Object); + mockAction.SetupGet(o => o.Parameters).Returns(new[] { mockParameter.Object }); + return mockAction.Object; + } - services.AddSingleton(); + private static IEdmModel CreateFakeModel(IEdmDirectValueAnnotationsManager annotationsManager) + { + Mock model = new Mock(); + model.Setup(m => m.DirectValueAnnotationsManager).Returns(annotationsManager); + return model.Object; + } - // Serializers. - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + private static IServiceProvider GetServiceProvider() + { + IServiceCollection services = new ServiceCollection(); - return services.BuildServiceProvider(); - } + services.AddSingleton(); - private class Customer - { - public Customer() - { - this.Orders = new List(); - } - public int ID { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public string City { get; set; } - public IList Orders { get; private set; } - } + // Serializers. + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - private class SpecialCustomer - { - public IList SpecialOrders { get; private set; } - } + return services.BuildServiceProvider(); + } - private class Order + private class Customer + { + public Customer() { - public int ID { get; set; } - public string Name { get; set; } - public string Shipment { get; set; } - public Customer Customer { get; set; } + this.Orders = new List(); } + public int ID { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string City { get; set; } + public IList Orders { get; private set; } + } + + private class SpecialCustomer + { + public IList SpecialOrders { get; private set; } + } - private class Result + private class Order + { + public int ID { get; set; } + public string Name { get; set; } + public string Shipment { get; set; } + public Customer Customer { get; set; } + } + + private class Result + { + public string Title { get; set; } + public IList Products { get; set; } + } + + private class SpecialOrder + { + public SpecialCustomer SpecialCustomer { get; set; } + } + + private class FakeBindableOperationFinder : BindableOperationFinder + { + private IEdmOperation[] _operations; + + public FakeBindableOperationFinder(params IEdmOperation[] operations) + : base(EdmCoreModel.Instance) { - public string Title { get; set; } - public IList Products { get; set; } + _operations = operations; } - private class SpecialOrder + public override IEnumerable FindOperations(IEdmEntityType entityType) { - public SpecialCustomer SpecialCustomer { get; set; } + return _operations; } + } + + private class FakeAnnotationsManager : IEdmDirectValueAnnotationsManager + { + IDictionary, object> annotations = + new Dictionary, object>(); - private class FakeBindableOperationFinder : BindableOperationFinder + public object GetAnnotationValue(IEdmElement element, string namespaceName, string localName) { - private IEdmOperation[] _operations; + object value; - public FakeBindableOperationFinder(params IEdmOperation[] operations) - : base(EdmCoreModel.Instance) + if (!annotations.TryGetValue(CreateKey(element, namespaceName, localName), out value)) { - _operations = operations; + return null; } - public override IEnumerable FindOperations(IEdmEntityType entityType) - { - return _operations; - } + return value; } - private class FakeAnnotationsManager : IEdmDirectValueAnnotationsManager + public object[] GetAnnotationValues(IEnumerable annotations) { - IDictionary, object> annotations = - new Dictionary, object>(); - - public object GetAnnotationValue(IEdmElement element, string namespaceName, string localName) - { - object value; - - if (!annotations.TryGetValue(CreateKey(element, namespaceName, localName), out value)) - { - return null; - } - - return value; - } - - public object[] GetAnnotationValues(IEnumerable annotations) - { - throw new NotImplementedException(); - } - - public IEnumerable GetDirectValueAnnotations(IEdmElement element) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); + } - public void SetAnnotationValue(IEdmElement element, string namespaceName, string localName, object value) - { - annotations[CreateKey(element, namespaceName, localName)] = value; - } + public IEnumerable GetDirectValueAnnotations(IEdmElement element) + { + throw new NotImplementedException(); + } - public void SetAnnotationValues(IEnumerable annotations) - { - throw new NotImplementedException(); - } + public void SetAnnotationValue(IEdmElement element, string namespaceName, string localName, object value) + { + annotations[CreateKey(element, namespaceName, localName)] = value; + } - private static Tuple CreateKey(IEdmElement element, string namespaceName, - string localName) - { - return new Tuple(element, namespaceName, localName); - } + public void SetAnnotationValues(IEnumerable annotations) + { + throw new NotImplementedException(); } + private static Tuple CreateKey(IEdmElement element, string namespaceName, + string localName) + { + return new Tuple(element, namespaceName, localName); + } } + } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSetSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSetSerializerTests.cs index 00e36f9ac..ff9250c51 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSetSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataResourceSetSerializerTests.cs @@ -31,901 +31,900 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataResourceSetSerializerTests { - public class ODataResourceSetSerializerTests + IEdmModel _model; + IEdmEntitySet _customerSet; + Customer[] _customers; + ODataResourceSetSerializer _serializer; + IEdmCollectionTypeReference _customersType; + IEdmCollectionTypeReference _addressesType; + ODataSerializerContext _writeContext; + + public ODataResourceSetSerializerTests() { - IEdmModel _model; - IEdmEntitySet _customerSet; - Customer[] _customers; - ODataResourceSetSerializer _serializer; - IEdmCollectionTypeReference _customersType; - IEdmCollectionTypeReference _addressesType; - ODataSerializerContext _writeContext; + _model = SerializationTestsHelpers.SimpleCustomerOrderModel(); + _customerSet = _model.EntityContainer.FindEntitySet("Customers"); + IEdmComplexType addressType = _model.SchemaElements.OfType() + .First(c => c.Name == "Address"); + _model.SetAnnotationValue(_customerSet.EntityType, new ClrTypeAnnotation(typeof(Customer))); + _model.SetAnnotationValue(addressType, new ClrTypeAnnotation(typeof(Address))); + _customers = new[] { + new Customer() + { + FirstName = "Foo", + LastName = "Bar", + ID = 10, + }, + new Customer() + { + FirstName = "Foo", + LastName = "Bar", + ID = 42, + } + }; - public ODataResourceSetSerializerTests() - { - _model = SerializationTestsHelpers.SimpleCustomerOrderModel(); - _customerSet = _model.EntityContainer.FindEntitySet("Customers"); - IEdmComplexType addressType = _model.SchemaElements.OfType() - .First(c => c.Name == "Address"); - _model.SetAnnotationValue(_customerSet.EntityType, new ClrTypeAnnotation(typeof(Customer))); - _model.SetAnnotationValue(addressType, new ClrTypeAnnotation(typeof(Address))); - _customers = new[] { - new Customer() - { - FirstName = "Foo", - LastName = "Bar", - ID = 10, - }, - new Customer() - { - FirstName = "Foo", - LastName = "Bar", - ID = 42, - } - }; + _customersType = _model.GetEdmTypeReference(typeof(Customer[])).AsCollection(); + _addressesType = _model.GetEdmTypeReference(typeof(Address[])).AsCollection(); + _writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model }; + } - _customersType = _model.GetEdmTypeReference(typeof(Customer[])).AsCollection(); - _addressesType = _model.GetEdmTypeReference(typeof(Address[])).AsCollection(); - _writeContext = new ODataSerializerContext() { NavigationSource = _customerSet, Model = _model }; - } + [Fact] + public void Ctor_ThrowsArgumentNull_SerializerProvider() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataResourceSetSerializer(serializerProvider: null), "serializerProvider"); + } - [Fact] - public void Ctor_ThrowsArgumentNull_SerializerProvider() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataResourceSetSerializer(serializerProvider: null), "serializerProvider"); - } + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + + // Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: null, type: null, messageWriter: null, writeContext: new ODataSerializerContext()), + "messageWriter"); + } - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() - { - // Arrange & Act - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - - // Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: null, type: null, messageWriter: null, writeContext: new ODataSerializerContext()), - "messageWriter"); - } + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_WriteContext() + { + // Arrange & Act + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + + // Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(graph: null, type: null, messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: null), + "writeContext"); + } - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_WriteContext() - { - // Arrange & Act - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - - // Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(graph: null, type: null, messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: null), - "writeContext"); - } + [Fact] + public async Task WriteObjectAsync_Calls_WriteObjectInline() + { + // Arrange + object graph = new object();//Enumerable.Empty(); + Mock serializerProvider = new Mock(); + Mock serializer = new Mock(serializerProvider.Object); + serializer.CallBase = true; + serializer + .Setup(s => s.WriteObjectInlineAsync(graph, It.Is(e => _customersType.IsEquivalentTo(e)), + It.IsAny(), _writeContext)) + .Returns(Task.CompletedTask) + .Verifiable(); + + // Act + await serializer.Object.WriteObjectAsync(graph, typeof(Customer[]), ODataTestUtil.GetMockODataMessageWriter(), _writeContext); + + // Assert + serializer.Verify(); + } - [Fact] - public async Task WriteObjectAsync_Calls_WriteObjectInline() - { - // Arrange - object graph = new object();//Enumerable.Empty(); - Mock serializerProvider = new Mock(); - Mock serializer = new Mock(serializerProvider.Object); - serializer.CallBase = true; - serializer - .Setup(s => s.WriteObjectInlineAsync(graph, It.Is(e => _customersType.IsEquivalentTo(e)), - It.IsAny(), _writeContext)) - .Returns(Task.CompletedTask) - .Verifiable(); - - // Act - await serializer.Object.WriteObjectAsync(graph, typeof(Customer[]), ODataTestUtil.GetMockODataMessageWriter(), _writeContext); - - // Assert - serializer.Verify(); - } + [Fact] + public async Task WriteObjectAsync_CanWriteTopLevelResourceSetContainsNullComplexElement() + { + // Arrange + IODataSerializerProvider serializerProvider = GetServiceProvider().GetService(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider); + MemoryStream stream = new MemoryStream(); + IODataResponseMessageAsync message = new ODataMessageWrapper(stream); - [Fact] - public async Task WriteObjectAsync_CanWriteTopLevelResourceSetContainsNullComplexElement() + ODataMessageWriterSettings settings = new ODataMessageWriterSettings { - // Arrange - IODataSerializerProvider serializerProvider = GetServiceProvider().GetService(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider); - MemoryStream stream = new MemoryStream(); - IODataResponseMessageAsync message = new ODataMessageWrapper(stream); + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } + }; + settings.SetContentType(ODataFormat.Json); - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } - }; - settings.SetContentType(ODataFormat.Json); - - ODataMessageWriter writer = new ODataMessageWriter(message, settings); - IList
addresses = new[] - { - new Address { City = "Redmond" }, - null, - new Address { City = "Shanghai" } - }; + ODataMessageWriter writer = new ODataMessageWriter(message, settings); + IList
addresses = new[] + { + new Address { City = "Redmond" }, + null, + new Address { City = "Shanghai" } + }; - ODataSerializerContext writeContext = new ODataSerializerContext { Model = _model }; + ODataSerializerContext writeContext = new ODataSerializerContext { Model = _model }; - // Act - await serializer.WriteObjectAsync(addresses, typeof(IList
), writer, writeContext); - stream.Seek(0, SeekOrigin.Begin); - string result = new StreamReader(stream).ReadToEnd(); + // Act + await serializer.WriteObjectAsync(addresses, typeof(IList
), writer, writeContext); + stream.Seek(0, SeekOrigin.Begin); + string result = new StreamReader(stream).ReadToEnd(); - // Assert - Assert.Equal("{\"@odata.context\":\"http://any/$metadata#Collection(Default.Address)\"," + + // Assert + Assert.Equal("{\"@odata.context\":\"http://any/$metadata#Collection(Default.Address)\"," + "\"value\":[" + - "{\"Street\":null,\"City\":\"Redmond\",\"State\":null,\"CountryOrRegion\":null,\"ZipCode\":null}," + - "null," + - "{\"Street\":null,\"City\":\"Shanghai\",\"State\":null,\"CountryOrRegion\":null,\"ZipCode\":null}" + - "]}", result); - } +"{\"Street\":null,\"City\":\"Redmond\",\"State\":null,\"CountryOrRegion\":null,\"ZipCode\":null}," + +"null," + +"{\"Street\":null,\"City\":\"Shanghai\",\"State\":null,\"CountryOrRegion\":null,\"ZipCode\":null}" + +"]}", result); + } - [Fact] - public async Task WriteObjectAsync_CanWrite_TopLevelResourceSet_ContainsEmptyCollectionOfDynamicComplexElement() - { - // Arrange - IODataSerializerProvider serializerProvider = GetServiceProvider().GetService(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider); - MemoryStream stream = new MemoryStream(); - IODataResponseMessageAsync message = new ODataMessageWrapper(stream); + [Fact] + public async Task WriteObjectAsync_CanWrite_TopLevelResourceSet_ContainsEmptyCollectionOfDynamicComplexElement() + { + // Arrange + IODataSerializerProvider serializerProvider = GetServiceProvider().GetService(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider); + MemoryStream stream = new MemoryStream(); + IODataResponseMessageAsync message = new ODataMessageWrapper(stream); - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } - }; - settings.SetContentType(ODataFormat.Json); + ODataMessageWriterSettings settings = new ODataMessageWriterSettings + { + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } + }; + settings.SetContentType(ODataFormat.Json); - ODataMessageWriter writer = new ODataMessageWriter(message, settings); - IList addresses = new[] + ODataMessageWriter writer = new ODataMessageWriter(message, settings); + IList addresses = new[] + { + new SimpleOpenAddress { - new SimpleOpenAddress - { - City = "Redmond", - Street = "Microsoft Rd", - Properties = new Dictionary - { - { "StringProp", "abc" }, - { "Locations", new SimpleOpenAddress[] {} } // empty collection of complex - } - } - }; - - var builder = new ODataConventionModelBuilder(); - builder.ComplexType(); - IEdmModel model = builder.GetEdmModel(); - ODataSerializerContext writeContext = new ODataSerializerContext { Model = model }; - - // Act - await serializer.WriteObjectAsync(addresses, typeof(IList), writer, writeContext); - stream.Seek(0, SeekOrigin.Begin); - JObject result = JObject.Parse(await new StreamReader(stream).ReadToEndAsync());//.ToString(); - - // Assert - Assert.Equal(JObject.Parse(@"{ - ""@odata.context"": ""http://any/$metadata#Collection(Microsoft.AspNetCore.OData.Tests.Formatter.Models.SimpleOpenAddress)"", - ""value"": [ + City = "Redmond", + Street = "Microsoft Rd", + Properties = new Dictionary { - ""Street"": ""Microsoft Rd"", - ""City"": ""Redmond"", - ""StringProp"": ""abc"", - ""Locations@odata.type"": ""#Collection(Microsoft.AspNetCore.OData.Tests.Formatter.Models.SimpleOpenAddress)"", - ""Locations"": [] + { "StringProp", "abc" }, + { "Locations", new SimpleOpenAddress[] {} } // empty collection of complex } - ] - }"), result); - } + } + }; + + var builder = new ODataConventionModelBuilder(); + builder.ComplexType(); + IEdmModel model = builder.GetEdmModel(); + ODataSerializerContext writeContext = new ODataSerializerContext { Model = model }; + + // Act + await serializer.WriteObjectAsync(addresses, typeof(IList), writer, writeContext); + stream.Seek(0, SeekOrigin.Begin); + JObject result = JObject.Parse(await new StreamReader(stream).ReadToEndAsync());//.ToString(); + + // Assert + Assert.Equal(JObject.Parse(@"{ + ""@odata.context"": ""http://any/$metadata#Collection(Microsoft.AspNetCore.OData.Tests.Formatter.Models.SimpleOpenAddress)"", + ""value"": [ + { + ""Street"": ""Microsoft Rd"", + ""City"": ""Redmond"", + ""StringProp"": ""abc"", + ""Locations@odata.type"": ""#Collection(Microsoft.AspNetCore.OData.Tests.Formatter.Models.SimpleOpenAddress)"", + ""Locations"": [] + } + ] + }"), result); + } - [Fact] - public async Task WriteObjectInlineAsync_ThrowsArgumentNull_Writer() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: null, writeContext: new ODataSerializerContext()), - "writer"); - } + [Fact] + public async Task WriteObjectInlineAsync_ThrowsArgumentNull_Writer() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: null, writeContext: new ODataSerializerContext()), + "writer"); + } - [Fact] - public async Task WriteObjectInlineAsync_ThrowsArgumentNull_WriteContext() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: new Mock().Object, writeContext: null), - "writeContext"); - } + [Fact] + public async Task WriteObjectInlineAsync_ThrowsArgumentNull_WriteContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectInlineAsync(graph: null, expectedType: null, writer: new Mock().Object, writeContext: null), + "writeContext"); + } - [Fact] - public async Task WriteObjectInlineAsync_ThrowsSerializationException_CannotSerializerNull() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectInlineAsync(graph: null, expectedType: _customersType, - writer: new Mock().Object, writeContext: _writeContext), - "Cannot serialize a null 'ResourceSet'."); - } + [Fact] + public async Task WriteObjectInlineAsync_ThrowsSerializationException_CannotSerializerNull() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectInlineAsync(graph: null, expectedType: _customersType, + writer: new Mock().Object, writeContext: _writeContext), + "Cannot serialize a null 'ResourceSet'."); + } - [Fact] - public async Task WriteObjectInlineAsync_ThrowsSerializationException_IfGraphIsNotEnumerable() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectInlineAsync(graph: 42, expectedType: _customersType, - writer: new Mock().Object, writeContext: _writeContext), - "ODataResourceSetSerializer cannot write an object of type 'System.Int32'."); - } + [Fact] + public async Task WriteObjectInlineAsync_ThrowsSerializationException_IfGraphIsNotEnumerable() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectInlineAsync(graph: 42, expectedType: _customersType, + writer: new Mock().Object, writeContext: _writeContext), + "ODataResourceSetSerializer cannot write an object of type 'System.Int32'."); + } - [Fact] - public async Task WriteObjectInlineAsync_Throws_NullElementInCollection_IfResourceSetContainsNullElement() - { - // Arrange - IEnumerable instance = new object[] { null }; - Mock serializerProvider = new Mock(); - Mock resourceSerializer = new Mock(serializerProvider.Object); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(resourceSerializer.Object); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectInlineAsync(instance, _customersType, new Mock().Object, _writeContext), - "Collections cannot contain null elements."); - } + [Fact] + public async Task WriteObjectInlineAsync_Throws_NullElementInCollection_IfResourceSetContainsNullElement() + { + // Arrange + IEnumerable instance = new object[] { null }; + Mock serializerProvider = new Mock(); + Mock resourceSerializer = new Mock(serializerProvider.Object); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(resourceSerializer.Object); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectInlineAsync(instance, _customersType, new Mock().Object, _writeContext), + "Collections cannot contain null elements."); + } - [Fact] - public void WriteObjectInlineAsync_DoesnotThrow_NullElementInCollection_IfResourceSetContainsNullComplexElement() - { - // Arrange - IEnumerable instance = new object[] { null }; - Mock serializerProvider = new Mock(); - Mock resourceSerializer = new Mock(serializerProvider.Object); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(resourceSerializer.Object); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - ODataSerializerContext writeContext = new ODataSerializerContext { NavigationSource = null, Model = _model }; - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => serializer.WriteObjectInlineAsync(instance, _addressesType, new Mock().Object, writeContext).Wait()); - } + [Fact] + public void WriteObjectInlineAsync_DoesnotThrow_NullElementInCollection_IfResourceSetContainsNullComplexElement() + { + // Arrange + IEnumerable instance = new object[] { null }; + Mock serializerProvider = new Mock(); + Mock resourceSerializer = new Mock(serializerProvider.Object); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(resourceSerializer.Object); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + ODataSerializerContext writeContext = new ODataSerializerContext { NavigationSource = null, Model = _model }; + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => serializer.WriteObjectInlineAsync(instance, _addressesType, new Mock().Object, writeContext).Wait()); + } - [Fact] - public async Task WriteObjectInlineAsync_Throws_TypeCannotBeSerialized_IfResourceSetContainsEntityThatCannotBeSerialized() - { - // Arrange - Mock serializerProvider = new Mock(); - var request = RequestFactory.Create(); - serializerProvider.Setup(s => s.GetODataPayloadSerializer(typeof(int), request)).Returns(null); - IEnumerable instance = new object[] { 42 }; - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectInlineAsync(instance, _customersType, new Mock().Object, _writeContext), - "'Default.Customer' cannot be serialized using the OData output formatter."); - } + [Fact] + public async Task WriteObjectInlineAsync_Throws_TypeCannotBeSerialized_IfResourceSetContainsEntityThatCannotBeSerialized() + { + // Arrange + Mock serializerProvider = new Mock(); + var request = RequestFactory.Create(); + serializerProvider.Setup(s => s.GetODataPayloadSerializer(typeof(int), request)).Returns(null); + IEnumerable instance = new object[] { 42 }; + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectInlineAsync(instance, _customersType, new Mock().Object, _writeContext), + "'Default.Customer' cannot be serialized using the OData output formatter."); + } - [Fact] - public async Task WriteObjectInlineAsync_Calls_CreateResourceSet() - { - // Arrange - IEnumerable instance = new object[0]; - Mock serializerProvider = new Mock(); - Mock resourceSerializer = new Mock(serializerProvider.Object); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(resourceSerializer.Object); - Mock serializer = new Mock(serializerProvider.Object); - serializer.CallBase = true; - serializer.Setup(s => s.CreateResourceSet(instance, _customersType, _writeContext)).Returns(new ODataResourceSet()).Verifiable(); - - // Act - await serializer.Object.WriteObjectInlineAsync(instance, _customersType, new Mock().Object, _writeContext); - - // Assert - serializer.Verify(); - } + [Fact] + public async Task WriteObjectInlineAsync_Calls_CreateResourceSet() + { + // Arrange + IEnumerable instance = new object[0]; + Mock serializerProvider = new Mock(); + Mock resourceSerializer = new Mock(serializerProvider.Object); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(resourceSerializer.Object); + Mock serializer = new Mock(serializerProvider.Object); + serializer.CallBase = true; + serializer.Setup(s => s.CreateResourceSet(instance, _customersType, _writeContext)).Returns(new ODataResourceSet()).Verifiable(); + + // Act + await serializer.Object.WriteObjectInlineAsync(instance, _customersType, new Mock().Object, _writeContext); + + // Assert + serializer.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_Throws_CannotSerializerNull_IfCreateResourceSetReturnsNull() - { - // Arrange - IEnumerable instance = new object[0]; - Mock serializerProvider = new Mock(); - Mock serializer = new Mock(serializerProvider.Object); - serializer.CallBase = true; - serializer.Setup(s => s.CreateResourceSet(instance, _customersType, _writeContext)).Returns(null); - ODataWriter writer = new Mock().Object; - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.Object.WriteObjectInlineAsync(instance, _customersType, writer, _writeContext), - "Cannot serialize a null 'ResourceSet'."); - } + [Fact] + public async Task WriteObjectInlineAsync_Throws_CannotSerializerNull_IfCreateResourceSetReturnsNull() + { + // Arrange + IEnumerable instance = new object[0]; + Mock serializerProvider = new Mock(); + Mock serializer = new Mock(serializerProvider.Object); + serializer.CallBase = true; + serializer.Setup(s => s.CreateResourceSet(instance, _customersType, _writeContext)).Returns(null); + ODataWriter writer = new Mock().Object; + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.Object.WriteObjectInlineAsync(instance, _customersType, writer, _writeContext), + "Cannot serialize a null 'ResourceSet'."); + } - [Fact] - public async Task WriteObjectInlineAsync_Writes_CreateResourceSetOutput() - { - // Arrange - IEnumerable instance = new object[0]; - ODataResourceSet resourceSet = new ODataResourceSet(); - Mock serializerProvider = new Mock(); - Mock resourceSerializer = new Mock(serializerProvider.Object); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(resourceSerializer.Object); - Mock serializer = new Mock(serializerProvider.Object); - serializer.CallBase = true; - serializer.Setup(s => s.CreateResourceSet(instance, _customersType, _writeContext)).Returns(resourceSet); - Mock writer = new Mock(); - writer.Setup(s => s.WriteStartAsync(resourceSet)).Verifiable(); - - // Act - await serializer.Object.WriteObjectInlineAsync(instance, _customersType, writer.Object, _writeContext); - - // Assert - writer.Verify(); - } + [Fact] + public async Task WriteObjectInlineAsync_Writes_CreateResourceSetOutput() + { + // Arrange + IEnumerable instance = new object[0]; + ODataResourceSet resourceSet = new ODataResourceSet(); + Mock serializerProvider = new Mock(); + Mock resourceSerializer = new Mock(serializerProvider.Object); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(resourceSerializer.Object); + Mock serializer = new Mock(serializerProvider.Object); + serializer.CallBase = true; + serializer.Setup(s => s.CreateResourceSet(instance, _customersType, _writeContext)).Returns(resourceSet); + Mock writer = new Mock(); + writer.Setup(s => s.WriteStartAsync(resourceSet)).Verifiable(); + + // Act + await serializer.Object.WriteObjectInlineAsync(instance, _customersType, writer.Object, _writeContext); + + // Assert + writer.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_WritesEachEntityInstance() - { - // Arrange - Mock customerSerializer = new Mock(ODataPayloadKind.Resource); - IODataSerializerProvider provider = ODataTestUtil.GetMockODataSerializerProvider(customerSerializer.Object); - var mockWriter = new Mock(); + [Fact] + public async Task WriteObjectInlineAsync_WritesEachEntityInstance() + { + // Arrange + Mock customerSerializer = new Mock(ODataPayloadKind.Resource); + IODataSerializerProvider provider = ODataTestUtil.GetMockODataSerializerProvider(customerSerializer.Object); + var mockWriter = new Mock(); - customerSerializer.Setup(s => s.WriteObjectInlineAsync(_customers[0], _customersType.ElementType(), mockWriter.Object, _writeContext)).Verifiable(); - customerSerializer.Setup(s => s.WriteObjectInlineAsync(_customers[1], _customersType.ElementType(), mockWriter.Object, _writeContext)).Verifiable(); + customerSerializer.Setup(s => s.WriteObjectInlineAsync(_customers[0], _customersType.ElementType(), mockWriter.Object, _writeContext)).Verifiable(); + customerSerializer.Setup(s => s.WriteObjectInlineAsync(_customers[1], _customersType.ElementType(), mockWriter.Object, _writeContext)).Verifiable(); - _serializer = new ODataResourceSetSerializer(provider); + _serializer = new ODataResourceSetSerializer(provider); - // Act - await _serializer.WriteObjectInlineAsync(_customers, _customersType, mockWriter.Object, _writeContext); + // Act + await _serializer.WriteObjectInlineAsync(_customers, _customersType, mockWriter.Object, _writeContext); - // Assert - customerSerializer.Verify(); - } + // Assert + customerSerializer.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_WritesEachResourceSetItemInstance_Untyped() + [Fact] + public async Task WriteObjectInlineAsync_WritesEachResourceSetItemInstance_Untyped() + { + // Arrange + IList lists = new List() { - // Arrange - IList lists = new List() - { - new List{1}, - new List{2} - }; - ODataSerializerContext writeContext = new ODataSerializerContext(); + new List{1}, + new List{2} + }; + ODataSerializerContext writeContext = new ODataSerializerContext(); - IEdmTypeReference edmType = EdmUntypedHelpers.NullableUntypedCollectionReference; + IEdmTypeReference edmType = EdmUntypedHelpers.NullableUntypedCollectionReference; - Mock customerSerializer = new Mock(ODataPayloadKind.ResourceSet); - IODataSerializerProvider provider = ODataTestUtil.GetMockODataSerializerProvider(customerSerializer.Object); - var mockWriter = new Mock(); + Mock customerSerializer = new Mock(ODataPayloadKind.ResourceSet); + IODataSerializerProvider provider = ODataTestUtil.GetMockODataSerializerProvider(customerSerializer.Object); + var mockWriter = new Mock(); - customerSerializer.Setup(s => s.WriteObjectInlineAsync(lists[0], edmType, mockWriter.Object, writeContext)).Verifiable(); - customerSerializer.Setup(s => s.WriteObjectInlineAsync(lists[1], edmType, mockWriter.Object, writeContext)).Verifiable(); + customerSerializer.Setup(s => s.WriteObjectInlineAsync(lists[0], edmType, mockWriter.Object, writeContext)).Verifiable(); + customerSerializer.Setup(s => s.WriteObjectInlineAsync(lists[1], edmType, mockWriter.Object, writeContext)).Verifiable(); - _serializer = new ODataResourceSetSerializer(provider); + _serializer = new ODataResourceSetSerializer(provider); - // Act - await _serializer.WriteObjectInlineAsync(lists, edmType, mockWriter.Object, writeContext); + // Act + await _serializer.WriteObjectInlineAsync(lists, edmType, mockWriter.Object, writeContext); - // Assert - customerSerializer.Verify(); - } + // Assert + customerSerializer.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_WritesEachResourceItemInstance_Untyped() + [Fact] + public async Task WriteObjectInlineAsync_WritesEachResourceItemInstance_Untyped() + { + // Arrange + IList lists = new List() { - // Arrange - IList lists = new List() - { - new object(), - new object() - }; - ODataSerializerContext writeContext = new ODataSerializerContext(); + new object(), + new object() + }; + ODataSerializerContext writeContext = new ODataSerializerContext(); - IEdmTypeReference edmType = EdmUntypedHelpers.NullableUntypedCollectionReference; - IEdmTypeReference elementType = edmType.AsCollection().ElementType(); + IEdmTypeReference edmType = EdmUntypedHelpers.NullableUntypedCollectionReference; + IEdmTypeReference elementType = edmType.AsCollection().ElementType(); - Mock customerSerializer = new Mock(ODataPayloadKind.Resource); - IODataSerializerProvider provider = ODataTestUtil.GetMockODataSerializerProvider(customerSerializer.Object); - var mockWriter = new Mock(); + Mock customerSerializer = new Mock(ODataPayloadKind.Resource); + IODataSerializerProvider provider = ODataTestUtil.GetMockODataSerializerProvider(customerSerializer.Object); + var mockWriter = new Mock(); - customerSerializer.Setup(s => s.WriteObjectInlineAsync(lists[0], elementType, mockWriter.Object, writeContext)).Verifiable(); - customerSerializer.Setup(s => s.WriteObjectInlineAsync(lists[1], elementType, mockWriter.Object, writeContext)).Verifiable(); + customerSerializer.Setup(s => s.WriteObjectInlineAsync(lists[0], elementType, mockWriter.Object, writeContext)).Verifiable(); + customerSerializer.Setup(s => s.WriteObjectInlineAsync(lists[1], elementType, mockWriter.Object, writeContext)).Verifiable(); - _serializer = new ODataResourceSetSerializer(provider); + _serializer = new ODataResourceSetSerializer(provider); - // Act - await _serializer.WriteObjectInlineAsync(lists, edmType, mockWriter.Object, writeContext); + // Act + await _serializer.WriteObjectInlineAsync(lists, edmType, mockWriter.Object, writeContext); - // Assert - customerSerializer.Verify(); - } + // Assert + customerSerializer.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_Can_WriteCollectionOfIEdmObjects() - { - // Arrange - IEdmTypeReference edmType = new EdmEntityTypeReference(new EdmEntityType("NS", "Name"), isNullable: false); - IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(edmType)); - Mock edmObject = new Mock(); - edmObject.Setup(e => e.GetEdmType()).Returns(edmType); + [Fact] + public async Task WriteObjectInlineAsync_Can_WriteCollectionOfIEdmObjects() + { + // Arrange + IEdmTypeReference edmType = new EdmEntityTypeReference(new EdmEntityType("NS", "Name"), isNullable: false); + IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(edmType)); + Mock edmObject = new Mock(); + edmObject.Setup(e => e.GetEdmType()).Returns(edmType); - var mockWriter = new Mock(); + var mockWriter = new Mock(); - Mock customSerializer = new Mock(ODataPayloadKind.Resource); - customSerializer.Setup(s => s.WriteObjectInlineAsync(edmObject.Object, edmType, mockWriter.Object, _writeContext)).Verifiable(); + Mock customSerializer = new Mock(ODataPayloadKind.Resource); + customSerializer.Setup(s => s.WriteObjectInlineAsync(edmObject.Object, edmType, mockWriter.Object, _writeContext)).Verifiable(); - Mock serializerProvider = new Mock(); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(edmType)).Returns(customSerializer.Object); + Mock serializerProvider = new Mock(); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(edmType)).Returns(customSerializer.Object); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - // Act - await serializer.WriteObjectInlineAsync(new[] { edmObject.Object }, collectionType, mockWriter.Object, _writeContext); + // Act + await serializer.WriteObjectInlineAsync(new[] { edmObject.Object }, collectionType, mockWriter.Object, _writeContext); - // Assert - customSerializer.Verify(); - } + // Assert + customSerializer.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_Sets_CountQueryOption_OnWriteStartAsync() - { - // Arrange - IEnumerable instance = new object[0]; - ODataResourceSet resourceSet = new ODataResourceSet { Count = 1000 }; - Mock serializerProvider = new Mock(); - Mock resourceSerializer = new Mock(serializerProvider.Object); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(resourceSerializer.Object); - Mock serializer = new Mock(serializerProvider.Object); - serializer.CallBase = true; - serializer.Setup(s => s.CreateResourceSet(instance, _customersType, _writeContext)).Returns(resourceSet); - var mockWriter = new Mock(); - - mockWriter.Setup(m => m.WriteStartAsync(It.Is(f => f.Count == 1000))).Verifiable(); - - // Act - await serializer.Object.WriteObjectInlineAsync(instance, _customersType, mockWriter.Object, _writeContext); - - // Assert - mockWriter.Verify(); - } + [Fact] + public async Task WriteObjectInlineAsync_Sets_CountQueryOption_OnWriteStartAsync() + { + // Arrange + IEnumerable instance = new object[0]; + ODataResourceSet resourceSet = new ODataResourceSet { Count = 1000 }; + Mock serializerProvider = new Mock(); + Mock resourceSerializer = new Mock(serializerProvider.Object); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(resourceSerializer.Object); + Mock serializer = new Mock(serializerProvider.Object); + serializer.CallBase = true; + serializer.Setup(s => s.CreateResourceSet(instance, _customersType, _writeContext)).Returns(resourceSet); + var mockWriter = new Mock(); + + mockWriter.Setup(m => m.WriteStartAsync(It.Is(f => f.Count == 1000))).Verifiable(); + + // Act + await serializer.Object.WriteObjectInlineAsync(instance, _customersType, mockWriter.Object, _writeContext); + + // Assert + mockWriter.Verify(); + } - [Fact] - public async Task WriteObjectInlineAsync_Sets_NextPageLink_OnWriteEndAsync() - { - // Arrange - IEnumerable instance = new object[0]; - ODataResourceSet resourceSet = new ODataResourceSet { NextPageLink = new Uri("http://nextlink.com/") }; - Mock serializerProvider = new Mock(); - Mock resourceSerializer = new Mock(serializerProvider.Object); - serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(resourceSerializer.Object); - Mock serializer = new Mock(serializerProvider.Object); - serializer.CallBase = true; - serializer.Setup(s => s.CreateResourceSet(instance, _customersType, _writeContext)).Returns(resourceSet); - var mockWriter = new Mock(); - - mockWriter.Setup(m => m.WriteStartAsync(It.Is(f => f.NextPageLink == null))).Verifiable(); - mockWriter - .Setup(m => m.WriteEndAsync()) - .Callback(() => - { - Assert.Equal("http://nextlink.com/", resourceSet.NextPageLink.AbsoluteUri); - }) - .Returns(Task.CompletedTask) - .Verifiable(); + [Fact] + public async Task WriteObjectInlineAsync_Sets_NextPageLink_OnWriteEndAsync() + { + // Arrange + IEnumerable instance = new object[0]; + ODataResourceSet resourceSet = new ODataResourceSet { NextPageLink = new Uri("http://nextlink.com/") }; + Mock serializerProvider = new Mock(); + Mock resourceSerializer = new Mock(serializerProvider.Object); + serializerProvider.Setup(s => s.GetEdmTypeSerializer(It.IsAny())).Returns(resourceSerializer.Object); + Mock serializer = new Mock(serializerProvider.Object); + serializer.CallBase = true; + serializer.Setup(s => s.CreateResourceSet(instance, _customersType, _writeContext)).Returns(resourceSet); + var mockWriter = new Mock(); + + mockWriter.Setup(m => m.WriteStartAsync(It.Is(f => f.NextPageLink == null))).Verifiable(); + mockWriter + .Setup(m => m.WriteEndAsync()) + .Callback(() => + { + Assert.Equal("http://nextlink.com/", resourceSet.NextPageLink.AbsoluteUri); + }) + .Returns(Task.CompletedTask) + .Verifiable(); - // Act - await serializer.Object.WriteObjectInlineAsync(instance, _customersType, mockWriter.Object, _writeContext); + // Act + await serializer.Object.WriteObjectInlineAsync(instance, _customersType, mockWriter.Object, _writeContext); - // Assert - mockWriter.Verify(); - } + // Assert + mockWriter.Verify(); + } - [Fact] - public void CreateResource_Sets_CountValueForPageResult() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - Uri expectedNextLink = new Uri("http://nextlink.com"); - const long ExpectedCountValue = 1000; + [Fact] + public void CreateResource_Sets_CountValueForPageResult() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + Uri expectedNextLink = new Uri("http://nextlink.com"); + const long ExpectedCountValue = 1000; - var result = new PageResult(_customers, expectedNextLink, ExpectedCountValue); + var result = new PageResult(_customers, expectedNextLink, ExpectedCountValue); - // Act - ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, new ODataSerializerContext()); + // Act + ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, new ODataSerializerContext()); - // Assert - Assert.Equal(ExpectedCountValue, resourceSet.Count); - } + // Assert + Assert.Equal(ExpectedCountValue, resourceSet.Count); + } - [Fact] - public void CreateResource_Sets_NextPageLinkForPageResult() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - Uri expectedNextLink = new Uri("http://nextlink.com"); - const long ExpectedCountValue = 1000; + [Fact] + public void CreateResource_Sets_NextPageLinkForPageResult() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + Uri expectedNextLink = new Uri("http://nextlink.com"); + const long ExpectedCountValue = 1000; - var result = new PageResult(_customers, expectedNextLink, ExpectedCountValue); + var result = new PageResult(_customers, expectedNextLink, ExpectedCountValue); - // Act - ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, new ODataSerializerContext()); + // Act + ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, new ODataSerializerContext()); - // Assert - Assert.Equal(expectedNextLink, resourceSet.NextPageLink); - } + // Assert + Assert.Equal(expectedNextLink, resourceSet.NextPageLink); + } - [Fact] - public void CreateResourceSet_Sets_CountValueFromContext() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - const long ExpectedCountValue = 1000; - var request = RequestFactory.Create(); - request.ODataFeature().TotalCount = ExpectedCountValue; - var result = new object[0]; - - // Act - ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, new ODataSerializerContext { Request = request }); - - // Assert - Assert.Equal(ExpectedCountValue, resourceSet.Count); - } + [Fact] + public void CreateResourceSet_Sets_CountValueFromContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + const long ExpectedCountValue = 1000; + var request = RequestFactory.Create(); + request.ODataFeature().TotalCount = ExpectedCountValue; + var result = new object[0]; + + // Act + ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, new ODataSerializerContext { Request = request }); + + // Assert + Assert.Equal(ExpectedCountValue, resourceSet.Count); + } - [Fact] - public void CreateResourceSet_Sets_NextPageLinkFromContext() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - Uri expectedNextLink = new Uri("http://nextlink.com"); - var request = RequestFactory.Create(); - request.ODataFeature().NextLink = expectedNextLink; - var result = new object[0]; - - // Act - ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, new ODataSerializerContext { Request = request }); - - // Assert - Assert.Equal(expectedNextLink, resourceSet.NextPageLink); - } + [Fact] + public void CreateResourceSet_Sets_NextPageLinkFromContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + Uri expectedNextLink = new Uri("http://nextlink.com"); + var request = RequestFactory.Create(); + request.ODataFeature().NextLink = expectedNextLink; + var result = new object[0]; + + // Act + ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, new ODataSerializerContext { Request = request }); + + // Assert + Assert.Equal(expectedNextLink, resourceSet.NextPageLink); + } - [Fact] - public void CreateODataFeed_Sets_DeltaLinkFromContext() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - Uri expectedDeltaLink = new Uri("http://deltalink.com"); - var request = RequestFactory.Create(); - request.ODataFeature().DeltaLink = expectedDeltaLink; - var result = new object[0]; - - // Act - ODataResourceSet feed = serializer.CreateResourceSet(result, _customersType, new ODataSerializerContext { Request = request }); - - // Assert - Assert.Equal(expectedDeltaLink, feed.DeltaLink); - } + [Fact] + public void CreateODataFeed_Sets_DeltaLinkFromContext() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + Uri expectedDeltaLink = new Uri("http://deltalink.com"); + var request = RequestFactory.Create(); + request.ODataFeature().DeltaLink = expectedDeltaLink; + var result = new object[0]; + + // Act + ODataResourceSet feed = serializer.CreateResourceSet(result, _customersType, new ODataSerializerContext { Request = request }); + + // Assert + Assert.Equal(expectedDeltaLink, feed.DeltaLink); + } - [Fact] - public void CreateResource_Ignores_NextPageLink_ForInnerResourceSets() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - Uri nextLink = new Uri("http://somelink"); - var request = RequestFactory.Create(); - request.ODataFeature().NextLink = nextLink; - var result = new object[0]; - IEdmNavigationProperty navProp = _customerSet.EntityType.NavigationProperties().First(); - SelectExpandClause selectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: true); - ResourceContext entity = new ResourceContext - { - SerializerContext = - new ODataSerializerContext { Request = request, NavigationSource = _customerSet, Model = _model } - }; - ODataSerializerContext nestedContext = new ODataSerializerContext(entity, selectExpandClause, navProp); + [Fact] + public void CreateResource_Ignores_NextPageLink_ForInnerResourceSets() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + Uri nextLink = new Uri("http://somelink"); + var request = RequestFactory.Create(); + request.ODataFeature().NextLink = nextLink; + var result = new object[0]; + IEdmNavigationProperty navProp = _customerSet.EntityType.NavigationProperties().First(); + SelectExpandClause selectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: true); + ResourceContext entity = new ResourceContext + { + SerializerContext = + new ODataSerializerContext { Request = request, NavigationSource = _customerSet, Model = _model } + }; + ODataSerializerContext nestedContext = new ODataSerializerContext(entity, selectExpandClause, navProp); + + // Act + ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, nestedContext); + + // Assert + Assert.Null(resourceSet.NextPageLink); + } - // Act - ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, nestedContext); + [Fact] + public void CreateResourceSet_Ignores_CountValue_ForInnerResourceSets() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + var request = RequestFactory.Create(); + request.ODataFeature().TotalCount = 42; + var result = new object[0]; + IEdmNavigationProperty navProp = _customerSet.EntityType.NavigationProperties().First(); + SelectExpandClause selectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: true); + ResourceContext entity = new ResourceContext + { + SerializerContext = + new ODataSerializerContext { Request = request, NavigationSource = _customerSet, Model = _model } + }; + ODataSerializerContext nestedContext = new ODataSerializerContext(entity, selectExpandClause, navProp); + + // Act + ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, nestedContext); + + // Assert + Assert.Null(resourceSet.Count); + } - // Assert - Assert.Null(resourceSet.NextPageLink); - } + [Fact] + public void CreateResourceSet_SetsODataOperations() + { + // Arrange + IEdmModel model = GetEdmModelWithOperations(out IEdmEntityType customerType, out IEdmEntitySet customers); + IEdmCollectionTypeReference customersType = new EdmCollectionTypeReference(new EdmCollectionType(customerType.AsReference())); - [Fact] - public void CreateResourceSet_Ignores_CountValue_ForInnerResourceSets() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - var request = RequestFactory.Create(); - request.ODataFeature().TotalCount = 42; - var result = new object[0]; - IEdmNavigationProperty navProp = _customerSet.EntityType.NavigationProperties().First(); - SelectExpandClause selectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: true); - ResourceContext entity = new ResourceContext - { - SerializerContext = - new ODataSerializerContext { Request = request, NavigationSource = _customerSet, Model = _model } - }; - ODataSerializerContext nestedContext = new ODataSerializerContext(entity, selectExpandClause, navProp); + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + var request = RequestFactory.Create(method: "Get", uri: "http://IgnoreMetadataPath", opt => opt.AddRouteComponents(model)); - // Act - ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, nestedContext); + ODataSerializerContext context = new ODataSerializerContext + { + NavigationSource = customers, + Request = request, + Model = model, + MetadataLevel = ODataMetadataLevel.Full, + }; + var result = new object[0]; - // Assert - Assert.Null(resourceSet.Count); - } + // Act + ODataResourceSet resourceSet = serializer.CreateResourceSet(result, customersType, context); - [Fact] - public void CreateResourceSet_SetsODataOperations() - { - // Arrange - IEdmModel model = GetEdmModelWithOperations(out IEdmEntityType customerType, out IEdmEntitySet customers); - IEdmCollectionTypeReference customersType = new EdmCollectionTypeReference(new EdmCollectionType(customerType.AsReference())); + // Assert + Assert.Single(resourceSet.Actions); + Assert.Equal(3, resourceSet.Functions.Count()); + } - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - var request = RequestFactory.Create(method: "Get", uri: "http://IgnoreMetadataPath", opt => opt.AddRouteComponents(model)); + private IEdmModel GetEdmModelWithOperations(out IEdmEntityType customerType, out IEdmEntitySet customers) + { + EdmModel model = new EdmModel(); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customerType = customer; + customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + model.AddElement(customer); + + EdmAction upgradeAll = new EdmAction("NS", "UpgradeAll", returnType: null, isBound: true, entitySetPathExpression: null); + upgradeAll.AddParameter("entityset", + new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + model.AddElement(upgradeAll); + + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmTypeReference stringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); + IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); + + EdmFunction IsAnyUpgraded = new EdmFunction( + "NS", + "IsAnyUpgraded", + returnType, + isBound: true, + entitySetPathExpression: null, + isComposable: false); + EdmCollectionType edmCollectionType = new EdmCollectionType(new EdmEntityTypeReference(customer, false)); + IsAnyUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(edmCollectionType)); + model.AddElement(IsAnyUpgraded); + + EdmFunction isCustomerUpgradedWithParam = new EdmFunction( + "NS", + "IsUpgradedWithParam", + returnType, + isBound: true, + entitySetPathExpression: null, + isComposable: false); + isCustomerUpgradedWithParam.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + isCustomerUpgradedWithParam.AddParameter("city", EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false)); + model.AddElement(isCustomerUpgradedWithParam); + + EdmFunction isAllUpgraded = new EdmFunction("NS", "IsAllUpgraded", returnType, isBound: true, + entitySetPathExpression: null, isComposable: false); + isAllUpgraded.AddParameter("entityset", + new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + isAllUpgraded.AddParameter("param", intType); + model.AddElement(isAllUpgraded); + + EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", intType, isBound: true, entitySetPathExpression: null, isComposable: false); + getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + getSalaray.AddParameter("minSalary", intType); + getSalaray.AddOptionalParameter("maxSalary", intType); + getSalaray.AddOptionalParameter("aveSalary", intType, "129"); + model.AddElement(getSalaray); + + EdmEntityContainer container = new EdmEntityContainer("NS", "ModelWithInheritance"); + model.AddElement(container); + customers = container.AddEntitySet("Customers", customer); + + return model; + } - ODataSerializerContext context = new ODataSerializerContext - { - NavigationSource = customers, - Request = request, - Model = model, - MetadataLevel = ODataMetadataLevel.Full, - }; - var result = new object[0]; - - // Act - ODataResourceSet resourceSet = serializer.CreateResourceSet(result, customersType, context); - - // Assert - Assert.Single(resourceSet.Actions); - Assert.Equal(3, resourceSet.Functions.Count()); - } + [Fact] + public void SetODataFeatureTotalCountValueNull() + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + var request = RequestFactory.Create(); + request.ODataFeature().TotalCount = null; - private IEdmModel GetEdmModelWithOperations(out IEdmEntityType customerType, out IEdmEntitySet customers) - { - EdmModel model = new EdmModel(); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customerType = customer; - customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - model.AddElement(customer); - - EdmAction upgradeAll = new EdmAction("NS", "UpgradeAll", returnType: null, isBound: true, entitySetPathExpression: null); - upgradeAll.AddParameter("entityset", - new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - model.AddElement(upgradeAll); - - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmTypeReference stringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); - IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); - - EdmFunction IsAnyUpgraded = new EdmFunction( - "NS", - "IsAnyUpgraded", - returnType, - isBound: true, - entitySetPathExpression: null, - isComposable: false); - EdmCollectionType edmCollectionType = new EdmCollectionType(new EdmEntityTypeReference(customer, false)); - IsAnyUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(edmCollectionType)); - model.AddElement(IsAnyUpgraded); - - EdmFunction isCustomerUpgradedWithParam = new EdmFunction( - "NS", - "IsUpgradedWithParam", - returnType, - isBound: true, - entitySetPathExpression: null, - isComposable: false); - isCustomerUpgradedWithParam.AddParameter("entity", new EdmEntityTypeReference(customer, false)); - isCustomerUpgradedWithParam.AddParameter("city", EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false)); - model.AddElement(isCustomerUpgradedWithParam); - - EdmFunction isAllUpgraded = new EdmFunction("NS", "IsAllUpgraded", returnType, isBound: true, - entitySetPathExpression: null, isComposable: false); - isAllUpgraded.AddParameter("entityset", - new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - isAllUpgraded.AddParameter("param", intType); - model.AddElement(isAllUpgraded); - - EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", intType, isBound: true, entitySetPathExpression: null, isComposable: false); - getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - getSalaray.AddParameter("minSalary", intType); - getSalaray.AddOptionalParameter("maxSalary", intType); - getSalaray.AddOptionalParameter("aveSalary", intType, "129"); - model.AddElement(getSalaray); - - EdmEntityContainer container = new EdmEntityContainer("NS", "ModelWithInheritance"); - model.AddElement(container); - customers = container.AddEntitySet("Customers", customer); - - return model; - } + var result = new object[0]; - [Fact] - public void SetODataFeatureTotalCountValueNull() - { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - var request = RequestFactory.Create(); - request.ODataFeature().TotalCount = null; + // Act + ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, new ODataSerializerContext { Request = request }); - var result = new object[0]; + // Assert + Assert.Null(resourceSet.Count); + } - // Act - ODataResourceSet resourceSet = serializer.CreateResourceSet(result, _customersType, new ODataSerializerContext { Request = request }); + [Fact] + public void CreateODataOperation_ThrowsArgumentNull_ForInputParameters() + { + // Arrange & Act & Assert + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - // Assert - Assert.Null(resourceSet.Count); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => serializer.CreateODataOperation(null, null, null), "operation"); - [Fact] - public void CreateODataOperation_ThrowsArgumentNull_ForInputParameters() - { - // Arrange & Act & Assert - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + // Act & Assert + IEdmOperation operation = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => serializer.CreateODataOperation(operation, null, null), "resourceSetContext"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => serializer.CreateODataOperation(null, null, null), "operation"); + // Act & Assert + ResourceSetContext context = new ResourceSetContext(); + ExceptionAssert.ThrowsArgumentNull(() => serializer.CreateODataOperation(operation, context, null), "writeContext"); + } - // Act & Assert - IEdmOperation operation = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => serializer.CreateODataOperation(operation, null, null), "resourceSetContext"); + [Theory] + [InlineData(ODataMetadataLevel.Minimal)] + [InlineData(ODataMetadataLevel.None)] + public void CreateODataOperation_OmitsOperations_WhenNonFullMetadata(ODataMetadataLevel metadataLevel) + { + // Arrange + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - // Act & Assert - ResourceSetContext context = new ResourceSetContext(); - ExceptionAssert.ThrowsArgumentNull(() => serializer.CreateODataOperation(operation, context, null), "writeContext"); - } + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); - [Theory] - [InlineData(ODataMetadataLevel.Minimal)] - [InlineData(ODataMetadataLevel.None)] - public void CreateODataOperation_OmitsOperations_WhenNonFullMetadata(ODataMetadataLevel metadataLevel) + ResourceSetContext resourceSetContext = new ResourceSetContext(); + ODataSerializerContext serializerContext = new ODataSerializerContext { - // Arrange - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmFunction function = new EdmFunction("NS", "Function", returnType, isBound: true, entitySetPathExpression: null, isComposable: false); + MetadataLevel = metadataLevel + }; + // Act - ResourceSetContext resourceSetContext = new ResourceSetContext(); - ODataSerializerContext serializerContext = new ODataSerializerContext - { - MetadataLevel = metadataLevel - }; - // Act + ODataOperation operation = serializer.CreateODataOperation(function, resourceSetContext, serializerContext); - ODataOperation operation = serializer.CreateODataOperation(function, resourceSetContext, serializerContext); + // Assert + Assert.Null(operation); + } - // Assert - Assert.Null(operation); - } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CreateODataOperations_CreateOperations(bool followConventions) + { + // Arrange + string expectedTarget = "aa://Target"; + Mock serializerProvider = new Mock(); + ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + var function = builder.EntityType().Collection.Function("MyFunction").Returns(); + IEdmModel model = builder.GetEdmModel(); + + IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); + IEdmFunction edmFunction = model.SchemaElements.OfType().First(f => f.Name == "MyFunction"); + + Func functionLinkFactory = a => new Uri(expectedTarget); + var operationLinkBuilder = new OperationLinkBuilder(functionLinkFactory, followConventions); + model.SetOperationLinkBuilder(edmFunction, operationLinkBuilder); + + var request = RequestFactory.Create(method: "Get", uri: "http://any", opt => opt.AddRouteComponents(model)); + ResourceSetContext resourceSetContext = new ResourceSetContext + { + EntitySetBase = customers, + Request = request, + }; + + ODataSerializerContext serializerContext = new ODataSerializerContext + { + NavigationSource = customers, + Request = request, + Model = model, + MetadataLevel = ODataMetadataLevel.Full, + }; + string expectedMetadataPrefix = "http://any/$metadata"; + + // Act + ODataOperation actualOperation = serializer.CreateODataOperation(edmFunction, resourceSetContext, serializerContext); + + // Assert + Assert.NotNull(actualOperation); + string expectedMetadata = expectedMetadataPrefix + "#Default.MyFunction"; + ODataOperation expectedFunction = new ODataFunction + { + Metadata = new Uri(expectedMetadata), + Target = new Uri(expectedTarget), + Title = "MyFunction" + }; + + AssertEqual(expectedFunction, actualOperation); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CreateODataOperations_CreateOperations(bool followConventions) + private static void AssertEqual(ODataOperation expected, ODataOperation actual) + { + if (expected == null) { - // Arrange - string expectedTarget = "aa://Target"; - Mock serializerProvider = new Mock(); - ODataResourceSetSerializer serializer = new ODataResourceSetSerializer(serializerProvider.Object); - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - var function = builder.EntityType().Collection.Function("MyFunction").Returns(); - IEdmModel model = builder.GetEdmModel(); - - IEdmEntitySet customers = model.EntityContainer.FindEntitySet("Customers"); - IEdmFunction edmFunction = model.SchemaElements.OfType().First(f => f.Name == "MyFunction"); - - Func functionLinkFactory = a => new Uri(expectedTarget); - var operationLinkBuilder = new OperationLinkBuilder(functionLinkFactory, followConventions); - model.SetOperationLinkBuilder(edmFunction, operationLinkBuilder); - - var request = RequestFactory.Create(method: "Get", uri: "http://any", opt => opt.AddRouteComponents(model)); - ResourceSetContext resourceSetContext = new ResourceSetContext - { - EntitySetBase = customers, - Request = request, - }; - - ODataSerializerContext serializerContext = new ODataSerializerContext - { - NavigationSource = customers, - Request = request, - Model = model, - MetadataLevel = ODataMetadataLevel.Full, - }; - string expectedMetadataPrefix = "http://any/$metadata"; - - // Act - ODataOperation actualOperation = serializer.CreateODataOperation(edmFunction, resourceSetContext, serializerContext); - - // Assert - Assert.NotNull(actualOperation); - string expectedMetadata = expectedMetadataPrefix + "#Default.MyFunction"; - ODataOperation expectedFunction = new ODataFunction - { - Metadata = new Uri(expectedMetadata), - Target = new Uri(expectedTarget), - Title = "MyFunction" - }; - - AssertEqual(expectedFunction, actualOperation); + Assert.Null(actual); + return; } - private static void AssertEqual(ODataOperation expected, ODataOperation actual) - { - if (expected == null) - { - Assert.Null(actual); - return; - } - - Assert.NotNull(actual); - AssertEqual(expected.Metadata, actual.Metadata); - AssertEqual(expected.Target, actual.Target); - Assert.Equal(expected.Title, actual.Title); - } + Assert.NotNull(actual); + AssertEqual(expected.Metadata, actual.Metadata); + AssertEqual(expected.Target, actual.Target); + Assert.Equal(expected.Title, actual.Title); + } - private static void AssertEqual(Uri expected, Uri actual) + private static void AssertEqual(Uri expected, Uri actual) + { + if (expected == null) { - if (expected == null) - { - Assert.Null(actual); - return; - } - - Assert.NotNull(actual); - Assert.Equal(expected.AbsoluteUri, actual.AbsoluteUri); + Assert.Null(actual); + return; } - private static IServiceProvider GetServiceProvider() - { - IServiceCollection services = new ServiceCollection(); + Assert.NotNull(actual); + Assert.Equal(expected.AbsoluteUri, actual.AbsoluteUri); + } - services.AddSingleton(); + private static IServiceProvider GetServiceProvider() + { + IServiceCollection services = new ServiceCollection(); - // Serializers. - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); - return services.BuildServiceProvider(); - } + // Serializers. + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - public class FeedCustomer - { - public int Id { get; set; } - } + return services.BuildServiceProvider(); + } + + public class FeedCustomer + { + public int Id { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerContextFactory.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerContextFactory.cs index 1ed4224c1..9386084cd 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerContextFactory.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerContextFactory.cs @@ -10,29 +10,28 @@ using Microsoft.OData.Edm; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +/// +/// A class to create ODataSerializerContext. +/// +public class ODataSerializerContextFactory { /// - /// A class to create ODataSerializerContext. + /// Initializes a new instance of the routing configuration class. /// - public class ODataSerializerContextFactory + /// A new instance of the routing configuration class. + public static ODataSerializerContext Create(IEdmModel model, IEdmNavigationSource navigationSource, HttpRequest request) { - /// - /// Initializes a new instance of the routing configuration class. - /// - /// A new instance of the routing configuration class. - public static ODataSerializerContext Create(IEdmModel model, IEdmNavigationSource navigationSource, HttpRequest request) - { - return new ODataSerializerContext { Model = model, NavigationSource = navigationSource, Request = request }; - } + return new ODataSerializerContext { Model = model, NavigationSource = navigationSource, Request = request }; + } - /// - /// Initializes a new instance of the routing configuration class. - /// - /// A new instance of the routing configuration class. - public static ODataSerializerContext Create(IEdmModel model, IEdmNavigationSource navigationSource, ODataPath path, HttpRequest request) - { - return new ODataSerializerContext { Model = model, NavigationSource = navigationSource, Path = path, Request = request }; - } + /// + /// Initializes a new instance of the routing configuration class. + /// + /// A new instance of the routing configuration class. + public static ODataSerializerContext Create(IEdmModel model, IEdmNavigationSource navigationSource, ODataPath path, HttpRequest request) + { + return new ODataSerializerContext { Model = model, NavigationSource = navigationSource, Path = path, Request = request }; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerContextTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerContextTest.cs index f5b457b31..7b3160426 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerContextTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerContextTest.cs @@ -18,164 +18,163 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataSerializerContextTest { - public class ODataSerializerContextTest - { - private static IEdmModel _model = GetEdmModel(); + private static IEdmModel _model = GetEdmModel(); - [Fact] - public void EmptyCtor_DoesnotThrow() - { - // Arrange & Act & Asserts - ExceptionAssert.DoesNotThrow(() => new ODataSerializerContext()); - } + [Fact] + public void EmptyCtor_DoesnotThrow() + { + // Arrange & Act & Asserts + ExceptionAssert.DoesNotThrow(() => new ODataSerializerContext()); + } - [Fact] - public void Ctor_ForNestedContext_ThrowsArgumentNull_Resource() - { - // Arrange - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); - IEdmNavigationProperty navProp = new Mock().Object; + [Fact] + public void Ctor_ForNestedContext_ThrowsArgumentNull_Resource() + { + // Arrange + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); + IEdmNavigationProperty navProp = new Mock().Object; - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new ODataSerializerContext(resource: null, selectExpandClause: selectExpand, edmProperty: navProp), "resource"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new ODataSerializerContext(resource: null, selectExpandClause: selectExpand, edmProperty: navProp), "resource"); + } - [Fact] - public void Ctor_ThatBuildsNestedContext_CopiesProperties() + [Fact] + public void Ctor_ThatBuildsNestedContext_CopiesProperties() + { + // Arrange + IEdmModel model = new Mock().Object; + HttpRequest request = new Mock().Object; + IEdmNavigationProperty navProp = new Mock().Object; + IEdmNavigationSource navigationSource = new Mock().Object; + ODataSerializerContext context = new ODataSerializerContext { - // Arrange - IEdmModel model = new Mock().Object; - HttpRequest request = new Mock().Object; - IEdmNavigationProperty navProp = new Mock().Object; - IEdmNavigationSource navigationSource = new Mock().Object; - ODataSerializerContext context = new ODataSerializerContext - { - NavigationSource = navigationSource, - MetadataLevel = ODataMetadataLevel.Full, - Model = model, - Path = new ODataPath(), - Request = request, - RootElementName = "somename", - SelectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: true), - SkipExpensiveAvailabilityChecks = true, - }; - ResourceContext resource = new ResourceContext { SerializerContext = context }; - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); - - // Act - ODataSerializerContext nestedContext = new ODataSerializerContext(resource, selectExpand, navProp); - - // Assert - Assert.Equal(context.MetadataLevel, nestedContext.MetadataLevel); - Assert.Same(context.Model, nestedContext.Model); - Assert.Same(context.Path, nestedContext.Path); - Assert.Same(context.Request, nestedContext.Request); - Assert.Equal(context.RootElementName, nestedContext.RootElementName); - Assert.Equal(context.SkipExpensiveAvailabilityChecks, nestedContext.SkipExpensiveAvailabilityChecks); - } + NavigationSource = navigationSource, + MetadataLevel = ODataMetadataLevel.Full, + Model = model, + Path = new ODataPath(), + Request = request, + RootElementName = "somename", + SelectExpandClause = new SelectExpandClause(new SelectItem[0], allSelected: true), + SkipExpensiveAvailabilityChecks = true, + }; + ResourceContext resource = new ResourceContext { SerializerContext = context }; + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); + + // Act + ODataSerializerContext nestedContext = new ODataSerializerContext(resource, selectExpand, navProp); + + // Assert + Assert.Equal(context.MetadataLevel, nestedContext.MetadataLevel); + Assert.Same(context.Model, nestedContext.Model); + Assert.Same(context.Path, nestedContext.Path); + Assert.Same(context.Request, nestedContext.Request); + Assert.Equal(context.RootElementName, nestedContext.RootElementName); + Assert.Equal(context.SkipExpensiveAvailabilityChecks, nestedContext.SkipExpensiveAvailabilityChecks); + } - [Fact] - public void Ctor_ThatBuildsNestedContext_InitializesRightValues() - { - // Arrange - IEdmEntitySet orders = _model.EntityContainer.FindEntitySet("Orders"); - IEdmEntitySet customers = _model.EntityContainer.FindEntitySet("Customers"); - IEdmEntityType customer = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "SerializerCustomer"); - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); - IEdmNavigationProperty navProp = customer.NavigationProperties().First(); - ODataSerializerContext context = new ODataSerializerContext { NavigationSource = customers, Model = _model }; - ResourceContext resource = new ResourceContext { SerializerContext = context }; - - // Act - ODataSerializerContext nestedContext = new ODataSerializerContext(resource, selectExpand, navProp); - - // Assert - Assert.Same(resource, nestedContext.ExpandedResource); - Assert.Same(navProp, nestedContext.NavigationProperty); - Assert.Same(selectExpand, nestedContext.SelectExpandClause); - Assert.Same(orders, nestedContext.NavigationSource); - Assert.Same(navProp, nestedContext.EdmProperty); - } + [Fact] + public void Ctor_ThatBuildsNestedContext_InitializesRightValues() + { + // Arrange + IEdmEntitySet orders = _model.EntityContainer.FindEntitySet("Orders"); + IEdmEntitySet customers = _model.EntityContainer.FindEntitySet("Customers"); + IEdmEntityType customer = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "SerializerCustomer"); + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); + IEdmNavigationProperty navProp = customer.NavigationProperties().First(); + ODataSerializerContext context = new ODataSerializerContext { NavigationSource = customers, Model = _model }; + ResourceContext resource = new ResourceContext { SerializerContext = context }; + + // Act + ODataSerializerContext nestedContext = new ODataSerializerContext(resource, selectExpand, navProp); + + // Assert + Assert.Same(resource, nestedContext.ExpandedResource); + Assert.Same(navProp, nestedContext.NavigationProperty); + Assert.Same(selectExpand, nestedContext.SelectExpandClause); + Assert.Same(orders, nestedContext.NavigationSource); + Assert.Same(navProp, nestedContext.EdmProperty); + } - [Fact] - public void Ctor_ThatBuildsNestedContext_InitializesRightValues_ForComplex() - { - // Arrange - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); - IEdmEntitySet customers = _model.EntityContainer.FindEntitySet("Customers"); - IEdmEntityType customer = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "SerializerCustomer"); - IEdmProperty complexProperty = customer.Properties().First(p => p.Name == "Address"); - ODataSerializerContext context = new ODataSerializerContext { NavigationSource = customers, Model = _model }; - ResourceContext resource = new ResourceContext { SerializerContext = context }; - - // Act - ODataSerializerContext nestedContext = new ODataSerializerContext(resource, selectExpand, complexProperty); - - // Assert - Assert.Same(resource, nestedContext.ExpandedResource); - Assert.Same(selectExpand, nestedContext.SelectExpandClause); - Assert.Same(complexProperty, nestedContext.EdmProperty); - } + [Fact] + public void Ctor_ThatBuildsNestedContext_InitializesRightValues_ForComplex() + { + // Arrange + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); + IEdmEntitySet customers = _model.EntityContainer.FindEntitySet("Customers"); + IEdmEntityType customer = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "SerializerCustomer"); + IEdmProperty complexProperty = customer.Properties().First(p => p.Name == "Address"); + ODataSerializerContext context = new ODataSerializerContext { NavigationSource = customers, Model = _model }; + ResourceContext resource = new ResourceContext { SerializerContext = context }; + + // Act + ODataSerializerContext nestedContext = new ODataSerializerContext(resource, selectExpand, complexProperty); + + // Assert + Assert.Same(resource, nestedContext.ExpandedResource); + Assert.Same(selectExpand, nestedContext.SelectExpandClause); + Assert.Same(complexProperty, nestedContext.EdmProperty); + } - [Fact] - public void Property_Items_IsInitialized() - { - ODataSerializerContext context = new ODataSerializerContext(); - Assert.NotNull(context.Items); - } + [Fact] + public void Property_Items_IsInitialized() + { + ODataSerializerContext context = new ODataSerializerContext(); + Assert.NotNull(context.Items); + } - [Fact] - public void GetEdmType_ThrowsInvalidOperation_IfEdmObjectGetEdmTypeReturnsNull() - { - // Arrange (this code path does not use ODataSerializerContext fields or properties) - var context = new ODataSerializerContext(); - NullEdmType edmObject = new NullEdmType(); - - // Act & Assert - ExceptionAssert.Throws(() => context.GetEdmType(edmObject, null), - exceptionMessage: "The EDM type of the object of type 'Microsoft.AspNetCore.OData.Tests.Formatter.Serialization.ODataSerializerContextTest+NullEdmType'" + - " is null. The EDM type of an 'IEdmObject' cannot be null."); - } + [Fact] + public void GetEdmType_ThrowsInvalidOperation_IfEdmObjectGetEdmTypeReturnsNull() + { + // Arrange (this code path does not use ODataSerializerContext fields or properties) + var context = new ODataSerializerContext(); + NullEdmType edmObject = new NullEdmType(); + + // Act & Assert + ExceptionAssert.Throws(() => context.GetEdmType(edmObject, null), + exceptionMessage: "The EDM type of the object of type 'Microsoft.AspNetCore.OData.Tests.Formatter.Serialization.ODataSerializerContextTest+NullEdmType'" + + " is null. The EDM type of an 'IEdmObject' cannot be null."); + } - private static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Orders"); - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); + return builder.GetEdmModel(); + } - /// - /// An instance of IEdmObject with no EdmType. - /// - private class NullEdmType : IEdmObject + /// + /// An instance of IEdmObject with no EdmType. + /// + private class NullEdmType : IEdmObject + { + public IEdmTypeReference GetEdmType() { - public IEdmTypeReference GetEdmType() - { - return null; - } + return null; } + } - private class SerializerCustomer - { - public int Id { get; set; } + private class SerializerCustomer + { + public int Id { get; set; } - public SerializerAddress Address { get; set; } + public SerializerAddress Address { get; set; } - public SerializerOrder Order { get; set; } - } + public SerializerOrder Order { get; set; } + } - private class SerializerOrder - { - public int Id { get; set; } - } + private class SerializerOrder + { + public int Id { get; set; } + } - private class SerializerAddress - { - public string City { get; set; } - } + private class SerializerAddress + { + public string City { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerProviderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerProviderTests.cs index 5b33a02a7..be1c82bb7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerProviderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerProviderTests.cs @@ -25,460 +25,459 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataSerializerProviderTests { - public class ODataSerializerProviderTests - { - private static IODataSerializerProvider _serializerProvider = GetServiceProvider().GetRequiredService(); + private static IODataSerializerProvider _serializerProvider = GetServiceProvider().GetRequiredService(); - private static IEdmModel _edmModel = GetEdmModel(); + private static IEdmModel _edmModel = GetEdmModel(); - private static IEdmModel _edmDollarCountModel = GetDollarCountEdmModel(); + private static IEdmModel _edmDollarCountModel = GetDollarCountEdmModel(); - public static TheoryDataSet EdmPrimitiveMappingData + public static TheoryDataSet EdmPrimitiveMappingData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { typeof(byte[]), EdmPrimitiveTypeKind.Binary }, - { typeof(bool), EdmPrimitiveTypeKind.Boolean }, - { typeof(byte), EdmPrimitiveTypeKind.Byte }, - { typeof(DateTime), EdmPrimitiveTypeKind.DateTimeOffset }, - { typeof(DateTimeOffset), EdmPrimitiveTypeKind.DateTimeOffset }, - { typeof(Date), EdmPrimitiveTypeKind.Date }, - { typeof(TimeOfDay), EdmPrimitiveTypeKind.TimeOfDay }, - { typeof(decimal), EdmPrimitiveTypeKind.Decimal }, - { typeof(double), EdmPrimitiveTypeKind.Double }, - { typeof(Guid), EdmPrimitiveTypeKind.Guid }, - { typeof(short), EdmPrimitiveTypeKind.Int16 }, - { typeof(int), EdmPrimitiveTypeKind.Int32 }, - { typeof(long), EdmPrimitiveTypeKind.Int64 }, - { typeof(sbyte), EdmPrimitiveTypeKind.SByte }, - { typeof(float), EdmPrimitiveTypeKind.Single }, - { typeof(Stream), EdmPrimitiveTypeKind.Stream }, - { typeof(string), EdmPrimitiveTypeKind.String }, - { typeof(TimeSpan), EdmPrimitiveTypeKind.Duration }, - }; - } + { typeof(byte[]), EdmPrimitiveTypeKind.Binary }, + { typeof(bool), EdmPrimitiveTypeKind.Boolean }, + { typeof(byte), EdmPrimitiveTypeKind.Byte }, + { typeof(DateTime), EdmPrimitiveTypeKind.DateTimeOffset }, + { typeof(DateTimeOffset), EdmPrimitiveTypeKind.DateTimeOffset }, + { typeof(Date), EdmPrimitiveTypeKind.Date }, + { typeof(TimeOfDay), EdmPrimitiveTypeKind.TimeOfDay }, + { typeof(decimal), EdmPrimitiveTypeKind.Decimal }, + { typeof(double), EdmPrimitiveTypeKind.Double }, + { typeof(Guid), EdmPrimitiveTypeKind.Guid }, + { typeof(short), EdmPrimitiveTypeKind.Int16 }, + { typeof(int), EdmPrimitiveTypeKind.Int32 }, + { typeof(long), EdmPrimitiveTypeKind.Int64 }, + { typeof(sbyte), EdmPrimitiveTypeKind.SByte }, + { typeof(float), EdmPrimitiveTypeKind.Single }, + { typeof(Stream), EdmPrimitiveTypeKind.Stream }, + { typeof(string), EdmPrimitiveTypeKind.String }, + { typeof(TimeSpan), EdmPrimitiveTypeKind.Duration }, + }; } + } - [Fact] - public void DefaultOdataSerializerProvider_Ctor_ThrowsArgumentNull_ServiceProvider() - { - ExceptionAssert.ThrowsArgumentNull( - () => new ODataSerializerProvider(serviceProvider: null), - "serviceProvider"); - } + [Fact] + public void DefaultOdataSerializerProvider_Ctor_ThrowsArgumentNull_ServiceProvider() + { + ExceptionAssert.ThrowsArgumentNull( + () => new ODataSerializerProvider(serviceProvider: null), + "serviceProvider"); + } - [Fact] - public void GetODataPayloadSerializer_ThrowsArgumentNull_Type() - { - // Arrange - HttpRequest request = GetRequest(model: null); + [Fact] + public void GetODataPayloadSerializer_ThrowsArgumentNull_Type() + { + // Arrange + HttpRequest request = GetRequest(model: null); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => _serializerProvider.GetODataPayloadSerializer(type: null, request: request), - "type"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => _serializerProvider.GetODataPayloadSerializer(type: null, request: request), + "type"); + } - [Fact] - public void GetODataPayloadSerializer_ThrowsArgumentNull_Request() - { - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => _serializerProvider.GetODataPayloadSerializer(typeof(int), request: null), - "request"); - } + [Fact] + public void GetODataPayloadSerializer_ThrowsArgumentNull_Request() + { + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => _serializerProvider.GetODataPayloadSerializer(typeof(int), request: null), + "request"); + } - [Theory] - [MemberData(nameof(EdmPrimitiveMappingData))] - public void GetODataSerializer_Primitive(Type type, EdmPrimitiveTypeKind edmPrimitiveTypeKind) - { - // Arrange - HttpRequest request = GetRequest(EdmCoreModel.Instance); + [Theory] + [MemberData(nameof(EdmPrimitiveMappingData))] + public void GetODataSerializer_Primitive(Type type, EdmPrimitiveTypeKind edmPrimitiveTypeKind) + { + // Arrange + HttpRequest request = GetRequest(EdmCoreModel.Instance); - // Act - IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(type, request); + // Act + IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(type, request); - // Assert - Assert.NotEqual(EdmPrimitiveTypeKind.None, edmPrimitiveTypeKind); - Assert.NotNull(serializer); - ODataPrimitiveSerializer primitiveSerializer = Assert.IsType(serializer); - Assert.Equal(ODataPayloadKind.Property, primitiveSerializer.ODataPayloadKind); - } + // Assert + Assert.NotEqual(EdmPrimitiveTypeKind.None, edmPrimitiveTypeKind); + Assert.NotNull(serializer); + ODataPrimitiveSerializer primitiveSerializer = Assert.IsType(serializer); + Assert.Equal(ODataPayloadKind.Property, primitiveSerializer.ODataPayloadKind); + } - [Theory] - [MemberData(nameof(EdmPrimitiveMappingData))] - public void GetODataPayloadSerializer_ReturnsRawValueSerializer_ForValueRequests(Type type, EdmPrimitiveTypeKind edmPrimitiveTypeKind) - { - // Arrange - ODataPath odataPath = new ODataPath(new ValueSegment(EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32))); - HttpRequest request = GetRequest(EdmCoreModel.Instance); - request.ODataFeature().Path = odataPath; - - // Act - IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(type, request); - - // Assert - Assert.NotEqual(EdmPrimitiveTypeKind.None, edmPrimitiveTypeKind); - Assert.NotNull(serializer); - Assert.Equal(ODataPayloadKind.Value, serializer.ODataPayloadKind); - } + [Theory] + [MemberData(nameof(EdmPrimitiveMappingData))] + public void GetODataPayloadSerializer_ReturnsRawValueSerializer_ForValueRequests(Type type, EdmPrimitiveTypeKind edmPrimitiveTypeKind) + { + // Arrange + ODataPath odataPath = new ODataPath(new ValueSegment(EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32))); + HttpRequest request = GetRequest(EdmCoreModel.Instance); + request.ODataFeature().Path = odataPath; + + // Act + IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(type, request); + + // Assert + Assert.NotEqual(EdmPrimitiveTypeKind.None, edmPrimitiveTypeKind); + Assert.NotNull(serializer); + Assert.Equal(ODataPayloadKind.Value, serializer.ODataPayloadKind); + } - [Fact] - public void GetODataSerializer_Enum() - { - // Arrange - HttpRequest request = GetRequest(_edmModel); + [Fact] + public void GetODataSerializer_Enum() + { + // Arrange + HttpRequest request = GetRequest(_edmModel); - // Act - IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(typeof(TestEnum), request); + // Act + IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(typeof(TestEnum), request); - // Assert - Assert.NotNull(serializer); - var enumSerializer = Assert.IsType(serializer); - Assert.Equal(ODataPayloadKind.Property, enumSerializer.ODataPayloadKind); - } + // Assert + Assert.NotNull(serializer); + var enumSerializer = Assert.IsType(serializer); + Assert.Equal(ODataPayloadKind.Property, enumSerializer.ODataPayloadKind); + } - [Fact] - public void GetODataPayloadSerializer_ReturnsRawValueSerializer_ForEnumValueRequests() - { - // Arrange - ODataPath odataPath = new ODataPath(new ValueSegment(EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32))); - HttpRequest request = GetRequest(_edmModel); - request.ODataFeature().Path = odataPath; - - // Act - IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(typeof(TestEnum), request); - - // Assert - Assert.NotNull(serializer); - var rawValueSerializer = Assert.IsType(serializer); - Assert.Equal(ODataPayloadKind.Value, rawValueSerializer.ODataPayloadKind); - } + [Fact] + public void GetODataPayloadSerializer_ReturnsRawValueSerializer_ForEnumValueRequests() + { + // Arrange + ODataPath odataPath = new ODataPath(new ValueSegment(EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32))); + HttpRequest request = GetRequest(_edmModel); + request.ODataFeature().Path = odataPath; + + // Act + IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(typeof(TestEnum), request); + + // Assert + Assert.NotNull(serializer); + var rawValueSerializer = Assert.IsType(serializer); + Assert.Equal(ODataPayloadKind.Value, rawValueSerializer.ODataPayloadKind); + } - [Theory] - [InlineData("DollarCountEntities/$count", typeof(DollarCountEntity))] - [InlineData("DollarCountEntities(5)/StringCollectionProp/$count", typeof(string))] - [InlineData("DollarCountEntities(5)/EnumCollectionProp/$count", typeof(Color))] - [InlineData("DollarCountEntities(5)/TimeSpanCollectionProp/$count", typeof(TimeSpan))] - [InlineData("DollarCountEntities(5)/ComplexCollectionProp/$count", typeof(DollarCountComplex))] - [InlineData("DollarCountEntities(5)/EntityCollectionProp/$count", typeof(DollarCountEntity))] - [InlineData("UnboundFunctionReturnsPrimitveCollection()/$count", typeof(int))] - [InlineData("UnboundFunctionReturnsEnumCollection()/$count", typeof(Color))] - [InlineData("UnboundFunctionReturnsDateTimeOffsetCollection()/$count", typeof(DateTimeOffset))] - [InlineData("UnboundFunctionReturnsDateCollection()/$count", typeof(Date))] - [InlineData("UnboundFunctionReturnsComplexCollection()/$count", typeof(DollarCountComplex))] - [InlineData("UnboundFunctionReturnsEntityCollection()/$count", typeof(DollarCountEntity))] - [InlineData("DollarCountEntities/Default.BoundFunctionReturnsPrimitveCollection()/$count", typeof(DateTimeOffset))] - [InlineData("DollarCountEntities/Default.BoundFunctionReturnsEnumCollection()/$count", typeof(Color))] - [InlineData("DollarCountEntities/Default.BoundFunctionReturnsDateTimeOffsetCollection()/$count", typeof(DateTimeOffset))] - [InlineData("DollarCountEntities/Default.BoundFunctionReturnsComplexCollection()/$count", typeof(DollarCountComplex))] - [InlineData("DollarCountEntities/Default.BoundFunctionReturnsEntityCollection()/$count", typeof(DollarCountEntity))] - public void GetODataPayloadSerializer_ReturnsRawValueSerializer_ForDollarCountRequests(string uri, Type elementType) - { - // Arrange - IEdmModel model = _edmDollarCountModel; + [Theory] + [InlineData("DollarCountEntities/$count", typeof(DollarCountEntity))] + [InlineData("DollarCountEntities(5)/StringCollectionProp/$count", typeof(string))] + [InlineData("DollarCountEntities(5)/EnumCollectionProp/$count", typeof(Color))] + [InlineData("DollarCountEntities(5)/TimeSpanCollectionProp/$count", typeof(TimeSpan))] + [InlineData("DollarCountEntities(5)/ComplexCollectionProp/$count", typeof(DollarCountComplex))] + [InlineData("DollarCountEntities(5)/EntityCollectionProp/$count", typeof(DollarCountEntity))] + [InlineData("UnboundFunctionReturnsPrimitveCollection()/$count", typeof(int))] + [InlineData("UnboundFunctionReturnsEnumCollection()/$count", typeof(Color))] + [InlineData("UnboundFunctionReturnsDateTimeOffsetCollection()/$count", typeof(DateTimeOffset))] + [InlineData("UnboundFunctionReturnsDateCollection()/$count", typeof(Date))] + [InlineData("UnboundFunctionReturnsComplexCollection()/$count", typeof(DollarCountComplex))] + [InlineData("UnboundFunctionReturnsEntityCollection()/$count", typeof(DollarCountEntity))] + [InlineData("DollarCountEntities/Default.BoundFunctionReturnsPrimitveCollection()/$count", typeof(DateTimeOffset))] + [InlineData("DollarCountEntities/Default.BoundFunctionReturnsEnumCollection()/$count", typeof(Color))] + [InlineData("DollarCountEntities/Default.BoundFunctionReturnsDateTimeOffsetCollection()/$count", typeof(DateTimeOffset))] + [InlineData("DollarCountEntities/Default.BoundFunctionReturnsComplexCollection()/$count", typeof(DollarCountComplex))] + [InlineData("DollarCountEntities/Default.BoundFunctionReturnsEntityCollection()/$count", typeof(DollarCountEntity))] + public void GetODataPayloadSerializer_ReturnsRawValueSerializer_ForDollarCountRequests(string uri, Type elementType) + { + // Arrange + IEdmModel model = _edmDollarCountModel; - Type type = typeof(ICollection<>).MakeGenericType(elementType); - ODataUriParser parser = new ODataUriParser(model, new Uri(uri, UriKind.Relative)); - var path = parser.ParsePath(); + Type type = typeof(ICollection<>).MakeGenericType(elementType); + ODataUriParser parser = new ODataUriParser(model, new Uri(uri, UriKind.Relative)); + var path = parser.ParsePath(); - var request = RequestFactory.Create(model); - request.ODataFeature().Path = path; + var request = RequestFactory.Create(model); + request.ODataFeature().Path = path; - // Act - var serializer = _serializerProvider.GetODataPayloadSerializer(type, request); + // Act + var serializer = _serializerProvider.GetODataPayloadSerializer(type, request); - // Assert - Assert.NotNull(serializer); - var rawValueSerializer = Assert.IsType(serializer); - Assert.Equal(ODataPayloadKind.Value, rawValueSerializer.ODataPayloadKind); - } + // Assert + Assert.NotNull(serializer); + var rawValueSerializer = Assert.IsType(serializer); + Assert.Equal(ODataPayloadKind.Value, rawValueSerializer.ODataPayloadKind); + } - public static IEdmModel GetDollarCountEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - var entityCollection = builder.EntitySet("DollarCountEntities").EntityType.Collection; + public static IEdmModel GetDollarCountEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + var entityCollection = builder.EntitySet("DollarCountEntities").EntityType.Collection; - // Add unbound functions that return collection. - FunctionConfiguration function = builder.Function("UnboundFunctionReturnsPrimitveCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + // Add unbound functions that return collection. + FunctionConfiguration function = builder.Function("UnboundFunctionReturnsPrimitveCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = builder.Function("UnboundFunctionReturnsEnumCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = builder.Function("UnboundFunctionReturnsEnumCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = builder.Function("UnboundFunctionReturnsDateTimeOffsetCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = builder.Function("UnboundFunctionReturnsDateTimeOffsetCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = builder.Function("UnboundFunctionReturnsDateCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = builder.Function("UnboundFunctionReturnsDateCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = builder.Function("UnboundFunctionReturnsComplexCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = builder.Function("UnboundFunctionReturnsComplexCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = builder.Function("UnboundFunctionReturnsEntityCollection"); - function.IsComposable = true; - function.ReturnsCollectionFromEntitySet("DollarCountEntities"); + function = builder.Function("UnboundFunctionReturnsEntityCollection"); + function.IsComposable = true; + function.ReturnsCollectionFromEntitySet("DollarCountEntities"); - // Add bound functions that return collection. - function = entityCollection.Function("BoundFunctionReturnsPrimitveCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + // Add bound functions that return collection. + function = entityCollection.Function("BoundFunctionReturnsPrimitveCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = entityCollection.Function("BoundFunctionReturnsEnumCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = entityCollection.Function("BoundFunctionReturnsEnumCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = entityCollection.Function("BoundFunctionReturnsDateTimeOffsetCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = entityCollection.Function("BoundFunctionReturnsDateTimeOffsetCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = entityCollection.Function("BoundFunctionReturnsComplexCollection"); - function.IsComposable = true; - function.ReturnsCollection(); + function = entityCollection.Function("BoundFunctionReturnsComplexCollection"); + function.IsComposable = true; + function.ReturnsCollection(); - function = entityCollection.Function("BoundFunctionReturnsEntityCollection"); - function.IsComposable = true; - function.ReturnsCollectionFromEntitySet("DollarCountEntities"); + function = entityCollection.Function("BoundFunctionReturnsEntityCollection"); + function.IsComposable = true; + function.ReturnsCollectionFromEntitySet("DollarCountEntities"); - return builder.GetEdmModel(); - } + return builder.GetEdmModel(); + } - private class DollarCountEntity - { - public int Id { get; set; } - public string[] StringCollectionProp { get; set; } - public Color[] EnumCollectionProp { get; set; } - public TimeSpan[] TimeSpanCollectionProp { get; set; } - public DollarCountComplex[] ComplexCollectionProp { get; set; } - public DollarCountEntity[] EntityCollectionProp { get; set; } - public int[] DollarCountNotAllowedCollectionProp { get; set; } - } + private class DollarCountEntity + { + public int Id { get; set; } + public string[] StringCollectionProp { get; set; } + public Color[] EnumCollectionProp { get; set; } + public TimeSpan[] TimeSpanCollectionProp { get; set; } + public DollarCountComplex[] ComplexCollectionProp { get; set; } + public DollarCountEntity[] EntityCollectionProp { get; set; } + public int[] DollarCountNotAllowedCollectionProp { get; set; } + } - private class DollarCountComplex - { - public string StringProp { get; set; } - } + private class DollarCountComplex + { + public string StringProp { get; set; } + } - [Fact] - public void GetODataSerializer_Resource_ForEntity() - { - // Arrange - HttpRequest request = GetRequest(_edmModel); + [Fact] + public void GetODataSerializer_Resource_ForEntity() + { + // Arrange + HttpRequest request = GetRequest(_edmModel); - // Act - IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(typeof(Product), request); + // Act + IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(typeof(Product), request); - // Assert - Assert.NotNull(serializer); - var entitySerializer = Assert.IsType(serializer); - Assert.Equal(entitySerializer.SerializerProvider, _serializerProvider); - Assert.Equal(ODataPayloadKind.Resource, entitySerializer.ODataPayloadKind); - } + // Assert + Assert.NotNull(serializer); + var entitySerializer = Assert.IsType(serializer); + Assert.Equal(entitySerializer.SerializerProvider, _serializerProvider); + Assert.Equal(ODataPayloadKind.Resource, entitySerializer.ODataPayloadKind); + } - [Fact] - public void GetODataSerializer_Resource_ForComplex() - { - // Arrange - HttpRequest request = GetRequest(_edmModel); + [Fact] + public void GetODataSerializer_Resource_ForComplex() + { + // Arrange + HttpRequest request = GetRequest(_edmModel); - // Act - IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(typeof(Address), request); + // Act + IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(typeof(Address), request); - // Assert - Assert.NotNull(serializer); - var complexSerializer = Assert.IsType(serializer); - Assert.Equal(complexSerializer.SerializerProvider, _serializerProvider); - Assert.Equal(ODataPayloadKind.Resource, complexSerializer.ODataPayloadKind); - } - - [Theory] - [InlineData(typeof(Product[]))] - [InlineData(typeof(IEnumerable))] - [InlineData(typeof(ICollection))] - [InlineData(typeof(IList))] - [InlineData(typeof(List))] - [InlineData(typeof(PageResult))] - public void GetODataSerializer_ResourceSet_ForEntityCollection(Type collectionType) - { - // Arrange - HttpRequest request = GetRequest(_edmModel); + // Assert + Assert.NotNull(serializer); + var complexSerializer = Assert.IsType(serializer); + Assert.Equal(complexSerializer.SerializerProvider, _serializerProvider); + Assert.Equal(ODataPayloadKind.Resource, complexSerializer.ODataPayloadKind); + } - // Act - IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(collectionType, request); + [Theory] + [InlineData(typeof(Product[]))] + [InlineData(typeof(IEnumerable))] + [InlineData(typeof(ICollection))] + [InlineData(typeof(IList))] + [InlineData(typeof(List))] + [InlineData(typeof(PageResult))] + public void GetODataSerializer_ResourceSet_ForEntityCollection(Type collectionType) + { + // Arrange + HttpRequest request = GetRequest(_edmModel); - // Assert - Assert.NotNull(serializer); - var resourceSetSerializer = Assert.IsType(serializer); - Assert.Equal(ODataPayloadKind.ResourceSet, resourceSetSerializer.ODataPayloadKind); - Assert.Same(resourceSetSerializer.SerializerProvider, _serializerProvider); - } + // Act + IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(collectionType, request); - [Theory] - [InlineData(typeof(Address[]))] - [InlineData(typeof(IEnumerable
))] - [InlineData(typeof(ICollection
))] - [InlineData(typeof(IList
))] - [InlineData(typeof(List
))] - [InlineData(typeof(PageResult
))] - public void GetODataSerializer_ResourceSet_ForComplexCollection(Type collectionType) - { - // Arrange - HttpRequest request = GetRequest(_edmModel); + // Assert + Assert.NotNull(serializer); + var resourceSetSerializer = Assert.IsType(serializer); + Assert.Equal(ODataPayloadKind.ResourceSet, resourceSetSerializer.ODataPayloadKind); + Assert.Same(resourceSetSerializer.SerializerProvider, _serializerProvider); + } - // Act - IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(collectionType, request); + [Theory] + [InlineData(typeof(Address[]))] + [InlineData(typeof(IEnumerable
))] + [InlineData(typeof(ICollection
))] + [InlineData(typeof(IList
))] + [InlineData(typeof(List
))] + [InlineData(typeof(PageResult
))] + public void GetODataSerializer_ResourceSet_ForComplexCollection(Type collectionType) + { + // Arrange + HttpRequest request = GetRequest(_edmModel); - // Assert - Assert.NotNull(serializer); - var resourceSetSerializer = Assert.IsType(serializer); - Assert.Equal(ODataPayloadKind.ResourceSet, resourceSetSerializer.ODataPayloadKind); - Assert.Same(resourceSetSerializer.SerializerProvider, _serializerProvider); - } + // Act + IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(collectionType, request); - [Theory] - [InlineData(typeof(ODataError), typeof(ODataErrorSerializer))] - [InlineData(typeof(Uri), typeof(ODataEntityReferenceLinkSerializer))] - [InlineData(typeof(ODataEntityReferenceLink), typeof(ODataEntityReferenceLinkSerializer))] - [InlineData(typeof(Uri[]), typeof(ODataEntityReferenceLinksSerializer))] - [InlineData(typeof(List), typeof(ODataEntityReferenceLinksSerializer))] - [InlineData(typeof(ODataEntityReferenceLinks), typeof(ODataEntityReferenceLinksSerializer))] - [InlineData(typeof(DeltaSet), typeof(ODataDeltaResourceSetSerializer))] - public void GetODataSerializer_Returns_ExpectedSerializerType(Type payloadType, Type expectedSerializerType) - { - // Arrange - HttpRequest request = GetRequest(EdmCoreModel.Instance); + // Assert + Assert.NotNull(serializer); + var resourceSetSerializer = Assert.IsType(serializer); + Assert.Equal(ODataPayloadKind.ResourceSet, resourceSetSerializer.ODataPayloadKind); + Assert.Same(resourceSetSerializer.SerializerProvider, _serializerProvider); + } - // Act - IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(payloadType, request); + [Theory] + [InlineData(typeof(ODataError), typeof(ODataErrorSerializer))] + [InlineData(typeof(Uri), typeof(ODataEntityReferenceLinkSerializer))] + [InlineData(typeof(ODataEntityReferenceLink), typeof(ODataEntityReferenceLinkSerializer))] + [InlineData(typeof(Uri[]), typeof(ODataEntityReferenceLinksSerializer))] + [InlineData(typeof(List), typeof(ODataEntityReferenceLinksSerializer))] + [InlineData(typeof(ODataEntityReferenceLinks), typeof(ODataEntityReferenceLinksSerializer))] + [InlineData(typeof(DeltaSet), typeof(ODataDeltaResourceSetSerializer))] + public void GetODataSerializer_Returns_ExpectedSerializerType(Type payloadType, Type expectedSerializerType) + { + // Arrange + HttpRequest request = GetRequest(EdmCoreModel.Instance); - // Assert - Assert.NotNull(serializer); - Assert.IsType(expectedSerializerType, serializer); - } + // Act + IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(payloadType, request); - [Fact] - public void GetODataSerializer_ReturnsSameSerializer_ForSameType() - { - // Arrange - HttpRequest request = GetRequest(_edmModel); + // Assert + Assert.NotNull(serializer); + Assert.IsType(expectedSerializerType, serializer); + } - // Act - IODataSerializer firstCallSerializer = _serializerProvider.GetODataPayloadSerializer(typeof(Product), request); - IODataSerializer secondCallSerializer = _serializerProvider.GetODataPayloadSerializer(typeof(Product), request); + [Fact] + public void GetODataSerializer_ReturnsSameSerializer_ForSameType() + { + // Arrange + HttpRequest request = GetRequest(_edmModel); - // Assert - Assert.Same(firstCallSerializer, secondCallSerializer); - } + // Act + IODataSerializer firstCallSerializer = _serializerProvider.GetODataPayloadSerializer(typeof(Product), request); + IODataSerializer secondCallSerializer = _serializerProvider.GetODataPayloadSerializer(typeof(Product), request); - [Fact] - public void GetODataSerializer_ReturnsNull_IfTypeUnknown() - { - // Arrange - HttpRequest request = GetRequest(_edmModel); + // Assert + Assert.Same(firstCallSerializer, secondCallSerializer); + } - // Act - IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(typeof(UnknownEntityType), request); + [Fact] + public void GetODataSerializer_ReturnsNull_IfTypeUnknown() + { + // Arrange + HttpRequest request = GetRequest(_edmModel); - // Assert - Assert.Null(serializer); - } + // Act + IODataSerializer serializer = _serializerProvider.GetODataPayloadSerializer(typeof(UnknownEntityType), request); - [Fact] - public void GetEdmTypeSerializer_ThrowsArgumentNull_EdmType() - { - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => _serializerProvider.GetEdmTypeSerializer(edmType: null), - "edmType"); - } + // Assert + Assert.Null(serializer); + } - [Fact] - public void GetEdmTypeSerializer_Caches_CreateEdmTypeSerializerOutput() - { - // Arrange - IEdmTypeReference edmType = new Mock().Object; + [Fact] + public void GetEdmTypeSerializer_ThrowsArgumentNull_EdmType() + { + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => _serializerProvider.GetEdmTypeSerializer(edmType: null), + "edmType"); + } - // Act - IODataSerializer serializer1 = _serializerProvider.GetEdmTypeSerializer(edmType); - IODataSerializer serializer2 = _serializerProvider.GetEdmTypeSerializer(edmType); + [Fact] + public void GetEdmTypeSerializer_Caches_CreateEdmTypeSerializerOutput() + { + // Arrange + IEdmTypeReference edmType = new Mock().Object; - // Assert - Assert.Same(serializer2, serializer1); - } + // Act + IODataSerializer serializer1 = _serializerProvider.GetEdmTypeSerializer(edmType); + IODataSerializer serializer2 = _serializerProvider.GetEdmTypeSerializer(edmType); - private static IServiceProvider GetServiceProvider() - { - IServiceCollection services = new ServiceCollection(); - - services.AddSingleton(); - - // Serializers. - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - return services.BuildServiceProvider(); - } + // Assert + Assert.Same(serializer2, serializer1); + } - private HttpRequest GetRequest(IEdmModel model) - { - HttpContext context = new DefaultHttpContext(); - context.ODataFeature().Model = model; - return context.Request; - } + private static IServiceProvider GetServiceProvider() + { + IServiceCollection services = new ServiceCollection(); + + services.AddSingleton(); + + // Serializers. + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services.BuildServiceProvider(); + } - private static IEdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); + private HttpRequest GetRequest(IEdmModel model) + { + HttpContext context = new DefaultHttpContext(); + context.ODataFeature().Model = model; + return context.Request; + } - EdmEntityType product = new EdmEntityType("NS", "Product"); - model.AddElement(product); - model.SetAnnotationValue(product, new ClrTypeAnnotation(typeof(Product))); + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); - EdmComplexType address = new EdmComplexType("NS", "Address"); - model.AddElement(address); - model.SetAnnotationValue(address, new ClrTypeAnnotation(typeof(Address))); + EdmEntityType product = new EdmEntityType("NS", "Product"); + model.AddElement(product); + model.SetAnnotationValue(product, new ClrTypeAnnotation(typeof(Product))); - EdmEnumType enumType = new EdmEnumType("NS", "TestEnum"); - enumType.AddMember(new EdmEnumMember(enumType, "FirstValue", new EdmEnumMemberValue(0))); - enumType.AddMember(new EdmEnumMember(enumType, "FirstValue", new EdmEnumMemberValue(1))); - model.AddElement(enumType); - model.SetAnnotationValue(enumType, new ClrTypeAnnotation(typeof(TestEnum))); + EdmComplexType address = new EdmComplexType("NS", "Address"); + model.AddElement(address); + model.SetAnnotationValue(address, new ClrTypeAnnotation(typeof(Address))); - return model; - } + EdmEnumType enumType = new EdmEnumType("NS", "TestEnum"); + enumType.AddMember(new EdmEnumMember(enumType, "FirstValue", new EdmEnumMemberValue(0))); + enumType.AddMember(new EdmEnumMember(enumType, "FirstValue", new EdmEnumMemberValue(1))); + model.AddElement(enumType); + model.SetAnnotationValue(enumType, new ClrTypeAnnotation(typeof(TestEnum))); - private class Product - { } + return model; + } - private class Address - { } + private class Product + { } - private enum TestEnum - { - FirstValue, - SecondValue - } + private class Address + { } - private class UnknownEntityType - { } + private enum TestEnum + { + FirstValue, + SecondValue } + + private class UnknownEntityType + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerTest.cs index bb0bc1776..53f8a32a0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataSerializerTest.cs @@ -13,30 +13,29 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataSerializerTest { - public class ODataSerializerTest + [Fact] + public void Ctor_SetsProperty_ODataPayloadKind() { - [Fact] - public void Ctor_SetsProperty_ODataPayloadKind() - { - // Arrange - ODataSerializer serializer = new Mock(ODataPayloadKind.Unsupported).Object; + // Arrange + ODataSerializer serializer = new Mock(ODataPayloadKind.Unsupported).Object; - // Act & Assert - Assert.Equal(ODataPayloadKind.Unsupported, serializer.ODataPayloadKind); - } + // Act & Assert + Assert.Equal(ODataPayloadKind.Unsupported, serializer.ODataPayloadKind); + } - [Fact] - public async Task WriteObjectAsync_Throws_NotSupported() - { - // Arrange - ODataSerializer serializer = new Mock(ODataPayloadKind.Unsupported) { CallBase = true }.Object; + [Fact] + public async Task WriteObjectAsync_Throws_NotSupported() + { + // Arrange + ODataSerializer serializer = new Mock(ODataPayloadKind.Unsupported) { CallBase = true }.Object; - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectAsync(graph: null, type: typeof(int), messageWriter: null, writeContext: null), - "ODataSerializerProxy does not support WriteObject."); - } + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectAsync(graph: null, type: typeof(int), messageWriter: null, writeContext: null), + "ODataSerializerProxy does not support WriteObject."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataServiceDocumentSerializerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataServiceDocumentSerializerTests.cs index 5517e83d9..55f3d2b09 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataServiceDocumentSerializerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataServiceDocumentSerializerTests.cs @@ -15,72 +15,71 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataServiceDocumentSerializerTests { - public class ODataServiceDocumentSerializerTests + private Type _workspaceType = typeof(ODataServiceDocument); + + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() { - private Type _workspaceType = typeof(ODataServiceDocument); + // Arrange + ODataServiceDocumentSerializer serializer = new ODataServiceDocumentSerializer(); - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_MessageWriter() - { - // Arrange - ODataServiceDocumentSerializer serializer = new ODataServiceDocumentSerializer(); + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(42, _workspaceType, messageWriter: null, writeContext: null), + "messageWriter"); + } - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(42, _workspaceType, messageWriter: null, writeContext: null), - "messageWriter"); - } + [Fact] + public async Task WriteObjectAsync_ThrowsArgumentNull_Graph() + { + // Arrange + ODataServiceDocumentSerializer serializer = new ODataServiceDocumentSerializer(); - [Fact] - public async Task WriteObjectAsync_ThrowsArgumentNull_Graph() - { - // Arrange - ODataServiceDocumentSerializer serializer = new ODataServiceDocumentSerializer(); + // Act & Assert + await ExceptionAssert.ThrowsArgumentNullAsync( + () => serializer.WriteObjectAsync(null, type: _workspaceType, messageWriter: null, writeContext: null), + "graph"); + } - // Act & Assert - await ExceptionAssert.ThrowsArgumentNullAsync( - () => serializer.WriteObjectAsync(null, type: _workspaceType, messageWriter: null, writeContext: null), - "graph"); - } + [Fact] + public async Task WriteObjectAsync_Throws_CannotWriteType() + { + // Arrange + ODataServiceDocumentSerializer serializer = new ODataServiceDocumentSerializer(); - [Fact] - public async Task WriteObjectAsync_Throws_CannotWriteType() - { - // Arrange - ODataServiceDocumentSerializer serializer = new ODataServiceDocumentSerializer(); + // Act & Assert + await ExceptionAssert.ThrowsAsync( + () => serializer.WriteObjectAsync(42, _workspaceType, messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: null), + "ODataServiceDocumentSerializer cannot write an object of type 'ODataServiceDocument'."); + } - // Act & Assert - await ExceptionAssert.ThrowsAsync( - () => serializer.WriteObjectAsync(42, _workspaceType, messageWriter: ODataTestUtil.GetMockODataMessageWriter(), writeContext: null), - "ODataServiceDocumentSerializer cannot write an object of type 'ODataServiceDocument'."); - } + [Fact] + public async Task ODataServiceDocumentSerializer_Works() + { + // Arrange + ODataServiceDocumentSerializer serializer = new ODataServiceDocumentSerializer(); + MemoryStream stream = new MemoryStream(); + IODataResponseMessage message = new ODataMessageWrapper(stream); - [Fact] - public async Task ODataServiceDocumentSerializer_Works() + ODataMessageWriterSettings settings = new ODataMessageWriterSettings { - // Arrange - ODataServiceDocumentSerializer serializer = new ODataServiceDocumentSerializer(); - MemoryStream stream = new MemoryStream(); - IODataResponseMessage message = new ODataMessageWrapper(stream); - - ODataMessageWriterSettings settings = new ODataMessageWriterSettings - { - ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } - }; - settings.SetContentType(ODataFormat.Json); + ODataUri = new ODataUri { ServiceRoot = new Uri("http://any/"), } + }; + settings.SetContentType(ODataFormat.Json); - ODataMessageWriter writer = new ODataMessageWriter(message, settings); + ODataMessageWriter writer = new ODataMessageWriter(message, settings); - // Act - await serializer.WriteObjectAsync(new ODataServiceDocument(), _workspaceType, writer, new ODataSerializerContext()); - await stream.FlushAsync(); - stream.Seek(0, SeekOrigin.Begin); - string result = new StreamReader(stream).ReadToEnd(); + // Act + await serializer.WriteObjectAsync(new ODataServiceDocument(), _workspaceType, writer, new ODataSerializerContext()); + await stream.FlushAsync(); + stream.Seek(0, SeekOrigin.Begin); + string result = new StreamReader(stream).ReadToEnd(); - // Assert - Assert.Equal("{\"@odata.context\":\"http://any/$metadata\",\"value\":[]}", result); - } + // Assert + Assert.Equal("{\"@odata.context\":\"http://any/$metadata\",\"value\":[]}", result); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataValueExtensionsTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataValueExtensionsTest.cs index c3d8b9ba5..5f7686800 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataValueExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/ODataValueExtensionsTest.cs @@ -10,33 +10,32 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class ODataValueExtensionsTest { - public class ODataValueExtensionsTest + public static TheoryDataSet GetInnerValueTestData { - public static TheoryDataSet GetInnerValueTestData + get { - get - { - ODataCollectionValue collectionValue = new ODataCollectionValue(); - ODataStreamReferenceValue streamReferenceValue = new ODataStreamReferenceValue(); + ODataCollectionValue collectionValue = new ODataCollectionValue(); + ODataStreamReferenceValue streamReferenceValue = new ODataStreamReferenceValue(); - return new TheoryDataSet - { - { new ODataPrimitiveValue(100), 100 }, - { new ODataNullValue(), null }, - { collectionValue, collectionValue }, - { streamReferenceValue, streamReferenceValue }, - { null, null } - }; - } + return new TheoryDataSet + { + { new ODataPrimitiveValue(100), 100 }, + { new ODataNullValue(), null }, + { collectionValue, collectionValue }, + { streamReferenceValue, streamReferenceValue }, + { null, null } + }; } + } - [Theory] - [MemberData(nameof(GetInnerValueTestData))] - public void GetInnerValue_Returns_CorrectObject(ODataValue value, object expectedResult) - { - Assert.Equal(expectedResult, value.GetInnerValue()); - } + [Theory] + [MemberData(nameof(GetInnerValueTestData))] + public void GetInnerValue_Returns_CorrectObject(ODataValue value, object expectedResult) + { + Assert.Equal(expectedResult, value.GetInnerValue()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SelectExpandNodeTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SelectExpandNodeTest.cs index a8ff65e24..9fbec302f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SelectExpandNodeTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SelectExpandNodeTest.cs @@ -17,774 +17,773 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +public class SelectExpandNodeTest { - public class SelectExpandNodeTest - { - private CustomersModelWithInheritance _model = new CustomersModelWithInheritance(); + private CustomersModelWithInheritance _model = new CustomersModelWithInheritance(); - [Fact] - public void Ctor_ThrowsArgumentNull_WriteContext() - { - // Arrange & Act & Assert - IEdmStructuredType structuredType = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => new SelectExpandNode(structuredType, null), "writeContext"); - } + [Fact] + public void Ctor_ThrowsArgumentNull_WriteContext() + { + // Arrange & Act & Assert + IEdmStructuredType structuredType = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => new SelectExpandNode(structuredType, null), "writeContext"); + } - [Fact] - public void Ctor_ThrowsArgumentNull_StructuredType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new SelectExpandNode(selectExpandClause: null, structuredType: null, model: EdmCoreModel.Instance), - "structuredType"); - } + [Fact] + public void Ctor_ThrowsArgumentNull_StructuredType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new SelectExpandNode(selectExpandClause: null, structuredType: null, model: EdmCoreModel.Instance), + "structuredType"); + } - [Fact] - public void Ctor_ThrowsArgumentNull_EdmModel() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new SelectExpandNode(selectExpandClause: null, structuredType: new Mock().Object, model: null), - "model"); - } + [Fact] + public void Ctor_ThrowsArgumentNull_EdmModel() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new SelectExpandNode(selectExpandClause: null, structuredType: new Mock().Object, model: null), + "model"); + } - [Theory] - [InlineData("ID,ID", "ID")] - [InlineData("NS.upgrade,NS.upgrade", "NS.upgrade")] - public void DuplicatedSelectPathInOneDollarSelectWorksAsSingle(string select, string expect) - { - // Arrange - ODataQueryOptionParser parser = new ODataQueryOptionParser(_model.Model, _model.Customer, _model.Customers, - new Dictionary { { "$select", select } }); + [Theory] + [InlineData("ID,ID", "ID")] + [InlineData("NS.upgrade,NS.upgrade", "NS.upgrade")] + public void DuplicatedSelectPathInOneDollarSelectWorksAsSingle(string select, string expect) + { + // Arrange + ODataQueryOptionParser parser = new ODataQueryOptionParser(_model.Model, _model.Customer, _model.Customers, + new Dictionary { { "$select", select } }); - // Act - SelectExpandClause selectAndExpand = parser.ParseSelectAndExpand(); + // Act + SelectExpandClause selectAndExpand = parser.ParseSelectAndExpand(); - // Assert - Assert.NotNull(selectAndExpand); - Assert.False(selectAndExpand.AllSelected); - SelectItem selectItem = Assert.Single(selectAndExpand.SelectedItems); - PathSelectItem pathSelectItem = Assert.IsType(selectItem); - ODataPathSegment pathSegment = Assert.Single(pathSelectItem.SelectedPath); + // Assert + Assert.NotNull(selectAndExpand); + Assert.False(selectAndExpand.AllSelected); + SelectItem selectItem = Assert.Single(selectAndExpand.SelectedItems); + PathSelectItem pathSelectItem = Assert.IsType(selectItem); + ODataPathSegment pathSegment = Assert.Single(pathSelectItem.SelectedPath); - if (expect == "ID") - { - PropertySegment propertySegment = Assert.IsType(pathSegment); - Assert.Equal(expect, propertySegment.Identifier); - } - else - { - OperationSegment operationSegment = Assert.IsType(pathSegment); - Assert.Equal(expect, operationSegment.Operations.Single().FullName()); - } + if (expect == "ID") + { + PropertySegment propertySegment = Assert.IsType(pathSegment); + Assert.Equal(expect, propertySegment.Identifier); } - - [Theory] - [InlineData(null, null, false, "City,ID,Name,SimpleEnum", "Account,Address,OtherAccounts")] // no select and expand -> select all - [InlineData(null, null, true, "City,ID,Name,SimpleEnum,SpecialCustomerProperty", "Account,Address,OtherAccounts,SpecialAddress")] // no select and expand on derived type -> select all - [InlineData("ID", null, false, "ID", null)] // simple select -> select requested - [InlineData("ID", null, true, "ID", null)] // simple select on derived type -> select requested - [InlineData("*", null, false, "City,ID,Name,SimpleEnum", "Account,Address,OtherAccounts")] // simple select with wild card -> select all, no duplication - [InlineData("*", null, true, "City,ID,Name,SimpleEnum,SpecialCustomerProperty", "Account,Address,OtherAccounts,SpecialAddress")] // simple select with wild card on derived type -> select all, no duplication - [InlineData("ID,*", null, false, "City,ID,Name,SimpleEnum", "Account,Address,OtherAccounts")] // simple select with wild card and duplicate -> select all, no duplicates - [InlineData("ID,*", null, true, "City,ID,Name,SimpleEnum,SpecialCustomerProperty", "Account,Address,OtherAccounts,SpecialAddress")] // simple select with wild card and duplicate -> select all, no duplicates - [InlineData("ID,Name", null, false, "ID,Name", null)] // multiple select -> select requested - [InlineData("ID,Name", null, true, "ID,Name", null)] // multiple select on derived type -> select requested - [InlineData("Orders", "Orders", false, null, null)] // only expand -> select no structural property - [InlineData("Orders", "Orders", true, null, null)] // only expand -> select no structural property - [InlineData(null, "Orders", false, "City,ID,Name,SimpleEnum", "Account,Address,OtherAccounts")] // simple expand -> select all - [InlineData(null, "Orders", true, "City,ID,Name,SimpleEnum,SpecialCustomerProperty", "Account,Address,OtherAccounts,SpecialAddress")] // simple expand on derived type -> select all - [InlineData("ID,Name,Orders", "Orders", false, "ID,Name", null)] // expand and select -> select requested - [InlineData("ID,Name,Orders", "Orders", true, "ID,Name", null)] // expand and select on derived type -> select requested - [InlineData("NS.SpecialCustomer/SpecialCustomerProperty", null, false, null, null)] // select derived type properties -> select none - [InlineData("NS.SpecialCustomer/SpecialCustomerProperty", null, true, "SpecialCustomerProperty", null)] // select derived type properties on derived type -> select requested - [InlineData("ID", "Orders($select=ID),Orders($expand=Customer($select=ID))", true, "ID", null)] // deep expand and selects - public void SelectProperties_SelectsExpectedProperties_OnCustomer( - string select, string expand, bool specialCustomer, string structuralsToSelect, string complexesToSelect) + else { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand); + OperationSegment operationSegment = Assert.IsType(pathSegment); + Assert.Equal(expect, operationSegment.Operations.Single().FullName()); + } + } - IEdmStructuredType structuralType = specialCustomer ? _model.SpecialCustomer : _model.Customer; + [Theory] + [InlineData(null, null, false, "City,ID,Name,SimpleEnum", "Account,Address,OtherAccounts")] // no select and expand -> select all + [InlineData(null, null, true, "City,ID,Name,SimpleEnum,SpecialCustomerProperty", "Account,Address,OtherAccounts,SpecialAddress")] // no select and expand on derived type -> select all + [InlineData("ID", null, false, "ID", null)] // simple select -> select requested + [InlineData("ID", null, true, "ID", null)] // simple select on derived type -> select requested + [InlineData("*", null, false, "City,ID,Name,SimpleEnum", "Account,Address,OtherAccounts")] // simple select with wild card -> select all, no duplication + [InlineData("*", null, true, "City,ID,Name,SimpleEnum,SpecialCustomerProperty", "Account,Address,OtherAccounts,SpecialAddress")] // simple select with wild card on derived type -> select all, no duplication + [InlineData("ID,*", null, false, "City,ID,Name,SimpleEnum", "Account,Address,OtherAccounts")] // simple select with wild card and duplicate -> select all, no duplicates + [InlineData("ID,*", null, true, "City,ID,Name,SimpleEnum,SpecialCustomerProperty", "Account,Address,OtherAccounts,SpecialAddress")] // simple select with wild card and duplicate -> select all, no duplicates + [InlineData("ID,Name", null, false, "ID,Name", null)] // multiple select -> select requested + [InlineData("ID,Name", null, true, "ID,Name", null)] // multiple select on derived type -> select requested + [InlineData("Orders", "Orders", false, null, null)] // only expand -> select no structural property + [InlineData("Orders", "Orders", true, null, null)] // only expand -> select no structural property + [InlineData(null, "Orders", false, "City,ID,Name,SimpleEnum", "Account,Address,OtherAccounts")] // simple expand -> select all + [InlineData(null, "Orders", true, "City,ID,Name,SimpleEnum,SpecialCustomerProperty", "Account,Address,OtherAccounts,SpecialAddress")] // simple expand on derived type -> select all + [InlineData("ID,Name,Orders", "Orders", false, "ID,Name", null)] // expand and select -> select requested + [InlineData("ID,Name,Orders", "Orders", true, "ID,Name", null)] // expand and select on derived type -> select requested + [InlineData("NS.SpecialCustomer/SpecialCustomerProperty", null, false, null, null)] // select derived type properties -> select none + [InlineData("NS.SpecialCustomer/SpecialCustomerProperty", null, true, "SpecialCustomerProperty", null)] // select derived type properties on derived type -> select requested + [InlineData("ID", "Orders($select=ID),Orders($expand=Customer($select=ID))", true, "ID", null)] // deep expand and selects + public void SelectProperties_SelectsExpectedProperties_OnCustomer( + string select, string expand, bool specialCustomer, string structuralsToSelect, string complexesToSelect) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand); - // Act - SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, structuralType, _model.Model); + IEdmStructuredType structuralType = specialCustomer ? _model.SpecialCustomer : _model.Customer; - // Assert - if (structuralsToSelect == null) - { - Assert.Null(selectExpandNode.SelectedStructuralProperties); - } - else - { - Assert.Equal(structuralsToSelect, string.Join(",", selectExpandNode.SelectedStructuralProperties.Select(p => p.Name).OrderBy(n => n))); - } + // Act + SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, structuralType, _model.Model); - if (complexesToSelect == null) - { - Assert.Null(selectExpandNode.SelectedComplexProperties); - } - else - { - Assert.Equal(complexesToSelect, string.Join(",", selectExpandNode.SelectedComplexProperties.Keys.Select(p => p.Name).OrderBy(n => n))); - } - } - - [Theory] - [InlineData("ID,Name,Orders", "Orders", false, "Amount,City,ID")] // expand and select -> select all - [InlineData("ID,Name,Orders", "Orders", true, "Amount,City,ID,SpecialOrderProperty")] // expand and select on derived type -> select all - [InlineData("ID,Name,Orders", "Orders($select=ID)", false, "ID")] // expand and select properties on expand -> select requested - [InlineData("ID,Name,Orders", "Orders($select=ID)", true, "ID")] // expand and select properties on expand on derived type -> select requested - [InlineData("Orders", "Orders,Orders($expand=Customer)", false, "Amount,City,ID")] - [InlineData("Orders", "Orders,Orders($expand=Customer)", true, "Amount,City,ID,SpecialOrderProperty")] - public void SelectProperties_Selects_ExpectedProperties_OnExpandedOrders(string select, string expand, bool specialOrder, string structuralPropertiesToSelect) + // Assert + if (structuralsToSelect == null) { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand); - SelectExpandClause nestedSelectExpandClause = selectExpandClause.SelectedItems.OfType().Single().SelectAndExpand; - IEdmStructuredType structuralType = specialOrder ? _model.SpecialOrder : _model.Order; - - // Act - SelectExpandNode selectExpandNode = new SelectExpandNode(nestedSelectExpandClause, structuralType, _model.Model); - - // Assert - Assert.Equal(structuralPropertiesToSelect, String.Join(",", selectExpandNode.SelectedStructuralProperties.Select(p => p.Name).OrderBy(n => n))); + Assert.Null(selectExpandNode.SelectedStructuralProperties); } - - [Theory] - [InlineData("Address/Street,Address/City,Address/ZipCode", "Address", "Street,City,ZipCode")] // complex - [InlineData("Address($select=Street,City,ZipCode)", "Address", "Street,City,ZipCode")] - [InlineData("OtherAccounts/Bank,OtherAccounts/CardNum", "OtherAccounts", "Bank,CardNum")] // Collection complex - [InlineData("OtherAccounts($select=Bank,CardNum)", "OtherAccounts", "Bank,CardNum")] - public void SelectProperties_OnSubPrimitivePropertyFromComplex_SelectsExpectedProperties(string select, string firstSelected, string secondSelected) + else { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null); - - // Act: Top Level - SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); - - // Assert - Assert.NotNull(selectExpandNode.SelectedComplexProperties); - var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); - Assert.Equal(firstSelected, firstLevelSelected.Key.Name); - - Assert.NotNull(firstLevelSelected.Value); - Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); - - // Act: Sub Level - IEdmStructuredType subLevelElementType = firstLevelSelected.Key.Type.ToStructuredType(); - SelectExpandNode subSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, subLevelElementType, _model.Model); - Assert.Null(subSelectExpandNode.SelectedComplexProperties); - - // Assert - Assert.NotNull(subSelectExpandNode.SelectedStructuralProperties); - var selectedProperties = secondSelected.Split(','); - Assert.Equal(selectedProperties.Length, subSelectExpandNode.SelectedStructuralProperties.Count); - Assert.Equal(secondSelected, String.Join(",", subSelectExpandNode.SelectedStructuralProperties.Select(s => s.Name))); + Assert.Equal(structuralsToSelect, string.Join(",", selectExpandNode.SelectedStructuralProperties.Select(p => p.Name).OrderBy(n => n))); } - [Theory] - [InlineData("Account/BankAddress/Street,Account/BankAddress/City,Account/BankAddress/ZipCode")] - [InlineData("Account/BankAddress($select=Street,City,ZipCode)")] - public void SelectProperties_OnMultipleLevelsPropertyFromComplex_SelectsExpectedProperties(string select) + if (complexesToSelect == null) { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null); - - // Act: First Level - SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); - - // Assert - Assert.Null(selectExpandNode.SelectedStructuralProperties); - Assert.NotNull(selectExpandNode.SelectedComplexProperties); - var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); - Assert.Equal("Account", firstLevelSelected.Key.Name); - Assert.NotNull(firstLevelSelected.Value); - Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); - - // Act: Second Level - SelectExpandNode secondSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, _model.Account, _model.Model); - - // Assert - Assert.Null(secondSelectExpandNode.SelectedStructuralProperties); - Assert.NotNull(secondSelectExpandNode.SelectedComplexProperties); - var secondLevelSelected = Assert.Single(secondSelectExpandNode.SelectedComplexProperties); - Assert.Equal("BankAddress", secondLevelSelected.Key.Name); - Assert.NotNull(secondLevelSelected.Value); - Assert.NotNull(secondLevelSelected.Value.SelectAndExpand); - - // Act: Third Level - SelectExpandNode thirdSelectExpandNode = new SelectExpandNode(secondLevelSelected.Value.SelectAndExpand, _model.Address, _model.Model); - - // Assert - Assert.Null(thirdSelectExpandNode.SelectedComplexProperties); - Assert.NotNull(thirdSelectExpandNode.SelectedStructuralProperties); - Assert.Equal(3, thirdSelectExpandNode.SelectedStructuralProperties.Count); - Assert.Equal(new[] { "Street", "City", "ZipCode" }, thirdSelectExpandNode.SelectedStructuralProperties.Select(s => s.Name)); + Assert.Null(selectExpandNode.SelectedComplexProperties); } - - [Theory] - [InlineData("Account/DynamicProperty", "Account")] - [InlineData("Account($select=DynamicProperty)", "Account")] - [InlineData("OtherAccounts/DynamicProperty", "OtherAccounts")] - [InlineData("OtherAccounts($select=DynamicProperty)", "OtherAccounts")] - public void SelectProperties_OnSubDynamicFromComplex_SelectsExpectedProperties(string select, string firstSelected) + else { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null); - - // Act - SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); - - // Assert: Top Level - Assert.False(selectExpandNode.SelectAllDynamicProperties); - Assert.Null(selectExpandNode.SelectedDynamicProperties); - Assert.Null(selectExpandNode.SelectedStructuralProperties); - Assert.Null(selectExpandNode.SelectedNavigationProperties); - Assert.NotNull(selectExpandNode.SelectedComplexProperties); - var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); - Assert.Equal(firstSelected, firstLevelSelected.Key.Name); - - Assert.NotNull(firstLevelSelected.Value); - Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); - - // Assert: Second Level - SelectExpandNode subSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, _model.Account, _model.Model); - Assert.Null(subSelectExpandNode.SelectedComplexProperties); - Assert.Null(subSelectExpandNode.SelectedStructuralProperties); - Assert.Null(subSelectExpandNode.SelectedNavigationProperties); - Assert.False(subSelectExpandNode.SelectAllDynamicProperties); - Assert.NotNull(subSelectExpandNode.SelectedDynamicProperties); - Assert.Equal("DynamicProperty", Assert.Single(subSelectExpandNode.SelectedDynamicProperties)); + Assert.Equal(complexesToSelect, string.Join(",", selectExpandNode.SelectedComplexProperties.Keys.Select(p => p.Name).OrderBy(n => n))); } + } - [Theory] - [InlineData("Account/AccountOrderNav", "Account")] - [InlineData("Account($select=AccountOrderNav)", "Account")] - [InlineData("OtherAccounts/AccountOrderNav", "OtherAccounts")] - [InlineData("OtherAccounts($select=AccountOrderNav)", "OtherAccounts")] - public void SelectProperties_OnSubNavigationPropertyFromComplex_SelectsExpectedProperties(string select, string firstSelected) - { - // Arrange - _model.Account.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "AccountOrderNav", - TargetMultiplicity = EdmMultiplicity.One, - Target = _model.Order - }); + [Theory] + [InlineData("ID,Name,Orders", "Orders", false, "Amount,City,ID")] // expand and select -> select all + [InlineData("ID,Name,Orders", "Orders", true, "Amount,City,ID,SpecialOrderProperty")] // expand and select on derived type -> select all + [InlineData("ID,Name,Orders", "Orders($select=ID)", false, "ID")] // expand and select properties on expand -> select requested + [InlineData("ID,Name,Orders", "Orders($select=ID)", true, "ID")] // expand and select properties on expand on derived type -> select requested + [InlineData("Orders", "Orders,Orders($expand=Customer)", false, "Amount,City,ID")] + [InlineData("Orders", "Orders,Orders($expand=Customer)", true, "Amount,City,ID,SpecialOrderProperty")] + public void SelectProperties_Selects_ExpectedProperties_OnExpandedOrders(string select, string expand, bool specialOrder, string structuralPropertiesToSelect) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand); + SelectExpandClause nestedSelectExpandClause = selectExpandClause.SelectedItems.OfType().Single().SelectAndExpand; + IEdmStructuredType structuralType = specialOrder ? _model.SpecialOrder : _model.Order; - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null); + // Act + SelectExpandNode selectExpandNode = new SelectExpandNode(nestedSelectExpandClause, structuralType, _model.Model); - // Act - SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); + // Assert + Assert.Equal(structuralPropertiesToSelect, String.Join(",", selectExpandNode.SelectedStructuralProperties.Select(p => p.Name).OrderBy(n => n))); + } - // Assert: Top Level - Assert.Null(selectExpandNode.SelectedStructuralProperties); - Assert.Null(selectExpandNode.SelectedNavigationProperties); - Assert.NotNull(selectExpandNode.SelectedComplexProperties); - var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); - Assert.Equal(firstSelected, firstLevelSelected.Key.Name); + [Theory] + [InlineData("Address/Street,Address/City,Address/ZipCode", "Address", "Street,City,ZipCode")] // complex + [InlineData("Address($select=Street,City,ZipCode)", "Address", "Street,City,ZipCode")] + [InlineData("OtherAccounts/Bank,OtherAccounts/CardNum", "OtherAccounts", "Bank,CardNum")] // Collection complex + [InlineData("OtherAccounts($select=Bank,CardNum)", "OtherAccounts", "Bank,CardNum")] + public void SelectProperties_OnSubPrimitivePropertyFromComplex_SelectsExpectedProperties(string select, string firstSelected, string secondSelected) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null); + + // Act: Top Level + SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); + + // Assert + Assert.NotNull(selectExpandNode.SelectedComplexProperties); + var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); + Assert.Equal(firstSelected, firstLevelSelected.Key.Name); + + Assert.NotNull(firstLevelSelected.Value); + Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); + + // Act: Sub Level + IEdmStructuredType subLevelElementType = firstLevelSelected.Key.Type.ToStructuredType(); + SelectExpandNode subSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, subLevelElementType, _model.Model); + Assert.Null(subSelectExpandNode.SelectedComplexProperties); + + // Assert + Assert.NotNull(subSelectExpandNode.SelectedStructuralProperties); + var selectedProperties = secondSelected.Split(','); + Assert.Equal(selectedProperties.Length, subSelectExpandNode.SelectedStructuralProperties.Count); + Assert.Equal(secondSelected, String.Join(",", subSelectExpandNode.SelectedStructuralProperties.Select(s => s.Name))); + } - Assert.NotNull(firstLevelSelected.Value); - Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); + [Theory] + [InlineData("Account/BankAddress/Street,Account/BankAddress/City,Account/BankAddress/ZipCode")] + [InlineData("Account/BankAddress($select=Street,City,ZipCode)")] + public void SelectProperties_OnMultipleLevelsPropertyFromComplex_SelectsExpectedProperties(string select) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null); + + // Act: First Level + SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); + + // Assert + Assert.Null(selectExpandNode.SelectedStructuralProperties); + Assert.NotNull(selectExpandNode.SelectedComplexProperties); + var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); + Assert.Equal("Account", firstLevelSelected.Key.Name); + Assert.NotNull(firstLevelSelected.Value); + Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); + + // Act: Second Level + SelectExpandNode secondSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, _model.Account, _model.Model); + + // Assert + Assert.Null(secondSelectExpandNode.SelectedStructuralProperties); + Assert.NotNull(secondSelectExpandNode.SelectedComplexProperties); + var secondLevelSelected = Assert.Single(secondSelectExpandNode.SelectedComplexProperties); + Assert.Equal("BankAddress", secondLevelSelected.Key.Name); + Assert.NotNull(secondLevelSelected.Value); + Assert.NotNull(secondLevelSelected.Value.SelectAndExpand); + + // Act: Third Level + SelectExpandNode thirdSelectExpandNode = new SelectExpandNode(secondLevelSelected.Value.SelectAndExpand, _model.Address, _model.Model); + + // Assert + Assert.Null(thirdSelectExpandNode.SelectedComplexProperties); + Assert.NotNull(thirdSelectExpandNode.SelectedStructuralProperties); + Assert.Equal(3, thirdSelectExpandNode.SelectedStructuralProperties.Count); + Assert.Equal(new[] { "Street", "City", "ZipCode" }, thirdSelectExpandNode.SelectedStructuralProperties.Select(s => s.Name)); + } - // Assert: Second Level - SelectExpandNode subSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, _model.Account, _model.Model); - Assert.Null(subSelectExpandNode.SelectedComplexProperties); - Assert.Null(subSelectExpandNode.SelectedStructuralProperties); - Assert.NotNull(subSelectExpandNode.SelectedNavigationProperties); - Assert.Equal("AccountOrderNav", Assert.Single(subSelectExpandNode.SelectedNavigationProperties).Name); - } + [Theory] + [InlineData("Account/DynamicProperty", "Account")] + [InlineData("Account($select=DynamicProperty)", "Account")] + [InlineData("OtherAccounts/DynamicProperty", "OtherAccounts")] + [InlineData("OtherAccounts($select=DynamicProperty)", "OtherAccounts")] + public void SelectProperties_OnSubDynamicFromComplex_SelectsExpectedProperties(string select, string firstSelected) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null); + + // Act + SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); + + // Assert: Top Level + Assert.False(selectExpandNode.SelectAllDynamicProperties); + Assert.Null(selectExpandNode.SelectedDynamicProperties); + Assert.Null(selectExpandNode.SelectedStructuralProperties); + Assert.Null(selectExpandNode.SelectedNavigationProperties); + Assert.NotNull(selectExpandNode.SelectedComplexProperties); + var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); + Assert.Equal(firstSelected, firstLevelSelected.Key.Name); + + Assert.NotNull(firstLevelSelected.Value); + Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); + + // Assert: Second Level + SelectExpandNode subSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, _model.Account, _model.Model); + Assert.Null(subSelectExpandNode.SelectedComplexProperties); + Assert.Null(subSelectExpandNode.SelectedStructuralProperties); + Assert.Null(subSelectExpandNode.SelectedNavigationProperties); + Assert.False(subSelectExpandNode.SelectAllDynamicProperties); + Assert.NotNull(subSelectExpandNode.SelectedDynamicProperties); + Assert.Equal("DynamicProperty", Assert.Single(subSelectExpandNode.SelectedDynamicProperties)); + } - [Fact] - public void SelectProperties_OnMultipleSubPropertiesFromComplex_SelectsExpectedProperties() + [Theory] + [InlineData("Account/AccountOrderNav", "Account")] + [InlineData("Account($select=AccountOrderNav)", "Account")] + [InlineData("OtherAccounts/AccountOrderNav", "OtherAccounts")] + [InlineData("OtherAccounts($select=AccountOrderNav)", "OtherAccounts")] + public void SelectProperties_OnSubNavigationPropertyFromComplex_SelectsExpectedProperties(string select, string firstSelected) + { + // Arrange + _model.Account.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - // Arrange - string select = "Account,Account/Bank,Account/BankAddress/Street"; + Name = "AccountOrderNav", + TargetMultiplicity = EdmMultiplicity.One, + Target = _model.Order + }); + + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null); + + // Act + SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); + + // Assert: Top Level + Assert.Null(selectExpandNode.SelectedStructuralProperties); + Assert.Null(selectExpandNode.SelectedNavigationProperties); + Assert.NotNull(selectExpandNode.SelectedComplexProperties); + var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); + Assert.Equal(firstSelected, firstLevelSelected.Key.Name); + + Assert.NotNull(firstLevelSelected.Value); + Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); + + // Assert: Second Level + SelectExpandNode subSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, _model.Account, _model.Model); + Assert.Null(subSelectExpandNode.SelectedComplexProperties); + Assert.Null(subSelectExpandNode.SelectedStructuralProperties); + Assert.NotNull(subSelectExpandNode.SelectedNavigationProperties); + Assert.Equal("AccountOrderNav", Assert.Single(subSelectExpandNode.SelectedNavigationProperties).Name); + } - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null); + [Fact] + public void SelectProperties_OnMultipleSubPropertiesFromComplex_SelectsExpectedProperties() + { + // Arrange + string select = "Account,Account/Bank,Account/BankAddress/Street"; + + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null); + + // Act + SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); + + // Assert: Top Level + Assert.Null(selectExpandNode.SelectedStructuralProperties); + Assert.Null(selectExpandNode.SelectedNavigationProperties); + Assert.NotNull(selectExpandNode.SelectedComplexProperties); + var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); + Assert.Equal("Account", firstLevelSelected.Key.Name); + + Assert.NotNull(firstLevelSelected.Value); + Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); + Assert.True(firstLevelSelected.Value.SelectAndExpand.AllSelected); + + // Assert: Second Level + SelectExpandNode secondSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, _model.Account, _model.Model); + Assert.NotNull(secondSelectExpandNode.SelectedStructuralProperties); + Assert.Equal(2, secondSelectExpandNode.SelectedStructuralProperties.Count); // Because it's select all from first select item. + Assert.Equal(new[] { "Bank", "CardNum" }, secondSelectExpandNode.SelectedStructuralProperties.Select(s => s.Name)); + + Assert.Null(secondSelectExpandNode.SelectedNavigationProperties); + + Assert.NotNull(secondSelectExpandNode.SelectedComplexProperties); + var secondLevelSelected = Assert.Single(secondSelectExpandNode.SelectedComplexProperties); + Assert.Equal("BankAddress", secondLevelSelected.Key.Name); + Assert.NotNull(secondLevelSelected.Value); + Assert.NotNull(secondLevelSelected.Value.SelectAndExpand); + Assert.False(secondLevelSelected.Value.SelectAndExpand.AllSelected); + + // Assert: Third level + SelectExpandNode thirdSelectExpandNode = new SelectExpandNode(secondLevelSelected.Value.SelectAndExpand, _model.Address, _model.Model); + Assert.NotNull(thirdSelectExpandNode.SelectedStructuralProperties); + Assert.Equal("Street", Assert.Single(thirdSelectExpandNode.SelectedStructuralProperties).Name); + Assert.Null(thirdSelectExpandNode.SelectedNavigationProperties); + Assert.Null(thirdSelectExpandNode.SelectedComplexProperties); + } - // Act - SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); + [Theory] + [InlineData("$select=Account/Bank&$expand=Account/AccountOrderNav", "Account")] + [InlineData("$select=Account($select=Bank)&$expand=Account/AccountOrderNav", "Account")] + [InlineData("$select=OtherAccounts/Bank&$expand=OtherAccounts/AccountOrderNav", "OtherAccounts")] + [InlineData("$select=OtherAccounts($select=Bank)&$expand=OtherAccounts/AccountOrderNav", "OtherAccounts")] + public void SelectProperties_OnSubSelectAndExpandFromComplex_SelectsExpectedProperties(string selectExpand, string firstSelected) + { + // Arrange + string[] items = selectExpand.Split('&'); + Assert.Equal(2, items.Length); - // Assert: Top Level - Assert.Null(selectExpandNode.SelectedStructuralProperties); - Assert.Null(selectExpandNode.SelectedNavigationProperties); - Assert.NotNull(selectExpandNode.SelectedComplexProperties); - var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); - Assert.Equal("Account", firstLevelSelected.Key.Name); - - Assert.NotNull(firstLevelSelected.Value); - Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); - Assert.True(firstLevelSelected.Value.SelectAndExpand.AllSelected); - - // Assert: Second Level - SelectExpandNode secondSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, _model.Account, _model.Model); - Assert.NotNull(secondSelectExpandNode.SelectedStructuralProperties); - Assert.Equal(2, secondSelectExpandNode.SelectedStructuralProperties.Count); // Because it's select all from first select item. - Assert.Equal(new[] { "Bank", "CardNum" }, secondSelectExpandNode.SelectedStructuralProperties.Select(s => s.Name)); - - Assert.Null(secondSelectExpandNode.SelectedNavigationProperties); - - Assert.NotNull(secondSelectExpandNode.SelectedComplexProperties); - var secondLevelSelected = Assert.Single(secondSelectExpandNode.SelectedComplexProperties); - Assert.Equal("BankAddress", secondLevelSelected.Key.Name); - Assert.NotNull(secondLevelSelected.Value); - Assert.NotNull(secondLevelSelected.Value.SelectAndExpand); - Assert.False(secondLevelSelected.Value.SelectAndExpand.AllSelected); - - // Assert: Third level - SelectExpandNode thirdSelectExpandNode = new SelectExpandNode(secondLevelSelected.Value.SelectAndExpand, _model.Address, _model.Model); - Assert.NotNull(thirdSelectExpandNode.SelectedStructuralProperties); - Assert.Equal("Street", Assert.Single(thirdSelectExpandNode.SelectedStructuralProperties).Name); - Assert.Null(thirdSelectExpandNode.SelectedNavigationProperties); - Assert.Null(thirdSelectExpandNode.SelectedComplexProperties); - } + string select = items[0].Substring(8); + string expand = items[1].Substring(8); - [Theory] - [InlineData("$select=Account/Bank&$expand=Account/AccountOrderNav", "Account")] - [InlineData("$select=Account($select=Bank)&$expand=Account/AccountOrderNav", "Account")] - [InlineData("$select=OtherAccounts/Bank&$expand=OtherAccounts/AccountOrderNav", "OtherAccounts")] - [InlineData("$select=OtherAccounts($select=Bank)&$expand=OtherAccounts/AccountOrderNav", "OtherAccounts")] - public void SelectProperties_OnSubSelectAndExpandFromComplex_SelectsExpectedProperties(string selectExpand, string firstSelected) + _model.Account.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - // Arrange - string[] items = selectExpand.Split('&'); - Assert.Equal(2, items.Length); + Name = "AccountOrderNav", + TargetMultiplicity = EdmMultiplicity.One, + Target = _model.Order + }); + + SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand); + + // Act + SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); + + // Assert: Top Level + Assert.Null(selectExpandNode.SelectedStructuralProperties); + Assert.Null(selectExpandNode.SelectedNavigationProperties); + Assert.Null(selectExpandNode.ExpandedProperties); // Not expanded at first level + + Assert.NotNull(selectExpandNode.SelectedComplexProperties); + var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); + Assert.Equal(firstSelected, firstLevelSelected.Key.Name); + + Assert.NotNull(firstLevelSelected.Value); + Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); + + // Assert: Second Level + SelectExpandNode subSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, _model.Account, _model.Model); + Assert.Null(subSelectExpandNode.SelectedComplexProperties); + Assert.NotNull(subSelectExpandNode.SelectedStructuralProperties); + Assert.Equal("Bank", Assert.Single(subSelectExpandNode.SelectedStructuralProperties).Name); + + Assert.NotNull(subSelectExpandNode.ExpandedProperties); + var expandedProperty = Assert.Single(subSelectExpandNode.ExpandedProperties); + Assert.Equal("AccountOrderNav", expandedProperty.Key.Name); + + Assert.NotNull(expandedProperty.Value); + Assert.NotNull(expandedProperty.Value.SelectAndExpand); + Assert.True(expandedProperty.Value.SelectAndExpand.AllSelected); + Assert.Empty(expandedProperty.Value.SelectAndExpand.SelectedItems); + } - string select = items[0].Substring(8); - string expand = items[1].Substring(8); + [Fact] + public void SelectProperties_OnSubPropertyWithTypeCastFromComplex_SelectsExpectedProperties() + { + // Arrange + EdmComplexType subComplexType = new EdmComplexType("NS", "CnAddress", _model.Address); + subComplexType.AddStructuralProperty("SubAddressProperty", EdmPrimitiveTypeKind.String); + subComplexType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo + { + Name = "CnAddressOrderNav", + TargetMultiplicity = EdmMultiplicity.One, + Target = _model.Order + }); + _model.Model.AddElement(subComplexType); - _model.Account.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "AccountOrderNav", - TargetMultiplicity = EdmMultiplicity.One, - Target = _model.Order - }); + string select = "Address/NS.CnAddress/SubAddressProperty"; + string expand = "Address/NS.CnAddress/CnAddressOrderNav"; - SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand); + SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand); - // Act - SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); + // Act + SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); - // Assert: Top Level - Assert.Null(selectExpandNode.SelectedStructuralProperties); - Assert.Null(selectExpandNode.SelectedNavigationProperties); - Assert.Null(selectExpandNode.ExpandedProperties); // Not expanded at first level + // Assert: Top Level + Assert.Null(selectExpandNode.SelectedStructuralProperties); + Assert.Null(selectExpandNode.SelectedNavigationProperties); + Assert.Null(selectExpandNode.ExpandedProperties); // Not expanded at first level - Assert.NotNull(selectExpandNode.SelectedComplexProperties); - var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); - Assert.Equal(firstSelected, firstLevelSelected.Key.Name); + Assert.NotNull(selectExpandNode.SelectedComplexProperties); + var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); + Assert.Equal("Address", firstLevelSelected.Key.Name); - Assert.NotNull(firstLevelSelected.Value); - Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); + Assert.NotNull(firstLevelSelected.Value); + Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); - // Assert: Second Level - SelectExpandNode subSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, _model.Account, _model.Model); + // Assert: Second Level + { + // use the base type to test + SelectExpandNode subSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, _model.Address, _model.Model); + Assert.Null(subSelectExpandNode.SelectedStructuralProperties); + Assert.Null(subSelectExpandNode.SelectedComplexProperties); + Assert.Null(subSelectExpandNode.ExpandedProperties); + Assert.Null(subSelectExpandNode.SelectedNavigationProperties); + } + { + // use the sub type to test + SelectExpandNode subSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, subComplexType, _model.Model); Assert.Null(subSelectExpandNode.SelectedComplexProperties); Assert.NotNull(subSelectExpandNode.SelectedStructuralProperties); - Assert.Equal("Bank", Assert.Single(subSelectExpandNode.SelectedStructuralProperties).Name); + Assert.Equal("SubAddressProperty", Assert.Single(subSelectExpandNode.SelectedStructuralProperties).Name); Assert.NotNull(subSelectExpandNode.ExpandedProperties); var expandedProperty = Assert.Single(subSelectExpandNode.ExpandedProperties); - Assert.Equal("AccountOrderNav", expandedProperty.Key.Name); + Assert.Equal("CnAddressOrderNav", expandedProperty.Key.Name); Assert.NotNull(expandedProperty.Value); Assert.NotNull(expandedProperty.Value.SelectAndExpand); Assert.True(expandedProperty.Value.SelectAndExpand.AllSelected); Assert.Empty(expandedProperty.Value.SelectAndExpand.SelectedItems); } + } - [Fact] - public void SelectProperties_OnSubPropertyWithTypeCastFromComplex_SelectsExpectedProperties() - { - // Arrange - EdmComplexType subComplexType = new EdmComplexType("NS", "CnAddress", _model.Address); - subComplexType.AddStructuralProperty("SubAddressProperty", EdmPrimitiveTypeKind.String); - subComplexType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "CnAddressOrderNav", - TargetMultiplicity = EdmMultiplicity.One, - Target = _model.Order - }); - _model.Model.AddElement(subComplexType); - - string select = "Address/NS.CnAddress/SubAddressProperty"; - string expand = "Address/NS.CnAddress/CnAddressOrderNav"; - - SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand); + [Theory] + [InlineData(null, null, false, "Orders")] // no select and expand -> select all + [InlineData(null, null, true, "Orders,SpecialOrders")] // no select and expand on derived type -> select all + [InlineData("ID", null, false, null)] // simple select -> select none + [InlineData("ID", null, true, null)] // simple select on derived type -> select none + [InlineData(null, "Orders", false, null)] // simple expand -> select non expanded + [InlineData(null, "Orders", true, "SpecialOrders")] // simple expand on derived type -> select non expanded + [InlineData("ID", "Orders", false, null)] // simple expand without corresponding select -> select none + [InlineData("ID", "Orders", true, null)] // simple expand without corresponding select on derived type -> select none + [InlineData("ID,Orders", "Orders", false, null)] // simple expand with corresponding select -> select none + [InlineData("ID,Orders", "Orders", true, null)] // simple expand with corresponding select on derived type -> select none + [InlineData("ID,Orders", null, false, "Orders")] // simple select without corresponding expand -> select requested + [InlineData("ID,Orders", null, true, "Orders")] // simple select with corresponding expand on derived type -> select requested + [InlineData("NS.SpecialCustomer/SpecialOrders", null, false, null)] // select derived type properties -> select none + [InlineData("NS.SpecialCustomer/SpecialOrders", null, true, "SpecialOrders")] // select derived type properties on derived type -> select requested + public void SelectNavigationProperties_SelectsExpectedProperties(string select, string expand, bool specialCustomer, string propertiesToSelect) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand); + IEdmStructuredType structuralType = specialCustomer ? _model.SpecialCustomer : _model.Customer; - // Act - SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, _model.Customer, _model.Model); + // Act + SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, structuralType, _model.Model); - // Assert: Top Level - Assert.Null(selectExpandNode.SelectedStructuralProperties); + // Assert + if (propertiesToSelect == null) + { Assert.Null(selectExpandNode.SelectedNavigationProperties); - Assert.Null(selectExpandNode.ExpandedProperties); // Not expanded at first level + } + else + { + Assert.Equal(propertiesToSelect, String.Join(",", selectExpandNode.SelectedNavigationProperties.Select(p => p.Name))); + } + } - Assert.NotNull(selectExpandNode.SelectedComplexProperties); - var firstLevelSelected = Assert.Single(selectExpandNode.SelectedComplexProperties); - Assert.Equal("Address", firstLevelSelected.Key.Name); + [Theory] + [InlineData(null, null, false, null)] // no select and expand -> expand none + [InlineData(null, null, true, null)] // no select and expand on derived type -> expand none + [InlineData("Orders", null, false, null)] // simple select and no expand -> expand none + [InlineData("Orders", null, true, null)] // simple select and no expand on derived type -> expand none + [InlineData(null, "Orders", false, "Orders")] // simple expand and no select -> expand requested + [InlineData(null, "Orders", true, "Orders")] // simple expand and no select on derived type -> expand requested + [InlineData(null, "Orders,Orders,Orders", false, "Orders")] // duplicate expand -> expand requested + [InlineData(null, "Orders,Orders,Orders", true, "Orders")] // duplicate expand on derived type -> expand requested + [InlineData("ID", "Orders", false, "Orders")] // Expanded navigation properties MUST be returned, even if they are not specified as a selectItem. + [InlineData("ID", "Orders", true, "Orders")] // Expanded navigation properties MUST be returned, even if they are not specified as a selectItem. + [InlineData("Orders", "Orders", false, "Orders")] // only expand -> expand requested + [InlineData("ID,Orders", "Orders", false, "Orders")] // simple expand and expand in select -> expand requested + [InlineData("ID,Orders", "Orders", true, "Orders")] // simple expand and expand in select on derived type -> expand requested + [InlineData(null, "NS.SpecialCustomer/SpecialOrders", false, null)] // expand derived navigation property -> expand requested + [InlineData(null, "NS.SpecialCustomer/SpecialOrders", true, "SpecialOrders")] // expand derived navigation property on derived type -> expand requested + public void ExpandNavigationProperties_ExpandsExpectedProperties(string select, string expand, bool specialCustomer, string propertiesToExpand) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand); + IEdmStructuredType structuralType = specialCustomer ? _model.SpecialCustomer : _model.Customer; - Assert.NotNull(firstLevelSelected.Value); - Assert.NotNull(firstLevelSelected.Value.SelectAndExpand); + // Act + SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, structuralType, _model.Model); - // Assert: Second Level - { - // use the base type to test - SelectExpandNode subSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, _model.Address, _model.Model); - Assert.Null(subSelectExpandNode.SelectedStructuralProperties); - Assert.Null(subSelectExpandNode.SelectedComplexProperties); - Assert.Null(subSelectExpandNode.ExpandedProperties); - Assert.Null(subSelectExpandNode.SelectedNavigationProperties); - } - { - // use the sub type to test - SelectExpandNode subSelectExpandNode = new SelectExpandNode(firstLevelSelected.Value.SelectAndExpand, subComplexType, _model.Model); - Assert.Null(subSelectExpandNode.SelectedComplexProperties); - Assert.NotNull(subSelectExpandNode.SelectedStructuralProperties); - Assert.Equal("SubAddressProperty", Assert.Single(subSelectExpandNode.SelectedStructuralProperties).Name); - - Assert.NotNull(subSelectExpandNode.ExpandedProperties); - var expandedProperty = Assert.Single(subSelectExpandNode.ExpandedProperties); - Assert.Equal("CnAddressOrderNav", expandedProperty.Key.Name); - - Assert.NotNull(expandedProperty.Value); - Assert.NotNull(expandedProperty.Value.SelectAndExpand); - Assert.True(expandedProperty.Value.SelectAndExpand.AllSelected); - Assert.Empty(expandedProperty.Value.SelectAndExpand.SelectedItems); - } + // Assert + if (propertiesToExpand == null) + { + Assert.Null(selectExpandNode.ExpandedProperties); } - - [Theory] - [InlineData(null, null, false, "Orders")] // no select and expand -> select all - [InlineData(null, null, true, "Orders,SpecialOrders")] // no select and expand on derived type -> select all - [InlineData("ID", null, false, null)] // simple select -> select none - [InlineData("ID", null, true, null)] // simple select on derived type -> select none - [InlineData(null, "Orders", false, null)] // simple expand -> select non expanded - [InlineData(null, "Orders", true, "SpecialOrders")] // simple expand on derived type -> select non expanded - [InlineData("ID", "Orders", false, null)] // simple expand without corresponding select -> select none - [InlineData("ID", "Orders", true, null)] // simple expand without corresponding select on derived type -> select none - [InlineData("ID,Orders", "Orders", false, null)] // simple expand with corresponding select -> select none - [InlineData("ID,Orders", "Orders", true, null)] // simple expand with corresponding select on derived type -> select none - [InlineData("ID,Orders", null, false, "Orders")] // simple select without corresponding expand -> select requested - [InlineData("ID,Orders", null, true, "Orders")] // simple select with corresponding expand on derived type -> select requested - [InlineData("NS.SpecialCustomer/SpecialOrders", null, false, null)] // select derived type properties -> select none - [InlineData("NS.SpecialCustomer/SpecialOrders", null, true, "SpecialOrders")] // select derived type properties on derived type -> select requested - public void SelectNavigationProperties_SelectsExpectedProperties(string select, string expand, bool specialCustomer, string propertiesToSelect) + else { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand); - IEdmStructuredType structuralType = specialCustomer ? _model.SpecialCustomer : _model.Customer; - - // Act - SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, structuralType, _model.Model); - - // Assert - if (propertiesToSelect == null) - { - Assert.Null(selectExpandNode.SelectedNavigationProperties); - } - else - { - Assert.Equal(propertiesToSelect, String.Join(",", selectExpandNode.SelectedNavigationProperties.Select(p => p.Name))); - } + Assert.Equal(propertiesToExpand, String.Join(",", selectExpandNode.ExpandedProperties.Select(p => p.Key.Name))); } + } - [Theory] - [InlineData(null, null, false, null)] // no select and expand -> expand none - [InlineData(null, null, true, null)] // no select and expand on derived type -> expand none - [InlineData("Orders", null, false, null)] // simple select and no expand -> expand none - [InlineData("Orders", null, true, null)] // simple select and no expand on derived type -> expand none - [InlineData(null, "Orders", false, "Orders")] // simple expand and no select -> expand requested - [InlineData(null, "Orders", true, "Orders")] // simple expand and no select on derived type -> expand requested - [InlineData(null, "Orders,Orders,Orders", false, "Orders")] // duplicate expand -> expand requested - [InlineData(null, "Orders,Orders,Orders", true, "Orders")] // duplicate expand on derived type -> expand requested - [InlineData("ID", "Orders", false, "Orders")] // Expanded navigation properties MUST be returned, even if they are not specified as a selectItem. - [InlineData("ID", "Orders", true, "Orders")] // Expanded navigation properties MUST be returned, even if they are not specified as a selectItem. - [InlineData("Orders", "Orders", false, "Orders")] // only expand -> expand requested - [InlineData("ID,Orders", "Orders", false, "Orders")] // simple expand and expand in select -> expand requested - [InlineData("ID,Orders", "Orders", true, "Orders")] // simple expand and expand in select on derived type -> expand requested - [InlineData(null, "NS.SpecialCustomer/SpecialOrders", false, null)] // expand derived navigation property -> expand requested - [InlineData(null, "NS.SpecialCustomer/SpecialOrders", true, "SpecialOrders")] // expand derived navigation property on derived type -> expand requested - public void ExpandNavigationProperties_ExpandsExpectedProperties(string select, string expand, bool specialCustomer, string propertiesToExpand) - { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand); - IEdmStructuredType structuralType = specialCustomer ? _model.SpecialCustomer : _model.Customer; + [Theory] + [InlineData(null, false, 1, 8)] // no select and no expand means to select all operations + [InlineData(null, true, 2, 10)] + [InlineData("*", false, null, null)] // select * means to select no operations + [InlineData("*", true, null, null)] + [InlineData("NS.*", false, 1, 8)] // select wild card actions means to select all starting with "NS" + [InlineData("NS.*", true, 2, 10)] + [InlineData("NS.upgrade", false, 1, null)] // select single action -> select requested action + [InlineData("NS.upgrade", true, 1, null)] + [InlineData("NS.SpecialCustomer/NS.specialUpgrade", false, null, null)] // select single derived action on base type -> select nothing + [InlineData("NS.SpecialCustomer/NS.specialUpgrade", true, 1, null)] // select single derived action on derived type -> select requested action + [InlineData("NS.GetSalary", false, null, 1)] // select single function -> select requested function + [InlineData("NS.GetSalary", true, null, 1)] + [InlineData("NS.SpecialCustomer/NS.IsSpecialUpgraded", false, null, null)] // select single derived function on base type -> select nothing + [InlineData("NS.SpecialCustomer/NS.IsSpecialUpgraded", true, null, 1)] // select single derived function on derived type -> select requested function + public void OperationsToBeSelected_Selects_ExpectedOperations(string select, bool specialCustomer, int? actionsSelected, int? functionsSelected) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand: null); + IEdmStructuredType structuralType = specialCustomer ? _model.SpecialCustomer : _model.Customer; - // Act - SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, structuralType, _model.Model); + // Act + SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, structuralType, _model.Model); - // Assert - if (propertiesToExpand == null) - { - Assert.Null(selectExpandNode.ExpandedProperties); - } - else - { - Assert.Equal(propertiesToExpand, String.Join(",", selectExpandNode.ExpandedProperties.Select(p => p.Key.Name))); - } + // Assert: Actions + if (actionsSelected == null) + { + Assert.Null(selectExpandNode.SelectedActions); + } + else + { + Assert.Equal(actionsSelected, selectExpandNode.SelectedActions.Count); } - [Theory] - [InlineData(null, false, 1, 8)] // no select and no expand means to select all operations - [InlineData(null, true, 2, 10)] - [InlineData("*", false, null, null)] // select * means to select no operations - [InlineData("*", true, null, null)] - [InlineData("NS.*", false, 1, 8)] // select wild card actions means to select all starting with "NS" - [InlineData("NS.*", true, 2, 10)] - [InlineData("NS.upgrade", false, 1, null)] // select single action -> select requested action - [InlineData("NS.upgrade", true, 1, null)] - [InlineData("NS.SpecialCustomer/NS.specialUpgrade", false, null, null)] // select single derived action on base type -> select nothing - [InlineData("NS.SpecialCustomer/NS.specialUpgrade", true, 1, null)] // select single derived action on derived type -> select requested action - [InlineData("NS.GetSalary", false, null, 1)] // select single function -> select requested function - [InlineData("NS.GetSalary", true, null, 1)] - [InlineData("NS.SpecialCustomer/NS.IsSpecialUpgraded", false, null, null)] // select single derived function on base type -> select nothing - [InlineData("NS.SpecialCustomer/NS.IsSpecialUpgraded", true, null, 1)] // select single derived function on derived type -> select requested function - public void OperationsToBeSelected_Selects_ExpectedOperations(string select, bool specialCustomer, int? actionsSelected, int? functionsSelected) + // Assert: Functions + if (functionsSelected == null) + { + Assert.Null(selectExpandNode.SelectedFunctions); + } + else { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand: null); - IEdmStructuredType structuralType = specialCustomer ? _model.SpecialCustomer : _model.Customer; + Assert.Equal(functionsSelected, selectExpandNode.SelectedFunctions.Count); + } + } - // Act - SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, structuralType, _model.Model); + [Fact] + public void BuildSelectExpandNode_ThrowsODataException_IfUnknownSelectItemPresent() + { + // Arrange + SelectExpandClause selectExpandClause = new SelectExpandClause(new SelectItem[] { new Mock().Object }, allSelected: false); + IEdmStructuredType structuralType = _model.Customer; - // Assert: Actions - if (actionsSelected == null) - { - Assert.Null(selectExpandNode.SelectedActions); - } - else - { - Assert.Equal(actionsSelected, selectExpandNode.SelectedActions.Count); - } + // Act & Assert + ExceptionAssert.Throws(() => new SelectExpandNode(selectExpandClause, structuralType, _model.Model), + "$select does not support selections of type 'SelectItemProxy'."); + } - // Assert: Functions - if (functionsSelected == null) - { - Assert.Null(selectExpandNode.SelectedFunctions); - } - else + [Fact] + public void BuildSelectExpandNode_Works_IfOnlyNavigationPropertyDefinedOnType() + { + // Assert + EdmModel model = new EdmModel(); + EdmEntityType entity = new EdmEntityType("NS", "Entity"); + entity.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo + { + Name = "Nav", + Target = entity, + TargetMultiplicity = EdmMultiplicity.One + }); + model.AddElement(entity); + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + EdmSingleton singleton = container.AddSingleton("Single", entity); + container.AddElement(singleton); + model.AddElement(container); + + // Act + SelectExpandClause selectExpandClause = new ODataQueryOptionParser(model, entity, singleton, + new Dictionary { - Assert.Equal(functionsSelected, selectExpandNode.SelectedFunctions.Count); - } - } + { "$select", "Nav" } + }).ParseSelectAndExpand(); - [Fact] - public void BuildSelectExpandNode_ThrowsODataException_IfUnknownSelectItemPresent() - { - // Arrange - SelectExpandClause selectExpandClause = new SelectExpandClause(new SelectItem[] { new Mock().Object }, allSelected: false); - IEdmStructuredType structuralType = _model.Customer; + SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, entity, model); - // Act & Assert - ExceptionAssert.Throws(() => new SelectExpandNode(selectExpandClause, structuralType, _model.Model), - "$select does not support selections of type 'SelectItemProxy'."); - } + // Assert + Assert.Null(selectExpandNode.SelectedStructuralProperties); + Assert.Null(selectExpandNode.SelectedComplexProperties); - [Fact] - public void BuildSelectExpandNode_Works_IfOnlyNavigationPropertyDefinedOnType() - { - // Assert - EdmModel model = new EdmModel(); - EdmEntityType entity = new EdmEntityType("NS", "Entity"); - entity.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "Nav", - Target = entity, - TargetMultiplicity = EdmMultiplicity.One - }); - model.AddElement(entity); - EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); - EdmSingleton singleton = container.AddSingleton("Single", entity); - container.AddElement(singleton); - model.AddElement(container); - - // Act - SelectExpandClause selectExpandClause = new ODataQueryOptionParser(model, entity, singleton, - new Dictionary - { - { "$select", "Nav" } - }).ParseSelectAndExpand(); - - SelectExpandNode selectExpandNode = new SelectExpandNode(selectExpandClause, entity, model); - - // Assert - Assert.Null(selectExpandNode.SelectedStructuralProperties); - Assert.Null(selectExpandNode.SelectedComplexProperties); + Assert.NotNull(selectExpandNode.SelectedNavigationProperties); + Assert.Single(selectExpandNode.SelectedNavigationProperties); + } - Assert.NotNull(selectExpandNode.SelectedNavigationProperties); - Assert.Single(selectExpandNode.SelectedNavigationProperties); - } + #region Test IsComplexOrCollectionComplex + [Fact] + public void IsComplexOrCollectionComplex_TestNullInputCorrect() + { + // Arrange & Act + IEdmStructuralProperty primitiveProperty = null; - #region Test IsComplexOrCollectionComplex - [Fact] - public void IsComplexOrCollectionComplex_TestNullInputCorrect() - { - // Arrange & Act - IEdmStructuralProperty primitiveProperty = null; + // Assert + Assert.False(SelectExpandNode.IsComplexOrCollectionComplex(primitiveProperty)); + } - // Assert - Assert.False(SelectExpandNode.IsComplexOrCollectionComplex(primitiveProperty)); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void IsComplexOrCollectionComplex_TestPrimitiveStructuralPropertyCorrect(bool isCollection) + { + // Arrange & Act + var stringType = EdmCoreModel.Instance.GetString(false); + EdmEntityType entityType = new EdmEntityType("NS", "Entity"); + IEdmStructuralProperty primitiveProperty; + if (isCollection) + { + primitiveProperty = entityType.AddStructuralProperty("Codes", new EdmCollectionTypeReference(new EdmCollectionType(stringType))); } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void IsComplexOrCollectionComplex_TestPrimitiveStructuralPropertyCorrect(bool isCollection) + else { - // Arrange & Act - var stringType = EdmCoreModel.Instance.GetString(false); - EdmEntityType entityType = new EdmEntityType("NS", "Entity"); - IEdmStructuralProperty primitiveProperty; - if (isCollection) - { - primitiveProperty = entityType.AddStructuralProperty("Codes", new EdmCollectionTypeReference(new EdmCollectionType(stringType))); - } - else - { - primitiveProperty = entityType.AddStructuralProperty("Id", stringType); - } - - // Assert - Assert.False(SelectExpandNode.IsComplexOrCollectionComplex(primitiveProperty)); + primitiveProperty = entityType.AddStructuralProperty("Id", stringType); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void IsComplexOrCollectionComplex_TestComplexStructuralPropertyCorrect(bool isCollection) - { - // Arrange & Act - var complexType = new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), false); - EdmEntityType entityType = new EdmEntityType("NS", "Entity"); + // Assert + Assert.False(SelectExpandNode.IsComplexOrCollectionComplex(primitiveProperty)); + } - IEdmStructuralProperty complexProperty; - if (isCollection) - { - complexProperty = entityType.AddStructuralProperty("Complexes", new EdmCollectionTypeReference(new EdmCollectionType(complexType))); - } - else - { - complexProperty = entityType.AddStructuralProperty("Single", complexType); - } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void IsComplexOrCollectionComplex_TestComplexStructuralPropertyCorrect(bool isCollection) + { + // Arrange & Act + var complexType = new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), false); + EdmEntityType entityType = new EdmEntityType("NS", "Entity"); - // Assert - Assert.True(SelectExpandNode.IsComplexOrCollectionComplex(complexProperty)); + IEdmStructuralProperty complexProperty; + if (isCollection) + { + complexProperty = entityType.AddStructuralProperty("Complexes", new EdmCollectionTypeReference(new EdmCollectionType(complexType))); } - #endregion - - #region Test EdmStructuralTypeInfo - [Theory] - [InlineData("Customer", 7, 1, 1, 8)] - [InlineData("SpecialCustomer", 9, 2, 2, 10)] - [InlineData("Order", 3, 1, 0, 0)] - [InlineData("Address", 5, 0, 0, 0)] - public void EdmStructuralTypeInfoCtor_ReturnsCorrectProperties(string typeName, int structurals, int navigations, int actions, int functions) + else { - // Assert - IEdmStructuredType structuralType = _model.Model.SchemaElements.OfType().FirstOrDefault(c => c.Name == typeName) as IEdmStructuredType; - Assert.NotNull(structuralType); // Guard + complexProperty = entityType.AddStructuralProperty("Single", complexType); + } - // Act - var structuralTypeInfo = new SelectExpandNode.EdmStructuralTypeInfo(_model.Model, structuralType); + // Assert + Assert.True(SelectExpandNode.IsComplexOrCollectionComplex(complexProperty)); + } + #endregion + + #region Test EdmStructuralTypeInfo + [Theory] + [InlineData("Customer", 7, 1, 1, 8)] + [InlineData("SpecialCustomer", 9, 2, 2, 10)] + [InlineData("Order", 3, 1, 0, 0)] + [InlineData("Address", 5, 0, 0, 0)] + public void EdmStructuralTypeInfoCtor_ReturnsCorrectProperties(string typeName, int structurals, int navigations, int actions, int functions) + { + // Assert + IEdmStructuredType structuralType = _model.Model.SchemaElements.OfType().FirstOrDefault(c => c.Name == typeName) as IEdmStructuredType; + Assert.NotNull(structuralType); // Guard - // Assert - Assert.NotNull(structuralTypeInfo.AllStructuralProperties); - Assert.Equal(structurals, structuralTypeInfo.AllStructuralProperties.Count); + // Act + var structuralTypeInfo = new SelectExpandNode.EdmStructuralTypeInfo(_model.Model, structuralType); - if (navigations == 0) - { - Assert.Null(structuralTypeInfo.AllNavigationProperties); - } - else - { - Assert.NotNull(structuralTypeInfo.AllNavigationProperties); - Assert.Equal(navigations, structuralTypeInfo.AllNavigationProperties.Count); - } + // Assert + Assert.NotNull(structuralTypeInfo.AllStructuralProperties); + Assert.Equal(structurals, structuralTypeInfo.AllStructuralProperties.Count); - if (actions == 0) - { - Assert.Null(structuralTypeInfo.AllActions); - } - else - { - Assert.NotNull(structuralTypeInfo.AllActions); - Assert.Equal(actions, structuralTypeInfo.AllActions.Count); - } + if (navigations == 0) + { + Assert.Null(structuralTypeInfo.AllNavigationProperties); + } + else + { + Assert.NotNull(structuralTypeInfo.AllNavigationProperties); + Assert.Equal(navigations, structuralTypeInfo.AllNavigationProperties.Count); + } - if (functions == 0) - { - Assert.Null(structuralTypeInfo.AllFunctions); - } - else - { - Assert.NotNull(structuralTypeInfo.AllFunctions); - Assert.Equal(functions, structuralTypeInfo.AllFunctions.Count); - } + if (actions == 0) + { + Assert.Null(structuralTypeInfo.AllActions); + } + else + { + Assert.NotNull(structuralTypeInfo.AllActions); + Assert.Equal(actions, structuralTypeInfo.AllActions.Count); } - [Fact] - public void EdmStructuralTypeInfo_IsStructuralPropertyDefined_ReturnsCorrectBoolean() + if (functions == 0) { - // Assert - var structuralTypeInfo = new SelectExpandNode.EdmStructuralTypeInfo(_model.Model, _model.Customer); + Assert.Null(structuralTypeInfo.AllFunctions); + } + else + { + Assert.NotNull(structuralTypeInfo.AllFunctions); + Assert.Equal(functions, structuralTypeInfo.AllFunctions.Count); + } + } - IEdmStructuralProperty property = _model.Customer.DeclaredStructuralProperties().FirstOrDefault(); - IEdmStructuralProperty addressProperty = _model.Address.DeclaredStructuralProperties().FirstOrDefault(); + [Fact] + public void EdmStructuralTypeInfo_IsStructuralPropertyDefined_ReturnsCorrectBoolean() + { + // Assert + var structuralTypeInfo = new SelectExpandNode.EdmStructuralTypeInfo(_model.Model, _model.Customer); - // Act & Assert - Assert.True(structuralTypeInfo.IsStructuralPropertyDefined(property)); - Assert.False(structuralTypeInfo.IsStructuralPropertyDefined(addressProperty)); - } + IEdmStructuralProperty property = _model.Customer.DeclaredStructuralProperties().FirstOrDefault(); + IEdmStructuralProperty addressProperty = _model.Address.DeclaredStructuralProperties().FirstOrDefault(); - [Fact] - public void EdmStructuralTypeInfo_IsNavigationPropertyDefined_ReturnsCorrectBoolean() - { - // Assert - var structuralTypeInfo = new SelectExpandNode.EdmStructuralTypeInfo(_model.Model, _model.Customer); + // Act & Assert + Assert.True(structuralTypeInfo.IsStructuralPropertyDefined(property)); + Assert.False(structuralTypeInfo.IsStructuralPropertyDefined(addressProperty)); + } - IEdmNavigationProperty property = _model.Customer.DeclaredNavigationProperties().FirstOrDefault(); - IEdmNavigationProperty orderProperty = _model.Order.DeclaredNavigationProperties().FirstOrDefault(); + [Fact] + public void EdmStructuralTypeInfo_IsNavigationPropertyDefined_ReturnsCorrectBoolean() + { + // Assert + var structuralTypeInfo = new SelectExpandNode.EdmStructuralTypeInfo(_model.Model, _model.Customer); - // Act & Assert - Assert.True(structuralTypeInfo.IsNavigationPropertyDefined(property)); - Assert.False(structuralTypeInfo.IsNavigationPropertyDefined(orderProperty)); - } + IEdmNavigationProperty property = _model.Customer.DeclaredNavigationProperties().FirstOrDefault(); + IEdmNavigationProperty orderProperty = _model.Order.DeclaredNavigationProperties().FirstOrDefault(); - [Fact] - public void EdmStructuralTypeInfo_ReturnsNullForEmptyStructuredType() - { - // Assert - EdmModel model = new EdmModel(); - EdmEntityType entity = new EdmEntityType("NS", "Entity"); - model.AddElement(entity); + // Act & Assert + Assert.True(structuralTypeInfo.IsNavigationPropertyDefined(property)); + Assert.False(structuralTypeInfo.IsNavigationPropertyDefined(orderProperty)); + } - // Act - var structuralTypeInfo = new SelectExpandNode.EdmStructuralTypeInfo(model, entity); + [Fact] + public void EdmStructuralTypeInfo_ReturnsNullForEmptyStructuredType() + { + // Assert + EdmModel model = new EdmModel(); + EdmEntityType entity = new EdmEntityType("NS", "Entity"); + model.AddElement(entity); - // Assert - Assert.Null(structuralTypeInfo.AllStructuralProperties); - Assert.Null(structuralTypeInfo.AllNavigationProperties); + // Act + var structuralTypeInfo = new SelectExpandNode.EdmStructuralTypeInfo(model, entity); - Assert.Null(structuralTypeInfo.AllActions); - Assert.Null(structuralTypeInfo.AllFunctions); - } + // Assert + Assert.Null(structuralTypeInfo.AllStructuralProperties); + Assert.Null(structuralTypeInfo.AllNavigationProperties); - #endregion + Assert.Null(structuralTypeInfo.AllActions); + Assert.Null(structuralTypeInfo.AllFunctions); + } - public SelectExpandClause ParseSelectExpand(string select, string expand) - { - return new ODataQueryOptionParser(_model.Model, _model.Customer, _model.Customers, - new Dictionary - { - { "$expand", expand == null ? "" : expand }, - { "$select", select == null ? "" : select } - }).ParseSelectAndExpand(); - } + #endregion + + public SelectExpandClause ParseSelectExpand(string select, string expand) + { + return new ODataQueryOptionParser(_model.Model, _model.Customer, _model.Customers, + new Dictionary + { + { "$expand", expand == null ? "" : expand }, + { "$select", select == null ? "" : select } + }).ParseSelectAndExpand(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SerializationTestsHelpers.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SerializationTestsHelpers.cs index c3e0aafc2..6ad3de765 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SerializationTestsHelpers.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Serialization/SerializationTestsHelpers.cs @@ -10,138 +10,137 @@ using Microsoft.AspNetCore.OData.Tests.Edm; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Serialization; + +internal class SerializationTestsHelpers { - internal class SerializationTestsHelpers + public static IEdmModel SimpleCustomerOrderModel() + { + var model = new EdmModel(); + var customerType = new EdmEntityType("Default", "Customer"); + customerType.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32); + customerType.AddStructuralProperty("FirstName", EdmPrimitiveTypeKind.String); + customerType.AddStructuralProperty("LastName", EdmPrimitiveTypeKind.String); + IEdmTypeReference primitiveTypeReference = EdmCoreModel.Instance.GetPrimitive( + EdmPrimitiveTypeKind.String, + isNullable: true); + IEdmStructuralProperty city = customerType.AddStructuralProperty( + "City", + primitiveTypeReference, + defaultValue: null); + model.AddElement(customerType); + + var specialCustomerType = new EdmEntityType("Default", "SpecialCustomer", customerType); + model.AddElement(specialCustomerType); + + var orderType = new EdmEntityType("Default", "Order"); + orderType.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32); + orderType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + orderType.AddStructuralProperty("Shipment", EdmPrimitiveTypeKind.String); + model.AddElement(orderType); + + var specialOrderType = new EdmEntityType("Default", "SpecialOrder", orderType); + model.AddElement(specialOrderType); + + var addressType = new EdmComplexType("Default", "Address"); + addressType.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + addressType.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + addressType.AddStructuralProperty("State", EdmPrimitiveTypeKind.String); + addressType.AddStructuralProperty("CountryOrRegion", EdmPrimitiveTypeKind.String); + addressType.AddStructuralProperty("ZipCode", EdmPrimitiveTypeKind.String); + model.AddElement(addressType); + + // add a derived complex type "UsAddress" + var usAddressType = new EdmComplexType("Default", "UsAddress", addressType); + usAddressType.AddStructuralProperty("UsProp", EdmPrimitiveTypeKind.String); + model.AddElement(usAddressType); + + // add a derived complex type "CnAddress" + var cnAddressType = new EdmComplexType("Default", "CnAddress", addressType); + cnAddressType.AddStructuralProperty("CnProp", EdmPrimitiveTypeKind.Guid); + model.AddElement(cnAddressType); + + // add a complex type "Location" with complex type property + var location = new EdmComplexType("Default", "Location"); + location.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + location.AddStructuralProperty("Address", new EdmComplexTypeReference(addressType, isNullable: true)); + model.AddElement(location); + + // Add navigations + customerType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo() { Name = "Orders", Target = orderType, TargetMultiplicity = EdmMultiplicity.Many }); + orderType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo() { Name = "Customer", Target = customerType, TargetMultiplicity = EdmMultiplicity.One }); + specialCustomerType.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "SpecialOrders", + Target = specialOrderType, + TargetMultiplicity = EdmMultiplicity.Many + }); + orderType.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "SpecialCustomer", + Target = specialCustomerType, + TargetMultiplicity = EdmMultiplicity.One + }); + + // Add Entity set + var container = new EdmEntityContainer("Default", "Container"); + var customerSet = container.AddEntitySet("Customers", customerType); + model.SetOptimisticConcurrencyAnnotation(customerSet, new[] { city }); + var orderSet = container.AddEntitySet("Orders", orderType); + customerSet.AddNavigationTarget(customerType.NavigationProperties().Single(np => np.Name == "Orders"), orderSet); + customerSet.AddNavigationTarget( + specialCustomerType.NavigationProperties().Single(np => np.Name == "SpecialOrders"), + orderSet); + orderSet.AddNavigationTarget(orderType.NavigationProperties().Single(np => np.Name == "Customer"), customerSet); + orderSet.AddNavigationTarget( + specialOrderType.NavigationProperties().Single(np => np.Name == "SpecialCustomer"), + customerSet); + + NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation(); + model.SetNavigationSourceLinkBuilder(customerSet, linkAnnotation); + model.SetNavigationSourceLinkBuilder(orderSet, linkAnnotation); + + model.AddElement(container); + return model; + } + + public static IEdmModel SimpleOpenTypeModel() { - public static IEdmModel SimpleCustomerOrderModel() - { - var model = new EdmModel(); - var customerType = new EdmEntityType("Default", "Customer"); - customerType.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32); - customerType.AddStructuralProperty("FirstName", EdmPrimitiveTypeKind.String); - customerType.AddStructuralProperty("LastName", EdmPrimitiveTypeKind.String); - IEdmTypeReference primitiveTypeReference = EdmCoreModel.Instance.GetPrimitive( - EdmPrimitiveTypeKind.String, - isNullable: true); - IEdmStructuralProperty city = customerType.AddStructuralProperty( - "City", - primitiveTypeReference, - defaultValue: null); - model.AddElement(customerType); - - var specialCustomerType = new EdmEntityType("Default", "SpecialCustomer", customerType); - model.AddElement(specialCustomerType); - - var orderType = new EdmEntityType("Default", "Order"); - orderType.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32); - orderType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - orderType.AddStructuralProperty("Shipment", EdmPrimitiveTypeKind.String); - model.AddElement(orderType); - - var specialOrderType = new EdmEntityType("Default", "SpecialOrder", orderType); - model.AddElement(specialOrderType); - - var addressType = new EdmComplexType("Default", "Address"); - addressType.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); - addressType.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); - addressType.AddStructuralProperty("State", EdmPrimitiveTypeKind.String); - addressType.AddStructuralProperty("CountryOrRegion", EdmPrimitiveTypeKind.String); - addressType.AddStructuralProperty("ZipCode", EdmPrimitiveTypeKind.String); - model.AddElement(addressType); - - // add a derived complex type "UsAddress" - var usAddressType = new EdmComplexType("Default", "UsAddress", addressType); - usAddressType.AddStructuralProperty("UsProp", EdmPrimitiveTypeKind.String); - model.AddElement(usAddressType); - - // add a derived complex type "CnAddress" - var cnAddressType = new EdmComplexType("Default", "CnAddress", addressType); - cnAddressType.AddStructuralProperty("CnProp", EdmPrimitiveTypeKind.Guid); - model.AddElement(cnAddressType); - - // add a complex type "Location" with complex type property - var location = new EdmComplexType("Default", "Location"); - location.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - location.AddStructuralProperty("Address", new EdmComplexTypeReference(addressType, isNullable: true)); - model.AddElement(location); - - // Add navigations - customerType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo() { Name = "Orders", Target = orderType, TargetMultiplicity = EdmMultiplicity.Many }); - orderType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo() { Name = "Customer", Target = customerType, TargetMultiplicity = EdmMultiplicity.One }); - specialCustomerType.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "SpecialOrders", - Target = specialOrderType, - TargetMultiplicity = EdmMultiplicity.Many - }); - orderType.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "SpecialCustomer", - Target = specialCustomerType, - TargetMultiplicity = EdmMultiplicity.One - }); - - // Add Entity set - var container = new EdmEntityContainer("Default", "Container"); - var customerSet = container.AddEntitySet("Customers", customerType); - model.SetOptimisticConcurrencyAnnotation(customerSet, new[] { city }); - var orderSet = container.AddEntitySet("Orders", orderType); - customerSet.AddNavigationTarget(customerType.NavigationProperties().Single(np => np.Name == "Orders"), orderSet); - customerSet.AddNavigationTarget( - specialCustomerType.NavigationProperties().Single(np => np.Name == "SpecialOrders"), - orderSet); - orderSet.AddNavigationTarget(orderType.NavigationProperties().Single(np => np.Name == "Customer"), customerSet); - orderSet.AddNavigationTarget( - specialOrderType.NavigationProperties().Single(np => np.Name == "SpecialCustomer"), - customerSet); - - NavigationSourceLinkBuilderAnnotation linkAnnotation = new MockNavigationSourceLinkBuilderAnnotation(); - model.SetNavigationSourceLinkBuilder(customerSet, linkAnnotation); - model.SetNavigationSourceLinkBuilder(orderSet, linkAnnotation); - - model.AddElement(container); - return model; - } - - public static IEdmModel SimpleOpenTypeModel() - { - var model = new EdmModel(); - - // Address is an open complex type - var addressType = new EdmComplexType("Default", "Address", null, false, true); - addressType.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); - addressType.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); - model.AddElement(addressType); - - // ZipCode is an open complex type also - var zipCodeType = new EdmComplexType("Default", "ZipCode", null, false, true); - zipCodeType.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Int32); - model.AddElement(zipCodeType); - - // Enum type simpleEnum - EdmEnumType simpleEnum = new EdmEnumType("Default", "SimpleEnum"); - simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "First", new EdmEnumMemberValue(0))); - simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "Second", new EdmEnumMemberValue(1))); - simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "Third", new EdmEnumMemberValue(2))); - simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "Fourth", new EdmEnumMemberValue(3))); - model.AddElement(simpleEnum); - - // Customer is an open entity type - var customerType = new EdmEntityType("Default", "Customer", null, false, true); - customerType.AddKeys(customerType.AddStructuralProperty("CustomerId", EdmPrimitiveTypeKind.Int32)); - customerType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - customerType.AddStructuralProperty("Address", addressType.ToEdmTypeReference(false)); - model.AddElement(customerType); - - var container = new EdmEntityContainer("Default", "Container"); - model.AddElement(container); - - var customers = new EdmEntitySet(container, "Customers", customerType); - container.AddElement(customers); - return model; - } + var model = new EdmModel(); + + // Address is an open complex type + var addressType = new EdmComplexType("Default", "Address", null, false, true); + addressType.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + addressType.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + model.AddElement(addressType); + + // ZipCode is an open complex type also + var zipCodeType = new EdmComplexType("Default", "ZipCode", null, false, true); + zipCodeType.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Int32); + model.AddElement(zipCodeType); + + // Enum type simpleEnum + EdmEnumType simpleEnum = new EdmEnumType("Default", "SimpleEnum"); + simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "First", new EdmEnumMemberValue(0))); + simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "Second", new EdmEnumMemberValue(1))); + simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "Third", new EdmEnumMemberValue(2))); + simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "Fourth", new EdmEnumMemberValue(3))); + model.AddElement(simpleEnum); + + // Customer is an open entity type + var customerType = new EdmEntityType("Default", "Customer", null, false, true); + customerType.AddKeys(customerType.AddStructuralProperty("CustomerId", EdmPrimitiveTypeKind.Int32)); + customerType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + customerType.AddStructuralProperty("Address", addressType.ToEdmTypeReference(false)); + model.AddElement(customerType); + + var container = new EdmEntityContainer("Default", "Container"); + model.AddElement(container); + + var customers = new EdmEntitySet(container, "Customers", customerType); + container.AddElement(customers); + return model; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmChangedObjectCollectionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmChangedObjectCollectionTests.cs index 3c1649e8d..534d7abdf 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmChangedObjectCollectionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmChangedObjectCollectionTests.cs @@ -12,45 +12,44 @@ using System.Collections.Generic; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmChangedObjectCollectionTests { - public class EdmChangedObjectCollectionTests + [Fact] + public void Ctor_ThrowsArgumentNull_EdmType() { - [Fact] - public void Ctor_ThrowsArgumentNull_EdmType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmChangedObjectCollection(entityType: null), "entityType"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmChangedObjectCollection(entityType: null), "entityType"); + } - [Fact] - public void Ctor_ThrowsArgumentNull_List() - { - // Arrange & Act & Assert - IEdmEntityType entityType = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => new EdmChangedObjectCollection(entityType, changedObjectList: null), "list"); - } + [Fact] + public void Ctor_ThrowsArgumentNull_List() + { + // Arrange & Act & Assert + IEdmEntityType entityType = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => new EdmChangedObjectCollection(entityType, changedObjectList: null), "list"); + } - [Fact] - public void Ctor_ConfigureProperties() - { - // Arrange & Act & Assert - IEdmEntityType entityType = new Mock().Object; - IList objects = new List(); - EdmChangedObjectCollection collection = new EdmChangedObjectCollection(entityType, objects); - Assert.Empty(collection); - } + [Fact] + public void Ctor_ConfigureProperties() + { + // Arrange & Act & Assert + IEdmEntityType entityType = new Mock().Object; + IList objects = new List(); + EdmChangedObjectCollection collection = new EdmChangedObjectCollection(entityType, objects); + Assert.Empty(collection); + } - [Fact] - public void GetEdmType_Returns_EdmTypeInitializedByCtor() - { - // Arrange - IEdmEntityType _entityType = new EdmEntityType("NS", "Entity"); - var edmObject = new EdmChangedObjectCollection(_entityType); - IEdmCollectionTypeReference collectionTypeReference = (IEdmCollectionTypeReference)edmObject.GetEdmType(); + [Fact] + public void GetEdmType_Returns_EdmTypeInitializedByCtor() + { + // Arrange + IEdmEntityType _entityType = new EdmEntityType("NS", "Entity"); + var edmObject = new EdmChangedObjectCollection(_entityType); + IEdmCollectionTypeReference collectionTypeReference = (IEdmCollectionTypeReference)edmObject.GetEdmType(); - // Act & Assert - Assert.Same(_entityType, collectionTypeReference.ElementType().Definition); - } + // Act & Assert + Assert.Same(_entityType, collectionTypeReference.ElementType().Definition); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmComplexCollectionObjectTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmComplexCollectionObjectTests.cs index 38e719493..49e8d1314 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmComplexCollectionObjectTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmComplexCollectionObjectTests.cs @@ -11,42 +11,41 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmComplexCollectionObjectTests { - public class EdmComplexCollectionObjectTests + [Fact] + public void Ctor_ThrowsArgumentNull_EdmType() + { + ExceptionAssert.ThrowsArgumentNull(() => new EdmComplexObjectCollection(edmType: null), "edmType"); + } + + [Fact] + public void Ctor_ThrowsArgumentNull_List() + { + IEdmCollectionTypeReference edmType = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => new EdmComplexObjectCollection(edmType, list: null), "list"); + } + + [Fact] + public void Ctor_ThrowsArgument_UnexpectedElementType() + { + IEdmTypeReference elementType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: true); + IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); + + ExceptionAssert.ThrowsArgument(() => new EdmComplexObjectCollection(collectionType), "edmType", + "The element type '[Edm.Int32 Nullable=True]' of the given collection type '[Collection([Edm.Int32 Nullable=True]) Nullable=True]' " + + "is not of the type 'IEdmComplexType'."); + } + + [Fact] + public void GetEdmType_Returns_EdmTypeInitializedByCtor() { - [Fact] - public void Ctor_ThrowsArgumentNull_EdmType() - { - ExceptionAssert.ThrowsArgumentNull(() => new EdmComplexObjectCollection(edmType: null), "edmType"); - } - - [Fact] - public void Ctor_ThrowsArgumentNull_List() - { - IEdmCollectionTypeReference edmType = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => new EdmComplexObjectCollection(edmType, list: null), "list"); - } - - [Fact] - public void Ctor_ThrowsArgument_UnexpectedElementType() - { - IEdmTypeReference elementType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: true); - IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); - - ExceptionAssert.ThrowsArgument(() => new EdmComplexObjectCollection(collectionType), "edmType", - "The element type '[Edm.Int32 Nullable=True]' of the given collection type '[Collection([Edm.Int32 Nullable=True]) Nullable=True]' " + - "is not of the type 'IEdmComplexType'."); - } - - [Fact] - public void GetEdmType_Returns_EdmTypeInitializedByCtor() - { - IEdmTypeReference elementType = new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), isNullable: false); - IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); - - var edmObject = new EdmComplexObjectCollection(collectionType); - Assert.Same(collectionType, edmObject.GetEdmType()); - } + IEdmTypeReference elementType = new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), isNullable: false); + IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); + + var edmObject = new EdmComplexObjectCollection(collectionType); + Assert.Same(collectionType, edmObject.GetEdmType()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmComplexObjectTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmComplexObjectTests.cs index c03471f43..3f8648bf5 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmComplexObjectTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmComplexObjectTests.cs @@ -10,47 +10,46 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmComplexObjectTests { - public class EdmComplexObjectTests + [Fact] + public void CtorEdmComplexObject_ThrowsArgumentNull_IEdmComplexType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmComplexObject((IEdmComplexType)null), "edmType"); + } + + [Fact] + public void CtorEdmComplexObject_ThrowsArgumentNull_IEdmComplexTypeAndNullable() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmComplexObject(null, true), "edmType"); + } + + [Fact] + public void CtorEdmComplexObject_ThrowsArgumentNull_IEdmComplexTypeReference() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmComplexObject((IEdmComplexTypeReference)null), "type"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CtorEdmComplexObject_SetProperties(bool isNullable) { - [Fact] - public void CtorEdmComplexObject_ThrowsArgumentNull_IEdmComplexType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmComplexObject((IEdmComplexType)null), "edmType"); - } - - [Fact] - public void CtorEdmComplexObject_ThrowsArgumentNull_IEdmComplexTypeAndNullable() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmComplexObject(null, true), "edmType"); - } - - [Fact] - public void CtorEdmComplexObject_ThrowsArgumentNull_IEdmComplexTypeReference() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmComplexObject((IEdmComplexTypeReference)null), "type"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CtorEdmComplexObject_SetProperties(bool isNullable) - { - // Arrange - EdmComplexType complexType = new EdmComplexType("NS", "Complex"); - IEdmComplexTypeReference complex = new EdmComplexTypeReference(complexType, isNullable); - - // Act - EdmComplexObject edmObject = new EdmComplexObject(complex); - - // Assert - Assert.Same(complexType, edmObject.ExpectedEdmType); - Assert.Same(complexType, edmObject.ActualEdmType); - Assert.Equal(isNullable, edmObject.IsNullable); - } + // Arrange + EdmComplexType complexType = new EdmComplexType("NS", "Complex"); + IEdmComplexTypeReference complex = new EdmComplexTypeReference(complexType, isNullable); + + // Act + EdmComplexObject edmObject = new EdmComplexObject(complex); + + // Assert + Assert.Same(complexType, edmObject.ExpectedEdmType); + Assert.Same(complexType, edmObject.ActualEdmType); + Assert.Equal(isNullable, edmObject.IsNullable); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaCollectionTypeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaCollectionTypeTests.cs index a941a964b..5bc020b9e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaCollectionTypeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaCollectionTypeTests.cs @@ -11,27 +11,26 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmDeltaCollectionTypeTests { - public class EdmDeltaCollectionTypeTests + [Fact] + public void CtorEdmDeltaCollectionType_ThrowsArgumentNull_Type() { - [Fact] - public void CtorEdmDeltaCollectionType_ThrowsArgumentNull_Type() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaCollectionType(null), "typeReference"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaCollectionType(null), "typeReference"); + } - [Fact] - public void CtorEdmDeltaCollectionType_SetsProperties() - { - // Arrange & Act - IEdmTypeReference typeRef = new Mock().Object; - EdmDeltaCollectionType deltaType = new EdmDeltaCollectionType(typeRef); + [Fact] + public void CtorEdmDeltaCollectionType_SetsProperties() + { + // Arrange & Act + IEdmTypeReference typeRef = new Mock().Object; + EdmDeltaCollectionType deltaType = new EdmDeltaCollectionType(typeRef); - // Assert - Assert.Equal(EdmTypeKind.Collection, deltaType.TypeKind); - Assert.Same(typeRef, deltaType.ElementType); - } + // Assert + Assert.Equal(EdmTypeKind.Collection, deltaType.TypeKind); + Assert.Same(typeRef, deltaType.ElementType); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaComplexObjectTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaComplexObjectTests.cs index 5e8676f1b..d8c39eb78 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaComplexObjectTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaComplexObjectTests.cs @@ -11,47 +11,46 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmDeltaComplexObjectTests { - public class EdmDeltaComplexObjectTests + [Fact] + public void CtorEdmDeltaComplexObject_ThrowsArgumentNull_IEdmEntityType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaComplexObject((IEdmComplexType)null), "edmType"); + } + + [Fact] + public void CtorEdmDeltaComplexObject_ThrowsArgumentNull_IEdmEntityTypeAndNullable() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaComplexObject(null, true), "edmType"); + } + + [Fact] + public void CtorEdmDeltaComplexObject_ThrowsArgumentNull_IEdmEntityTypeReference() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaComplexObject((IEdmComplexTypeReference)null), "type"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CtorEdmDeltaComplexObject_SetProperties(bool isNullable) { - [Fact] - public void CtorEdmDeltaComplexObject_ThrowsArgumentNull_IEdmEntityType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaComplexObject((IEdmComplexType)null), "edmType"); - } - - [Fact] - public void CtorEdmDeltaComplexObject_ThrowsArgumentNull_IEdmEntityTypeAndNullable() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaComplexObject(null, true), "edmType"); - } - - [Fact] - public void CtorEdmDeltaComplexObject_ThrowsArgumentNull_IEdmEntityTypeReference() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaComplexObject((IEdmComplexTypeReference)null), "type"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CtorEdmDeltaComplexObject_SetProperties(bool isNullable) - { - // Arrange - IEdmComplexType complexType = new EdmComplexType("NS", "Complex"); - IEdmComplexTypeReference complex = new EdmComplexTypeReference(complexType, isNullable); - - // Act - EdmDeltaComplexObject deltaObject = new EdmDeltaComplexObject(complex); - - // Assert - Assert.Same(complexType, deltaObject.ExpectedEdmType); - Assert.Same(complexType, deltaObject.ActualEdmType); - Assert.Equal(DeltaItemKind.Resource, deltaObject.Kind); - } + // Arrange + IEdmComplexType complexType = new EdmComplexType("NS", "Complex"); + IEdmComplexTypeReference complex = new EdmComplexTypeReference(complexType, isNullable); + + // Act + EdmDeltaComplexObject deltaObject = new EdmDeltaComplexObject(complex); + + // Assert + Assert.Same(complexType, deltaObject.ExpectedEdmType); + Assert.Same(complexType, deltaObject.ActualEdmType); + Assert.Equal(DeltaItemKind.Resource, deltaObject.Kind); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaDeletedLinkTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaDeletedLinkTests.cs index 3a225886c..454da15c2 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaDeletedLinkTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaDeletedLinkTests.cs @@ -12,48 +12,47 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmDeltaDeletedLinkTests { - public class EdmDeltaDeletedLinkTests + [Fact] + public void CtorEdmDeltaDeletedLink_ThrowsArgumentNull_TypeReference() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaDeletedLink(entityTypeReference: null), "typeReference"); + } + + [Fact] + public void CtorEdmDeltaDeletedLink_ThrowsArgumentNull_EntityType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaDeletedLink(entityType: null), "entityType"); + + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaDeletedLink(entityType: null, false), "entityType"); + } + + [Fact] + public void KindProperty_Returns_DeltaItemKind() + { + // Arrange & Act + Mock mock = new Mock(); + EdmDeltaDeletedLink edmDeletedLink = new EdmDeltaDeletedLink(mock.Object); + + // Assert + Assert.Equal(DeltaItemKind.DeltaDeletedLink, edmDeletedLink.Kind); + } + + [Fact] + public void CtorEdmDeltaDeletedLink_Sets_PropertyValue() { - [Fact] - public void CtorEdmDeltaDeletedLink_ThrowsArgumentNull_TypeReference() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaDeletedLink(entityTypeReference: null), "typeReference"); - } - - [Fact] - public void CtorEdmDeltaDeletedLink_ThrowsArgumentNull_EntityType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaDeletedLink(entityType: null), "entityType"); - - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaDeletedLink(entityType: null, false), "entityType"); - } - - [Fact] - public void KindProperty_Returns_DeltaItemKind() - { - // Arrange & Act - Mock mock = new Mock(); - EdmDeltaDeletedLink edmDeletedLink = new EdmDeltaDeletedLink(mock.Object); - - // Assert - Assert.Equal(DeltaItemKind.DeltaDeletedLink, edmDeletedLink.Kind); - } - - [Fact] - public void CtorEdmDeltaDeletedLink_Sets_PropertyValue() - { - // Arrange & Act - Mock mock = new Mock(); - EdmDeltaDeletedLink edmDeletedLink = new EdmDeltaDeletedLink(mock.Object, true); - - // Assert - Assert.Same(mock.Object, edmDeletedLink.EntityType); - Assert.Same(mock.Object, edmDeletedLink.GetEdmType().Definition); - Assert.True(edmDeletedLink.IsNullable); - } + // Arrange & Act + Mock mock = new Mock(); + EdmDeltaDeletedLink edmDeletedLink = new EdmDeltaDeletedLink(mock.Object, true); + + // Assert + Assert.Same(mock.Object, edmDeletedLink.EntityType); + Assert.Same(mock.Object, edmDeletedLink.GetEdmType().Definition); + Assert.True(edmDeletedLink.IsNullable); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaLinkTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaLinkTests.cs index 39e05e588..6e0d768ea 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaLinkTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaLinkTests.cs @@ -12,48 +12,47 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmDeltaLinkTests { - public class EdmDeltaLinkTests + [Fact] + public void CtorEdmDeltaLink_ThrowsArgumentNull_TypeReference() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaLink(entityTypeReference: null), "typeReference"); + } + + [Fact] + public void CtorEdmDeltaLink_ThrowsArgumentNull_EntityType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaLink(entityType: null), "entityType"); + + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaLink(entityType: null, false), "entityType"); + } + + [Fact] + public void KindProperty_Returns_DeltaItemKind() + { + // Arrange & Act + Mock mock = new Mock(); + EdmDeltaLink edmLink = new EdmDeltaLink(mock.Object); + + // Assert + Assert.Equal(DeltaItemKind.DeltaLink, edmLink.Kind); + } + + [Fact] + public void CtorEdmDeltaLink_Sets_PropertyValue() { - [Fact] - public void CtorEdmDeltaLink_ThrowsArgumentNull_TypeReference() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaLink(entityTypeReference: null), "typeReference"); - } - - [Fact] - public void CtorEdmDeltaLink_ThrowsArgumentNull_EntityType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaLink(entityType: null), "entityType"); - - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaLink(entityType: null, false), "entityType"); - } - - [Fact] - public void KindProperty_Returns_DeltaItemKind() - { - // Arrange & Act - Mock mock = new Mock(); - EdmDeltaLink edmLink = new EdmDeltaLink(mock.Object); - - // Assert - Assert.Equal(DeltaItemKind.DeltaLink, edmLink.Kind); - } - - [Fact] - public void CtorEdmDeltaLink_Sets_PropertyValue() - { - // Arrange & Act - Mock mock = new Mock(); - EdmDeltaLink edmLink = new EdmDeltaLink(mock.Object, true); - - // Assert - Assert.Same(mock.Object, edmLink.EntityType); - Assert.Same(mock.Object, edmLink.GetEdmType().Definition); - Assert.True(edmLink.IsNullable); - } + // Arrange & Act + Mock mock = new Mock(); + EdmDeltaLink edmLink = new EdmDeltaLink(mock.Object, true); + + // Assert + Assert.Same(mock.Object, edmLink.EntityType); + Assert.Same(mock.Object, edmLink.GetEdmType().Definition); + Assert.True(edmLink.IsNullable); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaResourceObjectTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaResourceObjectTests.cs index 8cb999af3..68d0d06a3 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaResourceObjectTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaResourceObjectTests.cs @@ -11,47 +11,46 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmDeltaResourceObjectTests { - public class EdmDeltaResourceObjectTests + [Fact] + public void CtorEdmDeltaResourceObject_ThrowsArgumentNull_IEdmEntityType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaResourceObject((IEdmEntityType)null), "edmType"); + } + + [Fact] + public void CtorEdmDeltaResourceObject_ThrowsArgumentNull_IEdmEntityTypeAndNullable() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaResourceObject(null, true), "edmType"); + } + + [Fact] + public void CtorEdmDeltaResourceObject_ThrowsArgumentNull_IEdmEntityTypeReference() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaResourceObject((IEdmEntityTypeReference)null), "type"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CtorEdmDeltaResourceObject_SetProperties(bool isNullable) { - [Fact] - public void CtorEdmDeltaResourceObject_ThrowsArgumentNull_IEdmEntityType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaResourceObject((IEdmEntityType)null), "edmType"); - } - - [Fact] - public void CtorEdmDeltaResourceObject_ThrowsArgumentNull_IEdmEntityTypeAndNullable() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaResourceObject(null, true), "edmType"); - } - - [Fact] - public void CtorEdmDeltaResourceObject_ThrowsArgumentNull_IEdmEntityTypeReference() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaResourceObject((IEdmEntityTypeReference)null), "type"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CtorEdmDeltaResourceObject_SetProperties(bool isNullable) - { - // Arrange - IEdmEntityType entityType = new EdmEntityType("NS", "Entity"); - IEdmEntityTypeReference entity = new EdmEntityTypeReference(entityType, isNullable); - - // Act - EdmDeltaResourceObject deltaObject = new EdmDeltaResourceObject(entity); - - // Assert - Assert.Same(entityType, deltaObject.ExpectedEdmType); - Assert.Same(entityType, deltaObject.ActualEdmType); - Assert.Equal(DeltaItemKind.Resource, deltaObject.DeltaKind); - } + // Arrange + IEdmEntityType entityType = new EdmEntityType("NS", "Entity"); + IEdmEntityTypeReference entity = new EdmEntityTypeReference(entityType, isNullable); + + // Act + EdmDeltaResourceObject deltaObject = new EdmDeltaResourceObject(entity); + + // Assert + Assert.Same(entityType, deltaObject.ExpectedEdmType); + Assert.Same(entityType, deltaObject.ActualEdmType); + Assert.Equal(DeltaItemKind.Resource, deltaObject.DeltaKind); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaTypeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaTypeTests.cs index e559fec70..37f6a6d63 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaTypeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmDeltaTypeTests.cs @@ -12,28 +12,27 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmDeltaTypeTests { - public class EdmDeltaTypeTests + [Fact] + public void CtorEdmDeltaType_ThrowsArgumentNull_EntityType() { - [Fact] - public void CtorEdmDeltaType_ThrowsArgumentNull_EntityType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaType(null, DeltaItemKind.Resource), "entityType"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmDeltaType(null, DeltaItemKind.Resource), "entityType"); + } - [Fact] - public void CtorEdmDeltaType_SetsProperties() - { - // Arrange & Act - IEdmEntityType typeRef = new Mock().Object; - EdmDeltaType deltaType = new EdmDeltaType(typeRef, DeltaItemKind.Resource); + [Fact] + public void CtorEdmDeltaType_SetsProperties() + { + // Arrange & Act + IEdmEntityType typeRef = new Mock().Object; + EdmDeltaType deltaType = new EdmDeltaType(typeRef, DeltaItemKind.Resource); - // Assert - Assert.Equal(EdmTypeKind.Entity, deltaType.TypeKind); - Assert.Same(typeRef, deltaType.EntityType); - Assert.Equal(DeltaItemKind.Resource, deltaType.DeltaKind); - } + // Assert + Assert.Equal(EdmTypeKind.Entity, deltaType.TypeKind); + Assert.Same(typeRef, deltaType.EntityType); + Assert.Equal(DeltaItemKind.Resource, deltaType.DeltaKind); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEntityCollectionObjectTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEntityCollectionObjectTests.cs index e34be8210..27a5d1ca4 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEntityCollectionObjectTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEntityCollectionObjectTests.cs @@ -11,42 +11,41 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmEntityCollectionObjectTests { - public class EdmEntityCollectionObjectTests + [Fact] + public void Ctor_ThrowsArgumentNull_EdmType() + { + ExceptionAssert.ThrowsArgumentNull(() => new EdmEntityObjectCollection(edmType: null), "edmType"); + } + + [Fact] + public void Ctor_ThrowsArgumentNull_List() + { + IEdmCollectionTypeReference edmType = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => new EdmEntityObjectCollection(edmType, list: null), "list"); + } + + [Fact] + public void Ctor_ThrowsArgument_UnexpectedElementType() + { + IEdmTypeReference elementType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: true); + IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); + + ExceptionAssert.ThrowsArgument(() => new EdmEntityObjectCollection(collectionType), "edmType", + "The element type '[Edm.Int32 Nullable=True]' of the given collection type '[Collection([Edm.Int32 Nullable=True]) Nullable=True]' " + + "is not of the type 'IEdmEntityType'."); + } + + [Fact] + public void GetEdmType_Returns_EdmTypeInitializedByCtor() { - [Fact] - public void Ctor_ThrowsArgumentNull_EdmType() - { - ExceptionAssert.ThrowsArgumentNull(() => new EdmEntityObjectCollection(edmType: null), "edmType"); - } - - [Fact] - public void Ctor_ThrowsArgumentNull_List() - { - IEdmCollectionTypeReference edmType = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => new EdmEntityObjectCollection(edmType, list: null), "list"); - } - - [Fact] - public void Ctor_ThrowsArgument_UnexpectedElementType() - { - IEdmTypeReference elementType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: true); - IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); - - ExceptionAssert.ThrowsArgument(() => new EdmEntityObjectCollection(collectionType), "edmType", - "The element type '[Edm.Int32 Nullable=True]' of the given collection type '[Collection([Edm.Int32 Nullable=True]) Nullable=True]' " + - "is not of the type 'IEdmEntityType'."); - } - - [Fact] - public void GetEdmType_Returns_EdmTypeInitializedByCtor() - { - IEdmTypeReference elementType = new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), isNullable: false); - IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); - - var edmObject = new EdmEntityObjectCollection(collectionType); - Assert.Same(collectionType, edmObject.GetEdmType()); - } + IEdmTypeReference elementType = new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), isNullable: false); + IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); + + var edmObject = new EdmEntityObjectCollection(collectionType); + Assert.Same(collectionType, edmObject.GetEdmType()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEntityObjectTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEntityObjectTests.cs index c2ebb2a2f..0b23994de 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEntityObjectTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEntityObjectTests.cs @@ -10,47 +10,46 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmEntityObjectTests { - public class EdmEntityObjectTests + [Fact] + public void CtorEdmEntityObject_ThrowsArgumentNull_IEdmEntityType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmEntityObject((IEdmEntityType)null), "edmType"); + } + + [Fact] + public void CtorEdmEntityObject_ThrowsArgumentNull_IEdmEntityTypeAndNullable() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmEntityObject(null, true), "edmType"); + } + + [Fact] + public void CtorEdmEntityObject_ThrowsArgumentNull_IEdmEntityTypeReference() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EdmEntityObject((IEdmEntityTypeReference)null), "type"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CtorEdmEntityObject_SetProperties(bool isNullable) { - [Fact] - public void CtorEdmEntityObject_ThrowsArgumentNull_IEdmEntityType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmEntityObject((IEdmEntityType)null), "edmType"); - } - - [Fact] - public void CtorEdmEntityObject_ThrowsArgumentNull_IEdmEntityTypeAndNullable() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmEntityObject(null, true), "edmType"); - } - - [Fact] - public void CtorEdmEntityObject_ThrowsArgumentNull_IEdmEntityTypeReference() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EdmEntityObject((IEdmEntityTypeReference)null), "type"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CtorEdmEntityObject_SetProperties(bool isNullable) - { - // Arrange - IEdmEntityType entityType = new EdmEntityType("NS", "Entity"); - IEdmEntityTypeReference entity = new EdmEntityTypeReference(entityType, isNullable); - - // Act - EdmEntityObject edmObject = new EdmEntityObject(entity); - - // Assert - Assert.Same(entityType, edmObject.ExpectedEdmType); - Assert.Same(entityType, edmObject.ActualEdmType); - Assert.Equal(isNullable, edmObject.IsNullable); - } + // Arrange + IEdmEntityType entityType = new EdmEntityType("NS", "Entity"); + IEdmEntityTypeReference entity = new EdmEntityTypeReference(entityType, isNullable); + + // Act + EdmEntityObject edmObject = new EdmEntityObject(entity); + + // Assert + Assert.Same(entityType, edmObject.ExpectedEdmType); + Assert.Same(entityType, edmObject.ActualEdmType); + Assert.Equal(isNullable, edmObject.IsNullable); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEnumObjectCollectionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEnumObjectCollectionTests.cs index c5dfbd0a3..cff5c8be0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEnumObjectCollectionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEnumObjectCollectionTests.cs @@ -11,50 +11,49 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmEnumObjectCollectionTests { - public class EdmEnumObjectCollectionTests + [Fact] + public void Ctor_ThrowsArgumentNull_EdmType() + { + ExceptionAssert.ThrowsArgumentNull(() => new EdmEnumObjectCollection(edmType: null), "edmType"); + } + + [Fact] + public void Ctor_ThrowsArgumentNull_List() + { + IEdmCollectionTypeReference edmType = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => new EdmEnumObjectCollection(edmType, list: null), "list"); + } + + [Fact] + public void Ctor_ThrowsArgument_UnexpectedElementType() { - [Fact] - public void Ctor_ThrowsArgumentNull_EdmType() - { - ExceptionAssert.ThrowsArgumentNull(() => new EdmEnumObjectCollection(edmType: null), "edmType"); - } - - [Fact] - public void Ctor_ThrowsArgumentNull_List() - { - IEdmCollectionTypeReference edmType = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => new EdmEnumObjectCollection(edmType, list: null), "list"); - } - - [Fact] - public void Ctor_ThrowsArgument_UnexpectedElementType() - { - // Arrange - IEdmTypeReference elementType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: true); - - // Act - IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); - - // Assert - ExceptionAssert.ThrowsArgument(() => new EdmEnumObjectCollection(collectionType), "edmType", - "The element type '[Edm.Int32 Nullable=True]' of the given collection type '[Collection([Edm.Int32 Nullable=True]) Nullable=True]' " + - "is not of the type 'IEdmEnumType'."); - } - - [Fact] - public void GetEdmType_Returns_EdmTypeInitializedByCtor() - { - // Arrange - IEdmTypeReference elementType = new EdmEnumTypeReference(new EdmEnumType("NS", "Enum"), isNullable: false); - IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); - - // Act - var edmObject = new EdmEnumObjectCollection(collectionType); - - // Assert - Assert.Same(collectionType, edmObject.GetEdmType()); - } + // Arrange + IEdmTypeReference elementType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: true); + + // Act + IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); + + // Assert + ExceptionAssert.ThrowsArgument(() => new EdmEnumObjectCollection(collectionType), "edmType", + "The element type '[Edm.Int32 Nullable=True]' of the given collection type '[Collection([Edm.Int32 Nullable=True]) Nullable=True]' " + + "is not of the type 'IEdmEnumType'."); + } + + [Fact] + public void GetEdmType_Returns_EdmTypeInitializedByCtor() + { + // Arrange + IEdmTypeReference elementType = new EdmEnumTypeReference(new EdmEnumType("NS", "Enum"), isNullable: false); + IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); + + // Act + var edmObject = new EdmEnumObjectCollection(collectionType); + + // Assert + Assert.Same(collectionType, edmObject.GetEdmType()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEnumObjectTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEnumObjectTests.cs index d279e7429..2f6a2fa0a 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEnumObjectTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmEnumObjectTests.cs @@ -10,60 +10,59 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmEnumObjectTests { - public class EdmEnumObjectTests + [Fact] + public void Ctor_ThrowsArgumentNull_EdmTypeOfTypeIEdmEnumType() { - [Fact] - public void Ctor_ThrowsArgumentNull_EdmTypeOfTypeIEdmEnumType() - { - ExceptionAssert.ThrowsArgumentNull(() => new TestEdmEnumObject((IEdmEnumType)null, "test"), "edmType"); - } + ExceptionAssert.ThrowsArgumentNull(() => new TestEdmEnumObject((IEdmEnumType)null, "test"), "edmType"); + } - [Fact] - public void Ctor_ThrowsArgumentNull_EdmTypeOfTypeIEdmEnumTypeReference() - { - ExceptionAssert.ThrowsArgumentNull(() => new TestEdmEnumObject((IEdmEnumTypeReference)null, "test"), "type"); - } + [Fact] + public void Ctor_ThrowsArgumentNull_EdmTypeOfTypeIEdmEnumTypeReference() + { + ExceptionAssert.ThrowsArgumentNull(() => new TestEdmEnumObject((IEdmEnumTypeReference)null, "test"), "type"); + } - [Fact] - public void Property_IsNullable() - { - TestEdmEnumObject edmObject = new TestEdmEnumObject(new EdmEnumType("NS", "Enum"), "test"); + [Fact] + public void Property_IsNullable() + { + TestEdmEnumObject edmObject = new TestEdmEnumObject(new EdmEnumType("NS", "Enum"), "test"); - ReflectionAssert.BooleanProperty(edmObject, e => e.IsNullable, expectedDefaultValue: false); - } + ReflectionAssert.BooleanProperty(edmObject, e => e.IsNullable, expectedDefaultValue: false); + } - [Fact] - public void GetEdmType_HasSameDefinition_AsInitializedEdmType() - { - var enumType = new EdmEnumType("NS", "Enum"); - var edmObject = new TestEdmEnumObject(enumType, "test"); + [Fact] + public void GetEdmType_HasSameDefinition_AsInitializedEdmType() + { + var enumType = new EdmEnumType("NS", "Enum"); + var edmObject = new TestEdmEnumObject(enumType, "test"); - Assert.Equal(enumType, edmObject.GetEdmType().Definition); - } + Assert.Equal(enumType, edmObject.GetEdmType().Definition); + } - [Fact] - public void GetEdmType_AgreesWithPropertyIsNullable() - { - var enumType = new EdmEnumType("NS", "Enum"); - var edmObject = new TestEdmEnumObject(enumType, "test"); - edmObject.IsNullable = true; + [Fact] + public void GetEdmType_AgreesWithPropertyIsNullable() + { + var enumType = new EdmEnumType("NS", "Enum"); + var edmObject = new TestEdmEnumObject(enumType, "test"); + edmObject.IsNullable = true; - Assert.True(edmObject.GetEdmType().IsNullable); - } + Assert.True(edmObject.GetEdmType().IsNullable); + } - private class TestEdmEnumObject : EdmEnumObject + private class TestEdmEnumObject : EdmEnumObject + { + public TestEdmEnumObject(IEdmEnumType edmType, string value) + : base(edmType, value) { - public TestEdmEnumObject(IEdmEnumType edmType, string value) - : base(edmType, value) - { - } + } - public TestEdmEnumObject(IEdmEnumTypeReference edmType, string value) - : base(edmType, value) - { - } + public TestEdmEnumObject(IEdmEnumTypeReference edmType, string value) + : base(edmType, value) + { } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmObjectExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmObjectExtensionsTests.cs index 15a8b9c8d..feb1bc0df 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmObjectExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmObjectExtensionsTests.cs @@ -10,24 +10,23 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmObjectExtensionsTests { - public class EdmObjectExtensionsTests + [Fact] + public void IsDeltaResourceSet_ThrowsArgumentNull_Type() { - [Fact] - public void IsDeltaResourceSet_ThrowsArgumentNull_Type() - { - // Arrange & Act & Assert - IEdmType edmType = null; - ExceptionAssert.ThrowsArgumentNull(() => edmType.IsDeltaResourceSet(), "type"); - } + // Arrange & Act & Assert + IEdmType edmType = null; + ExceptionAssert.ThrowsArgumentNull(() => edmType.IsDeltaResourceSet(), "type"); + } - [Fact] - public void IsDeltaResource_ThrowsArgumentNull_Resource() - { - // Arrange & Act & Assert - IEdmObject resource = null; - ExceptionAssert.ThrowsArgumentNull(() => resource.IsDeltaResource(), "resource"); - } + [Fact] + public void IsDeltaResource_ThrowsArgumentNull_Resource() + { + // Arrange & Act & Assert + IEdmObject resource = null; + ExceptionAssert.ThrowsArgumentNull(() => resource.IsDeltaResource(), "resource"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmObjectHelperTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmObjectHelperTests.cs index 0ea0288cd..99c44d7f2 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmObjectHelperTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmObjectHelperTests.cs @@ -10,28 +10,27 @@ using System.Collections.Generic; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmObjectHelperTests { - public class EdmObjectHelperTests + [Fact] + public void ConvertToEdmObject_Converts_ComplexCollection() { - [Fact] - public void ConvertToEdmObject_Converts_ComplexCollection() + // Arrange + EdmComplexType complexType = new EdmComplexType("NS", "Complex"); + IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(complexType, true))); + var source = new List { - // Arrange - EdmComplexType complexType = new EdmComplexType("NS", "Complex"); - IEdmCollectionTypeReference collectionType = new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(complexType, true))); - var source = new List - { - new EdmComplexObject(complexType, true), - new EdmComplexObject(complexType, true) - }; + new EdmComplexObject(complexType, true), + new EdmComplexObject(complexType, true) + }; - // Act - IEdmObject obj = source.ConvertToEdmObject(collectionType); + // Act + IEdmObject obj = source.ConvertToEdmObject(collectionType); - // Assert - EdmComplexObjectCollection complexCollection = Assert.IsType(obj); - Assert.Equal(2, complexCollection.Count); - } + // Assert + EdmComplexObjectCollection complexCollection = Assert.IsType(obj); + Assert.Equal(2, complexCollection.Count); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmStructuredObjectTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmStructuredObjectTests.cs index 2e5071623..67c5bd0ce 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmStructuredObjectTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmStructuredObjectTests.cs @@ -14,391 +14,390 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmStructuredObjectTest { - public class EdmStructuredObjectTest + [Fact] + public void Ctor_ThrowsArgumentNull_EdmTypeOfTypeIEdmStructuredType() { - [Fact] - public void Ctor_ThrowsArgumentNull_EdmTypeOfTypeIEdmStructuredType() - { - ExceptionAssert.ThrowsArgumentNull(() => new TestEdmStructuredObject((IEdmStructuredType)null), "edmType"); - } + ExceptionAssert.ThrowsArgumentNull(() => new TestEdmStructuredObject((IEdmStructuredType)null), "edmType"); + } - [Fact] - public void Ctor_ThrowsArgumentNull_EdmTypeOfTypeIEdmStructuredTypeReference() - { - ExceptionAssert.ThrowsArgumentNull(() => new TestEdmStructuredObject((IEdmStructuredTypeReference)null), "type"); - } + [Fact] + public void Ctor_ThrowsArgumentNull_EdmTypeOfTypeIEdmStructuredTypeReference() + { + ExceptionAssert.ThrowsArgumentNull(() => new TestEdmStructuredObject((IEdmStructuredTypeReference)null), "type"); + } - [Fact] - public void Property_IsNullable() - { - TestEdmStructuredObject edmObject = new TestEdmStructuredObject(new EdmComplexType("NS", "Complex")); + [Fact] + public void Property_IsNullable() + { + TestEdmStructuredObject edmObject = new TestEdmStructuredObject(new EdmComplexType("NS", "Complex")); - ReflectionAssert.BooleanProperty(edmObject, e => e.IsNullable, expectedDefaultValue: false); - } + ReflectionAssert.BooleanProperty(edmObject, e => e.IsNullable, expectedDefaultValue: false); + } - [Fact] - public void Property_ActualEdmType() - { - EdmEntityType edmBaseType = new EdmEntityType("NS", "Base"); - EdmEntityType edmDerivedType = new EdmEntityType("NS", "Derived", edmBaseType); - TestEdmStructuredObject edmObject = new TestEdmStructuredObject(edmBaseType); + [Fact] + public void Property_ActualEdmType() + { + EdmEntityType edmBaseType = new EdmEntityType("NS", "Base"); + EdmEntityType edmDerivedType = new EdmEntityType("NS", "Derived", edmBaseType); + TestEdmStructuredObject edmObject = new TestEdmStructuredObject(edmBaseType); - ReflectionAssert.Property(edmObject, o => o.ActualEdmType, edmBaseType, allowNull: false, roundTripTestValue: edmDerivedType); - } + ReflectionAssert.Property(edmObject, o => o.ActualEdmType, edmBaseType, allowNull: false, roundTripTestValue: edmDerivedType); + } - [Fact] - public void Property_ExpectedEdmType() - { - EdmEntityType edmBaseType = new EdmEntityType("NS", "Base"); - EdmEntityType edmDerivedType = new EdmEntityType("NS", "Derived", edmBaseType); - TestEdmStructuredObject edmObject = new TestEdmStructuredObject(edmDerivedType); + [Fact] + public void Property_ExpectedEdmType() + { + EdmEntityType edmBaseType = new EdmEntityType("NS", "Base"); + EdmEntityType edmDerivedType = new EdmEntityType("NS", "Derived", edmBaseType); + TestEdmStructuredObject edmObject = new TestEdmStructuredObject(edmDerivedType); - ReflectionAssert.Property(edmObject, o => o.ExpectedEdmType, edmDerivedType, allowNull: false, roundTripTestValue: edmBaseType); - } + ReflectionAssert.Property(edmObject, o => o.ExpectedEdmType, edmDerivedType, allowNull: false, roundTripTestValue: edmBaseType); + } - [Fact] - public void Property_ActualEdmType_CanBeSameAs_ExpectedEdmType() - { - EdmEntityType edmType = new EdmEntityType("NS", "Entity"); - TestEdmStructuredObject edmObject = new TestEdmStructuredObject(edmType); + [Fact] + public void Property_ActualEdmType_CanBeSameAs_ExpectedEdmType() + { + EdmEntityType edmType = new EdmEntityType("NS", "Entity"); + TestEdmStructuredObject edmObject = new TestEdmStructuredObject(edmType); - edmObject.ActualEdmType = edmType; + edmObject.ActualEdmType = edmType; - Assert.Same(edmType, edmObject.ActualEdmType); - } + Assert.Same(edmType, edmObject.ActualEdmType); + } - [Fact] - public void Property_ExpectedEdmType_CanBeSameAs_ActualEdmType() - { - EdmEntityType edmType = new EdmEntityType("NS", "Entity"); - TestEdmStructuredObject edmObject = new TestEdmStructuredObject(edmType); + [Fact] + public void Property_ExpectedEdmType_CanBeSameAs_ActualEdmType() + { + EdmEntityType edmType = new EdmEntityType("NS", "Entity"); + TestEdmStructuredObject edmObject = new TestEdmStructuredObject(edmType); - edmObject.ExpectedEdmType = edmType; + edmObject.ExpectedEdmType = edmType; - Assert.Same(edmType, edmObject.ExpectedEdmType); - } + Assert.Same(edmType, edmObject.ExpectedEdmType); + } - [Fact] - public void Property_ActualEdmType_ThrowsDeltaEntityTypeNotAssignable() - { - EdmEntityType edmType1 = new EdmEntityType("NS", "Entity1"); - EdmEntityType edmType2 = new EdmEntityType("NS", "Entity2"); - TestEdmStructuredObject edmObject = new TestEdmStructuredObject(edmType1); - - ExceptionAssert.Throws( - () => - { - edmObject.ActualEdmType = edmType2; - }, - "The actual entity type 'NS.Entity2' is not assignable to the expected type 'NS.Entity1'."); - } + [Fact] + public void Property_ActualEdmType_ThrowsDeltaEntityTypeNotAssignable() + { + EdmEntityType edmType1 = new EdmEntityType("NS", "Entity1"); + EdmEntityType edmType2 = new EdmEntityType("NS", "Entity2"); + TestEdmStructuredObject edmObject = new TestEdmStructuredObject(edmType1); - [Fact] - public void Property_ExpectedEdmType_ThrowsDeltaEntityTypeNotAssignable() - { - EdmEntityType edmType1 = new EdmEntityType("NS", "Entity1"); - EdmEntityType edmType2 = new EdmEntityType("NS", "Entity2"); - TestEdmStructuredObject edmObject = new TestEdmStructuredObject(edmType1); - - ExceptionAssert.Throws( - () => - { - edmObject.ExpectedEdmType = edmType2; - }, - "The actual entity type 'NS.Entity1' is not assignable to the expected type 'NS.Entity2'."); - } + ExceptionAssert.Throws( + () => + { + edmObject.ActualEdmType = edmType2; + }, + "The actual entity type 'NS.Entity2' is not assignable to the expected type 'NS.Entity1'."); + } - [Fact] - public void TrySetPropertyValue_ReturnsTrue_IfPropertyExists() - { - EdmComplexType edmType = new EdmComplexType("NS", "Complex"); - edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); - var edmObject = new TestEdmStructuredObject(edmType); + [Fact] + public void Property_ExpectedEdmType_ThrowsDeltaEntityTypeNotAssignable() + { + EdmEntityType edmType1 = new EdmEntityType("NS", "Entity1"); + EdmEntityType edmType2 = new EdmEntityType("NS", "Entity2"); + TestEdmStructuredObject edmObject = new TestEdmStructuredObject(edmType1); - bool result = edmObject.TrySetPropertyValue("Property", 42); + ExceptionAssert.Throws( + () => + { + edmObject.ExpectedEdmType = edmType2; + }, + "The actual entity type 'NS.Entity1' is not assignable to the expected type 'NS.Entity2'."); + } - Assert.True(result); - } + [Fact] + public void TrySetPropertyValue_ReturnsTrue_IfPropertyExists() + { + EdmComplexType edmType = new EdmComplexType("NS", "Complex"); + edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); + var edmObject = new TestEdmStructuredObject(edmType); - [Fact] - public void TrySetPropertyValue_IfPropertyExists_UpdatesGetChangedPropertyNames() - { - EdmComplexType edmType = new EdmComplexType("NS", "Complex"); - edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); - var edmObject = new TestEdmStructuredObject(edmType); - edmObject.TrySetPropertyValue("Property", 42); + bool result = edmObject.TrySetPropertyValue("Property", 42); - Assert.Contains("Property", edmObject.GetChangedPropertyNames()); - } + Assert.True(result); + } - [Fact] - public void TrySetPropertyValue_ReturnsFalse_IfPropertyDoesNotExist() - { - EdmComplexType edmType = new EdmComplexType("NS", "Complex"); - var edmObject = new TestEdmStructuredObject(edmType); + [Fact] + public void TrySetPropertyValue_IfPropertyExists_UpdatesGetChangedPropertyNames() + { + EdmComplexType edmType = new EdmComplexType("NS", "Complex"); + edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); + var edmObject = new TestEdmStructuredObject(edmType); + edmObject.TrySetPropertyValue("Property", 42); - bool result = edmObject.TrySetPropertyValue("NotPresentProperty", 42); + Assert.Contains("Property", edmObject.GetChangedPropertyNames()); + } - Assert.False(result); - } + [Fact] + public void TrySetPropertyValue_ReturnsFalse_IfPropertyDoesNotExist() + { + EdmComplexType edmType = new EdmComplexType("NS", "Complex"); + var edmObject = new TestEdmStructuredObject(edmType); - [Fact] - public void TrySetPropertyValue_IfPropertyDoesNotExist_DoesNotUpdateGetChangedPropertyNames() - { - EdmComplexType edmType = new EdmComplexType("NS", "Complex"); - var edmObject = new TestEdmStructuredObject(edmType); + bool result = edmObject.TrySetPropertyValue("NotPresentProperty", 42); - edmObject.TrySetPropertyValue("NotPresentProperty", 42); + Assert.False(result); + } - Assert.DoesNotContain("Property", edmObject.GetChangedPropertyNames()); - } + [Fact] + public void TrySetPropertyValue_IfPropertyDoesNotExist_DoesNotUpdateGetChangedPropertyNames() + { + EdmComplexType edmType = new EdmComplexType("NS", "Complex"); + var edmObject = new TestEdmStructuredObject(edmType); - [Fact] - public void TryGetPropertyValue_ReturnsTrue_IfPropertyExists() - { - EdmComplexType edmType = new EdmComplexType("NS", "Complex"); - edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); - var edmObject = new TestEdmStructuredObject(edmType); + edmObject.TrySetPropertyValue("NotPresentProperty", 42); - object propertyValue; - bool result = edmObject.TryGetPropertyValue("Property", out propertyValue); + Assert.DoesNotContain("Property", edmObject.GetChangedPropertyNames()); + } - Assert.True(result); - } + [Fact] + public void TryGetPropertyValue_ReturnsTrue_IfPropertyExists() + { + EdmComplexType edmType = new EdmComplexType("NS", "Complex"); + edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); + var edmObject = new TestEdmStructuredObject(edmType); - [Fact] - public void TryGetPropertyValue_ReturnsFalse_IfPropertyDoesNotExist() - { - EdmComplexType edmType = new EdmComplexType("NS", "Complex"); - var edmObject = new TestEdmStructuredObject(edmType); + object propertyValue; + bool result = edmObject.TryGetPropertyValue("Property", out propertyValue); - object propertyValue; - bool result = edmObject.TryGetPropertyValue("NotPresentProperty", out propertyValue); + Assert.True(result); + } - Assert.False(result); - } + [Fact] + public void TryGetPropertyValue_ReturnsFalse_IfPropertyDoesNotExist() + { + EdmComplexType edmType = new EdmComplexType("NS", "Complex"); + var edmObject = new TestEdmStructuredObject(edmType); - [Fact] - public void TryGetPropertyValue_After_TrySetPropertyValue() - { - string propertyName = "Property"; - EdmComplexType edmType = new EdmComplexType("NS", "Complex"); - edmType.AddStructuralProperty(propertyName, EdmPrimitiveTypeKind.Int32); - var edmObject = new TestEdmStructuredObject(edmType); - object propertyValue = new object(); + object propertyValue; + bool result = edmObject.TryGetPropertyValue("NotPresentProperty", out propertyValue); - object result; - edmObject.TrySetPropertyValue(propertyName, propertyValue); - edmObject.TryGetPropertyValue(propertyName, out result); + Assert.False(result); + } - Assert.Same(propertyValue, result); - } + [Fact] + public void TryGetPropertyValue_After_TrySetPropertyValue() + { + string propertyName = "Property"; + EdmComplexType edmType = new EdmComplexType("NS", "Complex"); + edmType.AddStructuralProperty(propertyName, EdmPrimitiveTypeKind.Int32); + var edmObject = new TestEdmStructuredObject(edmType); + object propertyValue = new object(); - [Fact] - public void TryGetPropertyValue_Without_TrySetPropertyValue_ReturnsDefault() - { - string propertyName = "Property"; - EdmComplexType edmType = new EdmComplexType("NS", "Complex"); - edmType.AddStructuralProperty(propertyName, EdmPrimitiveTypeKind.Int32, isNullable: false); - var edmObject = new TestEdmStructuredObject(edmType); + object result; + edmObject.TrySetPropertyValue(propertyName, propertyValue); + edmObject.TryGetPropertyValue(propertyName, out result); - object result; - edmObject.TryGetPropertyValue(propertyName, out result); + Assert.Same(propertyValue, result); + } - Assert.Equal(0, result); - } + [Fact] + public void TryGetPropertyValue_Without_TrySetPropertyValue_ReturnsDefault() + { + string propertyName = "Property"; + EdmComplexType edmType = new EdmComplexType("NS", "Complex"); + edmType.AddStructuralProperty(propertyName, EdmPrimitiveTypeKind.Int32, isNullable: false); + var edmObject = new TestEdmStructuredObject(edmType); + + object result; + edmObject.TryGetPropertyValue(propertyName, out result); - public static TheoryDataSet GetDefaultValueTestData + Assert.Equal(0, result); + } + + public static TheoryDataSet GetDefaultValueTestData + { + get { - get + IEdmTypeReference nonnullableDouble = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, isNullable: false); + IEdmTypeReference nullableDouble = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, isNullable: true); + IEdmTypeReference nullableEntity = new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), isNullable: true); + IEdmTypeReference nullableComplex = new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), isNullable: true); + EdmCollectionTypeReference entityCollection = new EdmCollectionTypeReference(new EdmCollectionType(nullableEntity)); + + return new TheoryDataSet { - IEdmTypeReference nonnullableDouble = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, isNullable: false); - IEdmTypeReference nullableDouble = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, isNullable: true); - IEdmTypeReference nullableEntity = new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), isNullable: true); - IEdmTypeReference nullableComplex = new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), isNullable: true); - EdmCollectionTypeReference entityCollection = new EdmCollectionTypeReference(new EdmCollectionType(nullableEntity)); - - return new TheoryDataSet - { - { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, isNullable: true), null }, - { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, isNullable: false), default(double) }, - { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.DateTimeOffset, isNullable: true), null }, - { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.DateTimeOffset, isNullable: false), default(DateTimeOffset) }, - { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Date, isNullable: true), null }, - { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Date, isNullable: false), default(Date) }, - { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.TimeOfDay, isNullable: true), null }, - { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.TimeOfDay, isNullable: false), default(TimeOfDay) }, - { new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), isNullable: true), null }, - { new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), isNullable: true), null }, - { new EdmCollectionTypeReference(new EdmCollectionType(nullableDouble)), new List() }, - { new EdmCollectionTypeReference(new EdmCollectionType(nonnullableDouble)), new List() }, - { new EdmCollectionTypeReference(new EdmCollectionType(nullableEntity)), new EdmEntityObjectCollection(entityCollection) }, - }; - } + { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, isNullable: true), null }, + { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, isNullable: false), default(double) }, + { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.DateTimeOffset, isNullable: true), null }, + { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.DateTimeOffset, isNullable: false), default(DateTimeOffset) }, + { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Date, isNullable: true), null }, + { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Date, isNullable: false), default(Date) }, + { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.TimeOfDay, isNullable: true), null }, + { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.TimeOfDay, isNullable: false), default(TimeOfDay) }, + { new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), isNullable: true), null }, + { new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), isNullable: true), null }, + { new EdmCollectionTypeReference(new EdmCollectionType(nullableDouble)), new List() }, + { new EdmCollectionTypeReference(new EdmCollectionType(nonnullableDouble)), new List() }, + { new EdmCollectionTypeReference(new EdmCollectionType(nullableEntity)), new EdmEntityObjectCollection(entityCollection) }, + }; } + } - [Theory] - [MemberData(nameof(GetDefaultValueTestData))] - public void GetDefaultValue(IEdmTypeReference edmType, object expectedResult) - { - Assert.Equal(expectedResult, EdmStructuredObject.GetDefaultValue(edmType)); - } + [Theory] + [MemberData(nameof(GetDefaultValueTestData))] + public void GetDefaultValue(IEdmTypeReference edmType, object expectedResult) + { + Assert.Equal(expectedResult, EdmStructuredObject.GetDefaultValue(edmType)); + } - [Fact] - public void GetDefaultValue_NonNullableComplexCollection() - { - IEdmTypeReference elementType = new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), isNullable: true); - IEdmCollectionTypeReference complexCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); + [Fact] + public void GetDefaultValue_NonNullableComplexCollection() + { + IEdmTypeReference elementType = new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), isNullable: true); + IEdmCollectionTypeReference complexCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); - var result = EdmStructuredObject.GetDefaultValue(complexCollectionType); + var result = EdmStructuredObject.GetDefaultValue(complexCollectionType); - var complexCollectionObject = Assert.IsType(result); - Assert.Equal(complexCollectionType, complexCollectionObject.GetEdmType(), new EdmTypeReferenceEqualityComparer()); - } + var complexCollectionObject = Assert.IsType(result); + Assert.Equal(complexCollectionType, complexCollectionObject.GetEdmType(), new EdmTypeReferenceEqualityComparer()); + } - [Fact] - public void GetDefaultValue_NonNullableEntityCollection() - { - IEdmTypeReference elementType = new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), isNullable: true); - IEdmCollectionTypeReference entityCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); + [Fact] + public void GetDefaultValue_NonNullableEntityCollection() + { + IEdmTypeReference elementType = new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), isNullable: true); + IEdmCollectionTypeReference entityCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(elementType)); - var result = EdmStructuredObject.GetDefaultValue(entityCollectionType); + var result = EdmStructuredObject.GetDefaultValue(entityCollectionType); - var entityCollectionObject = Assert.IsType(result); - Assert.Equal(entityCollectionType, entityCollectionObject.GetEdmType(), new EdmTypeReferenceEqualityComparer()); - } + var entityCollectionObject = Assert.IsType(result); + Assert.Equal(entityCollectionType, entityCollectionObject.GetEdmType(), new EdmTypeReferenceEqualityComparer()); + } - [Fact] - public void GetDefaultValue_NonNullableComplex() - { - IEdmTypeReference nonNullableComplexType = new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), isNullable: false); + [Fact] + public void GetDefaultValue_NonNullableComplex() + { + IEdmTypeReference nonNullableComplexType = new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), isNullable: false); - var result = EdmStructuredObject.GetDefaultValue(nonNullableComplexType); + var result = EdmStructuredObject.GetDefaultValue(nonNullableComplexType); - var complexObject = Assert.IsType(result); - Assert.Equal(nonNullableComplexType, complexObject.GetEdmType(), new EdmTypeReferenceEqualityComparer()); - } + var complexObject = Assert.IsType(result); + Assert.Equal(nonNullableComplexType, complexObject.GetEdmType(), new EdmTypeReferenceEqualityComparer()); + } - [Fact] - public void GetDefaultValue_NonNullableEntity() - { - IEdmTypeReference nonNullableEntityType = new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), isNullable: false); + [Fact] + public void GetDefaultValue_NonNullableEntity() + { + IEdmTypeReference nonNullableEntityType = new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), isNullable: false); - var result = EdmStructuredObject.GetDefaultValue(nonNullableEntityType); + var result = EdmStructuredObject.GetDefaultValue(nonNullableEntityType); - var entityObject = Assert.IsType(result); - Assert.Equal(nonNullableEntityType, entityObject.GetEdmType(), new EdmTypeReferenceEqualityComparer()); - } + var entityObject = Assert.IsType(result); + Assert.Equal(nonNullableEntityType, entityObject.GetEdmType(), new EdmTypeReferenceEqualityComparer()); + } - [Fact] - public void TryGetPropertyType_ReturnsTrue_IfPropertyExists() - { - EdmComplexType edmType = new EdmComplexType("NS", "Complex"); - edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); - var edmObject = new TestEdmStructuredObject(edmType); + [Fact] + public void TryGetPropertyType_ReturnsTrue_IfPropertyExists() + { + EdmComplexType edmType = new EdmComplexType("NS", "Complex"); + edmType.AddStructuralProperty("Property", EdmPrimitiveTypeKind.Int32); + var edmObject = new TestEdmStructuredObject(edmType); - Type propertyType; - bool result = edmObject.TryGetPropertyType("Property", out propertyType); + Type propertyType; + bool result = edmObject.TryGetPropertyType("Property", out propertyType); - Assert.True(result); - } + Assert.True(result); + } - [Fact] - public void TryGetPropertyType_ReturnsFalse_IfPropertyDoesNotExist() - { - EdmComplexType edmType = new EdmComplexType("NS", "Complex"); - var edmObject = new TestEdmStructuredObject(edmType); + [Fact] + public void TryGetPropertyType_ReturnsFalse_IfPropertyDoesNotExist() + { + EdmComplexType edmType = new EdmComplexType("NS", "Complex"); + var edmObject = new TestEdmStructuredObject(edmType); - Type propertyType; - bool result = edmObject.TryGetPropertyType("NotPresentProperty", out propertyType); + Type propertyType; + bool result = edmObject.TryGetPropertyType("NotPresentProperty", out propertyType); - Assert.False(result); - } + Assert.False(result); + } - public static TheoryDataSet GetClrTypeForUntypedDeltaTestData + public static TheoryDataSet GetClrTypeForUntypedDeltaTestData + { + get { - get + IEdmTypeReference nullableDouble = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, isNullable: true); + IEdmTypeReference entity = new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), isNullable: true); + IEdmTypeReference complex = new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), isNullable: true); + IEdmTypeReference enumType = new EdmEnumTypeReference(new EdmEnumType("NS", "Enum"), isNullable: true); + + return new TheoryDataSet { - IEdmTypeReference nullableDouble = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, isNullable: true); - IEdmTypeReference entity = new EdmEntityTypeReference(new EdmEntityType("NS", "Entity"), isNullable: true); - IEdmTypeReference complex = new EdmComplexTypeReference(new EdmComplexType("NS", "Complex"), isNullable: true); - IEdmTypeReference enumType = new EdmEnumTypeReference(new EdmEnumType("NS", "Enum"), isNullable: true); - - return new TheoryDataSet - { - { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: true), typeof(int?) }, - { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false), typeof(int) }, - { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: true), typeof(string) }, - { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false), typeof(string) }, - { entity, typeof(EdmEntityObject) }, - { complex, typeof(EdmComplexObject) }, - { enumType, typeof(EdmEnumObject) }, - { new EdmCollectionTypeReference(new EdmCollectionType(entity)), typeof(EdmEntityObjectCollection) }, - { new EdmCollectionTypeReference(new EdmCollectionType(complex)), typeof(EdmComplexObjectCollection) }, - { new EdmCollectionTypeReference(new EdmCollectionType(enumType)), typeof(EdmEnumObjectCollection) }, - { new EdmCollectionTypeReference(new EdmCollectionType(nullableDouble)), typeof(List) } - }; - } + { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: true), typeof(int?) }, + { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false), typeof(int) }, + { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: true), typeof(string) }, + { EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false), typeof(string) }, + { entity, typeof(EdmEntityObject) }, + { complex, typeof(EdmComplexObject) }, + { enumType, typeof(EdmEnumObject) }, + { new EdmCollectionTypeReference(new EdmCollectionType(entity)), typeof(EdmEntityObjectCollection) }, + { new EdmCollectionTypeReference(new EdmCollectionType(complex)), typeof(EdmComplexObjectCollection) }, + { new EdmCollectionTypeReference(new EdmCollectionType(enumType)), typeof(EdmEnumObjectCollection) }, + { new EdmCollectionTypeReference(new EdmCollectionType(nullableDouble)), typeof(List) } + }; } + } - [Theory] - [MemberData(nameof(GetClrTypeForUntypedDeltaTestData))] - public void GetClrTypeForUntypedDelta(IEdmTypeReference edmType, Type expectedType) - { - // Arrange & Act & Assert - Assert.Equal(expectedType, EdmStructuredObject.GetClrTypeForUntypedDelta(edmType)); - } + [Theory] + [MemberData(nameof(GetClrTypeForUntypedDeltaTestData))] + public void GetClrTypeForUntypedDelta(IEdmTypeReference edmType, Type expectedType) + { + // Arrange & Act & Assert + Assert.Equal(expectedType, EdmStructuredObject.GetClrTypeForUntypedDelta(edmType)); + } - [Fact] - public void GetClrTypeForUntypedDelta_ThrowsInvalidOperation_ForUnsupportedTypeKind() - { - // Arrange & Act - IEdmPathTypeReference path = EdmCoreModel.Instance.GetPathType(EdmPathTypeKind.PropertyPath, true); - Action test = () => EdmStructuredObject.GetClrTypeForUntypedDelta(path); - - // Assert - ExceptionAssert.Throws( - () => EdmStructuredObject.GetClrTypeForUntypedDelta(path), - "The EDM type '[Edm.PropertyPath Nullable=True]' of kind 'Path' is not supported."); - } + [Fact] + public void GetClrTypeForUntypedDelta_ThrowsInvalidOperation_ForUnsupportedTypeKind() + { + // Arrange & Act + IEdmPathTypeReference path = EdmCoreModel.Instance.GetPathType(EdmPathTypeKind.PropertyPath, true); + Action test = () => EdmStructuredObject.GetClrTypeForUntypedDelta(path); + + // Assert + ExceptionAssert.Throws( + () => EdmStructuredObject.GetClrTypeForUntypedDelta(path), + "The EDM type '[Edm.PropertyPath Nullable=True]' of kind 'Path' is not supported."); + } - [Fact] - public void GetEdmType_HasSameDefinition_AsInitializedEdmType() - { - var complexType = new EdmComplexType("NS", "Complex"); - var edmObject = new TestEdmStructuredObject(complexType); + [Fact] + public void GetEdmType_HasSameDefinition_AsInitializedEdmType() + { + var complexType = new EdmComplexType("NS", "Complex"); + var edmObject = new TestEdmStructuredObject(complexType); - Assert.Equal(complexType, edmObject.GetEdmType().Definition); - } + Assert.Equal(complexType, edmObject.GetEdmType().Definition); + } - [Fact] - public void GetEdmType_AgreesWithPropertyIsNullable() - { - var complexType = new EdmComplexType("NS", "Complex"); - var edmObject = new TestEdmStructuredObject(complexType); - edmObject.IsNullable = true; + [Fact] + public void GetEdmType_AgreesWithPropertyIsNullable() + { + var complexType = new EdmComplexType("NS", "Complex"); + var edmObject = new TestEdmStructuredObject(complexType); + edmObject.IsNullable = true; - Assert.True(edmObject.GetEdmType().IsNullable); - } + Assert.True(edmObject.GetEdmType().IsNullable); + } - private class TestEdmStructuredObject : EdmStructuredObject + private class TestEdmStructuredObject : EdmStructuredObject + { + public TestEdmStructuredObject(IEdmStructuredType edmType) + : base(edmType) { - public TestEdmStructuredObject(IEdmStructuredType edmType) - : base(edmType) - { - } + } - public TestEdmStructuredObject(IEdmStructuredTypeReference edmType) - : base(edmType) - { - } + public TestEdmStructuredObject(IEdmStructuredTypeReference edmType) + : base(edmType) + { + } - public TestEdmStructuredObject(IEdmStructuredType edmType, bool isNullable) - : base(edmType, isNullable) - { - } + public TestEdmStructuredObject(IEdmStructuredType edmType, bool isNullable) + : base(edmType, isNullable) + { } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmUntypedCollectionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmUntypedCollectionTests.cs index f7e4a71cb..e012d4365 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmUntypedCollectionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmUntypedCollectionTests.cs @@ -9,21 +9,20 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmUntypedCollectionTests { - public class EdmUntypedCollectionTests + [Fact] + public void GetEdmType_OnEdmUntypedCollection_Returns_EdmType() { - [Fact] - public void GetEdmType_OnEdmUntypedCollection_Returns_EdmType() - { - // Arrange - EdmUntypedCollection untypedCollection = new EdmUntypedCollection(); + // Arrange + EdmUntypedCollection untypedCollection = new EdmUntypedCollection(); - // Act - IEdmTypeReference edmType = untypedCollection.GetEdmType(); + // Act + IEdmTypeReference edmType = untypedCollection.GetEdmType(); - // Assert - Assert.Equal("Collection(Edm.Untyped)", edmType.FullName()); - } + // Assert + Assert.Equal("Collection(Edm.Untyped)", edmType.FullName()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmUntypedObjectTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmUntypedObjectTests.cs index 4e6215502..9287d51be 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmUntypedObjectTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/EdmUntypedObjectTests.cs @@ -10,38 +10,37 @@ using NuGet.Frameworks; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class EdmUntypedObjectTests { - public class EdmUntypedObjectTests + [Fact] + public void GetEdmType_OnEdmUntypedObject_Returns_EdmType() { - [Fact] - public void GetEdmType_OnEdmUntypedObject_Returns_EdmType() - { - // Arrange - EdmUntypedObject untypedObj = new EdmUntypedObject(); + // Arrange + EdmUntypedObject untypedObj = new EdmUntypedObject(); - // Act - IEdmTypeReference edmType = untypedObj.GetEdmType(); + // Act + IEdmTypeReference edmType = untypedObj.GetEdmType(); - // Assert - Assert.Equal("Edm.Untyped", edmType.FullName()); - } + // Assert + Assert.Equal("Edm.Untyped", edmType.FullName()); + } - [Fact] - public void TryGetPropertyValue_Returns_ExpectedPropertyValue() + [Fact] + public void TryGetPropertyValue_Returns_ExpectedPropertyValue() + { + EdmUntypedObject untypedObj = new EdmUntypedObject { - EdmUntypedObject untypedObj = new EdmUntypedObject - { - { "any", "anyValue" } - }; - - // Act - Unknown - Assert.False(untypedObj.TryGetPropertyValue("unknown", out object value)); - Assert.Null(value); - - // Act - Known - Assert.True(untypedObj.TryGetPropertyValue("any", out value)); - Assert.Equal("anyValue", value); - } + { "any", "anyValue" } + }; + + // Act - Unknown + Assert.False(untypedObj.TryGetPropertyValue("unknown", out object value)); + Assert.Null(value); + + // Act - Known + Assert.True(untypedObj.TryGetPropertyValue("any", out value)); + Assert.Equal("anyValue", value); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/NullEdmComplexObjectTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/NullEdmComplexObjectTests.cs index 877b44780..0313a2d6e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/NullEdmComplexObjectTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Value/NullEdmComplexObjectTests.cs @@ -12,39 +12,38 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Value; + +public class NullEdmComplexObjectTests { - public class NullEdmComplexObjectTests + [Fact] + public void CtorNullEdmComplexObject_ThrowsArgumentNull_EdmType() { - [Fact] - public void CtorNullEdmComplexObject_ThrowsArgumentNull_EdmType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new NullEdmComplexObject(null), "edmType"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new NullEdmComplexObject(null), "edmType"); + } - [Fact] - public void TryGetPropertyValue_ThrowsInvalidOperation() - { - // Arrange & Act & Assert - EdmComplexType complex = new EdmComplexType("NS", "Complex"); - IEdmComplexTypeReference typeRef = new EdmComplexTypeReference(complex, false); - NullEdmComplexObject obj = new NullEdmComplexObject(typeRef); - ExceptionAssert.Throws(() => obj.TryGetPropertyValue("name", out object value), - "Cannot get property 'name' of a null EDM object of type '[NS.Complex Nullable=False]'."); - } + [Fact] + public void TryGetPropertyValue_ThrowsInvalidOperation() + { + // Arrange & Act & Assert + EdmComplexType complex = new EdmComplexType("NS", "Complex"); + IEdmComplexTypeReference typeRef = new EdmComplexTypeReference(complex, false); + NullEdmComplexObject obj = new NullEdmComplexObject(typeRef); + ExceptionAssert.Throws(() => obj.TryGetPropertyValue("name", out object value), + "Cannot get property 'name' of a null EDM object of type '[NS.Complex Nullable=False]'."); + } - [Fact] - public void GetEdmType_ReturnsInputTypeReference() - { - // Arrange & Act - IEdmComplexTypeReference typeRef = new Mock().Object; - NullEdmComplexObject obj = new NullEdmComplexObject(typeRef); + [Fact] + public void GetEdmType_ReturnsInputTypeReference() + { + // Arrange & Act + IEdmComplexTypeReference typeRef = new Mock().Object; + NullEdmComplexObject obj = new NullEdmComplexObject(typeRef); - IEdmTypeReference actual = obj.GetEdmType(); + IEdmTypeReference actual = obj.GetEdmType(); - // Assert - Assert.Same(typeRef, actual); - } + // Assert + Assert.Same(typeRef, actual); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataPrimitiveWrapperTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataPrimitiveWrapperTests.cs index 0a9283d9c..3479205bc 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataPrimitiveWrapperTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataPrimitiveWrapperTests.cs @@ -10,28 +10,27 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Wrapper; + +public class ODataPrimitiveWrapperTests { - public class ODataPrimitiveWrapperTests + [Fact] + public void Ctor_ThrowsArgumentNull_PrimitiveValue() { - [Fact] - public void Ctor_ThrowsArgumentNull_PrimitiveValue() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataPrimitiveWrapper(null), "value"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataPrimitiveWrapper(null), "value"); + } - [Fact] - public void Ctor_Sets_CorrectProperties() - { - // Arrange & Act & Assert - ODataPrimitiveValue oDataPrimitiveValue = new ODataPrimitiveValue(42); + [Fact] + public void Ctor_Sets_CorrectProperties() + { + // Arrange & Act & Assert + ODataPrimitiveValue oDataPrimitiveValue = new ODataPrimitiveValue(42); - // Act - ODataPrimitiveWrapper wrapper = new ODataPrimitiveWrapper(oDataPrimitiveValue); + // Act + ODataPrimitiveWrapper wrapper = new ODataPrimitiveWrapper(oDataPrimitiveValue); - // Assert - Assert.Same(oDataPrimitiveValue, wrapper.Value); - } + // Assert + Assert.Same(oDataPrimitiveValue, wrapper.Value); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataReaderExtensionsTests.Untyped.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataReaderExtensionsTests.Untyped.cs index a6bef5430..4cc90b809 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataReaderExtensionsTests.Untyped.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataReaderExtensionsTests.Untyped.cs @@ -14,459 +14,458 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Wrapper; + +/// +/// Here contains reading test cases for 'Edm.Untyped' (Declared or undeclared) +/// +public partial class ODataReaderExtensionsTests { - /// - /// Here contains reading test cases for 'Edm.Untyped' (Declared or undeclared) - /// - public partial class ODataReaderExtensionsTests + private static readonly EdmModel UntypedModel = BuildUntypedModel(); + + private static EdmModel BuildUntypedModel() { - private static readonly EdmModel UntypedModel = BuildUntypedModel(); + EdmModel model = new EdmModel(); + + // Open Complex Type (Info) + EdmComplexType info = new EdmComplexType("NS", "Info", null, false, isOpen: true); + model.AddElement(info); + + // Open Entity Type (Person) + /* + + + + + + + */ + EdmEntityType person = new EdmEntityType("NS", "Person", null, false, isOpen: true); + model.AddElement(person); + person.AddKeys(person.AddStructuralProperty("ID", EdmCoreModel.Instance.GetInt32(false))); + person.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(true)); + person.AddStructuralProperty("Data", EdmCoreModel.Instance.GetUntyped(true)); + person.AddStructuralProperty("Infos", new EdmCollectionTypeReference + (new EdmCollectionType(new EdmComplexTypeReference(info, false)))); + + EdmEntityContainer defaultContainer = new EdmEntityContainer("NS", "Container"); + model.AddElement(defaultContainer); + EdmEntitySet people = defaultContainer.AddEntitySet("People", person); + return model; + } - private static EdmModel BuildUntypedModel() + public static TheoryDataSet PrimitiveValueCases + { + get { - EdmModel model = new EdmModel(); - - // Open Complex Type (Info) - EdmComplexType info = new EdmComplexType("NS", "Info", null, false, isOpen: true); - model.AddElement(info); - - // Open Entity Type (Person) - /* - - - - - - - */ - EdmEntityType person = new EdmEntityType("NS", "Person", null, false, isOpen: true); - model.AddElement(person); - person.AddKeys(person.AddStructuralProperty("ID", EdmCoreModel.Instance.GetInt32(false))); - person.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(true)); - person.AddStructuralProperty("Data", EdmCoreModel.Instance.GetUntyped(true)); - person.AddStructuralProperty("Infos", new EdmCollectionTypeReference - (new EdmCollectionType(new EdmComplexTypeReference(info, false)))); - - EdmEntityContainer defaultContainer = new EdmEntityContainer("NS", "Container"); - model.AddElement(defaultContainer); - EdmEntitySet people = defaultContainer.AddEntitySet("People", person); - return model; + var data = new TheoryDataSet + { + { "42", (decimal)42 }, // why it's decimal? see https://github.com/OData/odata.net/issues/2657 + { "9.19", (decimal)9.19 }, + { "3.1415926535897931", 3.1415926535897931m }, + { "true", true }, + { "false", false }, + { "\"false\"", "false" }, + { "\"a simple string\"", "a simple string" }, + + // looks like a Guid string, but it's string + { "\"969D9BB3-1A1D-40C7-8716-4E29CFE02E81\"", "969D9BB3-1A1D-40C7-8716-4E29CFE02E81" }, + { "\"Red\"", "Red" }, // looks like a Enum string, but it's string + { "null", null } + }; + + return data; } + } - public static TheoryDataSet PrimitiveValueCases - { - get + [Theory] + [MemberData(nameof(PrimitiveValueCases))] + public async Task ReadOpenResource_WithUntypedPrimitiveValue_WorksAsExpected(string value, object expect) + { + // Arrange + string payload = + "{" + + "\"ID\": 17," + + $"\"Data\": {value}," + + $"\"Dynamic\": {value}" + + "}"; + + IEdmEntitySet people = UntypedModel.EntityContainer.FindEntitySet("People"); + Assert.NotNull(people); // Guard + + // Act + Func> func = mr => mr.CreateODataResourceReaderAsync(people, people.EntityType); + ODataItemWrapper item = await ReadUntypedPayloadAsync(payload, UntypedModel, func); + + // Assert + Assert.NotNull(item); + ODataResourceWrapper resourceWrapper = Assert.IsType(item); + Assert.Empty(resourceWrapper.NestedResourceInfos); + + Assert.Collection(resourceWrapper.Resource.Properties.OfType(), + p => { - var data = new TheoryDataSet - { - { "42", (decimal)42 }, // why it's decimal? see https://github.com/OData/odata.net/issues/2657 - { "9.19", (decimal)9.19 }, - { "3.1415926535897931", 3.1415926535897931m }, - { "true", true }, - { "false", false }, - { "\"false\"", "false" }, - { "\"a simple string\"", "a simple string" }, - - // looks like a Guid string, but it's string - { "\"969D9BB3-1A1D-40C7-8716-4E29CFE02E81\"", "969D9BB3-1A1D-40C7-8716-4E29CFE02E81" }, - { "\"Red\"", "Red" }, // looks like a Enum string, but it's string - { "null", null } - }; - - return data; - } - } + // Declared property with 'Edm.Int32' + Assert.Equal("ID", p.Name); + Assert.Equal(17, p.Value); + }, + p => + { + // Declared property with 'Edm.Untyped' + Assert.Equal("Data", p.Name); + Assert.Equal(expect, p.Value); + }, + p => + { + // Un-declared (dynamic) property + Assert.Equal("Dynamic", p.Name); + Assert.Equal(expect, p.Value); + }); + } - [Theory] - [MemberData(nameof(PrimitiveValueCases))] - public async Task ReadOpenResource_WithUntypedPrimitiveValue_WorksAsExpected(string value, object expect) + [Fact] + public async Task ReadOpenResource_WithUntypedResourceValue_WorksAsExpected() + { + // Arrange + string resourceValue = "{\"Key1\": \"Value1\", \"Key2\": true}"; + string payload = + "{" + + "\"ID\": 17," + + $"\"Data\": {resourceValue}," + + $"\"Dynamic\": {resourceValue}" + + "}"; + + IEdmEntitySet people = UntypedModel.EntityContainer.FindEntitySet("People"); + Assert.NotNull(people); // Guard + + // Act + Func> func = mr => mr.CreateODataResourceReaderAsync(people, people.EntityType); + ODataItemWrapper item = await ReadUntypedPayloadAsync(payload, UntypedModel, func); + + // Assert + Assert.NotNull(item); + ODataResourceWrapper resourceWrapper = Assert.IsType(item); + + ODataProperty property = Assert.IsType(Assert.Single(resourceWrapper.Resource.Properties)); + // Declared property with 'Edm.Int32' + Assert.Equal("ID", property.Name); + Assert.Equal(17, property.Value); + + Assert.Equal(2, resourceWrapper.NestedResourceInfos.Count); + + Action verifyAction = (p, n) => { - // Arrange - string payload = - "{" + - "\"ID\": 17," + - $"\"Data\": {value}," + - $"\"Dynamic\": {value}" + - "}"; - - IEdmEntitySet people = UntypedModel.EntityContainer.FindEntitySet("People"); - Assert.NotNull(people); // Guard - - // Act - Func> func = mr => mr.CreateODataResourceReaderAsync(people, people.EntityType); - ODataItemWrapper item = await ReadUntypedPayloadAsync(payload, UntypedModel, func); - - // Assert - Assert.NotNull(item); - ODataResourceWrapper resourceWrapper = Assert.IsType(item); - Assert.Empty(resourceWrapper.NestedResourceInfos); - - Assert.Collection(resourceWrapper.Resource.Properties.OfType(), - p => - { - // Declared property with 'Edm.Int32' - Assert.Equal("ID", p.Name); - Assert.Equal(17, p.Value); - }, + Assert.Equal(n, p.NestedResourceInfo.Name); + ODataResourceWrapper nestedResourceWrapper = Assert.IsType(Assert.Single(p.NestedItems)); + Assert.Empty(nestedResourceWrapper.NestedResourceInfos); + Assert.Collection(nestedResourceWrapper.Resource.Properties.OfType(), p => { - // Declared property with 'Edm.Untyped' - Assert.Equal("Data", p.Name); - Assert.Equal(expect, p.Value); + Assert.Equal("Key1", p.Name); + Assert.Equal("Value1", p.Value); }, p => { - // Un-declared (dynamic) property - Assert.Equal("Dynamic", p.Name); - Assert.Equal(expect, p.Value); + Assert.Equal("Key2", p.Name); + Assert.True((bool)p.Value); }); - } + }; - [Fact] - public async Task ReadOpenResource_WithUntypedResourceValue_WorksAsExpected() - { - // Arrange - string resourceValue = "{\"Key1\": \"Value1\", \"Key2\": true}"; - string payload = - "{" + - "\"ID\": 17," + - $"\"Data\": {resourceValue}," + - $"\"Dynamic\": {resourceValue}" + - "}"; - - IEdmEntitySet people = UntypedModel.EntityContainer.FindEntitySet("People"); - Assert.NotNull(people); // Guard - - // Act - Func> func = mr => mr.CreateODataResourceReaderAsync(people, people.EntityType); - ODataItemWrapper item = await ReadUntypedPayloadAsync(payload, UntypedModel, func); - - // Assert - Assert.NotNull(item); - ODataResourceWrapper resourceWrapper = Assert.IsType(item); - - ODataProperty property = Assert.IsType(Assert.Single(resourceWrapper.Resource.Properties)); - // Declared property with 'Edm.Int32' - Assert.Equal("ID", property.Name); - Assert.Equal(17, property.Value); - - Assert.Equal(2, resourceWrapper.NestedResourceInfos.Count); - - Action verifyAction = (p, n) => + Assert.Collection(resourceWrapper.NestedResourceInfos, + p => { - Assert.Equal(n, p.NestedResourceInfo.Name); - ODataResourceWrapper nestedResourceWrapper = Assert.IsType(Assert.Single(p.NestedItems)); - Assert.Empty(nestedResourceWrapper.NestedResourceInfos); - Assert.Collection(nestedResourceWrapper.Resource.Properties.OfType(), - p => - { - Assert.Equal("Key1", p.Name); - Assert.Equal("Value1", p.Value); - }, - p => - { - Assert.Equal("Key2", p.Name); - Assert.True((bool)p.Value); - }); - }; + // Declared property with 'Edm.Untyped' + verifyAction(p, "Data"); + }, + p => + { + // Un-declared (dynamic) property + verifyAction(p, "Dynamic"); + }); + } - Assert.Collection(resourceWrapper.NestedResourceInfos, + [Fact] + public async Task ReadOpenResource_WithUntypedCollectionValue_WorksAsExpected() + { + // Arrange + string collectionValue = "[" + + "null," + + "{\"Key1\": \"Value1\", \"Key2\": true}," + + "\"A string item\"," + + "42" + + "]"; + + string payload = + "{" + + "\"ID\": 17," + + $"\"Data\": {collectionValue}," + + $"\"Dynamic\": {collectionValue}" + + "}"; + + IEdmEntitySet people = UntypedModel.EntityContainer.FindEntitySet("People"); + Assert.NotNull(people); // Guard + + // Act + Func> func = mr => mr.CreateODataResourceReaderAsync(people, people.EntityType); + ODataItemWrapper item = await ReadUntypedPayloadAsync(payload, UntypedModel, func); + + // Assert + Assert.NotNull(item); + ODataResourceWrapper resourceWrapper = Assert.IsType(item); + + ODataProperty property = Assert.IsType(Assert.Single(resourceWrapper.Resource.Properties)); + // Declared property with 'Edm.Int32' + Assert.Equal("ID", property.Name); + Assert.Equal(17, property.Value); + + Assert.Equal(2, resourceWrapper.NestedResourceInfos.Count); + + Action verifyAction = (p, n) => + { + Assert.Equal(n, p.NestedResourceInfo.Name); + ODataResourceSetWrapper nestedResourceSetWrapper = Assert.IsType(Assert.Single(p.NestedItems)); + Assert.Equal(2, nestedResourceSetWrapper.Resources.Count); + + Assert.Collection(nestedResourceSetWrapper.Resources, p => { - // Declared property with 'Edm.Untyped' - verifyAction(p, "Data"); + Assert.Null(p); // since it's a 'null' value in collection, + // ODL reads it as ResourceStart, ResourceEnd with null item. }, p => { - // Un-declared (dynamic) property - verifyAction(p, "Dynamic"); + Assert.Empty(p.NestedResourceInfos); + Assert.NotNull(p.Resource); + Assert.Collection(p.Resource.Properties.OfType(), + p => + { + Assert.Equal("Key1", p.Name); + Assert.Equal("Value1", p.Value); + }, + p => + { + Assert.Equal("Key2", p.Name); + Assert.True((bool)p.Value); + }); }); - } - [Fact] - public async Task ReadOpenResource_WithUntypedCollectionValue_WorksAsExpected() - { - // Arrange - string collectionValue = "[" + - "null," + - "{\"Key1\": \"Value1\", \"Key2\": true}," + - "\"A string item\"," + - "42" + - "]"; - - string payload = - "{" + - "\"ID\": 17," + - $"\"Data\": {collectionValue}," + - $"\"Dynamic\": {collectionValue}" + - "}"; - - IEdmEntitySet people = UntypedModel.EntityContainer.FindEntitySet("People"); - Assert.NotNull(people); // Guard - - // Act - Func> func = mr => mr.CreateODataResourceReaderAsync(people, people.EntityType); - ODataItemWrapper item = await ReadUntypedPayloadAsync(payload, UntypedModel, func); - - // Assert - Assert.NotNull(item); - ODataResourceWrapper resourceWrapper = Assert.IsType(item); - - ODataProperty property = Assert.IsType(Assert.Single(resourceWrapper.Resource.Properties)); - // Declared property with 'Edm.Int32' - Assert.Equal("ID", property.Name); - Assert.Equal(17, property.Value); - - Assert.Equal(2, resourceWrapper.NestedResourceInfos.Count); - - Action verifyAction = (p, n) => - { - Assert.Equal(n, p.NestedResourceInfo.Name); - ODataResourceSetWrapper nestedResourceSetWrapper = Assert.IsType(Assert.Single(p.NestedItems)); - Assert.Equal(2, nestedResourceSetWrapper.Resources.Count); - - Assert.Collection(nestedResourceSetWrapper.Resources, - p => - { - Assert.Null(p); // since it's a 'null' value in collection, - // ODL reads it as ResourceStart, ResourceEnd with null item. - }, - p => - { - Assert.Empty(p.NestedResourceInfos); - Assert.NotNull(p.Resource); - Assert.Collection(p.Resource.Properties.OfType(), - p => - { - Assert.Equal("Key1", p.Name); - Assert.Equal("Value1", p.Value); - }, - p => - { - Assert.Equal("Key2", p.Name); - Assert.True((bool)p.Value); - }); - }); - - Assert.Equal(4, nestedResourceSetWrapper.Items.Count); - Assert.Collection(nestedResourceSetWrapper.Items, - p => - { - Assert.Null(p); // since it's a 'null' value in collection, - // ODL reads it as ResourceStart, ResourceEnd with null item. - }, - p => - { - ODataResourceWrapper resourceWrapper = Assert.IsType(p); - Assert.Empty(resourceWrapper.NestedResourceInfos); - Assert.NotNull(resourceWrapper.Resource); - Assert.Collection(resourceWrapper.Resource.Properties.OfType(), - p => - { - Assert.Equal("Key1", p.Name); - Assert.Equal("Value1", p.Value); - }, - p => - { - Assert.Equal("Key2", p.Name); - Assert.True((bool)p.Value); - }); - }, - p => - { - ODataPrimitiveWrapper primitiveWrapper = Assert.IsType(p); - Assert.NotNull(primitiveWrapper.Value); - Assert.Equal("A string item", primitiveWrapper.Value.Value); - }, - p => - { - ODataPrimitiveWrapper primitiveWrapper = Assert.IsType(p); - Assert.NotNull(primitiveWrapper.Value); - Assert.Equal(42, primitiveWrapper.Value.Value); - }); - }; - - Assert.Collection(resourceWrapper.NestedResourceInfos, + Assert.Equal(4, nestedResourceSetWrapper.Items.Count); + Assert.Collection(nestedResourceSetWrapper.Items, + p => + { + Assert.Null(p); // since it's a 'null' value in collection, + // ODL reads it as ResourceStart, ResourceEnd with null item. + }, p => { - // Declared property with 'Edm.Untyped' - verifyAction(p, "Data"); + ODataResourceWrapper resourceWrapper = Assert.IsType(p); + Assert.Empty(resourceWrapper.NestedResourceInfos); + Assert.NotNull(resourceWrapper.Resource); + Assert.Collection(resourceWrapper.Resource.Properties.OfType(), + p => + { + Assert.Equal("Key1", p.Name); + Assert.Equal("Value1", p.Value); + }, + p => + { + Assert.Equal("Key2", p.Name); + Assert.True((bool)p.Value); + }); }, p => { - // Un-declared (dynamic) property - verifyAction(p, "Dynamic"); + ODataPrimitiveWrapper primitiveWrapper = Assert.IsType(p); + Assert.NotNull(primitiveWrapper.Value); + Assert.Equal("A string item", primitiveWrapper.Value.Value); + }, + p => + { + ODataPrimitiveWrapper primitiveWrapper = Assert.IsType(p); + Assert.NotNull(primitiveWrapper.Value); + Assert.Equal(42, primitiveWrapper.Value.Value); }); - } + }; - [Fact] - public async Task ReadOpenResource_WithUntypedCollectionValue_OnNestedResourceInfo_WorksAsExpected() - { - // Arrange - string collectionValue = "[" + - "{}," + - "{\"Key1\": \"Value1\"}," + - "{\"Key2\": {}}," + - "{\"Key3\": {\"@odata.type\":\"NS.Info\"}}," + - "{\"Key4\": []}" + - "]"; + Assert.Collection(resourceWrapper.NestedResourceInfos, + p => + { + // Declared property with 'Edm.Untyped' + verifyAction(p, "Data"); + }, + p => + { + // Un-declared (dynamic) property + verifyAction(p, "Dynamic"); + }); + } - string payload = - "{" + - $"\"Infos\": {collectionValue}" + - "}"; + [Fact] + public async Task ReadOpenResource_WithUntypedCollectionValue_OnNestedResourceInfo_WorksAsExpected() + { + // Arrange + string collectionValue = "[" + + "{}," + + "{\"Key1\": \"Value1\"}," + + "{\"Key2\": {}}," + + "{\"Key3\": {\"@odata.type\":\"NS.Info\"}}," + + "{\"Key4\": []}" + + "]"; - IEdmEntitySet people = UntypedModel.EntityContainer.FindEntitySet("People"); - Assert.NotNull(people); // Guard + string payload = + "{" + + $"\"Infos\": {collectionValue}" + + "}"; - // Act - Func> func = mr => mr.CreateODataResourceReaderAsync(people, people.EntityType); - ODataItemWrapper item = await ReadUntypedPayloadAsync(payload, UntypedModel, func); + IEdmEntitySet people = UntypedModel.EntityContainer.FindEntitySet("People"); + Assert.NotNull(people); // Guard - // Assert - Assert.NotNull(item); - ODataResourceWrapper resourceWrapper = Assert.IsType(item); + // Act + Func> func = mr => mr.CreateODataResourceReaderAsync(people, people.EntityType); + ODataItemWrapper item = await ReadUntypedPayloadAsync(payload, UntypedModel, func); - Assert.Empty(resourceWrapper.Resource.Properties); + // Assert + Assert.NotNull(item); + ODataResourceWrapper resourceWrapper = Assert.IsType(item); - ODataNestedResourceInfoWrapper nestedResourceInfoWrapper = Assert.Single(resourceWrapper.NestedResourceInfos); - Assert.Equal("Infos", nestedResourceInfoWrapper.NestedResourceInfo.Name); + Assert.Empty(resourceWrapper.Resource.Properties); - ODataResourceSetWrapper nestedResourceSetWrapper = Assert.IsType - (Assert.Single(nestedResourceInfoWrapper.NestedItems)); + ODataNestedResourceInfoWrapper nestedResourceInfoWrapper = Assert.Single(resourceWrapper.NestedResourceInfos); + Assert.Equal("Infos", nestedResourceInfoWrapper.NestedResourceInfo.Name); - Assert.Collection(nestedResourceSetWrapper.Resources, - p => - { - Assert.Equal("NS.Info", p.Resource.TypeName); + ODataResourceSetWrapper nestedResourceSetWrapper = Assert.IsType + (Assert.Single(nestedResourceInfoWrapper.NestedItems)); - Assert.Empty(p.Resource.Properties); - Assert.Empty(p.NestedResourceInfos); - }, - p => - { - Assert.Equal("NS.Info", p.Resource.TypeName); + Assert.Collection(nestedResourceSetWrapper.Resources, + p => + { + Assert.Equal("NS.Info", p.Resource.TypeName); - Assert.Empty(p.NestedResourceInfos); - ODataProperty property = Assert.IsType(Assert.Single(p.Resource.Properties)); - Assert.Equal("Key1", property.Name); - Assert.Equal("Value1", property.Value); - }, - p => - { - Assert.Equal("NS.Info", p.Resource.TypeName); - Assert.Empty(p.Resource.Properties); + Assert.Empty(p.Resource.Properties); + Assert.Empty(p.NestedResourceInfos); + }, + p => + { + Assert.Equal("NS.Info", p.Resource.TypeName); + + Assert.Empty(p.NestedResourceInfos); + ODataProperty property = Assert.IsType(Assert.Single(p.Resource.Properties)); + Assert.Equal("Key1", property.Name); + Assert.Equal("Value1", property.Value); + }, + p => + { + Assert.Equal("NS.Info", p.Resource.TypeName); + Assert.Empty(p.Resource.Properties); - ODataNestedResourceInfoWrapper nestedInfoWrapper = Assert.Single(p.NestedResourceInfos); - Assert.Equal("Key2", nestedInfoWrapper.NestedResourceInfo.Name); + ODataNestedResourceInfoWrapper nestedInfoWrapper = Assert.Single(p.NestedResourceInfos); + Assert.Equal("Key2", nestedInfoWrapper.NestedResourceInfo.Name); - ODataResourceWrapper nestedResourceWrapper = Assert.IsType(Assert.Single(nestedInfoWrapper.NestedItems)); - Assert.Equal("Edm.Untyped", nestedResourceWrapper.Resource.TypeName); - Assert.Empty(nestedResourceWrapper.Resource.Properties); - Assert.Empty(nestedResourceWrapper.NestedResourceInfos); - }, - p => - { - Assert.Equal("NS.Info", p.Resource.TypeName); - Assert.Empty(p.Resource.Properties); + ODataResourceWrapper nestedResourceWrapper = Assert.IsType(Assert.Single(nestedInfoWrapper.NestedItems)); + Assert.Equal("Edm.Untyped", nestedResourceWrapper.Resource.TypeName); + Assert.Empty(nestedResourceWrapper.Resource.Properties); + Assert.Empty(nestedResourceWrapper.NestedResourceInfos); + }, + p => + { + Assert.Equal("NS.Info", p.Resource.TypeName); + Assert.Empty(p.Resource.Properties); - ODataNestedResourceInfoWrapper nestedInfoWrapper = Assert.Single(p.NestedResourceInfos); - Assert.Equal("Key3", nestedInfoWrapper.NestedResourceInfo.Name); + ODataNestedResourceInfoWrapper nestedInfoWrapper = Assert.Single(p.NestedResourceInfos); + Assert.Equal("Key3", nestedInfoWrapper.NestedResourceInfo.Name); - ODataResourceWrapper nestedResourceWrapper = Assert.IsType(Assert.Single(nestedInfoWrapper.NestedItems)); - Assert.Equal("NS.Info", nestedResourceWrapper.Resource.TypeName); - Assert.Empty(nestedResourceWrapper.Resource.Properties); - Assert.Empty(nestedResourceWrapper.NestedResourceInfos); - }, - p => - { - Assert.Equal("NS.Info", p.Resource.TypeName); - Assert.Empty(p.Resource.Properties); + ODataResourceWrapper nestedResourceWrapper = Assert.IsType(Assert.Single(nestedInfoWrapper.NestedItems)); + Assert.Equal("NS.Info", nestedResourceWrapper.Resource.TypeName); + Assert.Empty(nestedResourceWrapper.Resource.Properties); + Assert.Empty(nestedResourceWrapper.NestedResourceInfos); + }, + p => + { + Assert.Equal("NS.Info", p.Resource.TypeName); + Assert.Empty(p.Resource.Properties); - ODataNestedResourceInfoWrapper nestedInfoWrapper = Assert.Single(p.NestedResourceInfos); - Assert.Equal("Key4", nestedInfoWrapper.NestedResourceInfo.Name); + ODataNestedResourceInfoWrapper nestedInfoWrapper = Assert.Single(p.NestedResourceInfos); + Assert.Equal("Key4", nestedInfoWrapper.NestedResourceInfo.Name); - ODataResourceSetWrapper nestedResourceSetWrapper = Assert.IsType(Assert.Single(nestedInfoWrapper.NestedItems)); - Assert.Empty(nestedResourceSetWrapper.Resources); - }); - } + ODataResourceSetWrapper nestedResourceSetWrapper = Assert.IsType(Assert.Single(nestedInfoWrapper.NestedItems)); + Assert.Empty(nestedResourceSetWrapper.Resources); + }); + } - [Fact] - public async Task ReadOpenResource_WithDeepUntypedCollectionValue_WorksAsExpected() + [Fact] + public async Task ReadOpenResource_WithDeepUntypedCollectionValue_WorksAsExpected() + { + // Arrange + const int Depth = 10; + string collectionValue = "[42]"; + for (int i = 0; i < Depth; i++) { - // Arrange - const int Depth = 10; - string collectionValue = "[42]"; - for (int i = 0; i < Depth; i++) - { - collectionValue = $"[42, {collectionValue}]"; - } - // [42, [42, [42, [42, [42, [42, [42, [42, [42, [42, [42]]]]]]]]]]] + collectionValue = $"[42, {collectionValue}]"; + } + // [42, [42, [42, [42, [42, [42, [42, [42, [42, [42, [42]]]]]]]]]]] - string payload = - "{" + - $"\"Data\": {collectionValue}," + - $"\"Dynamic\": {collectionValue}" + - "}"; + string payload = + "{" + + $"\"Data\": {collectionValue}," + + $"\"Dynamic\": {collectionValue}" + + "}"; - IEdmEntitySet people = UntypedModel.EntityContainer.FindEntitySet("People"); - Assert.NotNull(people); // Guard + IEdmEntitySet people = UntypedModel.EntityContainer.FindEntitySet("People"); + Assert.NotNull(people); // Guard - // Act - Func> func = mr => mr.CreateODataResourceReaderAsync(people, people.EntityType); - ODataItemWrapper item = await ReadUntypedPayloadAsync(payload, UntypedModel, func); + // Act + Func> func = mr => mr.CreateODataResourceReaderAsync(people, people.EntityType); + ODataItemWrapper item = await ReadUntypedPayloadAsync(payload, UntypedModel, func); - // Assert - Assert.NotNull(item); - ODataResourceWrapper resourceWrapper = Assert.IsType(item); + // Assert + Assert.NotNull(item); + ODataResourceWrapper resourceWrapper = Assert.IsType(item); - Assert.Empty(resourceWrapper.Resource.Properties); - Assert.Equal(2, resourceWrapper.NestedResourceInfos.Count); + Assert.Empty(resourceWrapper.Resource.Properties); + Assert.Equal(2, resourceWrapper.NestedResourceInfos.Count); - Action verifyAction = (p, n) => - { - Assert.Equal(n, p.NestedResourceInfo.Name); - ODataResourceSetWrapper nestedResourceSetWrapper = Assert.IsType(Assert.Single(p.NestedItems)); + Action verifyAction = (p, n) => + { + Assert.Equal(n, p.NestedResourceInfo.Name); + ODataResourceSetWrapper nestedResourceSetWrapper = Assert.IsType(Assert.Single(p.NestedItems)); - for (int i = 0; i <= Depth; ++i) // Why '<= Depth'? Because we have extra single primitive (42) in the deepest level. + for (int i = 0; i <= Depth; ++i) // Why '<= Depth'? Because we have extra single primitive (42) in the deepest level. + { + Assert.Empty(nestedResourceSetWrapper.Resources); + ODataPrimitiveWrapper primitiveWrapper; + if (i == Depth) { - Assert.Empty(nestedResourceSetWrapper.Resources); - ODataPrimitiveWrapper primitiveWrapper; - if (i == Depth) - { - primitiveWrapper = Assert.IsType(Assert.Single(nestedResourceSetWrapper.Items)); - } - else - { - Assert.Equal(2, nestedResourceSetWrapper.Items.Count); - primitiveWrapper = Assert.IsType(nestedResourceSetWrapper.Items.First()); - nestedResourceSetWrapper = Assert.IsType(nestedResourceSetWrapper.Items.Last()); - } - - Assert.NotNull(primitiveWrapper.Value); - Assert.Equal(42, primitiveWrapper.Value.Value); // in the collection, ODL reads 42 as integer. It's different with 42 in untyped single value property + primitiveWrapper = Assert.IsType(Assert.Single(nestedResourceSetWrapper.Items)); } - }; - - Assert.Collection(resourceWrapper.NestedResourceInfos, - p => - { - // Declared property with 'Edm.Untyped' - verifyAction(p, "Data"); - }, - p => + else { - // Un-declared (dynamic) property - verifyAction(p, "Dynamic"); - }); - } + Assert.Equal(2, nestedResourceSetWrapper.Items.Count); + primitiveWrapper = Assert.IsType(nestedResourceSetWrapper.Items.First()); + nestedResourceSetWrapper = Assert.IsType(nestedResourceSetWrapper.Items.Last()); + } - private async Task ReadUntypedPayloadAsync(string payload, - IEdmModel edmModel, Func> createReader, - bool readUntypedAsString = false) - { - return await ReadPayloadAsync(payload, edmModel, createReader, ODataVersion.V4, readUntypedAsString); - } + Assert.NotNull(primitiveWrapper.Value); + Assert.Equal(42, primitiveWrapper.Value.Value); // in the collection, ODL reads 42 as integer. It's different with 42 in untyped single value property + } + }; + + Assert.Collection(resourceWrapper.NestedResourceInfos, + p => + { + // Declared property with 'Edm.Untyped' + verifyAction(p, "Data"); + }, + p => + { + // Un-declared (dynamic) property + verifyAction(p, "Dynamic"); + }); + } + + private async Task ReadUntypedPayloadAsync(string payload, + IEdmModel edmModel, Func> createReader, + bool readUntypedAsString = false) + { + return await ReadPayloadAsync(payload, edmModel, createReader, ODataVersion.V4, readUntypedAsString); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataReaderExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataReaderExtensionsTests.cs index 04d86f4c4..18d73a12a 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataReaderExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Formatter/Wrapper/ODataReaderExtensionsTests.cs @@ -16,588 +16,587 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Formatter.Wrapper +namespace Microsoft.AspNetCore.OData.Tests.Formatter.Wrapper; + +public partial class ODataReaderExtensionsTests { - public partial class ODataReaderExtensionsTests + private static readonly EdmModel Model; + + static ODataReaderExtensionsTests() { - private static readonly EdmModel Model; + Model = new EdmModel(); + + // Address + EdmComplexType address = new EdmComplexType("NS", "Address"); + address.AddStructuralProperty("Street", EdmCoreModel.Instance.GetString(false)); + Model.AddElement(address); + + // Customer + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + Model.AddElement(customer); + customer.AddKeys(customer.AddStructuralProperty("CustomerID", EdmCoreModel.Instance.GetInt32(false))); + customer.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(true)); + customer.AddStructuralProperty("Location", new EdmComplexTypeReference(address, false)); + + // Order + EdmEntityType order = new EdmEntityType("NS", "Order"); + Model.AddElement(order); + order.AddKeys(order.AddStructuralProperty("OrderId", EdmCoreModel.Instance.GetInt32(false))); + order.AddStructuralProperty("Price", EdmCoreModel.Instance.GetInt32(false)); + + // VipOrder + EdmEntityType vipOrder = new EdmEntityType("NS", "VipOrder", order); + Model.AddElement(vipOrder); + vipOrder.AddKeys(vipOrder.AddStructuralProperty("Email", EdmCoreModel.Instance.GetString(false))); + + var orderNav = customer.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "Order", + Target = order, + TargetMultiplicity = EdmMultiplicity.ZeroOrOne + }); - static ODataReaderExtensionsTests() - { - Model = new EdmModel(); - - // Address - EdmComplexType address = new EdmComplexType("NS", "Address"); - address.AddStructuralProperty("Street", EdmCoreModel.Instance.GetString(false)); - Model.AddElement(address); - - // Customer - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - Model.AddElement(customer); - customer.AddKeys(customer.AddStructuralProperty("CustomerID", EdmCoreModel.Instance.GetInt32(false))); - customer.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(true)); - customer.AddStructuralProperty("Location", new EdmComplexTypeReference(address, false)); - - // Order - EdmEntityType order = new EdmEntityType("NS", "Order"); - Model.AddElement(order); - order.AddKeys(order.AddStructuralProperty("OrderId", EdmCoreModel.Instance.GetInt32(false))); - order.AddStructuralProperty("Price", EdmCoreModel.Instance.GetInt32(false)); - - // VipOrder - EdmEntityType vipOrder = new EdmEntityType("NS", "VipOrder", order); - Model.AddElement(vipOrder); - vipOrder.AddKeys(vipOrder.AddStructuralProperty("Email", EdmCoreModel.Instance.GetString(false))); - - var orderNav = customer.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "Order", - Target = order, - TargetMultiplicity = EdmMultiplicity.ZeroOrOne - }); - - var ordresNav = customer.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "Orders", - Target = order, - TargetMultiplicity = EdmMultiplicity.Many - }); - - EdmEntityContainer defaultContainer = new EdmEntityContainer("NS", "Container"); - Model.AddElement(defaultContainer); - EdmEntitySet customers = defaultContainer.AddEntitySet("Customers", customer); - EdmEntitySet orders = defaultContainer.AddEntitySet("Orders", order); - customers.AddNavigationTarget(orderNav, orders); - customers.AddNavigationTarget(ordresNav, orders); - } + var ordresNav = customer.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "Orders", + Target = order, + TargetMultiplicity = EdmMultiplicity.Many + }); + + EdmEntityContainer defaultContainer = new EdmEntityContainer("NS", "Container"); + Model.AddElement(defaultContainer); + EdmEntitySet customers = defaultContainer.AddEntitySet("Customers", customer); + EdmEntitySet orders = defaultContainer.AddEntitySet("Orders", order); + customers.AddNavigationTarget(orderNav, orders); + customers.AddNavigationTarget(ordresNav, orders); + } - [Fact] - public void ReadResourceOrResourceSet_ThrowsArgumentNull_EdmType() - { - // Arrange & Act & Assert - ODataReader reader = null; - ExceptionAssert.ThrowsArgumentNull(() => reader.ReadResourceOrResourceSet(), "reader"); - } + [Fact] + public void ReadResourceOrResourceSet_ThrowsArgumentNull_EdmType() + { + // Arrange & Act & Assert + ODataReader reader = null; + ExceptionAssert.ThrowsArgumentNull(() => reader.ReadResourceOrResourceSet(), "reader"); + } - [Fact] - public async Task ReadResourceOrResourceSetAsync_ThrowsArgumentNull_EdmType() - { - // Arrange & Act & Assert - ODataReader reader = null; - await ExceptionAssert.ThrowsArgumentNullAsync(() => reader.ReadResourceOrResourceSetAsync(), "reader"); - } + [Fact] + public async Task ReadResourceOrResourceSetAsync_ThrowsArgumentNull_EdmType() + { + // Arrange & Act & Assert + ODataReader reader = null; + await ExceptionAssert.ThrowsArgumentNullAsync(() => reader.ReadResourceOrResourceSetAsync(), "reader"); + } - [Fact] - public async Task ReadResourceWorksAsExpected() - { - // Arrange - const string payload = - "{" + - "\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," + + [Fact] + public async Task ReadResourceWorksAsExpected() + { + // Arrange + const string payload = + "{" + + "\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," + + "\"CustomerID\": 7," + + "\"Location\": { \"Street\":\"154TH AVE\"}," + + "\"Order\": {\"OrderId\": 8, \"Price\": 82 }," + + "\"Orders\": []" + + "}"; + + IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); + Assert.NotNull(customers); // Guard + + // Act + Func> func = mr => mr.CreateODataResourceReaderAsync(customers, customers.EntityType); + ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func); + + // Assert + Assert.NotNull(item); + ODataResourceWrapper resource = Assert.IsType(item); + Assert.Equal(3, resource.NestedResourceInfos.Count); + Assert.Equal(new[] { "Location", "Order", "Orders" }, resource.NestedResourceInfos.Select(n => n.NestedResourceInfo.Name)); + } + + [Fact] + public async Task ReadResourceSetWorksAsExpected() + { + // Arrange + const string payload = + "{" + + "\"@odata.context\":\"http://localhost/$metadata#Customers\"," + + "\"value\": [" + + "{" + "\"CustomerID\": 7," + "\"Location\": { \"Street\":\"154TH AVE\"}," + "\"Order\": {\"OrderId\": 8, \"Price\": 82 }," + "\"Orders\": []" + - "}"; - - IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); - Assert.NotNull(customers); // Guard - - // Act - Func> func = mr => mr.CreateODataResourceReaderAsync(customers, customers.EntityType); - ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func); - - // Assert - Assert.NotNull(item); - ODataResourceWrapper resource = Assert.IsType(item); - Assert.Equal(3, resource.NestedResourceInfos.Count); - Assert.Equal(new[] { "Location", "Order", "Orders" }, resource.NestedResourceInfos.Select(n => n.NestedResourceInfo.Name)); - } - - [Fact] - public async Task ReadResourceSetWorksAsExpected() - { - // Arrange - const string payload = - "{" + - "\"@odata.context\":\"http://localhost/$metadata#Customers\"," + - "\"value\": [" + - "{" + - "\"CustomerID\": 7," + - "\"Location\": { \"Street\":\"154TH AVE\"}," + - "\"Order\": {\"OrderId\": 8, \"Price\": 82 }," + - "\"Orders\": []" + - "}" + - "]" + - "}"; - - IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); - Assert.NotNull(customers); // Guard - - // Act - Func> func = mr => mr.CreateODataResourceSetReaderAsync(customers, customers.EntityType); - ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func); - - // Assert - Assert.NotNull(item); - ODataResourceSetWrapper resourceSet = Assert.IsType(item); - ODataResourceWrapper resource = Assert.Single(resourceSet.Resources); - Assert.Equal(new[] { "Location", "Order", "Orders" }, resource.NestedResourceInfos.Select(n => n.NestedResourceInfo.Name)); - } + "}" + + "]" + + "}"; + + IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); + Assert.NotNull(customers); // Guard + + // Act + Func> func = mr => mr.CreateODataResourceSetReaderAsync(customers, customers.EntityType); + ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func); + + // Assert + Assert.NotNull(item); + ODataResourceSetWrapper resourceSet = Assert.IsType(item); + ODataResourceWrapper resource = Assert.Single(resourceSet.Resources); + Assert.Equal(new[] { "Location", "Order", "Orders" }, resource.NestedResourceInfos.Select(n => n.NestedResourceInfo.Name)); + } - [Fact] - public async Task ReadResourceSetWithNestedResourceSetWorksAsExpected() - { - // Arrange - const string payload = - "{" + - "\"@odata.context\":\"http://localhost/$metadata#Customers\"," + - "\"value\": [" + - "{" + - "\"CustomerID\": 7," + - "\"Location\": { \"Street\":\"154TH AVE\"}," + - "\"Order\": {\"OrderId\": 8, \"Price\": 82 }," + - "\"Orders\": [" + - "{\"OrderId\": 8, \"Price\": 82 }," + - "{\"@odata.type\": \"#NS.VipOrder\",\"OrderId\": 9, \"Price\": 42, \"Email\": \"abc@efg.com\" }" + - "]" + - "}" + - "]" + - "}"; - - IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); - Assert.NotNull(customers); // Guard - - // Act - Func> func = mr => mr.CreateODataResourceSetReaderAsync(customers, customers.EntityType); - ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func); - - // Assert - Assert.NotNull(item); - ODataResourceSetWrapper resourceSet = Assert.IsType(item); - ODataResourceWrapper resource = Assert.Single(resourceSet.Resources); - Assert.Equal(new[] { "Location", "Order", "Orders" }, resource.NestedResourceInfos.Select(n => n.NestedResourceInfo.Name)); - - ODataNestedResourceInfoWrapper orders = resource.NestedResourceInfos.First(n => n.NestedResourceInfo.Name == "Orders"); - ODataItemWrapper nestedItem = Assert.Single(orders.NestedItems); - - ODataResourceSetWrapper ordersSet = Assert.IsType(nestedItem); - Assert.Equal(2, ordersSet.Resources.Count); - Assert.Collection(ordersSet.Resources, - r => - { - Assert.Equal("NS.Order", r.Resource.TypeName); - Assert.Equal(82, r.Resource.Properties.OfType().First(p => p.Name == "Price").Value); - }, - r => - { - Assert.Equal("NS.VipOrder", r.Resource.TypeName); - Assert.Equal("abc@efg.com", r.Resource.Properties.OfType().First(p => p.Name == "Email").Value); - }); - } + [Fact] + public async Task ReadResourceSetWithNestedResourceSetWorksAsExpected() + { + // Arrange + const string payload = + "{" + + "\"@odata.context\":\"http://localhost/$metadata#Customers\"," + + "\"value\": [" + + "{" + + "\"CustomerID\": 7," + + "\"Location\": { \"Street\":\"154TH AVE\"}," + + "\"Order\": {\"OrderId\": 8, \"Price\": 82 }," + + "\"Orders\": [" + + "{\"OrderId\": 8, \"Price\": 82 }," + + "{\"@odata.type\": \"#NS.VipOrder\",\"OrderId\": 9, \"Price\": 42, \"Email\": \"abc@efg.com\" }" + + "]" + + "}" + + "]" + + "}"; + + IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); + Assert.NotNull(customers); // Guard + + // Act + Func> func = mr => mr.CreateODataResourceSetReaderAsync(customers, customers.EntityType); + ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func); + + // Assert + Assert.NotNull(item); + ODataResourceSetWrapper resourceSet = Assert.IsType(item); + ODataResourceWrapper resource = Assert.Single(resourceSet.Resources); + Assert.Equal(new[] { "Location", "Order", "Orders" }, resource.NestedResourceInfos.Select(n => n.NestedResourceInfo.Name)); + + ODataNestedResourceInfoWrapper orders = resource.NestedResourceInfos.First(n => n.NestedResourceInfo.Name == "Orders"); + ODataItemWrapper nestedItem = Assert.Single(orders.NestedItems); + + ODataResourceSetWrapper ordersSet = Assert.IsType(nestedItem); + Assert.Equal(2, ordersSet.Resources.Count); + Assert.Collection(ordersSet.Resources, + r => + { + Assert.Equal("NS.Order", r.Resource.TypeName); + Assert.Equal(82, r.Resource.Properties.OfType().First(p => p.Name == "Price").Value); + }, + r => + { + Assert.Equal("NS.VipOrder", r.Resource.TypeName); + Assert.Equal("abc@efg.com", r.Resource.Properties.OfType().First(p => p.Name == "Email").Value); + }); + } - [Fact] - public async Task ReadDeltaResourceSetWorksAsExpected() - { - // Arrange - string payload = "{\"@context\":\"http://example.com/$metadata#Customers/$delta\"," + - "\"value\":[" + + [Fact] + public async Task ReadDeltaResourceSetWorksAsExpected() + { + // Arrange + string payload = "{\"@context\":\"http://example.com/$metadata#Customers/$delta\"," + + "\"value\":[" + + "{" + + "\"@removed\":{\"reason\":\"changed\"}," + + "\"CustomerID\":1," + + "\"Orders@delta\":[" + + "{" + + "\"@removed\":{\"reason\":\"deleted\"}," + + "\"OrderId\":10" + + "}," + "{" + - "\"@removed\":{\"reason\":\"changed\"}," + - "\"CustomerID\":1," + - "\"Orders@delta\":[" + - "{" + - "\"@removed\":{\"reason\":\"deleted\"}," + - "\"OrderId\":10" + - "}," + - "{" + - "\"@type\":\"#NS.VipOrder\"," + - "\"OrderId\":9," + - "\"Email\":\"a@abc.com\"" + - "}" + - "]" + + "\"@type\":\"#NS.VipOrder\"," + + "\"OrderId\":9," + + "\"Email\":\"a@abc.com\"" + "}" + "]" + - "}"; - - IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); - Assert.NotNull(customers); // Guard - - // Act - Func> func = mr => mr.CreateODataDeltaResourceSetReaderAsync(customers, customers.EntityType); - ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func, ODataVersion.V401); - - // Assert - Assert.NotNull(item); - - // --- DeltaResourceSet - // |--- DeleteResource (1) - // |--- NestedResourceInfo (1-1) - // |--- DeltaResourceSet - // |--- DelteResource - // |--- Normal Resource - ODataDeltaResourceSetWrapper deltaResourceSet = Assert.IsType(item); - ODataItemWrapper deltaItem = Assert.Single(deltaResourceSet.DeltaItems); - ODataResourceWrapper deletedResourceWrapper = Assert.IsType(deltaItem); - ODataDeletedResource deletedResource = Assert.IsType(deletedResourceWrapper.Resource); - Assert.Equal(DeltaDeletedEntryReason.Changed, deletedResource.Reason); - - ODataNestedResourceInfoWrapper nestedResourceInfo = Assert.Single(deletedResourceWrapper.NestedResourceInfos); - Assert.Equal("Orders", nestedResourceInfo.NestedResourceInfo.Name); - Assert.True(nestedResourceInfo.NestedResourceInfo.IsCollection); - - ODataItemWrapper nestedItem = Assert.Single(nestedResourceInfo.NestedItems); - ODataDeltaResourceSetWrapper ordersDeltaResourceSet = Assert.IsType(nestedItem); - Assert.Equal(2, ordersDeltaResourceSet.DeltaItems.Count); - ODataResourceWrapper resource1 = Assert.IsType(ordersDeltaResourceSet.DeltaItems.ElementAt(0)); - ODataDeletedResource deletedResource1 = Assert.IsType(resource1.Resource); - Assert.Equal(DeltaDeletedEntryReason.Deleted, deletedResource1.Reason); - - ODataResourceWrapper resource2 = Assert.IsType(ordersDeltaResourceSet.DeltaItems.ElementAt(1)); - Assert.Equal("NS.VipOrder", resource2.Resource.TypeName); - Assert.Collection(resource2.Resource.Properties.OfType(), - p => - { - Assert.Equal("OrderId", p.Name); - Assert.Equal(9, p.Value); - }, - p => - { - Assert.Equal("Email", p.Name); - Assert.Equal("a@abc.com", p.Value); - }); - } - - [Fact] - public async Task ReadDeletedLinkInDeltaResourceSetWorksAsExpected() - { - // Arrange - string payload = "{" + - "\"@odata.context\":\"http://localhost/$metadata#Customers/$delta\"," + - "\"@odata.count\":5," + - "\"value\":[" + - "{" + - "\"@odata.id\":\"Customers(42)\"," + - "\"Name\":\"Sammy\"" + - "}," + - "{" + - "\"@odata.context\":\"http://localhost/$metadata#Customers/$deletedLink\"," + - "\"source\":\"Customers(39)\"," + - "\"relationship\":\"Orders\"," + - "\"target\":\"Orders(10643)\"" + - "}," + - "{" + - "\"@odata.context\":\"http://localhost/$metadata#Customers/$link\"," + - "\"source\":\"Customers(32)\"," + - "\"relationship\":\"Orders\"," + - "\"target\":\"Orders(10645)\"" + - "}," + - "{" + - "\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\"," + - "\"@odata.id\":\"Orders(10643)\"," + - "\"Price\": 82" + - "}," + - "{" + - "\"@odata.context\":\"http://localhost/$metadata#Customers/$deletedEntity\"," + - "\"id\":\"Customers(21)\"," + - "\"reason\":\"deleted\"" + - "}" + - "]," + - "\"@odata.deltaLink\":\"Customers?$expand=Orders&$deltatoken=8015\"" + - "}"; - - IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); - Assert.NotNull(customers); // Guard - - // Act - Func> func = mr => mr.CreateODataDeltaResourceSetReaderAsync(customers, customers.EntityType); - ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func); - - // Assert - Assert.NotNull(item); - - // --- DeltaResourceSet - // |--- Resource (1) - // |--- DeltaDeletedLink - // |--- DeltaLink - // |--- Resource (2) - // |--- DeletedResource (1) - // | - ODataDeltaResourceSetWrapper deltaResourceSet = Assert.IsType(item); - - // Resources - Assert.Equal(5, deltaResourceSet.DeltaItems.Count); - Assert.Collection(deltaResourceSet.DeltaItems, - e => - { - // 1) Resource - ODataResourceWrapper resource1 = Assert.IsType(e); - Assert.Equal("Customers(42)", resource1.Resource.Id.OriginalString); - Assert.Equal("Sammy", resource1.Resource.Properties.OfType().First(p => p.Name == "Name").Value); - }, - e => - { - // 2) Deleted Link - ODataDeltaDeletedLinkWrapper deletedLinkWrapper = Assert.IsType(e); - Assert.Equal("Customers(39)", deletedLinkWrapper.DeltaDeletedLink.Source.OriginalString); - Assert.Equal("Orders(10643)", deletedLinkWrapper.DeltaDeletedLink.Target.OriginalString); - Assert.Equal("Orders", deletedLinkWrapper.DeltaDeletedLink.Relationship); - }, - e => - { - // 3) Added Link - ODataDeltaLinkWrapper linkWrapper = Assert.IsType(e); - Assert.Equal("Customers(32)", linkWrapper.DeltaLink.Source.OriginalString); - Assert.Equal("Orders(10645)", linkWrapper.DeltaLink.Target.OriginalString); - Assert.Equal("Orders", linkWrapper.DeltaLink.Relationship); - }, - e => - { - // 4) Resource - ODataResourceWrapper resource2 = Assert.IsType(e); - Assert.Equal("Orders(10643)", resource2.Resource.Id.OriginalString); - Assert.Equal(82, resource2.Resource.Properties.OfType().First(p => p.Name == "Price").Value); - }, - e => - { - // 5) Deleted resource - ODataResourceWrapper deletedResourceWrapper = Assert.IsType(e); - ODataDeletedResource deletedResource = Assert.IsType(deletedResourceWrapper.Resource); - Assert.Equal("Customers(21)", deletedResource.Id.OriginalString); - Assert.Equal(DeltaDeletedEntryReason.Deleted, deletedResource.Reason); - }); - } + "}" + + "]" + + "}"; + + IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); + Assert.NotNull(customers); // Guard + + // Act + Func> func = mr => mr.CreateODataDeltaResourceSetReaderAsync(customers, customers.EntityType); + ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func, ODataVersion.V401); + + // Assert + Assert.NotNull(item); + + // --- DeltaResourceSet + // |--- DeleteResource (1) + // |--- NestedResourceInfo (1-1) + // |--- DeltaResourceSet + // |--- DelteResource + // |--- Normal Resource + ODataDeltaResourceSetWrapper deltaResourceSet = Assert.IsType(item); + ODataItemWrapper deltaItem = Assert.Single(deltaResourceSet.DeltaItems); + ODataResourceWrapper deletedResourceWrapper = Assert.IsType(deltaItem); + ODataDeletedResource deletedResource = Assert.IsType(deletedResourceWrapper.Resource); + Assert.Equal(DeltaDeletedEntryReason.Changed, deletedResource.Reason); + + ODataNestedResourceInfoWrapper nestedResourceInfo = Assert.Single(deletedResourceWrapper.NestedResourceInfos); + Assert.Equal("Orders", nestedResourceInfo.NestedResourceInfo.Name); + Assert.True(nestedResourceInfo.NestedResourceInfo.IsCollection); + + ODataItemWrapper nestedItem = Assert.Single(nestedResourceInfo.NestedItems); + ODataDeltaResourceSetWrapper ordersDeltaResourceSet = Assert.IsType(nestedItem); + Assert.Equal(2, ordersDeltaResourceSet.DeltaItems.Count); + ODataResourceWrapper resource1 = Assert.IsType(ordersDeltaResourceSet.DeltaItems.ElementAt(0)); + ODataDeletedResource deletedResource1 = Assert.IsType(resource1.Resource); + Assert.Equal(DeltaDeletedEntryReason.Deleted, deletedResource1.Reason); + + ODataResourceWrapper resource2 = Assert.IsType(ordersDeltaResourceSet.DeltaItems.ElementAt(1)); + Assert.Equal("NS.VipOrder", resource2.Resource.TypeName); + Assert.Collection(resource2.Resource.Properties.OfType(), + p => + { + Assert.Equal("OrderId", p.Name); + Assert.Equal(9, p.Value); + }, + p => + { + Assert.Equal("Email", p.Name); + Assert.Equal("a@abc.com", p.Value); + }); + } - [Fact] - public async Task ReadDeletedLinkInDeltaResourceSetWorksAsExpected2() - { - // Arrange - string payload = "{\"@context\":\"http://example.com/$metadata#Customers/$delta\"," + + [Fact] + public async Task ReadDeletedLinkInDeltaResourceSetWorksAsExpected() + { + // Arrange + string payload = "{" + + "\"@odata.context\":\"http://localhost/$metadata#Customers/$delta\"," + + "\"@odata.count\":5," + "\"value\":[" + - "{" + - "\"@removed\":{\"reason\":\"changed\"}," + - "\"CustomerID\":1," + - "\"Order@delta\":{" + - "\"@context\":\"#Orders/$deletedEntity\"," + - "\"@removed\":{\"reason\":\"deleted\"}," + - "\"OrderId\":10" + - "}" + - "}" + - "]" + + "{" + + "\"@odata.id\":\"Customers(42)\"," + + "\"Name\":\"Sammy\"" + + "}," + + "{" + + "\"@odata.context\":\"http://localhost/$metadata#Customers/$deletedLink\"," + + "\"source\":\"Customers(39)\"," + + "\"relationship\":\"Orders\"," + + "\"target\":\"Orders(10643)\"" + + "}," + + "{" + + "\"@odata.context\":\"http://localhost/$metadata#Customers/$link\"," + + "\"source\":\"Customers(32)\"," + + "\"relationship\":\"Orders\"," + + "\"target\":\"Orders(10645)\"" + + "}," + + "{" + + "\"@odata.context\":\"http://localhost/$metadata#Orders/$entity\"," + + "\"@odata.id\":\"Orders(10643)\"," + + "\"Price\": 82" + + "}," + + "{" + + "\"@odata.context\":\"http://localhost/$metadata#Customers/$deletedEntity\"," + + "\"id\":\"Customers(21)\"," + + "\"reason\":\"deleted\"" + + "}" + + "]," + + "\"@odata.deltaLink\":\"Customers?$expand=Orders&$deltatoken=8015\"" + "}"; - IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); - Assert.NotNull(customers); // Guard - - // Act - Func> func = mr => mr.CreateODataDeltaResourceSetReaderAsync(customers, customers.EntityType); - ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func, ODataVersion.V401); - - // Assert - Assert.NotNull(item); - - // --- DeltaResourceSet - // |--- DeleteResource (Changed) - // |--- NestedResourceInfo - // |--- DeleteResource (Deleted) - ODataDeltaResourceSetWrapper deltaResourceSet = Assert.IsType(item); - ODataItemWrapper deltaItem = Assert.Single(deltaResourceSet.DeltaItems); - ODataResourceWrapper deletedResourceWrapper = Assert.IsType(deltaItem); - ODataDeletedResource deletedResource = Assert.IsType(deletedResourceWrapper.Resource); - Assert.Equal(DeltaDeletedEntryReason.Changed, deletedResource.Reason); - - ODataNestedResourceInfoWrapper nestedResourceInfo = Assert.Single(deletedResourceWrapper.NestedResourceInfos); - Assert.Equal("Order", nestedResourceInfo.NestedResourceInfo.Name); - Assert.False(nestedResourceInfo.NestedResourceInfo.IsCollection); - - ODataItemWrapper nestedItem = Assert.Single(nestedResourceInfo.NestedItems); - ODataResourceWrapper orderResource = Assert.IsType(nestedItem); - Assert.True(orderResource.IsDeletedResource); - ODataDeletedResource innerDeletedResource = Assert.IsType(orderResource.Resource); - Assert.Equal(DeltaDeletedEntryReason.Deleted, innerDeletedResource.Reason); - - ODataProperty property = Assert.Single(innerDeletedResource.Properties.OfType()); - Assert.Equal("OrderId", property.Name); - Assert.Equal(10, property.Value); - } - - [Theory] - [InlineData(ODataVersion.V4, "\"Order@odata.bind\":\"http://svc/Orders(7)\"")] - [InlineData(ODataVersion.V401, "\"Order\": {\"@id\": \"http://svc/Orders(8)\"}")] - public async Task ReadSingleEntityReferenceLinkWorksAsExpected(ODataVersion version, string referenceLink) - { - // Arrange - string payload = "{" + // -> ResourceStart - "\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," + - "\"CustomerID\": 7," + - referenceLink + - "}"; - - IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); - Assert.NotNull(customers); // Guard - - // Act - Func> func = mr => mr.CreateODataResourceReaderAsync(customers, customers.EntityType); - ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func, version); - - // Assert - Assert.NotNull(item); - ODataResourceWrapper resource = Assert.IsType(item); - ODataNestedResourceInfoWrapper nestedResourceInfo = Assert.Single(resource.NestedResourceInfos); - Assert.Equal("Order", nestedResourceInfo.NestedResourceInfo.Name); - if (version == ODataVersion.V401) + IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); + Assert.NotNull(customers); // Guard + + // Act + Func> func = mr => mr.CreateODataDeltaResourceSetReaderAsync(customers, customers.EntityType); + ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func); + + // Assert + Assert.NotNull(item); + + // --- DeltaResourceSet + // |--- Resource (1) + // |--- DeltaDeletedLink + // |--- DeltaLink + // |--- Resource (2) + // |--- DeletedResource (1) + // | + ODataDeltaResourceSetWrapper deltaResourceSet = Assert.IsType(item); + + // Resources + Assert.Equal(5, deltaResourceSet.DeltaItems.Count); + Assert.Collection(deltaResourceSet.DeltaItems, + e => { - // --- Resource - // |--- NestedResourceInfo (Order) - // |--- Resource - ODataResourceWrapper order = Assert.IsType(Assert.Single(nestedResourceInfo.NestedItems)); - Assert.Equal("http://svc/Orders(8)", order.Resource.Id.OriginalString); - Assert.Empty(order.Resource.Properties); - } - else + // 1) Resource + ODataResourceWrapper resource1 = Assert.IsType(e); + Assert.Equal("Customers(42)", resource1.Resource.Id.OriginalString); + Assert.Equal("Sammy", resource1.Resource.Properties.OfType().First(p => p.Name == "Name").Value); + }, + e => { - // --- Resource - // |--- NestedResourceInfo (Order) - // |--- EntityReferenceLink - ODataEntityReferenceLinkWrapper orderLink = Assert.IsType(Assert.Single(nestedResourceInfo.NestedItems)); - Assert.Equal("http://svc/Orders(7)", orderLink.EntityReferenceLink.Url.OriginalString); - } - } - - [Fact] - public async Task ReadEntityReferenceLinksSetWorksAsExpected_V40() - { - // Arrange - string payload = "{" + // -> ResourceStart - "\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," + - "\"CustomerID\": 7," + - "\"Orders@odata.bind\":[" + // -> NestedResourceInfoStart - "\"http://svc/Orders(2)\"," + - "\"http://svc/Orders(3)\"," + - "\"http://svc/Orders(4)\"" + - "]" + - "}"; - - IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); - Assert.NotNull(customers); // Guard - - // Act - Func> func = mr => mr.CreateODataResourceReaderAsync(customers, customers.EntityType); - ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func, ODataVersion.V4); + // 2) Deleted Link + ODataDeltaDeletedLinkWrapper deletedLinkWrapper = Assert.IsType(e); + Assert.Equal("Customers(39)", deletedLinkWrapper.DeltaDeletedLink.Source.OriginalString); + Assert.Equal("Orders(10643)", deletedLinkWrapper.DeltaDeletedLink.Target.OriginalString); + Assert.Equal("Orders", deletedLinkWrapper.DeltaDeletedLink.Relationship); + }, + e => + { + // 3) Added Link + ODataDeltaLinkWrapper linkWrapper = Assert.IsType(e); + Assert.Equal("Customers(32)", linkWrapper.DeltaLink.Source.OriginalString); + Assert.Equal("Orders(10645)", linkWrapper.DeltaLink.Target.OriginalString); + Assert.Equal("Orders", linkWrapper.DeltaLink.Relationship); + }, + e => + { + // 4) Resource + ODataResourceWrapper resource2 = Assert.IsType(e); + Assert.Equal("Orders(10643)", resource2.Resource.Id.OriginalString); + Assert.Equal(82, resource2.Resource.Properties.OfType().First(p => p.Name == "Price").Value); + }, + e => + { + // 5) Deleted resource + ODataResourceWrapper deletedResourceWrapper = Assert.IsType(e); + ODataDeletedResource deletedResource = Assert.IsType(deletedResourceWrapper.Resource); + Assert.Equal("Customers(21)", deletedResource.Id.OriginalString); + Assert.Equal(DeltaDeletedEntryReason.Deleted, deletedResource.Reason); + }); + } - // Assert - Assert.NotNull(item); + [Fact] + public async Task ReadDeletedLinkInDeltaResourceSetWorksAsExpected2() + { + // Arrange + string payload = "{\"@context\":\"http://example.com/$metadata#Customers/$delta\"," + + "\"value\":[" + + "{" + + "\"@removed\":{\"reason\":\"changed\"}," + + "\"CustomerID\":1," + + "\"Order@delta\":{" + + "\"@context\":\"#Orders/$deletedEntity\"," + + "\"@removed\":{\"reason\":\"deleted\"}," + + "\"OrderId\":10" + + "}" + + "}" + + "]" + + "}"; + + IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); + Assert.NotNull(customers); // Guard + + // Act + Func> func = mr => mr.CreateODataDeltaResourceSetReaderAsync(customers, customers.EntityType); + ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func, ODataVersion.V401); + + // Assert + Assert.NotNull(item); + + // --- DeltaResourceSet + // |--- DeleteResource (Changed) + // |--- NestedResourceInfo + // |--- DeleteResource (Deleted) + ODataDeltaResourceSetWrapper deltaResourceSet = Assert.IsType(item); + ODataItemWrapper deltaItem = Assert.Single(deltaResourceSet.DeltaItems); + ODataResourceWrapper deletedResourceWrapper = Assert.IsType(deltaItem); + ODataDeletedResource deletedResource = Assert.IsType(deletedResourceWrapper.Resource); + Assert.Equal(DeltaDeletedEntryReason.Changed, deletedResource.Reason); + + ODataNestedResourceInfoWrapper nestedResourceInfo = Assert.Single(deletedResourceWrapper.NestedResourceInfos); + Assert.Equal("Order", nestedResourceInfo.NestedResourceInfo.Name); + Assert.False(nestedResourceInfo.NestedResourceInfo.IsCollection); + + ODataItemWrapper nestedItem = Assert.Single(nestedResourceInfo.NestedItems); + ODataResourceWrapper orderResource = Assert.IsType(nestedItem); + Assert.True(orderResource.IsDeletedResource); + ODataDeletedResource innerDeletedResource = Assert.IsType(orderResource.Resource); + Assert.Equal(DeltaDeletedEntryReason.Deleted, innerDeletedResource.Reason); + + ODataProperty property = Assert.Single(innerDeletedResource.Properties.OfType()); + Assert.Equal("OrderId", property.Name); + Assert.Equal(10, property.Value); + } + [Theory] + [InlineData(ODataVersion.V4, "\"Order@odata.bind\":\"http://svc/Orders(7)\"")] + [InlineData(ODataVersion.V401, "\"Order\": {\"@id\": \"http://svc/Orders(8)\"}")] + public async Task ReadSingleEntityReferenceLinkWorksAsExpected(ODataVersion version, string referenceLink) + { + // Arrange + string payload = "{" + // -> ResourceStart + "\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," + + "\"CustomerID\": 7," + + referenceLink + + "}"; + + IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); + Assert.NotNull(customers); // Guard + + // Act + Func> func = mr => mr.CreateODataResourceReaderAsync(customers, customers.EntityType); + ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func, version); + + // Assert + Assert.NotNull(item); + ODataResourceWrapper resource = Assert.IsType(item); + ODataNestedResourceInfoWrapper nestedResourceInfo = Assert.Single(resource.NestedResourceInfos); + Assert.Equal("Order", nestedResourceInfo.NestedResourceInfo.Name); + if (version == ODataVersion.V401) + { // --- Resource - // |--- NestedResourceInfo - // |--- EntityReferceLink (2) - // |--- EntityReferceLink (3) - // |--- EntityReferceLink (4) - ODataResourceWrapper resource = Assert.IsType(item); - - ODataNestedResourceInfoWrapper nestedResourceInfo = Assert.Single(resource.NestedResourceInfos); - Assert.Equal("Orders", nestedResourceInfo.NestedResourceInfo.Name); - Assert.True(nestedResourceInfo.NestedResourceInfo.IsCollection); - - Assert.NotNull(nestedResourceInfo.NestedItems); - Assert.Equal(3, nestedResourceInfo.NestedItems.Count); - ODataEntityReferenceLinkWrapper[] links = nestedResourceInfo.NestedItems.OfType().ToArray(); - Assert.Equal(3, links.Length); - Assert.Collection(links, - r => - { - Assert.Equal("http://svc/Orders(2)", r.EntityReferenceLink.Url.OriginalString); - }, - r => - { - Assert.Equal("http://svc/Orders(3)", r.EntityReferenceLink.Url.OriginalString); - }, - r => - { - Assert.Equal("http://svc/Orders(4)", r.EntityReferenceLink.Url.OriginalString); - }); + // |--- NestedResourceInfo (Order) + // |--- Resource + ODataResourceWrapper order = Assert.IsType(Assert.Single(nestedResourceInfo.NestedItems)); + Assert.Equal("http://svc/Orders(8)", order.Resource.Id.OriginalString); + Assert.Empty(order.Resource.Properties); } - - [Fact] - public async Task ReadEntityReferenceLinksSetWorksAsExpected_V401() + else { - // Arrange - string payload = "{" + // -> ResourceStart - "\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," + - "\"CustomerID\": 7," + - "\"Orders\":[" + // -> NestedResourceInfoStart - "{ \"@id\": \"http://svc/Orders(2)\" }," + - "{ \"@id\": \"http://svc/Orders(3)\" }," + - "{ \"@id\": \"http://svc/Orders(4)\" }" + - "]" + - "}"; - - IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); - Assert.NotNull(customers); // Guard - - // Act - Func> func = mr => mr.CreateODataResourceReaderAsync(customers, customers.EntityType); - ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func, ODataVersion.V401); - - // Assert - Assert.NotNull(item); - // --- Resource - // |--- NestedResourceInfo - // |--- NestedResourceSet - // |--- Resource (2) - // |--- Resource (3) - // |--- Resource (4) - ODataResourceWrapper resource = Assert.IsType(item); - - ODataNestedResourceInfoWrapper nestedResourceInfo = Assert.Single(resource.NestedResourceInfos); - Assert.Equal("Orders", nestedResourceInfo.NestedResourceInfo.Name); - Assert.True(nestedResourceInfo.NestedResourceInfo.IsCollection); - - ODataResourceSetWrapper ordersResourceSet = Assert.IsType(Assert.Single(nestedResourceInfo.NestedItems)); - Assert.Equal(3, ordersResourceSet.Resources.Count); - Assert.Collection(ordersResourceSet.Resources, - r => - { - Assert.Equal("http://svc/Orders(2)", r.Resource.Id.OriginalString); - }, - r => - { - Assert.Equal("http://svc/Orders(3)", r.Resource.Id.OriginalString); - }, - r => - { - Assert.Equal("http://svc/Orders(4)", r.Resource.Id.OriginalString); - }); + // |--- NestedResourceInfo (Order) + // |--- EntityReferenceLink + ODataEntityReferenceLinkWrapper orderLink = Assert.IsType(Assert.Single(nestedResourceInfo.NestedItems)); + Assert.Equal("http://svc/Orders(7)", orderLink.EntityReferenceLink.Url.OriginalString); } + } - private async Task ReadPayloadAsync(string payload, - IEdmModel edmModel, Func> createReader, ODataVersion version = ODataVersion.V4, - bool readUntypedAsString = false) - { - var message = new InMemoryMessage() + [Fact] + public async Task ReadEntityReferenceLinksSetWorksAsExpected_V40() + { + // Arrange + string payload = "{" + // -> ResourceStart + "\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," + + "\"CustomerID\": 7," + + "\"Orders@odata.bind\":[" + // -> NestedResourceInfoStart + "\"http://svc/Orders(2)\"," + + "\"http://svc/Orders(3)\"," + + "\"http://svc/Orders(4)\"" + + "]" + + "}"; + + IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); + Assert.NotNull(customers); // Guard + + // Act + Func> func = mr => mr.CreateODataResourceReaderAsync(customers, customers.EntityType); + ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func, ODataVersion.V4); + + // Assert + Assert.NotNull(item); + + // --- Resource + // |--- NestedResourceInfo + // |--- EntityReferceLink (2) + // |--- EntityReferceLink (3) + // |--- EntityReferceLink (4) + ODataResourceWrapper resource = Assert.IsType(item); + + ODataNestedResourceInfoWrapper nestedResourceInfo = Assert.Single(resource.NestedResourceInfos); + Assert.Equal("Orders", nestedResourceInfo.NestedResourceInfo.Name); + Assert.True(nestedResourceInfo.NestedResourceInfo.IsCollection); + + Assert.NotNull(nestedResourceInfo.NestedItems); + Assert.Equal(3, nestedResourceInfo.NestedItems.Count); + ODataEntityReferenceLinkWrapper[] links = nestedResourceInfo.NestedItems.OfType().ToArray(); + Assert.Equal(3, links.Length); + Assert.Collection(links, + r => { - Stream = new MemoryStream(Encoding.UTF8.GetBytes(payload)) - }; - message.SetHeader("Content-Type", "application/json;odata.metadata=minimal"); + Assert.Equal("http://svc/Orders(2)", r.EntityReferenceLink.Url.OriginalString); + }, + r => + { + Assert.Equal("http://svc/Orders(3)", r.EntityReferenceLink.Url.OriginalString); + }, + r => + { + Assert.Equal("http://svc/Orders(4)", r.EntityReferenceLink.Url.OriginalString); + }); + } -#pragma warning disable CS0618 // Type or member is obsolete - ODataMessageReaderSettings readerSettings = new ODataMessageReaderSettings() + [Fact] + public async Task ReadEntityReferenceLinksSetWorksAsExpected_V401() + { + // Arrange + string payload = "{" + // -> ResourceStart + "\"@odata.context\":\"http://localhost/$metadata#Customers/$entity\"," + + "\"CustomerID\": 7," + + "\"Orders\":[" + // -> NestedResourceInfoStart + "{ \"@id\": \"http://svc/Orders(2)\" }," + + "{ \"@id\": \"http://svc/Orders(3)\" }," + + "{ \"@id\": \"http://svc/Orders(4)\" }" + + "]" + + "}"; + + IEdmEntitySet customers = Model.EntityContainer.FindEntitySet("Customers"); + Assert.NotNull(customers); // Guard + + // Act + Func> func = mr => mr.CreateODataResourceReaderAsync(customers, customers.EntityType); + ODataItemWrapper item = await ReadPayloadAsync(payload, Model, func, ODataVersion.V401); + + // Assert + Assert.NotNull(item); + + // --- Resource + // |--- NestedResourceInfo + // |--- NestedResourceSet + // |--- Resource (2) + // |--- Resource (3) + // |--- Resource (4) + ODataResourceWrapper resource = Assert.IsType(item); + + ODataNestedResourceInfoWrapper nestedResourceInfo = Assert.Single(resource.NestedResourceInfos); + Assert.Equal("Orders", nestedResourceInfo.NestedResourceInfo.Name); + Assert.True(nestedResourceInfo.NestedResourceInfo.IsCollection); + + ODataResourceSetWrapper ordersResourceSet = Assert.IsType(Assert.Single(nestedResourceInfo.NestedItems)); + Assert.Equal(3, ordersResourceSet.Resources.Count); + Assert.Collection(ordersResourceSet.Resources, + r => + { + Assert.Equal("http://svc/Orders(2)", r.Resource.Id.OriginalString); + }, + r => + { + Assert.Equal("http://svc/Orders(3)", r.Resource.Id.OriginalString); + }, + r => { - BaseUri = new Uri("http://localhost/$metadata"), - EnableMessageStreamDisposal = true, - ReadUntypedAsString = readUntypedAsString, - Version = version, - }; + Assert.Equal("http://svc/Orders(4)", r.Resource.Id.OriginalString); + }); + } + + private async Task ReadPayloadAsync(string payload, + IEdmModel edmModel, Func> createReader, ODataVersion version = ODataVersion.V4, + bool readUntypedAsString = false) + { + var message = new InMemoryMessage() + { + Stream = new MemoryStream(Encoding.UTF8.GetBytes(payload)) + }; + message.SetHeader("Content-Type", "application/json;odata.metadata=minimal"); + +#pragma warning disable CS0618 // Type or member is obsolete + ODataMessageReaderSettings readerSettings = new ODataMessageReaderSettings() + { + BaseUri = new Uri("http://localhost/$metadata"), + EnableMessageStreamDisposal = true, + ReadUntypedAsString = readUntypedAsString, + Version = version, + }; #pragma warning restore CS0618 // Type or member is obsolete - using (var msgReader = new ODataMessageReader((IODataRequestMessageAsync)message, readerSettings, edmModel)) - { - ODataReader reader = await createReader(msgReader); - return await reader.ReadResourceOrResourceSetAsync(); - } + using (var msgReader = new ODataMessageReader((IODataRequestMessageAsync)message, readerSettings, edmModel)) + { + ODataReader reader = await createReader(msgReader); + return await reader.ReadResourceOrResourceSetAsync(); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/Address.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/Address.cs index 499b2967e..ab50cb768 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/Address.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/Address.cs @@ -5,22 +5,21 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class Address { - public class Address - { - public string StreetAddress { get; set; } + public string StreetAddress { get; set; } - public string City { get; set; } + public string City { get; set; } - public string Street { get; set; } + public string Street { get; set; } - public string State { get; set; } + public string State { get; set; } - public int HouseNumber { get; set; } + public int HouseNumber { get; set; } - public ZipCode ZipCode { get; set; } + public ZipCode ZipCode { get; set; } - public string IgnoreThis { get; set; } - } + public string IgnoreThis { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/Category.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/Category.cs index 3b57198d5..05ffb482d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/Category.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/Category.cs @@ -8,24 +8,23 @@ using System.Collections.Generic; using System.Linq; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class Category { - public class Category - { - public int CategoryID { get; set; } - public string CategoryName { get; set; } + public int CategoryID { get; set; } + public string CategoryName { get; set; } - public Product Product { get; set; } + public Product Product { get; set; } - public IEnumerable Products { get; set; } + public IEnumerable Products { get; set; } - public IEnumerable EnumerableProducts { get; set; } + public IEnumerable EnumerableProducts { get; set; } - public IQueryable QueryableProducts { get; set; } - } + public IQueryable QueryableProducts { get; set; } +} - public class DerivedCategory : Category - { - public string DerivedCategoryName { get; set; } - } +public class DerivedCategory : Category +{ + public string DerivedCategoryName { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/Color.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/Color.cs index 89225724c..9ab532a79 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/Color.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/Color.cs @@ -7,15 +7,14 @@ using System; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +[Flags] +public enum Color { - [Flags] - public enum Color - { - Red = 1, + Red = 1, - Green = 2, + Green = 2, - Blue = 4 - } + Blue = 4 } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/Company.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/Company.cs index ab60a11ac..69699bd1f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/Company.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/Company.cs @@ -8,22 +8,21 @@ using System.Collections.Generic; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class Company { - public class Company - { - public int CompanyId { get; set; } - public string CompanyName { get; set; } - public string Website { get; set; } - public Address HeadQuarterAddress { get; set; } + public int CompanyId { get; set; } + public string CompanyName { get; set; } + public string Website { get; set; } + public Address HeadQuarterAddress { get; set; } - [Singleton] - public Employee CEO { get; set; } + [Singleton] + public Employee CEO { get; set; } - public int CEOID { get; set; } + public int CEOID { get; set; } - public List ComplanyEmployees { get; set; } - public List Customers { get; set; } - public List
Subsidiaries { get; set; } - } + public List ComplanyEmployees { get; set; } + public List Customers { get; set; } + public List
Subsidiaries { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/Customer.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/Customer.cs index 09bbde986..2d9dd2083 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/Customer.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/Customer.cs @@ -8,36 +8,35 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class Customer { - public class Customer - { - public int Id { get; set; } + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public string City { get; set; } + public string City { get; set; } - public Address Address { get; set; } + public Address Address { get; set; } - public Address WorkAddress { get; set; } + public Address WorkAddress { get; set; } - public string Website { get; set; } + public string Website { get; set; } - public string ShareSymbol { get; set; } + public string ShareSymbol { get; set; } - public decimal? SharePrice { get; set; } + public decimal? SharePrice { get; set; } - public Company Company { get; set; } + public Company Company { get; set; } - public List Orders { get; set; } + public List Orders { get; set; } - public List Aliases { get; set; } + public List Aliases { get; set; } - public List
Addresses { get; set; } + public List
Addresses { get; set; } - public Dictionary DynamicProperties { get; set; } + public Dictionary DynamicProperties { get; set; } - public DateTimeOffset? StartDate { get; set; } - } + public DateTimeOffset? StartDate { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/CustomersModelWithInheritance.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/CustomersModelWithInheritance.cs index 0237fd0c7..5007ae3a7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/CustomersModelWithInheritance.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/CustomersModelWithInheritance.cs @@ -8,468 +8,467 @@ using Microsoft.AspNetCore.OData.Edm; using Microsoft.OData.Edm; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class CustomersModelWithInheritance { - public class CustomersModelWithInheritance + public CustomersModelWithInheritance() { - public CustomersModelWithInheritance() - { - EdmModel model = new EdmModel(); - - // Enum type simpleEnum - EdmEnumType simpleEnum = new EdmEnumType("NS", "SimpleEnum"); - simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "First", new EdmEnumMemberValue(0))); - simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "Second", new EdmEnumMemberValue(1))); - simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "Third", new EdmEnumMemberValue(2))); - model.AddElement(simpleEnum); - - // complex type address - EdmComplexType address = new EdmComplexType("NS", "Address"); - address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); - address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); - address.AddStructuralProperty("State", EdmPrimitiveTypeKind.String); - address.AddStructuralProperty("ZipCode", EdmPrimitiveTypeKind.String); - address.AddStructuralProperty("CountryOrRegion", EdmPrimitiveTypeKind.String); - model.AddElement(address); - - // open complex type "Account" - EdmComplexType account = new EdmComplexType("NS", "Account", null, false, true); - account.AddStructuralProperty("Bank", EdmPrimitiveTypeKind.String); - account.AddStructuralProperty("CardNum", EdmPrimitiveTypeKind.Int64); - account.AddStructuralProperty("BankAddress", new EdmComplexTypeReference(address, isNullable: true)); - model.AddElement(account); - - EdmComplexType specialAccount = new EdmComplexType("NS", "SpecialAccount", account, false, true); - specialAccount.AddStructuralProperty("SpecialCard", EdmPrimitiveTypeKind.String); - model.AddElement(specialAccount); - - // entity type customer - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - IEdmProperty customerName = customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - customer.AddStructuralProperty("SimpleEnum", simpleEnum.ToEdmTypeReference(isNullable: false)); - customer.AddStructuralProperty("Address", new EdmComplexTypeReference(address, isNullable: true)); - customer.AddStructuralProperty("Account", new EdmComplexTypeReference(account, isNullable: true)); - customer.AddStructuralProperty("OtherAccounts", new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(account, true)))); - IEdmTypeReference primitiveTypeReference = EdmCoreModel.Instance.GetPrimitive( - EdmPrimitiveTypeKind.String, - isNullable: true); - var city = customer.AddStructuralProperty( - "City", - primitiveTypeReference, - defaultValue: null); - model.AddElement(customer); - - // derived entity type special customer - EdmEntityType specialCustomer = new EdmEntityType("NS", "SpecialCustomer", customer); - specialCustomer.AddStructuralProperty("SpecialCustomerProperty", EdmPrimitiveTypeKind.Guid); - specialCustomer.AddStructuralProperty("SpecialAddress", new EdmComplexTypeReference(address, isNullable: true)); - model.AddElement(specialCustomer); - - // entity type order (open entity type) - EdmEntityType order = new EdmEntityType("NS", "Order", null, false, true); - // EdmEntityType order = new EdmEntityType("NS", "Order"); - order.AddKeys(order.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - order.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); - order.AddStructuralProperty("Amount", EdmPrimitiveTypeKind.Int32); - model.AddElement(order); - - // derived entity type special order - EdmEntityType specialOrder = new EdmEntityType("NS", "SpecialOrder", order, false, true); - specialOrder.AddStructuralProperty("SpecialOrderProperty", EdmPrimitiveTypeKind.Guid); - model.AddElement(specialOrder); - - // test entity - EdmEntityType testEntity = new EdmEntityType("Microsoft.AspNetCore.OData.Tests.Query.Wrapper", "TestEntity"); - testEntity.AddStructuralProperty("SampleProperty", EdmPrimitiveTypeKind.Binary); - model.AddElement(testEntity); - - // containment - // my order - EdmEntityType myOrder = new EdmEntityType("NS", "MyOrder"); - myOrder.AddKeys(myOrder.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - myOrder.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - model.AddElement(myOrder); - - // order line - EdmEntityType orderLine = new EdmEntityType("NS", "OrderLine"); - orderLine.AddKeys(orderLine.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - orderLine.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - model.AddElement(orderLine); - - EdmNavigationProperty orderLinesNavProp = myOrder.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "OrderLines", - TargetMultiplicity = EdmMultiplicity.Many, - Target = orderLine, - ContainsTarget = true, - }); - - EdmNavigationProperty nonContainedOrderLinesNavProp = myOrder.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "NonContainedOrderLines", - TargetMultiplicity = EdmMultiplicity.Many, - Target = orderLine, - ContainsTarget = false, - }); - - EdmAction tag = new EdmAction("NS", "tag", returnType: null, isBound: true, entitySetPathExpression: null); - tag.AddParameter("entity", new EdmEntityTypeReference(orderLine, false)); - model.AddElement(tag); - - // entity sets - EdmEntityContainer container = new EdmEntityContainer("NS", "ModelWithInheritance"); - model.AddElement(container); - EdmEntitySet customers = container.AddEntitySet("Customers", customer); - EdmEntitySet orders = container.AddEntitySet("Orders", order); - EdmEntitySet myOrders = container.AddEntitySet("MyOrders", myOrder); - - // singletons - EdmSingleton vipCustomer = container.AddSingleton("VipCustomer", customer); - EdmSingleton mary = container.AddSingleton("Mary", customer); - EdmSingleton rootOrder = container.AddSingleton("RootOrder", order); - - // annotations - model.SetOptimisticConcurrencyAnnotation(customers, new[] { city }); - - // containment - IEdmContainedEntitySet orderLines = (IEdmContainedEntitySet)myOrders.FindNavigationTarget(orderLinesNavProp); - - // no-containment - IEdmNavigationSource nonContainedOrderLines = myOrders.FindNavigationTarget(nonContainedOrderLinesNavProp); - - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmTypeReference stringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); - IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); - - // actions - EdmAction upgrade = new EdmAction("NS", "upgrade", returnType: null, isBound: true, entitySetPathExpression: null); - upgrade.AddParameter("entity", new EdmEntityTypeReference(customer, false)); - model.AddElement(upgrade); - - EdmAction specialUpgrade = - new EdmAction("NS", "specialUpgrade", returnType: null, isBound: true, entitySetPathExpression: null); - specialUpgrade.AddParameter("entity", new EdmEntityTypeReference(specialCustomer, false)); - model.AddElement(specialUpgrade); - - // actions bound to collection - EdmAction upgradeAll = new EdmAction("NS", "UpgradeAll", returnType: null, isBound: true, entitySetPathExpression: null); - upgradeAll.AddParameter("entityset", - new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - model.AddElement(upgradeAll); - - EdmAction upgradeSpecialAll = new EdmAction("NS", "UpgradeSpecialAll", returnType: null, isBound: true, entitySetPathExpression: null); - upgradeSpecialAll.AddParameter("entityset", - new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(specialCustomer, false)))); - model.AddElement(upgradeSpecialAll); - - // action with optional parameters - EdmAction updateSalaray = new EdmAction("NS", "UpdateSalaray", returnType: null, isBound: true, entitySetPathExpression: null); - updateSalaray.AddParameter("entity", new EdmEntityTypeReference(customer, false)); - updateSalaray.AddParameter("minSalaray", intType); - updateSalaray.AddOptionalParameter("maxSalaray", intType); - updateSalaray.AddOptionalParameter("aveSalaray", intType, "129"); - - // functions - EdmFunction IsUpgraded = new EdmFunction( - "NS", - "IsUpgraded", - returnType, - isBound: true, - entitySetPathExpression: null, - isComposable: false); - IsUpgraded.AddParameter("entity", new EdmEntityTypeReference(customer, false)); - model.AddElement(IsUpgraded); - - EdmFunction orderByCityAndAmount = new EdmFunction( - "NS", - "OrderByCityAndAmount", - stringType, - isBound: true, - entitySetPathExpression: null, - isComposable: false); - orderByCityAndAmount.AddParameter("entity", new EdmEntityTypeReference(customer, false)); - orderByCityAndAmount.AddParameter("city", stringType); - orderByCityAndAmount.AddParameter("amount", intType); - model.AddElement(orderByCityAndAmount); - - EdmFunction getOrders = new EdmFunction( - "NS", - "GetOrders", - EdmCoreModel.GetCollection(order.ToEdmTypeReference(false)), - isBound: true, - entitySetPathExpression: null, - isComposable: true); - getOrders.AddParameter("entity", new EdmEntityTypeReference(customer, false)); - getOrders.AddParameter("parameter", intType); - model.AddElement(getOrders); - - EdmFunction IsSpecialUpgraded = new EdmFunction( - "NS", - "IsSpecialUpgraded", - returnType, - isBound: true, - entitySetPathExpression: null, - isComposable: false); - IsSpecialUpgraded.AddParameter("entity", new EdmEntityTypeReference(specialCustomer, false)); - model.AddElement(IsSpecialUpgraded); - - EdmFunction getSalary = new EdmFunction( - "NS", - "GetSalary", - stringType, - isBound: true, - entitySetPathExpression: null, - isComposable: false); - getSalary.AddParameter("entity", new EdmEntityTypeReference(customer, false)); - model.AddElement(getSalary); - - getSalary = new EdmFunction( - "NS", - "GetSalary", - stringType, - isBound: true, - entitySetPathExpression: null, - isComposable: false); - getSalary.AddParameter("entity", new EdmEntityTypeReference(specialCustomer, false)); - model.AddElement(getSalary); - - EdmFunction IsAnyUpgraded = new EdmFunction( - "NS", - "IsAnyUpgraded", - returnType, - isBound: true, - entitySetPathExpression: null, - isComposable: false); - EdmCollectionType edmCollectionType = new EdmCollectionType(new EdmEntityTypeReference(customer, false)); - IsAnyUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(edmCollectionType)); - model.AddElement(IsAnyUpgraded); - - EdmFunction isCustomerUpgradedWithParam = new EdmFunction( - "NS", - "IsUpgradedWithParam", - returnType, - isBound: true, - entitySetPathExpression: null, - isComposable: false); - isCustomerUpgradedWithParam.AddParameter("entity", new EdmEntityTypeReference(customer, false)); - isCustomerUpgradedWithParam.AddParameter("city", EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false)); - model.AddElement(isCustomerUpgradedWithParam); - - EdmFunction isCustomerLocal = new EdmFunction( - "NS", - "IsLocal", - returnType, - isBound: true, - entitySetPathExpression: null, - isComposable: false); - isCustomerLocal.AddParameter("entity", new EdmEntityTypeReference(customer, false)); - model.AddElement(isCustomerLocal); - - EdmFunction entityFunction = new EdmFunction( - "NS", - "GetCustomer", - returnType, - isBound: true, - entitySetPathExpression: null, - isComposable: false); - entityFunction.AddParameter("entity", new EdmEntityTypeReference(customer, false)); - entityFunction.AddParameter("customer", new EdmEntityTypeReference(customer, false)); - model.AddElement(entityFunction); - - EdmFunction getOrder = new EdmFunction( - "NS", - "GetOrder", - order.ToEdmTypeReference(false), - isBound: true, - entitySetPathExpression: null, - isComposable: true); // Composable - getOrder.AddParameter("entity", new EdmEntityTypeReference(customer, false)); - getOrder.AddParameter("orderId", intType); - model.AddElement(getOrder); - - // functions with entity set path - EdmFunction getSimilarCustomers = new EdmFunction( - "NS", - "GetTop", - new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false))), - isBound: true, - entitySetPathExpression: customers.Path, - isComposable: true); - getSimilarCustomers.AddParameter( - "Customers", - new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - model.AddElement(getSimilarCustomers); - - EdmFunction getBestOrders = new EdmFunction( - "NS", - "GetBestOrders", - new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(order, false))), - isBound: true, - entitySetPathExpression: new EdmPathExpression("Customers", "Orders"), - isComposable: true - ); - getBestOrders.AddParameter("Customers", - new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - model.AddElement(getBestOrders); - - // functions bound to collection - EdmFunction isAllUpgraded = new EdmFunction("NS", "IsAllUpgraded", returnType, isBound: true, - entitySetPathExpression: null, isComposable: false); - isAllUpgraded.AddParameter("entityset", - new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - isAllUpgraded.AddParameter("param", intType); - model.AddElement(isAllUpgraded); - - EdmFunction isSpecialAllUpgraded = new EdmFunction("NS", "IsSpecialAllUpgraded", returnType, isBound: true, - entitySetPathExpression: null, isComposable: false); - isSpecialAllUpgraded.AddParameter("entityset", - new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(specialCustomer, false)))); - isSpecialAllUpgraded.AddParameter("param", intType); - model.AddElement(isSpecialAllUpgraded); - - // unbound function and imports - // function with entity set path - EdmFunction getTopCustomers = new EdmFunction( - "NS", - "GetTopCustomers", - new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false))), - isBound: false, - entitySetPathExpression: customers.Path, - isComposable: false - ); - - container.AddFunctionImport("GetTopCustomers", getTopCustomers, customers.Path, includeInServiceDocument: true); - - // function without entity set path - EdmFunction getTotalSalesAmount = new EdmFunction( - "NS", - "GetTotalSalesAmount", - intType, - isBound: false, - entitySetPathExpression: null, - isComposable: false - ); - - container.AddFunctionImport("GetTotalSalesAmount", getTotalSalesAmount, null, includeInServiceDocument: true); - - // function with optional parameters - EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", intType, isBound: true, entitySetPathExpression: null, isComposable: false); - getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - getSalaray.AddParameter("minSalary", intType); - getSalaray.AddOptionalParameter("maxSalary", intType); - getSalaray.AddOptionalParameter("aveSalary", intType, "129"); - model.AddElement(getSalaray); - - // navigation properties - EdmNavigationProperty ordersNavProp = customer.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "Orders", - TargetMultiplicity = EdmMultiplicity.Many, - Target = order - }); - mary.AddNavigationTarget(ordersNavProp, orders); - vipCustomer.AddNavigationTarget(ordersNavProp, orders); - customers.AddNavigationTarget(ordersNavProp, orders); - orders.AddNavigationTarget( - order.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "Customer", - TargetMultiplicity = EdmMultiplicity.ZeroOrOne, - Target = customer - }), - customers); - - // navigation properties on derived types. - EdmNavigationProperty specialOrdersNavProp = specialCustomer.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "SpecialOrders", - TargetMultiplicity = EdmMultiplicity.Many, - Target = order - }); - vipCustomer.AddNavigationTarget(specialOrdersNavProp, orders); - customers.AddNavigationTarget(specialOrdersNavProp, orders); - orders.AddNavigationTarget( - specialOrder.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "SpecialCustomer", - TargetMultiplicity = EdmMultiplicity.ZeroOrOne, - Target = customer - }), - customers); - model.SetAnnotationValue(model, new BindableOperationFinder(model)); - - // set properties - Model = model; - Container = container; - Customer = customer; - Order = order; - Address = address; - Account = account; - SpecialCustomer = specialCustomer; - SpecialOrder = specialOrder; - Orders = orders; - Customers = customers; - VipCustomer = vipCustomer; - Mary = mary; - RootOrder = rootOrder; - OrderLine = orderLine; - OrderLines = orderLines; - NonContainedOrderLines = nonContainedOrderLines; - UpgradeCustomer = upgrade; - UpgradeSpecialCustomer = specialUpgrade; - CustomerName = customerName; - IsCustomerUpgraded = isCustomerUpgradedWithParam; - IsSpecialCustomerUpgraded = IsSpecialUpgraded; - Tag = tag; - } - - public EdmModel Model { get; private set; } - - public EdmEntityType Customer { get; private set; } - - public EdmEntityType SpecialCustomer { get; private set; } - - public EdmEntityType Order { get; private set; } - - public EdmEntityType SpecialOrder { get; private set; } - - public EdmEntityType OrderLine { get; private set; } - - public EdmComplexType Address { get; private set; } - - public EdmComplexType Account { get; private set; } // Open Complex Type - - public EdmEntitySet Customers { get; private set; } - - public EdmEntitySet Orders { get; private set; } - - public EdmSingleton VipCustomer { get; private set; } - - public EdmSingleton Mary { get; private set; } - - public EdmSingleton RootOrder { get; private set; } - - public IEdmContainedEntitySet OrderLines { get; private set; } - - public IEdmNavigationSource NonContainedOrderLines { get; private set; } - - public EdmEntityContainer Container { get; private set; } - - public EdmAction UpgradeCustomer { get; private set; } - - public EdmAction UpgradeSpecialCustomer { get; private set; } - - public EdmAction Tag { get; private set; } - - public IEdmProperty CustomerName { get; private set; } - - public EdmFunction IsCustomerUpgraded { get; private set; } - - public EdmFunction IsSpecialCustomerUpgraded { get; private set; } + EdmModel model = new EdmModel(); + + // Enum type simpleEnum + EdmEnumType simpleEnum = new EdmEnumType("NS", "SimpleEnum"); + simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "First", new EdmEnumMemberValue(0))); + simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "Second", new EdmEnumMemberValue(1))); + simpleEnum.AddMember(new EdmEnumMember(simpleEnum, "Third", new EdmEnumMemberValue(2))); + model.AddElement(simpleEnum); + + // complex type address + EdmComplexType address = new EdmComplexType("NS", "Address"); + address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("State", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("ZipCode", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("CountryOrRegion", EdmPrimitiveTypeKind.String); + model.AddElement(address); + + // open complex type "Account" + EdmComplexType account = new EdmComplexType("NS", "Account", null, false, true); + account.AddStructuralProperty("Bank", EdmPrimitiveTypeKind.String); + account.AddStructuralProperty("CardNum", EdmPrimitiveTypeKind.Int64); + account.AddStructuralProperty("BankAddress", new EdmComplexTypeReference(address, isNullable: true)); + model.AddElement(account); + + EdmComplexType specialAccount = new EdmComplexType("NS", "SpecialAccount", account, false, true); + specialAccount.AddStructuralProperty("SpecialCard", EdmPrimitiveTypeKind.String); + model.AddElement(specialAccount); + + // entity type customer + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + IEdmProperty customerName = customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + customer.AddStructuralProperty("SimpleEnum", simpleEnum.ToEdmTypeReference(isNullable: false)); + customer.AddStructuralProperty("Address", new EdmComplexTypeReference(address, isNullable: true)); + customer.AddStructuralProperty("Account", new EdmComplexTypeReference(account, isNullable: true)); + customer.AddStructuralProperty("OtherAccounts", new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(account, true)))); + IEdmTypeReference primitiveTypeReference = EdmCoreModel.Instance.GetPrimitive( + EdmPrimitiveTypeKind.String, + isNullable: true); + var city = customer.AddStructuralProperty( + "City", + primitiveTypeReference, + defaultValue: null); + model.AddElement(customer); + + // derived entity type special customer + EdmEntityType specialCustomer = new EdmEntityType("NS", "SpecialCustomer", customer); + specialCustomer.AddStructuralProperty("SpecialCustomerProperty", EdmPrimitiveTypeKind.Guid); + specialCustomer.AddStructuralProperty("SpecialAddress", new EdmComplexTypeReference(address, isNullable: true)); + model.AddElement(specialCustomer); + + // entity type order (open entity type) + EdmEntityType order = new EdmEntityType("NS", "Order", null, false, true); + // EdmEntityType order = new EdmEntityType("NS", "Order"); + order.AddKeys(order.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + order.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + order.AddStructuralProperty("Amount", EdmPrimitiveTypeKind.Int32); + model.AddElement(order); + + // derived entity type special order + EdmEntityType specialOrder = new EdmEntityType("NS", "SpecialOrder", order, false, true); + specialOrder.AddStructuralProperty("SpecialOrderProperty", EdmPrimitiveTypeKind.Guid); + model.AddElement(specialOrder); + + // test entity + EdmEntityType testEntity = new EdmEntityType("Microsoft.AspNetCore.OData.Tests.Query.Wrapper", "TestEntity"); + testEntity.AddStructuralProperty("SampleProperty", EdmPrimitiveTypeKind.Binary); + model.AddElement(testEntity); + + // containment + // my order + EdmEntityType myOrder = new EdmEntityType("NS", "MyOrder"); + myOrder.AddKeys(myOrder.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + myOrder.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + model.AddElement(myOrder); + + // order line + EdmEntityType orderLine = new EdmEntityType("NS", "OrderLine"); + orderLine.AddKeys(orderLine.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + orderLine.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + model.AddElement(orderLine); + + EdmNavigationProperty orderLinesNavProp = myOrder.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "OrderLines", + TargetMultiplicity = EdmMultiplicity.Many, + Target = orderLine, + ContainsTarget = true, + }); + + EdmNavigationProperty nonContainedOrderLinesNavProp = myOrder.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "NonContainedOrderLines", + TargetMultiplicity = EdmMultiplicity.Many, + Target = orderLine, + ContainsTarget = false, + }); + + EdmAction tag = new EdmAction("NS", "tag", returnType: null, isBound: true, entitySetPathExpression: null); + tag.AddParameter("entity", new EdmEntityTypeReference(orderLine, false)); + model.AddElement(tag); + + // entity sets + EdmEntityContainer container = new EdmEntityContainer("NS", "ModelWithInheritance"); + model.AddElement(container); + EdmEntitySet customers = container.AddEntitySet("Customers", customer); + EdmEntitySet orders = container.AddEntitySet("Orders", order); + EdmEntitySet myOrders = container.AddEntitySet("MyOrders", myOrder); + + // singletons + EdmSingleton vipCustomer = container.AddSingleton("VipCustomer", customer); + EdmSingleton mary = container.AddSingleton("Mary", customer); + EdmSingleton rootOrder = container.AddSingleton("RootOrder", order); + + // annotations + model.SetOptimisticConcurrencyAnnotation(customers, new[] { city }); + + // containment + IEdmContainedEntitySet orderLines = (IEdmContainedEntitySet)myOrders.FindNavigationTarget(orderLinesNavProp); + + // no-containment + IEdmNavigationSource nonContainedOrderLines = myOrders.FindNavigationTarget(nonContainedOrderLinesNavProp); + + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmTypeReference stringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); + IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); + + // actions + EdmAction upgrade = new EdmAction("NS", "upgrade", returnType: null, isBound: true, entitySetPathExpression: null); + upgrade.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + model.AddElement(upgrade); + + EdmAction specialUpgrade = + new EdmAction("NS", "specialUpgrade", returnType: null, isBound: true, entitySetPathExpression: null); + specialUpgrade.AddParameter("entity", new EdmEntityTypeReference(specialCustomer, false)); + model.AddElement(specialUpgrade); + + // actions bound to collection + EdmAction upgradeAll = new EdmAction("NS", "UpgradeAll", returnType: null, isBound: true, entitySetPathExpression: null); + upgradeAll.AddParameter("entityset", + new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + model.AddElement(upgradeAll); + + EdmAction upgradeSpecialAll = new EdmAction("NS", "UpgradeSpecialAll", returnType: null, isBound: true, entitySetPathExpression: null); + upgradeSpecialAll.AddParameter("entityset", + new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(specialCustomer, false)))); + model.AddElement(upgradeSpecialAll); + + // action with optional parameters + EdmAction updateSalaray = new EdmAction("NS", "UpdateSalaray", returnType: null, isBound: true, entitySetPathExpression: null); + updateSalaray.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + updateSalaray.AddParameter("minSalaray", intType); + updateSalaray.AddOptionalParameter("maxSalaray", intType); + updateSalaray.AddOptionalParameter("aveSalaray", intType, "129"); + + // functions + EdmFunction IsUpgraded = new EdmFunction( + "NS", + "IsUpgraded", + returnType, + isBound: true, + entitySetPathExpression: null, + isComposable: false); + IsUpgraded.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + model.AddElement(IsUpgraded); + + EdmFunction orderByCityAndAmount = new EdmFunction( + "NS", + "OrderByCityAndAmount", + stringType, + isBound: true, + entitySetPathExpression: null, + isComposable: false); + orderByCityAndAmount.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + orderByCityAndAmount.AddParameter("city", stringType); + orderByCityAndAmount.AddParameter("amount", intType); + model.AddElement(orderByCityAndAmount); + + EdmFunction getOrders = new EdmFunction( + "NS", + "GetOrders", + EdmCoreModel.GetCollection(order.ToEdmTypeReference(false)), + isBound: true, + entitySetPathExpression: null, + isComposable: true); + getOrders.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + getOrders.AddParameter("parameter", intType); + model.AddElement(getOrders); + + EdmFunction IsSpecialUpgraded = new EdmFunction( + "NS", + "IsSpecialUpgraded", + returnType, + isBound: true, + entitySetPathExpression: null, + isComposable: false); + IsSpecialUpgraded.AddParameter("entity", new EdmEntityTypeReference(specialCustomer, false)); + model.AddElement(IsSpecialUpgraded); + + EdmFunction getSalary = new EdmFunction( + "NS", + "GetSalary", + stringType, + isBound: true, + entitySetPathExpression: null, + isComposable: false); + getSalary.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + model.AddElement(getSalary); + + getSalary = new EdmFunction( + "NS", + "GetSalary", + stringType, + isBound: true, + entitySetPathExpression: null, + isComposable: false); + getSalary.AddParameter("entity", new EdmEntityTypeReference(specialCustomer, false)); + model.AddElement(getSalary); + + EdmFunction IsAnyUpgraded = new EdmFunction( + "NS", + "IsAnyUpgraded", + returnType, + isBound: true, + entitySetPathExpression: null, + isComposable: false); + EdmCollectionType edmCollectionType = new EdmCollectionType(new EdmEntityTypeReference(customer, false)); + IsAnyUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(edmCollectionType)); + model.AddElement(IsAnyUpgraded); + + EdmFunction isCustomerUpgradedWithParam = new EdmFunction( + "NS", + "IsUpgradedWithParam", + returnType, + isBound: true, + entitySetPathExpression: null, + isComposable: false); + isCustomerUpgradedWithParam.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + isCustomerUpgradedWithParam.AddParameter("city", EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false)); + model.AddElement(isCustomerUpgradedWithParam); + + EdmFunction isCustomerLocal = new EdmFunction( + "NS", + "IsLocal", + returnType, + isBound: true, + entitySetPathExpression: null, + isComposable: false); + isCustomerLocal.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + model.AddElement(isCustomerLocal); + + EdmFunction entityFunction = new EdmFunction( + "NS", + "GetCustomer", + returnType, + isBound: true, + entitySetPathExpression: null, + isComposable: false); + entityFunction.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + entityFunction.AddParameter("customer", new EdmEntityTypeReference(customer, false)); + model.AddElement(entityFunction); + + EdmFunction getOrder = new EdmFunction( + "NS", + "GetOrder", + order.ToEdmTypeReference(false), + isBound: true, + entitySetPathExpression: null, + isComposable: true); // Composable + getOrder.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + getOrder.AddParameter("orderId", intType); + model.AddElement(getOrder); + + // functions with entity set path + EdmFunction getSimilarCustomers = new EdmFunction( + "NS", + "GetTop", + new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false))), + isBound: true, + entitySetPathExpression: customers.Path, + isComposable: true); + getSimilarCustomers.AddParameter( + "Customers", + new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + model.AddElement(getSimilarCustomers); + + EdmFunction getBestOrders = new EdmFunction( + "NS", + "GetBestOrders", + new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(order, false))), + isBound: true, + entitySetPathExpression: new EdmPathExpression("Customers", "Orders"), + isComposable: true + ); + getBestOrders.AddParameter("Customers", + new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + model.AddElement(getBestOrders); + + // functions bound to collection + EdmFunction isAllUpgraded = new EdmFunction("NS", "IsAllUpgraded", returnType, isBound: true, + entitySetPathExpression: null, isComposable: false); + isAllUpgraded.AddParameter("entityset", + new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + isAllUpgraded.AddParameter("param", intType); + model.AddElement(isAllUpgraded); + + EdmFunction isSpecialAllUpgraded = new EdmFunction("NS", "IsSpecialAllUpgraded", returnType, isBound: true, + entitySetPathExpression: null, isComposable: false); + isSpecialAllUpgraded.AddParameter("entityset", + new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(specialCustomer, false)))); + isSpecialAllUpgraded.AddParameter("param", intType); + model.AddElement(isSpecialAllUpgraded); + + // unbound function and imports + // function with entity set path + EdmFunction getTopCustomers = new EdmFunction( + "NS", + "GetTopCustomers", + new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false))), + isBound: false, + entitySetPathExpression: customers.Path, + isComposable: false + ); + + container.AddFunctionImport("GetTopCustomers", getTopCustomers, customers.Path, includeInServiceDocument: true); + + // function without entity set path + EdmFunction getTotalSalesAmount = new EdmFunction( + "NS", + "GetTotalSalesAmount", + intType, + isBound: false, + entitySetPathExpression: null, + isComposable: false + ); + + container.AddFunctionImport("GetTotalSalesAmount", getTotalSalesAmount, null, includeInServiceDocument: true); + + // function with optional parameters + EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", intType, isBound: true, entitySetPathExpression: null, isComposable: false); + getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + getSalaray.AddParameter("minSalary", intType); + getSalaray.AddOptionalParameter("maxSalary", intType); + getSalaray.AddOptionalParameter("aveSalary", intType, "129"); + model.AddElement(getSalaray); + + // navigation properties + EdmNavigationProperty ordersNavProp = customer.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "Orders", + TargetMultiplicity = EdmMultiplicity.Many, + Target = order + }); + mary.AddNavigationTarget(ordersNavProp, orders); + vipCustomer.AddNavigationTarget(ordersNavProp, orders); + customers.AddNavigationTarget(ordersNavProp, orders); + orders.AddNavigationTarget( + order.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo + { + Name = "Customer", + TargetMultiplicity = EdmMultiplicity.ZeroOrOne, + Target = customer + }), + customers); + + // navigation properties on derived types. + EdmNavigationProperty specialOrdersNavProp = specialCustomer.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "SpecialOrders", + TargetMultiplicity = EdmMultiplicity.Many, + Target = order + }); + vipCustomer.AddNavigationTarget(specialOrdersNavProp, orders); + customers.AddNavigationTarget(specialOrdersNavProp, orders); + orders.AddNavigationTarget( + specialOrder.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo + { + Name = "SpecialCustomer", + TargetMultiplicity = EdmMultiplicity.ZeroOrOne, + Target = customer + }), + customers); + model.SetAnnotationValue(model, new BindableOperationFinder(model)); + + // set properties + Model = model; + Container = container; + Customer = customer; + Order = order; + Address = address; + Account = account; + SpecialCustomer = specialCustomer; + SpecialOrder = specialOrder; + Orders = orders; + Customers = customers; + VipCustomer = vipCustomer; + Mary = mary; + RootOrder = rootOrder; + OrderLine = orderLine; + OrderLines = orderLines; + NonContainedOrderLines = nonContainedOrderLines; + UpgradeCustomer = upgrade; + UpgradeSpecialCustomer = specialUpgrade; + CustomerName = customerName; + IsCustomerUpgraded = isCustomerUpgradedWithParam; + IsSpecialCustomerUpgraded = IsSpecialUpgraded; + Tag = tag; } + + public EdmModel Model { get; private set; } + + public EdmEntityType Customer { get; private set; } + + public EdmEntityType SpecialCustomer { get; private set; } + + public EdmEntityType Order { get; private set; } + + public EdmEntityType SpecialOrder { get; private set; } + + public EdmEntityType OrderLine { get; private set; } + + public EdmComplexType Address { get; private set; } + + public EdmComplexType Account { get; private set; } // Open Complex Type + + public EdmEntitySet Customers { get; private set; } + + public EdmEntitySet Orders { get; private set; } + + public EdmSingleton VipCustomer { get; private set; } + + public EdmSingleton Mary { get; private set; } + + public EdmSingleton RootOrder { get; private set; } + + public IEdmContainedEntitySet OrderLines { get; private set; } + + public IEdmNavigationSource NonContainedOrderLines { get; private set; } + + public EdmEntityContainer Container { get; private set; } + + public EdmAction UpgradeCustomer { get; private set; } + + public EdmAction UpgradeSpecialCustomer { get; private set; } + + public EdmAction Tag { get; private set; } + + public IEdmProperty CustomerName { get; private set; } + + public EdmFunction IsCustomerUpgraded { get; private set; } + + public EdmFunction IsSpecialCustomerUpgraded { get; private set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/DataTypes.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/DataTypes.cs index 2e17c41b6..cfef41016 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/DataTypes.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/DataTypes.cs @@ -8,62 +8,61 @@ using System; using System.Xml.Linq; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class DataTypes { - public class DataTypes - { - public int Id { get; set; } - public Guid GuidProp { get; set; } - public DateTime DateTimeProperty { get; set; } - public DateTimeOffset DateTimeProp { get; set; } - public DateTimeOffset DateTimeOffsetProp { get; set; } - public byte[] ByteArrayProp { get; set; } - public byte[] ByteArrayPropWithNullValue { get; set; } - public TimeSpan TimeSpanProp { get; set; } - public decimal DecimalProp { get; set; } - public double DoubleProp { get; set; } - public float FloatProp { get; set; } - public Single SingleProp { get; set; } - public long LongProp { get; set; } - public int IntProp { get; set; } - public string StringProp { get; set; } - public bool BoolProp { get; set; } + public int Id { get; set; } + public Guid GuidProp { get; set; } + public DateTime DateTimeProperty { get; set; } + public DateTimeOffset DateTimeProp { get; set; } + public DateTimeOffset DateTimeOffsetProp { get; set; } + public byte[] ByteArrayProp { get; set; } + public byte[] ByteArrayPropWithNullValue { get; set; } + public TimeSpan TimeSpanProp { get; set; } + public decimal DecimalProp { get; set; } + public double DoubleProp { get; set; } + public float FloatProp { get; set; } + public Single SingleProp { get; set; } + public long LongProp { get; set; } + public int IntProp { get; set; } + public string StringProp { get; set; } + public bool BoolProp { get; set; } - public ushort UShortProp { get; set; } - public uint UIntProp { get; set; } - public ulong ULongProp { get; set; } - public char CharProp { get; set; } - public byte ByteProp { get; set; } + public ushort UShortProp { get; set; } + public uint UIntProp { get; set; } + public ulong ULongProp { get; set; } + public char CharProp { get; set; } + public byte ByteProp { get; set; } - public short? NullableShortProp { get; set; } - public int? NullableIntProp { get; set; } - public long? NullableLongProp { get; set; } - public Single? NullableSingleProp { get; set; } - public double? NullableDoubleProp { get; set; } - public decimal? NullableDecimalProp { get; set; } - public bool? NullableBoolProp { get; set; } - public byte? NullableByteProp { get; set; } - public Guid? NullableGuidProp { get; set; } - public DateTimeOffset? NullableDateTimeOffsetProp { get; set; } - public TimeSpan? NullableTimeSpanProp { get; set; } + public short? NullableShortProp { get; set; } + public int? NullableIntProp { get; set; } + public long? NullableLongProp { get; set; } + public Single? NullableSingleProp { get; set; } + public double? NullableDoubleProp { get; set; } + public decimal? NullableDecimalProp { get; set; } + public bool? NullableBoolProp { get; set; } + public byte? NullableByteProp { get; set; } + public Guid? NullableGuidProp { get; set; } + public DateTimeOffset? NullableDateTimeOffsetProp { get; set; } + public TimeSpan? NullableTimeSpanProp { get; set; } - public ushort? NullableUShortProp { get; set; } - public uint? NullableUIntProp { get; set; } - public ulong? NullableULongProp { get; set; } - public char? NullableCharProp { get; set; } + public ushort? NullableUShortProp { get; set; } + public uint? NullableUIntProp { get; set; } + public ulong? NullableULongProp { get; set; } + public char? NullableCharProp { get; set; } - public char[] CharArrayProp { get; set; } + public char[] CharArrayProp { get; set; } - public XElement XElementProp { get; set; } + public XElement XElementProp { get; set; } - public SimpleEnum SimpleEnumProp { get; set; } - public FlagsEnum FlagsEnumProp { get; set; } - public LongEnum LongEnumProp { get; set; } - public SimpleEnum? NullableSimpleEnumProp { get; set; } + public SimpleEnum SimpleEnumProp { get; set; } + public FlagsEnum FlagsEnumProp { get; set; } + public LongEnum LongEnumProp { get; set; } + public SimpleEnum? NullableSimpleEnumProp { get; set; } - public Product EntityProp { get; set; } - public Address ComplexProp { get; set; } + public Product EntityProp { get; set; } + public Address ComplexProp { get; set; } - public string Inaccessible() { return String.Empty; } - } + public string Inaccessible() { return String.Empty; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/Employee.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/Employee.cs index d589a3b15..1420d8488 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/Employee.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/Employee.cs @@ -9,47 +9,46 @@ using System.Collections.Generic; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class Employee { - public class Employee - { - public int EmployeeID { get; set; } + public int EmployeeID { get; set; } - public string EmployeeName { get; set; } + public string EmployeeName { get; set; } - public Decimal BaseSalary { get; set; } + public Decimal BaseSalary { get; set; } - public DateTimeOffset Birthday { get; set; } + public DateTimeOffset Birthday { get; set; } - public IList IsCeoOf { get; set; } + public IList IsCeoOf { get; set; } - public int WorkCompanyId { get; set; } + public int WorkCompanyId { get; set; } - public Company WorkCompany { get; set; } + public Company WorkCompany { get; set; } - [Singleton] - public Employee Boss { get; set; } + [Singleton] + public Employee Boss { get; set; } - public Address HomeAddress { get; set; } + public Address HomeAddress { get; set; } - public IList DirectReports { get; set; } - } + public IList DirectReports { get; set; } +} - public class Manager : Employee - { - public Decimal ExtraDraw; - public Address ExtraOffice { get; set; } - } +public class Manager : Employee +{ + public Decimal ExtraDraw; + public Address ExtraOffice { get; set; } +} - public class Engineer : Employee - { - public int Level { get; set; } - public Decimal YearEndBonus { get; set; } - } +public class Engineer : Employee +{ + public int Level { get; set; } + public Decimal YearEndBonus { get; set; } +} - public class SalesPerson : Employee - { - public Decimal Bonus { get; set; } - public IList Customers { get; set; } - } +public class SalesPerson : Employee +{ + public Decimal Bonus { get; set; } + public IList Customers { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/EnumModel.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/EnumModel.cs index 8571f4942..b9f65bd9c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/EnumModel.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/EnumModel.cs @@ -7,96 +7,95 @@ using System; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class EnumModel { - public class EnumModel - { - public int Id { get; set; } + public int Id { get; set; } - public SimpleEnum Simple { get; set; } + public SimpleEnum Simple { get; set; } - public SimpleEnum? SimpleNullable { get; set; } + public SimpleEnum? SimpleNullable { get; set; } - public LongEnum Long { get; set; } + public LongEnum Long { get; set; } - public ByteEnum Byte { get; set; } + public ByteEnum Byte { get; set; } - public SByteEnum SByte { get; set; } + public SByteEnum SByte { get; set; } - public ShortEnum Short { get; set; } + public ShortEnum Short { get; set; } - public UShortEnum UShort { get; set; } + public UShortEnum UShort { get; set; } - public UIntEnum UInt { get; set; } + public UIntEnum UInt { get; set; } - public FlagsEnum Flag { get; set; } + public FlagsEnum Flag { get; set; } - public FlagsEnum? FlagNullable { get; set; } - } + public FlagsEnum? FlagNullable { get; set; } +} - [Flags] - public enum FlagsEnum - { - One = 0x1, +[Flags] +public enum FlagsEnum +{ + One = 0x1, - Two = 0x2, + Two = 0x2, - Four = 0x4 - } + Four = 0x4 +} - public enum LongEnum : long - { - FirstLong, +public enum LongEnum : long +{ + FirstLong, - SecondLong, + SecondLong, - ThirdLong, + ThirdLong, - FourthLong - } + FourthLong +} - public enum ShortEnum : short - { - FirstShort, +public enum ShortEnum : short +{ + FirstShort, - SecondShort, + SecondShort, - ThirdShort - } + ThirdShort +} - public enum ByteEnum : byte - { - FirstByte, +public enum ByteEnum : byte +{ + FirstByte, - SecondByte, + SecondByte, - ThirdByte - } + ThirdByte +} - public enum UIntEnum : uint - { - FirstUInt, +public enum UIntEnum : uint +{ + FirstUInt, - SecondUInt, + SecondUInt, - ThirdUInt - } + ThirdUInt +} - public enum SByteEnum : sbyte - { - FirstSByte, +public enum SByteEnum : sbyte +{ + FirstSByte, - SecondSByte, + SecondSByte, - ThirdSByte - } + ThirdSByte +} - public enum UShortEnum : ushort - { - FirstUShort, +public enum UShortEnum : ushort +{ + FirstUShort, - SecondUShort, + SecondUShort, - ThirdUShort - } + ThirdUShort } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/Gender.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/Gender.cs index 1a8fce04d..3c8f35b83 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/Gender.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/Gender.cs @@ -5,10 +5,9 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public enum Gender { - public enum Gender - { - } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/ModelBuilderHelpers.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/ModelBuilderHelpers.cs index 656dc23e5..d6b7bf25a 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/ModelBuilderHelpers.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/ModelBuilderHelpers.cs @@ -9,320 +9,319 @@ using System.Linq.Expressions; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public static class ModelBuilderHelpers { - public static class ModelBuilderHelpers - { - public static ODataModelBuilder Add_Color_EnumType(this ODataModelBuilder builder) - { - var color = builder.EnumType(); - color.Member(Color.Red); - color.Member(Color.Green); - color.Member(Color.Blue); - return builder; - } - - public static ODataModelBuilder Add_SimpleEnum_EnumType(this ODataModelBuilder builder) - { - var simpleEnum = builder.EnumType(); - simpleEnum.Member(SimpleEnum.First); - simpleEnum.Member(SimpleEnum.Second); - simpleEnum.Member(SimpleEnum.Third); - return builder; - } - - public static ODataModelBuilder Add_FlagsEnum_EnumType(this ODataModelBuilder builder) - { - var flagsEnum = builder.EnumType(); - flagsEnum.Member(FlagsEnum.One); - flagsEnum.Member(FlagsEnum.Two); - flagsEnum.Member(FlagsEnum.Four); - return builder; - } - - public static ODataModelBuilder Add_LongEnum_EnumType(this ODataModelBuilder builder) - { - var longEnum = builder.EnumType(); - longEnum.Member(LongEnum.FirstLong); - longEnum.Member(LongEnum.SecondLong); - longEnum.Member(LongEnum.ThirdLong); - return builder; - } - - public static ODataModelBuilder Add_ByteEnum_EnumType(this ODataModelBuilder builder) - { - EnumTypeConfiguration byteEnum = builder.EnumType(); - byteEnum.Member(ByteEnum.FirstByte); - byteEnum.Member(ByteEnum.SecondByte); - byteEnum.Member(ByteEnum.ThirdByte); - return builder; - } - - public static ODataModelBuilder Add_SByteEnum_EnumType(this ODataModelBuilder builder) - { - EnumTypeConfiguration sByteEnum = builder.EnumType(); - sByteEnum.Member(SByteEnum.FirstSByte); - sByteEnum.Member(SByteEnum.SecondSByte); - sByteEnum.Member(SByteEnum.ThirdSByte); - return builder; - } - - public static ODataModelBuilder Add_ShortEnum_EnumType(this ODataModelBuilder builder) - { - EnumTypeConfiguration shortEnum = builder.EnumType(); - shortEnum.Member(ShortEnum.FirstShort); - shortEnum.Member(ShortEnum.SecondShort); - shortEnum.Member(ShortEnum.ThirdShort); - return builder; - } - - public static ODataModelBuilder Add_Address_ComplexType(this ODataModelBuilder builder) - { - var address = builder.ComplexType
(); - address.Property(a => a.HouseNumber); - address.Property(a => a.Street); - address.Property(a => a.City); - address.Property(a => a.State); - return builder; - } - - public static ODataModelBuilder Add_ZipCode_ComplexType(this ODataModelBuilder builder) - { - var zipCode = builder.ComplexType(); - zipCode.Property(z => z.Part1); - zipCode.Property(z => z.Part2).IsNullable(); - return builder; - } - - public static ODataModelBuilder Add_RecursiveZipCode_ComplexType(this ODataModelBuilder builder) - { - var zipCode = builder.ComplexType(); - zipCode.Property(z => z.Part1); - zipCode.Property(z => z.Part2).IsNullable(); - return builder; - } - - public static ODataModelBuilder Add_Customer_EntityType(this ODataModelBuilder builder) - { - var customer = builder.EntityType(); - customer.HasKey(c => c.Id); - customer.Property(c => c.Id); - customer.Property(c => c.Name).IsConcurrencyToken(); - customer.Property(c => c.Website); - customer.Property(c => c.SharePrice); - customer.Property(c => c.ShareSymbol); - customer.Property(c => c.StartDate); - return builder; - } - - public static ODataModelBuilder Add_Customer_EntityType_With_DynamicProperties(this ODataModelBuilder builder) - { - builder.Add_Customer_EntityType(); - var customer = builder.EntityType(); - customer.HasDynamicProperties(c => c.DynamicProperties); - return builder; - } - - public static ODataModelBuilder Add_Customer_EntityType_With_Address(this ODataModelBuilder builder) - { - builder.Add_Customer_EntityType_With_DynamicProperties(); - builder.Add_Address_ComplexType(); - var customer = builder.EntityType(); - customer.ComplexProperty(c => c.Address); - customer.Property(c => c.City); - return builder; - } - - public static ODataModelBuilder Add_Customer_EntityType_With_DuplicatedAddress(this ODataModelBuilder builder) - { - builder.Add_Customer_EntityType(); - builder.Add_Address_ComplexType(); - var customer = builder.EntityType(); - customer.ComplexProperty(c => c.Address); - customer.ComplexProperty(c => c.WorkAddress); - customer.Property(c => c.City); - return builder; - } - - public static ODataModelBuilder Add_Customer_EntityType_With_CollectionProperties(this ODataModelBuilder builder) - { - builder.Add_Customer_EntityType(); - builder.EntityType().CollectionProperty(c => c.Aliases); - builder.EntityType().CollectionProperty(c => c.Addresses); - return builder; - } - - // Adds a Customer EntityType but allows caller to configure Keys() - public static ODataModelBuilder Add_Customer_With_Keys_EntityType(this ODataModelBuilder builder, Expression> keyDefinitionExpression) - { - var customer = builder.EntityType(); - customer.HasKey(keyDefinitionExpression); - customer.Property(c => c.Id); - customer.Property(c => c.Name); - customer.Property(c => c.Website); - customer.Property(c => c.SharePrice); - customer.Property(c => c.ShareSymbol); - return builder; - } - - // Adds a Customer EntityType that has no key properties - public static ODataModelBuilder Add_Customer_No_Keys_EntityType(this ODataModelBuilder builder) - { - var customer = builder.EntityType(); - customer.Property(c => c.Id); - customer.Property(c => c.Name); - customer.Property(c => c.Website); - customer.Property(c => c.SharePrice); - customer.Property(c => c.ShareSymbol); - return builder; - } - - public static ODataModelBuilder Add_Order_EntityType(this ODataModelBuilder builder) - { - var order = builder.EntityType(); - order.HasKey(o => o.OrderId); - order.Property(o => o.OrderDate); - order.Property(o => o.Price); - order.Property(o => o.OrderDate); - order.Property(o => o.DeliveryDate); - order.Ignore(o => o.Cost); - return builder; - } - - public static ODataModelBuilder Add_Company_EntityType(this ODataModelBuilder builder) - { - var company = builder.EntityType(); - company.HasKey(c => c.CompanyId); - company.Property(c => c.CompanyName).IsConcurrencyToken(); - company.Property(c => c.Website); - return builder; - } - - public static ODataModelBuilder Add_Company_Singleton(this ODataModelBuilder builder) - { - builder.Add_Company_EntityType().Singleton("OsCorp"); - return builder; - } - - public static ODataModelBuilder Add_Employee_EntityType(this ODataModelBuilder builder) - { - var employee = builder.EntityType(); - employee.HasKey(c => c.EmployeeID); - employee.Property(c => c.EmployeeName).IsConcurrencyToken(); - employee.Property(c => c.BaseSalary); - return builder; - } - - public static ODataModelBuilder Add_Employee_EntityType_With_HomeAddress(this ODataModelBuilder builder) - { - builder.Add_Employee_EntityType(); - builder.Add_Address_ComplexType(); - var customer = builder.EntityType(); - customer.ComplexProperty(c => c.HomeAddress); - return builder; - } - - public static ODataModelBuilder Add_CompanyEmployees_Relationship(this ODataModelBuilder builder) - { - builder.EntityType().HasMany(c => c.ComplanyEmployees); - builder.EntityType().HasRequired(c => c.CEO, (company, employee) => company.CEOID == employee.EmployeeID, employee => employee.IsCeoOf); - return builder; - } - - public static ODataModelBuilder Add_EmployeeComplany_Relationship(this ODataModelBuilder builder) - { - builder.EntityType().HasRequired(o => o.WorkCompany); - return builder; - } - - // EntitySet -> EntitySet - public static ODataModelBuilder Add_CompaniesEmployees_Binding(this ODataModelBuilder builder) - { - builder.EntitySet("Companies").HasManyBinding(c => c.ComplanyEmployees, "Employees"); - return builder; - } - - // EntitySet -> Singleton - public static ODataModelBuilder Add_CompaniesCEO_Binding(this ODataModelBuilder builder) - { - builder.EntitySet("Companies").HasSingletonBinding(c => c.CEO, "CEO"); - return builder; - } - - // Singleton -> EntitySet - public static ODataModelBuilder Add_MicrosoftEmployees_Binding(this ODataModelBuilder builder) - { - builder.Singleton("OsCorp").HasManyBinding(c => c.ComplanyEmployees, "Employees"); - return builder; - } - - // Singleton -> Singleton - public static ODataModelBuilder Add_MicrosoftCEO_Binding(this ODataModelBuilder builder) - { - builder.Singleton("OsCorp").HasSingletonBinding(c => c.CEO, "CEO"); - return builder; - } - - public static ODataModelBuilder Add_CustomerOrders_Relationship(this ODataModelBuilder builder) - { - builder.EntityType().HasMany(c => c.Orders); - return builder; - } - - public static ODataModelBuilder Add_CustomerCompany_Relationship(this ODataModelBuilder builder) - { - builder.EntityType().HasOptional(c => c.Company); - return builder; - } - - public static ODataModelBuilder Add_OrderCustomer_Relationship(this ODataModelBuilder builder) - { - builder.EntityType().HasRequired(o => o.Customer); - return builder; - } - - public static ODataModelBuilder Add_Customers_EntitySet(this ODataModelBuilder builder) - { - builder.Add_Customer_EntityType().EntitySet("Customers"); - return builder; - } - - public static ODataModelBuilder Add_Customers_Singleton(this ODataModelBuilder builder) - { - builder.Add_Customer_EntityType().Singleton("VipCustomer"); - return builder; - } - - // Adds a Customer EntitySet but allows caller to configure keys - public static ODataModelBuilder Add_Customers_With_Keys_EntitySet(this ODataModelBuilder builder, Expression> keyDefinitionExpression) - { - builder.Add_Customer_With_Keys_EntityType(keyDefinitionExpression).EntitySet("Customers"); - return builder; - } - - // Adds a Customer EntitySet with no key properties - public static ODataModelBuilder Add_Customers_No_Keys_EntitySet(this ODataModelBuilder builder) - { - builder.Add_Customer_No_Keys_EntityType().EntitySet("Customers"); - return builder; - } - - public static ODataModelBuilder Add_Orders_EntitySet(this ODataModelBuilder builder) - { - builder.EntitySet("Orders"); - return builder; - } - - public static ODataModelBuilder Add_CustomerOrders_Binding(this ODataModelBuilder builder) - { - builder.EntitySet("Customers").HasManyBinding(c => c.Orders, "Orders"); - return builder; - } - - public static ODataModelBuilder Add_OrderCustomer_Binding(this ODataModelBuilder builder) - { - builder.EntitySet("Orders").HasRequiredBinding(o => o.Customer, "Customer"); - return builder; - } + public static ODataModelBuilder Add_Color_EnumType(this ODataModelBuilder builder) + { + var color = builder.EnumType(); + color.Member(Color.Red); + color.Member(Color.Green); + color.Member(Color.Blue); + return builder; + } + + public static ODataModelBuilder Add_SimpleEnum_EnumType(this ODataModelBuilder builder) + { + var simpleEnum = builder.EnumType(); + simpleEnum.Member(SimpleEnum.First); + simpleEnum.Member(SimpleEnum.Second); + simpleEnum.Member(SimpleEnum.Third); + return builder; + } + + public static ODataModelBuilder Add_FlagsEnum_EnumType(this ODataModelBuilder builder) + { + var flagsEnum = builder.EnumType(); + flagsEnum.Member(FlagsEnum.One); + flagsEnum.Member(FlagsEnum.Two); + flagsEnum.Member(FlagsEnum.Four); + return builder; + } + + public static ODataModelBuilder Add_LongEnum_EnumType(this ODataModelBuilder builder) + { + var longEnum = builder.EnumType(); + longEnum.Member(LongEnum.FirstLong); + longEnum.Member(LongEnum.SecondLong); + longEnum.Member(LongEnum.ThirdLong); + return builder; + } + + public static ODataModelBuilder Add_ByteEnum_EnumType(this ODataModelBuilder builder) + { + EnumTypeConfiguration byteEnum = builder.EnumType(); + byteEnum.Member(ByteEnum.FirstByte); + byteEnum.Member(ByteEnum.SecondByte); + byteEnum.Member(ByteEnum.ThirdByte); + return builder; + } + + public static ODataModelBuilder Add_SByteEnum_EnumType(this ODataModelBuilder builder) + { + EnumTypeConfiguration sByteEnum = builder.EnumType(); + sByteEnum.Member(SByteEnum.FirstSByte); + sByteEnum.Member(SByteEnum.SecondSByte); + sByteEnum.Member(SByteEnum.ThirdSByte); + return builder; + } + + public static ODataModelBuilder Add_ShortEnum_EnumType(this ODataModelBuilder builder) + { + EnumTypeConfiguration shortEnum = builder.EnumType(); + shortEnum.Member(ShortEnum.FirstShort); + shortEnum.Member(ShortEnum.SecondShort); + shortEnum.Member(ShortEnum.ThirdShort); + return builder; + } + + public static ODataModelBuilder Add_Address_ComplexType(this ODataModelBuilder builder) + { + var address = builder.ComplexType
(); + address.Property(a => a.HouseNumber); + address.Property(a => a.Street); + address.Property(a => a.City); + address.Property(a => a.State); + return builder; + } + + public static ODataModelBuilder Add_ZipCode_ComplexType(this ODataModelBuilder builder) + { + var zipCode = builder.ComplexType(); + zipCode.Property(z => z.Part1); + zipCode.Property(z => z.Part2).IsNullable(); + return builder; + } + + public static ODataModelBuilder Add_RecursiveZipCode_ComplexType(this ODataModelBuilder builder) + { + var zipCode = builder.ComplexType(); + zipCode.Property(z => z.Part1); + zipCode.Property(z => z.Part2).IsNullable(); + return builder; + } + + public static ODataModelBuilder Add_Customer_EntityType(this ODataModelBuilder builder) + { + var customer = builder.EntityType(); + customer.HasKey(c => c.Id); + customer.Property(c => c.Id); + customer.Property(c => c.Name).IsConcurrencyToken(); + customer.Property(c => c.Website); + customer.Property(c => c.SharePrice); + customer.Property(c => c.ShareSymbol); + customer.Property(c => c.StartDate); + return builder; + } + + public static ODataModelBuilder Add_Customer_EntityType_With_DynamicProperties(this ODataModelBuilder builder) + { + builder.Add_Customer_EntityType(); + var customer = builder.EntityType(); + customer.HasDynamicProperties(c => c.DynamicProperties); + return builder; + } + + public static ODataModelBuilder Add_Customer_EntityType_With_Address(this ODataModelBuilder builder) + { + builder.Add_Customer_EntityType_With_DynamicProperties(); + builder.Add_Address_ComplexType(); + var customer = builder.EntityType(); + customer.ComplexProperty(c => c.Address); + customer.Property(c => c.City); + return builder; + } + + public static ODataModelBuilder Add_Customer_EntityType_With_DuplicatedAddress(this ODataModelBuilder builder) + { + builder.Add_Customer_EntityType(); + builder.Add_Address_ComplexType(); + var customer = builder.EntityType(); + customer.ComplexProperty(c => c.Address); + customer.ComplexProperty(c => c.WorkAddress); + customer.Property(c => c.City); + return builder; + } + + public static ODataModelBuilder Add_Customer_EntityType_With_CollectionProperties(this ODataModelBuilder builder) + { + builder.Add_Customer_EntityType(); + builder.EntityType().CollectionProperty(c => c.Aliases); + builder.EntityType().CollectionProperty(c => c.Addresses); + return builder; + } + + // Adds a Customer EntityType but allows caller to configure Keys() + public static ODataModelBuilder Add_Customer_With_Keys_EntityType(this ODataModelBuilder builder, Expression> keyDefinitionExpression) + { + var customer = builder.EntityType(); + customer.HasKey(keyDefinitionExpression); + customer.Property(c => c.Id); + customer.Property(c => c.Name); + customer.Property(c => c.Website); + customer.Property(c => c.SharePrice); + customer.Property(c => c.ShareSymbol); + return builder; + } + + // Adds a Customer EntityType that has no key properties + public static ODataModelBuilder Add_Customer_No_Keys_EntityType(this ODataModelBuilder builder) + { + var customer = builder.EntityType(); + customer.Property(c => c.Id); + customer.Property(c => c.Name); + customer.Property(c => c.Website); + customer.Property(c => c.SharePrice); + customer.Property(c => c.ShareSymbol); + return builder; + } + + public static ODataModelBuilder Add_Order_EntityType(this ODataModelBuilder builder) + { + var order = builder.EntityType(); + order.HasKey(o => o.OrderId); + order.Property(o => o.OrderDate); + order.Property(o => o.Price); + order.Property(o => o.OrderDate); + order.Property(o => o.DeliveryDate); + order.Ignore(o => o.Cost); + return builder; + } + + public static ODataModelBuilder Add_Company_EntityType(this ODataModelBuilder builder) + { + var company = builder.EntityType(); + company.HasKey(c => c.CompanyId); + company.Property(c => c.CompanyName).IsConcurrencyToken(); + company.Property(c => c.Website); + return builder; + } + + public static ODataModelBuilder Add_Company_Singleton(this ODataModelBuilder builder) + { + builder.Add_Company_EntityType().Singleton("OsCorp"); + return builder; + } + + public static ODataModelBuilder Add_Employee_EntityType(this ODataModelBuilder builder) + { + var employee = builder.EntityType(); + employee.HasKey(c => c.EmployeeID); + employee.Property(c => c.EmployeeName).IsConcurrencyToken(); + employee.Property(c => c.BaseSalary); + return builder; + } + + public static ODataModelBuilder Add_Employee_EntityType_With_HomeAddress(this ODataModelBuilder builder) + { + builder.Add_Employee_EntityType(); + builder.Add_Address_ComplexType(); + var customer = builder.EntityType(); + customer.ComplexProperty(c => c.HomeAddress); + return builder; + } + + public static ODataModelBuilder Add_CompanyEmployees_Relationship(this ODataModelBuilder builder) + { + builder.EntityType().HasMany(c => c.ComplanyEmployees); + builder.EntityType().HasRequired(c => c.CEO, (company, employee) => company.CEOID == employee.EmployeeID, employee => employee.IsCeoOf); + return builder; + } + + public static ODataModelBuilder Add_EmployeeComplany_Relationship(this ODataModelBuilder builder) + { + builder.EntityType().HasRequired(o => o.WorkCompany); + return builder; + } + + // EntitySet -> EntitySet + public static ODataModelBuilder Add_CompaniesEmployees_Binding(this ODataModelBuilder builder) + { + builder.EntitySet("Companies").HasManyBinding(c => c.ComplanyEmployees, "Employees"); + return builder; + } + + // EntitySet -> Singleton + public static ODataModelBuilder Add_CompaniesCEO_Binding(this ODataModelBuilder builder) + { + builder.EntitySet("Companies").HasSingletonBinding(c => c.CEO, "CEO"); + return builder; + } + + // Singleton -> EntitySet + public static ODataModelBuilder Add_MicrosoftEmployees_Binding(this ODataModelBuilder builder) + { + builder.Singleton("OsCorp").HasManyBinding(c => c.ComplanyEmployees, "Employees"); + return builder; + } + + // Singleton -> Singleton + public static ODataModelBuilder Add_MicrosoftCEO_Binding(this ODataModelBuilder builder) + { + builder.Singleton("OsCorp").HasSingletonBinding(c => c.CEO, "CEO"); + return builder; + } + + public static ODataModelBuilder Add_CustomerOrders_Relationship(this ODataModelBuilder builder) + { + builder.EntityType().HasMany(c => c.Orders); + return builder; + } + + public static ODataModelBuilder Add_CustomerCompany_Relationship(this ODataModelBuilder builder) + { + builder.EntityType().HasOptional(c => c.Company); + return builder; + } + + public static ODataModelBuilder Add_OrderCustomer_Relationship(this ODataModelBuilder builder) + { + builder.EntityType().HasRequired(o => o.Customer); + return builder; + } + + public static ODataModelBuilder Add_Customers_EntitySet(this ODataModelBuilder builder) + { + builder.Add_Customer_EntityType().EntitySet("Customers"); + return builder; + } + + public static ODataModelBuilder Add_Customers_Singleton(this ODataModelBuilder builder) + { + builder.Add_Customer_EntityType().Singleton("VipCustomer"); + return builder; + } + + // Adds a Customer EntitySet but allows caller to configure keys + public static ODataModelBuilder Add_Customers_With_Keys_EntitySet(this ODataModelBuilder builder, Expression> keyDefinitionExpression) + { + builder.Add_Customer_With_Keys_EntityType(keyDefinitionExpression).EntitySet("Customers"); + return builder; + } + + // Adds a Customer EntitySet with no key properties + public static ODataModelBuilder Add_Customers_No_Keys_EntitySet(this ODataModelBuilder builder) + { + builder.Add_Customer_No_Keys_EntityType().EntitySet("Customers"); + return builder; + } + + public static ODataModelBuilder Add_Orders_EntitySet(this ODataModelBuilder builder) + { + builder.EntitySet("Orders"); + return builder; + } + + public static ODataModelBuilder Add_CustomerOrders_Binding(this ODataModelBuilder builder) + { + builder.EntitySet("Customers").HasManyBinding(c => c.Orders, "Orders"); + return builder; + } + + public static ODataModelBuilder Add_OrderCustomer_Binding(this ODataModelBuilder builder) + { + builder.EntitySet("Orders").HasRequiredBinding(o => o.Customer, "Customer"); + return builder; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/NewCustomerDataContract.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/NewCustomerDataContract.cs index 91f2d208c..7f26936fe 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/NewCustomerDataContract.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/NewCustomerDataContract.cs @@ -1,22 +1,21 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +[DataContract] +public class NewCustomerDataContract { - [DataContract] - public class NewCustomerDataContract - { - [Key] - public int Id { get; set; } - [IgnoreDataMember] - [DataMember] - public string Name { get; set; } - [IgnoreDataMember] - public int Age { get; set; } + [Key] + public int Id { get; set; } + [IgnoreDataMember] + [DataMember] + public string Name { get; set; } + [IgnoreDataMember] + public int Age { get; set; } - [DataMember] - public string Street { get; set; } + [DataMember] + public string Street { get; set; } - public string City { get; set; } - } + public string City { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/NewCustomerUnmapped.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/NewCustomerUnmapped.cs index 126ecdf80..2f9fcbd43 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/NewCustomerUnmapped.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/NewCustomerUnmapped.cs @@ -1,25 +1,24 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.Serialization; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class NewCustomerUnmapped { - public class NewCustomerUnmapped - { - [Key] - public int Id { get; set; } - [IgnoreDataMember] - public string Name { get; set; } - [NotMapped] - public int Age { get; set; } + [Key] + public int Id { get; set; } + [IgnoreDataMember] + public string Name { get; set; } + [NotMapped] + public int Age { get; set; } - [DataMember] - [IgnoreDataMember] - public string Street { get; set; } + [DataMember] + [IgnoreDataMember] + public string Street { get; set; } - [DataMember] - public string City { get; set; } + [DataMember] + public string City { get; set; } - public string State { get; set; } - } + public string State { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/Order.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/Order.cs index c088583cd..73fb1976d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/Order.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/Order.cs @@ -7,20 +7,19 @@ using System; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class Order { - public class Order - { - public int OrderId { get; set; } + public int OrderId { get; set; } - public Customer Customer { get; set; } + public Customer Customer { get; set; } - public decimal Cost { get; set; } + public decimal Cost { get; set; } - public decimal Price { get; set; } + public decimal Price { get; set; } - public DateTimeOffset OrderDate { get; set; } + public DateTimeOffset OrderDate { get; set; } - public DateTimeOffset? DeliveryDate { get; set; } - } + public DateTimeOffset? DeliveryDate { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/OrderLine.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/OrderLine.cs index b28a590a1..041d47192 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/OrderLine.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/OrderLine.cs @@ -5,14 +5,13 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class OrderLine { - public class OrderLine - { - public int ID { get; set; } + public int ID { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public int OrderId { get; set; } - } + public int OrderId { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/Person.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/Person.cs index 32185b2f3..527fd9971 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/Person.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/Person.cs @@ -7,60 +7,59 @@ using System.Runtime.Serialization; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +[DataContract] +public class Person { - [DataContract] - public class Person - { - [DataMember] - public int Age { get; set; } + [DataMember] + public int Age { get; set; } - [DataMember] - public Gender Gender { get; set; } + [DataMember] + public Gender Gender { get; set; } - [DataMember(IsRequired = true)] - public string FirstName { get; set; } + [DataMember(IsRequired = true)] + public string FirstName { get; set; } - [DataMember] - public string[] Alias { get; set; } + [DataMember] + public string[] Alias { get; set; } - [DataMember] - public Address Address { get; set; } + [DataMember] + public Address Address { get; set; } - [DataMember] - public PhoneNumber HomeNumber { get; set; } + [DataMember] + public PhoneNumber HomeNumber { get; set; } - public string UnserializableSSN { get; set; } + public string UnserializableSSN { get; set; } - [DataMember] - public HobbyActivity FavoriteHobby { get; set; } + [DataMember] + public HobbyActivity FavoriteHobby { get; set; } - [DataContract] - public class HobbyActivity : IActivity + [DataContract] + public class HobbyActivity : IActivity + { + public HobbyActivity(string hobbyName) { - public HobbyActivity(string hobbyName) - { - this.ActivityName = hobbyName; - } + this.ActivityName = hobbyName; + } - [DataMember] - public string ActivityName - { - get; - set; - } + [DataMember] + public string ActivityName + { + get; + set; + } - public void DoActivity() - { - // Some Action - } + public void DoActivity() + { + // Some Action } } +} - public interface IActivity - { - string ActivityName { get; set; } +public interface IActivity +{ + string ActivityName { get; set; } - void DoActivity(); - } + void DoActivity(); } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/PhoneNumber.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/PhoneNumber.cs index a0603b999..1aaeab067 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/PhoneNumber.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/PhoneNumber.cs @@ -5,24 +5,23 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public struct PhoneNumber { - public struct PhoneNumber - { - public int CountryCode { get; set; } + public int CountryCode { get; set; } - public int AreaCode { get; set; } + public int AreaCode { get; set; } - public int Number { get; set; } + public int Number { get; set; } - public PhoneType PhoneType { get; set; } - } + public PhoneType PhoneType { get; set; } +} - public enum PhoneType - { - HomePhone, - CellPhone, - WorkPhone, - Fax - } +public enum PhoneType +{ + HomePhone, + CellPhone, + WorkPhone, + Fax } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/Product.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/Product.cs index dd62f56ac..9d97b296d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/Product.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/Product.cs @@ -10,64 +10,63 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class Product { - public class Product - { - public int ProductID { get; set; } + public int ProductID { get; set; } - public string ProductName { get; set; } - public int SupplierID { get; set; } - public int CategoryID { get; set; } - public string QuantityPerUnit { get; set; } - public decimal? UnitPrice { get; set; } - public double? Weight { get; set; } - public float? Width { get; set; } - public short? UnitsInStock { get; set; } - public short? UnitsOnOrder { get; set; } + public string ProductName { get; set; } + public int SupplierID { get; set; } + public int CategoryID { get; set; } + public string QuantityPerUnit { get; set; } + public decimal? UnitPrice { get; set; } + public double? Weight { get; set; } + public float? Width { get; set; } + public short? UnitsInStock { get; set; } + public short? UnitsOnOrder { get; set; } - public short? ReorderLevel { get; set; } - public bool? Discontinued { get; set; } - public DateTimeOffset? DiscontinuedDate { get; set; } - public DateTime Birthday { get; set; } + public short? ReorderLevel { get; set; } + public bool? Discontinued { get; set; } + public DateTimeOffset? DiscontinuedDate { get; set; } + public DateTime Birthday { get; set; } - public DateTimeOffset NonNullableDiscontinuedDate { get; set; } - [NotFilterable] - public DateTimeOffset NotFilterableDiscontinuedDate { get; set; } + public DateTimeOffset NonNullableDiscontinuedDate { get; set; } + [NotFilterable] + public DateTimeOffset NotFilterableDiscontinuedDate { get; set; } - public DateTimeOffset DiscontinuedOffset { get; set; } - public TimeSpan DiscontinuedSince { get; set; } + public DateTimeOffset DiscontinuedOffset { get; set; } + public TimeSpan DiscontinuedSince { get; set; } - public Date DateProperty { get; set; } - public Date? NullableDateProperty { get; set; } + public Date DateProperty { get; set; } + public Date? NullableDateProperty { get; set; } - public Guid GuidProperty { get; set; } - public Guid? NullableGuidProperty { get; set; } + public Guid GuidProperty { get; set; } + public Guid? NullableGuidProperty { get; set; } - public TimeOfDay TimeOfDayProperty { get; set; } - public TimeOfDay? NullableTimeOfDayProperty { get; set; } + public TimeOfDay TimeOfDayProperty { get; set; } + public TimeOfDay? NullableTimeOfDayProperty { get; set; } - public ushort? UnsignedReorderLevel { get; set; } + public ushort? UnsignedReorderLevel { get; set; } - public SimpleEnum Ranking { get; set; } + public SimpleEnum Ranking { get; set; } - public Category Category { get; set; } + public Category Category { get; set; } - public Address SupplierAddress { get; set; } + public Address SupplierAddress { get; set; } - public int[] AlternateIDs { get; set; } - public Address[] AlternateAddresses { get; set; } - [NotFilterable] - public Address[] NotFilterableAlternateAddresses { get; set; } - } + public int[] AlternateIDs { get; set; } + public Address[] AlternateAddresses { get; set; } + [NotFilterable] + public Address[] NotFilterableAlternateAddresses { get; set; } +} - public class DerivedProduct : Product - { - public string DerivedProductName { get; set; } - } +public class DerivedProduct : Product +{ + public string DerivedProductName { get; set; } +} - public class DynamicProduct : Product - { - public Dictionary ProductProperties { get; set; } - } +public class DynamicProduct : Product +{ + public Dictionary ProductProperties { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/PropertyAlias.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/PropertyAlias.cs index 08a75dba1..d5eca06c9 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/PropertyAlias.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/PropertyAlias.cs @@ -7,26 +7,25 @@ using System.Runtime.Serialization; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +[DataContract(Namespace = "com.contoso", Name = "PropertyAlias2")] +public class PropertyAlias { - [DataContract(Namespace = "com.contoso", Name = "PropertyAlias2")] - public class PropertyAlias - { - [DataMember] - public int Id { get; set; } + [DataMember] + public int Id { get; set; } - [DataMember(Name = "FirstNameAlias")] - public string FirstName { get; set; } + [DataMember(Name = "FirstNameAlias")] + public string FirstName { get; set; } - public int Points { get; set; } - } + public int Points { get; set; } +} - [DataContract(Namespace = "com.contoso", Name = "PropertyAliasDerived2")] - public class PropertyAliasDerived : PropertyAlias - { - [DataMember(Name = "LastNameAlias")] - public string LastName { get; set; } +[DataContract(Namespace = "com.contoso", Name = "PropertyAliasDerived2")] +public class PropertyAliasDerived : PropertyAlias +{ + [DataMember(Name = "LastNameAlias")] + public string LastName { get; set; } - public int Age { get; set; } - } + public int Age { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleEnum.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleEnum.cs index 6c155c531..ce7424dfe 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleEnum.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleEnum.cs @@ -5,16 +5,15 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public enum SimpleEnum { - public enum SimpleEnum - { - First, + First, - Second, + Second, - Third, + Third, - Fourth - } + Fourth } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleOpenAddress.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleOpenAddress.cs index 5336cefce..13183d12f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleOpenAddress.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleOpenAddress.cs @@ -7,12 +7,11 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class SimpleOpenAddress { - public class SimpleOpenAddress - { - public string Street { get; set; } - public string City { get; set; } - public IDictionary Properties { get; set; } - } + public string Street { get; set; } + public string City { get; set; } + public IDictionary Properties { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleOpenCustomer.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleOpenCustomer.cs index 1a53334f6..2932d5b9b 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleOpenCustomer.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleOpenCustomer.cs @@ -9,23 +9,22 @@ using System.ComponentModel.DataAnnotations; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class SimpleOpenCustomer { - public class SimpleOpenCustomer - { - [Key] - public int CustomerId { get; set; } + [Key] + public int CustomerId { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public SimpleOpenAddress Address { get; set; } + public SimpleOpenAddress Address { get; set; } - public string Website { get; set; } + public string Website { get; set; } - public List Orders { get; set; } + public List Orders { get; set; } - public IDictionary CustomerProperties { get; set; } + public IDictionary CustomerProperties { get; set; } - public ODataInstanceAnnotationContainer InstanceAnnotations { get; set; } - } + public ODataInstanceAnnotationContainer InstanceAnnotations { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleOpenOrder.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleOpenOrder.cs index e7d87e057..d7a1946b5 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleOpenOrder.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleOpenOrder.cs @@ -9,23 +9,22 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class SimpleOpenOrder { - public class SimpleOpenOrder - { - [Key] - public int OrderId { get; set; } + [Key] + public int OrderId { get; set; } - public decimal Cost { get; set; } + public decimal Cost { get; set; } - public decimal Price { get; set; } + public decimal Price { get; set; } - public SimpleOpenCustomer Customer { get; set; } + public SimpleOpenCustomer Customer { get; set; } - public DateTimeOffset OrderDate { get; set; } + public DateTimeOffset OrderDate { get; set; } - public SimpleOpenAddress Address { get; set; } + public SimpleOpenAddress Address { get; set; } - public IDictionary OrderProperties { get; set; } - } + public IDictionary OrderProperties { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleVipCustomer.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleVipCustomer.cs index 9887dfd57..c7cc3bc9c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleVipCustomer.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/SimpleVipCustomer.cs @@ -5,10 +5,9 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class SimpleVipCustomer : SimpleOpenCustomer { - public class SimpleVipCustomer : SimpleOpenCustomer - { - public string VipNum { get; set; } - } + public string VipNum { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/WorkItem.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/WorkItem.cs index 12924ac35..5251346fe 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/WorkItem.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/WorkItem.cs @@ -5,24 +5,23 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class WorkItem { - public class WorkItem - { - //Automatically is made Key - public int ID { get; set; } + //Automatically is made Key + public int ID { get; set; } - public int EmployeeID { get; set; } + public int EmployeeID { get; set; } - public bool IsCompleted { get; set; } + public bool IsCompleted { get; set; } - public float NumberOfHours { get; set; } + public float NumberOfHours { get; set; } - public int Field; - } + public int Field; +} - // Used as a type on which keys can be explicitly set - public class DerivedWorkItem : WorkItem - { - } +// Used as a type on which keys can be explicitly set +public class DerivedWorkItem : WorkItem +{ } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Models/ZipCode.cs b/test/Microsoft.AspNetCore.OData.Tests/Models/ZipCode.cs index 8bda9d9e0..bb99ad1c0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Models/ZipCode.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Models/ZipCode.cs @@ -5,21 +5,20 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Tests.Models +namespace Microsoft.AspNetCore.OData.Tests.Models; + +public class ZipCode { - public class ZipCode - { - public string Part1 { get; set; } + public string Part1 { get; set; } - public string Part2 { get; set; } - } + public string Part2 { get; set; } +} - public class RecursiveZipCode - { - public string Part1 { get; set; } +public class RecursiveZipCode +{ + public string Part1 { get; set; } - public string Part2 { get; set; } + public string Part2 { get; set; } - public RecursiveZipCode Recursive { get; set; } - } + public RecursiveZipCode Recursive { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/ODataApplicationBuilderExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/ODataApplicationBuilderExtensionsTests.cs index bd90dcbbe..34f38e1ca 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/ODataApplicationBuilderExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/ODataApplicationBuilderExtensionsTests.cs @@ -17,136 +17,135 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests +namespace Microsoft.AspNetCore.OData.Tests; + +public class ODataApplicationBuilderExtensionsTests { - public class ODataApplicationBuilderExtensionsTests + [Fact] + public void UseODataBatching_ThrowsArgumentNull_AppBuilder() { - [Fact] - public void UseODataBatching_ThrowsArgumentNull_AppBuilder() - { - // Arrange & Act & Assert - IApplicationBuilder builder = null; - ExceptionAssert.ThrowsArgumentNull(() => builder.UseODataBatching(), "app"); - } + // Arrange & Act & Assert + IApplicationBuilder builder = null; + ExceptionAssert.ThrowsArgumentNull(() => builder.UseODataBatching(), "app"); + } - [Fact] - public void UseODataQueryRequest_ThrowsArgumentNull_AppBuilder() - { - // Arrange & Act & Assert - IApplicationBuilder builder = null; - ExceptionAssert.ThrowsArgumentNull(() => builder.UseODataQueryRequest(), "app"); - } - - [Theory] - [InlineData("/$query", true)] - [InlineData("/$anyother", false)] - public async Task UseODataQueryRequest_Calls_ODataQueryMiddleware(string requestPath, bool parserCalledExpected) - { - // Arrange - bool parserCalled = false; - Mock parser = new Mock(); - parser.Setup(p => p.CanParse(It.IsAny())).Returns(() => - { - parserCalled = true; - return false; // return false to skip the transform - }); - - IServiceCollection services = new ServiceCollection(); - services.TryAddEnumerable(ServiceDescriptor.Singleton(parser.Object)); - - IServiceProvider sp = services.BuildServiceProvider(); - ApplicationBuilder builder = new ApplicationBuilder(serviceProvider: sp); - - DefaultHttpContext context = new DefaultHttpContext(); - context.RequestServices = sp; - context.Request.Path = new PathString(requestPath); - context.Request.Method = "Post"; - - bool lastCalled = false; - - // Act - builder.UseODataQueryRequest(); // first - middleware - builder.Run(context => // second - middleware - { - lastCalled = true; - return Task.CompletedTask; - }); - - await builder.Build().Invoke(context); - - // Assert - Assert.Equal(parserCalledExpected, parserCalled); - Assert.True(lastCalled); - } - - [Fact] - public void UseODataRouteDebug_ThrowsArgumentNull_AppBuilder() - { - // Arrange & Act & Assert - IApplicationBuilder builder = null; - ExceptionAssert.ThrowsArgumentNull(() => builder.UseODataRouteDebug(), "app"); - } + [Fact] + public void UseODataQueryRequest_ThrowsArgumentNull_AppBuilder() + { + // Arrange & Act & Assert + IApplicationBuilder builder = null; + ExceptionAssert.ThrowsArgumentNull(() => builder.UseODataQueryRequest(), "app"); + } - [Fact] - public void UseODataRouteDebug_UsingPattern_ThrowsArgumentNull_AppBuilder() + [Theory] + [InlineData("/$query", true)] + [InlineData("/$anyother", false)] + public async Task UseODataQueryRequest_Calls_ODataQueryMiddleware(string requestPath, bool parserCalledExpected) + { + // Arrange + bool parserCalled = false; + Mock parser = new Mock(); + parser.Setup(p => p.CanParse(It.IsAny())).Returns(() => { - // Arrange & Act & Assert - IApplicationBuilder builder = null; - ExceptionAssert.ThrowsArgumentNull(() => builder.UseODataRouteDebug("$odata"), "app"); - } + parserCalled = true; + return false; // return false to skip the transform + }); - [Fact] - public void UseODataRouteDebug_UsingPattern_ThrowsArgumentNull_RoutePattern() - { - // Arrange & Act & Assert - IApplicationBuilder builder = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => builder.UseODataRouteDebug(null), "routePattern"); - } - - [Theory] - [InlineData("/$odata2", true)] - [InlineData("/$odata", false)] - public async Task UseODataRouteDebug_Calls_ODataRouteDebugMiddleware(string requestPath, bool lastCalledExpected) - { - await VerifyRouteDebug(requestPath, lastCalledExpected, app => app.UseODataRouteDebug()); - } + IServiceCollection services = new ServiceCollection(); + services.TryAddEnumerable(ServiceDescriptor.Singleton(parser.Object)); - [Theory] - [InlineData("/$odata2", false)] - [InlineData("/$odata", true)] - public async Task UseODataRouteDebug_UsingPattern_Calls_ODataRouteDebugMiddleware(string requestPath, bool lastCalledExpected) - { - await VerifyRouteDebug(requestPath, lastCalledExpected, app => app.UseODataRouteDebug("$odata2")); - } + IServiceProvider sp = services.BuildServiceProvider(); + ApplicationBuilder builder = new ApplicationBuilder(serviceProvider: sp); + + DefaultHttpContext context = new DefaultHttpContext(); + context.RequestServices = sp; + context.Request.Path = new PathString(requestPath); + context.Request.Method = "Post"; + + bool lastCalled = false; - private static async Task VerifyRouteDebug(string requestPath, bool lastCalledExpected, Action config) + // Act + builder.UseODataQueryRequest(); // first - middleware + builder.Run(context => // second - middleware { - // Arrange - IServiceCollection services = new ServiceCollection(); - services.TryAddEnumerable( - ServiceDescriptor.Singleton(new DefaultEndpointDataSource(Array.Empty()))); - IServiceProvider sp = services.BuildServiceProvider(); - ApplicationBuilder builder = new ApplicationBuilder(serviceProvider: sp); + lastCalled = true; + return Task.CompletedTask; + }); - DefaultHttpContext context = new DefaultHttpContext(); - context.RequestServices = sp; - context.Request.Path = new PathString(requestPath); + await builder.Build().Invoke(context); - bool lastCalled = false; + // Assert + Assert.Equal(parserCalledExpected, parserCalled); + Assert.True(lastCalled); + } - // Act - config(builder);// first - middleware + [Fact] + public void UseODataRouteDebug_ThrowsArgumentNull_AppBuilder() + { + // Arrange & Act & Assert + IApplicationBuilder builder = null; + ExceptionAssert.ThrowsArgumentNull(() => builder.UseODataRouteDebug(), "app"); + } - builder.Run(context => // second - middleware - { - lastCalled = true; - return Task.CompletedTask; - }); + [Fact] + public void UseODataRouteDebug_UsingPattern_ThrowsArgumentNull_AppBuilder() + { + // Arrange & Act & Assert + IApplicationBuilder builder = null; + ExceptionAssert.ThrowsArgumentNull(() => builder.UseODataRouteDebug("$odata"), "app"); + } + + [Fact] + public void UseODataRouteDebug_UsingPattern_ThrowsArgumentNull_RoutePattern() + { + // Arrange & Act & Assert + IApplicationBuilder builder = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => builder.UseODataRouteDebug(null), "routePattern"); + } + + [Theory] + [InlineData("/$odata2", true)] + [InlineData("/$odata", false)] + public async Task UseODataRouteDebug_Calls_ODataRouteDebugMiddleware(string requestPath, bool lastCalledExpected) + { + await VerifyRouteDebug(requestPath, lastCalledExpected, app => app.UseODataRouteDebug()); + } + + [Theory] + [InlineData("/$odata2", false)] + [InlineData("/$odata", true)] + public async Task UseODataRouteDebug_UsingPattern_Calls_ODataRouteDebugMiddleware(string requestPath, bool lastCalledExpected) + { + await VerifyRouteDebug(requestPath, lastCalledExpected, app => app.UseODataRouteDebug("$odata2")); + } + + private static async Task VerifyRouteDebug(string requestPath, bool lastCalledExpected, Action config) + { + // Arrange + IServiceCollection services = new ServiceCollection(); + services.TryAddEnumerable( + ServiceDescriptor.Singleton(new DefaultEndpointDataSource(Array.Empty()))); + IServiceProvider sp = services.BuildServiceProvider(); + ApplicationBuilder builder = new ApplicationBuilder(serviceProvider: sp); + + DefaultHttpContext context = new DefaultHttpContext(); + context.RequestServices = sp; + context.Request.Path = new PathString(requestPath); + + bool lastCalled = false; + + // Act + config(builder);// first - middleware + + builder.Run(context => // second - middleware + { + lastCalled = true; + return Task.CompletedTask; + }); - await builder.Build().Invoke(context); + await builder.Build().Invoke(context); - // Assert - Assert.Equal(lastCalledExpected, lastCalled); - } + // Assert + Assert.Equal(lastCalledExpected, lastCalled); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/ODataJsonOptionsSetupTests.cs b/test/Microsoft.AspNetCore.OData.Tests/ODataJsonOptionsSetupTests.cs index 227628855..ed0f3865a 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/ODataJsonOptionsSetupTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/ODataJsonOptionsSetupTests.cs @@ -14,60 +14,59 @@ using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests +namespace Microsoft.AspNetCore.OData.Tests; + +public class ODataJsonOptionsSetupTests { - public class ODataJsonOptionsSetupTests + [Fact] + public void ConfigureODataJsonOptionsSetup_ThrowsArgumentNull_Options() { - [Fact] - public void ConfigureODataJsonOptionsSetup_ThrowsArgumentNull_Options() - { - // Arrange - ODataJsonOptionsSetup setup = new ODataJsonOptionsSetup(); + // Arrange + ODataJsonOptionsSetup setup = new ODataJsonOptionsSetup(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => setup.Configure(null), "options"); - } - - [Fact] - public void ODataJsonOptionsSetup_DoesNotSetup_ODataJsonConverters() - { - // Arrange & Act - JsonOptions options = GetJsonOptions(false); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => setup.Configure(null), "options"); + } - // Assert - Assert.Empty(options.JsonSerializerOptions.Converters); - } + [Fact] + public void ODataJsonOptionsSetup_DoesNotSetup_ODataJsonConverters() + { + // Arrange & Act + JsonOptions options = GetJsonOptions(false); - [Fact] - public void ODataJsonOptionsSetup_Setup_ODataJsonConverters() - { - // Arrange & Act - JsonOptions options = GetJsonOptions(true); + // Assert + Assert.Empty(options.JsonSerializerOptions.Converters); + } - // Assert - Assert.Equal(4, options.JsonSerializerOptions.Converters.Count); + [Fact] + public void ODataJsonOptionsSetup_Setup_ODataJsonConverters() + { + // Arrange & Act + JsonOptions options = GetJsonOptions(true); - Assert.Collection(options.JsonSerializerOptions.Converters, - e => Assert.IsType(e), - e => Assert.IsType(e), - e => Assert.IsType(e), - e => Assert.IsType(e)); - } + // Assert + Assert.Equal(4, options.JsonSerializerOptions.Converters.Count); - private static JsonOptions GetJsonOptions(bool withOData) - { - IServiceCollection services = new ServiceCollection(); + Assert.Collection(options.JsonSerializerOptions.Converters, + e => Assert.IsType(e), + e => Assert.IsType(e), + e => Assert.IsType(e), + e => Assert.IsType(e)); + } - services.AddControllers(); + private static JsonOptions GetJsonOptions(bool withOData) + { + IServiceCollection services = new ServiceCollection(); - if (withOData) - { - services.TryAddEnumerable( - ServiceDescriptor.Transient, ODataJsonOptionsSetup>()); - } + services.AddControllers(); - var serviceProvider = services.BuildServiceProvider(); - return serviceProvider.GetRequiredService>().Value; + if (withOData) + { + services.TryAddEnumerable( + ServiceDescriptor.Transient, ODataJsonOptionsSetup>()); } + + var serviceProvider = services.BuildServiceProvider(); + return serviceProvider.GetRequiredService>().Value; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/ODataMvcBuilderExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/ODataMvcBuilderExtensionsTests.cs index 108317920..07efb63a4 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/ODataMvcBuilderExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/ODataMvcBuilderExtensionsTests.cs @@ -17,198 +17,197 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests +namespace Microsoft.AspNetCore.OData.Tests; + +public class ODataMvcBuilderExtensionsTests { - public class ODataMvcBuilderExtensionsTests + [Fact] + public void AddOData_ThrowsArgumentNull_Builder() { - [Fact] - public void AddOData_ThrowsArgumentNull_Builder() - { - // Arrange - IMvcBuilder builder = null; - Action setupAction = null; - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "builder"); - } + // Arrange + IMvcBuilder builder = null; + Action setupAction = null; - [Fact] - public void AddOData_ThrowsArgumentNull_SetupAction() - { - // Arrange - IMvcBuilder builder = new Mock().Object; - Action setupAction = null; + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "builder"); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "setupAction"); - } + [Fact] + public void AddOData_ThrowsArgumentNull_SetupAction() + { + // Arrange + IMvcBuilder builder = new Mock().Object; + Action setupAction = null; - [Fact] - public void AddOData_ForServiceProvider_ThrowsArgumentNull_Builder() - { - // Arrange - IMvcBuilder builder = null; - Action setupAction = null; + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "setupAction"); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "builder"); - } + [Fact] + public void AddOData_ForServiceProvider_ThrowsArgumentNull_Builder() + { + // Arrange + IMvcBuilder builder = null; + Action setupAction = null; - [Fact] - public void AddOData_ForServiceProvider_ThrowsArgumentNull_SetupAction() - { - // Arrange - IMvcBuilder builder = new Mock().Object; - Action setupAction = null; + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "builder"); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "setupAction"); - } + [Fact] + public void AddOData_ForServiceProvider_ThrowsArgumentNull_SetupAction() + { + // Arrange + IMvcBuilder builder = new Mock().Object; + Action setupAction = null; - [Fact] - public void AddOData_RegistersRequiredServicesIdempotently() - { - // Arrange - var services = new ServiceCollection(); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "setupAction"); + } - // Act - services.AddControllers().AddOData().AddOData(); + [Fact] + public void AddOData_RegistersRequiredServicesIdempotently() + { + // Arrange + var services = new ServiceCollection(); - // Assert - var registerd = services.Where(s => s.ServiceType == typeof(IAssemblyResolver)); - Assert.Single(registerd); - } + // Act + services.AddControllers().AddOData().AddOData(); - [Fact] - public void AddOData_RegistersODataOptions() - { - // Arrange - var services = new ServiceCollection(); - services.AddLogging(); + // Assert + var registerd = services.Where(s => s.ServiceType == typeof(IAssemblyResolver)); + Assert.Single(registerd); + } - // Act - services.AddControllers().AddOData(); - IServiceProvider provider = services.BuildServiceProvider(); + [Fact] + public void AddOData_RegistersODataOptions() + { + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); - // Assert - IOptions options = provider.GetService>(); - Assert.NotNull(options); + // Act + services.AddControllers().AddOData(); + IServiceProvider provider = services.BuildServiceProvider(); - ODataOptions odataOptions = options.Value; - Assert.Empty(odataOptions.RouteComponents); - } + // Assert + IOptions options = provider.GetService>(); + Assert.NotNull(options); - [Fact] - public void AddODataWithSetup_RegistersODataOptions() - { - // Arrange - var services = new ServiceCollection(); - services.AddLogging(); - IEdmModel coreModel = EdmCoreModel.Instance; - - // Act - services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance)); - IServiceProvider provider = services.BuildServiceProvider(); - - // Assert - IOptions options = provider.GetService>(); - Assert.NotNull(options); - - ODataOptions odataOptions = options.Value; - var model = Assert.Single(odataOptions.RouteComponents); - Assert.Equal("odata", model.Key); - Assert.Same(coreModel, model.Value.Item1); - Assert.NotNull(model.Value.Item2); - } - - [Fact] - public void AddODataWithSetup_RegistersODataOptionsWithServiceProvider() - { - // Arrange - var services = new ServiceCollection(); - services.AddLogging(); - IEdmModel coreModel = EdmCoreModel.Instance; - - services.AddSingleton(_ => coreModel); - - // Act - services.AddControllers().AddOData((options, serviceProvider) => - { - var edmModel = serviceProvider.GetRequiredService(); - options.AddRouteComponents("odata", edmModel); - }); - - IServiceProvider provider = services.BuildServiceProvider(); - - // Assert - IOptions options = provider.GetService>(); - Assert.NotNull(options); - - ODataOptions odataOptions = options.Value; - var model = Assert.Single(odataOptions.RouteComponents); - Assert.Equal("odata", model.Key); - Assert.Same(coreModel, model.Value.Item1); - Assert.NotNull(model.Value.Item2); - } - - /* - [Fact] - public void AddConvention_RegistersODataRoutingConvention() - { - // Arrange - var services = new ServiceCollection(); - - // Act - services.AddLogging(); - services.AddOData().AddConvention(); - IServiceProvider provider = services.BuildServiceProvider(); + ODataOptions odataOptions = options.Value; + Assert.Empty(odataOptions.RouteComponents); + } - // Assert - IEnumerable conventions = provider.GetServices(); - Assert.NotNull(conventions); + [Fact] + public void AddODataWithSetup_RegistersODataOptions() + { + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); + IEdmModel coreModel = EdmCoreModel.Instance; + + // Act + services.AddControllers().AddOData(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance)); + IServiceProvider provider = services.BuildServiceProvider(); + + // Assert + IOptions options = provider.GetService>(); + Assert.NotNull(options); + + ODataOptions odataOptions = options.Value; + var model = Assert.Single(odataOptions.RouteComponents); + Assert.Equal("odata", model.Key); + Assert.Same(coreModel, model.Value.Item1); + Assert.NotNull(model.Value.Item2); + } - var registeredConventions = conventions.Where(c => c.Order == int.MaxValue); - var registeredConvention = Assert.Single(registeredConventions); + [Fact] + public void AddODataWithSetup_RegistersODataOptionsWithServiceProvider() + { + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); + IEdmModel coreModel = EdmCoreModel.Instance; - Assert.IsType(registeredConvention); - } + services.AddSingleton(_ => coreModel); - [Fact] - public void ReplaceConventions_ReplaceBuiltInRoutingConvention() + // Act + services.AddControllers().AddOData((options, serviceProvider) => { - // Arrange - var services = new ServiceCollection(); + var edmModel = serviceProvider.GetRequiredService(); + options.AddRouteComponents("odata", edmModel); + }); - // Act - services.AddLogging(); - services.AddOData().ReplaceConventions(typeof(MyConvention), typeof(MetadataRoutingConvention)); + IServiceProvider provider = services.BuildServiceProvider(); - IServiceProvider provider = services.BuildServiceProvider(); + // Assert + IOptions options = provider.GetService>(); + Assert.NotNull(options); - // Assert - IODataControllerActionConvention[] conventions = provider.GetServices().ToArray(); - Assert.NotNull(conventions); - - Assert.Equal(2, conventions.Length); - Assert.IsType(conventions[0]); - Assert.IsType(conventions[1]); - } - */ + ODataOptions odataOptions = options.Value; + var model = Assert.Single(odataOptions.RouteComponents); + Assert.Equal("odata", model.Key); + Assert.Same(coreModel, model.Value.Item1); + Assert.NotNull(model.Value.Item2); } - public class MyConvention : IODataControllerActionConvention + /* + [Fact] + public void AddConvention_RegistersODataRoutingConvention() { - public int Order => int.MaxValue; + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddLogging(); + services.AddOData().AddConvention(); + IServiceProvider provider = services.BuildServiceProvider(); + + // Assert + IEnumerable conventions = provider.GetServices(); + Assert.NotNull(conventions); - public bool AppliesToAction(ODataControllerActionContext context) => true; + var registeredConventions = conventions.Where(c => c.Order == int.MaxValue); + var registeredConvention = Assert.Single(registeredConventions); - public bool AppliesToController(ODataControllerActionContext context) => true; + Assert.IsType(registeredConvention); } - public class MyMvcBuilder : IMvcBuilder + [Fact] + public void ReplaceConventions_ReplaceBuiltInRoutingConvention() { - public ApplicationPartManager PartManager { get; set; } + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddLogging(); + services.AddOData().ReplaceConventions(typeof(MyConvention), typeof(MetadataRoutingConvention)); + + IServiceProvider provider = services.BuildServiceProvider(); - public IServiceCollection Services { get; set; } + // Assert + IODataControllerActionConvention[] conventions = provider.GetServices().ToArray(); + Assert.NotNull(conventions); + + Assert.Equal(2, conventions.Length); + Assert.IsType(conventions[0]); + Assert.IsType(conventions[1]); } + */ +} + +public class MyConvention : IODataControllerActionConvention +{ + public int Order => int.MaxValue; + + public bool AppliesToAction(ODataControllerActionContext context) => true; + + public bool AppliesToController(ODataControllerActionContext context) => true; +} + +public class MyMvcBuilder : IMvcBuilder +{ + public ApplicationPartManager PartManager { get; set; } + + public IServiceCollection Services { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/ODataMvcCoreBuilderExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/ODataMvcCoreBuilderExtensionsTests.cs index a9a7e7fab..f3745dfd6 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/ODataMvcCoreBuilderExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/ODataMvcCoreBuilderExtensionsTests.cs @@ -13,145 +13,144 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests +namespace Microsoft.AspNetCore.OData.Tests; + +public class ODataMvcCoreBuilderExtensionsTests { - public class ODataMvcCoreBuilderExtensionsTests + [Fact] + public void AddOData_Throws_NullBuilder() { - [Fact] - public void AddOData_Throws_NullBuilder() - { - // Arrange - IMvcCoreBuilder builder = null; + // Arrange + IMvcCoreBuilder builder = null; - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(), "builder"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(), "builder"); + } - [Fact] - public void AddOData_Throws_NullSetupAction() - { - // Arrange - IMvcCoreBuilder builder = new MyMvcCoreBuilder(); - Action setupAction = null; + [Fact] + public void AddOData_Throws_NullSetupAction() + { + // Arrange + IMvcCoreBuilder builder = new MyMvcCoreBuilder(); + Action setupAction = null; - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "setupAction"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "setupAction"); + } - [Fact] - public void AddOData_Throws_NullBuilderWithServiceProvider() - { - // Arrange - IMvcCoreBuilder builder = null; - Action setupAction = null; + [Fact] + public void AddOData_Throws_NullBuilderWithServiceProvider() + { + // Arrange + IMvcCoreBuilder builder = null; + Action setupAction = null; - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "builder"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "builder"); + } - [Fact] - public void AddOData_Throws_NullSetupActionWithServiceProvider() - { - // Arrange - IMvcCoreBuilder builder = new MyMvcCoreBuilder(); - Action setupAction = null; + [Fact] + public void AddOData_Throws_NullSetupActionWithServiceProvider() + { + // Arrange + IMvcCoreBuilder builder = new MyMvcCoreBuilder(); + Action setupAction = null; - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "setupAction"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => builder.AddOData(setupAction), "setupAction"); + } - [Fact] - public void AddOData_OnMvcCoreBuilder_RegistersODataOptions() - { - // Arrange - var services = new ServiceCollection(); - services.AddLogging(); - IMvcCoreBuilder builder = new MyMvcCoreBuilder - { - Services = services - }; - - // Act - builder.AddOData(); - IServiceProvider provider = services.BuildServiceProvider(); - - // Assert - IOptions options = provider.GetService>(); - Assert.NotNull(options); - - ODataOptions odataOptions = options.Value; - Assert.Empty(odataOptions.RouteComponents); - } - - [Fact] - public void AddODataWithSetup_OnMvcCoreBuilder_RegistersODataOptions() + [Fact] + public void AddOData_OnMvcCoreBuilder_RegistersODataOptions() + { + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); + IMvcCoreBuilder builder = new MyMvcCoreBuilder { - // Arrange - var services = new ServiceCollection(); - services.AddLogging(); - IMvcCoreBuilder builder = new MyMvcCoreBuilder - { - Services = services - }; - - IEdmModel coreModel = EdmCoreModel.Instance; - - // Act - builder.AddOData(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance)); - IServiceProvider provider = services.BuildServiceProvider(); - - // Assert - IOptions options = provider.GetService>(); - Assert.NotNull(options); - - ODataOptions odataOptions = options.Value; - var model = Assert.Single(odataOptions.RouteComponents); - Assert.Equal("odata", model.Key); - Assert.Same(coreModel, model.Value.Item1); - Assert.NotNull(model.Value.Item2); - } - - [Fact] - public void AddODataWithSetup_RegistersODataOptionsWithServiceProvider() + Services = services + }; + + // Act + builder.AddOData(); + IServiceProvider provider = services.BuildServiceProvider(); + + // Assert + IOptions options = provider.GetService>(); + Assert.NotNull(options); + + ODataOptions odataOptions = options.Value; + Assert.Empty(odataOptions.RouteComponents); + } + + [Fact] + public void AddODataWithSetup_OnMvcCoreBuilder_RegistersODataOptions() + { + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); + IMvcCoreBuilder builder = new MyMvcCoreBuilder { - // Arrange - var services = new ServiceCollection(); - services.AddLogging(); - IEdmModel coreModel = EdmCoreModel.Instance; - - services.AddSingleton(_ => coreModel); - IMvcCoreBuilder builder = new MyMvcCoreBuilder - { - Services = services - }; - - // Act - builder.AddOData((options, serviceProvider) => - { - var edmModel = serviceProvider.GetRequiredService(); - options.AddRouteComponents("odata", edmModel); - }); - - IServiceProvider provider = services.BuildServiceProvider(); - - // Assert - IOptions options = provider.GetService>(); - Assert.NotNull(options); - - ODataOptions odataOptions = options.Value; - var model = Assert.Single(odataOptions.RouteComponents); - Assert.Equal("odata", model.Key); - Assert.Same(coreModel, model.Value.Item1); - Assert.NotNull(model.Value.Item2); - } + Services = services + }; + + IEdmModel coreModel = EdmCoreModel.Instance; + + // Act + builder.AddOData(opt => opt.AddRouteComponents("odata", EdmCoreModel.Instance)); + IServiceProvider provider = services.BuildServiceProvider(); + + // Assert + IOptions options = provider.GetService>(); + Assert.NotNull(options); + + ODataOptions odataOptions = options.Value; + var model = Assert.Single(odataOptions.RouteComponents); + Assert.Equal("odata", model.Key); + Assert.Same(coreModel, model.Value.Item1); + Assert.NotNull(model.Value.Item2); } - internal class MyMvcCoreBuilder : IMvcCoreBuilder + [Fact] + public void AddODataWithSetup_RegistersODataOptionsWithServiceProvider() { - /// - public ApplicationPartManager PartManager { get; set; } + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); + IEdmModel coreModel = EdmCoreModel.Instance; + + services.AddSingleton(_ => coreModel); + IMvcCoreBuilder builder = new MyMvcCoreBuilder + { + Services = services + }; - /// - public IServiceCollection Services { get; set; } + // Act + builder.AddOData((options, serviceProvider) => + { + var edmModel = serviceProvider.GetRequiredService(); + options.AddRouteComponents("odata", edmModel); + }); + + IServiceProvider provider = services.BuildServiceProvider(); + + // Assert + IOptions options = provider.GetService>(); + Assert.NotNull(options); + + ODataOptions odataOptions = options.Value; + var model = Assert.Single(odataOptions.RouteComponents); + Assert.Equal("odata", model.Key); + Assert.Same(coreModel, model.Value.Item1); + Assert.NotNull(model.Value.Item2); } } + +internal class MyMvcCoreBuilder : IMvcCoreBuilder +{ + /// + public ApplicationPartManager PartManager { get; set; } + + /// + public IServiceCollection Services { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/ODataMvcOptionsSetupTests.cs b/test/Microsoft.AspNetCore.OData.Tests/ODataMvcOptionsSetupTests.cs index 76607e4bd..08335d816 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/ODataMvcOptionsSetupTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/ODataMvcOptionsSetupTests.cs @@ -15,65 +15,64 @@ using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests +namespace Microsoft.AspNetCore.OData.Tests; + +public class ODataMvcOptionsSetupTests { - public class ODataMvcOptionsSetupTests + [Fact] + public void Configure_ThrowsArgumentNull_Options() { - [Fact] - public void Configure_ThrowsArgumentNull_Options() - { - // Arrange - ODataMvcOptionsSetup setup = new ODataMvcOptionsSetup(); + // Arrange + ODataMvcOptionsSetup setup = new ODataMvcOptionsSetup(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => setup.Configure(null), "options"); - } - - [Fact] - public void ODataMvcOptionsSetup_DoesNotSetup_ODataFormatters() - { - // Arrange & Act - MvcOptions options = GetMvcOptions(false); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => setup.Configure(null), "options"); + } - // Assert - Assert.DoesNotContain(options.InputFormatters, e => e is ODataInputFormatter); - Assert.DoesNotContain(options.OutputFormatters, e => e is ODataOutputFormatter); - } + [Fact] + public void ODataMvcOptionsSetup_DoesNotSetup_ODataFormatters() + { + // Arrange & Act + MvcOptions options = GetMvcOptions(false); - [Fact] - public void ODataMvcOptionsSetup_Setup_ODataFormatters() - { - // Arrange & Act - MvcOptions options = GetMvcOptions(true); + // Assert + Assert.DoesNotContain(options.InputFormatters, e => e is ODataInputFormatter); + Assert.DoesNotContain(options.OutputFormatters, e => e is ODataOutputFormatter); + } - // Assert - Assert.Collection(options.InputFormatters.Take(3), - e => Assert.IsType(e), - e => Assert.IsType(e), - e => Assert.IsType(e)); + [Fact] + public void ODataMvcOptionsSetup_Setup_ODataFormatters() + { + // Arrange & Act + MvcOptions options = GetMvcOptions(true); - Assert.Collection(options.OutputFormatters.Take(3), - e => Assert.IsType(e), - e => Assert.IsType(e), - e => Assert.IsType(e)); - } + // Assert + Assert.Collection(options.InputFormatters.Take(3), + e => Assert.IsType(e), + e => Assert.IsType(e), + e => Assert.IsType(e)); - private static MvcOptions GetMvcOptions(bool withOData) - { - IServiceCollection services = new ServiceCollection(); + Assert.Collection(options.OutputFormatters.Take(3), + e => Assert.IsType(e), + e => Assert.IsType(e), + e => Assert.IsType(e)); + } - services.AddControllers(); + private static MvcOptions GetMvcOptions(bool withOData) + { + IServiceCollection services = new ServiceCollection(); - services.AddTransient(); + services.AddControllers(); - if (withOData) - { - services.TryAddEnumerable( - ServiceDescriptor.Transient, ODataMvcOptionsSetup>()); - } + services.AddTransient(); - var serviceProvider = services.BuildServiceProvider(); - return serviceProvider.GetRequiredService>().Value; + if (withOData) + { + services.TryAddEnumerable( + ServiceDescriptor.Transient, ODataMvcOptionsSetup>()); } + + var serviceProvider = services.BuildServiceProvider(); + return serviceProvider.GetRequiredService>().Value; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/ODataOptionsSetupTests.cs b/test/Microsoft.AspNetCore.OData.Tests/ODataOptionsSetupTests.cs index 012d0f737..3998872bd 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/ODataOptionsSetupTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/ODataOptionsSetupTests.cs @@ -18,73 +18,72 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests +namespace Microsoft.AspNetCore.OData.Tests; + +public class ODataOptionsSetupTests { - public class ODataOptionsSetupTests + [Fact] + public void Configure_ThrowsArgumentNull_Options() { - [Fact] - public void Configure_ThrowsArgumentNull_Options() - { - // Arrange - ILoggerFactory factory = new Mock().Object; - IODataPathTemplateParser parser = new Mock().Object; - ODataOptionsSetup setup = new ODataOptionsSetup(factory, parser); + // Arrange + ILoggerFactory factory = new Mock().Object; + IODataPathTemplateParser parser = new Mock().Object; + ODataOptionsSetup setup = new ODataOptionsSetup(factory, parser); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => setup.Configure(null), "options"); - } - - [Fact] - public void ODataOptionsSetup_DoesNotSetup_ODataRoutingConventions() - { - // Arrange & Act - ODataOptions options = GetODataOptions(false); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => setup.Configure(null), "options"); + } - // Assert - Assert.Empty(options.Conventions); - } + [Fact] + public void ODataOptionsSetup_DoesNotSetup_ODataRoutingConventions() + { + // Arrange & Act + ODataOptions options = GetODataOptions(false); - [Fact] - public void ODataOptionsSetup_Setup_ODataRoutingConventions() - { - // Arrange & Act - ODataOptions options = GetODataOptions(true); + // Assert + Assert.Empty(options.Conventions); + } - // Assert - Assert.Equal(11, options.Conventions.Count); + [Fact] + public void ODataOptionsSetup_Setup_ODataRoutingConventions() + { + // Arrange & Act + ODataOptions options = GetODataOptions(true); - // Test the following - Assert.Contains(options.Conventions, e => e is AttributeRoutingConvention); - Assert.Contains(options.Conventions, e => e is EntitySetRoutingConvention); - Assert.Contains(options.Conventions, e => e is SingletonRoutingConvention); - } + // Assert + Assert.Equal(11, options.Conventions.Count); - private static ODataOptions GetODataOptions(bool withOData) - { - IServiceCollection services = new ServiceCollection(); + // Test the following + Assert.Contains(options.Conventions, e => e is AttributeRoutingConvention); + Assert.Contains(options.Conventions, e => e is EntitySetRoutingConvention); + Assert.Contains(options.Conventions, e => e is SingletonRoutingConvention); + } - services.AddControllers(); + private static ODataOptions GetODataOptions(bool withOData) + { + IServiceCollection services = new ServiceCollection(); - services.TryAddSingleton(); - services.AddTransient(); - services.Configure(opt => { }); + services.AddControllers(); - if (withOData) - { - services.TryAddEnumerable( - ServiceDescriptor.Transient, ODataOptionsSetup>()); - } + services.TryAddSingleton(); + services.AddTransient(); + services.Configure(opt => { }); - var serviceProvider = services.BuildServiceProvider(); - return serviceProvider.GetRequiredService>().Value; + if (withOData) + { + services.TryAddEnumerable( + ServiceDescriptor.Transient, ODataOptionsSetup>()); } - private class MytemplateTranslater : IODataPathTemplateParser + var serviceProvider = services.BuildServiceProvider(); + return serviceProvider.GetRequiredService>().Value; + } + + private class MytemplateTranslater : IODataPathTemplateParser + { + public ODataPathTemplate Parse(IEdmModel model, string odataPath, IServiceProvider requestProvider) { - public ODataPathTemplate Parse(IEdmModel model, string odataPath, IServiceProvider requestProvider) - { - return new ODataPathTemplate(); - } + return new ODataPathTemplate(); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/ODataOptionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/ODataOptionsTests.cs index acc426214..b3163d08f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/ODataOptionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/ODataOptionsTests.cs @@ -17,367 +17,366 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests +namespace Microsoft.AspNetCore.OData.Tests; + +public class ODataOptionsTests { - public class ODataOptionsTests + [Fact] + public void PropertySetting_SetsCorrectValue() { - [Fact] - public void PropertySetting_SetsCorrectValue() - { - // Arrange - ODataOptions options = new ODataOptions(); - - // verify default - Assert.Equal(ODataUrlKeyDelimiter.Slash, options.UrlKeyDelimiter); // Guard - Assert.False(options.EnableContinueOnErrorHeader); - Assert.True(options.EnableAttributeRouting); - Assert.Equal(TimeZoneInfo.Local, options.TimeZone); - Assert.True(options.RouteOptions.EnableKeyAsSegment); - Assert.True(options.EnableNoDollarQueryOptions); - - // Act - options.UrlKeyDelimiter = ODataUrlKeyDelimiter.Parentheses; - options.EnableContinueOnErrorHeader = true; - options.EnableAttributeRouting = false; - options.TimeZone = TimeZoneInfo.Utc; - options.RouteOptions.EnableKeyAsSegment = false; - options.EnableNoDollarQueryOptions = false; - - // Act & Assert - Assert.Equal(ODataUrlKeyDelimiter.Parentheses, options.UrlKeyDelimiter); - Assert.True(options.EnableContinueOnErrorHeader); - Assert.False(options.EnableAttributeRouting); - Assert.Equal(TimeZoneInfo.Utc, options.TimeZone); - Assert.False(options.RouteOptions.EnableKeyAsSegment); - Assert.False(options.EnableNoDollarQueryOptions); - - Assert.Empty(options.RouteComponents); - } + // Arrange + ODataOptions options = new ODataOptions(); + + // verify default + Assert.Equal(ODataUrlKeyDelimiter.Slash, options.UrlKeyDelimiter); // Guard + Assert.False(options.EnableContinueOnErrorHeader); + Assert.True(options.EnableAttributeRouting); + Assert.Equal(TimeZoneInfo.Local, options.TimeZone); + Assert.True(options.RouteOptions.EnableKeyAsSegment); + Assert.True(options.EnableNoDollarQueryOptions); + + // Act + options.UrlKeyDelimiter = ODataUrlKeyDelimiter.Parentheses; + options.EnableContinueOnErrorHeader = true; + options.EnableAttributeRouting = false; + options.TimeZone = TimeZoneInfo.Utc; + options.RouteOptions.EnableKeyAsSegment = false; + options.EnableNoDollarQueryOptions = false; + + // Act & Assert + Assert.Equal(ODataUrlKeyDelimiter.Parentheses, options.UrlKeyDelimiter); + Assert.True(options.EnableContinueOnErrorHeader); + Assert.False(options.EnableAttributeRouting); + Assert.Equal(TimeZoneInfo.Utc, options.TimeZone); + Assert.False(options.RouteOptions.EnableKeyAsSegment); + Assert.False(options.EnableNoDollarQueryOptions); + + Assert.Empty(options.RouteComponents); + } - [Fact] - public void Conventions_InsertsConvention() - { - // Arrange - ODataOptions options = new ODataOptions(); - Assert.Empty(options.Conventions); // Guard - Mock mock = new Mock(); + [Fact] + public void Conventions_InsertsConvention() + { + // Arrange + ODataOptions options = new ODataOptions(); + Assert.Empty(options.Conventions); // Guard + Mock mock = new Mock(); - // Act - options.Conventions.Add(mock.Object); + // Act + options.Conventions.Add(mock.Object); - // & Assert - IODataControllerActionConvention convention = Assert.Single(options.Conventions); - Assert.Same(mock.Object, convention); - } + // & Assert + IODataControllerActionConvention convention = Assert.Single(options.Conventions); + Assert.Same(mock.Object, convention); + } - #region AddModel - [Theory] - [InlineData(null)] - [InlineData("odata")] - public void AddRouteComponents_WithoutOrWithPrefix_SetModel(string prefix) + #region AddModel + [Theory] + [InlineData(null)] + [InlineData("odata")] + public void AddRouteComponents_WithoutOrWithPrefix_SetModel(string prefix) + { + // Arrange + ODataOptions options = new ODataOptions(); + IEdmModel edmModel = EdmCoreModel.Instance; + + // Act + if (prefix == null) { - // Arrange - ODataOptions options = new ODataOptions(); - IEdmModel edmModel = EdmCoreModel.Instance; - - // Act - if (prefix == null) - { - options.AddRouteComponents(edmModel); - } - else - { - options.AddRouteComponents(prefix, edmModel); - } - - // Assert - KeyValuePair model = Assert.Single(options.RouteComponents); - - Assert.Equal(prefix ?? String.Empty, model.Key); - - Assert.Same(edmModel, model.Value.Item1); - Assert.NotNull(model.Value.Item2); + options.AddRouteComponents(edmModel); } - - [Fact] - public void AddRouteComponents_WithBatchHandler_SetModel() + else { - // Arrange - ODataOptions options = new ODataOptions(); - IEdmModel edmModel = EdmCoreModel.Instance; - ODataBatchHandler handler = new Mock().Object; - - // Act - options.AddRouteComponents(edmModel, handler); - - // Assert - KeyValuePair model = Assert.Single(options.RouteComponents); - Assert.Equal(String.Empty, model.Key); - - Assert.Same(edmModel, model.Value.Item1); - Assert.NotNull(model.Value.Item2); - ODataBatchHandler actual = model.Value.Item2.GetService(); - Assert.Same(handler, actual); + options.AddRouteComponents(prefix, edmModel); } - [Fact] - public void AddRouteComponents_WithDependencyInjection_SetModelAndServices() - { - // Arrange - ODataOptions options = new ODataOptions(); - IEdmModel edmModel = EdmCoreModel.Instance; + // Assert + KeyValuePair model = Assert.Single(options.RouteComponents); - // Act - options.AddRouteComponents("odata", edmModel, services => services.AddSingleton()); + Assert.Equal(prefix ?? String.Empty, model.Key); - // Assert - KeyValuePair model = Assert.Single(options.RouteComponents); - Assert.Equal("odata", model.Key); + Assert.Same(edmModel, model.Value.Item1); + Assert.NotNull(model.Value.Item2); + } - Assert.Same(edmModel, model.Value.Item1); - Assert.NotNull(model.Value.Item2); - IODataFeature actual = model.Value.Item2.GetService(); - Assert.IsType(actual); - } + [Fact] + public void AddRouteComponents_WithBatchHandler_SetModel() + { + // Arrange + ODataOptions options = new ODataOptions(); + IEdmModel edmModel = EdmCoreModel.Instance; + ODataBatchHandler handler = new Mock().Object; + + // Act + options.AddRouteComponents(edmModel, handler); + + // Assert + KeyValuePair model = Assert.Single(options.RouteComponents); + Assert.Equal(String.Empty, model.Key); + + Assert.Same(edmModel, model.Value.Item1); + Assert.NotNull(model.Value.Item2); + ODataBatchHandler actual = model.Value.Item2.GetService(); + Assert.Same(handler, actual); + } - [Theory] - [InlineData(ODataVersion.V4, true)] - [InlineData(ODataVersion.V401, true)] - public void AddRouteComponents_WithVersionAndDependencyInjection_SetModelAndServices(ODataVersion version, bool readingODataPrefixSetting) - { - // Arrange - ODataOptions options = new ODataOptions(); - IEdmModel edmModel = EdmCoreModel.Instance; + [Fact] + public void AddRouteComponents_WithDependencyInjection_SetModelAndServices() + { + // Arrange + ODataOptions options = new ODataOptions(); + IEdmModel edmModel = EdmCoreModel.Instance; - // Act - options.AddRouteComponents("odata", edmModel, version, null); + // Act + options.AddRouteComponents("odata", edmModel, services => services.AddSingleton()); - // Assert - KeyValuePair model = Assert.Single(options.RouteComponents); - Assert.Equal("odata", model.Key); + // Assert + KeyValuePair model = Assert.Single(options.RouteComponents); + Assert.Equal("odata", model.Key); - Assert.Same(edmModel, model.Value.Item1); - Assert.NotNull(model.Value.Item2); - ODataMessageReaderSettings actual = model.Value.Item2.GetService(); - Assert.Equal(readingODataPrefixSetting, actual.EnableReadingODataAnnotationWithoutPrefix); - } + Assert.Same(edmModel, model.Value.Item1); + Assert.NotNull(model.Value.Item2); + IODataFeature actual = model.Value.Item2.GetService(); + Assert.IsType(actual); + } - [Theory] - [InlineData("/odata", "odata")] - [InlineData("/odata/", "odata")] - [InlineData("odata/", "odata")] - [InlineData("/", "")] - public void AddRouteComponents_Strips_RoutePrefix_Leading_And_Trailing_Slashes(string routePrefix, string expectedRoutePrefix) - { - // Arrange - ODataOptions options = new ODataOptions(); - IEdmModel edmModel = EdmCoreModel.Instance; + [Theory] + [InlineData(ODataVersion.V4, true)] + [InlineData(ODataVersion.V401, true)] + public void AddRouteComponents_WithVersionAndDependencyInjection_SetModelAndServices(ODataVersion version, bool readingODataPrefixSetting) + { + // Arrange + ODataOptions options = new ODataOptions(); + IEdmModel edmModel = EdmCoreModel.Instance; - // Act - options.AddRouteComponents(routePrefix, edmModel, services => services.AddSingleton()); + // Act + options.AddRouteComponents("odata", edmModel, version, null); - // Assert - Assert.False(options.RouteComponents.ContainsKey(routePrefix)); - Assert.True(options.RouteComponents.ContainsKey(expectedRoutePrefix)); - } + // Assert + KeyValuePair model = Assert.Single(options.RouteComponents); + Assert.Equal("odata", model.Key); - [Fact] - public void AddRouteComponents_Throws_IfModelNull() - { - // Arrange - ODataOptions options = new ODataOptions(); + Assert.Same(edmModel, model.Value.Item1); + Assert.NotNull(model.Value.Item2); + ODataMessageReaderSettings actual = model.Value.Item2.GetService(); + Assert.Equal(readingODataPrefixSetting, actual.EnableReadingODataAnnotationWithoutPrefix); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => options.AddRouteComponents("odata", null, builder => { }), "model"); - } + [Theory] + [InlineData("/odata", "odata")] + [InlineData("/odata/", "odata")] + [InlineData("odata/", "odata")] + [InlineData("/", "")] + public void AddRouteComponents_Strips_RoutePrefix_Leading_And_Trailing_Slashes(string routePrefix, string expectedRoutePrefix) + { + // Arrange + ODataOptions options = new ODataOptions(); + IEdmModel edmModel = EdmCoreModel.Instance; - [Fact] - public void AddRouteComponents_Throws_IfRoutePrefixNull() - { - // Arrange - ODataOptions options = new ODataOptions(); + // Act + options.AddRouteComponents(routePrefix, edmModel, services => services.AddSingleton()); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => options.AddRouteComponents(null, EdmCoreModel.Instance, builder => { }), "routePrefix"); - } + // Assert + Assert.False(options.RouteComponents.ContainsKey(routePrefix)); + Assert.True(options.RouteComponents.ContainsKey(expectedRoutePrefix)); + } - [Fact] - public void AddRouteComponents_Throws_IfPrefixExisted() - { - // Arrange - ODataOptions options = new ODataOptions(); - IEdmModel edmModel = EdmCoreModel.Instance; - options.AddRouteComponents("odata", edmModel); + [Fact] + public void AddRouteComponents_Throws_IfModelNull() + { + // Arrange + ODataOptions options = new ODataOptions(); - // Act - Action test = () => options.AddRouteComponents("odata", edmModel); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => options.AddRouteComponents("odata", null, builder => { }), "model"); + } - // Assert - ExceptionAssert.Throws(test, "The prefix 'odata' was already used for other Edm model."); + [Fact] + public void AddRouteComponents_Throws_IfRoutePrefixNull() + { + // Arrange + ODataOptions options = new ODataOptions(); - } - #endregion + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => options.AddRouteComponents(null, EdmCoreModel.Instance, builder => { }), "routePrefix"); + } - [Fact] - public void GetRouteServices_ReturnsNull() - { - // Arrange - ODataOptions options = new ODataOptions(); + [Fact] + public void AddRouteComponents_Throws_IfPrefixExisted() + { + // Arrange + ODataOptions options = new ODataOptions(); + IEdmModel edmModel = EdmCoreModel.Instance; + options.AddRouteComponents("odata", edmModel); - // Act & Assert - Assert.Null(options.GetRouteServices(null)); - } + // Act + Action test = () => options.AddRouteComponents("odata", edmModel); - [Fact] - public void GetRouteServices_ReturnsCorrectServiceProvider() - { - // Arrange - ODataOptions options = new ODataOptions(); - IEdmModel edmModel = EdmCoreModel.Instance; + // Assert + ExceptionAssert.Throws(test, "The prefix 'odata' was already used for other Edm model."); - // Act - options.AddRouteComponents("odata", edmModel); + } + #endregion - // & Assert - IServiceProvider sp = options.GetRouteServices("odata"); - Assert.NotNull(sp); - } + [Fact] + public void GetRouteServices_ReturnsNull() + { + // Arrange + ODataOptions options = new ODataOptions(); - [Theory] - [InlineData("/odata")] - [InlineData("/odata/")] - [InlineData("odata/")] - public void GetRouteServices_ReturnsCorrectServiceProvider_When_Leading_Or_Trailing_Slashes(string routePrefix) - { - // Arrange - ODataOptions options = new ODataOptions(); - IEdmModel edmModel = EdmCoreModel.Instance; - - // Act - options.AddRouteComponents(routePrefix, edmModel); - - // & Assert - // can retrieve service provider using original routePrefix - IServiceProvider sp = options.GetRouteServices(routePrefix); - Assert.NotNull(sp); - - // can retrieve service provider using sanitized routePrefix - string sanitizedRoutePrefix = "odata"; - IServiceProvider sp2 = options.GetRouteServices(sanitizedRoutePrefix); - Assert.NotNull(sp2); - } + // Act & Assert + Assert.Null(options.GetRouteServices(null)); + } - #region QuerySetting - [Fact] - public void SetMaxTop_Throws_ForWrongValue() - { - // Arrange - ODataOptions options = new ODataOptions(); + [Fact] + public void GetRouteServices_ReturnsCorrectServiceProvider() + { + // Arrange + ODataOptions options = new ODataOptions(); + IEdmModel edmModel = EdmCoreModel.Instance; - // Act & Assert - ArgumentOutOfRangeException exception = ExceptionAssert.Throws(() => options.SetMaxTop(-2)); - Assert.Contains("Value must be greater than or equal to 0", exception.Message); - } + // Act + options.AddRouteComponents("odata", edmModel); - [Fact] - public void SetMaxTop_SetMaxTopValue() - { - // Arrange - ODataOptions options = new ODataOptions(); - Assert.Equal(0, options.QueryConfigurations.MaxTop); // Guard + // & Assert + IServiceProvider sp = options.GetRouteServices("odata"); + Assert.NotNull(sp); + } - // Act - options.SetMaxTop(2); + [Theory] + [InlineData("/odata")] + [InlineData("/odata/")] + [InlineData("odata/")] + public void GetRouteServices_ReturnsCorrectServiceProvider_When_Leading_Or_Trailing_Slashes(string routePrefix) + { + // Arrange + ODataOptions options = new ODataOptions(); + IEdmModel edmModel = EdmCoreModel.Instance; + + // Act + options.AddRouteComponents(routePrefix, edmModel); + + // & Assert + // can retrieve service provider using original routePrefix + IServiceProvider sp = options.GetRouteServices(routePrefix); + Assert.NotNull(sp); + + // can retrieve service provider using sanitized routePrefix + string sanitizedRoutePrefix = "odata"; + IServiceProvider sp2 = options.GetRouteServices(sanitizedRoutePrefix); + Assert.NotNull(sp2); + } - // Assert - Assert.Equal(2, options.QueryConfigurations.MaxTop.Value); - } + #region QuerySetting + [Fact] + public void SetMaxTop_Throws_ForWrongValue() + { + // Arrange + ODataOptions options = new ODataOptions(); - [Fact] - public void Expand_SetExpand() - { - // Arrange - ODataOptions options = new ODataOptions(); - Assert.False(options.QueryConfigurations.EnableExpand); // Guard + // Act & Assert + ArgumentOutOfRangeException exception = ExceptionAssert.Throws(() => options.SetMaxTop(-2)); + Assert.Contains("Value must be greater than or equal to 0", exception.Message); + } - // Act - options.Expand(); + [Fact] + public void SetMaxTop_SetMaxTopValue() + { + // Arrange + ODataOptions options = new ODataOptions(); + Assert.Equal(0, options.QueryConfigurations.MaxTop); // Guard - // Assert - Assert.True(options.QueryConfigurations.EnableExpand); - } + // Act + options.SetMaxTop(2); - [Fact] - public void Select_SetSelect() - { - // Arrange - ODataOptions options = new ODataOptions(); - Assert.False(options.QueryConfigurations.EnableSelect); // Guard + // Assert + Assert.Equal(2, options.QueryConfigurations.MaxTop.Value); + } - // Act - options.Select(); + [Fact] + public void Expand_SetExpand() + { + // Arrange + ODataOptions options = new ODataOptions(); + Assert.False(options.QueryConfigurations.EnableExpand); // Guard - // Assert - Assert.True(options.QueryConfigurations.EnableSelect); - } + // Act + options.Expand(); - [Fact] - public void Filter_SetFilter() - { - // Arrange - ODataOptions options = new ODataOptions(); - Assert.False(options.QueryConfigurations.EnableFilter); // Guard + // Assert + Assert.True(options.QueryConfigurations.EnableExpand); + } - // Act - options.Filter(); + [Fact] + public void Select_SetSelect() + { + // Arrange + ODataOptions options = new ODataOptions(); + Assert.False(options.QueryConfigurations.EnableSelect); // Guard - // Assert - Assert.True(options.QueryConfigurations.EnableFilter); - } + // Act + options.Select(); - [Fact] - public void OrderBy_SetOrderBy() - { - // Arrange - ODataOptions options = new ODataOptions(); - Assert.False(options.QueryConfigurations.EnableOrderBy); // Guard + // Assert + Assert.True(options.QueryConfigurations.EnableSelect); + } - // Act - options.OrderBy(); + [Fact] + public void Filter_SetFilter() + { + // Arrange + ODataOptions options = new ODataOptions(); + Assert.False(options.QueryConfigurations.EnableFilter); // Guard - // Assert - Assert.True(options.QueryConfigurations.EnableOrderBy); - } + // Act + options.Filter(); - [Fact] - public void Count_SetCount() - { - // Arrange - ODataOptions options = new ODataOptions(); - Assert.False(options.QueryConfigurations.EnableCount); // Guard + // Assert + Assert.True(options.QueryConfigurations.EnableFilter); + } - // Act - options.Count(); + [Fact] + public void OrderBy_SetOrderBy() + { + // Arrange + ODataOptions options = new ODataOptions(); + Assert.False(options.QueryConfigurations.EnableOrderBy); // Guard - // Assert - Assert.True(options.QueryConfigurations.EnableCount); - } + // Act + options.OrderBy(); - [Fact] - public void SkipToken_SetSkipToken() - { - // Arrange - ODataOptions options = new ODataOptions(); - Assert.False(options.QueryConfigurations.EnableSkipToken); // Guard + // Assert + Assert.True(options.QueryConfigurations.EnableOrderBy); + } - // Act - options.SkipToken(); + [Fact] + public void Count_SetCount() + { + // Arrange + ODataOptions options = new ODataOptions(); + Assert.False(options.QueryConfigurations.EnableCount); // Guard - // Assert - Assert.True(options.QueryConfigurations.EnableSkipToken); - } - #endregion + // Act + options.Count(); + + // Assert + Assert.True(options.QueryConfigurations.EnableCount); + } + + [Fact] + public void SkipToken_SetSkipToken() + { + // Arrange + ODataOptions options = new ODataOptions(); + Assert.False(options.QueryConfigurations.EnableSkipToken); // Guard + // Act + options.SkipToken(); + + // Assert + Assert.True(options.QueryConfigurations.EnableSkipToken); } + #endregion } + diff --git a/test/Microsoft.AspNetCore.OData.Tests/ODataPathExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/ODataPathExtensionsTests.cs index 3a6dfc566..11bc9127c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/ODataPathExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/ODataPathExtensionsTests.cs @@ -14,198 +14,197 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests +namespace Microsoft.AspNetCore.OData.Tests; + +public class ODataPathExtensionsTests { - public class ODataPathExtensionsTests + [Fact] + public void IsStreamPropertyPath_Returns_CollectBooleanValue() { - [Fact] - public void IsStreamPropertyPath_Returns_CollectBooleanValue() - { - // Arrange - ODataPath path = null; + // Arrange + ODataPath path = null; - // Act & Assert - Assert.False(path.IsStreamPropertyPath()); + // Act & Assert + Assert.False(path.IsStreamPropertyPath()); - // Act & Assert - path = new ODataPath(MetadataSegment.Instance); - Assert.False(path.IsStreamPropertyPath()); + // Act & Assert + path = new ODataPath(MetadataSegment.Instance); + Assert.False(path.IsStreamPropertyPath()); - // Act & Assert - IEdmTypeReference typeRef = EdmCoreModel.Instance.GetStream(false); - Mock mock = new Mock(); - mock.Setup(s => s.Name).Returns("any"); - mock.Setup(s => s.Type).Returns(typeRef); - PropertySegment segment = new PropertySegment(mock.Object); + // Act & Assert + IEdmTypeReference typeRef = EdmCoreModel.Instance.GetStream(false); + Mock mock = new Mock(); + mock.Setup(s => s.Name).Returns("any"); + mock.Setup(s => s.Type).Returns(typeRef); + PropertySegment segment = new PropertySegment(mock.Object); - path = new ODataPath(segment); - Assert.True(path.IsStreamPropertyPath()); - } + path = new ODataPath(segment); + Assert.True(path.IsStreamPropertyPath()); + } - [Fact] - public void GetEdmType_ThrowsArgumentNull_Path() - { - // Arrange & Act & Assert - ODataPath path = null; - ExceptionAssert.ThrowsArgumentNull(() => path.GetEdmType(), "path"); - } + [Fact] + public void GetEdmType_ThrowsArgumentNull_Path() + { + // Arrange & Act & Assert + ODataPath path = null; + ExceptionAssert.ThrowsArgumentNull(() => path.GetEdmType(), "path"); + } - [Fact] - public void GetNavigationSource_ThrowsArgumentNull_Path() + [Fact] + public void GetNavigationSource_ThrowsArgumentNull_Path() + { + // Arrange & Act & Assert + ODataPath path = null; + ExceptionAssert.ThrowsArgumentNull(() => path.GetNavigationSource(), "path"); + } + + [Theory] + // entity set segment should return the entity set + [InlineData("Customers", "Customers", EdmNavigationSourceKind.EntitySet)] + // key segment should return the corresponding entity set + [InlineData("Customers(1)", "Customers", EdmNavigationSourceKind.EntitySet)] + // property segment should return the previous segment's navigation source + [InlineData("Customers(1)/Name", "Customers", EdmNavigationSourceKind.EntitySet)] + // navigation property segment + [InlineData("Customers(1)/Orders", "Orders", EdmNavigationSourceKind.EntitySet)] + // key segment of a navigation property + [InlineData("Customers(1)/Orders(1)", "Orders", EdmNavigationSourceKind.EntitySet)] + // navigation property link segment + [InlineData("Customers(1)/Orders(1)/$ref", "Orders", EdmNavigationSourceKind.EntitySet)] + // property segment of a navigation property + [InlineData("Customers(1)/Orders(1)/Amount", "Orders", EdmNavigationSourceKind.EntitySet)] + // non-contained navigation property without an entity set should return UnknownEntitySet + [InlineData("MyOrders(1)/NonContainedOrderLines", "NonContainedOrderLines", EdmNavigationSourceKind.UnknownEntitySet)] + [InlineData("MyOrders(1)/NonContainedOrderLines(1)", "NonContainedOrderLines", EdmNavigationSourceKind.UnknownEntitySet)] + [InlineData("MyOrders(1)/NonContainedOrderLines(1)/$ref", "NonContainedOrderLines", EdmNavigationSourceKind.UnknownEntitySet)] + // contained navigation property should return ContainedEntitySet + [InlineData("MyOrders(1)/OrderLines", "OrderLines", EdmNavigationSourceKind.ContainedEntitySet)] + [InlineData("MyOrders(1)/OrderLines(1)", "OrderLines", EdmNavigationSourceKind.ContainedEntitySet)] + [InlineData("MyOrders(1)/OrderLines(1)/$ref", "OrderLines", EdmNavigationSourceKind.ContainedEntitySet)] + // property segment with a complex type, returns navigation source of previous segment + [InlineData("Customers(1)/Account", "Customers", EdmNavigationSourceKind.EntitySet)] + // property segment of a complex type, returns navigation source of previous segment + [InlineData("Customers(1)/Account/Bank", "Customers", EdmNavigationSourceKind.EntitySet)] + // dynamic path segment should return null + [InlineData("Customers(1)/Account/DynamicProperty", null, EdmNavigationSourceKind.None)] + // unbound operation import with an entity set path should return the entity set + [InlineData("GetTopCustomers()", "Customers", EdmNavigationSourceKind.EntitySet)] + // unbound operation import without an entity set path should return null + [InlineData("GetTotalSalesAmount()", null, EdmNavigationSourceKind.None)] + // bound operation without an entity set path should return null + [InlineData("Customers(1)/NS.IsUpgraded()", null, EdmNavigationSourceKind.None)] + [InlineData("Customers/NS.UpgradeAll()", null, EdmNavigationSourceKind.None)] + // bound operation with an entity set path should return the entity set + [InlineData("Customers/NS.GetTop", "Customers", EdmNavigationSourceKind.EntitySet)] + [InlineData("Customers/NS.GetBestOrders()", "Orders", EdmNavigationSourceKind.EntitySet)] + // singleton segment should return the singleton + [InlineData("VipCustomer", "VipCustomer", EdmNavigationSourceKind.Singleton)] + [InlineData("VipCustomer/Name", "VipCustomer", EdmNavigationSourceKind.Singleton)] + // navigation property segment after a singleton should return the navigation property's entity set + [InlineData("VipCustomer/Orders", "Orders", EdmNavigationSourceKind.EntitySet)] + [InlineData("VipCustomer/NS.IsUpgraded", null, EdmNavigationSourceKind.None)] + // type segment should return the corresponding navigation source + [InlineData("Customers(1)/NS.SpecialCustomer", "Customers", EdmNavigationSourceKind.EntitySet)] + [InlineData("VipCustomer/NS.SpecialCustomer", "VipCustomer", EdmNavigationSourceKind.Singleton)] + // coung segment should return null + [InlineData("Customers/$count", null, EdmNavigationSourceKind.None)] + // value segment should return null + [InlineData("Customers(1)/Account/Amount/$value", null, EdmNavigationSourceKind.None)] + // batch segment should return null + [InlineData("$batch", null, EdmNavigationSourceKind.None)] + // metadata segment should return null + [InlineData("$metadata", null, EdmNavigationSourceKind.None)] + public void GetNavigationSource_ReturnsCorrectNavigationSource(string path, string expectedNavigationSource, EdmNavigationSourceKind navigationSourceKind) + { + var model = new Models.CustomersModelWithInheritance(); + var parser = new ODataUriParser(model.Model, new Uri(path, UriKind.Relative)); + parser.EnableUriTemplateParsing = true; + ODataPath odataPath = parser.ParsePath(); + + var navigationSource = odataPath.GetNavigationSource(); + + if (expectedNavigationSource == null) { - // Arrange & Act & Assert - ODataPath path = null; - ExceptionAssert.ThrowsArgumentNull(() => path.GetNavigationSource(), "path"); + Assert.Null(navigationSource); } - - [Theory] - // entity set segment should return the entity set - [InlineData("Customers", "Customers", EdmNavigationSourceKind.EntitySet)] - // key segment should return the corresponding entity set - [InlineData("Customers(1)", "Customers", EdmNavigationSourceKind.EntitySet)] - // property segment should return the previous segment's navigation source - [InlineData("Customers(1)/Name", "Customers", EdmNavigationSourceKind.EntitySet)] - // navigation property segment - [InlineData("Customers(1)/Orders", "Orders", EdmNavigationSourceKind.EntitySet)] - // key segment of a navigation property - [InlineData("Customers(1)/Orders(1)", "Orders", EdmNavigationSourceKind.EntitySet)] - // navigation property link segment - [InlineData("Customers(1)/Orders(1)/$ref", "Orders", EdmNavigationSourceKind.EntitySet)] - // property segment of a navigation property - [InlineData("Customers(1)/Orders(1)/Amount", "Orders", EdmNavigationSourceKind.EntitySet)] - // non-contained navigation property without an entity set should return UnknownEntitySet - [InlineData("MyOrders(1)/NonContainedOrderLines", "NonContainedOrderLines", EdmNavigationSourceKind.UnknownEntitySet)] - [InlineData("MyOrders(1)/NonContainedOrderLines(1)", "NonContainedOrderLines", EdmNavigationSourceKind.UnknownEntitySet)] - [InlineData("MyOrders(1)/NonContainedOrderLines(1)/$ref", "NonContainedOrderLines", EdmNavigationSourceKind.UnknownEntitySet)] - // contained navigation property should return ContainedEntitySet - [InlineData("MyOrders(1)/OrderLines", "OrderLines", EdmNavigationSourceKind.ContainedEntitySet)] - [InlineData("MyOrders(1)/OrderLines(1)", "OrderLines", EdmNavigationSourceKind.ContainedEntitySet)] - [InlineData("MyOrders(1)/OrderLines(1)/$ref", "OrderLines", EdmNavigationSourceKind.ContainedEntitySet)] - // property segment with a complex type, returns navigation source of previous segment - [InlineData("Customers(1)/Account", "Customers", EdmNavigationSourceKind.EntitySet)] - // property segment of a complex type, returns navigation source of previous segment - [InlineData("Customers(1)/Account/Bank", "Customers", EdmNavigationSourceKind.EntitySet)] - // dynamic path segment should return null - [InlineData("Customers(1)/Account/DynamicProperty", null, EdmNavigationSourceKind.None)] - // unbound operation import with an entity set path should return the entity set - [InlineData("GetTopCustomers()", "Customers", EdmNavigationSourceKind.EntitySet)] - // unbound operation import without an entity set path should return null - [InlineData("GetTotalSalesAmount()", null, EdmNavigationSourceKind.None)] - // bound operation without an entity set path should return null - [InlineData("Customers(1)/NS.IsUpgraded()", null, EdmNavigationSourceKind.None)] - [InlineData("Customers/NS.UpgradeAll()", null, EdmNavigationSourceKind.None)] - // bound operation with an entity set path should return the entity set - [InlineData("Customers/NS.GetTop", "Customers", EdmNavigationSourceKind.EntitySet)] - [InlineData("Customers/NS.GetBestOrders()", "Orders", EdmNavigationSourceKind.EntitySet)] - // singleton segment should return the singleton - [InlineData("VipCustomer", "VipCustomer", EdmNavigationSourceKind.Singleton)] - [InlineData("VipCustomer/Name", "VipCustomer", EdmNavigationSourceKind.Singleton)] - // navigation property segment after a singleton should return the navigation property's entity set - [InlineData("VipCustomer/Orders", "Orders", EdmNavigationSourceKind.EntitySet)] - [InlineData("VipCustomer/NS.IsUpgraded", null, EdmNavigationSourceKind.None)] - // type segment should return the corresponding navigation source - [InlineData("Customers(1)/NS.SpecialCustomer", "Customers", EdmNavigationSourceKind.EntitySet)] - [InlineData("VipCustomer/NS.SpecialCustomer", "VipCustomer", EdmNavigationSourceKind.Singleton)] - // coung segment should return null - [InlineData("Customers/$count", null, EdmNavigationSourceKind.None)] - // value segment should return null - [InlineData("Customers(1)/Account/Amount/$value", null, EdmNavigationSourceKind.None)] - // batch segment should return null - [InlineData("$batch", null, EdmNavigationSourceKind.None)] - // metadata segment should return null - [InlineData("$metadata", null, EdmNavigationSourceKind.None)] - public void GetNavigationSource_ReturnsCorrectNavigationSource(string path, string expectedNavigationSource, EdmNavigationSourceKind navigationSourceKind) + else { - var model = new Models.CustomersModelWithInheritance(); - var parser = new ODataUriParser(model.Model, new Uri(path, UriKind.Relative)); - parser.EnableUriTemplateParsing = true; - ODataPath odataPath = parser.ParsePath(); - - var navigationSource = odataPath.GetNavigationSource(); - - if (expectedNavigationSource == null) - { - Assert.Null(navigationSource); - } - else - { - Assert.NotNull(navigationSource); - Assert.Equal(expectedNavigationSource, navigationSource.Name); - Assert.Equal(navigationSourceKind, navigationSource.NavigationSourceKind()); - } + Assert.NotNull(navigationSource); + Assert.Equal(expectedNavigationSource, navigationSource.Name); + Assert.Equal(navigationSourceKind, navigationSource.NavigationSourceKind()); } + } - [Fact] - public void GetNavigationSource_ReturnsNull_ForPathTemplateSegment() - { - var model = new Models.CustomersModelWithInheritance(); - ODataPath path = new ODataPath( - new EntitySetSegment(model.Customers), - new PathTemplateSegment("template") - ); + [Fact] + public void GetNavigationSource_ReturnsNull_ForPathTemplateSegment() + { + var model = new Models.CustomersModelWithInheritance(); + ODataPath path = new ODataPath( + new EntitySetSegment(model.Customers), + new PathTemplateSegment("template") + ); - var navigationSource = path.GetNavigationSource(); + var navigationSource = path.GetNavigationSource(); - Assert.Null(navigationSource); - } + Assert.Null(navigationSource); + } - [Fact] - public void GetNavigationSource_ReturnsNull_ForUnknownPathSegment() - { - var model = new Models.CustomersModelWithInheritance(); - ODataPath path = new ODataPath( - new EntitySetSegment(model.Customers), - new UnknownTestODataPathSegment() - ); + [Fact] + public void GetNavigationSource_ReturnsNull_ForUnknownPathSegment() + { + var model = new Models.CustomersModelWithInheritance(); + ODataPath path = new ODataPath( + new EntitySetSegment(model.Customers), + new UnknownTestODataPathSegment() + ); - var navigationSource = path.GetNavigationSource(); + var navigationSource = path.GetNavigationSource(); - Assert.Null(navigationSource); - } + Assert.Null(navigationSource); + } + + [Fact] + public void GetPathString_ThrowsArgumentNull_Path() + { + // Arrange & Act & Assert + ODataPath path = null; + ExceptionAssert.ThrowsArgumentNull(() => path.GetPathString(), "path"); - [Fact] - public void GetPathString_ThrowsArgumentNull_Path() + // Arrange & Act & Assert + IList segments = null; + ExceptionAssert.ThrowsArgumentNull(() => segments.GetPathString(), "segments"); + } + + [Fact] + public void GetPathString_Returns_Path() + { + // Arrange & Act & Assert + ODataPath path = new ODataPath(MetadataSegment.Instance); + Assert.Equal("$metadata", path.GetPathString()); + + // Arrange & Act & Assert + IList segments = new List { - // Arrange & Act & Assert - ODataPath path = null; - ExceptionAssert.ThrowsArgumentNull(() => path.GetPathString(), "path"); + MetadataSegment.Instance + }; + Assert.Equal("$metadata", segments.GetPathString()); + } - // Arrange & Act & Assert - IList segments = null; - ExceptionAssert.ThrowsArgumentNull(() => segments.GetPathString(), "segments"); - } + /// + /// Test path segment used to test handling of unknown path segments. + /// + class UnknownTestODataPathSegment : ODataPathSegment + { + public override IEdmType EdmType => throw new NotImplementedException(); - [Fact] - public void GetPathString_Returns_Path() + public override void HandleWith(PathSegmentHandler handler) { - // Arrange & Act & Assert - ODataPath path = new ODataPath(MetadataSegment.Instance); - Assert.Equal("$metadata", path.GetPathString()); - - // Arrange & Act & Assert - IList segments = new List - { - MetadataSegment.Instance - }; - Assert.Equal("$metadata", segments.GetPathString()); + handler.Handle(this); } - /// - /// Test path segment used to test handling of unknown path segments. - /// - class UnknownTestODataPathSegment : ODataPathSegment + public override T TranslateWith(PathSegmentTranslator translator) { - public override IEdmType EdmType => throw new NotImplementedException(); - - public override void HandleWith(PathSegmentHandler handler) - { - handler.Handle(this); - } - - public override T TranslateWith(PathSegmentTranslator translator) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/ODataServiceCollectionExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/ODataServiceCollectionExtensionsTests.cs index 573a950bf..5846a3eea 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/ODataServiceCollectionExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/ODataServiceCollectionExtensionsTests.cs @@ -9,18 +9,17 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests +namespace Microsoft.AspNetCore.OData.Tests; + +public class ODataServiceCollectionExtensionsTests { - public class ODataServiceCollectionExtensionsTests + [Fact] + public void AddODataCore_ThrowsArgumentNull_Services() { - [Fact] - public void AddODataCore_ThrowsArgumentNull_Services() - { - // Arrange - IServiceCollection services = null; + // Arrange + IServiceCollection services = null; - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => services.AddODataCore(), "services"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => services.AddODataCore(), "services"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/PublicApiHelper.cs b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/PublicApiHelper.cs index 8e2ee7cfe..fabdde5fa 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/PublicApiHelper.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/PublicApiHelper.cs @@ -20,1131 +20,1130 @@ using System.Runtime.InteropServices; using System.Text; -namespace Microsoft.AspNetCore.OData.Tests.PublicApi +namespace Microsoft.AspNetCore.OData.Tests.PublicApi; + +internal static class PublicApiHelper { - internal static class PublicApiHelper - { - private static bool AlphabeticalGrouping = false; - private static readonly List Assemblies = new List(); + private static bool AlphabeticalGrouping = false; + private static readonly List Assemblies = new List(); - private static Hashtable _synonms; - public static Hashtable Synonms + private static Hashtable _synonms; + public static Hashtable Synonms + { + get { - get + if (_synonms == null) { - if (_synonms == null) - { - _synonms = CreateSynonms(); - } - - return _synonms; + _synonms = CreateSynonms(); } + + return _synonms; } + } - public static void DumpPublicApi(StreamWriter streamWriter, params string[] assemblyNames) + public static void DumpPublicApi(StreamWriter streamWriter, params string[] assemblyNames) + { + IList assemblies = new List(); + for (int k = 0; k < assemblyNames.Length; ++k) { - IList assemblies = new List(); - for (int k = 0; k < assemblyNames.Length; ++k) + try { - try + Assembly assembly; + if (File.Exists(assemblyNames[k])) { - Assembly assembly; - if (File.Exists(assemblyNames[k])) - { - assembly = Assembly.LoadFrom(assemblyNames[k]); - } - else - { - assembly = Assembly.Load(assemblyNames[k]); - } - - assemblies.Add(assembly); + assembly = Assembly.LoadFrom(assemblyNames[k]); } - catch (Exception e) + else { - streamWriter.WriteLine(@"Error loading types from assembly '{0}':", assemblyNames[k]); - streamWriter.WriteLine(e.ToString()); - Environment.Exit(1); + assembly = Assembly.Load(assemblyNames[k]); } - } - DumpPublicApi(streamWriter, assemblies.ToArray()); - } - - public static void DumpPublicApi(StreamWriter streamWriter, params Assembly[] assemblies) - { - Reset(); - - if (assemblies.Length <= 0) - { - return; + assemblies.Add(assembly); } - - ArrayList typesList = new ArrayList(); - foreach (var assembly in assemblies) + catch (Exception e) { - Assemblies.Add(assembly); - typesList.AddRange(assembly.GetTypes()); + streamWriter.WriteLine(@"Error loading types from assembly '{0}':", assemblyNames[k]); + streamWriter.WriteLine(e.ToString()); + Environment.Exit(1); } - - typesList.Sort(TypeCompare.Default); - DumpPublicApiImplementation(streamWriter, typesList); } - private static Hashtable CreateSynonms() + DumpPublicApi(streamWriter, assemblies.ToArray()); + } + + public static void DumpPublicApi(StreamWriter streamWriter, params Assembly[] assemblies) + { + Reset(); + + if (assemblies.Length <= 0) { - Hashtable synonms = new Hashtable(); - - synonms.Add("System.Void", "void"); - synonms.Add("System.Object", "object"); - synonms.Add("System.String", "string"); - synonms.Add("System.Int16", "short"); - synonms.Add("System.Int32", "int"); - synonms.Add("System.Int64", "long"); - synonms.Add("System.Byte", "byte"); - synonms.Add("System.Boolean", "bool"); - synonms.Add("System.Char", "char"); - synonms.Add("System.Decimal", "decimal"); - synonms.Add("System.Double", "double"); - synonms.Add("System.Single", "float"); - - synonms.Add("System.Object[]", "object[]"); - synonms.Add("System.Char[]", "char[]"); - synonms.Add("System.Byte[]", "byte[]"); - synonms.Add("System.Int32[]", "int[]"); - synonms.Add("System.String[]", "string[]"); - - return synonms; + return; } - private static void DumpPublicApiImplementation(StreamWriter streamWriter, ArrayList sortedTypeList) + ArrayList typesList = new ArrayList(); + foreach (var assembly in assemblies) { - StringBuilder builder = new StringBuilder(); - string lastNamespace = ""; - foreach (Type type in sortedTypeList) - { - builder.Length = 0; - - if (type.IsSpecialName) - { - continue; - } - - string typeFullName = type.FullName; - if (typeFullName.StartsWith("")) - { - continue; - } - - Type declaringType = type; - while (null != declaringType) - { - switch (TypeAttributes.VisibilityMask & declaringType.Attributes) - { - case TypeAttributes.Public: - case TypeAttributes.NestedPublic: - case TypeAttributes.NestedFamily: - case TypeAttributes.NestedFamANDAssem: - case TypeAttributes.NestedFamORAssem: - declaringType = declaringType.DeclaringType; - continue; - case TypeAttributes.NotPublic: - case TypeAttributes.NestedPrivate: - case TypeAttributes.NestedAssembly: - Debug.Assert(null != declaringType, "Null declaringType"); - break; - default: - Debug.Assert(false, "Unknown type"); - break; - } - break; - } + Assemblies.Add(assembly); + typesList.AddRange(assembly.GetTypes()); + } - if (typeof(TypeConverter).IsAssignableFrom(type)) - { - ConstructorInfo ctor = type.GetConstructor(BindingFlags.Public | BindingFlags.CreateInstance | BindingFlags.Instance, null, EmptyTypes, EmptyParameterModifiers); - if (null != ctor) - { - streamWriter.WriteLine("{0}", type.FullName); - } - else - { - streamWriter.WriteLine("{0} missing public ctor", type.FullName); - } - } + typesList.Sort(TypeCompare.Default); + DumpPublicApiImplementation(streamWriter, typesList); + } - if (null != declaringType) - { - continue; - } + private static Hashtable CreateSynonms() + { + Hashtable synonms = new Hashtable(); + + synonms.Add("System.Void", "void"); + synonms.Add("System.Object", "object"); + synonms.Add("System.String", "string"); + synonms.Add("System.Int16", "short"); + synonms.Add("System.Int32", "int"); + synonms.Add("System.Int64", "long"); + synonms.Add("System.Byte", "byte"); + synonms.Add("System.Boolean", "bool"); + synonms.Add("System.Char", "char"); + synonms.Add("System.Decimal", "decimal"); + synonms.Add("System.Double", "double"); + synonms.Add("System.Single", "float"); + + synonms.Add("System.Object[]", "object[]"); + synonms.Add("System.Char[]", "char[]"); + synonms.Add("System.Byte[]", "byte[]"); + synonms.Add("System.Int32[]", "int[]"); + synonms.Add("System.String[]", "string[]"); + + return synonms; + } - bool abort = AppendCustomAttributes(builder, type.GetCustomAttributes(false), false, type.IsEnum, true); - if (abort) - { - continue; - } + private static void DumpPublicApiImplementation(StreamWriter streamWriter, ArrayList sortedTypeList) + { + StringBuilder builder = new StringBuilder(); + string lastNamespace = ""; + foreach (Type type in sortedTypeList) + { + builder.Length = 0; + + if (type.IsSpecialName) + { + continue; + } - AppendClassDeclarationApi(builder, type); - builder.Append(" {"); - builder.Append(Environment.NewLine); + string typeFullName = type.FullName; + if (typeFullName.StartsWith("")) + { + continue; + } - string currentNamespace = type.Namespace; - if (lastNamespace != currentNamespace) + Type declaringType = type; + while (null != declaringType) + { + switch (TypeAttributes.VisibilityMask & declaringType.Attributes) { - lastNamespace = currentNamespace; + case TypeAttributes.Public: + case TypeAttributes.NestedPublic: + case TypeAttributes.NestedFamily: + case TypeAttributes.NestedFamANDAssem: + case TypeAttributes.NestedFamORAssem: + declaringType = declaringType.DeclaringType; + continue; + case TypeAttributes.NotPublic: + case TypeAttributes.NestedPrivate: + case TypeAttributes.NestedAssembly: + Debug.Assert(null != declaringType, "Null declaringType"); + break; + default: + Debug.Assert(false, "Unknown type"); + break; } + break; + } - if (type.Name.Contains("UnmappedRequestRoutingConvention")) + if (typeof(TypeConverter).IsAssignableFrom(type)) + { + ConstructorInfo ctor = type.GetConstructor(BindingFlags.Public | BindingFlags.CreateInstance | BindingFlags.Instance, null, EmptyTypes, EmptyParameterModifiers); + if (null != ctor) { - int kk = 0; - kk += 1; + streamWriter.WriteLine("{0}", type.FullName); } - AppendClassMemberApi(builder, type); - if (builder.Length > 0) + else { - AssemblyFilter(builder); - streamWriter.Write(builder.ToString()); - builder.Length = 0; + streamWriter.WriteLine("{0} missing public ctor", type.FullName); } - streamWriter.Write("}"); - streamWriter.Write(Environment.NewLine); - streamWriter.Write(Environment.NewLine); } - } - private static void Reset() - { - _outputFilter = null; - Assemblies.Clear(); - } + if (null != declaringType) + { + continue; + } - private static String[] _outputFilter; - private static void AssemblyFilter(StringBuilder builder) - { - string[] filter = _outputFilter; - if (null == filter) + bool abort = AppendCustomAttributes(builder, type.GetCustomAttributes(false), false, type.IsEnum, true); + if (abort) { - filter = new string[2 + Assemblies.Count]; - filter[0] = ", " + typeof(object).Assembly.ToString(); - filter[1] = ", " + typeof(Uri).Assembly.ToString(); - for (int i = 2; i < filter.Length; i++) - { - filter[i] = ", " + Assemblies[i - 2].ToString(); - } - _outputFilter = filter; + continue; } - for (int i = 0; i < filter.Length; ++i) + + AppendClassDeclarationApi(builder, type); + builder.Append(" {"); + builder.Append(Environment.NewLine); + + string currentNamespace = type.Namespace; + if (lastNamespace != currentNamespace) { - builder.Replace(filter[i], ""); + lastNamespace = currentNamespace; } - } - private static void AppendClassDeclarationApi(StringBuilder builder, Type type) - { - if (type.IsPublic | type.IsNestedPublic) + if (type.Name.Contains("UnmappedRequestRoutingConvention")) { - builder.Append("public "); + int kk = 0; + kk += 1; } - else if (type.IsNestedFamily | type.IsNestedFamORAssem | type.IsNestedFamANDAssem) + AppendClassMemberApi(builder, type); + if (builder.Length > 0) { - builder.Append("protected "); + AssemblyFilter(builder); + streamWriter.Write(builder.ToString()); + builder.Length = 0; } - else + streamWriter.Write("}"); + streamWriter.Write(Environment.NewLine); + streamWriter.Write(Environment.NewLine); + } + } + + private static void Reset() + { + _outputFilter = null; + Assemblies.Clear(); + } + + private static String[] _outputFilter; + private static void AssemblyFilter(StringBuilder builder) + { + string[] filter = _outputFilter; + if (null == filter) + { + filter = new string[2 + Assemblies.Count]; + filter[0] = ", " + typeof(object).Assembly.ToString(); + filter[1] = ", " + typeof(Uri).Assembly.ToString(); + for (int i = 2; i < filter.Length; i++) { - Debug.Assert(false, "non public or protected type"); + filter[i] = ", " + Assemblies[i - 2].ToString(); } + _outputFilter = filter; + } + for (int i = 0; i < filter.Length; ++i) + { + builder.Replace(filter[i], ""); + } + } + + private static void AppendClassDeclarationApi(StringBuilder builder, Type type) + { + if (type.IsPublic | type.IsNestedPublic) + { + builder.Append("public "); + } + else if (type.IsNestedFamily | type.IsNestedFamORAssem | type.IsNestedFamANDAssem) + { + builder.Append("protected "); + } + else + { + Debug.Assert(false, "non public or protected type"); + } - if (type.IsInterface) + if (type.IsInterface) + { + builder.Append("interface "); + } + else if (type.IsEnum) + { + builder.Append("enum "); + } + else if (type.IsValueType) + { + builder.Append("struct "); + } + else if (type.IsClass) + { + if (type.IsSealed) { - builder.Append("interface "); + builder.Append("sealed "); } - else if (type.IsEnum) + else if (type.IsAbstract) { - builder.Append("enum "); + builder.Append("abstract "); } - else if (type.IsValueType) + builder.Append("class "); + } + else + { + builder.Append("? "); + } + builder.Append(type.FullName); + + bool haveColon = false; + Type baseType = type.BaseType; + if ((null != baseType) && (typeof(object) != baseType) && (typeof(ValueType) != baseType)) + { + if (typeof(Enum) == baseType) { - builder.Append("struct "); + baseType = Enum.GetUnderlyingType(type); } - else if (type.IsClass) + haveColon = true; + builder.Append(" : "); + AppendParameterType(builder, baseType); + } + + if (!type.IsEnum) + { + Type[] baseInterfaces = type.GetInterfaces(); + Array.Sort(baseInterfaces, TypeCompare.Default); + foreach (Type baseInterface in baseInterfaces) { - if (type.IsSealed) + if (haveColon) { - builder.Append("sealed "); + builder.Append(", "); } - else if (type.IsAbstract) + else { - builder.Append("abstract "); + haveColon = true; + builder.Append(" : "); } - builder.Append("class "); + builder.Append(baseInterface.Name); } - else + } + } + + private static void AppendClassMemberApi(StringBuilder builder, Type type) + { + MemberInfo[] members = type.GetMembers(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); + if (members.Length <= 0) + { + return; + } + + Array.Sort(members, new MemberCompare(type)); + + bool lastHadAttributes = false; + MemberTypes lastMemberType = 0; + + foreach (MemberInfo info in members) + { + bool rememberLast = lastHadAttributes; + MemberTypes rememberType = lastMemberType; + int startLength = builder.Length; + if (((lastMemberType != info.MemberType) && (0 != lastMemberType)) || lastHadAttributes) { - builder.Append("? "); + builder.Append(Environment.NewLine); + lastHadAttributes = false; } - builder.Append(type.FullName); + lastMemberType = info.MemberType; + int newlineLength = builder.Length; - bool haveColon = false; - Type baseType = type.BaseType; - if ((null != baseType) && (typeof(object) != baseType) && (typeof(ValueType) != baseType)) + bool abort = AppendCustomAttributes(builder, info.GetCustomAttributes(true), true, false, true); + if (abort) { - if (typeof(Enum) == baseType) - { - baseType = Enum.GetUnderlyingType(type); - } - haveColon = true; - builder.Append(" : "); - AppendParameterType(builder, baseType); + builder.Length = startLength; + lastHadAttributes = rememberLast; + lastMemberType = rememberType; + continue; } + lastHadAttributes = (newlineLength != builder.Length); + builder.Append("\t"); + int attributeLength = builder.Length; - if (!type.IsEnum) + switch (info.MemberType) { - Type[] baseInterfaces = type.GetInterfaces(); - Array.Sort(baseInterfaces, TypeCompare.Default); - foreach (Type baseInterface in baseInterfaces) - { - if (haveColon) - { - builder.Append(", "); - } - else - { - haveColon = true; - builder.Append(" : "); - } - builder.Append(baseInterface.Name); - } + case MemberTypes.Constructor: + AppendConstructorInfo(builder, type, info as ConstructorInfo); + break; + case MemberTypes.Event: + AppendEventInfo(builder, type, info as EventInfo); + break; + case MemberTypes.Field: + AppendFieldInfo(builder, type, info as FieldInfo); + break; + case MemberTypes.Method: + AppendMethodInfo(builder, type, info as MethodInfo); + break; + case MemberTypes.Property: + AppendPropertyInfo(builder, type, info as PropertyInfo); + break; + case MemberTypes.NestedType: + //DumpClassAPI(builder, info as Type); + break; + default: + builder.Append(" "); + builder.Append(info.Name); + builder.Append(" "); + break; + } + if (attributeLength == builder.Length) + { + builder.Length = startLength; + lastHadAttributes = rememberLast; + lastMemberType = rememberType; } } + } - private static void AppendClassMemberApi(StringBuilder builder, Type type) + private static bool AppendCustomAttributes(StringBuilder builder, object[] attributes, bool indent, bool isEnum, bool appendNewLine) + { + if (attributes.Length > 0) { - MemberInfo[] members = type.GetMembers(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); - if (members.Length <= 0) + int count = 0; + int startLength = builder.Length; + Array.Sort(attributes, ObjectTypeCompare.Default); + + if (indent) { - return; + builder.Append("\t"); } - - Array.Sort(members, new MemberCompare(type)); - - bool lastHadAttributes = false; - MemberTypes lastMemberType = 0; - - foreach (MemberInfo info in members) + builder.Append("["); + if (appendNewLine) { - bool rememberLast = lastHadAttributes; - MemberTypes rememberType = lastMemberType; - int startLength = builder.Length; - if (((lastMemberType != info.MemberType) && (0 != lastMemberType)) || lastHadAttributes) + builder.Append(Environment.NewLine); + } + foreach (object attribute in attributes) + { + if (attribute is MarshalAsAttribute) { - builder.Append(Environment.NewLine); - lastHadAttributes = false; + continue; } - lastMemberType = info.MemberType; - int newlineLength = builder.Length; - - bool abort = AppendCustomAttributes(builder, info.GetCustomAttributes(true), true, false, true); - if (abort) + if (attribute is StructLayoutAttribute) { - builder.Length = startLength; - lastHadAttributes = rememberLast; - lastMemberType = rememberType; continue; } - lastHadAttributes = (newlineLength != builder.Length); - builder.Append("\t"); - int attributeLength = builder.Length; - - switch (info.MemberType) + if (attribute is CompilerGeneratedAttribute) { - case MemberTypes.Constructor: - AppendConstructorInfo(builder, type, info as ConstructorInfo); - break; - case MemberTypes.Event: - AppendEventInfo(builder, type, info as EventInfo); - break; - case MemberTypes.Field: - AppendFieldInfo(builder, type, info as FieldInfo); - break; - case MemberTypes.Method: - AppendMethodInfo(builder, type, info as MethodInfo); - break; - case MemberTypes.Property: - AppendPropertyInfo(builder, type, info as PropertyInfo); - break; - case MemberTypes.NestedType: - //DumpClassAPI(builder, info as Type); - break; - default: - builder.Append(" "); - builder.Append(info.Name); - builder.Append(" "); - break; + continue; } - if (attributeLength == builder.Length) + if (attribute is MethodImplAttribute) { - builder.Length = startLength; - lastHadAttributes = rememberLast; - lastMemberType = rememberType; + continue; } - } - } - - private static bool AppendCustomAttributes(StringBuilder builder, object[] attributes, bool indent, bool isEnum, bool appendNewLine) - { - if (attributes.Length > 0) - { - int count = 0; - int startLength = builder.Length; - Array.Sort(attributes, ObjectTypeCompare.Default); + if (attribute is TargetedPatchingOptOutAttribute) + { + continue; + } + if (attribute is SuppressMessageAttribute) + { + continue; + } + if (attribute is IteratorStateMachineAttribute) + { + continue; + } + if (attribute is DebuggerStepThroughAttribute) + { + continue; + } + if (isEnum && (attribute is SerializableAttribute)) + { + continue; + } + count++; if (indent) { builder.Append("\t"); } - builder.Append("["); + builder.Append(attribute.GetType().Name); + builder.Append("("); + + builder.Append("),"); if (appendNewLine) { builder.Append(Environment.NewLine); } - foreach (object attribute in attributes) - { - if (attribute is MarshalAsAttribute) - { - continue; - } - if (attribute is StructLayoutAttribute) - { - continue; - } - if (attribute is CompilerGeneratedAttribute) - { - continue; - } - if (attribute is MethodImplAttribute) - { - continue; - } - if (attribute is TargetedPatchingOptOutAttribute) - { - continue; - } - if (attribute is SuppressMessageAttribute) - { - continue; - } - if (attribute is IteratorStateMachineAttribute) - { - continue; - } - if (attribute is DebuggerStepThroughAttribute) - { - continue; - } - if (isEnum && (attribute is SerializableAttribute)) - { - continue; - } - count++; - - if (indent) - { - builder.Append("\t"); - } - builder.Append(attribute.GetType().Name); - builder.Append("("); - - builder.Append("),"); - if (appendNewLine) - { - builder.Append(Environment.NewLine); - } - } - if (0 < count) + } + if (0 < count) + { + if (indent) { - if (indent) - { - builder.Append("\t"); - } - builder.Append("]"); - if (appendNewLine) - { - builder.Append(Environment.NewLine); - } + builder.Append("\t"); } - else + builder.Append("]"); + if (appendNewLine) { - builder.Length = startLength; + builder.Append(Environment.NewLine); } } + else + { + builder.Length = startLength; + } + } - return false; + return false; + } + + private static void AppendConstructorInfo(StringBuilder builder, Type type, ConstructorInfo info) + { + if (info.IsPublic) + { + builder.Append("public"); } + else if (!(type.IsClass && type.IsSealed) && (info.IsFamily || info.IsFamilyAndAssembly || info.IsFamilyOrAssembly)) + { + builder.Append("protected"); + } + else return; - private static void AppendConstructorInfo(StringBuilder builder, Type type, ConstructorInfo info) + builder.Append(" "); + builder.Append(type.Name); + builder.Append(" "); + AppendParameterInfo(builder, info.GetParameters(), true, true); + builder.Append(Environment.NewLine); + } + + private static void AppendEventInfo(StringBuilder builder, Type type, EventInfo info) + { + int propertyStart = builder.Length; + + AppendParameterType(builder, info.EventHandlerType); + builder.Append(" "); + builder.Append(info.Name); + + builder.Append(" {"); + bool gettable = AppendPropertyMethod(builder, type, info.GetAddMethod(), "add"); + bool settable = AppendPropertyMethod(builder, type, info.GetRemoveMethod(), "remove"); + if (gettable || settable) { - if (info.IsPublic) + builder.Append(" }"); + builder.Append(Environment.NewLine); + } + else + { + builder.Length = propertyStart; + } + } + + private static void AppendFieldInfo(StringBuilder builder, Type type, FieldInfo info) + { + if (type.IsEnum && info.IsSpecialName) + { + return; + } + if (info.IsPublic) + { + if (type.IsEnum) + { + builder.Append(""); + } + else { builder.Append("public"); } - else if (!(type.IsClass && type.IsSealed) && (info.IsFamily || info.IsFamilyAndAssembly || info.IsFamilyOrAssembly)) + } + else if (!(type.IsClass && type.IsSealed) && (info.IsFamily || info.IsFamilyAndAssembly || info.IsFamilyOrAssembly)) + { + if (type.IsEnum) + { + builder.Append(""); + } + else { builder.Append("protected"); } - else return; - - builder.Append(" "); - builder.Append(type.Name); - builder.Append(" "); - AppendParameterInfo(builder, info.GetParameters(), true, true); - builder.Append(Environment.NewLine); } + else return; - private static void AppendEventInfo(StringBuilder builder, Type type, EventInfo info) + if (!type.IsEnum) { - int propertyStart = builder.Length; - - AppendParameterType(builder, info.EventHandlerType); - builder.Append(" "); - builder.Append(info.Name); - - builder.Append(" {"); - bool gettable = AppendPropertyMethod(builder, type, info.GetAddMethod(), "add"); - bool settable = AppendPropertyMethod(builder, type, info.GetRemoveMethod(), "remove"); - if (gettable || settable) + if (info.IsStatic) { - builder.Append(" }"); - builder.Append(Environment.NewLine); + builder.Append(" static"); } - else + else if (info.IsInitOnly) + { + builder.Append(" const"); + } + if (info.IsInitOnly) { - builder.Length = propertyStart; + builder.Append(" readonly"); } } + if (!type.IsEnum) + { + builder.Append(" "); + AppendParameterType(builder, info.FieldType); + builder.Append(" "); + } + + builder.Append(info.Name); + builder.Append(" = "); - private static void AppendFieldInfo(StringBuilder builder, Type type, FieldInfo info) + if (info.IsLiteral || info.IsStatic) { - if (type.IsEnum && info.IsSpecialName) + object fieldValue = null; + try { - return; + fieldValue = info.GetValue(null); } - if (info.IsPublic) + catch (Exception) { - if (type.IsEnum) - { - builder.Append(""); - } - else - { - builder.Append("public"); - } } - else if (!(type.IsClass && type.IsSealed) && (info.IsFamily || info.IsFamilyAndAssembly || info.IsFamilyOrAssembly)) + + if (null != fieldValue) { - if (type.IsEnum) + if (fieldValue is string) { - builder.Append(""); + builder.Append('\"'); + builder.Append((string)fieldValue); + builder.Append('\"'); } - else + else if (fieldValue is long) { - builder.Append("protected"); + builder.Append(((long)fieldValue).ToString(CultureInfo.InvariantCulture)); } - } - else return; - - if (!type.IsEnum) - { - if (info.IsStatic) + else if (fieldValue is byte) { - builder.Append(" static"); + builder.Append(((byte)fieldValue).ToString(CultureInfo.InvariantCulture)); } - else if (info.IsInitOnly) + else if (fieldValue is bool) { - builder.Append(" const"); + builder.Append(((bool)fieldValue).ToString(CultureInfo.InvariantCulture)); } - if (info.IsInitOnly) + else if (fieldValue is double) { - builder.Append(" readonly"); + builder.Append(((double)fieldValue).ToString(CultureInfo.InvariantCulture)); } - } - if (!type.IsEnum) - { - builder.Append(" "); - AppendParameterType(builder, info.FieldType); - builder.Append(" "); - } - - builder.Append(info.Name); - builder.Append(" = "); - - if (info.IsLiteral || info.IsStatic) - { - object fieldValue = null; - try + else if (fieldValue is short) { - fieldValue = info.GetValue(null); + builder.Append(((short)fieldValue).ToString(CultureInfo.InvariantCulture)); } - catch (Exception) + else if (fieldValue is float) { + builder.Append(((float)fieldValue).ToString(CultureInfo.InvariantCulture)); } - - if (null != fieldValue) + else if (fieldValue is Guid) { - if (fieldValue is string) - { - builder.Append('\"'); - builder.Append((string)fieldValue); - builder.Append('\"'); - } - else if (fieldValue is long) - { - builder.Append(((long)fieldValue).ToString(CultureInfo.InvariantCulture)); - } - else if (fieldValue is byte) - { - builder.Append(((byte)fieldValue).ToString(CultureInfo.InvariantCulture)); - } - else if (fieldValue is bool) - { - builder.Append(((bool)fieldValue).ToString(CultureInfo.InvariantCulture)); - } - else if (fieldValue is double) - { - builder.Append(((double)fieldValue).ToString(CultureInfo.InvariantCulture)); - } - else if (fieldValue is short) - { - builder.Append(((short)fieldValue).ToString(CultureInfo.InvariantCulture)); - } - else if (fieldValue is float) - { - builder.Append(((float)fieldValue).ToString(CultureInfo.InvariantCulture)); - } - else if (fieldValue is Guid) - { - builder.Append('{'); - builder.Append((Guid)fieldValue); - builder.Append('}'); - } - else if (fieldValue is Enum) - { - // remove the enumness, without assuming a particular underlying type. - builder.Append(Convert.ChangeType(fieldValue, Enum.GetUnderlyingType(type))); - } - else + builder.Append('{'); + builder.Append((Guid)fieldValue); + builder.Append('}'); + } + else if (fieldValue is Enum) + { + // remove the enumness, without assuming a particular underlying type. + builder.Append(Convert.ChangeType(fieldValue, Enum.GetUnderlyingType(type))); + } + else + { + string svalue; + try { - string svalue; - try + MethodInfo tostring = fieldValue.GetType().GetMethod("ToString", ToStringFormatParameter); + if (null != tostring) { - MethodInfo tostring = fieldValue.GetType().GetMethod("ToString", ToStringFormatParameter); - if (null != tostring) - { - svalue = (string)tostring.Invoke(fieldValue, ToSTringFormatValues); - } - else - { - svalue = fieldValue.ToString(); - } + svalue = (string)tostring.Invoke(fieldValue, ToSTringFormatValues); } - catch (Exception e) + else { - svalue = e.ToString(); + svalue = fieldValue.ToString(); } - builder.Append(svalue); } + catch (Exception e) + { + svalue = e.ToString(); + } + builder.Append(svalue); } } - builder.Append(Environment.NewLine); } + builder.Append(Environment.NewLine); + } - private static readonly Type[] EmptyTypes = new Type[0]; - private static readonly ParameterModifier[] EmptyParameterModifiers = new ParameterModifier[0]; + private static readonly Type[] EmptyTypes = new Type[0]; + private static readonly ParameterModifier[] EmptyParameterModifiers = new ParameterModifier[0]; - private static readonly Type[] ToStringFormatParameter = new Type[] { typeof(IFormatProvider) }; - private static readonly object[] ToSTringFormatValues = new object[] { CultureInfo.InvariantCulture }; + private static readonly Type[] ToStringFormatParameter = new Type[] { typeof(IFormatProvider) }; + private static readonly object[] ToSTringFormatValues = new object[] { CultureInfo.InvariantCulture }; - private static void AppendMethodInfo(StringBuilder builder, Type type, MethodInfo info) + private static void AppendMethodInfo(StringBuilder builder, Type type, MethodInfo info) + { + string infoName = info.Name; + if ("IsRowOptimized" == infoName) + { + return; + } + if (info.IsSpecialName) { - string infoName = info.Name; - if ("IsRowOptimized" == infoName) + return; + } + if (info.IsPublic) + { + if (!type.IsInterface) { - return; + builder.Append("public "); } - if (info.IsSpecialName) + } + else if (!(type.IsClass && type.IsSealed) && (info.IsFamily || info.IsFamilyAndAssembly || info.IsFamilyOrAssembly)) + { + if (!type.IsInterface) { - return; + builder.Append("protected "); } - if (info.IsPublic) + } + else if (infoName.StartsWith("Reset") && ("Reset" != infoName)) + { + PropertyInfo propInfo = type.GetProperty(infoName.Substring("Reset".Length), BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty); + if (null != propInfo && (0 == info.GetParameters().Length)) { - if (!type.IsInterface) - { - builder.Append("public "); - } + builder.Append("private "); } - else if (!(type.IsClass && type.IsSealed) && (info.IsFamily || info.IsFamilyAndAssembly || info.IsFamilyOrAssembly)) + else return; + } + else if (infoName.StartsWith("ShouldSerialize") && ("ShouldSerialize" != infoName)) + { + PropertyInfo propInfo = type.GetProperty(infoName.Substring("ShouldSerialize".Length), BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty); + if (null != propInfo && (0 == info.GetParameters().Length)) { - if (!type.IsInterface) - { - builder.Append("protected "); - } + builder.Append("private "); } - else if (infoName.StartsWith("Reset") && ("Reset" != infoName)) + else return; + } + else if (!(type.IsClass && type.IsSealed) && info.IsVirtual) + { + if (-1 == info.Name.IndexOf(".")) { - PropertyInfo propInfo = type.GetProperty(infoName.Substring("Reset".Length), BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty); - if (null != propInfo && (0 == info.GetParameters().Length)) - { - builder.Append("private "); - } - else return; + builder.Append("internal "); } - else if (infoName.StartsWith("ShouldSerialize") && ("ShouldSerialize" != infoName)) + } + else return; + + if (!type.IsInterface) + { + if (info.IsAbstract) { - PropertyInfo propInfo = type.GetProperty(infoName.Substring("ShouldSerialize".Length), BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty); - if (null != propInfo && (0 == info.GetParameters().Length)) - { - builder.Append("private "); - } - else return; + builder.Append("abstract "); } - else if (!(type.IsClass && type.IsSealed) && info.IsVirtual) + else if (info.IsVirtual && (-1 == info.Name.IndexOf("."))) { - if (-1 == info.Name.IndexOf(".")) - { - builder.Append("internal "); - } + builder.Append("virtual "); } - else return; - - if (!type.IsInterface) + else if (info.IsStatic) { - if (info.IsAbstract) - { - builder.Append("abstract "); - } - else if (info.IsVirtual && (-1 == info.Name.IndexOf("."))) - { - builder.Append("virtual "); - } - else if (info.IsStatic) - { - builder.Append("static "); - } + builder.Append("static "); } + } - AppendParameterType(builder, info.ReturnType); - builder.Append(" "); - builder.Append(infoName); - builder.Append(" "); - AppendParameterInfo(builder, info.GetParameters(), true, true); + AppendParameterType(builder, info.ReturnType); + builder.Append(" "); + builder.Append(infoName); + builder.Append(" "); + AppendParameterInfo(builder, info.GetParameters(), true, true); + builder.Append(Environment.NewLine); + } + + private static void AppendPropertyInfo(StringBuilder builder, Type type, PropertyInfo info) + { + int propertyStart = builder.Length; + + builder.Append(""); + AppendParameterType(builder, info.PropertyType); + builder.Append(" "); + builder.Append(info.Name); + builder.Append(" "); + + ParameterInfo[] parameters = info.GetIndexParameters(); + if (0 < parameters.Length) + { + AppendParameterInfo(builder, parameters, false, true); + } + + builder.Append(" { "); + bool gettable = AppendPropertyMethod(builder, type, info.GetGetMethod(true), "get"); + if (gettable) + { + builder.Append(' '); + } + bool settable = AppendPropertyMethod(builder, type, info.GetSetMethod(true), "set"); + if (settable) + { + builder.Append(' '); + } + if (gettable || settable) + { + builder.Append("}"); builder.Append(Environment.NewLine); } + else + { + builder.Length = propertyStart; + } + } - private static void AppendPropertyInfo(StringBuilder builder, Type type, PropertyInfo info) + private static bool AppendPropertyMethod(StringBuilder builder, Type type, MethodInfo info, string method) + { + if (null != info) { - int propertyStart = builder.Length; + int setStart = builder.Length; - builder.Append(""); - AppendParameterType(builder, info.PropertyType); - builder.Append(" "); - builder.Append(info.Name); - builder.Append(" "); + AppendCustomAttributes(builder, info.GetCustomAttributes(true), false, false, false); - ParameterInfo[] parameters = info.GetIndexParameters(); - if (0 < parameters.Length) + if (info.IsPublic) + { + builder.Append("public "); + } + else if (!(type.IsClass && type.IsSealed) && (info.IsFamily || info.IsFamilyAndAssembly || info.IsFamilyOrAssembly)) { - AppendParameterInfo(builder, parameters, false, true); + builder.Append("protected "); } - - builder.Append(" { "); - bool gettable = AppendPropertyMethod(builder, type, info.GetGetMethod(true), "get"); - if (gettable) + else { - builder.Append(' '); + builder.Length = setStart; + return false; } - bool settable = AppendPropertyMethod(builder, type, info.GetSetMethod(true), "set"); - if (settable) + if (info.IsAbstract) { - builder.Append(' '); + builder.Append("abstract "); } - if (gettable || settable) + else if (info.IsVirtual) { - builder.Append("}"); - builder.Append(Environment.NewLine); + builder.Append("virtual "); } - else + else if (info.IsStatic) { - builder.Length = propertyStart; + builder.Append("static "); } + builder.Append(method); + builder.Append(';'); + return true; } - private static bool AppendPropertyMethod(StringBuilder builder, Type type, MethodInfo info, string method) + return false; + } + + private static void AppendParameterInfo(StringBuilder builder, ParameterInfo[] parameters, bool asMethod, bool withNames) + { + if (parameters.Length > 0) { - if (null != info) + builder.Append(asMethod ? '(' : '['); + for (int i = 0; i < parameters.Length; ++i) { - int setStart = builder.Length; - - AppendCustomAttributes(builder, info.GetCustomAttributes(true), false, false, false); - - if (info.IsPublic) + if (0 < i) { - builder.Append("public "); + builder.Append(", "); } - else if (!(type.IsClass && type.IsSealed) && (info.IsFamily || info.IsFamilyAndAssembly || info.IsFamilyOrAssembly)) + if (withNames) { - builder.Append("protected "); + AppendParameterInfo(builder, parameters[i]); } else { - builder.Length = setStart; - return false; - } - if (info.IsAbstract) - { - builder.Append("abstract "); - } - else if (info.IsVirtual) - { - builder.Append("virtual "); - } - else if (info.IsStatic) - { - builder.Append("static "); + builder.Append(parameters[i].ParameterType.FullName); } - builder.Append(method); - builder.Append(';'); - return true; } - return false; + builder.Append(asMethod ? ')' : ']'); } - - private static void AppendParameterInfo(StringBuilder builder, ParameterInfo[] parameters, bool asMethod, bool withNames) + else { - if (parameters.Length > 0) - { - builder.Append(asMethod ? '(' : '['); - for (int i = 0; i < parameters.Length; ++i) - { - if (0 < i) - { - builder.Append(", "); - } - if (withNames) - { - AppendParameterInfo(builder, parameters[i]); - } - else - { - builder.Append(parameters[i].ParameterType.FullName); - } - } - - builder.Append(asMethod ? ')' : ']'); - } - else - { - builder.Append("()"); - } + builder.Append("()"); } + } - private static void AppendParameterInfo(StringBuilder builder, ParameterInfo info) + private static void AppendParameterInfo(StringBuilder builder, ParameterInfo info) + { + if (info.IsOut) { - if (info.IsOut) - { - builder.Append("out "); - } - else if (info.IsOptional) - { - builder.Append("params "); - } - AppendParameterType(builder, info.ParameterType); - builder.Append(" "); - builder.Append(info.Name); + builder.Append("out "); + } + else if (info.IsOptional) + { + builder.Append("params "); } + AppendParameterType(builder, info.ParameterType); + builder.Append(" "); + builder.Append(info.Name); + } - private static void AppendParameterType(StringBuilder builder, Type parameterType) + private static void AppendParameterType(StringBuilder builder, Type parameterType) + { + string name = parameterType.FullName ?? parameterType.Name; + string synonm = (string)Synonms[name]; + if (null != synonm) { - string name = parameterType.FullName ?? parameterType.Name; - string synonm = (string)Synonms[name]; - if (null != synonm) - { - builder.Append(synonm); - } - else if (parameterType.IsGenericType && name.Contains("Version=")) - { - // If there is generic type with generic parameter (for e.g. IEnumerable), - // then AppendGenericTypeName produces 'System.IEnumerable[[]]' whereas - // type.Name is IEnumerable'1. Also, to avoid too any changes with the existing baseline, - // only going into this method if there is a "Version=" present in the name. - AppendGenericTypeName(builder, parameterType); - } - //else if (name.StartsWith("Microsoft.AspNetCore.OData.")) - //{ - // builder.Append(parameterType.Name); - //} - else - { - builder.Append(name); - } + builder.Append(synonm); + } + else if (parameterType.IsGenericType && name.Contains("Version=")) + { + // If there is generic type with generic parameter (for e.g. IEnumerable), + // then AppendGenericTypeName produces 'System.IEnumerable[[]]' whereas + // type.Name is IEnumerable'1. Also, to avoid too any changes with the existing baseline, + // only going into this method if there is a "Version=" present in the name. + AppendGenericTypeName(builder, parameterType); + } + //else if (name.StartsWith("Microsoft.AspNetCore.OData.")) + //{ + // builder.Append(parameterType.Name); + //} + else + { + builder.Append(name); } + } - private static void AppendGenericTypeName(StringBuilder builder, Type type) + private static void AppendGenericTypeName(StringBuilder builder, Type type) + { + if (type.IsGenericType) { - if (type.IsGenericType) + builder.Append(type.GetGenericTypeDefinition().FullName); + builder.Append("["); + bool first = true; + foreach (var argType in type.GetGenericArguments()) { - builder.Append(type.GetGenericTypeDefinition().FullName); - builder.Append("["); - bool first = true; - foreach (var argType in type.GetGenericArguments()) + if (!first) { - if (!first) - { - builder.Append(","); - } - builder.Append("["); - AppendGenericTypeName(builder, argType); - builder.Append("]"); - first = false; + builder.Append(","); } - + builder.Append("["); + AppendGenericTypeName(builder, argType); builder.Append("]"); + first = false; } - else - { - builder.Append(type.FullName); - } + + builder.Append("]"); + } + else + { + builder.Append(type.FullName); } + } - public sealed class AssemblyCompare : IComparer + public sealed class AssemblyCompare : IComparer + { + public int Compare(object x, object y) { - public int Compare(object x, object y) - { - string a = ((Assembly)x).GetName().Name; - string b = ((Assembly)y).GetName().Name; - int ac = 0, bc = 0; + string a = ((Assembly)x).GetName().Name; + string b = ((Assembly)y).GetName().Name; + int ac = 0, bc = 0; - for (int i = 0; i < a.Length; ++i) - { - if ('.' == a[i]) ac++; - } - for (int i = 0; i < b.Length; ++i) - { - if ('.' == b[i]) bc++; - } - int cmp = ac - bc; - if (0 == cmp) - { - cmp = String.Compare(a, b, StringComparison.Ordinal); - } - return cmp; + for (int i = 0; i < a.Length; ++i) + { + if ('.' == a[i]) ac++; + } + for (int i = 0; i < b.Length; ++i) + { + if ('.' == b[i]) bc++; } + int cmp = ac - bc; + if (0 == cmp) + { + cmp = String.Compare(a, b, StringComparison.Ordinal); + } + return cmp; } + } - public sealed class TypeCompare : IComparer - { - public static readonly TypeCompare Default = new TypeCompare(); + public sealed class TypeCompare : IComparer + { + public static readonly TypeCompare Default = new TypeCompare(); - public int Compare(object x, object y) - { - Type a = x as Type; - Type b = y as Type; + public int Compare(object x, object y) + { + Type a = x as Type; + Type b = y as Type; - string c = a.FullName ?? a.Name; - string d = b.FullName ?? b.Name; + string c = a.FullName ?? a.Name; + string d = b.FullName ?? b.Name; - int ac = 0, bc = 0; + int ac = 0, bc = 0; - for (int i = 0; i < c.Length; ++i) - { - if ('.' == c[i]) ac++; - } - for (int i = 0; i < d.Length; ++i) - { - if ('.' == d[i]) bc++; - } - int cmp = ac - bc; - if (0 == cmp) + for (int i = 0; i < c.Length; ++i) + { + if ('.' == c[i]) ac++; + } + for (int i = 0; i < d.Length; ++i) + { + if ('.' == d[i]) bc++; + } + int cmp = ac - bc; + if (0 == cmp) + { + if (!AlphabeticalGrouping) { - if (!AlphabeticalGrouping) - { - string e = (0 < ac) ? c.Substring(0, c.LastIndexOf('.')) : null; - string f = (0 < bc) ? d.Substring(0, d.LastIndexOf('.')) : null; + string e = (0 < ac) ? c.Substring(0, c.LastIndexOf('.')) : null; + string f = (0 < bc) ? d.Substring(0, d.LastIndexOf('.')) : null; - if (0 == String.Compare(e, f, false, CultureInfo.InvariantCulture)) + if (0 == String.Compare(e, f, false, CultureInfo.InvariantCulture)) + { + if (a.IsEnum) { - if (a.IsEnum) + if (!b.IsEnum) { - if (!b.IsEnum) - { - cmp = -1; - } + cmp = -1; } - else if (a.IsValueType) + } + else if (a.IsValueType) + { + if (b.IsEnum) { - if (b.IsEnum) - { - cmp = 1; - } - else if (!b.IsValueType) - { - cmp = -1; - } + cmp = 1; } - else if (b.IsEnum || b.IsValueType) + else if (!b.IsValueType) { - cmp = 1; + cmp = -1; + } + } + else if (b.IsEnum || b.IsValueType) + { + cmp = 1; + } + if (0 == cmp) + { + if (a.IsInterface != b.IsInterface) + { + cmp = (a.IsInterface ? -1 : 1); + } + else if (a.IsAbstract != b.IsAbstract) + { + cmp = (a.IsAbstract ? -1 : 1); } - if (0 == cmp) + else if (a.IsSealed != b.IsSealed) { - if (a.IsInterface != b.IsInterface) - { - cmp = (a.IsInterface ? -1 : 1); - } - else if (a.IsAbstract != b.IsAbstract) - { - cmp = (a.IsAbstract ? -1 : 1); - } - else if (a.IsSealed != b.IsSealed) - { - cmp = (a.IsSealed ? 1 : -1); - } + cmp = (a.IsSealed ? 1 : -1); } } } - if (0 == cmp) - { - cmp = String.Compare(c, d, false, CultureInfo.InvariantCulture); - } } - return cmp; + if (0 == cmp) + { + cmp = String.Compare(c, d, false, CultureInfo.InvariantCulture); + } } + return cmp; } + } - public sealed class ObjectTypeCompare : IComparer + public sealed class ObjectTypeCompare : IComparer + { + public static readonly ObjectTypeCompare Default = new ObjectTypeCompare(); + public int Compare(object x, object y) { - public static readonly ObjectTypeCompare Default = new ObjectTypeCompare(); - public int Compare(object x, object y) - { - string a = x.GetType().FullName; - string b = y.GetType().FullName; - int ac = 0, bc = 0; + string a = x.GetType().FullName; + string b = y.GetType().FullName; + int ac = 0, bc = 0; - for (int i = 0; i < a.Length; ++i) - { - if ('.' == a[i]) ac++; - } - for (int i = 0; i < b.Length; ++i) - { - if ('.' == b[i]) bc++; - } - int cmp = ac - bc; - if (0 == cmp) - { - cmp = String.Compare(a, b, false, CultureInfo.InvariantCulture); - } - return cmp; + for (int i = 0; i < a.Length; ++i) + { + if ('.' == a[i]) ac++; + } + for (int i = 0; i < b.Length; ++i) + { + if ('.' == b[i]) bc++; + } + int cmp = ac - bc; + if (0 == cmp) + { + cmp = String.Compare(a, b, false, CultureInfo.InvariantCulture); } + return cmp; + } + } + + public sealed class MemberCompare : IComparer + { + private static readonly Hashtable MemberType; + static MemberCompare() + { + Hashtable memberType = new Hashtable(); + memberType.Add(MemberTypes.Field, 1); + memberType.Add(MemberTypes.Constructor, 2); + memberType.Add(MemberTypes.Property, 3); + memberType.Add(MemberTypes.Event, 4); + memberType.Add(MemberTypes.Method, 5); + memberType.Add(MemberTypes.NestedType, 6); + memberType.Add(MemberTypes.TypeInfo, 7); + memberType.Add(MemberTypes.Custom, 8); + MemberType = memberType; } - public sealed class MemberCompare : IComparer + private readonly Hashtable _hash; + + public MemberCompare(Type type) { - private static readonly Hashtable MemberType; - static MemberCompare() + _hash = new Hashtable(); + for (int i = 0; null != type; ++i, type = type.BaseType) { - Hashtable memberType = new Hashtable(); - memberType.Add(MemberTypes.Field, 1); - memberType.Add(MemberTypes.Constructor, 2); - memberType.Add(MemberTypes.Property, 3); - memberType.Add(MemberTypes.Event, 4); - memberType.Add(MemberTypes.Method, 5); - memberType.Add(MemberTypes.NestedType, 6); - memberType.Add(MemberTypes.TypeInfo, 7); - memberType.Add(MemberTypes.Custom, 8); - MemberType = memberType; + _hash.Add(type, i); } + } - private readonly Hashtable _hash; + public int Compare(object x, object y) + { + return Compare((MemberInfo)x, (MemberInfo)y); + } - public MemberCompare(Type type) + public int Compare(MemberInfo x, MemberInfo y) + { + if (x.MemberType == y.MemberType) { - _hash = new Hashtable(); - for (int i = 0; null != type; ++i, type = type.BaseType) + Type xt = x.DeclaringType; + Type yt = y.DeclaringType; + if (xt != yt) { - _hash.Add(type, i); + return (int)_hash[yt] - (int)_hash[xt]; } - } - - public int Compare(object x, object y) - { - return Compare((MemberInfo)x, (MemberInfo)y); - } - public int Compare(MemberInfo x, MemberInfo y) - { - if (x.MemberType == y.MemberType) + int cmp = String.Compare(x.Name, y.Name, false, CultureInfo.InvariantCulture); + if (0 == cmp) { - Type xt = x.DeclaringType; - Type yt = y.DeclaringType; - if (xt != yt) + MethodInfo xMethodInfo = null, yMethodInfo = null; + ParameterInfo[] xParameterInfos, yParameterInfos; + switch (x.MemberType) { - return (int)_hash[yt] - (int)_hash[xt]; + case MemberTypes.Constructor: + xParameterInfos = ((ConstructorInfo)x).GetParameters(); + yParameterInfos = ((ConstructorInfo)y).GetParameters(); + break; + case MemberTypes.Method: + xMethodInfo = (MethodInfo)x; + yMethodInfo = (MethodInfo)y; + xParameterInfos = xMethodInfo.GetParameters(); + yParameterInfos = yMethodInfo.GetParameters(); + break; + case MemberTypes.Property: + xParameterInfos = ((PropertyInfo)x).GetIndexParameters(); + yParameterInfos = ((PropertyInfo)y).GetIndexParameters(); + break; + default: + xParameterInfos = yParameterInfos = new ParameterInfo[0]; + break; } - - int cmp = String.Compare(x.Name, y.Name, false, CultureInfo.InvariantCulture); + cmp = xParameterInfos.Length - yParameterInfos.Length; if (0 == cmp) { - MethodInfo xMethodInfo = null, yMethodInfo = null; - ParameterInfo[] xParameterInfos, yParameterInfos; - switch (x.MemberType) - { - case MemberTypes.Constructor: - xParameterInfos = ((ConstructorInfo)x).GetParameters(); - yParameterInfos = ((ConstructorInfo)y).GetParameters(); - break; - case MemberTypes.Method: - xMethodInfo = (MethodInfo)x; - yMethodInfo = (MethodInfo)y; - xParameterInfos = xMethodInfo.GetParameters(); - yParameterInfos = yMethodInfo.GetParameters(); - break; - case MemberTypes.Property: - xParameterInfos = ((PropertyInfo)x).GetIndexParameters(); - yParameterInfos = ((PropertyInfo)y).GetIndexParameters(); - break; - default: - xParameterInfos = yParameterInfos = new ParameterInfo[0]; - break; - } - cmp = xParameterInfos.Length - yParameterInfos.Length; - if (0 == cmp) + int count = xParameterInfos.Length; + for (int i = 0; i < count; ++i) { - int count = xParameterInfos.Length; - for (int i = 0; i < count; ++i) + cmp = String.Compare(xParameterInfos[i].ParameterType.FullName, yParameterInfos[i].ParameterType.FullName, false, CultureInfo.InvariantCulture); + if (cmp == 0) { - cmp = String.Compare(xParameterInfos[i].ParameterType.FullName, yParameterInfos[i].ParameterType.FullName, false, CultureInfo.InvariantCulture); - if (cmp == 0) - { - // For generic parameters, FullName is null. Hence comparing the names - cmp = String.Compare(xParameterInfos[i].ParameterType.Name, yParameterInfos[i].ParameterType.Name, false, CultureInfo.InvariantCulture); - } - if (0 != cmp) - { - break; - } + // For generic parameters, FullName is null. Hence comparing the names + cmp = String.Compare(xParameterInfos[i].ParameterType.Name, yParameterInfos[i].ParameterType.Name, false, CultureInfo.InvariantCulture); } - - if (0 == cmp && xMethodInfo != null) + if (0 != cmp) { - // Two methods with same name, same parameters. Sort by the # of generic type parameters. - cmp = xMethodInfo.GetGenericArguments().Count() - yMethodInfo.GetGenericArguments().Count(); + break; } } - } - return cmp; + if (0 == cmp && xMethodInfo != null) + { + // Two methods with same name, same parameters. Sort by the # of generic type parameters. + cmp = xMethodInfo.GetGenericArguments().Count() - yMethodInfo.GetGenericArguments().Count(); + } + } } - return ((int)MemberType[x.MemberType] - (int)MemberType[y.MemberType]); + + return cmp; } + return ((int)MemberType[x.MemberType] - (int)MemberType[y.MemberType]); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/PublicApiTest.cs b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/PublicApiTest.cs index 94b1c96e4..f4a8dc17e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/PublicApiTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/PublicApiTest.cs @@ -9,65 +9,64 @@ using System.IO; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.PublicApi +namespace Microsoft.AspNetCore.OData.Tests.PublicApi; + +public partial class PublicApiTest { - public partial class PublicApiTest - { - private const string AssemblyName = "Microsoft.AspNetCore.OData.dll"; - private const string OutputFileName = "Microsoft.AspNetCore.OData.PublicApi.out"; + private const string AssemblyName = "Microsoft.AspNetCore.OData.dll"; + private const string OutputFileName = "Microsoft.AspNetCore.OData.PublicApi.out"; - private const string BaseLineFileName = "Microsoft.AspNetCore.OData.PublicApi.Net8.bsl"; + private const string BaseLineFileName = "Microsoft.AspNetCore.OData.PublicApi.Net8.bsl"; - private const string BaseLineFileFolder = @"test\Microsoft.AspNetCore.OData.Tests\PublicApi\"; + private const string BaseLineFileFolder = @"test\Microsoft.AspNetCore.OData.Tests\PublicApi\"; - [Fact] - public void TestPublicApi() - { - // Arrange - string outputPath = Environment.CurrentDirectory; - string outputFile = outputPath + Path.DirectorySeparatorChar + OutputFileName; + [Fact] + public void TestPublicApi() + { + // Arrange + string outputPath = Environment.CurrentDirectory; + string outputFile = outputPath + Path.DirectorySeparatorChar + OutputFileName; - // Act - using (FileStream fs = new FileStream(outputFile, FileMode.Create)) + // Act + using (FileStream fs = new FileStream(outputFile, FileMode.Create)) + { + using (StreamWriter sw = new StreamWriter(fs)) { - using (StreamWriter sw = new StreamWriter(fs)) - { - string assemblyPath = outputPath + Path.DirectorySeparatorChar + AssemblyName; - Assert.True(File.Exists(assemblyPath), string.Format("{0} does not exist in current directory", assemblyPath)); - PublicApiHelper.DumpPublicApi(sw, assemblyPath); - } + string assemblyPath = outputPath + Path.DirectorySeparatorChar + AssemblyName; + Assert.True(File.Exists(assemblyPath), string.Format("{0} does not exist in current directory", assemblyPath)); + PublicApiHelper.DumpPublicApi(sw, assemblyPath); } - string outputString = File.ReadAllText(outputFile); - string baselineString = GetBaseLineString(); - - // Assert - Assert.True(String.Compare(baselineString, outputString, StringComparison.Ordinal) == 0, - String.Format("Base line file '{1}' and output file '{2}' do not match, please check.'{0}'" + - "To update the baseline, please run:{0}{0}" + - "copy /y \"{2}\" \"{1}\"", Environment.NewLine, - BaseLineFileFolder + BaseLineFileName, - outputFile)); } + string outputString = File.ReadAllText(outputFile); + string baselineString = GetBaseLineString(); - private string GetBaseLineString() - { - const string projectDefaultNamespace = "Microsoft.AspNetCore.OData.Tests"; - const string resourcesFolderName = "PublicApi"; - const string pathSeparator = "."; - string path = projectDefaultNamespace + pathSeparator + resourcesFolderName + pathSeparator + BaseLineFileName; + // Assert + Assert.True(String.Compare(baselineString, outputString, StringComparison.Ordinal) == 0, + String.Format("Base line file '{1}' and output file '{2}' do not match, please check.'{0}'" + + "To update the baseline, please run:{0}{0}" + + "copy /y \"{2}\" \"{1}\"", Environment.NewLine, + BaseLineFileFolder + BaseLineFileName, + outputFile)); + } + + private string GetBaseLineString() + { + const string projectDefaultNamespace = "Microsoft.AspNetCore.OData.Tests"; + const string resourcesFolderName = "PublicApi"; + const string pathSeparator = "."; + string path = projectDefaultNamespace + pathSeparator + resourcesFolderName + pathSeparator + BaseLineFileName; - using (Stream stream = typeof(PublicApiTest).Assembly.GetManifestResourceStream(path)) + using (Stream stream = typeof(PublicApiTest).Assembly.GetManifestResourceStream(path)) + { + if (stream == null) { - if (stream == null) - { - string message = Error.Format("The embedded resource '{0}' was not found.", path); - throw new FileNotFoundException(message, path); - } + string message = Error.Format("The embedded resource '{0}' was not found.", path); + throw new FileNotFoundException(message, path); + } - using (TextReader reader = new StreamReader(stream)) - { - return reader.ReadToEnd(); - } + using (TextReader reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedArithmeticOperatorsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedArithmeticOperatorsTests.cs index e9fb8a194..98894a979 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedArithmeticOperatorsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedArithmeticOperatorsTests.cs @@ -9,29 +9,28 @@ using Microsoft.AspNetCore.OData.Query; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class AllowedArithmeticOperatorsTests { - public class AllowedArithmeticOperatorsTests + [Fact] + public void None_MatchesNone() { - [Fact] - public void None_MatchesNone() - { - Assert.Equal(AllowedArithmeticOperators.None, AllowedArithmeticOperators.All & AllowedArithmeticOperators.None); - } + Assert.Equal(AllowedArithmeticOperators.None, AllowedArithmeticOperators.All & AllowedArithmeticOperators.None); + } - [Fact] - public void All_Contains_AllArithmeticOperators() + [Fact] + public void All_Contains_AllArithmeticOperators() + { + AllowedArithmeticOperators allArithmeticOperators = 0; + foreach (AllowedArithmeticOperators allowedArithmeticOperator in Enum.GetValues(typeof(AllowedArithmeticOperators))) { - AllowedArithmeticOperators allArithmeticOperators = 0; - foreach (AllowedArithmeticOperators allowedArithmeticOperator in Enum.GetValues(typeof(AllowedArithmeticOperators))) + if (allowedArithmeticOperator != AllowedArithmeticOperators.All) { - if (allowedArithmeticOperator != AllowedArithmeticOperators.All) - { - allArithmeticOperators |= allowedArithmeticOperator; - } + allArithmeticOperators |= allowedArithmeticOperator; } - - Assert.Equal(AllowedArithmeticOperators.All, allArithmeticOperators); } + + Assert.Equal(AllowedArithmeticOperators.All, allArithmeticOperators); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedFunctionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedFunctionsTests.cs index 02e43df86..507b72d01 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedFunctionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedFunctionsTests.cs @@ -12,160 +12,159 @@ using Microsoft.AspNetCore.OData.TestCommon; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class AllowedFunctionsTests { - public class AllowedFunctionsTests + public static TheoryDataSet AllStringFunctionsData { - public static TheoryDataSet AllStringFunctionsData + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - AllowedFunctions.Concat, - AllowedFunctions.IndexOf, - AllowedFunctions.Length, - AllowedFunctions.StartsWith, - AllowedFunctions.EndsWith, - AllowedFunctions.Substring, - AllowedFunctions.Contains, - AllowedFunctions.ToLower, - AllowedFunctions.ToUpper, - AllowedFunctions.Trim, - AllowedFunctions.MatchesPattern, - }; - } + AllowedFunctions.Concat, + AllowedFunctions.IndexOf, + AllowedFunctions.Length, + AllowedFunctions.StartsWith, + AllowedFunctions.EndsWith, + AllowedFunctions.Substring, + AllowedFunctions.Contains, + AllowedFunctions.ToLower, + AllowedFunctions.ToUpper, + AllowedFunctions.Trim, + AllowedFunctions.MatchesPattern, + }; } + } - public static IEnumerable AllNonStringFunctionsData + public static IEnumerable AllNonStringFunctionsData + { + get { - get - { - return - new List(Enum.GetValues(typeof(AllowedFunctions)) as AllowedFunctions[]) - .Except(new[] { AllowedFunctions.AllFunctions, AllowedFunctions.AllStringFunctions }) - .Except(AllStringFunctionsData.Select(t => (AllowedFunctions)t[0])) - .Select(t => new object[] { t }); - } + return + new List(Enum.GetValues(typeof(AllowedFunctions)) as AllowedFunctions[]) + .Except(new[] { AllowedFunctions.AllFunctions, AllowedFunctions.AllStringFunctions }) + .Except(AllStringFunctionsData.Select(t => (AllowedFunctions)t[0])) + .Select(t => new object[] { t }); } + } - public static TheoryDataSet AllDateTimeFunctionsData + public static TheoryDataSet AllDateTimeFunctionsData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - AllowedFunctions.Year, - AllowedFunctions.Month, - AllowedFunctions.Day, - AllowedFunctions.Hour, - AllowedFunctions.Minute, - AllowedFunctions.Second, - AllowedFunctions.FractionalSeconds, - AllowedFunctions.Date, - AllowedFunctions.Time, - }; - } + AllowedFunctions.Year, + AllowedFunctions.Month, + AllowedFunctions.Day, + AllowedFunctions.Hour, + AllowedFunctions.Minute, + AllowedFunctions.Second, + AllowedFunctions.FractionalSeconds, + AllowedFunctions.Date, + AllowedFunctions.Time, + }; } + } - public static IEnumerable AllNonDateTimeFunctionsData + public static IEnumerable AllNonDateTimeFunctionsData + { + get { - get - { - return - new List(Enum.GetValues(typeof(AllowedFunctions)) as AllowedFunctions[]) - .Except(new[] { AllowedFunctions.AllFunctions, AllowedFunctions.AllDateTimeFunctions }) - .Except(AllDateTimeFunctionsData.Select(t => (AllowedFunctions)t[0])) - .Select(t => new object[] { t }); - } + return + new List(Enum.GetValues(typeof(AllowedFunctions)) as AllowedFunctions[]) + .Except(new[] { AllowedFunctions.AllFunctions, AllowedFunctions.AllDateTimeFunctions }) + .Except(AllDateTimeFunctionsData.Select(t => (AllowedFunctions)t[0])) + .Select(t => new object[] { t }); } + } - public static TheoryDataSet AllMathFunctionsData + public static TheoryDataSet AllMathFunctionsData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - AllowedFunctions.Ceiling, - AllowedFunctions.Round, - AllowedFunctions.Floor - }; - } + AllowedFunctions.Ceiling, + AllowedFunctions.Round, + AllowedFunctions.Floor + }; } + } - public static IEnumerable AllNonMathFunctionsData + public static IEnumerable AllNonMathFunctionsData + { + get { - get - { - return - new List(Enum.GetValues(typeof(AllowedFunctions)) as AllowedFunctions[]) - .Except(new[] { AllowedFunctions.AllFunctions, AllowedFunctions.AllMathFunctions }) - .Except(AllMathFunctionsData.Select(t => (AllowedFunctions)t[0])) - .Select(t => new object[] { t }); - } + return + new List(Enum.GetValues(typeof(AllowedFunctions)) as AllowedFunctions[]) + .Except(new[] { AllowedFunctions.AllFunctions, AllowedFunctions.AllMathFunctions }) + .Except(AllMathFunctionsData.Select(t => (AllowedFunctions)t[0])) + .Select(t => new object[] { t }); } + } - [Fact] - public void None_MatchesNone() - { - Assert.Equal(AllowedFunctions.None, AllowedFunctions.AllFunctions & AllowedFunctions.None); - } + [Fact] + public void None_MatchesNone() + { + Assert.Equal(AllowedFunctions.None, AllowedFunctions.AllFunctions & AllowedFunctions.None); + } - [Theory] - [MemberData(nameof(AllStringFunctionsData))] - public void AllStringFunctions_Has_AllStringFunctions(AllowedFunctions stringFunction) - { - Assert.Equal(stringFunction, AllowedFunctions.AllStringFunctions & stringFunction); - } + [Theory] + [MemberData(nameof(AllStringFunctionsData))] + public void AllStringFunctions_Has_AllStringFunctions(AllowedFunctions stringFunction) + { + Assert.Equal(stringFunction, AllowedFunctions.AllStringFunctions & stringFunction); + } - [Theory] - [MemberData(nameof(AllNonStringFunctionsData))] - public void AllStringFunctions_DoesNot_Have_NonStringFunctions(AllowedFunctions nonStringFunction) - { - Assert.Equal(AllowedFunctions.None, AllowedFunctions.AllStringFunctions & nonStringFunction); - } + [Theory] + [MemberData(nameof(AllNonStringFunctionsData))] + public void AllStringFunctions_DoesNot_Have_NonStringFunctions(AllowedFunctions nonStringFunction) + { + Assert.Equal(AllowedFunctions.None, AllowedFunctions.AllStringFunctions & nonStringFunction); + } - [Theory] - [MemberData(nameof(AllDateTimeFunctionsData))] - public void AllDateTimeFunctions_Has_AllDateFunctions(AllowedFunctions dateFunction) - { - Assert.Equal(dateFunction, AllowedFunctions.AllDateTimeFunctions & dateFunction); - } + [Theory] + [MemberData(nameof(AllDateTimeFunctionsData))] + public void AllDateTimeFunctions_Has_AllDateFunctions(AllowedFunctions dateFunction) + { + Assert.Equal(dateFunction, AllowedFunctions.AllDateTimeFunctions & dateFunction); + } - [Theory] - [MemberData(nameof(AllNonDateTimeFunctionsData))] - public void AllDateTimeFunctions_DoesNot_Have_NontringFunctions(AllowedFunctions nonDateTimeFunction) - { - Assert.Equal(AllowedFunctions.None, AllowedFunctions.AllDateTimeFunctions & nonDateTimeFunction); - } + [Theory] + [MemberData(nameof(AllNonDateTimeFunctionsData))] + public void AllDateTimeFunctions_DoesNot_Have_NontringFunctions(AllowedFunctions nonDateTimeFunction) + { + Assert.Equal(AllowedFunctions.None, AllowedFunctions.AllDateTimeFunctions & nonDateTimeFunction); + } - [Theory] - [MemberData(nameof(AllMathFunctionsData))] - public void AllMathFunctions_Has_AllMathFunctions(AllowedFunctions mathFunction) - { - Assert.Equal(mathFunction, AllowedFunctions.AllMathFunctions & mathFunction); - } + [Theory] + [MemberData(nameof(AllMathFunctionsData))] + public void AllMathFunctions_Has_AllMathFunctions(AllowedFunctions mathFunction) + { + Assert.Equal(mathFunction, AllowedFunctions.AllMathFunctions & mathFunction); + } - [Theory] - [MemberData(nameof(AllNonMathFunctionsData))] - public void AllMathFunctions_DoesNot_Have_NonMathFunctions(AllowedFunctions nonMathFunction) - { - Assert.Equal(AllowedFunctions.None, AllowedFunctions.AllMathFunctions & nonMathFunction); - } + [Theory] + [MemberData(nameof(AllNonMathFunctionsData))] + public void AllMathFunctions_DoesNot_Have_NonMathFunctions(AllowedFunctions nonMathFunction) + { + Assert.Equal(AllowedFunctions.None, AllowedFunctions.AllMathFunctions & nonMathFunction); + } - [Fact] - public void AllFunctions_Contains_AllFunctionNames() + [Fact] + public void AllFunctions_Contains_AllFunctionNames() + { + AllowedFunctions allFunctions = 0; + foreach (AllowedFunctions allowedFunction in Enum.GetValues(typeof(AllowedFunctions))) { - AllowedFunctions allFunctions = 0; - foreach (AllowedFunctions allowedFunction in Enum.GetValues(typeof(AllowedFunctions))) + if (allowedFunction != AllowedFunctions.AllFunctions) { - if (allowedFunction != AllowedFunctions.AllFunctions) - { - allFunctions = allFunctions | allowedFunction; - } + allFunctions = allFunctions | allowedFunction; } - - Assert.Equal(AllowedFunctions.AllFunctions, allFunctions); } + + Assert.Equal(AllowedFunctions.AllFunctions, allFunctions); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedLogicalOperatorsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedLogicalOperatorsTests.cs index df5b75bd2..cc0c3a702 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedLogicalOperatorsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedLogicalOperatorsTests.cs @@ -9,29 +9,28 @@ using System; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class AllowedLogicalOperatorsTests { - public class AllowedLogicalOperatorsTests + [Fact] + public void None_MatchesNone() { - [Fact] - public void None_MatchesNone() - { - Assert.Equal(AllowedLogicalOperators.None, AllowedLogicalOperators.All & AllowedLogicalOperators.None); - } + Assert.Equal(AllowedLogicalOperators.None, AllowedLogicalOperators.All & AllowedLogicalOperators.None); + } - [Fact] - public void All_Contains_AllLogicalOperators() + [Fact] + public void All_Contains_AllLogicalOperators() + { + AllowedLogicalOperators allLogicalOperators = 0; + foreach (AllowedLogicalOperators allowedLogicalOperator in Enum.GetValues(typeof(AllowedLogicalOperators))) { - AllowedLogicalOperators allLogicalOperators = 0; - foreach (AllowedLogicalOperators allowedLogicalOperator in Enum.GetValues(typeof(AllowedLogicalOperators))) + if (allowedLogicalOperator != AllowedLogicalOperators.All) { - if (allowedLogicalOperator != AllowedLogicalOperators.All) - { - allLogicalOperators = allLogicalOperators | allowedLogicalOperator; - } + allLogicalOperators = allLogicalOperators | allowedLogicalOperator; } - - Assert.Equal(AllowedLogicalOperators.All, allLogicalOperators); } + + Assert.Equal(AllowedLogicalOperators.All, allLogicalOperators); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedQueryOptionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedQueryOptionsTests.cs index c0d8aa424..00dea8247 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedQueryOptionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/AllowedQueryOptionsTests.cs @@ -9,50 +9,49 @@ using Microsoft.AspNetCore.OData.Query; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class AllowedQueryOptionsTests { - public class AllowedQueryOptionsTests + [Fact] + public void None_MatchesNone() { - [Fact] - public void None_MatchesNone() - { - Assert.Equal(AllowedQueryOptions.None, AllowedQueryOptions.All & AllowedQueryOptions.None); - } + Assert.Equal(AllowedQueryOptions.None, AllowedQueryOptions.All & AllowedQueryOptions.None); + } - [Theory] - [InlineData(AllowedQueryOptions.Filter)] - [InlineData(AllowedQueryOptions.OrderBy)] - [InlineData(AllowedQueryOptions.Skip)] - [InlineData(AllowedQueryOptions.Top)] - [InlineData(AllowedQueryOptions.Count)] - [InlineData(AllowedQueryOptions.Select)] - [InlineData(AllowedQueryOptions.Expand)] - [InlineData(AllowedQueryOptions.Format)] - [InlineData(AllowedQueryOptions.SkipToken)] - public void Supported_Contains_SupportedQueryOptions(AllowedQueryOptions queryOption) - { - Assert.Equal(queryOption, AllowedQueryOptions.Supported & queryOption); - } + [Theory] + [InlineData(AllowedQueryOptions.Filter)] + [InlineData(AllowedQueryOptions.OrderBy)] + [InlineData(AllowedQueryOptions.Skip)] + [InlineData(AllowedQueryOptions.Top)] + [InlineData(AllowedQueryOptions.Count)] + [InlineData(AllowedQueryOptions.Select)] + [InlineData(AllowedQueryOptions.Expand)] + [InlineData(AllowedQueryOptions.Format)] + [InlineData(AllowedQueryOptions.SkipToken)] + public void Supported_Contains_SupportedQueryOptions(AllowedQueryOptions queryOption) + { + Assert.Equal(queryOption, AllowedQueryOptions.Supported & queryOption); + } - [Fact] - public void Supported_DoesNotContain_UnsupportedQueryOptions() - { - Assert.Equal(AllowedQueryOptions.None, AllowedQueryOptions.Supported & (AllowedQueryOptions.DeltaToken)); - } + [Fact] + public void Supported_DoesNotContain_UnsupportedQueryOptions() + { + Assert.Equal(AllowedQueryOptions.None, AllowedQueryOptions.Supported & (AllowedQueryOptions.DeltaToken)); + } - [Fact] - public void All_Contains_AllQueryOptions() + [Fact] + public void All_Contains_AllQueryOptions() + { + AllowedQueryOptions allQueryOptions = 0; + foreach (AllowedQueryOptions allowedQueryOption in Enum.GetValues(typeof(AllowedQueryOptions))) { - AllowedQueryOptions allQueryOptions = 0; - foreach (AllowedQueryOptions allowedQueryOption in Enum.GetValues(typeof(AllowedQueryOptions))) + if (allowedQueryOption != AllowedQueryOptions.All) { - if (allowedQueryOption != AllowedQueryOptions.All) - { - allQueryOptions = allQueryOptions | allowedQueryOption; - } + allQueryOptions = allQueryOptions | allowedQueryOption; } - - Assert.Equal(AllowedQueryOptions.All, allQueryOptions); } + + Assert.Equal(AllowedQueryOptions.All, allQueryOptions); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Container/JsonPropertyNameMapperTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Container/JsonPropertyNameMapperTest.cs index 5aa1e110a..addd21a9c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Container/JsonPropertyNameMapperTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Container/JsonPropertyNameMapperTest.cs @@ -11,56 +11,55 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Container +namespace Microsoft.AspNetCore.OData.Tests.Query.Container; + +public class JsonPropertyNameMapperTests { - public class JsonPropertyNameMapperTests + [Fact] + public void MapProperty_Maps_PropertyName() { - [Fact] - public void MapProperty_Maps_PropertyName() - { - // Arrange - (IEdmModel model, IEdmStructuredType address) = GetOData(); - JsonPropertyNameMapper mapper = new JsonPropertyNameMapper(model, address); + // Arrange + (IEdmModel model, IEdmStructuredType address) = GetOData(); + JsonPropertyNameMapper mapper = new JsonPropertyNameMapper(model, address); - // Act & Assert - Assert.Equal("Road", mapper.MapProperty("Street")); + // Act & Assert + Assert.Equal("Road", mapper.MapProperty("Street")); - // Act & Assert - Assert.Equal("City", mapper.MapProperty("City")); + // Act & Assert + Assert.Equal("City", mapper.MapProperty("City")); - // Act & Assert - Assert.Null(mapper.MapProperty("IgnoreThis")); - } + // Act & Assert + Assert.Null(mapper.MapProperty("IgnoreThis")); + } - private static (IEdmModel, IEdmStructuredType) GetOData() - { - EdmModel model = new EdmModel(); - EdmComplexType address = new EdmComplexType("NS", "Address"); - address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); - address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); - address.AddStructuralProperty("IgnoreThis", EdmPrimitiveTypeKind.String); - model.AddElement(address); + private static (IEdmModel, IEdmStructuredType) GetOData() + { + EdmModel model = new EdmModel(); + EdmComplexType address = new EdmComplexType("NS", "Address"); + address.AddStructuralProperty("City", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + address.AddStructuralProperty("IgnoreThis", EdmPrimitiveTypeKind.String); + model.AddElement(address); - model.SetAnnotationValue(address, new ClrTypeAnnotation(typeof(JAddress))); + model.SetAnnotationValue(address, new ClrTypeAnnotation(typeof(JAddress))); - model.SetAnnotationValue(address.FindProperty("Street"), - new ClrPropertyInfoAnnotation(typeof(JAddress).GetProperty("Street"))); + model.SetAnnotationValue(address.FindProperty("Street"), + new ClrPropertyInfoAnnotation(typeof(JAddress).GetProperty("Street"))); - model.SetAnnotationValue(address.FindProperty("IgnoreThis"), - new ClrPropertyInfoAnnotation(typeof(JAddress).GetProperty("IgnoreThis"))); + model.SetAnnotationValue(address.FindProperty("IgnoreThis"), + new ClrPropertyInfoAnnotation(typeof(JAddress).GetProperty("IgnoreThis"))); - return (model, address); - } + return (model, address); + } - private class JAddress - { - public string City { get; set; } + private class JAddress + { + public string City { get; set; } - [JsonPropertyName("Road")] - public string Street { get; set; } + [JsonPropertyName("Road")] + public string Street { get; set; } - [JsonIgnore] - public string IgnoreThis { get; set; } - } + [JsonIgnore] + public string IgnoreThis { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Container/PropertyContainerTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Container/PropertyContainerTest.cs index ddb455822..501ea721e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Container/PropertyContainerTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Container/PropertyContainerTest.cs @@ -14,245 +14,244 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Container +namespace Microsoft.AspNetCore.OData.Tests.Query.Container; + +public class PropertyContainerTest { - public class PropertyContainerTest + [Fact] + public void CreatePropertyContainer_CreatesMemberInitExpression() { - [Fact] - public void CreatePropertyContainer_CreatesMemberInitExpression() - { - // Arrange - Expression propertyName = Expression.Constant("PropertyName"); - Mock propertyValue = new Mock(); - propertyValue.Setup(p => p.Type).Returns(typeof(TestEntity)); + // Arrange + Expression propertyName = Expression.Constant("PropertyName"); + Mock propertyValue = new Mock(); + propertyValue.Setup(p => p.Type).Returns(typeof(TestEntity)); - var properties = new[] { new NamedPropertyExpression(propertyName, propertyValue.Object) }; + var properties = new[] { new NamedPropertyExpression(propertyName, propertyValue.Object) }; - // Act - Expression container = PropertyContainer.CreatePropertyContainer(properties); + // Act + Expression container = PropertyContainer.CreatePropertyContainer(properties); - // Assert - Assert.Equal(ExpressionType.MemberInit, container.NodeType); - MemberInitExpression memberInit = container as MemberInitExpression; - Assert.True(typeof(PropertyContainer).IsAssignableFrom(memberInit.NewExpression.Type)); - } + // Assert + Assert.Equal(ExpressionType.MemberInit, container.NodeType); + MemberInitExpression memberInit = container as MemberInitExpression; + Assert.True(typeof(PropertyContainer).IsAssignableFrom(memberInit.NewExpression.Type)); + } - [Fact] - public void CreatePropertyContainer_AutoSelectedProperty() - { - // Arrange - Expression propertyName = Expression.Constant("PropertyName"); - Expression propertyValue = Expression.Constant(42); - var properties = new[] { new NamedPropertyExpression(propertyName, propertyValue) { AutoSelected = true } }; + [Fact] + public void CreatePropertyContainer_AutoSelectedProperty() + { + // Arrange + Expression propertyName = Expression.Constant("PropertyName"); + Expression propertyValue = Expression.Constant(42); + var properties = new[] { new NamedPropertyExpression(propertyName, propertyValue) { AutoSelected = true } }; - // Act - Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); + // Act + Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); - // Assert - PropertyContainer container = ToContainer(containerExpression); - var dict = container.ToDictionary(new IdentityPropertyMapper(), includeAutoSelected: true); - Assert.Contains("PropertyName", dict.Keys); + // Assert + PropertyContainer container = ToContainer(containerExpression); + var dict = container.ToDictionary(new IdentityPropertyMapper(), includeAutoSelected: true); + Assert.Contains("PropertyName", dict.Keys); - dict = container.ToDictionary(new IdentityPropertyMapper(), includeAutoSelected: false); - Assert.DoesNotContain("PropertyName", dict.Keys); - } + dict = container.ToDictionary(new IdentityPropertyMapper(), includeAutoSelected: false); + Assert.DoesNotContain("PropertyName", dict.Keys); + } - [Fact] - public void CreatePropertyContainer_WithNullCheckTrue_PropertyIsNull() - { - // Arrange - string propertyName = "PropertyName"; - Expression propertyNameExpression = Expression.Constant(propertyName); - Expression propertyValueExpression = Expression.Constant(42); - Expression nullCheckExpression = Expression.Constant(true); - var properties = new[] { new NamedPropertyExpression(propertyNameExpression, propertyValueExpression) { NullCheck = nullCheckExpression } }; - - // Act - Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); - - // Assert - PropertyContainer container = ToContainer(containerExpression); - var dict = container.ToDictionary(new IdentityPropertyMapper()); - Assert.Contains(propertyName, dict.Keys); - Assert.Null(dict[propertyName]); - } - - [Fact] - public void CreatePropertyContainer_WithNullCheckFalse_PropertyIsNotNull() - { - // Arrange - string propertyName = "PropertyName"; - int propertyValue = 42; - Expression propertyNameExpression = Expression.Constant(propertyName); - Expression propertyValueExpression = Expression.Constant(propertyValue); - Expression nullCheckExpression = Expression.Constant(false); - var properties = new[] { new NamedPropertyExpression(propertyNameExpression, propertyValueExpression) { NullCheck = nullCheckExpression } }; - - // Act - Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); - - // Assert - PropertyContainer container = ToContainer(containerExpression); - var dict = container.ToDictionary(new IdentityPropertyMapper()); - Assert.Contains(propertyName, dict.Keys); - Assert.Equal(propertyValue, dict[propertyName]); - } - - [Fact] - public void CreatePropertyContainer_MultiplePropertiesWithNullCheck() - { - // Arrange - var properties = new[] - { - new NamedPropertyExpression(name: Expression.Constant("Prop1"), value: Expression.Constant(1)) { NullCheck = Expression.Constant(true) }, - new NamedPropertyExpression(name: Expression.Constant("Prop2"), value: Expression.Constant(2)) { NullCheck = Expression.Constant(false) }, - }; - - // Act - Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); - - // Assert - PropertyContainer container = ToContainer(containerExpression); - var dict = container.ToDictionary(new IdentityPropertyMapper()); - Assert.Null(dict["Prop1"]); - Assert.Equal(2, dict["Prop2"]); - } - - [Fact] - public void CreatePropertyContainer_PageSize() - { - // Arrange - int pageSize = 5; - Expression propertyName = Expression.Constant("PropertyName"); - Expression propertyValue = Expression.Constant(Enumerable.Range(0, 10)); - var properties = new[] { new NamedPropertyExpression(propertyName, propertyValue) { PageSize = pageSize } }; - - // Act - Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); - - // Assert - PropertyContainer container = ToContainer(containerExpression); - var result = container.ToDictionary(new IdentityPropertyMapper())["PropertyName"]; - var truncatedCollection = Assert.IsType>(result); - Assert.True(truncatedCollection.IsTruncated); - Assert.Equal(pageSize, truncatedCollection.PageSize); - Assert.Equal(Enumerable.Range(0, pageSize), truncatedCollection); - } - - [Fact] - public void CreatePropertyContainer_WithNullPropertyName_NotIncludeTheProperty() - { - // Arrange - Expression propertyName = Expression.Constant(null, typeof(string)); - Expression propertyValue = Expression.Constant(new TestEntity()); - NamedPropertyExpression property = new NamedPropertyExpression(propertyName, propertyValue); - var properties = new[] { property, property }; - - // Act - Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); - - // Assert - PropertyContainer container = ToContainer(containerExpression); - Assert.Empty(container.ToDictionary(new IdentityPropertyMapper(), includeAutoSelected: true)); - } - - [Fact] - public void CreatePropertyContainer_WithNullTruncatedCollection_DoesNotThrow() - { - // Arrange - int pageSize = 5; - Expression propertyName = Expression.Constant("PropertyName"); - Expression propertyValue = Expression.Constant(null, typeof(IEnumerable)); - var properties = new[] { new NamedPropertyExpression(propertyName, propertyValue) { PageSize = pageSize } }; - - // Act - Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); - - // Assert - PropertyContainer container = ToContainer(containerExpression); - var result = container.ToDictionary(new IdentityPropertyMapper())["PropertyName"]; - Assert.Null(result); - } - - [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(6)] - [InlineData(7)] - [InlineData(100)] - public void CreatePropertyContainer_CreatesPropertyContainer_WithVariousNumberOfProperties(int count) - { - // Arrange - IList properties = - Enumerable.Range(0, count) - .Select(i => new NamedPropertyExpression(Expression.Constant(i.ToString()), Expression.Constant(i))) - .ToList(); - - // Act - Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); - - // Assert - Dictionary dictionary = ToContainer(containerExpression).ToDictionary(new IdentityPropertyMapper(), includeAutoSelected: true); - Assert.Equal(Enumerable.Range(0, count).ToDictionary(i => i.ToString(), i => (object)i).OrderBy(kvp => kvp.Key), dictionary.OrderBy(kvp => kvp.Key)); - } - - [Fact] - public void ToDictionary_AppliesMappingToAllProperties() - { - // Arrange - IList properties = new NamedPropertyExpression[] - { - new NamedPropertyExpression(name: Expression.Constant("PropA"), value: Expression.Constant(3)), - new NamedPropertyExpression(name: Expression.Constant("PropB"), value: Expression.Constant(6)), - new NamedPropertyExpression(name: Expression.Constant("PropC"), value: Expression.Constant(9)) - }; - Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); - PropertyContainer container = ToContainer(containerExpression); - - Mock mapperMock = new Mock(); - mapperMock.Setup(m => m.MapProperty("PropA")).Returns("PropertyA"); - mapperMock.Setup(m => m.MapProperty("PropB")).Returns("PropB"); - mapperMock.Setup(m => m.MapProperty("PropC")).Returns((string)null); - - //Act - IDictionary result = container.ToDictionary(mapperMock.Object); - - //Assert - Assert.NotNull(result); - Assert.True(result.ContainsKey("PropertyA")); - Assert.True(result.ContainsKey("PropB")); - Assert.False(result.ContainsKey("PropC")); - } - - [Theory] - [InlineData("")] - public void ToDictionary_Throws_IfMappingFunctionReturns_Empty(string mappedName) - { - // Arrange - IList properties = new NamedPropertyExpression[] - { - new NamedPropertyExpression(name: Expression.Constant("PropA"), value: Expression.Constant(3)) - }; - Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); - PropertyContainer container = ToContainer(containerExpression); - - Mock mapperMock = new Mock(); - mapperMock.Setup(m => m.MapProperty("PropA")).Returns(mappedName); - - // Act & Assert - ExceptionAssert.Throws(() => - container.ToDictionary(mapperMock.Object), "The key mapping for the property 'PropA' can't be empty."); - } - - private static PropertyContainer ToContainer(Expression containerCreationExpression) + [Fact] + public void CreatePropertyContainer_WithNullCheckTrue_PropertyIsNull() + { + // Arrange + string propertyName = "PropertyName"; + Expression propertyNameExpression = Expression.Constant(propertyName); + Expression propertyValueExpression = Expression.Constant(42); + Expression nullCheckExpression = Expression.Constant(true); + var properties = new[] { new NamedPropertyExpression(propertyNameExpression, propertyValueExpression) { NullCheck = nullCheckExpression } }; + + // Act + Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); + + // Assert + PropertyContainer container = ToContainer(containerExpression); + var dict = container.ToDictionary(new IdentityPropertyMapper()); + Assert.Contains(propertyName, dict.Keys); + Assert.Null(dict[propertyName]); + } + + [Fact] + public void CreatePropertyContainer_WithNullCheckFalse_PropertyIsNotNull() + { + // Arrange + string propertyName = "PropertyName"; + int propertyValue = 42; + Expression propertyNameExpression = Expression.Constant(propertyName); + Expression propertyValueExpression = Expression.Constant(propertyValue); + Expression nullCheckExpression = Expression.Constant(false); + var properties = new[] { new NamedPropertyExpression(propertyNameExpression, propertyValueExpression) { NullCheck = nullCheckExpression } }; + + // Act + Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); + + // Assert + PropertyContainer container = ToContainer(containerExpression); + var dict = container.ToDictionary(new IdentityPropertyMapper()); + Assert.Contains(propertyName, dict.Keys); + Assert.Equal(propertyValue, dict[propertyName]); + } + + [Fact] + public void CreatePropertyContainer_MultiplePropertiesWithNullCheck() + { + // Arrange + var properties = new[] + { + new NamedPropertyExpression(name: Expression.Constant("Prop1"), value: Expression.Constant(1)) { NullCheck = Expression.Constant(true) }, + new NamedPropertyExpression(name: Expression.Constant("Prop2"), value: Expression.Constant(2)) { NullCheck = Expression.Constant(false) }, + }; + + // Act + Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); + + // Assert + PropertyContainer container = ToContainer(containerExpression); + var dict = container.ToDictionary(new IdentityPropertyMapper()); + Assert.Null(dict["Prop1"]); + Assert.Equal(2, dict["Prop2"]); + } + + [Fact] + public void CreatePropertyContainer_PageSize() + { + // Arrange + int pageSize = 5; + Expression propertyName = Expression.Constant("PropertyName"); + Expression propertyValue = Expression.Constant(Enumerable.Range(0, 10)); + var properties = new[] { new NamedPropertyExpression(propertyName, propertyValue) { PageSize = pageSize } }; + + // Act + Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); + + // Assert + PropertyContainer container = ToContainer(containerExpression); + var result = container.ToDictionary(new IdentityPropertyMapper())["PropertyName"]; + var truncatedCollection = Assert.IsType>(result); + Assert.True(truncatedCollection.IsTruncated); + Assert.Equal(pageSize, truncatedCollection.PageSize); + Assert.Equal(Enumerable.Range(0, pageSize), truncatedCollection); + } + + [Fact] + public void CreatePropertyContainer_WithNullPropertyName_NotIncludeTheProperty() + { + // Arrange + Expression propertyName = Expression.Constant(null, typeof(string)); + Expression propertyValue = Expression.Constant(new TestEntity()); + NamedPropertyExpression property = new NamedPropertyExpression(propertyName, propertyValue); + var properties = new[] { property, property }; + + // Act + Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); + + // Assert + PropertyContainer container = ToContainer(containerExpression); + Assert.Empty(container.ToDictionary(new IdentityPropertyMapper(), includeAutoSelected: true)); + } + + [Fact] + public void CreatePropertyContainer_WithNullTruncatedCollection_DoesNotThrow() + { + // Arrange + int pageSize = 5; + Expression propertyName = Expression.Constant("PropertyName"); + Expression propertyValue = Expression.Constant(null, typeof(IEnumerable)); + var properties = new[] { new NamedPropertyExpression(propertyName, propertyValue) { PageSize = pageSize } }; + + // Act + Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); + + // Assert + PropertyContainer container = ToContainer(containerExpression); + var result = container.ToDictionary(new IdentityPropertyMapper())["PropertyName"]; + Assert.Null(result); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(6)] + [InlineData(7)] + [InlineData(100)] + public void CreatePropertyContainer_CreatesPropertyContainer_WithVariousNumberOfProperties(int count) + { + // Arrange + IList properties = + Enumerable.Range(0, count) + .Select(i => new NamedPropertyExpression(Expression.Constant(i.ToString()), Expression.Constant(i))) + .ToList(); + + // Act + Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); + + // Assert + Dictionary dictionary = ToContainer(containerExpression).ToDictionary(new IdentityPropertyMapper(), includeAutoSelected: true); + Assert.Equal(Enumerable.Range(0, count).ToDictionary(i => i.ToString(), i => (object)i).OrderBy(kvp => kvp.Key), dictionary.OrderBy(kvp => kvp.Key)); + } + + [Fact] + public void ToDictionary_AppliesMappingToAllProperties() + { + // Arrange + IList properties = new NamedPropertyExpression[] { - LambdaExpression containerCreationLambda = Expression.Lambda(containerCreationExpression); - PropertyContainer container = containerCreationLambda.Compile().DynamicInvoke() as PropertyContainer; - return container; - } + new NamedPropertyExpression(name: Expression.Constant("PropA"), value: Expression.Constant(3)), + new NamedPropertyExpression(name: Expression.Constant("PropB"), value: Expression.Constant(6)), + new NamedPropertyExpression(name: Expression.Constant("PropC"), value: Expression.Constant(9)) + }; + Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); + PropertyContainer container = ToContainer(containerExpression); + + Mock mapperMock = new Mock(); + mapperMock.Setup(m => m.MapProperty("PropA")).Returns("PropertyA"); + mapperMock.Setup(m => m.MapProperty("PropB")).Returns("PropB"); + mapperMock.Setup(m => m.MapProperty("PropC")).Returns((string)null); + + //Act + IDictionary result = container.ToDictionary(mapperMock.Object); + + //Assert + Assert.NotNull(result); + Assert.True(result.ContainsKey("PropertyA")); + Assert.True(result.ContainsKey("PropB")); + Assert.False(result.ContainsKey("PropC")); + } - private class TestEntity + [Theory] + [InlineData("")] + public void ToDictionary_Throws_IfMappingFunctionReturns_Empty(string mappedName) + { + // Arrange + IList properties = new NamedPropertyExpression[] { - } + new NamedPropertyExpression(name: Expression.Constant("PropA"), value: Expression.Constant(3)) + }; + Expression containerExpression = PropertyContainer.CreatePropertyContainer(properties); + PropertyContainer container = ToContainer(containerExpression); + + Mock mapperMock = new Mock(); + mapperMock.Setup(m => m.MapProperty("PropA")).Returns(mappedName); + + // Act & Assert + ExceptionAssert.Throws(() => + container.ToDictionary(mapperMock.Object), "The key mapping for the property 'PropA' can't be empty."); + } + + private static PropertyContainer ToContainer(Expression containerCreationExpression) + { + LambdaExpression containerCreationLambda = Expression.Lambda(containerCreationExpression); + PropertyContainer container = containerCreationLambda.Compile().DynamicInvoke() as PropertyContainer; + return container; + } + + private class TestEntity + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Container/TruncatedCollectionOfTTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Container/TruncatedCollectionOfTTest.cs index 0fe54a464..2868e7245 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Container/TruncatedCollectionOfTTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Container/TruncatedCollectionOfTTest.cs @@ -12,94 +12,93 @@ using System.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Container +namespace Microsoft.AspNetCore.OData.Tests.Query.Container; + +public class TruncatedCollectionTest { - public class TruncatedCollectionTest + [Fact] + public void Ctor_ThrowsArgumentNull_Collection_Enumerable_Source() { - [Fact] - public void Ctor_ThrowsArgumentNull_Collection_Enumerable_Source() - { - ExceptionAssert.ThrowsArgumentNull(() => new TruncatedCollection(source: null, pageSize: 10), "source"); - } + ExceptionAssert.ThrowsArgumentNull(() => new TruncatedCollection(source: null, pageSize: 10), "source"); + } - [Fact] - public void Ctor_ThrowsArgumentNull_Collection_Queryable_Source() - { - ExceptionAssert.ThrowsArgumentNull(() => new TruncatedCollection(source: null, pageSize: 10, parameterize: false), "source"); - } + [Fact] + public void Ctor_ThrowsArgumentNull_Collection_Queryable_Source() + { + ExceptionAssert.ThrowsArgumentNull(() => new TruncatedCollection(source: null, pageSize: 10, parameterize: false), "source"); + } - [Fact] - public void Ctor_ThrowsArgumentGreater_Collection() - { - ExceptionAssert.ThrowsArgumentGreaterThanOrEqualTo( - () => new TruncatedCollection(source: new int[0], pageSize: 0), "pageSize", "1", "0"); - } + [Fact] + public void Ctor_ThrowsArgumentGreater_Collection() + { + ExceptionAssert.ThrowsArgumentGreaterThanOrEqualTo( + () => new TruncatedCollection(source: new int[0], pageSize: 0), "pageSize", "1", "0"); + } - [Fact] - public void CtorTruncatedCollection_SetsProperties() - { - // Arrange & Act - IEnumerable source = new[] { 1, 2, 3, 5, 7 }; - TruncatedCollection collection = new TruncatedCollection(source, 3, 5); + [Fact] + public void CtorTruncatedCollection_SetsProperties() + { + // Arrange & Act + IEnumerable source = new[] { 1, 2, 3, 5, 7 }; + TruncatedCollection collection = new TruncatedCollection(source, 3, 5); - // Arrange - Assert.Equal(3, collection.PageSize); - Assert.Equal(5, collection.TotalCount); - Assert.True(collection.IsTruncated); - Assert.Equal(3, collection.Count); - Assert.Equal(new[] { 1, 2, 3 }, collection); - } + // Arrange + Assert.Equal(3, collection.PageSize); + Assert.Equal(5, collection.TotalCount); + Assert.True(collection.IsTruncated); + Assert.Equal(3, collection.Count); + Assert.Equal(new[] { 1, 2, 3 }, collection); + } - [Fact] - [Obsolete] - public void CtorTruncatedCollection_WithQueryable_SetsProperties() - { - // Arrange & Act - var source = new[] { 1, 2, 3, 5, 7 }.AsQueryable(); - TruncatedCollection collection = new TruncatedCollection(source, 3, 5); + [Fact] + [Obsolete] + public void CtorTruncatedCollection_WithQueryable_SetsProperties() + { + // Arrange & Act + var source = new[] { 1, 2, 3, 5, 7 }.AsQueryable(); + TruncatedCollection collection = new TruncatedCollection(source, 3, 5); - // Arrange - Assert.Equal(3, collection.PageSize); - Assert.Equal(5, collection.TotalCount); - Assert.True(collection.IsTruncated); - Assert.Equal(3, collection.Count); - Assert.Equal(new[] { 1, 2, 3 }, collection); - } + // Arrange + Assert.Equal(3, collection.PageSize); + Assert.Equal(5, collection.TotalCount); + Assert.True(collection.IsTruncated); + Assert.Equal(3, collection.Count); + Assert.Equal(new[] { 1, 2, 3 }, collection); + } - [Theory] - [InlineData(1, true)] - [InlineData(3, false)] - [InlineData(10, false)] - public void Property_IsTruncated(int pageSize, bool expectedResult) - { - TruncatedCollection collection = new TruncatedCollection(new[] { 1, 2, 3 }, pageSize); - Assert.Equal(expectedResult, collection.IsTruncated); - } + [Theory] + [InlineData(1, true)] + [InlineData(3, false)] + [InlineData(10, false)] + public void Property_IsTruncated(int pageSize, bool expectedResult) + { + TruncatedCollection collection = new TruncatedCollection(new[] { 1, 2, 3 }, pageSize); + Assert.Equal(expectedResult, collection.IsTruncated); + } - [Fact] - public void Property_PageSize() - { - int pageSize = 42; - TruncatedCollection collection = new TruncatedCollection(new[] { 1, 2, 3 }, pageSize); - Assert.Equal(pageSize, collection.PageSize); - } + [Fact] + public void Property_PageSize() + { + int pageSize = 42; + TruncatedCollection collection = new TruncatedCollection(new[] { 1, 2, 3 }, pageSize); + Assert.Equal(pageSize, collection.PageSize); + } - [Fact] - public void GetEnumerator_Truncates_IfPageSizeIsLessThanCollectionSize() - { - TruncatedCollection collection = new TruncatedCollection(new[] { 1, 2, 3 }, pageSize: 2); + [Fact] + public void GetEnumerator_Truncates_IfPageSizeIsLessThanCollectionSize() + { + TruncatedCollection collection = new TruncatedCollection(new[] { 1, 2, 3 }, pageSize: 2); - Assert.Equal(new[] { 1, 2 }, collection); - } + Assert.Equal(new[] { 1, 2 }, collection); + } - [Theory] - [InlineData(3)] - [InlineData(42)] - public void GetEnumerator_DoesNotTruncate_IfPageSizeIsGreaterThanOrEqualToCollectionSize(int pageSize) - { - TruncatedCollection collection = new TruncatedCollection(new[] { 1, 2, 3 }, pageSize); + [Theory] + [InlineData(3)] + [InlineData(42)] + public void GetEnumerator_DoesNotTruncate_IfPageSizeIsGreaterThanOrEqualToCollectionSize(int pageSize) + { + TruncatedCollection collection = new TruncatedCollection(new[] { 1, 2, 3 }, pageSize); - Assert.Equal(new[] { 1, 2, 3 }, collection); - } + Assert.Equal(new[] { 1, 2, 3 }, collection); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/DefaultODataQueryRequestParserTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/DefaultODataQueryRequestParserTests.cs index 135e0455d..6cd16f6c4 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/DefaultODataQueryRequestParserTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/DefaultODataQueryRequestParserTests.cs @@ -13,87 +13,86 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class DefaultODataQueryRequestParserTests { - public class DefaultODataQueryRequestParserTests + private const string QueryOptionsString = "$filter=Id le 5"; + + [Fact] + public async Task ParseAsync_WithQueryOptionsInRequestBody() + { + // Arrange + HttpContext context = new DefaultHttpContext(); + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(QueryOptionsString)); + + // Act + var result = await new DefaultODataQueryRequestParser().ParseAsync(context.Request); + + // Assert + Assert.Equal(QueryOptionsString, result); + } + + [Fact] + public async Task ParseAsync_Throws_WithDisposedStream() + { + // Arrange + var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(QueryOptionsString)); + HttpContext context = new DefaultHttpContext(); + context.Request.Body = memoryStream; + memoryStream.Dispose(); + + // Act & Assert + await Assert.ThrowsAsync(async () => await new DefaultODataQueryRequestParser().ParseAsync(context.Request)); + } + + [Fact] + public async Task ParseAsync_WithEmptyStream() + { + // Arrange + HttpContext context = new DefaultHttpContext(); + context.Request.Body = new MemoryStream(); + + // Act + var result = await new DefaultODataQueryRequestParser().ParseAsync(context.Request); + + // Assert + Assert.Equal("", result); + } + + [Fact] + public void CanParse_MatchesRequest_WithMatchingContentType() + { + // Arrange + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; + request.ContentType = "text/plain"; + + // Act & Assert + Assert.True(new DefaultODataQueryRequestParser().CanParse(request)); + } + + [Fact] + public void CanParse_DoesNotMatchRequest_WithNonMatchingContentType() + { + // Arrange + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; + request.ContentType = "application/json"; + + // Act & Assert + Assert.False(new DefaultODataQueryRequestParser().CanParse(request)); + } + + [Fact] + public void CanParse_MatchesRequest_WithNonExactContentType() { - private const string QueryOptionsString = "$filter=Id le 5"; - - [Fact] - public async Task ParseAsync_WithQueryOptionsInRequestBody() - { - // Arrange - HttpContext context = new DefaultHttpContext(); - context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(QueryOptionsString)); - - // Act - var result = await new DefaultODataQueryRequestParser().ParseAsync(context.Request); - - // Assert - Assert.Equal(QueryOptionsString, result); - } - - [Fact] - public async Task ParseAsync_Throws_WithDisposedStream() - { - // Arrange - var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(QueryOptionsString)); - HttpContext context = new DefaultHttpContext(); - context.Request.Body = memoryStream; - memoryStream.Dispose(); - - // Act & Assert - await Assert.ThrowsAsync(async () => await new DefaultODataQueryRequestParser().ParseAsync(context.Request)); - } - - [Fact] - public async Task ParseAsync_WithEmptyStream() - { - // Arrange - HttpContext context = new DefaultHttpContext(); - context.Request.Body = new MemoryStream(); - - // Act - var result = await new DefaultODataQueryRequestParser().ParseAsync(context.Request); - - // Assert - Assert.Equal("", result); - } - - [Fact] - public void CanParse_MatchesRequest_WithMatchingContentType() - { - // Arrange - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - request.ContentType = "text/plain"; - - // Act & Assert - Assert.True(new DefaultODataQueryRequestParser().CanParse(request)); - } - - [Fact] - public void CanParse_DoesNotMatchRequest_WithNonMatchingContentType() - { - // Arrange - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - request.ContentType = "application/json"; - - // Act & Assert - Assert.False(new DefaultODataQueryRequestParser().CanParse(request)); - } - - [Fact] - public void CanParse_MatchesRequest_WithNonExactContentType() - { - // Arrange - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - request.ContentType = "text/plain;charset=utf-8"; - - // Act & Assert - Assert.True(new DefaultODataQueryRequestParser().CanParse(request)); - } + // Arrange + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; + request.ContentType = "text/plain;charset=utf-8"; + + // Act & Assert + Assert.True(new DefaultODataQueryRequestParser().CanParse(request)); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/ETagTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/ETagTests.cs index 1f30ba3d7..1bc5a1fb8 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/ETagTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/ETagTests.cs @@ -22,351 +22,350 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class ETagTests { - public class ETagTests + private readonly IList _customers; + + public ETagTests() { - private readonly IList _customers; + _customers = new List + { + new ETagCustomer + { + ID = 1, + FirstName = "Foo", + LastName = "Bar", + }, + new ETagCustomer + { + ID = 2, + FirstName = "Abc", + LastName = "Xyz", + }, + new ETagCustomer + { + ID = 3, + FirstName = "Def", + LastName = "Xyz", + }, + }; + } - public ETagTests() - { - _customers = new List - { - new ETagCustomer - { - ID = 1, - FirstName = "Foo", - LastName = "Bar", - }, - new ETagCustomer - { - ID = 2, - FirstName = "Abc", - LastName = "Xyz", - }, - new ETagCustomer - { - ID = 3, - FirstName = "Def", - LastName = "Xyz", - }, - }; - } + [Fact] + public void GetValue_Returns_SetValue() + { + // Arrange + ETag etag = new ETag(); - [Fact] - public void GetValue_Returns_SetValue() - { - // Arrange - ETag etag = new ETag(); + // Act & Assert + etag["Name"] = "Name1"; + Assert.Equal("Name1", etag["Name"]); + } - // Act & Assert - etag["Name"] = "Name1"; - Assert.Equal("Name1", etag["Name"]); - } + [Fact] + public void DynamicGetValue_Returns_DynamicSetValue() + { + // Arrange + dynamic etag = new ETag(); - [Fact] - public void DynamicGetValue_Returns_DynamicSetValue() - { - // Arrange - dynamic etag = new ETag(); + // Act & Assert + etag.Name = "Name1"; + Assert.Equal("Name1", etag.Name); + } - // Act & Assert - etag.Name = "Name1"; - Assert.Equal("Name1", etag.Name); - } + [Fact] + public void GetValue_ThrowsInvalidOperation_IfNotWellFormed() + { + // Arrange + ETag etag = new ETag(); + etag["Name"] = "Name1"; + etag.IsWellFormed = false; - [Fact] - public void GetValue_ThrowsInvalidOperation_IfNotWellFormed() - { - // Arrange - ETag etag = new ETag(); - etag["Name"] = "Name1"; - etag.IsWellFormed = false; + // Act && Assert + ExceptionAssert.Throws(() => etag["Name"], "The ETag is not well-formed."); + } - // Act && Assert - ExceptionAssert.Throws(() => etag["Name"], "The ETag is not well-formed."); - } + [Fact] + public void DynamicGetValue_ThrowsInvalidOperation_IfNotWellFormed() + { + // Arrange + ETag etag = new ETag(); + etag["Name"] = "Name1"; + etag.IsWellFormed = false; + dynamic dynamicETag = etag; + + // Act && Assert + ExceptionAssert.Throws(() => dynamicETag.Name, "The ETag is not well-formed."); + } - [Fact] - public void DynamicGetValue_ThrowsInvalidOperation_IfNotWellFormed() - { - // Arrange - ETag etag = new ETag(); - etag["Name"] = "Name1"; - etag.IsWellFormed = false; - dynamic dynamicETag = etag; - - // Act && Assert - ExceptionAssert.Throws(() => dynamicETag.Name, "The ETag is not well-formed."); - } + [Fact] + public void ApplyTo_NewQueryReturned_GivenQueryable() + { + // Arrange + ETag etagCustomer = new ETag { EntityType = typeof(ETagCustomer) }; + dynamic etag = etagCustomer; + etag.FirstName = "Foo"; + + // Act + IQueryable queryable = etagCustomer.ApplyTo(_customers.AsQueryable()); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); + Assert.Equal( + new[] { 1 }, + actualCustomers.Select(customer => customer.ID)); + MethodCallExpression methodCall = queryable.Expression as MethodCallExpression; + Assert.NotNull(methodCall); + Assert.Equal(2, methodCall.Arguments.Count); + Assert.Equal(@"Param_0 => (Param_0.FirstName == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty)", + methodCall.Arguments[1].ToString()); + } - [Fact] - public void ApplyTo_NewQueryReturned_GivenQueryable() - { - // Arrange - ETag etagCustomer = new ETag { EntityType = typeof(ETagCustomer) }; - dynamic etag = etagCustomer; - etag.FirstName = "Foo"; + [Fact] + public void ApplyTo_NewQueryReturned_GivenQueryableAndIsIfNoneMatch() + { + // Arrange + ETag etagCustomer = new ETag { EntityType = typeof(ETagCustomer), IsIfNoneMatch = true }; + dynamic etag = etagCustomer; + etag.LastName = "Xyz"; + + // Act + IQueryable queryable = etagCustomer.ApplyTo(_customers.AsQueryable()); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); + Assert.Equal( + new[] { 1 }, + actualCustomers.Select(customer => customer.ID)); + MethodCallExpression methodCall = queryable.Expression as MethodCallExpression; + Assert.NotNull(methodCall); + Assert.Equal(2, methodCall.Arguments.Count); + Assert.Equal( + @"Param_0 => Not((Param_0.LastName == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty))", + methodCall.Arguments[1].ToString()); + } - // Act - IQueryable queryable = etagCustomer.ApplyTo(_customers.AsQueryable()); + [Fact] + public void ApplyTo_NewQueryReturned_IsIfNoneMatchWithMultipleConcurrencyProperties() + { + // Arrange + ETag etagCustomer = new ETag { EntityType = typeof(ETagCustomer), IsIfNoneMatch = true }; + dynamic etag = etagCustomer; + etag.FirstName = "Def"; + etag.LastName = "Xyz"; + + // Act + IQueryable queryable = etagCustomer.ApplyTo(_customers.AsQueryable()); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); + Assert.Equal( + new[] { 1, 2 }, + actualCustomers.Select(customer => customer.ID)); + MethodCallExpression methodCall = queryable.Expression as MethodCallExpression; + Assert.NotNull(methodCall); + Assert.Equal(2, methodCall.Arguments.Count); + Assert.Equal( + @"Param_0 => Not(((Param_0.FirstName == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty) " + + "AndAlso (Param_0.LastName == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty)))", + methodCall.Arguments[1].ToString()); + } - // Assert - Assert.NotNull(queryable); - IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); - Assert.Equal( - new[] { 1 }, - actualCustomers.Select(customer => customer.ID)); - MethodCallExpression methodCall = queryable.Expression as MethodCallExpression; - Assert.NotNull(methodCall); - Assert.Equal(2, methodCall.Arguments.Count); - Assert.Equal(@"Param_0 => (Param_0.FirstName == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty)", - methodCall.Arguments[1].ToString()); - } + [Fact] + public void ApplyTo_SameQueryReturned_GivenQueryableAndETagAny() + { + // Arrange + var any = new ETag { IsAny = true }; + var customers = _customers.AsQueryable(); - [Fact] - public void ApplyTo_NewQueryReturned_GivenQueryableAndIsIfNoneMatch() - { - // Arrange - ETag etagCustomer = new ETag { EntityType = typeof(ETagCustomer), IsIfNoneMatch = true }; - dynamic etag = etagCustomer; - etag.LastName = "Xyz"; + // Act + var queryable = any.ApplyTo(customers); - // Act - IQueryable queryable = etagCustomer.ApplyTo(_customers.AsQueryable()); + // Assert + Assert.NotNull(queryable); + Assert.Same(queryable, customers); + } - // Assert - Assert.NotNull(queryable); - IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); - Assert.Equal( - new[] { 1 }, - actualCustomers.Select(customer => customer.ID)); - MethodCallExpression methodCall = queryable.Expression as MethodCallExpression; - Assert.NotNull(methodCall); - Assert.Equal(2, methodCall.Arguments.Count); - Assert.Equal( - @"Param_0 => Not((Param_0.LastName == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty))", - methodCall.Arguments[1].ToString()); + [Theory] + [InlineData(1.0, true, new[] { 1, 3 })] + [InlineData(1.0, false, new[] { 2 })] + [InlineData(1.1, true, null)] + [InlineData(1.1, false, new[] { 1, 2, 3 })] + public void ApplyTo_NewQueryReturned_ForDouble(double value, bool ifMatch, IList expect) + { + // Arrange + var myCustomers = new List + { + new MyETagCustomer + { + ID = 1, + DoubleETag = 1.0, + }, + new MyETagCustomer + { + ID = 2, + DoubleETag = 1.1, + }, + new MyETagCustomer + { + ID = 3, + DoubleETag = 1.0, + }, + }; + + IETagHandler handerl = new DefaultODataETagHandler(); + Dictionary properties = new Dictionary { { "DoubleETag", value } }; + EntityTagHeaderValue etagHeaderValue = handerl.CreateETag(properties); + + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + IEdmModel model = builder.GetEdmModel(); + IEdmEntityType customer = model.SchemaElements.OfType().FirstOrDefault(e => e.Name == "MyEtagCustomer"); + IEdmEntitySet customers = model.FindDeclaredEntitySet("Customers"); + ODataPath odataPath = new ODataPath(new[] { new EntitySetSegment(customers) }); + var request = RequestFactory.Create(model, opt => opt.AddRouteComponents(model)); + request.ODataFeature().Path = odataPath; + + ETag etagCustomer = request.GetETag(etagHeaderValue); + etagCustomer.EntityType = typeof(MyETagCustomer); + etagCustomer.IsIfNoneMatch = !ifMatch; + + // Act + IQueryable queryable = etagCustomer.ApplyTo(myCustomers.AsQueryable()); + + // Assert + Assert.NotNull(queryable); + IList actualCustomers = Assert.IsAssignableFrom>(queryable).ToList(); + if (expect != null) + { + Assert.Equal(expect, actualCustomers.Select(c => c.ID)); } - [Fact] - public void ApplyTo_NewQueryReturned_IsIfNoneMatchWithMultipleConcurrencyProperties() + MethodCallExpression methodCall = queryable.Expression as MethodCallExpression; + Assert.NotNull(methodCall); + Assert.Equal(2, methodCall.Arguments.Count); + if (ifMatch) { - // Arrange - ETag etagCustomer = new ETag { EntityType = typeof(ETagCustomer), IsIfNoneMatch = true }; - dynamic etag = etagCustomer; - etag.FirstName = "Def"; - etag.LastName = "Xyz"; - - // Act - IQueryable queryable = etagCustomer.ApplyTo(_customers.AsQueryable()); - - // Assert - Assert.NotNull(queryable); - IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); - Assert.Equal( - new[] { 1, 2 }, - actualCustomers.Select(customer => customer.ID)); - MethodCallExpression methodCall = queryable.Expression as MethodCallExpression; - Assert.NotNull(methodCall); - Assert.Equal(2, methodCall.Arguments.Count); Assert.Equal( - @"Param_0 => Not(((Param_0.FirstName == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty) " - + "AndAlso (Param_0.LastName == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.String]).TypedProperty)))", + "Param_0 => (Param_0.DoubleETag == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Double]).TypedProperty)", methodCall.Arguments[1].ToString()); } - - [Fact] - public void ApplyTo_SameQueryReturned_GivenQueryableAndETagAny() + else { - // Arrange - var any = new ETag { IsAny = true }; - var customers = _customers.AsQueryable(); + Assert.Equal( + "Param_0 => Not((Param_0.DoubleETag == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Double]).TypedProperty))", + methodCall.Arguments[1].ToString()); + } + } - // Act - var queryable = any.ApplyTo(customers); + public class MyETagCustomer + { + public int ID { get; set; } - // Assert - Assert.NotNull(queryable); - Assert.Same(queryable, customers); - } + [ConcurrencyCheck] + public double DoubleETag { get; set; } + } - [Theory] - [InlineData(1.0, true, new[] { 1, 3 })] - [InlineData(1.0, false, new[] { 2 })] - [InlineData(1.1, true, null)] - [InlineData(1.1, false, new[] { 1, 2, 3 })] - public void ApplyTo_NewQueryReturned_ForDouble(double value, bool ifMatch, IList expect) + [Theory] + [InlineData((sbyte)1, (short)1, true, new int[] {})] + [InlineData((sbyte)1, (short)1, false, new[] { 1, 2, 3 })] + [InlineData(SByte.MaxValue, Int16.MaxValue, true, new[] { 2 })] + [InlineData(SByte.MaxValue, Int16.MaxValue, false, new[] { 1, 3 })] + [InlineData(SByte.MinValue, Int16.MinValue, true, new[] { 3 })] + [InlineData(SByte.MinValue, Int16.MinValue, false, new[] { 1, 2 })] + public void ApplyTo_NewQueryReturned_ForInteger(sbyte byteVal, short shortVal, bool ifMatch, IList expect) + { + // Arrange + var mycustomers = new List { - // Arrange - var myCustomers = new List - { - new MyETagCustomer - { - ID = 1, - DoubleETag = 1.0, - }, - new MyETagCustomer - { - ID = 2, - DoubleETag = 1.1, - }, - new MyETagCustomer - { - ID = 3, - DoubleETag = 1.0, - }, - }; - - IETagHandler handerl = new DefaultODataETagHandler(); - Dictionary properties = new Dictionary { { "DoubleETag", value } }; - EntityTagHeaderValue etagHeaderValue = handerl.CreateETag(properties); - - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - IEdmModel model = builder.GetEdmModel(); - IEdmEntityType customer = model.SchemaElements.OfType().FirstOrDefault(e => e.Name == "MyEtagCustomer"); - IEdmEntitySet customers = model.FindDeclaredEntitySet("Customers"); - ODataPath odataPath = new ODataPath(new[] { new EntitySetSegment(customers) }); - var request = RequestFactory.Create(model, opt => opt.AddRouteComponents(model)); - request.ODataFeature().Path = odataPath; - - ETag etagCustomer = request.GetETag(etagHeaderValue); - etagCustomer.EntityType = typeof(MyETagCustomer); - etagCustomer.IsIfNoneMatch = !ifMatch; - - // Act - IQueryable queryable = etagCustomer.ApplyTo(myCustomers.AsQueryable()); - - // Assert - Assert.NotNull(queryable); - IList actualCustomers = Assert.IsAssignableFrom>(queryable).ToList(); - if (expect != null) + new MyETagOrder { - Assert.Equal(expect, actualCustomers.Select(c => c.ID)); - } - - MethodCallExpression methodCall = queryable.Expression as MethodCallExpression; - Assert.NotNull(methodCall); - Assert.Equal(2, methodCall.Arguments.Count); - if (ifMatch) + ID = 1, + ByteVal = 7, + ShortVal = 8 + }, + new MyETagOrder { - Assert.Equal( - "Param_0 => (Param_0.DoubleETag == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Double]).TypedProperty)", - methodCall.Arguments[1].ToString()); - } - else + ID = 2, + ByteVal = SByte.MaxValue, + ShortVal = Int16.MaxValue + }, + new MyETagOrder { - Assert.Equal( - "Param_0 => Not((Param_0.DoubleETag == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Double]).TypedProperty))", - methodCall.Arguments[1].ToString()); - } - } - - public class MyETagCustomer + ID = 3, + ByteVal = SByte.MinValue, + ShortVal = Int16.MinValue + }, + }; + IETagHandler handerl = new DefaultODataETagHandler(); + Dictionary properties = new Dictionary { - public int ID { get; set; } - - [ConcurrencyCheck] - public double DoubleETag { get; set; } + { "ByteVal", byteVal }, + { "ShortVal", shortVal } + }; + EntityTagHeaderValue etagHeaderValue = handerl.CreateETag(properties, null); + + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Orders"); + IEdmModel model = builder.GetEdmModel(); + IEdmEntitySet orders = model.FindDeclaredEntitySet("Orders"); + ODataPath odataPath = new ODataPath(new[] {new EntitySetSegment(orders) }); + var request = RequestFactory.Create(model, opt => opt.AddRouteComponents(model)); + request.ODataFeature().Path = odataPath; + + ETag etagCustomer = request.GetETag(etagHeaderValue); + etagCustomer.EntityType = typeof(MyETagOrder); + etagCustomer.IsIfNoneMatch = !ifMatch; + + // Act + IQueryable queryable = etagCustomer.ApplyTo(mycustomers.AsQueryable()); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualOrders = Assert.IsAssignableFrom>(queryable); + Assert.Equal(expect, actualOrders.Select(c => c.ID)); + MethodCallExpression methodCall = queryable.Expression as MethodCallExpression; + Assert.NotNull(methodCall); + Assert.Equal(2, methodCall.Arguments.Count); + + if (ifMatch) + { + Assert.Equal( + "Param_0 => ((Param_0.ByteVal == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.SByte]).TypedProperty) " + + "AndAlso (Param_0.ShortVal == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Int16]).TypedProperty))", + methodCall.Arguments[1].ToString()); } - - [Theory] - [InlineData((sbyte)1, (short)1, true, new int[] {})] - [InlineData((sbyte)1, (short)1, false, new[] { 1, 2, 3 })] - [InlineData(SByte.MaxValue, Int16.MaxValue, true, new[] { 2 })] - [InlineData(SByte.MaxValue, Int16.MaxValue, false, new[] { 1, 3 })] - [InlineData(SByte.MinValue, Int16.MinValue, true, new[] { 3 })] - [InlineData(SByte.MinValue, Int16.MinValue, false, new[] { 1, 2 })] - public void ApplyTo_NewQueryReturned_ForInteger(sbyte byteVal, short shortVal, bool ifMatch, IList expect) + else { - // Arrange - var mycustomers = new List - { - new MyETagOrder - { - ID = 1, - ByteVal = 7, - ShortVal = 8 - }, - new MyETagOrder - { - ID = 2, - ByteVal = SByte.MaxValue, - ShortVal = Int16.MaxValue - }, - new MyETagOrder - { - ID = 3, - ByteVal = SByte.MinValue, - ShortVal = Int16.MinValue - }, - }; - IETagHandler handerl = new DefaultODataETagHandler(); - Dictionary properties = new Dictionary - { - { "ByteVal", byteVal }, - { "ShortVal", shortVal } - }; - EntityTagHeaderValue etagHeaderValue = handerl.CreateETag(properties, null); - - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Orders"); - IEdmModel model = builder.GetEdmModel(); - IEdmEntitySet orders = model.FindDeclaredEntitySet("Orders"); - ODataPath odataPath = new ODataPath(new[] {new EntitySetSegment(orders) }); - var request = RequestFactory.Create(model, opt => opt.AddRouteComponents(model)); - request.ODataFeature().Path = odataPath; - - ETag etagCustomer = request.GetETag(etagHeaderValue); - etagCustomer.EntityType = typeof(MyETagOrder); - etagCustomer.IsIfNoneMatch = !ifMatch; - - // Act - IQueryable queryable = etagCustomer.ApplyTo(mycustomers.AsQueryable()); - - // Assert - Assert.NotNull(queryable); - IEnumerable actualOrders = Assert.IsAssignableFrom>(queryable); - Assert.Equal(expect, actualOrders.Select(c => c.ID)); - MethodCallExpression methodCall = queryable.Expression as MethodCallExpression; - Assert.NotNull(methodCall); - Assert.Equal(2, methodCall.Arguments.Count); - - if (ifMatch) - { - Assert.Equal( - "Param_0 => ((Param_0.ByteVal == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.SByte]).TypedProperty) " + - "AndAlso (Param_0.ShortVal == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Int16]).TypedProperty))", - methodCall.Arguments[1].ToString()); - } - else - { - Assert.Equal( - "Param_0 => Not(((Param_0.ByteVal == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.SByte]).TypedProperty) " + - "AndAlso (Param_0.ShortVal == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Int16]).TypedProperty)))", - methodCall.Arguments[1].ToString()); - } + Assert.Equal( + "Param_0 => Not(((Param_0.ByteVal == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.SByte]).TypedProperty) " + + "AndAlso (Param_0.ShortVal == value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Int16]).TypedProperty)))", + methodCall.Arguments[1].ToString()); } + } - public class MyETagOrder - { - public int ID { get; set; } + public class MyETagOrder + { + public int ID { get; set; } - [ConcurrencyCheck] - public sbyte ByteVal { get; set; } + [ConcurrencyCheck] + public sbyte ByteVal { get; set; } - [ConcurrencyCheck] - public short ShortVal { get; set; } - } + [ConcurrencyCheck] + public short ShortVal { get; set; } + } - public class ETagCustomer - { - public int ID { get; set; } - public string Name { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public string City { get; set; } - } + public class ETagCustomer + { + public int ID { get; set; } + public string Name { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string City { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/EnableQueryAttributeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/EnableQueryAttributeTests.cs index 52b79d5f7..eaf7f50aa 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/EnableQueryAttributeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/EnableQueryAttributeTests.cs @@ -27,1316 +27,1315 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class EnableQueryAttributeTests { - public class EnableQueryAttributeTests - { - private static IEdmModel _model = GetEdmModel(); + private static IEdmModel _model = GetEdmModel(); #if false - public static List CustomerList = new List() - { - new Customer(){ Name = "B" }, - new Customer(){ Name = "C" }, - new Customer(){ Name = "A" }, - }; + public static List CustomerList = new List() + { + new Customer(){ Name = "B" }, + new Customer(){ Name = "C" }, + new Customer(){ Name = "A" }, + }; - public static TheoryDataSet DifferentReturnTypeWorksTestData + public static TheoryDataSet DifferentReturnTypeWorksTestData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { "GetObject", new List(CustomerList), false }, - { "GetObject", new Collection(CustomerList), false }, - { "GetObject", new CustomerCollection(), false } - }; - } + { "GetObject", new List(CustomerList), false }, + { "GetObject", new Collection(CustomerList), false }, + { "GetObject", new CustomerCollection(), false } + }; } + } - public static TheoryDataSet SystemQueryOptionNames - { - get { return ODataQueryOptionTest.SystemQueryOptionNames; } - } + public static TheoryDataSet SystemQueryOptionNames + { + get { return ODataQueryOptionTest.SystemQueryOptionNames; } + } #endif - [Fact] - public void Ctor_Initializes_Properties() - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); + [Fact] + public void Ctor_Initializes_Properties() + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); - // Act & Assert - Assert.Equal(HandleNullPropagationOption.Default, attribute.HandleNullPropagation); - Assert.True(attribute.EnsureStableOrdering); - } + // Act & Assert + Assert.Equal(HandleNullPropagationOption.Default, attribute.HandleNullPropagation); + Assert.True(attribute.EnsureStableOrdering); + } - [Fact] - public void EnsureStableOrdering_Property_RoundTrips() - { - ReflectionAssert.BooleanProperty( - new EnableQueryAttribute(), - o => o.EnsureStableOrdering, - true); - } + [Fact] + public void EnsureStableOrdering_Property_RoundTrips() + { + ReflectionAssert.BooleanProperty( + new EnableQueryAttribute(), + o => o.EnsureStableOrdering, + true); + } - [Fact] - public void HandleNullPropagation_Property_RoundTrips() - { - ReflectionAssert.EnumProperty( - new EnableQueryAttribute(), - o => o.HandleNullPropagation, - HandleNullPropagationOption.Default, - HandleNullPropagationOption.Default - 1, - HandleNullPropagationOption.True); - } + [Fact] + public void HandleNullPropagation_Property_RoundTrips() + { + ReflectionAssert.EnumProperty( + new EnableQueryAttribute(), + o => o.HandleNullPropagation, + HandleNullPropagationOption.Default, + HandleNullPropagationOption.Default - 1, + HandleNullPropagationOption.True); + } - [Fact] - public void AllowedArithmeticOperators_Property_RoundTrips() - { - ReflectionAssert.EnumProperty( - new EnableQueryAttribute(), - o => o.AllowedArithmeticOperators, - AllowedArithmeticOperators.All, - AllowedArithmeticOperators.None - 1, - AllowedArithmeticOperators.Multiply); - } + [Fact] + public void AllowedArithmeticOperators_Property_RoundTrips() + { + ReflectionAssert.EnumProperty( + new EnableQueryAttribute(), + o => o.AllowedArithmeticOperators, + AllowedArithmeticOperators.All, + AllowedArithmeticOperators.None - 1, + AllowedArithmeticOperators.Multiply); + } - [Fact] - public void AllowedFunctions_Property_RoundTrips() - { - ReflectionAssert.EnumProperty( - new EnableQueryAttribute(), - o => o.AllowedFunctions, - AllowedFunctions.AllFunctions, - AllowedFunctions.None - 1, - AllowedFunctions.All); - } + [Fact] + public void AllowedFunctions_Property_RoundTrips() + { + ReflectionAssert.EnumProperty( + new EnableQueryAttribute(), + o => o.AllowedFunctions, + AllowedFunctions.AllFunctions, + AllowedFunctions.None - 1, + AllowedFunctions.All); + } - [Fact] - public void AllowedLogicalOperators_Property_RoundTrips() - { - ReflectionAssert.EnumProperty( - new EnableQueryAttribute(), - o => o.AllowedLogicalOperators, - AllowedLogicalOperators.All, - AllowedLogicalOperators.None - 1, - AllowedLogicalOperators.GreaterThanOrEqual); - } + [Fact] + public void AllowedLogicalOperators_Property_RoundTrips() + { + ReflectionAssert.EnumProperty( + new EnableQueryAttribute(), + o => o.AllowedLogicalOperators, + AllowedLogicalOperators.All, + AllowedLogicalOperators.None - 1, + AllowedLogicalOperators.GreaterThanOrEqual); + } - [Fact] - public void EnableConstantParameterization_Property_RoundTrips() - { - ReflectionAssert.BooleanProperty( - new EnableQueryAttribute(), - o => o.EnableConstantParameterization, - expectedDefaultValue: true); - } + [Fact] + public void EnableConstantParameterization_Property_RoundTrips() + { + ReflectionAssert.BooleanProperty( + new EnableQueryAttribute(), + o => o.EnableConstantParameterization, + expectedDefaultValue: true); + } - [Fact] - public void EnableCorrelatedSubqueryBuffering_Property_RoundTrips() - { - ReflectionAssert.BooleanProperty( - new EnableQueryAttribute(), - o => o.EnableCorrelatedSubqueryBuffering, - expectedDefaultValue: false); - } + [Fact] + public void EnableCorrelatedSubqueryBuffering_Property_RoundTrips() + { + ReflectionAssert.BooleanProperty( + new EnableQueryAttribute(), + o => o.EnableCorrelatedSubqueryBuffering, + expectedDefaultValue: false); + } - [Fact] - public void AllowedQueryOptions_Property_RoundTrips() - { - ReflectionAssert.EnumProperty( - new EnableQueryAttribute(), - o => o.AllowedQueryOptions, - AllowedQueryOptions.Supported, - AllowedQueryOptions.None - 1, - AllowedQueryOptions.All); - } + [Fact] + public void AllowedQueryOptions_Property_RoundTrips() + { + ReflectionAssert.EnumProperty( + new EnableQueryAttribute(), + o => o.AllowedQueryOptions, + AllowedQueryOptions.Supported, + AllowedQueryOptions.None - 1, + AllowedQueryOptions.All); + } - [Fact] - public void AllowedOrderByProperties_Property_RoundTrips() - { - ReflectionAssert.StringProperty( - new EnableQueryAttribute(), - o => o.AllowedOrderByProperties, - expectedDefaultValue: null, - allowNullAndEmpty: true, - treatNullAsEmpty: false); - } + [Fact] + public void AllowedOrderByProperties_Property_RoundTrips() + { + ReflectionAssert.StringProperty( + new EnableQueryAttribute(), + o => o.AllowedOrderByProperties, + expectedDefaultValue: null, + allowNullAndEmpty: true, + treatNullAsEmpty: false); + } - [Fact] - public void MaxAnyAllExpressionDepth_Property_RoundTrips() - { - ReflectionAssert.IntegerProperty( - new EnableQueryAttribute(), - o => o.MaxAnyAllExpressionDepth, - expectedDefaultValue: 1, - minLegalValue: 1, - illegalLowerValue: -1, - illegalUpperValue: null, - maxLegalValue: int.MaxValue, - roundTripTestValue: 2); - } + [Fact] + public void MaxAnyAllExpressionDepth_Property_RoundTrips() + { + ReflectionAssert.IntegerProperty( + new EnableQueryAttribute(), + o => o.MaxAnyAllExpressionDepth, + expectedDefaultValue: 1, + minLegalValue: 1, + illegalLowerValue: -1, + illegalUpperValue: null, + maxLegalValue: int.MaxValue, + roundTripTestValue: 2); + } - [Fact] - public void MaxNodeCount_Property_RoundTrips() - { - ReflectionAssert.IntegerProperty( - new EnableQueryAttribute(), - o => o.MaxNodeCount, - expectedDefaultValue: 100, - minLegalValue: 1, - maxLegalValue: int.MaxValue, - illegalLowerValue: 0, - illegalUpperValue: null, - roundTripTestValue: 2); - } + [Fact] + public void MaxNodeCount_Property_RoundTrips() + { + ReflectionAssert.IntegerProperty( + new EnableQueryAttribute(), + o => o.MaxNodeCount, + expectedDefaultValue: 100, + minLegalValue: 1, + maxLegalValue: int.MaxValue, + illegalLowerValue: 0, + illegalUpperValue: null, + roundTripTestValue: 2); + } - [Fact] - public void PageSize_Property_RoundTrips() - { - ReflectionAssert.IntegerProperty( - new EnableQueryAttribute(), - o => o.PageSize, - expectedDefaultValue: 0, - minLegalValue: 1, - illegalLowerValue: 0, - illegalUpperValue: null, - maxLegalValue: int.MaxValue, - roundTripTestValue: 2); - } + [Fact] + public void PageSize_Property_RoundTrips() + { + ReflectionAssert.IntegerProperty( + new EnableQueryAttribute(), + o => o.PageSize, + expectedDefaultValue: 0, + minLegalValue: 1, + illegalLowerValue: 0, + illegalUpperValue: null, + maxLegalValue: int.MaxValue, + roundTripTestValue: 2); + } - [Fact] - public void MaxExpansionDepth_Property_RoundTrips() - { - ReflectionAssert.IntegerProperty( - new EnableQueryAttribute(), - o => o.MaxExpansionDepth, - expectedDefaultValue: 2, - minLegalValue: 0, - illegalLowerValue: -1, - illegalUpperValue: null, - maxLegalValue: int.MaxValue, - roundTripTestValue: 100); - } + [Fact] + public void MaxExpansionDepth_Property_RoundTrips() + { + ReflectionAssert.IntegerProperty( + new EnableQueryAttribute(), + o => o.MaxExpansionDepth, + expectedDefaultValue: 2, + minLegalValue: 0, + illegalLowerValue: -1, + illegalUpperValue: null, + maxLegalValue: int.MaxValue, + roundTripTestValue: 100); + } - [Fact] - public void MaxOrderByNodeCount_Property_RoundTrips() - { - ReflectionAssert.IntegerProperty( - new EnableQueryAttribute(), - o => o.MaxOrderByNodeCount, - expectedDefaultValue: 5, - minLegalValue: 1, - illegalLowerValue: -1, - illegalUpperValue: null, - maxLegalValue: int.MaxValue, - roundTripTestValue: 100); - } + [Fact] + public void MaxOrderByNodeCount_Property_RoundTrips() + { + ReflectionAssert.IntegerProperty( + new EnableQueryAttribute(), + o => o.MaxOrderByNodeCount, + expectedDefaultValue: 5, + minLegalValue: 1, + illegalLowerValue: -1, + illegalUpperValue: null, + maxLegalValue: int.MaxValue, + roundTripTestValue: 100); + } - [Fact] - public void OnActionExecuted_Throws_Null_ActionExecutedContext() - { - ExceptionAssert.ThrowsArgumentNull(() => new EnableQueryAttribute().OnActionExecuted(null), "actionExecutedContext"); - } + [Fact] + public void OnActionExecuted_Throws_Null_ActionExecutedContext() + { + ExceptionAssert.ThrowsArgumentNull(() => new EnableQueryAttribute().OnActionExecuted(null), "actionExecutedContext"); + } - [Fact] - public void OnActionExecuted_HandlesStatusCodesCorrectly() - { - // Arrange - HttpContext httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "Get"; - ActionDescriptor actionDescriptor = new ActionDescriptor(); - ActionContext actionContext = new ActionContext(httpContext, new RouteData(), actionDescriptor); + [Fact] + public void OnActionExecuted_HandlesStatusCodesCorrectly() + { + // Arrange + HttpContext httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "Get"; + ActionDescriptor actionDescriptor = new ActionDescriptor(); + ActionContext actionContext = new ActionContext(httpContext, new RouteData(), actionDescriptor); - ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), "someController"); - context.Result = new ObjectResult(new { Error = "Error", Message = "Message" }) { StatusCode = 500 }; + ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), "someController"); + context.Result = new ObjectResult(new { Error = "Error", Message = "Message" }) { StatusCode = 500 }; - EnableQueryAttribute attribute = new EnableQueryAttribute(); + EnableQueryAttribute attribute = new EnableQueryAttribute(); - // Act and Assert - ExceptionAssert.DoesNotThrow(() => attribute.OnActionExecuted(context)); - } + // Act and Assert + ExceptionAssert.DoesNotThrow(() => attribute.OnActionExecuted(context)); + } - [Fact] - public void OnActionExecuted_HandlesRequestsNormally() - { - IEdmModel model = new CustomersModelWithInheritance().Model; + [Fact] + public void OnActionExecuted_HandlesRequestsNormally() + { + IEdmModel model = new CustomersModelWithInheritance().Model; - var request = RequestFactory.Create("Get", "http://localhost/Customers", opt => opt.AddRouteComponents(model).Filter()); + var request = RequestFactory.Create("Get", "http://localhost/Customers", opt => opt.AddRouteComponents(model).Filter()); - ODataUriParser parser = new ODataUriParser(model, new Uri("Customers", UriKind.Relative)); - HttpContext httpContext = request.HttpContext; + ODataUriParser parser = new ODataUriParser(model, new Uri("Customers", UriKind.Relative)); + HttpContext httpContext = request.HttpContext; - Assert.NotNull(httpContext.RequestServices); + Assert.NotNull(httpContext.RequestServices); - httpContext.Request.Method = "Get"; - request.Configure("odata", model, parser.ParsePath()); - ActionDescriptor actionDescriptor = CreateDescriptors("CustomersController", typeof(CustomersController)) - .First(descriptor => descriptor.ActionName.StartsWith("Get", StringComparison.OrdinalIgnoreCase)); - ActionContext actionContext = new ActionContext(httpContext, new RouteData(), actionDescriptor); + httpContext.Request.Method = "Get"; + request.Configure("odata", model, parser.ParsePath()); + ActionDescriptor actionDescriptor = CreateDescriptors("CustomersController", typeof(CustomersController)) + .First(descriptor => descriptor.ActionName.StartsWith("Get", StringComparison.OrdinalIgnoreCase)); + ActionContext actionContext = new ActionContext(httpContext, new RouteData(), actionDescriptor); - ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), new CustomersController()); - context.Result = new ObjectResult(new List() { new Customer { Id = 1, Name = "John Doe" } }) { StatusCode = 200 }; + ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), new CustomersController()); + context.Result = new ObjectResult(new List() { new Customer { Id = 1, Name = "John Doe" } }) { StatusCode = 200 }; - EnableQueryAttribute attribute = new EnableQueryAttribute(); + EnableQueryAttribute attribute = new EnableQueryAttribute(); - // Act and Assert - ExceptionAssert.DoesNotThrow(() => attribute.OnActionExecuted(context)); - var result = context.Result as ObjectResult; - Assert.NotNull(context.Result); - } + // Act and Assert + ExceptionAssert.DoesNotThrow(() => attribute.OnActionExecuted(context)); + var result = context.Result as ObjectResult; + Assert.NotNull(context.Result); + } - /// - /// Initializes a new instance of the [Http]ControllerDescriptor class. - /// - /// A new instance of the [Http]ControllerDescriptor class. - private static IEnumerable CreateDescriptors(string name, Type controllerType) - { - // Create descriptors. Search for non-public methods to pick up public methods in nested classes - // as controllers are usually a nested class for the test class ad by default, this are marked private. - List descriptors = new List(); - IEnumerable methods = controllerType.GetTypeInfo().DeclaredMethods; - foreach (MethodInfo methodInfo in methods) - { - ControllerActionDescriptor descriptor = new ControllerActionDescriptor(); - descriptor.ControllerName = name; - descriptor.ControllerTypeInfo = controllerType.GetTypeInfo(); - descriptor.ActionName = methodInfo.Name; - descriptor.DisplayName = methodInfo.Name; - descriptor.MethodInfo = methodInfo; - descriptor.Parameters = methodInfo - .GetParameters() - .Select(p => new ParameterDescriptor - { - Name = p.Name, - ParameterType = p.ParameterType - }) - .ToList(); - descriptors.Add(descriptor); - } - - return descriptors; + /// + /// Initializes a new instance of the [Http]ControllerDescriptor class. + /// + /// A new instance of the [Http]ControllerDescriptor class. + private static IEnumerable CreateDescriptors(string name, Type controllerType) + { + // Create descriptors. Search for non-public methods to pick up public methods in nested classes + // as controllers are usually a nested class for the test class ad by default, this are marked private. + List descriptors = new List(); + IEnumerable methods = controllerType.GetTypeInfo().DeclaredMethods; + foreach (MethodInfo methodInfo in methods) + { + ControllerActionDescriptor descriptor = new ControllerActionDescriptor(); + descriptor.ControllerName = name; + descriptor.ControllerTypeInfo = controllerType.GetTypeInfo(); + descriptor.ActionName = methodInfo.Name; + descriptor.DisplayName = methodInfo.Name; + descriptor.MethodInfo = methodInfo; + descriptor.Parameters = methodInfo + .GetParameters() + .Select(p => new ParameterDescriptor + { + Name = p.Name, + ParameterType = p.ParameterType + }) + .ToList(); + descriptors.Add(descriptor); } - [Fact] - public void Derived_Class_Can_Unit_Test_When_OnActionExecuted_Is_Overridden() - { - // https://github.com/OData/AspNetCoreOData/issues/239: unit tests for derived classes fail because RequestQueryOptions missing from HttpContext.Items - IEdmModel model = new CustomersModelWithInheritance().Model; + return descriptors; + } - var request = RequestFactory.Create("Get", "http://localhost/Customers?$filter=Id+eq+1", opt => opt.AddRouteComponents("odata", model, services => { }).Filter()); + [Fact] + public void Derived_Class_Can_Unit_Test_When_OnActionExecuted_Is_Overridden() + { + // https://github.com/OData/AspNetCoreOData/issues/239: unit tests for derived classes fail because RequestQueryOptions missing from HttpContext.Items + IEdmModel model = new CustomersModelWithInheritance().Model; - ODataUriParser parser = new ODataUriParser(model, new Uri("Customers", UriKind.Relative)); - HttpContext httpContext = request.HttpContext; + var request = RequestFactory.Create("Get", "http://localhost/Customers?$filter=Id+eq+1", opt => opt.AddRouteComponents("odata", model, services => { }).Filter()); - Assert.NotNull(httpContext.RequestServices); + ODataUriParser parser = new ODataUriParser(model, new Uri("Customers", UriKind.Relative)); + HttpContext httpContext = request.HttpContext; - httpContext.Request.Method = "Get"; - request.Configure("odata", model, parser.ParsePath()); - ActionDescriptor actionDescriptor = CreateDescriptors("CustomersController", typeof(CustomersController)) - .First(descriptor => descriptor.ActionName.StartsWith("Get", StringComparison.OrdinalIgnoreCase)); - ActionContext actionContext = new ActionContext(httpContext, new RouteData(), actionDescriptor); + Assert.NotNull(httpContext.RequestServices); - ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), new CustomersController()); - context.Result = new ObjectResult(new List() { new Customer { Id = 1, Name = "John Doe" } }) { StatusCode = 200 }; + httpContext.Request.Method = "Get"; + request.Configure("odata", model, parser.ParsePath()); + ActionDescriptor actionDescriptor = CreateDescriptors("CustomersController", typeof(CustomersController)) + .First(descriptor => descriptor.ActionName.StartsWith("Get", StringComparison.OrdinalIgnoreCase)); + ActionContext actionContext = new ActionContext(httpContext, new RouteData(), actionDescriptor); - MyEnableQueryAttribute attribute = new MyEnableQueryAttribute(); + ActionExecutedContext context = new ActionExecutedContext(actionContext, new List(), new CustomersController()); + context.Result = new ObjectResult(new List() { new Customer { Id = 1, Name = "John Doe" } }) { StatusCode = 200 }; - // Act and Assert - ExceptionAssert.DoesNotThrow(() => attribute.OnActionExecuted(context)); + MyEnableQueryAttribute attribute = new MyEnableQueryAttribute(); - } + // Act and Assert + ExceptionAssert.DoesNotThrow(() => attribute.OnActionExecuted(context)); - private class MyEnableQueryAttribute : EnableQueryAttribute - { - public override void OnActionExecuted(ActionExecutedContext actionExecutedContext) - { - base.OnActionExecuted(actionExecutedContext); - } - } + } - [Fact] - public void OnActionExecuting_Throws_Null_ActionExecutingContext() + private class MyEnableQueryAttribute : EnableQueryAttribute + { + public override void OnActionExecuted(ActionExecutedContext actionExecutedContext) { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EnableQueryAttribute().OnActionExecuting(null), "actionExecutingContext"); + base.OnActionExecuted(actionExecutedContext); } + } - [Fact] - public void OnActionExecuting_Calls_CreateQueryOptionsOnExecuting() - { - // Arrange - OverridQueryOptionEnableQueryAttribute enableQueryAttribute = new OverridQueryOptionEnableQueryAttribute(); + [Fact] + public void OnActionExecuting_Throws_Null_ActionExecutingContext() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EnableQueryAttribute().OnActionExecuting(null), "actionExecutingContext"); + } - ActionExecutingContext context = CreateDefaultActionExecutingContext(); + [Fact] + public void OnActionExecuting_Calls_CreateQueryOptionsOnExecuting() + { + // Arrange + OverridQueryOptionEnableQueryAttribute enableQueryAttribute = new OverridQueryOptionEnableQueryAttribute(); - // Act - Assert.False(enableQueryAttribute.Called); // Guard - enableQueryAttribute.OnActionExecuting(context); + ActionExecutingContext context = CreateDefaultActionExecutingContext(); - // Assert - Assert.True(enableQueryAttribute.Called); - } + // Act + Assert.False(enableQueryAttribute.Called); // Guard + enableQueryAttribute.OnActionExecuting(context); - private class OverridQueryOptionEnableQueryAttribute : EnableQueryAttribute - { - public bool Called { get; set; } = false; + // Assert + Assert.True(enableQueryAttribute.Called); + } - protected override ODataQueryOptions CreateQueryOptionsOnExecuting(ActionExecutingContext actionExecutingContext) - { - Called = true; - return null; - } - } + private class OverridQueryOptionEnableQueryAttribute : EnableQueryAttribute + { + public bool Called { get; set; } = false; -#if false // TODO #939: Enable these test on AspNetCore. - [Fact] - public void OnActionExecuted_Throws_Null_Request() + protected override ODataQueryOptions CreateQueryOptionsOnExecuting(ActionExecutingContext actionExecutingContext) { - ExceptionAssert.ThrowsArgument( - () => new EnableQueryAttribute().OnActionExecuted(new HttpActionExecutedContext()), - "actionExecutedContext", - String.Format("The HttpExecutedActionContext.Request is null.{0}Parameter name: actionExecutedContext", Environment.NewLine)); + Called = true; + return null; } + } - [Fact] - public void OnActionExecuted_Throws_Null_Configuration() - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer/?$orderby=Name"); - var config = RoutingConfigurationFactory.Create(); - HttpControllerContext controllerContext = new HttpControllerContext(config, new HttpRouteData(new HttpRoute()), request); - HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "CustomerHighLevel", typeof(CustomerHighLevelController)); - HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(CustomerHighLevelController).GetMethod("Get")); - HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); - HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); - - ExceptionAssert.ThrowsArgument( - () => new EnableQueryAttribute().OnActionExecuted(context), - "actionExecutedContext", - String.Format("Request message does not contain an HttpConfiguration object.{0}Parameter name: actionExecutedContext", Environment.NewLine)); - } +#if false // TODO #939: Enable these test on AspNetCore. + [Fact] + public void OnActionExecuted_Throws_Null_Request() + { + ExceptionAssert.ThrowsArgument( + () => new EnableQueryAttribute().OnActionExecuted(new HttpActionExecutedContext()), + "actionExecutedContext", + String.Format("The HttpExecutedActionContext.Request is null.{0}Parameter name: actionExecutedContext", Environment.NewLine)); + } - [Theory] - [MemberData(nameof(DifferentReturnTypeWorksTestData))] - public void DifferentReturnTypeWorks(string methodName, object responseObject, bool isNoOp) - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer?$orderby=Name"); - request.EnableODataDependencyInjectionSupport(); - request.GetConfiguration().Count().OrderBy().Filter().Expand().MaxTop(null); - HttpControllerContext controllerContext = new HttpControllerContext(request.GetConfiguration(), new HttpRouteData(new HttpRoute()), request); - HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "CustomerHighLevel", typeof(CustomerHighLevelController)); - HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(CustomerHighLevelController).GetMethod(methodName)); - HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); - HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); - context.Response = new HttpResponseMessage(HttpStatusCode.OK); - context.Response.Content = new ObjectContent(typeof(IEnumerable), responseObject, new JsonMediaTypeFormatter()); - - // Act and Assert - attribute.OnActionExecuted(context); - - Assert.Equal(HttpStatusCode.OK, context.Response.StatusCode); - Assert.True(context.Response.Content is ObjectContent); - Assert.Equal(isNoOp, ((ObjectContent)context.Response.Content).Value == responseObject); - } + [Fact] + public void OnActionExecuted_Throws_Null_Configuration() + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer/?$orderby=Name"); + var config = RoutingConfigurationFactory.Create(); + HttpControllerContext controllerContext = new HttpControllerContext(config, new HttpRouteData(new HttpRoute()), request); + HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "CustomerHighLevel", typeof(CustomerHighLevelController)); + HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(CustomerHighLevelController).GetMethod("Get")); + HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); + HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); + + ExceptionAssert.ThrowsArgument( + () => new EnableQueryAttribute().OnActionExecuted(context), + "actionExecutedContext", + String.Format("Request message does not contain an HttpConfiguration object.{0}Parameter name: actionExecutedContext", Environment.NewLine)); + } - [Fact] - public void CountValueReturnsAsContent_CountRequest() - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequestMessage request = new HttpRequestMessage( - HttpMethod.Get, - "http://localhost/DollarCountEntities(5)/StringCollectionProp/$count"); - request.ODataProperties().Path = new ODataPath(CountSegment.Instance); - request.EnableODataDependencyInjectionSupport(); - HttpControllerContext controllerContext = new HttpControllerContext( - request.GetConfiguration(), - new HttpRouteData(new HttpRoute()), - request); - HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor( - new HttpConfiguration(), - "DollarCountEntities", - typeof(ODataCountTest.DollarCountEntitiesController)); - HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor( - controllerDescriptor, - typeof(ODataCountTest.DollarCountEntitiesController).GetMethod("GetStringCollectionProp")); - HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); - HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); - context.Response = new HttpResponseMessage(HttpStatusCode.OK); - context.Response.Content = new ObjectContent( - typeof(IEnumerable), - new[] { "123", "abc", "A1B2" }, - new JsonMediaTypeFormatter()); - - // Act - attribute.OnActionExecuted(context); - - // Assert - Assert.Equal(HttpStatusCode.OK, context.Response.StatusCode); - Assert.True(context.Response.Content is ObjectContent); - Assert.Equal(3L, ((ObjectContent)context.Response.Content).Value); - } + [Theory] + [MemberData(nameof(DifferentReturnTypeWorksTestData))] + public void DifferentReturnTypeWorks(string methodName, object responseObject, bool isNoOp) + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer?$orderby=Name"); + request.EnableODataDependencyInjectionSupport(); + request.GetConfiguration().Count().OrderBy().Filter().Expand().MaxTop(null); + HttpControllerContext controllerContext = new HttpControllerContext(request.GetConfiguration(), new HttpRouteData(new HttpRoute()), request); + HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "CustomerHighLevel", typeof(CustomerHighLevelController)); + HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(CustomerHighLevelController).GetMethod(methodName)); + HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); + HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); + context.Response = new HttpResponseMessage(HttpStatusCode.OK); + context.Response.Content = new ObjectContent(typeof(IEnumerable), responseObject, new JsonMediaTypeFormatter()); + + // Act and Assert + attribute.OnActionExecuted(context); + + Assert.Equal(HttpStatusCode.OK, context.Response.StatusCode); + Assert.True(context.Response.Content is ObjectContent); + Assert.Equal(isNoOp, ((ObjectContent)context.Response.Content).Value == responseObject); + } - [Fact] - public void UnknownQueryNotStartingWithDollarSignWorks() - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer/?select"); - - // Enable DI with default resolver. - request.EnableODataDependencyInjectionSupport("default", - b => b.AddService(ServiceLifetime.Singleton, sp => new ODataUriResolver())); - - HttpControllerContext controllerContext = new HttpControllerContext(request.GetConfiguration(), new HttpRouteData(new HttpRoute()), request); - HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "CustomerHighLevel", typeof(CustomerHighLevelController)); - HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(CustomerHighLevelController).GetMethod("Get")); - HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); - HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); - context.Response = new HttpResponseMessage(HttpStatusCode.OK); - context.Response.Content = new ObjectContent(typeof(IEnumerable), new List(), new JsonMediaTypeFormatter()); - - // Act and Assert - attribute.OnActionExecuted(context); - - Assert.Equal(HttpStatusCode.OK, context.Response.StatusCode); - } + [Fact] + public void CountValueReturnsAsContent_CountRequest() + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequestMessage request = new HttpRequestMessage( + HttpMethod.Get, + "http://localhost/DollarCountEntities(5)/StringCollectionProp/$count"); + request.ODataProperties().Path = new ODataPath(CountSegment.Instance); + request.EnableODataDependencyInjectionSupport(); + HttpControllerContext controllerContext = new HttpControllerContext( + request.GetConfiguration(), + new HttpRouteData(new HttpRoute()), + request); + HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor( + new HttpConfiguration(), + "DollarCountEntities", + typeof(ODataCountTest.DollarCountEntitiesController)); + HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor( + controllerDescriptor, + typeof(ODataCountTest.DollarCountEntitiesController).GetMethod("GetStringCollectionProp")); + HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); + HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); + context.Response = new HttpResponseMessage(HttpStatusCode.OK); + context.Response.Content = new ObjectContent( + typeof(IEnumerable), + new[] { "123", "abc", "A1B2" }, + new JsonMediaTypeFormatter()); + + // Act + attribute.OnActionExecuted(context); + + // Assert + Assert.Equal(HttpStatusCode.OK, context.Response.StatusCode); + Assert.True(context.Response.Content is ObjectContent); + Assert.Equal(3L, ((ObjectContent)context.Response.Content).Value); + } - [Fact] - public void UnknownQueryStartingWithDollarSignThrows() - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer/?$custom"); - request.EnableODataDependencyInjectionSupport(); - HttpControllerContext controllerContext = new HttpControllerContext(request.GetConfiguration(), new HttpRouteData(new HttpRoute()), request); - HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "CustomerHighLevel", typeof(CustomerHighLevelController)); - HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(CustomerHighLevelController).GetMethod("Get")); - HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); - HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); - context.Response = new HttpResponseMessage(HttpStatusCode.OK); - context.Response.Content = new ObjectContent(typeof(IEnumerable), new List(), new JsonMediaTypeFormatter()); - - // Act and Assert - HttpResponseException errorResponse = ExceptionAssert.Throws(() => - attribute.OnActionExecuted(context)); - - Assert.Equal(HttpStatusCode.BadRequest, errorResponse.Response.StatusCode); - } + [Fact] + public void UnknownQueryNotStartingWithDollarSignWorks() + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer/?select"); + + // Enable DI with default resolver. + request.EnableODataDependencyInjectionSupport("default", + b => b.AddService(ServiceLifetime.Singleton, sp => new ODataUriResolver())); + + HttpControllerContext controllerContext = new HttpControllerContext(request.GetConfiguration(), new HttpRouteData(new HttpRoute()), request); + HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "CustomerHighLevel", typeof(CustomerHighLevelController)); + HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(CustomerHighLevelController).GetMethod("Get")); + HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); + HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); + context.Response = new HttpResponseMessage(HttpStatusCode.OK); + context.Response.Content = new ObjectContent(typeof(IEnumerable), new List(), new JsonMediaTypeFormatter()); + + // Act and Assert + attribute.OnActionExecuted(context); + + Assert.Equal(HttpStatusCode.OK, context.Response.StatusCode); + } - [Fact] - public async Task NonGenericEnumerableReturnType_ReturnsBadRequest() - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer/?$skip=1"); - var config = RoutingConfigurationFactory.Create(); - config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always; - request.SetConfiguration(config); - HttpControllerContext controllerContext = new HttpControllerContext(config, new HttpRouteData(new HttpRoute()), request); - HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "CustomerHighLevel", typeof(CustomerHighLevelController)); - HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(CustomerHighLevelController).GetMethod("GetNonGenericEnumerable")); - HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); - HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); - context.Response = new HttpResponseMessage(HttpStatusCode.OK); - context.Response.Content = new ObjectContent(typeof(IEnumerable), new NonGenericEnumerable(), new JsonMediaTypeFormatter()); - - // Act - attribute.OnActionExecuted(context); - string responseString = await context.Response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.BadRequest, context.Response.StatusCode); - Assert.Contains("The query specified in the URI is not valid. Cannot create an EDM model as the action 'EnableQueryAttribute' " + - "on controller 'GetNonGenericEnumerable' has a return type 'CustomerHighLevel' that does not implement IEnumerable.", - responseString); - } + [Fact] + public void UnknownQueryStartingWithDollarSignThrows() + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer/?$custom"); + request.EnableODataDependencyInjectionSupport(); + HttpControllerContext controllerContext = new HttpControllerContext(request.GetConfiguration(), new HttpRouteData(new HttpRoute()), request); + HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "CustomerHighLevel", typeof(CustomerHighLevelController)); + HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(CustomerHighLevelController).GetMethod("Get")); + HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); + HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); + context.Response = new HttpResponseMessage(HttpStatusCode.OK); + context.Response.Content = new ObjectContent(typeof(IEnumerable), new List(), new JsonMediaTypeFormatter()); + + // Act and Assert + HttpResponseException errorResponse = ExceptionAssert.Throws(() => + attribute.OnActionExecuted(context)); + + Assert.Equal(HttpStatusCode.BadRequest, errorResponse.Response.StatusCode); + } - [Fact] - public void NonObjectContentResponse_ThrowsArgumentException() - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer?$skip=1"); - var config = RoutingConfigurationFactory.Create(); - request.SetConfiguration(config); - HttpControllerContext controllerContext = new HttpControllerContext(config, new HttpRouteData(new HttpRoute()), request); - HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "CustomerHighLevel", typeof(CustomerHighLevelController)); - HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(CustomerHighLevelController).GetMethod("GetIEnumerableOfCustomer")); - HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); - HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); - context.Response = new HttpResponseMessage(HttpStatusCode.OK); - context.Response.Content = new StreamContent(new MemoryStream()); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => attribute.OnActionExecuted(context), - "actionExecutedContext", - "Queries can not be applied to a response content of type 'System.Net.Http.StreamContent'. The response content must be an ObjectContent."); - } + [Fact] + public async Task NonGenericEnumerableReturnType_ReturnsBadRequest() + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer/?$skip=1"); + var config = RoutingConfigurationFactory.Create(); + config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always; + request.SetConfiguration(config); + HttpControllerContext controllerContext = new HttpControllerContext(config, new HttpRouteData(new HttpRoute()), request); + HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "CustomerHighLevel", typeof(CustomerHighLevelController)); + HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(CustomerHighLevelController).GetMethod("GetNonGenericEnumerable")); + HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); + HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); + context.Response = new HttpResponseMessage(HttpStatusCode.OK); + context.Response.Content = new ObjectContent(typeof(IEnumerable), new NonGenericEnumerable(), new JsonMediaTypeFormatter()); + + // Act + attribute.OnActionExecuted(context); + string responseString = await context.Response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, context.Response.StatusCode); + Assert.Contains("The query specified in the URI is not valid. Cannot create an EDM model as the action 'EnableQueryAttribute' " + + "on controller 'GetNonGenericEnumerable' has a return type 'CustomerHighLevel' that does not implement IEnumerable.", + responseString); + } - [Fact] - public void NullContentResponse_DoesNotThrow() - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer?$skip=1"); - var config = RoutingConfigurationFactory.Create(); - request.SetConfiguration(config); - HttpControllerContext controllerContext = new HttpControllerContext( - config, - new HttpRouteData(new HttpRoute()), - request); - HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor( - new HttpConfiguration(), - "CustomerHighLevel", - typeof(CustomerHighLevelController)); - HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor( - controllerDescriptor, - typeof(CustomerHighLevelController).GetMethod("GetIEnumerableOfCustomer")); - HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); - HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null) - { - Response = new HttpResponseMessage(HttpStatusCode.OK) { Content = null } - }; + [Fact] + public void NonObjectContentResponse_ThrowsArgumentException() + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer?$skip=1"); + var config = RoutingConfigurationFactory.Create(); + request.SetConfiguration(config); + HttpControllerContext controllerContext = new HttpControllerContext(config, new HttpRouteData(new HttpRoute()), request); + HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "CustomerHighLevel", typeof(CustomerHighLevelController)); + HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(CustomerHighLevelController).GetMethod("GetIEnumerableOfCustomer")); + HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); + HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); + context.Response = new HttpResponseMessage(HttpStatusCode.OK); + context.Response.Content = new StreamContent(new MemoryStream()); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => attribute.OnActionExecuted(context), + "actionExecutedContext", + "Queries can not be applied to a response content of type 'System.Net.Http.StreamContent'. The response content must be an ObjectContent."); + } - // Act & Assert - ExceptionAssert.DoesNotThrow(() => attribute.OnActionExecuted(context)); - } + [Fact] + public void NullContentResponse_DoesNotThrow() + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customer?$skip=1"); + var config = RoutingConfigurationFactory.Create(); + request.SetConfiguration(config); + HttpControllerContext controllerContext = new HttpControllerContext( + config, + new HttpRouteData(new HttpRoute()), + request); + HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor( + new HttpConfiguration(), + "CustomerHighLevel", + typeof(CustomerHighLevelController)); + HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor( + controllerDescriptor, + typeof(CustomerHighLevelController).GetMethod("GetIEnumerableOfCustomer")); + HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); + HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null) + { + Response = new HttpResponseMessage(HttpStatusCode.OK) { Content = null } + }; - [Theory] - [InlineData("$top=1")] - [InlineData("$skip=1")] - public void Primitives_Can_Be_Used_For_Top_And_Skip(string filter) - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Primitive/?" + filter); - request.EnableODataDependencyInjectionSupport(); - HttpControllerContext controllerContext = new HttpControllerContext(request.GetConfiguration(), new HttpRouteData(new HttpRoute()), request); - HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "Primitive", typeof(PrimitiveController)); - HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(PrimitiveController).GetMethod("GetIEnumerableOfInt")); - HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); - HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); - context.Response = new HttpResponseMessage(HttpStatusCode.OK); - HttpContent expectedResponse = new ObjectContent(typeof(IEnumerable), new List(), new JsonMediaTypeFormatter()); - context.Response.Content = expectedResponse; - - // Act and Assert - attribute.OnActionExecuted(context); - HttpResponseMessage response = context.Response; - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(expectedResponse, response.Content); - } + // Act & Assert + ExceptionAssert.DoesNotThrow(() => attribute.OnActionExecuted(context)); + } + + [Theory] + [InlineData("$top=1")] + [InlineData("$skip=1")] + public void Primitives_Can_Be_Used_For_Top_And_Skip(string filter) + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Primitive/?" + filter); + request.EnableODataDependencyInjectionSupport(); + HttpControllerContext controllerContext = new HttpControllerContext(request.GetConfiguration(), new HttpRouteData(new HttpRoute()), request); + HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "Primitive", typeof(PrimitiveController)); + HttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor(controllerDescriptor, typeof(PrimitiveController).GetMethod("GetIEnumerableOfInt")); + HttpActionContext actionContext = new HttpActionContext(controllerContext, actionDescriptor); + HttpActionExecutedContext context = new HttpActionExecutedContext(actionContext, null); + context.Response = new HttpResponseMessage(HttpStatusCode.OK); + HttpContent expectedResponse = new ObjectContent(typeof(IEnumerable), new List(), new JsonMediaTypeFormatter()); + context.Response.Content = expectedResponse; + + // Act and Assert + attribute.OnActionExecuted(context); + HttpResponseMessage response = context.Response; + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expectedResponse, response.Content); + } #endif - [Fact] - public void ValidateQuery_Throws_With_Null_Request() - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequest request = new DefaultHttpContext().Request; - var options = new ODataQueryOptions(new ODataQueryContext(EdmCoreModel.Instance, typeof(int)), request); + [Fact] + public void ValidateQuery_Throws_With_Null_Request() + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequest request = new DefaultHttpContext().Request; + var options = new ODataQueryOptions(new ODataQueryContext(EdmCoreModel.Instance, typeof(int)), request); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => attribute.ValidateQuery(null, options), "request"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => attribute.ValidateQuery(null, options), "request"); + } - [Fact] - public void ValidateQuery_Throws_WithNullQueryOptions() - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequest request = new DefaultHttpContext().Request; + [Fact] + public void ValidateQuery_Throws_WithNullQueryOptions() + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequest request = new DefaultHttpContext().Request; - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => attribute.ValidateQuery(request, null), "queryOptions"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => attribute.ValidateQuery(request, null), "queryOptions"); + } - [Theory] - [InlineData("$filter=Name eq 'abc'")] - [InlineData("$orderby=Name")] - [InlineData("$skip=3")] - [InlineData("$top=2")] - public void ValidateQuery_Accepts_All_Supported_QueryNames(string query) - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequest request = RequestFactory.Create("Get", "http://localhost/?" + query); - - var context = new ODataQueryContext(_model, typeof(QCustomer)); - context.DefaultQueryConfigurations.EnableFilter = true; - context.DefaultQueryConfigurations.EnableOrderBy = true; - context.DefaultQueryConfigurations.MaxTop = null; - var options = new ODataQueryOptions(context, request); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => attribute.ValidateQuery(request, options)); - } + [Theory] + [InlineData("$filter=Name eq 'abc'")] + [InlineData("$orderby=Name")] + [InlineData("$skip=3")] + [InlineData("$top=2")] + public void ValidateQuery_Accepts_All_Supported_QueryNames(string query) + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequest request = RequestFactory.Create("Get", "http://localhost/?" + query); + + var context = new ODataQueryContext(_model, typeof(QCustomer)); + context.DefaultQueryConfigurations.EnableFilter = true; + context.DefaultQueryConfigurations.EnableOrderBy = true; + context.DefaultQueryConfigurations.MaxTop = null; + var options = new ODataQueryOptions(context, request); + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => attribute.ValidateQuery(request, options)); + } - [Fact] - public void ValidateQuery_ThrowsODataException_For_Unrecognized_QueryNames() - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequest request = RequestFactory.Create("Get", "http://localhost/?$xxx"); - var options = new ODataQueryOptions(new ODataQueryContext(_model, typeof(QCustomer)), request); + [Fact] + public void ValidateQuery_ThrowsODataException_For_Unrecognized_QueryNames() + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequest request = RequestFactory.Create("Get", "http://localhost/?$xxx"); + var options = new ODataQueryOptions(new ODataQueryContext(_model, typeof(QCustomer)), request); - // Act & Assert - ODataException ex = ExceptionAssert.Throws( - () => attribute.ValidateQuery(request, options)); + // Act & Assert + ODataException ex = ExceptionAssert.Throws( + () => attribute.ValidateQuery(request, options)); - Assert.Equal("Custom query option '$xxx' that starts with '$' is not supported.", ex.Message); - } + Assert.Equal("Custom query option '$xxx' that starts with '$' is not supported.", ex.Message); + } - [Fact] - public void ValidateQuery_Can_Override_Base() - { - // Arrange - Mock mockAttribute = new Mock(); - mockAttribute.Setup( - m => m.ValidateQuery(It.IsAny(), It.IsAny())).Callback(() => { }).Verifiable(); - - // Act & Assert - mockAttribute.Object.ValidateQuery(null, null); - mockAttribute.Verify(); - } + [Fact] + public void ValidateQuery_Can_Override_Base() + { + // Arrange + Mock mockAttribute = new Mock(); + mockAttribute.Setup( + m => m.ValidateQuery(It.IsAny(), It.IsAny())).Callback(() => { }).Verifiable(); + + // Act & Assert + mockAttribute.Object.ValidateQuery(null, null); + mockAttribute.Verify(); + } #if false - [Fact] - public void ApplyQuery_Throws_With_Null_Queryable() - { - // Arrange - var message = RequestFactory.Create(); - message.EnableHttpDependencyInjectionSupport(); - EnableQueryAttribute attribute = new EnableQueryAttribute(); - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Builder.TestModels.Customer)), message); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => attribute.ApplyQuery(null, options), "queryable"); - } + [Fact] + public void ApplyQuery_Throws_With_Null_Queryable() + { + // Arrange + var message = RequestFactory.Create(); + message.EnableHttpDependencyInjectionSupport(); + EnableQueryAttribute attribute = new EnableQueryAttribute(); + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Builder.TestModels.Customer)), message); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => attribute.ApplyQuery(null, options), "queryable"); + } - [Fact] - public void ApplyQuery_Throws_WithNullQueryOptions() - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); + [Fact] + public void ApplyQuery_Throws_WithNullQueryOptions() + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => attribute.ApplyQuery(CustomerList.AsQueryable(), null), "queryOptions"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => attribute.ApplyQuery(CustomerList.AsQueryable(), null), "queryOptions"); + } - [Theory] - [InlineData("$filter=Name eq 'abc'")] - [InlineData("$orderby=Name")] - [InlineData("$skip=3")] - [InlineData("$top=2")] - public void ApplyQuery_Accepts_All_Supported_QueryNames(string query) - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/?" + query); - request.EnableHttpDependencyInjectionSupport(); - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Builder.TestModels.Customer)), request); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => attribute.ApplyQuery(new List().AsQueryable(), options)); - } + [Theory] + [InlineData("$filter=Name eq 'abc'")] + [InlineData("$orderby=Name")] + [InlineData("$skip=3")] + [InlineData("$top=2")] + public void ApplyQuery_Accepts_All_Supported_QueryNames(string query) + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/?" + query); + request.EnableHttpDependencyInjectionSupport(); + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Builder.TestModels.Customer)), request); + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => attribute.ApplyQuery(new List().AsQueryable(), options)); + } - [Fact] - public void ApplyQuery_Can_Override_Base() - { - // Arrange - Mock mockAttribute = new Mock(); - IQueryable result = CustomerList.AsQueryable(); - mockAttribute.Setup(m => m.ApplyQuery(It.IsAny(), It.IsAny())) - .Returns(result); - mockAttribute.CallBase = false; - - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/?$top=2"); - request.EnableHttpDependencyInjectionSupport(); - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Builder.TestModels.Customer)), request); - - // Act & Assert - Assert.Same(result, mockAttribute.Object.ApplyQuery(result, options)); - } + [Fact] + public void ApplyQuery_Can_Override_Base() + { + // Arrange + Mock mockAttribute = new Mock(); + IQueryable result = CustomerList.AsQueryable(); + mockAttribute.Setup(m => m.ApplyQuery(It.IsAny(), It.IsAny())) + .Returns(result); + mockAttribute.CallBase = false; + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/?$top=2"); + request.EnableHttpDependencyInjectionSupport(); + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Builder.TestModels.Customer)), request); + + // Act & Assert + Assert.Same(result, mockAttribute.Object.ApplyQuery(result, options)); + } - [Theory] - [InlineData("Id,Address")] - [InlineData(" Id,Address ")] - [InlineData(" Id , Address ")] - [InlineData("Id, Address")] - public void OrderByDisllowedPropertiesWithSpaces(string allowedProperties) - { - EnableQueryAttribute attribute = new EnableQueryAttribute(); - attribute.AllowedOrderByProperties = allowedProperties; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customers/?$orderby=Id,Name"); - request.EnableHttpDependencyInjectionSupport(); - ODataQueryOptions queryOptions = new ODataQueryOptions(ValidationTestHelper.CreateCustomerContext(false), request); - - ExceptionAssert.Throws(() => attribute.ValidateQuery(request, queryOptions), - "Order by 'Name' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); - } + [Theory] + [InlineData("Id,Address")] + [InlineData(" Id,Address ")] + [InlineData(" Id , Address ")] + [InlineData("Id, Address")] + public void OrderByDisllowedPropertiesWithSpaces(string allowedProperties) + { + EnableQueryAttribute attribute = new EnableQueryAttribute(); + attribute.AllowedOrderByProperties = allowedProperties; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customers/?$orderby=Id,Name"); + request.EnableHttpDependencyInjectionSupport(); + ODataQueryOptions queryOptions = new ODataQueryOptions(ValidationTestHelper.CreateCustomerContext(false), request); + + ExceptionAssert.Throws(() => attribute.ValidateQuery(request, queryOptions), + "Order by 'Name' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); + } - [Theory] - [InlineData("Id,Name")] - [InlineData(" Id,Name ")] - [InlineData(" Id , Name ")] - [InlineData("Id, Name")] - [InlineData("")] - [InlineData(null)] - public void OrderByAllowedPropertiesWithSpaces(string allowedProperties) - { - EnableQueryAttribute attribute = new EnableQueryAttribute(); - attribute.AllowedOrderByProperties = allowedProperties; - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customers/?$orderby=Id,Name"); - var config = RoutingConfigurationFactory.Create(); - config.Count().OrderBy().Filter().Expand().MaxTop(null); - request.SetConfiguration(config); - request.EnableHttpDependencyInjectionSupport(); - - ODataQueryContext context = ValidationTestHelper.CreateCustomerContext(false); - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - - ExceptionAssert.DoesNotThrow(() => attribute.ValidateQuery(request, queryOptions)); - } + [Theory] + [InlineData("Id,Name")] + [InlineData(" Id,Name ")] + [InlineData(" Id , Name ")] + [InlineData("Id, Name")] + [InlineData("")] + [InlineData(null)] + public void OrderByAllowedPropertiesWithSpaces(string allowedProperties) + { + EnableQueryAttribute attribute = new EnableQueryAttribute(); + attribute.AllowedOrderByProperties = allowedProperties; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Customers/?$orderby=Id,Name"); + var config = RoutingConfigurationFactory.Create(); + config.Count().OrderBy().Filter().Expand().MaxTop(null); + request.SetConfiguration(config); + request.EnableHttpDependencyInjectionSupport(); + + ODataQueryContext context = ValidationTestHelper.CreateCustomerContext(false); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + + ExceptionAssert.DoesNotThrow(() => attribute.ValidateQuery(request, queryOptions)); + } - [Fact] - public void GetModel_ReturnsModel_ForNoModelOnRequest() - { - var entityClrType = typeof(QueryCompositionCustomer); - var config = new HttpConfiguration(); - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/"); - request.EnableHttpDependencyInjectionSupport(); - var descriptor = new ReflectedHttpActionDescriptor(); - descriptor.Configuration = config; - - var queryModel = new EnableQueryAttribute().GetModel(entityClrType, request, descriptor); - - Assert.NotNull(queryModel); - Assert.Same(descriptor.Properties["Microsoft.AspNet.OData.Model+Microsoft.AspNet.OData.Test.Query.QueryCompositionCustomer"], - queryModel); - } + [Fact] + public void GetModel_ReturnsModel_ForNoModelOnRequest() + { + var entityClrType = typeof(QueryCompositionCustomer); + var config = new HttpConfiguration(); + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/"); + request.EnableHttpDependencyInjectionSupport(); + var descriptor = new ReflectedHttpActionDescriptor(); + descriptor.Configuration = config; + + var queryModel = new EnableQueryAttribute().GetModel(entityClrType, request, descriptor); + + Assert.NotNull(queryModel); + Assert.Same(descriptor.Properties["Microsoft.AspNet.OData.Model+Microsoft.AspNet.OData.Test.Query.QueryCompositionCustomer"], + queryModel); + } - [Fact] - public void CreateQueryContext_ReturnsQueryContext_ForNonMatchingModelOnRequest() - { - var builder = ODataConventionModelBuilderFactory.Create(); - var model = builder.GetEdmModel(); - var entityClrType = typeof(QueryCompositionCustomer); - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/"); - request.EnableHttpDependencyInjectionSupport(model); - var descriptor = new ReflectedHttpActionDescriptor(); - descriptor.Configuration = request.GetConfiguration(); - - var queryModel = new EnableQueryAttribute().GetModel(entityClrType, request, descriptor); - - Assert.NotNull(queryModel); - Assert.Same(descriptor.Properties["Microsoft.AspNet.OData.Model+Microsoft.AspNet.OData.Test.Query.QueryCompositionCustomer"], - queryModel); - } + [Fact] + public void CreateQueryContext_ReturnsQueryContext_ForNonMatchingModelOnRequest() + { + var builder = ODataConventionModelBuilderFactory.Create(); + var model = builder.GetEdmModel(); + var entityClrType = typeof(QueryCompositionCustomer); + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/"); + request.EnableHttpDependencyInjectionSupport(model); + var descriptor = new ReflectedHttpActionDescriptor(); + descriptor.Configuration = request.GetConfiguration(); + + var queryModel = new EnableQueryAttribute().GetModel(entityClrType, request, descriptor); + + Assert.NotNull(queryModel); + Assert.Same(descriptor.Properties["Microsoft.AspNet.OData.Model+Microsoft.AspNet.OData.Test.Query.QueryCompositionCustomer"], + queryModel); + } - [Fact] - public void CreateQueryContext_ReturnsQueryContext_ForMatchingModelOnRequest() - { - var builder = ODataConventionModelBuilderFactory.Create(); - builder.EntitySet("customers"); - var model = builder.GetEdmModel(); - var entityClrType = typeof(QueryCompositionCustomer); - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/"); - request.EnableHttpDependencyInjectionSupport(model); - var descriptor = new ReflectedHttpActionDescriptor(); - descriptor.Configuration = request.GetConfiguration(); - - var queryModel = new EnableQueryAttribute().GetModel(entityClrType, request, descriptor); - - Assert.NotNull(queryModel); - Assert.Same(model, queryModel); - Assert.DoesNotContain("Microsoft.AspNet.OData.Model+Microsoft.AspNet.OData.Test.Query.QueryCompositionCustomer", - descriptor.Properties.Keys.OfType()); - } + [Fact] + public void CreateQueryContext_ReturnsQueryContext_ForMatchingModelOnRequest() + { + var builder = ODataConventionModelBuilderFactory.Create(); + builder.EntitySet("customers"); + var model = builder.GetEdmModel(); + var entityClrType = typeof(QueryCompositionCustomer); + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/"); + request.EnableHttpDependencyInjectionSupport(model); + var descriptor = new ReflectedHttpActionDescriptor(); + descriptor.Configuration = request.GetConfiguration(); + + var queryModel = new EnableQueryAttribute().GetModel(entityClrType, request, descriptor); + + Assert.NotNull(queryModel); + Assert.Same(model, queryModel); + Assert.DoesNotContain("Microsoft.AspNet.OData.Model+Microsoft.AspNet.OData.Test.Query.QueryCompositionCustomer", + descriptor.Properties.Keys.OfType()); + } - [Fact] - public async Task QueryableOnActionUnknownOperatorIsAllowed() - { - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext( - "http://localhost:8080/?$orderby=$it desc&unknown=12", - Enumerable.Range(0, 5).AsQueryable()); + [Fact] + public async Task QueryableOnActionUnknownOperatorIsAllowed() + { + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext( + "http://localhost:8080/?$orderby=$it desc&unknown=12", + Enumerable.Range(0, 5).AsQueryable()); - // unsupported operator - ignored - attribute.OnActionExecuted(actionExecutedContext); + // unsupported operator - ignored + attribute.OnActionExecuted(actionExecutedContext); - List result = await actionExecutedContext.Response.Content.ReadAsObject>(); - Assert.Equal(new[] { 4, 3, 2, 1, 0 }, result); - } + List result = await actionExecutedContext.Response.Content.ReadAsObject>(); + Assert.Equal(new[] { 4, 3, 2, 1, 0 }, result); + } - [Fact] - public void QueryableOnActionUnknownOperatorStartingDollarSignThrows() - { - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext( - "http://localhost:8080/QueryCompositionCustomer?$orderby=Name desc&$unknown=12", - QueryCompositionCustomerController.CustomerList.AsQueryable()); + [Fact] + public void QueryableOnActionUnknownOperatorStartingDollarSignThrows() + { + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext( + "http://localhost:8080/QueryCompositionCustomer?$orderby=Name desc&$unknown=12", + QueryCompositionCustomerController.CustomerList.AsQueryable()); - var exception = ExceptionAssert.Throws(() => attribute.OnActionExecuted(actionExecutedContext)); + var exception = ExceptionAssert.Throws(() => attribute.OnActionExecuted(actionExecutedContext)); - // EnableQueryAttribute will validate and throws - Assert.Equal(HttpStatusCode.BadRequest, exception.Response.StatusCode); - } + // EnableQueryAttribute will validate and throws + Assert.Equal(HttpStatusCode.BadRequest, exception.Response.StatusCode); + } - [Fact] - public virtual void QueryableUsesConfiguredAssembliesResolver_For_MappingDerivedTypes() - { - // Arrange - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext( - "http://localhost:8080/QueryCompositionCustomer/?$filter=Id eq 2", - QueryCompositionCustomerController.CustomerList.AsQueryable()); - - ODataModelBuilder modelBuilder = ODataConventionModelBuilderFactory.Create(); - modelBuilder.EntitySet(typeof(QueryCompositionCustomer).Name); - IEdmModel model = modelBuilder.GetEdmModel(); - model.SetAnnotationValue(model.FindType("Microsoft.AspNet.OData.Test.Query.QueryCompositionCustomer"), null); - - bool called = false; - Mock assembliesResolver = new Mock(); - assembliesResolver - .Setup(r => r.GetAssemblies()) - .Returns(new DefaultAssembliesResolver().GetAssemblies()) - .Callback(() => { called = true; }) - .Verifiable(); - actionExecutedContext.Request.GetConfiguration().Services.Replace(typeof(IAssembliesResolver), assembliesResolver.Object); - - // Act - attribute.OnActionExecuted(actionExecutedContext); - - // Assert - Assert.True(called); - } + [Fact] + public virtual void QueryableUsesConfiguredAssembliesResolver_For_MappingDerivedTypes() + { + // Arrange + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext( + "http://localhost:8080/QueryCompositionCustomer/?$filter=Id eq 2", + QueryCompositionCustomerController.CustomerList.AsQueryable()); + + ODataModelBuilder modelBuilder = ODataConventionModelBuilderFactory.Create(); + modelBuilder.EntitySet(typeof(QueryCompositionCustomer).Name); + IEdmModel model = modelBuilder.GetEdmModel(); + model.SetAnnotationValue(model.FindType("Microsoft.AspNet.OData.Test.Query.QueryCompositionCustomer"), null); + + bool called = false; + Mock assembliesResolver = new Mock(); + assembliesResolver + .Setup(r => r.GetAssemblies()) + .Returns(new DefaultAssembliesResolver().GetAssemblies()) + .Callback(() => { called = true; }) + .Verifiable(); + actionExecutedContext.Request.GetConfiguration().Services.Replace(typeof(IAssembliesResolver), assembliesResolver.Object); + + // Act + attribute.OnActionExecuted(actionExecutedContext); + + // Assert + Assert.True(called); + } - [Fact] - public void ApplyQuery_SingleEntity_ThrowsArgumentNull_Entity() - { - var message = RequestFactory.Create(); - message.EnableHttpDependencyInjectionSupport(); - EnableQueryAttribute attribute = new EnableQueryAttribute(); - ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(EdmCoreModel.Instance, typeof(int)), message); - - ExceptionAssert.ThrowsArgumentNull( - () => attribute.ApplyQuery(entity: null, queryOptions: options), - "entity"); - } + [Fact] + public void ApplyQuery_SingleEntity_ThrowsArgumentNull_Entity() + { + var message = RequestFactory.Create(); + message.EnableHttpDependencyInjectionSupport(); + EnableQueryAttribute attribute = new EnableQueryAttribute(); + ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(EdmCoreModel.Instance, typeof(int)), message); + + ExceptionAssert.ThrowsArgumentNull( + () => attribute.ApplyQuery(entity: null, queryOptions: options), + "entity"); + } - [Fact] - public void ApplyQuery_SingleEntity_ThrowsArgumentNull_QueryOptions() - { - EnableQueryAttribute attribute = new EnableQueryAttribute(); + [Fact] + public void ApplyQuery_SingleEntity_ThrowsArgumentNull_QueryOptions() + { + EnableQueryAttribute attribute = new EnableQueryAttribute(); - ExceptionAssert.ThrowsArgumentNull( - () => attribute.ApplyQuery(entity: 42, queryOptions: null), - "queryOptions"); - } + ExceptionAssert.ThrowsArgumentNull( + () => attribute.ApplyQuery(entity: 42, queryOptions: null), + "queryOptions"); + } - [Fact] - public void ApplyQuery_CallsApplyOnODataQueryOptions() - { - object entity = new object(); - EnableQueryAttribute attribute = new EnableQueryAttribute(); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - var request = RequestFactory.Create(); - request.EnableHttpDependencyInjectionSupport(); - Mock queryOptions = new Mock(context, request); + [Fact] + public void ApplyQuery_CallsApplyOnODataQueryOptions() + { + object entity = new object(); + EnableQueryAttribute attribute = new EnableQueryAttribute(); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + var request = RequestFactory.Create(); + request.EnableHttpDependencyInjectionSupport(); + Mock queryOptions = new Mock(context, request); - attribute.ApplyQuery(entity, queryOptions.Object); + attribute.ApplyQuery(entity, queryOptions.Object); - queryOptions.Verify(q => q.ApplyTo(entity, It.IsAny()), Times.Once()); - } + queryOptions.Verify(q => q.ApplyTo(entity, It.IsAny()), Times.Once()); + } - public static TheoryDataSet GetElementTypeTestData + public static TheoryDataSet GetElementTypeTestData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { Enumerable.Empty(), typeof(int) }, - { new List(), typeof(int) }, - { new int[0], typeof(int) }, - { Enumerable.Empty().AsQueryable(), typeof(string) }, - { new SingleResult(Enumerable.Empty().AsQueryable()), typeof(string) }, - { new Customer(), typeof(Customer) } - }; - } - } - - [Theory] - [MemberData(nameof(GetElementTypeTestData))] - public void GetElementType_Returns_ExpectedElementType(object response, Type expectedElementType) - { - HttpActionDescriptor actionDescriptor = new Mock().Object; - SingleResult singleResult = response as SingleResult; - IQueryable collection = (singleResult == null) ? null : singleResult.Queryable; - Assert.Equal(expectedElementType, EnableQueryAttribute.GetElementType(response, collection, new WebApiActionDescriptor(actionDescriptor))); + { Enumerable.Empty(), typeof(int) }, + { new List(), typeof(int) }, + { new int[0], typeof(int) }, + { Enumerable.Empty().AsQueryable(), typeof(string) }, + { new SingleResult(Enumerable.Empty().AsQueryable()), typeof(string) }, + { new Customer(), typeof(Customer) } + }; } + } - [Fact] - public void SingleOrDefault_IQueryableOfT_OneElementInSequence_ReturnsElement() - { - Customer customer = new Customer(); - IQueryable queryable = new[] { customer }.AsQueryable(); - HttpActionDescriptor actionDescriptor = new Mock().Object; + [Theory] + [MemberData(nameof(GetElementTypeTestData))] + public void GetElementType_Returns_ExpectedElementType(object response, Type expectedElementType) + { + HttpActionDescriptor actionDescriptor = new Mock().Object; + SingleResult singleResult = response as SingleResult; + IQueryable collection = (singleResult == null) ? null : singleResult.Queryable; + Assert.Equal(expectedElementType, EnableQueryAttribute.GetElementType(response, collection, new WebApiActionDescriptor(actionDescriptor))); + } - var result = EnableQueryAttribute.SingleOrDefault(queryable, new WebApiActionDescriptor(actionDescriptor)); + [Fact] + public void SingleOrDefault_IQueryableOfT_OneElementInSequence_ReturnsElement() + { + Customer customer = new Customer(); + IQueryable queryable = new[] { customer }.AsQueryable(); + HttpActionDescriptor actionDescriptor = new Mock().Object; - Assert.Same(customer, result); - } + var result = EnableQueryAttribute.SingleOrDefault(queryable, new WebApiActionDescriptor(actionDescriptor)); - [Fact] - public void SingleOrDefault_IQueryableOfT_ZeroElementsInSequence_ReturnsNull() - { - IQueryable queryable = Enumerable.Empty().AsQueryable(); - HttpActionDescriptor actionDescriptor = new Mock().Object; + Assert.Same(customer, result); + } - var result = EnableQueryAttribute.SingleOrDefault(queryable, new WebApiActionDescriptor(actionDescriptor)); + [Fact] + public void SingleOrDefault_IQueryableOfT_ZeroElementsInSequence_ReturnsNull() + { + IQueryable queryable = Enumerable.Empty().AsQueryable(); + HttpActionDescriptor actionDescriptor = new Mock().Object; - Assert.Null(result); - } + var result = EnableQueryAttribute.SingleOrDefault(queryable, new WebApiActionDescriptor(actionDescriptor)); - [Fact] - public void SingleOrDefault_IQueryableOfT_MoreThaneOneElementInSequence_Throws() - { - IQueryable queryable = new[] { new Customer(), new Customer() }.AsQueryable(); - ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor - { - Configuration = new HttpConfiguration(), - MethodInfo = GetType().GetMethod("SomeAction", BindingFlags.Instance | BindingFlags.NonPublic), - ControllerDescriptor = new HttpControllerDescriptor { ControllerName = "SomeName" } - }; - - ExceptionAssert.Throws( - () => EnableQueryAttribute.SingleOrDefault(queryable, new WebApiActionDescriptor(actionDescriptor)), - "The action 'SomeAction' on controller 'SomeName' returned a SingleResult containing more than one element. " + - "SingleResult must have zero or one elements."); - } + Assert.Null(result); + } - [Fact] - public void SingleOrDefault_DisposeCalled_EmptySequence() + [Fact] + public void SingleOrDefault_IQueryableOfT_MoreThaneOneElementInSequence_Throws() + { + IQueryable queryable = new[] { new Customer(), new Customer() }.AsQueryable(); + ReflectedHttpActionDescriptor actionDescriptor = new ReflectedHttpActionDescriptor { - // Arrange - var enumerator = new Mock(MockBehavior.Strict); - enumerator.Setup(mock => mock.MoveNext()).Returns(false); - - var disposable = enumerator.As(); - disposable.Setup(mock => mock.Dispose()).Verifiable(); + Configuration = new HttpConfiguration(), + MethodInfo = GetType().GetMethod("SomeAction", BindingFlags.Instance | BindingFlags.NonPublic), + ControllerDescriptor = new HttpControllerDescriptor { ControllerName = "SomeName" } + }; - var queryable = new Mock(MockBehavior.Strict); - queryable.Setup(mock => mock.GetEnumerator()).Returns(enumerator.Object); + ExceptionAssert.Throws( + () => EnableQueryAttribute.SingleOrDefault(queryable, new WebApiActionDescriptor(actionDescriptor)), + "The action 'SomeAction' on controller 'SomeName' returned a SingleResult containing more than one element. " + + "SingleResult must have zero or one elements."); + } - var actionDescriptor = new ReflectedHttpActionDescriptor - { - Configuration = new HttpConfiguration(), - MethodInfo = GetType().GetMethod("SomeAction", BindingFlags.Instance | BindingFlags.NonPublic), - ControllerDescriptor = new HttpControllerDescriptor { ControllerName = "SomeName" } - }; + [Fact] + public void SingleOrDefault_DisposeCalled_EmptySequence() + { + // Arrange + var enumerator = new Mock(MockBehavior.Strict); + enumerator.Setup(mock => mock.MoveNext()).Returns(false); - // Act - EnableQueryAttribute.SingleOrDefault(queryable.Object, new WebApiActionDescriptor(actionDescriptor)); + var disposable = enumerator.As(); + disposable.Setup(mock => mock.Dispose()).Verifiable(); - // Assert - disposable.Verify(); - } + var queryable = new Mock(MockBehavior.Strict); + queryable.Setup(mock => mock.GetEnumerator()).Returns(enumerator.Object); - [Fact] - public void SingleOrDefault_DisposeCalled_OneElementInSequence() + var actionDescriptor = new ReflectedHttpActionDescriptor { - // Arrange - var enumerator = new Mock(MockBehavior.Strict); - enumerator.SetupSequence(mock => mock.MoveNext()).Returns(true).Returns(false); - enumerator.SetupGet(mock => mock.Current).Returns(new Customer()); + Configuration = new HttpConfiguration(), + MethodInfo = GetType().GetMethod("SomeAction", BindingFlags.Instance | BindingFlags.NonPublic), + ControllerDescriptor = new HttpControllerDescriptor { ControllerName = "SomeName" } + }; - var disposable = enumerator.As(); - disposable.Setup(mock => mock.Dispose()).Verifiable(); + // Act + EnableQueryAttribute.SingleOrDefault(queryable.Object, new WebApiActionDescriptor(actionDescriptor)); - var queryable = new Mock(MockBehavior.Strict); - queryable.Setup(mock => mock.GetEnumerator()).Returns(enumerator.Object); + // Assert + disposable.Verify(); + } - var actionDescriptor = new ReflectedHttpActionDescriptor - { - Configuration = new HttpConfiguration(), - MethodInfo = GetType().GetMethod("SomeAction", BindingFlags.Instance | BindingFlags.NonPublic), - ControllerDescriptor = new HttpControllerDescriptor { ControllerName = "SomeName" } - }; + [Fact] + public void SingleOrDefault_DisposeCalled_OneElementInSequence() + { + // Arrange + var enumerator = new Mock(MockBehavior.Strict); + enumerator.SetupSequence(mock => mock.MoveNext()).Returns(true).Returns(false); + enumerator.SetupGet(mock => mock.Current).Returns(new Customer()); - // Act - EnableQueryAttribute.SingleOrDefault(queryable.Object, new WebApiActionDescriptor(actionDescriptor)); + var disposable = enumerator.As(); + disposable.Setup(mock => mock.Dispose()).Verifiable(); - // Assert - disposable.Verify(); - } + var queryable = new Mock(MockBehavior.Strict); + queryable.Setup(mock => mock.GetEnumerator()).Returns(enumerator.Object); - [Fact] - public void SingleOrDefault_DisposeCalled_MultipleElementsInSequence() + var actionDescriptor = new ReflectedHttpActionDescriptor { - // Arrange - var enumerator = new Mock(MockBehavior.Strict); - enumerator.Setup(mock => mock.MoveNext()).Returns(true); - enumerator.SetupGet(mock => mock.Current).Returns(new Customer()); + Configuration = new HttpConfiguration(), + MethodInfo = GetType().GetMethod("SomeAction", BindingFlags.Instance | BindingFlags.NonPublic), + ControllerDescriptor = new HttpControllerDescriptor { ControllerName = "SomeName" } + }; - var disposable = enumerator.As(); - disposable.Setup(mock => mock.Dispose()).Verifiable(); + // Act + EnableQueryAttribute.SingleOrDefault(queryable.Object, new WebApiActionDescriptor(actionDescriptor)); - var queryable = new Mock(MockBehavior.Strict); - queryable.Setup(mock => mock.GetEnumerator()).Returns(enumerator.Object); + // Assert + disposable.Verify(); + } - var actionDescriptor = new ReflectedHttpActionDescriptor - { - Configuration = new HttpConfiguration(), - MethodInfo = GetType().GetMethod("SomeAction", BindingFlags.Instance | BindingFlags.NonPublic), - ControllerDescriptor = new HttpControllerDescriptor { ControllerName = "SomeName" } - }; + [Fact] + public void SingleOrDefault_DisposeCalled_MultipleElementsInSequence() + { + // Arrange + var enumerator = new Mock(MockBehavior.Strict); + enumerator.Setup(mock => mock.MoveNext()).Returns(true); + enumerator.SetupGet(mock => mock.Current).Returns(new Customer()); - // Act (will throw) - try - { - EnableQueryAttribute.SingleOrDefault(queryable.Object, new WebApiActionDescriptor(actionDescriptor)); - } - catch - { - // Other tests confirm the Exception. - } + var disposable = enumerator.As(); + disposable.Setup(mock => mock.Dispose()).Verifiable(); - // Assert - disposable.Verify(); - } + var queryable = new Mock(MockBehavior.Strict); + queryable.Setup(mock => mock.GetEnumerator()).Returns(enumerator.Object); - [Fact] - public void OnActionExecuted_SingleResult_ReturnsSingleItemEvenIfThereIsNoSelectExpand() + var actionDescriptor = new ReflectedHttpActionDescriptor { - BellevueCustomer customer = new BellevueCustomer(); - SingleResult singleResult = new SingleResult(new BellevueCustomer[] { customer }.AsQueryable()); - HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/", singleResult); - EnableQueryAttribute attribute = new EnableQueryAttribute(); - - attribute.OnActionExecuted(actionExecutedContext); + Configuration = new HttpConfiguration(), + MethodInfo = GetType().GetMethod("SomeAction", BindingFlags.Instance | BindingFlags.NonPublic), + ControllerDescriptor = new HttpControllerDescriptor { ControllerName = "SomeName" } + }; - Assert.Equal(HttpStatusCode.OK, actionExecutedContext.Response.StatusCode); - Assert.Equal(customer, (actionExecutedContext.Response.Content as ObjectContent).Value); + // Act (will throw) + try + { + EnableQueryAttribute.SingleOrDefault(queryable.Object, new WebApiActionDescriptor(actionDescriptor)); } - - [Fact] - public void OnActionExecuted_SingleResult_ReturnsSingleItemForMultipleEnableQueryAttributeSet() + catch { - BellevueCustomer customer = new BellevueCustomer(); - SingleResult singleResult = new SingleResult(new BellevueCustomer[] { customer }.AsQueryable()); - HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/", singleResult); - EnableQueryAttribute attribute = new EnableQueryAttribute(); + // Other tests confirm the Exception. + } - attribute.OnActionExecuted(actionExecutedContext); - attribute.OnActionExecuted(actionExecutedContext); - attribute.OnActionExecuted(actionExecutedContext); + // Assert + disposable.Verify(); + } - Assert.Equal(HttpStatusCode.OK, actionExecutedContext.Response.StatusCode); - Assert.Equal(customer, (actionExecutedContext.Response.Content as ObjectContent).Value); - } + [Fact] + public void OnActionExecuted_SingleResult_ReturnsSingleItemEvenIfThereIsNoSelectExpand() + { + BellevueCustomer customer = new BellevueCustomer(); + SingleResult singleResult = new SingleResult(new BellevueCustomer[] { customer }.AsQueryable()); + HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/", singleResult); + EnableQueryAttribute attribute = new EnableQueryAttribute(); - [Fact] - public void OnActionExecuted_SingleResult_Returns400_IfQueryContainsNonSelectExpand() - { - HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/?$top=10", new Customer()); - EnableQueryAttribute attribute = new EnableQueryAttribute(); + attribute.OnActionExecuted(actionExecutedContext); - attribute.OnActionExecuted(actionExecutedContext); + Assert.Equal(HttpStatusCode.OK, actionExecutedContext.Response.StatusCode); + Assert.Equal(customer, (actionExecutedContext.Response.Content as ObjectContent).Value); + } - Assert.Equal(HttpStatusCode.BadRequest, actionExecutedContext.Response.StatusCode); - } + [Fact] + public void OnActionExecuted_SingleResult_ReturnsSingleItemForMultipleEnableQueryAttributeSet() + { + BellevueCustomer customer = new BellevueCustomer(); + SingleResult singleResult = new SingleResult(new BellevueCustomer[] { customer }.AsQueryable()); + HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/", singleResult); + EnableQueryAttribute attribute = new EnableQueryAttribute(); - [Fact] - public void OnActionExecuted_SingleResult_WithEmptyQueryResult_SetsNotFoundResponse() - { - // Arrange - var customers = Enumerable.Empty().AsQueryable(); - SingleResult result = SingleResult.Create(customers); - HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/", result); - EnableQueryAttribute attribute = new EnableQueryAttribute(); + attribute.OnActionExecuted(actionExecutedContext); + attribute.OnActionExecuted(actionExecutedContext); + attribute.OnActionExecuted(actionExecutedContext); - // Act - attribute.OnActionExecuted(actionExecutedContext); + Assert.Equal(HttpStatusCode.OK, actionExecutedContext.Response.StatusCode); + Assert.Equal(customer, (actionExecutedContext.Response.Content as ObjectContent).Value); + } - // Assert - Assert.Equal(HttpStatusCode.NotFound, actionExecutedContext.Response.StatusCode); - } + [Fact] + public void OnActionExecuted_SingleResult_Returns400_IfQueryContainsNonSelectExpand() + { + HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/?$top=10", new Customer()); + EnableQueryAttribute attribute = new EnableQueryAttribute(); - [Fact] - public void OnActionExecuted_SingleResult_WithEmptyQueryResult_SetsNotFound() - { - // Arrange - var customers = Enumerable.Empty().AsQueryable(); - SingleResult result = SingleResult.Create(customers); - HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/", result); - var container = new EdmEntityContainer("NS", "Default"); - var entityType = new EdmEntityType("NS", "entity"); - var entitySet = new EdmEntitySet(container, "entities", entityType); - actionExecutedContext.Request.ODataProperties().Path = new ODataPath(new EntitySetSegment(entitySet)); - EnableQueryAttribute attribute = new EnableQueryAttribute(); - - // Act - attribute.OnActionExecuted(actionExecutedContext); - - // Assert - Assert.Equal(HttpStatusCode.NotFound, actionExecutedContext.Response.StatusCode); - } + attribute.OnActionExecuted(actionExecutedContext); - [Fact] - public async Task OnActionExecuted_SingleResult_WithMoreThanASingleQueryResult_ReturnsBadRequest() - { - // Arrange - var customers = CustomerList.AsQueryable(); - SingleResult result = SingleResult.Create(customers); - HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/", result); - EnableQueryAttribute attribute = new EnableQueryAttribute(); - - // Act - attribute.OnActionExecuted(actionExecutedContext); - string responseString = await actionExecutedContext.Response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.BadRequest, actionExecutedContext.Response.StatusCode); - Assert.Contains("The query specified in the URI is not valid. The action 'Bar' on controller 'FooController' " + - "returned a SingleResult containing more than one element. SingleResult must have zero or one elements.", - responseString); - } + Assert.Equal(HttpStatusCode.BadRequest, actionExecutedContext.Response.StatusCode); + } + + [Fact] + public void OnActionExecuted_SingleResult_WithEmptyQueryResult_SetsNotFoundResponse() + { + // Arrange + var customers = Enumerable.Empty().AsQueryable(); + SingleResult result = SingleResult.Create(customers); + HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/", result); + EnableQueryAttribute attribute = new EnableQueryAttribute(); + + // Act + attribute.OnActionExecuted(actionExecutedContext); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, actionExecutedContext.Response.StatusCode); + } + + [Fact] + public void OnActionExecuted_SingleResult_WithEmptyQueryResult_SetsNotFound() + { + // Arrange + var customers = Enumerable.Empty().AsQueryable(); + SingleResult result = SingleResult.Create(customers); + HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/", result); + var container = new EdmEntityContainer("NS", "Default"); + var entityType = new EdmEntityType("NS", "entity"); + var entitySet = new EdmEntitySet(container, "entities", entityType); + actionExecutedContext.Request.ODataProperties().Path = new ODataPath(new EntitySetSegment(entitySet)); + EnableQueryAttribute attribute = new EnableQueryAttribute(); + + // Act + attribute.OnActionExecuted(actionExecutedContext); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, actionExecutedContext.Response.StatusCode); + } + + [Fact] + public async Task OnActionExecuted_SingleResult_WithMoreThanASingleQueryResult_ReturnsBadRequest() + { + // Arrange + var customers = CustomerList.AsQueryable(); + SingleResult result = SingleResult.Create(customers); + HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/", result); + EnableQueryAttribute attribute = new EnableQueryAttribute(); + + // Act + attribute.OnActionExecuted(actionExecutedContext); + string responseString = await actionExecutedContext.Response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, actionExecutedContext.Response.StatusCode); + Assert.Contains("The query specified in the URI is not valid. The action 'Bar' on controller 'FooController' " + + "returned a SingleResult containing more than one element. SingleResult must have zero or one elements.", + responseString); + } #endif - [Theory] - [InlineData("$filter=ID eq 1")] - [InlineData("$orderby=ID")] - [InlineData("$count=true")] - [InlineData("$skip=1")] - [InlineData("$top=0")] - public void ValidateSelectExpandOnly_ThrowsODataException_IfODataQueryOptionsHasNonSelectExpand(string parameter) - { - // Arrange - HttpRequest request = RequestFactory.Create("Get", "http://localhost?" + parameter); - ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - - // Act & Assert - ExceptionAssert.Throws( - () => EnableQueryAttribute.ValidateSelectExpandOnly(queryOptions), - "The requested resource is not a collection. Query options $filter, $orderby, $count, $skip, and $top can be applied only on collections."); - } + [Theory] + [InlineData("$filter=ID eq 1")] + [InlineData("$orderby=ID")] + [InlineData("$count=true")] + [InlineData("$skip=1")] + [InlineData("$top=0")] + public void ValidateSelectExpandOnly_ThrowsODataException_IfODataQueryOptionsHasNonSelectExpand(string parameter) + { + // Arrange + HttpRequest request = RequestFactory.Create("Get", "http://localhost?" + parameter); + ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + + // Act & Assert + ExceptionAssert.Throws( + () => EnableQueryAttribute.ValidateSelectExpandOnly(queryOptions), + "The requested resource is not a collection. Query options $filter, $orderby, $count, $skip, and $top can be applied only on collections."); + } #if false - [Fact] - public void OnActionExecuted_Works_WithPath() - { - // Arrange - Customer customer = new Customer(); - SingleResult singleResult = new SingleResult(new[] { customer }.AsQueryable()); - HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/", singleResult); - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpRequestMessage request = actionExecutedContext.Request; - var container = new EdmEntityContainer("NS", "Default"); - var entityType = new EdmEntityType("NS", "entity"); - var entitySet = new EdmEntitySet(container, "entities", entityType); - request.ODataProperties().Path = new ODataPath(new EntitySetSegment(entitySet)); - - // Act - attribute.OnActionExecuted(actionExecutedContext); - - // Assert - Assert.Equal(HttpStatusCode.OK, actionExecutedContext.Response.StatusCode); - Assert.Equal(customer, ((ObjectContent)actionExecutedContext.Response.Content).Value); - } + [Fact] + public void OnActionExecuted_Works_WithPath() + { + // Arrange + Customer customer = new Customer(); + SingleResult singleResult = new SingleResult(new[] { customer }.AsQueryable()); + HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/", singleResult); + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpRequestMessage request = actionExecutedContext.Request; + var container = new EdmEntityContainer("NS", "Default"); + var entityType = new EdmEntityType("NS", "entity"); + var entitySet = new EdmEntitySet(container, "entities", entityType); + request.ODataProperties().Path = new ODataPath(new EntitySetSegment(entitySet)); + + // Act + attribute.OnActionExecuted(actionExecutedContext); + + // Assert + Assert.Equal(HttpStatusCode.OK, actionExecutedContext.Response.StatusCode); + Assert.Equal(customer, ((ObjectContent)actionExecutedContext.Response.Content).Value); + } - [Fact] - public void OnActionExecuted_StringValue() - { - // Arrange - string stringResult = "foo"; - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/Suppliers(1)/CompanyName?customqueryoption=bar", stringResult); + [Fact] + public void OnActionExecuted_StringValue() + { + // Arrange + string stringResult = "foo"; + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/Suppliers(1)/CompanyName?customqueryoption=bar", stringResult); - // Act - attribute.OnActionExecuted(actionExecutedContext); + // Act + attribute.OnActionExecuted(actionExecutedContext); - // Assert - Assert.Equal(HttpStatusCode.OK, actionExecutedContext.Response.StatusCode); - Assert.Equal(stringResult, ((ObjectContent)actionExecutedContext.Response.Content).Value); - } + // Assert + Assert.Equal(HttpStatusCode.OK, actionExecutedContext.Response.StatusCode); + Assert.Equal(stringResult, ((ObjectContent)actionExecutedContext.Response.Content).Value); + } - [Fact] - public void OnActionExecuted_ByteArrayValue() - { - // Arrange - byte[] bytesResult = BitConverter.GetBytes(42); - EnableQueryAttribute attribute = new EnableQueryAttribute(); - HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/Suppliers(1)/Version?customqueryoption=bar", bytesResult); + [Fact] + public void OnActionExecuted_ByteArrayValue() + { + // Arrange + byte[] bytesResult = BitConverter.GetBytes(42); + EnableQueryAttribute attribute = new EnableQueryAttribute(); + HttpActionExecutedContext actionExecutedContext = GetActionExecutedContext("http://localhost/Suppliers(1)/Version?customqueryoption=bar", bytesResult); - // Act - attribute.OnActionExecuted(actionExecutedContext); + // Act + attribute.OnActionExecuted(actionExecutedContext); - // Assert - Assert.Equal(HttpStatusCode.OK, actionExecutedContext.Response.StatusCode); - Assert.Equal(bytesResult, ((ObjectContent)actionExecutedContext.Response.Content).Value); - } + // Assert + Assert.Equal(HttpStatusCode.OK, actionExecutedContext.Response.StatusCode); + Assert.Equal(bytesResult, ((ObjectContent)actionExecutedContext.Response.Content).Value); + } - private void SomeAction() - { - } + private void SomeAction() + { + } - private static HttpActionExecutedContext GetActionExecutedContext(string uri, TResponse result) - { - var request = new HttpRequestMessage(HttpMethod.Get, uri); - request.EnableODataDependencyInjectionSupport(); - var actionContext = ContextUtil.CreateActionContext(ContextUtil.CreateControllerContext(request: request)); - var response = request.CreateResponse(HttpStatusCode.OK, result); - var actionExecutedContext = new HttpActionExecutedContext { ActionContext = actionContext, Response = response }; - actionContext.ActionDescriptor.Configuration = request.GetConfiguration(); - return actionExecutedContext; - } + private static HttpActionExecutedContext GetActionExecutedContext(string uri, TResponse result) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + request.EnableODataDependencyInjectionSupport(); + var actionContext = ContextUtil.CreateActionContext(ContextUtil.CreateControllerContext(request: request)); + var response = request.CreateResponse(HttpStatusCode.OK, result); + var actionExecutedContext = new HttpActionExecutedContext { ActionContext = actionContext, Response = response }; + actionContext.ActionDescriptor.Configuration = request.GetConfiguration(); + return actionExecutedContext; + } #endif - private static ActionExecutedContext CreateActionExecutedContext(ActionExecutingContext context) + private static ActionExecutedContext CreateActionExecutedContext(ActionExecutingContext context) + { + return new ActionExecutedContext(context, context.Filters, context.Controller) { - return new ActionExecutedContext(context, context.Filters, context.Controller) - { - Result = context.Result, - }; - } + Result = context.Result, + }; + } - private static ActionExecutingContext CreateDefaultActionExecutingContext() - { - return new ActionExecutingContext( - CreateActionContext(), - new List(), - new Dictionary(), - controller: new object()); - } + private static ActionExecutingContext CreateDefaultActionExecutingContext() + { + return new ActionExecutingContext( + CreateActionContext(), + new List(), + new Dictionary(), + controller: new object()); + } - private static ActionContext CreateActionContext() - { - return new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); - } + private static ActionContext CreateActionContext() + { + return new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + } - private static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); + } - private class QCustomer - { - public int Id { get; set; } - public string Name { get; set; } - } + private class QCustomer + { + public int Id { get; set; } + public string Name { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/ExpressionHelperMethodsTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/ExpressionHelperMethodsTest.cs index 1f0dd77d9..a892e507d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/ExpressionHelperMethodsTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/ExpressionHelperMethodsTest.cs @@ -9,23 +9,22 @@ using Microsoft.AspNetCore.OData.Query; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class ExpressionHelperMethodsTests { - public class ExpressionHelperMethodsTests + [Fact] + public void EntityAsQueryable_Returns_MethodInfo() { - [Fact] - public void EntityAsQueryable_Returns_MethodInfo() - { - // Arrange & Act & Assert - Assert.Equal("ToQueryable", ExpressionHelperMethods.EntityAsQueryable.Name); + // Arrange & Act & Assert + Assert.Equal("ToQueryable", ExpressionHelperMethods.EntityAsQueryable.Name); - Assert.Equal("SelectMany", ExpressionHelperMethods.QueryableSelectManyGeneric.Name); + Assert.Equal("SelectMany", ExpressionHelperMethods.QueryableSelectManyGeneric.Name); - Assert.Equal("Max", ExpressionHelperMethods.QueryableMax.Name); - Assert.Equal("Min", ExpressionHelperMethods.QueryableMin.Name); + Assert.Equal("Max", ExpressionHelperMethods.QueryableMax.Name); + Assert.Equal("Min", ExpressionHelperMethods.QueryableMin.Name); - Assert.Equal("GroupBy", ExpressionHelperMethods.EnumerableGroupByGeneric.Name); - Assert.Equal("SelectMany", ExpressionHelperMethods.EnumerableSelectManyGeneric.Name); - } + Assert.Equal("GroupBy", ExpressionHelperMethods.EnumerableGroupByGeneric.Name); + Assert.Equal("SelectMany", ExpressionHelperMethods.EnumerableSelectManyGeneric.Name); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/ExpressionHelpersTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/ExpressionHelpersTests.cs index 355b6fbcd..ab951c260 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/ExpressionHelpersTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/ExpressionHelpersTests.cs @@ -12,137 +12,136 @@ using Microsoft.AspNetCore.OData.Query; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class ExpressionHelpersTests { - public class ExpressionHelpersTests + private static IQueryable _querable = new List() + { + new ECustomer { Id = 1, Name = "Ady", Age = 19, Phones = new List { 1, 2 } }, + new ECustomer { Id = 2, Name = "Peter", Age = 29, Phones = new List { 4, 5 } }, + new ECustomer { Id = 3, Name = "Sam", Age = 8, Phones = new List { 7, 8 } } + }.AsQueryable(); + + [Fact] + public void Count_Returns_CorrectCountFunc() + { + // Arrange & Act + Func countFunc = ExpressionHelpers.Count(_querable, typeof(ECustomer)); + + // Assert + Assert.Equal(3, countFunc()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Skip_Returns_CorrectQueryable(bool parameterize) + { + // Arrange & Act + IQueryable actual = ExpressionHelpers.Skip(_querable, 2, typeof(ECustomer), parameterize); + + // Assert + ECustomer customer = Assert.Single(actual.Cast()); + Assert.Equal(3, customer.Id); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Take_Returns_CorrectQueryable(bool parameterize) + { + // Arrange & Act + IQueryable actual = ExpressionHelpers.Take(_querable, 1, typeof(ECustomer), parameterize); + + // Assert + ECustomer customer = Assert.Single(actual.Cast()); + Assert.Equal(1, customer.Id); + } + + [Fact] + public void GroupBy_Returns_CorrectQueryable() + { + // Arrange + Expression> expression = x => x.Name; + + // Act + IQueryable actual = ExpressionHelpers.Select(_querable, expression, typeof(ECustomer)); + + // Assert + Assert.Equal(new string[] { "Ady", "Peter", "Sam" }, actual.Cast()); + } + + [Fact] + public void Select_Returns_CorrectQueryable() + { + // Arrange + Expression> expression = x => x.Name; + + // Act + IQueryable actual = ExpressionHelpers.Select(_querable, expression, typeof(ECustomer)); + + // Assert + Assert.Equal(new string[] { "Ady", "Peter", "Sam" }, actual.Cast()); + } + + [Fact] + public void Where_Returns_CorrectQueryable() + { + // Arrange + Expression> expression = x => x.Name == "Peter"; + + // Act + IQueryable actual = ExpressionHelpers.Where(_querable, expression, typeof(ECustomer)); + + // Assert + ECustomer customer = Assert.Single(actual.Cast()); + Assert.Equal(2, customer.Id); + } + + [Fact] + public void Default_Returns_CorrectExpression() { - private static IQueryable _querable = new List() - { - new ECustomer { Id = 1, Name = "Ady", Age = 19, Phones = new List { 1, 2 } }, - new ECustomer { Id = 2, Name = "Peter", Age = 29, Phones = new List { 4, 5 } }, - new ECustomer { Id = 3, Name = "Sam", Age = 8, Phones = new List { 7, 8 } } - }.AsQueryable(); - - [Fact] - public void Count_Returns_CorrectCountFunc() - { - // Arrange & Act - Func countFunc = ExpressionHelpers.Count(_querable, typeof(ECustomer)); - - // Assert - Assert.Equal(3, countFunc()); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void Skip_Returns_CorrectQueryable(bool parameterize) - { - // Arrange & Act - IQueryable actual = ExpressionHelpers.Skip(_querable, 2, typeof(ECustomer), parameterize); - - // Assert - ECustomer customer = Assert.Single(actual.Cast()); - Assert.Equal(3, customer.Id); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void Take_Returns_CorrectQueryable(bool parameterize) - { - // Arrange & Act - IQueryable actual = ExpressionHelpers.Take(_querable, 1, typeof(ECustomer), parameterize); - - // Assert - ECustomer customer = Assert.Single(actual.Cast()); - Assert.Equal(1, customer.Id); - } - - [Fact] - public void GroupBy_Returns_CorrectQueryable() - { - // Arrange - Expression> expression = x => x.Name; - - // Act - IQueryable actual = ExpressionHelpers.Select(_querable, expression, typeof(ECustomer)); - - // Assert - Assert.Equal(new string[] { "Ady", "Peter", "Sam" }, actual.Cast()); - } - - [Fact] - public void Select_Returns_CorrectQueryable() - { - // Arrange - Expression> expression = x => x.Name; - - // Act - IQueryable actual = ExpressionHelpers.Select(_querable, expression, typeof(ECustomer)); - - // Assert - Assert.Equal(new string[] { "Ady", "Peter", "Sam" }, actual.Cast()); - } - - [Fact] - public void Where_Returns_CorrectQueryable() - { - // Arrange - Expression> expression = x => x.Name == "Peter"; - - // Act - IQueryable actual = ExpressionHelpers.Where(_querable, expression, typeof(ECustomer)); - - // Assert - ECustomer customer = Assert.Single(actual.Cast()); - Assert.Equal(2, customer.Id); - } - - [Fact] - public void Default_Returns_CorrectExpression() - { - // Arrange & Act & Assert - Expression expression = ExpressionHelpers.Default(typeof(int)); - Assert.Equal(ExpressionType.Constant, expression.NodeType); - ConstantExpression constantExpression = Assert.IsType(expression); - Assert.Equal(0, constantExpression.Value); - Assert.Equal(typeof(int), constantExpression.Type); - - // Arrange & Act & Assert - expression = ExpressionHelpers.Default(typeof(ExpressionHelpersTests)); - Assert.Equal(ExpressionType.Constant, expression.NodeType); - constantExpression = Assert.IsAssignableFrom(expression); - Assert.Null(constantExpression.Value); - Assert.Equal(typeof(ExpressionHelpersTests), constantExpression.Type); - } + // Arrange & Act & Assert + Expression expression = ExpressionHelpers.Default(typeof(int)); + Assert.Equal(ExpressionType.Constant, expression.NodeType); + ConstantExpression constantExpression = Assert.IsType(expression); + Assert.Equal(0, constantExpression.Value); + Assert.Equal(typeof(int), constantExpression.Type); + + // Arrange & Act & Assert + expression = ExpressionHelpers.Default(typeof(ExpressionHelpersTests)); + Assert.Equal(ExpressionType.Constant, expression.NodeType); + constantExpression = Assert.IsAssignableFrom(expression); + Assert.Null(constantExpression.Value); + Assert.Equal(typeof(ExpressionHelpersTests), constantExpression.Type); + } #if false - [Fact] - public void SelectMany_Returns_CorrectQueryable() - { - // Arrange - Expression>> expression = x => x.Phones; - - // Act - IQueryable actual = ExpressionHelpers.SelectMany(_querable, expression, typeof(ECustomer)); - - // Assert - Assert.Equal(new[] { 1, 2, 4, 5, 7, 8 }, actual.Cast()); - } + [Fact] + public void SelectMany_Returns_CorrectQueryable() + { + // Arrange + Expression>> expression = x => x.Phones; + + // Act + IQueryable actual = ExpressionHelpers.SelectMany(_querable, expression, typeof(ECustomer)); + + // Assert + Assert.Equal(new[] { 1, 2, 4, 5, 7, 8 }, actual.Cast()); + } #endif - private class ECustomer - { - public int Id { get; set; } + private class ECustomer + { + public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public int Age { get; set; } + public int Age { get; set; } - public List Phones { get; set; } + public List Phones { get; set; } - public List Emails { get; set; } - } + public List Emails { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/AggregationBinderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/AggregationBinderTests.cs index e0c083ff3..42f5c9a38 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/AggregationBinderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/AggregationBinderTests.cs @@ -19,284 +19,283 @@ using Microsoft.OData.UriParser.Aggregation; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions +namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions; + +public class AggregationBinderTests { - public class AggregationBinderTests - { - private static readonly Uri _serviceBaseUri = new Uri("http://server/service/"); + private static readonly Uri _serviceBaseUri = new Uri("http://server/service/"); - private static Dictionary _modelCache = new Dictionary(); + private static Dictionary _modelCache = new Dictionary(); - [Fact] - public void SingleGroupBy() - { - var filters = VerifyQueryDeserialization( - "groupby((ProductName))", - ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = ProductName, Value = $it.ProductName, }, })" - + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); - } + [Fact] + public void SingleGroupBy() + { + var filters = VerifyQueryDeserialization( + "groupby((ProductName))", + ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = ProductName, Value = $it.ProductName, }, })" + + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); + } - [Fact] - public void MultipleGroupBy() - { - var filters = VerifyQueryDeserialization( - "groupby((ProductName, SupplierID))", - ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new AggregationPropertyContainer() {Name = SupplierID, Value = Convert($it.SupplierID), Next = new LastInChain() {Name = ProductName, Value = $it.ProductName, }, }, })" - + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); - } + [Fact] + public void MultipleGroupBy() + { + var filters = VerifyQueryDeserialization( + "groupby((ProductName, SupplierID))", + ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new AggregationPropertyContainer() {Name = SupplierID, Value = Convert($it.SupplierID), Next = new LastInChain() {Name = ProductName, Value = $it.ProductName, }, }, })" + + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); + } - [Fact] - public void NavigationGroupBy() - { - var filters = VerifyQueryDeserialization( - "groupby((Category/CategoryName))", - ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Category, NestedValue = new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = CategoryName, Value = $it.Category.CategoryName, }, }, }, })" - + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); - } + [Fact] + public void NavigationGroupBy() + { + var filters = VerifyQueryDeserialization( + "groupby((Category/CategoryName))", + ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Category, NestedValue = new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = CategoryName, Value = $it.Category.CategoryName, }, }, }, })" + + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); + } - [Fact] - public void NestedNavigationGroupBy() - { - var filters = VerifyQueryDeserialization( - "groupby((Category/Product/ProductName))", - ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Category, NestedValue = new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Product, NestedValue = new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = ProductName, Value = $it.Category.Product.ProductName, }, }, }, }, }, })" - + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); - } + [Fact] + public void NestedNavigationGroupBy() + { + var filters = VerifyQueryDeserialization( + "groupby((Category/Product/ProductName))", + ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Category, NestedValue = new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Product, NestedValue = new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = ProductName, Value = $it.Category.Product.ProductName, }, }, }, }, }, })" + + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); + } - [Fact] - public void NavigationMultipleGroupBy() - { - var filters = VerifyQueryDeserialization( - "groupby((Category/CategoryName, SupplierAddress/State))", - ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new NestedProperty() {Name = SupplierAddress, NestedValue = new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = State, Value = $it.SupplierAddress.State, }, }, Next = new NestedPropertyLastInChain() {Name = Category, NestedValue = new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = CategoryName, Value = $it.Category.CategoryName, }, }, }, }, })" - + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); - } + [Fact] + public void NavigationMultipleGroupBy() + { + var filters = VerifyQueryDeserialization( + "groupby((Category/CategoryName, SupplierAddress/State))", + ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new NestedProperty() {Name = SupplierAddress, NestedValue = new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = State, Value = $it.SupplierAddress.State, }, }, Next = new NestedPropertyLastInChain() {Name = Category, NestedValue = new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = CategoryName, Value = $it.Category.CategoryName, }, }, }, }, })" + + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); + } - [Fact] - public void NestedNavigationMultipleGroupBy() - { - var filters = VerifyQueryDeserialization( - "groupby((Category/Product/ProductName, Category/Product/UnitPrice))", - ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Category, NestedValue = new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Product, NestedValue = new GroupByWrapper() {GroupByContainer = new AggregationPropertyContainer() {Name = UnitPrice, Value = Convert($it.Category.Product.UnitPrice), Next = new LastInChain() {Name = ProductName, Value = $it.Category.Product.ProductName, }, }, }, }, }, }, })" - + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); - } + [Fact] + public void NestedNavigationMultipleGroupBy() + { + var filters = VerifyQueryDeserialization( + "groupby((Category/Product/ProductName, Category/Product/UnitPrice))", + ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Category, NestedValue = new GroupByWrapper() {GroupByContainer = new NestedPropertyLastInChain() {Name = Product, NestedValue = new GroupByWrapper() {GroupByContainer = new AggregationPropertyContainer() {Name = UnitPrice, Value = Convert($it.Category.Product.UnitPrice), Next = new LastInChain() {Name = ProductName, Value = $it.Category.Product.ProductName, }, }, }, }, }, }, })" + + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); + } - [Fact] - public void SingleDynamicGroupBy() - { - var filters = VerifyQueryDeserialization( - "groupby((ProductProperty))", - ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = ProductProperty, Value = IIF($it.ProductProperties.ContainsKey(ProductProperty), $it.ProductPropertiesProductProperty, null), }, })" - + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); - } + [Fact] + public void SingleDynamicGroupBy() + { + var filters = VerifyQueryDeserialization( + "groupby((ProductProperty))", + ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = ProductProperty, Value = IIF($it.ProductProperties.ContainsKey(ProductProperty), $it.ProductPropertiesProductProperty, null), }, })" + + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, })"); + } - [Fact] - public void SingleSum() - { - var filters = VerifyQueryDeserialization( - "aggregate(SupplierID with sum as SupplierID)", - ".GroupBy($it => new NoGroupByWrapper())" - + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = SupplierID, Value = Convert(Convert($it).Sum($it => $it.SupplierID)), }, })"); - } + [Fact] + public void SingleSum() + { + var filters = VerifyQueryDeserialization( + "aggregate(SupplierID with sum as SupplierID)", + ".GroupBy($it => new NoGroupByWrapper())" + + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = SupplierID, Value = Convert(Convert($it).Sum($it => $it.SupplierID)), }, })"); + } - [Fact] - public void SingleDynamicSum() - { - var filters = VerifyQueryDeserialization( - "aggregate(ProductProperty with sum as ProductProperty)", - ".GroupBy($it => new NoGroupByWrapper())" - + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = ProductProperty, Value = Convert(Convert($it).Sum($it => IIF($it.ProductProperties.ContainsKey(ProductProperty), $it.ProductPropertiesProductProperty, null).SafeConvertToDecimal())), }, })"); - } + [Fact] + public void SingleDynamicSum() + { + var filters = VerifyQueryDeserialization( + "aggregate(ProductProperty with sum as ProductProperty)", + ".GroupBy($it => new NoGroupByWrapper())" + + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = ProductProperty, Value = Convert(Convert($it).Sum($it => IIF($it.ProductProperties.ContainsKey(ProductProperty), $it.ProductPropertiesProductProperty, null).SafeConvertToDecimal())), }, })"); + } - [Fact] - public void SingleMin() - { - var filters = VerifyQueryDeserialization( - "aggregate(SupplierID with min as SupplierID)", - ".GroupBy($it => new NoGroupByWrapper())" - + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = SupplierID, Value = Convert(Convert($it).Min($it => $it.SupplierID)), }, })"); - } + [Fact] + public void SingleMin() + { + var filters = VerifyQueryDeserialization( + "aggregate(SupplierID with min as SupplierID)", + ".GroupBy($it => new NoGroupByWrapper())" + + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = SupplierID, Value = Convert(Convert($it).Min($it => $it.SupplierID)), }, })"); + } - [Fact] - public void SingleDynamicMin() - { - var filters = VerifyQueryDeserialization( - "aggregate(ProductProperty with min as MinProductProperty)", - ".GroupBy($it => new NoGroupByWrapper())" - + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = MinProductProperty, Value = Convert($it).Min($it => IIF($it.ProductProperties.ContainsKey(ProductProperty), $it.ProductPropertiesProductProperty, null)), }, })"); - } + [Fact] + public void SingleDynamicMin() + { + var filters = VerifyQueryDeserialization( + "aggregate(ProductProperty with min as MinProductProperty)", + ".GroupBy($it => new NoGroupByWrapper())" + + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = MinProductProperty, Value = Convert($it).Min($it => IIF($it.ProductProperties.ContainsKey(ProductProperty), $it.ProductPropertiesProductProperty, null)), }, })"); + } - [Fact] - public void SingleMax() - { - var filters = VerifyQueryDeserialization( - "aggregate(SupplierID with max as SupplierID)", - ".GroupBy($it => new NoGroupByWrapper())" - + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = SupplierID, Value = Convert(Convert($it).Max($it => $it.SupplierID)), }, })"); - } + [Fact] + public void SingleMax() + { + var filters = VerifyQueryDeserialization( + "aggregate(SupplierID with max as SupplierID)", + ".GroupBy($it => new NoGroupByWrapper())" + + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = SupplierID, Value = Convert(Convert($it).Max($it => $it.SupplierID)), }, })"); + } - [Fact] - public void SingleAverage() - { - var filters = VerifyQueryDeserialization( - "aggregate(UnitPrice with average as AvgUnitPrice)", - ".GroupBy($it => new NoGroupByWrapper())" - + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = AvgUnitPrice, Value = Convert(Convert($it).Average($it => $it.UnitPrice)), }, })"); - } + [Fact] + public void SingleAverage() + { + var filters = VerifyQueryDeserialization( + "aggregate(UnitPrice with average as AvgUnitPrice)", + ".GroupBy($it => new NoGroupByWrapper())" + + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = AvgUnitPrice, Value = Convert(Convert($it).Average($it => $it.UnitPrice)), }, })"); + } - [Fact] - public void SingleCountDistinct() - { - var filters = VerifyQueryDeserialization( - "aggregate(SupplierID with countdistinct as Count)", - ".GroupBy($it => new NoGroupByWrapper())" - + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = Count, Value = Convert(Convert($it).Select($it => $it.SupplierID).Distinct().LongCount()), }, })"); - } + [Fact] + public void SingleCountDistinct() + { + var filters = VerifyQueryDeserialization( + "aggregate(SupplierID with countdistinct as Count)", + ".GroupBy($it => new NoGroupByWrapper())" + + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = Count, Value = Convert(Convert($it).Select($it => $it.SupplierID).Distinct().LongCount()), }, })"); + } - [Fact] - public void MultipleAggregate() - { - var filters = VerifyQueryDeserialization( - "aggregate(SupplierID with sum as SupplierID, CategoryID with sum as CategoryID)", - ".GroupBy($it => new NoGroupByWrapper())" - + ".Select($it => new NoGroupByAggregationWrapper() {Container = new AggregationPropertyContainer() {Name = CategoryID, Value = Convert(Convert($it).Sum($it => $it.CategoryID)), Next = new LastInChain() {Name = SupplierID, Value = Convert(Convert($it).Sum($it => $it.SupplierID)), }, }, })"); - } + [Fact] + public void MultipleAggregate() + { + var filters = VerifyQueryDeserialization( + "aggregate(SupplierID with sum as SupplierID, CategoryID with sum as CategoryID)", + ".GroupBy($it => new NoGroupByWrapper())" + + ".Select($it => new NoGroupByAggregationWrapper() {Container = new AggregationPropertyContainer() {Name = CategoryID, Value = Convert(Convert($it).Sum($it => $it.CategoryID)), Next = new LastInChain() {Name = SupplierID, Value = Convert(Convert($it).Sum($it => $it.SupplierID)), }, }, })"); + } - [Fact] - public void GroupByAndAggregate() - { - var filters = VerifyQueryDeserialization( - "groupby((ProductName), aggregate(SupplierID with sum as SupplierID))", - ".Select($it => new FlatteningWrapper`1() {Source = $it, GroupByContainer = new LastInChain() {Name = Property0, Value = Convert($it.SupplierID), }, })" - + ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = ProductName, Value = $it.Source.ProductName, }, })" - + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, Container = new LastInChain() {Name = SupplierID, Value = Convert(Convert($it).Sum($it => Convert($it.GroupByContainer.Value))), }, })"); - } + [Fact] + public void GroupByAndAggregate() + { + var filters = VerifyQueryDeserialization( + "groupby((ProductName), aggregate(SupplierID with sum as SupplierID))", + ".Select($it => new FlatteningWrapper`1() {Source = $it, GroupByContainer = new LastInChain() {Name = Property0, Value = Convert($it.SupplierID), }, })" + + ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = ProductName, Value = $it.Source.ProductName, }, })" + + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, Container = new LastInChain() {Name = SupplierID, Value = Convert(Convert($it).Sum($it => Convert($it.GroupByContainer.Value))), }, })"); + } - [Fact] - public void GroupByAndMultipleAggregations() - { - var filters = VerifyQueryDeserialization( - "groupby((ProductName), aggregate(SupplierID with sum as SupplierID, CategoryID with sum as CategoryID))", - ".Select($it => new FlatteningWrapper`1() {Source = $it, GroupByContainer = new AggregationPropertyContainer() {Name = Property1, Value = Convert($it.SupplierID), Next = new LastInChain() {Name = Property0, Value = Convert($it.CategoryID), }, }, })" - + ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = ProductName, Value = $it.Source.ProductName, }, })" - + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, Container = new AggregationPropertyContainer() {Name = CategoryID, Value = Convert(Convert($it).Sum($it => Convert($it.GroupByContainer.Next.Value))), Next = new LastInChain() {Name = SupplierID, Value = Convert(Convert($it).Sum($it => Convert($it.GroupByContainer.Value))), }, }, })"); - } + [Fact] + public void GroupByAndMultipleAggregations() + { + var filters = VerifyQueryDeserialization( + "groupby((ProductName), aggregate(SupplierID with sum as SupplierID, CategoryID with sum as CategoryID))", + ".Select($it => new FlatteningWrapper`1() {Source = $it, GroupByContainer = new AggregationPropertyContainer() {Name = Property1, Value = Convert($it.SupplierID), Next = new LastInChain() {Name = Property0, Value = Convert($it.CategoryID), }, }, })" + + ".GroupBy($it => new GroupByWrapper() {GroupByContainer = new LastInChain() {Name = ProductName, Value = $it.Source.ProductName, }, })" + + ".Select($it => new AggregationWrapper() {GroupByContainer = $it.Key.GroupByContainer, Container = new AggregationPropertyContainer() {Name = CategoryID, Value = Convert(Convert($it).Sum($it => Convert($it.GroupByContainer.Next.Value))), Next = new LastInChain() {Name = SupplierID, Value = Convert(Convert($it).Sum($it => Convert($it.GroupByContainer.Value))), }, }, })"); + } - [Fact] - public void ClassicEFQueryShape() - { - var filters = VerifyQueryDeserialization( - "aggregate(SupplierID with sum as SupplierID)", - ".GroupBy($it => new NoGroupByWrapper())" - + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = SupplierID, Value = $it.AsQueryable().Sum($it => $it.SupplierID), }, })", - classicEF: true); - } + [Fact] + public void ClassicEFQueryShape() + { + var filters = VerifyQueryDeserialization( + "aggregate(SupplierID with sum as SupplierID)", + ".GroupBy($it => new NoGroupByWrapper())" + + ".Select($it => new NoGroupByAggregationWrapper() {Container = new LastInChain() {Name = SupplierID, Value = $it.AsQueryable().Sum($it => $it.SupplierID), }, })", + classicEF: true); + } - private Expression VerifyQueryDeserialization(string filter, string expectedResult = null, Action settingsCustomizer = null, bool classicEF = false) - { - return VerifyQueryDeserialization(filter, expectedResult, settingsCustomizer, classicEF); - } + private Expression VerifyQueryDeserialization(string filter, string expectedResult = null, Action settingsCustomizer = null, bool classicEF = false) + { + return VerifyQueryDeserialization(filter, expectedResult, settingsCustomizer, classicEF); + } - private Expression VerifyQueryDeserialization(string clauseString, string expectedResult = null, Action settingsCustomizer = null, bool classicEF = false) where T : class - { - IEdmModel model = GetModel(); - ApplyClause clause = CreateApplyNode(clauseString, model, typeof(T)); - IAssemblyResolver assembliesResolver = AssemblyResolverHelper.Default; + private Expression VerifyQueryDeserialization(string clauseString, string expectedResult = null, Action settingsCustomizer = null, bool classicEF = false) where T : class + { + IEdmModel model = GetModel(); + ApplyClause clause = CreateApplyNode(clauseString, model, typeof(T)); + IAssemblyResolver assembliesResolver = AssemblyResolverHelper.Default; - Func customizeSettings = (settings) => + Func customizeSettings = (settings) => + { + if (settingsCustomizer != null) { - if (settingsCustomizer != null) - { - settingsCustomizer.Invoke(settings); - } + settingsCustomizer.Invoke(settings); + } - return settings; - }; + return settings; + }; - var binder = classicEF - ? new AggregationBinderEFFake( - customizeSettings(new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }), - assembliesResolver, - typeof(T), - model, - clause.Transformations.First()) - : new AggregationBinder( - customizeSettings(new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }), - assembliesResolver, - typeof(T), - model, - clause.Transformations.First()); + var binder = classicEF + ? new AggregationBinderEFFake( + customizeSettings(new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }), + assembliesResolver, + typeof(T), + model, + clause.Transformations.First()) + : new AggregationBinder( + customizeSettings(new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }), + assembliesResolver, + typeof(T), + model, + clause.Transformations.First()); - var query = Enumerable.Empty().AsQueryable(); + var query = Enumerable.Empty().AsQueryable(); - var queryResult = binder.Bind(query); + var queryResult = binder.Bind(query); - var applyExpr = queryResult.Expression; + var applyExpr = queryResult.Expression; - VerifyExpression(applyExpr, expectedResult); + VerifyExpression(applyExpr, expectedResult); - return applyExpr; - } + return applyExpr; + } - private void VerifyExpression(Expression clause, string expectedExpression) - { - // strip off the beginning part of the expression to get to the first - // actual query operator - string resultExpression = ExpressionStringBuilder.ToString(clause); + private void VerifyExpression(Expression clause, string expectedExpression) + { + // strip off the beginning part of the expression to get to the first + // actual query operator + string resultExpression = ExpressionStringBuilder.ToString(clause); - var replace = "System.Linq.EmptyPartition`1[" + typeof(T).FullName + "]"; + var replace = "System.Linq.EmptyPartition`1[" + typeof(T).FullName + "]"; - resultExpression = resultExpression.Replace(replace, string.Empty); - Assert.True(resultExpression == expectedExpression, - String.Format("Expected expression '{0}' but the deserializer produced '{1}'", expectedExpression, resultExpression)); - } + resultExpression = resultExpression.Replace(replace, string.Empty); + Assert.True(resultExpression == expectedExpression, + String.Format("Expected expression '{0}' but the deserializer produced '{1}'", expectedExpression, resultExpression)); + } - private ApplyClause CreateApplyNode(string clause, IEdmModel model, Type entityType) - { - IEdmEntityType productType = model.SchemaElements.OfType().Single(t => t.Name == entityType.Name); - Assert.NotNull(productType); // Guard + private ApplyClause CreateApplyNode(string clause, IEdmModel model, Type entityType) + { + IEdmEntityType productType = model.SchemaElements.OfType().Single(t => t.Name == entityType.Name); + Assert.NotNull(productType); // Guard - IEdmEntitySet products = model.EntityContainer.FindEntitySet("Products"); - Assert.NotNull(products); // Guard + IEdmEntitySet products = model.EntityContainer.FindEntitySet("Products"); + Assert.NotNull(products); // Guard - ODataQueryOptionParser parser = new ODataQueryOptionParser(model, productType, products, - new Dictionary { { "$apply", clause } }); + ODataQueryOptionParser parser = new ODataQueryOptionParser(model, productType, products, + new Dictionary { { "$apply", clause } }); - return parser.ParseApply(); - } + return parser.ParseApply(); + } - private IEdmModel GetModel() where T : class - { - Type key = typeof(T); - IEdmModel value; + private IEdmModel GetModel() where T : class + { + Type key = typeof(T); + IEdmModel value; - if (!_modelCache.TryGetValue(key, out value)) + if (!_modelCache.TryGetValue(key, out value)) + { + ODataModelBuilder model = new ODataConventionModelBuilder(); + model.EntitySet("Products"); + if (key == typeof(Product)) { - ODataModelBuilder model = new ODataConventionModelBuilder(); - model.EntitySet("Products"); - if (key == typeof(Product)) - { - model.EntityType().DerivesFrom(); - model.EntityType().DerivesFrom(); - } - - value = _modelCache[key] = model.GetEdmModel(); + model.EntityType().DerivesFrom(); + model.EntityType().DerivesFrom(); } - return value; + + value = _modelCache[key] = model.GetEdmModel(); } + return value; + } - private class AggregationBinderEFFake : AggregationBinder + private class AggregationBinderEFFake : AggregationBinder + { + internal AggregationBinderEFFake(ODataQuerySettings settings, IAssemblyResolver assembliesResolver, Type elementType, IEdmModel model, TransformationNode transformation) + : base(settings, assembliesResolver, elementType, model, transformation) { - internal AggregationBinderEFFake(ODataQuerySettings settings, IAssemblyResolver assembliesResolver, Type elementType, IEdmModel model, TransformationNode transformation) - : base(settings, assembliesResolver, elementType, model, transformation) - { - } + } - internal override bool IsClassicEF(IQueryable query) - { - return true; - } + internal override bool IsClassicEF(IQueryable query) + { + return true; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/BinderExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/BinderExtensionsTests.cs index c2b8d7a0e..552e01c97 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/BinderExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/BinderExtensionsTests.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // // Copyright (c) .NET Foundation and Contributors. All rights reserved. // See License.txt in the project root for license information. @@ -21,371 +21,370 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions +namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions; + +public class BinderExtensionsTests { - public class BinderExtensionsTests + private static IEdmModel _model; + private static IEdmEntitySet _products; + private static IEdmStructuredTypeReference _productTypeReference; + + static BinderExtensionsTests() { - private static IEdmModel _model; - private static IEdmEntitySet _products; - private static IEdmStructuredTypeReference _productTypeReference; + _model = GetEdmModel(); - static BinderExtensionsTests() - { - _model = GetEdmModel(); + IEdmEntityType productType = _model.SchemaElements.OfType().Single(t => t.Name == "Product"); + Assert.NotNull(productType); // Guard - IEdmEntityType productType = _model.SchemaElements.OfType().Single(t => t.Name == "Product"); - Assert.NotNull(productType); // Guard + _products = _model.EntityContainer.FindEntitySet("Products"); + Assert.NotNull(_products); // Guard - _products = _model.EntityContainer.FindEntitySet("Products"); - Assert.NotNull(_products); // Guard + _productTypeReference = new EdmEntityTypeReference(productType, true); + } - _productTypeReference = new EdmEntityTypeReference(productType, true); - } + private static readonly ODataQuerySettings _defaultSettings = new ODataQuerySettings + { + HandleNullPropagation = HandleNullPropagationOption.False + }; - private static readonly ODataQuerySettings _defaultSettings = new ODataQuerySettings - { - HandleNullPropagation = HandleNullPropagationOption.False - }; + private static readonly ODataQuerySettings _defaultSettingsTrue = new ODataQuerySettings + { + HandleNullPropagation = HandleNullPropagationOption.True + }; - private static readonly ODataQuerySettings _defaultSettingsTrue = new ODataQuerySettings - { - HandleNullPropagation = HandleNullPropagationOption.True - }; + [Fact] + public void ApplyBind_OnIFilterBinder_WithEnumerable_ThrowsArgumentNull_ForInputs() + { + // Arrange & Act & Assert + IFilterBinder binder = null; + IEnumerable query = null; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "binder"); + + // Arrange & Act & Assert + binder = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "query"); + + // Arrange & Act & Assert + query = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "filterClause"); + + // Arrange & Act & Assert + FilterClause filterClause = new FilterClause(new Mock().Object, new Mock().Object); + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, filterClause, null), "context"); + } - [Fact] - public void ApplyBind_OnIFilterBinder_WithEnumerable_ThrowsArgumentNull_ForInputs() - { - // Arrange & Act & Assert - IFilterBinder binder = null; - IEnumerable query = null; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "binder"); - - // Arrange & Act & Assert - binder = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "query"); - - // Arrange & Act & Assert - query = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "filterClause"); - - // Arrange & Act & Assert - FilterClause filterClause = new FilterClause(new Mock().Object, new Mock().Object); - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, filterClause, null), "context"); - } - - [Fact] - public void ApplyBind_OnIFilterBinder_WithQueryable_ThrowsArgumentNull_ForInputs() - { - // Arrange & Act & Assert - IFilterBinder binder = null; - IQueryable query = null; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "binder"); - - // Arrange & Act & Assert - binder = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "query"); - - // Arrange & Act & Assert - query = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "filterClause"); - - // Arrange & Act & Assert - FilterClause filterClause = new FilterClause(new Mock().Object, new Mock().Object); - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, filterClause, null), "context"); - } - - [Fact] - public void ApplyBind_OnIFilterBinder_WithExpression_ThrowsArgumentNull_ForInputs() - { - // Arrange & Act & Assert - IFilterBinder binder = null; - Expression source = null; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "binder"); - - // Arrange & Act & Assert - binder = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "source"); - - // Arrange & Act & Assert - source = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "filterClause"); - - // Arrange & Act & Assert - FilterClause filterClause = new FilterClause(new Mock().Object, new Mock().Object); - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, filterClause, null), "context"); - } - - [Fact] - public void ApplyBind_OnIOrderByBinder_WithQueryable_ThrowsArgumentNull_ForInputs() - { - // Arrange & Act & Assert - IOrderByBinder binder = null; - IQueryable query = null; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null, true), "binder"); - - // Arrange & Act & Assert - binder = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null, true), "query"); - - // Arrange & Act & Assert - query = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null, true), "orderByClause"); - - // Arrange & Act & Assert - OrderByClause orderByClause = new OrderByClause(null, new Mock().Object, OrderByDirection.Descending, new Mock().Object); - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, orderByClause, null, true), "context"); - } - - [Fact] - public void ApplyBind_OnIOrderByBinder_WithExpression_ThrowsArgumentNull_ForInputs() - { - // Arrange & Act & Assert - IOrderByBinder binder = null; - Expression source = null; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null, true), "binder"); - - // Arrange & Act & Assert - binder = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null, true), "source"); - - // Arrange & Act & Assert - source = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null, true), "orderByClause"); - - // Arrange & Act & Assert - OrderByClause orderByClause = new OrderByClause(null, new Mock().Object, OrderByDirection.Descending, new Mock().Object); - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, orderByClause, null, true), "context"); - } - - [Fact] - public void ApplyBind_OnISelectExpandBinder_WithQueryable_ThrowsArgumentNull_ForInputs() - { - // Arrange & Act & Assert - ISelectExpandBinder binder = null; - IQueryable source = null; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "binder"); - - // Arrange & Act & Assert - binder = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "source"); - - // Arrange & Act & Assert - source = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "selectExpandClause"); - - // Arrange & Act & Assert - SelectExpandClause selectExpandClause = new SelectExpandClause(null, true); - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, selectExpandClause, null), "context"); - } - - [Fact] - public void ApplyBind_OnISelectExpandBinder_WithObject_ThrowsArgumentNull_ForInputs() - { - // Arrange & Act & Assert - ISelectExpandBinder binder = null; - object source = null; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "binder"); - - // Arrange & Act & Assert - binder = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "source"); - - // Arrange & Act & Assert - source = new object(); - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "selectExpandClause"); - - // Arrange & Act & Assert - SelectExpandClause selectExpandClause = new SelectExpandClause(null, true); - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, selectExpandClause, null), "context"); - } - - [Fact] - public void ApplyBind_OnISearchBinder_WithQueryable_ThrowsArgumentNull_ForInputs() - { - // Arrange & Act & Assert - ISearchBinder binder = null; - IQueryable query = null; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "binder"); - - // Arrange & Act & Assert - binder = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "source"); - - // Arrange & Act & Assert - query = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "searchClause"); - - // Arrange & Act & Assert - SearchClause searchClause = new SearchClause(new Mock().Object); - ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, searchClause, null), "context"); - } - - [Fact] - public void FilterBinder_ApplyBind_WorksForQueryable() - { - // Arrange - IQueryable products = new List().AsQueryable(); - QueryBinderContext context = new QueryBinderContext(_model, _defaultSettings, typeof(Product)); + [Fact] + public void ApplyBind_OnIFilterBinder_WithQueryable_ThrowsArgumentNull_ForInputs() + { + // Arrange & Act & Assert + IFilterBinder binder = null; + IQueryable query = null; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "binder"); + + // Arrange & Act & Assert + binder = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "query"); + + // Arrange & Act & Assert + query = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "filterClause"); + + // Arrange & Act & Assert + FilterClause filterClause = new FilterClause(new Mock().Object, new Mock().Object); + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, filterClause, null), "context"); + } + + [Fact] + public void ApplyBind_OnIFilterBinder_WithExpression_ThrowsArgumentNull_ForInputs() + { + // Arrange & Act & Assert + IFilterBinder binder = null; + Expression source = null; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "binder"); + + // Arrange & Act & Assert + binder = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "source"); + + // Arrange & Act & Assert + source = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "filterClause"); + + // Arrange & Act & Assert + FilterClause filterClause = new FilterClause(new Mock().Object, new Mock().Object); + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, filterClause, null), "context"); + } + + [Fact] + public void ApplyBind_OnIOrderByBinder_WithQueryable_ThrowsArgumentNull_ForInputs() + { + // Arrange & Act & Assert + IOrderByBinder binder = null; + IQueryable query = null; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null, true), "binder"); + + // Arrange & Act & Assert + binder = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null, true), "query"); + + // Arrange & Act & Assert + query = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null, true), "orderByClause"); + + // Arrange & Act & Assert + OrderByClause orderByClause = new OrderByClause(null, new Mock().Object, OrderByDirection.Descending, new Mock().Object); + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, orderByClause, null, true), "context"); + } + + [Fact] + public void ApplyBind_OnIOrderByBinder_WithExpression_ThrowsArgumentNull_ForInputs() + { + // Arrange & Act & Assert + IOrderByBinder binder = null; + Expression source = null; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null, true), "binder"); + + // Arrange & Act & Assert + binder = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null, true), "source"); + + // Arrange & Act & Assert + source = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null, true), "orderByClause"); + + // Arrange & Act & Assert + OrderByClause orderByClause = new OrderByClause(null, new Mock().Object, OrderByDirection.Descending, new Mock().Object); + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, orderByClause, null, true), "context"); + } + + [Fact] + public void ApplyBind_OnISelectExpandBinder_WithQueryable_ThrowsArgumentNull_ForInputs() + { + // Arrange & Act & Assert + ISelectExpandBinder binder = null; + IQueryable source = null; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "binder"); + + // Arrange & Act & Assert + binder = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "source"); + + // Arrange & Act & Assert + source = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "selectExpandClause"); + + // Arrange & Act & Assert + SelectExpandClause selectExpandClause = new SelectExpandClause(null, true); + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, selectExpandClause, null), "context"); + } + + [Fact] + public void ApplyBind_OnISelectExpandBinder_WithObject_ThrowsArgumentNull_ForInputs() + { + // Arrange & Act & Assert + ISelectExpandBinder binder = null; + object source = null; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "binder"); + + // Arrange & Act & Assert + binder = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "source"); + + // Arrange & Act & Assert + source = new object(); + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, null, null), "selectExpandClause"); + + // Arrange & Act & Assert + SelectExpandClause selectExpandClause = new SelectExpandClause(null, true); + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(source, selectExpandClause, null), "context"); + } - // Act & Assert - Mock binder = new Mock(); - FilterClause filterClause = new FilterClause(new Mock().Object, new Mock().Object); - Expression body = Expression.Constant(true);; - ParameterExpression filterParameter = context.CurrentParameter; - LambdaExpression filterExpr = Expression.Lambda(body, filterParameter); + [Fact] + public void ApplyBind_OnISearchBinder_WithQueryable_ThrowsArgumentNull_ForInputs() + { + // Arrange & Act & Assert + ISearchBinder binder = null; + IQueryable query = null; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "binder"); + + // Arrange & Act & Assert + binder = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "source"); + + // Arrange & Act & Assert + query = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, null, null), "searchClause"); + + // Arrange & Act & Assert + SearchClause searchClause = new SearchClause(new Mock().Object); + ExceptionAssert.ThrowsArgumentNull(() => binder.ApplyBind(query, searchClause, null), "context"); + } - binder.Setup(b => b.BindFilter(filterClause, context)).Returns(filterExpr); + [Fact] + public void FilterBinder_ApplyBind_WorksForQueryable() + { + // Arrange + IQueryable products = new List().AsQueryable(); + QueryBinderContext context = new QueryBinderContext(_model, _defaultSettings, typeof(Product)); - // Act - Expression result = binder.Object.ApplyBind(Expression.Constant(products), filterClause, context); + // Act & Assert + Mock binder = new Mock(); + FilterClause filterClause = new FilterClause(new Mock().Object, new Mock().Object); + Expression body = Expression.Constant(true);; + ParameterExpression filterParameter = context.CurrentParameter; + LambdaExpression filterExpr = Expression.Lambda(body, filterParameter); - // Assert - Assert.NotNull(result); - Assert.Equal("System.Collections.Generic.List`1[Microsoft.AspNetCore.OData.Tests.Models.Product].Where($it => True)", result.ToString()); - } + binder.Setup(b => b.BindFilter(filterClause, context)).Returns(filterExpr); - [Fact] - public void FilterBinder_ApplyBind_WorksForEnumerable() + // Act + Expression result = binder.Object.ApplyBind(Expression.Constant(products), filterClause, context); + + // Assert + Assert.NotNull(result); + Assert.Equal("System.Collections.Generic.List`1[Microsoft.AspNetCore.OData.Tests.Models.Product].Where($it => True)", result.ToString()); + } + + [Fact] + public void FilterBinder_ApplyBind_WorksForEnumerable() + { + // Arrange + IEnumerable products = new[] { - // Arrange - IEnumerable products = new[] - { - new Product { ProductID = 1, ProductName = "abc" }, - new Product { ProductID = 2, ProductName = null }, - new Product { ProductID = 3, ProductName = "xyz" }, - }; + new Product { ProductID = 1, ProductName = "abc" }, + new Product { ProductID = 2, ProductName = null }, + new Product { ProductID = 3, ProductName = "xyz" }, + }; - // Act & Assert - RunFilterTestAndVerify(products, "ProductName eq null", new[] { 2 }, "$it => ($it.ProductName == null)"); + // Act & Assert + RunFilterTestAndVerify(products, "ProductName eq null", new[] { 2 }, "$it => ($it.ProductName == null)"); - // Act & Assert - RunFilterTestAndVerify(products, "ProductName ne null", new[] { 1, 3 }, "$it => ($it.ProductName != null)"); - } + // Act & Assert + RunFilterTestAndVerify(products, "ProductName ne null", new[] { 1, 3 }, "$it => ($it.ProductName != null)"); + } - [Fact] - public void BindCountNode_Works() + [Fact] + public void BindCountNode_Works() + { + // Arrange + IEnumerable products = new[] { - // Arrange - IEnumerable products = new[] + new Product { - new Product + ProductID = 1, + AlternateAddresses = new Address[] // has 2 address items whose HourseNumber > 8 { - ProductID = 1, - AlternateAddresses = new Address[] // has 2 address items whose HourseNumber > 8 - { - new Address { HouseNumber = 6 }, - new Address { HouseNumber = 9 }, // > 8 - new Address { HouseNumber = 10 }, // > 8 - new Address { HouseNumber = 3 }, - new Address { HouseNumber = 1 }, - } - }, - new Product + new Address { HouseNumber = 6 }, + new Address { HouseNumber = 9 }, // > 8 + new Address { HouseNumber = 10 }, // > 8 + new Address { HouseNumber = 3 }, + new Address { HouseNumber = 1 }, + } + }, + new Product + { + ProductID = 2, + AlternateAddresses = new Address[] // only 1 address item whose HourseNumber > 8 { - ProductID = 2, - AlternateAddresses = new Address[] // only 1 address item whose HourseNumber > 8 - { - new Address { HouseNumber = 5 }, - new Address { HouseNumber = 6 }, - new Address { HouseNumber = 9 }, // > 8 - new Address { HouseNumber = 3 }, - } - }, - new Product + new Address { HouseNumber = 5 }, + new Address { HouseNumber = 6 }, + new Address { HouseNumber = 9 }, // > 8 + new Address { HouseNumber = 3 }, + } + }, + new Product + { + ProductID = 3, + AlternateAddresses = new Address[] // has 3 address items whose HourseNumber > 8 { - ProductID = 3, - AlternateAddresses = new Address[] // has 3 address items whose HourseNumber > 8 - { - new Address { HouseNumber = 10 }, - new Address { HouseNumber = 11 }, - new Address { HouseNumber = 9 }, - } - }, - }; - - // Act & Assert - string filter = "AlternateAddresses/$count eq 5"; - string expectedExpr = "$it => ($it.AlternateAddresses.LongCount() == 5)"; - RunFilterTestAndVerify(products, filter, new[] { 1 }, expectedExpr); - - // Act & Assert - filter = "AlternateAddresses/$count in [3,4]"; - expectedExpr = "$it => System.Collections.Generic.List`1[System.Int64].Contains($it.AlternateAddresses.LongCount())"; - RunFilterTestAndVerify(products, filter, new[] { 2, 3 }, expectedExpr); - - // Act & Assert - filter = "AlternateAddresses/$count($filter=HouseNumber gt 8) gt 2"; - expectedExpr = "$it => ($it.AlternateAddresses.Where($it => ($it.HouseNumber > 8)).LongCount() > 2)"; - RunFilterTestAndVerify(products, filter, new[] { 3 }, expectedExpr); - } - - private static Expression BindFilter(string filter, IEdmModel model, ODataQuerySettings querySettings = null, IAssemblyResolver assembliesResolver = null) - { - Type elementType = typeof(T); - FilterClause orderByClause = CreateFilterClause(filter, model, elementType); - Assert.NotNull(orderByClause); + new Address { HouseNumber = 10 }, + new Address { HouseNumber = 11 }, + new Address { HouseNumber = 9 }, + } + }, + }; - querySettings = querySettings ?? _defaultSettings; - QueryBinderContext context = new QueryBinderContext(model, querySettings, elementType) - { - AssembliesResolver = assembliesResolver, - }; + // Act & Assert + string filter = "AlternateAddresses/$count eq 5"; + string expectedExpr = "$it => ($it.AlternateAddresses.LongCount() == 5)"; + RunFilterTestAndVerify(products, filter, new[] { 1 }, expectedExpr); - IFilterBinder filterBinder = new FilterBinder(); - return filterBinder.BindFilter(orderByClause, context); - } + // Act & Assert + filter = "AlternateAddresses/$count in [3,4]"; + expectedExpr = "$it => System.Collections.Generic.List`1[System.Int64].Contains($it.AlternateAddresses.LongCount())"; + RunFilterTestAndVerify(products, filter, new[] { 2, 3 }, expectedExpr); - private void RunFilterTestAndVerify(IEnumerable products, string filter, int[] expectedIds, string expectedExpr, ODataQuerySettings querySettings = null, IAssemblyResolver assembliesResolver = null) + // Act & Assert + filter = "AlternateAddresses/$count($filter=HouseNumber gt 8) gt 2"; + expectedExpr = "$it => ($it.AlternateAddresses.Where($it => ($it.HouseNumber > 8)).LongCount() > 2)"; + RunFilterTestAndVerify(products, filter, new[] { 3 }, expectedExpr); + } + + private static Expression BindFilter(string filter, IEdmModel model, ODataQuerySettings querySettings = null, IAssemblyResolver assembliesResolver = null) + { + Type elementType = typeof(T); + FilterClause orderByClause = CreateFilterClause(filter, model, elementType); + Assert.NotNull(orderByClause); + + querySettings = querySettings ?? _defaultSettings; + QueryBinderContext context = new QueryBinderContext(model, querySettings, elementType) { - // Act - Bind string to Linq.Expression - Expression filterExpr = BindFilter(filter, _model, querySettings, assembliesResolver); + AssembliesResolver = assembliesResolver, + }; - // Assert - string actualExpr = ExpressionStringBuilder.ToString(filterExpr); - Assert.Equal(expectedExpr, actualExpr); + IFilterBinder filterBinder = new FilterBinder(); + return filterBinder.BindFilter(orderByClause, context); + } + + private void RunFilterTestAndVerify(IEnumerable products, string filter, int[] expectedIds, string expectedExpr, ODataQuerySettings querySettings = null, IAssemblyResolver assembliesResolver = null) + { + // Act - Bind string to Linq.Expression + Expression filterExpr = BindFilter(filter, _model, querySettings, assembliesResolver); - // Act - IEnumerable results = InvokeFilter(products, filterExpr); + // Assert + string actualExpr = ExpressionStringBuilder.ToString(filterExpr); + Assert.Equal(expectedExpr, actualExpr); - // Assert - Assert.True(expectedIds.SequenceEqual(results.Select(a => a.ProductID))); // ordered id - } + // Act + IEnumerable results = InvokeFilter(products, filterExpr); - public static IEnumerable InvokeFilter(IEnumerable collection, Expression filterExpr) - { - LambdaExpression filterLambda = filterExpr as LambdaExpression; - Assert.NotNull(filterLambda); + // Assert + Assert.True(expectedIds.SequenceEqual(results.Select(a => a.ProductID))); // ordered id + } - Type type = typeof(T); + public static IEnumerable InvokeFilter(IEnumerable collection, Expression filterExpr) + { + LambdaExpression filterLambda = filterExpr as LambdaExpression; + Assert.NotNull(filterLambda); - Delegate function = filterLambda.Compile(); + Type type = typeof(T); - MethodInfo whereMethod = ExpressionHelperMethods.EnumerableWhereGeneric.MakeGenericMethod(type); - return whereMethod.Invoke(null, new object[] { collection, function }) as IEnumerable; - } + Delegate function = filterLambda.Compile(); - private static FilterClause CreateFilterClause(string filter, IEdmModel model, Type type) - { - IEdmEntityType entityType = model.SchemaElements.OfType().Single(t => t.Name == type.Name); - Assert.NotNull(entityType); // Guard + MethodInfo whereMethod = ExpressionHelperMethods.EnumerableWhereGeneric.MakeGenericMethod(type); + return whereMethod.Invoke(null, new object[] { collection, function }) as IEnumerable; + } - IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Products"); - Assert.NotNull(entitySet); // Guard + private static FilterClause CreateFilterClause(string filter, IEdmModel model, Type type) + { + IEdmEntityType entityType = model.SchemaElements.OfType().Single(t => t.Name == type.Name); + Assert.NotNull(entityType); // Guard - ODataQueryOptionParser parser = new ODataQueryOptionParser(model, entityType, entitySet, - new Dictionary { { "$filter", filter } }); + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Products"); + Assert.NotNull(entitySet); // Guard - return parser.ParseFilter(); - } + ODataQueryOptionParser parser = new ODataQueryOptionParser(model, entityType, entitySet, + new Dictionary { { "$filter", filter } }); - private static IEdmModel GetEdmModel() - { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Products"); - builder.EntityType().DerivesFrom(); - builder.EntityType().DerivesFrom(); - return builder.GetEdmModel(); - } + return parser.ParseFilter(); + } + + private static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Products"); + builder.EntityType().DerivesFrom(); + builder.EntityType().DerivesFrom(); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/ComputeBinderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/ComputeBinderTests.cs index 03bcfe85e..7c52c0fed 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/ComputeBinderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/ComputeBinderTests.cs @@ -18,340 +18,339 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions +namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions; + +public class ComputeBinderTests { - public class ComputeBinderTests + private static IEdmModel _model = GetEdmModel(); + + [Fact] + public void FilterBinder_BindsComputedPropertyInDollarFilter_FromDollarCompute() { - private static IEdmModel _model = GetEdmModel(); + // Arrange + QueryClause clause = CreateQueryClause("$filter=Total lt 30&$compute=Price mul Qty as Total", _model, typeof(ComputeCustomer)); - [Fact] - public void FilterBinder_BindsComputedPropertyInDollarFilter_FromDollarCompute() + ODataQuerySettings querySettings = new ODataQuerySettings(); + IFilterBinder binder = new FilterBinder(); + QueryBinderContext context = new QueryBinderContext(_model, querySettings, typeof(ComputeCustomer)); + if (clause.Compute != null) { - // Arrange - QueryClause clause = CreateQueryClause("$filter=Total lt 30&$compute=Price mul Qty as Total", _model, typeof(ComputeCustomer)); + context.AddComputedProperties(clause.Compute.ComputedItems); + } - ODataQuerySettings querySettings = new ODataQuerySettings(); - IFilterBinder binder = new FilterBinder(); - QueryBinderContext context = new QueryBinderContext(_model, querySettings, typeof(ComputeCustomer)); - if (clause.Compute != null) - { - context.AddComputedProperties(clause.Compute.ComputedItems); - } + // Act + Expression filterExp = binder.BindFilter(clause.Filter, context); - // Act - Expression filterExp = binder.BindFilter(clause.Filter, context); + // Assert + string resultExpression = ExpressionStringBuilder.ToString(filterExp); + Assert.Equal("$it => (Convert(($it.Price * Convert($it.Qty))) < Convert(30))", resultExpression); - // Assert - string resultExpression = ExpressionStringBuilder.ToString(filterExp); - Assert.Equal("$it => (Convert(($it.Price * Convert($it.Qty))) < Convert(30))", resultExpression); + Assert.True(FilterBinderTests.InvokeFilter(new ComputeCustomer { Price = 10, Qty = 2 }, filterExp)); + Assert.False(FilterBinderTests.InvokeFilter(new ComputeCustomer { Price = 10, Qty = 4 }, filterExp)); + } - Assert.True(FilterBinderTests.InvokeFilter(new ComputeCustomer { Price = 10, Qty = 2 }, filterExp)); - Assert.False(FilterBinderTests.InvokeFilter(new ComputeCustomer { Price = 10, Qty = 4 }, filterExp)); - } + [Fact] + public void OrderByBinder_BindsComputedPropertyInDollarOrderBy_FromDollarCompute() + { + // Arrange + QueryClause clause = CreateQueryClause("$orderby=Total&$compute=Price mul Qty as Total", _model, typeof(ComputeCustomer)); - [Fact] - public void OrderByBinder_BindsComputedPropertyInDollarOrderBy_FromDollarCompute() + ODataQuerySettings querySettings = new ODataQuerySettings(); + IOrderByBinder binder = new OrderByBinder(); + QueryBinderContext context = new QueryBinderContext(_model, querySettings, typeof(ComputeCustomer)); + if (clause.Compute != null) { - // Arrange - QueryClause clause = CreateQueryClause("$orderby=Total&$compute=Price mul Qty as Total", _model, typeof(ComputeCustomer)); + context.AddComputedProperties(clause.Compute.ComputedItems); + } - ODataQuerySettings querySettings = new ODataQuerySettings(); - IOrderByBinder binder = new OrderByBinder(); - QueryBinderContext context = new QueryBinderContext(_model, querySettings, typeof(ComputeCustomer)); - if (clause.Compute != null) - { - context.AddComputedProperties(clause.Compute.ComputedItems); - } + // Act + OrderByBinderResult orderByResult = binder.BindOrderBy(clause.OrderBy, context); - // Act - OrderByBinderResult orderByResult = binder.BindOrderBy(clause.OrderBy, context); + // Assert + Assert.Null(orderByResult.ThenBy); + Assert.Equal(OrderByDirection.Ascending, orderByResult.Direction); + string resultExpression = ExpressionStringBuilder.ToString(orderByResult.OrderByExpression); + Assert.Equal("$it => ($it.Price * Convert($it.Qty))", resultExpression); - // Assert - Assert.Null(orderByResult.ThenBy); - Assert.Equal(OrderByDirection.Ascending, orderByResult.Direction); - string resultExpression = ExpressionStringBuilder.ToString(orderByResult.OrderByExpression); - Assert.Equal("$it => ($it.Price * Convert($it.Qty))", resultExpression); + IEnumerable customers = new[] + { + new ComputeCustomer { Id = 1, Qty = 3, Price = 5.99 }, + new ComputeCustomer { Id = 2, Qty = 5, Price = 2.99 }, + new ComputeCustomer { Id = 3, Qty = 2, Price = 18.01 }, + new ComputeCustomer { Id = 4, Qty = 4, Price = 9.99 }, + }; - IEnumerable customers = new[] - { - new ComputeCustomer { Id = 1, Qty = 3, Price = 5.99 }, - new ComputeCustomer { Id = 2, Qty = 5, Price = 2.99 }, - new ComputeCustomer { Id = 3, Qty = 2, Price = 18.01 }, - new ComputeCustomer { Id = 4, Qty = 4, Price = 9.99 }, - }; + var orderedResult = OrderByBinderTests.InvokeOrderBy(customers, orderByResult.OrderByExpression, orderByResult.Direction, false); - var orderedResult = OrderByBinderTests.InvokeOrderBy(customers, orderByResult.OrderByExpression, orderByResult.Direction, false); + Assert.True(new [] { 2, 1, 3, 4 }.SequenceEqual(orderedResult.Select(a => a.Id))); // ordered id + } - Assert.True(new [] { 2, 1, 3, 4 }.SequenceEqual(orderedResult.Select(a => a.Id))); // ordered id - } + [Fact] + public void SelectExpandBinder_BindsComputedPropertyInSelect_FromDollarCompute() + { + // Arrange + QueryClause clause = CreateQueryClause("$select=Name,Total&$compute=Price mul Qty as Total", _model, typeof(ComputeCustomer)); - [Fact] - public void SelectExpandBinder_BindsComputedPropertyInSelect_FromDollarCompute() + ODataQuerySettings querySettings = new ODataQuerySettings(); + SelectExpandBinder binder = new SelectExpandBinder(); + QueryBinderContext context = new QueryBinderContext(_model, querySettings, typeof(ComputeCustomer)); + if (clause.Compute != null) { - // Arrange - QueryClause clause = CreateQueryClause("$select=Name,Total&$compute=Price mul Qty as Total", _model, typeof(ComputeCustomer)); + context.AddComputedProperties(clause.Compute.ComputedItems); + } - ODataQuerySettings querySettings = new ODataQuerySettings(); - SelectExpandBinder binder = new SelectExpandBinder(); - QueryBinderContext context = new QueryBinderContext(_model, querySettings, typeof(ComputeCustomer)); - if (clause.Compute != null) + // Act + Expression selectExp = binder.BindSelectExpand(clause.SelectExpand, context); + + // Assert + Assert.NotNull(selectExp); + string resultExpression = ExpressionStringBuilder.ToString(selectExp); + Assert.Equal("$it => new SelectSome`1() {" + + "Model = Microsoft.OData.Edm.EdmModel, " + + "Container = new NamedPropertyWithNext1`1() {" + + "Name = Name, " + + "Value = $it.Name, " + + "Next0 = new NamedProperty`1() {" + + "Name = Total, " + + "Value = ($it.Price * Convert($it.Qty)), " + + "}, " + + "Next1 = new AutoSelectedNamedProperty`1() {" + + "Name = Id, " + + "Value = Convert($it.Id), " + + "}, }, }", resultExpression); + + ComputeCustomer customer = new ComputeCustomer { Name = "Peter", Price = 1.99, Qty = 3 }; + IDictionary result = SelectExpandBinderTest.InvokeSelectExpand(customer, selectExp); + + Assert.Equal(2, result.Count); + Assert.Collection(result, + e => { - context.AddComputedProperties(clause.Compute.ComputedItems); - } + Assert.Equal("Name", e.Key); + Assert.Equal("Peter", e.Value); + }, + e => + { + Assert.Equal("Total", e.Key); + Assert.Equal(5.97, e.Value); + }); + } - // Act - Expression selectExp = binder.BindSelectExpand(clause.SelectExpand, context); + [Fact] + public void SelectExpandBinder_BindsComputedPropertyInNestedSelect_FromDollarCompute() + { + // Arrange + QueryClause clause = CreateQueryClause("$select=Location($select=StateCode;$compute=Zipcode div 1000 as StateCode)", _model, typeof(ComputeCustomer)); - // Assert - Assert.NotNull(selectExp); - string resultExpression = ExpressionStringBuilder.ToString(selectExp); - Assert.Equal("$it => new SelectSome`1() {" + - "Model = Microsoft.OData.Edm.EdmModel, " + - "Container = new NamedPropertyWithNext1`1() {" + - "Name = Name, " + - "Value = $it.Name, " + - "Next0 = new NamedProperty`1() {" + - "Name = Total, " + - "Value = ($it.Price * Convert($it.Qty)), " + - "}, " + - "Next1 = new AutoSelectedNamedProperty`1() {" + - "Name = Id, " + - "Value = Convert($it.Id), " + - "}, }, }", resultExpression); - - ComputeCustomer customer = new ComputeCustomer { Name = "Peter", Price = 1.99, Qty = 3 }; - IDictionary result = SelectExpandBinderTest.InvokeSelectExpand(customer, selectExp); - - Assert.Equal(2, result.Count); - Assert.Collection(result, - e => - { - Assert.Equal("Name", e.Key); - Assert.Equal("Peter", e.Value); - }, - e => - { - Assert.Equal("Total", e.Key); - Assert.Equal(5.97, e.Value); - }); + ODataQuerySettings querySettings = new ODataQuerySettings(); + SelectExpandBinder binder = new SelectExpandBinder(); + QueryBinderContext context = new QueryBinderContext(_model, querySettings, typeof(ComputeCustomer)); + if (clause.Compute != null) + { + context.AddComputedProperties(clause.Compute.ComputedItems); } - [Fact] - public void SelectExpandBinder_BindsComputedPropertyInNestedSelect_FromDollarCompute() + // Act + Expression selectExp = binder.BindSelectExpand(clause.SelectExpand, context); + + // Assert + Assert.NotNull(selectExp); + string resultExpression = ExpressionStringBuilder.ToString(selectExp); + Assert.Equal("$it => new SelectSome`1() {" + + "Model = Microsoft.OData.Edm.EdmModel, " + + "Container = new NamedPropertyWithNext0`1() {" + + "Name = Location, " + + "Value = new SelectSome`1() {" + + "Model = Microsoft.OData.Edm.EdmModel, " + + "Container = new NamedProperty`1() {" + + "Name = StateCode, " + + "Value = ($it.Location.Zipcode / 1000), " + + "}, " + + "}, " + + "Next0 = new AutoSelectedNamedProperty`1() {Name = Id, Value = Convert($it.Id), }, }, }", resultExpression); + + ComputeCustomer customer = new ComputeCustomer { - // Arrange - QueryClause clause = CreateQueryClause("$select=Location($select=StateCode;$compute=Zipcode div 1000 as StateCode)", _model, typeof(ComputeCustomer)); + Location = new ComputeAddress { Zipcode = 98029 } + }; + IDictionary result = SelectExpandBinderTest.InvokeSelectExpand(customer, selectExp); + + var location = Assert.Single(result); + Assert.Equal("Location", location.Key); + SelectExpandWrapper itemWrapper = location.Value as SelectExpandWrapper; + IDictionary locationResult = itemWrapper.ToDictionary(); + + var stateCode = Assert.Single(locationResult); + Assert.Equal("StateCode", stateCode.Key); + Assert.Equal(98, stateCode.Value); + } - ODataQuerySettings querySettings = new ODataQuerySettings(); - SelectExpandBinder binder = new SelectExpandBinder(); - QueryBinderContext context = new QueryBinderContext(_model, querySettings, typeof(ComputeCustomer)); - if (clause.Compute != null) - { - context.AddComputedProperties(clause.Compute.ComputedItems); - } + [Fact] + public void SelectExpandBinder_BindsComputedPropertyInExpand_FromDollarCompute() + { + // Arrange + QueryClause clause = CreateQueryClause("$expand=Orders($select=Title,Tax;$compute=Amount mul TaxRate as Tax)", _model, typeof(ComputeCustomer)); - // Act - Expression selectExp = binder.BindSelectExpand(clause.SelectExpand, context); + ODataQuerySettings querySettings = new ODataQuerySettings(); + SelectExpandBinder binder = new SelectExpandBinder(); + QueryBinderContext context = new QueryBinderContext(_model, querySettings, typeof(ComputeCustomer)); + if (clause.Compute != null) + { + context.AddComputedProperties(clause.Compute.ComputedItems); + } - // Assert - Assert.NotNull(selectExp); - string resultExpression = ExpressionStringBuilder.ToString(selectExp); - Assert.Equal("$it => new SelectSome`1() {" + + // Act + Expression selectExp = binder.BindSelectExpand(clause.SelectExpand, context); + + // Assert + Assert.NotNull(selectExp); + string resultExpression = ExpressionStringBuilder.ToString(selectExp); + Assert.Equal("$it => new SelectAllAndExpand`1() {Model = Microsoft.OData.Edm.EdmModel, " + + "Instance = $it, UseInstanceForProperties = True, " + + "Container = new NamedPropertyWithNext0`1() " + + "{" + + "Name = Orders, " + + "Value = $it.Orders.Select($it => new SelectSome`1() " + + "{" + "Model = Microsoft.OData.Edm.EdmModel, " + - "Container = new NamedPropertyWithNext0`1() {" + - "Name = Location, " + - "Value = new SelectSome`1() {" + - "Model = Microsoft.OData.Edm.EdmModel, " + - "Container = new NamedProperty`1() {" + - "Name = StateCode, " + - "Value = ($it.Location.Zipcode / 1000), " + - "}, " + + "Container = new NamedPropertyWithNext1`1() " + + "{" + + "Name = Title, " + + "Value = $it.Title, " + + "Next0 = new NamedProperty`1() {Name = Tax, Value = (Convert($it.Amount) * $it.TaxRate), }, " + + "Next1 = new AutoSelectedNamedProperty`1() " + + "{" + + "Name = Id, Value = Convert($it.Id), " + "}, " + - "Next0 = new AutoSelectedNamedProperty`1() {Name = Id, Value = Convert($it.Id), }, }, }", resultExpression); + "}, " + + "}), " + + "Next0 = new NamedProperty`1() {Name = Dynamics, Value = $it.Dynamics, }, }, }", resultExpression); - ComputeCustomer customer = new ComputeCustomer - { - Location = new ComputeAddress { Zipcode = 98029 } - }; - IDictionary result = SelectExpandBinderTest.InvokeSelectExpand(customer, selectExp); - - var location = Assert.Single(result); - Assert.Equal("Location", location.Key); - SelectExpandWrapper itemWrapper = location.Value as SelectExpandWrapper; - IDictionary locationResult = itemWrapper.ToDictionary(); - - var stateCode = Assert.Single(locationResult); - Assert.Equal("StateCode", stateCode.Key); - Assert.Equal(98, stateCode.Value); - } - - [Fact] - public void SelectExpandBinder_BindsComputedPropertyInExpand_FromDollarCompute() + ComputeCustomer customer = new ComputeCustomer { - // Arrange - QueryClause clause = CreateQueryClause("$expand=Orders($select=Title,Tax;$compute=Amount mul TaxRate as Tax)", _model, typeof(ComputeCustomer)); - - ODataQuerySettings querySettings = new ODataQuerySettings(); - SelectExpandBinder binder = new SelectExpandBinder(); - QueryBinderContext context = new QueryBinderContext(_model, querySettings, typeof(ComputeCustomer)); - if (clause.Compute != null) + Orders = new List { - context.AddComputedProperties(clause.Compute.ComputedItems); + new ComputeOrder { Title = "Kerry", Amount = 4, TaxRate = 0.35 }, + new ComputeOrder { Title = "WU", Amount = 6, TaxRate = 0.5 }, + new ComputeOrder { Title = "XU", Amount = 5, TaxRate = 0.12 }, } + }; + IDictionary result = SelectExpandBinderTest.InvokeSelectExpand(customer, selectExp); - // Act - Expression selectExp = binder.BindSelectExpand(clause.SelectExpand, context); + Assert.Equal(8, result.Count); // Because it's select-all - // Assert - Assert.NotNull(selectExp); - string resultExpression = ExpressionStringBuilder.ToString(selectExp); - Assert.Equal("$it => new SelectAllAndExpand`1() {Model = Microsoft.OData.Edm.EdmModel, " + - "Instance = $it, UseInstanceForProperties = True, " + - "Container = new NamedPropertyWithNext0`1() " + - "{" + - "Name = Orders, " + - "Value = $it.Orders.Select($it => new SelectSome`1() " + - "{" + - "Model = Microsoft.OData.Edm.EdmModel, " + - "Container = new NamedPropertyWithNext1`1() " + - "{" + - "Name = Title, " + - "Value = $it.Title, " + - "Next0 = new NamedProperty`1() {Name = Tax, Value = (Convert($it.Amount) * $it.TaxRate), }, " + - "Next1 = new AutoSelectedNamedProperty`1() " + - "{" + - "Name = Id, Value = Convert($it.Id), " + - "}, " + - "}, " + - "}), " + - "Next0 = new NamedProperty`1() {Name = Dynamics, Value = $it.Dynamics, }, }, }", resultExpression); - - ComputeCustomer customer = new ComputeCustomer - { - Orders = new List - { - new ComputeOrder { Title = "Kerry", Amount = 4, TaxRate = 0.35 }, - new ComputeOrder { Title = "WU", Amount = 6, TaxRate = 0.5 }, - new ComputeOrder { Title = "XU", Amount = 5, TaxRate = 0.12 }, - } - }; - IDictionary result = SelectExpandBinderTest.InvokeSelectExpand(customer, selectExp); - - Assert.Equal(8, result.Count); // Because it's select-all - - int idx = 0; - var ordersValue = result["Orders"] as IEnumerable; - foreach (var order in ordersValue) - { - SelectExpandWrapper itemWrapper = order as SelectExpandWrapper; + int idx = 0; + var ordersValue = result["Orders"] as IEnumerable; + foreach (var order in ordersValue) + { + SelectExpandWrapper itemWrapper = order as SelectExpandWrapper; - var orderDic = itemWrapper.ToDictionary(); - Assert.Equal(customer.Orders[idx].Title, orderDic["Title"]); - Assert.Equal(customer.Orders[idx].Amount * customer.Orders[idx].TaxRate, orderDic["Tax"]); - idx++; - } + var orderDic = itemWrapper.ToDictionary(); + Assert.Equal(customer.Orders[idx].Title, orderDic["Title"]); + Assert.Equal(customer.Orders[idx].Amount * customer.Orders[idx].TaxRate, orderDic["Tax"]); + idx++; } + } - private static QueryClause CreateQueryClause(string query, IEdmModel model, Type type) - { - IEdmEntityType entityType = model.SchemaElements.OfType().Single(t => t.Name == type.Name); - Assert.NotNull(entityType); // Guard + private static QueryClause CreateQueryClause(string query, IEdmModel model, Type type) + { + IEdmEntityType entityType = model.SchemaElements.OfType().Single(t => t.Name == type.Name); + Assert.NotNull(entityType); // Guard - IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers"); - Assert.NotNull(entitySet); // Guard + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers"); + Assert.NotNull(entitySet); // Guard - string[] queryItems = query.Split('&'); + string[] queryItems = query.Split('&'); - Dictionary queries = new Dictionary(); - foreach (string item in queryItems) + Dictionary queries = new Dictionary(); + foreach (string item in queryItems) + { + if (item.StartsWith("$select=", StringComparison.Ordinal)) { - if (item.StartsWith("$select=", StringComparison.Ordinal)) - { - queries["$select"] = item.Substring(8); - } - else if (item.StartsWith("$expand=", StringComparison.Ordinal)) - { - queries["$expand"] = item.Substring(8); - } - else if (item.StartsWith("$filter=", StringComparison.Ordinal)) - { - queries["$filter"] = item.Substring(8); - } - else if (item.StartsWith("$orderby=", StringComparison.Ordinal)) - { - queries["$orderby"] = item.Substring(9); - } - else if (item.StartsWith("$compute=", StringComparison.Ordinal)) - { - queries["$compute"] = item.Substring(9); - } + queries["$select"] = item.Substring(8); } - - ODataQueryOptionParser parser = new ODataQueryOptionParser(model, entityType, entitySet, queries); - return new QueryClause + else if (item.StartsWith("$expand=", StringComparison.Ordinal)) { - Filter = parser.ParseFilter(), - OrderBy = parser.ParseOrderBy(), - SelectExpand = parser.ParseSelectAndExpand(), - Compute = parser.ParseCompute() - }; + queries["$expand"] = item.Substring(8); + } + else if (item.StartsWith("$filter=", StringComparison.Ordinal)) + { + queries["$filter"] = item.Substring(8); + } + else if (item.StartsWith("$orderby=", StringComparison.Ordinal)) + { + queries["$orderby"] = item.Substring(9); + } + else if (item.StartsWith("$compute=", StringComparison.Ordinal)) + { + queries["$compute"] = item.Substring(9); + } } - private static IEdmModel GetEdmModel() + ODataQueryOptionParser parser = new ODataQueryOptionParser(model, entityType, entitySet, queries); + return new QueryClause { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - builder.Namespace = "NS"; - builder.EntitySet("Customers"); - builder.EntitySet("Orders"); - return builder.GetEdmModel(); - } + Filter = parser.ParseFilter(), + OrderBy = parser.ParseOrderBy(), + SelectExpand = parser.ParseSelectAndExpand(), + Compute = parser.ParseCompute() + }; + } - private class QueryClause - { - public FilterClause Filter { get; set; } - public OrderByClause OrderBy { get; set; } - public SelectExpandClause SelectExpand { get; set; } - public ComputeClause Compute { get; set; } - } + private static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.Namespace = "NS"; + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); + return builder.GetEdmModel(); } - public class ComputeCustomer + private class QueryClause { - public int Id { get; set; } + public FilterClause Filter { get; set; } + public OrderByClause OrderBy { get; set; } + public SelectExpandClause SelectExpand { get; set; } + public ComputeClause Compute { get; set; } + } +} - public string Name { get; set; } +public class ComputeCustomer +{ + public int Id { get; set; } - public int Age { get; set; } + public string Name { get; set; } - public double Price { get; set; } + public int Age { get; set; } - public int Qty { get; set; } + public double Price { get; set; } - public ComputeAddress Location { get; set; } + public int Qty { get; set; } - public IList Orders { get; set; } + public ComputeAddress Location { get; set; } - public IDictionary Dynamics { get; set; } - } + public IList Orders { get; set; } - public class ComputeAddress - { - public string Street { get; set; } + public IDictionary Dynamics { get; set; } +} - public int Zipcode { get; set; } +public class ComputeAddress +{ + public string Street { get; set; } - public IDictionary Dynamics { get; set; } - } + public int Zipcode { get; set; } - public class ComputeOrder - { - public int Id { get; set; } + public IDictionary Dynamics { get; set; } +} + +public class ComputeOrder +{ + public int Id { get; set; } - public string Title { get; set; } - public int Amount { get; set; } - public double Price { get; set; } - public double TaxRate { get; set; } + public string Title { get; set; } + public int Amount { get; set; } + public double Price { get; set; } + public double TaxRate { get; set; } - public IDictionary Dynamics { get; set; } - } + public IDictionary Dynamics { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/ExpressionBinderBaseTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/ExpressionBinderBaseTests.cs index e3cb76f31..e51d02a99 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/ExpressionBinderBaseTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/ExpressionBinderBaseTests.cs @@ -17,57 +17,56 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions +namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions; + +/// +/// Tests to ExpressionBinderBase binder. +/// +public class ExpressionBinderBaseTests { - /// - /// Tests to ExpressionBinderBase binder. - /// - public class ExpressionBinderBaseTests + [Fact] + public void RetrieveClrTypeForConstant_WorksForEnum() { - [Fact] - public void RetrieveClrTypeForConstant_WorksForEnum() - { - var builder = new ODataConventionModelBuilder(); - builder.ComplexType
(); - var model = builder.GetEdmModel(); - var enumType = model.SchemaElements.OfType().First(); - var enumTypeRef = new EdmEnumTypeReference(enumType, true); + var builder = new ODataConventionModelBuilder(); + builder.ComplexType
(); + var model = builder.GetEdmModel(); + var enumType = model.SchemaElements.OfType().First(); + var enumTypeRef = new EdmEnumTypeReference(enumType, true); - MyExpressionBinder binder = new MyExpressionBinder(model, new ODataQuerySettings()); - object enumValue = new ODataEnumValue("low"); - Type type = binder.RetrieveClrTypeForConstant(enumTypeRef, ref enumValue); - Assert.NotNull(type); - Assert.Equal(typeof(Level), type); - Assert.Equal(Level.Low, enumValue); - } + MyExpressionBinder binder = new MyExpressionBinder(model, new ODataQuerySettings()); + object enumValue = new ODataEnumValue("low"); + Type type = binder.RetrieveClrTypeForConstant(enumTypeRef, ref enumValue); + Assert.NotNull(type); + Assert.Equal(typeof(Level), type); + Assert.Equal(Level.Low, enumValue); + } - public class Address - { - public Level Level { get; set; } - } + public class Address + { + public Level Level { get; set; } + } - [DataContract(Name = "level")] - public enum Level - { - [EnumMember(Value = "low")] - Low, + [DataContract(Name = "level")] + public enum Level + { + [EnumMember(Value = "low")] + Low, - [EnumMember(Value = "veryhigh")] - High - } + [EnumMember(Value = "veryhigh")] + High } +} - public class MyExpressionBinder : ExpressionBinderBase +public class MyExpressionBinder : ExpressionBinderBase +{ + public MyExpressionBinder(IEdmModel model, ODataQuerySettings querySettings) : base(model, querySettings) { - public MyExpressionBinder(IEdmModel model, ODataQuerySettings querySettings) : base(model, querySettings) - { - } + } - protected override ParameterExpression Parameter => throw new NotImplementedException(); + protected override ParameterExpression Parameter => throw new NotImplementedException(); - public override Expression Bind(QueryNode node) - { - throw new NotImplementedException(); - } + public override Expression Bind(QueryNode node) + { + throw new NotImplementedException(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/ExpressionStringBuilder.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/ExpressionStringBuilder.cs index d56a5ef69..5554a8984 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/ExpressionStringBuilder.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/ExpressionStringBuilder.cs @@ -13,269 +13,268 @@ using System.Linq.Expressions; using System.Text; -namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions +namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions; + +public class ExpressionStringBuilder : ExpressionVisitor { - public class ExpressionStringBuilder : ExpressionVisitor + private StringBuilder _builder = new StringBuilder(); + + private ExpressionStringBuilder() { - private StringBuilder _builder = new StringBuilder(); + } - private ExpressionStringBuilder() - { - } + public static string ToString(Expression expression) + { + ExpressionStringBuilder visitor = new ExpressionStringBuilder(); + visitor.Visit(expression); + return visitor._builder.ToString(); + } - public static string ToString(Expression expression) - { - ExpressionStringBuilder visitor = new ExpressionStringBuilder(); - visitor.Visit(expression); - return visitor._builder.ToString(); - } + protected override Expression VisitLambda(Expression node) + { + Out(String.Join(",", node.Parameters.Select(n => n.Name))); + Out(" => "); + Visit(node.Body); + return node; + } - protected override Expression VisitLambda(Expression node) + protected override Expression VisitBinary(BinaryExpression node) + { + Out("("); + Visit(node.Left); + Out(" "); + Out(ToString(node.NodeType)); + Out(" "); + Visit(node.Right); + Out(")"); + return node; + } + + protected override Expression VisitParameter(ParameterExpression node) + { + Out(node.Name); + return node; + } + + protected override Expression VisitMember(MemberExpression node) + { + // If it is a static member expression the Expression is null. + if (node.Expression == null && node.NodeType == ExpressionType.MemberAccess) { - Out(String.Join(",", node.Parameters.Select(n => n.Name))); - Out(" => "); - Visit(node.Body); - return node; + Visit(node.Expression); + Out(node.Member.DeclaringType.Name + "." + node.Member.Name); } - - protected override Expression VisitBinary(BinaryExpression node) + else if (node.Expression.NodeType == ExpressionType.Constant) { - Out("("); - Visit(node.Left); - Out(" "); - Out(ToString(node.NodeType)); - Out(" "); - Visit(node.Right); - Out(")"); - return node; + Visit(node.Expression); } - - protected override Expression VisitParameter(ParameterExpression node) + else { - Out(node.Name); - return node; + Visit(node.Expression); + Out("." + node.Member.Name); } - protected override Expression VisitMember(MemberExpression node) - { - // If it is a static member expression the Expression is null. - if (node.Expression == null && node.NodeType == ExpressionType.MemberAccess) - { - Visit(node.Expression); - Out(node.Member.DeclaringType.Name + "." + node.Member.Name); - } - else if (node.Expression.NodeType == ExpressionType.Constant) - { - Visit(node.Expression); - } - else - { - Visit(node.Expression); - Out("." + node.Member.Name); - } + return node; + } - return node; + protected override Expression VisitConstant(ConstantExpression node) + { + if (node.Value == null) + { + Out("null"); } - - protected override Expression VisitConstant(ConstantExpression node) + else { - if (node.Value == null) - { - Out("null"); - } - else + LinqParameterContainer container = node.Value as LinqParameterContainer; + string stringValue; + if (container != null) { - LinqParameterContainer container = node.Value as LinqParameterContainer; - string stringValue; - if (container != null) + stringValue = container.Property as string; + if (stringValue != null) { - stringValue = container.Property as string; - if (stringValue != null) - { - Out("\"" + stringValue + "\""); - } - else - { - stringValue = String.Format(CultureInfo.InvariantCulture, "{0}", container.Property); - Out(stringValue); - } + Out("\"" + stringValue + "\""); } else { - stringValue = String.Format(CultureInfo.InvariantCulture, "{0}", node.Value); + stringValue = String.Format(CultureInfo.InvariantCulture, "{0}", container.Property); Out(stringValue); } } - - return node; - } - - protected override Expression VisitUnary(UnaryExpression node) - { - if (node.NodeType == ExpressionType.Convert) - { - Out("Convert("); - Visit(node.Operand); - Out(")"); - return node; - } - else if (node.NodeType == ExpressionType.Not) - { - Out("Not("); - Visit(node.Operand); - Out(")"); - return node; - } - else if (node.NodeType == ExpressionType.TypeAs) + else { - Out("("); - Visit(node.Operand); - Out(" As " + node.Type.Name + ")"); - return node; + stringValue = String.Format(CultureInfo.InvariantCulture, "{0}", node.Value); + Out(stringValue); } - - return base.VisitUnary(node); } - protected override Expression VisitNew(NewExpression node) + return node; + } + + protected override Expression VisitUnary(UnaryExpression node) + { + if (node.NodeType == ExpressionType.Convert) { - Out("new " + node.Type.Name + "("); - VisitArguments(node.Arguments.ToArray()); + Out("Convert("); + Visit(node.Operand); Out(")"); return node; } - - protected override MemberListBinding VisitMemberListBinding(MemberListBinding node) + else if (node.NodeType == ExpressionType.Not) { - return base.VisitMemberListBinding(node); + Out("Not("); + Visit(node.Operand); + Out(")"); + return node; } - - protected override MemberMemberBinding VisitMemberMemberBinding(MemberMemberBinding node) + else if (node.NodeType == ExpressionType.TypeAs) { - return base.VisitMemberMemberBinding(node); + Out("("); + Visit(node.Operand); + Out(" As " + node.Type.Name + ")"); + return node; } - protected override Expression VisitMemberInit(MemberInitExpression node) - { - VisitNew(node.NewExpression); - Out(" {"); - foreach (MemberAssignment memberNode in node.Bindings) - { - Out(memberNode.Member.Name + " = "); + return base.VisitUnary(node); + } - Visit(memberNode.Expression); - Out(", "); - } - Out("}"); + protected override Expression VisitNew(NewExpression node) + { + Out("new " + node.Type.Name + "("); + VisitArguments(node.Arguments.ToArray()); + Out(")"); + return node; + } - return node; - } + protected override MemberListBinding VisitMemberListBinding(MemberListBinding node) + { + return base.VisitMemberListBinding(node); + } - protected override Expression VisitMethodCall(MethodCallExpression node) - { - int argindex = 0; - Visit(node.Object); + protected override MemberMemberBinding VisitMemberMemberBinding(MemberMemberBinding node) + { + return base.VisitMemberMemberBinding(node); + } - IEnumerable arguments = node.Arguments; - if (node.Method.IsStatic) - { - Visit(arguments.First()); - arguments = arguments.Skip(1); - argindex++; - } + protected override Expression VisitMemberInit(MemberInitExpression node) + { + VisitNew(node.NewExpression); + Out(" {"); + foreach (MemberAssignment memberNode in node.Bindings) + { + Out(memberNode.Member.Name + " = "); - Out("." + node.Method.Name + "("); - VisitArguments(arguments.ToArray()); - Out(")"); - return node; + Visit(memberNode.Expression); + Out(", "); } + Out("}"); - protected override Expression VisitTypeBinary(TypeBinaryExpression node) - { - if (node.NodeType == ExpressionType.TypeIs) - { - Out("("); - Visit(node.Expression); - Out(" Is "); - Out(node.TypeOperand.FullName + ")"); - } + return node; + } - return node; - } + protected override Expression VisitMethodCall(MethodCallExpression node) + { + int argindex = 0; + Visit(node.Object); - private void VisitArguments(Expression[] arguments) + IEnumerable arguments = node.Arguments; + if (node.Method.IsStatic) { - int argindex = 0; - while (argindex < arguments.Length) - { - Visit(arguments[argindex]); - argindex++; - - if (argindex < arguments.Length) - { - Out(", "); - } - } + Visit(arguments.First()); + arguments = arguments.Skip(1); + argindex++; } - protected override Expression VisitConditional(ConditionalExpression node) + Out("." + node.Method.Name + "("); + VisitArguments(arguments.ToArray()); + Out(")"); + return node; + } + + protected override Expression VisitTypeBinary(TypeBinaryExpression node) + { + if (node.NodeType == ExpressionType.TypeIs) { - Out("IIF("); - Visit(node.Test); - Out(", "); - Visit(node.IfTrue); - Out(", "); - Visit(node.IfFalse); - Out(")"); - return node; + Out("("); + Visit(node.Expression); + Out(" Is "); + Out(node.TypeOperand.FullName + ")"); } - private static string ToString(ExpressionType type) + return node; + } + + private void VisitArguments(Expression[] arguments) + { + int argindex = 0; + while (argindex < arguments.Length) { - switch (type) + Visit(arguments[argindex]); + argindex++; + + if (argindex < arguments.Length) { - case ExpressionType.Add: - return "+"; - case ExpressionType.And: - return "&"; - case ExpressionType.AndAlso: - return "AndAlso"; - case ExpressionType.Divide: - return "/"; - case ExpressionType.Equal: - return "=="; - case ExpressionType.GreaterThan: - return ">"; - case ExpressionType.GreaterThanOrEqual: - return ">="; - case ExpressionType.LessThan: - return "<"; - case ExpressionType.LessThanOrEqual: - return "<="; - case ExpressionType.Modulo: - return "%"; - case ExpressionType.Multiply: - return "*"; - case ExpressionType.Negate: - return "-"; - case ExpressionType.Not: - return "!"; - case ExpressionType.NotEqual: - return "!="; - case ExpressionType.Or: - return "|"; - case ExpressionType.OrElse: - return "OrElse"; - case ExpressionType.Subtract: - return "-"; - default: - throw new NotImplementedException(); + Out(", "); } } + } - private void Out(string s) + protected override Expression VisitConditional(ConditionalExpression node) + { + Out("IIF("); + Visit(node.Test); + Out(", "); + Visit(node.IfTrue); + Out(", "); + Visit(node.IfFalse); + Out(")"); + return node; + } + + private static string ToString(ExpressionType type) + { + switch (type) { - _builder.Append(s); + case ExpressionType.Add: + return "+"; + case ExpressionType.And: + return "&"; + case ExpressionType.AndAlso: + return "AndAlso"; + case ExpressionType.Divide: + return "/"; + case ExpressionType.Equal: + return "=="; + case ExpressionType.GreaterThan: + return ">"; + case ExpressionType.GreaterThanOrEqual: + return ">="; + case ExpressionType.LessThan: + return "<"; + case ExpressionType.LessThanOrEqual: + return "<="; + case ExpressionType.Modulo: + return "%"; + case ExpressionType.Multiply: + return "*"; + case ExpressionType.Negate: + return "-"; + case ExpressionType.Not: + return "!"; + case ExpressionType.NotEqual: + return "!="; + case ExpressionType.Or: + return "|"; + case ExpressionType.OrElse: + return "OrElse"; + case ExpressionType.Subtract: + return "-"; + default: + throw new NotImplementedException(); } } + + private void Out(string s) + { + _builder.Append(s); + } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTests.cs index 792402d93..5cea094e9 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTests.cs @@ -24,3242 +24,3241 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions +namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions; + +public class FilterBinderTests { - public class FilterBinderTests - { - private const string NotTesting = ""; + private const string NotTesting = ""; - private static Dictionary _modelCache = new Dictionary(); + private static Dictionary _modelCache = new Dictionary(); - [Fact] - public void BindFilter_ThrowsArgumentNull_ForInputs() - { - // Arrange & Act & Assert - FilterBinder binder = new FilterBinder(); - ExceptionAssert.ThrowsArgumentNull(() => binder.BindFilter(null, null), "filterClause"); + [Fact] + public void BindFilter_ThrowsArgumentNull_ForInputs() + { + // Arrange & Act & Assert + FilterBinder binder = new FilterBinder(); + ExceptionAssert.ThrowsArgumentNull(() => binder.BindFilter(null, null), "filterClause"); - // Arrange & Act & Assert - FilterClause filterClause = new FilterClause(new Mock().Object, new Mock().Object); - ExceptionAssert.ThrowsArgumentNull(() => binder.BindFilter(filterClause, null), "context"); - } + // Arrange & Act & Assert + FilterClause filterClause = new FilterClause(new Mock().Object, new Mock().Object); + ExceptionAssert.ThrowsArgumentNull(() => binder.BindFilter(filterClause, null), "context"); + } - #region Logical Operators - [Theory] - [InlineData(null, true, true)] - [InlineData("", false, false)] - [InlineData("Doritos", false, false)] - public void LogicalOperators_EqualityOperatorWithNull(string productName, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify("ProductName eq null", "$it => ($it.ProductName == null)"); + #region Logical Operators + [Theory] + [InlineData(null, true, true)] + [InlineData("", false, false)] + [InlineData("Doritos", false, false)] + public void LogicalOperators_EqualityOperatorWithNull(string productName, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify("ProductName eq null", "$it => ($it.ProductName == null)"); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (falseNullPropagation, trueNullPropagation)); - } + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (falseNullPropagation, trueNullPropagation)); + } - [Theory] - [InlineData(null, false, false)] - [InlineData("", true, true)] - [InlineData("Doritos", true, true)] - public void LogicalOperators_NotEqualityOperatorWithNull(string productName, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify("ProductName ne null", "$it => ($it.ProductName != null)"); + [Theory] + [InlineData(null, false, false)] + [InlineData("", true, true)] + [InlineData("Doritos", true, true)] + public void LogicalOperators_NotEqualityOperatorWithNull(string productName, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify("ProductName ne null", "$it => ($it.ProductName != null)"); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (falseNullPropagation, trueNullPropagation)); - } + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (falseNullPropagation, trueNullPropagation)); + } - [Theory] - [InlineData(null, false, false)] - [InlineData("", false, false)] - [InlineData("Doritos", true, true)] - public void LogicalOperators_EqualityOperatorWithValue(string productName, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify("ProductName eq 'Doritos'", "$it => ($it.ProductName == \"Doritos\")"); + [Theory] + [InlineData(null, false, false)] + [InlineData("", false, false)] + [InlineData("Doritos", true, true)] + public void LogicalOperators_EqualityOperatorWithValue(string productName, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify("ProductName eq 'Doritos'", "$it => ($it.ProductName == \"Doritos\")"); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (falseNullPropagation, trueNullPropagation)); - } + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (falseNullPropagation, trueNullPropagation)); + } - [Theory] - [InlineData(null, true, true)] - [InlineData("", true, true)] - [InlineData("Doritos", false, false)] - public void LogicalOperators_NotEqualOperator(string productName, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify("ProductName ne 'Doritos'", "$it => ($it.ProductName != \"Doritos\")"); + [Theory] + [InlineData(null, true, true)] + [InlineData("", true, true)] + [InlineData("Doritos", false, false)] + public void LogicalOperators_NotEqualOperator(string productName, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify("ProductName ne 'Doritos'", "$it => ($it.ProductName != \"Doritos\")"); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (falseNullPropagation, trueNullPropagation)); - } + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (falseNullPropagation, trueNullPropagation)); + } - [Theory] - [InlineData(null, false, false)] - [InlineData(5.01, true, true)] - [InlineData(4.99, false, false)] - public void LogicalOperators_GreaterThanOperator(object unitPrice, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify("UnitPrice gt 5.00m", - string.Format(CultureInfo.InvariantCulture, "$it => ($it.UnitPrice > Convert({0:0.00}))", 5.0), - string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice > Convert({0:0.00})) == True)", 5.0)); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { UnitPrice = ToNullable(unitPrice) }, - (falseNullPropagation, trueNullPropagation)); - } + [Theory] + [InlineData(null, false, false)] + [InlineData(5.01, true, true)] + [InlineData(4.99, false, false)] + public void LogicalOperators_GreaterThanOperator(object unitPrice, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify("UnitPrice gt 5.00m", + string.Format(CultureInfo.InvariantCulture, "$it => ($it.UnitPrice > Convert({0:0.00}))", 5.0), + string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice > Convert({0:0.00})) == True)", 5.0)); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { UnitPrice = ToNullable(unitPrice) }, + (falseNullPropagation, trueNullPropagation)); + } - [Theory] - [InlineData(null, false, false)] - [InlineData(5.0, true, true)] - [InlineData(4.99, false, false)] - public void LogicalOperators_GreaterThanEqualOperator(object unitPrice, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify("UnitPrice ge 5.00m", - string.Format(CultureInfo.InvariantCulture, "$it => ($it.UnitPrice >= Convert({0:0.00}))", 5.0), - string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice >= Convert({0:0.00})) == True)", 5.0)); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { UnitPrice = ToNullable(unitPrice) }, - (falseNullPropagation, trueNullPropagation)); - } + [Theory] + [InlineData(null, false, false)] + [InlineData(5.0, true, true)] + [InlineData(4.99, false, false)] + public void LogicalOperators_GreaterThanEqualOperator(object unitPrice, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify("UnitPrice ge 5.00m", + string.Format(CultureInfo.InvariantCulture, "$it => ($it.UnitPrice >= Convert({0:0.00}))", 5.0), + string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice >= Convert({0:0.00})) == True)", 5.0)); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { UnitPrice = ToNullable(unitPrice) }, + (falseNullPropagation, trueNullPropagation)); + } - [Theory] - [InlineData(null, false, false)] - [InlineData(4.99, true, true)] - [InlineData(5.01, false, false)] - public void LogicalOperators_LessThanOperator(object unitPrice, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify("UnitPrice lt 5.00m", - string.Format(CultureInfo.InvariantCulture, "$it => ($it.UnitPrice < Convert({0:0.00}))", 5.0), - string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice < Convert({0:0.00})) == True)", 5.0)); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { UnitPrice = ToNullable(unitPrice) }, - (falseNullPropagation, trueNullPropagation)); - } + [Theory] + [InlineData(null, false, false)] + [InlineData(4.99, true, true)] + [InlineData(5.01, false, false)] + public void LogicalOperators_LessThanOperator(object unitPrice, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify("UnitPrice lt 5.00m", + string.Format(CultureInfo.InvariantCulture, "$it => ($it.UnitPrice < Convert({0:0.00}))", 5.0), + string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice < Convert({0:0.00})) == True)", 5.0)); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { UnitPrice = ToNullable(unitPrice) }, + (falseNullPropagation, trueNullPropagation)); + } - [Theory] - [InlineData(null, false, false)] - [InlineData(5.0, true, true)] - [InlineData(5.01, false, false)] - public void LogicalOperators_LessThanOrEqualOperator(object unitPrice, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify("UnitPrice le 5.00m", - string.Format(CultureInfo.InvariantCulture, "$it => ($it.UnitPrice <= Convert({0:0.00}))", 5.0), - string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice <= Convert({0:0.00})) == True)", 5.0)); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { UnitPrice = ToNullable(unitPrice) }, - (falseNullPropagation, trueNullPropagation)); - } + [Theory] + [InlineData(null, false, false)] + [InlineData(5.0, true, true)] + [InlineData(5.01, false, false)] + public void LogicalOperators_LessThanOrEqualOperator(object unitPrice, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify("UnitPrice le 5.00m", + string.Format(CultureInfo.InvariantCulture, "$it => ($it.UnitPrice <= Convert({0:0.00}))", 5.0), + string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice <= Convert({0:0.00})) == True)", 5.0)); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { UnitPrice = ToNullable(unitPrice) }, + (falseNullPropagation, trueNullPropagation)); + } - [Fact] - public void LogicalOperators_NegativeNumbers() - { - // Arrange & Act & Assert - BindFilterAndVerify("UnitPrice le -5.00m", - string.Format(CultureInfo.InvariantCulture, "$it => ($it.UnitPrice <= Convert({0:0.00}))", -5.0), - NotTesting); - } + [Fact] + public void LogicalOperators_NegativeNumbers() + { + // Arrange & Act & Assert + BindFilterAndVerify("UnitPrice le -5.00m", + string.Format(CultureInfo.InvariantCulture, "$it => ($it.UnitPrice <= Convert({0:0.00}))", -5.0), + NotTesting); + } - [Theory] - [InlineData("DateTimeOffsetProp eq DateTimeOffsetProp", "$it => ($it.DateTimeOffsetProp == $it.DateTimeOffsetProp)")] - [InlineData("DateTimeOffsetProp ne DateTimeOffsetProp", "$it => ($it.DateTimeOffsetProp != $it.DateTimeOffsetProp)")] - [InlineData("DateTimeOffsetProp ge DateTimeOffsetProp", "$it => ($it.DateTimeOffsetProp >= $it.DateTimeOffsetProp)")] - [InlineData("DateTimeOffsetProp le DateTimeOffsetProp", "$it => ($it.DateTimeOffsetProp <= $it.DateTimeOffsetProp)")] - public void LogicalOperators_WithDateTimeOffsetInEqualities(string clause, string expectedExpression) - { - // Arrange & Act & Assert - BindFilterAndVerify(clause, expectedExpression); - } + [Theory] + [InlineData("DateTimeOffsetProp eq DateTimeOffsetProp", "$it => ($it.DateTimeOffsetProp == $it.DateTimeOffsetProp)")] + [InlineData("DateTimeOffsetProp ne DateTimeOffsetProp", "$it => ($it.DateTimeOffsetProp != $it.DateTimeOffsetProp)")] + [InlineData("DateTimeOffsetProp ge DateTimeOffsetProp", "$it => ($it.DateTimeOffsetProp >= $it.DateTimeOffsetProp)")] + [InlineData("DateTimeOffsetProp le DateTimeOffsetProp", "$it => ($it.DateTimeOffsetProp <= $it.DateTimeOffsetProp)")] + public void LogicalOperators_WithDateTimeOffsetInEqualities(string clause, string expectedExpression) + { + // Arrange & Act & Assert + BindFilterAndVerify(clause, expectedExpression); + } - [Theory] - [InlineData("DateTimeProperty eq DateTimeProperty", "$it => ($it.DateTimeProperty == $it.DateTimeProperty)")] - [InlineData("DateTimeProperty ne DateTimeProperty", "$it => ($it.DateTimeProperty != $it.DateTimeProperty)")] - [InlineData("DateTimeProperty ge DateTimeProperty", "$it => ($it.DateTimeProperty >= $it.DateTimeProperty)")] - [InlineData("DateTimeProperty le DateTimeProperty", "$it => ($it.DateTimeProperty <= $it.DateTimeProperty)")] - public void LogicalOperators_WithDateTimeInEqualities(string clause, string expectedExpression) - { - // Arrange & Act & Assert - BindFilterAndVerify(clause, expectedExpression); - } + [Theory] + [InlineData("DateTimeProperty eq DateTimeProperty", "$it => ($it.DateTimeProperty == $it.DateTimeProperty)")] + [InlineData("DateTimeProperty ne DateTimeProperty", "$it => ($it.DateTimeProperty != $it.DateTimeProperty)")] + [InlineData("DateTimeProperty ge DateTimeProperty", "$it => ($it.DateTimeProperty >= $it.DateTimeProperty)")] + [InlineData("DateTimeProperty le DateTimeProperty", "$it => ($it.DateTimeProperty <= $it.DateTimeProperty)")] + public void LogicalOperators_WithDateTimeInEqualities(string clause, string expectedExpression) + { + // Arrange & Act & Assert + BindFilterAndVerify(clause, expectedExpression); + } - [Fact] - [ReplaceCulture] - public void LogicalOperators_BooleanOperatorNullableTypes() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "UnitPrice eq 5.00m or CategoryID eq 0", - "$it => (($it.UnitPrice == Convert(5.00)) OrElse ($it.CategoryID == 0))", - NotTesting); - } + [Fact] + [ReplaceCulture] + public void LogicalOperators_BooleanOperatorNullableTypes() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "UnitPrice eq 5.00m or CategoryID eq 0", + "$it => (($it.UnitPrice == Convert(5.00)) OrElse ($it.CategoryID == 0))", + NotTesting); + } - [Fact] - public void LogicalOperators_BooleanComparisonOnNullableAndNonNullableType() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "Discontinued eq true", - "$it => ($it.Discontinued == Convert(True))", - "$it => (($it.Discontinued == Convert(True)) == True)"); - } + [Fact] + public void LogicalOperators_BooleanComparisonOnNullableAndNonNullableType() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "Discontinued eq true", + "$it => ($it.Discontinued == Convert(True))", + "$it => (($it.Discontinued == Convert(True)) == True)"); + } - [Fact] - public void LogicalOperators_BooleanComparisonOnNullableType() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "Discontinued eq Discontinued", - "$it => ($it.Discontinued == $it.Discontinued)", - "$it => (($it.Discontinued == $it.Discontinued) == True)"); - } + [Fact] + public void LogicalOperators_BooleanComparisonOnNullableType() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "Discontinued eq Discontinued", + "$it => ($it.Discontinued == $it.Discontinued)", + "$it => (($it.Discontinued == $it.Discontinued) == True)"); + } - [Theory] - [InlineData(null, null, false, false)] - [InlineData(5.0, 0, true, true)] - [InlineData(null, 1, false, false)] - public void LogicalOperators_With_OrOperator(object unitPrice, object unitsInStock, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "UnitPrice eq 5.00m or UnitsInStock eq 0", - string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice == Convert({0:0.00})) OrElse (Convert($it.UnitsInStock) == Convert({1})))", 5.0, 0), - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { UnitPrice = ToNullable(unitPrice), UnitsInStock = ToNullable(unitsInStock) }, - (falseNullPropagation, trueNullPropagation)); - } + [Theory] + [InlineData(null, null, false, false)] + [InlineData(5.0, 0, true, true)] + [InlineData(null, 1, false, false)] + public void LogicalOperators_With_OrOperator(object unitPrice, object unitsInStock, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "UnitPrice eq 5.00m or UnitsInStock eq 0", + string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice == Convert({0:0.00})) OrElse (Convert($it.UnitsInStock) == Convert({1})))", 5.0, 0), + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { UnitPrice = ToNullable(unitPrice), UnitsInStock = ToNullable(unitsInStock) }, + (falseNullPropagation, trueNullPropagation)); + } - [Theory] - [InlineData(null, null, false, false)] - [InlineData(5.0, 10, true, true)] - [InlineData(null, 1, false, false)] - public void LogicalOperators_With_AndOperator(object unitPrice, object unitsInStock, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "UnitPrice eq 5.00m and UnitsInStock eq 10.00m", - String.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice == Convert({0:0.00})) AndAlso (Convert($it.UnitsInStock) == Convert({1:0.00})))", 5.0, 10.0), - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { UnitPrice = ToNullable(unitPrice), UnitsInStock = ToNullable(unitsInStock) }, - (falseNullPropagation, trueNullPropagation)); - } + [Theory] + [InlineData(null, null, false, false)] + [InlineData(5.0, 10, true, true)] + [InlineData(null, 1, false, false)] + public void LogicalOperators_With_AndOperator(object unitPrice, object unitsInStock, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "UnitPrice eq 5.00m and UnitsInStock eq 10.00m", + String.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice == Convert({0:0.00})) AndAlso (Convert($it.UnitsInStock) == Convert({1:0.00})))", 5.0, 10.0), + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { UnitPrice = ToNullable(unitPrice), UnitsInStock = ToNullable(unitsInStock) }, + (falseNullPropagation, trueNullPropagation)); + } - [Theory] - [InlineData(null, true, false)] // This is an interesting case for null propagation. - [InlineData(5.0, false, false)] - [InlineData(5.5, true, true)] - public void LogicalOperators_Negation(object unitPrice, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "not (UnitPrice eq 5.00m)", - string.Format(CultureInfo.InvariantCulture, "$it => Not(($it.UnitPrice == Convert({0:0.00})))", 5.0), - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { UnitPrice = ToNullable(unitPrice) }, - (falseNullPropagation, trueNullPropagation)); - } + [Theory] + [InlineData(null, true, false)] // This is an interesting case for null propagation. + [InlineData(5.0, false, false)] + [InlineData(5.5, true, true)] + public void LogicalOperators_Negation(object unitPrice, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "not (UnitPrice eq 5.00m)", + string.Format(CultureInfo.InvariantCulture, "$it => Not(($it.UnitPrice == Convert({0:0.00})))", 5.0), + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { UnitPrice = ToNullable(unitPrice) }, + (falseNullPropagation, trueNullPropagation)); + } - [Theory] - [InlineData(true, false, false)] - [InlineData(false, true, true)] - public void LogicalOperators_BoolNegation(bool discontinued, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "not Discontinued", - "$it => Convert(Not($it.Discontinued))", - "$it => (Not($it.Discontinued) == True)"); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, new Product { Discontinued = discontinued }, (falseNullPropagation, trueNullPropagation)); - } + [Theory] + [InlineData(true, false, false)] + [InlineData(false, true, true)] + public void LogicalOperators_BoolNegation(bool discontinued, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "not Discontinued", + "$it => Convert(Not($it.Discontinued))", + "$it => (Not($it.Discontinued) == True)"); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, new Product { Discontinued = discontinued }, (falseNullPropagation, trueNullPropagation)); + } - [Fact] - public void LogicalOperators_NestedNegation() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "not (not(not (Discontinued)))", - "$it => Convert(Not(Not(Not($it.Discontinued))))", - "$it => (Not(Not(Not($it.Discontinued))) == True)"); - } - #endregion - - #region Arithmetic Operators - [Theory] - [InlineData(null, false, false)] - [InlineData(5.0, true, true)] - [InlineData(15.01, false, false)] - public void ArithmeticOperators_Subtraction(object unitPrice, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "UnitPrice sub 1.00m lt 5.00m", - string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice - Convert({0:0.00})) < Convert({1:0.00}))", 1.0, 5.0), - string.Format(CultureInfo.InvariantCulture, "$it => ((($it.UnitPrice - Convert({0:0.00})) < Convert({1:0.00})) == True)", 1.0, 5.0)); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { UnitPrice = ToNullable(unitPrice) }, - (falseNullPropagation, trueNullPropagation)); - } + [Fact] + public void LogicalOperators_NestedNegation() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "not (not(not (Discontinued)))", + "$it => Convert(Not(Not(Not($it.Discontinued))))", + "$it => (Not(Not(Not($it.Discontinued))) == True)"); + } + #endregion + + #region Arithmetic Operators + [Theory] + [InlineData(null, false, false)] + [InlineData(5.0, true, true)] + [InlineData(15.01, false, false)] + public void ArithmeticOperators_Subtraction(object unitPrice, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "UnitPrice sub 1.00m lt 5.00m", + string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice - Convert({0:0.00})) < Convert({1:0.00}))", 1.0, 5.0), + string.Format(CultureInfo.InvariantCulture, "$it => ((($it.UnitPrice - Convert({0:0.00})) < Convert({1:0.00})) == True)", 1.0, 5.0)); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { UnitPrice = ToNullable(unitPrice) }, + (falseNullPropagation, trueNullPropagation)); + } - [Fact] - public void ArithmeticOperators_Addition() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "UnitPrice add 1.00m lt 5.00m", - string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice + Convert({0:0.00})) < Convert({1:0.00}))", 1.0, 5.0), - NotTesting); - } + [Fact] + public void ArithmeticOperators_Addition() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "UnitPrice add 1.00m lt 5.00m", + string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice + Convert({0:0.00})) < Convert({1:0.00}))", 1.0, 5.0), + NotTesting); + } - [Fact] - public void ArithmeticOperators_Multiplication() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "UnitPrice mul 1.00m lt 5.00m", - string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice * Convert({0:0.00})) < Convert({1:0.00}))", 1.0, 5.0), - NotTesting); - } + [Fact] + public void ArithmeticOperators_Multiplication() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "UnitPrice mul 1.00m lt 5.00m", + string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice * Convert({0:0.00})) < Convert({1:0.00}))", 1.0, 5.0), + NotTesting); + } - [Fact] - public void ArithmeticOperators_Division() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "UnitPrice div 1.00m lt 5.00m", - string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice / Convert({0:0.00})) < Convert({1:0.00}))", 1.0, 5.0), - NotTesting); - } + [Fact] + public void ArithmeticOperators_Division() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "UnitPrice div 1.00m lt 5.00m", + string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice / Convert({0:0.00})) < Convert({1:0.00}))", 1.0, 5.0), + NotTesting); + } - [Fact] - public void ArithmeticOperators_Modulo() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "UnitPrice mod 1.00m lt 5.00m", - string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice % Convert({0:0.00})) < Convert({1:0.00}))", 1.0, 5.0), - NotTesting); - } - #endregion - - #region NULL handling - [Theory] - [InlineData("UnitsInStock eq UnitsOnOrder", null, null, true, false)] - [InlineData("UnitsInStock ne UnitsOnOrder", null, null, false, false)] - [InlineData("UnitsInStock gt UnitsOnOrder", null, null, false, false)] - [InlineData("UnitsInStock ge UnitsOnOrder", null, null, false, false)] - [InlineData("UnitsInStock lt UnitsOnOrder", null, null, false, false)] - [InlineData("UnitsInStock le UnitsOnOrder", null, null, false, false)] - [InlineData("(UnitsInStock add UnitsOnOrder) eq UnitsInStock", null, null, true, false)] - [InlineData("(UnitsInStock sub UnitsOnOrder) eq UnitsInStock", null, null, true, false)] - [InlineData("(UnitsInStock mul UnitsOnOrder) eq UnitsInStock", null, null, true, false)] - [InlineData("(UnitsInStock div UnitsOnOrder) eq UnitsInStock", null, null, true, false)] - [InlineData("(UnitsInStock mod UnitsOnOrder) eq UnitsInStock", null, null, true, false)] - [InlineData("UnitsInStock eq UnitsOnOrder", 1, null, false, false)] - [InlineData("UnitsInStock eq UnitsOnOrder", 1, 1, true, true)] - public void NullHandling(string filter, object unitsInStock, object unitsOnOrder, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify(filter); + [Fact] + public void ArithmeticOperators_Modulo() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "UnitPrice mod 1.00m lt 5.00m", + string.Format(CultureInfo.InvariantCulture, "$it => (($it.UnitPrice % Convert({0:0.00})) < Convert({1:0.00}))", 1.0, 5.0), + NotTesting); + } + #endregion + + #region NULL handling + [Theory] + [InlineData("UnitsInStock eq UnitsOnOrder", null, null, true, false)] + [InlineData("UnitsInStock ne UnitsOnOrder", null, null, false, false)] + [InlineData("UnitsInStock gt UnitsOnOrder", null, null, false, false)] + [InlineData("UnitsInStock ge UnitsOnOrder", null, null, false, false)] + [InlineData("UnitsInStock lt UnitsOnOrder", null, null, false, false)] + [InlineData("UnitsInStock le UnitsOnOrder", null, null, false, false)] + [InlineData("(UnitsInStock add UnitsOnOrder) eq UnitsInStock", null, null, true, false)] + [InlineData("(UnitsInStock sub UnitsOnOrder) eq UnitsInStock", null, null, true, false)] + [InlineData("(UnitsInStock mul UnitsOnOrder) eq UnitsInStock", null, null, true, false)] + [InlineData("(UnitsInStock div UnitsOnOrder) eq UnitsInStock", null, null, true, false)] + [InlineData("(UnitsInStock mod UnitsOnOrder) eq UnitsInStock", null, null, true, false)] + [InlineData("UnitsInStock eq UnitsOnOrder", 1, null, false, false)] + [InlineData("UnitsInStock eq UnitsOnOrder", 1, 1, true, true)] + public void NullHandling(string filter, object unitsInStock, object unitsOnOrder, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify(filter); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { UnitsInStock = ToNullable(unitsInStock), UnitsOnOrder = ToNullable(unitsOnOrder) }, - (falseNullPropagation, trueNullPropagation)); - } + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { UnitsInStock = ToNullable(unitsInStock), UnitsOnOrder = ToNullable(unitsOnOrder) }, + (falseNullPropagation, trueNullPropagation)); + } - [Theory] - [InlineData("UnitsInStock eq null", null, true, true)] // NULL == constant NULL is true when null propagation is enabled - [InlineData("UnitsInStock ne null", null, false, false)] // NULL != constant NULL is false when null propagation is enabled - public void NullHandling_LiteralNull(string filter, object unitsInStock, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify(filter); + [Theory] + [InlineData("UnitsInStock eq null", null, true, true)] // NULL == constant NULL is true when null propagation is enabled + [InlineData("UnitsInStock ne null", null, false, false)] // NULL != constant NULL is false when null propagation is enabled + public void NullHandling_LiteralNull(string filter, object unitsInStock, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify(filter); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { UnitsInStock = ToNullable(unitsInStock) }, - (falseNullPropagation, trueNullPropagation)); - } + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { UnitsInStock = ToNullable(unitsInStock) }, + (falseNullPropagation, trueNullPropagation)); + } - [Fact] - public void NullHandling_StringFunctionWithStringParameret() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "startswith(ProductName, 'Abc')", - NotTesting, - "$it => (IIF((($it.ProductName == null) OrElse (\"Abc\" == null)), null, Convert($it.ProductName.StartsWith(\"Abc\"))) == True)"); - } - #endregion - - [Theory] - [InlineData("StringProp gt 'Middle'", "Middle", false, false)] - [InlineData("StringProp ge 'Middle'", "Middle", true, true)] - [InlineData("StringProp lt 'Middle'", "Middle", false, false)] - [InlineData("StringProp le 'Middle'", "Middle", true, true)] - [InlineData("StringProp ge StringProp", "", true, true)] - [InlineData("StringProp gt null", "", true, true)] - [InlineData("null gt StringProp", "", false, false)] - [InlineData("'Middle' gt StringProp", "Middle", false, false)] - [InlineData("'a' lt 'b'", "", true, true)] - public void StringComparisons_Work(string filter, string value, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify(filter); + [Fact] + public void NullHandling_StringFunctionWithStringParameret() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "startswith(ProductName, 'Abc')", + NotTesting, + "$it => (IIF((($it.ProductName == null) OrElse (\"Abc\" == null)), null, Convert($it.ProductName.StartsWith(\"Abc\"))) == True)"); + } + #endregion + + [Theory] + [InlineData("StringProp gt 'Middle'", "Middle", false, false)] + [InlineData("StringProp ge 'Middle'", "Middle", true, true)] + [InlineData("StringProp lt 'Middle'", "Middle", false, false)] + [InlineData("StringProp le 'Middle'", "Middle", true, true)] + [InlineData("StringProp ge StringProp", "", true, true)] + [InlineData("StringProp gt null", "", true, true)] + [InlineData("null gt StringProp", "", false, false)] + [InlineData("'Middle' gt StringProp", "Middle", false, false)] + [InlineData("'a' lt 'b'", "", true, true)] + public void StringComparisons_Work(string filter, string value, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify(filter); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, new DataTypes { StringProp = value }, (falseNullPropagation, trueNullPropagation)); - } + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, new DataTypes { StringProp = value }, (falseNullPropagation, trueNullPropagation)); + } - // Issue: 477 - [Theory] - [InlineData("indexof('hello', StringProp) gt UIntProp")] - [InlineData("indexof('hello', StringProp) gt ULongProp")] - [InlineData("indexof('hello', StringProp) gt UShortProp")] - [InlineData("indexof('hello', StringProp) gt NullableUShortProp")] - [InlineData("indexof('hello', StringProp) gt NullableUIntProp")] - [InlineData("indexof('hello', StringProp) gt NullableULongProp")] - public void ComparisonsInvolvingCastsAndNullableValues(string filter) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify(filter); + // Issue: 477 + [Theory] + [InlineData("indexof('hello', StringProp) gt UIntProp")] + [InlineData("indexof('hello', StringProp) gt ULongProp")] + [InlineData("indexof('hello', StringProp) gt UShortProp")] + [InlineData("indexof('hello', StringProp) gt NullableUShortProp")] + [InlineData("indexof('hello', StringProp) gt NullableUIntProp")] + [InlineData("indexof('hello', StringProp) gt NullableULongProp")] + public void ComparisonsInvolvingCastsAndNullableValues(string filter) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify(filter); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new DataTypes(), (typeof(ArgumentNullException), false)); - } + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new DataTypes(), (typeof(ArgumentNullException), false)); + } - [Theory] - [InlineData(null, null, true, true)] - [InlineData("not doritos", 0, true, true)] - [InlineData("Doritos", 1, false, false)] - public void MultipleLogicOperators_Grouping(string productName, object unitsInStock, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "((ProductName ne 'Doritos') or (UnitPrice lt 5.00m))", - string.Format(CultureInfo.InvariantCulture, "$it => (($it.ProductName != \"Doritos\") OrElse ($it.UnitPrice < Convert({0:0.00})))", 5.0), - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { ProductName = productName, UnitsInStock = ToNullable(unitsInStock) }, - (falseNullPropagation, trueNullPropagation)); - } + [Theory] + [InlineData(null, null, true, true)] + [InlineData("not doritos", 0, true, true)] + [InlineData("Doritos", 1, false, false)] + public void MultipleLogicOperators_Grouping(string productName, object unitsInStock, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "((ProductName ne 'Doritos') or (UnitPrice lt 5.00m))", + string.Format(CultureInfo.InvariantCulture, "$it => (($it.ProductName != \"Doritos\") OrElse ($it.UnitPrice < Convert({0:0.00})))", 5.0), + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { ProductName = productName, UnitsInStock = ToNullable(unitsInStock) }, + (falseNullPropagation, trueNullPropagation)); + } - [Fact] - public void SubMemberExpressions() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "Category/CategoryName eq 'Snacks'", - "$it => ($it.Category.CategoryName == \"Snacks\")", - "$it => (IIF(($it.Category == null), null, $it.Category.CategoryName) == \"Snacks\")"); - - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product(), (typeof(NullReferenceException), false)); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { Category = new Category { CategoryName = "Snacks" } }, - (true, true)); - } + [Fact] + public void SubMemberExpressions() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "Category/CategoryName eq 'Snacks'", + "$it => ($it.Category.CategoryName == \"Snacks\")", + "$it => (IIF(($it.Category == null), null, $it.Category.CategoryName) == \"Snacks\")"); + + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product(), (typeof(NullReferenceException), false)); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { Category = new Category { CategoryName = "Snacks" } }, + (true, true)); + } - [Fact] - public void SubMemberExpressionsRecursive() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "Category/Product/Category/CategoryName eq 'Snacks'", - "$it => ($it.Category.Product.Category.CategoryName == \"Snacks\")", - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product(), (typeof(NullReferenceException), false)); - } + [Fact] + public void SubMemberExpressionsRecursive() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "Category/Product/Category/CategoryName eq 'Snacks'", + "$it => ($it.Category.Product.Category.CategoryName == \"Snacks\")", + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product(), (typeof(NullReferenceException), false)); + } - [Fact] - public void ComplexPropertyNavigation() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "SupplierAddress/City eq 'Redmond'", - "$it => ($it.SupplierAddress.City == \"Redmond\")", - "$it => (IIF(($it.SupplierAddress == null), null, $it.SupplierAddress.City) == \"Redmond\")"); + [Fact] + public void ComplexPropertyNavigation() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "SupplierAddress/City eq 'Redmond'", + "$it => ($it.SupplierAddress.City == \"Redmond\")", + "$it => (IIF(($it.SupplierAddress == null), null, $it.SupplierAddress.City) == \"Redmond\")"); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product(), (typeof(NullReferenceException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product(), (typeof(NullReferenceException), false)); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, new Product { SupplierAddress = new Address { City = "Redmond" } }, (true, true)); - } + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, new Product { SupplierAddress = new Address { City = "Redmond" } }, (true, true)); + } - #region Any/All - [Fact] - public void AnyOperator_OnNavigationEnumerableCollections() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "Category/EnumerableProducts/any(P: P/ProductName eq 'Snacks')", - "$it => $it.Category.EnumerableProducts.Any(P => (P.ProductName == \"Snacks\"))", - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product + #region Any/All + [Fact] + public void AnyOperator_OnNavigationEnumerableCollections() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "Category/EnumerableProducts/any(P: P/ProductName eq 'Snacks')", + "$it => $it.Category.EnumerableProducts.Any(P => (P.ProductName == \"Snacks\"))", + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product + { + Category = new Category { - Category = new Category + EnumerableProducts = new Product[] { - EnumerableProducts = new Product[] - { - new Product { ProductName = "Snacks" }, - new Product { ProductName = "NonSnacks" } - } + new Product { ProductName = "Snacks" }, + new Product { ProductName = "NonSnacks" } } - }, - (true, true)); + } + }, + (true, true)); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product + { + Category = new Category { - Category = new Category + EnumerableProducts = new Product[] { - EnumerableProducts = new Product[] - { - new Product { ProductName = "NonSnacks" } - } + new Product { ProductName = "NonSnacks" } } - }, - (false, false)); - } + } + }, + (false, false)); + } - [Fact] - public void AnyOperator_OnNavigationQueryableCollections() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "Category/QueryableProducts/any(P: P/ProductName eq 'Snacks')", - "$it => $it.Category.QueryableProducts.Any(P => (P.ProductName == \"Snacks\"))", - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product + [Fact] + public void AnyOperator_OnNavigationQueryableCollections() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "Category/QueryableProducts/any(P: P/ProductName eq 'Snacks')", + "$it => $it.Category.QueryableProducts.Any(P => (P.ProductName == \"Snacks\"))", + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product + { + Category = new Category { - Category = new Category + QueryableProducts = new Product[] { - QueryableProducts = new Product[] - { - new Product { ProductName = "Snacks" }, - new Product { ProductName = "NonSnacks" } - }.AsQueryable() - } - }, - (true, true)); + new Product { ProductName = "Snacks" }, + new Product { ProductName = "NonSnacks" } + }.AsQueryable() + } + }, + (true, true)); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product + { + Category = new Category { - Category = new Category + QueryableProducts = new Product[] { - QueryableProducts = new Product[] - { - new Product { ProductName = "NonSnacks" } - }.AsQueryable() - } - }, - (false, false)); - } + new Product { ProductName = "NonSnacks" } + }.AsQueryable() + } + }, + (false, false)); + } - [Fact] - public void AnyOperator_WithNestedFilterWithCountNode_OnNavigationQueryableCollections() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "Category/QueryableProducts/any(p: p/AlternateAddresses/$count($filter=HouseNumber ge 8) ge 2)", - "$it => $it.Category.QueryableProducts.Any(p => (p.AlternateAddresses.Where($it => ($it.HouseNumber >= 8)).LongCount() >= 2))", - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product + [Fact] + public void AnyOperator_WithNestedFilterWithCountNode_OnNavigationQueryableCollections() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "Category/QueryableProducts/any(p: p/AlternateAddresses/$count($filter=HouseNumber ge 8) ge 2)", + "$it => $it.Category.QueryableProducts.Any(p => (p.AlternateAddresses.Where($it => ($it.HouseNumber >= 8)).LongCount() >= 2))", + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product + { + Category = new Category { - Category = new Category + QueryableProducts = new Product[] { - QueryableProducts = new Product[] - { - new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 8 } } }, - new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 9 }, new Address { HouseNumber = 10 } } } - }.AsQueryable() - } - }, - (true, true)); + new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 8 } } }, + new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 9 }, new Address { HouseNumber = 10 } } } + }.AsQueryable() + } + }, + (true, true)); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product + { + Category = new Category { - Category = new Category + QueryableProducts = new Product[] { - QueryableProducts = new Product[] - { - new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 8 } } }, - new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 4 } } } // < 8 - }.AsQueryable() - } - }, - (false, false)); - } + new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 8 } } }, + new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 4 } } } // < 8 + }.AsQueryable() + } + }, + (false, false)); + } - [Fact] - public void AnyOperator_WithNestedAny_OnNavigationQueryableCollections() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "Category/QueryableProducts/any(p: p/AlternateAddresses/any(o:o/HouseNumber eq 8))", - "$it => $it.Category.QueryableProducts.Any(p => p.AlternateAddresses.Any(o => (o.HouseNumber == 8)))", - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product + [Fact] + public void AnyOperator_WithNestedAny_OnNavigationQueryableCollections() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "Category/QueryableProducts/any(p: p/AlternateAddresses/any(o:o/HouseNumber eq 8))", + "$it => $it.Category.QueryableProducts.Any(p => p.AlternateAddresses.Any(o => (o.HouseNumber == 8)))", + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product + { + Category = new Category { - Category = new Category + QueryableProducts = new Product[] { - QueryableProducts = new Product[] - { - new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 8 } } }, - new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 9 } } } - }.AsQueryable() - } - }, - (true, true)); + new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 8 } } }, + new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 9 } } } + }.AsQueryable() + } + }, + (true, true)); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product + { + Category = new Category { - Category = new Category + QueryableProducts = new Product[] { - QueryableProducts = new Product[] - { - new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 10 } } }, - new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 9 } } } - }.AsQueryable() - } - }, - (false, false)); - } + new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 10 } } }, + new Product { AlternateAddresses = new Address[] { new Address { HouseNumber = 9 } } } + }.AsQueryable() + } + }, + (false, false)); + } - [Theory] - [InlineData("Category/QueryableProducts/any(P: P/ProductID in (1))", "$it => $it.Category.QueryableProducts.Any(P => System.Collections.Generic.List`1[System.Int32].Contains(P.ProductID))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.QueryableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.QueryableProducts).Any(P => System.Collections.Generic.List`1[System.Int32].Cast().Contains(IIF((P == null), null, Convert(P.ProductID)))))) == True)")] - [InlineData("Category/EnumerableProducts/any(P: P/ProductID in (1))", "$it => $it.Category.EnumerableProducts.Any(P => System.Collections.Generic.List`1[System.Int32].Contains(P.ProductID))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.EnumerableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.EnumerableProducts).Any(P => System.Collections.Generic.List`1[System.Int32].Cast().Contains(IIF((P == null), null, Convert(P.ProductID)))))) == True)")] - [InlineData("Category/QueryableProducts/any(P: P/GuidProperty in (dc75698b-581d-488b-9638-3e28dd51d8f7))", "$it => $it.Category.QueryableProducts.Any(P => System.Collections.Generic.List`1[System.Guid].Contains(P.GuidProperty))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.QueryableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.QueryableProducts).Any(P => System.Collections.Generic.List`1[System.Guid].Cast().Contains(IIF((P == null), null, Convert(P.GuidProperty)))))) == True)")] - [InlineData("Category/EnumerableProducts/any(P: P/GuidProperty in (dc75698b-581d-488b-9638-3e28dd51d8f7))", "$it => $it.Category.EnumerableProducts.Any(P => System.Collections.Generic.List`1[System.Guid].Contains(P.GuidProperty))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.EnumerableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.EnumerableProducts).Any(P => System.Collections.Generic.List`1[System.Guid].Cast().Contains(IIF((P == null), null, Convert(P.GuidProperty)))))) == True)")] - [InlineData("Category/QueryableProducts/any(P: P/NullableGuidProperty in (dc75698b-581d-488b-9638-3e28dd51d8f7))", "$it => $it.Category.QueryableProducts.Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Guid]].Contains(P.NullableGuidProperty))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.QueryableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.QueryableProducts).Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Guid]].Contains(IIF((P == null), null, P.NullableGuidProperty))))) == True)")] - [InlineData("Category/EnumerableProducts/any(P: P/NullableGuidProperty in (dc75698b-581d-488b-9638-3e28dd51d8f7))", "$it => $it.Category.EnumerableProducts.Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Guid]].Contains(P.NullableGuidProperty))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.EnumerableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.EnumerableProducts).Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Guid]].Contains(IIF((P == null), null, P.NullableGuidProperty))))) == True)")] - [InlineData("Category/QueryableProducts/any(P: P/Discontinued in (false, null))", "$it => $it.Category.QueryableProducts.Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Boolean]].Contains(P.Discontinued))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.QueryableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.QueryableProducts).Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Boolean]].Contains(IIF((P == null), null, P.Discontinued))))) == True)")] - [InlineData("Category/EnumerableProducts/any(P: P/Discontinued in (false, null))", "$it => $it.Category.EnumerableProducts.Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Boolean]].Contains(P.Discontinued))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.EnumerableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.EnumerableProducts).Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Boolean]].Contains(IIF((P == null), null, P.Discontinued))))) == True)")] - public void AnyOperator_WithInOperator_OnNavigation(string filter, string falseNullExpression, string trueNullExpression) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, falseNullExpression, trueNullExpression); - } + [Theory] + [InlineData("Category/QueryableProducts/any(P: P/ProductID in (1))", "$it => $it.Category.QueryableProducts.Any(P => System.Collections.Generic.List`1[System.Int32].Contains(P.ProductID))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.QueryableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.QueryableProducts).Any(P => System.Collections.Generic.List`1[System.Int32].Cast().Contains(IIF((P == null), null, Convert(P.ProductID)))))) == True)")] + [InlineData("Category/EnumerableProducts/any(P: P/ProductID in (1))", "$it => $it.Category.EnumerableProducts.Any(P => System.Collections.Generic.List`1[System.Int32].Contains(P.ProductID))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.EnumerableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.EnumerableProducts).Any(P => System.Collections.Generic.List`1[System.Int32].Cast().Contains(IIF((P == null), null, Convert(P.ProductID)))))) == True)")] + [InlineData("Category/QueryableProducts/any(P: P/GuidProperty in (dc75698b-581d-488b-9638-3e28dd51d8f7))", "$it => $it.Category.QueryableProducts.Any(P => System.Collections.Generic.List`1[System.Guid].Contains(P.GuidProperty))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.QueryableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.QueryableProducts).Any(P => System.Collections.Generic.List`1[System.Guid].Cast().Contains(IIF((P == null), null, Convert(P.GuidProperty)))))) == True)")] + [InlineData("Category/EnumerableProducts/any(P: P/GuidProperty in (dc75698b-581d-488b-9638-3e28dd51d8f7))", "$it => $it.Category.EnumerableProducts.Any(P => System.Collections.Generic.List`1[System.Guid].Contains(P.GuidProperty))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.EnumerableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.EnumerableProducts).Any(P => System.Collections.Generic.List`1[System.Guid].Cast().Contains(IIF((P == null), null, Convert(P.GuidProperty)))))) == True)")] + [InlineData("Category/QueryableProducts/any(P: P/NullableGuidProperty in (dc75698b-581d-488b-9638-3e28dd51d8f7))", "$it => $it.Category.QueryableProducts.Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Guid]].Contains(P.NullableGuidProperty))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.QueryableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.QueryableProducts).Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Guid]].Contains(IIF((P == null), null, P.NullableGuidProperty))))) == True)")] + [InlineData("Category/EnumerableProducts/any(P: P/NullableGuidProperty in (dc75698b-581d-488b-9638-3e28dd51d8f7))", "$it => $it.Category.EnumerableProducts.Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Guid]].Contains(P.NullableGuidProperty))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.EnumerableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.EnumerableProducts).Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Guid]].Contains(IIF((P == null), null, P.NullableGuidProperty))))) == True)")] + [InlineData("Category/QueryableProducts/any(P: P/Discontinued in (false, null))", "$it => $it.Category.QueryableProducts.Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Boolean]].Contains(P.Discontinued))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.QueryableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.QueryableProducts).Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Boolean]].Contains(IIF((P == null), null, P.Discontinued))))) == True)")] + [InlineData("Category/EnumerableProducts/any(P: P/Discontinued in (false, null))", "$it => $it.Category.EnumerableProducts.Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Boolean]].Contains(P.Discontinued))", "$it => (IIF((IIF(($it.Category == null), null, $it.Category.EnumerableProducts) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.EnumerableProducts).Any(P => System.Collections.Generic.List`1[System.Nullable`1[System.Boolean]].Contains(IIF((P == null), null, P.Discontinued))))) == True)")] + public void AnyOperator_WithInOperator_OnNavigation(string filter, string falseNullExpression, string trueNullExpression) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, falseNullExpression, trueNullExpression); + } - [Theory] - [InlineData("Category/QueryableProducts/any(P: false)", "$it => False")] - [InlineData("Category/QueryableProducts/any(P: false and P/ProductName eq 'Snacks')", "$it => $it.Category.QueryableProducts.Any(P => (False AndAlso (P.ProductName == \"Snacks\")))")] - [InlineData("Category/QueryableProducts/any(P: true)", "$it => $it.Category.QueryableProducts.Any()")] - public void AnyOperator_OnNavigation_Contradiction(string filter, string expression) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expression, NotTesting); - } + [Theory] + [InlineData("Category/QueryableProducts/any(P: false)", "$it => False")] + [InlineData("Category/QueryableProducts/any(P: false and P/ProductName eq 'Snacks')", "$it => $it.Category.QueryableProducts.Any(P => (False AndAlso (P.ProductName == \"Snacks\")))")] + [InlineData("Category/QueryableProducts/any(P: true)", "$it => $it.Category.QueryableProducts.Any()")] + public void AnyOperator_OnNavigation_Contradiction(string filter, string expression) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expression, NotTesting); + } - [Fact] - public void AnyOperator_OnNavigation_NullCollection() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "Category/EnumerableProducts/any(P: P/ProductName eq 'Snacks')", - "$it => $it.Category.EnumerableProducts.Any(P => (P.ProductName == \"Snacks\"))", - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, - new Product + [Fact] + public void AnyOperator_OnNavigation_NullCollection() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "Category/EnumerableProducts/any(P: P/ProductName eq 'Snacks')", + "$it => $it.Category.EnumerableProducts.Any(P => (P.ProductName == \"Snacks\"))", + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, + new Product + { + Category = new Category { - Category = new Category - { - } - }, - (typeof(ArgumentNullException), false)); + } + }, + (typeof(ArgumentNullException), false)); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product + { + Category = new Category { - Category = new Category + EnumerableProducts = new Product[] { - EnumerableProducts = new Product[] - { - new Product { ProductName = "Snacks" } - } + new Product { ProductName = "Snacks" } } - }, - (true, true)); - } + } + }, + (true, true)); + } - [Fact] - public void AllOperator_OnNavigation_NullCollection() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "Category/EnumerableProducts/all(P: P/ProductName eq 'Snacks')", - "$it => $it.Category.EnumerableProducts.All(P => (P.ProductName == \"Snacks\"))", - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, - new Product + [Fact] + public void AllOperator_OnNavigation_NullCollection() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "Category/EnumerableProducts/all(P: P/ProductName eq 'Snacks')", + "$it => $it.Category.EnumerableProducts.All(P => (P.ProductName == \"Snacks\"))", + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, + new Product + { + Category = new Category { - Category = new Category - { - } - }, - (typeof(ArgumentNullException), false)); + } + }, + (typeof(ArgumentNullException), false)); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product + { + Category = new Category { - Category = new Category + EnumerableProducts = new Product[] { - EnumerableProducts = new Product[] - { - new Product { ProductName = "Snacks" } - } + new Product { ProductName = "Snacks" } } - }, - (true, true)); - } + } + }, + (true, true)); + } - [Fact] - public void MultipleAnys_WithSameRangeVariableName() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "AlternateIDs/any(n: n eq 42) and AlternateAddresses/any(n : n/City eq 'Redmond')", - "$it => ($it.AlternateIDs.Any(n => (n == 42)) AndAlso $it.AlternateAddresses.Any(n => (n.City == \"Redmond\")))", - NotTesting); - } + [Fact] + public void MultipleAnys_WithSameRangeVariableName() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "AlternateIDs/any(n: n eq 42) and AlternateAddresses/any(n : n/City eq 'Redmond')", + "$it => ($it.AlternateIDs.Any(n => (n == 42)) AndAlso $it.AlternateAddresses.Any(n => (n.City == \"Redmond\")))", + NotTesting); + } - [Fact] - public void MultipleAlls_WithSameRangeVariableName() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "AlternateIDs/all(n: n eq 42) and AlternateAddresses/all(n : n/City eq 'Redmond')", - "$it => ($it.AlternateIDs.All(n => (n == 42)) AndAlso $it.AlternateAddresses.All(n => (n.City == \"Redmond\")))", - NotTesting); - } + [Fact] + public void MultipleAlls_WithSameRangeVariableName() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "AlternateIDs/all(n: n eq 42) and AlternateAddresses/all(n : n/City eq 'Redmond')", + "$it => ($it.AlternateIDs.All(n => (n == 42)) AndAlso $it.AlternateAddresses.All(n => (n.City == \"Redmond\")))", + NotTesting); + } - [Fact] - public void AnyOnNavigationEnumerableCollections_EmptyFilter() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "Category/EnumerableProducts/any()", - "$it => $it.Category.EnumerableProducts.Any()", - NotTesting); - } + [Fact] + public void AnyOnNavigationEnumerableCollections_EmptyFilter() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "Category/EnumerableProducts/any()", + "$it => $it.Category.EnumerableProducts.Any()", + NotTesting); + } - [Fact] - public void AnyOnNavigationQueryableCollections_EmptyFilter() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "Category/QueryableProducts/any()", - "$it => $it.Category.QueryableProducts.Any()", - NotTesting); - } + [Fact] + public void AnyOnNavigationQueryableCollections_EmptyFilter() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "Category/QueryableProducts/any()", + "$it => $it.Category.QueryableProducts.Any()", + NotTesting); + } - [Fact] - public void AllOnNavigationEnumerableCollections() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "Category/EnumerableProducts/all(P: P/ProductName eq 'Snacks')", - "$it => $it.Category.EnumerableProducts.All(P => (P.ProductName == \"Snacks\"))", - NotTesting); - } + [Fact] + public void AllOnNavigationEnumerableCollections() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "Category/EnumerableProducts/all(P: P/ProductName eq 'Snacks')", + "$it => $it.Category.EnumerableProducts.All(P => (P.ProductName == \"Snacks\"))", + NotTesting); + } - [Fact] - public void AllOnNavigationQueryableCollections() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "Category/QueryableProducts/all(P: P/ProductName eq 'Snacks')", - "$it => $it.Category.QueryableProducts.All(P => (P.ProductName == \"Snacks\"))", - NotTesting); - } + [Fact] + public void AllOnNavigationQueryableCollections() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "Category/QueryableProducts/all(P: P/ProductName eq 'Snacks')", + "$it => $it.Category.QueryableProducts.All(P => (P.ProductName == \"Snacks\"))", + NotTesting); + } - [Fact] - public void AnyInSequenceNotNested() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "Category/QueryableProducts/any(P: P/ProductName eq 'Snacks') or Category/QueryableProducts/any(P2: P2/ProductName eq 'Snacks')", - "$it => ($it.Category.QueryableProducts.Any(P => (P.ProductName == \"Snacks\")) OrElse $it.Category.QueryableProducts.Any(P2 => (P2.ProductName == \"Snacks\")))", - NotTesting); - } + [Fact] + public void AnyInSequenceNotNested() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "Category/QueryableProducts/any(P: P/ProductName eq 'Snacks') or Category/QueryableProducts/any(P2: P2/ProductName eq 'Snacks')", + "$it => ($it.Category.QueryableProducts.Any(P => (P.ProductName == \"Snacks\")) OrElse $it.Category.QueryableProducts.Any(P2 => (P2.ProductName == \"Snacks\")))", + NotTesting); + } - [Fact] - public void AllInSequenceNotNested() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "Category/QueryableProducts/all(P: P/ProductName eq 'Snacks') or Category/QueryableProducts/all(P2: P2/ProductName eq 'Snacks')", - "$it => ($it.Category.QueryableProducts.All(P => (P.ProductName == \"Snacks\")) OrElse $it.Category.QueryableProducts.All(P2 => (P2.ProductName == \"Snacks\")))", - NotTesting); - } + [Fact] + public void AllInSequenceNotNested() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "Category/QueryableProducts/all(P: P/ProductName eq 'Snacks') or Category/QueryableProducts/all(P2: P2/ProductName eq 'Snacks')", + "$it => ($it.Category.QueryableProducts.All(P => (P.ProductName == \"Snacks\")) OrElse $it.Category.QueryableProducts.All(P2 => (P2.ProductName == \"Snacks\")))", + NotTesting); + } - [Fact] - public void AnyOnPrimitiveCollection() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "AlternateIDs/any(id: id eq 42)", - "$it => $it.AlternateIDs.Any(id => (id == 42))", - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { AlternateIDs = new[] { 1, 2, 42 } }, - (true, true)); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { AlternateIDs = new[] { 1, 2 } }, - (false, false)); - } + [Fact] + public void AnyOnPrimitiveCollection() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "AlternateIDs/any(id: id eq 42)", + "$it => $it.AlternateIDs.Any(id => (id == 42))", + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { AlternateIDs = new[] { 1, 2, 42 } }, + (true, true)); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { AlternateIDs = new[] { 1, 2 } }, + (false, false)); + } - [Fact] - public void AllOnPrimitiveCollection() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "AlternateIDs/all(id: id eq 42)", - "$it => $it.AlternateIDs.All(id => (id == 42))", - NotTesting); - } + [Fact] + public void AllOnPrimitiveCollection() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "AlternateIDs/all(id: id eq 42)", + "$it => $it.AlternateIDs.All(id => (id == 42))", + NotTesting); + } - [Fact] - public void AnyOnComplexCollection() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "AlternateAddresses/any(address: address/City eq 'Redmond')", - "$it => $it.AlternateAddresses.Any(address => (address.City == \"Redmond\"))", - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, - new Product { AlternateAddresses = new[] { new Address { City = "Redmond" } } }, - (true, true)); - - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, - new Product(), - (typeof(ArgumentNullException), false)); - } + [Fact] + public void AnyOnComplexCollection() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "AlternateAddresses/any(address: address/City eq 'Redmond')", + "$it => $it.AlternateAddresses.Any(address => (address.City == \"Redmond\"))", + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, + new Product { AlternateAddresses = new[] { new Address { City = "Redmond" } } }, + (true, true)); + + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, + new Product(), + (typeof(ArgumentNullException), false)); + } - [Fact] - public void AllOnComplexCollection() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "AlternateAddresses/all(address: address/City eq 'Redmond')", - "$it => $it.AlternateAddresses.All(address => (address.City == \"Redmond\"))", - NotTesting); - } + [Fact] + public void AllOnComplexCollection() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "AlternateAddresses/all(address: address/City eq 'Redmond')", + "$it => $it.AlternateAddresses.All(address => (address.City == \"Redmond\"))", + NotTesting); + } - [Fact] - public void RecursiveAllAny() + [Fact] + public void RecursiveAllAny() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "Category/QueryableProducts/all(p: p/Category/EnumerableProducts/any(o: o/ProductName eq 'Snacks'))", + "$it => $it.Category.QueryableProducts.All(p => p.Category.EnumerableProducts.Any(o => (o.ProductName == \"Snacks\")))", + NotTesting); + } + #endregion + + #region String Functions + [Theory] + [InlineData("Abcd", -1, "Abcd", true, typeof(ArgumentOutOfRangeException))] + [InlineData("Abcd", 0, "Abcd", true, true)] + [InlineData("Abcd", 1, "bcd", true, true)] + [InlineData("Abcd", 3, "d", true, true)] + [InlineData("Abcd", 4, "", true, true)] + [InlineData("Abcd", 5, "", true, typeof(ArgumentOutOfRangeException))] + public void StringSubstringStart(string productName, int index, string compareString, bool trueNullPropagation, object falseNullPropagation) + { + // Arrange & Act & Assert + string filter = $"substring(ProductName, {index}) eq '{compareString}'"; + var filters = BindFilterAndVerify(filter); + + // Arrange & Act & Assert + Product product = new Product { ProductName = productName }; + if (falseNullPropagation.GetType() == typeof(bool)) { - // Arrange & Act & Assert - BindFilterAndVerify( - "Category/QueryableProducts/all(p: p/Category/EnumerableProducts/any(o: o/ProductName eq 'Snacks'))", - "$it => $it.Category.QueryableProducts.All(p => p.Category.EnumerableProducts.Any(o => (o.ProductName == \"Snacks\")))", - NotTesting); + bool boolValue = (bool)falseNullPropagation; + InvokeFiltersAndVerify(filters, product, (boolValue, trueNullPropagation)); } - #endregion - - #region String Functions - [Theory] - [InlineData("Abcd", -1, "Abcd", true, typeof(ArgumentOutOfRangeException))] - [InlineData("Abcd", 0, "Abcd", true, true)] - [InlineData("Abcd", 1, "bcd", true, true)] - [InlineData("Abcd", 3, "d", true, true)] - [InlineData("Abcd", 4, "", true, true)] - [InlineData("Abcd", 5, "", true, typeof(ArgumentOutOfRangeException))] - public void StringSubstringStart(string productName, int index, string compareString, bool trueNullPropagation, object falseNullPropagation) + else { - // Arrange & Act & Assert - string filter = $"substring(ProductName, {index}) eq '{compareString}'"; - var filters = BindFilterAndVerify(filter); - - // Arrange & Act & Assert - Product product = new Product { ProductName = productName }; - if (falseNullPropagation.GetType() == typeof(bool)) - { - bool boolValue = (bool)falseNullPropagation; - InvokeFiltersAndVerify(filters, product, (boolValue, trueNullPropagation)); - } - else - { - Type typeValue = (Type)falseNullPropagation; - InvokeFiltersAndThrows(filters, product, (typeValue, trueNullPropagation)); - } + Type typeValue = (Type)falseNullPropagation; + InvokeFiltersAndThrows(filters, product, (typeValue, trueNullPropagation)); } + } - [Theory] - [InlineData("Abcd", -1, 4, "Abcd", true, typeof(ArgumentOutOfRangeException))] - [InlineData("Abcd", -1, 3, "Abc", true, typeof(ArgumentOutOfRangeException))] - [InlineData("Abcd", 0, 1, "A", true, true)] - [InlineData("Abcd", 0, 4, "Abcd", true, true)] - [InlineData("Abcd", 0, 3, "Abc", true, true)] - [InlineData("Abcd", 0, 5, "Abcd", true, typeof(ArgumentOutOfRangeException))] - [InlineData("Abcd", 1, 3, "bcd", true, true)] - [InlineData("Abcd", 1, 5, "bcd", true, typeof(ArgumentOutOfRangeException))] - [InlineData("Abcd", 2, 1, "c", true, true)] - [InlineData("Abcd", 3, 1, "d", true, true)] - [InlineData("Abcd", 4, 1, "", true, typeof(ArgumentOutOfRangeException))] - [InlineData("Abcd", 0, -1, "", true, typeof(ArgumentOutOfRangeException))] - [InlineData("Abcd", 5, -1, "", true, typeof(ArgumentOutOfRangeException))] - public void StringSubstringStartAndLength(string productName, int index, int length, string compareString, bool trueNullPropagation, object falseNullPropagation) - { - // Arrange & Act & Assert - string filter = $"substring(ProductName, {index}, {length}) eq '{compareString}'"; - var filters = BindFilterAndVerify(filter); + [Theory] + [InlineData("Abcd", -1, 4, "Abcd", true, typeof(ArgumentOutOfRangeException))] + [InlineData("Abcd", -1, 3, "Abc", true, typeof(ArgumentOutOfRangeException))] + [InlineData("Abcd", 0, 1, "A", true, true)] + [InlineData("Abcd", 0, 4, "Abcd", true, true)] + [InlineData("Abcd", 0, 3, "Abc", true, true)] + [InlineData("Abcd", 0, 5, "Abcd", true, typeof(ArgumentOutOfRangeException))] + [InlineData("Abcd", 1, 3, "bcd", true, true)] + [InlineData("Abcd", 1, 5, "bcd", true, typeof(ArgumentOutOfRangeException))] + [InlineData("Abcd", 2, 1, "c", true, true)] + [InlineData("Abcd", 3, 1, "d", true, true)] + [InlineData("Abcd", 4, 1, "", true, typeof(ArgumentOutOfRangeException))] + [InlineData("Abcd", 0, -1, "", true, typeof(ArgumentOutOfRangeException))] + [InlineData("Abcd", 5, -1, "", true, typeof(ArgumentOutOfRangeException))] + public void StringSubstringStartAndLength(string productName, int index, int length, string compareString, bool trueNullPropagation, object falseNullPropagation) + { + // Arrange & Act & Assert + string filter = $"substring(ProductName, {index}, {length}) eq '{compareString}'"; + var filters = BindFilterAndVerify(filter); - // Arrange & Act & Assert - Product product = new Product { ProductName = productName }; - if (falseNullPropagation.GetType() == typeof(bool)) - { - bool boolValue = (bool)falseNullPropagation; - InvokeFiltersAndVerify(filters, product, (boolValue, trueNullPropagation)); - } - else - { - Type typeValue = (Type)falseNullPropagation; - InvokeFiltersAndThrows(filters, product, (typeValue, trueNullPropagation)); - } + // Arrange & Act & Assert + Product product = new Product { ProductName = productName }; + if (falseNullPropagation.GetType() == typeof(bool)) + { + bool boolValue = (bool)falseNullPropagation; + InvokeFiltersAndVerify(filters, product, (boolValue, trueNullPropagation)); } - - [Fact] - public void StringFunctions_StringContains() + else { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "contains(ProductName, 'Abc')", - "$it => $it.ProductName.Contains(\"Abc\")", - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); - - InvokeFiltersAndVerify(filters, new Product { ProductName = "Abcd" }, (true, true)); - - InvokeFiltersAndVerify(filters, new Product { ProductName = "Abd" }, (false, false)); + Type typeValue = (Type)falseNullPropagation; + InvokeFiltersAndThrows(filters, product, (typeValue, trueNullPropagation)); } + } - [Fact] - public void StringFunctions_StringStartsWith() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "startswith(ProductName, 'Abc')", - "$it => $it.ProductName.StartsWith(\"Abc\")", - NotTesting); + [Fact] + public void StringFunctions_StringContains() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "contains(ProductName, 'Abc')", + "$it => $it.ProductName.Contains(\"Abc\")", + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "Abcd" }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { ProductName = "Abcd" }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "Abd" }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { ProductName = "Abd" }, (false, false)); + } - [Fact] - public void StringFunctions_StringEndsWith() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "endswith(ProductName, 'Abc')", - "$it => $it.ProductName.EndsWith(\"Abc\")", - NotTesting); + [Fact] + public void StringFunctions_StringStartsWith() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "startswith(ProductName, 'Abc')", + "$it => $it.ProductName.StartsWith(\"Abc\")", + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "AAbc" }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { ProductName = "Abcd" }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "Abcd" }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { ProductName = "Abd" }, (false, false)); + } - [Fact] - public void StringFunctions_StringLength() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "length(ProductName) gt 0", - "$it => ($it.ProductName.Length > 0)", - "$it => ((IIF(($it.ProductName == null), null, Convert($it.ProductName.Length)) > Convert(0)) == True)"); + [Fact] + public void StringFunctions_StringEndsWith() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "endswith(ProductName, 'Abc')", + "$it => $it.ProductName.EndsWith(\"Abc\")", + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "AAbc" }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { ProductName = "AAbc" }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "" }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { ProductName = "Abcd" }, (false, false)); + } - [Fact] - public void StringFunctions_StringIndexOf() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "indexof(ProductName, 'Abc') eq 5", - "$it => ($it.ProductName.IndexOf(\"Abc\") == 5)", - NotTesting); + [Fact] + public void StringFunctions_StringLength() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "length(ProductName) gt 0", + "$it => ($it.ProductName.Length > 0)", + "$it => ((IIF(($it.ProductName == null), null, Convert($it.ProductName.Length)) > Convert(0)) == True)"); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "12345Abc" }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { ProductName = "AAbc" }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "1234Abc" }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { ProductName = "" }, (false, false)); + } - [Fact] - public void StringFunctions_StringSubstring() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "substring(ProductName, 3) eq 'uctName'", - "$it => ($it.ProductName.Substring(3) == \"uctName\")", - NotTesting); + [Fact] + public void StringFunctions_StringIndexOf() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "indexof(ProductName, 'Abc') eq 5", + "$it => ($it.ProductName.IndexOf(\"Abc\") == 5)", + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "123uctName" }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { ProductName = "12345Abc" }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "1234Abc" }, (false, false)); + InvokeFiltersAndVerify(filters, new Product { ProductName = "1234Abc" }, (false, false)); + } - // Arrange & Act & Assert + [Fact] + public void StringFunctions_StringSubstring() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "substring(ProductName, 3) eq 'uctName'", + "$it => ($it.ProductName.Substring(3) == \"uctName\")", + NotTesting); - BindFilterAndVerify( - "substring(ProductName, 3, 4) eq 'uctN'", - "$it => ($it.ProductName.Substring(3, 4) == \"uctN\")", - NotTesting); - } + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); - [Fact] - public void StringFunctions_StringToLower() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "tolower(ProductName) eq 'tasty treats'", - "$it => ($it.ProductName.ToLower() == \"tasty treats\")", - NotTesting); + InvokeFiltersAndVerify(filters, new Product { ProductName = "123uctName" }, (true, true)); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); + InvokeFiltersAndVerify(filters, new Product { ProductName = "1234Abc" }, (false, false)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "Tasty Treats" }, (true, true)); + // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, new Product { ProductName = "Tasty Treatss" }, (false, false)); - } + BindFilterAndVerify( + "substring(ProductName, 3, 4) eq 'uctN'", + "$it => ($it.ProductName.Substring(3, 4) == \"uctN\")", + NotTesting); + } - [Fact] - public void StringFunctions_StringToUpper() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "toupper(ProductName) eq 'TASTY TREATS'", - "$it => ($it.ProductName.ToUpper() == \"TASTY TREATS\")", - NotTesting); + [Fact] + public void StringFunctions_StringToLower() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "tolower(ProductName) eq 'tasty treats'", + "$it => ($it.ProductName.ToLower() == \"tasty treats\")", + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "Tasty Treats" }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { ProductName = "Tasty Treats" }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "Tasty Treatss" }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { ProductName = "Tasty Treatss" }, (false, false)); + } - [Fact] - public void StringFunctions_StringTrim() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "trim(ProductName) eq 'Tasty Treats'", - "$it => ($it.ProductName.Trim() == \"Tasty Treats\")", - NotTesting); + [Fact] + public void StringFunctions_StringToUpper() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "toupper(ProductName) eq 'TASTY TREATS'", + "$it => ($it.ProductName.ToUpper() == \"TASTY TREATS\")", + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); - InvokeFiltersAndVerify(filters, new Product { ProductName = " Tasty Treats " }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { ProductName = "Tasty Treats" }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { ProductName = " Tasty Treatss " }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { ProductName = "Tasty Treatss" }, (false, false)); + } - [Fact] - public void StringFunctions_StringConcat() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "concat(ProductName, 'Bar') eq 'FoodBar'", - "$it => ($it.ProductName.Concat(\"Bar\") == \"FoodBar\")", - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, new Product { ProductName = "Food" }, (true, true)); - } + [Fact] + public void StringFunctions_StringTrim() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "trim(ProductName) eq 'Tasty Treats'", + "$it => ($it.ProductName.Trim() == \"Tasty Treats\")", + NotTesting); - [Fact] - public void StringFunctions_StringMatchesPattern() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "matchesPattern(ProductName, 'A\\wc')", - "$it => $it.ProductName.IsMatch(\"A\\wc\", ECMAScript)", - NotTesting); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(NullReferenceException), false)); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(ArgumentNullException), false)); + InvokeFiltersAndVerify(filters, new Product { ProductName = " Tasty Treats " }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { ProductName = "Abcd" }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { ProductName = " Tasty Treatss " }, (false, false)); + } - InvokeFiltersAndVerify(filters, new Product { ProductName = "Abd" }, (false, false)); + [Fact] + public void StringFunctions_StringConcat() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "concat(ProductName, 'Bar') eq 'FoodBar'", + "$it => ($it.ProductName.Concat(\"Bar\") == \"FoodBar\")", + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, new Product { ProductName = "Food" }, (true, true)); + } - InvokeFiltersAndVerify(filters, new Product { ProductName = "Aθd" }, (false, false)); // ECMAScript has strict matching of \w - } + [Fact] + public void StringFunctions_StringMatchesPattern() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "matchesPattern(ProductName, 'A\\wc')", + "$it => $it.ProductName.IsMatch(\"A\\wc\", ECMAScript)", + NotTesting); - [Fact] - public void StringFunctions_RecursiveMethodCall() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "floor(floor(UnitPrice)) eq 123m", - "$it => ($it.UnitPrice.Value.Floor().Floor() == 123)", - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { }, (typeof(InvalidOperationException), false)); - } - #endregion + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { ProductName = null }, (typeof(ArgumentNullException), false)); - #region Date Functions - [Fact] - public void DateFunctions_DateDay() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "day(DiscontinuedDate) eq 8", - "$it => ($it.DiscontinuedDate.Value.Day == 8)", - NotTesting); + InvokeFiltersAndVerify(filters, new Product { ProductName = "Abcd" }, (true, true)); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { }, (typeof(InvalidOperationException), false)); + InvokeFiltersAndVerify(filters, new Product { ProductName = "Abd" }, (false, false)); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, new Product { DiscontinuedDate = new DateTime(2000, 10, 8) }, (true, true)); - } + InvokeFiltersAndVerify(filters, new Product { ProductName = "Aθd" }, (false, false)); // ECMAScript has strict matching of \w + } - [Fact] - public void DateFunctions_DateDayNonNullable() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "day(NonNullableDiscontinuedDate) eq 8", - "$it => ($it.NonNullableDiscontinuedDate.Day == 8)"); - } + [Fact] + public void StringFunctions_RecursiveMethodCall() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "floor(floor(UnitPrice)) eq 123m", + "$it => ($it.UnitPrice.Value.Floor().Floor() == 123)", + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { }, (typeof(InvalidOperationException), false)); + } + #endregion - [Fact] - public void DateFunctions_DateMonth() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "month(DiscontinuedDate) eq 8", - "$it => ($it.DiscontinuedDate.Value.Month == 8)", - NotTesting); - } + #region Date Functions + [Fact] + public void DateFunctions_DateDay() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "day(DiscontinuedDate) eq 8", + "$it => ($it.DiscontinuedDate.Value.Day == 8)", + NotTesting); - [Fact] - public void DateFunctions_DateYear() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "year(DiscontinuedDate) eq 1974", - "$it => ($it.DiscontinuedDate.Value.Year == 1974)", - NotTesting); - } + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { }, (typeof(InvalidOperationException), false)); - [Fact] - public void DateFunctions_DateHour() - { - // Arrange & Act & Assert - BindFilterAndVerify("hour(DiscontinuedDate) eq 8", - "$it => ($it.DiscontinuedDate.Value.Hour == 8)", - NotTesting); - } + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, new Product { DiscontinuedDate = new DateTime(2000, 10, 8) }, (true, true)); + } - [Fact] - public void DateFunctions_DateMinute() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "minute(DiscontinuedDate) eq 12", - "$it => ($it.DiscontinuedDate.Value.Minute == 12)", - NotTesting); - } + [Fact] + public void DateFunctions_DateDayNonNullable() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "day(NonNullableDiscontinuedDate) eq 8", + "$it => ($it.NonNullableDiscontinuedDate.Day == 8)"); + } - [Fact] - public void DateFunctions_DateSecond() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "second(DiscontinuedDate) eq 33", - "$it => ($it.DiscontinuedDate.Value.Second == 33)", - NotTesting); - } + [Fact] + public void DateFunctions_DateMonth() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "month(DiscontinuedDate) eq 8", + "$it => ($it.DiscontinuedDate.Value.Month == 8)", + NotTesting); + } - [Theory] - [InlineData("year(DiscontinuedOffset) eq 100", "$it => ($it.DiscontinuedOffset.Year == 100)")] - [InlineData("month(DiscontinuedOffset) eq 100", "$it => ($it.DiscontinuedOffset.Month == 100)")] - [InlineData("day(DiscontinuedOffset) eq 100", "$it => ($it.DiscontinuedOffset.Day == 100)")] - [InlineData("hour(DiscontinuedOffset) eq 100", "$it => ($it.DiscontinuedOffset.Hour == 100)")] - [InlineData("minute(DiscontinuedOffset) eq 100", "$it => ($it.DiscontinuedOffset.Minute == 100)")] - [InlineData("second(DiscontinuedOffset) eq 100", "$it => ($it.DiscontinuedOffset.Second == 100)")] - [InlineData("now() eq 2016-11-08Z", "$it => (DateTimeOffset.UtcNow == 11/08/2016 00:00:00 +00:00)")] - public void DateFunctions_DateTimeOffsetFunctions(string filter, string expression) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expression); - } + [Fact] + public void DateFunctions_DateYear() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "year(DiscontinuedDate) eq 1974", + "$it => ($it.DiscontinuedDate.Value.Year == 1974)", + NotTesting); + } - [Theory] - [InlineData("year(Birthday) eq 100", "$it => {0}.Year == 100)")] - [InlineData("month(Birthday) eq 100", "$it => {0}.Month == 100)")] - [InlineData("day(Birthday) eq 100", "$it => {0}.Day == 100)")] - [InlineData("hour(Birthday) eq 100", "$it => {0}.Hour == 100)")] - [InlineData("minute(Birthday) eq 100", "$it => {0}.Minute == 100)")] - [InlineData("second(Birthday) eq 100", "$it => {0}.Second == 100)")] - public void DateFunctions_DateTimeFunctions(string filter, string expression) - { - // Arrange & Act & Assert - string expect = string.Format(expression, "($it.Birthday"); - BindFilterAndVerify(filter, expect); - } + [Fact] + public void DateFunctions_DateHour() + { + // Arrange & Act & Assert + BindFilterAndVerify("hour(DiscontinuedDate) eq 8", + "$it => ($it.DiscontinuedDate.Value.Hour == 8)", + NotTesting); + } - [Theory] - [InlineData("year(NullableDateProperty) eq 2015", "$it => ($it.NullableDateProperty.Value.Year == 2015)")] - [InlineData("month(NullableDateProperty) eq 12", "$it => ($it.NullableDateProperty.Value.Month == 12)")] - [InlineData("day(NullableDateProperty) eq 23", "$it => ($it.NullableDateProperty.Value.Day == 23)")] - public void DateFunctions_DateFunctions_Nullable(string filter, string expression) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expression, NotTesting); - } + [Fact] + public void DateFunctions_DateMinute() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "minute(DiscontinuedDate) eq 12", + "$it => ($it.DiscontinuedDate.Value.Minute == 12)", + NotTesting); + } - [Theory] - [InlineData("year(DateProperty) eq 2015", "$it => ($it.DateProperty.Year == 2015)")] - [InlineData("month(DateProperty) eq 12", "$it => ($it.DateProperty.Month == 12)")] - [InlineData("day(DateProperty) eq 23", "$it => ($it.DateProperty.Day == 23)")] - public void DateFunctions_DateFunctions_NonNullable(string filter, string expression) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expression); - } + [Fact] + public void DateFunctions_DateSecond() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "second(DiscontinuedDate) eq 33", + "$it => ($it.DiscontinuedDate.Value.Second == 33)", + NotTesting); + } - [Theory] - [InlineData("hour(NullableTimeOfDayProperty) eq 10", "$it => ($it.NullableTimeOfDayProperty.Value.Hours == 10)")] - [InlineData("minute(NullableTimeOfDayProperty) eq 20", "$it => ($it.NullableTimeOfDayProperty.Value.Minutes == 20)")] - [InlineData("second(NullableTimeOfDayProperty) eq 30", "$it => ($it.NullableTimeOfDayProperty.Value.Seconds == 30)")] - public void DateFunctions_TimeOfDayFunctions_Nullable(string filter, string expression) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expression, NotTesting); - } + [Theory] + [InlineData("year(DiscontinuedOffset) eq 100", "$it => ($it.DiscontinuedOffset.Year == 100)")] + [InlineData("month(DiscontinuedOffset) eq 100", "$it => ($it.DiscontinuedOffset.Month == 100)")] + [InlineData("day(DiscontinuedOffset) eq 100", "$it => ($it.DiscontinuedOffset.Day == 100)")] + [InlineData("hour(DiscontinuedOffset) eq 100", "$it => ($it.DiscontinuedOffset.Hour == 100)")] + [InlineData("minute(DiscontinuedOffset) eq 100", "$it => ($it.DiscontinuedOffset.Minute == 100)")] + [InlineData("second(DiscontinuedOffset) eq 100", "$it => ($it.DiscontinuedOffset.Second == 100)")] + [InlineData("now() eq 2016-11-08Z", "$it => (DateTimeOffset.UtcNow == 11/08/2016 00:00:00 +00:00)")] + public void DateFunctions_DateTimeOffsetFunctions(string filter, string expression) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expression); + } - [Theory] - [InlineData("hour(TimeOfDayProperty) eq 10", "$it => ($it.TimeOfDayProperty.Hours == 10)")] - [InlineData("minute(TimeOfDayProperty) eq 20", "$it => ($it.TimeOfDayProperty.Minutes == 20)")] - [InlineData("second(TimeOfDayProperty) eq 30", "$it => ($it.TimeOfDayProperty.Seconds == 30)")] - public void DateFunctions_TimeOfDayFunctions_NonNullable(string filter, string expression) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expression); - } + [Theory] + [InlineData("year(Birthday) eq 100", "$it => {0}.Year == 100)")] + [InlineData("month(Birthday) eq 100", "$it => {0}.Month == 100)")] + [InlineData("day(Birthday) eq 100", "$it => {0}.Day == 100)")] + [InlineData("hour(Birthday) eq 100", "$it => {0}.Hour == 100)")] + [InlineData("minute(Birthday) eq 100", "$it => {0}.Minute == 100)")] + [InlineData("second(Birthday) eq 100", "$it => {0}.Second == 100)")] + public void DateFunctions_DateTimeFunctions(string filter, string expression) + { + // Arrange & Act & Assert + string expect = string.Format(expression, "($it.Birthday"); + BindFilterAndVerify(filter, expect); + } - [Theory] - [InlineData("fractionalseconds(DiscontinuedDate) eq 0.2", "$it => ((Convert($it.DiscontinuedDate.Value.Millisecond) / 1000) == 0.2)")] - [InlineData("fractionalseconds(NullableTimeOfDayProperty) eq 0.2", "$it => ((Convert($it.NullableTimeOfDayProperty.Value.Milliseconds) / 1000) == 0.2)")] - public void DateFunctions_FractionalsecondsFunction_Nullable(string filter, string expression) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expression, NotTesting); - } + [Theory] + [InlineData("year(NullableDateProperty) eq 2015", "$it => ($it.NullableDateProperty.Value.Year == 2015)")] + [InlineData("month(NullableDateProperty) eq 12", "$it => ($it.NullableDateProperty.Value.Month == 12)")] + [InlineData("day(NullableDateProperty) eq 23", "$it => ($it.NullableDateProperty.Value.Day == 23)")] + public void DateFunctions_DateFunctions_Nullable(string filter, string expression) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expression, NotTesting); + } - [Theory] - [InlineData("fractionalseconds(NonNullableDiscontinuedDate) eq 0.2", "$it => ((Convert($it.NonNullableDiscontinuedDate.Millisecond) / 1000) == 0.2)")] - [InlineData("fractionalseconds(TimeOfDayProperty) eq 0.2", "$it => ((Convert($it.TimeOfDayProperty.Milliseconds) / 1000) == 0.2)")] - public void DateFunctions_FractionalsecondsFunction_NonNullable(string filter, string expression) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expression); - } + [Theory] + [InlineData("year(DateProperty) eq 2015", "$it => ($it.DateProperty.Year == 2015)")] + [InlineData("month(DateProperty) eq 12", "$it => ($it.DateProperty.Month == 12)")] + [InlineData("day(DateProperty) eq 23", "$it => ($it.DateProperty.Day == 23)")] + public void DateFunctions_DateFunctions_NonNullable(string filter, string expression) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expression); + } - [Theory] - [InlineData("date(DiscontinuedDate) eq 2015-02-26", - "$it => (((($it.DiscontinuedDate.Value.Year * 10000) + ($it.DiscontinuedDate.Value.Month * 100)) + $it.DiscontinuedDate.Value.Day) == (((2015-02-26.Year * 10000) + (2015-02-26.Month * 100)) + 2015-02-26.Day))")] - [InlineData("date(DiscontinuedDate) lt 2016-02-26", - "$it => (((($it.DiscontinuedDate.Value.Year * 10000) + ($it.DiscontinuedDate.Value.Month * 100)) + $it.DiscontinuedDate.Value.Day) < (((2016-02-26.Year * 10000) + (2016-02-26.Month * 100)) + 2016-02-26.Day))")] - [InlineData("2015-02-26 ge date(DiscontinuedDate)", - "$it => ((((2015-02-26.Year * 10000) + (2015-02-26.Month * 100)) + 2015-02-26.Day) >= ((($it.DiscontinuedDate.Value.Year * 10000) + ($it.DiscontinuedDate.Value.Month * 100)) + $it.DiscontinuedDate.Value.Day))")] - [InlineData("null ne date(DiscontinuedDate)", "$it => (null != $it.DiscontinuedDate)")] - [InlineData("date(DiscontinuedDate) eq null", "$it => ($it.DiscontinuedDate == null)")] - public void DateFunctions_DateFunction_Nullable(string filter, string expression) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expression, NotTesting); - } + [Theory] + [InlineData("hour(NullableTimeOfDayProperty) eq 10", "$it => ($it.NullableTimeOfDayProperty.Value.Hours == 10)")] + [InlineData("minute(NullableTimeOfDayProperty) eq 20", "$it => ($it.NullableTimeOfDayProperty.Value.Minutes == 20)")] + [InlineData("second(NullableTimeOfDayProperty) eq 30", "$it => ($it.NullableTimeOfDayProperty.Value.Seconds == 30)")] + public void DateFunctions_TimeOfDayFunctions_Nullable(string filter, string expression) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expression, NotTesting); + } - [Theory] - [InlineData("date(NonNullableDiscontinuedDate) eq 2015-02-26", - "$it => (((($it.NonNullableDiscontinuedDate.Year * 10000) + ($it.NonNullableDiscontinuedDate.Month * 100)) + $it.NonNullableDiscontinuedDate.Day) == (((2015-02-26.Year * 10000) + (2015-02-26.Month * 100)) + 2015-02-26.Day))")] - [InlineData("date(NonNullableDiscontinuedDate) lt 2016-02-26", - "$it => (((($it.NonNullableDiscontinuedDate.Year * 10000) + ($it.NonNullableDiscontinuedDate.Month * 100)) + $it.NonNullableDiscontinuedDate.Day) < (((2016-02-26.Year * 10000) + (2016-02-26.Month * 100)) + 2016-02-26.Day))")] - [InlineData("2015-02-26 ge date(NonNullableDiscontinuedDate)", - "$it => ((((2015-02-26.Year * 10000) + (2015-02-26.Month * 100)) + 2015-02-26.Day) >= ((($it.NonNullableDiscontinuedDate.Year * 10000) + ($it.NonNullableDiscontinuedDate.Month * 100)) + $it.NonNullableDiscontinuedDate.Day))")] - public void DateFunctions_DateFunction_NonNullable(string filter, string expression) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expression); - } + [Theory] + [InlineData("hour(TimeOfDayProperty) eq 10", "$it => ($it.TimeOfDayProperty.Hours == 10)")] + [InlineData("minute(TimeOfDayProperty) eq 20", "$it => ($it.TimeOfDayProperty.Minutes == 20)")] + [InlineData("second(TimeOfDayProperty) eq 30", "$it => ($it.TimeOfDayProperty.Seconds == 30)")] + public void DateFunctions_TimeOfDayFunctions_NonNullable(string filter, string expression) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expression); + } - [Theory] - [InlineData("time(DiscontinuedDate) eq 01:02:03.0040000", - "$it => (((Convert($it.DiscontinuedDate.Value.Hour) * 36000000000) + ((Convert($it.DiscontinuedDate.Value.Minute) * 600000000) + ((Convert($it.DiscontinuedDate.Value.Second) * 10000000) + Convert($it.DiscontinuedDate.Value.Millisecond)))) == ((Convert(01:02:03.0040000.Hours) * 36000000000) + ((Convert(01:02:03.0040000.Minutes) * 600000000) + ((Convert(01:02:03.0040000.Seconds) * 10000000) + Convert(01:02:03.0040000.Milliseconds)))))")] - [InlineData("time(DiscontinuedDate) ge 01:02:03.0040000", - "$it => (((Convert($it.DiscontinuedDate.Value.Hour) * 36000000000) + ((Convert($it.DiscontinuedDate.Value.Minute) * 600000000) + ((Convert($it.DiscontinuedDate.Value.Second) * 10000000) + Convert($it.DiscontinuedDate.Value.Millisecond)))) >= ((Convert(01:02:03.0040000.Hours) * 36000000000) + ((Convert(01:02:03.0040000.Minutes) * 600000000) + ((Convert(01:02:03.0040000.Seconds) * 10000000) + Convert(01:02:03.0040000.Milliseconds)))))")] - [InlineData("01:02:03.0040000 le time(DiscontinuedDate)", - "$it => (((Convert(01:02:03.0040000.Hours) * 36000000000) + ((Convert(01:02:03.0040000.Minutes) * 600000000) + ((Convert(01:02:03.0040000.Seconds) * 10000000) + Convert(01:02:03.0040000.Milliseconds)))) <= ((Convert($it.DiscontinuedDate.Value.Hour) * 36000000000) + ((Convert($it.DiscontinuedDate.Value.Minute) * 600000000) + ((Convert($it.DiscontinuedDate.Value.Second) * 10000000) + Convert($it.DiscontinuedDate.Value.Millisecond)))))")] - [InlineData("null ne time(DiscontinuedDate)", "$it => (null != $it.DiscontinuedDate)")] - [InlineData("time(DiscontinuedDate) eq null", "$it => ($it.DiscontinuedDate == null)")] - public void DateFunctions_TimeFunction_Nullable(string filter, string expression) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expression, NotTesting); - } + [Theory] + [InlineData("fractionalseconds(DiscontinuedDate) eq 0.2", "$it => ((Convert($it.DiscontinuedDate.Value.Millisecond) / 1000) == 0.2)")] + [InlineData("fractionalseconds(NullableTimeOfDayProperty) eq 0.2", "$it => ((Convert($it.NullableTimeOfDayProperty.Value.Milliseconds) / 1000) == 0.2)")] + public void DateFunctions_FractionalsecondsFunction_Nullable(string filter, string expression) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expression, NotTesting); + } - [Theory] - [InlineData("time(NonNullableDiscontinuedDate) eq 01:02:03.0040000", - "$it => (((Convert($it.NonNullableDiscontinuedDate.Hour) * 36000000000) + ((Convert($it.NonNullableDiscontinuedDate.Minute) * 600000000) + ((Convert($it.NonNullableDiscontinuedDate.Second) * 10000000) + Convert($it.NonNullableDiscontinuedDate.Millisecond)))) == ((Convert(01:02:03.0040000.Hours) * 36000000000) + ((Convert(01:02:03.0040000.Minutes) * 600000000) + ((Convert(01:02:03.0040000.Seconds) * 10000000) + Convert(01:02:03.0040000.Milliseconds)))))")] - [InlineData("time(NonNullableDiscontinuedDate) ge 01:02:03.0040000", - "$it => (((Convert($it.NonNullableDiscontinuedDate.Hour) * 36000000000) + ((Convert($it.NonNullableDiscontinuedDate.Minute) * 600000000) + ((Convert($it.NonNullableDiscontinuedDate.Second) * 10000000) + Convert($it.NonNullableDiscontinuedDate.Millisecond)))) >= ((Convert(01:02:03.0040000.Hours) * 36000000000) + ((Convert(01:02:03.0040000.Minutes) * 600000000) + ((Convert(01:02:03.0040000.Seconds) * 10000000) + Convert(01:02:03.0040000.Milliseconds)))))")] - [InlineData("01:02:03.0040000 le time(NonNullableDiscontinuedDate)", - "$it => (((Convert(01:02:03.0040000.Hours) * 36000000000) + ((Convert(01:02:03.0040000.Minutes) * 600000000) + ((Convert(01:02:03.0040000.Seconds) * 10000000) + Convert(01:02:03.0040000.Milliseconds)))) <= ((Convert($it.NonNullableDiscontinuedDate.Hour) * 36000000000) + ((Convert($it.NonNullableDiscontinuedDate.Minute) * 600000000) + ((Convert($it.NonNullableDiscontinuedDate.Second) * 10000000) + Convert($it.NonNullableDiscontinuedDate.Millisecond)))))")] - public void DateFunctions_TimeFunction_NonNullable(string filter, string expression) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expression); - } - #endregion + [Theory] + [InlineData("fractionalseconds(NonNullableDiscontinuedDate) eq 0.2", "$it => ((Convert($it.NonNullableDiscontinuedDate.Millisecond) / 1000) == 0.2)")] + [InlineData("fractionalseconds(TimeOfDayProperty) eq 0.2", "$it => ((Convert($it.TimeOfDayProperty.Milliseconds) / 1000) == 0.2)")] + public void DateFunctions_FractionalsecondsFunction_NonNullable(string filter, string expression) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expression); + } - #region Math Functions - [Fact] - public void MathFunctions_MathRoundDecimal() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "round(UnitPrice) gt 5.00m", - string.Format(CultureInfo.InvariantCulture, "$it => ($it.UnitPrice.Value.Round() > {0:0.00})", 5.0), - NotTesting); + [Theory] + [InlineData("date(DiscontinuedDate) eq 2015-02-26", + "$it => (((($it.DiscontinuedDate.Value.Year * 10000) + ($it.DiscontinuedDate.Value.Month * 100)) + $it.DiscontinuedDate.Value.Day) == (((2015-02-26.Year * 10000) + (2015-02-26.Month * 100)) + 2015-02-26.Day))")] + [InlineData("date(DiscontinuedDate) lt 2016-02-26", + "$it => (((($it.DiscontinuedDate.Value.Year * 10000) + ($it.DiscontinuedDate.Value.Month * 100)) + $it.DiscontinuedDate.Value.Day) < (((2016-02-26.Year * 10000) + (2016-02-26.Month * 100)) + 2016-02-26.Day))")] + [InlineData("2015-02-26 ge date(DiscontinuedDate)", + "$it => ((((2015-02-26.Year * 10000) + (2015-02-26.Month * 100)) + 2015-02-26.Day) >= ((($it.DiscontinuedDate.Value.Year * 10000) + ($it.DiscontinuedDate.Value.Month * 100)) + $it.DiscontinuedDate.Value.Day))")] + [InlineData("null ne date(DiscontinuedDate)", "$it => (null != $it.DiscontinuedDate)")] + [InlineData("date(DiscontinuedDate) eq null", "$it => ($it.DiscontinuedDate == null)")] + public void DateFunctions_DateFunction_Nullable(string filter, string expression) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expression, NotTesting); + } - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { UnitPrice = null }, (typeof(InvalidOperationException), false)); + [Theory] + [InlineData("date(NonNullableDiscontinuedDate) eq 2015-02-26", + "$it => (((($it.NonNullableDiscontinuedDate.Year * 10000) + ($it.NonNullableDiscontinuedDate.Month * 100)) + $it.NonNullableDiscontinuedDate.Day) == (((2015-02-26.Year * 10000) + (2015-02-26.Month * 100)) + 2015-02-26.Day))")] + [InlineData("date(NonNullableDiscontinuedDate) lt 2016-02-26", + "$it => (((($it.NonNullableDiscontinuedDate.Year * 10000) + ($it.NonNullableDiscontinuedDate.Month * 100)) + $it.NonNullableDiscontinuedDate.Day) < (((2016-02-26.Year * 10000) + (2016-02-26.Month * 100)) + 2016-02-26.Day))")] + [InlineData("2015-02-26 ge date(NonNullableDiscontinuedDate)", + "$it => ((((2015-02-26.Year * 10000) + (2015-02-26.Month * 100)) + 2015-02-26.Day) >= ((($it.NonNullableDiscontinuedDate.Year * 10000) + ($it.NonNullableDiscontinuedDate.Month * 100)) + $it.NonNullableDiscontinuedDate.Day))")] + public void DateFunctions_DateFunction_NonNullable(string filter, string expression) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expression); + } - InvokeFiltersAndVerify(filters, new Product { UnitPrice = 5.9m }, (true, true)); + [Theory] + [InlineData("time(DiscontinuedDate) eq 01:02:03.0040000", + "$it => (((Convert($it.DiscontinuedDate.Value.Hour) * 36000000000) + ((Convert($it.DiscontinuedDate.Value.Minute) * 600000000) + ((Convert($it.DiscontinuedDate.Value.Second) * 10000000) + Convert($it.DiscontinuedDate.Value.Millisecond)))) == ((Convert(01:02:03.0040000.Hours) * 36000000000) + ((Convert(01:02:03.0040000.Minutes) * 600000000) + ((Convert(01:02:03.0040000.Seconds) * 10000000) + Convert(01:02:03.0040000.Milliseconds)))))")] + [InlineData("time(DiscontinuedDate) ge 01:02:03.0040000", + "$it => (((Convert($it.DiscontinuedDate.Value.Hour) * 36000000000) + ((Convert($it.DiscontinuedDate.Value.Minute) * 600000000) + ((Convert($it.DiscontinuedDate.Value.Second) * 10000000) + Convert($it.DiscontinuedDate.Value.Millisecond)))) >= ((Convert(01:02:03.0040000.Hours) * 36000000000) + ((Convert(01:02:03.0040000.Minutes) * 600000000) + ((Convert(01:02:03.0040000.Seconds) * 10000000) + Convert(01:02:03.0040000.Milliseconds)))))")] + [InlineData("01:02:03.0040000 le time(DiscontinuedDate)", + "$it => (((Convert(01:02:03.0040000.Hours) * 36000000000) + ((Convert(01:02:03.0040000.Minutes) * 600000000) + ((Convert(01:02:03.0040000.Seconds) * 10000000) + Convert(01:02:03.0040000.Milliseconds)))) <= ((Convert($it.DiscontinuedDate.Value.Hour) * 36000000000) + ((Convert($it.DiscontinuedDate.Value.Minute) * 600000000) + ((Convert($it.DiscontinuedDate.Value.Second) * 10000000) + Convert($it.DiscontinuedDate.Value.Millisecond)))))")] + [InlineData("null ne time(DiscontinuedDate)", "$it => (null != $it.DiscontinuedDate)")] + [InlineData("time(DiscontinuedDate) eq null", "$it => ($it.DiscontinuedDate == null)")] + public void DateFunctions_TimeFunction_Nullable(string filter, string expression) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expression, NotTesting); + } - InvokeFiltersAndVerify(filters, new Product { UnitPrice = 5.4m }, (false, false)); - } + [Theory] + [InlineData("time(NonNullableDiscontinuedDate) eq 01:02:03.0040000", + "$it => (((Convert($it.NonNullableDiscontinuedDate.Hour) * 36000000000) + ((Convert($it.NonNullableDiscontinuedDate.Minute) * 600000000) + ((Convert($it.NonNullableDiscontinuedDate.Second) * 10000000) + Convert($it.NonNullableDiscontinuedDate.Millisecond)))) == ((Convert(01:02:03.0040000.Hours) * 36000000000) + ((Convert(01:02:03.0040000.Minutes) * 600000000) + ((Convert(01:02:03.0040000.Seconds) * 10000000) + Convert(01:02:03.0040000.Milliseconds)))))")] + [InlineData("time(NonNullableDiscontinuedDate) ge 01:02:03.0040000", + "$it => (((Convert($it.NonNullableDiscontinuedDate.Hour) * 36000000000) + ((Convert($it.NonNullableDiscontinuedDate.Minute) * 600000000) + ((Convert($it.NonNullableDiscontinuedDate.Second) * 10000000) + Convert($it.NonNullableDiscontinuedDate.Millisecond)))) >= ((Convert(01:02:03.0040000.Hours) * 36000000000) + ((Convert(01:02:03.0040000.Minutes) * 600000000) + ((Convert(01:02:03.0040000.Seconds) * 10000000) + Convert(01:02:03.0040000.Milliseconds)))))")] + [InlineData("01:02:03.0040000 le time(NonNullableDiscontinuedDate)", + "$it => (((Convert(01:02:03.0040000.Hours) * 36000000000) + ((Convert(01:02:03.0040000.Minutes) * 600000000) + ((Convert(01:02:03.0040000.Seconds) * 10000000) + Convert(01:02:03.0040000.Milliseconds)))) <= ((Convert($it.NonNullableDiscontinuedDate.Hour) * 36000000000) + ((Convert($it.NonNullableDiscontinuedDate.Minute) * 600000000) + ((Convert($it.NonNullableDiscontinuedDate.Second) * 10000000) + Convert($it.NonNullableDiscontinuedDate.Millisecond)))))")] + public void DateFunctions_TimeFunction_NonNullable(string filter, string expression) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expression); + } + #endregion - [Fact] - public void MathFunctions_MathRoundDouble() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "round(Weight) gt 5d", - string.Format(CultureInfo.InvariantCulture, "$it => ($it.Weight.Value.Round() > {0})", 5), - NotTesting); + #region Math Functions + [Fact] + public void MathFunctions_MathRoundDecimal() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "round(UnitPrice) gt 5.00m", + string.Format(CultureInfo.InvariantCulture, "$it => ($it.UnitPrice.Value.Round() > {0:0.00})", 5.0), + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { Weight = null }, (typeof(InvalidOperationException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { UnitPrice = null }, (typeof(InvalidOperationException), false)); - InvokeFiltersAndVerify(filters, new Product { Weight = 5.9d }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { UnitPrice = 5.9m }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { Weight = 5.4d }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { UnitPrice = 5.4m }, (false, false)); + } - [Fact] - public void MathFunctions_MathRoundFloat() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "round(Width) gt 5f", - string.Format(CultureInfo.InvariantCulture, "$it => (Convert($it.Width).Value.Round() > {0})", 5), - NotTesting); + [Fact] + public void MathFunctions_MathRoundDouble() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "round(Weight) gt 5d", + string.Format(CultureInfo.InvariantCulture, "$it => ($it.Weight.Value.Round() > {0})", 5), + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { Width = null }, (typeof(InvalidOperationException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { Weight = null }, (typeof(InvalidOperationException), false)); - InvokeFiltersAndVerify(filters, new Product { Width = 5.9f }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { Weight = 5.9d }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { Width = 5.4f }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { Weight = 5.4d }, (false, false)); + } - [Fact] - public void MathFunctions_MathFloorDecimal() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "floor(UnitPrice) eq 5", - "$it => ($it.UnitPrice.Value.Floor() == 5)", - NotTesting); + [Fact] + public void MathFunctions_MathRoundFloat() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "round(Width) gt 5f", + string.Format(CultureInfo.InvariantCulture, "$it => (Convert($it.Width).Value.Round() > {0})", 5), + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { UnitPrice = null }, (typeof(InvalidOperationException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { Width = null }, (typeof(InvalidOperationException), false)); - InvokeFiltersAndVerify(filters, new Product { UnitPrice = 5.4m }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { Width = 5.9f }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { UnitPrice = 4.4m }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { Width = 5.4f }, (false, false)); + } - [Fact] - public void MathFunctions_MathFloorDouble() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "floor(Weight) eq 5", - "$it => ($it.Weight.Value.Floor() == 5)", - NotTesting); + [Fact] + public void MathFunctions_MathFloorDecimal() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "floor(UnitPrice) eq 5", + "$it => ($it.UnitPrice.Value.Floor() == 5)", + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { Weight = null }, (typeof(InvalidOperationException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { UnitPrice = null }, (typeof(InvalidOperationException), false)); - InvokeFiltersAndVerify(filters, new Product { Weight = 5.4d }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { UnitPrice = 5.4m }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { Weight = 4.4d }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { UnitPrice = 4.4m }, (false, false)); + } - [Fact] - public void MathFunctions_MathFloorFloat() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "floor(Width) eq 5", - "$it => (Convert($it.Width).Value.Floor() == 5)", - NotTesting); + [Fact] + public void MathFunctions_MathFloorDouble() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "floor(Weight) eq 5", + "$it => ($it.Weight.Value.Floor() == 5)", + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { Width = null }, (typeof(InvalidOperationException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { Weight = null }, (typeof(InvalidOperationException), false)); - InvokeFiltersAndVerify(filters, new Product { Width = 5.4f }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { Weight = 5.4d }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { Width = 4.4f }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { Weight = 4.4d }, (false, false)); + } - [Fact] - public void MathFunctions_MathCeilingDecimal() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "ceiling(UnitPrice) eq 5", - "$it => ($it.UnitPrice.Value.Ceiling() == 5)", - NotTesting); + [Fact] + public void MathFunctions_MathFloorFloat() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "floor(Width) eq 5", + "$it => (Convert($it.Width).Value.Floor() == 5)", + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { UnitPrice = null }, (typeof(InvalidOperationException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { Width = null }, (typeof(InvalidOperationException), false)); - InvokeFiltersAndVerify(filters, new Product { UnitPrice = 4.1m }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { Width = 5.4f }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { UnitPrice = 5.9m }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { Width = 4.4f }, (false, false)); + } - [Fact] - public void MathFunctions_MathCeilingDouble() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "ceiling(Weight) eq 5", - "$it => ($it.Weight.Value.Ceiling() == 5)", - NotTesting); + [Fact] + public void MathFunctions_MathCeilingDecimal() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "ceiling(UnitPrice) eq 5", + "$it => ($it.UnitPrice.Value.Ceiling() == 5)", + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { Weight = null }, (typeof(InvalidOperationException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { UnitPrice = null }, (typeof(InvalidOperationException), false)); - InvokeFiltersAndVerify(filters, new Product { Weight = 4.1d }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { UnitPrice = 4.1m }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { Weight = 5.9d }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { UnitPrice = 5.9m }, (false, false)); + } - [Fact] - public void MathFunctions_MathCeilingFloat() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "ceiling(Width) eq 5", - "$it => (Convert($it.Width).Value.Ceiling() == 5)", - NotTesting); + [Fact] + public void MathFunctions_MathCeilingDouble() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "ceiling(Weight) eq 5", + "$it => ($it.Weight.Value.Ceiling() == 5)", + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndThrows(filters, new Product { Width = null }, (typeof(InvalidOperationException), false)); + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { Weight = null }, (typeof(InvalidOperationException), false)); - InvokeFiltersAndVerify(filters, new Product { Width = 4.1f }, (true, true)); + InvokeFiltersAndVerify(filters, new Product { Weight = 4.1d }, (true, true)); - InvokeFiltersAndVerify(filters, new Product { Width = 5.9f }, (false, false)); - } + InvokeFiltersAndVerify(filters, new Product { Weight = 5.9d }, (false, false)); + } - [Theory] - [InlineData("floor(FloatProp) eq floor(FloatProp)")] - [InlineData("round(FloatProp) eq round(FloatProp)")] - [InlineData("ceiling(FloatProp) eq ceiling(FloatProp)")] - [InlineData("floor(DoubleProp) eq floor(DoubleProp)")] - [InlineData("round(DoubleProp) eq round(DoubleProp)")] - [InlineData("ceiling(DoubleProp) eq ceiling(DoubleProp)")] - [InlineData("floor(DecimalProp) eq floor(DecimalProp)")] - [InlineData("round(DecimalProp) eq round(DecimalProp)")] - [InlineData("ceiling(DecimalProp) eq ceiling(DecimalProp)")] - public void MathFunctions_VariousTypes(string filter) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify(filter); + [Fact] + public void MathFunctions_MathCeilingFloat() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "ceiling(Width) eq 5", + "$it => (Convert($it.Width).Value.Ceiling() == 5)", + NotTesting); - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, new DataTypes(), (true, true)); - } - #endregion + // Arrange & Act & Assert + InvokeFiltersAndThrows(filters, new Product { Width = null }, (typeof(InvalidOperationException), false)); - #region Custom Functions - [Fact] - public void CustomMethod_InstanceMethodOfDeclaringType() - { - // Arrange - FunctionSignatureWithReturnType padrightStringEdmFunction = - new FunctionSignatureWithReturnType( - EdmCoreModel.Instance.GetString(true), - EdmCoreModel.Instance.GetString(true), - EdmCoreModel.Instance.GetInt32(false)); + InvokeFiltersAndVerify(filters, new Product { Width = 4.1f }, (true, true)); - MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - const string padrightMethodName = "padright"; - try - { - const string productName = "Abcd"; - const int totalWidth = 5; - const string expectedProductName = "Abcd "; + InvokeFiltersAndVerify(filters, new Product { Width = 5.9f }, (false, false)); + } - // Add the custom function - // Act & Assert - CustomUriFunctions.AddCustomUriFunction(padrightMethodName, padrightStringEdmFunction); - UriFunctionsBinder.BindUriFunctionName(padrightMethodName, padRightStringMethodInfo); + [Theory] + [InlineData("floor(FloatProp) eq floor(FloatProp)")] + [InlineData("round(FloatProp) eq round(FloatProp)")] + [InlineData("ceiling(FloatProp) eq ceiling(FloatProp)")] + [InlineData("floor(DoubleProp) eq floor(DoubleProp)")] + [InlineData("round(DoubleProp) eq round(DoubleProp)")] + [InlineData("ceiling(DoubleProp) eq ceiling(DoubleProp)")] + [InlineData("floor(DecimalProp) eq floor(DecimalProp)")] + [InlineData("round(DecimalProp) eq round(DecimalProp)")] + [InlineData("ceiling(DecimalProp) eq ceiling(DecimalProp)")] + public void MathFunctions_VariousTypes(string filter) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify(filter); - string filter = string.Format("padright(ProductName, {0}) eq '{1}'", totalWidth, expectedProductName); - var filters = BindFilterAndVerify(filter); + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, new DataTypes(), (true, true)); + } + #endregion - InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (true, true)); - } - finally - { - Assert.True(CustomUriFunctions.RemoveCustomUriFunction(padrightMethodName)); - Assert.True(UriFunctionsBinder.UnbindUriFunctionName(padrightMethodName, padRightStringMethodInfo)); - } - } + #region Custom Functions + [Fact] + public void CustomMethod_InstanceMethodOfDeclaringType() + { + // Arrange + FunctionSignatureWithReturnType padrightStringEdmFunction = + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetString(true), + EdmCoreModel.Instance.GetString(true), + EdmCoreModel.Instance.GetInt32(false)); - [Fact] - public void CustomMethod_InstanceMethodNotOfDeclaringType() + MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); + const string padrightMethodName = "padright"; + try { - // Arrange - FunctionSignatureWithReturnType padrightStringEdmFunction = new FunctionSignatureWithReturnType( - EdmCoreModel.Instance.GetString(true), - EdmCoreModel.Instance.GetString(true), - EdmCoreModel.Instance.GetInt32(false)); - - MethodInfo padRightStringMethodInfo = typeof(FilterBinderTests).GetMethod("PadRightInstance", BindingFlags.NonPublic | BindingFlags.Instance); - - const string padrightMethodName = "padright"; - try - { - const int totalWidth = 5; - const string expectedProductName = "Abcd "; + const string productName = "Abcd"; + const int totalWidth = 5; + const string expectedProductName = "Abcd "; - // Add the custom function - // Act & Assert - CustomUriFunctions.AddCustomUriFunction(padrightMethodName, padrightStringEdmFunction); - UriFunctionsBinder.BindUriFunctionName(padrightMethodName, padRightStringMethodInfo); + // Add the custom function + // Act & Assert + CustomUriFunctions.AddCustomUriFunction(padrightMethodName, padrightStringEdmFunction); + UriFunctionsBinder.BindUriFunctionName(padrightMethodName, padRightStringMethodInfo); - string filter = string.Format("padright(ProductName, {0}) eq '{1}'", totalWidth, expectedProductName); + string filter = string.Format("padright(ProductName, {0}) eq '{1}'", totalWidth, expectedProductName); + var filters = BindFilterAndVerify(filter); - Action filterToExpression = () => BindFilterAndVerify(filter); - ExceptionAssert.Throws(typeof(NotImplementedException),filterToExpression); - } - finally - { - Assert.True(CustomUriFunctions.RemoveCustomUriFunction(padrightMethodName)); - Assert.True(UriFunctionsBinder.UnbindUriFunctionName(padrightMethodName, padRightStringMethodInfo)); - } + InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (true, true)); } - - [Fact] - public void CustomMethod_StaticExtensionMethod() + finally { - // Arrange - FunctionSignatureWithReturnType padrightStringEdmFunction = new FunctionSignatureWithReturnType( + Assert.True(CustomUriFunctions.RemoveCustomUriFunction(padrightMethodName)); + Assert.True(UriFunctionsBinder.UnbindUriFunctionName(padrightMethodName, padRightStringMethodInfo)); + } + } + + [Fact] + public void CustomMethod_InstanceMethodNotOfDeclaringType() + { + // Arrange + FunctionSignatureWithReturnType padrightStringEdmFunction = new FunctionSignatureWithReturnType( EdmCoreModel.Instance.GetString(true), EdmCoreModel.Instance.GetString(true), EdmCoreModel.Instance.GetInt32(false)); - MethodInfo padRightStringMethodInfo = typeof(StringExtender).GetMethod("PadRightExStatic", BindingFlags.Public | BindingFlags.Static); + MethodInfo padRightStringMethodInfo = typeof(FilterBinderTests).GetMethod("PadRightInstance", BindingFlags.NonPublic | BindingFlags.Instance); - const string padrightMethodName = "padright"; - try - { - const string productName = "Abcd"; - const int totalWidth = 5; - const string expectedProductName = "Abcd "; + const string padrightMethodName = "padright"; + try + { + const int totalWidth = 5; + const string expectedProductName = "Abcd "; - // Add the custom function - // Act & Assert - CustomUriFunctions.AddCustomUriFunction(padrightMethodName, padrightStringEdmFunction); - UriFunctionsBinder.BindUriFunctionName(padrightMethodName, padRightStringMethodInfo); + // Add the custom function + // Act & Assert + CustomUriFunctions.AddCustomUriFunction(padrightMethodName, padrightStringEdmFunction); + UriFunctionsBinder.BindUriFunctionName(padrightMethodName, padRightStringMethodInfo); - string filter = String.Format("padright(ProductName, {0}) eq '{1}'", totalWidth, expectedProductName); - var filters = BindFilterAndVerify(filter); + string filter = string.Format("padright(ProductName, {0}) eq '{1}'", totalWidth, expectedProductName); - InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (true, true)); - } - finally - { - Assert.True(CustomUriFunctions.RemoveCustomUriFunction(padrightMethodName)); - Assert.True(UriFunctionsBinder.UnbindUriFunctionName(padrightMethodName, padRightStringMethodInfo)); - } + Action filterToExpression = () => BindFilterAndVerify(filter); + ExceptionAssert.Throws(typeof(NotImplementedException),filterToExpression); } - - [Fact] - public void CustomMethod_StaticMethodNotOfDeclaringType() + finally { - // Arrange - FunctionSignatureWithReturnType padrightStringEdmFunction = new FunctionSignatureWithReturnType( + Assert.True(CustomUriFunctions.RemoveCustomUriFunction(padrightMethodName)); + Assert.True(UriFunctionsBinder.UnbindUriFunctionName(padrightMethodName, padRightStringMethodInfo)); + } + } + + [Fact] + public void CustomMethod_StaticExtensionMethod() + { + // Arrange + FunctionSignatureWithReturnType padrightStringEdmFunction = new FunctionSignatureWithReturnType( EdmCoreModel.Instance.GetString(true), EdmCoreModel.Instance.GetString(true), EdmCoreModel.Instance.GetInt32(false)); - MethodInfo padRightStringMethodInfo = typeof(FilterBinderTests).GetMethod("PadRightStatic", BindingFlags.NonPublic | BindingFlags.Static); + MethodInfo padRightStringMethodInfo = typeof(StringExtender).GetMethod("PadRightExStatic", BindingFlags.Public | BindingFlags.Static); - const string padrightMethodName = "padright"; - try - { - const string productName = "Abcd"; - const int totalWidth = 5; - const string expectedProductName = "Abcd "; + const string padrightMethodName = "padright"; + try + { + const string productName = "Abcd"; + const int totalWidth = 5; + const string expectedProductName = "Abcd "; - // Add the custom function - // Act & Assert - CustomUriFunctions.AddCustomUriFunction(padrightMethodName, padrightStringEdmFunction); - UriFunctionsBinder.BindUriFunctionName(padrightMethodName, padRightStringMethodInfo); + // Add the custom function + // Act & Assert + CustomUriFunctions.AddCustomUriFunction(padrightMethodName, padrightStringEdmFunction); + UriFunctionsBinder.BindUriFunctionName(padrightMethodName, padRightStringMethodInfo); - string filter = String.Format("padright(ProductName, {0}) eq '{1}'", totalWidth, expectedProductName); - var filters = BindFilterAndVerify(filter); + string filter = String.Format("padright(ProductName, {0}) eq '{1}'", totalWidth, expectedProductName); + var filters = BindFilterAndVerify(filter); - InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (true, true)); - } - finally - { - Assert.True(CustomUriFunctions.RemoveCustomUriFunction(padrightMethodName)); - Assert.True(UriFunctionsBinder.UnbindUriFunctionName(padrightMethodName, padRightStringMethodInfo)); - } + InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (true, true)); } - - [Fact] - public void CustomMethod_AddSignatureAndBindFunctionWithShortcut() + finally { - // Arrange - FunctionSignatureWithReturnType padrightStringEdmFunction = - new FunctionSignatureWithReturnType( - EdmCoreModel.Instance.GetString(true), - EdmCoreModel.Instance.GetString(true), - EdmCoreModel.Instance.GetInt32(false)); + Assert.True(CustomUriFunctions.RemoveCustomUriFunction(padrightMethodName)); + Assert.True(UriFunctionsBinder.UnbindUriFunctionName(padrightMethodName, padRightStringMethodInfo)); + } + } - MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - const string padrightMethodName = "padright"; - try - { - const string productName = "Abcd"; - const int totalWidth = 5; - const string expectedProductName = "Abcd "; + [Fact] + public void CustomMethod_StaticMethodNotOfDeclaringType() + { + // Arrange + FunctionSignatureWithReturnType padrightStringEdmFunction = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetString(true), + EdmCoreModel.Instance.GetString(true), + EdmCoreModel.Instance.GetInt32(false)); - // Add the custom function - // Act & Assert - ODataUriFunctions.AddCustomUriFunction(padrightMethodName, padrightStringEdmFunction, padRightStringMethodInfo); + MethodInfo padRightStringMethodInfo = typeof(FilterBinderTests).GetMethod("PadRightStatic", BindingFlags.NonPublic | BindingFlags.Static); - string filter = String.Format("padright(ProductName, {0}) eq '{1}'", totalWidth, expectedProductName); - var filters = BindFilterAndVerify(filter); + const string padrightMethodName = "padright"; + try + { + const string productName = "Abcd"; + const int totalWidth = 5; + const string expectedProductName = "Abcd "; - InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (true, true)); - } - finally - { - Assert.True(CustomUriFunctions.RemoveCustomUriFunction(padrightMethodName)); - Assert.True(UriFunctionsBinder.UnbindUriFunctionName(padrightMethodName, padRightStringMethodInfo)); - } - } - #endregion + // Add the custom function + // Act & Assert + CustomUriFunctions.AddCustomUriFunction(padrightMethodName, padrightStringEdmFunction); + UriFunctionsBinder.BindUriFunctionName(padrightMethodName, padRightStringMethodInfo); - #region Data Types - [Fact] - public void GuidExpression() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "GuidProp eq 0EFDAECF-A9F0-42F3-A384-1295917AF95E", - "$it => ($it.GuidProp == 0efdaecf-a9f0-42f3-a384-1295917af95e)"); - - // Arrange & Act & Assert - verify case insensitivity ? - BindFilterAndVerify( - "GuidProp eq 0EFDAECF-A9F0-42F3-A384-1295917AF95E", - "$it => ($it.GuidProp == 0efdaecf-a9f0-42f3-a384-1295917af95e)"); - } + string filter = String.Format("padright(ProductName, {0}) eq '{1}'", totalWidth, expectedProductName); + var filters = BindFilterAndVerify(filter); - [Theory] - [InlineData("DateTimeProp eq 2000-12-12T12:00:00Z", "$it => ($it.DateTimeProp == {0})")] - [InlineData("DateTimeProp lt 2000-12-12T12:00:00Z", "$it => ($it.DateTimeProp < {0})")] - // TODO: [InlineData("DateTimeProp ge datetime'2000-12-12T12:00'", "$it => ($it.DateTimeProp >= {0})")] (uriparser fails on optional seconds) - public void DateTimeExpression(string clause, string expectedExpression) + InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (true, true)); + } + finally { - // Arrange & Act & Assert - var dateTime = new DateTimeOffset(new DateTime(2000, 12, 12, 12, 0, 0), TimeSpan.Zero); - BindFilterAndVerify( - clause, - string.Format(CultureInfo.InvariantCulture, expectedExpression, dateTime)); + Assert.True(CustomUriFunctions.RemoveCustomUriFunction(padrightMethodName)); + Assert.True(UriFunctionsBinder.UnbindUriFunctionName(padrightMethodName, padRightStringMethodInfo)); } + } - [Theory] - [InlineData("DateTimeOffsetProp eq datetimeoffset'2002-10-10T17:00:00Z'", "$it => ($it.DateTimeOffsetProp == {0})", 0)] - [InlineData("DateTimeOffsetProp ge datetimeoffset'2002-10-10T17:00:00Z'", "$it => ($it.DateTimeOffsetProp >= {0})", 0)] - [InlineData("DateTimeOffsetProp le datetimeoffset'2002-10-10T17:00:00-07:00'", "$it => ($it.DateTimeOffsetProp <= {0})", -7)] - [InlineData("DateTimeOffsetProp eq datetimeoffset'2002-10-10T17:00:00-0600'", "$it => ($it.DateTimeOffsetProp == {0})", -6)] - [InlineData("DateTimeOffsetProp lt datetimeoffset'2002-10-10T17:00:00-05'", "$it => ($it.DateTimeOffsetProp < {0})", -5)] - [InlineData("DateTimeOffsetProp ne datetimeoffset'2002-10-10T17:00:00%2B09:30'", "$it => ($it.DateTimeOffsetProp != {0})", 9.5)] - [InlineData("DateTimeOffsetProp gt datetimeoffset'2002-10-10T17:00:00%2B0545'", "$it => ($it.DateTimeOffsetProp > {0})", 5.75)] - public void DateTimeOffsetExpression(string clause, string expectedExpression, double offsetHours) + [Fact] + public void CustomMethod_AddSignatureAndBindFunctionWithShortcut() + { + // Arrange + FunctionSignatureWithReturnType padrightStringEdmFunction = + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetString(true), + EdmCoreModel.Instance.GetString(true), + EdmCoreModel.Instance.GetInt32(false)); + + MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); + const string padrightMethodName = "padright"; + try { - // Arrange & Act & Assert - var dateTimeOffset = new DateTimeOffset(2002, 10, 10, 17, 0, 0, TimeSpan.FromHours(offsetHours)); + const string productName = "Abcd"; + const int totalWidth = 5; + const string expectedProductName = "Abcd "; - ExceptionAssert.Throws( - () => BindFilterAndVerify( - clause, - string.Format(CultureInfo.InvariantCulture, expectedExpression, dateTimeOffset))); - } + // Add the custom function + // Act & Assert + ODataUriFunctions.AddCustomUriFunction(padrightMethodName, padrightStringEdmFunction, padRightStringMethodInfo); + + string filter = String.Format("padright(ProductName, {0}) eq '{1}'", totalWidth, expectedProductName); + var filters = BindFilterAndVerify(filter); - [Fact] - public void IntegerLiteralSuffix() + InvokeFiltersAndVerify(filters, new Product { ProductName = productName }, (true, true)); + } + finally { - // Arrange & Act & Assert - long L - BindFilterAndVerify( - "LongProp lt 987654321L and LongProp gt 123456789l", - "$it => (($it.LongProp < 987654321) AndAlso ($it.LongProp > 123456789))"); - - BindFilterAndVerify( - "LongProp lt -987654321L and LongProp gt -123456789l", - "$it => (($it.LongProp < -987654321) AndAlso ($it.LongProp > -123456789))"); + Assert.True(CustomUriFunctions.RemoveCustomUriFunction(padrightMethodName)); + Assert.True(UriFunctionsBinder.UnbindUriFunctionName(padrightMethodName, padRightStringMethodInfo)); } + } + #endregion - [Fact] - public void EnumInExpression() - { - // Arrange & Act & Assert - var result = BindFilterAndVerify( - "SimpleEnumProp in ('First', 'Second')", - "$it => System.Collections.Generic.List`1[Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum].Contains($it.SimpleEnumProp)"); + #region Data Types + [Fact] + public void GuidExpression() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "GuidProp eq 0EFDAECF-A9F0-42F3-A384-1295917AF95E", + "$it => ($it.GuidProp == 0efdaecf-a9f0-42f3-a384-1295917af95e)"); + + // Arrange & Act & Assert - verify case insensitivity ? + BindFilterAndVerify( + "GuidProp eq 0EFDAECF-A9F0-42F3-A384-1295917AF95E", + "$it => ($it.GuidProp == 0efdaecf-a9f0-42f3-a384-1295917af95e)"); + } - Expression> expression = result.Item2 as Expression>; + [Theory] + [InlineData("DateTimeProp eq 2000-12-12T12:00:00Z", "$it => ($it.DateTimeProp == {0})")] + [InlineData("DateTimeProp lt 2000-12-12T12:00:00Z", "$it => ($it.DateTimeProp < {0})")] + // TODO: [InlineData("DateTimeProp ge datetime'2000-12-12T12:00'", "$it => ($it.DateTimeProp >= {0})")] (uriparser fails on optional seconds) + public void DateTimeExpression(string clause, string expectedExpression) + { + // Arrange & Act & Assert + var dateTime = new DateTimeOffset(new DateTime(2000, 12, 12, 12, 0, 0), TimeSpan.Zero); + BindFilterAndVerify( + clause, + string.Format(CultureInfo.InvariantCulture, expectedExpression, dateTime)); + } - var memberAccess = (MemberExpression)((MethodCallExpression)expression.Body).Arguments[0]; - var values = (IList)ExpressionBinderHelper.ExtractParameterizedConstant(memberAccess); - Assert.Equal(new[] {SimpleEnum.First, SimpleEnum.Second}, values); - } + [Theory] + [InlineData("DateTimeOffsetProp eq datetimeoffset'2002-10-10T17:00:00Z'", "$it => ($it.DateTimeOffsetProp == {0})", 0)] + [InlineData("DateTimeOffsetProp ge datetimeoffset'2002-10-10T17:00:00Z'", "$it => ($it.DateTimeOffsetProp >= {0})", 0)] + [InlineData("DateTimeOffsetProp le datetimeoffset'2002-10-10T17:00:00-07:00'", "$it => ($it.DateTimeOffsetProp <= {0})", -7)] + [InlineData("DateTimeOffsetProp eq datetimeoffset'2002-10-10T17:00:00-0600'", "$it => ($it.DateTimeOffsetProp == {0})", -6)] + [InlineData("DateTimeOffsetProp lt datetimeoffset'2002-10-10T17:00:00-05'", "$it => ($it.DateTimeOffsetProp < {0})", -5)] + [InlineData("DateTimeOffsetProp ne datetimeoffset'2002-10-10T17:00:00%2B09:30'", "$it => ($it.DateTimeOffsetProp != {0})", 9.5)] + [InlineData("DateTimeOffsetProp gt datetimeoffset'2002-10-10T17:00:00%2B0545'", "$it => ($it.DateTimeOffsetProp > {0})", 5.75)] + public void DateTimeOffsetExpression(string clause, string expectedExpression, double offsetHours) + { + // Arrange & Act & Assert + var dateTimeOffset = new DateTimeOffset(2002, 10, 10, 17, 0, 0, TimeSpan.FromHours(offsetHours)); - [Fact] - public void EnumInExpression_WithNullValue_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.Throws( - () => BindFilterAndVerify("SimpleEnumProp in ('First', null)"), - "A null value was found with the expected type 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum[Nullable=False]'. The expected type 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum[Nullable=False]' does not allow null values."); - } + ExceptionAssert.Throws( + () => BindFilterAndVerify( + clause, + string.Format(CultureInfo.InvariantCulture, expectedExpression, dateTimeOffset))); + } - [Fact] - public void EnumInExpression_NullableEnum_WithNullable() - { - // Arrange & Act & Assert - var result = BindFilterAndVerify( - "NullableSimpleEnumProp in ('First', 'Second')", - "$it => System.Collections.Generic.List`1[System.Nullable`1[Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum]].Contains($it.NullableSimpleEnumProp)"); - Expression> expression = result.Item2 as Expression>; - - var memberAccess = (MemberExpression)((MethodCallExpression)expression.Body).Arguments[0]; - var values = (IList)ExpressionBinderHelper.ExtractParameterizedConstant(memberAccess); - Assert.Equal(new SimpleEnum?[] {SimpleEnum.First, SimpleEnum.Second}, values); - } - - [Fact] - public void EnumInExpression_NullableEnum_WithNullValue() - { - // Arrange & Act & Assert - var result = BindFilterAndVerify( - "NullableSimpleEnumProp in ('First', null)", - "$it => System.Collections.Generic.List`1[System.Nullable`1[Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum]].Contains($it.NullableSimpleEnumProp)"); - Expression> expression = result.Item2 as Expression>; - - var memberAccess = (MemberExpression)((MethodCallExpression)expression.Body).Arguments[0]; - var values = (IList)ExpressionBinderHelper.ExtractParameterizedConstant(memberAccess); - Assert.Equal(new SimpleEnum?[] {SimpleEnum.First, null}, values); - } + [Fact] + public void IntegerLiteralSuffix() + { + // Arrange & Act & Assert - long L + BindFilterAndVerify( + "LongProp lt 987654321L and LongProp gt 123456789l", + "$it => (($it.LongProp < 987654321) AndAlso ($it.LongProp > 123456789))"); + + BindFilterAndVerify( + "LongProp lt -987654321L and LongProp gt -123456789l", + "$it => (($it.LongProp < -987654321) AndAlso ($it.LongProp > -123456789))"); + } - [Fact] - public void RealLiteralSuffixes() - { - // Arrange & Act & Assert - Float F - BindFilterAndVerify( - "FloatProp lt 4321.56F and FloatProp gt 1234.56f", - String.Format(CultureInfo.InvariantCulture, "$it => (($it.FloatProp < {0:0.00}) AndAlso ($it.FloatProp > {1:0.00}))", 4321.56, 1234.56)); - - // Arrange & Act & Assert - Decimal M - BindFilterAndVerify( - "DecimalProp lt 4321.56M and DecimalProp gt 1234.56m", - String.Format(CultureInfo.InvariantCulture, "$it => (($it.DecimalProp < {0:0.00}) AndAlso ($it.DecimalProp > {1:0.00}))", 4321.56, 1234.56)); - } + [Fact] + public void EnumInExpression() + { + // Arrange & Act & Assert + var result = BindFilterAndVerify( + "SimpleEnumProp in ('First', 'Second')", + "$it => System.Collections.Generic.List`1[Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum].Contains($it.SimpleEnumProp)"); - [Theory] - [InlineData("'hello,world'", "hello,world")] - [InlineData("'''hello,world'", "'hello,world")] - [InlineData("'hello,world'''", "hello,world'")] - [InlineData("'hello,''wor''ld'", "hello,'wor'ld")] - [InlineData("'hello,''''''world'", "hello,'''world")] - [InlineData("'\"hello,world\"'", "\"hello,world\"")] - [InlineData("'\"hello,world'", "\"hello,world")] - [InlineData("'hello,world\"'", "hello,world\"")] - [InlineData("'hello,\"world'", "hello,\"world")] - [InlineData("'México D.F.'", "México D.F.")] - [InlineData("'æææøøøååå'", "æææøøøååå")] - [InlineData("'いくつかのテキスト'", "いくつかのテキスト")] - public void StringLiterals(string literal, string expected) - { - // Arrange & Act & Assert - BindFilterAndVerify( - "ProductName eq " + literal, - string.Format("$it => ($it.ProductName == \"{0}\")", expected)); - } + Expression> expression = result.Item2 as Expression>; - [Theory] - [InlineData('$')] - [InlineData('&')] - [InlineData('+')] - [InlineData(',')] - [InlineData('/')] - [InlineData(':')] - [InlineData(';')] - [InlineData('=')] - [InlineData('?')] - [InlineData('@')] - [InlineData(' ')] - [InlineData('<')] - [InlineData('>')] - [InlineData('#')] - [InlineData('%')] - [InlineData('{')] - [InlineData('}')] - [InlineData('|')] - [InlineData('\\')] - [InlineData('^')] - [InlineData('~')] - [InlineData('[')] - [InlineData(']')] - [InlineData('`')] - public void SpecialCharactersInStringLiteral(char c) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "ProductName eq '" + c + "'", - String.Format("$it => ($it.ProductName == \"{0}\")", c)); + var memberAccess = (MemberExpression)((MethodCallExpression)expression.Body).Arguments[0]; + var values = (IList)ExpressionBinderHelper.ExtractParameterizedConstant(memberAccess); + Assert.Equal(new[] {SimpleEnum.First, SimpleEnum.Second}, values); + } - InvokeFiltersAndVerify(filters, new Product { ProductName = c.ToString() }, (true, true)); - } - #endregion + [Fact] + public void EnumInExpression_WithNullValue_Throws() + { + // Arrange & Act & Assert + ExceptionAssert.Throws( + () => BindFilterAndVerify("SimpleEnumProp in ('First', null)"), + "A null value was found with the expected type 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum[Nullable=False]'. The expected type 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum[Nullable=False]' does not allow null values."); + } - #region Casts - [Fact] - public void NSCast_OnEnumerableEntityCollection_GeneratesExpression_WithOfTypeOnEnumerable() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "Category/EnumerableProducts/Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/any(p: p/ProductName eq 'ProductName')", - "$it => $it.Category.EnumerableProducts.OfType().Any(p => (p.ProductName == \"ProductName\"))", - NotTesting); - - Assert.NotNull(filters.Item1); - Assert.NotNull(filters.Item2); - } + [Fact] + public void EnumInExpression_NullableEnum_WithNullable() + { + // Arrange & Act & Assert + var result = BindFilterAndVerify( + "NullableSimpleEnumProp in ('First', 'Second')", + "$it => System.Collections.Generic.List`1[System.Nullable`1[Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum]].Contains($it.NullableSimpleEnumProp)"); + Expression> expression = result.Item2 as Expression>; + + var memberAccess = (MemberExpression)((MethodCallExpression)expression.Body).Arguments[0]; + var values = (IList)ExpressionBinderHelper.ExtractParameterizedConstant(memberAccess); + Assert.Equal(new SimpleEnum?[] {SimpleEnum.First, SimpleEnum.Second}, values); + } + + [Fact] + public void EnumInExpression_NullableEnum_WithNullValue() + { + // Arrange & Act & Assert + var result = BindFilterAndVerify( + "NullableSimpleEnumProp in ('First', null)", + "$it => System.Collections.Generic.List`1[System.Nullable`1[Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum]].Contains($it.NullableSimpleEnumProp)"); + Expression> expression = result.Item2 as Expression>; + + var memberAccess = (MemberExpression)((MethodCallExpression)expression.Body).Arguments[0]; + var values = (IList)ExpressionBinderHelper.ExtractParameterizedConstant(memberAccess); + Assert.Equal(new SimpleEnum?[] {SimpleEnum.First, null}, values); + } - [Fact] - public void NSCast_OnQueryableEntityCollection_GeneratesExpression_WithOfTypeOnQueryable() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "Category/QueryableProducts/Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/any(p: p/ProductName eq 'ProductName')", - "$it => $it.Category.QueryableProducts.OfType().Any(p => (p.ProductName == \"ProductName\"))", - NotTesting); - } + [Fact] + public void RealLiteralSuffixes() + { + // Arrange & Act & Assert - Float F + BindFilterAndVerify( + "FloatProp lt 4321.56F and FloatProp gt 1234.56f", + String.Format(CultureInfo.InvariantCulture, "$it => (($it.FloatProp < {0:0.00}) AndAlso ($it.FloatProp > {1:0.00}))", 4321.56, 1234.56)); + + // Arrange & Act & Assert - Decimal M + BindFilterAndVerify( + "DecimalProp lt 4321.56M and DecimalProp gt 1234.56m", + String.Format(CultureInfo.InvariantCulture, "$it => (($it.DecimalProp < {0:0.00}) AndAlso ($it.DecimalProp > {1:0.00}))", 4321.56, 1234.56)); + } - [Fact] - public void NSCast_OnEntityCollection_CanAccessDerivedInstanceProperty() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "Category/Products/Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/any(p: p/DerivedProductName eq 'DerivedProductName')"); - - InvokeFiltersAndVerify( - filters, - new Product { Category = new Category { Products = new Product[] { new DerivedProduct { DerivedProductName = "DerivedProductName" } } } }, - (true, true)); - - InvokeFiltersAndVerify( - filters, - new Product { Category = new Category { Products = new Product[] { new DerivedProduct { DerivedProductName = "NotDerivedProductName" } } } }, - (false, false)); - } + [Theory] + [InlineData("'hello,world'", "hello,world")] + [InlineData("'''hello,world'", "'hello,world")] + [InlineData("'hello,world'''", "hello,world'")] + [InlineData("'hello,''wor''ld'", "hello,'wor'ld")] + [InlineData("'hello,''''''world'", "hello,'''world")] + [InlineData("'\"hello,world\"'", "\"hello,world\"")] + [InlineData("'\"hello,world'", "\"hello,world")] + [InlineData("'hello,world\"'", "hello,world\"")] + [InlineData("'hello,\"world'", "hello,\"world")] + [InlineData("'México D.F.'", "México D.F.")] + [InlineData("'æææøøøååå'", "æææøøøååå")] + [InlineData("'いくつかのテキスト'", "いくつかのテキスト")] + public void StringLiterals(string literal, string expected) + { + // Arrange & Act & Assert + BindFilterAndVerify( + "ProductName eq " + literal, + string.Format("$it => ($it.ProductName == \"{0}\")", expected)); + } - [Fact] - public void NSCast_OnSingleEntity_GeneratesExpression_WithAsOperator() - { - // Arrange & Act & Assert - BindFilterAndVerify( - "Microsoft.AspNetCore.OData.Tests.Models.Product/ProductName eq 'ProductName'", - "$it => (($it As Product).ProductName == \"ProductName\")", - NotTesting); - } + [Theory] + [InlineData('$')] + [InlineData('&')] + [InlineData('+')] + [InlineData(',')] + [InlineData('/')] + [InlineData(':')] + [InlineData(';')] + [InlineData('=')] + [InlineData('?')] + [InlineData('@')] + [InlineData(' ')] + [InlineData('<')] + [InlineData('>')] + [InlineData('#')] + [InlineData('%')] + [InlineData('{')] + [InlineData('}')] + [InlineData('|')] + [InlineData('\\')] + [InlineData('^')] + [InlineData('~')] + [InlineData('[')] + [InlineData(']')] + [InlineData('`')] + public void SpecialCharactersInStringLiteral(char c) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "ProductName eq '" + c + "'", + String.Format("$it => ($it.ProductName == \"{0}\")", c)); - [Theory] - [InlineData("Microsoft.AspNetCore.OData.Tests.Models.Product/ProductName eq 'ProductName'")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/DerivedProductName eq 'DerivedProductName'")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/CategoryID eq 123")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/CategoryID eq 123")] - public void Inheritance_WithDerivedInstance(string filter) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify(filter); + InvokeFiltersAndVerify(filters, new Product { ProductName = c.ToString() }, (true, true)); + } + #endregion - InvokeFiltersAndVerify(filters, - new DerivedProduct { Category = new DerivedCategory { CategoryID = 123 }, ProductName = "ProductName", DerivedProductName = "DerivedProductName" }, - (true, true)); - } + #region Casts + [Fact] + public void NSCast_OnEnumerableEntityCollection_GeneratesExpression_WithOfTypeOnEnumerable() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "Category/EnumerableProducts/Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/any(p: p/ProductName eq 'ProductName')", + "$it => $it.Category.EnumerableProducts.OfType().Any(p => (p.ProductName == \"ProductName\"))", + NotTesting); + + Assert.NotNull(filters.Item1); + Assert.NotNull(filters.Item2); + } - [Theory] - [InlineData("Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/DerivedProductName eq 'ProductName'")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/CategoryID eq 123")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/CategoryID eq 123")] - public void Inheritance_WithBaseInstance(string filter) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify(filter); + [Fact] + public void NSCast_OnQueryableEntityCollection_GeneratesExpression_WithOfTypeOnQueryable() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "Category/QueryableProducts/Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/any(p: p/ProductName eq 'ProductName')", + "$it => $it.Category.QueryableProducts.OfType().Any(p => (p.ProductName == \"ProductName\"))", + NotTesting); + } - InvokeFiltersAndThrows(filters, new Product(), (typeof(NullReferenceException), false)); - } + [Fact] + public void NSCast_OnEntityCollection_CanAccessDerivedInstanceProperty() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "Category/Products/Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/any(p: p/DerivedProductName eq 'DerivedProductName')"); + + InvokeFiltersAndVerify( + filters, + new Product { Category = new Category { Products = new Product[] { new DerivedProduct { DerivedProductName = "DerivedProductName" } } } }, + (true, true)); + + InvokeFiltersAndVerify( + filters, + new Product { Category = new Category { Products = new Product[] { new DerivedProduct { DerivedProductName = "NotDerivedProductName" } } } }, + (false, false)); + } - [Fact] - public void CastToNonDerivedType_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.Throws( - () => BindFilterAndVerify("Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/CategoryID eq 123"), - "Encountered invalid type cast. 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'."); - } + [Fact] + public void NSCast_OnSingleEntity_GeneratesExpression_WithAsOperator() + { + // Arrange & Act & Assert + BindFilterAndVerify( + "Microsoft.AspNetCore.OData.Tests.Models.Product/ProductName eq 'ProductName'", + "$it => (($it As Product).ProductName == \"ProductName\")", + NotTesting); + } - [Theory] - [InlineData("Edm.Int32 eq 123", "A binary operator with incompatible types was detected. Found operand types 'Edm.String' and 'Edm.Int32' for operator kind 'Equal'.")] - [InlineData("ProductName/Edm.String eq 123", "A binary operator with incompatible types was detected. Found operand types 'Edm.String' and 'Edm.Int32' for operator kind 'Equal'.")] - public void CastToNonEntityType_Throws(string filter, string error) - { - // Arrange & Act & Assert - ExceptionAssert.Throws( - () => BindFilterAndVerify(filter), error); - } + [Theory] + [InlineData("Microsoft.AspNetCore.OData.Tests.Models.Product/ProductName eq 'ProductName'")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/DerivedProductName eq 'DerivedProductName'")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/CategoryID eq 123")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/CategoryID eq 123")] + public void Inheritance_WithDerivedInstance(string filter) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify(filter); - [Theory] - [InlineData("Edm.NonExistentType eq 123")] - [InlineData("Category/Edm.NonExistentType eq 123")] - [InlineData("Category/Products/Edm.NonExistentType eq 123")] - public void CastToNonExistantType_Throws(string filter) - { - // Arrange & Act & Assert - ExceptionAssert.Throws( - () => BindFilterAndVerify(filter), - "The child type 'Edm.NonExistentType' in a cast was not an entity type. Casts can only be performed on entity types."); - } - #endregion - - #region cast in query option - [Theory] - [InlineData("cast(null,Edm.Int16) eq null", "$it => (null == null)")] - [InlineData("cast(null,Edm.Int32) eq 123", "$it => (null == Convert(123))")] - [InlineData("cast(null,Edm.Int64) ne 123", "$it => (null != Convert(123))")] - [InlineData("cast(null,Edm.Single) ne 123", "$it => (null != Convert(123))")] - [InlineData("cast(null,Edm.Double) ne 123", "$it => (null != Convert(123))")] - [InlineData("cast(null,Edm.Decimal) ne 123", "$it => (null != Convert(123))")] - [InlineData("cast(null,Edm.Boolean) ne true", "$it => (null != Convert(True))")] - [InlineData("cast(null,Edm.Byte) ne 1", "$it => (null != Convert(1))")] - [InlineData("cast(null,Edm.Guid) eq 00000000-0000-0000-0000-000000000000", "$it => (null == Convert(00000000-0000-0000-0000-000000000000))")] - [InlineData("cast(null,Edm.String) ne '123'", "$it => (null != \"123\")")] - [InlineData("cast(null,Edm.DateTimeOffset) eq 2001-01-01T12:00:00.000+08:00", "$it => (null == Convert(01/01/2001 12:00:00 +08:00))")] - [InlineData("cast(null,Edm.Duration) eq duration'P8DT23H59M59.9999S'", "$it => (null == Convert(8.23:59:59.9999000))")] - [InlineData("cast(null,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq null", "$it => (null == null)")] - [InlineData("cast(null,'Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum') eq null", "$it => (null == null)")] - [InlineData("cast(IntProp,Edm.String) eq '123'", "$it => (Convert($it.IntProp.ToString()) == \"123\")")] - [InlineData("cast(LongProp,Edm.String) eq '123'", "$it => (Convert($it.LongProp.ToString()) == \"123\")")] - [InlineData("cast(SingleProp,Edm.String) eq '123'", "$it => (Convert($it.SingleProp.ToString()) == \"123\")")] - [InlineData("cast(DoubleProp,Edm.String) eq '123'", "$it => (Convert($it.DoubleProp.ToString()) == \"123\")")] - [InlineData("cast(DecimalProp,Edm.String) eq '123'", "$it => (Convert($it.DecimalProp.ToString()) == \"123\")")] - [InlineData("cast(BoolProp,Edm.String) eq '123'", "$it => (Convert($it.BoolProp.ToString()) == \"123\")")] - [InlineData("cast(ByteProp,Edm.String) eq '123'", "$it => (Convert($it.ByteProp.ToString()) == \"123\")")] - [InlineData("cast(GuidProp,Edm.String) eq '123'", "$it => (Convert($it.GuidProp.ToString()) == \"123\")")] - [InlineData("cast(StringProp,Edm.String) eq '123'", "$it => (Convert($it.StringProp) == \"123\")")] - [InlineData("cast(DateTimeOffsetProp,Edm.String) eq '123'", "$it => (Convert($it.DateTimeOffsetProp.ToString()) == \"123\")")] - [InlineData("cast(TimeSpanProp,Edm.String) eq '123'", "$it => (Convert($it.TimeSpanProp.ToString()) == \"123\")")] - [InlineData("cast(SimpleEnumProp,Edm.String) eq '123'", "$it => (Convert(Convert($it.SimpleEnumProp).ToString()) == \"123\")")] - [InlineData("cast(FlagsEnumProp,Edm.String) eq '123'", "$it => (Convert(Convert($it.FlagsEnumProp).ToString()) == \"123\")")] - [InlineData("cast(LongEnumProp,Edm.String) eq '123'", "$it => (Convert(Convert($it.LongEnumProp).ToString()) == \"123\")")] - [InlineData("cast(NullableIntProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableIntProp.HasValue, $it.NullableIntProp.Value.ToString(), null)) == \"123\")")] - [InlineData("cast(NullableLongProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableLongProp.HasValue, $it.NullableLongProp.Value.ToString(), null)) == \"123\")")] - [InlineData("cast(NullableSingleProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableSingleProp.HasValue, $it.NullableSingleProp.Value.ToString(), null)) == \"123\")")] - [InlineData("cast(NullableDoubleProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableDoubleProp.HasValue, $it.NullableDoubleProp.Value.ToString(), null)) == \"123\")")] - [InlineData("cast(NullableDecimalProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableDecimalProp.HasValue, $it.NullableDecimalProp.Value.ToString(), null)) == \"123\")")] - [InlineData("cast(NullableBoolProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableBoolProp.HasValue, $it.NullableBoolProp.Value.ToString(), null)) == \"123\")")] - [InlineData("cast(NullableByteProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableByteProp.HasValue, $it.NullableByteProp.Value.ToString(), null)) == \"123\")")] - [InlineData("cast(NullableGuidProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableGuidProp.HasValue, $it.NullableGuidProp.Value.ToString(), null)) == \"123\")")] - [InlineData("cast(NullableDateTimeOffsetProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableDateTimeOffsetProp.HasValue, $it.NullableDateTimeOffsetProp.Value.ToString(), null)) == \"123\")")] - [InlineData("cast(NullableTimeSpanProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableTimeSpanProp.HasValue, $it.NullableTimeSpanProp.Value.ToString(), null)) == \"123\")")] - [InlineData("cast(NullableSimpleEnumProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableSimpleEnumProp.HasValue, Convert($it.NullableSimpleEnumProp.Value).ToString(), null)) == \"123\")")] - [InlineData("cast(IntProp,Edm.Int64) eq 123", "$it => (Convert($it.IntProp) == 123)")] - [InlineData("cast(NullableLongProp,Edm.Double) eq 1.23", "$it => (Convert($it.NullableLongProp) == Convert(1.23))")] - [InlineData("cast(2147483647,Edm.Int16) ne null", "$it => (Convert(Convert(2147483647)) != null)")] - [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'1',Edm.String) eq '1'", "$it => (Convert(Convert(Second).ToString()) == \"1\")")] - [InlineData("cast(cast(cast(IntProp,Edm.Int64),Edm.Int16),Edm.String) eq '123'", "$it => (Convert(Convert(Convert($it.IntProp)).ToString()) == \"123\")")] - [InlineData("cast('123',Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null", "$it => (Convert(123) != null)")] - public void CastMethod_Succeeds(string filter, string expectedResult) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expectedResult, NotTesting); - } + InvokeFiltersAndVerify(filters, + new DerivedProduct { Category = new DerivedCategory { CategoryID = 123 }, ProductName = "ProductName", DerivedProductName = "DerivedProductName" }, + (true, true)); + } - [Theory] - [InlineData("cast(NoSuchProperty,Edm.Int32) ne null", - "Could not find a property named 'NoSuchProperty' on type 'Microsoft.AspNetCore.OData.Tests.Models.DataTypes'.")] - public void Cast_UndefinedSource_ThrowsODataException(string filter, string errorMessage) - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => BindFilterAndVerify(filter), errorMessage); - } + [Theory] + [InlineData("Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/DerivedProductName eq 'ProductName'")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/CategoryID eq 123")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct/Category/Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/CategoryID eq 123")] + public void Inheritance_WithBaseInstance(string filter) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify(filter); - public static TheoryDataSet CastToUnquotedUndefinedTarget - { - get - { - return new TheoryDataSet - { - { "cast(Edm.DateTime) eq null", "Edm.DateTime" }, - { "cast(Edm.Unknown) eq null", "Edm.Unknown" }, - { "cast(null,Edm.DateTime) eq null", "Edm.DateTime" }, - { "cast(null,Edm.Unknown) eq null", "Edm.Unknown" }, - { "cast('2001-01-01T12:00:00.000',Edm.DateTime) eq null", "Edm.DateTime" }, - { "cast('2001-01-01T12:00:00.000',Edm.Unknown) eq null", "Edm.Unknown" }, - { "cast(DateTimeProp,Edm.DateTime) eq null", "Edm.DateTime" }, - { "cast(DateTimeProp,Edm.Unknown) eq null", "Edm.Unknown" }, - }; - } - } + InvokeFiltersAndThrows(filters, new Product(), (typeof(NullReferenceException), false)); + } - // Exception messages here and in CastQuotedUndefinedTarget_ThrowsODataException should be consistent. - // Worse, this message is incorrect -- casts can be performed on most types but _not_ entity types. - [Theory] - [MemberData(nameof(CastToUnquotedUndefinedTarget))] - public void CastToUnquotedUndefinedTarget_ThrowsODataException(string filter, string typeName) - { - // Arrange - var expectedMessage = string.Format( - "The child type '{0}' in a cast was not an entity type. Casts can only be performed on entity types.", - typeName); + [Fact] + public void CastToNonDerivedType_Throws() + { + // Arrange & Act & Assert + ExceptionAssert.Throws( + () => BindFilterAndVerify("Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/CategoryID eq 123"), + "Encountered invalid type cast. 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'."); + } - // Act & Assert - ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); - } + [Theory] + [InlineData("Edm.Int32 eq 123", "A binary operator with incompatible types was detected. Found operand types 'Edm.String' and 'Edm.Int32' for operator kind 'Equal'.")] + [InlineData("ProductName/Edm.String eq 123", "A binary operator with incompatible types was detected. Found operand types 'Edm.String' and 'Edm.Int32' for operator kind 'Equal'.")] + public void CastToNonEntityType_Throws(string filter, string error) + { + // Arrange & Act & Assert + ExceptionAssert.Throws( + () => BindFilterAndVerify(filter), error); + } + + [Theory] + [InlineData("Edm.NonExistentType eq 123")] + [InlineData("Category/Edm.NonExistentType eq 123")] + [InlineData("Category/Products/Edm.NonExistentType eq 123")] + public void CastToNonExistantType_Throws(string filter) + { + // Arrange & Act & Assert + ExceptionAssert.Throws( + () => BindFilterAndVerify(filter), + "The child type 'Edm.NonExistentType' in a cast was not an entity type. Casts can only be performed on entity types."); + } + #endregion + + #region cast in query option + [Theory] + [InlineData("cast(null,Edm.Int16) eq null", "$it => (null == null)")] + [InlineData("cast(null,Edm.Int32) eq 123", "$it => (null == Convert(123))")] + [InlineData("cast(null,Edm.Int64) ne 123", "$it => (null != Convert(123))")] + [InlineData("cast(null,Edm.Single) ne 123", "$it => (null != Convert(123))")] + [InlineData("cast(null,Edm.Double) ne 123", "$it => (null != Convert(123))")] + [InlineData("cast(null,Edm.Decimal) ne 123", "$it => (null != Convert(123))")] + [InlineData("cast(null,Edm.Boolean) ne true", "$it => (null != Convert(True))")] + [InlineData("cast(null,Edm.Byte) ne 1", "$it => (null != Convert(1))")] + [InlineData("cast(null,Edm.Guid) eq 00000000-0000-0000-0000-000000000000", "$it => (null == Convert(00000000-0000-0000-0000-000000000000))")] + [InlineData("cast(null,Edm.String) ne '123'", "$it => (null != \"123\")")] + [InlineData("cast(null,Edm.DateTimeOffset) eq 2001-01-01T12:00:00.000+08:00", "$it => (null == Convert(01/01/2001 12:00:00 +08:00))")] + [InlineData("cast(null,Edm.Duration) eq duration'P8DT23H59M59.9999S'", "$it => (null == Convert(8.23:59:59.9999000))")] + [InlineData("cast(null,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq null", "$it => (null == null)")] + [InlineData("cast(null,'Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum') eq null", "$it => (null == null)")] + [InlineData("cast(IntProp,Edm.String) eq '123'", "$it => (Convert($it.IntProp.ToString()) == \"123\")")] + [InlineData("cast(LongProp,Edm.String) eq '123'", "$it => (Convert($it.LongProp.ToString()) == \"123\")")] + [InlineData("cast(SingleProp,Edm.String) eq '123'", "$it => (Convert($it.SingleProp.ToString()) == \"123\")")] + [InlineData("cast(DoubleProp,Edm.String) eq '123'", "$it => (Convert($it.DoubleProp.ToString()) == \"123\")")] + [InlineData("cast(DecimalProp,Edm.String) eq '123'", "$it => (Convert($it.DecimalProp.ToString()) == \"123\")")] + [InlineData("cast(BoolProp,Edm.String) eq '123'", "$it => (Convert($it.BoolProp.ToString()) == \"123\")")] + [InlineData("cast(ByteProp,Edm.String) eq '123'", "$it => (Convert($it.ByteProp.ToString()) == \"123\")")] + [InlineData("cast(GuidProp,Edm.String) eq '123'", "$it => (Convert($it.GuidProp.ToString()) == \"123\")")] + [InlineData("cast(StringProp,Edm.String) eq '123'", "$it => (Convert($it.StringProp) == \"123\")")] + [InlineData("cast(DateTimeOffsetProp,Edm.String) eq '123'", "$it => (Convert($it.DateTimeOffsetProp.ToString()) == \"123\")")] + [InlineData("cast(TimeSpanProp,Edm.String) eq '123'", "$it => (Convert($it.TimeSpanProp.ToString()) == \"123\")")] + [InlineData("cast(SimpleEnumProp,Edm.String) eq '123'", "$it => (Convert(Convert($it.SimpleEnumProp).ToString()) == \"123\")")] + [InlineData("cast(FlagsEnumProp,Edm.String) eq '123'", "$it => (Convert(Convert($it.FlagsEnumProp).ToString()) == \"123\")")] + [InlineData("cast(LongEnumProp,Edm.String) eq '123'", "$it => (Convert(Convert($it.LongEnumProp).ToString()) == \"123\")")] + [InlineData("cast(NullableIntProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableIntProp.HasValue, $it.NullableIntProp.Value.ToString(), null)) == \"123\")")] + [InlineData("cast(NullableLongProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableLongProp.HasValue, $it.NullableLongProp.Value.ToString(), null)) == \"123\")")] + [InlineData("cast(NullableSingleProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableSingleProp.HasValue, $it.NullableSingleProp.Value.ToString(), null)) == \"123\")")] + [InlineData("cast(NullableDoubleProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableDoubleProp.HasValue, $it.NullableDoubleProp.Value.ToString(), null)) == \"123\")")] + [InlineData("cast(NullableDecimalProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableDecimalProp.HasValue, $it.NullableDecimalProp.Value.ToString(), null)) == \"123\")")] + [InlineData("cast(NullableBoolProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableBoolProp.HasValue, $it.NullableBoolProp.Value.ToString(), null)) == \"123\")")] + [InlineData("cast(NullableByteProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableByteProp.HasValue, $it.NullableByteProp.Value.ToString(), null)) == \"123\")")] + [InlineData("cast(NullableGuidProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableGuidProp.HasValue, $it.NullableGuidProp.Value.ToString(), null)) == \"123\")")] + [InlineData("cast(NullableDateTimeOffsetProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableDateTimeOffsetProp.HasValue, $it.NullableDateTimeOffsetProp.Value.ToString(), null)) == \"123\")")] + [InlineData("cast(NullableTimeSpanProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableTimeSpanProp.HasValue, $it.NullableTimeSpanProp.Value.ToString(), null)) == \"123\")")] + [InlineData("cast(NullableSimpleEnumProp,Edm.String) eq '123'", "$it => (Convert(IIF($it.NullableSimpleEnumProp.HasValue, Convert($it.NullableSimpleEnumProp.Value).ToString(), null)) == \"123\")")] + [InlineData("cast(IntProp,Edm.Int64) eq 123", "$it => (Convert($it.IntProp) == 123)")] + [InlineData("cast(NullableLongProp,Edm.Double) eq 1.23", "$it => (Convert($it.NullableLongProp) == Convert(1.23))")] + [InlineData("cast(2147483647,Edm.Int16) ne null", "$it => (Convert(Convert(2147483647)) != null)")] + [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'1',Edm.String) eq '1'", "$it => (Convert(Convert(Second).ToString()) == \"1\")")] + [InlineData("cast(cast(cast(IntProp,Edm.Int64),Edm.Int16),Edm.String) eq '123'", "$it => (Convert(Convert(Convert($it.IntProp)).ToString()) == \"123\")")] + [InlineData("cast('123',Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null", "$it => (Convert(123) != null)")] + public void CastMethod_Succeeds(string filter, string expectedResult) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expectedResult, NotTesting); + } + + [Theory] + [InlineData("cast(NoSuchProperty,Edm.Int32) ne null", + "Could not find a property named 'NoSuchProperty' on type 'Microsoft.AspNetCore.OData.Tests.Models.DataTypes'.")] + public void Cast_UndefinedSource_ThrowsODataException(string filter, string errorMessage) + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => BindFilterAndVerify(filter), errorMessage); + } - public static TheoryDataSet CastToQuotedUndefinedTarget + public static TheoryDataSet CastToUnquotedUndefinedTarget + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { "cast('Edm.DateTime') eq null" }, - { "cast('Edm.Unknown') eq null" }, - { "cast(null,'Edm.DateTime') eq null" }, - { "cast(null,'Edm.Unknown') eq null" }, - { "cast('2001-01-01T12:00:00.000','Edm.DateTime') eq null" }, - { "cast('','Edm.Unknown') eq null" }, - { "cast(DateTimeProp,'Edm.DateTime') eq null" }, - { "cast(IntProp,'Edm.Unknown') eq null" }, - }; - } + { "cast(Edm.DateTime) eq null", "Edm.DateTime" }, + { "cast(Edm.Unknown) eq null", "Edm.Unknown" }, + { "cast(null,Edm.DateTime) eq null", "Edm.DateTime" }, + { "cast(null,Edm.Unknown) eq null", "Edm.Unknown" }, + { "cast('2001-01-01T12:00:00.000',Edm.DateTime) eq null", "Edm.DateTime" }, + { "cast('2001-01-01T12:00:00.000',Edm.Unknown) eq null", "Edm.Unknown" }, + { "cast(DateTimeProp,Edm.DateTime) eq null", "Edm.DateTime" }, + { "cast(DateTimeProp,Edm.Unknown) eq null", "Edm.Unknown" }, + }; } + } - [Theory] - [MemberData(nameof(CastToQuotedUndefinedTarget))] - public void CastToQuotedUndefinedTarget_ThrowsODataException(string filter) - { - // Arrange - var expectedMessage = "Cast or IsOf Function must have a type in its arguments."; + // Exception messages here and in CastQuotedUndefinedTarget_ThrowsODataException should be consistent. + // Worse, this message is incorrect -- casts can be performed on most types but _not_ entity types. + [Theory] + [MemberData(nameof(CastToUnquotedUndefinedTarget))] + public void CastToUnquotedUndefinedTarget_ThrowsODataException(string filter, string typeName) + { + // Arrange + var expectedMessage = string.Format( + "The child type '{0}' in a cast was not an entity type. Casts can only be performed on entity types.", + typeName); - // Act & Assert - ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); - } + // Act & Assert + ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); + } - [Theory] - [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] - [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum) ne null")] - [InlineData("cast(0,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] - [InlineData("cast(0,Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum) ne null")] - [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'0',Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] - [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'0',Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum) ne null")] - [InlineData("cast(SimpleEnumProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] - [InlineData("cast(FlagsEnumProp,Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum) ne null")] - [InlineData("cast(NullableSimpleEnumProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] - [InlineData("cast(IntProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] - [InlineData("cast(DateTimeOffsetProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] - [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'1',Edm.Int32) eq 1")] - [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'1',Edm.Int32) eq 1")] - [InlineData("cast(SimpleEnumProp,Edm.Int32) eq 123")] - [InlineData("cast(FlagsEnumProp,Edm.Int32) eq 123")] - [InlineData("cast(NullableSimpleEnumProp,Edm.Guid) ne null")] - - [InlineData("cast('Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] - [InlineData("cast('Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum') ne null")] - [InlineData("cast(0,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] - [InlineData("cast(0,'Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum') ne null")] - [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'0','Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] - [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'0','Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum') ne null")] - [InlineData("cast(SimpleEnumProp,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] - [InlineData("cast(FlagsEnumProp,'Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum') ne null")] - [InlineData("cast(NullableSimpleEnumProp,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] - [InlineData("cast(IntProp,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] - [InlineData("cast(DateTimeOffsetProp,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] - [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'1','Edm.Int32') eq 1")] - [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'1','Edm.Int32') eq 1")] - [InlineData("cast(SimpleEnumProp,'Edm.Int32') eq 123")] - [InlineData("cast(FlagsEnumProp,'Edm.Int32') eq 123")] - [InlineData("cast(NullableSimpleEnumProp,'Edm.Guid') ne null")] - public void Cast_UnsupportedSourceOrTargetForEnumCast_Throws(string filter) + public static TheoryDataSet CastToQuotedUndefinedTarget + { + get { - // Arrange & Act & Assert - // TODO : 1824 Should not throw exception for invalid enum cast in query option. - ExceptionAssert.Throws(() => BindFilterAndVerify(filter), "Enumeration type value can only be casted to or from string."); + return new TheoryDataSet + { + { "cast('Edm.DateTime') eq null" }, + { "cast('Edm.Unknown') eq null" }, + { "cast(null,'Edm.DateTime') eq null" }, + { "cast(null,'Edm.Unknown') eq null" }, + { "cast('2001-01-01T12:00:00.000','Edm.DateTime') eq null" }, + { "cast('','Edm.Unknown') eq null" }, + { "cast(DateTimeProp,'Edm.DateTime') eq null" }, + { "cast(IntProp,'Edm.Unknown') eq null" }, + }; } + } - [Theory] - [InlineData("cast(IntProp,Edm.DateTimeOffset) eq null")] - [InlineData("cast(ByteProp,Edm.Guid) eq null")] - [InlineData("cast(NullableLongProp,Edm.Duration) eq null")] - [InlineData("cast(StringProp,Edm.Double) eq null")] - [InlineData("cast(StringProp,Edm.Int16) eq null")] - [InlineData("cast(DateTimeOffsetProp,Edm.Int32) eq null")] - [InlineData("cast(NullableGuidProp,Edm.Int64) eq null")] - [InlineData("cast(Edm.Int32) eq null")] - [InlineData("cast($it,Edm.String) eq null")] - [InlineData("cast(ComplexProp,Edm.Double) eq null")] - [InlineData("cast(ComplexProp,Edm.String) eq null")] - [InlineData("cast(StringProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) eq null")] - [InlineData("cast(StringProp,Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum) eq null")] - public void Cast_UnsupportedTarget_ReturnsNull(string filter) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, "$it => (null == null)"); - } + [Theory] + [MemberData(nameof(CastToQuotedUndefinedTarget))] + public void CastToQuotedUndefinedTarget_ThrowsODataException(string filter) + { + // Arrange + var expectedMessage = "Cast or IsOf Function must have a type in its arguments."; - // See OtherFunctions_SomeTwoParameterCasts_ThrowODataException and OtherFunctions_SomeSingleParameterCasts_ThrowODataException - // in FilterQueryValidatorTest. ODL's ODataQueryOptionParser and FunctionCallBinder call the code throwing these exceptions. - [Theory] - [InlineData("cast(null,Microsoft.AspNetCore.OData.Tests.Models.Address) ne null", - "Encountered invalid type cast. " + - "'Microsoft.AspNetCore.OData.Tests.Models.Address' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.DataTypes'.")] - [InlineData("cast(null,Microsoft.AspNetCore.OData.Tests.Models.DataTypes) ne null", - "Cast or IsOf Function must have a type in its arguments.")] - public void Cast_NonPrimitiveTarget_ThrowsODataException(string filter, string expectErrorMessage) - { - // Arrange & Act & Assert - // TODO : 1827 Should not throw when the target type of cast is not primitive or enumeration type. - ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectErrorMessage); - } + // Act & Assert + ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); + } - [Theory] - [InlineData("cast(null,'Edm.Int32') ne null")] - [InlineData("cast(StringProp,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq null")] - [InlineData("cast(IntProp,'Edm.String') eq '123'")] - [InlineData("cast('Microsoft.AspNetCore.OData.Tests.Models.DataTypes') eq null")] - [InlineData("cast($it,'Microsoft.AspNetCore.OData.Tests.Models.DataTypes') eq null")] - public void SingleQuotesOnTypeNameOfCast_WorksForNow(string filter) - { - // Arrange - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - IEdmModel model = builder.GetEdmModel(); - IEdmEntitySet entitySet = model.FindDeclaredEntitySet("Customers"); - IEdmEntityType entityType = entitySet.EntityType; - var parser = new ODataQueryOptionParser(model, entityType, entitySet, - new Dictionary { { "$filter", filter } }); + [Theory] + [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] + [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum) ne null")] + [InlineData("cast(0,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] + [InlineData("cast(0,Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum) ne null")] + [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'0',Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] + [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'0',Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum) ne null")] + [InlineData("cast(SimpleEnumProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] + [InlineData("cast(FlagsEnumProp,Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum) ne null")] + [InlineData("cast(NullableSimpleEnumProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] + [InlineData("cast(IntProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] + [InlineData("cast(DateTimeOffsetProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne null")] + [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'1',Edm.Int32) eq 1")] + [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'1',Edm.Int32) eq 1")] + [InlineData("cast(SimpleEnumProp,Edm.Int32) eq 123")] + [InlineData("cast(FlagsEnumProp,Edm.Int32) eq 123")] + [InlineData("cast(NullableSimpleEnumProp,Edm.Guid) ne null")] + + [InlineData("cast('Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] + [InlineData("cast('Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum') ne null")] + [InlineData("cast(0,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] + [InlineData("cast(0,'Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum') ne null")] + [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'0','Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] + [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'0','Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum') ne null")] + [InlineData("cast(SimpleEnumProp,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] + [InlineData("cast(FlagsEnumProp,'Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum') ne null")] + [InlineData("cast(NullableSimpleEnumProp,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] + [InlineData("cast(IntProp,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] + [InlineData("cast(DateTimeOffsetProp,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') ne null")] + [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'1','Edm.Int32') eq 1")] + [InlineData("cast(Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'1','Edm.Int32') eq 1")] + [InlineData("cast(SimpleEnumProp,'Edm.Int32') eq 123")] + [InlineData("cast(FlagsEnumProp,'Edm.Int32') eq 123")] + [InlineData("cast(NullableSimpleEnumProp,'Edm.Guid') ne null")] + public void Cast_UnsupportedSourceOrTargetForEnumCast_Throws(string filter) + { + // Arrange & Act & Assert + // TODO : 1824 Should not throw exception for invalid enum cast in query option. + ExceptionAssert.Throws(() => BindFilterAndVerify(filter), "Enumeration type value can only be casted to or from string."); + } - // Act & Assert - // TODO : 1927 ODL parser should throw when there are single quotes on type name of cast. - Assert.NotNull(parser.ParseFilter()); - } + [Theory] + [InlineData("cast(IntProp,Edm.DateTimeOffset) eq null")] + [InlineData("cast(ByteProp,Edm.Guid) eq null")] + [InlineData("cast(NullableLongProp,Edm.Duration) eq null")] + [InlineData("cast(StringProp,Edm.Double) eq null")] + [InlineData("cast(StringProp,Edm.Int16) eq null")] + [InlineData("cast(DateTimeOffsetProp,Edm.Int32) eq null")] + [InlineData("cast(NullableGuidProp,Edm.Int64) eq null")] + [InlineData("cast(Edm.Int32) eq null")] + [InlineData("cast($it,Edm.String) eq null")] + [InlineData("cast(ComplexProp,Edm.Double) eq null")] + [InlineData("cast(ComplexProp,Edm.String) eq null")] + [InlineData("cast(StringProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) eq null")] + [InlineData("cast(StringProp,Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum) eq null")] + public void Cast_UnsupportedTarget_ReturnsNull(string filter) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, "$it => (null == null)"); + } - [Fact] - public void SingleQuotesOnEnumTypeNameOfCast_WorksForNow() - { - // Arrange - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - IEdmModel model = builder.GetEdmModel(); - IEdmEntitySet entitySet = model.FindDeclaredEntitySet("Customers"); - IEdmEntityType entityType = entitySet.EntityType; - var parser = new ODataQueryOptionParser(model, entityType, entitySet, - new Dictionary - { - { "$filter", "cast(StringProp,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq null" } - }); - - // Act - // TODO : 1927 ODL parser should throw when there are single quotes on type name of cast. - FilterClause filterClause = parser.ParseFilter(); - - // Assert - Assert.NotNull(filterClause); - var castNode = Assert.IsType(((BinaryOperatorNode)filterClause.Expression).Left); - Assert.Equal("cast", castNode.Name); - Assert.Equal("Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum", ((ConstantNode)castNode.Parameters.Last()).Value); - } + // See OtherFunctions_SomeTwoParameterCasts_ThrowODataException and OtherFunctions_SomeSingleParameterCasts_ThrowODataException + // in FilterQueryValidatorTest. ODL's ODataQueryOptionParser and FunctionCallBinder call the code throwing these exceptions. + [Theory] + [InlineData("cast(null,Microsoft.AspNetCore.OData.Tests.Models.Address) ne null", + "Encountered invalid type cast. " + + "'Microsoft.AspNetCore.OData.Tests.Models.Address' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.DataTypes'.")] + [InlineData("cast(null,Microsoft.AspNetCore.OData.Tests.Models.DataTypes) ne null", + "Cast or IsOf Function must have a type in its arguments.")] + public void Cast_NonPrimitiveTarget_ThrowsODataException(string filter, string expectErrorMessage) + { + // Arrange & Act & Assert + // TODO : 1827 Should not throw when the target type of cast is not primitive or enumeration type. + ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectErrorMessage); + } - public static TheoryDataSet CastToQuotedPrimitiveType - { - get + [Theory] + [InlineData("cast(null,'Edm.Int32') ne null")] + [InlineData("cast(StringProp,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq null")] + [InlineData("cast(IntProp,'Edm.String') eq '123'")] + [InlineData("cast('Microsoft.AspNetCore.OData.Tests.Models.DataTypes') eq null")] + [InlineData("cast($it,'Microsoft.AspNetCore.OData.Tests.Models.DataTypes') eq null")] + public void SingleQuotesOnTypeNameOfCast_WorksForNow(string filter) + { + // Arrange + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + IEdmModel model = builder.GetEdmModel(); + IEdmEntitySet entitySet = model.FindDeclaredEntitySet("Customers"); + IEdmEntityType entityType = entitySet.EntityType; + var parser = new ODataQueryOptionParser(model, entityType, entitySet, + new Dictionary { { "$filter", filter } }); + + // Act & Assert + // TODO : 1927 ODL parser should throw when there are single quotes on type name of cast. + Assert.NotNull(parser.ParseFilter()); + } + + [Fact] + public void SingleQuotesOnEnumTypeNameOfCast_WorksForNow() + { + // Arrange + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + IEdmModel model = builder.GetEdmModel(); + IEdmEntitySet entitySet = model.FindDeclaredEntitySet("Customers"); + IEdmEntityType entityType = entitySet.EntityType; + var parser = new ODataQueryOptionParser(model, entityType, entitySet, + new Dictionary { - return new TheoryDataSet - { - { "cast('Edm.Binary') eq null" }, - { "cast('Edm.Boolean') eq null" }, - { "cast('Edm.Byte') eq null" }, - { "cast('Edm.DateTimeOffset') eq null" }, - { "cast('Edm.Decimal') eq null" }, - { "cast('Edm.Double') eq null" }, - { "cast('Edm.Duration') eq null" }, - { "cast('Edm.Guid') eq null" }, - { "cast('Edm.Int16') eq null" }, - { "cast('Edm.Int32') eq null" }, - { "cast('Edm.Int64') eq null" }, - { "cast('Edm.SByte') eq null" }, - { "cast('Edm.Single') eq null" }, - { "cast('Edm.String') eq null" }, - - { "cast(null,'Edm.Binary') eq null" }, - { "cast(null,'Edm.Boolean') eq null" }, - { "cast(null,'Edm.Byte') eq null" }, - { "cast(null,'Edm.DateTimeOffset') eq null" }, - { "cast(null,'Edm.Decimal') eq null" }, - { "cast(null,'Edm.Double') eq null" }, - { "cast(null,'Edm.Duration') eq null" }, - { "cast(null,'Edm.Guid') eq null" }, - { "cast(null,'Edm.Int16') eq null" }, - { "cast(null,'Edm.Int32') eq null" }, - { "cast(null,'Edm.Int64') eq null" }, - { "cast(null,'Edm.SByte') eq null" }, - { "cast(null,'Edm.Single') eq null" }, - { "cast(null,'Edm.String') eq null" }, - - { "cast(binary'T0RhdGE=','Edm.Binary') eq binary'T0RhdGE='" }, - { "cast(false,'Edm.Boolean') eq false" }, - { "cast(23,'Edm.Byte') eq 23" }, - { "cast(2001-01-01T12:00:00.000+08:00,'Edm.DateTimeOffset') eq 2001-01-01T12:00:00.000+08:00" }, - { "cast(23,'Edm.Decimal') eq 23" }, - { "cast(23,'Edm.Double') eq 23" }, - { "cast(duration'PT12H','Edm.Duration') eq duration'PT12H'" }, - { "cast(00000000-0000-0000-0000-000000000000,'Edm.Guid') eq 00000000-0000-0000-0000-000000000000" }, - { "cast(23,'Edm.Int16') eq 23" }, - { "cast(23,'Edm.Int32') eq 23" }, - { "cast(23,'Edm.Int64') eq 23" }, - { "cast(23,'Edm.SByte') eq 23" }, - { "cast(23,'Edm.Single') eq 23" }, - { "cast('hello','Edm.String') eq 'hello'" }, - - { "cast(ByteArrayProp,'Edm.Binary') eq null" }, - { "cast(BoolProp,'Edm.Boolean') eq true" }, - { "cast(DateTimeOffsetProp,'Edm.DateTimeOffset') eq 2001-01-01T12:00:00.000+08:00" }, - { "cast(DecimalProp,'Edm.Decimal') eq 23" }, - { "cast(DoubleProp,'Edm.Double') eq 23" }, - { "cast(TimeSpanProp,'Edm.Duration') eq duration'PT23H'" }, - { "cast(GuidProp,'Edm.Guid') eq 0EFDAECF-A9F0-42F3-A384-1295917AF95E" }, - { "cast(NullableShortProp,'Edm.Int16') eq 23" }, - { "cast(IntProp,'Edm.Int32') eq 23" }, - { "cast(LongProp,'Edm.Int64') eq 23" }, - { "cast(FloatProp,'Edm.Single') eq 23" }, - { "cast(StringProp,'Edm.String') eq 'hello'" }, - }; - } - } + { "$filter", "cast(StringProp,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq null" } + }); + + // Act + // TODO : 1927 ODL parser should throw when there are single quotes on type name of cast. + FilterClause filterClause = parser.ParseFilter(); + + // Assert + Assert.NotNull(filterClause); + var castNode = Assert.IsType(((BinaryOperatorNode)filterClause.Expression).Left); + Assert.Equal("cast", castNode.Name); + Assert.Equal("Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum", ((ConstantNode)castNode.Parameters.Last()).Value); + } - [Theory] - [MemberData(nameof(CastToQuotedPrimitiveType))] - public void CastToQuotedPrimitiveType_Succeeds(string filter) + public static TheoryDataSet CastToQuotedPrimitiveType + { + get { - // Arrange - var model = new DataTypes + return new TheoryDataSet { - BoolProp = true, - DateTimeOffsetProp = DateTimeOffset.Parse("2001-01-01T12:00:00.000+08:00"), - DecimalProp = 23, - DoubleProp = 23, - GuidProp = Guid.Parse("0EFDAECF-A9F0-42F3-A384-1295917AF95E"), - NullableShortProp = 23, - IntProp = 23, - LongProp = 23, - FloatProp = 23, - StringProp = "hello", - TimeSpanProp = TimeSpan.FromHours(23), + { "cast('Edm.Binary') eq null" }, + { "cast('Edm.Boolean') eq null" }, + { "cast('Edm.Byte') eq null" }, + { "cast('Edm.DateTimeOffset') eq null" }, + { "cast('Edm.Decimal') eq null" }, + { "cast('Edm.Double') eq null" }, + { "cast('Edm.Duration') eq null" }, + { "cast('Edm.Guid') eq null" }, + { "cast('Edm.Int16') eq null" }, + { "cast('Edm.Int32') eq null" }, + { "cast('Edm.Int64') eq null" }, + { "cast('Edm.SByte') eq null" }, + { "cast('Edm.Single') eq null" }, + { "cast('Edm.String') eq null" }, + + { "cast(null,'Edm.Binary') eq null" }, + { "cast(null,'Edm.Boolean') eq null" }, + { "cast(null,'Edm.Byte') eq null" }, + { "cast(null,'Edm.DateTimeOffset') eq null" }, + { "cast(null,'Edm.Decimal') eq null" }, + { "cast(null,'Edm.Double') eq null" }, + { "cast(null,'Edm.Duration') eq null" }, + { "cast(null,'Edm.Guid') eq null" }, + { "cast(null,'Edm.Int16') eq null" }, + { "cast(null,'Edm.Int32') eq null" }, + { "cast(null,'Edm.Int64') eq null" }, + { "cast(null,'Edm.SByte') eq null" }, + { "cast(null,'Edm.Single') eq null" }, + { "cast(null,'Edm.String') eq null" }, + + { "cast(binary'T0RhdGE=','Edm.Binary') eq binary'T0RhdGE='" }, + { "cast(false,'Edm.Boolean') eq false" }, + { "cast(23,'Edm.Byte') eq 23" }, + { "cast(2001-01-01T12:00:00.000+08:00,'Edm.DateTimeOffset') eq 2001-01-01T12:00:00.000+08:00" }, + { "cast(23,'Edm.Decimal') eq 23" }, + { "cast(23,'Edm.Double') eq 23" }, + { "cast(duration'PT12H','Edm.Duration') eq duration'PT12H'" }, + { "cast(00000000-0000-0000-0000-000000000000,'Edm.Guid') eq 00000000-0000-0000-0000-000000000000" }, + { "cast(23,'Edm.Int16') eq 23" }, + { "cast(23,'Edm.Int32') eq 23" }, + { "cast(23,'Edm.Int64') eq 23" }, + { "cast(23,'Edm.SByte') eq 23" }, + { "cast(23,'Edm.Single') eq 23" }, + { "cast('hello','Edm.String') eq 'hello'" }, + + { "cast(ByteArrayProp,'Edm.Binary') eq null" }, + { "cast(BoolProp,'Edm.Boolean') eq true" }, + { "cast(DateTimeOffsetProp,'Edm.DateTimeOffset') eq 2001-01-01T12:00:00.000+08:00" }, + { "cast(DecimalProp,'Edm.Decimal') eq 23" }, + { "cast(DoubleProp,'Edm.Double') eq 23" }, + { "cast(TimeSpanProp,'Edm.Duration') eq duration'PT23H'" }, + { "cast(GuidProp,'Edm.Guid') eq 0EFDAECF-A9F0-42F3-A384-1295917AF95E" }, + { "cast(NullableShortProp,'Edm.Int16') eq 23" }, + { "cast(IntProp,'Edm.Int32') eq 23" }, + { "cast(LongProp,'Edm.Int64') eq 23" }, + { "cast(FloatProp,'Edm.Single') eq 23" }, + { "cast(StringProp,'Edm.String') eq 'hello'" }, }; - - // Act & Assert - var filters = BindFilterAndVerify(filter); - InvokeFiltersAndVerify(filters, model, (true,true)); } + } + + [Theory] + [MemberData(nameof(CastToQuotedPrimitiveType))] + public void CastToQuotedPrimitiveType_Succeeds(string filter) + { + // Arrange + var model = new DataTypes + { + BoolProp = true, + DateTimeOffsetProp = DateTimeOffset.Parse("2001-01-01T12:00:00.000+08:00"), + DecimalProp = 23, + DoubleProp = 23, + GuidProp = Guid.Parse("0EFDAECF-A9F0-42F3-A384-1295917AF95E"), + NullableShortProp = 23, + IntProp = 23, + LongProp = 23, + FloatProp = 23, + StringProp = "hello", + TimeSpanProp = TimeSpan.FromHours(23), + }; + + // Act & Assert + var filters = BindFilterAndVerify(filter); + InvokeFiltersAndVerify(filters, model, (true,true)); + } - public static TheoryDataSet CastToUnquotedComplexType + public static TheoryDataSet CastToUnquotedComplexType + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { "cast(Microsoft.AspNetCore.OData.Tests.Models.Address) eq null" }, - { "cast(null, Microsoft.AspNetCore.OData.Tests.Models.Address) eq null" }, - { "cast('', Microsoft.AspNetCore.OData.Tests.Models.Address) eq null" }, - { "cast(SupplierAddress, Microsoft.AspNetCore.OData.Tests.Models.Address) eq null" }, - }; - } + { "cast(Microsoft.AspNetCore.OData.Tests.Models.Address) eq null" }, + { "cast(null, Microsoft.AspNetCore.OData.Tests.Models.Address) eq null" }, + { "cast('', Microsoft.AspNetCore.OData.Tests.Models.Address) eq null" }, + { "cast(SupplierAddress, Microsoft.AspNetCore.OData.Tests.Models.Address) eq null" }, + }; } + } - [Theory] - [MemberData(nameof(CastToUnquotedComplexType))] - public void CastToUnquotedComplexType_ThrowsODataException(string filter) - { - // Arrange - var expectedMessage = - "Encountered invalid type cast. " + - "'Microsoft.AspNetCore.OData.Tests.Models.Address' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'."; + [Theory] + [MemberData(nameof(CastToUnquotedComplexType))] + public void CastToUnquotedComplexType_ThrowsODataException(string filter) + { + // Arrange + var expectedMessage = + "Encountered invalid type cast. " + + "'Microsoft.AspNetCore.OData.Tests.Models.Address' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'."; - // Act & Assert - ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); - } + // Act & Assert + ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); + } - public static TheoryDataSet CastToQuotedComplexType + public static TheoryDataSet CastToQuotedComplexType + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { "cast('Microsoft.AspNetCore.OData.Tests.Models.Address') eq null" }, - { "cast(null, 'Microsoft.AspNetCore.OData.Tests.Models.Address') eq null" }, - { "cast('', 'Microsoft.AspNetCore.OData.Tests.Models.Address') eq null" }, - { "cast(SupplierAddress, 'Microsoft.AspNetCore.OData.Tests.Models.Address') ne null" }, - }; - } + { "cast('Microsoft.AspNetCore.OData.Tests.Models.Address') eq null" }, + { "cast(null, 'Microsoft.AspNetCore.OData.Tests.Models.Address') eq null" }, + { "cast('', 'Microsoft.AspNetCore.OData.Tests.Models.Address') eq null" }, + { "cast(SupplierAddress, 'Microsoft.AspNetCore.OData.Tests.Models.Address') ne null" }, + }; } + } - [Theory] - [MemberData(nameof(CastToQuotedComplexType))] - public void CastToQuotedComplexType_Succeeds(string filter) + [Theory] + [MemberData(nameof(CastToQuotedComplexType))] + public void CastToQuotedComplexType_Succeeds(string filter) + { + // Arrange + var model = new Product { - // Arrange - var model = new Product - { - SupplierAddress = new Address { City = "Redmond", }, - }; + SupplierAddress = new Address { City = "Redmond", }, + }; - // Act & Assert - var filters = BindFilterAndVerify(filter); + // Act & Assert + var filters = BindFilterAndVerify(filter); - InvokeFiltersAndVerify(filters, model, (true, true)); - } + InvokeFiltersAndVerify(filters, model, (true, true)); + } - public static TheoryDataSet CastToUnquotedEntityType + public static TheoryDataSet CastToUnquotedEntityType + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet { - { - "cast(Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct)/DerivedProductName eq null", - "Cast or IsOf Function must have a type in its arguments." - }, - { - "cast(null, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)/DerivedCategoryName eq null", - "Encountered invalid type cast. " + - "'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'." - }, - { - "cast(Category, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)/DerivedCategoryName eq null", - "Encountered invalid type cast. " + - "'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'." - }, - }; - } + "cast(Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct)/DerivedProductName eq null", + "Cast or IsOf Function must have a type in its arguments." + }, + { + "cast(null, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)/DerivedCategoryName eq null", + "Encountered invalid type cast. " + + "'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'." + }, + { + "cast(Category, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)/DerivedCategoryName eq null", + "Encountered invalid type cast. " + + "'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'." + }, + }; } + } - [Theory] - [MemberData(nameof(CastToUnquotedEntityType))] - public void CastToUnquotedEntityType_ThrowsODataException(string filter, string expectedMessage) - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); - } + [Theory] + [MemberData(nameof(CastToUnquotedEntityType))] + public void CastToUnquotedEntityType_ThrowsODataException(string filter, string expectedMessage) + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); + } - [Theory] - [InlineData("cast('Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct')/DerivedProductName eq null", "$it => (($it As DerivedProduct).DerivedProductName == null)","$it => (IIF((($it As DerivedProduct) == null), null, ($it As DerivedProduct).DerivedProductName) == null)")] - [InlineData("cast(Category,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')/DerivedCategoryName eq null", "$it => (($it.Category As DerivedCategory).DerivedCategoryName == null)", "$it => (IIF((($it.Category As DerivedCategory) == null), null, ($it.Category As DerivedCategory).DerivedCategoryName) == null)")] - public void CastToQuotedEntityOrComplexType_DerivedProductName(string filter, string expectedExpression, string expectedExpressionWithNullCheck) - { - // Arrange, Act & Assert - BindFilterAndVerify(filter, expectedExpression, expectedExpressionWithNullCheck); - } - #endregion - - #region 'isof' in query option - - [Theory] - [InlineData("isof(Edm.Int16)", "$it => IIF(($it Is System.Int16), True, False)")] - [InlineData("isof('Microsoft.AspNetCore.OData.Tests.Models.Product')", "$it => IIF(($it Is Microsoft.AspNetCore.OData.Tests.Models.Product), True, False)")] - [InlineData("isof(ProductName,Edm.String)", "$it => IIF(($it.ProductName Is System.String), True, False)")] - [InlineData("isof(Category,'Microsoft.AspNetCore.OData.Tests.Models.Category')", "$it => IIF(($it.Category Is Microsoft.AspNetCore.OData.Tests.Models.Category), True, False)")] - [InlineData("isof(Category,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')", "$it => IIF(($it.Category Is Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory), True, False)")] - [InlineData("isof(Ranking, 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", "$it => IIF(($it.Ranking Is Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum), True, False)")] - public void IsofMethod_Succeeds(string filter, string expectedResult) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expectedResult, NotTesting); - } + [Theory] + [InlineData("cast('Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct')/DerivedProductName eq null", "$it => (($it As DerivedProduct).DerivedProductName == null)","$it => (IIF((($it As DerivedProduct) == null), null, ($it As DerivedProduct).DerivedProductName) == null)")] + [InlineData("cast(Category,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')/DerivedCategoryName eq null", "$it => (($it.Category As DerivedCategory).DerivedCategoryName == null)", "$it => (IIF((($it.Category As DerivedCategory) == null), null, ($it.Category As DerivedCategory).DerivedCategoryName) == null)")] + public void CastToQuotedEntityOrComplexType_DerivedProductName(string filter, string expectedExpression, string expectedExpressionWithNullCheck) + { + // Arrange, Act & Assert + BindFilterAndVerify(filter, expectedExpression, expectedExpressionWithNullCheck); + } + #endregion + + #region 'isof' in query option + + [Theory] + [InlineData("isof(Edm.Int16)", "$it => IIF(($it Is System.Int16), True, False)")] + [InlineData("isof('Microsoft.AspNetCore.OData.Tests.Models.Product')", "$it => IIF(($it Is Microsoft.AspNetCore.OData.Tests.Models.Product), True, False)")] + [InlineData("isof(ProductName,Edm.String)", "$it => IIF(($it.ProductName Is System.String), True, False)")] + [InlineData("isof(Category,'Microsoft.AspNetCore.OData.Tests.Models.Category')", "$it => IIF(($it.Category Is Microsoft.AspNetCore.OData.Tests.Models.Category), True, False)")] + [InlineData("isof(Category,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')", "$it => IIF(($it.Category Is Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory), True, False)")] + [InlineData("isof(Ranking, 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", "$it => IIF(($it.Ranking Is Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum), True, False)")] + public void IsofMethod_Succeeds(string filter, string expectedResult) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expectedResult, NotTesting); + } - [Theory] - [InlineData("isof(null)")] - [InlineData("isof(ProductName,null)")] - public void Isof_WithNullTypeName_ThrowsArgumentNullException(string filter) - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => BindFilterAndVerify(filter), - "Value cannot be null. (Parameter 'typeName')"); - } + [Theory] + [InlineData("isof(null)")] + [InlineData("isof(ProductName,null)")] + public void Isof_WithNullTypeName_ThrowsArgumentNullException(string filter) + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => BindFilterAndVerify(filter), + "Value cannot be null. (Parameter 'typeName')"); + } - [Theory] - [InlineData("isof(NoSuchProperty,Edm.Int32)", - "Could not find a property named 'NoSuchProperty' on type 'Microsoft.AspNetCore.OData.Tests.Models.DataTypes'.")] - public void IsOfUndefinedSource_ThrowsODataException(string filter, string errorMessage) - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => BindFilterAndVerify(filter), errorMessage); - } + [Theory] + [InlineData("isof(NoSuchProperty,Edm.Int32)", + "Could not find a property named 'NoSuchProperty' on type 'Microsoft.AspNetCore.OData.Tests.Models.DataTypes'.")] + public void IsOfUndefinedSource_ThrowsODataException(string filter, string errorMessage) + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => BindFilterAndVerify(filter), errorMessage); + } - [Theory] - [InlineData("isof(null,Edm.Binary)")] - [InlineData("isof(null,Edm.Boolean)")] - [InlineData("isof(null,Edm.Byte)")] - [InlineData("isof(null,Edm.DateTimeOffset)")] - [InlineData("isof(null,Edm.Decimal)")] - [InlineData("isof(null,Edm.Double)")] - [InlineData("isof(null,Edm.Duration)")] - [InlineData("isof(null,Edm.Guid)")] - [InlineData("isof(null,Edm.Int16)")] - [InlineData("isof(null,Edm.Int32)")] - [InlineData("isof(null,Edm.Int64)")] - [InlineData("isof(null,Edm.SByte)")] - [InlineData("isof(null,Edm.Single)")] - [InlineData("isof(null,Edm.Stream)")] - [InlineData("isof(null,Edm.String)")] - [InlineData("isof(null,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum)")] - [InlineData("isof(null,Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum)")] - - [InlineData("isof(ByteArrayProp,Edm.Binary)")] // ByteArrayProp == null - [InlineData("isof(IntProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum)")] - [InlineData("isof(NullableShortProp,'Edm.Int16')")] // NullableShortProp == null - - [InlineData("isof('Edm.Binary')")] - [InlineData("isof('Edm.Boolean')")] - [InlineData("isof('Edm.Byte')")] - [InlineData("isof('Edm.DateTimeOffset')")] - [InlineData("isof('Edm.Decimal')")] - [InlineData("isof('Edm.Double')")] - [InlineData("isof('Edm.Duration')")] - [InlineData("isof('Edm.Guid')")] - [InlineData("isof('Edm.Int16')")] - [InlineData("isof('Edm.Int32')")] - [InlineData("isof('Edm.Int64')")] - [InlineData("isof('Edm.SByte')")] - [InlineData("isof('Edm.Single')")] - [InlineData("isof('Edm.Stream')")] - [InlineData("isof('Edm.String')")] - [InlineData("isof('Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')")] - [InlineData("isof('Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum')")] - - [InlineData("isof(23,'Edm.Byte')")] - [InlineData("isof(23,'Edm.Decimal')")] - [InlineData("isof(23,'Edm.Double')")] - [InlineData("isof(23,'Edm.Int16')")] - [InlineData("isof(23,'Edm.Int64')")] - [InlineData("isof(23,'Edm.SByte')")] - [InlineData("isof(23,'Edm.Single')")] - [InlineData("isof('hello','Edm.Stream')")] - [InlineData("isof(0,'Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum')")] - [InlineData("isof(0,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')")] - - [InlineData("isof('2001-01-01T12:00:00.000+08:00','Edm.DateTimeOffset')")] // source is string - [InlineData("isof('00000000-0000-0000-0000-000000000000','Edm.Guid')")] // source is string - [InlineData("isof('23','Edm.Byte')")] - [InlineData("isof('23','Edm.Int16')")] - [InlineData("isof('23','Edm.Int32')")] - [InlineData("isof('false','Edm.Boolean')")] - [InlineData("isof('OData','Edm.Binary')")] - [InlineData("isof('PT12H','Edm.Duration')")] - [InlineData("isof(23,'Edm.String')")] - [InlineData("isof('0','Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum')")] - [InlineData("isof('0','Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')")] - public void IsOfPrimitiveType_Succeeds_WithFalse(string filter) - { - // Arrange - var model = new DataTypes(); + [Theory] + [InlineData("isof(null,Edm.Binary)")] + [InlineData("isof(null,Edm.Boolean)")] + [InlineData("isof(null,Edm.Byte)")] + [InlineData("isof(null,Edm.DateTimeOffset)")] + [InlineData("isof(null,Edm.Decimal)")] + [InlineData("isof(null,Edm.Double)")] + [InlineData("isof(null,Edm.Duration)")] + [InlineData("isof(null,Edm.Guid)")] + [InlineData("isof(null,Edm.Int16)")] + [InlineData("isof(null,Edm.Int32)")] + [InlineData("isof(null,Edm.Int64)")] + [InlineData("isof(null,Edm.SByte)")] + [InlineData("isof(null,Edm.Single)")] + [InlineData("isof(null,Edm.Stream)")] + [InlineData("isof(null,Edm.String)")] + [InlineData("isof(null,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum)")] + [InlineData("isof(null,Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum)")] + + [InlineData("isof(ByteArrayProp,Edm.Binary)")] // ByteArrayProp == null + [InlineData("isof(IntProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum)")] + [InlineData("isof(NullableShortProp,'Edm.Int16')")] // NullableShortProp == null + + [InlineData("isof('Edm.Binary')")] + [InlineData("isof('Edm.Boolean')")] + [InlineData("isof('Edm.Byte')")] + [InlineData("isof('Edm.DateTimeOffset')")] + [InlineData("isof('Edm.Decimal')")] + [InlineData("isof('Edm.Double')")] + [InlineData("isof('Edm.Duration')")] + [InlineData("isof('Edm.Guid')")] + [InlineData("isof('Edm.Int16')")] + [InlineData("isof('Edm.Int32')")] + [InlineData("isof('Edm.Int64')")] + [InlineData("isof('Edm.SByte')")] + [InlineData("isof('Edm.Single')")] + [InlineData("isof('Edm.Stream')")] + [InlineData("isof('Edm.String')")] + [InlineData("isof('Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')")] + [InlineData("isof('Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum')")] + + [InlineData("isof(23,'Edm.Byte')")] + [InlineData("isof(23,'Edm.Decimal')")] + [InlineData("isof(23,'Edm.Double')")] + [InlineData("isof(23,'Edm.Int16')")] + [InlineData("isof(23,'Edm.Int64')")] + [InlineData("isof(23,'Edm.SByte')")] + [InlineData("isof(23,'Edm.Single')")] + [InlineData("isof('hello','Edm.Stream')")] + [InlineData("isof(0,'Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum')")] + [InlineData("isof(0,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')")] + + [InlineData("isof('2001-01-01T12:00:00.000+08:00','Edm.DateTimeOffset')")] // source is string + [InlineData("isof('00000000-0000-0000-0000-000000000000','Edm.Guid')")] // source is string + [InlineData("isof('23','Edm.Byte')")] + [InlineData("isof('23','Edm.Int16')")] + [InlineData("isof('23','Edm.Int32')")] + [InlineData("isof('false','Edm.Boolean')")] + [InlineData("isof('OData','Edm.Binary')")] + [InlineData("isof('PT12H','Edm.Duration')")] + [InlineData("isof(23,'Edm.String')")] + [InlineData("isof('0','Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum')")] + [InlineData("isof('0','Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')")] + public void IsOfPrimitiveType_Succeeds_WithFalse(string filter) + { + // Arrange + var model = new DataTypes(); - // Act & Assert - var filters = BindFilterAndVerify(filter); - InvokeFiltersAndVerify(filters, model, (false, false)); - } + // Act & Assert + var filters = BindFilterAndVerify(filter); + InvokeFiltersAndVerify(filters, model, (false, false)); + } - // Exception messages here and in IsOfQuotedUndefinedTarget_ThrowsODataException should be consistent. - // Worse, this message is incorrect -- casts can be performed on most types but _not_ entity types and - // isof can't be performed. - [Theory] - [InlineData("isof(Edm.DateTime)", "Edm.DateTime")] - [InlineData("isof(Edm.Unknown)", "Edm.Unknown")] - [InlineData("isof(null,Edm.DateTime)", "Edm.DateTime")] - [InlineData("isof(null,Edm.Unknown)", "Edm.Unknown")] - [InlineData("isof('2001-01-01T12:00:00.000',Edm.DateTime)", "Edm.DateTime")] - [InlineData("isof('',Edm.Unknown)", "Edm.Unknown")] - [InlineData("isof(DateTimeProp,Edm.DateTime)", "Edm.DateTime")] - [InlineData("isof(IntProp,Edm.Unknown)", "Edm.Unknown")] - public void IsOfUndefinedTarget_ThrowsODataException(string filter, string typeName) - { - // Arrange - var expectedMessage = string.Format( - "The child type '{0}' in a cast was not an entity type. Casts can only be performed on entity types.", - typeName); + // Exception messages here and in IsOfQuotedUndefinedTarget_ThrowsODataException should be consistent. + // Worse, this message is incorrect -- casts can be performed on most types but _not_ entity types and + // isof can't be performed. + [Theory] + [InlineData("isof(Edm.DateTime)", "Edm.DateTime")] + [InlineData("isof(Edm.Unknown)", "Edm.Unknown")] + [InlineData("isof(null,Edm.DateTime)", "Edm.DateTime")] + [InlineData("isof(null,Edm.Unknown)", "Edm.Unknown")] + [InlineData("isof('2001-01-01T12:00:00.000',Edm.DateTime)", "Edm.DateTime")] + [InlineData("isof('',Edm.Unknown)", "Edm.Unknown")] + [InlineData("isof(DateTimeProp,Edm.DateTime)", "Edm.DateTime")] + [InlineData("isof(IntProp,Edm.Unknown)", "Edm.Unknown")] + public void IsOfUndefinedTarget_ThrowsODataException(string filter, string typeName) + { + // Arrange + var expectedMessage = string.Format( + "The child type '{0}' in a cast was not an entity type. Casts can only be performed on entity types.", + typeName); - // Act & Assert - ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); - } + // Act & Assert + ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); + } - [Theory] - [InlineData("isof('Edm.DateTime')")] - [InlineData("isof('Edm.Unknown')")] - [InlineData("isof(null,'Edm.DateTime')")] - [InlineData("isof(null,'Edm.Unknown')")] - [InlineData("isof('2001-01-01T12:00:00.000','Edm.DateTime')")] - [InlineData("isof('','Edm.Unknown')")] - [InlineData("isof(DateTimeProp,'Edm.DateTime')")] - [InlineData("isof(IntProp,'Edm.Unknown')")] - public void IsOfQuotedUndefinedTarget_ThrowsODataException(string filter) - { - // Arrange - var expectedMessage = "Cast or IsOf Function must have a type in its arguments."; + [Theory] + [InlineData("isof('Edm.DateTime')")] + [InlineData("isof('Edm.Unknown')")] + [InlineData("isof(null,'Edm.DateTime')")] + [InlineData("isof(null,'Edm.Unknown')")] + [InlineData("isof('2001-01-01T12:00:00.000','Edm.DateTime')")] + [InlineData("isof('','Edm.Unknown')")] + [InlineData("isof(DateTimeProp,'Edm.DateTime')")] + [InlineData("isof(IntProp,'Edm.Unknown')")] + public void IsOfQuotedUndefinedTarget_ThrowsODataException(string filter) + { + // Arrange + var expectedMessage = "Cast or IsOf Function must have a type in its arguments."; - // Act & Assert - ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); - } + // Act & Assert + ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); + } - [Theory] - [InlineData("isof(Microsoft.AspNetCore.OData.Tests.Models.Address)")] - [InlineData("isof(null,Microsoft.AspNetCore.OData.Tests.Models.Address)")] - [InlineData("isof(null, Microsoft.AspNetCore.OData.Tests.Models.Address)")] - [InlineData("isof(SupplierAddress,Microsoft.AspNetCore.OData.Tests.Models.Address)")] - [InlineData("isof(SupplierAddress, Microsoft.AspNetCore.OData.Tests.Models.Address)")] - public void IsOfUnquotedComplexType_ThrowsODataException(string filter) - { - // Arrange - var expectedMessage = - "Encountered invalid type cast. " + - "'Microsoft.AspNetCore.OData.Tests.Models.Address' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'."; + [Theory] + [InlineData("isof(Microsoft.AspNetCore.OData.Tests.Models.Address)")] + [InlineData("isof(null,Microsoft.AspNetCore.OData.Tests.Models.Address)")] + [InlineData("isof(null, Microsoft.AspNetCore.OData.Tests.Models.Address)")] + [InlineData("isof(SupplierAddress,Microsoft.AspNetCore.OData.Tests.Models.Address)")] + [InlineData("isof(SupplierAddress, Microsoft.AspNetCore.OData.Tests.Models.Address)")] + public void IsOfUnquotedComplexType_ThrowsODataException(string filter) + { + // Arrange + var expectedMessage = + "Encountered invalid type cast. " + + "'Microsoft.AspNetCore.OData.Tests.Models.Address' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'."; - // Act & Assert - ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); - } + // Act & Assert + ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); + } - [Theory] - [InlineData("isof(Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct)", "Cast or IsOf Function must have a type in its arguments.")] - [InlineData("isof(null,Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", - "Encountered invalid type cast. 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'.")] - [InlineData("isof(null, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", - "Encountered invalid type cast. 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'.")] - [InlineData("isof(Category,Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", - "Encountered invalid type cast. 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'.")] - [InlineData("isof(Category, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", - "Encountered invalid type cast. 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'.")] - public void IsOfUnquotedEntityType_ThrowsODataException(string filter, string expectedMessage) - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); - } + [Theory] + [InlineData("isof(Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct)", "Cast or IsOf Function must have a type in its arguments.")] + [InlineData("isof(null,Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", + "Encountered invalid type cast. 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'.")] + [InlineData("isof(null, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", + "Encountered invalid type cast. 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'.")] + [InlineData("isof(Category,Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", + "Encountered invalid type cast. 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'.")] + [InlineData("isof(Category, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", + "Encountered invalid type cast. 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory' is not assignable from 'Microsoft.AspNetCore.OData.Tests.Models.Product'.")] + public void IsOfUnquotedEntityType_ThrowsODataException(string filter, string expectedMessage) + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => BindFilterAndVerify(filter), expectedMessage); + } - [Theory] - [InlineData("isof('Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct')")] - [InlineData("isof(SupplierAddress,'Microsoft.AspNetCore.OData.Tests.Models.Address')")] - [InlineData("isof(SupplierAddress, 'Microsoft.AspNetCore.OData.Tests.Models.Address')")] - [InlineData("isof(Category,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')")] - [InlineData("isof(Category, 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')")] - public void IsOfQuotedNonPrimitiveType_Succeeds(string filter) + [Theory] + [InlineData("isof('Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct')")] + [InlineData("isof(SupplierAddress,'Microsoft.AspNetCore.OData.Tests.Models.Address')")] + [InlineData("isof(SupplierAddress, 'Microsoft.AspNetCore.OData.Tests.Models.Address')")] + [InlineData("isof(Category,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')")] + [InlineData("isof(Category, 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')")] + public void IsOfQuotedNonPrimitiveType_Succeeds(string filter) + { + // Arrange + var model = new DerivedProduct { - // Arrange - var model = new DerivedProduct - { - SupplierAddress = new Address { City = "Redmond", }, - Category = new DerivedCategory { DerivedCategoryName = "DerivedCategory" } - }; + SupplierAddress = new Address { City = "Redmond", }, + Category = new DerivedCategory { DerivedCategoryName = "DerivedCategory" } + }; - // Act & Assert - var filters = BindFilterAndVerify(filter); - InvokeFiltersAndVerify(filters, model, (true, true)); - } + // Act & Assert + var filters = BindFilterAndVerify(filter); + InvokeFiltersAndVerify(filters, model, (true, true)); + } - [Theory] - [InlineData("isof(null,'Microsoft.AspNetCore.OData.Tests.Models.Address')")] - [InlineData("isof(null, 'Microsoft.AspNetCore.OData.Tests.Models.Address')")] - [InlineData("isof(null,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')")] - [InlineData("isof(null, 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')")] - public void IsOfQuotedNonPrimitiveTypeWithNull_Succeeds_WithFalse(string filter) + [Theory] + [InlineData("isof(null,'Microsoft.AspNetCore.OData.Tests.Models.Address')")] + [InlineData("isof(null, 'Microsoft.AspNetCore.OData.Tests.Models.Address')")] + [InlineData("isof(null,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')")] + [InlineData("isof(null, 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')")] + public void IsOfQuotedNonPrimitiveTypeWithNull_Succeeds_WithFalse(string filter) + { + // Arrange + var model = new DerivedProduct { - // Arrange - var model = new DerivedProduct - { - SupplierAddress = new Address { City = "Redmond", }, - Category = new DerivedCategory { DerivedCategoryName = "DerivedCategory" } - }; + SupplierAddress = new Address { City = "Redmond", }, + Category = new DerivedCategory { DerivedCategoryName = "DerivedCategory" } + }; - // Act & Assert - var filters = BindFilterAndVerify(filter); - InvokeFiltersAndVerify(filters, model, (false, false)); - } - #endregion + // Act & Assert + var filters = BindFilterAndVerify(filter); + InvokeFiltersAndVerify(filters, model, (false, false)); + } + #endregion #if false - [Fact] - public void BindForNodeOnFilterBinder_ThrowsArgumentNull_Node() - { - // Arrange - ODataQuerySettings settings = new ODataQuerySettings(); - IEdmModel model = EdmCoreModel.Instance; - IAssemblyResolver resolver = new Mock().Object; - FilterBinder binder = new FilterBinder(settings, resolver, model); + [Fact] + public void BindForNodeOnFilterBinder_ThrowsArgumentNull_Node() + { + // Arrange + ODataQuerySettings settings = new ODataQuerySettings(); + IEdmModel model = EdmCoreModel.Instance; + IAssemblyResolver resolver = new Mock().Object; + FilterBinder binder = new FilterBinder(settings, resolver, model); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.Bind(null), "node"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.Bind(null), "node"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindDynamicPropertyAccessQueryNode(null), "openNode"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindDynamicPropertyAccessQueryNode(null), "openNode"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindSingleResourceFunctionCallNode(null), "node"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindSingleResourceFunctionCallNode(null), "node"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindSingleResourceCastNode(null), "node"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindSingleResourceCastNode(null), "node"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindCollectionResourceCastNode(null), "node"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindCollectionResourceCastNode(null), "node"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindBinaryOperatorNode(null), "binaryOperatorNode"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindBinaryOperatorNode(null), "binaryOperatorNode"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindInNode(null), "inNode"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindInNode(null), "inNode"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindRangeVariable(null), "rangeVariable"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindRangeVariable(null), "rangeVariable"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindCollectionPropertyAccessNode(null), "propertyAccessNode"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindCollectionPropertyAccessNode(null), "propertyAccessNode"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindCollectionComplexNode(null), "collectionComplexNode"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindCollectionComplexNode(null), "collectionComplexNode"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindPropertyAccessQueryNode(null), "propertyAccessNode"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindPropertyAccessQueryNode(null), "propertyAccessNode"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindSingleComplexNode(null), "singleComplexNode"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindSingleComplexNode(null), "singleComplexNode"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindUnaryOperatorNode(null), "unaryOperatorNode"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindUnaryOperatorNode(null), "unaryOperatorNode"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindAllNode(null), "allNode"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindAllNode(null), "allNode"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindAnyNode(null), "anyNode"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindAnyNode(null), "anyNode"); + } - [Fact] - public void BindOnFilterBinder_ThrowsNotSupported_InvalidNode() - { - // Arrange - ODataQuerySettings settings = new ODataQuerySettings(); - IEdmModel model = EdmCoreModel.Instance; - IAssemblyResolver resolver = new Mock().Object; - FilterBinder binder = new FilterBinder(settings, resolver, model); + [Fact] + public void BindOnFilterBinder_ThrowsNotSupported_InvalidNode() + { + // Arrange + ODataQuerySettings settings = new ODataQuerySettings(); + IEdmModel model = EdmCoreModel.Instance; + IAssemblyResolver resolver = new Mock().Object; + FilterBinder binder = new FilterBinder(settings, resolver, model); - MyNoneQueryNode node = new MyNoneQueryNode(); + MyNoneQueryNode node = new MyNoneQueryNode(); - // Act & Assert - ExceptionAssert.Throws(() => binder.Bind(node), - "Binding OData QueryNode of kind 'None' is not supported by 'FilterBinder'."); - } + // Act & Assert + ExceptionAssert.Throws(() => binder.Bind(node), + "Binding OData QueryNode of kind 'None' is not supported by 'FilterBinder'."); + } #endif #region parameter alias for filter query option - [Theory] - // Parameter alias value is not null. - [InlineData("IntProp eq @p", "1", "$it => ($it.IntProp == 1)")] - [InlineData("BoolProp eq @p", "true", "$it => ($it.BoolProp == True)")] - [InlineData("LongProp eq @p", "-123", "$it => ($it.LongProp == Convert(-123))")] - [InlineData("FloatProp eq @p", "1.23", "$it => ($it.FloatProp == 1.23)")] - [InlineData("DoubleProp eq @p", "4.56", "$it => ($it.DoubleProp == Convert(4.56))")] - [InlineData("StringProp eq @p", "'abc'", "$it => ($it.StringProp == \"abc\")")] - [InlineData("DateTimeOffsetProp eq @p", "2001-01-01T12:00:00.000+08:00", "$it => ($it.DateTimeOffsetProp == 01/01/2001 12:00:00 +08:00)")] - [InlineData("TimeSpanProp eq @p", "duration'P8DT23H59M59.9999S'", "$it => ($it.TimeSpanProp == 8.23:59:59.9999000)")] - [InlineData("GuidProp eq @p", "00000000-0000-0000-0000-000000000000", "$it => ($it.GuidProp == 00000000-0000-0000-0000-000000000000)")] - [InlineData("SimpleEnumProp eq @p", "Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "$it => (Convert($it.SimpleEnumProp) == 0)")] - // Parameter alias value is null. - [InlineData("NullableIntProp eq @p", "null", "$it => ($it.NullableIntProp == null)")] - [InlineData("NullableBoolProp eq @p", "null", "$it => ($it.NullableBoolProp == null)")] - [InlineData("NullableLongProp eq @p", "null", "$it => ($it.NullableLongProp == null)")] - [InlineData("NullableSingleProp eq @p", "null", "$it => ($it.NullableSingleProp == null)")] - [InlineData("NullableDoubleProp eq @p", "null", "$it => ($it.NullableDoubleProp == null)")] - [InlineData("StringProp eq @p", "null", "$it => ($it.StringProp == null)")] - [InlineData("NullableDateTimeOffsetProp eq @p", "null", "$it => ($it.NullableDateTimeOffsetProp == null)")] - [InlineData("NullableTimeSpanProp eq @p", "null", "$it => ($it.NullableTimeSpanProp == null)")] - [InlineData("NullableGuidProp eq @p", "null", "$it => ($it.NullableGuidProp == null)")] - [InlineData("NullableSimpleEnumProp eq @p", "null", "$it => (Convert($it.NullableSimpleEnumProp) == null)")] - // Parameter alias value is property. - [InlineData("@p eq 1", "IntProp", "$it => ($it.IntProp == 1)")] - [InlineData("@p eq true", "NullableBoolProp", "$it => ($it.NullableBoolProp == Convert(True))")] - [InlineData("@p eq -123", "LongProp", "$it => ($it.LongProp == -123)")] - [InlineData("@p eq 1.23", "FloatProp", "$it => ($it.FloatProp == 1.23)")] - [InlineData("@p eq 4.56", "NullableDoubleProp", "$it => ($it.NullableDoubleProp == Convert(4.56))")] - [InlineData("@p eq 'abc'", "StringProp", "$it => ($it.StringProp == \"abc\")")] - [InlineData("@p eq 2001-01-01T12:00:00.000+08:00", "DateTimeOffsetProp", "$it => ($it.DateTimeOffsetProp == 01/01/2001 12:00:00 +08:00)")] - [InlineData("@p eq duration'P8DT23H59M59.9999S'", "TimeSpanProp", "$it => ($it.TimeSpanProp == 8.23:59:59.9999000)")] - [InlineData("@p eq 00000000-0000-0000-0000-000000000000", "GuidProp", "$it => ($it.GuidProp == 00000000-0000-0000-0000-000000000000)")] - [InlineData("@p eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "SimpleEnumProp", "$it => (Convert($it.SimpleEnumProp) == 0)")] - // Parameter alias value has built-in functions. - [InlineData("@p eq 'abc'", "substring(StringProp,5)", "$it => ($it.StringProp.Substring(5) == \"abc\")")] - [InlineData("2 eq @p", "IntProp add 1", "$it => (2 == ($it.IntProp + 1))")] - [InlineData("EntityProp/AlternateAddresses/all(a: a/City ne @p)", "'abc'", "$it => $it.EntityProp.AlternateAddresses.All(a => (a.City != \"abc\"))")] - public void ParameterAlias_Succeeds(string filter, string parameterAliasValue, string expectedResult) - { - // Arrange - IEdmModel model = GetModel(); - IEdmType targetEdmType = model.FindType("Microsoft.AspNetCore.OData.Tests.Models.DataTypes"); - IEdmNavigationSource targetNavigationSource = model.FindDeclaredEntitySet("Microsoft.AspNetCore.OData.Tests.Models.Products"); - IDictionary queryOptions = new Dictionary { { "$filter", filter } }; - queryOptions.Add("@p", parameterAliasValue); - ODataQueryOptionParser parser = new ODataQueryOptionParser(model, targetEdmType, targetNavigationSource, queryOptions); - ODataQueryContext context = new ODataQueryContext(model, typeof(DataTypes)); - context.RequestContainer = new MockServiceProvider(); - FilterClause filterClause = new FilterQueryOption(filter, context, parser).FilterClause; - - // Act - Expression actualExpression = FilterBinderTestsHelper.TestBind( - filterClause, - typeof(DataTypes), - model, - AssemblyResolverHelper.Default, - new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }); - - // Assert - VerifyExpression(actualExpression, expectedResult); - } - - [Theory] - [InlineData("NullableIntProp eq @p", "$it => ($it.NullableIntProp == null)")] - [InlineData("NullableBoolProp eq @p", "$it => ($it.NullableBoolProp == null)")] - [InlineData("NullableDoubleProp eq @p", "$it => ($it.NullableDoubleProp == null)")] - [InlineData("StringProp eq @p", "$it => ($it.StringProp == null)")] - [InlineData("NullableDateTimeOffsetProp eq @p", "$it => ($it.NullableDateTimeOffsetProp == null)")] - [InlineData("NullableSimpleEnumProp eq @p", "$it => (Convert($it.NullableSimpleEnumProp) == null)")] - [InlineData("EntityProp/AlternateAddresses/any(a: a/City eq @p)", "$it => $it.EntityProp.AlternateAddresses.Any(a => (a.City == null))")] - public void ParameterAlias_AssumedToBeNull_ValueNotFound(string filter, string expectedResult) - { - // Arrange - IEdmModel model = GetModel(); - IEdmType targetEdmType = model.FindType("Microsoft.AspNetCore.OData.Tests.Models.DataTypes"); - IEdmNavigationSource targetNavigationSource = model.FindDeclaredEntitySet("Microsoft.AspNetCore.OData.Tests.Models.Products"); - IDictionary queryOptions = new Dictionary { { "$filter", filter } }; - ODataQueryOptionParser parser = new ODataQueryOptionParser(model, targetEdmType, targetNavigationSource, queryOptions); - ODataQueryContext context = new ODataQueryContext(model, typeof(DataTypes)); - context.RequestContainer = new MockServiceProvider(); - FilterClause filterClause = new FilterQueryOption(filter, context, parser).FilterClause; - - // Act - Expression actualExpression = FilterBinderTestsHelper.TestBind( - filterClause, - typeof(DataTypes), - model, - AssemblyResolverHelper.Default, - new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }); - - // Assert - VerifyExpression(actualExpression, expectedResult); - } - - [Fact] - public void ParameterAlias_NestedCase_Succeeds() - { - // Arrange - IEdmModel model = GetModel(); - IEdmType targetEdmType = model.FindType("Microsoft.AspNetCore.OData.Tests.Models.DataTypes"); - IEdmNavigationSource targetNavigationSource = model.FindDeclaredEntitySet("Microsoft.AspNetCore.OData.Tests.Models.Products"); - - ODataQueryOptionParser parser = new ODataQueryOptionParser( - model, - targetEdmType, - targetNavigationSource, - new Dictionary { { "$filter", "IntProp eq @p1" }, { "@p1", "@p2" }, { "@p2", "123" } }); - - ODataQueryContext context = new ODataQueryContext(model, typeof(DataTypes)); - context.RequestContainer = new MockServiceProvider(); - FilterClause filterClause = new FilterQueryOption("IntProp eq @p1", context, parser).FilterClause; - - // Act - Expression actualExpression = FilterBinderTestsHelper.TestBind( - filterClause, - typeof(DataTypes), - model, - AssemblyResolverHelper.Default, - new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }); - - // Assert - VerifyExpression(actualExpression, "$it => ($it.IntProp == 123)"); - } + [Theory] + // Parameter alias value is not null. + [InlineData("IntProp eq @p", "1", "$it => ($it.IntProp == 1)")] + [InlineData("BoolProp eq @p", "true", "$it => ($it.BoolProp == True)")] + [InlineData("LongProp eq @p", "-123", "$it => ($it.LongProp == Convert(-123))")] + [InlineData("FloatProp eq @p", "1.23", "$it => ($it.FloatProp == 1.23)")] + [InlineData("DoubleProp eq @p", "4.56", "$it => ($it.DoubleProp == Convert(4.56))")] + [InlineData("StringProp eq @p", "'abc'", "$it => ($it.StringProp == \"abc\")")] + [InlineData("DateTimeOffsetProp eq @p", "2001-01-01T12:00:00.000+08:00", "$it => ($it.DateTimeOffsetProp == 01/01/2001 12:00:00 +08:00)")] + [InlineData("TimeSpanProp eq @p", "duration'P8DT23H59M59.9999S'", "$it => ($it.TimeSpanProp == 8.23:59:59.9999000)")] + [InlineData("GuidProp eq @p", "00000000-0000-0000-0000-000000000000", "$it => ($it.GuidProp == 00000000-0000-0000-0000-000000000000)")] + [InlineData("SimpleEnumProp eq @p", "Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "$it => (Convert($it.SimpleEnumProp) == 0)")] + // Parameter alias value is null. + [InlineData("NullableIntProp eq @p", "null", "$it => ($it.NullableIntProp == null)")] + [InlineData("NullableBoolProp eq @p", "null", "$it => ($it.NullableBoolProp == null)")] + [InlineData("NullableLongProp eq @p", "null", "$it => ($it.NullableLongProp == null)")] + [InlineData("NullableSingleProp eq @p", "null", "$it => ($it.NullableSingleProp == null)")] + [InlineData("NullableDoubleProp eq @p", "null", "$it => ($it.NullableDoubleProp == null)")] + [InlineData("StringProp eq @p", "null", "$it => ($it.StringProp == null)")] + [InlineData("NullableDateTimeOffsetProp eq @p", "null", "$it => ($it.NullableDateTimeOffsetProp == null)")] + [InlineData("NullableTimeSpanProp eq @p", "null", "$it => ($it.NullableTimeSpanProp == null)")] + [InlineData("NullableGuidProp eq @p", "null", "$it => ($it.NullableGuidProp == null)")] + [InlineData("NullableSimpleEnumProp eq @p", "null", "$it => (Convert($it.NullableSimpleEnumProp) == null)")] + // Parameter alias value is property. + [InlineData("@p eq 1", "IntProp", "$it => ($it.IntProp == 1)")] + [InlineData("@p eq true", "NullableBoolProp", "$it => ($it.NullableBoolProp == Convert(True))")] + [InlineData("@p eq -123", "LongProp", "$it => ($it.LongProp == -123)")] + [InlineData("@p eq 1.23", "FloatProp", "$it => ($it.FloatProp == 1.23)")] + [InlineData("@p eq 4.56", "NullableDoubleProp", "$it => ($it.NullableDoubleProp == Convert(4.56))")] + [InlineData("@p eq 'abc'", "StringProp", "$it => ($it.StringProp == \"abc\")")] + [InlineData("@p eq 2001-01-01T12:00:00.000+08:00", "DateTimeOffsetProp", "$it => ($it.DateTimeOffsetProp == 01/01/2001 12:00:00 +08:00)")] + [InlineData("@p eq duration'P8DT23H59M59.9999S'", "TimeSpanProp", "$it => ($it.TimeSpanProp == 8.23:59:59.9999000)")] + [InlineData("@p eq 00000000-0000-0000-0000-000000000000", "GuidProp", "$it => ($it.GuidProp == 00000000-0000-0000-0000-000000000000)")] + [InlineData("@p eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "SimpleEnumProp", "$it => (Convert($it.SimpleEnumProp) == 0)")] + // Parameter alias value has built-in functions. + [InlineData("@p eq 'abc'", "substring(StringProp,5)", "$it => ($it.StringProp.Substring(5) == \"abc\")")] + [InlineData("2 eq @p", "IntProp add 1", "$it => (2 == ($it.IntProp + 1))")] + [InlineData("EntityProp/AlternateAddresses/all(a: a/City ne @p)", "'abc'", "$it => $it.EntityProp.AlternateAddresses.All(a => (a.City != \"abc\"))")] + public void ParameterAlias_Succeeds(string filter, string parameterAliasValue, string expectedResult) + { + // Arrange + IEdmModel model = GetModel(); + IEdmType targetEdmType = model.FindType("Microsoft.AspNetCore.OData.Tests.Models.DataTypes"); + IEdmNavigationSource targetNavigationSource = model.FindDeclaredEntitySet("Microsoft.AspNetCore.OData.Tests.Models.Products"); + IDictionary queryOptions = new Dictionary { { "$filter", filter } }; + queryOptions.Add("@p", parameterAliasValue); + ODataQueryOptionParser parser = new ODataQueryOptionParser(model, targetEdmType, targetNavigationSource, queryOptions); + ODataQueryContext context = new ODataQueryContext(model, typeof(DataTypes)); + context.RequestContainer = new MockServiceProvider(); + FilterClause filterClause = new FilterQueryOption(filter, context, parser).FilterClause; + + // Act + Expression actualExpression = FilterBinderTestsHelper.TestBind( + filterClause, + typeof(DataTypes), + model, + AssemblyResolverHelper.Default, + new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }); + + // Assert + VerifyExpression(actualExpression, expectedResult); + } - [Fact] - public void ParameterAlias_Throws_NotStartWithAt() - { - // Arrange - IEdmModel model = GetModel(); - IEdmType targetEdmType = model.FindType("Microsoft.AspNetCore.OData.Tests.Models.DataTypes"); - IEdmNavigationSource targetNavigationSource = model.FindDeclaredEntitySet("Microsoft.AspNetCore.OData.Tests.Models.Products"); + [Theory] + [InlineData("NullableIntProp eq @p", "$it => ($it.NullableIntProp == null)")] + [InlineData("NullableBoolProp eq @p", "$it => ($it.NullableBoolProp == null)")] + [InlineData("NullableDoubleProp eq @p", "$it => ($it.NullableDoubleProp == null)")] + [InlineData("StringProp eq @p", "$it => ($it.StringProp == null)")] + [InlineData("NullableDateTimeOffsetProp eq @p", "$it => ($it.NullableDateTimeOffsetProp == null)")] + [InlineData("NullableSimpleEnumProp eq @p", "$it => (Convert($it.NullableSimpleEnumProp) == null)")] + [InlineData("EntityProp/AlternateAddresses/any(a: a/City eq @p)", "$it => $it.EntityProp.AlternateAddresses.Any(a => (a.City == null))")] + public void ParameterAlias_AssumedToBeNull_ValueNotFound(string filter, string expectedResult) + { + // Arrange + IEdmModel model = GetModel(); + IEdmType targetEdmType = model.FindType("Microsoft.AspNetCore.OData.Tests.Models.DataTypes"); + IEdmNavigationSource targetNavigationSource = model.FindDeclaredEntitySet("Microsoft.AspNetCore.OData.Tests.Models.Products"); + IDictionary queryOptions = new Dictionary { { "$filter", filter } }; + ODataQueryOptionParser parser = new ODataQueryOptionParser(model, targetEdmType, targetNavigationSource, queryOptions); + ODataQueryContext context = new ODataQueryContext(model, typeof(DataTypes)); + context.RequestContainer = new MockServiceProvider(); + FilterClause filterClause = new FilterQueryOption(filter, context, parser).FilterClause; + + // Act + Expression actualExpression = FilterBinderTestsHelper.TestBind( + filterClause, + typeof(DataTypes), + model, + AssemblyResolverHelper.Default, + new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }); + + // Assert + VerifyExpression(actualExpression, expectedResult); + } - ODataQueryOptionParser parser = new ODataQueryOptionParser( - model, - targetEdmType, - targetNavigationSource, - new Dictionary { { "$filter", "IntProp eq #p" }, { "#p", "123" } }); + [Fact] + public void ParameterAlias_NestedCase_Succeeds() + { + // Arrange + IEdmModel model = GetModel(); + IEdmType targetEdmType = model.FindType("Microsoft.AspNetCore.OData.Tests.Models.DataTypes"); + IEdmNavigationSource targetNavigationSource = model.FindDeclaredEntitySet("Microsoft.AspNetCore.OData.Tests.Models.Products"); + + ODataQueryOptionParser parser = new ODataQueryOptionParser( + model, + targetEdmType, + targetNavigationSource, + new Dictionary { { "$filter", "IntProp eq @p1" }, { "@p1", "@p2" }, { "@p2", "123" } }); + + ODataQueryContext context = new ODataQueryContext(model, typeof(DataTypes)); + context.RequestContainer = new MockServiceProvider(); + FilterClause filterClause = new FilterQueryOption("IntProp eq @p1", context, parser).FilterClause; + + // Act + Expression actualExpression = FilterBinderTestsHelper.TestBind( + filterClause, + typeof(DataTypes), + model, + AssemblyResolverHelper.Default, + new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }); + + // Assert + VerifyExpression(actualExpression, "$it => ($it.IntProp == 123)"); + } - // Act & Assert - ExceptionAssert.Throws( - () => parser.ParseFilter(), - "Syntax error: character '#' is not valid at position 11 in 'IntProp eq #p'."); - } + [Fact] + public void ParameterAlias_Throws_NotStartWithAt() + { + // Arrange + IEdmModel model = GetModel(); + IEdmType targetEdmType = model.FindType("Microsoft.AspNetCore.OData.Tests.Models.DataTypes"); + IEdmNavigationSource targetNavigationSource = model.FindDeclaredEntitySet("Microsoft.AspNetCore.OData.Tests.Models.Products"); + + ODataQueryOptionParser parser = new ODataQueryOptionParser( + model, + targetEdmType, + targetNavigationSource, + new Dictionary { { "$filter", "IntProp eq #p" }, { "#p", "123" } }); + + // Act & Assert + ExceptionAssert.Throws( + () => parser.ParseFilter(), + "Syntax error: character '#' is not valid at position 11 in 'IntProp eq #p'."); + } #endregion - [Theory] - [InlineData("ByteArrayProp eq binary'I6v/'", "$it => ($it.ByteArrayProp == System.Byte[])", true, true)] - [InlineData("ByteArrayProp ne binary'I6v/'", "$it => ($it.ByteArrayProp != System.Byte[])", false, false)] - [InlineData("binary'I6v/' eq binary'I6v/'", "$it => (System.Byte[] == System.Byte[])", true, true)] - [InlineData("binary'I6v/' ne binary'I6v/'", "$it => (System.Byte[] != System.Byte[])", false, false)] - [InlineData("ByteArrayPropWithNullValue ne binary'I6v/'", "$it => ($it.ByteArrayPropWithNullValue != System.Byte[])", true, true)] - [InlineData("ByteArrayPropWithNullValue ne ByteArrayPropWithNullValue", "$it => ($it.ByteArrayPropWithNullValue != $it.ByteArrayPropWithNullValue)", false, false)] - [InlineData("ByteArrayPropWithNullValue ne null", "$it => ($it.ByteArrayPropWithNullValue != null)", false, false)] - [InlineData("ByteArrayPropWithNullValue eq null", "$it => ($it.ByteArrayPropWithNullValue == null)", true, true)] - [InlineData("null ne ByteArrayPropWithNullValue", "$it => (null != $it.ByteArrayPropWithNullValue)", false, false)] - [InlineData("null eq ByteArrayPropWithNullValue", "$it => (null == $it.ByteArrayPropWithNullValue)", true, true)] - public void ByteArrayComparisons(string filter, string expression, bool falseNullPropagation, bool trueNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify(filter, expression, NotTesting); - InvokeFiltersAndVerify(filters, - new DataTypes - { - ByteArrayProp = new byte[] { 35, 171, 255 } - }, - (falseNullPropagation, trueNullPropagation)); - } + [Theory] + [InlineData("ByteArrayProp eq binary'I6v/'", "$it => ($it.ByteArrayProp == System.Byte[])", true, true)] + [InlineData("ByteArrayProp ne binary'I6v/'", "$it => ($it.ByteArrayProp != System.Byte[])", false, false)] + [InlineData("binary'I6v/' eq binary'I6v/'", "$it => (System.Byte[] == System.Byte[])", true, true)] + [InlineData("binary'I6v/' ne binary'I6v/'", "$it => (System.Byte[] != System.Byte[])", false, false)] + [InlineData("ByteArrayPropWithNullValue ne binary'I6v/'", "$it => ($it.ByteArrayPropWithNullValue != System.Byte[])", true, true)] + [InlineData("ByteArrayPropWithNullValue ne ByteArrayPropWithNullValue", "$it => ($it.ByteArrayPropWithNullValue != $it.ByteArrayPropWithNullValue)", false, false)] + [InlineData("ByteArrayPropWithNullValue ne null", "$it => ($it.ByteArrayPropWithNullValue != null)", false, false)] + [InlineData("ByteArrayPropWithNullValue eq null", "$it => ($it.ByteArrayPropWithNullValue == null)", true, true)] + [InlineData("null ne ByteArrayPropWithNullValue", "$it => (null != $it.ByteArrayPropWithNullValue)", false, false)] + [InlineData("null eq ByteArrayPropWithNullValue", "$it => (null == $it.ByteArrayPropWithNullValue)", true, true)] + public void ByteArrayComparisons(string filter, string expression, bool falseNullPropagation, bool trueNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify(filter, expression, NotTesting); + InvokeFiltersAndVerify(filters, + new DataTypes + { + ByteArrayProp = new byte[] { 35, 171, 255 } + }, + (falseNullPropagation, trueNullPropagation)); + } - [Theory] - [InlineData("binary'AP8Q' ge binary'AP8Q'", "GreaterThanOrEqual")] - [InlineData("binary'AP8Q' le binary'AP8Q'", "LessThanOrEqual")] - [InlineData("binary'AP8Q' lt binary'AP8Q'", "LessThan")] - [InlineData("binary'AP8Q' gt binary'AP8Q'", "GreaterThan")] - [InlineData("binary'AP8Q' add binary'AP8Q'", "Add")] - [InlineData("binary'AP8Q' sub binary'AP8Q'", "Subtract")] - [InlineData("binary'AP8Q' mul binary'AP8Q'", "Multiply")] - [InlineData("binary'AP8Q' div binary'AP8Q'", "Divide")] - public void DisAllowed_ByteArrayComparisons(string filter, string op) - { - // Arrange & Act & Assert - ExceptionAssert.Throws( - () => BindFilterAndVerify(filter), - string.Format(CultureInfo.InvariantCulture, "A binary operator with incompatible types was detected. Found operand types 'Edm.Binary' and 'Edm.Binary' for operator kind '{0}'.", op)); - } + [Theory] + [InlineData("binary'AP8Q' ge binary'AP8Q'", "GreaterThanOrEqual")] + [InlineData("binary'AP8Q' le binary'AP8Q'", "LessThanOrEqual")] + [InlineData("binary'AP8Q' lt binary'AP8Q'", "LessThan")] + [InlineData("binary'AP8Q' gt binary'AP8Q'", "GreaterThan")] + [InlineData("binary'AP8Q' add binary'AP8Q'", "Add")] + [InlineData("binary'AP8Q' sub binary'AP8Q'", "Subtract")] + [InlineData("binary'AP8Q' mul binary'AP8Q'", "Multiply")] + [InlineData("binary'AP8Q' div binary'AP8Q'", "Divide")] + public void DisAllowed_ByteArrayComparisons(string filter, string op) + { + // Arrange & Act & Assert + ExceptionAssert.Throws( + () => BindFilterAndVerify(filter), + string.Format(CultureInfo.InvariantCulture, "A binary operator with incompatible types was detected. Found operand types 'Edm.Binary' and 'Edm.Binary' for operator kind '{0}'.", op)); + } - [Theory] - [InlineData("NullableUShortProp eq 12", "$it => (Convert($it.NullableUShortProp.Value) == Convert(12))")] - [InlineData("NullableULongProp eq 12L", "$it => (Convert($it.NullableULongProp.Value) == Convert(12))")] - [InlineData("NullableUIntProp eq 12", "$it => (Convert($it.NullableUIntProp.Value) == Convert(12))")] - [InlineData("NullableCharProp eq 'a'", "$it => ($it.NullableCharProp.Value.ToString() == \"a\")")] - public void Nullable_NonstandardEdmPrimitives(string filter, string expression) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify(filter, expression, NotTesting); + [Theory] + [InlineData("NullableUShortProp eq 12", "$it => (Convert($it.NullableUShortProp.Value) == Convert(12))")] + [InlineData("NullableULongProp eq 12L", "$it => (Convert($it.NullableULongProp.Value) == Convert(12))")] + [InlineData("NullableUIntProp eq 12", "$it => (Convert($it.NullableUIntProp.Value) == Convert(12))")] + [InlineData("NullableCharProp eq 'a'", "$it => ($it.NullableCharProp.Value.ToString() == \"a\")")] + public void Nullable_NonstandardEdmPrimitives(string filter, string expression) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify(filter, expression, NotTesting); - InvokeFiltersAndThrows(filters, - new DataTypes(), - (typeof(InvalidOperationException), false)); - } + InvokeFiltersAndThrows(filters, + new DataTypes(), + (typeof(InvalidOperationException), false)); + } - [Theory] - [InlineData("Category/Product/ProductID in (1)", "$it => System.Collections.Generic.List`1[System.Int32].Contains($it.Category.Product.ProductID)", "$it => System.Collections.Generic.List`1[System.Int32].Cast().Contains(IIF((IIF(($it.Category == null), null, $it.Category.Product) == null), null, Convert($it.Category.Product.ProductID)))")] - [InlineData("Category/Product/GuidProperty in (dc75698b-581d-488b-9638-3e28dd51d8f7)", "$it => System.Collections.Generic.List`1[System.Guid].Contains($it.Category.Product.GuidProperty)", "$it => System.Collections.Generic.List`1[System.Guid].Cast().Contains(IIF((IIF(($it.Category == null), null, $it.Category.Product) == null), null, Convert($it.Category.Product.GuidProperty)))")] - [InlineData("Category/Product/NullableGuidProperty in (dc75698b-581d-488b-9638-3e28dd51d8f7)", "$it => System.Collections.Generic.List`1[System.Nullable`1[System.Guid]].Contains($it.Category.Product.NullableGuidProperty)", "$it => System.Collections.Generic.List`1[System.Nullable`1[System.Guid]].Contains(IIF((IIF(($it.Category == null), null, $it.Category.Product) == null), null, $it.Category.Product.NullableGuidProperty))")] - public void InOnNavigation(string filter, string expression, string expressionWithNullPropagation) - { - // Arrange & Act & Assert - BindFilterAndVerify(filter, expression, expressionWithNullPropagation); - } + [Theory] + [InlineData("Category/Product/ProductID in (1)", "$it => System.Collections.Generic.List`1[System.Int32].Contains($it.Category.Product.ProductID)", "$it => System.Collections.Generic.List`1[System.Int32].Cast().Contains(IIF((IIF(($it.Category == null), null, $it.Category.Product) == null), null, Convert($it.Category.Product.ProductID)))")] + [InlineData("Category/Product/GuidProperty in (dc75698b-581d-488b-9638-3e28dd51d8f7)", "$it => System.Collections.Generic.List`1[System.Guid].Contains($it.Category.Product.GuidProperty)", "$it => System.Collections.Generic.List`1[System.Guid].Cast().Contains(IIF((IIF(($it.Category == null), null, $it.Category.Product) == null), null, Convert($it.Category.Product.GuidProperty)))")] + [InlineData("Category/Product/NullableGuidProperty in (dc75698b-581d-488b-9638-3e28dd51d8f7)", "$it => System.Collections.Generic.List`1[System.Nullable`1[System.Guid]].Contains($it.Category.Product.NullableGuidProperty)", "$it => System.Collections.Generic.List`1[System.Nullable`1[System.Guid]].Contains(IIF((IIF(($it.Category == null), null, $it.Category.Product) == null), null, $it.Category.Product.NullableGuidProperty))")] + public void InOnNavigation(string filter, string expression, string expressionWithNullPropagation) + { + // Arrange & Act & Assert + BindFilterAndVerify(filter, expression, expressionWithNullPropagation); + } - [Fact] - public void MultipleConstants_Are_Parameterized() - { - // Arrange & Act & Assert - BindFilterAndVerify("ProductName eq '1' or ProductName eq '2' or ProductName eq '3' or ProductName eq '4'", - "$it => (((($it.ProductName == \"1\") OrElse ($it.ProductName == \"2\")) OrElse ($it.ProductName == \"3\")) OrElse ($it.ProductName == \"4\"))", - NotTesting); - } + [Fact] + public void MultipleConstants_Are_Parameterized() + { + // Arrange & Act & Assert + BindFilterAndVerify("ProductName eq '1' or ProductName eq '2' or ProductName eq '3' or ProductName eq '4'", + "$it => (((($it.ProductName == \"1\") OrElse ($it.ProductName == \"2\")) OrElse ($it.ProductName == \"3\")) OrElse ($it.ProductName == \"4\"))", + NotTesting); + } - [Fact] - public void Constants_Are_Not_Parameterized_IfDisabled() - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify("ProductName eq '1'", settingsCustomizer: (settings) => - { - settings.EnableConstantParameterization = false; - }); + [Fact] + public void Constants_Are_Not_Parameterized_IfDisabled() + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify("ProductName eq '1'", settingsCustomizer: (settings) => + { + settings.EnableConstantParameterization = false; + }); - Assert.Equal("$it => ($it.ProductName == \"1\")", (filters.Item1 as Expression).ToString()); - } + Assert.Equal("$it => ($it.ProductName == \"1\")", (filters.Item1 as Expression).ToString()); + } - [Fact] - public void CollectionConstants_Are_Parameterized() - { - // Arrange & Act & Assert - var result = BindFilterAndVerify("ProductName in ('Prod1', 'Prod2')", - "$it => System.Collections.Generic.List`1[System.String].Contains($it.ProductName)"); + [Fact] + public void CollectionConstants_Are_Parameterized() + { + // Arrange & Act & Assert + var result = BindFilterAndVerify("ProductName in ('Prod1', 'Prod2')", + "$it => System.Collections.Generic.List`1[System.String].Contains($it.ProductName)"); - Expression> expression = result.Item2 as Expression>; + Expression> expression = result.Item2 as Expression>; - var memberAccess = (MemberExpression)((MethodCallExpression)expression.Body).Arguments[0]; - var values = (IList)ExpressionBinderHelper.ExtractParameterizedConstant(memberAccess); - Assert.Equal(new[] { "Prod1", "Prod2" }, values); - } + var memberAccess = (MemberExpression)((MethodCallExpression)expression.Body).Arguments[0]; + var values = (IList)ExpressionBinderHelper.ExtractParameterizedConstant(memberAccess); + Assert.Equal(new[] { "Prod1", "Prod2" }, values); + } - [Fact] - public void CollectionConstants_Are_Not_Parameterized_If_Disabled() - { - // Arrange & Act & Assert - var result = BindFilterAndVerify("ProductName in ('Prod1', 'Prod2')", - "$it => System.Collections.Generic.List`1[System.String].Contains($it.ProductName)", - settingsCustomizer: (settings) => - { - settings.EnableConstantParameterization = false; - }); + [Fact] + public void CollectionConstants_Are_Not_Parameterized_If_Disabled() + { + // Arrange & Act & Assert + var result = BindFilterAndVerify("ProductName in ('Prod1', 'Prod2')", + "$it => System.Collections.Generic.List`1[System.String].Contains($it.ProductName)", + settingsCustomizer: (settings) => + { + settings.EnableConstantParameterization = false; + }); - Expression> expression = result.Item2 as Expression>; - var values = (IList)((ConstantExpression)((MethodCallExpression)expression.Body).Arguments[0]).Value; - Assert.Equal(new[] { "Prod1", "Prod2" }, values); - } + Expression> expression = result.Item2 as Expression>; + var values = (IList)((ConstantExpression)((MethodCallExpression)expression.Body).Arguments[0]).Value; + Assert.Equal(new[] { "Prod1", "Prod2" }, values); + } - [Fact] - public void CollectionConstants_OfEnums_Are_Not_Parameterized_If_Disabled() - { - // Arrange & Act & Assert - var result = BindFilterAndVerify( - "SimpleEnumProp in ('First', 'Second')", - "$it => System.Collections.Generic.List`1[Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum].Contains($it.SimpleEnumProp)", - settingsCustomizer: (settings) => - { - settings.EnableConstantParameterization = false; - }); + [Fact] + public void CollectionConstants_OfEnums_Are_Not_Parameterized_If_Disabled() + { + // Arrange & Act & Assert + var result = BindFilterAndVerify( + "SimpleEnumProp in ('First', 'Second')", + "$it => System.Collections.Generic.List`1[Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum].Contains($it.SimpleEnumProp)", + settingsCustomizer: (settings) => + { + settings.EnableConstantParameterization = false; + }); - Expression> expression = result.Item2 as Expression>; - var values = (IList)((ConstantExpression)((MethodCallExpression)expression.Body).Arguments[0]).Value; - Assert.Equal(new[] { SimpleEnum.First, SimpleEnum.Second }, values); - } + Expression> expression = result.Item2 as Expression>; + var values = (IList)((ConstantExpression)((MethodCallExpression)expression.Body).Arguments[0]).Value; + Assert.Equal(new[] { SimpleEnum.First, SimpleEnum.Second }, values); + } - [Fact] - public void FilterByDynamicProperty() - { - // Arrange & Act & Assert - BindFilterAndVerify("Token eq '1'", - "$it => (Convert(IIF($it.ProductProperties.ContainsKey(Token), $it.ProductPropertiesToken, null)) == \"1\")", - "$it => (Convert(IIF((($it.ProductProperties != null) AndAlso $it.ProductProperties.ContainsKey(Token)), $it.ProductPropertiesToken, null)) == \"1\")"); - } + [Fact] + public void FilterByDynamicProperty() + { + // Arrange & Act & Assert + BindFilterAndVerify("Token eq '1'", + "$it => (Convert(IIF($it.ProductProperties.ContainsKey(Token), $it.ProductPropertiesToken, null)) == \"1\")", + "$it => (Convert(IIF((($it.ProductProperties != null) AndAlso $it.ProductProperties.ContainsKey(Token)), $it.ProductPropertiesToken, null)) == \"1\")"); + } - [Theory] - [InlineData(new[] { 1, 2, 42 }, true)] - [InlineData(new[] { 1, 2 }, false)] - public void InOnPrimitiveCollectionPropertyOnRHS(int[] alternateIds, bool withNullPropagation) - { - // Arrange & Act & Assert - var filters = BindFilterAndVerify( - "42 in AlternateIDs", - "$it => $it.AlternateIDs.Contains(42)", - NotTesting); - - // Arrange & Act & Assert - InvokeFiltersAndVerify(filters, new Product { AlternateIDs = alternateIds }, (withNullPropagation, withNullPropagation)); - } + [Theory] + [InlineData(new[] { 1, 2, 42 }, true)] + [InlineData(new[] { 1, 2 }, false)] + public void InOnPrimitiveCollectionPropertyOnRHS(int[] alternateIds, bool withNullPropagation) + { + // Arrange & Act & Assert + var filters = BindFilterAndVerify( + "42 in AlternateIDs", + "$it => $it.AlternateIDs.Contains(42)", + NotTesting); + + // Arrange & Act & Assert + InvokeFiltersAndVerify(filters, new Product { AlternateIDs = alternateIds }, (withNullPropagation, withNullPropagation)); + } - [Theory] - [InlineData("AlternateAddresses/$count gt 2", "$it => ($it.AlternateAddresses.LongCount() > 2)", "$it => ((IIF(($it.AlternateAddresses == null), null, Convert($it.AlternateAddresses.LongCount())) > Convert(2)) == True)")] // Products?$filter=AlternateAddresses/$count gt 2 - [InlineData("Category/Products/$count($filter=ProductID gt 2) gt 2", "$it => ($it.Category.Products.Where($it => ($it.ProductID > 2)).LongCount() > 2)", "$it => ((IIF((IIF(($it.Category == null), null, $it.Category.Products).Where($it => ($it.ProductID > 2)) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.Products).Where($it => ($it.ProductID > 2)).LongCount())) > Convert(2)) == True)")] // Products?$filter=Category/Products/$count($filter=ProductID gt 2) gt 2 - public void CountExpression(string clause, string expectedExpression, string expectedExpressionWithNullPropagation) - { - // Arrange & Act & Assert - BindFilterAndVerify( - clause, - expectedExpression, - expectedExpressionWithNullPropagation); - } + [Theory] + [InlineData("AlternateAddresses/$count gt 2", "$it => ($it.AlternateAddresses.LongCount() > 2)", "$it => ((IIF(($it.AlternateAddresses == null), null, Convert($it.AlternateAddresses.LongCount())) > Convert(2)) == True)")] // Products?$filter=AlternateAddresses/$count gt 2 + [InlineData("Category/Products/$count($filter=ProductID gt 2) gt 2", "$it => ($it.Category.Products.Where($it => ($it.ProductID > 2)).LongCount() > 2)", "$it => ((IIF((IIF(($it.Category == null), null, $it.Category.Products).Where($it => ($it.ProductID > 2)) == null), null, Convert(IIF(($it.Category == null), null, $it.Category.Products).Where($it => ($it.ProductID > 2)).LongCount())) > Convert(2)) == True)")] // Products?$filter=Category/Products/$count($filter=ProductID gt 2) gt 2 + public void CountExpression(string clause, string expectedExpression, string expectedExpressionWithNullPropagation) + { + // Arrange & Act & Assert + BindFilterAndVerify( + clause, + expectedExpression, + expectedExpressionWithNullPropagation); + } #region Negative Tests - [Fact] - public void TypeMismatchInComparison() - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => BindFilterAndVerify("length(123) eq 12")); - } + [Fact] + public void TypeMismatchInComparison() + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => BindFilterAndVerify("length(123) eq 12")); + } #endregion #region Helpers - internal static void InvokeFiltersAndThrows((Expression, Expression) filters, T instance, (Type, bool) expectedValue) - { - ExceptionAssert.Throws(expectedValue.Item1, () => InvokeFilter(instance, filters.Item1)); + internal static void InvokeFiltersAndThrows((Expression, Expression) filters, T instance, (Type, bool) expectedValue) + { + ExceptionAssert.Throws(expectedValue.Item1, () => InvokeFilter(instance, filters.Item1)); - bool expected = InvokeFilter(instance, filters.Item2); - Assert.Equal(expectedValue.Item2, expected); - } + bool expected = InvokeFilter(instance, filters.Item2); + Assert.Equal(expectedValue.Item2, expected); + } - internal static void InvokeFiltersAndVerify((Expression, Expression) filters, T instance, (bool, bool) expectedValue) - { - bool expected = InvokeFilter(instance, filters.Item1); - Assert.Equal(expectedValue.Item1, expected); + internal static void InvokeFiltersAndVerify((Expression, Expression) filters, T instance, (bool, bool) expectedValue) + { + bool expected = InvokeFilter(instance, filters.Item1); + Assert.Equal(expectedValue.Item1, expected); - expected = InvokeFilter(instance, filters.Item2); - Assert.Equal(expectedValue.Item2, expected); - } + expected = InvokeFilter(instance, filters.Item2); + Assert.Equal(expectedValue.Item2, expected); + } - public static bool InvokeFilter(T instance, Expression filter) - { - Expression> filterExpression = filter as Expression>; - Assert.NotNull(filterExpression); + public static bool InvokeFilter(T instance, Expression filter) + { + Expression> filterExpression = filter as Expression>; + Assert.NotNull(filterExpression); - return filterExpression.Compile().Invoke(instance); - } + return filterExpression.Compile().Invoke(instance); + } - internal static Expression BindFilter(IEdmModel model, FilterClause filterClause, Type elementType, ODataQuerySettings querySettings, IAssemblyResolver resolver = null) + internal static Expression BindFilter(IEdmModel model, FilterClause filterClause, Type elementType, ODataQuerySettings querySettings, IAssemblyResolver resolver = null) + { + IFilterBinder binder = new FilterBinder(); + QueryBinderContext context = new QueryBinderContext(model, querySettings, elementType) { - IFilterBinder binder = new FilterBinder(); - QueryBinderContext context = new QueryBinderContext(model, querySettings, elementType) - { - AssembliesResolver = resolver ?? AssemblyResolverHelper.Default, - }; + AssembliesResolver = resolver ?? AssemblyResolverHelper.Default, + }; - return binder.BindFilter(filterClause, context); - } + return binder.BindFilter(filterClause, context); + } - /// - /// Returns (FalseNullProp Expression, TrueNullProp Expression) - /// If expectedTrueNullPropagation is same as expectedFalseNullPropagation, don't need provide its value. - /// - internal static (Expression, Expression) BindFilterAndVerify(string filter, - string expectedFalseNullPropagation = null, - string expectedTrueNullPropagation = null, - Action settingsCustomizer = null, - IAssemblyResolver assembliesResolver = null) where T : class - { - Type elementType = typeof(T); - IEdmModel model = GetModel(); - FilterClause filterClause = CreateFilterClause(filter, model, elementType); - Assert.NotNull(filterClause); + /// + /// Returns (FalseNullProp Expression, TrueNullProp Expression) + /// If expectedTrueNullPropagation is same as expectedFalseNullPropagation, don't need provide its value. + /// + internal static (Expression, Expression) BindFilterAndVerify(string filter, + string expectedFalseNullPropagation = null, + string expectedTrueNullPropagation = null, + Action settingsCustomizer = null, + IAssemblyResolver assembliesResolver = null) where T : class + { + Type elementType = typeof(T); + IEdmModel model = GetModel(); + FilterClause filterClause = CreateFilterClause(filter, model, elementType); + Assert.NotNull(filterClause); - // HandleNullPropagation == false - ODataQuerySettings querySettings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }; - settingsCustomizer?.Invoke(querySettings); + // HandleNullPropagation == false + ODataQuerySettings querySettings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }; + settingsCustomizer?.Invoke(querySettings); - Expression filterExprFalseNull = BindFilter(model, filterClause, elementType, querySettings, assembliesResolver); - if (expectedFalseNullPropagation != null && expectedFalseNullPropagation != NotTesting) - { - VerifyExpression(filterExprFalseNull, expectedFalseNullPropagation); - } + Expression filterExprFalseNull = BindFilter(model, filterClause, elementType, querySettings, assembliesResolver); + if (expectedFalseNullPropagation != null && expectedFalseNullPropagation != NotTesting) + { + VerifyExpression(filterExprFalseNull, expectedFalseNullPropagation); + } - // HandleNullPropagation == true - querySettings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }; - settingsCustomizer?.Invoke(querySettings); - Expression filterExprTrueNull = BindFilter(model, filterClause, elementType, querySettings, assembliesResolver); + // HandleNullPropagation == true + querySettings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }; + settingsCustomizer?.Invoke(querySettings); + Expression filterExprTrueNull = BindFilter(model, filterClause, elementType, querySettings, assembliesResolver); - if (expectedTrueNullPropagation != NotTesting) + if (expectedTrueNullPropagation != NotTesting) + { + string nullPropagation = expectedTrueNullPropagation ?? expectedFalseNullPropagation; // Same expected + if (nullPropagation != null) { - string nullPropagation = expectedTrueNullPropagation ?? expectedFalseNullPropagation; // Same expected - if (nullPropagation != null) - { - VerifyExpression(filterExprTrueNull, nullPropagation); - } + VerifyExpression(filterExprTrueNull, nullPropagation); } - - return (filterExprFalseNull, filterExprTrueNull); } - private static void VerifyExpression(Expression filter, string expectedExpression) - { - // strip off the beginning part of the expression to get to the first - // actual query operator - string resultExpression = ExpressionStringBuilder.ToString(filter); - Assert.True(resultExpression == expectedExpression, - string.Format("Expected expression '{0}' but the deserializer produced '{1}'", expectedExpression, resultExpression)); - } + return (filterExprFalseNull, filterExprTrueNull); + } - private static FilterClause CreateFilterClause(string filter, IEdmModel model, Type type) - { - IEdmEntityType entityType = model.SchemaElements.OfType().Single(t => t.Name == type.Name); - Assert.NotNull(entityType); // Guard + private static void VerifyExpression(Expression filter, string expectedExpression) + { + // strip off the beginning part of the expression to get to the first + // actual query operator + string resultExpression = ExpressionStringBuilder.ToString(filter); + Assert.True(resultExpression == expectedExpression, + string.Format("Expected expression '{0}' but the deserializer produced '{1}'", expectedExpression, resultExpression)); + } - IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Entities"); - Assert.NotNull(entitySet); // Guard + private static FilterClause CreateFilterClause(string filter, IEdmModel model, Type type) + { + IEdmEntityType entityType = model.SchemaElements.OfType().Single(t => t.Name == type.Name); + Assert.NotNull(entityType); // Guard - ODataQueryOptionParser parser = new ODataQueryOptionParser(model, entityType, entitySet, - new Dictionary { { "$filter", filter } }); + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Entities"); + Assert.NotNull(entitySet); // Guard - return parser.ParseFilter(); - } + ODataQueryOptionParser parser = new ODataQueryOptionParser(model, entityType, entitySet, + new Dictionary { { "$filter", filter } }); - private static IEdmModel GetModel() where T : class - { - Type key = typeof(T); - IEdmModel value; + return parser.ParseFilter(); + } - if (!_modelCache.TryGetValue(key, out value)) - { - ODataModelBuilder model = new ODataConventionModelBuilder(); - model.EntitySet("Entities"); - if (key == typeof(Product)) - { - model.EntityType().DerivesFrom(); - model.EntityType().DerivesFrom(); - } + private static IEdmModel GetModel() where T : class + { + Type key = typeof(T); + IEdmModel value; - value = _modelCache[key] = model.GetEdmModel(); + if (!_modelCache.TryGetValue(key, out value)) + { + ODataModelBuilder model = new ODataConventionModelBuilder(); + model.EntitySet("Entities"); + if (key == typeof(Product)) + { + model.EntityType().DerivesFrom(); + model.EntityType().DerivesFrom(); } - return value; + value = _modelCache[key] = model.GetEdmModel(); } - private T? ToNullable(object value) where T : struct - { - return value == null ? null : (T?)Convert.ChangeType(value, typeof(T)); - } - - // Used by Custom Method binder tests - by reflection - private string PadRightInstance(string str, int number) - { - return str.PadRight(number); - } + return value; + } - // Used by Custom Method binder tests - by reflection - private static string PadRightStatic(string str, int number) - { - return str.PadRight(number); - } + private T? ToNullable(object value) where T : struct + { + return value == null ? null : (T?)Convert.ChangeType(value, typeof(T)); + } - #endregion + // Used by Custom Method binder tests - by reflection + private string PadRightInstance(string str, int number) + { + return str.PadRight(number); } // Used by Custom Method binder tests - by reflection - public static class StringExtender + private static string PadRightStatic(string str, int number) { - public static string PadRightExStatic(this string str, int width) - { - return str.PadRight(width); - } + return str.PadRight(number); + } + + #endregion +} + +// Used by Custom Method binder tests - by reflection +public static class StringExtender +{ + public static string PadRightExStatic(this string str, int width) + { + return str.PadRight(width); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTestsHelper.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTestsHelper.cs index 1af89da45..533a6cccb 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTestsHelper.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTestsHelper.cs @@ -13,46 +13,45 @@ using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; -namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions +namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions; + +public static class FilterBinderTestsHelper { - public static class FilterBinderTestsHelper + public static Expression TestBind(FilterClause filterClause, Type filterType, IEdmModel model, + IAssemblyResolver assembliesResolver, ODataQuerySettings querySettings) { - public static Expression TestBind(FilterClause filterClause, Type filterType, IEdmModel model, - IAssemblyResolver assembliesResolver, ODataQuerySettings querySettings) + if (filterClause == null) { - if (filterClause == null) - { - throw Error.ArgumentNull(nameof(filterClause)); - } - - if (filterType == null) - { - throw Error.ArgumentNull(nameof(filterType)); - } - - if (model == null) - { - throw Error.ArgumentNull(nameof(model)); - } - - if (assembliesResolver == null) - { - throw Error.ArgumentNull(nameof(assembliesResolver)); - } - - IFilterBinder binder = new FilterBinder(); - - QueryBinderContext context = new QueryBinderContext(model, querySettings, filterType) - { - AssembliesResolver = assembliesResolver, - }; - - return binder.BindFilter(filterClause, context); + throw Error.ArgumentNull(nameof(filterClause)); } - } - public class MyNoneQueryNode : QueryNode - { - public override QueryNodeKind Kind => QueryNodeKind.None; + if (filterType == null) + { + throw Error.ArgumentNull(nameof(filterType)); + } + + if (model == null) + { + throw Error.ArgumentNull(nameof(model)); + } + + if (assembliesResolver == null) + { + throw Error.ArgumentNull(nameof(assembliesResolver)); + } + + IFilterBinder binder = new FilterBinder(); + + QueryBinderContext context = new QueryBinderContext(model, querySettings, filterType) + { + AssembliesResolver = assembliesResolver, + }; + + return binder.BindFilter(filterClause, context); } } + +public class MyNoneQueryNode : QueryNode +{ + public override QueryNodeKind Kind => QueryNodeKind.None; +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/Linq2ObjectsComparisonMethodsTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/Linq2ObjectsComparisonMethodsTest.cs index d76124b4d..b0e9b832e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/Linq2ObjectsComparisonMethodsTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/Linq2ObjectsComparisonMethodsTest.cs @@ -9,42 +9,41 @@ using Microsoft.AspNetCore.OData.TestCommon; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions +namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions; + +public class Linq2ObjectsComparisonMethodsTest { - public class Linq2ObjectsComparisonMethodsTest + public static TheoryDataSet AreByteArraysEqualDataset { - public static TheoryDataSet AreByteArraysEqualDataset + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { new byte[] { 1, 2, 3}, new byte[] { 1, 2, 3} , true}, - { new byte[] { 1,2,3}, new byte[] { 1, 2} , false}, - { new byte[] { 1,2}, new byte[] { 1, 2, 3} , false}, - { null, new byte[] { 1, 2, 3} , false}, - { new byte[] { 1, 2, 3}, null , false}, - { null, null , true}, - }; - } + { new byte[] { 1, 2, 3}, new byte[] { 1, 2, 3} , true}, + { new byte[] { 1,2,3}, new byte[] { 1, 2} , false}, + { new byte[] { 1,2}, new byte[] { 1, 2, 3} , false}, + { null, new byte[] { 1, 2, 3} , false}, + { new byte[] { 1, 2, 3}, null , false}, + { null, null , true}, + }; } + } - [Theory] - [MemberData(nameof(AreByteArraysEqualDataset))] - public void AreByteArraysEqual(byte[] left, byte[] right, bool result) - { - Assert.Equal( - result, - Linq2ObjectsComparisonMethods.AreByteArraysEqual(left, right)); - } + [Theory] + [MemberData(nameof(AreByteArraysEqualDataset))] + public void AreByteArraysEqual(byte[] left, byte[] right, bool result) + { + Assert.Equal( + result, + Linq2ObjectsComparisonMethods.AreByteArraysEqual(left, right)); + } - [Theory] - [MemberData(nameof(AreByteArraysEqualDataset))] - public void AreByteArraysNotEqual(byte[] left, byte[] right, bool result) - { - Assert.Equal( - !result, - Linq2ObjectsComparisonMethods.AreByteArraysNotEqual(left, right)); - } + [Theory] + [MemberData(nameof(AreByteArraysEqualDataset))] + public void AreByteArraysNotEqual(byte[] left, byte[] right, bool result) + { + Assert.Equal( + !result, + Linq2ObjectsComparisonMethods.AreByteArraysNotEqual(left, right)); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/OrderByBinderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/OrderByBinderTests.cs index 02f79988a..a787fffb9 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/OrderByBinderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/OrderByBinderTests.cs @@ -20,337 +20,336 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions +namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions; + +public class OrderByBinderTests { - public class OrderByBinderTests - { - private static readonly MethodInfo _orderbyGenericMethod - = typeof(Enumerable).GetMethods().First(x => x.Name == "OrderBy" && x.GetParameters().Length == 2); + private static readonly MethodInfo _orderbyGenericMethod + = typeof(Enumerable).GetMethods().First(x => x.Name == "OrderBy" && x.GetParameters().Length == 2); - private static readonly MethodInfo _orderByDescendingGenericMethod - = typeof(Enumerable).GetMethods().First(x => x.Name == "OrderByDescending" && x.GetParameters().Length == 2); + private static readonly MethodInfo _orderByDescendingGenericMethod + = typeof(Enumerable).GetMethods().First(x => x.Name == "OrderByDescending" && x.GetParameters().Length == 2); - private static readonly MethodInfo _thenByGenericMethod - = typeof(Enumerable).GetMethods().First(x => x.Name == "ThenBy" && x.GetParameters().Length == 2); + private static readonly MethodInfo _thenByGenericMethod + = typeof(Enumerable).GetMethods().First(x => x.Name == "ThenBy" && x.GetParameters().Length == 2); - private static readonly MethodInfo _thenByDescendingGenericMethod - = typeof(Enumerable).GetMethods().First(x => x.Name == "ThenByDescending" && x.GetParameters().Length == 2); + private static readonly MethodInfo _thenByDescendingGenericMethod + = typeof(Enumerable).GetMethods().First(x => x.Name == "ThenByDescending" && x.GetParameters().Length == 2); - private static readonly ODataQuerySettings _defaultSettings = new ODataQuerySettings - { - HandleNullPropagation = HandleNullPropagationOption.False - }; + private static readonly ODataQuerySettings _defaultSettings = new ODataQuerySettings + { + HandleNullPropagation = HandleNullPropagationOption.False + }; - private static IEdmModel _model = GetEdmModel(); + private static IEdmModel _model = GetEdmModel(); - [Fact] - public void OrderByBinder_Binds_Throws_InputParameters() - { - // Arrange - IOrderByBinder binder = new OrderByBinder(); + [Fact] + public void OrderByBinder_Binds_Throws_InputParameters() + { + // Arrange + IOrderByBinder binder = new OrderByBinder(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindOrderBy(null, null), "orderByClause"); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindOrderBy(null, null), "orderByClause"); - // Act & Assert - SingleValueNode expression = new Mock().Object; - RangeVariable range = new Mock().Object; - OrderByClause orderByClause = new OrderByClause(null, expression, OrderByDirection.Descending, range); - ExceptionAssert.ThrowsArgumentNull(() => binder.BindOrderBy(orderByClause, null), "context"); - } + // Act & Assert + SingleValueNode expression = new Mock().Object; + RangeVariable range = new Mock().Object; + OrderByClause orderByClause = new OrderByClause(null, expression, OrderByDirection.Descending, range); + ExceptionAssert.ThrowsArgumentNull(() => binder.BindOrderBy(orderByClause, null), "context"); + } - [Fact] - public void OrderByBinder_Binds_DirectStringProperty_WorksAsExpected() + [Fact] + public void OrderByBinder_Binds_DirectStringProperty_WorksAsExpected() + { + // Arrange + IEnumerable products = new[] { - // Arrange - IEnumerable products = new[] - { - new Product { ProductID = 1, ProductName = "a2" }, - new Product { ProductID = 2, ProductName = "a1" }, - new Product { ProductID = 3, ProductName = "a3" }, - }; + new Product { ProductID = 1, ProductName = "a2" }, + new Product { ProductID = 2, ProductName = "a1" }, + new Product { ProductID = 3, ProductName = "a3" }, + }; - string orderBy = "ProductName"; + string orderBy = "ProductName"; - string expectedExpr = "$it => $it.ProductName"; + string expectedExpr = "$it => $it.ProductName"; - // Act & Assert -- Ascending - RunOrderByTestAndVerify(products, orderBy, new[] { 2, 1, 3 }, expectedExpr); + // Act & Assert -- Ascending + RunOrderByTestAndVerify(products, orderBy, new[] { 2, 1, 3 }, expectedExpr); - // Act & Assert -- Descending - RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 3, 1, 2 }, expectedExpr); - } + // Act & Assert -- Descending + RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 3, 1, 2 }, expectedExpr); + } - [Fact] - public void OrderByBinder_Binds_DirectIntProperty_WorksAsExpected() + [Fact] + public void OrderByBinder_Binds_DirectIntProperty_WorksAsExpected() + { + // Arrange + IEnumerable products = new[] { - // Arrange - IEnumerable products = new[] - { - new Product { ProductID = 1, CategoryID = 4 }, - new Product { ProductID = 2, CategoryID = 2 }, - new Product { ProductID = 3, CategoryID = 7 }, - }; + new Product { ProductID = 1, CategoryID = 4 }, + new Product { ProductID = 2, CategoryID = 2 }, + new Product { ProductID = 3, CategoryID = 7 }, + }; - string orderBy = "CategoryID"; + string orderBy = "CategoryID"; - string expectedExpr = "$it => $it.CategoryID"; + string expectedExpr = "$it => $it.CategoryID"; - // Act & Assert -- Ascending - RunOrderByTestAndVerify(products, orderBy, new[] { 2, 1, 3 }, expectedExpr); + // Act & Assert -- Ascending + RunOrderByTestAndVerify(products, orderBy, new[] { 2, 1, 3 }, expectedExpr); - // Act & Assert -- Descending - RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 3, 1, 2 }, expectedExpr); - } + // Act & Assert -- Descending + RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 3, 1, 2 }, expectedExpr); + } - [Fact] - public void OrderByBinder_Binds_DerivedStringFacterty_WorksAsExpected() + [Fact] + public void OrderByBinder_Binds_DerivedStringFacterty_WorksAsExpected() + { + // Arrange + IEnumerable products = new[] { - // Arrange - IEnumerable products = new[] - { - new Product { ProductID = 1, SupplierAddress = new Address { State = "WA" } }, - new Product { ProductID = 2, SupplierAddress = new Address { State = "ZA" } }, - new Product { ProductID = 3, SupplierAddress = new Address { State = "EA" } }, - }; + new Product { ProductID = 1, SupplierAddress = new Address { State = "WA" } }, + new Product { ProductID = 2, SupplierAddress = new Address { State = "ZA" } }, + new Product { ProductID = 3, SupplierAddress = new Address { State = "EA" } }, + }; - string orderBy = "SupplierAddress/State"; + string orderBy = "SupplierAddress/State"; - string expectedExpr = "$it => $it.SupplierAddress.State"; + string expectedExpr = "$it => $it.SupplierAddress.State"; - // Act & Assert -- Ascending - RunOrderByTestAndVerify(products, orderBy, new[] { 3, 1, 2 }, expectedExpr); + // Act & Assert -- Ascending + RunOrderByTestAndVerify(products, orderBy, new[] { 3, 1, 2 }, expectedExpr); - // Act & Assert -- Descending - RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 2, 1, 3 }, expectedExpr); - } + // Act & Assert -- Descending + RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 2, 1, 3 }, expectedExpr); + } - [Fact] - public void OrderByBinder_Binds_UsingDollarCountOnCollectionProperty_WorksAsExpected() + [Fact] + public void OrderByBinder_Binds_UsingDollarCountOnCollectionProperty_WorksAsExpected() + { + // Arrange + IEnumerable products = new[] { - // Arrange - IEnumerable products = new[] - { - new Product { ProductID = 1, AlternateIDs = new[] { 1, 2, 3 }}, // 3 items - new Product { ProductID = 2, AlternateIDs = new[] { 1, 2, 3, 5, 6}}, // 5 items - new Product { ProductID = 3, AlternateIDs = new[] { 1, 2 }} // 2 items - }; + new Product { ProductID = 1, AlternateIDs = new[] { 1, 2, 3 }}, // 3 items + new Product { ProductID = 2, AlternateIDs = new[] { 1, 2, 3, 5, 6}}, // 5 items + new Product { ProductID = 3, AlternateIDs = new[] { 1, 2 }} // 2 items + }; - string orderBy = "AlternateIDs/$count"; + string orderBy = "AlternateIDs/$count"; - string expectedExpr = "$it => $it.AlternateIDs.LongCount()"; + string expectedExpr = "$it => $it.AlternateIDs.LongCount()"; - // Act & Assert -- Ascending - RunOrderByTestAndVerify(products, orderBy, new[] { 3, 1, 2 }, expectedExpr); + // Act & Assert -- Ascending + RunOrderByTestAndVerify(products, orderBy, new[] { 3, 1, 2 }, expectedExpr); - // Act & Assert -- Descending - RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 2, 1, 3 }, expectedExpr); - } + // Act & Assert -- Descending + RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 2, 1, 3 }, expectedExpr); + } - [Fact] - public void OrderByBinder_Binds_UsingDollarCountWithFilterOnCollectionProperty_WorksAsExpected() + [Fact] + public void OrderByBinder_Binds_UsingDollarCountWithFilterOnCollectionProperty_WorksAsExpected() + { + // Arrange + IEnumerable products = new[] { - // Arrange - IEnumerable products = new[] - { - new Product { ProductID = 1, AlternateAddresses = new[] { new Address { HouseNumber = 9 }, new Address { HouseNumber = 10 } }}, // 2 addresses matched - new Product { ProductID = 2, AlternateAddresses = new[] { new Address { HouseNumber = 2 }, new Address { HouseNumber = 3 } }}, // 0 matched - new Product { ProductID = 3, AlternateAddresses = new[] { new Address { HouseNumber = 1 }, new Address { HouseNumber = 11 } }} // 1 matched - }; + new Product { ProductID = 1, AlternateAddresses = new[] { new Address { HouseNumber = 9 }, new Address { HouseNumber = 10 } }}, // 2 addresses matched + new Product { ProductID = 2, AlternateAddresses = new[] { new Address { HouseNumber = 2 }, new Address { HouseNumber = 3 } }}, // 0 matched + new Product { ProductID = 3, AlternateAddresses = new[] { new Address { HouseNumber = 1 }, new Address { HouseNumber = 11 } }} // 1 matched + }; - string orderBy = "AlternateAddresses/$count($filter=HouseNumber gt 8)"; + string orderBy = "AlternateAddresses/$count($filter=HouseNumber gt 8)"; - string expectedExpr = "$it => $it.AlternateAddresses.Where($it => ($it.HouseNumber > 8)).LongCount()"; + string expectedExpr = "$it => $it.AlternateAddresses.Where($it => ($it.HouseNumber > 8)).LongCount()"; - // Act & Assert -- Ascending - RunOrderByTestAndVerify(products, orderBy, new[] { 2, 3, 1 }, expectedExpr); + // Act & Assert -- Ascending + RunOrderByTestAndVerify(products, orderBy, new[] { 2, 3, 1 }, expectedExpr); - // Act & Assert -- Descending - RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 1, 3, 2 }, expectedExpr); - } + // Act & Assert -- Descending + RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 1, 3, 2 }, expectedExpr); + } - [Fact] - public void OrderByBinder_Binds_PropertyOnDerivedType_WorksAsExpected() + [Fact] + public void OrderByBinder_Binds_PropertyOnDerivedType_WorksAsExpected() + { + // Arrange + IEnumerable products = new[] { - // Arrange - IEnumerable products = new[] - { - new DerivedProduct { ProductID = 1, DerivedProductName = "N3" }, - new DerivedProduct { ProductID = 2, DerivedProductName = "N1" }, - new DerivedProduct { ProductID = 3, DerivedProductName = "N2" } - }; + new DerivedProduct { ProductID = 1, DerivedProductName = "N3" }, + new DerivedProduct { ProductID = 2, DerivedProductName = "N1" }, + new DerivedProduct { ProductID = 3, DerivedProductName = "N2" } + }; - string orderBy = "NS.DerivedProduct/DerivedProductName"; + string orderBy = "NS.DerivedProduct/DerivedProductName"; - string expectedExpr = "$it => ($it As DerivedProduct).DerivedProductName"; + string expectedExpr = "$it => ($it As DerivedProduct).DerivedProductName"; - // Act & Assert -- Ascending - RunOrderByTestAndVerify(products, orderBy, new[] { 2, 3, 1 }, expectedExpr); + // Act & Assert -- Ascending + RunOrderByTestAndVerify(products, orderBy, new[] { 2, 3, 1 }, expectedExpr); - // Act & Assert -- Descending - RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 1, 3, 2 }, expectedExpr); - } + // Act & Assert -- Descending + RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 1, 3, 2 }, expectedExpr); + } - [Fact] - public void OrderByBinder_Binds_MultipleProperties_WorksAsExpected() + [Fact] + public void OrderByBinder_Binds_MultipleProperties_WorksAsExpected() + { + // Arrange + IEnumerable products = new[] { - // Arrange - IEnumerable products = new[] - { - new Product { ProductID = 1, ProductName = "grape" }, - new Product { ProductID = 2, ProductName = "passionfruit" }, - new Product { ProductID = 3, ProductName = "banana" }, - new Product { ProductID = 4, ProductName = "mango" }, - new Product { ProductID = 5, ProductName = "orange" }, - new Product { ProductID = 6, ProductName = "raspberry" }, - new Product { ProductID = 7, ProductName = "apple" }, - new Product { ProductID = 8, ProductName = "blueberry" }, - }; - - string orderBy = "length(ProductName),ProductName"; - - string expectedExpr1 = "$it => $it.ProductName.Length"; - string expectedExpr2 = "$it => $it.ProductName"; - - // Act & Assert -- Ascending, and have the following sorting: - /* - apple - grape - mango - banana - orange - blueberry - raspberry - passionfruit - */ - RunOrderByTestAndVerify(products, orderBy, new[] { 7, 1, 4, 3, 5, 8, 6, 2 }, expectedExpr1, expectedExpr2); - - // Act & Assert -- Descending - // Be noted, only desc the second one and have the following sorting: - /* - mango - grape - apple - orange - banana - raspberry - blueberry - passionfruit - */ - RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 4, 1, 7, 5, 3, 6, 8, 2 }, expectedExpr1, expectedExpr2); - } + new Product { ProductID = 1, ProductName = "grape" }, + new Product { ProductID = 2, ProductName = "passionfruit" }, + new Product { ProductID = 3, ProductName = "banana" }, + new Product { ProductID = 4, ProductName = "mango" }, + new Product { ProductID = 5, ProductName = "orange" }, + new Product { ProductID = 6, ProductName = "raspberry" }, + new Product { ProductID = 7, ProductName = "apple" }, + new Product { ProductID = 8, ProductName = "blueberry" }, + }; - #region Helpers - private void RunOrderByTestAndVerify(IEnumerable products, string orderBy, int[] expectedOrderedId, params string[] expectedExprs) - { - // Act - Bind string to Linq.Expression - OrderByBinderResult binderResult = BindOrderBy(orderBy, _model); - - // Assert - Assert.NotNull(binderResult); - - bool alreadyOrdered = false; - IList exprs = new List(); - OrderByBinderResult result = binderResult; - IEnumerable collections = products; - IOrderedEnumerable orderedProducts; - do - { - exprs.Add(ExpressionStringBuilder.ToString(result.OrderByExpression)); + string orderBy = "length(ProductName),ProductName"; + + string expectedExpr1 = "$it => $it.ProductName.Length"; + string expectedExpr2 = "$it => $it.ProductName"; + + // Act & Assert -- Ascending, and have the following sorting: + /* + apple + grape + mango + banana + orange + blueberry + raspberry + passionfruit + */ + RunOrderByTestAndVerify(products, orderBy, new[] { 7, 1, 4, 3, 5, 8, 6, 2 }, expectedExpr1, expectedExpr2); + + // Act & Assert -- Descending + // Be noted, only desc the second one and have the following sorting: + /* + mango + grape + apple + orange + banana + raspberry + blueberry + passionfruit + */ + RunOrderByTestAndVerify(products, $"{orderBy} desc", new[] { 4, 1, 7, 5, 3, 6, 8, 2 }, expectedExpr1, expectedExpr2); + } - // Act - Invoke Orderby - orderedProducts = InvokeOrderBy(collections, result.OrderByExpression, result.Direction, alreadyOrdered); + #region Helpers + private void RunOrderByTestAndVerify(IEnumerable products, string orderBy, int[] expectedOrderedId, params string[] expectedExprs) + { + // Act - Bind string to Linq.Expression + OrderByBinderResult binderResult = BindOrderBy(orderBy, _model); + + // Assert + Assert.NotNull(binderResult); + + bool alreadyOrdered = false; + IList exprs = new List(); + OrderByBinderResult result = binderResult; + IEnumerable collections = products; + IOrderedEnumerable orderedProducts; + do + { + exprs.Add(ExpressionStringBuilder.ToString(result.OrderByExpression)); - alreadyOrdered = true; + // Act - Invoke Orderby + orderedProducts = InvokeOrderBy(collections, result.OrderByExpression, result.Direction, alreadyOrdered); - result = result.ThenBy; + alreadyOrdered = true; - collections = orderedProducts; - } - while (result != null); + result = result.ThenBy; - // Assert - Assert.True(expectedExprs.SequenceEqual(exprs)); - Assert.True(expectedOrderedId.SequenceEqual(orderedProducts.Select(a => a.ProductID))); // ordered id + collections = orderedProducts; } + while (result != null); - public static IOrderedEnumerable InvokeOrderBy(IEnumerable collection, Expression orderByExpr, OrderByDirection direction, bool alreadyOrdered = false) - { - LambdaExpression orderByLambda = orderByExpr as LambdaExpression; - Assert.NotNull(orderByLambda); + // Assert + Assert.True(expectedExprs.SequenceEqual(exprs)); + Assert.True(expectedOrderedId.SequenceEqual(orderedProducts.Select(a => a.ProductID))); // ordered id + } - Type returnType = orderByLambda.Body.Type; - Delegate function = orderByLambda.Compile(); - Type type = typeof(T); - MethodInfo orderByMethod; + public static IOrderedEnumerable InvokeOrderBy(IEnumerable collection, Expression orderByExpr, OrderByDirection direction, bool alreadyOrdered = false) + { + LambdaExpression orderByLambda = orderByExpr as LambdaExpression; + Assert.NotNull(orderByLambda); - if (alreadyOrdered) + Type returnType = orderByLambda.Body.Type; + Delegate function = orderByLambda.Compile(); + Type type = typeof(T); + MethodInfo orderByMethod; + + if (alreadyOrdered) + { + if (direction == OrderByDirection.Ascending) { - if (direction == OrderByDirection.Ascending) - { - orderByMethod = _thenByGenericMethod.MakeGenericMethod(type, returnType); - } - else - { - orderByMethod = _thenByDescendingGenericMethod.MakeGenericMethod(type, returnType); - } - - return orderByMethod.Invoke(null, new object[] { collection, function }) as IOrderedEnumerable; + orderByMethod = _thenByGenericMethod.MakeGenericMethod(type, returnType); } else { - if (direction == OrderByDirection.Ascending) - { - orderByMethod = _orderbyGenericMethod.MakeGenericMethod(type, returnType); - } - else - { - orderByMethod = _orderByDescendingGenericMethod.MakeGenericMethod(type, returnType); - } - - return orderByMethod.Invoke(null, new object[] { collection, function }) as IOrderedEnumerable; + orderByMethod = _thenByDescendingGenericMethod.MakeGenericMethod(type, returnType); } - } - private static OrderByBinderResult BindOrderBy(string orderBy, IEdmModel model, ODataQuerySettings querySettings = null, IAssemblyResolver assembliesResolver = null) + return orderByMethod.Invoke(null, new object[] { collection, function }) as IOrderedEnumerable; + } + else { - Type elementType = typeof(T); - OrderByClause orderByClause = CreateOrderByNode(orderBy, model, elementType); - Assert.NotNull(orderByClause); - - querySettings = querySettings ?? _defaultSettings; - QueryBinderContext context = new QueryBinderContext(model, querySettings, elementType) + if (direction == OrderByDirection.Ascending) { - AssembliesResolver = assembliesResolver, - }; + orderByMethod = _orderbyGenericMethod.MakeGenericMethod(type, returnType); + } + else + { + orderByMethod = _orderByDescendingGenericMethod.MakeGenericMethod(type, returnType); + } - IOrderByBinder orderByBinder = new OrderByBinder(); - return orderByBinder.BindOrderBy(orderByClause, context); + return orderByMethod.Invoke(null, new object[] { collection, function }) as IOrderedEnumerable; } + } + + private static OrderByBinderResult BindOrderBy(string orderBy, IEdmModel model, ODataQuerySettings querySettings = null, IAssemblyResolver assembliesResolver = null) + { + Type elementType = typeof(T); + OrderByClause orderByClause = CreateOrderByNode(orderBy, model, elementType); + Assert.NotNull(orderByClause); - private static OrderByClause CreateOrderByNode(string orderBy, IEdmModel model, Type entityType) + querySettings = querySettings ?? _defaultSettings; + QueryBinderContext context = new QueryBinderContext(model, querySettings, elementType) { - IEdmEntityType productType = model.SchemaElements.OfType().Single(t => t.Name == entityType.Name); - Assert.NotNull(productType); // Guard + AssembliesResolver = assembliesResolver, + }; - IEdmEntitySet products = model.EntityContainer.FindEntitySet("Products"); - Assert.NotNull(products); // Guard + IOrderByBinder orderByBinder = new OrderByBinder(); + return orderByBinder.BindOrderBy(orderByClause, context); + } - ODataQueryOptionParser parser = new ODataQueryOptionParser(model, productType, products, - new Dictionary { { "$orderby", orderBy } }); + private static OrderByClause CreateOrderByNode(string orderBy, IEdmModel model, Type entityType) + { + IEdmEntityType productType = model.SchemaElements.OfType().Single(t => t.Name == entityType.Name); + Assert.NotNull(productType); // Guard - return parser.ParseOrderBy(); - } - #endregion + IEdmEntitySet products = model.EntityContainer.FindEntitySet("Products"); + Assert.NotNull(products); // Guard - private static IEdmModel GetEdmModel() - { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - builder.Namespace = "NS"; - builder.EntitySet("Products"); - builder.EntityType().DerivesFrom(); - builder.EntityType().DerivesFrom(); - return builder.GetEdmModel(); - } + ODataQueryOptionParser parser = new ODataQueryOptionParser(model, productType, products, + new Dictionary { { "$orderby", orderBy } }); + + return parser.ParseOrderBy(); + } + #endregion + + private static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.Namespace = "NS"; + builder.EntitySet("Products"); + builder.EntityType().DerivesFrom(); + builder.EntityType().DerivesFrom(); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/QueryBinderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/QueryBinderTests.cs index e580c2f1f..6750f18f7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/QueryBinderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/QueryBinderTests.cs @@ -16,158 +16,157 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions +namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions; + +/// +/// Tests to QueryBinder +/// +public class QueryBinderTests { - /// - /// Tests to QueryBinder - /// - public class QueryBinderTests + [Fact] + public void Bind_ThrowsArgumentNull_ForInputs() + { + // Arrange + QueryBinder binder = new MyQueryBinder(); + QueryNode node = new Mock().Object; + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.Bind(null, null), "node"); + ExceptionAssert.ThrowsArgumentNull(() => binder.Bind(node, null), "context"); + } + + [Fact] + public void Bind_ThrowsNotSupported_ForNotAcceptNode() + { + // Arrange + QueryBinder binder = new MyQueryBinder(); + Mock node = new Mock(); + node.Setup(c => c.Kind).Returns(QueryNodeKind.None); + + // Act & Assert + ExceptionAssert.Throws(() => binder.Bind(node.Object, new QueryBinderContext()), + "Binding OData QueryNode of kind 'None' is not supported by 'QueryBinder'."); + } + + [Fact] + public void BindCollectionNode_ThrowsNotSupported_ForNotAcceptNode() + { + // Arrange + QueryBinder binder = new MyQueryBinder(); + Mock node = new Mock(); + node.Setup(c => c.Kind).Returns(QueryNodeKind.None); + + // Act & Assert + ExceptionAssert.Throws(() => binder.BindCollectionNode(node.Object, new QueryBinderContext()), + "Binding OData QueryNode of kind 'None' is not supported by 'QueryBinder'."); + } + + [Fact] + public void BindSingleValueNode_ThrowsNotSupported_ForNotAcceptNode() + { + // Arrange + QueryBinder binder = new MyQueryBinder(); + Mock node = new Mock(); + node.Setup(c => c.Kind).Returns(QueryNodeKind.None); + + // Act & Assert + ExceptionAssert.Throws(() => binder.BindSingleValueNode(node.Object, new QueryBinderContext()), + "Binding OData QueryNode of kind 'None' is not supported by 'QueryBinder'."); + } + + [Fact] + public void BindRangeVariable_ThrowsArgumentNull_ForInputs() + { + // Arrange + QueryBinder binder = new MyQueryBinder(); + RangeVariable variable = new Mock().Object; + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindRangeVariable(null, null), "rangeVariable"); + ExceptionAssert.ThrowsArgumentNull(() => binder.BindRangeVariable(variable, null), "context"); + } + + [Fact] + public void BindSingleResourceFunctionCallNode_ThrowsNotSupported_ForNotAcceptNode() { - [Fact] - public void Bind_ThrowsArgumentNull_ForInputs() - { - // Arrange - QueryBinder binder = new MyQueryBinder(); - QueryNode node = new Mock().Object; - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.Bind(null, null), "node"); - ExceptionAssert.ThrowsArgumentNull(() => binder.Bind(node, null), "context"); - } - - [Fact] - public void Bind_ThrowsNotSupported_ForNotAcceptNode() - { - // Arrange - QueryBinder binder = new MyQueryBinder(); - Mock node = new Mock(); - node.Setup(c => c.Kind).Returns(QueryNodeKind.None); - - // Act & Assert - ExceptionAssert.Throws(() => binder.Bind(node.Object, new QueryBinderContext()), - "Binding OData QueryNode of kind 'None' is not supported by 'QueryBinder'."); - } - - [Fact] - public void BindCollectionNode_ThrowsNotSupported_ForNotAcceptNode() - { - // Arrange - QueryBinder binder = new MyQueryBinder(); - Mock node = new Mock(); - node.Setup(c => c.Kind).Returns(QueryNodeKind.None); - - // Act & Assert - ExceptionAssert.Throws(() => binder.BindCollectionNode(node.Object, new QueryBinderContext()), - "Binding OData QueryNode of kind 'None' is not supported by 'QueryBinder'."); - } - - [Fact] - public void BindSingleValueNode_ThrowsNotSupported_ForNotAcceptNode() - { - // Arrange - QueryBinder binder = new MyQueryBinder(); - Mock node = new Mock(); - node.Setup(c => c.Kind).Returns(QueryNodeKind.None); - - // Act & Assert - ExceptionAssert.Throws(() => binder.BindSingleValueNode(node.Object, new QueryBinderContext()), - "Binding OData QueryNode of kind 'None' is not supported by 'QueryBinder'."); - } - - [Fact] - public void BindRangeVariable_ThrowsArgumentNull_ForInputs() - { - // Arrange - QueryBinder binder = new MyQueryBinder(); - RangeVariable variable = new Mock().Object; - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindRangeVariable(null, null), "rangeVariable"); - ExceptionAssert.ThrowsArgumentNull(() => binder.BindRangeVariable(variable, null), "context"); - } - - [Fact] - public void BindSingleResourceFunctionCallNode_ThrowsNotSupported_ForNotAcceptNode() - { - // Arrange - QueryBinder binder = new MyQueryBinder(); - Mock typeRef = new Mock(); - Mock navSource = new Mock(); - SingleResourceFunctionCallNode node = new SingleResourceFunctionCallNode("anyUnknown", null, typeRef.Object, navSource.Object); - - // Act & Assert - ExceptionAssert.Throws(() => binder.BindSingleResourceFunctionCallNode(node, new QueryBinderContext()), - "Unknown function 'anyUnknown'."); - } - - [Fact] - public void BindSingleValueFunctionCallNode_ThrowsArgumentNull_ForInputs() - { - // Arrange - QueryBinder binder = new MyQueryBinder(); - Mock type = new Mock(); - type.Setup(t => t.TypeKind).Returns(EdmTypeKind.Primitive); - Mock typeRef = new Mock(); - typeRef.Setup(t => t.Definition).Returns(type.Object); - SingleValueFunctionCallNode node = new SingleValueFunctionCallNode("any", null, typeRef.Object); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => binder.BindSingleValueFunctionCallNode(null, null), "node"); - ExceptionAssert.ThrowsArgumentNull(() => binder.BindSingleValueFunctionCallNode(node, null), "context"); - } - - [Fact] - public void GetDynamicPropertyContainer_ThrowsArgumentNull_ForInputs() - { - // Arrange - Mock singleValueNode = new Mock(); - SingleValueOpenPropertyAccessNode node = new SingleValueOpenPropertyAccessNode(singleValueNode.Object, "dynamic"); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => MyQueryBinder.Call_GetDynamicPropertyContainer(null, null), "openNode"); - ExceptionAssert.ThrowsArgumentNull(() => MyQueryBinder.Call_GetDynamicPropertyContainer(node, null), "context"); - } - - [Fact] - public void GetDynamicPropertyContainer_ThrowsNotSupported_ForNotAcceptNode() - { - // Arrange - Mock type = new Mock(); - type.Setup(t => t.TypeKind).Returns(EdmTypeKind.Primitive); - Mock typeRef = new Mock(); - typeRef.Setup(t => t.Definition).Returns(type.Object); - - Mock singleValueNode = new Mock(); - singleValueNode.Setup(s => s.TypeReference).Returns(typeRef.Object); - SingleValueOpenPropertyAccessNode node = new SingleValueOpenPropertyAccessNode(singleValueNode.Object, "dynamic"); - - // Act & Assert - ExceptionAssert.Throws(() => MyQueryBinder.Call_GetDynamicPropertyContainer(node, new QueryBinderContext()), - "Binding OData QueryNode of kind 'SingleValueOpenPropertyAccess' is not supported by 'QueryBinder'."); - } - - [Theory] - [InlineData(true, "$it.Values.Item[\"Values\"]")] - [InlineData(false, "$it.Values")] - public void GetPropertyExpression_Works_ForAggregateOrNonAggregate(bool isAggregated, string expected) - { - // Arrange - ParameterExpression source = Expression.Parameter(typeof(GroupByWrapper), "$it"); - - // Act - var expression = QueryBinder.GetPropertyExpression(source, "Values", isAggregated); - - // Assert - Assert.NotNull(expression); - Assert.Equal(expected, expression.ToString()); - } + // Arrange + QueryBinder binder = new MyQueryBinder(); + Mock typeRef = new Mock(); + Mock navSource = new Mock(); + SingleResourceFunctionCallNode node = new SingleResourceFunctionCallNode("anyUnknown", null, typeRef.Object, navSource.Object); + + // Act & Assert + ExceptionAssert.Throws(() => binder.BindSingleResourceFunctionCallNode(node, new QueryBinderContext()), + "Unknown function 'anyUnknown'."); } - public class MyQueryBinder : QueryBinder + [Fact] + public void BindSingleValueFunctionCallNode_ThrowsArgumentNull_ForInputs() + { + // Arrange + QueryBinder binder = new MyQueryBinder(); + Mock type = new Mock(); + type.Setup(t => t.TypeKind).Returns(EdmTypeKind.Primitive); + Mock typeRef = new Mock(); + typeRef.Setup(t => t.Definition).Returns(type.Object); + SingleValueFunctionCallNode node = new SingleValueFunctionCallNode("any", null, typeRef.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindSingleValueFunctionCallNode(null, null), "node"); + ExceptionAssert.ThrowsArgumentNull(() => binder.BindSingleValueFunctionCallNode(node, null), "context"); + } + + [Fact] + public void GetDynamicPropertyContainer_ThrowsArgumentNull_ForInputs() + { + // Arrange + Mock singleValueNode = new Mock(); + SingleValueOpenPropertyAccessNode node = new SingleValueOpenPropertyAccessNode(singleValueNode.Object, "dynamic"); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => MyQueryBinder.Call_GetDynamicPropertyContainer(null, null), "openNode"); + ExceptionAssert.ThrowsArgumentNull(() => MyQueryBinder.Call_GetDynamicPropertyContainer(node, null), "context"); + } + + [Fact] + public void GetDynamicPropertyContainer_ThrowsNotSupported_ForNotAcceptNode() + { + // Arrange + Mock type = new Mock(); + type.Setup(t => t.TypeKind).Returns(EdmTypeKind.Primitive); + Mock typeRef = new Mock(); + typeRef.Setup(t => t.Definition).Returns(type.Object); + + Mock singleValueNode = new Mock(); + singleValueNode.Setup(s => s.TypeReference).Returns(typeRef.Object); + SingleValueOpenPropertyAccessNode node = new SingleValueOpenPropertyAccessNode(singleValueNode.Object, "dynamic"); + + // Act & Assert + ExceptionAssert.Throws(() => MyQueryBinder.Call_GetDynamicPropertyContainer(node, new QueryBinderContext()), + "Binding OData QueryNode of kind 'SingleValueOpenPropertyAccess' is not supported by 'QueryBinder'."); + } + + [Theory] + [InlineData(true, "$it.Values.Item[\"Values\"]")] + [InlineData(false, "$it.Values")] + public void GetPropertyExpression_Works_ForAggregateOrNonAggregate(bool isAggregated, string expected) + { + // Arrange + ParameterExpression source = Expression.Parameter(typeof(GroupByWrapper), "$it"); + + // Act + var expression = QueryBinder.GetPropertyExpression(source, "Values", isAggregated); + + // Assert + Assert.NotNull(expression); + Assert.Equal(expected, expression.ToString()); + } +} + +public class MyQueryBinder : QueryBinder +{ + public static PropertyInfo Call_GetDynamicPropertyContainer(SingleValueOpenPropertyAccessNode openNode, QueryBinderContext context) { - public static PropertyInfo Call_GetDynamicPropertyContainer(SingleValueOpenPropertyAccessNode openNode, QueryBinderContext context) - { - return GetDynamicPropertyContainer(openNode, context); - } + return GetDynamicPropertyContainer(openNode, context); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/SelectExpandBinderTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/SelectExpandBinderTest.cs index adae7f691..f90875ade 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/SelectExpandBinderTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/SelectExpandBinderTest.cs @@ -27,2183 +27,2182 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions +namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions; + +public class SelectExpandBinderTest { - public class SelectExpandBinderTest - { - private static IPropertyMapper PropertyMapper = new IdentityPropertyMapper(); - - private readonly SelectExpandBinder _binder; - private readonly SelectExpandBinder _binder_lowerCamelCased; - private readonly IQueryable _queryable; - private readonly IQueryable _queryable_lowerCamelCased; - private readonly ODataQueryContext _context; - private readonly ODataQueryContext _context_lowerCamelCased; - private readonly ODataQuerySettings _settings; - private readonly ODataQuerySettings _settings_lowerCamelCased; - private readonly QueryBinderContext _queryBinderContext; - private readonly QueryBinderContext _queryBinderContext_lowerCamelCased; - - private readonly IEdmModel _model; - private readonly IEdmModel _model_lowerCamelCased; - private readonly IEdmEntityType _customer; - private readonly IEdmEntityType _customer_lowerCamelCased; - private readonly IEdmEntityType _order; - private readonly IEdmEntityType _order_lowerCamelCased; - private readonly IEdmEntityType _product; - private readonly IEdmEntityType _product_lowerCamelCased; - private readonly IEdmEntitySet _customers; - private readonly IEdmEntitySet _customers_lowerCamelCased; - private readonly IEdmEntitySet _orders; - private readonly IEdmEntitySet _orders_lowerCamelCased; - private readonly IEdmEntitySet _products; - private readonly IEdmEntitySet _products_lowerCamelCased; - - public SelectExpandBinderTest() - { - #region PascalCase EdmModel + private static IPropertyMapper PropertyMapper = new IdentityPropertyMapper(); + + private readonly SelectExpandBinder _binder; + private readonly SelectExpandBinder _binder_lowerCamelCased; + private readonly IQueryable _queryable; + private readonly IQueryable _queryable_lowerCamelCased; + private readonly ODataQueryContext _context; + private readonly ODataQueryContext _context_lowerCamelCased; + private readonly ODataQuerySettings _settings; + private readonly ODataQuerySettings _settings_lowerCamelCased; + private readonly QueryBinderContext _queryBinderContext; + private readonly QueryBinderContext _queryBinderContext_lowerCamelCased; + + private readonly IEdmModel _model; + private readonly IEdmModel _model_lowerCamelCased; + private readonly IEdmEntityType _customer; + private readonly IEdmEntityType _customer_lowerCamelCased; + private readonly IEdmEntityType _order; + private readonly IEdmEntityType _order_lowerCamelCased; + private readonly IEdmEntityType _product; + private readonly IEdmEntityType _product_lowerCamelCased; + private readonly IEdmEntitySet _customers; + private readonly IEdmEntitySet _customers_lowerCamelCased; + private readonly IEdmEntitySet _orders; + private readonly IEdmEntitySet _orders_lowerCamelCased; + private readonly IEdmEntitySet _products; + private readonly IEdmEntitySet _products_lowerCamelCased; + + public SelectExpandBinderTest() + { + #region PascalCase EdmModel - _model = GetEdmModel(); - _customer = _model.SchemaElements.OfType().First(c => c.Name == "QueryCustomer"); - _order = _model.SchemaElements.OfType().First(c => c.Name == "QueryOrder"); - _product = _model.SchemaElements.OfType().First(c => c.Name == "QueryProduct"); - _customers = _model.EntityContainer.FindEntitySet("Customers"); - _orders = _model.EntityContainer.FindEntitySet("Orders"); - _products = _model.EntityContainer.FindEntitySet("Products"); + _model = GetEdmModel(); + _customer = _model.SchemaElements.OfType().First(c => c.Name == "QueryCustomer"); + _order = _model.SchemaElements.OfType().First(c => c.Name == "QueryOrder"); + _product = _model.SchemaElements.OfType().First(c => c.Name == "QueryProduct"); + _customers = _model.EntityContainer.FindEntitySet("Customers"); + _orders = _model.EntityContainer.FindEntitySet("Orders"); + _products = _model.EntityContainer.FindEntitySet("Products"); - _settings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }; - _context = new ODataQueryContext(_model, typeof(QueryCustomer)) { RequestContainer = new MockServiceProvider() }; - _binder = new SelectExpandBinder(new FilterBinder(), new OrderByBinder()); + _settings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }; + _context = new ODataQueryContext(_model, typeof(QueryCustomer)) { RequestContainer = new MockServiceProvider() }; + _binder = new SelectExpandBinder(new FilterBinder(), new OrderByBinder()); - QueryCustomer customer = new QueryCustomer - { - Orders = new List() - }; - QueryOrder order = new QueryOrder { Id = 42, Title = "The order", Customer = customer }; - customer.Orders.Add(order); + QueryCustomer customer = new QueryCustomer + { + Orders = new List() + }; + QueryOrder order = new QueryOrder { Id = 42, Title = "The order", Customer = customer }; + customer.Orders.Add(order); - _queryable = new[] { customer }.AsQueryable(); + _queryable = new[] { customer }.AsQueryable(); - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption("Orders", expand: null, context: _context); + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption("Orders", expand: null, context: _context); - _queryBinderContext = new QueryBinderContext(_model, _settings, selectExpandQueryOption.Context.ElementClrType) - { - NavigationSource = _context.NavigationSource - }; + _queryBinderContext = new QueryBinderContext(_model, _settings, selectExpandQueryOption.Context.ElementClrType) + { + NavigationSource = _context.NavigationSource + }; - #endregion + #endregion - #region camelCase EdmModel + #region camelCase EdmModel - _model_lowerCamelCased = GetEdmModel_lowerCamelCased(); - _customer_lowerCamelCased = _model_lowerCamelCased.SchemaElements.OfType().First(c => c.Name == "QueryCustomer"); - _order_lowerCamelCased = _model_lowerCamelCased.SchemaElements.OfType().First(c => c.Name == "QueryOrder"); - _product_lowerCamelCased = _model_lowerCamelCased.SchemaElements.OfType().First(c => c.Name == "QueryProduct"); - _customers_lowerCamelCased = _model_lowerCamelCased.EntityContainer.FindEntitySet("Customers"); - _orders_lowerCamelCased = _model_lowerCamelCased.EntityContainer.FindEntitySet("Orders"); - _products_lowerCamelCased = _model_lowerCamelCased.EntityContainer.FindEntitySet("Products"); + _model_lowerCamelCased = GetEdmModel_lowerCamelCased(); + _customer_lowerCamelCased = _model_lowerCamelCased.SchemaElements.OfType().First(c => c.Name == "QueryCustomer"); + _order_lowerCamelCased = _model_lowerCamelCased.SchemaElements.OfType().First(c => c.Name == "QueryOrder"); + _product_lowerCamelCased = _model_lowerCamelCased.SchemaElements.OfType().First(c => c.Name == "QueryProduct"); + _customers_lowerCamelCased = _model_lowerCamelCased.EntityContainer.FindEntitySet("Customers"); + _orders_lowerCamelCased = _model_lowerCamelCased.EntityContainer.FindEntitySet("Orders"); + _products_lowerCamelCased = _model_lowerCamelCased.EntityContainer.FindEntitySet("Products"); - _settings_lowerCamelCased = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }; - _context_lowerCamelCased = new ODataQueryContext(_model_lowerCamelCased, typeof(QueryCustomer)) { RequestContainer = new MockServiceProvider() }; - _binder_lowerCamelCased = new SelectExpandBinder(new FilterBinder(), new OrderByBinder()); + _settings_lowerCamelCased = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }; + _context_lowerCamelCased = new ODataQueryContext(_model_lowerCamelCased, typeof(QueryCustomer)) { RequestContainer = new MockServiceProvider() }; + _binder_lowerCamelCased = new SelectExpandBinder(new FilterBinder(), new OrderByBinder()); - QueryCustomer customer_lowerCamelCased = new QueryCustomer - { - Orders = new List() - }; - QueryOrder order_lowerCamelCased = new QueryOrder { Id = 42, Title = "The order", Customer = customer_lowerCamelCased }; - customer_lowerCamelCased.Orders.Add(order_lowerCamelCased); + QueryCustomer customer_lowerCamelCased = new QueryCustomer + { + Orders = new List() + }; + QueryOrder order_lowerCamelCased = new QueryOrder { Id = 42, Title = "The order", Customer = customer_lowerCamelCased }; + customer_lowerCamelCased.Orders.Add(order_lowerCamelCased); - _queryable_lowerCamelCased = new[] { customer_lowerCamelCased }.AsQueryable(); + _queryable_lowerCamelCased = new[] { customer_lowerCamelCased }.AsQueryable(); - SelectExpandQueryOption selectExpandQueryOption_lowerCamelCased = new SelectExpandQueryOption("Orders", expand: null, context: _context_lowerCamelCased); + SelectExpandQueryOption selectExpandQueryOption_lowerCamelCased = new SelectExpandQueryOption("Orders", expand: null, context: _context_lowerCamelCased); - _queryBinderContext_lowerCamelCased = new QueryBinderContext(_model_lowerCamelCased, _settings_lowerCamelCased, selectExpandQueryOption_lowerCamelCased.Context.ElementClrType) - { - NavigationSource = _context_lowerCamelCased.NavigationSource - }; + _queryBinderContext_lowerCamelCased = new QueryBinderContext(_model_lowerCamelCased, _settings_lowerCamelCased, selectExpandQueryOption_lowerCamelCased.Context.ElementClrType) + { + NavigationSource = _context_lowerCamelCased.NavigationSource + }; - #endregion - } + #endregion + } - private static SelectExpandBinder GetBinder(IEdmModel model, HandleNullPropagationOption nullPropagation = HandleNullPropagationOption.False) - { - var settings = new ODataQuerySettings { HandleNullPropagation = nullPropagation }; + private static SelectExpandBinder GetBinder(IEdmModel model, HandleNullPropagationOption nullPropagation = HandleNullPropagationOption.False) + { + var settings = new ODataQuerySettings { HandleNullPropagation = nullPropagation }; - var context = new ODataQueryContext(model, typeof(T)) { RequestContainer = new MockServiceProvider() }; + var context = new ODataQueryContext(model, typeof(T)) { RequestContainer = new MockServiceProvider() }; - return new SelectExpandBinder(new FilterBinder(), new OrderByBinder()); - } + return new SelectExpandBinder(new FilterBinder(), new OrderByBinder()); + } - //[Fact] - //public void Bind_ReturnsIEdmObject_WithRightEdmType2() - //{ - // string csdl = Builder.MetadataTest.GetCSDL(_model); - // Console.WriteLine(csdl); - //} - - [Theory] - [InlineData("Id")] - [InlineData("Name")] - [InlineData("HomeAddress")] - public void Bind_ReturnsIEdmObject_WithRightEdmType(string select) - { - // Arrange - SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select: select, expand: null, context: _context); - - // Act - SelectExpandBinder binder = new SelectExpandBinder(); - IQueryable queryable = binder.ApplyBind(_queryable, selectExpand.SelectExpandClause, _queryBinderContext); - - // Assert - Assert.NotNull(queryable); - IEdmType edmType = _model.GetEdmType(queryable.GetType()); - Assert.NotNull(edmType); - Assert.Equal(EdmTypeKind.Collection, edmType.TypeKind); - Assert.Same(_customer, edmType.AsElementType()); - } + //[Fact] + //public void Bind_ReturnsIEdmObject_WithRightEdmType2() + //{ + // string csdl = Builder.MetadataTest.GetCSDL(_model); + // Console.WriteLine(csdl); + //} + + [Theory] + [InlineData("Id")] + [InlineData("Name")] + [InlineData("HomeAddress")] + public void Bind_ReturnsIEdmObject_WithRightEdmType(string select) + { + // Arrange + SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select: select, expand: null, context: _context); + + // Act + SelectExpandBinder binder = new SelectExpandBinder(); + IQueryable queryable = binder.ApplyBind(_queryable, selectExpand.SelectExpandClause, _queryBinderContext); + + // Assert + Assert.NotNull(queryable); + IEdmType edmType = _model.GetEdmType(queryable.GetType()); + Assert.NotNull(edmType); + Assert.Equal(EdmTypeKind.Collection, edmType.TypeKind); + Assert.Same(_customer, edmType.AsElementType()); + } - [Theory] - [InlineData("ProductTags")] - [InlineData("ProductTags($orderby=$this/Name)")] - [InlineData("ProductTags($orderby=$this/Name desc)")] - public void Bind_SelectAndOrderBy_PropertyFromDataMember(string expand) - { - // Arrange - IQueryable products; + [Theory] + [InlineData("ProductTags")] + [InlineData("ProductTags($orderby=$this/Name)")] + [InlineData("ProductTags($orderby=$this/Name desc)")] + public void Bind_SelectAndOrderBy_PropertyFromDataMember(string expand) + { + // Arrange + IQueryable products; - QueryProduct product1 = new QueryProduct + QueryProduct product1 = new QueryProduct + { + Id = 1, + Name = "Product 1", + Quantity = 1, + Tags = new List() { - Id = 1, - Name = "Product 1", - Quantity = 1, - Tags = new List() - { - new QueryProductTag(){Id = 1001, Name = "Tag 1" }, - new QueryProductTag(){Id = 1002, Name = "Tag 2" }, - new QueryProductTag(){Id = 1003, Name = "Tag 3" }, - new QueryProductTag(){Id = 1004, Name = "Tag 4" }, - } - }; + new QueryProductTag(){Id = 1001, Name = "Tag 1" }, + new QueryProductTag(){Id = 1002, Name = "Tag 2" }, + new QueryProductTag(){Id = 1003, Name = "Tag 3" }, + new QueryProductTag(){Id = 1004, Name = "Tag 4" }, + } + }; - products = new[] { product1 }.AsQueryable(); - ODataQueryContext context = new ODataQueryContext(_model, typeof(QueryProduct)) { RequestContainer = new MockServiceProvider() }; + products = new[] { product1 }.AsQueryable(); + ODataQueryContext context = new ODataQueryContext(_model, typeof(QueryProduct)) { RequestContainer = new MockServiceProvider() }; - SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select: null, expand: expand, context: context); + SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select: null, expand: expand, context: context); - _settings.PageSize = 2; + _settings.PageSize = 2; - QueryBinderContext queryBinderContext = new QueryBinderContext(_model, _settings, selectExpand.Context.ElementClrType) - { - NavigationSource = context.NavigationSource - }; + QueryBinderContext queryBinderContext = new QueryBinderContext(_model, _settings, selectExpand.Context.ElementClrType) + { + NavigationSource = context.NavigationSource + }; - // Act - SelectExpandBinder binder = new SelectExpandBinder(); - IQueryable queryable = binder.ApplyBind(products, selectExpand.SelectExpandClause, queryBinderContext); + // Act + SelectExpandBinder binder = new SelectExpandBinder(); + IQueryable queryable = binder.ApplyBind(products, selectExpand.SelectExpandClause, queryBinderContext); - // Assert - Assert.NotNull(queryable); + // Assert + Assert.NotNull(queryable); - IEnumerator enumerator = queryable.GetEnumerator(); - Assert.True(enumerator.MoveNext()); - var product = Assert.IsAssignableFrom>(enumerator.Current); - Assert.False(enumerator.MoveNext()); - Assert.NotNull(product.Instance); - Assert.Equal("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryProduct", product.Instance.GetType().ToString()); - IEnumerable> innerProductTags = product.Container - .ToDictionary(PropertyMapper)["ProductTags"] as IEnumerable>; - Assert.NotNull(innerProductTags); + IEnumerator enumerator = queryable.GetEnumerator(); + Assert.True(enumerator.MoveNext()); + var product = Assert.IsAssignableFrom>(enumerator.Current); + Assert.False(enumerator.MoveNext()); + Assert.NotNull(product.Instance); + Assert.Equal("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryProduct", product.Instance.GetType().ToString()); + IEnumerable> innerProductTags = product.Container + .ToDictionary(PropertyMapper)["ProductTags"] as IEnumerable>; + Assert.NotNull(innerProductTags); - SelectExpandWrapper firstTag = innerProductTags.FirstOrDefault(); - SelectExpandWrapper lastTag = innerProductTags.LastOrDefault(); + SelectExpandWrapper firstTag = innerProductTags.FirstOrDefault(); + SelectExpandWrapper lastTag = innerProductTags.LastOrDefault(); - if (expand.EndsWith("desc)")) - { - Assert.Equal("Tag 4", firstTag.Instance.Name); - Assert.Equal("Tag 3", lastTag.Instance.Name); - } - else - { - Assert.Equal("Tag 1", firstTag.Instance.Name); - Assert.Equal("Tag 2", lastTag.Instance.Name); - } + if (expand.EndsWith("desc)")) + { + Assert.Equal("Tag 4", firstTag.Instance.Name); + Assert.Equal("Tag 3", lastTag.Instance.Name); } - - [Fact] - public void Bind_GeneratedExpression_ContainsExpandedObject() + else { - // Arrange - SelectExpandQueryOption selectExpand = new SelectExpandQueryOption("Orders", "Orders,Orders($expand=Customer)", _context); - - // Act - SelectExpandBinder binder = new SelectExpandBinder(); - IQueryable queryable = binder.ApplyBind(_queryable, selectExpand.SelectExpandClause, _queryBinderContext); - - // Assert - IEnumerator enumerator = queryable.GetEnumerator(); - Assert.True(enumerator.MoveNext()); - var partialCustomer = Assert.IsAssignableFrom>(enumerator.Current); - Assert.False(enumerator.MoveNext()); - Assert.Null(partialCustomer.Instance); - Assert.Equal("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCustomer", partialCustomer.InstanceType); - IEnumerable> innerOrders = partialCustomer.Container - .ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; - Assert.NotNull(innerOrders); - SelectExpandWrapper partialOrder = innerOrders.Single(); - Assert.Same(_queryable.First().Orders.First(), partialOrder.Instance); - object customer = partialOrder.Container.ToDictionary(PropertyMapper)["Customer"]; - SelectExpandWrapper innerInnerCustomer = Assert.IsAssignableFrom>(customer); - Assert.Same(_queryable.First(), innerInnerCustomer.Instance); + Assert.Equal("Tag 1", firstTag.Instance.Name); + Assert.Equal("Tag 2", lastTag.Instance.Name); } + } - [Fact] - public void Bind_GeneratedExpression_CheckNullObjectWithinChainProjectionByKey() - { - // Arrange - SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(null, "Orders($expand=Customer($select=City))", _context); + [Fact] + public void Bind_GeneratedExpression_ContainsExpandedObject() + { + // Arrange + SelectExpandQueryOption selectExpand = new SelectExpandQueryOption("Orders", "Orders,Orders($expand=Customer)", _context); + + // Act + SelectExpandBinder binder = new SelectExpandBinder(); + IQueryable queryable = binder.ApplyBind(_queryable, selectExpand.SelectExpandClause, _queryBinderContext); + + // Assert + IEnumerator enumerator = queryable.GetEnumerator(); + Assert.True(enumerator.MoveNext()); + var partialCustomer = Assert.IsAssignableFrom>(enumerator.Current); + Assert.False(enumerator.MoveNext()); + Assert.Null(partialCustomer.Instance); + Assert.Equal("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCustomer", partialCustomer.InstanceType); + IEnumerable> innerOrders = partialCustomer.Container + .ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; + Assert.NotNull(innerOrders); + SelectExpandWrapper partialOrder = innerOrders.Single(); + Assert.Same(_queryable.First().Orders.First(), partialOrder.Instance); + object customer = partialOrder.Container.ToDictionary(PropertyMapper)["Customer"]; + SelectExpandWrapper innerInnerCustomer = Assert.IsAssignableFrom>(customer); + Assert.Same(_queryable.First(), innerInnerCustomer.Instance); + } - // Act - SelectExpandBinder binder = new SelectExpandBinder(); - IQueryable queryable = binder.ApplyBind(_queryable, selectExpand.SelectExpandClause, _queryBinderContext); + [Fact] + public void Bind_GeneratedExpression_CheckNullObjectWithinChainProjectionByKey() + { + // Arrange + SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(null, "Orders($expand=Customer($select=City))", _context); - // Assert - var unaryExpression = (UnaryExpression)((MethodCallExpression)queryable.Expression).Arguments.Single(a => a is UnaryExpression); - var expressionString = unaryExpression.Operand.ToString(); - Assert.Contains("IsNull = (Convert($it.Customer.Id, Nullable`1) == null)}", expressionString); - } + // Act + SelectExpandBinder binder = new SelectExpandBinder(); + IQueryable queryable = binder.ApplyBind(_queryable, selectExpand.SelectExpandClause, _queryBinderContext); - [Fact] - public void ProjectAsWrapper_NonCollection_ContainsRightInstance() - { - // Arrange - QueryOrder order = new QueryOrder(); - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); - Expression source = Expression.Constant(order); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); - - // Assert - SelectExpandWrapper projectedOrder = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - Assert.NotNull(projectedOrder); - Assert.Same(order, projectedOrder.Instance); - } + // Assert + var unaryExpression = (UnaryExpression)((MethodCallExpression)queryable.Expression).Arguments.Single(a => a is UnaryExpression); + var expressionString = unaryExpression.Operand.ToString(); + Assert.Contains("IsNull = (Convert($it.Customer.Id, Nullable`1) == null)}", expressionString); + } - [Fact] - public void ProjectAsWrapper_NonCollection_ProjectedValueNullAndHandleNullPropagationTrue() - { - // Arrange - _settings.HandleNullPropagation = HandleNullPropagationOption.True; - - IEdmNavigationProperty customerNav = _order.DeclaredNavigationProperties().Single(c => c.Name == "Customer"); - ExpandedNavigationSelectItem expandItem = new ExpandedNavigationSelectItem( - new ODataExpandPath(new NavigationPropertySegment(customerNav, navigationSource: _customers)), _customers, selectExpandOption: null); - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[] { expandItem }, allSelected: true); - Expression source = Expression.Constant(null, typeof(QueryOrder)); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); - - // Assert - SelectExpandWrapper projectedOrder = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - Assert.NotNull(projectedOrder); - Assert.Null(projectedOrder.Instance); - - SelectExpandWrapper projectCustomer = projectedOrder.Container.ToDictionary(PropertyMapper)["Customer"] as SelectExpandWrapper; - Assert.NotNull(projectCustomer); - Assert.Null(projectCustomer.Instance); - } + [Fact] + public void ProjectAsWrapper_NonCollection_ContainsRightInstance() + { + // Arrange + QueryOrder order = new QueryOrder(); + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); + Expression source = Expression.Constant(order); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); + + // Assert + SelectExpandWrapper projectedOrder = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + Assert.NotNull(projectedOrder); + Assert.Same(order, projectedOrder.Instance); + } - [Fact] - public void ProjectAsWrapper_NonCollection_ProjectedValueNullAndHandleNullPropagationFalse_Throws() - { - // Arrange - _settings.HandleNullPropagation = HandleNullPropagationOption.False; - IEdmNavigationProperty customerNav = _order.DeclaredNavigationProperties().Single(c => c.Name == "Customer"); - ExpandedNavigationSelectItem expandItem = new ExpandedNavigationSelectItem( - new ODataExpandPath(new NavigationPropertySegment(customerNav, navigationSource: _customers)), - _customers, - selectExpandOption: null); - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[] { expandItem }, allSelected: true); - Expression source = Expression.Constant(null, typeof(QueryOrder)); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); - - // Assert - var e = ExceptionAssert.Throws(() => Expression.Lambda(projection).Compile().DynamicInvoke()); - Assert.IsType(e.InnerException); - } + [Fact] + public void ProjectAsWrapper_NonCollection_ProjectedValueNullAndHandleNullPropagationTrue() + { + // Arrange + _settings.HandleNullPropagation = HandleNullPropagationOption.True; + + IEdmNavigationProperty customerNav = _order.DeclaredNavigationProperties().Single(c => c.Name == "Customer"); + ExpandedNavigationSelectItem expandItem = new ExpandedNavigationSelectItem( + new ODataExpandPath(new NavigationPropertySegment(customerNav, navigationSource: _customers)), _customers, selectExpandOption: null); + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[] { expandItem }, allSelected: true); + Expression source = Expression.Constant(null, typeof(QueryOrder)); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); + + // Assert + SelectExpandWrapper projectedOrder = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + Assert.NotNull(projectedOrder); + Assert.Null(projectedOrder.Instance); + + SelectExpandWrapper projectCustomer = projectedOrder.Container.ToDictionary(PropertyMapper)["Customer"] as SelectExpandWrapper; + Assert.NotNull(projectCustomer); + Assert.Null(projectCustomer.Instance); + } - [Fact] - public void ProjectAsWrapper_Collection_ContainsRightInstance() - { - // Arrange - QueryOrder[] orders = new QueryOrder[] { new QueryOrder() }; - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); - Expression source = Expression.Constant(orders); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); - - // Assert - IEnumerable> projectedOrders = Expression.Lambda(projection).Compile().DynamicInvoke() as IEnumerable>; - Assert.NotNull(projectedOrders); - Assert.Same(orders[0], projectedOrders.Single().Instance); - } + [Fact] + public void ProjectAsWrapper_NonCollection_ProjectedValueNullAndHandleNullPropagationFalse_Throws() + { + // Arrange + _settings.HandleNullPropagation = HandleNullPropagationOption.False; + IEdmNavigationProperty customerNav = _order.DeclaredNavigationProperties().Single(c => c.Name == "Customer"); + ExpandedNavigationSelectItem expandItem = new ExpandedNavigationSelectItem( + new ODataExpandPath(new NavigationPropertySegment(customerNav, navigationSource: _customers)), + _customers, + selectExpandOption: null); + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[] { expandItem }, allSelected: true); + Expression source = Expression.Constant(null, typeof(QueryOrder)); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); + + // Assert + var e = ExceptionAssert.Throws(() => Expression.Lambda(projection).Compile().DynamicInvoke()); + Assert.IsType(e.InnerException); + } - [Fact] - public void ProjectAsWrapper_Collection_AppliesPageSize_AndOrderBy() - { - // Arrange - int pageSize = 5; - var orders = Enumerable.Range(0, 10).Select(i => new QueryOrder - { - Id = 10 - i, - }); - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); - Expression source = Expression.Constant(orders); - _settings.PageSize = pageSize; - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); - - // Assert - IEnumerable> projectedOrders = Expression.Lambda(projection).Compile().DynamicInvoke() as IEnumerable>; - Assert.NotNull(projectedOrders); - Assert.Equal(pageSize + 1, projectedOrders.Count()); - Assert.Equal(1, projectedOrders.First().Instance.Id); - } + [Fact] + public void ProjectAsWrapper_Collection_ContainsRightInstance() + { + // Arrange + QueryOrder[] orders = new QueryOrder[] { new QueryOrder() }; + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); + Expression source = Expression.Constant(orders); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); + + // Assert + IEnumerable> projectedOrders = Expression.Lambda(projection).Compile().DynamicInvoke() as IEnumerable>; + Assert.NotNull(projectedOrders); + Assert.Same(orders[0], projectedOrders.Single().Instance); + } - [Fact] - public void ProjectAsWrapper_ProjectionContainsExpandedProperties() - { - // Arrange - QueryOrder order = new QueryOrder(); - IEdmNavigationProperty customerNav = _order.DeclaredNavigationProperties().Single(c => c.Name == "Customer"); - ExpandedNavigationSelectItem expandItem = new ExpandedNavigationSelectItem( - new ODataExpandPath(new NavigationPropertySegment(customerNav, navigationSource: _customers)), - _customers, - selectExpandOption: null); - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[] { expandItem }, allSelected: true); - Expression source = Expression.Constant(order); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); - - // Assert - SelectExpandWrapper projectedOrder = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - Assert.NotNull(projectedOrder); - Assert.Contains("Customer", projectedOrder.Container.ToDictionary(PropertyMapper).Keys); - } + [Fact] + public void ProjectAsWrapper_Collection_AppliesPageSize_AndOrderBy() + { + // Arrange + int pageSize = 5; + var orders = Enumerable.Range(0, 10).Select(i => new QueryOrder + { + Id = 10 - i, + }); + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); + Expression source = Expression.Constant(orders); + _settings.PageSize = pageSize; + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); + + // Assert + IEnumerable> projectedOrders = Expression.Lambda(projection).Compile().DynamicInvoke() as IEnumerable>; + Assert.NotNull(projectedOrders); + Assert.Equal(pageSize + 1, projectedOrders.Count()); + Assert.Equal(1, projectedOrders.First().Instance.Id); + } - [Fact] - public void ProjectAsWrapper_NullExpandedProperty_HasNullValueInProjectedWrapper() - { - // Arrange - QueryOrder order = new QueryOrder(); - IEdmNavigationProperty customerNav = _order.DeclaredNavigationProperties().Single(c => c.Name == "Customer"); - ExpandedNavigationSelectItem expandItem = new ExpandedNavigationSelectItem( - new ODataExpandPath(new NavigationPropertySegment(customerNav, navigationSource: _customers)), - _customers, - selectExpandOption: null); - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[] { expandItem }, allSelected: true); - Expression source = Expression.Constant(order); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); - - // Assert - SelectExpandWrapper projectedOrder = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - Assert.NotNull(projectedOrder); - Assert.Contains("Customer", projectedOrder.Container.ToDictionary(PropertyMapper).Keys); - - SelectExpandWrapper projectCustomer = projectedOrder.Container.ToDictionary(PropertyMapper)["Customer"] as SelectExpandWrapper; - Assert.NotNull(projectCustomer); - Assert.Null(projectCustomer.Instance); - } + [Fact] + public void ProjectAsWrapper_ProjectionContainsExpandedProperties() + { + // Arrange + QueryOrder order = new QueryOrder(); + IEdmNavigationProperty customerNav = _order.DeclaredNavigationProperties().Single(c => c.Name == "Customer"); + ExpandedNavigationSelectItem expandItem = new ExpandedNavigationSelectItem( + new ODataExpandPath(new NavigationPropertySegment(customerNav, navigationSource: _customers)), + _customers, + selectExpandOption: null); + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[] { expandItem }, allSelected: true); + Expression source = Expression.Constant(order); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); + + // Assert + SelectExpandWrapper projectedOrder = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + Assert.NotNull(projectedOrder); + Assert.Contains("Customer", projectedOrder.Container.ToDictionary(PropertyMapper).Keys); + } - [Fact] - public void ProjectAsWrapper_Collection_ProjectedValueNullAndHandleNullPropagationTrue() - { - // Arrange - _settings.HandleNullPropagation = HandleNullPropagationOption.True; + [Fact] + public void ProjectAsWrapper_NullExpandedProperty_HasNullValueInProjectedWrapper() + { + // Arrange + QueryOrder order = new QueryOrder(); + IEdmNavigationProperty customerNav = _order.DeclaredNavigationProperties().Single(c => c.Name == "Customer"); + ExpandedNavigationSelectItem expandItem = new ExpandedNavigationSelectItem( + new ODataExpandPath(new NavigationPropertySegment(customerNav, navigationSource: _customers)), + _customers, + selectExpandOption: null); + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[] { expandItem }, allSelected: true); + Expression source = Expression.Constant(order); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); + + // Assert + SelectExpandWrapper projectedOrder = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + Assert.NotNull(projectedOrder); + Assert.Contains("Customer", projectedOrder.Container.ToDictionary(PropertyMapper).Keys); + + SelectExpandWrapper projectCustomer = projectedOrder.Container.ToDictionary(PropertyMapper)["Customer"] as SelectExpandWrapper; + Assert.NotNull(projectCustomer); + Assert.Null(projectCustomer.Instance); + } - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); - Expression source = Expression.Constant(null, typeof(QueryOrder[])); + [Fact] + public void ProjectAsWrapper_Collection_ProjectedValueNullAndHandleNullPropagationTrue() + { + // Arrange + _settings.HandleNullPropagation = HandleNullPropagationOption.True; - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); + Expression source = Expression.Constant(null, typeof(QueryOrder[])); - // Assert - IEnumerable> projectedOrders = Expression.Lambda(projection).Compile().DynamicInvoke() as IEnumerable>; - Assert.Null(projectedOrders); - } + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); - [Fact] - public void ProjectAsWrapper_Collection_ProjectedValueNullAndHandleNullPropagationFalse_Throws() - { - // Arrange - _settings.HandleNullPropagation = HandleNullPropagationOption.False; + // Assert + IEnumerable> projectedOrders = Expression.Lambda(projection).Compile().DynamicInvoke() as IEnumerable>; + Assert.Null(projectedOrders); + } - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); + [Fact] + public void ProjectAsWrapper_Collection_ProjectedValueNullAndHandleNullPropagationFalse_Throws() + { + // Arrange + _settings.HandleNullPropagation = HandleNullPropagationOption.False; - Expression source = Expression.Constant(null, typeof(QueryOrder[])); + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); + Expression source = Expression.Constant(null, typeof(QueryOrder[])); - // Assert - var e = ExceptionAssert.Throws(() => Expression.Lambda(projection).Compile().DynamicInvoke()); - Assert.IsType(e.InnerException); - } + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _order, _orders); - [Fact] - public void ProjectAsWrapper_Element_ProjectedValueContainsModel() - { - // Arrange - SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); - QueryCustomer aCustomer = new QueryCustomer(); - Expression source = Expression.Constant(aCustomer); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _customer, _customers); - - // Assert - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - Assert.NotNull(customerWrapper.Model); - Assert.Same(_model, customerWrapper.Model); - } + // Assert + var e = ExceptionAssert.Throws(() => Expression.Lambda(projection).Compile().DynamicInvoke()); + Assert.IsType(e.InnerException); + } + + [Fact] + public void ProjectAsWrapper_Element_ProjectedValueContainsModel() + { + // Arrange + SelectExpandClause selectExpand = new SelectExpandClause(new SelectItem[0], allSelected: true); + QueryCustomer aCustomer = new QueryCustomer(); + Expression source = Expression.Constant(aCustomer); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpand, _customer, _customers); + + // Assert + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + Assert.NotNull(customerWrapper.Model); + Assert.Same(_model, customerWrapper.Model); + } - [Fact] - public void ProjectAsWrapper_Collection_ProjectedValueContainsSubKeys_IfDollarRefInDollarExpand() + [Fact] + public void ProjectAsWrapper_Collection_ProjectedValueContainsSubKeys_IfDollarRefInDollarExpand() + { + // Arrange + string expand = "Orders/$ref"; + QueryCustomer customer1 = new QueryCustomer { - // Arrange - string expand = "Orders/$ref"; - QueryCustomer customer1 = new QueryCustomer + Orders = new[] { - Orders = new[] - { - new QueryOrder { Id = 8 }, - new QueryVipOrder { Id = 9 } - } - }; - QueryCustomer customer2 = new QueryCustomer + new QueryOrder { Id = 8 }, + new QueryVipOrder { Id = 9 } + } + }; + QueryCustomer customer2 = new QueryCustomer + { + Orders = new[] { - Orders = new[] - { - new QueryOrder { Id = 18 }, - new QueryVipOrder { Id = 19 } - } - }; - Expression source = Expression.Constant(new[] { customer1, customer2 }); - - SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - - // Assert - Assert.Equal(ExpressionType.Call, projection.NodeType); - var customerWrappers = Expression.Lambda(projection).Compile().DynamicInvoke() as IEnumerable>; - Assert.Equal(2, customerWrappers.Count()); - - var orders = customerWrappers.ElementAt(0).Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; - Assert.NotNull(orders); - Assert.Equal(2, orders.Count()); - Assert.Equal(8, orders.ElementAt(0).Container.ToDictionary(PropertyMapper)["Id"]); - Assert.Equal(9, orders.ElementAt(1).Container.ToDictionary(PropertyMapper)["Id"]); - - orders = customerWrappers.ElementAt(1).Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; - Assert.NotNull(orders); - Assert.Equal(2, orders.Count()); - Assert.Equal(18, orders.ElementAt(0).Container.ToDictionary(PropertyMapper)["Id"]); - Assert.Equal(19, orders.ElementAt(1).Container.ToDictionary(PropertyMapper)["Id"]); - } + new QueryOrder { Id = 18 }, + new QueryVipOrder { Id = 19 } + } + }; + Expression source = Expression.Constant(new[] { customer1, customer2 }); + + SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + + // Assert + Assert.Equal(ExpressionType.Call, projection.NodeType); + var customerWrappers = Expression.Lambda(projection).Compile().DynamicInvoke() as IEnumerable>; + Assert.Equal(2, customerWrappers.Count()); + + var orders = customerWrappers.ElementAt(0).Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; + Assert.NotNull(orders); + Assert.Equal(2, orders.Count()); + Assert.Equal(8, orders.ElementAt(0).Container.ToDictionary(PropertyMapper)["Id"]); + Assert.Equal(9, orders.ElementAt(1).Container.ToDictionary(PropertyMapper)["Id"]); + + orders = customerWrappers.ElementAt(1).Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; + Assert.NotNull(orders); + Assert.Equal(2, orders.Count()); + Assert.Equal(18, orders.ElementAt(0).Container.ToDictionary(PropertyMapper)["Id"]); + Assert.Equal(19, orders.ElementAt(1).Container.ToDictionary(PropertyMapper)["Id"]); + } - [Fact] - public void ProjectAsWrapper_Collection_ProjectedValueContainsSubKeys_IfDollarRefInDollarExpand_AndNestedTopAndSkip() + [Fact] + public void ProjectAsWrapper_Collection_ProjectedValueContainsSubKeys_IfDollarRefInDollarExpand_AndNestedTopAndSkip() + { + // Arrange + string expand = "Orders/$ref($top=1;$skip=1)"; + QueryCustomer customer1 = new QueryCustomer { - // Arrange - string expand = "Orders/$ref($top=1;$skip=1)"; - QueryCustomer customer1 = new QueryCustomer + Orders = new[] { - Orders = new[] - { - new QueryOrder { Id = 8 }, - new QueryVipOrder { Id = 9 } - } - }; - QueryCustomer customer2 = new QueryCustomer + new QueryOrder { Id = 8 }, + new QueryVipOrder { Id = 9 } + } + }; + QueryCustomer customer2 = new QueryCustomer + { + Orders = new[] { - Orders = new[] - { - new QueryOrder { Id = 18 }, - new QueryVipOrder { Id = 19 } - } - }; + new QueryOrder { Id = 18 }, + new QueryVipOrder { Id = 19 } + } + }; - Expression source = Expression.Constant(new[] { customer1, customer2 }); - SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); + Expression source = Expression.Constant(new[] { customer1, customer2 }); + SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - // Assert - Assert.Equal(ExpressionType.Call, projection.NodeType); - var customerWrappers = Expression.Lambda(projection).Compile().DynamicInvoke() as IEnumerable>; - Assert.Equal(2, customerWrappers.Count()); + // Assert + Assert.Equal(ExpressionType.Call, projection.NodeType); + var customerWrappers = Expression.Lambda(projection).Compile().DynamicInvoke() as IEnumerable>; + Assert.Equal(2, customerWrappers.Count()); - var orders = customerWrappers.ElementAt(0).Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; - Assert.NotNull(orders); - var order = Assert.Single(orders); // only one - Assert.Equal(9, order.Container.ToDictionary(PropertyMapper)["Id"]); + var orders = customerWrappers.ElementAt(0).Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; + Assert.NotNull(orders); + var order = Assert.Single(orders); // only one + Assert.Equal(9, order.Container.ToDictionary(PropertyMapper)["Id"]); - orders = customerWrappers.ElementAt(1).Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; - Assert.NotNull(orders); - order = Assert.Single(orders); - Assert.Equal(19, order.Container.ToDictionary(PropertyMapper)["Id"]); - } + orders = customerWrappers.ElementAt(1).Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; + Assert.NotNull(orders); + order = Assert.Single(orders); + Assert.Equal(19, order.Container.ToDictionary(PropertyMapper)["Id"]); + } - [Theory] - [InlineData("*")] - [InlineData("Id,*")] - [InlineData("*,Name")] - [InlineData("*,HomeAddress/Street")] - [InlineData("")] - public void ProjectAsWrapper_Element_ProjectedValueContainsInstance_IfSelectionIsAll(string select) - { - // Arrange - QueryCustomer aCustomer = new QueryCustomer(); - Expression source = Expression.Constant(aCustomer); + [Theory] + [InlineData("*")] + [InlineData("Id,*")] + [InlineData("*,Name")] + [InlineData("*,HomeAddress/Street")] + [InlineData("")] + public void ProjectAsWrapper_Element_ProjectedValueContainsInstance_IfSelectionIsAll(string select) + { + // Arrange + QueryCustomer aCustomer = new QueryCustomer(); + Expression source = Expression.Constant(aCustomer); - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - // Assert - Assert.Equal(ExpressionType.MemberInit, projection.NodeType); - Assert.NotEmpty((projection as MemberInitExpression).Bindings.Where(p => p.Member.Name == "Instance")); - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - Assert.Same(aCustomer, customerWrapper.Instance); - } - - [Fact] - public void ProjectAsWrapper_Element_ProjectedValueDoesNotContainInstance_IfSelectionIsPartial() - { - // Arrange - string select = "Id,Orders"; - string expand = "Orders"; - QueryCustomer aCustomer = new QueryCustomer - { - Orders = new QueryOrder[0] - }; - Expression source = Expression.Constant(aCustomer); - - SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + // Assert + Assert.Equal(ExpressionType.MemberInit, projection.NodeType); + Assert.NotEmpty((projection as MemberInitExpression).Bindings.Where(p => p.Member.Name == "Instance")); + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + Assert.Same(aCustomer, customerWrapper.Instance); + } - // Assert - Assert.Equal(ExpressionType.MemberInit, projection.NodeType); - Assert.Empty((projection as MemberInitExpression).Bindings.Where(p => p.Member.Name == "Instance")); - Assert.NotEmpty((projection as MemberInitExpression).Bindings.Where(p => p.Member.Name == "InstanceType")); - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - Assert.Null(customerWrapper.Instance); - Assert.Equal("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCustomer", customerWrapper.InstanceType); - } + [Fact] + public void ProjectAsWrapper_Element_ProjectedValueDoesNotContainInstance_IfSelectionIsPartial() + { + // Arrange + string select = "Id,Orders"; + string expand = "Orders"; + QueryCustomer aCustomer = new QueryCustomer + { + Orders = new QueryOrder[0] + }; + Expression source = Expression.Constant(aCustomer); + + SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + + // Assert + Assert.Equal(ExpressionType.MemberInit, projection.NodeType); + Assert.Empty((projection as MemberInitExpression).Bindings.Where(p => p.Member.Name == "Instance")); + Assert.NotEmpty((projection as MemberInitExpression).Bindings.Where(p => p.Member.Name == "InstanceType")); + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + Assert.Null(customerWrapper.Instance); + Assert.Equal("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCustomer", customerWrapper.InstanceType); + } - [Theory] - [InlineData("Name", "OData")] - [InlineData("Age", 31)] - public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedStructuralProperties(string select, object expect) + [Theory] + [InlineData("Name", "OData")] + [InlineData("Age", 31)] + public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedStructuralProperties(string select, object expect) + { + // Arrange + QueryCustomer aCustomer = new QueryCustomer { - // Arrange - QueryCustomer aCustomer = new QueryCustomer - { - Name = "OData", - Age = 31 - }; - Expression source = Expression.Constant(aCustomer); + Name = "OData", + Age = 31 + }; + Expression source = Expression.Constant(aCustomer); - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - // Assert - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - Assert.Equal(expect, customerWrapper.Container.ToDictionary(PropertyMapper)[select]); - } + // Assert + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + Assert.Equal(expect, customerWrapper.Container.ToDictionary(PropertyMapper)[select]); + } - [Theory] - [InlineData("Emails", new[] { "E1", "E3", "E2" })] - [InlineData("Emails($orderby=$this)", new[] { "E1", "E2", "E3" })] - [InlineData("Emails($orderby=$this desc)", new[] { "E3", "E2", "E1" })] - [InlineData("Emails($top=1)", new[] { "E1" })] - [InlineData("Emails($top=1;$skip=1)", new[] { "E3" })] - [InlineData("Emails($filter=$this le 'E2')", new[] { "E1", "E2" })] - public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedCollectStructuralProperties(string select, object expect) + [Theory] + [InlineData("Emails", new[] { "E1", "E3", "E2" })] + [InlineData("Emails($orderby=$this)", new[] { "E1", "E2", "E3" })] + [InlineData("Emails($orderby=$this desc)", new[] { "E3", "E2", "E1" })] + [InlineData("Emails($top=1)", new[] { "E1" })] + [InlineData("Emails($top=1;$skip=1)", new[] { "E3" })] + [InlineData("Emails($filter=$this le 'E2')", new[] { "E1", "E2" })] + public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedCollectStructuralProperties(string select, object expect) + { + // Arrange + QueryCustomer aCustomer = new QueryCustomer + { + Emails = new [] { "E1", "E3", "E2" } + }; + Expression source = Expression.Constant(aCustomer); + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + + // Assert + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + var emails = customerWrapper.Container.ToDictionary(PropertyMapper)["Emails"]; + Assert.Equal(expect, emails); + } + + [Theory] + [InlineData("HomeAddress/Street,HomeAddress/Region")] + [InlineData("HomeAddress($select=Street, Region)")] + public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedSubStructuralProperties(string select) + { + // Arrange + QueryCustomer aCustomer = new QueryCustomer { - // Arrange - QueryCustomer aCustomer = new QueryCustomer + HomeAddress = new QueryAddress { - Emails = new [] { "E1", "E3", "E2" } - }; - Expression source = Expression.Constant(aCustomer); - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - - // Assert - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - var emails = customerWrapper.Container.ToDictionary(PropertyMapper)["Emails"]; - Assert.Equal(expect, emails); - } + Street = "148TH AVE NE", + Region = "Redmond" + } + }; + Expression source = Expression.Constant(aCustomer); + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + + // Assert + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + SelectExpandWrapper addressWrapper = customerWrapper.Container.ToDictionary(PropertyMapper)["HomeAddress"] as SelectExpandWrapper; + var addressProperties = addressWrapper.Container.ToDictionary(PropertyMapper); + Assert.Equal(2, addressProperties.Count); + Assert.Equal("148TH AVE NE", addressProperties["Street"]); + Assert.Equal("Redmond", addressProperties["Region"]); + } - [Theory] - [InlineData("HomeAddress/Street,HomeAddress/Region")] - [InlineData("HomeAddress($select=Street, Region)")] - public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedSubStructuralProperties(string select) + [Theory] + [InlineData("Addresses/Street,Addresses/Region")] + [InlineData("Addresses($select=Street,Region)")] + public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedTopCollectionWithSubStructuralProperties(string select) + { + // Arrange + QueryCustomer aCustomer = new QueryCustomer { - // Arrange - QueryCustomer aCustomer = new QueryCustomer + Addresses = new List { - HomeAddress = new QueryAddress + new QueryCnAddress + { + Street = "Being Rd", + Region = "Region#1" + }, + new QueryUsAddress { Street = "148TH AVE NE", - Region = "Redmond" + Region = "Region#2" } - }; - Expression source = Expression.Constant(aCustomer); - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - - // Assert - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - SelectExpandWrapper addressWrapper = customerWrapper.Container.ToDictionary(PropertyMapper)["HomeAddress"] as SelectExpandWrapper; - var addressProperties = addressWrapper.Container.ToDictionary(PropertyMapper); - Assert.Equal(2, addressProperties.Count); - Assert.Equal("148TH AVE NE", addressProperties["Street"]); - Assert.Equal("Redmond", addressProperties["Region"]); - } + } + }; + Expression source = Expression.Constant(aCustomer); + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + + // Assert + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + var addressesWrapper = customerWrapper.Container.ToDictionary(PropertyMapper)["Addresses"] as IEnumerable>; + Assert.Equal(2, addressesWrapper.Count()); + + var properties = addressesWrapper.ElementAt(0).Container.ToDictionary(PropertyMapper); + Assert.Equal("Being Rd", properties["Street"]); + Assert.Equal("Region#1", properties["Region"]); + + properties = addressesWrapper.ElementAt(1).Container.ToDictionary(PropertyMapper); + Assert.Equal("148TH AVE NE", properties["Street"]); + Assert.Equal("Region#2", properties["Region"]); + } - [Theory] - [InlineData("Addresses/Street,Addresses/Region")] - [InlineData("Addresses($select=Street,Region)")] - public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedTopCollectionWithSubStructuralProperties(string select) + [Fact] + public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedTopAndSubStructuralProperties() + { + // Arrange + QueryCustomer aCustomer = new QueryCustomer { - // Arrange - QueryCustomer aCustomer = new QueryCustomer + Name = "Peter", + HomeAddress = new QueryAddress { - Addresses = new List - { - new QueryCnAddress - { - Street = "Being Rd", - Region = "Region#1" - }, - new QueryUsAddress - { - Street = "148TH AVE NE", - Region = "Region#2" - } - } - }; - Expression source = Expression.Constant(aCustomer); - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - - // Assert - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - var addressesWrapper = customerWrapper.Container.ToDictionary(PropertyMapper)["Addresses"] as IEnumerable>; - Assert.Equal(2, addressesWrapper.Count()); - - var properties = addressesWrapper.ElementAt(0).Container.ToDictionary(PropertyMapper); - Assert.Equal("Being Rd", properties["Street"]); - Assert.Equal("Region#1", properties["Region"]); - - properties = addressesWrapper.ElementAt(1).Container.ToDictionary(PropertyMapper); - Assert.Equal("148TH AVE NE", properties["Street"]); - Assert.Equal("Region#2", properties["Region"]); - } + Street = "148TH AVE NE", + Region = "Redmond" + } + }; + Expression source = Expression.Constant(aCustomer); + string select = "Name,HomeAddress/Street"; + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + + // Assert + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + + var customerProperties = customerWrapper.Container.ToDictionary(PropertyMapper); + Assert.Equal("Peter", customerProperties["Name"]); + + SelectExpandWrapper addressWrapper = customerProperties["HomeAddress"] as SelectExpandWrapper; + var addressProperties = addressWrapper.Container.ToDictionary(PropertyMapper); + var streetProperty = Assert.Single(addressProperties); + Assert.Equal("Street", streetProperty.Key); + Assert.Equal("148TH AVE NE", streetProperty.Value); + } - [Fact] - public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedTopAndSubStructuralProperties() + [Theory] + [InlineData("HomeAddress/Codes", "C1,C4,C2")] + [InlineData("HomeAddress($select=Codes)", "C1,C4,C2")] + [InlineData("HomeAddress/Codes($top=2;$skip=1)", "C4,C2")] + [InlineData("HomeAddress($select=Codes($top=1;$skip=2))", "C2")] + [InlineData("HomeAddress/Codes($orderby=$this)", "C1,C2,C4")] + [InlineData("HomeAddress($select=Codes($orderby=$this desc))", "C4,C2,C1")] + [InlineData("HomeAddress($select=Codes($filter=$this eq 'C2'))", "C2")] + public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedSubCollectionStructuralProperties(string select, string expect) + { + // Arrange + QueryCustomer aCustomer = new QueryCustomer { - // Arrange - QueryCustomer aCustomer = new QueryCustomer + HomeAddress = new QueryAddress { - Name = "Peter", - HomeAddress = new QueryAddress - { - Street = "148TH AVE NE", - Region = "Redmond" - } - }; - Expression source = Expression.Constant(aCustomer); - string select = "Name,HomeAddress/Street"; - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - - // Assert - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - - var customerProperties = customerWrapper.Container.ToDictionary(PropertyMapper); - Assert.Equal("Peter", customerProperties["Name"]); - - SelectExpandWrapper addressWrapper = customerProperties["HomeAddress"] as SelectExpandWrapper; - var addressProperties = addressWrapper.Container.ToDictionary(PropertyMapper); - var streetProperty = Assert.Single(addressProperties); - Assert.Equal("Street", streetProperty.Key); - Assert.Equal("148TH AVE NE", streetProperty.Value); - } + Codes = new [] { "C1", "C4" , "C2" } + } + }; + Expression source = Expression.Constant(aCustomer); + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + + // Assert + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + SelectExpandWrapper addressWrapper = customerWrapper.Container.ToDictionary(PropertyMapper)["HomeAddress"] as SelectExpandWrapper; + var addressProperties = addressWrapper.Container.ToDictionary(PropertyMapper); + var codeProperty = Assert.Single(addressProperties); + Assert.Equal("Codes", codeProperty.Key); + var codes = codeProperty.Value as IEnumerable; + Assert.Equal(expect, string.Join(",", codes)); + } - [Theory] - [InlineData("HomeAddress/Codes", "C1,C4,C2")] - [InlineData("HomeAddress($select=Codes)", "C1,C4,C2")] - [InlineData("HomeAddress/Codes($top=2;$skip=1)", "C4,C2")] - [InlineData("HomeAddress($select=Codes($top=1;$skip=2))", "C2")] - [InlineData("HomeAddress/Codes($orderby=$this)", "C1,C2,C4")] - [InlineData("HomeAddress($select=Codes($orderby=$this desc))", "C4,C2,C1")] - [InlineData("HomeAddress($select=Codes($filter=$this eq 'C2'))", "C2")] - public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedSubCollectionStructuralProperties(string select, string expect) + [Theory] + [InlineData("Addresses/Codes", "C1,C4,C2", "C3,C6,C5")] + [InlineData("Addresses($select=Codes)", "C1,C4,C2", "C3,C6,C5")] + [InlineData("Addresses/Codes($top=2;$skip=1)", "C4,C2", "C6,C5")] + [InlineData("Addresses($select=Codes($top=1;$skip=2))", "C2", "C5")] + [InlineData("Addresses/Codes($orderby=$this)", "C1,C2,C4", "C3,C5,C6")] + [InlineData("Addresses($select=Codes($orderby=$this desc))", "C4,C2,C1", "C6,C5,C3")] + [InlineData("Addresses($select=Codes($filter=$this eq 'C2'))", "C2", "")] + public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedToCollectionAndSubCollectionStructuralProperties(string select, string expect1, string expect2) + { + // Arrange + QueryCustomer aCustomer = new QueryCustomer { - // Arrange - QueryCustomer aCustomer = new QueryCustomer + Addresses = new List { - HomeAddress = new QueryAddress + new QueryCnAddress { Codes = new [] { "C1", "C4" , "C2" } - } - }; - Expression source = Expression.Constant(aCustomer); - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - - // Assert - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - SelectExpandWrapper addressWrapper = customerWrapper.Container.ToDictionary(PropertyMapper)["HomeAddress"] as SelectExpandWrapper; - var addressProperties = addressWrapper.Container.ToDictionary(PropertyMapper); - var codeProperty = Assert.Single(addressProperties); - Assert.Equal("Codes", codeProperty.Key); - var codes = codeProperty.Value as IEnumerable; - Assert.Equal(expect, string.Join(",", codes)); - } - - [Theory] - [InlineData("Addresses/Codes", "C1,C4,C2", "C3,C6,C5")] - [InlineData("Addresses($select=Codes)", "C1,C4,C2", "C3,C6,C5")] - [InlineData("Addresses/Codes($top=2;$skip=1)", "C4,C2", "C6,C5")] - [InlineData("Addresses($select=Codes($top=1;$skip=2))", "C2", "C5")] - [InlineData("Addresses/Codes($orderby=$this)", "C1,C2,C4", "C3,C5,C6")] - [InlineData("Addresses($select=Codes($orderby=$this desc))", "C4,C2,C1", "C6,C5,C3")] - [InlineData("Addresses($select=Codes($filter=$this eq 'C2'))", "C2", "")] - public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedToCollectionAndSubCollectionStructuralProperties(string select, string expect1, string expect2) - { - // Arrange - QueryCustomer aCustomer = new QueryCustomer - { - Addresses = new List + }, + new QueryUsAddress { - new QueryCnAddress - { - Codes = new [] { "C1", "C4" , "C2" } - }, - new QueryUsAddress - { - Codes = new [] { "C3", "C6", "C5" } - } + Codes = new [] { "C3", "C6", "C5" } } - }; + } + }; - Expression source = Expression.Constant(aCustomer); - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); + Expression source = Expression.Constant(aCustomer); + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - // Assert - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - var addressesWrapper = customerWrapper.Container.ToDictionary(PropertyMapper)["Addresses"] as IEnumerable>; - Assert.Equal(2, addressesWrapper.Count()); + // Assert + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + var addressesWrapper = customerWrapper.Container.ToDictionary(PropertyMapper)["Addresses"] as IEnumerable>; + Assert.Equal(2, addressesWrapper.Count()); - var properties = addressesWrapper.ElementAt(0).Container.ToDictionary(PropertyMapper); - var codes = properties["Codes"] as IEnumerable; - Assert.Equal(expect1, string.Join(",", codes)); + var properties = addressesWrapper.ElementAt(0).Container.ToDictionary(PropertyMapper); + var codes = properties["Codes"] as IEnumerable; + Assert.Equal(expect1, string.Join(",", codes)); - properties = addressesWrapper.ElementAt(1).Container.ToDictionary(PropertyMapper); - codes = properties["Codes"] as IEnumerable; - Assert.Equal(expect2, string.Join(",", codes)); - } + properties = addressesWrapper.ElementAt(1).Container.ToDictionary(PropertyMapper); + codes = properties["Codes"] as IEnumerable; + Assert.Equal(expect2, string.Join(",", codes)); + } - [Fact(Skip = "ODL parses the following select as \"HomeAddress\\dynamic\\dynamic\", that's not correct.")] - public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedTypeCastSubStructuralProperties() + [Fact(Skip = "ODL parses the following select as \"HomeAddress\\dynamic\\dynamic\", that's not correct.")] + public void ProjectAsWrapper_Element_ProjectedValueContains_SelectedTypeCastSubStructuralProperties() + { + // Arrange + QueryCustomer aCustomer = new QueryCustomer { - // Arrange - QueryCustomer aCustomer = new QueryCustomer + HomeAddress = new QueryCnAddress { - HomeAddress = new QueryCnAddress - { - Street = "Cn Street", - PostCode = "201501", - } - }; - Expression source = Expression.Constant(aCustomer); + Street = "Cn Street", + PostCode = "201501", + } + }; + Expression source = Expression.Constant(aCustomer); - string select = "HomeAddress/Street,HomeAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCnAddress/PostCode"; - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); + string select = "HomeAddress/Street,HomeAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCnAddress/PostCode"; + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - // Assert - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - SelectExpandWrapper addressWrapper = customerWrapper.Container.ToDictionary(PropertyMapper)["HomeAddress"] as SelectExpandWrapper; - var addressProperties = addressWrapper.Container.ToDictionary(PropertyMapper); - Assert.Equal(2, addressProperties.Count); + // Assert + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + SelectExpandWrapper addressWrapper = customerWrapper.Container.ToDictionary(PropertyMapper)["HomeAddress"] as SelectExpandWrapper; + var addressProperties = addressWrapper.Container.ToDictionary(PropertyMapper); + Assert.Equal(2, addressProperties.Count); - Assert.Equal("Cn Street", addressProperties["Street"]); - Assert.Equal("201501", addressProperties["PostCode"]); - } + Assert.Equal("Cn Street", addressProperties["Street"]); + Assert.Equal("201501", addressProperties["PostCode"]); + } - [Fact] - public void ProjectAsWrapper_Element_ProjectedValueContainsSubKeys_IfDollarRefInDollarExpand() + [Fact] + public void ProjectAsWrapper_Element_ProjectedValueContainsSubKeys_IfDollarRefInDollarExpand() + { + // Arrange + string expand = "Orders/$ref"; + QueryCustomer aCustomer = new QueryCustomer { - // Arrange - string expand = "Orders/$ref"; - QueryCustomer aCustomer = new QueryCustomer + Orders = new[] { - Orders = new[] - { - new QueryOrder { Id = 42 }, - new QueryVipOrder { Id = 38 } - } - }; - Expression source = Expression.Constant(aCustomer); - SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - - // Assert - Assert.Equal(ExpressionType.MemberInit, projection.NodeType); - Assert.NotEmpty((projection as MemberInitExpression).Bindings.Where(p => p.Member.Name == "Instance")); - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - - var orders = customerWrapper.Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; - Assert.NotNull(orders); - Assert.Equal(2, orders.Count()); - Assert.Equal(42, orders.ElementAt(0).Container.ToDictionary(PropertyMapper)["Id"]); - Assert.Equal(38, orders.ElementAt(1).Container.ToDictionary(PropertyMapper)["Id"]); - } + new QueryOrder { Id = 42 }, + new QueryVipOrder { Id = 38 } + } + }; + Expression source = Expression.Constant(aCustomer); + SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + + // Assert + Assert.Equal(ExpressionType.MemberInit, projection.NodeType); + Assert.NotEmpty((projection as MemberInitExpression).Bindings.Where(p => p.Member.Name == "Instance")); + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + + var orders = customerWrapper.Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; + Assert.NotNull(orders); + Assert.Equal(2, orders.Count()); + Assert.Equal(42, orders.ElementAt(0).Container.ToDictionary(PropertyMapper)["Id"]); + Assert.Equal(38, orders.ElementAt(1).Container.ToDictionary(PropertyMapper)["Id"]); + } - [Fact] - public void ProjectAsWrapper_Element_ProjectedValueContainsSubKeys_IfDollarRefInDollarExpand_AndNestedFilterClause() + [Fact] + public void ProjectAsWrapper_Element_ProjectedValueContainsSubKeys_IfDollarRefInDollarExpand_AndNestedFilterClause() + { + // Arrange + string expand = "Orders/$ref($filter =Title eq 'abc')"; + QueryCustomer aCustomer = new QueryCustomer { - // Arrange - string expand = "Orders/$ref($filter =Title eq 'abc')"; - QueryCustomer aCustomer = new QueryCustomer + Orders = new[] { - Orders = new[] - { - new QueryOrder { Id = 42, Title = "xyz" }, - new QueryVipOrder { Id = 38, Title = "abc" } - } - }; + new QueryOrder { Id = 42, Title = "xyz" }, + new QueryVipOrder { Id = 38, Title = "abc" } + } + }; - Expression source = Expression.Constant(aCustomer); - SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); + Expression source = Expression.Constant(aCustomer); + SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - // Assert - Assert.Equal(ExpressionType.MemberInit, projection.NodeType); - Assert.NotEmpty((projection as MemberInitExpression).Bindings.Where(p => p.Member.Name == "Instance")); - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + // Assert + Assert.Equal(ExpressionType.MemberInit, projection.NodeType); + Assert.NotEmpty((projection as MemberInitExpression).Bindings.Where(p => p.Member.Name == "Instance")); + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - var orders = customerWrapper.Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; - Assert.NotNull(orders); - var order = Assert.Single(orders); // only one - Assert.Equal(38, order.Container.ToDictionary(PropertyMapper)["Id"]); - } + var orders = customerWrapper.Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; + Assert.NotNull(orders); + var order = Assert.Single(orders); // only one + Assert.Equal(38, order.Container.ToDictionary(PropertyMapper)["Id"]); + } - [Theory] - [InlineData("Orders($filter=Customer/HomeAddress/Cities/any(e:e/CityName eq 1001))", new[] { "QueryOrder1" })] - [InlineData("Orders($filter=Customer/HomeAddress/Cities/any(e:e/CityName eq 1002))", new[] { "QueryOrder1", "QueryOrder3" })] - [InlineData("Orders($filter=Customer/HomeAddress/Cities/any(e:e/CityName eq 1003))", new[] { "QueryOrder2", "QueryOrder3" })] - public void ProjectAsWrapper_Element_ExpandAndFilterByAny(string expand, object expected) - { - // Arrange - // Customer?$expand=Orders($filter=Customer/HomeAddress/Cities/any(e:e/CityName eq 1001)) - // Customer?$expand=Orders($filter=Customer/HomeAddress/Cities/any(e:e/CityName eq 1002)) - // Customer?$expand=Orders($filter=Customer/HomeAddress/Cities/any(e:e/CityName eq 1003)) + [Theory] + [InlineData("Orders($filter=Customer/HomeAddress/Cities/any(e:e/CityName eq 1001))", new[] { "QueryOrder1" })] + [InlineData("Orders($filter=Customer/HomeAddress/Cities/any(e:e/CityName eq 1002))", new[] { "QueryOrder1", "QueryOrder3" })] + [InlineData("Orders($filter=Customer/HomeAddress/Cities/any(e:e/CityName eq 1003))", new[] { "QueryOrder2", "QueryOrder3" })] + public void ProjectAsWrapper_Element_ExpandAndFilterByAny(string expand, object expected) + { + // Arrange + // Customer?$expand=Orders($filter=Customer/HomeAddress/Cities/any(e:e/CityName eq 1001)) + // Customer?$expand=Orders($filter=Customer/HomeAddress/Cities/any(e:e/CityName eq 1002)) + // Customer?$expand=Orders($filter=Customer/HomeAddress/Cities/any(e:e/CityName eq 1003)) - var city1 = new QueryCity() { Id = 1, CityName = 1001 }; - var city2 = new QueryCity() { Id = 2, CityName = 1002 }; - var city3 = new QueryCity() { Id = 3, CityName = 1003 }; - var city4 = new QueryCity() { Id = 4, CityName = 1004 }; + var city1 = new QueryCity() { Id = 1, CityName = 1001 }; + var city2 = new QueryCity() { Id = 2, CityName = 1002 }; + var city3 = new QueryCity() { Id = 3, CityName = 1003 }; + var city4 = new QueryCity() { Id = 4, CityName = 1004 }; - QueryCustomer customer1 = new QueryCustomer + QueryCustomer customer1 = new QueryCustomer + { + HomeAddress = new QueryAddress { - HomeAddress = new QueryAddress - { - Cities = new List() { city1, city2 } - } - }; + Cities = new List() { city1, city2 } + } + }; - QueryCustomer customer2 = new QueryCustomer + QueryCustomer customer2 = new QueryCustomer + { + HomeAddress = new QueryAddress { - HomeAddress = new QueryAddress - { - Cities = new List() { city3, city4 } - } - }; + Cities = new List() { city3, city4 } + } + }; - QueryCustomer customer3 = new QueryCustomer + QueryCustomer customer3 = new QueryCustomer + { + HomeAddress = new QueryAddress { - HomeAddress = new QueryAddress - { - Cities = new List() { city2, city3 } - } - }; + Cities = new List() { city2, city3 } + } + }; - var orders = new List - { - new QueryOrder{ Title = "QueryOrder1", Customer = customer1 }, - new QueryOrder{ Title = "QueryOrder2", Customer = customer2 }, - new QueryOrder{ Title = "QueryOrder3", Customer = customer3 }, - }; + var orders = new List + { + new QueryOrder{ Title = "QueryOrder1", Customer = customer1 }, + new QueryOrder{ Title = "QueryOrder2", Customer = customer2 }, + new QueryOrder{ Title = "QueryOrder3", Customer = customer3 }, + }; - Expression source = Expression.Constant(new QueryCustomer() { Orders = orders }); - SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); + Expression source = Expression.Constant(new QueryCustomer() { Orders = orders }); + SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - // Assert - var customerWrappers = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - var orderWrappers = customerWrappers.Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; - var orderTitleList = orderWrappers.Select(s => s.Instance.Title).ToList(); - Assert.Equal(expected, orderTitleList); - } + // Assert + var customerWrappers = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + var orderWrappers = customerWrappers.Container.ToDictionary(PropertyMapper)["Orders"] as IEnumerable>; + var orderTitleList = orderWrappers.Select(s => s.Instance.Title).ToList(); + Assert.Equal(expected, orderTitleList); + } - [Fact] - public void ProjectAsWrapper_Element_ProjectedValueContainsSubKeys_IfDollarRefInDollarExpandOnSubNavigationProperty() + [Fact] + public void ProjectAsWrapper_Element_ProjectedValueContainsSubKeys_IfDollarRefInDollarExpandOnSubNavigationProperty() + { + // Arrange + string expand = "HomeAddress/RelatedCity/$ref"; + QueryCustomer aCustomer = new QueryCustomer { - // Arrange - string expand = "HomeAddress/RelatedCity/$ref"; - QueryCustomer aCustomer = new QueryCustomer + HomeAddress = new QueryAddress { - HomeAddress = new QueryAddress + Street = "156TH", + RelatedCity = new QueryCity { - Street = "156TH", - RelatedCity = new QueryCity - { - Id = 101 - } + Id = 101 } - }; - Expression source = Expression.Constant(aCustomer); - SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - - // Assert - Assert.Equal(ExpressionType.MemberInit, projection.NodeType); - Assert.NotEmpty((projection as MemberInitExpression).Bindings.Where(p => p.Member.Name == "Instance")); - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + } + }; + Expression source = Expression.Constant(aCustomer); + SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + + // Assert + Assert.Equal(ExpressionType.MemberInit, projection.NodeType); + Assert.NotEmpty((projection as MemberInitExpression).Bindings.Where(p => p.Member.Name == "Instance")); + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + + var homeAddress = customerWrapper.Container.ToDictionary(PropertyMapper)["HomeAddress"] as SelectExpandWrapper; + var relatedCity = homeAddress.Container.ToDictionary(PropertyMapper)["RelatedCity"] as SelectExpandWrapper; + Assert.Equal(101, relatedCity.Container.ToDictionary(PropertyMapper)["Id"]); + } - var homeAddress = customerWrapper.Container.ToDictionary(PropertyMapper)["HomeAddress"] as SelectExpandWrapper; - var relatedCity = homeAddress.Container.ToDictionary(PropertyMapper)["RelatedCity"] as SelectExpandWrapper; - Assert.Equal(101, relatedCity.Container.ToDictionary(PropertyMapper)["Id"]); - } + [Theory] + [InlineData("Name", false, 3)] // 3 => Id, Name, ETag + [InlineData("HomeAddress/Street", true, 3)] // 3 => ID, HomeAddress/Street, ETag + [InlineData("Name,HomeAddress/Street", true, 4)] // 4 => ID, Name, HomeAddress/Street, ETag + [InlineData("NS.*", false, 2)] // 2 => ID, ETag + public void ProjectAsWrapper_ReturnsKeysAndConcurrencyProperties_EvenIfNotPresentInSelectClause(string select, bool containAddress, int count) + { + // Arrange + IEdmStructuralProperty etagProperty = _customer.DeclaredStructuralProperties().FirstOrDefault(c => c.Name == "CustomerETag"); + Assert.NotNull(etagProperty); // Guard + ((EdmModel)_model).SetOptimisticConcurrencyAnnotation(_customers, new[] { etagProperty }); - [Theory] - [InlineData("Name", false, 3)] // 3 => Id, Name, ETag - [InlineData("HomeAddress/Street", true, 3)] // 3 => ID, HomeAddress/Street, ETag - [InlineData("Name,HomeAddress/Street", true, 4)] // 4 => ID, Name, HomeAddress/Street, ETag - [InlineData("NS.*", false, 2)] // 2 => ID, ETag - public void ProjectAsWrapper_ReturnsKeysAndConcurrencyProperties_EvenIfNotPresentInSelectClause(string select, bool containAddress, int count) + QueryCustomer aCustomer = new QueryCustomer { - // Arrange - IEdmStructuralProperty etagProperty = _customer.DeclaredStructuralProperties().FirstOrDefault(c => c.Name == "CustomerETag"); - Assert.NotNull(etagProperty); // Guard - ((EdmModel)_model).SetOptimisticConcurrencyAnnotation(_customers, new[] { etagProperty }); - - QueryCustomer aCustomer = new QueryCustomer + Id = 42, + Name = "Peter", + HomeAddress = new QueryAddress { - Id = 42, - Name = "Peter", - HomeAddress = new QueryAddress - { - Street = "MyStreet" - }, - CustomerETag = 1.14926 - }; - Expression source = Expression.Constant(aCustomer); - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); + Street = "MyStreet" + }, + CustomerETag = 1.14926 + }; + Expression source = Expression.Constant(aCustomer); + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); - // Act - Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); + // Act + Expression projection = _binder.ProjectAsWrapper(_queryBinderContext, source, selectExpandClause, _customer, _customers); - // Assert - SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; - var customerSelectedProperties = customerWrapper.Container.ToDictionary(PropertyMapper); - Assert.Equal(count, customerSelectedProperties.Count); + // Assert + SelectExpandWrapper customerWrapper = Expression.Lambda(projection).Compile().DynamicInvoke() as SelectExpandWrapper; + var customerSelectedProperties = customerWrapper.Container.ToDictionary(PropertyMapper); + Assert.Equal(count, customerSelectedProperties.Count); - Assert.Equal(42, customerSelectedProperties["Id"]); - Assert.Equal(1.14926, customerSelectedProperties["CustomerETag"]); + Assert.Equal(42, customerSelectedProperties["Id"]); + Assert.Equal(1.14926, customerSelectedProperties["CustomerETag"]); - if (containAddress) - { - SelectExpandWrapper addressWrapper = customerSelectedProperties["HomeAddress"] as SelectExpandWrapper; - var addressSelectedProperties = addressWrapper.Container.ToDictionary(PropertyMapper); - Assert.Single(addressSelectedProperties); - Assert.Equal("MyStreet", addressSelectedProperties["Street"]); - } + if (containAddress) + { + SelectExpandWrapper addressWrapper = customerSelectedProperties["HomeAddress"] as SelectExpandWrapper; + var addressSelectedProperties = addressWrapper.Container.ToDictionary(PropertyMapper); + Assert.Single(addressSelectedProperties); + Assert.Equal("MyStreet", addressSelectedProperties["Street"]); } + } - #region GetSelectExpandProperties Tests - [Theory] - [InlineData("HomeAddress")] // $select=property - [InlineData("Addresses")] - [InlineData("Emails")] - [InlineData("Name")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/Level")] // $select=typeCast/Property - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/Birthday")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/Taxes")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/VipAddress")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/VipAddresses")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/HomeAddress")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/Addresses")] - public void GetSelectExpandProperties_ForDirectProperty_OutputCorrectProperties(string select) - { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - IDictionary propertiesToInclude; - IDictionary propertiesToExpand; - ISet autoSelectedProperties; - IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, - out propertiesToInclude, - out propertiesToExpand, - out autoSelectedProperties); - - // Assert - Assert.False(selectExpandClause.AllSelected); // guard - SelectItem selectItem = selectExpandClause.SelectedItems.First(); - PathSelectItem pathSelectItem = Assert.IsType(selectItem); // Guard - - Assert.Empty(dynamicPathSegments); - Assert.Null(propertiesToExpand); // No navigation property to expand - - Assert.NotNull(autoSelectedProperties); // auto select the keys - Assert.Equal("Id", Assert.Single(autoSelectedProperties).Name); - - Assert.NotNull(propertiesToInclude); // has one structural property to select - var propertyToInclude = Assert.Single(propertiesToInclude); - - string[] segments = select.Split('/'); - if (segments.Length == 2) - { - Assert.Equal(segments[1], propertyToInclude.Key.Name); - } - else - { - Assert.Equal(segments[0], propertyToInclude.Key.Name); - } + #region GetSelectExpandProperties Tests + [Theory] + [InlineData("HomeAddress")] // $select=property + [InlineData("Addresses")] + [InlineData("Emails")] + [InlineData("Name")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/Level")] // $select=typeCast/Property + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/Birthday")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/Taxes")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/VipAddress")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/VipAddresses")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/HomeAddress")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/Addresses")] + public void GetSelectExpandProperties_ForDirectProperty_OutputCorrectProperties(string select) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); - Assert.NotNull(propertyToInclude.Value); - Assert.Same(pathSelectItem, propertyToInclude.Value); - } + // Act + IDictionary propertiesToInclude; + IDictionary propertiesToExpand; + ISet autoSelectedProperties; + IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, + out propertiesToInclude, + out propertiesToExpand, + out autoSelectedProperties); - [Theory] - [InlineData("HomeAddress,HomeAddress/Codes")] - [InlineData("HomeAddress,HomeAddress/Codes($top=2)")] - public void GetSelectExpandProperties_ForSelectAllAndSelectSpecialy_OutputCorrectProperties(string select) - { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - IDictionary propertiesToInclude; - IDictionary propertiesToExpand; - ISet autoSelectedProperties; - IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, - out propertiesToInclude, - out propertiesToExpand, - out autoSelectedProperties); - - // Assert - Assert.Empty(dynamicPathSegments); - Assert.Null(propertiesToExpand); - - Assert.NotNull(autoSelectedProperties); - Assert.Equal("Id", Assert.Single(autoSelectedProperties).Name); // Key and ETag + // Assert + Assert.False(selectExpandClause.AllSelected); // guard + SelectItem selectItem = selectExpandClause.SelectedItems.First(); + PathSelectItem pathSelectItem = Assert.IsType(selectItem); // Guard - Assert.NotNull(propertiesToInclude); - var propertyToInclude = Assert.Single(propertiesToInclude); - Assert.Equal("HomeAddress", propertyToInclude.Key.Name); + Assert.Empty(dynamicPathSegments); + Assert.Null(propertiesToExpand); // No navigation property to expand - Assert.NotNull(propertyToInclude.Value.SelectAndExpand); - Assert.True(propertyToInclude.Value.SelectAndExpand.AllSelected); - var selectItem = Assert.Single(propertyToInclude.Value.SelectAndExpand.SelectedItems); - Assert.IsType(selectItem); - } + Assert.NotNull(autoSelectedProperties); // auto select the keys + Assert.Equal("Id", Assert.Single(autoSelectedProperties).Name); - [Theory] - [InlineData("HomeAddress/Street,HomeAddress/Region,HomeAddress/Codes")] - [InlineData("HomeAddress($select=Street,Region,Codes)")] - public void GetSelectExpandProperties_ForMultipleSubPropertiesSelection_OutputCorrectProperties(string select) - { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); + Assert.NotNull(propertiesToInclude); // has one structural property to select + var propertyToInclude = Assert.Single(propertiesToInclude); - // Act - IDictionary propertiesToInclude; - IDictionary propertiesToExpand; - ISet autoSelectedProperties; - IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, - out propertiesToInclude, - out propertiesToExpand, - out autoSelectedProperties); + string[] segments = select.Split('/'); + if (segments.Length == 2) + { + Assert.Equal(segments[1], propertyToInclude.Key.Name); + } + else + { + Assert.Equal(segments[0], propertyToInclude.Key.Name); + } - // Assert - Assert.Empty(dynamicPathSegments); - Assert.Null(propertiesToExpand); // No navigation property to expand + Assert.NotNull(propertyToInclude.Value); + Assert.Same(pathSelectItem, propertyToInclude.Value); + } - Assert.NotNull(autoSelectedProperties); // auto select the keys & ETags - Assert.Equal("Id", Assert.Single(autoSelectedProperties).Name); + [Theory] + [InlineData("HomeAddress,HomeAddress/Codes")] + [InlineData("HomeAddress,HomeAddress/Codes($top=2)")] + public void GetSelectExpandProperties_ForSelectAllAndSelectSpecialy_OutputCorrectProperties(string select) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + IDictionary propertiesToInclude; + IDictionary propertiesToExpand; + ISet autoSelectedProperties; + IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, + out propertiesToInclude, + out propertiesToExpand, + out autoSelectedProperties); + + // Assert + Assert.Empty(dynamicPathSegments); + Assert.Null(propertiesToExpand); + + Assert.NotNull(autoSelectedProperties); + Assert.Equal("Id", Assert.Single(autoSelectedProperties).Name); // Key and ETag + + Assert.NotNull(propertiesToInclude); + var propertyToInclude = Assert.Single(propertiesToInclude); + Assert.Equal("HomeAddress", propertyToInclude.Key.Name); + + Assert.NotNull(propertyToInclude.Value.SelectAndExpand); + Assert.True(propertyToInclude.Value.SelectAndExpand.AllSelected); + var selectItem = Assert.Single(propertyToInclude.Value.SelectAndExpand.SelectedItems); + Assert.IsType(selectItem); + } - Assert.NotNull(propertiesToInclude); // has one structural property to select - var propertyToInclude = Assert.Single(propertiesToInclude); - Assert.Equal("HomeAddress", propertyToInclude.Key.Name); + [Theory] + [InlineData("HomeAddress/Street,HomeAddress/Region,HomeAddress/Codes")] + [InlineData("HomeAddress($select=Street,Region,Codes)")] + public void GetSelectExpandProperties_ForMultipleSubPropertiesSelection_OutputCorrectProperties(string select) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); - Assert.NotNull(propertyToInclude.Value); + // Act + IDictionary propertiesToInclude; + IDictionary propertiesToExpand; + ISet autoSelectedProperties; + IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, + out propertiesToInclude, + out propertiesToExpand, + out autoSelectedProperties); - Assert.NotNull(propertyToInclude.Value.SelectAndExpand); // Sub select & expand - Assert.False(propertyToInclude.Value.SelectAndExpand.AllSelected); + // Assert + Assert.Empty(dynamicPathSegments); + Assert.Null(propertiesToExpand); // No navigation property to expand - Assert.Equal(3, propertyToInclude.Value.SelectAndExpand.SelectedItems.Count()); // Street, Region, Codes + Assert.NotNull(autoSelectedProperties); // auto select the keys & ETags + Assert.Equal("Id", Assert.Single(autoSelectedProperties).Name); - Assert.Equal(new [] { "Street", "Region", "Codes"}, - propertyToInclude.Value.SelectAndExpand.SelectedItems.Select(s => - { - PathSelectItem subSelectItem = (PathSelectItem)s; - PropertySegment propertySegment = subSelectItem.SelectedPath.Single() as PropertySegment; - Assert.NotNull(propertySegment); - return propertySegment.Property.Name; - })); - } + Assert.NotNull(propertiesToInclude); // has one structural property to select + var propertyToInclude = Assert.Single(propertiesToInclude); + Assert.Equal("HomeAddress", propertyToInclude.Key.Name); - [Theory] - [InlineData("CustomerDynamicProperty1", true)] - [InlineData("CustomerDynamicProperty2", true)] - [InlineData("HomeAddress/AddressDynPriperty", false)] - public void GetSelectExpandProperties_ForDynamicProperty_OutputCorrectBoolean(string select, bool expect) - { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - IDictionary propertiesToInclude; - IDictionary propertiesToExpand; - ISet autoSelectedProperties; - IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, - out propertiesToInclude, out propertiesToExpand, out autoSelectedProperties); - - // Assert - if (select.StartsWith("HomeAddress")) - { - Assert.NotNull(propertiesToInclude); - } - else - { - Assert.Null(propertiesToInclude); - } + Assert.NotNull(propertyToInclude.Value); - Assert.Null(propertiesToExpand); - Assert.NotNull(autoSelectedProperties); + Assert.NotNull(propertyToInclude.Value.SelectAndExpand); // Sub select & expand + Assert.False(propertyToInclude.Value.SelectAndExpand.AllSelected); - Assert.False(selectExpandClause.AllSelected); // guard - Assert.Equal(expect, dynamicPathSegments.Any()); - } + Assert.Equal(3, propertyToInclude.Value.SelectAndExpand.SelectedItems.Count()); // Street, Region, Codes - [Theory] - [InlineData("Orders")] - [InlineData("PrivateOrder")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrder")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrders")] - public void GetSelectExpandProperties_SkipForNavigationSelection(string select) + Assert.Equal(new [] { "Street", "Region", "Codes"}, + propertyToInclude.Value.SelectAndExpand.SelectedItems.Select(s => { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - IDictionary propertiesToInclude; - IDictionary propertiesToExpand; - ISet autoSelectedProperties; - IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, - out propertiesToInclude, - out propertiesToExpand, - out autoSelectedProperties); - - // Assert - Assert.Empty(dynamicPathSegments); - Assert.Null(propertiesToInclude); - Assert.Null(propertiesToExpand); + PathSelectItem subSelectItem = (PathSelectItem)s; + PropertySegment propertySegment = subSelectItem.SelectedPath.Single() as PropertySegment; + Assert.NotNull(propertySegment); + return propertySegment.Property.Name; + })); + } - Assert.NotNull(autoSelectedProperties); - } + [Theory] + [InlineData("CustomerDynamicProperty1", true)] + [InlineData("CustomerDynamicProperty2", true)] + [InlineData("HomeAddress/AddressDynPriperty", false)] + public void GetSelectExpandProperties_ForDynamicProperty_OutputCorrectBoolean(string select, bool expect) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); - [Theory] - [InlineData("PrivateOrder")] - [InlineData("PrivateOrder/$ref")] - [InlineData("Orders")] - [InlineData("Orders/$ref")] - [InlineData("Orders($top=2;$count=true)")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrder")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrder/$ref")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrders")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrders/$ref")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrders($search=abc)")] - public void GetSelectExpandProperties_ForDirectNavigationProperty_ReturnsProperties(string expand) - { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - IDictionary propertiesToInclude; - IDictionary propertiesToExpand; - ISet autoSelectedProperties; - IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, - out propertiesToInclude, - out propertiesToExpand, - out autoSelectedProperties); - - // Assert - var selectItem = Assert.Single(selectExpandClause.SelectedItems); - ExpandedReferenceSelectItem expandedItem = selectItem as ExpandedReferenceSelectItem; - Assert.NotNull(expandedItem); - var navigationSegment = expandedItem.PathToNavigationProperty.First(p => p is NavigationPropertySegment) as NavigationPropertySegment; - - Assert.Empty(dynamicPathSegments); // not container dynamic properties selection - Assert.Null(propertiesToInclude); // no structural properties to include - Assert.Null(autoSelectedProperties); // no auto select properties - - Assert.NotNull(propertiesToExpand); - var propertyToExpand = Assert.Single(propertiesToExpand); - - Assert.Same(navigationSegment.NavigationProperty, propertyToExpand.Key); - Assert.Same(expandedItem, propertyToExpand.Value); - } + // Act + IDictionary propertiesToInclude; + IDictionary propertiesToExpand; + ISet autoSelectedProperties; + IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, + out propertiesToInclude, out propertiesToExpand, out autoSelectedProperties); - [Theory] - [InlineData("HomeAddress/RelatedCity")] - [InlineData("HomeAddress/RelatedCity/$ref")] - [InlineData("HomeAddress/Cities")] - [InlineData("HomeAddress/Cities($top=2)")] - [InlineData("HomeAddress/Cities/$ref")] - [InlineData("Addresses/RelatedCity")] - [InlineData("Addresses/RelatedCity/$ref")] - [InlineData("Addresses/Cities")] - [InlineData("Addresses/Cities($count=true)")] - [InlineData("Addresses/Cities/$ref")] - [InlineData("HomeAddress/Info/InfoCity")] - [InlineData("Addresses/Info/InfoCity")] - [InlineData("HomeAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryUsAddress/UsCity")] - [InlineData("HomeAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryUsAddress/UsCities")] - [InlineData("HomeAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryUsAddress/UsCities($select=CityName)")] - [InlineData("HomeAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryUsAddress/UsCities/$ref")] - [InlineData("Addresses/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCnAddress/CnCity")] - [InlineData("Addresses/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCnAddress/CnCities")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/VipAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCnAddress/CnCities")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/VipAddresses/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryUsAddress/UsCities")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/HomeAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCnAddress/CnCities")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/Addresses/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryUsAddress/UsCities")] - public void GetSelectExpandProperties_ForNonDirectNavigationProperty_ReturnsCorrectExpandedProperties(string expand) + // Assert + if (select.StartsWith("HomeAddress")) { - // Arrange - SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - IDictionary propertiesToInclude; - IDictionary propertiesToExpand; - ISet autoSelectedProperties; - IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, - out propertiesToInclude, - out propertiesToExpand, - out autoSelectedProperties); - - // Assert - var selectItem = Assert.Single(selectExpandClause.SelectedItems); - ExpandedReferenceSelectItem expandedItem = selectItem as ExpandedReferenceSelectItem; - Assert.NotNull(expandedItem); - var propertySegment = expandedItem.PathToNavigationProperty.First(p => p is PropertySegment) as PropertySegment; - - Assert.Null(propertiesToExpand); // nothing to expand at current level - - Assert.NotEmpty(propertiesToInclude); - var propertyToInclude = Assert.Single(propertiesToInclude); - - Assert.Same(propertySegment.Property, propertyToInclude.Key); - Assert.NotNull(propertyToInclude.Value); - - PathSelectItem pathItem = propertyToInclude.Value; - Assert.NotNull(pathItem); - Assert.NotNull(pathItem.SelectAndExpand); - Assert.True(pathItem.SelectAndExpand.AllSelected); - var nextLevelSelectItem = Assert.Single(pathItem.SelectAndExpand.SelectedItems); - var nextLevelExpandedItem = nextLevelSelectItem as ExpandedReferenceSelectItem; - Assert.NotNull(nextLevelExpandedItem); + Assert.NotNull(propertiesToInclude); } - - [Fact] - public void GetSelectExpandProperties_FoSelectAndExpand_ReturnsCorrectExpandedProperties() + else { - // Arrange - string select = "HomeAddress($select=Street),Addresses/Codes($top=2)"; - string expand = "HomeAddress/RelatedCity/$ref,HomeAddress/Cities($count=true),PrivateOrder"; - SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand, _model, _customer, _customers); - Assert.NotNull(selectExpandClause); - - // Act - IDictionary propertiesToInclude; - IDictionary propertiesToExpand; - ISet autoSelectedProperties; - IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, - out propertiesToInclude, - out propertiesToExpand, - out autoSelectedProperties); - - // Arrange - Assert.Empty(dynamicPathSegments); - - Assert.NotNull(selectExpandClause); - Assert.False(selectExpandClause.AllSelected); - - // Why it's 6, because ODL includes "HomeAddress" as Selected automatic when parsing $expand=HomeAddress/Nav - // It's an issue reported at: https://github.com/OData/odata.net/issues/1574 - Assert.Equal(6, selectExpandClause.SelectedItems.Count()); - - Assert.NotNull(propertiesToInclude); - Assert.Equal(2, propertiesToInclude.Count); - Assert.Equal(new[] { "HomeAddress", "Addresses" }, propertiesToInclude.Keys.Select(e => e.Name)); - - Assert.NotNull(propertiesToExpand); - var propertyToExpand = Assert.Single(propertiesToExpand); - Assert.Equal("PrivateOrder", propertyToExpand.Key.Name); - - Assert.NotNull(autoSelectedProperties); - Assert.Equal("Id", Assert.Single(autoSelectedProperties).Name); + Assert.Null(propertiesToInclude); } - #endregion + Assert.Null(propertiesToExpand); + Assert.NotNull(autoSelectedProperties); - #region CreatePropertyNameExpression Tests - [Fact] - public void CreatePropertyNameExpression_ReturnsCorrectExpression() - { - // Arrange - // Retrieve base info - IEdmProperty baseProperty = _customer.FindProperty("PrivateOrder"); - Assert.NotNull(baseProperty); // Guard - - // Retrieve derived info - IEdmEntityType vipCustomer = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "QueryVipCustomer"); - Assert.NotNull(vipCustomer); // Guard - IEdmProperty derivedProperty = vipCustomer.FindProperty("Birthday"); - Assert.NotNull(derivedProperty); // Guard - - Expression source = Expression.Parameter(typeof(QueryCustomer), "aCustomer"); - SelectExpandBinder binder = GetBinder(_model); - - // Act & Assert - // #1. Base property on base type - Expression property = binder.CreatePropertyNameExpression(_queryBinderContext, _customer, baseProperty, source); - Assert.Equal(ExpressionType.Constant, property.NodeType); - Assert.Equal(typeof(string), property.Type); - Assert.Equal("PrivateOrder", (property as ConstantExpression).Value); - - // #2. Base property on derived type - property = binder.CreatePropertyNameExpression(_queryBinderContext, vipCustomer, baseProperty, source); - Assert.Equal(ExpressionType.Constant, property.NodeType); - Assert.Equal(typeof(string), property.Type); - Assert.Equal("PrivateOrder", (property as ConstantExpression).Value); - - // #3. Derived property on base type - property = binder.CreatePropertyNameExpression(_queryBinderContext, _customer, derivedProperty, source); - Assert.Equal(ExpressionType.Conditional, property.NodeType); - Assert.Equal(typeof(string), property.Type); - Assert.Equal("IIF((aCustomer Is QueryVipCustomer), \"Birthday\", null)", property.ToString()); - - // #4. Derived property on derived type. - property = binder.CreatePropertyNameExpression(_queryBinderContext, vipCustomer, derivedProperty, source); - Assert.Equal(ExpressionType.Constant, property.NodeType); - Assert.Equal(typeof(string), property.Type); - Assert.Equal("Birthday", (property as ConstantExpression).Value); - } + Assert.False(selectExpandClause.AllSelected); // guard + Assert.Equal(expect, dynamicPathSegments.Any()); + } - [Fact(Skip = "if (!castType.IsAssignableFrom(originalType)) retrun true?")] - public void CreatePropertyNameExpression_ReturnsConstantExpression_IfPropertyTypeCannotAssignedToElementType() - { - // Arrange - IEdmComplexType complexType = _model.SchemaElements.OfType().First(c => c.Name == "QueryAddress"); - Assert.False(complexType.IsOrInheritsFrom(_customer)); // make sure order has no inheritance-ship with customer. + [Theory] + [InlineData("Orders")] + [InlineData("PrivateOrder")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrder")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrders")] + public void GetSelectExpandProperties_SkipForNavigationSelection(string select) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(select, null, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + IDictionary propertiesToInclude; + IDictionary propertiesToExpand; + ISet autoSelectedProperties; + IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, + out propertiesToInclude, + out propertiesToExpand, + out autoSelectedProperties); + + // Assert + Assert.Empty(dynamicPathSegments); + Assert.Null(propertiesToInclude); + Assert.Null(propertiesToExpand); + + Assert.NotNull(autoSelectedProperties); + } - IEdmProperty edmProperty = complexType.FindProperty("Street"); - Assert.NotNull(edmProperty); + [Theory] + [InlineData("PrivateOrder")] + [InlineData("PrivateOrder/$ref")] + [InlineData("Orders")] + [InlineData("Orders/$ref")] + [InlineData("Orders($top=2;$count=true)")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrder")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrder/$ref")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrders")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrders/$ref")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/SpecialOrders($search=abc)")] + public void GetSelectExpandProperties_ForDirectNavigationProperty_ReturnsProperties(string expand) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + IDictionary propertiesToInclude; + IDictionary propertiesToExpand; + ISet autoSelectedProperties; + IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, + out propertiesToInclude, + out propertiesToExpand, + out autoSelectedProperties); + + // Assert + var selectItem = Assert.Single(selectExpandClause.SelectedItems); + ExpandedReferenceSelectItem expandedItem = selectItem as ExpandedReferenceSelectItem; + Assert.NotNull(expandedItem); + var navigationSegment = expandedItem.PathToNavigationProperty.First(p => p is NavigationPropertySegment) as NavigationPropertySegment; + + Assert.Empty(dynamicPathSegments); // not container dynamic properties selection + Assert.Null(propertiesToInclude); // no structural properties to include + Assert.Null(autoSelectedProperties); // no auto select properties + + Assert.NotNull(propertiesToExpand); + var propertyToExpand = Assert.Single(propertiesToExpand); + + Assert.Same(navigationSegment.NavigationProperty, propertyToExpand.Key); + Assert.Same(expandedItem, propertyToExpand.Value); + } - Expression source = Expression.Parameter(typeof(QueryCustomer), "aCustomer"); - SelectExpandBinder binder = GetBinder(_model); + [Theory] + [InlineData("HomeAddress/RelatedCity")] + [InlineData("HomeAddress/RelatedCity/$ref")] + [InlineData("HomeAddress/Cities")] + [InlineData("HomeAddress/Cities($top=2)")] + [InlineData("HomeAddress/Cities/$ref")] + [InlineData("Addresses/RelatedCity")] + [InlineData("Addresses/RelatedCity/$ref")] + [InlineData("Addresses/Cities")] + [InlineData("Addresses/Cities($count=true)")] + [InlineData("Addresses/Cities/$ref")] + [InlineData("HomeAddress/Info/InfoCity")] + [InlineData("Addresses/Info/InfoCity")] + [InlineData("HomeAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryUsAddress/UsCity")] + [InlineData("HomeAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryUsAddress/UsCities")] + [InlineData("HomeAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryUsAddress/UsCities($select=CityName)")] + [InlineData("HomeAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryUsAddress/UsCities/$ref")] + [InlineData("Addresses/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCnAddress/CnCity")] + [InlineData("Addresses/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCnAddress/CnCities")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/VipAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCnAddress/CnCities")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/VipAddresses/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryUsAddress/UsCities")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/HomeAddress/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryCnAddress/CnCities")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryVipCustomer/Addresses/Microsoft.AspNetCore.OData.Tests.Query.Expressions.QueryUsAddress/UsCities")] + public void GetSelectExpandProperties_ForNonDirectNavigationProperty_ReturnsCorrectExpandedProperties(string expand) + { + // Arrange + SelectExpandClause selectExpandClause = ParseSelectExpand(null, expand, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + IDictionary propertiesToInclude; + IDictionary propertiesToExpand; + ISet autoSelectedProperties; + IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, + out propertiesToInclude, + out propertiesToExpand, + out autoSelectedProperties); + + // Assert + var selectItem = Assert.Single(selectExpandClause.SelectedItems); + ExpandedReferenceSelectItem expandedItem = selectItem as ExpandedReferenceSelectItem; + Assert.NotNull(expandedItem); + var propertySegment = expandedItem.PathToNavigationProperty.First(p => p is PropertySegment) as PropertySegment; + + Assert.Null(propertiesToExpand); // nothing to expand at current level + + Assert.NotEmpty(propertiesToInclude); + var propertyToInclude = Assert.Single(propertiesToInclude); + + Assert.Same(propertySegment.Property, propertyToInclude.Key); + Assert.NotNull(propertyToInclude.Value); + + PathSelectItem pathItem = propertyToInclude.Value; + Assert.NotNull(pathItem); + Assert.NotNull(pathItem.SelectAndExpand); + Assert.True(pathItem.SelectAndExpand.AllSelected); + var nextLevelSelectItem = Assert.Single(pathItem.SelectAndExpand.SelectedItems); + var nextLevelExpandedItem = nextLevelSelectItem as ExpandedReferenceSelectItem; + Assert.NotNull(nextLevelExpandedItem); + } - // Act - Expression property = binder.CreatePropertyNameExpression(_queryBinderContext, _customer, edmProperty, source); + [Fact] + public void GetSelectExpandProperties_FoSelectAndExpand_ReturnsCorrectExpandedProperties() + { + // Arrange + string select = "HomeAddress($select=Street),Addresses/Codes($top=2)"; + string expand = "HomeAddress/RelatedCity/$ref,HomeAddress/Cities($count=true),PrivateOrder"; + SelectExpandClause selectExpandClause = ParseSelectExpand(select, expand, _model, _customer, _customers); + Assert.NotNull(selectExpandClause); + + // Act + IDictionary propertiesToInclude; + IDictionary propertiesToExpand; + ISet autoSelectedProperties; + IList dynamicPathSegments = SelectExpandBinder.GetSelectExpandProperties(_model, _customer, _customers, selectExpandClause, + out propertiesToInclude, + out propertiesToExpand, + out autoSelectedProperties); + + // Arrange + Assert.Empty(dynamicPathSegments); + + Assert.NotNull(selectExpandClause); + Assert.False(selectExpandClause.AllSelected); + + // Why it's 6, because ODL includes "HomeAddress" as Selected automatic when parsing $expand=HomeAddress/Nav + // It's an issue reported at: https://github.com/OData/odata.net/issues/1574 + Assert.Equal(6, selectExpandClause.SelectedItems.Count()); + + Assert.NotNull(propertiesToInclude); + Assert.Equal(2, propertiesToInclude.Count); + Assert.Equal(new[] { "HomeAddress", "Addresses" }, propertiesToInclude.Keys.Select(e => e.Name)); + + Assert.NotNull(propertiesToExpand); + var propertyToExpand = Assert.Single(propertiesToExpand); + Assert.Equal("PrivateOrder", propertyToExpand.Key.Name); + + Assert.NotNull(autoSelectedProperties); + Assert.Equal("Id", Assert.Single(autoSelectedProperties).Name); + } - // Assert - Assert.Equal(ExpressionType.Constant, property.NodeType); - Assert.Equal(typeof(string), property.Type); - Assert.Equal("Street", (property as ConstantExpression).Value); - } + #endregion - [Fact] - public void CreatePropertyNameExpression_ThrowsODataException_IfMappingTypeIsNotFoundInModel() - { - // Arrange - EdmModel model = _model as EdmModel; + #region CreatePropertyNameExpression Tests + [Fact] + public void CreatePropertyNameExpression_ReturnsCorrectExpression() + { + // Arrange + // Retrieve base info + IEdmProperty baseProperty = _customer.FindProperty("PrivateOrder"); + Assert.NotNull(baseProperty); // Guard + + // Retrieve derived info + IEdmEntityType vipCustomer = _model.SchemaElements.OfType().FirstOrDefault(c => c.Name == "QueryVipCustomer"); + Assert.NotNull(vipCustomer); // Guard + IEdmProperty derivedProperty = vipCustomer.FindProperty("Birthday"); + Assert.NotNull(derivedProperty); // Guard + + Expression source = Expression.Parameter(typeof(QueryCustomer), "aCustomer"); + SelectExpandBinder binder = GetBinder(_model); + + // Act & Assert + // #1. Base property on base type + Expression property = binder.CreatePropertyNameExpression(_queryBinderContext, _customer, baseProperty, source); + Assert.Equal(ExpressionType.Constant, property.NodeType); + Assert.Equal(typeof(string), property.Type); + Assert.Equal("PrivateOrder", (property as ConstantExpression).Value); + + // #2. Base property on derived type + property = binder.CreatePropertyNameExpression(_queryBinderContext, vipCustomer, baseProperty, source); + Assert.Equal(ExpressionType.Constant, property.NodeType); + Assert.Equal(typeof(string), property.Type); + Assert.Equal("PrivateOrder", (property as ConstantExpression).Value); + + // #3. Derived property on base type + property = binder.CreatePropertyNameExpression(_queryBinderContext, _customer, derivedProperty, source); + Assert.Equal(ExpressionType.Conditional, property.NodeType); + Assert.Equal(typeof(string), property.Type); + Assert.Equal("IIF((aCustomer Is QueryVipCustomer), \"Birthday\", null)", property.ToString()); + + // #4. Derived property on derived type. + property = binder.CreatePropertyNameExpression(_queryBinderContext, vipCustomer, derivedProperty, source); + Assert.Equal(ExpressionType.Constant, property.NodeType); + Assert.Equal(typeof(string), property.Type); + Assert.Equal("Birthday", (property as ConstantExpression).Value); + } - // Create a "SubCustomer" derived from "Customer", but without the CLR type in the Edm model. - EdmEntityType subCustomer = new EdmEntityType("NS", "SubCustomer", _customer); - EdmStructuralProperty subNameProperty = subCustomer.AddStructuralProperty("SubName", EdmPrimitiveTypeKind.String); - model.AddElement(subCustomer); + [Fact(Skip = "if (!castType.IsAssignableFrom(originalType)) retrun true?")] + public void CreatePropertyNameExpression_ReturnsConstantExpression_IfPropertyTypeCannotAssignedToElementType() + { + // Arrange + IEdmComplexType complexType = _model.SchemaElements.OfType().First(c => c.Name == "QueryAddress"); + Assert.False(complexType.IsOrInheritsFrom(_customer)); // make sure order has no inheritance-ship with customer. - Expression source = Expression.Constant(new QueryCustomer()); - SelectExpandBinder binder = GetBinder(model); + IEdmProperty edmProperty = complexType.FindProperty("Street"); + Assert.NotNull(edmProperty); - // Act & Assert - ExceptionAssert.Throws(() => binder.CreatePropertyNameExpression(_queryBinderContext, _customer, subNameProperty, source), - "The provided mapping does not contain a resource for the resource type 'NS.SubCustomer'."); - } - #endregion + Expression source = Expression.Parameter(typeof(QueryCustomer), "aCustomer"); + SelectExpandBinder binder = GetBinder(_model); - #region CreatePropertyValueExpression - [Theory] - [InlineData("PrivateOrder")] - [InlineData("Orders")] - public void CreatePropertyValueExpression_NonDerivedNavigationProperty_ReturnsMemberAccessExpression(string property) - { - // Arrange - Expression source = Expression.Constant(new QueryCustomer()); - SelectExpandBinder binder = GetBinder(_model); + // Act + Expression property = binder.CreatePropertyNameExpression(_queryBinderContext, _customer, edmProperty, source); - IEdmNavigationProperty navProperty = _customer.NavigationProperties().Single(c => c.Name == property); - Assert.NotNull(navProperty); + // Assert + Assert.Equal(ExpressionType.Constant, property.NodeType); + Assert.Equal(typeof(string), property.Type); + Assert.Equal("Street", (property as ConstantExpression).Value); + } - // Act - Expression propertyValue = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, navProperty, source, null); + [Fact] + public void CreatePropertyNameExpression_ThrowsODataException_IfMappingTypeIsNotFoundInModel() + { + // Arrange + EdmModel model = _model as EdmModel; - // Assert - Assert.Equal(ExpressionType.MemberAccess, propertyValue.NodeType); - Assert.Equal(typeof(QueryCustomer).GetProperty(property), (propertyValue as MemberExpression).Member); - Assert.Equal(String.Format("{0}.{1}", source.ToString(), property), propertyValue.ToString()); - } + // Create a "SubCustomer" derived from "Customer", but without the CLR type in the Edm model. + EdmEntityType subCustomer = new EdmEntityType("NS", "SubCustomer", _customer); + EdmStructuralProperty subNameProperty = subCustomer.AddStructuralProperty("SubName", EdmPrimitiveTypeKind.String); + model.AddElement(subCustomer); - [Theory] - [InlineData("SpecialOrder")] - [InlineData("SpecialOrders")] - public void CreatePropertyValueExpression_DerivedNavigationProperty_ReturnsPropertyAccessExpression(string property) - { - // Arrange - Expression source = Expression.Constant(new QueryCustomer()); - SelectExpandBinder binder = GetBinder(_model); + Expression source = Expression.Constant(new QueryCustomer()); + SelectExpandBinder binder = GetBinder(model); - IEdmStructuredType vipCustomer = _model.SchemaElements.OfType().First(c => c.Name == "QueryVipCustomer"); - Assert.NotNull(vipCustomer); + // Act & Assert + ExceptionAssert.Throws(() => binder.CreatePropertyNameExpression(_queryBinderContext, _customer, subNameProperty, source), + "The provided mapping does not contain a resource for the resource type 'NS.SubCustomer'."); + } + #endregion - IEdmNavigationProperty specialProperty = vipCustomer.DeclaredNavigationProperties().First(c => c.Name == property); - Assert.NotNull(specialProperty); + #region CreatePropertyValueExpression + [Theory] + [InlineData("PrivateOrder")] + [InlineData("Orders")] + public void CreatePropertyValueExpression_NonDerivedNavigationProperty_ReturnsMemberAccessExpression(string property) + { + // Arrange + Expression source = Expression.Constant(new QueryCustomer()); + SelectExpandBinder binder = GetBinder(_model); - // Act - Expression propertyValue = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, specialProperty, source, null); + IEdmNavigationProperty navProperty = _customer.NavigationProperties().Single(c => c.Name == property); + Assert.NotNull(navProperty); - // Assert - Assert.Equal(String.Format("({0} As QueryVipCustomer).{1}", source.ToString(), property), propertyValue.ToString()); - } + // Act + Expression propertyValue = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, navProperty, source, null); - [Theory] - [InlineData("Level")] - [InlineData("Birthday")] - [InlineData("Bonus")] - public void CreatePropertyValueExpression_DerivedValueProperty_ReturnsPropertyAccessExpression(string property) - { - // Arrange - Expression source = Expression.Constant(new QueryCustomer()); - SelectExpandBinder binder = GetBinder(_model); + // Assert + Assert.Equal(ExpressionType.MemberAccess, propertyValue.NodeType); + Assert.Equal(typeof(QueryCustomer).GetProperty(property), (propertyValue as MemberExpression).Member); + Assert.Equal(String.Format("{0}.{1}", source.ToString(), property), propertyValue.ToString()); + } - IEdmStructuredType vipCustomer = _model.SchemaElements.OfType().First(c => c.Name == "QueryVipCustomer"); - Assert.NotNull(vipCustomer); + [Theory] + [InlineData("SpecialOrder")] + [InlineData("SpecialOrders")] + public void CreatePropertyValueExpression_DerivedNavigationProperty_ReturnsPropertyAccessExpression(string property) + { + // Arrange + Expression source = Expression.Constant(new QueryCustomer()); + SelectExpandBinder binder = GetBinder(_model); - IEdmStructuralProperty edmProperty = vipCustomer.DeclaredStructuralProperties().First(c => c.Name == property); - Assert.NotNull(vipCustomer); + IEdmStructuredType vipCustomer = _model.SchemaElements.OfType().First(c => c.Name == "QueryVipCustomer"); + Assert.NotNull(vipCustomer); - // Act - Expression propertyValue = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, edmProperty, source, null); + IEdmNavigationProperty specialProperty = vipCustomer.DeclaredNavigationProperties().First(c => c.Name == property); + Assert.NotNull(specialProperty); - // Assert - Assert.Equal(String.Format("Convert(({0} As QueryVipCustomer).{1}, Nullable`1)", source.ToString(), property), propertyValue.ToString()); - } + // Act + Expression propertyValue = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, specialProperty, source, null); - [Theory] - [InlineData("Taxes")] - [InlineData("VipAddress")] - [InlineData("VipAddresses")] - public void CreatePropertyValueExpression_DerivedReferenceProperty_ReturnsPropertyAccessExpression(string property) - { - // Arrange - Expression source = Expression.Constant(new QueryCustomer()); - SelectExpandBinder binder = GetBinder(_model); + // Assert + Assert.Equal(String.Format("({0} As QueryVipCustomer).{1}", source.ToString(), property), propertyValue.ToString()); + } - IEdmStructuredType vipCustomer = _model.SchemaElements.OfType().First(c => c.Name == "QueryVipCustomer"); - Assert.NotNull(vipCustomer); + [Theory] + [InlineData("Level")] + [InlineData("Birthday")] + [InlineData("Bonus")] + public void CreatePropertyValueExpression_DerivedValueProperty_ReturnsPropertyAccessExpression(string property) + { + // Arrange + Expression source = Expression.Constant(new QueryCustomer()); + SelectExpandBinder binder = GetBinder(_model); - IEdmStructuralProperty edmProperty = vipCustomer.DeclaredStructuralProperties().First(c => c.Name == property); - Assert.NotNull(vipCustomer); + IEdmStructuredType vipCustomer = _model.SchemaElements.OfType().First(c => c.Name == "QueryVipCustomer"); + Assert.NotNull(vipCustomer); - // Act - Expression propertyValue = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, edmProperty, source, null); + IEdmStructuralProperty edmProperty = vipCustomer.DeclaredStructuralProperties().First(c => c.Name == property); + Assert.NotNull(vipCustomer); - // Assert - Assert.Equal(String.Format("({0} As QueryVipCustomer).{1}", source.ToString(), property), propertyValue.ToString()); - } + // Act + Expression propertyValue = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, edmProperty, source, null); - [Fact] - public void CreatePropertyValueExpression_HandleNullPropagationTrue_AddsNullCheck() - { - // Arrange - _settings.HandleNullPropagation = HandleNullPropagationOption.True; - Expression source = Expression.Constant(new QueryCustomer()); - SelectExpandBinder binder = GetBinder(_model); - IEdmProperty idProperty = _customer.StructuralProperties().Single(p => p.Name == "Id"); - - // Act - Expression property = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, idProperty, source, null); - - // Assert - // NetFx and NetCore differ in the way Expression is converted to a string. - Assert.Equal(ExpressionType.Conditional, property.NodeType); - ConditionalExpression conditionalExpression = property as ConditionalExpression; - Assert.NotNull(conditionalExpression); - Assert.Equal(typeof(int?), conditionalExpression.Type); - - Assert.Equal(ExpressionType.Convert, conditionalExpression.IfFalse.NodeType); - UnaryExpression falseUnaryExpression = conditionalExpression.IfFalse as UnaryExpression; - Assert.NotNull(falseUnaryExpression); - Assert.Equal(String.Format("{0}.Id", source.ToString()), falseUnaryExpression.Operand.ToString()); - Assert.Equal(typeof(int?), falseUnaryExpression.Type); - - Assert.Equal(ExpressionType.Constant, conditionalExpression.IfTrue.NodeType); - ConstantExpression trueUnaryExpression = conditionalExpression.IfTrue as ConstantExpression; - Assert.NotNull(trueUnaryExpression); - Assert.Equal("null", trueUnaryExpression.ToString()); - - Assert.Equal(ExpressionType.Equal, conditionalExpression.Test.NodeType); - BinaryExpression binaryExpression = conditionalExpression.Test as BinaryExpression; - Assert.NotNull(binaryExpression); - Assert.Equal(source.ToString(), binaryExpression.Left.ToString()); - Assert.Equal("null", binaryExpression.Right.ToString()); - Assert.Equal(typeof(bool), binaryExpression.Type); - } + // Assert + Assert.Equal(String.Format("Convert(({0} As QueryVipCustomer).{1}, Nullable`1)", source.ToString(), property), propertyValue.ToString()); + } - [Fact] - public void CreatePropertyValueExpression_HandleNullPropagationFalse_ConvertsToNullableType() - { - // Arrange - _settings.HandleNullPropagation = HandleNullPropagationOption.False; - Expression source = Expression.Constant(new QueryCustomer()); - SelectExpandBinder binder = GetBinder(_model); - IEdmProperty idProperty = _customer.StructuralProperties().Single(p => p.Name == "Id"); - - // Act - Expression property = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, idProperty, source, filterClause: null); - - // Assert - Assert.Equal(String.Format("Convert({0}.Id, Nullable`1)", source.ToString()), property.ToString()); - Assert.Equal(typeof(int?), property.Type); - } + [Theory] + [InlineData("Taxes")] + [InlineData("VipAddress")] + [InlineData("VipAddresses")] + public void CreatePropertyValueExpression_DerivedReferenceProperty_ReturnsPropertyAccessExpression(string property) + { + // Arrange + Expression source = Expression.Constant(new QueryCustomer()); + SelectExpandBinder binder = GetBinder(_model); - // OData.ModelBuilder 1.0.4 make the ClrTypeAnnotation ctor throws argument null exception - // 1.0.5 will allow null. So, please enable this when update to 1.0.5 model builder. - [Fact(Skip = "OData.ModelBuilder 1.0.4 throws null reference for ClrTypeAnnotation with null")] - public void CreatePropertyValueExpression_Collection_ThrowsODataException_IfMappingTypeIsNotFoundInModel() - { - // Arrange - _model.SetAnnotationValue(_order, new ClrTypeAnnotation(null)); + IEdmStructuredType vipCustomer = _model.SchemaElements.OfType().First(c => c.Name == "QueryVipCustomer"); + Assert.NotNull(vipCustomer); - var source = Expression.Constant(new QueryCustomer - { - Orders = new[] - { - new QueryOrder { Id = 1 }, - new QueryOrder { Id = 2 } - } - }); + IEdmStructuralProperty edmProperty = vipCustomer.DeclaredStructuralProperties().First(c => c.Name == property); + Assert.NotNull(vipCustomer); + + // Act + Expression propertyValue = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, edmProperty, source, null); - SelectExpandBinder binder = GetBinder(_model); + // Assert + Assert.Equal(String.Format("({0} As QueryVipCustomer).{1}", source.ToString(), property), propertyValue.ToString()); + } - var ordersProperty = _customer.NavigationProperties().Single(p => p.Name == "Orders"); + [Fact] + public void CreatePropertyValueExpression_HandleNullPropagationTrue_AddsNullCheck() + { + // Arrange + _settings.HandleNullPropagation = HandleNullPropagationOption.True; + Expression source = Expression.Constant(new QueryCustomer()); + SelectExpandBinder binder = GetBinder(_model); + IEdmProperty idProperty = _customer.StructuralProperties().Single(p => p.Name == "Id"); + + // Act + Expression property = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, idProperty, source, null); + + // Assert + // NetFx and NetCore differ in the way Expression is converted to a string. + Assert.Equal(ExpressionType.Conditional, property.NodeType); + ConditionalExpression conditionalExpression = property as ConditionalExpression; + Assert.NotNull(conditionalExpression); + Assert.Equal(typeof(int?), conditionalExpression.Type); + + Assert.Equal(ExpressionType.Convert, conditionalExpression.IfFalse.NodeType); + UnaryExpression falseUnaryExpression = conditionalExpression.IfFalse as UnaryExpression; + Assert.NotNull(falseUnaryExpression); + Assert.Equal(String.Format("{0}.Id", source.ToString()), falseUnaryExpression.Operand.ToString()); + Assert.Equal(typeof(int?), falseUnaryExpression.Type); + + Assert.Equal(ExpressionType.Constant, conditionalExpression.IfTrue.NodeType); + ConstantExpression trueUnaryExpression = conditionalExpression.IfTrue as ConstantExpression; + Assert.NotNull(trueUnaryExpression); + Assert.Equal("null", trueUnaryExpression.ToString()); + + Assert.Equal(ExpressionType.Equal, conditionalExpression.Test.NodeType); + BinaryExpression binaryExpression = conditionalExpression.Test as BinaryExpression; + Assert.NotNull(binaryExpression); + Assert.Equal(source.ToString(), binaryExpression.Left.ToString()); + Assert.Equal("null", binaryExpression.Right.ToString()); + Assert.Equal(typeof(bool), binaryExpression.Type); + } - SelectExpandClause selectExpand = ParseSelectExpand(null, "Orders($filter=Id eq 1)", _model, _customer, _customers); - ExpandedNavigationSelectItem expandItem = Assert.Single(selectExpand.SelectedItems) as ExpandedNavigationSelectItem; - Assert.NotNull(expandItem); - Assert.NotNull(expandItem.FilterOption); + [Fact] + public void CreatePropertyValueExpression_HandleNullPropagationFalse_ConvertsToNullableType() + { + // Arrange + _settings.HandleNullPropagation = HandleNullPropagationOption.False; + Expression source = Expression.Constant(new QueryCustomer()); + SelectExpandBinder binder = GetBinder(_model); + IEdmProperty idProperty = _customer.StructuralProperties().Single(p => p.Name == "Id"); + + // Act + Expression property = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, idProperty, source, filterClause: null); + + // Assert + Assert.Equal(String.Format("Convert({0}.Id, Nullable`1)", source.ToString()), property.ToString()); + Assert.Equal(typeof(int?), property.Type); + } - // Act & Assert - ExceptionAssert.Throws( - () => binder.CreatePropertyValueExpression(_queryBinderContext, _customer, ordersProperty, source, expandItem.FilterOption), - String.Format("The provided mapping does not contain a resource for the resource type '{0}'.", - ordersProperty.Type.Definition.AsElementType().FullTypeName())); - } + // OData.ModelBuilder 1.0.4 make the ClrTypeAnnotation ctor throws argument null exception + // 1.0.5 will allow null. So, please enable this when update to 1.0.5 model builder. + [Fact(Skip = "OData.ModelBuilder 1.0.4 throws null reference for ClrTypeAnnotation with null")] + public void CreatePropertyValueExpression_Collection_ThrowsODataException_IfMappingTypeIsNotFoundInModel() + { + // Arrange + _model.SetAnnotationValue(_order, new ClrTypeAnnotation(null)); - [Theory] - [InlineData(HandleNullPropagationOption.True)] - [InlineData(HandleNullPropagationOption.False)] - public void CreatePropertyValueExpression_Collection_Works_HandleNullPropagationOption(HandleNullPropagationOption nullOption) + var source = Expression.Constant(new QueryCustomer { - // Arrange - _settings.HandleNullPropagation = nullOption; - var source = Expression.Constant(new QueryCustomer + Orders = new[] { - Orders = new[] - { - new QueryOrder { Id = 1 }, - new QueryOrder { Id = 2 } - } - }); + new QueryOrder { Id = 1 }, + new QueryOrder { Id = 2 } + } + }); - SelectExpandBinder binder = GetBinder(_model); + SelectExpandBinder binder = GetBinder(_model); - var ordersProperty = _customer.NavigationProperties().Single(p => p.Name == "Orders"); + var ordersProperty = _customer.NavigationProperties().Single(p => p.Name == "Orders"); - SelectExpandClause selectExpand = ParseSelectExpand(null, "Orders($filter=Id eq 1)", _model, _customer, _customers); - ExpandedNavigationSelectItem expandItem = Assert.Single(selectExpand.SelectedItems) as ExpandedNavigationSelectItem; - Assert.NotNull(expandItem); - Assert.NotNull(expandItem.FilterOption); + SelectExpandClause selectExpand = ParseSelectExpand(null, "Orders($filter=Id eq 1)", _model, _customer, _customers); + ExpandedNavigationSelectItem expandItem = Assert.Single(selectExpand.SelectedItems) as ExpandedNavigationSelectItem; + Assert.NotNull(expandItem); + Assert.NotNull(expandItem.FilterOption); - // Act - var filterInExpand = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, ordersProperty, source, expandItem.FilterOption); + // Act & Assert + ExceptionAssert.Throws( + () => binder.CreatePropertyValueExpression(_queryBinderContext, _customer, ordersProperty, source, expandItem.FilterOption), + String.Format("The provided mapping does not contain a resource for the resource type '{0}'.", + ordersProperty.Type.Definition.AsElementType().FullTypeName())); + } - // Assert - if (nullOption == HandleNullPropagationOption.True) - { - Assert.Equal( - string.Format( - "IIF((value({0}) == null), null, IIF((value({0}).Orders == null), null, " + - "value({0}).Orders.Where($it => ($it.Id == value({1}).TypedProperty))))", - source.Type, - "Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Int32]"), - filterInExpand.ToString()); - } - else + [Theory] + [InlineData(HandleNullPropagationOption.True)] + [InlineData(HandleNullPropagationOption.False)] + public void CreatePropertyValueExpression_Collection_Works_HandleNullPropagationOption(HandleNullPropagationOption nullOption) + { + // Arrange + _settings.HandleNullPropagation = nullOption; + var source = Expression.Constant(new QueryCustomer + { + Orders = new[] { - Assert.Equal( - string.Format( - "value({0}).Orders.Where($it => ($it.Id == value(" + - "Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Int32]).TypedProperty))", - source.Type), - filterInExpand.ToString()); + new QueryOrder { Id = 1 }, + new QueryOrder { Id = 2 } } + }); - var orders = Expression.Lambda(filterInExpand).Compile().DynamicInvoke() as IEnumerable; - QueryOrder order = Assert.Single(orders); - Assert.Equal(1, order.Id); - } + SelectExpandBinder binder = GetBinder(_model); + + var ordersProperty = _customer.NavigationProperties().Single(p => p.Name == "Orders"); + + SelectExpandClause selectExpand = ParseSelectExpand(null, "Orders($filter=Id eq 1)", _model, _customer, _customers); + ExpandedNavigationSelectItem expandItem = Assert.Single(selectExpand.SelectedItems) as ExpandedNavigationSelectItem; + Assert.NotNull(expandItem); + Assert.NotNull(expandItem.FilterOption); - // OData.ModelBuilder 1.0.4 make the ClrTypeAnnotation ctor throws argument null exception - // 1.0.5 will allow null. So, please enable this when update to 1.0.5 model builder. - /* - [Fact] - public void CreatePropertyValueExpression_Single_ThrowsODataException_IfMappingTypeIsNotFoundInModel() + // Act + var filterInExpand = binder.CreatePropertyValueExpression(_queryBinderContext, _customer, ordersProperty, source, expandItem.FilterOption); + + // Assert + if (nullOption == HandleNullPropagationOption.True) { - // Arrange - _model.SetAnnotationValue(_customer, new ClrTypeAnnotation(null)); - - _settings.HandleReferenceNavigationPropertyExpandFilter = true; - var source = Expression.Constant(new QueryOrder()); - var customerProperty = _order.NavigationProperties().Single(p => p.Name == "Customer"); - - SelectExpandClause selectExpand = ParseSelectExpand(null, "Customer($filter=Id ne 1)", _model, _order, _orders); - ExpandedNavigationSelectItem expandItem = Assert.Single(selectExpand.SelectedItems) as ExpandedNavigationSelectItem; - Assert.NotNull(expandItem); - Assert.NotNull(expandItem.FilterOption); - - // Act & Assert - ExceptionAssert.Throws( - () => _binder.CreatePropertyValueExpression(_order, customerProperty, source, expandItem.FilterOption), - String.Format("The provided mapping does not contain a resource for the resource type '{0}'.", customerProperty.Type.FullName())); - }*/ - - [Fact] - public void CreatePropertyValueExpression_Single_Works_IfSettingIsOff() + Assert.Equal( + string.Format( + "IIF((value({0}) == null), null, IIF((value({0}).Orders == null), null, " + + "value({0}).Orders.Where($it => ($it.Id == value({1}).TypedProperty))))", + source.Type, + "Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Int32]"), + filterInExpand.ToString()); + } + else { - // Arrange - _settings.HandleReferenceNavigationPropertyExpandFilter = false; - var order = Expression.Constant( - new QueryOrder + Assert.Equal( + string.Format( + "value({0}).Orders.Where($it => ($it.Id == value(" + + "Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Int32]).TypedProperty))", + source.Type), + filterInExpand.ToString()); + } + + var orders = Expression.Lambda(filterInExpand).Compile().DynamicInvoke() as IEnumerable; + QueryOrder order = Assert.Single(orders); + Assert.Equal(1, order.Id); + } + + // OData.ModelBuilder 1.0.4 make the ClrTypeAnnotation ctor throws argument null exception + // 1.0.5 will allow null. So, please enable this when update to 1.0.5 model builder. + /* + [Fact] + public void CreatePropertyValueExpression_Single_ThrowsODataException_IfMappingTypeIsNotFoundInModel() + { + // Arrange + _model.SetAnnotationValue(_customer, new ClrTypeAnnotation(null)); + + _settings.HandleReferenceNavigationPropertyExpandFilter = true; + var source = Expression.Constant(new QueryOrder()); + var customerProperty = _order.NavigationProperties().Single(p => p.Name == "Customer"); + + SelectExpandClause selectExpand = ParseSelectExpand(null, "Customer($filter=Id ne 1)", _model, _order, _orders); + ExpandedNavigationSelectItem expandItem = Assert.Single(selectExpand.SelectedItems) as ExpandedNavigationSelectItem; + Assert.NotNull(expandItem); + Assert.NotNull(expandItem.FilterOption); + + // Act & Assert + ExceptionAssert.Throws( + () => _binder.CreatePropertyValueExpression(_order, customerProperty, source, expandItem.FilterOption), + String.Format("The provided mapping does not contain a resource for the resource type '{0}'.", customerProperty.Type.FullName())); + }*/ + + [Fact] + public void CreatePropertyValueExpression_Single_Works_IfSettingIsOff() + { + // Arrange + _settings.HandleReferenceNavigationPropertyExpandFilter = false; + var order = Expression.Constant( + new QueryOrder + { + Customer = new QueryCustomer { - Customer = new QueryCustomer - { - Id = 1 - } + Id = 1 } - ); + } + ); - SelectExpandBinder binder = GetBinder(_model); - var customerProperty = _order.NavigationProperties().Single(p => p.Name == "Customer"); + SelectExpandBinder binder = GetBinder(_model); + var customerProperty = _order.NavigationProperties().Single(p => p.Name == "Customer"); - SelectExpandClause selectExpand = ParseSelectExpand(null, "Customer($filter=Id ne 1)", _model, _order, _orders); - Assert.True(selectExpand.AllSelected); // Guard - ExpandedNavigationSelectItem expandItem = Assert.Single(selectExpand.SelectedItems) as ExpandedNavigationSelectItem; - Assert.NotNull(expandItem); - Assert.NotNull(expandItem.FilterOption); + SelectExpandClause selectExpand = ParseSelectExpand(null, "Customer($filter=Id ne 1)", _model, _order, _orders); + Assert.True(selectExpand.AllSelected); // Guard + ExpandedNavigationSelectItem expandItem = Assert.Single(selectExpand.SelectedItems) as ExpandedNavigationSelectItem; + Assert.NotNull(expandItem); + Assert.NotNull(expandItem.FilterOption); - // Act - var filterInExpand = binder.CreatePropertyValueExpression(_queryBinderContext, _order, customerProperty, order, expandItem.FilterOption); + // Act + var filterInExpand = binder.CreatePropertyValueExpression(_queryBinderContext, _order, customerProperty, order, expandItem.FilterOption); - // Assert - var customer = Expression.Lambda(filterInExpand).Compile().DynamicInvoke() as QueryCustomer; - Assert.NotNull(customer); - Assert.Equal(1, customer.Id); - } + // Assert + var customer = Expression.Lambda(filterInExpand).Compile().DynamicInvoke() as QueryCustomer; + Assert.NotNull(customer); + Assert.Equal(1, customer.Id); + } - [Theory] - [InlineData(HandleNullPropagationOption.True)] - [InlineData(HandleNullPropagationOption.False)] - public void CreatePropertyValueExpression_Single_Works_HandleNullPropagationOption(HandleNullPropagationOption nullOption) - { - // Arrange - _settings.HandleReferenceNavigationPropertyExpandFilter = true; - _settings.HandleNullPropagation = nullOption; - var source = Expression.Constant( - new QueryOrder + [Theory] + [InlineData(HandleNullPropagationOption.True)] + [InlineData(HandleNullPropagationOption.False)] + public void CreatePropertyValueExpression_Single_Works_HandleNullPropagationOption(HandleNullPropagationOption nullOption) + { + // Arrange + _settings.HandleReferenceNavigationPropertyExpandFilter = true; + _settings.HandleNullPropagation = nullOption; + var source = Expression.Constant( + new QueryOrder + { + Customer = new QueryCustomer { - Customer = new QueryCustomer - { - Id = 1 - } + Id = 1 } - ); - - SelectExpandBinder binder = GetBinder(_model); - var customerProperty = _order.NavigationProperties().Single(p => p.Name == "Customer"); + } + ); - SelectExpandClause selectExpand = ParseSelectExpand(null, "Customer($filter=Id ne 1)", _model, _order, _orders); - Assert.True(selectExpand.AllSelected); // Guard - ExpandedNavigationSelectItem expandItem = Assert.Single(selectExpand.SelectedItems) as ExpandedNavigationSelectItem; - Assert.NotNull(expandItem); - Assert.NotNull(expandItem.FilterOption); + SelectExpandBinder binder = GetBinder(_model); + var customerProperty = _order.NavigationProperties().Single(p => p.Name == "Customer"); - // Act - var filterInExpand = binder.CreatePropertyValueExpression(_queryBinderContext, _order, customerProperty, source, expandItem.FilterOption); + SelectExpandClause selectExpand = ParseSelectExpand(null, "Customer($filter=Id ne 1)", _model, _order, _orders); + Assert.True(selectExpand.AllSelected); // Guard + ExpandedNavigationSelectItem expandItem = Assert.Single(selectExpand.SelectedItems) as ExpandedNavigationSelectItem; + Assert.NotNull(expandItem); + Assert.NotNull(expandItem.FilterOption); - // Assert - if (nullOption == HandleNullPropagationOption.True) - { - Assert.Equal( - string.Format( - "IIF((value({0}) == null), null, IIF((value({0}).Customer == null), null, " + - "IIF((value({0}).Customer.Id != value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Int32]).TypedProperty), " + - "value({0}).Customer, null)))", - source.Type), - filterInExpand.ToString()); - } - else - { - Assert.Equal( - string.Format( - "IIF((value({0}).Customer.Id != value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Int32]).TypedProperty), " + - "value({0}).Customer, null)", - source.Type), - filterInExpand.ToString()); - } + // Act + var filterInExpand = binder.CreatePropertyValueExpression(_queryBinderContext, _order, customerProperty, source, expandItem.FilterOption); - var customer = Expression.Lambda(filterInExpand).Compile().DynamicInvoke() as QueryCustomer; - Assert.Null(customer); + // Assert + if (nullOption == HandleNullPropagationOption.True) + { + Assert.Equal( + string.Format( + "IIF((value({0}) == null), null, IIF((value({0}).Customer == null), null, " + + "IIF((value({0}).Customer.Id != value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Int32]).TypedProperty), " + + "value({0}).Customer, null)))", + source.Type), + filterInExpand.ToString()); } - #endregion - - [Fact] - public void CreateTypeNameExpression_ReturnsNull_IfTypeHasNoDerivedTypes() + else { - // Arrange - IEdmEntityType baseType = new EdmEntityType("NS", "BaseType"); - EdmModel model = new EdmModel(); - model.AddElement(baseType); + Assert.Equal( + string.Format( + "IIF((value({0}).Customer.Id != value(Microsoft.AspNetCore.OData.Query.Container.LinqParameterContainer+TypedLinqParameterContainer`1[System.Int32]).TypedProperty), " + + "value({0}).Customer, null)", + source.Type), + filterInExpand.ToString()); + } - Expression source = Expression.Constant(42); - SelectExpandBinder binder = GetBinder(_model); + var customer = Expression.Lambda(filterInExpand).Compile().DynamicInvoke() as QueryCustomer; + Assert.Null(customer); + } + #endregion - // Act - Expression result = binder.CreateTypeNameExpression(source, baseType, model); + [Fact] + public void CreateTypeNameExpression_ReturnsNull_IfTypeHasNoDerivedTypes() + { + // Arrange + IEdmEntityType baseType = new EdmEntityType("NS", "BaseType"); + EdmModel model = new EdmModel(); + model.AddElement(baseType); - // Assert - Assert.Null(result); - } + Expression source = Expression.Constant(42); + SelectExpandBinder binder = GetBinder(_model); - [Fact] - public void CreateTypeNameExpression_ThrowsODataException_IfTypeHasNoMapping() - { - // Arrange - IEdmEntityType baseType = new EdmEntityType("NS", "BaseType"); - IEdmEntityType derivedType = new EdmEntityType("NS", "DerivedType", baseType); - EdmModel model = new EdmModel(); - model.AddElement(baseType); - model.AddElement(derivedType); - - Expression source = Expression.Constant(42); - SelectExpandBinder binder = GetBinder(_model); - - // Act & Assert - ExceptionAssert.Throws( - () => binder.CreateTypeNameExpression(source, baseType, model), - "The provided mapping does not contain a resource for the resource type 'NS.DerivedType'."); - } + // Act + Expression result = binder.CreateTypeNameExpression(source, baseType, model); - [Fact] - public void CreateTypeNameExpression_ReturnsConditionalExpression_IfTypeHasDerivedTypes() - { - // Arrange - IEdmEntityType baseType = new EdmEntityType("NS", "BaseType"); - IEdmEntityType typeA = new EdmEntityType("NS", "A", baseType); - IEdmEntityType typeB = new EdmEntityType("NS", "B", baseType); - IEdmEntityType typeAA = new EdmEntityType("NS", "AA", typeA); - IEdmEntityType typeAAA = new EdmEntityType("NS", "AAA", typeAA); - IEdmEntityType[] types = new[] { baseType, typeA, typeAAA, typeB, typeAA }; - - EdmModel model = new EdmModel(); - foreach (var type in types) - { - model.AddElement(type); - model.SetAnnotationValue(type, new ClrTypeAnnotation(new MockType(type.Name, @namespace: type.Namespace))); - } - - Expression source = Expression.Constant(42); - SelectExpandBinder binder = GetBinder(_model); + // Assert + Assert.Null(result); + } - // Act - Expression result = binder.CreateTypeNameExpression(source, baseType, model); + [Fact] + public void CreateTypeNameExpression_ThrowsODataException_IfTypeHasNoMapping() + { + // Arrange + IEdmEntityType baseType = new EdmEntityType("NS", "BaseType"); + IEdmEntityType derivedType = new EdmEntityType("NS", "DerivedType", baseType); + EdmModel model = new EdmModel(); + model.AddElement(baseType); + model.AddElement(derivedType); + + Expression source = Expression.Constant(42); + SelectExpandBinder binder = GetBinder(_model); + + // Act & Assert + ExceptionAssert.Throws( + () => binder.CreateTypeNameExpression(source, baseType, model), + "The provided mapping does not contain a resource for the resource type 'NS.DerivedType'."); + } - // Assert - Assert.Equal( - @"IIF((42 Is AAA), ""NS.AAA"", IIF((42 Is AA), ""NS.AA"", IIF((42 Is B), ""NS.B"", IIF((42 Is A), ""NS.A"", ""NS.BaseType""))))", - result.ToString()); - } + [Fact] + public void CreateTypeNameExpression_ReturnsConditionalExpression_IfTypeHasDerivedTypes() + { + // Arrange + IEdmEntityType baseType = new EdmEntityType("NS", "BaseType"); + IEdmEntityType typeA = new EdmEntityType("NS", "A", baseType); + IEdmEntityType typeB = new EdmEntityType("NS", "B", baseType); + IEdmEntityType typeAA = new EdmEntityType("NS", "AA", typeA); + IEdmEntityType typeAAA = new EdmEntityType("NS", "AAA", typeAA); + IEdmEntityType[] types = new[] { baseType, typeA, typeAAA, typeB, typeAA }; - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CorrelatedSubqueryIncludesToListIfBufferingOptimizationIsTrue(bool enableOptimization) + EdmModel model = new EdmModel(); + foreach (var type in types) { - // Arrange - _settings.EnableCorrelatedSubqueryBuffering = enableOptimization; - var source = Expression.Constant(new QueryCustomer - { - Orders = new[] - { - new QueryOrder { Id = 1 }, - new QueryOrder { Id = 2 } - } - }); + model.AddElement(type); + model.SetAnnotationValue(type, new ClrTypeAnnotation(new MockType(type.Name, @namespace: type.Namespace))); + } - var expandClause = ParseSelectExpand(null, "Orders", _model, _customer, _customers); + Expression source = Expression.Constant(42); + SelectExpandBinder binder = GetBinder(_model); - // Act - var expand = _binder.ProjectAsWrapper(_queryBinderContext, source, expandClause, _customer, _customers); + // Act + Expression result = binder.CreateTypeNameExpression(source, baseType, model); - // Assert - Assert.True(expand.ToString().Contains("ToList") == enableOptimization); - } + // Assert + Assert.Equal( + @"IIF((42 Is AAA), ""NS.AAA"", IIF((42 Is AA), ""NS.AA"", IIF((42 Is B), ""NS.B"", IIF((42 Is A), ""NS.A"", ""NS.BaseType""))))", + result.ToString()); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CorrelatedSubqueryIncludesToListIfBufferingOptimizationIsTrueAndPagesizeIsSet(bool enableOptimization) + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CorrelatedSubqueryIncludesToListIfBufferingOptimizationIsTrue(bool enableOptimization) + { + // Arrange + _settings.EnableCorrelatedSubqueryBuffering = enableOptimization; + var source = Expression.Constant(new QueryCustomer { - // Arrange - _settings.EnableCorrelatedSubqueryBuffering = enableOptimization; - _settings.PageSize = 100; - var source = Expression.Constant(new QueryCustomer + Orders = new[] { - Orders = new[] - { - new QueryOrder { Id = 1 }, - new QueryOrder { Id = 2 } - } - }); - - var expandClause = ParseSelectExpand(null, "Orders", _model, _customer, _customers); + new QueryOrder { Id = 1 }, + new QueryOrder { Id = 2 } + } + }); - // Act - var expand = _binder.ProjectAsWrapper(_queryBinderContext, source, expandClause, _customer, _customers); + var expandClause = ParseSelectExpand(null, "Orders", _model, _customer, _customers); - // Assert - Assert.True(expand.ToString().Contains("ToList") == enableOptimization); - } + // Act + var expand = _binder.ProjectAsWrapper(_queryBinderContext, source, expandClause, _customer, _customers); - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - var customer = builder.EntitySet("Customers").EntityType; - builder.EntitySet("Orders"); - builder.EntitySet("Cities"); - builder.EntitySet("Products"); - - customer.Collection.Function("IsUpgraded").Returns().Namespace="NS"; - customer.Collection.Action("UpgradeAll").Namespace = "NS"; - return builder.GetEdmModel(); - } + // Assert + Assert.True(expand.ToString().Contains("ToList") == enableOptimization); + } - public static IEdmModel GetEdmModel_lowerCamelCased() - { - var builder = new ODataConventionModelBuilder(); - var customer = builder.EntitySet("Customers").EntityType; - builder.EntitySet("Orders"); - builder.EntitySet("Cities"); - builder.EntitySet("Products"); - - customer.Collection.Function("IsUpgraded").Returns().Namespace="NS"; - customer.Collection.Action("UpgradeAll").Namespace = "NS"; - builder.EnableLowerCamelCase(); - return builder.GetEdmModel(); - } - public static SelectExpandClause ParseSelectExpand(string select, string expand, IEdmModel model, IEdmType edmType, IEdmNavigationSource navigationSource) + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CorrelatedSubqueryIncludesToListIfBufferingOptimizationIsTrueAndPagesizeIsSet(bool enableOptimization) + { + // Arrange + _settings.EnableCorrelatedSubqueryBuffering = enableOptimization; + _settings.PageSize = 100; + var source = Expression.Constant(new QueryCustomer { - return new ODataQueryOptionParser(model, edmType, navigationSource, - new Dictionary - { - { "$expand", expand == null ? "" : expand }, - { "$select", select == null ? "" : select } - }).ParseSelectAndExpand(); - } + Orders = new[] + { + new QueryOrder { Id = 1 }, + new QueryOrder { Id = 2 } + } + }); - public static IDictionary InvokeSelectExpand(T instance, Expression selectExpandExp) - { - LambdaExpression projectionLambda = selectExpandExp as LambdaExpression; + var expandClause = ParseSelectExpand(null, "Orders", _model, _customer, _customers); - SelectExpandWrapper wrapper = projectionLambda.Compile().DynamicInvoke(instance) as SelectExpandWrapper; + // Act + var expand = _binder.ProjectAsWrapper(_queryBinderContext, source, expandClause, _customer, _customers); - return wrapper.ToDictionary(); - } + // Assert + Assert.True(expand.ToString().Contains("ToList") == enableOptimization); } - public class QueryCity + public static IEdmModel GetEdmModel() { - public int Id { get; set; } - - public int CityName { get; set; } + var builder = new ODataConventionModelBuilder(); + var customer = builder.EntitySet("Customers").EntityType; + builder.EntitySet("Orders"); + builder.EntitySet("Cities"); + builder.EntitySet("Products"); + + customer.Collection.Function("IsUpgraded").Returns().Namespace="NS"; + customer.Collection.Action("UpgradeAll").Namespace = "NS"; + return builder.GetEdmModel(); } - - public class QueryAddressInfo + public static IEdmModel GetEdmModel_lowerCamelCased() { - public QueryCity InfoCity { get; set; } + var builder = new ODataConventionModelBuilder(); + var customer = builder.EntitySet("Customers").EntityType; + builder.EntitySet("Orders"); + builder.EntitySet("Cities"); + builder.EntitySet("Products"); + + customer.Collection.Function("IsUpgraded").Returns().Namespace="NS"; + customer.Collection.Action("UpgradeAll").Namespace = "NS"; + builder.EnableLowerCamelCase(); + return builder.GetEdmModel(); + } + public static SelectExpandClause ParseSelectExpand(string select, string expand, IEdmModel model, IEdmType edmType, IEdmNavigationSource navigationSource) + { + return new ODataQueryOptionParser(model, edmType, navigationSource, + new Dictionary + { + { "$expand", expand == null ? "" : expand }, + { "$select", select == null ? "" : select } + }).ParseSelectAndExpand(); } - public class QueryAddress + public static IDictionary InvokeSelectExpand(T instance, Expression selectExpandExp) { - public string Street { get; set; } + LambdaExpression projectionLambda = selectExpandExp as LambdaExpression; - public string Region { get; set; } + SelectExpandWrapper wrapper = projectionLambda.Compile().DynamicInvoke(instance) as SelectExpandWrapper; - public IList Codes { get; set; } + return wrapper.ToDictionary(); + } +} - public QueryAddressInfo Info { get; set; } +public class QueryCity +{ + public int Id { get; set; } - public IList Prices { get; set; } + public int CityName { get; set; } +} - public QueryCity RelatedCity { get; set; } - [ConcurrencyCheck] // ETag on property of complex is not fully supported. - public double AddressETag { get; set; } +public class QueryAddressInfo +{ + public QueryCity InfoCity { get; set; } +} - public IList Cities { get; set; } +public class QueryAddress +{ + public string Street { get; set; } - public IDictionary AddressDynaicProperties { get; set; } - } + public string Region { get; set; } - public class QueryUsAddress : QueryAddress - { - public string ZipCode { get; set; } + public IList Codes { get; set; } - public QueryCity UsCity { get; set; } + public QueryAddressInfo Info { get; set; } - public IList UsCities { get; set; } - } + public IList Prices { get; set; } - public class QueryCnAddress : QueryAddress - { - public string PostCode { get; set; } + public QueryCity RelatedCity { get; set; } - public QueryCity CnCity { get; set; } + [ConcurrencyCheck] // ETag on property of complex is not fully supported. + public double AddressETag { get; set; } - public IList CnCities { get; set; } - } + public IList Cities { get; set; } - public class QueryOrder - { - public int Id { get; set; } + public IDictionary AddressDynaicProperties { get; set; } +} - public string Title { get; set; } +public class QueryUsAddress : QueryAddress +{ + public string ZipCode { get; set; } - public QueryCustomer Customer { get; set; } + public QueryCity UsCity { get; set; } - public IDictionary OrderProperties { get; set; } - } + public IList UsCities { get; set; } +} - [DataContract] - public class QueryProduct - { - [DataMember(Name = "ProductId")] - [Key] - public int Id { get; set; } +public class QueryCnAddress : QueryAddress +{ + public string PostCode { get; set; } - [DataMember(Name = "ProductName")] - public string Name { get; set; } + public QueryCity CnCity { get; set; } - [DataMember(Name = "ProductQuantity")] - public int Quantity { get; set; } + public IList CnCities { get; set; } +} - [DataMember(Name = "ProductTags")] - public IList Tags { get; set; } - } +public class QueryOrder +{ + public int Id { get; set; } - public class QueryProductTag - { - [Key] - public int Id { get; set; } + public string Title { get; set; } - public string Name { get; set; } - } + public QueryCustomer Customer { get; set; } - public class QueryCustomer - { - public int Id { get; set; } + public IDictionary OrderProperties { get; set; } +} - public string Name { get; set; } +[DataContract] +public class QueryProduct +{ + [DataMember(Name = "ProductId")] + [Key] + public int Id { get; set; } - public int Age { get; set; } + [DataMember(Name = "ProductName")] + public string Name { get; set; } - public IList Emails { get; set; } + [DataMember(Name = "ProductQuantity")] + public int Quantity { get; set; } - // [ConcurrencyCheck] - public double CustomerETag { get; set; } + [DataMember(Name = "ProductTags")] + public IList Tags { get; set; } +} - public QueryColor FarivateColor { get; set; } +public class QueryProductTag +{ + [Key] + public int Id { get; set; } - public QueryAddress HomeAddress { get; set; } + public string Name { get; set; } +} - public IList Addresses { get; set; } +public class QueryCustomer +{ + public int Id { get; set; } - public QueryOrder PrivateOrder { get; set; } + public string Name { get; set; } - public IList Orders { get; set; } + public int Age { get; set; } - public List TestReadonlyProperty - { - get { return new List() { "Test1", "Test2" }; } - } + public IList Emails { get; set; } - [ReadOnly(true)] - public int TestReadOnlyWithAttribute - { - get - { - return 2; - } - } + // [ConcurrencyCheck] + public double CustomerETag { get; set; } + + public QueryColor FarivateColor { get; set; } + + public QueryAddress HomeAddress { get; set; } - [ReadOnly(true)] - public int TestReadOnlyWithAttributeAndSetter + public IList Addresses { get; set; } + + public QueryOrder PrivateOrder { get; set; } + + public IList Orders { get; set; } + + public List TestReadonlyProperty + { + get { return new List() { "Test1", "Test2" }; } + } + + [ReadOnly(true)] + public int TestReadOnlyWithAttribute + { + get { - get; set; + return 2; } - - public IDictionary CustomerProperties { get; set; } } - public class QueryVipCustomer : QueryCustomer + [ReadOnly(true)] + public int TestReadOnlyWithAttributeAndSetter { - public int Level { get; set; } + get; set; + } + + public IDictionary CustomerProperties { get; set; } +} - public DateTimeOffset Birthday { get; set; } +public class QueryVipCustomer : QueryCustomer +{ + public int Level { get; set; } + + public DateTimeOffset Birthday { get; set; } - public IList Taxes { get; set; } + public IList Taxes { get; set; } - public decimal Bonus { get; set; } + public decimal Bonus { get; set; } - public QueryAddress VipAddress { get; set; } + public QueryAddress VipAddress { get; set; } - public QueryAddress[] VipAddresses { get; set; } + public QueryAddress[] VipAddresses { get; set; } - public new QueryUsAddress HomeAddress { get; set; } + public new QueryUsAddress HomeAddress { get; set; } - public new QueryCnAddress[] Addresses { get; set; } + public new QueryCnAddress[] Addresses { get; set; } - public QueryVipOrder SpecialOrder { get; set; } + public QueryVipOrder SpecialOrder { get; set; } - public QueryVipOrder[] SpecialOrders { get; set; } - } + public QueryVipOrder[] SpecialOrders { get; set; } +} - public class QueryVipOrder : QueryOrder - { - public QueryVipCustomer[] SpecialCustomers { get; set; } - } +public class QueryVipOrder : QueryOrder +{ + public QueryVipCustomer[] SpecialCustomers { get; set; } +} - public enum QueryColor - { - Red, +public enum QueryColor +{ + Red, - Green, + Green, - Blue - } + Blue } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/UriFunctionBinderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/UriFunctionBinderTests.cs index ae09cac34..763e9bca9 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/UriFunctionBinderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/UriFunctionBinderTests.cs @@ -12,460 +12,459 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions +namespace Microsoft.AspNetCore.OData.Tests.Query.Expressions; + +/// +/// Tests to UriFunctions binder. +/// +public class UriFunctionBinderTests { - /// - /// Tests to UriFunctions binder. - /// - public class UriFunctionBinderTests + #region BindUriFunctionName + + // Bind + + // Validations: + // method name null + // method name string.Empty + // methodInfo null + + [Fact] + public void BindUriFunctionName_FunctionNameNull() { - #region BindUriFunctionName + MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - // Bind + Action bindUriFunction = () => + UriFunctionsBinder.BindUriFunctionName(null, padRightStringMethodInfo); - // Validations: - // method name null - // method name string.Empty - // methodInfo null + ExceptionAssert.ThrowsArgumentNull(bindUriFunction, "functionName"); + } - [Fact] - public void BindUriFunctionName_FunctionNameNull() - { - MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); + [Fact] + public void BindUriFunctionName_FunctionNameStringEmpty() + { + MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - Action bindUriFunction = () => - UriFunctionsBinder.BindUriFunctionName(null, padRightStringMethodInfo); + Action bindUriFunction = () => + UriFunctionsBinder.BindUriFunctionName(string.Empty, padRightStringMethodInfo); - ExceptionAssert.ThrowsArgumentNull(bindUriFunction, "functionName"); - } + ExceptionAssert.ThrowsArgumentNull(bindUriFunction, "functionName"); + } - [Fact] - public void BindUriFunctionName_FunctionNameStringEmpty() + [Fact] + public void BindUriFunctionName_MethodInfoNull() + { + Action bindUriFunction = () => + UriFunctionsBinder.BindUriFunctionName("startswith", null); + + ExceptionAssert.ThrowsArgumentNull(bindUriFunction, "methodInfo"); + } + + // Add - succeeds + // Add already exists - fail + + // Type of MethodInfo: + // Static MethodInfo + // Static External MethoInfo + // Instance + // Instance different declaring type + // Add instance when static exists + // Add static when instance exists + + [Fact] + public void BindUriFunctionName_CanBind() + { + const string FUNCTION_NAME = "padright"; + MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); + + try { - MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo); - Action bindUriFunction = () => - UriFunctionsBinder.BindUriFunctionName(string.Empty, padRightStringMethodInfo); + MethodInfo resultMethoInfo; + UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(int) }, out resultMethoInfo); - ExceptionAssert.ThrowsArgumentNull(bindUriFunction, "functionName"); + Assert.Equal(padRightStringMethodInfo, resultMethoInfo); } + finally + { + Assert.True( + UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo)); + } + } + + [Fact] + public void BindUriFunctionName_CannotBindIfAlreadyBinded() + { + const string FUNCTION_NAME = "addtwice"; + MethodInfo addStrTwiceStaticMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceStatic", BindingFlags.NonPublic | BindingFlags.Static); - [Fact] - public void BindUriFunctionName_MethodInfoNull() + try { - Action bindUriFunction = () => - UriFunctionsBinder.BindUriFunctionName("startswith", null); + // Add for the first time + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo); - ExceptionAssert.ThrowsArgumentNull(bindUriFunction, "methodInfo"); - } + // Add for the second time + Action bindExisting = () => UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo); - // Add - succeeds - // Add already exists - fail + ExceptionAssert.Throws(bindExisting); + } + finally + { + Assert.True( + UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo)); + } + } - // Type of MethodInfo: - // Static MethodInfo - // Static External MethoInfo - // Instance - // Instance different declaring type - // Add instance when static exists - // Add static when instance exists + [Fact] + public void BindUriFunctionName_CanBindStaticMethod() + { + const string FUNCTION_NAME = "addtwice"; + MethodInfo addStrTwiceStaticMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceStatic", BindingFlags.NonPublic | BindingFlags.Static); - [Fact] - public void BindUriFunctionName_CanBind() + try + { + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo); + MethodInfo resultMethoInfo; + UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(string) }, out resultMethoInfo); + Assert.Equal(addStrTwiceStaticMethodInfo, resultMethoInfo); + } + finally { - const string FUNCTION_NAME = "padright"; - MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - - try - { - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo); - - MethodInfo resultMethoInfo; - UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(int) }, out resultMethoInfo); - - Assert.Equal(padRightStringMethodInfo, resultMethoInfo); - } - finally - { - Assert.True( - UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo)); - } + Assert.True( + UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo)); } + } - [Fact] - public void BindUriFunctionName_CannotBindIfAlreadyBinded() + [Fact] + public void BindUriFunctionName_CanBindStaticExtensionMethod() + { + const string FUNCTION_NAME = "addtwice"; + MethodInfo addStrTwiceStaticExtensionMethodInfo = + typeof(UriFunctionClrBinderTestsStaticExtensionMethods).GetMethod("AddStringTwice", BindingFlags.NonPublic | BindingFlags.Static); + + try { - const string FUNCTION_NAME = "addtwice"; - MethodInfo addStrTwiceStaticMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceStatic", BindingFlags.NonPublic | BindingFlags.Static); + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticExtensionMethodInfo); + + MethodInfo resultMethoInfo; + UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(string) }, out resultMethoInfo); - try - { - // Add for the first time - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo); - - // Add for the second time - Action bindExisting = () => UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo); - - ExceptionAssert.Throws(bindExisting); - } - finally - { - Assert.True( - UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo)); - } + Assert.Equal(addStrTwiceStaticExtensionMethodInfo, resultMethoInfo); + } + finally + { + Assert.True( + UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticExtensionMethodInfo)); } + } + + [Fact] + public void BindUriFunctionName_CanBindInstanceMethod() + { + // this test originally used "padright" as case, it has a running conflict with 'CustomMethod_InstanceMethodOfDeclaringType' in 'FilterBinderTests" + // Therefore, let's change it to use "PadLeft" as case. + // TODO: need to refactor the static logic for UriFunctionsBinder. + const string FUNCTION_NAME = "padLeft"; + MethodInfo padRightInstanceMethodInfo = typeof(string).GetMethod("PadLeft", new Type[] { typeof(int) }); - [Fact] - public void BindUriFunctionName_CanBindStaticMethod() + try { - const string FUNCTION_NAME = "addtwice"; - MethodInfo addStrTwiceStaticMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceStatic", BindingFlags.NonPublic | BindingFlags.Static); + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightInstanceMethodInfo); - try - { - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo); - MethodInfo resultMethoInfo; - UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(string) }, out resultMethoInfo); - Assert.Equal(addStrTwiceStaticMethodInfo, resultMethoInfo); - } - finally - { - Assert.True( - UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo)); - } - } + MethodInfo resultMethoInfo; + UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(int) }, out resultMethoInfo); - [Fact] - public void BindUriFunctionName_CanBindStaticExtensionMethod() + Assert.Equal(padRightInstanceMethodInfo, resultMethoInfo); + } + finally { - const string FUNCTION_NAME = "addtwice"; - MethodInfo addStrTwiceStaticExtensionMethodInfo = - typeof(UriFunctionClrBinderTestsStaticExtensionMethods).GetMethod("AddStringTwice", BindingFlags.NonPublic | BindingFlags.Static); + Assert.True( + UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightInstanceMethodInfo)); + } + } - try - { - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticExtensionMethodInfo); + [Fact] + public void BindUriFunctionName_CannotBindInstanceMethodOfDifferentDeclaringType() + { + const string FUNCTION_NAME = "addtwice"; + MethodInfo addTwiceInstanceThisDelcaringTypeMethodInfo = + typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceInstance", BindingFlags.NonPublic | BindingFlags.Instance); + + try + { + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addTwiceInstanceThisDelcaringTypeMethodInfo); - MethodInfo resultMethoInfo; + MethodInfo resultMethoInfo; + bool couldFindBinding = UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(string) }, out resultMethoInfo); - Assert.Equal(addStrTwiceStaticExtensionMethodInfo, resultMethoInfo); - } - finally - { - Assert.True( - UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticExtensionMethodInfo)); - } + Assert.False(couldFindBinding); + Assert.Null(resultMethoInfo); } - - [Fact] - public void BindUriFunctionName_CanBindInstanceMethod() + finally { - // this test originally used "padright" as case, it has a running conflict with 'CustomMethod_InstanceMethodOfDeclaringType' in 'FilterBinderTests" - // Therefore, let's change it to use "PadLeft" as case. - // TODO: need to refactor the static logic for UriFunctionsBinder. - const string FUNCTION_NAME = "padLeft"; - MethodInfo padRightInstanceMethodInfo = typeof(string).GetMethod("PadLeft", new Type[] { typeof(int) }); - - try - { - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightInstanceMethodInfo); - - MethodInfo resultMethoInfo; - UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(int) }, out resultMethoInfo); - - Assert.Equal(padRightInstanceMethodInfo, resultMethoInfo); - } - finally - { - Assert.True( - UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightInstanceMethodInfo)); - } + Assert.True( + UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addTwiceInstanceThisDelcaringTypeMethodInfo)); } + } - [Fact] - public void BindUriFunctionName_CannotBindInstanceMethodOfDifferentDeclaringType() - { - const string FUNCTION_NAME = "addtwice"; - MethodInfo addTwiceInstanceThisDelcaringTypeMethodInfo = - typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceInstance", BindingFlags.NonPublic | BindingFlags.Instance); - - try - { - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addTwiceInstanceThisDelcaringTypeMethodInfo); - - MethodInfo resultMethoInfo; - bool couldFindBinding = - UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(string) }, out resultMethoInfo); - - Assert.False(couldFindBinding); - Assert.Null(resultMethoInfo); - } - finally - { - Assert.True( - UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addTwiceInstanceThisDelcaringTypeMethodInfo)); - } - } + [Fact] + public void BindUriFunctionName_CannotBindStaticAndInstanceMethodWithSameArguments() + { + const string FUNCTION_NAME = "padright"; + MethodInfo padRightStaticMethodInfo = typeof(UriFunctionBinderTests).GetMethod("PadRightStatic", BindingFlags.NonPublic | BindingFlags.Static); + MethodInfo padRightInstanceMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - [Fact] - public void BindUriFunctionName_CannotBindStaticAndInstanceMethodWithSameArguments() + try { - const string FUNCTION_NAME = "padright"; - MethodInfo padRightStaticMethodInfo = typeof(UriFunctionBinderTests).GetMethod("PadRightStatic", BindingFlags.NonPublic | BindingFlags.Static); - MethodInfo padRightInstanceMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - - try - { - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightStaticMethodInfo); - Action bindingInstance = () => UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightInstanceMethodInfo); - - ExceptionAssert.Throws(bindingInstance); - } - finally - { - Assert.True( - UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightStaticMethodInfo)); - } - } + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightStaticMethodInfo); + Action bindingInstance = () => UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightInstanceMethodInfo); - [Fact] - public void BindUriFunctionName_CanBindStaticAndInstanceOfDifferentDeclerationType() + ExceptionAssert.Throws(bindingInstance); + } + finally { - const string FUNCTION_NAME = "addtwice"; - MethodInfo addTwiceStaticMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceStatic", BindingFlags.NonPublic | BindingFlags.Static); - MethodInfo addTwiceInstanceMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceInstance", BindingFlags.NonPublic | BindingFlags.Instance); - - try - { - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addTwiceStaticMethodInfo); - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addTwiceInstanceMethodInfo); - - MethodInfo resultMethoInfoStatic; - UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(string) }, out resultMethoInfoStatic); - - Assert.Equal(addTwiceStaticMethodInfo, resultMethoInfoStatic); - - MethodInfo resultMethoInfoInstance; - UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(UriFunctionBinderTests), typeof(string) }, out resultMethoInfoInstance); - - Assert.Equal(addTwiceInstanceMethodInfo, resultMethoInfoInstance); - } - finally - { - Assert.True( - UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addTwiceStaticMethodInfo)); - Assert.True( - UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addTwiceInstanceMethodInfo)); - } + Assert.True( + UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightStaticMethodInfo)); } + } + + [Fact] + public void BindUriFunctionName_CanBindStaticAndInstanceOfDifferentDeclerationType() + { + const string FUNCTION_NAME = "addtwice"; + MethodInfo addTwiceStaticMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceStatic", BindingFlags.NonPublic | BindingFlags.Static); + MethodInfo addTwiceInstanceMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceInstance", BindingFlags.NonPublic | BindingFlags.Instance); - #endregion + try + { + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addTwiceStaticMethodInfo); + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addTwiceInstanceMethodInfo); - #region UnbindUriFunctionName + MethodInfo resultMethoInfoStatic; + UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(string) }, out resultMethoInfoStatic); - // Unbind + Assert.Equal(addTwiceStaticMethodInfo, resultMethoInfoStatic); - // Validations - // method name null - // method name string.Empty - // methodInfo null + MethodInfo resultMethoInfoInstance; + UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(UriFunctionBinderTests), typeof(string) }, out resultMethoInfoInstance); - [Fact] - public void UnbindUriFunctionName_FunctionNameNull() + Assert.Equal(addTwiceInstanceMethodInfo, resultMethoInfoInstance); + } + finally { - MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); + Assert.True( + UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addTwiceStaticMethodInfo)); + Assert.True( + UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addTwiceInstanceMethodInfo)); + } + } - Action bindUriFunction = () => - UriFunctionsBinder.UnbindUriFunctionName(null, padRightStringMethodInfo); + #endregion - ExceptionAssert.ThrowsArgumentNull(bindUriFunction, "functionName"); - } + #region UnbindUriFunctionName - [Fact] - public void UnbindUriFunctionName_FunctionNameStringEmpty() - { - MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); + // Unbind - Action bindUriFunction = () => - UriFunctionsBinder.UnbindUriFunctionName(string.Empty, padRightStringMethodInfo); + // Validations + // method name null + // method name string.Empty + // methodInfo null - ExceptionAssert.ThrowsArgumentNull(bindUriFunction, "functionName"); - } + [Fact] + public void UnbindUriFunctionName_FunctionNameNull() + { + MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - [Fact] - public void UnbindUriFunctionName_MethodInfoNull() - { - Action bindUriFunction = () => - UriFunctionsBinder.UnbindUriFunctionName("startswith", null); + Action bindUriFunction = () => + UriFunctionsBinder.UnbindUriFunctionName(null, padRightStringMethodInfo); - ExceptionAssert.ThrowsArgumentNull(bindUriFunction, "methodInfo"); - } + ExceptionAssert.ThrowsArgumentNull(bindUriFunction, "functionName"); + } - // Remove - - // Removed not existing - // Remove static when instance is registered - failed - // Remove instance when static is registered - failed + [Fact] + public void UnbindUriFunctionName_FunctionNameStringEmpty() + { + MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - [Fact] - public void UnbindUriFunctionName_CanUnbind() - { - const string FUNCTION_NAME = "padright"; - MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); + Action bindUriFunction = () => + UriFunctionsBinder.UnbindUriFunctionName(string.Empty, padRightStringMethodInfo); - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo); + ExceptionAssert.ThrowsArgumentNull(bindUriFunction, "functionName"); + } - Assert.True(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo)); + [Fact] + public void UnbindUriFunctionName_MethodInfoNull() + { + Action bindUriFunction = () => + UriFunctionsBinder.UnbindUriFunctionName("startswith", null); + ExceptionAssert.ThrowsArgumentNull(bindUriFunction, "methodInfo"); + } - MethodInfo resultMethoInfo; - Assert.False(UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(int) }, out resultMethoInfo)); + // Remove - + // Removed not existing + // Remove static when instance is registered - failed + // Remove instance when static is registered - failed - Assert.Null(resultMethoInfo); - } + [Fact] + public void UnbindUriFunctionName_CanUnbind() + { + const string FUNCTION_NAME = "padright"; + MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - [Fact] - public void UnbindUriFunctionName_CannotUnbindNotBindedFunction_DifferentFunctionName() - { - const string FUNCTION_NAME = "padright"; - MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - - try - { - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo); - - Assert.False(UriFunctionsBinder.UnbindUriFunctionName("AnotherFunctionName", padRightStringMethodInfo)); - } - finally - { - Assert.True(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo)); - } - } + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo); - [Fact] - public void UnbindUriFunctionName_CannotUnbindNotBindedFunction_DifferentMethodInfo() - { - const string FUNCTION_NAME = "padright"; - MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); + Assert.True(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo)); - try - { - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo); - MethodInfo differentMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceInstance", BindingFlags.NonPublic | BindingFlags.Instance); + MethodInfo resultMethoInfo; + Assert.False(UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(int) }, out resultMethoInfo)); - Assert.False(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, differentMethodInfo)); - } - finally - { - Assert.True(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo)); - } - } + Assert.Null(resultMethoInfo); + } - [Fact] - public void UnbindUriFunctionName_CanUnbindInstanceMethod() - { - const string FUNCTION_NAME = "padright"; - MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); + [Fact] + public void UnbindUriFunctionName_CannotUnbindNotBindedFunction_DifferentFunctionName() + { + const string FUNCTION_NAME = "padright"; + MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); + try + { UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo); + Assert.False(UriFunctionsBinder.UnbindUriFunctionName("AnotherFunctionName", padRightStringMethodInfo)); + } + finally + { Assert.True(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo)); + } + } + [Fact] + public void UnbindUriFunctionName_CannotUnbindNotBindedFunction_DifferentMethodInfo() + { + const string FUNCTION_NAME = "padright"; + MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - MethodInfo resultMethoInfo; - Assert.False(UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(int) }, out resultMethoInfo)); + try + { + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo); - Assert.Null(resultMethoInfo); - } + MethodInfo differentMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceInstance", BindingFlags.NonPublic | BindingFlags.Instance); - [Fact] - public void UnbindUriFunctionName_CanUnbindStaticMethod() + Assert.False(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, differentMethodInfo)); + } + finally { - const string FUNCTION_NAME = "addtwice"; - MethodInfo addStrTwiceStaticMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceStatic", BindingFlags.NonPublic | BindingFlags.Static); + Assert.True(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo)); + } + } - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo); + [Fact] + public void UnbindUriFunctionName_CanUnbindInstanceMethod() + { + const string FUNCTION_NAME = "padright"; + MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - Assert.True(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo)); + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo); + Assert.True(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo)); - MethodInfo resultMethoInfo; - Assert.False(UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(string) }, out resultMethoInfo)); - Assert.Null(resultMethoInfo); - } + MethodInfo resultMethoInfo; + Assert.False(UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(int) }, out resultMethoInfo)); - [Fact] - public void UnbindUriFunctionName_CanUnbindExtensionStaticMethod() - { - const string FUNCTION_NAME = "addtwice"; - MethodInfo addStrTwiceStaticExtensionMethodInfo = - typeof(UriFunctionClrBinderTestsStaticExtensionMethods).GetMethod("AddStringTwice", BindingFlags.NonPublic | BindingFlags.Static); + Assert.Null(resultMethoInfo); + } - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticExtensionMethodInfo); + [Fact] + public void UnbindUriFunctionName_CanUnbindStaticMethod() + { + const string FUNCTION_NAME = "addtwice"; + MethodInfo addStrTwiceStaticMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceStatic", BindingFlags.NonPublic | BindingFlags.Static); - Assert.True(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticExtensionMethodInfo)); + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo); - MethodInfo resultMethoInfo; - Assert.False(UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(string) }, out resultMethoInfo)); + Assert.True(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo)); - Assert.Null(resultMethoInfo); - } - [Fact] - public void UnbindUriFunctionName_CannotUnbindWithDifferentMethod() - { - const string FUNCTION_NAME = "padright"; - MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); + MethodInfo resultMethoInfo; + Assert.False(UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(string) }, out resultMethoInfo)); + + Assert.Null(resultMethoInfo); + } - try - { - UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo); + [Fact] + public void UnbindUriFunctionName_CanUnbindExtensionStaticMethod() + { + const string FUNCTION_NAME = "addtwice"; + MethodInfo addStrTwiceStaticExtensionMethodInfo = + typeof(UriFunctionClrBinderTestsStaticExtensionMethods).GetMethod("AddStringTwice", BindingFlags.NonPublic | BindingFlags.Static); - MethodInfo addStrTwiceStaticMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceStatic", BindingFlags.NonPublic | BindingFlags.Static); + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticExtensionMethodInfo); - Assert.False(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo)); - } - finally - { - UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo); - } - } + Assert.True(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticExtensionMethodInfo)); - #endregion + MethodInfo resultMethoInfo; + Assert.False(UriFunctionsBinder.TryGetMethodInfo(FUNCTION_NAME, new Type[] { typeof(string), typeof(string) }, out resultMethoInfo)); - #region Private Methods - Helpers (Used by reflection) + Assert.Null(resultMethoInfo); + } - /// - /// Is used by reflection. - /// - private static string AddStringTwiceStatic(string str, string strToAdd) - { - return null; - } + [Fact] + public void UnbindUriFunctionName_CannotUnbindWithDifferentMethod() + { + const string FUNCTION_NAME = "padright"; + MethodInfo padRightStringMethodInfo = typeof(string).GetMethod("PadRight", new Type[] { typeof(int) }); - private string AddStringTwiceInstance(string strToAdd) + try { - return null; - } + UriFunctionsBinder.BindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo); + + MethodInfo addStrTwiceStaticMethodInfo = typeof(UriFunctionBinderTests).GetMethod("AddStringTwiceStatic", BindingFlags.NonPublic | BindingFlags.Static); - private static string PadRightStatic(string str, int width) + Assert.False(UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, addStrTwiceStaticMethodInfo)); + } + finally { - return str.PadRight(width); + UriFunctionsBinder.UnbindUriFunctionName(FUNCTION_NAME, padRightStringMethodInfo); } + } + + #endregion - #endregion + #region Private Methods - Helpers (Used by reflection) + + /// + /// Is used by reflection. + /// + private static string AddStringTwiceStatic(string str, string strToAdd) + { + return null; } - public static class UriFunctionClrBinderTestsStaticExtensionMethods + private string AddStringTwiceInstance(string strToAdd) { - /// - /// Is used by reflection. - /// - private static string AddStringTwice(this string str, string strToAdd) - { - return null; - } + return null; + } + + private static string PadRightStatic(string str, int width) + { + return str.PadRight(width); + } + + #endregion +} + +public static class UriFunctionClrBinderTestsStaticExtensionMethods +{ + /// + /// Is used by reflection. + /// + private static string AddStringTwice(this string str, string strToAdd) + { + return null; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/HttpRequestODataQueryExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/HttpRequestODataQueryExtensionsTests.cs index 7d64cdfbe..4c0d58e8d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/HttpRequestODataQueryExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/HttpRequestODataQueryExtensionsTests.cs @@ -25,58 +25,57 @@ using NuGet.Frameworks; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class HttpRequestODataQueryExtensionsTests { - public class HttpRequestODataQueryExtensionsTests + [Fact] + public void GetETag_ThrowsArgumentNull_Request() { - [Fact] - public void GetETag_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - HttpRequest request = null; - ExceptionAssert.ThrowsArgumentNull(() => request.GetETag(null), "request"); - } + // Arrange & Act & Assert + HttpRequest request = null; + ExceptionAssert.ThrowsArgumentNull(() => request.GetETag(null), "request"); + } - [Fact] - public void GetETag_Returns_Null() - { - // Arrange & Act & Assert - HttpRequest request = new Mock().Object; + [Fact] + public void GetETag_Returns_Null() + { + // Arrange & Act & Assert + HttpRequest request = new Mock().Object; - // Act - ETag etag = request.GetETag(null); + // Act + ETag etag = request.GetETag(null); - // Assert - Assert.Null(etag); - } + // Assert + Assert.Null(etag); + } - [Fact] - public void GetETag_Returns_ETagAny() - { - // Arrange & Act & Assert - HttpRequest request = new Mock().Object; + [Fact] + public void GetETag_Returns_ETagAny() + { + // Arrange & Act & Assert + HttpRequest request = new Mock().Object; - // Act - ETag etag = request.GetETag(EntityTagHeaderValue.Any); + // Act + ETag etag = request.GetETag(EntityTagHeaderValue.Any); - // Assert - Assert.NotNull(etag); - Assert.True(etag.IsAny); - } + // Assert + Assert.NotNull(etag); + Assert.True(etag.IsAny); + } - [Fact] - public void GetETagOfEntity_Returns_ETagAny() - { - // Arrange & Act & Assert - HttpRequest request = new Mock().Object; + [Fact] + public void GetETagOfEntity_Returns_ETagAny() + { + // Arrange & Act & Assert + HttpRequest request = new Mock().Object; - // Act - ETag etag = request.GetETag(EntityTagHeaderValue.Any); + // Act + ETag etag = request.GetETag(EntityTagHeaderValue.Any); - // Assert - Assert.NotNull(etag); - Assert.IsType>(etag); - Assert.True(etag.IsAny); - } + // Assert + Assert.NotNull(etag); + Assert.IsType>(etag); + Assert.True(etag.IsAny); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/ModelBoundQuerySettingsExtensionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/ModelBoundQuerySettingsExtensionsTests.cs index cefb1d056..d40aef33c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/ModelBoundQuerySettingsExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/ModelBoundQuerySettingsExtensionsTests.cs @@ -11,137 +11,136 @@ using Microsoft.OData.ModelBuilder.Config; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class ModelBoundQuerySettingsExtensionsTests { - public class ModelBoundQuerySettingsExtensionsTests + [Fact] + public void CopyOrderByConfigurations_Copies_OrderBy() + { + // Arrange + ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); + Assert.Empty(settings.OrderByConfigurations); + + // Act + settings.CopyOrderByConfigurations(new Dictionary { { "any", true } }); + + // Assert + Assert.NotEmpty(settings.OrderByConfigurations); + KeyValuePair item = Assert.Single(settings.OrderByConfigurations); + Assert.Equal("any", item.Key); + Assert.True(item.Value); + } + + [Fact] + public void CopySelectConfigurations_Copies_SelectExpand() + { + // Arrange + ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); + Assert.Empty(settings.SelectConfigurations); + + // Act + settings.CopySelectConfigurations(new Dictionary { { "any", SelectExpandType.Disabled } }); + + // Assert + Assert.NotEmpty(settings.SelectConfigurations); + KeyValuePair item = Assert.Single(settings.SelectConfigurations); + Assert.Equal("any", item.Key); + Assert.Equal(SelectExpandType.Disabled, item.Value); + } + + [Fact] + public void CopyFilterConfigurations_Copies_Filter() + { + // Arrange + ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); + Assert.Empty(settings.FilterConfigurations); + + // Act + settings.CopyFilterConfigurations(new Dictionary { { "any", false } }); + + // Assert + Assert.NotEmpty(settings.FilterConfigurations); + KeyValuePair item = Assert.Single(settings.FilterConfigurations); + Assert.Equal("any", item.Key); + Assert.False(item.Value); + } + + [Theory] + [InlineData(SelectExpandType.Disabled, false)] + [InlineData(SelectExpandType.Allowed, false)] + [InlineData(SelectExpandType.Automatic, true)] + public void IsAutomaticExpand_ReturnsCorrectly(SelectExpandType expandType, bool expected) + { + // Arrange + ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); + settings.ExpandConfigurations["navProperty"] = new ExpandConfiguration { ExpandType = expandType }; + + // Act & Assert + Assert.Equal(expected, settings.IsAutomaticExpand("navProperty")); + } + + [Theory] + [InlineData(SelectExpandType.Disabled, false)] + [InlineData(SelectExpandType.Allowed, false)] + [InlineData(SelectExpandType.Automatic, true)] + public void IsAutomaticSelect_ReturnsCorrectly(SelectExpandType expandType, bool expected) + { + // Arrange + ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); + settings.SelectConfigurations["property"] = expandType; + + // Act & Assert + Assert.Equal(expected, settings.IsAutomaticSelect("property")); + } + + [Theory] + [InlineData(SelectExpandType.Disabled, false)] + [InlineData(SelectExpandType.Allowed, true)] + [InlineData(SelectExpandType.Automatic, true)] + public void Selectable_ReturnsCorrectly_UsingDefaultConfiguration(SelectExpandType selectType, bool expected) + { + // Arrange + ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); + Assert.Empty(settings.SelectConfigurations); + settings.DefaultSelectType = selectType; + + // Act & Assert + Assert.Equal(expected, settings.Selectable("property")); + } + + [Theory] + [InlineData(SelectExpandType.Disabled, false)] + [InlineData(SelectExpandType.Allowed, true)] + [InlineData(SelectExpandType.Automatic, true)] + public void Selectable_ReturnsCorrectly(SelectExpandType selectType, bool expected) + { + // Arrange + ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); + settings.SelectConfigurations["property"] = selectType; + + // Act & Assert + Assert.Equal(expected, settings.Selectable("property")); + } + + [Fact] + public void Sortable_ReturnsCorrectly() { - [Fact] - public void CopyOrderByConfigurations_Copies_OrderBy() - { - // Arrange - ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); - Assert.Empty(settings.OrderByConfigurations); - - // Act - settings.CopyOrderByConfigurations(new Dictionary { { "any", true } }); - - // Assert - Assert.NotEmpty(settings.OrderByConfigurations); - KeyValuePair item = Assert.Single(settings.OrderByConfigurations); - Assert.Equal("any", item.Key); - Assert.True(item.Value); - } - - [Fact] - public void CopySelectConfigurations_Copies_SelectExpand() - { - // Arrange - ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); - Assert.Empty(settings.SelectConfigurations); - - // Act - settings.CopySelectConfigurations(new Dictionary { { "any", SelectExpandType.Disabled } }); - - // Assert - Assert.NotEmpty(settings.SelectConfigurations); - KeyValuePair item = Assert.Single(settings.SelectConfigurations); - Assert.Equal("any", item.Key); - Assert.Equal(SelectExpandType.Disabled, item.Value); - } - - [Fact] - public void CopyFilterConfigurations_Copies_Filter() - { - // Arrange - ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); - Assert.Empty(settings.FilterConfigurations); - - // Act - settings.CopyFilterConfigurations(new Dictionary { { "any", false } }); - - // Assert - Assert.NotEmpty(settings.FilterConfigurations); - KeyValuePair item = Assert.Single(settings.FilterConfigurations); - Assert.Equal("any", item.Key); - Assert.False(item.Value); - } - - [Theory] - [InlineData(SelectExpandType.Disabled, false)] - [InlineData(SelectExpandType.Allowed, false)] - [InlineData(SelectExpandType.Automatic, true)] - public void IsAutomaticExpand_ReturnsCorrectly(SelectExpandType expandType, bool expected) - { - // Arrange - ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); - settings.ExpandConfigurations["navProperty"] = new ExpandConfiguration { ExpandType = expandType }; - - // Act & Assert - Assert.Equal(expected, settings.IsAutomaticExpand("navProperty")); - } - - [Theory] - [InlineData(SelectExpandType.Disabled, false)] - [InlineData(SelectExpandType.Allowed, false)] - [InlineData(SelectExpandType.Automatic, true)] - public void IsAutomaticSelect_ReturnsCorrectly(SelectExpandType expandType, bool expected) - { - // Arrange - ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); - settings.SelectConfigurations["property"] = expandType; - - // Act & Assert - Assert.Equal(expected, settings.IsAutomaticSelect("property")); - } - - [Theory] - [InlineData(SelectExpandType.Disabled, false)] - [InlineData(SelectExpandType.Allowed, true)] - [InlineData(SelectExpandType.Automatic, true)] - public void Selectable_ReturnsCorrectly_UsingDefaultConfiguration(SelectExpandType selectType, bool expected) - { - // Arrange - ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); - Assert.Empty(settings.SelectConfigurations); - settings.DefaultSelectType = selectType; - - // Act & Assert - Assert.Equal(expected, settings.Selectable("property")); - } - - [Theory] - [InlineData(SelectExpandType.Disabled, false)] - [InlineData(SelectExpandType.Allowed, true)] - [InlineData(SelectExpandType.Automatic, true)] - public void Selectable_ReturnsCorrectly(SelectExpandType selectType, bool expected) - { - // Arrange - ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); - settings.SelectConfigurations["property"] = selectType; - - // Act & Assert - Assert.Equal(expected, settings.Selectable("property")); - } - - [Fact] - public void Sortable_ReturnsCorrectly() - { - // Arrange & Act & Assert - ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); - Assert.Empty(settings.OrderByConfigurations); - Assert.False(settings.Sortable("property")); - - // Arrange & Act & Assert - settings.DefaultEnableOrderBy = true; - Assert.True(settings.Sortable("property")); - - // Arrange & Act & Assert - settings.OrderByConfigurations["property"] = false; - Assert.False(settings.Sortable("property")); - - // Arrange & Act & Assert - settings.OrderByConfigurations["property"] = true; - Assert.True(settings.Sortable("property")); - } + // Arrange & Act & Assert + ModelBoundQuerySettings settings = new ModelBoundQuerySettings(); + Assert.Empty(settings.OrderByConfigurations); + Assert.False(settings.Sortable("property")); + + // Arrange & Act & Assert + settings.DefaultEnableOrderBy = true; + Assert.True(settings.Sortable("property")); + + // Arrange & Act & Assert + settings.OrderByConfigurations["property"] = false; + Assert.False(settings.Sortable("property")); + + // Arrange & Act & Assert + settings.OrderByConfigurations["property"] = true; + Assert.True(settings.Sortable("property")); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Models/QueryCompositionAddress.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Models/QueryCompositionAddress.cs index 13bb32045..4cd3142fd 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Models/QueryCompositionAddress.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Models/QueryCompositionAddress.cs @@ -7,18 +7,17 @@ using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Tests.Query.Models +namespace Microsoft.AspNetCore.OData.Tests.Query.Models; + +public class QueryCompositionAddress { - public class QueryCompositionAddress - { - public string Street { get; set; } - public string City { get; set; } - public string State { get; set; } - public string Zipcode { get; set; } + public string Street { get; set; } + public string City { get; set; } + public string State { get; set; } + public string Zipcode { get; set; } - [NotFilterable] - public string NotFilterableProperty { get; set; } - [NonFilterable] - public string NonFilterableProperty { get; set; } - } + [NotFilterable] + public string NotFilterableProperty { get; set; } + [NonFilterable] + public string NonFilterableProperty { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Models/QueryCompositionCategory.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Models/QueryCompositionCategory.cs index a529786f3..48466550a 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Models/QueryCompositionCategory.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Models/QueryCompositionCategory.cs @@ -5,13 +5,12 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.AspNetCore.OData.Tests.Query.Models +namespace Microsoft.AspNetCore.OData.Tests.Query.Models; + +public class QueryCompositionCategory { - public class QueryCompositionCategory - { - public int Id { get; set; } - public string Name { get; set; } - public QueryCompositionAddress[] Locations { get; set; } - public int[] AlternateIds { get; set; } - } + public int Id { get; set; } + public string Name { get; set; } + public QueryCompositionAddress[] Locations { get; set; } + public int[] AlternateIds { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Models/QueryCompositionCustomer.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Models/QueryCompositionCustomer.cs index 302ca9681..d1d4712c0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Models/QueryCompositionCustomer.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Models/QueryCompositionCustomer.cs @@ -10,39 +10,38 @@ using Microsoft.AspNetCore.OData.Tests.Models; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Tests.Query.Models +namespace Microsoft.AspNetCore.OData.Tests.Query.Models; + +public class QueryCompositionCustomerBase { - public class QueryCompositionCustomerBase - { - public int Id { get; set; } - } + public int Id { get; set; } +} - public class QueryCompositionCustomer : QueryCompositionCustomerBase - { - public string Name { get; set; } - public QueryCompositionAddress Address { get; set; } - public QueryCompositionCustomer RelationshipManager { get; set; } - public IEnumerable Tags { get; set; } +public class QueryCompositionCustomer : QueryCompositionCustomerBase +{ + public string Name { get; set; } + public QueryCompositionAddress Address { get; set; } + public QueryCompositionCustomer RelationshipManager { get; set; } + public IEnumerable Tags { get; set; } - public IEnumerable Contacts { get; set; } - public byte[] Image { get; set; } - public DateTimeOffset Birthday { get; set; } - public double AmountSpent { get; set; } - public Color FavoriteColor { get; set; } + public IEnumerable Contacts { get; set; } + public byte[] Image { get; set; } + public DateTimeOffset Birthday { get; set; } + public double AmountSpent { get; set; } + public Color FavoriteColor { get; set; } - public QueryCompositionAddress NavigationWithNotFilterableProperty { get; set; } - [NotFilterable] - public QueryCompositionCustomer NotFilterableNavigationProperty { get; set; } - [NotFilterable] - public string NotFilterableProperty { get; set; } - [NotSortable] - public string NotSortableProperty { get; set; } - [NonFilterable] - public QueryCompositionCustomer NonFilterableNavigationProperty { get; set; } - [NonFilterable] - public string NonFilterableProperty { get; set; } - [Unsortable] - public string UnsortableProperty { get; set; } - } + public QueryCompositionAddress NavigationWithNotFilterableProperty { get; set; } + [NotFilterable] + public QueryCompositionCustomer NotFilterableNavigationProperty { get; set; } + [NotFilterable] + public string NotFilterableProperty { get; set; } + [NotSortable] + public string NotSortableProperty { get; set; } + [NonFilterable] + public QueryCompositionCustomer NonFilterableNavigationProperty { get; set; } + [NonFilterable] + public string NonFilterableProperty { get; set; } + [Unsortable] + public string UnsortableProperty { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByClauseNodeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByClauseNodeTests.cs index 1a85b6339..8655feda8 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByClauseNodeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByClauseNodeTests.cs @@ -9,15 +9,14 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class OrderByClauseNodeTests { - public class OrderByClauseNodeTests + [Fact] + public void CtorOrderByClauseNode_ThrowsArgumentNull_OrderByClause() { - [Fact] - public void CtorOrderByClauseNode_ThrowsArgumentNull_OrderByClause() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new OrderByClauseNode(null), "orderByClause"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new OrderByClauseNode(null), "orderByClause"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByCountNodeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByCountNodeTests.cs index bf2e890c9..ee1cf757d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByCountNodeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByCountNodeTests.cs @@ -9,15 +9,14 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class OrderByCountNodeTests { - public class OrderByCountNodeTests + [Fact] + public void CtorOrderByCountNode_ThrowsArgumentNull_OrderByClause() { - [Fact] - public void CtorOrderByCountNode_ThrowsArgumentNull_OrderByClause() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new OrderByCountNode(null), "orderByClause"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new OrderByCountNode(null), "orderByClause"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByOpenPropertyNodeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByOpenPropertyNodeTests.cs index b685b661c..a584f332e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByOpenPropertyNodeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByOpenPropertyNodeTests.cs @@ -12,28 +12,27 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class OrderByOpenPropertyNodeTests { - public class OrderByOpenPropertyNodeTests + [Fact] + public void CtorOrderByOpenPropertyNodeTests_ThrowsArgumentNull_OrderByClause() { - [Fact] - public void CtorOrderByOpenPropertyNodeTests_ThrowsArgumentNull_OrderByClause() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new OrderByOpenPropertyNode(null), "orderByClause"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new OrderByOpenPropertyNode(null), "orderByClause"); + } - [Fact] - public void CtorOrderByOpenPropertyNodeTests_ThrowsODataException_NotOpenPropertyAccess() - { - // Arrange - SingleValueNode singleValue = new Mock().Object; - RangeVariable rangeVariable = new Mock().Object; - OrderByClause orderBy = new OrderByClause(null, singleValue, OrderByDirection.Descending, rangeVariable); + [Fact] + public void CtorOrderByOpenPropertyNodeTests_ThrowsODataException_NotOpenPropertyAccess() + { + // Arrange + SingleValueNode singleValue = new Mock().Object; + RangeVariable rangeVariable = new Mock().Object; + OrderByClause orderBy = new OrderByClause(null, singleValue, OrderByDirection.Descending, rangeVariable); - // Act & Assert - ExceptionAssert.Throws(() => new OrderByOpenPropertyNode(orderBy), - "OrderBy clause kind 'None' is not valid. Only kind 'SingleValueOpenPropertyAccess' is accepted."); - } + // Act & Assert + ExceptionAssert.Throws(() => new OrderByOpenPropertyNode(orderBy), + "OrderBy clause kind 'None' is not valid. Only kind 'SingleValueOpenPropertyAccess' is accepted."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByPropertyNodeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByPropertyNodeTests.cs index b1d90c76b..d0ba2324b 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByPropertyNodeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Nodes/OrderByPropertyNodeTests.cs @@ -9,15 +9,14 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class OrderByPropertyNodeTests { - public class OrderByPropertyNodeTests + [Fact] + public void CtorOrderByPropertyNode_ThrowsArgumentNull_OrderByClause() { - [Fact] - public void CtorOrderByPropertyNode_ThrowsArgumentNull_OrderByClause() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new OrderByPropertyNode(null), "orderByClause"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new OrderByPropertyNode(null), "orderByClause"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/ODataLevelTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/ODataLevelTests.cs index 8add63b80..bd1a807ca 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/ODataLevelTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/ODataLevelTests.cs @@ -24,699 +24,698 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class ODataLevelsTest : WebODataTestBase { - public class ODataLevelsTest : WebODataTestBase + public class Startup : TestStartupBase { - public class Startup : TestStartupBase + public override void ConfigureServices(IServiceCollection services) { - public override void ConfigureServices(IServiceCollection services) - { - services.ConfigureControllers(typeof(LevelsEntitiesController)); + services.ConfigureControllers(typeof(LevelsEntitiesController)); - IEdmModel model = GetEdmModel(); - services.AddControllers().AddOData(options => options.AddRouteComponents("odata", model) - .SetMaxTop(null).Expand().Select().OrderBy().Filter()); - } + IEdmModel model = GetEdmModel(); + services.AddControllers().AddOData(options => options.AddRouteComponents("odata", model) + .SetMaxTop(null).Expand().Select().OrderBy().Filter()); } + } - public ODataLevelsTest(WebODataTestFixture factory) - : base(factory) - { - } + public ODataLevelsTest(WebODataTestFixture factory) + : base(factory) + { + } - [Fact] - public async Task Levels_ExpandsNothing_EqualZero() - { - // Arrange - string uri = "odata/LevelsEntities?$expand=Parent($levels=0)"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - JToken entities = result["value"]; - Assert.Equal(10, entities.Count()); - AssertEntity(entities[0], 1); - Assert.Null(entities[0]["Parent"]); - AssertDerivedEntity(entities[1], 2); - Assert.Null(entities[1]["Parent"]); - } + [Fact] + public async Task Levels_ExpandsNothing_EqualZero() + { + // Arrange + string uri = "odata/LevelsEntities?$expand=Parent($levels=0)"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + JToken entities = result["value"]; + Assert.Equal(10, entities.Count()); + AssertEntity(entities[0], 1); + Assert.Null(entities[0]["Parent"]); + AssertDerivedEntity(entities[1], 2); + Assert.Null(entities[1]["Parent"]); + } - [Fact] - public async Task Levels_Throws_ExcceedsMaxExpandLevel() - { - // Arrange - string uri = "odata/LevelsEntities?$expand=Parent($levels=20)"; - - string expectedResponse = "{\"code\":\"400\",\"message\":" + - "\"The query specified in the URI is not valid. " + - "The request includes a $expand path which is too deep. " + - "The maximum depth allowed is 5. To increase the limit, set the 'MaxExpansionDepth' " + - "property on EnableQueryAttribute or ODataValidationSettings, or set the 'MaxDepth' " + - "property in ExpandAttribute.\"," + - "\"target\":null,\"details\":null,\"innerError\":null,\"instanceAnnotations\":[]," + - "\"typeAnnotation\":null}"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - string result = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedResponse, result); - } + [Fact] + public async Task Levels_Throws_ExcceedsMaxExpandLevel() + { + // Arrange + string uri = "odata/LevelsEntities?$expand=Parent($levels=20)"; + + string expectedResponse = "{\"code\":\"400\",\"message\":" + + "\"The query specified in the URI is not valid. " + + "The request includes a $expand path which is too deep. " + + "The maximum depth allowed is 5. To increase the limit, set the 'MaxExpansionDepth' " + + "property on EnableQueryAttribute or ODataValidationSettings, or set the 'MaxDepth' " + + "property in ExpandAttribute.\"," + + "\"target\":null,\"details\":null,\"innerError\":null,\"instanceAnnotations\":[]," + + "\"typeAnnotation\":null}"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + string result = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResponse, result); + } - [Fact] - public async Task Levels_ExpandsAllLevels_DollarLevelEqualToActualLevel() - { - // Arrange - string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=5)"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - // Level 1 - AssertEntity(result["Parent"], 5); - // Level 2 - AssertDerivedEntity(result["Parent"]["Parent"], 4); - // Level 3 - AssertEntity(result["Parent"]["Parent"]["Parent"], 3); - // Level 4 - AssertDerivedEntity(result["Parent"]["Parent"]["Parent"]["Parent"], 2); - // Level 5 - AssertEntity(result["Parent"]["Parent"]["Parent"]["Parent"]["Parent"], 1); - // No further expanding. - Assert.Null(result["Parent"]["Parent"]["Parent"]["Parent"]["Parent"]["Parent"]); - } + [Fact] + public async Task Levels_ExpandsAllLevels_DollarLevelEqualToActualLevel() + { + // Arrange + string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=5)"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + // Level 1 + AssertEntity(result["Parent"], 5); + // Level 2 + AssertDerivedEntity(result["Parent"]["Parent"], 4); + // Level 3 + AssertEntity(result["Parent"]["Parent"]["Parent"], 3); + // Level 4 + AssertDerivedEntity(result["Parent"]["Parent"]["Parent"]["Parent"], 2); + // Level 5 + AssertEntity(result["Parent"]["Parent"]["Parent"]["Parent"]["Parent"], 1); + // No further expanding. + Assert.Null(result["Parent"]["Parent"]["Parent"]["Parent"]["Parent"]["Parent"]); + } - [Fact] - public async Task Levels_ExpandsToNull_DollarLevelGreaterThanActualLevel() - { - // Arrange - string uri = "odata/LevelsEntities?$expand=Parent($levels=5)"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - JToken entities = result["value"]; - // Level 1 - AssertDerivedEntity(entities[2]["Parent"], 2); - // Level 2 - AssertDerivedEntity(entities[2]["Parent"], 2); - // Stop expanding for null. - AssertNullValue(entities[2]["Parent"]["Parent"]["Parent"]); - } + [Fact] + public async Task Levels_ExpandsToNull_DollarLevelGreaterThanActualLevel() + { + // Arrange + string uri = "odata/LevelsEntities?$expand=Parent($levels=5)"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + JToken entities = result["value"]; + // Level 1 + AssertDerivedEntity(entities[2]["Parent"], 2); + // Level 2 + AssertDerivedEntity(entities[2]["Parent"], 2); + // Stop expanding for null. + AssertNullValue(entities[2]["Parent"]["Parent"]["Parent"]); + } - [Fact] - public async Task Levels_ExpandsToLevels_DollarLevelLessThanActualLevel() - { - // Arrange - string uri = "odata/LevelsEntities(5)?$expand=Parent($levels=2)"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - // Level 1 - AssertDerivedEntity(result["Parent"], 4); - // Level 2 - AssertEntity(result["Parent"]["Parent"], 3); - // No further expanding. - Assert.Null(result["Parent"]["Parent"]["Parent"]); - } + [Fact] + public async Task Levels_ExpandsToLevels_DollarLevelLessThanActualLevel() + { + // Arrange + string uri = "odata/LevelsEntities(5)?$expand=Parent($levels=2)"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + // Level 1 + AssertDerivedEntity(result["Parent"], 4); + // Level 2 + AssertEntity(result["Parent"]["Parent"], 3); + // No further expanding. + Assert.Null(result["Parent"]["Parent"]["Parent"]); + } - [Fact] - public async Task Levels_Works_MaxDollarLevel() - { - // Arrange - string uri = "odata/LevelsEntities(5)?$expand=Parent($levels=max)"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - // Level 1 - AssertDerivedEntity(result["Parent"], 4); - // Level 2 - AssertEntity(result["Parent"]["Parent"], 3); - // Level 3 - AssertDerivedEntity(result["Parent"]["Parent"]["Parent"], 2); - // Level 4 - AssertEntity(result["Parent"]["Parent"]["Parent"]["Parent"], 1); - // Stop expanding for null. - AssertNullValue(result["Parent"]["Parent"]["Parent"]["Parent"]["Parent"]); - } + [Fact] + public async Task Levels_Works_MaxDollarLevel() + { + // Arrange + string uri = "odata/LevelsEntities(5)?$expand=Parent($levels=max)"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + // Level 1 + AssertDerivedEntity(result["Parent"], 4); + // Level 2 + AssertEntity(result["Parent"]["Parent"], 3); + // Level 3 + AssertDerivedEntity(result["Parent"]["Parent"]["Parent"], 2); + // Level 4 + AssertEntity(result["Parent"]["Parent"]["Parent"]["Parent"], 1); + // Stop expanding for null. + AssertNullValue(result["Parent"]["Parent"]["Parent"]["Parent"]["Parent"]); + } - [Fact] - public async Task Levels_ExpandsToLevels_LoopExists() - { - // Arrange - string uri = "odata/LevelsEntities(9)?$expand=Parent($expand=Parent($levels=2))"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - // Level 1 - AssertDerivedEntity(result["Parent"], 10); - // Level 2 - AssertEntity(result["Parent"]["Parent"], 9); - // Level 3 - AssertDerivedEntity(result["Parent"]["Parent"]["Parent"], 10); - // No further expanding. - Assert.Null(result["Parent"]["Parent"]["Parent"]["Parent"]); - } + [Fact] + public async Task Levels_ExpandsToLevels_LoopExists() + { + // Arrange + string uri = "odata/LevelsEntities(9)?$expand=Parent($expand=Parent($levels=2))"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + // Level 1 + AssertDerivedEntity(result["Parent"], 10); + // Level 2 + AssertEntity(result["Parent"]["Parent"], 9); + // Level 3 + AssertDerivedEntity(result["Parent"]["Parent"]["Parent"], 10); + // No further expanding. + Assert.Null(result["Parent"]["Parent"]["Parent"]["Parent"]); + } - [Fact] - public async Task Levels_ExpandsToMaxExpandLevel_LoopExists() - { - // Arrange - string uri = "odata/LevelsEntities(9)?$expand=Parent($levels=max)"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - // Level 1 - AssertDerivedEntity(result["Parent"], 10); - // Level 2 - AssertEntity(result["Parent"]["Parent"], 9); - // Level 3 - AssertDerivedEntity(result["Parent"]["Parent"]["Parent"], 10); - // Level 4 - AssertEntity(result["Parent"]["Parent"]["Parent"]["Parent"], 9); - // Level 5 - AssertDerivedEntity(result["Parent"]["Parent"]["Parent"]["Parent"]["Parent"], 10); - // No further expanding. - Assert.Null(result["Parent"]["Parent"]["Parent"]["Parent"]["Parent"]["Parent"]); - } + [Fact] + public async Task Levels_ExpandsToMaxExpandLevel_LoopExists() + { + // Arrange + string uri = "odata/LevelsEntities(9)?$expand=Parent($levels=max)"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + // Level 1 + AssertDerivedEntity(result["Parent"], 10); + // Level 2 + AssertEntity(result["Parent"]["Parent"], 9); + // Level 3 + AssertDerivedEntity(result["Parent"]["Parent"]["Parent"], 10); + // Level 4 + AssertEntity(result["Parent"]["Parent"]["Parent"]["Parent"], 9); + // Level 5 + AssertDerivedEntity(result["Parent"]["Parent"]["Parent"]["Parent"]["Parent"], 10); + // No further expanding. + Assert.Null(result["Parent"]["Parent"]["Parent"]["Parent"]["Parent"]["Parent"]); + } - [Fact] - public async Task Levels_Works_ExpandsBaseTypeProperty() - { - // Arrange - string uri = "odata/LevelsEntities(2)?$expand=BaseEntities($levels=3)"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - JToken baseEntities = result["BaseEntities"]; - Assert.Equal(2, baseEntities.Count()); - // Level 1 - AssertEntity(baseEntities[0], 1); - Assert.Single(baseEntities[0]["BaseEntities"]); - // Level 2 - AssertEntity(baseEntities[0]["BaseEntities"][0], 11); - // No further expanding - Assert.Null(baseEntities[0]["BaseEntities"][0]["BaseEntities"]); - // Level 1 - AssertEntity(baseEntities[1], 12); - // No further expanding - Assert.Null(baseEntities[1]["BaseEntities"]); - } + [Fact] + public async Task Levels_Works_ExpandsBaseTypeProperty() + { + // Arrange + string uri = "odata/LevelsEntities(2)?$expand=BaseEntities($levels=3)"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + JToken baseEntities = result["BaseEntities"]; + Assert.Equal(2, baseEntities.Count()); + // Level 1 + AssertEntity(baseEntities[0], 1); + Assert.Single(baseEntities[0]["BaseEntities"]); + // Level 2 + AssertEntity(baseEntities[0]["BaseEntities"][0], 11); + // No further expanding + Assert.Null(baseEntities[0]["BaseEntities"][0]["BaseEntities"]); + // Level 1 + AssertEntity(baseEntities[1], 12); + // No further expanding + Assert.Null(baseEntities[1]["BaseEntities"]); + } - [Fact] - public async Task Levels_Works_ExpandsDerivedTypeProperty() - { - // Arrange - string uri = "odata/LevelsEntities(5)?$expand=DerivedAncestors($levels=3)"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - JToken derivedEntities = result["DerivedAncestors"]; - Assert.Equal(2, derivedEntities.Count()); - // Level 1 - AssertDerivedEntity(derivedEntities[0], 2); - // Level 2 - AssertDerivedEntity(derivedEntities[0]["DerivedAncestors"][0]["DerivedAncestors"][0], 2); - // Level 1 - AssertDerivedEntity(derivedEntities[1], 4); - // Level 2 - Assert.Single(derivedEntities[1]["DerivedAncestors"]); - AssertDerivedEntity(derivedEntities[1]["DerivedAncestors"][0], 2); - // Level 3 - AssertDerivedEntity(derivedEntities[1]["DerivedAncestors"][0]["DerivedAncestors"][0], 4); - } + [Fact] + public async Task Levels_Works_ExpandsDerivedTypeProperty() + { + // Arrange + string uri = "odata/LevelsEntities(5)?$expand=DerivedAncestors($levels=3)"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + JToken derivedEntities = result["DerivedAncestors"]; + Assert.Equal(2, derivedEntities.Count()); + // Level 1 + AssertDerivedEntity(derivedEntities[0], 2); + // Level 2 + AssertDerivedEntity(derivedEntities[0]["DerivedAncestors"][0]["DerivedAncestors"][0], 2); + // Level 1 + AssertDerivedEntity(derivedEntities[1], 4); + // Level 2 + Assert.Single(derivedEntities[1]["DerivedAncestors"]); + AssertDerivedEntity(derivedEntities[1]["DerivedAncestors"][0], 2); + // Level 3 + AssertDerivedEntity(derivedEntities[1]["DerivedAncestors"][0]["DerivedAncestors"][0], 4); + } - [Fact] - public async Task Levels_Works_WithTypeCast() - { - // Arrange - string uri = "odata/LevelsEntities(6)?$expand=Microsoft.AspNetCore.OData.Tests.Query.LevelsDerivedEntity/AncestorsInDerivedEntity($levels=2)"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - JToken derivedEntities = result["AncestorsInDerivedEntity"]; - Assert.Equal(5, derivedEntities.Count()); - // Level 1 - AssertEntity(derivedEntities[0], 1); - // Stop expanding when casting fails. - Assert.Null(derivedEntities[0]["AncestorsInDerivedEntity"]); - // Level 1 - AssertDerivedEntity(derivedEntities[1], 2); - // Level 2 - Assert.Single(derivedEntities[1]["AncestorsInDerivedEntity"]); - AssertEntity(derivedEntities[1]["AncestorsInDerivedEntity"][0], 1); - // No further expanding. - Assert.Null(derivedEntities[1]["AncestorsInDerivedEntity"][0]["AncestorsInDerivedEntity"]); - // Level 1 - AssertEntity(derivedEntities[2], 3); - // Level 1 - AssertDerivedEntity(derivedEntities[3], 4); - // Level 2 - Assert.Equal(3, derivedEntities[3]["AncestorsInDerivedEntity"].Count()); - AssertEntity(derivedEntities[3]["AncestorsInDerivedEntity"][0], 1); - // Stop expanding when casting fails. - Assert.Null(derivedEntities[3]["AncestorsInDerivedEntity"][0]["AncestorsInDerivedEntity"]); - AssertDerivedEntity(derivedEntities[3]["AncestorsInDerivedEntity"][1], 2); - // No further expanding. - Assert.Null(derivedEntities[3]["AncestorsInDerivedEntity"][1]["AncestorsInDerivedEntity"]); - AssertEntity(derivedEntities[3]["AncestorsInDerivedEntity"][2], 3); - // Stop expanding when casting fails. - Assert.Null(derivedEntities[3]["AncestorsInDerivedEntity"][2]["AncestorsInDerivedEntity"]); - // Level 1 - AssertEntity(derivedEntities[4], 5); - // Stop expanding when casting fails. - Assert.Null(derivedEntities[4]["AncestorsInDerivedEntity"]); - } + [Fact] + public async Task Levels_Works_WithTypeCast() + { + // Arrange + string uri = "odata/LevelsEntities(6)?$expand=Microsoft.AspNetCore.OData.Tests.Query.LevelsDerivedEntity/AncestorsInDerivedEntity($levels=2)"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + JToken derivedEntities = result["AncestorsInDerivedEntity"]; + Assert.Equal(5, derivedEntities.Count()); + // Level 1 + AssertEntity(derivedEntities[0], 1); + // Stop expanding when casting fails. + Assert.Null(derivedEntities[0]["AncestorsInDerivedEntity"]); + // Level 1 + AssertDerivedEntity(derivedEntities[1], 2); + // Level 2 + Assert.Single(derivedEntities[1]["AncestorsInDerivedEntity"]); + AssertEntity(derivedEntities[1]["AncestorsInDerivedEntity"][0], 1); + // No further expanding. + Assert.Null(derivedEntities[1]["AncestorsInDerivedEntity"][0]["AncestorsInDerivedEntity"]); + // Level 1 + AssertEntity(derivedEntities[2], 3); + // Level 1 + AssertDerivedEntity(derivedEntities[3], 4); + // Level 2 + Assert.Equal(3, derivedEntities[3]["AncestorsInDerivedEntity"].Count()); + AssertEntity(derivedEntities[3]["AncestorsInDerivedEntity"][0], 1); + // Stop expanding when casting fails. + Assert.Null(derivedEntities[3]["AncestorsInDerivedEntity"][0]["AncestorsInDerivedEntity"]); + AssertDerivedEntity(derivedEntities[3]["AncestorsInDerivedEntity"][1], 2); + // No further expanding. + Assert.Null(derivedEntities[3]["AncestorsInDerivedEntity"][1]["AncestorsInDerivedEntity"]); + AssertEntity(derivedEntities[3]["AncestorsInDerivedEntity"][2], 3); + // Stop expanding when casting fails. + Assert.Null(derivedEntities[3]["AncestorsInDerivedEntity"][2]["AncestorsInDerivedEntity"]); + // Level 1 + AssertEntity(derivedEntities[4], 5); + // Stop expanding when casting fails. + Assert.Null(derivedEntities[4]["AncestorsInDerivedEntity"]); + } - [Fact] - public async Task Levels_AppliesSameSelectForEachLevel() - { - // Arrange - string uri = "odata/LevelsEntities(5)?$select=ID&$expand=Parent($levels=2;$select=Name)"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - Assert.Equal(5, result["ID"]); - Assert.Null(result["Name"]); - // Level 1 - Assert.Null(result["Parent"]["ID"]); - Assert.Equal("Name 4", result["Parent"]["Name"]); - Assert.Null(result["Parent"]["DerivedName"]); - // Level 2 - Assert.Null(result["Parent"]["Parent"]["ID"]); - Assert.Equal("Name 3", result["Parent"]["Parent"]["Name"]); - // No further expanding. - Assert.Null(result["Parent"]["Parent"]["Parent"]); - } + [Fact] + public async Task Levels_AppliesSameSelectForEachLevel() + { + // Arrange + string uri = "odata/LevelsEntities(5)?$select=ID&$expand=Parent($levels=2;$select=Name)"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + Assert.Equal(5, result["ID"]); + Assert.Null(result["Name"]); + // Level 1 + Assert.Null(result["Parent"]["ID"]); + Assert.Equal("Name 4", result["Parent"]["Name"]); + Assert.Null(result["Parent"]["DerivedName"]); + // Level 2 + Assert.Null(result["Parent"]["Parent"]["ID"]); + Assert.Equal("Name 3", result["Parent"]["Parent"]["Name"]); + // No further expanding. + Assert.Null(result["Parent"]["Parent"]["Parent"]); + } - [Fact] - public async Task Levels_Works_WithNestedLevels() - { - // Arrange - string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=2;$expand=DerivedAncestors($levels=2))"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - JToken parent = result["Parent"]; - // Level 1 - AssertEntity(parent, 5); - // Level 2 - AssertDerivedEntity(parent["Parent"], 4); - // No further expanding. - Assert.Null(parent["Parent"]["Parent"]); - // Level 1 - Assert.Equal(2, parent["DerivedAncestors"].Count()); - AssertDerivedEntity(parent["DerivedAncestors"][0], 2); - // Level 2 - AssertDerivedEntity(parent["DerivedAncestors"][0]["DerivedAncestors"][0], 4); - // Level 1 - AssertDerivedEntity(parent["DerivedAncestors"][1], 4); - // Level 2 - AssertDerivedEntity(parent["DerivedAncestors"][1]["DerivedAncestors"][0], 2); - // No further expanding. - Assert.Null(parent["DerivedAncestors"][1]["DerivedAncestors"][0]["DerivedAncestors"]); - // Level 1 - Assert.Single(parent["Parent"]["DerivedAncestors"]); - AssertDerivedEntity(parent["Parent"]["DerivedAncestors"][0], 2); - // Level 2 - Assert.Single(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"]); - } + [Fact] + public async Task Levels_Works_WithNestedLevels() + { + // Arrange + string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=2;$expand=DerivedAncestors($levels=2))"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + JToken parent = result["Parent"]; + // Level 1 + AssertEntity(parent, 5); + // Level 2 + AssertDerivedEntity(parent["Parent"], 4); + // No further expanding. + Assert.Null(parent["Parent"]["Parent"]); + // Level 1 + Assert.Equal(2, parent["DerivedAncestors"].Count()); + AssertDerivedEntity(parent["DerivedAncestors"][0], 2); + // Level 2 + AssertDerivedEntity(parent["DerivedAncestors"][0]["DerivedAncestors"][0], 4); + // Level 1 + AssertDerivedEntity(parent["DerivedAncestors"][1], 4); + // Level 2 + AssertDerivedEntity(parent["DerivedAncestors"][1]["DerivedAncestors"][0], 2); + // No further expanding. + Assert.Null(parent["DerivedAncestors"][1]["DerivedAncestors"][0]["DerivedAncestors"]); + // Level 1 + Assert.Single(parent["Parent"]["DerivedAncestors"]); + AssertDerivedEntity(parent["Parent"]["DerivedAncestors"][0], 2); + // Level 2 + Assert.Single(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"]); + } - [Fact] - public async Task Levels_Works_WithMaxLevelInNestedExpand() - { - // Arrange - string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=3;$expand=DerivedAncestors($levels=max))"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - JToken parent = result["Parent"]; - - // Level 3 - AssertEntity(parent["Parent"]["Parent"], 3); - // No furthur expanding for "Parent" - Assert.Null(parent["Parent"]["Parent"]["Parent"]); - // Level 5 - AssertDerivedEntity(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0], 4); - // Level 5 - AssertDerivedEntity(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0], 2); - // Level 5 - AssertDerivedEntity(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0], 4); - // No further expanding. - Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); - } + [Fact] + public async Task Levels_Works_WithMaxLevelInNestedExpand() + { + // Arrange + string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=3;$expand=DerivedAncestors($levels=max))"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + JToken parent = result["Parent"]; + + // Level 3 + AssertEntity(parent["Parent"]["Parent"], 3); + // No furthur expanding for "Parent" + Assert.Null(parent["Parent"]["Parent"]["Parent"]); + // Level 5 + AssertDerivedEntity(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0], 4); + // Level 5 + AssertDerivedEntity(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0], 2); + // Level 5 + AssertDerivedEntity(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0], 4); + // No further expanding. + Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); + } - [Fact] - public async Task Levels_Works_WithMaxLevelInEveryExpand() - { - // Arrange - string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=max;$expand=DerivedAncestors($levels=max))"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - JToken parent = result["Parent"]; - - // Level 5 - AssertEntity(parent["Parent"]["Parent"]["Parent"]["Parent"], 1); - // No further expanding on level5 Parent - Assert.Null(parent["Parent"]["Parent"]["Parent"]["Parent"]["Parent"]); - Assert.Null(parent["Parent"]["Parent"]["Parent"]["Parent"]["DerivedAncestors"]); - // Level 5 - AssertDerivedEntity(parent["Parent"]["Parent"]["Parent"]["DerivedAncestors"][0], 4); - // Level 5 - AssertDerivedEntity(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0], 4); - // Level 5 - AssertDerivedEntity(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0], 2); - // Level 5 - AssertDerivedEntity(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0], 4); - // No further expanding. - Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); - } + [Fact] + public async Task Levels_Works_WithMaxLevelInEveryExpand() + { + // Arrange + string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=max;$expand=DerivedAncestors($levels=max))"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + JToken parent = result["Parent"]; + + // Level 5 + AssertEntity(parent["Parent"]["Parent"]["Parent"]["Parent"], 1); + // No further expanding on level5 Parent + Assert.Null(parent["Parent"]["Parent"]["Parent"]["Parent"]["Parent"]); + Assert.Null(parent["Parent"]["Parent"]["Parent"]["Parent"]["DerivedAncestors"]); + // Level 5 + AssertDerivedEntity(parent["Parent"]["Parent"]["Parent"]["DerivedAncestors"][0], 4); + // Level 5 + AssertDerivedEntity(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0], 4); + // Level 5 + AssertDerivedEntity(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0], 2); + // Level 5 + AssertDerivedEntity(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0], 4); + // No further expanding. + Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); + } - [Fact] - public async Task Levels_Works_SelectWithNestedMaxLevels() - { - // Arrange - string uri = "odata/LevelsEntities(6)?$expand=Parent($select=ID;$levels=3;$expand=DerivedAncestors($levels=max;$select=DerivedName))"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - JToken parent = result["Parent"]; - - // Level 3 - Assert.Equal(3, parent["Parent"]["Parent"]["ID"]); - Assert.Null(parent["Parent"]["Parent"]["Name"]); - // No furthur expanding for "Parent" - Assert.Null(parent["Parent"]["Parent"]["Parent"]); - // Level 5 - Assert.Equal("DerivedName 4", parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedName"]); - Assert.Null(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["ID"]); - // Level 5 - Assert.Equal("DerivedName 2", parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedName"]); - Assert.Null(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["ID"]); - // Level 5 - Assert.Equal("DerivedName 4", parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedName"]); - Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["Name"]); - // No further expanding. - Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); - } + [Fact] + public async Task Levels_Works_SelectWithNestedMaxLevels() + { + // Arrange + string uri = "odata/LevelsEntities(6)?$expand=Parent($select=ID;$levels=3;$expand=DerivedAncestors($levels=max;$select=DerivedName))"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + JToken parent = result["Parent"]; + + // Level 3 + Assert.Equal(3, parent["Parent"]["Parent"]["ID"]); + Assert.Null(parent["Parent"]["Parent"]["Name"]); + // No furthur expanding for "Parent" + Assert.Null(parent["Parent"]["Parent"]["Parent"]); + // Level 5 + Assert.Equal("DerivedName 4", parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedName"]); + Assert.Null(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["ID"]); + // Level 5 + Assert.Equal("DerivedName 2", parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedName"]); + Assert.Null(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["ID"]); + // Level 5 + Assert.Equal("DerivedName 4", parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedName"]); + Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["Name"]); + // No further expanding. + Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); + } - [Fact] - public async Task Levels_Works_WithMaxOptionInOuterExpand() - { - // Arrange - string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=max;$expand=DerivedAncestors($levels=2))"; + [Fact] + public async Task Levels_Works_WithMaxOptionInOuterExpand() + { + // Arrange + string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=max;$expand=DerivedAncestors($levels=2))"; - // Act - HttpResponseMessage response = await Client.GetAsync(uri); + // Act + HttpResponseMessage response = await Client.GetAsync(uri); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - JToken parent = result["Parent"]; + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + JToken parent = result["Parent"]; - Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); - Assert.Null(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); - Assert.Null(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); - } + Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); + Assert.Null(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); + Assert.Null(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); + } - [Fact] - public async Task Levels_Works_WithMultiParallelExpand() - { - // Arrange - string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=max;$expand=DerivedAncestors($levels=2),BaseEntities($levels=3))"; + [Fact] + public async Task Levels_Works_WithMultiParallelExpand() + { + // Arrange + string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=max;$expand=DerivedAncestors($levels=2),BaseEntities($levels=3))"; - // Act - HttpResponseMessage response = await Client.GetAsync(uri); + // Act + HttpResponseMessage response = await Client.GetAsync(uri); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - JToken parent = result["Parent"]; + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + JToken parent = result["Parent"]; - // "Parent" => 2, "BaseEntities" => 3, "DerivedAncestors" => 2 - Assert.Null(parent["Parent"]["BaseEntities"][1]["BaseEntities"][0]["BaseEntities"][0]["BaseEntities"]); - Assert.Null(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); + // "Parent" => 2, "BaseEntities" => 3, "DerivedAncestors" => 2 + Assert.Null(parent["Parent"]["BaseEntities"][1]["BaseEntities"][0]["BaseEntities"][0]["BaseEntities"]); + Assert.Null(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); - // "Parent" => 1, "BaseEntities" => 3, "DerivedAncestors" => 2 - Assert.Null(parent["BaseEntities"][2]["BaseEntities"][1]["BaseEntities"][0]["BaseEntities"]); - Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); - } + // "Parent" => 1, "BaseEntities" => 3, "DerivedAncestors" => 2 + Assert.Null(parent["BaseEntities"][2]["BaseEntities"][1]["BaseEntities"][0]["BaseEntities"]); + Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); + } - [Fact] - public async Task Levels_Works_WithMaxLevelInMultiParallelExpand() - { - // Arrange - string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=max;$expand=DerivedAncestors($levels=2),BaseEntities($levels=max))"; + [Fact] + public async Task Levels_Works_WithMaxLevelInMultiParallelExpand() + { + // Arrange + string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=max;$expand=DerivedAncestors($levels=2),BaseEntities($levels=max))"; - // Act - HttpResponseMessage response = await Client.GetAsync(uri); + // Act + HttpResponseMessage response = await Client.GetAsync(uri); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - JToken parent = result["Parent"]; + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + JToken parent = result["Parent"]; - // "Parent" => 3, "BaseEntities" => 2, "DerivedAncestors" => 2 - Assert.Null(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); - Assert.Null(parent["Parent"]["Parent"]["BaseEntities"][0]["BaseEntities"][0]["BaseEntities"]); + // "Parent" => 3, "BaseEntities" => 2, "DerivedAncestors" => 2 + Assert.Null(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); + Assert.Null(parent["Parent"]["Parent"]["BaseEntities"][0]["BaseEntities"][0]["BaseEntities"]); - // "Parent" => 2, "BaseEntities" => 3, "DerivedAncestors" => 2 - Assert.Null(parent["Parent"]["BaseEntities"][1]["BaseEntities"][0]["BaseEntities"][0]["BaseEntities"]); - Assert.Null(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); + // "Parent" => 2, "BaseEntities" => 3, "DerivedAncestors" => 2 + Assert.Null(parent["Parent"]["BaseEntities"][1]["BaseEntities"][0]["BaseEntities"][0]["BaseEntities"]); + Assert.Null(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); - // "Parent" => 1, "BaseEntities" => 4, "DerivedAncestors" => 2 - Assert.Null(parent["BaseEntities"][2]["BaseEntities"][1]["BaseEntities"][0]["BaseEntities"][0]["BaseEntities"]); - Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); - } + // "Parent" => 1, "BaseEntities" => 4, "DerivedAncestors" => 2 + Assert.Null(parent["BaseEntities"][2]["BaseEntities"][1]["BaseEntities"][0]["BaseEntities"][0]["BaseEntities"]); + Assert.Null(parent["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); + } - [Fact] - public async Task Levels_Works_WithMultiLevelsExpand() - { - // Arrange - string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=max;$expand=DerivedAncestors($levels=2;$expand=BaseEntities($levels=max)))"; - - // Act - HttpResponseMessage response = await Client.GetAsync(uri); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - JObject result = await response.Content.ReadAsObject(); - JToken parent = result["Parent"]; - - // "Parent" => 3, "DerivedAncestors" => 2, max("BaseEntities") => 1 - Assert.Null(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); - Assert.Null(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["BaseEntities"]); - Assert.NotNull(parent["Parent"]["Parent"]["DerivedAncestors"][0]["BaseEntities"]); - Assert.Null(parent["Parent"]["Parent"]["DerivedAncestors"][0]["BaseEntities"][0]["BaseEntities"]); - - // "Parent" => 2, "DerivedAncestors" => 2, max("BaseEntities") => 2 - Assert.Null(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); - Assert.NotNull(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["BaseEntities"]); - Assert.Null(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["BaseEntities"][0]["BaseEntities"]); - Assert.NotNull(parent["Parent"]["DerivedAncestors"][0]["BaseEntities"]); - Assert.NotNull(parent["Parent"]["DerivedAncestors"][0]["BaseEntities"][0]["BaseEntities"]); - Assert.Null(parent["Parent"]["DerivedAncestors"][0]["BaseEntities"][0]["BaseEntities"][0]["BaseEntities"]); - - // "Parent" => 1, "DerivedAncestors" => 2, max("BaseEntities") => 3 - Assert.Null(parent["DerivedAncestors"][1]["BaseEntities"][2]["BaseEntities"][1]["BaseEntities"][0]["BaseEntities"]); - } + [Fact] + public async Task Levels_Works_WithMultiLevelsExpand() + { + // Arrange + string uri = "odata/LevelsEntities(6)?$expand=Parent($levels=max;$expand=DerivedAncestors($levels=2;$expand=BaseEntities($levels=max)))"; + + // Act + HttpResponseMessage response = await Client.GetAsync(uri); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + JObject result = await response.Content.ReadAsObject(); + JToken parent = result["Parent"]; + + // "Parent" => 3, "DerivedAncestors" => 2, max("BaseEntities") => 1 + Assert.Null(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); + Assert.Null(parent["Parent"]["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["BaseEntities"]); + Assert.NotNull(parent["Parent"]["Parent"]["DerivedAncestors"][0]["BaseEntities"]); + Assert.Null(parent["Parent"]["Parent"]["DerivedAncestors"][0]["BaseEntities"][0]["BaseEntities"]); + + // "Parent" => 2, "DerivedAncestors" => 2, max("BaseEntities") => 2 + Assert.Null(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["DerivedAncestors"]); + Assert.NotNull(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["BaseEntities"]); + Assert.Null(parent["Parent"]["DerivedAncestors"][0]["DerivedAncestors"][0]["BaseEntities"][0]["BaseEntities"]); + Assert.NotNull(parent["Parent"]["DerivedAncestors"][0]["BaseEntities"]); + Assert.NotNull(parent["Parent"]["DerivedAncestors"][0]["BaseEntities"][0]["BaseEntities"]); + Assert.Null(parent["Parent"]["DerivedAncestors"][0]["BaseEntities"][0]["BaseEntities"][0]["BaseEntities"]); + + // "Parent" => 1, "DerivedAncestors" => 2, max("BaseEntities") => 3 + Assert.Null(parent["DerivedAncestors"][1]["BaseEntities"][2]["BaseEntities"][1]["BaseEntities"][0]["BaseEntities"]); + } - private void AssertEntity(JToken entity, int key) - { - Assert.Equal(key, entity["ID"]); - Assert.Equal("Name " + key, entity["Name"]); - } + private void AssertEntity(JToken entity, int key) + { + Assert.Equal(key, entity["ID"]); + Assert.Equal("Name " + key, entity["Name"]); + } - private void AssertDerivedEntity(JToken entity, int key) - { - AssertEntity(entity, key); - Assert.Equal("DerivedName " + key, entity["DerivedName"]); - } + private void AssertDerivedEntity(JToken entity, int key) + { + AssertEntity(entity, key); + Assert.Equal("DerivedName " + key, entity["DerivedName"]); + } - private void AssertNullValue(JToken token) - { - JValue value = Assert.IsType(token); - Assert.Null(value.Value); - } + private void AssertNullValue(JToken token) + { + JValue value = Assert.IsType(token); + Assert.Null(value.Value); + } - public static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("LevelsBaseEntities"); - builder.EntitySet("LevelsEntities"); - builder.EntitySet("LevelsDerivedEntities"); - return builder.GetEdmModel(); - } + public static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("LevelsBaseEntities"); + builder.EntitySet("LevelsEntities"); + builder.EntitySet("LevelsDerivedEntities"); + return builder.GetEdmModel(); + } - public class LevelsEntitiesController : ODataController - { - public IList Entities; + public class LevelsEntitiesController : ODataController + { + public IList Entities; - public LevelsEntitiesController() + public LevelsEntitiesController() + { + Entities = new List(); + for (int i = 1; i <= 10; i++) { - Entities = new List(); - for (int i = 1; i <= 10; i++) + if (i % 2 == 1) { - if (i % 2 == 1) + var newEntity = new LevelsEntity { - var newEntity = new LevelsEntity - { - ID = i, - Name = "Name " + i, - Parent = Entities.LastOrDefault(), - BaseEntities = Entities.Concat(new[] + ID = i, + Name = "Name " + i, + Parent = Entities.LastOrDefault(), + BaseEntities = Entities.Concat(new[] + { + new LevelsBaseEntity { - new LevelsBaseEntity - { - ID = i + 10, - Name = "Name " + (i + 10) - } - }).ToArray(), - DerivedAncestors = Entities.OfType().ToArray() - }; - Entities.Add(newEntity); - } - else + ID = i + 10, + Name = "Name " + (i + 10) + } + }).ToArray(), + DerivedAncestors = Entities.OfType().ToArray() + }; + Entities.Add(newEntity); + } + else + { + var newEntity = new LevelsDerivedEntity { - var newEntity = new LevelsDerivedEntity - { - ID = i, - Name = "Name " + i, - DerivedName = "DerivedName " + i, - Parent = Entities.LastOrDefault(), - BaseEntities = Entities.Concat(new[] + ID = i, + Name = "Name " + i, + DerivedName = "DerivedName " + i, + Parent = Entities.LastOrDefault(), + BaseEntities = Entities.Concat(new[] + { + new LevelsBaseEntity { - new LevelsBaseEntity - { - ID = i + 10, - Name = "Name " + (i + 10) - } - }).ToArray(), - DerivedAncestors = Entities.OfType().ToArray(), - AncestorsInDerivedEntity = Entities.ToArray() - }; - Entities.Add(newEntity); - } + ID = i + 10, + Name = "Name " + (i + 10) + } + }).ToArray(), + DerivedAncestors = Entities.OfType().ToArray(), + AncestorsInDerivedEntity = Entities.ToArray() + }; + Entities.Add(newEntity); } - Entities[8].Parent = Entities[9]; - Entities[1].DerivedAncestors = new LevelsDerivedEntity[] { (LevelsDerivedEntity)Entities[3] }; } + Entities[8].Parent = Entities[9]; + Entities[1].DerivedAncestors = new LevelsDerivedEntity[] { (LevelsDerivedEntity)Entities[3] }; + } - public IActionResult Get(ODataQueryOptions queryOptions) - { - var validationSettings = new ODataValidationSettings { MaxExpansionDepth = 5 }; - - try - { - queryOptions.Validate(validationSettings); - } - catch (ODataException e) - { - //var responseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest); - //responseMessage.Content = new StringContent( - // Error.Format("The query specified in the URI is not valid. {0}", e.Message)); - Request.ODataFeature().Path = null; - return BadRequest(Error.Format("The query specified in the URI is not valid. {0}", e.Message)); - } - - var querySettings = new ODataQuerySettings(); - var result = queryOptions.ApplyTo(Entities.AsQueryable(), querySettings).AsQueryable(); + public IActionResult Get(ODataQueryOptions queryOptions) + { + var validationSettings = new ODataValidationSettings { MaxExpansionDepth = 5 }; - return Ok(result); - //return Ok(result, result.GetType()); + try + { + queryOptions.Validate(validationSettings); } - - [EnableQuery(MaxExpansionDepth = 5)] - public IActionResult Get(int key) + catch (ODataException e) { - return Ok(Entities.Single(e => e.ID == key)); + //var responseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest); + //responseMessage.Content = new StringContent( + // Error.Format("The query specified in the URI is not valid. {0}", e.Message)); + Request.ODataFeature().Path = null; + return BadRequest(Error.Format("The query specified in the URI is not valid. {0}", e.Message)); } - //private IActionResult Ok(object content, Type type) - //{ - // var resultType = typeof(OkNegotiatedContentResult<>).MakeGenericType(type); - // return Activator.CreateInstance(resultType, content, this) as IActionResult; - //} - } + var querySettings = new ODataQuerySettings(); + var result = queryOptions.ApplyTo(Entities.AsQueryable(), querySettings).AsQueryable(); - public class LevelsBaseEntity - { - public int ID { get; set; } - public string Name { get; set; } + return Ok(result); + //return Ok(result, result.GetType()); } - public class LevelsEntity : LevelsBaseEntity + [EnableQuery(MaxExpansionDepth = 5)] + public IActionResult Get(int key) { - public LevelsEntity Parent { get; set; } - public LevelsBaseEntity[] BaseEntities { get; set; } - public LevelsDerivedEntity[] DerivedAncestors { get; set; } + return Ok(Entities.Single(e => e.ID == key)); } - public class LevelsDerivedEntity : LevelsEntity - { - public string DerivedName { get; set; } - public LevelsEntity[] AncestorsInDerivedEntity { get; set; } - } + //private IActionResult Ok(object content, Type type) + //{ + // var resultType = typeof(OkNegotiatedContentResult<>).MakeGenericType(type); + // return Activator.CreateInstance(resultType, content, this) as IActionResult; + //} + } + + public class LevelsBaseEntity + { + public int ID { get; set; } + public string Name { get; set; } + } + + public class LevelsEntity : LevelsBaseEntity + { + public LevelsEntity Parent { get; set; } + public LevelsBaseEntity[] BaseEntities { get; set; } + public LevelsDerivedEntity[] DerivedAncestors { get; set; } + } + + public class LevelsDerivedEntity : LevelsEntity + { + public string DerivedName { get; set; } + public LevelsEntity[] AncestorsInDerivedEntity { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/ODataQueryContextTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/ODataQueryContextTests.cs index 1efe0944c..f4cbecea5 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/ODataQueryContextTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/ODataQueryContextTests.cs @@ -17,225 +17,224 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class ODataQueryContextTests { - public class ODataQueryContextTests + [Fact] + public void CtorODataQueryContext_TakingClrType_Throws_With_Null_Model() { - [Fact] - public void CtorODataQueryContext_TakingClrType_Throws_With_Null_Model() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new ODataQueryContext(model: null, elementClrType: typeof(int)), - "model"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new ODataQueryContext(model: null, elementClrType: typeof(int)), + "model"); + } - [Fact] - public void CtorODataQueryContext_TakingClrType_Throws_With_Null_Type() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new ODataQueryContext(EdmCoreModel.Instance, elementClrType: null), - "elementClrType"); - } + [Fact] + public void CtorODataQueryContext_TakingClrType_Throws_With_Null_Type() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new ODataQueryContext(EdmCoreModel.Instance, elementClrType: null), + "elementClrType"); + } - [Fact] - public void CtorODataQueryContext_TakingClrType_SetsProperties() - { - // Arrange - ODataModelBuilder builder = new ODataModelBuilder(); - builder.EntityType(); - IEdmModel model = builder.GetEdmModel(); + [Fact] + public void CtorODataQueryContext_TakingClrType_SetsProperties() + { + // Arrange + ODataModelBuilder builder = new ODataModelBuilder(); + builder.EntityType(); + IEdmModel model = builder.GetEdmModel(); - // Act - ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); + // Act + ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); - // Assert - Assert.Same(model, context.Model); - Assert.True(context.ElementClrType == typeof(Customer)); - } + // Assert + Assert.Same(model, context.Model); + Assert.True(context.ElementClrType == typeof(Customer)); + } - [Theory] - [InlineData(typeof(object))] - [InlineData(typeof(Order))] - public void CtorODataQueryContext_TakingClrType_Throws_For_UnknownType(Type elementType) - { - // Arrange - ODataModelBuilder builder = new ODataModelBuilder(); - builder.EntityType(); - IEdmModel model = builder.GetEdmModel(); - - // Act && Assert - ExceptionAssert.ThrowsArgument(() => new ODataQueryContext(model, elementType), - "elementClrType", - Error.Format("The given model does not contain the type '{0}'.", elementType.FullName)); - } + [Theory] + [InlineData(typeof(object))] + [InlineData(typeof(Order))] + public void CtorODataQueryContext_TakingClrType_Throws_For_UnknownType(Type elementType) + { + // Arrange + ODataModelBuilder builder = new ODataModelBuilder(); + builder.EntityType(); + IEdmModel model = builder.GetEdmModel(); + + // Act && Assert + ExceptionAssert.ThrowsArgument(() => new ODataQueryContext(model, elementType), + "elementClrType", + Error.Format("The given model does not contain the type '{0}'.", elementType.FullName)); + } - [Fact] - public void CtorODataQueryContext_TakingClrTypeAndPath_SetsProperties() - { - // Arrange - ODataModelBuilder odataModel = new ODataModelBuilder().Add_Customer_EntityType(); - string setName = typeof(Customer).Name; - odataModel.EntitySet(setName); - IEdmModel model = odataModel.GetEdmModel(); - IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet(setName); - IEdmEntityType entityType = entitySet.EntityType; - ODataPath path = new ODataPath(new EntitySetSegment(entitySet)); - - // Act - ODataQueryContext context = new ODataQueryContext(model, typeof(Customer), path); - - // Assert - Assert.Same(model, context.Model); - Assert.Same(entityType, context.ElementType); - Assert.Same(entitySet, context.NavigationSource); - Assert.Same(typeof(Customer), context.ElementClrType); - } + [Fact] + public void CtorODataQueryContext_TakingClrTypeAndPath_SetsProperties() + { + // Arrange + ODataModelBuilder odataModel = new ODataModelBuilder().Add_Customer_EntityType(); + string setName = typeof(Customer).Name; + odataModel.EntitySet(setName); + IEdmModel model = odataModel.GetEdmModel(); + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet(setName); + IEdmEntityType entityType = entitySet.EntityType; + ODataPath path = new ODataPath(new EntitySetSegment(entitySet)); + + // Act + ODataQueryContext context = new ODataQueryContext(model, typeof(Customer), path); + + // Assert + Assert.Same(model, context.Model); + Assert.Same(entityType, context.ElementType); + Assert.Same(entitySet, context.NavigationSource); + Assert.Same(typeof(Customer), context.ElementClrType); + } - [Fact] - public void CtorODataQueryContext_TakingOperationAndPath_SetsProperties() + [Fact] + public void CtorODataQueryContext_TakingOperationAndPath_SetsProperties() + { + // Arrange + ODataModelBuilder odataModel = new ODataModelBuilder().Add_Customer_EntityType(); + string setName = typeof(Customer).Name; + odataModel.EntitySet(setName); + odataModel.EntitySet("Customers").EntityType + .Collection.Function("GetAllCustomers") + .ReturnsCollectionFromEntitySet("GetAllCustomer"); + IEdmModel model = odataModel.GetEdmModel(); + IEnumerable operationConfiguration = odataModel.Operations; + + string qualifiedName = null; + foreach(var op in operationConfiguration) { - // Arrange - ODataModelBuilder odataModel = new ODataModelBuilder().Add_Customer_EntityType(); - string setName = typeof(Customer).Name; - odataModel.EntitySet(setName); - odataModel.EntitySet("Customers").EntityType - .Collection.Function("GetAllCustomers") - .ReturnsCollectionFromEntitySet("GetAllCustomer"); - IEdmModel model = odataModel.GetEdmModel(); - IEnumerable operationConfiguration = odataModel.Operations; - - string qualifiedName = null; - foreach(var op in operationConfiguration) - { - qualifiedName = op.FullyQualifiedName; - } - - IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet(setName); - IEdmEntityType entityType = entitySet.EntityType; - IEnumerable operations = model.FindDeclaredOperations(qualifiedName); - - ODataPath path = new ODataPath(new EntitySetSegment(entitySet), new OperationSegment(operations, entitySet)); - - // Act - ODataQueryContext context = new ODataQueryContext(model, typeof(Customer), path); - - // Assert - Assert.Same(model, context.Model); - Assert.Same(entityType, context.TargetStructuredType); - Assert.Same(typeof(Customer), context.ElementClrType); + qualifiedName = op.FullyQualifiedName; } - [Fact] - public void CtorODataQueryContext_TakingEdmType_ThrowsArgumentNull_Model() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new ODataQueryContext(model: null, elementType: new Mock().Object), - "model"); - } + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet(setName); + IEdmEntityType entityType = entitySet.EntityType; + IEnumerable operations = model.FindDeclaredOperations(qualifiedName); - [Fact] - public void CtorODataQueryContext_TakingEdmType_ThrowsArgumentNull_ElementType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new ODataQueryContext(EdmCoreModel.Instance, elementType: null), - "elementType"); - } + ODataPath path = new ODataPath(new EntitySetSegment(entitySet), new OperationSegment(operations, entitySet)); - [Fact] - public void CtorODataQueryContext_TakingEdmType_InitializesProperties() - { - // Arrange - IEdmModel model = new EdmModel(); - IEdmType elementType = new Mock().Object; + // Act + ODataQueryContext context = new ODataQueryContext(model, typeof(Customer), path); - // Act - var context = new ODataQueryContext(model, elementType); + // Assert + Assert.Same(model, context.Model); + Assert.Same(entityType, context.TargetStructuredType); + Assert.Same(typeof(Customer), context.ElementClrType); + } - // Assert - Assert.Same(model, context.Model); - Assert.Same(elementType, context.ElementType); - Assert.Null(context.ElementClrType); - } + [Fact] + public void CtorODataQueryContext_TakingEdmType_ThrowsArgumentNull_Model() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new ODataQueryContext(model: null, elementType: new Mock().Object), + "model"); + } - [Fact] - public void CtorODataQueryContext_TakingEdmTypeAndPath_SetsProperties() - { - // Arrange - IEdmModel model = new EdmModel(); - IEdmEntityType entityType = new Mock().Object; - IEdmEntityContainer entityContiner = new Mock().Object; - EdmEntitySet entitySet = new EdmEntitySet(entityContiner, "entitySet", entityType); - ODataPath path = new ODataPath(new EntitySetSegment(entitySet)); - - // Act - ODataQueryContext context = new ODataQueryContext(model, entityType, path); - - // Assert - Assert.Same(model, context.Model); - Assert.Same(entityType, context.ElementType); - Assert.Same(entitySet, context.NavigationSource); - Assert.Null(context.ElementClrType); - } + [Fact] + public void CtorODataQueryContext_TakingEdmType_ThrowsArgumentNull_ElementType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new ODataQueryContext(EdmCoreModel.Instance, elementType: null), + "elementType"); + } - [Theory] - // Edm primitive kinds - [InlineData(typeof(byte[]))] - [InlineData(typeof(bool))] - [InlineData(typeof(byte))] - [InlineData(typeof(DateTime))] - [InlineData(typeof(DateTimeOffset))] - [InlineData(typeof(Date))] - [InlineData(typeof(TimeOfDay))] - [InlineData(typeof(decimal))] - [InlineData(typeof(double))] - [InlineData(typeof(Guid))] - [InlineData(typeof(short))] - [InlineData(typeof(int))] - [InlineData(typeof(long))] - [InlineData(typeof(sbyte))] - [InlineData(typeof(float))] - [InlineData(typeof(string))] - [InlineData(typeof(TimeSpan))] - // additional types not considered Edm primitives - // but which we permit in $skip and $top - [InlineData(typeof(int?))] - [InlineData(typeof(char))] - [InlineData(typeof(ushort))] - [InlineData(typeof(uint))] - [InlineData(typeof(ulong))] - public void CtorODataQueryContext_TakingClrType_WithPrimitiveTypes(Type type) - { - // Arrange & Act - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, type); + [Fact] + public void CtorODataQueryContext_TakingEdmType_InitializesProperties() + { + // Arrange + IEdmModel model = new EdmModel(); + IEdmType elementType = new Mock().Object; - // Assert - Assert.True(context.ElementClrType == type); - } + // Act + var context = new ODataQueryContext(model, elementType); - [Theory] - [InlineData(typeof(FlagsEnum))] - [InlineData(typeof(SimpleEnum))] - [InlineData(typeof(SimpleEnum?))] - [InlineData(typeof(LongEnum))] - [InlineData(typeof(FlagsEnum?))] - public void CtorODataQueryContext_TakingClrType_WithEnumTypes(Type type) - { - // Arrange - Type enumType = Nullable.GetUnderlyingType(type) ?? type; + // Assert + Assert.Same(model, context.Model); + Assert.Same(elementType, context.ElementType); + Assert.Null(context.ElementClrType); + } - ODataModelBuilder builder = new ODataModelBuilder(); - builder.AddEnumType(enumType); - IEdmModel model = builder.GetEdmModel(); + [Fact] + public void CtorODataQueryContext_TakingEdmTypeAndPath_SetsProperties() + { + // Arrange + IEdmModel model = new EdmModel(); + IEdmEntityType entityType = new Mock().Object; + IEdmEntityContainer entityContiner = new Mock().Object; + EdmEntitySet entitySet = new EdmEntitySet(entityContiner, "entitySet", entityType); + ODataPath path = new ODataPath(new EntitySetSegment(entitySet)); + + // Act + ODataQueryContext context = new ODataQueryContext(model, entityType, path); + + // Assert + Assert.Same(model, context.Model); + Assert.Same(entityType, context.ElementType); + Assert.Same(entitySet, context.NavigationSource); + Assert.Null(context.ElementClrType); + } - // Act - ODataQueryContext context = new ODataQueryContext(model, type); + [Theory] + // Edm primitive kinds + [InlineData(typeof(byte[]))] + [InlineData(typeof(bool))] + [InlineData(typeof(byte))] + [InlineData(typeof(DateTime))] + [InlineData(typeof(DateTimeOffset))] + [InlineData(typeof(Date))] + [InlineData(typeof(TimeOfDay))] + [InlineData(typeof(decimal))] + [InlineData(typeof(double))] + [InlineData(typeof(Guid))] + [InlineData(typeof(short))] + [InlineData(typeof(int))] + [InlineData(typeof(long))] + [InlineData(typeof(sbyte))] + [InlineData(typeof(float))] + [InlineData(typeof(string))] + [InlineData(typeof(TimeSpan))] + // additional types not considered Edm primitives + // but which we permit in $skip and $top + [InlineData(typeof(int?))] + [InlineData(typeof(char))] + [InlineData(typeof(ushort))] + [InlineData(typeof(uint))] + [InlineData(typeof(ulong))] + public void CtorODataQueryContext_TakingClrType_WithPrimitiveTypes(Type type) + { + // Arrange & Act + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, type); - // Assert - Assert.True(context.ElementClrType == type); - } + // Assert + Assert.True(context.ElementClrType == type); + } + + [Theory] + [InlineData(typeof(FlagsEnum))] + [InlineData(typeof(SimpleEnum))] + [InlineData(typeof(SimpleEnum?))] + [InlineData(typeof(LongEnum))] + [InlineData(typeof(FlagsEnum?))] + public void CtorODataQueryContext_TakingClrType_WithEnumTypes(Type type) + { + // Arrange + Type enumType = Nullable.GetUnderlyingType(type) ?? type; + + ODataModelBuilder builder = new ODataModelBuilder(); + builder.AddEnumType(enumType); + IEdmModel model = builder.GetEdmModel(); + + // Act + ODataQueryContext context = new ODataQueryContext(model, type); + + // Assert + Assert.True(context.ElementClrType == type); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/ODataQueryRequestMiddlewareTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/ODataQueryRequestMiddlewareTests.cs index 2c8460dd3..6a52f75bb 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/ODataQueryRequestMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/ODataQueryRequestMiddlewareTests.cs @@ -14,55 +14,54 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class ODataQueryRequestMiddlewareTests { - public class ODataQueryRequestMiddlewareTests + [Fact] + public void InvokeQueryRequestMiddleware_ThrowsArgumentNull_Context() { - [Fact] - public void InvokeQueryRequestMiddleware_ThrowsArgumentNull_Context() - { - // Arrange - ODataQueryRequestMiddleware middleware = new ODataQueryRequestMiddleware(null, null); + // Arrange + ODataQueryRequestMiddleware middleware = new ODataQueryRequestMiddleware(null, null); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => middleware.Invoke(context: null).Wait(), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => middleware.Invoke(context: null).Wait(), "context"); + } - [Fact] - public async void InvokeQueryRequestMiddleware_Transforms_ODataQueryRequest() + [Fact] + public async void InvokeQueryRequestMiddleware_Transforms_ODataQueryRequest() + { + // Arrange + RequestDelegate next = c => Task.CompletedTask; + IODataQueryRequestParser[] parsers = new IODataQueryRequestParser[] { - // Arrange - RequestDelegate next = c => Task.CompletedTask; - IODataQueryRequestParser[] parsers = new IODataQueryRequestParser[] - { - new DefaultODataQueryRequestParser() - }; + new DefaultODataQueryRequestParser() + }; - ODataQueryRequestMiddleware middleware = new ODataQueryRequestMiddleware(parsers, next); + ODataQueryRequestMiddleware middleware = new ODataQueryRequestMiddleware(parsers, next); - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - context.Request.Path = new PathString("/$query"); - request.ContentType = "text/plain"; - request.Method = "Post"; - context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("$filter=Id le 5")); + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; + context.Request.Path = new PathString("/$query"); + request.ContentType = "text/plain"; + request.Method = "Post"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("$filter=Id le 5")); - // Act - await middleware.Invoke(context); + // Act + await middleware.Invoke(context); - // Assert - Assert.Equal("Get", request.Method, ignoreCase: true); - Assert.Equal("?$filter=Id le 5", request.QueryString.Value); - } + // Assert + Assert.Equal("Get", request.Method, ignoreCase: true); + Assert.Equal("?$filter=Id le 5", request.QueryString.Value); + } - [Fact] - public void TransformQueryRequestAsync_ThrowsArgumentNull_Request() - { - // Arrange - Mock parser = new Mock(); + [Fact] + public void TransformQueryRequestAsync_ThrowsArgumentNull_Request() + { + // Arrange + Mock parser = new Mock(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => ODataQueryRequestMiddleware.TransformQueryRequestAsync(parser.Object, null).Wait(), "request"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => ODataQueryRequestMiddleware.TransformQueryRequestAsync(parser.Object, null).Wait(), "request"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/ODataValidationSettingsTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/ODataValidationSettingsTest.cs index 772639319..5b5b0870f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/ODataValidationSettingsTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/ODataValidationSettingsTest.cs @@ -10,175 +10,174 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class ODataValidationSettingsTest { - public class ODataValidationSettingsTest + [Fact] + public void Ctor_Initializes_All_Properties() + { + // Arrange & Act + ODataValidationSettings querySettings = new ODataValidationSettings(); + + // Assert + Assert.Equal(AllowedArithmeticOperators.All, querySettings.AllowedArithmeticOperators); + Assert.Equal(AllowedFunctions.AllFunctions, querySettings.AllowedFunctions); + Assert.Equal(AllowedLogicalOperators.All, querySettings.AllowedLogicalOperators); + Assert.Empty(querySettings.AllowedOrderByProperties); + Assert.Equal(AllowedQueryOptions.Supported, querySettings.AllowedQueryOptions); + Assert.Equal(1, querySettings.MaxAnyAllExpressionDepth); + Assert.Equal(100, querySettings.MaxNodeCount); + Assert.Null(querySettings.MaxSkip); + Assert.Null(querySettings.MaxTop); + } + + [Fact] + public void AllowedFunctions_Property_RoundTrips() + { + ReflectionAssert.EnumProperty( + new ODataValidationSettings(), + o => o.AllowedFunctions, + expectedDefaultValue: AllowedFunctions.AllFunctions, + illegalValue: AllowedFunctions.AllFunctions + 1, + roundTripTestValue: AllowedFunctions.AllMathFunctions); + } + + [Fact] + public void AllowedFunctions_SetToAllFunctions_DoesNotThrow() + { + ExceptionAssert.DoesNotThrow(() => new ODataValidationSettings().AllowedFunctions = AllowedFunctions.AllFunctions); + } + + [Fact] + public void AllowedArithmeticOperators_Property_RoundTrips() + { + ReflectionAssert.EnumProperty( + new ODataValidationSettings(), + o => o.AllowedArithmeticOperators, + expectedDefaultValue: AllowedArithmeticOperators.All, + illegalValue: AllowedArithmeticOperators.All + 1, + roundTripTestValue: AllowedArithmeticOperators.Multiply); + } + + [Fact] + public void AllowedLogicalOperators_Property_RoundTrips() + { + ReflectionAssert.EnumProperty( + new ODataValidationSettings(), + o => o.AllowedLogicalOperators, + expectedDefaultValue: AllowedLogicalOperators.All, + illegalValue: AllowedLogicalOperators.All + 1, + roundTripTestValue: AllowedLogicalOperators.GreaterThanOrEqual | AllowedLogicalOperators.LessThanOrEqual); + } + + [Fact] + public void AllowedQueryOptions_Property_RoundTrips() + { + ReflectionAssert.EnumProperty( + new ODataValidationSettings(), + o => o.AllowedQueryOptions, + expectedDefaultValue: AllowedQueryOptions.Supported, + illegalValue: AllowedQueryOptions.All + 1, + roundTripTestValue: AllowedQueryOptions.Filter); + } + + [Fact] + public void AllowedOrderByProperties_Property_RoundTrips() + { + ODataValidationSettings settings = new ODataValidationSettings(); + Assert.NotNull(settings.AllowedOrderByProperties); + Assert.Empty(settings.AllowedOrderByProperties); + + settings.AllowedOrderByProperties.Add("Id"); + settings.AllowedOrderByProperties.Add("Name"); + + Assert.Equal(2, settings.AllowedOrderByProperties.Count); + Assert.Contains("Id", settings.AllowedOrderByProperties); + Assert.Contains("Name", settings.AllowedOrderByProperties); + } + + [Fact] + public void MaxAnyAllExpressionDepth_Property_RoundTrips() + { + ReflectionAssert.IntegerProperty( + new ODataValidationSettings(), + o => o.MaxAnyAllExpressionDepth, + expectedDefaultValue: 1, + minLegalValue: 1, + maxLegalValue: int.MaxValue, + illegalLowerValue: 0, + illegalUpperValue: null, + roundTripTestValue: 2); + } + + [Fact] + public void MaxNodeCount_Property_RoundTrips() + { + ReflectionAssert.IntegerProperty( + new ODataValidationSettings(), + o => o.MaxNodeCount, + expectedDefaultValue: 100, + minLegalValue: 1, + maxLegalValue: int.MaxValue, + illegalLowerValue: 0, + illegalUpperValue: null, + roundTripTestValue: 2); + } + + [Fact] + public void MaxTop_Property_RoundTrips() + { + ReflectionAssert.NullableIntegerProperty( + new ODataValidationSettings(), + o => o.MaxTop, + expectedDefaultValue: null, + minLegalValue: 0, + maxLegalValue: int.MaxValue, + illegalLowerValue: -1, + illegalUpperValue: null, + roundTripTestValue: 2); + } + + [Fact] + public void MaxSkip_Property_RoundTrips() + { + ReflectionAssert.NullableIntegerProperty( + new ODataValidationSettings(), + o => o.MaxSkip, + expectedDefaultValue: null, + minLegalValue: 0, + maxLegalValue: int.MaxValue, + illegalLowerValue: -1, + illegalUpperValue: null, + roundTripTestValue: 2); + } + + [Fact] + public void MaxExpansionDepth_Property_RoundTrips() + { + ReflectionAssert.IntegerProperty( + new ODataValidationSettings(), + o => o.MaxExpansionDepth, + expectedDefaultValue: 2, + minLegalValue: 0, + maxLegalValue: int.MaxValue, + illegalLowerValue: -1, + illegalUpperValue: null, + roundTripTestValue: 100); + } + + [Fact] + public void MaxOrderByNodeCount_Property_RoundTrips() { - [Fact] - public void Ctor_Initializes_All_Properties() - { - // Arrange & Act - ODataValidationSettings querySettings = new ODataValidationSettings(); - - // Assert - Assert.Equal(AllowedArithmeticOperators.All, querySettings.AllowedArithmeticOperators); - Assert.Equal(AllowedFunctions.AllFunctions, querySettings.AllowedFunctions); - Assert.Equal(AllowedLogicalOperators.All, querySettings.AllowedLogicalOperators); - Assert.Empty(querySettings.AllowedOrderByProperties); - Assert.Equal(AllowedQueryOptions.Supported, querySettings.AllowedQueryOptions); - Assert.Equal(1, querySettings.MaxAnyAllExpressionDepth); - Assert.Equal(100, querySettings.MaxNodeCount); - Assert.Null(querySettings.MaxSkip); - Assert.Null(querySettings.MaxTop); - } - - [Fact] - public void AllowedFunctions_Property_RoundTrips() - { - ReflectionAssert.EnumProperty( - new ODataValidationSettings(), - o => o.AllowedFunctions, - expectedDefaultValue: AllowedFunctions.AllFunctions, - illegalValue: AllowedFunctions.AllFunctions + 1, - roundTripTestValue: AllowedFunctions.AllMathFunctions); - } - - [Fact] - public void AllowedFunctions_SetToAllFunctions_DoesNotThrow() - { - ExceptionAssert.DoesNotThrow(() => new ODataValidationSettings().AllowedFunctions = AllowedFunctions.AllFunctions); - } - - [Fact] - public void AllowedArithmeticOperators_Property_RoundTrips() - { - ReflectionAssert.EnumProperty( - new ODataValidationSettings(), - o => o.AllowedArithmeticOperators, - expectedDefaultValue: AllowedArithmeticOperators.All, - illegalValue: AllowedArithmeticOperators.All + 1, - roundTripTestValue: AllowedArithmeticOperators.Multiply); - } - - [Fact] - public void AllowedLogicalOperators_Property_RoundTrips() - { - ReflectionAssert.EnumProperty( - new ODataValidationSettings(), - o => o.AllowedLogicalOperators, - expectedDefaultValue: AllowedLogicalOperators.All, - illegalValue: AllowedLogicalOperators.All + 1, - roundTripTestValue: AllowedLogicalOperators.GreaterThanOrEqual | AllowedLogicalOperators.LessThanOrEqual); - } - - [Fact] - public void AllowedQueryOptions_Property_RoundTrips() - { - ReflectionAssert.EnumProperty( - new ODataValidationSettings(), - o => o.AllowedQueryOptions, - expectedDefaultValue: AllowedQueryOptions.Supported, - illegalValue: AllowedQueryOptions.All + 1, - roundTripTestValue: AllowedQueryOptions.Filter); - } - - [Fact] - public void AllowedOrderByProperties_Property_RoundTrips() - { - ODataValidationSettings settings = new ODataValidationSettings(); - Assert.NotNull(settings.AllowedOrderByProperties); - Assert.Empty(settings.AllowedOrderByProperties); - - settings.AllowedOrderByProperties.Add("Id"); - settings.AllowedOrderByProperties.Add("Name"); - - Assert.Equal(2, settings.AllowedOrderByProperties.Count); - Assert.Contains("Id", settings.AllowedOrderByProperties); - Assert.Contains("Name", settings.AllowedOrderByProperties); - } - - [Fact] - public void MaxAnyAllExpressionDepth_Property_RoundTrips() - { - ReflectionAssert.IntegerProperty( - new ODataValidationSettings(), - o => o.MaxAnyAllExpressionDepth, - expectedDefaultValue: 1, - minLegalValue: 1, - maxLegalValue: int.MaxValue, - illegalLowerValue: 0, - illegalUpperValue: null, - roundTripTestValue: 2); - } - - [Fact] - public void MaxNodeCount_Property_RoundTrips() - { - ReflectionAssert.IntegerProperty( - new ODataValidationSettings(), - o => o.MaxNodeCount, - expectedDefaultValue: 100, - minLegalValue: 1, - maxLegalValue: int.MaxValue, - illegalLowerValue: 0, - illegalUpperValue: null, - roundTripTestValue: 2); - } - - [Fact] - public void MaxTop_Property_RoundTrips() - { - ReflectionAssert.NullableIntegerProperty( - new ODataValidationSettings(), - o => o.MaxTop, - expectedDefaultValue: null, - minLegalValue: 0, - maxLegalValue: int.MaxValue, - illegalLowerValue: -1, - illegalUpperValue: null, - roundTripTestValue: 2); - } - - [Fact] - public void MaxSkip_Property_RoundTrips() - { - ReflectionAssert.NullableIntegerProperty( - new ODataValidationSettings(), - o => o.MaxSkip, - expectedDefaultValue: null, - minLegalValue: 0, - maxLegalValue: int.MaxValue, - illegalLowerValue: -1, - illegalUpperValue: null, - roundTripTestValue: 2); - } - - [Fact] - public void MaxExpansionDepth_Property_RoundTrips() - { - ReflectionAssert.IntegerProperty( - new ODataValidationSettings(), - o => o.MaxExpansionDepth, - expectedDefaultValue: 2, - minLegalValue: 0, - maxLegalValue: int.MaxValue, - illegalLowerValue: -1, - illegalUpperValue: null, - roundTripTestValue: 100); - } - - [Fact] - public void MaxOrderByNodeCount_Property_RoundTrips() - { - ReflectionAssert.IntegerProperty( - new ODataValidationSettings(), - o => o.MaxOrderByNodeCount, - expectedDefaultValue: 5, - minLegalValue: 1, - maxLegalValue: int.MaxValue, - illegalLowerValue: -1, - illegalUpperValue: null, - roundTripTestValue: 100); - } + ReflectionAssert.IntegerProperty( + new ODataValidationSettings(), + o => o.MaxOrderByNodeCount, + expectedDefaultValue: 5, + minLegalValue: 1, + maxLegalValue: int.MaxValue, + illegalLowerValue: -1, + illegalUpperValue: null, + roundTripTestValue: 100); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/OrderByPropertyNodeTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/OrderByPropertyNodeTest.cs index 51e0678cb..f31cb6720 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/OrderByPropertyNodeTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/OrderByPropertyNodeTest.cs @@ -18,177 +18,176 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class OrderByPropertyNodeTest { - public class OrderByPropertyNodeTest + [Fact] + public void Constructor_With_Null_Throws() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new OrderByPropertyNode(property: null, direction: OrderByDirection.Ascending), + "property"); + } + + [Fact] + public void Constructor_Initializes_Correctly() + { + // Arrange + Mock mockProperty = new Mock(); + + // Act + OrderByPropertyNode node = new OrderByPropertyNode(mockProperty.Object, OrderByDirection.Descending); + + // Assert + Assert.Same(mockProperty.Object, node.Property); + Assert.Equal(OrderByDirection.Descending, node.Direction); + } + + [Fact] + public void Ctor_TakingOrderByClause_ThrowsArgumentNull_OrderByClause() { - [Fact] - public void Constructor_With_Null_Throws() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new OrderByPropertyNode(property: null, direction: OrderByDirection.Ascending), - "property"); - } - - [Fact] - public void Constructor_Initializes_Correctly() - { - // Arrange - Mock mockProperty = new Mock(); - - // Act - OrderByPropertyNode node = new OrderByPropertyNode(mockProperty.Object, OrderByDirection.Descending); - - // Assert - Assert.Same(mockProperty.Object, node.Property); - Assert.Equal(OrderByDirection.Descending, node.Direction); - } - - [Fact] - public void Ctor_TakingOrderByClause_ThrowsArgumentNull_OrderByClause() - { - ExceptionAssert.ThrowsArgumentNull(() => new OrderByPropertyNode(orderByClause: null), "orderByClause"); - } - - [Fact] - public void Ctor_TakingOrderByClause_InitializesProperty_Property() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - IEdmProperty property = model.Customer.FindProperty("ID"); - ResourceRangeVariable variable = new ResourceRangeVariable("it", model.Customer.AsReference(), model.Customers); - SingleValuePropertyAccessNode node = new SingleValuePropertyAccessNode(new ResourceRangeVariableReferenceNode("it", variable), property); - OrderByClause orderBy = new OrderByClause(thenBy: null, expression: node, direction: OrderByDirection.Ascending, rangeVariable: variable); - - // Act - OrderByPropertyNode orderByNode = new OrderByPropertyNode(orderBy); - - // Assert - Assert.Equal(property, orderByNode.Property); - } - - [Fact] - public void Ctor_TakingOrderByClause_InitializesProperty_Direction_and_PropertyPath() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - IEdmProperty property = model.Customer.FindProperty("ID"); - OrderByDirection direction = OrderByDirection.Ascending; - ResourceRangeVariable variable = new ResourceRangeVariable("it", model.Customer.AsReference(), model.Customers); - SingleValuePropertyAccessNode node = new SingleValuePropertyAccessNode(new ResourceRangeVariableReferenceNode("it", variable), property); - OrderByClause orderBy = new OrderByClause(thenBy: null, expression: node, direction: direction, rangeVariable: variable); - - // Act - OrderByPropertyNode orderByNode = new OrderByPropertyNode(orderBy); - - // Assert - Assert.Equal(direction, orderByNode.Direction); - Assert.Equal("ID", orderByNode.PropertyPath); - } - - [Fact] - public void CreateCollection_From_OrderByNode_Succeeds() - { - // Arrange - ODataConventionModelBuilder builder = ODataModelBuilderMocks.GetModelBuilderMock(); - builder.EntitySet("entityset"); - - IEdmModel model = builder.GetEdmModel(); - IEdmEntityType sampleClassEntityType = model.SchemaElements.Single(t => t.Name == "SampleClass") as IEdmEntityType; - Assert.NotNull(sampleClassEntityType); // Guard - IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("entityset"); - Assert.NotNull(entitySet); // Guard - - ODataQueryOptionParser parser = new ODataQueryOptionParser(model, sampleClassEntityType, entitySet, - new Dictionary { { "$orderby", "Property1 desc, Property2 asc" } }); - OrderByClause orderbyNode = parser.ParseOrderBy(); - - // Act - ICollection nodes = OrderByNode.CreateCollection(orderbyNode); - - // Assert - Assert.False(nodes.OfType().Any()); - IEnumerable propertyNodes = nodes.OfType(); - Assert.Equal(2, propertyNodes.Count()); - Assert.Equal("Property1", propertyNodes.First().Property.Name); - Assert.Equal(OrderByDirection.Descending, propertyNodes.First().Direction); - Assert.Equal("Property1", propertyNodes.First().PropertyPath); - - Assert.Equal("Property2", propertyNodes.Last().Property.Name); - Assert.Equal(OrderByDirection.Ascending, nodes.Last().Direction); - Assert.Equal("Property2", propertyNodes.Last().PropertyPath); - } - - [Theory] - [InlineData(true, "PropertyAlias2", "FirstNameAlias")] - [InlineData(false, "PropertyAlias", "FirstName")] - public void CreateCollection_PropertyAliased_IfEnabled(bool modelAliasing, string typeName, string propertyName) - { - // Arrange - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.ModelAliasingEnabled = modelAliasing; - builder.EntitySet("entityset"); - - IEdmModel model = builder.GetEdmModel(); - IEdmEntityType entityType = model.SchemaElements.Single(t => t.Name == typeName) as IEdmEntityType; - Assert.NotNull(entityType); // Guard - IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("entityset"); - Assert.NotNull(entitySet); // Guard - - ODataQueryOptionParser parser = new ODataQueryOptionParser(model, entityType, entitySet, - new Dictionary { { "$orderby", propertyName + " desc, Id asc" } }); - OrderByClause orderbyNode = parser.ParseOrderBy(); - - // Act - ICollection nodes = OrderByNode.CreateCollection(orderbyNode); - - // Assert - Assert.False(nodes.OfType().Any()); - IEnumerable propertyNodes = nodes.OfType(); - Assert.Equal(2, propertyNodes.Count()); - Assert.Equal(propertyName, propertyNodes.First().Property.Name); - Assert.Equal(OrderByDirection.Descending, propertyNodes.First().Direction); - Assert.Equal(propertyName, propertyNodes.First().PropertyPath); - - Assert.Equal("Id", propertyNodes.Last().Property.Name); - Assert.Equal(OrderByDirection.Ascending, nodes.Last().Direction); - Assert.Equal("Id", propertyNodes.Last().PropertyPath); - } - - [Fact] - public void CreateCollection_ComplexType_Succeeds() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryOptionParser parser = new ODataQueryOptionParser(model.Model, model.Customer, model.Customers, - new Dictionary { { "$orderby", "Address/Street desc, Address/City asc, Account/BankAddress/City asc" } }); - - OrderByClause orderByNode = parser.ParseOrderBy(); - - // Act - ICollection nodes = OrderByNode.CreateCollection(orderByNode); - - // Assert - Assert.Equal(3, nodes.Count()); - Assert.Equal("Street", (nodes.ToList()[0] as OrderByPropertyNode).Property.Name); - Assert.Equal(OrderByDirection.Descending, nodes.ToList()[0].Direction); - Assert.Equal("Address/Street", nodes.ToList()[0].PropertyPath); - - Assert.Equal("City", (nodes.ToList()[1] as OrderByPropertyNode).Property.Name); - Assert.Equal(OrderByDirection.Ascending, nodes.ToList()[1].Direction); - Assert.Equal("Address/City", nodes.ToList()[1].PropertyPath); - - Assert.Equal("City", (nodes.ToList()[2] as OrderByPropertyNode).Property.Name); - Assert.Equal(OrderByDirection.Ascending, nodes.ToList()[2].Direction); - Assert.Equal("Account/BankAddress/City", nodes.ToList()[2].PropertyPath); - } - - private class SampleClass - { - public string Property1 { get; set; } - - public string Property2 { get; set; } - } + ExceptionAssert.ThrowsArgumentNull(() => new OrderByPropertyNode(orderByClause: null), "orderByClause"); + } + + [Fact] + public void Ctor_TakingOrderByClause_InitializesProperty_Property() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + IEdmProperty property = model.Customer.FindProperty("ID"); + ResourceRangeVariable variable = new ResourceRangeVariable("it", model.Customer.AsReference(), model.Customers); + SingleValuePropertyAccessNode node = new SingleValuePropertyAccessNode(new ResourceRangeVariableReferenceNode("it", variable), property); + OrderByClause orderBy = new OrderByClause(thenBy: null, expression: node, direction: OrderByDirection.Ascending, rangeVariable: variable); + + // Act + OrderByPropertyNode orderByNode = new OrderByPropertyNode(orderBy); + + // Assert + Assert.Equal(property, orderByNode.Property); + } + + [Fact] + public void Ctor_TakingOrderByClause_InitializesProperty_Direction_and_PropertyPath() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + IEdmProperty property = model.Customer.FindProperty("ID"); + OrderByDirection direction = OrderByDirection.Ascending; + ResourceRangeVariable variable = new ResourceRangeVariable("it", model.Customer.AsReference(), model.Customers); + SingleValuePropertyAccessNode node = new SingleValuePropertyAccessNode(new ResourceRangeVariableReferenceNode("it", variable), property); + OrderByClause orderBy = new OrderByClause(thenBy: null, expression: node, direction: direction, rangeVariable: variable); + + // Act + OrderByPropertyNode orderByNode = new OrderByPropertyNode(orderBy); + + // Assert + Assert.Equal(direction, orderByNode.Direction); + Assert.Equal("ID", orderByNode.PropertyPath); + } + + [Fact] + public void CreateCollection_From_OrderByNode_Succeeds() + { + // Arrange + ODataConventionModelBuilder builder = ODataModelBuilderMocks.GetModelBuilderMock(); + builder.EntitySet("entityset"); + + IEdmModel model = builder.GetEdmModel(); + IEdmEntityType sampleClassEntityType = model.SchemaElements.Single(t => t.Name == "SampleClass") as IEdmEntityType; + Assert.NotNull(sampleClassEntityType); // Guard + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("entityset"); + Assert.NotNull(entitySet); // Guard + + ODataQueryOptionParser parser = new ODataQueryOptionParser(model, sampleClassEntityType, entitySet, + new Dictionary { { "$orderby", "Property1 desc, Property2 asc" } }); + OrderByClause orderbyNode = parser.ParseOrderBy(); + + // Act + ICollection nodes = OrderByNode.CreateCollection(orderbyNode); + + // Assert + Assert.False(nodes.OfType().Any()); + IEnumerable propertyNodes = nodes.OfType(); + Assert.Equal(2, propertyNodes.Count()); + Assert.Equal("Property1", propertyNodes.First().Property.Name); + Assert.Equal(OrderByDirection.Descending, propertyNodes.First().Direction); + Assert.Equal("Property1", propertyNodes.First().PropertyPath); + + Assert.Equal("Property2", propertyNodes.Last().Property.Name); + Assert.Equal(OrderByDirection.Ascending, nodes.Last().Direction); + Assert.Equal("Property2", propertyNodes.Last().PropertyPath); + } + + [Theory] + [InlineData(true, "PropertyAlias2", "FirstNameAlias")] + [InlineData(false, "PropertyAlias", "FirstName")] + public void CreateCollection_PropertyAliased_IfEnabled(bool modelAliasing, string typeName, string propertyName) + { + // Arrange + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.ModelAliasingEnabled = modelAliasing; + builder.EntitySet("entityset"); + + IEdmModel model = builder.GetEdmModel(); + IEdmEntityType entityType = model.SchemaElements.Single(t => t.Name == typeName) as IEdmEntityType; + Assert.NotNull(entityType); // Guard + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("entityset"); + Assert.NotNull(entitySet); // Guard + + ODataQueryOptionParser parser = new ODataQueryOptionParser(model, entityType, entitySet, + new Dictionary { { "$orderby", propertyName + " desc, Id asc" } }); + OrderByClause orderbyNode = parser.ParseOrderBy(); + + // Act + ICollection nodes = OrderByNode.CreateCollection(orderbyNode); + + // Assert + Assert.False(nodes.OfType().Any()); + IEnumerable propertyNodes = nodes.OfType(); + Assert.Equal(2, propertyNodes.Count()); + Assert.Equal(propertyName, propertyNodes.First().Property.Name); + Assert.Equal(OrderByDirection.Descending, propertyNodes.First().Direction); + Assert.Equal(propertyName, propertyNodes.First().PropertyPath); + + Assert.Equal("Id", propertyNodes.Last().Property.Name); + Assert.Equal(OrderByDirection.Ascending, nodes.Last().Direction); + Assert.Equal("Id", propertyNodes.Last().PropertyPath); + } + + [Fact] + public void CreateCollection_ComplexType_Succeeds() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryOptionParser parser = new ODataQueryOptionParser(model.Model, model.Customer, model.Customers, + new Dictionary { { "$orderby", "Address/Street desc, Address/City asc, Account/BankAddress/City asc" } }); + + OrderByClause orderByNode = parser.ParseOrderBy(); + + // Act + ICollection nodes = OrderByNode.CreateCollection(orderByNode); + + // Assert + Assert.Equal(3, nodes.Count()); + Assert.Equal("Street", (nodes.ToList()[0] as OrderByPropertyNode).Property.Name); + Assert.Equal(OrderByDirection.Descending, nodes.ToList()[0].Direction); + Assert.Equal("Address/Street", nodes.ToList()[0].PropertyPath); + + Assert.Equal("City", (nodes.ToList()[1] as OrderByPropertyNode).Property.Name); + Assert.Equal(OrderByDirection.Ascending, nodes.ToList()[1].Direction); + Assert.Equal("Address/City", nodes.ToList()[1].PropertyPath); + + Assert.Equal("City", (nodes.ToList()[2] as OrderByPropertyNode).Property.Name); + Assert.Equal(OrderByDirection.Ascending, nodes.ToList()[2].Direction); + Assert.Equal("Account/BankAddress/City", nodes.ToList()[2].PropertyPath); + } + + private class SampleClass + { + public string Property1 { get; set; } + + public string Property2 { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/ParameterAliasNodeTranslatorTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/ParameterAliasNodeTranslatorTest.cs index d9c668ac8..795a9691e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/ParameterAliasNodeTranslatorTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/ParameterAliasNodeTranslatorTest.cs @@ -14,377 +14,376 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class ParameterAliasNodeTranslatorTest { - public class ParameterAliasNodeTranslatorTest + private IEdmModel _model; + private IEdmEntitySet _customersEntitySet; + private IEdmEntityType _customerEntityType; + private SingleValueNode _parameterAliasMappedNode; + + public ParameterAliasNodeTranslatorTest() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); + + builder.EntityType().Function("CollectionFunctionCall") + .ReturnsCollection().Parameter("p1"); + + builder.EntityType().Function("EntityCollectionFunctionCall") + .ReturnsCollectionFromEntitySet("Customers").Parameter("p1"); + + builder.EntityType().Function("SingleEntityFunctionCall") + .Returns().Parameter("p1"); + + builder.EntityType().Function("SingleEntityFunctionCallWithoutParameters") + .Returns(); + + builder.EntityType().Function("SingleValueFunctionCall") + .Returns().Parameter("p1"); + + _model = builder.GetEdmModel(); + _customersEntitySet = _model.FindDeclaredEntitySet("Customers"); + _customerEntityType = _customersEntitySet.EntityType; + _parameterAliasMappedNode = new ConstantNode(123); + } + + [Fact] + public void Constructor_Throws_NullParameterAliasNodes() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ParameterAliasNodeTranslator(null), "parameterAliasNodes"); + } + + [Fact] + public void ReturnsNull_ParameterAliasNotFound() + { + // Arrange + var translator = new ParameterAliasNodeTranslator(new Dictionary()); + var parameterAliasNode = new ParameterAliasNode("@unknown", null); + + // Act + QueryNode translatedNode = parameterAliasNode.Accept(translator); + + // Assert + var constantNode = Assert.IsType(translatedNode); + Assert.Null(constantNode.Value); + } + + [Fact] + public void CanTranslate_ParameterAliasNode() + { + // Arrange + var translator = new ParameterAliasNodeTranslator( + new Dictionary { { "@p", _parameterAliasMappedNode } }); + var parameterAliasNode = new ParameterAliasNode("@p", null); + + // Act + QueryNode translatedNode = parameterAliasNode.Accept(translator); + + // Assert + Assert.Same(_parameterAliasMappedNode, translatedNode); + } + + [Fact] + public void CanTranslate_AllNode() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Orders/all(order: order/ID eq @p)"); + + // Assert + var allNode = Assert.IsType(translatedNode); + var binaryOperatorNode = Assert.IsType(allNode.Body); + var convertNode = Assert.IsType(binaryOperatorNode.Right); + Assert.Same(_parameterAliasMappedNode, convertNode.Source); + } + + [Fact] + public void CanTranslate_AnyNode() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Orders/any(order: @p ne order/ID)"); + + // Assert + var anyNode = Assert.IsType(translatedNode); + var binaryOperatorNode = Assert.IsType(anyNode.Body); + var convertNode = Assert.IsType(binaryOperatorNode.Left); + Assert.Same(_parameterAliasMappedNode, convertNode.Source); + } + + [Fact] + public void CanTranslate_CollectionFunctionCallNode() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Default.CollectionFunctionCall(p1=@p)/all(r : r ne null)"); + + // Assert + var allNode = Assert.IsType(translatedNode); + var collectionFunctionCallNode = Assert.IsType(allNode.Source); + var singleEntityFunctionCallNode = Assert.IsType(collectionFunctionCallNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + namedFunctionParameterNode = Assert.IsType(collectionFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_CollectionNavigationNode() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Orders/all(order : order ne null)"); + + // Assert + var allNode = Assert.IsType(translatedNode); + var collectionNavigationNode = Assert.IsType(allNode.Source); + var singleEntityFunctionCallNode = Assert.IsType(collectionNavigationNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_CollectionOpenPropertyAccessNode() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/OpenComplex/CollectionProperty/all(p : p ne null)"); + + // Assert + var allNode = Assert.IsType(translatedNode); + var collectionOpenPropertyAccessNode = Assert.IsType(allNode.Source); + var singleComplexNode = Assert.IsType(collectionOpenPropertyAccessNode.Source); + var singleEntityFunctionCallNode = Assert.IsType(singleComplexNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_CollectionPropertyAccessNode() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Addresses/all(a : a ne null)"); + + // Assert + var allNode = Assert.IsType(translatedNode); + var collectionComplexNode = Assert.IsType(allNode.Source); + var singleEntityFunctionCallNode = Assert.IsType(collectionComplexNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_CollectionResourceCastNode_FroComplex() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Addresses/Microsoft.AspNetCore.OData.Tests.Query.ParameterAliasAddress/all(a : a ne null)"); + + // Assert + var allNode = Assert.IsType(translatedNode); + var collectionPropertyCastNode = Assert.IsType(allNode.Source); + var collectionComplexNode = Assert.IsType(collectionPropertyCastNode.Source); + var singleEntityFunctionCallNode = Assert.IsType(collectionComplexNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_CollectionResourceCastNode_ForEntity() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Orders/Microsoft.AspNetCore.OData.Tests.Query.ParameterAliasOrder/all(o : o ne null)"); + + // Assert + var allNode = Assert.IsType(translatedNode); + var entityCollectionCastNode = Assert.IsType(allNode.Source); + var collectionNavigationNode = Assert.IsType(entityCollectionCastNode.Source); + var singleEntityFunctionCallNode = Assert.IsType(collectionNavigationNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_CollectionResourceFunctionCallNode() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Default.EntityCollectionFunctionCall(p1=@p)/any(r : r eq null)"); + + // Assert + var anyNode = Assert.IsType(translatedNode); + var entityCollectionFunctionCallNode = Assert.IsType(anyNode.Source); + var singleEntityFunctionCallNode = Assert.IsType(entityCollectionFunctionCallNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + namedFunctionParameterNode = Assert.IsType(entityCollectionFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_SingleResourceCastNode_ForEntity() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Microsoft.AspNetCore.OData.Tests.Query.ParameterAliasCustomer eq null"); + + // Assert + var binaryOperatorNode = Assert.IsType(translatedNode); + var singleEntityCastNode = Assert.IsType(binaryOperatorNode.Left); + var singleEntityFunctionCallNode = Assert.IsType(singleEntityCastNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_SingleResourceFunctionCallNode() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Default.SingleEntityFunctionCall(p1=@p) eq null"); + + // Assert + var binaryOperatorNode = Assert.IsType(translatedNode); + var singleEntityFunctionCallNode = Assert.IsType(binaryOperatorNode.Left); + var singleEntityFunctionCallSourceNode = Assert.IsType(singleEntityFunctionCallNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallSourceNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_SingleResourceFunctionCallNodeWithoutParameters() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Default.SingleEntityFunctionCallWithoutParameters() eq null"); + + // Assert + var binaryOperatorNode = Assert.IsType(translatedNode); + var singleEntityFunctionCallNode = Assert.IsType(binaryOperatorNode.Left); + var singleEntityFunctionCallSourceNode = Assert.IsType(singleEntityFunctionCallNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallSourceNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + Assert.Empty(singleEntityFunctionCallNode.Parameters); + } + + [Fact] + public void CanTranslate_SingleNavigationNode() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/SpecialOrder eq null"); + + // Assert + var binaryOperatorNode = Assert.IsType(translatedNode); + var singleNavigationNode = Assert.IsType(binaryOperatorNode.Left); + var singleEntityFunctionCallNode = Assert.IsType(singleNavigationNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_SingleResourceCastNode_ForComplex() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/SpecialAddress/Microsoft.AspNetCore.OData.Tests.Query.ParameterAliasAddress eq null"); + + // Assert + var binaryOperatorNode = Assert.IsType(translatedNode); + var singleValueCastNode = Assert.IsType(binaryOperatorNode.Left); + var singleComplexNode = Assert.IsType(singleValueCastNode.Source); + var singleEntityFunctionCallNode = Assert.IsType(singleComplexNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_SingleValueFunctionCallNode() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("null ne Default.SingleEntityFunctionCall(p1=@p)/Default.SingleValueFunctionCall(p1=@p)"); + + // Assert + var binaryOperatorNode = Assert.IsType(translatedNode); + var convertNode = Assert.IsType(binaryOperatorNode.Right); + var singleValueFunctionCallNode = Assert.IsType(convertNode.Source); + var singleEntityFunctionCallNode = Assert.IsType(singleValueFunctionCallNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + namedFunctionParameterNode = Assert.IsType(singleValueFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_SingleValueOpenPropertyAccessNode() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/OpenComplex/Unknown ne null"); + + // Assert + var binaryOperatorNode = Assert.IsType(translatedNode); + var singleValueOpenPropertyAccessNode = Assert.IsType(binaryOperatorNode.Left); + var singleComplexNode = Assert.IsType(singleValueOpenPropertyAccessNode.Source); + var singleEntityFunctionCallNode = Assert.IsType(singleComplexNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_SingleValuePropertyAccessNode() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/SpecialAddress ne null"); + + // Assert + var binaryOperatorNode = Assert.IsType(translatedNode); + var singleComplexNode = Assert.IsType(binaryOperatorNode.Left); + var singleEntityFunctionCallNode = Assert.IsType(singleComplexNode.Source); + var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); + Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); + } + + [Fact] + public void CanTranslate_UnaryOperatorNode() + { + // Arrange & Act + QueryNode translatedNode = TranslateFilterExpression("not(@p eq 123)"); + + // Assert + var unaryOperatorNode = Assert.IsType(translatedNode); + var binaryOperatorNode = Assert.IsType(unaryOperatorNode.Operand); + var convertNode = Assert.IsType(binaryOperatorNode.Left); + Assert.Same(_parameterAliasMappedNode, convertNode.Source); + } + + private QueryNode TranslateFilterExpression(string filter) + { + var parser = new ODataQueryOptionParser(_model, _customerEntityType, _customersEntitySet, + new Dictionary { { "$filter", filter } }); + FilterClause filterClause = parser.ParseFilter(); + var translator = new ParameterAliasNodeTranslator( + new Dictionary { { "@p", _parameterAliasMappedNode } }); + QueryNode translatedNode = filterClause.Expression.Accept(translator); + return translatedNode; + } + + private class ParameterAliasCustomer + { + public int ID { get; set; } + public ParameterAliasOpenComplexType OpenComplex { get; set; } + public ParameterAliasOrder SpecialOrder { get; set; } + public ParameterAliasAddress SpecialAddress { get; set; } + public IList Addresses { get; set; } + public IList Orders { get; set; } + } + + private class ParameterAliasOpenComplexType + { + public IDictionary DynamicProperties { get; set; } + } + + private class ParameterAliasOrder + { + public int ID { get; set; } + } + + private class ParameterAliasAddress { - private IEdmModel _model; - private IEdmEntitySet _customersEntitySet; - private IEdmEntityType _customerEntityType; - private SingleValueNode _parameterAliasMappedNode; - - public ParameterAliasNodeTranslatorTest() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.EntitySet("Orders"); - - builder.EntityType().Function("CollectionFunctionCall") - .ReturnsCollection().Parameter("p1"); - - builder.EntityType().Function("EntityCollectionFunctionCall") - .ReturnsCollectionFromEntitySet("Customers").Parameter("p1"); - - builder.EntityType().Function("SingleEntityFunctionCall") - .Returns().Parameter("p1"); - - builder.EntityType().Function("SingleEntityFunctionCallWithoutParameters") - .Returns(); - - builder.EntityType().Function("SingleValueFunctionCall") - .Returns().Parameter("p1"); - - _model = builder.GetEdmModel(); - _customersEntitySet = _model.FindDeclaredEntitySet("Customers"); - _customerEntityType = _customersEntitySet.EntityType; - _parameterAliasMappedNode = new ConstantNode(123); - } - - [Fact] - public void Constructor_Throws_NullParameterAliasNodes() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ParameterAliasNodeTranslator(null), "parameterAliasNodes"); - } - - [Fact] - public void ReturnsNull_ParameterAliasNotFound() - { - // Arrange - var translator = new ParameterAliasNodeTranslator(new Dictionary()); - var parameterAliasNode = new ParameterAliasNode("@unknown", null); - - // Act - QueryNode translatedNode = parameterAliasNode.Accept(translator); - - // Assert - var constantNode = Assert.IsType(translatedNode); - Assert.Null(constantNode.Value); - } - - [Fact] - public void CanTranslate_ParameterAliasNode() - { - // Arrange - var translator = new ParameterAliasNodeTranslator( - new Dictionary { { "@p", _parameterAliasMappedNode } }); - var parameterAliasNode = new ParameterAliasNode("@p", null); - - // Act - QueryNode translatedNode = parameterAliasNode.Accept(translator); - - // Assert - Assert.Same(_parameterAliasMappedNode, translatedNode); - } - - [Fact] - public void CanTranslate_AllNode() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Orders/all(order: order/ID eq @p)"); - - // Assert - var allNode = Assert.IsType(translatedNode); - var binaryOperatorNode = Assert.IsType(allNode.Body); - var convertNode = Assert.IsType(binaryOperatorNode.Right); - Assert.Same(_parameterAliasMappedNode, convertNode.Source); - } - - [Fact] - public void CanTranslate_AnyNode() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Orders/any(order: @p ne order/ID)"); - - // Assert - var anyNode = Assert.IsType(translatedNode); - var binaryOperatorNode = Assert.IsType(anyNode.Body); - var convertNode = Assert.IsType(binaryOperatorNode.Left); - Assert.Same(_parameterAliasMappedNode, convertNode.Source); - } - - [Fact] - public void CanTranslate_CollectionFunctionCallNode() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Default.CollectionFunctionCall(p1=@p)/all(r : r ne null)"); - - // Assert - var allNode = Assert.IsType(translatedNode); - var collectionFunctionCallNode = Assert.IsType(allNode.Source); - var singleEntityFunctionCallNode = Assert.IsType(collectionFunctionCallNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - namedFunctionParameterNode = Assert.IsType(collectionFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_CollectionNavigationNode() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Orders/all(order : order ne null)"); - - // Assert - var allNode = Assert.IsType(translatedNode); - var collectionNavigationNode = Assert.IsType(allNode.Source); - var singleEntityFunctionCallNode = Assert.IsType(collectionNavigationNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_CollectionOpenPropertyAccessNode() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/OpenComplex/CollectionProperty/all(p : p ne null)"); - - // Assert - var allNode = Assert.IsType(translatedNode); - var collectionOpenPropertyAccessNode = Assert.IsType(allNode.Source); - var singleComplexNode = Assert.IsType(collectionOpenPropertyAccessNode.Source); - var singleEntityFunctionCallNode = Assert.IsType(singleComplexNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_CollectionPropertyAccessNode() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Addresses/all(a : a ne null)"); - - // Assert - var allNode = Assert.IsType(translatedNode); - var collectionComplexNode = Assert.IsType(allNode.Source); - var singleEntityFunctionCallNode = Assert.IsType(collectionComplexNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_CollectionResourceCastNode_FroComplex() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Addresses/Microsoft.AspNetCore.OData.Tests.Query.ParameterAliasAddress/all(a : a ne null)"); - - // Assert - var allNode = Assert.IsType(translatedNode); - var collectionPropertyCastNode = Assert.IsType(allNode.Source); - var collectionComplexNode = Assert.IsType(collectionPropertyCastNode.Source); - var singleEntityFunctionCallNode = Assert.IsType(collectionComplexNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_CollectionResourceCastNode_ForEntity() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Orders/Microsoft.AspNetCore.OData.Tests.Query.ParameterAliasOrder/all(o : o ne null)"); - - // Assert - var allNode = Assert.IsType(translatedNode); - var entityCollectionCastNode = Assert.IsType(allNode.Source); - var collectionNavigationNode = Assert.IsType(entityCollectionCastNode.Source); - var singleEntityFunctionCallNode = Assert.IsType(collectionNavigationNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_CollectionResourceFunctionCallNode() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Default.EntityCollectionFunctionCall(p1=@p)/any(r : r eq null)"); - - // Assert - var anyNode = Assert.IsType(translatedNode); - var entityCollectionFunctionCallNode = Assert.IsType(anyNode.Source); - var singleEntityFunctionCallNode = Assert.IsType(entityCollectionFunctionCallNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - namedFunctionParameterNode = Assert.IsType(entityCollectionFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_SingleResourceCastNode_ForEntity() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Microsoft.AspNetCore.OData.Tests.Query.ParameterAliasCustomer eq null"); - - // Assert - var binaryOperatorNode = Assert.IsType(translatedNode); - var singleEntityCastNode = Assert.IsType(binaryOperatorNode.Left); - var singleEntityFunctionCallNode = Assert.IsType(singleEntityCastNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_SingleResourceFunctionCallNode() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Default.SingleEntityFunctionCall(p1=@p) eq null"); - - // Assert - var binaryOperatorNode = Assert.IsType(translatedNode); - var singleEntityFunctionCallNode = Assert.IsType(binaryOperatorNode.Left); - var singleEntityFunctionCallSourceNode = Assert.IsType(singleEntityFunctionCallNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallSourceNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_SingleResourceFunctionCallNodeWithoutParameters() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/Default.SingleEntityFunctionCallWithoutParameters() eq null"); - - // Assert - var binaryOperatorNode = Assert.IsType(translatedNode); - var singleEntityFunctionCallNode = Assert.IsType(binaryOperatorNode.Left); - var singleEntityFunctionCallSourceNode = Assert.IsType(singleEntityFunctionCallNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallSourceNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - Assert.Empty(singleEntityFunctionCallNode.Parameters); - } - - [Fact] - public void CanTranslate_SingleNavigationNode() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/SpecialOrder eq null"); - - // Assert - var binaryOperatorNode = Assert.IsType(translatedNode); - var singleNavigationNode = Assert.IsType(binaryOperatorNode.Left); - var singleEntityFunctionCallNode = Assert.IsType(singleNavigationNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_SingleResourceCastNode_ForComplex() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/SpecialAddress/Microsoft.AspNetCore.OData.Tests.Query.ParameterAliasAddress eq null"); - - // Assert - var binaryOperatorNode = Assert.IsType(translatedNode); - var singleValueCastNode = Assert.IsType(binaryOperatorNode.Left); - var singleComplexNode = Assert.IsType(singleValueCastNode.Source); - var singleEntityFunctionCallNode = Assert.IsType(singleComplexNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_SingleValueFunctionCallNode() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("null ne Default.SingleEntityFunctionCall(p1=@p)/Default.SingleValueFunctionCall(p1=@p)"); - - // Assert - var binaryOperatorNode = Assert.IsType(translatedNode); - var convertNode = Assert.IsType(binaryOperatorNode.Right); - var singleValueFunctionCallNode = Assert.IsType(convertNode.Source); - var singleEntityFunctionCallNode = Assert.IsType(singleValueFunctionCallNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - namedFunctionParameterNode = Assert.IsType(singleValueFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_SingleValueOpenPropertyAccessNode() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/OpenComplex/Unknown ne null"); - - // Assert - var binaryOperatorNode = Assert.IsType(translatedNode); - var singleValueOpenPropertyAccessNode = Assert.IsType(binaryOperatorNode.Left); - var singleComplexNode = Assert.IsType(singleValueOpenPropertyAccessNode.Source); - var singleEntityFunctionCallNode = Assert.IsType(singleComplexNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_SingleValuePropertyAccessNode() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("Default.SingleEntityFunctionCall(p1=@p)/SpecialAddress ne null"); - - // Assert - var binaryOperatorNode = Assert.IsType(translatedNode); - var singleComplexNode = Assert.IsType(binaryOperatorNode.Left); - var singleEntityFunctionCallNode = Assert.IsType(singleComplexNode.Source); - var namedFunctionParameterNode = Assert.IsType(singleEntityFunctionCallNode.Parameters.Single()); - Assert.Same(_parameterAliasMappedNode, namedFunctionParameterNode.Value); - } - - [Fact] - public void CanTranslate_UnaryOperatorNode() - { - // Arrange & Act - QueryNode translatedNode = TranslateFilterExpression("not(@p eq 123)"); - - // Assert - var unaryOperatorNode = Assert.IsType(translatedNode); - var binaryOperatorNode = Assert.IsType(unaryOperatorNode.Operand); - var convertNode = Assert.IsType(binaryOperatorNode.Left); - Assert.Same(_parameterAliasMappedNode, convertNode.Source); - } - - private QueryNode TranslateFilterExpression(string filter) - { - var parser = new ODataQueryOptionParser(_model, _customerEntityType, _customersEntitySet, - new Dictionary { { "$filter", filter } }); - FilterClause filterClause = parser.ParseFilter(); - var translator = new ParameterAliasNodeTranslator( - new Dictionary { { "@p", _parameterAliasMappedNode } }); - QueryNode translatedNode = filterClause.Expression.Accept(translator); - return translatedNode; - } - - private class ParameterAliasCustomer - { - public int ID { get; set; } - public ParameterAliasOpenComplexType OpenComplex { get; set; } - public ParameterAliasOrder SpecialOrder { get; set; } - public ParameterAliasAddress SpecialAddress { get; set; } - public IList Addresses { get; set; } - public IList Orders { get; set; } - } - - private class ParameterAliasOpenComplexType - { - public IDictionary DynamicProperties { get; set; } - } - - private class ParameterAliasOrder - { - public int ID { get; set; } - } - - private class ParameterAliasAddress - { - public string Street { get; set; } - } + public string Street { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ApplyQueryOptionTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ApplyQueryOptionTest.cs index 372c5e106..6fe777a0a 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ApplyQueryOptionTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ApplyQueryOptionTest.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Query.Wrapper; using Microsoft.AspNetCore.OData.Routing.Controllers; @@ -21,1593 +20,1566 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query -{ - public class ApplyQueryOptionTest - { - private static IEdmModel _model = GetEdmModel(); +namespace Microsoft.AspNetCore.OData.Tests.Query; - [Fact] - public void CtorApplyQueryOption_ThrowsArgumentNull_ForInputParameter() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new ApplyQueryOption(null, null, null), "rawValue"); - ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new ApplyQueryOption(string.Empty, null, null), "rawValue"); +public class ApplyQueryOptionTest +{ + private static IEdmModel _model = GetEdmModel(); - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ApplyQueryOption("groupby", null, null), "context"); + [Fact] + public void CtorApplyQueryOption_ThrowsArgumentNull_ForInputParameter() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new ApplyQueryOption(null, null, null), "rawValue"); + ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new ApplyQueryOption(string.Empty, null, null), "rawValue"); - // Arrange & Act & Assert - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - ExceptionAssert.ThrowsArgumentNull(() => new ApplyQueryOption("groupby", context, null), "queryOptionParser"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ApplyQueryOption("groupby", null, null), "context"); - [Fact] - public void ApplyToApplyQueryOption_ThrowsArgumentNull_ForInputParameter() - { - // Arrange - IEdmType type = EdmCoreModel.Instance.GetString(false).Definition; - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, type); - ApplyQueryOption apply = new ApplyQueryOption("groupby", context); - - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => apply.ApplyTo(null, null), "query"); - - // Arrange & Act & Assert - Mock queryable = new Mock(); - ExceptionAssert.ThrowsArgumentNull(() => apply.ApplyTo(queryable.Object, null), "querySettings"); - - // Arrange & Act & Assert - ExceptionAssert.Throws(() => apply.ApplyTo(queryable.Object, new ODataQuerySettings()), - "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); - - // Arrange & Act & Assert - context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - apply = new ApplyQueryOption("groupby", context); - queryable.Setup(q => q.Provider).Returns(new System.Data.Linq.MyQueryProvider()); - ExceptionAssert.Throws(() => apply.ApplyTo(queryable.Object, new ODataQuerySettings()), - "$apply query options not supported for LINQ to SQL providers."); - } - - // Legal apply queries usable against CustomerApplyTestData. - // Tuple is: apply, expected number - public static TheoryDataSet>> CustomerTestApplies - { - get - { - return new TheoryDataSet>> - { - { - "aggregate($count as Count)", - new List> - { - new Dictionary { { "Count", 5L} } - } - }, - { - "aggregate(Id with sum as Id)", - new List> - { - new Dictionary { { "Id", 15} } - } - }, - { - "aggregate(cast(Id, Edm.Int64) with sum as Id)", - new List> - { - new Dictionary { { "Id", 15L} } - } - }, - { - "aggregate(SharePrice with sum as SharePrice)", - new List> - { - new Dictionary { { "SharePrice", 22.5M} } - } - }, - { - "aggregate(SharePrice with min as SharePrice)", - new List> - { - new Dictionary { { "SharePrice", 2.5M} } - } - }, - { - "aggregate(SharePrice with max as SharePrice)", - new List> - { - new Dictionary { { "SharePrice", 10M} } - } - }, - { - "aggregate(SharePrice with average as SharePrice)", - new List> - { - new Dictionary { { "SharePrice", 7.5M} } - } - }, - { - "aggregate(Id with sum as Total, SharePrice with countdistinct as SharePriceDistinctCount)", - new List> - { - new Dictionary { { "SharePriceDistinctCount", 3L}, { "Total", 15} } - } - }, - { - "groupby((Name))", - new List> - { - new Dictionary { { "Name", "Lowest"} }, - new Dictionary { { "Name", "Highest"} }, - new Dictionary { { "Name", "Middle"} } - } - }, - { - "groupby((Name), aggregate(Id with sum as Total))", - new List> - { - new Dictionary { { "Name", "Lowest"}, { "Total", 10} }, - new Dictionary { { "Name", "Highest"}, { "Total", 2} }, - new Dictionary { { "Name", "Middle"}, { "Total", 3 } } - } - }, - { - "groupby((Name), aggregate($count as Count))", - new List> - { - new Dictionary { { "Name", "Lowest"}, { "Count", 3L} }, - new Dictionary { { "Name", "Highest"}, { "Count", 1L} }, - new Dictionary { { "Name", "Middle"}, { "Count", 1L} } - } - }, - { - "filter(Name eq 'Lowest')/groupby((Name))", - new List> - { - new Dictionary { { "Name", "Lowest"} } - } - }, - /* TODO: Sam XU enable this test case when we refactor aggregationBinder with FilterBinder - { - "groupby((Name), aggregate(Id with sum as Total))/filter(Total eq 3)", - new List> - { - new Dictionary { { "Name", "Middle"}, { "Total", 3 } } - } - }, - { - "groupby((Name))/filter(Name eq 'Lowest')", - new List> - { - new Dictionary { { "Name", "Lowest"} } - } - }, - */ - { - "groupby((Address/City))", - new List> - { - new Dictionary { { "Address/City", "redmond"} }, - new Dictionary { { "Address/City", "seattle"} }, - new Dictionary { { "Address/City", "hobart"} }, - new Dictionary { { "Address/City", null} }, - } - }, - { - "groupby((Address/City, Address/State))", - new List> - { - new Dictionary { { "Address/City", "redmond"}, { "Address/State", "WA"} }, - new Dictionary { { "Address/City", "seattle"}, { "Address/State", "WA"} }, - new Dictionary { { "Address/City", "hobart"}, { "Address/State", null} }, - new Dictionary { { "Address/City", null}, { "Address/State", null} }, - } - }, - { - "groupby((Address/City, Address/State))/groupby((Address/State), aggregate(Address/City with max as MaxCity))", - new List> - { - new Dictionary { { "MaxCity", "seattle"}, { "Address/State", "WA"} }, - new Dictionary { { "MaxCity", "hobart"}, { "Address/State", null} }, - } - }, - { - "groupby((Address/State), aggregate(Address/City with max as MaxCity))", - new List> - { - new Dictionary { { "MaxCity", "seattle"}, { "Address/State", "WA"} }, - new Dictionary { { "MaxCity", "hobart"}, { "Address/State", null} }, - } - }, - { - "groupby((Address/State), aggregate(startswith(Address/City, 's') with max as MaxCity))", - new List> - { - new Dictionary { { "MaxCity", true}, { "Address/State", "WA"} }, - new Dictionary { { "MaxCity", false}, { "Address/State", null} }, - } - }, - { - "groupby((Address/State), aggregate(endswith(Address/City, 't') with max as MaxCity))", - new List> - { - new Dictionary { { "MaxCity", false}, { "Address/State", "WA"} }, - new Dictionary { { "MaxCity", true}, { "Address/State", null} }, - } - }, - { - "groupby((Address/State), aggregate(contains(Address/City, 'o') with max as MaxCity))", - new List> - { - new Dictionary { { "MaxCity", true}, { "Address/State", "WA"} }, - new Dictionary { { "MaxCity", true}, { "Address/State", null} }, - } - }, - { - "groupby((Address/State), aggregate(length(Address/City) with max as MaxCity))", - new List> - { - new Dictionary { { "MaxCity", 7}, { "Address/State", "WA"} }, - new Dictionary { { "MaxCity", 6}, { "Address/State", null} }, - } - }, - { - "aggregate(year(StartDate) with max as MaxYear, year(StartDate) with min as MinYear)", - new List> - { - new Dictionary { { "MaxYear", 2018}, { "MinYear", 2016} }, - } - }, - { - "aggregate(month(StartDate) with max as MaxMonth, month(StartDate) with min as MinMonth)", - new List> - { - new Dictionary { { "MaxMonth", 5}, { "MinMonth", 1} }, - } - }, - { - "aggregate(day(StartDate) with max as MaxDay, day(StartDate) with min as MinDay)", - new List> - { - new Dictionary { { "MaxDay", 7}, { "MinDay", 1} }, - } - }, - { - "aggregate(hour(StartDate) with max as MaxHour, hour(StartDate) with min as MinHour)", - new List> - { - new Dictionary { { "MaxHour", 5 }, { "MinHour", 1} }, - } - }, - { - "aggregate(minute(StartDate) with max as MaxMinute, minute(StartDate) with min as MinMinute)", - new List> - { - new Dictionary { { "MaxMinute", 6}, { "MinMinute", 2} }, - } - }, - { - "aggregate(second(StartDate) with max as MaxSecond, second(StartDate) with min as MinSecond)", - new List> - { - new Dictionary { { "MaxSecond", 7}, { "MinSecond", 3} }, - } - }, - { - "groupby((Address/State), aggregate(concat(Address/City,Address/State) with max as MaxCity))", - new List> - { - new Dictionary { { "MaxCity", "seattleWA"}, { "Address/State", "WA"} }, - new Dictionary { { "MaxCity", null}, { "Address/State", null} }, - } - }, - { - "groupby((Address/State), aggregate(Address/City with max as MaxCity, Address/City with min as MinCity))", - new List> - { - new Dictionary { { "MaxCity", "seattle"}, { "MinCity", "redmond"}, { "Address/State", "WA"} }, - new Dictionary { { "MaxCity", "hobart"}, { "MinCity", "hobart" }, { "Address/State", null} }, - } - }, - { - "groupby((Address/State), aggregate(Address/City with max as MaxCity, Id mul Id with sum as Id))", - new List> - { - new Dictionary { { "MaxCity", "seattle"}, { "Id", 30}, { "Address/State", "WA"} }, - new Dictionary { { "MaxCity", "hobart"}, { "Id", 25 }, { "Address/State", null} }, - } - }, - { - "groupby((Address/State), aggregate(Id mul Id with sum as Id))", - new List> - { - new Dictionary { { "Id", 30}, { "Address/State", "WA"} }, - new Dictionary { { "Id", 25}, { "Address/State", null} }, - } - }, - { - "filter(Company/CEO/EmployeeName eq 'john')/groupby((Company/CEO/EmployeeName))", - new List> - { - new Dictionary {{ "Company/CEO/EmployeeName", "john"} } - } - }, - /* TODO: Sam XU enable this test case when we refactor aggregationBinder with FilterBinder - { - "groupby((Company/CEO/EmployeeName))/filter(Company/CEO/EmployeeName eq 'john')", - new List> - { - new Dictionary {{ "Company/CEO/EmployeeName", "john"} } - } - }, - */ - { - "groupby((Name, Company/CEO/EmployeeName))", - new List> - { - new Dictionary { { "Name", "Lowest"}, { "Company/CEO/EmployeeName", "john" } }, - new Dictionary { { "Name", "Highest"}, { "Company/CEO/EmployeeName", "tom" } }, - new Dictionary { { "Name", "Middle"}, { "Company/CEO/EmployeeName", "john" } }, - new Dictionary { { "Name", "Lowest"}, { "Company/CEO/EmployeeName", "alex" } }, - new Dictionary { { "Name", "Lowest"}, { "Company/CEO/EmployeeName", null } } - } - }, - { - "groupby((Address/City, Company/CEO/EmployeeName))", - new List> - { - new Dictionary { { "Address/City", "redmond"}, { "Company/CEO/EmployeeName", "john" } }, - new Dictionary { { "Address/City", "seattle"}, { "Company/CEO/EmployeeName", "tom" } }, - new Dictionary { { "Address/City", "hobart"}, { "Company/CEO/EmployeeName", "john" } }, - new Dictionary { { "Address/City", null}, { "Company/CEO/EmployeeName", "alex" } }, - new Dictionary { { "Address/City", "redmond"}, { "Company/CEO/EmployeeName", null } } - } - }, - { - "groupby((Company/CEO/HomeAddress/City))", - new List> - { - new Dictionary { { "Company/CEO/HomeAddress/City", "redmond"} }, - new Dictionary { { "Company/CEO/HomeAddress/City", "seattle"} }, - new Dictionary { { "Company/CEO/HomeAddress/City", "hobart"} }, - new Dictionary { { "Company/CEO/HomeAddress/City", null} }, - } - }, - { - "groupby((Company/CEO/HomeAddress/City, Company/CEO/HomeAddress/State))", - new List> - { - new Dictionary { { "Company/CEO/HomeAddress/City", "redmond"}, { "Company/CEO/HomeAddress/State", "WA"} }, - new Dictionary { { "Company/CEO/HomeAddress/City", "seattle"}, { "Company/CEO/HomeAddress/State", "WA"} }, - new Dictionary { { "Company/CEO/HomeAddress/City", "hobart"}, { "Company/CEO/HomeAddress/State", null} }, - new Dictionary { { "Company/CEO/HomeAddress/City", null}, { "Company/CEO/HomeAddress/State", null} }, - } - }, - { - "groupby((Company/CEO/HomeAddress/City, Company/CEO/HomeAddress/State))/groupby((Company/CEO/HomeAddress/State), aggregate(Company/CEO/HomeAddress/City with max as MaxCity))", - new List> - { - new Dictionary { { "MaxCity", "seattle"}, { "Company/CEO/HomeAddress/State", "WA"} }, - new Dictionary { { "MaxCity", "hobart"}, { "Company/CEO/HomeAddress/State", null} }, - } - }, - { - "groupby((Company/CEO/EmployeeName))", - new List> - { - new Dictionary {{ "Company/CEO/EmployeeName", "john"} }, - new Dictionary {{ "Company/CEO/EmployeeName", "tom"} }, - new Dictionary {{ "Company/CEO/EmployeeName", "alex"} }, - new Dictionary {{ "Company/CEO/EmployeeName", null} } - } - }, - { - "groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))", - new List> - { - new Dictionary {{ "Company/CEO/EmployeeName", "john"}, { "Company/CEO/BaseSalary", 20M} }, - new Dictionary {{ "Company/CEO/EmployeeName", "tom"}, { "Company/CEO/BaseSalary", 20M} }, - new Dictionary {{ "Company/CEO/EmployeeName", "alex"}, { "Company/CEO/BaseSalary", 0M} }, - new Dictionary {{ "Company/CEO/EmployeeName", null}, { "Company/CEO/BaseSalary", null} } - } - }, - { - "groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))/groupby((Company/CEO/BaseSalary), aggregate(Company/CEO/EmployeeName with max as MaxEmployeeName))", - new List> - { - new Dictionary {{ "MaxEmployeeName", "tom"}, { "Company/CEO/BaseSalary", 20M} }, - new Dictionary {{ "MaxEmployeeName", "alex"}, { "Company/CEO/BaseSalary", 0M} }, - new Dictionary {{ "MaxEmployeeName", null}, { "Company/CEO/BaseSalary", null} } - } - }, - { - "groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))/groupby((Company/CEO/EmployeeName), aggregate(Company/CEO/BaseSalary with average as AverageBaseSalary))", - new List> - { - new Dictionary {{ "AverageBaseSalary", 20M }, { "Company/CEO/EmployeeName", "john"} }, - new Dictionary {{ "AverageBaseSalary", 20M }, { "Company/CEO/EmployeeName", "tom"} }, - new Dictionary {{ "AverageBaseSalary", 0M }, { "Company/CEO/EmployeeName", "alex"} }, - new Dictionary {{ "AverageBaseSalary", null }, { "Company/CEO/EmployeeName", null} } - } - }, - { - "aggregate(Id mul Id with sum as Id)", - new List> - { - new Dictionary { { "Id", 55} } - } - }, - { - // Note SharePrice and Id have different type - "aggregate(SharePrice mul Id with sum as Result)", - new List> - { - new Dictionary { { "Result", 65.0M} } - } - }, - { - "groupby((Website))", - new List> - { - new Dictionary { { "Website", null} }, - } - }, - { - "aggregate(IntProp with max as MaxIntProp)", - new List> - { - new Dictionary { { "MaxIntProp", 2} } - } - }, - { - "aggregate(IntProp with min as MinIntProp)", - new List> - { - new Dictionary { { "MinIntProp", 1} } - } - }, - { - "aggregate(IntProp with countdistinct as DistinctIntProp)", - new List> - { - new Dictionary { { "DistinctIntProp", 3L} } - } - }, - { - "aggregate(IntProp with sum as TotalIntProp)", - new List> - { - new Dictionary { { "TotalIntProp", 3M} } - } - }, - { - "aggregate(IntProp with average as TotalIntProp)", - new List> - { - new Dictionary { { "TotalIntProp", 1.5M} } - } - }, - { - "aggregate(MixedProp with sum as TotalMixedProp)", - new List> - { - new Dictionary { { "TotalMixedProp", 1M} } - } - }, - { - "groupby((StringProp), aggregate(IntProp with min as MinIntProp))", - new List> - { - new Dictionary { { "StringProp", "Test1" }, { "MinIntProp", 1} }, - new Dictionary { { "StringProp", "Test2" }, { "MinIntProp", 2} }, - new Dictionary { { "StringProp", "Test3" }, { "MinIntProp", null} }, - new Dictionary { { "StringProp", null }, { "MinIntProp", null} }, - } - }, - { - "groupby((StringProp), aggregate(IntProp with min as MinIntProp))/groupby((StringProp))", - new List> - { - new Dictionary { { "StringProp", "Test1" } }, - new Dictionary { { "StringProp", "Test2" } }, - new Dictionary { { "StringProp", "Test3" } }, - new Dictionary { { "StringProp", null } }, - } - }, - { - "aggregate($count as Count)/compute(Count add Count as DoubleCount)", - new List> - { - new Dictionary { { "Count", 5L}, { "DoubleCount", 10L } } - } - }, - /* TODO: Sam XU enable this test case when we refactor aggregationBinder with FilterBinder - { - "groupby((Name), aggregate(Id with sum as Total))/compute(Total add Total as DoubleTotal, length(Name) as NameLen)", - new List> - { - new Dictionary { { "Name", "Lowest"}, { "Total", 10}, { "DoubleTotal", 20}, { "NameLen", 6},}, - new Dictionary { { "Name", "Highest"}, { "Total", 2} , { "DoubleTotal", 4} , { "NameLen", 7} ,}, - new Dictionary { { "Name", "Middle"}, { "Total", 3 }, { "DoubleTotal", 6 }, { "NameLen", 6 }, } - } - }, - */ - { - "compute(length(Name) as NameLen)", - new List> - { - new Dictionary { { "Name", "Lowest" }, { "NameLen", 6}, { "Id", 1},}, - new Dictionary { { "Name", "Highest"}, { "NameLen", 7}, { "Id", 2},}, - new Dictionary { { "Name", "Middle" }, { "NameLen", 6}, { "Id", 3},}, - new Dictionary { { "Name", "Lowest" }, { "NameLen", 6}, { "Id", 4},}, - new Dictionary { { "Name", "Lowest" }, { "NameLen", 6}, { "Id", 5},}, - } - }, - { - "compute(length(ShareSymbol) as NameLen)", - new List> - { - new Dictionary { { "Name", "Lowest" }, { "NameLen", null}, { "Id", 1},}, - new Dictionary { { "Name", "Highest"}, { "NameLen", null}, { "Id", 2},}, - new Dictionary { { "Name", "Middle" }, { "NameLen", null}, { "Id", 3},}, - new Dictionary { { "Name", "Lowest" }, { "NameLen", null}, { "Id", 4},}, - new Dictionary { { "Name", "Lowest" }, { "NameLen", null}, { "Id", 5},}, - } - }, - { - "compute(length(Name) as NameLen)/aggregate(NameLen with sum as TotalLen)", - new List> - { - new Dictionary { { "TotalLen", 31} } - } - }, - { - "compute(length(Name) as NameLen)/aggregate(NameLen add Id with sum as TotalLen)", - new List> - { - new Dictionary { { "TotalLen", 46} } - } - }, - { - "compute(length(Name) as NameLen)/groupby((Name),aggregate(Id with sum as Total, NameLen with max as MaxNameLen))", - new List> - { - new Dictionary { { "Name", "Lowest"}, { "Total", 10}, { "MaxNameLen", 6},}, - new Dictionary { { "Name", "Highest"}, { "Total", 2} , { "MaxNameLen", 7} ,}, - new Dictionary { { "Name", "Middle"}, { "Total", 3 }, { "MaxNameLen", 6 }, } - } - }, - { - "compute(length(Name) as NameLen)/groupby((NameLen),aggregate(Id with sum as Total))", - new List> - { - new Dictionary { { "Total", 13}, { "NameLen", 6},}, - new Dictionary { { "Total", 2} , { "NameLen", 7} ,}, - } - }, - { - "groupby((Address/State), aggregate(Address/City with max as MaxCity, Address/City with min as MinCity))/compute(length(MaxCity) as MaxCityLen)", - new List> - { - new Dictionary { { "MaxCity", "seattle"}, { "MinCity", "redmond"}, { "Address/State", "WA"}, {"MaxCityLen", 7 } }, - new Dictionary { { "MaxCity", "hobart"}, { "MinCity", "hobart" }, { "Address/State", null}, {"MaxCityLen", 6 } }, - } - }, - { - "compute(length(Address/City) as CityLength)/groupby((Address/State), aggregate(Address/City with max as MaxCity, Address/City with min as MinCity, CityLength with max as MaxCityLen))", - new List> - { - new Dictionary { { "MaxCity", "seattle"}, { "MinCity", "redmond"}, { "Address/State", "WA"}, {"MaxCityLen", 7 } }, - new Dictionary { { "MaxCity", "hobart"}, { "MinCity", "hobart" }, { "Address/State", null}, {"MaxCityLen", 6 } }, - } - }, - }; - } - } + // Arrange & Act & Assert + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + ExceptionAssert.ThrowsArgumentNull(() => new ApplyQueryOption("groupby", context, null), "queryOptionParser"); + } - public static TheoryDataSet>> CustomerTestAppliesMixedWithOthers - { - get - { - return new TheoryDataSet>> - { - { - "$apply=groupby((Name), aggregate(Id with sum as Total))&$filter=Total eq 3", - new List> - { - new Dictionary {{"Name", "Middle"}, {"Total", 3}} - } - }, - { - "$apply=groupby((Name), aggregate(Id with sum as Total))&$orderby=Name", - new List> - { - new Dictionary {{"Name", "Highest"}, {"Total", 2}}, - new Dictionary {{"Name", "Lowest"}, {"Total", 10}}, - new Dictionary {{"Name", "Middle"}, {"Total", 3}}, - } - }, - { - "$apply=groupby((Name))&$orderby=Name", - new List> - { - new Dictionary {{"Name", "Highest"}}, - new Dictionary {{"Name", "Lowest"}}, - new Dictionary {{"Name", "Middle"}}, - } - }, - { - "$apply=groupby((Name), aggregate(Id with sum as Total, Id with sum as Total2))&$orderby=Total", - new List> - { - new Dictionary {{"Name", "Highest"}, {"Total", 2}}, - new Dictionary {{"Name", "Middle"}, {"Total", 3}}, - new Dictionary {{"Name", "Lowest"}, {"Total", 10}}, - } - }, - { - "$apply=groupby((Name), aggregate(Id with sum as Total, Id with sum as Total2))&$orderby=Total, Total2", - new List> - { - new Dictionary {{"Name", "Highest"}, {"Total", 2}}, - new Dictionary {{"Name", "Middle"}, {"Total", 3}}, - new Dictionary {{"Name", "Lowest"}, {"Total", 10}}, - } - }, - { - "$apply=groupby((Name), aggregate(Id with sum as Total))&$orderby=Name, Total", - new List> - { - new Dictionary {{"Name", "Highest"}, {"Total", 2}}, - new Dictionary {{"Name", "Lowest"}, {"Total", 10}}, - new Dictionary {{"Name", "Middle"}, {"Total", 3}}, - } - }, - { - "$apply=groupby((Address/City))&$orderby=Address/City", - new List> - { - new Dictionary {{"Address/City", null}}, - new Dictionary {{"Address/City", "hobart"}}, - new Dictionary {{"Address/City", "redmond"}}, - new Dictionary {{"Address/City", "seattle"}}, - } - }, - { - "$apply=groupby((Address/City))&$filter=Address/City eq 'redmond'&$orderby=Address/City", - new List> - { - new Dictionary {{"Address/City", "redmond"}}, - } - }, - { - "$apply=groupby((Address/City, Address/State))&$filter=Address/State eq 'WA'&$orderby=Address/City", - new List> - { - new Dictionary {{"Address/City", "redmond"}, {"Address/State", "WA"}}, - new Dictionary {{"Address/City", "seattle"}, {"Address/State", "WA"}}, - } - }, - { - "$apply=groupby((Address/City, Address/State))&$orderby=Address/State desc, Address/City", - new List> - { - new Dictionary {{"Address/City", "redmond"}, {"Address/State", "WA"}}, - new Dictionary {{"Address/City", "seattle"}, {"Address/State", "WA"}}, - new Dictionary {{"Address/City", null}, {"Address/State", null}}, - new Dictionary {{"Address/City", "hobart"}, {"Address/State", null}}, - } - }, - { - "$apply=groupby((Address/City))&$filter=Address/City eq 'redmond'", - new List> - { - new Dictionary {{"Address/City", "redmond"}}, - } - }, - { - "$apply=groupby((Company/CEO/HomeAddress/City))&$orderby=Company/CEO/HomeAddress/City", - new List> - { - new Dictionary {{"Company/CEO/HomeAddress/City", null}}, - new Dictionary {{"Company/CEO/HomeAddress/City", "hobart"}}, - new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}}, - new Dictionary {{"Company/CEO/HomeAddress/City", "seattle"}}, - } - }, - { - "$apply=groupby((Company/CEO/HomeAddress/City))&$filter=Company/CEO/HomeAddress/City eq 'redmond'&$orderby=Company/CEO/HomeAddress/City", - new List> - { - new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}}, - } - }, - { - "$apply=groupby((Company/CEO/HomeAddress/City, Company/CEO/HomeAddress/State))&$filter=Company/CEO/HomeAddress/State eq 'WA'&$orderby=Company/CEO/HomeAddress/City", - new List> - { - new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}, {"Company/CEO/HomeAddress/State", "WA"}}, - new Dictionary {{"Company/CEO/HomeAddress/City", "seattle"}, {"Company/CEO/HomeAddress/State", "WA"}}, - } - }, - { - "$apply=groupby((Company/CEO/HomeAddress/City, Company/CEO/HomeAddress/State))&$orderby=Company/CEO/HomeAddress/State desc, Company/CEO/HomeAddress/City", - new List> - { - new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}, {"Company/CEO/HomeAddress/State", "WA"}}, - new Dictionary {{"Company/CEO/HomeAddress/City", "seattle"}, {"Company/CEO/HomeAddress/State", "WA"}}, - new Dictionary {{"Company/CEO/HomeAddress/City", null}, {"Company/CEO/HomeAddress/State", null}}, - new Dictionary {{"Company/CEO/HomeAddress/City", "hobart"}, {"Company/CEO/HomeAddress/State", null}}, - } - }, - { - "$apply=groupby((Company/CEO/HomeAddress/City))&$filter=Company/CEO/HomeAddress/City eq 'redmond'", - new List> - { - new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}}, - } - }, - { - "$apply=groupby((Company/CEO/EmployeeName))&$orderby=Company/CEO/EmployeeName", - new List> - { - new Dictionary {{ "Company/CEO/EmployeeName", null} }, - new Dictionary {{ "Company/CEO/EmployeeName", "alex"} }, - new Dictionary {{ "Company/CEO/EmployeeName", "john"} }, - new Dictionary {{ "Company/CEO/EmployeeName", "tom"} } - } - }, - { - "$apply=groupby((Company/CEO/EmployeeName))&$filter=Company/CEO/EmployeeName eq 'alex'&$orderby=Company/CEO/EmployeeName", - new List> - { - new Dictionary {{"Company/CEO/EmployeeName", "alex"}}, - } - }, - { - "$apply=groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))&$filter= Company/CEO/BaseSalary eq 20&$orderby=Company/CEO/EmployeeName", - new List> - { - new Dictionary {{ "Company/CEO/EmployeeName", "john"}, { "Company/CEO/BaseSalary", 20M} }, - new Dictionary {{ "Company/CEO/EmployeeName", "tom"}, { "Company/CEO/BaseSalary", 20M} } - } - }, - { - "$apply=groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))&$orderby=Company/CEO/BaseSalary desc, Company/CEO/EmployeeName desc", - new List> - { - new Dictionary {{ "Company/CEO/EmployeeName", "tom"}, { "Company/CEO/BaseSalary", 20M} }, - new Dictionary {{ "Company/CEO/EmployeeName", "john"}, { "Company/CEO/BaseSalary", 20M} }, - new Dictionary {{ "Company/CEO/EmployeeName", "alex"}, { "Company/CEO/BaseSalary", 0M} }, - new Dictionary {{ "Company/CEO/EmployeeName", null}, { "Company/CEO/BaseSalary", null} } - } - }, - { - "$apply=groupby((Company/CEO/EmployeeName))&$filter=Company/CEO/EmployeeName eq 'john'", - new List> - { - new Dictionary {{"Company/CEO/EmployeeName", "john"}}, - } - }, - { - "$apply=groupby((Name), aggregate(Id with sum as Total))/compute(Total mul 2 as NewTotal)&$orderby=Name, NewTotal", - new List> - { - new Dictionary {{"Name", "Highest"}, {"Total", 2}, {"NewTotal", 4}, }, - new Dictionary {{"Name", "Lowest"}, {"Total", 10}, {"NewTotal", 20},}, - new Dictionary {{"Name", "Middle"}, {"Total", 3}, {"NewTotal", 6}, }, - } - }, - { - "$apply=groupby((Name), aggregate(Id with sum as Total))/compute(Total mul 2 as NewTotal)/filter(NewTotal gt 6)&$orderby=Name, NewTotal", - new List> - { - new Dictionary {{"Name", "Lowest"}, {"Total", 10}, {"NewTotal", 20},}, - } - }, - //{ - // "$apply=groupby((Name))&$top=1", - // new List> - // { - // new Dictionary {{"Name", "Highest"}}, - // } - //}, - //{ - // "$apply=groupby((Name))&$skip=1", - // new List> - // { - // new Dictionary {{"Name", "Lowest"}}, - // new Dictionary {{"Name", "Middle"}}, - // } - //}, - }; - } - } + [Fact] + public void ApplyToApplyQueryOption_ThrowsArgumentNull_ForInputParameter() + { + // Arrange + IEdmType type = EdmCoreModel.Instance.GetString(false).Definition; + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, type); + ApplyQueryOption apply = new ApplyQueryOption("groupby", context); + + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => apply.ApplyTo(null, null), "query"); + + // Arrange & Act & Assert + Mock queryable = new Mock(); + ExceptionAssert.ThrowsArgumentNull(() => apply.ApplyTo(queryable.Object, null), "querySettings"); + + // Arrange & Act & Assert + ExceptionAssert.Throws(() => apply.ApplyTo(queryable.Object, new ODataQuerySettings()), + "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); + + // Arrange & Act & Assert + context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + apply = new ApplyQueryOption("groupby", context); + queryable.Setup(q => q.Provider).Returns(new System.Data.Linq.MyQueryProvider()); + ExceptionAssert.Throws(() => apply.ApplyTo(queryable.Object, new ODataQuerySettings()), + "$apply query options not supported for LINQ to SQL providers."); + } - public static TheoryDataSet>> CustomerTestAppliesForPaging + // Legal apply queries usable against CustomerApplyTestData. + // Tuple is: apply, expected number + public static TheoryDataSet>> CustomerTestApplies + { + get { - get + return new TheoryDataSet>> { - return new TheoryDataSet>> - { - { - "$apply=aggregate(Id with sum as Id)", - new List> - { - new Dictionary { { "Id", 15} } - } - }, - { - "$apply=aggregate(Id with sum as Total)", - new List> - { - new Dictionary { { "Total", 15} } - } - }, - { - "$apply=groupby((Name), aggregate(Id with sum as Total, Id with sum as Total2))", - new List> - { - new Dictionary {{"Name", "Highest"}, {"Total", 2}}, - new Dictionary {{"Name", "Lowest"}, {"Total", 10}}, - } - }, - { - "$apply=groupby((Name), aggregate(Id with sum as Total, Id with sum as Total2))&$skip=2", - new List> - { - new Dictionary {{"Name", "Middle"}, {"Total", 3}}, - } - }, - { - "$apply=groupby((Name))", - new List> - { - new Dictionary {{"Name", "Highest"}}, - new Dictionary {{"Name", "Lowest"}}, - } - }, - { - "$apply=groupby((Name))&$skip=2", - new List> - { - new Dictionary {{"Name", "Middle"}}, - } - }, - { - "$apply=groupby((Id, Name))", - new List> - { - new Dictionary {{"Name", "Lowest"}, { "Id", 1}}, - new Dictionary {{"Name", "Highest"}, { "Id", 2}}, - } - }, - { - "$apply=groupby((Name, Id))", - new List> - { - new Dictionary {{"Name", "Highest" }, { "Id", 2}}, - new Dictionary {{"Name", "Lowest" }, { "Id", 1}}, - } - }, - { - "$apply=groupby((Name, Id))&$skip=2", - new List> - { - new Dictionary {{"Name", "Lowest" }, { "Id", 4}}, - new Dictionary {{"Name", "Lowest" }, { "Id", 5}}, - } - }, - { - "$apply=groupby((Name), aggregate(Id with sum as Total))", - new List> - { - new Dictionary {{"Name", "Highest"}, {"Total", 2}}, - new Dictionary {{"Name", "Lowest"}, {"Total", 10}}, - } - }, - { - "$apply=groupby((Name), aggregate(Id with sum as Total))&$skip=2", - new List> - { - new Dictionary {{"Name", "Middle"}, {"Total", 3}}, - } - }, - { - "$apply=groupby((Address/City))", - new List> - { - new Dictionary {{"Address/City", null}}, - new Dictionary {{"Address/City", "hobart"}}, - } - }, - { - "$apply=groupby((Address/City))&$skip=2", - new List> - { - new Dictionary {{"Address/City", "redmond"}}, - new Dictionary {{"Address/City", "seattle"}}, - } - }, - { - "$apply=groupby((Address/City, Address/State))", - new List> - { - new Dictionary {{"Address/City", null}, {"Address/State", null}}, - new Dictionary {{"Address/City", "hobart"}, {"Address/State", null}}, - } - }, - { - "$apply=groupby((Address/City, Address/State))&$skip=2", - new List> - { - new Dictionary {{"Address/City", "redmond"}, {"Address/State", "WA"}}, - new Dictionary {{"Address/City", "seattle"}, {"Address/State", "WA"}}, - } - }, - { - "$apply=groupby((Company/CEO/HomeAddress/City))", - new List> - { - new Dictionary {{"Company/CEO/HomeAddress/City", null}}, - new Dictionary {{"Company/CEO/HomeAddress/City", "hobart"}}, - } - }, - { - "$apply=groupby((Company/CEO/HomeAddress/City))&$skip=2", - new List> - { - new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}}, - new Dictionary {{"Company/CEO/HomeAddress/City", "seattle"}}, - } - }, - { - "$apply=groupby((Company/CEO/HomeAddress/City, Company/CEO/HomeAddress/State))", - new List> - { - new Dictionary {{"Company/CEO/HomeAddress/City", null}, {"Company/CEO/HomeAddress/State", null}}, - new Dictionary {{"Company/CEO/HomeAddress/City", "hobart"}, {"Company/CEO/HomeAddress/State", null}}, - } - }, - { - "$apply=groupby((Company/CEO/HomeAddress/City, Company/CEO/HomeAddress/State))&$skip=2", - new List> - { - new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}, {"Company/CEO/HomeAddress/State", "WA"}}, - new Dictionary {{"Company/CEO/HomeAddress/City", "seattle"}, {"Company/CEO/HomeAddress/State", "WA"}}, - } - }, - { - "$apply=groupby((Company/CEO/EmployeeName))", - new List> - { - new Dictionary {{ "Company/CEO/EmployeeName", null} }, - new Dictionary {{ "Company/CEO/EmployeeName", "alex"} } - } - }, - { - "$apply=groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))", - new List> - { - new Dictionary {{ "Company/CEO/EmployeeName", null}, { "Company/CEO/BaseSalary", null} }, - new Dictionary {{ "Company/CEO/EmployeeName", "alex"}, { "Company/CEO/BaseSalary", 0M} } - } - }, - { - "$apply=groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))&$skip=2", - new List> - { - new Dictionary {{ "Company/CEO/EmployeeName", "john"}, { "Company/CEO/BaseSalary", 20M} }, - new Dictionary {{ "Company/CEO/EmployeeName", "tom"}, { "Company/CEO/BaseSalary", 20M} } - } - }, - { - "$apply=groupby((Name))&$orderby=Name", - new List> - { - new Dictionary {{"Name", "Highest"}}, - new Dictionary {{"Name", "Lowest"}}, - } - }, - { - "$apply=groupby((Name))&$skip=2&$orderby=Name", - new List> - { - new Dictionary {{"Name", "Middle"}}, - } - }, - { - "$apply=groupby((Name))&$orderby=Name desc", - new List> - { - new Dictionary {{"Name", "Middle"}}, - new Dictionary {{"Name", "Lowest"}}, - } - }, - { - "$apply=groupby((Name))&$skip=2&$orderby=Name desc", - new List> - { - new Dictionary {{"Name", "Highest"}}, - } - }, - { - "$apply=groupby((Id, Name))&$orderby=Id", - new List> - { - new Dictionary {{"Name", "Lowest"}, { "Id", 1}}, - new Dictionary {{"Name", "Highest"}, { "Id", 2}}, - } - }, - { - "$apply=groupby((Id, Name))&$orderby=Name", - new List> - { - new Dictionary {{"Name", "Highest"}, { "Id", 2}}, - new Dictionary {{"Name", "Lowest"}, { "Id", 1}}, - } - }, - { - "$apply=groupby((Id, Name))&$orderby=Name&$skip=2", - new List> - { - new Dictionary {{"Name", "Lowest"}, { "Id", 4}}, - new Dictionary {{"Name", "Lowest"}, { "Id", 5}}, - } - }, - { - "$apply=groupby((Address/City))&$orderby=Address/City", - new List> - { - new Dictionary {{"Address/City", null}}, - new Dictionary {{"Address/City", "hobart"}}, - } - }, - { - "$apply=groupby((Address/City, Address/State))&$orderby=Address/State", - new List> - { - new Dictionary {{"Address/City", null}, {"Address/State", null}}, - new Dictionary {{"Address/City", "hobart"}, {"Address/State", null}}, - } - }, - { - "$apply=compute(0 as ComputeProperty)/groupby((ComputeProperty))&$orderby=ComputeProperty desc", - new List> - { - new Dictionary {{ "ComputeProperty", 0}}, - } - }, - }; - } + { + "aggregate($count as Count)", + new List> + { + new Dictionary { { "Count", 5L} } + } + }, + { + "aggregate(Id with sum as Id)", + new List> + { + new Dictionary { { "Id", 15} } + } + }, + { + "aggregate(cast(Id, Edm.Int64) with sum as Id)", + new List> + { + new Dictionary { { "Id", 15L} } + } + }, + { + "aggregate(SharePrice with sum as SharePrice)", + new List> + { + new Dictionary { { "SharePrice", 22.5M} } + } + }, + { + "aggregate(SharePrice with min as SharePrice)", + new List> + { + new Dictionary { { "SharePrice", 2.5M} } + } + }, + { + "aggregate(SharePrice with max as SharePrice)", + new List> + { + new Dictionary { { "SharePrice", 10M} } + } + }, + { + "aggregate(SharePrice with average as SharePrice)", + new List> + { + new Dictionary { { "SharePrice", 7.5M} } + } + }, + { + "aggregate(Id with sum as Total, SharePrice with countdistinct as SharePriceDistinctCount)", + new List> + { + new Dictionary { { "SharePriceDistinctCount", 3L}, { "Total", 15} } + } + }, + { + "groupby((Name))", + new List> + { + new Dictionary { { "Name", "Lowest"} }, + new Dictionary { { "Name", "Highest"} }, + new Dictionary { { "Name", "Middle"} } + } + }, + { + "groupby((Name), aggregate(Id with sum as Total))", + new List> + { + new Dictionary { { "Name", "Lowest"}, { "Total", 10} }, + new Dictionary { { "Name", "Highest"}, { "Total", 2} }, + new Dictionary { { "Name", "Middle"}, { "Total", 3 } } + } + }, + { + "groupby((Name), aggregate($count as Count))", + new List> + { + new Dictionary { { "Name", "Lowest"}, { "Count", 3L} }, + new Dictionary { { "Name", "Highest"}, { "Count", 1L} }, + new Dictionary { { "Name", "Middle"}, { "Count", 1L} } + } + }, + { + "filter(Name eq 'Lowest')/groupby((Name))", + new List> + { + new Dictionary { { "Name", "Lowest"} } + } + }, + /* TODO: Sam XU enable this test case when we refactor aggregationBinder with FilterBinder + { + "groupby((Name), aggregate(Id with sum as Total))/filter(Total eq 3)", + new List> + { + new Dictionary { { "Name", "Middle"}, { "Total", 3 } } + } + }, + { + "groupby((Name))/filter(Name eq 'Lowest')", + new List> + { + new Dictionary { { "Name", "Lowest"} } + } + }, + */ + { + "groupby((Address/City))", + new List> + { + new Dictionary { { "Address/City", "redmond"} }, + new Dictionary { { "Address/City", "seattle"} }, + new Dictionary { { "Address/City", "hobart"} }, + new Dictionary { { "Address/City", null} }, + } + }, + { + "groupby((Address/City, Address/State))", + new List> + { + new Dictionary { { "Address/City", "redmond"}, { "Address/State", "WA"} }, + new Dictionary { { "Address/City", "seattle"}, { "Address/State", "WA"} }, + new Dictionary { { "Address/City", "hobart"}, { "Address/State", null} }, + new Dictionary { { "Address/City", null}, { "Address/State", null} }, + } + }, + { + "groupby((Address/City, Address/State))/groupby((Address/State), aggregate(Address/City with max as MaxCity))", + new List> + { + new Dictionary { { "MaxCity", "seattle"}, { "Address/State", "WA"} }, + new Dictionary { { "MaxCity", "hobart"}, { "Address/State", null} }, + } + }, + { + "groupby((Address/State), aggregate(Address/City with max as MaxCity))", + new List> + { + new Dictionary { { "MaxCity", "seattle"}, { "Address/State", "WA"} }, + new Dictionary { { "MaxCity", "hobart"}, { "Address/State", null} }, + } + }, + { + "groupby((Address/State), aggregate(startswith(Address/City, 's') with max as MaxCity))", + new List> + { + new Dictionary { { "MaxCity", true}, { "Address/State", "WA"} }, + new Dictionary { { "MaxCity", false}, { "Address/State", null} }, + } + }, + { + "groupby((Address/State), aggregate(endswith(Address/City, 't') with max as MaxCity))", + new List> + { + new Dictionary { { "MaxCity", false}, { "Address/State", "WA"} }, + new Dictionary { { "MaxCity", true}, { "Address/State", null} }, + } + }, + { + "groupby((Address/State), aggregate(contains(Address/City, 'o') with max as MaxCity))", + new List> + { + new Dictionary { { "MaxCity", true}, { "Address/State", "WA"} }, + new Dictionary { { "MaxCity", true}, { "Address/State", null} }, + } + }, + { + "groupby((Address/State), aggregate(length(Address/City) with max as MaxCity))", + new List> + { + new Dictionary { { "MaxCity", 7}, { "Address/State", "WA"} }, + new Dictionary { { "MaxCity", 6}, { "Address/State", null} }, + } + }, + { + "aggregate(year(StartDate) with max as MaxYear, year(StartDate) with min as MinYear)", + new List> + { + new Dictionary { { "MaxYear", 2018}, { "MinYear", 2016} }, + } + }, + { + "aggregate(month(StartDate) with max as MaxMonth, month(StartDate) with min as MinMonth)", + new List> + { + new Dictionary { { "MaxMonth", 5}, { "MinMonth", 1} }, + } + }, + { + "aggregate(day(StartDate) with max as MaxDay, day(StartDate) with min as MinDay)", + new List> + { + new Dictionary { { "MaxDay", 7}, { "MinDay", 1} }, + } + }, + { + "aggregate(hour(StartDate) with max as MaxHour, hour(StartDate) with min as MinHour)", + new List> + { + new Dictionary { { "MaxHour", 5 }, { "MinHour", 1} }, + } + }, + { + "aggregate(minute(StartDate) with max as MaxMinute, minute(StartDate) with min as MinMinute)", + new List> + { + new Dictionary { { "MaxMinute", 6}, { "MinMinute", 2} }, + } + }, + { + "aggregate(second(StartDate) with max as MaxSecond, second(StartDate) with min as MinSecond)", + new List> + { + new Dictionary { { "MaxSecond", 7}, { "MinSecond", 3} }, + } + }, + { + "groupby((Address/State), aggregate(concat(Address/City,Address/State) with max as MaxCity))", + new List> + { + new Dictionary { { "MaxCity", "seattleWA"}, { "Address/State", "WA"} }, + new Dictionary { { "MaxCity", null}, { "Address/State", null} }, + } + }, + { + "groupby((Address/State), aggregate(Address/City with max as MaxCity, Address/City with min as MinCity))", + new List> + { + new Dictionary { { "MaxCity", "seattle"}, { "MinCity", "redmond"}, { "Address/State", "WA"} }, + new Dictionary { { "MaxCity", "hobart"}, { "MinCity", "hobart" }, { "Address/State", null} }, + } + }, + { + "groupby((Address/State), aggregate(Address/City with max as MaxCity, Id mul Id with sum as Id))", + new List> + { + new Dictionary { { "MaxCity", "seattle"}, { "Id", 30}, { "Address/State", "WA"} }, + new Dictionary { { "MaxCity", "hobart"}, { "Id", 25 }, { "Address/State", null} }, + } + }, + { + "groupby((Address/State), aggregate(Id mul Id with sum as Id))", + new List> + { + new Dictionary { { "Id", 30}, { "Address/State", "WA"} }, + new Dictionary { { "Id", 25}, { "Address/State", null} }, + } + }, + { + "filter(Company/CEO/EmployeeName eq 'john')/groupby((Company/CEO/EmployeeName))", + new List> + { + new Dictionary {{ "Company/CEO/EmployeeName", "john"} } + } + }, + /* TODO: Sam XU enable this test case when we refactor aggregationBinder with FilterBinder + { + "groupby((Company/CEO/EmployeeName))/filter(Company/CEO/EmployeeName eq 'john')", + new List> + { + new Dictionary {{ "Company/CEO/EmployeeName", "john"} } + } + }, + */ + { + "groupby((Name, Company/CEO/EmployeeName))", + new List> + { + new Dictionary { { "Name", "Lowest"}, { "Company/CEO/EmployeeName", "john" } }, + new Dictionary { { "Name", "Highest"}, { "Company/CEO/EmployeeName", "tom" } }, + new Dictionary { { "Name", "Middle"}, { "Company/CEO/EmployeeName", "john" } }, + new Dictionary { { "Name", "Lowest"}, { "Company/CEO/EmployeeName", "alex" } }, + new Dictionary { { "Name", "Lowest"}, { "Company/CEO/EmployeeName", null } } + } + }, + { + "groupby((Address/City, Company/CEO/EmployeeName))", + new List> + { + new Dictionary { { "Address/City", "redmond"}, { "Company/CEO/EmployeeName", "john" } }, + new Dictionary { { "Address/City", "seattle"}, { "Company/CEO/EmployeeName", "tom" } }, + new Dictionary { { "Address/City", "hobart"}, { "Company/CEO/EmployeeName", "john" } }, + new Dictionary { { "Address/City", null}, { "Company/CEO/EmployeeName", "alex" } }, + new Dictionary { { "Address/City", "redmond"}, { "Company/CEO/EmployeeName", null } } + } + }, + { + "groupby((Company/CEO/HomeAddress/City))", + new List> + { + new Dictionary { { "Company/CEO/HomeAddress/City", "redmond"} }, + new Dictionary { { "Company/CEO/HomeAddress/City", "seattle"} }, + new Dictionary { { "Company/CEO/HomeAddress/City", "hobart"} }, + new Dictionary { { "Company/CEO/HomeAddress/City", null} }, + } + }, + { + "groupby((Company/CEO/HomeAddress/City, Company/CEO/HomeAddress/State))", + new List> + { + new Dictionary { { "Company/CEO/HomeAddress/City", "redmond"}, { "Company/CEO/HomeAddress/State", "WA"} }, + new Dictionary { { "Company/CEO/HomeAddress/City", "seattle"}, { "Company/CEO/HomeAddress/State", "WA"} }, + new Dictionary { { "Company/CEO/HomeAddress/City", "hobart"}, { "Company/CEO/HomeAddress/State", null} }, + new Dictionary { { "Company/CEO/HomeAddress/City", null}, { "Company/CEO/HomeAddress/State", null} }, + } + }, + { + "groupby((Company/CEO/HomeAddress/City, Company/CEO/HomeAddress/State))/groupby((Company/CEO/HomeAddress/State), aggregate(Company/CEO/HomeAddress/City with max as MaxCity))", + new List> + { + new Dictionary { { "MaxCity", "seattle"}, { "Company/CEO/HomeAddress/State", "WA"} }, + new Dictionary { { "MaxCity", "hobart"}, { "Company/CEO/HomeAddress/State", null} }, + } + }, + { + "groupby((Company/CEO/EmployeeName))", + new List> + { + new Dictionary {{ "Company/CEO/EmployeeName", "john"} }, + new Dictionary {{ "Company/CEO/EmployeeName", "tom"} }, + new Dictionary {{ "Company/CEO/EmployeeName", "alex"} }, + new Dictionary {{ "Company/CEO/EmployeeName", null} } + } + }, + { + "groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))", + new List> + { + new Dictionary {{ "Company/CEO/EmployeeName", "john"}, { "Company/CEO/BaseSalary", 20M} }, + new Dictionary {{ "Company/CEO/EmployeeName", "tom"}, { "Company/CEO/BaseSalary", 20M} }, + new Dictionary {{ "Company/CEO/EmployeeName", "alex"}, { "Company/CEO/BaseSalary", 0M} }, + new Dictionary {{ "Company/CEO/EmployeeName", null}, { "Company/CEO/BaseSalary", null} } + } + }, + { + "groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))/groupby((Company/CEO/BaseSalary), aggregate(Company/CEO/EmployeeName with max as MaxEmployeeName))", + new List> + { + new Dictionary {{ "MaxEmployeeName", "tom"}, { "Company/CEO/BaseSalary", 20M} }, + new Dictionary {{ "MaxEmployeeName", "alex"}, { "Company/CEO/BaseSalary", 0M} }, + new Dictionary {{ "MaxEmployeeName", null}, { "Company/CEO/BaseSalary", null} } + } + }, + { + "groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))/groupby((Company/CEO/EmployeeName), aggregate(Company/CEO/BaseSalary with average as AverageBaseSalary))", + new List> + { + new Dictionary {{ "AverageBaseSalary", 20M }, { "Company/CEO/EmployeeName", "john"} }, + new Dictionary {{ "AverageBaseSalary", 20M }, { "Company/CEO/EmployeeName", "tom"} }, + new Dictionary {{ "AverageBaseSalary", 0M }, { "Company/CEO/EmployeeName", "alex"} }, + new Dictionary {{ "AverageBaseSalary", null }, { "Company/CEO/EmployeeName", null} } + } + }, + { + "aggregate(Id mul Id with sum as Id)", + new List> + { + new Dictionary { { "Id", 55} } + } + }, + { + // Note SharePrice and Id have different type + "aggregate(SharePrice mul Id with sum as Result)", + new List> + { + new Dictionary { { "Result", 65.0M} } + } + }, + { + "groupby((Website))", + new List> + { + new Dictionary { { "Website", null} }, + } + }, + { + "aggregate(IntProp with max as MaxIntProp)", + new List> + { + new Dictionary { { "MaxIntProp", 2} } + } + }, + { + "aggregate(IntProp with min as MinIntProp)", + new List> + { + new Dictionary { { "MinIntProp", 1} } + } + }, + { + "aggregate(IntProp with countdistinct as DistinctIntProp)", + new List> + { + new Dictionary { { "DistinctIntProp", 3L} } + } + }, + { + "aggregate(IntProp with sum as TotalIntProp)", + new List> + { + new Dictionary { { "TotalIntProp", 3M} } + } + }, + { + "aggregate(IntProp with average as TotalIntProp)", + new List> + { + new Dictionary { { "TotalIntProp", 1.5M} } + } + }, + { + "aggregate(MixedProp with sum as TotalMixedProp)", + new List> + { + new Dictionary { { "TotalMixedProp", 1M} } + } + }, + { + "groupby((StringProp), aggregate(IntProp with min as MinIntProp))", + new List> + { + new Dictionary { { "StringProp", "Test1" }, { "MinIntProp", 1} }, + new Dictionary { { "StringProp", "Test2" }, { "MinIntProp", 2} }, + new Dictionary { { "StringProp", "Test3" }, { "MinIntProp", null} }, + new Dictionary { { "StringProp", null }, { "MinIntProp", null} }, + } + }, + { + "groupby((StringProp), aggregate(IntProp with min as MinIntProp))/groupby((StringProp))", + new List> + { + new Dictionary { { "StringProp", "Test1" } }, + new Dictionary { { "StringProp", "Test2" } }, + new Dictionary { { "StringProp", "Test3" } }, + new Dictionary { { "StringProp", null } }, + } + }, + { + "aggregate($count as Count)/compute(Count add Count as DoubleCount)", + new List> + { + new Dictionary { { "Count", 5L}, { "DoubleCount", 10L } } + } + }, + /* TODO: Sam XU enable this test case when we refactor aggregationBinder with FilterBinder + { + "groupby((Name), aggregate(Id with sum as Total))/compute(Total add Total as DoubleTotal, length(Name) as NameLen)", + new List> + { + new Dictionary { { "Name", "Lowest"}, { "Total", 10}, { "DoubleTotal", 20}, { "NameLen", 6},}, + new Dictionary { { "Name", "Highest"}, { "Total", 2} , { "DoubleTotal", 4} , { "NameLen", 7} ,}, + new Dictionary { { "Name", "Middle"}, { "Total", 3 }, { "DoubleTotal", 6 }, { "NameLen", 6 }, } + } + }, + */ + { + "compute(length(Name) as NameLen)", + new List> + { + new Dictionary { { "Name", "Lowest" }, { "NameLen", 6}, { "Id", 1},}, + new Dictionary { { "Name", "Highest"}, { "NameLen", 7}, { "Id", 2},}, + new Dictionary { { "Name", "Middle" }, { "NameLen", 6}, { "Id", 3},}, + new Dictionary { { "Name", "Lowest" }, { "NameLen", 6}, { "Id", 4},}, + new Dictionary { { "Name", "Lowest" }, { "NameLen", 6}, { "Id", 5},}, + } + }, + { + "compute(length(ShareSymbol) as NameLen)", + new List> + { + new Dictionary { { "Name", "Lowest" }, { "NameLen", null}, { "Id", 1},}, + new Dictionary { { "Name", "Highest"}, { "NameLen", null}, { "Id", 2},}, + new Dictionary { { "Name", "Middle" }, { "NameLen", null}, { "Id", 3},}, + new Dictionary { { "Name", "Lowest" }, { "NameLen", null}, { "Id", 4},}, + new Dictionary { { "Name", "Lowest" }, { "NameLen", null}, { "Id", 5},}, + } + }, + { + "compute(length(Name) as NameLen)/aggregate(NameLen with sum as TotalLen)", + new List> + { + new Dictionary { { "TotalLen", 31} } + } + }, + { + "compute(length(Name) as NameLen)/aggregate(NameLen add Id with sum as TotalLen)", + new List> + { + new Dictionary { { "TotalLen", 46} } + } + }, + { + "compute(length(Name) as NameLen)/groupby((Name),aggregate(Id with sum as Total, NameLen with max as MaxNameLen))", + new List> + { + new Dictionary { { "Name", "Lowest"}, { "Total", 10}, { "MaxNameLen", 6},}, + new Dictionary { { "Name", "Highest"}, { "Total", 2} , { "MaxNameLen", 7} ,}, + new Dictionary { { "Name", "Middle"}, { "Total", 3 }, { "MaxNameLen", 6 }, } + } + }, + { + "compute(length(Name) as NameLen)/groupby((NameLen),aggregate(Id with sum as Total))", + new List> + { + new Dictionary { { "Total", 13}, { "NameLen", 6},}, + new Dictionary { { "Total", 2} , { "NameLen", 7} ,}, + } + }, + { + "groupby((Address/State), aggregate(Address/City with max as MaxCity, Address/City with min as MinCity))/compute(length(MaxCity) as MaxCityLen)", + new List> + { + new Dictionary { { "MaxCity", "seattle"}, { "MinCity", "redmond"}, { "Address/State", "WA"}, {"MaxCityLen", 7 } }, + new Dictionary { { "MaxCity", "hobart"}, { "MinCity", "hobart" }, { "Address/State", null}, {"MaxCityLen", 6 } }, + } + }, + { + "compute(length(Address/City) as CityLength)/groupby((Address/State), aggregate(Address/City with max as MaxCity, Address/City with min as MinCity, CityLength with max as MaxCityLen))", + new List> + { + new Dictionary { { "MaxCity", "seattle"}, { "MinCity", "redmond"}, { "Address/State", "WA"}, {"MaxCityLen", 7 } }, + new Dictionary { { "MaxCity", "hobart"}, { "MinCity", "hobart" }, { "Address/State", null}, {"MaxCityLen", 6 } }, + } + }, + }; } + } - - // Legal filter queries usable against CustomerFilterTestData. - // Tuple is: filter, expected list of customer ID's - public static TheoryDataSet CustomerTestFilters + public static TheoryDataSet>> CustomerTestAppliesMixedWithOthers + { + get { - get + return new TheoryDataSet>> { - return new TheoryDataSet - { - // Primitive properties - { "Name eq 'Highest'", new int[] { 2 } }, - { "endswith(Name, 'est')", new int[] { 1, 2, 4, 5 } }, - - // Complex properties - { "Address/City eq 'redmond'", new int[] { 1, 5 } }, - { "contains(Address/City, 'e')", new int[] { 1, 2, 5 } }, - { "Company/CEO/HomeAddress/City eq 'redmond'", new int[] { 1, 4 } }, - { "contains(Company/CEO/HomeAddress/City, 'e')", new int[] { 1, 2, 4 } }, - - // Primitive property collections - { "Aliases/any(alias: alias eq 'alias34')", new int[] { 3, 4 } }, - { "Aliases/any(alias: alias eq 'alias4')", new int[] { 4 } }, - { "Aliases/all(alias: alias eq 'alias2')", new int[] { 2 } }, - - // Navigational properties - { "Orders/any(order: order/OrderId eq 12)", new int[] { 1 } }, - { "startswith(Company/CompanyName, 'company')", new int[] { 1, 2, 3, 4 } }, - { "Company/CompanyName eq 'company1'", new int[] { 1, 2 } }, - { "Company/CompanyName eq 'company2'", new int[] { 3 } }, - { "Company/CompanyName eq 'company3'", new int[] { 4 } }, - { "Company/CEO/EmployeeName eq 'john'", new int[] { 1, 3 } }, - { "Company/CEO/EmployeeName eq 'tom'", new int[] { 2 } }, - { "Company/CEO/EmployeeName eq 'alex'", new int[] { 4 } }, - { "Company/CEO/BaseSalary eq 0", new int[] { 4 } }, - { "Company/CEO/BaseSalary eq 20", new int[] { 1, 2, 3 } }, - }; - } + { + "$apply=groupby((Name), aggregate(Id with sum as Total))&$filter=Total eq 3", + new List> + { + new Dictionary {{"Name", "Middle"}, {"Total", 3}} + } + }, + { + "$apply=groupby((Name), aggregate(Id with sum as Total))&$orderby=Name", + new List> + { + new Dictionary {{"Name", "Highest"}, {"Total", 2}}, + new Dictionary {{"Name", "Lowest"}, {"Total", 10}}, + new Dictionary {{"Name", "Middle"}, {"Total", 3}}, + } + }, + { + "$apply=groupby((Name))&$orderby=Name", + new List> + { + new Dictionary {{"Name", "Highest"}}, + new Dictionary {{"Name", "Lowest"}}, + new Dictionary {{"Name", "Middle"}}, + } + }, + { + "$apply=groupby((Name), aggregate(Id with sum as Total, Id with sum as Total2))&$orderby=Total", + new List> + { + new Dictionary {{"Name", "Highest"}, {"Total", 2}}, + new Dictionary {{"Name", "Middle"}, {"Total", 3}}, + new Dictionary {{"Name", "Lowest"}, {"Total", 10}}, + } + }, + { + "$apply=groupby((Name), aggregate(Id with sum as Total, Id with sum as Total2))&$orderby=Total, Total2", + new List> + { + new Dictionary {{"Name", "Highest"}, {"Total", 2}}, + new Dictionary {{"Name", "Middle"}, {"Total", 3}}, + new Dictionary {{"Name", "Lowest"}, {"Total", 10}}, + } + }, + { + "$apply=groupby((Name), aggregate(Id with sum as Total))&$orderby=Name, Total", + new List> + { + new Dictionary {{"Name", "Highest"}, {"Total", 2}}, + new Dictionary {{"Name", "Lowest"}, {"Total", 10}}, + new Dictionary {{"Name", "Middle"}, {"Total", 3}}, + } + }, + { + "$apply=groupby((Address/City))&$orderby=Address/City", + new List> + { + new Dictionary {{"Address/City", null}}, + new Dictionary {{"Address/City", "hobart"}}, + new Dictionary {{"Address/City", "redmond"}}, + new Dictionary {{"Address/City", "seattle"}}, + } + }, + { + "$apply=groupby((Address/City))&$filter=Address/City eq 'redmond'&$orderby=Address/City", + new List> + { + new Dictionary {{"Address/City", "redmond"}}, + } + }, + { + "$apply=groupby((Address/City, Address/State))&$filter=Address/State eq 'WA'&$orderby=Address/City", + new List> + { + new Dictionary {{"Address/City", "redmond"}, {"Address/State", "WA"}}, + new Dictionary {{"Address/City", "seattle"}, {"Address/State", "WA"}}, + } + }, + { + "$apply=groupby((Address/City, Address/State))&$orderby=Address/State desc, Address/City", + new List> + { + new Dictionary {{"Address/City", "redmond"}, {"Address/State", "WA"}}, + new Dictionary {{"Address/City", "seattle"}, {"Address/State", "WA"}}, + new Dictionary {{"Address/City", null}, {"Address/State", null}}, + new Dictionary {{"Address/City", "hobart"}, {"Address/State", null}}, + } + }, + { + "$apply=groupby((Address/City))&$filter=Address/City eq 'redmond'", + new List> + { + new Dictionary {{"Address/City", "redmond"}}, + } + }, + { + "$apply=groupby((Company/CEO/HomeAddress/City))&$orderby=Company/CEO/HomeAddress/City", + new List> + { + new Dictionary {{"Company/CEO/HomeAddress/City", null}}, + new Dictionary {{"Company/CEO/HomeAddress/City", "hobart"}}, + new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}}, + new Dictionary {{"Company/CEO/HomeAddress/City", "seattle"}}, + } + }, + { + "$apply=groupby((Company/CEO/HomeAddress/City))&$filter=Company/CEO/HomeAddress/City eq 'redmond'&$orderby=Company/CEO/HomeAddress/City", + new List> + { + new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}}, + } + }, + { + "$apply=groupby((Company/CEO/HomeAddress/City, Company/CEO/HomeAddress/State))&$filter=Company/CEO/HomeAddress/State eq 'WA'&$orderby=Company/CEO/HomeAddress/City", + new List> + { + new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}, {"Company/CEO/HomeAddress/State", "WA"}}, + new Dictionary {{"Company/CEO/HomeAddress/City", "seattle"}, {"Company/CEO/HomeAddress/State", "WA"}}, + } + }, + { + "$apply=groupby((Company/CEO/HomeAddress/City, Company/CEO/HomeAddress/State))&$orderby=Company/CEO/HomeAddress/State desc, Company/CEO/HomeAddress/City", + new List> + { + new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}, {"Company/CEO/HomeAddress/State", "WA"}}, + new Dictionary {{"Company/CEO/HomeAddress/City", "seattle"}, {"Company/CEO/HomeAddress/State", "WA"}}, + new Dictionary {{"Company/CEO/HomeAddress/City", null}, {"Company/CEO/HomeAddress/State", null}}, + new Dictionary {{"Company/CEO/HomeAddress/City", "hobart"}, {"Company/CEO/HomeAddress/State", null}}, + } + }, + { + "$apply=groupby((Company/CEO/HomeAddress/City))&$filter=Company/CEO/HomeAddress/City eq 'redmond'", + new List> + { + new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}}, + } + }, + { + "$apply=groupby((Company/CEO/EmployeeName))&$orderby=Company/CEO/EmployeeName", + new List> + { + new Dictionary {{ "Company/CEO/EmployeeName", null} }, + new Dictionary {{ "Company/CEO/EmployeeName", "alex"} }, + new Dictionary {{ "Company/CEO/EmployeeName", "john"} }, + new Dictionary {{ "Company/CEO/EmployeeName", "tom"} } + } + }, + { + "$apply=groupby((Company/CEO/EmployeeName))&$filter=Company/CEO/EmployeeName eq 'alex'&$orderby=Company/CEO/EmployeeName", + new List> + { + new Dictionary {{"Company/CEO/EmployeeName", "alex"}}, + } + }, + { + "$apply=groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))&$filter= Company/CEO/BaseSalary eq 20&$orderby=Company/CEO/EmployeeName", + new List> + { + new Dictionary {{ "Company/CEO/EmployeeName", "john"}, { "Company/CEO/BaseSalary", 20M} }, + new Dictionary {{ "Company/CEO/EmployeeName", "tom"}, { "Company/CEO/BaseSalary", 20M} } + } + }, + { + "$apply=groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))&$orderby=Company/CEO/BaseSalary desc, Company/CEO/EmployeeName desc", + new List> + { + new Dictionary {{ "Company/CEO/EmployeeName", "tom"}, { "Company/CEO/BaseSalary", 20M} }, + new Dictionary {{ "Company/CEO/EmployeeName", "john"}, { "Company/CEO/BaseSalary", 20M} }, + new Dictionary {{ "Company/CEO/EmployeeName", "alex"}, { "Company/CEO/BaseSalary", 0M} }, + new Dictionary {{ "Company/CEO/EmployeeName", null}, { "Company/CEO/BaseSalary", null} } + } + }, + { + "$apply=groupby((Company/CEO/EmployeeName))&$filter=Company/CEO/EmployeeName eq 'john'", + new List> + { + new Dictionary {{"Company/CEO/EmployeeName", "john"}}, + } + }, + { + "$apply=groupby((Name), aggregate(Id with sum as Total))/compute(Total mul 2 as NewTotal)&$orderby=Name, NewTotal", + new List> + { + new Dictionary {{"Name", "Highest"}, {"Total", 2}, {"NewTotal", 4}, }, + new Dictionary {{"Name", "Lowest"}, {"Total", 10}, {"NewTotal", 20},}, + new Dictionary {{"Name", "Middle"}, {"Total", 3}, {"NewTotal", 6}, }, + } + }, + { + "$apply=groupby((Name), aggregate(Id with sum as Total))/compute(Total mul 2 as NewTotal)/filter(NewTotal gt 6)&$orderby=Name, NewTotal", + new List> + { + new Dictionary {{"Name", "Lowest"}, {"Total", 10}, {"NewTotal", 20},}, + } + }, + //{ + // "$apply=groupby((Name))&$top=1", + // new List> + // { + // new Dictionary {{"Name", "Highest"}}, + // } + //}, + //{ + // "$apply=groupby((Name))&$skip=1", + // new List> + // { + // new Dictionary {{"Name", "Lowest"}}, + // new Dictionary {{"Name", "Middle"}}, + // } + //}, + }; } + } - // Test data used by CustomerTestApplies TheoryDataSet - public static List CustomerApplyTestData + public static TheoryDataSet>> CustomerTestAppliesForPaging + { + get { - get + return new TheoryDataSet>> { - List customerList = new List(); - - Customer c = new Customer { - Id = 1, - Name = "Lowest", - SharePrice = 10, - Address = new Address { City = "redmond", State = "WA" }, - DynamicProperties = new Dictionary { { "StringProp", "Test1" }, { "IntProp", 1 }, { "MixedProp", 1 } }, - StartDate = new DateTimeOffset(new DateTime(2018, 02, 07, 1, 2, 3)), - }; - c.Company = new Company() + "$apply=aggregate(Id with sum as Id)", + new List> + { + new Dictionary { { "Id", 15} } + } + }, { - CompanyName = "company1", - CEO = new Employee() + "$apply=aggregate(Id with sum as Total)", + new List> { - EmployeeName = "john", - BaseSalary = 20, - HomeAddress = new Address { City = "redmond", State = "WA" } + new Dictionary { { "Total", 15} } } - }; - c.Orders = new List + }, { - new Order { OrderId = 11, Customer = c }, - new Order { OrderId = 12, Customer = c }, - }; - customerList.Add(c); - - c = new Customer + "$apply=groupby((Name), aggregate(Id with sum as Total, Id with sum as Total2))", + new List> + { + new Dictionary {{"Name", "Highest"}, {"Total", 2}}, + new Dictionary {{"Name", "Lowest"}, {"Total", 10}}, + } + }, { - Id = 2, - Name = "Highest", - SharePrice = 2.5M, - Address = new Address { City = "seattle", State = "WA" }, - Aliases = new List { "alias2", "alias2" }, - DynamicProperties = new Dictionary { { "StringProp", "Test2" }, { "IntProp", 2 }, { "MixedProp", "String" } }, - StartDate = new DateTimeOffset(new DateTime(2017, 03, 07, 5, 6, 7)) - }; - c.Company = new Company() + "$apply=groupby((Name), aggregate(Id with sum as Total, Id with sum as Total2))&$skip=2", + new List> + { + new Dictionary {{"Name", "Middle"}, {"Total", 3}}, + } + }, { - CompanyName = "company1", - CEO = new Employee() + "$apply=groupby((Name))", + new List> { - EmployeeName = "tom", - BaseSalary = 20, - HomeAddress = new Address { City = "seattle", State = "WA" } + new Dictionary {{"Name", "Highest"}}, + new Dictionary {{"Name", "Lowest"}}, } - }; - customerList.Add(c); - - c = new Customer + }, { - Id = 3, - Name = "Middle", - Address = new Address { City = "hobart" }, - Aliases = new List { "alias2", "alias34", "alias31" }, - DynamicProperties = new Dictionary { { "StringProp", "Test3" } }, - StartDate = new DateTimeOffset(new DateTime(2018, 01, 01, 2, 3, 4)), - }; - c.Company = new Company() + "$apply=groupby((Name))&$skip=2", + new List> + { + new Dictionary {{"Name", "Middle"}}, + } + }, { - CompanyName = "company2", - CEO = new Employee() + "$apply=groupby((Id, Name))", + new List> { - EmployeeName = "john", - BaseSalary = 20, - HomeAddress = new Address { City = "hobart" } + new Dictionary {{"Name", "Lowest"}, { "Id", 1}}, + new Dictionary {{"Name", "Highest"}, { "Id", 2}}, } - }; - customerList.Add(c); - - c = new Customer + }, { - Id = 4, - Name = "Lowest", - Aliases = new List { "alias34", "alias4" }, - StartDate = new DateTimeOffset(new DateTime(2016, 05, 07, 2, 3, 4)), - }; - c.Company = new Company() + "$apply=groupby((Name, Id))", + new List> + { + new Dictionary {{"Name", "Highest" }, { "Id", 2}}, + new Dictionary {{"Name", "Lowest" }, { "Id", 1}}, + } + }, { - CompanyName = "company3", - CEO = new Employee() + "$apply=groupby((Name, Id))&$skip=2", + new List> { - EmployeeName = "alex", - HomeAddress = new Address { City = "redmond", State = "WA" } + new Dictionary {{"Name", "Lowest" }, { "Id", 4}}, + new Dictionary {{"Name", "Lowest" }, { "Id", 5}}, } - }; - customerList.Add(c); - - c = new Customer + }, { - Id = 5, - Name = "Lowest", - SharePrice = 10, - Address = new Address { City = "redmond", State = "WA" }, - }; - customerList.Add(c); - - return customerList; - } + "$apply=groupby((Name), aggregate(Id with sum as Total))", + new List> + { + new Dictionary {{"Name", "Highest"}, {"Total", 2}}, + new Dictionary {{"Name", "Lowest"}, {"Total", 10}}, + } + }, + { + "$apply=groupby((Name), aggregate(Id with sum as Total))&$skip=2", + new List> + { + new Dictionary {{"Name", "Middle"}, {"Total", 3}}, + } + }, + { + "$apply=groupby((Address/City))", + new List> + { + new Dictionary {{"Address/City", null}}, + new Dictionary {{"Address/City", "hobart"}}, + } + }, + { + "$apply=groupby((Address/City))&$skip=2", + new List> + { + new Dictionary {{"Address/City", "redmond"}}, + new Dictionary {{"Address/City", "seattle"}}, + } + }, + { + "$apply=groupby((Address/City, Address/State))", + new List> + { + new Dictionary {{"Address/City", null}, {"Address/State", null}}, + new Dictionary {{"Address/City", "hobart"}, {"Address/State", null}}, + } + }, + { + "$apply=groupby((Address/City, Address/State))&$skip=2", + new List> + { + new Dictionary {{"Address/City", "redmond"}, {"Address/State", "WA"}}, + new Dictionary {{"Address/City", "seattle"}, {"Address/State", "WA"}}, + } + }, + { + "$apply=groupby((Company/CEO/HomeAddress/City))", + new List> + { + new Dictionary {{"Company/CEO/HomeAddress/City", null}}, + new Dictionary {{"Company/CEO/HomeAddress/City", "hobart"}}, + } + }, + { + "$apply=groupby((Company/CEO/HomeAddress/City))&$skip=2", + new List> + { + new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}}, + new Dictionary {{"Company/CEO/HomeAddress/City", "seattle"}}, + } + }, + { + "$apply=groupby((Company/CEO/HomeAddress/City, Company/CEO/HomeAddress/State))", + new List> + { + new Dictionary {{"Company/CEO/HomeAddress/City", null}, {"Company/CEO/HomeAddress/State", null}}, + new Dictionary {{"Company/CEO/HomeAddress/City", "hobart"}, {"Company/CEO/HomeAddress/State", null}}, + } + }, + { + "$apply=groupby((Company/CEO/HomeAddress/City, Company/CEO/HomeAddress/State))&$skip=2", + new List> + { + new Dictionary {{"Company/CEO/HomeAddress/City", "redmond"}, {"Company/CEO/HomeAddress/State", "WA"}}, + new Dictionary {{"Company/CEO/HomeAddress/City", "seattle"}, {"Company/CEO/HomeAddress/State", "WA"}}, + } + }, + { + "$apply=groupby((Company/CEO/EmployeeName))", + new List> + { + new Dictionary {{ "Company/CEO/EmployeeName", null} }, + new Dictionary {{ "Company/CEO/EmployeeName", "alex"} } + } + }, + { + "$apply=groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))", + new List> + { + new Dictionary {{ "Company/CEO/EmployeeName", null}, { "Company/CEO/BaseSalary", null} }, + new Dictionary {{ "Company/CEO/EmployeeName", "alex"}, { "Company/CEO/BaseSalary", 0M} } + } + }, + { + "$apply=groupby((Company/CEO/EmployeeName, Company/CEO/BaseSalary))&$skip=2", + new List> + { + new Dictionary {{ "Company/CEO/EmployeeName", "john"}, { "Company/CEO/BaseSalary", 20M} }, + new Dictionary {{ "Company/CEO/EmployeeName", "tom"}, { "Company/CEO/BaseSalary", 20M} } + } + }, + { + "$apply=groupby((Name))&$orderby=Name", + new List> + { + new Dictionary {{"Name", "Highest"}}, + new Dictionary {{"Name", "Lowest"}}, + } + }, + { + "$apply=groupby((Name))&$skip=2&$orderby=Name", + new List> + { + new Dictionary {{"Name", "Middle"}}, + } + }, + { + "$apply=groupby((Name))&$orderby=Name desc", + new List> + { + new Dictionary {{"Name", "Middle"}}, + new Dictionary {{"Name", "Lowest"}}, + } + }, + { + "$apply=groupby((Name))&$skip=2&$orderby=Name desc", + new List> + { + new Dictionary {{"Name", "Highest"}}, + } + }, + { + "$apply=groupby((Id, Name))&$orderby=Id", + new List> + { + new Dictionary {{"Name", "Lowest"}, { "Id", 1}}, + new Dictionary {{"Name", "Highest"}, { "Id", 2}}, + } + }, + { + "$apply=groupby((Id, Name))&$orderby=Name", + new List> + { + new Dictionary {{"Name", "Highest"}, { "Id", 2}}, + new Dictionary {{"Name", "Lowest"}, { "Id", 1}}, + } + }, + { + "$apply=groupby((Id, Name))&$orderby=Name&$skip=2", + new List> + { + new Dictionary {{"Name", "Lowest"}, { "Id", 4}}, + new Dictionary {{"Name", "Lowest"}, { "Id", 5}}, + } + }, + { + "$apply=groupby((Address/City))&$orderby=Address/City", + new List> + { + new Dictionary {{"Address/City", null}}, + new Dictionary {{"Address/City", "hobart"}}, + } + }, + { + "$apply=groupby((Address/City, Address/State))&$orderby=Address/State", + new List> + { + new Dictionary {{"Address/City", null}, {"Address/State", null}}, + new Dictionary {{"Address/City", "hobart"}, {"Address/State", null}}, + } + }, + { + "$apply=compute(0 as ComputeProperty)/groupby((ComputeProperty))&$orderby=ComputeProperty desc", + new List> + { + new Dictionary {{ "ComputeProperty", 0}}, + } + }, + }; } + } + - public static TheoryDataSet AppliesWithReferencesOnGroupedOut + // Legal filter queries usable against CustomerFilterTestData. + // Tuple is: filter, expected list of customer ID's + public static TheoryDataSet CustomerTestFilters + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - "$apply=groupby((Name))&$filter=Id eq 1", - "$apply=groupby((Name))/filter(Id eq 1)", - "$apply=groupby((Name))/filter(Address/City eq 1)", - "$apply=groupby((Name))/groupby((Id))", - "$apply=groupby((Company/CEO/EmployeeName))&$filter=Id eq 1", - "$apply=groupby((Company/CEO/EmployeeName))/filter(Id eq 1)", - "$apply=groupby((Company/CEO/EmployeeName))/filter(Address/City eq 1)", - "$apply=groupby((Company/CEO/EmployeeName))/groupby((Id))", - "$apply=groupby((Company/CEO/EmployeeName))/groupby((Company/CEO/BaseSalary))", - "$apply=groupby((Company/CEO/EmployeeName))/filter(Company/CEO/BaseSalary eq 20)", - "$apply=groupby((Company/CEO/EmployeeName))&$filter=Company/CEO/BaseSalary eq 20", - "$apply=groupby((Company/CEO/EmployeeName))/groupby((Company/CEO/BaseSalary))" - }; - } + // Primitive properties + { "Name eq 'Highest'", new int[] { 2 } }, + { "endswith(Name, 'est')", new int[] { 1, 2, 4, 5 } }, + + // Complex properties + { "Address/City eq 'redmond'", new int[] { 1, 5 } }, + { "contains(Address/City, 'e')", new int[] { 1, 2, 5 } }, + { "Company/CEO/HomeAddress/City eq 'redmond'", new int[] { 1, 4 } }, + { "contains(Company/CEO/HomeAddress/City, 'e')", new int[] { 1, 2, 4 } }, + + // Primitive property collections + { "Aliases/any(alias: alias eq 'alias34')", new int[] { 3, 4 } }, + { "Aliases/any(alias: alias eq 'alias4')", new int[] { 4 } }, + { "Aliases/all(alias: alias eq 'alias2')", new int[] { 2 } }, + + // Navigational properties + { "Orders/any(order: order/OrderId eq 12)", new int[] { 1 } }, + { "startswith(Company/CompanyName, 'company')", new int[] { 1, 2, 3, 4 } }, + { "Company/CompanyName eq 'company1'", new int[] { 1, 2 } }, + { "Company/CompanyName eq 'company2'", new int[] { 3 } }, + { "Company/CompanyName eq 'company3'", new int[] { 4 } }, + { "Company/CEO/EmployeeName eq 'john'", new int[] { 1, 3 } }, + { "Company/CEO/EmployeeName eq 'tom'", new int[] { 2 } }, + { "Company/CEO/EmployeeName eq 'alex'", new int[] { 4 } }, + { "Company/CEO/BaseSalary eq 0", new int[] { 4 } }, + { "Company/CEO/BaseSalary eq 20", new int[] { 1, 2, 3 } }, + }; } + } - [Theory] - [MemberData(nameof(CustomerTestApplies))] - public void ApplyTo_Returns_Correct_Queryable(string filter, List> aggregation) + // Test data used by CustomerTestApplies TheoryDataSet + public static List CustomerApplyTestData + { + get { - // Arrange - var model = new ODataModelBuilder() - .Add_Order_EntityType() - .Add_Customer_EntityType_With_Address() - .Add_CustomerOrders_Relationship() - .Add_Customer_EntityType_With_CollectionProperties() - .Add_Company_EntityType() - .Add_CustomerCompany_Relationship() - .Add_Employee_EntityType_With_HomeAddress() - .Add_CompanyEmployees_Relationship() - .Add_Customers_EntitySet() - .GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var queryOptionParser = new ODataQueryOptionParser( - context.Model, - context.ElementType, - context.NavigationSource, - new Dictionary { { "$apply", filter } }); - var applyOption = new ApplyQueryOption(filter, context, queryOptionParser); - IEnumerable customers = CustomerApplyTestData; - - // Act - IQueryable queryable = applyOption.ApplyTo(customers.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); - - // Assert - Assert.NotNull(queryable); - var actualCustomers = Assert.IsAssignableFrom>(queryable).ToList(); - - Assert.Equal(aggregation.Count(), actualCustomers.Count()); - - var aggEnum = actualCustomers.GetEnumerator(); - - foreach (var expected in aggregation) + List customerList = new List(); + + Customer c = new Customer + { + Id = 1, + Name = "Lowest", + SharePrice = 10, + Address = new Address { City = "redmond", State = "WA" }, + DynamicProperties = new Dictionary { { "StringProp", "Test1" }, { "IntProp", 1 }, { "MixedProp", 1 } }, + StartDate = new DateTimeOffset(new DateTime(2018, 02, 07, 1, 2, 3)), + }; + c.Company = new Company() { - aggEnum.MoveNext(); - var agg = aggEnum.Current; - foreach (var key in expected.Keys) + CompanyName = "company1", + CEO = new Employee() { - object value = GetValue(agg, key); - Assert.Equal(expected[key], value); + EmployeeName = "john", + BaseSalary = 20, + HomeAddress = new Address { City = "redmond", State = "WA" } } - } - } - /* - [Theory] - [MemberData(nameof(CustomerTestAppliesMixedWithOthers))] - public void ClausesAfterApplyTo_Returns_Correct_Queryable(string filter, List> aggregation) - { - // Arrange - var model = new ODataModelBuilder() - .Add_Order_EntityType() - .Add_Customer_EntityType_With_Address() - .Add_CustomerOrders_Relationship() - .Add_Customer_EntityType_With_CollectionProperties() - .Add_Company_EntityType() - .Add_CustomerCompany_Relationship() - .Add_Employee_EntityType_With_HomeAddress() - .Add_CompanyEmployees_Relationship() - .Add_Customers_EntitySet() - .GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)); - - var configuration = RoutingConfigurationFactory.CreateWithRootContainer("OData"); - var request = RequestFactory.Create(HttpMethod.Get, "http://localhost/?" + filter, configuration, "OData"); - - var options = new ODataQueryOptions(context, request); - - IEnumerable customers = CustomerApplyTestData; - // Act - IQueryable queryable = options.ApplyTo(customers.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); - - - // Assert - Assert.NotNull(queryable); - var actualCustomers = Assert.IsAssignableFrom>(queryable).ToList(); + }; + c.Orders = new List + { + new Order { OrderId = 11, Customer = c }, + new Order { OrderId = 12, Customer = c }, + }; + customerList.Add(c); - Assert.Equal(aggregation.Count(), actualCustomers.Count()); + c = new Customer + { + Id = 2, + Name = "Highest", + SharePrice = 2.5M, + Address = new Address { City = "seattle", State = "WA" }, + Aliases = new List { "alias2", "alias2" }, + DynamicProperties = new Dictionary { { "StringProp", "Test2" }, { "IntProp", 2 }, { "MixedProp", "String" } }, + StartDate = new DateTimeOffset(new DateTime(2017, 03, 07, 5, 6, 7)) + }; + c.Company = new Company() + { + CompanyName = "company1", + CEO = new Employee() + { + EmployeeName = "tom", + BaseSalary = 20, + HomeAddress = new Address { City = "seattle", State = "WA" } + } + }; + customerList.Add(c); - var aggEnum = actualCustomers.GetEnumerator(); + c = new Customer + { + Id = 3, + Name = "Middle", + Address = new Address { City = "hobart" }, + Aliases = new List { "alias2", "alias34", "alias31" }, + DynamicProperties = new Dictionary { { "StringProp", "Test3" } }, + StartDate = new DateTimeOffset(new DateTime(2018, 01, 01, 2, 3, 4)), + }; + c.Company = new Company() + { + CompanyName = "company2", + CEO = new Employee() + { + EmployeeName = "john", + BaseSalary = 20, + HomeAddress = new Address { City = "hobart" } + } + }; + customerList.Add(c); - foreach (var expected in aggregation) + c = new Customer { - aggEnum.MoveNext(); - var agg = aggEnum.Current; - foreach (var key in expected.Keys) + Id = 4, + Name = "Lowest", + Aliases = new List { "alias34", "alias4" }, + StartDate = new DateTimeOffset(new DateTime(2016, 05, 07, 2, 3, 4)), + }; + c.Company = new Company() + { + CompanyName = "company3", + CEO = new Employee() { - object value = GetValue(agg, key); - Assert.Equal(expected[key], value); + EmployeeName = "alex", + HomeAddress = new Address { City = "redmond", State = "WA" } } - } + }; + customerList.Add(c); + + c = new Customer + { + Id = 5, + Name = "Lowest", + SharePrice = 10, + Address = new Address { City = "redmond", State = "WA" }, + }; + customerList.Add(c); + + return customerList; } + } - [Theory] - [MemberData(nameof(AppliesWithReferencesOnGroupedOut))] - public void ClausesWithGroupedOutReferences_Throw_ODataException(string clause) + public static TheoryDataSet AppliesWithReferencesOnGroupedOut + { + get { - // Arrange - var model = new ODataModelBuilder() - .Add_Order_EntityType() - .Add_Customer_EntityType_With_Address() - .Add_CustomerOrders_Relationship() - .Add_Customer_EntityType_With_CollectionProperties() - .Add_Customers_EntitySet() - .GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)); - - var configuration = RoutingConfigurationFactory.CreateWithRootContainer("OData"); - var request = RequestFactory.Create(HttpMethod.Get, "http://localhost/?" + clause, configuration, "OData"); - - var options = new ODataQueryOptions(context, request); - - IEnumerable customers = CustomerApplyTestData; - - // Act & Assert - ExceptionAssert.Throws(() => + return new TheoryDataSet { - IQueryable queryable = options.ApplyTo(customers.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); - }); + "$apply=groupby((Name))&$filter=Id eq 1", + "$apply=groupby((Name))/filter(Id eq 1)", + "$apply=groupby((Name))/filter(Address/City eq 1)", + "$apply=groupby((Name))/groupby((Id))", + "$apply=groupby((Company/CEO/EmployeeName))&$filter=Id eq 1", + "$apply=groupby((Company/CEO/EmployeeName))/filter(Id eq 1)", + "$apply=groupby((Company/CEO/EmployeeName))/filter(Address/City eq 1)", + "$apply=groupby((Company/CEO/EmployeeName))/groupby((Id))", + "$apply=groupby((Company/CEO/EmployeeName))/groupby((Company/CEO/BaseSalary))", + "$apply=groupby((Company/CEO/EmployeeName))/filter(Company/CEO/BaseSalary eq 20)", + "$apply=groupby((Company/CEO/EmployeeName))&$filter=Company/CEO/BaseSalary eq 20", + "$apply=groupby((Company/CEO/EmployeeName))/groupby((Company/CEO/BaseSalary))" + }; } + } - [Theory] - [MemberData(nameof(CustomerTestAppliesForPaging))] - public void StableSortingAndPagingApplyTo_Returns_Correct_Queryable(string filter, List> aggregation) + [Theory] + [MemberData(nameof(CustomerTestApplies))] + public void ApplyTo_Returns_Correct_Queryable(string filter, List> aggregation) + { + // Arrange + var model = new ODataModelBuilder() + .Add_Order_EntityType() + .Add_Customer_EntityType_With_Address() + .Add_CustomerOrders_Relationship() + .Add_Customer_EntityType_With_CollectionProperties() + .Add_Company_EntityType() + .Add_CustomerCompany_Relationship() + .Add_Employee_EntityType_With_HomeAddress() + .Add_CompanyEmployees_Relationship() + .Add_Customers_EntitySet() + .GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var queryOptionParser = new ODataQueryOptionParser( + context.Model, + context.ElementType, + context.NavigationSource, + new Dictionary { { "$apply", filter } }); + var applyOption = new ApplyQueryOption(filter, context, queryOptionParser); + IEnumerable customers = CustomerApplyTestData; + + // Act + IQueryable queryable = applyOption.ApplyTo(customers.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); + + // Assert + Assert.NotNull(queryable); + var actualCustomers = Assert.IsAssignableFrom>(queryable).ToList(); + + Assert.Equal(aggregation.Count(), actualCustomers.Count()); + + var aggEnum = actualCustomers.GetEnumerator(); + + foreach (var expected in aggregation) { - // Arrange - var model = new ODataModelBuilder() - .Add_Order_EntityType() - .Add_Customer_EntityType_With_Address() - .Add_CustomerOrders_Relationship() - .Add_Customer_EntityType_With_CollectionProperties() - .Add_Company_EntityType() - .Add_CustomerCompany_Relationship() - .Add_Employee_EntityType_With_HomeAddress() - .Add_CompanyEmployees_Relationship() - .Add_Customers_EntitySet() - .GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)); - - var configuration = RoutingConfigurationFactory.CreateWithRootContainer("OData"); - var request = RequestFactory.Create(HttpMethod.Get, "http://localhost/?" + filter, configuration, "OData"); - - var options = new ODataQueryOptions(context, request); - - IEnumerable customers = CustomerApplyTestData; - // Act - IQueryable queryable = options.ApplyTo(customers.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True, PageSize = 2 }); - - // Assert - Assert.NotNull(queryable); - var actualCustomers = Assert.IsAssignableFrom>(queryable).ToList(); - - Assert.Equal(aggregation.Count(), actualCustomers.Count()); - - var aggEnum = actualCustomers.GetEnumerator(); - - foreach (var expected in aggregation) + aggEnum.MoveNext(); + var agg = aggEnum.Current; + foreach (var key in expected.Keys) { - aggEnum.MoveNext(); - var agg = aggEnum.Current; - foreach (var key in expected.Keys) - { - object value = GetValue(agg, key); - Assert.Equal(expected[key], value); - } + object value = GetValue(agg, key); + Assert.Equal(expected[key], value); } } - */ + } + /* + [Theory] + [MemberData(nameof(CustomerTestAppliesMixedWithOthers))] + public void ClausesAfterApplyTo_Returns_Correct_Queryable(string filter, List> aggregation) + { + // Arrange + var model = new ODataModelBuilder() + .Add_Order_EntityType() + .Add_Customer_EntityType_With_Address() + .Add_CustomerOrders_Relationship() + .Add_Customer_EntityType_With_CollectionProperties() + .Add_Company_EntityType() + .Add_CustomerCompany_Relationship() + .Add_Employee_EntityType_With_HomeAddress() + .Add_CompanyEmployees_Relationship() + .Add_Customers_EntitySet() + .GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)); - [Theory] - [MemberData(nameof(CustomerTestFilters))] - public void ApplyTo_Returns_Correct_Queryable_ForFilter(string filter, int[] customerIds) - { - // Arrange - var model = new ODataModelBuilder() - .Add_Order_EntityType() - .Add_Customer_EntityType_With_Address() - .Add_CustomerOrders_Relationship() - .Add_Customer_EntityType_With_CollectionProperties() - .Add_Company_EntityType() - .Add_CustomerCompany_Relationship() - .Add_Employee_EntityType_With_HomeAddress() - .Add_CompanyEmployees_Relationship() - .Add_Customers_EntitySet() - .GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var queryOptionParser = new ODataQueryOptionParser( - context.Model, - context.ElementType, - context.NavigationSource, - new Dictionary { { "$apply", string.Format("filter({0})", filter) } }); - var filterOption = new ApplyQueryOption(string.Format("filter({0})", filter), context, queryOptionParser); - IEnumerable customers = CustomerApplyTestData; - - // Act - IQueryable queryable = filterOption.ApplyTo(customers.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); - - // Assert - Assert.NotNull(queryable); - IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); - Assert.Equal( - customerIds, - actualCustomers.Select(customer => customer.Id)); - } + var configuration = RoutingConfigurationFactory.CreateWithRootContainer("OData"); + var request = RequestFactory.Create(HttpMethod.Get, "http://localhost/?" + filter, configuration, "OData"); + + var options = new ODataQueryOptions(context, request); + + IEnumerable customers = CustomerApplyTestData; + // Act + IQueryable queryable = options.ApplyTo(customers.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); + + + // Assert + Assert.NotNull(queryable); + var actualCustomers = Assert.IsAssignableFrom>(queryable).ToList(); - //[Fact] - //public async Task ApplyToSerializationWorks() - //{ - // // Arrange - // var model = new ODataModelBuilder() - // .Add_Order_EntityType() - // .Add_Customer_EntityType_With_Address() - // .Add_CustomerOrders_Relationship() - // .Add_Customer_EntityType_With_CollectionProperties() - // .Add_Customers_EntitySet() - // .GetEdmModel(); - - // var controllers = new[] { typeof(MetadataController), typeof(CustomersController) }; - // var server = TestServerFactory.Create(controllers, (config) => - // { - // config.MapODataServiceRoute("odata", "odata", model); - // }); - - // HttpClient client = TestServerFactory.CreateClient(server); - - // HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, - // "http://localhost/odata/Customers?$apply=groupby((Name), aggregate(Id with sum as TotalId))"); - - // // Act - // HttpResponseMessage response = await client.SendAsync(request); - - // // Assert - // Assert.True(response.IsSuccessStatusCode); - // Assert.NotNull(response); - // var result = await response.Content.ReadAsObject(); - // var results = result["value"] as JArray; - // Assert.Equal(3, results.Count); - // Assert.Equal("10", results[0]["TotalId"].ToString()); - // Assert.Equal("Lowest", results[0]["Name"].ToString()); - // Assert.Equal("2", results[1]["TotalId"].ToString()); - // Assert.Equal("Highest", results[1]["Name"].ToString()); - // Assert.Equal("3", results[2]["TotalId"].ToString()); - // Assert.Equal("Middle", results[2]["Name"].ToString()); - //} - - //[Fact] - //public async Task ApplyToSerializationWorksForCompelxTypes() - //{ - // // Arrange - // var model = new ODataModelBuilder() - // .Add_Order_EntityType() - // .Add_Customer_EntityType_With_Address() - // .Add_CustomerOrders_Relationship() - // .Add_Customer_EntityType_With_CollectionProperties() - // .Add_Customers_EntitySet() - // .GetEdmModel(); - - // var controllers = new[] { typeof(MetadataController), typeof(CustomersController) }; - // var server = TestServerFactory.Create(controllers, (config) => - // { - // config.MapODataServiceRoute("odata", "odata", model); - // }); - - // HttpClient client = TestServerFactory.CreateClient(server); - - // HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, - // "http://localhost/odata/Customers?$apply=groupby((Address/City), aggregate(Id with sum as TotalId))"); - - // // Act - // HttpResponseMessage response = await client.SendAsync(request); - - // // Assert - // Assert.True(response.IsSuccessStatusCode); - // Assert.NotNull(response); - // var result = await response.Content.ReadAsObject(); - // var results = result["value"] as JArray; - // Assert.Equal(4, results.Count); - // Assert.Equal("6", results[0]["TotalId"].ToString()); - // var address0 = results[0]["Address"] as JObject; - // Assert.Equal("redmond", address0["City"].ToString()); - //} - - - private object GetValue(DynamicTypeWrapper wrapper, string path) + Assert.Equal(aggregation.Count(), actualCustomers.Count()); + + var aggEnum = actualCustomers.GetEnumerator(); + + foreach (var expected in aggregation) { - var parts = path.Split('/'); - foreach (var part in parts) + aggEnum.MoveNext(); + var agg = aggEnum.Current; + foreach (var key in expected.Keys) { - object value; - wrapper.TryGetPropertyValue(part, out value); - wrapper = value as DynamicTypeWrapper; - if (wrapper == null) - { - return value; - } + object value = GetValue(agg, key); + Assert.Equal(expected[key], value); } - - Assert.False(true, "Property " + path + " not found"); - return null; } + } + + [Theory] + [MemberData(nameof(AppliesWithReferencesOnGroupedOut))] + public void ClausesWithGroupedOutReferences_Throw_ODataException(string clause) + { + // Arrange + var model = new ODataModelBuilder() + .Add_Order_EntityType() + .Add_Customer_EntityType_With_Address() + .Add_CustomerOrders_Relationship() + .Add_Customer_EntityType_With_CollectionProperties() + .Add_Customers_EntitySet() + .GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)); + + var configuration = RoutingConfigurationFactory.CreateWithRootContainer("OData"); + var request = RequestFactory.Create(HttpMethod.Get, "http://localhost/?" + clause, configuration, "OData"); + + var options = new ODataQueryOptions(context, request); + + IEnumerable customers = CustomerApplyTestData; - private static IEdmModel GetEdmModel() + // Act & Assert + ExceptionAssert.Throws(() => { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } + IQueryable queryable = options.ApplyTo(customers.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); + }); + } - private class ApplyCustomer + [Theory] + [MemberData(nameof(CustomerTestAppliesForPaging))] + public void StableSortingAndPagingApplyTo_Returns_Correct_Queryable(string filter, List> aggregation) + { + // Arrange + var model = new ODataModelBuilder() + .Add_Order_EntityType() + .Add_Customer_EntityType_With_Address() + .Add_CustomerOrders_Relationship() + .Add_Customer_EntityType_With_CollectionProperties() + .Add_Company_EntityType() + .Add_CustomerCompany_Relationship() + .Add_Employee_EntityType_With_HomeAddress() + .Add_CompanyEmployees_Relationship() + .Add_Customers_EntitySet() + .GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)); + + var configuration = RoutingConfigurationFactory.CreateWithRootContainer("OData"); + var request = RequestFactory.Create(HttpMethod.Get, "http://localhost/?" + filter, configuration, "OData"); + + var options = new ODataQueryOptions(context, request); + + IEnumerable customers = CustomerApplyTestData; + // Act + IQueryable queryable = options.ApplyTo(customers.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True, PageSize = 2 }); + + // Assert + Assert.NotNull(queryable); + var actualCustomers = Assert.IsAssignableFrom>(queryable).ToList(); + + Assert.Equal(aggregation.Count(), actualCustomers.Count()); + + var aggEnum = actualCustomers.GetEnumerator(); + + foreach (var expected in aggregation) { - public int Id { get; set; } - public string Name { get; set; } - public string City { get; set; } + aggEnum.MoveNext(); + var agg = aggEnum.Current; + foreach (var key in expected.Keys) + { + object value = GetValue(agg, key); + Assert.Equal(expected[key], value); + } } } + */ - public class CustomersController : ODataController + [Theory] + [MemberData(nameof(CustomerTestFilters))] + public void ApplyTo_Returns_Correct_Queryable_ForFilter(string filter, int[] customerIds) { - private List _customers; + // Arrange + var model = new ODataModelBuilder() + .Add_Order_EntityType() + .Add_Customer_EntityType_With_Address() + .Add_CustomerOrders_Relationship() + .Add_Customer_EntityType_With_CollectionProperties() + .Add_Company_EntityType() + .Add_CustomerCompany_Relationship() + .Add_Employee_EntityType_With_HomeAddress() + .Add_CompanyEmployees_Relationship() + .Add_Customers_EntitySet() + .GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var queryOptionParser = new ODataQueryOptionParser( + context.Model, + context.ElementType, + context.NavigationSource, + new Dictionary { { "$apply", string.Format("filter({0})", filter) } }); + var filterOption = new ApplyQueryOption(string.Format("filter({0})", filter), context, queryOptionParser); + IEnumerable customers = CustomerApplyTestData; + + // Act + IQueryable queryable = filterOption.ApplyTo(customers.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); + Assert.Equal( + customerIds, + actualCustomers.Select(customer => customer.Id)); + } - public CustomersController() + //[Fact] + //public async Task ApplyToSerializationWorks() + //{ + // // Arrange + // var model = new ODataModelBuilder() + // .Add_Order_EntityType() + // .Add_Customer_EntityType_With_Address() + // .Add_CustomerOrders_Relationship() + // .Add_Customer_EntityType_With_CollectionProperties() + // .Add_Customers_EntitySet() + // .GetEdmModel(); + + // var controllers = new[] { typeof(MetadataController), typeof(CustomersController) }; + // var server = TestServerFactory.Create(controllers, (config) => + // { + // config.MapODataServiceRoute("odata", "odata", model); + // }); + + // HttpClient client = TestServerFactory.CreateClient(server); + + // HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, + // "http://localhost/odata/Customers?$apply=groupby((Name), aggregate(Id with sum as TotalId))"); + + // // Act + // HttpResponseMessage response = await client.SendAsync(request); + + // // Assert + // Assert.True(response.IsSuccessStatusCode); + // Assert.NotNull(response); + // var result = await response.Content.ReadAsObject(); + // var results = result["value"] as JArray; + // Assert.Equal(3, results.Count); + // Assert.Equal("10", results[0]["TotalId"].ToString()); + // Assert.Equal("Lowest", results[0]["Name"].ToString()); + // Assert.Equal("2", results[1]["TotalId"].ToString()); + // Assert.Equal("Highest", results[1]["Name"].ToString()); + // Assert.Equal("3", results[2]["TotalId"].ToString()); + // Assert.Equal("Middle", results[2]["Name"].ToString()); + //} + + //[Fact] + //public async Task ApplyToSerializationWorksForCompelxTypes() + //{ + // // Arrange + // var model = new ODataModelBuilder() + // .Add_Order_EntityType() + // .Add_Customer_EntityType_With_Address() + // .Add_CustomerOrders_Relationship() + // .Add_Customer_EntityType_With_CollectionProperties() + // .Add_Customers_EntitySet() + // .GetEdmModel(); + + // var controllers = new[] { typeof(MetadataController), typeof(CustomersController) }; + // var server = TestServerFactory.Create(controllers, (config) => + // { + // config.MapODataServiceRoute("odata", "odata", model); + // }); + + // HttpClient client = TestServerFactory.CreateClient(server); + + // HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, + // "http://localhost/odata/Customers?$apply=groupby((Address/City), aggregate(Id with sum as TotalId))"); + + // // Act + // HttpResponseMessage response = await client.SendAsync(request); + + // // Assert + // Assert.True(response.IsSuccessStatusCode); + // Assert.NotNull(response); + // var result = await response.Content.ReadAsObject(); + // var results = result["value"] as JArray; + // Assert.Equal(4, results.Count); + // Assert.Equal("6", results[0]["TotalId"].ToString()); + // var address0 = results[0]["Address"] as JObject; + // Assert.Equal("redmond", address0["City"].ToString()); + //} + + + private object GetValue(DynamicTypeWrapper wrapper, string path) + { + var parts = path.Split('/'); + foreach (var part in parts) { - _customers = ApplyQueryOptionTest.CustomerApplyTestData; + object value; + wrapper.TryGetPropertyValue(part, out value); + wrapper = value as DynamicTypeWrapper; + if (wrapper == null) + { + return value; + } } - [EnableQuery] - public IQueryable Get() - { - return _customers.AsQueryable(); - } + Assert.False(true, "Property " + path + " not found"); + return null; } -} -namespace System.Data.Linq -{ - public class MyQueryProvider : IQueryProvider + private static IEdmModel GetEdmModel() { - public IQueryable CreateQuery(Expression expression) - { - throw new NotImplementedException(); - } + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); + } - public IQueryable CreateQuery(Expression expression) - { - throw new NotImplementedException(); - } + private class ApplyCustomer + { + public int Id { get; set; } + public string Name { get; set; } + public string City { get; set; } + } +} - public object Execute(Expression expression) - { - throw new NotImplementedException(); - } +public class CustomersController : ODataController +{ + private List _customers; - public TResult Execute(Expression expression) - { - throw new NotImplementedException(); - } + public CustomersController() + { + _customers = ApplyQueryOptionTest.CustomerApplyTestData; + } + + [EnableQuery] + public IQueryable Get() + { + return _customers.AsQueryable(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ComputeQueryOptionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ComputeQueryOptionTests.cs index 8ea19be07..3a6d30605 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ComputeQueryOptionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ComputeQueryOptionTests.cs @@ -15,113 +15,112 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class ComputeQueryOptionTests { - public class ComputeQueryOptionTests - { - private static IEdmModel _model = GetModel(); - - [Fact] - public void CtorComputeQueryOption_ThrowsArgumentNull_ForInputParameter() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new ComputeQueryOption(null, null, null), "rawValue"); - ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new ComputeQueryOption(string.Empty, null, null), "rawValue"); - - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ComputeQueryOption("groupby", null, null), "context"); - - // Arrange & Act & Assert - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - ExceptionAssert.ThrowsArgumentNull(() => new ComputeQueryOption("groupby", context, null), "queryOptionParser"); - } - - [Fact] - public void CtorComputeQueryOption_CanConstructValidComputeQuery() - { - // Arrange - IEdmModel model = _model; - ODataQueryContext context = new ODataQueryContext(model, typeof(ComputeCustomer)); - - // Act - ComputeQueryOption compute = new ComputeQueryOption("Price mul Qty as Total", context); - - // Assert - Assert.Same(context, compute.Context); - Assert.Equal("Price mul Qty as Total", compute.RawValue); - } - - [Fact] - public void CtorComputeQueryOption_GetQueryNodeParsesQuery() - { - // Arrange - IEdmModel model = _model; - ODataQueryContext context = new ODataQueryContext(model, typeof(ComputeCustomer)) { RequestContainer = new MockServiceProvider() }; - - // Act - ComputeQueryOption compute = new ComputeQueryOption("Price mul Qty as Total,Price mul 2.0 as Tax", context); - ComputeClause computeClause = compute.ComputeClause; - - // Assert - Assert.Equal(2, computeClause.ComputedItems.Count()); - - Assert.Collection(computeClause.ComputedItems, - e => - { - Assert.Equal("Total", e.Alias); - - Assert.Equal(QueryNodeKind.BinaryOperator, e.Expression.Kind); - BinaryOperatorNode binaryNode = e.Expression as BinaryOperatorNode; - Assert.Equal(BinaryOperatorKind.Multiply, binaryNode.OperatorKind); - Assert.Equal(QueryNodeKind.Convert, binaryNode.Right.Kind); - ConvertNode convertNode = (ConvertNode)binaryNode.Right; - Assert.Equal("Qty", ((SingleValuePropertyAccessNode)convertNode.Source).Property.Name); - - Assert.Equal(QueryNodeKind.SingleValuePropertyAccess, binaryNode.Left.Kind); - var propertyAccessNode = binaryNode.Left as SingleValuePropertyAccessNode; - Assert.Equal("Price", propertyAccessNode.Property.Name); - }, - e => - { - Assert.Equal("Tax", e.Alias); - - Assert.Equal(QueryNodeKind.BinaryOperator, e.Expression.Kind); - BinaryOperatorNode binaryNode = e.Expression as BinaryOperatorNode; - Assert.Equal(BinaryOperatorKind.Multiply, binaryNode.OperatorKind); - Assert.Equal(QueryNodeKind.Constant, binaryNode.Right.Kind); - Assert.Equal(2.0, ((ConstantNode)binaryNode.Right).Value); - - Assert.Equal(QueryNodeKind.SingleValuePropertyAccess, binaryNode.Left.Kind); - var propertyAccessNode = binaryNode.Left as SingleValuePropertyAccessNode; - Assert.Equal("Price", propertyAccessNode.Property.Name); - }); - } - - private static IEdmModel GetModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } - } + private static IEdmModel _model = GetModel(); - public class ComputeCustomer + [Fact] + public void CtorComputeQueryOption_ThrowsArgumentNull_ForInputParameter() { - public int Id { get; set; } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new ComputeQueryOption(null, null, null), "rawValue"); + ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new ComputeQueryOption(string.Empty, null, null), "rawValue"); - public string Name { get; set; } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ComputeQueryOption("groupby", null, null), "context"); + + // Arrange & Act & Assert + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + ExceptionAssert.ThrowsArgumentNull(() => new ComputeQueryOption("groupby", context, null), "queryOptionParser"); + } - public int Age { get; set; } + [Fact] + public void CtorComputeQueryOption_CanConstructValidComputeQuery() + { + // Arrange + IEdmModel model = _model; + ODataQueryContext context = new ODataQueryContext(model, typeof(ComputeCustomer)); - public double Price { get; set; } + // Act + ComputeQueryOption compute = new ComputeQueryOption("Price mul Qty as Total", context); - public int Qty { get; set; } + // Assert + Assert.Same(context, compute.Context); + Assert.Equal("Price mul Qty as Total", compute.RawValue); + } - public IDictionary Dynamics { get; set; } + [Fact] + public void CtorComputeQueryOption_GetQueryNodeParsesQuery() + { + // Arrange + IEdmModel model = _model; + ODataQueryContext context = new ODataQueryContext(model, typeof(ComputeCustomer)) { RequestContainer = new MockServiceProvider() }; + + // Act + ComputeQueryOption compute = new ComputeQueryOption("Price mul Qty as Total,Price mul 2.0 as Tax", context); + ComputeClause computeClause = compute.ComputeClause; + + // Assert + Assert.Equal(2, computeClause.ComputedItems.Count()); + + Assert.Collection(computeClause.ComputedItems, + e => + { + Assert.Equal("Total", e.Alias); + + Assert.Equal(QueryNodeKind.BinaryOperator, e.Expression.Kind); + BinaryOperatorNode binaryNode = e.Expression as BinaryOperatorNode; + Assert.Equal(BinaryOperatorKind.Multiply, binaryNode.OperatorKind); + Assert.Equal(QueryNodeKind.Convert, binaryNode.Right.Kind); + ConvertNode convertNode = (ConvertNode)binaryNode.Right; + Assert.Equal("Qty", ((SingleValuePropertyAccessNode)convertNode.Source).Property.Name); + + Assert.Equal(QueryNodeKind.SingleValuePropertyAccess, binaryNode.Left.Kind); + var propertyAccessNode = binaryNode.Left as SingleValuePropertyAccessNode; + Assert.Equal("Price", propertyAccessNode.Property.Name); + }, + e => + { + Assert.Equal("Tax", e.Alias); + + Assert.Equal(QueryNodeKind.BinaryOperator, e.Expression.Kind); + BinaryOperatorNode binaryNode = e.Expression as BinaryOperatorNode; + Assert.Equal(BinaryOperatorKind.Multiply, binaryNode.OperatorKind); + Assert.Equal(QueryNodeKind.Constant, binaryNode.Right.Kind); + Assert.Equal(2.0, ((ConstantNode)binaryNode.Right).Value); + + Assert.Equal(QueryNodeKind.SingleValuePropertyAccess, binaryNode.Left.Kind); + var propertyAccessNode = binaryNode.Left as SingleValuePropertyAccessNode; + Assert.Equal("Price", propertyAccessNode.Property.Name); + }); } - public class ComputeAddress + private static IEdmModel GetModel() { - public string Street { get; set; } + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); } } + +public class ComputeCustomer +{ + public int Id { get; set; } + + public string Name { get; set; } + + public int Age { get; set; } + + public double Price { get; set; } + + public int Qty { get; set; } + + public IDictionary Dynamics { get; set; } +} + +public class ComputeAddress +{ + public string Street { get; set; } +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/CountQueryOptionTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/CountQueryOptionTest.cs index c0b8075a1..790aa27d6 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/CountQueryOptionTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/CountQueryOptionTest.cs @@ -17,171 +17,170 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class CountQueryOptionTest { - public class CountQueryOptionTest - { - private static IEdmModel _model = new ODataModelBuilder() - .Add_Customer_EntityType() - .Add_Customers_EntitySet().GetEdmModel(); - private static ODataQueryContext _context = new ODataQueryContext(_model, typeof(Customer)); - private static IQueryable _customers = new List() - { - new Customer { Id = 1, Name = "Andy" }, - new Customer { Id = 2, Name = "Aaron" }, - new Customer { Id = 3, Name = "Alex" } - }.AsQueryable(); - - [Fact] - public void Constructor_ThrowsException_IfNullContextArgument() + private static IEdmModel _model = new ODataModelBuilder() + .Add_Customer_EntityType() + .Add_Customers_EntitySet().GetEdmModel(); + private static ODataQueryContext _context = new ODataQueryContext(_model, typeof(Customer)); + private static IQueryable _customers = new List() { - ExceptionAssert.ThrowsArgumentNull(() => new CountQueryOption("false", context: null, queryOptionParser: null), - "context"); - } + new Customer { Id = 1, Name = "Andy" }, + new Customer { Id = 2, Name = "Aaron" }, + new Customer { Id = 3, Name = "Alex" } + }.AsQueryable(); - [Fact] - public void Constructor_ThrowsException_IfNullRawValueArgument() - { - ExceptionAssert.Throws(() => new CountQueryOption(null, _context, null), - "The argument 'rawValue' is null or empty. (Parameter 'rawValue')"); - } + [Fact] + public void Constructor_ThrowsException_IfNullContextArgument() + { + ExceptionAssert.ThrowsArgumentNull(() => new CountQueryOption("false", context: null, queryOptionParser: null), + "context"); + } - [Fact] - public void Constructor_ThrowsException_IfEmptyRawValue() - { - ExceptionAssert.Throws(() => new CountQueryOption(string.Empty, _context, null), - "The argument 'rawValue' is null or empty. (Parameter 'rawValue')"); - } + [Fact] + public void Constructor_ThrowsException_IfNullRawValueArgument() + { + ExceptionAssert.Throws(() => new CountQueryOption(null, _context, null), + "The argument 'rawValue' is null or empty. (Parameter 'rawValue')"); + } - [Fact] - public void Constructor_ThrowsException_IfNullQueryOptionParser() - { - ExceptionAssert.ThrowsArgumentNull(() => new CountQueryOption("false", _context, queryOptionParser: null), - "queryOptionParser"); - } + [Fact] + public void Constructor_ThrowsException_IfEmptyRawValue() + { + ExceptionAssert.Throws(() => new CountQueryOption(string.Empty, _context, null), + "The argument 'rawValue' is null or empty. (Parameter 'rawValue')"); + } - [Fact] - public void Constructor_CanSetContextProperty() - { - // Arrange - var countOption = new CountQueryOption("test", _context); + [Fact] + public void Constructor_ThrowsException_IfNullQueryOptionParser() + { + ExceptionAssert.ThrowsArgumentNull(() => new CountQueryOption("false", _context, queryOptionParser: null), + "queryOptionParser"); + } - // Act & Assert - Assert.Same(_context, countOption.Context); - } + [Fact] + public void Constructor_CanSetContextProperty() + { + // Arrange + var countOption = new CountQueryOption("test", _context); - [Fact] - public void Constructor_CanSetRawValueProperty() - { - // Arrange - var countOption = new CountQueryOption("test", _context); + // Act & Assert + Assert.Same(_context, countOption.Context); + } - // Act & Assert - Assert.Same("test", countOption.RawValue); - } + [Fact] + public void Constructor_CanSetRawValueProperty() + { + // Arrange + var countOption = new CountQueryOption("test", _context); - [Fact] - public void Value_ReturnsTrue_IfParseTrueRawValue() - { - // Assert - var countOption = new CountQueryOption("true", _context); + // Act & Assert + Assert.Same("test", countOption.RawValue); + } - // Act & Assert - Assert.True(countOption.Value); - } + [Fact] + public void Value_ReturnsTrue_IfParseTrueRawValue() + { + // Assert + var countOption = new CountQueryOption("true", _context); - [Fact] - public void Value_ReturnsFalse_IfParseFalseRawValue() - { - // Assert - var countOption = new CountQueryOption("false", _context); - - // Act & Assert - Assert.False(countOption.Value); - } - - [Theory] - [InlineData("onions")] - [InlineData(" ")] - [InlineData("Trrue")] - [InlineData("TrUe")] - [InlineData("TRUE")] - [InlineData("False")] - [InlineData("FALSE")] - public void Value_ThrowsODataException_ForInvalidValues(string countValue) - { - // Arrange - var countOption = new CountQueryOption(countValue, _context); + // Act & Assert + Assert.True(countOption.Value); + } - // Act & Assert - ExceptionAssert.Throws(() => countOption.Value, - "'" + countValue + "' is not a valid count option."); - } + [Fact] + public void Value_ReturnsFalse_IfParseFalseRawValue() + { + // Assert + var countOption = new CountQueryOption("false", _context); - [Fact] - public void GetEntityCount_ReturnsCount_IfValueIsTrue() - { - // Arrange - var countOption = new CountQueryOption("true", _context); + // Act & Assert + Assert.False(countOption.Value); + } - // Act & Assert - Assert.Equal(3, countOption.GetEntityCount(_customers)); - } + [Theory] + [InlineData("onions")] + [InlineData(" ")] + [InlineData("Trrue")] + [InlineData("TrUe")] + [InlineData("TRUE")] + [InlineData("False")] + [InlineData("FALSE")] + public void Value_ThrowsODataException_ForInvalidValues(string countValue) + { + // Arrange + var countOption = new CountQueryOption(countValue, _context); - [Fact] - public void GetEntityCount_ReturnsNull_IfValueIsFalse() - { - // Arrange - var countOption = new CountQueryOption("false", _context); + // Act & Assert + ExceptionAssert.Throws(() => countOption.Value, + "'" + countValue + "' is not a valid count option."); + } - // Act & Assert - Assert.Null(countOption.GetEntityCount(_customers)); - } + [Fact] + public void GetEntityCount_ReturnsCount_IfValueIsTrue() + { + // Arrange + var countOption = new CountQueryOption("true", _context); - [Fact] - public void GetEntityCountFunc_ReturnsFunc_IfValueIsTrue() - { - // Arrange - var countOption = new CountQueryOption("true", _context); + // Act & Assert + Assert.Equal(3, countOption.GetEntityCount(_customers)); + } - // Act & Assert - Assert.Equal(3, countOption.GetEntityCountFunc(_customers)()); - } + [Fact] + public void GetEntityCount_ReturnsNull_IfValueIsFalse() + { + // Arrange + var countOption = new CountQueryOption("false", _context); - [Fact] - public void GetEntityCountFunc_ReturnsNull_IfValueIsFalse() - { - // Arrange - var countOption = new CountQueryOption("false", _context); + // Act & Assert + Assert.Null(countOption.GetEntityCount(_customers)); + } - // Act & Assert - Assert.Null(countOption.GetEntityCountFunc(_customers)); - } + [Fact] + public void GetEntityCountFunc_ReturnsFunc_IfValueIsTrue() + { + // Arrange + var countOption = new CountQueryOption("true", _context); - [Fact] - public void Property_Value_WorksWithUnTypedContext() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - CountQueryOption countOption = new CountQueryOption("true", context); + // Act & Assert + Assert.Equal(3, countOption.GetEntityCountFunc(_customers)()); + } - // Act & Assert - Assert.True(countOption.Value); - } + [Fact] + public void GetEntityCountFunc_ReturnsNull_IfValueIsFalse() + { + // Arrange + var countOption = new CountQueryOption("false", _context); - [Fact] - public void GetEntityCount_WithUnTypedContext_Throws_InvalidOperation() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - CountQueryOption countOption = new CountQueryOption("true", context); - IQueryable queryable = new Mock().Object; - - // Act & Assert - ExceptionAssert.Throws(() => countOption.GetEntityCount(queryable), - "The query option is not bound to any CLR type. 'GetEntityCount' is only supported with a query option bound to a CLR type."); - } + // Act & Assert + Assert.Null(countOption.GetEntityCountFunc(_customers)); + } + + [Fact] + public void Property_Value_WorksWithUnTypedContext() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + CountQueryOption countOption = new CountQueryOption("true", context); + + // Act & Assert + Assert.True(countOption.Value); + } + + [Fact] + public void GetEntityCount_WithUnTypedContext_Throws_InvalidOperation() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + CountQueryOption countOption = new CountQueryOption("true", context); + IQueryable queryable = new Mock().Object; + + // Act & Assert + ExceptionAssert.Throws(() => countOption.GetEntityCount(queryable), + "The query option is not bound to any CLR type. 'GetEntityCount' is only supported with a query option bound to a CLR type."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/DefaultSkipTokenHandlerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/DefaultSkipTokenHandlerTests.cs index 64c8e4a04..b0f43e4be 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/DefaultSkipTokenHandlerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/DefaultSkipTokenHandlerTests.cs @@ -19,678 +19,677 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +using Microsoft.AspNetCore.Http; +using System.Runtime.Serialization; + +public class DefaultSkipTokenHandlerTests { - using Microsoft.AspNetCore.Http; - using System.Runtime.Serialization; + private static IEdmModel _model = GetEdmModel(); + + private static IEdmModel _modelLowerCamelCased = GetEdmModelLowerCamelCased(); - public class DefaultSkipTokenHandlerTests + private static IEdmModel _modelAliased = GetEdmModelAliased(); + + [Fact] + public void GenerateNextPageLink_ReturnsNull_NullContext() { - private static IEdmModel _model = GetEdmModel(); + // Arrange + DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); - private static IEdmModel _modelLowerCamelCased = GetEdmModelLowerCamelCased(); + // Act & Assert + Assert.Null(handler.GenerateNextPageLink(null, 2, null, null)); + } - private static IEdmModel _modelAliased = GetEdmModelAliased(); + [Theory] + [InlineData("http://localhost/Customers(1)/Orders", "http://localhost/Customers(1)/Orders?$skip=10")] + [InlineData("http://localhost/Customers?$expand=Orders", "http://localhost/Customers?$expand=Orders&$skip=10")] + public void GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectNextLink(string baseUri, string expectedUri) + { + // Arrange + ODataSerializerContext serializerContext = GetSerializerContext(_model, false); + DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); - [Fact] - public void GenerateNextPageLink_ReturnsNull_NullContext() - { - // Arrange - DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); + // Act + var uri = handler.GenerateNextPageLink(new Uri(baseUri), 10, null, serializerContext); + var actualUri = uri.ToString(); - // Act & Assert - Assert.Null(handler.GenerateNextPageLink(null, 2, null, null)); - } + // Assert + Assert.Equal(expectedUri, actualUri); + } - [Theory] - [InlineData("http://localhost/Customers(1)/Orders", "http://localhost/Customers(1)/Orders?$skip=10")] - [InlineData("http://localhost/Customers?$expand=Orders", "http://localhost/Customers?$expand=Orders&$skip=10")] - public void GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectNextLink(string baseUri, string expectedUri) - { - // Arrange - ODataSerializerContext serializerContext = GetSerializerContext(_model, false); - DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); - - // Act - var uri = handler.GenerateNextPageLink(new Uri(baseUri), 10, null, serializerContext); - var actualUri = uri.ToString(); - - // Assert - Assert.Equal(expectedUri, actualUri); - } - - [Theory] - [InlineData("http://localhost/Customers(1)/Orders", "http://localhost/Customers(1)/Orders?$skiptoken=Id-42")] - [InlineData("http://localhost/Customers?$expand=Orders", "http://localhost/Customers?$expand=Orders&$skiptoken=Id-42")] - [InlineData("http://localhost/Customers?$select=Name", "http://localhost/Customers?$select=Name&$skiptoken=Id-42")] - public void GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink(string baseUri, string expectedUri) - { - this.GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink_Implementation(baseUri, - expectedUri, _model); - } - - [Theory] - [InlineData("http://localhost/Customers(1)/Orders", "http://localhost/Customers(1)/Orders?$skiptoken=id-42")] - [InlineData("http://localhost/Customers?$expand=orders", "http://localhost/Customers?$expand=orders&$skiptoken=id-42")] - [InlineData("http://localhost/Customers?$select=name", "http://localhost/Customers?$select=name&$skiptoken=id-42")] - public void GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink_WithLowerCamelCase(string baseUri, string expectedUri) - { - this.GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink_Implementation(baseUri, - expectedUri, _modelLowerCamelCased); - } - - [Theory] - [InlineData("http://localhost/Customers(1)/Orders", "http://localhost/Customers(1)/Orders?$skiptoken=SkipCustomerId-42")] - [InlineData("http://localhost/Customers?$expand=Orders", "http://localhost/Customers?$expand=Orders&$skiptoken=SkipCustomerId-42")] - [InlineData("http://localhost/Customers?$select=FirstAndLastName", "http://localhost/Customers?$select=FirstAndLastName&$skiptoken=SkipCustomerId-42")] - public void GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink_WithAlias(string baseUri, string expectedUri) - { - this.GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink_Implementation(baseUri, - expectedUri, _modelAliased); - } + [Theory] + [InlineData("http://localhost/Customers(1)/Orders", "http://localhost/Customers(1)/Orders?$skiptoken=Id-42")] + [InlineData("http://localhost/Customers?$expand=Orders", "http://localhost/Customers?$expand=Orders&$skiptoken=Id-42")] + [InlineData("http://localhost/Customers?$select=Name", "http://localhost/Customers?$select=Name&$skiptoken=Id-42")] + public void GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink(string baseUri, string expectedUri) + { + this.GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink_Implementation(baseUri, + expectedUri, _model); + } - [Fact] - public void GenerateSkipTokenValue_Returns_StringEmpty() - { - // Arrange & Act & Assert - Assert.Equal(string.Empty, DefaultSkipTokenHandler.GenerateSkipTokenValue(null, null, null)); - } + [Theory] + [InlineData("http://localhost/Customers(1)/Orders", "http://localhost/Customers(1)/Orders?$skiptoken=id-42")] + [InlineData("http://localhost/Customers?$expand=orders", "http://localhost/Customers?$expand=orders&$skiptoken=id-42")] + [InlineData("http://localhost/Customers?$select=name", "http://localhost/Customers?$select=name&$skiptoken=id-42")] + public void GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink_WithLowerCamelCase(string baseUri, string expectedUri) + { + this.GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink_Implementation(baseUri, + expectedUri, _modelLowerCamelCased); + } - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_Implementation(_model, - "Id-42"); - } + [Theory] + [InlineData("http://localhost/Customers(1)/Orders", "http://localhost/Customers(1)/Orders?$skiptoken=SkipCustomerId-42")] + [InlineData("http://localhost/Customers?$expand=Orders", "http://localhost/Customers?$expand=Orders&$skiptoken=SkipCustomerId-42")] + [InlineData("http://localhost/Customers?$select=FirstAndLastName", "http://localhost/Customers?$select=FirstAndLastName&$skiptoken=SkipCustomerId-42")] + public void GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink_WithAlias(string baseUri, string expectedUri) + { + this.GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink_Implementation(baseUri, + expectedUri, _modelAliased); + } - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithLowerCamelCase() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_Implementation(_modelLowerCamelCased, - "id-42"); - } + [Fact] + public void GenerateSkipTokenValue_Returns_StringEmpty() + { + // Arrange & Act & Assert + Assert.Equal(string.Empty, DefaultSkipTokenHandler.GenerateSkipTokenValue(null, null, null)); + } - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithAlias() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_Implementation(_modelAliased, - "SkipCustomerId-42"); - } + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_Implementation(_model, + "Id-42"); + } - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_Implementation( - _model, - "Name", - "Name-%27ZX%27,Id-42"); - } - - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithLowerCamelCase_WithOrderby() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_Implementation( - _modelLowerCamelCased, - "name", - "name-%27ZX%27,id-42"); - } - - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithAlias_WithOrderby() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_Implementation( - _modelAliased, - "FirstAndLastName", - "FirstAndLastName-%27ZX%27,SkipCustomerId-42"); - } - - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_IfNullValue() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_IfNullValue_Implementation( - _model, - "Name", - "Name-null,Id-42"); - } - - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithLowerCamelCase_WithOrderby_IfNullValue() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_IfNullValue_Implementation( - _modelLowerCamelCased, - "name", - "name-null,id-42"); - } - - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithAlias_WithOrderby_IfNullValue() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_IfNullValue_Implementation( - _modelAliased, - "FirstAndLastName", - "FirstAndLastName-null,SkipCustomerId-42"); - } - - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithDateTimeOffset() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_WithDateTimeOffset_Implementation( - _model, - "Birthday", - "Birthday-2021-01-19T19%3A04%3A05-08%3A00,Id-42"); - } - - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithLowerCamelCase_WithDateTimeOffset() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_WithDateTimeOffset_Implementation( - _modelLowerCamelCased, - "birthday", - "birthday-2021-01-19T19%3A04%3A05-08%3A00,id-42"); - } - - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithAlias_WithDateTimeOffset() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_WithDateTimeOffset_Implementation( - _modelAliased, - "DateOfBirth", - "DateOfBirth-2021-01-19T19%3A04%3A05-08%3A00,SkipCustomerId-42"); - } - - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_WithEnumValue() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_WithEnumValue_Implementation( - _model, - "Gender", - "Gender-Microsoft.AspNetCore.OData.Tests.Query.DefaultSkipTokenHandlerTests%2BGender%27Male%27,Id-42"); - } - - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithLowerCamelCase_WithOrderby_WithEnumValue() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_WithEnumValue_Implementation( - _modelLowerCamelCased, - "gender", - "gender-Microsoft.AspNetCore.OData.Tests.Query.DefaultSkipTokenHandlerTests%2BGender%27Male%27,id-42"); - } - - [Fact] - public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithAlias_WithOrderby_WithEnumValue() - { - GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_WithEnumValue_Implementation( - _modelAliased, - "MaleOrFemale", - "MaleOrFemale-Microsoft.AspNetCore.OData.Tests.Query.DefaultSkipTokenHandlerTests%2BGender%27Male%27,SkipCustomerId-42"); - } - - [Fact] - public void ApplyToSkipTokenHandler_ThrowsArgumentNull_Query() - { - // Arrange - DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithLowerCamelCase() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_Implementation(_modelLowerCamelCased, + "id-42"); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => handler.ApplyTo(query: null, null, null, null), "query"); - } + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithAlias() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_Implementation(_modelAliased, + "SkipCustomerId-42"); + } - [Fact] - public void ApplyToSkipTokenHandler_ThrowsArgumentNull_SkipTokenQueryOption() - { - // Arrange - DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); - IQueryable query = Array.Empty().AsQueryable(); + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_Implementation( + _model, + "Name", + "Name-%27ZX%27,Id-42"); + } + + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithLowerCamelCase_WithOrderby() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_Implementation( + _modelLowerCamelCased, + "name", + "name-%27ZX%27,id-42"); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => handler.ApplyTo(query, null, null, null), "skipTokenQueryOption"); - } + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithAlias_WithOrderby() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_Implementation( + _modelAliased, + "FirstAndLastName", + "FirstAndLastName-%27ZX%27,SkipCustomerId-42"); + } - [Fact] - public void ApplyToSkipTokenHandler_ThrowsArgumentNull_QuerySettings() - { - // Arrange - DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); - IQueryable query = Array.Empty().AsQueryable(); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, EdmCoreModel.Instance.GetInt32(false).Definition); - SkipTokenQueryOption skipTokenQueryOption = new SkipTokenQueryOption("abc", context); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => handler.ApplyTo(query, skipTokenQueryOption, null, null), "querySettings"); - } - - [Fact] - public void ApplyToSkipTokenHandler_ThrowsNotSupported_WithoutElementType() - { - // Arrange - DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, EdmCoreModel.Instance.GetInt32(false).Definition); - SkipTokenQueryOption skipTokenQueryOption = new SkipTokenQueryOption("abc", context); - IQueryable query = Array.Empty().AsQueryable(); - - // Act & Assert - ExceptionAssert.Throws( - () => handler.ApplyTo(query, skipTokenQueryOption, new ODataQuerySettings(), null), - "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); - } - - [Fact] - public void ApplyToSkipTokenHandler_ThrowsODataException_InvalidSkipTokenValue() - { - ApplyToSkipTokenHandler_ThrowsODataException_InvalidSkipTokenValue_Implementation(_model); - } + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_IfNullValue() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_IfNullValue_Implementation( + _model, + "Name", + "Name-null,Id-42"); + } - [Fact] - public void ApplyToSkipTokenHandler_ThrowsODataException_WithLowerCamelCase_InvalidSkipTokenValue() - { - ApplyToSkipTokenHandler_ThrowsODataException_InvalidSkipTokenValue_Implementation(_modelLowerCamelCased); - } + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithLowerCamelCase_WithOrderby_IfNullValue() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_IfNullValue_Implementation( + _modelLowerCamelCased, + "name", + "name-null,id-42"); + } + + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithAlias_WithOrderby_IfNullValue() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_IfNullValue_Implementation( + _modelAliased, + "FirstAndLastName", + "FirstAndLastName-null,SkipCustomerId-42"); + } + + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithDateTimeOffset() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_WithDateTimeOffset_Implementation( + _model, + "Birthday", + "Birthday-2021-01-19T19%3A04%3A05-08%3A00,Id-42"); + } + + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithLowerCamelCase_WithDateTimeOffset() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_WithDateTimeOffset_Implementation( + _modelLowerCamelCased, + "birthday", + "birthday-2021-01-19T19%3A04%3A05-08%3A00,id-42"); + } + + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithAlias_WithDateTimeOffset() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_WithDateTimeOffset_Implementation( + _modelAliased, + "DateOfBirth", + "DateOfBirth-2021-01-19T19%3A04%3A05-08%3A00,SkipCustomerId-42"); + } + + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_WithEnumValue() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_WithEnumValue_Implementation( + _model, + "Gender", + "Gender-Microsoft.AspNetCore.OData.Tests.Query.DefaultSkipTokenHandlerTests%2BGender%27Male%27,Id-42"); + } + + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithLowerCamelCase_WithOrderby_WithEnumValue() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_WithEnumValue_Implementation( + _modelLowerCamelCased, + "gender", + "gender-Microsoft.AspNetCore.OData.Tests.Query.DefaultSkipTokenHandlerTests%2BGender%27Male%27,id-42"); + } + + [Fact] + public void GenerateSkipTokenValue_Returns_SkipTokenValue_WithAlias_WithOrderby_WithEnumValue() + { + GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_WithEnumValue_Implementation( + _modelAliased, + "MaleOrFemale", + "MaleOrFemale-Microsoft.AspNetCore.OData.Tests.Query.DefaultSkipTokenHandlerTests%2BGender%27Male%27,SkipCustomerId-42"); + } + + [Fact] + public void ApplyToSkipTokenHandler_ThrowsArgumentNull_Query() + { + // Arrange + DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => handler.ApplyTo(query: null, null, null, null), "query"); + } + + [Fact] + public void ApplyToSkipTokenHandler_ThrowsArgumentNull_SkipTokenQueryOption() + { + // Arrange + DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); + IQueryable query = Array.Empty().AsQueryable(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => handler.ApplyTo(query, null, null, null), "skipTokenQueryOption"); + } + + [Fact] + public void ApplyToSkipTokenHandler_ThrowsArgumentNull_QuerySettings() + { + // Arrange + DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); + IQueryable query = Array.Empty().AsQueryable(); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, EdmCoreModel.Instance.GetInt32(false).Definition); + SkipTokenQueryOption skipTokenQueryOption = new SkipTokenQueryOption("abc", context); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => handler.ApplyTo(query, skipTokenQueryOption, null, null), "querySettings"); + } + + [Fact] + public void ApplyToSkipTokenHandler_ThrowsNotSupported_WithoutElementType() + { + // Arrange + DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, EdmCoreModel.Instance.GetInt32(false).Definition); + SkipTokenQueryOption skipTokenQueryOption = new SkipTokenQueryOption("abc", context); + IQueryable query = Array.Empty().AsQueryable(); + + // Act & Assert + ExceptionAssert.Throws( + () => handler.ApplyTo(query, skipTokenQueryOption, new ODataQuerySettings(), null), + "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); + } + + [Fact] + public void ApplyToSkipTokenHandler_ThrowsODataException_InvalidSkipTokenValue() + { + ApplyToSkipTokenHandler_ThrowsODataException_InvalidSkipTokenValue_Implementation(_model); + } + + [Fact] + public void ApplyToSkipTokenHandler_ThrowsODataException_WithLowerCamelCase_InvalidSkipTokenValue() + { + ApplyToSkipTokenHandler_ThrowsODataException_InvalidSkipTokenValue_Implementation(_modelLowerCamelCased); + } - [Fact] - public void ApplyToSkipTokenHandler_ThrowsODataException_WithAlias_InvalidSkipTokenValue() - { - ApplyToSkipTokenHandler_ThrowsODataException_InvalidSkipTokenValue_Implementation(_modelAliased); - } + [Fact] + public void ApplyToSkipTokenHandler_ThrowsODataException_WithAlias_InvalidSkipTokenValue() + { + ApplyToSkipTokenHandler_ThrowsODataException_InvalidSkipTokenValue_Implementation(_modelAliased); + } - [Fact] - public void ApplyToOfTDefaultSkipTokenHandler_Applies_ToQueryable() - { - ApplyToOfTDefaultSkipTokenHandler_Applies_ToQueryable_Implementation( - _model, - "Id-2"); - } + [Fact] + public void ApplyToOfTDefaultSkipTokenHandler_Applies_ToQueryable() + { + ApplyToOfTDefaultSkipTokenHandler_Applies_ToQueryable_Implementation( + _model, + "Id-2"); + } - [Fact] - public void ApplyToOfTDefaultSkipTokenHandler_Applies_WithLowerCamelCase_ToQueryable() - { - ApplyToOfTDefaultSkipTokenHandler_Applies_ToQueryable_Implementation( - _modelLowerCamelCased, - "id-2"); - } + [Fact] + public void ApplyToOfTDefaultSkipTokenHandler_Applies_WithLowerCamelCase_ToQueryable() + { + ApplyToOfTDefaultSkipTokenHandler_Applies_ToQueryable_Implementation( + _modelLowerCamelCased, + "id-2"); + } - [Fact] - public void ApplyToOfTDefaultSkipTokenHandler_Applies_WithAlias_ToQueryable() - { - ApplyToOfTDefaultSkipTokenHandler_Applies_ToQueryable_Implementation( - _modelAliased, - "SkipCustomerId-2"); - } + [Fact] + public void ApplyToOfTDefaultSkipTokenHandler_Applies_WithAlias_ToQueryable() + { + ApplyToOfTDefaultSkipTokenHandler_Applies_ToQueryable_Implementation( + _modelAliased, + "SkipCustomerId-2"); + } - [Fact] - public void ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_ToQueryable() - { - ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_ToQueryable_Implementation( - _model, - "Name", - "Name-'Alex',Id-3"); - } - - [Fact] - public void ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_WithLowerCamelCase_ToQueryable() - { - ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_ToQueryable_Implementation( - _modelLowerCamelCased, - "name", - "name-'Alex',Id-3"); - } - - [Fact] - public void ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_WithAlias_ToQueryable() - { - ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_ToQueryable_Implementation( - _modelAliased, - "FirstAndLastName", - "FirstAndLastName-'Alex',SkipCustomerId-3"); - } + [Fact] + public void ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_ToQueryable() + { + ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_ToQueryable_Implementation( + _model, + "Name", + "Name-'Alex',Id-3"); + } - private ODataSerializerContext GetSerializerContext(IEdmModel model, bool enableSkipToken = false) - { - IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers"); - IEdmEntityType entityType = entitySet.EntityType; - IEdmProperty edmProperty = entityType.FindProperty("Name"); - IEdmType edmType = entitySet.Type; - ODataPath path = new ODataPath(new EntitySetSegment(entitySet)); - ODataQueryContext queryContext = new ODataQueryContext(model, edmType, path); - queryContext.DefaultQueryConfigurations.EnableSkipToken = enableSkipToken; - - var request = RequestFactory.Create(opt => opt.AddRouteComponents(model)); - ResourceContext resource = new ResourceContext(); - ODataSerializerContext context = new ODataSerializerContext(resource, edmProperty, queryContext, null) - { - Model = model - }; + [Fact] + public void ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_WithLowerCamelCase_ToQueryable() + { + ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_ToQueryable_Implementation( + _modelLowerCamelCased, + "name", + "name-'Alex',Id-3"); + } - return context; - } + [Fact] + public void ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_WithAlias_ToQueryable() + { + ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_ToQueryable_Implementation( + _modelAliased, + "FirstAndLastName", + "FirstAndLastName-'Alex',SkipCustomerId-3"); + } - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder {ModelAliasingEnabled = false}; - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } + private ODataSerializerContext GetSerializerContext(IEdmModel model, bool enableSkipToken = false) + { + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers"); + IEdmEntityType entityType = entitySet.EntityType; + IEdmProperty edmProperty = entityType.FindProperty("Name"); + IEdmType edmType = entitySet.Type; + ODataPath path = new ODataPath(new EntitySetSegment(entitySet)); + ODataQueryContext queryContext = new ODataQueryContext(model, edmType, path); + queryContext.DefaultQueryConfigurations.EnableSkipToken = enableSkipToken; + + var request = RequestFactory.Create(opt => opt.AddRouteComponents(model)); + ResourceContext resource = new ResourceContext(); + ODataSerializerContext context = new ODataSerializerContext(resource, edmProperty, queryContext, null) + { + Model = model + }; + + return context; + } - private static IEdmModel GetEdmModelLowerCamelCased() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder {ModelAliasingEnabled = false}; - builder.EntitySet("Customers"); - builder.EnableLowerCamelCase(); - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder {ModelAliasingEnabled = false}; + builder.EntitySet("Customers"); + return builder.GetEdmModel(); + } - private static IEdmModel GetEdmModelAliased() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder {ModelAliasingEnabled = true}; - var entitySetConfiguration = builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModelLowerCamelCased() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder {ModelAliasingEnabled = false}; + builder.EntitySet("Customers"); + builder.EnableLowerCamelCase(); + return builder.GetEdmModel(); + } + + private static IEdmModel GetEdmModelAliased() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder {ModelAliasingEnabled = true}; + var entitySetConfiguration = builder.EntitySet("Customers"); + return builder.GetEdmModel(); + } - private void GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink_Implementation( - string baseUri, - string expectedUri, - IEdmModel edmModel) - { - // Arrange - ODataSerializerContext serializerContext = this.GetSerializerContext( - edmModel, - true); - DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); - SkipCustomer instance = new SkipCustomer {Id = 42, Name = "ZX"}; - - // Act - Uri uri = handler.GenerateNextPageLink( - new Uri(baseUri), - 10, - instance, - serializerContext); - var actualUri = uri.ToString(); - - // Assert - Assert.Equal( - expectedUri, - actualUri); - } + private void GetNextPageLinkDefaultSkipTokenHandler_Returns_CorrectSkipTokenLink_Implementation( + string baseUri, + string expectedUri, + IEdmModel edmModel) + { + // Arrange + ODataSerializerContext serializerContext = this.GetSerializerContext( + edmModel, + true); + DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); + SkipCustomer instance = new SkipCustomer {Id = 42, Name = "ZX"}; + + // Act + Uri uri = handler.GenerateNextPageLink( + new Uri(baseUri), + 10, + instance, + serializerContext); + var actualUri = uri.ToString(); + + // Assert + Assert.Equal( + expectedUri, + actualUri); + } - private static void GenerateSkipTokenValue_Returns_SkipTokenValue_Implementation(IEdmModel edmModel, string expectedSkipToken) - { - // Arrange - SkipCustomer lastMember = new SkipCustomer {Id = 42, Name = "ZX"}; - - // Act - string skipTokenValue = DefaultSkipTokenHandler.GenerateSkipTokenValue( - lastMember, - edmModel, - null - ); - - // Assert - Assert.Equal( - expectedSkipToken, - skipTokenValue); - } - - private static void GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_Implementation( - IEdmModel edmModel, - string propertyName, - string expectedSkipToken) - { - // Arrange - SkipCustomer lastMember = new SkipCustomer {Id = 42, Name = "ZX"}; + private static void GenerateSkipTokenValue_Returns_SkipTokenValue_Implementation(IEdmModel edmModel, string expectedSkipToken) + { + // Arrange + SkipCustomer lastMember = new SkipCustomer {Id = 42, Name = "ZX"}; + + // Act + string skipTokenValue = DefaultSkipTokenHandler.GenerateSkipTokenValue( + lastMember, + edmModel, + null + ); + + // Assert + Assert.Equal( + expectedSkipToken, + skipTokenValue); + } - IEdmEntityType entityType = edmModel.SchemaElements.OfType() - .First(c => c.Name == "SkipCustomer"); - IEdmProperty property = entityType.FindProperty(propertyName); + private static void GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_Implementation( + IEdmModel edmModel, + string propertyName, + string expectedSkipToken) + { + // Arrange + SkipCustomer lastMember = new SkipCustomer {Id = 42, Name = "ZX"}; - OrderByClause clause = BuildOrderByClause(edmModel, propertyName); + IEdmEntityType entityType = edmModel.SchemaElements.OfType() + .First(c => c.Name == "SkipCustomer"); + IEdmProperty property = entityType.FindProperty(propertyName); - // Act - string skipTokenValue = DefaultSkipTokenHandler.GenerateSkipTokenValue( - lastMember, - edmModel, - clause); + OrderByClause clause = BuildOrderByClause(edmModel, propertyName); - // Assert - Assert.Equal( - expectedSkipToken, - skipTokenValue); - } + // Act + string skipTokenValue = DefaultSkipTokenHandler.GenerateSkipTokenValue( + lastMember, + edmModel, + clause); - private static OrderByClause BuildOrderByClause(IEdmModel edmModel, string propertyName) - { - IEdmEntityType entityType = edmModel.SchemaElements.OfType() - .First(c => c.Name == "SkipCustomer"); - IEdmProperty property = entityType.FindProperty(propertyName); - - IEdmNavigationSource entitySet = edmModel.FindDeclaredEntitySet("Customers"); - ResourceRangeVariable rangeVariable = new ResourceRangeVariable("$it", new EdmEntityTypeReference(entityType, true), entitySet); - - ResourceRangeVariableReferenceNode source = new ResourceRangeVariableReferenceNode("$it", rangeVariable); - SingleValuePropertyAccessNode node = new SingleValuePropertyAccessNode(source, property); - return new OrderByClause(null, node, OrderByDirection.Ascending, rangeVariable); - } - - private static void GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_IfNullValue_Implementation( - IEdmModel edmModel, - string propertyName, - string expectedSkipToken) - { - // Arrange - SkipCustomer lastMember = new SkipCustomer {Id = 42, Name = null}; - - OrderByClause clause = BuildOrderByClause(edmModel, propertyName); - - // Act - string skipTokenValue = DefaultSkipTokenHandler.GenerateSkipTokenValue( - lastMember, - edmModel, - clause); - - // Assert - Assert.Equal( - expectedSkipToken, - skipTokenValue); - } - - private static void GenerateSkipTokenValue_Returns_SkipTokenValue_WithDateTimeOffset_Implementation( - IEdmModel edmModel, - string propertyName, - string expectedSkipToken) - { - // Arrange - SkipCustomer lastMember = new SkipCustomer - { - Id = 42, - Birthday = new DateTime( - 2021, - 01, - 20, - 3, - 4, - 5, - DateTimeKind.Utc) - }; - - TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); // -8 - IEdmEntityType entityType = edmModel.SchemaElements.OfType() - .First(c => c.Name == "SkipCustomer"); - - OrderByClause clause = BuildOrderByClause(edmModel, propertyName); - ODataSerializerContext context = new ODataSerializerContext() + // Assert + Assert.Equal( + expectedSkipToken, + skipTokenValue); + } + + private static OrderByClause BuildOrderByClause(IEdmModel edmModel, string propertyName) + { + IEdmEntityType entityType = edmModel.SchemaElements.OfType() + .First(c => c.Name == "SkipCustomer"); + IEdmProperty property = entityType.FindProperty(propertyName); + + IEdmNavigationSource entitySet = edmModel.FindDeclaredEntitySet("Customers"); + ResourceRangeVariable rangeVariable = new ResourceRangeVariable("$it", new EdmEntityTypeReference(entityType, true), entitySet); + + ResourceRangeVariableReferenceNode source = new ResourceRangeVariableReferenceNode("$it", rangeVariable); + SingleValuePropertyAccessNode node = new SingleValuePropertyAccessNode(source, property); + return new OrderByClause(null, node, OrderByDirection.Ascending, rangeVariable); + } + + private static void GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_IfNullValue_Implementation( + IEdmModel edmModel, + string propertyName, + string expectedSkipToken) + { + // Arrange + SkipCustomer lastMember = new SkipCustomer {Id = 42, Name = null}; + + OrderByClause clause = BuildOrderByClause(edmModel, propertyName); + + // Act + string skipTokenValue = DefaultSkipTokenHandler.GenerateSkipTokenValue( + lastMember, + edmModel, + clause); + + // Assert + Assert.Equal( + expectedSkipToken, + skipTokenValue); + } + + private static void GenerateSkipTokenValue_Returns_SkipTokenValue_WithDateTimeOffset_Implementation( + IEdmModel edmModel, + string propertyName, + string expectedSkipToken) + { + // Arrange + SkipCustomer lastMember = new SkipCustomer { - TimeZone = timeZone + Id = 42, + Birthday = new DateTime( + 2021, + 01, + 20, + 3, + 4, + 5, + DateTimeKind.Utc) }; - // Act - string skipTokenValue = DefaultSkipTokenHandler.GenerateSkipTokenValue( - lastMember, - edmModel, - clause, - context); - - // Assert - Assert.Equal( - expectedSkipToken, - skipTokenValue); - } - - private static void GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_WithEnumValue_Implementation( - IEdmModel edmModel, - string propertyName, - string expectedSkipToken) + TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); // -8 + IEdmEntityType entityType = edmModel.SchemaElements.OfType() + .First(c => c.Name == "SkipCustomer"); + + OrderByClause clause = BuildOrderByClause(edmModel, propertyName); + ODataSerializerContext context = new ODataSerializerContext() { - // Arrange - SkipCustomer lastMember = new SkipCustomer {Id = 42, Name = "ZX", Gender = Gender.Male}; + TimeZone = timeZone + }; + + // Act + string skipTokenValue = DefaultSkipTokenHandler.GenerateSkipTokenValue( + lastMember, + edmModel, + clause, + context); - OrderByClause clause = BuildOrderByClause(edmModel, propertyName); + // Assert + Assert.Equal( + expectedSkipToken, + skipTokenValue); + } + + private static void GenerateSkipTokenValue_Returns_SkipTokenValue_WithOrderby_WithEnumValue_Implementation( + IEdmModel edmModel, + string propertyName, + string expectedSkipToken) + { + // Arrange + SkipCustomer lastMember = new SkipCustomer {Id = 42, Name = "ZX", Gender = Gender.Male}; - // Act - string skipTokenValue = DefaultSkipTokenHandler.GenerateSkipTokenValue( - lastMember, - edmModel, - clause); + OrderByClause clause = BuildOrderByClause(edmModel, propertyName); - // Assert - Assert.Equal( - expectedSkipToken, - skipTokenValue); - } + // Act + string skipTokenValue = DefaultSkipTokenHandler.GenerateSkipTokenValue( + lastMember, + edmModel, + clause); - private static void ApplyToSkipTokenHandler_ThrowsODataException_InvalidSkipTokenValue_Implementation( - IEdmModel edmModel) - { - // Arrange - DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); - ODataQuerySettings settings = new ODataQuerySettings {HandleNullPropagation = HandleNullPropagationOption.False}; - ODataQueryContext context = new ODataQueryContext( - edmModel, - typeof(SkipCustomer)); - HttpRequest request = RequestFactory.Create("Get", "http://localhost/"); - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - - SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption( - "abc", - context); - IQueryable customers = new List().AsQueryable(); - - // Act & Assert - ExceptionAssert.Throws( - () => handler.ApplyTo( - customers, - skipTokenQuery, - new ODataQuerySettings(), - queryOptions), - "Could not find a property named 'abc' on type 'Microsoft.AspNetCore.OData.Tests.Query.SkipCustomer'."); - } + // Assert + Assert.Equal( + expectedSkipToken, + skipTokenValue); + } + + private static void ApplyToSkipTokenHandler_ThrowsODataException_InvalidSkipTokenValue_Implementation( + IEdmModel edmModel) + { + // Arrange + DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); + ODataQuerySettings settings = new ODataQuerySettings {HandleNullPropagation = HandleNullPropagationOption.False}; + ODataQueryContext context = new ODataQueryContext( + edmModel, + typeof(SkipCustomer)); + HttpRequest request = RequestFactory.Create("Get", "http://localhost/"); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + + SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption( + "abc", + context); + IQueryable customers = new List().AsQueryable(); + + // Act & Assert + ExceptionAssert.Throws( + () => handler.ApplyTo( + customers, + skipTokenQuery, + new ODataQuerySettings(), + queryOptions), + "Could not find a property named 'abc' on type 'Microsoft.AspNetCore.OData.Tests.Query.SkipCustomer'."); + } - private static void ApplyToOfTDefaultSkipTokenHandler_Applies_ToQueryable_Implementation( - IEdmModel edmModel, - string skipTokenQueryOptionRawValue) - { - // Arrange - ODataQuerySettings settings = new ODataQuerySettings {HandleNullPropagation = HandleNullPropagationOption.False}; - ODataQueryContext context = new ODataQueryContext( - edmModel, - typeof(SkipCustomer)); - HttpRequest request = RequestFactory.Create("Get", "http://localhost/"); - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - - SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption( - skipTokenQueryOptionRawValue, - context); - DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); - IQueryable customers = new List - { - new SkipCustomer {Id = 2, Name = "Aaron"}, - new SkipCustomer {Id = 1, Name = "Andy"}, - new SkipCustomer {Id = 3, Name = "Alex"} - }.AsQueryable(); - - // Act - SkipCustomer[] results = handler.ApplyTo( - customers, - skipTokenQuery, - settings, - queryOptions) - .ToArray(); - - // Assert - SkipCustomer skipTokenCustomer = Assert.Single(results); - Assert.Equal( - 3, - skipTokenCustomer.Id); - Assert.Equal( - "Alex", - skipTokenCustomer.Name); - } - - private static void ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_ToQueryable_Implementation( - IEdmModel edmModel, - string orderByPropertyName, - string skipTokenQueryOptionRawValue) - { - // Arrange - ODataQuerySettings settings = new ODataQuerySettings {HandleNullPropagation = HandleNullPropagationOption.False}; - ODataQueryContext context = new ODataQueryContext( - edmModel, - typeof(SkipCustomer)); - - HttpRequest request = RequestFactory.Create( - HttpMethods.Get, - $"http://server/service/Customers/?$orderby={orderByPropertyName} desc&$skiptoken={skipTokenQueryOptionRawValue}"); - - // Act - ODataQueryOptions oDataQueryOptions = new ODataQueryOptions(context, request); - - SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption( - skipTokenQueryOptionRawValue, - context); - DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); - IQueryable customers = new List - { - new SkipCustomer {Id = 2, Name = "Aaron"}, - new SkipCustomer {Id = 1, Name = "Andy"}, - new SkipCustomer {Id = 3, Name = "Alex"} - }.AsQueryable(); - - // Act - SkipCustomer[] results = handler.ApplyTo( - customers, - skipTokenQuery, - settings, - oDataQueryOptions) - .ToArray(); - - // Assert - SkipCustomer skipTokenCustomer = Assert.Single(results); - Assert.Equal( - 2, - skipTokenCustomer.Id); - Assert.Equal( - "Aaron", - skipTokenCustomer.Name); - } - - [DataContract] - public class SkipCustomer - { - [DataMember(Name = "SkipCustomerId")] - public int Id { get; set; } + private static void ApplyToOfTDefaultSkipTokenHandler_Applies_ToQueryable_Implementation( + IEdmModel edmModel, + string skipTokenQueryOptionRawValue) + { + // Arrange + ODataQuerySettings settings = new ODataQuerySettings {HandleNullPropagation = HandleNullPropagationOption.False}; + ODataQueryContext context = new ODataQueryContext( + edmModel, + typeof(SkipCustomer)); + HttpRequest request = RequestFactory.Create("Get", "http://localhost/"); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + + SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption( + skipTokenQueryOptionRawValue, + context); + DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); + IQueryable customers = new List + { + new SkipCustomer {Id = 2, Name = "Aaron"}, + new SkipCustomer {Id = 1, Name = "Andy"}, + new SkipCustomer {Id = 3, Name = "Alex"} + }.AsQueryable(); + + // Act + SkipCustomer[] results = handler.ApplyTo( + customers, + skipTokenQuery, + settings, + queryOptions) + .ToArray(); + + // Assert + SkipCustomer skipTokenCustomer = Assert.Single(results); + Assert.Equal( + 3, + skipTokenCustomer.Id); + Assert.Equal( + "Alex", + skipTokenCustomer.Name); + } - [DataMember(Name = "FirstAndLastName")] - public string Name { get; set; } + private static void ApplyToOfTDefaultSkipTokenHandler_Applies_WithOrderByDesc_ToQueryable_Implementation( + IEdmModel edmModel, + string orderByPropertyName, + string skipTokenQueryOptionRawValue) + { + // Arrange + ODataQuerySettings settings = new ODataQuerySettings {HandleNullPropagation = HandleNullPropagationOption.False}; + ODataQueryContext context = new ODataQueryContext( + edmModel, + typeof(SkipCustomer)); + + HttpRequest request = RequestFactory.Create( + HttpMethods.Get, + $"http://server/service/Customers/?$orderby={orderByPropertyName} desc&$skiptoken={skipTokenQueryOptionRawValue}"); + + // Act + ODataQueryOptions oDataQueryOptions = new ODataQueryOptions(context, request); + + SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption( + skipTokenQueryOptionRawValue, + context); + DefaultSkipTokenHandler handler = new DefaultSkipTokenHandler(); + IQueryable customers = new List + { + new SkipCustomer {Id = 2, Name = "Aaron"}, + new SkipCustomer {Id = 1, Name = "Andy"}, + new SkipCustomer {Id = 3, Name = "Alex"} + }.AsQueryable(); + + // Act + SkipCustomer[] results = handler.ApplyTo( + customers, + skipTokenQuery, + settings, + oDataQueryOptions) + .ToArray(); + + // Assert + SkipCustomer skipTokenCustomer = Assert.Single(results); + Assert.Equal( + 2, + skipTokenCustomer.Id); + Assert.Equal( + "Aaron", + skipTokenCustomer.Name); + } - [DataMember(Name = "DateOfBirth")] - public DateTime Birthday { get; set; } + [DataContract] + public class SkipCustomer + { + [DataMember(Name = "SkipCustomerId")] + public int Id { get; set; } - [DataMember(Name = "MaleOrFemale")] - public Gender Gender { get; set; } - } + [DataMember(Name = "FirstAndLastName")] + public string Name { get; set; } - public enum Gender - { - Male, + [DataMember(Name = "DateOfBirth")] + public DateTime Birthday { get; set; } + + [DataMember(Name = "MaleOrFemale")] + public Gender Gender { get; set; } + } + + public enum Gender + { + Male, - Female - } + Female } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/FilterQueryOptionTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/FilterQueryOptionTest.cs index 65e1e684a..d6dd7787e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/FilterQueryOptionTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/FilterQueryOptionTest.cs @@ -19,962 +19,961 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class FilterQueryOptionTest { - public class FilterQueryOptionTest + // Legal filter queries usable against CustomerFilterTestData. + // Tuple is: filter, expected list of customer ID's + public static TheoryDataSet CustomerTestFilters { - // Legal filter queries usable against CustomerFilterTestData. - // Tuple is: filter, expected list of customer ID's - public static TheoryDataSet CustomerTestFilters + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - // Primitive properties - { "Name eq 'Highest'", new int[] { 2 } }, - { "endswith(Name, 'est')", new int[] { 1, 2 } }, - - // Complex properties - { "Address/City eq 'redmond'", new int[] { 1 } }, - { "contains(Address/City, 'e')", new int[] { 1, 2 } }, - - // Primitive property collections - { "Aliases/any(alias: alias eq 'alias34')", new int[] { 3, 4 } }, - { "Aliases/any(alias: alias eq 'alias4')", new int[] { 4 } }, - { "Aliases/all(alias: alias eq 'alias2')", new int[] { 2 } }, - - // Navigational properties - { "Orders/any(order: order/OrderId eq 12)", new int[] { 1 } }, - - // Collection count - { "Orders/$count eq 2", new int[] { 1 } }, - { "Addresses/$count ge 1", new int[] { 1 } }, - { "Aliases/$count gt 2", new int[] { 3 } }, - - // case insensitive forms - { "NaME eq 'Highest'", new int[] { 2 } }, - { "AdDrEsS/CiTy eq 'redmond'", new int[] { 1 } }, - { "AlIaSeS/any(alias: alias eq 'alias34')", new int[] { 3, 4 } }, - { "OrDeRs/any(order: order/OrDeRId eq 12)", new int[] { 1 } }, - - }; - } + // Primitive properties + { "Name eq 'Highest'", new int[] { 2 } }, + { "endswith(Name, 'est')", new int[] { 1, 2 } }, + + // Complex properties + { "Address/City eq 'redmond'", new int[] { 1 } }, + { "contains(Address/City, 'e')", new int[] { 1, 2 } }, + + // Primitive property collections + { "Aliases/any(alias: alias eq 'alias34')", new int[] { 3, 4 } }, + { "Aliases/any(alias: alias eq 'alias4')", new int[] { 4 } }, + { "Aliases/all(alias: alias eq 'alias2')", new int[] { 2 } }, + + // Navigational properties + { "Orders/any(order: order/OrderId eq 12)", new int[] { 1 } }, + + // Collection count + { "Orders/$count eq 2", new int[] { 1 } }, + { "Addresses/$count ge 1", new int[] { 1 } }, + { "Aliases/$count gt 2", new int[] { 3 } }, + + // case insensitive forms + { "NaME eq 'Highest'", new int[] { 2 } }, + { "AdDrEsS/CiTy eq 'redmond'", new int[] { 1 } }, + { "AlIaSeS/any(alias: alias eq 'alias34')", new int[] { 3, 4 } }, + { "OrDeRs/any(order: order/OrDeRId eq 12)", new int[] { 1 } }, + + }; } + } - // Test data used by CustomerTestFilters TheoryDataSet - public static List CustomerFilterTestData + // Test data used by CustomerTestFilters TheoryDataSet + public static List CustomerFilterTestData + { + get { - get - { - List customerList = new List(); - - Customer c = new Customer - { - Id = 1, - Name = "Lowest", - Address = new Address { City = "redmond" }, - }; - c.Orders = new List - { - new Order { OrderId = 11, Customer = c }, - new Order { OrderId = 12, Customer = c }, - }; - c.Addresses = new List
- { - new Address {City = "Shanghai"} - }; - customerList.Add(c); + List customerList = new List(); - c = new Customer - { - Id = 2, - Name = "Highest", - Address = new Address { City = "seattle" }, - Aliases = new List { "alias2", "alias2" } - }; - customerList.Add(c); + Customer c = new Customer + { + Id = 1, + Name = "Lowest", + Address = new Address { City = "redmond" }, + }; + c.Orders = new List + { + new Order { OrderId = 11, Customer = c }, + new Order { OrderId = 12, Customer = c }, + }; + c.Addresses = new List
+ { + new Address {City = "Shanghai"} + }; + customerList.Add(c); - c = new Customer - { - Id = 3, - Name = "Middle", - Address = new Address { City = "hobart" }, - Aliases = new List { "alias2", "alias34", "alias31" } - }; - customerList.Add(c); + c = new Customer + { + Id = 2, + Name = "Highest", + Address = new Address { City = "seattle" }, + Aliases = new List { "alias2", "alias2" } + }; + customerList.Add(c); + + c = new Customer + { + Id = 3, + Name = "Middle", + Address = new Address { City = "hobart" }, + Aliases = new List { "alias2", "alias34", "alias31" } + }; + customerList.Add(c); + + c = new Customer + { + Id = 4, + Name = "NewLow", + Aliases = new List { "alias34", "alias4" } + }; + customerList.Add(c); - c = new Customer - { - Id = 4, - Name = "NewLow", - Aliases = new List { "alias34", "alias4" } - }; - customerList.Add(c); - - return customerList; - } + return customerList; } + } - // Legal filter queries usable against EnumModelTestData. - // Tuple is: filter, expected list of customer ID's - public static TheoryDataSet EnumModelTestFilters + // Legal filter queries usable against EnumModelTestData. + // Tuple is: filter, expected list of customer ID's + public static TheoryDataSet EnumModelTestFilters + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - // Simple Enums - { "Simple eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", new int[] { 1, 3 } }, - { "Simple eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'0'", new int[] { 1, 3 } }, - { "Simple eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'Fourth'", new int[] { } }, - { "Simple eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'3'", new int[] { } }, - { "Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First' eq Simple", new int[] { 1, 3 } }, - { "Simple eq cast('0',Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum)", new int[] { 1, 3} }, - { "Simple eq cast('First','Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", new int[] { 1, 3} }, - { "Simple eq null", new int[] { } }, - { "null eq Simple", new int[] { } }, - { "Simple eq SimpleNullable", new int[] { 1 } }, - { "Simple has Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", new int[] { 1, 2, 3, 5, 6 } }, - { "Simple has Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'0'", new int[] { 1, 2, 3, 5, 6 } }, - { "Simple has Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'Second'", new int[] { 5 } }, - { "SimpleNullable eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", new int[] { 1 } }, - { "Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First' eq SimpleNullable", new int[] { 1 } }, - { "SimpleNullable eq null", new int[] { 3, 5 } }, - { "null eq SimpleNullable", new int[] { 3, 5 } }, - - // Long enums - { "Long eq Microsoft.AspNetCore.OData.Tests.Models.LongEnum'SecondLong'", new int[] { 2 } }, - { "Long eq Microsoft.AspNetCore.OData.Tests.Models.LongEnum'FourthLong'", new int[] { } }, - { "Long eq Microsoft.AspNetCore.OData.Tests.Models.LongEnum'3'", new int[] { } }, - - // Byte enums - { "Byte eq Microsoft.AspNetCore.OData.Tests.Models.ByteEnum'SecondByte'", new int[] { 2 } }, - - // SByte enums - { "SByte eq Microsoft.AspNetCore.OData.Tests.Models.SByteEnum'SecondSByte'", new int[] { 2 } }, - - // Short enums - { "Short eq Microsoft.AspNetCore.OData.Tests.Models.ShortEnum'SecondShort'", new int[] { 2 } }, - - // UShort enums - { "UShort eq Microsoft.AspNetCore.OData.Tests.Models.UShortEnum'SecondUShort'", new int[] { 2 } }, - - // UInt enums - { "UInt eq Microsoft.AspNetCore.OData.Tests.Models.UIntEnum'SecondUInt'", new int[] { 2 } }, - - // Flag enums - { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four'", new int[] { 1 } }, - { "Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four' eq Flag", new int[] { 1 } }, - { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'0'", new int[] { } }, - { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'1'", new int[] { 5 } }, - { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'5'", new int[] { 1 } }, - { "Flag has Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four'", new int[] { 1 } }, - { "Flag has Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One'", new int[] { 1, 2, 5 } }, - { "Flag eq null", new int[] { } }, - { "null eq Flag", new int[] { } }, - { "Flag eq FlagNullable", new int[] { 1 } }, - { "FlagNullable eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four'", new int[] { 1 } }, - { "Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four' eq FlagNullable", new int[] { 1 } }, - { "FlagNullable eq null", new int[] { 3, 5 } }, - { "null eq FlagNullable", new int[] { 3, 5 } }, - - // Flag enums with different formats - { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One,Four'", new int[] { 1 } }, - { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four'", new int[] { 1 } }, - { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'Four, One'", new int[] { 1 } }, - - // Other expressions - { "Flag ne Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four'", new int[] { 2, 3, 5, 6 } }, - { "Flag eq FlagNullable and Simple eq SimpleNullable", new int[] { 1 } }, - { "Simple gt Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", new int[] { 2, 5, 6 } }, - { "Flag ge Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'Four,One'", new int[] { 1, 3, 6 } } - }; - } + // Simple Enums + { "Simple eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", new int[] { 1, 3 } }, + { "Simple eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'0'", new int[] { 1, 3 } }, + { "Simple eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'Fourth'", new int[] { } }, + { "Simple eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'3'", new int[] { } }, + { "Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First' eq Simple", new int[] { 1, 3 } }, + { "Simple eq cast('0',Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum)", new int[] { 1, 3} }, + { "Simple eq cast('First','Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", new int[] { 1, 3} }, + { "Simple eq null", new int[] { } }, + { "null eq Simple", new int[] { } }, + { "Simple eq SimpleNullable", new int[] { 1 } }, + { "Simple has Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", new int[] { 1, 2, 3, 5, 6 } }, + { "Simple has Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'0'", new int[] { 1, 2, 3, 5, 6 } }, + { "Simple has Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'Second'", new int[] { 5 } }, + { "SimpleNullable eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", new int[] { 1 } }, + { "Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First' eq SimpleNullable", new int[] { 1 } }, + { "SimpleNullable eq null", new int[] { 3, 5 } }, + { "null eq SimpleNullable", new int[] { 3, 5 } }, + + // Long enums + { "Long eq Microsoft.AspNetCore.OData.Tests.Models.LongEnum'SecondLong'", new int[] { 2 } }, + { "Long eq Microsoft.AspNetCore.OData.Tests.Models.LongEnum'FourthLong'", new int[] { } }, + { "Long eq Microsoft.AspNetCore.OData.Tests.Models.LongEnum'3'", new int[] { } }, + + // Byte enums + { "Byte eq Microsoft.AspNetCore.OData.Tests.Models.ByteEnum'SecondByte'", new int[] { 2 } }, + + // SByte enums + { "SByte eq Microsoft.AspNetCore.OData.Tests.Models.SByteEnum'SecondSByte'", new int[] { 2 } }, + + // Short enums + { "Short eq Microsoft.AspNetCore.OData.Tests.Models.ShortEnum'SecondShort'", new int[] { 2 } }, + + // UShort enums + { "UShort eq Microsoft.AspNetCore.OData.Tests.Models.UShortEnum'SecondUShort'", new int[] { 2 } }, + + // UInt enums + { "UInt eq Microsoft.AspNetCore.OData.Tests.Models.UIntEnum'SecondUInt'", new int[] { 2 } }, + + // Flag enums + { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four'", new int[] { 1 } }, + { "Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four' eq Flag", new int[] { 1 } }, + { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'0'", new int[] { } }, + { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'1'", new int[] { 5 } }, + { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'5'", new int[] { 1 } }, + { "Flag has Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four'", new int[] { 1 } }, + { "Flag has Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One'", new int[] { 1, 2, 5 } }, + { "Flag eq null", new int[] { } }, + { "null eq Flag", new int[] { } }, + { "Flag eq FlagNullable", new int[] { 1 } }, + { "FlagNullable eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four'", new int[] { 1 } }, + { "Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four' eq FlagNullable", new int[] { 1 } }, + { "FlagNullable eq null", new int[] { 3, 5 } }, + { "null eq FlagNullable", new int[] { 3, 5 } }, + + // Flag enums with different formats + { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One,Four'", new int[] { 1 } }, + { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four'", new int[] { 1 } }, + { "Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'Four, One'", new int[] { 1 } }, + + // Other expressions + { "Flag ne Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'One, Four'", new int[] { 2, 3, 5, 6 } }, + { "Flag eq FlagNullable and Simple eq SimpleNullable", new int[] { 1 } }, + { "Simple gt Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", new int[] { 2, 5, 6 } }, + { "Flag ge Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'Four,One'", new int[] { 1, 3, 6 } } + }; } + } - // Test data used by EnumModelTestFilters TheoryDataSet - public static List EnumModelTestData + // Test data used by EnumModelTestFilters TheoryDataSet + public static List EnumModelTestData + { + get { - get + return new List() { - return new List() + new EnumModel() { - new EnumModel() - { - Id = 1, - Simple = SimpleEnum.First, - SimpleNullable = SimpleEnum.First, - Long = LongEnum.ThirdLong, - Byte = ByteEnum.ThirdByte, - SByte = SByteEnum.ThirdSByte, - Short = ShortEnum.ThirdShort, - UShort = UShortEnum.ThirdUShort, - UInt = UIntEnum.ThirdUInt, - Flag = FlagsEnum.One | FlagsEnum.Four, - FlagNullable = FlagsEnum.One | FlagsEnum.Four - }, - new EnumModel() - { - Id = 2, - Simple = SimpleEnum.Third, - SimpleNullable = SimpleEnum.Second, - Long = LongEnum.SecondLong, - Byte = ByteEnum.SecondByte, - SByte = SByteEnum.SecondSByte, - Short = ShortEnum.SecondShort, - UShort = UShortEnum.SecondUShort, - UInt = UIntEnum.SecondUInt, - Flag = FlagsEnum.One | FlagsEnum.Two, - FlagNullable = FlagsEnum.Two | FlagsEnum.Four - }, - new EnumModel() - { - Id = 3, - Simple = SimpleEnum.First, - SimpleNullable = null, - Long = LongEnum.FirstLong, - Byte = ByteEnum.FirstByte, - SByte = SByteEnum.FirstSByte, - Short = ShortEnum.FirstShort, - UShort = UShortEnum.FirstUShort, - UInt = UIntEnum.FirstUInt, - Flag = FlagsEnum.Two | FlagsEnum.Four, - FlagNullable = null - }, - new EnumModel() - { - Id = 5, - Simple = SimpleEnum.Second, - SimpleNullable = null, - Long = LongEnum.FirstLong, - Byte = ByteEnum.FirstByte, - SByte = SByteEnum.FirstSByte, - Short = ShortEnum.FirstShort, - UShort = UShortEnum.FirstUShort, - UInt = UIntEnum.FirstUInt, - Flag = FlagsEnum.One, - FlagNullable = null - }, - new EnumModel() - { - Id = 6, - Simple = (SimpleEnum)4, - SimpleNullable = (SimpleEnum)8, - Long = (LongEnum)4, - Byte = (ByteEnum)8, - SByte = (SByteEnum)8, - Short = (ShortEnum)8, - UShort = (UShortEnum)8, - UInt = (UIntEnum)8, - Flag = (FlagsEnum)8, - FlagNullable = (FlagsEnum)16 - } - }; - } + Id = 1, + Simple = SimpleEnum.First, + SimpleNullable = SimpleEnum.First, + Long = LongEnum.ThirdLong, + Byte = ByteEnum.ThirdByte, + SByte = SByteEnum.ThirdSByte, + Short = ShortEnum.ThirdShort, + UShort = UShortEnum.ThirdUShort, + UInt = UIntEnum.ThirdUInt, + Flag = FlagsEnum.One | FlagsEnum.Four, + FlagNullable = FlagsEnum.One | FlagsEnum.Four + }, + new EnumModel() + { + Id = 2, + Simple = SimpleEnum.Third, + SimpleNullable = SimpleEnum.Second, + Long = LongEnum.SecondLong, + Byte = ByteEnum.SecondByte, + SByte = SByteEnum.SecondSByte, + Short = ShortEnum.SecondShort, + UShort = UShortEnum.SecondUShort, + UInt = UIntEnum.SecondUInt, + Flag = FlagsEnum.One | FlagsEnum.Two, + FlagNullable = FlagsEnum.Two | FlagsEnum.Four + }, + new EnumModel() + { + Id = 3, + Simple = SimpleEnum.First, + SimpleNullable = null, + Long = LongEnum.FirstLong, + Byte = ByteEnum.FirstByte, + SByte = SByteEnum.FirstSByte, + Short = ShortEnum.FirstShort, + UShort = UShortEnum.FirstUShort, + UInt = UIntEnum.FirstUInt, + Flag = FlagsEnum.Two | FlagsEnum.Four, + FlagNullable = null + }, + new EnumModel() + { + Id = 5, + Simple = SimpleEnum.Second, + SimpleNullable = null, + Long = LongEnum.FirstLong, + Byte = ByteEnum.FirstByte, + SByte = SByteEnum.FirstSByte, + Short = ShortEnum.FirstShort, + UShort = UShortEnum.FirstUShort, + UInt = UIntEnum.FirstUInt, + Flag = FlagsEnum.One, + FlagNullable = null + }, + new EnumModel() + { + Id = 6, + Simple = (SimpleEnum)4, + SimpleNullable = (SimpleEnum)8, + Long = (LongEnum)4, + Byte = (ByteEnum)8, + SByte = (SByteEnum)8, + Short = (ShortEnum)8, + UShort = (UShortEnum)8, + UInt = (UIntEnum)8, + Flag = (FlagsEnum)8, + FlagNullable = (FlagsEnum)16 + } + }; } + } - // Legal filter queries usable against CastModelTestData. - // Tuple is: filter, expected list of Id's - public static TheoryDataSet CastModelTestFilters + // Legal filter queries usable against CastModelTestData. + // Tuple is: filter, expected list of Id's + public static TheoryDataSet CastModelTestFilters + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - // Cast works. - { "cast(IntProp,Edm.Double) lt 2.5", new int[] { 1, 2 }}, - { "cast(IntProp,Edm.String) eq '3'", new int[] { 3 } }, - { "cast(NullableIntProp,Edm.Int64) eq 1", new int[] { 1 }}, - { "cast(NullableIntProp,Edm.String) eq null", new int[] { 3 }}, - { "cast('Two, Four',Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum) has FlagsEnumProp", new int[] { 2, 3 } }, - { "contains(cast(StringProp,Edm.String),'String')", new int[] { 1, 3 } }, - { "cast(NullableSimpleEnumProp,Edm.String) ne '0'", new int[] { 2, 3 } }, - { "cast(DateTimeOffsetProp,Edm.DateTimeOffset) eq 2001-01-01T01:01:01.000+00:00", new int[] { 1 } }, - { "cast(GuidProp,Edm.String) eq '00000000-0000-0000-0000-000000000000'", new int[] { 1, 3 } }, - { "cast(null,Edm.Int32) ne null", new int[] { } }, - { "cast(null,Edm.String) eq null", new int[] { 1, 2, 3 } }, - { "cast(cast(cast(IntProp,Edm.Int64),Edm.Int16),Edm.Double) gt 1.5", new int[] { 2, 3 } }, - // Cast fails. - { "cast(IntProp,Edm.DateTimeOffset) eq null", new int[] { 1, 2, 3 } }, - { "cast(NullableIntProp,Edm.Guid) eq null", new int[] { 1, 2, 3 } }, - { "cast(StringProp,Edm.Double) eq null", new int[] { 1, 2, 3 } }, - { "cast(StringProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) eq null", new int[] { 1, 2, 3 } }, - { "cast(DateTimeOffsetProp,Edm.Int32) eq null", new int[] { 1, 2, 3 } }, - { "cast(Edm.Int32) eq null", new int[] { 1, 2, 3 } }, - { "cast($it,Edm.String) ne null", new int[] { } }, - { "cast(ComplexProp,Edm.Double) ne null", new int[] { } }, - { "cast(ComplexProp,Edm.String) ne null", new int[] { } }, - { "cast('123',Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne 'First'", new int[] { 1, 2, 3 } } - }; - } + // Cast works. + { "cast(IntProp,Edm.Double) lt 2.5", new int[] { 1, 2 }}, + { "cast(IntProp,Edm.String) eq '3'", new int[] { 3 } }, + { "cast(NullableIntProp,Edm.Int64) eq 1", new int[] { 1 }}, + { "cast(NullableIntProp,Edm.String) eq null", new int[] { 3 }}, + { "cast('Two, Four',Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum) has FlagsEnumProp", new int[] { 2, 3 } }, + { "contains(cast(StringProp,Edm.String),'String')", new int[] { 1, 3 } }, + { "cast(NullableSimpleEnumProp,Edm.String) ne '0'", new int[] { 2, 3 } }, + { "cast(DateTimeOffsetProp,Edm.DateTimeOffset) eq 2001-01-01T01:01:01.000+00:00", new int[] { 1 } }, + { "cast(GuidProp,Edm.String) eq '00000000-0000-0000-0000-000000000000'", new int[] { 1, 3 } }, + { "cast(null,Edm.Int32) ne null", new int[] { } }, + { "cast(null,Edm.String) eq null", new int[] { 1, 2, 3 } }, + { "cast(cast(cast(IntProp,Edm.Int64),Edm.Int16),Edm.Double) gt 1.5", new int[] { 2, 3 } }, + // Cast fails. + { "cast(IntProp,Edm.DateTimeOffset) eq null", new int[] { 1, 2, 3 } }, + { "cast(NullableIntProp,Edm.Guid) eq null", new int[] { 1, 2, 3 } }, + { "cast(StringProp,Edm.Double) eq null", new int[] { 1, 2, 3 } }, + { "cast(StringProp,Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) eq null", new int[] { 1, 2, 3 } }, + { "cast(DateTimeOffsetProp,Edm.Int32) eq null", new int[] { 1, 2, 3 } }, + { "cast(Edm.Int32) eq null", new int[] { 1, 2, 3 } }, + { "cast($it,Edm.String) ne null", new int[] { } }, + { "cast(ComplexProp,Edm.Double) ne null", new int[] { } }, + { "cast(ComplexProp,Edm.String) ne null", new int[] { } }, + { "cast('123',Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum) ne 'First'", new int[] { 1, 2, 3 } } + }; } + } - // Test data used by CastModelTestFilters TheoryDataSet - public static List CastModelTestData + // Test data used by CastModelTestFilters TheoryDataSet + public static List CastModelTestData + { + get { - get + return new List() { - return new List() + new DataTypes() { - new DataTypes() - { - IntProp = 1, - NullableIntProp = 1, - StringProp = "String 1", - FlagsEnumProp = FlagsEnum.One | FlagsEnum.Four, - NullableSimpleEnumProp = SimpleEnum.First, - DateTimeOffsetProp = new DateTimeOffset(new DateTime(2001, 1, 1, 1, 1, 1, DateTimeKind.Utc)), - GuidProp = Guid.Empty, - ComplexProp = new Address() - }, - new DataTypes() - { - IntProp = 2, - NullableIntProp = 2, - FlagsEnumProp = FlagsEnum.Four, - NullableSimpleEnumProp = SimpleEnum.Second, - DateTimeOffsetProp = new DateTimeOffset(new DateTime(2002, 2, 2, 2, 2, 2, DateTimeKind.Utc)), - EntityProp = new Product(), - GuidProp = Guid.NewGuid(), - ComplexProp = new Address() - }, - new DataTypes() - { - IntProp = 3, - StringProp = "String 3", - FlagsEnumProp = FlagsEnum.Two | FlagsEnum.Four, - NullableSimpleEnumProp = SimpleEnum.Third, - DateTimeOffsetProp = new DateTimeOffset(new DateTime(2003, 3, 3, 3, 3, 3, DateTimeKind.Utc)), - GuidProp = Guid.Empty, - EntityProp = new Product() - } - }; - } + IntProp = 1, + NullableIntProp = 1, + StringProp = "String 1", + FlagsEnumProp = FlagsEnum.One | FlagsEnum.Four, + NullableSimpleEnumProp = SimpleEnum.First, + DateTimeOffsetProp = new DateTimeOffset(new DateTime(2001, 1, 1, 1, 1, 1, DateTimeKind.Utc)), + GuidProp = Guid.Empty, + ComplexProp = new Address() + }, + new DataTypes() + { + IntProp = 2, + NullableIntProp = 2, + FlagsEnumProp = FlagsEnum.Four, + NullableSimpleEnumProp = SimpleEnum.Second, + DateTimeOffsetProp = new DateTimeOffset(new DateTime(2002, 2, 2, 2, 2, 2, DateTimeKind.Utc)), + EntityProp = new Product(), + GuidProp = Guid.NewGuid(), + ComplexProp = new Address() + }, + new DataTypes() + { + IntProp = 3, + StringProp = "String 3", + FlagsEnumProp = FlagsEnum.Two | FlagsEnum.Four, + NullableSimpleEnumProp = SimpleEnum.Third, + DateTimeOffsetProp = new DateTimeOffset(new DateTime(2003, 3, 3, 3, 3, 3, DateTimeKind.Utc)), + GuidProp = Guid.Empty, + EntityProp = new Product() + } + }; } + } - // Legal filter queries usable against ParameterAliasTestData. - // Tuple is: filter, parameter alias value, expected list of Id's - public static TheoryDataSet ParameterAliasTestFilters + // Legal filter queries usable against ParameterAliasTestData. + // Tuple is: filter, parameter alias value, expected list of Id's + public static TheoryDataSet ParameterAliasTestFilters + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { "IntProp gt @p", "1.5", new int[] { 2, 3 }}, - { "NullableIntProp lt @p", "1.5", new int[] { 1}}, - { "contains(StringProp,@p)", "'3'", new int[] { 3 } }, - { "FlagsEnumProp has @p", "Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'Two'", new int[] { 2, 3 } }, - { "NullableSimpleEnumProp ne @p", "Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'1'", new int[] { 1, 3 } }, - { "DateTimeOffsetProp ne @p", "2001-01-01T01:01:01.000+00:00", new int[] { 2, 3 } }, - { "GuidProp eq @p", "00000000-0000-0000-0000-000000000000", new int[] { 1, 3 } }, - { "EntityProp/AlternateAddresses/all(a: a/City ne @p)", "'bc'", new int[] { 1 } }, - { "EntityProp/AlternateAddresses/any(a: a/City eq @p)", "'bc'", new int[] { 2, 3 } } - }; - } + { "IntProp gt @p", "1.5", new int[] { 2, 3 }}, + { "NullableIntProp lt @p", "1.5", new int[] { 1}}, + { "contains(StringProp,@p)", "'3'", new int[] { 3 } }, + { "FlagsEnumProp has @p", "Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'Two'", new int[] { 2, 3 } }, + { "NullableSimpleEnumProp ne @p", "Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'1'", new int[] { 1, 3 } }, + { "DateTimeOffsetProp ne @p", "2001-01-01T01:01:01.000+00:00", new int[] { 2, 3 } }, + { "GuidProp eq @p", "00000000-0000-0000-0000-000000000000", new int[] { 1, 3 } }, + { "EntityProp/AlternateAddresses/all(a: a/City ne @p)", "'bc'", new int[] { 1 } }, + { "EntityProp/AlternateAddresses/any(a: a/City eq @p)", "'bc'", new int[] { 2, 3 } } + }; } + } - // Test data used by ParameterAliasTestFilters TheoryDataSet - public static List ParameterAliasTestData + // Test data used by ParameterAliasTestFilters TheoryDataSet + public static List ParameterAliasTestData + { + get { - get + return new List() { - return new List() + new DataTypes() { - new DataTypes() + IntProp = 1, + NullableIntProp = 1, + StringProp = "String 1", + FlagsEnumProp = FlagsEnum.One | FlagsEnum.Four, + NullableSimpleEnumProp = SimpleEnum.First, + DateTimeOffsetProp = new DateTimeOffset(new DateTime(2001, 1, 1, 1, 1, 1, DateTimeKind.Utc)), + GuidProp = Guid.Empty, + EntityProp = new Product { AlternateAddresses = new[] { - IntProp = 1, - NullableIntProp = 1, - StringProp = "String 1", - FlagsEnumProp = FlagsEnum.One | FlagsEnum.Four, - NullableSimpleEnumProp = SimpleEnum.First, - DateTimeOffsetProp = new DateTimeOffset(new DateTime(2001, 1, 1, 1, 1, 1, DateTimeKind.Utc)), - GuidProp = Guid.Empty, - EntityProp = new Product { AlternateAddresses = new[] - { - new Address { City = "a" }, - }} - }, - new DataTypes() + new Address { City = "a" }, + }} + }, + new DataTypes() + { + IntProp = 2, + NullableIntProp = 2, + FlagsEnumProp = FlagsEnum.Two, + NullableSimpleEnumProp = SimpleEnum.Second, + DateTimeOffsetProp = new DateTimeOffset(new DateTime(2002, 2, 2, 2, 2, 2, DateTimeKind.Utc)), + GuidProp = Guid.NewGuid(), + EntityProp = new Product { AlternateAddresses = new[] { - IntProp = 2, - NullableIntProp = 2, - FlagsEnumProp = FlagsEnum.Two, - NullableSimpleEnumProp = SimpleEnum.Second, - DateTimeOffsetProp = new DateTimeOffset(new DateTime(2002, 2, 2, 2, 2, 2, DateTimeKind.Utc)), - GuidProp = Guid.NewGuid(), - EntityProp = new Product { AlternateAddresses = new[] - { - new Address { City = "a" }, - new Address { City = "bc" }, - }} - }, - new DataTypes() + new Address { City = "a" }, + new Address { City = "bc" }, + }} + }, + new DataTypes() + { + IntProp = 3, + StringProp = "String 3", + FlagsEnumProp = FlagsEnum.Two | FlagsEnum.Four, + NullableSimpleEnumProp = SimpleEnum.Third, + DateTimeOffsetProp = new DateTimeOffset(new DateTime(2003, 3, 3, 3, 3, 3, DateTimeKind.Utc)), + GuidProp = Guid.Empty, + EntityProp = new Product { AlternateAddresses = new[] { - IntProp = 3, - StringProp = "String 3", - FlagsEnumProp = FlagsEnum.Two | FlagsEnum.Four, - NullableSimpleEnumProp = SimpleEnum.Third, - DateTimeOffsetProp = new DateTimeOffset(new DateTime(2003, 3, 3, 3, 3, 3, DateTimeKind.Utc)), - GuidProp = Guid.Empty, - EntityProp = new Product { AlternateAddresses = new[] - { - new Address { City = "bc" }, - }} - } - }; - } + new Address { City = "bc" }, + }} + } + }; } + } - // Legal filter queries usable against PropertyAliasTestData. - // Tuple is: filter, expected list of IDs - public static TheoryDataSet PropertyAliasTestFilters + // Legal filter queries usable against PropertyAliasTestData. + // Tuple is: filter, expected list of IDs + public static TheoryDataSet PropertyAliasTestFilters + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { "FirstNameAlias eq 'abc'", new int[] { 2 } }, - { "'abc' eq FirstNameAlias", new int[] { 2 } }, - { "FirstNameAlias eq null", new int[] { } }, - { "null eq FirstNameAlias", new int[] { } }, - { "FirstNameAlias ne 'abc'", new int[] { 1, 3 } }, - { "FirstNameAlias eq 'abc' and Id eq 2", new int[] { 2 } }, - { "FirstNameAlias eq 'abc' and Id eq 1", new int[] { } }, - { "FirstNameAlias gt 'abc'", new int[] { 1, 3 } }, - { "FirstNameAlias ge 'def'", new int[] { 1, 3 } }, - }; - } + { "FirstNameAlias eq 'abc'", new int[] { 2 } }, + { "'abc' eq FirstNameAlias", new int[] { 2 } }, + { "FirstNameAlias eq null", new int[] { } }, + { "null eq FirstNameAlias", new int[] { } }, + { "FirstNameAlias ne 'abc'", new int[] { 1, 3 } }, + { "FirstNameAlias eq 'abc' and Id eq 2", new int[] { 2 } }, + { "FirstNameAlias eq 'abc' and Id eq 1", new int[] { } }, + { "FirstNameAlias gt 'abc'", new int[] { 1, 3 } }, + { "FirstNameAlias ge 'def'", new int[] { 1, 3 } }, + }; } + } - // Test data used by PropertyAliasTestFilters TheoryDataSet - public static List PropertyAliasTestData + // Test data used by PropertyAliasTestFilters TheoryDataSet + public static List PropertyAliasTestData + { + get { - get + return new List() { - return new List() + new PropertyAlias() { - new PropertyAlias() - { - Id = 1, - FirstName = "def" - }, - new PropertyAlias() - { - Id = 2, - FirstName = "abc" - }, - new PropertyAlias() - { - Id = 3, - FirstName = "xyz" - }, - }; - } + Id = 1, + FirstName = "def" + }, + new PropertyAlias() + { + Id = 2, + FirstName = "abc" + }, + new PropertyAlias() + { + Id = 3, + FirstName = "xyz" + }, + }; } + } - [Fact] - public void ConstructorNullContextThrows() - { - ExceptionAssert.Throws(() => - new FilterQueryOption("Name eq 'MSFT'", null)); - } + [Fact] + public void ConstructorNullContextThrows() + { + ExceptionAssert.Throws(() => + new FilterQueryOption("Name eq 'MSFT'", null)); + } - [Fact] - public void ConstructorNullRawValueThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + [Fact] + public void ConstructorNullRawValueThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - // Act & Assert - ExceptionAssert.Throws(() => - new FilterQueryOption(null, new ODataQueryContext(model, typeof(Customer)))); - } + // Act & Assert + ExceptionAssert.Throws(() => + new FilterQueryOption(null, new ODataQueryContext(model, typeof(Customer)))); + } - [Fact] - public void ConstructorEmptyRawValueThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + [Fact] + public void ConstructorEmptyRawValueThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - // Act & Assert - ExceptionAssert.Throws(() => - new FilterQueryOption(string.Empty, new ODataQueryContext(model, typeof(Customer)))); - } + // Act & Assert + ExceptionAssert.Throws(() => + new FilterQueryOption(string.Empty, new ODataQueryContext(model, typeof(Customer)))); + } - [Fact] - public void ConstructorNullQueryOptionParserThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + [Fact] + public void ConstructorNullQueryOptionParserThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => - new FilterQueryOption("test", new ODataQueryContext(model, typeof(Customer)), queryOptionParser: null), - "queryOptionParser"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => + new FilterQueryOption("test", new ODataQueryContext(model, typeof(Customer)), queryOptionParser: null), + "queryOptionParser"); + } - [Theory] - [InlineData("Name eq 'MSFT'")] - [InlineData("''")] - public void CanConstructValidFilterQuery(string filterValue) - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var filter = new FilterQueryOption(filterValue, context); + [Theory] + [InlineData("Name eq 'MSFT'")] + [InlineData("''")] + public void CanConstructValidFilterQuery(string filterValue) + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var filter = new FilterQueryOption(filterValue, context); - Assert.Same(context, filter.Context); - Assert.Equal(filterValue, filter.RawValue); - } + Assert.Same(context, filter.Context); + Assert.Equal(filterValue, filter.RawValue); + } - [Fact] - public void GetQueryNodeParsesQuery() - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var filter = new FilterQueryOption("Name eq 'MSFT'", context); - var node = filter.FilterClause; - - Assert.Equal(QueryNodeKind.BinaryOperator, node.Expression.Kind); - var binaryNode = node.Expression as BinaryOperatorNode; - Assert.Equal(BinaryOperatorKind.Equal, binaryNode.OperatorKind); - Assert.Equal(QueryNodeKind.Constant, binaryNode.Right.Kind); - Assert.Equal("MSFT", ((ConstantNode)binaryNode.Right).Value); - Assert.Equal(QueryNodeKind.SingleValuePropertyAccess, binaryNode.Left.Kind); - var propertyAccessNode = binaryNode.Left as SingleValuePropertyAccessNode; - Assert.Equal("Name", propertyAccessNode.Property.Name); - } + [Fact] + public void GetQueryNodeParsesQuery() + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var filter = new FilterQueryOption("Name eq 'MSFT'", context); + var node = filter.FilterClause; + + Assert.Equal(QueryNodeKind.BinaryOperator, node.Expression.Kind); + var binaryNode = node.Expression as BinaryOperatorNode; + Assert.Equal(BinaryOperatorKind.Equal, binaryNode.OperatorKind); + Assert.Equal(QueryNodeKind.Constant, binaryNode.Right.Kind); + Assert.Equal("MSFT", ((ConstantNode)binaryNode.Right).Value); + Assert.Equal(QueryNodeKind.SingleValuePropertyAccess, binaryNode.Left.Kind); + var propertyAccessNode = binaryNode.Left as SingleValuePropertyAccessNode; + Assert.Equal("Name", propertyAccessNode.Property.Name); + } - [Fact] - public void CanConstructValidAnyQueryOverPrimitiveCollectionProperty() - { - var model = new ODataModelBuilder().Add_Customer_EntityType_With_CollectionProperties().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var filter = new FilterQueryOption("Aliases/any(a: a eq 'alias')", context); - var node = filter.FilterClause; - var anyNode = node.Expression as AnyNode; - var aParameter = anyNode.RangeVariables.SingleOrDefault(p => p.Name == "a"); - var aParameterType = aParameter.TypeReference.Definition as IEdmPrimitiveType; - - Assert.NotNull(aParameter); - - Assert.NotNull(aParameterType); - Assert.Equal("a", aParameter.Name); - } + [Fact] + public void CanConstructValidAnyQueryOverPrimitiveCollectionProperty() + { + var model = new ODataModelBuilder().Add_Customer_EntityType_With_CollectionProperties().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var filter = new FilterQueryOption("Aliases/any(a: a eq 'alias')", context); + var node = filter.FilterClause; + var anyNode = node.Expression as AnyNode; + var aParameter = anyNode.RangeVariables.SingleOrDefault(p => p.Name == "a"); + var aParameterType = aParameter.TypeReference.Definition as IEdmPrimitiveType; + + Assert.NotNull(aParameter); + + Assert.NotNull(aParameterType); + Assert.Equal("a", aParameter.Name); + } - [Fact] - public void CanConstructValidAnyQueryOverComplexCollectionProperty() - { - var model = new ODataModelBuilder().Add_Customer_EntityType_With_CollectionProperties().Add_Customers_EntitySet().Add_Address_ComplexType().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var filter = new FilterQueryOption("Addresses/any(a: a/HouseNumber eq 1)", context); - var node = filter.FilterClause; - var anyNode = node.Expression as AnyNode; - var aParameter = anyNode.RangeVariables.SingleOrDefault(p => p.Name == "a"); - var aParameterType = aParameter.TypeReference.Definition as IEdmComplexType; - - Assert.NotNull(aParameter); - - Assert.NotNull(aParameterType); - Assert.Equal("a", aParameter.Name); - } + [Fact] + public void CanConstructValidAnyQueryOverComplexCollectionProperty() + { + var model = new ODataModelBuilder().Add_Customer_EntityType_With_CollectionProperties().Add_Customers_EntitySet().Add_Address_ComplexType().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var filter = new FilterQueryOption("Addresses/any(a: a/HouseNumber eq 1)", context); + var node = filter.FilterClause; + var anyNode = node.Expression as AnyNode; + var aParameter = anyNode.RangeVariables.SingleOrDefault(p => p.Name == "a"); + var aParameterType = aParameter.TypeReference.Definition as IEdmComplexType; + + Assert.NotNull(aParameter); + + Assert.NotNull(aParameterType); + Assert.Equal("a", aParameter.Name); + } - //[Fact] - //public void CanTurnOffValidationForFilter() - //{ - // ODataValidationSettings settings = new ODataValidationSettings() { AllowedFunctions = AllowedFunctions.AllDateTimeFunctions }; - // ODataQueryContext context = ValidationTestHelper.CreateCustomerContext(); - // FilterQueryOption option = new FilterQueryOption("substring(Name,8,1) eq '7'", context); + //[Fact] + //public void CanTurnOffValidationForFilter() + //{ + // ODataValidationSettings settings = new ODataValidationSettings() { AllowedFunctions = AllowedFunctions.AllDateTimeFunctions }; + // ODataQueryContext context = ValidationTestHelper.CreateCustomerContext(); + // FilterQueryOption option = new FilterQueryOption("substring(Name,8,1) eq '7'", context); - // ExceptionAssert.Throws(() => - // option.Validate(settings), - // "Function 'substring' is not allowed. To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings."); + // ExceptionAssert.Throws(() => + // option.Validate(settings), + // "Function 'substring' is not allowed. To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings."); - // option.Validator = null; - // ExceptionAssert.DoesNotThrow(() => option.Validate(settings)); - //} + // option.Validator = null; + // ExceptionAssert.DoesNotThrow(() => option.Validate(settings)); + //} - [Fact] - public void ApplyTo_Throws_Null_Query() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType_With_CollectionProperties().Add_Customers_EntitySet().Add_Address_ComplexType().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var filter = new FilterQueryOption("Addresses/any(a: a/HouseNumber eq 1)", context); + [Fact] + public void ApplyTo_Throws_Null_Query() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType_With_CollectionProperties().Add_Customers_EntitySet().Add_Address_ComplexType().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var filter = new FilterQueryOption("Addresses/any(a: a/HouseNumber eq 1)", context); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => filter.ApplyTo(null, new ODataQuerySettings()), "query"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => filter.ApplyTo(null, new ODataQuerySettings()), "query"); + } - [Fact] - public void ApplyTo_Throws_Null_QuerySettings() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType_With_CollectionProperties().Add_Customers_EntitySet().Add_Address_ComplexType().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var filter = new FilterQueryOption("Addresses/any(a: a/HouseNumber eq 1)", context); + [Fact] + public void ApplyTo_Throws_Null_QuerySettings() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType_With_CollectionProperties().Add_Customers_EntitySet().Add_Address_ComplexType().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var filter = new FilterQueryOption("Addresses/any(a: a/HouseNumber eq 1)", context); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => filter.ApplyTo(new Customer[0].AsQueryable(), null), "querySettings"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => filter.ApplyTo(new Customer[0].AsQueryable(), null), "querySettings"); + } - [Theory] - [MemberData(nameof(CustomerTestFilters))] - public void ApplyTo_Returns_Correct_Queryable(string filter, int[] customerIds) - { - // Arrange - var model = new ODataModelBuilder() - .Add_Order_EntityType() - .Add_Customer_EntityType_With_Address() - .Add_CustomerOrders_Relationship() - .Add_Customer_EntityType_With_CollectionProperties() - .Add_Customers_EntitySet() - .GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var filterOption = new FilterQueryOption(filter, context); - IEnumerable customers = CustomerFilterTestData; - - // Act - IQueryable queryable = filterOption.ApplyTo(customers.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); - - // Assert - Assert.NotNull(queryable); - IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); - Assert.Equal( - customerIds, - actualCustomers.Select(customer => customer.Id)); - } + [Theory] + [MemberData(nameof(CustomerTestFilters))] + public void ApplyTo_Returns_Correct_Queryable(string filter, int[] customerIds) + { + // Arrange + var model = new ODataModelBuilder() + .Add_Order_EntityType() + .Add_Customer_EntityType_With_Address() + .Add_CustomerOrders_Relationship() + .Add_Customer_EntityType_With_CollectionProperties() + .Add_Customers_EntitySet() + .GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var filterOption = new FilterQueryOption(filter, context); + IEnumerable customers = CustomerFilterTestData; + + // Act + IQueryable queryable = filterOption.ApplyTo(customers.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); + Assert.Equal( + customerIds, + actualCustomers.Select(customer => customer.Id)); + } - [Fact] - public void ApplyTo_Returns_Correct_Queryable_Without_Services() - { - // Arrange - var model = new ODataModelBuilder() - .Add_Order_EntityType() - .Add_Customer_EntityType_With_Address() - .Add_CustomerOrders_Relationship() - .Add_Customer_EntityType_With_CollectionProperties() - .Add_Customers_EntitySet() - .GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer), null); - var filterOption = new FilterQueryOption("Id eq 1", context); - IEnumerable customers = CustomerFilterTestData; - - // Act - IQueryable queryable = filterOption.ApplyTo(customers.AsQueryable(), new ODataQuerySettings()); - - // Assert - Assert.NotNull(queryable); - IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); - Assert.Single(actualCustomers); - Assert.Equal(1, actualCustomers.First().Id); - } + [Fact] + public void ApplyTo_Returns_Correct_Queryable_Without_Services() + { + // Arrange + var model = new ODataModelBuilder() + .Add_Order_EntityType() + .Add_Customer_EntityType_With_Address() + .Add_CustomerOrders_Relationship() + .Add_Customer_EntityType_With_CollectionProperties() + .Add_Customers_EntitySet() + .GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer), null); + var filterOption = new FilterQueryOption("Id eq 1", context); + IEnumerable customers = CustomerFilterTestData; + + // Act + IQueryable queryable = filterOption.ApplyTo(customers.AsQueryable(), new ODataQuerySettings()); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); + Assert.Single(actualCustomers); + Assert.Equal(1, actualCustomers.First().Id); + } - [Theory] - [MemberData(nameof(EnumModelTestFilters))] - public void ApplyToEnums_ReturnsCorrectQueryable(string filter, int[] enumModelIds) - { - // Arrange - var model = GetEnumModel(); - var context = new ODataQueryContext(model, typeof(EnumModel)) { RequestContainer = new MockServiceProvider(model) }; - var filterOption = new FilterQueryOption(filter, context); - IEnumerable enumModels = EnumModelTestData; - - // Act - IQueryable queryable = filterOption.ApplyTo(enumModels.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); - - // Assert - Assert.NotNull(queryable); - IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); - Assert.Equal( - enumModelIds, - actualCustomers.Select(enumModel => enumModel.Id)); - } + [Theory] + [MemberData(nameof(EnumModelTestFilters))] + public void ApplyToEnums_ReturnsCorrectQueryable(string filter, int[] enumModelIds) + { + // Arrange + var model = GetEnumModel(); + var context = new ODataQueryContext(model, typeof(EnumModel)) { RequestContainer = new MockServiceProvider(model) }; + var filterOption = new FilterQueryOption(filter, context); + IEnumerable enumModels = EnumModelTestData; + + // Act + IQueryable queryable = filterOption.ApplyTo(enumModels.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); + Assert.Equal( + enumModelIds, + actualCustomers.Select(enumModel => enumModel.Id)); + } - [Theory] - [MemberData(nameof(EnumModelTestFilters))] - public void ApplyToEnums_ReturnsCorrectQueryabl_IfEnableConstantParameterizationSetFalse(string filter, int[] enumModelIds) - { - // Arrange - var model = GetEnumModel(); - var context = new ODataQueryContext(model, typeof(EnumModel)) { RequestContainer = new MockServiceProvider(model) }; - var filterOption = new FilterQueryOption(filter, context); - IEnumerable enumModels = EnumModelTestData; - - // Act - IQueryable queryable = filterOption.ApplyTo(enumModels.AsQueryable(), + [Theory] + [MemberData(nameof(EnumModelTestFilters))] + public void ApplyToEnums_ReturnsCorrectQueryabl_IfEnableConstantParameterizationSetFalse(string filter, int[] enumModelIds) + { + // Arrange + var model = GetEnumModel(); + var context = new ODataQueryContext(model, typeof(EnumModel)) { RequestContainer = new MockServiceProvider(model) }; + var filterOption = new FilterQueryOption(filter, context); + IEnumerable enumModels = EnumModelTestData; + + // Act + IQueryable queryable = filterOption.ApplyTo(enumModels.AsQueryable(), + new ODataQuerySettings + { + HandleNullPropagation = HandleNullPropagationOption.True, + EnableConstantParameterization = false + }); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); + Assert.Equal( + enumModelIds, + actualCustomers.Select(enumModel => enumModel.Id)); + } + [Theory] + [InlineData("Simple has null", typeof(NotSupportedException))] + [InlineData("null has Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", typeof(NotSupportedException))] + [InlineData("Id has Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", typeof(ODataException))] + [InlineData("null has null", typeof(NotSupportedException))] + [InlineData("Simple has 23", typeof(ODataException))] + [InlineData("'Some string' has 0", typeof(ODataException))] + public void ApplyToEnums_Throws_WithInvalidFilter(string filter, Type exceptionType) + { + // Arrange + var model = GetEnumModel(); + var context = new ODataQueryContext(model, typeof(EnumModel)) { RequestContainer = new MockServiceProvider() }; + var filterOption = new FilterQueryOption(filter, context); + IEnumerable enumModels = EnumModelTestData; + + // Act & Assert + ExceptionAssert.Throws( + exceptionType, + () => filterOption.ApplyTo( + enumModels.AsQueryable(), + new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True })); + } + + [Theory] + [InlineData("Simple eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'4'")] + [InlineData("Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'8'")] + public void ApplyToEnums_DoesnotThrow_ForUndefinedValue(string filter) + { + // Arrange + var model = GetEnumModel(); + var context = new ODataQueryContext(model, typeof(EnumModel)) { RequestContainer = new MockServiceProvider() }; + var filterOption = new FilterQueryOption(filter, context); + IEnumerable enumModels = EnumModelTestData; + + // Act + ExceptionAssert.DoesNotThrow( + () => filterOption.ApplyTo(enumModels.AsQueryable(), new ODataQuerySettings { - HandleNullPropagation = HandleNullPropagationOption.True, - EnableConstantParameterization = false - }); - - // Assert - Assert.NotNull(queryable); - IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); - Assert.Equal( - enumModelIds, - actualCustomers.Select(enumModel => enumModel.Id)); - } - [Theory] - [InlineData("Simple has null", typeof(NotSupportedException))] - [InlineData("null has Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", typeof(NotSupportedException))] - [InlineData("Id has Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", typeof(ODataException))] - [InlineData("null has null", typeof(NotSupportedException))] - [InlineData("Simple has 23", typeof(ODataException))] - [InlineData("'Some string' has 0", typeof(ODataException))] - public void ApplyToEnums_Throws_WithInvalidFilter(string filter, Type exceptionType) - { - // Arrange - var model = GetEnumModel(); - var context = new ODataQueryContext(model, typeof(EnumModel)) { RequestContainer = new MockServiceProvider() }; - var filterOption = new FilterQueryOption(filter, context); - IEnumerable enumModels = EnumModelTestData; - - // Act & Assert - ExceptionAssert.Throws( - exceptionType, - () => filterOption.ApplyTo( - enumModels.AsQueryable(), - new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True })); - } - - [Theory] - [InlineData("Simple eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'4'")] - [InlineData("Flag eq Microsoft.AspNetCore.OData.Tests.Models.FlagsEnum'8'")] - public void ApplyToEnums_DoesnotThrow_ForUndefinedValue(string filter) - { - // Arrange - var model = GetEnumModel(); - var context = new ODataQueryContext(model, typeof(EnumModel)) { RequestContainer = new MockServiceProvider() }; - var filterOption = new FilterQueryOption(filter, context); - IEnumerable enumModels = EnumModelTestData; - - // Act - ExceptionAssert.DoesNotThrow( - () => filterOption.ApplyTo(enumModels.AsQueryable(), - new ODataQuerySettings - { - HandleNullPropagation = HandleNullPropagationOption.True - }) - ); - } + HandleNullPropagation = HandleNullPropagationOption.True + }) + ); + } - [Theory] - [MemberData(nameof(CastModelTestFilters))] - public void ApplyWithCast_ReturnsCorrectQueryable(string filter, int[] castModelIds) - { - // Arrange - var model = GetCastModel(); - var context = new ODataQueryContext(model, typeof(DataTypes)) { RequestContainer = new MockServiceProvider(model) }; - var filterOption = new FilterQueryOption(filter, context); - IEnumerable castModels = CastModelTestData; - - // Act - IQueryable queryable = filterOption.ApplyTo(castModels.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); - - // Assert - Assert.NotNull(queryable); - IEnumerable actualProducts = Assert.IsAssignableFrom>(queryable); - Assert.Equal( - castModelIds, - actualProducts.Select(product => product.IntProp)); - } + [Theory] + [MemberData(nameof(CastModelTestFilters))] + public void ApplyWithCast_ReturnsCorrectQueryable(string filter, int[] castModelIds) + { + // Arrange + var model = GetCastModel(); + var context = new ODataQueryContext(model, typeof(DataTypes)) { RequestContainer = new MockServiceProvider(model) }; + var filterOption = new FilterQueryOption(filter, context); + IEnumerable castModels = CastModelTestData; + + // Act + IQueryable queryable = filterOption.ApplyTo(castModels.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualProducts = Assert.IsAssignableFrom>(queryable); + Assert.Equal( + castModelIds, + actualProducts.Select(product => product.IntProp)); + } - [Theory] - [InlineData("cast(NoSuchProperty,Edm.String) eq null", typeof(ODataException))] - [InlineData("cast(ProductId,Edm.NoSuchType) eq null", typeof(ODataException))] - [InlineData("cast(ProductId,Edm.String) eq 123", typeof(ODataException))] - [InlineData("cast('123',Microsoft.TestCommon.Types.SimpleEnum) ne 'First'", typeof(ODataException))] - [InlineData("cast(Edm.Int32) eq '123'", typeof(ODataException))] - public void ApplyWithCast_Throws_WithInvalidFilter(string filter, Type exceptionType) - { - // Arrange - var model = GetCastModel(); - var context = new ODataQueryContext(model, typeof(DataTypes)) { RequestContainer = new MockServiceProvider() }; - var filterOption = new FilterQueryOption(filter, context); - IEnumerable castModels = CastModelTestData; - - // Act & Assert - ExceptionAssert.Throws( - exceptionType, - () => filterOption.ApplyTo( - castModels.AsQueryable(), - new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True })); - } + [Theory] + [InlineData("cast(NoSuchProperty,Edm.String) eq null", typeof(ODataException))] + [InlineData("cast(ProductId,Edm.NoSuchType) eq null", typeof(ODataException))] + [InlineData("cast(ProductId,Edm.String) eq 123", typeof(ODataException))] + [InlineData("cast('123',Microsoft.TestCommon.Types.SimpleEnum) ne 'First'", typeof(ODataException))] + [InlineData("cast(Edm.Int32) eq '123'", typeof(ODataException))] + public void ApplyWithCast_Throws_WithInvalidFilter(string filter, Type exceptionType) + { + // Arrange + var model = GetCastModel(); + var context = new ODataQueryContext(model, typeof(DataTypes)) { RequestContainer = new MockServiceProvider() }; + var filterOption = new FilterQueryOption(filter, context); + IEnumerable castModels = CastModelTestData; + + // Act & Assert + ExceptionAssert.Throws( + exceptionType, + () => filterOption.ApplyTo( + castModels.AsQueryable(), + new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True })); + } - [Theory] - [InlineData( - "length(Simple) eq 5", - "No function signature for the function with name 'length' matches the specified arguments. The function signatures considered are: length(Edm.String Nullable=true).")] - [InlineData( - "length(SimpleNullable) eq 5", - "No function signature for the function with name 'length' matches the specified arguments. The function signatures considered are: length(Edm.String Nullable=true).")] - [InlineData( - "length(Flag) eq 5", - "No function signature for the function with name 'length' matches the specified arguments. The function signatures considered are: length(Edm.String Nullable=true).")] - [InlineData( - "length(FlagNullable) eq 5", - "No function signature for the function with name 'length' matches the specified arguments. The function signatures considered are: length(Edm.String Nullable=true).")] - [InlineData( - "contains(Simple, 'foo') eq true", - "No function signature for the function with name 'contains' matches the specified arguments. The function signatures considered are: contains(Edm.String Nullable=true, Edm.String Nullable=true).")] - [InlineData( - "startswith(Simple, 'foo') eq true", - "No function signature for the function with name 'startswith' matches the specified arguments. The function signatures considered are: startswith(Edm.String Nullable=true, Edm.String Nullable=true).")] - [InlineData( - "endswith(Simple, 'foo') eq true", - "No function signature for the function with name 'endswith' matches the specified arguments. The function signatures considered are: endswith(Edm.String Nullable=true, Edm.String Nullable=true).")] - [InlineData( - "tolower(Simple) eq 'foo'", - "No function signature for the function with name 'tolower' matches the specified arguments. The function signatures considered are: tolower(Edm.String Nullable=true).")] - [InlineData( - "toupper(Simple) eq 'foo'", - "No function signature for the function with name 'toupper' matches the specified arguments. The function signatures considered are: toupper(Edm.String Nullable=true).")] - [InlineData( - "trim(Simple) eq 'foo'", - "No function signature for the function with name 'trim' matches the specified arguments. The function signatures considered are: trim(Edm.String Nullable=true).")] - [InlineData( - "indexof(Simple, 'foo') eq 2", - "No function signature for the function with name 'indexof' matches the specified arguments. The function signatures considered are: indexof(Edm.String Nullable=true, Edm.String Nullable=true).")] - [InlineData( - "substring(Simple, 3) eq 'foo'", - "No function signature for the function with name 'substring' matches the specified arguments. The function signatures considered are: substring(Edm.String Nullable=true, Edm.Int32); substring(Edm.String Nullable=true, Edm.Int32 Nullable=true); substring(Edm.String Nullable=true, Edm.Int32, Edm.Int32); substring(Edm.String Nullable=true, Edm.Int32 Nullable=true, Edm.Int32); substring(Edm.String Nullable=true, Edm.Int32, Edm.Int32 Nullable=true); substring(Edm.String Nullable=true, Edm.Int32 Nullable=true, Edm.Int32 Nullable=true).")] - [InlineData( - "substring(Simple, 1, 3) eq 'foo'", - "No function signature for the function with name 'substring' matches the specified arguments. The function signatures considered are: substring(Edm.String Nullable=true, Edm.Int32); substring(Edm.String Nullable=true, Edm.Int32 Nullable=true); substring(Edm.String Nullable=true, Edm.Int32, Edm.Int32); substring(Edm.String Nullable=true, Edm.Int32 Nullable=true, Edm.Int32); substring(Edm.String Nullable=true, Edm.Int32, Edm.Int32 Nullable=true); substring(Edm.String Nullable=true, Edm.Int32 Nullable=true, Edm.Int32 Nullable=true).")] - [InlineData( - "concat(Simple, 'bar') eq 'foo'", - "No function signature for the function with name 'concat' matches the specified arguments. The function signatures considered are: concat(Edm.String Nullable=true, Edm.String Nullable=true).")] - public void ApplyToEnums_ThrowsNotSupported_ForStringFunctions(string filter, string exceptionMessage) - { - // Arrange - var model = GetEnumModel(); - var context = new ODataQueryContext(model, typeof(EnumModel)) { RequestContainer = new MockServiceProvider() }; - var filterOption = new FilterQueryOption(filter, context); - IEnumerable enumModels = EnumModelTestData; - - // Act - ExceptionAssert.Throws( - () => filterOption.ApplyTo(enumModels.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }), - exceptionMessage - ); - } + [Theory] + [InlineData( + "length(Simple) eq 5", + "No function signature for the function with name 'length' matches the specified arguments. The function signatures considered are: length(Edm.String Nullable=true).")] + [InlineData( + "length(SimpleNullable) eq 5", + "No function signature for the function with name 'length' matches the specified arguments. The function signatures considered are: length(Edm.String Nullable=true).")] + [InlineData( + "length(Flag) eq 5", + "No function signature for the function with name 'length' matches the specified arguments. The function signatures considered are: length(Edm.String Nullable=true).")] + [InlineData( + "length(FlagNullable) eq 5", + "No function signature for the function with name 'length' matches the specified arguments. The function signatures considered are: length(Edm.String Nullable=true).")] + [InlineData( + "contains(Simple, 'foo') eq true", + "No function signature for the function with name 'contains' matches the specified arguments. The function signatures considered are: contains(Edm.String Nullable=true, Edm.String Nullable=true).")] + [InlineData( + "startswith(Simple, 'foo') eq true", + "No function signature for the function with name 'startswith' matches the specified arguments. The function signatures considered are: startswith(Edm.String Nullable=true, Edm.String Nullable=true).")] + [InlineData( + "endswith(Simple, 'foo') eq true", + "No function signature for the function with name 'endswith' matches the specified arguments. The function signatures considered are: endswith(Edm.String Nullable=true, Edm.String Nullable=true).")] + [InlineData( + "tolower(Simple) eq 'foo'", + "No function signature for the function with name 'tolower' matches the specified arguments. The function signatures considered are: tolower(Edm.String Nullable=true).")] + [InlineData( + "toupper(Simple) eq 'foo'", + "No function signature for the function with name 'toupper' matches the specified arguments. The function signatures considered are: toupper(Edm.String Nullable=true).")] + [InlineData( + "trim(Simple) eq 'foo'", + "No function signature for the function with name 'trim' matches the specified arguments. The function signatures considered are: trim(Edm.String Nullable=true).")] + [InlineData( + "indexof(Simple, 'foo') eq 2", + "No function signature for the function with name 'indexof' matches the specified arguments. The function signatures considered are: indexof(Edm.String Nullable=true, Edm.String Nullable=true).")] + [InlineData( + "substring(Simple, 3) eq 'foo'", + "No function signature for the function with name 'substring' matches the specified arguments. The function signatures considered are: substring(Edm.String Nullable=true, Edm.Int32); substring(Edm.String Nullable=true, Edm.Int32 Nullable=true); substring(Edm.String Nullable=true, Edm.Int32, Edm.Int32); substring(Edm.String Nullable=true, Edm.Int32 Nullable=true, Edm.Int32); substring(Edm.String Nullable=true, Edm.Int32, Edm.Int32 Nullable=true); substring(Edm.String Nullable=true, Edm.Int32 Nullable=true, Edm.Int32 Nullable=true).")] + [InlineData( + "substring(Simple, 1, 3) eq 'foo'", + "No function signature for the function with name 'substring' matches the specified arguments. The function signatures considered are: substring(Edm.String Nullable=true, Edm.Int32); substring(Edm.String Nullable=true, Edm.Int32 Nullable=true); substring(Edm.String Nullable=true, Edm.Int32, Edm.Int32); substring(Edm.String Nullable=true, Edm.Int32 Nullable=true, Edm.Int32); substring(Edm.String Nullable=true, Edm.Int32, Edm.Int32 Nullable=true); substring(Edm.String Nullable=true, Edm.Int32 Nullable=true, Edm.Int32 Nullable=true).")] + [InlineData( + "concat(Simple, 'bar') eq 'foo'", + "No function signature for the function with name 'concat' matches the specified arguments. The function signatures considered are: concat(Edm.String Nullable=true, Edm.String Nullable=true).")] + public void ApplyToEnums_ThrowsNotSupported_ForStringFunctions(string filter, string exceptionMessage) + { + // Arrange + var model = GetEnumModel(); + var context = new ODataQueryContext(model, typeof(EnumModel)) { RequestContainer = new MockServiceProvider() }; + var filterOption = new FilterQueryOption(filter, context); + IEnumerable enumModels = EnumModelTestData; + + // Act + ExceptionAssert.Throws( + () => filterOption.ApplyTo(enumModels.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }), + exceptionMessage + ); + } - [Theory] - [MemberData(nameof(ParameterAliasTestFilters))] - public void ApplyWithParameterAlias_ReturnsCorrectQueryable(string filter, string parameterAliasValue, int[] parameterAliasModelIds) - { - // Arrange - var model = GetParameterAliasModel(); - var context = new ODataQueryContext(model, typeof(DataTypes)) { RequestContainer = new MockServiceProvider(model) }; - IEdmType targetEdmType = model.FindType("Microsoft.AspNetCore.OData.Tests.Models.DataTypes"); - IEdmNavigationSource targetNavigationSource = model.FindDeclaredEntitySet("Microsoft.AspNetCore.OData.Tests.Models.Products"); - - ODataQueryOptionParser parser = new ODataQueryOptionParser( - model, - targetEdmType, - targetNavigationSource, - new Dictionary { { "$filter", filter }, { "@p", parameterAliasValue } }); - - var filterOption = new FilterQueryOption(filter, context, parser); - IEnumerable parameterAliasModels = ParameterAliasTestData; - - // Act - IQueryable queryable = filterOption.ApplyTo(parameterAliasModels.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); - - // Assert - Assert.NotNull(queryable); - IEnumerable actualResult = Assert.IsAssignableFrom>(queryable); - Assert.Equal( - parameterAliasModelIds, - actualResult.Select(result => result.IntProp)); - } + [Theory] + [MemberData(nameof(ParameterAliasTestFilters))] + public void ApplyWithParameterAlias_ReturnsCorrectQueryable(string filter, string parameterAliasValue, int[] parameterAliasModelIds) + { + // Arrange + var model = GetParameterAliasModel(); + var context = new ODataQueryContext(model, typeof(DataTypes)) { RequestContainer = new MockServiceProvider(model) }; + IEdmType targetEdmType = model.FindType("Microsoft.AspNetCore.OData.Tests.Models.DataTypes"); + IEdmNavigationSource targetNavigationSource = model.FindDeclaredEntitySet("Microsoft.AspNetCore.OData.Tests.Models.Products"); + + ODataQueryOptionParser parser = new ODataQueryOptionParser( + model, + targetEdmType, + targetNavigationSource, + new Dictionary { { "$filter", filter }, { "@p", parameterAliasValue } }); + + var filterOption = new FilterQueryOption(filter, context, parser); + IEnumerable parameterAliasModels = ParameterAliasTestData; + + // Act + IQueryable queryable = filterOption.ApplyTo(parameterAliasModels.AsQueryable(), new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualResult = Assert.IsAssignableFrom>(queryable); + Assert.Equal( + parameterAliasModelIds, + actualResult.Select(result => result.IntProp)); + } - [Theory] - [MemberData(nameof(PropertyAliasTestFilters))] - public void ApplyTo_ReturnsCorrectQueryable_PropertyAlias(string filter, int[] propertyAliasIds) - { - // Arrange - var model = GetPropertyAliasModel(); - var context = new ODataQueryContext(model, typeof(PropertyAlias)) { RequestContainer = new MockServiceProvider(model) }; - var filterOption = new FilterQueryOption(filter, context); - IEnumerable propertyAliases = PropertyAliasTestData; - - // Act - IQueryable queryable = filterOption.ApplyTo( - propertyAliases.AsQueryable(), - new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); - - // Assert - Assert.NotNull(queryable); - IEnumerable actualPropertyAliases = Assert.IsAssignableFrom>(queryable); - Assert.Equal( - propertyAliasIds, - actualPropertyAliases.Select(propertyAlias => propertyAlias.Id)); - } + [Theory] + [MemberData(nameof(PropertyAliasTestFilters))] + public void ApplyTo_ReturnsCorrectQueryable_PropertyAlias(string filter, int[] propertyAliasIds) + { + // Arrange + var model = GetPropertyAliasModel(); + var context = new ODataQueryContext(model, typeof(PropertyAlias)) { RequestContainer = new MockServiceProvider(model) }; + var filterOption = new FilterQueryOption(filter, context); + IEnumerable propertyAliases = PropertyAliasTestData; + + // Act + IQueryable queryable = filterOption.ApplyTo( + propertyAliases.AsQueryable(), + new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualPropertyAliases = Assert.IsAssignableFrom>(queryable); + Assert.Equal( + propertyAliasIds, + actualPropertyAliases.Select(propertyAlias => propertyAlias.Id)); + } - [Fact] - public void Property_FilterClause_WorksWithUnTypedContext() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - context.RequestContainer = new MockServiceProvider(); - FilterQueryOption filter = new FilterQueryOption("ID eq 42", context); - - // Act & Assert - Assert.NotNull(filter.FilterClause); - } + [Fact] + public void Property_FilterClause_WorksWithUnTypedContext() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + context.RequestContainer = new MockServiceProvider(); + FilterQueryOption filter = new FilterQueryOption("ID eq 42", context); + + // Act & Assert + Assert.NotNull(filter.FilterClause); + } - [Fact] - public void ApplyTo_WithUnTypedContext_Throws_InvalidOperation() - { - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - context.RequestContainer = new MockServiceProvider(); - FilterQueryOption filter = new FilterQueryOption("Id eq 42", context); - IQueryable queryable = new Mock().Object; - - ExceptionAssert.Throws(() => filter.ApplyTo(queryable, new ODataQuerySettings()), - "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); - } + [Fact] + public void ApplyTo_WithUnTypedContext_Throws_InvalidOperation() + { + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + context.RequestContainer = new MockServiceProvider(); + FilterQueryOption filter = new FilterQueryOption("Id eq 42", context); + IQueryable queryable = new Mock().Object; + + ExceptionAssert.Throws(() => filter.ApplyTo(queryable, new ODataQuerySettings()), + "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); + } - private static IEdmModel GetEnumModel() - { - //var config = RoutingConfigurationFactory.CreateWithTypes(typeof(EnumModel)); - //var builder = ODataConventionModelBuilderFactory.Create(config); - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("EnumModels"); - return builder.GetEdmModel(); - } + private static IEdmModel GetEnumModel() + { + //var config = RoutingConfigurationFactory.CreateWithTypes(typeof(EnumModel)); + //var builder = ODataConventionModelBuilderFactory.Create(config); + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("EnumModels"); + return builder.GetEdmModel(); + } - private static IEdmModel GetCastModel() - { - //var config = RoutingConfigurationFactory.CreateWithTypes(typeof(DataTypes)); - //var builder = ODataConventionModelBuilderFactory.Create(config); + private static IEdmModel GetCastModel() + { + //var config = RoutingConfigurationFactory.CreateWithTypes(typeof(DataTypes)); + //var builder = ODataConventionModelBuilderFactory.Create(config); - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("CastModels"); - return builder.GetEdmModel(); - } + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("CastModels"); + return builder.GetEdmModel(); + } - private static IEdmModel GetParameterAliasModel() - { - //var config = RoutingConfigurationFactory.CreateWithTypes(typeof(DataTypes)); - //var builder = ODataConventionModelBuilderFactory.Create(config); - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("ParameterAliasModels"); - return builder.GetEdmModel(); - } + private static IEdmModel GetParameterAliasModel() + { + //var config = RoutingConfigurationFactory.CreateWithTypes(typeof(DataTypes)); + //var builder = ODataConventionModelBuilderFactory.Create(config); + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("ParameterAliasModels"); + return builder.GetEdmModel(); + } - private static IEdmModel GetPropertyAliasModel() - { - //var config = RoutingConfigurationFactory.CreateWithTypes(typeof(PropertyAlias)); - // var builder = ODataConventionModelBuilderFactory.CreateWithModelAliasing(config, modelAliasing: true); - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("PropertyAliases"); - return builder.GetEdmModel(); - } + private static IEdmModel GetPropertyAliasModel() + { + //var config = RoutingConfigurationFactory.CreateWithTypes(typeof(PropertyAlias)); + // var builder = ODataConventionModelBuilderFactory.CreateWithModelAliasing(config, modelAliasing: true); + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("PropertyAliases"); + return builder.GetEdmModel(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/MyQueryProvider.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/MyQueryProvider.cs new file mode 100644 index 000000000..66fdfa315 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/MyQueryProvider.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Linq; +using System.Linq.Expressions; + +namespace System.Data.Linq; + +public class MyQueryProvider : IQueryProvider +{ + public IQueryable CreateQuery(Expression expression) + { + throw new NotImplementedException(); + } + + public IQueryable CreateQuery(Expression expression) + { + throw new NotImplementedException(); + } + + public object Execute(Expression expression) + { + throw new NotImplementedException(); + } + + public TResult Execute(Expression expression) + { + throw new NotImplementedException(); + } +} diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryOptionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryOptionTests.cs index 326c1da40..dbfff824f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryOptionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryOptionTests.cs @@ -32,1427 +32,1426 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class ODataQueryOptionTests { - public class ODataQueryOptionTests - { - internal static IQueryable Customers = new List().AsQueryable(); + internal static IQueryable Customers = new List().AsQueryable(); - [Fact] - public void CtorODataQueryOption_ThrowsArgumentNull_Context() - { - // Arrange - Mock request = new Mock(); + [Fact] + public void CtorODataQueryOption_ThrowsArgumentNull_Context() + { + // Arrange + Mock request = new Mock(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new ODataQueryOptions(null, request.Object), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new ODataQueryOptions(null, request.Object), "context"); + } - [Fact] - public void CtorODataQueryOption_ThrowsArgumentNull_Request() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new ODataQueryOptions(new ODataQueryContext(EdmCoreModel.Instance, typeof(bool)), null), "request"); - } + [Fact] + public void CtorODataQueryOption_ThrowsArgumentNull_Request() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new ODataQueryOptions(new ODataQueryContext(EdmCoreModel.Instance, typeof(bool)), null), "request"); + } - [Fact] - public void ValidateODataQueryOption_ThrowsArgumentNull_ValidationSettingst() - { - // Arrange & Act & Assert - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://any"); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + [Fact] + public void ValidateODataQueryOption_ThrowsArgumentNull_ValidationSettingst() + { + // Arrange & Act & Assert + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://any"); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - ExceptionAssert.ThrowsArgumentNull(() => queryOptions.Validate(null), "validationSettings"); - } + ExceptionAssert.ThrowsArgumentNull(() => queryOptions.Validate(null), "validationSettings"); + } - [Theory] - [InlineData("$filter")] - [InlineData("$count")] - [InlineData("$orderby")] - [InlineData("$skip")] - [InlineData("$top")] - public void CtorODataQueryOption_ThrowsIfEmptyQueryOptionValue(string queryName) - { - // Arrange - ODataModelBuilder builder = new ODataModelBuilder(); - builder.EntityType(); - IEdmModel model = builder.GetEdmModel(); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers/?" + queryName + "=", setupAction: null); - - // Act & Assert - ExceptionAssert.Throws(() => new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request), - "The value for OData query '" + queryName + "' cannot be empty."); - } + [Theory] + [InlineData("$filter")] + [InlineData("$count")] + [InlineData("$orderby")] + [InlineData("$skip")] + [InlineData("$top")] + public void CtorODataQueryOption_ThrowsIfEmptyQueryOptionValue(string queryName) + { + // Arrange + ODataModelBuilder builder = new ODataModelBuilder(); + builder.EntityType(); + IEdmModel model = builder.GetEdmModel(); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers/?" + queryName + "=", setupAction: null); + + // Act & Assert + ExceptionAssert.Throws(() => new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request), + "The value for OData query '" + queryName + "' cannot be empty."); + } - [Fact] - public void CtorODataQueryOption_CanExtractQueryOptionsCorrectly() - { - // Arrange - ODataModelBuilder builder = new ODataModelBuilder(); - builder.EntityType(); - IEdmModel model = builder.GetEdmModel(); - - HttpRequest request = RequestFactory.Create( - HttpMethods.Get, - "http://server/service/Customers/?$filter=Filter&$select=Select&$orderby=OrderBy&$expand=Expand&$top=10&$skip=20&$count=true&$skiptoken=SkipToken&$deltatoken=DeltaToken"); - - // Act - ODataQueryOptions queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - - // Assert - Assert.Equal("Filter", queryOptions.RawValues.Filter); - Assert.NotNull(queryOptions.Filter); - Assert.Equal("OrderBy", queryOptions.RawValues.OrderBy); - Assert.NotNull(queryOptions.OrderBy); - Assert.Equal("10", queryOptions.RawValues.Top); - Assert.NotNull(queryOptions.Top); - Assert.Equal("20", queryOptions.RawValues.Skip); - Assert.NotNull(queryOptions.Skip); - Assert.Equal("Expand", queryOptions.RawValues.Expand); - Assert.Equal("Select", queryOptions.RawValues.Select); - Assert.NotNull(queryOptions.SelectExpand); - Assert.Equal("true", queryOptions.RawValues.Count); - Assert.Equal("SkipToken", queryOptions.RawValues.SkipToken); - Assert.Equal("DeltaToken", queryOptions.RawValues.DeltaToken); - } + [Fact] + public void CtorODataQueryOption_CanExtractQueryOptionsCorrectly() + { + // Arrange + ODataModelBuilder builder = new ODataModelBuilder(); + builder.EntityType(); + IEdmModel model = builder.GetEdmModel(); + + HttpRequest request = RequestFactory.Create( + HttpMethods.Get, + "http://server/service/Customers/?$filter=Filter&$select=Select&$orderby=OrderBy&$expand=Expand&$top=10&$skip=20&$count=true&$skiptoken=SkipToken&$deltatoken=DeltaToken"); + + // Act + ODataQueryOptions queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + + // Assert + Assert.Equal("Filter", queryOptions.RawValues.Filter); + Assert.NotNull(queryOptions.Filter); + Assert.Equal("OrderBy", queryOptions.RawValues.OrderBy); + Assert.NotNull(queryOptions.OrderBy); + Assert.Equal("10", queryOptions.RawValues.Top); + Assert.NotNull(queryOptions.Top); + Assert.Equal("20", queryOptions.RawValues.Skip); + Assert.NotNull(queryOptions.Skip); + Assert.Equal("Expand", queryOptions.RawValues.Expand); + Assert.Equal("Select", queryOptions.RawValues.Select); + Assert.NotNull(queryOptions.SelectExpand); + Assert.Equal("true", queryOptions.RawValues.Count); + Assert.Equal("SkipToken", queryOptions.RawValues.SkipToken); + Assert.Equal("DeltaToken", queryOptions.RawValues.DeltaToken); + } - [Theory] - [InlineData(" $filter=Filter& $select=Select& $orderby=OrderBy& $expand=Expand& $top=10& $skip=20& $count=true& $skiptoken=SkipToken& $deltatoken=DeltaToken")] - [InlineData("%20$filter=Filter&%20$select=Select&%20$orderby=OrderBy&%20$expand=Expand&%20$top=10&%20$skip=20&%20$count=true&%20$skiptoken=SkipToken&%20$deltatoken=DeltaToken")] - [InlineData("$filter =Filter&$select =Select&$orderby =OrderBy&$expand =Expand&$top =10&$skip =20&$count =true&$skiptoken =SkipToken&$deltatoken =DeltaToken")] - [InlineData("$filter%20=Filter&$select%20=Select&$orderby%20=OrderBy&$expand%20=Expand&$top%20=10&$skip%20=20&$count%20=true&$skiptoken%20=SkipToken&$deltatoken%20=DeltaToken")] - [InlineData(" $filter =Filter& $select =Select& $orderby =OrderBy& $expand =Expand& $top =10& $skip =20& $count =true& $skiptoken =SkipToken& $deltatoken =DeltaToken")] - public void CtorODataQueryOption_CanExtractQueryOptionsWithExtraSpacesCorrectly(string clause) - { - // Arrange - ODataModelBuilder builder = new ODataModelBuilder(); - builder.EntityType(); - IEdmModel model = builder.GetEdmModel(); - - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers/?"+ clause); - - // Act - ODataQueryOptions queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - - // Assert - Assert.Equal("Filter", queryOptions.RawValues.Filter); - Assert.NotNull(queryOptions.Filter); - Assert.Equal("OrderBy", queryOptions.RawValues.OrderBy); - Assert.NotNull(queryOptions.OrderBy); - Assert.Equal("10", queryOptions.RawValues.Top); - Assert.NotNull(queryOptions.Top); - Assert.Equal("20", queryOptions.RawValues.Skip); - Assert.NotNull(queryOptions.Skip); - Assert.Equal("Expand", queryOptions.RawValues.Expand); - Assert.Equal("Select", queryOptions.RawValues.Select); - Assert.NotNull(queryOptions.SelectExpand); - Assert.Equal("true", queryOptions.RawValues.Count); - Assert.Equal("SkipToken", queryOptions.RawValues.SkipToken); - Assert.Equal("DeltaToken", queryOptions.RawValues.DeltaToken); - } + [Theory] + [InlineData(" $filter=Filter& $select=Select& $orderby=OrderBy& $expand=Expand& $top=10& $skip=20& $count=true& $skiptoken=SkipToken& $deltatoken=DeltaToken")] + [InlineData("%20$filter=Filter&%20$select=Select&%20$orderby=OrderBy&%20$expand=Expand&%20$top=10&%20$skip=20&%20$count=true&%20$skiptoken=SkipToken&%20$deltatoken=DeltaToken")] + [InlineData("$filter =Filter&$select =Select&$orderby =OrderBy&$expand =Expand&$top =10&$skip =20&$count =true&$skiptoken =SkipToken&$deltatoken =DeltaToken")] + [InlineData("$filter%20=Filter&$select%20=Select&$orderby%20=OrderBy&$expand%20=Expand&$top%20=10&$skip%20=20&$count%20=true&$skiptoken%20=SkipToken&$deltatoken%20=DeltaToken")] + [InlineData(" $filter =Filter& $select =Select& $orderby =OrderBy& $expand =Expand& $top =10& $skip =20& $count =true& $skiptoken =SkipToken& $deltatoken =DeltaToken")] + public void CtorODataQueryOption_CanExtractQueryOptionsWithExtraSpacesCorrectly(string clause) + { + // Arrange + ODataModelBuilder builder = new ODataModelBuilder(); + builder.EntityType(); + IEdmModel model = builder.GetEdmModel(); + + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers/?"+ clause); + + // Act + ODataQueryOptions queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + + // Assert + Assert.Equal("Filter", queryOptions.RawValues.Filter); + Assert.NotNull(queryOptions.Filter); + Assert.Equal("OrderBy", queryOptions.RawValues.OrderBy); + Assert.NotNull(queryOptions.OrderBy); + Assert.Equal("10", queryOptions.RawValues.Top); + Assert.NotNull(queryOptions.Top); + Assert.Equal("20", queryOptions.RawValues.Skip); + Assert.NotNull(queryOptions.Skip); + Assert.Equal("Expand", queryOptions.RawValues.Expand); + Assert.Equal("Select", queryOptions.RawValues.Select); + Assert.NotNull(queryOptions.SelectExpand); + Assert.Equal("true", queryOptions.RawValues.Count); + Assert.Equal("SkipToken", queryOptions.RawValues.SkipToken); + Assert.Equal("DeltaToken", queryOptions.RawValues.DeltaToken); + } - [Fact] - public void ApplyToODataQueryOption_Throws_With_Null_Queryable() - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://any"); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + [Fact] + public void ApplyToODataQueryOption_Throws_With_Null_Queryable() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://any"); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => queryOptions.ApplyTo(null), "query"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => queryOptions.ApplyTo(null), "query"); + } - [Fact] - public void ApplyToODataQueryOption_With_QuerySettings_Throws_With_Null_Queryable() - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://any"); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + [Fact] + public void ApplyToODataQueryOption_With_QuerySettings_Throws_With_Null_Queryable() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://any"); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => queryOptions.ApplyTo(null, new ODataQuerySettings()), "query"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => queryOptions.ApplyTo(null, new ODataQuerySettings()), "query"); + } - [Fact] - public void ApplyToODataQueryOption_Throws_With_Null_QuerySettings() - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://any"); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + [Fact] + public void ApplyToODataQueryOption_Throws_With_Null_QuerySettings() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://any"); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => queryOptions.ApplyTo(new Customer[0].AsQueryable(), null), "querySettings"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => queryOptions.ApplyTo(new Customer[0].AsQueryable(), null), "querySettings"); + } - // Used to test modifications to $orderby when $skip or $top are present - // and the entity type has 2 keys -- CustomerId and Name. - // Tuple is: query expression, ensureStableOrdering, expected expression - public static TheoryDataSet SkipTopOrderByUsingKeysTestData + // Used to test modifications to $orderby when $skip or $top are present + // and the entity type has 2 keys -- CustomerId and Name. + // Tuple is: query expression, ensureStableOrdering, expected expression + public static TheoryDataSet SkipTopOrderByUsingKeysTestData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - // First key present with $skip, adds 2nd key - { "$orderby=CustomerId&$skip=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).Skip(1)" }, + // First key present with $skip, adds 2nd key + { "$orderby=CustomerId&$skip=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).Skip(1)" }, - // First key present with $top, adds 2nd key - { "$orderby=CustomerId&$top=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).Take(1)" }, + // First key present with $top, adds 2nd key + { "$orderby=CustomerId&$top=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).Take(1)" }, - // First key present with $skip and $top, adds 2nd key - { "$orderby=CustomerId&$skip=1&$top=2", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).Skip(1).Take(2)" }, + // First key present with $skip and $top, adds 2nd key + { "$orderby=CustomerId&$skip=1&$top=2", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).Skip(1).Take(2)" }, - // First key present, no $skip or $top, no modification - { "$orderby=CustomerId", false, "OrderBy($it => $it.CustomerId)" }, + // First key present, no $skip or $top, no modification + { "$orderby=CustomerId", false, "OrderBy($it => $it.CustomerId)" }, - // First key present, 'ensureStableOrdering' is false, no modification - { "$orderby=CustomerId&$skip=1", false, "OrderBy($it => $it.CustomerId).Skip(1)" }, + // First key present, 'ensureStableOrdering' is false, no modification + { "$orderby=CustomerId&$skip=1", false, "OrderBy($it => $it.CustomerId).Skip(1)" }, - // Second key present, adds 1st key after 2nd - { "$orderby=Name&$skip=1", true, "OrderBy($it => $it.Name).ThenBy($it => $it.CustomerId).Skip(1)" }, + // Second key present, adds 1st key after 2nd + { "$orderby=Name&$skip=1", true, "OrderBy($it => $it.Name).ThenBy($it => $it.CustomerId).Skip(1)" }, - // Second key plus 'asc' suffix, adds 1st key and preserves suffix - { "$orderby=Name asc&$skip=1", true, "OrderBy($it => $it.Name).ThenBy($it => $it.CustomerId).Skip(1)" }, + // Second key plus 'asc' suffix, adds 1st key and preserves suffix + { "$orderby=Name asc&$skip=1", true, "OrderBy($it => $it.Name).ThenBy($it => $it.CustomerId).Skip(1)" }, - // Second key plus 'desc' suffix, adds 1st key and preserves suffix - { "$orderby=Name desc&$skip=1", true, "OrderByDescending($it => $it.Name).ThenBy($it => $it.CustomerId).Skip(1)" }, + // Second key plus 'desc' suffix, adds 1st key and preserves suffix + { "$orderby=Name desc&$skip=1", true, "OrderByDescending($it => $it.Name).ThenBy($it => $it.CustomerId).Skip(1)" }, - // All keys present, no modification - { "$orderby=CustomerId,Name&$skip=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).Skip(1)" }, + // All keys present, no modification + { "$orderby=CustomerId,Name&$skip=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).Skip(1)" }, - // All keys present but in reverse order, no modification - { "$orderby=Name,CustomerId&$skip=1", true, "OrderBy($it => $it.Name).ThenBy($it => $it.CustomerId).Skip(1)" }, + // All keys present but in reverse order, no modification + { "$orderby=Name,CustomerId&$skip=1", true, "OrderBy($it => $it.Name).ThenBy($it => $it.CustomerId).Skip(1)" }, - // First key present but with extraneous whitespace, adds 2nd key - { "$orderby= CustomerId &$skip=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).Skip(1)" }, + // First key present but with extraneous whitespace, adds 2nd key + { "$orderby= CustomerId &$skip=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).Skip(1)" }, - // All keys present with extraneous whitespace, no modification - { "$orderby= \t CustomerId \t , Name \t desc \t &$skip=1", true, "OrderBy($it => $it.CustomerId).ThenByDescending($it => $it.Name).Skip(1)" }, + // All keys present with extraneous whitespace, no modification + { "$orderby= \t CustomerId \t , Name \t desc \t &$skip=1", true, "OrderBy($it => $it.CustomerId).ThenByDescending($it => $it.Name).Skip(1)" }, - // Ordering on non-key property, adds all keys - { "$orderby=Website&$skip=1", true, "OrderBy($it => $it.Website).ThenBy($it => $it.CustomerId).ThenBy($it => $it.Name).Skip(1)" }, - }; - } + // Ordering on non-key property, adds all keys + { "$orderby=Website&$skip=1", true, "OrderBy($it => $it.Website).ThenBy($it => $it.CustomerId).ThenBy($it => $it.Name).Skip(1)" }, + }; } + } - [Theory] - [MemberData(nameof(SkipTopOrderByUsingKeysTestData))] - public void ApplyToODataQueryOption_Adds_Missing_Keys_To_OrderBy(string oDataQuery, bool ensureStableOrdering, string expectedExpression) - { - // Arrange - IEdmModel model = GetEdmModel(c => new { c.CustomerId, c.Name }); + [Theory] + [MemberData(nameof(SkipTopOrderByUsingKeysTestData))] + public void ApplyToODataQueryOption_Adds_Missing_Keys_To_OrderBy(string oDataQuery, bool ensureStableOrdering, string expectedExpression) + { + // Arrange + IEdmModel model = GetEdmModel(c => new { c.CustomerId, c.Name }); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?" + oDataQuery); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?" + oDataQuery); - var queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - ODataQuerySettings querySettings = new ODataQuerySettings - { - EnsureStableOrdering = ensureStableOrdering, - }; + var queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + ODataQuerySettings querySettings = new ODataQuerySettings + { + EnsureStableOrdering = ensureStableOrdering, + }; - // Act - IQueryable finalQuery = queryOptions.ApplyTo(new Customer[0].AsQueryable(), querySettings); + // Act + IQueryable finalQuery = queryOptions.ApplyTo(new Customer[0].AsQueryable(), querySettings); - // Assert - string queryExpression = ExpressionStringBuilder.ToString(finalQuery.Expression); - queryExpression = queryExpression.Substring(queryExpression.IndexOf("]") + 2); + // Assert + string queryExpression = ExpressionStringBuilder.ToString(finalQuery.Expression); + queryExpression = queryExpression.Substring(queryExpression.IndexOf("]") + 2); - Assert.Equal(queryExpression, expectedExpression); - } + Assert.Equal(queryExpression, expectedExpression); + } - // Used to test modifications to $orderby when $skip or $top are present - // and the entity type has a no key properties. - // Tuple is: query expression, ensureStableOrdering, expected expression - public static TheoryDataSet SkipTopOrderByWithNoKeysTestData + // Used to test modifications to $orderby when $skip or $top are present + // and the entity type has a no key properties. + // Tuple is: query expression, ensureStableOrdering, expected expression + public static TheoryDataSet SkipTopOrderByWithNoKeysTestData + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - // Single property present with $skip, adds all remaining in alphabetic order - { "$orderby=CustomerId&$skip=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).ThenBy($it => $it.SharePrice).ThenBy($it => $it.ShareSymbol).ThenBy($it => $it.Website).Skip(1)" }, + // Single property present with $skip, adds all remaining in alphabetic order + { "$orderby=CustomerId&$skip=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).ThenBy($it => $it.SharePrice).ThenBy($it => $it.ShareSymbol).ThenBy($it => $it.Website).Skip(1)" }, - // Single property present with $top, adds all remaining in alphabetic order - { "$orderby=CustomerId&$top=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).ThenBy($it => $it.SharePrice).ThenBy($it => $it.ShareSymbol).ThenBy($it => $it.Website).Take(1)" }, + // Single property present with $top, adds all remaining in alphabetic order + { "$orderby=CustomerId&$top=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).ThenBy($it => $it.SharePrice).ThenBy($it => $it.ShareSymbol).ThenBy($it => $it.Website).Take(1)" }, - // Single property present with $skip and $top, adds all remaining in alphabetic order - { "$orderby=CustomerId&$skip=1&$top=2", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).ThenBy($it => $it.SharePrice).ThenBy($it => $it.ShareSymbol).ThenBy($it => $it.Website).Skip(1).Take(2)" }, + // Single property present with $skip and $top, adds all remaining in alphabetic order + { "$orderby=CustomerId&$skip=1&$top=2", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).ThenBy($it => $it.SharePrice).ThenBy($it => $it.ShareSymbol).ThenBy($it => $it.Website).Skip(1).Take(2)" }, - // Single property present, no $skip or $top, no modification - { "$orderby=SharePrice", false, "OrderBy($it => $it.SharePrice)" }, + // Single property present, no $skip or $top, no modification + { "$orderby=SharePrice", false, "OrderBy($it => $it.SharePrice)" }, - // Single property present, ensureStableOrdering is false, no modification - { "$orderby=SharePrice&$skip=1", false, "OrderBy($it => $it.SharePrice).Skip(1)" }, + // Single property present, ensureStableOrdering is false, no modification + { "$orderby=SharePrice&$skip=1", false, "OrderBy($it => $it.SharePrice).Skip(1)" }, - // All properties present, non-alphabetic order, no modification - { "$orderby=Name,SharePrice,CustomerId,Website,ShareSymbol&$skip=1", true, "OrderBy($it => $it.Name).ThenBy($it => $it.SharePrice).ThenBy($it => $it.CustomerId).ThenBy($it => $it.Website).ThenBy($it => $it.ShareSymbol).Skip(1)" }, + // All properties present, non-alphabetic order, no modification + { "$orderby=Name,SharePrice,CustomerId,Website,ShareSymbol&$skip=1", true, "OrderBy($it => $it.Name).ThenBy($it => $it.SharePrice).ThenBy($it => $it.CustomerId).ThenBy($it => $it.Website).ThenBy($it => $it.ShareSymbol).Skip(1)" }, - // All properties present, extraneous whitespace, non-alphabetic order, no modification - { "$orderby= \t Name \t , \t SharePrice \t , \t CustomerId \t , \t Website \t , \t ShareSymbol \t &$skip=1", true, "OrderBy($it => $it.Name).ThenBy($it => $it.SharePrice).ThenBy($it => $it.CustomerId).ThenBy($it => $it.Website).ThenBy($it => $it.ShareSymbol).Skip(1)" }, + // All properties present, extraneous whitespace, non-alphabetic order, no modification + { "$orderby= \t Name \t , \t SharePrice \t , \t CustomerId \t , \t Website \t , \t ShareSymbol \t &$skip=1", true, "OrderBy($it => $it.Name).ThenBy($it => $it.SharePrice).ThenBy($it => $it.CustomerId).ThenBy($it => $it.Website).ThenBy($it => $it.ShareSymbol).Skip(1)" }, - }; - } + }; } + } + + [Theory] + [MemberData(nameof(SkipTopOrderByWithNoKeysTestData))] + public void ApplyToODataQueryOption_Adds_Missing_NonKey_Properties_To_OrderBy(string oDataQuery, bool ensureStableOrdering, string expectedExpression) + { + // Arrange + IEdmModel model = GetEdmModelWithoutKey(); - [Theory] - [MemberData(nameof(SkipTopOrderByWithNoKeysTestData))] - public void ApplyToODataQueryOption_Adds_Missing_NonKey_Properties_To_OrderBy(string oDataQuery, bool ensureStableOrdering, string expectedExpression) + var request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?" + oDataQuery); + + var queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + ODataQuerySettings querySettings = new ODataQuerySettings { - // Arrange - IEdmModel model = GetEdmModelWithoutKey(); + EnsureStableOrdering = ensureStableOrdering + }; - var request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?" + oDataQuery); + // Act + IQueryable finalQuery = queryOptions.ApplyTo(new Customer[0].AsQueryable(), querySettings); - var queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - ODataQuerySettings querySettings = new ODataQuerySettings - { - EnsureStableOrdering = ensureStableOrdering - }; + // Assert + string queryExpression = ExpressionStringBuilder.ToString(finalQuery.Expression); + queryExpression = queryExpression.Substring(queryExpression.IndexOf("]") + 2); - // Act - IQueryable finalQuery = queryOptions.ApplyTo(new Customer[0].AsQueryable(), querySettings); + Assert.Equal(queryExpression, expectedExpression); + } - // Assert - string queryExpression = ExpressionStringBuilder.ToString(finalQuery.Expression); - queryExpression = queryExpression.Substring(queryExpression.IndexOf("]") + 2); + [Fact] + public void ApplyToODataQueryOption_Does_Not_Replace_Original_OrderBy_With_Missing_Keys() + { + // Arrange + IEdmModel model = GetEdmModelWithoutKey(); - Assert.Equal(queryExpression, expectedExpression); - } + var request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$orderby=Name"); - [Fact] - public void ApplyToODataQueryOption_Does_Not_Replace_Original_OrderBy_With_Missing_Keys() - { - // Arrange - IEdmModel model = GetEdmModelWithoutKey(); + // Act + var queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + OrderByQueryOption originalOption = queryOptions.OrderBy; + ODataQuerySettings querySettings = new ODataQuerySettings(); - var request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$orderby=Name"); + queryOptions.ApplyTo(new Customer[0].AsQueryable(), querySettings); - // Act - var queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - OrderByQueryOption originalOption = queryOptions.OrderBy; - ODataQuerySettings querySettings = new ODataQuerySettings(); + // Assert + Assert.Same(originalOption, queryOptions.OrderBy); + } - queryOptions.ApplyTo(new Customer[0].AsQueryable(), querySettings); + [Fact] + public void ApplyToODataQueryOption_SetsRequestSelectExpandClause_IfSelectExpandIsNotNull() + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$select=Name"); + ODataQueryOptions queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - // Assert - Assert.Same(originalOption, queryOptions.OrderBy); - } + // Act + queryOptions.ApplyTo(Enumerable.Empty().AsQueryable()); - [Fact] - public void ApplyToODataQueryOption_SetsRequestSelectExpandClause_IfSelectExpandIsNotNull() - { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$select=Name"); - ODataQueryOptions queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + // Assert + Assert.NotNull(request.ODataFeature().SelectExpandClause); + } - // Act - queryOptions.ApplyTo(Enumerable.Empty().AsQueryable()); + [Fact] + [Trait("ODataQueryOption", "Can bind a typed ODataQueryOption to the request uri without any query")] + public void CtorODataQueryOption_ContextPropertyGetter() + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers"); - // Assert - Assert.NotNull(request.ODataFeature().SelectExpandClause); - } + // Act + ODataQueryOptions queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - [Fact] - [Trait("ODataQueryOption", "Can bind a typed ODataQueryOption to the request uri without any query")] - public void CtorODataQueryOption_ContextPropertyGetter() + // Assert + Type entityType = queryOptions.Context.ElementClrType; + Assert.NotNull(entityType); + Assert.Equal("Microsoft.AspNetCore.OData.Tests.Query.Customer", entityType.Namespace + "." + entityType.Name); + } + + public static TheoryDataSet QueryTestData + { + get { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers"); + return new TheoryDataSet + { + { "$filter", null }, + { "$filter", "''" }, + { "$filter", "" }, + { "$filter", " " }, + { "$filter", "Name eq 'MSFT'" }, + + { "$orderby", null }, + { "$orderby", "''" }, + { "$orderby", "" }, + { "$orderby", " " }, + { "$orderby", "Name" }, + + { "$top", null }, + { "$top", "''" }, + { "$top", "" }, + { "$top", " " }, + { "$top", "12" }, + + { "$skip", null }, + { "$skip", "''" }, + { "$skip", "" }, + { "$skip", " " }, + { "$skip", "12" }, + + { "$apply", null }, + { "$apply", "" }, + { "$apply", " " }, + { "$apply", "aggregate(SharePrice mul CustomerId with sum as Name)" }, + }; + } + } - // Act - ODataQueryOptions queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + [Theory] + [MemberData(nameof(QueryTestData))] + public void CtorODataQueryOptions_QueryTest(string queryName, string queryValue) + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); - // Assert - Type entityType = queryOptions.Context.ElementClrType; - Assert.NotNull(entityType); - Assert.Equal("Microsoft.AspNetCore.OData.Tests.Query.Customer", entityType.Namespace + "." + entityType.Name); + string uri; + if (queryValue == null) + { + // same as not passing the query - - this would work + uri = string.Format("http://server/service/Customers?{0}=", queryName); } - - public static TheoryDataSet QueryTestData + else { - get - { - return new TheoryDataSet - { - { "$filter", null }, - { "$filter", "''" }, - { "$filter", "" }, - { "$filter", " " }, - { "$filter", "Name eq 'MSFT'" }, - - { "$orderby", null }, - { "$orderby", "''" }, - { "$orderby", "" }, - { "$orderby", " " }, - { "$orderby", "Name" }, - - { "$top", null }, - { "$top", "''" }, - { "$top", "" }, - { "$top", " " }, - { "$top", "12" }, - - { "$skip", null }, - { "$skip", "''" }, - { "$skip", "" }, - { "$skip", " " }, - { "$skip", "12" }, - - { "$apply", null }, - { "$apply", "" }, - { "$apply", " " }, - { "$apply", "aggregate(SharePrice mul CustomerId with sum as Name)" }, - }; - } + // if queryValue is invalid, such as whitespace or not a number for top and skip + uri = string.Format("http://server/service/Customers?{0}={1}", queryName, queryValue); } - [Theory] - [MemberData(nameof(QueryTestData))] - public void CtorODataQueryOptions_QueryTest(string queryName, string queryValue) + var request = RequestFactory.Create(HttpMethods.Get, uri); + + // Act && Assert + if (string.IsNullOrWhiteSpace(queryValue)) + { + ExceptionAssert.Throws(() => + new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request)); + } + else { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); + var queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - string uri; - if (queryValue == null) + if (queryName == "$filter") { - // same as not passing the query - - this would work - uri = string.Format("http://server/service/Customers?{0}=", queryName); + Assert.Equal(queryValue, queryOptions.RawValues.Filter); } - else + else if (queryName == "$orderby") { - // if queryValue is invalid, such as whitespace or not a number for top and skip - uri = string.Format("http://server/service/Customers?{0}={1}", queryName, queryValue); + Assert.Equal(queryValue, queryOptions.RawValues.OrderBy); } - - var request = RequestFactory.Create(HttpMethods.Get, uri); - - // Act && Assert - if (string.IsNullOrWhiteSpace(queryValue)) + else if (queryName == "$top") { - ExceptionAssert.Throws(() => - new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request)); + Assert.Equal(queryValue, queryOptions.RawValues.Top); } - else + else if (queryName == "$skip") { - var queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - - if (queryName == "$filter") - { - Assert.Equal(queryValue, queryOptions.RawValues.Filter); - } - else if (queryName == "$orderby") - { - Assert.Equal(queryValue, queryOptions.RawValues.OrderBy); - } - else if (queryName == "$top") - { - Assert.Equal(queryValue, queryOptions.RawValues.Top); - } - else if (queryName == "$skip") - { - Assert.Equal(queryValue, queryOptions.RawValues.Skip); - } - else if (queryName == "$apply") - { - Assert.Equal(queryValue, queryOptions.RawValues.Apply); - } + Assert.Equal(queryValue, queryOptions.RawValues.Skip); + } + else if (queryName == "$apply") + { + Assert.Equal(queryValue, queryOptions.RawValues.Apply); } } + } - [Fact] - public void CtorODataQueryOptions_MissingQueryReturnsOriginalList() - { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); - - // the query is completely missing - this would work - string uri = "http://server/service/Customers"; - HttpRequest request = RequestFactory.Create(HttpMethods.Get, uri); - - // Act - ODataQueryOptions queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + [Fact] + public void CtorODataQueryOptions_MissingQueryReturnsOriginalList() + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); - // Assert: everything is null - Assert.Null(queryOptions.RawValues.OrderBy); - Assert.Null(queryOptions.RawValues.Filter); - Assert.Null(queryOptions.RawValues.Skip); - Assert.Null(queryOptions.RawValues.Top); - } + // the query is completely missing - this would work + string uri = "http://server/service/Customers"; + HttpRequest request = RequestFactory.Create(HttpMethods.Get, uri); - [Fact] - public void ApplyToODataQueryOptions_OrderbyWithUnknownPropertyThrows() - { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$orderby=UnknownProperty"); + // Act + ODataQueryOptions queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - // Act & Assert - ODataQueryOptions option = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - ExceptionAssert.Throws(() => option.ApplyTo(new List().AsQueryable())); - } - - [Fact] - public void ApplyToODataQueryOptions_CannotConvertBadTopQueryThrows() - { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$top=NotANumber"); + // Assert: everything is null + Assert.Null(queryOptions.RawValues.OrderBy); + Assert.Null(queryOptions.RawValues.Filter); + Assert.Null(queryOptions.RawValues.Skip); + Assert.Null(queryOptions.RawValues.Top); + } - // Act & Assert - ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - ExceptionAssert.Throws(() => - options.ApplyTo(Customers), - "Invalid value 'NotANumber' for $top query option found. " + - "The $top query option requires a non-negative integer value."); + [Fact] + public void ApplyToODataQueryOptions_OrderbyWithUnknownPropertyThrows() + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$orderby=UnknownProperty"); - // Act & Assert - request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$top=''"); + // Act & Assert + ODataQueryOptions option = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + ExceptionAssert.Throws(() => option.ApplyTo(new List().AsQueryable())); + } - options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - ExceptionAssert.Throws(() => - options.ApplyTo(Customers), - "Invalid value '''' for $top query option found. " + - "The $top query option requires a non-negative integer value."); - } + [Fact] + public void ApplyToODataQueryOptions_CannotConvertBadTopQueryThrows() + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$top=NotANumber"); + + // Act & Assert + ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + ExceptionAssert.Throws(() => + options.ApplyTo(Customers), + "Invalid value 'NotANumber' for $top query option found. " + + "The $top query option requires a non-negative integer value."); + + // Act & Assert + request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$top=''"); + + options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + ExceptionAssert.Throws(() => + options.ApplyTo(Customers), + "Invalid value '''' for $top query option found. " + + "The $top query option requires a non-negative integer value."); + } - [Fact] - public void ApplyToODataQueryOptions_CannotConvertBadSkipQueryThrows() - { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); + [Fact] + public void ApplyToODataQueryOptions_CannotConvertBadSkipQueryThrows() + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); + + // Act & Assert + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$skip=NotANumber"); + ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + ExceptionAssert.Throws(() => + options.ApplyTo(Customers), + "Invalid value 'NotANumber' for $skip query option found. " + + "The $skip query option requires a non-negative integer value."); + + // Act & Assert + request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$skip=''"); + options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + ExceptionAssert.Throws(() => + options.ApplyTo(Customers), + "Invalid value '''' for $skip query option found. " + + "The $skip query option requires a non-negative integer value."); + } - // Act & Assert - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$skip=NotANumber"); - ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - ExceptionAssert.Throws(() => - options.ApplyTo(Customers), - "Invalid value 'NotANumber' for $skip query option found. " + - "The $skip query option requires a non-negative integer value."); + [Theory] + [InlineData("$skip=1", typeof(ODataQueryOptionTest_ComplexModel), "OrderBy($it => $it.A).ThenBy($it => $it.B).Skip(1)")] + [InlineData("$skip=1", typeof(ODataQueryOptionTest_EntityModel), "OrderBy($it => $it.ID).Skip(1)")] + [InlineData("$skip=1", typeof(ODataQueryOptionTest_EntityModelMultipleKeys), "OrderBy($it => $it.ID1).ThenBy($it => $it.ID2).Skip(1)")] + [InlineData("$top=1", typeof(ODataQueryOptionTest_ComplexModel), "OrderBy($it => $it.A).ThenBy($it => $it.B).Take(1)")] + [InlineData("$top=1", typeof(ODataQueryOptionTest_EntityModel), "OrderBy($it => $it.ID).Take(1)")] + [InlineData("$top=1", typeof(ODataQueryOptionTest_EntityModelMultipleKeys), "OrderBy($it => $it.ID1).ThenBy($it => $it.ID2).Take(1)")] + [InlineData("$skip=1&$top=1", typeof(ODataQueryOptionTest_ComplexModel), "OrderBy($it => $it.A).ThenBy($it => $it.B).Skip(1).Take(1)")] + [InlineData("$skip=1&$top=1", typeof(ODataQueryOptionTest_EntityModel), "OrderBy($it => $it.ID).Skip(1).Take(1)")] + [InlineData("$skip=1&$top=1", typeof(ODataQueryOptionTest_EntityModelMultipleKeys), "OrderBy($it => $it.ID1).ThenBy($it => $it.ID2).Skip(1).Take(1)")] + public void ApplyToODataQueryOptions_Picks_DefaultOrder(string oDataQuery, Type elementType, string expectedExpression) + { + // Arrange + IQueryable query = Array.CreateInstance(elementType, 0).AsQueryable(); + ODataConventionModelBuilder modelBuilder = ODataModelBuilderMocks.GetModelBuilderMock(); + modelBuilder.AddEntitySet("entityset", modelBuilder.AddEntityType(elementType)); + IEdmModel model = modelBuilder.GetEdmModel(); - // Act & Assert - request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$skip=''"); - options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - ExceptionAssert.Throws(() => - options.ApplyTo(Customers), - "Invalid value '''' for $skip query option found. " + - "The $skip query option requires a non-negative integer value."); - } + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/entityset?" + oDataQuery); - [Theory] - [InlineData("$skip=1", typeof(ODataQueryOptionTest_ComplexModel), "OrderBy($it => $it.A).ThenBy($it => $it.B).Skip(1)")] - [InlineData("$skip=1", typeof(ODataQueryOptionTest_EntityModel), "OrderBy($it => $it.ID).Skip(1)")] - [InlineData("$skip=1", typeof(ODataQueryOptionTest_EntityModelMultipleKeys), "OrderBy($it => $it.ID1).ThenBy($it => $it.ID2).Skip(1)")] - [InlineData("$top=1", typeof(ODataQueryOptionTest_ComplexModel), "OrderBy($it => $it.A).ThenBy($it => $it.B).Take(1)")] - [InlineData("$top=1", typeof(ODataQueryOptionTest_EntityModel), "OrderBy($it => $it.ID).Take(1)")] - [InlineData("$top=1", typeof(ODataQueryOptionTest_EntityModelMultipleKeys), "OrderBy($it => $it.ID1).ThenBy($it => $it.ID2).Take(1)")] - [InlineData("$skip=1&$top=1", typeof(ODataQueryOptionTest_ComplexModel), "OrderBy($it => $it.A).ThenBy($it => $it.B).Skip(1).Take(1)")] - [InlineData("$skip=1&$top=1", typeof(ODataQueryOptionTest_EntityModel), "OrderBy($it => $it.ID).Skip(1).Take(1)")] - [InlineData("$skip=1&$top=1", typeof(ODataQueryOptionTest_EntityModelMultipleKeys), "OrderBy($it => $it.ID1).ThenBy($it => $it.ID2).Skip(1).Take(1)")] - public void ApplyToODataQueryOptions_Picks_DefaultOrder(string oDataQuery, Type elementType, string expectedExpression) - { - // Arrange - IQueryable query = Array.CreateInstance(elementType, 0).AsQueryable(); - ODataConventionModelBuilder modelBuilder = ODataModelBuilderMocks.GetModelBuilderMock(); - modelBuilder.AddEntitySet("entityset", modelBuilder.AddEntityType(elementType)); - IEdmModel model = modelBuilder.GetEdmModel(); + // Act + ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(model, elementType), request); + IQueryable finalQuery = options.ApplyTo(query); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/entityset?" + oDataQuery); + // Assert + string queryExpression = ExpressionStringBuilder.ToString(finalQuery.Expression); + queryExpression = queryExpression.Substring(queryExpression.IndexOf("OrderBy")); - // Act - ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(model, elementType), request); - IQueryable finalQuery = options.ApplyTo(query); + Assert.Equal(queryExpression, expectedExpression); + } - // Assert - string queryExpression = ExpressionStringBuilder.ToString(finalQuery.Expression); - queryExpression = queryExpression.Substring(queryExpression.IndexOf("OrderBy")); + [Theory] + [InlineData("$filter=1 eq 1")] + [InlineData("")] + public void ApplyToODataQueryOptions_DoesnotPickDefaultOrder_IfSkipAndTopAreNotPresent(string oDataQuery) + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); - Assert.Equal(queryExpression, expectedExpression); - } + // Act + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?" + oDataQuery); - [Theory] - [InlineData("$filter=1 eq 1")] - [InlineData("")] - public void ApplyToODataQueryOptions_DoesnotPickDefaultOrder_IfSkipAndTopAreNotPresent(string oDataQuery) - { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); + ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + IQueryable finalQuery = options.ApplyTo(Customers); - // Act - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?" + oDataQuery); + // Assert + string queryExpression = finalQuery.Expression.ToString(); + Assert.DoesNotContain("OrderBy", queryExpression); + } - ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - IQueryable finalQuery = options.ApplyTo(Customers); + [Theory] + [InlineData("$orderby=Name", "OrderBy($it => $it.Name)")] + [InlineData("$orderby=Website", "OrderBy($it => $it.Website)")] + [InlineData("$orderby=Name&$skip=1", "OrderBy($it => $it.Name).ThenBy($it => $it.CustomerId).Skip(1)")] + [InlineData("$orderby=Website&$top=1&$skip=1", "OrderBy($it => $it.Website).ThenBy($it => $it.CustomerId).Skip(1).Take(1)")] + public void ApplyToODataQueryOptions__DoesnotPickDefaultOrder_IfOrderByIsPresent(string oDataQuery, string expectedExpression) + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); - // Assert - string queryExpression = finalQuery.Expression.ToString(); - Assert.DoesNotContain("OrderBy", queryExpression); - } + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?" + oDataQuery); - [Theory] - [InlineData("$orderby=Name", "OrderBy($it => $it.Name)")] - [InlineData("$orderby=Website", "OrderBy($it => $it.Website)")] - [InlineData("$orderby=Name&$skip=1", "OrderBy($it => $it.Name).ThenBy($it => $it.CustomerId).Skip(1)")] - [InlineData("$orderby=Website&$top=1&$skip=1", "OrderBy($it => $it.Website).ThenBy($it => $it.CustomerId).Skip(1).Take(1)")] - public void ApplyToODataQueryOptions__DoesnotPickDefaultOrder_IfOrderByIsPresent(string oDataQuery, string expectedExpression) - { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); + // Act + ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + IQueryable finalQuery = options.ApplyTo(Customers); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?" + oDataQuery); + // Assert + string queryExpression = ExpressionStringBuilder.ToString(finalQuery.Expression); + queryExpression = queryExpression.Substring(queryExpression.IndexOf("OrderBy")); - // Act - ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - IQueryable finalQuery = options.ApplyTo(Customers); + Assert.Equal(queryExpression, expectedExpression); + } - // Assert - string queryExpression = ExpressionStringBuilder.ToString(finalQuery.Expression); - queryExpression = queryExpression.Substring(queryExpression.IndexOf("OrderBy")); + [Theory] + [InlineData("$skip=1", true, "OrderBy($it => $it.CustomerId).Skip(1)")] + [InlineData("$skip=1", false, "Skip(1)")] + [InlineData("$filter=1 eq 1", true, "Where($it => (1 == 1))")] + [InlineData("$filter=1 eq 1", false, "Where($it => (1 == 1))")] + public void ApplyToODataQueryOptions_Builds_Default_OrderBy_With_Keys(string oDataQuery, bool ensureStableOrdering, string expectedExpression) + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); - Assert.Equal(queryExpression, expectedExpression); - } + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?" + oDataQuery); - [Theory] - [InlineData("$skip=1", true, "OrderBy($it => $it.CustomerId).Skip(1)")] - [InlineData("$skip=1", false, "Skip(1)")] - [InlineData("$filter=1 eq 1", true, "Where($it => (1 == 1))")] - [InlineData("$filter=1 eq 1", false, "Where($it => (1 == 1))")] - public void ApplyToODataQueryOptions_Builds_Default_OrderBy_With_Keys(string oDataQuery, bool ensureStableOrdering, string expectedExpression) + ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + ODataQuerySettings querySettings = new ODataQuerySettings { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); + EnsureStableOrdering = ensureStableOrdering + }; - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?" + oDataQuery); + // Act + IQueryable finalQuery = options.ApplyTo(new Customer[0].AsQueryable(), querySettings); - ODataQueryOptions options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - ODataQuerySettings querySettings = new ODataQuerySettings - { - EnsureStableOrdering = ensureStableOrdering - }; + // Assert + string queryExpression = ExpressionStringBuilder.ToString(finalQuery.Expression); + queryExpression = queryExpression.Substring(queryExpression.IndexOf("]") + 2); - // Act - IQueryable finalQuery = options.ApplyTo(new Customer[0].AsQueryable(), querySettings); + Assert.Equal(queryExpression, expectedExpression); + } - // Assert - string queryExpression = ExpressionStringBuilder.ToString(finalQuery.Expression); - queryExpression = queryExpression.Substring(queryExpression.IndexOf("]") + 2); + [Theory] + [InlineData("$skip=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).ThenBy($it => $it.SharePrice).ThenBy($it => $it.ShareSymbol).ThenBy($it => $it.Website).Skip(1)")] + [InlineData("$skip=1", false, "Skip(1)")] + [InlineData("$filter=1 eq 1", true, "Where($it => (1 == 1))")] + [InlineData("$filter=1 eq 1", false, "Where($it => (1 == 1))")] + public void ApplyToODataQueryOptions_Builds_Default_OrderBy_No_Keys(string oDataQuery, bool ensureStableOrdering, string expectedExpression) + { + // Arrange + IEdmModel model = GetEdmModelWithoutKey(); - Assert.Equal(queryExpression, expectedExpression); - } + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?" + oDataQuery); - [Theory] - [InlineData("$skip=1", true, "OrderBy($it => $it.CustomerId).ThenBy($it => $it.Name).ThenBy($it => $it.SharePrice).ThenBy($it => $it.ShareSymbol).ThenBy($it => $it.Website).Skip(1)")] - [InlineData("$skip=1", false, "Skip(1)")] - [InlineData("$filter=1 eq 1", true, "Where($it => (1 == 1))")] - [InlineData("$filter=1 eq 1", false, "Where($it => (1 == 1))")] - public void ApplyToODataQueryOptions_Builds_Default_OrderBy_No_Keys(string oDataQuery, bool ensureStableOrdering, string expectedExpression) + var options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + ODataQuerySettings querySettings = new ODataQuerySettings { - // Arrange - IEdmModel model = GetEdmModelWithoutKey(); + EnsureStableOrdering = ensureStableOrdering + }; - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?" + oDataQuery); + // Act + IQueryable finalQuery = options.ApplyTo(new Customer[0].AsQueryable(), querySettings); - var options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - ODataQuerySettings querySettings = new ODataQuerySettings - { - EnsureStableOrdering = ensureStableOrdering - }; + // Assert + string queryExpression = ExpressionStringBuilder.ToString(finalQuery.Expression); + queryExpression = queryExpression.Substring(queryExpression.IndexOf("]") + 2); - // Act - IQueryable finalQuery = options.ApplyTo(new Customer[0].AsQueryable(), querySettings); + Assert.Equal(expectedExpression, queryExpression); + } - // Assert - string queryExpression = ExpressionStringBuilder.ToString(finalQuery.Expression); - queryExpression = queryExpression.Substring(queryExpression.IndexOf("]") + 2); + [Theory] + [InlineData(null, 1)] + [InlineData(1, null)] + public void ApplyToODataQueryOptions_Builds_Default_OrderBy_With_Paging(int? pageSize, int? modelBoundPageSize) + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/Customers"); + + var options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + ODataQuerySettings querySettings = new ODataQuerySettings + { + PageSize = pageSize, + ModelBoundPageSize = modelBoundPageSize + }; + + Customer[] customers = new[] { + new Customer() { CustomerId = 4 }, + new Customer() { CustomerId = 3 }, + new Customer() { CustomerId = 1 }, + new Customer() { CustomerId = 2 } + }; + + // Act + IQueryable query = options.ApplyTo(customers.AsQueryable(), querySettings); + Customer[] results = (query as IQueryable).ToArray(); + + // Assert + Assert.Equal(querySettings.PageSize ?? querySettings.ModelBoundPageSize, results.Length); + Assert.Equal(customers.OrderBy(c => c.CustomerId).First().CustomerId, results[0].CustomerId); + } - Assert.Equal(expectedExpression, queryExpression); - } + [Fact] + public void Validate_ThrowsValidationErrors_ForOrderBy() + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$orderby=CustomerId,Name"); + + ODataQueryContext queryContext = new ODataQueryContext(model, typeof(Customer)); + queryContext.DefaultQueryConfigurations.EnableOrderBy = true; + var options = new ODataQueryOptions(queryContext, request); + ODataValidationSettings validationSettings = new ODataValidationSettings { MaxOrderByNodeCount = 1 }; + + // Act & Assert + ExceptionAssert.Throws(() => options.Validate(validationSettings), + "The number of clauses in $orderby query option exceeded the maximum number allowed. The maximum number of $orderby clauses allowed is 1."); + } - [Theory] - [InlineData(null, 1)] - [InlineData(1, null)] - public void ApplyToODataQueryOptions_Builds_Default_OrderBy_With_Paging(int? pageSize, int? modelBoundPageSize) - { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/Customers"); + [Theory] + [InlineData("$orderby")] + [InlineData("$filter")] + [InlineData("$top")] + [InlineData("$skip")] + [InlineData("$count")] + [InlineData("$expand")] + [InlineData("$select")] + [InlineData("$format")] + [InlineData("$skiptoken")] + [InlineData("$deltatoken")] + public void IsSystemQueryOption_Returns_True_For_All_Supported_Query_Names(string queryName) + { + // Arrange & Act & Assert + Assert.True(ODataQueryOptions.IsSystemQueryOption(queryName)); - var options = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - ODataQuerySettings querySettings = new ODataQuerySettings - { - PageSize = pageSize, - ModelBoundPageSize = modelBoundPageSize - }; + string newQueryName = queryName.Substring(1); // remove "$" + Assert.False(ODataQueryOptions.IsSystemQueryOption(newQueryName, false)); + Assert.True(ODataQueryOptions.IsSystemQueryOption(newQueryName, true)); + } - Customer[] customers = new[] { - new Customer() { CustomerId = 4 }, - new Customer() { CustomerId = 3 }, - new Customer() { CustomerId = 1 }, - new Customer() { CustomerId = 2 } - }; + [Fact] + public void IsSystemQueryOption_Returns_False_For_Unrecognized_Query_Name() + { + // Arrange & Act & Assert + Assert.False(ODataQueryOptions.IsSystemQueryOption("$invalidqueryname")); + } - // Act - IQueryable query = options.ApplyTo(customers.AsQueryable(), querySettings); - Customer[] results = (query as IQueryable).ToArray(); + [Fact] + public void IsSystemQueryOption_ThrowsArgumentNull_QueryOptionName() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNullOrEmpty(() => ODataQueryOptions.IsSystemQueryOption(null), "queryOptionName"); + ExceptionAssert.ThrowsArgumentNullOrEmpty(() => ODataQueryOptions.IsSystemQueryOption(string.Empty), "queryOptionName"); + } - // Assert - Assert.Equal(querySettings.PageSize ?? querySettings.ModelBoundPageSize, results.Length); - Assert.Equal(customers.OrderBy(c => c.CustomerId).First().CustomerId, results[0].CustomerId); - } + [Fact] + public void GenerateStableOrder_Works_WithGroupbyApplyClause() + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/Customers?$apply=groupby((CustomerId, Name))&$orderby=Name"); - [Fact] - public void Validate_ThrowsValidationErrors_ForOrderBy() - { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/service/Customers?$orderby=CustomerId,Name"); - - ODataQueryContext queryContext = new ODataQueryContext(model, typeof(Customer)); - queryContext.DefaultQueryConfigurations.EnableOrderBy = true; - var options = new ODataQueryOptions(queryContext, request); - ODataValidationSettings validationSettings = new ODataValidationSettings { MaxOrderByNodeCount = 1 }; - - // Act & Assert - ExceptionAssert.Throws(() => options.Validate(validationSettings), - "The number of clauses in $orderby query option exceeded the maximum number allowed. The maximum number of $orderby clauses allowed is 1."); - } + ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); + ODataQueryOptions option = new ODataQueryOptions(context, request); - [Theory] - [InlineData("$orderby")] - [InlineData("$filter")] - [InlineData("$top")] - [InlineData("$skip")] - [InlineData("$count")] - [InlineData("$expand")] - [InlineData("$select")] - [InlineData("$format")] - [InlineData("$skiptoken")] - [InlineData("$deltatoken")] - public void IsSystemQueryOption_Returns_True_For_All_Supported_Query_Names(string queryName) - { - // Arrange & Act & Assert - Assert.True(ODataQueryOptions.IsSystemQueryOption(queryName)); + // Act + OrderByQueryOption orderByQuery = option.GenerateStableOrder(); - string newQueryName = queryName.Substring(1); // remove "$" - Assert.False(ODataQueryOptions.IsSystemQueryOption(newQueryName, false)); - Assert.True(ODataQueryOptions.IsSystemQueryOption(newQueryName, true)); - } + // Assert + Assert.NotNull(orderByQuery); + Assert.Equal(2, orderByQuery.OrderByNodes.Count); + Assert.Collection(orderByQuery.OrderByNodes, + e => + { + OrderByPropertyNode node = Assert.IsType(e); + Assert.Equal("Name", node.Property.Name); + }, + e => + { + OrderByPropertyNode node = Assert.IsType(e); + Assert.Equal("CustomerId", node.Property.Name); + }); + } - [Fact] - public void IsSystemQueryOption_Returns_False_For_Unrecognized_Query_Name() - { - // Arrange & Act & Assert - Assert.False(ODataQueryOptions.IsSystemQueryOption("$invalidqueryname")); - } + [Fact] + public void GenerateStableOrder_Works_WithAggregateApplyClause() + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/Customers?$apply=aggregate(CustomerId with sum as Total)&$orderby=Total"); - [Fact] - public void IsSystemQueryOption_ThrowsArgumentNull_QueryOptionName() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNullOrEmpty(() => ODataQueryOptions.IsSystemQueryOption(null), "queryOptionName"); - ExceptionAssert.ThrowsArgumentNullOrEmpty(() => ODataQueryOptions.IsSystemQueryOption(string.Empty), "queryOptionName"); - } + ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); + ODataQueryOptions option = new ODataQueryOptions(context, request); - [Fact] - public void GenerateStableOrder_Works_WithGroupbyApplyClause() - { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/Customers?$apply=groupby((CustomerId, Name))&$orderby=Name"); + // Act + OrderByQueryOption orderByQuery = option.GenerateStableOrder(); - ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); - ODataQueryOptions option = new ODataQueryOptions(context, request); + // Assert + Assert.NotNull(orderByQuery); + OrderByNode orderbyNode = Assert.Single(orderByQuery.OrderByNodes); + OrderByOpenPropertyNode node = Assert.IsType(orderbyNode); + Assert.Equal("Total", node.PropertyName); + } - // Act - OrderByQueryOption orderByQuery = option.GenerateStableOrder(); + [Theory] + [InlineData(1, true)] + [InlineData(2, true)] + [InlineData(4, false)] + [InlineData(8, false)] + public void LimitResults_LimitsResults(int limit, bool resultsLimitedExpected) + { + // Arrange + IQueryable queryable = new List() { + new Customer() { CustomerId = 0 }, + new Customer() { CustomerId = 1 }, + new Customer() { CustomerId = 2 }, + new Customer() { CustomerId = 3 } + }.AsQueryable(); + IEdmModel model = GetEdmModel(c => c.CustomerId); + ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); + + // Act + bool resultsLimited; + IQueryable result = ODataQueryOptions.LimitResults(queryable, limit, false, out resultsLimited) as IQueryable; + + // Assert + Assert.Equal(Math.Min(limit, 4), result.Count()); + Assert.Equal(resultsLimitedExpected, resultsLimited); + } - // Assert - Assert.NotNull(orderByQuery); - Assert.Equal(2, orderByQuery.OrderByNodes.Count); - Assert.Collection(orderByQuery.OrderByNodes, - e => - { - OrderByPropertyNode node = Assert.IsType(e); - Assert.Equal("Name", node.Property.Name); - }, - e => - { - OrderByPropertyNode node = Assert.IsType(e); - Assert.Equal("CustomerId", node.Property.Name); - }); - } + [Fact] + public void CanTurnOffAllValidation() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/?$filter=Name eq 'abc'"); - [Fact] - public void GenerateStableOrder_Works_WithAggregateApplyClause() + IEdmModel model = GetEdmModel(c => c.CustomerId); + ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); + ODataQueryOptions option = new ODataQueryOptions(context, request); + ODataValidationSettings settings = new ODataValidationSettings() { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/Customers?$apply=aggregate(CustomerId with sum as Total)&$orderby=Total"); + AllowedQueryOptions = AllowedQueryOptions.OrderBy + }; - ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); - ODataQueryOptions option = new ODataQueryOptions(context, request); - - // Act - OrderByQueryOption orderByQuery = option.GenerateStableOrder(); - - // Assert - Assert.NotNull(orderByQuery); - OrderByNode orderbyNode = Assert.Single(orderByQuery.OrderByNodes); - OrderByOpenPropertyNode node = Assert.IsType(orderbyNode); - Assert.Equal("Total", node.PropertyName); - } + // Act & Assert + ExceptionAssert.Throws(() => option.Validate(settings), + "Query option 'Filter' is not allowed. To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings."); - [Theory] - [InlineData(1, true)] - [InlineData(2, true)] - [InlineData(4, false)] - [InlineData(8, false)] - public void LimitResults_LimitsResults(int limit, bool resultsLimitedExpected) - { - // Arrange - IQueryable queryable = new List() { - new Customer() { CustomerId = 0 }, - new Customer() { CustomerId = 1 }, - new Customer() { CustomerId = 2 }, - new Customer() { CustomerId = 3 } - }.AsQueryable(); - IEdmModel model = GetEdmModel(c => c.CustomerId); - ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); - - // Act - bool resultsLimited; - IQueryable result = ODataQueryOptions.LimitResults(queryable, limit, false, out resultsLimited) as IQueryable; - - // Assert - Assert.Equal(Math.Min(limit, 4), result.Count()); - Assert.Equal(resultsLimitedExpected, resultsLimited); - } + option.Validator = null; + ExceptionAssert.DoesNotThrow(() => option.Validate(settings)); + } - [Fact] - public void CanTurnOffAllValidation() + public static TheoryDataSet Querying_Primitive_Collections_Data + { + get { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/?$filter=Name eq 'abc'"); - - IEdmModel model = GetEdmModel(c => c.CustomerId); - ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); - ODataQueryOptions option = new ODataQueryOptions(context, request); - ODataValidationSettings settings = new ODataValidationSettings() + IQueryable e = Enumerable.Range(1, 9).AsQueryable(); + return new TheoryDataSet { - AllowedQueryOptions = AllowedQueryOptions.OrderBy + { e.Select(i => (short)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (short)6 }, + { e.Select(i => i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", 6 }, + { e.Select(i => (long)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (long)6 }, + { e.Select(i => (ushort)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (ushort)6 }, + { e.Select(i => (uint)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (uint)6 }, + { e.Select(i => (ulong)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (ulong)6 }, + { e.Select(i => (float)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (float)6 }, + { e.Select(i => (double)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (double)6 }, + { e.Select(i => (decimal)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (decimal)6 }, + { e.Select(i => new DateTimeOffset(new DateTime(i, 1, 1), TimeSpan.Zero)), "$filter=year($it) ge 5&$orderby=$it desc&$skip=3&$top=1", new DateTimeOffset(new DateTime(year: 6, month: 1, day: 1), TimeSpan.Zero) }, + { e.Select(i => i.ToString()), "$filter=$it ge '5'&$orderby=$it desc&$skip=3&$top=1", "6" }, + + { e.Select(i => (i % 2 != 0 ? null : (short?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (short?)6 }, + { e.Select(i => (i % 2 != 0 ? null : (int?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (int?)6 }, + { e.Select(i => (i % 2 != 0 ? null : (long?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (long?)6 }, + { e.Select(i => (i % 2 != 0 ? null : (ushort?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (ushort?)6 }, + { e.Select(i => (i % 2 != 0 ? null : (uint?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (uint?)6 }, + { e.Select(i => (i % 2 != 0 ? null : (ulong?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (ulong?)6 }, + { e.Select(i => (i % 2 != 0 ? null : (float?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (float?)6 }, + { e.Select(i => (i % 2 != 0 ? null : (double?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (double?)6 }, + { e.Select(i => (i % 2 != 0 ? null : (decimal?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (decimal?)6 }, }; - - // Act & Assert - ExceptionAssert.Throws(() => option.Validate(settings), - "Query option 'Filter' is not allowed. To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings."); - - option.Validator = null; - ExceptionAssert.DoesNotThrow(() => option.Validate(settings)); - } - - public static TheoryDataSet Querying_Primitive_Collections_Data - { - get - { - IQueryable e = Enumerable.Range(1, 9).AsQueryable(); - return new TheoryDataSet - { - { e.Select(i => (short)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (short)6 }, - { e.Select(i => i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", 6 }, - { e.Select(i => (long)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (long)6 }, - { e.Select(i => (ushort)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (ushort)6 }, - { e.Select(i => (uint)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (uint)6 }, - { e.Select(i => (ulong)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (ulong)6 }, - { e.Select(i => (float)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (float)6 }, - { e.Select(i => (double)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (double)6 }, - { e.Select(i => (decimal)i), "$filter=$it ge 5&$orderby=$it desc&$skip=3&$top=1", (decimal)6 }, - { e.Select(i => new DateTimeOffset(new DateTime(i, 1, 1), TimeSpan.Zero)), "$filter=year($it) ge 5&$orderby=$it desc&$skip=3&$top=1", new DateTimeOffset(new DateTime(year: 6, month: 1, day: 1), TimeSpan.Zero) }, - { e.Select(i => i.ToString()), "$filter=$it ge '5'&$orderby=$it desc&$skip=3&$top=1", "6" }, - - { e.Select(i => (i % 2 != 0 ? null : (short?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (short?)6 }, - { e.Select(i => (i % 2 != 0 ? null : (int?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (int?)6 }, - { e.Select(i => (i % 2 != 0 ? null : (long?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (long?)6 }, - { e.Select(i => (i % 2 != 0 ? null : (ushort?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (ushort?)6 }, - { e.Select(i => (i % 2 != 0 ? null : (uint?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (uint?)6 }, - { e.Select(i => (i % 2 != 0 ? null : (ulong?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (ulong?)6 }, - { e.Select(i => (i % 2 != 0 ? null : (float?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (float?)6 }, - { e.Select(i => (i % 2 != 0 ? null : (double?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (double?)6 }, - { e.Select(i => (i % 2 != 0 ? null : (decimal?)i)), "$filter=$it ge 5&$orderby=$it desc&$skip=1&$top=1", (decimal?)6 }, - }; - } } + } - [Theory] - [MemberData(nameof(Querying_Primitive_Collections_Data))] - public void Querying_Primitive_Collections(IQueryable queryable, string query, object result) - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/?" + query); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, queryable.ElementType); - ODataQueryOptions options = new ODataQueryOptions(context, request); - - // Act - queryable = options.ApplyTo(queryable); - - // Assert - IEnumerator enumerator = queryable.GetEnumerator(); - Assert.True(enumerator.MoveNext()); - Assert.Equal(result, enumerator.Current); - } + [Theory] + [MemberData(nameof(Querying_Primitive_Collections_Data))] + public void Querying_Primitive_Collections(IQueryable queryable, string query, object result) + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/?" + query); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, queryable.ElementType); + ODataQueryOptions options = new ODataQueryOptions(context, request); + + // Act + queryable = options.ApplyTo(queryable); + + // Assert + IEnumerator enumerator = queryable.GetEnumerator(); + Assert.True(enumerator.MoveNext()); + Assert.Equal(result, enumerator.Current); + } - private enum SimpleEnum - { - First, - Second, - Third, - Fourth - } + private enum SimpleEnum + { + First, + Second, + Third, + Fourth + } - public static TheoryDataSet Querying_Enum_Collections_Data + public static TheoryDataSet Querying_Enum_Collections_Data + { + get { - get + IQueryable e = Enumerable.Range(1, 9).AsQueryable(); + return new TheoryDataSet { - IQueryable e = Enumerable.Range(1, 9).AsQueryable(); - return new TheoryDataSet - { - { e.Select(i => (SimpleEnum)(i%3)), "$filter=$it eq Microsoft.AspNetCore.OData.Tests.Query.SimpleEnum'First'&$orderby=$it desc&$skip=1&$top=1", SimpleEnum.First }, - { e.Select(i => (SimpleEnum?)null), "$filter=$it eq null&$orderby=$it desc&$skip=1&$top=1", null }, - }; - } + { e.Select(i => (SimpleEnum)(i%3)), "$filter=$it eq Microsoft.AspNetCore.OData.Tests.Query.SimpleEnum'First'&$orderby=$it desc&$skip=1&$top=1", SimpleEnum.First }, + { e.Select(i => (SimpleEnum?)null), "$filter=$it eq null&$orderby=$it desc&$skip=1&$top=1", null }, + }; } + } - [Theory] - [MemberData(nameof(Querying_Enum_Collections_Data))] - public void Querying_Enum_Collections(IQueryable queryable, string query, object result) - { - // Arrange - ODataModelBuilder builder = new ODataModelBuilder(); - EnumTypeConfiguration simpleEnum = builder.EnumType(); - simpleEnum.Member(SimpleEnum.First); - simpleEnum.Member(SimpleEnum.Second); - simpleEnum.Member(SimpleEnum.Third); - IEdmModel model = builder.GetEdmModel(); - - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/?" + query); - ODataQueryContext context = new ODataQueryContext(model, queryable.ElementType); - ODataQueryOptions options = new ODataQueryOptions(context, request); - - // Act - queryable = options.ApplyTo(queryable); - - // Assert - IEnumerator enumerator = queryable.GetEnumerator(); - Assert.True(enumerator.MoveNext()); - Assert.Equal(result, enumerator.Current); - } + [Theory] + [MemberData(nameof(Querying_Enum_Collections_Data))] + public void Querying_Enum_Collections(IQueryable queryable, string query, object result) + { + // Arrange + ODataModelBuilder builder = new ODataModelBuilder(); + EnumTypeConfiguration simpleEnum = builder.EnumType(); + simpleEnum.Member(SimpleEnum.First); + simpleEnum.Member(SimpleEnum.Second); + simpleEnum.Member(SimpleEnum.Third); + IEdmModel model = builder.GetEdmModel(); + + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/?" + query); + ODataQueryContext context = new ODataQueryContext(model, queryable.ElementType); + ODataQueryOptions options = new ODataQueryOptions(context, request); + + // Act + queryable = options.ApplyTo(queryable); + + // Assert + IEnumerator enumerator = queryable.GetEnumerator(); + Assert.True(enumerator.MoveNext()); + Assert.Equal(result, enumerator.Current); + } - [Fact] - public void ODataQueryOptions_IgnoresUnknownOperatorStartingWithDollar() - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/?$filter=$it eq 6&$unknown=value"); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - ODataQueryOptions options = new ODataQueryOptions(context, request); - - // Act - var queryable = options.ApplyTo(Enumerable.Range(0, 10).AsQueryable()); - - // Assert - IEnumerator enumerator = queryable.GetEnumerator(); - Assert.True(enumerator.MoveNext()); - Assert.Equal(6, enumerator.Current); - } + [Fact] + public void ODataQueryOptions_IgnoresUnknownOperatorStartingWithDollar() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/?$filter=$it eq 6&$unknown=value"); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + ODataQueryOptions options = new ODataQueryOptions(context, request); + + // Act + var queryable = options.ApplyTo(Enumerable.Range(0, 10).AsQueryable()); + + // Assert + IEnumerator enumerator = queryable.GetEnumerator(); + Assert.True(enumerator.MoveNext()); + Assert.Equal(6, enumerator.Current); + } - [Fact] - public void ApplyTo_Entity_ThrowsArgumentNull_Entity() - { - // Arrange & Act - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://any"); + [Fact] + public void ApplyTo_Entity_ThrowsArgumentNull_Entity() + { + // Arrange & Act + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://any"); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - // Assert - ExceptionAssert.ThrowsArgumentNull( - () => queryOptions.ApplyTo(entity: null, querySettings: new ODataQuerySettings()), - "entity"); - } + // Assert + ExceptionAssert.ThrowsArgumentNull( + () => queryOptions.ApplyTo(entity: null, querySettings: new ODataQuerySettings()), + "entity"); + } - [Fact] - public void ApplyTo_IgnoresCount_IfRequestAlreadyHasCount() - { - // Arrange - long count = 42; - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/?$count=true"); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - ODataQueryOptions options = new ODataQueryOptions(context, request); - request.ODataFeature().TotalCount = count; - - // Act - options.ApplyTo(Enumerable.Empty().AsQueryable()); - - // Assert - Assert.Equal(count, request.ODataFeature().TotalCount); - } + [Fact] + public void ApplyTo_IgnoresCount_IfRequestAlreadyHasCount() + { + // Arrange + long count = 42; + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/?$count=true"); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + ODataQueryOptions options = new ODataQueryOptions(context, request); + request.ODataFeature().TotalCount = count; + + // Act + options.ApplyTo(Enumerable.Empty().AsQueryable()); + + // Assert + Assert.Equal(count, request.ODataFeature().TotalCount); + } - [Fact] - public void ApplyTo_Entity_ThrowsArgumentNull_QuerySettings() - { - // Arrange & Act - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://any"); + [Fact] + public void ApplyTo_Entity_ThrowsArgumentNull_QuerySettings() + { + // Arrange & Act + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://any"); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - // Arrange - ExceptionAssert.ThrowsArgumentNull( - () => queryOptions.ApplyTo(entity: 42, querySettings: null), - "querySettings"); - } + // Arrange + ExceptionAssert.ThrowsArgumentNull( + () => queryOptions.ApplyTo(entity: 42, querySettings: null), + "querySettings"); + } - [Theory] - [InlineData("$filter=CustomerId eq 1")] - [InlineData("$orderby=CustomerId")] - [InlineData("$count=true")] - [InlineData("$skip=1")] - [InlineData("$top=0")] - public void ApplyTo_Entity_ThrowsInvalidOperation_IfNonSelectExpand(string parameter) - { - // Arrange & Act - IEdmModel model = GetEdmModel(c => c.CustomerId); + [Theory] + [InlineData("$filter=CustomerId eq 1")] + [InlineData("$orderby=CustomerId")] + [InlineData("$count=true")] + [InlineData("$skip=1")] + [InlineData("$top=0")] + public void ApplyTo_Entity_ThrowsInvalidOperation_IfNonSelectExpand(string parameter) + { + // Arrange & Act + IEdmModel model = GetEdmModel(c => c.CustomerId); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost?" + parameter); - ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost?" + parameter); + ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - // Assert - ExceptionAssert.Throws( - () => queryOptions.ApplyTo(42, new ODataQuerySettings()), - "The requested resource is not a collection. Query options $filter, $orderby, $count, $skip, and $top can be applied only on collections."); - } + // Assert + ExceptionAssert.Throws( + () => queryOptions.ApplyTo(42, new ODataQuerySettings()), + "The requested resource is not a collection. Query options $filter, $orderby, $count, $skip, and $top can be applied only on collections."); + } - [Theory] - [InlineData("?$select=Orders/OrderId", AllowedQueryOptions.Select)] - [InlineData("?$expand=Orders", AllowedQueryOptions.Expand)] - public void ApplyTo_Entity_DoesnotApply_IfSetApplied(string queryOption, AllowedQueryOptions appliedQueryOptions) - { - // Arrange - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - ODataQueryContext context = new ODataQueryContext(builder.GetEdmModel(), typeof(Customer)); - Customer customer = new Customer + [Theory] + [InlineData("?$select=Orders/OrderId", AllowedQueryOptions.Select)] + [InlineData("?$expand=Orders", AllowedQueryOptions.Expand)] + public void ApplyTo_Entity_DoesnotApply_IfSetApplied(string queryOption, AllowedQueryOptions appliedQueryOptions) + { + // Arrange + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + ODataQueryContext context = new ODataQueryContext(builder.GetEdmModel(), typeof(Customer)); + Customer customer = new Customer + { + CustomerId = 1, + Orders = new List { - CustomerId = 1, - Orders = new List - { - new Order {OrderId = 1} - } - }; + new Order {OrderId = 1} + } + }; - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost" + queryOption); - ODataQueryOptions options = new ODataQueryOptions(context, request); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost" + queryOption); + ODataQueryOptions options = new ODataQueryOptions(context, request); - // Act - object result = options.ApplyTo(customer, new ODataQuerySettings(), appliedQueryOptions); + // Act + object result = options.ApplyTo(customer, new ODataQuerySettings(), appliedQueryOptions); - // Assert - Assert.Equal(customer, (result as Customer)); - } + // Assert + Assert.Equal(customer, (result as Customer)); + } - [Theory] - [InlineData("?$filter=CustomerId eq 1", AllowedQueryOptions.Filter)] - [InlineData("?$orderby=CustomerId", AllowedQueryOptions.OrderBy)] - [InlineData("?$count=true", AllowedQueryOptions.Count)] - [InlineData("?$skip=1", AllowedQueryOptions.Skip)] - [InlineData("?$top=1", AllowedQueryOptions.Top)] - [InlineData("?$select=CustomerId", AllowedQueryOptions.Select)] - [InlineData("?$expand=Orders", AllowedQueryOptions.Expand)] - public void ApplyTo_DoesnotApply_IfSetApplied(string queryOption, AllowedQueryOptions appliedQueryOptions) - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost" + queryOption); - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - ODataQueryContext context = new ODataQueryContext(builder.GetEdmModel(), typeof(Customer)); - ODataQueryOptions options = new ODataQueryOptions(context, request); - IQueryable customers = - Enumerable.Range(1, 10).Select( - i => new Customer + [Theory] + [InlineData("?$filter=CustomerId eq 1", AllowedQueryOptions.Filter)] + [InlineData("?$orderby=CustomerId", AllowedQueryOptions.OrderBy)] + [InlineData("?$count=true", AllowedQueryOptions.Count)] + [InlineData("?$skip=1", AllowedQueryOptions.Skip)] + [InlineData("?$top=1", AllowedQueryOptions.Top)] + [InlineData("?$select=CustomerId", AllowedQueryOptions.Select)] + [InlineData("?$expand=Orders", AllowedQueryOptions.Expand)] + public void ApplyTo_DoesnotApply_IfSetApplied(string queryOption, AllowedQueryOptions appliedQueryOptions) + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost" + queryOption); + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + ODataQueryContext context = new ODataQueryContext(builder.GetEdmModel(), typeof(Customer)); + ODataQueryOptions options = new ODataQueryOptions(context, request); + IQueryable customers = + Enumerable.Range(1, 10).Select( + i => new Customer + { + CustomerId = i, + Orders = new List { - CustomerId = i, - Orders = new List - { - new Order {OrderId = i} - } - }) - .AsQueryable(); - - // Act - IQueryable result = options.ApplyTo(customers, new ODataQuerySettings(), appliedQueryOptions); - - // Assert - Assert.Equal(10, (result as IQueryable).Count()); - } + new Order {OrderId = i} + } + }) + .AsQueryable(); - [Fact] - public void ApplyTo_DoesnotCalculateNextPageLink_IfRequestAlreadyHasNextPageLink() - { - // Arrange - Uri nextPageLink = new Uri("http://localhost/nextpagelink"); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/"); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - ODataQueryOptions options = new ODataQueryOptions(context, request); - request.ODataFeature().NextLink = nextPageLink; - - // Act - IQueryable result = options.ApplyTo(Enumerable.Range(0, 100).AsQueryable(), new ODataQuerySettings { PageSize = 1 }); - - // Assert - Assert.Equal(nextPageLink, request.ODataFeature().NextLink); - Assert.Single((result as IQueryable)); - } - - [Fact] - public void ODataQueryOptions_WithUnTypedContext_CanBeBuilt() - { - // Arrange - EdmModel model = new EdmModel(); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - model.AddElement(customer); - - ODataQueryContext context = new ODataQueryContext(model, customer); - HttpRequest request = RequestFactory.Create(HttpMethods.Get, - "http://localhost/?$filter=Id eq 42&$orderby=Id&$skip=42&$top=42&$count=true&$select=Id&$expand=Orders"); - - // Act - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - - // Assert - Assert.NotNull(queryOptions.Filter); - Assert.NotNull(queryOptions.OrderBy); - Assert.NotNull(queryOptions.Skip); - Assert.NotNull(queryOptions.Top); - Assert.NotNull(queryOptions.SelectExpand); - Assert.NotNull(queryOptions.Count); - } + // Act + IQueryable result = options.ApplyTo(customers, new ODataQuerySettings(), appliedQueryOptions); - [Fact] - public async Task ODataQueryOptions_SetToApplied() - { - // Arrange - string url = "http://localhost/odata/EntityModels?$filter=ID eq 1&$skip=1&$select=A&$expand=ExpandProp"; - HttpClient client = CreateClient(); - - // Act - HttpResponseMessage response = await client.GetAsync(url); - var responseString = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Contains("Property", responseString); - Assert.DoesNotContain("1", responseString); - Assert.DoesNotContain("ExpandProperty", responseString); - } + // Assert + Assert.Equal(10, (result as IQueryable).Count()); + } - [Theory] - [InlineData("ExpandProp1")] - [InlineData("ExpandProp2")] - public async Task ODataQueryOptions_ApplyOrderByInExpandResult_WhenSetPageSize(string propName) - { - // Arrange - string url = "http://localhost/odata/Products?$expand=" + propName; - HttpClient client = CreateClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); - - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - var responseObject = JObject.Parse(await response.Content.ReadAsStringAsync()); - var result = responseObject["value"] as JArray; - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(result); - var expandProp = result[0][propName] as JArray; - Assert.Equal(2, expandProp.Count); - Assert.Equal(1, expandProp[0]["ID"]); - Assert.Equal(2, expandProp[1]["ID"]); - } + [Fact] + public void ApplyTo_DoesnotCalculateNextPageLink_IfRequestAlreadyHasNextPageLink() + { + // Arrange + Uri nextPageLink = new Uri("http://localhost/nextpagelink"); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost/"); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + ODataQueryOptions options = new ODataQueryOptions(context, request); + request.ODataFeature().NextLink = nextPageLink; + + // Act + IQueryable result = options.ApplyTo(Enumerable.Range(0, 100).AsQueryable(), new ODataQuerySettings { PageSize = 1 }); + + // Assert + Assert.Equal(nextPageLink, request.ODataFeature().NextLink); + Assert.Single((result as IQueryable)); + } - [Theory] - [InlineData("ExpandProp3")] - [InlineData("ExpandProp4")] - public async Task ODataQueryOptions_ApplyOrderByInExpandResult_WhenSetPageSize_MultiplyKeys(string propName) - { - // Arrange - string url = "http://localhost/odata/Products?$expand=" + propName; - HttpClient client = CreateClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - var responseObject = JObject.Parse(await response.Content.ReadAsStringAsync()); - var result = responseObject["value"] as JArray; - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(result); - var expandProp = result[0][propName] as JArray; - Assert.Equal(2, expandProp.Count); - Assert.Equal(1, expandProp[0]["ID1"]); - Assert.Equal(1, expandProp[0]["ID2"]); - Assert.Equal(2, expandProp[1]["ID1"]); - Assert.Equal(1, expandProp[1]["ID2"]); - } + [Fact] + public void ODataQueryOptions_WithUnTypedContext_CanBeBuilt() + { + // Arrange + EdmModel model = new EdmModel(); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + model.AddElement(customer); + + ODataQueryContext context = new ODataQueryContext(model, customer); + HttpRequest request = RequestFactory.Create(HttpMethods.Get, + "http://localhost/?$filter=Id eq 42&$orderby=Id&$skip=42&$top=42&$count=true&$select=Id&$expand=Orders"); + + // Act + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + + // Assert + Assert.NotNull(queryOptions.Filter); + Assert.NotNull(queryOptions.OrderBy); + Assert.NotNull(queryOptions.Skip); + Assert.NotNull(queryOptions.Top); + Assert.NotNull(queryOptions.SelectExpand); + Assert.NotNull(queryOptions.Count); + } - [Fact] - public void DuplicateUnsupportedQueryParametersIgnored() - { - // Arrange - IEdmModel model = GetEdmModel(c => c.CustomerId); + [Fact] + public async Task ODataQueryOptions_SetToApplied() + { + // Arrange + string url = "http://localhost/odata/EntityModels?$filter=ID eq 1&$skip=1&$select=A&$expand=ExpandProp"; + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.GetAsync(url); + var responseString = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("Property", responseString); + Assert.DoesNotContain("1", responseString); + Assert.DoesNotContain("ExpandProperty", responseString); + } - // a simple query with duplicate ignored parameters (key=test) - string uri = "http://server/service/Customers?$top=10&test=1&test=2"; - var request = RequestFactory.Create(HttpMethods.Get, uri); + [Theory] + [InlineData("ExpandProp1")] + [InlineData("ExpandProp2")] + public async Task ODataQueryOptions_ApplyOrderByInExpandResult_WhenSetPageSize(string propName) + { + // Arrange + string url = "http://localhost/odata/Products?$expand=" + propName; + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); + + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + var responseObject = JObject.Parse(await response.Content.ReadAsStringAsync()); + var result = responseObject["value"] as JArray; + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(result); + var expandProp = result[0][propName] as JArray; + Assert.Equal(2, expandProp.Count); + Assert.Equal(1, expandProp[0]["ID"]); + Assert.Equal(2, expandProp[1]["ID"]); + } - // Act - var queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); + [Theory] + [InlineData("ExpandProp3")] + [InlineData("ExpandProp4")] + public async Task ODataQueryOptions_ApplyOrderByInExpandResult_WhenSetPageSize_MultiplyKeys(string propName) + { + // Arrange + string url = "http://localhost/odata/Products?$expand=" + propName; + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + var responseObject = JObject.Parse(await response.Content.ReadAsStringAsync()); + var result = responseObject["value"] as JArray; + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(result); + var expandProp = result[0][propName] as JArray; + Assert.Equal(2, expandProp.Count); + Assert.Equal(1, expandProp[0]["ID1"]); + Assert.Equal(1, expandProp[0]["ID2"]); + Assert.Equal(2, expandProp[1]["ID1"]); + Assert.Equal(1, expandProp[1]["ID2"]); + } - // Assert - Assert.Equal("10", queryOptions.RawValues.Top); - } + [Fact] + public void DuplicateUnsupportedQueryParametersIgnored() + { + // Arrange + IEdmModel model = GetEdmModel(c => c.CustomerId); - [Fact] - public async Task DuplicateUnsupportedQueryParametersIgnoredWithNoException() - { - // Arrange - string url = "http://localhost/odata/Products?$top=1&test=1&test=2"; - HttpClient client = CreateClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + // a simple query with duplicate ignored parameters (key=test) + string uri = "http://server/service/Customers?$top=10&test=1&test=2"; + var request = RequestFactory.Create(HttpMethods.Get, uri); - // Act - HttpResponseMessage response = await client.SendAsync(request); + // Act + var queryOptions = new ODataQueryOptions(new ODataQueryContext(model, typeof(Customer)), request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } + // Assert + Assert.Equal("10", queryOptions.RawValues.Top); + } - private static HttpClient CreateClient() - { - var controllers = new[] { typeof(EntityModelsController), typeof(ProductsController) }; + [Fact] + public async Task DuplicateUnsupportedQueryParametersIgnoredWithNoException() + { + // Arrange + string url = "http://localhost/odata/Products?$top=1&test=1&test=2"; + HttpClient client = CreateClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); - ODataModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("EntityModels"); - builder.EntitySet("Products"); - builder.EntitySet("ODataQueryOptionTest_EntityModelMultipleKeys"); - IEdmModel model = builder.GetEdmModel(); + // Act + HttpResponseMessage response = await client.SendAsync(request); - var server = TestServerUtils.Create( - opt => - { - opt.Count().OrderBy().Filter().Expand().SetMaxTop(null); - opt.AddRouteComponents("odata", model); - }, - controllers); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } - return server.CreateClient(); - } + private static HttpClient CreateClient() + { + var controllers = new[] { typeof(EntityModelsController), typeof(ProductsController) }; - private static IEdmModel GetEdmModelWithoutKey() - { - return GetEdmModel(null); - } + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("EntityModels"); + builder.EntitySet("Products"); + builder.EntitySet("ODataQueryOptionTest_EntityModelMultipleKeys"); + IEdmModel model = builder.GetEdmModel(); - private static IEdmModel GetEdmModel(Expression> keyDefinitionExpression) - { - Mock mock = new Mock(); - mock.Setup(b => b.ValidateModel(It.IsAny())).Callback(() => { }); - mock.CallBase = true; - ODataModelBuilder builder = mock.Object; - EntityTypeConfiguration customer = builder.EntityType(); - if (keyDefinitionExpression != null) + var server = TestServerUtils.Create( + opt => { - customer.HasKey(keyDefinitionExpression); - } + opt.Count().OrderBy().Filter().Expand().SetMaxTop(null); + opt.AddRouteComponents("odata", model); + }, + controllers); - customer.Property(c => c.CustomerId); - customer.Property(c => c.Name); - customer.Property(c => c.Website); - customer.Property(c => c.SharePrice); - customer.Property(c => c.ShareSymbol); - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } + return server.CreateClient(); + } - private class Customer - { - public int CustomerId { get; set; } - public string Name { get; set; } - public string Website { get; set; } - public string ShareSymbol { get; set; } - public decimal? SharePrice { get; set; } - public List Orders { get; set; } - } + private static IEdmModel GetEdmModelWithoutKey() + { + return GetEdmModel(null); + } - private class Order - { - public int OrderId { get; set; } - } + private static IEdmModel GetEdmModel(Expression> keyDefinitionExpression) + { + Mock mock = new Mock(); + mock.Setup(b => b.ValidateModel(It.IsAny())).Callback(() => { }); + mock.CallBase = true; + ODataModelBuilder builder = mock.Object; + EntityTypeConfiguration customer = builder.EntityType(); + if (keyDefinitionExpression != null) + { + customer.HasKey(keyDefinitionExpression); + } + + customer.Property(c => c.CustomerId); + customer.Property(c => c.Name); + customer.Property(c => c.Website); + customer.Property(c => c.SharePrice); + customer.Property(c => c.ShareSymbol); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); } - public class EntityModelsController : ODataController + private class Customer { - private static readonly IQueryable _entityModels; + public int CustomerId { get; set; } + public string Name { get; set; } + public string Website { get; set; } + public string ShareSymbol { get; set; } + public decimal? SharePrice { get; set; } + public List Orders { get; set; } + } - public IActionResult Get(ODataQueryOptions queryOptions) - { - // Don't apply Filter and Expand, but apply Select. - var appliedQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Filter | AllowedQueryOptions.Expand; - var res = queryOptions.ApplyTo(_entityModels, appliedQueryOptions); - return Ok(res.AsQueryable()); - } + private class Order + { + public int OrderId { get; set; } + } +} - private static IEnumerable CreateODataQueryOptionTest_EntityModel() - { - var entityModel = new ODataQueryOptionTest_EntityModel - { - ID = 1, - A = "Property", - ExpandProp = new ODataQueryOptionTest_EntityModelMultipleKeys - { - ID1 = 2, - ID2 = 3, - A = "ExpandProperty" - } - }; - yield return entityModel; - } +public class EntityModelsController : ODataController +{ + private static readonly IQueryable _entityModels; - static EntityModelsController() + public IActionResult Get(ODataQueryOptions queryOptions) + { + // Don't apply Filter and Expand, but apply Select. + var appliedQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Filter | AllowedQueryOptions.Expand; + var res = queryOptions.ApplyTo(_entityModels, appliedQueryOptions); + return Ok(res.AsQueryable()); + } + + private static IEnumerable CreateODataQueryOptionTest_EntityModel() + { + var entityModel = new ODataQueryOptionTest_EntityModel { - _entityModels = CreateODataQueryOptionTest_EntityModel().AsQueryable(); - } + ID = 1, + A = "Property", + ExpandProp = new ODataQueryOptionTest_EntityModelMultipleKeys + { + ID1 = 2, + ID2 = 3, + A = "ExpandProperty" + } + }; + yield return entityModel; } - public class ProductsController : ODataController + static EntityModelsController() { - private static readonly IQueryable _products; + _entityModels = CreateODataQueryOptionTest_EntityModel().AsQueryable(); + } +} - [EnableQuery(PageSize = 2)] - public IActionResult Get() - { - return Ok(_products); - } +public class ProductsController : ODataController +{ + private static readonly IQueryable _products; - private static IEnumerable CreateProducts() + [EnableQuery(PageSize = 2)] + public IActionResult Get() + { + return Ok(_products); + } + + private static IEnumerable CreateProducts() + { + var prop1 = new List { - var prop1 = new List + new ODataQueryOptionTest_EntityModel { - new ODataQueryOptionTest_EntityModel - { - ID = 2, - A = "", - ExpandProp = null - }, - new ODataQueryOptionTest_EntityModel - { - ID = 1, - A = "", - ExpandProp = null - }, - new ODataQueryOptionTest_EntityModel - { - ID = 3, - A = "", - ExpandProp = null - } - }; - var prop2 = new List + ID = 2, + A = "", + ExpandProp = null + }, + new ODataQueryOptionTest_EntityModel { - new ODataQueryOptionTest_EntityModelMultipleKeys - { - ID1 = 2, - ID2 = 3, - A = "" - }, - new ODataQueryOptionTest_EntityModelMultipleKeys - { - ID1 = 1, - ID2 = 1, - A = "" - }, - new ODataQueryOptionTest_EntityModelMultipleKeys - { - ID1 = 2, - ID2 = 1, - A = "" - } - }; - var product = new MyProduct + ID = 1, + A = "", + ExpandProp = null + }, + new ODataQueryOptionTest_EntityModel { - Id = 1, - ExpandProp1 = prop1, - ExpandProp2 = prop1.AsQueryable(), - ExpandProp3 = prop2, - ExpandProp4 = prop2.AsQueryable() - }; - yield return product; - } - - static ProductsController() + ID = 3, + A = "", + ExpandProp = null + } + }; + var prop2 = new List { - _products = CreateProducts().AsQueryable(); - } + new ODataQueryOptionTest_EntityModelMultipleKeys + { + ID1 = 2, + ID2 = 3, + A = "" + }, + new ODataQueryOptionTest_EntityModelMultipleKeys + { + ID1 = 1, + ID2 = 1, + A = "" + }, + new ODataQueryOptionTest_EntityModelMultipleKeys + { + ID1 = 2, + ID2 = 1, + A = "" + } + }; + var product = new MyProduct + { + Id = 1, + ExpandProp1 = prop1, + ExpandProp2 = prop1.AsQueryable(), + ExpandProp3 = prop2, + ExpandProp4 = prop2.AsQueryable() + }; + yield return product; } - public class MyProduct + static ProductsController() { - public int Id { get; set; } + _products = CreateProducts().AsQueryable(); + } +} - public List ExpandProp1 { get; set; } +public class MyProduct +{ + public int Id { get; set; } - public IQueryable ExpandProp2 { get; set; } + public List ExpandProp1 { get; set; } - public List ExpandProp3 { get; set; } + public IQueryable ExpandProp2 { get; set; } - public IQueryable ExpandProp4 { get; set; } - } + public List ExpandProp3 { get; set; } - public class ODataQueryOptionTest_ComplexModel - { - public int A { get; set; } + public IQueryable ExpandProp4 { get; set; } +} - public string B { get; set; } - } +public class ODataQueryOptionTest_ComplexModel +{ + public int A { get; set; } - public class ODataQueryOptionTest_EntityModel - { - public int ID { get; set; } + public string B { get; set; } +} - public string A { get; set; } +public class ODataQueryOptionTest_EntityModel +{ + public int ID { get; set; } - public ODataQueryOptionTest_EntityModelMultipleKeys ExpandProp { get; set; } - } + public string A { get; set; } - public class ODataQueryOptionTest_EntityModelMultipleKeys - { - [Key] - public int ID1 { get; set; } + public ODataQueryOptionTest_EntityModelMultipleKeys ExpandProp { get; set; } +} - [Key] - public int ID2 { get; set; } +public class ODataQueryOptionTest_EntityModelMultipleKeys +{ + [Key] + public int ID1 { get; set; } - public string A { get; set; } - } + [Key] + public int ID2 { get; set; } + + public string A { get; set; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryOptionsOfTEntityTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryOptionsOfTEntityTests.cs index 7bca69e59..f8bd6c798 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryOptionsOfTEntityTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryOptionsOfTEntityTests.cs @@ -19,233 +19,232 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class ODataQueryOptionsOfTEntityTests { - public class ODataQueryOptionsOfTEntityTests - { - private static IEdmModel _model = GetEdmModel(); + private static IEdmModel _model = GetEdmModel(); - [Fact] - public void Ctor_Throws_Argument_IfContextIsofDifferentEntityType() - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); + [Fact] + public void Ctor_Throws_Argument_IfContextIsofDifferentEntityType() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); - ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); + ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => new ODataQueryOptions(context, request), - "context", "The entity type 'Microsoft.AspNetCore.OData.Tests.Query.ODataQueryOptionsOfTEntityTests+QCustomer' does not match the expected entity type 'System.Int32' as set on the query context."); - } + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new ODataQueryOptions(context, request), + "context", "The entity type 'Microsoft.AspNetCore.OData.Tests.Query.ODataQueryOptionsOfTEntityTests+QCustomer' does not match the expected entity type 'System.Int32' as set on the query context."); + } - [Fact] - public void Ctor_Throws_Argument_IfContextIsUnTyped() - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); + [Fact] + public void Ctor_Throws_Argument_IfContextIsUnTyped() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://localhost"); - IEdmModel model = EdmCoreModel.Instance; - IEdmType elementType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); - ODataQueryContext context = new ODataQueryContext(model, elementType); + IEdmModel model = EdmCoreModel.Instance; + IEdmType elementType = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); + ODataQueryContext context = new ODataQueryContext(model, elementType); - // At & Assert - ExceptionAssert.ThrowsArgument( - () => new ODataQueryOptions(context, request), - "context", "The property 'ElementClrType' of ODataQueryContext cannot be null."); - } + // At & Assert + ExceptionAssert.ThrowsArgument( + () => new ODataQueryOptions(context, request), + "context", "The property 'ElementClrType' of ODataQueryContext cannot be null."); + } - [Fact] - public void Ctor_SuccedsIfEntityTypesMatch() - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); + [Fact] + public void Ctor_SuccedsIfEntityTypesMatch() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); - ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); + ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); - // Act - ODataQueryOptions query = new ODataQueryOptions(context, request); + // Act + ODataQueryOptions query = new ODataQueryOptions(context, request); - // Assert - Assert.Equal("10", query.Top.RawValue); - } + // Assert + Assert.Equal("10", query.Top.RawValue); + } - [Theory] - [InlineData("IfMatch")] - [InlineData("IfNoneMatch")] - public void GetIfMatchOrNoneMatch_ReturnsETag_SetETagHeaderValue(string header) + [Theory] + [InlineData("IfMatch")] + [InlineData("IfNoneMatch")] + public void GetIfMatchOrNoneMatch_ReturnsETag_SetETagHeaderValue(string header) + { + // Arrange + ODataModelBuilder builder = new ODataModelBuilder(); + EntityTypeConfiguration customer = builder.EntityType(); + customer.HasKey(c => c.Id); + customer.Property(c => c.Id); + customer.Property(c => c.Name).IsConcurrencyToken(); + builder.EntitySet("Customers"); + IEdmModel model = builder.GetEdmModel(); + + IEdmEntitySet customers = model.FindDeclaredEntitySet("Customers"); + + HttpRequest request = RequestFactory.Create(model, opt => opt.AddRouteComponents(model)); + + EntitySetSegment entitySetSegment = new EntitySetSegment(customers); + ODataPath odataPath = new ODataPath(new[] { entitySetSegment }); + request.ODataFeature().Path = odataPath; + + Dictionary properties = new Dictionary { { "Name", "Foo" } }; + EntityTagHeaderValue etagHeaderValue = new DefaultODataETagHandler().CreateETag(properties); + if (header.Equals("IfMatch")) { - // Arrange - ODataModelBuilder builder = new ODataModelBuilder(); - EntityTypeConfiguration customer = builder.EntityType(); - customer.HasKey(c => c.Id); - customer.Property(c => c.Id); - customer.Property(c => c.Name).IsConcurrencyToken(); - builder.EntitySet("Customers"); - IEdmModel model = builder.GetEdmModel(); - - IEdmEntitySet customers = model.FindDeclaredEntitySet("Customers"); - - HttpRequest request = RequestFactory.Create(model, opt => opt.AddRouteComponents(model)); - - EntitySetSegment entitySetSegment = new EntitySetSegment(customers); - ODataPath odataPath = new ODataPath(new[] { entitySetSegment }); - request.ODataFeature().Path = odataPath; - - Dictionary properties = new Dictionary { { "Name", "Foo" } }; - EntityTagHeaderValue etagHeaderValue = new DefaultODataETagHandler().CreateETag(properties); - if (header.Equals("IfMatch")) - { - request.Headers.AddIfMatch(etagHeaderValue); - } - else - { - request.Headers.AddIfNoneMatch(etagHeaderValue); - } - - ODataQueryContext context = new ODataQueryContext(model, typeof(QCustomer)); - - // Act - ODataQueryOptions query = new ODataQueryOptions(context, request); - ETag result = header.Equals("IfMatch") ? query.IfMatch : query.IfNoneMatch; - dynamic dynamicResult = result; - - // Assert - Assert.Equal("Foo", result["Name"]); - Assert.Equal("Foo", dynamicResult.Name); + request.Headers.AddIfMatch(etagHeaderValue); } - - [Theory] - [InlineData("IfMatch")] - [InlineData("IfNoneMatch")] - public void GetIfMatchOrNoneMatch_ETagIsNull_IfETagHeaderValueNotSet(string header) + else { - // Arrange - ODataModelBuilder builder = new ODataModelBuilder(); - EntityTypeConfiguration customer = builder.EntityType(); - customer.HasKey(c => c.Id); - customer.Property(c => c.Id); - IEdmModel model = builder.GetEdmModel(); - HttpRequest request = RequestFactory.Create(model, opt => opt.AddRouteComponents(model)); - ODataQueryContext context = new ODataQueryContext(model, typeof(QCustomer)); - - // Act - ODataQueryOptions query = new ODataQueryOptions(context, request); - ETag result = header.Equals("IfMatch") ? query.IfMatch : query.IfNoneMatch; - - // Assert - Assert.Null(result); + request.Headers.AddIfNoneMatch(etagHeaderValue); } - [Fact] - public void ApplyTo_ThrowsArgument_If_QueryTypeDoesnotMatch() - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); + ODataQueryContext context = new ODataQueryContext(model, typeof(QCustomer)); - ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); + // Act + ODataQueryOptions query = new ODataQueryOptions(context, request); + ETag result = header.Equals("IfMatch") ? query.IfMatch : query.IfNoneMatch; + dynamic dynamicResult = result; - ODataQueryOptions query = new ODataQueryOptions(context, request); + // Assert + Assert.Equal("Foo", result["Name"]); + Assert.Equal("Foo", dynamicResult.Name); + } - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => query.ApplyTo(Enumerable.Empty().AsQueryable()), - "query", - "Cannot apply ODataQueryOptions of 'Microsoft.AspNetCore.OData.Tests.Query.ODataQueryOptionsOfTEntityTests+QCustomer' to IQueryable of 'System.Int32'. (Parameter 'query')"); - } + [Theory] + [InlineData("IfMatch")] + [InlineData("IfNoneMatch")] + public void GetIfMatchOrNoneMatch_ETagIsNull_IfETagHeaderValueNotSet(string header) + { + // Arrange + ODataModelBuilder builder = new ODataModelBuilder(); + EntityTypeConfiguration customer = builder.EntityType(); + customer.HasKey(c => c.Id); + customer.Property(c => c.Id); + IEdmModel model = builder.GetEdmModel(); + HttpRequest request = RequestFactory.Create(model, opt => opt.AddRouteComponents(model)); + ODataQueryContext context = new ODataQueryContext(model, typeof(QCustomer)); + + // Act + ODataQueryOptions query = new ODataQueryOptions(context, request); + ETag result = header.Equals("IfMatch") ? query.IfMatch : query.IfNoneMatch; + + // Assert + Assert.Null(result); + } - [Fact] - public void ApplyTo_Succeeds_If_QueryTypeDerivesFromOptionsType() - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); + [Fact] + public void ApplyTo_ThrowsArgument_If_QueryTypeDoesnotMatch() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); - ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); + ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); - ODataQueryOptions query = new ODataQueryOptions(context, request); + ODataQueryOptions query = new ODataQueryOptions(context, request); - ExceptionAssert.DoesNotThrow( - () => query.ApplyTo(Enumerable.Empty().AsQueryable())); - } + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => query.ApplyTo(Enumerable.Empty().AsQueryable()), + "query", + "Cannot apply ODataQueryOptions of 'Microsoft.AspNetCore.OData.Tests.Query.ODataQueryOptionsOfTEntityTests+QCustomer' to IQueryable of 'System.Int32'. (Parameter 'query')"); + } - [Fact] - public void ApplyTo_Succeeds_If_QueryTypeMatchesOptionsType() - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); + [Fact] + public void ApplyTo_Succeeds_If_QueryTypeDerivesFromOptionsType() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); - ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); + ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); - ODataQueryOptions query = new ODataQueryOptions(context, request); + ODataQueryOptions query = new ODataQueryOptions(context, request); - // Act & Assert - ExceptionAssert.DoesNotThrow( - () => query.ApplyTo(Enumerable.Empty().AsQueryable())); - } + ExceptionAssert.DoesNotThrow( + () => query.ApplyTo(Enumerable.Empty().AsQueryable())); + } - [Fact] - public void ApplyTo_WithQuerySettings_ThrowsArgument_If_QueryTypeDoesnotMatch() - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); + [Fact] + public void ApplyTo_Succeeds_If_QueryTypeMatchesOptionsType() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); - ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); + ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); - ODataQueryOptions query = new ODataQueryOptions(context, request); + ODataQueryOptions query = new ODataQueryOptions(context, request); - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => query.ApplyTo(Enumerable.Empty().AsQueryable(), new ODataQuerySettings()), - "query", - "Cannot apply ODataQueryOptions of 'Microsoft.AspNetCore.OData.Tests.Query.ODataQueryOptionsOfTEntityTests+QCustomer' to IQueryable of 'System.Int32'. (Parameter 'query')"); - } + // Act & Assert + ExceptionAssert.DoesNotThrow( + () => query.ApplyTo(Enumerable.Empty().AsQueryable())); + } - [Fact] - public void ApplyTo_WithQuerySettings_Succeeds_If_QueryTypeDerivesFromOptionsType() - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); + [Fact] + public void ApplyTo_WithQuerySettings_ThrowsArgument_If_QueryTypeDoesnotMatch() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); - ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); + ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); - ODataQueryOptions query = new ODataQueryOptions(context, request); + ODataQueryOptions query = new ODataQueryOptions(context, request); - // Act & Assert - ExceptionAssert.DoesNotThrow( - () => query.ApplyTo(Enumerable.Empty().AsQueryable(), new ODataQuerySettings())); - } + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => query.ApplyTo(Enumerable.Empty().AsQueryable(), new ODataQuerySettings()), + "query", + "Cannot apply ODataQueryOptions of 'Microsoft.AspNetCore.OData.Tests.Query.ODataQueryOptionsOfTEntityTests+QCustomer' to IQueryable of 'System.Int32'. (Parameter 'query')"); + } - [Fact] - public void ApplyTo_WithQuerySettings_Succeeds_If_QueryTypeMatchesOptionsType() - { - // Arrange - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); + [Fact] + public void ApplyTo_WithQuerySettings_Succeeds_If_QueryTypeDerivesFromOptionsType() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); - ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); + ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); - ODataQueryOptions query = new ODataQueryOptions(context, request); + ODataQueryOptions query = new ODataQueryOptions(context, request); - // Act & Assert - ExceptionAssert.DoesNotThrow( - () => query.ApplyTo(Enumerable.Empty().AsQueryable(), new ODataQuerySettings())); - } + // Act & Assert + ExceptionAssert.DoesNotThrow( + () => query.ApplyTo(Enumerable.Empty().AsQueryable(), new ODataQuerySettings())); + } - private static IEdmModel GetEdmModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } + [Fact] + public void ApplyTo_WithQuerySettings_Succeeds_If_QueryTypeMatchesOptionsType() + { + // Arrange + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/?$top=10"); - public class QCustomer - { - public int Id { get; set; } + ODataQueryContext context = new ODataQueryContext(_model, typeof(QCustomer)); - public string Name { get; set; } - } + ODataQueryOptions query = new ODataQueryOptions(context, request); - public class SubQCustomer : QCustomer - { - } + // Act & Assert + ExceptionAssert.DoesNotThrow( + () => query.ApplyTo(Enumerable.Empty().AsQueryable(), new ODataQuerySettings())); + } + + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); + } + + public class QCustomer + { + public int Id { get; set; } + + public string Name { get; set; } + } + + public class SubQCustomer : QCustomer + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryParameterBindingAttributeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryParameterBindingAttributeTests.cs index fe0cc942a..02c5bb3b5 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryParameterBindingAttributeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/ODataQueryParameterBindingAttributeTests.cs @@ -20,139 +20,138 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class ODataQueryParameterBindingAttributeTests { - public class ODataQueryParameterBindingAttributeTests + [Fact] + public void BindModelAsync_ThrowsArgumentNull_BindingContext() { - [Fact] - public void BindModelAsync_ThrowsArgumentNull_BindingContext() - { - // Arrange & Act & Assert - ODataQueryParameterBindingAttribute.ODataQueryParameterBinding binding = new ODataQueryParameterBindingAttribute.ODataQueryParameterBinding(); - ExceptionAssert.ThrowsArgumentNull(() => binding.BindModelAsync(null), "bindingContext"); - } + // Arrange & Act & Assert + ODataQueryParameterBindingAttribute.ODataQueryParameterBinding binding = new ODataQueryParameterBindingAttribute.ODataQueryParameterBinding(); + ExceptionAssert.ThrowsArgumentNull(() => binding.BindModelAsync(null), "bindingContext"); + } - [Fact] - public void BindModelAsync_ThrowsArgument_ModelBindingContextMustHaveRequest() - { - // Arrange - ODataQueryParameterBindingAttribute.ODataQueryParameterBinding binding = new ODataQueryParameterBindingAttribute.ODataQueryParameterBinding(); - Mock httpContext = new Mock(); - httpContext.Setup(c => c.Request).Returns((HttpRequest)null); - - Mock context = new Mock(); - context.Setup(c => c.HttpContext).Returns(httpContext.Object); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => binding.BindModelAsync(context.Object), - "bindingContext", - "The model binding context requires an attached request in order to model binding."); - } - - [Fact] - public void BindModelAsync_ThrowsArgument_ActionContextMustHaveDescriptor() - { - // Arrange - ODataQueryParameterBindingAttribute.ODataQueryParameterBinding binding = new ODataQueryParameterBindingAttribute.ODataQueryParameterBinding(); - - Mock context = new Mock(); - context.Setup(c => c.HttpContext).Returns(new DefaultHttpContext()); - - ActionContext actionContext = new ActionContext - { - ActionDescriptor = null - }; - - context.Setup(c => c.ActionContext).Returns(actionContext); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => binding.BindModelAsync(context.Object), - "actionContext", - "The HttpActionContext.ActionDescriptor is null."); - } - - [Theory] - [InlineData(typeof(int[]), typeof(int))] - [InlineData(typeof(IEnumerable), typeof(int))] - [InlineData(typeof(List), typeof(int))] - [InlineData(typeof(IQueryable), typeof(int))] - [InlineData(typeof(Task>), typeof(int))] - public void GetEntityClrTypeFromActionReturnType_Returns_CorrectEntityType(Type returnType, Type elementType) - { - // Arrange - Mock mock = new Mock(); - mock.Setup(s => s.ReturnType).Returns(returnType); - ControllerActionDescriptor descriptor = new ControllerActionDescriptor(); - descriptor.MethodInfo = mock.Object; - - // Act & Assert - Assert.Equal( - elementType, - ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.GetEntityClrTypeFromActionReturnType(descriptor)); - } - - [Theory] - [InlineData(typeof(ODataQueryOptions), typeof(int))] - [InlineData(typeof(ODataQueryOptions), typeof(string))] - [InlineData(typeof(ODataQueryOptions), null)] - [InlineData(typeof(int), null)] - public void GetEntityClrTypeFromParameterType_Returns_CorrectEntityType(Type parameterType, Type elementType) - { - // Arrange & Act & Assert - Assert.Equal(elementType, - ODataQueryParameterBindingAttribute.GetEntityClrTypeFromParameterType(parameterType)); - } + [Fact] + public void BindModelAsync_ThrowsArgument_ModelBindingContextMustHaveRequest() + { + // Arrange + ODataQueryParameterBindingAttribute.ODataQueryParameterBinding binding = new ODataQueryParameterBindingAttribute.ODataQueryParameterBinding(); + Mock httpContext = new Mock(); + httpContext.Setup(c => c.Request).Returns((HttpRequest)null); + + Mock context = new Mock(); + context.Setup(c => c.HttpContext).Returns(httpContext.Object); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => binding.BindModelAsync(context.Object), + "bindingContext", + "The model binding context requires an attached request in order to model binding."); + } - [Fact] - public void GetEntityClrTypeFromActionReturnType_ThrowsInvalidOperation_ActionDescriptorNotControllerActionDescriptor() - { - // Arrange & Act & Assert - ActionDescriptor descriptor = new Mock().Object; + [Fact] + public void BindModelAsync_ThrowsArgument_ActionContextMustHaveDescriptor() + { + // Arrange + ODataQueryParameterBindingAttribute.ODataQueryParameterBinding binding = new ODataQueryParameterBindingAttribute.ODataQueryParameterBinding(); - ExceptionAssert.Throws( - () => ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.GetEntityClrTypeFromActionReturnType(descriptor), - "ActionDescriptor is not ControllerActionDescriptor."); - } + Mock context = new Mock(); + context.Setup(c => c.HttpContext).Returns(new DefaultHttpContext()); - [Fact] - public void GetEntityClrTypeFromActionReturnType_ThrowsInvalidOperation_FailedToBuildEdmModelBecauseReturnTypeIsNull() - { - // Arrange & Act & Assert - ControllerActionDescriptor descriptor = new ControllerActionDescriptor(); - Mock mock = new Mock(); - mock.Setup(s => s.ReturnType).Returns((Type)null); - descriptor.MethodInfo = mock.Object; - - ExceptionAssert.Throws( - () => ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.GetEntityClrTypeFromActionReturnType(descriptor), - "Cannot create an EDM model as the action '' on controller '' has a void return type."); - } - - [Fact] - public void GetEntityClrTypeFromActionReturnType_ThrowsInvalidOperation_FailedToRetrieveTypeToBuildEdmModel() - { - // Arrange & Act & Assert - ControllerActionDescriptor descriptor = new ControllerActionDescriptor(); - Mock mock = new Mock(); - mock.Setup(s => s.ReturnType).Returns(typeof(int)); - descriptor.MethodInfo = mock.Object; - - ExceptionAssert.Throws( - () => ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.GetEntityClrTypeFromActionReturnType(descriptor), - "Cannot create an EDM model as the action '' on controller '' has a return type 'System.Int32' that does not implement IEnumerable."); - } - - [Fact] - public void IsODataQueryOptions_Returns_BooleanAsExpected() + ActionContext actionContext = new ActionContext { - // Arrange & Act & Assert - Assert.False(ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.IsODataQueryOptions(null)); - Assert.False(ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.IsODataQueryOptions(typeof(int))); + ActionDescriptor = null + }; + + context.Setup(c => c.ActionContext).Returns(actionContext); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => binding.BindModelAsync(context.Object), + "actionContext", + "The HttpActionContext.ActionDescriptor is null."); + } + + [Theory] + [InlineData(typeof(int[]), typeof(int))] + [InlineData(typeof(IEnumerable), typeof(int))] + [InlineData(typeof(List), typeof(int))] + [InlineData(typeof(IQueryable), typeof(int))] + [InlineData(typeof(Task>), typeof(int))] + public void GetEntityClrTypeFromActionReturnType_Returns_CorrectEntityType(Type returnType, Type elementType) + { + // Arrange + Mock mock = new Mock(); + mock.Setup(s => s.ReturnType).Returns(returnType); + ControllerActionDescriptor descriptor = new ControllerActionDescriptor(); + descriptor.MethodInfo = mock.Object; + + // Act & Assert + Assert.Equal( + elementType, + ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.GetEntityClrTypeFromActionReturnType(descriptor)); + } + + [Theory] + [InlineData(typeof(ODataQueryOptions), typeof(int))] + [InlineData(typeof(ODataQueryOptions), typeof(string))] + [InlineData(typeof(ODataQueryOptions), null)] + [InlineData(typeof(int), null)] + public void GetEntityClrTypeFromParameterType_Returns_CorrectEntityType(Type parameterType, Type elementType) + { + // Arrange & Act & Assert + Assert.Equal(elementType, + ODataQueryParameterBindingAttribute.GetEntityClrTypeFromParameterType(parameterType)); + } + + [Fact] + public void GetEntityClrTypeFromActionReturnType_ThrowsInvalidOperation_ActionDescriptorNotControllerActionDescriptor() + { + // Arrange & Act & Assert + ActionDescriptor descriptor = new Mock().Object; + + ExceptionAssert.Throws( + () => ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.GetEntityClrTypeFromActionReturnType(descriptor), + "ActionDescriptor is not ControllerActionDescriptor."); + } + + [Fact] + public void GetEntityClrTypeFromActionReturnType_ThrowsInvalidOperation_FailedToBuildEdmModelBecauseReturnTypeIsNull() + { + // Arrange & Act & Assert + ControllerActionDescriptor descriptor = new ControllerActionDescriptor(); + Mock mock = new Mock(); + mock.Setup(s => s.ReturnType).Returns((Type)null); + descriptor.MethodInfo = mock.Object; + + ExceptionAssert.Throws( + () => ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.GetEntityClrTypeFromActionReturnType(descriptor), + "Cannot create an EDM model as the action '' on controller '' has a void return type."); + } + + [Fact] + public void GetEntityClrTypeFromActionReturnType_ThrowsInvalidOperation_FailedToRetrieveTypeToBuildEdmModel() + { + // Arrange & Act & Assert + ControllerActionDescriptor descriptor = new ControllerActionDescriptor(); + Mock mock = new Mock(); + mock.Setup(s => s.ReturnType).Returns(typeof(int)); + descriptor.MethodInfo = mock.Object; + + ExceptionAssert.Throws( + () => ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.GetEntityClrTypeFromActionReturnType(descriptor), + "Cannot create an EDM model as the action '' on controller '' has a return type 'System.Int32' that does not implement IEnumerable."); + } + + [Fact] + public void IsODataQueryOptions_Returns_BooleanAsExpected() + { + // Arrange & Act & Assert + Assert.False(ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.IsODataQueryOptions(null)); + Assert.False(ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.IsODataQueryOptions(typeof(int))); - Assert.True(ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.IsODataQueryOptions(typeof(ODataQueryOptions))); - Assert.True(ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.IsODataQueryOptions(typeof(ODataQueryOptions<>))); - } + Assert.True(ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.IsODataQueryOptions(typeof(ODataQueryOptions))); + Assert.True(ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.IsODataQueryOptions(typeof(ODataQueryOptions<>))); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/OrderByQueryOptionTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/OrderByQueryOptionTest.cs index 06f5322e7..e80667bc3 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/OrderByQueryOptionTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/OrderByQueryOptionTest.cs @@ -19,708 +19,707 @@ using System.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class OrderByQueryOptionTest { - public class OrderByQueryOptionTest + [Fact] + public void ConstructorNullContextThrows() { - [Fact] - public void ConstructorNullContextThrows() - { - ExceptionAssert.Throws(() => - new OrderByQueryOption("Name", null)); - } + ExceptionAssert.Throws(() => + new OrderByQueryOption("Name", null)); + } - [Fact] - public void ConstructorNullRawValueThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + [Fact] + public void ConstructorNullRawValueThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - // Act & Assert - ExceptionAssert.Throws(() => - new OrderByQueryOption(null, new ODataQueryContext(model, typeof(Customer)))); - } + // Act & Assert + ExceptionAssert.Throws(() => + new OrderByQueryOption(null, new ODataQueryContext(model, typeof(Customer)))); + } - [Fact] - public void ConstructorEmptyRawValueThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + [Fact] + public void ConstructorEmptyRawValueThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - // Act & Assert - ExceptionAssert.Throws(() => - new OrderByQueryOption(string.Empty, new ODataQueryContext(model, typeof(Customer)))); - } + // Act & Assert + ExceptionAssert.Throws(() => + new OrderByQueryOption(string.Empty, new ODataQueryContext(model, typeof(Customer)))); + } - [Fact] - public void ConstructorNullQueryOptionParserThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => - new OrderByQueryOption("test", new ODataQueryContext(model, typeof(Customer)), queryOptionParser: null), - "queryOptionParser"); - } - - [Theory] - [InlineData("Name")] - [InlineData("''")] - public void CanConstructValidFilterQuery(string orderbyValue) - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)); - var orderby = new OrderByQueryOption(orderbyValue, context); + [Fact] + public void ConstructorNullQueryOptionParserThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - Assert.Same(context, orderby.Context); - Assert.Equal(orderbyValue, orderby.RawValue); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => + new OrderByQueryOption("test", new ODataQueryContext(model, typeof(Customer)), queryOptionParser: null), + "queryOptionParser"); + } - [Fact] - public void PropertyNodes_Getter_Parses_Query() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderby = new OrderByQueryOption("Name,Website", context); - - ICollection nodes = orderby.OrderByNodes; - - // Assert - Assert.False(nodes.OfType().Any()); - IEnumerable propertyNodes = nodes.OfType(); - Assert.NotNull(propertyNodes); - Assert.Equal(2, propertyNodes.Count()); - Assert.Equal("Name", propertyNodes.First().Property.Name); - Assert.Equal("Website", propertyNodes.Last().Property.Name); - } - - //[Theory] - //[InlineData("BadPropertyName")] - //[InlineData("''")] - //[InlineData(" ")] - //[InlineData("Id,Id")] - //[InlineData("Id,Name,Id")] - //public void ApplyInValidOrderbyQueryThrows(string orderbyValue) - //{ - // var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - // var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - // var orderby = new OrderByQueryOption(orderbyValue, context); - - // ExceptionAssert.Throws(() => - // orderby.ApplyTo(ODataQueryOptionTest.Customers)); - //} - - [Theory] - [InlineData("id")] - [InlineData("iD")] - [InlineData("ID")] - public void CanApplyOrderByQueryCaseInsensitive(string orderbyValue) - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderby = new OrderByQueryOption(orderbyValue, context); - - var customers = (new List{ - new Customer { Id = 2, Name = "Aaron" }, - new Customer { Id = 1, Name = "Andy" }, - new Customer { Id = 3, Name = "Alex" } - }).AsQueryable(); - - var results = orderby.ApplyTo(customers).ToArray(); - Assert.Equal(1, results[0].Id); - Assert.Equal(2, results[1].Id); - Assert.Equal(3, results[2].Id); - } - - [Fact] - [Trait("Description", "Can apply an orderby")] - public void CanApplyOrderBy() - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("Name", context); - - var customers = (new List{ - new Customer { Id = 1, Name = "Andy" }, - new Customer { Id = 2, Name = "Aaron" }, - new Customer { Id = 3, Name = "Alex" } - }).AsQueryable(); - - var results = orderByOption.ApplyTo(customers).ToArray(); - Assert.Equal(2, results[0].Id); - Assert.Equal(3, results[1].Id); - Assert.Equal(1, results[2].Id); - } - - [Fact] - [Trait("Description", "Can apply an orderby")] - public void CanApplyOrderByAsc() - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("Name asc", context); - - var customers = (new List{ - new Customer { Id = 1, Name = "Andy" }, - new Customer { Id = 2, Name = "Aaron" }, - new Customer { Id = 3, Name = "Alex" } - }).AsQueryable(); - - var results = orderByOption.ApplyTo(customers).ToArray(); - Assert.Equal(2, results[0].Id); - Assert.Equal(3, results[1].Id); - Assert.Equal(1, results[2].Id); - } - - [Fact] - [Trait("Description", "Can apply an orderby descending")] - public void CanApplyOrderByDescending() - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("Name desc", context); - - var customers = (new List{ - new Customer { Id = 1, Name = "Andy" }, - new Customer { Id = 2, Name = "Aaron" }, - new Customer { Id = 3, Name = "Alex" } - }).AsQueryable(); - - var results = orderByOption.ApplyTo(customers).ToArray(); - Assert.Equal(1, results[0].Id); - Assert.Equal(3, results[1].Id); - Assert.Equal(2, results[2].Id); - } - - [Fact] - [Trait("Description", "Can apply a compound orderby")] - public void CanApplyOrderByThenBy() - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("Name,Website", context); - - var customers = (new List{ - new Customer { Id = 1, Name = "ACME", Website = "http://www.acme.net" }, - new Customer { Id = 2, Name = "AAAA", Website = "http://www.aaaa.com" }, - new Customer { Id = 3, Name = "ACME", Website = "http://www.acme.com" } - }).AsQueryable(); - - var results = orderByOption.ApplyTo(customers).ToArray(); - Assert.Equal(2, results[0].Id); - Assert.Equal(3, results[1].Id); - Assert.Equal(1, results[2].Id); - } - - [Fact] - [Trait("Description", "Can apply a OrderByDescending followed by ThenBy")] - public void CanApplyOrderByDescThenBy() - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("Name desc,Website", context); - - var customers = (new List{ - new Customer { Id = 1, Name = "ACME", Website = "http://www.acme.net" }, - new Customer { Id = 2, Name = "AAAA", Website = "http://www.aaaa.com" }, - new Customer { Id = 3, Name = "ACME", Website = "http://www.acme.com" } - }).AsQueryable(); - - var results = orderByOption.ApplyTo(customers).ToArray(); - Assert.Equal(3, results[0].Id); - Assert.Equal(1, results[1].Id); - Assert.Equal(2, results[2].Id); - } - - [Fact] - [Trait("Description", "Can apply a OrderByDescending followed by ThenBy")] - public void CanApplyOrderByDescThenByDesc() - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("Name desc,Website desc", context); - - var customers = (new List{ - new Customer { Id = 1, Name = "ACME", Website = "http://www.acme.net" }, - new Customer { Id = 2, Name = "AAAA", Website = "http://www.aaaa.com" }, - new Customer { Id = 3, Name = "ACME", Website = "http://www.acme.com" } - }).AsQueryable(); - - var results = orderByOption.ApplyTo(customers).ToArray(); - Assert.Equal(1, results[0].Id); - Assert.Equal(3, results[1].Id); - Assert.Equal(2, results[2].Id); - } - - [Fact] - public void ApplyToEnums_ReturnsCorrectQueryable() - { - // Arrange - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("EnumModels"); - var model = builder.GetEdmModel(); - - var context = new ODataQueryContext(model, typeof(EnumModel)) { RequestContainer = new MockServiceProvider() }; - var orderbyOption = new OrderByQueryOption("Flag", context); - IEnumerable enumModels = FilterQueryOptionTest.EnumModelTestData; - - // Act - IQueryable queryable = orderbyOption.ApplyTo(enumModels.AsQueryable()); - - // Assert - Assert.NotNull(queryable); - IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); - Assert.Equal( - new int[] { 5, 2, 1, 3, 6 }, - actualCustomers.Select(enumModel => enumModel.Id)); - } - - [Theory] - [InlineData(true, "FirstNameAlias")] - [InlineData(false, "FirstName")] - public void ApplyTo_PropertyAliased_IfEnabled(bool modelAliasing, string propertyName) - { - // Arrange - var builder = new ODataConventionModelBuilder(TestAssemblyResolver.Instance); - builder.ModelAliasingEnabled = modelAliasing; - builder.EntitySet("PropertyAliases"); - var model = builder.GetEdmModel(); - - var context = new ODataQueryContext(model, typeof(PropertyAlias)) { RequestContainer = new MockServiceProvider(model) }; - var orderByOption = new OrderByQueryOption(propertyName, context); - IEnumerable propertyAliases = FilterQueryOptionTest.PropertyAliasTestData; - - // Act - IQueryable queryable = orderByOption.ApplyTo(propertyAliases.AsQueryable()); - - // Assert - Assert.NotNull(queryable); - IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); - Assert.Equal( - new[] { "abc", "def", "xyz" }, - actualCustomers.Select(propertyAlias => propertyAlias.FirstName)); - } - - [Theory] - [InlineData("SharePrice add 1")] - [InlineData("tolower(Name)")] - public void OrderBy_Works_For_Expressions(string orderByQuery) - { - var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); - var orderByOption = new OrderByQueryOption(orderByQuery, new ODataQueryContext(model, typeof(Customer))); - - var nodes = orderByOption.OrderByNodes; - OrderByNode orderByNode = Assert.Single(nodes); - OrderByClauseNode clauseNode = Assert.IsType(orderByNode); - } - - //[Fact] - //public void CanTurnOffValidationForOrderBy() - //{ - // // Arrange - // ODataQueryContext context = ValidationTestHelper.CreateCustomerContext(); - - // OrderByQueryOption option = new OrderByQueryOption("Name", context); - // ODataValidationSettings settings = new ODataValidationSettings(); - // settings.AllowedOrderByProperties.Add("Id"); - - // // Act & Assert - // ExceptionAssert.Throws(() => option.Validate(settings), - // "Order by 'Name' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); - - // option.Validator = null; - // ExceptionAssert.DoesNotThrow(() => option.Validate(settings)); - //} - - [Fact] - public void OrderByDuplicatePropertyThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + [Theory] + [InlineData("Name")] + [InlineData("''")] + public void CanConstructValidFilterQuery(string orderbyValue) + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)); + var orderby = new OrderByQueryOption(orderbyValue, context); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderbyOption = new OrderByQueryOption("Name, Name", context); + Assert.Same(context, orderby.Context); + Assert.Equal(orderbyValue, orderby.RawValue); + } - // Act - ExceptionAssert.Throws( - () => orderbyOption.ApplyTo(Enumerable.Empty().AsQueryable()), - "Duplicate property named 'Name' is not supported in '$orderby'."); - } + [Fact] + public void PropertyNodes_Getter_Parses_Query() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderby = new OrderByQueryOption("Name,Website", context); + + ICollection nodes = orderby.OrderByNodes; + + // Assert + Assert.False(nodes.OfType().Any()); + IEnumerable propertyNodes = nodes.OfType(); + Assert.NotNull(propertyNodes); + Assert.Equal(2, propertyNodes.Count()); + Assert.Equal("Name", propertyNodes.First().Property.Name); + Assert.Equal("Website", propertyNodes.Last().Property.Name); + } - [Fact] - public void OrderByDuplicateItThrows() - { - // Arrange - var context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - var orderbyOption = new OrderByQueryOption("$it, $it", context); - - // Act - ExceptionAssert.Throws( - () => orderbyOption.ApplyTo(Enumerable.Empty().AsQueryable()), - "Multiple '$it' nodes are not supported in '$orderby'."); - } - - [Fact] - public void OrderByDuplicatePropertyOfComplexTypeThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); + //[Theory] + //[InlineData("BadPropertyName")] + //[InlineData("''")] + //[InlineData(" ")] + //[InlineData("Id,Id")] + //[InlineData("Id,Name,Id")] + //public void ApplyInValidOrderbyQueryThrows(string orderbyValue) + //{ + // var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + // var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + // var orderby = new OrderByQueryOption(orderbyValue, context); + + // ExceptionAssert.Throws(() => + // orderby.ApplyTo(ODataQueryOptionTest.Customers)); + //} + + [Theory] + [InlineData("id")] + [InlineData("iD")] + [InlineData("ID")] + public void CanApplyOrderByQueryCaseInsensitive(string orderbyValue) + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderby = new OrderByQueryOption(orderbyValue, context); + + var customers = (new List{ + new Customer { Id = 2, Name = "Aaron" }, + new Customer { Id = 1, Name = "Andy" }, + new Customer { Id = 3, Name = "Alex" } + }).AsQueryable(); + + var results = orderby.ApplyTo(customers).ToArray(); + Assert.Equal(1, results[0].Id); + Assert.Equal(2, results[1].Id); + Assert.Equal(3, results[2].Id); + } - var context = new ODataQueryContext(model, typeof(Customer)){ RequestContainer = new MockServiceProvider() }; - var orderbyOption = new OrderByQueryOption("Address/City, Address/City", context); + [Fact] + [Trait("Description", "Can apply an orderby")] + public void CanApplyOrderBy() + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("Name", context); + + var customers = (new List{ + new Customer { Id = 1, Name = "Andy" }, + new Customer { Id = 2, Name = "Aaron" }, + new Customer { Id = 3, Name = "Alex" } + }).AsQueryable(); + + var results = orderByOption.ApplyTo(customers).ToArray(); + Assert.Equal(2, results[0].Id); + Assert.Equal(3, results[1].Id); + Assert.Equal(1, results[2].Id); + } - // Act - ExceptionAssert.Throws( - () => orderbyOption.ApplyTo(Enumerable.Empty().AsQueryable()), - "Duplicate property named 'Address/City' is not supported in '$orderby'."); - } + [Fact] + [Trait("Description", "Can apply an orderby")] + public void CanApplyOrderByAsc() + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("Name asc", context); + + var customers = (new List{ + new Customer { Id = 1, Name = "Andy" }, + new Customer { Id = 2, Name = "Aaron" }, + new Customer { Id = 3, Name = "Alex" } + }).AsQueryable(); + + var results = orderByOption.ApplyTo(customers).ToArray(); + Assert.Equal(2, results[0].Id); + Assert.Equal(3, results[1].Id); + Assert.Equal(1, results[2].Id); + } - [Fact] - public void ApplyTo_NestedProperties_Succeeds() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("Address/City asc", context); - - var customers = (new List{ - new Customer { Id = 1, Address = new Address { City = "C" } }, - new Customer { Id = 2, Address = new Address { City = "B" } }, - new Customer { Id = 3, Address = new Address { City = "A" } } - }).AsQueryable(); - - // Act - var results = orderByOption.ApplyTo(customers).ToArray(); - - // Assert - Assert.Equal(3, results[0].Id); - Assert.Equal(2, results[1].Id); - Assert.Equal(1, results[2].Id); - } - - [Fact] - public void ApplyTo_NestedProperties_WithDuplicateName_Succeeds() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("Address/City,City", context); - - var customers = (new List{ - new Customer { Id = 1, City = "A", Address = new Address { City = "A" } }, - new Customer { Id = 2, City = "B", Address = new Address { City = "B" } }, - new Customer { Id = 3, City = "A", Address = new Address { City = "B" } } - }).AsQueryable(); - - // Act - var results = orderByOption.ApplyTo(customers).ToArray(); - - // Assert - Assert.Equal(1, results[0].Id); - Assert.Equal(3, results[1].Id); - Assert.Equal(2, results[2].Id); - } - - [Fact] - public void ApplyTo_NestedProperties_WithDuplicatePathType_Succeeds() - { - // Arrange - var model = - new ODataModelBuilder().Add_Customer_EntityType_With_DuplicatedAddress() - .Add_Customers_EntitySet() - .GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) {RequestContainer = new MockServiceProvider()}; - var orderByOption = new OrderByQueryOption("City,Address/City,WorkAddress/City", context); - var customers = (new List + [Fact] + [Trait("Description", "Can apply an orderby descending")] + public void CanApplyOrderByDescending() + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("Name desc", context); + + var customers = (new List{ + new Customer { Id = 1, Name = "Andy" }, + new Customer { Id = 2, Name = "Aaron" }, + new Customer { Id = 3, Name = "Alex" } + }).AsQueryable(); + + var results = orderByOption.ApplyTo(customers).ToArray(); + Assert.Equal(1, results[0].Id); + Assert.Equal(3, results[1].Id); + Assert.Equal(2, results[2].Id); + } + + [Fact] + [Trait("Description", "Can apply a compound orderby")] + public void CanApplyOrderByThenBy() + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("Name,Website", context); + + var customers = (new List{ + new Customer { Id = 1, Name = "ACME", Website = "http://www.acme.net" }, + new Customer { Id = 2, Name = "AAAA", Website = "http://www.aaaa.com" }, + new Customer { Id = 3, Name = "ACME", Website = "http://www.acme.com" } + }).AsQueryable(); + + var results = orderByOption.ApplyTo(customers).ToArray(); + Assert.Equal(2, results[0].Id); + Assert.Equal(3, results[1].Id); + Assert.Equal(1, results[2].Id); + } + + [Fact] + [Trait("Description", "Can apply a OrderByDescending followed by ThenBy")] + public void CanApplyOrderByDescThenBy() + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("Name desc,Website", context); + + var customers = (new List{ + new Customer { Id = 1, Name = "ACME", Website = "http://www.acme.net" }, + new Customer { Id = 2, Name = "AAAA", Website = "http://www.aaaa.com" }, + new Customer { Id = 3, Name = "ACME", Website = "http://www.acme.com" } + }).AsQueryable(); + + var results = orderByOption.ApplyTo(customers).ToArray(); + Assert.Equal(3, results[0].Id); + Assert.Equal(1, results[1].Id); + Assert.Equal(2, results[2].Id); + } + + [Fact] + [Trait("Description", "Can apply a OrderByDescending followed by ThenBy")] + public void CanApplyOrderByDescThenByDesc() + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("Name desc,Website desc", context); + + var customers = (new List{ + new Customer { Id = 1, Name = "ACME", Website = "http://www.acme.net" }, + new Customer { Id = 2, Name = "AAAA", Website = "http://www.aaaa.com" }, + new Customer { Id = 3, Name = "ACME", Website = "http://www.acme.com" } + }).AsQueryable(); + + var results = orderByOption.ApplyTo(customers).ToArray(); + Assert.Equal(1, results[0].Id); + Assert.Equal(3, results[1].Id); + Assert.Equal(2, results[2].Id); + } + + [Fact] + public void ApplyToEnums_ReturnsCorrectQueryable() + { + // Arrange + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("EnumModels"); + var model = builder.GetEdmModel(); + + var context = new ODataQueryContext(model, typeof(EnumModel)) { RequestContainer = new MockServiceProvider() }; + var orderbyOption = new OrderByQueryOption("Flag", context); + IEnumerable enumModels = FilterQueryOptionTest.EnumModelTestData; + + // Act + IQueryable queryable = orderbyOption.ApplyTo(enumModels.AsQueryable()); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); + Assert.Equal( + new int[] { 5, 2, 1, 3, 6 }, + actualCustomers.Select(enumModel => enumModel.Id)); + } + + [Theory] + [InlineData(true, "FirstNameAlias")] + [InlineData(false, "FirstName")] + public void ApplyTo_PropertyAliased_IfEnabled(bool modelAliasing, string propertyName) + { + // Arrange + var builder = new ODataConventionModelBuilder(TestAssemblyResolver.Instance); + builder.ModelAliasingEnabled = modelAliasing; + builder.EntitySet("PropertyAliases"); + var model = builder.GetEdmModel(); + + var context = new ODataQueryContext(model, typeof(PropertyAlias)) { RequestContainer = new MockServiceProvider(model) }; + var orderByOption = new OrderByQueryOption(propertyName, context); + IEnumerable propertyAliases = FilterQueryOptionTest.PropertyAliasTestData; + + // Act + IQueryable queryable = orderByOption.ApplyTo(propertyAliases.AsQueryable()); + + // Assert + Assert.NotNull(queryable); + IEnumerable actualCustomers = Assert.IsAssignableFrom>(queryable); + Assert.Equal( + new[] { "abc", "def", "xyz" }, + actualCustomers.Select(propertyAlias => propertyAlias.FirstName)); + } + + [Theory] + [InlineData("SharePrice add 1")] + [InlineData("tolower(Name)")] + public void OrderBy_Works_For_Expressions(string orderByQuery) + { + var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); + var orderByOption = new OrderByQueryOption(orderByQuery, new ODataQueryContext(model, typeof(Customer))); + + var nodes = orderByOption.OrderByNodes; + OrderByNode orderByNode = Assert.Single(nodes); + OrderByClauseNode clauseNode = Assert.IsType(orderByNode); + } + + //[Fact] + //public void CanTurnOffValidationForOrderBy() + //{ + // // Arrange + // ODataQueryContext context = ValidationTestHelper.CreateCustomerContext(); + + // OrderByQueryOption option = new OrderByQueryOption("Name", context); + // ODataValidationSettings settings = new ODataValidationSettings(); + // settings.AllowedOrderByProperties.Add("Id"); + + // // Act & Assert + // ExceptionAssert.Throws(() => option.Validate(settings), + // "Order by 'Name' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); + + // option.Validator = null; + // ExceptionAssert.DoesNotThrow(() => option.Validate(settings)); + //} + + [Fact] + public void OrderByDuplicatePropertyThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderbyOption = new OrderByQueryOption("Name, Name", context); + + // Act + ExceptionAssert.Throws( + () => orderbyOption.ApplyTo(Enumerable.Empty().AsQueryable()), + "Duplicate property named 'Name' is not supported in '$orderby'."); + } + + [Fact] + public void OrderByDuplicateItThrows() + { + // Arrange + var context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + var orderbyOption = new OrderByQueryOption("$it, $it", context); + + // Act + ExceptionAssert.Throws( + () => orderbyOption.ApplyTo(Enumerable.Empty().AsQueryable()), + "Multiple '$it' nodes are not supported in '$orderby'."); + } + + [Fact] + public void OrderByDuplicatePropertyOfComplexTypeThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); + + var context = new ODataQueryContext(model, typeof(Customer)){ RequestContainer = new MockServiceProvider() }; + var orderbyOption = new OrderByQueryOption("Address/City, Address/City", context); + + // Act + ExceptionAssert.Throws( + () => orderbyOption.ApplyTo(Enumerable.Empty().AsQueryable()), + "Duplicate property named 'Address/City' is not supported in '$orderby'."); + } + + [Fact] + public void ApplyTo_NestedProperties_Succeeds() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("Address/City asc", context); + + var customers = (new List{ + new Customer { Id = 1, Address = new Address { City = "C" } }, + new Customer { Id = 2, Address = new Address { City = "B" } }, + new Customer { Id = 3, Address = new Address { City = "A" } } + }).AsQueryable(); + + // Act + var results = orderByOption.ApplyTo(customers).ToArray(); + + // Assert + Assert.Equal(3, results[0].Id); + Assert.Equal(2, results[1].Id); + Assert.Equal(1, results[2].Id); + } + + [Fact] + public void ApplyTo_NestedProperties_WithDuplicateName_Succeeds() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("Address/City,City", context); + + var customers = (new List{ + new Customer { Id = 1, City = "A", Address = new Address { City = "A" } }, + new Customer { Id = 2, City = "B", Address = new Address { City = "B" } }, + new Customer { Id = 3, City = "A", Address = new Address { City = "B" } } + }).AsQueryable(); + + // Act + var results = orderByOption.ApplyTo(customers).ToArray(); + + // Assert + Assert.Equal(1, results[0].Id); + Assert.Equal(3, results[1].Id); + Assert.Equal(2, results[2].Id); + } + + [Fact] + public void ApplyTo_NestedProperties_WithDuplicatePathType_Succeeds() + { + // Arrange + var model = + new ODataModelBuilder().Add_Customer_EntityType_With_DuplicatedAddress() + .Add_Customers_EntitySet() + .GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) {RequestContainer = new MockServiceProvider()}; + var orderByOption = new OrderByQueryOption("City,Address/City,WorkAddress/City", context); + var customers = (new List + { + new Customer { - new Customer - { - Id = 1, - City = "B", - Address = new Address {City = "B"}, - WorkAddress = new Address {City = "B"} - }, - new Customer + Id = 1, + City = "B", + Address = new Address {City = "B"}, + WorkAddress = new Address {City = "B"} + }, + new Customer + { + Id = 2, + City = "B", + Address = new Address {City = "B"}, + WorkAddress = new Address {City = "A"} + }, + new Customer + { + Id = 3, + City = "B", + Address = new Address {City = "A"}, + WorkAddress = new Address {City = "A"} + }, + new Customer + { + Id = 4, + City = "A", + Address = new Address {City = "A"}, + WorkAddress = new Address {City = "A"} + } + }).AsQueryable(); + + // Act + var results = orderByOption.ApplyTo(customers).ToArray(); + + // Assert + Assert.Equal(4, results[0].Id); + Assert.Equal(3, results[1].Id); + Assert.Equal(2, results[2].Id); + Assert.Equal(1, results[3].Id); + } + + [Fact] + public void ApplyTo_NestedProperties_HandlesNullPropagation_Succeeds() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("Address/City asc", context); + + var customers = (new List{ + new Customer { Id = 1, Address = null }, + new Customer { Id = 2, Address = new Address { City = "B" } }, + new Customer { Id = 3, Address = new Address { City = "A" } } + }).AsQueryable(); + + // Act + ODataQuerySettings settings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }; + var results = orderByOption.ApplyTo(customers, settings).ToArray(); + + // Assert + Assert.Equal(1, results[0].Id); + Assert.Equal(3, results[1].Id); + Assert.Equal(2, results[2].Id); + } + + [Fact] + public void ApplyTo_NestedProperties_DoesNotHandleNullPropagation_IfExplicitInSettings() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("Address/City asc", context); + + var customers = (new List{ + new Customer { Id = 1, Address = null }, + new Customer { Id = 2, Address = new Address { City = "B" } }, + new Customer { Id = 3, Address = new Address { City = "A" } } + }).AsQueryable(); + ODataQuerySettings settings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }; + + // Act & Assert + ExceptionAssert.Throws(() => orderByOption.ApplyTo(customers, settings).ToArray()); + } + + [Fact] + public void Property_OrderByNodes_WorksWithUnTypedContext() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + OrderByQueryOption orderBy = new OrderByQueryOption("ID desc", context); + + // Act & Assert + Assert.NotNull(orderBy.OrderByNodes); + } + + [Fact] + public void ApplyTo_WithUnTypedContext_Throws_InvalidOperation() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + OrderByQueryOption orderBy = new OrderByQueryOption("ID desc", context); + IQueryable queryable = new Mock().Object; + + // Act & Assert + ExceptionAssert.Throws(() => orderBy.ApplyTo(queryable), + "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); + } + + [Fact] + public void CanApplyOrderBy_WithParameterAlias() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Address_ComplexType().GetEdmModel(); + + var parser = new ODataQueryOptionParser( + model, + model.FindType("Microsoft.AspNetCore.OData.Tests.Models.Customer"), + model.FindDeclaredNavigationSource("Default.Container.Customers"), + new Dictionary { { "$orderby", "@q desc,@p asc" }, { "@q", "Address/HouseNumber" }, { "@p", "Id" } }); + + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("@q desc,@p asc", context, parser); + + var customers = (new List{ + new Customer { Id = 1, Address = new Address{HouseNumber = 2}}, + new Customer { Id = 2, Address = new Address{HouseNumber = 1}}, + new Customer { Id = 3, Address = new Address{HouseNumber = 3}}, + new Customer { Id = 4, Address = new Address{HouseNumber = 2}}, + new Customer { Id = 5, Address = new Address{HouseNumber = 1}}, + }).AsQueryable(); + + // Act + var results = orderByOption.ApplyTo(customers).ToArray(); + + // Assert + Assert.Equal(3, results[0].Id); + Assert.Equal(1, results[1].Id); + Assert.Equal(4, results[2].Id); + Assert.Equal(2, results[3].Id); + Assert.Equal(5, results[4].Id); + } + + [Fact] + public void CanApplyOrderBy_WithNestedParameterAlias() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + + var parser = new ODataQueryOptionParser( + model, + model.FindType("Microsoft.AspNetCore.OData.Tests.Models.Customer"), + model.FindDeclaredNavigationSource("Default.Container.Customers"), + new Dictionary { { "$orderby", "@p1" }, { "@p2", "Name" }, { "@p1", "@p2" } }); + + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("@p1", context, parser); + + var customers = (new List{ + new Customer { Id = 1, Name = "Andy" }, + new Customer { Id = 2, Name = "Aaron" }, + new Customer { Id = 3, Name = "Alex" } + }).AsQueryable(); + + // Act + var results = orderByOption.ApplyTo(customers).ToArray(); + + // Assert + Assert.Equal(2, results[0].Id); + Assert.Equal(3, results[1].Id); + Assert.Equal(1, results[2].Id); + } + + [Theory] + [InlineData("Orders/$count")] + [InlineData("Addresses/$count")] + [InlineData("Aliases/$count")] + public void CanApplyOrderBy_WithCollectionCount(string orderby) + { + // Arrange + var model = new ODataModelBuilder() + .Add_Order_EntityType() + .Add_Customer_EntityType_With_Address() + .Add_CustomerOrders_Relationship() + .Add_Customer_EntityType_With_CollectionProperties() + .Add_Customers_EntitySet() + .GetEdmModel(); + + var parser = new ODataQueryOptionParser( + model, + model.FindType("Microsoft.AspNetCore.OData.Tests.Models.Customer"), + model.FindDeclaredNavigationSource("Default.Container.Customers"), + new Dictionary { { "$orderby", orderby } }); + + var orderByOption = new OrderByQueryOption(orderby, new ODataQueryContext(model, typeof(Customer)), parser); + + var customers = (new List + { + new Customer + { + Id = 1, + Name = "Andy", + Orders = new List { - Id = 2, - City = "B", - Address = new Address {City = "B"}, - WorkAddress = new Address {City = "A"} + new Order { OrderId = 1 }, + new Order { OrderId = 2 } }, - new Customer + Addresses = new List
{ - Id = 3, - City = "B", - Address = new Address {City = "A"}, - WorkAddress = new Address {City = "A"} + new Address { City = "1" }, + new Address { City = "2" } }, - new Customer - { - Id = 4, - City = "A", - Address = new Address {City = "A"}, - WorkAddress = new Address {City = "A"} - } - }).AsQueryable(); - - // Act - var results = orderByOption.ApplyTo(customers).ToArray(); - - // Assert - Assert.Equal(4, results[0].Id); - Assert.Equal(3, results[1].Id); - Assert.Equal(2, results[2].Id); - Assert.Equal(1, results[3].Id); - } - - [Fact] - public void ApplyTo_NestedProperties_HandlesNullPropagation_Succeeds() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("Address/City asc", context); - - var customers = (new List{ - new Customer { Id = 1, Address = null }, - new Customer { Id = 2, Address = new Address { City = "B" } }, - new Customer { Id = 3, Address = new Address { City = "A" } } - }).AsQueryable(); - - // Act - ODataQuerySettings settings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }; - var results = orderByOption.ApplyTo(customers, settings).ToArray(); - - // Assert - Assert.Equal(1, results[0].Id); - Assert.Equal(3, results[1].Id); - Assert.Equal(2, results[2].Id); - } - - [Fact] - public void ApplyTo_NestedProperties_DoesNotHandleNullPropagation_IfExplicitInSettings() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("Address/City asc", context); - - var customers = (new List{ - new Customer { Id = 1, Address = null }, - new Customer { Id = 2, Address = new Address { City = "B" } }, - new Customer { Id = 3, Address = new Address { City = "A" } } - }).AsQueryable(); - ODataQuerySettings settings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False }; - - // Act & Assert - ExceptionAssert.Throws(() => orderByOption.ApplyTo(customers, settings).ToArray()); - } - - [Fact] - public void Property_OrderByNodes_WorksWithUnTypedContext() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - OrderByQueryOption orderBy = new OrderByQueryOption("ID desc", context); - - // Act & Assert - Assert.NotNull(orderBy.OrderByNodes); - } - - [Fact] - public void ApplyTo_WithUnTypedContext_Throws_InvalidOperation() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - OrderByQueryOption orderBy = new OrderByQueryOption("ID desc", context); - IQueryable queryable = new Mock().Object; - - // Act & Assert - ExceptionAssert.Throws(() => orderBy.ApplyTo(queryable), - "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); - } - - [Fact] - public void CanApplyOrderBy_WithParameterAlias() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Address_ComplexType().GetEdmModel(); - - var parser = new ODataQueryOptionParser( - model, - model.FindType("Microsoft.AspNetCore.OData.Tests.Models.Customer"), - model.FindDeclaredNavigationSource("Default.Container.Customers"), - new Dictionary { { "$orderby", "@q desc,@p asc" }, { "@q", "Address/HouseNumber" }, { "@p", "Id" } }); - - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("@q desc,@p asc", context, parser); - - var customers = (new List{ - new Customer { Id = 1, Address = new Address{HouseNumber = 2}}, - new Customer { Id = 2, Address = new Address{HouseNumber = 1}}, - new Customer { Id = 3, Address = new Address{HouseNumber = 3}}, - new Customer { Id = 4, Address = new Address{HouseNumber = 2}}, - new Customer { Id = 5, Address = new Address{HouseNumber = 1}}, - }).AsQueryable(); - - // Act - var results = orderByOption.ApplyTo(customers).ToArray(); - - // Assert - Assert.Equal(3, results[0].Id); - Assert.Equal(1, results[1].Id); - Assert.Equal(4, results[2].Id); - Assert.Equal(2, results[3].Id); - Assert.Equal(5, results[4].Id); - } - - [Fact] - public void CanApplyOrderBy_WithNestedParameterAlias() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - - var parser = new ODataQueryOptionParser( - model, - model.FindType("Microsoft.AspNetCore.OData.Tests.Models.Customer"), - model.FindDeclaredNavigationSource("Default.Container.Customers"), - new Dictionary { { "$orderby", "@p1" }, { "@p2", "Name" }, { "@p1", "@p2" } }); - - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("@p1", context, parser); - - var customers = (new List{ - new Customer { Id = 1, Name = "Andy" }, - new Customer { Id = 2, Name = "Aaron" }, - new Customer { Id = 3, Name = "Alex" } - }).AsQueryable(); - - // Act - var results = orderByOption.ApplyTo(customers).ToArray(); - - // Assert - Assert.Equal(2, results[0].Id); - Assert.Equal(3, results[1].Id); - Assert.Equal(1, results[2].Id); - } - - [Theory] - [InlineData("Orders/$count")] - [InlineData("Addresses/$count")] - [InlineData("Aliases/$count")] - public void CanApplyOrderBy_WithCollectionCount(string orderby) - { - // Arrange - var model = new ODataModelBuilder() - .Add_Order_EntityType() - .Add_Customer_EntityType_With_Address() - .Add_CustomerOrders_Relationship() - .Add_Customer_EntityType_With_CollectionProperties() - .Add_Customers_EntitySet() - .GetEdmModel(); - - var parser = new ODataQueryOptionParser( - model, - model.FindType("Microsoft.AspNetCore.OData.Tests.Models.Customer"), - model.FindDeclaredNavigationSource("Default.Container.Customers"), - new Dictionary { { "$orderby", orderby } }); - - var orderByOption = new OrderByQueryOption(orderby, new ODataQueryContext(model, typeof(Customer)), parser); - - var customers = (new List + Aliases = new List { "1", "2" } + }, + new Customer { - new Customer + Id = 2, + Name = "Aaron", + Orders = new List { - Id = 1, - Name = "Andy", - Orders = new List - { - new Order { OrderId = 1 }, - new Order { OrderId = 2 } - }, - Addresses = new List
- { - new Address { City = "1" }, - new Address { City = "2" } - }, - Aliases = new List { "1", "2" } + new Order { OrderId = 3 } }, - new Customer + Addresses = new List
{ - Id = 2, - Name = "Aaron", - Orders = new List - { - new Order { OrderId = 3 } - }, - Addresses = new List
- { - new Address { City = "3" } - }, - Aliases = new List { "3" } + new Address { City = "3" } }, - new Customer { Id = 3, Name = "Alex" } - }).AsQueryable(); - - // Act - ODataQuerySettings settings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }; - var results = orderByOption.ApplyTo(customers, settings).ToArray(); + Aliases = new List { "3" } + }, + new Customer { Id = 3, Name = "Alex" } + }).AsQueryable(); + + // Act + ODataQuerySettings settings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.True }; + var results = orderByOption.ApplyTo(customers, settings).ToArray(); + + // Assert + Assert.Equal(3, results[0].Id); + Assert.Equal(2, results[1].Id); + Assert.Equal(1, results[2].Id); + } - // Assert - Assert.Equal(3, results[0].Id); - Assert.Equal(2, results[1].Id); - Assert.Equal(1, results[2].Id); - } + [Fact] + public void OrderBy_Works_ParameterAlias() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var orderByOption = new OrderByQueryOption("@p", new ODataQueryContext(model, typeof(Customer))); + + // Act & Assert + var nodes = orderByOption.OrderByNodes; + OrderByNode orderByNode = Assert.Single(nodes); + OrderByClauseNode clauseNode = Assert.IsType(orderByNode); + } - [Fact] - public void OrderBy_Works_ParameterAlias() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var orderByOption = new OrderByQueryOption("@p", new ODataQueryContext(model, typeof(Customer))); - - // Act & Assert - var nodes = orderByOption.OrderByNodes; - OrderByNode orderByNode = Assert.Single(nodes); - OrderByClauseNode clauseNode = Assert.IsType(orderByNode); - } - - [Fact] - public void GetOrderByRawValues_Works_OrderByClause() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("Address/City asc", context); + [Fact] + public void GetOrderByRawValues_Works_OrderByClause() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("Address/City asc", context); - // Act - List rawValues = orderByOption.GetOrderByRawValues(); + // Act + List rawValues = orderByOption.GetOrderByRawValues(); - // Assert - string clause = Assert.Single(rawValues); - Assert.Equal("Address/City asc", clause); - } + // Assert + string clause = Assert.Single(rawValues); + Assert.Equal("Address/City asc", clause); + } - [Fact] - public void GetOrderByRawValues_Works_MultipleOrderByClause() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderByOption = new OrderByQueryOption("Address/City asc,substring(Name,2,1),tolower(substring(Name,2,1))", context); - - // Act - List rawValues = orderByOption.GetOrderByRawValues(); - - // Assert - Assert.Equal(3, rawValues.Count); - Assert.Equal("Address/City", rawValues[0]); // Here, without 'asc' since it's omitted by default - Assert.Equal("substring(Name,2,1)", rawValues[1]); - Assert.Equal("tolower(substring(Name,2,1))", rawValues[2]); - } + [Fact] + public void GetOrderByRawValues_Works_MultipleOrderByClause() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType_With_Address().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderByOption = new OrderByQueryOption("Address/City asc,substring(Name,2,1),tolower(substring(Name,2,1))", context); + + // Act + List rawValues = orderByOption.GetOrderByRawValues(); + + // Assert + Assert.Equal(3, rawValues.Count); + Assert.Equal("Address/City", rawValues[0]); // Here, without 'asc' since it's omitted by default + Assert.Equal("substring(Name,2,1)", rawValues[1]); + Assert.Equal("tolower(substring(Name,2,1))", rawValues[2]); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SearchQueryOptionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SearchQueryOptionTests.cs index f8cc59036..50bd46c37 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SearchQueryOptionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SearchQueryOptionTests.cs @@ -20,167 +20,166 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class SearchQueryOptionTests { - public class SearchQueryOptionTests + private static IEdmModel _model = GetModel(); + + [Fact] + public void CtorSearchQueryOption_ThrowsArgumentNull_ForInputParameter() { - private static IEdmModel _model = GetModel(); + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new SearchQueryOption(null, null, null), "rawValue"); + ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new SearchQueryOption(string.Empty, null, null), "rawValue"); - [Fact] - public void CtorSearchQueryOption_ThrowsArgumentNull_ForInputParameter() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new SearchQueryOption(null, null, null), "rawValue"); - ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new SearchQueryOption(string.Empty, null, null), "rawValue"); + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new SearchQueryOption("any", null, null), "context"); - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new SearchQueryOption("any", null, null), "context"); + // Arrange & Act & Assert + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + ExceptionAssert.ThrowsArgumentNull(() => new SearchQueryOption("any", context, null), "queryOptionParser"); + } - // Arrange & Act & Assert - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - ExceptionAssert.ThrowsArgumentNull(() => new SearchQueryOption("any", context, null), "queryOptionParser"); - } + [Fact] + public void CtorSearchQueryOption_CanConstructValidSearchQuery() + { + // Arrange + ODataQueryContext context = new ODataQueryContext(_model, typeof(int)); - [Fact] - public void CtorSearchQueryOption_CanConstructValidSearchQuery() - { - // Arrange - ODataQueryContext context = new ODataQueryContext(_model, typeof(int)); + // Act + SearchQueryOption search = new SearchQueryOption("any", context); - // Act - SearchQueryOption search = new SearchQueryOption("any", context); + // Assert + Assert.Same(context, search.Context); + Assert.Equal("any", search.RawValue); + } - // Assert - Assert.Same(context, search.Context); - Assert.Equal("any", search.RawValue); - } + [Fact] + public void CtorSearchQueryOption_GetQueryNodeParsesQuery() + { + // Arrange + ODataQueryContext context = new ODataQueryContext(_model, typeof(int)) { RequestContainer = new MockServiceProvider() }; - [Fact] - public void CtorSearchQueryOption_GetQueryNodeParsesQuery() - { - // Arrange - ODataQueryContext context = new ODataQueryContext(_model, typeof(int)) { RequestContainer = new MockServiceProvider() }; + // Act + SearchQueryOption search = new SearchQueryOption("any", context); + SearchClause searchClause = search.SearchClause; - // Act - SearchQueryOption search = new SearchQueryOption("any", context); - SearchClause searchClause = search.SearchClause; + // Assert + Assert.NotNull(searchClause); - // Assert - Assert.NotNull(searchClause); + SearchTermNode searchTermNode = Assert.IsType(searchClause.Expression); + Assert.Equal(QueryNodeKind.SearchTerm, searchTermNode.Kind); + Assert.Equal("any", searchTermNode.Text); + } - SearchTermNode searchTermNode = Assert.IsType(searchClause.Expression); - Assert.Equal(QueryNodeKind.SearchTerm, searchTermNode.Kind); - Assert.Equal("any", searchTermNode.Text); - } + [Fact] + public void SearchQueryOption_ApplyTo_Throws_Null_Query() + { + // Arrange + ODataQueryContext context = new ODataQueryContext(_model, typeof(SearchProduct)) { RequestContainer = new MockServiceProvider() }; + SearchQueryOption search = new SearchQueryOption("any", context); - [Fact] - public void SearchQueryOption_ApplyTo_Throws_Null_Query() - { - // Arrange - ODataQueryContext context = new ODataQueryContext(_model, typeof(SearchProduct)) { RequestContainer = new MockServiceProvider() }; - SearchQueryOption search = new SearchQueryOption("any", context); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => search.ApplyTo(null, new ODataQuerySettings()), "query"); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => search.ApplyTo(null, new ODataQuerySettings()), "query"); - } + [Fact] + public void SearchQueryOption_ApplyTo_Throws_Null_QuerySettings() + { + // Arrange + ODataQueryContext context = new ODataQueryContext(_model, typeof(SearchProduct)) { RequestContainer = new MockServiceProvider() }; + SearchQueryOption search = new SearchQueryOption("any", context); - [Fact] - public void SearchQueryOption_ApplyTo_Throws_Null_QuerySettings() - { - // Arrange - ODataQueryContext context = new ODataQueryContext(_model, typeof(SearchProduct)) { RequestContainer = new MockServiceProvider() }; - SearchQueryOption search = new SearchQueryOption("any", context); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => search.ApplyTo(new SearchProduct[0].AsQueryable(), null), "querySettings"); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => search.ApplyTo(new SearchProduct[0].AsQueryable(), null), "querySettings"); - } + [Fact] + public void SearchQueryOption_ApplyTo_ReturnsOrigialQuery_WithoutISearchBinder() + { + // Arrange + ODataQueryContext context = new ODataQueryContext(_model, typeof(SearchProduct)) { RequestContainer = new MockServiceProvider() }; - [Fact] - public void SearchQueryOption_ApplyTo_ReturnsOrigialQuery_WithoutISearchBinder() + SearchQueryOption search = new SearchQueryOption("food", context); + + var searchProducts = (new List { - // Arrange - ODataQueryContext context = new ODataQueryContext(_model, typeof(SearchProduct)) { RequestContainer = new MockServiceProvider() }; + new SearchProduct { Id = 1, Category = "food" }, + new SearchProduct { Id = 2, Category = "non-food" }, + new SearchProduct { Id = 3, Category = "food" } + }).AsQueryable(); - SearchQueryOption search = new SearchQueryOption("food", context); + // Act + var results = search.ApplyTo(searchProducts, new ODataQuerySettings()).Cast(); - var searchProducts = (new List - { - new SearchProduct { Id = 1, Category = "food" }, - new SearchProduct { Id = 2, Category = "non-food" }, - new SearchProduct { Id = 3, Category = "food" } - }).AsQueryable(); + // Arrange + Assert.Equal(3, results.Count()); + } - // Act - var results = search.ApplyTo(searchProducts, new ODataQuerySettings()).Cast(); + [Fact] + public void SearchQueryOption_ApplyTo_ReturnsOrigialQuery_WithISearchBinder() + { + // Arrange + IServiceProvider serviceProvider = new MockServiceProvider( + a => a.AddSingleton()); - // Arrange - Assert.Equal(3, results.Count()); - } + ODataQueryContext context = new ODataQueryContext(_model, typeof(SearchProduct)) { RequestContainer = serviceProvider }; + + SearchQueryOption search = new SearchQueryOption("food", context); - [Fact] - public void SearchQueryOption_ApplyTo_ReturnsOrigialQuery_WithISearchBinder() + var searchProducts = (new List { - // Arrange - IServiceProvider serviceProvider = new MockServiceProvider( - a => a.AddSingleton()); + new SearchProduct { Id = 1, Category = "food" }, + new SearchProduct { Id = 2, Category = "non-food" }, + new SearchProduct { Id = 3, Category = "food" } + }).AsQueryable(); - ODataQueryContext context = new ODataQueryContext(_model, typeof(SearchProduct)) { RequestContainer = serviceProvider }; + // Act + var results = search.ApplyTo(searchProducts, new ODataQuerySettings()).Cast(); - SearchQueryOption search = new SearchQueryOption("food", context); + // Arrange + Assert.Equal(2, results.Count()); - var searchProducts = (new List + Assert.Collection(results, + e => { - new SearchProduct { Id = 1, Category = "food" }, - new SearchProduct { Id = 2, Category = "non-food" }, - new SearchProduct { Id = 3, Category = "food" } - }).AsQueryable(); - - // Act - var results = search.ApplyTo(searchProducts, new ODataQuerySettings()).Cast(); - - // Arrange - Assert.Equal(2, results.Count()); - - Assert.Collection(results, - e => - { - Assert.Equal(1, e.Id); - Assert.Equal("food", e.Category); - }, - e => - { - Assert.Equal(3, e.Id); - Assert.Equal("food", e.Category); - }); - } - - private static IEdmModel GetModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Products"); - return builder.GetEdmModel(); - } + Assert.Equal(1, e.Id); + Assert.Equal("food", e.Category); + }, + e => + { + Assert.Equal(3, e.Id); + Assert.Equal("food", e.Category); + }); } - public class SearchProduct + private static IEdmModel GetModel() { - public int Id { get; set; } - - public string Category { get; set; } + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Products"); + return builder.GetEdmModel(); } +} + +public class SearchProduct +{ + public int Id { get; set; } + + public string Category { get; set; } +} - public class TestSearchBinder : ISearchBinder +public class TestSearchBinder : ISearchBinder +{ + public Expression BindSearch(SearchClause searchClause, QueryBinderContext context) { - public Expression BindSearch(SearchClause searchClause, QueryBinderContext context) + SearchTermNode node = searchClause.Expression as SearchTermNode; + if (node != null) { - SearchTermNode node = searchClause.Expression as SearchTermNode; - if (node != null) - { - Expression> exp = p => p.Category == node.Text; - return exp; - } - - throw new NotImplementedException(); + Expression> exp = p => p.Category == node.Text; + return exp; } + + throw new NotImplementedException(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SelectExpandQueryOptionTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SelectExpandQueryOptionTest.cs index 5aae64a7e..93038ab52 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SelectExpandQueryOptionTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SelectExpandQueryOptionTest.cs @@ -23,775 +23,774 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class SelectExpandQueryOptionTest { - public class SelectExpandQueryOptionTest + private CustomersModelWithInheritance _model = new CustomersModelWithInheritance(); + + [Fact] + public void Ctor_ThrowsArgumentNull_Context() { - private CustomersModelWithInheritance _model = new CustomersModelWithInheritance(); + ExceptionAssert.ThrowsArgumentNull( + () => new SelectExpandQueryOption(select: "select", expand: "expand", context: null), + "context"); + } - [Fact] - public void Ctor_ThrowsArgumentNull_Context() - { - ExceptionAssert.ThrowsArgumentNull( - () => new SelectExpandQueryOption(select: "select", expand: "expand", context: null), - "context"); - } + [Theory] + [InlineData(null, null)] + [InlineData("", "")] + [InlineData(" ", " ")] + public void Ctor_ThrowsArgument_IfBothSelectAndExpandAreNullOrWhitespace(string select, string expand) + { + // Arrange + _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Customer))); + ODataQueryContext context = new ODataQueryContext(_model.Model, typeof(Customer)); + + // Act & Assert + ExceptionAssert.Throws( + () => new SelectExpandQueryOption(select, expand, context: context), + "'select' and 'expand' cannot be both null or empty."); + } - [Theory] - [InlineData(null, null)] - [InlineData("", "")] - [InlineData(" ", " ")] - public void Ctor_ThrowsArgument_IfBothSelectAndExpandAreNullOrWhitespace(string select, string expand) - { - // Arrange - _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Customer))); - ODataQueryContext context = new ODataQueryContext(_model.Model, typeof(Customer)); - - // Act & Assert - ExceptionAssert.Throws( - () => new SelectExpandQueryOption(select, expand, context: context), - "'select' and 'expand' cannot be both null or empty."); - } + [Fact] + public void Ctor_ThrowsArgument_IfContextIsNotForStructuredType() + { + // Arrange + ODataQueryContext context = new ODataQueryContext(_model.Model, typeof(int)); + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new SelectExpandQueryOption(select: "Name", expand: "Name", context: context), + "context", + "The type 'Edm.Int32' is not a structured type. Only structured types support $select and $expand."); + } - [Fact] - public void Ctor_ThrowsArgument_IfContextIsNotForStructuredType() - { - // Arrange - ODataQueryContext context = new ODataQueryContext(_model.Model, typeof(int)); - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => new SelectExpandQueryOption(select: "Name", expand: "Name", context: context), - "context", - "The type 'Edm.Int32' is not a structured type. Only structured types support $select and $expand."); - } + [Fact] + public void Ctor_ThrowsArgumentNull_QueryOptionParser() + { + // Arrange + ODataQueryContext context = new ODataQueryContext(_model.Model, typeof(int)); - [Fact] - public void Ctor_ThrowsArgumentNull_QueryOptionParser() - { - // Arrange - ODataQueryContext context = new ODataQueryContext(_model.Model, typeof(int)); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new SelectExpandQueryOption("select", "expand", context, queryOptionParser: null), + "queryOptionParser"); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new SelectExpandQueryOption("select", "expand", context, queryOptionParser: null), - "queryOptionParser"); - } + [Fact] + public void Ctor_SetsProperty_RawSelect() + { + // Arrange + string selectValue = "select"; + _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Customer))); + ODataQueryContext context = new ODataQueryContext(_model.Model, typeof(Customer)); + context.RequestContainer = new MockServiceProvider(); - [Fact] - public void Ctor_SetsProperty_RawSelect() - { - // Arrange - string selectValue = "select"; - _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Customer))); - ODataQueryContext context = new ODataQueryContext(_model.Model, typeof(Customer)); - context.RequestContainer = new MockServiceProvider(); + // Act + SelectExpandQueryOption result = new SelectExpandQueryOption(selectValue, expand: null, context: context); - // Act - SelectExpandQueryOption result = new SelectExpandQueryOption(selectValue, expand: null, context: context); + // Assert + Assert.Equal(selectValue, result.RawSelect); + } - // Assert - Assert.Equal(selectValue, result.RawSelect); - } + [Fact] + public void Ctor_SetsProperty_RawExpand() + { + // Arrange + string expandValue = "expand"; + _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Customer))); + ODataQueryContext context = new ODataQueryContext(_model.Model, typeof(Customer)); + context.RequestContainer = new MockServiceProvider(); - [Fact] - public void Ctor_SetsProperty_RawExpand() - { - // Arrange - string expandValue = "expand"; - _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Customer))); - ODataQueryContext context = new ODataQueryContext(_model.Model, typeof(Customer)); - context.RequestContainer = new MockServiceProvider(); + // Act + SelectExpandQueryOption result = new SelectExpandQueryOption(select: null, expand: expandValue, context: context); - // Act - SelectExpandQueryOption result = new SelectExpandQueryOption(select: null, expand: expandValue, context: context); + // Assert + Assert.Equal(expandValue, result.RawExpand); + } - // Assert - Assert.Equal(expandValue, result.RawExpand); - } + [Fact] + public void SelectExpandClause_Property_ParsesRawSelectAndRawExpand() + { + // Arrange + IEdmModel model = _model.Model; + _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Customer))); + ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); + context.RequestContainer = new MockServiceProvider(); + SelectExpandQueryOption option = new SelectExpandQueryOption("ID,Name,SimpleEnum,Orders", "Orders", context); + + // Act + SelectExpandClause selectExpandClause = option.SelectExpandClause; + + // Assert + Assert.NotEmpty(selectExpandClause.SelectedItems.OfType()); + Assert.NotEmpty(selectExpandClause.SelectedItems.OfType()); + } - [Fact] - public void SelectExpandClause_Property_ParsesRawSelectAndRawExpand() - { - // Arrange - IEdmModel model = _model.Model; - _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Customer))); - ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); - context.RequestContainer = new MockServiceProvider(); - SelectExpandQueryOption option = new SelectExpandQueryOption("ID,Name,SimpleEnum,Orders", "Orders", context); - - // Act - SelectExpandClause selectExpandClause = option.SelectExpandClause; - - // Assert - Assert.NotEmpty(selectExpandClause.SelectedItems.OfType()); - Assert.NotEmpty(selectExpandClause.SelectedItems.OfType()); - } + [Fact] + public void SelectExpandClause_Property_ParsesWithNavigationSource() + { + // Arrange + IEdmModel model = _model.Model; + _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Customer))); + ODataPath odataPath = new ODataPath(new EntitySetSegment(_model.Customers)); + ODataQueryContext context = new ODataQueryContext(model, typeof(Customer), odataPath); + context.RequestContainer = new MockServiceProvider(); + SelectExpandQueryOption option = new SelectExpandQueryOption("ID,Name,SimpleEnum,Orders", "Orders", context); + + // Act + SelectExpandClause selectExpandClause = option.SelectExpandClause; + + // Assert + Assert.NotEmpty(selectExpandClause.SelectedItems.OfType()); + Assert.NotEmpty(selectExpandClause.SelectedItems.OfType()); + } - [Fact] - public void SelectExpandClause_Property_ParsesWithNavigationSource() - { - // Arrange - IEdmModel model = _model.Model; - _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Customer))); - ODataPath odataPath = new ODataPath(new EntitySetSegment(_model.Customers)); - ODataQueryContext context = new ODataQueryContext(model, typeof(Customer), odataPath); - context.RequestContainer = new MockServiceProvider(); - SelectExpandQueryOption option = new SelectExpandQueryOption("ID,Name,SimpleEnum,Orders", "Orders", context); - - // Act - SelectExpandClause selectExpandClause = option.SelectExpandClause; - - // Assert - Assert.NotEmpty(selectExpandClause.SelectedItems.OfType()); - Assert.NotEmpty(selectExpandClause.SelectedItems.OfType()); - } + [Theory] + [InlineData("ID,Name,SimpleEnum,Orders", "Orders")] + [InlineData("iD,NaMe,SiMpLeEnUm,OrDeRs", "OrDeRs")] + public void SelectExpandClause_Property_ParsesWithEdmTypeAndNavigationSource(string select, string expand) + { + // Arrange + IEdmModel model = _model.Model; + _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Formatter.Models.Customer))); + ODataPath odataPath = new ODataPath(new EntitySetSegment(_model.Customers)); + ODataQueryContext context = new ODataQueryContext(model, _model.Customer, odataPath); + context.RequestContainer = new MockServiceProvider(); + SelectExpandQueryOption option = new SelectExpandQueryOption(select, expand, context); + + // Act + SelectExpandClause selectExpandClause = option.SelectExpandClause; + + // Assert + Assert.NotEmpty(selectExpandClause.SelectedItems.OfType()); + IEnumerable ids = selectExpandClause.SelectedItems.OfType() + .Select(p => (p.SelectedPath.FirstSegment as PropertySegment)?.Identifier); + Assert.Equal(new []{"ID", "Name", "SimpleEnum"}, ids.OfType().ToArray()); + + IEnumerable expands = selectExpandClause.SelectedItems.OfType(); + Assert.NotEmpty(expands); + Assert.Equal("Orders", expands.Single().NavigationSource.Name); + } - [Theory] - [InlineData("ID,Name,SimpleEnum,Orders", "Orders")] - [InlineData("iD,NaMe,SiMpLeEnUm,OrDeRs", "OrDeRs")] - public void SelectExpandClause_Property_ParsesWithEdmTypeAndNavigationSource(string select, string expand) - { - // Arrange - IEdmModel model = _model.Model; - _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Formatter.Models.Customer))); - ODataPath odataPath = new ODataPath(new EntitySetSegment(_model.Customers)); - ODataQueryContext context = new ODataQueryContext(model, _model.Customer, odataPath); - context.RequestContainer = new MockServiceProvider(); - SelectExpandQueryOption option = new SelectExpandQueryOption(select, expand, context); - - // Act - SelectExpandClause selectExpandClause = option.SelectExpandClause; - - // Assert - Assert.NotEmpty(selectExpandClause.SelectedItems.OfType()); - IEnumerable ids = selectExpandClause.SelectedItems.OfType() - .Select(p => (p.SelectedPath.FirstSegment as PropertySegment)?.Identifier); - Assert.Equal(new []{"ID", "Name", "SimpleEnum"}, ids.OfType().ToArray()); - - IEnumerable expands = selectExpandClause.SelectedItems.OfType(); - Assert.NotEmpty(expands); - Assert.Equal("Orders", expands.Single().NavigationSource.Name); - } + //[Theory] + //[InlineData("ID", null)] + //[InlineData("LastName,FirstName", null)] + //[InlineData("LastName, FirstName", null)] + //[InlineData("LastName,FirstName", "Orders")] + //[InlineData("LastName,FirstName,Orders", "Orders")] + //[InlineData("Orders", "Orders,Orders($expand=Customer),Orders($expand=Customer($expand=Orders))")] + //[InlineData("SimpleEnum", "Orders")] + //public void SelectExpandClause_CanParse_ModelBuiltForQueryable(string select, string expand) + //{ + // // Arrange + // var config = RoutingConfigurationFactory.Create(); + // ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(config, isQueryCompositionMode: true); + // builder.EntityType(); + // IEdmModel model = builder.GetEdmModel(); + + // ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); + // context.RequestContainer = new MockServiceProvider(); + // SelectExpandQueryOption option = new SelectExpandQueryOption(select, expand, context); + + // // Act & Assert + // ExceptionAssert.DoesNotThrow(() => option.SelectExpandClause.ToString()); + //} + + [Theory] + [InlineData("IDD", null, "Could not find a property named 'IDD' on type 'NS.Customer'.")] + [InlineData("ID, Namee", null, "Could not find a property named 'Namee' on type 'NS.Customer'.")] + [InlineData("NS+Name", null, "Syntax error: character '+' is not valid at position 2 in 'NS+Name'.")] + [InlineData("NS.Customerrr/SpecialCustomerProperty", null, "The type 'NS.Customerrr' is not defined in the model.")] + public void SelectExpandCaluse_ThrowsODataException_InvalidQuery(string select, string expand, string error) + { + // Arrange + IEdmModel model = _model.Model; + _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Customer))); + ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); + context.RequestContainer = new MockServiceProvider(); + SelectExpandQueryOption option = new SelectExpandQueryOption(select, expand, context); + + // Act + ExceptionAssert.Throws( + () => option.SelectExpandClause.ToString(), + error); + } - //[Theory] - //[InlineData("ID", null)] - //[InlineData("LastName,FirstName", null)] - //[InlineData("LastName, FirstName", null)] - //[InlineData("LastName,FirstName", "Orders")] - //[InlineData("LastName,FirstName,Orders", "Orders")] - //[InlineData("Orders", "Orders,Orders($expand=Customer),Orders($expand=Customer($expand=Orders))")] - //[InlineData("SimpleEnum", "Orders")] - //public void SelectExpandClause_CanParse_ModelBuiltForQueryable(string select, string expand) - //{ - // // Arrange - // var config = RoutingConfigurationFactory.Create(); - // ODataConventionModelBuilder builder = ODataConventionModelBuilderFactory.Create(config, isQueryCompositionMode: true); - // builder.EntityType(); - // IEdmModel model = builder.GetEdmModel(); - - // ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); - // context.RequestContainer = new MockServiceProvider(); - // SelectExpandQueryOption option = new SelectExpandQueryOption(select, expand, context); - - // // Act & Assert - // ExceptionAssert.DoesNotThrow(() => option.SelectExpandClause.ToString()); - //} - - [Theory] - [InlineData("IDD", null, "Could not find a property named 'IDD' on type 'NS.Customer'.")] - [InlineData("ID, Namee", null, "Could not find a property named 'Namee' on type 'NS.Customer'.")] - [InlineData("NS+Name", null, "Syntax error: character '+' is not valid at position 2 in 'NS+Name'.")] - [InlineData("NS.Customerrr/SpecialCustomerProperty", null, "The type 'NS.Customerrr' is not defined in the model.")] - public void SelectExpandCaluse_ThrowsODataException_InvalidQuery(string select, string expand, string error) - { - // Arrange - IEdmModel model = _model.Model; - _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(Customer))); - ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); - context.RequestContainer = new MockServiceProvider(); - SelectExpandQueryOption option = new SelectExpandQueryOption(select, expand, context); - - // Act - ExceptionAssert.Throws( - () => option.SelectExpandClause.ToString(), - error); - } + [Fact] + public void Property_SelectExpandClause_WorksWithUnTypedContext() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + context.RequestContainer = new MockServiceProvider(); + SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select: "ID", expand: null, context: context); + + // Act & Assert + Assert.NotNull(selectExpand.SelectExpandClause); + } - [Fact] - public void Property_SelectExpandClause_WorksWithUnTypedContext() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - context.RequestContainer = new MockServiceProvider(); - SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select: "ID", expand: null, context: context); - - // Act & Assert - Assert.NotNull(selectExpand.SelectExpandClause); - } + [Fact] + public void ApplyTo_OnQueryable_WithUnTypedContext_Throws_InvalidOperation() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + context.RequestContainer = new MockServiceProvider(); + SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select: "ID", expand: null, context: context); + IQueryable queryable = new Mock().Object; + + // Act & Assert + ExceptionAssert.Throws(() => selectExpand.ApplyTo(queryable, new ODataQuerySettings()), + "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); + } - [Fact] - public void ApplyTo_OnQueryable_WithUnTypedContext_Throws_InvalidOperation() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - context.RequestContainer = new MockServiceProvider(); - SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select: "ID", expand: null, context: context); - IQueryable queryable = new Mock().Object; - - // Act & Assert - ExceptionAssert.Throws(() => selectExpand.ApplyTo(queryable, new ODataQuerySettings()), - "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); - } + [Fact] + public void ApplyTo_OnSingleEntity_WithUnTypedContext_Throws_InvalidOperation() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + context.RequestContainer = new MockServiceProvider(); + SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select: "ID", expand: null, context: context); + object entity = new object(); + + // Act & Assert + ExceptionAssert.Throws(() => selectExpand.ApplyTo(entity, new ODataQuerySettings()), + "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); + } - [Fact] - public void ApplyTo_OnSingleEntity_WithUnTypedContext_Throws_InvalidOperation() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ProcessLevelsCorrectly_PreserveOtherOptions(bool autoSelect) + { + // Arrange + var model = ODataLevelsTest.GetEdmModel(); + var entityType = model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.LevelsEntity"); + var context = new ODataQueryContext( + model, + entityType); + + if (autoSelect) { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - context.RequestContainer = new MockServiceProvider(); - SelectExpandQueryOption selectExpand = new SelectExpandQueryOption(select: "ID", expand: null, context: context); - object entity = new object(); - - // Act & Assert - ExceptionAssert.Throws(() => selectExpand.ApplyTo(entity, new ODataQuerySettings()), - "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); + var modelBound = model.GetAnnotationValue(entityType) ?? new ModelBoundQuerySettings(); + modelBound.DefaultSelectType = SelectExpandType.Automatic; + model.SetAnnotationValue(entityType, modelBound); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ProcessLevelsCorrectly_PreserveOtherOptions(bool autoSelect) - { - // Arrange - var model = ODataLevelsTest.GetEdmModel(); - var entityType = model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.LevelsEntity"); - var context = new ODataQueryContext( - model, - entityType); - - if (autoSelect) - { - var modelBound = model.GetAnnotationValue(entityType) ?? new ModelBoundQuerySettings(); - modelBound.DefaultSelectType = SelectExpandType.Automatic; - model.SetAnnotationValue(entityType, modelBound); - } - - context.RequestContainer = new MockServiceProvider(); - var selectExpand = new SelectExpandQueryOption( - select: null, - expand: "Parent($filter=Cnt gt 1;$apply=aggregate($count as Cnt))", - context: context); - selectExpand.LevelsMaxLiteralExpansionDepth = 1; - - // Act - SelectExpandClause clause = selectExpand.ProcessLevels(); - - // Assert - var item = Assert.IsType(clause.SelectedItems.Single()); - Assert.NotNull(item.FilterOption); - Assert.NotNull(item.ApplyOption); - } + context.RequestContainer = new MockServiceProvider(); + var selectExpand = new SelectExpandQueryOption( + select: null, + expand: "Parent($filter=Cnt gt 1;$apply=aggregate($count as Cnt))", + context: context); + selectExpand.LevelsMaxLiteralExpansionDepth = 1; - [Fact] - public void ProcessLevelsCorrectly_AllSelected() - { - // Arrange - var model = ODataLevelsTest.GetEdmModel(); - var context = new ODataQueryContext( - model, - model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.LevelsEntity")); - context.RequestContainer = new MockServiceProvider(); - var selectExpand = new SelectExpandQueryOption( - select: null, - expand: "Parent($expand=Parent($levels=2))", - context: context); - selectExpand.LevelsMaxLiteralExpansionDepth = 3; - - // Act - SelectExpandClause clause = selectExpand.ProcessLevels(); - - // Assert - // Level 1. - Assert.True(clause.AllSelected); - Assert.Single(clause.SelectedItems); - - var item = Assert.IsType(clause.SelectedItems.Single()); - Assert.Equal( - "Parent", - ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); - Assert.Null(item.LevelsOption); - - // Level 2. - clause = item.SelectAndExpand; - Assert.True(clause.AllSelected); - Assert.Single(clause.SelectedItems); - - item = Assert.IsType(clause.SelectedItems.Single()); - Assert.Equal( - "Parent", - ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); - Assert.Null(item.LevelsOption); - - // Level 3. - clause = item.SelectAndExpand; - Assert.True(clause.AllSelected); - Assert.Single(clause.SelectedItems); - - item = Assert.IsType(clause.SelectedItems.Single()); - Assert.Equal( - "Parent", - ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); - Assert.Null(item.LevelsOption); - - clause = item.SelectAndExpand; - Assert.True(clause.AllSelected); - Assert.Empty(clause.SelectedItems); - } + // Act + SelectExpandClause clause = selectExpand.ProcessLevels(); - [Fact] - public void ProcessLevelsCorrectly_NotAllSelected() - { - // Arrange - var model = ODataLevelsTest.GetEdmModel(); - var context = new ODataQueryContext( - model, - model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.LevelsEntity")); - context.RequestContainer = new MockServiceProvider(); - var selectExpand = new SelectExpandQueryOption( - select: "Name", - expand: "Parent($select=ID;$levels=max)", - context: context); - - // Act - SelectExpandClause clause = selectExpand.ProcessLevels(); - - // Assert - // Level 1. - Assert.False(clause.AllSelected); - Assert.Equal(2, clause.SelectedItems.Count()); - - var nameSelectItem = Assert.Single(clause.SelectedItems.OfType().Where( - item => item.SelectedPath.FirstSegment is PropertySegment)); - Assert.Equal("Name", ((PropertySegment)nameSelectItem.SelectedPath.FirstSegment).Property.Name); - - // Before ODL 7.6, the expand navigation property will be added as a select item (PathSelectItem). - // After ODL 7.6 (include 7.6), the expand navigation property will not be added. - // Comment the following codes for visibility later. - /* - var parentSelectItem = Assert.Single(clause.SelectedItems.OfType().Where( - item => item.SelectedPath.FirstSegment is NavigationPropertySegment)); - Assert.Equal( - "Parent", - ((NavigationPropertySegment)parentSelectItem.SelectedPath.FirstSegment).NavigationProperty.Name); - */ - Assert.Empty(clause.SelectedItems.OfType().Where(item => item.SelectedPath.FirstSegment is NavigationPropertySegment)); - - var expandedItem = Assert.Single(clause.SelectedItems.OfType()); - Assert.Equal( - "Parent", - ((NavigationPropertySegment)expandedItem.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); - Assert.Null(expandedItem.LevelsOption); - - // Level 2. - clause = expandedItem.SelectAndExpand; - Assert.False(clause.AllSelected); - Assert.Equal(3, clause.SelectedItems.Count()); - - var idSelectItem = Assert.Single(clause.SelectedItems.OfType().Where( - item => item.SelectedPath.FirstSegment is PropertySegment)); - Assert.Equal("ID", ((PropertySegment)idSelectItem.SelectedPath.FirstSegment).Property.Name); - - var parentSelectItem = Assert.Single(clause.SelectedItems.OfType().Where( - item => item.SelectedPath.FirstSegment is NavigationPropertySegment)); - Assert.Equal( - "Parent", - ((NavigationPropertySegment)parentSelectItem.SelectedPath.FirstSegment).NavigationProperty.Name); - - expandedItem = Assert.Single(clause.SelectedItems.OfType()); - Assert.Equal( - "Parent", - ((NavigationPropertySegment)expandedItem.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); - Assert.Null(expandedItem.LevelsOption); - - clause = expandedItem.SelectAndExpand; - Assert.False(clause.AllSelected); - Assert.Single(clause.SelectedItems); - - idSelectItem = Assert.IsType(clause.SelectedItems.Single()); - Assert.Equal("ID", ((PropertySegment)idSelectItem.SelectedPath.FirstSegment).Property.Name); - } + // Assert + var item = Assert.IsType(clause.SelectedItems.Single()); + Assert.NotNull(item.FilterOption); + Assert.NotNull(item.ApplyOption); + } - [Fact] - public void ProcessLevelsCorrectly_WithMultipleProperties() - { - // Arrange - var model = ODataLevelsTest.GetEdmModel(); - var context = new ODataQueryContext( - model, - model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.LevelsEntity")); - context.RequestContainer = new MockServiceProvider(); - var selectExpand = new SelectExpandQueryOption( - select: null, - expand: "Parent($expand=Parent($levels=max),DerivedAncestors($levels=2;$select=ID)),BaseEntities($levels=2)", - context: context); - selectExpand.LevelsMaxLiteralExpansionDepth = 3; - - // Act - SelectExpandClause clause = selectExpand.ProcessLevels(); - - // Assert - Assert.True(clause.AllSelected); - Assert.Equal(2, clause.SelectedItems.Count()); - - // Top level Parent. - var parent = Assert.Single(clause.SelectedItems.OfType().Where( - item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && - ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "Parent")); - Assert.Null(parent.LevelsOption); + [Fact] + public void ProcessLevelsCorrectly_AllSelected() + { + // Arrange + var model = ODataLevelsTest.GetEdmModel(); + var context = new ODataQueryContext( + model, + model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.LevelsEntity")); + context.RequestContainer = new MockServiceProvider(); + var selectExpand = new SelectExpandQueryOption( + select: null, + expand: "Parent($expand=Parent($levels=2))", + context: context); + selectExpand.LevelsMaxLiteralExpansionDepth = 3; + + // Act + SelectExpandClause clause = selectExpand.ProcessLevels(); + + // Assert + // Level 1. + Assert.True(clause.AllSelected); + Assert.Single(clause.SelectedItems); + + var item = Assert.IsType(clause.SelectedItems.Single()); + Assert.Equal( + "Parent", + ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); + Assert.Null(item.LevelsOption); + + // Level 2. + clause = item.SelectAndExpand; + Assert.True(clause.AllSelected); + Assert.Single(clause.SelectedItems); + + item = Assert.IsType(clause.SelectedItems.Single()); + Assert.Equal( + "Parent", + ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); + Assert.Null(item.LevelsOption); + + // Level 3. + clause = item.SelectAndExpand; + Assert.True(clause.AllSelected); + Assert.Single(clause.SelectedItems); + + item = Assert.IsType(clause.SelectedItems.Single()); + Assert.Equal( + "Parent", + ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); + Assert.Null(item.LevelsOption); + + clause = item.SelectAndExpand; + Assert.True(clause.AllSelected); + Assert.Empty(clause.SelectedItems); + } - var clauseOfParent = parent.SelectAndExpand; - Assert.True(clauseOfParent.AllSelected); - Assert.Equal(2, clauseOfParent.SelectedItems.Count()); + [Fact] + public void ProcessLevelsCorrectly_NotAllSelected() + { + // Arrange + var model = ODataLevelsTest.GetEdmModel(); + var context = new ODataQueryContext( + model, + model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.LevelsEntity")); + context.RequestContainer = new MockServiceProvider(); + var selectExpand = new SelectExpandQueryOption( + select: "Name", + expand: "Parent($select=ID;$levels=max)", + context: context); + + // Act + SelectExpandClause clause = selectExpand.ProcessLevels(); + + // Assert + // Level 1. + Assert.False(clause.AllSelected); + Assert.Equal(2, clause.SelectedItems.Count()); + + var nameSelectItem = Assert.Single(clause.SelectedItems.OfType().Where( + item => item.SelectedPath.FirstSegment is PropertySegment)); + Assert.Equal("Name", ((PropertySegment)nameSelectItem.SelectedPath.FirstSegment).Property.Name); + + // Before ODL 7.6, the expand navigation property will be added as a select item (PathSelectItem). + // After ODL 7.6 (include 7.6), the expand navigation property will not be added. + // Comment the following codes for visibility later. + /* + var parentSelectItem = Assert.Single(clause.SelectedItems.OfType().Where( + item => item.SelectedPath.FirstSegment is NavigationPropertySegment)); + Assert.Equal( + "Parent", + ((NavigationPropertySegment)parentSelectItem.SelectedPath.FirstSegment).NavigationProperty.Name); + */ + Assert.Empty(clause.SelectedItems.OfType().Where(item => item.SelectedPath.FirstSegment is NavigationPropertySegment)); + + var expandedItem = Assert.Single(clause.SelectedItems.OfType()); + Assert.Equal( + "Parent", + ((NavigationPropertySegment)expandedItem.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); + Assert.Null(expandedItem.LevelsOption); + + // Level 2. + clause = expandedItem.SelectAndExpand; + Assert.False(clause.AllSelected); + Assert.Equal(3, clause.SelectedItems.Count()); + + var idSelectItem = Assert.Single(clause.SelectedItems.OfType().Where( + item => item.SelectedPath.FirstSegment is PropertySegment)); + Assert.Equal("ID", ((PropertySegment)idSelectItem.SelectedPath.FirstSegment).Property.Name); + + var parentSelectItem = Assert.Single(clause.SelectedItems.OfType().Where( + item => item.SelectedPath.FirstSegment is NavigationPropertySegment)); + Assert.Equal( + "Parent", + ((NavigationPropertySegment)parentSelectItem.SelectedPath.FirstSegment).NavigationProperty.Name); + + expandedItem = Assert.Single(clause.SelectedItems.OfType()); + Assert.Equal( + "Parent", + ((NavigationPropertySegment)expandedItem.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); + Assert.Null(expandedItem.LevelsOption); + + clause = expandedItem.SelectAndExpand; + Assert.False(clause.AllSelected); + Assert.Single(clause.SelectedItems); + + idSelectItem = Assert.IsType(clause.SelectedItems.Single()); + Assert.Equal("ID", ((PropertySegment)idSelectItem.SelectedPath.FirstSegment).Property.Name); + } - // Level 1 of inline Parent. - var inlineParent = Assert.Single(clauseOfParent.SelectedItems.OfType().Where( - item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && - ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "Parent")); - Assert.Null(inlineParent.LevelsOption); - - // Level 2 of inline Parent. - var inlineParentClause = inlineParent.SelectAndExpand; - Assert.True(inlineParentClause.AllSelected); - Assert.Single(inlineParentClause.SelectedItems); - - inlineParent = Assert.IsType(inlineParentClause.SelectedItems.Single()); - Assert.Equal( - "Parent", - ((NavigationPropertySegment)inlineParent.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); - Assert.Null(inlineParent.LevelsOption); - - inlineParentClause = inlineParent.SelectAndExpand; - Assert.True(inlineParentClause.AllSelected); - Assert.Empty(inlineParentClause.SelectedItems); - - // Level 1 of inline DerivedAncestors. - var inlineDerivedAncestors = Assert.Single(clauseOfParent.SelectedItems.OfType().Where( - item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && - ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "DerivedAncestors")); - Assert.Null(inlineDerivedAncestors.LevelsOption); - - // Level 2 of inline DerivedAncestors. - var inlineDerivedAncestorsClause = inlineDerivedAncestors.SelectAndExpand; - Assert.False(inlineDerivedAncestorsClause.AllSelected); - Assert.Equal(3, inlineDerivedAncestorsClause.SelectedItems.Count()); - - var idItem = Assert.Single(inlineDerivedAncestorsClause.SelectedItems.OfType().Where( - item => item.SelectedPath.FirstSegment is PropertySegment)); - Assert.Equal("ID", ((PropertySegment)idItem.SelectedPath.FirstSegment).Property.Name); - - var derivedAncestorsItem = Assert.Single(inlineDerivedAncestorsClause.SelectedItems.OfType().Where( - item => item.SelectedPath.FirstSegment is NavigationPropertySegment)); - Assert.Equal( - "DerivedAncestors", - ((NavigationPropertySegment)derivedAncestorsItem.SelectedPath.FirstSegment).NavigationProperty.Name); - - inlineDerivedAncestors = Assert.Single(inlineDerivedAncestorsClause.SelectedItems.OfType()); - Assert.Equal( - "DerivedAncestors", - ((NavigationPropertySegment)inlineDerivedAncestors.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); - Assert.Null(inlineDerivedAncestors.LevelsOption); - - inlineDerivedAncestorsClause = inlineDerivedAncestors.SelectAndExpand; - Assert.False(inlineDerivedAncestorsClause.AllSelected); - Assert.Single(inlineDerivedAncestorsClause.SelectedItems); - - idItem = Assert.Single(inlineDerivedAncestorsClause.SelectedItems.OfType()); - Assert.Equal("ID", ((PropertySegment)idItem.SelectedPath.FirstSegment).Property.Name); - - // Level 1 of BaseEntities. - var baseEntities = Assert.Single(clause.SelectedItems.OfType().Where( - item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && - ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "BaseEntities")); - Assert.Null(baseEntities.LevelsOption); - - // Level 2 of BaseEntities. - var baseEntitiesClause = baseEntities.SelectAndExpand; - Assert.True(baseEntitiesClause.AllSelected); - Assert.Single(baseEntitiesClause.SelectedItems); - - baseEntities = Assert.IsType(baseEntitiesClause.SelectedItems.Single()); - Assert.Equal( - "BaseEntities", - ((NavigationPropertySegment)baseEntities.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); - Assert.Null(baseEntities.LevelsOption); - - baseEntitiesClause = baseEntities.SelectAndExpand; - Assert.True(baseEntitiesClause.AllSelected); - Assert.Empty(baseEntitiesClause.SelectedItems); - } + [Fact] + public void ProcessLevelsCorrectly_WithMultipleProperties() + { + // Arrange + var model = ODataLevelsTest.GetEdmModel(); + var context = new ODataQueryContext( + model, + model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.LevelsEntity")); + context.RequestContainer = new MockServiceProvider(); + var selectExpand = new SelectExpandQueryOption( + select: null, + expand: "Parent($expand=Parent($levels=max),DerivedAncestors($levels=2;$select=ID)),BaseEntities($levels=2)", + context: context); + selectExpand.LevelsMaxLiteralExpansionDepth = 3; + + // Act + SelectExpandClause clause = selectExpand.ProcessLevels(); + + // Assert + Assert.True(clause.AllSelected); + Assert.Equal(2, clause.SelectedItems.Count()); + + // Top level Parent. + var parent = Assert.Single(clause.SelectedItems.OfType().Where( + item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && + ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "Parent")); + Assert.Null(parent.LevelsOption); + + var clauseOfParent = parent.SelectAndExpand; + Assert.True(clauseOfParent.AllSelected); + Assert.Equal(2, clauseOfParent.SelectedItems.Count()); + + // Level 1 of inline Parent. + var inlineParent = Assert.Single(clauseOfParent.SelectedItems.OfType().Where( + item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && + ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "Parent")); + Assert.Null(inlineParent.LevelsOption); + + // Level 2 of inline Parent. + var inlineParentClause = inlineParent.SelectAndExpand; + Assert.True(inlineParentClause.AllSelected); + Assert.Single(inlineParentClause.SelectedItems); + + inlineParent = Assert.IsType(inlineParentClause.SelectedItems.Single()); + Assert.Equal( + "Parent", + ((NavigationPropertySegment)inlineParent.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); + Assert.Null(inlineParent.LevelsOption); + + inlineParentClause = inlineParent.SelectAndExpand; + Assert.True(inlineParentClause.AllSelected); + Assert.Empty(inlineParentClause.SelectedItems); + + // Level 1 of inline DerivedAncestors. + var inlineDerivedAncestors = Assert.Single(clauseOfParent.SelectedItems.OfType().Where( + item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && + ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "DerivedAncestors")); + Assert.Null(inlineDerivedAncestors.LevelsOption); + + // Level 2 of inline DerivedAncestors. + var inlineDerivedAncestorsClause = inlineDerivedAncestors.SelectAndExpand; + Assert.False(inlineDerivedAncestorsClause.AllSelected); + Assert.Equal(3, inlineDerivedAncestorsClause.SelectedItems.Count()); + + var idItem = Assert.Single(inlineDerivedAncestorsClause.SelectedItems.OfType().Where( + item => item.SelectedPath.FirstSegment is PropertySegment)); + Assert.Equal("ID", ((PropertySegment)idItem.SelectedPath.FirstSegment).Property.Name); + + var derivedAncestorsItem = Assert.Single(inlineDerivedAncestorsClause.SelectedItems.OfType().Where( + item => item.SelectedPath.FirstSegment is NavigationPropertySegment)); + Assert.Equal( + "DerivedAncestors", + ((NavigationPropertySegment)derivedAncestorsItem.SelectedPath.FirstSegment).NavigationProperty.Name); + + inlineDerivedAncestors = Assert.Single(inlineDerivedAncestorsClause.SelectedItems.OfType()); + Assert.Equal( + "DerivedAncestors", + ((NavigationPropertySegment)inlineDerivedAncestors.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); + Assert.Null(inlineDerivedAncestors.LevelsOption); + + inlineDerivedAncestorsClause = inlineDerivedAncestors.SelectAndExpand; + Assert.False(inlineDerivedAncestorsClause.AllSelected); + Assert.Single(inlineDerivedAncestorsClause.SelectedItems); + + idItem = Assert.Single(inlineDerivedAncestorsClause.SelectedItems.OfType()); + Assert.Equal("ID", ((PropertySegment)idItem.SelectedPath.FirstSegment).Property.Name); + + // Level 1 of BaseEntities. + var baseEntities = Assert.Single(clause.SelectedItems.OfType().Where( + item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && + ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "BaseEntities")); + Assert.Null(baseEntities.LevelsOption); + + // Level 2 of BaseEntities. + var baseEntitiesClause = baseEntities.SelectAndExpand; + Assert.True(baseEntitiesClause.AllSelected); + Assert.Single(baseEntitiesClause.SelectedItems); + + baseEntities = Assert.IsType(baseEntitiesClause.SelectedItems.Single()); + Assert.Equal( + "BaseEntities", + ((NavigationPropertySegment)baseEntities.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); + Assert.Null(baseEntities.LevelsOption); + + baseEntitiesClause = baseEntities.SelectAndExpand; + Assert.True(baseEntitiesClause.AllSelected); + Assert.Empty(baseEntitiesClause.SelectedItems); + } - [Fact] - public void ProcessLevelsCorrectly_WithNestedLevels() - { - // Arrange - var model = ODataLevelsTest.GetEdmModel(); - var context = new ODataQueryContext( - model, - model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.LevelsEntity")); - context.RequestContainer = new MockServiceProvider(); - var selectExpand = new SelectExpandQueryOption( - select: null, - expand: "Parent($expand=DerivedAncestors($levels=2);$levels=max)", - context: context); - selectExpand.LevelsMaxLiteralExpansionDepth = 4; - - // Act - SelectExpandClause clause = selectExpand.ProcessLevels(); - - // Assert - Assert.True(clause.AllSelected); - Assert.Single(clause.SelectedItems); - - // Level 1 of Parent. - var parent = Assert.IsType(clause.SelectedItems.Single()); - Assert.Equal( - "Parent", - ((NavigationPropertySegment)parent.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); - Assert.Null(parent.LevelsOption); - - var clauseOfParent = parent.SelectAndExpand; - Assert.True(clauseOfParent.AllSelected); - Assert.Equal(2, clauseOfParent.SelectedItems.Count()); - - // Level 1 of DerivedAncestors. - var derivedAncestors = Assert.Single(clauseOfParent.SelectedItems.OfType().Where( + [Fact] + public void ProcessLevelsCorrectly_WithNestedLevels() + { + // Arrange + var model = ODataLevelsTest.GetEdmModel(); + var context = new ODataQueryContext( + model, + model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.LevelsEntity")); + context.RequestContainer = new MockServiceProvider(); + var selectExpand = new SelectExpandQueryOption( + select: null, + expand: "Parent($expand=DerivedAncestors($levels=2);$levels=max)", + context: context); + selectExpand.LevelsMaxLiteralExpansionDepth = 4; + + // Act + SelectExpandClause clause = selectExpand.ProcessLevels(); + + // Assert + Assert.True(clause.AllSelected); + Assert.Single(clause.SelectedItems); + + // Level 1 of Parent. + var parent = Assert.IsType(clause.SelectedItems.Single()); + Assert.Equal( + "Parent", + ((NavigationPropertySegment)parent.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); + Assert.Null(parent.LevelsOption); + + var clauseOfParent = parent.SelectAndExpand; + Assert.True(clauseOfParent.AllSelected); + Assert.Equal(2, clauseOfParent.SelectedItems.Count()); + + // Level 1 of DerivedAncestors. + var derivedAncestors = Assert.Single(clauseOfParent.SelectedItems.OfType().Where( + item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && + ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "DerivedAncestors")); + Assert.Null(derivedAncestors.LevelsOption); + + var clauseOfDerivedAncestors = derivedAncestors.SelectAndExpand; + Assert.True(clauseOfDerivedAncestors.AllSelected); + Assert.Single(clauseOfDerivedAncestors.SelectedItems); + + // Level 2 of DerivedAncestors. + derivedAncestors = Assert.IsType(clauseOfDerivedAncestors.SelectedItems.Single()); + Assert.Equal( + "DerivedAncestors", + ((NavigationPropertySegment)derivedAncestors.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); + Assert.Null(derivedAncestors.LevelsOption); + + clauseOfDerivedAncestors = derivedAncestors.SelectAndExpand; + Assert.True(clauseOfDerivedAncestors.AllSelected); + Assert.Empty(clauseOfDerivedAncestors.SelectedItems); + + // Level 2 of Parent. + parent = Assert.Single(clauseOfParent.SelectedItems.OfType().Where( + item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && + ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "Parent")); + Assert.Null(parent.LevelsOption); + + clauseOfParent = parent.SelectAndExpand; + Assert.True(clauseOfParent.AllSelected); + Assert.Single(clauseOfParent.SelectedItems); + + // Level 1 of DerivedAncestors. + derivedAncestors = Assert.Single(clauseOfParent.SelectedItems.OfType().Where( + item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && + ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "DerivedAncestors")); + Assert.Null(derivedAncestors.LevelsOption); + + clauseOfDerivedAncestors = derivedAncestors.SelectAndExpand; + Assert.True(clauseOfDerivedAncestors.AllSelected); + Assert.Single(clauseOfDerivedAncestors.SelectedItems); + + // Level 2 of DerivedAncestors. + derivedAncestors = Assert.IsType(clauseOfDerivedAncestors.SelectedItems.Single()); + Assert.Equal( + "DerivedAncestors", + ((NavigationPropertySegment)derivedAncestors.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); + Assert.Null(derivedAncestors.LevelsOption); + + clauseOfDerivedAncestors = derivedAncestors.SelectAndExpand; + Assert.True(clauseOfDerivedAncestors.AllSelected); + Assert.Empty(clauseOfDerivedAncestors.SelectedItems); + } + + [Fact] + public void ProcessLevelsCorrectly_WithMaxNestedLevels() + { + // Arrange + var model = ODataLevelsTest.GetEdmModel(); + var context = new ODataQueryContext( + model, + model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.LevelsEntity")); + context.RequestContainer = new MockServiceProvider(); + var selectExpand = new SelectExpandQueryOption( + select: null, + expand: "Parent($expand=DerivedAncestors($levels=max);$levels=2)", + context: context); + + // Act + SelectExpandClause clause = selectExpand.ProcessLevels(); + + // Assert + Assert.True(clause.AllSelected); + Assert.Single(clause.SelectedItems); + + // Level 1 of Parent. + var parent = Assert.IsType(clause.SelectedItems.Single()); + Assert.Equal( + "Parent", + ((NavigationPropertySegment)parent.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); + Assert.Null(parent.LevelsOption); + + var clauseOfParent = parent.SelectAndExpand; + Assert.True(clauseOfParent.AllSelected); + Assert.Equal(2, clauseOfParent.SelectedItems.Count()); + + // Level 1 of DerivedAncestors. + var derivedAncestors = Assert.Single( + clauseOfParent.SelectedItems.OfType().Where( item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && - ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "DerivedAncestors")); - Assert.Null(derivedAncestors.LevelsOption); - - var clauseOfDerivedAncestors = derivedAncestors.SelectAndExpand; - Assert.True(clauseOfDerivedAncestors.AllSelected); - Assert.Single(clauseOfDerivedAncestors.SelectedItems); - - // Level 2 of DerivedAncestors. - derivedAncestors = Assert.IsType(clauseOfDerivedAncestors.SelectedItems.Single()); - Assert.Equal( - "DerivedAncestors", - ((NavigationPropertySegment)derivedAncestors.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); - Assert.Null(derivedAncestors.LevelsOption); - - clauseOfDerivedAncestors = derivedAncestors.SelectAndExpand; - Assert.True(clauseOfDerivedAncestors.AllSelected); - Assert.Empty(clauseOfDerivedAncestors.SelectedItems); - - // Level 2 of Parent. - parent = Assert.Single(clauseOfParent.SelectedItems.OfType().Where( + ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "DerivedAncestors") + ); + Assert.Null(derivedAncestors.LevelsOption); + + var clauseOfDerivedAncestors = derivedAncestors.SelectAndExpand; + Assert.True(clauseOfDerivedAncestors.AllSelected); + Assert.Empty(clauseOfDerivedAncestors.SelectedItems); + + // Level 2 of Parent. + parent = Assert.Single( + clauseOfParent.SelectedItems.OfType().Where( item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && - ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "Parent")); - Assert.Null(parent.LevelsOption); + ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "Parent") + ); + Assert.Null(parent.LevelsOption); - clauseOfParent = parent.SelectAndExpand; - Assert.True(clauseOfParent.AllSelected); - Assert.Single(clauseOfParent.SelectedItems); + clauseOfParent = parent.SelectAndExpand; + Assert.True(clauseOfParent.AllSelected); + Assert.Empty(clauseOfParent.SelectedItems); + } - // Level 1 of DerivedAncestors. - derivedAncestors = Assert.Single(clauseOfParent.SelectedItems.OfType().Where( + [Theory] + [InlineData("http://test")] + [InlineData("http://test?$expand=Friend($levels=max)")] + public void ProcessLevelsCorrectly_WithAutoExpand(string url) + { + // Arrange + var model = GetAutoExpandEdmModel(); + var context = new ODataQueryContext( + model, + model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.AutoExpandCustomer")); + var request = RequestFactory.Create("Get", url, opt => opt.AddRouteComponents(model)); + request.ODataFeature().RoutePrefix = ""; + request.ODataFeature().Model = model; + var queryOption = new ODataQueryOptions(context, request); + queryOption.AddAutoSelectExpandProperties(); + var selectExpand = queryOption.SelectExpand; + + // Act + SelectExpandClause clause = selectExpand.ProcessLevels(); + + // Assert + Assert.True(clause.AllSelected); + Assert.Equal(2, clause.SelectedItems.Count()); + + // Level 1 of Customer. + var cutomer = Assert.Single( + clause.SelectedItems.OfType().Where( item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && - ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "DerivedAncestors")); - Assert.Null(derivedAncestors.LevelsOption); - - clauseOfDerivedAncestors = derivedAncestors.SelectAndExpand; - Assert.True(clauseOfDerivedAncestors.AllSelected); - Assert.Single(clauseOfDerivedAncestors.SelectedItems); - - // Level 2 of DerivedAncestors. - derivedAncestors = Assert.IsType(clauseOfDerivedAncestors.SelectedItems.Single()); - Assert.Equal( - "DerivedAncestors", - ((NavigationPropertySegment)derivedAncestors.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); - Assert.Null(derivedAncestors.LevelsOption); - - clauseOfDerivedAncestors = derivedAncestors.SelectAndExpand; - Assert.True(clauseOfDerivedAncestors.AllSelected); - Assert.Empty(clauseOfDerivedAncestors.SelectedItems); - } + ((NavigationPropertySegment) item.PathToNavigationProperty.FirstSegment).NavigationProperty + .Name == "Friend") + ); - [Fact] - public void ProcessLevelsCorrectly_WithMaxNestedLevels() - { - // Arrange - var model = ODataLevelsTest.GetEdmModel(); - var context = new ODataQueryContext( - model, - model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.LevelsEntity")); - context.RequestContainer = new MockServiceProvider(); - var selectExpand = new SelectExpandQueryOption( - select: null, - expand: "Parent($expand=DerivedAncestors($levels=max);$levels=2)", - context: context); - - // Act - SelectExpandClause clause = selectExpand.ProcessLevels(); - - // Assert - Assert.True(clause.AllSelected); - Assert.Single(clause.SelectedItems); - - // Level 1 of Parent. - var parent = Assert.IsType(clause.SelectedItems.Single()); - Assert.Equal( - "Parent", - ((NavigationPropertySegment)parent.PathToNavigationProperty.FirstSegment).NavigationProperty.Name); - Assert.Null(parent.LevelsOption); - - var clauseOfParent = parent.SelectAndExpand; - Assert.True(clauseOfParent.AllSelected); - Assert.Equal(2, clauseOfParent.SelectedItems.Count()); - - // Level 1 of DerivedAncestors. - var derivedAncestors = Assert.Single( - clauseOfParent.SelectedItems.OfType().Where( - item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && - ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "DerivedAncestors") - ); - Assert.Null(derivedAncestors.LevelsOption); - - var clauseOfDerivedAncestors = derivedAncestors.SelectAndExpand; - Assert.True(clauseOfDerivedAncestors.AllSelected); - Assert.Empty(clauseOfDerivedAncestors.SelectedItems); - - // Level 2 of Parent. - parent = Assert.Single( - clauseOfParent.SelectedItems.OfType().Where( - item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && - ((NavigationPropertySegment)item.PathToNavigationProperty.FirstSegment).NavigationProperty.Name == "Parent") - ); - Assert.Null(parent.LevelsOption); - - clauseOfParent = parent.SelectAndExpand; - Assert.True(clauseOfParent.AllSelected); - Assert.Empty(clauseOfParent.SelectedItems); - } + var clauseOfCustomer = cutomer.SelectAndExpand; + Assert.True(clauseOfCustomer.AllSelected); + Assert.Equal(2, clauseOfCustomer.SelectedItems.Count()); - [Theory] - [InlineData("http://test")] - [InlineData("http://test?$expand=Friend($levels=max)")] - public void ProcessLevelsCorrectly_WithAutoExpand(string url) - { - // Arrange - var model = GetAutoExpandEdmModel(); - var context = new ODataQueryContext( - model, - model.FindDeclaredType("Microsoft.AspNetCore.OData.Tests.Query.AutoExpandCustomer")); - var request = RequestFactory.Create("Get", url, opt => opt.AddRouteComponents(model)); - request.ODataFeature().RoutePrefix = ""; - request.ODataFeature().Model = model; - var queryOption = new ODataQueryOptions(context, request); - queryOption.AddAutoSelectExpandProperties(); - var selectExpand = queryOption.SelectExpand; - - // Act - SelectExpandClause clause = selectExpand.ProcessLevels(); - - // Assert - Assert.True(clause.AllSelected); - Assert.Equal(2, clause.SelectedItems.Count()); - - // Level 1 of Customer. - var cutomer = Assert.Single( - clause.SelectedItems.OfType().Where( - item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && - ((NavigationPropertySegment) item.PathToNavigationProperty.FirstSegment).NavigationProperty - .Name == "Friend") - ); - - var clauseOfCustomer = cutomer.SelectAndExpand; - Assert.True(clauseOfCustomer.AllSelected); - Assert.Equal(2, clauseOfCustomer.SelectedItems.Count()); - - // Order under Customer. - var order = Assert.Single( - clause.SelectedItems.OfType().Where( - item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && - ((NavigationPropertySegment) item.PathToNavigationProperty.FirstSegment).NavigationProperty - .Name == "Order") - ); - Assert.Null(order.LevelsOption); - - var clauseOfOrder = order.SelectAndExpand; - Assert.True(clauseOfOrder.AllSelected); - Assert.Single(clauseOfOrder.SelectedItems); - - // Choice Order under Order - var choiceOrder = Assert.IsType(clauseOfOrder.SelectedItems.Single()); - Assert.Null(choiceOrder.LevelsOption); - Assert.True(choiceOrder.SelectAndExpand.AllSelected); - Assert.Empty(choiceOrder.SelectAndExpand.SelectedItems); - - // Level 2 of Order. - order = Assert.Single( - clauseOfCustomer.SelectedItems.OfType().Where( - item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && - ((NavigationPropertySegment) item.PathToNavigationProperty.FirstSegment).NavigationProperty - .Name == "Order") - ); - Assert.Null(order.LevelsOption); - - clauseOfOrder = order.SelectAndExpand; - Assert.True(clauseOfOrder.AllSelected); - Assert.Empty(clauseOfOrder.SelectedItems); - - // Level 2 of Customer. - cutomer = Assert.Single( - clauseOfCustomer.SelectedItems.OfType().Where( - item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && - ((NavigationPropertySegment) item.PathToNavigationProperty.FirstSegment).NavigationProperty - .Name == "Friend") - ); - Assert.Null(cutomer.LevelsOption); - - clauseOfCustomer = cutomer.SelectAndExpand; - Assert.True(clauseOfCustomer.AllSelected); - Assert.Empty(clauseOfCustomer.SelectedItems); - } + // Order under Customer. + var order = Assert.Single( + clause.SelectedItems.OfType().Where( + item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && + ((NavigationPropertySegment) item.PathToNavigationProperty.FirstSegment).NavigationProperty + .Name == "Order") + ); + Assert.Null(order.LevelsOption); + + var clauseOfOrder = order.SelectAndExpand; + Assert.True(clauseOfOrder.AllSelected); + Assert.Single(clauseOfOrder.SelectedItems); + + // Choice Order under Order + var choiceOrder = Assert.IsType(clauseOfOrder.SelectedItems.Single()); + Assert.Null(choiceOrder.LevelsOption); + Assert.True(choiceOrder.SelectAndExpand.AllSelected); + Assert.Empty(choiceOrder.SelectAndExpand.SelectedItems); + + // Level 2 of Order. + order = Assert.Single( + clauseOfCustomer.SelectedItems.OfType().Where( + item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && + ((NavigationPropertySegment) item.PathToNavigationProperty.FirstSegment).NavigationProperty + .Name == "Order") + ); + Assert.Null(order.LevelsOption); + + clauseOfOrder = order.SelectAndExpand; + Assert.True(clauseOfOrder.AllSelected); + Assert.Empty(clauseOfOrder.SelectedItems); + + // Level 2 of Customer. + cutomer = Assert.Single( + clauseOfCustomer.SelectedItems.OfType().Where( + item => item.PathToNavigationProperty.FirstSegment is NavigationPropertySegment && + ((NavigationPropertySegment) item.PathToNavigationProperty.FirstSegment).NavigationProperty + .Name == "Friend") + ); + Assert.Null(cutomer.LevelsOption); + + clauseOfCustomer = cutomer.SelectAndExpand; + Assert.True(clauseOfCustomer.AllSelected); + Assert.Empty(clauseOfCustomer.SelectedItems); + } - private IEdmModel GetAutoExpandEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("AutoExpandCustomers"); - builder.EntitySet("AutoExpandOrders"); - builder.EntitySet("AutoExpandChoiceOrders"); - return builder.GetEdmModel(); - } + private IEdmModel GetAutoExpandEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("AutoExpandCustomers"); + builder.EntitySet("AutoExpandOrders"); + builder.EntitySet("AutoExpandChoiceOrders"); + return builder.GetEdmModel(); + } - public class SelectExpandCustomer - { - public int Id { get; set; } - public string Name { get; set; } - } + public class SelectExpandCustomer + { + public int Id { get; set; } + public string Name { get; set; } + } - [AutoExpand] - public class AutoExpandCustomer : SelectExpandCustomer - { - public AutoExpandOrder Order { get; set; } - public AutoExpandCustomer Friend { get; set; } - } + [AutoExpand] + public class AutoExpandCustomer : SelectExpandCustomer + { + public AutoExpandOrder Order { get; set; } + public AutoExpandCustomer Friend { get; set; } + } - public class AutoExpandOrder - { - public int Id { get; set; } - [AutoExpand] - public AutoExpandChoiceOrder Choice { get; set; } - } + public class AutoExpandOrder + { + public int Id { get; set; } + [AutoExpand] + public AutoExpandChoiceOrder Choice { get; set; } + } - public class AutoExpandChoiceOrder - { - public int Id { get; set; } - public string Name { get; set; } - } + public class AutoExpandChoiceOrder + { + public int Id { get; set; } + public string Name { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SkipQueryOptionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SkipQueryOptionTests.cs index 8e7a02a54..df8dda798 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SkipQueryOptionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SkipQueryOptionTests.cs @@ -17,216 +17,215 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class SkipQueryOptionTests { - public class SkipQueryOptionTests + [Fact] + public void ConstructorNullContextThrows() + { + ExceptionAssert.ThrowsArgumentNull(() => new SkipQueryOption("1", null), "context"); + ExceptionAssert.ThrowsArgumentNull(() => new SkipQueryOption("1", null, queryOptionParser: null), "context"); + } + + [Fact] + public void ConstructorNullRawValueThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); + + // Act & Assert + ExceptionAssert.Throws(() => new SkipQueryOption(null, context)); + ExceptionAssert.Throws(() => new SkipQueryOption(null, context, queryOptionParser: null)); + } + + [Fact] + public void ConstructorEmptyRawValueThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + + // Act & Assert + ExceptionAssert.Throws(() => + new SkipQueryOption(string.Empty, new ODataQueryContext(model, typeof(Customer)))); + } + + [Fact] + public void ConstructorNullQueryOptionParserThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => + new SkipQueryOption("5", new ODataQueryContext(model, typeof(Customer)), queryOptionParser: null), + "queryOptionParser"); + } + + [Theory] + [InlineData("2")] + [InlineData("100")] + [InlineData("0")] + [InlineData("-1")] + public void CanConstructValidFilterQuery(string skipValue) + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)); + var skip = new SkipQueryOption(skipValue, context); + + Assert.Same(context, skip.Context); + Assert.Equal(skipValue, skip.RawValue); + } + + //[Theory] + //[InlineData("NotANumber")] + //[InlineData("''")] + //[InlineData(" ")] + //public void ApplyInValidSkipQueryThrows(string skipValue) + //{ + // var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + // var context = new ODataQueryContext(model, typeof(Customer)); + // var skip = new SkipQueryOption(skipValue, context); + + // ExceptionAssert.Throws(() => + // skip.ApplyTo(ODataQueryOptionTest.Customers, new ODataQuerySettings())); + //} + + [Theory] + [InlineData("0", 0)] + [InlineData("100", 100)] + public void Value_Returns_ParsedSkipValue(string skipValue, int expectedValue) + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)); + var skip = new SkipQueryOption(skipValue, context); + + Assert.Equal(expectedValue, skip.Value); + } + + [Theory] + [InlineData("NotANumber")] + [InlineData("''")] + [InlineData(" ")] + [InlineData("-1")] + [InlineData("6926906880")] + public void Value_ThrowsODataException_ForInvalidValues(string skipValue) + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)); + var skip = new SkipQueryOption(skipValue, context); + + ExceptionAssert.Throws(() => skip.Value); + } + + [Fact] + public void CanApplySkip() + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var skipOption = new SkipQueryOption("1", new ODataQueryContext(model, typeof(Customer))); + + var customers = (new List{ + new Customer { Id = 1, Name = "Andy" }, + new Customer { Id = 2, Name = "Aaron" }, + new Customer { Id = 3, Name = "Alex" } + }).AsQueryable(); + + var results = skipOption.ApplyTo(customers, new ODataQuerySettings()).ToArray(); + Assert.Equal(2, results.Length); + Assert.Equal(2, results[0].Id); + Assert.Equal(3, results[1].Id); + } + + [Fact] + public void CanApplySkipOrderby() + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderbyOption = new OrderByQueryOption("Name", context); + var skipOption = new SkipQueryOption("1", context); + + var customers = (new List{ + new Customer { Id = 1, Name = "Andy" }, + new Customer { Id = 2, Name = "Aaron" }, + new Customer { Id = 3, Name = "Alex" } + }).AsQueryable(); + + IQueryable queryable = orderbyOption.ApplyTo(customers); + queryable = skipOption.ApplyTo(queryable, new ODataQuerySettings()); + var results = ((IQueryable)queryable).ToArray(); + Assert.Equal(2, results.Length); + Assert.Equal(3, results[0].Id); + Assert.Equal(1, results[1].Id); + } + + //[Fact] + //public void CanTurnOffValidationForSkip() + //{ + // // Arrange + // ODataValidationSettings settings = new ODataValidationSettings() + // { + // MaxSkip = 10 + // }; + // SkipQueryOption option = new SkipQueryOption("11", ValidationTestHelper.CreateCustomerContext()); + + // // Act and Assert + // ExceptionAssert.Throws(() => + // option.Validate(settings), + // "The limit of '10' for Skip query has been exceeded. The value from the incoming request is '11'."); + // option.Validator = null; + // ExceptionAssert.DoesNotThrow(() => option.Validate(settings)); + //} + + [Fact] + public void Validate_ThrowsArgumentNull_Settings() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + SkipQueryOption skip = new SkipQueryOption("42", context); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => skip.Validate(null), "validationSettings"); + } + + [Fact] + public void ApplyTo_ThrowsArgumentNull_ForInputs() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + SkipQueryOption skip = new SkipQueryOption("42", context); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => skip.ApplyTo(null, null), "query"); + + ExceptionAssert.ThrowsArgumentNull(() => skip.ApplyTo(new Mock().Object, null), "querySettings"); + } + + [Fact] + public void Property_Value_WorksWithUnTypedContext() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + SkipQueryOption skip = new SkipQueryOption("42", context); + + // Act & Assert + Assert.Equal(42, skip.Value); + } + + [Fact] + public void ApplyTo_WithUnTypedContext_Throws_InvalidOperation() { - [Fact] - public void ConstructorNullContextThrows() - { - ExceptionAssert.ThrowsArgumentNull(() => new SkipQueryOption("1", null), "context"); - ExceptionAssert.ThrowsArgumentNull(() => new SkipQueryOption("1", null, queryOptionParser: null), "context"); - } - - [Fact] - public void ConstructorNullRawValueThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); - - // Act & Assert - ExceptionAssert.Throws(() => new SkipQueryOption(null, context)); - ExceptionAssert.Throws(() => new SkipQueryOption(null, context, queryOptionParser: null)); - } - - [Fact] - public void ConstructorEmptyRawValueThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - - // Act & Assert - ExceptionAssert.Throws(() => - new SkipQueryOption(string.Empty, new ODataQueryContext(model, typeof(Customer)))); - } - - [Fact] - public void ConstructorNullQueryOptionParserThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => - new SkipQueryOption("5", new ODataQueryContext(model, typeof(Customer)), queryOptionParser: null), - "queryOptionParser"); - } - - [Theory] - [InlineData("2")] - [InlineData("100")] - [InlineData("0")] - [InlineData("-1")] - public void CanConstructValidFilterQuery(string skipValue) - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)); - var skip = new SkipQueryOption(skipValue, context); - - Assert.Same(context, skip.Context); - Assert.Equal(skipValue, skip.RawValue); - } - - //[Theory] - //[InlineData("NotANumber")] - //[InlineData("''")] - //[InlineData(" ")] - //public void ApplyInValidSkipQueryThrows(string skipValue) - //{ - // var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - // var context = new ODataQueryContext(model, typeof(Customer)); - // var skip = new SkipQueryOption(skipValue, context); - - // ExceptionAssert.Throws(() => - // skip.ApplyTo(ODataQueryOptionTest.Customers, new ODataQuerySettings())); - //} - - [Theory] - [InlineData("0", 0)] - [InlineData("100", 100)] - public void Value_Returns_ParsedSkipValue(string skipValue, int expectedValue) - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)); - var skip = new SkipQueryOption(skipValue, context); - - Assert.Equal(expectedValue, skip.Value); - } - - [Theory] - [InlineData("NotANumber")] - [InlineData("''")] - [InlineData(" ")] - [InlineData("-1")] - [InlineData("6926906880")] - public void Value_ThrowsODataException_ForInvalidValues(string skipValue) - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)); - var skip = new SkipQueryOption(skipValue, context); - - ExceptionAssert.Throws(() => skip.Value); - } - - [Fact] - public void CanApplySkip() - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var skipOption = new SkipQueryOption("1", new ODataQueryContext(model, typeof(Customer))); - - var customers = (new List{ - new Customer { Id = 1, Name = "Andy" }, - new Customer { Id = 2, Name = "Aaron" }, - new Customer { Id = 3, Name = "Alex" } - }).AsQueryable(); - - var results = skipOption.ApplyTo(customers, new ODataQuerySettings()).ToArray(); - Assert.Equal(2, results.Length); - Assert.Equal(2, results[0].Id); - Assert.Equal(3, results[1].Id); - } - - [Fact] - public void CanApplySkipOrderby() - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderbyOption = new OrderByQueryOption("Name", context); - var skipOption = new SkipQueryOption("1", context); - - var customers = (new List{ - new Customer { Id = 1, Name = "Andy" }, - new Customer { Id = 2, Name = "Aaron" }, - new Customer { Id = 3, Name = "Alex" } - }).AsQueryable(); - - IQueryable queryable = orderbyOption.ApplyTo(customers); - queryable = skipOption.ApplyTo(queryable, new ODataQuerySettings()); - var results = ((IQueryable)queryable).ToArray(); - Assert.Equal(2, results.Length); - Assert.Equal(3, results[0].Id); - Assert.Equal(1, results[1].Id); - } - - //[Fact] - //public void CanTurnOffValidationForSkip() - //{ - // // Arrange - // ODataValidationSettings settings = new ODataValidationSettings() - // { - // MaxSkip = 10 - // }; - // SkipQueryOption option = new SkipQueryOption("11", ValidationTestHelper.CreateCustomerContext()); - - // // Act and Assert - // ExceptionAssert.Throws(() => - // option.Validate(settings), - // "The limit of '10' for Skip query has been exceeded. The value from the incoming request is '11'."); - // option.Validator = null; - // ExceptionAssert.DoesNotThrow(() => option.Validate(settings)); - //} - - [Fact] - public void Validate_ThrowsArgumentNull_Settings() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - SkipQueryOption skip = new SkipQueryOption("42", context); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => skip.Validate(null), "validationSettings"); - } - - [Fact] - public void ApplyTo_ThrowsArgumentNull_ForInputs() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - SkipQueryOption skip = new SkipQueryOption("42", context); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => skip.ApplyTo(null, null), "query"); - - ExceptionAssert.ThrowsArgumentNull(() => skip.ApplyTo(new Mock().Object, null), "querySettings"); - } - - [Fact] - public void Property_Value_WorksWithUnTypedContext() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - SkipQueryOption skip = new SkipQueryOption("42", context); - - // Act & Assert - Assert.Equal(42, skip.Value); - } - - [Fact] - public void ApplyTo_WithUnTypedContext_Throws_InvalidOperation() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - SkipQueryOption skip = new SkipQueryOption("42", context); - IQueryable queryable = new Mock().Object; - - // Act & Assert - ExceptionAssert.Throws(() => skip.ApplyTo(queryable, new ODataQuerySettings()), - "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); - } + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + SkipQueryOption skip = new SkipQueryOption("42", context); + IQueryable queryable = new Mock().Object; + + // Act & Assert + ExceptionAssert.Throws(() => skip.ApplyTo(queryable, new ODataQuerySettings()), + "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SkipTokenQueryOptionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SkipTokenQueryOptionTests.cs index aebb843cd..e62eaffc6 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SkipTokenQueryOptionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/SkipTokenQueryOptionTests.cs @@ -19,197 +19,196 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class SkipTokenQueryOptionTests { - public class SkipTokenQueryOptionTests + [Theory] + [InlineData(null)] + [InlineData("")] + public void CtorSkipTokenQueryOption_ThrowsArgumentNullOrEmpty_RawValue(string rawValue) { - [Theory] - [InlineData(null)] - [InlineData("")] - public void CtorSkipTokenQueryOption_ThrowsArgumentNullOrEmpty_RawValue(string rawValue) - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new SkipTokenQueryOption(rawValue, null), "rawValue"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNullOrEmpty(() => new SkipTokenQueryOption(rawValue, null), "rawValue"); + } - [Fact] - public void CtorSkipTokenQueryOption_ThrowsArgumentNull_Context() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new SkipTokenQueryOption("abc", null), "context"); - } + [Fact] + public void CtorSkipTokenQueryOption_ThrowsArgumentNull_Context() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new SkipTokenQueryOption("abc", null), "context"); + } - [Fact] - public void CtorSkipTokenQueryOption_SetsProperties() - { - // Arrange - Mock handler = new Mock(); - SkipTokenQueryValidator validator = new SkipTokenQueryValidator(); - IServiceProvider sp = new ServiceCollection() - .AddSingleton(handler.Object) - .AddSingleton(validator) - .BuildServiceProvider(); - - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)) - { - RequestContainer = sp - }; - - // Act - SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption("abc", context); - - // Assert - Assert.Equal("abc", skipTokenQuery.RawValue); - Assert.Same(context, skipTokenQuery.Context); - Assert.Same(handler.Object, skipTokenQuery.Handler); - Assert.Same(validator, skipTokenQuery.Validator); - } - - [Fact] - public void ApplyToOfTSkipTokenQueryOption_Calls_ApplyToOfTOnSkipTokenHandler() + [Fact] + public void CtorSkipTokenQueryOption_SetsProperties() + { + // Arrange + Mock handler = new Mock(); + SkipTokenQueryValidator validator = new SkipTokenQueryValidator(); + IServiceProvider sp = new ServiceCollection() + .AddSingleton(handler.Object) + .AddSingleton(validator) + .BuildServiceProvider(); + + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)) { - // Arrange - Mock handler = new Mock(); - ODataQuerySettings settings = new ODataQuerySettings(); - SkipTokenQueryValidator validator = new SkipTokenQueryValidator(); - IServiceProvider sp = new ServiceCollection() - .AddSingleton(handler.Object) - .AddSingleton(validator) - .BuildServiceProvider(); - - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)) - { - RequestContainer = sp - }; - - SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption("abc", context); - - IQueryable query = Array.Empty().AsQueryable(); - handler.Setup(h => h.ApplyTo(query, skipTokenQuery, settings, null)).Returns(query).Verifiable(); - - // Act - skipTokenQuery.ApplyTo(query, settings, null); - - // Assert - handler.Verify(); - } - - [Fact] - public void ApplyToOfTSkipTokenQueryOption_Applies_ToQuaryable() + RequestContainer = sp + }; + + // Act + SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption("abc", context); + + // Assert + Assert.Equal("abc", skipTokenQuery.RawValue); + Assert.Same(context, skipTokenQuery.Context); + Assert.Same(handler.Object, skipTokenQuery.Handler); + Assert.Same(validator, skipTokenQuery.Validator); + } + + [Fact] + public void ApplyToOfTSkipTokenQueryOption_Calls_ApplyToOfTOnSkipTokenHandler() + { + // Arrange + Mock handler = new Mock(); + ODataQuerySettings settings = new ODataQuerySettings(); + SkipTokenQueryValidator validator = new SkipTokenQueryValidator(); + IServiceProvider sp = new ServiceCollection() + .AddSingleton(handler.Object) + .AddSingleton(validator) + .BuildServiceProvider(); + + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)) { - // Arrange - IEdmModel model = GetEdmModel(); - ODataQuerySettings settings = new ODataQuerySettings - { - HandleNullPropagation = HandleNullPropagationOption.False - }; - ODataQueryContext context = new ODataQueryContext(model, typeof(SkipTokenCustomer)); - HttpRequest request = RequestFactory.Create("Get", "http://localhost/"); - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - - SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption("Id-2", context); - - IQueryable customers = new List - { - new SkipTokenCustomer { Id = 2, Name = "Aaron" }, - new SkipTokenCustomer { Id = 1, Name = "Andy" }, - new SkipTokenCustomer { Id = 3, Name = "Alex" } - }.AsQueryable(); - - // Act - SkipTokenCustomer[] results = skipTokenQuery.ApplyTo(customers, settings, queryOptions).ToArray(); - - // Assert - SkipTokenCustomer skipTokenCustomer = Assert.Single(results); - Assert.Equal(3, skipTokenCustomer.Id); - Assert.Equal("Alex", skipTokenCustomer.Name); - } - - [Fact] - public void ApplyToOfTSkipTokenQueryOption_Applies_ToQuaryable_WithOrderby() + RequestContainer = sp + }; + + SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption("abc", context); + + IQueryable query = Array.Empty().AsQueryable(); + handler.Setup(h => h.ApplyTo(query, skipTokenQuery, settings, null)).Returns(query).Verifiable(); + + // Act + skipTokenQuery.ApplyTo(query, settings, null); + + // Assert + handler.Verify(); + } + + [Fact] + public void ApplyToOfTSkipTokenQueryOption_Applies_ToQuaryable() + { + // Arrange + IEdmModel model = GetEdmModel(); + ODataQuerySettings settings = new ODataQuerySettings { - // Arrange - IEdmModel model = GetEdmModel(); - ODataQuerySettings settings = new ODataQuerySettings - { - HandleNullPropagation = HandleNullPropagationOption.False - }; - ODataQueryContext context = new ODataQueryContext(model, typeof(SkipTokenCustomer)); - - HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/Customers/?$orderby=Name&$skiptoken=Name-'Alex',Id-3"); - - ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); - SkipTokenQueryOption skipTokenQuery = queryOptions.SkipToken; - - IQueryable customers = new List - { - new SkipTokenCustomer { Id = 2, Name = "Caron" }, - new SkipTokenCustomer { Id = 1, Name = "Bndy" }, - new SkipTokenCustomer { Id = 3, Name = "Alex" }, - new SkipTokenCustomer { Id = 4, Name = "Aab" } - }.AsQueryable(); - - // Act - SkipTokenCustomer[] results = skipTokenQuery.ApplyTo(customers, settings, queryOptions).ToArray(); - - // Assert - Assert.Equal(2, results.Length); - Assert.Equal(2, results[0].Id); - Assert.Equal(1, results[1].Id); - } - - [Fact] - public void ApplyToSkipTokenQueryOption_Calls_ApplyToOnSkipTokenHandler() + HandleNullPropagation = HandleNullPropagationOption.False + }; + ODataQueryContext context = new ODataQueryContext(model, typeof(SkipTokenCustomer)); + HttpRequest request = RequestFactory.Create("Get", "http://localhost/"); + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + + SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption("Id-2", context); + + IQueryable customers = new List { - // Arrange - Mock handler = new Mock(); - ODataQuerySettings settings = new ODataQuerySettings(); - SkipTokenQueryValidator validator = new SkipTokenQueryValidator(); - IServiceProvider sp = new ServiceCollection() - .AddSingleton(handler.Object) - .AddSingleton(validator) - .BuildServiceProvider(); - - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)) - { - RequestContainer = sp - }; - - SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption("abc", context); - - IQueryable queryable = new Mock().Object; - handler.Setup(h => h.ApplyTo(queryable, skipTokenQuery, settings, null)).Returns(queryable).Verifiable(); - - // Act - skipTokenQuery.ApplyTo(queryable, settings, null); - - // Assert - handler.Verify(); - } - - [Fact] - public void ValidateSkipTokenQueryOption_ThrowsArgumentNull_ValidationSettings() + new SkipTokenCustomer { Id = 2, Name = "Aaron" }, + new SkipTokenCustomer { Id = 1, Name = "Andy" }, + new SkipTokenCustomer { Id = 3, Name = "Alex" } + }.AsQueryable(); + + // Act + SkipTokenCustomer[] results = skipTokenQuery.ApplyTo(customers, settings, queryOptions).ToArray(); + + // Assert + SkipTokenCustomer skipTokenCustomer = Assert.Single(results); + Assert.Equal(3, skipTokenCustomer.Id); + Assert.Equal("Alex", skipTokenCustomer.Name); + } + + [Fact] + public void ApplyToOfTSkipTokenQueryOption_Applies_ToQuaryable_WithOrderby() + { + // Arrange + IEdmModel model = GetEdmModel(); + ODataQuerySettings settings = new ODataQuerySettings { - // Arrange - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - SkipTokenQueryOption skipToken = new SkipTokenQueryOption("abc", context); + HandleNullPropagation = HandleNullPropagationOption.False + }; + ODataQueryContext context = new ODataQueryContext(model, typeof(SkipTokenCustomer)); + + HttpRequest request = RequestFactory.Create(HttpMethods.Get, "http://server/Customers/?$orderby=Name&$skiptoken=Name-'Alex',Id-3"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => skipToken.Validate(null), "validationSettings"); - } + ODataQueryOptions queryOptions = new ODataQueryOptions(context, request); + SkipTokenQueryOption skipTokenQuery = queryOptions.SkipToken; - private static IEdmModel GetEdmModel() + IQueryable customers = new List { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - return builder.GetEdmModel(); - } + new SkipTokenCustomer { Id = 2, Name = "Caron" }, + new SkipTokenCustomer { Id = 1, Name = "Bndy" }, + new SkipTokenCustomer { Id = 3, Name = "Alex" }, + new SkipTokenCustomer { Id = 4, Name = "Aab" } + }.AsQueryable(); + + // Act + SkipTokenCustomer[] results = skipTokenQuery.ApplyTo(customers, settings, queryOptions).ToArray(); + + // Assert + Assert.Equal(2, results.Length); + Assert.Equal(2, results[0].Id); + Assert.Equal(1, results[1].Id); + } - private class SkipTokenCustomer + [Fact] + public void ApplyToSkipTokenQueryOption_Calls_ApplyToOnSkipTokenHandler() + { + // Arrange + Mock handler = new Mock(); + ODataQuerySettings settings = new ODataQuerySettings(); + SkipTokenQueryValidator validator = new SkipTokenQueryValidator(); + IServiceProvider sp = new ServiceCollection() + .AddSingleton(handler.Object) + .AddSingleton(validator) + .BuildServiceProvider(); + + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)) { - public int Id { get; set; } + RequestContainer = sp + }; + + SkipTokenQueryOption skipTokenQuery = new SkipTokenQueryOption("abc", context); + + IQueryable queryable = new Mock().Object; + handler.Setup(h => h.ApplyTo(queryable, skipTokenQuery, settings, null)).Returns(queryable).Verifiable(); + + // Act + skipTokenQuery.ApplyTo(queryable, settings, null); + + // Assert + handler.Verify(); + } + + [Fact] + public void ValidateSkipTokenQueryOption_ThrowsArgumentNull_ValidationSettings() + { + // Arrange + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + SkipTokenQueryOption skipToken = new SkipTokenQueryOption("abc", context); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => skipToken.Validate(null), "validationSettings"); + } + + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + return builder.GetEdmModel(); + } + + private class SkipTokenCustomer + { + public int Id { get; set; } - public string Name { get; set; } - } + public string Name { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/TopQueryOptionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/TopQueryOptionTests.cs index 59b0fc649..dd12f9c6e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Query/TopQueryOptionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Query/TopQueryOptionTests.cs @@ -17,235 +17,234 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class TopQueryOptionTests { - public class TopQueryOptionTests + [Fact] + public void ConstructorNullContextThrows() + { + ExceptionAssert.Throws(() => new TopQueryOption("1", null)); + ExceptionAssert.Throws(() => new TopQueryOption("1", null, queryOptionParser: null)); + } + + [Fact] + public void ConstructorNullRawValueThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); + + // Act & Assert + ExceptionAssert.Throws(() => new TopQueryOption(null, context)); + ExceptionAssert.Throws(() => new TopQueryOption(null, context, queryOptionParser: null)); + } + + [Fact] + public void ConstructorEmptyRawValueThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + + // Act & Assert + ExceptionAssert.Throws(() => + new TopQueryOption(string.Empty, new ODataQueryContext(model, typeof(Customer)))); + } + + [Fact] + public void ConstructorNullQueryOptionParserThrows() + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => + new TopQueryOption("7", new ODataQueryContext(model, typeof(Customer)), queryOptionParser: null), + "queryOptionParser"); + } + + [Theory] + [InlineData("2")] + [InlineData("100")] + [InlineData("0")] + public void CanConstructValidFilterQuery(string topValue) + { + // Arrange + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)); + + // Act + var top = new TopQueryOption(topValue, context); + + // Assert + Assert.Same(context, top.Context); + Assert.Equal(topValue, top.RawValue); + } + + //[Theory] + //[InlineData("NotANumber")] + //[InlineData("''")] + //[InlineData(" ")] + //[InlineData("-1")] + //[InlineData("6926906880")] + //public void ApplyInvalidTopQueryThrows(string topValue) + //{ + // var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + // var context = new ODataQueryContext(model, typeof(Customer)); + // var top = new TopQueryOption(topValue, context); + + // ExceptionAssert.Throws(() => + // top.ApplyTo(ODataQueryOptionTest.Customers, new ODataQuerySettings())); + //} + + [Theory] + [InlineData("0", 0)] + [InlineData("100", 100)] + public void Value_Returns_ParsedTopValue(string topValue, int expectedValue) + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)); + var top = new TopQueryOption(topValue, context); + + Assert.Equal(expectedValue, top.Value); + } + + [Theory] + [InlineData("NotANumber")] + [InlineData("''")] + [InlineData(" ")] + [InlineData("-1")] + public void Value_ThrowsODataException_ForInvalidValues(string skipValue) + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)); + var top = new TopQueryOption(skipValue, context); + + ExceptionAssert.Throws(() => top.Value, + $"Invalid value '{skipValue}' for $top query option found. The $top query option requires a non-negative integer value."); + } + + [Fact] + public void Value_ThrowsODataException_ForLargeValue() + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)); + var top = new TopQueryOption("2147483648", context); // int.MaxValue + 1 + + ExceptionAssert.Throws(() => top.Value, + "The limit of '2147483647' for Top query has been exceeded. The value from the incoming request is '2147483648'."); + } + + [Fact] + public void CanApplyTop() + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var topOption = new TopQueryOption("1", new ODataQueryContext(model, typeof(Customer))); + + var customers = (new List{ + new Customer { Id = 1, Name = "Andy" }, + new Customer { Id = 2, Name = "Aaron" }, + new Customer { Id = 3, Name = "Alex" } + }).AsQueryable(); + + var results = topOption.ApplyTo(customers, new ODataQuerySettings()).ToArray(); + Assert.Single(results); + Assert.Equal(1, results[0].Id); + } + + [Fact] + public void CanApplySkipTopOrderby() + { + var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); + var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; + var orderbyOption = new OrderByQueryOption("Name", context); + var skipOption = new SkipQueryOption("2", context); + var topOption = new TopQueryOption("2", context); + + var customers = (new List{ + new Customer { Id = 1, Name = "Andy" }, + new Customer { Id = 2, Name = "Aaron" }, + new Customer { Id = 3, Name = "Alex" }, + new Customer { Id = 4, Name = "Ace" }, + new Customer { Id = 5, Name = "Abner" } + }).AsQueryable(); + + IQueryable queryable = orderbyOption.ApplyTo(customers); + queryable = skipOption.ApplyTo(queryable, new ODataQuerySettings()); + queryable = topOption.ApplyTo(queryable, new ODataQuerySettings()); + var results = ((IQueryable)queryable).ToArray(); + Assert.Equal(2, results.Length); + Assert.Equal(4, results[0].Id); + Assert.Equal(3, results[1].Id); + } + + //[Fact] + //public void CanTurnOffValidationForTop() + //{ + // // Arrange + // ODataValidationSettings settings = new ODataValidationSettings() + // { + // MaxTop = 10 + // }; + // TopQueryOption option = new TopQueryOption("11", ValidationTestHelper.CreateCustomerContext()); + + // // Act and Assert + // ExceptionAssert.Throws(() => + // option.Validate(settings), + // "The limit of '10' for Top query has been exceeded. The value from the incoming request is '11'."); + // option.Validator = null; + // ExceptionAssert.DoesNotThrow(() => option.Validate(settings)); + //} + + [Fact] + public void Validate_ThrowsArgumentNull_Settings() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + TopQueryOption top = new TopQueryOption("42", context); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => top.Validate(null), "validationSettings"); + } + + [Fact] + public void ApplyTo_ThrowsArgumentNull_ForInputs() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + TopQueryOption top = new TopQueryOption("42", context); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => top.ApplyTo(null, null), "query"); + + ExceptionAssert.ThrowsArgumentNull(() => top.ApplyTo(new Mock().Object, null), "querySettings"); + } + + [Fact] + public void Property_Value_WorksWithUnTypedContext() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + TopQueryOption top = new TopQueryOption("42", context); + + // Act & Assert + Assert.Equal(42, top.Value); + } + + [Fact] + public void ApplyTo_WithUnTypedContext_Throws_InvalidOperation() { - [Fact] - public void ConstructorNullContextThrows() - { - ExceptionAssert.Throws(() => new TopQueryOption("1", null)); - ExceptionAssert.Throws(() => new TopQueryOption("1", null, queryOptionParser: null)); - } - - [Fact] - public void ConstructorNullRawValueThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - ODataQueryContext context = new ODataQueryContext(model, typeof(Customer)); - - // Act & Assert - ExceptionAssert.Throws(() => new TopQueryOption(null, context)); - ExceptionAssert.Throws(() => new TopQueryOption(null, context, queryOptionParser: null)); - } - - [Fact] - public void ConstructorEmptyRawValueThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - - // Act & Assert - ExceptionAssert.Throws(() => - new TopQueryOption(string.Empty, new ODataQueryContext(model, typeof(Customer)))); - } - - [Fact] - public void ConstructorNullQueryOptionParserThrows() - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => - new TopQueryOption("7", new ODataQueryContext(model, typeof(Customer)), queryOptionParser: null), - "queryOptionParser"); - } - - [Theory] - [InlineData("2")] - [InlineData("100")] - [InlineData("0")] - public void CanConstructValidFilterQuery(string topValue) - { - // Arrange - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)); - - // Act - var top = new TopQueryOption(topValue, context); - - // Assert - Assert.Same(context, top.Context); - Assert.Equal(topValue, top.RawValue); - } - - //[Theory] - //[InlineData("NotANumber")] - //[InlineData("''")] - //[InlineData(" ")] - //[InlineData("-1")] - //[InlineData("6926906880")] - //public void ApplyInvalidTopQueryThrows(string topValue) - //{ - // var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - // var context = new ODataQueryContext(model, typeof(Customer)); - // var top = new TopQueryOption(topValue, context); - - // ExceptionAssert.Throws(() => - // top.ApplyTo(ODataQueryOptionTest.Customers, new ODataQuerySettings())); - //} - - [Theory] - [InlineData("0", 0)] - [InlineData("100", 100)] - public void Value_Returns_ParsedTopValue(string topValue, int expectedValue) - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)); - var top = new TopQueryOption(topValue, context); - - Assert.Equal(expectedValue, top.Value); - } - - [Theory] - [InlineData("NotANumber")] - [InlineData("''")] - [InlineData(" ")] - [InlineData("-1")] - public void Value_ThrowsODataException_ForInvalidValues(string skipValue) - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)); - var top = new TopQueryOption(skipValue, context); - - ExceptionAssert.Throws(() => top.Value, - $"Invalid value '{skipValue}' for $top query option found. The $top query option requires a non-negative integer value."); - } - - [Fact] - public void Value_ThrowsODataException_ForLargeValue() - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)); - var top = new TopQueryOption("2147483648", context); // int.MaxValue + 1 - - ExceptionAssert.Throws(() => top.Value, - "The limit of '2147483647' for Top query has been exceeded. The value from the incoming request is '2147483648'."); - } - - [Fact] - public void CanApplyTop() - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var topOption = new TopQueryOption("1", new ODataQueryContext(model, typeof(Customer))); - - var customers = (new List{ - new Customer { Id = 1, Name = "Andy" }, - new Customer { Id = 2, Name = "Aaron" }, - new Customer { Id = 3, Name = "Alex" } - }).AsQueryable(); - - var results = topOption.ApplyTo(customers, new ODataQuerySettings()).ToArray(); - Assert.Single(results); - Assert.Equal(1, results[0].Id); - } - - [Fact] - public void CanApplySkipTopOrderby() - { - var model = new ODataModelBuilder().Add_Customer_EntityType().Add_Customers_EntitySet().GetEdmModel(); - var context = new ODataQueryContext(model, typeof(Customer)) { RequestContainer = new MockServiceProvider() }; - var orderbyOption = new OrderByQueryOption("Name", context); - var skipOption = new SkipQueryOption("2", context); - var topOption = new TopQueryOption("2", context); - - var customers = (new List{ - new Customer { Id = 1, Name = "Andy" }, - new Customer { Id = 2, Name = "Aaron" }, - new Customer { Id = 3, Name = "Alex" }, - new Customer { Id = 4, Name = "Ace" }, - new Customer { Id = 5, Name = "Abner" } - }).AsQueryable(); - - IQueryable queryable = orderbyOption.ApplyTo(customers); - queryable = skipOption.ApplyTo(queryable, new ODataQuerySettings()); - queryable = topOption.ApplyTo(queryable, new ODataQuerySettings()); - var results = ((IQueryable)queryable).ToArray(); - Assert.Equal(2, results.Length); - Assert.Equal(4, results[0].Id); - Assert.Equal(3, results[1].Id); - } - - //[Fact] - //public void CanTurnOffValidationForTop() - //{ - // // Arrange - // ODataValidationSettings settings = new ODataValidationSettings() - // { - // MaxTop = 10 - // }; - // TopQueryOption option = new TopQueryOption("11", ValidationTestHelper.CreateCustomerContext()); - - // // Act and Assert - // ExceptionAssert.Throws(() => - // option.Validate(settings), - // "The limit of '10' for Top query has been exceeded. The value from the incoming request is '11'."); - // option.Validator = null; - // ExceptionAssert.DoesNotThrow(() => option.Validate(settings)); - //} - - [Fact] - public void Validate_ThrowsArgumentNull_Settings() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - TopQueryOption top = new TopQueryOption("42", context); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => top.Validate(null), "validationSettings"); - } - - [Fact] - public void ApplyTo_ThrowsArgumentNull_ForInputs() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - TopQueryOption top = new TopQueryOption("42", context); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => top.ApplyTo(null, null), "query"); - - ExceptionAssert.ThrowsArgumentNull(() => top.ApplyTo(new Mock().Object, null), "querySettings"); - } - - [Fact] - public void Property_Value_WorksWithUnTypedContext() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - TopQueryOption top = new TopQueryOption("42", context); - - // Act & Assert - Assert.Equal(42, top.Value); - } - - [Fact] - public void ApplyTo_WithUnTypedContext_Throws_InvalidOperation() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); - TopQueryOption top = new TopQueryOption("42", context); - IQueryable queryable = new Mock().Object; - - // Act & Assert - ExceptionAssert.Throws(() => top.ApplyTo(queryable, new ODataQuerySettings()), - "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); - } + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataQueryContext context = new ODataQueryContext(model.Model, model.Customer); + TopQueryOption top = new TopQueryOption("42", context); + IQueryable queryable = new Mock().Object; + + // Act & Assert + ExceptionAssert.Throws(() => top.ApplyTo(queryable, new ODataQuerySettings()), + "The query option is not bound to any CLR type. 'ApplyTo' is only supported with a query option bound to a CLR type."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/QueryFilterProviderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/QueryFilterProviderTests.cs index e94a7c82c..e199d9635 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/QueryFilterProviderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/QueryFilterProviderTests.cs @@ -21,85 +21,84 @@ using System; using System.Reflection; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class QueryFilterProviderTests { - public class QueryFilterProviderTests + [Fact] + public void Ctor_ThrowsArgumentNull_QueryFilter() { - [Fact] - public void Ctor_ThrowsArgumentNull_QueryFilter() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new QueryFilterProvider(null), "queryFilter"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new QueryFilterProvider(null), "queryFilter"); + } - [Fact] - public void Ctor_SetsPropertiesCorrectly() - { - // Arrange & Act & Assert - Mock filter = new Mock(); - QueryFilterProvider provider = new QueryFilterProvider(filter.Object); - Assert.Same(filter.Object, provider.QueryFilter); - Assert.Equal(0, provider.Order); - } + [Fact] + public void Ctor_SetsPropertiesCorrectly() + { + // Arrange & Act & Assert + Mock filter = new Mock(); + QueryFilterProvider provider = new QueryFilterProvider(filter.Object); + Assert.Same(filter.Object, provider.QueryFilter); + Assert.Equal(0, provider.Order); + } - [Fact] - public void OnProvidersExecuting_ThrowsArgumentNull_Context() - { - // Arrange & Act & Assert - Mock filter = new Mock(); - QueryFilterProvider provider = new QueryFilterProvider(filter.Object); - ExceptionAssert.ThrowsArgumentNull(() => provider.OnProvidersExecuting(null), "context"); - } + [Fact] + public void OnProvidersExecuting_ThrowsArgumentNull_Context() + { + // Arrange & Act & Assert + Mock filter = new Mock(); + QueryFilterProvider provider = new QueryFilterProvider(filter.Object); + ExceptionAssert.ThrowsArgumentNull(() => provider.OnProvidersExecuting(null), "context"); + } - [Fact] - public void OnProvidersExecuting_AddQueryFilter() - { - // Arrange - Mock filter = new Mock(); - QueryFilterProvider provider = new QueryFilterProvider(filter.Object); + [Fact] + public void OnProvidersExecuting_AddQueryFilter() + { + // Arrange + Mock filter = new Mock(); + QueryFilterProvider provider = new QueryFilterProvider(filter.Object); - List items = new List(); - ControllerActionDescriptor descriptor = new ControllerActionDescriptor() - { - ControllerTypeInfo = typeof(UserController).GetTypeInfo(), - MethodInfo = typeof(UserController).GetMethod("GetUser"), - Parameters = new List() - }; - FilterProviderContext context = CreateFilterContext(items, descriptor); + List items = new List(); + ControllerActionDescriptor descriptor = new ControllerActionDescriptor() + { + ControllerTypeInfo = typeof(UserController).GetTypeInfo(), + MethodInfo = typeof(UserController).GetMethod("GetUser"), + Parameters = new List() + }; + FilterProviderContext context = CreateFilterContext(items, descriptor); - // Act - provider.OnProvidersExecuting(context); + // Act + provider.OnProvidersExecuting(context); - // Assert - FilterItem item = Assert.Single(items); - Assert.Same(filter.Object, item.Filter); - } + // Assert + FilterItem item = Assert.Single(items); + Assert.Same(filter.Object, item.Filter); + } - [Fact] - public void IsIQueryable_Works_ForInputTypes() - { - // Arrange & Act & Assert - Assert.True(QueryFilterProvider.IsIQueryable(typeof(IQueryable))); - Assert.True(QueryFilterProvider.IsIQueryable(typeof(IQueryable<>))); - Assert.False(QueryFilterProvider.IsIQueryable(typeof(IEnumerable<>))); - Assert.False(QueryFilterProvider.IsIQueryable(typeof(int))); - } + [Fact] + public void IsIQueryable_Works_ForInputTypes() + { + // Arrange & Act & Assert + Assert.True(QueryFilterProvider.IsIQueryable(typeof(IQueryable))); + Assert.True(QueryFilterProvider.IsIQueryable(typeof(IQueryable<>))); + Assert.False(QueryFilterProvider.IsIQueryable(typeof(IEnumerable<>))); + Assert.False(QueryFilterProvider.IsIQueryable(typeof(int))); + } - private FilterProviderContext CreateFilterContext(List items, ControllerActionDescriptor descriptor) - { - var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor); - actionContext.ActionDescriptor.FilterDescriptors = new List( - items.Select(item => item.Descriptor)); + private FilterProviderContext CreateFilterContext(List items, ControllerActionDescriptor descriptor) + { + var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), descriptor); + actionContext.ActionDescriptor.FilterDescriptors = new List( + items.Select(item => item.Descriptor)); - return new FilterProviderContext(actionContext, items); - } + return new FilterProviderContext(actionContext, items); + } - private class UserController : ControllerBase + private class UserController : ControllerBase + { + public IQueryable GetUser() { - public IQueryable GetUser() - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/SelectExpandIncludedPropertyTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/SelectExpandIncludedPropertyTest.cs index 6dc240953..4368abfab 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/SelectExpandIncludedPropertyTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/SelectExpandIncludedPropertyTest.cs @@ -9,16 +9,15 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class SelectExpandIncludePropertyTest { - public class SelectExpandIncludePropertyTest + [Fact] + public void Constructor_ThrowsPropertySegmentArgumentNull_IfMissPropertySegment() { - [Fact] - public void Constructor_ThrowsPropertySegmentArgumentNull_IfMissPropertySegment() - { - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new SelectExpandIncludedProperty(null, null), - "propertySegment"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new SelectExpandIncludedProperty(null, null), + "propertySegment"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/SelectExpandPathExtensionsTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/SelectExpandPathExtensionsTest.cs index 77b07380b..9544d677e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/SelectExpandPathExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/SelectExpandPathExtensionsTest.cs @@ -14,192 +14,191 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query +namespace Microsoft.AspNetCore.OData.Tests.Query; + +public class SelectExpandPathExtensionsTest { - public class SelectExpandPathExtensionsTest + [Fact] + public void GetFirstNonTypeCastSegment_SelectPath_ThrowsArgumentNull() { - [Fact] - public void GetFirstNonTypeCastSegment_SelectPath_ThrowsArgumentNull() - { - // Arrange & Act - ODataSelectPath selectPath = null; - IList remainingSegments; + // Arrange & Act + ODataSelectPath selectPath = null; + IList remainingSegments; - // Assert - ExceptionAssert.ThrowsArgumentNull(() => selectPath.GetFirstNonTypeCastSegment(out remainingSegments), "selectPath"); - } + // Assert + ExceptionAssert.ThrowsArgumentNull(() => selectPath.GetFirstNonTypeCastSegment(out remainingSegments), "selectPath"); + } - [Fact] - public void GetFirstNonTypeCastSegment_ExpandPath_ThrowsArgumentNull() - { - // Arrange & Act - ODataExpandPath expandPath = null; - IList remainingSegments; + [Fact] + public void GetFirstNonTypeCastSegment_ExpandPath_ThrowsArgumentNull() + { + // Arrange & Act + ODataExpandPath expandPath = null; + IList remainingSegments; - // Assert - ExceptionAssert.ThrowsArgumentNull(() => expandPath.GetFirstNonTypeCastSegment(out remainingSegments), "expandPath"); - } + // Assert + ExceptionAssert.ThrowsArgumentNull(() => expandPath.GetFirstNonTypeCastSegment(out remainingSegments), "expandPath"); + } - [Fact] - public void GetFirstNonTypeCastSegment_ThrowsForUnsupportedMiddleSegmentInSelectPath() - { - // Arrange & Act - ODataSelectPath selectPath = new ODataSelectPath(new DynamicPathSegment("abc"), new DynamicPathSegment("xyz")); - IList remainingSegments; + [Fact] + public void GetFirstNonTypeCastSegment_ThrowsForUnsupportedMiddleSegmentInSelectPath() + { + // Arrange & Act + ODataSelectPath selectPath = new ODataSelectPath(new DynamicPathSegment("abc"), new DynamicPathSegment("xyz")); + IList remainingSegments; - // Assert - ExceptionAssert.Throws(() => selectPath.GetFirstNonTypeCastSegment(out remainingSegments), - String.Format(SRResources.InvalidSegmentInSelectExpandPath, "DynamicPathSegment")); - } + // Assert + ExceptionAssert.Throws(() => selectPath.GetFirstNonTypeCastSegment(out remainingSegments), + String.Format(SRResources.InvalidSegmentInSelectExpandPath, "DynamicPathSegment")); + } - [Fact] - public void GetFirstNonTypeCastSegment_ThrowsForUnsupportedLastSegmentInSelectPath() - { - // Arrange & Act - IEdmType stringType = EdmCoreModel.Instance.GetString(false).Definition; - ODataSelectPath selectPath = new ODataSelectPath(new TypeSegment(stringType, null), new TypeSegment(stringType, null)); - IList remainingSegments; - - // Assert - ExceptionAssert.Throws(() => selectPath.GetFirstNonTypeCastSegment(out remainingSegments), - String.Format(SRResources.InvalidLastSegmentInSelectExpandPath, "TypeSegment")); - } - - [Fact] - public void GetFirstNonTypeCastSegment_WorksForSelectPathWithFirstNonTypeSegmentAtBegin() - { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "Customer"); - IEdmStructuralProperty property = entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String); + [Fact] + public void GetFirstNonTypeCastSegment_ThrowsForUnsupportedLastSegmentInSelectPath() + { + // Arrange & Act + IEdmType stringType = EdmCoreModel.Instance.GetString(false).Definition; + ODataSelectPath selectPath = new ODataSelectPath(new TypeSegment(stringType, null), new TypeSegment(stringType, null)); + IList remainingSegments; + + // Assert + ExceptionAssert.Throws(() => selectPath.GetFirstNonTypeCastSegment(out remainingSegments), + String.Format(SRResources.InvalidLastSegmentInSelectExpandPath, "TypeSegment")); + } + + [Fact] + public void GetFirstNonTypeCastSegment_WorksForSelectPathWithFirstNonTypeSegmentAtBegin() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "Customer"); + IEdmStructuralProperty property = entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String); - ODataPathSegment firstPropertySegment = new PropertySegment(property); - ODataPathSegment secondPropertySegment = new PropertySegment(property); - ODataSelectPath selectPath = new ODataSelectPath(firstPropertySegment, secondPropertySegment); + ODataPathSegment firstPropertySegment = new PropertySegment(property); + ODataPathSegment secondPropertySegment = new PropertySegment(property); + ODataSelectPath selectPath = new ODataSelectPath(firstPropertySegment, secondPropertySegment); - // Act - IList remainingSegments; - ODataPathSegment firstNonTypeSegment = selectPath.GetFirstNonTypeCastSegment(out remainingSegments); + // Act + IList remainingSegments; + ODataPathSegment firstNonTypeSegment = selectPath.GetFirstNonTypeCastSegment(out remainingSegments); - // Assert - Assert.NotNull(firstNonTypeSegment); - Assert.Same(firstPropertySegment, firstNonTypeSegment); + // Assert + Assert.NotNull(firstNonTypeSegment); + Assert.Same(firstPropertySegment, firstNonTypeSegment); - Assert.NotNull(remainingSegments); - Assert.Same(secondPropertySegment, Assert.Single(remainingSegments)); - } + Assert.NotNull(remainingSegments); + Assert.Same(secondPropertySegment, Assert.Single(remainingSegments)); + } - [Fact] - public void GetFirstNonTypeCastSegment_WorksForSelectPathWithFirstNonTypeSegmentAtMiddle() - { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "Customer"); - IEdmStructuralProperty property = entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String); - - ODataPathSegment firstTypeSegment = new TypeSegment(entityType, null); - ODataPathSegment secondTypeSegment = new TypeSegment(entityType, null); - ODataPathSegment firstPropertySegment = new PropertySegment(property); - ODataPathSegment secondPropertySegment = new PropertySegment(property); - ODataSelectPath selectPath = new ODataSelectPath( - firstTypeSegment, - secondTypeSegment, - firstPropertySegment, // here - secondPropertySegment); - - // Act - IList remainingSegments; - ODataPathSegment firstNonTypeSegment = selectPath.GetFirstNonTypeCastSegment(out remainingSegments); - - // Assert - Assert.NotNull(firstNonTypeSegment); - Assert.Same(firstPropertySegment, firstNonTypeSegment); - - Assert.NotNull(remainingSegments); - Assert.Same(secondPropertySegment, Assert.Single(remainingSegments)); - } - - [Fact] - public void GetFirstNonTypeCastSegment_WorksForSelectPathWithFirstNonTypeSegmentAtLast() - { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "Customer"); - IEdmStructuralProperty property = entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String); - - ODataPathSegment firstTypeSegment = new TypeSegment(entityType, null); - ODataPathSegment secondTypeSegment = new TypeSegment(entityType, null); - ODataPathSegment firstPropertySegment = new PropertySegment(property); - ODataSelectPath selectPath = new ODataSelectPath( - firstTypeSegment, - secondTypeSegment, - firstPropertySegment); - - // Act - IList remainingSegments; - ODataPathSegment firstNonTypeSegment = selectPath.GetFirstNonTypeCastSegment(out remainingSegments); - - // Assert - Assert.NotNull(firstNonTypeSegment); - Assert.Same(firstPropertySegment, firstNonTypeSegment); - - Assert.Null(remainingSegments); - } - - [Fact] - public void GetFirstNonTypeCastSegment_WorksForExpandPathOnlyWithTypeAndNavigationSegment() + [Fact] + public void GetFirstNonTypeCastSegment_WorksForSelectPathWithFirstNonTypeSegmentAtMiddle() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "Customer"); + IEdmStructuralProperty property = entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String); + + ODataPathSegment firstTypeSegment = new TypeSegment(entityType, null); + ODataPathSegment secondTypeSegment = new TypeSegment(entityType, null); + ODataPathSegment firstPropertySegment = new PropertySegment(property); + ODataPathSegment secondPropertySegment = new PropertySegment(property); + ODataSelectPath selectPath = new ODataSelectPath( + firstTypeSegment, + secondTypeSegment, + firstPropertySegment, // here + secondPropertySegment); + + // Act + IList remainingSegments; + ODataPathSegment firstNonTypeSegment = selectPath.GetFirstNonTypeCastSegment(out remainingSegments); + + // Assert + Assert.NotNull(firstNonTypeSegment); + Assert.Same(firstPropertySegment, firstNonTypeSegment); + + Assert.NotNull(remainingSegments); + Assert.Same(secondPropertySegment, Assert.Single(remainingSegments)); + } + + [Fact] + public void GetFirstNonTypeCastSegment_WorksForSelectPathWithFirstNonTypeSegmentAtLast() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "Customer"); + IEdmStructuralProperty property = entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String); + + ODataPathSegment firstTypeSegment = new TypeSegment(entityType, null); + ODataPathSegment secondTypeSegment = new TypeSegment(entityType, null); + ODataPathSegment firstPropertySegment = new PropertySegment(property); + ODataSelectPath selectPath = new ODataSelectPath( + firstTypeSegment, + secondTypeSegment, + firstPropertySegment); + + // Act + IList remainingSegments; + ODataPathSegment firstNonTypeSegment = selectPath.GetFirstNonTypeCastSegment(out remainingSegments); + + // Assert + Assert.NotNull(firstNonTypeSegment); + Assert.Same(firstPropertySegment, firstNonTypeSegment); + + Assert.Null(remainingSegments); + } + + [Fact] + public void GetFirstNonTypeCastSegment_WorksForExpandPathOnlyWithTypeAndNavigationSegment() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "Customer"); + var navProperty = entityType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "Customer"); - var navProperty = entityType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "Nav", - Target = entityType, - TargetMultiplicity = EdmMultiplicity.One - }); - EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); - EdmEntitySet set = new EdmEntitySet(container, "set", entityType); - NavigationPropertySegment navSegment = new NavigationPropertySegment(navProperty, set); - TypeSegment typeSegment = new TypeSegment(entityType, null); - ODataExpandPath expandPath = new ODataExpandPath(typeSegment, navSegment); - - // Act - IList remainingSegments; - ODataPathSegment firstNonTypeSegment = expandPath.GetFirstNonTypeCastSegment(out remainingSegments); - - // Assert - Assert.NotNull(firstNonTypeSegment); - Assert.Same(navSegment, firstNonTypeSegment); - - Assert.Null(remainingSegments); - } - - [Fact] - public void GetFirstNonTypeCastSegment_WorksForExpandPathWithPropertySegment() + Name = "Nav", + Target = entityType, + TargetMultiplicity = EdmMultiplicity.One + }); + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + EdmEntitySet set = new EdmEntitySet(container, "set", entityType); + NavigationPropertySegment navSegment = new NavigationPropertySegment(navProperty, set); + TypeSegment typeSegment = new TypeSegment(entityType, null); + ODataExpandPath expandPath = new ODataExpandPath(typeSegment, navSegment); + + // Act + IList remainingSegments; + ODataPathSegment firstNonTypeSegment = expandPath.GetFirstNonTypeCastSegment(out remainingSegments); + + // Assert + Assert.NotNull(firstNonTypeSegment); + Assert.Same(navSegment, firstNonTypeSegment); + + Assert.Null(remainingSegments); + } + + [Fact] + public void GetFirstNonTypeCastSegment_WorksForExpandPathWithPropertySegment() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "Customer"); + IEdmStructuralProperty property = entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String); + var navProperty = entityType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "Customer"); - IEdmStructuralProperty property = entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String); - var navProperty = entityType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "Nav", - Target = entityType, - TargetMultiplicity = EdmMultiplicity.One - }); - EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); - EdmEntitySet set = new EdmEntitySet(container, "set", entityType); - NavigationPropertySegment navSegment = new NavigationPropertySegment(navProperty, set); - TypeSegment typeSegment = new TypeSegment(entityType, null); - ODataPathSegment propertySegment = new PropertySegment(property); - ODataExpandPath expandPath = new ODataExpandPath(typeSegment, propertySegment, navSegment); - - // Act - IList remainingSegments; - ODataPathSegment firstNonTypeSegment = expandPath.GetFirstNonTypeCastSegment(out remainingSegments); - - // Assert - Assert.NotNull(firstNonTypeSegment); - Assert.Same(propertySegment, firstNonTypeSegment); - - Assert.NotNull(remainingSegments); - Assert.Same(navSegment, Assert.Single(remainingSegments)); - } + Name = "Nav", + Target = entityType, + TargetMultiplicity = EdmMultiplicity.One + }); + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + EdmEntitySet set = new EdmEntitySet(container, "set", entityType); + NavigationPropertySegment navSegment = new NavigationPropertySegment(navProperty, set); + TypeSegment typeSegment = new TypeSegment(entityType, null); + ODataPathSegment propertySegment = new PropertySegment(property); + ODataExpandPath expandPath = new ODataExpandPath(typeSegment, propertySegment, navSegment); + + // Act + IList remainingSegments; + ODataPathSegment firstNonTypeSegment = expandPath.GetFirstNonTypeCastSegment(out remainingSegments); + + // Assert + Assert.NotNull(firstNonTypeSegment); + Assert.Same(propertySegment, firstNonTypeSegment); + + Assert.NotNull(remainingSegments); + Assert.Same(navSegment, Assert.Single(remainingSegments)); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ComputeQueryValidatorTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ComputeQueryValidatorTests.cs index 610e5f205..426dc9647 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ComputeQueryValidatorTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ComputeQueryValidatorTests.cs @@ -11,49 +11,48 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Validator +namespace Microsoft.AspNetCore.OData.Tests.Query.Validator; + +public class ComputeQueryValidatorTests { - public class ComputeQueryValidatorTests - { - private ComputeQueryValidator _validator = new ComputeQueryValidator(); - private ODataQueryContext _context = ValidationTestHelper.CreateCustomerContext(); + private ComputeQueryValidator _validator = new ComputeQueryValidator(); + private ODataQueryContext _context = ValidationTestHelper.CreateCustomerContext(); - [Fact] - public void ValidateComputeQueryValidator_ThrowsOnNullOption() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => _validator.Validate(null, new ODataValidationSettings()), "computeQueryOption"); - } + [Fact] + public void ValidateComputeQueryValidator_ThrowsOnNullOption() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => _validator.Validate(null, new ODataValidationSettings()), "computeQueryOption"); + } - [Fact] - public void ValidateComputeQueryValidator_ThrowsOnNullSettings() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => _validator.Validate(new ComputeQueryOption("substring(Name, 0, 1) as FirstChar", _context), null), "validationSettings"); - } + [Fact] + public void ValidateComputeQueryValidator_ThrowsOnNullSettings() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => _validator.Validate(new ComputeQueryOption("substring(Name, 0, 1) as FirstChar", _context), null), "validationSettings"); + } - [Fact] - public void ValidateComputeQueryValidator_ThrowsIfWithoutAsInComputeClause() - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => - _validator.Validate( - new ComputeQueryOption("test add p12m", _context), - new ODataValidationSettings()), - "'as' expected at position 13 in 'test add p12m'."); - } + [Fact] + public void ValidateComputeQueryValidator_ThrowsIfWithoutAsInComputeClause() + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => + _validator.Validate( + new ComputeQueryOption("test add p12m", _context), + new ODataValidationSettings()), + "'as' expected at position 13 in 'test add p12m'."); + } - [Fact] - public void ValidateComputeQueryValidator_ThrowsIfUnknownPropertyInComputeClause() - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => - _validator.Validate( - new ComputeQueryOption("test add p12m as Any", _context), - new ODataValidationSettings()), - "Could not find a property named 'test' on type 'Microsoft.AspNetCore.OData.Tests.Query.Models.QueryCompositionCustomer'."); - } + [Fact] + public void ValidateComputeQueryValidator_ThrowsIfUnknownPropertyInComputeClause() + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => + _validator.Validate( + new ComputeQueryOption("test add p12m as Any", _context), + new ODataValidationSettings()), + "Could not find a property named 'test' on type 'Microsoft.AspNetCore.OData.Tests.Query.Models.QueryCompositionCustomer'."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/CountQueryValidatorTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/CountQueryValidatorTests.cs index 6dd150b56..37a6fba6c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/CountQueryValidatorTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/CountQueryValidatorTests.cs @@ -16,117 +16,116 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Validator +namespace Microsoft.AspNetCore.OData.Tests.Query.Validator; + +public class CountQueryValidatorTests { - public class CountQueryValidatorTests + private readonly CountQueryValidator _validator; + + public CountQueryValidatorTests() + { + _validator = new CountQueryValidator(); + } + + [Fact] + public void ValidateCountQueryValidator_Throws_NullOption() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => _validator.Validate(null, new ODataValidationSettings()), "countQueryOption"); + } + + [Fact] + public void ValidateCountQueryValidator_Throws_NullSettings() + { + // Arrange + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + CountQueryOption option = new CountQueryOption("true", context); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => _validator.Validate(option, null), "validationSettings"); + } + + [Theory] + [InlineData("LimitedEntities(1)/Integers", "The property 'Integers' cannot be used for $count.")] + [InlineData("LimitedEntities(1)/ComplexCollectionProperty", "The property 'ComplexCollectionProperty' cannot be used for $count.")] + [InlineData("LimitedEntities(1)/EntityCollectionProperty", "The property 'EntityCollectionProperty' cannot be used for $count.")] + [InlineData("LimitedEntities(1)/ComplexProperty/Strings", "The property 'Strings' cannot be used for $count.")] + [InlineData("LimitedEntities(1)/ComplexProperty/SimpleEnums", "The property 'SimpleEnums' cannot be used for $count.")] + [InlineData("LimitedEntities(1)/EntityCollectionProperty(1)/ComplexCollectionProperty", "The property 'ComplexCollectionProperty' cannot be used for $count.")] + [InlineData("LimitedEntities(1)/Integers/$count", "The property 'Integers' cannot be used for $count.")] + [InlineData("LimitedEntities(1)/ComplexCollectionProperty/$count", "The property 'ComplexCollectionProperty' cannot be used for $count.")] + [InlineData("LimitedEntities(1)/EntityCollectionProperty/$count", "The property 'EntityCollectionProperty' cannot be used for $count.")] + [InlineData("LimitedEntities(1)/ComplexProperty/Strings/$count", "The property 'Strings' cannot be used for $count.")] + [InlineData("LimitedEntities(1)/ComplexProperty/SimpleEnums/$count", "The property 'SimpleEnums' cannot be used for $count.")] + [InlineData("LimitedEntities(1)/EntityCollectionProperty(1)/ComplexCollectionProperty/$count", "The property 'ComplexCollectionProperty' cannot be used for $count.")] + public void Validate_Throws_DollarCountAppliedOnNotCountableCollection(string uri, string message) + { + // Arrange + IEdmModel model = GetEdmModel(); + string serviceRoot = "http://localhost/"; + ODataUriParser pathHandler = new ODataUriParser(model, new Uri(serviceRoot), new Uri(uri, UriKind.RelativeOrAbsolute)); + ODataPath path = pathHandler.ParsePath(); + ODataQueryContext context = new ODataQueryContext(model, EdmCoreModel.Instance.GetInt32(false).Definition, path); + CountQueryOption option = new CountQueryOption("true", context); + ODataValidationSettings settings = new ODataValidationSettings(); + + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(option, settings), message); + } + + private static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataModelBuilder(); + + // Configure LimitedEntity + EntitySetConfiguration limitedEntities = builder.EntitySet("LimitedEntities"); + limitedEntities.EntityType.HasKey(p => p.Id); + limitedEntities.EntityType.ComplexProperty(c => c.ComplexProperty); + limitedEntities.EntityType.CollectionProperty(c => c.ComplexCollectionProperty).IsNotCountable(); + limitedEntities.EntityType.HasMany(l => l.EntityCollectionProperty).IsNotCountable(); + limitedEntities.EntityType.CollectionProperty(cp => cp.Integers).IsNotCountable(); + + // Configure LimitedRelatedEntity + EntitySetConfiguration limitedRelatedEntities = + builder.EntitySet("LimitedRelatedEntities"); + limitedRelatedEntities.EntityType.HasKey(p => p.Id); + limitedRelatedEntities.EntityType.CollectionProperty(p => p.ComplexCollectionProperty).IsNotCountable(); + + // Configure Complextype + ComplexTypeConfiguration complexType = builder.ComplexType(); + complexType.CollectionProperty(p => p.Strings).IsNotCountable(); + complexType.Property(p => p.Value); + complexType.CollectionProperty(p => p.SimpleEnums).IsNotCountable(); + + // Configure EnumType + EnumTypeConfiguration enumType = builder.EnumType(); + enumType.Member(SimpleEnum.First); + enumType.Member(SimpleEnum.Second); + enumType.Member(SimpleEnum.Third); + enumType.Member(SimpleEnum.Fourth); + + return builder.GetEdmModel(); + } + + private class LimitedEntity + { + public int Id { get; set; } + public LimitedComplex ComplexProperty { get; set; } + public LimitedComplex[] ComplexCollectionProperty { get; set; } + public IEnumerable EntityCollectionProperty { get; set; } + public ICollection Integers { get; set; } + } + + private class LimitedComplex + { + public int Value { get; set; } + public string[] Strings { get; set; } + public IList SimpleEnums { get; set; } + } + + private class LimitedRelatedEntity { - private readonly CountQueryValidator _validator; - - public CountQueryValidatorTests() - { - _validator = new CountQueryValidator(); - } - - [Fact] - public void ValidateCountQueryValidator_Throws_NullOption() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => _validator.Validate(null, new ODataValidationSettings()), "countQueryOption"); - } - - [Fact] - public void ValidateCountQueryValidator_Throws_NullSettings() - { - // Arrange - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - CountQueryOption option = new CountQueryOption("true", context); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => _validator.Validate(option, null), "validationSettings"); - } - - [Theory] - [InlineData("LimitedEntities(1)/Integers", "The property 'Integers' cannot be used for $count.")] - [InlineData("LimitedEntities(1)/ComplexCollectionProperty", "The property 'ComplexCollectionProperty' cannot be used for $count.")] - [InlineData("LimitedEntities(1)/EntityCollectionProperty", "The property 'EntityCollectionProperty' cannot be used for $count.")] - [InlineData("LimitedEntities(1)/ComplexProperty/Strings", "The property 'Strings' cannot be used for $count.")] - [InlineData("LimitedEntities(1)/ComplexProperty/SimpleEnums", "The property 'SimpleEnums' cannot be used for $count.")] - [InlineData("LimitedEntities(1)/EntityCollectionProperty(1)/ComplexCollectionProperty", "The property 'ComplexCollectionProperty' cannot be used for $count.")] - [InlineData("LimitedEntities(1)/Integers/$count", "The property 'Integers' cannot be used for $count.")] - [InlineData("LimitedEntities(1)/ComplexCollectionProperty/$count", "The property 'ComplexCollectionProperty' cannot be used for $count.")] - [InlineData("LimitedEntities(1)/EntityCollectionProperty/$count", "The property 'EntityCollectionProperty' cannot be used for $count.")] - [InlineData("LimitedEntities(1)/ComplexProperty/Strings/$count", "The property 'Strings' cannot be used for $count.")] - [InlineData("LimitedEntities(1)/ComplexProperty/SimpleEnums/$count", "The property 'SimpleEnums' cannot be used for $count.")] - [InlineData("LimitedEntities(1)/EntityCollectionProperty(1)/ComplexCollectionProperty/$count", "The property 'ComplexCollectionProperty' cannot be used for $count.")] - public void Validate_Throws_DollarCountAppliedOnNotCountableCollection(string uri, string message) - { - // Arrange - IEdmModel model = GetEdmModel(); - string serviceRoot = "http://localhost/"; - ODataUriParser pathHandler = new ODataUriParser(model, new Uri(serviceRoot), new Uri(uri, UriKind.RelativeOrAbsolute)); - ODataPath path = pathHandler.ParsePath(); - ODataQueryContext context = new ODataQueryContext(model, EdmCoreModel.Instance.GetInt32(false).Definition, path); - CountQueryOption option = new CountQueryOption("true", context); - ODataValidationSettings settings = new ODataValidationSettings(); - - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(option, settings), message); - } - - private static IEdmModel GetEdmModel() - { - ODataModelBuilder builder = new ODataModelBuilder(); - - // Configure LimitedEntity - EntitySetConfiguration limitedEntities = builder.EntitySet("LimitedEntities"); - limitedEntities.EntityType.HasKey(p => p.Id); - limitedEntities.EntityType.ComplexProperty(c => c.ComplexProperty); - limitedEntities.EntityType.CollectionProperty(c => c.ComplexCollectionProperty).IsNotCountable(); - limitedEntities.EntityType.HasMany(l => l.EntityCollectionProperty).IsNotCountable(); - limitedEntities.EntityType.CollectionProperty(cp => cp.Integers).IsNotCountable(); - - // Configure LimitedRelatedEntity - EntitySetConfiguration limitedRelatedEntities = - builder.EntitySet("LimitedRelatedEntities"); - limitedRelatedEntities.EntityType.HasKey(p => p.Id); - limitedRelatedEntities.EntityType.CollectionProperty(p => p.ComplexCollectionProperty).IsNotCountable(); - - // Configure Complextype - ComplexTypeConfiguration complexType = builder.ComplexType(); - complexType.CollectionProperty(p => p.Strings).IsNotCountable(); - complexType.Property(p => p.Value); - complexType.CollectionProperty(p => p.SimpleEnums).IsNotCountable(); - - // Configure EnumType - EnumTypeConfiguration enumType = builder.EnumType(); - enumType.Member(SimpleEnum.First); - enumType.Member(SimpleEnum.Second); - enumType.Member(SimpleEnum.Third); - enumType.Member(SimpleEnum.Fourth); - - return builder.GetEdmModel(); - } - - private class LimitedEntity - { - public int Id { get; set; } - public LimitedComplex ComplexProperty { get; set; } - public LimitedComplex[] ComplexCollectionProperty { get; set; } - public IEnumerable EntityCollectionProperty { get; set; } - public ICollection Integers { get; set; } - } - - private class LimitedComplex - { - public int Value { get; set; } - public string[] Strings { get; set; } - public IList SimpleEnums { get; set; } - } - - private class LimitedRelatedEntity - { - public int Id { get; set; } - public LimitedComplex[] ComplexCollectionProperty { get; set; } - } + public int Id { get; set; } + public LimitedComplex[] ComplexCollectionProperty { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/FilterQueryValidatorTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/FilterQueryValidatorTests.cs index e4094997c..6e384dee6 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/FilterQueryValidatorTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/FilterQueryValidatorTests.cs @@ -17,1444 +17,1443 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Validator +namespace Microsoft.AspNetCore.OData.Tests.Query.Validator; + +public class FilterQueryValidatorTests { - public class FilterQueryValidatorTests + private MyFilterValidator _validator; + private ODataValidationSettings _settings = new ODataValidationSettings(); + private ODataQueryContext _context; + private ODataQueryContext _productContext; + + public FilterQueryValidatorTests() { - private MyFilterValidator _validator; - private ODataValidationSettings _settings = new ODataValidationSettings(); - private ODataQueryContext _context; - private ODataQueryContext _productContext; + _context = ValidationTestHelper.CreateCustomerContext(); + _context.DefaultQueryConfigurations.EnableFilter = true; - public FilterQueryValidatorTests() - { - _context = ValidationTestHelper.CreateCustomerContext(); - _context.DefaultQueryConfigurations.EnableFilter = true; + _productContext = ValidationTestHelper.CreateDerivedProductsContext(); + _validator = new MyFilterValidator(); + } - _productContext = ValidationTestHelper.CreateDerivedProductsContext(); - _validator = new MyFilterValidator(); - } + [Fact] + public void ValidateFilterQueryValidator_ThrowsOnNullOption() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => _validator.Validate(null, new ODataValidationSettings()), "filterQueryOption"); + } - [Fact] - public void ValidateFilterQueryValidator_ThrowsOnNullOption() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => _validator.Validate(null, new ODataValidationSettings()), "filterQueryOption"); - } + [Fact] + public void ValidateFilterQueryValidator_ThrowsOnNullSettings() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => _validator.Validate(new FilterQueryOption("Name eq 'abc'", _context), null), "settings"); + } - [Fact] - public void ValidateFilterQueryValidator_ThrowsOnNullSettings() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => _validator.Validate(new FilterQueryOption("Name eq 'abc'", _context), null), "settings"); - } + // want to test if all the virtual methods are being invoked correctly + [Fact] + public void ValidateFilterQueryValidator_VisitAll() + { + // Arrange + FilterQueryOption option = new FilterQueryOption("Tags/all(t: t eq '42')", _context); + + // Act + _validator.Validate(option, _settings); + + // Assert + Assert.Equal(7, _validator.Times.Keys.Count); + Assert.Equal(1, _validator.Times["Validate"]); // entry + Assert.Equal(1, _validator.Times["ValidateAllQueryNode"]); // all + Assert.Equal(1, _validator.Times["ValidateLogicalOperator"]); // eq + Assert.Equal(1, _validator.Times["ValidateCollectionPropertyAccessNode"]); // Tags + Assert.Equal(1, _validator.Times["ValidateConstantQueryNode"]); // 42 + Assert.Equal(1, _validator.Times["ValidateBinaryOperatorQueryNode"]); // eq + Assert.Equal(2, _validator.Times["ValidateParameterQueryNode"]); // $it, t + } - // want to test if all the virtual methods are being invoked correctly - [Fact] - public void ValidateFilterQueryValidator_VisitAll() - { - // Arrange - FilterQueryOption option = new FilterQueryOption("Tags/all(t: t eq '42')", _context); - - // Act - _validator.Validate(option, _settings); - - // Assert - Assert.Equal(7, _validator.Times.Keys.Count); - Assert.Equal(1, _validator.Times["Validate"]); // entry - Assert.Equal(1, _validator.Times["ValidateAllQueryNode"]); // all - Assert.Equal(1, _validator.Times["ValidateLogicalOperator"]); // eq - Assert.Equal(1, _validator.Times["ValidateCollectionPropertyAccessNode"]); // Tags - Assert.Equal(1, _validator.Times["ValidateConstantQueryNode"]); // 42 - Assert.Equal(1, _validator.Times["ValidateBinaryOperatorQueryNode"]); // eq - Assert.Equal(2, _validator.Times["ValidateParameterQueryNode"]); // $it, t - } + [Fact] + public void ValidateFilterQueryValidator_VisitAny() + { + // Arrange + FilterQueryOption option = new FilterQueryOption("Tags/any(t: t eq '42')", _context); + + // Act + _validator.Validate(option, _settings); + + // Assert + Assert.Equal(7, _validator.Times.Keys.Count); + Assert.Equal(1, _validator.Times["Validate"]); // entry + Assert.Equal(1, _validator.Times["ValidateAnyQueryNode"]); // all + Assert.Equal(1, _validator.Times["ValidateLogicalOperator"]); // eq + Assert.Equal(1, _validator.Times["ValidateCollectionPropertyAccessNode"]); // Tags + Assert.Equal(1, _validator.Times["ValidateConstantQueryNode"]); // 42 + Assert.Equal(1, _validator.Times["ValidateBinaryOperatorQueryNode"]); // eq + Assert.Equal(2, _validator.Times["ValidateParameterQueryNode"]); // $it, t + } - [Fact] - public void ValidateFilterQueryValidator_VisitAny() - { - // Arrange - FilterQueryOption option = new FilterQueryOption("Tags/any(t: t eq '42')", _context); + [Theory] + [InlineData("NotFilterableProperty")] + [InlineData("NonFilterableProperty")] + public void ValidateFilterQueryValidator_ThrowsIfNotFilterableProperty(string property) + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => + _validator.Validate( + new FilterQueryOption(string.Format("{0} eq 'David'", property), _context), + new ODataValidationSettings()), + string.Format("The property '{0}' cannot be used in the $filter query option.", property)); + } - // Act - _validator.Validate(option, _settings); + [Theory] + [InlineData("NotFilterableNavigationProperty")] + [InlineData("NonFilterableNavigationProperty")] + public void ValidateFilterQueryValidator_ThrowsIfNotFilterableNavigationProperty(string property) + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => + _validator.Validate( + new FilterQueryOption(string.Format("{0}/Name eq 'Seattle'", property), _context), + new ODataValidationSettings()), + string.Format("The property '{0}' cannot be used in the $filter query option.", property)); + } - // Assert - Assert.Equal(7, _validator.Times.Keys.Count); - Assert.Equal(1, _validator.Times["Validate"]); // entry - Assert.Equal(1, _validator.Times["ValidateAnyQueryNode"]); // all - Assert.Equal(1, _validator.Times["ValidateLogicalOperator"]); // eq - Assert.Equal(1, _validator.Times["ValidateCollectionPropertyAccessNode"]); // Tags - Assert.Equal(1, _validator.Times["ValidateConstantQueryNode"]); // 42 - Assert.Equal(1, _validator.Times["ValidateBinaryOperatorQueryNode"]); // eq - Assert.Equal(2, _validator.Times["ValidateParameterQueryNode"]); // $it, t - } + [Theory] + [InlineData("NotFilterableProperty")] + [InlineData("NonFilterableProperty")] + public void ValidateFilterQueryValidator_ThrowsIfNavigationHasNotFilterableProperty(string property) + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => + _validator.Validate( + new FilterQueryOption(string.Format("NavigationWithNotFilterableProperty/{0} eq 'David'", property), _context), + new ODataValidationSettings()), + string.Format("The property '{0}' cannot be used in the $filter query option.", property)); + } - [Theory] - [InlineData("NotFilterableProperty")] - [InlineData("NonFilterableProperty")] - public void ValidateFilterQueryValidator_ThrowsIfNotFilterableProperty(string property) - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => - _validator.Validate( - new FilterQueryOption(string.Format("{0} eq 'David'", property), _context), - new ODataValidationSettings()), - string.Format("The property '{0}' cannot be used in the $filter query option.", property)); + public static TheoryDataSet NestedAnyAllInputs + { + get + { + return new TheoryDataSet + { + "Category/QueryableProducts/any(P: P/Category/EnumerableProducts/any(PP: PP/ProductName eq 'Snacks'))", + "Category/QueryableProducts/all(P: P/Category/EnumerableProducts/all(PP: PP/ProductName eq 'Snacks'))", + "Category/QueryableProducts/any(P: P/Category/EnumerableProducts/all(PP: PP/ProductName eq 'Snacks'))", + "Category/QueryableProducts/all(P: P/Category/EnumerableProducts/any(PP: PP/ProductName eq 'Snacks'))", + }; } + } - [Theory] - [InlineData("NotFilterableNavigationProperty")] - [InlineData("NonFilterableNavigationProperty")] - public void ValidateFilterQueryValidator_ThrowsIfNotFilterableNavigationProperty(string property) - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => - _validator.Validate( - new FilterQueryOption(string.Format("{0}/Name eq 'Seattle'", property), _context), - new ODataValidationSettings()), - string.Format("The property '{0}' cannot be used in the $filter query option.", property)); - } + [Theory] + [MemberData(nameof(NestedAnyAllInputs))] + public void MaxAnyAllExpressionDepthLimitExceeded(string filter) + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings(); + settings.MaxAnyAllExpressionDepth = 1; + + // Act & Assert + ExceptionAssert.Throws( + () => _validator.Validate(new FilterQueryOption(filter, _productContext), settings), + "The Any/All nesting limit of '1' has been exceeded. 'MaxAnyAllExpressionDepth' can be configured on ODataQuerySettings or EnableQueryAttribute."); + } - [Theory] - [InlineData("NotFilterableProperty")] - [InlineData("NonFilterableProperty")] - public void ValidateFilterQueryValidator_ThrowsIfNavigationHasNotFilterableProperty(string property) - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => - _validator.Validate( - new FilterQueryOption(string.Format("NavigationWithNotFilterableProperty/{0} eq 'David'", property), _context), - new ODataValidationSettings()), - string.Format("The property '{0}' cannot be used in the $filter query option.", property)); - } + [Theory] + [MemberData(nameof(NestedAnyAllInputs))] + public void IncreaseMaxAnyAllExpressionDepthWillAllowNestedAnyAllInputs(string filter) + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings(); + settings.MaxAnyAllExpressionDepth = 2; + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(new FilterQueryOption(filter, _productContext), settings)); + } - public static TheoryDataSet NestedAnyAllInputs + public static TheoryDataSet LongInputs + { + get { - get - { - return new TheoryDataSet - { - "Category/QueryableProducts/any(P: P/Category/EnumerableProducts/any(PP: PP/ProductName eq 'Snacks'))", - "Category/QueryableProducts/all(P: P/Category/EnumerableProducts/all(PP: PP/ProductName eq 'Snacks'))", - "Category/QueryableProducts/any(P: P/Category/EnumerableProducts/all(PP: PP/ProductName eq 'Snacks'))", - "Category/QueryableProducts/all(P: P/Category/EnumerableProducts/any(PP: PP/ProductName eq 'Snacks'))", - }; - } + return GetLongInputsTestData(100); } + } - [Theory] - [MemberData(nameof(NestedAnyAllInputs))] - public void MaxAnyAllExpressionDepthLimitExceeded(string filter) + [Theory] + [MemberData(nameof(LongInputs))] + public void LongInputs_CauseMaxNodeCountExceededException(string filter) + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings(); - settings.MaxAnyAllExpressionDepth = 1; + MaxAnyAllExpressionDepth = Int32.MaxValue + }; - // Act & Assert - ExceptionAssert.Throws( - () => _validator.Validate(new FilterQueryOption(filter, _productContext), settings), - "The Any/All nesting limit of '1' has been exceeded. 'MaxAnyAllExpressionDepth' can be configured on ODataQuerySettings or EnableQueryAttribute."); - } + FilterQueryOption option = new FilterQueryOption(filter, _productContext); + + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(option, settings), + "The node count limit of '100' has been exceeded. To increase the limit, set the 'MaxNodeCount' property on EnableQueryAttribute or ODataValidationSettings."); + } - [Theory] - [MemberData(nameof(NestedAnyAllInputs))] - public void IncreaseMaxAnyAllExpressionDepthWillAllowNestedAnyAllInputs(string filter) + [Theory] + [MemberData(nameof(LongInputs))] + public void IncreaseMaxNodeCountWillAllowLongInputs(string filter) + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings(); - settings.MaxAnyAllExpressionDepth = 2; + MaxAnyAllExpressionDepth = Int32.MaxValue, + MaxNodeCount = 105, + }; - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(new FilterQueryOption(filter, _productContext), settings)); - } + FilterQueryOption option = new FilterQueryOption(filter, _productContext); + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } - public static TheoryDataSet LongInputs + public static TheoryDataSet CloseToLongInputs + { + get { - get - { - return GetLongInputsTestData(100); - } + return GetLongInputsTestData(95); } + } - [Theory] - [MemberData(nameof(LongInputs))] - public void LongInputs_CauseMaxNodeCountExceededException(string filter) + [Theory] + [MemberData(nameof(CloseToLongInputs))] + public void AlmostLongInputs_DonotCauseMaxNodeCountExceededExceptionOrTimeoutDuringCompilation(string filter) + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings - { - MaxAnyAllExpressionDepth = Int32.MaxValue - }; + MaxAnyAllExpressionDepth = Int32.MaxValue + }; - FilterQueryOption option = new FilterQueryOption(filter, _productContext); + FilterQueryOption option = new FilterQueryOption(filter, _productContext); - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(option, settings), - "The node count limit of '100' has been exceeded. To increase the limit, set the 'MaxNodeCount' property on EnableQueryAttribute or ODataValidationSettings."); - } + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } - [Theory] - [MemberData(nameof(LongInputs))] - public void IncreaseMaxNodeCountWillAllowLongInputs(string filter) + public static TheoryDataSet ArithmeticOperators + { + get { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings + return new TheoryDataSet { - MaxAnyAllExpressionDepth = Int32.MaxValue, - MaxNodeCount = 105, + { AllowedArithmeticOperators.Add, "UnitPrice add 0 eq 23", "Add" }, + { AllowedArithmeticOperators.Divide, "UnitPrice div 23 eq 1", "Divide" }, + { AllowedArithmeticOperators.Modulo, "UnitPrice mod 23 eq 0", "Modulo" }, + { AllowedArithmeticOperators.Multiply, "UnitPrice mul 1 eq 23", "Multiply" }, + { AllowedArithmeticOperators.Subtract, "UnitPrice sub 0 eq 23", "Subtract" }, }; - - FilterQueryOption option = new FilterQueryOption(filter, _productContext); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); } + } - public static TheoryDataSet CloseToLongInputs + [Fact] + public void ArithmeticOperatorsDataSet_CoversAllValues() + { + // Arrange + // Get all values in the AllowedArithmeticOperators enum. + var values = new HashSet( + Enum.GetValues(typeof(AllowedArithmeticOperators)).Cast()); + var groupValues = new[] { - get - { - return GetLongInputsTestData(95); - } - } + AllowedArithmeticOperators.All, + AllowedArithmeticOperators.None, + }; - [Theory] - [MemberData(nameof(CloseToLongInputs))] - public void AlmostLongInputs_DonotCauseMaxNodeCountExceededExceptionOrTimeoutDuringCompilation(string filter) + // Act + // Remove the group items. + foreach (var allowed in groupValues) { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings - { - MaxAnyAllExpressionDepth = Int32.MaxValue - }; - - FilterQueryOption option = new FilterQueryOption(filter, _productContext); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + values.Remove(allowed); } - public static TheoryDataSet ArithmeticOperators + // Remove the individual items. + foreach (var allowed in ArithmeticOperators.Select(item => (AllowedArithmeticOperators)(item[0]))) { - get - { - return new TheoryDataSet - { - { AllowedArithmeticOperators.Add, "UnitPrice add 0 eq 23", "Add" }, - { AllowedArithmeticOperators.Divide, "UnitPrice div 23 eq 1", "Divide" }, - { AllowedArithmeticOperators.Modulo, "UnitPrice mod 23 eq 0", "Modulo" }, - { AllowedArithmeticOperators.Multiply, "UnitPrice mul 1 eq 23", "Multiply" }, - { AllowedArithmeticOperators.Subtract, "UnitPrice sub 0 eq 23", "Subtract" }, - }; - } + values.Remove(allowed); } - [Fact] - public void ArithmeticOperatorsDataSet_CoversAllValues() + // Assert + // Should have nothing left. + Assert.Empty(values); + } + + [Theory] + [MemberData(nameof(ArithmeticOperators))] + public void AllowedArithmeticOperators_SucceedIfAllowed(AllowedArithmeticOperators allow, string query, string unused) + { + // Arrange + var settings = new ODataValidationSettings { - // Arrange - // Get all values in the AllowedArithmeticOperators enum. - var values = new HashSet( - Enum.GetValues(typeof(AllowedArithmeticOperators)).Cast()); - var groupValues = new[] - { - AllowedArithmeticOperators.All, - AllowedArithmeticOperators.None, - }; + AllowedArithmeticOperators = allow, + }; + var option = new FilterQueryOption(query, _productContext); - // Act - // Remove the group items. - foreach (var allowed in groupValues) - { - values.Remove(allowed); - } + // Act & Assert + Assert.NotNull(unused); + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } - // Remove the individual items. - foreach (var allowed in ArithmeticOperators.Select(item => (AllowedArithmeticOperators)(item[0]))) - { - values.Remove(allowed); - } + [Theory] + [MemberData(nameof(ArithmeticOperators))] + public void AllowedArithmeticOperators_ThrowIfNotAllowed(AllowedArithmeticOperators exclude, string query, string operatorName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedArithmeticOperators = AllowedArithmeticOperators.All & ~exclude, + }; + var expectedMessage = string.Format( + "Arithmetic operator '{0}' is not allowed. " + + "To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings.", + operatorName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } - // Assert - // Should have nothing left. - Assert.Empty(values); - } + [Theory] + [MemberData(nameof(ArithmeticOperators))] + public void AllowedArithmeticOperators_ThrowIfNoneAllowed(AllowedArithmeticOperators unused, string query, string operatorName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedArithmeticOperators = AllowedArithmeticOperators.None, + }; + var expectedMessage = string.Format( + "Arithmetic operator '{0}' is not allowed. " + + "To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings.", + operatorName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.NotEqual(unused, settings.AllowedArithmeticOperators); + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } - [Theory] - [MemberData(nameof(ArithmeticOperators))] - public void AllowedArithmeticOperators_SucceedIfAllowed(AllowedArithmeticOperators allow, string query, string unused) + public static TheoryDataSet ArithmeticOperators_CheckArguments + { + get { - // Arrange - var settings = new ODataValidationSettings + return new TheoryDataSet { - AllowedArithmeticOperators = allow, + { "day(DiscontinuedDate) add 0 eq 23" }, + { "day(DiscontinuedDate) div 23 eq 1" }, + { "day(DiscontinuedDate) mod 23 eq 0" }, + { "day(DiscontinuedDate) mul 1 eq 23" }, + { "day(DiscontinuedDate) sub 0 eq 23" }, + { "0 add day(DiscontinuedDate) eq 23" }, + { "23 div day(DiscontinuedDate) eq 1" }, + { "23 mod day(DiscontinuedDate) eq 0" }, + { "1 mul day(DiscontinuedDate) eq 23" }, + { "0 sub day(DiscontinuedDate) eq -23" }, }; - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - Assert.NotNull(unused); - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); } + } - [Theory] - [MemberData(nameof(ArithmeticOperators))] - public void AllowedArithmeticOperators_ThrowIfNotAllowed(AllowedArithmeticOperators exclude, string query, string operatorName) + [Theory] + [MemberData(nameof(ArithmeticOperators_CheckArguments))] + public void ArithmeticOperators_CheckArguments_SucceedIfAllowed(string query) + { + // Arrange + var settings = new ODataValidationSettings { - // Arrange - var settings = new ODataValidationSettings - { - AllowedArithmeticOperators = AllowedArithmeticOperators.All & ~exclude, + AllowedFunctions = AllowedFunctions.Day, + }; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [MemberData(nameof(ArithmeticOperators_CheckArguments))] + public void ArithmeticOperators_CheckArguments_ThrowIfNotAllowed(string query) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.Day, + }; + var expectedMessage = string.Format( + "Function 'day' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings."); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + // No support for OData v4 functions: + // maxdatetime, mindatetime, now, totaloffsetminutes, or totalseconds? + public static TheoryDataSet DateTimeFunctions + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.Day, "day(null) eq 20", "day" }, + { AllowedFunctions.Day, "day(DiscontinuedDate) eq 20", "day" }, + { AllowedFunctions.Hour, "hour(null) eq 10", "hour" }, + { AllowedFunctions.Hour, "hour(DiscontinuedDate) eq 10", "hour" }, + { AllowedFunctions.Minute, "minute(null) eq 20", "minute" }, + { AllowedFunctions.Minute, "minute(DiscontinuedDate) eq 20", "minute" }, + { AllowedFunctions.Month, "month(null) eq 10", "month" }, + { AllowedFunctions.Month, "month(DiscontinuedDate) eq 10", "month" }, + { AllowedFunctions.Second, "second(null) eq 20", "second" }, + { AllowedFunctions.Second, "second(DiscontinuedDate) eq 20", "second" }, + { AllowedFunctions.Year, "year(null) eq 2000", "year" }, + { AllowedFunctions.Year, "year(DiscontinuedDate) eq 2000", "year" }, + { AllowedFunctions.Date, "date(null) eq 2015-02-26", "date" }, + { AllowedFunctions.Date, "date(DiscontinuedDate) eq 2015-02-26", "date" }, + { AllowedFunctions.Time, "time(null) eq 03:4:05.90100", "time" }, + { AllowedFunctions.Time, "time(DiscontinuedDate) eq 03:4:05.90100", "time" }, + { AllowedFunctions.FractionalSeconds, "fractionalseconds(null) eq 0.05", "fractionalseconds" }, + { AllowedFunctions.FractionalSeconds, "fractionalseconds(DiscontinuedDate) eq 0.050", "fractionalseconds" }, }; - var expectedMessage = string.Format( - "Arithmetic operator '{0}' is not allowed. " + - "To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings.", - operatorName); - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); } + } - [Theory] - [MemberData(nameof(ArithmeticOperators))] - public void AllowedArithmeticOperators_ThrowIfNoneAllowed(AllowedArithmeticOperators unused, string query, string operatorName) + public static TheoryDataSet MathFunctions + { + get { - // Arrange - var settings = new ODataValidationSettings + return new TheoryDataSet { - AllowedArithmeticOperators = AllowedArithmeticOperators.None, + { AllowedFunctions.Ceiling, "ceiling(null) eq 0", "ceiling" }, + { AllowedFunctions.Ceiling, "ceiling(Weight) eq 0", "ceiling" }, + { AllowedFunctions.Floor, "floor(null) eq 0", "floor" }, + { AllowedFunctions.Floor, "floor(Weight) eq 0", "floor" }, + { AllowedFunctions.Round, "round(null) eq 0", "round" }, + { AllowedFunctions.Round, "round(Weight) eq 0", "round" }, }; - var expectedMessage = string.Format( - "Arithmetic operator '{0}' is not allowed. " + - "To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings.", - operatorName); - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - Assert.NotEqual(unused, settings.AllowedArithmeticOperators); - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); } + } - public static TheoryDataSet ArithmeticOperators_CheckArguments - { - get - { - return new TheoryDataSet - { - { "day(DiscontinuedDate) add 0 eq 23" }, - { "day(DiscontinuedDate) div 23 eq 1" }, - { "day(DiscontinuedDate) mod 23 eq 0" }, - { "day(DiscontinuedDate) mul 1 eq 23" }, - { "day(DiscontinuedDate) sub 0 eq 23" }, - { "0 add day(DiscontinuedDate) eq 23" }, - { "23 div day(DiscontinuedDate) eq 1" }, - { "23 mod day(DiscontinuedDate) eq 0" }, - { "1 mul day(DiscontinuedDate) eq 23" }, - { "0 sub day(DiscontinuedDate) eq -23" }, - }; - } + public static TheoryDataSet OtherFunctions + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.All, "AlternateIDs/all(t : null eq 1)", "all" }, + { AllowedFunctions.All, "AlternateIDs/all(t : t eq 1)", "all" }, + { AllowedFunctions.All, "AlternateAddresses/all(t : null eq 'Redmond')", "all" }, + { AllowedFunctions.All, "AlternateAddresses/all(t : t/City eq 'Redmond')", "all" }, + { AllowedFunctions.All, "Category/QueryableProducts/all(t : null eq 'Name')", "all" }, + { AllowedFunctions.All, "Category/QueryableProducts/all(t : t/ProductName eq 'Name')", "all" }, + { AllowedFunctions.All, "Category/EnumerableProducts/all(t : null eq 'Name')", "all" }, + { AllowedFunctions.All, "Category/EnumerableProducts/all(t : t/ProductName eq 'Name')", "all" }, + + { AllowedFunctions.Any, "AlternateIDs/any()", "any" }, + { AllowedFunctions.Any, "AlternateIDs/any(t : null eq 1)", "any" }, + { AllowedFunctions.Any, "AlternateIDs/any(t : t eq 1)", "any" }, + { AllowedFunctions.Any, "AlternateAddresses/any()", "any" }, + { AllowedFunctions.Any, "AlternateAddresses/any(t : null eq 'Redmond')", "any" }, + { AllowedFunctions.Any, "AlternateAddresses/any(t : t/City eq 'Redmond')", "any" }, + { AllowedFunctions.Any, "Category/QueryableProducts/any()", "any" }, + { AllowedFunctions.Any, "Category/QueryableProducts/any(t : null eq 'Name')", "any" }, + { AllowedFunctions.Any, "Category/QueryableProducts/any(t : t/ProductName eq 'Name')", "any" }, + { AllowedFunctions.Any, "Category/EnumerableProducts/any()", "any" }, + { AllowedFunctions.Any, "Category/EnumerableProducts/any(t : null eq 'Name')", "any" }, + { AllowedFunctions.Any, "Category/EnumerableProducts/any(t : t/ProductName eq 'Name')", "any" }, + + { AllowedFunctions.Cast, "cast('Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(Edm.String) eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast('Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast('Microsoft.AspNetCore.OData.Tests.Models.Address')/City eq 'Redmond'", "cast" }, + { AllowedFunctions.Cast, "cast('Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct')/DerivedProductName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null,'Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(null, 'Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(null,Edm.String) eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null, Edm.String) eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null,'Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null, 'Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "cast" }, + { AllowedFunctions.Cast, "cast(null, 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "cast" }, + + // TODO: uncomment the follow two cases after ODL fix the issues. + // { AllowedFunctions.Cast, "cast(null,'Microsoft.AspNetCore.OData.Tests.Models.Address')/City eq 'Redmond'", "cast" }, + // { AllowedFunctions.Cast, "cast(null, 'Microsoft.AspNetCore.OData.Tests.Models.Address')/City eq 'Redmond'", "cast" }, + { AllowedFunctions.Cast, "cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First','Edm.String') eq 'First'", "cast" }, + { AllowedFunctions.Cast, "cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First', 'Edm.String') eq 'First'", "cast" }, + { AllowedFunctions.Cast, "cast(CategoryID,'Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(CategoryID, 'Edm.Int64') eq 0", "cast" }, + { AllowedFunctions.Cast, "cast(ReorderLevel,Edm.String) eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(ReorderLevel, Edm.String) eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(ReorderLevel,'Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(ReorderLevel, 'Edm.String') eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(Ranking,'Edm.String') eq 'First'", "cast" }, + { AllowedFunctions.Cast, "cast(Ranking, 'Edm.String') eq 'First'", "cast" }, + { AllowedFunctions.Cast, "cast(ProductName,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "cast" }, + { AllowedFunctions.Cast, "cast(ProductName, 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "cast" }, + { AllowedFunctions.Cast, "cast(SupplierAddress,'Microsoft.AspNetCore.OData.Tests.Models.Address')/City eq 'Redmond'", "cast" }, + { AllowedFunctions.Cast, "cast(SupplierAddress, 'Microsoft.AspNetCore.OData.Tests.Models.Address')/City eq 'Redmond'", "cast" }, + { AllowedFunctions.Cast, "cast(Category,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(Category, 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, + + { AllowedFunctions.IsOf, "isof('Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(Edm.String)", "isof" }, + { AllowedFunctions.IsOf, "isof('Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof('Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", "isof" }, + { AllowedFunctions.IsOf, "isof('Microsoft.AspNetCore.OData.Tests.Models.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof('Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')", "isof" }, + { AllowedFunctions.IsOf, "isof('Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,Edm.String)", "isof" }, + { AllowedFunctions.IsOf, "isof(null, Edm.String)", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'Microsoft.AspNetCore.OData.Tests.Models.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'Microsoft.AspNetCore.OData.Tests.Models.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof(null,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')", "isof" }, + { AllowedFunctions.IsOf, "isof(null, 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')", "isof" }, + { AllowedFunctions.IsOf, "isof(CategoryID,'Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(CategoryID, 'Edm.Int64')", "isof" }, + { AllowedFunctions.IsOf, "isof(ReorderLevel,Edm.String)", "isof" }, + { AllowedFunctions.IsOf, "isof(ReorderLevel, Edm.String)", "isof" }, + { AllowedFunctions.IsOf, "isof(ReorderLevel,'Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof(ReorderLevel, 'Edm.String')", "isof" }, + { AllowedFunctions.IsOf, "isof(Ranking,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", "isof" }, + { AllowedFunctions.IsOf, "isof(Ranking, 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", "isof" }, + { AllowedFunctions.IsOf, "isof(SupplierAddress,'Microsoft.AspNetCore.OData.Tests.Models.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof(SupplierAddress, 'Microsoft.AspNetCore.OData.Tests.Models.Address')", "isof" }, + { AllowedFunctions.IsOf, "isof(Category,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')", "isof" }, + { AllowedFunctions.IsOf, "isof(Category, 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')", "isof" }, + }; } + } - [Theory] - [MemberData(nameof(ArithmeticOperators_CheckArguments))] - public void ArithmeticOperators_CheckArguments_SucceedIfAllowed(string query) - { - // Arrange - var settings = new ODataValidationSettings - { - AllowedFunctions = AllowedFunctions.Day, + public static TheoryDataSet StringFunctions + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.Concat, "concat(null,'Name') eq 'Name'", "concat" }, + { AllowedFunctions.Concat, "concat(null, 'Name') eq 'Name'", "concat" }, + { AllowedFunctions.Concat, "concat(ProductName,'Name') eq 'Name'", "concat" }, + { AllowedFunctions.Concat, "concat(ProductName, 'Name') eq 'Name'", "concat" }, + { AllowedFunctions.EndsWith, "endswith(null,'Name')", "endswith" }, + { AllowedFunctions.EndsWith, "endswith(null, 'Name')", "endswith" }, + { AllowedFunctions.EndsWith, "endswith(ProductName,'Name')", "endswith" }, + { AllowedFunctions.EndsWith, "endswith(ProductName, 'Name')", "endswith" }, + { AllowedFunctions.IndexOf, "indexof(null,'Name') eq 1", "indexof" }, + { AllowedFunctions.IndexOf, "indexof(null, 'Name') eq 1", "indexof" }, + { AllowedFunctions.IndexOf, "indexof(ProductName,'Name') eq 1", "indexof" }, + { AllowedFunctions.IndexOf, "indexof(ProductName, 'Name') eq 1", "indexof" }, + { AllowedFunctions.Length, "length(null) eq 6", "length" }, + { AllowedFunctions.Length, "length(ProductName) eq 6", "length" }, + { AllowedFunctions.StartsWith, "startswith(null,'Name')", "startswith" }, + { AllowedFunctions.StartsWith, "startswith(null, 'Name')", "startswith" }, + { AllowedFunctions.StartsWith, "startswith(ProductName,'Name')", "startswith" }, + { AllowedFunctions.StartsWith, "startswith(ProductName, 'Name')", "startswith" }, + { AllowedFunctions.MatchesPattern, "matchesPattern(null,'Name')", "matchesPattern" }, + { AllowedFunctions.MatchesPattern, "matchesPattern(null, 'Name')", "matchesPattern" }, + { AllowedFunctions.MatchesPattern, "matchesPattern(ProductName,'Name')", "matchesPattern" }, + { AllowedFunctions.MatchesPattern, "matchesPattern(ProductName, 'Name')", "matchesPattern" }, + { AllowedFunctions.Substring, "substring(null,1) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(null, 1) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(ProductName,1) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(ProductName, 1) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(null,1,2) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(null, 1, 2) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(ProductName,1,2) eq 'Name'", "substring" }, + { AllowedFunctions.Substring, "substring(ProductName, 1, 2) eq 'Name'", "substring" }, + // Contains isn't in `AllowedFunctions` with expected name. + { AllowedFunctions.Contains, "contains(null,'Name')", "contains" }, + { AllowedFunctions.Contains, "contains(null, 'Name')", "contains" }, + { AllowedFunctions.Contains, "contains(ProductName,'Name')", "contains" }, + { AllowedFunctions.Contains, "contains(ProductName, 'Name')", "contains" }, + { AllowedFunctions.ToLower, "tolower(null) eq 'Name'", "tolower" }, + { AllowedFunctions.ToLower, "tolower(ProductName) eq 'Name'", "tolower" }, + { AllowedFunctions.ToUpper, "toupper(null) eq 'Name'", "toupper" }, + { AllowedFunctions.ToUpper, "toupper(ProductName) eq 'Name'", "toupper" }, + { AllowedFunctions.Trim, "trim(null) eq 'Name'", "trim" }, + { AllowedFunctions.Trim, "trim(ProductName) eq 'Name'", "trim" }, }; - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); } + } + + [Fact] + public void AllowedFunctionDataSets_CoverAllValues() + { + // Arrange + // Get all values in the AllowedFunctions enum. + var values = new HashSet(Enum.GetValues(typeof(AllowedFunctions)).Cast()); - [Theory] - [MemberData(nameof(ArithmeticOperators_CheckArguments))] - public void ArithmeticOperators_CheckArguments_ThrowIfNotAllowed(string query) + var groupValues = new[] { - // Arrange - var settings = new ODataValidationSettings - { - AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.Day, - }; - var expectedMessage = string.Format( - "Function 'day' is not allowed. " + - "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings."); - var option = new FilterQueryOption(query, _productContext); + AllowedFunctions.None, + AllowedFunctions.AllFunctions, + AllowedFunctions.AllDateTimeFunctions, + AllowedFunctions.AllMathFunctions, + AllowedFunctions.AllStringFunctions + }; - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); - } + // No need to include OtherFunctions_* here since they cover enum values also in OtherFunctions. + var dataSets = DateTimeFunctions + .Concat(MathFunctions) + .Concat(OtherFunctions) + .Concat(StringFunctions); - // No support for OData v4 functions: - // maxdatetime, mindatetime, now, totaloffsetminutes, or totalseconds? - public static TheoryDataSet DateTimeFunctions + // Act + // Remove the group items. + foreach (var allowed in groupValues) { - get - { - return new TheoryDataSet - { - { AllowedFunctions.Day, "day(null) eq 20", "day" }, - { AllowedFunctions.Day, "day(DiscontinuedDate) eq 20", "day" }, - { AllowedFunctions.Hour, "hour(null) eq 10", "hour" }, - { AllowedFunctions.Hour, "hour(DiscontinuedDate) eq 10", "hour" }, - { AllowedFunctions.Minute, "minute(null) eq 20", "minute" }, - { AllowedFunctions.Minute, "minute(DiscontinuedDate) eq 20", "minute" }, - { AllowedFunctions.Month, "month(null) eq 10", "month" }, - { AllowedFunctions.Month, "month(DiscontinuedDate) eq 10", "month" }, - { AllowedFunctions.Second, "second(null) eq 20", "second" }, - { AllowedFunctions.Second, "second(DiscontinuedDate) eq 20", "second" }, - { AllowedFunctions.Year, "year(null) eq 2000", "year" }, - { AllowedFunctions.Year, "year(DiscontinuedDate) eq 2000", "year" }, - { AllowedFunctions.Date, "date(null) eq 2015-02-26", "date" }, - { AllowedFunctions.Date, "date(DiscontinuedDate) eq 2015-02-26", "date" }, - { AllowedFunctions.Time, "time(null) eq 03:4:05.90100", "time" }, - { AllowedFunctions.Time, "time(DiscontinuedDate) eq 03:4:05.90100", "time" }, - { AllowedFunctions.FractionalSeconds, "fractionalseconds(null) eq 0.05", "fractionalseconds" }, - { AllowedFunctions.FractionalSeconds, "fractionalseconds(DiscontinuedDate) eq 0.050", "fractionalseconds" }, - }; - } + values.Remove(allowed); } - public static TheoryDataSet MathFunctions + // Remove the individual items. + foreach (var allowed in dataSets.Select(item => (AllowedFunctions)(item[0]))) { - get - { - return new TheoryDataSet - { - { AllowedFunctions.Ceiling, "ceiling(null) eq 0", "ceiling" }, - { AllowedFunctions.Ceiling, "ceiling(Weight) eq 0", "ceiling" }, - { AllowedFunctions.Floor, "floor(null) eq 0", "floor" }, - { AllowedFunctions.Floor, "floor(Weight) eq 0", "floor" }, - { AllowedFunctions.Round, "round(null) eq 0", "round" }, - { AllowedFunctions.Round, "round(Weight) eq 0", "round" }, - }; - } + values.Remove(allowed); } - public static TheoryDataSet OtherFunctions - { - get - { - return new TheoryDataSet - { - { AllowedFunctions.All, "AlternateIDs/all(t : null eq 1)", "all" }, - { AllowedFunctions.All, "AlternateIDs/all(t : t eq 1)", "all" }, - { AllowedFunctions.All, "AlternateAddresses/all(t : null eq 'Redmond')", "all" }, - { AllowedFunctions.All, "AlternateAddresses/all(t : t/City eq 'Redmond')", "all" }, - { AllowedFunctions.All, "Category/QueryableProducts/all(t : null eq 'Name')", "all" }, - { AllowedFunctions.All, "Category/QueryableProducts/all(t : t/ProductName eq 'Name')", "all" }, - { AllowedFunctions.All, "Category/EnumerableProducts/all(t : null eq 'Name')", "all" }, - { AllowedFunctions.All, "Category/EnumerableProducts/all(t : t/ProductName eq 'Name')", "all" }, - - { AllowedFunctions.Any, "AlternateIDs/any()", "any" }, - { AllowedFunctions.Any, "AlternateIDs/any(t : null eq 1)", "any" }, - { AllowedFunctions.Any, "AlternateIDs/any(t : t eq 1)", "any" }, - { AllowedFunctions.Any, "AlternateAddresses/any()", "any" }, - { AllowedFunctions.Any, "AlternateAddresses/any(t : null eq 'Redmond')", "any" }, - { AllowedFunctions.Any, "AlternateAddresses/any(t : t/City eq 'Redmond')", "any" }, - { AllowedFunctions.Any, "Category/QueryableProducts/any()", "any" }, - { AllowedFunctions.Any, "Category/QueryableProducts/any(t : null eq 'Name')", "any" }, - { AllowedFunctions.Any, "Category/QueryableProducts/any(t : t/ProductName eq 'Name')", "any" }, - { AllowedFunctions.Any, "Category/EnumerableProducts/any()", "any" }, - { AllowedFunctions.Any, "Category/EnumerableProducts/any(t : null eq 'Name')", "any" }, - { AllowedFunctions.Any, "Category/EnumerableProducts/any(t : t/ProductName eq 'Name')", "any" }, - - { AllowedFunctions.Cast, "cast('Edm.Int64') eq 0", "cast" }, - { AllowedFunctions.Cast, "cast(Edm.String) eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast('Edm.String') eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast('Microsoft.AspNetCore.OData.Tests.Models.Address')/City eq 'Redmond'", "cast" }, - { AllowedFunctions.Cast, "cast('Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct')/DerivedProductName eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(null,'Edm.Int64') eq 0", "cast" }, - { AllowedFunctions.Cast, "cast(null, 'Edm.Int64') eq 0", "cast" }, - { AllowedFunctions.Cast, "cast(null,Edm.String) eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(null, Edm.String) eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(null,'Edm.String') eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(null, 'Edm.String') eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(null,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "cast" }, - { AllowedFunctions.Cast, "cast(null, 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "cast" }, - - // TODO: uncomment the follow two cases after ODL fix the issues. - // { AllowedFunctions.Cast, "cast(null,'Microsoft.AspNetCore.OData.Tests.Models.Address')/City eq 'Redmond'", "cast" }, - // { AllowedFunctions.Cast, "cast(null, 'Microsoft.AspNetCore.OData.Tests.Models.Address')/City eq 'Redmond'", "cast" }, - { AllowedFunctions.Cast, "cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First','Edm.String') eq 'First'", "cast" }, - { AllowedFunctions.Cast, "cast(Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First', 'Edm.String') eq 'First'", "cast" }, - { AllowedFunctions.Cast, "cast(CategoryID,'Edm.Int64') eq 0", "cast" }, - { AllowedFunctions.Cast, "cast(CategoryID, 'Edm.Int64') eq 0", "cast" }, - { AllowedFunctions.Cast, "cast(ReorderLevel,Edm.String) eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(ReorderLevel, Edm.String) eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(ReorderLevel,'Edm.String') eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(ReorderLevel, 'Edm.String') eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(Ranking,'Edm.String') eq 'First'", "cast" }, - { AllowedFunctions.Cast, "cast(Ranking, 'Edm.String') eq 'First'", "cast" }, - { AllowedFunctions.Cast, "cast(ProductName,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "cast" }, - { AllowedFunctions.Cast, "cast(ProductName, 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum') eq Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "cast" }, - { AllowedFunctions.Cast, "cast(SupplierAddress,'Microsoft.AspNetCore.OData.Tests.Models.Address')/City eq 'Redmond'", "cast" }, - { AllowedFunctions.Cast, "cast(SupplierAddress, 'Microsoft.AspNetCore.OData.Tests.Models.Address')/City eq 'Redmond'", "cast" }, - { AllowedFunctions.Cast, "cast(Category,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(Category, 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, - - { AllowedFunctions.IsOf, "isof('Edm.Int64')", "isof" }, - { AllowedFunctions.IsOf, "isof(Edm.String)", "isof" }, - { AllowedFunctions.IsOf, "isof('Edm.String')", "isof" }, - { AllowedFunctions.IsOf, "isof('Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", "isof" }, - { AllowedFunctions.IsOf, "isof('Microsoft.AspNetCore.OData.Tests.Models.Address')", "isof" }, - { AllowedFunctions.IsOf, "isof('Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')", "isof" }, - { AllowedFunctions.IsOf, "isof('Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct')", "isof" }, - { AllowedFunctions.IsOf, "isof(null,'Edm.Int64')", "isof" }, - { AllowedFunctions.IsOf, "isof(null, 'Edm.Int64')", "isof" }, - { AllowedFunctions.IsOf, "isof(null,Edm.String)", "isof" }, - { AllowedFunctions.IsOf, "isof(null, Edm.String)", "isof" }, - { AllowedFunctions.IsOf, "isof(null,'Edm.String')", "isof" }, - { AllowedFunctions.IsOf, "isof(null, 'Edm.String')", "isof" }, - { AllowedFunctions.IsOf, "isof(null,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", "isof" }, - { AllowedFunctions.IsOf, "isof(null, 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", "isof" }, - { AllowedFunctions.IsOf, "isof(null,'Microsoft.AspNetCore.OData.Tests.Models.Address')", "isof" }, - { AllowedFunctions.IsOf, "isof(null, 'Microsoft.AspNetCore.OData.Tests.Models.Address')", "isof" }, - { AllowedFunctions.IsOf, "isof(null,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')", "isof" }, - { AllowedFunctions.IsOf, "isof(null, 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')", "isof" }, - { AllowedFunctions.IsOf, "isof(CategoryID,'Edm.Int64')", "isof" }, - { AllowedFunctions.IsOf, "isof(CategoryID, 'Edm.Int64')", "isof" }, - { AllowedFunctions.IsOf, "isof(ReorderLevel,Edm.String)", "isof" }, - { AllowedFunctions.IsOf, "isof(ReorderLevel, Edm.String)", "isof" }, - { AllowedFunctions.IsOf, "isof(ReorderLevel,'Edm.String')", "isof" }, - { AllowedFunctions.IsOf, "isof(ReorderLevel, 'Edm.String')", "isof" }, - { AllowedFunctions.IsOf, "isof(Ranking,'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", "isof" }, - { AllowedFunctions.IsOf, "isof(Ranking, 'Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum')", "isof" }, - { AllowedFunctions.IsOf, "isof(SupplierAddress,'Microsoft.AspNetCore.OData.Tests.Models.Address')", "isof" }, - { AllowedFunctions.IsOf, "isof(SupplierAddress, 'Microsoft.AspNetCore.OData.Tests.Models.Address')", "isof" }, - { AllowedFunctions.IsOf, "isof(Category,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')", "isof" }, - { AllowedFunctions.IsOf, "isof(Category, 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')", "isof" }, - }; - } - } + // Assert + // Should have nothing left. + Assert.Empty(values); + } - public static TheoryDataSet StringFunctions + [Theory] + [MemberData(nameof(DateTimeFunctions))] + [MemberData(nameof(MathFunctions))] + [MemberData(nameof(OtherFunctions))] + [MemberData(nameof(StringFunctions))] + public void AllowedFunctions_SucceedIfAllowed(AllowedFunctions allow, string query, string unused) + { + // Arrange + var settings = new ODataValidationSettings { - get - { - return new TheoryDataSet - { - { AllowedFunctions.Concat, "concat(null,'Name') eq 'Name'", "concat" }, - { AllowedFunctions.Concat, "concat(null, 'Name') eq 'Name'", "concat" }, - { AllowedFunctions.Concat, "concat(ProductName,'Name') eq 'Name'", "concat" }, - { AllowedFunctions.Concat, "concat(ProductName, 'Name') eq 'Name'", "concat" }, - { AllowedFunctions.EndsWith, "endswith(null,'Name')", "endswith" }, - { AllowedFunctions.EndsWith, "endswith(null, 'Name')", "endswith" }, - { AllowedFunctions.EndsWith, "endswith(ProductName,'Name')", "endswith" }, - { AllowedFunctions.EndsWith, "endswith(ProductName, 'Name')", "endswith" }, - { AllowedFunctions.IndexOf, "indexof(null,'Name') eq 1", "indexof" }, - { AllowedFunctions.IndexOf, "indexof(null, 'Name') eq 1", "indexof" }, - { AllowedFunctions.IndexOf, "indexof(ProductName,'Name') eq 1", "indexof" }, - { AllowedFunctions.IndexOf, "indexof(ProductName, 'Name') eq 1", "indexof" }, - { AllowedFunctions.Length, "length(null) eq 6", "length" }, - { AllowedFunctions.Length, "length(ProductName) eq 6", "length" }, - { AllowedFunctions.StartsWith, "startswith(null,'Name')", "startswith" }, - { AllowedFunctions.StartsWith, "startswith(null, 'Name')", "startswith" }, - { AllowedFunctions.StartsWith, "startswith(ProductName,'Name')", "startswith" }, - { AllowedFunctions.StartsWith, "startswith(ProductName, 'Name')", "startswith" }, - { AllowedFunctions.MatchesPattern, "matchesPattern(null,'Name')", "matchesPattern" }, - { AllowedFunctions.MatchesPattern, "matchesPattern(null, 'Name')", "matchesPattern" }, - { AllowedFunctions.MatchesPattern, "matchesPattern(ProductName,'Name')", "matchesPattern" }, - { AllowedFunctions.MatchesPattern, "matchesPattern(ProductName, 'Name')", "matchesPattern" }, - { AllowedFunctions.Substring, "substring(null,1) eq 'Name'", "substring" }, - { AllowedFunctions.Substring, "substring(null, 1) eq 'Name'", "substring" }, - { AllowedFunctions.Substring, "substring(ProductName,1) eq 'Name'", "substring" }, - { AllowedFunctions.Substring, "substring(ProductName, 1) eq 'Name'", "substring" }, - { AllowedFunctions.Substring, "substring(null,1,2) eq 'Name'", "substring" }, - { AllowedFunctions.Substring, "substring(null, 1, 2) eq 'Name'", "substring" }, - { AllowedFunctions.Substring, "substring(ProductName,1,2) eq 'Name'", "substring" }, - { AllowedFunctions.Substring, "substring(ProductName, 1, 2) eq 'Name'", "substring" }, - // Contains isn't in `AllowedFunctions` with expected name. - { AllowedFunctions.Contains, "contains(null,'Name')", "contains" }, - { AllowedFunctions.Contains, "contains(null, 'Name')", "contains" }, - { AllowedFunctions.Contains, "contains(ProductName,'Name')", "contains" }, - { AllowedFunctions.Contains, "contains(ProductName, 'Name')", "contains" }, - { AllowedFunctions.ToLower, "tolower(null) eq 'Name'", "tolower" }, - { AllowedFunctions.ToLower, "tolower(ProductName) eq 'Name'", "tolower" }, - { AllowedFunctions.ToUpper, "toupper(null) eq 'Name'", "toupper" }, - { AllowedFunctions.ToUpper, "toupper(ProductName) eq 'Name'", "toupper" }, - { AllowedFunctions.Trim, "trim(null) eq 'Name'", "trim" }, - { AllowedFunctions.Trim, "trim(ProductName) eq 'Name'", "trim" }, - }; - } - } + AllowedFunctions = allow, + }; + var option = new FilterQueryOption(query, _productContext); - [Fact] - public void AllowedFunctionDataSets_CoverAllValues() - { - // Arrange - // Get all values in the AllowedFunctions enum. - var values = new HashSet(Enum.GetValues(typeof(AllowedFunctions)).Cast()); + // Act & Assert + Assert.NotNull(unused); + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } - var groupValues = new[] - { - AllowedFunctions.None, - AllowedFunctions.AllFunctions, - AllowedFunctions.AllDateTimeFunctions, - AllowedFunctions.AllMathFunctions, - AllowedFunctions.AllStringFunctions - }; + [Theory] + [MemberData(nameof(DateTimeFunctions))] + [MemberData(nameof(MathFunctions))] + [MemberData(nameof(OtherFunctions))] + [MemberData(nameof(StringFunctions))] + public void AllowedFunctions_ThrowIfNotAllowed(AllowedFunctions exclude, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~exclude, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } - // No need to include OtherFunctions_* here since they cover enum values also in OtherFunctions. - var dataSets = DateTimeFunctions - .Concat(MathFunctions) - .Concat(OtherFunctions) - .Concat(StringFunctions); + [Theory] + [MemberData(nameof(DateTimeFunctions))] + [MemberData(nameof(MathFunctions))] + [MemberData(nameof(OtherFunctions))] + [MemberData(nameof(StringFunctions))] + public void AllowedFunctions_ThrowIfNoneAllowed(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.None, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.NotEqual(AllowedFunctions.None, unused); + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } - // Act - // Remove the group items. - foreach (var allowed in groupValues) - { - values.Remove(allowed); - } + [Theory] + [MemberData(nameof(DateTimeFunctions))] + public void DateTimeFunctions_SucceedIfGroupAllowed(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllDateTimeFunctions, + }; + var option = new FilterQueryOption(query, _productContext); - // Remove the individual items. - foreach (var allowed in dataSets.Select(item => (AllowedFunctions)(item[0]))) - { - values.Remove(allowed); - } + // Act & Assert + Assert.NotEqual(AllowedFunctions.None, unused); + Assert.NotNull(unusedName); + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } - // Assert - // Should have nothing left. - Assert.Empty(values); - } + [Theory] + [MemberData(nameof(DateTimeFunctions))] + public void DateTimeFunctions_ThrowIfGroupNotAllowed(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.AllDateTimeFunctions, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.NotEqual(AllowedFunctions.None, unused); + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } - [Theory] - [MemberData(nameof(DateTimeFunctions))] - [MemberData(nameof(MathFunctions))] - [MemberData(nameof(OtherFunctions))] - [MemberData(nameof(StringFunctions))] - public void AllowedFunctions_SucceedIfAllowed(AllowedFunctions allow, string query, string unused) + [Theory] + [MemberData(nameof(MathFunctions))] + public void MathFunctions_SucceedIfGroupAllowed(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings { - // Arrange - var settings = new ODataValidationSettings - { - AllowedFunctions = allow, - }; - var option = new FilterQueryOption(query, _productContext); + AllowedFunctions = AllowedFunctions.AllMathFunctions, + }; + var option = new FilterQueryOption(query, _productContext); - // Act & Assert - Assert.NotNull(unused); - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); - } + // Act & Assert + Assert.NotEqual(AllowedFunctions.None, unused); + Assert.NotNull(unusedName); + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } - [Theory] - [MemberData(nameof(DateTimeFunctions))] - [MemberData(nameof(MathFunctions))] - [MemberData(nameof(OtherFunctions))] - [MemberData(nameof(StringFunctions))] - public void AllowedFunctions_ThrowIfNotAllowed(AllowedFunctions exclude, string query, string functionName) + [Theory] + [MemberData(nameof(MathFunctions))] + public void MathFunctions_ThrowIfGroupNotAllowed(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.AllMathFunctions, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.NotEqual(AllowedFunctions.None, unused); + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [MemberData(nameof(StringFunctions))] + public void StringFunctions_SucceedIfGroupAllowed(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings { - // Arrange - var settings = new ODataValidationSettings - { - AllowedFunctions = AllowedFunctions.AllFunctions & ~exclude, - }; - var expectedMessage = string.Format( - "Function '{0}' is not allowed. " + - "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", - functionName); - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); - } + AllowedFunctions = AllowedFunctions.AllStringFunctions, + }; + var option = new FilterQueryOption(query, _productContext); - [Theory] - [MemberData(nameof(DateTimeFunctions))] - [MemberData(nameof(MathFunctions))] - [MemberData(nameof(OtherFunctions))] - [MemberData(nameof(StringFunctions))] - public void AllowedFunctions_ThrowIfNoneAllowed(AllowedFunctions unused, string query, string functionName) + // Act & Assert + Assert.NotEqual(AllowedFunctions.None, unused); + Assert.NotNull(unusedName); + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [MemberData(nameof(StringFunctions))] + public void StringFunctions_ThrowIfGroupNotAllowed(AllowedFunctions unused, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.AllStringFunctions, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.NotEqual(AllowedFunctions.None, unused); + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + public static TheoryDataSet OtherFunctions_SomeSingleParameterCasts + { + get { - // Arrange - var settings = new ODataValidationSettings + return new TheoryDataSet { - AllowedFunctions = AllowedFunctions.None, + // Single-parameter casts without quotes around the type name. + { AllowedFunctions.Cast, "cast(Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct)/DerivedProductName eq 'Name'", "cast" }, + { AllowedFunctions.IsOf, "isof(Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct)", "isof" }, }; - var expectedMessage = string.Format( - "Function '{0}' is not allowed. " + - "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", - functionName); - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - Assert.NotEqual(AllowedFunctions.None, unused); - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); } + } + + [Theory] + [MemberData(nameof(OtherFunctions_SomeSingleParameterCasts))] + public void OtherFunctions_SomeSingleParameterCasts_ThrowODataException(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.None, + }; + var expectedMessage = "Cast or IsOf Function must have a type in its arguments."; + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.NotEqual(AllowedFunctions.None, unused); + Assert.NotNull(unusedName); + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } - [Theory] - [MemberData(nameof(DateTimeFunctions))] - public void DateTimeFunctions_SucceedIfGroupAllowed(AllowedFunctions unused, string query, string unusedName) + public static TheoryDataSet OtherFunctions_SomeTwoParameterCasts + { + get { - // Arrange - var settings = new ODataValidationSettings + return new TheoryDataSet { - AllowedFunctions = AllowedFunctions.AllDateTimeFunctions, - }; - var option = new FilterQueryOption(query, _productContext); + // Two-parameter casts without quotes around the type name. + { AllowedFunctions.Cast, "cast(null,Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(Category,Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(Category, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, - // Act & Assert - Assert.NotEqual(AllowedFunctions.None, unused); - Assert.NotNull(unusedName); - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + { AllowedFunctions.IsOf, "isof(null,Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", "isof" }, + { AllowedFunctions.IsOf, "isof(null, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", "isof" }, + { AllowedFunctions.IsOf, "isof(Category,Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", "isof" }, + { AllowedFunctions.IsOf, "isof(Category, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", "isof" }, + }; } + } - [Theory] - [MemberData(nameof(DateTimeFunctions))] - public void DateTimeFunctions_ThrowIfGroupNotAllowed(AllowedFunctions unused, string query, string functionName) + /* + [Theory] + [MemberData(nameof(OtherFunctions_SomeTwoParameterCasts))] + public void OtherFunctions_SomeTwoParameterCasts_ThrowODataException(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.None, + }; + var expectedMessage = string.Format( + "Encountered invalid type cast. '{0}' is not assignable from '{1}'.", + typeof(DerivedCategory).FullName, + typeof(Product).FullName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.NotEqual(AllowedFunctions.None, unused); + Assert.NotNull(unusedName); + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + }*/ + + public static TheoryDataSet OtherFunctions_SomeQuotedTwoParameterCasts + { + get { - // Arrange - var settings = new ODataValidationSettings + return new TheoryDataSet { - AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.AllDateTimeFunctions, + // Cast null to an entity type. Note 'isof' with same arguments is fine. + { AllowedFunctions.Cast, "cast(null,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, + { AllowedFunctions.Cast, "cast(null, 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, }; - var expectedMessage = string.Format( - "Function '{0}' is not allowed. " + - "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", - functionName); - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - Assert.NotEqual(AllowedFunctions.None, unused); - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); } + } - [Theory] - [MemberData(nameof(MathFunctions))] - public void MathFunctions_SucceedIfGroupAllowed(AllowedFunctions unused, string query, string unusedName) + [Theory] + [MemberData(nameof(OtherFunctions_SomeQuotedTwoParameterCasts))] + public void OtherFunctions_SomeQuotedTwoParameterCasts_DoesnotThrowArgumentException(AllowedFunctions unused, string query, string unusedName) + { + // Arrange + var settings = new ODataValidationSettings { - // Arrange - var settings = new ODataValidationSettings - { - AllowedFunctions = AllowedFunctions.AllMathFunctions, - }; - var option = new FilterQueryOption(query, _productContext); + AllowedFunctions = AllowedFunctions.AllFunctions, + }; + var option = new FilterQueryOption(query, _productContext); - // Act & Assert - Assert.NotEqual(AllowedFunctions.None, unused); - Assert.NotNull(unusedName); - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); - } + // Act & Assert + Assert.NotEqual(AllowedFunctions.None, unused); + Assert.NotNull(unusedName); + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } - [Theory] - [MemberData(nameof(MathFunctions))] - public void MathFunctions_ThrowIfGroupNotAllowed(AllowedFunctions unused, string query, string functionName) - { - // Arrange - var settings = new ODataValidationSettings - { - AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.AllMathFunctions, + public static TheoryDataSet Functions_CheckArguments + { + get + { + return new TheoryDataSet + { + { AllowedFunctions.Ceiling, AllowedFunctions.IndexOf, "ceiling(indexof(ProductName, 'Name')) eq 0", "indexof" }, + { AllowedFunctions.Floor, AllowedFunctions.IndexOf, "floor(indexof(ProductName, 'Name')) eq 0", "indexof" }, + { AllowedFunctions.Round, AllowedFunctions.IndexOf, "round(indexof(ProductName, 'Name')) eq 0", "indexof" }, + + { AllowedFunctions.All, AllowedFunctions.IndexOf, "AlternateAddresses/all(t : indexof(t/City, 'Name') eq 3)", "indexof" }, + { AllowedFunctions.Any, AllowedFunctions.IndexOf, "AlternateAddresses/any(t : indexof(t/City, 'Name') eq 3)", "indexof" }, + { AllowedFunctions.Cast, AllowedFunctions.IndexOf, "cast(indexof(ProductName, 'Name'), 'Edm.Int64') eq 0", "indexof" }, + { AllowedFunctions.IsOf, AllowedFunctions.IndexOf, "isof(indexof(ProductName, 'Name'), 'Edm.Int64')", "indexof" }, + + { AllowedFunctions.Concat, AllowedFunctions.Substring, "concat(substring(ProductName, 1), 'Name') eq 'Name'", "substring" }, + { AllowedFunctions.Concat, AllowedFunctions.Substring, "concat(ProductName, substring(ProductName, 1)) eq 'Name'", "substring" }, + { AllowedFunctions.EndsWith, AllowedFunctions.Substring, "endswith(substring(ProductName, 1), 'Name')", "substring" }, + { AllowedFunctions.EndsWith, AllowedFunctions.Substring, "endswith(ProductName, substring(ProductName, 1))", "substring" }, + { AllowedFunctions.IndexOf, AllowedFunctions.Substring, "indexof(substring(ProductName, 1), 'Name') eq 1", "substring" }, + { AllowedFunctions.IndexOf, AllowedFunctions.Substring, "indexof(ProductName, substring(ProductName, 1)) eq 1", "substring" }, + { AllowedFunctions.Length, AllowedFunctions.Substring, "length(substring(ProductName, 1)) eq 6", "substring" }, + { AllowedFunctions.StartsWith, AllowedFunctions.Substring, "startswith(substring(ProductName, 1), 'Name')", "substring" }, + { AllowedFunctions.StartsWith, AllowedFunctions.Substring, "startswith(ProductName, substring(ProductName, 1))", "substring" }, + { AllowedFunctions.Substring, AllowedFunctions.Concat, "substring(concat(ProductName, 'Name'), 1) eq 'Name'", "concat" }, + { AllowedFunctions.Substring, AllowedFunctions.IndexOf, "substring(ProductName, indexof(ProductName, 'Name')) eq 'Name'", "indexof" }, + { AllowedFunctions.Substring, AllowedFunctions.Concat, "substring(concat(ProductName, 'Name'), 1, 2) eq 'Name'", "concat" }, + { AllowedFunctions.Substring, AllowedFunctions.IndexOf, "substring(ProductName, indexof(ProductName, 'Name'), 2) eq 'Name'", "indexof" }, + { AllowedFunctions.Substring, AllowedFunctions.IndexOf, "substring(ProductName, 1, indexof(ProductName, 'Name')) eq 'Name'", "indexof" }, + { AllowedFunctions.Contains, AllowedFunctions.Substring, "contains(substring(ProductName, 1), 'Name')", "substring" }, + { AllowedFunctions.Contains, AllowedFunctions.Substring, "contains(ProductName, substring(ProductName, 1))", "substring" }, + { AllowedFunctions.ToLower, AllowedFunctions.Substring, "tolower(substring(ProductName, 1)) eq 'Name'", "substring" }, + { AllowedFunctions.ToUpper, AllowedFunctions.Substring, "toupper(substring(ProductName, 1)) eq 'Name'", "substring" }, + { AllowedFunctions.Trim, AllowedFunctions.Substring, "trim(substring(ProductName, 1)) eq 'Name'", "substring" }, }; - var expectedMessage = string.Format( - "Function '{0}' is not allowed. " + - "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", - functionName); - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - Assert.NotEqual(AllowedFunctions.None, unused); - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); } + } - [Theory] - [MemberData(nameof(StringFunctions))] - public void StringFunctions_SucceedIfGroupAllowed(AllowedFunctions unused, string query, string unusedName) + [Theory] + [MemberData(nameof(Functions_CheckArguments))] + public void Functions_CheckArguments_SucceedIfAllowed(AllowedFunctions outer, AllowedFunctions inner, string query, string unused) + { + // Arrange + var settings = new ODataValidationSettings { - // Arrange - var settings = new ODataValidationSettings - { - AllowedFunctions = AllowedFunctions.AllStringFunctions, - }; - var option = new FilterQueryOption(query, _productContext); + AllowedFunctions = outer | inner, + }; + var option = new FilterQueryOption(query, _productContext); - // Act & Assert - Assert.NotEqual(AllowedFunctions.None, unused); - Assert.NotNull(unusedName); - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); - } + // Act & Assert + Assert.NotNull(unused); + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Theory] + [MemberData(nameof(Functions_CheckArguments))] + public void Functions_CheckArguments_ThrowIfNotAllowed(AllowedFunctions outer, AllowedFunctions inner, string query, string functionName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = outer, + }; + var expectedMessage = string.Format( + "Function '{0}' is not allowed. " + + "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", + functionName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.NotEqual(AllowedFunctions.None, inner); + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } - [Theory] - [MemberData(nameof(StringFunctions))] - public void StringFunctions_ThrowIfGroupNotAllowed(AllowedFunctions unused, string query, string functionName) + public static TheoryDataSet Functions_CheckNotFilterable + { + get { - // Arrange - var settings = new ODataValidationSettings + return new TheoryDataSet { - AllowedFunctions = AllowedFunctions.AllFunctions & ~AllowedFunctions.AllStringFunctions, + { "day(NotFilterableDiscontinuedDate) eq 20", "NotFilterableDiscontinuedDate" }, + { "hour(NotFilterableDiscontinuedDate) eq 10", "NotFilterableDiscontinuedDate" }, + { "minute(NotFilterableDiscontinuedDate) eq 20", "NotFilterableDiscontinuedDate" }, + { "month(NotFilterableDiscontinuedDate) eq 10", "NotFilterableDiscontinuedDate" }, + { "second(NotFilterableDiscontinuedDate) eq 20", "NotFilterableDiscontinuedDate" }, + { "year(NotFilterableDiscontinuedDate) eq 2000", "NotFilterableDiscontinuedDate" }, + + { "date(NotFilterableDiscontinuedDate) eq 2015-02-26", "NotFilterableDiscontinuedDate" }, + { "time(NotFilterableDiscontinuedDate) eq 03:4:05.90100", "NotFilterableDiscontinuedDate" }, + { "fractionalseconds(NotFilterableDiscontinuedDate) eq 0.1", "NotFilterableDiscontinuedDate" }, + + { "NotFilterableAlternateAddresses/all(t : t/City eq 'Redmond')", "NotFilterableAlternateAddresses" }, + { "NotFilterableAlternateAddresses/any(t : t/City eq 'Redmond')", "NotFilterableAlternateAddresses" }, }; - var expectedMessage = string.Format( - "Function '{0}' is not allowed. " + - "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", - functionName); - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - Assert.NotEqual(AllowedFunctions.None, unused); - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); } + } - public static TheoryDataSet OtherFunctions_SomeSingleParameterCasts - { - get - { - return new TheoryDataSet - { - // Single-parameter casts without quotes around the type name. - { AllowedFunctions.Cast, "cast(Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct)/DerivedProductName eq 'Name'", "cast" }, - { AllowedFunctions.IsOf, "isof(Microsoft.AspNetCore.OData.Tests.Models.DerivedProduct)", "isof" }, - }; - } + [Theory] + [MemberData(nameof(Functions_CheckNotFilterable))] + public void Functions_CheckNotFilterable_ThrowODataException(string query, string propertyName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedFunctions = AllowedFunctions.AllFunctions, + }; + var expectedMessage = string.Format( + "The property '{0}' cannot be used in the $filter query option.", + propertyName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + public static TheoryDataSet LogicalOperators + { + get + { + return new TheoryDataSet + { + { AllowedLogicalOperators.And, "Discontinued and AlternateIDs/any()", "And" }, + { AllowedLogicalOperators.Equal, "UnitPrice add 0 eq UnitPrice", "Equal" }, + { AllowedLogicalOperators.GreaterThan, "UnitPrice add 1 gt UnitPrice", "GreaterThan" }, + { AllowedLogicalOperators.GreaterThanOrEqual, "UnitPrice add 0 ge UnitPrice", "GreaterThanOrEqual" }, + { AllowedLogicalOperators.Has, "Ranking has Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "Has" }, + { AllowedLogicalOperators.LessThan, "UnitPrice add -1 lt UnitPrice", "LessThan" }, + { AllowedLogicalOperators.LessThanOrEqual, "UnitPrice add 0 le UnitPrice", "LessThanOrEqual" }, + { AllowedLogicalOperators.Not, "not Discontinued", "Not" }, + { AllowedLogicalOperators.NotEqual, "UnitPrice add 1 ne UnitPrice", "NotEqual" }, + { AllowedLogicalOperators.Or, "Discontinued or AlternateIDs/any()", "Or" }, + }; } + } - [Theory] - [MemberData(nameof(OtherFunctions_SomeSingleParameterCasts))] - public void OtherFunctions_SomeSingleParameterCasts_ThrowODataException(AllowedFunctions unused, string query, string unusedName) + [Fact] + public void LogicalOperatorsDataSet_CoversAllValues() + { + // Arrange + // Get all values in the AllowedLogicalOperators enum. + var values = new HashSet( + Enum.GetValues(typeof(AllowedLogicalOperators)).Cast()); + var groupValues = new[] { - // Arrange - var settings = new ODataValidationSettings - { - AllowedFunctions = AllowedFunctions.None, - }; - var expectedMessage = "Cast or IsOf Function must have a type in its arguments."; - var option = new FilterQueryOption(query, _productContext); + AllowedLogicalOperators.All, + AllowedLogicalOperators.None, + }; - // Act & Assert - Assert.NotEqual(AllowedFunctions.None, unused); - Assert.NotNull(unusedName); - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + // Act + // Remove the group items. + foreach (var allowed in groupValues) + { + values.Remove(allowed); } - public static TheoryDataSet OtherFunctions_SomeTwoParameterCasts + // Remove the individual items. + foreach (var allowed in LogicalOperators.Select(item => (AllowedLogicalOperators)(item[0]))) { - get - { - return new TheoryDataSet - { - // Two-parameter casts without quotes around the type name. - { AllowedFunctions.Cast, "cast(null,Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(null, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(Category,Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(Category, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)/DerivedCategoryName eq 'Name'", "cast" }, - - { AllowedFunctions.IsOf, "isof(null,Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", "isof" }, - { AllowedFunctions.IsOf, "isof(null, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", "isof" }, - { AllowedFunctions.IsOf, "isof(Category,Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", "isof" }, - { AllowedFunctions.IsOf, "isof(Category, Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory)", "isof" }, - }; - } + values.Remove(allowed); } - /* - [Theory] - [MemberData(nameof(OtherFunctions_SomeTwoParameterCasts))] - public void OtherFunctions_SomeTwoParameterCasts_ThrowODataException(AllowedFunctions unused, string query, string unusedName) + // Assert + // Should have nothing left. + Assert.Empty(values); + } + + [Theory] + [MemberData(nameof(LogicalOperators))] + public void AllowedLogicalOperators_SucceedIfAllowed(AllowedLogicalOperators allow, string query, string unused) + { + // Arrange + var settings = new ODataValidationSettings { - // Arrange - var settings = new ODataValidationSettings - { - AllowedFunctions = AllowedFunctions.None, - }; - var expectedMessage = string.Format( - "Encountered invalid type cast. '{0}' is not assignable from '{1}'.", - typeof(DerivedCategory).FullName, - typeof(Product).FullName); - var option = new FilterQueryOption(query, _productContext); + AllowedLogicalOperators = allow, + }; + var option = new FilterQueryOption(query, _productContext); - // Act & Assert - Assert.NotEqual(AllowedFunctions.None, unused); - Assert.NotNull(unusedName); - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); - }*/ + // Act & Assert + Assert.NotNull(unused); + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } - public static TheoryDataSet OtherFunctions_SomeQuotedTwoParameterCasts - { - get - { - return new TheoryDataSet - { - // Cast null to an entity type. Note 'isof' with same arguments is fine. - { AllowedFunctions.Cast, "cast(null,'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, - { AllowedFunctions.Cast, "cast(null, 'Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory')/DerivedCategoryName eq 'Name'", "cast" }, - }; - } - } + [Theory] + [MemberData(nameof(LogicalOperators))] + public void AllowedLogicalOperators_ThrowIfNotAllowed(AllowedLogicalOperators exclude, string query, string operatorName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedLogicalOperators = AllowedLogicalOperators.All & ~exclude, + }; + var expectedMessage = string.Format( + "Logical operator '{0}' is not allowed. " + + "To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings.", + operatorName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Theory] + [MemberData(nameof(LogicalOperators))] + public void AllowedLogicalOperators_ThrowIfNoneAllowed(AllowedLogicalOperators unused, string query, string operatorName) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedLogicalOperators = AllowedLogicalOperators.None, + }; + var expectedMessage = string.Format( + "Logical operator '{0}' is not allowed. " + + "To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings.", + operatorName); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + Assert.NotEqual(unused, settings.AllowedLogicalOperators); + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } - [Theory] - [MemberData(nameof(OtherFunctions_SomeQuotedTwoParameterCasts))] - public void OtherFunctions_SomeQuotedTwoParameterCasts_DoesnotThrowArgumentException(AllowedFunctions unused, string query, string unusedName) + public static TheoryDataSet LogicalOperators_CheckArguments + { + get { - // Arrange - var settings = new ODataValidationSettings + return new TheoryDataSet { - AllowedFunctions = AllowedFunctions.AllFunctions, + { "(UnitPrice add 0 eq UnitPrice) and AlternateIDs/any()" }, + { "UnitPrice add 0 eq UnitPrice" }, + { "UnitPrice add 1 gt UnitPrice" }, + { "UnitPrice add 0 ge UnitPrice" }, + { "UnitPrice add -1 lt UnitPrice" }, + { "UnitPrice add 0 le UnitPrice" }, + { "not (UnitPrice add 0 eq UnitPrice)" }, + { "UnitPrice add 1 ne UnitPrice" }, + { "(UnitPrice add 0 eq UnitPrice) or AlternateIDs/any()" }, + + { "Discontinued and (UnitPrice add 0 eq UnitPrice)" }, + { "UnitPrice eq UnitPrice add 0" }, + { "UnitPrice gt UnitPrice add -1" }, + { "UnitPrice ge UnitPrice add 0" }, + { "UnitPrice lt UnitPrice add 1" }, + { "UnitPrice le UnitPrice add 0" }, + { "UnitPrice ne UnitPrice add 1" }, + { "Discontinued or (UnitPrice add 0 eq UnitPrice)" }, }; - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - Assert.NotEqual(AllowedFunctions.None, unused); - Assert.NotNull(unusedName); - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); } + } - public static TheoryDataSet Functions_CheckArguments + [Theory] + [MemberData(nameof(LogicalOperators_CheckArguments))] + public void LogicalOperators_CheckArguments_SucceedIfAllowed(string query) + { + // Arrange + var settings = new ODataValidationSettings { - get - { - return new TheoryDataSet - { - { AllowedFunctions.Ceiling, AllowedFunctions.IndexOf, "ceiling(indexof(ProductName, 'Name')) eq 0", "indexof" }, - { AllowedFunctions.Floor, AllowedFunctions.IndexOf, "floor(indexof(ProductName, 'Name')) eq 0", "indexof" }, - { AllowedFunctions.Round, AllowedFunctions.IndexOf, "round(indexof(ProductName, 'Name')) eq 0", "indexof" }, - - { AllowedFunctions.All, AllowedFunctions.IndexOf, "AlternateAddresses/all(t : indexof(t/City, 'Name') eq 3)", "indexof" }, - { AllowedFunctions.Any, AllowedFunctions.IndexOf, "AlternateAddresses/any(t : indexof(t/City, 'Name') eq 3)", "indexof" }, - { AllowedFunctions.Cast, AllowedFunctions.IndexOf, "cast(indexof(ProductName, 'Name'), 'Edm.Int64') eq 0", "indexof" }, - { AllowedFunctions.IsOf, AllowedFunctions.IndexOf, "isof(indexof(ProductName, 'Name'), 'Edm.Int64')", "indexof" }, - - { AllowedFunctions.Concat, AllowedFunctions.Substring, "concat(substring(ProductName, 1), 'Name') eq 'Name'", "substring" }, - { AllowedFunctions.Concat, AllowedFunctions.Substring, "concat(ProductName, substring(ProductName, 1)) eq 'Name'", "substring" }, - { AllowedFunctions.EndsWith, AllowedFunctions.Substring, "endswith(substring(ProductName, 1), 'Name')", "substring" }, - { AllowedFunctions.EndsWith, AllowedFunctions.Substring, "endswith(ProductName, substring(ProductName, 1))", "substring" }, - { AllowedFunctions.IndexOf, AllowedFunctions.Substring, "indexof(substring(ProductName, 1), 'Name') eq 1", "substring" }, - { AllowedFunctions.IndexOf, AllowedFunctions.Substring, "indexof(ProductName, substring(ProductName, 1)) eq 1", "substring" }, - { AllowedFunctions.Length, AllowedFunctions.Substring, "length(substring(ProductName, 1)) eq 6", "substring" }, - { AllowedFunctions.StartsWith, AllowedFunctions.Substring, "startswith(substring(ProductName, 1), 'Name')", "substring" }, - { AllowedFunctions.StartsWith, AllowedFunctions.Substring, "startswith(ProductName, substring(ProductName, 1))", "substring" }, - { AllowedFunctions.Substring, AllowedFunctions.Concat, "substring(concat(ProductName, 'Name'), 1) eq 'Name'", "concat" }, - { AllowedFunctions.Substring, AllowedFunctions.IndexOf, "substring(ProductName, indexof(ProductName, 'Name')) eq 'Name'", "indexof" }, - { AllowedFunctions.Substring, AllowedFunctions.Concat, "substring(concat(ProductName, 'Name'), 1, 2) eq 'Name'", "concat" }, - { AllowedFunctions.Substring, AllowedFunctions.IndexOf, "substring(ProductName, indexof(ProductName, 'Name'), 2) eq 'Name'", "indexof" }, - { AllowedFunctions.Substring, AllowedFunctions.IndexOf, "substring(ProductName, 1, indexof(ProductName, 'Name')) eq 'Name'", "indexof" }, - { AllowedFunctions.Contains, AllowedFunctions.Substring, "contains(substring(ProductName, 1), 'Name')", "substring" }, - { AllowedFunctions.Contains, AllowedFunctions.Substring, "contains(ProductName, substring(ProductName, 1))", "substring" }, - { AllowedFunctions.ToLower, AllowedFunctions.Substring, "tolower(substring(ProductName, 1)) eq 'Name'", "substring" }, - { AllowedFunctions.ToUpper, AllowedFunctions.Substring, "toupper(substring(ProductName, 1)) eq 'Name'", "substring" }, - { AllowedFunctions.Trim, AllowedFunctions.Substring, "trim(substring(ProductName, 1)) eq 'Name'", "substring" }, - }; - } - } + AllowedArithmeticOperators = AllowedArithmeticOperators.Add, + }; + var option = new FilterQueryOption(query, _productContext); - [Theory] - [MemberData(nameof(Functions_CheckArguments))] - public void Functions_CheckArguments_SucceedIfAllowed(AllowedFunctions outer, AllowedFunctions inner, string query, string unused) - { - // Arrange - var settings = new ODataValidationSettings - { - AllowedFunctions = outer | inner, - }; - var option = new FilterQueryOption(query, _productContext); + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } - // Act & Assert - Assert.NotNull(unused); - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); - } + [Theory] + [MemberData(nameof(LogicalOperators_CheckArguments))] + public void LogicalOperators_CheckArguments_ThrowIfNotAllowed(string query) + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedArithmeticOperators = AllowedArithmeticOperators.All & ~AllowedArithmeticOperators.Add, + }; + var expectedMessage = string.Format( + "Arithmetic operator 'Add' is not allowed. " + + "To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings."); + var option = new FilterQueryOption(query, _productContext); + + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } - [Theory] - [MemberData(nameof(Functions_CheckArguments))] - public void Functions_CheckArguments_ThrowIfNotAllowed(AllowedFunctions outer, AllowedFunctions inner, string query, string functionName) + [Fact] + public void ArithmeticNegation_SucceedsIfLogicalNotIsAllowed() + { + // Arrange + var settings = new ODataValidationSettings { - // Arrange - var settings = new ODataValidationSettings - { - AllowedFunctions = outer, + AllowedLogicalOperators = AllowedLogicalOperators.LessThan | AllowedLogicalOperators.Not, + }; + var option = new FilterQueryOption("-UnitPrice lt 0", _productContext); + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + // Note Negate is _not_ a logical operator. + [Fact] + public void ArithmeticNegation_ThrowsIfLogicalNotIsNotAllowed() + { + // Arrange + var settings = new ODataValidationSettings + { + AllowedLogicalOperators = AllowedLogicalOperators.LessThan, + }; + var expectedMessage = string.Format( + "Logical operator 'Negate' is not allowed. " + + "To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings."); + var option = new FilterQueryOption("-UnitPrice lt 0", _productContext); + + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } + + [Fact] + public void ValidateVisitLogicalOperatorEqual() + { + // Arrange + FilterQueryOption option = new FilterQueryOption("Id eq 1", _context); + + // Act + _validator.Validate(option, _settings); + + // Assert + Assert.Equal(6, _validator.Times.Keys.Count); + Assert.Equal(1, _validator.Times["Validate"]); // entry + Assert.Equal(1, _validator.Times["ValidateSingleValuePropertyAccessNode"]); // Id + Assert.Equal(1, _validator.Times["ValidateLogicalOperator"]); // eq + Assert.Equal(1, _validator.Times["ValidateConstantQueryNode"]); // 1 + Assert.Equal(1, _validator.Times["ValidateBinaryOperatorQueryNode"]); // eq + Assert.Equal(1, _validator.Times["ValidateParameterQueryNode"]); // $it + } + + [Fact] + public void ValidateVisitLogicalOperatorHas() + { + // Arrange + FilterQueryOption option = new FilterQueryOption("FavoriteColor has Microsoft.AspNetCore.OData.Tests.Models.Color'Red'", _context); + + // Act + _validator.Validate(option, _settings); + + // Assert + Assert.Equal(6, _validator.Times.Keys.Count); + Assert.Equal(1, _validator.Times["Validate"]); // entry + Assert.Equal(1, _validator.Times["ValidateSingleValuePropertyAccessNode"]); // FavouriteColor + Assert.Equal(1, _validator.Times["ValidateLogicalOperator"]); // has + Assert.Equal(1, _validator.Times["ValidateBinaryOperatorQueryNode"]); // has + Assert.Equal(1, _validator.Times["ValidateParameterQueryNode"]); // $it + } + + [Theory] + [InlineData("Id eq 1")] + [InlineData("Id ne 1")] + [InlineData("Id gt 1")] + [InlineData("Id lt 1")] + [InlineData("Id ge 1")] + [InlineData("Id le 1")] + [InlineData("Id eq Id add 1")] + [InlineData("Id eq Id sub 1")] + [InlineData("Id eq Id mul 1")] + [InlineData("Id eq Id div 1")] + [InlineData("Id eq Id mod 1")] + [InlineData("startswith(Name, 'Microsoft')")] + [InlineData("endswith(Name, 'Microsoft')")] + [InlineData("contains(Name, 'Microsoft')")] + [InlineData("substring(Name, 1) eq 'Name'")] + [InlineData("substring(Name, 1, 2) eq 'Name'")] + [InlineData("length(Name) eq 1")] + [InlineData("tolower(Name) eq 'Name'")] + [InlineData("toupper(Name) eq 'Name'")] + [InlineData("trim(Name) eq 'Name'")] + [InlineData("indexof(Name, 'Microsoft') eq 1")] + [InlineData("concat(Name, 'Microsoft') eq 'Microsoft'")] + [InlineData("year(Birthday) eq 2000")] + [InlineData("month(Birthday) eq 2000")] + [InlineData("day(Birthday) eq 2000")] + [InlineData("hour(Birthday) eq 2000")] + [InlineData("minute(Birthday) eq 2000")] + [InlineData("round(AmountSpent) eq 0")] + [InlineData("floor(AmountSpent) eq 0")] + [InlineData("ceiling(AmountSpent) eq 0")] + [InlineData("Tags/any()")] + [InlineData("Tags/all(t : t eq '1')")] + [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Models.QueryCompositionCustomerBase/Id eq 1")] + [InlineData("Contacts/Microsoft.AspNetCore.OData.Tests.Query.Models.QueryCompositionCustomerBase/any()")] + [InlineData("FavoriteColor has Microsoft.AspNetCore.OData.Tests.Models.Color'Red'")] + public void Validator_Doesnot_Throw_For_ValidQueries(string filter) + { + // Arrange + FilterQueryOption option = new FilterQueryOption(filter, _context); + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, _settings)); + } + + [Fact] + public void Validator_Doesnot_Throw_For_ParameterAlias() + { + // Arrange + FilterQueryOption option = new FilterQueryOption( + "Id eq @p", + _context, + new ODataQueryOptionParser( + _context.Model, + _context.ElementType, + _context.NavigationSource, + new Dictionary { { "$filter", "Id eq @p" }, { "@p", "1" } })); + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, _settings)); + Assert.Equal(6, _validator.Times.Keys.Count); + Assert.Equal(1, _validator.Times["Validate"]); // entry + Assert.Equal(1, _validator.Times["ValidateParameterQueryNode"]); // $it + Assert.Equal(1, _validator.Times["ValidateSingleValuePropertyAccessNode"]); // Id + Assert.Equal(1, _validator.Times["ValidateBinaryOperatorQueryNode"]); // eq + Assert.Equal(1, _validator.Times["ValidateLogicalOperator"]); // eq + Assert.Equal(1, _validator.Times["ValidateConstantQueryNode"]); // 1 + } + + private static TheoryDataSet GetLongInputsTestData(int maxCount) + { + return new TheoryDataSet + { + "" + String.Join(" and ", Enumerable.Range(1, (maxCount/5) + 1).Select(_ => "SupplierID eq 1")), + "" + String.Join(" ", Enumerable.Range(1, maxCount).Select(_ => "not")) + " Discontinued", + "" + String.Join(" add ", Enumerable.Range(1, maxCount/2)) + " eq 5050", + "" + String.Join("/", Enumerable.Range(1, maxCount/2).Select(_ => "Category/Product")) + "/ProductID eq 1", + "" + String.Join("/", Enumerable.Range(1, maxCount/2).Select(_ => "Category/Product")) + "/UnsignedReorderLevel eq 1", + "" + Enumerable.Range(1, maxCount-1).Aggregate("'abc'", (prev,i) => String.Format("trim({0})", prev)) + " eq '123'", + " Category/Products/any(" + Enumerable.Range(1,maxCount/4).Aggregate("", (prev,i) => String.Format("p{1}: p{1}/Category/Products/any({0})", prev, i)) +")" }; - var expectedMessage = string.Format( - "Function '{0}' is not allowed. " + - "To allow it, set the 'AllowedFunctions' property on EnableQueryAttribute or QueryValidationSettings.", - functionName); - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - Assert.NotEqual(AllowedFunctions.None, inner); - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); - } + } + + private class MyFilterValidator : FilterQueryValidator + { + private Dictionary _times = new Dictionary(); - public static TheoryDataSet Functions_CheckNotFilterable + public Dictionary Times { get { - return new TheoryDataSet - { - { "day(NotFilterableDiscontinuedDate) eq 20", "NotFilterableDiscontinuedDate" }, - { "hour(NotFilterableDiscontinuedDate) eq 10", "NotFilterableDiscontinuedDate" }, - { "minute(NotFilterableDiscontinuedDate) eq 20", "NotFilterableDiscontinuedDate" }, - { "month(NotFilterableDiscontinuedDate) eq 10", "NotFilterableDiscontinuedDate" }, - { "second(NotFilterableDiscontinuedDate) eq 20", "NotFilterableDiscontinuedDate" }, - { "year(NotFilterableDiscontinuedDate) eq 2000", "NotFilterableDiscontinuedDate" }, - - { "date(NotFilterableDiscontinuedDate) eq 2015-02-26", "NotFilterableDiscontinuedDate" }, - { "time(NotFilterableDiscontinuedDate) eq 03:4:05.90100", "NotFilterableDiscontinuedDate" }, - { "fractionalseconds(NotFilterableDiscontinuedDate) eq 0.1", "NotFilterableDiscontinuedDate" }, - - { "NotFilterableAlternateAddresses/all(t : t/City eq 'Redmond')", "NotFilterableAlternateAddresses" }, - { "NotFilterableAlternateAddresses/any(t : t/City eq 'Redmond')", "NotFilterableAlternateAddresses" }, - }; + return _times; } } - [Theory] - [MemberData(nameof(Functions_CheckNotFilterable))] - public void Functions_CheckNotFilterable_ThrowODataException(string query, string propertyName) + public override void Validate(FilterQueryOption filterQueryOption, ODataValidationSettings settings) { - // Arrange - var settings = new ODataValidationSettings - { - AllowedFunctions = AllowedFunctions.AllFunctions, - }; - var expectedMessage = string.Format( - "The property '{0}' cannot be used in the $filter query option.", - propertyName); - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + IncrementCount("Validate"); + base.Validate(filterQueryOption, settings); } - public static TheoryDataSet LogicalOperators + protected override void ValidateAllNode(AllNode allQueryNode, FilterValidatorContext context) { - get - { - return new TheoryDataSet - { - { AllowedLogicalOperators.And, "Discontinued and AlternateIDs/any()", "And" }, - { AllowedLogicalOperators.Equal, "UnitPrice add 0 eq UnitPrice", "Equal" }, - { AllowedLogicalOperators.GreaterThan, "UnitPrice add 1 gt UnitPrice", "GreaterThan" }, - { AllowedLogicalOperators.GreaterThanOrEqual, "UnitPrice add 0 ge UnitPrice", "GreaterThanOrEqual" }, - { AllowedLogicalOperators.Has, "Ranking has Microsoft.AspNetCore.OData.Tests.Models.SimpleEnum'First'", "Has" }, - { AllowedLogicalOperators.LessThan, "UnitPrice add -1 lt UnitPrice", "LessThan" }, - { AllowedLogicalOperators.LessThanOrEqual, "UnitPrice add 0 le UnitPrice", "LessThanOrEqual" }, - { AllowedLogicalOperators.Not, "not Discontinued", "Not" }, - { AllowedLogicalOperators.NotEqual, "UnitPrice add 1 ne UnitPrice", "NotEqual" }, - { AllowedLogicalOperators.Or, "Discontinued or AlternateIDs/any()", "Or" }, - }; - } + IncrementCount("ValidateAllQueryNode"); + base.ValidateAllNode(allQueryNode, context); } - [Fact] - public void LogicalOperatorsDataSet_CoversAllValues() + protected override void ValidateAnyNode(AnyNode anyQueryNode, FilterValidatorContext context) { - // Arrange - // Get all values in the AllowedLogicalOperators enum. - var values = new HashSet( - Enum.GetValues(typeof(AllowedLogicalOperators)).Cast()); - var groupValues = new[] - { - AllowedLogicalOperators.All, - AllowedLogicalOperators.None, - }; - - // Act - // Remove the group items. - foreach (var allowed in groupValues) - { - values.Remove(allowed); - } - - // Remove the individual items. - foreach (var allowed in LogicalOperators.Select(item => (AllowedLogicalOperators)(item[0]))) - { - values.Remove(allowed); - } - - // Assert - // Should have nothing left. - Assert.Empty(values); + IncrementCount("ValidateAnyQueryNode"); + base.ValidateAnyNode(anyQueryNode, context); } - [Theory] - [MemberData(nameof(LogicalOperators))] - public void AllowedLogicalOperators_SucceedIfAllowed(AllowedLogicalOperators allow, string query, string unused) + protected override void ValidateArithmeticOperator(BinaryOperatorNode binaryNode, FilterValidatorContext context) { - // Arrange - var settings = new ODataValidationSettings - { - AllowedLogicalOperators = allow, - }; - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - Assert.NotNull(unused); - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + IncrementCount("ValidateArithmeticOperator"); + base.ValidateArithmeticOperator(binaryNode, context); } - [Theory] - [MemberData(nameof(LogicalOperators))] - public void AllowedLogicalOperators_ThrowIfNotAllowed(AllowedLogicalOperators exclude, string query, string operatorName) + protected override void ValidateBinaryOperatorNode(BinaryOperatorNode binaryOperatorNode, FilterValidatorContext context) { - // Arrange - var settings = new ODataValidationSettings - { - AllowedLogicalOperators = AllowedLogicalOperators.All & ~exclude, - }; - var expectedMessage = string.Format( - "Logical operator '{0}' is not allowed. " + - "To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings.", - operatorName); - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + IncrementCount("ValidateBinaryOperatorQueryNode"); + base.ValidateBinaryOperatorNode(binaryOperatorNode, context); } - [Theory] - [MemberData(nameof(LogicalOperators))] - public void AllowedLogicalOperators_ThrowIfNoneAllowed(AllowedLogicalOperators unused, string query, string operatorName) + protected override void ValidateConstantNode(ConstantNode constantNode, FilterValidatorContext context) { - // Arrange - var settings = new ODataValidationSettings - { - AllowedLogicalOperators = AllowedLogicalOperators.None, - }; - var expectedMessage = string.Format( - "Logical operator '{0}' is not allowed. " + - "To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings.", - operatorName); - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - Assert.NotEqual(unused, settings.AllowedLogicalOperators); - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + IncrementCount("ValidateConstantQueryNode"); + base.ValidateConstantNode(constantNode, context); } - public static TheoryDataSet LogicalOperators_CheckArguments + protected override void ValidateConvertNode(ConvertNode convertQueryNode, FilterValidatorContext context) { - get - { - return new TheoryDataSet - { - { "(UnitPrice add 0 eq UnitPrice) and AlternateIDs/any()" }, - { "UnitPrice add 0 eq UnitPrice" }, - { "UnitPrice add 1 gt UnitPrice" }, - { "UnitPrice add 0 ge UnitPrice" }, - { "UnitPrice add -1 lt UnitPrice" }, - { "UnitPrice add 0 le UnitPrice" }, - { "not (UnitPrice add 0 eq UnitPrice)" }, - { "UnitPrice add 1 ne UnitPrice" }, - { "(UnitPrice add 0 eq UnitPrice) or AlternateIDs/any()" }, - - { "Discontinued and (UnitPrice add 0 eq UnitPrice)" }, - { "UnitPrice eq UnitPrice add 0" }, - { "UnitPrice gt UnitPrice add -1" }, - { "UnitPrice ge UnitPrice add 0" }, - { "UnitPrice lt UnitPrice add 1" }, - { "UnitPrice le UnitPrice add 0" }, - { "UnitPrice ne UnitPrice add 1" }, - { "Discontinued or (UnitPrice add 0 eq UnitPrice)" }, - }; - } + IncrementCount("ValidateConvertQueryNode"); + base.ValidateConvertNode(convertQueryNode, context); } - [Theory] - [MemberData(nameof(LogicalOperators_CheckArguments))] - public void LogicalOperators_CheckArguments_SucceedIfAllowed(string query) + protected override void ValidateLogicalOperator(BinaryOperatorNode binaryNode, FilterValidatorContext context) { - // Arrange - var settings = new ODataValidationSettings - { - AllowedArithmeticOperators = AllowedArithmeticOperators.Add, - }; - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + IncrementCount("ValidateLogicalOperator"); + base.ValidateLogicalOperator(binaryNode, context); } - [Theory] - [MemberData(nameof(LogicalOperators_CheckArguments))] - public void LogicalOperators_CheckArguments_ThrowIfNotAllowed(string query) + protected override void ValidateNavigationPropertyNode(QueryNode sourceNode, IEdmNavigationProperty navigationProperty, FilterValidatorContext context) { - // Arrange - var settings = new ODataValidationSettings - { - AllowedArithmeticOperators = AllowedArithmeticOperators.All & ~AllowedArithmeticOperators.Add, - }; - var expectedMessage = string.Format( - "Arithmetic operator 'Add' is not allowed. " + - "To allow it, set the 'AllowedArithmeticOperators' property on EnableQueryAttribute or QueryValidationSettings."); - var option = new FilterQueryOption(query, _productContext); - - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + IncrementCount("ValidateNavigationPropertyNode"); + base.ValidateNavigationPropertyNode(sourceNode, navigationProperty, context); } - [Fact] - public void ArithmeticNegation_SucceedsIfLogicalNotIsAllowed() + protected override void ValidateRangeVariable(RangeVariable rangeVariable, FilterValidatorContext context) { - // Arrange - var settings = new ODataValidationSettings - { - AllowedLogicalOperators = AllowedLogicalOperators.LessThan | AllowedLogicalOperators.Not, - }; - var option = new FilterQueryOption("-UnitPrice lt 0", _productContext); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + IncrementCount("ValidateParameterQueryNode"); + base.ValidateRangeVariable(rangeVariable, context); } - // Note Negate is _not_ a logical operator. - [Fact] - public void ArithmeticNegation_ThrowsIfLogicalNotIsNotAllowed() + protected override void ValidateSingleValuePropertyAccessNode(SingleValuePropertyAccessNode propertyAccessNode, FilterValidatorContext context) { - // Arrange - var settings = new ODataValidationSettings - { - AllowedLogicalOperators = AllowedLogicalOperators.LessThan, - }; - var expectedMessage = string.Format( - "Logical operator 'Negate' is not allowed. " + - "To allow it, set the 'AllowedLogicalOperators' property on EnableQueryAttribute or QueryValidationSettings."); - var option = new FilterQueryOption("-UnitPrice lt 0", _productContext); - - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + IncrementCount("ValidateSingleValuePropertyAccessNode"); + base.ValidateSingleValuePropertyAccessNode(propertyAccessNode, context); } - [Fact] - public void ValidateVisitLogicalOperatorEqual() + protected override void ValidateSingleComplexNode(SingleComplexNode singleComplexNode, FilterValidatorContext context) { - // Arrange - FilterQueryOption option = new FilterQueryOption("Id eq 1", _context); - - // Act - _validator.Validate(option, _settings); - - // Assert - Assert.Equal(6, _validator.Times.Keys.Count); - Assert.Equal(1, _validator.Times["Validate"]); // entry - Assert.Equal(1, _validator.Times["ValidateSingleValuePropertyAccessNode"]); // Id - Assert.Equal(1, _validator.Times["ValidateLogicalOperator"]); // eq - Assert.Equal(1, _validator.Times["ValidateConstantQueryNode"]); // 1 - Assert.Equal(1, _validator.Times["ValidateBinaryOperatorQueryNode"]); // eq - Assert.Equal(1, _validator.Times["ValidateParameterQueryNode"]); // $it + IncrementCount("ValidateSingleComplexNode"); + base.ValidateSingleComplexNode(singleComplexNode, context); } - [Fact] - public void ValidateVisitLogicalOperatorHas() + protected override void ValidateCollectionPropertyAccessNode(CollectionPropertyAccessNode propertyAccessNode, FilterValidatorContext context) { - // Arrange - FilterQueryOption option = new FilterQueryOption("FavoriteColor has Microsoft.AspNetCore.OData.Tests.Models.Color'Red'", _context); - - // Act - _validator.Validate(option, _settings); - - // Assert - Assert.Equal(6, _validator.Times.Keys.Count); - Assert.Equal(1, _validator.Times["Validate"]); // entry - Assert.Equal(1, _validator.Times["ValidateSingleValuePropertyAccessNode"]); // FavouriteColor - Assert.Equal(1, _validator.Times["ValidateLogicalOperator"]); // has - Assert.Equal(1, _validator.Times["ValidateBinaryOperatorQueryNode"]); // has - Assert.Equal(1, _validator.Times["ValidateParameterQueryNode"]); // $it + IncrementCount("ValidateCollectionPropertyAccessNode"); + base.ValidateCollectionPropertyAccessNode(propertyAccessNode, context); } - [Theory] - [InlineData("Id eq 1")] - [InlineData("Id ne 1")] - [InlineData("Id gt 1")] - [InlineData("Id lt 1")] - [InlineData("Id ge 1")] - [InlineData("Id le 1")] - [InlineData("Id eq Id add 1")] - [InlineData("Id eq Id sub 1")] - [InlineData("Id eq Id mul 1")] - [InlineData("Id eq Id div 1")] - [InlineData("Id eq Id mod 1")] - [InlineData("startswith(Name, 'Microsoft')")] - [InlineData("endswith(Name, 'Microsoft')")] - [InlineData("contains(Name, 'Microsoft')")] - [InlineData("substring(Name, 1) eq 'Name'")] - [InlineData("substring(Name, 1, 2) eq 'Name'")] - [InlineData("length(Name) eq 1")] - [InlineData("tolower(Name) eq 'Name'")] - [InlineData("toupper(Name) eq 'Name'")] - [InlineData("trim(Name) eq 'Name'")] - [InlineData("indexof(Name, 'Microsoft') eq 1")] - [InlineData("concat(Name, 'Microsoft') eq 'Microsoft'")] - [InlineData("year(Birthday) eq 2000")] - [InlineData("month(Birthday) eq 2000")] - [InlineData("day(Birthday) eq 2000")] - [InlineData("hour(Birthday) eq 2000")] - [InlineData("minute(Birthday) eq 2000")] - [InlineData("round(AmountSpent) eq 0")] - [InlineData("floor(AmountSpent) eq 0")] - [InlineData("ceiling(AmountSpent) eq 0")] - [InlineData("Tags/any()")] - [InlineData("Tags/all(t : t eq '1')")] - [InlineData("Microsoft.AspNetCore.OData.Tests.Query.Models.QueryCompositionCustomerBase/Id eq 1")] - [InlineData("Contacts/Microsoft.AspNetCore.OData.Tests.Query.Models.QueryCompositionCustomerBase/any()")] - [InlineData("FavoriteColor has Microsoft.AspNetCore.OData.Tests.Models.Color'Red'")] - public void Validator_Doesnot_Throw_For_ValidQueries(string filter) - { - // Arrange - FilterQueryOption option = new FilterQueryOption(filter, _context); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, _settings)); + protected override void ValidateCollectionComplexNode(CollectionComplexNode collectionComplexNode, FilterValidatorContext context) + { + IncrementCount("ValidateCollectionComplexNode"); + base.ValidateCollectionComplexNode(collectionComplexNode, context); } - [Fact] - public void Validator_Doesnot_Throw_For_ParameterAlias() - { - // Arrange - FilterQueryOption option = new FilterQueryOption( - "Id eq @p", - _context, - new ODataQueryOptionParser( - _context.Model, - _context.ElementType, - _context.NavigationSource, - new Dictionary { { "$filter", "Id eq @p" }, { "@p", "1" } })); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, _settings)); - Assert.Equal(6, _validator.Times.Keys.Count); - Assert.Equal(1, _validator.Times["Validate"]); // entry - Assert.Equal(1, _validator.Times["ValidateParameterQueryNode"]); // $it - Assert.Equal(1, _validator.Times["ValidateSingleValuePropertyAccessNode"]); // Id - Assert.Equal(1, _validator.Times["ValidateBinaryOperatorQueryNode"]); // eq - Assert.Equal(1, _validator.Times["ValidateLogicalOperator"]); // eq - Assert.Equal(1, _validator.Times["ValidateConstantQueryNode"]); // 1 + protected override void ValidateSingleValueFunctionCallNode(SingleValueFunctionCallNode node, FilterValidatorContext context) + { + IncrementCount("ValidateSingleValueFunctionCallQueryNode"); + base.ValidateSingleValueFunctionCallNode(node, context); } - private static TheoryDataSet GetLongInputsTestData(int maxCount) + protected override void ValidateUnaryOperatorNode(UnaryOperatorNode unaryOperatorQueryNode, FilterValidatorContext context) { - return new TheoryDataSet - { - "" + String.Join(" and ", Enumerable.Range(1, (maxCount/5) + 1).Select(_ => "SupplierID eq 1")), - "" + String.Join(" ", Enumerable.Range(1, maxCount).Select(_ => "not")) + " Discontinued", - "" + String.Join(" add ", Enumerable.Range(1, maxCount/2)) + " eq 5050", - "" + String.Join("/", Enumerable.Range(1, maxCount/2).Select(_ => "Category/Product")) + "/ProductID eq 1", - "" + String.Join("/", Enumerable.Range(1, maxCount/2).Select(_ => "Category/Product")) + "/UnsignedReorderLevel eq 1", - "" + Enumerable.Range(1, maxCount-1).Aggregate("'abc'", (prev,i) => String.Format("trim({0})", prev)) + " eq '123'", - " Category/Products/any(" + Enumerable.Range(1,maxCount/4).Aggregate("", (prev,i) => String.Format("p{1}: p{1}/Category/Products/any({0})", prev, i)) +")" - }; + IncrementCount("ValidateUnaryOperatorQueryNode"); + base.ValidateUnaryOperatorNode(unaryOperatorQueryNode, context); } - private class MyFilterValidator : FilterQueryValidator + private void IncrementCount(string functionName) { - private Dictionary _times = new Dictionary(); - - public Dictionary Times + int count; + if (_times.TryGetValue(functionName, out count)) { - get - { - return _times; - } + _times[functionName] = ++count; } - - public override void Validate(FilterQueryOption filterQueryOption, ODataValidationSettings settings) - { - IncrementCount("Validate"); - base.Validate(filterQueryOption, settings); - } - - protected override void ValidateAllNode(AllNode allQueryNode, FilterValidatorContext context) - { - IncrementCount("ValidateAllQueryNode"); - base.ValidateAllNode(allQueryNode, context); - } - - protected override void ValidateAnyNode(AnyNode anyQueryNode, FilterValidatorContext context) - { - IncrementCount("ValidateAnyQueryNode"); - base.ValidateAnyNode(anyQueryNode, context); - } - - protected override void ValidateArithmeticOperator(BinaryOperatorNode binaryNode, FilterValidatorContext context) - { - IncrementCount("ValidateArithmeticOperator"); - base.ValidateArithmeticOperator(binaryNode, context); - } - - protected override void ValidateBinaryOperatorNode(BinaryOperatorNode binaryOperatorNode, FilterValidatorContext context) - { - IncrementCount("ValidateBinaryOperatorQueryNode"); - base.ValidateBinaryOperatorNode(binaryOperatorNode, context); - } - - protected override void ValidateConstantNode(ConstantNode constantNode, FilterValidatorContext context) - { - IncrementCount("ValidateConstantQueryNode"); - base.ValidateConstantNode(constantNode, context); - } - - protected override void ValidateConvertNode(ConvertNode convertQueryNode, FilterValidatorContext context) - { - IncrementCount("ValidateConvertQueryNode"); - base.ValidateConvertNode(convertQueryNode, context); - } - - protected override void ValidateLogicalOperator(BinaryOperatorNode binaryNode, FilterValidatorContext context) - { - IncrementCount("ValidateLogicalOperator"); - base.ValidateLogicalOperator(binaryNode, context); - } - - protected override void ValidateNavigationPropertyNode(QueryNode sourceNode, IEdmNavigationProperty navigationProperty, FilterValidatorContext context) - { - IncrementCount("ValidateNavigationPropertyNode"); - base.ValidateNavigationPropertyNode(sourceNode, navigationProperty, context); - } - - protected override void ValidateRangeVariable(RangeVariable rangeVariable, FilterValidatorContext context) - { - IncrementCount("ValidateParameterQueryNode"); - base.ValidateRangeVariable(rangeVariable, context); - } - - protected override void ValidateSingleValuePropertyAccessNode(SingleValuePropertyAccessNode propertyAccessNode, FilterValidatorContext context) - { - IncrementCount("ValidateSingleValuePropertyAccessNode"); - base.ValidateSingleValuePropertyAccessNode(propertyAccessNode, context); - } - - protected override void ValidateSingleComplexNode(SingleComplexNode singleComplexNode, FilterValidatorContext context) - { - IncrementCount("ValidateSingleComplexNode"); - base.ValidateSingleComplexNode(singleComplexNode, context); - } - - protected override void ValidateCollectionPropertyAccessNode(CollectionPropertyAccessNode propertyAccessNode, FilterValidatorContext context) - { - IncrementCount("ValidateCollectionPropertyAccessNode"); - base.ValidateCollectionPropertyAccessNode(propertyAccessNode, context); - } - - protected override void ValidateCollectionComplexNode(CollectionComplexNode collectionComplexNode, FilterValidatorContext context) - { - IncrementCount("ValidateCollectionComplexNode"); - base.ValidateCollectionComplexNode(collectionComplexNode, context); - } - - protected override void ValidateSingleValueFunctionCallNode(SingleValueFunctionCallNode node, FilterValidatorContext context) - { - IncrementCount("ValidateSingleValueFunctionCallQueryNode"); - base.ValidateSingleValueFunctionCallNode(node, context); - } - - protected override void ValidateUnaryOperatorNode(UnaryOperatorNode unaryOperatorQueryNode, FilterValidatorContext context) - { - IncrementCount("ValidateUnaryOperatorQueryNode"); - base.ValidateUnaryOperatorNode(unaryOperatorQueryNode, context); - } - - private void IncrementCount(string functionName) + else { - int count; - if (_times.TryGetValue(functionName, out count)) - { - _times[functionName] = ++count; - } - else - { - // first time - _times[functionName] = 1; - } + // first time + _times[functionName] = 1; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/FilterValidatorContextTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/FilterValidatorContextTests.cs index ff7f82573..7e7c91079 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/FilterValidatorContextTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/FilterValidatorContextTests.cs @@ -8,66 +8,65 @@ using Microsoft.AspNetCore.OData.Query.Validator; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Validator +namespace Microsoft.AspNetCore.OData.Tests.Query.Validator; + +public class FilterValidatorContextTests { - public class FilterValidatorContextTests + [Fact] + public void Clone_ClosesContext() { - [Fact] - public void Clone_ClosesContext() + // Arrange & Act & Assert + FilterValidatorContext context = new FilterValidatorContext { - // Arrange & Act & Assert - FilterValidatorContext context = new FilterValidatorContext - { - ValidationSettings = new ODataValidationSettings { MaxNodeCount = 99 } - }; + ValidationSettings = new ODataValidationSettings { MaxNodeCount = 99 } + }; - FilterValidatorContext newContext = context.Clone(); - Assert.Equal(99, newContext.ValidationSettings.MaxNodeCount); - } + FilterValidatorContext newContext = context.Clone(); + Assert.Equal(99, newContext.ValidationSettings.MaxNodeCount); + } - [Fact] - public void IncrementNodeCount_IncreasesNodeCount() + [Fact] + public void IncrementNodeCount_IncreasesNodeCount() + { + // Arrange & Act & Assert + FilterValidatorContext context = new FilterValidatorContext { - // Arrange & Act & Assert - FilterValidatorContext context = new FilterValidatorContext - { - ValidationSettings = new ODataValidationSettings { MaxNodeCount = 100 } - }; - Assert.Equal(0, context.CurrentNodeCount); + ValidationSettings = new ODataValidationSettings { MaxNodeCount = 100 } + }; + Assert.Equal(0, context.CurrentNodeCount); - context.IncrementNodeCount(); - Assert.Equal(1, context.CurrentNodeCount); - } + context.IncrementNodeCount(); + Assert.Equal(1, context.CurrentNodeCount); + } - [Fact] - public void EnterLambda_IncreasesAnyAllExpressionDepth() + [Fact] + public void EnterLambda_IncreasesAnyAllExpressionDepth() + { + // Arrange + FilterValidatorContext context = new FilterValidatorContext { - // Arrange - FilterValidatorContext context = new FilterValidatorContext - { - ValidationSettings = new ODataValidationSettings { MaxAnyAllExpressionDepth = 100 } - }; - Assert.Equal(0, context.CurrentAnyAllExpressionDepth); + ValidationSettings = new ODataValidationSettings { MaxAnyAllExpressionDepth = 100 } + }; + Assert.Equal(0, context.CurrentAnyAllExpressionDepth); - context.EnterLambda(); - Assert.Equal(1, context.CurrentAnyAllExpressionDepth); - } + context.EnterLambda(); + Assert.Equal(1, context.CurrentAnyAllExpressionDepth); + } - [Fact] - public void ExitLambda_DecreasesAnyAllExpressionDepth() + [Fact] + public void ExitLambda_DecreasesAnyAllExpressionDepth() + { + // Arrange + FilterValidatorContext context = new FilterValidatorContext { - // Arrange - FilterValidatorContext context = new FilterValidatorContext - { - ValidationSettings = new ODataValidationSettings { MaxAnyAllExpressionDepth = 100 } - }; - Assert.Equal(0, context.CurrentAnyAllExpressionDepth); + ValidationSettings = new ODataValidationSettings { MaxAnyAllExpressionDepth = 100 } + }; + Assert.Equal(0, context.CurrentAnyAllExpressionDepth); - context.EnterLambda(); - context.EnterLambda(); - Assert.Equal(2, context.CurrentAnyAllExpressionDepth); - context.ExitLambda(); - Assert.Equal(1, context.CurrentAnyAllExpressionDepth); - } + context.EnterLambda(); + context.EnterLambda(); + Assert.Equal(2, context.CurrentAnyAllExpressionDepth); + context.ExitLambda(); + Assert.Equal(1, context.CurrentAnyAllExpressionDepth); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ODataQueryValidatorTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ODataQueryValidatorTest.cs index 7db2107d5..0ad3c0c9e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ODataQueryValidatorTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ODataQueryValidatorTest.cs @@ -17,276 +17,275 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Validator +namespace Microsoft.AspNetCore.OData.Tests.Query.Validator; + +public class ODataQueryValidatorTest { - public class ODataQueryValidatorTest - { - private ODataQueryValidator _validator; - private ODataQueryContext _context; + private ODataQueryValidator _validator; + private ODataQueryContext _context; - public ODataQueryValidatorTest() - { - _validator = new ODataQueryValidator(); - _context = ValidationTestHelper.CreateCustomerContext(false); - } + public ODataQueryValidatorTest() + { + _validator = new ODataQueryValidator(); + _context = ValidationTestHelper.CreateCustomerContext(false); + } - public static TheoryDataSet SupportedQueryOptions + public static TheoryDataSet SupportedQueryOptions + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { AllowedQueryOptions.Count, "$count=true", "Count" }, - { AllowedQueryOptions.Expand, "$expand=Contacts", "Expand" }, - { AllowedQueryOptions.Filter, "$filter=Name eq 'Name'", "Filter" }, - { AllowedQueryOptions.Format, "$format=json", "Format" }, - { AllowedQueryOptions.OrderBy, "$orderby=Name", "OrderBy" }, - { AllowedQueryOptions.Select, "$select=Name", "Select" }, - { AllowedQueryOptions.Skip, "$skip=5", "Skip" }, - { AllowedQueryOptions.Top, "$top=10", "Top" }, - { AllowedQueryOptions.Apply, "$apply=groupby((Name))", "Apply" }, - { AllowedQueryOptions.Compute, "$compute=AmountSpent mul 2 as DoubleAmount", "Compute" }, - { AllowedQueryOptions.Search, "$search=text", "Search" }, - { AllowedQueryOptions.SkipToken, "$skiptoken=__skip__", "SkipToken" }, - }; - } + { AllowedQueryOptions.Count, "$count=true", "Count" }, + { AllowedQueryOptions.Expand, "$expand=Contacts", "Expand" }, + { AllowedQueryOptions.Filter, "$filter=Name eq 'Name'", "Filter" }, + { AllowedQueryOptions.Format, "$format=json", "Format" }, + { AllowedQueryOptions.OrderBy, "$orderby=Name", "OrderBy" }, + { AllowedQueryOptions.Select, "$select=Name", "Select" }, + { AllowedQueryOptions.Skip, "$skip=5", "Skip" }, + { AllowedQueryOptions.Top, "$top=10", "Top" }, + { AllowedQueryOptions.Apply, "$apply=groupby((Name))", "Apply" }, + { AllowedQueryOptions.Compute, "$compute=AmountSpent mul 2 as DoubleAmount", "Compute" }, + { AllowedQueryOptions.Search, "$search=text", "Search" }, + { AllowedQueryOptions.SkipToken, "$skiptoken=__skip__", "SkipToken" }, + }; } + } - public static TheoryDataSet UnsupportedQueryOptions + public static TheoryDataSet UnsupportedQueryOptions + { + get { - get + return new TheoryDataSet { - return new TheoryDataSet - { - { AllowedQueryOptions.DeltaToken, "$deltatoken=__delta__", "DeltaToken" }, - }; - } - } - - [Fact] - public void ValidateThrowsOnNullOption() - { - ExceptionAssert.Throws(() => - _validator.Validate(null, new ODataValidationSettings())); - } - - [Fact] - public void ValidateThrowsOnNullSettings() - { - var message = RequestFactory.Create(); - - ExceptionAssert.Throws(() => - _validator.Validate(new ODataQueryOptions(_context, message), null)); + { AllowedQueryOptions.DeltaToken, "$deltatoken=__delta__", "DeltaToken" }, + }; } + } - [Fact] - public void QueryOptionDataSets_CoverAllValues() - { - // Arrange - // Get all values in the AllowedQueryOptions enum. - var values = new HashSet( - Enum.GetValues(typeof(AllowedQueryOptions)).Cast()); + [Fact] + public void ValidateThrowsOnNullOption() + { + ExceptionAssert.Throws(() => + _validator.Validate(null, new ODataValidationSettings())); + } - var groupValues = new[] - { - AllowedQueryOptions.All, - AllowedQueryOptions.None, - AllowedQueryOptions.Supported, - }; - var dataSets = SupportedQueryOptions.Concat(UnsupportedQueryOptions); + [Fact] + public void ValidateThrowsOnNullSettings() + { + var message = RequestFactory.Create(); - // Act - // Remove the group items. - foreach (var allowed in groupValues) - { - values.Remove(allowed); - } + ExceptionAssert.Throws(() => + _validator.Validate(new ODataQueryOptions(_context, message), null)); + } - // Remove the individual items. - foreach (var allowed in dataSets.Select(item => (AllowedQueryOptions)(item[0]))) - { - values.Remove(allowed); - } + [Fact] + public void QueryOptionDataSets_CoverAllValues() + { + // Arrange + // Get all values in the AllowedQueryOptions enum. + var values = new HashSet( + Enum.GetValues(typeof(AllowedQueryOptions)).Cast()); - // Assert - // Should have nothing left. - Assert.Empty(values); + var groupValues = new[] + { + AllowedQueryOptions.All, + AllowedQueryOptions.None, + AllowedQueryOptions.Supported, + }; + var dataSets = SupportedQueryOptions.Concat(UnsupportedQueryOptions); + + // Act + // Remove the group items. + foreach (var allowed in groupValues) + { + values.Remove(allowed); } - [Theory] - [MemberData(nameof(SupportedQueryOptions))] - [MemberData(nameof(UnsupportedQueryOptions))] - public void AllowedQueryOptions_SucceedIfAllowed(AllowedQueryOptions allow, string query, string unused) + // Remove the individual items. + foreach (var allowed in dataSets.Select(item => (AllowedQueryOptions)(item[0]))) { - // Arrange - var message = RequestFactory.Create("Get", "http://localhost/?$" + query, setupAction: null); - ODataQueryOptions option = new ODataQueryOptions(_context, message); - ODataValidationSettings settings = new ODataValidationSettings() - { - AllowedQueryOptions = allow, - }; - - // Act & Assert - Assert.NotNull(unused); - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + values.Remove(allowed); } - [Theory] - [MemberData(nameof(SupportedQueryOptions))] - [MemberData(nameof(UnsupportedQueryOptions))] - public void AllowedQueryOptions_ThrowIfNotAllowed(AllowedQueryOptions exclude, string query, string optionName) + // Assert + // Should have nothing left. + Assert.Empty(values); + } + + [Theory] + [MemberData(nameof(SupportedQueryOptions))] + [MemberData(nameof(UnsupportedQueryOptions))] + public void AllowedQueryOptions_SucceedIfAllowed(AllowedQueryOptions allow, string query, string unused) + { + // Arrange + var message = RequestFactory.Create("Get", "http://localhost/?$" + query, setupAction: null); + ODataQueryOptions option = new ODataQueryOptions(_context, message); + ODataValidationSettings settings = new ODataValidationSettings() { - // Arrange - var message = RequestFactory.Create("Get", "http://localhost/?" + query, setupAction: null); - var option = new ODataQueryOptions(_context, message); - var expectedMessage = string.Format( - "Query option '{0}' is not allowed. " + - "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", - optionName); - var settings = new ODataValidationSettings() - { - AllowedQueryOptions = AllowedQueryOptions.All & ~exclude, - }; + AllowedQueryOptions = allow, + }; - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); - } + // Act & Assert + Assert.NotNull(unused); + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } - [Theory] - [MemberData(nameof(SupportedQueryOptions))] - [MemberData(nameof(UnsupportedQueryOptions))] - public void AllowedQueryOptions_ThrowIfNoneAllowed(AllowedQueryOptions unused, string query, string optionName) + [Theory] + [MemberData(nameof(SupportedQueryOptions))] + [MemberData(nameof(UnsupportedQueryOptions))] + public void AllowedQueryOptions_ThrowIfNotAllowed(AllowedQueryOptions exclude, string query, string optionName) + { + // Arrange + var message = RequestFactory.Create("Get", "http://localhost/?" + query, setupAction: null); + var option = new ODataQueryOptions(_context, message); + var expectedMessage = string.Format( + "Query option '{0}' is not allowed. " + + "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", + optionName); + var settings = new ODataValidationSettings() { - // Arrange - var message = RequestFactory.Create("Get", "http://localhost/?" + query, setupAction: null); - var option = new ODataQueryOptions(_context, message); - var expectedMessage = string.Format( - "Query option '{0}' is not allowed. " + - "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", - optionName); - var settings = new ODataValidationSettings() - { - AllowedQueryOptions = AllowedQueryOptions.None, - }; + AllowedQueryOptions = AllowedQueryOptions.All & ~exclude, + }; - // Act & Assert - Assert.NotEqual(unused, settings.AllowedQueryOptions); - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); - } + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } - [Theory] - [MemberData(nameof(SupportedQueryOptions))] - public void SupportedQueryOptions_SucceedIfGroupAllowed(AllowedQueryOptions unused, string query, string unusedName) + [Theory] + [MemberData(nameof(SupportedQueryOptions))] + [MemberData(nameof(UnsupportedQueryOptions))] + public void AllowedQueryOptions_ThrowIfNoneAllowed(AllowedQueryOptions unused, string query, string optionName) + { + // Arrange + var message = RequestFactory.Create("Get", "http://localhost/?" + query, setupAction: null); + var option = new ODataQueryOptions(_context, message); + var expectedMessage = string.Format( + "Query option '{0}' is not allowed. " + + "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", + optionName); + var settings = new ODataValidationSettings() { - // Arrange - var message = RequestFactory.Create("Get", "http://localhost/?$" + query, setupAction: null); - ODataQueryOptions option = new ODataQueryOptions(_context, message); - ODataValidationSettings settings = new ODataValidationSettings() - { - AllowedQueryOptions = AllowedQueryOptions.Supported, - }; + AllowedQueryOptions = AllowedQueryOptions.None, + }; - // Act & Assert - Assert.NotEqual(unused, settings.AllowedQueryOptions); - Assert.NotNull(unusedName); - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); - } + // Act & Assert + Assert.NotEqual(unused, settings.AllowedQueryOptions); + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } - [Theory] - [MemberData(nameof(SupportedQueryOptions))] - public void SupportedQueryOptions_ThrowIfGroupNotAllowed(AllowedQueryOptions unused, string query, string optionName) + [Theory] + [MemberData(nameof(SupportedQueryOptions))] + public void SupportedQueryOptions_SucceedIfGroupAllowed(AllowedQueryOptions unused, string query, string unusedName) + { + // Arrange + var message = RequestFactory.Create("Get", "http://localhost/?$" + query, setupAction: null); + ODataQueryOptions option = new ODataQueryOptions(_context, message); + ODataValidationSettings settings = new ODataValidationSettings() { - // Arrange - var message = RequestFactory.Create("Get", "http://localhost/?" + query, setupAction: null); - var option = new ODataQueryOptions(_context, message); - var expectedMessage = string.Format( - "Query option '{0}' is not allowed. " + - "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", - optionName); - var settings = new ODataValidationSettings() - { - AllowedQueryOptions = AllowedQueryOptions.All & ~AllowedQueryOptions.Supported, - }; + AllowedQueryOptions = AllowedQueryOptions.Supported, + }; - // Act & Assert - Assert.NotEqual(unused, settings.AllowedQueryOptions); - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); - } + // Act & Assert + Assert.NotEqual(unused, settings.AllowedQueryOptions); + Assert.NotNull(unusedName); + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } - [Theory] - [MemberData(nameof(UnsupportedQueryOptions))] - public void UnsupportedQueryOptions_SucceedIfGroupAllowed(AllowedQueryOptions unused, string query, string unusedName) + [Theory] + [MemberData(nameof(SupportedQueryOptions))] + public void SupportedQueryOptions_ThrowIfGroupNotAllowed(AllowedQueryOptions unused, string query, string optionName) + { + // Arrange + var message = RequestFactory.Create("Get", "http://localhost/?" + query, setupAction: null); + var option = new ODataQueryOptions(_context, message); + var expectedMessage = string.Format( + "Query option '{0}' is not allowed. " + + "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", + optionName); + var settings = new ODataValidationSettings() { - // Arrange - var message = RequestFactory.Create("Get", "http://localhost/?$" + query, setupAction: null); - ODataQueryOptions option = new ODataQueryOptions(_context, message); - ODataValidationSettings settings = new ODataValidationSettings() - { - AllowedQueryOptions = AllowedQueryOptions.All & ~AllowedQueryOptions.Supported, - }; + AllowedQueryOptions = AllowedQueryOptions.All & ~AllowedQueryOptions.Supported, + }; - // Act & Assert - Assert.Equal(unused, settings.AllowedQueryOptions); //Equal because only Delta token is unsupported. - Assert.NotNull(unusedName); - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); - } + // Act & Assert + Assert.NotEqual(unused, settings.AllowedQueryOptions); + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } - [Theory] - [MemberData(nameof(UnsupportedQueryOptions))] - public void UnsupportedQueryOptions_ThrowIfGroupNotAllowed(AllowedQueryOptions unused, string query, string optionName) + [Theory] + [MemberData(nameof(UnsupportedQueryOptions))] + public void UnsupportedQueryOptions_SucceedIfGroupAllowed(AllowedQueryOptions unused, string query, string unusedName) + { + // Arrange + var message = RequestFactory.Create("Get", "http://localhost/?$" + query, setupAction: null); + ODataQueryOptions option = new ODataQueryOptions(_context, message); + ODataValidationSettings settings = new ODataValidationSettings() { - // Arrange - var message = RequestFactory.Create("Get", "http://localhost/?" + query, setupAction: null); - var option = new ODataQueryOptions(_context, message); - var expectedMessage = string.Format( - "Query option '{0}' is not allowed. " + - "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", - optionName); - var settings = new ODataValidationSettings() - { - AllowedQueryOptions = AllowedQueryOptions.Supported, - }; + AllowedQueryOptions = AllowedQueryOptions.All & ~AllowedQueryOptions.Supported, + }; - // Act & Assert - Assert.NotEqual(unused, settings.AllowedQueryOptions); - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); - } + // Act & Assert + Assert.Equal(unused, settings.AllowedQueryOptions); //Equal because only Delta token is unsupported. + Assert.NotNull(unusedName); + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } - [Fact] - public void Validate_ValidatesSelectExpandQueryOption_IfItIsNotNull() + [Theory] + [MemberData(nameof(UnsupportedQueryOptions))] + public void UnsupportedQueryOptions_ThrowIfGroupNotAllowed(AllowedQueryOptions unused, string query, string optionName) + { + // Arrange + var message = RequestFactory.Create("Get", "http://localhost/?" + query, setupAction: null); + var option = new ODataQueryOptions(_context, message); + var expectedMessage = string.Format( + "Query option '{0}' is not allowed. " + + "To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.", + optionName); + var settings = new ODataValidationSettings() { - // Arrange - var message = RequestFactory.Create("Get", "http://localhost/?$expand=Contacts/Contacts", setupAction: null); - ODataQueryOptions option = new ODataQueryOptions(_context, message); + AllowedQueryOptions = AllowedQueryOptions.Supported, + }; - Mock selectExpandValidator = new Mock(); - option.SelectExpand.Validator = selectExpandValidator.Object; - ODataValidationSettings settings = new ODataValidationSettings(); + // Act & Assert + Assert.NotEqual(unused, settings.AllowedQueryOptions); + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); + } - // Act - _validator.Validate(option, settings); + [Fact] + public void Validate_ValidatesSelectExpandQueryOption_IfItIsNotNull() + { + // Arrange + var message = RequestFactory.Create("Get", "http://localhost/?$expand=Contacts/Contacts", setupAction: null); + ODataQueryOptions option = new ODataQueryOptions(_context, message); - // Assert - selectExpandValidator.Verify(v => v.Validate(option.SelectExpand, settings), Times.Once()); - } + Mock selectExpandValidator = new Mock(); + option.SelectExpand.Validator = selectExpandValidator.Object; + ODataValidationSettings settings = new ODataValidationSettings(); - [Theory] - [InlineData("$select=")] - [InlineData("$select= ")] - [InlineData("$expand=")] - [InlineData("$expand= ")] - [InlineData("$select= &$expand= &")] - public void Validate_ValidatesNotEmptyOrWhitespaceSelectExpandQueryOption_IfEmptyOrWhitespace(string query) - { - var expectedMessage = "'select' and 'expand' cannot be empty or whitespace. Omit the parameter from the query if it is not used."; + // Act + _validator.Validate(option, settings); + + // Assert + selectExpandValidator.Verify(v => v.Validate(option.SelectExpand, settings), Times.Once()); + } + + [Theory] + [InlineData("$select=")] + [InlineData("$select= ")] + [InlineData("$expand=")] + [InlineData("$expand= ")] + [InlineData("$select= &$expand= &")] + public void Validate_ValidatesNotEmptyOrWhitespaceSelectExpandQueryOption_IfEmptyOrWhitespace(string query) + { + var expectedMessage = "'select' and 'expand' cannot be empty or whitespace. Omit the parameter from the query if it is not used."; - // Arrange - var message = RequestFactory.Create("Get", $"http://localhost/?{query}", setupAction: null); - ODataQueryOptions option = new ODataQueryOptions(_context, message); - ODataValidationSettings settings = new ODataValidationSettings(); + // Arrange + var message = RequestFactory.Create("Get", $"http://localhost/?{query}", setupAction: null); + ODataQueryOptions option = new ODataQueryOptions(_context, message); + ODataValidationSettings settings = new ODataValidationSettings(); - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); - } + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(option, settings), expectedMessage); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ODataValidationSettingsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ODataValidationSettingsTests.cs index f86075410..20c0dd353 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ODataValidationSettingsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ODataValidationSettingsTests.cs @@ -10,188 +10,187 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Validator +namespace Microsoft.AspNetCore.OData.Tests.Query.Validator; + +public class ODataValidationSettingsTests { - public class ODataValidationSettingsTests + [Fact] + public void Ctor_Initializes_All_Properties() + { + // Arrange & Act + ODataValidationSettings querySettings = new ODataValidationSettings(); + + // Assert + Assert.Equal(AllowedArithmeticOperators.All, querySettings.AllowedArithmeticOperators); + Assert.Equal(AllowedFunctions.AllFunctions, querySettings.AllowedFunctions); + Assert.Equal(AllowedLogicalOperators.All, querySettings.AllowedLogicalOperators); + Assert.Empty(querySettings.AllowedOrderByProperties); + Assert.Equal(AllowedQueryOptions.Supported, querySettings.AllowedQueryOptions); + Assert.Equal(1, querySettings.MaxAnyAllExpressionDepth); + Assert.Equal(100, querySettings.MaxNodeCount); + Assert.Null(querySettings.MaxSkip); + Assert.Null(querySettings.MaxTop); + } + + [Fact] + public void AllowedFunctions_Property_RoundTrips() + { + // Arrange & Act & Assert + ReflectionAssert.EnumProperty( + new ODataValidationSettings(), + o => o.AllowedFunctions, + expectedDefaultValue: AllowedFunctions.AllFunctions, + illegalValue: AllowedFunctions.AllFunctions + 1, + roundTripTestValue: AllowedFunctions.AllMathFunctions); + } + + [Fact] + public void AllowedFunctions_SetToAllFunctions_DoesNotThrow() + { + // Arrange & Act & Assert + ExceptionAssert.DoesNotThrow(() => new ODataValidationSettings().AllowedFunctions = AllowedFunctions.AllFunctions); + } + + [Fact] + public void AllowedArithmeticOperators_Property_RoundTrips() + { + // Arrange & Act & Assert + ReflectionAssert.EnumProperty( + new ODataValidationSettings(), + o => o.AllowedArithmeticOperators, + expectedDefaultValue: AllowedArithmeticOperators.All, + illegalValue: AllowedArithmeticOperators.All + 1, + roundTripTestValue: AllowedArithmeticOperators.Multiply); + } + + [Fact] + public void AllowedLogicalOperators_Property_RoundTrips() + { + // Arrange & Act & Assert + ReflectionAssert.EnumProperty( + new ODataValidationSettings(), + o => o.AllowedLogicalOperators, + expectedDefaultValue: AllowedLogicalOperators.All, + illegalValue: AllowedLogicalOperators.All + 1, + roundTripTestValue: AllowedLogicalOperators.GreaterThanOrEqual | AllowedLogicalOperators.LessThanOrEqual); + } + + [Fact] + public void AllowedQueryOptions_Property_RoundTrips() + { + // Arrange & Act & Assert + ReflectionAssert.EnumProperty( + new ODataValidationSettings(), + o => o.AllowedQueryOptions, + expectedDefaultValue: AllowedQueryOptions.Supported, + illegalValue: AllowedQueryOptions.All + 1, + roundTripTestValue: AllowedQueryOptions.Filter); + } + + [Fact] + public void AllowedOrderByProperties_Property_RoundTrips() + { + // Arrange & Act + ODataValidationSettings settings = new ODataValidationSettings(); + Assert.NotNull(settings.AllowedOrderByProperties); + Assert.Empty(settings.AllowedOrderByProperties); + + settings.AllowedOrderByProperties.Add("Id"); + settings.AllowedOrderByProperties.Add("Name"); + + // Assert + Assert.Equal(2, settings.AllowedOrderByProperties.Count); + Assert.Contains("Id", settings.AllowedOrderByProperties); + Assert.Contains("Name", settings.AllowedOrderByProperties); + } + + [Fact] + public void MaxAnyAllExpressionDepth_Property_RoundTrips() + { + // Arrange & Act & Assert + ReflectionAssert.IntegerProperty( + new ODataValidationSettings(), + o => o.MaxAnyAllExpressionDepth, + expectedDefaultValue: 1, + minLegalValue: 1, + maxLegalValue: int.MaxValue, + illegalLowerValue: 0, + illegalUpperValue: null, + roundTripTestValue: 2); + } + + [Fact] + public void MaxNodeCount_Property_RoundTrips() + { + // Arrange & Act & Assert + ReflectionAssert.IntegerProperty( + new ODataValidationSettings(), + o => o.MaxNodeCount, + expectedDefaultValue: 100, + minLegalValue: 1, + maxLegalValue: int.MaxValue, + illegalLowerValue: 0, + illegalUpperValue: null, + roundTripTestValue: 2); + } + + [Fact] + public void MaxTop_Property_RoundTrips() + { + // Arrange & Act & Assert + ReflectionAssert.NullableIntegerProperty( + new ODataValidationSettings(), + o => o.MaxTop, + expectedDefaultValue: null, + minLegalValue: 0, + maxLegalValue: int.MaxValue, + illegalLowerValue: -1, + illegalUpperValue: null, + roundTripTestValue: 2); + } + + [Fact] + public void MaxSkip_Property_RoundTrips() + { + // Arrange & Act & Assert + ReflectionAssert.NullableIntegerProperty( + new ODataValidationSettings(), + o => o.MaxSkip, + expectedDefaultValue: null, + minLegalValue: 0, + maxLegalValue: int.MaxValue, + illegalLowerValue: -1, + illegalUpperValue: null, + roundTripTestValue: 2); + } + + [Fact] + public void MaxExpansionDepth_Property_RoundTrips() + { + // Arrange & Act & Assert + ReflectionAssert.IntegerProperty( + new ODataValidationSettings(), + o => o.MaxExpansionDepth, + expectedDefaultValue: 2, + minLegalValue: 0, + maxLegalValue: int.MaxValue, + illegalLowerValue: -1, + illegalUpperValue: null, + roundTripTestValue: 100); + } + + [Fact] + public void MaxOrderByNodeCount_Property_RoundTrips() { - [Fact] - public void Ctor_Initializes_All_Properties() - { - // Arrange & Act - ODataValidationSettings querySettings = new ODataValidationSettings(); - - // Assert - Assert.Equal(AllowedArithmeticOperators.All, querySettings.AllowedArithmeticOperators); - Assert.Equal(AllowedFunctions.AllFunctions, querySettings.AllowedFunctions); - Assert.Equal(AllowedLogicalOperators.All, querySettings.AllowedLogicalOperators); - Assert.Empty(querySettings.AllowedOrderByProperties); - Assert.Equal(AllowedQueryOptions.Supported, querySettings.AllowedQueryOptions); - Assert.Equal(1, querySettings.MaxAnyAllExpressionDepth); - Assert.Equal(100, querySettings.MaxNodeCount); - Assert.Null(querySettings.MaxSkip); - Assert.Null(querySettings.MaxTop); - } - - [Fact] - public void AllowedFunctions_Property_RoundTrips() - { - // Arrange & Act & Assert - ReflectionAssert.EnumProperty( - new ODataValidationSettings(), - o => o.AllowedFunctions, - expectedDefaultValue: AllowedFunctions.AllFunctions, - illegalValue: AllowedFunctions.AllFunctions + 1, - roundTripTestValue: AllowedFunctions.AllMathFunctions); - } - - [Fact] - public void AllowedFunctions_SetToAllFunctions_DoesNotThrow() - { - // Arrange & Act & Assert - ExceptionAssert.DoesNotThrow(() => new ODataValidationSettings().AllowedFunctions = AllowedFunctions.AllFunctions); - } - - [Fact] - public void AllowedArithmeticOperators_Property_RoundTrips() - { - // Arrange & Act & Assert - ReflectionAssert.EnumProperty( - new ODataValidationSettings(), - o => o.AllowedArithmeticOperators, - expectedDefaultValue: AllowedArithmeticOperators.All, - illegalValue: AllowedArithmeticOperators.All + 1, - roundTripTestValue: AllowedArithmeticOperators.Multiply); - } - - [Fact] - public void AllowedLogicalOperators_Property_RoundTrips() - { - // Arrange & Act & Assert - ReflectionAssert.EnumProperty( - new ODataValidationSettings(), - o => o.AllowedLogicalOperators, - expectedDefaultValue: AllowedLogicalOperators.All, - illegalValue: AllowedLogicalOperators.All + 1, - roundTripTestValue: AllowedLogicalOperators.GreaterThanOrEqual | AllowedLogicalOperators.LessThanOrEqual); - } - - [Fact] - public void AllowedQueryOptions_Property_RoundTrips() - { - // Arrange & Act & Assert - ReflectionAssert.EnumProperty( - new ODataValidationSettings(), - o => o.AllowedQueryOptions, - expectedDefaultValue: AllowedQueryOptions.Supported, - illegalValue: AllowedQueryOptions.All + 1, - roundTripTestValue: AllowedQueryOptions.Filter); - } - - [Fact] - public void AllowedOrderByProperties_Property_RoundTrips() - { - // Arrange & Act - ODataValidationSettings settings = new ODataValidationSettings(); - Assert.NotNull(settings.AllowedOrderByProperties); - Assert.Empty(settings.AllowedOrderByProperties); - - settings.AllowedOrderByProperties.Add("Id"); - settings.AllowedOrderByProperties.Add("Name"); - - // Assert - Assert.Equal(2, settings.AllowedOrderByProperties.Count); - Assert.Contains("Id", settings.AllowedOrderByProperties); - Assert.Contains("Name", settings.AllowedOrderByProperties); - } - - [Fact] - public void MaxAnyAllExpressionDepth_Property_RoundTrips() - { - // Arrange & Act & Assert - ReflectionAssert.IntegerProperty( - new ODataValidationSettings(), - o => o.MaxAnyAllExpressionDepth, - expectedDefaultValue: 1, - minLegalValue: 1, - maxLegalValue: int.MaxValue, - illegalLowerValue: 0, - illegalUpperValue: null, - roundTripTestValue: 2); - } - - [Fact] - public void MaxNodeCount_Property_RoundTrips() - { - // Arrange & Act & Assert - ReflectionAssert.IntegerProperty( - new ODataValidationSettings(), - o => o.MaxNodeCount, - expectedDefaultValue: 100, - minLegalValue: 1, - maxLegalValue: int.MaxValue, - illegalLowerValue: 0, - illegalUpperValue: null, - roundTripTestValue: 2); - } - - [Fact] - public void MaxTop_Property_RoundTrips() - { - // Arrange & Act & Assert - ReflectionAssert.NullableIntegerProperty( - new ODataValidationSettings(), - o => o.MaxTop, - expectedDefaultValue: null, - minLegalValue: 0, - maxLegalValue: int.MaxValue, - illegalLowerValue: -1, - illegalUpperValue: null, - roundTripTestValue: 2); - } - - [Fact] - public void MaxSkip_Property_RoundTrips() - { - // Arrange & Act & Assert - ReflectionAssert.NullableIntegerProperty( - new ODataValidationSettings(), - o => o.MaxSkip, - expectedDefaultValue: null, - minLegalValue: 0, - maxLegalValue: int.MaxValue, - illegalLowerValue: -1, - illegalUpperValue: null, - roundTripTestValue: 2); - } - - [Fact] - public void MaxExpansionDepth_Property_RoundTrips() - { - // Arrange & Act & Assert - ReflectionAssert.IntegerProperty( - new ODataValidationSettings(), - o => o.MaxExpansionDepth, - expectedDefaultValue: 2, - minLegalValue: 0, - maxLegalValue: int.MaxValue, - illegalLowerValue: -1, - illegalUpperValue: null, - roundTripTestValue: 100); - } - - [Fact] - public void MaxOrderByNodeCount_Property_RoundTrips() - { - // Arrange & Act & Assert - ReflectionAssert.IntegerProperty( - new ODataValidationSettings(), - o => o.MaxOrderByNodeCount, - expectedDefaultValue: 5, - minLegalValue: 1, - maxLegalValue: int.MaxValue, - illegalLowerValue: -1, - illegalUpperValue: null, - roundTripTestValue: 100); - } + // Arrange & Act & Assert + ReflectionAssert.IntegerProperty( + new ODataValidationSettings(), + o => o.MaxOrderByNodeCount, + expectedDefaultValue: 5, + minLegalValue: 1, + maxLegalValue: int.MaxValue, + illegalLowerValue: -1, + illegalUpperValue: null, + roundTripTestValue: 100); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/OrderByQueryValidatorTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/OrderByQueryValidatorTest.cs index 4276eb01d..b072991b0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/OrderByQueryValidatorTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/OrderByQueryValidatorTest.cs @@ -17,354 +17,353 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Validator +namespace Microsoft.AspNetCore.OData.Tests.Query.Validator; + +public class OrderByQueryValidatorTest { - public class OrderByQueryValidatorTest + private OrderByQueryValidator _validator; + private ODataQueryContext _context; + + public OrderByQueryValidatorTest() + { + _context = ValidationTestHelper.CreateCustomerContext(); + _validator = new OrderByQueryValidator(); + } + + [Fact] + public void ValidateOrderByQueryValidator_ThrowsOnNullOption() + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(null, new ODataValidationSettings())); + } + + [Fact] + public void ValidateOrderByQueryValidator_ThrowsOnNullSettings() + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(new OrderByQueryOption("Name eq 'abc'", _context), null)); + } + + [Theory] + [InlineData("NotSortableProperty")] + [InlineData("UnsortableProperty")] + public void ValidateOrderByQueryValidator_ThrowsNotSortableException_ForNotSortableProperty_OnEmptyAllowedPropertiesList(string property) + { + // Arrange : empty allowed orderby list + ODataValidationSettings settings = new ODataValidationSettings(); + + // Act & Assert + ExceptionAssert.Throws(() => + _validator.Validate(new OrderByQueryOption(String.Format("{0} asc", property), _context), settings), + string.Format("The property '{0}' cannot be used in the $orderby query option.", property)); + } + + [Theory] + [InlineData("NotSortableProperty")] + [InlineData("UnsortableProperty")] + public void ValidateOrderByQueryValidator_DoesntThrowNotSortableException_ForNotSortableProperty_OnNonEmptyAllowedPropertiesList(string property) + { + // Arrange : nonempty allowed orderby list + ODataValidationSettings settings = new ODataValidationSettings(); + settings.AllowedOrderByProperties.Add(property); + + // Act & Assert + _validator.Validate(new OrderByQueryOption(string.Format("{0} asc", property), _context), settings); + } + + [Fact] + public void ValidateOrderByQueryValidator_NoException_ForAllowedAndSortableUnlimitedProperty_OnEmptyAllowedPropertiesList() + { + // Arrange: empty allowed orderby list + ODataValidationSettings settings = new ODataValidationSettings(); + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(new OrderByQueryOption("Name asc", _context), settings)); + } + + [Fact] + public void ValidateOrderByQueryValidator_NoException_ForAllowedAndSortableUnlimitedProperty_OnNonEmptyAllowedPropertiesList() + { + // Arrange: nonempty allowed orbderby list + ODataValidationSettings settings = new ODataValidationSettings(); + settings.AllowedOrderByProperties.Add("Name"); + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(new OrderByQueryOption("Name asc", _context), settings)); + } + + [Theory] + [InlineData("NotSortableProperty")] + [InlineData("UnsortableProperty")] + public void ValidateOrderByQueryValidator_ThrowsNotAllowedException_ForNotAllowedAndSortableLimitedProperty(string property) + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings(); + settings.AllowedOrderByProperties.Add("Name"); + + // Act & Assert + ExceptionAssert.Throws( + () => _validator.Validate(new OrderByQueryOption(string.Format("{0} asc", property), _context), settings), + string.Format("Order by '{0}' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings.", property)); + } + + [Fact] + public void ValidateOrderByQueryValidator_ThrowsNotAllowedException_ForNotAllowedAndSortableUnlimitedProperty() + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings(); + settings.AllowedOrderByProperties.Add("Address"); + + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(new OrderByQueryOption("Name asc", _context), settings), + "Order by 'Name' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); + } + + [Fact] + public void ValidateOrderByQueryValidator_WillNotAllowName() + { + // Arrange + OrderByQueryOption option = new OrderByQueryOption("Name", _context); + ODataValidationSettings settings = new ODataValidationSettings(); + settings.AllowedOrderByProperties.Add("Id"); + + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(option, settings), + "Order by 'Name' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); + } + + [Fact] + public void ValidateOrderByQueryValidator_WillNotAllowMultipleProperties() + { + // Arrange + OrderByQueryOption option = new OrderByQueryOption("Name desc, Id asc", _context); + ODataValidationSettings settings = new ODataValidationSettings(); + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + + settings.AllowedOrderByProperties.Add("Address"); + settings.AllowedOrderByProperties.Add("Name"); + + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(option, settings), + "Order by 'Id' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); + } + + [Fact] + public void ValidateOrderByQueryValidator_WillAllowId() + { + // Arrange + OrderByQueryOption option = new OrderByQueryOption("Id", _context); + ODataValidationSettings settings = new ODataValidationSettings(); + settings.AllowedOrderByProperties.Add("Id"); + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Fact] + public void ValidateOrderByQueryValidator_AllowsOrderByIt() + { + // Arrange + OrderByQueryOption option = new OrderByQueryOption("$it", new ODataQueryContext(EdmCoreModel.Instance, typeof(int))); + ODataValidationSettings settings = new ODataValidationSettings(); + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Fact] + public void ValidateOrderByQueryValidator_AllowsOrderByIt_IfExplicitlySpecified() + { + // Arrange + OrderByQueryOption option = new OrderByQueryOption("$it", new ODataQueryContext(EdmCoreModel.Instance, typeof(int))); + ODataValidationSettings settings = new ODataValidationSettings { AllowedOrderByProperties = { "$it" } }; + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + [Fact] + public void ValidateOrderByQueryValidator_DisallowsOrderByIt_IfTurnedOff() + { + // Arrange + _context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + OrderByQueryOption option = new OrderByQueryOption("$it", _context); + ODataValidationSettings settings = new ODataValidationSettings(); + settings.AllowedOrderByProperties.Add("dummy"); + + // Act & Assert + ExceptionAssert.Throws( + () => _validator.Validate(option, settings), + "Order by '$it' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); + } + + [Fact] + public void ValidateOrderByQueryValidator__ThrowsCountExceeded() + { + // Arrange + OrderByQueryOption option = new OrderByQueryOption("Name desc, Id asc", _context); + ODataValidationSettings settings = new ODataValidationSettings { MaxOrderByNodeCount = 1 }; + + // Act & Assert + ExceptionAssert.Throws( + () => _validator.Validate(option, settings), + "The number of clauses in $orderby query option exceeded the maximum number allowed. The maximum number of $orderby clauses allowed is 1."); + } + + [Theory] + // Works with complex properties + [InlineData("ComplexProperty/Value", "LimitedEntity", + "The property 'ComplexProperty' cannot be used in the $orderby query option.")] + // Works with simple properties + [InlineData("RelatedEntity/RelatedComplexProperty/NotSortableValue", "LimitedEntity", + "The property 'NotSortableValue' cannot be used in the $orderby query option.")] + [InlineData("RelatedEntity/RelatedComplexProperty/UnsortableValue", "LimitedEntity", + "The property 'UnsortableValue' cannot be used in the $orderby query option.")] + // Works with navigation properties + [InlineData("RelatedEntity/BackReference/Id", "LimitedEntity", + "The property 'BackReference' cannot be used in the $orderby query option.")] + // Works with inheritance + [InlineData("RelatedEntity/NS.LimitedSpecializedEntity/SpecializedComplexProperty/Value", "LimitedEntity", + "The property 'SpecializedComplexProperty' cannot be used in the $orderby query option.")] + // Works with multiple clauses + [InlineData("Id, ComplexProperty/NotSortableValue", "LimitedEntity", + "The property 'NotSortableValue' cannot be used in the $orderby query option.")] + [InlineData("Id, ComplexProperty/UnsortableValue", "LimitedEntity", + "The property 'UnsortableValue' cannot be used in the $orderby query option.")] + public void ValidateOrderByQueryValidator_ThrowsIfTryingToValidateALimitedProperty(string query, string edmTypeName, string message) + { + // Arrange + IEdmModel model = GetEdmModel(); + IEdmEntityType edmType = model.SchemaElements.OfType().Single(t => t.Name == edmTypeName); + ODataQueryContext context = new ODataQueryContext(model, edmType); + context.DefaultQueryConfigurations.EnableOrderBy = true; + OrderByQueryOption option = new OrderByQueryOption(query, context); + ODataValidationSettings settings = new ODataValidationSettings(); + + // Act & Assert + OrderByQueryValidator validator = new OrderByQueryValidator(); + ExceptionAssert.Throws(() => validator.Validate(option, settings), message); + } + + [Fact] + public void ValidateOrderByQueryValidator_DoesntThrowIfTheLeafOfThePathIsWithinTheAllowedProperties() + { + // Arrange + IEdmModel model = GetEdmModel(); + IEdmEntityType edmType = model.SchemaElements.OfType().Single(t => t.Name == "LimitedEntity"); + ODataQueryContext context = new ODataQueryContext(model, edmType); + OrderByQueryOption option = new OrderByQueryOption("ComplexProperty/Value", context); + ODataValidationSettings settings = new ODataValidationSettings(); + settings.AllowedOrderByProperties.Add("ComplexProperty"); + settings.AllowedOrderByProperties.Add("Value"); + + // Act & Assert + IOrderByQueryValidator validator = context.GetOrderByQueryValidator(); + ExceptionAssert.DoesNotThrow(() => validator.Validate(option, settings)); + } + + [Fact] + public void ValidateOrderByQueryValidator_ThrowsIfTheLeafOfThePathIsntWithinTheAllowedProperties() + { + // Arrange + IEdmModel model = GetEdmModel(); + IEdmEntityType edmType = model.SchemaElements.OfType().Single(t => t.Name == "LimitedEntity"); + ODataQueryContext context = new ODataQueryContext(model, edmType); + OrderByQueryOption option = new OrderByQueryOption("ComplexProperty/Value", context); + ODataValidationSettings settings = new ODataValidationSettings(); + settings.AllowedOrderByProperties.Add("NotSortableProperty"); + + // Act & Assert + IOrderByQueryValidator validator = context.GetOrderByQueryValidator(); + ExceptionAssert.Throws(() => + validator.Validate(option, settings), + "Order by 'Value' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); + } + + [Fact] + public void ValidateOrderByQueryValidator_NoException_ForParameterAlias() + { + // Arrange + IEdmModel model = GetEdmModel(); + IEdmEntityType edmType = model.SchemaElements.OfType().Single(t => t.Name == "LimitedEntity"); + IEdmEntitySet entitySet = model.FindDeclaredEntitySet("LimitedEntities"); + Assert.NotNull(entitySet); + ODataQueryContext context = new ODataQueryContext(model, edmType); + context.DefaultQueryConfigurations.EnableOrderBy = true; + + OrderByQueryOption option = new OrderByQueryOption( + "@p,@q desc", + context, + new ODataQueryOptionParser( + model, + edmType, + entitySet, + new Dictionary { { "$orderby", "@p,@q desc" }, { "@p", "Id" }, { "@q", "RelatedEntity/Id" } })); + + ODataValidationSettings settings = new ODataValidationSettings(); + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); + } + + private static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataModelBuilder(); + + // Configure LimitedEntity + EntitySetConfiguration limitedEntities = builder.EntitySet("LimitedEntities"); + limitedEntities.EntityType.HasKey(p => p.Id); + limitedEntities.EntityType.ComplexProperty(c => c.ComplexProperty).IsNotSortable(); + limitedEntities.EntityType.HasOptional(l => l.RelatedEntity); + limitedEntities.EntityType.CollectionProperty(cp => cp.Integers); + + // Configure LimitedRelatedEntity + EntitySetConfiguration limitedRelatedEntities = + builder.EntitySet("LimitedRelatedEntities"); + limitedRelatedEntities.EntityType.HasKey(p => p.Id); + limitedRelatedEntities.EntityType.HasOptional(p => p.BackReference).IsNotSortable(); + limitedRelatedEntities.EntityType.ComplexProperty(p => p.RelatedComplexProperty).IsNotSortable(); + + // Configure SpecializedEntity + EntityTypeConfiguration specializedEntity = + builder.EntityType().DerivesFrom(); + specializedEntity.Namespace = "NS"; + specializedEntity.ComplexProperty(p => p.SpecializedComplexProperty).IsNotSortable(); + + // Configure Complextype + ComplexTypeConfiguration complexType = builder.ComplexType(); + complexType.Property(p => p.NotSortableValue).IsNotSortable(); + complexType.Property(p => p.UnsortableValue).IsUnsortable(); + complexType.Property(p => p.Value); + + return builder.GetEdmModel(); + } + + private class LimitedEntity + { + public int Id { get; set; } + public LimitedComplexType ComplexProperty { get; set; } + public LimitedRelatedEntity RelatedEntity { get; set; } + public ICollection Integers { get; set; } + } + + private class LimitedComplexType + { + public int Value { get; set; } + public int NotSortableValue { get; set; } + public int UnsortableValue { get; set; } + } + + private class LimitedRelatedEntity + { + public int Id { get; set; } + public LimitedEntity BackReference { get; set; } + public LimitedComplexType RelatedComplexProperty { get; set; } + } + + private class LimitedSpecializedEntity : LimitedRelatedEntity { - private OrderByQueryValidator _validator; - private ODataQueryContext _context; - - public OrderByQueryValidatorTest() - { - _context = ValidationTestHelper.CreateCustomerContext(); - _validator = new OrderByQueryValidator(); - } - - [Fact] - public void ValidateOrderByQueryValidator_ThrowsOnNullOption() - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(null, new ODataValidationSettings())); - } - - [Fact] - public void ValidateOrderByQueryValidator_ThrowsOnNullSettings() - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(new OrderByQueryOption("Name eq 'abc'", _context), null)); - } - - [Theory] - [InlineData("NotSortableProperty")] - [InlineData("UnsortableProperty")] - public void ValidateOrderByQueryValidator_ThrowsNotSortableException_ForNotSortableProperty_OnEmptyAllowedPropertiesList(string property) - { - // Arrange : empty allowed orderby list - ODataValidationSettings settings = new ODataValidationSettings(); - - // Act & Assert - ExceptionAssert.Throws(() => - _validator.Validate(new OrderByQueryOption(String.Format("{0} asc", property), _context), settings), - string.Format("The property '{0}' cannot be used in the $orderby query option.", property)); - } - - [Theory] - [InlineData("NotSortableProperty")] - [InlineData("UnsortableProperty")] - public void ValidateOrderByQueryValidator_DoesntThrowNotSortableException_ForNotSortableProperty_OnNonEmptyAllowedPropertiesList(string property) - { - // Arrange : nonempty allowed orderby list - ODataValidationSettings settings = new ODataValidationSettings(); - settings.AllowedOrderByProperties.Add(property); - - // Act & Assert - _validator.Validate(new OrderByQueryOption(string.Format("{0} asc", property), _context), settings); - } - - [Fact] - public void ValidateOrderByQueryValidator_NoException_ForAllowedAndSortableUnlimitedProperty_OnEmptyAllowedPropertiesList() - { - // Arrange: empty allowed orderby list - ODataValidationSettings settings = new ODataValidationSettings(); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(new OrderByQueryOption("Name asc", _context), settings)); - } - - [Fact] - public void ValidateOrderByQueryValidator_NoException_ForAllowedAndSortableUnlimitedProperty_OnNonEmptyAllowedPropertiesList() - { - // Arrange: nonempty allowed orbderby list - ODataValidationSettings settings = new ODataValidationSettings(); - settings.AllowedOrderByProperties.Add("Name"); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(new OrderByQueryOption("Name asc", _context), settings)); - } - - [Theory] - [InlineData("NotSortableProperty")] - [InlineData("UnsortableProperty")] - public void ValidateOrderByQueryValidator_ThrowsNotAllowedException_ForNotAllowedAndSortableLimitedProperty(string property) - { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings(); - settings.AllowedOrderByProperties.Add("Name"); - - // Act & Assert - ExceptionAssert.Throws( - () => _validator.Validate(new OrderByQueryOption(string.Format("{0} asc", property), _context), settings), - string.Format("Order by '{0}' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings.", property)); - } - - [Fact] - public void ValidateOrderByQueryValidator_ThrowsNotAllowedException_ForNotAllowedAndSortableUnlimitedProperty() - { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings(); - settings.AllowedOrderByProperties.Add("Address"); - - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(new OrderByQueryOption("Name asc", _context), settings), - "Order by 'Name' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); - } - - [Fact] - public void ValidateOrderByQueryValidator_WillNotAllowName() - { - // Arrange - OrderByQueryOption option = new OrderByQueryOption("Name", _context); - ODataValidationSettings settings = new ODataValidationSettings(); - settings.AllowedOrderByProperties.Add("Id"); - - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(option, settings), - "Order by 'Name' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); - } - - [Fact] - public void ValidateOrderByQueryValidator_WillNotAllowMultipleProperties() - { - // Arrange - OrderByQueryOption option = new OrderByQueryOption("Name desc, Id asc", _context); - ODataValidationSettings settings = new ODataValidationSettings(); - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); - - settings.AllowedOrderByProperties.Add("Address"); - settings.AllowedOrderByProperties.Add("Name"); - - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(option, settings), - "Order by 'Id' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); - } - - [Fact] - public void ValidateOrderByQueryValidator_WillAllowId() - { - // Arrange - OrderByQueryOption option = new OrderByQueryOption("Id", _context); - ODataValidationSettings settings = new ODataValidationSettings(); - settings.AllowedOrderByProperties.Add("Id"); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); - } - - [Fact] - public void ValidateOrderByQueryValidator_AllowsOrderByIt() - { - // Arrange - OrderByQueryOption option = new OrderByQueryOption("$it", new ODataQueryContext(EdmCoreModel.Instance, typeof(int))); - ODataValidationSettings settings = new ODataValidationSettings(); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); - } - - [Fact] - public void ValidateOrderByQueryValidator_AllowsOrderByIt_IfExplicitlySpecified() - { - // Arrange - OrderByQueryOption option = new OrderByQueryOption("$it", new ODataQueryContext(EdmCoreModel.Instance, typeof(int))); - ODataValidationSettings settings = new ODataValidationSettings { AllowedOrderByProperties = { "$it" } }; - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); - } - - [Fact] - public void ValidateOrderByQueryValidator_DisallowsOrderByIt_IfTurnedOff() - { - // Arrange - _context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - OrderByQueryOption option = new OrderByQueryOption("$it", _context); - ODataValidationSettings settings = new ODataValidationSettings(); - settings.AllowedOrderByProperties.Add("dummy"); - - // Act & Assert - ExceptionAssert.Throws( - () => _validator.Validate(option, settings), - "Order by '$it' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); - } - - [Fact] - public void ValidateOrderByQueryValidator__ThrowsCountExceeded() - { - // Arrange - OrderByQueryOption option = new OrderByQueryOption("Name desc, Id asc", _context); - ODataValidationSettings settings = new ODataValidationSettings { MaxOrderByNodeCount = 1 }; - - // Act & Assert - ExceptionAssert.Throws( - () => _validator.Validate(option, settings), - "The number of clauses in $orderby query option exceeded the maximum number allowed. The maximum number of $orderby clauses allowed is 1."); - } - - [Theory] - // Works with complex properties - [InlineData("ComplexProperty/Value", "LimitedEntity", - "The property 'ComplexProperty' cannot be used in the $orderby query option.")] - // Works with simple properties - [InlineData("RelatedEntity/RelatedComplexProperty/NotSortableValue", "LimitedEntity", - "The property 'NotSortableValue' cannot be used in the $orderby query option.")] - [InlineData("RelatedEntity/RelatedComplexProperty/UnsortableValue", "LimitedEntity", - "The property 'UnsortableValue' cannot be used in the $orderby query option.")] - // Works with navigation properties - [InlineData("RelatedEntity/BackReference/Id", "LimitedEntity", - "The property 'BackReference' cannot be used in the $orderby query option.")] - // Works with inheritance - [InlineData("RelatedEntity/NS.LimitedSpecializedEntity/SpecializedComplexProperty/Value", "LimitedEntity", - "The property 'SpecializedComplexProperty' cannot be used in the $orderby query option.")] - // Works with multiple clauses - [InlineData("Id, ComplexProperty/NotSortableValue", "LimitedEntity", - "The property 'NotSortableValue' cannot be used in the $orderby query option.")] - [InlineData("Id, ComplexProperty/UnsortableValue", "LimitedEntity", - "The property 'UnsortableValue' cannot be used in the $orderby query option.")] - public void ValidateOrderByQueryValidator_ThrowsIfTryingToValidateALimitedProperty(string query, string edmTypeName, string message) - { - // Arrange - IEdmModel model = GetEdmModel(); - IEdmEntityType edmType = model.SchemaElements.OfType().Single(t => t.Name == edmTypeName); - ODataQueryContext context = new ODataQueryContext(model, edmType); - context.DefaultQueryConfigurations.EnableOrderBy = true; - OrderByQueryOption option = new OrderByQueryOption(query, context); - ODataValidationSettings settings = new ODataValidationSettings(); - - // Act & Assert - OrderByQueryValidator validator = new OrderByQueryValidator(); - ExceptionAssert.Throws(() => validator.Validate(option, settings), message); - } - - [Fact] - public void ValidateOrderByQueryValidator_DoesntThrowIfTheLeafOfThePathIsWithinTheAllowedProperties() - { - // Arrange - IEdmModel model = GetEdmModel(); - IEdmEntityType edmType = model.SchemaElements.OfType().Single(t => t.Name == "LimitedEntity"); - ODataQueryContext context = new ODataQueryContext(model, edmType); - OrderByQueryOption option = new OrderByQueryOption("ComplexProperty/Value", context); - ODataValidationSettings settings = new ODataValidationSettings(); - settings.AllowedOrderByProperties.Add("ComplexProperty"); - settings.AllowedOrderByProperties.Add("Value"); - - // Act & Assert - IOrderByQueryValidator validator = context.GetOrderByQueryValidator(); - ExceptionAssert.DoesNotThrow(() => validator.Validate(option, settings)); - } - - [Fact] - public void ValidateOrderByQueryValidator_ThrowsIfTheLeafOfThePathIsntWithinTheAllowedProperties() - { - // Arrange - IEdmModel model = GetEdmModel(); - IEdmEntityType edmType = model.SchemaElements.OfType().Single(t => t.Name == "LimitedEntity"); - ODataQueryContext context = new ODataQueryContext(model, edmType); - OrderByQueryOption option = new OrderByQueryOption("ComplexProperty/Value", context); - ODataValidationSettings settings = new ODataValidationSettings(); - settings.AllowedOrderByProperties.Add("NotSortableProperty"); - - // Act & Assert - IOrderByQueryValidator validator = context.GetOrderByQueryValidator(); - ExceptionAssert.Throws(() => - validator.Validate(option, settings), - "Order by 'Value' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on EnableQueryAttribute or QueryValidationSettings."); - } - - [Fact] - public void ValidateOrderByQueryValidator_NoException_ForParameterAlias() - { - // Arrange - IEdmModel model = GetEdmModel(); - IEdmEntityType edmType = model.SchemaElements.OfType().Single(t => t.Name == "LimitedEntity"); - IEdmEntitySet entitySet = model.FindDeclaredEntitySet("LimitedEntities"); - Assert.NotNull(entitySet); - ODataQueryContext context = new ODataQueryContext(model, edmType); - context.DefaultQueryConfigurations.EnableOrderBy = true; - - OrderByQueryOption option = new OrderByQueryOption( - "@p,@q desc", - context, - new ODataQueryOptionParser( - model, - edmType, - entitySet, - new Dictionary { { "$orderby", "@p,@q desc" }, { "@p", "Id" }, { "@q", "RelatedEntity/Id" } })); - - ODataValidationSettings settings = new ODataValidationSettings(); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(option, settings)); - } - - private static IEdmModel GetEdmModel() - { - ODataModelBuilder builder = new ODataModelBuilder(); - - // Configure LimitedEntity - EntitySetConfiguration limitedEntities = builder.EntitySet("LimitedEntities"); - limitedEntities.EntityType.HasKey(p => p.Id); - limitedEntities.EntityType.ComplexProperty(c => c.ComplexProperty).IsNotSortable(); - limitedEntities.EntityType.HasOptional(l => l.RelatedEntity); - limitedEntities.EntityType.CollectionProperty(cp => cp.Integers); - - // Configure LimitedRelatedEntity - EntitySetConfiguration limitedRelatedEntities = - builder.EntitySet("LimitedRelatedEntities"); - limitedRelatedEntities.EntityType.HasKey(p => p.Id); - limitedRelatedEntities.EntityType.HasOptional(p => p.BackReference).IsNotSortable(); - limitedRelatedEntities.EntityType.ComplexProperty(p => p.RelatedComplexProperty).IsNotSortable(); - - // Configure SpecializedEntity - EntityTypeConfiguration specializedEntity = - builder.EntityType().DerivesFrom(); - specializedEntity.Namespace = "NS"; - specializedEntity.ComplexProperty(p => p.SpecializedComplexProperty).IsNotSortable(); - - // Configure Complextype - ComplexTypeConfiguration complexType = builder.ComplexType(); - complexType.Property(p => p.NotSortableValue).IsNotSortable(); - complexType.Property(p => p.UnsortableValue).IsUnsortable(); - complexType.Property(p => p.Value); - - return builder.GetEdmModel(); - } - - private class LimitedEntity - { - public int Id { get; set; } - public LimitedComplexType ComplexProperty { get; set; } - public LimitedRelatedEntity RelatedEntity { get; set; } - public ICollection Integers { get; set; } - } - - private class LimitedComplexType - { - public int Value { get; set; } - public int NotSortableValue { get; set; } - public int UnsortableValue { get; set; } - } - - private class LimitedRelatedEntity - { - public int Id { get; set; } - public LimitedEntity BackReference { get; set; } - public LimitedComplexType RelatedComplexProperty { get; set; } - } - - private class LimitedSpecializedEntity : LimitedRelatedEntity - { - public string Name { get; set; } - public LimitedComplexType SpecializedComplexProperty { get; set; } - } + public string Name { get; set; } + public LimitedComplexType SpecializedComplexProperty { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/SelectExpandQueryValidatorTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/SelectExpandQueryValidatorTest.cs index e382f5d11..006c9c811 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/SelectExpandQueryValidatorTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/SelectExpandQueryValidatorTest.cs @@ -21,478 +21,477 @@ using Microsoft.OData.ModelBuilder.Config; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Validator +namespace Microsoft.AspNetCore.OData.Tests.Query.Validator; + +public class SelectExpandQueryValidatorTest { - public class SelectExpandQueryValidatorTest + private ODataQueryContext _queryContext; + + public const string MaxExpandDepthExceededErrorString = + "The request includes a $expand path which is too deep. The maximum depth allowed is {0}. " + + "To increase the limit, set the 'MaxExpansionDepth' property on EnableQueryAttribute or ODataValidationSettings, or set the 'MaxDepth' property in ExpandAttribute."; + + public SelectExpandQueryValidatorTest() { - private ODataQueryContext _queryContext; + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); + _queryContext = new ODataQueryContext(model.Model, typeof(Customer), null); + _queryContext.RequestContainer = new MockServiceProvider(); + _queryContext.DefaultQueryConfigurations.EnableExpand = true; + } - public const string MaxExpandDepthExceededErrorString = - "The request includes a $expand path which is too deep. The maximum depth allowed is {0}. " + - "To increase the limit, set the 'MaxExpansionDepth' property on EnableQueryAttribute or ODataValidationSettings, or set the 'MaxDepth' property in ExpandAttribute."; + [Fact] + public void ValidateSelectExpandQueryValidator_Throws_NullOption() + { + // Arrange & Act & Assert + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + ExceptionAssert.ThrowsArgumentNull(() => validator.Validate(null, new ODataValidationSettings()), "selectExpandQueryOption"); + } - public SelectExpandQueryValidatorTest() - { - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); - _queryContext = new ODataQueryContext(model.Model, typeof(Customer), null); - _queryContext.RequestContainer = new MockServiceProvider(); - _queryContext.DefaultQueryConfigurations.EnableExpand = true; - } - - [Fact] - public void ValidateSelectExpandQueryValidator_Throws_NullOption() - { - // Arrange & Act & Assert - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - ExceptionAssert.ThrowsArgumentNull(() => validator.Validate(null, new ODataValidationSettings()), "selectExpandQueryOption"); - } + [Fact] + public void ValidateSelectExpandQueryValidator_Throws_NullSettings() + { + // Arrange + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + SelectExpandQueryOption option = new SelectExpandQueryOption("any", null, _queryContext); - [Fact] - public void ValidateSelectExpandQueryValidator_Throws_NullSettings() - { - // Arrange - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - SelectExpandQueryOption option = new SelectExpandQueryOption("any", null, _queryContext); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => validator.Validate(option, null), "validationSettings"); - } - - [Theory] - [InlineData("Orders($expand=Customer)", 1)] - [InlineData("Orders,Orders($expand=Customer)", 1)] - [InlineData("Orders($expand=Customer($expand=Orders))", 2)] - [InlineData("Orders($expand=Customer($expand=Orders($expand=Customer($expand=Orders($expand=Customer)))))", 5)] - [InlineData("Orders($expand=NS.SpecialOrder/SpecialCustomer)", 1)] - public void ValidateSelectExpandQueryValidator_DepthChecks(string expand, int maxExpansionDepth) - { - // Arrange - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, _queryContext); - selectExpandQueryOption.LevelsMaxLiteralExpansionDepth = 1; - - // Act & Assert - ExceptionAssert.Throws( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth }), - String.Format(CultureInfo.CurrentCulture, MaxExpandDepthExceededErrorString, maxExpansionDepth)); - - ExceptionAssert.DoesNotThrow( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth + 1 })); - } - - [Theory] - [InlineData("Orders($expand=Customer)", 1)] - [InlineData("Orders,Orders($expand=Customer)", 1)] - [InlineData("Orders($expand=Customer($expand=Orders))", 2)] - [InlineData("Orders($expand=Customer($expand=Orders($expand=Customer($expand=Orders($expand=Customer)))))", 5)] - [InlineData("Orders($expand=NS.SpecialOrder/SpecialCustomer)", 1)] - public void ValidateSelectExpandQueryValidator_DepthChecks_QuerySettings(string expand, int maxExpansionDepth) - { - // Arrange - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); - ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); - queryContext.DefaultQueryConfigurations.EnableExpand = true; - queryContext.RequestContainer = new MockServiceProvider(); - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, queryContext); - selectExpandQueryOption.LevelsMaxLiteralExpansionDepth = 1; - IEdmStructuredType customerType = - model.Model.SchemaElements.First(e => e.Name.Equals("Customer")) as IEdmStructuredType; - ModelBoundQuerySettings querySettings = new ModelBoundQuerySettings(); - querySettings.ExpandConfigurations.Add("Orders", new ExpandConfiguration - { - ExpandType = SelectExpandType.Allowed, - MaxDepth = maxExpansionDepth - }); - model.Model.SetAnnotationValue(customerType, querySettings); - - // Act & Assert - ExceptionAssert.Throws( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth + 1 }), - String.Format(CultureInfo.CurrentCulture, MaxExpandDepthExceededErrorString, maxExpansionDepth)); - } - - [Theory] - [InlineData("Parent($levels=5)", 4)] - [InlineData("Parent($expand=Parent($levels=4))", 4)] - [InlineData("Parent($expand=Parent($expand=Parent($levels=0)))", 1)] - [InlineData("Parent($expand=Parent($levels=4);$levels=5)", 8)] - [InlineData("Parent($levels=4),DerivedAncestors($levels=5)", 4)] - [InlineData("DerivedAncestors($levels=5),Parent($levels=4)", 4)] - public void ValidateSelectExpandQueryValidator_DepthChecks_DollarLevels(string expand, int maxExpansionDepth) - { - // Arrange - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Entities"); - IEdmModel model = builder.GetEdmModel(); - var context = new ODataQueryContext(model, typeof(ODataLevelsTest.LevelsEntity)); - context.DefaultQueryConfigurations.EnableExpand = true; - context.RequestContainer = new MockServiceProvider(); - var selectExpandQueryOption = new SelectExpandQueryOption(null, expand, context); - selectExpandQueryOption.LevelsMaxLiteralExpansionDepth = 1; - - // Act & Assert - ExceptionAssert.Throws( - () => validator.Validate( - selectExpandQueryOption, - new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth }), - string.Format( - CultureInfo.CurrentCulture, - MaxExpandDepthExceededErrorString, - maxExpansionDepth)); - - ExceptionAssert.DoesNotThrow( - () => validator.Validate( - selectExpandQueryOption, - new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth + 1 })); - } - - [Fact] - public void ValidateSelectExpandQueryValidator_DoesNotThrow_IfExpansionDepthIsZero_DollarLevels() - { - // Arrange - string expand = "Parent($expand=Parent($expand=Parent($levels=10)))"; - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Entities"); - IEdmModel model = builder.GetEdmModel(); - var context = new ODataQueryContext(model, typeof(ODataLevelsTest.LevelsEntity)); - context.DefaultQueryConfigurations.EnableExpand = true; - context.RequestContainer = new MockServiceProvider(); - var selectExpandQueryOption = new SelectExpandQueryOption(null, expand, context); - - // Act & Assert - ExceptionAssert.DoesNotThrow( - () => validator.Validate( - selectExpandQueryOption, - new ODataValidationSettings { MaxExpansionDepth = 0 })); - } - - [Fact] - public void ValidateSelectExpandQueryValidator_Throws_LevelsMaxLiteralExpansionDepthGreaterThanMaxExpansionDepth() - { - // Arrange - string expand = "Parent($levels=2)"; - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Entities"); - IEdmModel model = builder.GetEdmModel(); - var context = new ODataQueryContext(model, typeof(ODataLevelsTest.LevelsEntity)); - context.DefaultQueryConfigurations.EnableExpand = true; - context.RequestContainer = new MockServiceProvider(); - var selectExpandQueryOption = new SelectExpandQueryOption(null, expand, context); - selectExpandQueryOption.LevelsMaxLiteralExpansionDepth = 4; - - // Act & Assert - ExceptionAssert.Throws( - () => validator.Validate( - selectExpandQueryOption, - new ODataValidationSettings { MaxExpansionDepth = 3 }), - "'LevelsMaxLiteralExpansionDepth' should be less than or equal to 'MaxExpansionDepth'."); - } - - [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - public void ValidateSelectExpandQueryValidator_DoesNotThrow_DefaultLevelsMaxLiteralExpansionDepth(int maxExpansionDepth) - { - // Arrange - string expand = "Parent($levels=1)"; - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Entities"); - IEdmModel model = builder.GetEdmModel(); - var context = new ODataQueryContext(model, typeof(ODataLevelsTest.LevelsEntity)); - context.DefaultQueryConfigurations.EnableExpand = true; - context.RequestContainer = new MockServiceProvider(); - var selectExpandQueryOption = new SelectExpandQueryOption(null, expand, context); - - // Act & Assert - ExceptionAssert.DoesNotThrow( - () => validator.Validate( - selectExpandQueryOption, - new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth })); - } - - [Fact] - public void ValidateSelectExpandQueryValidator_Throw_WithInvalidMaxExpansionDepth() - { - int maxExpansionDepth = -1; - // Arrange - string expand = "Parent($levels=1)"; - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Entities"); - IEdmModel model = builder.GetEdmModel(); - var context = new ODataQueryContext(model, typeof(ODataLevelsTest.LevelsEntity)); - context.RequestContainer = new MockServiceProvider(); - var validator = context.GetSelectExpandQueryValidator(); - var selectExpandQueryOption = new SelectExpandQueryOption(null, expand, context); - - // Act & Assert - ExceptionAssert.Throws( - () => validator.Validate( - selectExpandQueryOption, - new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth }), - "Value must be greater than or equal to 0. (Parameter 'value')\r\nActual value was -1."); - } - - [Theory] - [InlineData(2, 3)] - [InlineData(4, 4)] - [InlineData(3, 0)] - public void ValidateSelectExpandQueryValidator_DoesNotThrow_LevelsMaxLiteralExpansionDepthAndMaxExpansionDepth( - int levelsMaxLiteralExpansionDepth, - int maxExpansionDepth) - { - // Arrange - string expand = "Parent($levels=2)"; - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Entities"); - IEdmModel model = builder.GetEdmModel(); - var context = new ODataQueryContext(model, typeof(ODataLevelsTest.LevelsEntity)); - context.DefaultQueryConfigurations.EnableExpand = true; - context.RequestContainer = new MockServiceProvider(); - var selectExpandQueryOption = new SelectExpandQueryOption(null, expand, context); - selectExpandQueryOption.LevelsMaxLiteralExpansionDepth = levelsMaxLiteralExpansionDepth; - - // Act & Assert - ExceptionAssert.DoesNotThrow( - () => validator.Validate( - selectExpandQueryOption, - new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth })); - } - - [Fact] - public void ValidateSelectExpandQueryValidator_Throws_IfNotAllowTop() - { - // Arrange - string expand = "Orders($top=4)"; - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - _queryContext.DefaultQueryConfigurations.EnableExpand = true; - _queryContext.DefaultQueryConfigurations.MaxTop = 2; - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, _queryContext); - - // Act & Assert - ExceptionAssert.Throws( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), - "The limit of '2' for Top query has been exceeded. The value from the incoming request is '4'."); - } - - [Fact] - public void ValidateSelectExpandQueryValidator_Throws_IfNotAllowCount() - { - // Arrange - string expand = "Orders($count=true)"; - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - _queryContext.DefaultQueryConfigurations.EnableExpand = true; - _queryContext.DefaultQueryConfigurations.EnableCount = false; - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, _queryContext); - - // Act & Assert - ExceptionAssert.Throws( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), - "The property 'Orders' cannot be used for $count."); - } - - [Fact] - public void ValidateSelectExpandQueryValidator_Throws_IfNotAllowOrderby() - { - // Arrange - string expand = "Orders($orderby=Amount)"; - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - _queryContext.DefaultQueryConfigurations.EnableExpand = true; - _queryContext.DefaultQueryConfigurations.EnableOrderBy = false; - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, _queryContext); - - // Act & Assert - ExceptionAssert.Throws( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), - "The property 'Amount' cannot be used in the $orderby query option."); - } - - [Fact] - public void ValidateSelectExpandQueryValidator_Throws_IfNotAllowFilter() - { - // Arrange - string expand = "Orders($filter=Amount eq 42)"; - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - _queryContext.DefaultQueryConfigurations.EnableExpand = true; - _queryContext.DefaultQueryConfigurations.EnableFilter = false; - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, _queryContext); - - // Act & Assert - ExceptionAssert.Throws( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), - "The property 'Amount' cannot be used in the $filter query option."); - } - - [Fact] - public void ValidateSelectExpandQueryValidator_DoesNotThrow_IfExpansionDepthIsZero() - { - // Arrange - string expand = "Orders($expand=Customer($expand=Orders($expand=Customer($expand=Orders($expand=Customer)))))"; - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - _queryContext.DefaultQueryConfigurations.EnableExpand = true; - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, _queryContext); - - // Act & Assert - ExceptionAssert.DoesNotThrow( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings { MaxExpansionDepth = 0 })); - } - - [Fact] - public void ValidateSelectExpandQueryValidator_DoesNotThrow_IfExpansionDepthIsZero_QuerySettings() - { - // Arrange - string expand = - "Orders($expand=Customer($expand=Orders($expand=Customer($expand=Orders($expand=Customer)))))"; - SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); - ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); - queryContext.DefaultQueryConfigurations.EnableExpand = true; - queryContext.RequestContainer = new MockServiceProvider(); - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, queryContext); - IEdmStructuredType customerType = - model.Model.SchemaElements.First(e => e.Name.Equals("Customer")) as IEdmStructuredType; - ModelBoundQuerySettings querySettings = new ModelBoundQuerySettings(); - querySettings.ExpandConfigurations.Add("Orders", new ExpandConfiguration - { - ExpandType = SelectExpandType.Allowed, - MaxDepth = 0 - }); - model.Model.SetAnnotationValue(customerType, querySettings); - - // Act & Assert - ExceptionAssert.DoesNotThrow( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings { MaxExpansionDepth = 0 })); - } - - [Fact] - public void ValidateSelectExpandQueryValidator_ThrowException_IfNotNavigable() - { - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); - ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); - queryContext.RequestContainer = new MockServiceProvider(); - model.Model.SetAnnotationValue( - model.Customer.FindProperty("Orders"), - new QueryableRestrictionsAnnotation(new QueryableRestrictions { NotNavigable = true })); - - string select = "Orders"; - ISelectExpandQueryValidator validator = queryContext.GetSelectExpandQueryValidator(); - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(select, null, queryContext); - ExceptionAssert.Throws( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), - "The property 'Orders' cannot be used for navigation."); - } - - [Theory] - [InlineData("Customer", "Orders")] - [InlineData("SpecialCustomer", "SpecialOrders")] - public void ValidateSelectExpandQueryValidator_ThrowException_IfBaseOrDerivedClassPropertyNotNavigable(string className, string propertyName) - { - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.SpecialCustomer, new ClrTypeAnnotation(typeof(Customer))); - ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); - queryContext.RequestContainer = new MockServiceProvider(); - EdmEntityType classType = (className == "Customer") ? model.Customer : model.SpecialCustomer; - model.Model.SetAnnotationValue(classType.FindProperty(propertyName), new QueryableRestrictionsAnnotation(new QueryableRestrictions { NotNavigable = true })); - - string select = "NS.SpecialCustomer/" + propertyName; - ISelectExpandQueryValidator validator = queryContext.GetSelectExpandQueryValidator(); - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(select, null, queryContext); - ExceptionAssert.Throws( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), - String.Format(CultureInfo.InvariantCulture, "The property '{0}' cannot be used for navigation.", propertyName)); - } - - [Fact] - public void ValidateSelectExpandQueryValidator_ThrowException_IfNotExpandable() - { - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); - ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); - queryContext.RequestContainer = new MockServiceProvider(); - model.Model.SetAnnotationValue(model.Customer.FindProperty("Orders"), new QueryableRestrictionsAnnotation(new QueryableRestrictions { NotExpandable = true })); - - string expand = "Orders"; - ISelectExpandQueryValidator validator = queryContext.GetSelectExpandQueryValidator(); - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, queryContext); - ExceptionAssert.Throws( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), - "The property 'Orders' cannot be used in the $expand query option."); - } - - [Fact] - public void ValidateSelectExpandQueryValidator_ThrowException_IfNotExpandable_QuerySettings() + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => validator.Validate(option, null), "validationSettings"); + } + + [Theory] + [InlineData("Orders($expand=Customer)", 1)] + [InlineData("Orders,Orders($expand=Customer)", 1)] + [InlineData("Orders($expand=Customer($expand=Orders))", 2)] + [InlineData("Orders($expand=Customer($expand=Orders($expand=Customer($expand=Orders($expand=Customer)))))", 5)] + [InlineData("Orders($expand=NS.SpecialOrder/SpecialCustomer)", 1)] + public void ValidateSelectExpandQueryValidator_DepthChecks(string expand, int maxExpansionDepth) + { + // Arrange + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, _queryContext); + selectExpandQueryOption.LevelsMaxLiteralExpansionDepth = 1; + + // Act & Assert + ExceptionAssert.Throws( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth }), + String.Format(CultureInfo.CurrentCulture, MaxExpandDepthExceededErrorString, maxExpansionDepth)); + + ExceptionAssert.DoesNotThrow( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth + 1 })); + } + + [Theory] + [InlineData("Orders($expand=Customer)", 1)] + [InlineData("Orders,Orders($expand=Customer)", 1)] + [InlineData("Orders($expand=Customer($expand=Orders))", 2)] + [InlineData("Orders($expand=Customer($expand=Orders($expand=Customer($expand=Orders($expand=Customer)))))", 5)] + [InlineData("Orders($expand=NS.SpecialOrder/SpecialCustomer)", 1)] + public void ValidateSelectExpandQueryValidator_DepthChecks_QuerySettings(string expand, int maxExpansionDepth) + { + // Arrange + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); + ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); + queryContext.DefaultQueryConfigurations.EnableExpand = true; + queryContext.RequestContainer = new MockServiceProvider(); + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, queryContext); + selectExpandQueryOption.LevelsMaxLiteralExpansionDepth = 1; + IEdmStructuredType customerType = + model.Model.SchemaElements.First(e => e.Name.Equals("Customer")) as IEdmStructuredType; + ModelBoundQuerySettings querySettings = new ModelBoundQuerySettings(); + querySettings.ExpandConfigurations.Add("Orders", new ExpandConfiguration { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); - ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); - queryContext.RequestContainer = new MockServiceProvider(); - ISelectExpandQueryValidator validator = queryContext.GetSelectExpandQueryValidator(); - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, "Orders", queryContext); - IEdmStructuredType customerType = - model.Model.SchemaElements.First(e => e.Name.Equals("Customer")) as IEdmStructuredType; - ModelBoundQuerySettings querySettings = new ModelBoundQuerySettings(); - querySettings.ExpandConfigurations.Add("Orders", new ExpandConfiguration - { - ExpandType = SelectExpandType.Disabled, - MaxDepth = 0 - }); - model.Model.SetAnnotationValue(customerType, querySettings); - - // Act & Assert - ExceptionAssert.Throws( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), - "The property 'Orders' cannot be used in the $expand query option."); - } - - [Theory] - [InlineData("Customer", "Orders")] - [InlineData("SpecialCustomer", "SpecialOrders")] - public void ValidateSelectExpandQueryValidator_ThrowException_IfBaseOrDerivedClassPropertyNotExpandable(string className, string propertyName) + ExpandType = SelectExpandType.Allowed, + MaxDepth = maxExpansionDepth + }); + model.Model.SetAnnotationValue(customerType, querySettings); + + // Act & Assert + ExceptionAssert.Throws( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth + 1 }), + String.Format(CultureInfo.CurrentCulture, MaxExpandDepthExceededErrorString, maxExpansionDepth)); + } + + [Theory] + [InlineData("Parent($levels=5)", 4)] + [InlineData("Parent($expand=Parent($levels=4))", 4)] + [InlineData("Parent($expand=Parent($expand=Parent($levels=0)))", 1)] + [InlineData("Parent($expand=Parent($levels=4);$levels=5)", 8)] + [InlineData("Parent($levels=4),DerivedAncestors($levels=5)", 4)] + [InlineData("DerivedAncestors($levels=5),Parent($levels=4)", 4)] + public void ValidateSelectExpandQueryValidator_DepthChecks_DollarLevels(string expand, int maxExpansionDepth) + { + // Arrange + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Entities"); + IEdmModel model = builder.GetEdmModel(); + var context = new ODataQueryContext(model, typeof(ODataLevelsTest.LevelsEntity)); + context.DefaultQueryConfigurations.EnableExpand = true; + context.RequestContainer = new MockServiceProvider(); + var selectExpandQueryOption = new SelectExpandQueryOption(null, expand, context); + selectExpandQueryOption.LevelsMaxLiteralExpansionDepth = 1; + + // Act & Assert + ExceptionAssert.Throws( + () => validator.Validate( + selectExpandQueryOption, + new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth }), + string.Format( + CultureInfo.CurrentCulture, + MaxExpandDepthExceededErrorString, + maxExpansionDepth)); + + ExceptionAssert.DoesNotThrow( + () => validator.Validate( + selectExpandQueryOption, + new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth + 1 })); + } + + [Fact] + public void ValidateSelectExpandQueryValidator_DoesNotThrow_IfExpansionDepthIsZero_DollarLevels() + { + // Arrange + string expand = "Parent($expand=Parent($expand=Parent($levels=10)))"; + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Entities"); + IEdmModel model = builder.GetEdmModel(); + var context = new ODataQueryContext(model, typeof(ODataLevelsTest.LevelsEntity)); + context.DefaultQueryConfigurations.EnableExpand = true; + context.RequestContainer = new MockServiceProvider(); + var selectExpandQueryOption = new SelectExpandQueryOption(null, expand, context); + + // Act & Assert + ExceptionAssert.DoesNotThrow( + () => validator.Validate( + selectExpandQueryOption, + new ODataValidationSettings { MaxExpansionDepth = 0 })); + } + + [Fact] + public void ValidateSelectExpandQueryValidator_Throws_LevelsMaxLiteralExpansionDepthGreaterThanMaxExpansionDepth() + { + // Arrange + string expand = "Parent($levels=2)"; + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Entities"); + IEdmModel model = builder.GetEdmModel(); + var context = new ODataQueryContext(model, typeof(ODataLevelsTest.LevelsEntity)); + context.DefaultQueryConfigurations.EnableExpand = true; + context.RequestContainer = new MockServiceProvider(); + var selectExpandQueryOption = new SelectExpandQueryOption(null, expand, context); + selectExpandQueryOption.LevelsMaxLiteralExpansionDepth = 4; + + // Act & Assert + ExceptionAssert.Throws( + () => validator.Validate( + selectExpandQueryOption, + new ODataValidationSettings { MaxExpansionDepth = 3 }), + "'LevelsMaxLiteralExpansionDepth' should be less than or equal to 'MaxExpansionDepth'."); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void ValidateSelectExpandQueryValidator_DoesNotThrow_DefaultLevelsMaxLiteralExpansionDepth(int maxExpansionDepth) + { + // Arrange + string expand = "Parent($levels=1)"; + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Entities"); + IEdmModel model = builder.GetEdmModel(); + var context = new ODataQueryContext(model, typeof(ODataLevelsTest.LevelsEntity)); + context.DefaultQueryConfigurations.EnableExpand = true; + context.RequestContainer = new MockServiceProvider(); + var selectExpandQueryOption = new SelectExpandQueryOption(null, expand, context); + + // Act & Assert + ExceptionAssert.DoesNotThrow( + () => validator.Validate( + selectExpandQueryOption, + new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth })); + } + + [Fact] + public void ValidateSelectExpandQueryValidator_Throw_WithInvalidMaxExpansionDepth() + { + int maxExpansionDepth = -1; + // Arrange + string expand = "Parent($levels=1)"; + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Entities"); + IEdmModel model = builder.GetEdmModel(); + var context = new ODataQueryContext(model, typeof(ODataLevelsTest.LevelsEntity)); + context.RequestContainer = new MockServiceProvider(); + var validator = context.GetSelectExpandQueryValidator(); + var selectExpandQueryOption = new SelectExpandQueryOption(null, expand, context); + + // Act & Assert + ExceptionAssert.Throws( + () => validator.Validate( + selectExpandQueryOption, + new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth }), + "Value must be greater than or equal to 0. (Parameter 'value')\r\nActual value was -1."); + } + + [Theory] + [InlineData(2, 3)] + [InlineData(4, 4)] + [InlineData(3, 0)] + public void ValidateSelectExpandQueryValidator_DoesNotThrow_LevelsMaxLiteralExpansionDepthAndMaxExpansionDepth( + int levelsMaxLiteralExpansionDepth, + int maxExpansionDepth) + { + // Arrange + string expand = "Parent($levels=2)"; + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Entities"); + IEdmModel model = builder.GetEdmModel(); + var context = new ODataQueryContext(model, typeof(ODataLevelsTest.LevelsEntity)); + context.DefaultQueryConfigurations.EnableExpand = true; + context.RequestContainer = new MockServiceProvider(); + var selectExpandQueryOption = new SelectExpandQueryOption(null, expand, context); + selectExpandQueryOption.LevelsMaxLiteralExpansionDepth = levelsMaxLiteralExpansionDepth; + + // Act & Assert + ExceptionAssert.DoesNotThrow( + () => validator.Validate( + selectExpandQueryOption, + new ODataValidationSettings { MaxExpansionDepth = maxExpansionDepth })); + } + + [Fact] + public void ValidateSelectExpandQueryValidator_Throws_IfNotAllowTop() + { + // Arrange + string expand = "Orders($top=4)"; + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + _queryContext.DefaultQueryConfigurations.EnableExpand = true; + _queryContext.DefaultQueryConfigurations.MaxTop = 2; + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, _queryContext); + + // Act & Assert + ExceptionAssert.Throws( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), + "The limit of '2' for Top query has been exceeded. The value from the incoming request is '4'."); + } + + [Fact] + public void ValidateSelectExpandQueryValidator_Throws_IfNotAllowCount() + { + // Arrange + string expand = "Orders($count=true)"; + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + _queryContext.DefaultQueryConfigurations.EnableExpand = true; + _queryContext.DefaultQueryConfigurations.EnableCount = false; + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, _queryContext); + + // Act & Assert + ExceptionAssert.Throws( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), + "The property 'Orders' cannot be used for $count."); + } + + [Fact] + public void ValidateSelectExpandQueryValidator_Throws_IfNotAllowOrderby() + { + // Arrange + string expand = "Orders($orderby=Amount)"; + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + _queryContext.DefaultQueryConfigurations.EnableExpand = true; + _queryContext.DefaultQueryConfigurations.EnableOrderBy = false; + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, _queryContext); + + // Act & Assert + ExceptionAssert.Throws( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), + "The property 'Amount' cannot be used in the $orderby query option."); + } + + [Fact] + public void ValidateSelectExpandQueryValidator_Throws_IfNotAllowFilter() + { + // Arrange + string expand = "Orders($filter=Amount eq 42)"; + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + _queryContext.DefaultQueryConfigurations.EnableExpand = true; + _queryContext.DefaultQueryConfigurations.EnableFilter = false; + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, _queryContext); + + // Act & Assert + ExceptionAssert.Throws( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), + "The property 'Amount' cannot be used in the $filter query option."); + } + + [Fact] + public void ValidateSelectExpandQueryValidator_DoesNotThrow_IfExpansionDepthIsZero() + { + // Arrange + string expand = "Orders($expand=Customer($expand=Orders($expand=Customer($expand=Orders($expand=Customer)))))"; + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + _queryContext.DefaultQueryConfigurations.EnableExpand = true; + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, _queryContext); + + // Act & Assert + ExceptionAssert.DoesNotThrow( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings { MaxExpansionDepth = 0 })); + } + + [Fact] + public void ValidateSelectExpandQueryValidator_DoesNotThrow_IfExpansionDepthIsZero_QuerySettings() + { + // Arrange + string expand = + "Orders($expand=Customer($expand=Orders($expand=Customer($expand=Orders($expand=Customer)))))"; + SelectExpandQueryValidator validator = new SelectExpandQueryValidator(); + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); + ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); + queryContext.DefaultQueryConfigurations.EnableExpand = true; + queryContext.RequestContainer = new MockServiceProvider(); + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, queryContext); + IEdmStructuredType customerType = + model.Model.SchemaElements.First(e => e.Name.Equals("Customer")) as IEdmStructuredType; + ModelBoundQuerySettings querySettings = new ModelBoundQuerySettings(); + querySettings.ExpandConfigurations.Add("Orders", new ExpandConfiguration { - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.SpecialCustomer, new ClrTypeAnnotation(typeof(Customer))); - ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); - queryContext.RequestContainer = new MockServiceProvider(); - EdmEntityType classType = (className == "Customer") ? model.Customer : model.SpecialCustomer; - model.Model.SetAnnotationValue(classType.FindProperty(propertyName), new QueryableRestrictionsAnnotation(new QueryableRestrictions { NotExpandable = true })); - - string expand = "NS.SpecialCustomer/" + propertyName; - ISelectExpandQueryValidator validator = queryContext.GetSelectExpandQueryValidator(); - SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, queryContext); - ExceptionAssert.Throws( - () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), - string.Format(CultureInfo.InvariantCulture, "The property '{0}' cannot be used in the $expand query option.", propertyName)); - } - - [Fact] - public void GetSelectExpandQueryValidator_Returns_Validator() + ExpandType = SelectExpandType.Allowed, + MaxDepth = 0 + }); + model.Model.SetAnnotationValue(customerType, querySettings); + + // Act & Assert + ExceptionAssert.DoesNotThrow( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings { MaxExpansionDepth = 0 })); + } + + [Fact] + public void ValidateSelectExpandQueryValidator_ThrowException_IfNotNavigable() + { + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); + ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); + queryContext.RequestContainer = new MockServiceProvider(); + model.Model.SetAnnotationValue( + model.Customer.FindProperty("Orders"), + new QueryableRestrictionsAnnotation(new QueryableRestrictions { NotNavigable = true })); + + string select = "Orders"; + ISelectExpandQueryValidator validator = queryContext.GetSelectExpandQueryValidator(); + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(select, null, queryContext); + ExceptionAssert.Throws( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), + "The property 'Orders' cannot be used for navigation."); + } + + [Theory] + [InlineData("Customer", "Orders")] + [InlineData("SpecialCustomer", "SpecialOrders")] + public void ValidateSelectExpandQueryValidator_ThrowException_IfBaseOrDerivedClassPropertyNotNavigable(string className, string propertyName) + { + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.SpecialCustomer, new ClrTypeAnnotation(typeof(Customer))); + ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); + queryContext.RequestContainer = new MockServiceProvider(); + EdmEntityType classType = (className == "Customer") ? model.Customer : model.SpecialCustomer; + model.Model.SetAnnotationValue(classType.FindProperty(propertyName), new QueryableRestrictionsAnnotation(new QueryableRestrictions { NotNavigable = true })); + + string select = "NS.SpecialCustomer/" + propertyName; + ISelectExpandQueryValidator validator = queryContext.GetSelectExpandQueryValidator(); + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(select, null, queryContext); + ExceptionAssert.Throws( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), + String.Format(CultureInfo.InvariantCulture, "The property '{0}' cannot be used for navigation.", propertyName)); + } + + [Fact] + public void ValidateSelectExpandQueryValidator_ThrowException_IfNotExpandable() + { + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); + ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); + queryContext.RequestContainer = new MockServiceProvider(); + model.Model.SetAnnotationValue(model.Customer.FindProperty("Orders"), new QueryableRestrictionsAnnotation(new QueryableRestrictions { NotExpandable = true })); + + string expand = "Orders"; + ISelectExpandQueryValidator validator = queryContext.GetSelectExpandQueryValidator(); + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, queryContext); + ExceptionAssert.Throws( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), + "The property 'Orders' cannot be used in the $expand query option."); + } + + [Fact] + public void ValidateSelectExpandQueryValidator_ThrowException_IfNotExpandable_QuerySettings() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(Customer))); + ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); + queryContext.RequestContainer = new MockServiceProvider(); + ISelectExpandQueryValidator validator = queryContext.GetSelectExpandQueryValidator(); + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, "Orders", queryContext); + IEdmStructuredType customerType = + model.Model.SchemaElements.First(e => e.Name.Equals("Customer")) as IEdmStructuredType; + ModelBoundQuerySettings querySettings = new ModelBoundQuerySettings(); + querySettings.ExpandConfigurations.Add("Orders", new ExpandConfiguration { - // Arrange & Act & Assert - ODataQueryContext context = null; - Assert.NotNull(context.GetSelectExpandQueryValidator()); - - // Arrange & Act & Assert - context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - Assert.NotNull(context.GetSelectExpandQueryValidator()); - - // Arrange & Act & Assert - IServiceProvider services = new ServiceCollection() - .AddSingleton() - .AddSingleton().BuildServiceProvider(); - context.RequestContainer = services; - Assert.NotNull(context.GetSelectExpandQueryValidator()); - } + ExpandType = SelectExpandType.Disabled, + MaxDepth = 0 + }); + model.Model.SetAnnotationValue(customerType, querySettings); + + // Act & Assert + ExceptionAssert.Throws( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), + "The property 'Orders' cannot be used in the $expand query option."); + } + + [Theory] + [InlineData("Customer", "Orders")] + [InlineData("SpecialCustomer", "SpecialOrders")] + public void ValidateSelectExpandQueryValidator_ThrowException_IfBaseOrDerivedClassPropertyNotExpandable(string className, string propertyName) + { + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.SpecialCustomer, new ClrTypeAnnotation(typeof(Customer))); + ODataQueryContext queryContext = new ODataQueryContext(model.Model, typeof(Customer)); + queryContext.RequestContainer = new MockServiceProvider(); + EdmEntityType classType = (className == "Customer") ? model.Customer : model.SpecialCustomer; + model.Model.SetAnnotationValue(classType.FindProperty(propertyName), new QueryableRestrictionsAnnotation(new QueryableRestrictions { NotExpandable = true })); + + string expand = "NS.SpecialCustomer/" + propertyName; + ISelectExpandQueryValidator validator = queryContext.GetSelectExpandQueryValidator(); + SelectExpandQueryOption selectExpandQueryOption = new SelectExpandQueryOption(null, expand, queryContext); + ExceptionAssert.Throws( + () => validator.Validate(selectExpandQueryOption, new ODataValidationSettings()), + string.Format(CultureInfo.InvariantCulture, "The property '{0}' cannot be used in the $expand query option.", propertyName)); + } + + [Fact] + public void GetSelectExpandQueryValidator_Returns_Validator() + { + // Arrange & Act & Assert + ODataQueryContext context = null; + Assert.NotNull(context.GetSelectExpandQueryValidator()); + + // Arrange & Act & Assert + context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + Assert.NotNull(context.GetSelectExpandQueryValidator()); + + // Arrange & Act & Assert + IServiceProvider services = new ServiceCollection() + .AddSingleton() + .AddSingleton().BuildServiceProvider(); + context.RequestContainer = services; + Assert.NotNull(context.GetSelectExpandQueryValidator()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/SkipQueryValidatorTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/SkipQueryValidatorTest.cs index 9f06b892c..910f231d3 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/SkipQueryValidatorTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/SkipQueryValidatorTest.cs @@ -14,89 +14,88 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Validator +namespace Microsoft.AspNetCore.OData.Tests.Query.Validator; + +public class SkipQueryValidatorTest { - public class SkipQueryValidatorTest - { - private SkipQueryValidator _validator; - private ODataQueryContext _context; + private SkipQueryValidator _validator; + private ODataQueryContext _context; - public SkipQueryValidatorTest() - { - _validator = new SkipQueryValidator(); - _context = ValidationTestHelper.CreateCustomerContext(); - } + public SkipQueryValidatorTest() + { + _validator = new SkipQueryValidator(); + _context = ValidationTestHelper.CreateCustomerContext(); + } - [Fact] - public void ValidateSkipQueryValidator_ThrowsOnNullOption() - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(null, new ODataValidationSettings())); - } + [Fact] + public void ValidateSkipQueryValidator_ThrowsOnNullOption() + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(null, new ODataValidationSettings())); + } - [Fact] - public void ValidateSkipQueryValidator_ThrowsOnNullSettings() - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(new SkipQueryOption("2", _context), null)); - } + [Fact] + public void ValidateSkipQueryValidator_ThrowsOnNullSettings() + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(new SkipQueryOption("2", _context), null)); + } - [Fact] - public void ValidateSkipQueryValidator_ThrowsWhenLimitIsExceeded() + [Fact] + public void ValidateSkipQueryValidator_ThrowsWhenLimitIsExceeded() + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings() { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings() - { - MaxSkip = 10 - }; + MaxSkip = 10 + }; - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(new SkipQueryOption("11", _context), settings), - "The limit of '10' for Skip query has been exceeded. The value from the incoming request is '11'."); - } + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(new SkipQueryOption("11", _context), settings), + "The limit of '10' for Skip query has been exceeded. The value from the incoming request is '11'."); + } - [Fact] - public void ValidateSkipQueryValidator_PassWhenLimitIsReached() + [Fact] + public void ValidateSkipQueryValidator_PassWhenLimitIsReached() + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings() { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings() - { - MaxSkip = 10 - }; + MaxSkip = 10 + }; - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(new SkipQueryOption("10", _context), settings)); - } + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(new SkipQueryOption("10", _context), settings)); + } - [Fact] - public void ValidateSkipQueryValidator_PassWhenLimitIsNotReached() + [Fact] + public void ValidateSkipQueryValidator_PassWhenLimitIsNotReached() + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings() { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings() - { - MaxSkip = 10 - }; + MaxSkip = 10 + }; - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(new SkipQueryOption("9", _context), settings)); - } + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(new SkipQueryOption("9", _context), settings)); + } - [Fact] - public void GetSkipQueryValidator_Returns_Validator() - { - // Arrange & Act & Assert - ODataQueryContext context = null; - Assert.NotNull(context.GetSkipQueryValidator()); + [Fact] + public void GetSkipQueryValidator_Returns_Validator() + { + // Arrange & Act & Assert + ODataQueryContext context = null; + Assert.NotNull(context.GetSkipQueryValidator()); - // Arrange & Act & Assert - context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - Assert.NotNull(context.GetSkipQueryValidator()); + // Arrange & Act & Assert + context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + Assert.NotNull(context.GetSkipQueryValidator()); - // Arrange & Act & Assert - IServiceProvider services = new ServiceCollection() - .AddSingleton().BuildServiceProvider(); - context.RequestContainer = services; - Assert.NotNull(context.GetSkipQueryValidator()); - } + // Arrange & Act & Assert + IServiceProvider services = new ServiceCollection() + .AddSingleton().BuildServiceProvider(); + context.RequestContainer = services; + Assert.NotNull(context.GetSkipQueryValidator()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/SkipTokenQueryValidatorTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/SkipTokenQueryValidatorTests.cs index d8fe5b155..e78eb6b59 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/SkipTokenQueryValidatorTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/SkipTokenQueryValidatorTests.cs @@ -13,46 +13,45 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Validator +namespace Microsoft.AspNetCore.OData.Tests.Query.Validator; + +public class SkipTokenQueryValidatorTests { - public class SkipTokenQueryValidatorTests + [Fact] + public void ValidateSkipTokenQueryValidator_ThrowsOnNullOption() + { + // Arrange & Act & Assert + SkipTokenQueryValidator validator = new SkipTokenQueryValidator(); + ExceptionAssert.Throws(() => validator.Validate(null, new ODataValidationSettings())); + } + + [Fact] + public void ValidateSkipTokenQueryValidator_ThrowsOnNullSettings() + { + // Arrange & Act + SkipTokenQueryValidator validator = new SkipTokenQueryValidator(); + + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int), null); + SkipTokenQueryOption query = new SkipTokenQueryOption("abc", context); + + // Assert + ExceptionAssert.Throws(() => validator.Validate(query, null)); + } + + [Fact] + public void ValidateSkipTokenQueryValidator_ThrowsNotAllowedQueryOption() { - [Fact] - public void ValidateSkipTokenQueryValidator_ThrowsOnNullOption() - { - // Arrange & Act & Assert - SkipTokenQueryValidator validator = new SkipTokenQueryValidator(); - ExceptionAssert.Throws(() => validator.Validate(null, new ODataValidationSettings())); - } - - [Fact] - public void ValidateSkipTokenQueryValidator_ThrowsOnNullSettings() - { - // Arrange & Act - SkipTokenQueryValidator validator = new SkipTokenQueryValidator(); - - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int), null); - SkipTokenQueryOption query = new SkipTokenQueryOption("abc", context); - - // Assert - ExceptionAssert.Throws(() => validator.Validate(query, null)); - } - - [Fact] - public void ValidateSkipTokenQueryValidator_ThrowsNotAllowedQueryOption() - { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings(); - - ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int), null); - context.DefaultQueryConfigurations.EnableSkipToken = false; - SkipTokenQueryOption query = new SkipTokenQueryOption("abc", context); - - SkipTokenQueryValidator validator = new SkipTokenQueryValidator(); - - // Act & Assert - ExceptionAssert.Throws(() => validator.Validate(query, settings), - "Query option 'SkipToken' is not allowed. To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings."); - } + // Arrange + ODataValidationSettings settings = new ODataValidationSettings(); + + ODataQueryContext context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int), null); + context.DefaultQueryConfigurations.EnableSkipToken = false; + SkipTokenQueryOption query = new SkipTokenQueryOption("abc", context); + + SkipTokenQueryValidator validator = new SkipTokenQueryValidator(); + + // Act & Assert + ExceptionAssert.Throws(() => validator.Validate(query, settings), + "Query option 'SkipToken' is not allowed. To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/TopQueryValidatorTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/TopQueryValidatorTest.cs index 5c7d21619..7aaa60846 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/TopQueryValidatorTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/TopQueryValidatorTest.cs @@ -15,124 +15,123 @@ using Microsoft.OData.ModelBuilder.Config; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Validator +namespace Microsoft.AspNetCore.OData.Tests.Query.Validator; + +public class TopQueryValidatorTest { - public class TopQueryValidatorTest + private TopQueryValidator _validator; + private ODataQueryContext _context; + + public TopQueryValidatorTest() { - private TopQueryValidator _validator; - private ODataQueryContext _context; + _validator = new TopQueryValidator(); + _context = ValidationTestHelper.CreateCustomerContext(); + } - public TopQueryValidatorTest() - { - _validator = new TopQueryValidator(); - _context = ValidationTestHelper.CreateCustomerContext(); - } + [Fact] + public void ValidateTopQueryValidator_ThrowsOnNullOption() + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(null, new ODataValidationSettings())); + } - [Fact] - public void ValidateTopQueryValidator_ThrowsOnNullOption() - { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(null, new ODataValidationSettings())); - } + [Fact] + public void ValidateTopQueryValidator_ThrowsOnNullSettings() + { + // Arrange & Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(new TopQueryOption("2", _context), null)); + } - [Fact] - public void ValidateTopQueryValidator_ThrowsOnNullSettings() + [Fact] + public void ValidateTopQueryValidator_ThrowsWhenLimitIsExceeded() + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings() { - // Arrange & Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(new TopQueryOption("2", _context), null)); - } + MaxTop = 10 + }; - [Fact] - public void ValidateTopQueryValidator_ThrowsWhenLimitIsExceeded() - { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings() - { - MaxTop = 10 - }; - - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(new TopQueryOption("11", _context), settings), - "The limit of '10' for Top query has been exceeded. The value from the incoming request is '11'."); - } - - [Fact] - public void ValidateTopQueryValidator_PassWhenLimitIsReached() - { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings() - { - MaxTop = 10 - }; - - // Act - ExceptionAssert.DoesNotThrow(() => _validator.Validate(new TopQueryOption("10", _context), settings)); - } - - [Fact] - public void ValidateTopQueryValidator_PassWhenLimitIsNotReached() + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(new TopQueryOption("11", _context), settings), + "The limit of '10' for Top query has been exceeded. The value from the incoming request is '11'."); + } + + [Fact] + public void ValidateTopQueryValidator_PassWhenLimitIsReached() + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings() { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings() - { - MaxTop = 10 - }; - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(new TopQueryOption("9", _context), settings)); - } - - [Fact] - public void ValidateTopQueryValidator_PassWhenQuerySettingsLimitIsNotReached() + MaxTop = 10 + }; + + // Act + ExceptionAssert.DoesNotThrow(() => _validator.Validate(new TopQueryOption("10", _context), settings)); + } + + [Fact] + public void ValidateTopQueryValidator_PassWhenLimitIsNotReached() + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings() { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings() - { - MaxTop = 20 - }; - ModelBoundQuerySettings modelBoundQuerySettings = new ModelBoundQuerySettings(); - modelBoundQuerySettings.MaxTop = 20; - ODataQueryContext context = ValidationTestHelper.CreateCustomerContext(); - context.Model.SetAnnotationValue(context.ElementType as IEdmStructuredType, modelBoundQuerySettings); - - // Act & Assert - ExceptionAssert.DoesNotThrow(() => _validator.Validate(new TopQueryOption("20", context), settings)); - } - - [Fact] - public void ValidateTopQueryValidator_ThrowsWhenQuerySettingsLimitIsExceeded() + MaxTop = 10 + }; + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(new TopQueryOption("9", _context), settings)); + } + + [Fact] + public void ValidateTopQueryValidator_PassWhenQuerySettingsLimitIsNotReached() + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings() { - // Arrange - ODataValidationSettings settings = new ODataValidationSettings() - { - MaxTop = 20 - }; - ModelBoundQuerySettings modelBoundQuerySettings = new ModelBoundQuerySettings(); - modelBoundQuerySettings.MaxTop = 10; - ODataQueryContext context = ValidationTestHelper.CreateCustomerContext(); - context.Model.SetAnnotationValue(context.ElementType as IEdmStructuredType, modelBoundQuerySettings); - - // Act & Assert - ExceptionAssert.Throws(() => _validator.Validate(new TopQueryOption("11", context), settings), - "The limit of '10' for Top query has been exceeded. The value from the incoming request is '11'."); - } - - [Fact] - public void GetTopQueryValidator_Returns_Validator() + MaxTop = 20 + }; + ModelBoundQuerySettings modelBoundQuerySettings = new ModelBoundQuerySettings(); + modelBoundQuerySettings.MaxTop = 20; + ODataQueryContext context = ValidationTestHelper.CreateCustomerContext(); + context.Model.SetAnnotationValue(context.ElementType as IEdmStructuredType, modelBoundQuerySettings); + + // Act & Assert + ExceptionAssert.DoesNotThrow(() => _validator.Validate(new TopQueryOption("20", context), settings)); + } + + [Fact] + public void ValidateTopQueryValidator_ThrowsWhenQuerySettingsLimitIsExceeded() + { + // Arrange + ODataValidationSettings settings = new ODataValidationSettings() { - // Arrange & Act & Assert - ODataQueryContext context = null; - Assert.NotNull(context.GetTopQueryValidator()); - - // Arrange & Act & Assert - context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); - Assert.NotNull(context.GetTopQueryValidator()); - - // Arrange & Act & Assert - IServiceProvider services = new ServiceCollection() - .AddSingleton().BuildServiceProvider(); - context.RequestContainer = services; - Assert.NotNull(context.GetTopQueryValidator()); - } + MaxTop = 20 + }; + ModelBoundQuerySettings modelBoundQuerySettings = new ModelBoundQuerySettings(); + modelBoundQuerySettings.MaxTop = 10; + ODataQueryContext context = ValidationTestHelper.CreateCustomerContext(); + context.Model.SetAnnotationValue(context.ElementType as IEdmStructuredType, modelBoundQuerySettings); + + // Act & Assert + ExceptionAssert.Throws(() => _validator.Validate(new TopQueryOption("11", context), settings), + "The limit of '10' for Top query has been exceeded. The value from the incoming request is '11'."); + } + + [Fact] + public void GetTopQueryValidator_Returns_Validator() + { + // Arrange & Act & Assert + ODataQueryContext context = null; + Assert.NotNull(context.GetTopQueryValidator()); + + // Arrange & Act & Assert + context = new ODataQueryContext(EdmCoreModel.Instance, typeof(int)); + Assert.NotNull(context.GetTopQueryValidator()); + + // Arrange & Act & Assert + IServiceProvider services = new ServiceCollection() + .AddSingleton().BuildServiceProvider(); + context.RequestContainer = services; + Assert.NotNull(context.GetTopQueryValidator()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ValidationTestHelper.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ValidationTestHelper.cs index 84b181a3e..d87bf7eda 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ValidationTestHelper.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Validator/ValidationTestHelper.cs @@ -12,69 +12,68 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -namespace Microsoft.AspNetCore.OData.Tests.Query.Validator +namespace Microsoft.AspNetCore.OData.Tests.Query.Validator; + +internal static class ValidationTestHelper { - internal static class ValidationTestHelper + internal static ODataQueryContext CreateCustomerContext() { - internal static ODataQueryContext CreateCustomerContext() - { - return CreateCustomerContext(true); - } + return CreateCustomerContext(true); + } - internal static ODataQueryContext CreateCustomerContext(bool setRequestContainer) + internal static ODataQueryContext CreateCustomerContext(bool setRequestContainer) + { + ODataQueryContext context = new ODataQueryContext(GetCustomersModel(), typeof(QueryCompositionCustomer), null); + if (setRequestContainer) { - ODataQueryContext context = new ODataQueryContext(GetCustomersModel(), typeof(QueryCompositionCustomer), null); - if (setRequestContainer) - { - context.RequestContainer = new MockServiceProvider(); - } - - context.DefaultQueryConfigurations.EnableOrderBy = true; - context.DefaultQueryConfigurations.MaxTop = null; - return context; + context.RequestContainer = new MockServiceProvider(); } - internal static ODataQueryContext CreateProductContext() - { - return new ODataQueryContext(GetProductsModel(), typeof(Product)); - } + context.DefaultQueryConfigurations.EnableOrderBy = true; + context.DefaultQueryConfigurations.MaxTop = null; + return context; + } - internal static ODataQueryContext CreateDerivedProductsContext() - { - ODataQueryContext context = new ODataQueryContext(GetDerivedProductsModel(), typeof(Product), null); - context.RequestContainer = new MockServiceProvider(); - context.DefaultQueryConfigurations.EnableFilter = true; - return context; - } + internal static ODataQueryContext CreateProductContext() + { + return new ODataQueryContext(GetProductsModel(), typeof(Product)); + } - private static IEdmModel GetCustomersModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customer"); - builder.EntityType(); - return builder.GetEdmModel(); - } + internal static ODataQueryContext CreateDerivedProductsContext() + { + ODataQueryContext context = new ODataQueryContext(GetDerivedProductsModel(), typeof(Product), null); + context.RequestContainer = new MockServiceProvider(); + context.DefaultQueryConfigurations.EnableFilter = true; + return context; + } - private static IEdmModel GetProductsModel() - { - var builder = GetProductsBuilder(); - return builder.GetEdmModel(); - } + private static IEdmModel GetCustomersModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customer"); + builder.EntityType(); + return builder.GetEdmModel(); + } - private static IEdmModel GetDerivedProductsModel() - { - var builder = GetProductsBuilder(); - builder.EntitySet("Product"); - builder.EntityType().DerivesFrom(); - builder.EntityType().DerivesFrom(); - return builder.GetEdmModel(); - } + private static IEdmModel GetProductsModel() + { + var builder = GetProductsBuilder(); + return builder.GetEdmModel(); + } - private static ODataConventionModelBuilder GetProductsBuilder() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("Product"); - return builder; - } + private static IEdmModel GetDerivedProductsModel() + { + var builder = GetProductsBuilder(); + builder.EntitySet("Product"); + builder.EntityType().DerivesFrom(); + builder.EntityType().DerivesFrom(); + return builder.GetEdmModel(); + } + + private static ODataConventionModelBuilder GetProductsBuilder() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("Product"); + return builder; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/DynamicTypeWrapperConverterTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/DynamicTypeWrapperConverterTests.cs index 9314a1325..9b6eae9b4 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/DynamicTypeWrapperConverterTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/DynamicTypeWrapperConverterTests.cs @@ -14,207 +14,206 @@ using Microsoft.AspNetCore.OData.Tests.Extensions; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Tests.Query.Wrapper; + +public class DynamicTypeWrapperConverterTests { - public class DynamicTypeWrapperConverterTests + [Theory] + [InlineData(typeof(AggregationWrapper), true)] + [InlineData(typeof(ComputeWrapper), true)] + [InlineData(typeof(EntitySetAggregationWrapper), true)] + [InlineData(typeof(FlatteningWrapper), true)] + [InlineData(typeof(GroupByWrapper), true)] + [InlineData(typeof(NoGroupByAggregationWrapper), true)] + [InlineData(typeof(NoGroupByWrapper), true)] + [InlineData(typeof(object), false)] + [InlineData(typeof(SelectExpandWrapper), false)] + [InlineData(null, false)] + public void CanConvertWorksForDynamicTypeWrapper(Type type, bool expected) { - [Theory] - [InlineData(typeof(AggregationWrapper), true)] - [InlineData(typeof(ComputeWrapper), true)] - [InlineData(typeof(EntitySetAggregationWrapper), true)] - [InlineData(typeof(FlatteningWrapper), true)] - [InlineData(typeof(GroupByWrapper), true)] - [InlineData(typeof(NoGroupByAggregationWrapper), true)] - [InlineData(typeof(NoGroupByWrapper), true)] - [InlineData(typeof(object), false)] - [InlineData(typeof(SelectExpandWrapper), false)] - [InlineData(null, false)] - public void CanConvertWorksForDynamicTypeWrapper(Type type, bool expected) - { - // Arrange - DynamicTypeWrapperConverter converter = new DynamicTypeWrapperConverter(); + // Arrange + DynamicTypeWrapperConverter converter = new DynamicTypeWrapperConverter(); - // Act & Assert - Assert.Equal(expected, converter.CanConvert(type)); - } + // Act & Assert + Assert.Equal(expected, converter.CanConvert(type)); + } - [Theory] - [InlineData(typeof(AggregationWrapper), typeof(AggregationWrapperConverter))] - [InlineData(typeof(ComputeWrapper), typeof(ComputeWrapperConverter))] - [InlineData(typeof(EntitySetAggregationWrapper), typeof(EntitySetAggregationWrapperConverter))] - [InlineData(typeof(FlatteningWrapper), typeof(FlatteningWrapperConverter))] - [InlineData(typeof(GroupByWrapper), typeof(GroupByWrapperConverter))] - [InlineData(typeof(NoGroupByAggregationWrapper), typeof(NoGroupByAggregationWrapperConverter))] - [InlineData(typeof(NoGroupByWrapper), typeof(NoGroupByWrapperConverter))] - [InlineData(typeof(object), null)] - [InlineData(typeof(SelectExpandWrapper), null)] - [InlineData(null, null)] - public void CreateConverterWorksForDynamicTypeWrapper(Type type, Type expected) - { - // Arrange - JsonSerializerOptions options = new JsonSerializerOptions(); - DynamicTypeWrapperConverter converter = new DynamicTypeWrapperConverter(); + [Theory] + [InlineData(typeof(AggregationWrapper), typeof(AggregationWrapperConverter))] + [InlineData(typeof(ComputeWrapper), typeof(ComputeWrapperConverter))] + [InlineData(typeof(EntitySetAggregationWrapper), typeof(EntitySetAggregationWrapperConverter))] + [InlineData(typeof(FlatteningWrapper), typeof(FlatteningWrapperConverter))] + [InlineData(typeof(GroupByWrapper), typeof(GroupByWrapperConverter))] + [InlineData(typeof(NoGroupByAggregationWrapper), typeof(NoGroupByAggregationWrapperConverter))] + [InlineData(typeof(NoGroupByWrapper), typeof(NoGroupByWrapperConverter))] + [InlineData(typeof(object), null)] + [InlineData(typeof(SelectExpandWrapper), null)] + [InlineData(null, null)] + public void CreateConverterWorksForDynamicTypeWrapper(Type type, Type expected) + { + // Arrange + JsonSerializerOptions options = new JsonSerializerOptions(); + DynamicTypeWrapperConverter converter = new DynamicTypeWrapperConverter(); - // Act - JsonConverter typeConverter = converter.CreateConverter(type, options); + // Act + JsonConverter typeConverter = converter.CreateConverter(type, options); - // Assert - if (expected == null) - { - Assert.Null(typeConverter); - } - else - { - Assert.Equal(expected, typeConverter.GetType()); - } - } - - [Fact] - public void AggregationWrapperConverter_Works_AggregationWrapper() + // Assert + if (expected == null) { - // Arrange & Act & Assert - TestDynamicTypeWrapperConverterRead(); - - // Arrange & Act & Assert - TestDynamicTypeWrapperConverterWrite(); + Assert.Null(typeConverter); } - - [Fact] - public void EntitySetAggregationWrapperConverter_Works_EntitySetAggregationWrapper() + else { - // Arrange & Act & Assert - TestDynamicTypeWrapperConverterRead(); - - // Arrange & Act & Assert - TestDynamicTypeWrapperConverterWrite(); + Assert.Equal(expected, typeConverter.GetType()); } + } - [Fact] - public void GroupByWrapperWrapperConverter_Works_GroupByWrapper() - { - // Arrange & Act & Assert - TestDynamicTypeWrapperConverterRead(); + [Fact] + public void AggregationWrapperConverter_Works_AggregationWrapper() + { + // Arrange & Act & Assert + TestDynamicTypeWrapperConverterRead(); - // Arrange & Act & Assert - TestDynamicTypeWrapperConverterWrite(); - } + // Arrange & Act & Assert + TestDynamicTypeWrapperConverterWrite(); + } - [Fact] - public void NoGroupByAggregationWrapperConverter_Works_NoGroupByAggregationWrapper() - { - // Arrange & Act & Assert - TestDynamicTypeWrapperConverterRead(); + [Fact] + public void EntitySetAggregationWrapperConverter_Works_EntitySetAggregationWrapper() + { + // Arrange & Act & Assert + TestDynamicTypeWrapperConverterRead(); - // Arrange & Act & Assert - TestDynamicTypeWrapperConverterWrite(); - } + // Arrange & Act & Assert + TestDynamicTypeWrapperConverterWrite(); + } - [Fact] - public void NoGroupByWrapperWrapperConverter_Works_NoGroupByWrapper() - { - // Arrange & Act & Assert - TestDynamicTypeWrapperConverterRead(); + [Fact] + public void GroupByWrapperWrapperConverter_Works_GroupByWrapper() + { + // Arrange & Act & Assert + TestDynamicTypeWrapperConverterRead(); - // Arrange & Act & Assert - TestDynamicTypeWrapperConverterWrite(); - } + // Arrange & Act & Assert + TestDynamicTypeWrapperConverterWrite(); + } - internal static void TestDynamicTypeWrapperConverterRead() where T : GroupByWrapper - { - // Arrange - JsonSerializerOptions options = new JsonSerializerOptions(); - DynamicTypeWrapperConverter converter = new DynamicTypeWrapperConverter(); - JsonConverter typeConverter = converter.CreateConverter(typeof(T), options) as JsonConverter; + [Fact] + public void NoGroupByAggregationWrapperConverter_Works_NoGroupByAggregationWrapper() + { + // Arrange & Act & Assert + TestDynamicTypeWrapperConverterRead(); - try - { - // Act - ReadOnlySpan jsonReadOnlySpan = Encoding.UTF8.GetBytes("any"); - Utf8JsonReader reader = new Utf8JsonReader(jsonReadOnlySpan); - typeConverter.Read(ref reader, typeof(AggregationWrapper), options); - } - catch (NotImplementedException ex) - { - // Assert - Assert.Equal($"'{typeof(T).Name}' is internal and should never be deserialized into.", ex.Message); - } - } + // Arrange & Act & Assert + TestDynamicTypeWrapperConverterWrite(); + } - internal static void TestDynamicTypeWrapperConverterWrite() where T : GroupByWrapper - { - // Arrange - T wrapper = (T)Activator.CreateInstance(typeof(T)); + [Fact] + public void NoGroupByWrapperWrapperConverter_Works_NoGroupByWrapper() + { + // Arrange & Act & Assert + TestDynamicTypeWrapperConverterRead(); - wrapper.GroupByContainer = new AggregationPropertyContainer() - { - Name = "TestProp", - Value = "TestValue" - }; + // Arrange & Act & Assert + TestDynamicTypeWrapperConverterWrite(); + } - JsonSerializerOptions options = new JsonSerializerOptions(); - DynamicTypeWrapperConverter converter = new DynamicTypeWrapperConverter(); - JsonConverter typeConverter = converter.CreateConverter(typeof(T), options) as JsonConverter; + internal static void TestDynamicTypeWrapperConverterRead() where T : GroupByWrapper + { + // Arrange + JsonSerializerOptions options = new JsonSerializerOptions(); + DynamicTypeWrapperConverter converter = new DynamicTypeWrapperConverter(); + JsonConverter typeConverter = converter.CreateConverter(typeof(T), options) as JsonConverter; + try + { // Act - string json = SerializeUtils.SerializeAsJson(jsonWriter => typeConverter.Write(jsonWriter, wrapper, options)); - + ReadOnlySpan jsonReadOnlySpan = Encoding.UTF8.GetBytes("any"); + Utf8JsonReader reader = new Utf8JsonReader(jsonReadOnlySpan); + typeConverter.Read(ref reader, typeof(AggregationWrapper), options); + } + catch (NotImplementedException ex) + { // Assert - Assert.Equal("{\"TestProp\":\"TestValue\"}", json); + Assert.Equal($"'{typeof(T).Name}' is internal and should never be deserialized into.", ex.Message); } + } - [Fact] - public void ComputeWrapperOfTypeConverter_Works_ComputeWrapper() + internal static void TestDynamicTypeWrapperConverterWrite() where T : GroupByWrapper + { + // Arrange + T wrapper = (T)Activator.CreateInstance(typeof(T)); + + wrapper.GroupByContainer = new AggregationPropertyContainer() { - // Arrange & Act & Assert - TestDynamicTypeWrapperConverterRead>(); + Name = "TestProp", + Value = "TestValue" + }; - // Arrange - GroupByWrapper wrapper = new GroupByWrapper(); - wrapper.GroupByContainer = new AggregationPropertyContainer() - { - Name = "TestProp", - Value = "TestValue" - }; + JsonSerializerOptions options = new JsonSerializerOptions(); + DynamicTypeWrapperConverter converter = new DynamicTypeWrapperConverter(); + JsonConverter typeConverter = converter.CreateConverter(typeof(T), options) as JsonConverter; - ComputeWrapper computeWrapper = new ComputeWrapper - { - Instance = wrapper - }; + // Act + string json = SerializeUtils.SerializeAsJson(jsonWriter => typeConverter.Write(jsonWriter, wrapper, options)); - JsonSerializerOptions options = new JsonSerializerOptions(); - ComputeWrapperConverter converter = new ComputeWrapperConverter(); + // Assert + Assert.Equal("{\"TestProp\":\"TestValue\"}", json); + } - // Act - string json = SerializeUtils.SerializeAsJson(jsonWriter => converter.Write(jsonWriter, computeWrapper, options)); + [Fact] + public void ComputeWrapperOfTypeConverter_Works_ComputeWrapper() + { + // Arrange & Act & Assert + TestDynamicTypeWrapperConverterRead>(); - // Assert - Assert.Equal("{\"TestProp\":\"TestValue\"}", json); - } + // Arrange + GroupByWrapper wrapper = new GroupByWrapper(); + wrapper.GroupByContainer = new AggregationPropertyContainer() + { + Name = "TestProp", + Value = "TestValue" + }; - [Fact] - public void FlatteningWrapperOfTypeConverter_Works_FlatteningWrapper() + ComputeWrapper computeWrapper = new ComputeWrapper { - // Arrange & Act & Assert - TestDynamicTypeWrapperConverterRead>(); + Instance = wrapper + }; + + JsonSerializerOptions options = new JsonSerializerOptions(); + ComputeWrapperConverter converter = new ComputeWrapperConverter(); + + // Act + string json = SerializeUtils.SerializeAsJson(jsonWriter => converter.Write(jsonWriter, computeWrapper, options)); + + // Assert + Assert.Equal("{\"TestProp\":\"TestValue\"}", json); + } - // Arrange - FlatteningWrapper flatteningWrapper = new FlatteningWrapper + [Fact] + public void FlatteningWrapperOfTypeConverter_Works_FlatteningWrapper() + { + // Arrange & Act & Assert + TestDynamicTypeWrapperConverterRead>(); + + // Arrange + FlatteningWrapper flatteningWrapper = new FlatteningWrapper + { + GroupByContainer = new AggregationPropertyContainer() { - GroupByContainer = new AggregationPropertyContainer() - { - Name = "TestProp", - Value = "TestValue" - } - }; + Name = "TestProp", + Value = "TestValue" + } + }; - JsonSerializerOptions options = new JsonSerializerOptions(); - FlatteningWrapperConverter converter = new FlatteningWrapperConverter(); + JsonSerializerOptions options = new JsonSerializerOptions(); + FlatteningWrapperConverter converter = new FlatteningWrapperConverter(); - // Act - string json = SerializeUtils.SerializeAsJson(jsonWriter => converter.Write(jsonWriter, flatteningWrapper, options)); + // Act + string json = SerializeUtils.SerializeAsJson(jsonWriter => converter.Write(jsonWriter, flatteningWrapper, options)); - // Assert - Assert.Equal("{\"TestProp\":\"TestValue\"}", json); - } + // Assert + Assert.Equal("{\"TestProp\":\"TestValue\"}", json); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/DynamicTypeWrapperTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/DynamicTypeWrapperTests.cs index 87e7a0a76..39ea19972 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/DynamicTypeWrapperTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/DynamicTypeWrapperTests.cs @@ -9,87 +9,86 @@ using Microsoft.AspNetCore.OData.Query.Wrapper; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Tests.Query.Wrapper; + +public class DynamicTypeWrapperTests { - public class DynamicTypeWrapperTests + [Fact] + public void CanSetProperty() { - [Fact] - public void CanSetProperty() + var expectedValue = "TestValue"; + var propName = "TestProp"; + var wrapper = new GroupByWrapper(); + wrapper.GroupByContainer = new AggregationPropertyContainer() { - var expectedValue = "TestValue"; - var propName = "TestProp"; - var wrapper = new GroupByWrapper(); - wrapper.GroupByContainer = new AggregationPropertyContainer() - { - Name = propName, - Value = expectedValue - }; + Name = propName, + Value = expectedValue + }; - var actual = wrapper.Values[propName]; + var actual = wrapper.Values[propName]; - Assert.Equal(expectedValue, actual); - } + Assert.Equal(expectedValue, actual); + } - [Fact] - public void CanTryGetProperty() + [Fact] + public void CanTryGetProperty() + { + var expectedValue = "TestValue"; + var propName = "TestProp"; + var wrapper = new GroupByWrapper(); + wrapper.GroupByContainer = new AggregationPropertyContainer() { - var expectedValue = "TestValue"; - var propName = "TestProp"; - var wrapper = new GroupByWrapper(); - wrapper.GroupByContainer = new AggregationPropertyContainer() - { - Name = propName, - Value = expectedValue - }; + Name = propName, + Value = expectedValue + }; - object actual; - Assert.True(wrapper.TryGetPropertyValue(propName, out actual)); + object actual; + Assert.True(wrapper.TryGetPropertyValue(propName, out actual)); - Assert.Equal(expectedValue, actual); - } + Assert.Equal(expectedValue, actual); + } - [Fact] - public void CanEqualWrappers() + [Fact] + public void CanEqualWrappers() + { + var expectedValue = "TestValue"; + var propName = "TestProp"; + var wrapper = new GroupByWrapper(); + wrapper.GroupByContainer = new AggregationPropertyContainer() { - var expectedValue = "TestValue"; - var propName = "TestProp"; - var wrapper = new GroupByWrapper(); - wrapper.GroupByContainer = new AggregationPropertyContainer() - { - Name = propName, - Value = expectedValue - }; + Name = propName, + Value = expectedValue + }; - var wrapper2 = new GroupByWrapper(); - wrapper2.GroupByContainer = new AggregationPropertyContainer() - { - Name = propName, - Value = expectedValue - }; + var wrapper2 = new GroupByWrapper(); + wrapper2.GroupByContainer = new AggregationPropertyContainer() + { + Name = propName, + Value = expectedValue + }; - Assert.Equal(wrapper, wrapper2); - } + Assert.Equal(wrapper, wrapper2); + } - [Fact] - public void GetHashCodeEqualForEqualWrappers() + [Fact] + public void GetHashCodeEqualForEqualWrappers() + { + var expectedValue = "TestValue"; + var propName = "TestProp"; + var wrapper = new GroupByWrapper(); + wrapper.GroupByContainer = new AggregationPropertyContainer() { - var expectedValue = "TestValue"; - var propName = "TestProp"; - var wrapper = new GroupByWrapper(); - wrapper.GroupByContainer = new AggregationPropertyContainer() - { - Name = propName, - Value = expectedValue - }; + Name = propName, + Value = expectedValue + }; - var wrapper2 = new GroupByWrapper(); - wrapper2.GroupByContainer = new AggregationPropertyContainer() - { - Name = propName, - Value = expectedValue - }; + var wrapper2 = new GroupByWrapper(); + wrapper2.GroupByContainer = new AggregationPropertyContainer() + { + Name = propName, + Value = expectedValue + }; - Assert.Equal(wrapper.GetHashCode(), wrapper2.GetHashCode()); - } + Assert.Equal(wrapper.GetHashCode(), wrapper2.GetHashCode()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/MockPropertyContainer.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/MockPropertyContainer.cs index 264e48ab6..50284a2c7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/MockPropertyContainer.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/MockPropertyContainer.cs @@ -8,24 +8,23 @@ using System.Collections.Generic; using Microsoft.AspNetCore.OData.Query.Container; -namespace Microsoft.AspNetCore.OData.Tests.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Tests.Query.Wrapper; + +internal class MockPropertyContainer : PropertyContainer { - internal class MockPropertyContainer : PropertyContainer + public MockPropertyContainer() { - public MockPropertyContainer() - { - Properties = new Dictionary(); - } + Properties = new Dictionary(); + } - public Dictionary Properties { get; private set; } + public Dictionary Properties { get; private set; } - public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, - bool includeAutoSelected) + public override void ToDictionaryCore(Dictionary dictionary, IPropertyMapper propertyMapper, + bool includeAutoSelected) + { + foreach (var kvp in Properties) { - foreach (var kvp in Properties) - { - dictionary.Add(kvp.Key, kvp.Value); - } + dictionary.Add(kvp.Key, kvp.Value); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/SelectExpandWrapperConverterTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/SelectExpandWrapperConverterTests.cs index e2268bca1..84a9bcd13 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/SelectExpandWrapperConverterTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/SelectExpandWrapperConverterTests.cs @@ -15,172 +15,171 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Tests.Query.Wrapper; + +public class SelectExpandWrapperConverterTests { - public class SelectExpandWrapperConverterTests + private static IEdmModel _edmModel = GetEdmModel(); + + [Theory] + [InlineData(typeof(SelectAllAndExpand), true)] + [InlineData(typeof(SelectAll), true)] + [InlineData(typeof(SelectExpandWrapper), true)] + [InlineData(typeof(SelectSomeAndInheritance), true)] + [InlineData(typeof(SelectSome), true)] + [InlineData(typeof(SelectExpandWrapper), false)] + [InlineData(typeof(FlatteningWrapper), false)] + [InlineData(typeof(NoGroupByWrapper), false)] + [InlineData(typeof(object), false)] + [InlineData(null, false)] + public void CanConvertWorksForSelectExpandWrapper(Type type, bool expected) { - private static IEdmModel _edmModel = GetEdmModel(); - - [Theory] - [InlineData(typeof(SelectAllAndExpand), true)] - [InlineData(typeof(SelectAll), true)] - [InlineData(typeof(SelectExpandWrapper), true)] - [InlineData(typeof(SelectSomeAndInheritance), true)] - [InlineData(typeof(SelectSome), true)] - [InlineData(typeof(SelectExpandWrapper), false)] - [InlineData(typeof(FlatteningWrapper), false)] - [InlineData(typeof(NoGroupByWrapper), false)] - [InlineData(typeof(object), false)] - [InlineData(null, false)] - public void CanConvertWorksForSelectExpandWrapper(Type type, bool expected) - { - // Arrange - SelectExpandWrapperConverter converter = new SelectExpandWrapperConverter(); - - // Act & Assert - Assert.Equal(expected, converter.CanConvert(type)); - } + // Arrange + SelectExpandWrapperConverter converter = new SelectExpandWrapperConverter(); - [Theory] - [InlineData(typeof(SelectAllAndExpand), typeof(SelectAllAndExpandConverter))] - [InlineData(typeof(SelectAll), typeof(SelectAllConverter))] - [InlineData(typeof(SelectSomeAndInheritance), typeof(SelectSomeAndInheritanceConverter))] - [InlineData(typeof(SelectSome), typeof(SelectSomeConverter))] - [InlineData(typeof(SelectExpandWrapper), typeof(SelectExpandWrapperConverter))] - [InlineData(typeof(SelectExpandWrapper), null)] - [InlineData(typeof(FlatteningWrapper), null)] - [InlineData(typeof(NoGroupByWrapper), null)] - [InlineData(typeof(object), null)] - [InlineData(null, null)] - public void CreateConverterWorksForSelectExpandWrapper(Type type, Type expected) - { - // Arrange - JsonSerializerOptions options = new JsonSerializerOptions(); - SelectExpandWrapperConverter converter = new SelectExpandWrapperConverter(); + // Act & Assert + Assert.Equal(expected, converter.CanConvert(type)); + } - // Act - JsonConverter typeConverter = converter.CreateConverter(type, options); + [Theory] + [InlineData(typeof(SelectAllAndExpand), typeof(SelectAllAndExpandConverter))] + [InlineData(typeof(SelectAll), typeof(SelectAllConverter))] + [InlineData(typeof(SelectSomeAndInheritance), typeof(SelectSomeAndInheritanceConverter))] + [InlineData(typeof(SelectSome), typeof(SelectSomeConverter))] + [InlineData(typeof(SelectExpandWrapper), typeof(SelectExpandWrapperConverter))] + [InlineData(typeof(SelectExpandWrapper), null)] + [InlineData(typeof(FlatteningWrapper), null)] + [InlineData(typeof(NoGroupByWrapper), null)] + [InlineData(typeof(object), null)] + [InlineData(null, null)] + public void CreateConverterWorksForSelectExpandWrapper(Type type, Type expected) + { + // Arrange + JsonSerializerOptions options = new JsonSerializerOptions(); + SelectExpandWrapperConverter converter = new SelectExpandWrapperConverter(); - // Assert - if (expected == null) - { - Assert.Null(typeConverter); - } - else - { - Assert.Equal(expected, typeConverter.GetType()); - } - } + // Act + JsonConverter typeConverter = converter.CreateConverter(type, options); - [Fact] - public void SelectExpandWrapperConverter_Works_SelectExpandWrapper() + // Assert + if (expected == null) { - // Arrange & Act & Assert - TestSelectExpandWrapperConverterRead>(); - - // Arrange & Act & Assert - TestSelectExpandWrapperConverterWrite>(); + Assert.Null(typeConverter); } - - [Fact] - public void SelectSomeAndInheritanceWrapperConverter_Works_SelectSomeAndInheritance() + else { - // Arrange & Act & Assert - TestSelectExpandWrapperConverterRead>(); - - // Arrange & Act & Assert - TestSelectExpandWrapperConverterWrite>(); + Assert.Equal(expected, typeConverter.GetType()); } + } - [Fact] - public void SelectAllWrapperConverter_Works_SelectAll() - { - // Arrange & Act & Assert - TestSelectExpandWrapperConverterRead>(); + [Fact] + public void SelectExpandWrapperConverter_Works_SelectExpandWrapper() + { + // Arrange & Act & Assert + TestSelectExpandWrapperConverterRead>(); - // Arrange & Act & Assert - TestSelectExpandWrapperConverterWrite>(); - } + // Arrange & Act & Assert + TestSelectExpandWrapperConverterWrite>(); + } - [Fact] - public void SelectAllAndExpandWrapperConverter_Works_SelectAllAndExpand() - { - // Arrange & Act & Assert - TestSelectExpandWrapperConverterRead>(); + [Fact] + public void SelectSomeAndInheritanceWrapperConverter_Works_SelectSomeAndInheritance() + { + // Arrange & Act & Assert + TestSelectExpandWrapperConverterRead>(); - // Arrange & Act & Assert - TestSelectExpandWrapperConverterWrite>(); - } + // Arrange & Act & Assert + TestSelectExpandWrapperConverterWrite>(); + } - [Fact] - public void SelectSomeWrapperConverter_Works_SelectSome() - { - // Arrange & Act & Assert - TestSelectExpandWrapperConverterRead>(); + [Fact] + public void SelectAllWrapperConverter_Works_SelectAll() + { + // Arrange & Act & Assert + TestSelectExpandWrapperConverterRead>(); - // Arrange & Act & Assert - TestSelectExpandWrapperConverterWrite>(); - } + // Arrange & Act & Assert + TestSelectExpandWrapperConverterWrite>(); + } - internal static void TestSelectExpandWrapperConverterRead() where T : SelectExpandWrapper - { - // Arrange - JsonSerializerOptions options = new JsonSerializerOptions(); - SelectExpandWrapperConverter converter = new SelectExpandWrapperConverter(); - JsonConverter typeConverter = converter.CreateConverter(typeof(T), options) as JsonConverter; - - try - { - // Act - ReadOnlySpan jsonReadOnlySpan = Encoding.UTF8.GetBytes("any"); - Utf8JsonReader reader = new Utf8JsonReader(jsonReadOnlySpan); - typeConverter.Read(ref reader, typeof(object), options); - } - catch (NotImplementedException ex) - { - // Assert - Assert.Equal($"'{typeof(T).Name}' is internal and should never be deserialized into.", ex.Message); - } - } + [Fact] + public void SelectAllAndExpandWrapperConverter_Works_SelectAllAndExpand() + { + // Arrange & Act & Assert + TestSelectExpandWrapperConverterRead>(); - internal static void TestSelectExpandWrapperConverterWrite() where T : SelectExpandWrapper - { - // Arrange - T wrapper = (T)Activator.CreateInstance(typeof(T)); - MockPropertyContainer container = new MockPropertyContainer(); + // Arrange & Act & Assert + TestSelectExpandWrapperConverterWrite>(); + } - wrapper.Container = new MockPropertyContainer(); - wrapper.Model = _edmModel; - wrapper.UseInstanceForProperties = true; + [Fact] + public void SelectSomeWrapperConverter_Works_SelectSome() + { + // Arrange & Act & Assert + TestSelectExpandWrapperConverterRead>(); - SelectExpandWrapper selectExpandWrapper = wrapper as SelectExpandWrapper; - Assert.NotNull(selectExpandWrapper); - selectExpandWrapper.Instance = new SelectExpandWrapperEntity - { - Name = "abc" - }; + // Arrange & Act & Assert + TestSelectExpandWrapperConverterWrite>(); + } - JsonSerializerOptions options = new JsonSerializerOptions(); - SelectExpandWrapperConverter converterFactory = new SelectExpandWrapperConverter(); - JsonConverter typeConverter = converterFactory.CreateConverter(typeof(T), options) as JsonConverter; + internal static void TestSelectExpandWrapperConverterRead() where T : SelectExpandWrapper + { + // Arrange + JsonSerializerOptions options = new JsonSerializerOptions(); + SelectExpandWrapperConverter converter = new SelectExpandWrapperConverter(); + JsonConverter typeConverter = converter.CreateConverter(typeof(T), options) as JsonConverter; + try + { // Act - string json = SerializeUtils.SerializeAsJson(jsonWriter => typeConverter.Write(jsonWriter, wrapper, options)); - - // Assert - Assert.Equal("{\"Name\":\"abc\"}", json); + ReadOnlySpan jsonReadOnlySpan = Encoding.UTF8.GetBytes("any"); + Utf8JsonReader reader = new Utf8JsonReader(jsonReadOnlySpan); + typeConverter.Read(ref reader, typeof(object), options); } - - private static IEdmModel GetEdmModel() + catch (NotImplementedException ex) { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntityType(); - return builder.GetEdmModel(); + // Assert + Assert.Equal($"'{typeof(T).Name}' is internal and should never be deserialized into.", ex.Message); } + } - private class SelectExpandWrapperEntity + internal static void TestSelectExpandWrapperConverterWrite() where T : SelectExpandWrapper + { + // Arrange + T wrapper = (T)Activator.CreateInstance(typeof(T)); + MockPropertyContainer container = new MockPropertyContainer(); + + wrapper.Container = new MockPropertyContainer(); + wrapper.Model = _edmModel; + wrapper.UseInstanceForProperties = true; + + SelectExpandWrapper selectExpandWrapper = wrapper as SelectExpandWrapper; + Assert.NotNull(selectExpandWrapper); + selectExpandWrapper.Instance = new SelectExpandWrapperEntity { - public string Name { get; set; } - } + Name = "abc" + }; + + JsonSerializerOptions options = new JsonSerializerOptions(); + SelectExpandWrapperConverter converterFactory = new SelectExpandWrapperConverter(); + JsonConverter typeConverter = converterFactory.CreateConverter(typeof(T), options) as JsonConverter; + + // Act + string json = SerializeUtils.SerializeAsJson(jsonWriter => typeConverter.Write(jsonWriter, wrapper, options)); + + // Assert + Assert.Equal("{\"Name\":\"abc\"}", json); + } + + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntityType(); + return builder.GetEdmModel(); + } + + private class SelectExpandWrapperEntity + { + public string Name { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/SelectExpandWrapperTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/SelectExpandWrapperTest.cs index 3d23261ea..659da6e38 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/SelectExpandWrapperTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Wrapper/SelectExpandWrapperTest.cs @@ -17,345 +17,344 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Query.Wrapper +namespace Microsoft.AspNetCore.OData.Tests.Query.Wrapper; + +public class SelectExpandWrapperTest { - public class SelectExpandWrapperTest + private CustomersModelWithInheritance _model; + + public SelectExpandWrapperTest() { - private CustomersModelWithInheritance _model; + _model = new CustomersModelWithInheritance(); + } - public SelectExpandWrapperTest() - { - _model = new CustomersModelWithInheritance(); - } + [Fact] + public void Property_Instance_RoundTrips() + { + SelectExpandWrapper wrapper = new SelectExpandWrapper(); + ReflectionAssert.Property(wrapper, w => w.Instance, expectedDefaultValue: null, allowNull: true, roundTripTestValue: new TestEntity()); + } - [Fact] - public void Property_Instance_RoundTrips() - { - SelectExpandWrapper wrapper = new SelectExpandWrapper(); - ReflectionAssert.Property(wrapper, w => w.Instance, expectedDefaultValue: null, allowNull: true, roundTripTestValue: new TestEntity()); - } + [Fact] + public void Property_Container_RoundTrips() + { + SelectExpandWrapper wrapper = new SelectExpandWrapper(); - [Fact] - public void Property_Container_RoundTrips() - { - SelectExpandWrapper wrapper = new SelectExpandWrapper(); + ReflectionAssert.Property( + wrapper, w => w.Container, expectedDefaultValue: null, allowNull: true, roundTripTestValue: new MockPropertyContainer()); + } - ReflectionAssert.Property( - wrapper, w => w.Container, expectedDefaultValue: null, allowNull: true, roundTripTestValue: new MockPropertyContainer()); - } + [Fact] + public void GetEdmType_Returns_InstanceType() + { + _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); + _model.Model.SetAnnotationValue(_model.SpecialCustomer, new ClrTypeAnnotation(typeof(DerivedEntity))); + SelectExpandWrapper wrapper = new SelectExpandWrapper { Model = _model.Model }; + wrapper.Instance = new DerivedEntity(); - [Fact] - public void GetEdmType_Returns_InstanceType() - { - _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); - _model.Model.SetAnnotationValue(_model.SpecialCustomer, new ClrTypeAnnotation(typeof(DerivedEntity))); - SelectExpandWrapper wrapper = new SelectExpandWrapper { Model = _model.Model }; - wrapper.Instance = new DerivedEntity(); + IEdmTypeReference edmType = wrapper.GetEdmType(); - IEdmTypeReference edmType = wrapper.GetEdmType(); + Assert.Same(_model.SpecialCustomer, edmType.Definition); + } - Assert.Same(_model.SpecialCustomer, edmType.Definition); - } + [Fact] + public void GetEdmType_Returns_ElementTypeIfInstanceIsNull() + { + _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); + _model.Model.SetAnnotationValue(_model.SpecialCustomer, new ClrTypeAnnotation(typeof(DerivedEntity))); + SelectExpandWrapper wrapper = new SelectExpandWrapper { Model = _model.Model }; - [Fact] - public void GetEdmType_Returns_ElementTypeIfInstanceIsNull() - { - _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); - _model.Model.SetAnnotationValue(_model.SpecialCustomer, new ClrTypeAnnotation(typeof(DerivedEntity))); - SelectExpandWrapper wrapper = new SelectExpandWrapper { Model = _model.Model }; + IEdmTypeReference edmType = wrapper.GetEdmType(); - IEdmTypeReference edmType = wrapper.GetEdmType(); + Assert.Same(_model.Customer, edmType.Definition); + } - Assert.Same(_model.Customer, edmType.Definition); - } + [Fact] + public void TryGetValue_ReturnsValueFromPropertyContainer_IfPresent() + { + object expectedPropertyValue = new object(); + MockPropertyContainer container = new MockPropertyContainer(); + container.Properties.Add("SampleProperty", expectedPropertyValue); + SelectExpandWrapper wrapper = new SelectExpandWrapper + { + Model = _model.Model, + Container = container + }; + wrapper.Instance = new TestEntity(); - [Fact] - public void TryGetValue_ReturnsValueFromPropertyContainer_IfPresent() - { - object expectedPropertyValue = new object(); - MockPropertyContainer container = new MockPropertyContainer(); - container.Properties.Add("SampleProperty", expectedPropertyValue); - SelectExpandWrapper wrapper = new SelectExpandWrapper - { - Model = _model.Model, - Container = container - }; - wrapper.Instance = new TestEntity(); - - object value; - bool result = wrapper.TryGetPropertyValue("SampleProperty", out value); - - Assert.True(result); - Assert.Same(expectedPropertyValue, value); - } - - [Fact] - public void TryGetValue_ReturnsValueFromInstance_IfNotPresentInContainer() - { - object expectedPropertyValue = new object(); - MockPropertyContainer container = new MockPropertyContainer(); - SelectExpandWrapper wrapper = new SelectExpandWrapper - { - Model = _model.Model, - Container = container - }; - wrapper.Instance = new TestEntity { SampleProperty = expectedPropertyValue }; - wrapper.UseInstanceForProperties = true; - - object value; - bool result = wrapper.TryGetPropertyValue("SampleProperty", out value); - - Assert.True(result); - Assert.Same(expectedPropertyValue, value); - } - - [Fact] - public void TryGetValue_ReturnsValueFromInstance_IfContainerIsNull() - { - object expectedPropertyValue = new object(); - SelectExpandWrapper wrapper = new SelectExpandWrapper { Model = _model.Model }; - wrapper.Instance = new TestEntity { SampleProperty = expectedPropertyValue }; - wrapper.UseInstanceForProperties = true; + object value; + bool result = wrapper.TryGetPropertyValue("SampleProperty", out value); - object value; - bool result = wrapper.TryGetPropertyValue("SampleProperty", out value); + Assert.True(result); + Assert.Same(expectedPropertyValue, value); + } - Assert.True(result); - Assert.Same(expectedPropertyValue, value); - } + [Fact] + public void TryGetValue_ReturnsValueFromInstance_IfNotPresentInContainer() + { + object expectedPropertyValue = new object(); + MockPropertyContainer container = new MockPropertyContainer(); + SelectExpandWrapper wrapper = new SelectExpandWrapper + { + Model = _model.Model, + Container = container + }; + wrapper.Instance = new TestEntity { SampleProperty = expectedPropertyValue }; + wrapper.UseInstanceForProperties = true; - [Fact] - public void TryGetValue_PropertyAliased_IfAnnotationSet() - { - // Arrange - _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(TestEntityWithAlias))); - _model.Model.SetAnnotationValue( - _model.CustomerName, - new ClrPropertyInfoAnnotation(typeof(TestEntityWithAlias).GetProperty("SampleProperty"))); - object expectedPropertyValue = new object(); - SelectExpandWrapper wrapper = new SelectExpandWrapper { Model = _model.Model }; - wrapper.Instance = new TestEntityWithAlias { SampleProperty = expectedPropertyValue }; - wrapper.UseInstanceForProperties = true; - - // Act - object value; - bool result = wrapper.TryGetPropertyValue("Name", out value); - - // Assert - Assert.True(result); - Assert.Same(expectedPropertyValue, value); - } - - [Fact] - public void TryGetValue_ReturnsFalse_IfContainerAndInstanceAreNull() - { - SelectExpandWrapper wrapper = new SelectExpandWrapper { Model = _model.Model }; + object value; + bool result = wrapper.TryGetPropertyValue("SampleProperty", out value); - object value; - bool result = wrapper.TryGetPropertyValue("SampleProperty", out value); + Assert.True(result); + Assert.Same(expectedPropertyValue, value); + } - Assert.False(result); - } + [Fact] + public void TryGetValue_ReturnsValueFromInstance_IfContainerIsNull() + { + object expectedPropertyValue = new object(); + SelectExpandWrapper wrapper = new SelectExpandWrapper { Model = _model.Model }; + wrapper.Instance = new TestEntity { SampleProperty = expectedPropertyValue }; + wrapper.UseInstanceForProperties = true; - [Fact] - public void TryGetValue_ReturnsFalse_IfPropertyNotPresentInElement() - { - SelectExpandWrapper wrapper = new SelectExpandWrapper { Model = _model.Model }; + object value; + bool result = wrapper.TryGetPropertyValue("SampleProperty", out value); - object value; - bool result = wrapper.TryGetPropertyValue("SampleNotPresentProperty", out value); + Assert.True(result); + Assert.Same(expectedPropertyValue, value); + } - Assert.False(result); - } + [Fact] + public void TryGetValue_PropertyAliased_IfAnnotationSet() + { + // Arrange + _model.Model.SetAnnotationValue(_model.Customer, new ClrTypeAnnotation(typeof(TestEntityWithAlias))); + _model.Model.SetAnnotationValue( + _model.CustomerName, + new ClrPropertyInfoAnnotation(typeof(TestEntityWithAlias).GetProperty("SampleProperty"))); + object expectedPropertyValue = new object(); + SelectExpandWrapper wrapper = new SelectExpandWrapper { Model = _model.Model }; + wrapper.Instance = new TestEntityWithAlias { SampleProperty = expectedPropertyValue }; + wrapper.UseInstanceForProperties = true; + + // Act + object value; + bool result = wrapper.TryGetPropertyValue("Name", out value); + + // Assert + Assert.True(result); + Assert.Same(expectedPropertyValue, value); + } - [Fact] - public void ToDictionary_ContainsAllStructuralProperties_IfInstanceIsNotNull() - { - // Arrange - EdmModel model = new EdmModel(); - EdmEntityType entityType = new EdmEntityType("NS", "Name"); - model.AddElement(entityType); - model.SetAnnotationValue(entityType, new ClrTypeAnnotation(typeof(TestEntity))); - entityType.AddStructuralProperty("SampleProperty", EdmPrimitiveTypeKind.Int32); - IEdmTypeReference edmType = new EdmEntityTypeReference(entityType, isNullable: false); - SelectExpandWrapper testWrapper = new SelectExpandWrapper - { - Instance = new TestEntity { SampleProperty = 42 }, - Model = model, - UseInstanceForProperties = true, - }; + [Fact] + public void TryGetValue_ReturnsFalse_IfContainerAndInstanceAreNull() + { + SelectExpandWrapper wrapper = new SelectExpandWrapper { Model = _model.Model }; - // Act - var result = testWrapper.ToDictionary(); + object value; + bool result = wrapper.TryGetPropertyValue("SampleProperty", out value); - // Assert - Assert.Equal(42, result["SampleProperty"]); - } + Assert.False(result); + } - [Fact] - public void ToDictionary_ContainsAllProperties_FromContainer() - { - // Arrange - EdmModel model = new EdmModel(); - EdmEntityType entityType = new EdmEntityType("NS", "Name"); - model.AddElement(entityType); - model.SetAnnotationValue(entityType, new ClrTypeAnnotation(typeof(TestEntity))); - entityType.AddStructuralProperty("SampleProperty", EdmPrimitiveTypeKind.Int32); - MockPropertyContainer container = new MockPropertyContainer(); - container.Properties.Add("Property", 42); - SelectExpandWrapper wrapper = new SelectExpandWrapper - { - Container = container, - Model = model - }; + [Fact] + public void TryGetValue_ReturnsFalse_IfPropertyNotPresentInElement() + { + SelectExpandWrapper wrapper = new SelectExpandWrapper { Model = _model.Model }; - // Act - var result = wrapper.ToDictionary(); + object value; + bool result = wrapper.TryGetPropertyValue("SampleNotPresentProperty", out value); - // Assert - Assert.Equal(42, result["Property"]); - } + Assert.False(result); + } - [Fact] - public void ToDictionary_Throws_IfMapperProviderIsNull() + [Fact] + public void ToDictionary_ContainsAllStructuralProperties_IfInstanceIsNotNull() + { + // Arrange + EdmModel model = new EdmModel(); + EdmEntityType entityType = new EdmEntityType("NS", "Name"); + model.AddElement(entityType); + model.SetAnnotationValue(entityType, new ClrTypeAnnotation(typeof(TestEntity))); + entityType.AddStructuralProperty("SampleProperty", EdmPrimitiveTypeKind.Int32); + IEdmTypeReference edmType = new EdmEntityTypeReference(entityType, isNullable: false); + SelectExpandWrapper testWrapper = new SelectExpandWrapper { - // Arrange - SelectExpandWrapper wrapper = new SelectExpandWrapper(); + Instance = new TestEntity { SampleProperty = 42 }, + Model = model, + UseInstanceForProperties = true, + }; + + // Act + var result = testWrapper.ToDictionary(); - // Act & Assert - ExceptionAssert.Throws(() => wrapper.ToDictionary(mapperProvider: null)); - } + // Assert + Assert.Equal(42, result["SampleProperty"]); + } - [Fact] - public void ToDictionary_Throws_IfMapperProvider_ReturnsNullPropertyMapper() + [Fact] + public void ToDictionary_ContainsAllProperties_FromContainer() + { + // Arrange + EdmModel model = new EdmModel(); + EdmEntityType entityType = new EdmEntityType("NS", "Name"); + model.AddElement(entityType); + model.SetAnnotationValue(entityType, new ClrTypeAnnotation(typeof(TestEntity))); + entityType.AddStructuralProperty("SampleProperty", EdmPrimitiveTypeKind.Int32); + MockPropertyContainer container = new MockPropertyContainer(); + container.Properties.Add("Property", 42); + SelectExpandWrapper wrapper = new SelectExpandWrapper { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "Name"); - entityType.AddStructuralProperty("SampleProperty", EdmPrimitiveTypeKind.Int32); + Container = container, + Model = model + }; - EdmModel model = new EdmModel(); - model.AddElement(entityType); - model.SetAnnotationValue(entityType, new ClrTypeAnnotation(typeof(TestEntity))); - IEdmTypeReference edmType = new EdmEntityTypeReference(entityType, isNullable: false); + // Act + var result = wrapper.ToDictionary(); - SelectExpandWrapper wrapper = new SelectExpandWrapper - { - Instance = new TestEntity { SampleProperty = 42 }, - Model = model - }; - - Func mapperProvider = - (IEdmModel m, IEdmStructuredType t) => null; + // Assert + Assert.Equal(42, result["Property"]); + } - // Act & Assert - ExceptionAssert.Throws(() => - wrapper.ToDictionary(mapperProvider: mapperProvider), - "The mapper provider must return a valid 'Microsoft.AspNetCore.OData.Query.Container.IPropertyMapper' instance for the given 'NS.Name' IEdmType."); - } + [Fact] + public void ToDictionary_Throws_IfMapperProviderIsNull() + { + // Arrange + SelectExpandWrapper wrapper = new SelectExpandWrapper(); - [Theory] - [InlineData("")] - public void ToDictionary_Throws_IfMappingIsEmpty_ForAGivenProperty(string propertyMapping) - { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "Name"); - entityType.AddStructuralProperty("SampleProperty", EdmPrimitiveTypeKind.Int32); - - EdmModel model = new EdmModel(); - model.AddElement(entityType); - model.SetAnnotationValue(entityType, new ClrTypeAnnotation(typeof(TestEntity))); - IEdmTypeReference edmType = new EdmEntityTypeReference(entityType, isNullable: false); - - SelectExpandWrapper testWrapper = new SelectExpandWrapper - { - Instance = new TestEntity { SampleProperty = 42 }, - Model = model, - UseInstanceForProperties = true, - }; + // Act & Assert + ExceptionAssert.Throws(() => wrapper.ToDictionary(mapperProvider: null)); + } - Mock mapperMock = new Mock(); - mapperMock.Setup(m => m.MapProperty("SampleProperty")).Returns(propertyMapping); - Func mapperProvider = - (IEdmModel m, IEdmStructuredType t) => mapperMock.Object; + [Fact] + public void ToDictionary_Throws_IfMapperProvider_ReturnsNullPropertyMapper() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "Name"); + entityType.AddStructuralProperty("SampleProperty", EdmPrimitiveTypeKind.Int32); - // Act & Assert - ExceptionAssert.Throws(() => - testWrapper.ToDictionary(mapperProvider), - "The key mapping for the property 'SampleProperty' can't be empty."); - } + EdmModel model = new EdmModel(); + model.AddElement(entityType); + model.SetAnnotationValue(entityType, new ClrTypeAnnotation(typeof(TestEntity))); + IEdmTypeReference edmType = new EdmEntityTypeReference(entityType, isNullable: false); - [Fact] - public void ToDictionary_AppliesMappingToAllProperties_IfInstanceIsNotNull_IfPropertyIsRenamed() + SelectExpandWrapper wrapper = new SelectExpandWrapper { - // Arrange - var testWrapper = GetSamplePropertyTestWrapper(); - var mapperProvider = GetMapperProvider("SampleProperty", "Sample"); + Instance = new TestEntity { SampleProperty = 42 }, + Model = model + }; - // Act - var result = testWrapper.ToDictionary(mapperProvider); + Func mapperProvider = + (IEdmModel m, IEdmStructuredType t) => null; - // Assert - Assert.Equal(42, result["Sample"]); - } + // Act & Assert + ExceptionAssert.Throws(() => + wrapper.ToDictionary(mapperProvider: mapperProvider), + "The mapper provider must return a valid 'Microsoft.AspNetCore.OData.Query.Container.IPropertyMapper' instance for the given 'NS.Name' IEdmType."); + } - [Fact] - public void ToDictionary_AppliesMappingToAllProperties_IfInstanceIsNotNull_IfPropertyIsIgnored() + [Theory] + [InlineData("")] + public void ToDictionary_Throws_IfMappingIsEmpty_ForAGivenProperty(string propertyMapping) + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "Name"); + entityType.AddStructuralProperty("SampleProperty", EdmPrimitiveTypeKind.Int32); + + EdmModel model = new EdmModel(); + model.AddElement(entityType); + model.SetAnnotationValue(entityType, new ClrTypeAnnotation(typeof(TestEntity))); + IEdmTypeReference edmType = new EdmEntityTypeReference(entityType, isNullable: false); + + SelectExpandWrapper testWrapper = new SelectExpandWrapper { - // Arrange - var testWrapper = GetSamplePropertyTestWrapper(); - var mapperProvider = GetMapperProvider("SampleProperty", null); + Instance = new TestEntity { SampleProperty = 42 }, + Model = model, + UseInstanceForProperties = true, + }; + + Mock mapperMock = new Mock(); + mapperMock.Setup(m => m.MapProperty("SampleProperty")).Returns(propertyMapping); + Func mapperProvider = + (IEdmModel m, IEdmStructuredType t) => mapperMock.Object; + + // Act & Assert + ExceptionAssert.Throws(() => + testWrapper.ToDictionary(mapperProvider), + "The key mapping for the property 'SampleProperty' can't be empty."); + } + + [Fact] + public void ToDictionary_AppliesMappingToAllProperties_IfInstanceIsNotNull_IfPropertyIsRenamed() + { + // Arrange + var testWrapper = GetSamplePropertyTestWrapper(); + var mapperProvider = GetMapperProvider("SampleProperty", "Sample"); - // Act - var result = testWrapper.ToDictionary(mapperProvider); + // Act + var result = testWrapper.ToDictionary(mapperProvider); - // Assert - Assert.False(result.ContainsKey("SampleProperty")); - } + // Assert + Assert.Equal(42, result["Sample"]); + } - private Func GetMapperProvider(string mapFrom, string mapTo) - { - Mock mapperMock = new Mock(); - mapperMock.Setup(m => m.MapProperty(mapFrom)).Returns(mapTo); - Func mapperProvider = - (IEdmModel m, IEdmStructuredType t) => mapperMock.Object; + [Fact] + public void ToDictionary_AppliesMappingToAllProperties_IfInstanceIsNotNull_IfPropertyIsIgnored() + { + // Arrange + var testWrapper = GetSamplePropertyTestWrapper(); + var mapperProvider = GetMapperProvider("SampleProperty", null); - return mapperProvider; - } + // Act + var result = testWrapper.ToDictionary(mapperProvider); - private SelectExpandWrapper GetSamplePropertyTestWrapper() - { - EdmEntityType entityType = new EdmEntityType("NS", "Name"); - entityType.AddStructuralProperty("SampleProperty", EdmPrimitiveTypeKind.Int32); + // Assert + Assert.False(result.ContainsKey("SampleProperty")); + } - EdmModel model = new EdmModel(); - model.AddElement(entityType); - model.SetAnnotationValue(entityType, new ClrTypeAnnotation(typeof(TestEntity))); - IEdmTypeReference edmType = new EdmEntityTypeReference(entityType, isNullable: false); + private Func GetMapperProvider(string mapFrom, string mapTo) + { + Mock mapperMock = new Mock(); + mapperMock.Setup(m => m.MapProperty(mapFrom)).Returns(mapTo); + Func mapperProvider = + (IEdmModel m, IEdmStructuredType t) => mapperMock.Object; - SelectExpandWrapper testWrapper = new SelectExpandWrapper - { - Instance = new TestEntity { SampleProperty = 42 }, - Model = model, - UseInstanceForProperties = true, - }; + return mapperProvider; + } + + private SelectExpandWrapper GetSamplePropertyTestWrapper() + { + EdmEntityType entityType = new EdmEntityType("NS", "Name"); + entityType.AddStructuralProperty("SampleProperty", EdmPrimitiveTypeKind.Int32); - return testWrapper; - } + EdmModel model = new EdmModel(); + model.AddElement(entityType); + model.SetAnnotationValue(entityType, new ClrTypeAnnotation(typeof(TestEntity))); + IEdmTypeReference edmType = new EdmEntityTypeReference(entityType, isNullable: false); - private class TestEntity + SelectExpandWrapper testWrapper = new SelectExpandWrapper { - public object SampleProperty { get; set; } - } + Instance = new TestEntity { SampleProperty = 42 }, + Model = model, + UseInstanceForProperties = true, + }; - private class DerivedEntity : TestEntity - { - } + return testWrapper; + } - [DataContract(Namespace = "NS", Name = "Customer")] - private class TestEntityWithAlias - { - [DataMember(Name = "Name")] - public object SampleProperty { get; set; } - } + private class TestEntity + { + public object SampleProperty { get; set; } + } + + private class DerivedEntity : TestEntity + { + } + + [DataContract(Namespace = "NS", Name = "Customer")] + private class TestEntityWithAlias + { + [DataMember(Name = "Name")] + public object SampleProperty { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Results/CreatedODataResultTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Results/CreatedODataResultTest.cs index 0a59ff0fc..501c0b026 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Results/CreatedODataResultTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Results/CreatedODataResultTest.cs @@ -24,302 +24,301 @@ using Xunit; using NavigationSourceLinkBuilderAnnotation = Microsoft.AspNetCore.OData.Edm.NavigationSourceLinkBuilderAnnotation; -namespace Microsoft.AspNetCore.OData.Tests.Results +namespace Microsoft.AspNetCore.OData.Tests.Results; + +public class CreatedODataResultTest { - public class CreatedODataResultTest + private readonly TestEntity _entity = new TestEntity(); + + [Fact] + public void Ctor_ControllerDependency_ThrowsArgumentNull_Entity() { - private readonly TestEntity _entity = new TestEntity(); + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new CreatedODataResult(entity: null), "entity"); + } - [Fact] - public void Ctor_ControllerDependency_ThrowsArgumentNull_Entity() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new CreatedODataResult(entity: null), "entity"); - } + [Fact] + public void GetEntity_ReturnsCorrect() + { + // Arrange + Mock mock = new Mock(); + CreatedODataResult result = + new CreatedODataResult(mock.Object); - [Fact] - public void GetEntity_ReturnsCorrect() - { - // Arrange - Mock mock = new Mock(); - CreatedODataResult result = - new CreatedODataResult(mock.Object); + // Act & Assert + Assert.Same(mock.Object, result.Entity); + } - // Act & Assert - Assert.Same(mock.Object, result.Entity); - } + [Fact] + public void GetInnerActionResult_ReturnsNegotiatedContentResult_IfRequestHasNoPreferenceHeader() + { + // Arrange + var request = CreateRequest(); + var createdODataResult = GetCreatedODataResult(_entity, request); - [Fact] - public void GetInnerActionResult_ReturnsNegotiatedContentResult_IfRequestHasNoPreferenceHeader() - { - // Arrange - var request = CreateRequest(); - var createdODataResult = GetCreatedODataResult(_entity, request); + // Act + var result = createdODataResult.GetInnerActionResult(request); - // Act - var result = createdODataResult.GetInnerActionResult(request); + // Assert + ObjectResult objectResult = Assert.IsType(result); + Assert.Same(typeof(TestEntity), objectResult.Value.GetType()); + Assert.Equal(HttpStatusCode.Created, (HttpStatusCode)objectResult.StatusCode); + } - // Assert - ObjectResult objectResult = Assert.IsType(result); - Assert.Same(typeof(TestEntity), objectResult.Value.GetType()); - Assert.Equal(HttpStatusCode.Created, (HttpStatusCode)objectResult.StatusCode); - } + [Fact] + public void GetInnerActionResult_ReturnsNoContentStatusCodeResult_IfRequestAsksForNoContent() + { + // Arrange + var request = CreateRequest("return=minimal"); + var createdODataResult = GetCreatedODataResult(_entity, request); - [Fact] - public void GetInnerActionResult_ReturnsNoContentStatusCodeResult_IfRequestAsksForNoContent() - { - // Arrange - var request = CreateRequest("return=minimal"); - var createdODataResult = GetCreatedODataResult(_entity, request); + // Act + var result = createdODataResult.GetInnerActionResult(request); - // Act - var result = createdODataResult.GetInnerActionResult(request); + // Assert + StatusCodeResult statusCodeResult = Assert.IsType(result); + Assert.Equal(HttpStatusCode.NoContent, (HttpStatusCode)statusCodeResult.StatusCode); + } - // Assert - StatusCodeResult statusCodeResult = Assert.IsType(result); - Assert.Equal(HttpStatusCode.NoContent, (HttpStatusCode)statusCodeResult.StatusCode); - } + [Fact] + public void GetInnerActionResult_ReturnsNegotiatedContentResult_IfRequestAsksForContent() + { + // Arrange + var request = CreateRequest("return=representation"); + var createdODataResult = GetCreatedODataResult(_entity, request); - [Fact] - public void GetInnerActionResult_ReturnsNegotiatedContentResult_IfRequestAsksForContent() - { - // Arrange - var request = CreateRequest("return=representation"); - var createdODataResult = GetCreatedODataResult(_entity, request); + // Act + var result = createdODataResult.GetInnerActionResult(request); - // Act - var result = createdODataResult.GetInnerActionResult(request); + // Assert + ObjectResult objectResult = Assert.IsType(result); + Assert.Same(typeof(TestEntity), objectResult.Value.GetType()); + Assert.Equal(HttpStatusCode.Created, (HttpStatusCode)objectResult.StatusCode); + } - // Assert - ObjectResult objectResult = Assert.IsType(result); - Assert.Same(typeof(TestEntity), objectResult.Value.GetType()); - Assert.Equal(HttpStatusCode.Created, (HttpStatusCode)objectResult.StatusCode); - } + [Fact] + public void GenerateLocationHeader_ThrowsODataPathMissing_IfRequestDoesNotHaveODataPath() + { + var request = RequestFactory.Create(EdmCoreModel.Instance); + var createdODataResult = GetCreatedODataResult(_entity, request); - [Fact] - public void GenerateLocationHeader_ThrowsODataPathMissing_IfRequestDoesNotHaveODataPath() - { - var request = RequestFactory.Create(EdmCoreModel.Instance); - var createdODataResult = GetCreatedODataResult(_entity, request); + // Act & Assert + ExceptionAssert.Throws(() => createdODataResult.GenerateLocationHeader(request), + "The operation cannot be completed because no ODataPath is available for the request."); + } - // Act & Assert - ExceptionAssert.Throws(() => createdODataResult.GenerateLocationHeader(request), - "The operation cannot be completed because no ODataPath is available for the request."); - } + [Fact] + public void GenerateLocationHeader_ThrowsEntitySetMissingDuringSerialization_IfODataPathEntitySetIsNull() + { + // Arrange + ODataPath path = new ODataPath(); + var request = RequestFactory.Create(EdmCoreModel.Instance, path: path); + var createdODataResult = GetCreatedODataResult(_entity, request); + + // Act & Assert + ExceptionAssert.Throws(() => createdODataResult.GenerateLocationHeader(request), + "The related entity set or singleton cannot be found from the OData path. The related entity set or singleton is required to serialize the payload."); + } - [Fact] - public void GenerateLocationHeader_ThrowsEntitySetMissingDuringSerialization_IfODataPathEntitySetIsNull() - { - // Arrange - ODataPath path = new ODataPath(); - var request = RequestFactory.Create(EdmCoreModel.Instance, path: path); - var createdODataResult = GetCreatedODataResult(_entity, request); - - // Act & Assert - ExceptionAssert.Throws(() => createdODataResult.GenerateLocationHeader(request), - "The related entity set or singleton cannot be found from the OData path. The related entity set or singleton is required to serialize the payload."); - } + [Fact] + public void GenerateLocationHeader_ThrowsEntityTypeNotInModel_IfContentTypeIsNotThereInModel() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); + var request = RequestFactory.Create(opt => opt.AddRouteComponents(model.Model)); + request.Configure("", model.Model, path); + var createdODataResult = GetCreatedODataResult(_entity, request); + + // Act & Assert + ExceptionAssert.Throws(() => createdODataResult.GenerateLocationHeader(request), + "Cannot find the resource type 'Microsoft.AspNetCore.OData.Tests.Results.CreatedODataResultTest+TestEntity' in the model."); + } - [Fact] - public void GenerateLocationHeader_ThrowsEntityTypeNotInModel_IfContentTypeIsNotThereInModel() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); - var request = RequestFactory.Create(opt => opt.AddRouteComponents(model.Model)); - request.Configure("", model.Model, path); - var createdODataResult = GetCreatedODataResult(_entity, request); - - // Act & Assert - ExceptionAssert.Throws(() => createdODataResult.GenerateLocationHeader(request), - "Cannot find the resource type 'Microsoft.AspNetCore.OData.Tests.Results.CreatedODataResultTest+TestEntity' in the model."); - } + [Fact] + public void GenerateLocationHeader_ThrowsTypeMustBeEntity_IfMappingTypeIsNotEntity() + { + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); + var request = RequestFactory.Create(model.Model, path: path); + model.Model.SetAnnotationValue(model.Address, new ClrTypeAnnotation(typeof(TestEntity))); + var createdODataResult = GetCreatedODataResult(_entity, request); + + // Act & Assert + ExceptionAssert.Throws(() => createdODataResult.GenerateLocationHeader(request), + "NS.Address is not an entity type. Only entity types are supported."); + } - [Fact] - public void GenerateLocationHeader_ThrowsTypeMustBeEntity_IfMappingTypeIsNotEntity() - { - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); - var request = RequestFactory.Create(model.Model, path: path); - model.Model.SetAnnotationValue(model.Address, new ClrTypeAnnotation(typeof(TestEntity))); - var createdODataResult = GetCreatedODataResult(_entity, request); - - // Act & Assert - ExceptionAssert.Throws(() => createdODataResult.GenerateLocationHeader(request), - "NS.Address is not an entity type. Only entity types are supported."); - } + [Fact] + public void GenerateLocationHeader_UsesEntitySetLinkBuilder_ToGenerateLocationHeader() + { + // Arrange + Uri editLink = new Uri("http://id-link"); + Mock linkBuilder = new Mock(); + linkBuilder.CallBase = true; + linkBuilder.Setup( + b => b.BuildEditLink(It.IsAny(), ODataMetadataLevel.Full, null)) + .Returns(editLink); + + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); + model.Model.SetNavigationSourceLinkBuilder(model.Customers, linkBuilder.Object); + ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); + var request = RequestFactory.Create(model.Model, path: path); + var createdODataResult = GetCreatedODataResult(_entity, request); + + // Act + var locationHeader = createdODataResult.GenerateLocationHeader(request); + + // Assert + Assert.Same(editLink, locationHeader); + } - [Fact] - public void GenerateLocationHeader_UsesEntitySetLinkBuilder_ToGenerateLocationHeader() - { - // Arrange - Uri editLink = new Uri("http://id-link"); - Mock linkBuilder = new Mock(); - linkBuilder.CallBase = true; - linkBuilder.Setup( - b => b.BuildEditLink(It.IsAny(), ODataMetadataLevel.Full, null)) - .Returns(editLink); - - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); - model.Model.SetNavigationSourceLinkBuilder(model.Customers, linkBuilder.Object); - ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); - var request = RequestFactory.Create(model.Model, path: path); - var createdODataResult = GetCreatedODataResult(_entity, request); - - // Act - var locationHeader = createdODataResult.GenerateLocationHeader(request); - - // Assert - Assert.Same(editLink, locationHeader); - } + [Fact] + public void GenerateLocationHeader_ForContainment() + { + // Arrange + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.OrderLine, new ClrTypeAnnotation(typeof(OrderLine))); + ODataPath path = new ODataUriParser(model.Model, + new Uri("http://localhost/"), + new Uri("MyOrders(1)/OrderLines", UriKind.Relative)).ParsePath(); + + var request = RequestFactory.Create(opt => opt.AddRouteComponents(model.Model)); + request.Configure("", model.Model, path); + var orderLine = new OrderLine { ID = 2 }; + var createdODataResult = GetCreatedODataResult(orderLine, request); + + // Act + var locationHeader = createdODataResult.GenerateLocationHeader(request); + + // Assert + Assert.Equal("http://localhost/MyOrders(1)/OrderLines(2)", locationHeader.ToString()); + } - [Fact] - public void GenerateLocationHeader_ForContainment() - { - // Arrange - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.OrderLine, new ClrTypeAnnotation(typeof(OrderLine))); - ODataPath path = new ODataUriParser(model.Model, - new Uri("http://localhost/"), - new Uri("MyOrders(1)/OrderLines", UriKind.Relative)).ParsePath(); - - var request = RequestFactory.Create(opt => opt.AddRouteComponents(model.Model)); - request.Configure("", model.Model, path); - var orderLine = new OrderLine { ID = 2 }; - var createdODataResult = GetCreatedODataResult(orderLine, request); - - // Act - var locationHeader = createdODataResult.GenerateLocationHeader(request); - - // Assert - Assert.Equal("http://localhost/MyOrders(1)/OrderLines(2)", locationHeader.ToString()); - } + [Fact] + public void GenerateLocationHeader_ThrowsEditLinkNullForLocationHeader_IfEntitySetLinkBuilderReturnsNull() + { + // Arrange + Mock linkBuilder = new Mock(); + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); + model.Model.SetNavigationSourceLinkBuilder(model.Customers, linkBuilder.Object); + ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); + var request = RequestFactory.Create(model.Model, path: path); + var createdODataResult = GetCreatedODataResult(_entity, request); + + // Act + ExceptionAssert.Throws(() => createdODataResult.GenerateLocationHeader(request), + "The edit link builder for the entity set 'Customers' returned null. An edit link is required for the location header."); + } - [Fact] - public void GenerateLocationHeader_ThrowsEditLinkNullForLocationHeader_IfEntitySetLinkBuilderReturnsNull() - { - // Arrange - Mock linkBuilder = new Mock(); - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); - model.Model.SetNavigationSourceLinkBuilder(model.Customers, linkBuilder.Object); - ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); - var request = RequestFactory.Create(model.Model, path: path); - var createdODataResult = GetCreatedODataResult(_entity, request); - - // Act - ExceptionAssert.Throws(() => createdODataResult.GenerateLocationHeader(request), - "The edit link builder for the entity set 'Customers' returned null. An edit link is required for the location header."); - } + [Fact] + public void Property_LocationHeader_IsEvaluatedLazily() + { + // Arrange + Uri editLink = new Uri("http://edit-link"); + Mock linkBuilder = new Mock(); + linkBuilder.Setup(b => b.BuildEditLink(It.IsAny(), ODataMetadataLevel.Full, null)) + .Returns(editLink); + + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); + model.Model.SetNavigationSourceLinkBuilder(model.Customers, linkBuilder.Object); + ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); + var request = RequestFactory.Create(model.Model, path: path); + TestController controller = CreateController(request); + var createdODataResult = GetCreatedODataResult(_entity, request, controller); + + // Act + Uri locationHeader = createdODataResult.GenerateLocationHeader(request); + + // Assert + Assert.Same(editLink, locationHeader); + } - [Fact] - public void Property_LocationHeader_IsEvaluatedLazily() - { - // Arrange - Uri editLink = new Uri("http://edit-link"); - Mock linkBuilder = new Mock(); - linkBuilder.Setup(b => b.BuildEditLink(It.IsAny(), ODataMetadataLevel.Full, null)) - .Returns(editLink); - - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); - model.Model.SetNavigationSourceLinkBuilder(model.Customers, linkBuilder.Object); - ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); - var request = RequestFactory.Create(model.Model, path: path); - TestController controller = CreateController(request); - var createdODataResult = GetCreatedODataResult(_entity, request, controller); - - // Act - Uri locationHeader = createdODataResult.GenerateLocationHeader(request); - - // Assert - Assert.Same(editLink, locationHeader); - } + [Fact] + public void Property_LocationHeader_IsEvaluatedOnlyOnce() + { + // Arrange + Uri editLink = new Uri("http://edit-link"); + Mock linkBuilder = new Mock(); + linkBuilder.Setup(b => b.BuildEditLink(It.IsAny(), ODataMetadataLevel.Full, null)) + .Returns(editLink); + + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); + model.Model.SetNavigationSourceLinkBuilder(model.Customers, linkBuilder.Object); + ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); + var request = RequestFactory.Create(model.Model, path: path); + TestController controller = CreateController(request); + var createdODataResult = GetCreatedODataResult(_entity, request, controller); + + // Act + Uri locationHeader = createdODataResult.GenerateLocationHeader(request); + + // Assert + linkBuilder.Verify( + (b) => b.BuildEditLink(It.IsAny(), ODataMetadataLevel.Full, null), + Times.Once()); + } - [Fact] - public void Property_LocationHeader_IsEvaluatedOnlyOnce() - { - // Arrange - Uri editLink = new Uri("http://edit-link"); - Mock linkBuilder = new Mock(); - linkBuilder.Setup(b => b.BuildEditLink(It.IsAny(), ODataMetadataLevel.Full, null)) - .Returns(editLink); - - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); - model.Model.SetNavigationSourceLinkBuilder(model.Customers, linkBuilder.Object); - ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); - var request = RequestFactory.Create(model.Model, path: path); - TestController controller = CreateController(request); - var createdODataResult = GetCreatedODataResult(_entity, request, controller); - - // Act - Uri locationHeader = createdODataResult.GenerateLocationHeader(request); - - // Assert - linkBuilder.Verify( - (b) => b.BuildEditLink(It.IsAny(), ODataMetadataLevel.Full, null), - Times.Once()); - } + [Fact] + public void Property_EntityIdHeader_IsEvaluatedLazilyAndOnlyOnce() + { + // Arrange + Uri idLink = new Uri("http://id-link"); + Mock linkBuilder = new Mock(); + linkBuilder.CallBase = true; + linkBuilder.Setup(b => b.BuildIdLink(It.IsAny(), ODataMetadataLevel.Full)) + .Returns(idLink); + + CustomersModelWithInheritance model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); + model.Model.SetNavigationSourceLinkBuilder(model.Customers, linkBuilder.Object); + ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); + var request = RequestFactory.Create(model.Model, path: path); + TestController controller = CreateController(request); + var createdODataResult = GetCreatedODataResult(_entity, request, controller); + + // Act + Uri entityIdHeader = createdODataResult.GenerateEntityId(request); + + // Assert + Assert.Same(idLink, entityIdHeader); + linkBuilder.Verify( + b => b.BuildIdLink(It.IsAny(), ODataMetadataLevel.Full), + Times.Once()); + } - [Fact] - public void Property_EntityIdHeader_IsEvaluatedLazilyAndOnlyOnce() - { - // Arrange - Uri idLink = new Uri("http://id-link"); - Mock linkBuilder = new Mock(); - linkBuilder.CallBase = true; - linkBuilder.Setup(b => b.BuildIdLink(It.IsAny(), ODataMetadataLevel.Full)) - .Returns(idLink); - - CustomersModelWithInheritance model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new ClrTypeAnnotation(typeof(TestEntity))); - model.Model.SetNavigationSourceLinkBuilder(model.Customers, linkBuilder.Object); - ODataPath path = new ODataPath(new EntitySetSegment(model.Customers)); - var request = RequestFactory.Create(model.Model, path: path); - TestController controller = CreateController(request); - var createdODataResult = GetCreatedODataResult(_entity, request, controller); - - // Act - Uri entityIdHeader = createdODataResult.GenerateEntityId(request); - - // Assert - Assert.Same(idLink, entityIdHeader); - linkBuilder.Verify( - b => b.BuildIdLink(It.IsAny(), ODataMetadataLevel.Full), - Times.Once()); - } + private class TestEntity + { + } - private class TestEntity - { - } + private class TestController : ODataController + { + } - private class TestController : ODataController - { - } + private CreatedODataResult GetCreatedODataResult(T entity, HttpRequest request, TestController controller = null) + { + return new CreatedODataResult(entity); + } - private CreatedODataResult GetCreatedODataResult(T entity, HttpRequest request, TestController controller = null) + private HttpRequest CreateRequest(string preferHeaderValue = null) + { + var request = RequestFactory.Create(); + if (!string.IsNullOrEmpty(preferHeaderValue)) { - return new CreatedODataResult(entity); + request.Headers.Append("Prefer", new StringValues(preferHeaderValue)); } - private HttpRequest CreateRequest(string preferHeaderValue = null) - { - var request = RequestFactory.Create(); - if (!string.IsNullOrEmpty(preferHeaderValue)) - { - request.Headers.Append("Prefer", new StringValues(preferHeaderValue)); - } - - return request; - } + return request; + } - private TestController CreateController(AspNetCore.Http.HttpRequest request) - { - TestController controller = new TestController(); - return controller; - } + private TestController CreateController(AspNetCore.Http.HttpRequest request) + { + TestController controller = new TestController(); + return controller; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Results/PageResultOfTTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Results/PageResultOfTTests.cs index fea9d0b55..eec0a90c2 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Results/PageResultOfTTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Results/PageResultOfTTests.cs @@ -12,88 +12,87 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Results +namespace Microsoft.AspNetCore.OData.Tests.Results; + +public class PageResultOfTTests { - public class PageResultOfTTests + [Fact] + public void CtorPageResultOfT_ThrowsArgumentNull_Items() { - [Fact] - public void CtorPageResultOfT_ThrowsArgumentNull_Items() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new PageResult(null, null, null), "items"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new PageResult(null, null, null), "items"); + } - [Fact] - public void CtorPageResultOfT_SetProperties() - { - // Arrange - IEnumerable customers = Enumerable.Empty(); - Uri nextPageLink = new Uri("http://any"); - long? count = 1; + [Fact] + public void CtorPageResultOfT_SetProperties() + { + // Arrange + IEnumerable customers = Enumerable.Empty(); + Uri nextPageLink = new Uri("http://any"); + long? count = 1; - // Act - PageResult result = new PageResult(customers, nextPageLink, count); + // Act + PageResult result = new PageResult(customers, nextPageLink, count); - // Assert - Assert.Same(result.Items, customers); - Assert.Same(nextPageLink, result.NextPageLink); - Assert.Equal(1, result.Count); - } + // Assert + Assert.Same(result.Items, customers); + Assert.Same(nextPageLink, result.NextPageLink); + Assert.Equal(1, result.Count); + } - [Fact] - public void CtorPageResultOfT_ThrowsArgumentOutOfRange_NegativeCount() - { - // Arrange - IEnumerable customers = Enumerable.Empty(); - Uri nextPageLink = new Uri("http://any"); - long? count = -1; + [Fact] + public void CtorPageResultOfT_ThrowsArgumentOutOfRange_NegativeCount() + { + // Arrange + IEnumerable customers = Enumerable.Empty(); + Uri nextPageLink = new Uri("http://any"); + long? count = -1; - // Act - Action test = () => new PageResult(customers, nextPageLink, count); + // Act + Action test = () => new PageResult(customers, nextPageLink, count); - // Assert - ArgumentOutOfRangeException exception = ExceptionAssert.Throws(test); - Assert.Contains("Value must be greater than or equal to 0. (Parameter 'value')\r\nActual value was -1.", exception.Message); - } + // Assert + ArgumentOutOfRangeException exception = ExceptionAssert.Throws(test); + Assert.Contains("Value must be greater than or equal to 0. (Parameter 'value')\r\nActual value was -1.", exception.Message); + } - [Fact] - public void ToDictionaryPageResultOfT_Works() + [Fact] + public void ToDictionaryPageResultOfT_Works() + { + // Arrange + IEnumerable customers = new Customer[] { - // Arrange - IEnumerable customers = new Customer[] - { - new Customer(), - new Customer() - }; - Uri nextPageLink = new Uri("http://any"); - long? count = 2; + new Customer(), + new Customer() + }; + Uri nextPageLink = new Uri("http://any"); + long? count = 2; - // Act - PageResult result = new PageResult(customers, nextPageLink, count); - IDictionary dics = result.ToDictionary(); + // Act + PageResult result = new PageResult(customers, nextPageLink, count); + IDictionary dics = result.ToDictionary(); - // Assert - Assert.Equal(3, dics.Count); - Assert.Collection(dics, - e => - { - Assert.Equal("items", e.Key); - Assert.Same(customers, e.Value); - }, - e => - { - Assert.Equal("nextpagelink", e.Key); - Assert.Equal("http://any", e.Value); - }, - e => - { - Assert.Equal("count", e.Key); - Assert.Equal((long)2, e.Value); - }); - } + // Assert + Assert.Equal(3, dics.Count); + Assert.Collection(dics, + e => + { + Assert.Equal("items", e.Key); + Assert.Same(customers, e.Value); + }, + e => + { + Assert.Equal("nextpagelink", e.Key); + Assert.Equal("http://any", e.Value); + }, + e => + { + Assert.Equal("count", e.Key); + Assert.Equal((long)2, e.Value); + }); + } - private class Customer - { - } + private class Customer + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Results/PageResultValueConverterTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Results/PageResultValueConverterTests.cs index c1daa87d9..cec5a7d54 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Results/PageResultValueConverterTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Results/PageResultValueConverterTests.cs @@ -13,71 +13,70 @@ using Microsoft.AspNetCore.OData.Tests.Extensions; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Results +namespace Microsoft.AspNetCore.OData.Tests.Results; + +public class PageResultValueConverterTests { - public class PageResultValueConverterTests + [Theory] + [InlineData(null, false)] + [InlineData(typeof(object), false)] + [InlineData(typeof(PageResult), true)] + public void CanConvert_WorksForPageResultValueConverter(Type type, bool expected) { - [Theory] - [InlineData(null, false)] - [InlineData(typeof(object), false)] - [InlineData(typeof(PageResult), true)] - public void CanConvert_WorksForPageResultValueConverter(Type type, bool expected) - { - // Arrange - PageResultValueConverter converter = new PageResultValueConverter(); + // Arrange + PageResultValueConverter converter = new PageResultValueConverter(); - // Act & Assert - Assert.Equal(expected, converter.CanConvert(type)); - } + // Act & Assert + Assert.Equal(expected, converter.CanConvert(type)); + } - [Fact] - public void CreateConverter_WorksForPageResultValueConverter() - { - // Arrange - JsonSerializerOptions options = new JsonSerializerOptions(); - PageResultValueConverter converter = new PageResultValueConverter(); + [Fact] + public void CreateConverter_WorksForPageResultValueConverter() + { + // Arrange + JsonSerializerOptions options = new JsonSerializerOptions(); + PageResultValueConverter converter = new PageResultValueConverter(); - // Act & Assert - Type type = typeof(PageResult); - JsonConverter typeConverter = converter.CreateConverter(type, options); - Assert.Equal(typeof(PageResultConverter), typeConverter.GetType()); + // Act & Assert + Type type = typeof(PageResult); + JsonConverter typeConverter = converter.CreateConverter(type, options); + Assert.Equal(typeof(PageResultConverter), typeConverter.GetType()); - // Act & Assert - type = typeof(IEnumerable); - typeConverter = converter.CreateConverter(type, options); - Assert.Null(typeConverter); - } + // Act & Assert + type = typeof(IEnumerable); + typeConverter = converter.CreateConverter(type, options); + Assert.Null(typeConverter); + } - [Fact] - public void PageResultValueConverter_CanSerializePageResultOfT() + [Fact] + public void PageResultValueConverter_CanSerializePageResultOfT() + { + // Arrange & Act & Assert + IEnumerable customers = new Customer[] { - // Arrange & Act & Assert - IEnumerable customers = new Customer[] - { - new Customer { Id = 1, Name = "abc" }, - new Customer { Id = 2, Name = "efg" }, - }; - Uri nextPageLink = new Uri("http://any"); - long? count = 2; - PageResult result = new PageResult(customers, nextPageLink, count); + new Customer { Id = 1, Name = "abc" }, + new Customer { Id = 2, Name = "efg" }, + }; + Uri nextPageLink = new Uri("http://any"); + long? count = 2; + PageResult result = new PageResult(customers, nextPageLink, count); - JsonSerializerOptions options = new JsonSerializerOptions(); - PageResultValueConverter converterFactory = new PageResultValueConverter(); - Type type = typeof(PageResult); - PageResultConverter typeConverter = converterFactory.CreateConverter(type, options) as PageResultConverter; + JsonSerializerOptions options = new JsonSerializerOptions(); + PageResultValueConverter converterFactory = new PageResultValueConverter(); + Type type = typeof(PageResult); + PageResultConverter typeConverter = converterFactory.CreateConverter(type, options) as PageResultConverter; - // Act - string json = SerializeUtils.SerializeAsJson(jsonWriter => typeConverter.Write(jsonWriter, result, options)); + // Act + string json = SerializeUtils.SerializeAsJson(jsonWriter => typeConverter.Write(jsonWriter, result, options)); - // Assert - Assert.Equal("{\"items\":[{\"Id\":1,\"Name\":\"abc\"},{\"Id\":2,\"Name\":\"efg\"}],\"nextpagelink\":\"http://any\",\"count\":2}", json); - } + // Assert + Assert.Equal("{\"items\":[{\"Id\":1,\"Name\":\"abc\"},{\"Id\":2,\"Name\":\"efg\"}],\"nextpagelink\":\"http://any\",\"count\":2}", json); + } - private class Customer - { - public int Id { get; set; } + private class Customer + { + public int Id { get; set; } - public string Name { get; set; } - } + public string Name { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Results/ResultHelpersTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Results/ResultHelpersTest.cs index b97d95135..f99b4d62c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Results/ResultHelpersTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Results/ResultHelpersTest.cs @@ -21,105 +21,104 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Results +namespace Microsoft.AspNetCore.OData.Tests.Results; + +public class ResultHelpersTest { - public class ResultHelpersTest + private readonly TestEntity _entity = new TestEntity(); + private readonly Uri _entityId = new Uri("http://entity_id"); + private readonly string _version = "4.0.1.101"; + + [Fact] + public void GenerateODataLink_ThrowsIdLinkNullForEntityIdHeader_IfEntitySetLinkBuilderReturnsNull() + { + // Arrange + var linkBuilder = new Mock(); + var model = new CustomersModelWithInheritance(); + model.Model.SetAnnotationValue(model.Customer, new Microsoft.OData.ModelBuilder.ClrTypeAnnotation(typeof(TestEntity))); + model.Model.SetNavigationSourceLinkBuilder(model.Customers, linkBuilder.Object); + var path = new ODataPath(new EntitySetSegment(model.Customers)); + var request = RequestFactory.Create(model.Model, path: path); + + // Act & Assert + ExceptionAssert.Throws( + () => ResultHelpers.GenerateODataLink(request, _entity, isEntityId: true), + "The Id link builder for the entity set 'Customers' returned null. An Id link is required for the OData-EntityId header."); + } + + [Fact] + public void GenerateODataLink_CanResolveIEdmObject() + { + // Arrange + var model = new CustomersModelWithInheritance(); + var path = new ODataPath(new EntitySetSegment(model.Customers)); + var request = RequestFactory.Create(model.Model, path: path); + request.ODataFeature().BaseAddress = "http://localhost"; + var edmEntity = new EdmEntityObject(model.Customer); + edmEntity.TrySetPropertyValue("ID", 1); + + // Act & Assert + Assert.Equal("http://localhost/Customers(1)", + ResultHelpers.GenerateODataLink(request, edmEntity, isEntityId: true).AbsoluteUri); + } + + [Fact] + public void AddEntityId_AddsEntityId_IfResponseStatusCodeIsNoContent() + { + // Arrange + var response = ResponseFactory.Create(StatusCodes.Status204NoContent); + + // Act + ResultHelpers.AddEntityId(response, () => _entityId); + + // Assert + var entityIdHeaderValues = response.Headers[ResultHelpers.EntityIdHeaderName].ToList(); + Assert.Single(entityIdHeaderValues); + Assert.Equal(_entityId.ToString(), entityIdHeaderValues.Single()); + } + + [Fact] + public void AddEntityId_DoesNotAddEntityId_IfResponseStatusCodeIsOtherThanNoContent() + { + // Arrange + var response = ResponseFactory.Create(StatusCodes.Status200OK); + + // Act + ResultHelpers.AddEntityId(response, () => _entityId); + + // Assert + Assert.False(response.Headers.ContainsKey(ResultHelpers.EntityIdHeaderName)); + } + + [Fact] + public void AddServiceVersion_AddsODataVersion_IfResponseStatusCodeIsNoContent() + { + // Arrange + var response = ResponseFactory.Create(StatusCodes.Status204NoContent); + + // Act + ResultHelpers.AddServiceVersion(response, () => _version); + + // Assert + var versionHeaderValues = response.Headers[ODataVersionConstraint.ODataServiceVersionHeader].ToList(); + Assert.Single(versionHeaderValues); + Assert.Equal(_version, versionHeaderValues.Single()); + } + + [Fact] + public void AddServiceVersion_DoesNotAddServiceVersion_IfResponseStatusCodeIsOtherThanNoContent() + { + // Arrange + var response = ResponseFactory.Create(StatusCodes.Status200OK); + + // Act + ResultHelpers.AddServiceVersion(response, () => _version); + + // Assert + Assert.False(response.Headers.ContainsKey(ODataVersionConstraint.ODataServiceVersionHeader)); + } + + private class TestEntity { - private readonly TestEntity _entity = new TestEntity(); - private readonly Uri _entityId = new Uri("http://entity_id"); - private readonly string _version = "4.0.1.101"; - - [Fact] - public void GenerateODataLink_ThrowsIdLinkNullForEntityIdHeader_IfEntitySetLinkBuilderReturnsNull() - { - // Arrange - var linkBuilder = new Mock(); - var model = new CustomersModelWithInheritance(); - model.Model.SetAnnotationValue(model.Customer, new Microsoft.OData.ModelBuilder.ClrTypeAnnotation(typeof(TestEntity))); - model.Model.SetNavigationSourceLinkBuilder(model.Customers, linkBuilder.Object); - var path = new ODataPath(new EntitySetSegment(model.Customers)); - var request = RequestFactory.Create(model.Model, path: path); - - // Act & Assert - ExceptionAssert.Throws( - () => ResultHelpers.GenerateODataLink(request, _entity, isEntityId: true), - "The Id link builder for the entity set 'Customers' returned null. An Id link is required for the OData-EntityId header."); - } - - [Fact] - public void GenerateODataLink_CanResolveIEdmObject() - { - // Arrange - var model = new CustomersModelWithInheritance(); - var path = new ODataPath(new EntitySetSegment(model.Customers)); - var request = RequestFactory.Create(model.Model, path: path); - request.ODataFeature().BaseAddress = "http://localhost"; - var edmEntity = new EdmEntityObject(model.Customer); - edmEntity.TrySetPropertyValue("ID", 1); - - // Act & Assert - Assert.Equal("http://localhost/Customers(1)", - ResultHelpers.GenerateODataLink(request, edmEntity, isEntityId: true).AbsoluteUri); - } - - [Fact] - public void AddEntityId_AddsEntityId_IfResponseStatusCodeIsNoContent() - { - // Arrange - var response = ResponseFactory.Create(StatusCodes.Status204NoContent); - - // Act - ResultHelpers.AddEntityId(response, () => _entityId); - - // Assert - var entityIdHeaderValues = response.Headers[ResultHelpers.EntityIdHeaderName].ToList(); - Assert.Single(entityIdHeaderValues); - Assert.Equal(_entityId.ToString(), entityIdHeaderValues.Single()); - } - - [Fact] - public void AddEntityId_DoesNotAddEntityId_IfResponseStatusCodeIsOtherThanNoContent() - { - // Arrange - var response = ResponseFactory.Create(StatusCodes.Status200OK); - - // Act - ResultHelpers.AddEntityId(response, () => _entityId); - - // Assert - Assert.False(response.Headers.ContainsKey(ResultHelpers.EntityIdHeaderName)); - } - - [Fact] - public void AddServiceVersion_AddsODataVersion_IfResponseStatusCodeIsNoContent() - { - // Arrange - var response = ResponseFactory.Create(StatusCodes.Status204NoContent); - - // Act - ResultHelpers.AddServiceVersion(response, () => _version); - - // Assert - var versionHeaderValues = response.Headers[ODataVersionConstraint.ODataServiceVersionHeader].ToList(); - Assert.Single(versionHeaderValues); - Assert.Equal(_version, versionHeaderValues.Single()); - } - - [Fact] - public void AddServiceVersion_DoesNotAddServiceVersion_IfResponseStatusCodeIsOtherThanNoContent() - { - // Arrange - var response = ResponseFactory.Create(StatusCodes.Status200OK); - - // Act - ResultHelpers.AddServiceVersion(response, () => _version); - - // Assert - Assert.False(response.Headers.ContainsKey(ODataVersionConstraint.ODataServiceVersionHeader)); - } - - private class TestEntity - { - } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Results/SingleResultValueConverterTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Results/SingleResultValueConverterTests.cs index f4b70978d..a19024532 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Results/SingleResultValueConverterTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Results/SingleResultValueConverterTests.cs @@ -14,70 +14,69 @@ using Microsoft.AspNetCore.OData.Tests.Extensions; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Results +namespace Microsoft.AspNetCore.OData.Tests.Results; + +public class SingleResultValueConverterTests { - public class SingleResultValueConverterTests + [Theory] + [InlineData(null, false)] + [InlineData(typeof(object), false)] + [InlineData(typeof(SingleResult), true)] + public void CanConvert_WorksForSingleResultValueConverter(Type type, bool expected) { - [Theory] - [InlineData(null, false)] - [InlineData(typeof(object), false)] - [InlineData(typeof(SingleResult), true)] - public void CanConvert_WorksForSingleResultValueConverter(Type type, bool expected) - { - // Arrange - SingleResultValueConverter converter = new SingleResultValueConverter(); + // Arrange + SingleResultValueConverter converter = new SingleResultValueConverter(); - // Act & Assert - Assert.Equal(expected, converter.CanConvert(type)); - } + // Act & Assert + Assert.Equal(expected, converter.CanConvert(type)); + } - [Fact] - public void CreateConverter_WorksForSingleResultValueConverter() - { - // Arrange - JsonSerializerOptions options = new JsonSerializerOptions(); - SingleResultValueConverter converter = new SingleResultValueConverter(); + [Fact] + public void CreateConverter_WorksForSingleResultValueConverter() + { + // Arrange + JsonSerializerOptions options = new JsonSerializerOptions(); + SingleResultValueConverter converter = new SingleResultValueConverter(); - // Act & Assert - Type type = typeof(SingleResult); - JsonConverter typeConverter = converter.CreateConverter(type, options); - Assert.Equal(typeof(SingleResultConverter), typeConverter.GetType()); + // Act & Assert + Type type = typeof(SingleResult); + JsonConverter typeConverter = converter.CreateConverter(type, options); + Assert.Equal(typeof(SingleResultConverter), typeConverter.GetType()); - // Act & Assert - type = typeof(IEnumerable); - typeConverter = converter.CreateConverter(type, options); - Assert.Null(typeConverter); - } + // Act & Assert + type = typeof(IEnumerable); + typeConverter = converter.CreateConverter(type, options); + Assert.Null(typeConverter); + } - [Fact] - public void SingleResultValueConverter_CanSerializeSingleResultOfT() + [Fact] + public void SingleResultValueConverter_CanSerializeSingleResultOfT() + { + // Arrange & Act & Assert + IEnumerable customers = new Customer[] { - // Arrange & Act & Assert - IEnumerable customers = new Customer[] - { - new Customer { Id = 1, Name = "abc" }, - new Customer { Id = 2, Name = "efg" } - }; + new Customer { Id = 1, Name = "abc" }, + new Customer { Id = 2, Name = "efg" } + }; - SingleResult result = new SingleResult(customers.AsQueryable()); + SingleResult result = new SingleResult(customers.AsQueryable()); - JsonSerializerOptions options = new JsonSerializerOptions(); - SingleResultValueConverter converterFactory = new SingleResultValueConverter(); - Type type = typeof(SingleResult); - SingleResultConverter typeConverter = converterFactory.CreateConverter(type, options) as SingleResultConverter; + JsonSerializerOptions options = new JsonSerializerOptions(); + SingleResultValueConverter converterFactory = new SingleResultValueConverter(); + Type type = typeof(SingleResult); + SingleResultConverter typeConverter = converterFactory.CreateConverter(type, options) as SingleResultConverter; - // Act - string json = SerializeUtils.SerializeAsJson(jsonWriter => typeConverter.Write(jsonWriter, result, options)); + // Act + string json = SerializeUtils.SerializeAsJson(jsonWriter => typeConverter.Write(jsonWriter, result, options)); - // Assert - Assert.Equal("{\"Id\":1,\"Name\":\"abc\"}", json); - } + // Assert + Assert.Equal("{\"Id\":1,\"Name\":\"abc\"}", json); + } - private class Customer - { - public int Id { get; set; } + private class Customer + { + public int Id { get; set; } - public string Name { get; set; } - } + public string Name { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Results/UpdatedODataResultTest.cs b/test/Microsoft.AspNetCore.OData.Tests/Results/UpdatedODataResultTest.cs index b4dd2b328..bbe875770 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Results/UpdatedODataResultTest.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Results/UpdatedODataResultTest.cs @@ -16,97 +16,96 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Results +namespace Microsoft.AspNetCore.OData.Tests.Results; + +public class UpdatedODataResultTest { - public class UpdatedODataResultTest + private readonly TestEntity _entity = new TestEntity(); + + [Fact] + public void Ctor_ControllerDependency_ThrowsArgumentNull_Entity() { - private readonly TestEntity _entity = new TestEntity(); + ExceptionAssert.ThrowsArgumentNull( + () => new UpdatedODataResult(entity: null), "entity"); + } - [Fact] - public void Ctor_ControllerDependency_ThrowsArgumentNull_Entity() - { - ExceptionAssert.ThrowsArgumentNull( - () => new UpdatedODataResult(entity: null), "entity"); - } + [Fact] + public void GetEntity_ReturnsCorrect() + { + // Arrange + Mock mock = new Mock(); + UpdatedODataResult updatedODataResult = + new UpdatedODataResult(mock.Object); - [Fact] - public void GetEntity_ReturnsCorrect() - { - // Arrange - Mock mock = new Mock(); - UpdatedODataResult updatedODataResult = - new UpdatedODataResult(mock.Object); + // Act & Assert + Assert.Same(mock.Object, updatedODataResult.Entity); + } - // Act & Assert - Assert.Same(mock.Object, updatedODataResult.Entity); - } + [Fact] + public void GetActionResult_ReturnsNoContentStatusCodeResult_IfRequestHasNoPreferenceHeader() + { + // Arrange + var request = CreateRequest(); - [Fact] - public void GetActionResult_ReturnsNoContentStatusCodeResult_IfRequestHasNoPreferenceHeader() - { - // Arrange - var request = CreateRequest(); + // Act + var result = CreateActionResult(request); - // Act - var result = CreateActionResult(request); + // Assert + StatusCodeResult statusCodeResult = Assert.IsType(result); + Assert.Equal(HttpStatusCode.NoContent, (HttpStatusCode)statusCodeResult.StatusCode); + } - // Assert - StatusCodeResult statusCodeResult = Assert.IsType(result); - Assert.Equal(HttpStatusCode.NoContent, (HttpStatusCode)statusCodeResult.StatusCode); - } + [Fact] + public void GetActionResult_ReturnsNoContentStatusCodeResult_IfRequestAsksForNoContent() + { + // Arrange + var request = CreateRequest("return=minimal"); - [Fact] - public void GetActionResult_ReturnsNoContentStatusCodeResult_IfRequestAsksForNoContent() - { - // Arrange - var request = CreateRequest("return=minimal"); + // Act + var result = CreateActionResult(request); - // Act - var result = CreateActionResult(request); + // Assert + StatusCodeResult statusCodeResult = Assert.IsType(result); + Assert.Equal(HttpStatusCode.NoContent, (HttpStatusCode)statusCodeResult.StatusCode); + } - // Assert - StatusCodeResult statusCodeResult = Assert.IsType(result); - Assert.Equal(HttpStatusCode.NoContent, (HttpStatusCode)statusCodeResult.StatusCode); - } + [Fact] + public void GetActionResult_ReturnsNegotiatedContentResult_IfRequestAsksForContent() + { + // Arrange + var request = CreateRequest("return=representation"); - [Fact] - public void GetActionResult_ReturnsNegotiatedContentResult_IfRequestAsksForContent() - { - // Arrange - var request = CreateRequest("return=representation"); + // Act + var result = CreateActionResult(request); - // Act - var result = CreateActionResult(request); + // Assert + ObjectResult objectResult = Assert.IsType(result); + Assert.Same(typeof(TestEntity), objectResult.Value.GetType()); + Assert.Equal(HttpStatusCode.OK, (HttpStatusCode)objectResult.StatusCode); + } - // Assert - ObjectResult objectResult = Assert.IsType(result); - Assert.Same(typeof(TestEntity), objectResult.Value.GetType()); - Assert.Equal(HttpStatusCode.OK, (HttpStatusCode)objectResult.StatusCode); - } + private class TestEntity + { + } - private class TestEntity - { - } + private class TestController : ODataController + { + } - private class TestController : ODataController + private HttpRequest CreateRequest(string preferHeaderValue = null) + { + var request = RequestFactory.Create(); + if (!string.IsNullOrEmpty(preferHeaderValue)) { + request.Headers.Append("Prefer", new StringValues(preferHeaderValue)); } - private HttpRequest CreateRequest(string preferHeaderValue = null) - { - var request = RequestFactory.Create(); - if (!string.IsNullOrEmpty(preferHeaderValue)) - { - request.Headers.Append("Prefer", new StringValues(preferHeaderValue)); - } - - return request; - } + return request; + } - private IActionResult CreateActionResult(HttpRequest request) - { - UpdatedODataResult updatedODataResult = new UpdatedODataResult(_entity); - return updatedODataResult.GetInnerActionResult(request); - } + private IActionResult CreateActionResult(HttpRequest request) + { + UpdatedODataResult updatedODataResult = new UpdatedODataResult(_entity); + return updatedODataResult.GetInnerActionResult(request); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Attributes/ODataModelAttributeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Attributes/ODataModelAttributeTests.cs index 6a2d79cf8..854004f02 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Attributes/ODataModelAttributeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Attributes/ODataModelAttributeTests.cs @@ -9,27 +9,26 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Attributes +namespace Microsoft.AspNetCore.OData.Tests.Routing.Attributes; + +public class ODataModelAttributeTests { - public class ODataModelAttributeTests + [Fact] + public void CtorODataModelAttribute_SetsModel() { - [Fact] - public void CtorODataModelAttribute_SetsModel() - { - // Assert & Act & Assert - ODataRouteComponentAttribute odataModel = new ODataRouteComponentAttribute(); - Assert.Equal(string.Empty, odataModel.RoutePrefix); + // Assert & Act & Assert + ODataRouteComponentAttribute odataModel = new ODataRouteComponentAttribute(); + Assert.Equal(string.Empty, odataModel.RoutePrefix); - // Assert & Act & Assert - odataModel = new ODataRouteComponentAttribute("odata"); - Assert.Equal("odata", odataModel.RoutePrefix); - } + // Assert & Act & Assert + odataModel = new ODataRouteComponentAttribute("odata"); + Assert.Equal("odata", odataModel.RoutePrefix); + } - [Fact] - public void CtorODataModelAttribute_ThrowsArgumentNull_Model() - { - // Assert & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataRouteComponentAttribute(routePrefix: null), "routePrefix"); - } + [Fact] + public void CtorODataModelAttribute_ThrowsArgumentNull_Model() + { + // Assert & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataRouteComponentAttribute(routePrefix: null), "routePrefix"); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Controllers/MetadataControllerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Controllers/MetadataControllerTests.cs index a68fd1389..860850209 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Controllers/MetadataControllerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Controllers/MetadataControllerTests.cs @@ -14,86 +14,85 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Controllers +namespace Microsoft.AspNetCore.OData.Tests.Routing.Controllers; + +public class MetadataControllerTests { - public class MetadataControllerTests + [Fact] + public void GetMetadataOnMetadataController_ReturnsODataModel() + { + // Arrange + IEdmModel model = new EdmModel(); + HttpRequest reqest = RequestFactory.Create(model); + + MetadataController metadataController = new MetadataController(); + metadataController.ControllerContext.HttpContext = reqest.HttpContext; + + // Act + IEdmModel actual = metadataController.GetMetadata(); + + // Assert + Assert.Same(model, actual); + } + + [Fact] + public void GetMetadataOnMetadataController_ThrowsInvalidOperationException() { - [Fact] - public void GetMetadataOnMetadataController_ReturnsODataModel() - { - // Arrange - IEdmModel model = new EdmModel(); - HttpRequest reqest = RequestFactory.Create(model); - - MetadataController metadataController = new MetadataController(); - metadataController.ControllerContext.HttpContext = reqest.HttpContext; - - // Act - IEdmModel actual = metadataController.GetMetadata(); - - // Assert - Assert.Same(model, actual); - } - - [Fact] - public void GetMetadataOnMetadataController_ThrowsInvalidOperationException() - { - // Arrange - HttpRequest reqest = RequestFactory.Create(model: null); - - MetadataController metadataController = new MetadataController(); - metadataController.ControllerContext.HttpContext = reqest.HttpContext; - - // Act - Action test = () => metadataController.GetMetadata(); - - // Assert - ExceptionAssert.Throws(test, - "The request must have an associated EDM model. Consider registering Edm model calling AddOData()."); - } - - [Fact] - public void GetODataServiceDocumentOnMetadataController_ReturnsODataODataServiceDocument() - { - // Arrange - EdmModel model = new EdmModel(); - EdmEntityType entity = new EdmEntityType("NS", "Entity"); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - container.AddSingleton("me", entity); - model.AddElement(entity); - model.AddElement(container); - - HttpRequest reqest = RequestFactory.Create(model); - - MetadataController metadataController = new MetadataController(); - metadataController.ControllerContext.HttpContext = reqest.HttpContext; - - // Act - ODataServiceDocument actual = metadataController.GetServiceDocument(); - - // Assert - Assert.NotNull(actual); - Assert.Empty(actual.EntitySets); - Assert.Empty(actual.FunctionImports); - ODataSingletonInfo singletonInfo = Assert.Single(actual.Singletons); - Assert.Equal("me", singletonInfo.Name); - } - - [Fact] - public void GetODataServiceDocumentOnMetadataController_ThrowsInvalidOperationException_() - { - // Arrange - HttpRequest reqest = RequestFactory.Create(model: null); - - MetadataController metadataController = new MetadataController(); - metadataController.ControllerContext.HttpContext = reqest.HttpContext; - - // Act - Action test = () => metadataController.GetServiceDocument(); - - // Assert - ExceptionAssert.Throws(test, - "The request must have an associated EDM model. Consider registering Edm model calling AddOData()."); - } + // Arrange + HttpRequest reqest = RequestFactory.Create(model: null); + + MetadataController metadataController = new MetadataController(); + metadataController.ControllerContext.HttpContext = reqest.HttpContext; + + // Act + Action test = () => metadataController.GetMetadata(); + + // Assert + ExceptionAssert.Throws(test, + "The request must have an associated EDM model. Consider registering Edm model calling AddOData()."); + } + + [Fact] + public void GetODataServiceDocumentOnMetadataController_ReturnsODataODataServiceDocument() + { + // Arrange + EdmModel model = new EdmModel(); + EdmEntityType entity = new EdmEntityType("NS", "Entity"); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + container.AddSingleton("me", entity); + model.AddElement(entity); + model.AddElement(container); + + HttpRequest reqest = RequestFactory.Create(model); + + MetadataController metadataController = new MetadataController(); + metadataController.ControllerContext.HttpContext = reqest.HttpContext; + + // Act + ODataServiceDocument actual = metadataController.GetServiceDocument(); + + // Assert + Assert.NotNull(actual); + Assert.Empty(actual.EntitySets); + Assert.Empty(actual.FunctionImports); + ODataSingletonInfo singletonInfo = Assert.Single(actual.Singletons); + Assert.Equal("me", singletonInfo.Name); + } + + [Fact] + public void GetODataServiceDocumentOnMetadataController_ThrowsInvalidOperationException_() + { + // Arrange + HttpRequest reqest = RequestFactory.Create(model: null); + + MetadataController metadataController = new MetadataController(); + metadataController.ControllerContext.HttpContext = reqest.HttpContext; + + // Act + Action test = () => metadataController.GetServiceDocument(); + + // Assert + ExceptionAssert.Throws(test, + "The request must have an associated EDM model. Consider registering Edm model calling AddOData()."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Controllers/ODataControllerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Controllers/ODataControllerTests.cs index e6ea69d74..231b0706d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Controllers/ODataControllerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Controllers/ODataControllerTests.cs @@ -10,45 +10,44 @@ using Microsoft.AspNetCore.OData.Tests.Commons; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Controllers +namespace Microsoft.AspNetCore.OData.Tests.Routing.Controllers; + +public class ODataControllerTests +{ + [Fact] + public void CreatedOnODataController_ThrowsArgumentNull_Entity() + { + // Arrange + MyController myController = new MyController(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => myController.MyCreated(null), "entity"); + } + + [Fact] + public void UpdatedOnODataController_ThrowsArgumentNull_Entity() + { + // Arrange + MyController myController = new MyController(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => myController.MyUpdated(null), "entity"); + } + + private class Customer + { + } +} + +public class MyController : ODataController { - public class ODataControllerTests + public CreatedODataResult MyCreated(TEntity entity) { - [Fact] - public void CreatedOnODataController_ThrowsArgumentNull_Entity() - { - // Arrange - MyController myController = new MyController(); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => myController.MyCreated(null), "entity"); - } - - [Fact] - public void UpdatedOnODataController_ThrowsArgumentNull_Entity() - { - // Arrange - MyController myController = new MyController(); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => myController.MyUpdated(null), "entity"); - } - - private class Customer - { - } + return base.Created(entity); } - public class MyController : ODataController + public UpdatedODataResult MyUpdated(TEntity entity) { - public CreatedODataResult MyCreated(TEntity entity) - { - return base.Created(entity); - } - - public UpdatedODataResult MyUpdated(TEntity entity) - { - return base.Updated(entity); - } + return base.Updated(entity); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ActionRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ActionRoutingConventionTests.cs index d790c1da4..eab03a4de 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ActionRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ActionRoutingConventionTests.cs @@ -19,527 +19,526 @@ using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +public class ActionRoutingConventionTests { - public class ActionRoutingConventionTests - { - private readonly ITestOutputHelper _testOutputHelper; - private static ActionRoutingConvention ActionConvention = ConventionHelpers.CreateConvention(); - private static IEdmModel EdmModel = GetEdmModel(); + private readonly ITestOutputHelper _testOutputHelper; + private static ActionRoutingConvention ActionConvention = ConventionHelpers.CreateConvention(); + private static IEdmModel EdmModel = GetEdmModel(); - public ActionRoutingConventionTests(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - } + public ActionRoutingConventionTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } - [Fact] - public void AppliesToActionOnActionRoutingConvention_Throws_Context() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => ActionConvention.AppliesToController(null), "context"); - ExceptionAssert.ThrowsArgumentNull(() => ActionConvention.AppliesToAction(null), "context"); - } + [Fact] + public void AppliesToActionOnActionRoutingConvention_Throws_Context() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => ActionConvention.AppliesToController(null), "context"); + ExceptionAssert.ThrowsArgumentNull(() => ActionConvention.AppliesToAction(null), "context"); + } - [Theory] - [InlineData(typeof(CustomersController), true)] - [InlineData(typeof(MeController), true)] - [InlineData(typeof(UnknownController), false)] - public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + [Theory] + [InlineData(typeof(CustomersController), true)] + [InlineData(typeof(MeController), true)] + [InlineData(typeof(UnknownController), false)] + public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - // Act - bool actual = ActionConvention.AppliesToController(context); + // Act + bool actual = ActionConvention.AppliesToController(context); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - public static TheoryDataSet ActionRoutingConventionTestData + public static TheoryDataSet ActionRoutingConventionTestData + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() + // Bound to single { - // Bound to single + typeof(CustomersController), + "IsBaseUpgraded", + new[] { - typeof(CustomersController), - "IsBaseUpgraded", - new[] - { - "/Customers({key})/NS.IsBaseUpgraded", - "/Customers({key})/IsBaseUpgraded", - "/Customers/{key}/NS.IsBaseUpgraded", - "/Customers/{key}/IsBaseUpgraded", - } - }, + "/Customers({key})/NS.IsBaseUpgraded", + "/Customers({key})/IsBaseUpgraded", + "/Customers/{key}/NS.IsBaseUpgraded", + "/Customers/{key}/IsBaseUpgraded", + } + }, + { + typeof(MeController), + "IsBaseUpgraded", + new[] { - typeof(MeController), - "IsBaseUpgraded", - new[] - { - "/Me/NS.IsBaseUpgraded", - "/Me/IsBaseUpgraded" - } - }, + "/Me/NS.IsBaseUpgraded", + "/Me/IsBaseUpgraded" + } + }, + { + typeof(CustomersController), + "IsUpgraded", + new[] { - typeof(CustomersController), - "IsUpgraded", - new[] - { - "/Customers({key})/NS.IsUpgraded", - "/Customers({key})/IsUpgraded", - "/Customers/{key}/NS.IsUpgraded", - "/Customers/{key}/IsUpgraded" - } - }, + "/Customers({key})/NS.IsUpgraded", + "/Customers({key})/IsUpgraded", + "/Customers/{key}/NS.IsUpgraded", + "/Customers/{key}/IsUpgraded" + } + }, + { + typeof(MeController), + "IsUpgraded", + new[] { - typeof(MeController), - "IsUpgraded", - new[] - { - "/Me/NS.IsUpgraded", - "/Me/IsUpgraded" - } - }, + "/Me/NS.IsUpgraded", + "/Me/IsUpgraded" + } + }, + { + typeof(CustomersController), + "IsVipUpgraded", + new[] { - typeof(CustomersController), - "IsVipUpgraded", - new[] - { - "/Customers({key})/NS.VipCustomer/NS.IsVipUpgraded", - "/Customers({key})/NS.VipCustomer/IsVipUpgraded", - "/Customers/{key}/NS.VipCustomer/NS.IsVipUpgraded", - "/Customers/{key}/NS.VipCustomer/IsVipUpgraded" - } - }, + "/Customers({key})/NS.VipCustomer/NS.IsVipUpgraded", + "/Customers({key})/NS.VipCustomer/IsVipUpgraded", + "/Customers/{key}/NS.VipCustomer/NS.IsVipUpgraded", + "/Customers/{key}/NS.VipCustomer/IsVipUpgraded" + } + }, + { + typeof(MeController), + "IsVipUpgraded", + new[] { - typeof(MeController), - "IsVipUpgraded", - new[] - { - "/Me/NS.VipCustomer/NS.IsVipUpgraded", - "/Me/NS.VipCustomer/IsVipUpgraded" - } - }, - // bound to collection + "/Me/NS.VipCustomer/NS.IsVipUpgraded", + "/Me/NS.VipCustomer/IsVipUpgraded" + } + }, + // bound to collection + { + typeof(CustomersController), + "IsBaseAllUpgraded", + new[] { - typeof(CustomersController), - "IsBaseAllUpgraded", - new[] - { - "/Customers/NS.IsBaseAllUpgraded", - "/Customers/IsBaseAllUpgraded" - } - }, + "/Customers/NS.IsBaseAllUpgraded", + "/Customers/IsBaseAllUpgraded" + } + }, + { + typeof(CustomersController), + "IsAllCustomersUpgraded", + new[] { - typeof(CustomersController), - "IsAllCustomersUpgraded", - new[] - { - "/Customers/NS.IsAllCustomersUpgraded", - "/Customers/IsAllCustomersUpgraded" - } - }, + "/Customers/NS.IsAllCustomersUpgraded", + "/Customers/IsAllCustomersUpgraded" + } + }, + { + typeof(CustomersController), + "IsVipAllUpgraded", + new[] { - typeof(CustomersController), - "IsVipAllUpgraded", - new[] - { - "/Customers/NS.VipCustomer/NS.IsVipAllUpgraded", - "/Customers/NS.VipCustomer/IsVipAllUpgraded" - } - }, - // overload + "/Customers/NS.VipCustomer/NS.IsVipAllUpgraded", + "/Customers/NS.VipCustomer/IsVipAllUpgraded" + } + }, + // overload + { + typeof(CustomersController), + "UpgradedAllOnCustomer", + new[] { - typeof(CustomersController), - "UpgradedAllOnCustomer", - new[] - { - "/Customers({key})/NS.UpgradedAll", - "/Customers({key})/UpgradedAll", - "/Customers/{key}/NS.UpgradedAll", - "/Customers/{key}/UpgradedAll" - } - }, + "/Customers({key})/NS.UpgradedAll", + "/Customers({key})/UpgradedAll", + "/Customers/{key}/NS.UpgradedAll", + "/Customers/{key}/UpgradedAll" + } + }, + { + typeof(CustomersController), + "UpgradedAllOnCollectionOfCustomer", + new[] { - typeof(CustomersController), - "UpgradedAllOnCollectionOfCustomer", - new[] - { - "/Customers/NS.UpgradedAll", - "/Customers/UpgradedAll" - } - }, + "/Customers/NS.UpgradedAll", + "/Customers/UpgradedAll" + } + }, + { + typeof(CustomersController), + "UpgradedAllOnCollectionOfVipCustomer", + new[] { - typeof(CustomersController), - "UpgradedAllOnCollectionOfVipCustomer", - new[] - { - "/Customers/NS.VipCustomer/NS.UpgradedAll", - "/Customers/NS.VipCustomer/UpgradedAll" - } + "/Customers/NS.VipCustomer/NS.UpgradedAll", + "/Customers/NS.VipCustomer/UpgradedAll" } - }; - } + } + }; } + } - public static TheoryDataSet ActionRoutingConventionCaseInsensitiveTestData + public static TheoryDataSet ActionRoutingConventionCaseInsensitiveTestData + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() + // Bound to single { - // Bound to single + typeof(CustomersCaseInsensitiveController), + "ISBASEUPGRADED", + new[] { - typeof(CustomersCaseInsensitiveController), - "ISBASEUPGRADED", - new[] - { - "/CustomersCaseInsensitive({key})/NS.IsBaseUpgraded", - "/CustomersCaseInsensitive({key})/IsBaseUpgraded", - "/CustomersCaseInsensitive/{key}/NS.IsBaseUpgraded", - "/CustomersCaseInsensitive/{key}/IsBaseUpgraded", - } - }, + "/CustomersCaseInsensitive({key})/NS.IsBaseUpgraded", + "/CustomersCaseInsensitive({key})/IsBaseUpgraded", + "/CustomersCaseInsensitive/{key}/NS.IsBaseUpgraded", + "/CustomersCaseInsensitive/{key}/IsBaseUpgraded", + } + }, + { + typeof(CustomersCaseInsensitiveController), + "ISUPGRADED", + new[] { - typeof(CustomersCaseInsensitiveController), - "ISUPGRADED", - new[] - { - "/CustomersCaseInsensitive({key})/NS.IsUpgraded", - "/CustomersCaseInsensitive({key})/IsUpgraded", - "/CustomersCaseInsensitive/{key}/NS.IsUpgraded", - "/CustomersCaseInsensitive/{key}/IsUpgraded" - } - }, + "/CustomersCaseInsensitive({key})/NS.IsUpgraded", + "/CustomersCaseInsensitive({key})/IsUpgraded", + "/CustomersCaseInsensitive/{key}/NS.IsUpgraded", + "/CustomersCaseInsensitive/{key}/IsUpgraded" + } + }, + { + typeof(CustomersCaseInsensitiveController), + "ISVIPUPGRADED", + new[] { - typeof(CustomersCaseInsensitiveController), - "ISVIPUPGRADED", - new[] - { - "/CustomersCaseInsensitive({key})/NS.VipCustomer/NS.IsVipUpgraded", - "/CustomersCaseInsensitive({key})/NS.VipCustomer/IsVipUpgraded", - "/CustomersCaseInsensitive/{key}/NS.VipCustomer/NS.IsVipUpgraded", - "/CustomersCaseInsensitive/{key}/NS.VipCustomer/IsVipUpgraded" - } - }, - // bound to collection + "/CustomersCaseInsensitive({key})/NS.VipCustomer/NS.IsVipUpgraded", + "/CustomersCaseInsensitive({key})/NS.VipCustomer/IsVipUpgraded", + "/CustomersCaseInsensitive/{key}/NS.VipCustomer/NS.IsVipUpgraded", + "/CustomersCaseInsensitive/{key}/NS.VipCustomer/IsVipUpgraded" + } + }, + // bound to collection + { + typeof(CustomersCaseInsensitiveController), + "ISBASEALLUPGRADED", + new[] { - typeof(CustomersCaseInsensitiveController), - "ISBASEALLUPGRADED", - new[] - { - "/CustomersCaseInsensitive/NS.IsBaseAllUpgraded", - "/CustomersCaseInsensitive/IsBaseAllUpgraded" - } - }, - // overload + "/CustomersCaseInsensitive/NS.IsBaseAllUpgraded", + "/CustomersCaseInsensitive/IsBaseAllUpgraded" + } + }, + // overload + { + typeof(CustomersCaseInsensitiveController), + "UPGRADEDALLOnCUSTOMER", + new[] { - typeof(CustomersCaseInsensitiveController), - "UPGRADEDALLOnCUSTOMER", - new[] - { - "/CustomersCaseInsensitive({key})/NS.UpgradedAll", - "/CustomersCaseInsensitive({key})/UpgradedAll", - "/CustomersCaseInsensitive/{key}/NS.UpgradedAll", - "/CustomersCaseInsensitive/{key}/UpgradedAll" - } - }, + "/CustomersCaseInsensitive({key})/NS.UpgradedAll", + "/CustomersCaseInsensitive({key})/UpgradedAll", + "/CustomersCaseInsensitive/{key}/NS.UpgradedAll", + "/CustomersCaseInsensitive/{key}/UpgradedAll" + } + }, + { + typeof(CustomersCaseInsensitiveController), + "UPGRADEDALLOnCollectionOfCUSTOMER", + new[] { - typeof(CustomersCaseInsensitiveController), - "UPGRADEDALLOnCollectionOfCUSTOMER", - new[] - { - "/CustomersCaseInsensitive/NS.UpgradedAll", - "/CustomersCaseInsensitive/UpgradedAll" - } - }, + "/CustomersCaseInsensitive/NS.UpgradedAll", + "/CustomersCaseInsensitive/UpgradedAll" + } + }, + { + typeof(CustomersCaseInsensitiveController), + "UPGRADEDALLOnCollectionOfVIPCUSTOMER", + new[] { - typeof(CustomersCaseInsensitiveController), - "UPGRADEDALLOnCollectionOfVIPCUSTOMER", - new[] - { - "/CustomersCaseInsensitive/NS.VipCustomer/NS.UpgradedAll", - "/CustomersCaseInsensitive/NS.VipCustomer/UpgradedAll" - } + "/CustomersCaseInsensitive/NS.VipCustomer/NS.UpgradedAll", + "/CustomersCaseInsensitive/NS.VipCustomer/UpgradedAll" } - }; - } + } + }; } + } - [Theory] - [MemberData(nameof(ActionRoutingConventionTestData))] - public void ActionRoutingConventionTestDataRunsAsExpected(Type controllerType, string actionName, string[] templates) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [MemberData(nameof(ActionRoutingConventionTestData))] + public void ActionRoutingConventionTestDataRunsAsExpected(Type controllerType, string actionName, string[] templates) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; - // Act - ActionConvention.AppliesToAction(context); + // Act + ActionConvention.AppliesToAction(context); - // Assert - Assert.Equal(templates.Length, action.Selectors.Count); - Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); - } + // Assert + Assert.Equal(templates.Length, action.Selectors.Count); + Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } - [Theory] - [MemberData(nameof(ActionRoutingConventionCaseInsensitiveTestData))] - [MemberData(nameof(ActionRoutingConventionTestData))] - public void ActionRoutingConventionTestDataWithCaseInsensitiveActionNameRunsAsExpected(Type controllerType, string actionName, string[] templates) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [MemberData(nameof(ActionRoutingConventionCaseInsensitiveTestData))] + [MemberData(nameof(ActionRoutingConventionTestData))] + public void ActionRoutingConventionTestDataWithCaseInsensitiveActionNameRunsAsExpected(Type controllerType, string actionName, string[] templates) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Options.RouteOptions.EnableActionNameCaseInsensitive = true; - context.Action = action; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Options.RouteOptions.EnableActionNameCaseInsensitive = true; + context.Action = action; - // Act - ActionConvention.AppliesToAction(context); + // Act + ActionConvention.AppliesToAction(context); - // Assert - Assert.Equal(templates.Length, action.Selectors.Count); - Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); - } + // Assert + Assert.Equal(templates.Length, action.Selectors.Count); + Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } - [Theory] - [MemberData(nameof(ActionRoutingConventionCaseInsensitiveTestData))] - public void ActionRoutingConventionDoesCaseSensitiveMatchingByDefault(Type controllerType, string actionName, string[] templates) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); - - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; - - // Act - ActionConvention.AppliesToAction(context); - - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Null(selector.AttributeRouteModel); - Assert.NotEmpty(templates); - } - - [Theory] - [InlineData("Post")] - [InlineData("UnknownAction")] - [InlineData("NonSupportedOn")] - [InlineData("NonSupportedOnCollectionOf")] - public void PropertyRoutingConventionDoesNothingForNotSupportedAction(string actionName) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [MemberData(nameof(ActionRoutingConventionCaseInsensitiveTestData))] + public void ActionRoutingConventionDoesCaseSensitiveMatchingByDefault(Type controllerType, string actionName, string[] templates) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); + + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + + // Act + ActionConvention.AppliesToAction(context); + + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Null(selector.AttributeRouteModel); + Assert.NotEmpty(templates); + } + + [Theory] + [InlineData("Post")] + [InlineData("UnknownAction")] + [InlineData("NonSupportedOn")] + [InlineData("NonSupportedOnCollectionOf")] + public void PropertyRoutingConventionDoesNothingForNotSupportedAction(string actionName) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = controller.Actions.First(); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = controller.Actions.First(); - // Act - bool returnValue = ActionConvention.AppliesToAction(context); - Assert.False(returnValue); + // Act + bool returnValue = ActionConvention.AppliesToAction(context); + Assert.False(returnValue); - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Null(selector.AttributeRouteModel); - } - - private static IEdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); - - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmTypeReference stringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); - IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); - - // Entity - EdmEntityType entity = new EdmEntityType("NS", "Entity", null, true, false); - model.AddElement(entity); - - // Customer - EdmEntityType customer = new EdmEntityType("NS", "Customer", entity); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - model.AddElement(customer); - - // VipCustomer - EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); - model.AddElement(vipCustomer); - - // action bound to single - EdmAction isBaseUpgraded = new EdmAction("NS", "IsBaseUpgraded", returnType, true, entitySetPathExpression: null); - isBaseUpgraded.AddParameter("entity", new EdmEntityTypeReference(entity, false)); - isBaseUpgraded.AddParameter("param", stringType); - model.AddElement(isBaseUpgraded); - - EdmAction isUpgraded = new EdmAction("NS", "IsUpgraded", returnType, true, entitySetPathExpression: null); - isUpgraded.AddParameter("entity", new EdmEntityTypeReference(customer, false)); - model.AddElement(isUpgraded); - - EdmAction isVipUpgraded = new EdmAction("NS", "IsVipUpgraded", returnType, true, entitySetPathExpression: null); - isVipUpgraded.AddParameter("entity", new EdmEntityTypeReference(vipCustomer, false)); - isVipUpgraded.AddParameter("param", stringType); - model.AddElement(isVipUpgraded); - - // actions bound to collection - EdmAction isBaseAllUpgraded = new EdmAction("NS", "IsBaseAllUpgraded", returnType, true, entitySetPathExpression: null); - isBaseAllUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(entity, false)))); - isBaseAllUpgraded.AddParameter("param", intType); - model.AddElement(isBaseAllUpgraded); - - EdmAction isAllUpgraded = new EdmAction("NS", "IsAllCustomersUpgraded", returnType, true, entitySetPathExpression: null); - isAllUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - isAllUpgraded.AddParameter("param", intType); - model.AddElement(isAllUpgraded); - - EdmAction isVipAllUpgraded = new EdmAction("NS", "IsVipAllUpgraded", returnType, true, entitySetPathExpression: null); - isVipAllUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(vipCustomer, false)))); - isVipAllUpgraded.AddParameter("param", intType); - model.AddElement(isVipAllUpgraded); - - // overloads - EdmAction upgradeAll1 = new EdmAction("NS", "UpgradedAll", returnType, true, entitySetPathExpression: null); - upgradeAll1.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - upgradeAll1.AddParameter("age", intType); - upgradeAll1.AddParameter("name", stringType); - model.AddElement(upgradeAll1); - - EdmAction upgradeAll2 = new EdmAction("NS", "UpgradedAll", returnType, true, entitySetPathExpression: null); - upgradeAll2.AddParameter("entityset", new EdmEntityTypeReference(customer, false)); - upgradeAll2.AddParameter("age", intType); - upgradeAll2.AddParameter("name", stringType); - upgradeAll2.AddParameter("gender", stringType); - model.AddElement(upgradeAll2); - - EdmAction upgradeAll3 = new EdmAction("NS", "UpgradedAll", returnType, true, entitySetPathExpression: null); - upgradeAll3.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(vipCustomer, false)))); - upgradeAll3.AddParameter("age", intType); - upgradeAll3.AddParameter("name", stringType); - model.AddElement(upgradeAll3); - - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - container.AddEntitySet("Customers", customer); - container.AddEntitySet("CustomersCaseInsensitive", customer); - container.AddSingleton("Me", customer); - model.AddElement(container); - return model; - } + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Null(selector.AttributeRouteModel); + } - private class CustomersController - { - [HttpPost] - public void Post() - { } + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmTypeReference stringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); + IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); + + // Entity + EdmEntityType entity = new EdmEntityType("NS", "Entity", null, true, false); + model.AddElement(entity); + + // Customer + EdmEntityType customer = new EdmEntityType("NS", "Customer", entity); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + model.AddElement(customer); + + // VipCustomer + EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); + model.AddElement(vipCustomer); + + // action bound to single + EdmAction isBaseUpgraded = new EdmAction("NS", "IsBaseUpgraded", returnType, true, entitySetPathExpression: null); + isBaseUpgraded.AddParameter("entity", new EdmEntityTypeReference(entity, false)); + isBaseUpgraded.AddParameter("param", stringType); + model.AddElement(isBaseUpgraded); + + EdmAction isUpgraded = new EdmAction("NS", "IsUpgraded", returnType, true, entitySetPathExpression: null); + isUpgraded.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + model.AddElement(isUpgraded); + + EdmAction isVipUpgraded = new EdmAction("NS", "IsVipUpgraded", returnType, true, entitySetPathExpression: null); + isVipUpgraded.AddParameter("entity", new EdmEntityTypeReference(vipCustomer, false)); + isVipUpgraded.AddParameter("param", stringType); + model.AddElement(isVipUpgraded); + + // actions bound to collection + EdmAction isBaseAllUpgraded = new EdmAction("NS", "IsBaseAllUpgraded", returnType, true, entitySetPathExpression: null); + isBaseAllUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(entity, false)))); + isBaseAllUpgraded.AddParameter("param", intType); + model.AddElement(isBaseAllUpgraded); + + EdmAction isAllUpgraded = new EdmAction("NS", "IsAllCustomersUpgraded", returnType, true, entitySetPathExpression: null); + isAllUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + isAllUpgraded.AddParameter("param", intType); + model.AddElement(isAllUpgraded); + + EdmAction isVipAllUpgraded = new EdmAction("NS", "IsVipAllUpgraded", returnType, true, entitySetPathExpression: null); + isVipAllUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(vipCustomer, false)))); + isVipAllUpgraded.AddParameter("param", intType); + model.AddElement(isVipAllUpgraded); + + // overloads + EdmAction upgradeAll1 = new EdmAction("NS", "UpgradedAll", returnType, true, entitySetPathExpression: null); + upgradeAll1.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + upgradeAll1.AddParameter("age", intType); + upgradeAll1.AddParameter("name", stringType); + model.AddElement(upgradeAll1); + + EdmAction upgradeAll2 = new EdmAction("NS", "UpgradedAll", returnType, true, entitySetPathExpression: null); + upgradeAll2.AddParameter("entityset", new EdmEntityTypeReference(customer, false)); + upgradeAll2.AddParameter("age", intType); + upgradeAll2.AddParameter("name", stringType); + upgradeAll2.AddParameter("gender", stringType); + model.AddElement(upgradeAll2); + + EdmAction upgradeAll3 = new EdmAction("NS", "UpgradedAll", returnType, true, entitySetPathExpression: null); + upgradeAll3.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(vipCustomer, false)))); + upgradeAll3.AddParameter("age", intType); + upgradeAll3.AddParameter("name", stringType); + model.AddElement(upgradeAll3); + + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + container.AddEntitySet("Customers", customer); + container.AddEntitySet("CustomersCaseInsensitive", customer); + container.AddSingleton("Me", customer); + model.AddElement(container); + return model; + } - [HttpPost] - public void IsBaseUpgraded(int key, CancellationToken cancellation, ODataActionParameters parameters) - { } + private class CustomersController + { + [HttpPost] + public void Post() + { } - [HttpPost] - public void IsUpgraded(int key, CancellationToken cancellation) - { } + [HttpPost] + public void IsBaseUpgraded(int key, CancellationToken cancellation, ODataActionParameters parameters) + { } - [HttpPost] - public void IsVipUpgraded(int key, ODataActionParameters parameters) - { } + [HttpPost] + public void IsUpgraded(int key, CancellationToken cancellation) + { } - [HttpPost] - public void IsBaseAllUpgraded(ODataActionParameters parameters) - { } + [HttpPost] + public void IsVipUpgraded(int key, ODataActionParameters parameters) + { } - [HttpPost] - public void IsAllCustomersUpgraded(ODataActionParameters parameters) - { } + [HttpPost] + public void IsBaseAllUpgraded(ODataActionParameters parameters) + { } - [HttpPost] - public void IsVipAllUpgraded(CancellationToken cancellation, ODataUntypedActionParameters parameters) - { } + [HttpPost] + public void IsAllCustomersUpgraded(ODataActionParameters parameters) + { } - [HttpPost] - public void UpgradedAllOnCustomer(int key, ODataActionParameters parameters) - { } + [HttpPost] + public void IsVipAllUpgraded(CancellationToken cancellation, ODataUntypedActionParameters parameters) + { } - [HttpPost] - public void UpgradedAllOnCollectionOfCustomer(ODataActionParameters parameters) - { } + [HttpPost] + public void UpgradedAllOnCustomer(int key, ODataActionParameters parameters) + { } - [HttpPost] - public void UpgradedAllOnCollectionOfVipCustomer(ODataActionParameters parameters) - { } + [HttpPost] + public void UpgradedAllOnCollectionOfCustomer(ODataActionParameters parameters) + { } - [HttpPost] - public void UnknownAction() - { } + [HttpPost] + public void UpgradedAllOnCollectionOfVipCustomer(ODataActionParameters parameters) + { } - [HttpPost] - public void NonSupportedOn(int key, ODataActionParameters parameters) - { - } + [HttpPost] + public void UnknownAction() + { } - [HttpPost] - public void NonSupportedOnCollectionOf(ODataActionParameters parameters) - { - } + [HttpPost] + public void NonSupportedOn(int key, ODataActionParameters parameters) + { } - private class MeController + [HttpPost] + public void NonSupportedOnCollectionOf(ODataActionParameters parameters) { - [HttpPost] - public void IsBaseUpgraded(CancellationToken cancellation, ODataActionParameters parameters) - { } + } + } + + private class MeController + { + [HttpPost] + public void IsBaseUpgraded(CancellationToken cancellation, ODataActionParameters parameters) + { } - [HttpPost] - public void IsUpgraded(CancellationToken cancellation) - { } + [HttpPost] + public void IsUpgraded(CancellationToken cancellation) + { } - [HttpPost] - public void IsVipUpgraded(ODataActionParameters parameters, string param) - { } - } + [HttpPost] + public void IsVipUpgraded(ODataActionParameters parameters, string param) + { } + } - private class CustomersCaseInsensitiveController - { - [HttpPost] - public void ISBASEUPGRADED(int key, CancellationToken cancellation, ODataActionParameters parameters) - { } + private class CustomersCaseInsensitiveController + { + [HttpPost] + public void ISBASEUPGRADED(int key, CancellationToken cancellation, ODataActionParameters parameters) + { } - [HttpPost] - public void ISUPGRADED(int key, CancellationToken cancellation) - { } + [HttpPost] + public void ISUPGRADED(int key, CancellationToken cancellation) + { } - [HttpPost] - public void ISVIPUPGRADED(int key, ODataActionParameters parameters) - { } + [HttpPost] + public void ISVIPUPGRADED(int key, ODataActionParameters parameters) + { } - [HttpPost] - public void ISBASEALLUPGRADED(ODataActionParameters parameters) - { } + [HttpPost] + public void ISBASEALLUPGRADED(ODataActionParameters parameters) + { } - [HttpPost] - public void UPGRADEDALLOnCUSTOMER(int key, ODataActionParameters parameters) - { } + [HttpPost] + public void UPGRADEDALLOnCUSTOMER(int key, ODataActionParameters parameters) + { } - [HttpPost] - public void UPGRADEDALLOnCollectionOfCUSTOMER(ODataActionParameters parameters) - { } + [HttpPost] + public void UPGRADEDALLOnCollectionOfCUSTOMER(ODataActionParameters parameters) + { } - [HttpPost] - public void UPGRADEDALLOnCollectionOfVIPCUSTOMER(ODataActionParameters parameters) - { } - } + [HttpPost] + public void UPGRADEDALLOnCollectionOfVIPCUSTOMER(ODataActionParameters parameters) + { } + } - private class AnotherCustomersController - { } + private class AnotherCustomersController + { } - private class UnknownController - { } - } + private class UnknownController + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/AttributeRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/AttributeRoutingConventionTests.cs index 5040a10fd..80b9ca8ab 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/AttributeRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/AttributeRoutingConventionTests.cs @@ -21,375 +21,374 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +public class AttributeRoutingConventionTests { - public class AttributeRoutingConventionTests + private static IEdmModel _edmModel; + private static ODataOptions _options; + private static AttributeRoutingConvention _attributeConvention; + + static AttributeRoutingConventionTests() { - private static IEdmModel _edmModel; - private static ODataOptions _options; - private static AttributeRoutingConvention _attributeConvention; + _edmModel = GetEdmModel(); + _options = new ODataOptions(); + _options.AddRouteComponents(_edmModel); + _attributeConvention = CreateConvention(); + } - static AttributeRoutingConventionTests() - { - _edmModel = GetEdmModel(); - _options = new ODataOptions(); - _options.AddRouteComponents(_edmModel); - _attributeConvention = CreateConvention(); - } + [Fact] + public void AppliesToControllerOnAttributeRoutingConvention_ReturnsTrue() + { + // Arrange & Act & Assert + Assert.True(_attributeConvention.AppliesToController(null)); + } - [Fact] - public void AppliesToControllerOnAttributeRoutingConvention_ReturnsTrue() - { - // Arrange & Act & Assert - Assert.True(_attributeConvention.AppliesToController(null)); - } + [Fact] + public void AppliesToActionOnAttributeRoutingConvention_Throws_Context() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => _attributeConvention.AppliesToAction(null), "context"); + } - [Fact] - public void AppliesToActionOnAttributeRoutingConvention_Throws_Context() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => _attributeConvention.AppliesToAction(null), "context"); - } + [Fact] + public void AppliesToActionWithoutRoutePrefixWorksAsExpected() + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel("MyAction"); + ActionModel action = controller.Actions.First(); + Assert.Collection(action.Selectors, // Guard + e => + { + Assert.Equal("Customers({key})", e.AttributeRouteModel.Template); + Assert.DoesNotContain(e.EndpointMetadata, a => a is ODataRoutingMetadata); + }, + e => + { + Assert.Equal("Customers/{key}/Name", e.AttributeRouteModel.Template); + Assert.DoesNotContain(e.EndpointMetadata, a => a is ODataRoutingMetadata); + }); - [Fact] - public void AppliesToActionWithoutRoutePrefixWorksAsExpected() + ODataControllerActionContext context = new ODataControllerActionContext(string.Empty, _edmModel, controller); + context.Action = action; + context.Options = _options; + + // Act + bool ok = _attributeConvention.AppliesToAction(context); + Assert.False(ok); + + Assert.Equal(2, action.Selectors.Count); + Assert.Collection(action.Selectors, + e => + { + Assert.Equal("/Customers({key})", e.AttributeRouteModel.Template); + Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); + }, + e => + { + Assert.Equal("/Customers/{key}/Name", e.AttributeRouteModel.Template); + Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); + }); + } + + [Fact] + public void AppliesToActionWithLongTemplateWorksAsExpected() + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel("LongAction"); + ActionModel action = controller.Actions.First(); + + SelectorModel selectorModel = Assert.Single(action.Selectors); // Guard + Assert.Equal("Customers({key})/Orders({relatedKey})/NS.MyOrder/Title", selectorModel.AttributeRouteModel.Template); + Assert.DoesNotContain(selectorModel.EndpointMetadata, a => a is ODataRoutingMetadata); + + ODataControllerActionContext context = new ODataControllerActionContext(string.Empty, _edmModel, controller) { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel("MyAction"); - ActionModel action = controller.Actions.First(); - Assert.Collection(action.Selectors, // Guard - e => - { - Assert.Equal("Customers({key})", e.AttributeRouteModel.Template); - Assert.DoesNotContain(e.EndpointMetadata, a => a is ODataRoutingMetadata); - }, - e => - { - Assert.Equal("Customers/{key}/Name", e.AttributeRouteModel.Template); - Assert.DoesNotContain(e.EndpointMetadata, a => a is ODataRoutingMetadata); - }); - - ODataControllerActionContext context = new ODataControllerActionContext(string.Empty, _edmModel, controller); - context.Action = action; - context.Options = _options; - - // Act - bool ok = _attributeConvention.AppliesToAction(context); - Assert.False(ok); - - Assert.Equal(2, action.Selectors.Count); - Assert.Collection(action.Selectors, - e => - { - Assert.Equal("/Customers({key})", e.AttributeRouteModel.Template); - Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); - }, - e => - { - Assert.Equal("/Customers/{key}/Name", e.AttributeRouteModel.Template); - Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); - }); - } + Action = action, + Options = _options, + }; + + // Act + bool ok = _attributeConvention.AppliesToAction(context); + Assert.False(ok); + + // Assert + SelectorModel actualSelectorModel = Assert.Single(action.Selectors); + Assert.Equal("/Customers({key})/Orders({relatedKey})/NS.MyOrder/Title", actualSelectorModel.AttributeRouteModel.Template); + Assert.Null(actualSelectorModel.AttributeRouteModel.Order); + Assert.Contains(actualSelectorModel.EndpointMetadata, a => a is ODataRoutingMetadata); + } + + [Fact] + public void AppliesToActionWithRoutePrefixWorksAsExpected() + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel("List"); + ActionModel action = controller.Actions.First(); + Assert.Equal(2, action.Selectors.Count); - [Fact] - public void AppliesToActionWithLongTemplateWorksAsExpected() + ODataControllerActionContext context = new ODataControllerActionContext(string.Empty, _edmModel, controller) { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel("LongAction"); - ActionModel action = controller.Actions.First(); + Action = action, + Options = _options, + }; - SelectorModel selectorModel = Assert.Single(action.Selectors); // Guard - Assert.Equal("Customers({key})/Orders({relatedKey})/NS.MyOrder/Title", selectorModel.AttributeRouteModel.Template); - Assert.DoesNotContain(selectorModel.EndpointMetadata, a => a is ODataRoutingMetadata); + AttributeRoutingConvention attributeConvention = CreateConvention(); - ODataControllerActionContext context = new ODataControllerActionContext(string.Empty, _edmModel, controller) + // Act + bool ok = _attributeConvention.AppliesToAction(context); + Assert.False(ok); + + // Assert + Assert.Equal(4, action.Selectors.Count); + Assert.Collection(action.Selectors, + e => { - Action = action, - Options = _options, - }; - - // Act - bool ok = _attributeConvention.AppliesToAction(context); - Assert.False(ok); - - // Assert - SelectorModel actualSelectorModel = Assert.Single(action.Selectors); - Assert.Equal("/Customers({key})/Orders({relatedKey})/NS.MyOrder/Title", actualSelectorModel.AttributeRouteModel.Template); - Assert.Null(actualSelectorModel.AttributeRouteModel.Order); - Assert.Contains(actualSelectorModel.EndpointMetadata, a => a is ODataRoutingMetadata); - } + Assert.Equal("/Customers/{key}", e.AttributeRouteModel.Template); + Assert.Equal(9, e.AttributeRouteModel.Order); + Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); + }, + e => + { + Assert.Equal("/Orders/{key}", e.AttributeRouteModel.Template); + Assert.Equal(9, e.AttributeRouteModel.Order); + Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); + }, + e => + { + Assert.Equal("/Customers", e.AttributeRouteModel.Template); + Assert.Equal(3, e.AttributeRouteModel.Order); + Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); + }, + e => + { + Assert.Equal("/Orders", e.AttributeRouteModel.Template); + Assert.Equal(3, e.AttributeRouteModel.Order); + Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); + }); + } + + [Fact] + public void AppliesToActionWithOrderOnControllerRoutePrefixWorksAsExpected() + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel("List"); + ActionModel action = controller.Actions.First(); + Assert.Equal(2, action.Selectors.Count); - [Fact] - public void AppliesToActionWithRoutePrefixWorksAsExpected() + ODataControllerActionContext context = new ODataControllerActionContext(string.Empty, _edmModel, controller) { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel("List"); - ActionModel action = controller.Actions.First(); - Assert.Equal(2, action.Selectors.Count); + Action = action, + Options = _options, + }; + + AttributeRoutingConvention attributeConvention = CreateConvention(); + + // Act + bool ok = _attributeConvention.AppliesToAction(context); + Assert.False(ok); - ODataControllerActionContext context = new ODataControllerActionContext(string.Empty, _edmModel, controller) + // Assert + Assert.Equal(4, action.Selectors.Count); + Assert.Collection(action.Selectors, + e => { - Action = action, - Options = _options, - }; - - AttributeRoutingConvention attributeConvention = CreateConvention(); - - // Act - bool ok = _attributeConvention.AppliesToAction(context); - Assert.False(ok); - - // Assert - Assert.Equal(4, action.Selectors.Count); - Assert.Collection(action.Selectors, - e => - { - Assert.Equal("/Customers/{key}", e.AttributeRouteModel.Template); - Assert.Equal(9, e.AttributeRouteModel.Order); - Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); - }, - e => - { - Assert.Equal("/Orders/{key}", e.AttributeRouteModel.Template); - Assert.Equal(9, e.AttributeRouteModel.Order); - Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); - }, - e => - { - Assert.Equal("/Customers", e.AttributeRouteModel.Template); - Assert.Equal(3, e.AttributeRouteModel.Order); - Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); - }, - e => - { - Assert.Equal("/Orders", e.AttributeRouteModel.Template); - Assert.Equal(3, e.AttributeRouteModel.Order); - Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); - }); - } + Assert.Equal("/Customers/{key}", e.AttributeRouteModel.Template); + Assert.Equal(9, e.AttributeRouteModel.Order); // Order from controller + Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); + }, + e => + { + Assert.Equal("/Orders/{key}", e.AttributeRouteModel.Template); + Assert.Equal(8, e.AttributeRouteModel.Order); // Order from controller + Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); + }, + e => + { + Assert.Equal("/Customers", e.AttributeRouteModel.Template); + Assert.Equal(3, e.AttributeRouteModel.Order); // Order from action + Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); + }, + e => + { + Assert.Equal("/Orders", e.AttributeRouteModel.Template); + Assert.Equal(3, e.AttributeRouteModel.Order); // Order from action + Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); + }); + } - [Fact] - public void AppliesToActionWithOrderOnControllerRoutePrefixWorksAsExpected() - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel("List"); - ActionModel action = controller.Actions.First(); - Assert.Equal(2, action.Selectors.Count); + [Theory] + [InlineData("GetVipCustomerWithPrefix", "/VipCustomer")] + [InlineData("GetVipCustomerOrdersWithPrefix", "/VipCustomer/Orders")] + [InlineData("GetVipCustomerNameWithPrefix", "/VipCustomer/Name")] + public void AppliesToActionForSingletonWorksAsExpected(string actionName, string expectedTemplate) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); + + ODataControllerActionContext context = new ODataControllerActionContext(string.Empty, _edmModel, controller); + context.Action = action; + context.Options = _options; + AttributeRoutingConvention attributeConvention = CreateConvention(); + + // Act + bool ok = _attributeConvention.AppliesToAction(context); + Assert.False(ok); + + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.NotNull(selector.AttributeRouteModel); + Assert.Equal(expectedTemplate, selector.AttributeRouteModel.Template); + Assert.Contains(selector.EndpointMetadata, a => a is ODataRoutingMetadata); + } + + [Fact] + public void AppliesToActionForSingletonDoesnotWorksAsExpected() + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel("GetVipCustomerAliasWithPrefix"); + ActionModel action = controller.Actions.First(); + + ODataControllerActionContext context = new ODataControllerActionContext(string.Empty, _edmModel, controller); + context.Action = action; + context.Options = _options; + + // Act + bool ok = _attributeConvention.AppliesToAction(context); + Assert.False(ok); + + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.NotNull(selector.AttributeRouteModel); + Assert.Equal("Alias", selector.AttributeRouteModel.Template); + Assert.DoesNotContain(selector.EndpointMetadata, a => a is ODataRoutingMetadata); + } + + private static AttributeRoutingConvention CreateConvention() + { + var services = new ServiceCollection() + .AddLogging(); - ODataControllerActionContext context = new ODataControllerActionContext(string.Empty, _edmModel, controller) + services.AddSingleton(); + services.AddSingleton(); + + return services.BuildServiceProvider().GetRequiredService(); + } + + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + + // Customer + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.String)); + customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + customer.AddStructuralProperty("Alias", EdmPrimitiveTypeKind.String); + model.AddElement(customer); + + // Order + EdmEntityType order = new EdmEntityType("NS", "Order", null, false, true); + order.AddKeys(order.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + model.AddElement(order); + + // MyOrder + EdmEntityType myOrder = new EdmEntityType("NS", "MyOrder", order); + myOrder.AddKeys(myOrder.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + myOrder.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); + model.AddElement(myOrder); + + EdmNavigationProperty ordersNavProp = customer.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo { - Action = action, - Options = _options, - }; - - AttributeRoutingConvention attributeConvention = CreateConvention(); - - // Act - bool ok = _attributeConvention.AppliesToAction(context); - Assert.False(ok); - - // Assert - Assert.Equal(4, action.Selectors.Count); - Assert.Collection(action.Selectors, - e => - { - Assert.Equal("/Customers/{key}", e.AttributeRouteModel.Template); - Assert.Equal(9, e.AttributeRouteModel.Order); // Order from controller - Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); - }, - e => - { - Assert.Equal("/Orders/{key}", e.AttributeRouteModel.Template); - Assert.Equal(8, e.AttributeRouteModel.Order); // Order from controller - Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); - }, - e => - { - Assert.Equal("/Customers", e.AttributeRouteModel.Template); - Assert.Equal(3, e.AttributeRouteModel.Order); // Order from action - Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); - }, - e => - { - Assert.Equal("/Orders", e.AttributeRouteModel.Template); - Assert.Equal(3, e.AttributeRouteModel.Order); // Order from action - Assert.Contains(e.EndpointMetadata, a => a is ODataRoutingMetadata); - }); - } + Name = "Orders", + TargetMultiplicity = EdmMultiplicity.Many, + Target = order + }); + + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + EdmEntitySet orders = container.AddEntitySet("Orders", order); + EdmEntitySet customers = container.AddEntitySet("Customers", customer); + customers.AddNavigationTarget(ordersNavProp, orders); + EdmSingleton vipCustomer = container.AddSingleton("VipCustomer", customer); + vipCustomer.AddNavigationTarget(ordersNavProp, orders); + + model.AddElement(container); + return model; + } - [Theory] - [InlineData("GetVipCustomerWithPrefix", "/VipCustomer")] - [InlineData("GetVipCustomerOrdersWithPrefix", "/VipCustomer/Orders")] - [InlineData("GetVipCustomerNameWithPrefix", "/VipCustomer/Name")] - public void AppliesToActionForSingletonWorksAsExpected(string actionName, string expectedTemplate) + private class WithoutPrefixController : ODataController + { + [HttpGet("Customers({key})")] + [Route("Customers/{key}/Name")] + [HttpPost] + [HttpPatch] + public void MyAction(int key) { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); - - ODataControllerActionContext context = new ODataControllerActionContext(string.Empty, _edmModel, controller); - context.Action = action; - context.Options = _options; - AttributeRoutingConvention attributeConvention = CreateConvention(); - - // Act - bool ok = _attributeConvention.AppliesToAction(context); - Assert.False(ok); - - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.NotNull(selector.AttributeRouteModel); - Assert.Equal(expectedTemplate, selector.AttributeRouteModel.Template); - Assert.Contains(selector.EndpointMetadata, a => a is ODataRoutingMetadata); } - [Fact] - public void AppliesToActionForSingletonDoesnotWorksAsExpected() + [HttpGet("Customers({key})/Orders({relatedKey})/NS.MyOrder/Title")] + public void LongAction() { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel("GetVipCustomerAliasWithPrefix"); - ActionModel action = controller.Actions.First(); - - ODataControllerActionContext context = new ODataControllerActionContext(string.Empty, _edmModel, controller); - context.Action = action; - context.Options = _options; - - // Act - bool ok = _attributeConvention.AppliesToAction(context); - Assert.False(ok); - - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.NotNull(selector.AttributeRouteModel); - Assert.Equal("Alias", selector.AttributeRouteModel.Template); - Assert.DoesNotContain(selector.EndpointMetadata, a => a is ODataRoutingMetadata); } + } - private static AttributeRoutingConvention CreateConvention() + [ODataAttributeRouting] // using this attribute if not derived from ODataController + [Route("Customers")] + [Route("Orders")] + private class WithPrefixController + { + [HttpGet("{key}", Order = 9)] + [HttpPost("", Order = 3)] + public void List(int key) { - var services = new ServiceCollection() - .AddLogging(); - - services.AddSingleton(); - services.AddSingleton(); - - return services.BuildServiceProvider().GetRequiredService(); } + } - private static IEdmModel GetEdmModel() + [ODataAttributeRouting] // using this attribute if not derived from ODataController + [Route("Customers", Order = 9)] + [Route("Orders", Order = 8)] + private class WithPrefixController2 + { + [HttpGet("{key}")] + [HttpPost("", Order = 3)] // 3 should override 8 on controller + public void List() { - EdmModel model = new EdmModel(); - - // Customer - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("ID", EdmPrimitiveTypeKind.String)); - customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - customer.AddStructuralProperty("Alias", EdmPrimitiveTypeKind.String); - model.AddElement(customer); - - // Order - EdmEntityType order = new EdmEntityType("NS", "Order", null, false, true); - order.AddKeys(order.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - model.AddElement(order); - - // MyOrder - EdmEntityType myOrder = new EdmEntityType("NS", "MyOrder", order); - myOrder.AddKeys(myOrder.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - myOrder.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); - model.AddElement(myOrder); - - EdmNavigationProperty ordersNavProp = customer.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "Orders", - TargetMultiplicity = EdmMultiplicity.Many, - Target = order - }); - - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - EdmEntitySet orders = container.AddEntitySet("Orders", order); - EdmEntitySet customers = container.AddEntitySet("Customers", customer); - customers.AddNavigationTarget(ordersNavProp, orders); - EdmSingleton vipCustomer = container.AddSingleton("VipCustomer", customer); - vipCustomer.AddNavigationTarget(ordersNavProp, orders); - - model.AddElement(container); - return model; } + } - private class WithoutPrefixController : ODataController + [Route("VipCustomer")] + public class SingletonTestControllerWithPrefix + { + [ODataAttributeRouting] + [HttpGet("")] + public void GetVipCustomerWithPrefix() { - [HttpGet("Customers({key})")] - [Route("Customers/{key}/Name")] - [HttpPost] - [HttpPatch] - public void MyAction(int key) - { - } - - [HttpGet("Customers({key})/Orders({relatedKey})/NS.MyOrder/Title")] - public void LongAction() - { - } } - [ODataAttributeRouting] // using this attribute if not derived from ODataController - [Route("Customers")] - [Route("Orders")] - private class WithPrefixController + [ODataAttributeRouting] + [HttpPost("Orders")] + public void GetVipCustomerOrdersWithPrefix() { - [HttpGet("{key}", Order = 9)] - [HttpPost("", Order = 3)] - public void List(int key) - { - } } - [ODataAttributeRouting] // using this attribute if not derived from ODataController - [Route("Customers", Order = 9)] - [Route("Orders", Order = 8)] - private class WithPrefixController2 + [ODataAttributeRouting] + [HttpGet("Name")] + public void GetVipCustomerNameWithPrefix() { - [HttpGet("{key}")] - [HttpPost("", Order = 3)] // 3 should override 8 on controller - public void List() - { - } } - [Route("VipCustomer")] - public class SingletonTestControllerWithPrefix + [HttpGet("Alias")] + public void GetVipCustomerAliasWithPrefix() { - [ODataAttributeRouting] - [HttpGet("")] - public void GetVipCustomerWithPrefix() - { - } - - [ODataAttributeRouting] - [HttpPost("Orders")] - public void GetVipCustomerOrdersWithPrefix() - { - } - - [ODataAttributeRouting] - [HttpGet("Name")] - public void GetVipCustomerNameWithPrefix() - { - } - - [HttpGet("Alias")] - public void GetVipCustomerAliasWithPrefix() - { - } } + } - private class AttributeRoutingConventionTestPathTemplateParser : IODataPathTemplateParser + private class AttributeRoutingConventionTestPathTemplateParser : IODataPathTemplateParser + { + public ODataPathTemplate Parse(IEdmModel model, string odataPath, IServiceProvider requestProvider) { - public ODataPathTemplate Parse(IEdmModel model, string odataPath, IServiceProvider requestProvider) - { - return new ODataPathTemplate(); - } + return new ODataPathTemplate(); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ConventionHelpers.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ConventionHelpers.cs index b6e06a191..1f4791027 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ConventionHelpers.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ConventionHelpers.cs @@ -8,17 +8,16 @@ using Microsoft.AspNetCore.OData.Routing.Conventions; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +internal static class ConventionHelpers { - internal static class ConventionHelpers + public static T CreateConvention() where T: class, IODataControllerActionConvention { - public static T CreateConvention() where T: class, IODataControllerActionConvention - { - var services = new ServiceCollection() - .AddLogging(); + var services = new ServiceCollection() + .AddLogging(); - services.AddSingleton(); - return services.BuildServiceProvider().GetRequiredService(); - } + services.AddSingleton(); + return services.BuildServiceProvider().GetRequiredService(); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntityRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntityRoutingConventionTests.cs index 2c6db35a2..ddc374b83 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntityRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntityRoutingConventionTests.cs @@ -15,352 +15,351 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +public class EntityRoutingConventionTests { - public class EntityRoutingConventionTests + private static IEdmModel EdmModel = GetEdmModel(); + + [Fact] + public void AppliesToControllerAndActionOnEntityRoutingConvention_Throws_Context() + { + // Arrange + EntityRoutingConvention convention = new EntityRoutingConvention(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToController(null), "context"); + ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToAction(null), "context"); + } + + [Theory] + [InlineData(typeof(CustomersController), true)] + [InlineData(typeof(AnotherCustomersController), true)] + [InlineData(typeof(UnknownController), false)] + public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); + + // Act + bool actual = entityConvention.AppliesToController(context); + + // Assert + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("Get")] + [InlineData("Put")] + [InlineData("Patch")] + [InlineData("Delete")] + public void AppliesToActionForBasicEntityActionWorksAsExpected(string actionName) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); + + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + + EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); + + // Act + bool returnValue = entityConvention.AppliesToAction(context); + Assert.True(returnValue); + + // Assert + Assert.Equal(2, action.Selectors.Count); + Assert.Collection(action.Selectors, + e => + { + Assert.Equal("/Customers(FirstName={keyFirstName},LastName={keyLastName})", e.AttributeRouteModel.Template); + }, + e => + { + Assert.Equal("/Customers/FirstName={keyFirstName},LastName={keyLastName}", e.AttributeRouteModel.Template); + }); + } + + [Theory] + [InlineData("GetCustomer")] + [InlineData("PutCustomer")] + [InlineData("PatchCustomer")] + [InlineData("DeleteCustomer")] + public void AppliesToActionForEntityActionWithEntityTypeNameSameAsEntityTypeOnEntitySetWorksAsExpected(string actionName) { - private static IEdmModel EdmModel = GetEdmModel(); - - [Fact] - public void AppliesToControllerAndActionOnEntityRoutingConvention_Throws_Context() - { - // Arrange - EntityRoutingConvention convention = new EntityRoutingConvention(); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToController(null), "context"); - ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToAction(null), "context"); - } - - [Theory] - [InlineData(typeof(CustomersController), true)] - [InlineData(typeof(AnotherCustomersController), true)] - [InlineData(typeof(UnknownController), false)] - public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); - - // Act - bool actual = entityConvention.AppliesToController(context); - - // Assert - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData("Get")] - [InlineData("Put")] - [InlineData("Patch")] - [InlineData("Delete")] - public void AppliesToActionForBasicEntityActionWorksAsExpected(string actionName) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); - - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; - - EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); - - // Act - bool returnValue = entityConvention.AppliesToAction(context); - Assert.True(returnValue); - - // Assert - Assert.Equal(2, action.Selectors.Count); - Assert.Collection(action.Selectors, - e => - { - Assert.Equal("/Customers(FirstName={keyFirstName},LastName={keyLastName})", e.AttributeRouteModel.Template); - }, - e => - { - Assert.Equal("/Customers/FirstName={keyFirstName},LastName={keyLastName}", e.AttributeRouteModel.Template); - }); - } - - [Theory] - [InlineData("GetCustomer")] - [InlineData("PutCustomer")] - [InlineData("PatchCustomer")] - [InlineData("DeleteCustomer")] - public void AppliesToActionForEntityActionWithEntityTypeNameSameAsEntityTypeOnEntitySetWorksAsExpected(string actionName) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); - - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; - - EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); - - // Act - bool returnValue = entityConvention.AppliesToAction(context); - Assert.True(returnValue); - - // Assert - Assert.Equal(4, action.Selectors.Count); - Assert.Collection(action.Selectors, - e => - { - Assert.Equal("/Customers(FirstName={keyFirstName},LastName={keyLastName})", e.AttributeRouteModel.Template); - }, - e => - { - Assert.Equal("/Customers/FirstName={keyFirstName},LastName={keyLastName}", e.AttributeRouteModel.Template); - }, - e => - { - Assert.Equal("/Customers(FirstName={keyFirstName},LastName={keyLastName})/NS.Customer", e.AttributeRouteModel.Template); - }, - e => - { - Assert.Equal("/Customers/FirstName={keyFirstName},LastName={keyLastName}/NS.Customer", e.AttributeRouteModel.Template); - }); - } + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); + + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + + EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); + + // Act + bool returnValue = entityConvention.AppliesToAction(context); + Assert.True(returnValue); + + // Assert + Assert.Equal(4, action.Selectors.Count); + Assert.Collection(action.Selectors, + e => + { + Assert.Equal("/Customers(FirstName={keyFirstName},LastName={keyLastName})", e.AttributeRouteModel.Template); + }, + e => + { + Assert.Equal("/Customers/FirstName={keyFirstName},LastName={keyLastName}", e.AttributeRouteModel.Template); + }, + e => + { + Assert.Equal("/Customers(FirstName={keyFirstName},LastName={keyLastName})/NS.Customer", e.AttributeRouteModel.Template); + }, + e => + { + Assert.Equal("/Customers/FirstName={keyFirstName},LastName={keyLastName}/NS.Customer", e.AttributeRouteModel.Template); + }); + } - [Fact] - public void AppliesToActionForEntityActionWithEntityTypeNameSameAsEntityTypeOnEntitySetWorksAndCaseInsensitiveAsExpected() - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel("GetCUSTOMER"); - ActionModel action = controller.Actions.First(); - - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; - context.Options.RouteOptions.EnableActionNameCaseInsensitive = true; - - EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); - - // Act - bool returnValue = entityConvention.AppliesToAction(context); - Assert.True(returnValue); - - // Assert - Assert.Equal(4, action.Selectors.Count); - Assert.Collection(action.Selectors, - e => - { - Assert.Equal("/CaseInsensitiveCustomers(FirstName={keyFirstName},LastName={keyLastName})", e.AttributeRouteModel.Template); - }, - e => - { - Assert.Equal("/CaseInsensitiveCustomers/FirstName={keyFirstName},LastName={keyLastName}", e.AttributeRouteModel.Template); - }, - e => - { - Assert.Equal("/CaseInsensitiveCustomers(FirstName={keyFirstName},LastName={keyLastName})/NS.Customer", e.AttributeRouteModel.Template); - }, - e => - { - Assert.Equal("/CaseInsensitiveCustomers/FirstName={keyFirstName},LastName={keyLastName}/NS.Customer", e.AttributeRouteModel.Template); - }); - } - - [Theory] - [InlineData("GetVipCustomer")] - [InlineData("PutVipCustomer")] - [InlineData("PatchVipCustomer")] - [InlineData("DeleteVipCustomer")] - public void AppliesToActionForEntityActionWithDerivedEntityTypeWorksAsExpected(string actionName) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); - - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; - - EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); - - // Act - bool returnValue = entityConvention.AppliesToAction(context); - Assert.True(returnValue); - - // Assert - Assert.Equal(2, action.Selectors.Count); - Assert.Collection(action.Selectors, - e => - { - Assert.Equal("/Customers(FirstName={keyFirstName},LastName={keyLastName})/NS.VipCustomer", e.AttributeRouteModel.Template); - }, - e => - { - Assert.Equal("/Customers/FirstName={keyFirstName},LastName={keyLastName}/NS.VipCustomer", e.AttributeRouteModel.Template); - }); - } + [Fact] + public void AppliesToActionForEntityActionWithEntityTypeNameSameAsEntityTypeOnEntitySetWorksAndCaseInsensitiveAsExpected() + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel("GetCUSTOMER"); + ActionModel action = controller.Actions.First(); + + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + context.Options.RouteOptions.EnableActionNameCaseInsensitive = true; + + EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); + + // Act + bool returnValue = entityConvention.AppliesToAction(context); + Assert.True(returnValue); + + // Assert + Assert.Equal(4, action.Selectors.Count); + Assert.Collection(action.Selectors, + e => + { + Assert.Equal("/CaseInsensitiveCustomers(FirstName={keyFirstName},LastName={keyLastName})", e.AttributeRouteModel.Template); + }, + e => + { + Assert.Equal("/CaseInsensitiveCustomers/FirstName={keyFirstName},LastName={keyLastName}", e.AttributeRouteModel.Template); + }, + e => + { + Assert.Equal("/CaseInsensitiveCustomers(FirstName={keyFirstName},LastName={keyLastName})/NS.Customer", e.AttributeRouteModel.Template); + }, + e => + { + Assert.Equal("/CaseInsensitiveCustomers/FirstName={keyFirstName},LastName={keyLastName}/NS.Customer", e.AttributeRouteModel.Template); + }); + } + + [Theory] + [InlineData("GetVipCustomer")] + [InlineData("PutVipCustomer")] + [InlineData("PatchVipCustomer")] + [InlineData("DeleteVipCustomer")] + public void AppliesToActionForEntityActionWithDerivedEntityTypeWorksAsExpected(string actionName) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); + + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + + EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); + + // Act + bool returnValue = entityConvention.AppliesToAction(context); + Assert.True(returnValue); + + // Assert + Assert.Equal(2, action.Selectors.Count); + Assert.Collection(action.Selectors, + e => + { + Assert.Equal("/Customers(FirstName={keyFirstName},LastName={keyLastName})/NS.VipCustomer", e.AttributeRouteModel.Template); + }, + e => + { + Assert.Equal("/Customers/FirstName={keyFirstName},LastName={keyLastName}/NS.VipCustomer", e.AttributeRouteModel.Template); + }); + } - [Fact] - public void AppliesToActionForEntityActionWithDerivedEntityTypeAndCaseInsensitiveWorksAsExpected() - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel("GetVIPCUSTOMER"); - ActionModel action = controller.Actions.First(); - - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; - context.Options.RouteOptions.EnableActionNameCaseInsensitive = true; - - EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); - - // Act - bool returnValue = entityConvention.AppliesToAction(context); - Assert.True(returnValue); - - // Assert - Assert.Equal(2, action.Selectors.Count); - Assert.Collection(action.Selectors, - e => - { - Assert.Equal("/CaseInsensitiveCustomers(FirstName={keyFirstName},LastName={keyLastName})/NS.VipCustomer", e.AttributeRouteModel.Template); - }, - e => - { - Assert.Equal("/CaseInsensitiveCustomers/FirstName={keyFirstName},LastName={keyLastName}/NS.VipCustomer", e.AttributeRouteModel.Template); - }); - } - - [Theory] - [InlineData("Post")] - [InlineData("Get")] - public void AppliesToActionDoesNothingForNotSupportedAction(string actionName) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); - - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = controller.Actions.First(); - - EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); - - // Act - bool returnValue = entityConvention.AppliesToAction(context); - Assert.False(returnValue); - - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Null(selector.AttributeRouteModel); - } - - #region Split Tests - [Theory] - [InlineData("Post", null, null)] - [InlineData("Get", "Get", null)] - [InlineData("GetVipCustomer", "Get", "VipCustomer")] - [InlineData("Put", "Put", null)] - [InlineData("PutVipCustomer", "Put", "VipCustomer")] - [InlineData("Patch", "Patch", null)] - [InlineData("PatchVipCustomer", "Patch", "VipCustomer")] - [InlineData("Delete", "Delete", null)] - [InlineData("DeleteVipCustomer", "Delete", "VipCustomer")] - public void SplitActionNameForEntity_Works(string actionName, string expectMethod, string expectCast) - { - // Arrange - (string httpMethod, string castTypeName) = EntityRoutingConvention.Split(actionName); - - // Act & Assert - Assert.Equal(expectMethod, httpMethod); - Assert.Equal(expectCast, castTypeName); - } - #endregion + [Fact] + public void AppliesToActionForEntityActionWithDerivedEntityTypeAndCaseInsensitiveWorksAsExpected() + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel("GetVIPCUSTOMER"); + ActionModel action = controller.Actions.First(); + + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + context.Options.RouteOptions.EnableActionNameCaseInsensitive = true; + + EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); + + // Act + bool returnValue = entityConvention.AppliesToAction(context); + Assert.True(returnValue); + + // Assert + Assert.Equal(2, action.Selectors.Count); + Assert.Collection(action.Selectors, + e => + { + Assert.Equal("/CaseInsensitiveCustomers(FirstName={keyFirstName},LastName={keyLastName})/NS.VipCustomer", e.AttributeRouteModel.Template); + }, + e => + { + Assert.Equal("/CaseInsensitiveCustomers/FirstName={keyFirstName},LastName={keyLastName}/NS.VipCustomer", e.AttributeRouteModel.Template); + }); + } - private static IEdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); + [Theory] + [InlineData("Post")] + [InlineData("Get")] + public void AppliesToActionDoesNothingForNotSupportedAction(string actionName) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); - // Customer - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("FirstName", EdmPrimitiveTypeKind.String), - customer.AddStructuralProperty("LastName", EdmPrimitiveTypeKind.String)); - model.AddElement(customer); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = controller.Actions.First(); - // VipCustomer - EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); - model.AddElement(vipCustomer); + EntityRoutingConvention entityConvention = ConventionHelpers.CreateConvention(); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - container.AddEntitySet("Customers", customer); - container.AddEntitySet("AnotherCustomers", customer); - container.AddEntitySet("CaseInsensitiveCustomers", customer); - model.AddElement(container); - return model; - } + // Act + bool returnValue = entityConvention.AppliesToAction(context); + Assert.False(returnValue); - private class CustomersController - { - #region Basic Action Names - public void Get(string keyLastName, string keyFirstName, CancellationToken cancellation) - { } + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Null(selector.AttributeRouteModel); + } - public void Put(string keyLastName, string keyFirstName) - { } + #region Split Tests + [Theory] + [InlineData("Post", null, null)] + [InlineData("Get", "Get", null)] + [InlineData("GetVipCustomer", "Get", "VipCustomer")] + [InlineData("Put", "Put", null)] + [InlineData("PutVipCustomer", "Put", "VipCustomer")] + [InlineData("Patch", "Patch", null)] + [InlineData("PatchVipCustomer", "Patch", "VipCustomer")] + [InlineData("Delete", "Delete", null)] + [InlineData("DeleteVipCustomer", "Delete", "VipCustomer")] + public void SplitActionNameForEntity_Works(string actionName, string expectMethod, string expectCast) + { + // Arrange + (string httpMethod, string castTypeName) = EntityRoutingConvention.Split(actionName); - public void Patch(string keyLastName, string keyFirstName, CancellationToken cancellation) - { } + // Act & Assert + Assert.Equal(expectMethod, httpMethod); + Assert.Equal(expectCast, castTypeName); + } + #endregion - public void Delete(string keyLastName, string keyFirstName) - { } - #endregion + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + + // Customer + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("FirstName", EdmPrimitiveTypeKind.String), + customer.AddStructuralProperty("LastName", EdmPrimitiveTypeKind.String)); + model.AddElement(customer); + + // VipCustomer + EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); + model.AddElement(vipCustomer); + + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + container.AddEntitySet("Customers", customer); + container.AddEntitySet("AnotherCustomers", customer); + container.AddEntitySet("CaseInsensitiveCustomers", customer); + model.AddElement(container); + return model; + } - #region Action Name with EntityType of entity set - public void GetCustomer(string keyLastName, string keyFirstName, CancellationToken cancellation) - { } + private class CustomersController + { + #region Basic Action Names + public void Get(string keyLastName, string keyFirstName, CancellationToken cancellation) + { } - public void PutCustomer(string keyLastName, string keyFirstName) - { } + public void Put(string keyLastName, string keyFirstName) + { } - public void PatchCustomer(string keyLastName, string keyFirstName) - { } + public void Patch(string keyLastName, string keyFirstName, CancellationToken cancellation) + { } - public void DeleteCustomer(string keyLastName, string keyFirstName, CancellationToken cancellation) - { } + public void Delete(string keyLastName, string keyFirstName) + { } + #endregion - #endregion + #region Action Name with EntityType of entity set + public void GetCustomer(string keyLastName, string keyFirstName, CancellationToken cancellation) + { } - #region Action Name with derived entity type - public void GetVipCustomer(string keyLastName, string keyFirstName, CancellationToken cancellation) - { } + public void PutCustomer(string keyLastName, string keyFirstName) + { } - public void PutVipCustomer(string keyLastName, string keyFirstName, CancellationToken cancellation) - { } + public void PatchCustomer(string keyLastName, string keyFirstName) + { } - public void PatchVipCustomer(CancellationToken cancellation, string keyLastName, string keyFirstName) - { } + public void DeleteCustomer(string keyLastName, string keyFirstName, CancellationToken cancellation) + { } - public void DeleteVipCustomer(string keyFirstName, string keyLastName, CancellationToken cancellation) - { } - #endregion - } + #endregion - private class AnotherCustomersController - { - public void Post(string keyLastName, string keyFirstName) - { } + #region Action Name with derived entity type + public void GetVipCustomer(string keyLastName, string keyFirstName, CancellationToken cancellation) + { } - public void Get(int key) - { } - } - - private class CaseInsensitiveCustomersController - { - #region Action Name with EntityType of entity set - public void GetCUSTOMER(string keyLastName, string keyFirstName, CancellationToken cancellation) - { } - #endregion - - #region Action Name with derived entity type - public void GetVIPCUSTOMER(string keyLastName, string keyFirstName, CancellationToken cancellation) - { } - #endregion - } - - private class UnknownController + public void PutVipCustomer(string keyLastName, string keyFirstName, CancellationToken cancellation) + { } + + public void PatchVipCustomer(CancellationToken cancellation, string keyLastName, string keyFirstName) + { } + + public void DeleteVipCustomer(string keyFirstName, string keyLastName, CancellationToken cancellation) + { } + #endregion + } + + private class AnotherCustomersController + { + public void Post(string keyLastName, string keyFirstName) + { } + + public void Get(int key) { } } + + private class CaseInsensitiveCustomersController + { + #region Action Name with EntityType of entity set + public void GetCUSTOMER(string keyLastName, string keyFirstName, CancellationToken cancellation) + { } + #endregion + + #region Action Name with derived entity type + public void GetVIPCUSTOMER(string keyLastName, string keyFirstName, CancellationToken cancellation) + { } + #endregion + } + + private class UnknownController + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntitySetRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntitySetRoutingConventionTests.cs index 4f5ea539f..d6c88ac1d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntitySetRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/EntitySetRoutingConventionTests.cs @@ -14,286 +14,285 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +public class EntitySetRoutingConventionTests { - public class EntitySetRoutingConventionTests + private static IEdmModel EdmModel = GetEdmModel(); + + [Fact] + public void AppliesToControllerAndActionOnEntitySetRoutingConvention_Throws_Context() { - private static IEdmModel EdmModel = GetEdmModel(); + // Arrange + EntitySetRoutingConvention convention = new EntitySetRoutingConvention(); - [Fact] - public void AppliesToControllerAndActionOnEntitySetRoutingConvention_Throws_Context() - { - // Arrange - EntitySetRoutingConvention convention = new EntitySetRoutingConvention(); + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToController(null), "context"); + ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToAction(null), "context"); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToController(null), "context"); - ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToAction(null), "context"); - } + [Theory] + [InlineData(typeof(CustomersController), true)] + [InlineData(typeof(AnotherCustomersController), true)] + [InlineData(typeof(UnknownController), false)] + public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); - [Theory] - [InlineData(typeof(CustomersController), true)] - [InlineData(typeof(AnotherCustomersController), true)] - [InlineData(typeof(UnknownController), false)] - public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); + // Act + bool actual = entitySetConvention.AppliesToController(context); - // Act - bool actual = entitySetConvention.AppliesToController(context); + // Assert + Assert.Equal(expected, actual); + } - // Assert - Assert.Equal(expected, actual); - } + [Theory] + [InlineData(typeof(CustomersController), "Get", "Customers", false)] + [InlineData(typeof(CustomersController), "GetCustomersFromVipCustomer", "Customers/NS.VipCustomer", false)] + [InlineData(typeof(CaseInsensitiveCustomersController), "GET", "CaseInsensitiveCustomers", true)] + [InlineData(typeof(CaseInsensitiveCustomersController), "GETCASEINSENSITIVECUSTOMERSFromVIPCUSTOMER", "CaseInsensitiveCustomers/NS.VipCustomer", true)] + public void AppliesToActionForGetActionWorksAsExpected(Type controllerType, string actionName, string expectedTemplate, bool ignoreCase) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); - [Theory] - [InlineData(typeof(CustomersController), "Get", "Customers", false)] - [InlineData(typeof(CustomersController), "GetCustomersFromVipCustomer", "Customers/NS.VipCustomer", false)] - [InlineData(typeof(CaseInsensitiveCustomersController), "GET", "CaseInsensitiveCustomers", true)] - [InlineData(typeof(CaseInsensitiveCustomersController), "GETCASEINSENSITIVECUSTOMERSFromVIPCUSTOMER", "CaseInsensitiveCustomers/NS.VipCustomer", true)] - public void AppliesToActionForGetActionWorksAsExpected(Type controllerType, string actionName, string expectedTemplate, bool ignoreCase) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); - - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; - context.Options.RouteOptions.EnableActionNameCaseInsensitive = ignoreCase; - - EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); - - // Act - bool returnValue = entitySetConvention.AppliesToAction(context); - Assert.True(returnValue); - - // Assert - Assert.Equal(2, action.Selectors.Count); - Assert.Equal(new[] - { - $"/{expectedTemplate}", - $"/{expectedTemplate}/$count" - }, - action.Selectors.Select(s => s.AttributeRouteModel.Template)); - } + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + context.Options.RouteOptions.EnableActionNameCaseInsensitive = ignoreCase; - [Theory] - [InlineData(typeof(CustomersController), "Get", 1, false)] - [InlineData(typeof(CustomersController), "Get", 2, true)] - [InlineData(typeof(CustomersController), "GetCustomersFromVipCustomer", 1, false)] - [InlineData(typeof(CustomersController), "GetCustomersFromVipCustomer", 2, true)] - public void AppliesToActionForGetActionAddDollarCountAsExpectedBasedOnConfiguration(Type controllerType, string actionName, int expectCount, bool enableDollarCount) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); + EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; - context.Options.RouteOptions.EnableDollarCountRouting = enableDollarCount; + // Act + bool returnValue = entitySetConvention.AppliesToAction(context); + Assert.True(returnValue); - EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); + // Assert + Assert.Equal(2, action.Selectors.Count); + Assert.Equal(new[] + { + $"/{expectedTemplate}", + $"/{expectedTemplate}/$count" + }, + action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } - // Act - bool returnValue = entitySetConvention.AppliesToAction(context); - Assert.True(returnValue); + [Theory] + [InlineData(typeof(CustomersController), "Get", 1, false)] + [InlineData(typeof(CustomersController), "Get", 2, true)] + [InlineData(typeof(CustomersController), "GetCustomersFromVipCustomer", 1, false)] + [InlineData(typeof(CustomersController), "GetCustomersFromVipCustomer", 2, true)] + public void AppliesToActionForGetActionAddDollarCountAsExpectedBasedOnConfiguration(Type controllerType, string actionName, int expectCount, bool enableDollarCount) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); - // Assert - Assert.Equal(expectCount, action.Selectors.Count); - if (enableDollarCount) - { - Assert.Contains("/$count", string.Join(",", action.Selectors.Select(s => s.AttributeRouteModel.Template))); - } - else - { - Assert.DoesNotContain("/$count", string.Join(",", action.Selectors.Select(s => s.AttributeRouteModel.Template))); - } - } + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + context.Options.RouteOptions.EnableDollarCountRouting = enableDollarCount; + + EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); - [Theory] - [InlineData(typeof(CustomersController), "Post", "/Customers", false)] - [InlineData(typeof(CustomersController), "PostFromVipCustomer", "/Customers/NS.VipCustomer", false)] - [InlineData(typeof(CaseInsensitiveCustomersController), "POST", "/CaseInsensitiveCustomers", true)] - [InlineData(typeof(CaseInsensitiveCustomersController), "POSTFromVIPCUSTOMER", "/CaseInsensitiveCustomers/NS.VipCustomer", true)] - public void AppliesToActionForPostActionWorksAsExpected(Type controllerType, string actionName, string expected, bool ignoreCase) + // Act + bool returnValue = entitySetConvention.AppliesToAction(context); + Assert.True(returnValue); + + // Assert + Assert.Equal(expectCount, action.Selectors.Count); + if (enableDollarCount) { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); + Assert.Contains("/$count", string.Join(",", action.Selectors.Select(s => s.AttributeRouteModel.Template))); + } + else + { + Assert.DoesNotContain("/$count", string.Join(",", action.Selectors.Select(s => s.AttributeRouteModel.Template))); + } + } - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = controller.Actions.First(); - context.Options.RouteOptions.EnableActionNameCaseInsensitive = ignoreCase; + [Theory] + [InlineData(typeof(CustomersController), "Post", "/Customers", false)] + [InlineData(typeof(CustomersController), "PostFromVipCustomer", "/Customers/NS.VipCustomer", false)] + [InlineData(typeof(CaseInsensitiveCustomersController), "POST", "/CaseInsensitiveCustomers", true)] + [InlineData(typeof(CaseInsensitiveCustomersController), "POSTFromVIPCUSTOMER", "/CaseInsensitiveCustomers/NS.VipCustomer", true)] + public void AppliesToActionForPostActionWorksAsExpected(Type controllerType, string actionName, string expected, bool ignoreCase) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); - EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = controller.Actions.First(); + context.Options.RouteOptions.EnableActionNameCaseInsensitive = ignoreCase; - // Act - bool returnValue = entitySetConvention.AppliesToAction(context); - Assert.True(returnValue); + EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Equal(expected, selector.AttributeRouteModel.Template); - } + // Act + bool returnValue = entitySetConvention.AppliesToAction(context); + Assert.True(returnValue); - [Theory] - [InlineData(typeof(CustomersController), "Patch", "/Customers", false)] - [InlineData(typeof(CustomersController), "PatchCustomers", "/Customers", false)] - [InlineData(typeof(CaseInsensitiveCustomersController), "PATCH", "/CaseInsensitiveCustomers", true)] - [InlineData(typeof(CaseInsensitiveCustomersController), "PATCHCASEINSENSITIVECUSTOMERS", "/CaseInsensitiveCustomers", true)] - public void AppliesToAction_Works_ForPatchActionWorksAsExpected(Type controllerType, string actionName, string expected, bool ignoreCase) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Equal(expected, selector.AttributeRouteModel.Template); + } - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = controller.Actions.First(); - context.Options.RouteOptions.EnableActionNameCaseInsensitive = ignoreCase; + [Theory] + [InlineData(typeof(CustomersController), "Patch", "/Customers", false)] + [InlineData(typeof(CustomersController), "PatchCustomers", "/Customers", false)] + [InlineData(typeof(CaseInsensitiveCustomersController), "PATCH", "/CaseInsensitiveCustomers", true)] + [InlineData(typeof(CaseInsensitiveCustomersController), "PATCHCASEINSENSITIVECUSTOMERS", "/CaseInsensitiveCustomers", true)] + public void AppliesToAction_Works_ForPatchActionWorksAsExpected(Type controllerType, string actionName, string expected, bool ignoreCase) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); - EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = controller.Actions.First(); + context.Options.RouteOptions.EnableActionNameCaseInsensitive = ignoreCase; - // Act - bool returnValue = entitySetConvention.AppliesToAction(context); - Assert.True(returnValue); + EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Equal(expected, selector.AttributeRouteModel.Template); - } + // Act + bool returnValue = entitySetConvention.AppliesToAction(context); + Assert.True(returnValue); - [Theory] - [InlineData("GET")] - [InlineData("Get")] - [InlineData("PostTo")] - [InlineData("GetFrom")] - [InlineData("PostFrom")] - [InlineData("PatchFrom")] - [InlineData("GetAnotherCustomersFrom")] - [InlineData("PatchAnotherCustomersFrom")] - [InlineData("PostAnotherCustomerFrom")] - public void AppliesToActionDoesNothingForNonConventionAction(string actionName) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Equal(expected, selector.AttributeRouteModel.Template); + } - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = controller.Actions.First(); + [Theory] + [InlineData("GET")] + [InlineData("Get")] + [InlineData("PostTo")] + [InlineData("GetFrom")] + [InlineData("PostFrom")] + [InlineData("PatchFrom")] + [InlineData("GetAnotherCustomersFrom")] + [InlineData("PatchAnotherCustomersFrom")] + [InlineData("PostAnotherCustomerFrom")] + public void AppliesToActionDoesNothingForNonConventionAction(string actionName) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); - EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = controller.Actions.First(); - // Act - entitySetConvention.AppliesToAction(context); + EntitySetRoutingConvention entitySetConvention = ConventionHelpers.CreateConvention(); - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Null(selector.AttributeRouteModel); - } + // Act + entitySetConvention.AppliesToAction(context); - private static IEdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); - - // Customer - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - model.AddElement(customer); - - // VipCustomer - EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); - model.AddElement(vipCustomer); - - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - container.AddEntitySet("Customers", customer); - container.AddEntitySet("CaseInsensitiveCustomers", customer); - container.AddEntitySet("AnotherCustomers", customer); - model.AddElement(container); - return model; - } + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Null(selector.AttributeRouteModel); + } - private class CustomersController - { - public void Get() - { } + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + + // Customer + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + model.AddElement(customer); + + // VipCustomer + EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); + model.AddElement(vipCustomer); + + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + container.AddEntitySet("Customers", customer); + container.AddEntitySet("CaseInsensitiveCustomers", customer); + container.AddEntitySet("AnotherCustomers", customer); + model.AddElement(container); + return model; + } + + private class CustomersController + { + public void Get() + { } - public void GetCustomersFromVipCustomer() - { } + public void GetCustomersFromVipCustomer() + { } - public void Post() - { } + public void Post() + { } - public void PostFromVipCustomer() - { } + public void PostFromVipCustomer() + { } - public void Patch() - { } + public void Patch() + { } - public void PatchCustomers() - { } - } + public void PatchCustomers() + { } + } - private class CaseInsensitiveCustomersController - { - public void GET() - { } + private class CaseInsensitiveCustomersController + { + public void GET() + { } - public void GETCASEINSENSITIVECUSTOMERSFromVIPCUSTOMER() - { } + public void GETCASEINSENSITIVECUSTOMERSFromVIPCUSTOMER() + { } - public void POST() - { } + public void POST() + { } - public void POSTFromVIPCUSTOMER() - { } + public void POSTFromVIPCUSTOMER() + { } - public void PATCH() - { } + public void PATCH() + { } - public void PATCHCASEINSENSITIVECUSTOMERS() - { } - } + public void PATCHCASEINSENSITIVECUSTOMERS() + { } + } - private class AnotherCustomersController - { - public void GET() // Verify case insensitive by default - { } + private class AnotherCustomersController + { + public void GET() // Verify case insensitive by default + { } - public void Get(int key) - { } - - public void PostTo() - { } + public void Get(int key) + { } - public void GetFrom() - { - } + public void PostTo() + { } - public void PostFrom() - { - } + public void GetFrom() + { + } - public void PatchFrom() - { - } + public void PostFrom() + { + } - public void GetAnotherCustomersFrom() - { - } + public void PatchFrom() + { + } - public void PatchAnotherCustomersFrom() - { - } + public void GetAnotherCustomersFrom() + { + } - public void PostAnotherCustomerFrom() - { - } + public void PatchAnotherCustomersFrom() + { } - private class UnknownController - { } + public void PostAnotherCustomerFrom() + { + } } + + private class UnknownController + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/FunctionRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/FunctionRoutingConventionTests.cs index 618d11fad..6ac13fb2d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/FunctionRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/FunctionRoutingConventionTests.cs @@ -18,610 +18,609 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +public class FunctionRoutingConventionTests { - public class FunctionRoutingConventionTests - { - private static FunctionRoutingConvention FunctionConvention = ConventionHelpers.CreateConvention(); - private static IEdmModel EdmModel = GetEdmModel(); + private static FunctionRoutingConvention FunctionConvention = ConventionHelpers.CreateConvention(); + private static IEdmModel EdmModel = GetEdmModel(); - [Fact] - public void AppliesToActionOnFunctionRoutingConvention_Throws_Context() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => FunctionConvention.AppliesToController(null), "context"); - ExceptionAssert.ThrowsArgumentNull(() => FunctionConvention.AppliesToAction(null), "context"); - } + [Fact] + public void AppliesToActionOnFunctionRoutingConvention_Throws_Context() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => FunctionConvention.AppliesToController(null), "context"); + ExceptionAssert.ThrowsArgumentNull(() => FunctionConvention.AppliesToAction(null), "context"); + } - [Theory] - [InlineData(typeof(CustomersController), true)] - [InlineData(typeof(MeController), true)] - [InlineData(typeof(UnknownController), false)] - public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + [Theory] + [InlineData(typeof(CustomersController), true)] + [InlineData(typeof(MeController), true)] + [InlineData(typeof(UnknownController), false)] + public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - // Act - bool actual = FunctionConvention.AppliesToController(context); + // Act + bool actual = FunctionConvention.AppliesToController(context); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - public static TheoryDataSet FunctionRoutingConventionTestData + public static TheoryDataSet FunctionRoutingConventionTestData + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() + // Bound to single { - // Bound to single + typeof(CustomersController), + "IsBaseUpgraded", + new[] { - typeof(CustomersController), - "IsBaseUpgraded", - new[] - { - "/Customers({key})/NS.IsBaseUpgraded()", - "/Customers({key})/IsBaseUpgraded()", - "/Customers/{key}/NS.IsBaseUpgraded()", - "/Customers/{key}/IsBaseUpgraded()" - } - }, + "/Customers({key})/NS.IsBaseUpgraded()", + "/Customers({key})/IsBaseUpgraded()", + "/Customers/{key}/NS.IsBaseUpgraded()", + "/Customers/{key}/IsBaseUpgraded()" + } + }, + { + typeof(MeController), + "IsBaseUpgraded", + new[] { - typeof(MeController), - "IsBaseUpgraded", - new[] - { - "/Me/NS.IsBaseUpgraded()", - "/Me/IsBaseUpgraded()" - } - }, + "/Me/NS.IsBaseUpgraded()", + "/Me/IsBaseUpgraded()" + } + }, + { + typeof(CustomersController), + "IsUpgraded", + new[] { - typeof(CustomersController), - "IsUpgraded", - new[] - { - "/Customers({key})/NS.IsUpgraded()", - "/Customers({key})/IsUpgraded()", - "/Customers/{key}/NS.IsUpgraded()", - "/Customers/{key}/IsUpgraded()" - } - }, + "/Customers({key})/NS.IsUpgraded()", + "/Customers({key})/IsUpgraded()", + "/Customers/{key}/NS.IsUpgraded()", + "/Customers/{key}/IsUpgraded()" + } + }, + { + typeof(MeController), + "IsUpgraded", + new[] { - typeof(MeController), - "IsUpgraded", - new[] - { - "/Me/NS.IsUpgraded()", - "/Me/IsUpgraded()" - } - }, + "/Me/NS.IsUpgraded()", + "/Me/IsUpgraded()" + } + }, + { + typeof(CustomersController), + "IsVipUpgraded", + new[] { - typeof(CustomersController), - "IsVipUpgraded", - new[] - { - "/Customers({key})/NS.VipCustomer/NS.IsVipUpgraded(param={param})", - "/Customers({key})/NS.VipCustomer/IsVipUpgraded(param={param})", - "/Customers/{key}/NS.VipCustomer/NS.IsVipUpgraded(param={param})", - "/Customers/{key}/NS.VipCustomer/IsVipUpgraded(param={param})" - } - }, + "/Customers({key})/NS.VipCustomer/NS.IsVipUpgraded(param={param})", + "/Customers({key})/NS.VipCustomer/IsVipUpgraded(param={param})", + "/Customers/{key}/NS.VipCustomer/NS.IsVipUpgraded(param={param})", + "/Customers/{key}/NS.VipCustomer/IsVipUpgraded(param={param})" + } + }, + { + typeof(MeController), + "IsVipUpgraded", + new[] { - typeof(MeController), - "IsVipUpgraded", - new[] - { - "/Me/NS.VipCustomer/NS.IsVipUpgraded(param={param})", - "/Me/NS.VipCustomer/IsVipUpgraded(param={param})" - } - }, - // bound to collection + "/Me/NS.VipCustomer/NS.IsVipUpgraded(param={param})", + "/Me/NS.VipCustomer/IsVipUpgraded(param={param})" + } + }, + // bound to collection + { + typeof(CustomersController), + "IsBaseAllUpgraded", + new[] { - typeof(CustomersController), - "IsBaseAllUpgraded", - new[] - { - "/Customers/NS.IsBaseAllUpgraded(param={param})", - "/Customers/IsBaseAllUpgraded(param={param})" - } - }, + "/Customers/NS.IsBaseAllUpgraded(param={param})", + "/Customers/IsBaseAllUpgraded(param={param})" + } + }, + { + typeof(CustomersController), + "IsAllCustomersUpgraded", + new[] { - typeof(CustomersController), - "IsAllCustomersUpgraded", - new[] - { - "/Customers/NS.IsAllCustomersUpgraded(param={param})", - "/Customers/IsAllCustomersUpgraded(param={param})" - } - }, + "/Customers/NS.IsAllCustomersUpgraded(param={param})", + "/Customers/IsAllCustomersUpgraded(param={param})" + } + }, + { + typeof(CustomersController), + "IsVipAllUpgraded", + new[] { - typeof(CustomersController), - "IsVipAllUpgraded", - new[] - { - "/Customers/NS.VipCustomer/NS.IsVipAllUpgraded(param={param})", - "/Customers/NS.VipCustomer/IsVipAllUpgraded(param={param})" - } - }, - // optional parameter + "/Customers/NS.VipCustomer/NS.IsVipAllUpgraded(param={param})", + "/Customers/NS.VipCustomer/IsVipAllUpgraded(param={param})" + } + }, + // optional parameter + { + typeof(CustomersController), + "GetWholeSalary", + new[] { - typeof(CustomersController), - "GetWholeSalary", - new[] - { - "/Customers/NS.GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary},aveSalary={aveSalary})", - "/Customers/GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary},aveSalary={aveSalary})" - } - }, + "/Customers/NS.GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary},aveSalary={aveSalary})", + "/Customers/GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary},aveSalary={aveSalary})" + } + }, + { + typeof(CustomersController), + "GetStatusOnLineOfflineUser", + new[] { - typeof(CustomersController), - "GetStatusOnLineOfflineUser", - new[] - { - "/Customers/NS.GetStatusOnLineOfflineUser()", - "/Customers/GetStatusOnLineOfflineUser()" - } - }, + "/Customers/NS.GetStatusOnLineOfflineUser()", + "/Customers/GetStatusOnLineOfflineUser()" + } + }, + { + typeof(CustomersController), + "GetStatusOnLineOfflineUser", + new[] { - typeof(CustomersController), - "GetStatusOnLineOfflineUser", - new[] - { - "/Customers/NS.GetStatusOnLineOfflineUser()", - "/Customers/GetStatusOnLineOfflineUser()" - } - }, + "/Customers/NS.GetStatusOnLineOfflineUser()", + "/Customers/GetStatusOnLineOfflineUser()" + } + }, + { + typeof(CustomersController), + "StatusLineOfflineUserOn", + new[] { - typeof(CustomersController), - "StatusLineOfflineUserOn", - new[] - { - "/Customers/NS.StatusLineOfflineUserOn()", - "/Customers/StatusLineOfflineUserOn()" - } - }, + "/Customers/NS.StatusLineOfflineUserOn()", + "/Customers/StatusLineOfflineUserOn()" + } + }, + { + typeof(CustomersController), + "GetStatusOnLineOfflineUserOnVipCustomer", + new[] { - typeof(CustomersController), - "GetStatusOnLineOfflineUserOnVipCustomer", - new[] - { - "/Customers/NS.VipCustomer/NS.GetStatusOnLineOfflineUser(param={param})", - "/Customers/NS.VipCustomer/GetStatusOnLineOfflineUser(param={param})" - } + "/Customers/NS.VipCustomer/NS.GetStatusOnLineOfflineUser(param={param})", + "/Customers/NS.VipCustomer/GetStatusOnLineOfflineUser(param={param})" } - }; - } + } + }; } + } - public static TheoryDataSet FunctionRoutingConventionCaseInsensitiveTestData + public static TheoryDataSet FunctionRoutingConventionCaseInsensitiveTestData + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() + // Bound to single { - // Bound to single + typeof(CustomersCaseInsensitiveController), + "ISBASEUPGRADED", + new[] { - typeof(CustomersCaseInsensitiveController), - "ISBASEUPGRADED", - new[] - { - "/CustomersCaseInsensitive({key})/NS.IsBaseUpgraded()", - "/CustomersCaseInsensitive({key})/IsBaseUpgraded()", - "/CustomersCaseInsensitive/{key}/NS.IsBaseUpgraded()", - "/CustomersCaseInsensitive/{key}/IsBaseUpgraded()" - } - }, + "/CustomersCaseInsensitive({key})/NS.IsBaseUpgraded()", + "/CustomersCaseInsensitive({key})/IsBaseUpgraded()", + "/CustomersCaseInsensitive/{key}/NS.IsBaseUpgraded()", + "/CustomersCaseInsensitive/{key}/IsBaseUpgraded()" + } + }, + { + typeof(CustomersCaseInsensitiveController), + "ISUPGRADED", + new[] { - typeof(CustomersCaseInsensitiveController), - "ISUPGRADED", - new[] - { - "/CustomersCaseInsensitive({key})/NS.IsUpgraded()", - "/CustomersCaseInsensitive({key})/IsUpgraded()", - "/CustomersCaseInsensitive/{key}/NS.IsUpgraded()", - "/CustomersCaseInsensitive/{key}/IsUpgraded()" - } - }, + "/CustomersCaseInsensitive({key})/NS.IsUpgraded()", + "/CustomersCaseInsensitive({key})/IsUpgraded()", + "/CustomersCaseInsensitive/{key}/NS.IsUpgraded()", + "/CustomersCaseInsensitive/{key}/IsUpgraded()" + } + }, + { + typeof(CustomersCaseInsensitiveController), + "ISVIPUPGRADED", + new[] { - typeof(CustomersCaseInsensitiveController), - "ISVIPUPGRADED", - new[] - { - "/CustomersCaseInsensitive({key})/NS.VipCustomer/NS.IsVipUpgraded(param={param})", - "/CustomersCaseInsensitive({key})/NS.VipCustomer/IsVipUpgraded(param={param})", - "/CustomersCaseInsensitive/{key}/NS.VipCustomer/NS.IsVipUpgraded(param={param})", - "/CustomersCaseInsensitive/{key}/NS.VipCustomer/IsVipUpgraded(param={param})" - } - }, - // bound to collection + "/CustomersCaseInsensitive({key})/NS.VipCustomer/NS.IsVipUpgraded(param={param})", + "/CustomersCaseInsensitive({key})/NS.VipCustomer/IsVipUpgraded(param={param})", + "/CustomersCaseInsensitive/{key}/NS.VipCustomer/NS.IsVipUpgraded(param={param})", + "/CustomersCaseInsensitive/{key}/NS.VipCustomer/IsVipUpgraded(param={param})" + } + }, + // bound to collection + { + typeof(CustomersCaseInsensitiveController), + "ISBASEALLUPGRADED", + new[] { - typeof(CustomersCaseInsensitiveController), - "ISBASEALLUPGRADED", - new[] - { - "/CustomersCaseInsensitive/NS.IsBaseAllUpgraded(param={param})", - "/CustomersCaseInsensitive/IsBaseAllUpgraded(param={param})" - } - }, + "/CustomersCaseInsensitive/NS.IsBaseAllUpgraded(param={param})", + "/CustomersCaseInsensitive/IsBaseAllUpgraded(param={param})" + } + }, + { + typeof(CustomersCaseInsensitiveController), + "ISALLCUSTOMERSUPGRADED", + new[] { - typeof(CustomersCaseInsensitiveController), - "ISALLCUSTOMERSUPGRADED", - new[] - { - "/CustomersCaseInsensitive/NS.IsAllCustomersUpgraded(param={param})", - "/CustomersCaseInsensitive/IsAllCustomersUpgraded(param={param})" - } - }, + "/CustomersCaseInsensitive/NS.IsAllCustomersUpgraded(param={param})", + "/CustomersCaseInsensitive/IsAllCustomersUpgraded(param={param})" + } + }, + { + typeof(CustomersCaseInsensitiveController), + "ISVIPALLUPGRADED", + new[] { - typeof(CustomersCaseInsensitiveController), - "ISVIPALLUPGRADED", - new[] - { - "/CustomersCaseInsensitive/NS.VipCustomer/NS.IsVipAllUpgraded(param={param})", - "/CustomersCaseInsensitive/NS.VipCustomer/IsVipAllUpgraded(param={param})" - } + "/CustomersCaseInsensitive/NS.VipCustomer/NS.IsVipAllUpgraded(param={param})", + "/CustomersCaseInsensitive/NS.VipCustomer/IsVipAllUpgraded(param={param})" } - }; - } + } + }; } + } - [Theory] - [MemberData(nameof(FunctionRoutingConventionTestData))] - public void FunctionRoutingConventionTestDataRunsAsExpected(Type controllerType, string actionName, string[] templates) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [MemberData(nameof(FunctionRoutingConventionTestData))] + public void FunctionRoutingConventionTestDataRunsAsExpected(Type controllerType, string actionName, string[] templates) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; - // Act - FunctionConvention.AppliesToAction(context); + // Act + FunctionConvention.AppliesToAction(context); - // Assert - Assert.Equal(templates.Length, action.Selectors.Count); - Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); - } + // Assert + Assert.Equal(templates.Length, action.Selectors.Count); + Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } - [Theory] - [MemberData(nameof(FunctionRoutingConventionTestData))] - [MemberData(nameof(FunctionRoutingConventionCaseInsensitiveTestData))] - public void FunctionRoutingConventionCaseInsensitiveTestDataRunsAsExpected(Type controllerType, string actionName, string[] templates) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [MemberData(nameof(FunctionRoutingConventionTestData))] + [MemberData(nameof(FunctionRoutingConventionCaseInsensitiveTestData))] + public void FunctionRoutingConventionCaseInsensitiveTestDataRunsAsExpected(Type controllerType, string actionName, string[] templates) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; - context.Options.RouteOptions.EnableActionNameCaseInsensitive = true; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + context.Options.RouteOptions.EnableActionNameCaseInsensitive = true; - // Act - FunctionConvention.AppliesToAction(context); + // Act + FunctionConvention.AppliesToAction(context); - // Assert - Assert.Equal(templates.Length, action.Selectors.Count); - Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); - } + // Assert + Assert.Equal(templates.Length, action.Selectors.Count); + Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } - [Theory] - [MemberData(nameof(FunctionRoutingConventionCaseInsensitiveTestData))] - public void FunctionRoutingConventionCaseSensitiveByDefault(Type controllerType, string actionName, string[] templates) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [MemberData(nameof(FunctionRoutingConventionCaseInsensitiveTestData))] + public void FunctionRoutingConventionCaseSensitiveByDefault(Type controllerType, string actionName, string[] templates) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; - // Act - FunctionConvention.AppliesToAction(context); + // Act + FunctionConvention.AppliesToAction(context); - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Null(selector.AttributeRouteModel); - Assert.NotEmpty(templates); - } + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Null(selector.AttributeRouteModel); + Assert.NotEmpty(templates); + } - public static TheoryDataSet OverloadFunctionTestData + public static TheoryDataSet OverloadFunctionTestData + { + get { - get + Type controller = typeof(CustomersController); + MethodInfo method1 = controller.GetMethod("UpgradedAll", new Type[] { typeof(int), typeof(string) }); + MethodInfo method2 = controller.GetMethod("UpgradedAll", new Type[] { typeof(int), typeof(string), typeof(string) }); + return new TheoryDataSet() { - Type controller = typeof(CustomersController); - MethodInfo method1 = controller.GetMethod("UpgradedAll", new Type[] { typeof(int), typeof(string) }); - MethodInfo method2 = controller.GetMethod("UpgradedAll", new Type[] { typeof(int), typeof(string), typeof(string) }); - return new TheoryDataSet() { + method1, + new[] { - method1, - new[] - { - "/Customers/NS.UpgradedAll(age={age},name={name})", - "/Customers/UpgradedAll(age={age},name={name})", - "/Customers/NS.VipCustomer/NS.UpgradedAll(age={age},name={name})", - "/Customers/NS.VipCustomer/UpgradedAll(age={age},name={name})" - } - }, + "/Customers/NS.UpgradedAll(age={age},name={name})", + "/Customers/UpgradedAll(age={age},name={name})", + "/Customers/NS.VipCustomer/NS.UpgradedAll(age={age},name={name})", + "/Customers/NS.VipCustomer/UpgradedAll(age={age},name={name})" + } + }, + { + // TODO: this overload result looks not good, let's improve it later. + method2, + new[] { - // TODO: this overload result looks not good, let's improve it later. - method2, - new[] - { - "/Customers/NS.UpgradedAll(age={age},name={name})", - "/Customers/UpgradedAll(age={age},name={name})", - "/Customers/NS.UpgradedAll(age={age},name={name},gender={gender})", - "/Customers/UpgradedAll(age={age},name={name},gender={gender})", - "/Customers/NS.VipCustomer/NS.UpgradedAll(age={age},name={name})", - "/Customers/NS.VipCustomer/UpgradedAll(age={age},name={name})" - } + "/Customers/NS.UpgradedAll(age={age},name={name})", + "/Customers/UpgradedAll(age={age},name={name})", + "/Customers/NS.UpgradedAll(age={age},name={name},gender={gender})", + "/Customers/UpgradedAll(age={age},name={name},gender={gender})", + "/Customers/NS.VipCustomer/NS.UpgradedAll(age={age},name={name})", + "/Customers/NS.VipCustomer/UpgradedAll(age={age},name={name})" } - }; - } + } + }; } + } - [Theory] - [MemberData(nameof(OverloadFunctionTestData))] - public void FunctionRoutingConventionResolveFunctionOverloadAsExpected(MethodInfo method, string[] templates) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModelByMethodInfo(method); - ActionModel action = controller.Actions.First(); + [Theory] + [MemberData(nameof(OverloadFunctionTestData))] + public void FunctionRoutingConventionResolveFunctionOverloadAsExpected(MethodInfo method, string[] templates) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModelByMethodInfo(method); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; - // Act - FunctionConvention.AppliesToAction(context); + // Act + FunctionConvention.AppliesToAction(context); - // Assert - Assert.Equal(templates.Length, action.Selectors.Count); - Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); - } + // Assert + Assert.Equal(templates.Length, action.Selectors.Count); + Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } - [Theory] - [InlineData("Get")] - [InlineData("UnknownFunction")] - [InlineData("NonSupportedOn")] - [InlineData("NonSupportedOnCollectionOf")] - public void PropertyRoutingConventionDoesNothingForNotSupportedAction(string actionName) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [InlineData("Get")] + [InlineData("UnknownFunction")] + [InlineData("NonSupportedOn")] + [InlineData("NonSupportedOnCollectionOf")] + public void PropertyRoutingConventionDoesNothingForNotSupportedAction(string actionName) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = controller.Actions.First(); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = controller.Actions.First(); - // Act - bool returnValue = FunctionConvention.AppliesToAction(context); - Assert.False(returnValue); + // Act + bool returnValue = FunctionConvention.AppliesToAction(context); + Assert.False(returnValue); - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Null(selector.AttributeRouteModel); - } - - private static IEdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); - - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmTypeReference stringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); - IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); - - // Entity - EdmEntityType entity = new EdmEntityType("NS", "Entity", null, true, false); - model.AddElement(entity); - - // Customer - EdmEntityType customer = new EdmEntityType("NS", "Customer", entity); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - model.AddElement(customer); - - // VipCustomer - EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); - model.AddElement(vipCustomer); - - // functions bound to single - EdmFunction isBaseUpgraded = new EdmFunction("NS", "IsBaseUpgraded", returnType, true, entitySetPathExpression: null, isComposable: false); - isBaseUpgraded.AddParameter("entity", new EdmEntityTypeReference(entity, false)); - model.AddElement(isBaseUpgraded); - - EdmFunction isUpgraded = new EdmFunction("NS", "IsUpgraded", returnType, true, entitySetPathExpression: null, isComposable: false); - isUpgraded.AddParameter("entity", new EdmEntityTypeReference(customer, false)); - model.AddElement(isUpgraded); - - EdmFunction isVipUpgraded = new EdmFunction("NS", "IsVipUpgraded", returnType, true, entitySetPathExpression: null, isComposable: false); - isVipUpgraded.AddParameter("entity", new EdmEntityTypeReference(vipCustomer, false)); - isVipUpgraded.AddParameter("param", stringType); - model.AddElement(isVipUpgraded); - - // functions bound to collection - EdmFunction isBaseAllUpgraded = new EdmFunction("NS", "IsBaseAllUpgraded", returnType, true, entitySetPathExpression: null, isComposable: false); - isBaseAllUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(entity, false)))); - isBaseAllUpgraded.AddParameter("param", intType); - model.AddElement(isBaseAllUpgraded); - - EdmFunction isAllUpgraded = new EdmFunction("NS", "IsAllCustomersUpgraded", returnType, true, entitySetPathExpression: null, isComposable: false); - isAllUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - isAllUpgraded.AddParameter("param", intType); - model.AddElement(isAllUpgraded); - - EdmFunction isVipAllUpgraded = new EdmFunction("NS", "IsVipAllUpgraded", returnType, true, entitySetPathExpression: null, isComposable: false); - isVipAllUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(vipCustomer, false)))); - isVipAllUpgraded.AddParameter("param", intType); - model.AddElement(isVipAllUpgraded); - - // overloads - EdmFunction upgradeAll1 = new EdmFunction("NS", "UpgradedAll", returnType, true, entitySetPathExpression: null, isComposable: false); - upgradeAll1.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - upgradeAll1.AddParameter("age", intType); - upgradeAll1.AddParameter("name", stringType); - model.AddElement(upgradeAll1); - - EdmFunction upgradeAll2 = new EdmFunction("NS", "UpgradedAll", returnType, true, entitySetPathExpression: null, isComposable: false); - upgradeAll2.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - upgradeAll2.AddParameter("age", intType); - upgradeAll2.AddParameter("name", stringType); - upgradeAll2.AddParameter("gender", stringType); - model.AddElement(upgradeAll2); - - EdmFunction upgradeAll3 = new EdmFunction("NS", "UpgradedAll", returnType, true, entitySetPathExpression: null, isComposable: false); - upgradeAll3.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(vipCustomer, false)))); - upgradeAll3.AddParameter("age", intType); - upgradeAll3.AddParameter("name", stringType); - model.AddElement(upgradeAll3); - - // function with optional parameters - EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", intType, isBound: true, entitySetPathExpression: null, isComposable: false); - getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - getSalaray.AddParameter("minSalary", intType); - getSalaray.AddOptionalParameter("maxSalary", intType); - getSalaray.AddOptionalParameter("aveSalary", intType, "129"); - model.AddElement(getSalaray); - - EdmFunction f = new EdmFunction("NS", "GetStatusOnLineOfflineUser", intType, isBound: true, entitySetPathExpression: null, isComposable: false); - f.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - model.AddElement(f); - - EdmFunction f2 = new EdmFunction("NS", "StatusLineOfflineUserOn", intType, isBound: true, entitySetPathExpression: null, isComposable: false); - f2.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - model.AddElement(f2); - - EdmFunction f3 = new EdmFunction("NS", "GetStatusOnLineOfflineUser", intType, isBound: true, entitySetPathExpression: null, isComposable: false); - f3.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(vipCustomer, false)))); - f3.AddParameter("param", intType); - model.AddElement(f3); - - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - container.AddEntitySet("Customers", customer); - container.AddEntitySet("CustomersCaseInsensitive", customer); - container.AddSingleton("Me", customer); - model.AddElement(container); - return model; - } + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Null(selector.AttributeRouteModel); + } - private class CustomersController - { - public void Get() - { } + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmTypeReference stringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); + IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); + + // Entity + EdmEntityType entity = new EdmEntityType("NS", "Entity", null, true, false); + model.AddElement(entity); + + // Customer + EdmEntityType customer = new EdmEntityType("NS", "Customer", entity); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + model.AddElement(customer); + + // VipCustomer + EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); + model.AddElement(vipCustomer); + + // functions bound to single + EdmFunction isBaseUpgraded = new EdmFunction("NS", "IsBaseUpgraded", returnType, true, entitySetPathExpression: null, isComposable: false); + isBaseUpgraded.AddParameter("entity", new EdmEntityTypeReference(entity, false)); + model.AddElement(isBaseUpgraded); + + EdmFunction isUpgraded = new EdmFunction("NS", "IsUpgraded", returnType, true, entitySetPathExpression: null, isComposable: false); + isUpgraded.AddParameter("entity", new EdmEntityTypeReference(customer, false)); + model.AddElement(isUpgraded); + + EdmFunction isVipUpgraded = new EdmFunction("NS", "IsVipUpgraded", returnType, true, entitySetPathExpression: null, isComposable: false); + isVipUpgraded.AddParameter("entity", new EdmEntityTypeReference(vipCustomer, false)); + isVipUpgraded.AddParameter("param", stringType); + model.AddElement(isVipUpgraded); + + // functions bound to collection + EdmFunction isBaseAllUpgraded = new EdmFunction("NS", "IsBaseAllUpgraded", returnType, true, entitySetPathExpression: null, isComposable: false); + isBaseAllUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(entity, false)))); + isBaseAllUpgraded.AddParameter("param", intType); + model.AddElement(isBaseAllUpgraded); + + EdmFunction isAllUpgraded = new EdmFunction("NS", "IsAllCustomersUpgraded", returnType, true, entitySetPathExpression: null, isComposable: false); + isAllUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + isAllUpgraded.AddParameter("param", intType); + model.AddElement(isAllUpgraded); + + EdmFunction isVipAllUpgraded = new EdmFunction("NS", "IsVipAllUpgraded", returnType, true, entitySetPathExpression: null, isComposable: false); + isVipAllUpgraded.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(vipCustomer, false)))); + isVipAllUpgraded.AddParameter("param", intType); + model.AddElement(isVipAllUpgraded); + + // overloads + EdmFunction upgradeAll1 = new EdmFunction("NS", "UpgradedAll", returnType, true, entitySetPathExpression: null, isComposable: false); + upgradeAll1.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + upgradeAll1.AddParameter("age", intType); + upgradeAll1.AddParameter("name", stringType); + model.AddElement(upgradeAll1); + + EdmFunction upgradeAll2 = new EdmFunction("NS", "UpgradedAll", returnType, true, entitySetPathExpression: null, isComposable: false); + upgradeAll2.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + upgradeAll2.AddParameter("age", intType); + upgradeAll2.AddParameter("name", stringType); + upgradeAll2.AddParameter("gender", stringType); + model.AddElement(upgradeAll2); + + EdmFunction upgradeAll3 = new EdmFunction("NS", "UpgradedAll", returnType, true, entitySetPathExpression: null, isComposable: false); + upgradeAll3.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(vipCustomer, false)))); + upgradeAll3.AddParameter("age", intType); + upgradeAll3.AddParameter("name", stringType); + model.AddElement(upgradeAll3); + + // function with optional parameters + EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", intType, isBound: true, entitySetPathExpression: null, isComposable: false); + getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + getSalaray.AddParameter("minSalary", intType); + getSalaray.AddOptionalParameter("maxSalary", intType); + getSalaray.AddOptionalParameter("aveSalary", intType, "129"); + model.AddElement(getSalaray); + + EdmFunction f = new EdmFunction("NS", "GetStatusOnLineOfflineUser", intType, isBound: true, entitySetPathExpression: null, isComposable: false); + f.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + model.AddElement(f); + + EdmFunction f2 = new EdmFunction("NS", "StatusLineOfflineUserOn", intType, isBound: true, entitySetPathExpression: null, isComposable: false); + f2.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + model.AddElement(f2); + + EdmFunction f3 = new EdmFunction("NS", "GetStatusOnLineOfflineUser", intType, isBound: true, entitySetPathExpression: null, isComposable: false); + f3.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(vipCustomer, false)))); + f3.AddParameter("param", intType); + model.AddElement(f3); + + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + container.AddEntitySet("Customers", customer); + container.AddEntitySet("CustomersCaseInsensitive", customer); + container.AddSingleton("Me", customer); + model.AddElement(container); + return model; + } - [HttpGet] - public void GetStatusOnLineOfflineUser() { } + private class CustomersController + { + public void Get() + { } - [HttpGet] - public void GetStatusOnLineOfflineUserOnVipCustomer(int param) { } + [HttpGet] + public void GetStatusOnLineOfflineUser() { } - [HttpGet] - public void StatusLineOfflineUserOn() { } + [HttpGet] + public void GetStatusOnLineOfflineUserOnVipCustomer(int param) { } - [HttpGet] - public void IsBaseUpgraded(int key, CancellationToken cancellation) - { } + [HttpGet] + public void StatusLineOfflineUserOn() { } - [HttpGet] - public void IsUpgraded(int key, CancellationToken cancellation) - { } + [HttpGet] + public void IsBaseUpgraded(int key, CancellationToken cancellation) + { } - [HttpGet] - public void IsVipUpgraded(int key, string param) - { } + [HttpGet] + public void IsUpgraded(int key, CancellationToken cancellation) + { } - [HttpGet] - public void IsBaseAllUpgraded(int param) - { } + [HttpGet] + public void IsVipUpgraded(int key, string param) + { } - [HttpGet] - public void IsAllCustomersUpgraded(int param) - { } + [HttpGet] + public void IsBaseAllUpgraded(int param) + { } - [HttpGet] - public void IsVipAllUpgraded(CancellationToken cancellation, int param) - { } + [HttpGet] + public void IsAllCustomersUpgraded(int param) + { } - [HttpGet] - public void UpgradedAll(int age, string name) - { - // this one will match two - } + [HttpGet] + public void IsVipAllUpgraded(CancellationToken cancellation, int param) + { } - [HttpGet] - public void UpgradedAll(int age, string name, string gender) - { } + [HttpGet] + public void UpgradedAll(int age, string name) + { + // this one will match two + } - [HttpGet] - public void GetWholeSalary(int minSalary, int maxSalary, int aveSalary) - { } + [HttpGet] + public void UpgradedAll(int age, string name, string gender) + { } - [HttpGet] - public void UnknownFunction() - { } + [HttpGet] + public void GetWholeSalary(int minSalary, int maxSalary, int aveSalary) + { } - [HttpGet] - public void NonSupportedOn() - { - } + [HttpGet] + public void UnknownFunction() + { } - [HttpGet] - public void NonSupportedOnCollectionOf() - { - } + [HttpGet] + public void NonSupportedOn() + { } - private class MeController + [HttpGet] + public void NonSupportedOnCollectionOf() { - [HttpGet] - public void IsBaseUpgraded(CancellationToken cancellation) - { } - - [HttpGet] - public void IsUpgraded(CancellationToken cancellation) - { } - - [HttpGet] - public void IsVipUpgraded(string param) - { } } - - private class CustomersCaseInsensitiveController - { - public void GET() - { } + } - [HttpGet] - public void ISBASEUPGRADED(int key, CancellationToken cancellation) - { } + private class MeController + { + [HttpGet] + public void IsBaseUpgraded(CancellationToken cancellation) + { } - [HttpGet] - public void ISUPGRADED(int key, CancellationToken cancellation) - { } + [HttpGet] + public void IsUpgraded(CancellationToken cancellation) + { } - [HttpGet] - public void ISVIPUPGRADED(int key, string param) - { } + [HttpGet] + public void IsVipUpgraded(string param) + { } + } + + private class CustomersCaseInsensitiveController + { + public void GET() + { } - [HttpGet] - public void ISBASEALLUPGRADED(int param) - { } + [HttpGet] + public void ISBASEUPGRADED(int key, CancellationToken cancellation) + { } - [HttpGet] - public void ISALLCUSTOMERSUPGRADED(int param) - { } + [HttpGet] + public void ISUPGRADED(int key, CancellationToken cancellation) + { } - [HttpGet] - public void ISVIPALLUPGRADED(CancellationToken cancellation, int param) - { } - } - + [HttpGet] + public void ISVIPUPGRADED(int key, string param) + { } + + [HttpGet] + public void ISBASEALLUPGRADED(int param) + { } - private class AnotherCustomersController + [HttpGet] + public void ISALLCUSTOMERSUPGRADED(int param) { } - private class UnknownController + [HttpGet] + public void ISVIPALLUPGRADED(CancellationToken cancellation, int param) { } } + + + private class AnotherCustomersController + { } + + private class UnknownController + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/MetadataRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/MetadataRoutingConventionTests.cs index 99309ba8e..3832440c3 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/MetadataRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/MetadataRoutingConventionTests.cs @@ -15,81 +15,80 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +public class MetadataRoutingConventionTests { - public class MetadataRoutingConventionTests + private static MetadataRoutingConvention _metadataConvention = ConventionHelpers.CreateConvention(); + + [Fact] + public void AppliesToControllerAndActionOnMetadataRoutingConvention_Throws_Context() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => _metadataConvention.AppliesToController(null), "context"); + ExceptionAssert.ThrowsArgumentNull(() => _metadataConvention.AppliesToAction(null), "context"); + } + + [Theory] + [InlineData(typeof(MetadataController), true)] + [InlineData(typeof(UnknownController), false)] + public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmCoreModel.Instance, controller); + + // Act + bool actual = _metadataConvention.AppliesToController(context); + + // Assert + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("")] + [InlineData("odata")] + [InlineData("odata{data}")] + public void AppliesToActionAddTemplateForMetadataWithPrefix(string prefix) { - private static MetadataRoutingConvention _metadataConvention = ConventionHelpers.CreateConvention(); - - [Fact] - public void AppliesToControllerAndActionOnMetadataRoutingConvention_Throws_Context() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => _metadataConvention.AppliesToController(null), "context"); - ExceptionAssert.ThrowsArgumentNull(() => _metadataConvention.AppliesToAction(null), "context"); - } - - [Theory] - [InlineData(typeof(MetadataController), true)] - [InlineData(typeof(UnknownController), false)] - public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmCoreModel.Instance, controller); - - // Act - bool actual = _metadataConvention.AppliesToController(context); - - // Assert - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData("")] - [InlineData("odata")] - [InlineData("odata{data}")] - public void AppliesToActionAddTemplateForMetadataWithPrefix(string prefix) - { - // Arrange - string expected = string.IsNullOrEmpty(prefix) ? "/$metadata" : $"/{prefix}/$metadata"; - ControllerModel controller = ControllerModelHelpers.BuildControllerModel("GetMetadata"); - ActionModel action = controller.Actions.First(); - - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(prefix, EdmCoreModel.Instance, controller); - context.Action = action; - - // Act - _metadataConvention.AppliesToAction(context); - - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Equal(expected, selector.AttributeRouteModel.Template); - } - - [Theory] - [InlineData("")] - [InlineData("odata")] - [InlineData("odata{data}")] - public void AppliesToActionAddTemplateForServiceDocumentWithPrefix(string prefix) - { - // Arrange - string expected = string.IsNullOrEmpty(prefix) ? "/" : $"/{prefix}/"; - ControllerModel controller = ControllerModelHelpers.BuildControllerModel("GetServiceDocument"); - ActionModel action = controller.Actions.First(); - - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(prefix, EdmCoreModel.Instance, controller); - context.Action = action; - - // Act - _metadataConvention.AppliesToAction(context); - - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Equal(expected, selector.AttributeRouteModel.Template); - } - - private class UnknownController - { } + // Arrange + string expected = string.IsNullOrEmpty(prefix) ? "/$metadata" : $"/{prefix}/$metadata"; + ControllerModel controller = ControllerModelHelpers.BuildControllerModel("GetMetadata"); + ActionModel action = controller.Actions.First(); + + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(prefix, EdmCoreModel.Instance, controller); + context.Action = action; + + // Act + _metadataConvention.AppliesToAction(context); + + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Equal(expected, selector.AttributeRouteModel.Template); + } + + [Theory] + [InlineData("")] + [InlineData("odata")] + [InlineData("odata{data}")] + public void AppliesToActionAddTemplateForServiceDocumentWithPrefix(string prefix) + { + // Arrange + string expected = string.IsNullOrEmpty(prefix) ? "/" : $"/{prefix}/"; + ControllerModel controller = ControllerModelHelpers.BuildControllerModel("GetServiceDocument"); + ActionModel action = controller.Actions.First(); + + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(prefix, EdmCoreModel.Instance, controller); + context.Action = action; + + // Act + _metadataConvention.AppliesToAction(context); + + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Equal(expected, selector.AttributeRouteModel.Template); } + + private class UnknownController + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/NavigationRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/NavigationRoutingConventionTests.cs index 1976a789b..43b6bde80 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/NavigationRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/NavigationRoutingConventionTests.cs @@ -16,324 +16,323 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +public class NavigationRoutingConventionTests { - public class NavigationRoutingConventionTests - { - private static NavigationRoutingConvention NavigationConvention = ConventionHelpers.CreateConvention(); - private static IEdmModel EdmModel = GetEdmModel(); + private static NavigationRoutingConvention NavigationConvention = ConventionHelpers.CreateConvention(); + private static IEdmModel EdmModel = GetEdmModel(); - [Fact] - public void AppliesToControllerAndActionOnNavigationRoutingConvention_Throws_Context() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => NavigationConvention.AppliesToController(null), "context"); - ExceptionAssert.ThrowsArgumentNull(() => NavigationConvention.AppliesToAction(null), "context"); - } + [Fact] + public void AppliesToControllerAndActionOnNavigationRoutingConvention_Throws_Context() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => NavigationConvention.AppliesToController(null), "context"); + ExceptionAssert.ThrowsArgumentNull(() => NavigationConvention.AppliesToAction(null), "context"); + } - [Theory] - [InlineData(typeof(CustomersController), true)] - [InlineData(typeof(MeController), true)] - [InlineData(typeof(UnknownController), false)] - public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + [Theory] + [InlineData(typeof(CustomersController), true)] + [InlineData(typeof(MeController), true)] + [InlineData(typeof(UnknownController), false)] + public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - // Act - bool actual = NavigationConvention.AppliesToController(context); + // Act + bool actual = NavigationConvention.AppliesToController(context); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - public static TheoryDataSet NavigationRoutingTestData + public static TheoryDataSet NavigationRoutingTestData + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() + // Get { - // Get + typeof(CustomersController), + "GetOrders", + new[] { - typeof(CustomersController), - "GetOrders", - new[] - { - "/Customers({key})/Orders", - "/Customers/{key}/Orders", - "/Customers({key})/Orders/$count", - "/Customers/{key}/Orders/$count" - } - }, + "/Customers({key})/Orders", + "/Customers/{key}/Orders", + "/Customers({key})/Orders/$count", + "/Customers/{key}/Orders/$count" + } + }, + { + typeof(CustomersController), + "GetSubOrdersFromVipCustomer", + new[] { - typeof(CustomersController), - "GetSubOrdersFromVipCustomer", - new[] - { - "/Customers({key})/NS.VipCustomer/SubOrders", - "/Customers/{key}/NS.VipCustomer/SubOrders", - "/Customers({key})/NS.VipCustomer/SubOrders/$count", - "/Customers/{key}/NS.VipCustomer/SubOrders/$count" - } - }, + "/Customers({key})/NS.VipCustomer/SubOrders", + "/Customers/{key}/NS.VipCustomer/SubOrders", + "/Customers({key})/NS.VipCustomer/SubOrders/$count", + "/Customers/{key}/NS.VipCustomer/SubOrders/$count" + } + }, + { + typeof(CustomersController), + "PostToOrders", + new[] { - typeof(CustomersController), - "PostToOrders", - new[] - { - "/Customers({key})/Orders", - "/Customers/{key}/Orders" - } - }, + "/Customers({key})/Orders", + "/Customers/{key}/Orders" + } + }, + { + typeof(CustomersController), + "PutToSubOrderFromVipCustomer", + new[] { - typeof(CustomersController), - "PutToSubOrderFromVipCustomer", - new[] - { - "/Customers({key})/NS.VipCustomer/SubOrder", - "/Customers/{key}/NS.VipCustomer/SubOrder" - } - }, - // singleton + "/Customers({key})/NS.VipCustomer/SubOrder", + "/Customers/{key}/NS.VipCustomer/SubOrder" + } + }, + // singleton + { + typeof(MeController), + "PostToSubOrdersFromVipCustomer", + new[] { - typeof(MeController), - "PostToSubOrdersFromVipCustomer", - new[] - { - "/Me/NS.VipCustomer/SubOrders" - } - }, + "/Me/NS.VipCustomer/SubOrders" + } + }, + { + typeof(MeController), + "PutToOrder", + new[] { - typeof(MeController), - "PutToOrder", - new[] - { - "/Me/Order" - } - }, + "/Me/Order" + } + }, + { + typeof(MeController), + "PatchToSubOrderFromVipCustomer", + new[] { - typeof(MeController), - "PatchToSubOrderFromVipCustomer", - new[] - { - "/Me/NS.VipCustomer/SubOrder" - } - }, - }; - } + "/Me/NS.VipCustomer/SubOrder" + } + }, + }; } + } - [Theory] - [MemberData(nameof(NavigationRoutingTestData))] - public void NavigationRoutingConventionTestDataRunsAsExpected(Type controllerType, string actionName, string[] templates) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [MemberData(nameof(NavigationRoutingTestData))] + public void NavigationRoutingConventionTestDataRunsAsExpected(Type controllerType, string actionName, string[] templates) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; - // Act - bool returnValue = NavigationConvention.AppliesToAction(context); - Assert.True(returnValue); + // Act + bool returnValue = NavigationConvention.AppliesToAction(context); + Assert.True(returnValue); - // Assert - Assert.Equal(templates.Length, action.Selectors.Count); - Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); - } + // Assert + Assert.Equal(templates.Length, action.Selectors.Count); + Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } - [Theory] - [InlineData(typeof(CustomersController), "GetOrders", 2, false)] - [InlineData(typeof(CustomersController), "GetOrders", 4, true)] - [InlineData(typeof(CustomersController), "GetSubOrdersFromVipCustomer", 2, false)] - [InlineData(typeof(CustomersController), "GetSubOrdersFromVipCustomer", 4, true)] - public void NavigationRoutingConventionAddDollarCountAsExpectedBasedOnConfiguration(Type controllerType, string actionName, int expectCount, bool enableDollarCount) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [InlineData(typeof(CustomersController), "GetOrders", 2, false)] + [InlineData(typeof(CustomersController), "GetOrders", 4, true)] + [InlineData(typeof(CustomersController), "GetSubOrdersFromVipCustomer", 2, false)] + [InlineData(typeof(CustomersController), "GetSubOrdersFromVipCustomer", 4, true)] + public void NavigationRoutingConventionAddDollarCountAsExpectedBasedOnConfiguration(Type controllerType, string actionName, int expectCount, bool enableDollarCount) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; - context.Options.RouteOptions.EnableDollarCountRouting = enableDollarCount; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + context.Options.RouteOptions.EnableDollarCountRouting = enableDollarCount; - // Act - bool returnValue = NavigationConvention.AppliesToAction(context); - Assert.True(returnValue); + // Act + bool returnValue = NavigationConvention.AppliesToAction(context); + Assert.True(returnValue); - // Assert - Assert.Equal(expectCount, action.Selectors.Count); - if (enableDollarCount) - { - Assert.Contains("/$count", string.Join(",", action.Selectors.Select(s => s.AttributeRouteModel.Template))); - } - else - { - Assert.DoesNotContain("/$count", string.Join(",", action.Selectors.Select(s => s.AttributeRouteModel.Template))); - } + // Assert + Assert.Equal(expectCount, action.Selectors.Count); + if (enableDollarCount) + { + Assert.Contains("/$count", string.Join(",", action.Selectors.Select(s => s.AttributeRouteModel.Template))); } - - [Theory] - [InlineData("PostToName")] - [InlineData("Get")] - [InlineData("GetSubOrdersFrom")] - [InlineData("PostToSubOrdersFrom")] - [InlineData("PutToSubOrderFrom")] - [InlineData("PatchToSubOrderFrom")] - public void PropertyRoutingConventionDoesNothingForNotSupportedAction(string actionName) + else { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); + Assert.DoesNotContain("/$count", string.Join(",", action.Selectors.Select(s => s.AttributeRouteModel.Template))); + } + } - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = controller.Actions.First(); + [Theory] + [InlineData("PostToName")] + [InlineData("Get")] + [InlineData("GetSubOrdersFrom")] + [InlineData("PostToSubOrdersFrom")] + [InlineData("PutToSubOrderFrom")] + [InlineData("PatchToSubOrderFrom")] + public void PropertyRoutingConventionDoesNothingForNotSupportedAction(string actionName) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); - // Act - bool returnValue = NavigationConvention.AppliesToAction(context); - Assert.False(returnValue); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = controller.Actions.First(); - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Null(selector.AttributeRouteModel); - } + // Act + bool returnValue = NavigationConvention.AppliesToAction(context); + Assert.False(returnValue); - [Theory] - [InlineData("GetOrders", "Get", "Orders", null)] - [InlineData("GetOrdersFromVipCustomer", "Get", "Orders", "VipCustomer")] - [InlineData("PostToOrdersFromVipCustomer", "PostTo", "Orders", "VipCustomer")] - [InlineData("PatchToSubOrder", "PatchTo", "SubOrder", null)] - [InlineData("DeleteToSubOrder", null, null, null)] - public void SplitActionNameForNavigationRoutingConventionWorksAsExpected(string action, string method, string property, string cast) - { - // Arrange - string actual = NavigationRoutingConvention.SplitActionName(action, out string actualProperty, out string actualCast); + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Null(selector.AttributeRouteModel); + } - // Act & Assert - Assert.Equal(actual, method); - Assert.Equal(property, actualProperty); - Assert.Equal(actualCast, cast); - } + [Theory] + [InlineData("GetOrders", "Get", "Orders", null)] + [InlineData("GetOrdersFromVipCustomer", "Get", "Orders", "VipCustomer")] + [InlineData("PostToOrdersFromVipCustomer", "PostTo", "Orders", "VipCustomer")] + [InlineData("PatchToSubOrder", "PatchTo", "SubOrder", null)] + [InlineData("DeleteToSubOrder", null, null, null)] + public void SplitActionNameForNavigationRoutingConventionWorksAsExpected(string action, string method, string property, string cast) + { + // Arrange + string actual = NavigationRoutingConvention.SplitActionName(action, out string actualProperty, out string actualCast); - private static IEdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); - - // Order - EdmEntityType order = new EdmEntityType("NS", "Order", null, false, true); - order.AddKeys(order.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); - - // Customer - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - model.AddElement(customer); - - // VipCustomer - EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); - model.AddElement(vipCustomer); - - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - EdmEntitySet orders = container.AddEntitySet("Orders", order); - EdmEntitySet customers = container.AddEntitySet("Customers", customer); - EdmSingleton me = container.AddSingleton("Me", customer); - model.AddElement(container); - - EdmNavigationProperty ordersNavProp = customer.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "Orders", - TargetMultiplicity = EdmMultiplicity.Many, - Target = order - }); + // Act & Assert + Assert.Equal(actual, method); + Assert.Equal(property, actualProperty); + Assert.Equal(actualCast, cast); + } - customers.AddNavigationTarget(ordersNavProp, orders); - me.AddNavigationTarget(ordersNavProp, orders); + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + + // Order + EdmEntityType order = new EdmEntityType("NS", "Order", null, false, true); + order.AddKeys(order.AddStructuralProperty("ID", EdmPrimitiveTypeKind.Int32)); + + // Customer + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + model.AddElement(customer); + + // VipCustomer + EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); + model.AddElement(vipCustomer); + + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + EdmEntitySet orders = container.AddEntitySet("Orders", order); + EdmEntitySet customers = container.AddEntitySet("Customers", customer); + EdmSingleton me = container.AddSingleton("Me", customer); + model.AddElement(container); + + EdmNavigationProperty ordersNavProp = customer.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "Orders", + TargetMultiplicity = EdmMultiplicity.Many, + Target = order + }); - EdmNavigationProperty orderNavProp = customer.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "Order", - TargetMultiplicity = EdmMultiplicity.One, - Target = order - }); - customers.AddNavigationTarget(orderNavProp, orders); - me.AddNavigationTarget(orderNavProp, orders); - - EdmNavigationProperty subOrdersNavProp = customer.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "SubOrders", - TargetMultiplicity = EdmMultiplicity.Many, - Target = order - }); + customers.AddNavigationTarget(ordersNavProp, orders); + me.AddNavigationTarget(ordersNavProp, orders); - customers.AddNavigationTarget(subOrdersNavProp, orders, new EdmPathExpression("NS.VipCustomer/SubOrders")); - me.AddNavigationTarget(subOrdersNavProp, orders, new EdmPathExpression("NS.VipCustomer/SubOrders")); + EdmNavigationProperty orderNavProp = customer.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "Order", + TargetMultiplicity = EdmMultiplicity.One, + Target = order + }); + customers.AddNavigationTarget(orderNavProp, orders); + me.AddNavigationTarget(orderNavProp, orders); + + EdmNavigationProperty subOrdersNavProp = customer.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "SubOrders", + TargetMultiplicity = EdmMultiplicity.Many, + Target = order + }); + customers.AddNavigationTarget(subOrdersNavProp, orders, new EdmPathExpression("NS.VipCustomer/SubOrders")); + me.AddNavigationTarget(subOrdersNavProp, orders, new EdmPathExpression("NS.VipCustomer/SubOrders")); - EdmNavigationProperty subOrderNavProp = customer.AddUnidirectionalNavigation( - new EdmNavigationPropertyInfo - { - Name = "SubOrder", - TargetMultiplicity = EdmMultiplicity.Many, - Target = order - }); - customers.AddNavigationTarget(subOrderNavProp, orders, new EdmPathExpression("NS.VipCustomer/SubOrder")); - me.AddNavigationTarget(subOrderNavProp, orders, new EdmPathExpression("NS.VipCustomer/SubOrder")); + EdmNavigationProperty subOrderNavProp = customer.AddUnidirectionalNavigation( + new EdmNavigationPropertyInfo + { + Name = "SubOrder", + TargetMultiplicity = EdmMultiplicity.Many, + Target = order + }); - return model; - } + customers.AddNavigationTarget(subOrderNavProp, orders, new EdmPathExpression("NS.VipCustomer/SubOrder")); + me.AddNavigationTarget(subOrderNavProp, orders, new EdmPathExpression("NS.VipCustomer/SubOrder")); - private class CustomersController - { - public void GetOrders(int key, CancellationToken cancellation) - { } + return model; + } - public void GetSubOrdersFromVipCustomer(int key, CancellationToken cancellation) - { } + private class CustomersController + { + public void GetOrders(int key, CancellationToken cancellation) + { } - public void PostToOrders(int key) - { } + public void GetSubOrdersFromVipCustomer(int key, CancellationToken cancellation) + { } - public void PutToSubOrderFromVipCustomer(int key) - { } + public void PostToOrders(int key) + { } - public void PostToName(int key) - { } + public void PutToSubOrderFromVipCustomer(int key) + { } - public void Get(int key) - { } + public void PostToName(int key) + { } - public void GetSubOrdersFrom(int key) - { } + public void Get(int key) + { } - public void PostToSubOrdersFrom(int key) - { } + public void GetSubOrdersFrom(int key) + { } - public void PutToSubOrderFrom(int key) - { } + public void PostToSubOrdersFrom(int key) + { } - public void PatchToSubOrderFrom(int key) - { } - } + public void PutToSubOrderFrom(int key) + { } - private class MeController - { - public void PostToSubOrdersFromVipCustomer(CancellationToken cancellation) - { } + public void PatchToSubOrderFrom(int key) + { } + } - public void PutToOrder() - { } + private class MeController + { + public void PostToSubOrdersFromVipCustomer(CancellationToken cancellation) + { } - public void PatchToSubOrderFromVipCustomer(CancellationToken cancellation) - { } - } + public void PutToOrder() + { } - private class UnknownController + public void PatchToSubOrderFromVipCustomer(CancellationToken cancellation) { } } + + private class UnknownController + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ODataControllerActionContextHelpers.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ODataControllerActionContextHelpers.cs index 7abd76f3a..60b63238e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ODataControllerActionContextHelpers.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ODataControllerActionContextHelpers.cs @@ -10,33 +10,32 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +internal class ODataControllerActionContextHelpers { - internal class ODataControllerActionContextHelpers + public static ODataControllerActionContext BuildContext(string modelPrefix, IEdmModel model, ControllerModel controller) { - public static ODataControllerActionContext BuildContext(string modelPrefix, IEdmModel model, ControllerModel controller) - { - Assert.NotNull(model); + Assert.NotNull(model); - // The reason why to create a context is that: - // We don't need to call te FindEntitySet or FindSingleton before every convention. - // So, for a controller, we try to call "FindEntitySet" or "FindSingleton" once. - string controllerName = controller.ControllerName; - ODataControllerActionContext context = new ODataControllerActionContext(modelPrefix, model, controller); + // The reason why to create a context is that: + // We don't need to call te FindEntitySet or FindSingleton before every convention. + // So, for a controller, we try to call "FindEntitySet" or "FindSingleton" once. + string controllerName = controller.ControllerName; + ODataControllerActionContext context = new ODataControllerActionContext(modelPrefix, model, controller); - IEdmEntitySet entitySet = model.EntityContainer?.FindEntitySet(controllerName); - if (entitySet != null) - { - context.NavigationSource = entitySet; - } - - IEdmSingleton singleton = model.EntityContainer?.FindSingleton(controllerName); - if (singleton != null) - { - context.NavigationSource = singleton; - } + IEdmEntitySet entitySet = model.EntityContainer?.FindEntitySet(controllerName); + if (entitySet != null) + { + context.NavigationSource = entitySet; + } - return context; + IEdmSingleton singleton = model.EntityContainer?.FindSingleton(controllerName); + if (singleton != null) + { + context.NavigationSource = singleton; } + + return context; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ODataControllerActionContextTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ODataControllerActionContextTests.cs index c84d9177f..1d15de50f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ODataControllerActionContextTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/ODataControllerActionContextTests.cs @@ -14,55 +14,54 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +public class ODataControllerActionContextTests { - public class ODataControllerActionContextTests + [Fact] + public void CtorODataControllerActionContext_ThrowsArgumentNull() { - [Fact] - public void CtorODataControllerActionContext_ThrowsArgumentNull() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataControllerActionContext(null, null, null), "prefix"); + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataControllerActionContext(null, null, null), "prefix"); - // Arrange & Act & Assert - string prefix = "odata"; - ExceptionAssert.ThrowsArgumentNull(() => new ODataControllerActionContext(prefix, null, null), "model"); + // Arrange & Act & Assert + string prefix = "odata"; + ExceptionAssert.ThrowsArgumentNull(() => new ODataControllerActionContext(prefix, null, null), "model"); - // Arrange & Act & Assert - IEdmModel model = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => new ODataControllerActionContext(prefix, model, null), "controller"); - } + // Arrange & Act & Assert + IEdmModel model = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => new ODataControllerActionContext(prefix, model, null), "controller"); + } - [Fact] - public void CtorODataControllerActionContext_SetProperties() + [Fact] + public void CtorODataControllerActionContext_SetProperties() + { + // Arrange & Act + string prefix = "odata"; + IEdmModel model = new Mock().Object; + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + EdmEntityType entityType = new EdmEntityType("NS", "Entity"); + IEdmSingleton singleton = container.AddSingleton("Me", entityType); + ControllerModel controller = new ControllerModel(typeof(TestController).GetTypeInfo(), new List()); + ODataControllerActionContext context = new ODataControllerActionContext(prefix, model, controller) { - // Arrange & Act - string prefix = "odata"; - IEdmModel model = new Mock().Object; - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - EdmEntityType entityType = new EdmEntityType("NS", "Entity"); - IEdmSingleton singleton = container.AddSingleton("Me", entityType); - ControllerModel controller = new ControllerModel(typeof(TestController).GetTypeInfo(), new List()); - ODataControllerActionContext context = new ODataControllerActionContext(prefix, model, controller) - { - NavigationSource = singleton - }; + NavigationSource = singleton + }; - // Assert - Assert.Equal(prefix, context.Prefix); - Assert.Same(model, context.Model); - Assert.Same(controller, context.Controller); + // Assert + Assert.Equal(prefix, context.Prefix); + Assert.Same(model, context.Model); + Assert.Same(controller, context.Controller); - Assert.Null(context.EntitySet); - Assert.Same(singleton, context.Singleton); - Assert.Same(singleton, context.NavigationSource); - Assert.Same(entityType, context.EntityType); + Assert.Null(context.EntitySet); + Assert.Same(singleton, context.Singleton); + Assert.Same(singleton, context.NavigationSource); + Assert.Same(entityType, context.EntityType); - Assert.NotNull(context.Options); - } + Assert.NotNull(context.Options); + } - private class TestController - { - } + private class TestController + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/OperationImportRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/OperationImportRoutingConventionTests.cs index 696e20bd1..08b2bd431 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/OperationImportRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/OperationImportRoutingConventionTests.cs @@ -18,154 +18,153 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +public class OperationImportRoutingConventionTests { - public class OperationImportRoutingConventionTests - { - private static OperationImportRoutingConvention ImportConvention = ConventionHelpers.CreateConvention(); - private static IEdmModel EdmModel = GetEdmModel(); + private static OperationImportRoutingConvention ImportConvention = ConventionHelpers.CreateConvention(); + private static IEdmModel EdmModel = GetEdmModel(); - [Fact] - public void AppliesToControllerAndActionOnOperationImportRoutingConvention_Throws_Context() - { - // Arrange - OperationImportRoutingConvention convention = new OperationImportRoutingConvention(); + [Fact] + public void AppliesToControllerAndActionOnOperationImportRoutingConvention_Throws_Context() + { + // Arrange + OperationImportRoutingConvention convention = new OperationImportRoutingConvention(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToController(null), "context"); - ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToAction(null), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToController(null), "context"); + ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToAction(null), "context"); + } - [Theory] - [InlineData(typeof(ODataOperationImportController), true)] - [InlineData(typeof(UnknownController), false)] - public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + [Theory] + [InlineData(typeof(ODataOperationImportController), true)] + [InlineData(typeof(UnknownController), false)] + public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - // Act - bool actual = ImportConvention.AppliesToController(context); + // Act + bool actual = ImportConvention.AppliesToController(context); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - public static TheoryDataSet ImportRoutingConventionTestData + public static TheoryDataSet ImportRoutingConventionTestData + { + get { - get + Type controller = typeof(ODataOperationImportController); + MethodInfo method1 = controller.GetMethod("CalcByRating", new Type[] { typeof(int) }); + MethodInfo method2 = controller.GetMethod("CalcByRating", new Type[] { typeof(string) }); + return new TheoryDataSet() { - Type controller = typeof(ODataOperationImportController); - MethodInfo method1 = controller.GetMethod("CalcByRating", new Type[] { typeof(int) }); - MethodInfo method2 = controller.GetMethod("CalcByRating", new Type[] { typeof(string) }); - return new TheoryDataSet() { - { - method1, new[] { "/CalcByRating(order={order})" } - }, - { - method2, new[] { "/CalcByRating(name={name})" } - }, - { - controller.GetMethod("CalcByRatingAction"), new[] { "/CalcByRatingAction" } - } - }; - } + method1, new[] { "/CalcByRating(order={order})" } + }, + { + method2, new[] { "/CalcByRating(name={name})" } + }, + { + controller.GetMethod("CalcByRatingAction"), new[] { "/CalcByRatingAction" } + } + }; } + } - [Theory] - [MemberData(nameof(ImportRoutingConventionTestData))] - public void OperationImportRoutingConventionResolveFunctionOverloadAsExpected(MethodInfo method, string[] templates) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModelByMethodInfo(method); - ActionModel action = controller.Actions.First(); - - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; + [Theory] + [MemberData(nameof(ImportRoutingConventionTestData))] + public void OperationImportRoutingConventionResolveFunctionOverloadAsExpected(MethodInfo method, string[] templates) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModelByMethodInfo(method); + ActionModel action = controller.Actions.First(); - // Act - ImportConvention.AppliesToAction(context); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; - // Assert - Assert.Equal(templates.Length, action.Selectors.Count); - Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); - } + // Act + ImportConvention.AppliesToAction(context); - [Theory] - [InlineData("OtherFunctionImport")] - [InlineData("OtherActionImport")] - public void OperationImportRoutingConventionDoesNothingForNotSupportedAction(string actionName) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); + // Assert + Assert.Equal(templates.Length, action.Selectors.Count); + Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = controller.Actions.First(); + [Theory] + [InlineData("OtherFunctionImport")] + [InlineData("OtherActionImport")] + public void OperationImportRoutingConventionDoesNothingForNotSupportedAction(string actionName) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); - // Act - bool returnValue = ImportConvention.AppliesToAction(context); - Assert.True(returnValue); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = controller.Actions.First(); - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Null(selector.AttributeRouteModel); - } + // Act + bool returnValue = ImportConvention.AppliesToAction(context); + Assert.True(returnValue); - private static IEdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); - - IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - IEdmTypeReference stringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); - IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); - - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - EdmFunction calcByRating1 = new EdmFunction("NS", "CalcByRating", returnType, false, entitySetPathExpression: null, isComposable: false); - calcByRating1.AddParameter("order", intType); - model.AddElement(calcByRating1); - container.AddFunctionImport(calcByRating1); - - EdmFunction calcByRating2 = new EdmFunction("NS", "CalcByRating", returnType, false, entitySetPathExpression: null, isComposable: false); - calcByRating2.AddParameter("name", stringType); - model.AddElement(calcByRating2); - container.AddFunctionImport(calcByRating2); - - EdmAction calcByRatingAction = new EdmAction("NS", "CalcByRatingAction", returnType, false, null); - calcByRatingAction.AddParameter("name", stringType); - model.AddElement(calcByRatingAction); - container.AddActionImport(calcByRatingAction); - - model.AddElement(container); - return model; - } + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Null(selector.AttributeRouteModel); + } - private class ODataOperationImportController - { - [HttpGet] - public void CalcByRating(int order) - { } + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + + IEdmTypeReference returnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + IEdmTypeReference stringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); + IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); + + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + EdmFunction calcByRating1 = new EdmFunction("NS", "CalcByRating", returnType, false, entitySetPathExpression: null, isComposable: false); + calcByRating1.AddParameter("order", intType); + model.AddElement(calcByRating1); + container.AddFunctionImport(calcByRating1); + + EdmFunction calcByRating2 = new EdmFunction("NS", "CalcByRating", returnType, false, entitySetPathExpression: null, isComposable: false); + calcByRating2.AddParameter("name", stringType); + model.AddElement(calcByRating2); + container.AddFunctionImport(calcByRating2); + + EdmAction calcByRatingAction = new EdmAction("NS", "CalcByRatingAction", returnType, false, null); + calcByRatingAction.AddParameter("name", stringType); + model.AddElement(calcByRatingAction); + container.AddActionImport(calcByRatingAction); + + model.AddElement(container); + return model; + } - [HttpGet] - public void CalcByRating(string name) - { } + private class ODataOperationImportController + { + [HttpGet] + public void CalcByRating(int order) + { } - [HttpPost] - public void CalcByRatingAction(ODataActionParameters parameters) - { } + [HttpGet] + public void CalcByRating(string name) + { } - [HttpGet] - public void OtherFunctionImport(string name) - { } + [HttpPost] + public void CalcByRatingAction(ODataActionParameters parameters) + { } - [HttpPost] - public void OtherActionImport(ODataActionParameters parameters) - { } - } + [HttpGet] + public void OtherFunctionImport(string name) + { } - private class UnknownController + [HttpPost] + public void OtherActionImport(ODataActionParameters parameters) { } } + + private class UnknownController + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/OperationRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/OperationRoutingConventionTests.cs index 8b01268f5..bd6403040 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/OperationRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/OperationRoutingConventionTests.cs @@ -8,25 +8,24 @@ using Microsoft.AspNetCore.OData.Routing.Conventions; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +public class OperationRoutingConventionTests { - public class OperationRoutingConventionTests + [Theory] + [InlineData("", "", null, false)] + [InlineData("IsUpdate", "IsUpdate", null, false)] + [InlineData("IsUpdateOnVipCustomer", "IsUpdate", "VipCustomer", false)] + [InlineData("IsUpdateOnCollectionOfVipCustomer", "IsUpdate", "VipCustomer", true)] + [InlineData("GetMostProfitableOnCollectionOfVIP", "GetMostProfitable", "VIP", true)] + public void SplitActionNameWorksAsExpected(string action, string operation, string cast, bool isCollection) { - [Theory] - [InlineData("", "", null, false)] - [InlineData("IsUpdate", "IsUpdate", null, false)] - [InlineData("IsUpdateOnVipCustomer", "IsUpdate", "VipCustomer", false)] - [InlineData("IsUpdateOnCollectionOfVipCustomer", "IsUpdate", "VipCustomer", true)] - [InlineData("GetMostProfitableOnCollectionOfVIP", "GetMostProfitable", "VIP", true)] - public void SplitActionNameWorksAsExpected(string action, string operation, string cast, bool isCollection) - { - // Arrange - string actual = OperationRoutingConvention.SplitActionName(action, out string actualCast, out bool actualIsCollection); + // Arrange + string actual = OperationRoutingConvention.SplitActionName(action, out string actualCast, out bool actualIsCollection); - // Act & Assert - Assert.Equal(actual, operation); - Assert.Equal(actualCast, cast); - Assert.Equal(actualIsCollection, isCollection); - } + // Act & Assert + Assert.Equal(actual, operation); + Assert.Equal(actualCast, cast); + Assert.Equal(actualIsCollection, isCollection); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/PropertyRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/PropertyRoutingConventionTests.cs index cd21b3381..514f8b4e0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/PropertyRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/PropertyRoutingConventionTests.cs @@ -16,433 +16,432 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +public class PropertyRoutingConventionTests { - public class PropertyRoutingConventionTests - { - private static PropertyRoutingConvention PropertyConvention = ConventionHelpers.CreateConvention(); - private static IEdmModel EdmModel = GetEdmModel(); + private static PropertyRoutingConvention PropertyConvention = ConventionHelpers.CreateConvention(); + private static IEdmModel EdmModel = GetEdmModel(); - [Fact] - public void AppliesToActionOnPropertyRoutingConvention_Throws_Context() - { - // Arrange - PropertyRoutingConvention convention = new PropertyRoutingConvention(); + [Fact] + public void AppliesToActionOnPropertyRoutingConvention_Throws_Context() + { + // Arrange + PropertyRoutingConvention convention = new PropertyRoutingConvention(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToController(null), "context"); - ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToAction(null), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToController(null), "context"); + ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToAction(null), "context"); + } - [Theory] - [InlineData(typeof(CustomersController), true)] - [InlineData(typeof(MeController), true)] - [InlineData(typeof(UnknownController), false)] - public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + [Theory] + [InlineData(typeof(CustomersController), true)] + [InlineData(typeof(MeController), true)] + [InlineData(typeof(UnknownController), false)] + public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - // Act - bool actual = PropertyConvention.AppliesToController(context); + // Act + bool actual = PropertyConvention.AppliesToController(context); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - public static TheoryDataSet PropertyRoutingConventionTestData + public static TheoryDataSet PropertyRoutingConventionTestData + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() + // Get { - // Get + typeof(CustomersController), + "GetName", + new[] { - typeof(CustomersController), - "GetName", - new[] - { - "/Customers({key})/Name", - "/Customers/{key}/Name", - "/Customers({key})/Name/$value", - "/Customers/{key}/Name/$value" - } - }, - { typeof(MeController), "GetName", new[] { "/Me/Name", "/Me/Name/$value" } }, + "/Customers({key})/Name", + "/Customers/{key}/Name", + "/Customers({key})/Name/$value", + "/Customers/{key}/Name/$value" + } + }, + { typeof(MeController), "GetName", new[] { "/Me/Name", "/Me/Name/$value" } }, + { + typeof(CustomersController), + "GetEmails", + new[] { - typeof(CustomersController), - "GetEmails", - new[] - { - "/Customers({key})/Emails", - "/Customers/{key}/Emails", - "/Customers({key})/Emails/$count", - "/Customers/{key}/Emails/$count" - } - }, - { typeof(MeController), "GetEmails", new[] { "/Me/Emails", "/Me/Emails/$count" } }, - - // Get complex property - { typeof(CustomersController), "GetAddress", new[] { "/Customers({key})/Address", "/Customers/{key}/Address" } }, - { typeof(MeController), "GetAddress", new[] { "/Me/Address" } }, + "/Customers({key})/Emails", + "/Customers/{key}/Emails", + "/Customers({key})/Emails/$count", + "/Customers/{key}/Emails/$count" + } + }, + { typeof(MeController), "GetEmails", new[] { "/Me/Emails", "/Me/Emails/$count" } }, + + // Get complex property + { typeof(CustomersController), "GetAddress", new[] { "/Customers({key})/Address", "/Customers/{key}/Address" } }, + { typeof(MeController), "GetAddress", new[] { "/Me/Address" } }, + { + typeof(CustomersController), + "GetLocations", + new[] { - typeof(CustomersController), - "GetLocations", - new[] - { - "/Customers({key})/Locations", - "/Customers/{key}/Locations", - "/Customers({key})/Locations/$count", - "/Customers/{key}/Locations/$count" - } - }, - { typeof(MeController), "GetLocations", new[] { "/Me/Locations", "/Me/Locations/$count" } }, - - // Post - { typeof(CustomersController), "PostToEmails", new[] { "/Customers({key})/Emails", "/Customers/{key}/Emails" } }, - { typeof(MeController), "PostToEmails", new[] { "/Me/Emails" } }, - - // Put, Patch, Delete - { typeof(CustomersController), "PutToName", new[] { "/Customers({key})/Name", "/Customers/{key}/Name" } }, - { typeof(CustomersController), "PatchToName", new[] { "/Customers({key})/Name", "/Customers/{key}/Name" } }, - { typeof(CustomersController), "DeleteToName", new[] { "/Customers({key})/Name", "/Customers/{key}/Name" } }, - { typeof(MeController), "PutToName", new[] { "/Me/Name" } }, - { typeof(MeController), "PatchToName", new[] { "/Me/Name" } }, - { typeof(MeController), "DeleteToName", new[] { "/Me/Name" } }, - - // with type cast + "/Customers({key})/Locations", + "/Customers/{key}/Locations", + "/Customers({key})/Locations/$count", + "/Customers/{key}/Locations/$count" + } + }, + { typeof(MeController), "GetLocations", new[] { "/Me/Locations", "/Me/Locations/$count" } }, + + // Post + { typeof(CustomersController), "PostToEmails", new[] { "/Customers({key})/Emails", "/Customers/{key}/Emails" } }, + { typeof(MeController), "PostToEmails", new[] { "/Me/Emails" } }, + + // Put, Patch, Delete + { typeof(CustomersController), "PutToName", new[] { "/Customers({key})/Name", "/Customers/{key}/Name" } }, + { typeof(CustomersController), "PatchToName", new[] { "/Customers({key})/Name", "/Customers/{key}/Name" } }, + { typeof(CustomersController), "DeleteToName", new[] { "/Customers({key})/Name", "/Customers/{key}/Name" } }, + { typeof(MeController), "PutToName", new[] { "/Me/Name" } }, + { typeof(MeController), "PatchToName", new[] { "/Me/Name" } }, + { typeof(MeController), "DeleteToName", new[] { "/Me/Name" } }, + + // with type cast + { + typeof(CustomersController), + "GetSubAddressFromVipCustomer", + new[] { - typeof(CustomersController), - "GetSubAddressFromVipCustomer", - new[] - { - "/Customers({key})/NS.VipCustomer/SubAddress", - "/Customers/{key}/NS.VipCustomer/SubAddress" - } - }, + "/Customers({key})/NS.VipCustomer/SubAddress", + "/Customers/{key}/NS.VipCustomer/SubAddress" + } + }, + { + typeof(CustomersController), + "PutToLocationsOfUsAddress", + new[] { - typeof(CustomersController), - "PutToLocationsOfUsAddress", - new[] - { - "/Customers({key})/Locations/NS.UsAddress", - "/Customers/{key}/Locations/NS.UsAddress" - } - }, + "/Customers({key})/Locations/NS.UsAddress", + "/Customers/{key}/Locations/NS.UsAddress" + } + }, + { + typeof(CustomersController), + "PatchToSubAddressOfCnAddressFromVipCustomer", + new[] { - typeof(CustomersController), - "PatchToSubAddressOfCnAddressFromVipCustomer", - new[] - { - "/Customers({key})/NS.VipCustomer/SubAddress/NS.CnAddress", - "/Customers/{key}/NS.VipCustomer/SubAddress/NS.CnAddress" - } - }, + "/Customers({key})/NS.VipCustomer/SubAddress/NS.CnAddress", + "/Customers/{key}/NS.VipCustomer/SubAddress/NS.CnAddress" + } + }, + { + typeof(MeController), + "PutToSubAddressOfCnAddressFromVipCustomer", + new[] { - typeof(MeController), - "PutToSubAddressOfCnAddressFromVipCustomer", - new[] - { - "/Me/NS.VipCustomer/SubAddress/NS.CnAddress" - } - }, + "/Me/NS.VipCustomer/SubAddress/NS.CnAddress" + } + }, + { + typeof(MeController), + "PostToSubLocationsOfUsAddressFromVipCustomer", + new[] { - typeof(MeController), - "PostToSubLocationsOfUsAddressFromVipCustomer", - new[] - { - "/Me/NS.VipCustomer/SubLocations/NS.UsAddress" - } - }, + "/Me/NS.VipCustomer/SubLocations/NS.UsAddress" + } + }, + { + typeof(MeController), + "GetSubLocationsOfUsAddressFromVipCustomer", + new[] { - typeof(MeController), - "GetSubLocationsOfUsAddressFromVipCustomer", - new[] - { - "/Me/NS.VipCustomer/SubLocations/NS.UsAddress", - "/Me/NS.VipCustomer/SubLocations/NS.UsAddress/$count" - } + "/Me/NS.VipCustomer/SubLocations/NS.UsAddress", + "/Me/NS.VipCustomer/SubLocations/NS.UsAddress/$count" } - }; - } + } + }; } + } - [Theory] - [MemberData(nameof(PropertyRoutingConventionTestData))] - public void PropertyRoutingConventionTestDataRunsAsExpected(Type controllerType, string actionName, string[] templates) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [MemberData(nameof(PropertyRoutingConventionTestData))] + public void PropertyRoutingConventionTestDataRunsAsExpected(Type controllerType, string actionName, string[] templates) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; - // Act - bool returnValue = PropertyConvention.AppliesToAction(context); - Assert.True(returnValue); + // Act + bool returnValue = PropertyConvention.AppliesToAction(context); + Assert.True(returnValue); - // Assert - Assert.Equal(templates.Length, action.Selectors.Count); - Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); - } + // Assert + Assert.Equal(templates.Length, action.Selectors.Count); + Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } - [Theory] - [InlineData(typeof(MeController), "GetEmails", 1, false)] - [InlineData(typeof(MeController), "GetEmails", 2, true)] - [InlineData(typeof(MeController), "GetSubLocationsOfUsAddressFromVipCustomer", 1, false)] - [InlineData(typeof(MeController), "GetSubLocationsOfUsAddressFromVipCustomer", 2, true)] - public void PropertyRoutingConventionAddDollarCountAsExpectedBasedOnConfiguration(Type controllerType, string actionName, int expectCount, bool enableDollarCount) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [InlineData(typeof(MeController), "GetEmails", 1, false)] + [InlineData(typeof(MeController), "GetEmails", 2, true)] + [InlineData(typeof(MeController), "GetSubLocationsOfUsAddressFromVipCustomer", 1, false)] + [InlineData(typeof(MeController), "GetSubLocationsOfUsAddressFromVipCustomer", 2, true)] + public void PropertyRoutingConventionAddDollarCountAsExpectedBasedOnConfiguration(Type controllerType, string actionName, int expectCount, bool enableDollarCount) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType, actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; - context.Options.RouteOptions.EnableDollarCountRouting = enableDollarCount; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + context.Options.RouteOptions.EnableDollarCountRouting = enableDollarCount; - // Act - bool returnValue = PropertyConvention.AppliesToAction(context); - Assert.True(returnValue); + // Act + bool returnValue = PropertyConvention.AppliesToAction(context); + Assert.True(returnValue); - // Assert - Assert.Equal(expectCount, action.Selectors.Count); - if (enableDollarCount) - { - Assert.Contains("/$count", string.Join(",", action.Selectors.Select(s => s.AttributeRouteModel.Template))); - } - else - { - Assert.DoesNotContain("/$count", string.Join(",", action.Selectors.Select(s => s.AttributeRouteModel.Template))); - } + // Assert + Assert.Equal(expectCount, action.Selectors.Count); + if (enableDollarCount) + { + Assert.Contains("/$count", string.Join(",", action.Selectors.Select(s => s.AttributeRouteModel.Template))); } - - [Theory] - [InlineData("PostToName")] - [InlineData("Get")] - [InlineData("PostToSubAddressOfUsAddressFromVipCustomer")] - [InlineData("GetSubAddressFrom")] - [InlineData("PostToSubAddressFrom")] - [InlineData("PutToSubAddressFrom")] - [InlineData("PatchToSubAddressFrom")] - [InlineData("DeleteToSubAddressFrom")] - [InlineData("GetSubAddressOfUsAddressFrom")] - [InlineData("PostToSubAddressOfUsAddressFrom")] - [InlineData("PutToSubAddressOfUsAddressFrom")] - [InlineData("PatchToSubAddressOfUsAddressFrom")] - [InlineData("DeleteToSubAddressOfUsAddressFrom")] - [InlineData("GetAddressOf")] - [InlineData("PostToLocationsOf")] - [InlineData("PutToAddressOf")] - [InlineData("PatchToAddressOf")] - public void PropertyRoutingConventionDoesNothingForNotSupportedAction(string actionName) + else { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); + Assert.DoesNotContain("/$count", string.Join(",", action.Selectors.Select(s => s.AttributeRouteModel.Template))); + } + } - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = controller.Actions.First(); + [Theory] + [InlineData("PostToName")] + [InlineData("Get")] + [InlineData("PostToSubAddressOfUsAddressFromVipCustomer")] + [InlineData("GetSubAddressFrom")] + [InlineData("PostToSubAddressFrom")] + [InlineData("PutToSubAddressFrom")] + [InlineData("PatchToSubAddressFrom")] + [InlineData("DeleteToSubAddressFrom")] + [InlineData("GetSubAddressOfUsAddressFrom")] + [InlineData("PostToSubAddressOfUsAddressFrom")] + [InlineData("PutToSubAddressOfUsAddressFrom")] + [InlineData("PatchToSubAddressOfUsAddressFrom")] + [InlineData("DeleteToSubAddressOfUsAddressFrom")] + [InlineData("GetAddressOf")] + [InlineData("PostToLocationsOf")] + [InlineData("PutToAddressOf")] + [InlineData("PatchToAddressOf")] + public void PropertyRoutingConventionDoesNothingForNotSupportedAction(string actionName) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); - // Act - bool returnValue = PropertyConvention.AppliesToAction(context); - Assert.False(returnValue); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = controller.Actions.First(); - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Null(selector.AttributeRouteModel); - } + // Act + bool returnValue = PropertyConvention.AppliesToAction(context); + Assert.False(returnValue); - private static IEdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); - - // Address - EdmComplexType address = new EdmComplexType("NS", "Address"); - address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); - model.AddElement(address); - - // CnAddess - EdmComplexType cnAddress = new EdmComplexType("NS", "CnAddress", address); - cnAddress.AddStructuralProperty("Postcode", EdmPrimitiveTypeKind.String); - model.AddElement(cnAddress); - - // UsAddress - EdmComplexType usAddress = new EdmComplexType("NS", "UsAddress", address); - usAddress.AddStructuralProperty("Zipcode", EdmPrimitiveTypeKind.Int32); - model.AddElement(usAddress); - - // Customer - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - customer.AddStructuralProperty("Emails", new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, false)))); - customer.AddStructuralProperty("Address", new EdmComplexTypeReference(address, false)); - customer.AddStructuralProperty("Locations", new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(address, false)))); - model.AddElement(customer); - - // VipCustomer - EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); - vipCustomer.AddStructuralProperty("SubAddress", new EdmComplexTypeReference(address, false)); - vipCustomer.AddStructuralProperty("SubLocations", new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(address, false)))); - model.AddElement(vipCustomer); - - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - container.AddEntitySet("Customers", customer); - container.AddEntitySet("AnotherCustomers", customer); - container.AddSingleton("Me", customer); - model.AddElement(container); - return model; - } + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Null(selector.AttributeRouteModel); + } - private class CustomersController - { - public void GetName(int key, CancellationToken cancellation) - { } + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + + // Address + EdmComplexType address = new EdmComplexType("NS", "Address"); + address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + model.AddElement(address); + + // CnAddess + EdmComplexType cnAddress = new EdmComplexType("NS", "CnAddress", address); + cnAddress.AddStructuralProperty("Postcode", EdmPrimitiveTypeKind.String); + model.AddElement(cnAddress); + + // UsAddress + EdmComplexType usAddress = new EdmComplexType("NS", "UsAddress", address); + usAddress.AddStructuralProperty("Zipcode", EdmPrimitiveTypeKind.Int32); + model.AddElement(usAddress); + + // Customer + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + customer.AddStructuralProperty("Emails", new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, false)))); + customer.AddStructuralProperty("Address", new EdmComplexTypeReference(address, false)); + customer.AddStructuralProperty("Locations", new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(address, false)))); + model.AddElement(customer); + + // VipCustomer + EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); + vipCustomer.AddStructuralProperty("SubAddress", new EdmComplexTypeReference(address, false)); + vipCustomer.AddStructuralProperty("SubLocations", new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(address, false)))); + model.AddElement(vipCustomer); + + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + container.AddEntitySet("Customers", customer); + container.AddEntitySet("AnotherCustomers", customer); + container.AddSingleton("Me", customer); + model.AddElement(container); + return model; + } - public void GetAddress(int key, CancellationToken cancellation) - { } + private class CustomersController + { + public void GetName(int key, CancellationToken cancellation) + { } - public void GetLocations(int key) - { } + public void GetAddress(int key, CancellationToken cancellation) + { } - public void GetEmails(int key) - { } + public void GetLocations(int key) + { } - public void PutToName(int key) - { } + public void GetEmails(int key) + { } - public void PatchToName(CancellationToken cancellation, int key) - { } + public void PutToName(int key) + { } - public void DeleteToName(int key) - { } + public void PatchToName(CancellationToken cancellation, int key) + { } - public void PostToEmails(int key) - { } + public void DeleteToName(int key) + { } - public void GetSubAddressFromVipCustomer(int key) - { } + public void PostToEmails(int key) + { } - public void PutToLocationsOfUsAddress(int key) - { } + public void GetSubAddressFromVipCustomer(int key) + { } - // PATCH ~/Customers(1)/NS.VipCustomer/SubAddress/NS.CnAddress - public void PatchToSubAddressOfCnAddressFromVipCustomer(int key) - { } - } + public void PutToLocationsOfUsAddress(int key) + { } - private class MeController - { - public void GetName(CancellationToken cancellation) - { } + // PATCH ~/Customers(1)/NS.VipCustomer/SubAddress/NS.CnAddress + public void PatchToSubAddressOfCnAddressFromVipCustomer(int key) + { } + } - public void GetAddress() - { } + private class MeController + { + public void GetName(CancellationToken cancellation) + { } - public void GetEmails() - { } + public void GetAddress() + { } - public void GetLocations() - { } + public void GetEmails() + { } - public void PutToName() - { } + public void GetLocations() + { } - public void PatchToName(CancellationToken cancellation) - { } + public void PutToName() + { } - public void DeleteToName() - { } + public void PatchToName(CancellationToken cancellation) + { } - public void PostToEmails() - { } + public void DeleteToName() + { } - // Get ~/Me/NS.VipCustomer/SubAddress/CN.UsAddress - public void GetSubLocationsOfUsAddressFromVipCustomer() - { } + public void PostToEmails() + { } - // PATCH ~/Me/NS.VipCustomer/SubAddress/CN.CnAddress - public void PutToSubAddressOfCnAddressFromVipCustomer() - { } + // Get ~/Me/NS.VipCustomer/SubAddress/CN.UsAddress + public void GetSubLocationsOfUsAddressFromVipCustomer() + { } - // Post ~/Me/NS.VipCustomer/SubAddress/CN.UsAddress - public void PostToSubLocationsOfUsAddressFromVipCustomer() - { } - } + // PATCH ~/Me/NS.VipCustomer/SubAddress/CN.CnAddress + public void PutToSubAddressOfCnAddressFromVipCustomer() + { } - private class AnotherCustomersController - { - public void PostToName(string keyLastName, string keyFirstName) - { } + // Post ~/Me/NS.VipCustomer/SubAddress/CN.UsAddress + public void PostToSubLocationsOfUsAddressFromVipCustomer() + { } + } - public void Get(int key) - { } + private class AnotherCustomersController + { + public void PostToName(string keyLastName, string keyFirstName) + { } - // Post to non-collection is not allowed. - public void PostToSubAddressOfUsAddressFromVipCustomer() - { } + public void Get(int key) + { } - public void GetSubAddressFrom(int key) - { - } + // Post to non-collection is not allowed. + public void PostToSubAddressOfUsAddressFromVipCustomer() + { } - public void PostToSubAddressFrom(int key) - { - } + public void GetSubAddressFrom(int key) + { + } - public void PutToSubAddressFrom(int key) - { - } + public void PostToSubAddressFrom(int key) + { + } - public void PatchToSubAddressFrom(int key) - { - } + public void PutToSubAddressFrom(int key) + { + } - public void DeleteToSubAddressFrom(int key) - { - } + public void PatchToSubAddressFrom(int key) + { + } - public void GetSubAddressOfUsAddressFrom(int key) - { - } + public void DeleteToSubAddressFrom(int key) + { + } - public void PostToSubAddressOfUsAddressFrom(int key) - { - } + public void GetSubAddressOfUsAddressFrom(int key) + { + } - public void PutToSubAddressOfUsAddressFrom(int key) - { - } + public void PostToSubAddressOfUsAddressFrom(int key) + { + } - public void PatchToSubAddressOfUsAddressFrom(int key) - { - } + public void PutToSubAddressOfUsAddressFrom(int key) + { + } - public void DeleteToSubAddressOfUsAddressFrom(int key) - { - } + public void PatchToSubAddressOfUsAddressFrom(int key) + { + } - public void GetAddressOf(int key) - { - } + public void DeleteToSubAddressOfUsAddressFrom(int key) + { + } - public void PostToLocationsOf(int key) - { - } + public void GetAddressOf(int key) + { + } - public void PutToAddressOf(int key) - { - } + public void PostToLocationsOf(int key) + { + } - public void PatchToAddressOf(int key) - { - } + public void PutToAddressOf(int key) + { } - private class UnknownController - { } + public void PatchToAddressOf(int key) + { + } } + + private class UnknownController + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/RefRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/RefRoutingConventionTests.cs index 79a7a122b..2b08ee064 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/RefRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/RefRoutingConventionTests.cs @@ -18,276 +18,275 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +public class RefRoutingConventionTests { - public class RefRoutingConventionTests - { - private static IEdmModel _edmModel = GetModel(); - private static RefRoutingConvention _refConvention = ConventionHelpers.CreateConvention(); + private static IEdmModel _edmModel = GetModel(); + private static RefRoutingConvention _refConvention = ConventionHelpers.CreateConvention(); - [Fact] - public void AppliesToControllerAndActionOnRefRoutingConvention_Throws_Context() - { - // Arrange - RefRoutingConvention convention = new RefRoutingConvention(); + [Fact] + public void AppliesToControllerAndActionOnRefRoutingConvention_Throws_Context() + { + // Arrange + RefRoutingConvention convention = new RefRoutingConvention(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToController(null), "context"); - ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToAction(null), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToController(null), "context"); + ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToAction(null), "context"); + } - [Fact] - public void SplitRefActionName_WorksAsExpected() - { - // Arranges & Act & Assert - string actionMethodName = "CreateRef"; - string method = RefRoutingConvention.SplitRefActionName(actionMethodName, out string httpMethod, out string prop, out string declaring); - Assert.Equal("Post,Put", httpMethod); - Assert.Equal("CreateRef", method); - Assert.Null(prop); - Assert.Null(declaring); - - // Arranges & Act & Assert - actionMethodName = "GetRefToPropertyFromAbc"; - method = RefRoutingConvention.SplitRefActionName(actionMethodName, out httpMethod, out prop, out declaring); - Assert.Equal("Get", httpMethod); - Assert.Equal("GetRef", method); - Assert.Equal("Property", prop); - Assert.Equal("Abc", declaring); - - // Arranges & Act & Assert - actionMethodName = "CreateRefFromAbcToProperty"; - method = RefRoutingConvention.SplitRefActionName(actionMethodName, out httpMethod, out prop, out declaring); - Assert.Null(method); - } + [Fact] + public void SplitRefActionName_WorksAsExpected() + { + // Arranges & Act & Assert + string actionMethodName = "CreateRef"; + string method = RefRoutingConvention.SplitRefActionName(actionMethodName, out string httpMethod, out string prop, out string declaring); + Assert.Equal("Post,Put", httpMethod); + Assert.Equal("CreateRef", method); + Assert.Null(prop); + Assert.Null(declaring); + + // Arranges & Act & Assert + actionMethodName = "GetRefToPropertyFromAbc"; + method = RefRoutingConvention.SplitRefActionName(actionMethodName, out httpMethod, out prop, out declaring); + Assert.Equal("Get", httpMethod); + Assert.Equal("GetRef", method); + Assert.Equal("Property", prop); + Assert.Equal("Abc", declaring); + + // Arranges & Act & Assert + actionMethodName = "CreateRefFromAbcToProperty"; + method = RefRoutingConvention.SplitRefActionName(actionMethodName, out httpMethod, out prop, out declaring); + Assert.Null(method); + } - public static TheoryDataSet RefConventionTestData + public static TheoryDataSet RefConventionTestData + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() + // Bound to single { - // Bound to single + "CreateRefToOrder", + new[] { - "CreateRefToOrder", - new[] - { - "/RefCustomers({key})/Order/$ref", - "/RefCustomers/{key}/Order/$ref" - } - }, + "/RefCustomers({key})/Order/$ref", + "/RefCustomers/{key}/Order/$ref" + } + }, + { + "DeleteRefToOrder", + new[] { - "DeleteRefToOrder", - new[] - { - "/RefCustomers({key})/Order/$ref", - "/RefCustomers/{key}/Order/$ref" - } - }, + "/RefCustomers({key})/Order/$ref", + "/RefCustomers/{key}/Order/$ref" + } + }, + { + "DeleteRefToOrders", + new[] { - "DeleteRefToOrders", - new[] - { - "/RefCustomers({key})/Orders({relatedKey})/$ref", - "/RefCustomers({key})/Orders/{relatedKey}/$ref", - "/RefCustomers/{key}/Orders({relatedKey})/$ref", - "/RefCustomers/{key}/Orders/{relatedKey}/$ref" - } + "/RefCustomers({key})/Orders({relatedKey})/$ref", + "/RefCustomers({key})/Orders/{relatedKey}/$ref", + "/RefCustomers/{key}/Orders({relatedKey})/$ref", + "/RefCustomers/{key}/Orders/{relatedKey}/$ref" } - }; - } + } + }; } + } - [Theory] - [MemberData(nameof(RefConventionTestData))] - public void RefRoutingConventionTestDataRunsAsExpected(string actionName, string[] templates) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [MemberData(nameof(RefConventionTestData))] + public void RefRoutingConventionTestDataRunsAsExpected(string actionName, string[] templates) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, _edmModel, controller); - context.Action = action; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, _edmModel, controller); + context.Action = action; - // Act - _refConvention.AppliesToAction(context); + // Act + _refConvention.AppliesToAction(context); - // Assert - Assert.Equal(templates.Length, action.Selectors.Count); - Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); - } + // Assert + Assert.Equal(templates.Length, action.Selectors.Count); + Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } - [Theory] - [InlineData("GetRefTo")] - [InlineData("CreateRefTo")] - [InlineData("DeleteRefTo")] - [InlineData("GetRefToSubOrderFrom")] - [InlineData("CreateRefToSubOrderFrom")] - [InlineData("DeleteRefToSubOrderFrom")] - public void RefRoutingConventionDoesNothingForNotSupportedAction(string actionName) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [InlineData("GetRefTo")] + [InlineData("CreateRefTo")] + [InlineData("DeleteRefTo")] + [InlineData("GetRefToSubOrderFrom")] + [InlineData("CreateRefToSubOrderFrom")] + [InlineData("DeleteRefToSubOrderFrom")] + public void RefRoutingConventionDoesNothingForNotSupportedAction(string actionName) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, _edmModel, controller); - context.Action = controller.Actions.First(); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, _edmModel, controller); + context.Action = controller.Actions.First(); - // Act - bool returnValue = _refConvention.AppliesToAction(context); - Assert.False(returnValue); + // Act + bool returnValue = _refConvention.AppliesToAction(context); + Assert.False(returnValue); - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Null(selector.AttributeRouteModel); - } + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Null(selector.AttributeRouteModel); + } - public static IEdmModel GetModel() - { - ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("RefCustomers"); - builder.EntitySet("RefOrders"); + public static IEdmModel GetModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("RefCustomers"); + builder.EntitySet("RefOrders"); - builder.EntitySet("RefAlbums"); - builder.EntitySet("RefArtists"); - builder.EntitySet("RefSongs"); + builder.EntitySet("RefAlbums"); + builder.EntitySet("RefArtists"); + builder.EntitySet("RefSongs"); - return builder.GetEdmModel(); + return builder.GetEdmModel(); + } + + private class RefCustomer + { + public int Id { get; set; } + public RefOrder Order { get; set; } + public RefOrder[] Orders { get; set; } + } + + private class RefOrder + { + public int Id { get; set; } + } + + private class RefCustomersController + { + [HttpPost] + public string CreateRefToOrder(int key, string navigationProperty) + { + return string.Format(CultureInfo.InvariantCulture, "CreateRef({0})({1})", key, navigationProperty); } - private class RefCustomer + public string DeleteRefToOrder(int key, string navigationProperty) { - public int Id { get; set; } - public RefOrder Order { get; set; } - public RefOrder[] Orders { get; set; } + return string.Format(CultureInfo.InvariantCulture, "DeleteRef({0})({1})", key, navigationProperty); } - private class RefOrder + public string DeleteRefToOrders(int key, int relatedKey, string navigationProperty) { - public int Id { get; set; } + return string.Format(CultureInfo.InvariantCulture, "DeleteRef({0})({1})({2})", key, relatedKey, navigationProperty); } - private class RefCustomersController + public void GetRefTo(int key, int relatedKey) { - [HttpPost] - public string CreateRefToOrder(int key, string navigationProperty) - { - return string.Format(CultureInfo.InvariantCulture, "CreateRef({0})({1})", key, navigationProperty); - } + } - public string DeleteRefToOrder(int key, string navigationProperty) - { - return string.Format(CultureInfo.InvariantCulture, "DeleteRef({0})({1})", key, navigationProperty); - } + public void CreateRefTo(int key, int relatedKey) + { + } - public string DeleteRefToOrders(int key, int relatedKey, string navigationProperty) - { - return string.Format(CultureInfo.InvariantCulture, "DeleteRef({0})({1})({2})", key, relatedKey, navigationProperty); - } + public void DeleteRefTo(int key, int relatedKey) + { + } - public void GetRefTo(int key, int relatedKey) - { - } + public void GetRefToSubOrderFrom(int key, int relatedKey) + { + } - public void CreateRefTo(int key, int relatedKey) - { - } + public void CreateRefToSubOrderFrom(int key, int relatedKey) + { + } - public void DeleteRefTo(int key, int relatedKey) - { - } + public void DeleteRefToSubOrderFrom(int key, int relatedKey) + { + } + } - public void GetRefToSubOrderFrom(int key, int relatedKey) - { - } + [Fact] + public void RefRoutingConventionIgnoreCaseNotUniquePropertyThrowsException() + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel("DeleteRefToSongs"); + ActionModel action = controller.Actions.First(); - public void CreateRefToSubOrderFrom(int key, int relatedKey) - { - } + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, _edmModel, controller); + context.Action = controller.Actions.First(); + context.Options.RouteOptions.EnablePropertyNameCaseInsensitive = true; - public void DeleteRefToSubOrderFrom(int key, int relatedKey) - { - } - } + // Act & Assert + Assert.Throws(() => _ = _refConvention.AppliesToAction(context)); + } - [Fact] - public void RefRoutingConventionIgnoreCaseNotUniquePropertyThrowsException() - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel("DeleteRefToSongs"); - ActionModel action = controller.Actions.First(); + [Theory] + [InlineData("DeleteRefToArtist", false, false)] + [InlineData("DeleteRefToArtist", true, true)] + [InlineData("DeleteRefToSONGS", false, true)] + [InlineData("DeleteRefToSONGS", true, true)] + public void RefRoutingConventionIgnoreCaseForTestData(string actionName, bool enablePropertyNameCaseInsensitive, bool expectedResult) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, _edmModel, controller); - context.Action = controller.Actions.First(); - context.Options.RouteOptions.EnablePropertyNameCaseInsensitive = true; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, _edmModel, controller); + context.Action = controller.Actions.First(); + context.Options.RouteOptions.EnablePropertyNameCaseInsensitive = enablePropertyNameCaseInsensitive; - // Act & Assert - Assert.Throws(() => _ = _refConvention.AppliesToAction(context)); - } + // Act + bool result = _refConvention.AppliesToAction(context); - [Theory] - [InlineData("DeleteRefToArtist", false, false)] - [InlineData("DeleteRefToArtist", true, true)] - [InlineData("DeleteRefToSONGS", false, true)] - [InlineData("DeleteRefToSONGS", true, true)] - public void RefRoutingConventionIgnoreCaseForTestData(string actionName, bool enablePropertyNameCaseInsensitive, bool expectedResult) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); + // Assert + Assert.Equal(expectedResult, result); + } - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, _edmModel, controller); - context.Action = controller.Actions.First(); - context.Options.RouteOptions.EnablePropertyNameCaseInsensitive = enablePropertyNameCaseInsensitive; + private class RefAlbum + { + public int Id { get; set; } - // Act - bool result = _refConvention.AppliesToAction(context); + public RefArtist artist { get; set; } - // Assert - Assert.Equal(expectedResult, result); - } + public RefSong[] SONGS { get; set; } - private class RefAlbum - { - public int Id { get; set; } + public RefSong[] songs { get; set; } + } - public RefArtist artist { get; set; } + private class RefArtist + { + public int Id { get; set; } - public RefSong[] SONGS { get; set; } + public string Name { get; set; } + } - public RefSong[] songs { get; set; } - } + private class RefSong + { + public int Id { get; set; } - private class RefArtist - { - public int Id { get; set; } + public RefAlbum Album { get; set; } + } - public string Name { get; set; } + private class RefAlbumsController + { + public string DeleteRefToArtist(int key) + { + return string.Format(CultureInfo.InvariantCulture, "DeleteRef({0})", key); } - private class RefSong + public string DeleteRefToSongs(int key, int relatedKey) { - public int Id { get; set; } - - public RefAlbum Album { get; set; } + return string.Format(CultureInfo.InvariantCulture, "DeleteRef({0})({1})", key, relatedKey); } - private class RefAlbumsController + public string DeleteRefToSONGS(int key, int relatedKey) { - public string DeleteRefToArtist(int key) - { - return string.Format(CultureInfo.InvariantCulture, "DeleteRef({0})", key); - } - - public string DeleteRefToSongs(int key, int relatedKey) - { - return string.Format(CultureInfo.InvariantCulture, "DeleteRef({0})({1})", key, relatedKey); - } - - public string DeleteRefToSONGS(int key, int relatedKey) - { - return string.Format(CultureInfo.InvariantCulture, "DeleteRef({0})({1})", key, relatedKey); - } + return string.Format(CultureInfo.InvariantCulture, "DeleteRef({0})({1})", key, relatedKey); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/SingletonRoutingConventionTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/SingletonRoutingConventionTests.cs index 40ff8a435..8a1eef5fb 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/SingletonRoutingConventionTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Conventions/SingletonRoutingConventionTests.cs @@ -15,224 +15,223 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions +namespace Microsoft.AspNetCore.OData.Tests.Routing.Conventions; + +public class SingletonRoutingConventionTests { - public class SingletonRoutingConventionTests - { - private static SingletonRoutingConvention SingletonConvention = ConventionHelpers.CreateConvention(); - private static IEdmModel EdmModel = GetEdmModel(); + private static SingletonRoutingConvention SingletonConvention = ConventionHelpers.CreateConvention(); + private static IEdmModel EdmModel = GetEdmModel(); - [Fact] - public void OrderOnSingletonAsExpected() - { - // Arrange & Act & Assert - Assert.Equal(200, SingletonConvention.Order); - } + [Fact] + public void OrderOnSingletonAsExpected() + { + // Arrange & Act & Assert + Assert.Equal(200, SingletonConvention.Order); + } - [Fact] - public void AppliesToActionOnSingletonRoutingConvention_Throws_Context() - { - // Arrange - SingletonRoutingConvention convention = new SingletonRoutingConvention(); + [Fact] + public void AppliesToActionOnSingletonRoutingConvention_Throws_Context() + { + // Arrange + SingletonRoutingConvention convention = new SingletonRoutingConvention(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToAction(null), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => convention.AppliesToAction(null), "context"); + } - [Theory] - [InlineData(typeof(CustomersController), false)] - [InlineData(typeof(MeController), true)] - public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + [Theory] + [InlineData(typeof(CustomersController), false)] + [InlineData(typeof(MeController), true)] + public void AppliesToControllerReturnsExpectedForController(Type controllerType, bool expected) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(controllerType); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - // Act - bool actual = SingletonConvention.AppliesToController(context); + // Act + bool actual = SingletonConvention.AppliesToController(context); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - public static TheoryDataSet SingletonConventionTestData + public static TheoryDataSet SingletonConventionTestData + { + get { - get - { - return new TheoryDataSet() - { - // Bound to single - { "Get", new[] { "/Me" } }, - { "GetFromVipCustomer", new[] { "/Me/NS.VipCustomer" } }, - { "Put", new[] { "/Me" } }, - { "PutFromVipCustomer", new[] { "/Me/NS.VipCustomer" } }, - { "Patch", new[] { "/Me" } }, - { "PatchFromVipCustomer", new[] { "/Me/NS.VipCustomer" } }, - }; - } + return new TheoryDataSet() + { + // Bound to single + { "Get", new[] { "/Me" } }, + { "GetFromVipCustomer", new[] { "/Me/NS.VipCustomer" } }, + { "Put", new[] { "/Me" } }, + { "PutFromVipCustomer", new[] { "/Me/NS.VipCustomer" } }, + { "Patch", new[] { "/Me" } }, + { "PatchFromVipCustomer", new[] { "/Me/NS.VipCustomer" } }, + }; } + } - public static TheoryDataSet SingletonConventionCaseInsensitiveTestData + public static TheoryDataSet SingletonConventionCaseInsensitiveTestData + { + get { - get - { - return new TheoryDataSet() - { - // Bound to single - { "GET", new[] { "/CaseInsensitiveMe" } }, - { "GETFromVIPCUSTOMER", new[] { "/CaseInsensitiveMe/NS.VipCustomer" } }, - { "PUT", new[] { "/CaseInsensitiveMe" } }, - { "PUTFromVIPCUSTOMER", new[] { "/CaseInsensitiveMe/NS.VipCustomer" } }, - { "PATCH", new[] { "/CaseInsensitiveMe" } }, - { "PATCHFromVIPCUSTOMER", new[] { "/CaseInsensitiveMe/NS.VipCustomer" } }, - }; - } + return new TheoryDataSet() + { + // Bound to single + { "GET", new[] { "/CaseInsensitiveMe" } }, + { "GETFromVIPCUSTOMER", new[] { "/CaseInsensitiveMe/NS.VipCustomer" } }, + { "PUT", new[] { "/CaseInsensitiveMe" } }, + { "PUTFromVIPCUSTOMER", new[] { "/CaseInsensitiveMe/NS.VipCustomer" } }, + { "PATCH", new[] { "/CaseInsensitiveMe" } }, + { "PATCHFromVIPCUSTOMER", new[] { "/CaseInsensitiveMe/NS.VipCustomer" } }, + }; } + } - [Theory] - [MemberData(nameof(SingletonConventionTestData))] - public void SingletonRoutingConventionTestDataRunsAsExpected(string actionName, string[] templates) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [MemberData(nameof(SingletonConventionTestData))] + public void SingletonRoutingConventionTestDataRunsAsExpected(string actionName, string[] templates) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; - // Act - SingletonConvention.AppliesToAction(context); + // Act + SingletonConvention.AppliesToAction(context); - // Assert - Assert.Equal(templates.Length, action.Selectors.Count); - Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); - } + // Assert + Assert.Equal(templates.Length, action.Selectors.Count); + Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } - [Theory] - [MemberData(nameof(SingletonConventionCaseInsensitiveTestData))] - public void SingletonRoutingConventionCaseInsensitiveTestDataRunsAsExpected(string actionName, string[] templates) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [MemberData(nameof(SingletonConventionCaseInsensitiveTestData))] + public void SingletonRoutingConventionCaseInsensitiveTestDataRunsAsExpected(string actionName, string[] templates) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = action; - context.Options.RouteOptions.EnableActionNameCaseInsensitive = true; + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = action; + context.Options.RouteOptions.EnableActionNameCaseInsensitive = true; - // Act - SingletonConvention.AppliesToAction(context); + // Act + SingletonConvention.AppliesToAction(context); - // Assert - Assert.Equal(templates.Length, action.Selectors.Count); - Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); - } + // Assert + Assert.Equal(templates.Length, action.Selectors.Count); + Assert.Equal(templates, action.Selectors.Select(s => s.AttributeRouteModel.Template)); + } - [Theory] - [InlineData("GetFrom")] - [InlineData("PutFrom")] - [InlineData("PatchFrom")] - public void SingletonRoutingConventionDoesNothingForNotSupportedAction(string actionName) - { - // Arrange - ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); - ActionModel action = controller.Actions.First(); + [Theory] + [InlineData("GetFrom")] + [InlineData("PutFrom")] + [InlineData("PatchFrom")] + public void SingletonRoutingConventionDoesNothingForNotSupportedAction(string actionName) + { + // Arrange + ControllerModel controller = ControllerModelHelpers.BuildControllerModel(actionName); + ActionModel action = controller.Actions.First(); - ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); - context.Action = controller.Actions.First(); + ODataControllerActionContext context = ODataControllerActionContextHelpers.BuildContext(string.Empty, EdmModel, controller); + context.Action = controller.Actions.First(); - // Act - bool returnValue = SingletonConvention.AppliesToAction(context); - Assert.False(returnValue); + // Act + bool returnValue = SingletonConvention.AppliesToAction(context); + Assert.False(returnValue); - // Assert - SelectorModel selector = Assert.Single(action.Selectors); - Assert.Null(selector.AttributeRouteModel); - } + // Assert + SelectorModel selector = Assert.Single(action.Selectors); + Assert.Null(selector.AttributeRouteModel); + } - private static IEdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + + // Customer + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); + model.AddElement(customer); + model.AddElement(vipCustomer); + var entityContainer = new EdmEntityContainer("NS", "Default"); + entityContainer.AddSingleton("Me", customer); + entityContainer.AddSingleton("CaseInsensitiveMe", customer); + model.AddElement(entityContainer); + return model; + } - // Customer - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); - model.AddElement(customer); - model.AddElement(vipCustomer); - var entityContainer = new EdmEntityContainer("NS", "Default"); - entityContainer.AddSingleton("Me", customer); - entityContainer.AddSingleton("CaseInsensitiveMe", customer); - model.AddElement(entityContainer); - return model; + private class MeController + { + public void Get() + { } - private class MeController + public void GetFromVipCustomer() { - public void Get() - { - } - - public void GetFromVipCustomer() - { - } + } - public void Put() - { - } + public void Put() + { + } - public void PutFromVipCustomer() - { - } + public void PutFromVipCustomer() + { + } - public void Patch() - { - } + public void Patch() + { + } - public void PatchFromVipCustomer() - { - } + public void PatchFromVipCustomer() + { + } - public void GetFrom() - { - } + public void GetFrom() + { + } - public void PutFrom() - { - } + public void PutFrom() + { + } - public void PatchFrom() - { - } - } + public void PatchFrom() + { + } + } - private class CaseInsensitiveMeController + private class CaseInsensitiveMeController + { + public void GET() { - public void GET() - { - } - - public void GETFromVIPCUSTOMER() - { - } + } - public void PUT() - { - } + public void GETFromVIPCUSTOMER() + { + } - public void PUTFromVIPCUSTOMER() - { - } + public void PUT() + { + } - public void PATCH() - { - } + public void PUTFromVIPCUSTOMER() + { + } - public void PATCHFromVIPCUSTOMER() - { - } + public void PATCH() + { } - private class CustomersController - { } + public void PATCHFromVIPCUSTOMER() + { + } } + + private class CustomersController + { } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataPathSegmentHandlerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataPathSegmentHandlerTests.cs index 1d93d6563..76417dd2c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataPathSegmentHandlerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataPathSegmentHandlerTests.cs @@ -14,418 +14,417 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing +namespace Microsoft.AspNetCore.OData.Tests.Routing; + +public class ODataPathSegmentHandlerTests { - public class ODataPathSegmentHandlerTests + [Fact] + public void ODataPathSegmentHandler_Handles_MetadataSegment() { - [Fact] - public void ODataPathSegmentHandler_Handles_MetadataSegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - // Act - handler.Handle(MetadataSegment.Instance); + // Act + handler.Handle(MetadataSegment.Instance); - // Assert - Assert.Equal("$metadata", handler.PathLiteral); - Assert.Null(handler.NavigationSource); - } + // Assert + Assert.Equal("$metadata", handler.PathLiteral); + Assert.Null(handler.NavigationSource); + } - [Fact] - public void ODataPathSegmentHandler_Handles_ValueSegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - IEdmType intType = EdmCoreModel.Instance.GetInt32(false).Definition; - ValueSegment segment = new ValueSegment(intType); + [Fact] + public void ODataPathSegmentHandler_Handles_ValueSegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + IEdmType intType = EdmCoreModel.Instance.GetInt32(false).Definition; + ValueSegment segment = new ValueSegment(intType); - // Act - handler.Handle(segment); + // Act + handler.Handle(segment); - // Assert - Assert.Equal("$value", handler.PathLiteral); - Assert.Null(handler.NavigationSource); - } + // Assert + Assert.Equal("$value", handler.PathLiteral); + Assert.Null(handler.NavigationSource); + } - [Fact] - public void ODataPathSegmentHandler_Handles_NavigationPropertyLinkSegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - - EdmEntityType customer = new EdmEntityType("NS", "customer"); - EdmEntityType order = new EdmEntityType("NS", "order"); - IEdmNavigationProperty ordersNavProperty = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "Orders", - Target = order, - TargetMultiplicity = EdmMultiplicity.Many - }); - NavigationPropertyLinkSegment segment = new NavigationPropertyLinkSegment(ordersNavProperty, null); - - // Act - handler.Handle(segment); - - // Assert - Assert.Equal("Orders/$ref", handler.PathLiteral); - Assert.Null(handler.NavigationSource); - } - - [Fact] - public void ODataPathSegmentHandler_Handles_CountSegment() + [Fact] + public void ODataPathSegmentHandler_Handles_NavigationPropertyLinkSegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + + EdmEntityType customer = new EdmEntityType("NS", "customer"); + EdmEntityType order = new EdmEntityType("NS", "order"); + IEdmNavigationProperty ordersNavProperty = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + Name = "Orders", + Target = order, + TargetMultiplicity = EdmMultiplicity.Many + }); + NavigationPropertyLinkSegment segment = new NavigationPropertyLinkSegment(ordersNavProperty, null); + + // Act + handler.Handle(segment); + + // Assert + Assert.Equal("Orders/$ref", handler.PathLiteral); + Assert.Null(handler.NavigationSource); + } - // Act - handler.Handle(CountSegment.Instance); + [Fact] + public void ODataPathSegmentHandler_Handles_CountSegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - // Assert - Assert.Equal("$count", handler.PathLiteral); - Assert.Null(handler.NavigationSource); - } + // Act + handler.Handle(CountSegment.Instance); - [Fact] - public void ODataPathSegmentHandler_Handles_DynamicPathSegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - DynamicPathSegment segment = new DynamicPathSegment("any"); + // Assert + Assert.Equal("$count", handler.PathLiteral); + Assert.Null(handler.NavigationSource); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentHandler_Handles_DynamicPathSegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + DynamicPathSegment segment = new DynamicPathSegment("any"); - // Assert - Assert.Equal("any", handler.PathLiteral); - Assert.Null(handler.NavigationSource); - } + // Act + handler.Handle(segment); - [Fact] - public void ODataPathSegmentHandler_Handles_ActionSegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); - IEdmAction action = new EdmAction("NS", "action", intType); - OperationSegment segment = new OperationSegment(action, null); - - // Act - handler.Handle(segment); - - // Assert - Assert.Equal("NS.action", handler.PathLiteral); - Assert.Null(handler.NavigationSource); - } - - [Fact] - public void ODataPathSegmentHandler_Handles_FunctionSegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); + // Assert + Assert.Equal("any", handler.PathLiteral); + Assert.Null(handler.NavigationSource); + } - IEdmFunction function = new EdmFunction("NS", "function", intType); - OperationSegment segment = new OperationSegment(function, null); + [Fact] + public void ODataPathSegmentHandler_Handles_ActionSegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); + IEdmAction action = new EdmAction("NS", "action", intType); + OperationSegment segment = new OperationSegment(action, null); + + // Act + handler.Handle(segment); + + // Assert + Assert.Equal("NS.action", handler.PathLiteral); + Assert.Null(handler.NavigationSource); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentHandler_Handles_FunctionSegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); - // Assert - Assert.Equal("NS.function()", handler.PathLiteral); - Assert.Null(handler.NavigationSource); - } + IEdmFunction function = new EdmFunction("NS", "function", intType); + OperationSegment segment = new OperationSegment(function, null); - [Fact] - public void ODataPathSegmentHandler_Handles_ActionImportSegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); + // Act + handler.Handle(segment); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - IEdmAction action = new EdmAction("NS", "action", intType); - IEdmActionImport actionImport = new EdmActionImport(entityContainer, "action", action); - OperationImportSegment segment = new OperationImportSegment(actionImport, null); + // Assert + Assert.Equal("NS.function()", handler.PathLiteral); + Assert.Null(handler.NavigationSource); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentHandler_Handles_ActionImportSegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); - // Assert - Assert.Equal("action", handler.PathLiteral); - Assert.Null(handler.NavigationSource); - } + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + IEdmAction action = new EdmAction("NS", "action", intType); + IEdmActionImport actionImport = new EdmActionImport(entityContainer, "action", action); + OperationImportSegment segment = new OperationImportSegment(actionImport, null); - [Fact] - public void ODataPathSegmentHandler_Handles_FunctionImportSegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); + // Act + handler.Handle(segment); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - IEdmFunction function = new EdmFunction("NS", "function", intType); - IEdmFunctionImport functionImport = new EdmFunctionImport(entityContainer, "function", function); - OperationImportSegment segment = new OperationImportSegment(functionImport, null); + // Assert + Assert.Equal("action", handler.PathLiteral); + Assert.Null(handler.NavigationSource); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentHandler_Handles_FunctionImportSegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); - // Assert - Assert.Equal("function()", handler.PathLiteral); - Assert.Null(handler.NavigationSource); - } + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + IEdmFunction function = new EdmFunction("NS", "function", intType); + IEdmFunctionImport functionImport = new EdmFunctionImport(entityContainer, "function", function); + OperationImportSegment segment = new OperationImportSegment(functionImport, null); - [Fact] - public void ODataPathSegmentHandler_Handles_PropertySegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + // Act + handler.Handle(segment); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - IEdmStructuralProperty property = customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - PropertySegment segment = new PropertySegment(property); + // Assert + Assert.Equal("function()", handler.PathLiteral); + Assert.Null(handler.NavigationSource); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentHandler_Handles_PropertySegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - // Assert - Assert.Equal("Name", handler.PathLiteral); - Assert.Null(handler.NavigationSource); - } + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + IEdmStructuralProperty property = customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + PropertySegment segment = new PropertySegment(property); - [Fact] - public void ODataPathSegmentHandler_Handles_SingletonSegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + // Act + handler.Handle(segment); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - EdmSingleton me = entityContainer.AddSingleton("me", customer); - SingletonSegment segment = new SingletonSegment(me); + // Assert + Assert.Equal("Name", handler.PathLiteral); + Assert.Null(handler.NavigationSource); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentHandler_Handles_SingletonSegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - // Assert - Assert.Equal("me", handler.PathLiteral); - Assert.Same(me, handler.NavigationSource); - } + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + EdmSingleton me = entityContainer.AddSingleton("me", customer); + SingletonSegment segment = new SingletonSegment(me); - [Fact] - public void ODataPathSegmentHandler_Handles_EntitySetSegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); - EntitySetSegment segment = new EntitySetSegment(customers); - - // Act - handler.Handle(segment); - - // Assert - Assert.Equal("Customers", handler.PathLiteral); - Assert.Same(customers, handler.NavigationSource); - } - - [Fact] - public void ODataPathSegmentHandler_Handles_NavigationPropertySegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - - EdmEntityType customer = new EdmEntityType("NS", "customer"); - EdmEntityType order = new EdmEntityType("NS", "order"); - IEdmNavigationProperty ordersNavProperty = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "Orders", - Target = order, - TargetMultiplicity = EdmMultiplicity.Many - }); - NavigationPropertySegment segment = new NavigationPropertySegment(ordersNavProperty, null); - - // Act - handler.Handle(segment); - - // Assert - Assert.Equal("Orders", handler.PathLiteral); - Assert.Null(handler.NavigationSource); - } - - [Fact] - public void ODataPathSegmentHandler_Handles_TypeCastSegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + // Act + handler.Handle(segment); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); - TypeSegment segment = new TypeSegment(vipCustomer, customer, customers); + // Assert + Assert.Equal("me", handler.PathLiteral); + Assert.Same(me, handler.NavigationSource); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentHandler_Handles_EntitySetSegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); + EntitySetSegment segment = new EntitySetSegment(customers); + + // Act + handler.Handle(segment); + + // Assert + Assert.Equal("Customers", handler.PathLiteral); + Assert.Same(customers, handler.NavigationSource); + } - // Assert - Assert.Equal("NS.VipCustomer", handler.PathLiteral); - Assert.Same(customers, handler.NavigationSource); - } + [Fact] + public void ODataPathSegmentHandler_Handles_NavigationPropertySegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - [Fact] - public void ODataPathSegmentHandler_Handles_PathTemplateSegment() + EdmEntityType customer = new EdmEntityType("NS", "customer"); + EdmEntityType order = new EdmEntityType("NS", "order"); + IEdmNavigationProperty ordersNavProperty = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - PathTemplateSegment segment = new PathTemplateSegment("{any}"); + Name = "Orders", + Target = order, + TargetMultiplicity = EdmMultiplicity.Many + }); + NavigationPropertySegment segment = new NavigationPropertySegment(ordersNavProperty, null); + + // Act + handler.Handle(segment); + + // Assert + Assert.Equal("Orders", handler.PathLiteral); + Assert.Null(handler.NavigationSource); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentHandler_Handles_TypeCastSegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - // Assert - Assert.Equal("{any}", handler.PathLiteral); - Assert.Null(handler.NavigationSource); - } + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); + TypeSegment segment = new TypeSegment(vipCustomer, customer, customers); - [Fact] - public void ODataPathSegmentHandler_Handles_BatchSegment() - { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + // Act + handler.Handle(segment); + + // Assert + Assert.Equal("NS.VipCustomer", handler.PathLiteral); + Assert.Same(customers, handler.NavigationSource); + } - // Act - handler.Handle(BatchSegment.Instance); + [Fact] + public void ODataPathSegmentHandler_Handles_PathTemplateSegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + PathTemplateSegment segment = new PathTemplateSegment("{any}"); - // Assert - Assert.Equal("$batch", handler.PathLiteral); - Assert.Null(handler.NavigationSource); - } + // Act + handler.Handle(segment); - [Fact] - public void ODataPathSegmentHandler_Handles_KeySegment() + // Assert + Assert.Equal("{any}", handler.PathLiteral); + Assert.Null(handler.NavigationSource); + } + + [Fact] + public void ODataPathSegmentHandler_Handles_BatchSegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + + // Act + handler.Handle(BatchSegment.Instance); + + // Assert + Assert.Equal("$batch", handler.PathLiteral); + Assert.Null(handler.NavigationSource); + } + + [Fact] + public void ODataPathSegmentHandler_Handles_KeySegment() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String)); + + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); + IDictionary keys = new Dictionary { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); + { "Id", "abc" } + }; - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String)); + KeySegment segment = new KeySegment(keys, customer, customers); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); - IDictionary keys = new Dictionary - { - { "Id", "abc" } - }; + // Act + handler.Handle(segment); + + // Assert + Assert.Equal("('abc')", handler.PathLiteral); + Assert.Same(customers, handler.NavigationSource); + } - KeySegment segment = new KeySegment(keys, customer, customers); + [Fact] + public void ODataPathSegmentHandler_Handles_KeySegment_AfterNavigationProperty() + { + // Arrange + ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - // Act - handler.Handle(segment); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - // Assert - Assert.Equal("('abc')", handler.PathLiteral); - Assert.Same(customers, handler.NavigationSource); - } + EdmEntityType order = new EdmEntityType("NS", "order"); + order.AddKeys(order.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - [Fact] - public void ODataPathSegmentHandler_Handles_KeySegment_AfterNavigationProperty() + IEdmNavigationProperty ordersNavProperty = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - // Arrange - ODataPathSegmentHandler handler = new ODataPathSegmentHandler(); - - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - - EdmEntityType order = new EdmEntityType("NS", "order"); - order.AddKeys(order.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - - IEdmNavigationProperty ordersNavProperty = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "Orders", - Target = order, - TargetMultiplicity = EdmMultiplicity.Many - }); - NavigationPropertyLinkSegment segment1 = new NavigationPropertyLinkSegment(ordersNavProperty, null); - - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - EdmEntitySet orders = entityContainer.AddEntitySet("Orders", order); - IDictionary keys = new Dictionary - { - { "Id", 42 } - }; - - KeySegment segment2 = new KeySegment(keys, order, orders); - - // Act - handler.Handle(segment1); - handler.Handle(segment2); - - // Assert - Assert.Equal("Orders(42)/$ref", handler.PathLiteral); - Assert.Same(orders, handler.NavigationSource); - } - - [Fact] - public void ConvertKeysToString_ConvertKeysValues() + Name = "Orders", + Target = order, + TargetMultiplicity = EdmMultiplicity.Many + }); + NavigationPropertyLinkSegment segment1 = new NavigationPropertyLinkSegment(ordersNavProperty, null); + + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + EdmEntitySet orders = entityContainer.AddEntitySet("Orders", order); + IDictionary keys = new Dictionary { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "Entity"); - IEdmStructuralProperty key1 = entityType.AddStructuralProperty("Id", EdmCoreModel.Instance.GetInt32(false)); - IEdmStructuralProperty key2 = entityType.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(false)); - - entityType.AddKeys(key1, key2); - IEnumerable> keys = new KeyValuePair[] - { - KeyValuePair.Create("Id", (object)4), - KeyValuePair.Create("Name", (object)"abc") - }; - - // Act - string actual = ODataPathSegmentHandler.ConvertKeysToString(keys, entityType); - - // Assert - Assert.Equal("Id=4,Name='abc'", actual); - } - - [Fact] - public void ConvertKeysToString_ConvertKeysValues_ShouldEscapeUriString() + { "Id", 42 } + }; + + KeySegment segment2 = new KeySegment(keys, order, orders); + + // Act + handler.Handle(segment1); + handler.Handle(segment2); + + // Assert + Assert.Equal("Orders(42)/$ref", handler.PathLiteral); + Assert.Same(orders, handler.NavigationSource); + } + + [Fact] + public void ConvertKeysToString_ConvertKeysValues() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "Entity"); + IEdmStructuralProperty key1 = entityType.AddStructuralProperty("Id", EdmCoreModel.Instance.GetInt32(false)); + IEdmStructuralProperty key2 = entityType.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(false)); + + entityType.AddKeys(key1, key2); + IEnumerable> keys = new KeyValuePair[] { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "Entity"); - IEdmStructuralProperty key1 = entityType.AddStructuralProperty("Id", EdmCoreModel.Instance.GetInt32(false)); - IEdmStructuralProperty key2 = entityType.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(false)); - - entityType.AddKeys(key1, key2); - IEnumerable> keys = new KeyValuePair[] - { - KeyValuePair.Create("Id", (object)4), - KeyValuePair.Create("Name", (object)"2425/&Foo") - }; - - // Act - string actual = ODataPathSegmentHandler.ConvertKeysToString(keys, entityType); - - // Assert - Assert.Equal("Id=4,Name='2425%2F&Foo'", actual); - } - - [Fact] - public void TranslateNode_TranslatesValue() + KeyValuePair.Create("Id", (object)4), + KeyValuePair.Create("Name", (object)"abc") + }; + + // Act + string actual = ODataPathSegmentHandler.ConvertKeysToString(keys, entityType); + + // Assert + Assert.Equal("Id=4,Name='abc'", actual); + } + + [Fact] + public void ConvertKeysToString_ConvertKeysValues_ShouldEscapeUriString() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "Entity"); + IEdmStructuralProperty key1 = entityType.AddStructuralProperty("Id", EdmCoreModel.Instance.GetInt32(false)); + IEdmStructuralProperty key2 = entityType.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(false)); + + entityType.AddKeys(key1, key2); + IEnumerable> keys = new KeyValuePair[] { - // Arrange & Act & Assert - UriTemplateExpression expression = KeySegmentTemplateTests.BuildExpression("{key}"); - ConstantNode node = new ConstantNode(expression); - Assert.Equal("{key}", ODataPathSegmentHandler.TranslateNode(node)); - - // Arrange & Act & Assert - EdmEnumType enumType = new EdmEnumType("NS", "Color"); - enumType.AddMember(new EdmEnumMember(enumType, "Red", new EdmEnumMemberValue(1))); - ODataEnumValue enumValue = new ODataEnumValue("Red", "NS.Color"); - node = new ConstantNode(enumValue); - Assert.Equal("NS.Color'Red'", ODataPathSegmentHandler.TranslateNode(node)); - } + KeyValuePair.Create("Id", (object)4), + KeyValuePair.Create("Name", (object)"2425/&Foo") + }; + + // Act + string actual = ODataPathSegmentHandler.ConvertKeysToString(keys, entityType); + + // Assert + Assert.Equal("Id=4,Name='2425%2F&Foo'", actual); + } + + [Fact] + public void TranslateNode_TranslatesValue() + { + // Arrange & Act & Assert + UriTemplateExpression expression = KeySegmentTemplateTests.BuildExpression("{key}"); + ConstantNode node = new ConstantNode(expression); + Assert.Equal("{key}", ODataPathSegmentHandler.TranslateNode(node)); + + // Arrange & Act & Assert + EdmEnumType enumType = new EdmEnumType("NS", "Color"); + enumType.AddMember(new EdmEnumMember(enumType, "Red", new EdmEnumMemberValue(1))); + ODataEnumValue enumValue = new ODataEnumValue("Red", "NS.Color"); + node = new ConstantNode(enumValue); + Assert.Equal("NS.Color'Red'", ODataPathSegmentHandler.TranslateNode(node)); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRouteOptionsTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRouteOptionsTests.cs index 27e15e035..2b8774d29 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRouteOptionsTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRouteOptionsTests.cs @@ -11,150 +11,149 @@ using Microsoft.OData; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing +namespace Microsoft.AspNetCore.OData.Tests.Routing; + +public class ODataRouteOptionsTests { - public class ODataRouteOptionsTests + [Fact] + public void DefaultODataRouteOptions_HasDefaultProperties() { - [Fact] - public void DefaultODataRouteOptions_HasDefaultProperties() - { - // Arrange & Act - ODataRouteOptions options = ODataRouteOptions.Default; - - // Assert - Assert.True(options.EnableKeyInParenthesis); - Assert.True(options.EnableKeyAsSegment); - Assert.True(options.EnableQualifiedOperationCall); - Assert.True(options.EnableUnqualifiedOperationCall); - Assert.False(options.EnableNonParenthesisForEmptyParameterFunction); - Assert.False(options.EnableActionNameCaseInsensitive); - Assert.False(options.EnableControllerNameCaseInsensitive); - Assert.False(options.EnablePropertyNameCaseInsensitive); - } - - [Fact] - public void CtorODataRouteOptions_HasDefaultProperties() - { - // Arrange & Act - ODataRouteOptions options = new ODataRouteOptions(); + // Arrange & Act + ODataRouteOptions options = ODataRouteOptions.Default; + + // Assert + Assert.True(options.EnableKeyInParenthesis); + Assert.True(options.EnableKeyAsSegment); + Assert.True(options.EnableQualifiedOperationCall); + Assert.True(options.EnableUnqualifiedOperationCall); + Assert.False(options.EnableNonParenthesisForEmptyParameterFunction); + Assert.False(options.EnableActionNameCaseInsensitive); + Assert.False(options.EnableControllerNameCaseInsensitive); + Assert.False(options.EnablePropertyNameCaseInsensitive); + } - // Assert - Assert.True(options.EnableKeyInParenthesis); - Assert.True(options.EnableKeyAsSegment); - Assert.True(options.EnableQualifiedOperationCall); - Assert.True(options.EnableUnqualifiedOperationCall); - Assert.False(options.EnableNonParenthesisForEmptyParameterFunction); - Assert.False(options.EnableActionNameCaseInsensitive); - Assert.False(options.EnableControllerNameCaseInsensitive); - Assert.False(options.EnablePropertyNameCaseInsensitive); - } - - [Fact] - public void ConfigProperties_WorksForEachProperty() - { - Verify(opt => opt.EnableKeyInParenthesis, (opt, b) => opt.EnableKeyInParenthesis = b); - Verify(opt => opt.EnableKeyAsSegment, (opt, b) => opt.EnableKeyAsSegment = b); - Verify(opt => opt.EnableQualifiedOperationCall, (opt, b) => opt.EnableQualifiedOperationCall = b); - Verify(opt => opt.EnableUnqualifiedOperationCall, (opt, b) => opt.EnableUnqualifiedOperationCall = b); - Verify(opt => opt.EnableNonParenthesisForEmptyParameterFunction, (opt, b) => opt.EnableNonParenthesisForEmptyParameterFunction = b, false); - Verify(opt => opt.EnableActionNameCaseInsensitive, (opt, b) => opt.EnableActionNameCaseInsensitive = b, false); - Verify(opt => opt.EnableControllerNameCaseInsensitive, (opt, b) => opt.EnableControllerNameCaseInsensitive = b, false); - Verify(opt => opt.EnablePropertyNameCaseInsensitive, (opt, b) => opt.EnablePropertyNameCaseInsensitive = b, false); - } - - private static void Verify(Func func, Action config, bool defValue = true) - { - // Arrange - ODataRouteOptions options = new ODataRouteOptions(); - Assert.Equal(defValue, func(options)); + [Fact] + public void CtorODataRouteOptions_HasDefaultProperties() + { + // Arrange & Act + ODataRouteOptions options = new ODataRouteOptions(); + + // Assert + Assert.True(options.EnableKeyInParenthesis); + Assert.True(options.EnableKeyAsSegment); + Assert.True(options.EnableQualifiedOperationCall); + Assert.True(options.EnableUnqualifiedOperationCall); + Assert.False(options.EnableNonParenthesisForEmptyParameterFunction); + Assert.False(options.EnableActionNameCaseInsensitive); + Assert.False(options.EnableControllerNameCaseInsensitive); + Assert.False(options.EnablePropertyNameCaseInsensitive); + } - // Act - config(options, !defValue); + [Fact] + public void ConfigProperties_WorksForEachProperty() + { + Verify(opt => opt.EnableKeyInParenthesis, (opt, b) => opt.EnableKeyInParenthesis = b); + Verify(opt => opt.EnableKeyAsSegment, (opt, b) => opt.EnableKeyAsSegment = b); + Verify(opt => opt.EnableQualifiedOperationCall, (opt, b) => opt.EnableQualifiedOperationCall = b); + Verify(opt => opt.EnableUnqualifiedOperationCall, (opt, b) => opt.EnableUnqualifiedOperationCall = b); + Verify(opt => opt.EnableNonParenthesisForEmptyParameterFunction, (opt, b) => opt.EnableNonParenthesisForEmptyParameterFunction = b, false); + Verify(opt => opt.EnableActionNameCaseInsensitive, (opt, b) => opt.EnableActionNameCaseInsensitive = b, false); + Verify(opt => opt.EnableControllerNameCaseInsensitive, (opt, b) => opt.EnableControllerNameCaseInsensitive = b, false); + Verify(opt => opt.EnablePropertyNameCaseInsensitive, (opt, b) => opt.EnablePropertyNameCaseInsensitive = b, false); + } + + private static void Verify(Func func, Action config, bool defValue = true) + { + // Arrange + ODataRouteOptions options = new ODataRouteOptions(); + Assert.Equal(defValue, func(options)); + + // Act + config(options, !defValue); + + // Assert + Assert.Equal(!defValue, func(options)); + } + + [Fact] + public void ConfigKeyOptions_DoesNotThrowsODataException() + { + // Arrange & Act & Assert + ODataRouteOptions options = new ODataRouteOptions(); + options.EnableKeyAsSegment = false; + options.EnableKeyInParenthesis = true; + + // Arrange & Act & Assert + options = new ODataRouteOptions(); + options.EnableKeyInParenthesis = false; + options.EnableKeyAsSegment = true; + } - // Assert - Assert.Equal(!defValue, func(options)); - } + [Fact] + public void ConfigKeyOptions_ThrowsODataException() + { + // Arrange + string expect = "The route option disables key in parenthesis and key as segment. At least one option should enable."; - [Fact] - public void ConfigKeyOptions_DoesNotThrowsODataException() + // Act & Assert + Action test = () => { - // Arrange & Act & Assert ODataRouteOptions options = new ODataRouteOptions(); options.EnableKeyAsSegment = false; - options.EnableKeyInParenthesis = true; - - // Arrange & Act & Assert - options = new ODataRouteOptions(); options.EnableKeyInParenthesis = false; - options.EnableKeyAsSegment = true; - } + }; + + ExceptionAssert.Throws(test, expect); - [Fact] - public void ConfigKeyOptions_ThrowsODataException() + // Act & Assert + test = () => { - // Arrange - string expect = "The route option disables key in parenthesis and key as segment. At least one option should enable."; - - // Act & Assert - Action test = () => - { - ODataRouteOptions options = new ODataRouteOptions(); - options.EnableKeyAsSegment = false; - options.EnableKeyInParenthesis = false; - }; - - ExceptionAssert.Throws(test, expect); - - // Act & Assert - test = () => - { - ODataRouteOptions options = new ODataRouteOptions(); - options.EnableKeyInParenthesis = false; - options.EnableKeyAsSegment = false; - }; - - ExceptionAssert.Throws(test, expect); - } - - [Fact] - public void ConfigOperationOptions_DoesNotThrowsODataException() + ODataRouteOptions options = new ODataRouteOptions(); + options.EnableKeyInParenthesis = false; + options.EnableKeyAsSegment = false; + }; + + ExceptionAssert.Throws(test, expect); + } + + [Fact] + public void ConfigOperationOptions_DoesNotThrowsODataException() + { + // Arrange & Act & Assert + ODataRouteOptions options = new ODataRouteOptions(); + options.EnableQualifiedOperationCall = false; + options.EnableUnqualifiedOperationCall = true; + + // Arrange & Act & Assert + options = new ODataRouteOptions(); + options.EnableUnqualifiedOperationCall = false; + options.EnableQualifiedOperationCall = true; + } + + [Fact] + public void ConfigOperationOptions_ThrowsODataException() + { + // Arrange + string expect = "The route option disables qualified and unqualified operation call. At least one option should enable."; + + // Act & Assert + Action test = () => { - // Arrange & Act & Assert ODataRouteOptions options = new ODataRouteOptions(); options.EnableQualifiedOperationCall = false; - options.EnableUnqualifiedOperationCall = true; - - // Arrange & Act & Assert - options = new ODataRouteOptions(); options.EnableUnqualifiedOperationCall = false; - options.EnableQualifiedOperationCall = true; - } + }; - [Fact] - public void ConfigOperationOptions_ThrowsODataException() + ExceptionAssert.Throws(test, expect); + + // Act & Assert + test = () => { - // Arrange - string expect = "The route option disables qualified and unqualified operation call. At least one option should enable."; - - // Act & Assert - Action test = () => - { - ODataRouteOptions options = new ODataRouteOptions(); - options.EnableQualifiedOperationCall = false; - options.EnableUnqualifiedOperationCall = false; - }; - - ExceptionAssert.Throws(test, expect); - - // Act & Assert - test = () => - { - ODataRouteOptions options = new ODataRouteOptions(); - options.EnableUnqualifiedOperationCall = false; - options.EnableQualifiedOperationCall = false; - }; - - ExceptionAssert.Throws(test, expect); - } + ODataRouteOptions options = new ODataRouteOptions(); + options.EnableUnqualifiedOperationCall = false; + options.EnableQualifiedOperationCall = false; + }; + + ExceptionAssert.Throws(test, expect); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRoutingApplicationModelProviderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRoutingApplicationModelProviderTests.cs index d9f17fe44..06a9c8c86 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRoutingApplicationModelProviderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRoutingApplicationModelProviderTests.cs @@ -22,217 +22,216 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing +namespace Microsoft.AspNetCore.OData.Tests.Routing; + +public class ODataRoutingApplicationModelProviderTests { - public class ODataRoutingApplicationModelProviderTests + private static IEdmModel _model = GetEdmModel(); + + [Fact] + public void OnProvidersExecuting_DoesNothing() { - private static IEdmModel _model = GetEdmModel(); + // Arrange + ODataOptions options = new ODataOptions(); + options.AddRouteComponents("odata", _model); - [Fact] - public void OnProvidersExecuting_DoesNothing() + IODataControllerActionConvention[] conventions = new IODataControllerActionConvention[] { - // Arrange - ODataOptions options = new ODataOptions(); - options.AddRouteComponents("odata", _model); + new EntitySetRoutingConvention() + }; + + ODataRoutingApplicationModelProvider provider = CreateProvider(options, conventions); - IODataControllerActionConvention[] conventions = new IODataControllerActionConvention[] + Type controllerType = typeof(CustomersController); + ApplicationModelProviderContext providerContext = CreateProviderContext(controllerType); + + // Act + provider.OnProvidersExecuting(providerContext); + + // Assert + var controller = Assert.Single(providerContext.Result.Controllers); + Assert.Collection(controller.Actions, + e => { - new EntitySetRoutingConvention() - }; - - ODataRoutingApplicationModelProvider provider = CreateProvider(options, conventions); - - Type controllerType = typeof(CustomersController); - ApplicationModelProviderContext providerContext = CreateProviderContext(controllerType); - - // Act - provider.OnProvidersExecuting(providerContext); - - // Assert - var controller = Assert.Single(providerContext.Result.Controllers); - Assert.Collection(controller.Actions, - e => - { - // Get() - Assert.Equal("Get", e.ActionMethod.Name); - Assert.Empty(e.Parameters); - Assert.Single(e.Selectors); - }, - e => - { - // Get(int key) - Assert.Equal("Get", e.ActionMethod.Name); - Assert.Single(e.Parameters); - Assert.Single(e.Selectors); - }); - } + // Get() + Assert.Equal("Get", e.ActionMethod.Name); + Assert.Empty(e.Parameters); + Assert.Single(e.Selectors); + }, + e => + { + // Get(int key) + Assert.Equal("Get", e.ActionMethod.Name); + Assert.Single(e.Parameters); + Assert.Single(e.Selectors); + }); + } + + [Fact] + public void OnProvidersExecuted_AddODataRoutingSelector_WhenEntitySetRoutingConvention() + { + // Arrange + ODataOptions options = new ODataOptions(); + options.AddRouteComponents("odata", _model); - [Fact] - public void OnProvidersExecuted_AddODataRoutingSelector_WhenEntitySetRoutingConvention() + IODataControllerActionConvention[] conventions = new IODataControllerActionConvention[] { - // Arrange - ODataOptions options = new ODataOptions(); - options.AddRouteComponents("odata", _model); + new EntitySetRoutingConvention() + }; + + ODataRoutingApplicationModelProvider provider = CreateProvider(options, conventions); + + Type controllerType = typeof(CustomersController); + ApplicationModelProviderContext providerContext = CreateProviderContext(controllerType); - IODataControllerActionConvention[] conventions = new IODataControllerActionConvention[] + // Act + provider.OnProvidersExecuted(providerContext); + + // Assert + var controller = Assert.Single(providerContext.Result.Controllers); + + Assert.Equal(2, controller.Actions.Count); + Assert.Collection(controller.Actions, + e => { - new EntitySetRoutingConvention() - }; - - ODataRoutingApplicationModelProvider provider = CreateProvider(options, conventions); - - Type controllerType = typeof(CustomersController); - ApplicationModelProviderContext providerContext = CreateProviderContext(controllerType); - - // Act - provider.OnProvidersExecuted(providerContext); - - // Assert - var controller = Assert.Single(providerContext.Result.Controllers); - - Assert.Equal(2, controller.Actions.Count); - Assert.Collection(controller.Actions, - e => - { - // Get() - Assert.Equal("Get", e.ActionMethod.Name); - Assert.Empty(e.Parameters); - Assert.Equal(2, e.Selectors.Count); - Assert.Equal(new[] { "/odata/Customers", "/odata/Customers/$count" }, e.Selectors.Select(s => s.AttributeRouteModel.Template)); - }, - e => - { - // Get(int key) - Assert.Equal("Get", e.ActionMethod.Name); - Assert.Single(e.Parameters); - Assert.Single(e.Selectors); - }); - } + // Get() + Assert.Equal("Get", e.ActionMethod.Name); + Assert.Empty(e.Parameters); + Assert.Equal(2, e.Selectors.Count); + Assert.Equal(new[] { "/odata/Customers", "/odata/Customers/$count" }, e.Selectors.Select(s => s.AttributeRouteModel.Template)); + }, + e => + { + // Get(int key) + Assert.Equal("Get", e.ActionMethod.Name); + Assert.Single(e.Parameters); + Assert.Single(e.Selectors); + }); + } + + [Fact] + public void OnProvidersExecuted_AddODataRoutingSelector_WhenEntityRoutingConvention() + { + // Arrange + ODataOptions options = new ODataOptions(); + options.AddRouteComponents("odata", _model); - [Fact] - public void OnProvidersExecuted_AddODataRoutingSelector_WhenEntityRoutingConvention() + IODataControllerActionConvention[] conventions = new IODataControllerActionConvention[] { - // Arrange - ODataOptions options = new ODataOptions(); - options.AddRouteComponents("odata", _model); + new EntityRoutingConvention() + }; + + ODataRoutingApplicationModelProvider provider = CreateProvider(options, conventions); + + Type controllerType = typeof(CustomersController); + ApplicationModelProviderContext providerContext = CreateProviderContext(controllerType); - IODataControllerActionConvention[] conventions = new IODataControllerActionConvention[] + // Act + provider.OnProvidersExecuted(providerContext); + + // Assert + ControllerModel controller = Assert.Single(providerContext.Result.Controllers); + + Assert.Equal(2, controller.Actions.Count); + Assert.Collection(controller.Actions, + e => { - new EntityRoutingConvention() - }; - - ODataRoutingApplicationModelProvider provider = CreateProvider(options, conventions); - - Type controllerType = typeof(CustomersController); - ApplicationModelProviderContext providerContext = CreateProviderContext(controllerType); - - // Act - provider.OnProvidersExecuted(providerContext); - - // Assert - ControllerModel controller = Assert.Single(providerContext.Result.Controllers); - - Assert.Equal(2, controller.Actions.Count); - Assert.Collection(controller.Actions, - e => - { - // Get() - Assert.Equal("Get", e.ActionMethod.Name); - Assert.Empty(e.Parameters); - var selector = Assert.Single(e.Selectors); - Assert.Null(selector.AttributeRouteModel); - }, - e => - { - // Get(int key) - Assert.Equal("Get", e.ActionMethod.Name); - Assert.Single(e.Parameters); - Assert.Equal(2, e.Selectors.Count); - Assert.Equal(new[] { "/odata/Customers({key})", "/odata/Customers/{key}" }, e.Selectors.Select(s => s.AttributeRouteModel.Template)); - }); - } + // Get() + Assert.Equal("Get", e.ActionMethod.Name); + Assert.Empty(e.Parameters); + var selector = Assert.Single(e.Selectors); + Assert.Null(selector.AttributeRouteModel); + }, + e => + { + // Get(int key) + Assert.Equal("Get", e.ActionMethod.Name); + Assert.Single(e.Parameters); + Assert.Equal(2, e.Selectors.Count); + Assert.Equal(new[] { "/odata/Customers({key})", "/odata/Customers/{key}" }, e.Selectors.Select(s => s.AttributeRouteModel.Template)); + }); + } - [Fact] - public void OnProvidersExecuted_AddODataRoutingSelector_WhenAttributeRoutingConvention() + [Fact] + public void OnProvidersExecuted_AddODataRoutingSelector_WhenAttributeRoutingConvention() + { + // Arrange + ODataOptions options = new ODataOptions(); + options.AddRouteComponents("odata", _model); + + LoggerFactory loggerFactory = new LoggerFactory(); + var logger = new Logger(loggerFactory); + IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); + IODataControllerActionConvention[] conventions = new IODataControllerActionConvention[] { - // Arrange - ODataOptions options = new ODataOptions(); - options.AddRouteComponents("odata", _model); - - LoggerFactory loggerFactory = new LoggerFactory(); - var logger = new Logger(loggerFactory); - IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); - IODataControllerActionConvention[] conventions = new IODataControllerActionConvention[] - { - new AttributeRoutingConvention(logger, parser) - }; + new AttributeRoutingConvention(logger, parser) + }; - ODataRoutingApplicationModelProvider provider = CreateProvider(options, conventions); - ApplicationModelProviderContext providerContext = CreateProviderContext(typeof(AttributeRoutingController)); + ODataRoutingApplicationModelProvider provider = CreateProvider(options, conventions); + ApplicationModelProviderContext providerContext = CreateProviderContext(typeof(AttributeRoutingController)); - // Act - provider.OnProvidersExecuted(providerContext); + // Act + provider.OnProvidersExecuted(providerContext); - // Assert - ControllerModel controller = Assert.Single(providerContext.Result.Controllers); + // Assert + ControllerModel controller = Assert.Single(providerContext.Result.Controllers); - ActionModel action = Assert.Single(controller.Actions); - Assert.Equal("AnyMethodNameHere", action.ActionMethod.Name); - Assert.Single(action.Parameters); - SelectorModel selectorModel = Assert.Single(action.Selectors); - Assert.Equal("/odata/Customers({key})/Name", selectorModel.AttributeRouteModel.Template); - Assert.Contains(selectorModel.EndpointMetadata, a => a is ODataRoutingMetadata); - } + ActionModel action = Assert.Single(controller.Actions); + Assert.Equal("AnyMethodNameHere", action.ActionMethod.Name); + Assert.Single(action.Parameters); + SelectorModel selectorModel = Assert.Single(action.Selectors); + Assert.Equal("/odata/Customers({key})/Name", selectorModel.AttributeRouteModel.Template); + Assert.Contains(selectorModel.EndpointMetadata, a => a is ODataRoutingMetadata); + } - private static ODataRoutingApplicationModelProvider CreateProvider(ODataOptions options, - IEnumerable conventions) + private static ODataRoutingApplicationModelProvider CreateProvider(ODataOptions options, + IEnumerable conventions) + { + foreach (var c in conventions) { - foreach (var c in conventions) - { - options.Conventions.Add(c); - } - IOptions odataOptions = Options.Create(options); - return new ODataRoutingApplicationModelProvider(/*conventions, */odataOptions); + options.Conventions.Add(c); } + IOptions odataOptions = Options.Create(options); + return new ODataRoutingApplicationModelProvider(/*conventions, */odataOptions); + } - private static ApplicationModelProviderContext CreateProviderContext(Type controllerType) - { - var context = new ApplicationModelProviderContext(new[] { controllerType.GetTypeInfo() }); - ControllerModel controllerModel = ControllerModelHelpers.BuildControllerModelWithAllActions(controllerType); - context.Result.Controllers.Add(controllerModel); - return context; - } + private static ApplicationModelProviderContext CreateProviderContext(Type controllerType) + { + var context = new ApplicationModelProviderContext(new[] { controllerType.GetTypeInfo() }); + ControllerModel controllerModel = ControllerModelHelpers.BuildControllerModelWithAllActions(controllerType); + context.Result.Controllers.Add(controllerModel); + return context; + } + + private static IEdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + model.AddElement(customer); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + container.AddEntitySet("Customers", customer); + model.AddElement(container); + return model; + } - private static IEdmModel GetEdmModel() + public class CustomersController + { + public void Get() { - EdmModel model = new EdmModel(); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - model.AddElement(customer); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - container.AddEntitySet("Customers", customer); - model.AddElement(container); - return model; } - public class CustomersController + public void Get(int key) { - public void Get() - { - } - - public void Get(int key) - { - } } + } - public class AttributeRoutingController : ODataController + public class AttributeRoutingController : ODataController + { + [HttpGet("odata/Customers({key})/Name")] + public void AnyMethodNameHere(int key) { - [HttpGet("odata/Customers({key})/Name")] - public void AnyMethodNameHere(int key) - { - } } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRoutingMatcherPolicyTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRoutingMatcherPolicyTests.cs index 2bc7811f7..80cec17c0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRoutingMatcherPolicyTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRoutingMatcherPolicyTests.cs @@ -18,125 +18,124 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing +namespace Microsoft.AspNetCore.OData.Tests.Routing; + +public class ODataRoutingMatcherPolicyTests { - public class ODataRoutingMatcherPolicyTests + [Fact] + public void AppliesToEndpoints_EndpointWithoutODataRoutingMetadata_ReturnsFalse() { - [Fact] - public void AppliesToEndpoints_EndpointWithoutODataRoutingMetadata_ReturnsFalse() - { - // Arrange - Endpoint[] endpoints = new[] { CreateEndpoint("/", null), }; + // Arrange + Endpoint[] endpoints = new[] { CreateEndpoint("/", null), }; - ODataRoutingMatcherPolicy policy = CreatePolicy(); + ODataRoutingMatcherPolicy policy = CreatePolicy(); - // Act - bool result = policy.AppliesToEndpoints(endpoints); + // Act + bool result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void AppliesToEndpoints_EndpointHasODataRoutingMetadata_ReturnsTrue() + [Fact] + public void AppliesToEndpoints_EndpointHasODataRoutingMetadata_ReturnsTrue() + { + // Arrange + IODataRoutingMetadata routingMetadata = new ODataRoutingMetadata(); + Endpoint[] endpoints = new[] { - // Arrange - IODataRoutingMetadata routingMetadata = new ODataRoutingMetadata(); - Endpoint[] endpoints = new[] - { - CreateEndpoint("/", routingMetadata), - CreateEndpoint("/", null), - }; + CreateEndpoint("/", routingMetadata), + CreateEndpoint("/", null), + }; - ODataRoutingMatcherPolicy policy = CreatePolicy(); + ODataRoutingMatcherPolicy policy = CreatePolicy(); - // Act - bool result = policy.AppliesToEndpoints(endpoints); + // Act + bool result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } + + [Fact] + public async Task ApplyAsync_PrefersEndpointsWithODataRoutingMetadata() + { + // Arrange + IEdmModel model = EdmCoreModel.Instance; + IODataRoutingMetadata routingMetadata = new ODataRoutingMetadata("odata", model, new ODataPathTemplate()); - [Fact] - public async Task ApplyAsync_PrefersEndpointsWithODataRoutingMetadata() + Endpoint[] endpoints = new[] { - // Arrange - IEdmModel model = EdmCoreModel.Instance; - IODataRoutingMetadata routingMetadata = new ODataRoutingMetadata("odata", model, new ODataPathTemplate()); + CreateEndpoint("/", routingMetadata, new HttpMethodMetadata(new[] { "get" })), + CreateEndpoint("/", routingMetadata, new HttpMethodMetadata(new[] { "post" })), + CreateEndpoint("/", routingMetadata, new HttpMethodMetadata(new[] { "delete" })) + }; - Endpoint[] endpoints = new[] - { - CreateEndpoint("/", routingMetadata, new HttpMethodMetadata(new[] { "get" })), - CreateEndpoint("/", routingMetadata, new HttpMethodMetadata(new[] { "post" })), - CreateEndpoint("/", routingMetadata, new HttpMethodMetadata(new[] { "delete" })) - }; + CandidateSet candidateSet = CreateCandidateSet(endpoints); - CandidateSet candidateSet = CreateCandidateSet(endpoints); + HttpContext httpContext = CreateHttpContext("POST"); - HttpContext httpContext = CreateHttpContext("POST"); + HttpMethodMatcherPolicy httpMethodPolicy = new HttpMethodMatcherPolicy(); - HttpMethodMatcherPolicy httpMethodPolicy = new HttpMethodMatcherPolicy(); + ODataRoutingMatcherPolicy policy = CreatePolicy(); - ODataRoutingMatcherPolicy policy = CreatePolicy(); + // Act + await httpMethodPolicy.ApplyAsync(httpContext, candidateSet); + await policy.ApplyAsync(httpContext, candidateSet); - // Act - await httpMethodPolicy.ApplyAsync(httpContext, candidateSet); - await policy.ApplyAsync(httpContext, candidateSet); + // Assert + Assert.False(candidateSet.IsValidCandidate(0)); + Assert.True(candidateSet.IsValidCandidate(1)); + Assert.False(candidateSet.IsValidCandidate(2)); + } - // Assert - Assert.False(candidateSet.IsValidCandidate(0)); - Assert.True(candidateSet.IsValidCandidate(1)); - Assert.False(candidateSet.IsValidCandidate(2)); + private static RouteEndpoint CreateEndpoint(string template, IODataRoutingMetadata odataMetadata, params object[] more) + { + var metadata = new List(); + if (odataMetadata != null) + { + metadata.Add(odataMetadata); } - private static RouteEndpoint CreateEndpoint(string template, IODataRoutingMetadata odataMetadata, params object[] more) + if (more != null) { - var metadata = new List(); - if (odataMetadata != null) - { - metadata.Add(odataMetadata); - } - - if (more != null) - { - metadata.AddRange(more); - } - - return new RouteEndpoint( - (context) => Task.CompletedTask, - RoutePatternFactory.Parse(template), - 0, - new EndpointMetadataCollection(metadata), - $"test: {template} - { odataMetadata?.Prefix ?? ""}"); + metadata.AddRange(more); } - private static CandidateSet CreateCandidateSet(Endpoint[] endpoints) + return new RouteEndpoint( + (context) => Task.CompletedTask, + RoutePatternFactory.Parse(template), + 0, + new EndpointMetadataCollection(metadata), + $"test: {template} - { odataMetadata?.Prefix ?? ""}"); + } + + private static CandidateSet CreateCandidateSet(Endpoint[] endpoints) + { + var values = new RouteValueDictionary[endpoints.Length]; + for (var i = 0; i < endpoints.Length; i++) { - var values = new RouteValueDictionary[endpoints.Length]; - for (var i = 0; i < endpoints.Length; i++) - { - values[i] = new RouteValueDictionary(); - } - - CandidateSet candidateSet = new CandidateSet(endpoints, values, new int[endpoints.Length]); - return candidateSet; + values[i] = new RouteValueDictionary(); } - private static ODataRoutingMatcherPolicy CreatePolicy() - { - var translator = new Mock(); - translator - .Setup(a => a.Translate(It.IsAny(), It.IsAny())) - .Returns(new ODataPath()); + CandidateSet candidateSet = new CandidateSet(endpoints, values, new int[endpoints.Length]); + return candidateSet; + } - return new ODataRoutingMatcherPolicy(translator.Object); - } + private static ODataRoutingMatcherPolicy CreatePolicy() + { + var translator = new Mock(); + translator + .Setup(a => a.Translate(It.IsAny(), It.IsAny())) + .Returns(new ODataPath()); - private static HttpContext CreateHttpContext(string httpMethod) - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = httpMethod; - return httpContext; - } + return new ODataRoutingMatcherPolicy(translator.Object); + } + + private static HttpContext CreateHttpContext(string httpMethod) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = httpMethod; + return httpContext; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRoutingMetadataTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRoutingMetadataTests.cs index 13457c9e8..cbcab642f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRoutingMetadataTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/ODataRoutingMetadataTests.cs @@ -11,45 +11,44 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing +namespace Microsoft.AspNetCore.OData.Tests.Routing; + +public class ODataRoutingMetadataTests { - public class ODataRoutingMetadataTests + [Fact] + public void Ctor_ThrowsArgumentNull_Prefix() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataRoutingMetadata(prefix: null, null, null), "prefix"); + } + + [Fact] + public void Ctor_ThrowsArgumentNull_Model() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataRoutingMetadata("prefix", null, null), "model"); + } + + [Fact] + public void Ctor_ThrowsArgumentNull_template() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ODataRoutingMetadata("prefix", EdmCoreModel.Instance, null), "template"); + } + + [Fact] + public void Ctor_SetPropertiesCorrectly() { - [Fact] - public void Ctor_ThrowsArgumentNull_Prefix() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataRoutingMetadata(prefix: null, null, null), "prefix"); - } - - [Fact] - public void Ctor_ThrowsArgumentNull_Model() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataRoutingMetadata("prefix", null, null), "model"); - } - - [Fact] - public void Ctor_ThrowsArgumentNull_template() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ODataRoutingMetadata("prefix", EdmCoreModel.Instance, null), "template"); - } - - [Fact] - public void Ctor_SetPropertiesCorrectly() - { - // Arrange - IEdmModel model = EdmCoreModel.Instance; - ODataPathTemplate path = new ODataPathTemplate(); - - // Act - ODataRoutingMetadata metadata = new ODataRoutingMetadata("prefix", model, path); - - // Assert - Assert.Equal("prefix", metadata.Prefix); - Assert.Same(model, metadata.Model); - Assert.Same(path, metadata.Template); - } + // Arrange + IEdmModel model = EdmCoreModel.Instance; + ODataPathTemplate path = new ODataPathTemplate(); + + // Act + ODataRoutingMetadata metadata = new ODataRoutingMetadata("prefix", model, path); + + // Assert + Assert.Equal("prefix", metadata.Prefix); + Assert.Same(model, metadata.Model); + Assert.Same(path, metadata.Template); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/DefaultODataPathParserTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/DefaultODataPathParserTests.cs index 43da8d02c..11eaa8e2c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/DefaultODataPathParserTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/DefaultODataPathParserTests.cs @@ -11,41 +11,40 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Parser +namespace Microsoft.AspNetCore.OData.Tests.Routing.Parser; + +public class DefaultODataPathParserTests { - public class DefaultODataPathParserTests + [Theory] + [InlineData(null)] + [InlineData("http://any")] + public void CanParseEntitySetPathWithOrWithoutServiceRoot(string serviceRoot) { - [Theory] - [InlineData(null)] - [InlineData("http://any")] - public void CanParseEntitySetPathWithOrWithoutServiceRoot(string serviceRoot) - { - // Arrange - IEdmModel model = GetEdmModel(); - IODataPathParser parser = new DefaultODataPathParser(); - Uri serviceRootUri = serviceRoot == null ? null : new Uri(serviceRoot); + // Arrange + IEdmModel model = GetEdmModel(); + IODataPathParser parser = new DefaultODataPathParser(); + Uri serviceRootUri = serviceRoot == null ? null : new Uri(serviceRoot); - // Act - ODataPath path = parser.Parse(model, serviceRootUri, new Uri("Customers", UriKind.RelativeOrAbsolute), null); + // Act + ODataPath path = parser.Parse(model, serviceRootUri, new Uri("Customers", UriKind.RelativeOrAbsolute), null); - // Assert - Assert.NotNull(path); - ODataPathSegment segment = Assert.Single(path); - EntitySetSegment setSegment = Assert.IsType(segment); - Assert.Equal("Customers", setSegment.EntitySet.Name); - } + // Assert + Assert.NotNull(path); + ODataPathSegment segment = Assert.Single(path); + EntitySetSegment setSegment = Assert.IsType(segment); + Assert.Equal("Customers", setSegment.EntitySet.Name); + } - private static EdmModel GetEdmModel() - { - EdmModel model = new EdmModel(); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + private static EdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - entityContainer.AddEntitySet("Customers", customer); - model.AddElement(entityContainer); - return model; - } + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + entityContainer.AddEntitySet("Customers", customer); + model.AddElement(entityContainer); + return model; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/DefaultODataPathTemplateParserTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/DefaultODataPathTemplateParserTests.cs index 7febb9c93..4181d5da9 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/DefaultODataPathTemplateParserTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/DefaultODataPathTemplateParserTests.cs @@ -12,327 +12,326 @@ using Microsoft.OData.Edm; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Parser +namespace Microsoft.AspNetCore.OData.Tests.Routing.Parser; + +public class DefaultODataPathTemplateParserTests { - public class DefaultODataPathTemplateParserTests + private static IEdmTypeReference ReturnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); + private static IEdmTypeReference StringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); + private static IEdmTypeReference IntType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); + + private EdmModel _edmModel; + private EdmEntityType _customerType; + public DefaultODataPathTemplateParserTests() { - private static IEdmTypeReference ReturnType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, isNullable: false); - private static IEdmTypeReference StringType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, isNullable: false); - private static IEdmTypeReference IntType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); + _edmModel = GetEdmModel(); + _customerType = _edmModel.SchemaElements.OfType().First(c => c.Name == "Customer"); + } - private EdmModel _edmModel; - private EdmEntityType _customerType; - public DefaultODataPathTemplateParserTests() - { - _edmModel = GetEdmModel(); - _customerType = _edmModel.SchemaElements.OfType().First(c => c.Name == "Customer"); - } + [Fact] + public void ParseODataUriTemplate_ForEntitySet() + { + // Arrange + IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); - [Fact] - public void ParseODataUriTemplate_ForEntitySet() - { - // Arrange - IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); + // Act + ODataPathTemplate path = parser.Parse(_edmModel, "Customers", null); - // Act - ODataPathTemplate path = parser.Parse(_edmModel, "Customers", null); + // Assert + Assert.NotNull(path); + ODataSegmentTemplate pathSegment = Assert.Single(path); + EntitySetSegmentTemplate setSegment = Assert.IsType(pathSegment); + Assert.Equal("Customers", setSegment.EntitySet.Name); + } - // Assert - Assert.NotNull(path); - ODataSegmentTemplate pathSegment = Assert.Single(path); - EntitySetSegmentTemplate setSegment = Assert.IsType(pathSegment); - Assert.Equal("Customers", setSegment.EntitySet.Name); - } + [Fact] + public void ParseODataUriTemplate_ForSingleton() + { + // Arrange + IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); - [Fact] - public void ParseODataUriTemplate_ForSingleton() - { - // Arrange - IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); + // Act + ODataPathTemplate path = parser.Parse(_edmModel, "VipCustomer", null); - // Act - ODataPathTemplate path = parser.Parse(_edmModel, "VipCustomer", null); + // Assert + Assert.NotNull(path); + ODataSegmentTemplate pathSegment = Assert.Single(path); + SingletonSegmentTemplate singletonSegment = Assert.IsType(pathSegment); + Assert.Equal("VipCustomer", singletonSegment.Singleton.Name); + } - // Assert - Assert.NotNull(path); - ODataSegmentTemplate pathSegment = Assert.Single(path); - SingletonSegmentTemplate singletonSegment = Assert.IsType(pathSegment); - Assert.Equal("VipCustomer", singletonSegment.Singleton.Name); - } + [Theory] + [InlineData("Customers({idKey})")] + [InlineData("Customers/{idKey}")] + public void ParseODataUriTemplate_ForBasicKey(string template) + { + // Arrange + IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); - [Theory] - [InlineData("Customers({idKey})")] - [InlineData("Customers/{idKey}")] - public void ParseODataUriTemplate_ForBasicKey(string template) - { - // Arrange - IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); + // Act + ODataPathTemplate path = parser.Parse(_edmModel, template, null); - // Act - ODataPathTemplate path = parser.Parse(_edmModel, template, null); + // Assert + Assert.NotNull(path); + Assert.Equal(2, path.Count); + KeySegmentTemplate keySegment = Assert.IsType(path[1]); + var keyMap = Assert.Single(keySegment.KeyMappings); - // Assert - Assert.NotNull(path); - Assert.Equal(2, path.Count); - KeySegmentTemplate keySegment = Assert.IsType(path[1]); - var keyMap = Assert.Single(keySegment.KeyMappings); + Assert.Equal("Id", keyMap.Key); + Assert.Equal("idKey", keyMap.Value); + } - Assert.Equal("Id", keyMap.Key); - Assert.Equal("idKey", keyMap.Value); - } + [Theory] + [InlineData("People(firstName={first},lastName={last})")] + [InlineData("People(lastName={last},firstName={first})")] + public void ParseODataUriTemplate_ForCompositeKeys(string template) + { + // Arrange + // function with optional parameters + EdmEntityType person = new EdmEntityType("NS", "Person"); + EdmStructuralProperty firstName = person.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); + EdmStructuralProperty lastName = person.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); + person.AddKeys(firstName, lastName); + _edmModel.AddElement(person); + + EdmEntityContainer container = _edmModel.SchemaElements.OfType().First(); + container.AddEntitySet("People", person); + + IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); + + // Act + ODataPathTemplate path = parser.Parse(_edmModel, template, null); + + // Assert + Assert.NotNull(path); + Assert.Equal(2, path.Count); + KeySegmentTemplate keySegment = Assert.IsType(path[1]); + Assert.Equal(2, keySegment.KeyMappings.Count); + + Assert.Equal(new[] { "firstName", "lastName" }, keySegment.KeyMappings.Keys); + Assert.Equal(new[] { "first", "last" }, keySegment.KeyMappings.Values); + } - [Theory] - [InlineData("People(firstName={first},lastName={last})")] - [InlineData("People(lastName={last},firstName={first})")] - public void ParseODataUriTemplate_ForCompositeKeys(string template) - { - // Arrange - // function with optional parameters - EdmEntityType person = new EdmEntityType("NS", "Person"); - EdmStructuralProperty firstName = person.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); - EdmStructuralProperty lastName = person.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); - person.AddKeys(firstName, lastName); - _edmModel.AddElement(person); - - EdmEntityContainer container = _edmModel.SchemaElements.OfType().First(); - container.AddEntitySet("People", person); - - IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); - - // Act - ODataPathTemplate path = parser.Parse(_edmModel, template, null); - - // Assert - Assert.NotNull(path); - Assert.Equal(2, path.Count); - KeySegmentTemplate keySegment = Assert.IsType(path[1]); - Assert.Equal(2, keySegment.KeyMappings.Count); - - Assert.Equal(new[] { "firstName", "lastName" }, keySegment.KeyMappings.Keys); - Assert.Equal(new[] { "first", "last" }, keySegment.KeyMappings.Values); - } + [Theory] + [InlineData("Customers({idKey})/Name", 3)] + [InlineData("Customers/{idKey}/Name", 3)] + [InlineData("VipCustomer/Name", 2)] + public void ParseODataUriTemplate_ForProperty(string template, int count) + { + // Arrange + IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); - [Theory] - [InlineData("Customers({idKey})/Name", 3)] - [InlineData("Customers/{idKey}/Name", 3)] - [InlineData("VipCustomer/Name", 2)] - public void ParseODataUriTemplate_ForProperty(string template, int count) - { - // Arrange - IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); + // Act + ODataPathTemplate path = parser.Parse(_edmModel, template, null); - // Act - ODataPathTemplate path = parser.Parse(_edmModel, template, null); + // Assert + Assert.NotNull(path); + Assert.Equal(count, path.Count); + PropertySegmentTemplate propertySegment = Assert.IsType(path[count - 1]); + Assert.Equal("Name", propertySegment.Property.Name); + } - // Assert - Assert.NotNull(path); - Assert.Equal(count, path.Count); - PropertySegmentTemplate propertySegment = Assert.IsType(path[count - 1]); - Assert.Equal("Name", propertySegment.Property.Name); - } + [Theory] + [InlineData("Customers({idKey})/Orders", 3)] + [InlineData("Customers/{idKey}/Orders", 3)] + [InlineData("VipCustomer/Orders", 2)] + public void ParseODataUriTemplate_ForNavigationProperty(string template, int count) + { + // Arrange + IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); - [Theory] - [InlineData("Customers({idKey})/Orders", 3)] - [InlineData("Customers/{idKey}/Orders", 3)] - [InlineData("VipCustomer/Orders", 2)] - public void ParseODataUriTemplate_ForNavigationProperty(string template, int count) - { - // Arrange - IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); + // Act + ODataPathTemplate path = parser.Parse(_edmModel, template, null); - // Act - ODataPathTemplate path = parser.Parse(_edmModel, template, null); + // Assert + Assert.NotNull(path); + Assert.Equal(count, path.Count); + NavigationSegmentTemplate navigationSegment = Assert.IsType(path[count - 1]); + Assert.Equal("Orders", navigationSegment.NavigationProperty.Name); + } - // Assert - Assert.NotNull(path); - Assert.Equal(count, path.Count); - NavigationSegmentTemplate navigationSegment = Assert.IsType(path[count - 1]); - Assert.Equal("Orders", navigationSegment.NavigationProperty.Name); - } + [Theory] + [InlineData("Customers({idKey})/Orders/$ref", 3)] + [InlineData("Customers/{idKey}/Orders/$ref", 3)] + [InlineData("VipCustomer/Orders/$ref", 2)] + public void ParseODataUriTemplate_ForNavigationPropertyLink(string template, int count) + { + // Arrange + IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); - [Theory] - [InlineData("Customers({idKey})/Orders/$ref", 3)] - [InlineData("Customers/{idKey}/Orders/$ref", 3)] - [InlineData("VipCustomer/Orders/$ref", 2)] - public void ParseODataUriTemplate_ForNavigationPropertyLink(string template, int count) - { - // Arrange - IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); + // Act + ODataPathTemplate path = parser.Parse(_edmModel, template, null); - // Act - ODataPathTemplate path = parser.Parse(_edmModel, template, null); + // Assert + Assert.NotNull(path); + Assert.Equal(count, path.Count); + NavigationLinkSegmentTemplate navigationSegment = Assert.IsType(path[count - 1]); + Assert.Equal("Orders", navigationSegment.NavigationProperty.Name); + } - // Assert - Assert.NotNull(path); - Assert.Equal(count, path.Count); - NavigationLinkSegmentTemplate navigationSegment = Assert.IsType(path[count - 1]); - Assert.Equal("Orders", navigationSegment.NavigationProperty.Name); + [Theory] + [InlineData("Customers/NS.GetWholeSalary(minSalary={min},maxSalary={max})", 2)] + [InlineData("Customers/NS.GetWholeSalary(minSalary={min},maxSalary={max},aveSalary={ave})", 3)] + [InlineData("Customers/GetWholeSalary(minSalary={min},maxSalary={max})", 2)] + [InlineData("Customers/GetWholeSalary(minSalary={min},maxSalary={max},aveSalary={ave})", 3)] + public void ParseODataUriTemplate_ForFunctions(string template, int count) + { + // Arrange + // function with optional parameters + EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", IntType, isBound: true, entitySetPathExpression: null, isComposable: false); + getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(_customerType, false)))); + getSalaray.AddParameter("minSalary", IntType); + getSalaray.AddOptionalParameter("maxSalary", IntType); + getSalaray.AddOptionalParameter("aveSalary", IntType, "129"); + _edmModel.AddElement(getSalaray); + + IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); + MockServiceProvider sp = null; + if (!template.Contains("NS.")) + { + sp = new MockServiceProvider(_edmModel); } - [Theory] - [InlineData("Customers/NS.GetWholeSalary(minSalary={min},maxSalary={max})", 2)] - [InlineData("Customers/NS.GetWholeSalary(minSalary={min},maxSalary={max},aveSalary={ave})", 3)] - [InlineData("Customers/GetWholeSalary(minSalary={min},maxSalary={max})", 2)] - [InlineData("Customers/GetWholeSalary(minSalary={min},maxSalary={max},aveSalary={ave})", 3)] - public void ParseODataUriTemplate_ForFunctions(string template, int count) + // Act + ODataPathTemplate path = parser.Parse(_edmModel, template, sp); + + // Assert + Assert.NotNull(path); + Assert.Equal(2, path.Count); + FunctionSegmentTemplate functionSegment = Assert.IsType(path[1]); + Assert.Equal(count, functionSegment.ParameterMappings.Count); + + if (count == 2) { - // Arrange - // function with optional parameters - EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", IntType, isBound: true, entitySetPathExpression: null, isComposable: false); - getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(_customerType, false)))); - getSalaray.AddParameter("minSalary", IntType); - getSalaray.AddOptionalParameter("maxSalary", IntType); - getSalaray.AddOptionalParameter("aveSalary", IntType, "129"); - _edmModel.AddElement(getSalaray); - - IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); - MockServiceProvider sp = null; - if (!template.Contains("NS.")) - { - sp = new MockServiceProvider(_edmModel); - } - - // Act - ODataPathTemplate path = parser.Parse(_edmModel, template, sp); - - // Assert - Assert.NotNull(path); - Assert.Equal(2, path.Count); - FunctionSegmentTemplate functionSegment = Assert.IsType(path[1]); - Assert.Equal(count, functionSegment.ParameterMappings.Count); - - if (count == 2) - { - Assert.Equal(new[] { "minSalary", "maxSalary" }, functionSegment.ParameterMappings.Keys); - Assert.Equal(new[] { "min", "max" }, functionSegment.ParameterMappings.Values); - } - else - { - Assert.Equal(new[] { "minSalary", "maxSalary", "aveSalary" }, functionSegment.ParameterMappings.Keys); - Assert.Equal(new[] { "min", "max", "ave" }, functionSegment.ParameterMappings.Values); - } + Assert.Equal(new[] { "minSalary", "maxSalary" }, functionSegment.ParameterMappings.Keys); + Assert.Equal(new[] { "min", "max" }, functionSegment.ParameterMappings.Values); } - - [Theory] - [InlineData("Customers/NS.SetWholeSalary")] - [InlineData("Customers/SetWholeSalary")] - public void ParseODataUriTemplate_ForActions(string template) + else { - // Arrange - // function with optional parameters - EdmAction setSalaray = new EdmAction("NS", "SetWholeSalary", IntType, isBound: true, entitySetPathExpression: null); - setSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(_customerType, false)))); - setSalaray.AddParameter("minSalary", IntType); - setSalaray.AddOptionalParameter("maxSalary", IntType); - setSalaray.AddOptionalParameter("aveSalary", IntType, "129"); - _edmModel.AddElement(setSalaray); - - IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); - MockServiceProvider sp = null; - if (!template.Contains("NS.")) - { - sp = new MockServiceProvider(_edmModel); - } - - // Act - ODataPathTemplate path = parser.Parse(_edmModel, template, sp); - - // Assert - Assert.NotNull(path); - Assert.Equal(2, path.Count); - ActionSegmentTemplate actionSegment = Assert.IsType(path[1]); - Assert.Equal("NS.SetWholeSalary", actionSegment.Action.FullName()); + Assert.Equal(new[] { "minSalary", "maxSalary", "aveSalary" }, functionSegment.ParameterMappings.Keys); + Assert.Equal(new[] { "min", "max", "ave" }, functionSegment.ParameterMappings.Values); } + } - [Theory] - [InlineData("GetWholeSalaryImport(minSalary={min},maxSalary={max})", 2)] - [InlineData("GetWholeSalaryImport(minSalary={min},maxSalary={max},aveSalary={ave})", 3)] - public void ParseODataUriTemplate_ForFunctionImport(string template, int parameterCount) + [Theory] + [InlineData("Customers/NS.SetWholeSalary")] + [InlineData("Customers/SetWholeSalary")] + public void ParseODataUriTemplate_ForActions(string template) + { + // Arrange + // function with optional parameters + EdmAction setSalaray = new EdmAction("NS", "SetWholeSalary", IntType, isBound: true, entitySetPathExpression: null); + setSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(_customerType, false)))); + setSalaray.AddParameter("minSalary", IntType); + setSalaray.AddOptionalParameter("maxSalary", IntType); + setSalaray.AddOptionalParameter("aveSalary", IntType, "129"); + _edmModel.AddElement(setSalaray); + + IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); + MockServiceProvider sp = null; + if (!template.Contains("NS.")) { - // Arrange - EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalaryImport", IntType, isBound: false, entitySetPathExpression: null, isComposable: false); - getSalaray.AddParameter("minSalary", IntType); - getSalaray.AddOptionalParameter("maxSalary", IntType); - getSalaray.AddOptionalParameter("aveSalary", IntType, "129"); - _edmModel.AddElement(getSalaray); - - EdmEntityContainer container = _edmModel.SchemaElements.OfType().First(); - container.AddFunctionImport(getSalaray); - - IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); - - // Act - ODataPathTemplate path = parser.Parse(_edmModel, template, null); - - // Assert - Assert.NotNull(path); - ODataSegmentTemplate segmentTemplate = Assert.Single(path); - FunctionImportSegmentTemplate functionImportSegment = Assert.IsType(segmentTemplate); - - if (parameterCount == 2) - { - Assert.Equal(new[] { "minSalary", "maxSalary" }, functionImportSegment.ParameterMappings.Keys); - Assert.Equal(new[] { "min", "max" }, functionImportSegment.ParameterMappings.Values); - } - else - { - Assert.Equal(new[] { "minSalary", "maxSalary", "aveSalary" }, functionImportSegment.ParameterMappings.Keys); - Assert.Equal(new[] { "min", "max", "ave" }, functionImportSegment.ParameterMappings.Values); - } + sp = new MockServiceProvider(_edmModel); } - [Fact] - public void ParseODataUriTemplate_ForActionImport() + // Act + ODataPathTemplate path = parser.Parse(_edmModel, template, sp); + + // Assert + Assert.NotNull(path); + Assert.Equal(2, path.Count); + ActionSegmentTemplate actionSegment = Assert.IsType(path[1]); + Assert.Equal("NS.SetWholeSalary", actionSegment.Action.FullName()); + } + + [Theory] + [InlineData("GetWholeSalaryImport(minSalary={min},maxSalary={max})", 2)] + [InlineData("GetWholeSalaryImport(minSalary={min},maxSalary={max},aveSalary={ave})", 3)] + public void ParseODataUriTemplate_ForFunctionImport(string template, int parameterCount) + { + // Arrange + EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalaryImport", IntType, isBound: false, entitySetPathExpression: null, isComposable: false); + getSalaray.AddParameter("minSalary", IntType); + getSalaray.AddOptionalParameter("maxSalary", IntType); + getSalaray.AddOptionalParameter("aveSalary", IntType, "129"); + _edmModel.AddElement(getSalaray); + + EdmEntityContainer container = _edmModel.SchemaElements.OfType().First(); + container.AddFunctionImport(getSalaray); + + IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); + + // Act + ODataPathTemplate path = parser.Parse(_edmModel, template, null); + + // Assert + Assert.NotNull(path); + ODataSegmentTemplate segmentTemplate = Assert.Single(path); + FunctionImportSegmentTemplate functionImportSegment = Assert.IsType(segmentTemplate); + + if (parameterCount == 2) { - // Arrange - EdmAction setSalaray = new EdmAction("NS", "SetWholeSalaryImport", IntType, isBound: false, entitySetPathExpression: null); - setSalaray.AddParameter("minSalary", IntType); - setSalaray.AddOptionalParameter("maxSalary", IntType); - setSalaray.AddOptionalParameter("aveSalary", IntType, "129"); - _edmModel.AddElement(setSalaray); - - EdmEntityContainer container = _edmModel.SchemaElements.OfType().First(); - container.AddActionImport(setSalaray); - - IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); - - // Act - ODataPathTemplate path = parser.Parse(_edmModel, "SetWholeSalaryImport", null); - - // Assert - Assert.NotNull(path); - ODataSegmentTemplate segmentTemplate = Assert.Single(path); - ActionImportSegmentTemplate actionImportSegment = Assert.IsType(segmentTemplate); - Assert.Equal("SetWholeSalaryImport", actionImportSegment.ActionImport.Name); + Assert.Equal(new[] { "minSalary", "maxSalary" }, functionImportSegment.ParameterMappings.Keys); + Assert.Equal(new[] { "min", "max" }, functionImportSegment.ParameterMappings.Values); } - - private static EdmModel GetEdmModel() + else { - EdmModel model = new EdmModel(); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - - EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); - model.AddElement(customer); - model.AddElement(vipCustomer); - - EdmEntityType order = new EdmEntityType("NS", "Order"); - order.AddKeys(order.AddStructuralProperty("OrderId", EdmPrimitiveTypeKind.Int32)); - model.AddElement(order); - - customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "Orders", - Target = order, - TargetMultiplicity = EdmMultiplicity.Many - }); - - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - entityContainer.AddEntitySet("Customers", customer); - entityContainer.AddSingleton("VipCustomer", customer); - model.AddElement(entityContainer); - return model; + Assert.Equal(new[] { "minSalary", "maxSalary", "aveSalary" }, functionImportSegment.ParameterMappings.Keys); + Assert.Equal(new[] { "min", "max", "ave" }, functionImportSegment.ParameterMappings.Values); } } + + [Fact] + public void ParseODataUriTemplate_ForActionImport() + { + // Arrange + EdmAction setSalaray = new EdmAction("NS", "SetWholeSalaryImport", IntType, isBound: false, entitySetPathExpression: null); + setSalaray.AddParameter("minSalary", IntType); + setSalaray.AddOptionalParameter("maxSalary", IntType); + setSalaray.AddOptionalParameter("aveSalary", IntType, "129"); + _edmModel.AddElement(setSalaray); + + EdmEntityContainer container = _edmModel.SchemaElements.OfType().First(); + container.AddActionImport(setSalaray); + + IODataPathTemplateParser parser = new DefaultODataPathTemplateParser(); + + // Act + ODataPathTemplate path = parser.Parse(_edmModel, "SetWholeSalaryImport", null); + + // Assert + Assert.NotNull(path); + ODataSegmentTemplate segmentTemplate = Assert.Single(path); + ActionImportSegmentTemplate actionImportSegment = Assert.IsType(segmentTemplate); + Assert.Equal("SetWholeSalaryImport", actionImportSegment.ActionImport.Name); + } + + private static EdmModel GetEdmModel() + { + EdmModel model = new EdmModel(); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + + EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); + model.AddElement(customer); + model.AddElement(vipCustomer); + + EdmEntityType order = new EdmEntityType("NS", "Order"); + order.AddKeys(order.AddStructuralProperty("OrderId", EdmPrimitiveTypeKind.Int32)); + model.AddElement(order); + + customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo + { + Name = "Orders", + Target = order, + TargetMultiplicity = EdmMultiplicity.Many + }); + + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + entityContainer.AddEntitySet("Customers", customer); + entityContainer.AddSingleton("VipCustomer", customer); + model.AddElement(entityContainer); + return model; + } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/ODataPathSegmentToTemplateHandlerTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/ODataPathSegmentToTemplateHandlerTests.cs index a6eaa806a..5c3a1cd5b 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/ODataPathSegmentToTemplateHandlerTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/ODataPathSegmentToTemplateHandlerTests.cs @@ -15,441 +15,440 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Parser +namespace Microsoft.AspNetCore.OData.Tests.Routing.Parser; + +public class ODataPathSegmentToTemplateHandlerTests { - public class ODataPathSegmentToTemplateHandlerTests + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_Metadata() { - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_Metadata() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - // Act - handler.Handle(MetadataSegment.Instance); + // Act + handler.Handle(MetadataSegment.Instance); - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_Value() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - IEdmType intType = EdmCoreModel.Instance.GetInt32(false).Definition; - ValueSegment segment = new ValueSegment(intType); + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_Value() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + IEdmType intType = EdmCoreModel.Instance.GetInt32(false).Definition; + ValueSegment segment = new ValueSegment(intType); - // Act - handler.Handle(segment); + // Act + handler.Handle(segment); - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_NavigationPropertyLink() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - - EdmEntityType customer = new EdmEntityType("NS", "customer"); - EdmEntityType order = new EdmEntityType("NS", "order"); - IEdmNavigationProperty ordersNavProperty = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "Orders", - Target = order, - TargetMultiplicity = EdmMultiplicity.Many - }); - NavigationPropertyLinkSegment segment = new NavigationPropertyLinkSegment(ordersNavProperty, null); - - // Act - handler.Handle(segment); - - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } - - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_Count() + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_NavigationPropertyLink() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + + EdmEntityType customer = new EdmEntityType("NS", "customer"); + EdmEntityType order = new EdmEntityType("NS", "order"); + IEdmNavigationProperty ordersNavProperty = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + Name = "Orders", + Target = order, + TargetMultiplicity = EdmMultiplicity.Many + }); + NavigationPropertyLinkSegment segment = new NavigationPropertyLinkSegment(ordersNavProperty, null); + + // Act + handler.Handle(segment); + + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - // Act - handler.Handle(CountSegment.Instance); + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_Count() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } + // Act + handler.Handle(CountSegment.Instance); - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_DynamicPath() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - DynamicPathSegment segment = new DynamicPathSegment("any"); + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_DynamicPath() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + DynamicPathSegment segment = new DynamicPathSegment("any"); - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } + // Act + handler.Handle(segment); - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_Action() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); - IEdmAction action = new EdmAction("NS", "action", intType); - OperationSegment segment = new OperationSegment(action, null); - - // Act - handler.Handle(segment); - - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } - - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_Function() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - IEdmFunction function = new EdmFunction("NS", "function", intType); - OperationSegment segment = new OperationSegment(function, null); + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_Action() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); + IEdmAction action = new EdmAction("NS", "action", intType); + OperationSegment segment = new OperationSegment(action, null); + + // Act + handler.Handle(segment); + + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_Function() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } + IEdmFunction function = new EdmFunction("NS", "function", intType); + OperationSegment segment = new OperationSegment(function, null); - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_ActionImport() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); + // Act + handler.Handle(segment); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - IEdmAction action = new EdmAction("NS", "action", intType); - IEdmActionImport actionImport = new EdmActionImport(entityContainer, "action", action); - OperationImportSegment segment = new OperationImportSegment(actionImport, null); + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_ActionImport() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + IEdmAction action = new EdmAction("NS", "action", intType); + IEdmActionImport actionImport = new EdmActionImport(entityContainer, "action", action); + OperationImportSegment segment = new OperationImportSegment(actionImport, null); - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_FunctionImport() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); + // Act + handler.Handle(segment); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - IEdmFunction function = new EdmFunction("NS", "function", intType); - IEdmFunctionImport functionImport = new EdmFunctionImport(entityContainer, "function", function); - OperationImportSegment segment = new OperationImportSegment(functionImport, null); + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_FunctionImport() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + IEdmTypeReference intType = EdmCoreModel.Instance.GetInt32(false); - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + IEdmFunction function = new EdmFunction("NS", "function", intType); + IEdmFunctionImport functionImport = new EdmFunctionImport(entityContainer, "function", function); + OperationImportSegment segment = new OperationImportSegment(functionImport, null); - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_Property() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + // Act + handler.Handle(segment); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - IEdmStructuralProperty property = customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - PropertySegment segment = new PropertySegment(property); + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_Property() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + IEdmStructuralProperty property = customer.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + PropertySegment segment = new PropertySegment(property); - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_Singleton() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + // Act + handler.Handle(segment); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - EdmSingleton me = entityContainer.AddSingleton("me", customer); - SingletonSegment segment = new SingletonSegment(me); + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_Singleton() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + EdmSingleton me = entityContainer.AddSingleton("me", customer); + SingletonSegment segment = new SingletonSegment(me); - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_EntitySet() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + // Act + handler.Handle(segment); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); - EntitySetSegment segment = new EntitySetSegment(customers); + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_EntitySet() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); + EntitySetSegment segment = new EntitySetSegment(customers); - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_NavigationProperty() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - - EdmEntityType customer = new EdmEntityType("NS", "customer"); - EdmEntityType order = new EdmEntityType("NS", "order"); - IEdmNavigationProperty ordersNavProperty = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "Orders", - Target = order, - TargetMultiplicity = EdmMultiplicity.Many - }); - NavigationPropertySegment segment = new NavigationPropertySegment(ordersNavProperty, null); - - // Act - handler.Handle(segment); - - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } - - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_TypeCast() + // Act + handler.Handle(segment); + + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } + + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_NavigationProperty() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + + EdmEntityType customer = new EdmEntityType("NS", "customer"); + EdmEntityType order = new EdmEntityType("NS", "order"); + IEdmNavigationProperty ordersNavProperty = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + Name = "Orders", + Target = order, + TargetMultiplicity = EdmMultiplicity.Many + }); + NavigationPropertySegment segment = new NavigationPropertySegment(ordersNavProperty, null); + + // Act + handler.Handle(segment); + + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); - TypeSegment segment = new TypeSegment(vipCustomer, customer, customers); + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_TypeCast() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - // Act - handler.Handle(segment); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); + TypeSegment segment = new TypeSegment(vipCustomer, customer, customers); - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } + // Act + handler.Handle(segment); - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_PathTemplate() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - PathTemplateSegment segment = new PathTemplateSegment("{any}"); + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_PathTemplate() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + PathTemplateSegment segment = new PathTemplateSegment("{any}"); - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } + // Act + handler.Handle(segment); - [Fact] - public void ODataPathSegmentToTemplateHandler_Throws_BatchSegment() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } + + [Fact] + public void ODataPathSegmentToTemplateHandler_Throws_BatchSegment() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - // Act - Action test = () => handler.Handle(BatchSegment.Instance); + // Act + Action test = () => handler.Handle(BatchSegment.Instance); - // Assert - ExceptionAssert.Throws(test, "'ODataPathSegment' of kind 'BatchSegment' is not implemented."); - } + // Assert + ExceptionAssert.Throws(test, "'ODataPathSegment' of kind 'BatchSegment' is not implemented."); + } - [Fact] - public void ODataPathSegmentToTemplateHandler_Throws_BatchReferenceSegment() - { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + [Fact] + public void ODataPathSegmentToTemplateHandler_Throws_BatchReferenceSegment() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); + BatchReferenceSegment segment = new BatchReferenceSegment("$4", customer, customers); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); - BatchReferenceSegment segment = new BatchReferenceSegment("$4", customer, customers); + // Act + Action test = () => handler.Handle(segment); - // Act - Action test = () => handler.Handle(segment); + // Assert + ExceptionAssert.Throws(test, "'ODataPathSegment' of kind 'BatchReferenceSegment' is not implemented."); + } + + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_Key() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - // Assert - ExceptionAssert.Throws(test, "'ODataPathSegment' of kind 'BatchReferenceSegment' is not implemented."); - } + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_Key() + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); + IDictionary keys = new Dictionary { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); + { "Id", "{key}" } + }; - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + KeySegment segment = new KeySegment(keys, customer, customers); - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); - IDictionary keys = new Dictionary - { - { "Id", "{key}" } - }; + // Act + handler.Handle(segment); - KeySegment segment = new KeySegment(keys, customer, customers); + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } - // Act - handler.Handle(segment); + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_Key_AfterNavigationProperty() + { + // Arrange + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_Key_AfterNavigationProperty() + EdmEntityType order = new EdmEntityType("NS", "order"); + order.AddKeys(order.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + + IEdmNavigationProperty ordersNavProperty = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo + { + Name = "Orders", + Target = order, + TargetMultiplicity = EdmMultiplicity.Many + }); + NavigationPropertyLinkSegment segment1 = new NavigationPropertyLinkSegment(ordersNavProperty, null); + + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + EdmEntitySet orders = entityContainer.AddEntitySet("Orders", order); + IDictionary keys = new Dictionary { - // Arrange - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(null); - - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - - EdmEntityType order = new EdmEntityType("NS", "order"); - order.AddKeys(order.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - - IEdmNavigationProperty ordersNavProperty = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "Orders", - Target = order, - TargetMultiplicity = EdmMultiplicity.Many - }); - NavigationPropertyLinkSegment segment1 = new NavigationPropertyLinkSegment(ordersNavProperty, null); - - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - EdmEntitySet orders = entityContainer.AddEntitySet("Orders", order); - IDictionary keys = new Dictionary - { - { "Id", "{relatedKey}" } - }; - - KeySegment segment2 = new KeySegment(keys, order, orders); - - // Act - handler.Handle(segment1); - handler.Handle(segment2); - - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - NavigationLinkSegmentTemplate nlTemplate = Assert.IsType(segmentTemplate); - Assert.NotNull(nlTemplate.Key); - } - - [Fact] - public void ODataPathSegmentToTemplateHandler_Handles_AlternateKey() + { "Id", "{relatedKey}" } + }; + + KeySegment segment2 = new KeySegment(keys, order, orders); + + // Act + handler.Handle(segment1); + handler.Handle(segment2); + + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + NavigationLinkSegmentTemplate nlTemplate = Assert.IsType(segmentTemplate); + Assert.NotNull(nlTemplate.Key); + } + + [Fact] + public void ODataPathSegmentToTemplateHandler_Handles_AlternateKey() + { + // Arrange + EdmModel model = new EdmModel(); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + IEdmStructuralProperty code = customer.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Int32); + model.AddElement(customer); + + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); + model.AddElement(entityContainer); + + IDictionary alternateKeys = new Dictionary { - // Arrange - EdmModel model = new EdmModel(); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - IEdmStructuralProperty code = customer.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Int32); - model.AddElement(customer); - - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); - model.AddElement(entityContainer); - - IDictionary alternateKeys = new Dictionary - { - { "Code", code } - }; - model.AddAlternateKeyAnnotation(customer, alternateKeys); - - IDictionary keys = new Dictionary - { - { "Code", "{Code}" } - }; - - KeySegment segment = new KeySegment(keys, customer, customers); - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(model); - - // Act - handler.Handle(segment); - - // Assert - ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); - Assert.IsType(segmentTemplate); - } - - [Fact] - public void ODataPathSegmentToTemplateHandler_Throws_WithoutAlternateKey() + { "Code", code } + }; + model.AddAlternateKeyAnnotation(customer, alternateKeys); + + IDictionary keys = new Dictionary { - // Arrange - EdmModel model = new EdmModel(); - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - customer.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Int32); - model.AddElement(customer); - - EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); - EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); - model.AddElement(entityContainer); - IDictionary keys = new Dictionary - { - { "Code", "{Code}" } - }; - - KeySegment segment = new KeySegment(keys, customer, customers); - ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(model); - - // Act - Action test = () => handler.Handle(segment); - - // Assert - ExceptionAssert.Throws(test, "Cannot find key 'Code' in the 'NS.Customer' type."); - } + { "Code", "{Code}" } + }; + + KeySegment segment = new KeySegment(keys, customer, customers); + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(model); + + // Act + handler.Handle(segment); + + // Assert + ODataSegmentTemplate segmentTemplate = Assert.Single(handler.Templates); + Assert.IsType(segmentTemplate); + } + + [Fact] + public void ODataPathSegmentToTemplateHandler_Throws_WithoutAlternateKey() + { + // Arrange + EdmModel model = new EdmModel(); + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + customer.AddStructuralProperty("Code", EdmPrimitiveTypeKind.Int32); + model.AddElement(customer); + + EdmEntityContainer entityContainer = new EdmEntityContainer("NS", "Default"); + EdmEntitySet customers = entityContainer.AddEntitySet("Customers", customer); + model.AddElement(entityContainer); + IDictionary keys = new Dictionary + { + { "Code", "{Code}" } + }; + + KeySegment segment = new KeySegment(keys, customer, customers); + ODataPathSegmentToTemplateHandler handler = new ODataPathSegmentToTemplateHandler(model); + + // Act + Action test = () => handler.Handle(segment); + + // Assert + ExceptionAssert.Throws(test, "Cannot find key 'Code' in the 'NS.Customer' type."); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/UnqualifiedCallAndAlternateKeyResolverTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/UnqualifiedCallAndAlternateKeyResolverTests.cs index 42cd56ac6..19b95e539 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/UnqualifiedCallAndAlternateKeyResolverTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Parser/UnqualifiedCallAndAlternateKeyResolverTests.cs @@ -13,64 +13,63 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Parser +namespace Microsoft.AspNetCore.OData.Tests.Routing.Parser; + +public class UnqualifiedCallAndAlternateKeyResolverTests { - public class UnqualifiedCallAndAlternateKeyResolverTests - { - private static IEdmModel _model = GetEdmModel(); + private static IEdmModel _model = GetEdmModel(); - [Fact] - public void Ctor_ThrowsArgumentNull_Model() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new UnqualifiedCallAndAlternateKeyResolver(null), "model"); - } + [Fact] + public void Ctor_ThrowsArgumentNull_Model() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new UnqualifiedCallAndAlternateKeyResolver(null), "model"); + } - [Fact] - public void ResolveUnboundOperations_CanUnboundOperations() - { - // Arrange - UnqualifiedCallAndAlternateKeyResolver resolver = new UnqualifiedCallAndAlternateKeyResolver(_model); + [Fact] + public void ResolveUnboundOperations_CanUnboundOperations() + { + // Arrange + UnqualifiedCallAndAlternateKeyResolver resolver = new UnqualifiedCallAndAlternateKeyResolver(_model); - // Act - IEnumerable operations = resolver.ResolveUnboundOperations(_model, "UnboundFunc"); + // Act + IEnumerable operations = resolver.ResolveUnboundOperations(_model, "UnboundFunc"); - // Assert - IEdmOperation operation = Assert.Single(operations); - EdmFunction function = Assert.IsType(operation); - Assert.False(function.IsBound); - Assert.Equal("UnboundFunc", function.Name); - } + // Assert + IEdmOperation operation = Assert.Single(operations); + EdmFunction function = Assert.IsType(operation); + Assert.False(function.IsBound); + Assert.Equal("UnboundFunc", function.Name); + } - [Fact] - public void ResolveBoundOperations_CanBoundOperations() - { - // Arrange - UnqualifiedCallAndAlternateKeyResolver resolver = new UnqualifiedCallAndAlternateKeyResolver(_model); - IEdmEntityType type = _model.SchemaElements.OfType().First(); + [Fact] + public void ResolveBoundOperations_CanBoundOperations() + { + // Arrange + UnqualifiedCallAndAlternateKeyResolver resolver = new UnqualifiedCallAndAlternateKeyResolver(_model); + IEdmEntityType type = _model.SchemaElements.OfType().First(); - // Act - IEnumerable operations = resolver.ResolveBoundOperations(_model, "BoundFunc", type); + // Act + IEnumerable operations = resolver.ResolveBoundOperations(_model, "BoundFunc", type); - // Assert - IEdmOperation operation = Assert.Single(operations); - EdmFunction function = Assert.IsType(operation); - Assert.True(function.IsBound); - Assert.Equal("BoundFunc", function.Name); - } + // Assert + IEdmOperation operation = Assert.Single(operations); + EdmFunction function = Assert.IsType(operation); + Assert.True(function.IsBound); + Assert.Equal("BoundFunc", function.Name); + } - private static IEdmModel GetEdmModel() - { - var builder = new ODataConventionModelBuilder(); - builder.EntitySet("Customers"); - builder.Function("UnboundFunc").Returns(); - builder.EntityType().Function("BoundFunc").Returns(); - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.Function("UnboundFunc").Returns(); + builder.EntityType().Function("BoundFunc").Returns(); + return builder.GetEdmModel(); + } - private class Customer - { - public int Id { get; set; } - } + private class Customer + { + public int Id { get; set; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ActionImportSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ActionImportSegmentTemplateTests.cs index baec24a9d..d77a1347c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ActionImportSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ActionImportSegmentTemplateTests.cs @@ -16,105 +16,104 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class ActionImportSegmentTemplateTests { - public class ActionImportSegmentTemplateTests + [Fact] + public void CtorActionImportSegmentTemplate_ThrowsArgumentNull_ActionImport() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ActionImportSegmentTemplate(actionImport: null, null), "actionImport"); + } + + [Fact] + public void CtorActionImportSegmentTemplate_ThrowsArgumentNull_Segment() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ActionImportSegmentTemplate(segment: null), "segment"); + } + + [Fact] + public void CtorActionImportSegmentTemplate_ThrowsException_NonActionImport() + { + // Arrange + IEdmPrimitiveTypeReference IntPrimitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); + EdmFunction function = new EdmFunction("NS", "MyFunction", IntPrimitive, false, null, false); + + Mock import = new Mock(); + import.Setup(i => i.Name).Returns("any"); + import.Setup(i => i.ContainerElementKind).Returns(EdmContainerElementKind.FunctionImport); + import.Setup(i => i.Operation).Returns(function); + OperationImportSegment operationImportSegment = new OperationImportSegment(import.Object, null); + + // Act + Action test = () => new ActionImportSegmentTemplate(operationImportSegment); + + // Assert + ExceptionAssert.Throws(test, "The input segment should be 'ActionImport' in 'ActionImportSegmentTemplate'."); + } + + [Fact] + public void CtorActionSegmentTemplate_SetsProperties() + { + // Arrange & Act + ActionImportSegmentTemplate segment = GetSegmentTemplate(out IEdmActionImport actionImport); + + // Assert + Assert.Same(actionImport, segment.ActionImport); + Assert.NotNull(segment.Segment); + Assert.Single(segment.Segment.OperationImports); + + // Act & Assert + ActionImportSegmentTemplate segment1 = new ActionImportSegmentTemplate(segment.Segment); + Assert.Same(segment.Segment, segment1.Segment); + } + + [Fact] + public void GetTemplatesActionImportSegmentTemplate_ReturnsTemplates() + { + // Arrange + ActionImportSegmentTemplate segment = GetSegmentTemplate(out _); + + // Act & Assert + IEnumerable templates = segment.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/actionImport", template); + } + + [Fact] + public void TryTranslateActionImportSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + ActionImportSegmentTemplate segment = GetSegmentTemplate(out _); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => segment.TryTranslate(null), "context"); + } + + [Fact] + public void TryTranslateActionImportSegmentTemplate_ReturnsODataActionImportSegment() + { + // Arrange + ActionImportSegmentTemplate template = GetSegmentTemplate(out IEdmActionImport actionImport); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + + // Act + bool ok = template.TryTranslate(context); + + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + OperationImportSegment actionImportSegment = Assert.IsType(actual); + Assert.Same(actionImport, actionImportSegment.OperationImports.First()); + } + + private static ActionImportSegmentTemplate GetSegmentTemplate(out IEdmActionImport actionImport) { - [Fact] - public void CtorActionImportSegmentTemplate_ThrowsArgumentNull_ActionImport() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ActionImportSegmentTemplate(actionImport: null, null), "actionImport"); - } - - [Fact] - public void CtorActionImportSegmentTemplate_ThrowsArgumentNull_Segment() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ActionImportSegmentTemplate(segment: null), "segment"); - } - - [Fact] - public void CtorActionImportSegmentTemplate_ThrowsException_NonActionImport() - { - // Arrange - IEdmPrimitiveTypeReference IntPrimitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); - EdmFunction function = new EdmFunction("NS", "MyFunction", IntPrimitive, false, null, false); - - Mock import = new Mock(); - import.Setup(i => i.Name).Returns("any"); - import.Setup(i => i.ContainerElementKind).Returns(EdmContainerElementKind.FunctionImport); - import.Setup(i => i.Operation).Returns(function); - OperationImportSegment operationImportSegment = new OperationImportSegment(import.Object, null); - - // Act - Action test = () => new ActionImportSegmentTemplate(operationImportSegment); - - // Assert - ExceptionAssert.Throws(test, "The input segment should be 'ActionImport' in 'ActionImportSegmentTemplate'."); - } - - [Fact] - public void CtorActionSegmentTemplate_SetsProperties() - { - // Arrange & Act - ActionImportSegmentTemplate segment = GetSegmentTemplate(out IEdmActionImport actionImport); - - // Assert - Assert.Same(actionImport, segment.ActionImport); - Assert.NotNull(segment.Segment); - Assert.Single(segment.Segment.OperationImports); - - // Act & Assert - ActionImportSegmentTemplate segment1 = new ActionImportSegmentTemplate(segment.Segment); - Assert.Same(segment.Segment, segment1.Segment); - } - - [Fact] - public void GetTemplatesActionImportSegmentTemplate_ReturnsTemplates() - { - // Arrange - ActionImportSegmentTemplate segment = GetSegmentTemplate(out _); - - // Act & Assert - IEnumerable templates = segment.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/actionImport", template); - } - - [Fact] - public void TryTranslateActionImportSegmentTemplate_ThrowsArgumentNull_Context() - { - // Arrange - ActionImportSegmentTemplate segment = GetSegmentTemplate(out _); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => segment.TryTranslate(null), "context"); - } - - [Fact] - public void TryTranslateActionImportSegmentTemplate_ReturnsODataActionImportSegment() - { - // Arrange - ActionImportSegmentTemplate template = GetSegmentTemplate(out IEdmActionImport actionImport); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - - // Act - bool ok = template.TryTranslate(context); - - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - OperationImportSegment actionImportSegment = Assert.IsType(actual); - Assert.Same(actionImport, actionImportSegment.OperationImports.First()); - } - - private static ActionImportSegmentTemplate GetSegmentTemplate(out IEdmActionImport actionImport) - { - EdmEntityContainer container = new EdmEntityContainer("NS", "default"); - EdmAction action = new EdmAction("NS", "action", null); - actionImport = new EdmActionImport(container, "actionImport", action); - return new ActionImportSegmentTemplate(actionImport, null); - } + EdmEntityContainer container = new EdmEntityContainer("NS", "default"); + EdmAction action = new EdmAction("NS", "action", null); + actionImport = new EdmActionImport(container, "actionImport", action); + return new ActionImportSegmentTemplate(actionImport, null); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ActionSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ActionSegmentTemplateTests.cs index 4556e5338..3cf1e969c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ActionSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ActionSegmentTemplateTests.cs @@ -21,164 +21,163 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class ActionSegmentTemplateTests { - public class ActionSegmentTemplateTests + [Fact] + public void CtorActionSegmentTemplate_ThrowsArgumentNull_Action() { - [Fact] - public void CtorActionSegmentTemplate_ThrowsArgumentNull_Action() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ActionSegmentTemplate(action: null, null), "action"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ActionSegmentTemplate(action: null, null), "action"); + } - [Fact] - public void CtorActionSegmentTemplate_ThrowsArgumentNull_Segment() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ActionSegmentTemplate(null), "segment"); - } + [Fact] + public void CtorActionSegmentTemplate_ThrowsArgumentNull_Segment() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ActionSegmentTemplate(null), "segment"); + } - [Fact] - public void CtorActionSegmentTemplate_ThrowsArgument_NonboundAction() - { - // Arrange - var primitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); - EdmAction action = new EdmAction("NS", "MyAction", primitive, false, null); + [Fact] + public void CtorActionSegmentTemplate_ThrowsArgument_NonboundAction() + { + // Arrange + var primitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); + EdmAction action = new EdmAction("NS", "MyAction", primitive, false, null); - // Act & Assert - ExceptionAssert.Throws(() => new ActionSegmentTemplate(action, null), - "The input operation 'MyAction' is not a bound 'action'."); - } + // Act & Assert + ExceptionAssert.Throws(() => new ActionSegmentTemplate(action, null), + "The input operation 'MyAction' is not a bound 'action'."); + } - [Fact] - public void CtorActionSegmentTemplate_ThrowsODataException_NonAction() - { - // Arrange - IEdmPrimitiveTypeReference intPrimitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); - EdmFunction function = new EdmFunction("NS", "MyFunction", intPrimitive, false, null, false); - OperationSegment operationSegment = new OperationSegment(function, null); + [Fact] + public void CtorActionSegmentTemplate_ThrowsODataException_NonAction() + { + // Arrange + IEdmPrimitiveTypeReference intPrimitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); + EdmFunction function = new EdmFunction("NS", "MyFunction", intPrimitive, false, null, false); + OperationSegment operationSegment = new OperationSegment(function, null); - // Act - Action test = () => new ActionSegmentTemplate(operationSegment); + // Act + Action test = () => new ActionSegmentTemplate(operationSegment); - // Assert - ExceptionAssert.Throws(test, "The input segment should be 'Action' in 'ActionSegmentTemplate'."); - } + // Assert + ExceptionAssert.Throws(test, "The input segment should be 'Action' in 'ActionSegmentTemplate'."); + } - [Fact] - public void CtorActionSegmentTemplate_SetsProperties() - { - // Arrange & Act - EdmAction action = new EdmAction("NS", "action", null, true, null); - ActionSegmentTemplate segment = new ActionSegmentTemplate(action, null); - - // Assert - Assert.Same(action, segment.Action); - Assert.NotNull(segment.Segment); - Assert.Null(segment.NavigationSource); - - // Act & Assert - ActionSegmentTemplate segment1 = new ActionSegmentTemplate(segment.Segment); - Assert.Same(segment.Segment, segment1.Segment); - } - - [Fact] - public void GetTemplatesActionSegmentTemplate_ReturnsTemplates() - { - // Assert - EdmAction action = new EdmAction("NS", "action", null, true, null); - ActionSegmentTemplate segment = new ActionSegmentTemplate(action, null); - - // 1- Act & Assert - IEnumerable templates = segment.GetTemplates(); - Assert.Collection(templates, - e => - { - Assert.Equal("/NS.action", e); - }, - e => - { - Assert.Equal("/action", e); - }); - - // 2- Act & Assert - templates = segment.GetTemplates(new ODataRouteOptions - { - EnableQualifiedOperationCall = false - }); - string template = Assert.Single(templates); - Assert.Equal("/action", template); + [Fact] + public void CtorActionSegmentTemplate_SetsProperties() + { + // Arrange & Act + EdmAction action = new EdmAction("NS", "action", null, true, null); + ActionSegmentTemplate segment = new ActionSegmentTemplate(action, null); + + // Assert + Assert.Same(action, segment.Action); + Assert.NotNull(segment.Segment); + Assert.Null(segment.NavigationSource); + + // Act & Assert + ActionSegmentTemplate segment1 = new ActionSegmentTemplate(segment.Segment); + Assert.Same(segment.Segment, segment1.Segment); + } - // 3- Act & Assert - templates = segment.GetTemplates(new ODataRouteOptions + [Fact] + public void GetTemplatesActionSegmentTemplate_ReturnsTemplates() + { + // Assert + EdmAction action = new EdmAction("NS", "action", null, true, null); + ActionSegmentTemplate segment = new ActionSegmentTemplate(action, null); + + // 1- Act & Assert + IEnumerable templates = segment.GetTemplates(); + Assert.Collection(templates, + e => + { + Assert.Equal("/NS.action", e); + }, + e => { - EnableUnqualifiedOperationCall = false + Assert.Equal("/action", e); }); - template = Assert.Single(templates); - Assert.Equal("/NS.action", template); - } - [Fact] - public void TryTranslateActionSegmentTemplate_ThrowsArgumentNull_Context() + // 2- Act & Assert + templates = segment.GetTemplates(new ODataRouteOptions { - // Arrange - EdmAction action = new EdmAction("NS", "action", null, true, null); - ActionSegmentTemplate template = new ActionSegmentTemplate(action, null); + EnableQualifiedOperationCall = false + }); + string template = Assert.Single(templates); + Assert.Equal("/action", template); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => template.TryTranslate(null), "context"); - } - - [Fact] - public void TryTranslateActionSegmentTemplate_ReturnsODataActionImportSegment() - { - // Arrange - EdmAction action = new EdmAction("NS", "action", null, true, null); - ActionSegmentTemplate template = new ActionSegmentTemplate(action, null); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - - // Act - bool ok = template.TryTranslate(context); - - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - OperationSegment actionSegment = Assert.IsType(actual); - Assert.Same(action, actionSegment.Operations.First()); - } - - [Fact] - public void TryTranslateActionSegmentTemplate_ReturnsODataActionSegment_WithReturnedEntitySet() + // 3- Act & Assert + templates = segment.GetTemplates(new ODataRouteOptions { - // Arrange - var httpContext = new Mock().Object; - var endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test"); - var routeValues = new RouteValueDictionary(); - - var model = new EdmModel(); - var entityType = new EdmEntityType("NS", "Entity"); - entityType.AddKeys(entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - model.AddElement(entityType); - EdmAction action = new EdmAction("NS", "Action", new EdmEntityTypeReference(entityType, true), true, null); - model.AddElement(action); - var entityContainer = new EdmEntityContainer("NS", "Default"); - var entitySet = entityContainer.AddEntitySet("EntitySet", entityType); - model.AddElement(entityContainer); - model.SetAnnotationValue(action, new ReturnedEntitySetAnnotation("EntitySet")); - - var template = new ActionSegmentTemplate(action, null); - var translateContext = new ODataTemplateTranslateContext(httpContext, endpoint, routeValues, model); - - // Act - bool ok = template.TryTranslate(translateContext); - - // Assert - Assert.True(ok); - var actual = Assert.Single(translateContext.Segments); - var actionSegment = Assert.IsType(actual); - Assert.Equal(actionSegment.EdmType, entityType); - Assert.Equal(actionSegment.EntitySet, entitySet); - } + EnableUnqualifiedOperationCall = false + }); + template = Assert.Single(templates); + Assert.Equal("/NS.action", template); + } + + [Fact] + public void TryTranslateActionSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + EdmAction action = new EdmAction("NS", "action", null, true, null); + ActionSegmentTemplate template = new ActionSegmentTemplate(action, null); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => template.TryTranslate(null), "context"); + } + + [Fact] + public void TryTranslateActionSegmentTemplate_ReturnsODataActionImportSegment() + { + // Arrange + EdmAction action = new EdmAction("NS", "action", null, true, null); + ActionSegmentTemplate template = new ActionSegmentTemplate(action, null); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + + // Act + bool ok = template.TryTranslate(context); + + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + OperationSegment actionSegment = Assert.IsType(actual); + Assert.Same(action, actionSegment.Operations.First()); + } + + [Fact] + public void TryTranslateActionSegmentTemplate_ReturnsODataActionSegment_WithReturnedEntitySet() + { + // Arrange + var httpContext = new Mock().Object; + var endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test"); + var routeValues = new RouteValueDictionary(); + + var model = new EdmModel(); + var entityType = new EdmEntityType("NS", "Entity"); + entityType.AddKeys(entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + model.AddElement(entityType); + EdmAction action = new EdmAction("NS", "Action", new EdmEntityTypeReference(entityType, true), true, null); + model.AddElement(action); + var entityContainer = new EdmEntityContainer("NS", "Default"); + var entitySet = entityContainer.AddEntitySet("EntitySet", entityType); + model.AddElement(entityContainer); + model.SetAnnotationValue(action, new ReturnedEntitySetAnnotation("EntitySet")); + + var template = new ActionSegmentTemplate(action, null); + var translateContext = new ODataTemplateTranslateContext(httpContext, endpoint, routeValues, model); + + // Act + bool ok = template.TryTranslate(translateContext); + + // Assert + Assert.True(ok); + var actual = Assert.Single(translateContext.Segments); + var actionSegment = Assert.IsType(actual); + Assert.Equal(actionSegment.EdmType, entityType); + Assert.Equal(actionSegment.EntitySet, entitySet); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/CastSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/CastSegmentTemplateTests.cs index bd7a5780e..999b64f05 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/CastSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/CastSegmentTemplateTests.cs @@ -13,95 +13,94 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class CastSegmentTemplateTests { - public class CastSegmentTemplateTests + [Fact] + public void CtorCastSegmentTemplate_ThrowsArgumentNull_CastType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new CastSegmentTemplate(null, null, null), "castType"); + } + + [Fact] + public void CtorCastSegmentTemplate_ThrowsArgumentNull_ExpectedType() + { + // Assert + Mock edmType = new Mock(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new CastSegmentTemplate(edmType.Object, null, null), "expectedType"); + } + + [Fact] + public void CtorCastSegmentTemplate_ThrowsArgumentNull_TypeSegment() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new CastSegmentTemplate(null), "typeSegment"); + } + + [Fact] + public void CtorCastSegmentTemplate_SetsProperties() + { + // Arrange & Act + EdmEntityType baseType = new EdmEntityType("NS", "base"); + EdmEntityType subType = new EdmEntityType("NS", "sub", baseType); + CastSegmentTemplate segment = new CastSegmentTemplate(subType, baseType, null); + + // Assert + Assert.Same(subType, segment.CastType); + Assert.Same(baseType, segment.ExpectedType); + Assert.NotNull(segment.TypeSegment); + + // Act & Assert + CastSegmentTemplate segment1 = new CastSegmentTemplate(segment.TypeSegment); + Assert.Same(segment.TypeSegment, segment1.TypeSegment); + } + + [Fact] + public void GetTemplatesCastSegmentTemplate_ReturnsTemplates() + { + // Assert + EdmEntityType baseType = new EdmEntityType("NS", "base"); + EdmEntityType subType = new EdmEntityType("NS", "sub", baseType); + CastSegmentTemplate segment = new CastSegmentTemplate(subType, baseType, null); + + // Act & Assert + IEnumerable templates = segment.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/NS.sub", template); + } + + [Fact] + public void TryTranslateCastSegmentTemplate_ThrowsArgumentNull_Context() { - [Fact] - public void CtorCastSegmentTemplate_ThrowsArgumentNull_CastType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new CastSegmentTemplate(null, null, null), "castType"); - } - - [Fact] - public void CtorCastSegmentTemplate_ThrowsArgumentNull_ExpectedType() - { - // Assert - Mock edmType = new Mock(); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new CastSegmentTemplate(edmType.Object, null, null), "expectedType"); - } - - [Fact] - public void CtorCastSegmentTemplate_ThrowsArgumentNull_TypeSegment() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new CastSegmentTemplate(null), "typeSegment"); - } - - [Fact] - public void CtorCastSegmentTemplate_SetsProperties() - { - // Arrange & Act - EdmEntityType baseType = new EdmEntityType("NS", "base"); - EdmEntityType subType = new EdmEntityType("NS", "sub", baseType); - CastSegmentTemplate segment = new CastSegmentTemplate(subType, baseType, null); - - // Assert - Assert.Same(subType, segment.CastType); - Assert.Same(baseType, segment.ExpectedType); - Assert.NotNull(segment.TypeSegment); - - // Act & Assert - CastSegmentTemplate segment1 = new CastSegmentTemplate(segment.TypeSegment); - Assert.Same(segment.TypeSegment, segment1.TypeSegment); - } - - [Fact] - public void GetTemplatesCastSegmentTemplate_ReturnsTemplates() - { - // Assert - EdmEntityType baseType = new EdmEntityType("NS", "base"); - EdmEntityType subType = new EdmEntityType("NS", "sub", baseType); - CastSegmentTemplate segment = new CastSegmentTemplate(subType, baseType, null); - - // Act & Assert - IEnumerable templates = segment.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/NS.sub", template); - } - - [Fact] - public void TryTranslateCastSegmentTemplate_ThrowsArgumentNull_Context() - { - // Arrange - EdmEntityType baseType = new EdmEntityType("NS", "base"); - EdmEntityType subType = new EdmEntityType("NS", "sub", baseType); - CastSegmentTemplate segment = new CastSegmentTemplate(subType, baseType, null); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => segment.TryTranslate(null), "context"); - } - - [Fact] - public void TryTranslateCastSegmentTemplate_ReturnsODataTypeSegment() - { - // Arrange - EdmEntityType baseType = new EdmEntityType("NS", "base"); - EdmEntityType subType = new EdmEntityType("NS", "sub", baseType); - CastSegmentTemplate template = new CastSegmentTemplate(subType, baseType, null); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - - // Act - bool ok = template.TryTranslate(context); - - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - TypeSegment typeSegment = Assert.IsType(actual); - Assert.Same(subType, typeSegment.EdmType); - } + // Arrange + EdmEntityType baseType = new EdmEntityType("NS", "base"); + EdmEntityType subType = new EdmEntityType("NS", "sub", baseType); + CastSegmentTemplate segment = new CastSegmentTemplate(subType, baseType, null); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => segment.TryTranslate(null), "context"); + } + + [Fact] + public void TryTranslateCastSegmentTemplate_ReturnsODataTypeSegment() + { + // Arrange + EdmEntityType baseType = new EdmEntityType("NS", "base"); + EdmEntityType subType = new EdmEntityType("NS", "sub", baseType); + CastSegmentTemplate template = new CastSegmentTemplate(subType, baseType, null); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + + // Act + bool ok = template.TryTranslate(context); + + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + TypeSegment typeSegment = Assert.IsType(actual); + Assert.Same(subType, typeSegment.EdmType); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/CountSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/CountSegmentTemplateTests.cs index 0937eb01f..f29950745 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/CountSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/CountSegmentTemplateTests.cs @@ -11,40 +11,39 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class CountSegmentTemplateTests { - public class CountSegmentTemplateTests + [Fact] + public void GetTemplatesCountSegmentTemplate_ReturnsTemplates() { - [Fact] - public void GetTemplatesCountSegmentTemplate_ReturnsTemplates() - { - // Assert & Act & Assert - IEnumerable templates = CountSegmentTemplate.Instance.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/$count", template); - } + // Assert & Act & Assert + IEnumerable templates = CountSegmentTemplate.Instance.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/$count", template); + } - [Fact] - public void TryTranslateCountSegmentTemplate_ThrowsArgumentNull_Context() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => CountSegmentTemplate.Instance.TryTranslate(null), "context"); - } + [Fact] + public void TryTranslateCountSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => CountSegmentTemplate.Instance.TryTranslate(null), "context"); + } - [Fact] - public void TryTranslateCountSegmentTemplate_ReturnsODataCountSegment() - { - // Arrange - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + [Fact] + public void TryTranslateCountSegmentTemplate_ReturnsODataCountSegment() + { + // Arrange + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - // Act - bool ok = CountSegmentTemplate.Instance.TryTranslate(context); + // Act + bool ok = CountSegmentTemplate.Instance.TryTranslate(context); - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - CountSegment countSegment = Assert.IsType(actual); - Assert.Same(CountSegment.Instance, countSegment); - } + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + CountSegment countSegment = Assert.IsType(actual); + Assert.Same(CountSegment.Instance, countSegment); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/DefaultODataTemplateTranslatorTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/DefaultODataTemplateTranslatorTests.cs index 547851e28..acb1d272f 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/DefaultODataTemplateTranslatorTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/DefaultODataTemplateTranslatorTests.cs @@ -14,92 +14,91 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class DefaultODataTemplateTranslatorTests { - public class DefaultODataTemplateTranslatorTests + [Fact] + public void TranslateODataPathTemplate_ThrowsArgumentNull_Path() { - [Fact] - public void TranslateODataPathTemplate_ThrowsArgumentNull_Path() - { - // Arrange - DefaultODataTemplateTranslator translator = new DefaultODataTemplateTranslator(); + // Arrange + DefaultODataTemplateTranslator translator = new DefaultODataTemplateTranslator(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => translator.Translate(null, null), "path"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => translator.Translate(null, null), "path"); + } - [Fact] - public void TranslateODataPathTemplate_ThrowsArgumentNull_Context() - { - // Arrange - DefaultODataTemplateTranslator translator = new DefaultODataTemplateTranslator(); - ODataPathTemplate template = new ODataPathTemplate(); + [Fact] + public void TranslateODataPathTemplate_ThrowsArgumentNull_Context() + { + // Arrange + DefaultODataTemplateTranslator translator = new DefaultODataTemplateTranslator(); + ODataPathTemplate template = new ODataPathTemplate(); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => translator.Translate(template, null), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => translator.Translate(template, null), "context"); + } + + [Fact] + public void TranslateODataPathTemplate_ReturnsNull() + { + // Arrange + DefaultODataTemplateTranslator translator = new DefaultODataTemplateTranslator(); + ODataPathTemplate path = new ODataPathTemplate(new MySegmentTemplate()); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - [Fact] - public void TranslateODataPathTemplate_ReturnsNull() + // Act & Assert + Assert.Null(translator.Translate(path, context)); + } + + [Fact] + public void TranslateODataPathTemplate_ToODataPath() + { + // Assert + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("CustomerId", EdmPrimitiveTypeKind.Int32)); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + var entitySet = container.AddEntitySet("Customers", customer); + EdmModel model = new EdmModel(); + model.AddElement(customer); + model.AddElement(container); + + ODataPathTemplate template = new ODataPathTemplate( + new EntitySetSegmentTemplate(entitySet), + KeySegmentTemplate.CreateKeySegment(customer, entitySet)); + + DefaultODataTemplateTranslator translator = new DefaultODataTemplateTranslator(); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - DefaultODataTemplateTranslator translator = new DefaultODataTemplateTranslator(); - ODataPathTemplate path = new ODataPathTemplate(new MySegmentTemplate()); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + RouteValues = new RouteValueDictionary(new { key = "42" }), + Model = model + }; - // Act & Assert - Assert.Null(translator.Translate(path, context)); - } + // Act + ODataPath path = translator.Translate(template, context); + + // Assert + Assert.NotNull(path); + Assert.Equal(2, path.Count); + EntitySetSegment entitySetSegment = Assert.IsType(path.FirstSegment); + Assert.Equal("Customers", entitySetSegment.EntitySet.Name); - [Fact] - public void TranslateODataPathTemplate_ToODataPath() + KeySegment keySegment = Assert.IsType(path.LastSegment); + KeyValuePair key = Assert.Single(keySegment.Keys); + Assert.Equal("CustomerId", key.Key); + Assert.Equal(42, key.Value); + } + + private class MySegmentTemplate : ODataSegmentTemplate + { + public override IEnumerable GetTemplates(ODataRouteOptions options) { - // Assert - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("CustomerId", EdmPrimitiveTypeKind.Int32)); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - var entitySet = container.AddEntitySet("Customers", customer); - EdmModel model = new EdmModel(); - model.AddElement(customer); - model.AddElement(container); - - ODataPathTemplate template = new ODataPathTemplate( - new EntitySetSegmentTemplate(entitySet), - KeySegmentTemplate.CreateKeySegment(customer, entitySet)); - - DefaultODataTemplateTranslator translator = new DefaultODataTemplateTranslator(); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = new RouteValueDictionary(new { key = "42" }), - Model = model - }; - - // Act - ODataPath path = translator.Translate(template, context); - - // Assert - Assert.NotNull(path); - Assert.Equal(2, path.Count); - EntitySetSegment entitySetSegment = Assert.IsType(path.FirstSegment); - Assert.Equal("Customers", entitySetSegment.EntitySet.Name); - - KeySegment keySegment = Assert.IsType(path.LastSegment); - KeyValuePair key = Assert.Single(keySegment.Keys); - Assert.Equal("CustomerId", key.Key); - Assert.Equal(42, key.Value); + return null; } - private class MySegmentTemplate : ODataSegmentTemplate + public override bool TryTranslate(ODataTemplateTranslateContext context) { - public override IEnumerable GetTemplates(ODataRouteOptions options) - { - return null; - } - - public override bool TryTranslate(ODataTemplateTranslateContext context) - { - return false; - } + return false; } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/DynamicSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/DynamicSegmentTemplateTests.cs index 81052cfe5..8e3f3f71e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/DynamicSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/DynamicSegmentTemplateTests.cs @@ -11,68 +11,67 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class DynamicSegmentTemplateTests { - public class DynamicSegmentTemplateTests + [Fact] + public void CtorDynamicSegmentTemplate_ThrowsArgumentNull_Segment() { - [Fact] - public void CtorDynamicSegmentTemplate_ThrowsArgumentNull_Segment() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new DynamicSegmentTemplate(segment: null), "segment"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new DynamicSegmentTemplate(segment: null), "segment"); + } - [Fact] - public void CtorDynamicSegmentTemplate_SetsProperties() - { - // Arrange & Act - DynamicPathSegment segment = new DynamicPathSegment("dynamic"); - DynamicSegmentTemplate dynamicSegment = new DynamicSegmentTemplate(segment); + [Fact] + public void CtorDynamicSegmentTemplate_SetsProperties() + { + // Arrange & Act + DynamicPathSegment segment = new DynamicPathSegment("dynamic"); + DynamicSegmentTemplate dynamicSegment = new DynamicSegmentTemplate(segment); - // Assert - Assert.Same(segment, dynamicSegment.Segment); - } + // Assert + Assert.Same(segment, dynamicSegment.Segment); + } - [Fact] - public void GetTemplatesDynamicSegmentTemplate_ReturnsTemplates() - { - // Arrange - DynamicPathSegment segment = new DynamicPathSegment("dynamic"); - DynamicSegmentTemplate dynamicSegment = new DynamicSegmentTemplate(segment); + [Fact] + public void GetTemplatesDynamicSegmentTemplate_ReturnsTemplates() + { + // Arrange + DynamicPathSegment segment = new DynamicPathSegment("dynamic"); + DynamicSegmentTemplate dynamicSegment = new DynamicSegmentTemplate(segment); - // Act & Assert - IEnumerable templates = dynamicSegment.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/dynamic", template); - } + // Act & Assert + IEnumerable templates = dynamicSegment.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/dynamic", template); + } - [Fact] - public void TryTranslateDynamicSegmentTemplate_ThrowsArgumentNull_Context() - { - // Arrange - DynamicPathSegment segment = new DynamicPathSegment("dynamic"); - DynamicSegmentTemplate dynamicSegment = new DynamicSegmentTemplate(segment); + [Fact] + public void TryTranslateDynamicSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + DynamicPathSegment segment = new DynamicPathSegment("dynamic"); + DynamicSegmentTemplate dynamicSegment = new DynamicSegmentTemplate(segment); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => dynamicSegment.TryTranslate(null), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => dynamicSegment.TryTranslate(null), "context"); + } - [Fact] - public void TryTranslateDynamicSegmentTemplate_ReturnsODataCountSegment() - { - // Arrange - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - DynamicPathSegment segment = new DynamicPathSegment("dynamic"); - DynamicSegmentTemplate dynamicSegment = new DynamicSegmentTemplate(segment); + [Fact] + public void TryTranslateDynamicSegmentTemplate_ReturnsODataCountSegment() + { + // Arrange + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + DynamicPathSegment segment = new DynamicPathSegment("dynamic"); + DynamicSegmentTemplate dynamicSegment = new DynamicSegmentTemplate(segment); - // Act - bool ok = dynamicSegment.TryTranslate(context); + // Act + bool ok = dynamicSegment.TryTranslate(context); - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - DynamicPathSegment actualSegment = Assert.IsType(actual); - Assert.Same(segment, actualSegment); - } + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + DynamicPathSegment actualSegment = Assert.IsType(actual); + Assert.Same(segment, actualSegment); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/EntitySetSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/EntitySetSegmentTemplateTests.cs index c018cec05..37569d3c0 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/EntitySetSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/EntitySetSegmentTemplateTests.cs @@ -12,80 +12,79 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class EntitySetSegmentTemplateTests { - public class EntitySetSegmentTemplateTests + [Fact] + public void CtorEntitySetSegmentTemplate_ThrowsArgumentNull_EntitySet() { - [Fact] - public void CtorEntitySetSegmentTemplate_ThrowsArgumentNull_EntitySet() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EntitySetSegmentTemplate(entitySet: null), "entitySet"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EntitySetSegmentTemplate(entitySet: null), "entitySet"); + } - [Fact] - public void CtorEntitySetSegmentTemplate_ThrowsArgumentNull_EntitySetSegment() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new EntitySetSegmentTemplate(segment: null), "segment"); - } + [Fact] + public void CtorEntitySetSegmentTemplate_ThrowsArgumentNull_EntitySetSegment() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new EntitySetSegmentTemplate(segment: null), "segment"); + } - [Fact] - public void CtorEntitySetSegmentTemplate_SetsProperties() - { - // Arrange & Act - EntitySetSegmentTemplate segmentTemplate = GetSegmentTemplate(out EdmEntitySet entitySet); + [Fact] + public void CtorEntitySetSegmentTemplate_SetsProperties() + { + // Arrange & Act + EntitySetSegmentTemplate segmentTemplate = GetSegmentTemplate(out EdmEntitySet entitySet); - // Assert - Assert.NotNull(segmentTemplate.Segment); - Assert.Same(entitySet, segmentTemplate.EntitySet); - } + // Assert + Assert.NotNull(segmentTemplate.Segment); + Assert.Same(entitySet, segmentTemplate.EntitySet); + } - [Fact] - public void GetTemplatesEntitySetSegmentTemplate_ReturnsTemplates() - { - // Arrange - EntitySetSegmentTemplate segmentTemplate = GetSegmentTemplate(out _); + [Fact] + public void GetTemplatesEntitySetSegmentTemplate_ReturnsTemplates() + { + // Arrange + EntitySetSegmentTemplate segmentTemplate = GetSegmentTemplate(out _); - // Act & Assert - IEnumerable templates = segmentTemplate.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/entities", template); - } + // Act & Assert + IEnumerable templates = segmentTemplate.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/entities", template); + } - [Fact] - public void TryTranslateEntitySetSegmentTemplate_ThrowsArgumentNull_Context() - { - // Arrange - EntitySetSegmentTemplate template = GetSegmentTemplate(out _); + [Fact] + public void TryTranslateEntitySetSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + EntitySetSegmentTemplate template = GetSegmentTemplate(out _); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => template.TryTranslate(null), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => template.TryTranslate(null), "context"); + } - [Fact] - public void TryTranslateEntitySetSegmentTemplate_ReturnsODataEntitySetSegment() - { - // Arrange - EntitySetSegmentTemplate template = GetSegmentTemplate(out EdmEntitySet entitySet); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + [Fact] + public void TryTranslateEntitySetSegmentTemplate_ReturnsODataEntitySetSegment() + { + // Arrange + EntitySetSegmentTemplate template = GetSegmentTemplate(out EdmEntitySet entitySet); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - // Act - bool ok = template.TryTranslate(context); + // Act + bool ok = template.TryTranslate(context); - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - EntitySetSegment setSegment = Assert.IsType(actual); - Assert.Same(entitySet, setSegment.EntitySet); - } + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + EntitySetSegment setSegment = Assert.IsType(actual); + Assert.Same(entitySet, setSegment.EntitySet); + } - private static EntitySetSegmentTemplate GetSegmentTemplate(out EdmEntitySet entitySet) - { - EdmEntityType entityType = new EdmEntityType("NS", "entity"); - EdmEntityContainer container = new EdmEntityContainer("NS", "default"); - entitySet = new EdmEntitySet(container, "entities", entityType); - return new EntitySetSegmentTemplate(entitySet); - } + private static EntitySetSegmentTemplate GetSegmentTemplate(out EdmEntitySet entitySet) + { + EdmEntityType entityType = new EdmEntityType("NS", "entity"); + EdmEntityContainer container = new EdmEntityContainer("NS", "default"); + entitySet = new EdmEntitySet(container, "entities", entityType); + return new EntitySetSegmentTemplate(entitySet); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/FunctionImportSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/FunctionImportSegmentTemplateTests.cs index da0722c43..5f05a1354 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/FunctionImportSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/FunctionImportSegmentTemplateTests.cs @@ -18,272 +18,271 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class FunctionImportSegmentTemplateTests { - public class FunctionImportSegmentTemplateTests + private static IEdmPrimitiveTypeReference IntPrimitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); + private static IEdmPrimitiveTypeReference StringPrimitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, false); + + private EdmFunctionImport _functionImport; + private EdmFunction _function; + private EdmEntityContainer _container; + + public FunctionImportSegmentTemplateTests() { - private static IEdmPrimitiveTypeReference IntPrimitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); - private static IEdmPrimitiveTypeReference StringPrimitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, false); + _function = new EdmFunction("NS", "MyFunctionImport", IntPrimitive, false, null, false); + _function.AddParameter("name", StringPrimitive); + _function.AddParameter("title", StringPrimitive); + _container = new EdmEntityContainer("NS", "Default"); + _functionImport = new EdmFunctionImport(_container, "MyFunctionImport", _function); + } - private EdmFunctionImport _functionImport; - private EdmFunction _function; - private EdmEntityContainer _container; + [Fact] + public void CtorFunctionImportSegmentTemplate_ThrowsArgumentNull_FunctionImport() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new FunctionImportSegmentTemplate(functionImport: null, null), "functionImport"); + } - public FunctionImportSegmentTemplateTests() - { - _function = new EdmFunction("NS", "MyFunctionImport", IntPrimitive, false, null, false); - _function.AddParameter("name", StringPrimitive); - _function.AddParameter("title", StringPrimitive); - _container = new EdmEntityContainer("NS", "Default"); - _functionImport = new EdmFunctionImport(_container, "MyFunctionImport", _function); - } - - [Fact] - public void CtorFunctionImportSegmentTemplate_ThrowsArgumentNull_FunctionImport() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new FunctionImportSegmentTemplate(functionImport: null, null), "functionImport"); - } + [Fact] + public void CtorFunctionImportSegmentTemplate_ThrowsArgumentNull_Parameters() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new FunctionImportSegmentTemplate(parameters: null, null, null), "parameters"); + } - [Fact] - public void CtorFunctionImportSegmentTemplate_ThrowsArgumentNull_Parameters() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new FunctionImportSegmentTemplate(parameters: null, null, null), "parameters"); - } + [Fact] + public void CtorFunctionImportSegmentTemplate_ThrowsArgumentNull_FunctionImport_InParametersCtor() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new FunctionImportSegmentTemplate(new Dictionary(), null, null), "functionImport"); + } - [Fact] - public void CtorFunctionImportSegmentTemplate_ThrowsArgumentNull_FunctionImport_InParametersCtor() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new FunctionImportSegmentTemplate(new Dictionary(), null, null), "functionImport"); - } + [Fact] + public void CtorFunctionImportSegmentTemplate_ThrowsArgumentNull_Segment() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new FunctionImportSegmentTemplate(segment: null), "segment"); + } - [Fact] - public void CtorFunctionImportSegmentTemplate_ThrowsArgumentNull_Segment() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new FunctionImportSegmentTemplate(segment: null), "segment"); - } + [Fact] + public void CtorFunctionImportSegmentTemplate_ThrowsException_NonFunctionImport() + { + // Arrange + EdmAction action = new EdmAction("NS", "MyAction", null); + Mock import = new Mock(); + import.Setup(i => i.Name).Returns("any"); + import.Setup(i => i.ContainerElementKind).Returns(EdmContainerElementKind.ActionImport); + import.Setup(i => i.Operation).Returns(action); + OperationImportSegment operationImportSegment = new OperationImportSegment(import.Object, null); + + // Act + Action test = () => new FunctionImportSegmentTemplate(operationImportSegment); + + // Assert + ExceptionAssert.Throws(test, "The input segment should be 'FunctionImport' in 'FunctionImportSegmentTemplate'."); + } - [Fact] - public void CtorFunctionImportSegmentTemplate_ThrowsException_NonFunctionImport() - { - // Arrange - EdmAction action = new EdmAction("NS", "MyAction", null); - Mock import = new Mock(); - import.Setup(i => i.Name).Returns("any"); - import.Setup(i => i.ContainerElementKind).Returns(EdmContainerElementKind.ActionImport); - import.Setup(i => i.Operation).Returns(action); - OperationImportSegment operationImportSegment = new OperationImportSegment(import.Object, null); - - // Act - Action test = () => new FunctionImportSegmentTemplate(operationImportSegment); - - // Assert - ExceptionAssert.Throws(test, "The input segment should be 'FunctionImport' in 'FunctionImportSegmentTemplate'."); - } - - [Fact] - public void CtorFunctionImportSegmentTemplate_SetsProperties() - { - // Arrange & Act - FunctionImportSegmentTemplate functionImportSegment = new FunctionImportSegmentTemplate(_functionImport, null); - - // Assert - Assert.Same(_functionImport, functionImportSegment.FunctionImport); - Assert.Null(functionImportSegment.NavigationSource); - Assert.NotNull(functionImportSegment.ParameterMappings); - Assert.Collection(functionImportSegment.ParameterMappings, - e => - { - Assert.Equal("name", e.Key); - Assert.Equal("name", e.Value); - }, - e => - { - Assert.Equal("title", e.Key); - Assert.Equal("title", e.Value); - }); - - // Arrange & Act - OperationImportSegment importSegment = new OperationImportSegment(_functionImport, null); - functionImportSegment = new FunctionImportSegmentTemplate(importSegment); - Assert.Empty(functionImportSegment.ParameterMappings); - } - - [Fact] - public void GetTemplatesFunctionImportSegmentTemplate_ReturnsTemplates() - { - // Arrange - FunctionImportSegmentTemplate functionImportSegment = new FunctionImportSegmentTemplate(_functionImport, null); - - // Act & Assert - IEnumerable templates = functionImportSegment.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/MyFunctionImport(name={name},title={title})", template); - Assert.Null(functionImportSegment.NavigationSource); - } - - [Fact] - public void GetTemplatesFunctionImportSegmentTemplate_ReturnsTemplates_ForEmptyParameter() - { - // Arrange - EdmFunction function = new EdmFunction("NS", "MyFunctionImport", IntPrimitive, false, null, false); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - EdmFunctionImport functionImport = new EdmFunctionImport(container, "MyFunctionImport", function); - FunctionImportSegmentTemplate functionImportSegment = new FunctionImportSegmentTemplate(functionImport, null); - - // Act & Assert - IEnumerable templates = functionImportSegment.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/MyFunctionImport()", template); - - // Act & Assert - templates = functionImportSegment.GetTemplates(new ODataRouteOptions + [Fact] + public void CtorFunctionImportSegmentTemplate_SetsProperties() + { + // Arrange & Act + FunctionImportSegmentTemplate functionImportSegment = new FunctionImportSegmentTemplate(_functionImport, null); + + // Assert + Assert.Same(_functionImport, functionImportSegment.FunctionImport); + Assert.Null(functionImportSegment.NavigationSource); + Assert.NotNull(functionImportSegment.ParameterMappings); + Assert.Collection(functionImportSegment.ParameterMappings, + e => + { + Assert.Equal("name", e.Key); + Assert.Equal("name", e.Value); + }, + e => { - EnableNonParenthesisForEmptyParameterFunction = true + Assert.Equal("title", e.Key); + Assert.Equal("title", e.Value); }); - template = Assert.Single(templates); - Assert.Equal("/MyFunctionImport", template); - } - [Fact] - public void TryTranslateActionImportSegmentTemplate_ThrowsArgumentNull_Context() + // Arrange & Act + OperationImportSegment importSegment = new OperationImportSegment(_functionImport, null); + functionImportSegment = new FunctionImportSegmentTemplate(importSegment); + Assert.Empty(functionImportSegment.ParameterMappings); + } + + [Fact] + public void GetTemplatesFunctionImportSegmentTemplate_ReturnsTemplates() + { + // Arrange + FunctionImportSegmentTemplate functionImportSegment = new FunctionImportSegmentTemplate(_functionImport, null); + + // Act & Assert + IEnumerable templates = functionImportSegment.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/MyFunctionImport(name={name},title={title})", template); + Assert.Null(functionImportSegment.NavigationSource); + } + + [Fact] + public void GetTemplatesFunctionImportSegmentTemplate_ReturnsTemplates_ForEmptyParameter() + { + // Arrange + EdmFunction function = new EdmFunction("NS", "MyFunctionImport", IntPrimitive, false, null, false); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + EdmFunctionImport functionImport = new EdmFunctionImport(container, "MyFunctionImport", function); + FunctionImportSegmentTemplate functionImportSegment = new FunctionImportSegmentTemplate(functionImport, null); + + // Act & Assert + IEnumerable templates = functionImportSegment.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/MyFunctionImport()", template); + + // Act & Assert + templates = functionImportSegment.GetTemplates(new ODataRouteOptions { - // Arrange - FunctionImportSegmentTemplate functionImportSegment = new FunctionImportSegmentTemplate(_functionImport, null); + EnableNonParenthesisForEmptyParameterFunction = true + }); + template = Assert.Single(templates); + Assert.Equal("/MyFunctionImport", template); + } + + [Fact] + public void TryTranslateActionImportSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + FunctionImportSegmentTemplate functionImportSegment = new FunctionImportSegmentTemplate(_functionImport, null); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => functionImportSegment.TryTranslate(null), "context"); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => functionImportSegment.TryTranslate(null), "context"); - } + [Fact] + public void TryTranslateFunctionImportSegmentTemplate_ReturnsODataFunctionImportSegment() + { + // Arrange + EdmFunction function = new EdmFunction("NS", "MyFunctionImport", IntPrimitive, false, null, false); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + EdmFunctionImport functionImport = new EdmFunctionImport(container, "MyFunctionImport", function); - [Fact] - public void TryTranslateFunctionImportSegmentTemplate_ReturnsODataFunctionImportSegment() + FunctionImportSegmentTemplate template = new FunctionImportSegmentTemplate(functionImport, null); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - EdmFunction function = new EdmFunction("NS", "MyFunctionImport", IntPrimitive, false, null, false); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - EdmFunctionImport functionImport = new EdmFunctionImport(container, "MyFunctionImport", function); + RouteValues = new RouteValueDictionary() + }; - FunctionImportSegmentTemplate template = new FunctionImportSegmentTemplate(functionImport, null); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = new RouteValueDictionary() - }; + // Act + bool ok = template.TryTranslate(context); - // Act - bool ok = template.TryTranslate(context); + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + OperationImportSegment functionImportSegment = Assert.IsType(actual); + Assert.Same(function, functionImportSegment.OperationImports.First().Operation); + } - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - OperationImportSegment functionImportSegment = Assert.IsType(actual); - Assert.Same(function, functionImportSegment.OperationImports.First().Operation); - } + [Fact] + public void TryTranslateFunctionImportSegmentTemplate_ReturnsODataFunctionImportSegment_WithOptionalParameter() + { + // Arrange + _function.AddOptionalParameter("min", IntPrimitive); + _function.AddOptionalParameter("max", IntPrimitive); + EdmModel model = new EdmModel(); + model.AddElement(_function); + model.AddElement(_container); + + IDictionary parameters = new Dictionary + { + { "name", "{nameTemp}" }, + { "title", "{titleTemp}" }, + { "min", "{minTemp}" }, + }; + FunctionImportSegmentTemplate template = new FunctionImportSegmentTemplate(parameters, _functionImport, null); + + RouteValueDictionary routeValues = new RouteValueDictionary(new { nameTemp = "'pt'", titleTemp = "'abc'", minTemp = "42" }); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + { + RouteValues = routeValues, + Model = model + }; + + // Act + bool ok = template.TryTranslate(context); + + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + OperationImportSegment functionImportSegment = Assert.IsType(actual); + Assert.Same(_function, functionImportSegment.OperationImports.First().Operation); + Assert.Equal(3, functionImportSegment.Parameters.Count()); + Assert.Equal(new[] { "name", "title", "min" }, functionImportSegment.Parameters.Select(p => p.Name)); + + Assert.Equal("pt", context.UpdatedValues["nameTemp"]); + Assert.Equal("abc", context.UpdatedValues["titleTemp"]); + Assert.Equal(42, context.UpdatedValues["minTemp"]); + } - [Fact] - public void TryTranslateFunctionImportSegmentTemplate_ReturnsODataFunctionImportSegment_WithOptionalParameter() + [Fact] + public void TryTranslateFunctionImportSegmentTemplate_ReturnsFalse_WithOptionalParameterMisMatch() + { + // Arrange + _function.AddOptionalParameter("min", IntPrimitive); + _function.AddOptionalParameter("max", IntPrimitive); + EdmModel model = new EdmModel(); + model.AddElement(_function); + model.AddElement(_container); + + IDictionary parameters = new Dictionary { - // Arrange - _function.AddOptionalParameter("min", IntPrimitive); - _function.AddOptionalParameter("max", IntPrimitive); - EdmModel model = new EdmModel(); - model.AddElement(_function); - model.AddElement(_container); - - IDictionary parameters = new Dictionary - { - { "name", "{nameTemp}" }, - { "title", "{titleTemp}" }, - { "min", "{minTemp}" }, - }; - FunctionImportSegmentTemplate template = new FunctionImportSegmentTemplate(parameters, _functionImport, null); - - RouteValueDictionary routeValues = new RouteValueDictionary(new { nameTemp = "'pt'", titleTemp = "'abc'", minTemp = "42" }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValues, - Model = model - }; - - // Act - bool ok = template.TryTranslate(context); - - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - OperationImportSegment functionImportSegment = Assert.IsType(actual); - Assert.Same(_function, functionImportSegment.OperationImports.First().Operation); - Assert.Equal(3, functionImportSegment.Parameters.Count()); - Assert.Equal(new[] { "name", "title", "min" }, functionImportSegment.Parameters.Select(p => p.Name)); - - Assert.Equal("pt", context.UpdatedValues["nameTemp"]); - Assert.Equal("abc", context.UpdatedValues["titleTemp"]); - Assert.Equal(42, context.UpdatedValues["minTemp"]); - } - - [Fact] - public void TryTranslateFunctionImportSegmentTemplate_ReturnsFalse_WithOptionalParameterMisMatch() + { "name", "{nameTemp}" }, + { "title", "{titleTemp}" }, + { "min", "{minTemp}" }, + }; + FunctionImportSegmentTemplate template = new FunctionImportSegmentTemplate(parameters, _functionImport, null); + + RouteValueDictionary routeValues = new RouteValueDictionary(new { name = "'pt'", title = "'abc'", min = "42,max=5" }); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - _function.AddOptionalParameter("min", IntPrimitive); - _function.AddOptionalParameter("max", IntPrimitive); - EdmModel model = new EdmModel(); - model.AddElement(_function); - model.AddElement(_container); - - IDictionary parameters = new Dictionary - { - { "name", "{nameTemp}" }, - { "title", "{titleTemp}" }, - { "min", "{minTemp}" }, - }; - FunctionImportSegmentTemplate template = new FunctionImportSegmentTemplate(parameters, _functionImport, null); - - RouteValueDictionary routeValues = new RouteValueDictionary(new { name = "'pt'", title = "'abc'", min = "42,max=5" }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValues, - Model = model - }; + RouteValues = routeValues, + Model = model + }; - // Act - bool ok = template.TryTranslate(context); + // Act + bool ok = template.TryTranslate(context); - // Assert - Assert.False(ok); - Assert.Empty(context.Segments); - } + // Assert + Assert.False(ok); + Assert.Empty(context.Segments); + } - [Fact] - public void TryTranslateFunctionImportSegmentTemplate_ReturnsFalse_WithMisMatchParameter() - { - // Arrange - EdmModel model = new EdmModel(); - model.AddElement(_function); - model.AddElement(_container); + [Fact] + public void TryTranslateFunctionImportSegmentTemplate_ReturnsFalse_WithMisMatchParameter() + { + // Arrange + EdmModel model = new EdmModel(); + model.AddElement(_function); + model.AddElement(_container); - IDictionary parameters = new Dictionary - { - { "name", "{nameTemp}" }, - { "title", "{titleTemp}" }, - }; - FunctionImportSegmentTemplate template = new FunctionImportSegmentTemplate(parameters, _functionImport, null); + IDictionary parameters = new Dictionary + { + { "name", "{nameTemp}" }, + { "title", "{titleTemp}" }, + }; + FunctionImportSegmentTemplate template = new FunctionImportSegmentTemplate(parameters, _functionImport, null); - RouteValueDictionary routeValues = new RouteValueDictionary(new { name = "'pt'" }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValues, - Model = model - }; + RouteValueDictionary routeValues = new RouteValueDictionary(new { name = "'pt'" }); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + { + RouteValues = routeValues, + Model = model + }; - // Act - bool ok = template.TryTranslate(context); + // Act + bool ok = template.TryTranslate(context); - // Assert - Assert.False(ok); - Assert.Empty(context.Segments); - } + // Assert + Assert.False(ok); + Assert.Empty(context.Segments); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/FunctionSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/FunctionSegmentTemplateTests.cs index a07509d3b..3bae49fe7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/FunctionSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/FunctionSegmentTemplateTests.cs @@ -21,424 +21,423 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template -{ - public class FunctionSegmentTemplateTests - { - private static IEdmPrimitiveTypeReference IntType = EdmCoreModel.Instance.GetInt32(false); - private static IEdmPrimitiveTypeReference StrType = EdmCoreModel.Instance.GetString(false); - private EdmFunction _edmFunction; +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; - public FunctionSegmentTemplateTests() - { - _edmFunction = new EdmFunction("NS", "MyFunction", IntType, true, null, false); - _edmFunction.AddParameter("bindingParameter", IntType); - _edmFunction.AddParameter("name", StrType); - _edmFunction.AddParameter("title", StrType); - } +public class FunctionSegmentTemplateTests +{ + private static IEdmPrimitiveTypeReference IntType = EdmCoreModel.Instance.GetInt32(false); + private static IEdmPrimitiveTypeReference StrType = EdmCoreModel.Instance.GetString(false); + private EdmFunction _edmFunction; - [Fact] - public void CtorFunctionSegmentTemplate_ThrowsArgumentNull_Function() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new FunctionSegmentTemplate(function: null, null), "function"); - } + public FunctionSegmentTemplateTests() + { + _edmFunction = new EdmFunction("NS", "MyFunction", IntType, true, null, false); + _edmFunction.AddParameter("bindingParameter", IntType); + _edmFunction.AddParameter("name", StrType); + _edmFunction.AddParameter("title", StrType); + } - [Fact] - public void CtorFunctionSegmentTemplate_ThrowsArgumentNull_Parameters() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new FunctionSegmentTemplate(null, null, navigationSource: null), "parameters"); - } + [Fact] + public void CtorFunctionSegmentTemplate_ThrowsArgumentNull_Function() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new FunctionSegmentTemplate(function: null, null), "function"); + } - [Fact] - public void CtorFunctionSegmentTemplate_ThrowsArgumentNull_Function_InParametersCtor() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new FunctionSegmentTemplate(new Dictionary(), null, navigationSource: null), "function"); - } + [Fact] + public void CtorFunctionSegmentTemplate_ThrowsArgumentNull_Parameters() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new FunctionSegmentTemplate(null, null, navigationSource: null), "parameters"); + } - [Fact] - public void CtorFunctionSegmentTemplate_ThrowsArgumentNull_OperationSegment() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new FunctionSegmentTemplate(operationSegment: null), "operationSegment"); - } + [Fact] + public void CtorFunctionSegmentTemplate_ThrowsArgumentNull_Function_InParametersCtor() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new FunctionSegmentTemplate(new Dictionary(), null, navigationSource: null), "function"); + } - [Fact] - public void CtorFunctionSegmentTemplate_ThrowsArgument_NonboundFunction() - { - // Arrange - IEdmPrimitiveTypeReference primitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); - EdmFunction function = new EdmFunction("NS", "MyFunction", primitive); - function.AddParameter("title", primitive); - - // Act & Assert - ExceptionAssert.Throws(() => new FunctionSegmentTemplate(function, null), - "The input operation 'MyFunction' is not a bound 'function'."); - } + [Fact] + public void CtorFunctionSegmentTemplate_ThrowsArgumentNull_OperationSegment() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new FunctionSegmentTemplate(operationSegment: null), "operationSegment"); + } - [Fact] - public void CtorFunctionSegmentTemplate_ThrowsODataException_NonFunction() - { - // Arrange - IEdmPrimitiveTypeReference intPrimitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); - EdmAction action = new EdmAction("NS", "MyAction", intPrimitive); - OperationSegment operationSegment = new OperationSegment(action, null); + [Fact] + public void CtorFunctionSegmentTemplate_ThrowsArgument_NonboundFunction() + { + // Arrange + IEdmPrimitiveTypeReference primitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); + EdmFunction function = new EdmFunction("NS", "MyFunction", primitive); + function.AddParameter("title", primitive); + + // Act & Assert + ExceptionAssert.Throws(() => new FunctionSegmentTemplate(function, null), + "The input operation 'MyFunction' is not a bound 'function'."); + } - // Act - Action test = () => new FunctionSegmentTemplate(operationSegment); + [Fact] + public void CtorFunctionSegmentTemplate_ThrowsODataException_NonFunction() + { + // Arrange + IEdmPrimitiveTypeReference intPrimitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); + EdmAction action = new EdmAction("NS", "MyAction", intPrimitive); + OperationSegment operationSegment = new OperationSegment(action, null); - // Assert - ExceptionAssert.Throws(test, "The input segment should be 'Function' in 'FunctionSegmentTemplate'."); - } + // Act + Action test = () => new FunctionSegmentTemplate(operationSegment); - [Fact] - public void CtorFunctionSegmentTemplate_SetsProperties() - { - // Arrange & Act - FunctionSegmentTemplate segment = new FunctionSegmentTemplate(_edmFunction, null); - - // Assert - Assert.Same(_edmFunction, segment.Function); - Assert.Null(segment.NavigationSource); - Assert.Collection(segment.ParameterMappings, - e => - { - Assert.Equal("name", e.Key); - Assert.Equal("name", e.Value); - }, - e => - { - Assert.Equal("title", e.Key); - Assert.Equal("title", e.Value); - }); - - // Arrange - OperationSegment operationSegment = new OperationSegment(_edmFunction, null); - - // Act - segment = new FunctionSegmentTemplate(operationSegment); - - // Assert - Assert.Same(_edmFunction, segment.Function); - Assert.Null(segment.NavigationSource); - } + // Assert + ExceptionAssert.Throws(test, "The input segment should be 'Function' in 'FunctionSegmentTemplate'."); + } - [Fact] - public void GetTemplatesFunctionSegmentTemplate_ReturnsTemplates() - { - // Assert - _edmFunction.AddOptionalParameter("option1", IntType); - _edmFunction.AddOptionalParameter("option2", IntType); - IDictionary parameters = new Dictionary + [Fact] + public void CtorFunctionSegmentTemplate_SetsProperties() + { + // Arrange & Act + FunctionSegmentTemplate segment = new FunctionSegmentTemplate(_edmFunction, null); + + // Assert + Assert.Same(_edmFunction, segment.Function); + Assert.Null(segment.NavigationSource); + Assert.Collection(segment.ParameterMappings, + e => { - { "name", "{nameTemp}" }, - { "title", "{titleTemp}" }, - { "option2", "{option2Temp}" }, - }; - - FunctionSegmentTemplate functionSegment = new FunctionSegmentTemplate(parameters, _edmFunction, null); - - // Act & Assert - IEnumerable templates = functionSegment.GetTemplates(); - Assert.Collection(templates, - e => - { - Assert.Equal("/NS.MyFunction(name={nameTemp},title={titleTemp},option2={option2Temp})", e); - }, - e => - { - Assert.Equal("/MyFunction(name={nameTemp},title={titleTemp},option2={option2Temp})", e); - }); - - // Act & Assert - templates = functionSegment.GetTemplates(new ODataRouteOptions + Assert.Equal("name", e.Key); + Assert.Equal("name", e.Value); + }, + e => { - EnableUnqualifiedOperationCall = false + Assert.Equal("title", e.Key); + Assert.Equal("title", e.Value); }); - string template = Assert.Single(templates); - Assert.Equal("/NS.MyFunction(name={nameTemp},title={titleTemp},option2={option2Temp})", template); + // Arrange + OperationSegment operationSegment = new OperationSegment(_edmFunction, null); - // Act & Assert - templates = functionSegment.GetTemplates(new ODataRouteOptions - { - EnableQualifiedOperationCall = false - }); + // Act + segment = new FunctionSegmentTemplate(operationSegment); - template = Assert.Single(templates); - Assert.Equal("/MyFunction(name={nameTemp},title={titleTemp},option2={option2Temp})", template); - } + // Assert + Assert.Same(_edmFunction, segment.Function); + Assert.Null(segment.NavigationSource); + } - [Fact] - public void GetTemplatesFunctionImportSegmentTemplate_ReturnsTemplates_ForEmptyParameter() + [Fact] + public void GetTemplatesFunctionSegmentTemplate_ReturnsTemplates() + { + // Assert + _edmFunction.AddOptionalParameter("option1", IntType); + _edmFunction.AddOptionalParameter("option2", IntType); + IDictionary parameters = new Dictionary { - // Arrange - EdmFunction function = new EdmFunction("NS", "MyFunction", IntType, true, null, false); - function.AddParameter("bindingParameter", IntType); - FunctionSegmentTemplate functionSegment = new FunctionSegmentTemplate(function, null); - - // Act & Assert - IEnumerable templates = functionSegment.GetTemplates(); - Assert.Collection(templates, - e => - { - Assert.Equal("/NS.MyFunction()", e); - }, - e => - { - Assert.Equal("/MyFunction()", e); - }); - - // Act & Assert - templates = functionSegment.GetTemplates(new ODataRouteOptions + { "name", "{nameTemp}" }, + { "title", "{titleTemp}" }, + { "option2", "{option2Temp}" }, + }; + + FunctionSegmentTemplate functionSegment = new FunctionSegmentTemplate(parameters, _edmFunction, null); + + // Act & Assert + IEnumerable templates = functionSegment.GetTemplates(); + Assert.Collection(templates, + e => { - EnableNonParenthesisForEmptyParameterFunction = true, - EnableQualifiedOperationCall = false + Assert.Equal("/NS.MyFunction(name={nameTemp},title={titleTemp},option2={option2Temp})", e); + }, + e => + { + Assert.Equal("/MyFunction(name={nameTemp},title={titleTemp},option2={option2Temp})", e); }); - string template = Assert.Single(templates); - Assert.Equal("/MyFunction", template); - } - [Fact] - public void TryTranslateFunctionSegmentTemplate_ThrowsArgumentNull_Context() + // Act & Assert + templates = functionSegment.GetTemplates(new ODataRouteOptions { - // Arrange - FunctionSegmentTemplate template = new FunctionSegmentTemplate(_edmFunction, null); + EnableUnqualifiedOperationCall = false + }); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => template.TryTranslate(null), "context"); - } + string template = Assert.Single(templates); + Assert.Equal("/NS.MyFunction(name={nameTemp},title={titleTemp},option2={option2Temp})", template); - [Fact] - public void TryTranslateFunctionImportSegmentTemplate_ReturnsTemplates_ForEmptyParameter() + // Act & Assert + templates = functionSegment.GetTemplates(new ODataRouteOptions { - // Arrange - EdmModel edmModel = new EdmModel(); - EdmFunction function = new EdmFunction("NS", "MyFunction", IntType, true, null, false); - function.AddParameter("bindingParameter", IntType); - edmModel.AddElement(function); - - FunctionSegmentTemplate template = new FunctionSegmentTemplate(function, null); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = new RouteValueDictionary(), - Model = edmModel - }; + EnableQualifiedOperationCall = false + }); - // Act - bool ok = template.TryTranslate(context); - - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - OperationSegment functionSegment = Assert.IsType(actual); - Assert.Same(function, functionSegment.Operations.First()); - Assert.Empty(functionSegment.Parameters); - } + template = Assert.Single(templates); + Assert.Equal("/MyFunction(name={nameTemp},title={titleTemp},option2={option2Temp})", template); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void TryTranslateFunctionSegmentTemplate_ReturnsODataFunctionSegment(bool hasRouteData) - { - // Arrange - var primitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); - EdmFunction function = new EdmFunction("NS", "MyFunction", primitive, true, null, false); - function.AddParameter("bindingParameter", primitive); - function.AddParameter("age", primitive); - function.AddParameter("price", primitive); - - FunctionSegmentTemplate template = new FunctionSegmentTemplate(function, null); - EdmModel edmModel = new EdmModel(); - edmModel.AddElement(function); - - RouteValueDictionary routeValue; - if (hasRouteData) + [Fact] + public void GetTemplatesFunctionImportSegmentTemplate_ReturnsTemplates_ForEmptyParameter() + { + // Arrange + EdmFunction function = new EdmFunction("NS", "MyFunction", IntType, true, null, false); + function.AddParameter("bindingParameter", IntType); + FunctionSegmentTemplate functionSegment = new FunctionSegmentTemplate(function, null); + + // Act & Assert + IEnumerable templates = functionSegment.GetTemplates(); + Assert.Collection(templates, + e => { - routeValue = new RouteValueDictionary(new { age = "34", price = "9" }); - } - else + Assert.Equal("/NS.MyFunction()", e); + }, + e => { - routeValue = new RouteValueDictionary(); // Empty - } + Assert.Equal("/MyFunction()", e); + }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValue, - Model = edmModel - }; + // Act & Assert + templates = functionSegment.GetTemplates(new ODataRouteOptions + { + EnableNonParenthesisForEmptyParameterFunction = true, + EnableQualifiedOperationCall = false + }); + string template = Assert.Single(templates); + Assert.Equal("/MyFunction", template); + } - // Act - bool ok = template.TryTranslate(context); + [Fact] + public void TryTranslateFunctionSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + FunctionSegmentTemplate template = new FunctionSegmentTemplate(_edmFunction, null); - // Assert - if (hasRouteData) - { - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - OperationSegment functionSegment = Assert.IsType(actual); - Assert.Same(function, functionSegment.Operations.First()); - Assert.Equal(2, functionSegment.Parameters.Count()); - Assert.Equal(new[] { "age", "price" }, functionSegment.Parameters.Select(p => p.Name)); - } - else - { - Assert.False(ok); - Assert.Empty(context.Segments); - } - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => template.TryTranslate(null), "context"); + } - [Fact] - public void TryTranslateFunctionSegmentTemplate_ReturnsFalse_WithOptionalParameterMisMatch() + [Fact] + public void TryTranslateFunctionImportSegmentTemplate_ReturnsTemplates_ForEmptyParameter() + { + // Arrange + EdmModel edmModel = new EdmModel(); + EdmFunction function = new EdmFunction("NS", "MyFunction", IntType, true, null, false); + function.AddParameter("bindingParameter", IntType); + edmModel.AddElement(function); + + FunctionSegmentTemplate template = new FunctionSegmentTemplate(function, null); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - EdmModel model = new EdmModel(); - _edmFunction.AddOptionalParameter("min", IntType); - _edmFunction.AddOptionalParameter("max", IntType); - model.AddElement(_edmFunction); - - IDictionary parameters = new Dictionary - { - { "name", "{name}" }, - { "title", "{title}" }, - { "min", "{min}" }, - }; - FunctionSegmentTemplate template = new FunctionSegmentTemplate(parameters, _edmFunction, null); - - RouteValueDictionary routeValues = new RouteValueDictionary(new { name = "'pt'", title = "'abc'", min = "42,max=5" }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValues, - Model = model - }; - - // Act - bool ok = template.TryTranslate(context); + RouteValues = new RouteValueDictionary(), + Model = edmModel + }; + + // Act + bool ok = template.TryTranslate(context); + + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + OperationSegment functionSegment = Assert.IsType(actual); + Assert.Same(function, functionSegment.Operations.First()); + Assert.Empty(functionSegment.Parameters); + } - // Assert - Assert.False(ok); - Assert.Empty(context.Segments); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TryTranslateFunctionSegmentTemplate_ReturnsODataFunctionSegment(bool hasRouteData) + { + // Arrange + var primitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, false); + EdmFunction function = new EdmFunction("NS", "MyFunction", primitive, true, null, false); + function.AddParameter("bindingParameter", primitive); + function.AddParameter("age", primitive); + function.AddParameter("price", primitive); + + FunctionSegmentTemplate template = new FunctionSegmentTemplate(function, null); + EdmModel edmModel = new EdmModel(); + edmModel.AddElement(function); + + RouteValueDictionary routeValue; + if (hasRouteData) + { + routeValue = new RouteValueDictionary(new { age = "34", price = "9" }); } - - [Fact] - public void TryTranslateFunctionSegmentTemplate_ReturnsODataFunctionSegment_WithOptionalParameters() + else { - // Arrange - EdmModel model = new EdmModel(); - _edmFunction.AddOptionalParameter("min", IntType); - _edmFunction.AddOptionalParameter("max", IntType); - model.AddElement(_edmFunction); + routeValue = new RouteValueDictionary(); // Empty + } - IDictionary parameters = new Dictionary - { - { "name", "{nameTemp}" }, - { "title", "{titleTemp}" }, - { "min", "{minTemp}" }, - }; - FunctionSegmentTemplate template = new FunctionSegmentTemplate(parameters, _edmFunction, null); - - RouteValueDictionary routeValues = new RouteValueDictionary(new { nameTemp = "'pt'", titleTemp = "'abc'", minTemp = "42" }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValues, - Model = model - }; + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + { + RouteValues = routeValue, + Model = edmModel + }; - // Act - bool ok = template.TryTranslate(context); + // Act + bool ok = template.TryTranslate(context); - // Assert + // Assert + if (hasRouteData) + { Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); OperationSegment functionSegment = Assert.IsType(actual); - Assert.Same(_edmFunction, functionSegment.Operations.First()); - Assert.Equal(3, functionSegment.Parameters.Count()); - Assert.Collection(functionSegment.Parameters, - e => - { - Assert.Equal("name", e.Name); - Assert.Equal("pt", e.Value); - }, - e => - { - Assert.Equal("title", e.Name); - Assert.Equal("abc", e.Value); - }, - e => - { - Assert.Equal("min", e.Name); - Assert.Equal(42, e.Value); - }); + Assert.Same(function, functionSegment.Operations.First()); + Assert.Equal(2, functionSegment.Parameters.Count()); + Assert.Equal(new[] { "age", "price" }, functionSegment.Parameters.Select(p => p.Name)); } + else + { + Assert.False(ok); + Assert.Empty(context.Segments); + } + } - [Fact] - public void TryTranslateFunctionSegmentTemplate_ReturnsODataFunctionSegment_UsingEscapedString() + [Fact] + public void TryTranslateFunctionSegmentTemplate_ReturnsFalse_WithOptionalParameterMisMatch() + { + // Arrange + EdmModel model = new EdmModel(); + _edmFunction.AddOptionalParameter("min", IntType); + _edmFunction.AddOptionalParameter("max", IntType); + model.AddElement(_edmFunction); + + IDictionary parameters = new Dictionary + { + { "name", "{name}" }, + { "title", "{title}" }, + { "min", "{min}" }, + }; + FunctionSegmentTemplate template = new FunctionSegmentTemplate(parameters, _edmFunction, null); + + RouteValueDictionary routeValues = new RouteValueDictionary(new { name = "'pt'", title = "'abc'", min = "42,max=5" }); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - var primitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, false); - EdmFunction function = new EdmFunction("NS", "MyFunction", primitive, true, null, false); - function.AddParameter("bindingParameter", primitive); - function.AddParameter("name", primitive); + RouteValues = routeValues, + Model = model + }; + + // Act + bool ok = template.TryTranslate(context); - FunctionSegmentTemplate template = new FunctionSegmentTemplate(function, null); - EdmModel edmModel = new EdmModel(); - edmModel.AddElement(function); + // Assert + Assert.False(ok); + Assert.Empty(context.Segments); + } - RouteValueDictionary routeValue = new RouteValueDictionary(new { name = "'Ji%2FChange%23%20T'" }); + [Fact] + public void TryTranslateFunctionSegmentTemplate_ReturnsODataFunctionSegment_WithOptionalParameters() + { + // Arrange + EdmModel model = new EdmModel(); + _edmFunction.AddOptionalParameter("min", IntType); + _edmFunction.AddOptionalParameter("max", IntType); + model.AddElement(_edmFunction); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + IDictionary parameters = new Dictionary + { + { "name", "{nameTemp}" }, + { "title", "{titleTemp}" }, + { "min", "{minTemp}" }, + }; + FunctionSegmentTemplate template = new FunctionSegmentTemplate(parameters, _edmFunction, null); + + RouteValueDictionary routeValues = new RouteValueDictionary(new { nameTemp = "'pt'", titleTemp = "'abc'", minTemp = "42" }); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + { + RouteValues = routeValues, + Model = model + }; + + // Act + bool ok = template.TryTranslate(context); + + // Assert + Assert.True(ok); + + ODataPathSegment actual = Assert.Single(context.Segments); + OperationSegment functionSegment = Assert.IsType(actual); + Assert.Same(_edmFunction, functionSegment.Operations.First()); + Assert.Equal(3, functionSegment.Parameters.Count()); + Assert.Collection(functionSegment.Parameters, + e => { - RouteValues = routeValue, - Model = edmModel - }; + Assert.Equal("name", e.Name); + Assert.Equal("pt", e.Value); + }, + e => + { + Assert.Equal("title", e.Name); + Assert.Equal("abc", e.Value); + }, + e => + { + Assert.Equal("min", e.Name); + Assert.Equal(42, e.Value); + }); + } - // Act - bool ok = template.TryTranslate(context); + [Fact] + public void TryTranslateFunctionSegmentTemplate_ReturnsODataFunctionSegment_UsingEscapedString() + { + // Arrange + var primitive = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, false); + EdmFunction function = new EdmFunction("NS", "MyFunction", primitive, true, null, false); + function.AddParameter("bindingParameter", primitive); + function.AddParameter("name", primitive); - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - OperationSegment functionSegment = Assert.IsType(actual); - Assert.Same(function, functionSegment.Operations.First()); - var parameter = Assert.Single(functionSegment.Parameters); - Assert.Equal("name", parameter.Name); - Assert.Equal("Ji/Change%23%20T", parameter.Value.ToString()); - } + FunctionSegmentTemplate template = new FunctionSegmentTemplate(function, null); + EdmModel edmModel = new EdmModel(); + edmModel.AddElement(function); - [Fact] - public void TryTranslateFunctionSegmentTemplate_ReturnsODataFunctionSegment_WithReturnedEntitySet() + RouteValueDictionary routeValue = new RouteValueDictionary(new { name = "'Ji%2FChange%23%20T'" }); + + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - var httpContext = new Mock().Object; - var endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test"); - var routeValues = new RouteValueDictionary(); - - var model = new EdmModel(); - var entityType = new EdmEntityType("NS", "Entity"); - entityType.AddKeys(entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - model.AddElement(entityType); - EdmFunction function = new EdmFunction("NS", "Function", new EdmEntityTypeReference(entityType, true), true, null, true); - model.AddElement(function); - var entityContainer = new EdmEntityContainer("NS", "Default"); - var entitySet = entityContainer.AddEntitySet("EntitySet", entityType); - model.AddElement(entityContainer); - model.SetAnnotationValue(function, new ReturnedEntitySetAnnotation("EntitySet")); - - var template = new FunctionSegmentTemplate(function, null); - var translateContext = new ODataTemplateTranslateContext(httpContext, endpoint, routeValues, model); - - // Act - bool ok = template.TryTranslate(translateContext); - - // Assert - Assert.True(ok); - var actual = Assert.Single(translateContext.Segments); - var functionSegment = Assert.IsType(actual); - Assert.Equal(functionSegment.EdmType, entityType); - Assert.Equal(functionSegment.EntitySet, entitySet); - } + RouteValues = routeValue, + Model = edmModel + }; + + // Act + bool ok = template.TryTranslate(context); + + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + OperationSegment functionSegment = Assert.IsType(actual); + Assert.Same(function, functionSegment.Operations.First()); + var parameter = Assert.Single(functionSegment.Parameters); + Assert.Equal("name", parameter.Name); + Assert.Equal("Ji/Change%23%20T", parameter.Value.ToString()); + } + + [Fact] + public void TryTranslateFunctionSegmentTemplate_ReturnsODataFunctionSegment_WithReturnedEntitySet() + { + // Arrange + var httpContext = new Mock().Object; + var endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test"); + var routeValues = new RouteValueDictionary(); + + var model = new EdmModel(); + var entityType = new EdmEntityType("NS", "Entity"); + entityType.AddKeys(entityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + model.AddElement(entityType); + EdmFunction function = new EdmFunction("NS", "Function", new EdmEntityTypeReference(entityType, true), true, null, true); + model.AddElement(function); + var entityContainer = new EdmEntityContainer("NS", "Default"); + var entitySet = entityContainer.AddEntitySet("EntitySet", entityType); + model.AddElement(entityContainer); + model.SetAnnotationValue(function, new ReturnedEntitySetAnnotation("EntitySet")); + + var template = new FunctionSegmentTemplate(function, null); + var translateContext = new ODataTemplateTranslateContext(httpContext, endpoint, routeValues, model); + + // Act + bool ok = template.TryTranslate(translateContext); + + // Assert + Assert.True(ok); + var actual = Assert.Single(translateContext.Segments); + var functionSegment = Assert.IsType(actual); + Assert.Equal(functionSegment.EdmType, entityType); + Assert.Equal(functionSegment.EntitySet, entitySet); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/KeySegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/KeySegmentTemplateTests.cs index ab6fb288c..e735443c5 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/KeySegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/KeySegmentTemplateTests.cs @@ -21,692 +21,691 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class KeySegmentTemplateTests { - public class KeySegmentTemplateTests + private static EdmEntityType _customerType; + private static EdmEntityContainer _container; + private static EdmEntitySet _customers; + + static KeySegmentTemplateTests() { - private static EdmEntityType _customerType; - private static EdmEntityContainer _container; - private static EdmEntitySet _customers; + // EntityType + _customerType = new EdmEntityType("NS", "Customer"); + EdmStructuralProperty idProperty = _customerType.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.Int32); + _customerType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + _customerType.AddKeys(idProperty); + + // EntitySet + _container = new EdmEntityContainer("NS", "default"); + _customers = _container.AddEntitySet("Customers", _customerType); + } - static KeySegmentTemplateTests() - { - // EntityType - _customerType = new EdmEntityType("NS", "Customer"); - EdmStructuralProperty idProperty = _customerType.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.Int32); - _customerType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - _customerType.AddKeys(idProperty); + [Fact] + public void CtorKeySegmentTemplate_ThrowsArgumentNull_Keys() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new KeySegmentTemplate(null, _customerType, _customers), "keys"); + } - // EntitySet - _container = new EdmEntityContainer("NS", "default"); - _customers = _container.AddEntitySet("Customers", _customerType); - } + [Fact] + public void CtorKeySegmentTemplate_ThrowsArgumentNull_EntityType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new KeySegmentTemplate(new Dictionary(), null, null), "entityType"); + } - [Fact] - public void CtorKeySegmentTemplate_ThrowsArgumentNull_Keys() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new KeySegmentTemplate(null, _customerType, _customers), "keys"); - } + [Fact] + public void CtorKeySegmentTemplate_ThrowsArgumentNull_Segment() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new KeySegmentTemplate(segment: null), "segment"); + } - [Fact] - public void CtorKeySegmentTemplate_ThrowsArgumentNull_EntityType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new KeySegmentTemplate(new Dictionary(), null, null), "entityType"); - } + [Fact] + public void CtorKeySegmentTemplate_ThrowsArgumentNull_Segment_AlternateKey() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new KeySegmentTemplate(segment: null, keyProperties: null), "segment"); + } - [Fact] - public void CtorKeySegmentTemplate_ThrowsArgumentNull_Segment() + [Fact] + public void CtorKeySegmentTemplate_ThrowsArgumentNull_KeyProperties_AlternateKey() + { + // Arrange + KeySegment segment = new KeySegment(new Dictionary(), _customerType, _customers); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new KeySegmentTemplate(segment, keyProperties: null), "keyProperties"); + } + + [Fact] + public void CtorKeySegmentTemplate_ThrowsODataException_MismatchKey() + { + // Arrange + Action test = () => new KeySegmentTemplate(new Dictionary(), _customerType, _customers); + + // Act & Assert + ExceptionAssert.Throws(test, "The input key count '0' does not match the number '1' of the key of entity type 'NS.Customer'."); + } + + [Fact] + public void CtorKeySegmentTemplate_SetsProperties() + { + // 1) Arrange & Act + IDictionary keys1 = new Dictionary { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new KeySegmentTemplate(segment: null), "segment"); - } + { "customerId", "{key}"} + }; + KeySegmentTemplate segment1 = new KeySegmentTemplate(keys1, _customerType, _customers); - [Fact] - public void CtorKeySegmentTemplate_ThrowsArgumentNull_Segment_AlternateKey() + // 2) Arrange & Act + IDictionary keys2 = new Dictionary { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new KeySegmentTemplate(segment: null, keyProperties: null), "segment"); - } + { "customerId", BuildExpression("{key}") } + }; + KeySegment keySegment = new KeySegment(keys2, _customerType, _customers); + KeySegmentTemplate segment2 = new KeySegmentTemplate(keySegment); - [Fact] - public void CtorKeySegmentTemplate_ThrowsArgumentNull_KeyProperties_AlternateKey() + // Assert + foreach (var segment in new[] { segment1, segment2 }) { - // Arrange - KeySegment segment = new KeySegment(new Dictionary(), _customerType, _customers); + KeyValuePair keyMapping = Assert.Single(segment.KeyMappings); + Assert.Equal("customerId", keyMapping.Key); + Assert.Equal("key", keyMapping.Value); + + KeyValuePair keyProperty = Assert.Single(segment.KeyProperties); + Assert.Equal("customerId", keyProperty.Key); + Assert.Equal("customerId", keyProperty.Value.Name); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new KeySegmentTemplate(segment, keyProperties: null), "keyProperties"); + Assert.Same(_customers, segment.NavigationSource); + Assert.Same(_customerType, segment.EntityType); + Assert.Equal(1, segment.Count); } + } - [Fact] - public void CtorKeySegmentTemplate_ThrowsODataException_MismatchKey() + [Fact] + public void CtorKeySegmentTemplate_SetsProperties_ForCompositeKeys() + { + // Arrange & Act + EdmEntityType customerType = new EdmEntityType("NS", "Customer"); + EdmStructuralProperty firstProperty = customerType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); + EdmStructuralProperty lastProperty = customerType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); + customerType.AddKeys(firstProperty, lastProperty); + IDictionary keys = new Dictionary { - // Arrange - Action test = () => new KeySegmentTemplate(new Dictionary(), _customerType, _customers); + { "firstName", "{key1}" }, + { "lastName", "{key2}" } + }; - // Act & Assert - ExceptionAssert.Throws(test, "The input key count '0' does not match the number '1' of the key of entity type 'NS.Customer'."); - } + KeySegment segment = new KeySegment(keys, customerType, null); + KeySegmentTemplate template = new KeySegmentTemplate(segment); - [Fact] - public void CtorKeySegmentTemplate_SetsProperties() - { - // 1) Arrange & Act - IDictionary keys1 = new Dictionary + // Assert + Assert.Collection(template.KeyMappings, + e => { - { "customerId", "{key}"} - }; - KeySegmentTemplate segment1 = new KeySegmentTemplate(keys1, _customerType, _customers); - - // 2) Arrange & Act - IDictionary keys2 = new Dictionary + Assert.Equal("firstName", e.Key); + Assert.Equal("key1", e.Value); + }, + e => { - { "customerId", BuildExpression("{key}") } - }; - KeySegment keySegment = new KeySegment(keys2, _customerType, _customers); - KeySegmentTemplate segment2 = new KeySegmentTemplate(keySegment); + Assert.Equal("lastName", e.Key); + Assert.Equal("key2", e.Value); + }); + } - // Assert - foreach (var segment in new[] { segment1, segment2 }) - { - KeyValuePair keyMapping = Assert.Single(segment.KeyMappings); - Assert.Equal("customerId", keyMapping.Key); - Assert.Equal("key", keyMapping.Value); - - KeyValuePair keyProperty = Assert.Single(segment.KeyProperties); - Assert.Equal("customerId", keyProperty.Key); - Assert.Equal("customerId", keyProperty.Value.Name); - - Assert.Same(_customers, segment.NavigationSource); - Assert.Same(_customerType, segment.EntityType); - Assert.Equal(1, segment.Count); - } - } + [Fact] + public void CtorKeySegmentTemplate_ForAlternateKey_SetsProperties() + { + // Arrange & Act + IDictionary keys = new Dictionary + { + { "name", BuildExpression("{nameValue}") } + }; + KeySegment keySegment = new KeySegment(keys, _customerType, _customers); - [Fact] - public void CtorKeySegmentTemplate_SetsProperties_ForCompositeKeys() + IEdmStructuralProperty nameProperty = _customerType.StructuralProperties().First(c => c.Name == "Name"); + IDictionary properties = new Dictionary { - // Arrange & Act - EdmEntityType customerType = new EdmEntityType("NS", "Customer"); - EdmStructuralProperty firstProperty = customerType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); - EdmStructuralProperty lastProperty = customerType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); - customerType.AddKeys(firstProperty, lastProperty); - IDictionary keys = new Dictionary - { - { "firstName", "{key1}" }, - { "lastName", "{key2}" } - }; - - KeySegment segment = new KeySegment(keys, customerType, null); - KeySegmentTemplate template = new KeySegmentTemplate(segment); - - // Assert - Assert.Collection(template.KeyMappings, - e => - { - Assert.Equal("firstName", e.Key); - Assert.Equal("key1", e.Value); - }, - e => - { - Assert.Equal("lastName", e.Key); - Assert.Equal("key2", e.Value); - }); - } + { "name", nameProperty} + }; + KeySegmentTemplate segmentTemplate = new KeySegmentTemplate(keySegment, properties); + + // Assert + KeyValuePair keyMapping = Assert.Single(segmentTemplate.KeyMappings); + Assert.Equal("name", keyMapping.Key); + Assert.Equal("nameValue", keyMapping.Value); + + KeyValuePair keyProperty = Assert.Single(segmentTemplate.KeyProperties); + Assert.Equal("name", keyProperty.Key); + Assert.Equal("Name", keyProperty.Value.Name); + + Assert.Same(_customers, segmentTemplate.NavigationSource); + Assert.Same(_customerType, segmentTemplate.EntityType); + Assert.Equal(1, segmentTemplate.Count); + } - [Fact] - public void CtorKeySegmentTemplate_ForAlternateKey_SetsProperties() + [Fact] + public void GetTemplatesKeySegmentTemplate_ReturnsTemplates() + { + // Arrange + IDictionary keys = new Dictionary { - // Arrange & Act - IDictionary keys = new Dictionary - { - { "name", BuildExpression("{nameValue}") } - }; - KeySegment keySegment = new KeySegment(keys, _customerType, _customers); + { "customerId", "{key}" } + }; - IEdmStructuralProperty nameProperty = _customerType.StructuralProperties().First(c => c.Name == "Name"); - IDictionary properties = new Dictionary - { - { "name", nameProperty} - }; - KeySegmentTemplate segmentTemplate = new KeySegmentTemplate(keySegment, properties); - - // Assert - KeyValuePair keyMapping = Assert.Single(segmentTemplate.KeyMappings); - Assert.Equal("name", keyMapping.Key); - Assert.Equal("nameValue", keyMapping.Value); - - KeyValuePair keyProperty = Assert.Single(segmentTemplate.KeyProperties); - Assert.Equal("name", keyProperty.Key); - Assert.Equal("Name", keyProperty.Value.Name); - - Assert.Same(_customers, segmentTemplate.NavigationSource); - Assert.Same(_customerType, segmentTemplate.EntityType); - Assert.Equal(1, segmentTemplate.Count); - } + KeySegmentTemplate keySegment = new KeySegmentTemplate(keys, _customerType, _customers); - [Fact] - public void GetTemplatesKeySegmentTemplate_ReturnsTemplates() - { - // Arrange - IDictionary keys = new Dictionary + // Act & Assert + IEnumerable templates = keySegment.GetTemplates(); + Assert.Collection(templates, + e => { - { "customerId", "{key}" } - }; - - KeySegmentTemplate keySegment = new KeySegmentTemplate(keys, _customerType, _customers); - - // Act & Assert - IEnumerable templates = keySegment.GetTemplates(); - Assert.Collection(templates, - e => - { - Assert.Equal("({key})", e); - }, - e => - { - Assert.Equal("/{key}", e); - }); - - // Act & Assert - templates = keySegment.GetTemplates(new ODataRouteOptions + Assert.Equal("({key})", e); + }, + e => { - EnableKeyAsSegment = false + Assert.Equal("/{key}", e); }); - string template = Assert.Single(templates); - Assert.Equal("({key})", template); + // Act & Assert + templates = keySegment.GetTemplates(new ODataRouteOptions + { + EnableKeyAsSegment = false + }); - // Act & Assert - templates = keySegment.GetTemplates(new ODataRouteOptions - { - EnableKeyInParenthesis = false - }); + string template = Assert.Single(templates); + Assert.Equal("({key})", template); - template = Assert.Single(templates); - Assert.Equal("/{key}", template); - } + // Act & Assert + templates = keySegment.GetTemplates(new ODataRouteOptions + { + EnableKeyInParenthesis = false + }); - [Fact] - public void GetTemplatesKeySegmentTemplate_ReturnsTemplates_ForCompositeKeys() + template = Assert.Single(templates); + Assert.Equal("/{key}", template); + } + + [Fact] + public void GetTemplatesKeySegmentTemplate_ReturnsTemplates_ForCompositeKeys() + { + // Arrange + EdmEntityType customerType = new EdmEntityType("NS", "Customer"); + EdmStructuralProperty firstProperty = customerType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); + EdmStructuralProperty lastProperty = customerType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); + customerType.AddKeys(firstProperty, lastProperty); + IDictionary keys = new Dictionary { - // Arrange - EdmEntityType customerType = new EdmEntityType("NS", "Customer"); - EdmStructuralProperty firstProperty = customerType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); - EdmStructuralProperty lastProperty = customerType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); - customerType.AddKeys(firstProperty, lastProperty); - IDictionary keys = new Dictionary - { - { "firstName", "{key1}" }, - { "lastName", "{key2}" } - }; - - KeySegmentTemplate keySegment = new KeySegmentTemplate(keys, customerType, null); - - // Act & Assert - IEnumerable templates = keySegment.GetTemplates(); - Assert.Collection(templates, - e => - { - Assert.Equal("(firstName={key1},lastName={key2})", e); - }, - e => - { - Assert.Equal("/firstName={key1},lastName={key2}", e); - }); - - // Act & Assert - templates = keySegment.GetTemplates(new ODataRouteOptions - { - EnableKeyAsSegment = false - }); + { "firstName", "{key1}" }, + { "lastName", "{key2}" } + }; - string template = Assert.Single(templates); - Assert.Equal("(firstName={key1},lastName={key2})", template); + KeySegmentTemplate keySegment = new KeySegmentTemplate(keys, customerType, null); - // Act & Assert - templates = keySegment.GetTemplates(new ODataRouteOptions + // Act & Assert + IEnumerable templates = keySegment.GetTemplates(); + Assert.Collection(templates, + e => + { + Assert.Equal("(firstName={key1},lastName={key2})", e); + }, + e => { - EnableKeyInParenthesis = false + Assert.Equal("/firstName={key1},lastName={key2}", e); }); - template = Assert.Single(templates); - Assert.Equal("/firstName={key1},lastName={key2}", template); - } - - [Fact] - public void TryTranslateKeySegmentTemplate_ThrowsArgumentNull_Context() + // Act & Assert + templates = keySegment.GetTemplates(new ODataRouteOptions { - // Arrange - IDictionary keys = new Dictionary - { - { "customerId", "{key}" } - }; - KeySegmentTemplate keySegment = new KeySegmentTemplate(keys, _customerType, _customers); + EnableKeyAsSegment = false + }); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => keySegment.TryTranslate(null), "context"); - } + string template = Assert.Single(templates); + Assert.Equal("(firstName={key1},lastName={key2})", template); - [Fact] - public void TryTranslateKeySegmentTemplate_ReturnsODataKeySegment() + // Act & Assert + templates = keySegment.GetTemplates(new ODataRouteOptions { - // Arrange - EdmModel model = new EdmModel(); - model.AddElement(_customerType); - model.AddElement(_container); + EnableKeyInParenthesis = false + }); - IDictionary keys = new Dictionary - { - { "customerId", "{key}" } - }; - - KeySegmentTemplate template = new KeySegmentTemplate(keys, _customerType, _customers); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = new RouteValueDictionary(new { key = "42" }), - Model = model - }; - - // Act - bool ok = template.TryTranslate(context); - - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - KeySegment keySegment = Assert.IsType(actual); - var actualKey = Assert.Single(keySegment.Keys); - Assert.Equal("customerId", actualKey.Key); - Assert.Equal(42, actualKey.Value); - } + template = Assert.Single(templates); + Assert.Equal("/firstName={key1},lastName={key2}", template); + } - [Fact] - public void TryTranslateKeySegmentTemplate_ReturnsODataKeySegment_ForCompositeKey() + [Fact] + public void TryTranslateKeySegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + IDictionary keys = new Dictionary { - // Arrange - EdmModel model = new EdmModel(); - EdmEntityType customerType = new EdmEntityType("NS", "Customer"); - EdmStructuralProperty firstProperty = customerType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); - EdmStructuralProperty lastProperty = customerType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); - customerType.AddKeys(firstProperty, lastProperty); - IDictionary keys = new Dictionary - { - { "firstName", "{key1}" }, - { "lastName", "{key2}" } - }; - model.AddElement(customerType); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - EdmEntitySet customers = container.AddEntitySet("Customers", customerType); - model.AddElement(container); - RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { key1 = "'Peter'", key2 = "'Sam'" }); - - KeySegmentTemplate template = new KeySegmentTemplate(keys, customerType, customers); - - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValueDictionary, - Model = model - }; - - // Act - bool ok = template.TryTranslate(context); - - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - KeySegment keySegment = Assert.IsType(actual); - Assert.Equal(2, keySegment.Keys.Count()); - Assert.Equal(new[] { "firstName", "lastName" }, keySegment.Keys.Select(c => c.Key)); - Assert.Equal(new[] { "Peter", "Sam" }, keySegment.Keys.Select(c => c.Value.ToString())); - } + { "customerId", "{key}" } + }; + KeySegmentTemplate keySegment = new KeySegmentTemplate(keys, _customerType, _customers); - [Fact] - public void TryTranslateKeySegmentTemplate_ReturnsODataKeySegment_ForKeyAsSegment() - { - // Arrange - EdmModel model = new EdmModel(); - EdmEntityType customerType = new EdmEntityType("NS", "Customer"); - EdmStructuralProperty idProperty = customerType.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.String); - customerType.AddKeys(idProperty); - model.AddElement(customerType); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - EdmEntitySet customers = container.AddEntitySet("Customers", customerType); - model.AddElement(container); - RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { key = "Peter" }); - IDictionary keys = new Dictionary - { - { "customerId", "{key}" } - }; - KeySegmentTemplate template = new KeySegmentTemplate(keys, customerType, customers); - - RouteEndpoint endpoint = new RouteEndpoint( - c => Task.CompletedTask, - RoutePatternFactory.Parse("odata/customers/{key}/Name"), - 0, - EndpointMetadataCollection.Empty, - "test"); - - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValueDictionary, - Model = model, - Endpoint = endpoint - }; - - // Act - bool ok = template.TryTranslate(context); - - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - KeySegment keySegment = Assert.IsType(actual); - KeyValuePair actualKeys = Assert.Single(keySegment.Keys); - Assert.Equal("customerId", actualKeys.Key); - Assert.Equal("Peter", actualKeys.Value); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => keySegment.TryTranslate(null), "context"); + } - [Fact] - public void TryTranslateKeySegmentTemplate_ThrowsODataException_ForInvalidKey() - { - // Arrange - EdmModel model = new EdmModel(); - EdmEntityType customerType = new EdmEntityType("NS", "Customer"); - EdmStructuralProperty idProperty = customerType.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.Int32); - customerType.AddKeys(idProperty); - model.AddElement(customerType); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - EdmEntitySet customers = container.AddEntitySet("Customers", customerType); - model.AddElement(container); - RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { key = "abc12" }); - IDictionary keys = new Dictionary - { - { "customerId", "{key}" } - }; - KeySegmentTemplate template = new KeySegmentTemplate(keys, customerType, customers); - - RouteEndpoint endpoint = new RouteEndpoint( - c => Task.CompletedTask, - RoutePatternFactory.Parse("odata/customers/{key}/Name"), - 0, - EndpointMetadataCollection.Empty, - "test"); - - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValueDictionary, - Model = model, - Endpoint = endpoint - }; + [Fact] + public void TryTranslateKeySegmentTemplate_ReturnsODataKeySegment() + { + // Arrange + EdmModel model = new EdmModel(); + model.AddElement(_customerType); + model.AddElement(_container); - // Act - Action test = () => template.TryTranslate(context); + IDictionary keys = new Dictionary + { + { "customerId", "{key}" } + }; - // Assert - ExceptionAssert.Throws(test, "The key value (abc12) from request is not valid. The key value should be format of type 'Edm.Int32'."); - } + KeySegmentTemplate template = new KeySegmentTemplate(keys, _customerType, _customers); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + { + RouteValues = new RouteValueDictionary(new { key = "42" }), + Model = model + }; + + // Act + bool ok = template.TryTranslate(context); + + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + KeySegment keySegment = Assert.IsType(actual); + var actualKey = Assert.Single(keySegment.Keys); + Assert.Equal("customerId", actualKey.Key); + Assert.Equal(42, actualKey.Value); + } - [Fact] - public void TryTranslateKeySegmentTemplate_WorksWithKeyParametersAlias() + [Fact] + public void TryTranslateKeySegmentTemplate_ReturnsODataKeySegment_ForCompositeKey() + { + // Arrange + EdmModel model = new EdmModel(); + EdmEntityType customerType = new EdmEntityType("NS", "Customer"); + EdmStructuralProperty firstProperty = customerType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); + EdmStructuralProperty lastProperty = customerType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); + customerType.AddKeys(firstProperty, lastProperty); + IDictionary keys = new Dictionary { - // Arrange - IDictionary keys = new Dictionary - { - { "customerId", "{key}" } - }; - KeySegmentTemplate template = new KeySegmentTemplate(keys, _customerType, _customers); + { "firstName", "{key1}" }, + { "lastName", "{key2}" } + }; + model.AddElement(customerType); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + EdmEntitySet customers = container.AddEntitySet("Customers", customerType); + model.AddElement(container); + RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { key1 = "'Peter'", key2 = "'Sam'" }); + + KeySegmentTemplate template = new KeySegmentTemplate(keys, customerType, customers); + + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + { + RouteValues = routeValueDictionary, + Model = model + }; + + // Act + bool ok = template.TryTranslate(context); + + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + KeySegment keySegment = Assert.IsType(actual); + Assert.Equal(2, keySegment.Keys.Count()); + Assert.Equal(new[] { "firstName", "lastName" }, keySegment.Keys.Select(c => c.Key)); + Assert.Equal(new[] { "Peter", "Sam" }, keySegment.Keys.Select(c => c.Value.ToString())); + } - RouteValueDictionary routeValues = new RouteValueDictionary() - { - { "key", "@p" } - }; - EdmModel model = new EdmModel(); - model.AddElement(_customerType); - HttpContext httpContext = new DefaultHttpContext(); - httpContext.Request.QueryString = new QueryString("?@p=42"); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - HttpContext = httpContext, - RouteValues = routeValues, - Model = model - }; - - // Act - bool ok = template.TryTranslate(context); - - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - KeySegment keySegment = Assert.IsType(actual); - KeyValuePair actualKeys = Assert.Single(keySegment.Keys); - Assert.Equal("customerId", actualKeys.Key); - Assert.Equal(42, actualKeys.Value); - } + [Fact] + public void TryTranslateKeySegmentTemplate_ReturnsODataKeySegment_ForKeyAsSegment() + { + // Arrange + EdmModel model = new EdmModel(); + EdmEntityType customerType = new EdmEntityType("NS", "Customer"); + EdmStructuralProperty idProperty = customerType.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.String); + customerType.AddKeys(idProperty); + model.AddElement(customerType); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + EdmEntitySet customers = container.AddEntitySet("Customers", customerType); + model.AddElement(container); + RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { key = "Peter" }); + IDictionary keys = new Dictionary + { + { "customerId", "{key}" } + }; + KeySegmentTemplate template = new KeySegmentTemplate(keys, customerType, customers); + + RouteEndpoint endpoint = new RouteEndpoint( + c => Task.CompletedTask, + RoutePatternFactory.Parse("odata/customers/{key}/Name"), + 0, + EndpointMetadataCollection.Empty, + "test"); + + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + { + RouteValues = routeValueDictionary, + Model = model, + Endpoint = endpoint + }; + + // Act + bool ok = template.TryTranslate(context); + + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + KeySegment keySegment = Assert.IsType(actual); + KeyValuePair actualKeys = Assert.Single(keySegment.Keys); + Assert.Equal("customerId", actualKeys.Key); + Assert.Equal("Peter", actualKeys.Value); + } - [Fact] - public void TryTranslateKeySegmentTemplate_WorksWithKeyValue_UsingEscapedString() - { - // Arrange - EdmModel model = new EdmModel(); - EdmEntityType customerType = new EdmEntityType("NS", "Customer"); - EdmStructuralProperty firstName = customerType.AddStructuralProperty("FirstName", EdmPrimitiveTypeKind.String); - EdmStructuralProperty lastName = customerType.AddStructuralProperty("LastName", EdmPrimitiveTypeKind.String); - customerType.AddKeys(firstName, lastName); - model.AddElement(customerType); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - EdmEntitySet customers = container.AddEntitySet("Customers", customerType); - model.AddElement(container); - RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { First = "'Zhang'", Last = "'Gan%2Fnng# T'" }); - IDictionary keys = new Dictionary - { - { "FirstName", "{first}" }, - { "LastName", "{last}" } - }; - KeySegmentTemplate template = new KeySegmentTemplate(keys, customerType, customers); + [Fact] + public void TryTranslateKeySegmentTemplate_ThrowsODataException_ForInvalidKey() + { + // Arrange + EdmModel model = new EdmModel(); + EdmEntityType customerType = new EdmEntityType("NS", "Customer"); + EdmStructuralProperty idProperty = customerType.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.Int32); + customerType.AddKeys(idProperty); + model.AddElement(customerType); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + EdmEntitySet customers = container.AddEntitySet("Customers", customerType); + model.AddElement(container); + RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { key = "abc12" }); + IDictionary keys = new Dictionary + { + { "customerId", "{key}" } + }; + KeySegmentTemplate template = new KeySegmentTemplate(keys, customerType, customers); + + RouteEndpoint endpoint = new RouteEndpoint( + c => Task.CompletedTask, + RoutePatternFactory.Parse("odata/customers/{key}/Name"), + 0, + EndpointMetadataCollection.Empty, + "test"); + + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + { + RouteValues = routeValueDictionary, + Model = model, + Endpoint = endpoint + }; - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValueDictionary, - Model = model - }; - - // Act - bool ok = template.TryTranslate(context); - - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - KeySegment keySegment = Assert.IsType(actual); - Assert.Collection(keySegment.Keys, - e => - { - Assert.Equal("FirstName", e.Key); - Assert.Equal("Zhang", e.Value); - }, - e => - { - Assert.Equal("LastName", e.Key); - Assert.Equal("Gan/nng# T", e.Value); - }); - } + // Act + Action test = () => template.TryTranslate(context); - [Fact] - public void CreateKeySegmentKeySegmentTemplate_ThrowsArgumentNull_EntityType() + // Assert + ExceptionAssert.Throws(test, "The key value (abc12) from request is not valid. The key value should be format of type 'Edm.Int32'."); + } + + [Fact] + public void TryTranslateKeySegmentTemplate_WorksWithKeyParametersAlias() + { + // Arrange + IDictionary keys = new Dictionary { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => KeySegmentTemplate.CreateKeySegment(null, null), "entityType"); - } + { "customerId", "{key}" } + }; + KeySegmentTemplate template = new KeySegmentTemplate(keys, _customerType, _customers); - [Fact] - public void CreateKeySegmentKeySegmentTemplate_ReturnsKeySegmentTemplate() + RouteValueDictionary routeValues = new RouteValueDictionary() + { + { "key", "@p" } + }; + EdmModel model = new EdmModel(); + model.AddElement(_customerType); + HttpContext httpContext = new DefaultHttpContext(); + httpContext.Request.QueryString = new QueryString("?@p=42"); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - KeySegmentTemplate template = KeySegmentTemplate.CreateKeySegment(_customerType, _customers, "id"); + HttpContext = httpContext, + RouteValues = routeValues, + Model = model + }; + + // Act + bool ok = template.TryTranslate(context); + + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + KeySegment keySegment = Assert.IsType(actual); + KeyValuePair actualKeys = Assert.Single(keySegment.Keys); + Assert.Equal("customerId", actualKeys.Key); + Assert.Equal(42, actualKeys.Value); + } - // Assert - Assert.NotNull(template); - Assert.Equal(1, template.Count); - KeyValuePair keyMapping = Assert.Single(template.KeyMappings); - Assert.Equal("customerId", keyMapping.Key); - Assert.Equal("id", keyMapping.Value); - } + [Fact] + public void TryTranslateKeySegmentTemplate_WorksWithKeyValue_UsingEscapedString() + { + // Arrange + EdmModel model = new EdmModel(); + EdmEntityType customerType = new EdmEntityType("NS", "Customer"); + EdmStructuralProperty firstName = customerType.AddStructuralProperty("FirstName", EdmPrimitiveTypeKind.String); + EdmStructuralProperty lastName = customerType.AddStructuralProperty("LastName", EdmPrimitiveTypeKind.String); + customerType.AddKeys(firstName, lastName); + model.AddElement(customerType); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + EdmEntitySet customers = container.AddEntitySet("Customers", customerType); + model.AddElement(container); + RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { First = "'Zhang'", Last = "'Gan%2Fnng# T'" }); + IDictionary keys = new Dictionary + { + { "FirstName", "{first}" }, + { "LastName", "{last}" } + }; + KeySegmentTemplate template = new KeySegmentTemplate(keys, customerType, customers); - [Fact] - public void CreateKeySegmentKeySegmentTemplate_ReturnsKeySegmentTemplate_CompositeKey() + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - EdmEntityType customerType = new EdmEntityType("NS", "Customer"); - EdmStructuralProperty firstProperty = customerType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); - EdmStructuralProperty lastProperty = customerType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); + RouteValues = routeValueDictionary, + Model = model + }; + + // Act + bool ok = template.TryTranslate(context); + + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + KeySegment keySegment = Assert.IsType(actual); + Assert.Collection(keySegment.Keys, + e => + { + Assert.Equal("FirstName", e.Key); + Assert.Equal("Zhang", e.Value); + }, + e => + { + Assert.Equal("LastName", e.Key); + Assert.Equal("Gan/nng# T", e.Value); + }); + } - customerType.AddKeys(firstProperty, lastProperty); + [Fact] + public void CreateKeySegmentKeySegmentTemplate_ThrowsArgumentNull_EntityType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => KeySegmentTemplate.CreateKeySegment(null, null), "entityType"); + } - // Act - KeySegmentTemplate template = KeySegmentTemplate.CreateKeySegment(customerType, null); + [Fact] + public void CreateKeySegmentKeySegmentTemplate_ReturnsKeySegmentTemplate() + { + // Arrange + KeySegmentTemplate template = KeySegmentTemplate.CreateKeySegment(_customerType, _customers, "id"); + + // Assert + Assert.NotNull(template); + Assert.Equal(1, template.Count); + KeyValuePair keyMapping = Assert.Single(template.KeyMappings); + Assert.Equal("customerId", keyMapping.Key); + Assert.Equal("id", keyMapping.Value); + } - // Assert - Assert.NotNull(template); - Assert.Equal(2, template.Count); - } + [Fact] + public void CreateKeySegmentKeySegmentTemplate_ReturnsKeySegmentTemplate_CompositeKey() + { + // Arrange + EdmEntityType customerType = new EdmEntityType("NS", "Customer"); + EdmStructuralProperty firstProperty = customerType.AddStructuralProperty("firstName", EdmPrimitiveTypeKind.String); + EdmStructuralProperty lastProperty = customerType.AddStructuralProperty("lastName", EdmPrimitiveTypeKind.String); - [Fact] - public void BuildKeyMappingsKeySegmentTemplate_ThrowsODataException_NoProperty() - { - // Arrange - IDictionary keys = new Dictionary - { - { "customerId", "{key}" } - }; + customerType.AddKeys(firstProperty, lastProperty); - IEdmStructuralProperty nameProperty = _customerType.StructuralProperties().First(c => c.Name == "Name"); - IDictionary properties = new Dictionary - { - { "name", nameProperty} - }; + // Act + KeySegmentTemplate template = KeySegmentTemplate.CreateKeySegment(customerType, null); - // Act - Action test = () => KeySegmentTemplate.BuildKeyMappings(keys, _customerType, properties); + // Assert + Assert.NotNull(template); + Assert.Equal(2, template.Count); + } - // Assert - ExceptionAssert.Throws(test, "Cannot find key 'customerId' in the 'NS.Customer' type."); - } + [Fact] + public void BuildKeyMappingsKeySegmentTemplate_ThrowsODataException_NoProperty() + { + // Arrange + IDictionary keys = new Dictionary + { + { "customerId", "{key}" } + }; - [Fact] - public void BuildKeyMappingsKeySegmentTemplate_ThrowsODataException_InvalidTemplateLiteral() + IEdmStructuralProperty nameProperty = _customerType.StructuralProperties().First(c => c.Name == "Name"); + IDictionary properties = new Dictionary { - // Arrange - IDictionary keys = new Dictionary - { - { "customerId", "key" } - }; + { "name", nameProperty} + }; - // Act - Action test = () => KeySegmentTemplate.BuildKeyMappings(keys, _customerType); + // Act + Action test = () => KeySegmentTemplate.BuildKeyMappings(keys, _customerType, properties); - // Assert - ExceptionAssert.Throws(test, "Key template value 'key' for key segment 'customerId' does not start with '{' or ends with '}'."); - } + // Assert + ExceptionAssert.Throws(test, "Cannot find key 'customerId' in the 'NS.Customer' type."); + } - [Fact] - public void BuildKeyMappingsKeySegmentTemplate_ThrowsODataException_NullTemplateLiteral() + [Fact] + public void BuildKeyMappingsKeySegmentTemplate_ThrowsODataException_InvalidTemplateLiteral() + { + // Arrange + IDictionary keys = new Dictionary { - // Arrange - IDictionary keys = new Dictionary - { - { "customerId", null } - }; + { "customerId", "key" } + }; - // Act - Action test = () => KeySegmentTemplate.BuildKeyMappings(keys, _customerType); + // Act + Action test = () => KeySegmentTemplate.BuildKeyMappings(keys, _customerType); - // Assert - ExceptionAssert.Throws(test, "Key template value '' for key segment 'customerId' does not start with '{' or ends with '}'."); - } + // Assert + ExceptionAssert.Throws(test, "Key template value 'key' for key segment 'customerId' does not start with '{' or ends with '}'."); + } - [Fact] - public void BuildKeyMappingsKeySegmentTemplate_ThrowsODataException_EmptyTemplateLiteral() + [Fact] + public void BuildKeyMappingsKeySegmentTemplate_ThrowsODataException_NullTemplateLiteral() + { + // Arrange + IDictionary keys = new Dictionary { - // Arrange - IDictionary keys = new Dictionary - { - { "customerId", "{}" } - }; + { "customerId", null } + }; - // Act - Action test = () => KeySegmentTemplate.BuildKeyMappings(keys, _customerType); + // Act + Action test = () => KeySegmentTemplate.BuildKeyMappings(keys, _customerType); - // Assert - ExceptionAssert.Throws(test, "Key template value '{}' for key segment 'customerId' is empty."); - } + // Assert + ExceptionAssert.Throws(test, "Key template value '' for key segment 'customerId' does not start with '{' or ends with '}'."); + } - [Fact] - public void BuildKeyMappingsKeySegmentTemplate_ReturnsKeyMapping_NormalStringTemplate() + [Fact] + public void BuildKeyMappingsKeySegmentTemplate_ThrowsODataException_EmptyTemplateLiteral() + { + // Arrange + IDictionary keys = new Dictionary { - // Arrange - EdmEntityType customerType = new EdmEntityType("NS", "Customer"); - EdmStructuralProperty idProperty = customerType.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.Int32); - customerType.AddKeys(idProperty); - IDictionary keys = new Dictionary - { - { "customerId", "{youkey}" } - }; + { "customerId", "{}" } + }; - // Act - IDictionary mapped = KeySegmentTemplate.BuildKeyMappings(keys, customerType); + // Act + Action test = () => KeySegmentTemplate.BuildKeyMappings(keys, _customerType); - // Assert - Assert.NotNull(mapped); - KeyValuePair actual = Assert.Single(mapped); - Assert.Equal("customerId", actual.Key); - Assert.Equal("youkey", actual.Value); - } + // Assert + ExceptionAssert.Throws(test, "Key template value '{}' for key segment 'customerId' is empty."); + } - [Fact] - public void BuildKeyMappingsKeySegmentTemplate_ReturnsKeyMapping_UriTemplateExpression() + [Fact] + public void BuildKeyMappingsKeySegmentTemplate_ReturnsKeyMapping_NormalStringTemplate() + { + // Arrange + EdmEntityType customerType = new EdmEntityType("NS", "Customer"); + EdmStructuralProperty idProperty = customerType.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.Int32); + customerType.AddKeys(idProperty); + IDictionary keys = new Dictionary { - // Arrange - EdmEntityType customerType = new EdmEntityType("NS", "Customer"); - EdmStructuralProperty idProperty = customerType.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.Int32); - customerType.AddKeys(idProperty); + { "customerId", "{youkey}" } + }; - UriTemplateExpression tempateExpression = BuildExpression("{yourId}", idProperty.Type); + // Act + IDictionary mapped = KeySegmentTemplate.BuildKeyMappings(keys, customerType); - IDictionary keys = new Dictionary - { - { "customerId", tempateExpression } - }; + // Assert + Assert.NotNull(mapped); + KeyValuePair actual = Assert.Single(mapped); + Assert.Equal("customerId", actual.Key); + Assert.Equal("youkey", actual.Value); + } - // Act - IDictionary mapped = KeySegmentTemplate.BuildKeyMappings(keys, customerType); + [Fact] + public void BuildKeyMappingsKeySegmentTemplate_ReturnsKeyMapping_UriTemplateExpression() + { + // Arrange + EdmEntityType customerType = new EdmEntityType("NS", "Customer"); + EdmStructuralProperty idProperty = customerType.AddStructuralProperty("customerId", EdmPrimitiveTypeKind.Int32); + customerType.AddKeys(idProperty); - // Assert - Assert.NotNull(mapped); - KeyValuePair actual = Assert.Single(mapped); - Assert.Equal("customerId", actual.Key); - Assert.Equal("yourId", actual.Value); - } + UriTemplateExpression tempateExpression = BuildExpression("{yourId}", idProperty.Type); - internal static UriTemplateExpression BuildExpression(string value, object expectType = null) + IDictionary keys = new Dictionary { - // The properties of UriTemplateExpression are internal set. - // We use the reflect to set the value. + { "customerId", tempateExpression } + }; - PropertyInfo literalText = typeof(UriTemplateExpression).GetProperty("LiteralText"); - Assert.NotNull(literalText); + // Act + IDictionary mapped = KeySegmentTemplate.BuildKeyMappings(keys, customerType); - PropertyInfo expectedType = typeof(UriTemplateExpression).GetProperty("ExpectedType"); - Assert.NotNull(expectedType); + // Assert + Assert.NotNull(mapped); + KeyValuePair actual = Assert.Single(mapped); + Assert.Equal("customerId", actual.Key); + Assert.Equal("yourId", actual.Value); + } - UriTemplateExpression tempateExpression = new UriTemplateExpression(); - literalText.SetValue(tempateExpression, value); + internal static UriTemplateExpression BuildExpression(string value, object expectType = null) + { + // The properties of UriTemplateExpression are internal set. + // We use the reflect to set the value. - if (expectType != null) - { - expectedType.SetValue(tempateExpression, expectType); - } + PropertyInfo literalText = typeof(UriTemplateExpression).GetProperty("LiteralText"); + Assert.NotNull(literalText); - return tempateExpression; + PropertyInfo expectedType = typeof(UriTemplateExpression).GetProperty("ExpectedType"); + Assert.NotNull(expectedType); + + UriTemplateExpression tempateExpression = new UriTemplateExpression(); + literalText.SetValue(tempateExpression, value); + + if (expectType != null) + { + expectedType.SetValue(tempateExpression, expectType); } + + return tempateExpression; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/MetadataSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/MetadataSegmentTemplateTests.cs index 7eda4c18c..86b3d96ce 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/MetadataSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/MetadataSegmentTemplateTests.cs @@ -11,39 +11,38 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class MetadataSegmentTemplateTests { - public class MetadataSegmentTemplateTests + [Fact] + public void GetTemplatesMetadataSegmentTemplate_ReturnsTemplates() { - [Fact] - public void GetTemplatesMetadataSegmentTemplate_ReturnsTemplates() - { - // Arrange & Act & Assert - IEnumerable templates = MetadataSegmentTemplate.Instance.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/$metadata", template); - } + // Arrange & Act & Assert + IEnumerable templates = MetadataSegmentTemplate.Instance.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/$metadata", template); + } - [Fact] - public void TryTranslateMetadataSegmentTemplate_ThrowsArgumentNull_Context() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => MetadataSegmentTemplate.Instance.TryTranslate(null), "context"); - } + [Fact] + public void TryTranslateMetadataSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => MetadataSegmentTemplate.Instance.TryTranslate(null), "context"); + } - [Fact] - public void TryTranslateMetadataTemplate_ReturnsODataMetadataSegment() - { - // Arrange - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + [Fact] + public void TryTranslateMetadataTemplate_ReturnsODataMetadataSegment() + { + // Arrange + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - // Act - bool ok = MetadataSegmentTemplate.Instance.TryTranslate(context); + // Act + bool ok = MetadataSegmentTemplate.Instance.TryTranslate(context); - // Assert - Assert.True(ok); - ODataPathSegment segment = Assert.Single(context.Segments); - Assert.Same(MetadataSegment.Instance, segment); - } + // Assert + Assert.True(ok); + ODataPathSegment segment = Assert.Single(context.Segments); + Assert.Same(MetadataSegment.Instance, segment); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/NavigationLinkSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/NavigationLinkSegmentTemplateTests.cs index e2465a69c..db2965153 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/NavigationLinkSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/NavigationLinkSegmentTemplateTests.cs @@ -13,135 +13,134 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class NavigationLinkSegmentTemplateTests { - public class NavigationLinkSegmentTemplateTests - { - private static IEdmEntityType _employee; - private static IEdmNavigationProperty _navigation; + private static IEdmEntityType _employee; + private static IEdmNavigationProperty _navigation; - static NavigationLinkSegmentTemplateTests() + static NavigationLinkSegmentTemplateTests() + { + EdmEntityType employee = new EdmEntityType("NS", "Employee"); + employee.AddKeys(employee.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + _navigation = employee.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - EdmEntityType employee = new EdmEntityType("NS", "Employee"); - employee.AddKeys(employee.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - _navigation = employee.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "DirectReports", - Target = employee, - TargetMultiplicity = EdmMultiplicity.Many - }); + Name = "DirectReports", + Target = employee, + TargetMultiplicity = EdmMultiplicity.Many + }); - _employee = employee; - } + _employee = employee; + } - [Fact] - public void CtorNavigationLinkSegmentTemplate_ThrowsArgumentNull_NavigationProperty() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new NavigationLinkSegmentTemplate(navigationProperty: null, navigationSource: null), "navigationProperty"); - } + [Fact] + public void CtorNavigationLinkSegmentTemplate_ThrowsArgumentNull_NavigationProperty() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new NavigationLinkSegmentTemplate(navigationProperty: null, navigationSource: null), "navigationProperty"); + } - [Fact] - public void CtorNavigationLinkSegmentTemplate_ThrowsArgumentNull_Segment() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new NavigationLinkSegmentTemplate(segment: null), "segment"); - } + [Fact] + public void CtorNavigationLinkSegmentTemplate_ThrowsArgumentNull_Segment() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new NavigationLinkSegmentTemplate(segment: null), "segment"); + } - [Fact] - public void CtorNavigationLinkSegmentTemplate_SetsProperties() - { - // Arrange & Act - NavigationLinkSegmentTemplate linkSegment = new NavigationLinkSegmentTemplate(_navigation, null); - - // Assert - Assert.Null(linkSegment.Key); - Assert.Same(_navigation, linkSegment.NavigationProperty); - Assert.Null(linkSegment.NavigationSource); - Assert.NotNull(linkSegment.Segment); - } - - [Fact] - public void GetTemplatesNavigationLinkSegmentTemplate_ReturnsTemplates() - { - // Arrange - NavigationLinkSegmentTemplate linkSegment = new NavigationLinkSegmentTemplate(_navigation, null); - - // Act & Assert - IEnumerable templates = linkSegment.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/DirectReports/$ref", template); - - // Act & Assert - linkSegment.Key = KeySegmentTemplate.CreateKeySegment(_employee, null); - templates = linkSegment.GetTemplates(); - Assert.Collection(templates, - e => Assert.Equal("/DirectReports({key})/$ref", e), - e => Assert.Equal("/DirectReports/{key}/$ref", e)); - } - - [Fact] - public void TryTranslateNavigationLinkSegmentTemplate_ThrowsArgumentNull_Context() - { - // Arrange - NavigationLinkSegmentTemplate linkSegment = new NavigationLinkSegmentTemplate(_navigation, null); + [Fact] + public void CtorNavigationLinkSegmentTemplate_SetsProperties() + { + // Arrange & Act + NavigationLinkSegmentTemplate linkSegment = new NavigationLinkSegmentTemplate(_navigation, null); + + // Assert + Assert.Null(linkSegment.Key); + Assert.Same(_navigation, linkSegment.NavigationProperty); + Assert.Null(linkSegment.NavigationSource); + Assert.NotNull(linkSegment.Segment); + } + + [Fact] + public void GetTemplatesNavigationLinkSegmentTemplate_ReturnsTemplates() + { + // Arrange + NavigationLinkSegmentTemplate linkSegment = new NavigationLinkSegmentTemplate(_navigation, null); + + // Act & Assert + IEnumerable templates = linkSegment.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/DirectReports/$ref", template); + + // Act & Assert + linkSegment.Key = KeySegmentTemplate.CreateKeySegment(_employee, null); + templates = linkSegment.GetTemplates(); + Assert.Collection(templates, + e => Assert.Equal("/DirectReports({key})/$ref", e), + e => Assert.Equal("/DirectReports/{key}/$ref", e)); + } + + [Fact] + public void TryTranslateNavigationLinkSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + NavigationLinkSegmentTemplate linkSegment = new NavigationLinkSegmentTemplate(_navigation, null); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => linkSegment.TryTranslate(null), "context"); + } - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => linkSegment.TryTranslate(null), "context"); - } + [Fact] + public void TryTranslateNavigationLinkSegmentTemplate_ReturnsODataNavigationSegment() + { + // Arrange + EdmModel model = new EdmModel(); + model.AddElement(_employee); - [Fact] - public void TryTranslateNavigationLinkSegmentTemplate_ReturnsODataNavigationSegment() + NavigationLinkSegmentTemplate linkSegment = new NavigationLinkSegmentTemplate(_navigation, null); + RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { id = "42" }); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - EdmModel model = new EdmModel(); - model.AddElement(_employee); + RouteValues = routeValueDictionary, + Model = model + }; + + // Without key segment + // Act & Assert + bool ok = linkSegment.TryTranslate(context); + + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + NavigationPropertyLinkSegment navLinkSegment = Assert.IsType(actual); + Assert.Same(_navigation, navLinkSegment.NavigationProperty); + Assert.Null(navLinkSegment.NavigationSource); + + // With Key segment + // Act & Assert + context = new ODataTemplateTranslateContext + { + RouteValues = routeValueDictionary, + Model = model + }; + + linkSegment.Key = KeySegmentTemplate.CreateKeySegment(_employee, null, "id"); + ok = linkSegment.TryTranslate(context); - NavigationLinkSegmentTemplate linkSegment = new NavigationLinkSegmentTemplate(_navigation, null); - RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { id = "42" }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + Assert.True(ok); + Assert.Collection(context.Segments, + e => { - RouteValues = routeValueDictionary, - Model = model - }; - - // Without key segment - // Act & Assert - bool ok = linkSegment.TryTranslate(context); - - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - NavigationPropertyLinkSegment navLinkSegment = Assert.IsType(actual); - Assert.Same(_navigation, navLinkSegment.NavigationProperty); - Assert.Null(navLinkSegment.NavigationSource); - - // With Key segment - // Act & Assert - context = new ODataTemplateTranslateContext + NavigationPropertyLinkSegment navLinkSegment = Assert.IsType(e); + Assert.Same(_navigation, navLinkSegment.NavigationProperty); + Assert.Null(navLinkSegment.NavigationSource); + }, + e => { - RouteValues = routeValueDictionary, - Model = model - }; - - linkSegment.Key = KeySegmentTemplate.CreateKeySegment(_employee, null, "id"); - ok = linkSegment.TryTranslate(context); - - Assert.True(ok); - Assert.Collection(context.Segments, - e => - { - NavigationPropertyLinkSegment navLinkSegment = Assert.IsType(e); - Assert.Same(_navigation, navLinkSegment.NavigationProperty); - Assert.Null(navLinkSegment.NavigationSource); - }, - e => - { - KeySegment keySegment = Assert.IsType(e); - KeyValuePair key = Assert.Single(keySegment.Keys); - Assert.Equal("Id", key.Key); - Assert.Equal(42, key.Value); - }); - } + KeySegment keySegment = Assert.IsType(e); + KeyValuePair key = Assert.Single(keySegment.Keys); + Assert.Equal("Id", key.Key); + Assert.Equal(42, key.Value); + }); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/NavigationLinkTemplateSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/NavigationLinkTemplateSegmentTemplateTests.cs index 8ee0961e2..ea6a8ae41 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/NavigationLinkTemplateSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/NavigationLinkTemplateSegmentTemplateTests.cs @@ -15,168 +15,167 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class NavigationLinkTemplateSegmentTemplateTests { - public class NavigationLinkTemplateSegmentTemplateTests + private static IEdmModel _model; + private static IEdmStructuredType _employeeType; + private static IEdmEntitySet _entitySet; + private static IEdmNavigationProperty _directReportsNav; + private static IEdmNavigationProperty _subordinatesNav; + + static NavigationLinkTemplateSegmentTemplateTests() { - private static IEdmModel _model; - private static IEdmStructuredType _employeeType; - private static IEdmEntitySet _entitySet; - private static IEdmNavigationProperty _directReportsNav; - private static IEdmNavigationProperty _subordinatesNav; + EdmModel model = new EdmModel(); + + // Employee type + EdmEntityType employee = new EdmEntityType("NS", "Employee"); + EdmStructuralProperty idProperty = employee.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32); + employee.AddKeys(idProperty); + model.AddElement(employee); + _employeeType = employee; + + // Entity container + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + model.AddElement(container); - static NavigationLinkTemplateSegmentTemplateTests() + // Navigation property + _directReportsNav = employee.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - EdmModel model = new EdmModel(); - - // Employee type - EdmEntityType employee = new EdmEntityType("NS", "Employee"); - EdmStructuralProperty idProperty = employee.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32); - employee.AddKeys(idProperty); - model.AddElement(employee); - _employeeType = employee; - - // Entity container - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - model.AddElement(container); - - // Navigation property - _directReportsNav = employee.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "DirectReports", - Target = employee, - TargetMultiplicity = EdmMultiplicity.Many - }); - EdmEntitySet employees = container.AddEntitySet("Employees", employee); - employees.AddNavigationTarget(_directReportsNav, employees); - - _subordinatesNav = employee.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "Subordinates", - Target = employee, - TargetMultiplicity = EdmMultiplicity.Many - }); - employees.AddNavigationTarget(_subordinatesNav, employees); - - _entitySet = employees; - _model = model; - } - - [Fact] - public void CtorNavigationRefSegmentTemplate_ThrowsArgumentNull_DeclaringType() + Name = "DirectReports", + Target = employee, + TargetMultiplicity = EdmMultiplicity.Many + }); + EdmEntitySet employees = container.AddEntitySet("Employees", employee); + employees.AddNavigationTarget(_directReportsNav, employees); + + _subordinatesNav = employee.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new NavigationLinkTemplateSegmentTemplate(declaringType: null, navigationSource: null), "declaringType"); - } + Name = "Subordinates", + Target = employee, + TargetMultiplicity = EdmMultiplicity.Many + }); + employees.AddNavigationTarget(_subordinatesNav, employees); + + _entitySet = employees; + _model = model; + } - [Fact] - public void CtorNavigationRefSegmentTemplate_ThrowsArgumentNull_NavigationSource() - { - // Arrange & Act & Assert - IEdmStructuredType structuredType = new Mock().Object; - ExceptionAssert.ThrowsArgumentNull(() => new NavigationLinkTemplateSegmentTemplate(structuredType, null), "navigationSource"); - } + [Fact] + public void CtorNavigationRefSegmentTemplate_ThrowsArgumentNull_DeclaringType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new NavigationLinkTemplateSegmentTemplate(declaringType: null, navigationSource: null), "declaringType"); + } - [Fact] - public void CtorNavigationRefSegmentTemplate_SetsProperties() - { - // Arrange & Act - IEdmStructuredType structuredType = new Mock().Object; - IEdmNavigationSource navigationSource = new Mock().Object; + [Fact] + public void CtorNavigationRefSegmentTemplate_ThrowsArgumentNull_NavigationSource() + { + // Arrange & Act & Assert + IEdmStructuredType structuredType = new Mock().Object; + ExceptionAssert.ThrowsArgumentNull(() => new NavigationLinkTemplateSegmentTemplate(structuredType, null), "navigationSource"); + } - NavigationLinkTemplateSegmentTemplate navigationRefSegment = new NavigationLinkTemplateSegmentTemplate(structuredType, navigationSource); + [Fact] + public void CtorNavigationRefSegmentTemplate_SetsProperties() + { + // Arrange & Act + IEdmStructuredType structuredType = new Mock().Object; + IEdmNavigationSource navigationSource = new Mock().Object; - // Assert - Assert.Same(structuredType, navigationRefSegment.DeclaringType); - Assert.Same(navigationSource, navigationRefSegment.NavigationSource); - } + NavigationLinkTemplateSegmentTemplate navigationRefSegment = new NavigationLinkTemplateSegmentTemplate(structuredType, navigationSource); - [Fact] - public void GetTemplatesNavigationRefSegmentTemplate_ReturnsTemplates() + // Assert + Assert.Same(structuredType, navigationRefSegment.DeclaringType); + Assert.Same(navigationSource, navigationRefSegment.NavigationSource); + } + + [Fact] + public void GetTemplatesNavigationRefSegmentTemplate_ReturnsTemplates() + { + // Arrange + NavigationLinkTemplateSegmentTemplate navigationSegment = new NavigationLinkTemplateSegmentTemplate(_employeeType, _entitySet); + + // Act & Assert + IEnumerable templates = navigationSegment.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/{navigationProperty}/$ref", template); + + // Arrange + navigationSegment = new NavigationLinkTemplateSegmentTemplate(_employeeType, _entitySet) { - // Arrange - NavigationLinkTemplateSegmentTemplate navigationSegment = new NavigationLinkTemplateSegmentTemplate(_employeeType, _entitySet); - - // Act & Assert - IEnumerable templates = navigationSegment.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/{navigationProperty}/$ref", template); - - // Arrange - navigationSegment = new NavigationLinkTemplateSegmentTemplate(_employeeType, _entitySet) - { - RelatedKey = "relatedId" - }; - - // Act & Assert - templates = navigationSegment.GetTemplates(); - Assert.Collection(templates, - e => Assert.Equal("/{navigationProperty}({relatedId})/$ref", e), - e => Assert.Equal("/{navigationProperty}/{relatedId}/$ref", e)); - } - - [Fact] - public void GetTemplatesNavigationRefSegmentTemplate_ReturnsTemplates_WithRouteOptions() + RelatedKey = "relatedId" + }; + + // Act & Assert + templates = navigationSegment.GetTemplates(); + Assert.Collection(templates, + e => Assert.Equal("/{navigationProperty}({relatedId})/$ref", e), + e => Assert.Equal("/{navigationProperty}/{relatedId}/$ref", e)); + } + + [Fact] + public void GetTemplatesNavigationRefSegmentTemplate_ReturnsTemplates_WithRouteOptions() + { + // Arrange + NavigationLinkTemplateSegmentTemplate navigationSegment = new NavigationLinkTemplateSegmentTemplate(_employeeType, _entitySet) + { + RelatedKey = "relatedId" + }; + + // Act & Assert + IEnumerable templates = navigationSegment.GetTemplates(new ODataRouteOptions { - // Arrange - NavigationLinkTemplateSegmentTemplate navigationSegment = new NavigationLinkTemplateSegmentTemplate(_employeeType, _entitySet) - { - RelatedKey = "relatedId" - }; - - // Act & Assert - IEnumerable templates = navigationSegment.GetTemplates(new ODataRouteOptions - { - EnableKeyInParenthesis = false - }); - string template = Assert.Single(templates); - Assert.Equal("/{navigationProperty}/{relatedId}/$ref", template); - - // Act & Assert - templates = navigationSegment.GetTemplates(new ODataRouteOptions - { - EnableKeyAsSegment = false - }); - template = Assert.Single(templates); - Assert.Equal("/{navigationProperty}({relatedId})/$ref", template); - } - - [Fact] - public void TryTranslateSingletonSegmentTemplate_ThrowsArgumentNull_Context() + EnableKeyInParenthesis = false + }); + string template = Assert.Single(templates); + Assert.Equal("/{navigationProperty}/{relatedId}/$ref", template); + + // Act & Assert + templates = navigationSegment.GetTemplates(new ODataRouteOptions { - // Arrange - IEdmStructuredType structuredType = new Mock().Object; - IEdmNavigationSource navigationSource = new Mock().Object; + EnableKeyAsSegment = false + }); + template = Assert.Single(templates); + Assert.Equal("/{navigationProperty}({relatedId})/$ref", template); + } + + [Fact] + public void TryTranslateSingletonSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + IEdmStructuredType structuredType = new Mock().Object; + IEdmNavigationSource navigationSource = new Mock().Object; - NavigationLinkTemplateSegmentTemplate navigationRefSegment = new NavigationLinkTemplateSegmentTemplate(structuredType, navigationSource); + NavigationLinkTemplateSegmentTemplate navigationRefSegment = new NavigationLinkTemplateSegmentTemplate(structuredType, navigationSource); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => navigationRefSegment.TryTranslate(null), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => navigationRefSegment.TryTranslate(null), "context"); + } + + [Fact] + public void TryTranslateNavigationSegmentTemplate_ReturnsODataNavigationLinkSegment() + { + // Arrange + NavigationLinkTemplateSegmentTemplate navigationSegment = new NavigationLinkTemplateSegmentTemplate(_employeeType, _entitySet); - [Fact] - public void TryTranslateNavigationSegmentTemplate_ReturnsODataNavigationLinkSegment() + RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { navigationProperty = "DirectReports" }); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - NavigationLinkTemplateSegmentTemplate navigationSegment = new NavigationLinkTemplateSegmentTemplate(_employeeType, _entitySet); - - RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { navigationProperty = "DirectReports" }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValueDictionary, - Model = _model - }; - - // Act - bool ok = navigationSegment.TryTranslate(context); - - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - NavigationPropertyLinkSegment navSegment = Assert.IsType(actual); - Assert.Same(_directReportsNav, navSegment.NavigationProperty); - Assert.Same(_entitySet, navSegment.NavigationSource); - } + RouteValues = routeValueDictionary, + Model = _model + }; + + // Act + bool ok = navigationSegment.TryTranslate(context); + + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + NavigationPropertyLinkSegment navSegment = Assert.IsType(actual); + Assert.Same(_directReportsNav, navSegment.NavigationProperty); + Assert.Same(_entitySet, navSegment.NavigationSource); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/NavigationSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/NavigationSegmentTemplateTests.cs index 619bb7ab7..2463a5b05 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/NavigationSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/NavigationSegmentTemplateTests.cs @@ -12,87 +12,86 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class NavigationSegmentTemplateTests { - public class NavigationSegmentTemplateTests + [Fact] + public void CtorNavigationSegmentTemplate_ThrowsArgumentNull_Navigation() { - [Fact] - public void CtorNavigationSegmentTemplate_ThrowsArgumentNull_Navigation() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new NavigationSegmentTemplate(navigationProperty: null, navigationSource: null), "navigationProperty"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new NavigationSegmentTemplate(navigationProperty: null, navigationSource: null), "navigationProperty"); + } - [Fact] - public void CtorNavigationSegmentTemplate_ThrowsArgumentNull_Segment() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new NavigationSegmentTemplate(segment: null), "segment"); - } + [Fact] + public void CtorNavigationSegmentTemplate_ThrowsArgumentNull_Segment() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new NavigationSegmentTemplate(segment: null), "segment"); + } - [Fact] - public void CtorNavigationSegmentTemplate_SetsProperties() - { - // Arrange & Act - NavigationSegmentTemplate navigationSegment = GetSegmentTemplate(out IEdmNavigationProperty navigation); + [Fact] + public void CtorNavigationSegmentTemplate_SetsProperties() + { + // Arrange & Act + NavigationSegmentTemplate navigationSegment = GetSegmentTemplate(out IEdmNavigationProperty navigation); - // Assert - Assert.NotNull(navigationSegment.Segment); - Assert.Same(navigation, navigationSegment.NavigationProperty); - } + // Assert + Assert.NotNull(navigationSegment.Segment); + Assert.Same(navigation, navigationSegment.NavigationProperty); + } - [Fact] - public void GetTemplatesNavigationSegmentTemplate_ReturnsTemplates() - { - // Arrange - NavigationSegmentTemplate navigationSegment = GetSegmentTemplate(out _); + [Fact] + public void GetTemplatesNavigationSegmentTemplate_ReturnsTemplates() + { + // Arrange + NavigationSegmentTemplate navigationSegment = GetSegmentTemplate(out _); - // Act & Assert - IEnumerable templates = navigationSegment.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/DirectReports", template); - } + // Act & Assert + IEnumerable templates = navigationSegment.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/DirectReports", template); + } - [Fact] - public void TryTranslateSingletonSegmentTemplate_ThrowsArgumentNull_Context() - { - // Arrange - NavigationSegmentTemplate navigationSegment = GetSegmentTemplate(out _); + [Fact] + public void TryTranslateSingletonSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + NavigationSegmentTemplate navigationSegment = GetSegmentTemplate(out _); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => navigationSegment.TryTranslate(null), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => navigationSegment.TryTranslate(null), "context"); + } - [Fact] - public void TryTranslateNavigationSegmentTemplate_ReturnsODataNavigationSegment() - { - // Arrange - NavigationSegmentTemplate navigationSegment = GetSegmentTemplate(out IEdmNavigationProperty navigation); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + [Fact] + public void TryTranslateNavigationSegmentTemplate_ReturnsODataNavigationSegment() + { + // Arrange + NavigationSegmentTemplate navigationSegment = GetSegmentTemplate(out IEdmNavigationProperty navigation); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - // Act - bool ok = navigationSegment.TryTranslate(context); + // Act + bool ok = navigationSegment.TryTranslate(context); - // Assert - Assert.True(ok); - ODataPathSegment actual = Assert.Single(context.Segments); - NavigationPropertySegment navSegment = Assert.IsType(actual); - Assert.Same(navigation, navSegment.NavigationProperty); - Assert.Null(navSegment.NavigationSource); - } + // Assert + Assert.True(ok); + ODataPathSegment actual = Assert.Single(context.Segments); + NavigationPropertySegment navSegment = Assert.IsType(actual); + Assert.Same(navigation, navSegment.NavigationProperty); + Assert.Null(navSegment.NavigationSource); + } - private static NavigationSegmentTemplate GetSegmentTemplate(out IEdmNavigationProperty navigation) + private static NavigationSegmentTemplate GetSegmentTemplate(out IEdmNavigationProperty navigation) + { + EdmEntityType employee = new EdmEntityType("NS", "Employee"); + navigation = employee.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - EdmEntityType employee = new EdmEntityType("NS", "Employee"); - navigation = employee.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "DirectReports", - Target = employee, - TargetMultiplicity = EdmMultiplicity.Many - }); + Name = "DirectReports", + Target = employee, + TargetMultiplicity = EdmMultiplicity.Many + }); - return new NavigationSegmentTemplate(navigation, null); - } + return new NavigationSegmentTemplate(navigation, null); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ODataPathTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ODataPathTemplateTests.cs index 88e0041eb..6a8ee6175 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ODataPathTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ODataPathTemplateTests.cs @@ -15,398 +15,397 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template -{ - public class ODataPathTemplateTests - { - private static IEdmTypeReference IntType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); - - [Fact] - public void CtorODataPathTemplate_SetsSegments_UsingEnumerable() - { - // Arrange - ODataSegmentTemplate template = new Mock().Object; - IEnumerable templates = new ODataSegmentTemplate[] - { - template - }; +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; - // Act - ODataPathTemplate path = new ODataPathTemplate(templates); - - // Assert - ODataSegmentTemplate actual = Assert.Single(path); - Assert.Same(template, actual); - } +public class ODataPathTemplateTests +{ + private static IEdmTypeReference IntType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: false); - [Fact] - public void CtorODataPathTemplate_SetsSegments_UsingList() + [Fact] + public void CtorODataPathTemplate_SetsSegments_UsingEnumerable() + { + // Arrange + ODataSegmentTemplate template = new Mock().Object; + IEnumerable templates = new ODataSegmentTemplate[] { - // Arrange - ODataSegmentTemplate template = new Mock().Object; - IList templates = new List - { - template - }; + template + }; - // Act - ODataPathTemplate path = new ODataPathTemplate(templates); + // Act + ODataPathTemplate path = new ODataPathTemplate(templates); - // Assert - ODataSegmentTemplate actual = Assert.Single(path); - Assert.Same(template, actual); - } + // Assert + ODataSegmentTemplate actual = Assert.Single(path); + Assert.Same(template, actual); + } - [Fact] - public void GetTemplatesReturnsCorrectWithEmptySegments() + [Fact] + public void CtorODataPathTemplate_SetsSegments_UsingList() + { + // Arrange + ODataSegmentTemplate template = new Mock().Object; + IList templates = new List { - // Arrange - ODataPathTemplate path = new ODataPathTemplate(); + template + }; - // Act - IEnumerable templates = path.GetTemplates(); + // Act + ODataPathTemplate path = new ODataPathTemplate(templates); - // Assert - var template = Assert.Single(templates); - Assert.Equal("", template); - } + // Assert + ODataSegmentTemplate actual = Assert.Single(path); + Assert.Same(template, actual); + } - [Fact] - public void GetTemplatesReturnsCorrectWithMetadataSegment() - { - // Arrange - ODataPathTemplate path = new ODataPathTemplate(MetadataSegmentTemplate.Instance); + [Fact] + public void GetTemplatesReturnsCorrectWithEmptySegments() + { + // Arrange + ODataPathTemplate path = new ODataPathTemplate(); - // Act - IEnumerable templates = path.GetTemplates(); + // Act + IEnumerable templates = path.GetTemplates(); - // Assert - var template = Assert.Single(templates); - Assert.Equal("$metadata", template); - } + // Assert + var template = Assert.Single(templates); + Assert.Equal("", template); + } - [Fact] - public void GetTemplatesReturnsCorrectWithTwoSegments() - { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "entity"); - EdmEntityContainer container = new EdmEntityContainer("NS", "default"); - EdmEntitySet entitySet = new EdmEntitySet(container, "set", entityType); - EdmAction action = new EdmAction("NS", "action", null, true, null); - ODataPathTemplate path = new ODataPathTemplate(new EntitySetSegmentTemplate(entitySet), - new ActionSegmentTemplate(action, null)); - - // Act - IEnumerable templates = path.GetTemplates(); - - // Act & Assert - Assert.Collection(templates, - e => - { - Assert.Equal("set/NS.action", e); - }, - e => - { - Assert.Equal("set/action", e); - }); - } + [Fact] + public void GetTemplatesReturnsCorrectWithMetadataSegment() + { + // Arrange + ODataPathTemplate path = new ODataPathTemplate(MetadataSegmentTemplate.Instance); - [Fact] - public void GetTemplatesWorksForBasicPath() - { - // Arrange - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - var entitySet = container.AddEntitySet("Customers", customer); + // Act + IEnumerable templates = path.GetTemplates(); - ODataPathTemplate template = new ODataPathTemplate( - new EntitySetSegmentTemplate(entitySet), - KeySegmentTemplate.CreateKeySegment(customer, entitySet)); + // Assert + var template = Assert.Single(templates); + Assert.Equal("$metadata", template); + } - // Act - IEnumerable actual = template.GetTemplates(); + [Fact] + public void GetTemplatesReturnsCorrectWithTwoSegments() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "entity"); + EdmEntityContainer container = new EdmEntityContainer("NS", "default"); + EdmEntitySet entitySet = new EdmEntitySet(container, "set", entityType); + EdmAction action = new EdmAction("NS", "action", null, true, null); + ODataPathTemplate path = new ODataPathTemplate(new EntitySetSegmentTemplate(entitySet), + new ActionSegmentTemplate(action, null)); + + // Act + IEnumerable templates = path.GetTemplates(); + + // Act & Assert + Assert.Collection(templates, + e => + { + Assert.Equal("set/NS.action", e); + }, + e => + { + Assert.Equal("set/action", e); + }); + } - // Assert - Assert.Equal(2, actual.Count()); - Assert.Equal(new[] { "Customers({key})", "Customers/{key}" }, actual); - } + [Fact] + public void GetTemplatesWorksForBasicPath() + { + // Arrange + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + var entitySet = container.AddEntitySet("Customers", customer); + + ODataPathTemplate template = new ODataPathTemplate( + new EntitySetSegmentTemplate(entitySet), + KeySegmentTemplate.CreateKeySegment(customer, entitySet)); + + // Act + IEnumerable actual = template.GetTemplates(); + + // Assert + Assert.Equal(2, actual.Count()); + Assert.Equal(new[] { "Customers({key})", "Customers/{key}" }, actual); + } - [Fact] - public void GetTemplatesWorksForODataPathWithDollarRefOnSingleNavigation() + [Fact] + public void GetTemplatesWorksForODataPathWithDollarRefOnSingleNavigation() + { + // Arrange + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + var entitySet = container.AddEntitySet("Customers", customer); + var navigation = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - // Arrange - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - var entitySet = container.AddEntitySet("Customers", customer); - var navigation = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo + TargetMultiplicity = EdmMultiplicity.One, + Name = "SubCustomer", + Target = customer + }); + + ODataPathTemplate template = new ODataPathTemplate( + new EntitySetSegmentTemplate(entitySet), + KeySegmentTemplate.CreateKeySegment(customer, entitySet), + new NavigationLinkSegmentTemplate(navigation, entitySet)); + + // Act + IEnumerable actual = template.GetTemplates(); + + // Assert + Assert.Equal(2, actual.Count()); + Assert.Equal(new[] { - TargetMultiplicity = EdmMultiplicity.One, - Name = "SubCustomer", - Target = customer - }); + "Customers({key})/SubCustomer/$ref", + "Customers/{key}/SubCustomer/$ref" + }, actual); + } - ODataPathTemplate template = new ODataPathTemplate( - new EntitySetSegmentTemplate(entitySet), - KeySegmentTemplate.CreateKeySegment(customer, entitySet), - new NavigationLinkSegmentTemplate(navigation, entitySet)); - - // Act - IEnumerable actual = template.GetTemplates(); - - // Assert - Assert.Equal(2, actual.Count()); - Assert.Equal(new[] - { - "Customers({key})/SubCustomer/$ref", - "Customers/{key}/SubCustomer/$ref" - }, actual); - } + [Fact] + public void GetTemplatesWorksForODataPathWithDollarRefOnCollectionNavigation() + { + // Arrange + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + var entitySet = container.AddEntitySet("Customers", customer); + var navigation = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo + { + TargetMultiplicity = EdmMultiplicity.Many, + Name = "SubCustomers", + Target = customer + }); - [Fact] - public void GetTemplatesWorksForODataPathWithDollarRefOnCollectionNavigation() + KeyValuePair[] keys = new KeyValuePair[] { - // Arrange - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - var entitySet = container.AddEntitySet("Customers", customer); - var navigation = customer.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo + new KeyValuePair("Id", "{nextKey}") + }; + KeySegment keySegment = new KeySegment(keys, customer, entitySet); + ODataPathTemplate template = new ODataPathTemplate( + new EntitySetSegmentTemplate(entitySet), + KeySegmentTemplate.CreateKeySegment(customer, entitySet), + new NavigationLinkSegmentTemplate(navigation, entitySet) { - TargetMultiplicity = EdmMultiplicity.Many, - Name = "SubCustomers", - Target = customer + Key = new KeySegmentTemplate(keySegment) }); - KeyValuePair[] keys = new KeyValuePair[] + // Act + IEnumerable actual = template.GetTemplates(); + + // Assert + Assert.Equal(4, actual.Count()); + Assert.Equal(new[] { - new KeyValuePair("Id", "{nextKey}") - }; - KeySegment keySegment = new KeySegment(keys, customer, entitySet); - ODataPathTemplate template = new ODataPathTemplate( - new EntitySetSegmentTemplate(entitySet), - KeySegmentTemplate.CreateKeySegment(customer, entitySet), - new NavigationLinkSegmentTemplate(navigation, entitySet) - { - Key = new KeySegmentTemplate(keySegment) - }); - - // Act - IEnumerable actual = template.GetTemplates(); - - // Assert - Assert.Equal(4, actual.Count()); - Assert.Equal(new[] - { - "Customers({key})/SubCustomers({nextKey})/$ref", - "Customers({key})/SubCustomers/{nextKey}/$ref", - "Customers/{key}/SubCustomers({nextKey})/$ref", - "Customers/{key}/SubCustomers/{nextKey}/$ref" - }, actual); - } + "Customers({key})/SubCustomers({nextKey})/$ref", + "Customers({key})/SubCustomers/{nextKey}/$ref", + "Customers/{key}/SubCustomers({nextKey})/$ref", + "Customers/{key}/SubCustomers/{nextKey}/$ref" + }, actual); + } - [Fact] - public void GetTemplatesWorksForPathWithTypeCastAndFunction() - { - // Arrange - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + [Fact] + public void GetTemplatesWorksForPathWithTypeCastAndFunction() + { + // Arrange + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - // VipCustomer - EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); + // VipCustomer + EdmEntityType vipCustomer = new EdmEntityType("NS", "VipCustomer", customer); - EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); - var entitySet = container.AddEntitySet("Customers", customer); + EdmEntityContainer container = new EdmEntityContainer("NS", "Default"); + var entitySet = container.AddEntitySet("Customers", customer); - // function with optional parameters - EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", IntType, isBound: true, entitySetPathExpression: null, isComposable: false); - getSalaray.AddParameter("entityset", new EdmEntityTypeReference(vipCustomer, false)); - getSalaray.AddParameter("salary", IntType); - getSalaray.AddOptionalParameter("minSalary", IntType); - getSalaray.AddOptionalParameter("maxSalary", IntType, "129"); + // function with optional parameters + EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", IntType, isBound: true, entitySetPathExpression: null, isComposable: false); + getSalaray.AddParameter("entityset", new EdmEntityTypeReference(vipCustomer, false)); + getSalaray.AddParameter("salary", IntType); + getSalaray.AddOptionalParameter("minSalary", IntType); + getSalaray.AddOptionalParameter("maxSalary", IntType, "129"); - ODataPathTemplate template = new ODataPathTemplate( - new EntitySetSegmentTemplate(entitySet), - KeySegmentTemplate.CreateKeySegment(customer, entitySet), - new CastSegmentTemplate(vipCustomer, customer, entitySet), - new FunctionSegmentTemplate(getSalaray, null)); + ODataPathTemplate template = new ODataPathTemplate( + new EntitySetSegmentTemplate(entitySet), + KeySegmentTemplate.CreateKeySegment(customer, entitySet), + new CastSegmentTemplate(vipCustomer, customer, entitySet), + new FunctionSegmentTemplate(getSalaray, null)); - // Act - IEnumerable actual = template.GetTemplates(); + // Act + IEnumerable actual = template.GetTemplates(); - Assert.Equal(4, actual.Count()); + Assert.Equal(4, actual.Count()); - Assert.Equal(new[] - { - "Customers({key})/NS.VipCustomer/NS.GetWholeSalary(salary={salary},minSalary={minSalary},maxSalary={maxSalary})", - "Customers({key})/NS.VipCustomer/GetWholeSalary(salary={salary},minSalary={minSalary},maxSalary={maxSalary})", - "Customers/{key}/NS.VipCustomer/NS.GetWholeSalary(salary={salary},minSalary={minSalary},maxSalary={maxSalary})", - "Customers/{key}/NS.VipCustomer/GetWholeSalary(salary={salary},minSalary={minSalary},maxSalary={maxSalary})", - }, actual); - } + Assert.Equal(new[] + { + "Customers({key})/NS.VipCustomer/NS.GetWholeSalary(salary={salary},minSalary={minSalary},maxSalary={maxSalary})", + "Customers({key})/NS.VipCustomer/GetWholeSalary(salary={salary},minSalary={minSalary},maxSalary={maxSalary})", + "Customers/{key}/NS.VipCustomer/NS.GetWholeSalary(salary={salary},minSalary={minSalary},maxSalary={maxSalary})", + "Customers/{key}/NS.VipCustomer/GetWholeSalary(salary={salary},minSalary={minSalary},maxSalary={maxSalary})", + }, actual); + } - #region GenerateFunctionTemplates + #region GenerateFunctionTemplates - public static TheoryDataSet FunctionWithoutParametersData + public static TheoryDataSet FunctionWithoutParametersData + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() - { - { true, new[] - { - "NS.GetWholeSalary()", - "GetWholeSalary()" - } - }, - { false, new[] { "GetWholeSalary()" } }, - }; - } + { true, new[] + { + "NS.GetWholeSalary()", + "GetWholeSalary()" + } + }, + { false, new[] { "GetWholeSalary()" } }, + }; } + } - [Theory] - [MemberData(nameof(FunctionWithoutParametersData))] - public void GetTemplatesWorks_ForEdmFunctionWithoutParameters(bool bound, string[] expects) + [Theory] + [MemberData(nameof(FunctionWithoutParametersData))] + public void GetTemplatesWorks_ForEdmFunctionWithoutParameters(bool bound, string[] expects) + { + // Arrange + ODataPathTemplate template; + EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", IntType, isBound: bound, entitySetPathExpression: null, isComposable: false); + if (bound) { - // Arrange - ODataPathTemplate template; - EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", IntType, isBound: bound, entitySetPathExpression: null, isComposable: false); - if (bound) - { - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - template = new ODataPathTemplate(new FunctionSegmentTemplate(getSalaray, null)); - } - else - { - EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); - template = new ODataPathTemplate(new FunctionImportSegmentTemplate(new EdmFunctionImport(container, "GetWholeSalary", getSalaray), null)); - } + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + template = new ODataPathTemplate(new FunctionSegmentTemplate(getSalaray, null)); + } + else + { + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + template = new ODataPathTemplate(new FunctionImportSegmentTemplate(new EdmFunctionImport(container, "GetWholeSalary", getSalaray), null)); + } - // Act - IEnumerable items = template.GetTemplates(); + // Act + IEnumerable items = template.GetTemplates(); - // Assert - Assert.Equal(expects.Length, items.Count()); - Assert.Equal(expects, items); - } + // Assert + Assert.Equal(expects.Length, items.Count()); + Assert.Equal(expects, items); + } - public static TheoryDataSet FunctionWithoutOptionalParametersData + public static TheoryDataSet FunctionWithoutOptionalParametersData + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() - { - { true, new[] - { - "NS.GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary})", - "GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary})" - } - }, - { false, new[] { "GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary})" } }, - }; - } + { true, new[] + { + "NS.GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary})", + "GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary})" + } + }, + { false, new[] { "GetWholeSalary(minSalary={minSalary},maxSalary={maxSalary})" } }, + }; } + } - [Theory] - [MemberData(nameof(FunctionWithoutOptionalParametersData))] - public void GetTemplatesWorks_ForEdmFunctionWithoutOptionalParameters(bool bound, string[] expects) + [Theory] + [MemberData(nameof(FunctionWithoutOptionalParametersData))] + public void GetTemplatesWorks_ForEdmFunctionWithoutOptionalParameters(bool bound, string[] expects) + { + // Arrange + ODataPathTemplate template; + EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", IntType, isBound: bound, entitySetPathExpression: null, isComposable: false); + if (bound) { - // Arrange - ODataPathTemplate template; - EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", IntType, isBound: bound, entitySetPathExpression: null, isComposable: false); - if (bound) - { - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); - getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - } + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + customer.AddKeys(customer.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + } - getSalaray.AddParameter("minSalary", IntType); - getSalaray.AddParameter("maxSalary", IntType); + getSalaray.AddParameter("minSalary", IntType); + getSalaray.AddParameter("maxSalary", IntType); - if (bound) - { - template = new ODataPathTemplate(new FunctionSegmentTemplate(getSalaray, null)); - } - else - { - EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); - template = new ODataPathTemplate(new FunctionImportSegmentTemplate(new EdmFunctionImport(container, "GetWholeSalary", getSalaray), null)); - } + if (bound) + { + template = new ODataPathTemplate(new FunctionSegmentTemplate(getSalaray, null)); + } + else + { + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + template = new ODataPathTemplate(new FunctionImportSegmentTemplate(new EdmFunctionImport(container, "GetWholeSalary", getSalaray), null)); + } - // Act - IEnumerable items = template.GetTemplates(); + // Act + IEnumerable items = template.GetTemplates(); - // Assert - Assert.Equal(expects.Length, items.Count()); - Assert.Equal(expects, items); - } + // Assert + Assert.Equal(expects.Length, items.Count()); + Assert.Equal(expects, items); + } - public static TheoryDataSet FunctionWithOptionalParametersData + public static TheoryDataSet FunctionWithOptionalParametersData + { + get { - get + return new TheoryDataSet() { - return new TheoryDataSet() - { - { true, new[] - { - "NS.GetWholeSalary(salary={salary},minSalary={min},maxSalary={max})", - "GetWholeSalary(salary={salary},minSalary={min},maxSalary={max})" - } - }, - { false, new[] - { - "GetWholeSalary(salary={salary},minSalary={min},maxSalary={max})" - } + { true, new[] + { + "NS.GetWholeSalary(salary={salary},minSalary={min},maxSalary={max})", + "GetWholeSalary(salary={salary},minSalary={min},maxSalary={max})" } - }; - } + }, + { false, new[] + { + "GetWholeSalary(salary={salary},minSalary={min},maxSalary={max})" + } + } + }; } + } - [Theory] - [MemberData(nameof(FunctionWithOptionalParametersData))] - public void GenerateFunctionTemplatesWorksForEdmFunctionWithOptionalParameters(bool bound, string[] expects) + [Theory] + [MemberData(nameof(FunctionWithOptionalParametersData))] + public void GenerateFunctionTemplatesWorksForEdmFunctionWithOptionalParameters(bool bound, string[] expects) + { + // Arrange + EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", IntType, isBound: bound, entitySetPathExpression: null, isComposable: false); + if (bound) { - // Arrange - EdmFunction getSalaray = new EdmFunction("NS", "GetWholeSalary", IntType, isBound: bound, entitySetPathExpression: null, isComposable: false); - if (bound) - { - EdmEntityType customer = new EdmEntityType("NS", "Customer"); - getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); - } + EdmEntityType customer = new EdmEntityType("NS", "Customer"); + getSalaray.AddParameter("entityset", new EdmCollectionTypeReference(new EdmCollectionType(new EdmEntityTypeReference(customer, false)))); + } - getSalaray.AddParameter("salary", IntType); - getSalaray.AddOptionalParameter("minSalary", IntType); - getSalaray.AddOptionalParameter("maxSalary", IntType); - getSalaray.AddOptionalParameter("aveSalary", IntType, "129"); + getSalaray.AddParameter("salary", IntType); + getSalaray.AddOptionalParameter("minSalary", IntType); + getSalaray.AddOptionalParameter("maxSalary", IntType); + getSalaray.AddOptionalParameter("aveSalary", IntType, "129"); - IDictionary parameters = new Dictionary - { - { "salary", "{salary}" }, - { "minSalary", "{min}" }, - { "maxSalary", "{max}" }, // without "aveSalary" - }; + IDictionary parameters = new Dictionary + { + { "salary", "{salary}" }, + { "minSalary", "{min}" }, + { "maxSalary", "{max}" }, // without "aveSalary" + }; - ODataPathTemplate template; - if (bound) - { - template = new ODataPathTemplate(new FunctionSegmentTemplate(parameters, getSalaray, null)); - } - else - { - EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); - template = new ODataPathTemplate(new FunctionImportSegmentTemplate(parameters, new EdmFunctionImport(container, "GetWholeSalary", getSalaray), null)); - } + ODataPathTemplate template; + if (bound) + { + template = new ODataPathTemplate(new FunctionSegmentTemplate(parameters, getSalaray, null)); + } + else + { + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + template = new ODataPathTemplate(new FunctionImportSegmentTemplate(parameters, new EdmFunctionImport(container, "GetWholeSalary", getSalaray), null)); + } - // Act - IEnumerable items = template.GetTemplates(); + // Act + IEnumerable items = template.GetTemplates(); - // Assert - Assert.Equal(expects.Length, items.Count()); - Assert.Equal(expects, items); - } - #endregion + // Assert + Assert.Equal(expects.Length, items.Count()); + Assert.Equal(expects, items); } + #endregion } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ODataTemplateTranslateContextTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ODataTemplateTranslateContextTests.cs index 35c60ef0f..0353628de 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ODataTemplateTranslateContextTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ODataTemplateTranslateContextTests.cs @@ -18,231 +18,230 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class ODataTemplateTranslateContextTests { - public class ODataTemplateTranslateContextTests + [Fact] + public void CtorODataTemplateTranslateContext_ThrowsArgumentNull_Context() { - [Fact] - public void CtorODataTemplateTranslateContext_ThrowsArgumentNull_Context() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new ODataTemplateTranslateContext(null, null, null, null), "context"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new ODataTemplateTranslateContext(null, null, null, null), "context"); + } - [Fact] - public void CtorODataTemplateTranslateContext_ThrowsArgumentNull_Endpoint() - { - // Arrange - HttpContext context = new Mock().Object; + [Fact] + public void CtorODataTemplateTranslateContext_ThrowsArgumentNull_Endpoint() + { + // Arrange + HttpContext context = new Mock().Object; - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new ODataTemplateTranslateContext(context, null, null, null), "endpoint"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new ODataTemplateTranslateContext(context, null, null, null), "endpoint"); + } - [Fact] - public void CtorODataTemplateTranslateContext_ThrowsArgumentNull_RouteValues() - { - // Arrange - HttpContext context = new Mock().Object; - Endpoint endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test"); + [Fact] + public void CtorODataTemplateTranslateContext_ThrowsArgumentNull_RouteValues() + { + // Arrange + HttpContext context = new Mock().Object; + Endpoint endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test"); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new ODataTemplateTranslateContext(context, endpoint, null, null), "routeValues"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new ODataTemplateTranslateContext(context, endpoint, null, null), "routeValues"); + } - [Fact] - public void CtorODataTemplateTranslateContext_ThrowsArgumentNull_Model() - { - // Arrange - HttpContext context = new Mock().Object; - Endpoint endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test"); - RouteValueDictionary routeValues = new RouteValueDictionary(); - - // Act & Assert - ExceptionAssert.ThrowsArgumentNull( - () => new ODataTemplateTranslateContext(context, endpoint, routeValues, null), "model"); - } - - [Fact] - public void CtorODataTemplateTranslateContext_SetsProperties() - { - // Arrange - HttpContext context = new Mock().Object; - Endpoint endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test"); - RouteValueDictionary routeValues = new RouteValueDictionary(); - IEdmModel model = EdmCoreModel.Instance; - - // Act - ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext(context, endpoint, routeValues, model); - - // Assert - Assert.Same(context, translateContext.HttpContext); - Assert.Same(endpoint, translateContext.Endpoint); - Assert.Same(routeValues, translateContext.RouteValues); - Assert.Same(model, translateContext.Model); - Assert.Empty(translateContext.UpdatedValues); - Assert.Empty(translateContext.Segments); - } - - [Fact] - public void GetParameterAliasOrSelf_ReturnsNull_IfAliasNull() - { - // Arrange - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - IHttpRequestFeature requestFeature = request.HttpContext.Features.Get(); - requestFeature.QueryString = "?@p=[1, 2, null, 7, 8]"; - - ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext - { - HttpContext = context - }; - - // Act - string alias = translateContext.GetParameterAliasOrSelf(null); - - // Assert - Assert.Null(alias); - } - - [Fact] - public void GetParameterAliasOrSelf_ReturnsExpectedAliasValue() + [Fact] + public void CtorODataTemplateTranslateContext_ThrowsArgumentNull_Model() + { + // Arrange + HttpContext context = new Mock().Object; + Endpoint endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test"); + RouteValueDictionary routeValues = new RouteValueDictionary(); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull( + () => new ODataTemplateTranslateContext(context, endpoint, routeValues, null), "model"); + } + + [Fact] + public void CtorODataTemplateTranslateContext_SetsProperties() + { + // Arrange + HttpContext context = new Mock().Object; + Endpoint endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test"); + RouteValueDictionary routeValues = new RouteValueDictionary(); + IEdmModel model = EdmCoreModel.Instance; + + // Act + ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext(context, endpoint, routeValues, model); + + // Assert + Assert.Same(context, translateContext.HttpContext); + Assert.Same(endpoint, translateContext.Endpoint); + Assert.Same(routeValues, translateContext.RouteValues); + Assert.Same(model, translateContext.Model); + Assert.Empty(translateContext.UpdatedValues); + Assert.Empty(translateContext.Segments); + } + + [Fact] + public void GetParameterAliasOrSelf_ReturnsNull_IfAliasNull() + { + // Arrange + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; + IHttpRequestFeature requestFeature = request.HttpContext.Features.Get(); + requestFeature.QueryString = "?@p=[1, 2, null, 7, 8]"; + + ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext { - // Arrange - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - IHttpRequestFeature requestFeature = request.HttpContext.Features.Get(); - requestFeature.QueryString = "?@p=[1, 2, null, 7, 8]"; - - ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext - { - HttpContext = context - }; - - // Act - string alias = translateContext.GetParameterAliasOrSelf("@p"); - - // Assert - Assert.Equal("[1, 2, null, 7, 8]", alias); - } - - [Fact] - public void GetParameterAliasOrSelf_ReturnsExpectedAliasValue_ForMultiple() + HttpContext = context + }; + + // Act + string alias = translateContext.GetParameterAliasOrSelf(null); + + // Assert + Assert.Null(alias); + } + + [Fact] + public void GetParameterAliasOrSelf_ReturnsExpectedAliasValue() + { + // Arrange + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; + IHttpRequestFeature requestFeature = request.HttpContext.Features.Get(); + requestFeature.QueryString = "?@p=[1, 2, null, 7, 8]"; + + ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext { - // Arrange - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - IHttpRequestFeature requestFeature = request.HttpContext.Features.Get(); - requestFeature.QueryString = "?@p=@age&@age=@para1&@para1='ab''c'"; - - ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext - { - HttpContext = context - }; - - // Act - string alias = translateContext.GetParameterAliasOrSelf("@p"); - - // Assert - Assert.Equal("'ab''c'", alias); - } - - [Fact] - public void GetParameterAliasOrSelf_Throws_ForInfiniteLoopParameterAlias() + HttpContext = context + }; + + // Act + string alias = translateContext.GetParameterAliasOrSelf("@p"); + + // Assert + Assert.Equal("[1, 2, null, 7, 8]", alias); + } + + [Fact] + public void GetParameterAliasOrSelf_ReturnsExpectedAliasValue_ForMultiple() + { + // Arrange + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; + IHttpRequestFeature requestFeature = request.HttpContext.Features.Get(); + requestFeature.QueryString = "?@p=@age&@age=@para1&@para1='ab''c'"; + + ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext { - // Arrange - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - IHttpRequestFeature requestFeature = request.HttpContext.Features.Get(); - requestFeature.QueryString = "?@p=@age&@age=@p1&@p1=@p"; - - ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext - { - HttpContext = context - }; - - // Act - Action test = () => translateContext.GetParameterAliasOrSelf("@p"); - - // Assert - ExceptionAssert.Throws(test, "The parameter alias '@p' is in an infinite loop."); - } - - [Fact] - public void GetParameterAliasOrSelf_Throws_ForMissingParameterAlias() + HttpContext = context + }; + + // Act + string alias = translateContext.GetParameterAliasOrSelf("@p"); + + // Assert + Assert.Equal("'ab''c'", alias); + } + + [Fact] + public void GetParameterAliasOrSelf_Throws_ForInfiniteLoopParameterAlias() + { + // Arrange + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; + IHttpRequestFeature requestFeature = request.HttpContext.Features.Get(); + requestFeature.QueryString = "?@p=@age&@age=@p1&@p1=@p"; + + ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext { - // Arrange - HttpContext context = new DefaultHttpContext(); - HttpRequest request = context.Request; - IHttpRequestFeature requestFeature = request.HttpContext.Features.Get(); - requestFeature.QueryString = "?@p=@age&@para1='abc'"; - - ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext - { - HttpContext = context - }; - - // Act - Action test = () => translateContext.GetParameterAliasOrSelf("@p"); - - // Assert - ExceptionAssert.Throws(test, "Missing the parameter alias '@age' in the request query string."); - } - - [Theory] - [InlineData("/{key}", true)] - [InlineData("/{KEY}", true)] - [InlineData("({key})", false)] - [InlineData("({KEY})", false)] - [InlineData("a/customer", true)] - public void IsPartOfRouteTemplate_ReturnsCorrect_ForGivenPart(string part, bool expect) + HttpContext = context + }; + + // Act + Action test = () => translateContext.GetParameterAliasOrSelf("@p"); + + // Assert + ExceptionAssert.Throws(test, "The parameter alias '@p' is in an infinite loop."); + } + + [Fact] + public void GetParameterAliasOrSelf_Throws_ForMissingParameterAlias() + { + // Arrange + HttpContext context = new DefaultHttpContext(); + HttpRequest request = context.Request; + IHttpRequestFeature requestFeature = request.HttpContext.Features.Get(); + requestFeature.QueryString = "?@p=@age&@para1='abc'"; + + ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext { - // Arrange - RouteEndpoint endpoint = new RouteEndpoint( - c => Task.CompletedTask, - RoutePatternFactory.Parse("odata/customers/{key}/Name"), - 0, - EndpointMetadataCollection.Empty, - "test"); - - ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext - { - Endpoint = endpoint - }; - - // Act - bool actual = translateContext.IsPartOfRouteTemplate(part); - - // Assert - Assert.Equal(expect, actual); - } - - [Fact] - public void IsPartOfRouteTemplate_ReturnsFalse_ForNonRouteEndpoint() + HttpContext = context + }; + + // Act + Action test = () => translateContext.GetParameterAliasOrSelf("@p"); + + // Assert + ExceptionAssert.Throws(test, "Missing the parameter alias '@age' in the request query string."); + } + + [Theory] + [InlineData("/{key}", true)] + [InlineData("/{KEY}", true)] + [InlineData("({key})", false)] + [InlineData("({KEY})", false)] + [InlineData("a/customer", true)] + public void IsPartOfRouteTemplate_ReturnsCorrect_ForGivenPart(string part, bool expect) + { + // Arrange + RouteEndpoint endpoint = new RouteEndpoint( + c => Task.CompletedTask, + RoutePatternFactory.Parse("odata/customers/{key}/Name"), + 0, + EndpointMetadataCollection.Empty, + "test"); + + ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext { - // Arrange - MyEndpoint endpoint = new MyEndpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "test"); + Endpoint = endpoint + }; - ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext - { - Endpoint = endpoint - }; + // Act + bool actual = translateContext.IsPartOfRouteTemplate(part); - // Act - bool actual = translateContext.IsPartOfRouteTemplate("/{key}"); + // Assert + Assert.Equal(expect, actual); + } - // Assert - Assert.False(actual); - } + [Fact] + public void IsPartOfRouteTemplate_ReturnsFalse_ForNonRouteEndpoint() + { + // Arrange + MyEndpoint endpoint = new MyEndpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "test"); - internal class MyEndpoint : Endpoint + ODataTemplateTranslateContext translateContext = new ODataTemplateTranslateContext { - public MyEndpoint(RequestDelegate requestDelegate, EndpointMetadataCollection metadata, string displayName) - : base(requestDelegate, metadata, displayName) - { } - } + Endpoint = endpoint + }; + + // Act + bool actual = translateContext.IsPartOfRouteTemplate("/{key}"); + + // Assert + Assert.False(actual); + } + + internal class MyEndpoint : Endpoint + { + public MyEndpoint(RequestDelegate requestDelegate, EndpointMetadataCollection metadata, string displayName) + : base(requestDelegate, metadata, displayName) + { } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/PathTemplateSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/PathTemplateSegmentTemplateTests.cs index f6d22031a..82926da0d 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/PathTemplateSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/PathTemplateSegmentTemplateTests.cs @@ -14,291 +14,290 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class PathTemplateSegmentTemplateTests { - public class PathTemplateSegmentTemplateTests + private static IEdmEntityType _entityType; + + static PathTemplateSegmentTemplateTests() { - private static IEdmEntityType _entityType; + EdmEntityType entityType = new EdmEntityType("NS", "Customer", null, false, true); + entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); - static PathTemplateSegmentTemplateTests() + entityType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { - EdmEntityType entityType = new EdmEntityType("NS", "Customer", null, false, true); - entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); - - entityType.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo - { - Name = "RelatedCustomers", - Target = entityType, - TargetMultiplicity = EdmMultiplicity.Many - }); + Name = "RelatedCustomers", + Target = entityType, + TargetMultiplicity = EdmMultiplicity.Many + }); - _entityType = entityType; - } + _entityType = entityType; + } - [Fact] - public void CtorPathTemplateSegmentTemplate_ThrowsArgumentNull_Segment() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new PathTemplateSegmentTemplate(segment: null), "segment"); - } + [Fact] + public void CtorPathTemplateSegmentTemplate_ThrowsArgumentNull_Segment() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new PathTemplateSegmentTemplate(segment: null), "segment"); + } - [Fact] - public void CtorPathTemplateSegmentTemplate_ThrowsODataException_WrongTemplate() - { - // Arrange - PathTemplateSegment segment = new PathTemplateSegment("property"); + [Fact] + public void CtorPathTemplateSegmentTemplate_ThrowsODataException_WrongTemplate() + { + // Arrange + PathTemplateSegment segment = new PathTemplateSegment("property"); - // Act & Assert - ODataException exception = ExceptionAssert.Throws(() => new PathTemplateSegmentTemplate(segment)); - Assert.Equal("The attribute routing template contains invalid segment 'property'. The template string does not start with '{' or ends with '}'.", - exception.Message); - } + // Act & Assert + ODataException exception = ExceptionAssert.Throws(() => new PathTemplateSegmentTemplate(segment)); + Assert.Equal("The attribute routing template contains invalid segment 'property'. The template string does not start with '{' or ends with '}'.", + exception.Message); + } - [Fact] - public void CtorPathTemplateSegmentTemplate_ThrowsODataException_EmptyTemplate() - { - // Arrange - PathTemplateSegment segment = new PathTemplateSegment("{}"); + [Fact] + public void CtorPathTemplateSegmentTemplate_ThrowsODataException_EmptyTemplate() + { + // Arrange + PathTemplateSegment segment = new PathTemplateSegment("{}"); - // Act & Assert - ODataException exception = ExceptionAssert.Throws(() => new PathTemplateSegmentTemplate(segment)); - Assert.Equal("The route template in path template '{}' is empty.", exception.Message); - } + // Act & Assert + ODataException exception = ExceptionAssert.Throws(() => new PathTemplateSegmentTemplate(segment)); + Assert.Equal("The route template in path template '{}' is empty.", exception.Message); + } - [Fact] - public void CtorPathTemplateSegmentTemplate_SetsProperties() - { - // Arrange & Act - PathTemplateSegment segment = new PathTemplateSegment("{any}"); - PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); + [Fact] + public void CtorPathTemplateSegmentTemplate_SetsProperties() + { + // Arrange & Act + PathTemplateSegment segment = new PathTemplateSegment("{any}"); + PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); - // Assert - Assert.Equal("any", pathSegment.ParameterName); - Assert.Same(segment, pathSegment.Segment); - } + // Assert + Assert.Equal("any", pathSegment.ParameterName); + Assert.Same(segment, pathSegment.Segment); + } - [Fact] - public void GetTemplatesPathTemplateSegmentTemplate_ReturnsTemplates() - { - // Arrange - PathTemplateSegment segment = new PathTemplateSegment("{any}"); - PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); - - // Act & Assert - IEnumerable templates = pathSegment.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/{any}", template); - } + [Fact] + public void GetTemplatesPathTemplateSegmentTemplate_ReturnsTemplates() + { + // Arrange + PathTemplateSegment segment = new PathTemplateSegment("{any}"); + PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); + + // Act & Assert + IEnumerable templates = pathSegment.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/{any}", template); + } - [Fact] - public void TryTranslatePathTemplateSegmentTemplate_ThrowsArgumentNull_Context() - { - // Arrange - PathTemplateSegment segment = new PathTemplateSegment("{any}"); - PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); + [Fact] + public void TryTranslatePathTemplateSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + PathTemplateSegment segment = new PathTemplateSegment("{any}"); + PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => pathSegment.TryTranslate(null), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => pathSegment.TryTranslate(null), "context"); + } - [Theory] - [InlineData("{any}")] // false because unknown parameter name - [InlineData("{property}")] // false because no previous segment in the context - [InlineData("{dynamicproperty}")] // false because no previous segment in the context - public void TryTranslatePathTemplateSegmentTemplate_ReturnsFalse_NotSupportedTemplate(string template) - { - // Arrange - PathTemplateSegment segment = new PathTemplateSegment(template); - PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + [Theory] + [InlineData("{any}")] // false because unknown parameter name + [InlineData("{property}")] // false because no previous segment in the context + [InlineData("{dynamicproperty}")] // false because no previous segment in the context + public void TryTranslatePathTemplateSegmentTemplate_ReturnsFalse_NotSupportedTemplate(string template) + { + // Arrange + PathTemplateSegment segment = new PathTemplateSegment(template); + PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - // Act & Assert - Assert.False(pathSegment.TryTranslate(context)); - } + // Act & Assert + Assert.False(pathSegment.TryTranslate(context)); + } - [Theory] - [InlineData("{property}")] - [InlineData("{dynamicproperty}")] - public void TryTranslatePathTemplateSegmentTemplate_ReturnsFalse_PreviousSegmentNotStructuredType(string template) - { - // Arrange - MySegment previousSegment = new MySegment(EdmCoreModel.Instance.GetString(false).Definition); + [Theory] + [InlineData("{property}")] + [InlineData("{dynamicproperty}")] + public void TryTranslatePathTemplateSegmentTemplate_ReturnsFalse_PreviousSegmentNotStructuredType(string template) + { + // Arrange + MySegment previousSegment = new MySegment(EdmCoreModel.Instance.GetString(false).Definition); - PathTemplateSegment segment = new PathTemplateSegment(template); - PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - context.Segments.Add(previousSegment); + PathTemplateSegment segment = new PathTemplateSegment(template); + PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + context.Segments.Add(previousSegment); - // Act & Assert - Assert.False(pathSegment.TryTranslate(context)); - } + // Act & Assert + Assert.False(pathSegment.TryTranslate(context)); + } - [Theory] - [InlineData("{property}")] - [InlineData("{dynamicproperty}")] - public void TryTranslatePathTemplateSegmentTemplate_ReturnsFalse_NoCorrectRouteData(string template) + [Theory] + [InlineData("{property}")] + [InlineData("{dynamicproperty}")] + public void TryTranslatePathTemplateSegmentTemplate_ReturnsFalse_NoCorrectRouteData(string template) + { + // Arrange + PathTemplateSegment segment = new PathTemplateSegment(template); + PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - PathTemplateSegment segment = new PathTemplateSegment(template); - PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = new RouteValueDictionary() - }; - context.Segments.Add(new MySegment(_entityType)); + RouteValues = new RouteValueDictionary() + }; + context.Segments.Add(new MySegment(_entityType)); - // Act & Assert - Assert.False(pathSegment.TryTranslate(context)); - } + // Act & Assert + Assert.False(pathSegment.TryTranslate(context)); + } - [Fact] - public void TryTranslatePathTemplateSegmentTemplate_ReturnsFalse_UnknownProperty() + [Fact] + public void TryTranslatePathTemplateSegmentTemplate_ReturnsFalse_UnknownProperty() + { + // Arrange + RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { property = "Unknown" }); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { property = "Unknown" }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValueDictionary - }; - context.Segments.Add(new MySegment(_entityType)); + RouteValues = routeValueDictionary + }; + context.Segments.Add(new MySegment(_entityType)); - PathTemplateSegment segment = new PathTemplateSegment("{property}"); - PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); + PathTemplateSegment segment = new PathTemplateSegment("{property}"); + PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); - // Act & Assert - Assert.False(pathSegment.TryTranslate(context)); - } + // Act & Assert + Assert.False(pathSegment.TryTranslate(context)); + } - [Fact] - public void TryTranslatePathTemplateSegmentTemplate_ReturnsPropertySegment() + [Fact] + public void TryTranslatePathTemplateSegmentTemplate_ReturnsPropertySegment() + { + // Arrange + RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { property = "Title" }); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { property = "Title" }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + RouteValues = routeValueDictionary + }; + context.Segments.Add(new MySegment(_entityType)); + + PathTemplateSegment segment = new PathTemplateSegment("{property}"); + PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); + + // Act + Assert.True(pathSegment.TryTranslate(context)); + + // Assert + Assert.Equal(2, context.Segments.Count); // 1 - MySegment, 2 - Property Segment + Assert.Collection(context.Segments, + e => { - RouteValues = routeValueDictionary - }; - context.Segments.Add(new MySegment(_entityType)); - - PathTemplateSegment segment = new PathTemplateSegment("{property}"); - PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); - - // Act - Assert.True(pathSegment.TryTranslate(context)); - - // Assert - Assert.Equal(2, context.Segments.Count); // 1 - MySegment, 2 - Property Segment - Assert.Collection(context.Segments, - e => - { - Assert.IsType(e); - }, - e => - { - PropertySegment propertySegment = Assert.IsType(e); - Assert.Equal("Title", propertySegment.Property.Name); - }); - } + Assert.IsType(e); + }, + e => + { + PropertySegment propertySegment = Assert.IsType(e); + Assert.Equal("Title", propertySegment.Property.Name); + }); + } - [Fact] - public void TryTranslatePathTemplateSegmentTemplate_ReturnsNavigationPropertySegment() + [Fact] + public void TryTranslatePathTemplateSegmentTemplate_ReturnsNavigationPropertySegment() + { + // Arrange + RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { property = "RelatedCustomers" }); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { property = "RelatedCustomers" }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + RouteValues = routeValueDictionary + }; + context.Segments.Add(new MySegment(_entityType)); + + PathTemplateSegment segment = new PathTemplateSegment("{property}"); + PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); + + // Act + Assert.True(pathSegment.TryTranslate(context)); + + // Assert + Assert.Equal(2, context.Segments.Count); // 1 - MySegment, 2 - Property Segment + Assert.Collection(context.Segments, + e => { - RouteValues = routeValueDictionary - }; - context.Segments.Add(new MySegment(_entityType)); - - PathTemplateSegment segment = new PathTemplateSegment("{property}"); - PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); - - // Act - Assert.True(pathSegment.TryTranslate(context)); - - // Assert - Assert.Equal(2, context.Segments.Count); // 1 - MySegment, 2 - Property Segment - Assert.Collection(context.Segments, - e => - { - Assert.IsType(e); - }, - e => - { - NavigationPropertySegment propertySegment = Assert.IsType(e); - Assert.Equal("RelatedCustomers", propertySegment.NavigationProperty.Name); - }); - } + Assert.IsType(e); + }, + e => + { + NavigationPropertySegment propertySegment = Assert.IsType(e); + Assert.Equal("RelatedCustomers", propertySegment.NavigationProperty.Name); + }); + } - [Fact] - public void TryTranslatePathTemplateSegmentTemplate_ReturnsFalse_DynamicPathSegmentWithKnowProperty() + [Fact] + public void TryTranslatePathTemplateSegmentTemplate_ReturnsFalse_DynamicPathSegmentWithKnowProperty() + { + // Arrange + RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { dynamicproperty = "Name" }); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { dynamicproperty = "Name" }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValueDictionary - }; - context.Segments.Add(new MySegment(_entityType)); + RouteValues = routeValueDictionary + }; + context.Segments.Add(new MySegment(_entityType)); - PathTemplateSegment segment = new PathTemplateSegment("{dynamicproperty}"); - PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); + PathTemplateSegment segment = new PathTemplateSegment("{dynamicproperty}"); + PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); - // Act & Assert - Assert.False(pathSegment.TryTranslate(context)); - } + // Act & Assert + Assert.False(pathSegment.TryTranslate(context)); + } - [Fact] - public void TryTranslatePathTemplateSegmentTemplate_ReturnsDynamicPathSegment() + [Fact] + public void TryTranslatePathTemplateSegmentTemplate_ReturnsDynamicPathSegment() + { + // Arrange + RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { dynamicproperty = "Dynamic" }); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { dynamicproperty = "Dynamic" }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + RouteValues = routeValueDictionary + }; + context.Segments.Add(new MySegment(_entityType)); + + PathTemplateSegment segment = new PathTemplateSegment("{dynamicproperty}"); + PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); + + // Act + Assert.True(pathSegment.TryTranslate(context)); + + // Assert + Assert.Equal(2, context.Segments.Count); // 1 - MySegment, 2 - Property Segment + Assert.Collection(context.Segments, + e => { - RouteValues = routeValueDictionary - }; - context.Segments.Add(new MySegment(_entityType)); - - PathTemplateSegment segment = new PathTemplateSegment("{dynamicproperty}"); - PathTemplateSegmentTemplate pathSegment = new PathTemplateSegmentTemplate(segment); - - // Act - Assert.True(pathSegment.TryTranslate(context)); - - // Assert - Assert.Equal(2, context.Segments.Count); // 1 - MySegment, 2 - Property Segment - Assert.Collection(context.Segments, - e => - { - Assert.IsType(e); - }, - e => - { - DynamicPathSegment dynamicSegment = Assert.IsType(e); - Assert.Equal("Dynamic", dynamicSegment.Identifier); - }); - } + Assert.IsType(e); + }, + e => + { + DynamicPathSegment dynamicSegment = Assert.IsType(e); + Assert.Equal("Dynamic", dynamicSegment.Identifier); + }); + } - private class MySegment : ODataPathSegment + private class MySegment : ODataPathSegment + { + public MySegment(IEdmType type) { - public MySegment(IEdmType type) - { - EdmType = type; - } + EdmType = type; + } - public override IEdmType EdmType { get; } + public override IEdmType EdmType { get; } - public override void HandleWith(PathSegmentHandler handler) - { - throw new System.NotImplementedException(); - } + public override void HandleWith(PathSegmentHandler handler) + { + throw new System.NotImplementedException(); + } - public override T TranslateWith(PathSegmentTranslator translator) - { - throw new System.NotImplementedException(); - } + public override T TranslateWith(PathSegmentTranslator translator) + { + throw new System.NotImplementedException(); } } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/PropertyCatchAllSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/PropertyCatchAllSegmentTemplateTests.cs index 87172f5eb..eb8e428e2 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/PropertyCatchAllSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/PropertyCatchAllSegmentTemplateTests.cs @@ -13,96 +13,95 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class PropertyCatchAllSegmentTemplateTests { - public class PropertyCatchAllSegmentTemplateTests + private static IEdmEntityType _entityType; + + static PropertyCatchAllSegmentTemplateTests() { - private static IEdmEntityType _entityType; + EdmEntityType entityType = new EdmEntityType("NS", "Customer", null, false, true); + entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + entityType.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); + _entityType = entityType; + } - static PropertyCatchAllSegmentTemplateTests() - { - EdmEntityType entityType = new EdmEntityType("NS", "Customer", null, false, true); - entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - entityType.AddStructuralProperty("Title", EdmPrimitiveTypeKind.String); - _entityType = entityType; - } - - [Fact] - public void CtorPropertyCatchAllSegmentTemplate_ThrowsArgumentNull_DeclaredType() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new PropertyCatchAllSegmentTemplate(null), "declaredType"); - } + [Fact] + public void CtorPropertyCatchAllSegmentTemplate_ThrowsArgumentNull_DeclaredType() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new PropertyCatchAllSegmentTemplate(null), "declaredType"); + } - [Fact] - public void CtorPropertyCatchAllSegmentTemplate_SetsProperties() - { - // Arrange & Act - PropertyCatchAllSegmentTemplate pathSegment = new PropertyCatchAllSegmentTemplate(_entityType); + [Fact] + public void CtorPropertyCatchAllSegmentTemplate_SetsProperties() + { + // Arrange & Act + PropertyCatchAllSegmentTemplate pathSegment = new PropertyCatchAllSegmentTemplate(_entityType); - // Assert - Assert.Same(_entityType, pathSegment.StructuredType); - } + // Assert + Assert.Same(_entityType, pathSegment.StructuredType); + } - [Fact] - public void GetTemplatesPropertyCatchAllSegmentTemplate_ReturnsTemplates() - { - // Arrange - PropertyCatchAllSegmentTemplate pathSegment = new PropertyCatchAllSegmentTemplate(_entityType); + [Fact] + public void GetTemplatesPropertyCatchAllSegmentTemplate_ReturnsTemplates() + { + // Arrange + PropertyCatchAllSegmentTemplate pathSegment = new PropertyCatchAllSegmentTemplate(_entityType); - // Act & Assert - IEnumerable templates = pathSegment.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/{property}", template); - } + // Act & Assert + IEnumerable templates = pathSegment.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/{property}", template); + } - [Fact] - public void TryTranslatePropertyCatchAllSegmentTemplate_ThrowsArgumentNull_Context() - { - // Arrange - PropertyCatchAllSegmentTemplate pathSegment = new PropertyCatchAllSegmentTemplate(_entityType); + [Fact] + public void TryTranslatePropertyCatchAllSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + PropertyCatchAllSegmentTemplate pathSegment = new PropertyCatchAllSegmentTemplate(_entityType); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => pathSegment.TryTranslate(null), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => pathSegment.TryTranslate(null), "context"); + } - [Fact] - public void TryTranslatePropertyCatchAllSegmentTemplate_ReturnsFalse_NoCorrectRouteData() + [Fact] + public void TryTranslatePropertyCatchAllSegmentTemplate_ReturnsFalse_NoCorrectRouteData() + { + // Arrange + PropertyCatchAllSegmentTemplate pathSegment = new PropertyCatchAllSegmentTemplate(_entityType); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - PropertyCatchAllSegmentTemplate pathSegment = new PropertyCatchAllSegmentTemplate(_entityType); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = new RouteValueDictionary() - }; - - // Act & Assert - Assert.False(pathSegment.TryTranslate(context)); - } - - [Theory] - [InlineData("Title")] - [InlineData("title")] - [InlineData("tiTLE")] - public void TryTranslatePropertyCatchAllSegmentTemplate_ReturnsPropertySegment(string property) + RouteValues = new RouteValueDictionary() + }; + + // Act & Assert + Assert.False(pathSegment.TryTranslate(context)); + } + + [Theory] + [InlineData("Title")] + [InlineData("title")] + [InlineData("tiTLE")] + public void TryTranslatePropertyCatchAllSegmentTemplate_ReturnsPropertySegment(string property) + { + // Arrange + RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { property = $"{property}" }); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext { - // Arrange - RouteValueDictionary routeValueDictionary = new RouteValueDictionary(new { property = $"{property}" }); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValueDictionary - }; - - PropertyCatchAllSegmentTemplate pathSegment = new PropertyCatchAllSegmentTemplate(_entityType); - - // Act - bool ok = pathSegment.TryTranslate(context); - - // Assert - Assert.True(ok); - ODataPathSegment segment = Assert.Single(context.Segments); - PropertySegment propertySegment = Assert.IsType(segment); - Assert.Equal("Title", propertySegment.Property.Name); - } + RouteValues = routeValueDictionary + }; + + PropertyCatchAllSegmentTemplate pathSegment = new PropertyCatchAllSegmentTemplate(_entityType); + + // Act + bool ok = pathSegment.TryTranslate(context); + + // Assert + Assert.True(ok); + ODataPathSegment segment = Assert.Single(context.Segments); + PropertySegment propertySegment = Assert.IsType(segment); + Assert.Equal("Title", propertySegment.Property.Name); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/PropertySegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/PropertySegmentTemplateTests.cs index 807e48867..3e61435c8 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/PropertySegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/PropertySegmentTemplateTests.cs @@ -12,79 +12,78 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class PropertySegmentTemplateTests { - public class PropertySegmentTemplateTests - { - private static PropertySegmentTemplate _propertySegment; - private static IEdmStructuralProperty _edmProperty; + private static PropertySegmentTemplate _propertySegment; + private static IEdmStructuralProperty _edmProperty; - static PropertySegmentTemplateTests() - { - EdmEntityType entityType = new EdmEntityType("NS", "Customer"); - _edmProperty = entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - _propertySegment = new PropertySegmentTemplate(_edmProperty); - } + static PropertySegmentTemplateTests() + { + EdmEntityType entityType = new EdmEntityType("NS", "Customer"); + _edmProperty = entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + _propertySegment = new PropertySegmentTemplate(_edmProperty); + } - [Fact] - public void CtorPropertySegmentTemplate_ThrowsArgumentNull_Property() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new PropertySegmentTemplate(property: null), "property"); - } + [Fact] + public void CtorPropertySegmentTemplate_ThrowsArgumentNull_Property() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new PropertySegmentTemplate(property: null), "property"); + } - [Fact] - public void CtorPropertySegmentTemplate_ThrowsArgumentNull_Segment() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new PropertySegmentTemplate(segment: null), "segment"); - } + [Fact] + public void CtorPropertySegmentTemplate_ThrowsArgumentNull_Segment() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new PropertySegmentTemplate(segment: null), "segment"); + } - [Fact] - public void CtorPropertySegmentTemplate_SetsProperties() - { - // Arrange & Act & Assert - EdmEntityType entityType = new EdmEntityType("NS", "Customer"); - IEdmStructuralProperty property = entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); - PropertySegmentTemplate propertySegment = new PropertySegmentTemplate(property); - Assert.NotNull(propertySegment.Segment); - Assert.Same(property, propertySegment.Property); + [Fact] + public void CtorPropertySegmentTemplate_SetsProperties() + { + // Arrange & Act & Assert + EdmEntityType entityType = new EdmEntityType("NS", "Customer"); + IEdmStructuralProperty property = entityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String); + PropertySegmentTemplate propertySegment = new PropertySegmentTemplate(property); + Assert.NotNull(propertySegment.Segment); + Assert.Same(property, propertySegment.Property); - // Arrange & Act & Assert - PropertySegmentTemplate propertySegment2 = new PropertySegmentTemplate(propertySegment.Segment); - Assert.Same(propertySegment.Segment, propertySegment2.Segment); - } + // Arrange & Act & Assert + PropertySegmentTemplate propertySegment2 = new PropertySegmentTemplate(propertySegment.Segment); + Assert.Same(propertySegment.Segment, propertySegment2.Segment); + } - [Fact] - public void GetTemplatesPropertySegmentTemplate_ReturnsTemplates() - { - // Assert & Act & Assert - IEnumerable templates = _propertySegment.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/Name", template); - } + [Fact] + public void GetTemplatesPropertySegmentTemplate_ReturnsTemplates() + { + // Assert & Act & Assert + IEnumerable templates = _propertySegment.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/Name", template); + } - [Fact] - public void TryTranslatePropertySegmentTemplate_ThrowsArgumentNull_Context() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => _propertySegment.TryTranslate(null), "context"); - } + [Fact] + public void TryTranslatePropertySegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => _propertySegment.TryTranslate(null), "context"); + } - [Fact] - public void TryTranslatePropertySegmentTemplate_ReturnsPropertySegment() - { - // Arrange - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + [Fact] + public void TryTranslatePropertySegmentTemplate_ReturnsPropertySegment() + { + // Arrange + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - // Act - bool ok = _propertySegment.TryTranslate(context); + // Act + bool ok = _propertySegment.TryTranslate(context); - // Assert - Assert.True(ok); - ODataPathSegment segment = Assert.Single(context.Segments); - PropertySegment odataPropertySegment = Assert.IsType(segment); - Assert.Equal("Edm.String", odataPropertySegment.EdmType.FullTypeName()); - } + // Assert + Assert.True(ok); + ODataPathSegment segment = Assert.Single(context.Segments); + PropertySegment odataPropertySegment = Assert.IsType(segment); + Assert.Equal("Edm.String", odataPropertySegment.EdmType.FullTypeName()); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/SegmentTemplateHelpersTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/SegmentTemplateHelpersTests.cs index e2be7f729..97025bb38 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/SegmentTemplateHelpersTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/SegmentTemplateHelpersTests.cs @@ -16,239 +16,238 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class SegmentTemplateHelpersTests { - public class SegmentTemplateHelpersTests + private static IEdmPrimitiveTypeReference _IntType = EdmCoreModel.Instance.GetInt32(false); + private static IEdmPrimitiveTypeReference _StrType = EdmCoreModel.Instance.GetString(false); + + [Fact] + public void MatchForFunction_ThrowsODataException_ForInvalidUriValue() { - private static IEdmPrimitiveTypeReference _IntType = EdmCoreModel.Instance.GetInt32(false); - private static IEdmPrimitiveTypeReference _StrType = EdmCoreModel.Instance.GetString(false); + // Arrange + EdmModel model = new EdmModel(); + EdmFunction function = new EdmFunction("NS", "MyFunction", _IntType, true, null, false); + function.AddParameter("data", _IntType); + model.AddElement(function); - [Fact] - public void MatchForFunction_ThrowsODataException_ForInvalidUriValue() + RouteValueDictionary routeValues = new RouteValueDictionary() { - // Arrange - EdmModel model = new EdmModel(); - EdmFunction function = new EdmFunction("NS", "MyFunction", _IntType, true, null, false); - function.AddParameter("data", _IntType); - model.AddElement(function); + { "dataValue", "ef12abc" } + }; - RouteValueDictionary routeValues = new RouteValueDictionary() - { - { "dataValue", "ef12abc" } - }; + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + { + RouteValues = routeValues, + Model = model + }; - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValues, - Model = model - }; + IDictionary parameterMappings = new Dictionary + { + { "data", "dataValue" } + }; - IDictionary parameterMappings = new Dictionary - { - { "data", "dataValue" } - }; - - // Act - Action test = () => SegmentTemplateHelpers.Match(context, function, parameterMappings); - ExceptionAssert.Throws(test, - "The parameter value (ef12abc) from request is not valid. The parameter value should be format of type 'Edm.Int32'."); - } - - [Fact] - public void MatchForFunction_ReturnsBuiltParameters() - { - // Arrange - EdmComplexType complex = new EdmComplexType("NS", "Address"); - complex.AddStructuralProperty("street", _StrType); - - EdmModel model = new EdmModel(); - EdmFunction function = new EdmFunction("NS", "MyFunction", _IntType, true, null, false); - function.AddParameter("name", _StrType); - function.AddParameter("title", _IntType); - function.AddOptionalParameter("address", new EdmComplexTypeReference(complex, false)); - model.AddElement(function); - - RouteValueDictionary routeValues = new RouteValueDictionary() - { - { "nameValue", "'abc'" }, - { "titleValue", "10001" }, - { "addressValue", "{\"street\":\"efg\" }" } - }; + // Act + Action test = () => SegmentTemplateHelpers.Match(context, function, parameterMappings); + ExceptionAssert.Throws(test, + "The parameter value (ef12abc) from request is not valid. The parameter value should be format of type 'Edm.Int32'."); + } - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValues, - Model = model - }; + [Fact] + public void MatchForFunction_ReturnsBuiltParameters() + { + // Arrange + EdmComplexType complex = new EdmComplexType("NS", "Address"); + complex.AddStructuralProperty("street", _StrType); + + EdmModel model = new EdmModel(); + EdmFunction function = new EdmFunction("NS", "MyFunction", _IntType, true, null, false); + function.AddParameter("name", _StrType); + function.AddParameter("title", _IntType); + function.AddOptionalParameter("address", new EdmComplexTypeReference(complex, false)); + model.AddElement(function); + + RouteValueDictionary routeValues = new RouteValueDictionary() + { + { "nameValue", "'abc'" }, + { "titleValue", "10001" }, + { "addressValue", "{\"street\":\"efg\" }" } + }; - IDictionary parameterMappings = new Dictionary - { - { "name", "nameValue" }, - { "title", "titleValue" }, - { "address", "addressValue" }, - }; - - // Act - IList parameters = SegmentTemplateHelpers.Match(context, function, parameterMappings); - - // Assert - Assert.Collection(parameters, - e => - { - Assert.Equal("name", e.Name); - Assert.Equal("abc", e.Value); - }, - e => - { - Assert.Equal("title", e.Name); - Assert.Equal(10001, e.Value); - }, - e => - { - Assert.Equal("address", e.Name); - Assert.Equal("{\"street\":\"efg\" }", e.Value); - }); - } - - [Theory] - [InlineData("'Green'")] - [InlineData("NS.Color'Green'")] - public void MatchForFunction_ReturnsBuiltEnumParameters(string enumValue) - { - // Arrange - EdmModel model = new EdmModel(); - EdmEnumType enumType = new EdmEnumType("NS", "Color"); - enumType.AddMember("Red", new EdmEnumMemberValue(1)); - enumType.AddMember("Green", new EdmEnumMemberValue(2)); - model.AddElement(enumType); - - var intType = EdmCoreModel.Instance.GetInt32(false); - EdmFunction function = new EdmFunction("NS", "MyFunction", intType); - function.AddParameter("favoriateColor", new EdmEnumTypeReference(enumType, false)); - model.AddElement(function); - - RouteValueDictionary routeValues = new RouteValueDictionary() - { - { "favoriateColorValue", $"{enumValue}" } - }; + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + { + RouteValues = routeValues, + Model = model + }; - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - RouteValues = routeValues, - Model = model - }; + IDictionary parameterMappings = new Dictionary + { + { "name", "nameValue" }, + { "title", "titleValue" }, + { "address", "addressValue" }, + }; + + // Act + IList parameters = SegmentTemplateHelpers.Match(context, function, parameterMappings); + + // Assert + Assert.Collection(parameters, + e => + { + Assert.Equal("name", e.Name); + Assert.Equal("abc", e.Value); + }, + e => + { + Assert.Equal("title", e.Name); + Assert.Equal(10001, e.Value); + }, + e => + { + Assert.Equal("address", e.Name); + Assert.Equal("{\"street\":\"efg\" }", e.Value); + }); + } - IDictionary parameterMappings = new Dictionary - { - { "favoriateColor", "favoriateColorValue" } - }; - - // Act - IList parameters = SegmentTemplateHelpers.Match(context, function, parameterMappings); - - // Assert - OperationSegmentParameter operationParameter = Assert.Single(parameters); - Assert.Equal("favoriateColor", operationParameter.Name); - ODataEnumValue oDataEnumValue = Assert.IsType(operationParameter.Value); - Assert.Equal("2", oDataEnumValue.Value); - } - - [Fact] - public void MatchForFunction_ReturnsBuiltParameters_ParameterAlias() - { - // Arrange - var strType = EdmCoreModel.Instance.GetString(false); - EdmModel model = new EdmModel(); - EdmFunction function = new EdmFunction("NS", "MyFunction", strType, true, null, false); - function.AddParameter("name", strType); - model.AddElement(function); - - RouteValueDictionary routeValues = new RouteValueDictionary() - { - { "nameValue", "@p" } - }; + [Theory] + [InlineData("'Green'")] + [InlineData("NS.Color'Green'")] + public void MatchForFunction_ReturnsBuiltEnumParameters(string enumValue) + { + // Arrange + EdmModel model = new EdmModel(); + EdmEnumType enumType = new EdmEnumType("NS", "Color"); + enumType.AddMember("Red", new EdmEnumMemberValue(1)); + enumType.AddMember("Green", new EdmEnumMemberValue(2)); + model.AddElement(enumType); + + var intType = EdmCoreModel.Instance.GetInt32(false); + EdmFunction function = new EdmFunction("NS", "MyFunction", intType); + function.AddParameter("favoriateColor", new EdmEnumTypeReference(enumType, false)); + model.AddElement(function); + + RouteValueDictionary routeValues = new RouteValueDictionary() + { + { "favoriateColorValue", $"{enumValue}" } + }; - HttpContext httpContext = new DefaultHttpContext(); - httpContext.Request.QueryString = new QueryString("?@p='abc'"); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext - { - HttpContext = httpContext, - RouteValues = routeValues, - Model = model - }; + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + { + RouteValues = routeValues, + Model = model + }; - IDictionary parameterMappings = new Dictionary - { - { "name", "nameValue" } - }; + IDictionary parameterMappings = new Dictionary + { + { "favoriateColor", "favoriateColorValue" } + }; - // Act - IList parameters = SegmentTemplateHelpers.Match(context, function, parameterMappings); + // Act + IList parameters = SegmentTemplateHelpers.Match(context, function, parameterMappings); - // Assert - OperationSegmentParameter functionParameter = Assert.Single(parameters); - Assert.Equal("name", functionParameter.Name); - Assert.Equal("abc", functionParameter.Value); - } + // Assert + OperationSegmentParameter operationParameter = Assert.Single(parameters); + Assert.Equal("favoriateColor", operationParameter.Name); + ODataEnumValue oDataEnumValue = Assert.IsType(operationParameter.Value); + Assert.Equal("2", oDataEnumValue.Value); + } - [Fact] - public void IsMatchParameters_ReturnsCorrect_ForDifferentParameters() + [Fact] + public void MatchForFunction_ReturnsBuiltParameters_ParameterAlias() + { + // Arrange + var strType = EdmCoreModel.Instance.GetString(false); + EdmModel model = new EdmModel(); + EdmFunction function = new EdmFunction("NS", "MyFunction", strType, true, null, false); + function.AddParameter("name", strType); + model.AddElement(function); + + RouteValueDictionary routeValues = new RouteValueDictionary() { - // Arrange - RouteValueDictionary routeValues = new RouteValueDictionary() - { - { "p1", "a" }, - { "p2", "b,p3=c" }, - }; + { "nameValue", "@p" } + }; - // 1) Act & Assert - IDictionary parameterMappings = new Dictionary - { - { "p1", "p1" }, - { "p2", "p2" }, - }; - Assert.False(SegmentTemplateHelpers.IsMatchParameters(routeValues, parameterMappings)); + HttpContext httpContext = new DefaultHttpContext(); + httpContext.Request.QueryString = new QueryString("?@p='abc'"); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext + { + HttpContext = httpContext, + RouteValues = routeValues, + Model = model + }; - // 2) Act & Assert - parameterMappings = new Dictionary - { - { "p1", "p1" }, - { "p2", "p2" }, - { "p3", "p3" }, - }; + IDictionary parameterMappings = new Dictionary + { + { "name", "nameValue" } + }; - Assert.True(SegmentTemplateHelpers.IsMatchParameters(routeValues, parameterMappings)); + // Act + IList parameters = SegmentTemplateHelpers.Match(context, function, parameterMappings); - // 3) Act & Assert - parameterMappings = new Dictionary - { - { "Name", "name" } - }; - Assert.False(SegmentTemplateHelpers.IsMatchParameters(routeValues, parameterMappings)); + // Assert + OperationSegmentParameter functionParameter = Assert.Single(parameters); + Assert.Equal("name", functionParameter.Name); + Assert.Equal("abc", functionParameter.Value); + } - // 4) Act & Assert - parameterMappings = new Dictionary(); - Assert.True(SegmentTemplateHelpers.IsMatchParameters(routeValues, parameterMappings)); - } + [Fact] + public void IsMatchParameters_ReturnsCorrect_ForDifferentParameters() + { + // Arrange + RouteValueDictionary routeValues = new RouteValueDictionary() + { + { "p1", "a" }, + { "p2", "b,p3=c" }, + }; - [Fact] - public void IsMatchParameters_ReturnsCorrect_ForWrongParameterFormatter() + // 1) Act & Assert + IDictionary parameterMappings = new Dictionary { - // Arrange - RouteValueDictionary routeValues = new RouteValueDictionary() - { - { "p1", "b,p2===c" }, - }; + { "p1", "p1" }, + { "p2", "p2" }, + }; + Assert.False(SegmentTemplateHelpers.IsMatchParameters(routeValues, parameterMappings)); - // Act - IDictionary parameterMappings = new Dictionary - { - { "p1", "p1" }, - { "p2", "p2" }, - }; + // 2) Act & Assert + parameterMappings = new Dictionary + { + { "p1", "p1" }, + { "p2", "p2" }, + { "p3", "p3" }, + }; + + Assert.True(SegmentTemplateHelpers.IsMatchParameters(routeValues, parameterMappings)); + + // 3) Act & Assert + parameterMappings = new Dictionary + { + { "Name", "name" } + }; + Assert.False(SegmentTemplateHelpers.IsMatchParameters(routeValues, parameterMappings)); + + // 4) Act & Assert + parameterMappings = new Dictionary(); + Assert.True(SegmentTemplateHelpers.IsMatchParameters(routeValues, parameterMappings)); + } + + [Fact] + public void IsMatchParameters_ReturnsCorrect_ForWrongParameterFormatter() + { + // Arrange + RouteValueDictionary routeValues = new RouteValueDictionary() + { + { "p1", "b,p2===c" }, + }; + + // Act + IDictionary parameterMappings = new Dictionary + { + { "p1", "p1" }, + { "p2", "p2" }, + }; - // Assert - Assert.False(SegmentTemplateHelpers.IsMatchParameters(routeValues, parameterMappings)); - } + // Assert + Assert.False(SegmentTemplateHelpers.IsMatchParameters(routeValues, parameterMappings)); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/SingletonSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/SingletonSegmentTemplateTests.cs index 66312844f..b30c7bc64 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/SingletonSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/SingletonSegmentTemplateTests.cs @@ -12,84 +12,83 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class SingletonSegmentTemplateTests { - public class SingletonSegmentTemplateTests + [Fact] + public void CtorSingletonSegmentTemplate_ThrowsArgumentNull_Singleton() { - [Fact] - public void CtorSingletonSegmentTemplate_ThrowsArgumentNull_Singleton() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new SingletonSegmentTemplate(singleton: null), "singleton"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new SingletonSegmentTemplate(singleton: null), "singleton"); + } - [Fact] - public void CtorSingletonSegmentTemplate_ThrowsArgumentNull_Segment() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new SingletonSegmentTemplate(segment: null), "segment"); - } + [Fact] + public void CtorSingletonSegmentTemplate_ThrowsArgumentNull_Segment() + { + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new SingletonSegmentTemplate(segment: null), "segment"); + } - [Fact] - public void CtorSingletonSegmentTemplate_SetsProperties() - { - // Arrange & Act - EdmEntityType entityType = new EdmEntityType("NS", "entity"); - IEdmEntityContainer container = new EdmEntityContainer("NS", "default"); - IEdmSingleton singleton = new EdmSingleton(container, "singleton", entityType); - SingletonSegmentTemplate singletonSegment = new SingletonSegmentTemplate(singleton); + [Fact] + public void CtorSingletonSegmentTemplate_SetsProperties() + { + // Arrange & Act + EdmEntityType entityType = new EdmEntityType("NS", "entity"); + IEdmEntityContainer container = new EdmEntityContainer("NS", "default"); + IEdmSingleton singleton = new EdmSingleton(container, "singleton", entityType); + SingletonSegmentTemplate singletonSegment = new SingletonSegmentTemplate(singleton); - // Assert - Assert.NotNull(singletonSegment.Segment); - Assert.Same(singleton, singletonSegment.Singleton); - } + // Assert + Assert.NotNull(singletonSegment.Segment); + Assert.Same(singleton, singletonSegment.Singleton); + } - [Fact] - public void GetTemplatesSingletonSegmentTemplate_ReturnsTemplates() - { - // Assert - EdmEntityType entityType = new EdmEntityType("NS", "entity"); - IEdmEntityContainer container = new EdmEntityContainer("NS", "default"); - IEdmSingleton singleton = new EdmSingleton(container, "singleton", entityType); - SingletonSegmentTemplate singletonSegment = new SingletonSegmentTemplate(singleton); + [Fact] + public void GetTemplatesSingletonSegmentTemplate_ReturnsTemplates() + { + // Assert + EdmEntityType entityType = new EdmEntityType("NS", "entity"); + IEdmEntityContainer container = new EdmEntityContainer("NS", "default"); + IEdmSingleton singleton = new EdmSingleton(container, "singleton", entityType); + SingletonSegmentTemplate singletonSegment = new SingletonSegmentTemplate(singleton); - // Act & Assert - IEnumerable templates = singletonSegment.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/singleton", template); - } + // Act & Assert + IEnumerable templates = singletonSegment.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/singleton", template); + } - [Fact] - public void TryTranslateSingletonSegmentTemplate_ThrowsArgumentNull_Context() - { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "entity"); - IEdmEntityContainer container = new EdmEntityContainer("NS", "default"); - IEdmSingleton singleton = new EdmSingleton(container, "singleton", entityType); - SingletonSegmentTemplate singletonSegment = new SingletonSegmentTemplate(singleton); + [Fact] + public void TryTranslateSingletonSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "entity"); + IEdmEntityContainer container = new EdmEntityContainer("NS", "default"); + IEdmSingleton singleton = new EdmSingleton(container, "singleton", entityType); + SingletonSegmentTemplate singletonSegment = new SingletonSegmentTemplate(singleton); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => singletonSegment.TryTranslate(null), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => singletonSegment.TryTranslate(null), "context"); + } - [Fact] - public void TryTranslateSingletonSegmentTemplate_ReturnsSingletonSegment() - { - // Arrange - EdmEntityType entityType = new EdmEntityType("NS", "entity"); - IEdmEntityContainer container = new EdmEntityContainer("NS", "default"); - IEdmSingleton singleton = new EdmSingleton(container, "singleton", entityType); + [Fact] + public void TryTranslateSingletonSegmentTemplate_ReturnsSingletonSegment() + { + // Arrange + EdmEntityType entityType = new EdmEntityType("NS", "entity"); + IEdmEntityContainer container = new EdmEntityContainer("NS", "default"); + IEdmSingleton singleton = new EdmSingleton(container, "singleton", entityType); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - SingletonSegmentTemplate template = new SingletonSegmentTemplate(new SingletonSegment(singleton)); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + SingletonSegmentTemplate template = new SingletonSegmentTemplate(new SingletonSegment(singleton)); - // Act - Assert.True(template.TryTranslate(context)); + // Act + Assert.True(template.TryTranslate(context)); - // Assert - ODataPathSegment segment = Assert.Single(context.Segments); - SingletonSegment singletonTemplate = Assert.IsType(segment); - Assert.Same(singleton, singletonTemplate.Singleton); - } + // Assert + ODataPathSegment segment = Assert.Single(context.Segments); + SingletonSegment singletonTemplate = Assert.IsType(segment); + Assert.Same(singleton, singletonTemplate.Singleton); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ValueSegmentTemplateTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ValueSegmentTemplateTests.cs index 025a968fb..fa1add6b9 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ValueSegmentTemplateTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Routing/Template/ValueSegmentTemplateTests.cs @@ -12,62 +12,61 @@ using Microsoft.OData.UriParser; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Routing.Template +namespace Microsoft.AspNetCore.OData.Tests.Routing.Template; + +public class ValueSegmentTemplateTests { - public class ValueSegmentTemplateTests + [Fact] + public void CtorValueSegmentTemplate_ThrowsArgumentNull_Segment() { - [Fact] - public void CtorValueSegmentTemplate_ThrowsArgumentNull_Segment() - { - // Arrange & Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => new ValueSegmentTemplate(segment: null), "segment"); - } + // Arrange & Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => new ValueSegmentTemplate(segment: null), "segment"); + } - [Fact] - public void GetTemplatesValueSegmentTemplate_ReturnsTemplates() - { - // Arrange - IEdmPrimitiveType primitive = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); - ValueSegmentTemplate valueSegment = new ValueSegmentTemplate(primitive); + [Fact] + public void GetTemplatesValueSegmentTemplate_ReturnsTemplates() + { + // Arrange + IEdmPrimitiveType primitive = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); + ValueSegmentTemplate valueSegment = new ValueSegmentTemplate(primitive); - // Act & Assert - IEnumerable templates = valueSegment.GetTemplates(); - string template = Assert.Single(templates); - Assert.Equal("/$value", template); - } + // Act & Assert + IEnumerable templates = valueSegment.GetTemplates(); + string template = Assert.Single(templates); + Assert.Equal("/$value", template); + } - [Fact] - public void TryTranslateValueSegmentTemplate_ThrowsArgumentNull_Context() - { - // Arrange - IEdmPrimitiveType primitive = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - ValueSegmentTemplate valueSegment = new ValueSegmentTemplate(primitive); + [Fact] + public void TryTranslateValueSegmentTemplate_ThrowsArgumentNull_Context() + { + // Arrange + IEdmPrimitiveType primitive = EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Int32); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + ValueSegmentTemplate valueSegment = new ValueSegmentTemplate(primitive); - // Act & Assert - ExceptionAssert.ThrowsArgumentNull(() => valueSegment.TryTranslate(null), "context"); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => valueSegment.TryTranslate(null), "context"); + } - [Theory] - [InlineData(EdmPrimitiveTypeKind.Int32)] - [InlineData(EdmPrimitiveTypeKind.String)] - [InlineData(EdmPrimitiveTypeKind.Double)] - [InlineData(EdmPrimitiveTypeKind.Guid)] - [InlineData(EdmPrimitiveTypeKind.Date)] - public void TryTranslateValueSegmentTemplate_ReturnsValueSegment(EdmPrimitiveTypeKind kind) - { - // Arrange - IEdmPrimitiveType primitive = EdmCoreModel.Instance.GetPrimitiveType(kind); - ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); - ValueSegmentTemplate valueSegment = new ValueSegmentTemplate(primitive); + [Theory] + [InlineData(EdmPrimitiveTypeKind.Int32)] + [InlineData(EdmPrimitiveTypeKind.String)] + [InlineData(EdmPrimitiveTypeKind.Double)] + [InlineData(EdmPrimitiveTypeKind.Guid)] + [InlineData(EdmPrimitiveTypeKind.Date)] + public void TryTranslateValueSegmentTemplate_ReturnsValueSegment(EdmPrimitiveTypeKind kind) + { + // Arrange + IEdmPrimitiveType primitive = EdmCoreModel.Instance.GetPrimitiveType(kind); + ODataTemplateTranslateContext context = new ODataTemplateTranslateContext(); + ValueSegmentTemplate valueSegment = new ValueSegmentTemplate(primitive); - // Act - Assert.True(valueSegment.TryTranslate(context)); + // Act + Assert.True(valueSegment.TryTranslate(context)); - // Assert - ODataPathSegment segment = Assert.Single(context.Segments); - ValueSegment odataValueSegment = Assert.IsType(segment); - Assert.Same(primitive, odataValueSegment.EdmType); - } + // Assert + ODataPathSegment segment = Assert.Single(context.Segments); + ValueSegment odataValueSegment = Assert.IsType(segment); + Assert.Same(primitive, odataValueSegment.EdmType); } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Scenarios/OpenComplexTypeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Scenarios/OpenComplexTypeTests.cs index d444dad6c..dbb2d5d83 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Scenarios/OpenComplexTypeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Scenarios/OpenComplexTypeTests.cs @@ -23,650 +23,649 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Scenarios +namespace Microsoft.AspNetCore.OData.Tests.Scenarios; + +public class OpenComplexTypeTests { - public class OpenComplexTypeTests + [Fact] + public async Task OpenComplexType_SimpleSerialization() { - [Fact] - public async Task OpenComplexType_SimpleSerialization() - { - // Arrange - const string RequestUri = "http://localhost/odata/OpenCustomers(2)/Address"; - HttpClient client = GetClient(); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, RequestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal("http://localhost/odata/$metadata#OpenCustomers(2)/Address", result["@odata.context"]); - Assert.Equal("Street 2", result["Street"]); - Assert.Equal("City 2", result["City"]); - Assert.Equal("300", result["IntProp"]); - Assert.Equal("My Dynamic Place", result["Place"]); - Assert.Equal("2c1f450a-a2a7-4fe1-a25d-4d9332fc0694", result["Token"]); - Assert.Equal("2015-03-02", result["Birthday"]); - Assert.Equal(JTokenType.Null, result["Region"].Type); - } + // Arrange + const string RequestUri = "http://localhost/odata/OpenCustomers(2)/Address"; + HttpClient client = GetClient(); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, RequestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("http://localhost/odata/$metadata#OpenCustomers(2)/Address", result["@odata.context"]); + Assert.Equal("Street 2", result["Street"]); + Assert.Equal("City 2", result["City"]); + Assert.Equal("300", result["IntProp"]); + Assert.Equal("My Dynamic Place", result["Place"]); + Assert.Equal("2c1f450a-a2a7-4fe1-a25d-4d9332fc0694", result["Token"]); + Assert.Equal("2015-03-02", result["Birthday"]); + Assert.Equal(JTokenType.Null, result["Region"].Type); + } - [Fact] - public async Task OpenComplexType_SimpleDeserialization() - { - // Arrange - const string Payload = "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#OpenCustomers/$entity\"," + - "\"CustomerId\":6,\"Name\":\"FirstName 6\"," + - "\"Address\":{" + - "\"Street\":\"Street 6\",\"City\":\"City 6\",\"Place@odata.type\":\"#String\",\"Place\":\"Earth\",\"Token@odata.type\":\"#Guid\"," + - "\"Token\":\"4DB52263-4382-4BCB-A63E-3129C1B5FA0D\"," + - "\"Number@odata.type\":\"#Int32\"," + - "\"Number\":990," + - "\"BirthTime@odata.type\":\"#TimeOfDay\"," + - "\"BirthTime\":\"11:12:13.0140000\"" + - "}" + - "}"; - - const string RequestUri = "http://localhost/odata/OpenCustomers"; - HttpClient client = GetClient(); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUri); - request.Content = new StringContentWithLength(Payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal("http://localhost/odata/$metadata#OpenCustomers/$entity", result["@odata.context"]); - Assert.Equal("Earth", result["Address"]["Place"]); - Assert.Equal(990, result["Address"]["Number"]); - Assert.Equal("4DB52263-4382-4BCB-A63E-3129C1B5FA0D".ToLower(), result["Address"]["Token"]); - Assert.Equal("11:12:13.0140000", result["Address"]["BirthTime"]); - } + [Fact] + public async Task OpenComplexType_SimpleDeserialization() + { + // Arrange + const string Payload = "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#OpenCustomers/$entity\"," + + "\"CustomerId\":6,\"Name\":\"FirstName 6\"," + + "\"Address\":{" + + "\"Street\":\"Street 6\",\"City\":\"City 6\",\"Place@odata.type\":\"#String\",\"Place\":\"Earth\",\"Token@odata.type\":\"#Guid\"," + + "\"Token\":\"4DB52263-4382-4BCB-A63E-3129C1B5FA0D\"," + + "\"Number@odata.type\":\"#Int32\"," + + "\"Number\":990," + + "\"BirthTime@odata.type\":\"#TimeOfDay\"," + + "\"BirthTime\":\"11:12:13.0140000\"" + + "}" + + "}"; + + const string RequestUri = "http://localhost/odata/OpenCustomers"; + HttpClient client = GetClient(); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUri); + request.Content = new StringContentWithLength(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("http://localhost/odata/$metadata#OpenCustomers/$entity", result["@odata.context"]); + Assert.Equal("Earth", result["Address"]["Place"]); + Assert.Equal(990, result["Address"]["Number"]); + Assert.Equal("4DB52263-4382-4BCB-A63E-3129C1B5FA0D".ToLower(), result["Address"]["Token"]); + Assert.Equal("11:12:13.0140000", result["Address"]["BirthTime"]); + } - [Fact] - public async Task OpenComplexType_PutComplexTypeProperty() - { - // Arrange - const string payload = "{" + - "\"Street\":\"UpdatedStreet\"," + - "\"City\":\"UpdatedCity\"," + - "\"Publish@odata.type\":\"#Date\"," + - "\"Publish\":\"2016-02-02\"," + - "\"LineA\": {" + - "\"Description\": \"DescLineA.\"," + - "\"Fee\" : 0," + - "\"PhoneInfo\" : {" + - "\"ContactName\" : \"ContactNameA\"," + - "\"PhoneNumber\" : 9876543," + - "\"Spec\" : { \"Make\" : \"Apple\",\"ScreenSize\" : 6 }" + - "}" + - "}," + - "\"LineB\": {" + - "\"Description\": \"DescLineB.\"," + - "\"Fee\" : 0," + - "\"PhoneInfo\" : {" + - "\"ContactName\" : \"ContactNameA\"," + - "\"PhoneNumber\" : 9876543," + - "\"Spec\" : { \"Make\" : \"Apple\",\"ScreenSize\" : 6 }" + - "}" + - "}" + - "}"; - - const string requestUri = "http://localhost/odata/OpenCustomers(1)/Address"; - HttpClient client = GetClient(); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, requestUri); - request.Content = new StringContentWithLength(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } + [Fact] + public async Task OpenComplexType_PutComplexTypeProperty() + { + // Arrange + const string payload = "{" + + "\"Street\":\"UpdatedStreet\"," + + "\"City\":\"UpdatedCity\"," + + "\"Publish@odata.type\":\"#Date\"," + + "\"Publish\":\"2016-02-02\"," + + "\"LineA\": {" + + "\"Description\": \"DescLineA.\"," + + "\"Fee\" : 0," + + "\"PhoneInfo\" : {" + + "\"ContactName\" : \"ContactNameA\"," + + "\"PhoneNumber\" : 9876543," + + "\"Spec\" : { \"Make\" : \"Apple\",\"ScreenSize\" : 6 }" + + "}" + + "}," + + "\"LineB\": {" + + "\"Description\": \"DescLineB.\"," + + "\"Fee\" : 0," + + "\"PhoneInfo\" : {" + + "\"ContactName\" : \"ContactNameA\"," + + "\"PhoneNumber\" : 9876543," + + "\"Spec\" : { \"Make\" : \"Apple\",\"ScreenSize\" : 6 }" + + "}" + + "}" + + "}"; + + const string requestUri = "http://localhost/odata/OpenCustomers(1)/Address"; + HttpClient client = GetClient(); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, requestUri); + request.Content = new StringContentWithLength(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } - [Fact] - public async Task OpenComplexType_PatchComplexTypeProperty() - { - string payload = "{" + - "\"Street\":\"UpdatedStreet\"," + - "\"Token@odata.type\":\"#Guid\"," + - "\"Token\":\"2E724E81-8462-4BA0-B920-DC87A61C8EA3\"," + - "\"BirthDay@odata.type\":\"#Date\"," + - "\"BirthDay\":\"2016-01-29\"" + - "}"; - - await ExecutePatchRequest(payload); - } + [Fact] + public async Task OpenComplexType_PatchComplexTypeProperty() + { + string payload = "{" + + "\"Street\":\"UpdatedStreet\"," + + "\"Token@odata.type\":\"#Guid\"," + + "\"Token\":\"2E724E81-8462-4BA0-B920-DC87A61C8EA3\"," + + "\"BirthDay@odata.type\":\"#Date\"," + + "\"BirthDay\":\"2016-01-29\"" + + "}"; + + await ExecutePatchRequest(payload); + } - [Fact] - public async Task OpenComplexType_PatchNestedComplexTypeProperty() - { - string payload = @"{ - ""Street"":""UpdatedStreet"", - ""Token@odata.type"":""#Guid"", - ""Token"":""9B198CA0-9546-4162-A4C0-14EAA255ACA7"", - ""BirthDay@odata.type"":""#Date"", - ""BirthDay"":""2016-01-29"", - ""LineB"": {""Description"": ""DescriptionB""} - }"; - - await ExecutePatchRequest(payload); - } + [Fact] + public async Task OpenComplexType_PatchNestedComplexTypeProperty() + { + string payload = @"{ + ""Street"":""UpdatedStreet"", + ""Token@odata.type"":""#Guid"", + ""Token"":""9B198CA0-9546-4162-A4C0-14EAA255ACA7"", + ""BirthDay@odata.type"":""#Date"", + ""BirthDay"":""2016-01-29"", + ""LineB"": {""Description"": ""DescriptionB""} + }"; + + await ExecutePatchRequest(payload); + } - [Fact] - public async Task OpenComplexType_PatchComplexTypeDynamicProperty() - { - string payload = @"{ - ""Street"":""UpdatedStreet"", - ""Token@odata.type"":""#Guid"", - ""Token"":""250EFC6E-8FA4-4B68-951C-F1E26DE09D1D"", - ""BirthDay@odata.type"":""#Date"", - ""BirthDay"":""2016-01-29"", + [Fact] + public async Task OpenComplexType_PatchComplexTypeDynamicProperty() + { + string payload = @"{ + ""Street"":""UpdatedStreet"", + ""Token@odata.type"":""#Guid"", + ""Token"":""250EFC6E-8FA4-4B68-951C-F1E26DE09D1D"", + ""BirthDay@odata.type"":""#Date"", + ""BirthDay"":""2016-01-29"", + ""Telephone"":{ + ""@odata.type"":""#Microsoft.AspNetCore.OData.Tests.Scenarios.Phone"", + ""ContactName"":""ContactNameX"", + ""PhoneNumber"":13, + ""Spec"":{""ScreenSize"":7} + } + }"; + + await ExecutePatchRequest(payload); + } + + [Fact] + public async Task OpenComplexType_PatchComplexTypeDynamicProperty_Nested() + { + string payload = @"{ + ""Street"":""UpdatedStreet"", + ""Token@odata.type"":""#Guid"", + ""Token"":""40CEEEDE-031C-45CB-9E44-E6017D635814"", + ""BirthDay@odata.type"":""#Date"", + ""BirthDay"":""2016-01-29"", + ""Building"":{ + ""@odata.type"":""#Microsoft.AspNetCore.OData.Tests.Scenarios.Building"", + ""BuildingName"":""BuildingNameY"", ""Telephone"":{ ""@odata.type"":""#Microsoft.AspNetCore.OData.Tests.Scenarios.Phone"", - ""ContactName"":""ContactNameX"", - ""PhoneNumber"":13, - ""Spec"":{""ScreenSize"":7} + ""ContactName"":""ContactNameZ"", + ""PhoneNumber"":17, + ""Spec"":{""ScreenSize"":5} } - }"; + } + }"; - await ExecutePatchRequest(payload); - } + await ExecutePatchRequest(payload); + } - [Fact] - public async Task OpenComplexType_PatchComplexTypeDynamicProperty_Nested() - { - string payload = @"{ - ""Street"":""UpdatedStreet"", - ""Token@odata.type"":""#Guid"", - ""Token"":""40CEEEDE-031C-45CB-9E44-E6017D635814"", - ""BirthDay@odata.type"":""#Date"", - ""BirthDay"":""2016-01-29"", - ""Building"":{ - ""@odata.type"":""#Microsoft.AspNetCore.OData.Tests.Scenarios.Building"", - ""BuildingName"":""BuildingNameY"", - ""Telephone"":{ - ""@odata.type"":""#Microsoft.AspNetCore.OData.Tests.Scenarios.Phone"", - ""ContactName"":""ContactNameZ"", - ""PhoneNumber"":17, - ""Spec"":{""ScreenSize"":5} - } - } - }"; + [Fact] + public async Task OpenComplexType_PatchNestedComplexTypeProperty_DoubleNested() + { + string payload = @"{ + ""Street"":""UpdatedStreet"", + ""Token@odata.type"":""#Guid"", + ""Token"":""A4D09554-5551-4B36-A1CB-CFBCDB1F4EAD"", + ""BirthDay@odata.type"":""#Date"", + ""BirthDay"":""2016-01-29"", + ""LineA"": { + ""Description"": ""DescriptionA"", + ""PhoneInfo"" : {""ContactName"":""ContactNameA"", ""PhoneNumber"": 7654321} + }, + ""LineB"": { + ""PhoneInfo"": {""ContactName"": ""ContactNameB""} + } + }"; - await ExecutePatchRequest(payload); - } + await ExecutePatchRequest(payload); + } - [Fact] - public async Task OpenComplexType_PatchNestedComplexTypeProperty_DoubleNested() - { - string payload = @"{ - ""Street"":""UpdatedStreet"", - ""Token@odata.type"":""#Guid"", - ""Token"":""A4D09554-5551-4B36-A1CB-CFBCDB1F4EAD"", - ""BirthDay@odata.type"":""#Date"", - ""BirthDay"":""2016-01-29"", - ""LineA"": { - ""Description"": ""DescriptionA"", - ""PhoneInfo"" : {""ContactName"":""ContactNameA"", ""PhoneNumber"": 7654321} - }, - ""LineB"": { - ""PhoneInfo"": {""ContactName"": ""ContactNameB""} + [Fact] + public async Task OpenComplexType_PatchNestedComplexTypeProperty_DeepNestedResourceOnNewSubNode() + { + // Arrange + string payload = @"{ + ""Street"":""UpdatedStreet"", + ""Token@odata.type"":""#Guid"", + ""Token"":""2D071BD4-E4FB-4639-8024-BBC173850441"", + ""BirthDay@odata.type"":""#Date"", + ""BirthDay"":""2016-01-29"", + ""LineA"": { + ""Description"": ""LineDetailsWithNewDeepSubNode."", + ""PhoneInfo"" : { + ""ContactName"":""ContactNameA"", + ""Spec"" : { ""ScreenSize"" : 6 } } - }"; + } + }"; - await ExecutePatchRequest(payload); - } + await ExecutePatchRequest(payload); + } - [Fact] - public async Task OpenComplexType_PatchNestedComplexTypeProperty_DeepNestedResourceOnNewSubNode() - { - // Arrange - string payload = @"{ - ""Street"":""UpdatedStreet"", - ""Token@odata.type"":""#Guid"", - ""Token"":""2D071BD4-E4FB-4639-8024-BBC173850441"", - ""BirthDay@odata.type"":""#Date"", - ""BirthDay"":""2016-01-29"", - ""LineA"": { - ""Description"": ""LineDetailsWithNewDeepSubNode."", - ""PhoneInfo"" : { - ""ContactName"":""ContactNameA"", - ""Spec"" : { ""ScreenSize"" : 6 } - } - } - }"; + [Fact] + public async Task OpenComplexType_PatchNestedComplexTypeProperty_GetInstance() + { + string payload = @"{ + ""Street"":""UpdatedStreet"", + ""Token@odata.type"":""#Guid"", + ""Token"":""3CA243CF-460A-4144-B6EB-F5E1180ABDC8"", + ""BirthDay@odata.type"":""#Date"", + ""BirthDay"":""2016-01-29"", + ""LineA"": { + ""Description"": ""DescriptionA"", + ""PhoneInfo"" : {""ContactName"":""ContactNameA"", ""PhoneNumber"": 7654321} + } + }"; - await ExecutePatchRequest(payload); - } + await ExecutePatchRequest(payload); + } - [Fact] - public async Task OpenComplexType_PatchNestedComplexTypeProperty_GetInstance() - { - string payload = @"{ - ""Street"":""UpdatedStreet"", - ""Token@odata.type"":""#Guid"", - ""Token"":""3CA243CF-460A-4144-B6EB-F5E1180ABDC8"", - ""BirthDay@odata.type"":""#Date"", - ""BirthDay"":""2016-01-29"", - ""LineA"": { - ""Description"": ""DescriptionA"", - ""PhoneInfo"" : {""ContactName"":""ContactNameA"", ""PhoneNumber"": 7654321} - } - }"; + private async Task ExecutePatchRequest(string payload) + { + // Arrange + payload = Regex.Replace(payload, @"\s*", "", RegexOptions.Multiline); - await ExecutePatchRequest(payload); - } + const string requestUri = "http://localhost/odata/OpenCustomers(1)/Address"; + HttpClient client = GetClient(); - private async Task ExecutePatchRequest(string payload) - { - // Arrange - payload = Regex.Replace(payload, @"\s*", "", RegexOptions.Multiline); + // Act + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("Patch"), requestUri); + request.Content = new StringContentWithLength(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - const string requestUri = "http://localhost/odata/OpenCustomers(1)/Address"; - HttpClient client = GetClient(); + HttpResponseMessage response = await client.SendAsync(request); - // Act - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("Patch"), requestUri); - request.Content = new StringContentWithLength(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } - HttpResponseMessage response = await client.SendAsync(request); + [Fact] + public async Task OpenComplexType_DeleteComplexTypeProperty() + { + // Arrange + const string requestUri = "http://localhost/odata/OpenCustomers(1)/Address"; + HttpClient client = GetClient(); - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, requestUri); + HttpResponseMessage response = await client.SendAsync(request); - [Fact] - public async Task OpenComplexType_DeleteComplexTypeProperty() - { - // Arrange - const string requestUri = "http://localhost/odata/OpenCustomers(1)/Address"; - HttpClient client = GetClient(); + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, requestUri); - HttpResponseMessage response = await client.SendAsync(request); + private static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("OpenCustomers"); + builder.ComplexType(); + return builder.GetEdmModel(); + } - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } + private HttpClient GetClient() + { + var controllers = new[] { typeof(OpenCustomersController) }; - private static IEdmModel GetEdmModel() - { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("OpenCustomers"); - builder.ComplexType(); - return builder.GetEdmModel(); - } + var model = GetEdmModel(); + var server = TestServerUtils.Create(opt => opt.AddRouteComponents("odata", model), typeof(OpenCustomersController)); + return server.CreateClient(); + } +} - private HttpClient GetClient() - { - var controllers = new[] { typeof(OpenCustomersController) }; +// Controller +public class OpenCustomersController : ODataController +{ + private static IList CreateCustomers() + { + int[] IntValues = { 200, 100, 300, 0, 400 }; + IList customers = Enumerable.Range(0, 5).Select(i => + new OpenCustomer + { + CustomerId = i, + Name = "FirstName " + i, + Address = new OpenAddress + { + Street = "Street " + i, + City = "City " + i, + DynamicProperties = new Dictionary { { "IntProp", IntValues[i] } }, + LineA = null, // Leaving LineA as null + LineB = new LineDetails() { Fee = LineDetails.DefaultValue_Fee } + } + }).ToList(); - var model = GetEdmModel(); - var server = TestServerUtils.Create(opt => opt.AddRouteComponents("odata", model), typeof(OpenCustomersController)); - return server.CreateClient(); - } + return customers; } - // Controller - public class OpenCustomersController : ODataController + public IActionResult GetAddress(int key) { - private static IList CreateCustomers() + IList customers = CreateCustomers(); + OpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); + if (customer == null) { - int[] IntValues = { 200, 100, 300, 0, 400 }; - IList customers = Enumerable.Range(0, 5).Select(i => - new OpenCustomer - { - CustomerId = i, - Name = "FirstName " + i, - Address = new OpenAddress - { - Street = "Street " + i, - City = "City " + i, - DynamicProperties = new Dictionary { { "IntProp", IntValues[i] } }, - LineA = null, // Leaving LineA as null - LineB = new LineDetails() { Fee = LineDetails.DefaultValue_Fee } - } - }).ToList(); - - return customers; + return NotFound(); } - public IActionResult GetAddress(int key) - { - IList customers = CreateCustomers(); - OpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); - if (customer == null) - { - return NotFound(); - } + OpenAddress address = customer.Address; - OpenAddress address = customer.Address; + // Add more dynamic properties + address.DynamicProperties.Add("Place", "My Dynamic Place"); + address.DynamicProperties.Add("Token", new Guid("2C1F450A-A2A7-4FE1-A25D-4D9332FC0694")); + address.DynamicProperties.Add("Birthday", new Date(2015, 3, 2)); + address.DynamicProperties.Add("Region", null); + return Ok(address); + } - // Add more dynamic properties - address.DynamicProperties.Add("Place", "My Dynamic Place"); - address.DynamicProperties.Add("Token", new Guid("2C1F450A-A2A7-4FE1-A25D-4D9332FC0694")); - address.DynamicProperties.Add("Birthday", new Date(2015, 3, 2)); - address.DynamicProperties.Add("Region", null); - return Ok(address); - } + public IActionResult PostOpenCustomer([FromBody] OpenCustomer customer) + { + // Verify there is a string dynamic property + object countryValue; + customer.Address.DynamicProperties.TryGetValue("Place", out countryValue); + Assert.NotNull(countryValue); + Assert.IsType(countryValue); + Assert.Equal("Earth", countryValue); + + // Verify there is an int dynamic property + object numberValue; + customer.Address.DynamicProperties.TryGetValue("Number", out numberValue); + Assert.NotNull(numberValue); + Assert.IsType(numberValue); + Assert.Equal(990, numberValue); + + // Verify there is a Guid dynamic property + object tokenValue; + customer.Address.DynamicProperties.TryGetValue("Token", out tokenValue); + Assert.NotNull(tokenValue); + Assert.IsType(tokenValue); + Assert.Equal(new Guid("4DB52263-4382-4BCB-A63E-3129C1B5FA0D"), tokenValue); + + // Verify there is a TimeOfDay dynamic property + object timeOfDayValue; + customer.Address.DynamicProperties.TryGetValue("BirthTime", out timeOfDayValue); + Assert.NotNull(timeOfDayValue); + Assert.IsType(timeOfDayValue); + Assert.Equal(new TimeOfDay(11, 12, 13, 14), timeOfDayValue); + + return Ok(customer); + } - public IActionResult PostOpenCustomer([FromBody] OpenCustomer customer) + public IActionResult PutToAddress(int key, Delta address) + { + IList customers = CreateCustomers(); + OpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); + if (customer == null) { - // Verify there is a string dynamic property - object countryValue; - customer.Address.DynamicProperties.TryGetValue("Place", out countryValue); - Assert.NotNull(countryValue); - Assert.IsType(countryValue); - Assert.Equal("Earth", countryValue); - - // Verify there is an int dynamic property - object numberValue; - customer.Address.DynamicProperties.TryGetValue("Number", out numberValue); - Assert.NotNull(numberValue); - Assert.IsType(numberValue); - Assert.Equal(990, numberValue); - - // Verify there is a Guid dynamic property - object tokenValue; - customer.Address.DynamicProperties.TryGetValue("Token", out tokenValue); - Assert.NotNull(tokenValue); - Assert.IsType(tokenValue); - Assert.Equal(new Guid("4DB52263-4382-4BCB-A63E-3129C1B5FA0D"), tokenValue); - - // Verify there is a TimeOfDay dynamic property - object timeOfDayValue; - customer.Address.DynamicProperties.TryGetValue("BirthTime", out timeOfDayValue); - Assert.NotNull(timeOfDayValue); - Assert.IsType(timeOfDayValue); - Assert.Equal(new TimeOfDay(11, 12, 13, 14), timeOfDayValue); - - return Ok(customer); + return NotFound(); } - public IActionResult PutToAddress(int key, Delta address) - { - IList customers = CreateCustomers(); - OpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); - if (customer == null) - { - return NotFound(); - } - - // Verify the origin address - OpenAddress origin = customer.Address; - VerifyOriginAddress(key, origin); + // Verify the origin address + OpenAddress origin = customer.Address; + VerifyOriginAddress(key, origin); - address.Put(origin); // Do put + address.Put(origin); // Do put - // Verify the put address - Assert.Equal("UpdatedStreet", origin.Street); - Assert.Equal("UpdatedCity", origin.City); + // Verify the put address + Assert.Equal("UpdatedStreet", origin.Street); + Assert.Equal("UpdatedCity", origin.City); - Assert.NotNull(origin.DynamicProperties); - KeyValuePair dynamicProperty = Assert.Single(origin.DynamicProperties); // only one - Assert.Equal("Publish", dynamicProperty.Key); - Assert.Equal(new Date(2016, 2, 2), dynamicProperty.Value); + Assert.NotNull(origin.DynamicProperties); + KeyValuePair dynamicProperty = Assert.Single(origin.DynamicProperties); // only one + Assert.Equal("Publish", dynamicProperty.Key); + Assert.Equal(new Date(2016, 2, 2), dynamicProperty.Value); - return Updated(customer); - } + return Updated(customer); + } - public IActionResult PatchToAddress(int key, Delta address) + public IActionResult PatchToAddress(int key, Delta address) + { + IList customers = CreateCustomers(); + OpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); + if (customer == null) { - IList customers = CreateCustomers(); - OpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); - if (customer == null) - { - return NotFound(); - } - - // Verify the origin address - OpenAddress origin = customer.Address; - VerifyOriginAddress(key, origin); - - address.Patch(origin); // Do patch + return NotFound(); + } - // Verify the patched address - Assert.Equal("UpdatedStreet", origin.Street); - Assert.Equal("City " + key, origin.City); // not changed - Assert.NotNull(origin.DynamicProperties); + // Verify the origin address + OpenAddress origin = customer.Address; + VerifyOriginAddress(key, origin); - Assert.True(origin.DynamicProperties.Count >= 3); // Including the origin dynamic properties + address.Patch(origin); // Do patch - KeyValuePair dynamicPropertyBirthDay = origin.DynamicProperties.FirstOrDefault(e => e.Key == "BirthDay"); - Assert.Equal(new Date(2016, 1, 29), dynamicPropertyBirthDay.Value); + // Verify the patched address + Assert.Equal("UpdatedStreet", origin.Street); + Assert.Equal("City " + key, origin.City); // not changed + Assert.NotNull(origin.DynamicProperties); - string dynamicPropertyToken = origin.DynamicProperties.FirstOrDefault(e => e.Key == "Token") - .Value.ToString().ToUpperInvariant(); + Assert.True(origin.DynamicProperties.Count >= 3); // Including the origin dynamic properties - switch (dynamicPropertyToken) - { - case "2E724E81-8462-4BA0-B920-DC87A61C8EA3": - // All changed non-dynamic properties: ["Street"] - Assert.True(address.GetChangedPropertyNames().Count() == 1); - break; - - case "9B198CA0-9546-4162-A4C0-14EAA255ACA7": - - // All changed non-dynamic properties: ["Street", "LineB"] - Assert.True(address.GetChangedPropertyNames().Count() == 2); - - Assert.Null(origin.LineA); - - Assert.NotNull(origin.LineB); - Assert.Equal("DescriptionB", origin.LineB.Description); - - // Fee is not overwritten. - Assert.Equal(LineDetails.DefaultValue_Fee, origin.LineB.Fee); - break; - - case "A4D09554-5551-4B36-A1CB-CFBCDB1F4EAD": - - // All changed non-dynamic properties: ["Street", "LineA", "LineB"] - Assert.True(address.GetChangedPropertyNames().Count() == 3); - - // --- LineA --- - Assert.NotNull(origin.LineA); - Assert.Equal("DescriptionA", origin.LineA.Description); - - // LineA.Fee is left as uninitialized. - Assert.Equal(LineDetails.UninitializedValue_Fee, origin.LineA.Fee); - - Assert.NotNull(origin.LineA.PhoneInfo); - Assert.Equal("ContactNameA", origin.LineA.PhoneInfo.ContactName); - Assert.Equal(7654321, origin.LineA.PhoneInfo.PhoneNumber); - - // --- LineB --- - Assert.NotNull(origin.LineB); - Assert.Null(origin.LineB.Description); - - // LineB.Fee is originally initialized for OpenAddress is created for each customer. - Assert.Equal(LineDetails.DefaultValue_Fee, origin.LineB.Fee); - - Assert.NotNull(origin.LineB.PhoneInfo); - Assert.Equal("ContactNameB", origin.LineB.PhoneInfo.ContactName); - Assert.Equal(0, origin.LineB.PhoneInfo.PhoneNumber); - break; - - case "2D071BD4-E4FB-4639-8024-BBC173850441": - // All changed non-dynamic properties: ["Street", "LineA"] - Assert.True(address.GetChangedPropertyNames().Count() == 2); - - // --- LineA --- - Assert.NotNull(origin.LineA); - Assert.Equal("LineDetailsWithNewDeepSubNode.", origin.LineA.Description); - Assert.Equal(LineDetails.UninitializedValue_Fee, origin.LineA.Fee); - - Assert.NotNull(origin.LineA.PhoneInfo); - Assert.Equal("ContactNameA", origin.LineA.PhoneInfo.ContactName); - Assert.Equal(0, origin.LineA.PhoneInfo.PhoneNumber); - - Assert.NotNull(origin.LineA.PhoneInfo.Spec); - Assert.Null(origin.LineA.PhoneInfo.Spec.Make); - Assert.Equal(6, origin.LineA.PhoneInfo.Spec.ScreenSize); - - // --- LineB --- - Assert.NotNull(origin.LineB); - break; - - case "250EFC6E-8FA4-4B68-951C-F1E26DE09D1D": - Assert.True(origin.DynamicProperties.ContainsKey("Telephone")); - // Telephone dynamic property - Phone telephone = origin.DynamicProperties["Telephone"] as Phone; - Assert.NotNull(telephone); - Assert.Equal("ContactNameX", telephone.ContactName); - Assert.Equal(13, telephone.PhoneNumber); - - // Nested complex property - Assert.NotNull(telephone.Spec); - Assert.Equal(7, telephone.Spec.ScreenSize); - break; - - case "40CEEEDE-031C-45CB-9E44-E6017D635814": - Assert.True(origin.DynamicProperties.ContainsKey("Building")); - // Building dynamic property - Building building = origin.DynamicProperties["Building"] as Building; - Assert.NotNull(building); - Assert.Equal("BuildingNameY", building.BuildingName); - - // Nested telephone complex dynamic property - Assert.True(building.DynamicProperties.ContainsKey("Telephone")); - Phone phone = building.DynamicProperties["Telephone"] as Phone; - Assert.NotNull(phone); - Assert.Equal("ContactNameZ", phone.ContactName); - Assert.Equal(17, phone.PhoneNumber); - - // Nested complex property - Assert.NotNull(phone.Spec); - Assert.Equal(5, phone.Spec.ScreenSize); - break; - case "3CA243CF-460A-4144-B6EB-F5E1180ABDC8": - OpenAddress addressInstance = address.GetInstance(); - Assert.NotNull(addressInstance); - - // Complex property - Assert.NotNull(addressInstance.LineA); - Assert.NotNull(addressInstance.LineA.PhoneInfo); - Assert.Equal(7654321, addressInstance.LineA.PhoneInfo.PhoneNumber); - - object nestedLineAValue; - // Fetch LineA property using TryGetNestedPropertyValue - Assert.True(address.TryGetNestedPropertyValue("LineA", out nestedLineAValue)); - Delta deltaLineA = nestedLineAValue as Delta; - Assert.NotNull(deltaLineA); - - // Nested complex property - dynamic nestedLineA = deltaLineA; - Assert.NotNull(nestedLineA.PhoneInfo); - Assert.Equal(7654321, nestedLineA.PhoneInfo.PhoneNumber); - break; - default: - // Error - Assert.True(false, "Unexpected token value " + dynamicPropertyToken); - break; - } + KeyValuePair dynamicPropertyBirthDay = origin.DynamicProperties.FirstOrDefault(e => e.Key == "BirthDay"); + Assert.Equal(new Date(2016, 1, 29), dynamicPropertyBirthDay.Value); - return Updated(customer); - } + string dynamicPropertyToken = origin.DynamicProperties.FirstOrDefault(e => e.Key == "Token") + .Value.ToString().ToUpperInvariant(); - public IActionResult DeleteToAddress(int key) + switch (dynamicPropertyToken) { - IList customers = CreateCustomers(); - OpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); - if (customer == null) - { - return NotFound(); - } - - customer.Address = null; // A successful DELETE request to the edit URL for a structural property, ... sets the property to null. - return Updated(customer); // On success, the service MUST respond with 204 No Content and an empty body. + case "2E724E81-8462-4BA0-B920-DC87A61C8EA3": + // All changed non-dynamic properties: ["Street"] + Assert.True(address.GetChangedPropertyNames().Count() == 1); + break; + + case "9B198CA0-9546-4162-A4C0-14EAA255ACA7": + + // All changed non-dynamic properties: ["Street", "LineB"] + Assert.True(address.GetChangedPropertyNames().Count() == 2); + + Assert.Null(origin.LineA); + + Assert.NotNull(origin.LineB); + Assert.Equal("DescriptionB", origin.LineB.Description); + + // Fee is not overwritten. + Assert.Equal(LineDetails.DefaultValue_Fee, origin.LineB.Fee); + break; + + case "A4D09554-5551-4B36-A1CB-CFBCDB1F4EAD": + + // All changed non-dynamic properties: ["Street", "LineA", "LineB"] + Assert.True(address.GetChangedPropertyNames().Count() == 3); + + // --- LineA --- + Assert.NotNull(origin.LineA); + Assert.Equal("DescriptionA", origin.LineA.Description); + + // LineA.Fee is left as uninitialized. + Assert.Equal(LineDetails.UninitializedValue_Fee, origin.LineA.Fee); + + Assert.NotNull(origin.LineA.PhoneInfo); + Assert.Equal("ContactNameA", origin.LineA.PhoneInfo.ContactName); + Assert.Equal(7654321, origin.LineA.PhoneInfo.PhoneNumber); + + // --- LineB --- + Assert.NotNull(origin.LineB); + Assert.Null(origin.LineB.Description); + + // LineB.Fee is originally initialized for OpenAddress is created for each customer. + Assert.Equal(LineDetails.DefaultValue_Fee, origin.LineB.Fee); + + Assert.NotNull(origin.LineB.PhoneInfo); + Assert.Equal("ContactNameB", origin.LineB.PhoneInfo.ContactName); + Assert.Equal(0, origin.LineB.PhoneInfo.PhoneNumber); + break; + + case "2D071BD4-E4FB-4639-8024-BBC173850441": + // All changed non-dynamic properties: ["Street", "LineA"] + Assert.True(address.GetChangedPropertyNames().Count() == 2); + + // --- LineA --- + Assert.NotNull(origin.LineA); + Assert.Equal("LineDetailsWithNewDeepSubNode.", origin.LineA.Description); + Assert.Equal(LineDetails.UninitializedValue_Fee, origin.LineA.Fee); + + Assert.NotNull(origin.LineA.PhoneInfo); + Assert.Equal("ContactNameA", origin.LineA.PhoneInfo.ContactName); + Assert.Equal(0, origin.LineA.PhoneInfo.PhoneNumber); + + Assert.NotNull(origin.LineA.PhoneInfo.Spec); + Assert.Null(origin.LineA.PhoneInfo.Spec.Make); + Assert.Equal(6, origin.LineA.PhoneInfo.Spec.ScreenSize); + + // --- LineB --- + Assert.NotNull(origin.LineB); + break; + + case "250EFC6E-8FA4-4B68-951C-F1E26DE09D1D": + Assert.True(origin.DynamicProperties.ContainsKey("Telephone")); + // Telephone dynamic property + Phone telephone = origin.DynamicProperties["Telephone"] as Phone; + Assert.NotNull(telephone); + Assert.Equal("ContactNameX", telephone.ContactName); + Assert.Equal(13, telephone.PhoneNumber); + + // Nested complex property + Assert.NotNull(telephone.Spec); + Assert.Equal(7, telephone.Spec.ScreenSize); + break; + + case "40CEEEDE-031C-45CB-9E44-E6017D635814": + Assert.True(origin.DynamicProperties.ContainsKey("Building")); + // Building dynamic property + Building building = origin.DynamicProperties["Building"] as Building; + Assert.NotNull(building); + Assert.Equal("BuildingNameY", building.BuildingName); + + // Nested telephone complex dynamic property + Assert.True(building.DynamicProperties.ContainsKey("Telephone")); + Phone phone = building.DynamicProperties["Telephone"] as Phone; + Assert.NotNull(phone); + Assert.Equal("ContactNameZ", phone.ContactName); + Assert.Equal(17, phone.PhoneNumber); + + // Nested complex property + Assert.NotNull(phone.Spec); + Assert.Equal(5, phone.Spec.ScreenSize); + break; + case "3CA243CF-460A-4144-B6EB-F5E1180ABDC8": + OpenAddress addressInstance = address.GetInstance(); + Assert.NotNull(addressInstance); + + // Complex property + Assert.NotNull(addressInstance.LineA); + Assert.NotNull(addressInstance.LineA.PhoneInfo); + Assert.Equal(7654321, addressInstance.LineA.PhoneInfo.PhoneNumber); + + object nestedLineAValue; + // Fetch LineA property using TryGetNestedPropertyValue + Assert.True(address.TryGetNestedPropertyValue("LineA", out nestedLineAValue)); + Delta deltaLineA = nestedLineAValue as Delta; + Assert.NotNull(deltaLineA); + + // Nested complex property + dynamic nestedLineA = deltaLineA; + Assert.NotNull(nestedLineA.PhoneInfo); + Assert.Equal(7654321, nestedLineA.PhoneInfo.PhoneNumber); + break; + default: + // Error + Assert.True(false, "Unexpected token value " + dynamicPropertyToken); + break; } - private static void VerifyOriginAddress(int key, OpenAddress address) + return Updated(customer); + } + + public IActionResult DeleteToAddress(int key) + { + IList customers = CreateCustomers(); + OpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); + if (customer == null) { - Assert.NotNull(address); - Assert.Equal("Street " + key, address.Street); - Assert.Equal("City " + key, address.City); - Assert.NotNull(address.DynamicProperties); - KeyValuePair dynamicProperty = Assert.Single(address.DynamicProperties); - Assert.Equal("IntProp", dynamicProperty.Key); - Assert.Equal(new int[] { 200, 100, 300, 0, 400 }[key], dynamicProperty.Value); + return NotFound(); } + + customer.Address = null; // A successful DELETE request to the edit URL for a structural property, ... sets the property to null. + return Updated(customer); // On success, the service MUST respond with 204 No Content and an empty body. } - // Models - public class OpenCustomer + private static void VerifyOriginAddress(int key, OpenAddress address) { - [Key] - public int CustomerId { get; set; } - public string Name { get; set; } - public OpenAddress Address { get; set; } + Assert.NotNull(address); + Assert.Equal("Street " + key, address.Street); + Assert.Equal("City " + key, address.City); + Assert.NotNull(address.DynamicProperties); + KeyValuePair dynamicProperty = Assert.Single(address.DynamicProperties); + Assert.Equal("IntProp", dynamicProperty.Key); + Assert.Equal(new int[] { 200, 100, 300, 0, 400 }[key], dynamicProperty.Value); } +} - public class OpenAddress +// Models +public class OpenCustomer +{ + [Key] + public int CustomerId { get; set; } + public string Name { get; set; } + public OpenAddress Address { get; set; } +} + +public class OpenAddress +{ + public OpenAddress() { - public OpenAddress() - { - DynamicProperties = new Dictionary(); - } + DynamicProperties = new Dictionary(); + } - public string Street { get; set; } - public string City { get; set; } - public IDictionary DynamicProperties { get; set; } + public string Street { get; set; } + public string City { get; set; } + public IDictionary DynamicProperties { get; set; } - public LineDetails LineA { get; set; } + public LineDetails LineA { get; set; } - public LineDetails LineB { get; set; } - } + public LineDetails LineB { get; set; } +} - public class LineDetails - { - internal const int UninitializedValue_Fee = -1; - internal const int DefaultValue_Fee = 20; +public class LineDetails +{ + internal const int UninitializedValue_Fee = -1; + internal const int DefaultValue_Fee = 20; - public LineDetails() - { - Fee = UninitializedValue_Fee; - } + public LineDetails() + { + Fee = UninitializedValue_Fee; + } - public string Description { get; set; } + public string Description { get; set; } - public int Fee { get; set; } + public int Fee { get; set; } - public Phone PhoneInfo { get; set; } - } + public Phone PhoneInfo { get; set; } +} - public class Phone - { - public string ContactName { get; set; } +public class Phone +{ + public string ContactName { get; set; } - public int PhoneNumber { get; set; } + public int PhoneNumber { get; set; } - public PhoneHardwareSpec Spec { get; set; } - } + public PhoneHardwareSpec Spec { get; set; } +} - public class PhoneHardwareSpec - { - public string Make { get; set; } +public class PhoneHardwareSpec +{ + public string Make { get; set; } - public int ScreenSize { get; set; } - } + public int ScreenSize { get; set; } +} - public class Building - { - public string BuildingName { get; set; } +public class Building +{ + public string BuildingName { get; set; } - public IDictionary DynamicProperties { get; set; } = new Dictionary(); - } + public IDictionary DynamicProperties { get; set; } = new Dictionary(); } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Scenarios/OpenEntityTypeTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Scenarios/OpenEntityTypeTests.cs index 9f19cc500..362c02cb9 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Scenarios/OpenEntityTypeTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Scenarios/OpenEntityTypeTests.cs @@ -26,183 +26,183 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Scenarios +namespace Microsoft.AspNetCore.OData.Tests.Scenarios; + +public class OpenEntityTypeTests { - public class OpenEntityTypeTests - { - private const string _untypedCustomerRequestRooturl = "http://localhost/odata/UntypedSimpleOpenCustomers"; + private const string _untypedCustomerRequestRooturl = "http://localhost/odata/UntypedSimpleOpenCustomers"; - [Fact] - public async Task Get_OpenEntityType() - { - // Arrange - const string RequestUri = "http://localhost/odata/SimpleOpenCustomers(9)"; - - var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, RequestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal("http://localhost/odata/$metadata#SimpleOpenCustomers/Microsoft.AspNetCore.OData.Tests.Models.SimpleVipCustomer/$entity", result["@odata.context"]); - Assert.Equal("#Microsoft.AspNetCore.OData.Tests.Models.SimpleVipCustomer", result["@odata.type"]); - Assert.Equal(9, result["CustomerId"]); - Assert.Equal("VipCustomer", result["Name"]); - Assert.Equal("#Collection(Int32)", result["ListProp@odata.type"]); - Assert.Equal(new JArray(new[] { 200, 100, 300, 0, 400 }), result["ListProp"]); - Assert.Equal("0001-01-01", result["DateList"][0]); - Assert.Equal("9999-12-31", result["DateList"][1]); - Assert.Equal(JTokenType.Null, result["Receipt"].Type); - } + [Fact] + public async Task Get_OpenEntityType() + { + // Arrange + const string RequestUri = "http://localhost/odata/SimpleOpenCustomers(9)"; + + var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, RequestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("http://localhost/odata/$metadata#SimpleOpenCustomers/Microsoft.AspNetCore.OData.Tests.Models.SimpleVipCustomer/$entity", result["@odata.context"]); + Assert.Equal("#Microsoft.AspNetCore.OData.Tests.Models.SimpleVipCustomer", result["@odata.type"]); + Assert.Equal(9, result["CustomerId"]); + Assert.Equal("VipCustomer", result["Name"]); + Assert.Equal("#Collection(Int32)", result["ListProp@odata.type"]); + Assert.Equal(new JArray(new[] { 200, 100, 300, 0, 400 }), result["ListProp"]); + Assert.Equal("0001-01-01", result["DateList"][0]); + Assert.Equal("9999-12-31", result["DateList"][1]); + Assert.Equal(JTokenType.Null, result["Receipt"].Type); + } - [Theory] - [InlineData("http://localhost/odata/SimpleOpenCustomers?$orderby=Token&$filter=Token ne null", new[] { 2, 4 })] - [InlineData("http://localhost/odata/SimpleOpenCustomers?$orderby=Token desc&$filter=Token ne null", new[] { 4, 2 })] - [InlineData("http://localhost/odata/SimpleOpenCustomers?$filter=Token ne null", new[] { 2, 4 })] - public async Task Get_OpenEntityTypeWithOrderbyAndFilter(string uri, int[] customerIds) - { - // Arrange - var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); - var resultArray = result["value"] as JArray; - Assert.Equal(2, resultArray.Count); - for (var i = 0; i < customerIds.Length; i++) - Assert.Equal(customerIds[i], resultArray[i]["CustomerId"]); - } + [Theory] + [InlineData("http://localhost/odata/SimpleOpenCustomers?$orderby=Token&$filter=Token ne null", new[] { 2, 4 })] + [InlineData("http://localhost/odata/SimpleOpenCustomers?$orderby=Token desc&$filter=Token ne null", new[] { 4, 2 })] + [InlineData("http://localhost/odata/SimpleOpenCustomers?$filter=Token ne null", new[] { 2, 4 })] + public async Task Get_OpenEntityTypeWithOrderbyAndFilter(string uri, int[] customerIds) + { + // Arrange + var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); + var resultArray = result["value"] as JArray; + Assert.Equal(2, resultArray.Count); + for (var i = 0; i < customerIds.Length; i++) + Assert.Equal(customerIds[i], resultArray[i]["CustomerId"]); + } - [Fact] - public async Task Get_OpenEntityType_Enum_Collection_Property() - { - // Arrange - const string RequestUri = "http://localhost/odata/UntypedSimpleOpenCustomers(1)"; - var client = GetClient(GetUntypedEdmModel(), typeof(UntypedSimpleOpenCustomersController)); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, RequestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal("http://localhost/odata/$metadata#UntypedSimpleOpenCustomers/$entity", result["@odata.context"]); - Assert.Equal("#Collection(NS.Color)", result["Colors@odata.type"]); - Assert.Equal(new JArray(new[] { "Red", "0", "Red" }), result["Colors"]); - Assert.Equal("Red", result["Color"]); - } + [Fact] + public async Task Get_OpenEntityType_Enum_Collection_Property() + { + // Arrange + const string RequestUri = "http://localhost/odata/UntypedSimpleOpenCustomers(1)"; + var client = GetClient(GetUntypedEdmModel(), typeof(UntypedSimpleOpenCustomersController)); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, RequestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("http://localhost/odata/$metadata#UntypedSimpleOpenCustomers/$entity", result["@odata.context"]); + Assert.Equal("#Collection(NS.Color)", result["Colors@odata.type"]); + Assert.Equal(new JArray(new[] { "Red", "0", "Red" }), result["Colors"]); + Assert.Equal("Red", result["Color"]); + } - [Theory] - [InlineData("/$count", "1")] - [InlineData("(1)/DeclaredNumbers/$count", "2")] - [InlineData("(1)/DeclaredColors/$count", "3")] - [InlineData("(1)/DeclaredAddresses/$count", "2")] - public async Task Get_UnTyped_DollarCount(string requestUri, string expectedResult) - { - // Arrange - IEdmModel model = GetUntypedEdmModel(); - var server = TestServerUtils.Create(opt => opt.AddRouteComponents("odata", model).Count(), typeof(UntypedSimpleOpenCustomersController)); - var client = server.CreateClient(); + [Theory] + [InlineData("/$count", "1")] + [InlineData("(1)/DeclaredNumbers/$count", "2")] + [InlineData("(1)/DeclaredColors/$count", "3")] + [InlineData("(1)/DeclaredAddresses/$count", "2")] + public async Task Get_UnTyped_DollarCount(string requestUri, string expectedResult) + { + // Arrange + IEdmModel model = GetUntypedEdmModel(); + var server = TestServerUtils.Create(opt => opt.AddRouteComponents("odata", model).Count(), typeof(UntypedSimpleOpenCustomersController)); + var client = server.CreateClient(); - // Act - HttpResponseMessage response = await client.GetAsync(_untypedCustomerRequestRooturl + requestUri); + // Act + HttpResponseMessage response = await client.GetAsync(_untypedCustomerRequestRooturl + requestUri); - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(expectedResult, await response.Content.ReadAsStringAsync()); - } + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(expectedResult, await response.Content.ReadAsStringAsync()); + } - [Theory] - [InlineData("(1)/Color/$value", "Red")] - [InlineData("(1)/Color", "Red")] - [InlineData("(1)/DeclaredColors", "0")] - public async Task Get_UnTyped_Enum_Collection_Property(string requestUri, string expectedContainsResult) - { - // Arrange - var client = GetClient(GetUntypedEdmModel(), typeof(UntypedSimpleOpenCustomersController)); + [Theory] + [InlineData("(1)/Color/$value", "Red")] + [InlineData("(1)/Color", "Red")] + [InlineData("(1)/DeclaredColors", "0")] + public async Task Get_UnTyped_Enum_Collection_Property(string requestUri, string expectedContainsResult) + { + // Arrange + var client = GetClient(GetUntypedEdmModel(), typeof(UntypedSimpleOpenCustomersController)); - // Act - HttpResponseMessage response = await client.GetAsync(_untypedCustomerRequestRooturl + requestUri); + // Act + HttpResponseMessage response = await client.GetAsync(_untypedCustomerRequestRooturl + requestUri); - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Contains(expectedContainsResult, await response.Content.ReadAsStringAsync()); - } + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Contains(expectedContainsResult, await response.Content.ReadAsStringAsync()); + } - [Fact] - public async Task CanDispatch_ActionPayload_With_EdmEnumObject() - { - const string RequestUri = "http://localhost/odata/UntypedSimpleOpenCustomers(1)/NS.AddColor"; - const string Payload = @"{ - ""Color"": ""0"" - }"; + [Fact] + public async Task CanDispatch_ActionPayload_With_EdmEnumObject() + { + const string RequestUri = "http://localhost/odata/UntypedSimpleOpenCustomers(1)/NS.AddColor"; + const string Payload = @"{ + ""Color"": ""0"" + }"; - var client = GetClient(GetUntypedEdmModel(), typeof(UntypedSimpleOpenCustomersController)); + var client = GetClient(GetUntypedEdmModel(), typeof(UntypedSimpleOpenCustomersController)); - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUri); - request.Content = new StringContentWithLength(Payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUri); + request.Content = new StringContentWithLength(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.True(response.IsSuccessStatusCode); - } + // Assert + Assert.True(response.IsSuccessStatusCode); + } - [Fact] - public async Task Get_OpenEntityTypeWithSelect() - { - // Arrange - const string RequestUri = "http://localhost/odata/SimpleOpenCustomers?$select=Token"; - var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, RequestUri); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); - var resultArray = result["value"] as JArray; - Assert.Equal(6, resultArray.Count); - Assert.NotNull(resultArray[2]["Token"]);//customer 2 has a token - Assert.NotNull(resultArray[4]["Token"]);//customer 4 has a token - } + [Fact] + public async Task Get_OpenEntityTypeWithSelect() + { + // Arrange + const string RequestUri = "http://localhost/odata/SimpleOpenCustomers?$select=Token"; + var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, RequestUri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); + var resultArray = result["value"] as JArray; + Assert.Equal(6, resultArray.Count); + Assert.NotNull(resultArray[2]["Token"]);//customer 2 has a token + Assert.NotNull(resultArray[4]["Token"]);//customer 4 has a token + } - [Theory] - [InlineData("$select=Address/Street,Address/City")] - [InlineData("$select=Address($select=Street,City)")] - public async Task Get_OpenEntityTypeWithMultiplePropertySelect(string select) - { - // Arrange - string requestUri = "http://localhost/odata/SimpleOpenCustomers?" + select; - - IEdmModel model = GetEdmModel(); - var server = TestServerUtils.Create(opt => opt.AddRouteComponents("odata", model).Select(), typeof(SimpleOpenCustomersController)); - var client = server.CreateClient(); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - // Assert.True(response.IsSuccessStatusCode); - JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); - var resultArray = result["value"] as JArray; - Assert.Equal(6, resultArray.Count); - Assert.Equal(@"[ + [Theory] + [InlineData("$select=Address/Street,Address/City")] + [InlineData("$select=Address($select=Street,City)")] + public async Task Get_OpenEntityTypeWithMultiplePropertySelect(string select) + { + // Arrange + string requestUri = "http://localhost/odata/SimpleOpenCustomers?" + select; + + IEdmModel model = GetEdmModel(); + var server = TestServerUtils.Create(opt => opt.AddRouteComponents("odata", model).Select(), typeof(SimpleOpenCustomersController)); + var client = server.CreateClient(); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + // Assert.True(response.IsSuccessStatusCode); + JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); + var resultArray = result["value"] as JArray; + Assert.Equal(6, resultArray.Count); + Assert.Equal(@"[ { ""Address"": { ""Street"": ""Street 0"", @@ -241,589 +241,588 @@ public async Task Get_OpenEntityTypeWithMultiplePropertySelect(string select) } } ]", resultArray.ToString()); - } + } - [Fact] - public async Task Get_OpenEntityTypeWithSelectWildcard() - { - // Arrange - const string RequestUri = "http://localhost/odata/SimpleOpenCustomers(1)?$select=*"; - var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, RequestUri); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.NotNull(result["Token"]); - Assert.NotNull(result["MyList"]); - } + [Fact] + public async Task Get_OpenEntityTypeWithSelectWildcard() + { + // Arrange + const string RequestUri = "http://localhost/odata/SimpleOpenCustomers(1)?$select=*"; + var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, RequestUri); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.NotNull(result["Token"]); + Assert.NotNull(result["MyList"]); + } - [Fact] - public async Task Post_OpenEntityType() - { - // Arrange - const string Payload = "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#OpenCustomers/$entity\"," + - "\"CustomerId\":6,\"Name\":\"FirstName 6\"," + - "\"Address\":{" + - "\"Street\":\"Street 6\",\"City\":\"City 6\",\"Place\":\"Earth\",\"Token@odata.type\":\"#Guid\"," + - "\"Token\":\"4DB52263-4382-4BCB-A63E-3129C1B5FA0D\"," + - "\"@NS1.abc\": \"Test\"," + - "\"Number\":990" + - "}," + - "\"Website@NS.Street\": \"Test\"," + - "\"Website\": \"WebSite #6\"," + - "\"Place@odata.type\":\"#String\",\"Place\":\"My Dynamic Place\"," + // odata.type is necessary, otherwise it will get an ODataUntypedValue - "\"Token@odata.type\":\"#Guid\",\"Token\":\"2c1f450a-a2a7-4fe1-a25d-4d9332fc0694\"," + - "\"DoubleList@odata.type\":\"#Collection(Double)\"," + - "\"DoubleList\":[5.5, 4.4, 3.3]" + - "}"; - - const string RequestUri = "http://localhost/odata/SimpleOpenCustomers"; - var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUri); - request.Content = new StringContentWithLength(Payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - } + [Fact] + public async Task Post_OpenEntityType() + { + // Arrange + const string Payload = "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#OpenCustomers/$entity\"," + + "\"CustomerId\":6,\"Name\":\"FirstName 6\"," + + "\"Address\":{" + + "\"Street\":\"Street 6\",\"City\":\"City 6\",\"Place\":\"Earth\",\"Token@odata.type\":\"#Guid\"," + + "\"Token\":\"4DB52263-4382-4BCB-A63E-3129C1B5FA0D\"," + + "\"@NS1.abc\": \"Test\"," + + "\"Number\":990" + + "}," + + "\"Website@NS.Street\": \"Test\"," + + "\"Website\": \"WebSite #6\"," + + "\"Place@odata.type\":\"#String\",\"Place\":\"My Dynamic Place\"," + // odata.type is necessary, otherwise it will get an ODataUntypedValue + "\"Token@odata.type\":\"#Guid\",\"Token\":\"2c1f450a-a2a7-4fe1-a25d-4d9332fc0694\"," + + "\"DoubleList@odata.type\":\"#Collection(Double)\"," + + "\"DoubleList\":[5.5, 4.4, 3.3]" + + "}"; + + const string RequestUri = "http://localhost/odata/SimpleOpenCustomers"; + var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUri); + request.Content = new StringContentWithLength(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + } - [Fact] - public async Task Post_OpenEntityTypeWithNullComplexTypeProperty() - { - // Arrange - const string Payload = "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#OpenCustomers/$entity\"," + - "\"CustomerId\":99,\"Name\":\"FirstName 99\"," + // special CustomerId to test the Address == null in the controller. - "\"Address\": null," + - "\"Website\": \"WebSite #6\"," + - "\"Place@odata.type\":\"#String\",\"Place\":\"My Dynamic Place\"," + // odata.type is necessary, otherwise it will get an ODataUntypedValue - "\"Token@odata.type\":\"#Guid\",\"Token\":\"2c1f450a-a2a7-4fe1-a25d-4d9332fc0694\"," + - "\"DoubleList@odata.type\":\"#Collection(Double)\"," + - "\"DoubleList\":[5.5, 4.4, 3.3]" + - "}"; - - const string RequestUri = "http://localhost/odata/SimpleOpenCustomers"; - var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUri); - request.Content = new StringContentWithLength(Payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - } + [Fact] + public async Task Post_OpenEntityTypeWithNullComplexTypeProperty() + { + // Arrange + const string Payload = "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#OpenCustomers/$entity\"," + + "\"CustomerId\":99,\"Name\":\"FirstName 99\"," + // special CustomerId to test the Address == null in the controller. + "\"Address\": null," + + "\"Website\": \"WebSite #6\"," + + "\"Place@odata.type\":\"#String\",\"Place\":\"My Dynamic Place\"," + // odata.type is necessary, otherwise it will get an ODataUntypedValue + "\"Token@odata.type\":\"#Guid\",\"Token\":\"2c1f450a-a2a7-4fe1-a25d-4d9332fc0694\"," + + "\"DoubleList@odata.type\":\"#Collection(Double)\"," + + "\"DoubleList\":[5.5, 4.4, 3.3]" + + "}"; + + const string RequestUri = "http://localhost/odata/SimpleOpenCustomers"; + var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, RequestUri); + request.Content = new StringContentWithLength(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + } - [Fact] - public async Task Post_UnTyped_OpenEntityType() - { - // Arrange - const string Payload = "{" + - "\"@odata.context\":\"http://localhost/odata/$metadata#UntypedSimpleOpenCustomers/$entity\"," + - "\"CustomerId\":6,\"Name@odata.type\":\"#String\",\"Name\":\"FirstName 6\"," + - "\"Address\":{" + - "\"@odata.type\":\"#NS.Address\",\"Street\":\"Street 6\",\"City\":\"City 6\"" + - "}," + - "\"Addresses@odata.type\":\"#Collection(NS.Address)\"," + - "\"Addresses\":[{" + - "\"@odata.type\":\"#NS.Address\",\"Street\":\"Street 7\",\"City\":\"City 7\"" + - "}]," + - "\"DoubleList@odata.type\":\"#Collection(Double)\"," + - "\"DoubleList\":[5.5, 4.4, 3.3]," + - "\"FavoriteColor@odata.type\":\"#NS.Color\"," + - "\"FavoriteColor\":\"Red\"," + - "\"Color\":\"Red\"," + - "\"FavoriteColors@odata.type\":\"#Collection(NS.Color)\"," + - "\"FavoriteColors\":[\"0\", \"1\"]" + - "}"; - var client = GetClient(GetUntypedEdmModel(), typeof(UntypedSimpleOpenCustomersController)); - - // Act - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, _untypedCustomerRequestRooturl); - request.Content = new StringContentWithLength(Payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - } + [Fact] + public async Task Post_UnTyped_OpenEntityType() + { + // Arrange + const string Payload = "{" + + "\"@odata.context\":\"http://localhost/odata/$metadata#UntypedSimpleOpenCustomers/$entity\"," + + "\"CustomerId\":6,\"Name@odata.type\":\"#String\",\"Name\":\"FirstName 6\"," + + "\"Address\":{" + + "\"@odata.type\":\"#NS.Address\",\"Street\":\"Street 6\",\"City\":\"City 6\"" + + "}," + + "\"Addresses@odata.type\":\"#Collection(NS.Address)\"," + + "\"Addresses\":[{" + + "\"@odata.type\":\"#NS.Address\",\"Street\":\"Street 7\",\"City\":\"City 7\"" + + "}]," + + "\"DoubleList@odata.type\":\"#Collection(Double)\"," + + "\"DoubleList\":[5.5, 4.4, 3.3]," + + "\"FavoriteColor@odata.type\":\"#NS.Color\"," + + "\"FavoriteColor\":\"Red\"," + + "\"Color\":\"Red\"," + + "\"FavoriteColors@odata.type\":\"#Collection(NS.Color)\"," + + "\"FavoriteColors\":[\"0\", \"1\"]" + + "}"; + var client = GetClient(GetUntypedEdmModel(), typeof(UntypedSimpleOpenCustomersController)); + + // Act + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, _untypedCustomerRequestRooturl); + request.Content = new StringContentWithLength(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + } - [Fact] - public async Task Patch_OpenEntityType() - { - // Arrange - const string Payload = "{" + - "\"CustomerId\":99,\"Name\":\"ChangedName\"," + - "\"Token@odata.type\":\"#DateTimeOffset\",\"Token\":\"2014-01-01T00:00:00Z\"," + - "\"DoubleList@odata.type\":\"#Collection(Double)\"," + - "\"DoubleList\":[5.5, 4.4, 3.3]" + - "}"; + [Fact] + public async Task Patch_OpenEntityType() + { + // Arrange + const string Payload = "{" + + "\"CustomerId\":99,\"Name\":\"ChangedName\"," + + "\"Token@odata.type\":\"#DateTimeOffset\",\"Token\":\"2014-01-01T00:00:00Z\"," + + "\"DoubleList@odata.type\":\"#Collection(Double)\"," + + "\"DoubleList\":[5.5, 4.4, 3.3]" + + "}"; - const string RequestUri = "http://localhost/odata/SimpleOpenCustomers(2)"; + const string RequestUri = "http://localhost/odata/SimpleOpenCustomers(2)"; - var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); + var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); - // Act - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), RequestUri); - request.Content = new StringContentWithLength(Payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - HttpResponseMessage response = await client.SendAsync(request); + // Act + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), RequestUri); + request.Content = new StringContentWithLength(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + HttpResponseMessage response = await client.SendAsync(request); - // Assert - Assert.True(response.IsSuccessStatusCode); - JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); + // Assert + Assert.True(response.IsSuccessStatusCode); + JObject result = JObject.Parse(await response.Content.ReadAsStringAsync()); - Assert.Equal(99, result["CustomerId"]); - Assert.Equal("ChangedName", result["Name"]); + Assert.Equal(99, result["CustomerId"]); + Assert.Equal("ChangedName", result["Name"]); - // The type and the value of "Token" are changed. - Assert.Equal("#DateTimeOffset", result["Token@odata.type"]); + // The type and the value of "Token" are changed. + Assert.Equal("#DateTimeOffset", result["Token@odata.type"]); - // The type and the value of "IntList" are un-changed. - Assert.Equal(new JArray(new[] { 1, 2, 3, 4, 5, 6, 7 }), result["IntList"]); + // The type and the value of "IntList" are un-changed. + Assert.Equal(new JArray(new[] { 1, 2, 3, 4, 5, 6, 7 }), result["IntList"]); - // New dynamic property "DoubleList" is added. - Assert.Equal(new JArray(new[] { 5.5, 4.4, 3.3 }), result["DoubleList"]); - } + // New dynamic property "DoubleList" is added. + Assert.Equal(new JArray(new[] { 5.5, 4.4, 3.3 }), result["DoubleList"]); + } - [Fact] - public async Task Put_OpenEntityType() - { - // Arrange - const string Payload = "{" + - "\"CustomerId\":99,\"Name\":\"ChangedName\"," + - "\"Token@odata.type\":\"#DateTimeOffset\",\"Token\":\"2014-01-01T00:00:00Z\"" + - "}"; - - const string RequestUri = "http://localhost/odata/SimpleOpenCustomers(2)"; - var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); - - // Act - HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("Put"), RequestUri); - request.Content = new StringContentWithLength(Payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } + [Fact] + public async Task Put_OpenEntityType() + { + // Arrange + const string Payload = "{" + + "\"CustomerId\":99,\"Name\":\"ChangedName\"," + + "\"Token@odata.type\":\"#DateTimeOffset\",\"Token\":\"2014-01-01T00:00:00Z\"" + + "}"; + + const string RequestUri = "http://localhost/odata/SimpleOpenCustomers(2)"; + var client = GetClient(GetEdmModel(), typeof(SimpleOpenCustomersController)); + + // Act + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("Put"), RequestUri); + request.Content = new StringContentWithLength(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } - private HttpClient GetClient(IEdmModel model, params Type[] controllers) - { - var server = TestServerUtils.Create(opt => opt.AddRouteComponents("odata", model), controllers); - return server.CreateClient(); - } + private HttpClient GetClient(IEdmModel model, params Type[] controllers) + { + var server = TestServerUtils.Create(opt => opt.AddRouteComponents("odata", model), controllers); + return server.CreateClient(); + } - private static IEdmModel GetEdmModel() - { - ODataModelBuilder builder = new ODataConventionModelBuilder(); - builder.EntitySet("SimpleOpenCustomers"); - builder.EntitySet("SimpleOpenOrders"); - return builder.GetEdmModel(); - } + private static IEdmModel GetEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + builder.EntitySet("SimpleOpenCustomers"); + builder.EntitySet("SimpleOpenOrders"); + return builder.GetEdmModel(); + } - private static IEdmModel _untypedEdmModel; - public static IEdmModel GetUntypedEdmModel() - { - if (_untypedEdmModel != null) - { - return _untypedEdmModel; - } + private static IEdmModel _untypedEdmModel; + public static IEdmModel GetUntypedEdmModel() + { + if (_untypedEdmModel != null) + { + return _untypedEdmModel; + } + + var model = new EdmModel(); + // complex type address + EdmComplexType address = new EdmComplexType("NS", "Address", null, false, true); + address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); + model.AddElement(address); + IEdmCollectionTypeReference complexCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(address.ToEdmTypeReference(false))); + + // enum type color + EdmEnumType color = new EdmEnumType("NS", "Color"); + color.AddMember(new EdmEnumMember(color, "Red", new EdmEnumMemberValue(0))); + model.AddElement(color); + IEdmCollectionTypeReference enumCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(color.ToEdmTypeReference(false))); + + // primitive collection type + IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: true); + EdmCollectionTypeReference primitiveCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(intType)); + + // entity type customer + EdmEntityType customer = new EdmEntityType("NS", "UntypedSimpleOpenCustomer", null, false, true); + customer.AddKeys(customer.AddStructuralProperty("CustomerId", EdmPrimitiveTypeKind.Int32)); + customer.AddStructuralProperty("Color", new EdmEnumTypeReference(color, isNullable: true)); + customer.AddStructuralProperty("DeclaredAddresses", complexCollectionType); + customer.AddStructuralProperty("DeclaredColors", enumCollectionType); + customer.AddStructuralProperty("DeclaredNumbers", primitiveCollectionType); + + model.AddElement(customer); + + EdmAction action = new EdmAction( + "NS", + "AddColor", + null, + isBound: true, + entitySetPathExpression: null); + action.AddParameter("bindingParameter", new EdmEntityTypeReference(customer, false)); + action.AddParameter("Color", new EdmEnumTypeReference(color, true)); + model.AddElement(action); + + EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); + container.AddEntitySet("UntypedSimpleOpenCustomers", customer); + + model.AddElement(container); + _untypedEdmModel = model; + return model; + } +} - var model = new EdmModel(); - // complex type address - EdmComplexType address = new EdmComplexType("NS", "Address", null, false, true); - address.AddStructuralProperty("Street", EdmPrimitiveTypeKind.String); - model.AddElement(address); - IEdmCollectionTypeReference complexCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(address.ToEdmTypeReference(false))); - - // enum type color - EdmEnumType color = new EdmEnumType("NS", "Color"); - color.AddMember(new EdmEnumMember(color, "Red", new EdmEnumMemberValue(0))); - model.AddElement(color); - IEdmCollectionTypeReference enumCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(color.ToEdmTypeReference(false))); - - // primitive collection type - IEdmTypeReference intType = EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Int32, isNullable: true); - EdmCollectionTypeReference primitiveCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(intType)); - - // entity type customer - EdmEntityType customer = new EdmEntityType("NS", "UntypedSimpleOpenCustomer", null, false, true); - customer.AddKeys(customer.AddStructuralProperty("CustomerId", EdmPrimitiveTypeKind.Int32)); - customer.AddStructuralProperty("Color", new EdmEnumTypeReference(color, isNullable: true)); - customer.AddStructuralProperty("DeclaredAddresses", complexCollectionType); - customer.AddStructuralProperty("DeclaredColors", enumCollectionType); - customer.AddStructuralProperty("DeclaredNumbers", primitiveCollectionType); - - model.AddElement(customer); - - EdmAction action = new EdmAction( - "NS", - "AddColor", - null, - isBound: true, - entitySetPathExpression: null); - action.AddParameter("bindingParameter", new EdmEntityTypeReference(customer, false)); - action.AddParameter("Color", new EdmEnumTypeReference(color, true)); - model.AddElement(action); - - EdmEntityContainer container = new EdmEntityContainer("NS", "Container"); - container.AddEntitySet("UntypedSimpleOpenCustomers", customer); - - model.AddElement(container); - _untypedEdmModel = model; - return model; - } +// Controller +public class SimpleOpenCustomersController : ODataController +{ + [EnableQuery] + public IQueryable Get() + { + return CreateCustomers().AsQueryable(); } - // Controller - public class SimpleOpenCustomersController : ODataController + public IActionResult Get(int key) { - [EnableQuery] - public IQueryable Get() + IList customers = CreateCustomers(); + SimpleOpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); + if (customer == null) { - return CreateCustomers().AsQueryable(); + return NotFound(); } - public IActionResult Get(int key) + if (customer.CustomerProperties == null) { - IList customers = CreateCustomers(); - SimpleOpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); - if (customer == null) - { - return NotFound(); - } - - if (customer.CustomerProperties == null) - { - customer.CustomerProperties = new Dictionary(); - customer.CustomerProperties.Add("Token", new Guid("2C1F450A-A2A7-4FE1-A25D-4D9332FC0694")); - IList lists = new List { 1, 2, 3, 4, 5, 6, 7 }; - customer.CustomerProperties.Add("MyList", lists); - } - - return Ok(customer); + customer.CustomerProperties = new Dictionary(); + customer.CustomerProperties.Add("Token", new Guid("2C1F450A-A2A7-4FE1-A25D-4D9332FC0694")); + IList lists = new List { 1, 2, 3, 4, 5, 6, 7 }; + customer.CustomerProperties.Add("MyList", lists); } - public IActionResult GetAddress(int key) - { - IList customers = CreateCustomers(); - SimpleOpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); - if (customer == null) - { - return NotFound(); - } - - return Ok(customer.Address); - } + return Ok(customer); + } - public IActionResult PostSimpleOpenCustomer([FromBody] SimpleOpenCustomer customer) + public IActionResult GetAddress(int key) + { + IList customers = CreateCustomers(); + SimpleOpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); + if (customer == null) { - // Verify there is a string dynamic property - object countryValue; - customer.CustomerProperties.TryGetValue("Place", out countryValue); - Assert.NotNull(countryValue); - Assert.Equal(typeof(String), countryValue.GetType()); - Assert.Equal("My Dynamic Place", countryValue); - - // Verify there is a Guid dynamic property - object tokenValue; - customer.CustomerProperties.TryGetValue("Token", out tokenValue); - Assert.NotNull(tokenValue); - Assert.Equal(typeof(Guid), tokenValue.GetType()); - Assert.Equal(new Guid("2c1f450a-a2a7-4fe1-a25d-4d9332fc0694"), tokenValue); - - // Verify there is an dynamic collection property - object value; - customer.CustomerProperties.TryGetValue("DoubleList", out value); - Assert.NotNull(value); - List doubleValues = Assert.IsType>(value); - Assert.Equal(new[] { 5.5, 4.4, 3.3 }, doubleValues); - - // special test cases to test the complex type property value is null. - if (customer.CustomerId == 99) - { - Assert.Null(customer.Address); - } - - return Ok(); + return NotFound(); } - public IActionResult Patch(int key, Delta patch) - { - IList customers = CreateCustomers(); - SimpleOpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); - if (customer == null) - { - return NotFound(); - } - - patch.Patch(customer); + return Ok(customer.Address); + } - // As normal, return Updated(customer) for *PATCH*. - // But, Here returns the *Ok* to test the payload - return Ok(customer); - } + public IActionResult PostSimpleOpenCustomer([FromBody] SimpleOpenCustomer customer) + { + // Verify there is a string dynamic property + object countryValue; + customer.CustomerProperties.TryGetValue("Place", out countryValue); + Assert.NotNull(countryValue); + Assert.Equal(typeof(String), countryValue.GetType()); + Assert.Equal("My Dynamic Place", countryValue); + + // Verify there is a Guid dynamic property + object tokenValue; + customer.CustomerProperties.TryGetValue("Token", out tokenValue); + Assert.NotNull(tokenValue); + Assert.Equal(typeof(Guid), tokenValue.GetType()); + Assert.Equal(new Guid("2c1f450a-a2a7-4fe1-a25d-4d9332fc0694"), tokenValue); + + // Verify there is an dynamic collection property + object value; + customer.CustomerProperties.TryGetValue("DoubleList", out value); + Assert.NotNull(value); + List doubleValues = Assert.IsType>(value); + Assert.Equal(new[] { 5.5, 4.4, 3.3 }, doubleValues); + + // special test cases to test the complex type property value is null. + if (customer.CustomerId == 99) + { + Assert.Null(customer.Address); + } + + return Ok(); + } - public IActionResult Put(int key, [FromBody] SimpleOpenCustomer changedCustomer) + public IActionResult Patch(int key, Delta patch) + { + IList customers = CreateCustomers(); + SimpleOpenCustomer customer = customers.FirstOrDefault(c => c.CustomerId == key); + if (customer == null) { - Assert.Equal(99, changedCustomer.CustomerId); - Assert.Equal("ChangedName", changedCustomer.Name); - Assert.Null(changedCustomer.Address); - Assert.Null(changedCustomer.Website); - Assert.Single(changedCustomer.CustomerProperties); - return Updated(changedCustomer); // Updated(customer); + return NotFound(); } - private static IList CreateCustomers() - { - int[] IntValues = { 200, 100, 300, 0, 400 }; - IList customers = Enumerable.Range(0, 5).Select(i => - new SimpleOpenCustomer - { - CustomerId = i, - Name = "FirstName " + i, - Address = new SimpleOpenAddress - { - Street = "Street " + i, - City = "City " + i, - Properties = new Dictionary { { "IntProp", IntValues[i] } } - }, - Website = "WebSite #" + i - }).ToList(); - - customers[2].CustomerProperties = new Dictionary - { - {"Token", new Guid("2C1F450A-A2A7-4FE1-A25D-4D9332FC0694")}, - {"IntList", new List { 1, 2, 3, 4, 5, 6, 7 }}, - }; + patch.Patch(customer); - customers[4].CustomerProperties = new Dictionary - { - {"Token", new Guid("A6A594ED-375B-424E-AC0A-945D89CF7B9B")}, - {"IntList", new List { 1, 2, 3, 4, 5, 6, 7 }}, - }; + // As normal, return Updated(customer) for *PATCH*. + // But, Here returns the *Ok* to test the payload + return Ok(customer); + } - SimpleOpenAddress address = new SimpleOpenAddress - { - Street = "SubStreet", - City = "City" - }; - customers[3].CustomerProperties = new Dictionary { { "ComplexList", new[] { address, address } } }; + public IActionResult Put(int key, [FromBody] SimpleOpenCustomer changedCustomer) + { + Assert.Equal(99, changedCustomer.CustomerId); + Assert.Equal("ChangedName", changedCustomer.Name); + Assert.Null(changedCustomer.Address); + Assert.Null(changedCustomer.Website); + Assert.Single(changedCustomer.CustomerProperties); + return Updated(changedCustomer); // Updated(customer); + } - SimpleVipCustomer vipCustomer = new SimpleVipCustomer + private static IList CreateCustomers() + { + int[] IntValues = { 200, 100, 300, 0, 400 }; + IList customers = Enumerable.Range(0, 5).Select(i => + new SimpleOpenCustomer { - CustomerId = 9, - Name = "VipCustomer", + CustomerId = i, + Name = "FirstName " + i, Address = new SimpleOpenAddress { - Street = "Vip Street ", - City = "Vip City ", + Street = "Street " + i, + City = "City " + i, + Properties = new Dictionary { { "IntProp", IntValues[i] } } }, - VipNum = "99-001", - CustomerProperties = new Dictionary - { - { "ListProp", IntValues }, - { "DateList", new[] { Date.MinValue, Date.MaxValue } }, - { "Receipt", null } - } - }; - - customers.Add(vipCustomer); - return customers; - } - } + Website = "WebSite #" + i + }).ToList(); - // Controller - public class UntypedSimpleOpenCustomersController : ODataController - { - private static EdmEntityObjectCollection _untypedSimpleOpenCustormers; + customers[2].CustomerProperties = new Dictionary + { + {"Token", new Guid("2C1F450A-A2A7-4FE1-A25D-4D9332FC0694")}, + {"IntList", new List { 1, 2, 3, 4, 5, 6, 7 }}, + }; - [EnableQuery] - public IActionResult Get() + customers[4].CustomerProperties = new Dictionary { - return Ok(GetCustomers()); - } + {"Token", new Guid("A6A594ED-375B-424E-AC0A-945D89CF7B9B")}, + {"IntList", new List { 1, 2, 3, 4, 5, 6, 7 }}, + }; - public IActionResult Get(int key) + SimpleOpenAddress address = new SimpleOpenAddress { - return Ok(GetCustomers()[0]); - } + Street = "SubStreet", + City = "City" + }; + customers[3].CustomerProperties = new Dictionary { { "ComplexList", new[] { address, address } } }; - [EnableQuery] - public IActionResult GetDeclaredAddresses(int key) + SimpleVipCustomer vipCustomer = new SimpleVipCustomer { - object addresses; - GetCustomers()[0].TryGetPropertyValue("DeclaredAddresses", out addresses); - return Ok((EdmComplexObjectCollection)addresses); - } + CustomerId = 9, + Name = "VipCustomer", + Address = new SimpleOpenAddress + { + Street = "Vip Street ", + City = "Vip City ", + }, + VipNum = "99-001", + CustomerProperties = new Dictionary + { + { "ListProp", IntValues }, + { "DateList", new[] { Date.MinValue, Date.MaxValue } }, + { "Receipt", null } + } + }; + customers.Add(vipCustomer); + return customers; + } +} - [EnableQuery] - public IActionResult GetColor(int key) - { - object color; - GetCustomers()[0].TryGetPropertyValue("Color", out color); - return Ok((EdmEnumObject)color); - } +// Controller +public class UntypedSimpleOpenCustomersController : ODataController +{ + private static EdmEntityObjectCollection _untypedSimpleOpenCustormers; - [EnableQuery] - public IActionResult GetDeclaredColors(int key) - { - object colors; - GetCustomers()[0].TryGetPropertyValue("DeclaredColors", out colors); - return Ok((EdmEnumObjectCollection)colors); - } + [EnableQuery] + public IActionResult Get() + { + return Ok(GetCustomers()); + } - [EnableQuery] - public IActionResult GetDeclaredNumbers(int key) - { - object numbers; - GetCustomers()[0].TryGetPropertyValue("DeclaredNumbers", out numbers); - return Ok(numbers as int[]); - } + public IActionResult Get(int key) + { + return Ok(GetCustomers()[0]); + } - [HttpPost] - public IActionResult AddColor(int key, ODataUntypedActionParameters parameters) - { - Assert.Equal("0", ((EdmEnumObject)parameters["Color"]).Value); - return Ok(); - } + [EnableQuery] + public IActionResult GetDeclaredAddresses(int key) + { + object addresses; + GetCustomers()[0].TryGetPropertyValue("DeclaredAddresses", out addresses); + return Ok((EdmComplexObjectCollection)addresses); + } - public IActionResult PostUntypedSimpleOpenCustomer(EdmEntityObject customer) - { - // Verify there is a string dynamic property in OpenEntityType - object nameValue; - customer.TryGetPropertyValue("Name", out nameValue); - Type nameType; - customer.TryGetPropertyType("Name", out nameType); - Assert.NotNull(nameValue); - Assert.Equal(typeof(String), nameType); - Assert.Equal("FirstName 6", nameValue); + [EnableQuery] + public IActionResult GetColor(int key) + { + object color; + GetCustomers()[0].TryGetPropertyValue("Color", out color); + return Ok((EdmEnumObject)color); + } - // Verify there is a collection of double dynamic property in OpenEntityType - object doubleListValue; - customer.TryGetPropertyValue("DoubleList", out doubleListValue); - Type doubleListType; - customer.TryGetPropertyType("DoubleList", out doubleListType); + [EnableQuery] + public IActionResult GetDeclaredColors(int key) + { + object colors; + GetCustomers()[0].TryGetPropertyValue("DeclaredColors", out colors); + return Ok((EdmEnumObjectCollection)colors); + } - Assert.NotNull(doubleListValue); - Assert.Equal(typeof(List), doubleListType); + [EnableQuery] + public IActionResult GetDeclaredNumbers(int key) + { + object numbers; + GetCustomers()[0].TryGetPropertyValue("DeclaredNumbers", out numbers); + return Ok(numbers as int[]); + } - // Verify there is a collection of complex type dynamic property in OpenEntityType - object addressesValue; - customer.TryGetPropertyValue("Addresses", out addressesValue); + [HttpPost] + public IActionResult AddColor(int key, ODataUntypedActionParameters parameters) + { + Assert.Equal("0", ((EdmEnumObject)parameters["Color"]).Value); + return Ok(); + } - Assert.NotNull(addressesValue); + public IActionResult PostUntypedSimpleOpenCustomer(EdmEntityObject customer) + { + // Verify there is a string dynamic property in OpenEntityType + object nameValue; + customer.TryGetPropertyValue("Name", out nameValue); + Type nameType; + customer.TryGetPropertyType("Name", out nameType); - // Verify there is a complex type dynamic property in OpenEntityType - object addressValue; - customer.TryGetPropertyValue("Address", out addressValue); + Assert.NotNull(nameValue); + Assert.Equal(typeof(String), nameType); + Assert.Equal("FirstName 6", nameValue); - Type addressType; - customer.TryGetPropertyType("Address", out addressType); + // Verify there is a collection of double dynamic property in OpenEntityType + object doubleListValue; + customer.TryGetPropertyValue("DoubleList", out doubleListValue); + Type doubleListType; + customer.TryGetPropertyType("DoubleList", out doubleListType); - Assert.NotNull(addressValue); - Assert.Equal(typeof(EdmComplexObject), addressType); + Assert.NotNull(doubleListValue); + Assert.Equal(typeof(List), doubleListType); - // Verify there is a collection of enum type dynamic property in OpenEntityType - object favoriteColorsValue; - customer.TryGetPropertyValue("FavoriteColors", out favoriteColorsValue); - EdmEnumObjectCollection favoriteColors = favoriteColorsValue as EdmEnumObjectCollection; + // Verify there is a collection of complex type dynamic property in OpenEntityType + object addressesValue; + customer.TryGetPropertyValue("Addresses", out addressesValue); - Assert.NotNull(favoriteColorsValue); - Assert.NotNull(favoriteColors); - Assert.Equal(typeof(EdmEnumObject), favoriteColors[0].GetType()); + Assert.NotNull(addressesValue); - // Verify there is an enum type dynamic property in OpenEntityType - object favoriteColorValue; - customer.TryGetPropertyValue("FavoriteColor", out favoriteColorValue); + // Verify there is a complex type dynamic property in OpenEntityType + object addressValue; + customer.TryGetPropertyValue("Address", out addressValue); - Assert.NotNull(favoriteColorValue); - Assert.Equal("Red", ((EdmEnumObject)favoriteColorValue).Value); + Type addressType; + customer.TryGetPropertyType("Address", out addressType); - Type favoriteColorType; - customer.TryGetPropertyType("FavoriteColor", out favoriteColorType); + Assert.NotNull(addressValue); + Assert.Equal(typeof(EdmComplexObject), addressType); - Assert.Equal(typeof(EdmEnumObject), favoriteColorType); + // Verify there is a collection of enum type dynamic property in OpenEntityType + object favoriteColorsValue; + customer.TryGetPropertyValue("FavoriteColors", out favoriteColorsValue); + EdmEnumObjectCollection favoriteColors = favoriteColorsValue as EdmEnumObjectCollection; - // Verify there is a string dynamic property in OpenComplexType - EdmComplexObject address = addressValue as EdmComplexObject; - object cityValue; - address.TryGetPropertyValue("City", out cityValue); - Type cityType; - address.TryGetPropertyType("City", out cityType); + Assert.NotNull(favoriteColorsValue); + Assert.NotNull(favoriteColors); + Assert.Equal(typeof(EdmEnumObject), favoriteColors[0].GetType()); - Assert.NotNull(cityValue); - Assert.Equal(typeof(String), cityType); - Assert.Equal("City 6", cityValue); // It reads as ODataUntypedValue, and the RawValue is the string with the "" + // Verify there is an enum type dynamic property in OpenEntityType + object favoriteColorValue; + customer.TryGetPropertyValue("FavoriteColor", out favoriteColorValue); - return Ok(customer); - } + Assert.NotNull(favoriteColorValue); + Assert.Equal("Red", ((EdmEnumObject)favoriteColorValue).Value); - private static EdmEntityObjectCollection GetCustomers() - { - if (_untypedSimpleOpenCustormers != null) - { - return _untypedSimpleOpenCustormers; - } + Type favoriteColorType; + customer.TryGetPropertyType("FavoriteColor", out favoriteColorType); + + Assert.Equal(typeof(EdmEnumObject), favoriteColorType); + + // Verify there is a string dynamic property in OpenComplexType + EdmComplexObject address = addressValue as EdmComplexObject; + object cityValue; + address.TryGetPropertyValue("City", out cityValue); + Type cityType; + address.TryGetPropertyType("City", out cityType); - IEdmModel edmModel = OpenEntityTypeTests.GetUntypedEdmModel(); - IEdmEntityType customerType = edmModel.SchemaElements.OfType().First(c => c.Name == "UntypedSimpleOpenCustomer"); - EdmEntityObject customer = new EdmEntityObject(customerType); - customer.TrySetPropertyValue("CustomerId", 1); - - //Add Numbers primitive collection property - customer.TrySetPropertyValue("DeclaredNumbers", new[] { 1, 2 }); - - //Add Color, Colors enum(collection) property - IEdmEnumType colorType = edmModel.SchemaElements.OfType().First(c => c.Name == "Color"); - EdmEnumObject color = new EdmEnumObject(colorType, "Red"); - EdmEnumObject color2 = new EdmEnumObject(colorType, "0"); - EdmEnumObject color3 = new EdmEnumObject(colorType, "Red"); - customer.TrySetPropertyValue("Color", color); - - List colorList = new List(); - colorList.Add(color); - colorList.Add(color2); - colorList.Add(color3); - IEdmCollectionTypeReference enumCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(colorType.ToEdmTypeReference(false))); - EdmEnumObjectCollection colors = new EdmEnumObjectCollection(enumCollectionType, colorList); - customer.TrySetPropertyValue("Colors", colors); - customer.TrySetPropertyValue("DeclaredColors", colors); - - //Add Addresses complex(collection) property - EdmComplexType addressType = - edmModel.SchemaElements.OfType().First(c => c.Name == "Address") as EdmComplexType; - EdmComplexObject address = new EdmComplexObject(addressType); - address.TrySetPropertyValue("Street", "No1"); - EdmComplexObject address2 = new EdmComplexObject(addressType); - address2.TrySetPropertyValue("Street", "No2"); - - List addressList = new List(); - addressList.Add(address); - addressList.Add(address2); - IEdmCollectionTypeReference complexCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(addressType.ToEdmTypeReference(false))); - EdmComplexObjectCollection addresses = new EdmComplexObjectCollection(complexCollectionType, addressList); - customer.TrySetPropertyValue("DeclaredAddresses", addresses); - - EdmEntityObjectCollection customers = new EdmEntityObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(customerType.ToEdmTypeReference(false)))); - customers.Add(customer); - _untypedSimpleOpenCustormers = customers; + Assert.NotNull(cityValue); + Assert.Equal(typeof(String), cityType); + Assert.Equal("City 6", cityValue); // It reads as ODataUntypedValue, and the RawValue is the string with the "" + + return Ok(customer); + } + + private static EdmEntityObjectCollection GetCustomers() + { + if (_untypedSimpleOpenCustormers != null) + { return _untypedSimpleOpenCustormers; } + + IEdmModel edmModel = OpenEntityTypeTests.GetUntypedEdmModel(); + IEdmEntityType customerType = edmModel.SchemaElements.OfType().First(c => c.Name == "UntypedSimpleOpenCustomer"); + EdmEntityObject customer = new EdmEntityObject(customerType); + customer.TrySetPropertyValue("CustomerId", 1); + + //Add Numbers primitive collection property + customer.TrySetPropertyValue("DeclaredNumbers", new[] { 1, 2 }); + + //Add Color, Colors enum(collection) property + IEdmEnumType colorType = edmModel.SchemaElements.OfType().First(c => c.Name == "Color"); + EdmEnumObject color = new EdmEnumObject(colorType, "Red"); + EdmEnumObject color2 = new EdmEnumObject(colorType, "0"); + EdmEnumObject color3 = new EdmEnumObject(colorType, "Red"); + customer.TrySetPropertyValue("Color", color); + + List colorList = new List(); + colorList.Add(color); + colorList.Add(color2); + colorList.Add(color3); + IEdmCollectionTypeReference enumCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(colorType.ToEdmTypeReference(false))); + EdmEnumObjectCollection colors = new EdmEnumObjectCollection(enumCollectionType, colorList); + customer.TrySetPropertyValue("Colors", colors); + customer.TrySetPropertyValue("DeclaredColors", colors); + + //Add Addresses complex(collection) property + EdmComplexType addressType = + edmModel.SchemaElements.OfType().First(c => c.Name == "Address") as EdmComplexType; + EdmComplexObject address = new EdmComplexObject(addressType); + address.TrySetPropertyValue("Street", "No1"); + EdmComplexObject address2 = new EdmComplexObject(addressType); + address2.TrySetPropertyValue("Street", "No2"); + + List addressList = new List(); + addressList.Add(address); + addressList.Add(address2); + IEdmCollectionTypeReference complexCollectionType = new EdmCollectionTypeReference(new EdmCollectionType(addressType.ToEdmTypeReference(false))); + EdmComplexObjectCollection addresses = new EdmComplexObjectCollection(complexCollectionType, addressList); + customer.TrySetPropertyValue("DeclaredAddresses", addresses); + + EdmEntityObjectCollection customers = new EdmEntityObjectCollection(new EdmCollectionTypeReference(new EdmCollectionType(customerType.ToEdmTypeReference(false)))); + customers.Add(customer); + _untypedSimpleOpenCustormers = customers; + return _untypedSimpleOpenCustormers; } } diff --git a/test/Microsoft.AspNetCore.OData.Tests/Scenarios/UnicodeCharactersTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Scenarios/UnicodeCharactersTests.cs index 0201f959f..edcbd5f7c 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Scenarios/UnicodeCharactersTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Scenarios/UnicodeCharactersTests.cs @@ -18,98 +18,97 @@ using Microsoft.OData.ModelBuilder; using Xunit; -namespace Microsoft.AspNetCore.OData.Tests.Scenarios +namespace Microsoft.AspNetCore.OData.Tests.Scenarios; + +public class UnicodeCharactersTest { - public class UnicodeCharactersTest + [Fact] + public async Task PostEntity_WithUnicodeCharactersInKey() { - [Fact] - public async Task PostEntity_WithUnicodeCharactersInKey() - { - // Arrange - const string payload = "{" + - "\"LogonName\":\"Ärne Bjørn\"," + - "\"Email\":\"ärnebjørn@test.com\"" + - "}"; - - const string uri = "http://localhost/odata/UnicodeCharUsers"; - const string expectedLocation = "http://localhost/odata/UnicodeCharUsers('%C3%84rne%20Bj%C3%B8rn')"; - - HttpClient client = GetClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri); - request.Content = new StringContentWithLength(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.Equal(expectedLocation, response.Headers.Location.AbsoluteUri); - } - - [Fact] - public async Task PostEntity_WithUnicodeCharactersInKey_PreferNoContent() - { - // Arrange - const string payload = "{" + - "\"LogonName\":\"Ärne Bjørn\"," + - "\"Email\":\"ärnebjørn@test.com\"" + - "}"; - - const string uri = "http://localhost/odata/UnicodeCharUsers"; - const string preferNoContent = "return=minimal"; - const string expectedLocation = "http://localhost/odata/UnicodeCharUsers('%C3%84rne%20Bj%C3%B8rn')"; - - HttpClient client = GetClient(); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri); - request.Headers.TryAddWithoutValidation("Prefer", preferNoContent); - - request.Content = new StringContentWithLength(payload); - request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); - - // Act - HttpResponseMessage response = await client.SendAsync(request); - - // Assert - Assert.True(response.IsSuccessStatusCode); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - Assert.Equal(expectedLocation, response.Headers.Location.AbsoluteUri); - IEnumerable values; - Assert.True(response.Headers.TryGetValues("OData-EntityId", out values)); - Assert.Single(values); - Assert.Equal(expectedLocation, values.First()); - } - - private static HttpClient GetClient() - { - ODataConventionModelBuilder modelBuilder = new ODataConventionModelBuilder(); - modelBuilder.EntitySet("UnicodeCharUsers"); - - var controllers = new[] { typeof(UnicodeCharUsersController) }; - var server = TestServerUtils.Create(opt => - { - opt.Select().OrderBy().Filter().Expand().Count().SetMaxTop(null); - opt.AddRouteComponents("odata", modelBuilder.GetEdmModel()); - }, - controllers); - - return server.CreateClient(); - } + // Arrange + const string payload = "{" + + "\"LogonName\":\"Ärne Bjørn\"," + + "\"Email\":\"ärnebjørn@test.com\"" + + "}"; + + const string uri = "http://localhost/odata/UnicodeCharUsers"; + const string expectedLocation = "http://localhost/odata/UnicodeCharUsers('%C3%84rne%20Bj%C3%B8rn')"; + + HttpClient client = GetClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri); + request.Content = new StringContentWithLength(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.Equal(expectedLocation, response.Headers.Location.AbsoluteUri); + } + + [Fact] + public async Task PostEntity_WithUnicodeCharactersInKey_PreferNoContent() + { + // Arrange + const string payload = "{" + + "\"LogonName\":\"Ärne Bjørn\"," + + "\"Email\":\"ärnebjørn@test.com\"" + + "}"; + + const string uri = "http://localhost/odata/UnicodeCharUsers"; + const string preferNoContent = "return=minimal"; + const string expectedLocation = "http://localhost/odata/UnicodeCharUsers('%C3%84rne%20Bj%C3%B8rn')"; + + HttpClient client = GetClient(); + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri); + request.Headers.TryAddWithoutValidation("Prefer", preferNoContent); + + request.Content = new StringContentWithLength(payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + Assert.Equal(expectedLocation, response.Headers.Location.AbsoluteUri); + IEnumerable values; + Assert.True(response.Headers.TryGetValues("OData-EntityId", out values)); + Assert.Single(values); + Assert.Equal(expectedLocation, values.First()); } - public class UnicodeCharUsersController : ODataController + private static HttpClient GetClient() { - public IActionResult Post([FromBody] UnicodeCharUser user) + ODataConventionModelBuilder modelBuilder = new ODataConventionModelBuilder(); + modelBuilder.EntitySet("UnicodeCharUsers"); + + var controllers = new[] { typeof(UnicodeCharUsersController) }; + var server = TestServerUtils.Create(opt => { - return Created(user); - } + opt.Select().OrderBy().Filter().Expand().Count().SetMaxTop(null); + opt.AddRouteComponents("odata", modelBuilder.GetEdmModel()); + }, + controllers); + + return server.CreateClient(); } +} - public class UnicodeCharUser +public class UnicodeCharUsersController : ODataController +{ + public IActionResult Post([FromBody] UnicodeCharUser user) { - [Key] - public string LogonName { get; set; } - public string Email { get; set; } + return Created(user); } } + +public class UnicodeCharUser +{ + [Key] + public string LogonName { get; set; } + public string Email { get; set; } +}